@crazyhappyone/auto-graph 0.2.9 → 0.2.10

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/index.js CHANGED
@@ -4805,7 +4805,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4805
4805
  detail: {
4806
4806
  xsCount: xs.length,
4807
4807
  ysCount: ys.length,
4808
- maxNodes
4808
+ maxNodes,
4809
+ obstacleCount: obstacles.length,
4810
+ stage: "corridor-filtered",
4811
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4809
4812
  }
4810
4813
  });
4811
4814
  return null;
@@ -4851,7 +4854,14 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4851
4854
  severity: "warning",
4852
4855
  code: "routing.astar.grid_overflow",
4853
4856
  message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4854
- detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4857
+ detail: {
4858
+ xsCount: xsFull.length,
4859
+ ysCount: ysFull.length,
4860
+ maxNodes,
4861
+ obstacleCount: obstacles.length,
4862
+ stage: "full-retry",
4863
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4864
+ }
4855
4865
  });
4856
4866
  return null;
4857
4867
  }
@@ -5094,6 +5104,64 @@ function areCollinear(a, b, c) {
5094
5104
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
5095
5105
  }
5096
5106
 
5107
+ // src/routing/budget.ts
5108
+ var MIN_CORNER_BUDGET = 600;
5109
+ var MAX_CORNER_BUDGET = 3e3;
5110
+ var MIN_NODE_BUDGET = 4e3;
5111
+ var MAX_NODE_BUDGET = 64e3;
5112
+ var CORNERS_PER_OBSTACLE = 12;
5113
+ var CORNER_HEADROOM = 2;
5114
+ var GRID_SAFETY_FACTOR = 3;
5115
+ var CORRIDOR_SCALING_K = 0.5;
5116
+ var CORRIDOR_SCALING_BASE = 200;
5117
+ function computeRoutingBudget(cornerObstacles, allObstacles, corridorMargin, overrides = {}) {
5118
+ const adaptiveMaxCorners = deriveMaxCorners(
5119
+ cornerObstacles.length,
5120
+ corridorMargin
5121
+ );
5122
+ const adaptiveMaxNodes = deriveMaxNodes(
5123
+ allObstacles.length,
5124
+ corridorMargin
5125
+ );
5126
+ return {
5127
+ maxCorners: resolveBudget(
5128
+ overrides.maxCorners,
5129
+ adaptiveMaxCorners,
5130
+ MIN_CORNER_BUDGET,
5131
+ MAX_CORNER_BUDGET
5132
+ ),
5133
+ maxNodes: resolveBudget(
5134
+ overrides.maxNodes,
5135
+ adaptiveMaxNodes,
5136
+ MIN_NODE_BUDGET,
5137
+ MAX_NODE_BUDGET
5138
+ ),
5139
+ cornerObstacleCount: cornerObstacles.length,
5140
+ gridObstacleCount: allObstacles.length,
5141
+ corridorMargin
5142
+ };
5143
+ }
5144
+ function deriveMaxCorners(obstacleCount, corridorMargin) {
5145
+ const base = 2 + obstacleCount * CORNERS_PER_OBSTACLE * CORNER_HEADROOM;
5146
+ const corridorFactor = corridorScalingFactor(corridorMargin);
5147
+ return Math.ceil(base * corridorFactor);
5148
+ }
5149
+ function deriveMaxNodes(obstacleCount, corridorMargin) {
5150
+ const base = 4 * obstacleCount * obstacleCount + 4 * obstacleCount + 100;
5151
+ const corridorFactor = corridorScalingFactor(corridorMargin);
5152
+ return Math.ceil(base * GRID_SAFETY_FACTOR * corridorFactor);
5153
+ }
5154
+ function corridorScalingFactor(corridorMargin) {
5155
+ return 1 + corridorMargin / CORRIDOR_SCALING_BASE * CORRIDOR_SCALING_K;
5156
+ }
5157
+ function resolveBudget(override, adaptive, min, max) {
5158
+ const chosen = override !== void 0 && Number.isFinite(override) && override >= 1 ? override : adaptive;
5159
+ return clamp(chosen, min, max);
5160
+ }
5161
+ function clamp(value, min, max) {
5162
+ return Math.max(min, Math.min(max, value));
5163
+ }
5164
+
5097
5165
  // src/routing/visibility-router.ts
