@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
@@ -0,0 +1,287 @@
1
+ export const NODE_DEFINITIONS = {
2
+ // Events — start
3
+ startEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '开始事件', icon: '', kind: 'event', eventStage: 'start', eventDefinition: null, width: 56, height: 56, palette: 'startEvents' },
4
+ messageStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '消息开始', icon: '✉', kind: 'event', eventStage: 'start', eventDefinition: 'message', width: 56, height: 56, palette: 'startEvents' },
5
+ timerStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '定时开始', icon: '◷', kind: 'event', eventStage: 'start', eventDefinition: 'timer', width: 56, height: 56, palette: 'startEvents' },
6
+ conditionalStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '条件开始', icon: '≡', kind: 'event', eventStage: 'start', eventDefinition: 'conditional', width: 56, height: 56, palette: 'startEvents' },
7
+ signalStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '信号开始', icon: '△', kind: 'event', eventStage: 'start', eventDefinition: 'signal', width: 56, height: 56, palette: 'startEvents' },
8
+ errorStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '错误开始', icon: '⚡', kind: 'event', eventStage: 'start', eventDefinition: 'error', width: 56, height: 56, palette: 'startEvents' },
9
+ escalationStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '升级开始', icon: '▲', kind: 'event', eventStage: 'start', eventDefinition: 'escalation', width: 56, height: 56, palette: 'startEvents' },
10
+ compensationStartEvent: { bpmnType: 'bpmn:StartEvent', localName: 'startEvent', label: '补偿开始', icon: '≪', kind: 'event', eventStage: 'start', eventDefinition: 'compensate', width: 56, height: 56, palette: 'startEvents' },
11
+
12
+ // Events — intermediate catch / throw
13
+ intermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '中间捕获事件', icon: '', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: null, width: 56, height: 56, palette: 'intermediateEvents' },
14
+ messageIntermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '消息捕获', icon: '✉', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: 'message', width: 56, height: 56, palette: 'intermediateEvents' },
15
+ timerIntermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '定时捕获', icon: '◷', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: 'timer', width: 56, height: 56, palette: 'intermediateEvents' },
16
+ conditionalIntermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '条件捕获', icon: '≡', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: 'conditional', width: 56, height: 56, palette: 'intermediateEvents' },
17
+ signalIntermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '信号捕获', icon: '△', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: 'signal', width: 56, height: 56, palette: 'intermediateEvents' },
18
+ linkIntermediateCatchEvent: { bpmnType: 'bpmn:IntermediateCatchEvent', localName: 'intermediateCatchEvent', label: '链接捕获', icon: '↙', kind: 'event', eventStage: 'intermediate', eventRole: 'catch', eventDefinition: 'link', width: 56, height: 56, palette: 'intermediateEvents' },
19
+ intermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '中间抛出事件', icon: '', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: null, width: 56, height: 56, palette: 'intermediateEvents' },
20
+ messageIntermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '消息抛出', icon: '✉', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: 'message', width: 56, height: 56, palette: 'intermediateEvents' },
21
+ escalationIntermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '升级抛出', icon: '▲', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: 'escalation', width: 56, height: 56, palette: 'intermediateEvents' },
22
+ signalIntermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '信号抛出', icon: '△', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: 'signal', width: 56, height: 56, palette: 'intermediateEvents' },
23
+ linkIntermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '链接抛出', icon: '↗', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: 'link', width: 56, height: 56, palette: 'intermediateEvents' },
24
+ compensationIntermediateThrowEvent: { bpmnType: 'bpmn:IntermediateThrowEvent', localName: 'intermediateThrowEvent', label: '补偿抛出', icon: '≪', kind: 'event', eventStage: 'intermediate', eventRole: 'throw', eventDefinition: 'compensate', width: 56, height: 56, palette: 'intermediateEvents' },
25
+
26
+ // Events — boundary
27
+ timerBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '定时边界事件', icon: '◷', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'timer', width: 46, height: 46, palette: 'boundaryEvents' },
28
+ messageBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '消息边界事件', icon: '✉', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'message', width: 46, height: 46, palette: 'boundaryEvents' },
29
+ errorBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '错误边界事件', icon: '⚡', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'error', width: 46, height: 46, palette: 'boundaryEvents' },
30
+ escalationBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '升级边界事件', icon: '▲', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'escalation', width: 46, height: 46, palette: 'boundaryEvents' },
31
+ signalBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '信号边界事件', icon: '△', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'signal', width: 46, height: 46, palette: 'boundaryEvents' },
32
+ conditionalBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '条件边界事件', icon: '≡', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'conditional', width: 46, height: 46, palette: 'boundaryEvents' },
33
+ compensationBoundaryEvent: { bpmnType: 'bpmn:BoundaryEvent', localName: 'boundaryEvent', label: '补偿边界事件', icon: '≪', kind: 'boundary', eventStage: 'boundary', eventRole: 'catch', eventDefinition: 'compensate', width: 46, height: 46, palette: 'boundaryEvents' },
34
+
35
+ // Events — end
36
+ endEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '结束事件', icon: '', kind: 'event', eventStage: 'end', eventDefinition: null, width: 56, height: 56, palette: 'endEvents' },
37
+ messageEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '消息结束', icon: '✉', kind: 'event', eventStage: 'end', eventDefinition: 'message', width: 56, height: 56, palette: 'endEvents' },
38
+ errorEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '错误结束', icon: '⚡', kind: 'event', eventStage: 'end', eventDefinition: 'error', width: 56, height: 56, palette: 'endEvents' },
39
+ escalationEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '升级结束', icon: '▲', kind: 'event', eventStage: 'end', eventDefinition: 'escalation', width: 56, height: 56, palette: 'endEvents' },
40
+ cancelEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '取消结束', icon: '×', kind: 'event', eventStage: 'end', eventDefinition: 'cancel', width: 56, height: 56, palette: 'endEvents' },
41
+ compensationEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '补偿结束', icon: '≪', kind: 'event', eventStage: 'end', eventDefinition: 'compensate', width: 56, height: 56, palette: 'endEvents' },
42
+ signalEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '信号结束', icon: '△', kind: 'event', eventStage: 'end', eventDefinition: 'signal', width: 56, height: 56, palette: 'endEvents' },
43
+ terminateEndEvent: { bpmnType: 'bpmn:EndEvent', localName: 'endEvent', label: '终止结束', icon: '●', kind: 'event', eventStage: 'end', eventDefinition: 'terminate', width: 56, height: 56, palette: 'endEvents' },
44
+
45
+ // Activities
46
+ task: { bpmnType: 'bpmn:Task', localName: 'task', label: '任务', icon: '□', kind: 'task', width: 216, height: 72, palette: 'tasks' },
47
+ userTask: { bpmnType: 'bpmn:UserTask', localName: 'userTask', label: '用户任务', icon: '♙', kind: 'task', width: 216, height: 72, palette: 'tasks' },
48
+ manualTask: { bpmnType: 'bpmn:ManualTask', localName: 'manualTask', label: '手工任务', icon: '✋', kind: 'task', width: 216, height: 72, palette: 'tasks' },
49
+ serviceTask: { bpmnType: 'bpmn:ServiceTask', localName: 'serviceTask', label: '服务任务', icon: '⚙', kind: 'task', width: 216, height: 72, palette: 'tasks' },
50
+ scriptTask: { bpmnType: 'bpmn:ScriptTask', localName: 'scriptTask', label: '脚本任务', icon: '</>', kind: 'task', width: 216, height: 72, palette: 'tasks' },
51
+ businessRuleTask: { bpmnType: 'bpmn:BusinessRuleTask', localName: 'businessRuleTask', label: '业务规则任务', icon: '▦', kind: 'task', width: 216, height: 72, palette: 'tasks' },
52
+ sendTask: { bpmnType: 'bpmn:SendTask', localName: 'sendTask', label: '发送任务', icon: '✉', kind: 'task', width: 216, height: 72, palette: 'tasks' },
53
+ receiveTask: { bpmnType: 'bpmn:ReceiveTask', localName: 'receiveTask', label: '接收任务', icon: '✉', kind: 'task', width: 216, height: 72, palette: 'tasks' },
54
+ callActivity: { bpmnType: 'bpmn:CallActivity', localName: 'callActivity', label: '调用活动', icon: '↗', kind: 'task', activityKind: 'call', width: 216, height: 72, palette: 'activities' },
55
+ subProcess: { bpmnType: 'bpmn:SubProcess', localName: 'subProcess', label: '子流程', icon: '+', kind: 'container', activityKind: 'subProcess', width: 272, height: 126, palette: 'activities' },
56
+ eventSubProcess: { bpmnType: 'bpmn:SubProcess', localName: 'subProcess', label: '事件子流程', icon: '◉', kind: 'container', activityKind: 'eventSubProcess', width: 272, height: 126, palette: 'activities' },
57
+ transaction: { bpmnType: 'bpmn:Transaction', localName: 'transaction', label: '事务子流程', icon: '▣', kind: 'container', activityKind: 'transaction', width: 272, height: 126, palette: 'activities' },
58
+ adHocSubProcess: { bpmnType: 'bpmn:AdHocSubProcess', localName: 'adHocSubProcess', label: 'Ad-hoc 子流程', icon: '~', kind: 'container', activityKind: 'adHoc', width: 272, height: 126, palette: 'activities' },
59
+
60
+ // Gateways
61
+ exclusiveGateway: { bpmnType: 'bpmn:ExclusiveGateway', localName: 'exclusiveGateway', label: '排他网关', icon: '×', kind: 'gateway', width: 72, height: 72, palette: 'gateways' },
62
+ parallelGateway: { bpmnType: 'bpmn:ParallelGateway', localName: 'parallelGateway', label: '并行网关', icon: '+', kind: 'gateway', width: 72, height: 72, palette: 'gateways' },
63
+ inclusiveGateway: { bpmnType: 'bpmn:InclusiveGateway', localName: 'inclusiveGateway', label: '包容网关', icon: '○', kind: 'gateway', width: 72, height: 72, palette: 'gateways' },
64
+ eventBasedGateway: { bpmnType: 'bpmn:EventBasedGateway', localName: 'eventBasedGateway', label: '事件网关', icon: '◉', kind: 'gateway', width: 72, height: 72, palette: 'gateways' },
65
+ complexGateway: { bpmnType: 'bpmn:ComplexGateway', localName: 'complexGateway', label: '复杂网关', icon: '✣', kind: 'gateway', width: 72, height: 72, palette: 'gateways' },
66
+
67
+ // Data / artifacts / collaboration
68
+ dataObjectReference: { bpmnType: 'bpmn:DataObjectReference', localName: 'dataObjectReference', label: '数据对象', icon: '▤', kind: 'data', width: 72, height: 88, palette: 'data' },
69
+ dataStoreReference: { bpmnType: 'bpmn:DataStoreReference', localName: 'dataStoreReference', label: '数据存储', icon: '⌁', kind: 'dataStore', width: 78, height: 78, palette: 'data' },
70
+ textAnnotation: { bpmnType: 'bpmn:TextAnnotation', localName: 'textAnnotation', label: '文本注释', icon: 'T', kind: 'annotation', width: 180, height: 88, palette: 'artifacts' },
71
+ group: { bpmnType: 'bpmn:Group', localName: 'group', label: '分组', icon: '▦', kind: 'group', width: 300, height: 180, palette: 'artifacts' },
72
+ participant: { bpmnType: 'bpmn:Participant', localName: 'participant', label: '参与者 / Pool', icon: '▥', kind: 'participant', width: 760, height: 240, palette: 'collaboration' },
73
+ lane: { bpmnType: 'bpmn:Lane', localName: 'lane', label: '泳道 Lane', icon: '═', kind: 'lane', width: 700, height: 140, palette: 'collaboration' },
74
+
75
+ generic: { bpmnType: 'bpmn:Task', localName: 'task', label: 'BPMN 节点', icon: '◆', kind: 'task', width: 216, height: 72 },
76
+ };
77
+
78
+ export const PALETTE_GROUPS = [
79
+ { id: 'favorites', label: '常用', types: ['startEvent', 'userTask', 'serviceTask', 'exclusiveGateway', 'parallelGateway', 'subProcess', 'endEvent'] },
80
+ { id: 'startEvents', label: '开始事件', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'startEvents') },
81
+ { id: 'intermediateEvents', label: '中间事件', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'intermediateEvents') },
82
+ { id: 'boundaryEvents', label: '边界事件', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'boundaryEvents') },
83
+ { id: 'endEvents', label: '结束事件', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'endEvents') },
84
+ { id: 'tasks', label: '任务', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'tasks') },
85
+ { id: 'activities', label: '活动 / 子流程', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'activities') },
86
+ { id: 'gateways', label: '网关', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'gateways') },
87
+ { id: 'data', label: '数据', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'data') },
88
+ { id: 'artifacts', label: '注释 / 分组', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'artifacts') },
89
+ { id: 'collaboration', label: '协作 / 泳道', types: Object.keys(NODE_DEFINITIONS).filter((type) => NODE_DEFINITIONS[type]?.palette === 'collaboration') },
90
+ ];
91
+
92
+ export const EDGE_DEFINITIONS = {
93
+ sequenceFlow: { bpmnType: 'bpmn:SequenceFlow', label: '顺序流' },
94
+ messageFlow: { bpmnType: 'bpmn:MessageFlow', label: '消息流' },
95
+ association: { bpmnType: 'bpmn:Association', label: '关联' },
96
+ };
97
+
98
+ let idSeed = 0;
99
+ export function uid(prefix = 'Element') {
100
+ idSeed += 1;
101
+ return `${prefix}_${Date.now().toString(36)}_${idSeed.toString(36)}`;
102
+ }
103
+
104
+ function defaultNodeProperties(type, def, overrides = {}) {
105
+ const props = {
106
+ documentation: '',
107
+ loopType: 'none',
108
+ isForCompensation: false,
109
+ async: false,
110
+ exclusive: true,
111
+ asyncLeave: false,
112
+ failedJobRetryTimeCycle: '',
113
+ executionListeners: [],
114
+ loopCondition: '',
115
+ loopMaximum: '',
116
+ testBefore: false,
117
+ extensionAttributes: {},
118
+ };
119
+
120
+ if (type === 'userTask') Object.assign(props, { assignee: '', owner: '', candidateUsers: '', candidateGroups: '', formKey: '', dueDate: '', priority: '', category: '', skipExpression: '', taskListeners: [] });
121
+ if (['serviceTask', 'sendTask', 'businessRuleTask'].includes(type)) Object.assign(props, { implementationType: 'class', implementation: '', fields: [], resultVariable: '' });
122
+ if (type === 'sendTask') Object.assign(props, { messageRef: '', operationRef: '' });
123
+ if (type === 'receiveTask') Object.assign(props, { messageRef: '', operationRef: '', instantiate: false });
124
+ if (type === 'scriptTask') Object.assign(props, { scriptFormat: 'javascript', script: '', resultVariable: '' });
125
+ if (type === 'callActivity') Object.assign(props, { calledElement: '', businessKey: '', inheritBusinessKey: false, inheritVariables: false, sameDeployment: false, processInstanceName: '', inParameters: [], outParameters: [] });
126
+ if (def.eventDefinition === 'timer') Object.assign(props, { timerType: 'duration', timerValue: 'PT1H' });
127
+ if (def.eventDefinition === 'conditional') Object.assign(props, { condition: '${approved == true}' });
128
+ if (['message', 'signal', 'error', 'escalation', 'link'].includes(def.eventDefinition)) Object.assign(props, { eventRef: '' });
129
+ if ((def.eventStage === 'start' || (def.eventStage === 'intermediate' && def.eventRole === 'catch')) && ['message', 'signal'].includes(def.eventDefinition)) Object.assign(props, { parallelMultiple: false });
130
+ if (def.kind === 'boundary') Object.assign(props, { attachedToRef: '', cancelActivity: true });
131
+ if (type === 'eventSubProcess') Object.assign(props, { triggeredByEvent: true });
132
+ if (type === 'adHocSubProcess') Object.assign(props, { ordering: 'Parallel', adHocCompletionCondition: '', cancelRemainingInstances: true });
133
+ if (['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway', 'complexGateway'].includes(type)) Object.assign(props, { gatewayDirection: 'Unspecified' });
134
+ if (type === 'eventBasedGateway') Object.assign(props, { instantiate: false, eventGatewayType: 'Exclusive' });
135
+ if (type === 'complexGateway') Object.assign(props, { activationCondition: '' });
136
+ if (def.eventDefinition === 'compensate') Object.assign(props, { activityRef: '', waitForCompletion: true });
137
+ if (type === 'textAnnotation') Object.assign(props, { text: '说明文字', textFormat: 'text/plain' });
138
+ if (type === 'dataObjectReference') Object.assign(props, { dataObjectRef: '', dataState: '' });
139
+ if (type === 'dataStoreReference') Object.assign(props, { dataStoreRef: '', dataState: '' });
140
+ if (type === 'group') Object.assign(props, { categoryValue: '' });
141
+ if (type === 'participant') Object.assign(props, { processRef: '', swimlaneLabelPlacement: 'side' });
142
+ if (type === 'lane') Object.assign(props, { flowNodeRefs: [], swimlaneLabelPlacement: 'side' });
143
+
144
+ return { ...props, ...(overrides.properties || {}) };
145
+ }
146
+
147
+ export function createNode(type, x = 100, y = 100, overrides = {}) {
148
+ const def = NODE_DEFINITIONS[type] || NODE_DEFINITIONS.generic;
149
+ const base = {
150
+ id: uid(type),
151
+ type,
152
+ bpmnType: def.bpmnType,
153
+ name: overrides.name || def.label,
154
+ x,
155
+ y,
156
+ width: def.width,
157
+ height: def.height,
158
+ properties: defaultNodeProperties(type, def, overrides),
159
+ unknownAttributes: [],
160
+ extensionElements: [],
161
+ ...(['subProcess', 'eventSubProcess', 'transaction', 'adHocSubProcess'].includes(type) ? { isExpanded: false } : {}),
162
+ };
163
+ return { ...base, ...overrides, properties: base.properties };
164
+ }
165
+
166
+ export function createEdge(source, target, overrides = {}) {
167
+ return {
168
+ id: uid('Flow'),
169
+ type: 'sequenceFlow',
170
+ source,
171
+ target,
172
+ name: '',
173
+ condition: '',
174
+ isDefault: false,
175
+ waypoints: null,
176
+ routeStyle: 'rounded',
177
+ cornerRadius: 14,
178
+ unknownAttributes: [],
179
+ extensionElements: [],
180
+ properties: { documentation: '', executionListeners: [], extensionAttributes: {}, messageRef: '', associationDirection: 'None' },
181
+ ...overrides,
182
+ };
183
+ }
184
+
185
+ export function createEmptyProcess(engine = 'flowable') {
186
+ return {
187
+ id: 'Process_1',
188
+ name: '未命名流程',
189
+ engine,
190
+ isExecutable: true,
191
+ targetNamespace: 'urn:bpmn-nova:process',
192
+ namespaceDeclarations: {},
193
+ properties: {
194
+ documentation: '',
195
+ candidateStarterUsers: '',
196
+ candidateStarterGroups: '',
197
+ executionListeners: [],
198
+ extensionAttributes: {},
199
+ },
200
+ resources: { messages: [], signals: [], errors: [], escalations: [] },
201
+ extensionElements: [],
202
+ nodes: [],
203
+ edges: [],
204
+ settings: {
205
+ direction: 'horizontal',
206
+ layoutDensity: 'balanced',
207
+ edgeStyle: 'rounded',
208
+ cornerRadius: 14,
209
+ showGrid: true,
210
+ // gridSize controls the visual / optional grid. Dragging is pixel-smooth
211
+ // by default and uses magnetic alignment instead of coarse grid steps.
212
+ gridSize: 16,
213
+ snapToGrid: false,
214
+ alignmentGuides: true,
215
+ alignmentThreshold: 7,
216
+ mergeGatewayConnections: true,
217
+ },
218
+ };
219
+ }
220
+
221
+ export function cloneModel(model) {
222
+ return JSON.parse(JSON.stringify(model));
223
+ }
224
+
225
+ export function getNode(model, id) {
226
+ return model.nodes.find((node) => node.id === id) || null;
227
+ }
228
+
229
+ export function getEdge(model, id) {
230
+ return model.edges.find((edge) => edge.id === id) || null;
231
+ }
232
+
233
+ export function clearConnectedWaypoints(model, nodeId) {
234
+ for (const edge of model.edges) {
235
+ if (edge.source === nodeId || edge.target === nodeId) edge.waypoints = null;
236
+ }
237
+ }
238
+
239
+ export function createSampleProcess(engine = 'flowable') {
240
+ const model = createEmptyProcess(engine);
241
+ model.id = 'Process_PurchaseApproval';
242
+ model.name = '采购申请审批流程';
243
+
244
+ const start = createNode('startEvent', 96, 318, { id: 'StartEvent_1', name: '开始' });
245
+ const submit = createNode('userTask', 210, 310, {
246
+ id: 'UserTask_Submit',
247
+ name: '提交采购申请',
248
+ properties: { assignee: '${initiator}', candidateUsers: '', candidateGroups: '', formKey: 'purchase-form' },
249
+ });
250
+ const manager = createNode('userTask', 516, 310, {
251
+ id: 'UserTask_Manager',
252
+ name: '部门负责人审批',
253
+ properties: { assignee: '${deptManager}', candidateUsers: '', candidateGroups: 'department-manager', formKey: 'approval-form' },
254
+ });
255
+ const gateway = createNode('exclusiveGateway', 836, 310, { id: 'Gateway_Amount', name: '金额判断' });
256
+ const director = createNode('userTask', 1038, 170, {
257
+ id: 'UserTask_Director',
258
+ name: '总经理审批',
259
+ properties: { assignee: '${director}', candidateUsers: '', candidateGroups: 'director', formKey: 'approval-form' },
260
+ });
261
+ const finance = createNode('userTask', 1038, 446, {
262
+ id: 'UserTask_Finance',
263
+ name: '财务复核',
264
+ properties: { assignee: '${finance}', candidateUsers: '', candidateGroups: 'finance', formKey: 'finance-form' },
265
+ });
266
+ const merge = createNode('exclusiveGateway', 1352, 310, { id: 'Gateway_Merge', name: '汇合' });
267
+ const archive = createNode('serviceTask', 1540, 310, {
268
+ id: 'ServiceTask_Archive',
269
+ name: '生成订单并通知',
270
+ properties: { implementationType: 'class', implementation: 'com.example.workflow.CreateOrderDelegate' },
271
+ });
272
+ const end = createNode('endEvent', 1860, 318, { id: 'EndEvent_1', name: '结束' });
273
+
274
+ model.nodes.push(start, submit, manager, gateway, director, finance, merge, archive, end);
275
+ model.edges.push(
276
+ createEdge(start.id, submit.id, { id: 'Flow_Start_Submit' }),
277
+ createEdge(submit.id, manager.id, { id: 'Flow_Submit_Manager' }),
278
+ createEdge(manager.id, gateway.id, { id: 'Flow_Manager_Gateway' }),
279
+ createEdge(gateway.id, director.id, { id: 'Flow_To_Director', name: '金额 ≥ 50,000', condition: '${amount >= 50000}' }),
280
+ createEdge(gateway.id, finance.id, { id: 'Flow_To_Finance', name: '金额 < 50,000', condition: '${amount < 50000}', isDefault: true }),
281
+ createEdge(director.id, merge.id, { id: 'Flow_Director_Merge', name: '审批通过' }),
282
+ createEdge(finance.id, merge.id, { id: 'Flow_Finance_Merge', name: '复核完成' }),
283
+ createEdge(merge.id, archive.id, { id: 'Flow_Merge_Archive' }),
284
+ createEdge(archive.id, end.id, { id: 'Flow_Archive_End' }),
285
+ );
286
+ return model;
287
+ }
@@ -0,0 +1,360 @@
1
+ import { edgeWaypoints } from './layout.js';
2
+
3
+ const EPSILON = 0.01;
4
+ const DEFAULTS = Object.freeze({
5
+ nodeClearance: 16,
6
+ laneGap: 28,
7
+ laneSpacing: 20,
8
+ nearParallelDistance: 12,
9
+ anchorOverlapTolerance: 32,
10
+ maxLanesPerSide: 8,
11
+ });
12
+
13
+ function center(node) {
14
+ return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
15
+ }
16
+
17
+ function expandRect(rect, amount) {
18
+ return {
19
+ x: rect.x - amount,
20
+ y: rect.y - amount,
21
+ width: rect.width + amount * 2,
22
+ height: rect.height + amount * 2,
23
+ };
24
+ }
25
+
26
+ function segments(points = []) {
27
+ return points.slice(0, -1).map((start, index) => ({ start, end: points[index + 1] }));
28
+ }
29
+
30
+ function orientation(segment) {
31
+ if (Math.abs(segment.start.x - segment.end.x) < EPSILON) return 'vertical';
32
+ if (Math.abs(segment.start.y - segment.end.y) < EPSILON) return 'horizontal';
33
+ return 'diagonal';
34
+ }
35
+
36
+ function interval(segment) {
37
+ return orientation(segment) === 'vertical'
38
+ ? [Math.min(segment.start.y, segment.end.y), Math.max(segment.start.y, segment.end.y)]
39
+ : [Math.min(segment.start.x, segment.end.x), Math.max(segment.start.x, segment.end.x)];
40
+ }
41
+
42
+ function intervalOverlap(a, b) {
43
+ return Math.max(0, Math.min(a[1], b[1]) - Math.max(a[0], b[0]));
44
+ }
45
+
46
+ function segmentHitsRect(segment, rect) {
47
+ const kind = orientation(segment);
48
+ const right = rect.x + rect.width;
49
+ const bottom = rect.y + rect.height;
50
+ if (kind === 'vertical') {
51
+ return segment.start.x > rect.x && segment.start.x < right
52
+ && intervalOverlap(interval(segment), [rect.y, bottom]) > EPSILON;
53
+ }
54
+ if (kind === 'horizontal') {
55
+ return segment.start.y > rect.y && segment.start.y < bottom
56
+ && intervalOverlap(interval(segment), [rect.x, right]) > EPSILON;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function compactPoints(points) {
62
+ return points.filter((point, index) => {
63
+ if (!index) return true;
64
+ const previous = points[index - 1];
65
+ return Math.abs(point.x - previous.x) >= EPSILON || Math.abs(point.y - previous.y) >= EPSILON;
66
+ });
67
+ }
68
+
69
+ function labelRect(point, size = {}) {
70
+ const width = Math.max(0, Number(size.width) || 0);
71
+ const height = Math.max(0, Number(size.height) || 0);
72
+ return { x: point.x - width / 2, y: point.y - height / 2, width, height };
73
+ }
74
+
75
+ function rectsIntersect(a, b) {
76
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
77
+ }
78
+
79
+ function selectLabelPoint(points, obstacles, occupiedRoutes, size) {
80
+ const routeSegments = segments(points)
81
+ .map((segment, index) => ({
82
+ ...segment,
83
+ index,
84
+ kind: orientation(segment),
85
+ length: Math.hypot(segment.end.x - segment.start.x, segment.end.y - segment.start.y),
86
+ point: {
87
+ x: (segment.start.x + segment.end.x) / 2,
88
+ y: (segment.start.y + segment.end.y) / 2,
89
+ },
90
+ }))
91
+ .filter((segment) => segment.length > EPSILON);
92
+ const occupiedSegments = occupiedRoutes.flatMap((route) => segments(route.points || route));
93
+ const scored = routeSegments.map((segment) => {
94
+ const rect = labelRect(segment.point, size);
95
+ const obstacleConflicts = obstacles.filter((obstacle) => rectsIntersect(rect, obstacle)).length;
96
+ const lineConflicts = occupiedSegments.filter((occupied) => segmentHitsRect(occupied, rect)).length;
97
+ return {
98
+ ...segment,
99
+ conflicts: obstacleConflicts + lineConflicts,
100
+ fitPenalty: segment.length < ((segment.kind === 'horizontal' ? Number(size?.width) : Number(size?.height)) || 0) + 12 ? 1 : 0,
101
+ // Horizontal labels are easier to associate with a horizontal route segment.
102
+ orientationPenalty: segment.kind === 'horizontal' ? 0 : 1,
103
+ };
104
+ });
105
+ scored.sort((a, b) => a.conflicts - b.conflicts
106
+ || a.fitPenalty - b.fitPenalty
107
+ || a.orientationPenalty - b.orientationPenalty
108
+ || b.length - a.length
109
+ || a.index - b.index);
110
+ return scored[0] || { point: points[0], conflicts: 0 };
111
+ }
112
+
113
+ function crossing(a, b) {
114
+ const ak = orientation(a);
115
+ const bk = orientation(b);
116
+ if (ak === bk || ak === 'diagonal' || bk === 'diagonal') return false;
117
+ const vertical = ak === 'vertical' ? a : b;
118
+ const horizontal = ak === 'horizontal' ? a : b;
119
+ const vx = vertical.start.x;
120
+ const hy = horizontal.start.y;
121
+ const [vy1, vy2] = interval(vertical);
122
+ const [hx1, hx2] = interval(horizontal);
123
+ return vx > hx1 + EPSILON && vx < hx2 - EPSILON && hy > vy1 + EPSILON && hy < vy2 - EPSILON;
124
+ }
125
+
126
+ function routeMetrics(points, occupiedRoutes, obstacles, config) {
127
+ const routeSegments = segments(points);
128
+ const occupiedSegments = occupiedRoutes.flatMap((route) => segments(route.points || route));
129
+ let overlapLength = 0;
130
+ let nearParallelLength = 0;
131
+ let crossings = 0;
132
+ for (const candidate of routeSegments) {
133
+ const candidateKind = orientation(candidate);
134
+ for (const occupied of occupiedSegments) {
135
+ const occupiedKind = orientation(occupied);
136
+ if (candidateKind === occupiedKind && ['horizontal', 'vertical'].includes(candidateKind)) {
137
+ const axisDistance = candidateKind === 'vertical'
138
+ ? Math.abs(candidate.start.x - occupied.start.x)
139
+ : Math.abs(candidate.start.y - occupied.start.y);
140
+ const shared = intervalOverlap(interval(candidate), interval(occupied));
141
+ if (axisDistance < EPSILON) overlapLength += Math.max(0, shared - config.anchorOverlapTolerance);
142
+ else if (axisDistance < config.nearParallelDistance) nearParallelLength += shared;
143
+ } else if (crossing(candidate, occupied)) crossings += 1;
144
+ }
145
+ }
146
+ const nodeHits = obstacles.reduce((count, rect) => count + (routeSegments.some((segment) => segmentHitsRect(segment, rect)) ? 1 : 0), 0);
147
+ const length = routeSegments.reduce((sum, segment) => sum + Math.hypot(segment.end.x - segment.start.x, segment.end.y - segment.start.y), 0);
148
+ return { nodeHits, overlapLength, nearParallelLength, crossings, bends: Math.max(0, points.length - 2), length };
149
+ }
150
+
151
+ function compareCandidates(a, b) {
152
+ const fields = ['nodeHits', 'portConflicts', 'overlapLength', 'labelConflicts', 'nearParallelLength', 'crossings', 'strategyPenalty', 'bends', 'length', 'order'];
153
+ for (const field of fields) {
154
+ const delta = (a[field] || 0) - (b[field] || 0);
155
+ if (Math.abs(delta) > EPSILON) return delta;
156
+ }
157
+ return 0;
158
+ }
159
+
160
+ function sideAnchor(node, side) {
161
+ if (side === 'left') return { x: node.x, y: node.y + node.height / 2 };
162
+ if (side === 'right') return { x: node.x + node.width, y: node.y + node.height / 2 };
163
+ if (side === 'top') return { x: node.x + node.width / 2, y: node.y };
164
+ return { x: node.x + node.width / 2, y: node.y + node.height };
165
+ }
166
+
167
+ function pointAnchorSide(node, point, tolerance = 2) {
168
+ if (!node || !point) return null;
169
+ const withinX = point.x >= node.x - tolerance && point.x <= node.x + node.width + tolerance;
170
+ const withinY = point.y >= node.y - tolerance && point.y <= node.y + node.height + tolerance;
171
+ const candidates = [
172
+ { side: 'left', distance: Math.abs(point.x - node.x), valid: withinY },
173
+ { side: 'right', distance: Math.abs(point.x - (node.x + node.width)), valid: withinY },
174
+ { side: 'top', distance: Math.abs(point.y - node.y), valid: withinX },
175
+ { side: 'bottom', distance: Math.abs(point.y - (node.y + node.height)), valid: withinX },
176
+ ].filter((candidate) => candidate.valid && candidate.distance <= tolerance);
177
+ candidates.sort((a, b) => a.distance - b.distance);
178
+ return candidates[0]?.side || null;
179
+ }
180
+
181
+ function sequenceFlowPortUsage(model, edges) {
182
+ const usage = new Map();
183
+ const add = (nodeId, side) => {
184
+ if (!side) return;
185
+ const key = `${nodeId}:${side}`;
186
+ usage.set(key, (usage.get(key) || 0) + 1);
187
+ };
188
+ for (const edge of edges) {
189
+ if ((edge.type || 'sequenceFlow') !== 'sequenceFlow') continue;
190
+ const points = edgeWaypoints(model, edge);
191
+ if (points.length < 2) continue;
192
+ const source = model.nodes.find((node) => node.id === edge.source);
193
+ const target = model.nodes.find((node) => node.id === edge.target);
194
+ add(edge.source, pointAnchorSide(source, points[0]));
195
+ add(edge.target, pointAnchorSide(target, points.at(-1)));
196
+ }
197
+ return usage;
198
+ }
199
+
200
+ function candidatePortConflicts(candidate, source, target, portUsage) {
201
+ return (portUsage.get(`${source.id}:${candidate.sourceSide}`) || 0)
202
+ + (portUsage.get(`${target.id}:${candidate.targetSide}`) || 0);
203
+ }
204
+
205
+ function outerSameSideCandidate(source, target, bounds, direction, side, laneIndex, config, leadDistance = config.laneGap) {
206
+ const start = sideAnchor(source, side);
207
+ const end = sideAnchor(target, side);
208
+ const laneOffset = config.laneGap + laneIndex * config.laneSpacing;
209
+ if (direction === 'vertical') {
210
+ const sign = side === 'left' ? -1 : 1;
211
+ const lane = side === 'left' ? bounds.left - laneOffset : bounds.right + laneOffset;
212
+ const sourceLead = { x: start.x + sign * leadDistance, y: start.y };
213
+ const targetLead = { x: end.x + sign * leadDistance, y: end.y };
214
+ return {
215
+ points: compactPoints([start, sourceLead, { x: lane, y: sourceLead.y }, { x: lane, y: targetLead.y }, targetLead, end]),
216
+ laneSide: side,
217
+ laneIndex,
218
+ sourceSide: side,
219
+ targetSide: side,
220
+ anchorStrategy: 'outer-same-side',
221
+ strategyPenalty: 0,
222
+ };
223
+ }
224
+ const sign = side === 'top' ? -1 : 1;
225
+ const lane = side === 'top' ? bounds.top - laneOffset : bounds.bottom + laneOffset;
226
+ const sourceLead = { x: start.x, y: start.y + sign * leadDistance };
227
+ const targetLead = { x: end.x, y: end.y + sign * leadDistance };
228
+ return {
229
+ points: compactPoints([start, sourceLead, { x: sourceLead.x, y: lane }, { x: targetLead.x, y: lane }, targetLead, end]),
230
+ laneSide: side,
231
+ laneIndex,
232
+ sourceSide: side,
233
+ targetSide: side,
234
+ anchorStrategy: 'outer-same-side',
235
+ strategyPenalty: 0,
236
+ };
237
+ }
238
+
239
+ function inferDirection(model, source, target) {
240
+ if (['horizontal', 'vertical'].includes(model?.settings?.direction)) return model.settings.direction;
241
+ const s = center(source);
242
+ const t = center(target);
243
+ return Math.abs(s.y - t.y) >= Math.abs(s.x - t.x) ? 'vertical' : 'horizontal';
244
+ }
245
+
246
+ function verticalCandidate(source, target, bounds, side, laneIndex, config, leadDistance = config.laneGap) {
247
+ const s = center(source);
248
+ const t = center(target);
249
+ const backwards = s.y >= t.y;
250
+ const start = backwards ? { x: s.x, y: source.y } : { x: s.x, y: source.y + source.height };
251
+ const end = backwards ? { x: t.x, y: target.y + target.height } : { x: t.x, y: target.y };
252
+ const sourceLead = { x: start.x, y: start.y + (backwards ? -leadDistance : leadDistance) };
253
+ const targetLead = { x: end.x, y: end.y + (backwards ? leadDistance : -leadDistance) };
254
+ const laneOffset = config.laneGap + laneIndex * config.laneSpacing;
255
+ const lane = side === 'left'
256
+ ? bounds.left - laneOffset
257
+ : bounds.right + laneOffset;
258
+ const points = compactPoints([start, sourceLead, { x: lane, y: sourceLead.y }, { x: lane, y: targetLead.y }, targetLead, end]);
259
+ return {
260
+ points,
261
+ laneSide: side,
262
+ laneIndex,
263
+ sourceSide: backwards ? 'top' : 'bottom',
264
+ targetSide: backwards ? 'bottom' : 'top',
265
+ anchorStrategy: 'flow-axis-fallback',
266
+ strategyPenalty: 1,
267
+ };
268
+ }
269
+
270
+ function horizontalCandidate(source, target, bounds, side, laneIndex, config, leadDistance = config.laneGap) {
271
+ const s = center(source);
272
+ const t = center(target);
273
+ const backwards = s.x >= t.x;
274
+ const start = backwards ? { x: source.x, y: s.y } : { x: source.x + source.width, y: s.y };
275
+ const end = backwards ? { x: target.x + target.width, y: t.y } : { x: target.x, y: t.y };
276
+ const sourceLead = { x: start.x + (backwards ? -leadDistance : leadDistance), y: start.y };
277
+ const targetLead = { x: end.x + (backwards ? leadDistance : -leadDistance), y: end.y };
278
+ const laneOffset = config.laneGap + laneIndex * config.laneSpacing;
279
+ const lane = side === 'top'
280
+ ? bounds.top - laneOffset
281
+ : bounds.bottom + laneOffset;
282
+ const points = compactPoints([start, sourceLead, { x: sourceLead.x, y: lane }, { x: targetLead.x, y: lane }, targetLead, end]);
283
+ return {
284
+ points,
285
+ laneSide: side,
286
+ laneIndex,
287
+ sourceSide: backwards ? 'left' : 'right',
288
+ targetSide: backwards ? 'right' : 'left',
289
+ anchorStrategy: 'flow-axis-fallback',
290
+ strategyPenalty: 1,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Routes a runtime-only rejection/return transition without mutating the BPMN model.
296
+ */
297
+ export function routeRuntimeTransition(model, transition, options = {}) {
298
+ const source = model?.nodes?.find((node) => node.id === transition?.sourceElementId);
299
+ const target = model?.nodes?.find((node) => node.id === transition?.targetElementId);
300
+ if (!source || !target) return null;
301
+ const sourceScopeId = source.scopeId || model.id;
302
+ if ((target.scopeId || model.id) !== sourceScopeId) return null;
303
+ const config = { ...DEFAULTS, ...options };
304
+ const direction = inferDirection(model, source, target);
305
+ const scopedNodes = (model.nodes || []).filter((node) => (node.scopeId || model.id) === sourceScopeId);
306
+ const scopedEdges = (model.edges || []).filter((edge) => (edge.scopeId || model.id) === sourceScopeId);
307
+ const portUsage = sequenceFlowPortUsage(model, scopedEdges);
308
+ const occupiedRoutes = [
309
+ ...scopedEdges.map((edge) => edgeWaypoints(model, edge)),
310
+ ...(options.occupiedRoutes || []),
311
+ ];
312
+ const obstacles = [
313
+ ...scopedNodes
314
+ .filter((node) => node.id !== source.id && node.id !== target.id)
315
+ .map((node) => expandRect(node, config.nodeClearance)),
316
+ ...(options.obstacles || []),
317
+ ];
318
+ const bounds = scopedNodes.reduce((result, node) => ({
319
+ left: Math.min(result.left, node.x),
320
+ top: Math.min(result.top, node.y),
321
+ right: Math.max(result.right, node.x + node.width),
322
+ bottom: Math.max(result.bottom, node.y + node.height),
323
+ }), { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity });
324
+ const labelObstacles = [
325
+ ...obstacles,
326
+ expandRect(source, config.nodeClearance),
327
+ expandRect(target, config.nodeClearance),
328
+ ];
329
+ const sides = direction === 'vertical' ? ['left', 'right'] : ['top', 'bottom'];
330
+ const candidates = [];
331
+ let order = 0;
332
+ const leadDistances = [config.laneGap, config.laneGap / 2, 0];
333
+ for (const side of sides) {
334
+ for (let laneIndex = 0; laneIndex < config.maxLanesPerSide; laneIndex += 1) {
335
+ for (const leadDistance of leadDistances) {
336
+ const strategyCandidates = [
337
+ outerSameSideCandidate(source, target, bounds, direction, side, laneIndex, config, leadDistance),
338
+ direction === 'vertical'
339
+ ? verticalCandidate(source, target, bounds, side, laneIndex, config, leadDistance)
340
+ : horizontalCandidate(source, target, bounds, side, laneIndex, config, leadDistance),
341
+ ];
342
+ for (const candidate of strategyCandidates) {
343
+ candidate.portConflicts = candidatePortConflicts(candidate, source, target, portUsage);
344
+ const label = selectLabelPoint(candidate.points, labelObstacles, occupiedRoutes, options.labelSize);
345
+ const labelConflicts = label.conflicts;
346
+ candidates.push({
347
+ ...candidate,
348
+ labelPoint: label.point,
349
+ ...routeMetrics(candidate.points, occupiedRoutes, obstacles, config),
350
+ labelConflicts,
351
+ order,
352
+ });
353
+ order += 1;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ const selected = candidates.filter((candidate) => candidate.nodeHits === 0).sort(compareCandidates)[0];
359
+ return selected ? { ...selected, direction } : null;
360
+ }