@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.cjs CHANGED
@@ -4808,7 +4808,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4808
4808
  detail: {
4809
4809
  xsCount: xs.length,
4810
4810
  ysCount: ys.length,
4811
- maxNodes
4811
+ maxNodes,
4812
+ obstacleCount: obstacles.length,
4813
+ stage: "corridor-filtered",
4814
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4812
4815
  }
4813
4816
  });
4814
4817
  return null;
@@ -4854,7 +4857,14 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4854
4857
  severity: "warning",
4855
4858
  code: "routing.astar.grid_overflow",
4856
4859
  message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4857
- detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4860
+ detail: {
4861
+ xsCount: xsFull.length,
4862
+ ysCount: ysFull.length,
4863
+ maxNodes,
4864
+ obstacleCount: obstacles.length,
4865
+ stage: "full-retry",
4866
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4867
+ }
4858
4868
  });
4859
4869
  return null;
4860
4870
  }
@@ -5097,6 +5107,64 @@ function areCollinear(a, b, c) {
5097
5107
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
5098
5108
  }
5099
5109
 
5110
+ // src/routing/budget.ts
5111
+ var MIN_CORNER_BUDGET = 600;
5112
+ var MAX_CORNER_BUDGET = 3e3;
5113
+ var MIN_NODE_BUDGET = 4e3;
5114
+ var MAX_NODE_BUDGET = 64e3;
5115
+ var CORNERS_PER_OBSTACLE = 12;
5116
+ var CORNER_HEADROOM = 2;
5117
+ var GRID_SAFETY_FACTOR = 3;
5118
+ var CORRIDOR_SCALING_K = 0.5;
5119
+ var CORRIDOR_SCALING_BASE = 200;
5120
+ function computeRoutingBudget(cornerObstacles, allObstacles, corridorMargin, overrides = {}) {
5121
+ const adaptiveMaxCorners = deriveMaxCorners(
5122
+ cornerObstacles.length,
5123
+ corridorMargin
5124
+ );
5125
+ const adaptiveMaxNodes = deriveMaxNodes(
5126
+ allObstacles.length,
5127
+ corridorMargin
5128
+ );
5129
+ return {
5130
+ maxCorners: resolveBudget(
5131
+ overrides.maxCorners,
5132
+ adaptiveMaxCorners,
5133
+ MIN_CORNER_BUDGET,
5134
+ MAX_CORNER_BUDGET
5135
+ ),
5136
+ maxNodes: resolveBudget(
5137
+ overrides.maxNodes,
5138
+ adaptiveMaxNodes,
5139
+ MIN_NODE_BUDGET,
5140
+ MAX_NODE_BUDGET
5141
+ ),
5142
+ cornerObstacleCount: cornerObstacles.length,
5143
+ gridObstacleCount: allObstacles.length,
5144
+ corridorMargin
5145
+ };
5146
+ }
5147
+ function deriveMaxCorners(obstacleCount, corridorMargin) {
5148
+ const base = 2 + obstacleCount * CORNERS_PER_OBSTACLE * CORNER_HEADROOM;
5149
+ const corridorFactor = corridorScalingFactor(corridorMargin);
5150
+ return Math.ceil(base * corridorFactor);
5151
+ }
5152
+ function deriveMaxNodes(obstacleCount, corridorMargin) {
5153
+ const base = 4 * obstacleCount * obstacleCount + 4 * obstacleCount + 100;
5154
+ const corridorFactor = corridorScalingFactor(corridorMargin);
5155
+ return Math.ceil(base * GRID_SAFETY_FACTOR * corridorFactor);
5156
+ }
5157
+ function corridorScalingFactor(corridorMargin) {
5158
+ return 1 + corridorMargin / CORRIDOR_SCALING_BASE * CORRIDOR_SCALING_K;
5159
+ }
5160
+ function resolveBudget(override, adaptive, min, max) {
5161
+ const chosen = override !== void 0 && Number.isFinite(override) && override >= 1 ? override : adaptive;
5162
+ return clamp(chosen, min, max);
5163
+ }
5164
+ function clamp(value, min, max) {
5165
+ return Math.max(min, Math.min(max, value));
5166
+ }
5167
+
5100
5168
  // src/routing/visibility-router.ts
