Flowchart 流程图
通用布局能力:
RenderFlowchart实例统一支持width、height、min/max、margin和槽位对齐。节点坐标和尺寸属于 Flowchart world space,viewport 平移与缩放不进入文档。详见组件通用布局属性。
RenderFlowchart 是 DirectSurface UI 原生 Canvas 流程图组件。它提供 start、end、process、decision 节点、有向边、一层 Group、固定 Pool → Lane → Node 泳道、选择、手工编辑、平移缩放、自动布局、JSON 和 PNG 输出,不依赖 React Flow、ELK、Dagre、Mermaid、Cytoscape 或其他图编辑运行时。
它不是 BPMN 建模器或流程执行引擎,也不提供任意嵌套 Group/Pool/Lane、折叠容器、子流程、跨 Pool message 语义、边避障、minimap、实时协作或服务端持久化。工具栏、属性面板和工作台应在组件外使用现有 Toolbar、PropertyGrid 和 DockWorkbench 组合。
API 总览
import { BuiltinFlowchartLayoutProvider, FLOWCHART_MAX_PORTS, FLOWCHART_MAX_PORTS_PER_NODE, FLOWCHART_MAX_GROUPS, FLOWCHART_MAX_POOLS, FLOWCHART_MAX_LANES, FLOWCHART_MAX_LANES_PER_POOL, FLOWCHART_MAX_WAYPOINTS, FLOWCHART_MAX_WAYPOINTS_PER_EDGE, FlowchartController, FlowchartExportError, RenderFlowchart, layoutFlowchart, type FlowchartAutoLayoutOptions, type FlowchartConnectionValidator, type FlowchartConnectionValidationContext, type FlowchartDocument, type FlowchartEdgePatch, type FlowchartGroup, type FlowchartGroupPatch, type FlowchartPool, type FlowchartPoolPatch, type FlowchartLane, type FlowchartLanePatch, type FlowchartPoolOrientation, type FlowchartAutoLayoutResult, type FlowchartLayoutProvider, type FlowchartLayoutRequest, type FlowchartLayoutResult, type FlowchartNodePaintContext, type FlowchartNodePaintState, type FlowchartPngOptions, type FlowchartPort, type FlowchartPortDirection, type FlowchartPortSide, type FlowchartReconnectEndpoint, type FlowchartViewport,} from 'ds-ui'
| API | 类型 | 用途 |
|---|---|---|
RenderFlowchart |
class | Canvas 查看、编辑、viewport、自动布局和 PNG 导出。 |
RenderFlowchart.importJson() |
method | 通过 Render 门面原子导入 JSON,并使用 Controller 的可撤销 import 合同。 |
RenderFlowchart.exportJson() |
method | 通过 Render 门面导出规范化 document JSON。 |
FlowchartController |
class | 文档、选择、历史、会话内复制/粘贴、连接校验和 JSON 导入导出。 |
FlowchartController.duplicateSelection(offset) |
method | 深复制当前 node/container closure 并按精确 offset 插入。 |
FlowchartController.copySelection() |
method | 把当前 node/container closure 写入当前 Controller 的私有会话 buffer。 |
FlowchartController.pasteSelection(offsetDelta) |
method | 从会话 buffer 粘贴,并累加本次 offset delta。 |
FlowchartController.setNodePorts(nodeId, ports) |
method | 原子设置或清除节点端口,成功 reason 为 port-update。 |
FlowchartController.setEdgeWaypoints(edgeId, waypoints) |
method | 原子设置或清除持久化路径点,成功 reason 为 waypoint-update。 |
| Group commands | methods | createGroup、updateGroup、assignNodesToGroup、moveGroups、resizeGroup、fitGroupToMembers、deleteGroup、selectGroup。 |
| Pool commands | methods | createPool、updatePool、movePools、deletePool、selectPool。 |
| Lane commands | methods | createLane、updateLane、resizeLane、reorderLane、reparentNodesToLane、deleteLane、selectLane。 |
BuiltinFlowchartLayoutProvider |
class | 内置确定性 down/right 分层布局 provider。 |
layoutFlowchart() |
function | 不修改输入的同步布局纯函数。 |
FlowchartLayoutProvider |
interface | 可同步或异步返回布局位置的扩展合同。 |
FlowchartLayoutRequest |
interface | provider 输入,包含文档和通用布局参数。 |
FlowchartLayoutResult |
interface | provider 输出,包含精确节点位置和 diagnostics。 |
FlowchartAutoLayoutResult |
type | Render 调度结果:applied、unchanged 或 rejected。 |
FlowchartDebugState |
interface | 当前可见/绘制数量和 pan、drag、resize、connect/reconnect 瞬态状态。 |
FlowchartDocument |
interface | 接受 v1~v5;Controller 内部规范化为当前 v5。 |
FlowchartGroup / FlowchartGroupPatch |
types | 一层持久化 Group rect 和受限业务字段 patch。 |
FlowchartPool / FlowchartPoolPatch |
types | Pool origin、主轴长度、方向和受限 patch。 |
FlowchartLane / FlowchartLanePatch |
types | Pool-owned lane、cross-axis size 和受限 patch。 |
FlowchartNodePaintContext |
interface | 自定义节点 painter 的节点、矩形、绘制上下文和状态。 |
FlowchartNodePaintState |
interface | hover、selection、focus、readonly、dragging 和 anchor 状态。 |
FlowchartEdgePatch |
type | 只允许更新 edge 的 label、anchor side 和 JSON-safe data。 |
FlowchartReconnectEndpoint |
type | 指定 source/target 单端重连的 node,以及可选 side 或 portId。 |
FlowchartConnectionValidationContext |
interface | 区分 connect、reconnect、duplicate、paste 和 port-update,并提供需要排除的 edge ID。 |
FlowchartExportError |
class | PNG 导出失败,使用稳定的 code 分类。 |
最小只读示例
import { RenderFlowchart, type FlowchartDocument } from 'ds-ui' const document: FlowchartDocument = { version: 1, nodes: [ { id: 'start', kind: 'start', label: '开始', x: 0, y: 40 }, { id: 'review', kind: 'process', label: '审核', x: 180, y: 30 }, { id: 'end', kind: 'end', label: '完成', x: 380, y: 40 }, ], edges: [ { id: 'to-review', source: 'start', target: 'review' }, { id: 'to-end', source: 'review', target: 'end', label: '通过' }, ],} const viewer = new RenderFlowchart({ value: document, readonly: true, width: 720, height: 420,})
只读模式仍允许选择、平移、滚轮缩放、fitView()、revealNode()、节点激活、JSON/PNG 导出和 Ctrl / Cmd+C 复制到 Controller 会话 buffer;Ctrl / Cmd+V 会被消费但不会粘贴。由 Render 处理的用户交互不能拖动节点、框选、建立连接、删除、键盘移动或通过快捷键 undo/redo。readonly 是用户交互门,不是全局权限控制:业务代码仍可显式调用 editor.importJson()、controller.importJson()、controller.pasteSelection()、controller.undo() 等程序式 mutation,组件会正常刷新。editor.autoLayout() 有独立的 readonly 检查,当前会返回 layout-readonly rejection。
可编辑示例和 Controller 生命周期
import { FlowchartController, RenderFlowchart, type FlowchartDocument,} from 'ds-ui' const initialDocument: FlowchartDocument = { version: 1, nodes: [ { id: 'draft', kind: 'process', label: '起草', x: 0, y: 0 }, { id: 'approve', kind: 'decision', label: '批准?', x: 220, y: 0 }, ], edges: [{ id: 'submit', source: 'draft', target: 'approve' }],} const controller = new FlowchartController({ value: initialDocument, historyLimit: 100,})const unsubscribe = controller.subscribeChange(event => { console.log(event.reason, event.revision, event.document)}) const editableFlowchart = new RenderFlowchart({ controller, snapToGrid: true, gridSize: 24, onSelectionChange: selection => console.log(selection), onViewportChange: viewport => console.log(viewport),}) // 页面卸载:Render 只解除自己的 Controller 订阅。editableFlowchart.dispose()unsubscribe()
传入外部 controller 时,ownsController === false。RenderFlowchart.dispose() 不销毁或重置外部 Controller,业务可以在其他视图继续使用它。FlowchartController 本身没有 dispose();业务只需释放自己通过 subscribeChange() / subscribeSelection() 创建的订阅,并在不再使用时释放引用。
同时传 controller 和 value 时以 Controller 为准。外部 Controller 的 validateConnection 必须在 Controller 构造时配置;同时向 Render 传外部 controller 和 validateConnection 会抛出 RangeError。
文档模型
interface FlowchartDocument {
version: 1 | 2 | 3 | 4 | 5
nodes: FlowchartNode[]
edges: FlowchartEdge[]
groups?: FlowchartGroup[]
pools?: FlowchartPool[]
lanes?: FlowchartLane[]
}
interface FlowchartNode {
id: string
kind: string
label: string
x: number
y: number
width?: number
height?: number
tone?: 'default' | 'info' | 'success' | 'warning' | 'danger'
ports?: FlowchartPort[]
groupId?: string
laneId?: string
data?: FlowchartData
}
interface FlowchartGroup {
id: string
label: string
x: number
y: number
width: number
height: number
tone?: FlowchartTone
data?: FlowchartData
}
type FlowchartPoolOrientation = 'horizontal' | 'vertical'
interface FlowchartPool {
id: string
label: string
x: number
y: number
length: number
orientation: FlowchartPoolOrientation
data?: FlowchartData
}
interface FlowchartLane {
id: string
poolId: string
label: string
size: number
data?: FlowchartData
}
interface FlowchartPort {
id: string
side: 'top' | 'right' | 'bottom' | 'left'
offset: number
direction: 'input' | 'output' | 'bidirectional'
data?: FlowchartData
}
interface FlowchartEdge {
id: string
source: string
target: string
label?: string
sourceSide?: 'top' | 'right' | 'bottom' | 'left' | 'auto'
targetSide?: 'top' | 'right' | 'bottom' | 'left' | 'auto'
sourcePortId?: string
targetPortId?: string
waypoints?: Offset[]
data?: FlowchartData
}
x/y 是节点、Group 或 Pool 外框左上角的 world 坐标,可以为负数。nodes 数组顺序也是 z-order:后面的节点覆盖前面的节点,选择不会重排数组;拖动 preview 只在交互期间临时绘制到最上层。Group、Pool 和 Lane ID 各自在本类别内 document-wide 唯一,不同类别可以复用同一字符串;Lane 的 poolId 精确引用一个 Pool。
一个节点最多有一个 containment ref:groupId 与 laneId 互斥。Group 与 Pool/Lane 可以在同一 v5 文档共存,但成员集合必须不相交;Group、Pool 和 Lane 不能作为 edge endpoint,也不能声明 ports。Group 不嵌套;Pool 不嵌套;Lane 只能直接属于一个 Pool。membership 只由 ref 决定,不从坐标推断。
Pool/Lane 几何常量为 poolHeader=28、laneHeader=56、lanePadding=16。horizontal Pool 的 lanes 从上到下,length 是 lane body 宽度,lane size 是高度;vertical Pool 的 lanes 从左到右,length 是 lane body 高度,lane size 是宽度。节点 rect 必须完全位于所属 lane body 的 16-unit inset 内。normalize 会按 Pool document order 把交错 lanes 规范化为连续 Pool blocks,并保留每个 Pool 内的 lane 相对顺序。
import { type FlowchartDocument } from 'ds-ui' const clinicalPathway: FlowchartDocument = { version: 5, groups: [{ id: 'pre-op', label: '术前', x: -300, y: 0, width: 240, height: 180 }], pools: [{ id: 'clinical', label: '临床路径', x: 0, y: 0, length: 600, orientation: 'horizontal', }], lanes: [ { id: 'patient', poolId: 'clinical', label: '患者', size: 100 }, { id: 'doctor', poolId: 'clinical', label: '医生', size: 140 }, ], nodes: [ { id: 'assessment', kind: 'process', label: '评估', x: -284, y: 44, width: 64, height: 40, groupId: 'pre-op', }, { id: 'arrival', kind: 'start', label: '到院', x: 72, y: 44, width: 64, height: 40, laneId: 'patient', }, ], edges: [{ id: 'to-arrival', source: 'assessment', target: 'arrival' }],}
默认尺寸:
| kind | 默认尺寸 |
|---|---|
start / end |
96 × 44 |
process |
144 × 64 |
decision |
112 × 88 |
| 未知 kind | 144 × 64,并使用 process fallback;可由自定义 painter 接管。 |
Controller 入口会规范化并防御性复制文档:缺省尺寸、tone 和 anchor 分别补为对应默认尺寸、default 和 auto。未知的 document/node/edge 结构字段会被丢弃并产生 warning;data 内的 JSON-safe 业务字段会保留。
文档校验包括:
- version 必须为 1~5,nodes/edges 必须是数组;v4 激活 Group,v5 激活 Pool/Lane;
- node ID 和 edge ID 各自非空且各自唯一;
- x/y 和尺寸必须有限,尺寸必须大于 0;
- edge source/target 必须引用已有节点;
- v2 起启用 node-local ports 和 edge port refs;v3 启用 ordered world-coordinate waypoints;v4 启用 groups/groupId;v5 启用 pools/lanes/laneId;
- 旧版本携带尚未激活的 containment 字段会 hard reject,不会 warning 后静默丢弃;
- Group/Lane membership 必须存在、唯一、满足 XOR 与容器几何;Pool 至少包含一条 Lane;
- source port 必须是 output/bidirectional,target port 必须是 input/bidirectional;显式 port ref 与 concrete side 不能并存;
- waypoint x/y 必须有限,相邻重复点拒绝,非相邻重复允许形成回路;
- tone、anchor、label 和 JSON-safe data 必须合法;
- data number 不允许
NaN或 Infinity,也不允许循环引用。
允许自环、相同 source/target 的多条边和环形流程。任何 error 都原子拒绝整个候选,不暴露部分规范化文档。
规范化、校验和公共常量
当数据来自配置文件、接口或其他不可信边界时,应先使用公共规范化 API,再把结果交给 Controller:
import { cloneFlowchartDocument, getFlowchartDefaultNodeSize, normalizeFlowchartDocument, validateFlowchartDocument, type FlowchartNodeSize, type FlowchartNormalizationResult, type FlowchartValidationResult, type NormalizedFlowchartDocument,} from 'ds-ui' const candidateFlowchartDocument: unknown = { version: 1, nodes: [{ id: 'start', kind: 'start', label: '开始', x: 0, y: 0 }], edges: [],} const flowchartValidation: FlowchartValidationResult = validateFlowchartDocument(candidateFlowchartDocument)const flowchartNormalization: FlowchartNormalizationResult = normalizeFlowchartDocument(candidateFlowchartDocument)const processNodeSize: FlowchartNodeSize = getFlowchartDefaultNodeSize('process') if (flowchartNormalization.status === 'normalized') { const normalizedFlowchart: NormalizedFlowchartDocument = flowchartNormalization.document const independentFlowchartCopy = cloneFlowchartDocument(normalizedFlowchart) console.log(flowchartValidation.valid, processNodeSize, independentFlowchartCopy)}
| API | 合同 |
|---|---|
normalizeFlowchartDocument(candidate) |
完整读取、规范化和校验候选;成功返回 NormalizedFlowchartDocument,失败返回 diagnostics 且不暴露部分文档。 |
validateFlowchartDocument(candidate) |
执行相同安全校验,只返回 FlowchartValidationResult,不返回规范化文档。 |
cloneFlowchartDocument(document) |
深度复制一个已经可信的 FlowchartDocument;它不会代替规范化或校验。 |
getFlowchartDefaultNodeSize(kind) |
返回独立的 FlowchartNodeSize;未知 kind 使用 process 默认尺寸。 |
规范化成功后,节点、边、Group、Pool 和 Lane 分别使用对应的 normalized 类型,容器是 current v5 NormalizedFlowchartDocument,并始终包含 groups、pools、lanes 数组。成功分支由 FlowchartNormalizationResult 表达;只读校验结果使用 FlowchartValidationResult。
公共常量:
| 常量 | 值 / 用途 |
|---|---|
FLOWCHART_DOCUMENT_VERSION |
当前文档版本 5。v1~v4 输入成功规范化时各产生恰好一个 leading version-migrated warning。 |
FLOWCHART_DEFAULT_TONE |
默认 tone:default。 |
FLOWCHART_DEFAULT_ANCHOR |
默认 anchor:auto。 |
FLOWCHART_MAX_NODES |
最多 10,000 个节点。 |
FLOWCHART_MAX_EDGES |
最多 20,000 条边。 |
FLOWCHART_MAX_GROUPS |
最多 1,000 个 Group。 |
FLOWCHART_MAX_POOLS |
最多 100 个 Pool。 |
FLOWCHART_MAX_LANES |
最多 1,000 条 Lane。 |
FLOWCHART_MAX_LANES_PER_POOL |
每个 Pool 最多 64 条 Lane。 |
FLOWCHART_MAX_LABEL_LENGTH |
label 最多 4,096 UTF-16 units。 |
FLOWCHART_MAX_DATA_DEPTH |
data 容器深度最多 32。 |
FLOWCHART_MAX_PORTS_PER_NODE / FLOWCHART_MAX_PORTS |
每节点最多 64 个端口;文档最多 100,000 个端口。 |
FLOWCHART_MAX_WAYPOINTS_PER_EDGE / FLOWCHART_MAX_WAYPOINTS |
每边最多 64 个路径点;文档最多 200,000 个路径点。 |
内部额外限制为每个 Group 最多 1,000 个成员、每条 Lane 最多 1,000 个成员;成员总数仍受 FLOWCHART_MAX_NODES 限制。collection/global/per-owner overflow 会在读取对应 nested data、port items、waypoint items 或 Fragment payload 前拒绝。
状态、选择和历史
controller.document 是深度冻结的规范化快照;不要原地修改节点或数组,应调用 Controller mutation。controller.selection 和 editor.viewport 每次返回独立投影,可以安全读取。
| 状态 | 是否进入 document | 是否推进 revision/history |
|---|---|---|
| 节点、边、Group、Pool、Lane、membership 和持久化几何 | 是 | 成功 mutation 推进一次。 |
| selection | 否 | 否。 |
| viewport、hover、container/drop/drag/connection preview | 否 | 否。 |
| 一次节点拖动 | pointer up 才提交 | 最多一次。 |
| 一次节点缩放 | pointer up 才调用 resizeNode() |
最多一次。 |
| 一次边重连 | pointer up 才调用 reconnectEdge() |
最多一次。 |
| 一次 Group/Pool drag、Group/Lane resize 或 Lane reparent | pointer up 才调用对应 Controller command | 最多一次。 |
| pointer cancel / Escape 取消 preview | 否 | 0 mutation。 |
默认历史上限为 100。新编辑清空 redo;删除节点会同时删除关联边。setDocument() 是新的外部 baseline,会清空 undo/redo 和 selection;importJson() 是可撤销的一次用户编辑。undo、redo、删除和 import 后,已不存在的 selection ID 会被自动剔除。
selection 的 nodeIds、edgeIds、groupIds、poolIds、laneIds 去重并按当前文档的 canonical collection order 返回;空的可选 container arrays 会省略。选择 Group/Pool/Lane 不会隐式把成员加入 public selection。selectAll() 包含当前文档的五类对象;Ctrl / Cmd 追加或移除选择不会改变 z-order。
RenderFlowchart.minNodeWidth 和 minNodeHeight 是构造后可读的 public readonly 字段。debugState(): FlowchartDebugState 中 resizing 包含 active node/Group/Lane resize;connecting 同时表示新建连接或 edge reconnect。dragging 也包含 Group/Pool drag、Lane reparent 和 waypoint drag;这些状态与 boxSelecting、panning、autoPanning 都不进入 document/history。
Group、Pool 与 Lane 命令
Group 命令使用持久化 rect。moveGroups() 平移 Group、全部成员和 endpoint same-delta 的 manual waypoints;resizeGroup() 只修改容器且不能排除成员;fitGroupToMembers() 使用固定 header/padding 贴合非空成员。deleteGroup() 只删除容器并清除成员 groupId,不会删除业务节点或边。assignNodesToGroup(non-null) 遇到任何 Lane member 会整批 rejected;跨 containment 必须先显式传 null 清除旧归属。
Pool/Lane 命令遵循以下合同:
| command | 行为 |
|---|---|
createPool(pool, lanes) |
原子创建一个 Pool 和 1~64 条 owner-matching lanes。 |
updatePool(poolId, patch) |
只允许 label、length、data;orientation 创建后不可改,缩短 length 不能排除成员。 |
movePools(poolIds, delta) |
平移 Pool origins、全部 Lane members 和 endpoint same-delta manual waypoints。 |
deletePool(poolId) |
删除 Pool 与其 lanes,清除成员 laneId;节点和边保留。 |
createLane(lane) |
追加到 owner Pool block 末尾。 |
updateLane(laneId, patch) |
只允许 label、data。 |
resizeLane(laneId, size) |
保持目标 Lane origin/members 不动,后续 lanes/members 按 cross-axis delta 平移。 |
reorderLane(laneId, toIndex) |
toIndex 是 Pool-local;不允许跨 Pool,成员保持 lane-local 坐标。 |
reparentNodesToLane(nodeIds, laneId, positions) |
positions key set 必须精确覆盖去重 node IDs;同 Pool 或 uncontained 节点可进入目标,Group member/跨 Pool member rejected;null 要求节点当前属于 Lane,仍原子应用 positions,但只清 membership 且不做 Lane bounds 校验。 |
deleteLane(laneId) |
清除本 Lane 成员的 laneId,后续 lanes/members 前移;最后一条 Lane 不可删除。 |
所有 containment 命令都不调用 connection validator。Group reasons 为 group-create、group-update、group-membership、group-move、group-resize、group-delete;Pool/Lane reasons 为 pool-create、pool-update、pool-move、pool-delete、lane-create、lane-update、lane-resize、lane-reorder、lane-reparent、lane-delete。加上既有 reasons 后总数为 33,connection validator operations 仍只有 connect、reconnect、duplicate、paste、port-update 五个。
changed mutation 是一个 revision、一个 history snapshot 和一个精确 reason;unchanged/rejected 为 0 mutation 且保留 redo。deleteSelection() 可以一次组合 nodes、edges、Groups、Pools 和 Lanes,并始终只产生一个 delete reason/history unit:selected containers 执行 preserve-members delete,selected nodes 仍删除 incident edges;重复 closure 会去重。delete 后先发布 committed change,再按 canonical order发布必要的 selection prune;history 只保存 document,undo/redo 不恢复历史 selection。
子图重复、复制和会话内粘贴
import { FlowchartController, type FlowchartDocument } from 'ds-ui' const copyDocument: FlowchartDocument = { version: 1, nodes: [ { id: 'draft', kind: 'process', label: '起草', x: 0, y: 0 }, { id: 'approve', kind: 'process', label: '审核', x: 180, y: 0 }, ], edges: [{ id: 'to-approve', source: 'draft', target: 'approve' }],}const controller = new FlowchartController({ value: copyDocument }) controller.setSelection({ nodeIds: ['draft', 'approve'], edgeIds: [] }) controller.duplicateSelection({ x: 24, y: 24 }) if (controller.copySelection()) { controller.pasteSelection({ x: 24, y: 24 }) controller.pasteSelection({ x: 24, y: 24 })}
这三个 Controller member 的签名和结果是:
| member | 结果合同 |
|---|---|
duplicateSelection(offset): FlowchartMutationResult |
使用调用时的精确 offset 复制当前 node/container closure;没有 selected node、Group、Pool 或 Lane closure 时为 unchanged。成功 change reason 为 duplicate。 |
copySelection(): boolean |
成功写入私有 buffer 并返回 true;没有 selected node、Group、Pool 或 Lane closure 时返回 false,保留已有 buffer 和累计 offset。它不产生 document mutation、history 或事件。 |
pasteSelection(offsetDelta): FlowchartMutationResult |
从私有 buffer 插入;buffer 为空时为 unchanged。成功 change reason 为 paste。 |
Fragment 是内部会话模型,不是 root export。普通 node-based closure 仍按 node document order复制 nodes,并自动包含两端都在集合中的 internal edges。只选部分 Group/Lane members、未选容器时会复制节点并清除相应 membership。
- selected Group:复制 Group、全部成员和 internal edges;
- selected Pool:复制完整 Pool、全部 lanes、全部 lane members 和 internal edges;
- selected Lane、未选 Pool:生成一个保留 Pool label/length/orientation/data 的 parent shell,只包含显式 lanes/members;多条 nonadjacent lanes 会按 Pool-local order compact,成员保持 lane-local 坐标;
- Pool 与 owned Lane 同时选择会去重为完整 Pool closure;自动 parent shell 和自动 owned lanes 不会冒充显式选择;
- Group/Pool/Lane/node/edge 使用独立的 deterministic copy ID allocator;ports、refs、data 深复制,global paste offset 作用于容器、节点和 waypoints,lane compaction 的 waypoint 只按 endpoint same-delta 规则追加平移。
自环和平行边会保留;只选 edge 且没有任何 node/container closure 时不会产生 Fragment。复制后更新或删除源对象不会改变 buffer。
pasteSelection() 的参数是“本次增量”,不是绝对位置。一次新 copy 把累计 offset 重置为 { x: 0, y: 0 };随后连续传入 { x: 24, y: 24 } 会把同一快照放在 24、48、72 的累计位置。中途把 delta 改为 10 时会从上一次成功累计值继续增加 10。rejected 或 unchanged paste 不推进累计 offset。
buffer 属于单个 FlowchartController 会话,不属于 document、selection 或 history。外部 Controller 从一个 RenderFlowchart 卸载后再挂到另一个 Render 时仍保留 buffer;setDocument() 或 importJson() 的输入通过规范化后会清空 buffer 和累计 offset,即使新 document 与当前值相同。被拒绝的 replacement/import 会保留 buffer。普通节点/边编辑、layout、selection、undo/redo 和 duplicate 不会清空它。
成功 duplicate/paste 各产生一个 history 项,并选择本批全部新 nodes/internal edges,以及源 selection 中显式选中的 cloned Groups、Pools、Lanes。Lane-only closure 自动生成的 parent Pool shell、selected Pool 自动带入的 owned lanes 都不会进入结果 selection。undo 会移除本批对象并剔除失效 selection;redo 恢复 document,但不会重新选择恢复的对象。undo/redo 不改变 buffer 或累计 paste offset。
Ctrl / Cmd+C 和 Ctrl / Cmd+V 只操作这个 Controller 会话 buffer,不读取或写入系统剪贴板,也不通过 ClipboardController 或 navigator.clipboard 交换 JSON。buffer 不会跨 Controller 复制;跨文档或跨应用交换请显式使用 exportJson() / importJson()。active drag、resize、box、pan、connect 或 reconnect 期间 C/V 会被消费但不执行,也不会取消当前交互。
节点尺寸与边原子命令
import { FlowchartController, type FlowchartDocument, type FlowchartEdgePatch, type FlowchartReconnectEndpoint,} from 'ds-ui' const commandDocument: FlowchartDocument = { version: 1, nodes: [ { id: 'review', kind: 'process', label: '审核', x: 180, y: 24 }, { id: 'archive', kind: 'end', label: '归档', x: 420, y: 24 }, ], edges: [{ id: 'to-end', source: 'review', target: 'archive' }],}const commandController = new FlowchartController({ value: commandDocument }) commandController.resizeNode('review', { x: 180, y: 24, width: 168, height: 72,}) const edgePatch: FlowchartEdgePatch = { label: '审核通过', sourceSide: 'right', targetSide: 'left', data: { condition: 'approved' },}commandController.updateEdge('to-end', edgePatch) const endpoint: FlowchartReconnectEndpoint = { end: 'target', nodeId: 'archive', side: 'top',}if (commandController.canReconnect('to-end', endpoint)) { commandController.reconnectEdge('to-end', endpoint)}
resizeNode()原子替换单个节点的 x/y/width/height;Controller 接受任意有限正尺寸,minNodeWidth/minNodeHeight只约束 pointer resize。updateEdge()只能修改label、sourceSide、targetSide和data;运行时传入 id/source/target 会 rejected。成功时 edge ID 和数组位置保持不变。canReconnect()与reconnectEdge()使用同一结构和业务校验路径。重连只替换指定端,省略 side 时解析为auto,另一端、label、data、edge ID 和数组位置保持不变;自环合法。- 上述三个 mutation 命令
resizeNode()、updateEdge()、reconnectEdge()的 rejected/unchanged 都不推进 revision/history,也不清除 redo;成功 mutation 各产生一个 history 项,change reason 分别为resize、edge-update、reconnect。canReconnect()只返回 boolean,不提交 mutation。
Ports 与 manual waypoints
版本激活矩阵:
| 输入版本 | 激活字段 | normalize/export |
|---|---|---|
| v1 | legacy nodes/edges | 迁移为 v5,恰好一个 warning。v1 携带 ports/refs/waypoints/containment 会拒绝。 |
| v2 | ports、sourcePortId/targetPortId | 迁移为 v5,恰好一个 warning。v2 携带 waypoints/containment 会拒绝。 |
| v3 | legacy、ports、refs、waypoints 可混用 | 迁移为 v5,恰好一个 warning;containment 字段未激活。 |
| v4 | v3 + groups + node.groupId | 迁移为 v5,恰好一个 warning;pools/lanes/laneId 即使为空也拒绝。 |
| v5 | Groups + Pools/Lanes + groupId/laneId | 保持 v5;current→current 无 migration warning。 |
normalize 是唯一 migration 入口;不会产生 v1→v4→v5 两个 warning。cloneFlowchartDocument() 保留可信输入的原 version 和已激活字段,不执行 migration,也不重新诊断 dangling refs 或 geometry。future、missing 或非整数 version 会拒绝。
端口 ID 在所属 node 内按原始 code units 唯一,不 trim、不区分大小写转换。offset 位于 [0, 1];top/bottom 从左到右,left/right 从上到下。显式端口决定实际 endpoint 和 side;未引用端口的 edge 继续使用 legacy anchors。
import { FlowchartController, type FlowchartDocument } from 'ds-ui' const portWaypointDocument: FlowchartDocument = { version: 3, nodes: [ { id: 'review', kind: 'process', label: '审核', x: 0, y: 0 }, { id: 'archive', kind: 'end', label: '归档', x: 240, y: 0 }, ], edges: [{ id: 'to-end', source: 'review', target: 'archive' }],}const controller = new FlowchartController({ value: portWaypointDocument }) controller.setNodePorts('review', [ { id: 'in', side: 'left', offset: 0.5, direction: 'input' }, { id: 'out', side: 'right', offset: 0.5, direction: 'output' },]) controller.setEdgeWaypoints('to-end', [ { x: 280, y: 40 }, { x: 360, y: 120 },]) controller.setEdgeWaypoints('to-end', []) // clear
两条命令各形成一个 history item,selection 不变;unchanged/rejected 为 0 mutation。setNodePorts() 会先验证所有受影响 explicit edges,再按 edge document order 以 port-update 调用 connection validator;setEdgeWaypoints() 不调用 connection validator。
manual waypoints 是 edge-owned、ordered、持久化的 world-coordinate exact polyline vertices。存在 waypoints 时,straight/orthogonal 都严格使用 [dynamic source, ...waypoints, dynamic target],不会加入隐藏折点。普通 edge 只移动一个端点、resize、reconnect、port update 和 layout 都保留绝对路径点;updateNode() 修改自环 owner 的 x/y 时按 node delta 平移该自环路径点,普通单端 edge 仍保持 absolute。moveNodes()、moveGroups()、movePools() 以及 Lane resize/reorder/delete/reparent 只在 edge 两端拥有相同 delta 时同步平移 waypoints(自环使用 owner delta);两端 delta 不同或只有一端移动时保持 absolute。Fragment/duplicate/paste 深复制端口与 refs,并按 fragment offset 平移路径点;lane-only compaction 也遵守 endpoint same-delta。owner/global limit 在读取 point items 前预检。
Viewport
import { RenderFlowchart, type FlowchartViewport } from 'ds-ui' const flowchart = new RenderFlowchart()const viewport: FlowchartViewport = { offsetX: 40, offsetY: 24, zoom: 1.25 } flowchart.setViewport(viewport)flowchart.fitView()flowchart.revealNode('review')
坐标变换为 screen = world * zoom + offset。默认 zoom 范围是 0.25–4;setViewport() 拒绝非有限值,并把有限 zoom clamp 到配置范围。viewport 变化可高频触发 onViewportChange,但不修改 document revision。
鼠标和键盘交互
| 操作 | 行为 |
|---|---|
| 单击节点、边、Group、Pool 或 Lane | 替换当前选择;container selection 不隐式扩展成员。 |
Ctrl / Cmd + 单击 |
按类型追加或移除选择,并按 document order 规范化。 |
| 拖动节点 | 拖动所选节点;拖动未选节点会先替换选择。 |
| 拖动 Group header/外边框 | 移动同类 selected Groups 及成员。 |
| 拖动 Pool header/外边框 | 移动同类 selected Pools 及全部 Lane members。 |
| 拖动单选 Group 四角 handle | 缩放持久化 Group rect,不能排除成员。 |
| 拖动单选 Lane trailing separator | 调整 lane size;horizontal 使用 bottom,vertical 使用 right,最后一条使用 Pool outer trailing border。命中区两侧各 4 screen px。 |
| 把节点拖入 Lane body | 主节点先 snap,selected nodes 使用同一 delta;topmost candidate 唯一,invalid candidate 消费命中且不 fallback。uncontained 或同 Pool member 可进入;Group/cross-Pool member rejected。 |
| 拖动单选节点的四角 handle | 缩放节点;四角 handle 为固定 8 screen px,pointer up 单次提交。 |
| 左键拖动空白 | 平移 viewport。 |
Shift + 空白拖动 |
框选完全包含在矩形内的节点、Groups、Pools、Lanes 并替换选择。 |
Ctrl / Cmd + Shift + 空白拖动 |
追加框选节点和 containers;框选不直接选择边。 |
| 从节点四边 anchor 或 output/bidirectional port 拖到目标 anchor/port | 建立连接;input-only source 和 output-only target 显示为 direction-invalid。 |
| 拖动 selected edge 的 source/target handle | 重连单个端点;可落到 legacy anchor/body 或 explicit port。 |
| 双击 selected edge 的 route segment | 投影到最近 segment 后 snap 并插入路径点;等距取最小 segment index。自动 route 首次插入会物化当前 internal vertices。 |
| 拖动 selected edge 的 waypoint handle | 只更新 preview;pointer up 最多一次 setEdgeWaypoints()。 |
| 双击 waypoint handle | 删除 persisted point;删除最后一点即 clear 并恢复自动 route。 |
| 滚轮 | 以指针所在 world point 为中心缩放。 |
| 双击节点 | 调用 onNodeActivate。 |
Delete / Backspace |
删除选择;删除节点会级联删除边。 |
Ctrl / Cmd+A |
全选节点、边、Groups、Pools 和 Lanes。 |
Ctrl / Cmd+D |
深复制当前 node/container closure,offset 为当前 gridSize;只读或 active pointer interaction 时只消费快捷键。 |
Ctrl / Cmd+C |
把当前 node/container closure 复制到当前 Controller 会话 buffer;只读模式允许。 |
Ctrl / Cmd+V |
从当前 Controller 会话 buffer 粘贴,本次 offset delta 为当前 gridSize;只读模式只消费快捷键。 |
Ctrl / Cmd+Z |
undo。 |
Ctrl / Cmd+Shift+Z 或 Ctrl+Y |
redo。 |
| 方向键 | 移动所选节点;Shift 使用 10 倍步长。 |
Escape |
先取消当前瞬态操作;没有瞬态操作时清空选择。 |
+ / - |
缩放。 |
0 |
fit view。 |
节点/Group/Pool 拖动、节点/Group/Lane 缩放、Lane reparent、框选、连接、重连和 waypoint drag 在指针接近边缘时自动平移。这些拖动类编辑手势严格超过 4 screen px 才启动 preview,并在 auto-pan 后用同一个 client pointer 重新投影;pointer up 最多调用一个 Controller command。edge hit tolerance 为 6 screen px,handles 均保持 screen-space 尺寸。snap 开启时,主对象 origin、被拖动 corner/separator、Lane reparent 主节点和 waypoint 投影点吸附到最近 grid。
插入或拖动 waypoint 后,如果 snap 结果与相邻 persisted point 完全相同,操作保持 unchanged:不调用 Controller、不推进 revision/history,也不会删除既有非相邻重复点。
缩放只提供 north-west、north-east、south-east、south-west 四角;固定对角保持不动。minNodeWidth / minNodeHeight 默认均为 24 world units,snap 后再应用最小尺寸。多节点 selection 不显示 resize handles。
重连期间原 edge 以 muted route 保留,candidate route 绘制在其上方。同一 node/side 内连续移动不会重复调用 canReconnect();node body 的 auto 和同 node 的显式 anchor side 是不同 candidate。有效 pointer up 最多调用一次 reconnectEdge();blank、validator false/throw 和 unchanged 都不产生额外 history。
自环的 auto anchor 使用稳定的 source=right、target=top 路由;显式 sourceSide/targetSide 会保留并作为自环实际端点。auto 端点拖离后返回原 node 的同一 resolved anchor 时继续保持 auto,因此不会产生多余 history。
每条与 viewport 相交的 selected edge 显示 source/target reconnect handles 和逐点 cull 的 waypoint handles。普通半径为 5 screen px;waypoint 与 reconnect 中心完全重合时,中心 5 px 命中 waypoint,5–8 px 外环命中 reconnect。完整 hit priority 为:node resize → reconnect/重合中心 waypoint 规则 → waypoint → explicit port → legacy anchor → node → active Group resize → Lane separator/container handles → Group/Lane/Pool headers → edge → Group body → Lane body → Pool body → blank。同类 overlap 取 reverse canonical paint order;topmost invalid Lane target 会消费 drop,不检查下层 Lane。
pointer cancel、Escape、失焦、隐藏、切换 readonly、外部文档或 selection 变化和 dispose() 都会清理当前节点/Group/Pool 拖动、节点/Group/Lane 缩放、Lane reparent/drop target、框选、连接、重连、waypoint preview 和 auto-pan 状态;之后到达的 stale pointer up 为 0 mutation。
建立连接和业务校验
import { FlowchartController, type FlowchartConnectionValidationContext, type FlowchartConnectionValidator, type FlowchartDocument,} from 'ds-ui' const validateConnection: FlowchartConnectionValidator = ( input, currentDocument, context: FlowchartConnectionValidationContext,) => { if (input.source === input.target) return false return !currentDocument.edges.some(edge => edge.id !== context.edgeId && edge.source === input.source && edge.target === input.target)} const initialDocument: FlowchartDocument = { version: 1, nodes: [], edges: [] }const controller = new FlowchartController({ value: initialDocument, validateConnection,})
validator 是同步布尔回调,在结构校验通过后执行:
- 普通连接收到
{ operation: 'connect' };重连收到{ operation: 'reconnect', edgeId }。重连 candidate input 的 id 固定为原 edge ID,而 document 参数仍包含原 edge。 - duplicate/paste 对本批每条 remapped edge 各调用一次,分别收到
{ operation: 'duplicate', edgeId: newEdgeId }或{ operation: 'paste', edgeId: newEdgeId }。所有调用共享同一个深度冻结的完整 candidate document,其中包含原 document、本批全部新 nodes/edges,也包含当前正在校验的 candidate edge;每次 input 都是独立副本。 setNodePorts()只对引用该 node explicit ports 的受影响边调用,context 为{ operation: 'port-update', edgeId };schema、ownership、direction 和 dangling 校验先执行。setEdgeWaypoints()不调用 connection validator。- 检查重复边时统一使用
edge.id !== context.edgeId排除当前 edge。对于 duplicate/paste,如果不排除 candidate 自身,完整 candidate 语义会把它误判为重复边。 - 已有双参数 validator 在 TypeScript 上仍可赋值;但第二个参数在 duplicate/paste 中是包含当前 candidate edge 的完整 document。依赖“document 不含待校验 edge”假设的 validator 应改为读取第三个 context 并做上述自排除。
- 任一 callback 返回 false 或抛出异常都会在首个失败处停止并原子拒绝整批,document、history、revision、selection 和 paste offset 保持不变。
validator 必须是对同一 Controller 无副作用的同步判断。不要在 validator 内对同一 Controller 调用 mutation、selection、duplicate/copy/paste、undo/redo、canConnect() 或 canReconnect();这种重入不受支持,也没有针对 callback 自身副作用的回滚保证。preview 不能绕过最终 Controller 校验。普通 connect 未显式提供 edge ID 时生成第一个可用的 edge_N。
自动布局
import { RenderFlowchart } from 'ds-ui' const autoLayoutFlowchart = new RenderFlowchart({ layoutOptions: { direction: 'down', layerSpacing: 80, nodeSpacing: 32, },}) const autoLayoutResult = await autoLayoutFlowchart.autoLayout({ direction: 'right' })if (autoLayoutResult.status === 'rejected') { console.warn(autoLayoutResult.reason, autoLayoutResult.diagnostics)}
autoLayout() 参数逐字段覆盖构造时的 layoutOptions。builtin provider 使用节点实际规范化尺寸,保持 document order 的确定性分层;环图不会抛错或死循环,而是返回带 owner ID 的 cycle-detected warning。
builtin containment 布局顺序固定为 Groups → Lanes → root/uncontained;每个 partition 只使用两端都在该 owner 内的 internal edges。Group 使用请求的 down/right 并从 group.x+16, group.y+28+16 开始;horizontal Lane 强制 right,vertical Lane 强制 down,并从 derived lane body +16 开始。Lane 空间不足时整条 Lane 的成员保持原位置,不做 partial placement,并返回一个携带 poolId/laneId 的 lane-layout-insufficient-space warning。布局先计算 planned effective Group rect 和全部 Pool rect,再按二者最大 bottom/right 放置 root partition,因此 Controller apply 后重复布局为 unchanged。
布局是一次 Controller mutation:结果必须恰好覆盖全部 node ID、不能包含未知 ID,而且所有坐标必须有限;否则原子拒绝。有效变更只产生一个 revision 和一个 undo 历史项。
autoLayout() 不修改 viewport,也不会隐式调用 fitView()。如果页面希望布局后立即展示完整流程,应在 applied 或 unchanged 后显式调用 fitView()。正式 Demo 的自动适配属于页面组合行为,不是 RenderFlowchart 的默认副作用。
自定义 LayoutProvider
import { RenderFlowchart, type FlowchartLayoutProvider, type FlowchartLayoutResult,} from 'ds-ui' const serviceLayoutProvider: FlowchartLayoutProvider = { id: 'service-layout', async layout(request, signal): Promise<FlowchartLayoutResult> { const response = await fetch('/layout/flowchart', { method: 'POST', body: JSON.stringify(request), signal, }) return await response.json() as FlowchartLayoutResult },} const providerFlowchart = new RenderFlowchart({ layoutProvider: serviceLayoutProvider })
provider 可以同步或异步返回,Render 统一提供 Promise API。连续调用会 abort 旧请求;结果返回时还会检查 AbortSignal、组件 dispose 状态和捕获的 Controller revision。过期、异常、readonly 或无效 positions 都返回 rejected,不会修改文档。
layoutFlowchart(document, options) 和 BuiltinFlowchartLayoutProvider 都不修改输入,也不修改 membership、Group/Pool/Lane geometry、ports、refs、waypoints 或 routes;provider 结果仍只精确覆盖全部业务 node IDs,container IDs 不能进入 positions。builtin owner index 对 nodes/edges 各做一次线性遍历。第三方布局配置不进入 FlowchartDocument 或通用 FlowchartAutoLayoutOptions。
官方 ELK Provider(可选)
Root-only 用户不需要安装 elkjs;builtin 仍是 RenderFlowchart 的默认布局。需要复杂分层布局时,同时安装 optional peer,并从唯一正式子路径显式注入官方 Provider:
npm install ds-ui elkjs@^0.12.0
import { RenderFlowchart } from 'ds-ui'import { ElkFlowchartLayoutProvider } from 'ds-ui/flowchart-layout-elk' const elkProvider = new ElkFlowchartLayoutProvider()const elkFlowchart = new RenderFlowchart({ layoutProvider: elkProvider,}) try { const result = await elkFlowchart.autoLayout({ direction: 'right' }) if (result.status === 'rejected') { console.warn(result.reason, result.diagnostics) }} finally { elkFlowchart.dispose() elkProvider.dispose()}
官方 Adapter 固定使用 ELK layered 的 Worker 模式,不提供 main-thread/Worker execution-mode 选择,也不会静默回退 builtin。应用应先 dispose RenderFlowchart 以取消活动请求,再 dispose 自己创建的 ElkFlowchartLayoutProvider;页面隐藏、路由卸载和应用退出都应执行这两个步骤。部署方必须发布构建器生成的 ELK Worker asset,并让 CSP worker-src 允许该 asset 所在源。
Adapter 仍遵守 positions-only 合同:按 Group、Lane、root/uncontained owner partitions 分别布局,horizontal Lane 强制 right,vertical Lane 强制 down。explicit ports 只作为 fixed-side constraints;ELK edge sections、routes 和 bend points 不写回。已有 manual waypoints、edge refs、ports、membership 与 container geometry 均保留。每次请求重排全部节点;当前没有 pinned/locked 节点、partial、incremental 或 continuous layout,也不支持 ELK compound graph。
elkjs 是 optional peer,但缺失时不能依赖构建器代替运行检查:packed Node ESM 导入 ds-ui/flowchart-layout-elk 会得到 ERR_MODULE_NOT_FOUND: elkjs;Vite 5 production 可能依据 optional peer metadata 生成空代理并成功构建。empty/single-node partition 可能短路而未执行 ELK,任何确实需要 ELK 的多节点布局仍会失败,且不会回退 builtin。因此使用该子路径的应用必须显式安装兼容版本并运行真实多节点 smoke test。
本项目按 EPL-2.0 OR GPL-3.0-or-later 表达式选择 EPL-2.0 分支。Demo 部署包含 elkjs 0.12.0 LICENSE 和 NOTICE;分发 elkjs 的应用仍需自行满足所选上游许可证义务。当前包不包含 Dagre adapter,也不提供多引擎 registry、Provider 热切换或 ELK routing 封装。
JSON 导入和导出
import { RenderFlowchart } from 'ds-ui' const jsonFlowchart = new RenderFlowchart()const exportedFlowchartJson = jsonFlowchart.exportJson({ pretty: true })const importResult = jsonFlowchart.importJson(exportedFlowchartJson) if (importResult.status === 'rejected') { console.warn(importResult.diagnostics)}
RenderFlowchart.importJson() / exportJson() 是 Controller 同名方法的便利门面;持有外部 Controller 时也可以直接调用 controller.importJson() / controller.exportJson()。JSON 只包含 committed document 的 version、nodes、edges、groups、pools、lanes;不包含 viewport、selection、hover、preview 或主题。规范化文档 export → import 后深度等价,不承诺保留原 JSON 字段顺序、省略的默认值或未知结构字段。
安全限制:
- nodes ≤ 10,000;
- edges ≤ 20,000;
- Groups ≤ 1,000;Pools ≤ 100;Lanes ≤ 1,000 且每 Pool ≤ 64;
- 每 Group/Lane ≤ 1,000 个成员,一个节点只能属于 Group 或 Lane 之一;
- node/edge label ≤ 4,096 UTF-16 units;
- data 容器深度 ≤ 32,data 根 record 计为第 1 层;
- ports ≤ 64/node、100,000/document;waypoints ≤ 64/edge、200,000/document;
- 所有几何值和 data number 必须有限。
这些是拒绝明显异常输入的安全上限,不是该规模下的流畅编辑承诺。
PNG 导出
import { FlowchartExportError, type FlowchartPngOptions, type RenderFlowchart,} from 'ds-ui' async function exportDiagram(flowchart: RenderFlowchart): Promise<Blob | null> { const options: FlowchartPngOptions = { padding: 32, scale: 2, pixelRatio: window.devicePixelRatio, transparent: false, includeGrid: false, } try { return await flowchart.exportPng(options) } catch (error) { if (error instanceof FlowchartExportError) console.error(error.code, error.message) return null }}
| 参数 | 默认值 | 说明 |
|---|---|---|
padding |
32 |
world bounds 外边距;必须有限且非负。 |
scale |
1 |
逻辑输出倍率;必须有限且大于 0。 |
pixelRatio |
1 |
物理像素倍率;必须有限且大于 0。 |
transparent |
false |
是否保留透明背景。 |
includeGrid |
false |
是否包含网格。 |
PNG 使用和屏幕相同的 Pool/Lane/Group、节点、边 painter 以及组件当前主题。committed container backgrounds、headers、borders 和 manual routes 会导出;hover、selection、focus、drop target、anchor、separator、port/resize/reconnect/waypoint handles,以及任何 container/node/edge preview 都不会导出。导出始终读取 committed document;bounds 覆盖 containers、节点、完整 route、箭头和 label。负坐标会被平移到输出画布中。空文档输出 64×64 logical px,之后再应用 scale/pixelRatio。
物理输出限制为每边最多 8,192 px、总像素最多 64,000,000。FlowchartExportError.code:
invalid-optionscanvas-too-largecanvas-unavailableencode-failed
自定义节点 painter
import { RenderFlowchart, type FlowchartNodePaintContext, type FlowchartNodePaintState,} from 'ds-ui' function paintApproval(context: FlowchartNodePaintContext): boolean { if (context.node.kind !== 'approval') return false const state: FlowchartNodePaintState = context.state const canvas = context.paintContext.ctx canvas.fillStyle = state.selected ? '#dbeafe' : '#eff6ff' canvas.strokeStyle = '#2563eb' canvas.fillRect(context.rect.x, context.rect.y, context.rect.width, context.rect.height) canvas.strokeRect(context.rect.x, context.rect.y, context.rect.width, context.rect.height) return true} const customPaintFlowchart = new RenderFlowchart({ paintCustomNode: paintApproval })
返回 true 表示接管节点 body/content,返回 false 使用内置 shape。框架始终在 custom painter 之后绘制 selection、hover、focus、drag、连接 anchor、edge reconnect handles 和 node resize handles。painter 会被裁剪到 node rect;抛出异常时框架捕获错误并绘制 process fallback,单个业务节点不会中断屏幕绘制或 PNG 导出。painter 不应修改 Controller。
构造参数速查
| 参数 | 默认值 | 说明 |
|---|---|---|
controller / value |
内部空 Controller | 外部状态或初始文档;同时提供时 Controller 优先。 |
readonly |
false |
保留查看和选择,阻断 Render 用户交互 mutation;不替代业务权限或 Controller 程序式调用。 |
appearance |
'framed' |
'framed' 或 'flush'。 |
showGrid / snapToGrid |
true / true |
网格显示与节点拖动吸附。 |
gridSize |
24 |
有限正 world units。 |
edgeRouting |
'orthogonal' |
'straight' 或 'orthogonal'。 |
layoutDirection |
'down' |
auto anchor 和 edge route 的主要方向。 |
layoutProvider |
builtin | 自动布局 provider。 |
layoutOptions |
down / 80 / 32 | 自动布局方向、层距和节点距。 |
minZoom / maxZoom |
0.25 / 4 |
有限正 zoom 范围。 |
minNodeWidth / minNodeHeight |
24 / 24 |
有限正 world units;只约束 pointer resize 的最小尺寸。 |
paintCustomNode |
undefined |
自定义节点 body/content painter。 |
validateConnection |
undefined |
仅由 Render 创建内部 Controller 时传入。 |
onChange |
undefined |
文档 mutation 事件。 |
onSelectionChange |
undefined |
selection 事件。 |
onViewportChange |
undefined |
viewport 事件。 |
onNodeActivate |
undefined |
双击节点激活。 |
性能边界和首版限制
- 当前规范文档为 v5;v1~v4 只作为迁移输入。最终根包 Flowchart 公共面为 24 个 runtime symbols、51 个 type symbols,共 75 个;Fragment、copy buffer、migration helper 和内部 geometry/scene/hit/preview/handle 类型不公开。
- builtin layout 按 owner 建立线性 node/edge index,相同输入产生相同结果。
- 只绘制与 viewport 相交的 containers、节点和 routes;container labels 只对 visible owners 测量。containment index 按 document identity/revision 缓存,selection、hover、preview 和 viewport 变化不重建;pointer hit 只遍历实际 visible containers/handles。
- 首版不建立 R-tree/quadtree,也不承诺最大导入边界可流畅交互。大图应按真实设备、主题、label 和 custom painter 做 profile。
- drag preview 不推进 revision,pointer up 才提交一次完整位置变更。
- 当前没有 edge 避障、交叉优化、locked/pinned 节点、任意嵌套/折叠 Group 或泳道、BPMN/subprocess、跨 Pool message 语义、minimap、palette、Port Editor 或 Waypoint Editor wrapper。
- 未知 kind 支持 process fallback 和 custom painter;端口仅支持 node boundary fixed ports,不支持 interior/free ports。