5098
5166
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
5099
5167
  const margin = options.margin ?? 0;
@@ -5107,7 +5175,12 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
5107
5175
  severity: "warning",
5108
5176
  code: "routing.visibility.corner_overflow",
5109
5177
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
5110
- detail: { vertexCount: vertices.length, maxCorners }
5178
+ detail: {
5179
+ vertexCount: vertices.length,
5180
+ maxCorners,
5181
+ obstacleCount: obstacles.length,
5182
+ ...options.corridorMargin === void 0 ? {} : { corridorMargin: options.corridorMargin }
5183
+ }
5111
5184
  });
5112
5185
  return null;
5113
5186
  }
@@ -5381,7 +5454,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5381
5454
  }
5382
5455
 
5383
5456
  // src/routing/routes.ts
5384
- function checkBacktracking(points, source, target, diagnostics) {
5457
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5385
5458
  if (points.length < 2) return;
5386
5459
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5387
5460
  if (direct <= 0) return;
@@ -5391,7 +5464,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5391
5464
  const b = points[i + 1];
5392
5465
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5393
5466
  }
5394
- const threshold = 10;
5467
+ const threshold = maxRatio ?? 20;
5395
5468
  if (routeLen > direct * threshold) {
5396
5469
  diagnostics.push({
5397
5470
  severity: "warning",
@@ -5409,8 +5482,20 @@ function routeEdge(input) {
5409
5482
  const diagnostics = [];
5410
5483
  const softObstacles = input.obstacles ?? [];
5411
5484
  const hardObstacles = input.hardObstacles ?? [];
5485
+ let bestRejectedPath;
5486
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5412
5487
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5413
5488
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
5489
+ const recordRejected = (candidate) => {
5490
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
5491
+ return;
5492
+ }
5493
+ const crossings = countObstacleCrossings(candidate, softObstacles);
5494
+ if (crossings < bestRejectedCrossings) {
5495
+ bestRejectedCrossings = crossings;
5496
+ bestRejectedPath = candidate;
5497
+ }
5498
+ };
5414
5499
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5415
5500
  const defaultAnchors = defaultAnchorsForGeometry(
5416
5501
  input.source.box,
@@ -5470,20 +5555,32 @@ function routeEdge(input) {
5470
5555
  targetAnchor
5471
5556
  );
5472
5557
  const allObstacles = [...softObstacles, ...hardObstacles];
5558
+ const corridorMargin = input.corridorMargin ?? 32;
5473
5559
  const corridorObstacles = filterObstaclesByCorridor(
5474
5560
  source,
5475
5561
  target,
5476
5562
  allObstacles,
5477
5563
  [],
5478
5564
  // endpointObstacles passed separately via options
5479
- 32
5565
+ corridorMargin
5480
5566
  );
5481
5567
  const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5568
+ const budget = computeRoutingBudget(
5569
+ cornerObstacles,
5570
+ allObstacles,
5571
+ corridorMargin,
5572
+ { maxCorners: input.maxCorners, maxNodes: input.maxNodes }
5573
+ );
5482
5574
  let cornerPath = findCornerGraphPath(
5483
5575
  source,
5484
5576
  target,
5485
5577
  cornerObstacles,
5486
- { endpointObstacles, margin: 2 },
5578
+ {
5579
+ endpointObstacles,
5580
+ margin: 2,
5581
+ maxCorners: budget.maxCorners,
5582
+ corridorMargin
5583
+ },
5487
5584
  diagnostics
5488
5585
  );
5489
5586
  if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
@@ -5491,7 +5588,12 @@ function routeEdge(input) {
5491
5588
  source,
5492
5589
  target,
5493
5590
  allObstacles,
5494
- { endpointObstacles, margin: 2 },
5591
+ {
5592
+ endpointObstacles,
5593
+ margin: 2,
5594
+ maxCorners: budget.maxCorners,
5595
+ corridorMargin
5596
+ },
5495
5597
  diagnostics
5496
5598
  );
5497
5599
  }
@@ -5499,7 +5601,12 @@ function routeEdge(input) {
5499
5601
  source,
5500
5602
  target,
5501
5603
  allObstacles,
5502
- { endpointObstacles, margin: 0 },
5604
+ {
5605
+ endpointObstacles,
5606
+ margin: 0,
5607
+ corridorMargin,
5608
+ maxNodes: budget.maxNodes
5609
+ },
5503
5610
  diagnostics
5504
5611
  );
5505
5612
  if (path !== null && path.length >= 2) {
@@ -5516,15 +5623,27 @@ function routeEdge(input) {
5516
5623
  softObstacles,
5517
5624
  softObstacleIndex
5518
5625
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5519
- checkBacktracking(finalized, source, target, diagnostics);
5626
+ checkBacktracking(
5627
+ finalized,
5628
+ source,
5629
+ target,
5630
+ diagnostics,
5631
+ input.maxBacktrackingRatio
5632
+ );
5520
5633
  return { points: finalized, diagnostics };
5521
5634
  }
5635
+ recordRejected(finalized);
5522
5636
  if (cornerPath !== null) {
5523
5637
  const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5524
5638
  source,
5525
5639
  target,
5526
5640
  allObstacles,
5527
- { endpointObstacles, margin: 2 },
5641
+ {
5642
+ endpointObstacles,
5643
+ margin: 2,
5644
+ maxCorners: budget.maxCorners,
5645
+ corridorMargin
5646
+ },
5528
5647
  diagnostics
5529
5648
  ) : null;
5530
5649
  if (fullCornerPath !== null && fullCornerPath.length >= 2) {
@@ -5545,15 +5664,27 @@ function routeEdge(input) {
5545
5664
  hardObstacles,
5546
5665
  hardObstacleIndex
5547
5666
  )) {
5548
- checkBacktracking(fullFinalized, source, target, diagnostics);
5667
+ checkBacktracking(
5668
+ fullFinalized,
5669
+ source,
5670
+ target,
5671
+ diagnostics,
5672
+ input.maxBacktrackingRatio
5673
+ );
5549
5674
  return { points: fullFinalized, diagnostics };
5550
5675
  }
5676
+ recordRejected(fullFinalized);
5551
5677
  }
5552
5678
  const gridPath = findObstacleFreePath(
5553
5679
  source,
5554
5680
  target,
5555
5681
  allObstacles,
5556
- { endpointObstacles, margin: 0 },
5682
+ {
5683
+ endpointObstacles,
5684
+ margin: 0,
5685
+ corridorMargin,
5686
+ maxNodes: budget.maxNodes
5687
+ },
5557
5688
  diagnostics
5558
5689
  );
5559
5690
  if (gridPath !== null && gridPath.length >= 2) {
@@ -5574,9 +5705,16 @@ function routeEdge(input) {
5574
5705
  hardObstacles,
5575
5706
  hardObstacleIndex
5576
5707
  )) {
5577
- checkBacktracking(gridFinalized, source, target, diagnostics);
5708
+ checkBacktracking(
5709
+ gridFinalized,
5710
+ source,
5711
+ target,
5712
+ diagnostics,
5713
+ input.maxBacktrackingRatio
5714
+ );
5578
5715
  return { points: gridFinalized, diagnostics };
5579
5716
  }
5717
+ recordRejected(gridFinalized);
5580
5718
  }
5581
5719
  }
5582
5720
  }
