流程图与节点编辑器
流程图的真相是 节点表 + 边表,几何是投影。<g> 上的 transform 是视图,不是数据库。连线存成截图像素等于没有文档。世界单位自定(常见 1 = 1 CSS 像素 at scale=1);相机是根 viewBox。
1. 场景怎么切
文档:Node[] / Edge[](id、父级、端口、标签)
世界:node.x/y/w/h、edge 的 path 控制点
相机:viewBox = 视口在世界里的矩形
层:
g.edges 边,z 在节点下
g.nodes 节点外壳 + 端口
g.overlay 选中框、对齐线、正在拉的边(不进导出)
DOM:节点标题 IME、右键菜单
小地图:可以是第二相机的光栅
必须是节点:业务节点、端口、可点标签、可改的边。命中、框选、对齐、撤销都认这些 id。
可以光栅:小地图、网格点、超长画布上未进视口的页。正在拖的橡胶筋可以临时 canvas,pointerup 写回 path。
export interface FlowNode {
id: string;
x: number;
y: number;
w: number;
h: number;
label: string;
}
export interface FlowEdge {
id: string;
from: { nodeId: string; port: string };
to: { nodeId: string; port: string };
}
export interface FlowDoc {
nodes: FlowNode[];
edges: FlowEdge[];
}
export function cameraToViewBox(
cam: { x: number; y: number; scale: number },
cssW: number,
cssH: number,
): string {
return `${cam.x} ${cam.y} ${cssW * cam.scale} ${cssH * cam.scale}`;
}
边的几何从端口世界坐标算贝塞尔,存在边对象里或每次从端口派生。派生更抗「挪节点忘了改线」。存副本就要在 SetNodePos 命令里同时更新边。
2. 交互
框选、多选、变换手柄走文档命中:先 viewToWorld(getScreenCTM 逆),再 AABB / 端口半径。不要对整棵树 elementFromPoint 然后还自己算一次偏移——两者会在 stroke 扩展、透明区域上打架。见 指针 与 手柄。
拖节点:pointerdown 记起点,move 用 coalesce 命令改 x/y,up 提交。撤销一步回到拖之前,见 撤销。
端口几何从节点矩形派生,不要另存一份屏幕坐标。连线 d 在世界空间生成,线宽若要屏幕恒定,用 non-scaling-stroke 或 overlay。
3. 不适用
- 节点内部是富文本页面:外壳 SVG,内容
foreignObject或 HTML overlay。用户上传的foreignObject仍按 XSS 禁。 - 自动布局一次烘成图:布局结果要写回
x/y,否则下次打开会跳。 - 把边画在 canvas 上当唯一存储:缩放即糊,命中要对齐两套表面。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 缩放后线对不上端口 | 边存在了屏幕坐标 |
| 导出没有连线 | 线画在 overlay / canvas,没进文档 |
| 拖一下 undo 40 次 | 没 coalesce |
| 框选漏了细线 | 命中只用 fill,忽略了 stroke 宽度 |
| 打印只有节点没有字 | 字在 HTML overlay 且打印 CSS 藏了 DOM |
权威资料
核对日期:2026-08-26