Imported from LogicAurora/Mindrizzle (
AGENTS.md). Install upstream withnpx skills add LogicAurora/Mindrizzle. Copyright stays with the author.
AGENTS.md
本文件面向在此仓库工作的编码代理与新的贡献者,只写本仓库特有的约定,通用最佳实践不重复。
项目概览
Mindrizzle 是把笔记放上无限画布的桌面应用:内容以「块」的形式自由摆放,块内可嵌富文本或代码块,笔记以 .mdrf 归档存于本地。
- 前端:Vue 3 + TypeScript + Vite + Vuetify,包管理用 bun
- 编辑器:ProseKit / ProseMirror / KaTeX / highlight.js
- 桌面容器:Tauri 2;文件层:Rust(tar + flate2 + quick-xml)
常用命令
bun install
bun run dev # 只跑前端(浏览器 http://localhost:1420)
bun run tauri dev # 起桌面应用
bunx tsc --noEmit # 类型检查
bunx vite build # 构建验证
bun run build # Vue/TypeScript 检查与 Vite 构建
cd src-tauri && cargo check # Rust 侧编译检查
cd src-tauri && cargo test # 含模块文档里的 no_run doctest
bun run build是完整前端验证入口,先运行vue-tsc --noEmit再执行 Vite 构建。typescript保持在 6.0.x;当前vue-tsc通过typescript/lib/tsc加载编译器,TypeScript 7 不导出该内部路径。升级 TypeScript 前需先确认vue-tsc已兼容。
注释规范(最高优先级)
注释只回答「为什么」,不回答「做什么」。代码本身能说明的地方不得有注释。
- 只在必要时写注释,内容须是业务背景、第三方库行为或非常规处理。
- 禁止复读机注释:
getUser上方不得出现「获取用户」。 - 禁止在简单赋值/调用后加尾随注释(如
const max = 100 // 最大值),原因写成上一行独立注释。 - 复杂正则、位运算、坐标换算公式上方必须有一行中文说明意图。
- 禁止
// ====================之类装饰性分隔线;// #region/// #endregion是编辑器折叠标记,保留。 - 多行 why 注释要压到一行;只有真正复杂的坐标系/时序陷阱才允许两三行。
- 删代码时同时删除其专属注释;改行为时同步更新相关 why 注释——注释说谎比没有注释更糟。
示例:
// 浏览器 BCR 是否含 zoom 不一致,所以用实测比例反推
domScale: blockRect.width / blockLayout.w
// 邻块会盖住块边缘内侧的缩放手柄,所以选中块 z 取 Z_LAYER.selectedBlock 提层
const SELECTED_Z_BASE = Z_LAYER.selectedBlock
代码规范
- 函数纯逻辑 ≤ 25 行,超出即拆;只服务当前父函数的子函数就近放置。
- 嵌套
if/for/switch≤ 2 层,用早返回或continue拍平。 - 函数参数 ≤ 3 个,超出封装为对象。
- 命名:布尔以
is/has/can/should开头;函数用「动词 + 名词」;变量禁用单字母(i/j/e除外)与data/res/result之类占位词(作用域 ≤ 5 行除外)。 - 除
0/1/-1/''外的魔法数字必须提为常量,常量名用大写下划线。 - 错误处理:只在调用边界(IPC 命令入口、事件入口、定时任务触发点)统一捕获;内部工具函数直接
throw原生错误,不封装Result<T>/Either。 - 资源配对:内存、文件句柄、监听器、rAF 与定时器必须在同一作用域内释放(
onUnmounted里成对 remove/cancel)。 - 安全:所有外部输入(IPC 参数、文件名、外部
.mdrf)在入口校验;Rust 侧resolve_mdrf_file_name与mdr_file_tar的路径穿越检查是既有防线,不要绕过。 - 日志:关键业务分支与异常捕获处用既有设施(Rust
log宏 / 前端@tauri-apps/plugin-log)并带关键上下文;浏览器调试环境无 IPC,上报前用isTauri()守卫。 - 导入:分组为 标准库 → 第三方 → 项目内,组间一个空行;禁止通配符导入;改完立即删除未引用导入。
目录与职责
| 路径 | 职责 |
|---|---|
src/Controls/Canvas/Core/ |
画布本体:MdrCanvas.vue、块壳、缩放手柄、浮层几何与 z-index |
src/Controls/Canvas/Core/MdrCanvas.vue |
画布核心:平移/缩放/框选/拖拽/联结/贴合遮罩/自动布局 |
src/Controls/Canvas/Core/ResizeBox.vue |
单块容器与缩放手柄(手柄 Teleport 到 .canvas 顶层) |
src/Controls/Canvas/Core/MdrCanvasComponentBase.vue |
块通用壳:几何、拖拽栏、设置插槽、controller(见 src/docs/components/) |
src/Controls/Canvas/Core/canvasComponentControllers.ts |
块 ↔ 画布控制器契约(移动 / 改尺寸 / 平移 / 缩放 / 视图) |
src/Controls/Canvas/Core/blockFloatGeometry.ts |
拖拽栏、设置栏、遮罩几何与浮层 z 的唯一来源 |
src/Controls/Canvas/Core/zIndex.ts |
叠加层 z-index 的唯一事实来源 |
src/Controls/Canvas/Registry/ |
可注册组件的唯一注册表(canvasComponents.ts)与尺寸约束 |
src/Controls/Canvas/Components/ |
可注册的块组件:富文本、代码块、测试台 |
src/Controls/Canvas/Components/RichEditor/ |
富文本编辑器、block-handle 扩展、插入组件节点视图 |
src/Controls/Canvas/Components/RichEditor/extensions/ |
ProseKit 扩展与命中/坐标工具(坐标工具见 blockHandleUtils.ts) |
src/utils/canvasCoords.ts |
content ↔ 视口坐标换算的唯一来源 |
src/utils/componentDataStore.ts |
组件私有文件 mdrfcd:// 读写唯一入口(归档内 ComponentsData/<uuid>/) |
src/Views/Editor.vue |
路由页面与工具栏交互(滚轮切工具、选区确认 overlay) |
src-tauri/src/mdr_file_*.rs |
.mdrf 归档读写、缓存目录、IPC 命令 |
硬约束
坐标系(最容易翻车处)
- 存在三套坐标:
content(块存储坐标)、layout(.canvas内 BCR 坐标,Chrome 下不含 zoom)、viewport(视觉坐标)。 - 换算只走
utils/canvasCoords.ts与blockHandleUtils的layoutToViewportX/Y/Size、viewportToLayout,禁止手写公式。 - 视口 → content 一律
screenToContent;布局 → 视口一律layoutToViewport*。 clientX/Y与getBoundingClientRect()不可直接比较(BCR 是布局坐标、指针是视觉坐标):命中判定必须把矩形经layoutToViewportX/Y换算到视觉空间(barAt、滚动条轨道翻页均已按此修)。凡是新增「按矩形判定指针」的代码,验证至少覆盖 zoom=0.5 一档。- 禁用
posAtCoords/coordsAtPos/posFromCaret(内部拿 BCR 与鼠标坐标直接比较,zoom≠1 必错)。落点解析用elementFromPoint→.ProseMirror直系子块 →posAtDOM。 getCanvasScale的入参既可为 view 也可为元素;只认 view 会让传元素的调用静默拿到 1。- 画布 pan/zoom 落定后必须
invalidateCanvasScaleCache()。
事件与监听
- 与指针拖拽相关的 window 监听必须用捕获阶段(
addEventListener(..., true)),移除时同参。原因:useHoverState会在 window 捕获阶段stopPropagation。 - 伪指针事件必须过滤:
if (!e.isTrusted) return;-9999坐标是hideAllBlockHandles的清 hover 信号。 - 高频指针回调禁止
document.elementFromPoint(每块 × 每事件的强制布局),改用event.target+dom.contains。 - 高频路径用 rAF 合并,既有约定(
panRafId/customDragRafId/geometryRafId/scheduleScrollMetrics)勿改成逐事件处理。
画布与叠层
.canvas用 CSSzoom缩放;禁止给块加translate3d(位图纹理放大会让文字发虚),用普通translate。.canvas-container必须overflow: clip(保留overflow: hidden作旧引擎回退):hidden下 ProseMirror 的scrollIntoView会改写scrollLeft/Top,导致点阵露白与坐标偏移。- 叠加层 z-index 全部登记在
src/Controls/Canvas/Core/zIndex.ts;TS 常量与 CSS 硬编码值必须一致,改一处要同步另一处。 - block-handle popup 的
Root必须 Teleport 到.canvas(不是.canvas-container),否则 store context 会丢、reference 与定位不处于同一布局坐标空间。 - 贴合遮罩、圆角、曲别针候选都是
layout推导的 O(N²) 结果,统一由recomputeMasks()刷新;交互期间冻结,收尾再刷新。增删块、布局落定后不要忘记调用。 - 浮层渲染走小集合(
floatingOwnerId/sideSettingsItem/selectedOutlineItems),禁止整表v-for+ 逐块v-if。 - 视口外的块会被虚拟化(
MdrCanvas的virtualSnapshots/reconcileVirtualBlocks):实例被销毁、只留静态快照,故componentRefs[id]只保证「可见 ∪ 选中 ∪ 拖拽/resize/popup ∪ 焦点所在」的块存在——跨层批量操作不得假设所有块都有活实例。 - 命中测试禁止逐 mousemove 读 DOM 矩形,用
screenRectFactory()按公式计算;几何常量必须与ResizeBox.vue的HANDLE_POS/HANDLE_SIZE/ 手柄样式同步。 - 命中测试先取
selectedBlockItems()(选中块小集合),不要遍历state.items再逐个过滤——后者让每次 pointermove 随块数线性劣化。 .drag-wrapper只允许contain: layout style。禁止加paint(会裁掉 RichTextEditor 用负边距外扩 64/80px 的 gutter);禁止加content-visibility(视口外块被跳过布局会让块内 RO 报 0,onAutoHeight随即把块高压到 minH)。- rAF 热路径内同一目标块的测量必须走帧号缓存(
richTextTargetForFrame/isTargetVisibleForFrame,帧号取 rAF 回调 timestamp、同帧一致;帧号 0 表示帧外直测,勿缓存)。
ProseMirror / ProseKit
editor.view只能在editor.mount()之后访问;在 setup 或watch(..., { immediate: true })里访问会抛错并中断整棵组件树(表现为「所有添加块失效」)。editor.mount(el)把 ProseMirror 挂在.editor-mount同一元素上,因此.editor-mount :deep(.ProseMirror)这类后代选择器恒不命中,统一写.editor-mount.ProseMirror。- block-handle 的 store 必须从 positioner 自身派发
aria-ui:context-request解析(positioner 已 Teleport 出 wrapper)。 .forced-open/.popup-keep只写display/visibility的!important,不要一起写opacity/scale,否则会盖掉@starting-style入场动画。- 缩放环境下的 hover 由
useHoverState自解析并经store.hoverState.set()独占,同时用合成pointermove让 ProseKit 的定位自洽;两者缺一都会出现「popup 不出现」或「拖拽源为空」。
交互语义
- 选中与聚焦只由点击/框选决定,悬停只切换浮层归属(
hoveredUuid),不要随手改选中或抢焦点。 - 移动端锁水平(
x恒 0、宽贴画布)并纵向堆叠;桌面端按视口宽瀑布流平铺。改动布局后同步arranged与移动端堆叠平移。 - 「拖组件入富文本块」时,块完全可见则只滚块内内容,块未完全显示才平移画布(commit 767ea68 的既有规则,不要改成画布优先)。
useBlockDrag的段落拖拽路径不要改:块内滚动与画布平移并存是既定行为。- 拖拽/框选松手后浏览器会补发
click,用dragJustFinished/justFinishedSelection吞掉,避免拖拽结果被点击语义覆盖。
构建与调试环境
- bun 的依赖经
~/.bun/install/cache符号链接暴露,realpath 落在 workspace 外;vite.config.ts的server.fs.allow必须包含该目录(并带上'.')。 - 依赖树里
prosemirror-view曾被解析出两份(1.42.1/1.42.3),prosekit 的pm/view与pm/state各用一份,会让EditorView类型互不兼容;package.json的overrides已锁死 1.42.3,勿删。 bun.lock一旦与package.json不同步,bun install会重解析整棵树(曾一次改动 261 条解析),故改依赖后要确认 diff 范围。- 路由用
createMemoryHistory,浏览器里改 URL 不会切页;验证页面需点击导航。 - 编辑器链路(ProseKit + highlight.js + KaTeX)保持懒加载:新增路由沿用
() => import(),新增重型依赖同步canvasComponents.ts的defineAsyncComponent。 optimizeDeps.entries已覆盖index.html与src/**/*.{vue,ts},勿删;否则 dev 首次进/editor会触发依赖重优化整页 reload。- 模板里自动导入的 Vuetify 组件扫不到,dev 首次进入某懒加载页面仍会有一次 optimize + reload,属正常现象。
验证清单
bunx tsc --noEmit:应零报错。- 改动画布/编辑器交互后,用内置浏览器实操一遍:拖块、拖组件入富文本、缩放手柄、框选、右键平移、切换缩放(0.5 / 1 / 2 / 3)、切移动端模拟。
- 改动 Rust 侧跑
cargo check;改动打包逻辑跑cargo test(mdr_file_tar/mdr_file_cache的模块文档里有no_rundoctest,精简文档时不要删掉示例代码块)。 - 浏览器自动化注意:
page.mouse在此内置浏览器里坐标不可靠(事件根本送不进页面),用elementFromPoint/dispatchEvent合成事件;onCustomDragMove有isTrusted过滤,块拖拽无法用合成事件验证(框选/缩放/选中可以)。更关键:该页面里 rAF 与 ResizeObserver 完全不派发(visibilityState仍为visible),凡依赖 rAF/RO 的行为(滚动条补测、autoHeight 报高、Vue transition)都观测不到,别把它当成 bug——只能用同步路径或getBoundingClientRect强制布局验证。 - 缩放相关验证需等 vite 依赖优化完成后再交互,否则整页 reload 会丢掉状态。
- 如果有对于画布、编辑器的逻辑修改,一定要同步
src/docs/flow下的流程图和对应验证日期