@@ -5640,7 +5778,8 @@ function routeEdge(input) {
5640
5778
  finalizedClean,
5641
5779
  candidate.points[0],
5642
5780
  candidate.points[candidate.points.length - 1],
5643
- diagnostics
5781
+ diagnostics,
5782
+ input.maxBacktrackingRatio
5644
5783
  );
5645
5784
  return { points: finalizedClean, diagnostics };
5646
5785
  }
@@ -5706,13 +5845,41 @@ function routeEdge(input) {
5706
5845
  code: "routing.obstacle.unavoidable",
5707
5846
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5708
5847
  });
5709
- return {
5710
- points: finalizeRoute(
5711
- bestPoints2,
5848
+ const finalizedSoftBest = finalizeRoute(
5849
+ bestPoints2,
5850
+ softObstacles,
5851
+ hardObstacles,
5852
+ diagnostics
5853
+ );
5854
+ let softFallback = finalizedSoftBest;
5855
+ if (bestRejectedPath !== void 0) {
5856
+ const finalizedRejected = finalizeRoute(
5857
+ bestRejectedPath,
5712
5858
  softObstacles,
5713
5859
  hardObstacles,
5714
5860
  diagnostics
5715
- ),
5861
+ );
5862
+ const rejectedCrossings = countObstacleCrossings(
5863
+ finalizedRejected,
5864
+ softObstacles
5865
+ );
5866
+ const heuristicCrossings = countObstacleCrossings(
5867
+ finalizedSoftBest,
5868
+ softObstacles
5869
+ );
5870
+ if (rejectedCrossings < heuristicCrossings) {
5871
+ softFallback = finalizedRejected;
5872
+ }
5873
+ }
5874
+ checkBacktracking(
5875
+ softFallback,
5876
+ softFallback[0],
5877
+ softFallback[softFallback.length - 1],
5878
+ diagnostics,
5879
+ input.maxBacktrackingRatio
5880
+ );
5881
+ return {
5882
+ points: softFallback,
5716
5883
  diagnostics
5717
5884
  };
5718
5885
  }
