@bpmnkit/core 0.0.26 → 0.1.0

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