@bpmnkit/core 0.0.27 → 0.1.1

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 (56) hide show
  1. package/dist/bpmn/auto-layout.js +39 -126
  2. package/dist/bpmn/bpmn-builder.d.ts +30 -0
  3. package/dist/bpmn/bpmn-builder.js +350 -31
  4. package/dist/bpmn/bpmn-model.d.ts +35 -2
  5. package/dist/bpmn/bpmn-parser.js +39 -1
  6. package/dist/bpmn/bpmn-serializer.js +27 -0
  7. package/dist/bpmn/compact.js +11 -1
  8. package/dist/bpmn/di-check.d.ts +14 -0
  9. package/dist/bpmn/di-check.js +51 -0
  10. package/dist/bpmn/di-planes.d.ts +13 -0
  11. package/dist/bpmn/di-planes.js +20 -0
  12. package/dist/bpmn/optimize/tasks.js +1 -0
  13. package/dist/bpmn/svg.js +36 -4
  14. package/dist/bpmn/type-guards.d.ts +7 -1
  15. package/dist/bpmn/type-guards.js +13 -0
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.js +3 -1
  18. package/dist/layout/annotations.d.ts +27 -0
  19. package/dist/layout/annotations.js +251 -0
  20. package/dist/layout/grid/edge-labels.d.ts +8 -0
  21. package/dist/layout/grid/edge-labels.js +126 -0
  22. package/dist/layout/grid/flow-graph.d.ts +25 -0
  23. package/dist/layout/grid/flow-graph.js +99 -0
  24. package/dist/layout/grid/grid-engine.d.ts +4 -0
  25. package/dist/layout/grid/grid-engine.js +214 -0
  26. package/dist/layout/grid/grid-router.d.ts +36 -0
  27. package/dist/layout/grid/grid-router.js +190 -0
  28. package/dist/layout/grid/grid.d.ts +43 -0
  29. package/dist/layout/grid/grid.js +174 -0
  30. package/dist/layout/grid/walker.d.ts +11 -0
  31. package/dist/layout/grid/walker.js +126 -0
  32. package/dist/layout/index.d.ts +2 -5
  33. package/dist/layout/index.js +1 -4
  34. package/dist/layout/layout-engine.d.ts +5 -15
  35. package/dist/layout/layout-engine.js +8 -486
  36. package/dist/layout/types.js +4 -0
  37. package/dist/xml/xml-parser.js +5 -0
  38. package/package.json +1 -1
  39. package/dist/layout/astar.d.ts +0 -15
  40. package/dist/layout/astar.js +0 -191
  41. package/dist/layout/block-builder.d.ts +0 -37
  42. package/dist/layout/block-builder.js +0 -154
  43. package/dist/layout/block-layout.d.ts +0 -9
  44. package/dist/layout/block-layout.js +0 -163
  45. package/dist/layout/coordinates.d.ts +0 -85
  46. package/dist/layout/coordinates.js +0 -1392
  47. package/dist/layout/crossing.d.ts +0 -8
  48. package/dist/layout/crossing.js +0 -60
  49. package/dist/layout/graph.d.ts +0 -28
  50. package/dist/layout/graph.js +0 -126
  51. package/dist/layout/layers.d.ts +0 -13
  52. package/dist/layout/layers.js +0 -49
  53. package/dist/layout/routing.d.ts +0 -33
  54. package/dist/layout/routing.js +0 -622
  55. package/dist/layout/subprocess.d.ts +0 -14
  56. package/dist/layout/subprocess.js +0 -115