@@ -5744,6 +5911,22 @@ function routeEdge(input) {
5744
5911
  maxAttempts
5745
5912
  );
5746
5913
  }
5914
+ if (bestRejectedPath !== void 0) {
5915
+ diagnostics.push({
5916
+ severity: "warning",
5917
+ code: "routing.obstacle.unavoidable",
5918
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5919
+ });
5920
+ return {
5921
+ points: finalizeRoute(
5922
+ bestRejectedPath,
5923
+ softObstacles,
5924
+ hardObstacles,
5925
+ diagnostics
5926
+ ),
5927
+ diagnostics
5928
+ };
5929
+ }
5747
5930
  diagnostics.push({
5748
5931
  severity: "error",
5749
5932
  code: "routing.evidence.crossing_forbidden",
@@ -5791,13 +5974,41 @@ function routeEdge(input) {
5791
5974
  code: "routing.obstacle.unavoidable",
5792
5975
  message: "No bounded orthogonal route candidate avoided all obstacles."
5793
5976
  });
5794
- return {
5795
- points: finalizeRoute(
5796
- bestPoints,
5977
+ const finalizedBestPoints = finalizeRoute(
5978
+ bestPoints,
5979
+ softObstacles,
5980
+ hardObstacles,
5981
+ diagnostics
5982
+ );
5983
+ let fallbackPoints = finalizedBestPoints;
5984
+ if (bestRejectedPath !== void 0) {
5985
+ const finalizedRejected = finalizeRoute(
5986
+ bestRejectedPath,
5797
5987
  softObstacles,
5798
5988
  hardObstacles,
5799
5989
  diagnostics
5800
- ),
5990
+ );
5991
+ const rejectedCrossings = countObstacleCrossings(
5992
+ finalizedRejected,
5993
+ softObstacles
5994
+ );
5995
+ const heuristicCrossings = countObstacleCrossings(
5996
+ finalizedBestPoints,
5997
+ softObstacles
5998
+ );
5999
+ if (rejectedCrossings < heuristicCrossings) {
6000
+ fallbackPoints = finalizedRejected;
6001
+ }
6002
+ }
6003
+ checkBacktracking(
6004
+ fallbackPoints,
6005
+ fallbackPoints[0],
6006
+ fallbackPoints[fallbackPoints.length - 1],
6007
+ diagnostics,
6008
+ input.maxBacktrackingRatio
6009
+ );
6010
+ return {
6011
+ points: fallbackPoints,
5801
6012
  diagnostics
5802
6013
  };
5803
6014
  }
@@ -6296,6 +6507,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6296
6507
  }
6297
6508
  return false;
6298
6509
  }