5101
5169
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
5102
5170
  const margin = options.margin ?? 0;
@@ -5110,7 +5178,12 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
5110
5178
  severity: "warning",
5111
5179
  code: "routing.visibility.corner_overflow",
5112
5180
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
5113
- detail: { vertexCount: vertices.length, maxCorners }
5181
+ detail: {
5182
+ vertexCount: vertices.length,
5183
+ maxCorners,
5184
+ obstacleCount: obstacles.length,
5185
+ ...options.corridorMargin === void 0 ? {} : { corridorMargin: options.corridorMargin }
5186
+ }
5114
5187
  });
5115
5188
  return null;
5116
5189
  }
@@ -5384,7 +5457,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5384
5457
  }
5385
5458
 
5386
5459
  // src/routing/routes.ts
5387
- function checkBacktracking(points, source, target, diagnostics) {
5460
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5388
5461
  if (points.length < 2) return;
5389
5462
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5390
5463
  if (direct <= 0) return;
@@ -5394,7 +5467,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5394
5467
  const b = points[i + 1];
5395
5468
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5396
5469
  }
5397
- const threshold = 10;
5470
+ const threshold = maxRatio ?? 20;
5398
5471
  if (routeLen > direct * threshold) {
5399
5472
  diagnostics.push({
5400
5473
  severity: "warning",
@@ -5412,8 +5485,20 @@ function routeEdge(input) {
5412
5485
  const diagnostics = [];
5413
5486
  const softObstacles = input.obstacles ?? [];
5414
5487
  const hardObstacles = input.hardObstacles ?? [];
5488
+ let bestRejectedPath;
5489
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5415
5490
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5416
5491
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
5492
+ const recordRejected = (candidate) => {
5493
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
5494
+ return;
5495
+ }
5496
+ const crossings = countObstacleCrossings(candidate, softObstacles);
5497
+ if (crossings < bestRejectedCrossings) {
5498
+ bestRejectedCrossings = crossings;
5499
+ bestRejectedPath = candidate;
5500
+ }
5501
+ };
5417
5502
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5418
5503
  const defaultAnchors = defaultAnchorsForGeometry(
5419
5504
  input.source.box,
@@ -5473,20 +5558,32 @@ function routeEdge(input) {
5473
5558
  targetAnchor
5474
5559
  );
5475
5560
  const allObstacles = [...softObstacles, ...hardObstacles];
5561
+ const corridorMargin = input.corridorMargin ?? 32;
5476
5562
  const corridorObstacles = filterObstaclesByCorridor(
5477
5563
  source,
5478
5564
  target,
5479
5565
  allObstacles,
5480
5566
  [],
5481
5567
  // endpointObstacles passed separately via options
5482
- 32
5568
+ corridorMargin
5483
5569
  );
5484
5570
  const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5571
+ const budget = computeRoutingBudget(
5572
+ cornerObstacles,
5573
+ allObstacles,
5574
+ corridorMargin,
5575
+ { maxCorners: input.maxCorners, maxNodes: input.maxNodes }
5576
+ );
5485
5577
  let cornerPath = findCornerGraphPath(
5486
5578
  source,
5487
5579
  target,
5488
5580
  cornerObstacles,
5489
- { endpointObstacles, margin: 2 },
5581
+ {
5582
+ endpointObstacles,
5583
+ margin: 2,
5584
+ maxCorners: budget.maxCorners,
5585
+ corridorMargin
5586
+ },
5490
5587
  diagnostics
5491
5588
  );
5492
5589
  if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
@@ -5494,7 +5591,12 @@ function routeEdge(input) {
5494
5591
  source,
5495
5592
  target,
5496
5593
  allObstacles,
5497
- { endpointObstacles, margin: 2 },
5594
+ {
5595
+ endpointObstacles,
5596
+ margin: 2,
5597
+ maxCorners: budget.maxCorners,
5598
+ corridorMargin
5599
+ },
5498
5600
  diagnostics
5499
5601
  );
5500
5602
  }
@@ -5502,7 +5604,12 @@ function routeEdge(input) {
5502
5604
  source,
5503
5605
  target,
5504
5606
  allObstacles,
5505
- { endpointObstacles, margin: 0 },
5607
+ {
5608
+ endpointObstacles,
5609
+ margin: 0,
5610
+ corridorMargin,
5611
+ maxNodes: budget.maxNodes
5612
+ },
5506
5613
  diagnostics
5507
5614
  );
5508
5615
  if (path !== null && path.length >= 2) {
@@ -5519,15 +5626,27 @@ function routeEdge(input) {
5519
5626
  softObstacles,
5520
5627
  softObstacleIndex
5521
5628
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5522
- checkBacktracking(finalized, source, target, diagnostics);
5629
+ checkBacktracking(
5630
+ finalized,
5631
+ source,
5632
+ target,
5633
+ diagnostics,
5634
+ input.maxBacktrackingRatio
5635
+ );
5523
5636
  return { points: finalized, diagnostics };
5524
5637
  }
5638
+ recordRejected(finalized);
5525
5639
  if (cornerPath !== null) {
5526
5640
  const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5527
5641
  source,
5528
5642
  target,
5529
5643
  allObstacles,
5530
- { endpointObstacles, margin: 2 },
5644
+ {
5645
+ endpointObstacles,
5646
+ margin: 2,
5647
+ maxCorners: budget.maxCorners,
5648
+ corridorMargin
5649
+ },
5531
5650
  diagnostics
5532
5651
  ) : null;
5533
5652
  if (fullCornerPath !== null && fullCornerPath.length >= 2) {
@@ -5548,15 +5667,27 @@ function routeEdge(input) {
5548
5667
  hardObstacles,
5549
5668
  hardObstacleIndex
5550
5669
  )) {
5551
- checkBacktracking(fullFinalized, source, target, diagnostics);
5670
+ checkBacktracking(
5671
+ fullFinalized,
5672
+ source,
5673
+ target,
5674
+ diagnostics,
5675
+ input.maxBacktrackingRatio
5676
+ );
5552
5677
  return { points: fullFinalized, diagnostics };
5553
5678
  }
5679
+ recordRejected(fullFinalized);
5554
5680
  }
5555
5681
  const gridPath = findObstacleFreePath(
5556
5682
  source,
5557
5683
  target,
5558
5684
  allObstacles,
5559
- { endpointObstacles, margin: 0 },
5685
+ {
5686
+ endpointObstacles,
5687
+ margin: 0,
5688
+ corridorMargin,
5689
+ maxNodes: budget.maxNodes
5690
+ },
5560
5691
  diagnostics
5561
5692
  );
5562
5693
  if (gridPath !== null && gridPath.length >= 2) {
@@ -5577,9 +5708,16 @@ function routeEdge(input) {
5577
5708
  hardObstacles,
5578
5709
  hardObstacleIndex
5579
5710
  )) {
5580
- checkBacktracking(gridFinalized, source, target, diagnostics);
5711
+ checkBacktracking(
5712
+ gridFinalized,
5713
+ source,
5714
+ target,
5715
+ diagnostics,
5716
+ input.maxBacktrackingRatio
5717
+ );
5581
5718
  return { points: gridFinalized, diagnostics };
5582
5719
  }
5720
+ recordRejected(gridFinalized);
5583
5721
  }
5584
5722
  }
5585
5723
  }
