@bpmnkit/core 0.0.8

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 (104) hide show
  1. package/README.md +185 -0
  2. package/dist/bpmn/auto-layout.d.ts +10 -0
  3. package/dist/bpmn/auto-layout.js +203 -0
  4. package/dist/bpmn/bpmn-builder.d.ts +381 -0
  5. package/dist/bpmn/bpmn-builder.js +1223 -0
  6. package/dist/bpmn/bpmn-model.d.ts +403 -0
  7. package/dist/bpmn/bpmn-model.js +2 -0
  8. package/dist/bpmn/bpmn-parser.d.ts +4 -0
  9. package/dist/bpmn/bpmn-parser.js +642 -0
  10. package/dist/bpmn/bpmn-serializer.d.ts +4 -0
  11. package/dist/bpmn/bpmn-serializer.js +515 -0
  12. package/dist/bpmn/compact.d.ts +91 -0
  13. package/dist/bpmn/compact.js +349 -0
  14. package/dist/bpmn/di-color.d.ts +9 -0
  15. package/dist/bpmn/di-color.js +32 -0
  16. package/dist/bpmn/index.d.ts +105 -0
  17. package/dist/bpmn/index.js +176 -0
  18. package/dist/bpmn/optimize/feel.d.ts +4 -0
  19. package/dist/bpmn/optimize/feel.js +186 -0
  20. package/dist/bpmn/optimize/flow.d.ts +4 -0
  21. package/dist/bpmn/optimize/flow.js +323 -0
  22. package/dist/bpmn/optimize/index.d.ts +5 -0
  23. package/dist/bpmn/optimize/index.js +51 -0
  24. package/dist/bpmn/optimize/naming.d.ts +8 -0
  25. package/dist/bpmn/optimize/naming.js +145 -0
  26. package/dist/bpmn/optimize/tasks.d.ts +4 -0
  27. package/dist/bpmn/optimize/tasks.js +273 -0
  28. package/dist/bpmn/optimize/types.d.ts +44 -0
  29. package/dist/bpmn/optimize/types.js +2 -0
  30. package/dist/bpmn/optimize/utils.d.ts +23 -0
  31. package/dist/bpmn/optimize/utils.js +129 -0
  32. package/dist/bpmn/rest-connector.d.ts +48 -0
  33. package/dist/bpmn/rest-connector.js +69 -0
  34. package/dist/bpmn/svg.d.ts +35 -0
  35. package/dist/bpmn/svg.js +557 -0
  36. package/dist/bpmn/type-guards.d.ts +136 -0
  37. package/dist/bpmn/type-guards.js +211 -0
  38. package/dist/bpmn/utils.d.ts +88 -0
  39. package/dist/bpmn/utils.js +207 -0
  40. package/dist/bpmn/zeebe-extensions.d.ts +72 -0
  41. package/dist/bpmn/zeebe-extensions.js +96 -0
  42. package/dist/dmn/compact.d.ts +44 -0
  43. package/dist/dmn/compact.js +125 -0
  44. package/dist/dmn/dmn-builder.d.ts +59 -0
  45. package/dist/dmn/dmn-builder.js +144 -0
  46. package/dist/dmn/dmn-layout.d.ts +57 -0
  47. package/dist/dmn/dmn-layout.js +256 -0
  48. package/dist/dmn/dmn-model.d.ts +158 -0
  49. package/dist/dmn/dmn-model.js +2 -0
  50. package/dist/dmn/dmn-parser.d.ts +4 -0
  51. package/dist/dmn/dmn-parser.js +275 -0
  52. package/dist/dmn/dmn-serializer.d.ts +4 -0
  53. package/dist/dmn/dmn-serializer.js +296 -0
  54. package/dist/dmn/index.d.ts +28 -0
  55. package/dist/dmn/index.js +51 -0
  56. package/dist/errors.d.ts +77 -0
  57. package/dist/errors.js +66 -0
  58. package/dist/form/compact.d.ts +20 -0
  59. package/dist/form/compact.js +146 -0
  60. package/dist/form/form-builder.d.ts +111 -0
  61. package/dist/form/form-builder.js +347 -0
  62. package/dist/form/form-model.d.ts +221 -0
  63. package/dist/form/form-model.js +2 -0
  64. package/dist/form/form-parser.d.ts +4 -0
  65. package/dist/form/form-parser.js +514 -0
  66. package/dist/form/form-serializer.d.ts +4 -0
  67. package/dist/form/form-serializer.js +240 -0
  68. package/dist/form/index.d.ts +27 -0
  69. package/dist/form/index.js +43 -0
  70. package/dist/index.d.ts +37 -0
  71. package/dist/index.js +19 -0
  72. package/dist/layout/bench.d.ts +86 -0
  73. package/dist/layout/bench.js +225 -0
  74. package/dist/layout/coordinates.d.ts +62 -0
  75. package/dist/layout/coordinates.js +868 -0
  76. package/dist/layout/crossing.d.ts +8 -0
  77. package/dist/layout/crossing.js +60 -0
  78. package/dist/layout/graph.d.ts +28 -0
  79. package/dist/layout/graph.js +126 -0
  80. package/dist/layout/index.d.ts +7 -0
  81. package/dist/layout/index.js +5 -0
  82. package/dist/layout/layers.d.ts +13 -0
  83. package/dist/layout/layers.js +49 -0
  84. package/dist/layout/layout-engine.d.ts +21 -0
  85. package/dist/layout/layout-engine.js +191 -0
  86. package/dist/layout/overlap.d.ts +10 -0
  87. package/dist/layout/overlap.js +70 -0
  88. package/dist/layout/routing.d.ts +26 -0
  89. package/dist/layout/routing.js +439 -0
  90. package/dist/layout/subprocess.d.ts +14 -0
  91. package/dist/layout/subprocess.js +77 -0
  92. package/dist/layout/types.d.ts +72 -0
  93. package/dist/layout/types.js +37 -0
  94. package/dist/types/id-generator.d.ts +8 -0
  95. package/dist/types/id-generator.js +31 -0
  96. package/dist/types/index.d.ts +3 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/xml-element.d.ts +12 -0
  99. package/dist/types/xml-element.js +2 -0
  100. package/dist/xml/index.d.ts +2 -0
  101. package/dist/xml/index.js +2 -0
  102. package/dist/xml/xml-parser.d.ts +13 -0
  103. package/dist/xml/xml-parser.js +271 -0
  104. package/package.json +38 -0
