Skip to content

五段管线 ​

画一张图要做五件事。每一件都是「一段数据进、一段数据出」,所以你可以只用其中一件, 也可以全部交给 createDrawHandle 一次做完。

顺序这一步交付什么形态
① parseDocument有哪些元素、谁指向谁DrawDocument(没有坐标)
② resolveTheme调色板与排版量ResolvedTheme(具体值,不是 token 名)
③ layoutDocument元素多大、放哪、线怎么绕LayoutResult(绝对几何)
④ buildScene每个元素由哪些零件组成Scene(rect / ellipse / path / text / group)
⑤ renderToSvg画到哪SVG 字符串;DOM 侧是 createDomHost

一句话记住产出:文档 → 调色板 / 排版量 → 绝对几何 → 场景树 → 介质。

前两步互不依赖(文档和主题谁先解析都行),所以代码里 resolveTheme 会先跑; 上面这张表按「读者手里先有文档」来排,后面所有页面都用这个顺序。

自己走一遍 ​

左边改文档,右边出图;点下面每一段的标签,看那一段产出什么——这一步的产出就是下一步的输入。

Live五段管线 Playground左边改文档,右边看结果
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。

接着往下读 ​

五段管线是骨架。每一段内部还有自己的机制,左侧「机制」从 文档与校验开始,依次讲节点注册、端口、主题、布局、场景和宿主。

MIT License