@@ -5643,7 +5781,8 @@ function routeEdge(input) {
5643
5781
  finalizedClean,
5644
5782
  candidate.points[0],
5645
5783
  candidate.points[candidate.points.length - 1],
5646
- diagnostics
5784
+ diagnostics,
5785
+ input.maxBacktrackingRatio
5647
5786
  );
5648
5787
  return { points: finalizedClean, diagnostics };
5649
5788
  }
@@ -5709,13 +5848,41 @@ function routeEdge(input) {
5709
5848
  code: "routing.obstacle.unavoidable",
5710
5849
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5711
5850
  });
5712
- return {
5713
- points: finalizeRoute(
5714
- bestPoints2,
5851
+ const finalizedSoftBest = finalizeRoute(
5852
+ bestPoints2,
5853
+ softObstacles,
5854
+ hardObstacles,
5855
+ diagnostics
5856
+ );
5857
+ let softFallback = finalizedSoftBest;
5858
+ if (bestRejectedPath !== void 0) {
5859
+ const finalizedRejected = finalizeRoute(
5860
+ bestRejectedPath,
5715
5861
  softObstacles,
5716
5862
  hardObstacles,
5717
5863
  diagnostics
5718
- ),
5864
+ );
5865
+ const rejectedCrossings = countObstacleCrossings(
5866
+ finalizedRejected,
5867
+ softObstacles
5868
+ );
5869
+ const heuristicCrossings = countObstacleCrossings(
5870
+ finalizedSoftBest,
5871
+ softObstacles
5872
+ );
5873
+ if (rejectedCrossings < heuristicCrossings) {
5874
+ softFallback = finalizedRejected;
5875
+ }
5876
+ }
5877
+ checkBacktracking(
5878
+ softFallback,
5879
+ softFallback[0],
5880
+ softFallback[softFallback.length - 1],
5881
+ diagnostics,
5882
+ input.maxBacktrackingRatio
5883
+ );
5884
+ return {
5885
+ points: softFallback,
5719
5886
  diagnostics
5720
5887
  };
5721
5888
  }
