Skip to content

示例 ​

八个例子,每个只比上一个大一样东西——这样多出来的那一行到底买到了什么,一眼就能看出来。 页面里的图都是当前代码实时渲染的,不是截图:本站直接 import packages/draw/src。

① 一份文档,一张图 ​

文档里只有「谁连谁」,一个坐标都没有。

Live一份文档,一张图契约里只有「谁连谁」
订单服务 库存服务

零警告:整条管线没有产生任何诊断

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 的输入没变,所以两张图的节点位置完全一样:

订单服务 库存服务
浅色 token同一份文档
订单服务 库存服务
荧光 token几何一个像素都没动
ts
const neon = resolveTheme({
  tokens: { surface: "#0b1020", border: "#22d3ee", text: "#e8fbff", edge: "#22d3ee" },
});

③ 容器:有 children 的节点就是容器 ​

没有 groups[] 这第二套结构:一个节点带了 children,它就是容器,边连的是容器整体。

Live容器即节点有 children 的节点就是容器
订单服务 COMPONENT repository Web

零警告:整条管线没有产生任何诊断

json
{
  "id": "order",
  "type": "component",
  "label": "订单服务",
  "props": { "width": 230, "height": 150, "band": true },
  "children": ["orderRepo"]
}

④ 端口:端口的位置就是边的落点 ​

在 ③ 上只加 ports。端口不是装饰——它的渲染点就是边的挂接点(和边共用同一个 anchors)。

Live端口端口的位置就是边的落点
订单服务 COMPONENT repository Web Postgres

零警告:整条管线没有产生任何诊断

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 一行都不用改。

Live注册自己的形状actor 不在库里,是注册项
用户 下单 支付

零警告:整条管线没有产生任何诊断

15 个零件 · key 与绘制顺序
@roote1e1/marker-ende2e2/marker-enduser/avataruser/avatar-headuser/avatar-bodyuser/labeluser/port-orderuser/port-payorderorder/labelpaypay/label
ts
import { actorShape } from "./usecase-draw";

registry.registerShape("actor", actorShape);

真正的自定义形状里,每个零件仍由 ctx.out.node(...) 输出,ctx.out.key(suffix) 负责稳定 key。 展开上面这张卡最下面的「零件 key」就能看到这套命名 (只有这一节的图卡带这个面板:别处的重点是别的东西)。 更完整的生命周期见节点注册。

⑥ 再注册一个五角星 ​

五角星也没有被写进库里。它还是普通的 ShapeSpec:五个外顶点、五个内顶点都由盒子算出来, 描边和填充继续走同一套 ctx.style。

Live再注册一个五角星十个顶点由盒子计算,颜色走同一套 token
收藏

零警告:整条管线没有产生任何诊断

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 的边界见节点注册。

⑦ 注册自己的边类型 ​

库内没有内置边类型。同一个文件里两种边,就是两条注册项:实线开放箭头,和虚线实心箭头。

Live注册自己的边类型库内没有内置边类型;虚线也是注册进来的
订单服务 库存服务 审计

零警告:整条管线没有产生任何诊断

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 按盒子口径兜底, 所以画面照样出得来,只是形状退化成矩形 + 居中文字。

Live未注册类型:回落 + 诊断build 报 UNKNOWN_SHAPE,画面照样出得来
已注册 未注册类型
  • UNKNOWN_SHAPEb未注册的形状类型:sequence,回落 rect
  • FALLBACK_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 可以改文档、切引擎、切主题,并逐段查看每一段的产出。

MIT License