Skip to content

节点注册 ​

文档里的 type 只是一个名字。注册表把它映射成一套行为:怎么量、端口落在哪里、 最后往场景里写哪些零件。

一次节点绘制经过什么 ​

这里有一个重要分界:

  • measure 属于布局,决定节点占多大;
  • render 属于场景构建,只画已经确定好的盒子。

render 不能改节点位置,也不能偷偷扩大布局盒子。要改大小,改 measure 或文档里的 显式几何;要改位置,换布局引擎或 pin。

一个最小形状 ​

ts
import {
  builtinRegistry,
  createRegistry,
  type ShapeSpec,
} from "@ggn/draw";

const dot: ShapeSpec = {
  measure() {
    return { width: 24, height: 24 };
  },
  render(ctx) {
    ctx.out.node({
      kind: "ellipse",
      key: ctx.out.key(""),
      cx: ctx.box.x + ctx.box.width / 2,
      cy: ctx.box.y + ctx.box.height / 2,
      rx: ctx.box.width / 2,
      ry: ctx.box.height / 2,
      style: ctx.style,
    });
  },
};

const registry = createRegistry({ base: builtinRegistry })
  .registerShape("dot", dot);

createRegistry({ base: builtinRegistry }) 会先复制矩形、椭圆、路径等内置原语; 如果省略 base,你就只有一个空注册表。自定义形状通常都应该从内置注册表起步。

四个扩展点 ​

字段什么时候用负责什么
schemaprops 需要校验或默认值时在布局度量前整理 props
measure需要根据文字、props 算尺寸时返回 { width, height }
anchors端口不能落在普通矩形边缘时返回每个 port 的绝对点
render每次构建场景时通过 ctx.out 写出场景零件

schema 是可选的。没有 schema 时,props 会原样传下去。schema 的默认值会同时作用于 度量与绘制,避免“量的时候是 30,画的时候却变成 undefined”。

render 拿得到什么 ​

ctx 刻意只给当前节点需要的东西:

能拿到用途
ctx.box布局已经确定的位置和尺寸
ctx.props当前节点经过 schema 整理的参数
ctx.label当前节点的文字
ctx.anchors当前节点的端口坐标
ctx.style已经解析成具体值的画法
ctx.out往当前节点的 key 前缀下写零件

它拿不到整份文档、其他节点的盒子或布局结果。这样自定义形状不会依赖别的元素,也不会 在不知不觉中把跨节点几何写死。

key 是增量更新的基础 ​

每个零件都要通过 ctx.out.key(suffix) 生成 key。后缀最好表达零件含义,例如 label、icon、border,不要用数组下标。同一个形状下一次还产出同一组 key, 宿主才能只更新变化的零件。

复合形状可以继续调用另一个注册项:

ts
ctx.out.child("icon", "icon", { name: "cart" }, iconBox);

子节点的 key 会自然变成 当前节点/icon。组合关系由形状自己表达,库不需要知道 “这是一个按钮”或“这是一个 UML 节点”。

常见误解 ​

  • 把尺寸只写在 render 里:场景会绕开布局,父容器和连线都对不上。
  • measure 没有给标签留空间:文字画出了盒子,布局却按更小的尺寸算。
  • 端口没有在文档里声明:anchors 无从生成,边也不能引用它。
  • 重复注册同一个 type:后注册的会替换先注册的,注册表不会自动报警。
  • 忘记以 builtinRegistry 为 base:内置形状和 marker 不在你的注册表里, 图可能看起来少了一层东西。

具体的端口规则见端口与锚点,颜色与样式见 主题与样式。如果自定义的是边,见边注册。

MIT License