@@ -5747,6 +5914,22 @@ function routeEdge(input) {
5747
5914
  maxAttempts
5748
5915
  );
5749
5916
  }
5917
+ if (bestRejectedPath !== void 0) {
5918
+ diagnostics.push({
5919
+ severity: "warning",
5920
+ code: "routing.obstacle.unavoidable",
5921
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5922
+ });
5923
+ return {
5924
+ points: finalizeRoute(
5925
+ bestRejectedPath,
5926
+ softObstacles,
5927
+ hardObstacles,
5928
+ diagnostics
5929
+ ),
5930
+ diagnostics
5931
+ };
5932
+ }
5750
5933
  diagnostics.push({
5751
5934
  severity: "error",
5752
5935
  code: "routing.evidence.crossing_forbidden",
@@ -5794,13 +5977,41 @@ function routeEdge(input) {
5794
5977
  code: "routing.obstacle.unavoidable",
5795
5978
  message: "No bounded orthogonal route candidate avoided all obstacles."
5796
5979
  });
5797
- return {
5798
- points: finalizeRoute(
5799
- bestPoints,
5980
+ const finalizedBestPoints = finalizeRoute(
5981
+ bestPoints,
5982
+ softObstacles,
5983
+ hardObstacles,
5984
+ diagnostics
5985
+ );
5986
+ let fallbackPoints = finalizedBestPoints;
5987
+ if (bestRejectedPath !== void 0) {
5988
+ const finalizedRejected = finalizeRoute(
5989
+ bestRejectedPath,
5800
5990
  softObstacles,
5801
5991
  hardObstacles,
5802
5992
  diagnostics
5803
- ),
5993
+ );
5994
+ const rejectedCrossings = countObstacleCrossings(
5995
+ finalizedRejected,
5996
+ softObstacles
5997
+ );
5998
+ const heuristicCrossings = countObstacleCrossings(
5999
+ finalizedBestPoints,
6000
+ softObstacles
6001
+ );
6002
+ if (rejectedCrossings < heuristicCrossings) {
6003
+ fallbackPoints = finalizedRejected;
6004
+ }
6005
+ }
6006
+ checkBacktracking(
6007
+ fallbackPoints,
6008
+ fallbackPoints[0],
6009
+ fallbackPoints[fallbackPoints.length - 1],
6010
+ diagnostics,
6011
+ input.maxBacktrackingRatio
6012
+ );
6013
+ return {
6014
+ points: fallbackPoints,
5804
6015
  diagnostics
5805
6016
  };
5806
6017
  }
@@ -6299,6 +6510,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6299
6510
  }
6300
6511
  return false;
6301
6512
  }
