@bpmnkit/core 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +185 -0
  2. package/dist/bpmn/auto-layout.d.ts +10 -0
  3. package/dist/bpmn/auto-layout.js +203 -0
  4. package/dist/bpmn/bpmn-builder.d.ts +381 -0
  5. package/dist/bpmn/bpmn-builder.js +1223 -0
  6. package/dist/bpmn/bpmn-model.d.ts +403 -0
  7. package/dist/bpmn/bpmn-model.js +2 -0
  8. package/dist/bpmn/bpmn-parser.d.ts +4 -0
  9. package/dist/bpmn/bpmn-parser.js +642 -0
  10. package/dist/bpmn/bpmn-serializer.d.ts +4 -0
  11. package/dist/bpmn/bpmn-serializer.js +515 -0
  12. package/dist/bpmn/compact.d.ts +91 -0
  13. package/dist/bpmn/compact.js +349 -0
  14. package/dist/bpmn/di-color.d.ts +9 -0
  15. package/dist/bpmn/di-color.js +32 -0
  16. package/dist/bpmn/index.d.ts +105 -0
  17. package/dist/bpmn/index.js +176 -0
  18. package/dist/bpmn/optimize/feel.d.ts +4 -0
  19. package/dist/bpmn/optimize/feel.js +186 -0
  20. package/dist/bpmn/optimize/flow.d.ts +4 -0
  21. package/dist/bpmn/optimize/flow.js +323 -0
  22. package/dist/bpmn/optimize/index.d.ts +5 -0
  23. package/dist/bpmn/optimize/index.js +51 -0
  24. package/dist/bpmn/optimize/naming.d.ts +8 -0
  25. package/dist/bpmn/optimize/naming.js +145 -0
  26. package/dist/bpmn/optimize/tasks.d.ts +4 -0
  27. package/dist/bpmn/optimize/tasks.js +273 -0
  28. package/dist/bpmn/optimize/types.d.ts +44 -0
  29. package/dist/bpmn/optimize/types.js +2 -0
  30. package/dist/bpmn/optimize/utils.d.ts +23 -0
  31. package/dist/bpmn/optimize/utils.js +129 -0
  32. package/dist/bpmn/rest-connector.d.ts +48 -0
  33. package/dist/bpmn/rest-connector.js +69 -0
  34. package/dist/bpmn/svg.d.ts +35 -0
  35. package/dist/bpmn/svg.js +557 -0
  36. package/dist/bpmn/type-guards.d.ts +136 -0
  37. package/dist/bpmn/type-guards.js +211 -0
  38. package/dist/bpmn/utils.d.ts +88 -0
  39. package/dist/bpmn/utils.js +207 -0
  40. package/dist/bpmn/zeebe-extensions.d.ts +72 -0
  41. package/dist/bpmn/zeebe-extensions.js +96 -0
  42. package/dist/dmn/compact.d.ts +44 -0
  43. package/dist/dmn/compact.js +125 -0
  44. package/dist/dmn/dmn-builder.d.ts +59 -0
  45. package/dist/dmn/dmn-builder.js +144 -0
  46. package/dist/dmn/dmn-layout.d.ts +57 -0
  47. package/dist/dmn/dmn-layout.js +256 -0
  48. package/dist/dmn/dmn-model.d.ts +158 -0
  49. package/dist/dmn/dmn-model.js +2 -0
  50. package/dist/dmn/dmn-parser.d.ts +4 -0
  51. package/dist/dmn/dmn-parser.js +275 -0
  52. package/dist/dmn/dmn-serializer.d.ts +4 -0
  53. package/dist/dmn/dmn-serializer.js +296 -0
  54. package/dist/dmn/index.d.ts +28 -0
  55. package/dist/dmn/index.js +51 -0
  56. package/dist/errors.d.ts +77 -0
  57. package/dist/errors.js +66 -0
  58. package/dist/form/compact.d.ts +20 -0
  59. package/dist/form/compact.js +146 -0
  60. package/dist/form/form-builder.d.ts +111 -0
  61. package/dist/form/form-builder.js +347 -0
  62. package/dist/form/form-model.d.ts +221 -0
  63. package/dist/form/form-model.js +2 -0
  64. package/dist/form/form-parser.d.ts +4 -0
  65. package/dist/form/form-parser.js +514 -0
  66. package/dist/form/form-serializer.d.ts +4 -0
  67. package/dist/form/form-serializer.js +240 -0
  68. package/dist/form/index.d.ts +27 -0
  69. package/dist/form/index.js +43 -0
  70. package/dist/index.d.ts +37 -0
  71. package/dist/index.js +19 -0
  72. package/dist/layout/bench.d.ts +86 -0
  73. package/dist/layout/bench.js +225 -0
  74. package/dist/layout/coordinates.d.ts +62 -0
  75. package/dist/layout/coordinates.js +868 -0
  76. package/dist/layout/crossing.d.ts +8 -0
  77. package/dist/layout/crossing.js +60 -0
  78. package/dist/layout/graph.d.ts +28 -0
  79. package/dist/layout/graph.js +126 -0
  80. package/dist/layout/index.d.ts +7 -0
  81. package/dist/layout/index.js +5 -0
  82. package/dist/layout/layers.d.ts +13 -0
  83. package/dist/layout/layers.js +49 -0
  84. package/dist/layout/layout-engine.d.ts +21 -0
  85. package/dist/layout/layout-engine.js +191 -0
  86. package/dist/layout/overlap.d.ts +10 -0
  87. package/dist/layout/overlap.js +70 -0
  88. package/dist/layout/routing.d.ts +26 -0
  89. package/dist/layout/routing.js +439 -0
  90. package/dist/layout/subprocess.d.ts +14 -0
  91. package/dist/layout/subprocess.js +77 -0
  92. package/dist/layout/types.d.ts +72 -0
  93. package/dist/layout/types.js +37 -0
  94. package/dist/types/id-generator.d.ts +8 -0
  95. package/dist/types/id-generator.js +31 -0
  96. package/dist/types/index.d.ts +3 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/xml-element.d.ts +12 -0
  99. package/dist/types/xml-element.js +2 -0
  100. package/dist/xml/index.d.ts +2 -0
  101. package/dist/xml/index.js +2 -0
  102. package/dist/xml/xml-parser.d.ts +13 -0
  103. package/dist/xml/xml-parser.js +271 -0
  104. package/package.json +38 -0
