@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.
- package/README.md +185 -0
- package/dist/bpmn/auto-layout.d.ts +10 -0
- package/dist/bpmn/auto-layout.js +203 -0
- package/dist/bpmn/bpmn-builder.d.ts +381 -0
- package/dist/bpmn/bpmn-builder.js +1223 -0
- package/dist/bpmn/bpmn-model.d.ts +403 -0
- package/dist/bpmn/bpmn-model.js +2 -0
- package/dist/bpmn/bpmn-parser.d.ts +4 -0
- package/dist/bpmn/bpmn-parser.js +642 -0
- package/dist/bpmn/bpmn-serializer.d.ts +4 -0
- package/dist/bpmn/bpmn-serializer.js +515 -0
- package/dist/bpmn/compact.d.ts +91 -0
- package/dist/bpmn/compact.js +349 -0
- package/dist/bpmn/di-color.d.ts +9 -0
- package/dist/bpmn/di-color.js +32 -0
- package/dist/bpmn/index.d.ts +105 -0
- package/dist/bpmn/index.js +176 -0
- package/dist/bpmn/optimize/feel.d.ts +4 -0
- package/dist/bpmn/optimize/feel.js +186 -0
- package/dist/bpmn/optimize/flow.d.ts +4 -0
- package/dist/bpmn/optimize/flow.js +323 -0
- package/dist/bpmn/optimize/index.d.ts +5 -0
- package/dist/bpmn/optimize/index.js +51 -0
- package/dist/bpmn/optimize/naming.d.ts +8 -0
- package/dist/bpmn/optimize/naming.js +145 -0
- package/dist/bpmn/optimize/tasks.d.ts +4 -0
- package/dist/bpmn/optimize/tasks.js +273 -0
- package/dist/bpmn/optimize/types.d.ts +44 -0
- package/dist/bpmn/optimize/types.js +2 -0
- package/dist/bpmn/optimize/utils.d.ts +23 -0
- package/dist/bpmn/optimize/utils.js +129 -0
- package/dist/bpmn/rest-connector.d.ts +48 -0
- package/dist/bpmn/rest-connector.js +69 -0
- package/dist/bpmn/svg.d.ts +35 -0
- package/dist/bpmn/svg.js +557 -0
- package/dist/bpmn/type-guards.d.ts +136 -0
- package/dist/bpmn/type-guards.js +211 -0
- package/dist/bpmn/utils.d.ts +88 -0
- package/dist/bpmn/utils.js +207 -0
- package/dist/bpmn/zeebe-extensions.d.ts +72 -0
- package/dist/bpmn/zeebe-extensions.js +96 -0
- package/dist/dmn/compact.d.ts +44 -0
- package/dist/dmn/compact.js +125 -0
- package/dist/dmn/dmn-builder.d.ts +59 -0
- package/dist/dmn/dmn-builder.js +144 -0
- package/dist/dmn/dmn-layout.d.ts +57 -0
- package/dist/dmn/dmn-layout.js +256 -0
- package/dist/dmn/dmn-model.d.ts +158 -0
- package/dist/dmn/dmn-model.js +2 -0
- package/dist/dmn/dmn-parser.d.ts +4 -0
- package/dist/dmn/dmn-parser.js +275 -0
- package/dist/dmn/dmn-serializer.d.ts +4 -0
- package/dist/dmn/dmn-serializer.js +296 -0
- package/dist/dmn/index.d.ts +28 -0
- package/dist/dmn/index.js +51 -0
- package/dist/errors.d.ts +77 -0
- package/dist/errors.js +66 -0
- package/dist/form/compact.d.ts +20 -0
- package/dist/form/compact.js +146 -0
- package/dist/form/form-builder.d.ts +111 -0
- package/dist/form/form-builder.js +347 -0
- package/dist/form/form-model.d.ts +221 -0
- package/dist/form/form-model.js +2 -0
- package/dist/form/form-parser.d.ts +4 -0
- package/dist/form/form-parser.js +514 -0
- package/dist/form/form-serializer.d.ts +4 -0
- package/dist/form/form-serializer.js +240 -0
- package/dist/form/index.d.ts +27 -0
- package/dist/form/index.js +43 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +19 -0
- package/dist/layout/bench.d.ts +86 -0
- package/dist/layout/bench.js +225 -0
- package/dist/layout/coordinates.d.ts +62 -0
- package/dist/layout/coordinates.js +868 -0
- package/dist/layout/crossing.d.ts +8 -0
- package/dist/layout/crossing.js +60 -0
- package/dist/layout/graph.d.ts +28 -0
- package/dist/layout/graph.js +126 -0
- package/dist/layout/index.d.ts +7 -0
- package/dist/layout/index.js +5 -0
- package/dist/layout/layers.d.ts +13 -0
- package/dist/layout/layers.js +49 -0
- package/dist/layout/layout-engine.d.ts +21 -0
- package/dist/layout/layout-engine.js +191 -0
- package/dist/layout/overlap.d.ts +10 -0
- package/dist/layout/overlap.js +70 -0
- package/dist/layout/routing.d.ts +26 -0
- package/dist/layout/routing.js +439 -0
- package/dist/layout/subprocess.d.ts +14 -0
- package/dist/layout/subprocess.js +77 -0
- package/dist/layout/types.d.ts +72 -0
- package/dist/layout/types.js +37 -0
- package/dist/types/id-generator.d.ts +8 -0
- package/dist/types/id-generator.js +31 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +2 -0
- package/dist/types/xml-element.d.ts +12 -0
- package/dist/types/xml-element.js +2 -0
- package/dist/xml/index.d.ts +2 -0
- package/dist/xml/index.js +2 -0
- package/dist/xml/xml-parser.d.ts +13 -0
- package/dist/xml/xml-parser.js +271 -0
- 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
|