6513
+ function countObstacleCrossings(points, obstacles) {
6514
+ let count = 0;
6515
+ for (const obstacle of obstacles) {
6516
+ validateBox(obstacle);
6517
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
6518
+ const a = points[pointIndex];
6519
+ const b = points[pointIndex + 1];
6520
+ if (a === void 0 || b === void 0) {
6521
+ continue;
6522
+ }
6523
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
6524
+ count += 1;
6525
+ break;
6526
+ }
6527
+ }
6528
+ }
6529
+ return count;
6530
+ }
6302
6531
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6303
6532
  for (let index = 0; index < points.length - 1; index += 1) {
6304
6533
  const a = points[index];
@@ -6744,7 +6973,8 @@ function solveDiagram(diagram, options = {}) {
6744
6973
  constrained.boxes,
6745
6974
  constrained.locks,
6746
6975
  options?.overlapSpacing ?? 40,
6747
- Math.max(0, options?.minLaneGutter ?? 0)
6976
+ Math.max(0, options?.minLaneGutter ?? 0),
6977
+ options.distributeContainedChildren ?? true
6748
6978
  );
6749
6979
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6750
6980
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6911,7 +7141,8 @@ function solveDiagram(diagram, options = {}) {
6911
7141
  diagram.direction,
6912
7142
  options,
6913
7143
  diagnostics,
6914
- coordinatedGroups
7144
+ coordinatedGroups,
7145
+ contentBounds
6915
7146
  );
6916
7147
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6917
7148
  coordinatedEdges,
@@ -7333,7 +7564,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7333
7564
  function containsCjk(value) {
7334
7565
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7335
7566
  }
7336
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7567
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7337
7568
  const layouts = /* @__PURE__ */ new Map();
7338
7569
  const diagnostics = [];
7339
7570
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7352,7 +7583,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7352
7583
  locks,
7353
7584
  diagnostics,
7354
7585
  movedChildIds,
7355
- laneGutter
7586
+ laneGutter,
7587
+ constraints,
7588
+ distributeContainedChildren
7356
7589
  );
7357
7590
  if (layout2 !== void 0) {
7358
7591
  layouts.set(swimlane.id, layout2);
@@ -7544,7 +7777,7 @@ function isStackRunaway(boxes, nodes, direction, options) {
7544
7777
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7545
7778
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7546
7779
  }
7547
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7780
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7548
7781
  const headerHeight = swimlane.headerHeight ?? 28;
7549
7782
  const padding = swimlane.padding ?? 16;
7550
7783
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7569,7 +7802,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7569
7802
  locks,
7570
7803
  diagnostics,
7571
7804
  movedChildIds,
7572
- laneGutter
7805
+ laneGutter,
7806
+ constraints,
7807
+ distributeContainedChildren
7573
7808
  );
7574
7809
  }
7575
7810
  return applyHorizontalSwimlaneContract(
@@ -7584,13 +7819,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7584
7819
  laneGutter
7585
7820
  );
7586
7821
  }
7587
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
7822
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7588
7823
  const populatedBounds = laneBounds.filter(
7589
7824
  (box) => box !== void 0
7590
7825
  );
7591
7826
  const top = Math.min(...populatedBounds.map((box) => box.y));
7592
7827
  const left = Math.min(...populatedBounds.map((box) => box.x));
7593
7828
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
7829
+ const containedChildIds = /* @__PURE__ */ new Set();
7830
+ if (distributeContainedChildren) {
7831
+ for (const c of constraints) {
7832
+ if (c.kind !== "containment") continue;
7833
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
7834
+ const distributable = c.childIds.filter((childId) => {
7835
+ if (nodeBoxes.get(childId) === void 0) return false;
7836
+ const lock = locks.get(childId);
7837
+ return lock === void 0 || lock.source === "fixed-position";
7838
+ });
7839
+ if (distributable.length < 2) continue;
7840
+ for (const childId of distributable) {
7841
+ containedChildIds.add(childId);
7842
+ }
7843
+ }
7844
+ }
7594
7845
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7595
7846
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7596
7847
  const rankStackGap = Math.max(8, padding / 2);
@@ -7607,7 +7858,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7607
7858
  nodeBoxes,
