@bpmnkit/core 0.0.22 → 0.0.24

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.
@@ -1,6 +1,11 @@
1
1
  export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
+ export { buildBlockTree } from "./block-builder.js";
3
+ export type { FlowBlock, GatewayBlock, NodeBlock, SequenceBlock } from "./block-builder.js";
4
+ export { applyBlockLayout } from "./block-layout.js";
5
+ export { routeEdgeAstar } from "./astar.js";
2
6
  export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
3
7
  export type { BenchmarkResult, BoundingBox, ElementComparison, ElementPosition, FlowOrderViolation, } from "./bench.js";
8
+ export { assignGridRows } from "./coordinates.js";
4
9
  export { assertNoOverlap } from "./overlap.js";
5
10
  export type { Bounds, LayoutEdge, LayoutNode, LayoutResult, Waypoint } from "./types.js";
6
11
  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";
@@ -1,5 +1,9 @@
1
1
  export { layoutProcess, layoutFlowNodes } from "./layout-engine.js";
2
+ export { buildBlockTree } from "./block-builder.js";
3
+ export { applyBlockLayout } from "./block-layout.js";
4
+ export { routeEdgeAstar } from "./astar.js";
2
5
  export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLayout, parseReferenceLayout, } from "./bench.js";
6
+ export { assignGridRows } from "./coordinates.js";
3
7
  export { assertNoOverlap } from "./overlap.js";
4
8
  export { ELEMENT_SIZES, GRID_CELL_HEIGHT, GRID_CELL_WIDTH, HORIZONTAL_SPACING, LABEL_CHAR_WIDTH, LABEL_HEIGHT, LABEL_MIN_WIDTH, LABEL_VERTICAL_OFFSET, SUBPROCESS_PADDING, VERTICAL_SPACING, } from "./types.js";
5
9
  //# sourceMappingURL=index.js.map
@@ -10,7 +10,8 @@ import type { LayoutResult } from "./types.js";
10
10
  * 4. Coordinate assignment — Fixed element sizes with spacing
11
11
  * 5. Sub-process layout — Recursive nested passes
12
12
  * 6. Edge routing — Orthogonal waypoints
13
- * 7. Overlap assertionPost-condition validation
13
+ * 7. Boundary event repositioning place events on host border
14
+ * 8. Overlap assertion — Post-condition validation
14
15
  */
15
16
  export declare function layoutProcess(process: BpmnProcess): LayoutResult;