@@ -0,0 +1,126 @@
1
+ import { formsLoop, hasOtherIncoming, isFutureIncoming, isTaskLike } from "./flow-graph.js";
2
+ import { Grid } from "./grid.js";
3
+ const COMPACT_MAX_COLS = 4;
4
+ /**
5
+ * Place every element of the graph into a Grid via the bpmn-io DFS walk.
6
+ * `compact` bypasses the walk and packs row-major (adHoc tool palettes).
7
+ */
8
+ export function createGridLayout(graph, opts = {}) {
9
+ const grid = new Grid();
10
+ if (opts.compact) {
11
+ for (let i = 0; i < graph.elements.length; i++) {
12
+ const el = graph.elements[i];
13
+ if (el)
14
+ grid.add(el, [Math.floor(i / COMPACT_MAX_COLS), i % COMPACT_MAX_COLS]);
15
+ }
16
+ return grid;
17
+ }
18
+ const visited = new Set();
19
+ while (visited.size < graph.elements.length) {
20
+ let starts = graph.elements.filter((el) => !visited.has(el.id) && !hasOtherIncoming(el, graph));
21
+ if (starts.length === 0) {
22
+ // pure cycles or unreachable joins — force-start with the first leftover
23
+ const leftover = graph.elements.find((el) => !visited.has(el.id));
24
+ starts = leftover ? [leftover] : [];
25
+ }
26
+ if (starts.length === 0)
27
+ break;
28
+ const stack = [];
29
+ for (const s of starts) {
30
+ grid.add(s);
31
+ visited.add(s.id);
32
+ stack.push(s);
33
+ }
34
+ walk(grid, graph, visited, stack);
35
+ }
36
+ return grid;
37
+ }
38
+ function walk(grid, graph, visited, stack) {
39
+ while (stack.length > 0) {
40
+ const current = stack.pop();
41
+ if (!current)
42
+ break;
43
+ incomingStep(current, grid, graph);
44
+ const next = [
45
+ ...outgoingStep(current, grid, graph, visited, stack),
46
+ ...attacherStep(current, grid, graph, visited),
47
+ ];
48
+ for (const el of next)
49
+ stack.push(el);
50
+ }
51
+ }
52
+ /** Realign a join with its feeders before its successors are placed. */
53
+ function incomingStep(el, grid, graph) {
54
+ const sources = (graph.incoming.get(el.id) ?? [])
55
+ .map((f) => graph.byId.get(f.sourceRef))
56
+ .filter((s) => s !== undefined);
57
+ if (sources.length > 1) {
58
+ grid.adjustColumnForMultipleIncoming(sources, el);
59
+ grid.adjustRowForMultipleIncoming(sources, el);
60
+ }
61
+ }
62
+ /** Place successors: first one to the right (happy path), the rest stacked below. */
63
+ function outgoingStep(el, grid, graph, visited, stack) {
64
+ const targets = (graph.outgoing.get(el.id) ?? [])
65
+ .map((f) => graph.byId.get(f.targetRef))
66
+ .filter((t) => t !== undefined);
67
+ if (targets.length > 1 && targets.every((t) => isTaskLike(t.type))) {
68
+ grid.adjustGridPosition(el);
69
+ }
70
+ let previous = null;
71
+ const placed = [];
72
+ for (let i = 0; i < targets.length; i++) {
73
+ const target = targets[i];
74
+ if (!target || visited.has(target.id))
75
+ continue;
76
+ if ((previous !== null || stack.length > 0) &&
77
+ isFutureIncoming(target, visited, graph) &&
78
+ !formsLoop(target, visited, graph)) {
79
+ continue; // defer join until its last feeder is processed
80
+ }
81
+ if (previous === null) {
82
+ grid.addAfter(el, target);
83
+ }
84
+ else if (el.type === "exclusiveGateway" && target.type === "exclusiveGateway") {
85
+ grid.addAfter(previous, target);
86
+ }
87
+ else {
88
+ const anchor = targets[i - 1];
89
+ grid.addBelow(anchor && grid.find(anchor)[0] >= 0 ? anchor : previous, target);
90
+ }
91
+ if (target.id !== el.id)
92
+ previous = target;
93
+ placed.unshift(target);
94
+ visited.add(target.id);
95
+ }
96
+ // exclusive gateways first → popped from the LIFO stack last
97
+ return [
98
+ ...placed.filter((t) => t.type === "exclusiveGateway"),
99
+ ...placed.filter((t) => t.type !== "exclusiveGateway"),
100
+ ];
101
+ }
102
+ /** Place the successors of this host's boundary events one row down, one col right. */
103
+ function attacherStep(host, grid, graph, visited) {
104
+ const out = [];
105
+ for (const be of graph.attachers.get(host.id) ?? []) {
106
+ const targets = (graph.outgoing.get(be.id) ?? [])
107
+ .map((f) => graph.byId.get(f.targetRef))
108
+ .filter((t) => t !== undefined)
109
+ .reverse();
110
+ for (const target of targets) {
111
+ if (visited.has(target.id))
112
+ continue;
113
+ const [r, c] = grid.find(host);
114
+ if (r < 0)
115
+ continue;
116
+ if (grid.get(r + 1, c) !== undefined || grid.get(r + 1, c + 1) !== undefined) {
117
+ grid.createRow(r);
118
+ }
119
+ grid.add(target, [r + 1, c + 1]);
120
+ visited.add(target.id);
121
+ out.push(target);
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ //# sourceMappingURL=walker.js.map
@@ -1,12 +1,9 @@
1
1
  export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
- export { buildBlockTree } from "./block-builder.js";
3
- export type { FlowBlock, GatewayBlock, NodeBlock, SequenceBlock } from "./block-builder.js";
4
- export { applyBlockLayout } from "./block-layout.js";
5
- export { routeEdgeAstar } from "./astar.js";
6
2
  export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
7
3
  export type { BenchmarkResult, BoundingBox, ElementComparison, ElementPosition, FlowOrderViolation, } from "./bench.js";
8
- export { assignGridRows } from "./coordinates.js";
9
4
  export { assertNoOverlap } from "./overlap.js";
5
+ export { checkDiCompleteness } from "../bpmn/di-check.js";
6
+ export type { DiCompleteness } from "../bpmn/di-check.js";
10
7
  export type { Bounds, LayoutEdge, LayoutNode, LayoutResult, Waypoint } from "./types.js";
11
8
  export { ELEMENT_SIZES, GRID_CELL_HEIGHT, GRID_CELL_WIDTH, HORIZONTAL_SPACING, LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET, SUBPROCESS_PADDING, VERTICAL_SPACING, } from "./types.js";
12
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,6 @@
1
1
  export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
- export { buildBlockTree } from "./block-builder.js";
3
- export { applyBlockLayout } from "./block-layout.js";
4
- export { routeEdgeAstar } from "./astar.js";
5
2
  export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
6
- export { assignGridRows } from "./coordinates.js";
7
3
  export { assertNoOverlap } from "./overlap.js";
4
+ export { checkDiCompleteness } from "../bpmn/di-check.js";
8
5
  export { ELEMENT_SIZES, GRID_CELL_HEIGHT, GRID_CELL_WIDTH, HORIZONTAL_SPACING, LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET, SUBPROCESS_PADDING, VERTICAL_SPACING, } from "./types.js";
9
6
  //# sourceMappingURL=index.js.map
@@ -1,22 +1,12 @@
1
1
  import type { BpmnFlowElement, BpmnProcess, BpmnSequenceFlow } from "../bpmn/bpmn-model.js";
2
2
  import type { LayoutResult } from "./types.js";
3
3
  /**
4
- * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
5
- *
6
- * Phases:
7
- * 1. Cycle removal DFS back-edge detection and reversal
8
- * 2. Layer assignment — Longest-path layering
9
- * 3. Crossing minimization — Barycenter heuristic
10
- * 4. Coordinate assignment — Fixed element sizes with spacing
11
- * 5. Sub-process layout — Recursive nested passes
12
- * 6. Edge routing — Orthogonal waypoints
13
- * 7. Boundary event repositioning — place events on host border
14
- * 8. Overlap assertion — Post-condition validation
4
+ * Layout a full process (grid engine). Boundary events, expanded
5
+ * subprocesses and edge labels are handled inside the engine.
6
+ * Never throws on residual label overlap — call assertNoOverlap yourself
7
+ * in tests that validate known-good fixtures.
15
8
  */
16
9
  export declare function layoutProcess(process: BpmnProcess): LayoutResult;
17
- /**
18
- * Layout a set of flow nodes and sequence flows.
19
- * Used both for top-level processes and recursively for sub-processes.
20
- */
10
+ /** Layout a set of flow nodes and sequence flows (used by ascii, proxy, compact). */
21
11
  export declare function layoutFlowNodes(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): LayoutResult;
22
12
  //# sourceMappingURL=layout-engine.d.ts.map
@@ -1,493 +1,15 @@
1
- import { buildBlockTree } from "./block-builder.js";
2
- import { applyBlockLayout } from "./block-layout.js";
3
- import { alignBaselinePath, alignBranchBaselines, alignSplitJoinPairs, assignCoordinates, assignGridRows, compactBranches, distributeSplitBranches, ensureEarlyReturnOffBaseline, resolveLayerOverlaps, snapToYRows, } from "./coordinates.js";
4
- import { minimizeCrossings } from "./crossing.js";
5
- import { buildGraph, detectBackEdges, reverseBackEdges } from "./graph.js";
6
- import { assignLayers, groupByLayer } from "./layers.js";
7
- import { assertNoOverlap } from "./overlap.js";
8
- import { routeEdges } from "./routing.js";
9
- import { layoutSubProcesses } from "./subprocess.js";
10
- const CHAIN_GAP = 30;
11
- const CHAIN_V_GAP = 20;
1
+ import { gridLayoutFlowNodes } from "./grid/grid-engine.js";
12
2
  /**
13
- * Reposition boundary events to the bottom edge of their host task, then walk
14
- * each boundary event's exclusive downstream chain and place those nodes
15
- * horizontally to the right of the host task. Re-routes all affected edges.
16
- */
17
- function repositionBoundaryEvents(flowElements, result) {
18
- const boundaryMap = new Map();
19
- for (const el of flowElements) {
20
- if (el.type !== "boundaryEvent")
21
- continue;
22
- const list = boundaryMap.get(el.attachedToRef) ?? [];
23
- list.push(el.id);
24
- boundaryMap.set(el.attachedToRef, list);
25
- }
26
- if (boundaryMap.size === 0)
27
- return;
28
- const nodeById = new Map(result.nodes.map((n) => [n.id, n]));
29
- const succIds = new Map();
30
- const predIds = new Map();
31
- for (const edge of result.edges) {
32
- const se = succIds.get(edge.sourceRef) ?? [];
33
- se.push(edge.targetRef);
34
- succIds.set(edge.sourceRef, se);
35
- const ps = predIds.get(edge.targetRef) ?? new Set();
36
- ps.add(edge.sourceRef);
37
- predIds.set(edge.targetRef, ps);
38
- }
39
- const allChainNodes = new Set();
40
- for (const [hostId, beIds] of boundaryMap) {
41
- const hostNode = nodeById.get(hostId);
42
- if (!hostNode)
43
- continue;
44
- // Pre-compute distribution parameters (all boundary events share the same fixed size).
45
- // Distribute events evenly along the bottom edge, centered on the task.
46
- // effectiveSpacing guarantees events don't overlap (min bW + 4px gap).
47
- const firstBeNode = nodeById.get(beIds[0] ?? "");
48
- const bW = firstBeNode?.bounds.width ?? 36;
49
- const bH = firstBeNode?.bounds.height ?? 36;
50
- const n = beIds.length;
51
- const effectiveSpacing = Math.max(Math.round(hostNode.bounds.width / (n + 1)), bW + 4);
52
- const groupWidth = Math.max(0, n - 1) * effectiveSpacing;
53
- const groupStartCenterX = Math.round(hostNode.bounds.x + hostNode.bounds.width / 2 - groupWidth / 2);
54
- for (let i = 0; i < beIds.length; i++) {
55
- const beId = beIds[i];
56
- if (!beId)
57
- continue;
58
- const beNode = nodeById.get(beId);
59
- if (!beNode)
60
- continue;
61
- // bW / bH come from the pre-loop computation (all BEs are fixed 36×36)
62
- // Center-bottom distribution: single event → task center; multiple → even spread
63
- beNode.bounds.x = Math.round(groupStartCenterX + i * effectiveSpacing - bW / 2);
64
- beNode.bounds.y = Math.round(hostNode.bounds.y + hostNode.bounds.height - bH / 2);
65
- if (beNode.labelBounds) {
66
- beNode.labelBounds.x = beNode.bounds.x + Math.round(bW / 2 - beNode.labelBounds.width / 2);
67
- beNode.labelBounds.y = beNode.bounds.y + bH + 4;
68
- }
69
- // Collect nodes exclusively reachable from this boundary event (BFS)
70
- const chainSet = new Set([beId]);
71
- const chainOrder = [];
72
- const queue = [...(succIds.get(beId) ?? [])];
73
- while (queue.length > 0) {
74
- const id = queue.shift();
75
- if (!id || chainSet.has(id))
76
- continue;
77
- const preds = predIds.get(id) ?? new Set();
78
- if ([...preds].every((p) => chainSet.has(p))) {
79
- chainSet.add(id);
80
- chainOrder.push(id);
81
- queue.push(...(succIds.get(id) ?? []));
82
- }
83
- }
84
- // Record all chain members so the forward pass can identify them.
85
- for (const cid of chainSet)
86
- allChainNodes.add(cid);
87
- // Each boundary event's chain gets its own vertical lane
88
- let maxChainH = 0;
89
- for (const id of chainOrder) {
90
- const n = nodeById.get(id);
91
- if (n)
92
- maxChainH = Math.max(maxChainH, n.bounds.height);
93
- }
94
- const laneOffset = i * (maxChainH + CHAIN_V_GAP + 10);
95
- const chainCenterY = Math.round(beNode.bounds.y + bH + CHAIN_V_GAP + maxChainH / 2 + laneOffset);
96
- const chainStartX = Math.max(Math.round(beNode.bounds.x + bW / 2) + CHAIN_GAP, hostNode.bounds.x + hostNode.bounds.width + CHAIN_GAP);
97
- let curX = chainStartX;
98
- for (const id of chainOrder) {
99
- const n = nodeById.get(id);
100
- if (!n)
101
- continue;
102
- n.bounds.x = curX;
103
- n.bounds.y = chainCenterY - Math.round(n.bounds.height / 2);
104
- if (n.labelBounds) {
105
- // Center label on node, but clamp so it never extends left of the node —
106
- // wide event labels would otherwise overlap the preceding chain element.
107
- const labelX = n.bounds.x + Math.round(n.bounds.width / 2 - n.labelBounds.width / 2);
108
- n.labelBounds.x = Math.max(labelX, n.bounds.x);
109
- n.labelBounds.y = n.bounds.y + n.bounds.height + 4;
110
- }
111
- // Advance past the node AND its label so the next element doesn't overlap.
112
- const nodeRight = n.bounds.x + n.bounds.width;
113
- const labelRight = n.labelBounds ? n.labelBounds.x + n.labelBounds.width : 0;
114
- curX = Math.max(nodeRight, labelRight) + CHAIN_GAP;
115
- }
116
- // Re-route edges touching the boundary event or its chain
117
- for (const edge of result.edges) {
118
- if (!chainSet.has(edge.sourceRef))
119
- continue;
120
- const src = nodeById.get(edge.sourceRef);
121
- const tgt = nodeById.get(edge.targetRef);
122
- if (!src || !tgt)
123
- continue;
124
- if (edge.sourceRef === beId) {
125
- const srcX = Math.round(src.bounds.x + bW / 2);
126
- const srcY = Math.round(src.bounds.y + bH);
127
- const tgtX = Math.round(tgt.bounds.x);
128
- const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
129
- edge.waypoints = [
130
- { x: srcX, y: srcY },
131
- { x: srcX, y: tgtY },
132
- { x: tgtX, y: tgtY },
133
- ];
134
- }
135
- else {
136
- const srcX = Math.round(src.bounds.x + src.bounds.width);
137
- const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
138
- const tgtX = Math.round(tgt.bounds.x);
139
- const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
140
- edge.waypoints = [
141
- { x: srcX, y: srcY },
142
- { x: tgtX, y: tgtY },
143
- ];
144
- }
145
- }
146
- }
147
- }
148
- // Forward-placement pass: any node not in any chain but whose predecessor
149
- // has been relocated further right must be pushed rightward.
150
- // Process in topological order (Kahn's algorithm over the sequenceFlow graph).
151
- const inDegree = new Map();
152
- for (const id of nodeById.keys()) {
153
- inDegree.set(id, (predIds.get(id) ?? new Set()).size);
154
- }
155
- const topoQueue = [];
156
- for (const [id, deg] of inDegree) {
157
- if (deg === 0)
158
- topoQueue.push(id);
159
- }
160
- const topoOrder = [];
161
- while (topoQueue.length > 0) {
162
- const id = topoQueue.shift();
163
- if (!id)
164
- break;
165
- topoOrder.push(id);
166
- for (const succId of succIds.get(id) ?? []) {
167
- const newDeg = (inDegree.get(succId) ?? 1) - 1;
168
- inDegree.set(succId, newDeg);
169
- if (newDeg === 0)
170
- topoQueue.push(succId);
171
- }
172
- }
173
- const movedInPass = new Set();
174
- for (const id of topoOrder) {
175
- if (allChainNodes.has(id))
176
- continue;
177
- const node = nodeById.get(id);
178
- if (!node)
179
- continue;
180
- const preds = predIds.get(id) ?? new Set();
181
- if (preds.size === 0)
182
- continue;
183
- let maxPredRight = 0;
184
- for (const predId of preds) {
185
- const pred = nodeById.get(predId);
186
- if (pred)
187
- maxPredRight = Math.max(maxPredRight, pred.bounds.x + pred.bounds.width);
188
- }
189
- const minX = maxPredRight + CHAIN_GAP;
190
- if (minX > node.bounds.x) {
191
- const delta = minX - node.bounds.x;
192
- node.bounds.x = minX;
193
- if (node.labelBounds)
194
- node.labelBounds.x += delta;
195
- movedInPass.add(id);
196
- }
197
- }
198
- // Spatial bump: if a moved node now overlaps a non-chain node on a parallel
199
- // path (no predecessor/successor relationship), push it clear and cascade.
200
- let bumped = true;
201
- while (bumped) {
202
- bumped = false;
203
- for (const movedId of movedInPass) {
204
- const moved = nodeById.get(movedId);
205
- if (!moved)
206
- continue;
207
- const movedRight = moved.bounds.x + moved.bounds.width;
208
- for (const [otherId, other] of nodeById) {
209
- if (otherId === movedId)
210
- continue;
211
- if (allChainNodes.has(otherId))
212
- continue;
213
- if (movedInPass.has(otherId))
214
- continue;
215
- // Check y overlap
216
- if (other.bounds.y + other.bounds.height <= moved.bounds.y)
217
- continue;
218
- if (other.bounds.y >= moved.bounds.y + moved.bounds.height)
219
- continue;
220
- // Check x overlap (moved node intrudes into other's space)
221
- if (other.bounds.x >= movedRight)
222
- continue;
223
- if (other.bounds.x + other.bounds.width <= moved.bounds.x)
224
- continue;
225
- // Push other right of moved
226
- const newX = movedRight + CHAIN_GAP;
227
- if (newX > other.bounds.x) {
228
- const delta = newX - other.bounds.x;
229
- other.bounds.x = newX;
230
- if (other.labelBounds)
231
- other.labelBounds.x += delta;
232
- movedInPass.add(otherId);
233
- bumped = true;
234
- }
235
- }
236
- }
237
- }
238
- // Re-route edges where a chain source now points at a moved target,
239
- // or where the source itself was moved by the forward pass.
240
- for (const edge of result.edges) {
241
- const srcMoved = movedInPass.has(edge.sourceRef);
242
- const tgtMoved = movedInPass.has(edge.targetRef);
243
- if (!srcMoved && !tgtMoved)
244
- continue;
245
- const src = nodeById.get(edge.sourceRef);
246
- const tgt = nodeById.get(edge.targetRef);
247
- if (!src || !tgt)
248
- continue;
249
- const srcX = Math.round(src.bounds.x + src.bounds.width);
250
- const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
251
- const tgtX = Math.round(tgt.bounds.x);
252
- const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
253
- edge.waypoints = [
254
- { x: srcX, y: srcY },
255
- { x: tgtX, y: tgtY },
256
- ];
257
- }
258
- }
259
- /**
260
- * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
261
- *
262
- * Phases:
263
- * 1. Cycle removal — DFS back-edge detection and reversal
264
- * 2. Layer assignment — Longest-path layering
265
- * 3. Crossing minimization — Barycenter heuristic
266
- * 4. Coordinate assignment — Fixed element sizes with spacing
267
- * 5. Sub-process layout — Recursive nested passes
268
- * 6. Edge routing — Orthogonal waypoints
269
- * 7. Boundary event repositioning — place events on host border
270
- * 8. Overlap assertion — Post-condition validation
3
+ * Layout a full process (grid engine). Boundary events, expanded
4
+ * subprocesses and edge labels are handled inside the engine.
5
+ * Never throws on residual label overlap call assertNoOverlap yourself
6
+ * in tests that validate known-good fixtures.
271
7
  */
272
8
  export function layoutProcess(process) {
273
- const result = layoutFlowNodes(process.flowElements, process.sequenceFlows);
274
- repositionBoundaryEvents(process.flowElements, result);
275
- assertNoOverlap(result);
276
- return result;
9
+ return layoutFlowNodes(process.flowElements, process.sequenceFlows);
277
10
  }
278
- /**
279
- * Layout a set of flow nodes and sequence flows.
280
- * Used both for top-level processes and recursively for sub-processes.
281
- */
11
+ /** Layout a set of flow nodes and sequence flows (used by ascii, proxy, compact). */
282
12
  export function layoutFlowNodes(flowNodes, sequenceFlows) {
283
- if (flowNodes.length === 0) {
284
- return { nodes: [], edges: [] };
285
- }
286
- // Build node index
287
- const nodeIndex = new Map();
288
- for (const node of flowNodes) {
289
- nodeIndex.set(node.id, node);
290
- }
291
- // Phase 1: Build graph and detect/remove cycles
292
- const graph = buildGraph(flowNodes, sequenceFlows);
293
- const backEdges = detectBackEdges(graph, sequenceFlows);
294
- const dag = backEdges.length > 0 ? reverseBackEdges(graph, backEdges) : graph;
295
- // Phase 2: Try block-based layout (primary path for structured processes)
296
- // Block layout only works well for processes without back-edges (loops).
297
- let layoutNodes;
298
- const blockTree = backEdges.length === 0 ? buildBlockTree(dag, nodeIndex) : null;
299
- const usedBlockLayout = blockTree !== null;
300
- if (blockTree) {
301
- layoutNodes = applyBlockLayout(blockTree, nodeIndex);
302
- }
303
- else {
304
- layoutNodes = sugiyamaLayout(dag, nodeIndex, backEdges);
305
- }
306
- // Phase 5: Sub-process layout — expand containers and lay out children
307
- const childResults = layoutSubProcesses(layoutNodes, nodeIndex);
308
- // After subprocess expansion, push nodes that now overlap with expanded containers.
309
- // For block layout, assign unique layer indices first so resolveLayerOverlaps works
310
- // correctly (block layout nodes all start at layer=0).
311
- if (usedBlockLayout && childResults.length > 0) {
312
- // Assign each block-layout node a unique layer so overlap resolution doesn't
313
- // collapse them all into the same bucket and push them vertically apart.
314
- for (let idx = 0; idx < layoutNodes.length; idx++) {
315
- const n = layoutNodes[idx];
316
- if (n)
317
- n.layer = idx;
318
- }
319
- }
320
- resolveSubProcessOverlaps(layoutNodes);
321
- // Phase 5b: Resolve Y-direction overlaps caused by subprocess expansion.
322
- // Expanded subprocesses grow in-place and can overlap same-layer siblings.
323
- // For block layout without subprocesses, skip this — overlaps are impossible by construction.
324
- if (!usedBlockLayout || childResults.length > 0) {
325
- resolveLayerOverlaps(layoutNodes);
326
- }
327
- // Phase 5c: Sync child positions to their subprocess containers.
328
- // resolveLayerOverlaps (including its Y-normalization pass) may have shifted
329
- // subprocess containers after their children were already translated to
330
- // absolute coordinates — children must follow.
331
- syncSubProcessChildren(childResults, layoutNodes);
332
- // Assign grid-row indices based on final center-Y positions (eliminates pixel-tolerance
333
- // guessing in port-side decisions).
334
- assignGridRows(layoutNodes);
335
- // Phase 6: Edge routing (uses original back-edges for routing, not reversed)
336
- const nodeMap = new Map();
337
- for (const node of layoutNodes) {
338
- nodeMap.set(node.id, node);
339
- }
340
- const edges = routeEdges(sequenceFlows, nodeMap, backEdges);
341
- // Flatten child results into the main layout
342
- const allNodes = [...layoutNodes];
343
- const allEdges = [...edges];
344
- for (const child of childResults) {
345
- for (const cn of child.result.nodes) {
346
- allNodes.push(cn);
347
- }
348
- for (const ce of child.result.edges) {
349
- allEdges.push(ce);
350
- }
351
- }
352
- return { nodes: allNodes, edges: allEdges };
353
- }
354
- /**
355
- * Run the Sugiyama layered layout pipeline.
356
- * Used as fallback for unstructured or loop-containing processes.
357
- */
358
- function sugiyamaLayout(dag, nodeIndex, backEdges) {
359
- // Phase 2: Layer assignment
360
- const layers = assignLayers(dag);
361
- // Phase 3: Group by layer and minimize crossings
362
- const layerGroups = groupByLayer(layers);
363
- const orderedLayers = minimizeCrossings(layerGroups, dag);
364
- // Phase 4: Coordinate assignment
365
- const layoutNodes = assignCoordinates(orderedLayers, nodeIndex);
366
- // Phase 4b: Align linear sequences to a common y-baseline
367
- alignBranchBaselines(layoutNodes, dag);
368
- // Phase 4c: Align split/join gateway pairs to same y-coordinate
369
- alignSplitJoinPairs(layoutNodes, dag, backEdges);
370
- // Phase 4d: Align all baseline-path nodes to the same center-Y
371
- alignBaselinePath(layoutNodes, dag, backEdges);
372
- // Phase 4e: Ensure early-return branches are never on the baseline
373
- ensureEarlyReturnOffBaseline(layoutNodes, dag, backEdges);
374
- // Re-align linear chains that may have been disrupted by position swaps
375
- alignBranchBaselines(layoutNodes, dag);
376
- // Phase 4f: Distribute split gateway branches symmetrically
377
- distributeSplitBranches(layoutNodes, dag, backEdges);
378
- // Re-align split/join pairs that may have been separated during branch distribution
379
- alignSplitJoinPairs(layoutNodes, dag, backEdges);
380
- // Re-align branch spines after distribution moved chains and alignSplitJoinPairs
381
- // adjusted join gateways (continuation nodes after joins must follow)
382
- alignBranchBaselines(layoutNodes, dag);
383
- // Phase 4g: Resolve any layer overlaps from redistribution
384
- resolveLayerOverlaps(layoutNodes);
385
- // Phase 4h: Re-align baseline after overlap resolution (overlap resolution may push
386
- // baseline nodes off-center when they share a layer with branch nodes)
387
- alignBaselinePath(layoutNodes, dag, backEdges);
388
- // Phase 4i: Final overlap resolution — baseline re-alignment may pull a node back into
389
- // an overlap that resolveLayerOverlaps already fixed; one more pass eliminates these.
390
- resolveLayerOverlaps(layoutNodes);
391
- // Phase 4j: Branch compaction — pull branch subtrees toward baseline
392
- compactBranches(layoutNodes, dag, backEdges);
393
- resolveLayerOverlaps(layoutNodes);
394
- alignBaselinePath(layoutNodes, dag, backEdges);
395
- // Phase 4k: Row snapping — merge close Y rows for matrix-like alignment
396
- snapToYRows(layoutNodes);
397
- resolveLayerOverlaps(layoutNodes);
398
- return layoutNodes;
399
- }
400
- /**
401
- * After subprocess expansion, cascade-shift all subsequent layers
402
- * so that inter-layer spacing is preserved.
403
- */
404
- function resolveSubProcessOverlaps(nodes) {
405
- const expanded = nodes.filter((n) => n.isExpanded);
406
- if (expanded.length === 0)
407
- return;
408
- // Group nodes by layer
409
- const byLayer = new Map();
410
- for (const n of nodes) {
411
- const arr = byLayer.get(n.layer);
412
- if (arr)
413
- arr.push(n);
414
- else
415
- byLayer.set(n.layer, [n]);
416
- }
417
- const layers = [...byLayer.keys()].sort((a, b) => a - b);
418
- const MIN_GAP = 50;
419
- // Cascade: ensure each layer starts after previous layer's rightmost edge
420
- for (let i = 1; i < layers.length; i++) {
421
- const prevKey = layers[i - 1];
422
- const curKey = layers[i];
423
- if (prevKey === undefined || curKey === undefined)
424
- continue;
425
- const prevNodes = byLayer.get(prevKey);
426
- const curNodes = byLayer.get(curKey);
427
- if (!prevNodes || !curNodes)
428
- continue;
429
- // Find rightmost edge in previous layer (including labels)
430
- let prevRight = 0;
431
- for (const n of prevNodes) {
432
- prevRight = Math.max(prevRight, n.bounds.x + n.bounds.width);
433
- if (n.labelBounds) {
434
- prevRight = Math.max(prevRight, n.labelBounds.x + n.labelBounds.width);
435
- }
436
- }
437
- // Find leftmost edge in current layer
438
- let curLeft = Number.POSITIVE_INFINITY;
439
- for (const n of curNodes) {
440
- curLeft = Math.min(curLeft, n.bounds.x);
441
- }
442
- const gap = curLeft - prevRight;
443
- if (gap < MIN_GAP) {
444
- const dx = MIN_GAP - gap;
445
- for (const n of curNodes) {
446
- n.bounds.x += dx;
447
- if (n.labelBounds) {
448
- n.labelBounds.x += dx;
449
- }
450
- }
451
- }
452
- }
453
- }
454
- /**
455
- * After post-expansion adjustments (resolveSubProcessOverlaps, resolveLayerOverlaps),
456
- * subprocess containers may have been shifted. Translate their children by the same
457
- * delta so that children remain correctly positioned inside their parent.
458
- */
459
- function syncSubProcessChildren(childResults, layoutNodes) {
460
- if (childResults.length === 0)
461
- return;
462
- const nodeMap = new Map();
463
- for (const n of layoutNodes)
464
- nodeMap.set(n.id, n);
465
- for (const cr of childResults) {
466
- const parent = nodeMap.get(cr.parentId);
467
- if (!parent)
468
- continue;
469
- const dx = parent.bounds.x - cr.parentX;
470
- const dy = parent.bounds.y - cr.parentY;
471
- if (dx === 0 && dy === 0)
472
- continue;
473
- for (const child of cr.result.nodes) {
474
- child.bounds.x += dx;
475
- child.bounds.y += dy;
476
- if (child.labelBounds) {
477
- child.labelBounds.x += dx;
478
- child.labelBounds.y += dy;
479
- }
480
- }
481
- for (const edge of cr.result.edges) {
482
- for (const wp of edge.waypoints) {
483
- wp.x += dx;
484
- wp.y += dy;
485
- }
486
- if (edge.labelBounds) {
487
- edge.labelBounds.x += dx;
488
- edge.labelBounds.y += dy;
489
- }
490
- }
491
- }
13
+ return gridLayoutFlowNodes(flowNodes, sequenceFlows);
492
14
  }
493
15
  //# sourceMappingURL=layout-engine.js.map