@@ -0,0 +1,8 @@
1
+ import type { DirectedGraph } from "./graph.js";
2
+ /**
3
+ * Minimize edge crossings using the barycenter heuristic.
4
+ * For each layer, order nodes by the average position of their neighbors
5
+ * in the adjacent layer. Iterates a fixed number of passes.
6
+ */
7
+ export declare function minimizeCrossings(layerGroups: string[][], graph: DirectedGraph, iterations?: number): string[][];
8
+ //# sourceMappingURL=crossing.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Minimize edge crossings using the barycenter heuristic.
3
+ * For each layer, order nodes by the average position of their neighbors
4
+ * in the adjacent layer. Iterates a fixed number of passes.
5
+ */
6
+ export function minimizeCrossings(layerGroups, graph, iterations = 4) {
7
+ const result = layerGroups.map((layer) => [...layer]);
8
+ for (let iter = 0; iter < iterations; iter++) {
9
+ // Forward sweep (left to right): order by predecessor positions
10
+ for (let i = 1; i < result.length; i++) {
11
+ orderByBarycenter(result, i, graph, "predecessors");
12
+ }
13
+ // Backward sweep (right to left): order by successor positions
14
+ for (let i = result.length - 2; i >= 0; i--) {
15
+ orderByBarycenter(result, i, graph, "successors");
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ function orderByBarycenter(layers, layerIndex, graph, direction) {
21
+ const layer = layers[layerIndex];
22
+ if (!layer)
23
+ return;
24
+ const adjacentLayer = direction === "predecessors" ? layers[layerIndex - 1] : layers[layerIndex + 1];
25
+ if (!adjacentLayer || adjacentLayer.length === 0)
26
+ return;
27
+ // Build position index for the adjacent layer
28
+ const posIndex = new Map();
29
+ for (let i = 0; i < adjacentLayer.length; i++) {
30
+ const adjNode = adjacentLayer[i];
31
+ if (!adjNode)
32
+ continue;
33
+ posIndex.set(adjNode, i);
34
+ }
35
+ // Compute barycenter for each node in current layer
36
+ const barycenters = new Map();
37
+ for (const nodeId of layer) {
38
+ const neighbors = direction === "predecessors"
39
+ ? (graph.predecessors.get(nodeId) ?? [])
40
+ : (graph.successors.get(nodeId) ?? []);
41
+ const adjacentPositions = [];
42
+ for (const neighbor of neighbors) {
43
+ const pos = posIndex.get(neighbor);
44
+ if (pos !== undefined) {
45
+ adjacentPositions.push(pos);
46
+ }
47
+ }
48
+ if (adjacentPositions.length > 0) {
49
+ const sum = adjacentPositions.reduce((a, b) => a + b, 0);
50
+ barycenters.set(nodeId, sum / adjacentPositions.length);
51
+ }
52
+ else {
53
+ // Keep original position for nodes without connections
54
+ barycenters.set(nodeId, layer.indexOf(nodeId));
55
+ }
56
+ }
57
+ // Sort by barycenter, preserving relative order for equal values
58
+ layer.sort((a, b) => (barycenters.get(a) ?? 0) - (barycenters.get(b) ?? 0));
59
+ }
60
+ //# sourceMappingURL=crossing.js.map
@@ -0,0 +1,28 @@
1
+ import type { BpmnFlowElement, BpmnSequenceFlow } from "../bpmn/bpmn-model.js";
2
+ /** Adjacency list representation of a directed graph. */
3
+ export interface DirectedGraph {
4
+ /** Node IDs. */
5
+ nodes: string[];
6
+ /** Adjacency: nodeId → list of successor nodeIds. */
7
+ successors: Map<string, string[]>;
8
+ /** Reverse adjacency: nodeId → list of predecessor nodeIds. */
9
+ predecessors: Map<string, string[]>;
10
+ }
11
+ /** Back-edge detected during DFS. */
12
+ export interface BackEdge {
13
+ flowId: string;
14
+ sourceRef: string;
15
+ targetRef: string;
16
+ }
17
+ /** Build a directed graph from BPMN flow nodes and sequence flows. */
18
+ export declare function buildGraph(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): DirectedGraph;
19
+ /**
20
+ * Detect back-edges via DFS.
21
+ * A back-edge is an edge where the target is an ancestor of the source in the DFS tree.
22
+ */
23
+ export declare function detectBackEdges(graph: DirectedGraph, sequenceFlows: BpmnSequenceFlow[]): BackEdge[];
24
+ /** Reverse back-edges in the graph to make it a DAG. Returns the modified graph. */
25
+ export declare function reverseBackEdges(graph: DirectedGraph, backEdges: BackEdge[]): DirectedGraph;
26
+ /** Topological sort of a DAG. Throws if cycles remain. */
27
+ export declare function topologicalSort(graph: DirectedGraph): string[];
28
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1,126 @@
1
+ /** Build a directed graph from BPMN flow nodes and sequence flows. */
2
+ export function buildGraph(flowNodes, sequenceFlows) {
3
+ const nodes = flowNodes.map((n) => n.id);
4
+ const successors = new Map();
5
+ const predecessors = new Map();
6
+ for (const id of nodes) {
7
+ successors.set(id, []);
8
+ predecessors.set(id, []);
9
+ }
10
+ for (const flow of sequenceFlows) {
11
+ const succs = successors.get(flow.sourceRef);
12
+ if (succs)
13
+ succs.push(flow.targetRef);
14
+ const preds = predecessors.get(flow.targetRef);
15
+ if (preds)
16
+ preds.push(flow.sourceRef);
17
+ }
18
+ return { nodes, successors, predecessors };
19
+ }
20
+ /**
21
+ * Detect back-edges via DFS.
22
+ * A back-edge is an edge where the target is an ancestor of the source in the DFS tree.
23
+ */
24
+ export function detectBackEdges(graph, sequenceFlows) {
25
+ const WHITE = 0;
26
+ const GRAY = 1;
27
+ const BLACK = 2;
28
+ const color = new Map();
29
+ for (const id of graph.nodes) {
30
+ color.set(id, WHITE);
31
+ }
32
+ const backEdges = [];
33
+ const flowIndex = new Map();
34
+ for (const flow of sequenceFlows) {
35
+ flowIndex.set(`${flow.sourceRef}->${flow.targetRef}`, flow);
36
+ }
37
+ function dfs(nodeId) {
38
+ color.set(nodeId, GRAY);
39
+ const succs = graph.successors.get(nodeId) ?? [];
40
+ for (const succ of succs) {
41
+ const c = color.get(succ);
42
+ if (c === GRAY) {
43
+ const flow = flowIndex.get(`${nodeId}->${succ}`);
44
+ if (flow) {
45
+ backEdges.push({
46
+ flowId: flow.id,
47
+ sourceRef: nodeId,
48
+ targetRef: succ,
49
+ });
50
+ }
51
+ }
52
+ else if (c === WHITE) {
53
+ dfs(succ);
54
+ }
55
+ }
56
+ color.set(nodeId, BLACK);
57
+ }
58
+ for (const id of graph.nodes) {
59
+ if (color.get(id) === WHITE) {
60
+ dfs(id);
61
+ }
62
+ }
63
+ return backEdges;
64
+ }
65
+ /** Reverse back-edges in the graph to make it a DAG. Returns the modified graph. */
66
+ export function reverseBackEdges(graph, backEdges) {
67
+ const successors = new Map();
68
+ const predecessors = new Map();
69
+ for (const id of graph.nodes) {
70
+ successors.set(id, [...(graph.successors.get(id) ?? [])]);
71
+ predecessors.set(id, [...(graph.predecessors.get(id) ?? [])]);
72
+ }
73
+ for (const be of backEdges) {
74
+ // Remove forward direction
75
+ const succs = successors.get(be.sourceRef);
76
+ if (succs) {
77
+ const idx = succs.indexOf(be.targetRef);
78
+ if (idx >= 0)
79
+ succs.splice(idx, 1);
80
+ }
81
+ const preds = predecessors.get(be.targetRef);
82
+ if (preds) {
83
+ const idx = preds.indexOf(be.sourceRef);
84
+ if (idx >= 0)
85
+ preds.splice(idx, 1);
86
+ }
87
+ // Add reversed direction
88
+ const revSuccs = successors.get(be.targetRef);
89
+ if (revSuccs)
90
+ revSuccs.push(be.sourceRef);
91
+ const revPreds = predecessors.get(be.sourceRef);
92
+ if (revPreds)
93
+ revPreds.push(be.targetRef);
94
+ }
95
+ return { nodes: [...graph.nodes], successors, predecessors };
96
+ }
97
+ /** Topological sort of a DAG. Throws if cycles remain. */
98
+ export function topologicalSort(graph) {
99
+ const inDegree = new Map();
100
+ for (const id of graph.nodes) {
101
+ inDegree.set(id, (graph.predecessors.get(id) ?? []).length);
102
+ }
103
+ const queue = [];
104
+ for (const [id, deg] of inDegree) {
105
+ if (deg === 0)
106
+ queue.push(id);
107
+ }
108
+ const sorted = [];
109
+ while (queue.length > 0) {
110
+ const node = queue.shift();
111
+ if (!node)
112
+ break;
113
+ sorted.push(node);
114
+ for (const succ of graph.successors.get(node) ?? []) {
115
+ const newDeg = (inDegree.get(succ) ?? 1) - 1;
116
+ inDegree.set(succ, newDeg);
117
+ if (newDeg === 0)
118
+ queue.push(succ);
119
+ }
120
+ }
121
+ if (sorted.length !== graph.nodes.length) {
122
+ throw new Error(`Layout error: graph contains cycles after back-edge reversal (sorted ${sorted.length} of ${graph.nodes.length} nodes)`);
123
+ }
124
+ return sorted;
125
+ }
126
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1,7 @@
1
+ export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
+ export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
3
+ export type { BenchmarkResult, BoundingBox, ElementComparison, ElementPosition, FlowOrderViolation, } from "./bench.js";
4
+ export { assertNoOverlap } from "./overlap.js";
5
+ export type { Bounds, LayoutEdge, LayoutNode, LayoutResult, Waypoint } from "./types.js";
6
+ 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";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
+ export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
3
+ export { assertNoOverlap } from "./overlap.js";
4
+ 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";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import type { DirectedGraph } from "./graph.js";
2
+ /**
3
+ * Assign layers using longest-path algorithm.
4
+ * Each node gets a layer equal to the longest path from any source to it.
5
+ * This produces a left-to-right layout where layers represent columns.
6
+ */
7
+ export declare function assignLayers(graph: DirectedGraph): Map<string, number>;
8
+ /**
9
+ * Group nodes by their assigned layer.
10
+ * Returns an array of arrays, where index = layer number.
11
+ */
12
+ export declare function groupByLayer(layers: Map<string, number>): string[][];
13
+ //# sourceMappingURL=layers.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { topologicalSort } from "./graph.js";
2
+ /**
3
+ * Assign layers using longest-path algorithm.
4
+ * Each node gets a layer equal to the longest path from any source to it.
5
+ * This produces a left-to-right layout where layers represent columns.
6
+ */
7
+ export function assignLayers(graph) {
8
+ const sorted = topologicalSort(graph);
9
+ const layers = new Map();
10
+ // Initialize all nodes at layer 0
11
+ for (const id of sorted) {
12
+ layers.set(id, 0);
13
+ }
14
+ // Forward pass: each node's layer = max(predecessor layers) + 1
15
+ for (const id of sorted) {
16
+ const preds = graph.predecessors.get(id) ?? [];
17
+ if (preds.length > 0) {
18
+ let maxPredLayer = 0;
19
+ for (const pred of preds) {
20
+ const predLayer = layers.get(pred) ?? 0;
21
+ if (predLayer >= maxPredLayer) {
22
+ maxPredLayer = predLayer + 1;
23
+ }
24
+ }
25
+ layers.set(id, maxPredLayer);
26
+ }
27
+ }
28
+ return layers;
29
+ }
30
+ /**
31
+ * Group nodes by their assigned layer.
32
+ * Returns an array of arrays, where index = layer number.
33
+ */
34
+ export function groupByLayer(layers) {
35
+ let maxLayer = 0;
36
+ for (const layer of layers.values()) {
37
+ if (layer > maxLayer)
38
+ maxLayer = layer;
39
+ }
40
+ const groups = [];
41
+ for (let i = 0; i <= maxLayer; i++) {
42
+ groups.push([]);
43
+ }
44
+ for (const [id, layer] of layers) {
45
+ groups[layer]?.push(id);
46
+ }
47
+ return groups;
48
+ }
49
+ //# sourceMappingURL=layers.js.map
@@ -0,0 +1,21 @@
1
+ import type { BpmnFlowElement, BpmnProcess, BpmnSequenceFlow } from "../bpmn/bpmn-model.js";
2
+ import type { LayoutResult } from "./types.js";
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. Overlap assertion — Post-condition validation
14
+ */
15
+ export declare function layoutProcess(process: BpmnProcess): LayoutResult;
16
+ /**
17
+ * Layout a set of flow nodes and sequence flows.
18
+ * Used both for top-level processes and recursively for sub-processes.
19
+ */
20
+ export declare function layoutFlowNodes(flowNodes: BpmnFlowElement[], sequenceFlows: BpmnSequenceFlow[]): LayoutResult;
21
+ //# sourceMappingURL=layout-engine.d.ts.map
@@ -0,0 +1,191 @@
1
+ import { alignBaselinePath, alignBranchBaselines, alignSplitJoinPairs, assignCoordinates, distributeSplitBranches, ensureEarlyReturnOffBaseline, resolveLayerOverlaps, } from "./coordinates.js";
2
+ import { minimizeCrossings } from "./crossing.js";
3
+ import { buildGraph, detectBackEdges, reverseBackEdges } from "./graph.js";
4
+ import { assignLayers, groupByLayer } from "./layers.js";
5
+ import { assertNoOverlap } from "./overlap.js";
6
+ import { routeEdges } from "./routing.js";
7
+ import { layoutSubProcesses } from "./subprocess.js";
8
+ /**
9
+ * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
10
+ *
11
+ * Phases:
12
+ * 1. Cycle removal — DFS back-edge detection and reversal
13
+ * 2. Layer assignment — Longest-path layering
14
+ * 3. Crossing minimization — Barycenter heuristic
15
+ * 4. Coordinate assignment — Fixed element sizes with spacing
16
+ * 5. Sub-process layout — Recursive nested passes
17
+ * 6. Edge routing — Orthogonal waypoints
18
+ * 7. Overlap assertion — Post-condition validation
19
+ */
20
+ export function layoutProcess(process) {
21
+ const result = layoutFlowNodes(process.flowElements, process.sequenceFlows);
22
+ assertNoOverlap(result);
23
+ return result;
24
+ }
25
+ /**
26
+ * Layout a set of flow nodes and sequence flows.
27
+ * Used both for top-level processes and recursively for sub-processes.
28
+ */
29
+ export function layoutFlowNodes(flowNodes, sequenceFlows) {
30
+ if (flowNodes.length === 0) {
31
+ return { nodes: [], edges: [] };
32
+ }
33
+ // Build node index
34
+ const nodeIndex = new Map();
35
+ for (const node of flowNodes) {
36
+ nodeIndex.set(node.id, node);
37
+ }
38
+ // Phase 1: Build graph and detect/remove cycles
39
+ const graph = buildGraph(flowNodes, sequenceFlows);
40
+ const backEdges = detectBackEdges(graph, sequenceFlows);
41
+ const dag = backEdges.length > 0 ? reverseBackEdges(graph, backEdges) : graph;
42
+ // Phase 2: Layer assignment
43
+ const layers = assignLayers(dag);
44
+ // Phase 3: Group by layer and minimize crossings
45
+ const layerGroups = groupByLayer(layers);
46
+ const orderedLayers = minimizeCrossings(layerGroups, dag);
47
+ // Phase 4: Coordinate assignment
48
+ const layoutNodes = assignCoordinates(orderedLayers, nodeIndex);
49
+ // Phase 4b: Align linear sequences to a common y-baseline
50
+ alignBranchBaselines(layoutNodes, dag);
51
+ // Phase 4c: Align split/join gateway pairs to same y-coordinate
52
+ alignSplitJoinPairs(layoutNodes, dag, backEdges);
53
+ // Phase 4d: Align all baseline-path nodes to the same center-Y
54
+ alignBaselinePath(layoutNodes, dag, backEdges);
55
+ // Phase 4e: Ensure early-return branches are never on the baseline
56
+ ensureEarlyReturnOffBaseline(layoutNodes, dag, backEdges);
57
+ // Phase 4f: Distribute split gateway branches symmetrically
58
+ distributeSplitBranches(layoutNodes, dag, backEdges);
59
+ // Phase 4g: Resolve any layer overlaps from redistribution
60
+ resolveLayerOverlaps(layoutNodes);
61
+ // Phase 4h: Re-align baseline after overlap resolution (overlap resolution may push
62
+ // baseline nodes off-center when they share a layer with branch nodes)
63
+ alignBaselinePath(layoutNodes, dag, backEdges);
64
+ // Phase 4i: Final overlap resolution — baseline re-alignment may pull a node back into
65
+ // an overlap that resolveLayerOverlaps already fixed; one more pass eliminates these.
66
+ resolveLayerOverlaps(layoutNodes);
67
+ // Phase 5: Sub-process layout — expand containers and lay out children
68
+ const childResults = layoutSubProcesses(layoutNodes, nodeIndex);
69
+ // After subprocess expansion, push nodes that now overlap with expanded containers
70
+ resolveSubProcessOverlaps(layoutNodes);
71
+ // Phase 5b: Resolve Y-direction overlaps caused by subprocess expansion.
72
+ // Expanded subprocesses grow in-place and can overlap same-layer siblings.
73
+ resolveLayerOverlaps(layoutNodes);
74
+ // Phase 5c: Sync child positions to their subprocess containers.
75
+ // resolveLayerOverlaps (including its Y-normalization pass) may have shifted
76
+ // subprocess containers after their children were already translated to
77
+ // absolute coordinates — children must follow.
78
+ syncSubProcessChildren(childResults, layoutNodes);
79
+ // Phase 6: Edge routing (uses original back-edges for routing, not reversed)
80
+ const nodeMap = new Map();
81
+ for (const node of layoutNodes) {
82
+ nodeMap.set(node.id, node);
83
+ }
84
+ const edges = routeEdges(sequenceFlows, nodeMap, backEdges);
85
+ // Flatten child results into the main layout
86
+ const allNodes = [...layoutNodes];
87
+ const allEdges = [...edges];
88
+ for (const child of childResults) {
89
+ for (const cn of child.result.nodes) {
90
+ allNodes.push(cn);
91
+ }
92
+ for (const ce of child.result.edges) {
93
+ allEdges.push(ce);
94
+ }
95
+ }
96
+ return { nodes: allNodes, edges: allEdges };
97
+ }
98
+ /**
99
+ * After subprocess expansion, cascade-shift all subsequent layers
100
+ * so that inter-layer spacing is preserved.
101
+ */
102
+ function resolveSubProcessOverlaps(nodes) {
103
+ const expanded = nodes.filter((n) => n.isExpanded);
104
+ if (expanded.length === 0)
105
+ return;
106
+ // Group nodes by layer
107
+ const byLayer = new Map();
108
+ for (const n of nodes) {
109
+ const arr = byLayer.get(n.layer);
110
+ if (arr)
111
+ arr.push(n);
112
+ else
113
+ byLayer.set(n.layer, [n]);
114
+ }
115
+ const layers = [...byLayer.keys()].sort((a, b) => a - b);
116
+ const MIN_GAP = 50;
117
+ // Cascade: ensure each layer starts after previous layer's rightmost edge
118
+ for (let i = 1; i < layers.length; i++) {
119
+ const prevKey = layers[i - 1];
120
+ const curKey = layers[i];
121
+ if (prevKey === undefined || curKey === undefined)
122
+ continue;
123
+ const prevNodes = byLayer.get(prevKey);
124
+ const curNodes = byLayer.get(curKey);
125
+ if (!prevNodes || !curNodes)
126
+ continue;
127
+ // Find rightmost edge in previous layer (including labels)
128
+ let prevRight = 0;
129
+ for (const n of prevNodes) {
130
+ prevRight = Math.max(prevRight, n.bounds.x + n.bounds.width);
131
+ if (n.labelBounds) {
132
+ prevRight = Math.max(prevRight, n.labelBounds.x + n.labelBounds.width);
133
+ }
134
+ }
135
+ // Find leftmost edge in current layer
136
+ let curLeft = Number.POSITIVE_INFINITY;
137
+ for (const n of curNodes) {
138
+ curLeft = Math.min(curLeft, n.bounds.x);
139
+ }
140
+ const gap = curLeft - prevRight;
141
+ if (gap < MIN_GAP) {
142
+ const dx = MIN_GAP - gap;
143
+ for (const n of curNodes) {
144
+ n.bounds.x += dx;
145
+ if (n.labelBounds) {
146
+ n.labelBounds.x += dx;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * After post-expansion adjustments (resolveSubProcessOverlaps, resolveLayerOverlaps),
154
+ * subprocess containers may have been shifted. Translate their children by the same
155
+ * delta so that children remain correctly positioned inside their parent.
156
+ */
157
+ function syncSubProcessChildren(childResults, layoutNodes) {
158
+ if (childResults.length === 0)
159
+ return;
160
+ const nodeMap = new Map();
161
+ for (const n of layoutNodes)
162
+ nodeMap.set(n.id, n);
163
+ for (const cr of childResults) {
164
+ const parent = nodeMap.get(cr.parentId);
165
+ if (!parent)
166
+ continue;
167
+ const dx = parent.bounds.x - cr.parentX;
168
+ const dy = parent.bounds.y - cr.parentY;
169
+ if (dx === 0 && dy === 0)
170
+ continue;
171
+ for (const child of cr.result.nodes) {
172
+ child.bounds.x += dx;
173
+ child.bounds.y += dy;
174
+ if (child.labelBounds) {
175
+ child.labelBounds.x += dx;
176
+ child.labelBounds.y += dy;
177
+ }
178
+ }
179
+ for (const edge of cr.result.edges) {
180
+ for (const wp of edge.waypoints) {
181
+ wp.x += dx;
182
+ wp.y += dy;
183
+ }
184
+ if (edge.labelBounds) {
185
+ edge.labelBounds.x += dx;
186
+ edge.labelBounds.y += dy;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ //# sourceMappingURL=layout-engine.js.map
@@ -0,0 +1,10 @@
1
+ import type { LayoutResult } from "./types.js";
2
+ /**
3
+ * Assert that no two element bounding boxes overlap and
4
+ * no element overlaps a label.
5
+ * Label-vs-label overlaps are allowed when both labels belong to
6
+ * a node and an edge connected to that node.
7
+ * Throws if any overlap is detected.
8
+ */
9
+ export declare function assertNoOverlap(result: LayoutResult): void;
10
+ //# sourceMappingURL=overlap.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Assert that no two element bounding boxes overlap and
3
+ * no element overlaps a label.
4
+ * Label-vs-label overlaps are allowed when both labels belong to
5
+ * a node and an edge connected to that node.
6
+ * Throws if any overlap is detected.
7
+ */
8
+ export function assertNoOverlap(result) {
9
+ const allBounds = [];
10
+ for (const node of result.nodes) {
11
+ allBounds.push({ id: node.id, kind: "element", bounds: node.bounds });
12
+ if (node.labelBounds) {
13
+ allBounds.push({ id: `${node.id}-label`, kind: "label", bounds: node.labelBounds });
14
+ }
15
+ }
16
+ // Track which nodes are connected to which edges
17
+ const edgeEndpoints = new Set();
18
+ for (const edge of result.edges) {
19
+ if (edge.labelBounds) {
20
+ allBounds.push({ id: `${edge.id}-label`, kind: "label", bounds: edge.labelBounds });
21
+ }
22
+ edgeEndpoints.add(`${edge.id}:${edge.sourceRef}`);
23
+ edgeEndpoints.add(`${edge.id}:${edge.targetRef}`);
24
+ }
25
+ for (let i = 0; i < allBounds.length; i++) {
26
+ for (let j = i + 1; j < allBounds.length; j++) {
27
+ const a = allBounds[i];
28
+ if (!a)
29
+ continue;
30
+ const b = allBounds[j];
31
+ if (!b)
32
+ continue;
33
+ // Skip label-to-same-element overlap checks (labels belong to their element)
34
+ if (a.id.replace("-label", "") === b.id.replace("-label", ""))
35
+ continue;
36
+ // Skip label-vs-label overlaps between a node and its connected edge
37
+ if (a.kind === "label" && b.kind === "label") {
38
+ if (areConnectedLabels(a.id, b.id, edgeEndpoints))
39
+ continue;
40
+ }
41
+ // Skip checking elements that are in a parent-child relationship
42
+ // (sub-process children are inside the sub-process bounds by design)
43
+ if (boundsOverlap(a.bounds, b.bounds)) {
44
+ // Check if one is a sub-process containing the other
45
+ if (isContainedWithin(a.bounds, b.bounds) || isContainedWithin(b.bounds, a.bounds)) {
46
+ continue;
47
+ }
48
+ throw new Error(`Layout overlap detected: ${a.kind} "${a.id}" overlaps with ${b.kind} "${b.id}"`);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ /** Check if two labels belong to a node and an edge connected to that node. */
54
+ function areConnectedLabels(idA, idB, edgeEndpoints) {
55
+ const baseA = idA.replace("-label", "");
56
+ const baseB = idB.replace("-label", "");
57
+ return edgeEndpoints.has(`${baseA}:${baseB}`) || edgeEndpoints.has(`${baseB}:${baseA}`);
58
+ }
59
+ /** Check if two bounding boxes overlap (exclusive of touching edges). */
60
+ function boundsOverlap(a, b) {
61
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
62
+ }
63
+ /** Check if bounds `inner` is fully contained within `outer`. */
64
+ function isContainedWithin(inner, outer) {
65
+ return (inner.x >= outer.x &&
66
+ inner.y >= outer.y &&
67
+ inner.x + inner.width <= outer.x + outer.width &&
68
+ inner.y + inner.height <= outer.y + outer.height);
69
+ }
70
+ //# sourceMappingURL=overlap.js.map
@@ -0,0 +1,26 @@
1
+ import type { BpmnSequenceFlow } from "../bpmn/bpmn-model.js";
2
+ import type { BackEdge } from "./graph.js";
3
+ import type { LayoutEdge, LayoutNode } from "./types.js";
4
+ /** Port side for gateway edge connection. */
5
+ export type PortSide = "right" | "top" | "bottom";
6
+ /**
7
+ * Determine which side of the target a forward edge should connect to.
8
+ * Non-gateway targets always receive edges from the left side.
9
+ * Split gateways (starting): incoming always from the left.
10
+ * Join gateways (closing): incoming based on relative position (top/bottom/left).
11
+ */
12
+ export declare function resolveTargetPort(source: LayoutNode, target: LayoutNode, joinGateways: ReadonlySet<string>): "left" | "top" | "bottom";
13
+ /**
14
+ * Assign source ports for outgoing edges of a gateway.
15
+ * - Single output: right port.
16
+ * - Odd count: middle (by target y) → right, upper half → top, lower half → bottom.
17
+ * - Even count: upper half → top, lower half → bottom, no right port.
18
+ */
19
+ export declare function assignGatewayPorts(outgoingFlows: BpmnSequenceFlow[], nodeMap: Map<string, LayoutNode>): Map<string, PortSide>;
20
+ /**
21
+ * Route edges with orthogonal (horizontal + vertical) segments.
22
+ * Forward edges go left-to-right; back-edges route above or below.
23
+ * Gateway sources use port-based routing (top/right/bottom).
24
+ */
25
+ export declare function routeEdges(sequenceFlows: BpmnSequenceFlow[], nodeMap: Map<string, LayoutNode>, backEdges: BackEdge[]): LayoutEdge[];
26
+ //# sourceMappingURL=routing.d.ts.map