@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.
@@ -0,0 +1,163 @@
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
@@ -22,6 +22,7 @@ export declare function reassignXCoordinates(layoutNodes: LayoutNode[], orderedL
22
22
  * Align nodes in linear sequences to a common y-baseline.
23
23
  * A "linear" node has ≤1 predecessor and ≤1 successor, and is not a gateway.
24
24
  * Walks forward from each chain root, setting successors to the same center-y.
25
+ * Crosses split/join gateway pairs to align the full branch spine.
25
26
  */
26
27
  export declare function alignBranchBaselines(layoutNodes: LayoutNode[], dag: DirectedGraph): void;
27
28
  /**
@@ -53,6 +54,28 @@ export declare function alignBaselinePath(layoutNodes: LayoutNode[], dag: Direct
53
54
  * Pass 2: single-branch gateways — placed one full grid row away, with peer-aware gap enforcement.
54
55
  */
55
56
  export declare function distributeSplitBranches(layoutNodes: LayoutNode[], dag: DirectedGraph, backEdges?: BackEdge[]): void;
57
+ /**
58
+ * Assign integer grid-row indices to all nodes based on their final center-Y positions.
59
+ * Nodes whose center-Y values are within EPSILON pixels are placed in the same row.
60
+ * This eliminates pixel-tolerance guessing in port-side decisions: two nodes with the
61
+ * same gridRow are on the same horizontal row and should connect left-to-right.
62
+ *
63
+ * Called once, after ALL coordinate adjustments, just before edge routing.
64
+ */
65
+ export declare function assignGridRows(layoutNodes: LayoutNode[]): void;
66
+ /**
67
+ * Snap nodes to common Y rows for matrix-like alignment.
68
+ * Groups nodes that share a CY (from alignment passes), then merges
69
+ * close groups into a single row — moving entire groups as units.
70
+ * Boundary events are excluded (they are repositioned later).
71
+ */
72
+ export declare function snapToYRows(layoutNodes: LayoutNode[]): void;
73
+ /**
74
+ * Pull each branch subtree toward the baseline, closing unnecessary vertical gaps.
75
+ * Uses per-element 2D collision detection — only actual element-to-element overlaps
76
+ * constrain the movement, not the branch's full bounding box.
77
+ */
78
+ export declare function compactBranches(layoutNodes: LayoutNode[], dag: DirectedGraph, backEdges?: BackEdge[]): void;
56
79
  /**
57
80
  * Resolve overlaps within each layer by pushing nodes apart.
58
81
  * Sorts nodes by Y within each layer and ensures minimum gap.