@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
package/dist/layout/types.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ export declare const ELEMENT_SIZES: Record<string, {
|
|
|
5
5
|
height: number;
|
|
6
6
|
}>;
|
|
7
7
|
/** Virtual grid cell dimensions for element placement. */
|
|
8
|
-
export declare const GRID_CELL_WIDTH =
|
|
8
|
+
export declare const GRID_CELL_WIDTH = 150;
|
|
9
9
|
export declare const GRID_CELL_HEIGHT = 140;
|
|
10
10
|
/** Minimum spacing between elements (derived from grid). */
|
|
11
11
|
export declare const HORIZONTAL_SPACING: number;
|
|
12
12
|
export declare const VERTICAL_SPACING: number;
|
|
13
13
|
/** Padding inside sub-process containers. */
|
|
14
|
-
export declare const SUBPROCESS_PADDING =
|
|
14
|
+
export declare const SUBPROCESS_PADDING = 50;
|
|
15
15
|
/** Edge-label sizing constants (used for placement & collision detection). */
|
|
16
16
|
export declare const LABEL_CHAR_WIDTH = 7;
|
|
17
17
|
export declare const LABEL_MIN_WIDTH = 40;
|
package/dist/layout/types.js
CHANGED
|
@@ -12,23 +12,27 @@ export const ELEMENT_SIZES = {
|
|
|
12
12
|
receiveTask: { width: 100, height: 80 },
|
|
13
13
|
businessRuleTask: { width: 100, height: 80 },
|
|
14
14
|
callActivity: { width: 100, height: 80 },
|
|
15
|
+
task: { width: 100, height: 80 },
|
|
16
|
+
manualTask: { width: 100, height: 80 },
|
|
15
17
|
exclusiveGateway: { width: 50, height: 50 },
|
|
16
18
|
parallelGateway: { width: 50, height: 50 },
|
|
17
19
|
inclusiveGateway: { width: 50, height: 50 },
|
|
18
20
|
eventBasedGateway: { width: 50, height: 50 },
|
|
21
|
+
complexGateway: { width: 50, height: 50 },
|
|
19
22
|
// Sub-processes are sized dynamically
|
|
20
23
|
subProcess: { width: 100, height: 80 },
|
|
21
24
|
adHocSubProcess: { width: 100, height: 80 },
|
|
22
25
|
eventSubProcess: { width: 100, height: 80 },
|
|
26
|
+
transaction: { width: 100, height: 80 },
|
|
23
27
|
};
|
|
24
28
|
/** Virtual grid cell dimensions for element placement. */
|
|
25
|
-
export const GRID_CELL_WIDTH =
|
|
29
|
+
export const GRID_CELL_WIDTH = 150;
|
|
26
30
|
export const GRID_CELL_HEIGHT = 140;
|
|
27
31
|
/** Minimum spacing between elements (derived from grid). */
|
|
28
32
|
export const HORIZONTAL_SPACING = GRID_CELL_WIDTH - 100; // 100 = max element width
|
|
29
33
|
export const VERTICAL_SPACING = GRID_CELL_HEIGHT - 80; // 80 = max element height
|
|
30
34
|
/** Padding inside sub-process containers. */
|
|
31
|
-
export const SUBPROCESS_PADDING =
|
|
35
|
+
export const SUBPROCESS_PADDING = 50;
|
|
32
36
|
/** Edge-label sizing constants (used for placement & collision detection). */
|
|
33
37
|
export const LABEL_CHAR_WIDTH = 7;
|
|
34
38
|
export const LABEL_MIN_WIDTH = 40;
|
package/dist/xml/xml-parser.js
CHANGED
|
@@ -238,6 +238,11 @@ function writeElement(parts, el, depth) {
|
|
|
238
238
|
const indent = " ".repeat(depth);
|
|
239
239
|
parts.push(indent, "<", el.name);
|
|
240
240
|
for (const [key, value] of Object.entries(el.attributes)) {
|
|
241
|
+
// el.attributes is typed Record<string, string>, but content built from
|
|
242
|
+
// generated/untyped code can leave a value undefined at runtime — skip
|
|
243
|
+
// rather than crash the whole serialization on one bad attribute.
|
|
244
|
+
if (typeof value !== "string")
|
|
245
|
+
continue;
|
|
241
246
|
parts.push(" ", key, '="', escapeAttr(value), '"');
|
|
242
247
|
}
|
|
243
248
|
const hasChildren = el.children.length > 0;
|
package/package.json
CHANGED
package/dist/layout/astar.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Bounds, Waypoint } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Route a single edge using A* on a 10px grid.
|
|
4
|
-
* source/target are center points of source/target nodes.
|
|
5
|
-
* obstacles are node bounding boxes to avoid (inflated by 6px margin).
|
|
6
|
-
* Returns simplified orthogonal waypoints.
|
|
7
|
-
*/
|
|
8
|
-
export declare function routeEdgeAstar(source: {
|
|
9
|
-
x: number;
|
|
10
|
-
y: number;
|
|
11
|
-
}, target: {
|
|
12
|
-
x: number;
|
|
13
|
-
y: number;
|
|
14
|
-
}, obstacles: Bounds[], canvasWidth: number, canvasHeight: number): Waypoint[];
|
|
15
|
-
//# sourceMappingURL=astar.d.ts.map
|
package/dist/layout/astar.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
const GRID_RES = 10;
|
|
2
|
-
const OBSTACLE_MARGIN = 6;
|
|
3
|
-
const TURN_PENALTY = 5;
|
|
4
|
-
const CANVAS_EXTEND = 80;
|
|
5
|
-
const DX = [1, 0, -1, 0];
|
|
6
|
-
const DY = [0, 1, 0, -1];
|
|
7
|
-
/** Minimal binary min-heap keyed by f-score. */
|
|
8
|
-
class MinHeap {
|
|
9
|
-
data = [];
|
|
10
|
-
push(f, key) {
|
|
11
|
-
this.data.push({ f, key });
|
|
12
|
-
this.bubbleUp(this.data.length - 1);
|
|
13
|
-
}
|
|
14
|
-
pop() {
|
|
15
|
-
const top = this.data[0];
|
|
16
|
-
const last = this.data.pop();
|
|
17
|
-
if (this.data.length > 0 && last !== undefined) {
|
|
18
|
-
this.data[0] = last;
|
|
19
|
-
this.sinkDown(0);
|
|
20
|
-
}
|
|
21
|
-
return top;
|
|
22
|
-
}
|
|
23
|
-
get size() {
|
|
24
|
-
return this.data.length;
|
|
25
|
-
}
|
|
26
|
-
bubbleUp(startIdx) {
|
|
27
|
-
let i = startIdx;
|
|
28
|
-
while (i > 0) {
|
|
29
|
-
const parent = (i - 1) >> 1;
|
|
30
|
-
const d = this.data[i];
|
|
31
|
-
const p = this.data[parent];
|
|
32
|
-
if (!d || !p || p.f <= d.f)
|
|
33
|
-
break;
|
|
34
|
-
this.data[i] = p;
|
|
35
|
-
this.data[parent] = d;
|
|
36
|
-
i = parent;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
sinkDown(startIdx) {
|
|
40
|
-
let i = startIdx;
|
|
41
|
-
for (;;) {
|
|
42
|
-
const left = 2 * i + 1;
|
|
43
|
-
const right = 2 * i + 2;
|
|
44
|
-
let smallest = i;
|
|
45
|
-
const d = this.data[smallest];
|
|
46
|
-
const l = this.data[left];
|
|
47
|
-
const r = this.data[right];
|
|
48
|
-
if (l && l.f < (d?.f ?? Number.POSITIVE_INFINITY))
|
|
49
|
-
smallest = left;
|
|
50
|
-
const s = this.data[smallest];
|
|
51
|
-
if (r && r.f < (s?.f ?? Number.POSITIVE_INFINITY))
|
|
52
|
-
smallest = right;
|
|
53
|
-
if (smallest === i)
|
|
54
|
-
break;
|
|
55
|
-
const tmp = this.data[i];
|
|
56
|
-
const sm = this.data[smallest];
|
|
57
|
-
if (!tmp || !sm)
|
|
58
|
-
break;
|
|
59
|
-
this.data[i] = sm;
|
|
60
|
-
this.data[smallest] = tmp;
|
|
61
|
-
i = smallest;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Route a single edge using A* on a 10px grid.
|
|
67
|
-
* source/target are center points of source/target nodes.
|
|
68
|
-
* obstacles are node bounding boxes to avoid (inflated by 6px margin).
|
|
69
|
-
* Returns simplified orthogonal waypoints.
|
|
70
|
-
*/
|
|
71
|
-
export function routeEdgeAstar(source, target, obstacles, canvasWidth, canvasHeight) {
|
|
72
|
-
// Extend canvas to allow routing around edges
|
|
73
|
-
const minX = Math.max(0, Math.min(source.x, target.x) - CANVAS_EXTEND);
|
|
74
|
-
const minY = Math.max(0, Math.min(source.y, target.y) - CANVAS_EXTEND);
|
|
75
|
-
const maxX = Math.max(source.x, target.x) + CANVAS_EXTEND + canvasWidth;
|
|
76
|
-
const maxY = Math.max(source.y, target.y) + CANVAS_EXTEND + canvasHeight;
|
|
77
|
-
const cols = Math.ceil((maxX - minX) / GRID_RES) + 1;
|
|
78
|
-
const rows = Math.ceil((maxY - minY) / GRID_RES) + 1;
|
|
79
|
-
// Snap source and target to grid
|
|
80
|
-
const sx = Math.round((source.x - minX) / GRID_RES);
|
|
81
|
-
const sy = Math.round((source.y - minY) / GRID_RES);
|
|
82
|
-
const tx = Math.round((target.x - minX) / GRID_RES);
|
|
83
|
-
const ty = Math.round((target.y - minY) / GRID_RES);
|
|
84
|
-
// If they're at the same grid cell, return straight line
|
|
85
|
-
if (sx === tx && sy === ty) {
|
|
86
|
-
return [source, target];
|
|
87
|
-
}
|
|
88
|
-
// Build blocked grid
|
|
89
|
-
const blocked = new Uint8Array(cols * rows);
|
|
90
|
-
for (const ob of obstacles) {
|
|
91
|
-
const ox1 = Math.floor((ob.x - OBSTACLE_MARGIN - minX) / GRID_RES);
|
|
92
|
-
const oy1 = Math.floor((ob.y - OBSTACLE_MARGIN - minY) / GRID_RES);
|
|
93
|
-
const ox2 = Math.ceil((ob.x + ob.width + OBSTACLE_MARGIN - minX) / GRID_RES);
|
|
94
|
-
const oy2 = Math.ceil((ob.y + ob.height + OBSTACLE_MARGIN - minY) / GRID_RES);
|
|
95
|
-
for (let gy = Math.max(0, oy1); gy <= Math.min(rows - 1, oy2); gy++) {
|
|
96
|
-
for (let gx = Math.max(0, ox1); gx <= Math.min(cols - 1, ox2); gx++) {
|
|
97
|
-
blocked[gy * cols + gx] = 1;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Unblock source and target cells (they're inside nodes)
|
|
102
|
-
blocked[sy * cols + sx] = 0;
|
|
103
|
-
blocked[ty * cols + tx] = 0;
|
|
104
|
-
// A* with direction-aware state: state = cell * 4 + dir
|
|
105
|
-
const INF = Number.MAX_SAFE_INTEGER;
|
|
106
|
-
// g-scores per (cell, dir)
|
|
107
|
-
const g = new Float32Array(cols * rows * 4).fill(INF);
|
|
108
|
-
const parent = new Int32Array(cols * rows * 4).fill(-1);
|
|
109
|
-
const parentDir = new Int8Array(cols * rows * 4).fill(-1);
|
|
110
|
-
const heap = new MinHeap();
|
|
111
|
-
// Initialize: try all 4 directions from source
|
|
112
|
-
for (let d = 0; d < 4; d++) {
|
|
113
|
-
const key = (sy * cols + sx) * 4 + d;
|
|
114
|
-
g[key] = 0;
|
|
115
|
-
const h = Math.abs(tx - sx) + Math.abs(ty - sy);
|
|
116
|
-
heap.push(h, key);
|
|
117
|
-
}
|
|
118
|
-
let found = false;
|
|
119
|
-
while (heap.size > 0) {
|
|
120
|
-
const item = heap.pop();
|
|
121
|
-
if (!item)
|
|
122
|
-
break;
|
|
123
|
-
const { key } = item;
|
|
124
|
-
const dir = key % 4;
|
|
125
|
-
const cell = (key - dir) / 4;
|
|
126
|
-
const cx = cell % cols;
|
|
127
|
-
const cy = (cell - cx) / cols;
|
|
128
|
-
if (cx === tx && cy === ty) {
|
|
129
|
-
found = true;
|
|
130
|
-
// Reconstruct path
|
|
131
|
-
const path = [];
|
|
132
|
-
let k = key;
|
|
133
|
-
while (k !== -1) {
|
|
134
|
-
const kDir = k % 4;
|
|
135
|
-
const kCell = (k - kDir) / 4;
|
|
136
|
-
const kx = kCell % cols;
|
|
137
|
-
const ky = (kCell - kx) / cols;
|
|
138
|
-
path.push({ x: kx * GRID_RES + minX, y: ky * GRID_RES + minY });
|
|
139
|
-
k = parent[k] ?? -1;
|
|
140
|
-
}
|
|
141
|
-
path.reverse();
|
|
142
|
-
// Simplify collinear points
|
|
143
|
-
return simplifyPath(path);
|
|
144
|
-
}
|
|
145
|
-
const gCur = g[key] ?? INF;
|
|
146
|
-
for (let nd = 0; nd < 4; nd++) {
|
|
147
|
-
const nx = cx + (DX[nd] ?? 0);
|
|
148
|
-
const ny = cy + (DY[nd] ?? 0);
|
|
149
|
-
if (nx < 0 || nx >= cols || ny < 0 || ny >= rows)
|
|
150
|
-
continue;
|
|
151
|
-
const ncell = ny * cols + nx;
|
|
152
|
-
if (blocked[ncell])
|
|
153
|
-
continue;
|
|
154
|
-
const nkey = ncell * 4 + nd;
|
|
155
|
-
const turnCost = nd !== dir ? TURN_PENALTY : 0;
|
|
156
|
-
const ng = gCur + 1 + turnCost;
|
|
157
|
-
const prevG = g[nkey] ?? INF;
|
|
158
|
-
if (ng < prevG) {
|
|
159
|
-
g[nkey] = ng;
|
|
160
|
-
parent[nkey] = key;
|
|
161
|
-
parentDir[nkey] = dir;
|
|
162
|
-
const h = Math.abs(tx - nx) + Math.abs(ty - ny);
|
|
163
|
-
heap.push(ng + h, nkey);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (!found) {
|
|
168
|
-
// Fall back to straight line
|
|
169
|
-
return [source, target];
|
|
170
|
-
}
|
|
171
|
-
return [source, target];
|
|
172
|
-
}
|
|
173
|
-
/** Remove collinear intermediate waypoints. */
|
|
174
|
-
function simplifyPath(path) {
|
|
175
|
-
if (path.length <= 2)
|
|
176
|
-
return path;
|
|
177
|
-
const result = [path[0]];
|
|
178
|
-
for (let i = 1; i < path.length - 1; i++) {
|
|
179
|
-
const prev = path[i - 1];
|
|
180
|
-
const curr = path[i];
|
|
181
|
-
const next = path[i + 1];
|
|
182
|
-
const isCollinear = (Math.abs(prev.x - curr.x) < 0.5 && Math.abs(curr.x - next.x) < 0.5) ||
|
|
183
|
-
(Math.abs(prev.y - curr.y) < 0.5 && Math.abs(curr.y - next.y) < 0.5);
|
|
184
|
-
if (!isCollinear) {
|
|
185
|
-
result.push(curr);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
result.push(path[path.length - 1]);
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
//# sourceMappingURL=astar.js.map
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { BpmnElementType, BpmnFlowElement } from "../bpmn/bpmn-model.js";
|
|
2
|
-
import type { DirectedGraph } from "./graph.js";
|
|
3
|
-
export type FlowBlock = NodeBlock | GatewayBlock | SequenceBlock;
|
|
4
|
-
export interface NodeBlock {
|
|
5
|
-
kind: "node";
|
|
6
|
-
id: string;
|
|
7
|
-
type: BpmnElementType;
|
|
8
|
-
label?: string;
|
|
9
|
-
width: number;
|
|
10
|
-
height: number;
|
|
11
|
-
x: number;
|
|
12
|
-
y: number;
|
|
13
|
-
}
|
|
14
|
-
export interface SequenceBlock {
|
|
15
|
-
kind: "sequence";
|
|
16
|
-
items: FlowBlock[];
|
|
17
|
-
width: number;
|
|
18
|
-
height: number;
|
|
19
|
-
x: number;
|
|
20
|
-
y: number;
|
|
21
|
-
}
|
|
22
|
-
export interface GatewayBlock {
|
|
23
|
-
kind: "gateway";
|
|
24
|
-
split: NodeBlock;
|
|
25
|
-
join: NodeBlock;
|
|
26
|
-
branches: SequenceBlock[];
|
|
27
|
-
branchColumnWidth: number;
|
|
28
|
-
width: number;
|
|
29
|
-
height: number;
|
|
30
|
-
x: number;
|
|
31
|
-
y: number;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Attempt to build block tree. Returns null if process is unstructured.
|
|
35
|
-
*/
|
|
36
|
-
export declare function buildBlockTree(dag: DirectedGraph, nodeIndex: Map<string, BpmnFlowElement>): SequenceBlock | null;
|
|
37
|
-
//# sourceMappingURL=block-builder.d.ts.map
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { topologicalSort } from "./graph.js";
|
|
2
|
-
import { ELEMENT_SIZES } from "./types.js";
|
|
3
|
-
const GATEWAY_TYPES = new Set([
|
|
4
|
-
"exclusiveGateway",
|
|
5
|
-
"parallelGateway",
|
|
6
|
-
"inclusiveGateway",
|
|
7
|
-
"eventBasedGateway",
|
|
8
|
-
"complexGateway",
|
|
9
|
-
]);
|
|
10
|
-
/** Make a NodeBlock for the given element. */
|
|
11
|
-
function makeNodeBlock(id, nodeIndex) {
|
|
12
|
-
const el = nodeIndex.get(id);
|
|
13
|
-
const type = el?.type ?? "serviceTask";
|
|
14
|
-
const size = ELEMENT_SIZES[type] ?? { width: 100, height: 80 };
|
|
15
|
-
return {
|
|
16
|
-
kind: "node",
|
|
17
|
-
id,
|
|
18
|
-
type,
|
|
19
|
-
label: el?.name,
|
|
20
|
-
width: size.width,
|
|
21
|
-
height: size.height,
|
|
22
|
-
x: 0,
|
|
23
|
-
y: 0,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Find the matching join gateway for a split gateway.
|
|
28
|
-
* Uses a depth counter across the topoOrder of provided node IDs:
|
|
29
|
-
* +1 for each split (outDegree≥2), -1 for each join (inDegree≥2).
|
|
30
|
-
* First time depth returns to 0 → that's the matching join.
|
|
31
|
-
*/
|
|
32
|
-
function findMatchingJoin(splitId, orderedIds, splitPos, dag, nodeIndex) {
|
|
33
|
-
let depth = 1;
|
|
34
|
-
for (let j = splitPos + 1; j < orderedIds.length; j++) {
|
|
35
|
-
const id = orderedIds[j];
|
|
36
|
-
if (!id)
|
|
37
|
-
continue;
|
|
38
|
-
const el = nodeIndex.get(id);
|
|
39
|
-
if (!el || !GATEWAY_TYPES.has(el.type))
|
|
40
|
-
continue;
|
|
41
|
-
const outDegree = (dag.successors.get(id) ?? []).length;
|
|
42
|
-
const inDegree = (dag.predecessors.get(id) ?? []).length;
|
|
43
|
-
if (outDegree >= 2)
|
|
44
|
-
depth++;
|
|
45
|
-
if (inDegree >= 2) {
|
|
46
|
-
depth--;
|
|
47
|
-
if (depth === 0)
|
|
48
|
-
return id;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Collect all node IDs reachable from startId before reaching stopId (exclusive).
|
|
55
|
-
* Uses BFS over DAG successors. The stop node is not included.
|
|
56
|
-
*/
|
|
57
|
-
function collectBranchNodes(startId, stopId, dag) {
|
|
58
|
-
const result = [];
|
|
59
|
-
const seen = new Set();
|
|
60
|
-
const queue = [startId];
|
|
61
|
-
while (queue.length > 0) {
|
|
62
|
-
const id = queue.shift();
|
|
63
|
-
if (!id || seen.has(id) || id === stopId)
|
|
64
|
-
continue;
|
|
65
|
-
seen.add(id);
|
|
66
|
-
result.push(id);
|
|
67
|
-
for (const succ of dag.successors.get(id) ?? []) {
|
|
68
|
-
if (!seen.has(succ) && succ !== stopId) {
|
|
69
|
-
queue.push(succ);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Build a SequenceBlock for the given ordered list of node IDs.
|
|
77
|
-
* Recursively handles nested gateways.
|
|
78
|
-
* Throws if the structure is unstructured (no matching join found).
|
|
79
|
-
*/
|
|
80
|
-
function buildSequenceFromIds(orderedIds, dag, nodeIndex, topoPos) {
|
|
81
|
-
const items = [];
|
|
82
|
-
let i = 0;
|
|
83
|
-
while (i < orderedIds.length) {
|
|
84
|
-
const id = orderedIds[i];
|
|
85
|
-
if (!id) {
|
|
86
|
-
i++;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const el = nodeIndex.get(id);
|
|
90
|
-
const outDegree = (dag.successors.get(id) ?? []).length;
|
|
91
|
-
const isSplit = outDegree >= 2 && el !== undefined && GATEWAY_TYPES.has(el.type);
|
|
92
|
-
if (isSplit) {
|
|
93
|
-
// This is a split gateway — find its matching join within orderedIds
|
|
94
|
-
const joinId = findMatchingJoin(id, orderedIds, i, dag, nodeIndex);
|
|
95
|
-
if (!joinId)
|
|
96
|
-
throw new Error(`No matching join for split gateway ${id}`);
|
|
97
|
-
const joinPos = orderedIds.indexOf(joinId);
|
|
98
|
-
if (joinPos < 0)
|
|
99
|
-
throw new Error(`Join ${joinId} not in current sequence`);
|
|
100
|
-
// For each successor of split, collect branch nodes (up to join)
|
|
101
|
-
const successors = dag.successors.get(id) ?? [];
|
|
102
|
-
const branches = [];
|
|
103
|
-
for (const succId of successors) {
|
|
104
|
-
// Collect nodes in this branch (BFS stopping at join)
|
|
105
|
-
const branchNodes = collectBranchNodes(succId, joinId, dag);
|
|
106
|
-
// Filter to only nodes that appear in orderedIds (safety)
|
|
107
|
-
const branchSet = new Set(branchNodes);
|
|
108
|
-
// Sort by topo position
|
|
109
|
-
const branchOrdered = orderedIds.filter((nid) => branchSet.has(nid));
|
|
110
|
-
branches.push(buildSequenceFromIds(branchOrdered, dag, nodeIndex, topoPos));
|
|
111
|
-
}
|
|
112
|
-
const splitBlock = makeNodeBlock(id, nodeIndex);
|
|
113
|
-
const joinBlock = makeNodeBlock(joinId, nodeIndex);
|
|
114
|
-
const gatewayBlock = {
|
|
115
|
-
kind: "gateway",
|
|
116
|
-
split: splitBlock,
|
|
117
|
-
join: joinBlock,
|
|
118
|
-
branches,
|
|
119
|
-
branchColumnWidth: 0,
|
|
120
|
-
width: 0,
|
|
121
|
-
height: 0,
|
|
122
|
-
x: 0,
|
|
123
|
-
y: 0,
|
|
124
|
-
};
|
|
125
|
-
items.push(gatewayBlock);
|
|
126
|
-
// Skip past the join gateway
|
|
127
|
-
i = joinPos + 1;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
items.push(makeNodeBlock(id, nodeIndex));
|
|
131
|
-
i++;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { kind: "sequence", items, width: 0, height: 0, x: 0, y: 0 };
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Attempt to build block tree. Returns null if process is unstructured.
|
|
138
|
-
*/
|
|
139
|
-
export function buildBlockTree(dag, nodeIndex) {
|
|
140
|
-
try {
|
|
141
|
-
const topoOrder = topologicalSort(dag);
|
|
142
|
-
const topoPos = new Map();
|
|
143
|
-
for (let i = 0; i < topoOrder.length; i++) {
|
|
144
|
-
const id = topoOrder[i];
|
|
145
|
-
if (id)
|
|
146
|
-
topoPos.set(id, i);
|
|
147
|
-
}
|
|
148
|
-
return buildSequenceFromIds(topoOrder, dag, nodeIndex, topoPos);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
//# sourceMappingURL=block-builder.js.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { BpmnFlowElement } from "../bpmn/bpmn-model.js";
|
|
2
|
-
import type { SequenceBlock } from "./block-builder.js";
|
|
3
|
-
import type { LayoutNode } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
* Apply block-based layout: size (bottom-up) then position (top-down).
|
|
6
|
-
* Returns LayoutNode[] with absolute positions.
|
|
7
|
-
*/
|
|
8
|
-
export declare function applyBlockLayout(root: SequenceBlock, nodeIndex: Map<string, BpmnFlowElement>): LayoutNode[];
|
|
9
|
-
//# sourceMappingURL=block-layout.d.ts.map
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { ELEMENT_SIZES } from "./types.js";
|
|
2
|
-
import { GRID_CELL_WIDTH } from "./types.js";
|
|
3
|
-
const H_GAP = 50;
|
|
4
|
-
const V_GAP = 80;
|
|
5
|
-
const OUTER_PADDING = 40;
|
|
6
|
-
/** Compute label bounds for a laid-out block node (same logic as coordinates.ts). */
|
|
7
|
-
function computeLabelBoundsForBlock(block, nodeIndex) {
|
|
8
|
-
const el = nodeIndex.get(block.id);
|
|
9
|
-
if (!el?.name)
|
|
10
|
-
return undefined;
|
|
11
|
-
const labelWidth = Math.min(Math.max(el.name.length * 7, 40), GRID_CELL_WIDTH);
|
|
12
|
-
const labelHeight = 14;
|
|
13
|
-
switch (block.type) {
|
|
14
|
-
case "startEvent":
|
|
15
|
-
case "endEvent":
|
|
16
|
-
case "intermediateThrowEvent":
|
|
17
|
-
case "intermediateCatchEvent":
|
|
18
|
-
case "exclusiveGateway":
|
|
19
|
-
case "parallelGateway":
|
|
20
|
-
case "inclusiveGateway":
|
|
21
|
-
case "eventBasedGateway":
|
|
22
|
-
return {
|
|
23
|
-
x: block.x + block.width / 2 - labelWidth / 2,
|
|
24
|
-
y: block.y + block.height + 4,
|
|
25
|
-
width: labelWidth,
|
|
26
|
-
height: labelHeight,
|
|
27
|
-
};
|
|
28
|
-
default:
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function countBlockNodes(block) {
|
|
33
|
-
if (block.kind === "node")
|
|
34
|
-
return 1;
|
|
35
|
-
if (block.kind === "sequence")
|
|
36
|
-
return block.items.reduce((s, b) => s + countBlockNodes(b), 0);
|
|
37
|
-
return 2 + block.branches.reduce((s, b) => s + countBlockNodes(b), 0);
|
|
38
|
-
}
|
|
39
|
-
/** Size a block bottom-up: sets width/height on all blocks in the tree. */
|
|
40
|
-
function sizeBlock(block) {
|
|
41
|
-
if (block.kind === "node") {
|
|
42
|
-
const size = ELEMENT_SIZES[block.type] ?? { width: 100, height: 80 };
|
|
43
|
-
block.width = size.width;
|
|
44
|
-
block.height = size.height;
|
|
45
|
-
}
|
|
46
|
-
else if (block.kind === "sequence") {
|
|
47
|
-
for (const item of block.items) {
|
|
48
|
-
sizeBlock(item);
|
|
49
|
-
}
|
|
50
|
-
const n = block.items.length;
|
|
51
|
-
block.width =
|
|
52
|
-
block.items.reduce((sum, item) => sum + item.width, 0) + Math.max(0, n - 1) * H_GAP;
|
|
53
|
-
block.height = block.items.reduce((max, item) => Math.max(max, item.height), 0);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
// GatewayBlock
|
|
57
|
-
sizeBlock(block.split);
|
|
58
|
-
sizeBlock(block.join);
|
|
59
|
-
for (const branch of block.branches) {
|
|
60
|
-
sizeBlock(branch);
|
|
61
|
-
}
|
|
62
|
-
block.branchColumnWidth =
|
|
63
|
-
block.branches.length > 0 ? block.branches.reduce((max, b) => Math.max(max, b.width), 0) : 0;
|
|
64
|
-
block.width = block.split.width + H_GAP + block.branchColumnWidth + H_GAP + block.join.width;
|
|
65
|
-
// Each branch gets at least V_GAP height (for empty bypass edges).
|
|
66
|
-
// Use effectiveH consistently in both sizing and positioning.
|
|
67
|
-
const totalBranchH = block.branches.reduce((sum, b) => sum + Math.max(b.height, V_GAP), 0) +
|
|
68
|
-
Math.max(0, block.branches.length - 1) * V_GAP;
|
|
69
|
-
block.height = Math.max(totalBranchH, block.split.height, block.join.height);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/** Position a block top-down given its absolute (x,y) top-left origin. */
|
|
73
|
-
function positionBlock(block, x, y) {
|
|
74
|
-
block.x = x;
|
|
75
|
-
block.y = y;
|
|
76
|
-
if (block.kind === "node") {
|
|
77
|
-
// Nothing more to do — leaf
|
|
78
|
-
}
|
|
79
|
-
else if (block.kind === "sequence") {
|
|
80
|
-
let curX = x;
|
|
81
|
-
for (const item of block.items) {
|
|
82
|
-
// Center each item vertically within the sequence
|
|
83
|
-
const itemY = y + (block.height - item.height) / 2;
|
|
84
|
-
positionBlock(item, curX, itemY);
|
|
85
|
-
curX += item.width + H_GAP;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
// GatewayBlock: split at left, join at right, branches stacked in middle
|
|
90
|
-
const splitY = y + block.height / 2 - block.split.height / 2;
|
|
91
|
-
positionBlock(block.split, x, splitY);
|
|
92
|
-
const joinX = x + block.width - block.join.width;
|
|
93
|
-
const joinY = y + block.height / 2 - block.join.height / 2;
|
|
94
|
-
positionBlock(block.join, joinX, joinY);
|
|
95
|
-
// Reorder branches: heaviest (most nodes) at center, alternating above/below
|
|
96
|
-
const sortedBySize = [...block.branches].sort((a, b) => countBlockNodes(b) - countBlockNodes(a));
|
|
97
|
-
const bc = block.branches.length;
|
|
98
|
-
const orderedBranches = new Array(bc);
|
|
99
|
-
const bm = Math.floor((bc - 1) / 2);
|
|
100
|
-
// biome-ignore lint/style/noNonNullAssertion: sortedBySize is non-empty (bc >= 1)
|
|
101
|
-
orderedBranches[bm] = sortedBySize[0];
|
|
102
|
-
let ba = bm - 1;
|
|
103
|
-
let bb = bm + 1;
|
|
104
|
-
for (let si = 1; si < bc;) {
|
|
105
|
-
// biome-ignore lint/style/noNonNullAssertion: si < bc ensures element exists
|
|
106
|
-
if (bb < bc && si < bc)
|
|
107
|
-
orderedBranches[bb++] = sortedBySize[si++];
|
|
108
|
-
// biome-ignore lint/style/noNonNullAssertion: si < bc ensures element exists
|
|
109
|
-
if (ba >= 0 && si < bc)
|
|
110
|
-
orderedBranches[ba--] = sortedBySize[si++];
|
|
111
|
-
}
|
|
112
|
-
block.branches = orderedBranches;
|
|
113
|
-
// Branches stacked top-to-bottom, using effectiveH matching sizeBlock
|
|
114
|
-
const totalBranchH = block.branches.reduce((sum, b) => sum + Math.max(b.height, V_GAP), 0) +
|
|
115
|
-
Math.max(0, block.branches.length - 1) * V_GAP;
|
|
116
|
-
const branchX = x + block.split.width + H_GAP;
|
|
117
|
-
let branchY = y + (block.height - totalBranchH) / 2;
|
|
118
|
-
for (const branch of block.branches) {
|
|
119
|
-
const effectiveH = Math.max(branch.height, V_GAP);
|
|
120
|
-
positionBlock(branch, branchX, branchY + (effectiveH - branch.height) / 2);
|
|
121
|
-
branchY += effectiveH + V_GAP;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/** Flatten block tree into LayoutNode list. */
|
|
126
|
-
function flattenBlock(block, nodeIndex, out) {
|
|
127
|
-
if (block.kind === "node") {
|
|
128
|
-
const layoutNode = {
|
|
129
|
-
id: block.id,
|
|
130
|
-
type: block.type,
|
|
131
|
-
bounds: { x: block.x, y: block.y, width: block.width, height: block.height },
|
|
132
|
-
layer: 0,
|
|
133
|
-
position: 0,
|
|
134
|
-
label: block.label,
|
|
135
|
-
};
|
|
136
|
-
layoutNode.labelBounds = computeLabelBoundsForBlock(block, nodeIndex);
|
|
137
|
-
out.push(layoutNode);
|
|
138
|
-
}
|
|
139
|
-
else if (block.kind === "sequence") {
|
|
140
|
-
for (const item of block.items) {
|
|
141
|
-
flattenBlock(item, nodeIndex, out);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
flattenBlock(block.split, nodeIndex, out);
|
|
146
|
-
flattenBlock(block.join, nodeIndex, out);
|
|
147
|
-
for (const branch of block.branches) {
|
|
148
|
-
flattenBlock(branch, nodeIndex, out);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Apply block-based layout: size (bottom-up) then position (top-down).
|
|
154
|
-
* Returns LayoutNode[] with absolute positions.
|
|
155
|
-
*/
|
|
156
|
-
export function applyBlockLayout(root, nodeIndex) {
|
|
157
|
-
sizeBlock(root);
|
|
158
|
-
positionBlock(root, OUTER_PADDING, OUTER_PADDING);
|
|
159
|
-
const nodes = [];
|
|
160
|
-
flattenBlock(root, nodeIndex, nodes);
|
|
161
|
-
return nodes;
|
|
162
|
-
}
|
|
163
|
-
//# sourceMappingURL=block-layout.js.map
|