7608
7859
  flowRanks,
7609
7860
  locks,
7610
- rankStackGap
7861
+ rankStackGap,
7862
+ containedChildIds
7611
7863
  );
7612
7864
  const slotWidth = Math.max(
7613
7865
  Math.max(...populatedBounds.map((box) => box.width)),
@@ -7629,7 +7881,10 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7629
7881
  const distributable = lane.children.filter(
7630
7882
  (childId) => !locks.has(childId)
7631
7883
  );
7632
- if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7884
+ const coveredByContainment = lane.children.some(
7885
+ (childId) => containedChildIds.has(childId)
7886
+ );
7887
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7633
7888
  moveRankedVerticalLaneChildren(
7634
7889
  lane.children,
7635
7890
  nodeBoxes,
@@ -7657,6 +7912,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7657
7912
  );
7658
7913
  continue;
7659
7914
  }
7915
+ const rankedCoveredByContainment = lane.children.some(
7916
+ (childId) => containedChildIds.has(childId)
7917
+ );
7660
7918
  moveRankedVerticalLaneChildren(
7661
7919
  lane.children,
7662
7920
  nodeBoxes,
@@ -7667,7 +7925,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7667
7925
  rankSpacing,
7668
7926
  rankStackGap,
7669
7927
  { x: target.x, y: laneContentTop },
7670
- slotWidth - padding * 2
7928
+ slotWidth - padding * 2,
7929
+ rankedCoveredByContainment
7671
7930
  );
7672
7931
  }
7673
7932
  return {
@@ -7783,9 +8042,12 @@ function crossAxisSpreadWidth(items, gap) {
7783
8042
  0
7784
8043
  );
7785
8044
  }
7786
- function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
8045
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7787
8046
  let maxWidth = 0;
7788
8047
  for (const lane of swimlane.lanes) {
8048
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
8049
+ continue;
8050
+ }
7789
8051
  for (const stack of rankStacks(
7790
8052
  lane.children,
7791
8053
  nodeBoxes,
@@ -7798,7 +8060,7 @@ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7798
8060
  }
7799
8061
  return maxWidth;
7800
8062
  }
7801
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
8063
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7802
8064
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7803
8065
  const unlocked = [];
7804
8066
  for (const item of stack) {
@@ -7827,7 +8089,7 @@ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics,
7827
8089
  }
7828
8090
  nodeBoxes.set(childId, next);
7829
8091
  } else {
7830
- const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
8092
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7831
8093
  if (!shouldSpread) {
7832
8094
  let yOffset = 0;
7833
8095
  for (const { childId, box } of unlocked) {
@@ -8956,14 +9218,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8956
9218
  }
8957
9219
  };
8958
9220
  }
8959
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
9221
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8960
9222
  const coordinated = [];
8961
9223
  const coordinatedNodeById = new Map(
8962
9224
  coordinatedNodes.map((node) => [node.id, node])
8963
9225
  );
9226
+ const corridorMarginOption = options.corridorMargin ?? "auto";
9227
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
9228
+ 200,
9229
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
9230
+ );
9231
+ const routingGutter = options.routingGutter ?? 160;
9232
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8964
9233
  const nodeObstacleIndex = createBoxSpatialIndex(
8965
9234
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8966
- options.routingGutter ?? 160
9235
+ queryGutter
8967
9236
  );
8968
9237
  for (const edge of edges) {
8969
9238
  const source = nodes.get(edge.source.nodeId);
@@ -8985,11 +9254,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8985
9254
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8986
9255
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8987
9256
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8988
- const corridor = edgeCorridorBox(
8989
- source.box,
8990
- target.box,
8991
- options.routingGutter ?? 160
8992
- );
9257
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8993
9258
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8994
9259
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8995
9260
  );
@@ -9007,7 +9272,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
9007
9272
  ...routeTextObstacles
9008
9273
  ],
9009
9274
  hardObstacles,
9010
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
9275
+ corridorMargin,
9276
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
9277
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
9011
9278
  });
9012
9279
  diagnostics.push(
9013
9280
  ...route.diagnostics.map((diagnostic) => ({