@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.
- package/dist/bpmn/auto-layout.js +71 -139
- package/dist/bpmn/bpmn-builder.d.ts +30 -0
- package/dist/bpmn/bpmn-builder.js +343 -31
- package/dist/bpmn/di-check.d.ts +14 -0
- package/dist/bpmn/di-check.js +51 -0
- package/dist/bpmn/svg.js +14 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/layout/annotations.d.ts +27 -0
- package/dist/layout/annotations.js +251 -0
- package/dist/layout/grid/edge-labels.d.ts +8 -0
- package/dist/layout/grid/edge-labels.js +126 -0
- package/dist/layout/grid/flow-graph.d.ts +25 -0
- package/dist/layout/grid/flow-graph.js +99 -0
- package/dist/layout/grid/grid-engine.d.ts +4 -0
- package/dist/layout/grid/grid-engine.js +214 -0
- package/dist/layout/grid/grid-router.d.ts +36 -0
- package/dist/layout/grid/grid-router.js +190 -0
- package/dist/layout/grid/grid.d.ts +43 -0
- package/dist/layout/grid/grid.js +174 -0
- package/dist/layout/grid/walker.d.ts +11 -0
- package/dist/layout/grid/walker.js +126 -0
- package/dist/layout/index.d.ts +2 -5
- package/dist/layout/index.js +1 -4
- package/dist/layout/layout-engine.d.ts +5 -15
- package/dist/layout/layout-engine.js +8 -478
- package/dist/layout/types.d.ts +2 -2
- package/dist/layout/types.js +6 -2
- package/dist/xml/xml-parser.js +5 -0
- package/package.json +1 -1
- package/dist/layout/astar.d.ts +0 -15
- package/dist/layout/astar.js +0 -191
- package/dist/layout/block-builder.d.ts +0 -37
- package/dist/layout/block-builder.js +0 -154
- package/dist/layout/block-layout.d.ts +0 -9
- package/dist/layout/block-layout.js +0 -163
- package/dist/layout/coordinates.d.ts +0 -85
- package/dist/layout/coordinates.js +0 -1392
- package/dist/layout/crossing.d.ts +0 -8
- package/dist/layout/crossing.js +0 -60
- package/dist/layout/graph.d.ts +0 -28
- package/dist/layout/graph.js +0 -126
- package/dist/layout/layers.d.ts +0 -13
- package/dist/layout/layers.js +0 -49
- package/dist/layout/routing.d.ts +0 -33
- package/dist/layout/routing.js +0 -622
- package/dist/layout/subprocess.d.ts +0 -14
- 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
|
package/dist/layout/index.d.ts
CHANGED
|
@@ -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
|
package/dist/layout/index.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|