6510
+ function countObstacleCrossings(points, obstacles) {
6511
+ let count = 0;
6512
+ for (const obstacle of obstacles) {
6513
+ validateBox(obstacle);
6514
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
6515
+ const a = points[pointIndex];
6516
+ const b = points[pointIndex + 1];
6517
+ if (a === void 0 || b === void 0) {
6518
+ continue;
6519
+ }
6520
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
6521
+ count += 1;
6522
+ break;
6523
+ }
6524
+ }
6525
+ }
6526
+ return count;
6527
+ }
6299
6528
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6300
6529
  for (let index = 0; index < points.length - 1; index += 1) {
6301
6530
  const a = points[index];
@@ -6741,7 +6970,8 @@ function solveDiagram(diagram, options = {}) {
6741
6970
  constrained.boxes,
6742
6971
  constrained.locks,
6743
6972
  options?.overlapSpacing ?? 40,
6744
- Math.max(0, options?.minLaneGutter ?? 0)
6973
+ Math.max(0, options?.minLaneGutter ?? 0),
6974
+ options.distributeContainedChildren ?? true
6745
6975
  );
6746
6976
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6747
6977
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6908,7 +7138,8 @@ function solveDiagram(diagram, options = {}) {
6908
7138
  diagram.direction,
6909
7139
  options,
6910
7140
  diagnostics,
6911
- coordinatedGroups
7141
+ coordinatedGroups,
7142
+ contentBounds
6912
7143
  );
6913
7144
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6914
7145
  coordinatedEdges,
@@ -7330,7 +7561,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7330
7561
  function containsCjk(value) {
7331
7562
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7332
7563
  }
7333
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7564
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7334
7565
  const layouts = /* @__PURE__ */ new Map();
7335
7566
  const diagnostics = [];
7336
7567
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7349,7 +7580,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7349
7580
  locks,
7350
7581
  diagnostics,
7351
7582
  movedChildIds,
7352
- laneGutter
7583
+ laneGutter,
7584
+ constraints,
7585
+ distributeContainedChildren
7353
7586
  );
7354
7587
  if (layout2 !== void 0) {
7355
7588
  layouts.set(swimlane.id, layout2);
@@ -7541,7 +7774,7 @@ function isStackRunaway(boxes, nodes, direction, options) {
7541
7774
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7542
7775
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7543
7776
  }
7544
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7777
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7545
7778
  const headerHeight = swimlane.headerHeight ?? 28;
7546
7779
  const padding = swimlane.padding ?? 16;
7547
7780
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7566,7 +7799,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7566
7799
  locks,
7567
7800
  diagnostics,
7568
7801
  movedChildIds,
7569
- laneGutter
7802
+ laneGutter,
7803
+ constraints,
7804
+ distributeContainedChildren
7570
7805
  );
7571
7806
  }
7572
7807
  return applyHorizontalSwimlaneContract(
@@ -7581,13 +7816,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7581
7816
  laneGutter
7582
7817
  );
7583
7818
  }
7584
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
7819
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7585
7820
  const populatedBounds = laneBounds.filter(
7586
7821
  (box) => box !== void 0
7587
7822
  );
7588
7823
  const top = Math.min(...populatedBounds.map((box) => box.y));
7589
7824
  const left = Math.min(...populatedBounds.map((box) => box.x));
7590
7825
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
7826
+ const containedChildIds = /* @__PURE__ */ new Set();
7827
+ if (distributeContainedChildren) {
7828
+ for (const c of constraints) {
7829
+ if (c.kind !== "containment") continue;
7830
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
7831
+ const distributable = c.childIds.filter((childId) => {
7832
+ if (nodeBoxes.get(childId) === void 0) return false;
7833
+ const lock = locks.get(childId);
7834
+ return lock === void 0 || lock.source === "fixed-position";
7835
+ });
7836
+ if (distributable.length < 2) continue;
7837
+ for (const childId of distributable) {
7838
+ containedChildIds.add(childId);
7839
+ }
7840
+ }
7841
+ }
7591
7842
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7592
7843
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7593
7844
  const rankStackGap = Math.max(8, padding / 2);
@@ -7604,7 +7855,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7604
7855
  nodeBoxes,
