@bpmnkit/core 0.0.22 → 0.0.23
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 +320 -21
- package/dist/bpmn/bpmn-builder.js +1 -0
- package/dist/bpmn/bpmn-model.d.ts +6 -0
- package/dist/bpmn/bpmn-parser.js +10 -0
- package/dist/bpmn/bpmn-serializer.js +10 -1
- package/dist/bpmn/compact.js +1 -0
- package/dist/bpmn/optimize/tasks.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/astar.d.ts +15 -0
- package/dist/layout/astar.js +191 -0
- package/dist/layout/block-builder.d.ts +37 -0
- package/dist/layout/block-builder.js +154 -0
- package/dist/layout/block-layout.d.ts +9 -0
- package/dist/layout/block-layout.js +163 -0
- package/dist/layout/coordinates.d.ts +23 -0
- package/dist/layout/coordinates.js +612 -88
- package/dist/layout/index.d.ts +5 -0
- package/dist/layout/index.js +4 -0
- package/dist/layout/layout-engine.js +79 -28
- package/dist/layout/routing.d.ts +10 -3
- package/dist/layout/routing.js +254 -71
- package/dist/layout/types.d.ts +6 -0
- package/package.json +3 -2
|
@@ -246,6 +246,7 @@ function countForwardReachable(startId, dag) {
|
|
|
246
246
|
* Align nodes in linear sequences to a common y-baseline.
|
|
247
247
|
* A "linear" node has ≤1 predecessor and ≤1 successor, and is not a gateway.
|
|
248
248
|
* Walks forward from each chain root, setting successors to the same center-y.
|
|
249
|
+
* Crosses split/join gateway pairs to align the full branch spine.
|
|
249
250
|
*/
|
|
250
251
|
export function alignBranchBaselines(layoutNodes, dag) {
|
|
251
252
|
const nodeMap = new Map();
|
|
@@ -288,8 +289,36 @@ export function alignBranchBaselines(layoutNodes, dag) {
|
|
|
288
289
|
const current = nodeMap.get(currentId);
|
|
289
290
|
if (!current)
|
|
290
291
|
break;
|
|
291
|
-
if (GATEWAY_TYPE_SET.has(current.type))
|
|
292
|
+
if (GATEWAY_TYPE_SET.has(current.type)) {
|
|
293
|
+
// If this is a split gateway, align it, find its join, align that, continue after
|
|
294
|
+
const trueSuccs = dag.successors.get(currentId) ?? [];
|
|
295
|
+
if (trueSuccs.length >= 2) {
|
|
296
|
+
const dy = baselineCenterY - (current.bounds.y + current.bounds.height / 2);
|
|
297
|
+
if (Math.abs(dy) > 0.5) {
|
|
298
|
+
current.bounds.y += dy;
|
|
299
|
+
if (current.labelBounds)
|
|
300
|
+
current.labelBounds.y += dy;
|
|
301
|
+
}
|
|
302
|
+
const joinId = findJoinGateway(currentId, dag, nodeMap);
|
|
303
|
+
if (joinId && !visited.has(joinId)) {
|
|
304
|
+
const joinNode = nodeMap.get(joinId);
|
|
305
|
+
if (joinNode) {
|
|
306
|
+
visited.add(joinId);
|
|
307
|
+
const jdy = baselineCenterY - (joinNode.bounds.y + joinNode.bounds.height / 2);
|
|
308
|
+
if (Math.abs(jdy) > 0.5) {
|
|
309
|
+
joinNode.bounds.y += jdy;
|
|
310
|
+
if (joinNode.labelBounds)
|
|
311
|
+
joinNode.labelBounds.y += jdy;
|
|
312
|
+
}
|
|
313
|
+
// Continue after the join
|
|
314
|
+
const joinSuccs = dag.successors.get(joinId) ?? [];
|
|
315
|
+
currentId = joinSuccs.length === 1 ? joinSuccs[0] : undefined;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
292
320
|
break;
|
|
321
|
+
}
|
|
293
322
|
const dy = baselineCenterY - (current.bounds.y + current.bounds.height / 2);
|
|
294
323
|
if (Math.abs(dy) > 0.5) {
|
|
295
324
|
current.bounds.y += dy;
|
|
@@ -302,8 +331,6 @@ export function alignBranchBaselines(layoutNodes, dag) {
|
|
|
302
331
|
nextId = succs[0];
|
|
303
332
|
}
|
|
304
333
|
else if (succs.length > 1 && !GATEWAY_TYPE_SET.has(current.type)) {
|
|
305
|
-
// Non-gateway with multiple successors (back-edge reversal artifact).
|
|
306
|
-
// Follow the unique-predecessor successor — the main flow continuation.
|
|
307
334
|
nextId = succs.find((s) => (dag.predecessors.get(s) ?? []).length === 1);
|
|
308
335
|
}
|
|
309
336
|
else {
|
|
@@ -312,10 +339,10 @@ export function alignBranchBaselines(layoutNodes, dag) {
|
|
|
312
339
|
if (!nextId)
|
|
313
340
|
break;
|
|
314
341
|
const nextNode = nodeMap.get(nextId);
|
|
315
|
-
if (!nextNode
|
|
342
|
+
if (!nextNode)
|
|
316
343
|
break;
|
|
317
344
|
const nextPreds = dag.predecessors.get(nextId) ?? [];
|
|
318
|
-
if (nextPreds.length !== 1)
|
|
345
|
+
if (nextPreds.length !== 1 && !GATEWAY_TYPE_SET.has(nextNode.type))
|
|
319
346
|
break;
|
|
320
347
|
currentId = nextId;
|
|
321
348
|
}
|
|
@@ -526,19 +553,10 @@ export function findBaselinePath(layoutNodes, dag, backEdges = []) {
|
|
|
526
553
|
// True split gateway: find join and jump to it
|
|
527
554
|
const joinId = findJoinGateway(currentId, dag, nodeMap);
|
|
528
555
|
if (joinId) {
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
const nonBypass = trueSuccs.filter((s) => s !== joinId);
|
|
534
|
-
currentId =
|
|
535
|
-
nonBypass.length === 1
|
|
536
|
-
? nonBypass[0]
|
|
537
|
-
: (findContinuationSuccessor(nonBypass, dag, nodeMap, visited) ?? joinId);
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
currentId = joinId;
|
|
541
|
-
}
|
|
556
|
+
// Always jump directly to the join gateway. Task-bearing branches are
|
|
557
|
+
// off-baseline and distributed by distributeSplitBranches, so the direct
|
|
558
|
+
// bypass edge never routes through task nodes.
|
|
559
|
+
currentId = joinId;
|
|
542
560
|
}
|
|
543
561
|
else {
|
|
544
562
|
currentId = findContinuationSuccessor(trueSuccs, dag, nodeMap, visited);
|
|
@@ -653,34 +671,306 @@ export function distributeSplitBranches(layoutNodes, dag, backEdges = []) {
|
|
|
653
671
|
continue;
|
|
654
672
|
splitGateways.push({ node: n, succs: trueSuccs, branchStarts, joinId });
|
|
655
673
|
}
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
674
|
+
// Sort deepest-first so child gateway branches are distributed before
|
|
675
|
+
// parent gateways compute branch heights (avoids underestimating sub-branch area).
|
|
676
|
+
splitGateways.sort((a, b) => b.node.layer - a.node.layer);
|
|
677
|
+
// Process all gateways deepest-first (single loop, handles both multi- and single-branch)
|
|
678
|
+
for (const { node: n, succs, branchStarts, joinId } of splitGateways) {
|
|
660
679
|
const gatewayCY = n.bounds.y + n.bounds.height / 2;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
680
|
+
// Collect baseline obstacles (actual element bboxes) for 2D collision detection.
|
|
681
|
+
// Unlike the old corridor approach, this only pushes branches past elements
|
|
682
|
+
// that actually overlap in both X and Y — not past the entire baseline extent.
|
|
683
|
+
const branchSet = new Set(branchStarts);
|
|
684
|
+
const baselineSucc = succs.find((s) => !branchSet.has(s));
|
|
685
|
+
const baselineObstacles = [];
|
|
686
|
+
if (baselineSucc) {
|
|
687
|
+
for (const bid of collectBranchChain(baselineSucc, dag, joinId)) {
|
|
688
|
+
const bnode = nodeMap.get(bid);
|
|
689
|
+
if (!bnode)
|
|
690
|
+
continue;
|
|
691
|
+
const bottom = bnode.labelBounds
|
|
692
|
+
? Math.max(bnode.bounds.y + bnode.bounds.height, bnode.labelBounds.y + bnode.labelBounds.height)
|
|
693
|
+
: bnode.bounds.y + bnode.bounds.height;
|
|
694
|
+
baselineObstacles.push({
|
|
695
|
+
x: bnode.bounds.x,
|
|
696
|
+
y: bnode.bounds.y,
|
|
697
|
+
right: bnode.bounds.x + bnode.bounds.width,
|
|
698
|
+
bottom,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const yMargin = GRID_CELL_HEIGHT / 2;
|
|
703
|
+
const xMargin = GRID_CELL_WIDTH / 2;
|
|
704
|
+
if (branchStarts.length >= 2) {
|
|
705
|
+
// Multi-branch: distribute symmetrically, heaviest branch at center
|
|
706
|
+
const sorted = [...branchStarts].sort((a, b) => collectBranchChain(b, dag, joinId).length - collectBranchChain(a, dag, joinId).length);
|
|
707
|
+
const count = sorted.length;
|
|
708
|
+
const positioned = new Array(count);
|
|
709
|
+
const m = Math.floor((count - 1) / 2);
|
|
710
|
+
// biome-ignore lint/style/noNonNullAssertion: sorted is non-empty (count >= 2)
|
|
711
|
+
positioned[m] = sorted[0];
|
|
712
|
+
let above = m - 1;
|
|
713
|
+
let below = m + 1;
|
|
714
|
+
for (let si = 1; si < count;) {
|
|
715
|
+
// biome-ignore lint/style/noNonNullAssertion: si < count ensures element exists
|
|
716
|
+
if (below < count && si < count)
|
|
717
|
+
positioned[below++] = sorted[si++];
|
|
718
|
+
// biome-ignore lint/style/noNonNullAssertion: si < count ensures element exists
|
|
719
|
+
if (above >= 0 && si < count)
|
|
720
|
+
positioned[above--] = sorted[si++];
|
|
721
|
+
}
|
|
722
|
+
// Compute anchor-relative extents and X range per branch
|
|
723
|
+
// Uses backbone extent for stacking height (initial placement),
|
|
724
|
+
// and full subtree extent for collision detection (safety validation).
|
|
725
|
+
const branchInfo = [];
|
|
726
|
+
for (let i = 0; i < count; i++) {
|
|
727
|
+
const branchId = positioned[i];
|
|
728
|
+
if (!branchId) {
|
|
729
|
+
branchInfo.push(null);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const chain = collectBranchChain(branchId, dag, joinId);
|
|
733
|
+
const startNode = nodeMap.get(branchId);
|
|
734
|
+
if (!startNode) {
|
|
735
|
+
branchInfo.push(null);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const startCY = startNode.bounds.y + startNode.bounds.height / 2;
|
|
739
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
740
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
741
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
742
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
743
|
+
for (const bid of chain) {
|
|
744
|
+
if (baselineSet.has(bid))
|
|
745
|
+
continue;
|
|
746
|
+
const bnode = nodeMap.get(bid);
|
|
747
|
+
if (!bnode)
|
|
748
|
+
continue;
|
|
749
|
+
minY = Math.min(minY, bnode.bounds.y);
|
|
750
|
+
maxY = Math.max(maxY, bnode.bounds.y + bnode.bounds.height);
|
|
751
|
+
if (bnode.labelBounds)
|
|
752
|
+
maxY = Math.max(maxY, bnode.labelBounds.y + bnode.labelBounds.height);
|
|
753
|
+
minX = Math.min(minX, bnode.bounds.x);
|
|
754
|
+
maxX = Math.max(maxX, bnode.bounds.x + bnode.bounds.width);
|
|
755
|
+
}
|
|
756
|
+
const height = minY < maxY ? Math.max(maxY - minY, GRID_CELL_HEIGHT) : GRID_CELL_HEIGHT;
|
|
757
|
+
// Backbone extent for stacking
|
|
758
|
+
const backbone = collectBranchBackbone(branchId, dag, joinId, nodeMap);
|
|
759
|
+
let bbMinY = Number.POSITIVE_INFINITY;
|
|
760
|
+
let bbMaxY = Number.NEGATIVE_INFINITY;
|
|
761
|
+
for (const bid of backbone) {
|
|
762
|
+
if (baselineSet.has(bid))
|
|
763
|
+
continue;
|
|
764
|
+
const bnode = nodeMap.get(bid);
|
|
765
|
+
if (!bnode)
|
|
766
|
+
continue;
|
|
767
|
+
bbMinY = Math.min(bbMinY, bnode.bounds.y);
|
|
768
|
+
bbMaxY = Math.max(bbMaxY, bnode.bounds.y + bnode.bounds.height);
|
|
769
|
+
if (bnode.labelBounds)
|
|
770
|
+
bbMaxY = Math.max(bbMaxY, bnode.labelBounds.y + bnode.labelBounds.height);
|
|
771
|
+
}
|
|
772
|
+
const backboneHeight = bbMinY < bbMaxY ? Math.max(bbMaxY - bbMinY, GRID_CELL_HEIGHT) : height;
|
|
773
|
+
branchInfo.push({
|
|
774
|
+
chain,
|
|
775
|
+
extAbove: minY < Number.POSITIVE_INFINITY ? startCY - minY : height / 2,
|
|
776
|
+
extBelow: maxY > Number.NEGATIVE_INFINITY ? maxY - startCY : height / 2,
|
|
777
|
+
backboneHeight,
|
|
778
|
+
height,
|
|
779
|
+
minX: minX < Number.POSITIVE_INFINITY ? minX : startNode.bounds.x,
|
|
780
|
+
maxX: maxX > Number.NEGATIVE_INFINITY ? maxX : startNode.bounds.x + startNode.bounds.width,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const minSpacing = GRID_CELL_HEIGHT / 2;
|
|
784
|
+
// Use backbone height for initial stacking (tighter spacing)
|
|
785
|
+
let totalH = 0;
|
|
786
|
+
for (let i = 0; i < count; i++) {
|
|
787
|
+
totalH += branchInfo[i]?.backboneHeight ?? GRID_CELL_HEIGHT;
|
|
788
|
+
if (i < count - 1)
|
|
789
|
+
totalH += minSpacing;
|
|
790
|
+
}
|
|
791
|
+
let currentTop = gatewayCY - totalH / 2;
|
|
792
|
+
// Frontier tracking: placed branch extents for branch-to-branch spacing
|
|
793
|
+
let aboveFrontierY = gatewayCY;
|
|
794
|
+
let belowFrontierY = gatewayCY;
|
|
795
|
+
for (let i = 0; i < count; i++) {
|
|
796
|
+
const info = branchInfo[i];
|
|
797
|
+
if (!info) {
|
|
798
|
+
currentTop += GRID_CELL_HEIGHT + minSpacing;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const branchId = positioned[i];
|
|
802
|
+
if (!branchId)
|
|
803
|
+
continue;
|
|
804
|
+
const branchNode = nodeMap.get(branchId);
|
|
805
|
+
if (!branchNode)
|
|
806
|
+
continue;
|
|
807
|
+
// Use backbone height for initial placement, full extent for collision
|
|
808
|
+
const bh = info.backboneHeight;
|
|
809
|
+
let targetCY = currentTop + bh / 2;
|
|
810
|
+
// Resolve 2D collisions with baseline obstacles (full extent)
|
|
811
|
+
targetCY = resolveBranchObstacles(targetCY, info.extAbove, info.extBelow, info.minX, info.maxX, gatewayCY, baselineObstacles, yMargin, xMargin);
|
|
812
|
+
// Enforce frontier: prevent branch-to-branch overlap (full extent)
|
|
813
|
+
if (targetCY >= gatewayCY) {
|
|
814
|
+
const newTop = targetCY - info.extAbove;
|
|
815
|
+
if (newTop < belowFrontierY + minSpacing) {
|
|
816
|
+
targetCY = belowFrontierY + minSpacing + info.extAbove;
|
|
817
|
+
}
|
|
818
|
+
belowFrontierY = targetCY + info.extBelow;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
const newBottom = targetCY + info.extBelow;
|
|
822
|
+
if (newBottom > aboveFrontierY - minSpacing) {
|
|
823
|
+
targetCY = aboveFrontierY - minSpacing - info.extBelow;
|
|
824
|
+
}
|
|
825
|
+
aboveFrontierY = targetCY - info.extAbove;
|
|
826
|
+
}
|
|
827
|
+
const currentCY = branchNode.bounds.y + branchNode.bounds.height / 2;
|
|
828
|
+
const dy = targetCY - currentCY;
|
|
829
|
+
if (Math.abs(dy) > 0.5) {
|
|
830
|
+
for (const bid of info.chain) {
|
|
831
|
+
if (baselineSet.has(bid))
|
|
832
|
+
continue;
|
|
833
|
+
const bnode = nodeMap.get(bid);
|
|
834
|
+
if (!bnode)
|
|
835
|
+
continue;
|
|
836
|
+
bnode.bounds.y += dy;
|
|
837
|
+
if (bnode.labelBounds)
|
|
838
|
+
bnode.labelBounds.y += dy;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
currentTop += bh + minSpacing;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
// Single-branch: collision-based offset with peer-aware gap enforcement
|
|
846
|
+
const branchId = branchStarts[0];
|
|
673
847
|
if (!branchId)
|
|
674
848
|
continue;
|
|
675
849
|
const branchNode = nodeMap.get(branchId);
|
|
676
850
|
if (!branchNode)
|
|
677
851
|
continue;
|
|
678
|
-
const
|
|
852
|
+
const chain = collectBranchChain(branchId, dag, joinId);
|
|
853
|
+
// Compute full subtree extents (for collision validation)
|
|
854
|
+
let branchMinY = Number.POSITIVE_INFINITY;
|
|
855
|
+
let branchMaxY = Number.NEGATIVE_INFINITY;
|
|
856
|
+
let branchMinX = Number.POSITIVE_INFINITY;
|
|
857
|
+
let branchMaxX = Number.NEGATIVE_INFINITY;
|
|
858
|
+
for (const bid of chain) {
|
|
859
|
+
if (baselineSet.has(bid))
|
|
860
|
+
continue;
|
|
861
|
+
const bnode = nodeMap.get(bid);
|
|
862
|
+
if (!bnode)
|
|
863
|
+
continue;
|
|
864
|
+
branchMinY = Math.min(branchMinY, bnode.bounds.y);
|
|
865
|
+
branchMaxY = Math.max(branchMaxY, bnode.bounds.y + bnode.bounds.height);
|
|
866
|
+
if (bnode.labelBounds) {
|
|
867
|
+
branchMaxY = Math.max(branchMaxY, bnode.labelBounds.y + bnode.labelBounds.height);
|
|
868
|
+
}
|
|
869
|
+
branchMinX = Math.min(branchMinX, bnode.bounds.x);
|
|
870
|
+
branchMaxX = Math.max(branchMaxX, bnode.bounds.x + bnode.bounds.width);
|
|
871
|
+
}
|
|
679
872
|
const currentCY = branchNode.bounds.y + branchNode.bounds.height / 2;
|
|
873
|
+
const extAbove = branchMinY < Number.POSITIVE_INFINITY ? currentCY - branchMinY : GRID_CELL_HEIGHT / 2;
|
|
874
|
+
const extBelow = branchMaxY > Number.NEGATIVE_INFINITY ? branchMaxY - currentCY : GRID_CELL_HEIGHT / 2;
|
|
875
|
+
if (branchMinX === Number.POSITIVE_INFINITY)
|
|
876
|
+
branchMinX = branchNode.bounds.x;
|
|
877
|
+
if (branchMaxX === Number.NEGATIVE_INFINITY)
|
|
878
|
+
branchMaxX = branchNode.bounds.x + branchNode.bounds.width;
|
|
879
|
+
// Compute backbone extent (spine only, skipping sub-gateway branches)
|
|
880
|
+
// for a tighter initial offset that places the branch closer to baseline.
|
|
881
|
+
const backbone = collectBranchBackbone(branchId, dag, joinId, nodeMap);
|
|
882
|
+
let bbMinY = Number.POSITIVE_INFINITY;
|
|
883
|
+
let bbMaxY = Number.NEGATIVE_INFINITY;
|
|
884
|
+
for (const bid of backbone) {
|
|
885
|
+
if (baselineSet.has(bid))
|
|
886
|
+
continue;
|
|
887
|
+
const bnode = nodeMap.get(bid);
|
|
888
|
+
if (!bnode)
|
|
889
|
+
continue;
|
|
890
|
+
bbMinY = Math.min(bbMinY, bnode.bounds.y);
|
|
891
|
+
bbMaxY = Math.max(bbMaxY, bnode.bounds.y + bnode.bounds.height);
|
|
892
|
+
if (bnode.labelBounds) {
|
|
893
|
+
bbMaxY = Math.max(bbMaxY, bnode.labelBounds.y + bnode.labelBounds.height);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const bbExtAbove = bbMinY < Number.POSITIVE_INFINITY ? currentCY - bbMinY : extAbove;
|
|
897
|
+
const bbExtBelow = bbMaxY > Number.NEGATIVE_INFINITY ? bbMaxY - currentCY : extBelow;
|
|
898
|
+
// Use backbone extent for initial offset (closer to baseline)
|
|
899
|
+
const basicMinOffset = Math.max(GRID_CELL_HEIGHT, bbExtAbove + bbExtBelow + n.bounds.height / 2 + 20);
|
|
900
|
+
const direction = currentCY < gatewayCY ? -1 : 1;
|
|
901
|
+
let targetCY = gatewayCY + direction * basicMinOffset;
|
|
902
|
+
// Validate using per-element collision against baseline obstacles.
|
|
903
|
+
// Only backbone elements checked — sub-branches at different X
|
|
904
|
+
// positions don't constrain placement.
|
|
905
|
+
for (const obs of baselineObstacles) {
|
|
906
|
+
for (const bid of backbone) {
|
|
907
|
+
if (baselineSet.has(bid))
|
|
908
|
+
continue;
|
|
909
|
+
const bnode = nodeMap.get(bid);
|
|
910
|
+
if (!bnode)
|
|
911
|
+
continue;
|
|
912
|
+
const elemRight = bnode.bounds.x + bnode.bounds.width;
|
|
913
|
+
if (elemRight + xMargin < obs.x || bnode.bounds.x - xMargin > obs.right)
|
|
914
|
+
continue;
|
|
915
|
+
if (direction === 1) {
|
|
916
|
+
const elemNewY = bnode.bounds.y + (targetCY - currentCY);
|
|
917
|
+
if (elemNewY < obs.bottom + yMargin) {
|
|
918
|
+
const needed = currentCY + obs.bottom + yMargin - bnode.bounds.y;
|
|
919
|
+
if (needed > targetCY)
|
|
920
|
+
targetCY = needed;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
const elemNewBottom = bnode.bounds.y + bnode.bounds.height + (targetCY - currentCY);
|
|
925
|
+
if (elemNewBottom > obs.y - yMargin) {
|
|
926
|
+
const needed = currentCY + obs.y - yMargin - bnode.bounds.y - bnode.bounds.height;
|
|
927
|
+
if (needed < targetCY)
|
|
928
|
+
targetCY = needed;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Peer-aware gap enforcement — only backbone elements checked against
|
|
934
|
+
// same-layer peers. Sub-branch elements from nested gateways are
|
|
935
|
+
// handled by the nested gateway's own distribution step.
|
|
936
|
+
const chainSet = new Set(chain);
|
|
937
|
+
const backboneSet = new Set(backbone);
|
|
938
|
+
const minGap = GRID_CELL_HEIGHT / 2;
|
|
939
|
+
const initialDy = targetCY - currentCY;
|
|
940
|
+
let extraDy = 0;
|
|
941
|
+
for (const chainNodeId of backbone) {
|
|
942
|
+
if (baselineSet.has(chainNodeId))
|
|
943
|
+
continue;
|
|
944
|
+
const chainNode = nodeMap.get(chainNodeId);
|
|
945
|
+
if (!chainNode)
|
|
946
|
+
continue;
|
|
947
|
+
const chainNodeCY = chainNode.bounds.y + chainNode.bounds.height / 2;
|
|
948
|
+
const chainNodeNewCY = chainNodeCY + initialDy;
|
|
949
|
+
for (const peer of layoutNodes) {
|
|
950
|
+
if (peer.layer !== chainNode.layer || chainSet.has(peer.id))
|
|
951
|
+
continue;
|
|
952
|
+
const peerCY = peer.bounds.y + peer.bounds.height / 2;
|
|
953
|
+
const minDist = (chainNode.bounds.height + peer.bounds.height) / 2 + minGap;
|
|
954
|
+
if (Math.abs(chainNodeNewCY + extraDy - peerCY) < minDist) {
|
|
955
|
+
if (direction === -1) {
|
|
956
|
+
const needed = peerCY - minDist - chainNodeNewCY;
|
|
957
|
+
if (needed < extraDy)
|
|
958
|
+
extraDy = needed;
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
const needed = peerCY + minDist - chainNodeNewCY;
|
|
962
|
+
if (needed > extraDy)
|
|
963
|
+
extraDy = needed;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
targetCY += extraDy;
|
|
680
969
|
const dy = targetCY - currentCY;
|
|
681
970
|
if (Math.abs(dy) > 0.5) {
|
|
682
|
-
const chain = collectBranchChain(branchId, dag, joinId);
|
|
683
971
|
for (const bid of chain) {
|
|
972
|
+
if (baselineSet.has(bid))
|
|
973
|
+
continue;
|
|
684
974
|
const bnode = nodeMap.get(bid);
|
|
685
975
|
if (!bnode)
|
|
686
976
|
continue;
|
|
@@ -691,60 +981,256 @@ export function distributeSplitBranches(layoutNodes, dag, backEdges = []) {
|
|
|
691
981
|
}
|
|
692
982
|
}
|
|
693
983
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Push targetCY away from gatewayCY until it no longer overlaps any obstacle in both X and Y.
|
|
987
|
+
* Only elements that overlap the branch's X range (with margin) trigger a push.
|
|
988
|
+
*/
|
|
989
|
+
function resolveBranchObstacles(initialCY, extAbove, extBelow, branchMinX, branchMaxX, gatewayCY, obstacles, yMargin, xMargin) {
|
|
990
|
+
let targetCY = initialCY;
|
|
991
|
+
const direction = targetCY >= gatewayCY ? 1 : -1;
|
|
992
|
+
const sorted = direction > 0
|
|
993
|
+
? [...obstacles].sort((a, b) => a.bottom - b.bottom)
|
|
994
|
+
: [...obstacles].sort((a, b) => b.y - a.y);
|
|
995
|
+
for (const obs of sorted) {
|
|
996
|
+
if (branchMaxX + xMargin < obs.x || branchMinX - xMargin > obs.right)
|
|
697
997
|
continue;
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
701
|
-
continue;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
998
|
+
const newTop = targetCY - extAbove;
|
|
999
|
+
const newBottom = targetCY + extBelow;
|
|
1000
|
+
if (newBottom + yMargin <= obs.y || newTop - yMargin >= obs.bottom)
|
|
1001
|
+
continue;
|
|
1002
|
+
if (direction > 0) {
|
|
1003
|
+
targetCY = obs.bottom + yMargin + extAbove;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
targetCY = obs.y - yMargin - extBelow;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return targetCY;
|
|
1010
|
+
}
|
|
1011
|
+
/** Centre-Y of a layout node. */
|
|
1012
|
+
function getCY(node) {
|
|
1013
|
+
return node.bounds.y + node.bounds.height / 2;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Assign integer grid-row indices to all nodes based on their final center-Y positions.
|
|
1017
|
+
* Nodes whose center-Y values are within EPSILON pixels are placed in the same row.
|
|
1018
|
+
* This eliminates pixel-tolerance guessing in port-side decisions: two nodes with the
|
|
1019
|
+
* same gridRow are on the same horizontal row and should connect left-to-right.
|
|
1020
|
+
*
|
|
1021
|
+
* Called once, after ALL coordinate adjustments, just before edge routing.
|
|
1022
|
+
*/
|
|
1023
|
+
export function assignGridRows(layoutNodes) {
|
|
1024
|
+
if (layoutNodes.length === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
const EPSILON = 5; // nodes within 5px center-Y share a row
|
|
1027
|
+
const sorted = [...layoutNodes].sort((a, b) => getCY(a) - getCY(b));
|
|
1028
|
+
const first = sorted[0];
|
|
1029
|
+
if (!first)
|
|
1030
|
+
return;
|
|
1031
|
+
let rowIdx = 0;
|
|
1032
|
+
let rowCY = getCY(first);
|
|
1033
|
+
for (const node of sorted) {
|
|
1034
|
+
const cy = getCY(node);
|
|
1035
|
+
if (cy - rowCY > EPSILON) {
|
|
1036
|
+
rowIdx++;
|
|
1037
|
+
rowCY = cy;
|
|
1038
|
+
}
|
|
1039
|
+
node.gridRow = rowIdx;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Snap nodes to common Y rows for matrix-like alignment.
|
|
1044
|
+
* Groups nodes that share a CY (from alignment passes), then merges
|
|
1045
|
+
* close groups into a single row — moving entire groups as units.
|
|
1046
|
+
* Boundary events are excluded (they are repositioned later).
|
|
1047
|
+
*/
|
|
1048
|
+
export function snapToYRows(layoutNodes) {
|
|
1049
|
+
if (layoutNodes.length < 2)
|
|
1050
|
+
return;
|
|
1051
|
+
const MERGE_THRESHOLD = 35;
|
|
1052
|
+
const GROUP_EPSILON = 3;
|
|
1053
|
+
// Exclude boundary events (repositioned later in auto-layout.ts)
|
|
1054
|
+
const candidates = layoutNodes.filter((n) => n.type !== "boundaryEvent");
|
|
1055
|
+
if (candidates.length < 2)
|
|
1056
|
+
return;
|
|
1057
|
+
// Step 1: Group by current CY (nodes aligned by earlier passes share exact CY)
|
|
1058
|
+
const sorted = [...candidates].sort((a, b) => getCY(a) - getCY(b));
|
|
1059
|
+
const first = sorted[0];
|
|
1060
|
+
if (!first)
|
|
1061
|
+
return;
|
|
1062
|
+
const rows = [];
|
|
1063
|
+
let currentGroup = [first];
|
|
1064
|
+
let groupCY = getCY(first);
|
|
1065
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1066
|
+
const node = sorted[i];
|
|
1067
|
+
if (!node)
|
|
1068
|
+
continue;
|
|
1069
|
+
const cy = getCY(node);
|
|
1070
|
+
if (cy - groupCY <= GROUP_EPSILON) {
|
|
1071
|
+
currentGroup.push(node);
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
rows.push({ nodes: currentGroup, cy: groupCY });
|
|
1075
|
+
currentGroup = [node];
|
|
1076
|
+
groupCY = cy;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
rows.push({ nodes: currentGroup, cy: groupCY });
|
|
1080
|
+
// Step 2: Merge close consecutive rows (move smaller row to larger row's CY)
|
|
1081
|
+
let changed = true;
|
|
1082
|
+
while (changed) {
|
|
1083
|
+
changed = false;
|
|
1084
|
+
for (let i = 0; i < rows.length - 1; i++) {
|
|
1085
|
+
const a = rows[i];
|
|
1086
|
+
const b = rows[i + 1];
|
|
1087
|
+
if (!a || !b)
|
|
1088
|
+
continue;
|
|
1089
|
+
if (b.cy - a.cy > MERGE_THRESHOLD)
|
|
1090
|
+
continue;
|
|
1091
|
+
// Skip if merge would create same-layer overlap
|
|
1092
|
+
const aLayers = new Set(a.nodes.map((n) => n.layer));
|
|
1093
|
+
if (b.nodes.some((n) => aLayers.has(n.layer)))
|
|
1094
|
+
continue;
|
|
1095
|
+
// Move smaller group to larger group's CY
|
|
1096
|
+
const [target, source] = a.nodes.length >= b.nodes.length ? [a, b] : [b, a];
|
|
1097
|
+
for (const node of source.nodes) {
|
|
1098
|
+
const dy = target.cy - getCY(node);
|
|
1099
|
+
if (Math.abs(dy) > 0.5) {
|
|
1100
|
+
node.bounds.y += dy;
|
|
1101
|
+
if (node.labelBounds)
|
|
1102
|
+
node.labelBounds.y += dy;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
rows[i] = { nodes: [...a.nodes, ...b.nodes], cy: target.cy };
|
|
1106
|
+
rows.splice(i + 1, 1);
|
|
1107
|
+
changed = true;
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Pull each branch subtree toward the baseline, closing unnecessary vertical gaps.
|
|
1114
|
+
* Uses per-element 2D collision detection — only actual element-to-element overlaps
|
|
1115
|
+
* constrain the movement, not the branch's full bounding box.
|
|
1116
|
+
*/
|
|
1117
|
+
export function compactBranches(layoutNodes, dag, backEdges = []) {
|
|
1118
|
+
const backEdgeOriginals = buildBackEdgeOriginals(backEdges);
|
|
1119
|
+
const nodeMap = new Map();
|
|
1120
|
+
for (const n of layoutNodes)
|
|
1121
|
+
nodeMap.set(n.id, n);
|
|
1122
|
+
const baselinePath = findBaselinePath(layoutNodes, dag, backEdges);
|
|
1123
|
+
const baselineSet = new Set(baselinePath);
|
|
1124
|
+
// Collect split gateways and their branches
|
|
1125
|
+
const splitGateways = [];
|
|
1126
|
+
for (const n of layoutNodes) {
|
|
1127
|
+
if (!GATEWAY_TYPE_SET.has(n.type))
|
|
1128
|
+
continue;
|
|
1129
|
+
const trueSuccs = getTrueSuccessors(n.id, dag, backEdgeOriginals);
|
|
1130
|
+
if (trueSuccs.length < 2)
|
|
1131
|
+
continue;
|
|
1132
|
+
const joinId = findJoinGateway(n.id, dag, nodeMap);
|
|
1133
|
+
const branchStarts = trueSuccs.filter((s) => !baselineSet.has(s));
|
|
1134
|
+
if (branchStarts.length === 0)
|
|
1135
|
+
continue;
|
|
1136
|
+
splitGateways.push({ node: n, branchStarts, joinId });
|
|
1137
|
+
}
|
|
1138
|
+
// Process outermost gateways first (shallowest-first)
|
|
1139
|
+
splitGateways.sort((a, b) => a.node.layer - b.node.layer);
|
|
1140
|
+
const margin = GRID_CELL_HEIGHT / 2;
|
|
1141
|
+
const movedElements = new Set();
|
|
1142
|
+
for (const { node: gw, branchStarts, joinId } of splitGateways) {
|
|
1143
|
+
// Skip open-ended branches (joinId=undefined) — unbounded chains
|
|
1144
|
+
if (!joinId)
|
|
1145
|
+
continue;
|
|
1146
|
+
const gatewayCY = getCY(gw);
|
|
1147
|
+
for (const branchId of branchStarts) {
|
|
1148
|
+
const chain = collectBranchChain(branchId, dag, joinId);
|
|
1149
|
+
if (chain.some((id) => movedElements.has(id)))
|
|
1150
|
+
continue;
|
|
1151
|
+
// Get non-baseline elements in the chain
|
|
1152
|
+
const chainElements = [];
|
|
1153
|
+
for (const bid of chain) {
|
|
1154
|
+
if (baselineSet.has(bid))
|
|
1155
|
+
continue;
|
|
1156
|
+
const bnode = nodeMap.get(bid);
|
|
1157
|
+
if (bnode)
|
|
1158
|
+
chainElements.push(bnode);
|
|
1159
|
+
}
|
|
1160
|
+
if (chainElements.length === 0)
|
|
1161
|
+
continue;
|
|
1162
|
+
const chainSet = new Set(chain);
|
|
1163
|
+
const startNode = nodeMap.get(branchId);
|
|
1164
|
+
if (!startNode)
|
|
1165
|
+
continue;
|
|
1166
|
+
const currentCY = getCY(startNode);
|
|
1167
|
+
const direction = currentCY > gatewayCY ? 1 : -1;
|
|
1168
|
+
// Compute minimum offset from gateway (backbone + half gateway)
|
|
1169
|
+
const backbone = collectBranchBackbone(branchId, dag, joinId, nodeMap);
|
|
1170
|
+
let bbMinY = Number.POSITIVE_INFINITY;
|
|
1171
|
+
let bbMaxY = Number.NEGATIVE_INFINITY;
|
|
1172
|
+
for (const bid of backbone) {
|
|
1173
|
+
if (baselineSet.has(bid))
|
|
1174
|
+
continue;
|
|
1175
|
+
const bnode = nodeMap.get(bid);
|
|
1176
|
+
if (!bnode)
|
|
1177
|
+
continue;
|
|
1178
|
+
bbMinY = Math.min(bbMinY, bnode.bounds.y);
|
|
1179
|
+
bbMaxY = Math.max(bbMaxY, bnode.bounds.y + bnode.bounds.height);
|
|
1180
|
+
}
|
|
1181
|
+
const bbExtAbove = bbMinY < Number.POSITIVE_INFINITY ? currentCY - bbMinY : GRID_CELL_HEIGHT / 2;
|
|
1182
|
+
const bbExtBelow = bbMaxY > Number.NEGATIVE_INFINITY ? bbMaxY - currentCY : GRID_CELL_HEIGHT / 2;
|
|
1183
|
+
const closestCY = direction > 0 ? gatewayCY + bbExtAbove + margin : gatewayCY - bbExtBelow - margin;
|
|
1184
|
+
if (direction > 0 ? closestCY >= currentCY : closestCY <= currentCY)
|
|
716
1185
|
continue;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1186
|
+
// Per-element collision detection: for each obstacle, check every element
|
|
1187
|
+
// in the chain. Only actual X+Y overlaps constrain movement.
|
|
1188
|
+
let bestCY = closestCY;
|
|
1189
|
+
for (const obs of layoutNodes) {
|
|
1190
|
+
if (chainSet.has(obs.id))
|
|
721
1191
|
continue;
|
|
722
|
-
const
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1192
|
+
const obsLeft = obs.bounds.x;
|
|
1193
|
+
const obsRight = obs.bounds.x + obs.bounds.width;
|
|
1194
|
+
const obsTop = obs.labelBounds ? Math.min(obs.bounds.y, obs.labelBounds.y) : obs.bounds.y;
|
|
1195
|
+
const obsBottom = obs.labelBounds
|
|
1196
|
+
? Math.max(obs.bounds.y + obs.bounds.height, obs.labelBounds.y + obs.labelBounds.height)
|
|
1197
|
+
: obs.bounds.y + obs.bounds.height;
|
|
1198
|
+
for (const elem of chainElements) {
|
|
1199
|
+
const elemRight = elem.bounds.x + elem.bounds.width;
|
|
1200
|
+
// X overlap check
|
|
1201
|
+
if (elemRight < obsLeft - margin || elem.bounds.x > obsRight + margin)
|
|
1202
|
+
continue;
|
|
1203
|
+
if (direction > 0) {
|
|
1204
|
+
// Element top (after moving) must be below obstacle bottom
|
|
1205
|
+
// elem.bounds.y + dy ≥ obsBottom + margin
|
|
1206
|
+
// bestCY ≥ currentCY + obsBottom + margin - elem.bounds.y
|
|
1207
|
+
const needed = currentCY + obsBottom + margin - elem.bounds.y;
|
|
1208
|
+
if (needed > bestCY)
|
|
1209
|
+
bestCY = needed;
|
|
729
1210
|
}
|
|
730
1211
|
else {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1212
|
+
// Element bottom (after moving) must be above obstacle top
|
|
1213
|
+
const elemBottom = elem.bounds.y + elem.bounds.height;
|
|
1214
|
+
const needed = currentCY + obsTop - margin - elemBottom;
|
|
1215
|
+
if (needed < bestCY)
|
|
1216
|
+
bestCY = needed;
|
|
734
1217
|
}
|
|
735
1218
|
}
|
|
736
1219
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1220
|
+
// Only move if we're actually pulling closer
|
|
1221
|
+
if (direction > 0 ? bestCY >= currentCY : bestCY <= currentCY)
|
|
1222
|
+
continue;
|
|
1223
|
+
const dy = bestCY - currentCY;
|
|
741
1224
|
for (const bid of chain) {
|
|
1225
|
+
if (baselineSet.has(bid))
|
|
1226
|
+
continue;
|
|
742
1227
|
const bnode = nodeMap.get(bid);
|
|
743
1228
|
if (!bnode)
|
|
744
1229
|
continue;
|
|
745
1230
|
bnode.bounds.y += dy;
|
|
746
1231
|
if (bnode.labelBounds)
|
|
747
1232
|
bnode.labelBounds.y += dy;
|
|
1233
|
+
movedElements.add(bid);
|
|
748
1234
|
}
|
|
749
1235
|
}
|
|
750
1236
|
}
|
|
@@ -769,6 +1255,46 @@ function collectBranchChain(startId, dag, joinId) {
|
|
|
769
1255
|
}
|
|
770
1256
|
return ids;
|
|
771
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Collect only the backbone (spine) of a branch — following the linear path
|
|
1260
|
+
* and jumping over nested sub-gateways via their join.
|
|
1261
|
+
* Used to compute a tighter extent estimate for distribution placement.
|
|
1262
|
+
*/
|
|
1263
|
+
function collectBranchBackbone(startId, dag, joinId, nodeMap) {
|
|
1264
|
+
const ids = [];
|
|
1265
|
+
let currentId = startId;
|
|
1266
|
+
const seen = new Set();
|
|
1267
|
+
while (currentId && !seen.has(currentId)) {
|
|
1268
|
+
if (joinId && currentId === joinId)
|
|
1269
|
+
break;
|
|
1270
|
+
seen.add(currentId);
|
|
1271
|
+
ids.push(currentId);
|
|
1272
|
+
const succs = dag.successors.get(currentId) ?? [];
|
|
1273
|
+
if (succs.length === 0)
|
|
1274
|
+
break;
|
|
1275
|
+
if (succs.length === 1) {
|
|
1276
|
+
currentId = succs[0];
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
// At a sub-split gateway: jump to its join
|
|
1280
|
+
const node = nodeMap.get(currentId);
|
|
1281
|
+
if (node && GATEWAY_TYPE_SET.has(node.type)) {
|
|
1282
|
+
const subJoinId = findJoinGateway(currentId, dag, nodeMap);
|
|
1283
|
+
if (subJoinId) {
|
|
1284
|
+
currentId = subJoinId;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
// No join found — stop backbone here, fall back to full extent
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
currentId = succs[0];
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return ids;
|
|
1297
|
+
}
|
|
772
1298
|
/**
|
|
773
1299
|
* Resolve overlaps within each layer by pushing nodes apart.
|
|
774
1300
|
* Sorts nodes by Y within each layer and ensures minimum gap.
|
|
@@ -838,7 +1364,7 @@ function swapBranchPositions(branchA, branchB, dag, nodeMap, joinId) {
|
|
|
838
1364
|
};
|
|
839
1365
|
const nodesA = collectBranch(branchA);
|
|
840
1366
|
const nodesB = collectBranch(branchB);
|
|
841
|
-
// Swap
|
|
1367
|
+
// Swap center-Y pairwise (not raw Y) so elements of different heights stay aligned
|
|
842
1368
|
const swapCount = Math.min(nodesA.length, nodesB.length);
|
|
843
1369
|
for (let i = 0; i < swapCount; i++) {
|
|
844
1370
|
const idA = nodesA[i];
|
|
@@ -849,20 +1375,18 @@ function swapBranchPositions(branchA, branchB, dag, nodeMap, joinId) {
|
|
|
849
1375
|
const b = nodeMap.get(idB);
|
|
850
1376
|
if (!a || !b)
|
|
851
1377
|
continue;
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
a.labelBounds.y
|
|
858
|
-
b.labelBounds.y = tmpLabelY;
|
|
859
|
-
}
|
|
860
|
-
else if (a.labelBounds) {
|
|
861
|
-
a.labelBounds.y += b.bounds.y - a.bounds.y;
|
|
1378
|
+
const aCY = a.bounds.y + a.bounds.height / 2;
|
|
1379
|
+
const bCY = b.bounds.y + b.bounds.height / 2;
|
|
1380
|
+
const aNewY = bCY - a.bounds.height / 2;
|
|
1381
|
+
const bNewY = aCY - b.bounds.height / 2;
|
|
1382
|
+
if (a.labelBounds) {
|
|
1383
|
+
a.labelBounds.y += aNewY - a.bounds.y;
|
|
862
1384
|
}
|
|
863
|
-
|
|
864
|
-
b.labelBounds.y +=
|
|
1385
|
+
if (b.labelBounds) {
|
|
1386
|
+
b.labelBounds.y += bNewY - b.bounds.y;
|
|
865
1387
|
}
|
|
1388
|
+
a.bounds.y = aNewY;
|
|
1389
|
+
b.bounds.y = bNewY;
|
|
866
1390
|
}
|
|
867
1391
|
}
|
|
868
1392
|
//# sourceMappingURL=coordinates.js.map
|