16
17
  /**
@@ -1,10 +1,139 @@
1
- import { alignBaselinePath, alignBranchBaselines, alignSplitJoinPairs, assignCoordinates, distributeSplitBranches, ensureEarlyReturnOffBaseline, resolveLayerOverlaps, } from "./coordinates.js";
1
+ import { buildBlockTree } from "./block-builder.js";
2
+ import { applyBlockLayout } from "./block-layout.js";
3
+ import { alignBaselinePath, alignBranchBaselines, alignSplitJoinPairs, assignCoordinates, assignGridRows, compactBranches, distributeSplitBranches, ensureEarlyReturnOffBaseline, resolveLayerOverlaps, snapToYRows, } from "./coordinates.js";
2
4
  import { minimizeCrossings } from "./crossing.js";
3
5
  import { buildGraph, detectBackEdges, reverseBackEdges } from "./graph.js";
4
6
  import { assignLayers, groupByLayer } from "./layers.js";
5
7
  import { assertNoOverlap } from "./overlap.js";
6
8
  import { routeEdges } from "./routing.js";
7
9
  import { layoutSubProcesses } from "./subprocess.js";
10
+ const CHAIN_GAP = 30;
11
+ const CHAIN_V_GAP = 20;
12
+ /**
13
+ * Reposition boundary events to the bottom edge of their host task, then walk
14
+ * each boundary event's exclusive downstream chain and place those nodes
15
+ * horizontally to the right of the host task. Re-routes all affected edges.
16
+ */
17
+ function repositionBoundaryEvents(flowElements, result) {
18
+ const boundaryMap = new Map();
19
+ for (const el of flowElements) {
20
+ if (el.type !== "boundaryEvent")
21
+ continue;
22
+ const list = boundaryMap.get(el.attachedToRef) ?? [];
23
+ list.push(el.id);
24
+ boundaryMap.set(el.attachedToRef, list);
25
+ }
26
+ if (boundaryMap.size === 0)
27
+ return;
28
+ const nodeById = new Map(result.nodes.map((n) => [n.id, n]));
29
+ const succIds = new Map();
30
+ const predIds = new Map();
31
+ for (const edge of result.edges) {
32
+ const se = succIds.get(edge.sourceRef) ?? [];
33
+ se.push(edge.targetRef);
34
+ succIds.set(edge.sourceRef, se);
35
+ const ps = predIds.get(edge.targetRef) ?? new Set();
36
+ ps.add(edge.sourceRef);
37
+ predIds.set(edge.targetRef, ps);
38
+ }
39
+ for (const [hostId, beIds] of boundaryMap) {
40
+ const hostNode = nodeById.get(hostId);
41
+ if (!hostNode)
42
+ continue;
43
+ for (let i = 0; i < beIds.length; i++) {
44
+ const beId = beIds[i];
45
+ if (!beId)
46
+ continue;
47
+ const beNode = nodeById.get(beId);
48
+ if (!beNode)
49
+ continue;
50
+ const bW = beNode.bounds.width;
51
+ const bH = beNode.bounds.height;
52
+ // Place boundary event on the bottom edge of the host task, stacking leftward
53
+ const rightEdge = hostNode.bounds.x + hostNode.bounds.width;
54
+ beNode.bounds.x = Math.round(rightEdge - bW / 2 - i * (bW + 4));
55
+ beNode.bounds.y = Math.round(hostNode.bounds.y + hostNode.bounds.height - bH / 2);
56
+ if (beNode.labelBounds) {
57
+ beNode.labelBounds.x = beNode.bounds.x + Math.round(bW / 2 - beNode.labelBounds.width / 2);
58
+ beNode.labelBounds.y = beNode.bounds.y + bH + 4;
59
+ }
60
+ // Collect nodes exclusively reachable from this boundary event (BFS)
61
+ const chainSet = new Set([beId]);
62
+ const chainOrder = [];
63
+ const queue = [...(succIds.get(beId) ?? [])];
64
+ while (queue.length > 0) {
65
+ const id = queue.shift();
66
+ if (!id || chainSet.has(id))
67
+ continue;
68
+ const preds = predIds.get(id) ?? new Set();
69
+ if ([...preds].every((p) => chainSet.has(p))) {
70
+ chainSet.add(id);
71
+ chainOrder.push(id);
72
+ queue.push(...(succIds.get(id) ?? []));
73
+ }
74
+ }
75
+ // Each boundary event's chain gets its own vertical lane
76
+ let maxChainH = 0;
77
+ for (const id of chainOrder) {
78
+ const n = nodeById.get(id);
79
+ if (n)
80
+ maxChainH = Math.max(maxChainH, n.bounds.height);
81
+ }
82
+ const laneOffset = i * (maxChainH + CHAIN_V_GAP + 10);
83
+ const chainCenterY = Math.round(beNode.bounds.y + bH + CHAIN_V_GAP + maxChainH / 2 + laneOffset);
84
+ const chainStartX = Math.max(Math.round(beNode.bounds.x + bW / 2) + CHAIN_GAP, hostNode.bounds.x + hostNode.bounds.width + CHAIN_GAP);
85
+ let curX = chainStartX;
86
+ for (const id of chainOrder) {
87
+ const n = nodeById.get(id);
88
+ if (!n)
89
+ continue;
90
+ n.bounds.x = curX;
91
+ n.bounds.y = chainCenterY - Math.round(n.bounds.height / 2);
92
+ if (n.labelBounds) {
93
+ // Center label on node, but clamp so it never extends left of the node —
94
+ // wide event labels would otherwise overlap the preceding chain element.
95
+ const labelX = n.bounds.x + Math.round(n.bounds.width / 2 - n.labelBounds.width / 2);
96
+ n.labelBounds.x = Math.max(labelX, n.bounds.x);
97
+ n.labelBounds.y = n.bounds.y + n.bounds.height + 4;
98
+ }
99
+ // Advance past the node AND its label so the next element doesn't overlap.
100
+ const nodeRight = n.bounds.x + n.bounds.width;
101
+ const labelRight = n.labelBounds ? n.labelBounds.x + n.labelBounds.width : 0;
102
+ curX = Math.max(nodeRight, labelRight) + CHAIN_GAP;
103
+ }
104
+ // Re-route edges touching the boundary event or its chain
105
+ for (const edge of result.edges) {
106
+ if (!chainSet.has(edge.sourceRef))
107
+ continue;
108
+ const src = nodeById.get(edge.sourceRef);
109
+ const tgt = nodeById.get(edge.targetRef);
110
+ if (!src || !tgt)
111
+ continue;
112
+ if (edge.sourceRef === beId) {
113
+ const srcX = Math.round(src.bounds.x + bW / 2);
114
+ const srcY = Math.round(src.bounds.y + bH);
115
+ const tgtX = Math.round(tgt.bounds.x);
116
+ const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
117
+ edge.waypoints = [
118
+ { x: srcX, y: srcY },
119
+ { x: srcX, y: tgtY },
120
+ { x: tgtX, y: tgtY },
121
+ ];
122
+ }
123
+ else {
124
+ const srcX = Math.round(src.bounds.x + src.bounds.width);
125
+ const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
126
+ const tgtX = Math.round(tgt.bounds.x);
127
+ const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
128
+ edge.waypoints = [
129
+ { x: srcX, y: srcY },
130
+ { x: tgtX, y: tgtY },
131
+ ];
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
8
137
  /**
9
138
  * Auto-layout a BPMN process using the Sugiyama/layered algorithm.
10
139
  *
@@ -15,10 +144,12 @@ import { layoutSubProcesses } from "./subprocess.js";
15
144
  * 4. Coordinate assignment — Fixed element sizes with spacing
16
145
  * 5. Sub-process layout — Recursive nested passes
17
146
  * 6. Edge routing — Orthogonal waypoints
18
- * 7. Overlap assertionPost-condition validation
147
+ * 7. Boundary event repositioning place events on host border
148
+ * 8. Overlap assertion — Post-condition validation
19
149
  */
20
150
  export function layoutProcess(process) {
21
151
  const result = layoutFlowNodes(process.flowElements, process.sequenceFlows);
152
+ repositionBoundaryEvents(process.flowElements, result);
22
153
  assertNoOverlap(result);
23
154
  return result;
24
155
  }
@@ -39,43 +170,46 @@ export function layoutFlowNodes(flowNodes, sequenceFlows) {
39
170
  const graph = buildGraph(flowNodes, sequenceFlows);
40
171
  const backEdges = detectBackEdges(graph, sequenceFlows);
41
172
  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);
173
+ // Phase 2: Try block-based layout (primary path for structured processes)
174
+ // Block layout only works well for processes without back-edges (loops).
175
+ let layoutNodes;
176
+ const blockTree = backEdges.length === 0 ? buildBlockTree(dag, nodeIndex) : null;
177
+ const usedBlockLayout = blockTree !== null;
178
+ if (blockTree) {
179
+ layoutNodes = applyBlockLayout(blockTree, nodeIndex);
180
+ }
181
+ else {
182
+ layoutNodes = sugiyamaLayout(dag, nodeIndex, backEdges);
183
+ }
67
184
  // Phase 5: Sub-process layout — expand containers and lay out children
68
185
  const childResults = layoutSubProcesses(layoutNodes, nodeIndex);
69
- // After subprocess expansion, push nodes that now overlap with expanded containers
186
+ // After subprocess expansion, push nodes that now overlap with expanded containers.
187
+ // For block layout, assign unique layer indices first so resolveLayerOverlaps works
188
+ // correctly (block layout nodes all start at layer=0).
189
+ if (usedBlockLayout && childResults.length > 0) {
190
+ // Assign each block-layout node a unique layer so overlap resolution doesn't
191
+ // collapse them all into the same bucket and push them vertically apart.
192
+ for (let idx = 0; idx < layoutNodes.length; idx++) {
193
+ const n = layoutNodes[idx];
194
+ if (n)
195
+ n.layer = idx;
196
+ }
197
+ }
70
198
  resolveSubProcessOverlaps(layoutNodes);
71
199
  // Phase 5b: Resolve Y-direction overlaps caused by subprocess expansion.
72
200
  // Expanded subprocesses grow in-place and can overlap same-layer siblings.
73
- resolveLayerOverlaps(layoutNodes);
201
+ // For block layout without subprocesses, skip this — overlaps are impossible by construction.
202
+ if (!usedBlockLayout || childResults.length > 0) {
203
+ resolveLayerOverlaps(layoutNodes);
204
+ }
74
205
  // Phase 5c: Sync child positions to their subprocess containers.
75
206
  // resolveLayerOverlaps (including its Y-normalization pass) may have shifted
76
207
  // subprocess containers after their children were already translated to
77
208
  // absolute coordinates — children must follow.
78
209
  syncSubProcessChildren(childResults, layoutNodes);
210
+ // Assign grid-row indices based on final center-Y positions (eliminates pixel-tolerance
211
+ // guessing in port-side decisions).
212
+ assignGridRows(layoutNodes);
79
213
  // Phase 6: Edge routing (uses original back-edges for routing, not reversed)
80
214
  const nodeMap = new Map();
81
215
  for (const node of layoutNodes) {
@@ -95,6 +229,52 @@ export function layoutFlowNodes(flowNodes, sequenceFlows) {
95
229
  }
96
230
  return { nodes: allNodes, edges: allEdges };
97
231
  }
232
+ /**
233
+ * Run the Sugiyama layered layout pipeline.
234
+ * Used as fallback for unstructured or loop-containing processes.
235
+ */
236
+ function sugiyamaLayout(dag, nodeIndex, backEdges) {
237
+ // Phase 2: Layer assignment
238
+ const layers = assignLayers(dag);
239
+ // Phase 3: Group by layer and minimize crossings
240
+ const layerGroups = groupByLayer(layers);
241
+ const orderedLayers = minimizeCrossings(layerGroups, dag);
242
+ // Phase 4: Coordinate assignment
243
+ const layoutNodes = assignCoordinates(orderedLayers, nodeIndex);
244
+ // Phase 4b: Align linear sequences to a common y-baseline
245
+ alignBranchBaselines(layoutNodes, dag);
246
+ // Phase 4c: Align split/join gateway pairs to same y-coordinate
247
+ alignSplitJoinPairs(layoutNodes, dag, backEdges);
248
+ // Phase 4d: Align all baseline-path nodes to the same center-Y
249
+ alignBaselinePath(layoutNodes, dag, backEdges);
250
+ // Phase 4e: Ensure early-return branches are never on the baseline
251
+ ensureEarlyReturnOffBaseline(layoutNodes, dag, backEdges);
252
+ // Re-align linear chains that may have been disrupted by position swaps
253
+ alignBranchBaselines(layoutNodes, dag);
254
+ // Phase 4f: Distribute split gateway branches symmetrically
255
+ distributeSplitBranches(layoutNodes, dag, backEdges);
256
+ // Re-align split/join pairs that may have been separated during branch distribution
257
+ alignSplitJoinPairs(layoutNodes, dag, backEdges);
258
+ // Re-align branch spines after distribution moved chains and alignSplitJoinPairs
259
+ // adjusted join gateways (continuation nodes after joins must follow)
260
+ alignBranchBaselines(layoutNodes, dag);
261
+ // Phase 4g: Resolve any layer overlaps from redistribution
262
+ resolveLayerOverlaps(layoutNodes);
263
+ // Phase 4h: Re-align baseline after overlap resolution (overlap resolution may push
264
+ // baseline nodes off-center when they share a layer with branch nodes)
265
+ alignBaselinePath(layoutNodes, dag, backEdges);
266
+ // Phase 4i: Final overlap resolution — baseline re-alignment may pull a node back into
267
+ // an overlap that resolveLayerOverlaps already fixed; one more pass eliminates these.
268
+ resolveLayerOverlaps(layoutNodes);
269
+ // Phase 4j: Branch compaction — pull branch subtrees toward baseline
270
+ compactBranches(layoutNodes, dag, backEdges);
271
+ resolveLayerOverlaps(layoutNodes);
272
+ alignBaselinePath(layoutNodes, dag, backEdges);
273
+ // Phase 4k: Row snapping — merge close Y rows for matrix-like alignment
274
+ snapToYRows(layoutNodes);
275
+ resolveLayerOverlaps(layoutNodes);
276
+ return layoutNodes;
277
+ }
98
278
  /**
99
279
  * After subprocess expansion, cascade-shift all subsequent layers
100
280
  * so that inter-layer spacing is preserved.
@@ -8,6 +8,9 @@
8
8
  export function assertNoOverlap(result) {
9
9
  const allBounds = [];
10
10
  for (const node of result.nodes) {
11
+ // Boundary events intentionally overlap their host activity — skip them.
12
+ if (node.type === "boundaryEvent")
13
+ continue;
11
14
  allBounds.push({ id: node.id, kind: "element", bounds: node.bounds });
12
15
  if (node.labelBounds) {
13
16
  allBounds.push({ id: `${node.id}-label`, kind: "label", bounds: node.labelBounds });
@@ -8,13 +8,15 @@ export type PortSide = "right" | "top" | "bottom";
8
8
  * Non-gateway targets always receive edges from the left side.
9
9
  * Split gateways (starting): incoming always from the left.
10
10
  * Join gateways (closing): incoming based on relative position (top/bottom/left).
11
+ *
12
+ * Uses gridRow (integer row index) when available for exact comparison;
13
+ * falls back to pixel-Y with PORT_SAME_Y_TOLERANCE for nodes without gridRow.
11
14
  */
12
15
  export declare function resolveTargetPort(source: LayoutNode, target: LayoutNode, joinGateways: ReadonlySet<string>): "left" | "top" | "bottom";
13
16
  /**
14
17
  * 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
+ * Uses absolute direction: target above → top, below → bottom, same level → right.
19
+ * Single output always exits from the right port.
18
20
  */
19
21
  export declare function assignGatewayPorts(outgoingFlows: BpmnSequenceFlow[], nodeMap: Map<string, LayoutNode>): Map<string, PortSide>;
20
22
  /**
@@ -23,4 +25,9 @@ export declare function assignGatewayPorts(outgoingFlows: BpmnSequenceFlow[], no
23
25
  * Gateway sources use port-based routing (top/right/bottom).
24
26
  */
25
27
  export declare function routeEdges(sequenceFlows: BpmnSequenceFlow[], nodeMap: Map<string, LayoutNode>, backEdges: BackEdge[]): LayoutEdge[];
28
+ /**
29
+ * Post-process routed edges to avoid crossing through intermediate shapes.
30
+ * For each segment that passes through a shape, adds detour waypoints around it.
31
+ */
32
+ export declare function resolveEdgeCrossings(edges: LayoutEdge[], nodeMap: Map<string, LayoutNode>): void;
26
33
  //# sourceMappingURL=routing.d.ts.map