7605
7856
  flowRanks,
7606
7857
  locks,
7607
- rankStackGap
7858
+ rankStackGap,
7859
+ containedChildIds
7608
7860
  );
7609
7861
  const slotWidth = Math.max(
7610
7862
  Math.max(...populatedBounds.map((box) => box.width)),
@@ -7626,7 +7878,10 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7626
7878
  const distributable = lane.children.filter(
7627
7879
  (childId) => !locks.has(childId)
7628
7880
  );
7629
- if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7881
+ const coveredByContainment = lane.children.some(
7882
+ (childId) => containedChildIds.has(childId)
7883
+ );
7884
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7630
7885
  moveRankedVerticalLaneChildren(
7631
7886
  lane.children,
7632
7887
  nodeBoxes,
@@ -7654,6 +7909,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7654
7909
  );
7655
7910
  continue;
7656
7911
  }
7912
+ const rankedCoveredByContainment = lane.children.some(
7913
+ (childId) => containedChildIds.has(childId)
7914
+ );
7657
7915
  moveRankedVerticalLaneChildren(
7658
7916
  lane.children,
7659
7917
  nodeBoxes,
@@ -7664,7 +7922,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7664
7922
  rankSpacing,
7665
7923
  rankStackGap,
7666
7924
  { x: target.x, y: laneContentTop },
7667
- slotWidth - padding * 2
7925
+ slotWidth - padding * 2,
7926
+ rankedCoveredByContainment
7668
7927
  );
7669
7928
  }
7670
7929
  return {
@@ -7780,9 +8039,12 @@ function crossAxisSpreadWidth(items, gap) {
7780
8039
  0
7781
8040
  );
7782
8041
  }
7783
- function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
8042
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7784
8043
  let maxWidth = 0;
7785
8044
  for (const lane of swimlane.lanes) {
8045
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
8046
+ continue;
8047
+ }
7786
8048
  for (const stack of rankStacks(
7787
8049
  lane.children,
7788
8050
  nodeBoxes,
@@ -7795,7 +8057,7 @@ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7795
8057
  }
7796
8058
  return maxWidth;
7797
8059
  }
7798
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
8060
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7799
8061
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7800
8062
  const unlocked = [];
7801
8063
  for (const item of stack) {
@@ -7824,7 +8086,7 @@ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics,
7824
8086
  }
7825
8087
  nodeBoxes.set(childId, next);
7826
8088
  } else {
7827
- const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
8089
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7828
8090
  if (!shouldSpread) {
7829
8091
  let yOffset = 0;
7830
8092
  for (const { childId, box } of unlocked) {
@@ -8953,14 +9215,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8953
9215
  }
8954
9216
  };
8955
9217
  }
8956
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
9218
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8957
9219
  const coordinated = [];
8958
9220
  const coordinatedNodeById = new Map(
8959
9221
  coordinatedNodes.map((node) => [node.id, node])
8960
9222
  );
9223
+ const corridorMarginOption = options.corridorMargin ?? "auto";
9224
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
9225
+ 200,
9226
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
9227
+ );
9228
+ const routingGutter = options.routingGutter ?? 160;
9229
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8961
9230
  const nodeObstacleIndex = createBoxSpatialIndex(
8962
9231
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8963
- options.routingGutter ?? 160
9232
+ queryGutter
8964
9233
  );
8965
9234
  for (const edge of edges) {
8966
9235
  const source = nodes.get(edge.source.nodeId);
@@ -8982,11 +9251,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8982
9251
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8983
9252
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8984
9253
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8985
- const corridor = edgeCorridorBox(
8986
- source.box,
8987
- target.box,
8988
- options.routingGutter ?? 160
8989
- );
9254
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8990
9255
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8991
9256
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8992
9257
  );
@@ -9004,7 +9269,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
9004
9269
  ...routeTextObstacles
9005
9270
  ],
9006
9271
  hardObstacles,
9007
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
9272
+ corridorMargin,
9273
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
9274
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
9008
9275
  });
9009
9276
  diagnostics.push(
9010
9277
  ...route.diagnostics.map((diagnostic) => ({