主题
节点注册
文档里的 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,你就只有一个空注册表。自定义形状通常都应该从内置注册表起步。
四个扩展点
| 字段 | 什么时候用 | 负责什么 |
|---|---|---|
schema | props 需要校验或默认值时 | 在布局度量前整理 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 不在你的注册表里, 图可能看起来少了一层东西。