主题
架构设计
@ggn/draw 是领域无关的绘制内核。它不认识 UML、系统地图或任务,只负责把文档、注册表、 主题和布局策略转换成可测试的几何与场景。
目标
| 目标 | 可验证结果 |
|---|---|
| 每个中间产物可获取 | parse / bind / measure / place / attach / route / build 都有独立入口 |
| 同一问题只诊断一次 | 类型、props、marker 在 bind 收口;layout / build 不重复报 |
| 换主题不重算几何 | createPipeline 只重跑 build |
| 增量稳定 | 中间插入一个兄弟只产生一条 create |
| 扩展是值 | registry、engine、router、measurer、host 都通过参数注入 |
| 主入口 headless | @ggn/draw 不出现 DOM API;DOM 与 Canvas 度量走独立子路径 |
非目标
- 不内置 UML、系统地图、任务等语义;
- 不做 Canvas / WebGL / PNG 输出;
- 不做编辑器、手势和选择器;
- 不内置 ELK;
- 不把主题写进文档。
六段管线
正在渲染图表…
主题与文档互不依赖;图中按理解顺序排列。
| 阶段 | 输入 → 输出 | 读 registry |
|---|---|---|
| parse | unknown → DrawDocument | 否 |
| bind | DrawDocument + registry → BoundDocument | 是,且只在这里读 |
| theme | ThemeInput → ResolvedTheme | 否 |
| measure | BoundDocument → Measured | 否 |
| place | Measured → Placement | 否 |
| attach | Placement → Attached | 否 |
| route | Attached → LayoutResult | 否 |
| build | BoundDocument + LayoutResult + theme → Scene | 读 render |
| host | Scene / patch → 介质 | 否 |
四层数据
| 层 | 负责 | 不允许 |
|---|---|---|
DrawDocument | 意图、引用、样式名、布局意图 | 最终坐标(pins 仅 explicit)、SVG 属性 |
BoundDocument | 生效类型、归一化 props、容器标记、注册项快照 | 几何、render、主题 |
LayoutResult | 盒子、端口落点、路由、标签落点 | 画法、颜色、宿主 API |
Scene | 绝对几何、已解析样式、稳定 key、ref | 领域语义、重新布局 |
模块边界
text
contract ──→ bind ──→ layout/{measure,place,attach,route}
│ │
registry ────────┘ │
▼
theme ─────────────────────────→ build ──→ scene ──→ host-svg / host-dom
▲
pipeline ──→ bind + layout + build ┘
handle ──→ pipeline + diff + host规则:
contract不依赖注册表、主题或几何。registry不依赖 theme、layout 或 build。bind是 registry 与 layout / build 之间唯一的桥。- layout 拿不到 render;build 是唯一同时使用几何与 render 的模块。
- scene 不依赖 document、layout 或 host。
- host 不依赖 document、build 或 pipeline。
- pipeline / handle 是组合根,不能被核心模块反向依赖。
不变量
- L1:只有 layout 把引用解析成几何;pins 是唯一显式坐标。
- L2:render 只拿自己的 box、props、label、anchors 和 style。
- L3:host 只读场景坐标;相机由显式
Scene.camera或bounds + padding + overflow得到。 - L4:bind 之后类型与 props 已定,下游不再判断“是否已注册”。
- L5:DomHost.apply 先校验再提交,失败不改变介质。
- D1:同输入、同注册表版本、同主题、同度量器 fingerprint、同引擎与路由 → 相同结果。
关键设计决策
bind 独立成段
未知类型、props schema、children 容器判断和 marker 是否存在,都集中在 bind。这样 layout 不需要 registry.metrics() 这种收窄视图,也不会和 build 各报一次同一问题。
layout 拆成四个原子
text
measureDocument → placeDocument → attachEndpoints → routeEdgeslayoutDocument 只是组合。标签尺寸在 measure 完成;端口槽位在 attach 完成;route 只决定 连线路由和边标签落点。
端口约束
fixed:兼容 v1,side + offset 固定;side:side 固定,同侧槽位自动分配;free:先按对端方向选 side,再分配槽位。
场景可回指文档
Scene 零件可带 SceneRef。宿主做命中、选择、拖拽时直接读 ref,不解析 key 字符串。 hitTest 使用场景几何从上到下返回 ref。
注册表不可变
createRegistry() 默认带内置原语;with(entries) 返回新版本。同名冲突抛 RegistryConflictError,{ override: true } 才能覆盖。实例即缓存版本。
Pipeline 缓存
Pipeline 以阶段产物为缓存边界:
- 只换 tokens / overlay → build;
- 只换 router → route;
- 只换 measurer fingerprint → measure 及之后;
- 全部相同 → 整帧命中。
当前迁移边界
以下能力保留为下一阶段:
- 平面引擎自动包装成层次引擎的
hierarchical(); - Diagnostic 的
subject从兼容字符串完全迁移到对象模型; - 异步 Standard Schema。
这些边界不影响当前公开契约;新增字段会在兼容窗口内逐步加入。
验收
测试覆盖:
- bind 未知类型和 props 诊断恰好一次;
- 四个 layout 原子可独立调用;
- 同侧端口槽位与边书写顺序无关;
- 标签由 layout 产出;
- Scene 中间插入只产生 create,重排产生 move;
hitTest返回文档 id;- host apply 失败时介质不变;
- Pipeline 在 tokens / overlay / router 变化时只重跑对应阶段。