主题
五段管线
画一张图要做五件事。每一件都是「一段数据进、一段数据出」,所以你可以只用其中一件, 也可以全部交给 createDrawHandle 一次做完。
| 顺序 | 这一步交付什么 | 形态 |
|---|---|---|
① parseDocument | 有哪些元素、谁指向谁 | DrawDocument(没有坐标) |
② resolveTheme | 调色板与排版量 | ResolvedTheme(具体值,不是 token 名) |
③ layoutDocument | 元素多大、放哪、线怎么绕 | LayoutResult(绝对几何) |
④ buildScene | 每个元素由哪些零件组成 | Scene(rect / ellipse / path / text / group) |
⑤ renderToSvg | 画到哪 | SVG 字符串;DOM 侧是 createDomHost |
一句话记住产出:文档 → 调色板 / 排版量 → 绝对几何 → 场景树 → 介质。
前两步互不依赖(文档和主题谁先解析都行),所以代码里 resolveTheme 会先跑; 上面这张表按「读者手里先有文档」来排,后面所有页面都用这个顺序。
自己走一遍
左边改文档,右边出图;点下面每一段的标签,看那一段产出什么——这一步的产出就是下一步的输入。
DrawDocument(没有坐标)
渲染结果
- 契约
- 2 节点 · 1 边
- 几何
- 2 盒子 · 1 路由
- 场景
- 9 个零件
{
"root": {
"kind": "group",
"key": "@root",
"children": [
{
"kind": "path",
"key": "e1",
"d": "M 120 48 L 120 112",
"style": {
"fill": "none",
"stroke": "#64748b",
"strokeWidth": 1.5,
"linecap": "round",
"linejoin": "round",
"color": "#64748b",
"fontFamily": "system-ui, sans-serif",
"fontSize": 16,
"lineHeight": 1.35
}
},
{
"kind": "path",
"key": "e1/marker-end",
"d": "M 116 104 L 120 112 L 124 104",
"style": {
"fill": "none",
"stroke": "#64748b",
"strokeWidth": 1.5,
"color": "#64748b"
}
},
{
"kind": "rect",
"key": "order",
"x": 60,
"y": 0,
"width": 120,
"height": 48,
"rx": 8,
"style": {
"fill": {
"type": "linear",
"x1": 0,
"y1": 0,
"x2": 0,
"y2": 1,
"stops": [
{
"offset": 0,
"color": "#ffffff"
},
{
"offset": 1,
"color": "#f8fafc"
}
]
},
"stroke": "#cbd5e1",
"strokeWidth": 1,
"shadow": true,
"elevation": "soft",
"color": "#0f172a",
"fontFamily": "system-ui, sans-serif",
"fontSize": 16,
"lineHeight": 1.35
}
},
{
"kind": "path",
"key": "order/sheen",
"d": "M 68 0.5 L 172 0.5",
"style": {
"fill": "none",
"stroke": "#ffffff",
"strokeWidth": 1,
"opacity": 0.08,
"linecap": "round"
}
},
{
"kind": "text",
"key": "order/label",
"x": 120,
"y": 29.6,
"lines": [
"订单服务"
],
"anchor": "middle",
"style": {
"fill": "#0f172a",
"stroke": "none",
"strokeWidth": 1,
"shadow": true,
"elevation": "soft",
"color": "#0f172a",
"fontFamily": "system-ui, sans-serif",
"fontSize": 16,
"lineHeight": 1.35,
"fontWeight": 500,
"letterSpacing": -0.1
}
},
{
"kind": "rect",
"key": "stock",
"x": 60,
"y": 112,
"width": 120,
"height": 48,
"rx": 8,
"style": {
"fill": {
"type": "linear",
"x1": 0,
"y1": 0,
"x2": 0,
"y2": 1,
"stops": [
{
"offset": 0,
"color": "#ffffff"
},
{
"offset": 1,
"color": "#f8fafc"
}
]
},
"stroke": "#cbd5e1",
"strokeWidth": 1,
"shadow": true,
"elevation": "soft",
"color": "#0f172a",
"fontFamily": "system-ui, sans-serif",
"fontSize": 16,
"lineHeight": 1.35
}
},
{
"kind": "path",
"key": "stock/sheen",
"d": "M 68 112.5 L 172 112.5",
"style": {
"fill": "none",
"stroke": "#ffffff",
"strokeWidth": 1,
"opacity": 0.08,
"linecap": "round"
}
},
{
"kind": "text",
"key": "stock/label",
"x": 120,
"y": 141.6,
"lines": [
"库存服务"
],
"anchor": "middle",
"style": {
"fill": "#0f172a",
"stroke": "none",
"strokeWidth": 1,
"shadow": true,
"elevation": "soft",
"color": "#0f172a",
"fontFamily": "system-ui, sans-serif",
"fontSize": 16,
"lineHeight": 1.35,
"fontWeight": 500,
"letterSpacing": -0.1
}
}
]
},
"bounds": {
"x": 52,
"y": -8,
"width": 136,
"height": 176
}
} 这一段就是下一步的输入——管线里没有隐藏状态; contract → theme → layout → scene → svg
零警告:五段全部跑通,没有任何回落
为什么切五段
一张图里有三类信息:有哪些元素(结构)、摆在哪(几何)、长什么样(画法)。 它们改动的频率不一样、依赖方向也不一样,混在一起就没法各自替换:
- 换调色板不该动几何,换布局算法不该动文档;
- 服务端只想要一棵场景树,不该被迫装一个 DOM。
所以切成五段,每段只认自己那一类信息——这也是主页那三条硬判据(正交 / 原子 / 可组合)的落地方式。
时序耦合(写清楚才不会踩)
| 耦合 | 方向 | 理由 |
|---|---|---|
| registry → layout | 先有注册表,再 layout | 度量要 measure,锚点要 anchors |
| theme.typography → layout | 先有排版量,再 layout | 行高 / 字号参与度量 |
| registry schema → layout 入口 | props 校验推迟到 layout 开始处 | contract 不认识注册表,而度量需要合法 props |
| theme.tokens → build | 排版量已在 layout 用掉,调色板留给 build | 两半各归其位;未知 token 的诊断只能在这里产生 |
三条坐标不变量
- L1:
DrawDocument里的layout.pins是唯一的显式坐标,只被 explicit 引擎消费。 - L2:形状只往自己的 key 前缀下写零件,跨元素几何泄漏不出去。
- L3:
renderToSvg/createDomHost只把Scene翻成介质,Scene.bounds决定viewBox。
容器即节点
没有 groups[],只有 children:一个有 children 的节点就是容器。 Component 因此可以既有 port(挂在容器边框上)又含 part(在容器内的子节点),不需要第三种元素类型。
绘制顺序由结构推导
容器背景(树先序)→ 边(含端点 marker)→ 叶子节点 → 边标签。host 只按 Scene 顺序输出, 所以「线在节点下面、标签在节点上面」这件事不需要 z-index。
接着往下读
五段管线是骨架。每一段内部还有自己的机制,左侧「机制」从 文档与校验开始,依次讲节点注册、端口、主题、布局、场景和宿主。