@@ -0,0 +1,439 @@
1
+ import { LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET } from "./types.js";
2
+ const GATEWAY_TYPES = new Set([
3
+ "exclusiveGateway",
4
+ "parallelGateway",
5
+ "inclusiveGateway",
6
+ "eventBasedGateway",
7
+ ]);
8
+ /**
9
+ * Determine which side of the target a forward edge should connect to.
10
+ * Non-gateway targets always receive edges from the left side.
11
+ * Split gateways (starting): incoming always from the left.
12
+ * Join gateways (closing): incoming based on relative position (top/bottom/left).
13
+ */
14
+ export function resolveTargetPort(source, target, joinGateways) {
15
+ if (!GATEWAY_TYPES.has(target.type)) {
16
+ return "left";
17
+ }
18
+ // Split/starting gateways always receive from left
19
+ if (!joinGateways.has(target.id)) {
20
+ return "left";
21
+ }
22
+ // Join/closing gateways: connect based on relative position
23
+ const srcCy = source.bounds.y + source.bounds.height / 2;
24
+ const tgtCy = target.bounds.y + target.bounds.height / 2;
25
+ if (Math.abs(srcCy - tgtCy) <= 1) {
26
+ return "left";
27
+ }
28
+ return srcCy < tgtCy ? "top" : "bottom";
29
+ }
30
+ /**
31
+ * Assign source ports for outgoing edges of a gateway.
32
+ * - Single output: right port.
33
+ * - Odd count: middle (by target y) → right, upper half → top, lower half → bottom.
34
+ * - Even count: upper half → top, lower half → bottom, no right port.
35
+ */
36
+ export function assignGatewayPorts(outgoingFlows, nodeMap) {
37
+ const portMap = new Map();
38
+ const count = outgoingFlows.length;
39
+ if (count === 0)
40
+ return portMap;
41
+ if (count === 1) {
42
+ const first = outgoingFlows[0];
43
+ if (first)
44
+ portMap.set(first.id, "right");
45
+ return portMap;
46
+ }
47
+ // Sort flows by target's center-y (ascending = topmost first)
48
+ const sorted = [...outgoingFlows].sort((a, b) => {
49
+ const targetA = nodeMap.get(a.targetRef);
50
+ const targetB = nodeMap.get(b.targetRef);
51
+ const yA = targetA ? targetA.bounds.y + targetA.bounds.height / 2 : 0;
52
+ const yB = targetB ? targetB.bounds.y + targetB.bounds.height / 2 : 0;
53
+ return yA - yB;
54
+ });
55
+ if (count % 2 === 1) {
56
+ const midIndex = Math.floor(count / 2);
57
+ for (let i = 0; i < sorted.length; i++) {
58
+ const flow = sorted[i];
59
+ if (!flow)
60
+ continue;
61
+ if (i < midIndex) {
62
+ portMap.set(flow.id, "top");
63
+ }
64
+ else if (i === midIndex) {
65
+ portMap.set(flow.id, "right");
66
+ }
67
+ else {
68
+ portMap.set(flow.id, "bottom");
69
+ }
70
+ }
71
+ }
72
+ else {
73
+ const midIndex = count / 2;
74
+ for (let i = 0; i < sorted.length; i++) {
75
+ const flow = sorted[i];
76
+ if (!flow)
77
+ continue;
78
+ portMap.set(flow.id, i < midIndex ? "top" : "bottom");
79
+ }
80
+ }
81
+ return portMap;
82
+ }
83
+ /**
84
+ * Route edges with orthogonal (horizontal + vertical) segments.
85
+ * Forward edges go left-to-right; back-edges route above or below.
86
+ * Gateway sources use port-based routing (top/right/bottom).
87
+ */
88
+ export function routeEdges(sequenceFlows, nodeMap, backEdges) {
89
+ const backEdgeIds = new Set(backEdges.map((be) => be.flowId));
90
+ // Group forward flows by source for gateway port assignment
91
+ const forwardFlowsBySource = new Map();
92
+ const forwardIncomingCount = new Map();
93
+ for (const flow of sequenceFlows) {
94
+ if (backEdgeIds.has(flow.id))
95
+ continue;
96
+ let bucket = forwardFlowsBySource.get(flow.sourceRef);
97
+ if (!bucket) {
98
+ bucket = [];
99
+ forwardFlowsBySource.set(flow.sourceRef, bucket);
100
+ }
101
+ bucket.push(flow);
102
+ forwardIncomingCount.set(flow.targetRef, (forwardIncomingCount.get(flow.targetRef) ?? 0) + 1);
103
+ }
104
+ // Identify join gateways (gateways with multiple incoming forward edges)
105
+ const joinGateways = new Set();
106
+ for (const [targetId, count] of forwardIncomingCount) {
107
+ if (count >= 2) {
108
+ const node = nodeMap.get(targetId);
109
+ if (node && GATEWAY_TYPES.has(node.type)) {
110
+ joinGateways.add(targetId);
111
+ }
112
+ }
113
+ }
114
+ // Assign ports for gateway sources
115
+ const portAssignments = new Map();
116
+ for (const [sourceId, flows] of forwardFlowsBySource) {
117
+ const source = nodeMap.get(sourceId);
118
+ if (!source || !GATEWAY_TYPES.has(source.type))
119
+ continue;
120
+ const ports = assignGatewayPorts(flows, nodeMap);
121
+ for (const [flowId, port] of ports) {
122
+ portAssignments.set(flowId, port);
123
+ }
124
+ }
125
+ const edges = [];
126
+ for (const flow of sequenceFlows) {
127
+ const source = nodeMap.get(flow.sourceRef);
128
+ const target = nodeMap.get(flow.targetRef);
129
+ if (!source || !target)
130
+ continue;
131
+ const isBackEdge = backEdgeIds.has(flow.id);
132
+ let waypoints;
133
+ if (isBackEdge) {
134
+ waypoints = routeBackEdge(source, target, nodeMap);
135
+ }
136
+ else {
137
+ const port = portAssignments.get(flow.id);
138
+ waypoints = port
139
+ ? routeFromPort(source, target, port, joinGateways)
140
+ : routeForwardEdge(source, target, joinGateways);
141
+ }
142
+ edges.push({
143
+ id: flow.id,
144
+ sourceRef: flow.sourceRef,
145
+ targetRef: flow.targetRef,
146
+ waypoints,
147
+ label: flow.name,
148
+ labelBounds: undefined,
149
+ });
150
+ }
151
+ // Collision-aware label placement
152
+ placeEdgeLabels(edges, nodeMap);
153
+ return edges;
154
+ }
155
+ /** Route a forward edge with orthogonal segments, preferring L-shaped over Z-shaped. */
156
+ function routeForwardEdge(source, target, joinGateways) {
157
+ const targetPort = resolveTargetPort(source, target, joinGateways);
158
+ if (targetPort === "top" || targetPort === "bottom") {
159
+ const sourceRight = source.bounds.x + source.bounds.width;
160
+ const sourceCenterY = source.bounds.y + source.bounds.height / 2;
161
+ const tgtX = target.bounds.x + target.bounds.width / 2;
162
+ const tgtY = targetPort === "top" ? target.bounds.y : target.bounds.y + target.bounds.height;
163
+ return [
164
+ { x: sourceRight, y: sourceCenterY },
165
+ { x: tgtX, y: sourceCenterY },
166
+ { x: tgtX, y: tgtY },
167
+ ];
168
+ }
169
+ const sourceRight = source.bounds.x + source.bounds.width;
170
+ const sourceCenterY = source.bounds.y + source.bounds.height / 2;
171
+ const targetLeft = target.bounds.x;
172
+ const targetCenterY = target.bounds.y + target.bounds.height / 2;
173
+ // Same vertical position: straight horizontal line
174
+ if (Math.abs(sourceCenterY - targetCenterY) < 1) {
175
+ return [
176
+ { x: sourceRight, y: sourceCenterY },
177
+ { x: targetLeft, y: targetCenterY },
178
+ ];
179
+ }
180
+ // Different vertical positions: prefer L-shaped routing
181
+ // L-shape option 1: horizontal to target's X, then vertical down/up
182
+ // L-shape option 2: vertical to target's Y, then horizontal to target
183
+ // For left-to-right flow, option 1 (horizontal first, then vertical into target) is cleaner
184
+ return [
185
+ { x: sourceRight, y: sourceCenterY },
186
+ { x: targetLeft, y: sourceCenterY },
187
+ { x: targetLeft, y: targetCenterY },
188
+ ];
189
+ }
190
+ /** Count the number of direction changes (bends) in a waypoint sequence. */
191
+ function countBends(waypoints) {
192
+ let bends = 0;
193
+ for (let i = 1; i < waypoints.length - 1; i++) {
194
+ const prev = waypoints[i - 1];
195
+ const curr = waypoints[i];
196
+ const next = waypoints[i + 1];
197
+ if (!prev || !curr || !next)
198
+ continue;
199
+ const dx1 = curr.x - prev.x;
200
+ const dy1 = curr.y - prev.y;
201
+ const dx2 = next.x - curr.x;
202
+ const dy2 = next.y - curr.y;
203
+ // Direction changes when we go from horizontal to vertical or vice versa
204
+ if ((Math.abs(dx1) > 0.1 && Math.abs(dy2) > 0.1) ||
205
+ (Math.abs(dy1) > 0.1 && Math.abs(dx2) > 0.1)) {
206
+ bends++;
207
+ }
208
+ }
209
+ return bends;
210
+ }
211
+ /** Route a forward edge from a specific port side on the source node, choosing minimum bends. */
212
+ function routeFromPort(source, target, port, joinGateways) {
213
+ if (port === "right") {
214
+ return routeForwardEdge(source, target, joinGateways);
215
+ }
216
+ // Generate candidate routes: assigned port route + right-port alternative
217
+ const portRoute = routeFromPortDirect(source, target, port, joinGateways);
218
+ const rightRoute = routeForwardEdge(source, target, joinGateways);
219
+ const portBends = countBends(portRoute);
220
+ const rightBends = countBends(rightRoute);
221
+ // Prefer the assigned port route unless right route has strictly fewer bends
222
+ return rightBends < portBends ? rightRoute : portRoute;
223
+ }
224
+ /** Route directly from top/bottom port, preferring L-shaped path. */
225
+ function routeFromPortDirect(source, target, port, joinGateways) {
226
+ const targetPort = resolveTargetPort(source, target, joinGateways);
227
+ const srcX = source.bounds.x + source.bounds.width / 2;
228
+ const srcY = port === "top" ? source.bounds.y : source.bounds.y + source.bounds.height;
229
+ if (targetPort === "top" || targetPort === "bottom") {
230
+ const tgtX = target.bounds.x + target.bounds.width / 2;
231
+ const tgtY = targetPort === "top" ? target.bounds.y : target.bounds.y + target.bounds.height;
232
+ if (Math.abs(srcX - tgtX) < 1) {
233
+ return [
234
+ { x: srcX, y: srcY },
235
+ { x: tgtX, y: tgtY },
236
+ ];
237
+ }
238
+ // L-shape: vertical to target Y, then horizontal to target X
239
+ return [
240
+ { x: srcX, y: srcY },
241
+ { x: srcX, y: tgtY },
242
+ { x: tgtX, y: tgtY },
243
+ ];
244
+ }
245
+ const targetLeft = target.bounds.x;
246
+ const targetCenterY = target.bounds.y + target.bounds.height / 2;
247
+ // Same vertical position as target: straight horizontal
248
+ if (Math.abs(srcY - targetCenterY) < 1) {
249
+ return [
250
+ { x: srcX, y: srcY },
251
+ { x: targetLeft, y: targetCenterY },
252
+ ];
253
+ }
254
+ // L-shape: vertical to target's center-Y, then horizontal to target
255
+ return [
256
+ { x: srcX, y: srcY },
257
+ { x: srcX, y: targetCenterY },
258
+ { x: targetLeft, y: targetCenterY },
259
+ ];
260
+ }
261
+ /**
262
+ * Route a back-edge (loop) above or below all nodes, choosing the shorter path.
263
+ */
264
+ function routeBackEdge(source, target, nodeMap) {
265
+ let minY = Number.POSITIVE_INFINITY;
266
+ let maxY = Number.NEGATIVE_INFINITY;
267
+ for (const node of nodeMap.values()) {
268
+ const top = node.bounds.y - (node.labelBounds ? node.labelBounds.height + 8 : 0);
269
+ if (top < minY)
270
+ minY = top;
271
+ const bottom = node.bounds.y + node.bounds.height;
272
+ if (bottom > maxY)
273
+ maxY = bottom;
274
+ }
275
+ const sourceRight = source.bounds.x + source.bounds.width;
276
+ const sourceCenterY = source.bounds.y + source.bounds.height / 2;
277
+ const targetLeft = target.bounds.x;
278
+ const targetCenterY = target.bounds.y + target.bounds.height / 2;
279
+ // Route above
280
+ const routeAboveY = minY - 30;
281
+ const aboveRoute = [
282
+ { x: sourceRight, y: sourceCenterY },
283
+ { x: sourceRight + 20, y: sourceCenterY },
284
+ { x: sourceRight + 20, y: routeAboveY },
285
+ { x: targetLeft - 20, y: routeAboveY },
286
+ { x: targetLeft - 20, y: targetCenterY },
287
+ { x: targetLeft, y: targetCenterY },
288
+ ];
289
+ // Route below
290
+ const routeBelowY = maxY + 30;
291
+ const belowRoute = [
292
+ { x: sourceRight, y: sourceCenterY },
293
+ { x: sourceRight + 20, y: sourceCenterY },
294
+ { x: sourceRight + 20, y: routeBelowY },
295
+ { x: targetLeft - 20, y: routeBelowY },
296
+ { x: targetLeft - 20, y: targetCenterY },
297
+ { x: targetLeft, y: targetCenterY },
298
+ ];
299
+ // Compare total path length and pick shorter
300
+ const aboveLen = pathLength(aboveRoute);
301
+ const belowLen = pathLength(belowRoute);
302
+ return belowLen < aboveLen ? belowRoute : aboveRoute;
303
+ }
304
+ function pathLength(waypoints) {
305
+ let len = 0;
306
+ for (let i = 1; i < waypoints.length; i++) {
307
+ const a = waypoints[i - 1];
308
+ const b = waypoints[i];
309
+ if (!a || !b)
310
+ continue;
311
+ len += Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
312
+ }
313
+ return len;
314
+ }
315
+ /** Collision tolerance in pixels — small overlap allowed for rounding. */
316
+ const LABEL_COLLISION_TOLERANCE = 2;
317
+ /** Number of slide steps along a segment when searching for clear space. */
318
+ const LABEL_SLIDE_STEPS = 10;
319
+ function boundsOverlap(a, b) {
320
+ return !(a.x + a.width + LABEL_COLLISION_TOLERANCE <= b.x ||
321
+ b.x + b.width + LABEL_COLLISION_TOLERANCE <= a.x ||
322
+ a.y + a.height + LABEL_COLLISION_TOLERANCE <= b.y ||
323
+ b.y + b.height + LABEL_COLLISION_TOLERANCE <= a.y);
324
+ }
325
+ /**
326
+ * Collision-aware edge label placement.
327
+ * For each labeled edge, generates candidate positions on the longest segment
328
+ * and picks the first one that doesn't overlap nodes or already-placed labels.
329
+ */
330
+ function placeEdgeLabels(edges, nodeMap) {
331
+ const occupied = [];
332
+ // Collect all node bounds as obstacles
333
+ for (const node of nodeMap.values()) {
334
+ occupied.push(node.bounds);
335
+ if (node.labelBounds)
336
+ occupied.push(node.labelBounds);
337
+ }
338
+ for (const edge of edges) {
339
+ if (!edge.label)
340
+ continue;
341
+ const labelWidth = Math.max(edge.label.length * LABEL_CHAR_WIDTH, LABEL_MIN_WIDTH);
342
+ const labelHeight = LABEL_HEIGHT;
343
+ // Find the longest segment
344
+ const { segStart, segEnd } = findLongestSegment(edge.waypoints);
345
+ // Generate candidate positions along the segment
346
+ const candidates = generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight);
347
+ // Pick the first non-overlapping candidate
348
+ let placed = false;
349
+ for (const candidate of candidates) {
350
+ if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
351
+ edge.labelBounds = candidate;
352
+ occupied.push(candidate);
353
+ placed = true;
354
+ break;
355
+ }
356
+ }
357
+ // Fallback: slide along segment to find clear space
358
+ if (!placed) {
359
+ const fallback = slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied);
360
+ if (fallback) {
361
+ edge.labelBounds = fallback;
362
+ occupied.push(fallback);
363
+ }
364
+ // If no clear position exists, leave labelBounds undefined (text preserved in edge.label)
365
+ }
366
+ }
367
+ }
368
+ function findLongestSegment(waypoints) {
369
+ let bestLen = 0;
370
+ let bestStart = waypoints[0] ?? { x: 0, y: 0 };
371
+ let bestEnd = waypoints[1] ?? waypoints[0] ?? { x: 0, y: 0 };
372
+ for (let i = 1; i < waypoints.length; i++) {
373
+ const a = waypoints[i - 1];
374
+ const b = waypoints[i];
375
+ if (!a || !b)
376
+ continue;
377
+ const len = Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
378
+ if (len > bestLen) {
379
+ bestLen = len;
380
+ bestStart = a;
381
+ bestEnd = b;
382
+ }
383
+ }
384
+ return { segStart: bestStart, segEnd: bestEnd };
385
+ }
386
+ function generateLabelCandidates(segStart, segEnd, labelWidth, labelHeight) {
387
+ const candidates = [];
388
+ // Positions along segment: 0.5, 0.25, 0.75, 0.33, 0.67
389
+ const fractions = [0.5, 0.25, 0.75, 0.33, 0.67];
390
+ // Perpendicular offsets: above, below
391
+ const offsets = [-LABEL_VERTICAL_OFFSET - labelHeight, LABEL_VERTICAL_OFFSET];
392
+ for (const f of fractions) {
393
+ const px = segStart.x + (segEnd.x - segStart.x) * f;
394
+ const py = segStart.y + (segEnd.y - segStart.y) * f;
395
+ for (const offset of offsets) {
396
+ // Determine perpendicular direction
397
+ const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
398
+ let lx;
399
+ let ly;
400
+ if (isHorizontal) {
401
+ lx = px - labelWidth / 2;
402
+ ly = py + offset;
403
+ }
404
+ else {
405
+ lx = px + offset;
406
+ ly = py - labelHeight / 2;
407
+ }
408
+ candidates.push({ x: lx, y: ly, width: labelWidth, height: labelHeight });
409
+ }
410
+ }
411
+ return candidates;
412
+ }
413
+ function slideLabelAlongSegment(segStart, segEnd, labelWidth, labelHeight, occupied) {
414
+ const isHorizontal = Math.abs(segEnd.y - segStart.y) < 1;
415
+ for (let step = 0; step <= LABEL_SLIDE_STEPS; step++) {
416
+ const t = step / LABEL_SLIDE_STEPS;
417
+ const px = segStart.x + (segEnd.x - segStart.x) * t;
418
+ const py = segStart.y + (segEnd.y - segStart.y) * t;
419
+ const candidate = isHorizontal
420
+ ? {
421
+ x: px - labelWidth / 2,
422
+ y: py - labelHeight - LABEL_VERTICAL_OFFSET,
423
+ width: labelWidth,
424
+ height: labelHeight,
425
+ }
426
+ : {
427
+ x: px - labelWidth - LABEL_VERTICAL_OFFSET,
428
+ y: py - labelHeight / 2,
429
+ width: labelWidth,
430
+ height: labelHeight,
431
+ };
432
+ if (!occupied.some((ob) => boundsOverlap(candidate, ob))) {
433
+ return candidate;
434
+ }
435
+ }
436
+ // No clear position found — skip label placement rather than forcing an overlap
437
+ return undefined;
438
+ }
439
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1,14 @@
1
+ import type { BpmnFlowElement } from "../bpmn/bpmn-model.js";
2
+ import type { LayoutNode, SubProcessChildResult } from "./types.js";
3
+ /**
4
+ * Check if a node type is a sub-process container.
5
+ */
6
+ export declare function isSubProcess(type: string): boolean;
7
+ /**
8
+ * Perform recursive layout for sub-process containers.
9
+ * Lays out internal elements in local coordinates, then sizes the
10
+ * sub-process to fit its content with padding, and translates internal
11
+ * elements into the parent coordinate space.
12
+ */
13
+ export declare function layoutSubProcesses(layoutNodes: LayoutNode[], nodeIndex: Map<string, BpmnFlowElement>): SubProcessChildResult[];
14
+ //# sourceMappingURL=subprocess.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { layoutFlowNodes } from "./layout-engine.js";
2
+ import { SUBPROCESS_PADDING } from "./types.js";
3
+ /**
4
+ * Check if a node type is a sub-process container.
5
+ */
6
+ export function isSubProcess(type) {
7
+ return type === "subProcess" || type === "adHocSubProcess" || type === "eventSubProcess";
8
+ }
9
+ /**
10
+ * Perform recursive layout for sub-process containers.
11
+ * Lays out internal elements in local coordinates, then sizes the
12
+ * sub-process to fit its content with padding, and translates internal
13
+ * elements into the parent coordinate space.
14
+ */
15
+ export function layoutSubProcesses(layoutNodes, nodeIndex) {
16
+ const childResults = [];
17
+ for (const layoutNode of layoutNodes) {
18
+ const bpmnNode = nodeIndex.get(layoutNode.id);
19
+ if (!bpmnNode || !isSubProcess(bpmnNode.type))
20
+ continue;
21
+ const subProcess = bpmnNode;
22
+ if (!subProcess.flowElements || subProcess.flowElements.length === 0)
23
+ continue;
24
+ const childResult = layoutFlowNodes(subProcess.flowElements, subProcess.sequenceFlows ?? []);
25
+ if (childResult.nodes.length === 0)
26
+ continue;
27
+ // Compute bounding box of child elements
28
+ let minX = Number.POSITIVE_INFINITY;
29
+ let minY = Number.POSITIVE_INFINITY;
30
+ let maxX = Number.NEGATIVE_INFINITY;
31
+ let maxY = Number.NEGATIVE_INFINITY;
32
+ for (const child of childResult.nodes) {
33
+ minX = Math.min(minX, child.bounds.x);
34
+ minY = Math.min(minY, child.bounds.y);
35
+ maxX = Math.max(maxX, child.bounds.x + child.bounds.width);
36
+ maxY = Math.max(maxY, child.bounds.y + child.bounds.height);
37
+ }
38
+ const contentWidth = maxX - minX;
39
+ const contentHeight = maxY - minY;
40
+ // Size the sub-process to fit content with padding
41
+ const originalCenterY = layoutNode.bounds.y + layoutNode.bounds.height / 2;
42
+ layoutNode.bounds.width = contentWidth + SUBPROCESS_PADDING * 2;
43
+ layoutNode.bounds.height = contentHeight + SUBPROCESS_PADDING * 2;
44
+ layoutNode.isExpanded = true;
45
+ // Re-center vertically on the original baseline position
46
+ layoutNode.bounds.y = originalCenterY - layoutNode.bounds.height / 2;
47
+ // Translate child coordinates into parent space
48
+ const offsetX = layoutNode.bounds.x + SUBPROCESS_PADDING - minX;
49
+ const offsetY = layoutNode.bounds.y + SUBPROCESS_PADDING - minY;
50
+ for (const child of childResult.nodes) {
51
+ child.bounds.x += offsetX;
52
+ child.bounds.y += offsetY;
53
+ if (child.labelBounds) {
54
+ child.labelBounds.x += offsetX;
55
+ child.labelBounds.y += offsetY;
56
+ }
57
+ }
58
+ for (const edge of childResult.edges) {
59
+ for (const wp of edge.waypoints) {
60
+ wp.x += offsetX;
61
+ wp.y += offsetY;
62
+ }
63
+ if (edge.labelBounds) {
64
+ edge.labelBounds.x += offsetX;
65
+ edge.labelBounds.y += offsetY;
66
+ }
67
+ }
68
+ childResults.push({
69
+ parentId: layoutNode.id,
70
+ result: childResult,
71
+ parentX: layoutNode.bounds.x,
72
+ parentY: layoutNode.bounds.y,
73
+ });
74
+ }
75
+ return childResults;
76
+ }
77
+ //# sourceMappingURL=subprocess.js.map
@@ -0,0 +1,72 @@
1
+ import type { BpmnElementType } from "../bpmn/bpmn-model.js";
2
+ /** Fixed element dimensions by type. */
3
+ export declare const ELEMENT_SIZES: Record<string, {
4
+ width: number;
5
+ height: number;
6
+ }>;
7
+ /** Virtual grid cell dimensions for element placement. */
8
+ export declare const GRID_CELL_WIDTH = 130;
9
+ export declare const GRID_CELL_HEIGHT = 140;
10
+ /** Minimum spacing between elements (derived from grid). */
11
+ export declare const HORIZONTAL_SPACING: number;
12
+ export declare const VERTICAL_SPACING: number;
13
+ /** Padding inside sub-process containers. */
14
+ export declare const SUBPROCESS_PADDING = 20;
15
+ /** Edge-label sizing constants (used for placement & collision detection). */
16
+ export declare const LABEL_CHAR_WIDTH = 7;
17
+ export declare const LABEL_MIN_WIDTH = 40;
18
+ export declare const LABEL_HEIGHT = 14;
19
+ export declare const LABEL_VERTICAL_OFFSET = 10;
20
+ /** Axis-aligned bounding box. */
21
+ export interface Bounds {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ /** A waypoint in an edge route. */
28
+ export interface Waypoint {
29
+ x: number;
30
+ y: number;
31
+ }
32
+ /** A node in the layout graph. */
33
+ export interface LayoutNode {
34
+ id: string;
35
+ type: BpmnElementType;
36
+ bounds: Bounds;
37
+ /** Layer (column) index assigned by Sugiyama algorithm. */
38
+ layer: number;
39
+ /** Position within the layer (row index). */
40
+ position: number;
41
+ /** Label text for the node. */
42
+ label?: string;
43
+ /** Label bounds for overlap checking. */
44
+ labelBounds?: Bounds;
45
+ /** Whether this node is an expanded sub-process container. */
46
+ isExpanded?: boolean;
47
+ }
48
+ /** A routed edge in the layout. */
49
+ export interface LayoutEdge {
50
+ id: string;
51
+ sourceRef: string;
52
+ targetRef: string;
53
+ waypoints: Waypoint[];
54
+ /** Label text for the edge. */
55
+ label?: string;
56
+ /** Label bounds for overlap checking. */
57
+ labelBounds?: Bounds;
58
+ }
59
+ /** Result of laying out a sub-process's children, linked to its parent. */
60
+ export interface SubProcessChildResult {
61
+ parentId: string;
62
+ result: LayoutResult;
63
+ /** Subprocess position at the time children were translated to absolute coordinates. */
64
+ parentX: number;
65
+ parentY: number;
66
+ }
67
+ /** Complete layout result for a process. */
68
+ export interface LayoutResult {
69
+ nodes: LayoutNode[];
70
+ edges: LayoutEdge[];
71
+ }
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,37 @@
1
+ /** Fixed element dimensions by type. */
2
+ export const ELEMENT_SIZES = {
3
+ startEvent: { width: 36, height: 36 },
4
+ endEvent: { width: 36, height: 36 },
5
+ intermediateThrowEvent: { width: 36, height: 36 },
6
+ intermediateCatchEvent: { width: 36, height: 36 },
7
+ boundaryEvent: { width: 36, height: 36 },
8
+ serviceTask: { width: 100, height: 80 },
9
+ scriptTask: { width: 100, height: 80 },
10
+ userTask: { width: 100, height: 80 },
11
+ sendTask: { width: 100, height: 80 },
12
+ receiveTask: { width: 100, height: 80 },
13
+ businessRuleTask: { width: 100, height: 80 },
14
+ callActivity: { width: 100, height: 80 },
15
+ exclusiveGateway: { width: 50, height: 50 },
16
+ parallelGateway: { width: 50, height: 50 },
17
+ inclusiveGateway: { width: 50, height: 50 },
18
+ eventBasedGateway: { width: 50, height: 50 },
19
+ // Sub-processes are sized dynamically
20
+ subProcess: { width: 100, height: 80 },
21
+ adHocSubProcess: { width: 100, height: 80 },
22
+ eventSubProcess: { width: 100, height: 80 },
23
+ };
24
+ /** Virtual grid cell dimensions for element placement. */
25
+ export const GRID_CELL_WIDTH = 130;
26
+ export const GRID_CELL_HEIGHT = 140;
27
+ /** Minimum spacing between elements (derived from grid). */
28
+ export const HORIZONTAL_SPACING = GRID_CELL_WIDTH - 100; // 100 = max element width
29
+ export const VERTICAL_SPACING = GRID_CELL_HEIGHT - 80; // 80 = max element height
30
+ /** Padding inside sub-process containers. */
31
+ export const SUBPROCESS_PADDING = 20;
32
+ /** Edge-label sizing constants (used for placement & collision detection). */
33
+ export const LABEL_CHAR_WIDTH = 7;
34
+ export const LABEL_MIN_WIDTH = 40;
35
+ export const LABEL_HEIGHT = 14;
36
+ export const LABEL_VERTICAL_OFFSET = 10;
37
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,8 @@
1
+ /** Generates a unique ID with the given prefix. */
2
+ export declare function generateId(prefix: string): string;
3
+ /**
4
+ * Switches to deterministic counter-based IDs and resets the counter.
5
+ * Call this in test `beforeEach` to get stable, predictable IDs.
6
+ */
7
+ export declare function resetIdCounter(): void;
8
+ //# sourceMappingURL=id-generator.d.ts.map
@@ -0,0 +1,31 @@
1
+ const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
2
+ const ID_SIZE = 8;
3
+ function nanoId() {
4
+ const bytes = new Uint8Array(ID_SIZE);
5
+ crypto.getRandomValues(bytes);
6
+ let id = "";
7
+ for (let i = 0; i < ID_SIZE; i++) {
8
+ id += ALPHABET[bytes[i] % ALPHABET.length];
9
+ }
10
+ return id;
11
+ }
12
+ // Counter used only in deterministic test mode (activated by resetIdCounter())
13
+ let _counter = 0;
14
+ let _deterministic = false;
15
+ /** Generates a unique ID with the given prefix. */
16
+ export function generateId(prefix) {
17
+ if (_deterministic) {
18
+ _counter++;
19
+ return `${prefix}_${_counter.toString(36).padStart(7, "0")}`;
20
+ }
21
+ return `${prefix}_${nanoId()}`;
22
+ }
23
+ /**
24
+ * Switches to deterministic counter-based IDs and resets the counter.
25
+ * Call this in test `beforeEach` to get stable, predictable IDs.
26
+ */
27
+ export function resetIdCounter() {
28
+ _counter = 0;
29
+ _deterministic = true;
30
+ }
31
+ //# sourceMappingURL=id-generator.js.map
@@ -0,0 +1,3 @@
1
+ export type { XmlElement } from "./xml-element.js";
2
+ export { generateId, resetIdCounter } from "./id-generator.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { generateId, resetIdCounter } from "./id-generator.js";
2
+ //# sourceMappingURL=index.js.map