主题
示例
八个例子,每个只比上一个大一样东西——这样多出来的那一行到底买到了什么,一眼就能看出来。 页面里的图都是当前代码实时渲染的,不是截图:本站直接 import packages/draw/src。
① 一份文档,一张图
文档里只有「谁连谁」,一个坐标都没有。
零警告:整条管线没有产生任何诊断
json
{
"version": 1,
"nodes": [
{ "id": "order", "type": "rect", "label": "订单服务", "props": { "width": 120, "height": 48 } },
{ "id": "stock", "type": "rect", "label": "库存服务", "props": { "width": 120, "height": 48 } }
],
"edges": [
{ "id": "e1", "type": "link", "source": { "node": "order" }, "target": { "node": "stock" }, "label": "扣减" }
]
}link 不在库里——它是注册进来的一个边类型(后面 ⑦ 会自己写一个):
ts
registry.registerEdge("link", {
markers: { end: "open-arrow" },
render(ctx) {
ctx.out.node({ kind: "path", key: ctx.out.key(""), d: polylineD(ctx.route), style: { ...ctx.style, fill: "none" } });
},
});② 换 tokens:几何一个像素都不动
主题只换颜色这类量。layout 的输入没变,所以两张图的节点位置完全一样:
ts
const neon = resolveTheme({
tokens: { surface: "#0b1020", border: "#22d3ee", text: "#e8fbff", edge: "#22d3ee" },
});③ 容器:有 children 的节点就是容器
没有 groups[] 这第二套结构:一个节点带了 children,它就是容器,边连的是容器整体。
零警告:整条管线没有产生任何诊断
json
{
"id": "order",
"type": "component",
"label": "订单服务",
"props": { "width": 230, "height": 150, "band": true },
"children": ["orderRepo"]
}④ 端口:端口的位置就是边的落点
在 ③ 上只加 ports。端口不是装饰——它的渲染点就是边的挂接点(和边共用同一个 anchors)。
零警告:整条管线没有产生任何诊断
json
{
"id": "order",
"ports": [
{ "id": "api", "side": "right" },
{ "id": "db", "side": "bottom" }
]
}json
{ "id": "e1", "type": "link", "source": { "node": "web" }, "target": { "node": "order", "port": "api" } }⑤ 注册自己的形状
actor 不是内置形状,是这里注册的一个 ShapeSpec。示例直接复用了复合图元示例的 actorShape:圆盘、头、肩、文字和端口都由它自己摆,@ggn/draw 一行都不用改。
零警告:整条管线没有产生任何诊断
ts
import { actorShape } from "./usecase-draw";
registry.registerShape("actor", actorShape);真正的自定义形状里,每个零件仍由 ctx.out.node(...) 输出,ctx.out.key(suffix) 负责稳定 key。 展开上面这张卡最下面的「零件 key」就能看到这套命名 (只有这一节的图卡带这个面板:别处的重点是别的东西)。 更完整的生命周期见节点注册。
⑥ 再注册一个五角星
五角星也没有被写进库里。它还是普通的 ShapeSpec:五个外顶点、五个内顶点都由盒子算出来, 描边和填充继续走同一套 ctx.style。
零警告:整条管线没有产生任何诊断
ts
const starShape: ShapeSpec = {
measure(props, label, typography, measurer) {
// 盒子要同时装下星星和标签
},
render(ctx) {
const cx = ctx.box.x + ctx.box.width / 2;
const cy = ctx.box.y + ctx.box.height / 2;
const radius = Math.min(ctx.box.width, ctx.box.height) * 0.42;
ctx.out.node({
kind: "path",
key: ctx.out.key(""),
d: starPath(cx, cy, radius), // 十个顶点全部由盒子计算
style: ctx.style,
});
},
};
registry.registerShape("star", starShape);换成自己的图标也是同一条路:先量出节点大小,再把零件写到自己的 key 下面。 形状、schema、anchors 和 render 的边界见节点注册。
⑦ 注册自己的边类型
库内没有内置边类型。同一个文件里两种边,就是两条注册项:实线开放箭头,和虚线实心箭头。
零警告:整条管线没有产生任何诊断
ts
registry.registerEdge("dashed", {
markers: { end: "arrow" },
render(ctx) {
ctx.out.node({
kind: "path",
key: ctx.out.key(""),
d: polylineD(ctx.route),
style: { ...ctx.style, fill: "none", dash: "6 4" },
});
},
});⑧ 未注册类型:回落长什么样
故意写一个没注册的 type。build 报 UNKNOWN_SHAPE,layout 按盒子口径兜底, 所以画面照样出得来,只是形状退化成矩形 + 居中文字。
UNKNOWN_SHAPEb未注册的形状类型:sequence,回落 rectFALLBACK_USEDb形状类型未注册,已用矩形 + 居中 label 回落
json
{ "id": "b", "type": "sequence", "label": "未注册类型" }UNKNOWN_SHAPE 未注册的形状类型:sequence,回落 rect
FALLBACK_USED 形状类型未注册,已用矩形 + 居中 label 回落它们共用的一段接线
八个例子跑的都是同一条管线,只有 document 和 registry 在变——所以先看图,再看这段代码:
ts
import {
parseDocument, resolveTheme, createRegistry, builtinRegistry,
layoutDocument, buildScene, renderToSvg,
} from "@ggn/draw";
const registry = createRegistry({ base: builtinRegistry }); // 只有内置形状和 marker
const document = parseDocument(json); // ① 校验文档
const theme = resolveTheme(); // ② 主题 + 排版量
const layout = layoutDocument(document.value, { // ③ 算几何
typography: theme.value.typography,
metrics: registry.metrics(),
});
const scene = buildScene(document.value, layout.value, { // ④ 生成场景
registry,
theme: theme.value,
});
const svg = renderToSvg(scene.value); // ⑤ 输出想看更多
五段管线 那个 Playground 可以改文档、切引擎、切主题,并逐段查看每一段的产出。