@crazyhappyone/auto-graph 0.2.9 → 0.2.11

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.d.cts CHANGED
@@ -770,6 +770,12 @@ interface RouteEdgeInput {
770
770
  hardObstacleIndex?: BoxSpatialIndex;
771
771
  /** Maximum greedy rerouting iterations (default 5). */
772
772
  maxRoutingAttempts?: number;
773
+ /** Corridor expansion margin in px for corner-graph prefilter (default 32).
774
+ * Larger values include more obstacles in the local routing window. */
775
+ corridorMargin?: number;
776
+ /** Route-length / direct-distance ratio above which a backtracking
777
+ * warning is emitted (default 20). */
778
+ maxBacktrackingRatio?: number;
773
779
  }
774
780
  interface RouteEdgeResult {
775
781
  points: Point[];
@@ -832,6 +838,15 @@ interface SolveDiagramOptions {
832
838
  labelPlacement?: "beside" | "on-path";
833
839
  /** Pixels to offset edge labels from the edge path when labelPlacement is "beside". */
834
840
  labelOffset?: number;
841
+ /** Corridor expansion margin for corner-graph prefilter.
842
+ * - number: fixed px margin
843
+ * - "auto" (default): max(200, contentDiagonal * 0.3)
844
+ * Larger margins include more obstacles in the local routing window,
845
+ * improving path quality on dense diagrams at the cost of more vertices. */
846
+ corridorMargin?: number | "auto";
847
+ /** Route-length / direct-distance ratio above which a backtracking
848
+ * warning is emitted (default 20). */
849
+ maxBacktrackingRatio?: number;
835
850
  }
836
851
  interface PortShiftingOptions {
837
852
  enabled?: boolean;
package/dist/index.d.ts CHANGED
@@ -770,6 +770,12 @@ interface RouteEdgeInput {
770
770
  hardObstacleIndex?: BoxSpatialIndex;
771
771
  /** Maximum greedy rerouting iterations (default 5). */
772
772
  maxRoutingAttempts?: number;
773
+ /** Corridor expansion margin in px for corner-graph prefilter (default 32).
774
+ * Larger values include more obstacles in the local routing window. */
775
+ corridorMargin?: number;
776
+ /** Route-length / direct-distance ratio above which a backtracking
777
+ * warning is emitted (default 20). */
778
+ maxBacktrackingRatio?: number;
773
779
  }
774
780
  interface RouteEdgeResult {
775
781
  points: Point[];
@@ -832,6 +838,15 @@ interface SolveDiagramOptions {
832
838
  labelPlacement?: "beside" | "on-path";
833
839
  /** Pixels to offset edge labels from the edge path when labelPlacement is "beside". */
834
840
  labelOffset?: number;
841
+ /** Corridor expansion margin for corner-graph prefilter.
842
+ * - number: fixed px margin
843
+ * - "auto" (default): max(200, contentDiagonal * 0.3)
844
+ * Larger margins include more obstacles in the local routing window,
845
+ * improving path quality on dense diagrams at the cost of more vertices. */
846
+ corridorMargin?: number | "auto";
847
+ /** Route-length / direct-distance ratio above which a backtracking
848
+ * warning is emitted (default 20). */
849
+ maxBacktrackingRatio?: number;
835
850
  }
836
851
  interface PortShiftingOptions {
837
852
  enabled?: boolean;
package/dist/index.js CHANGED
@@ -5381,7 +5381,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5381
5381
  }
5382
5382
 
5383
5383
  // src/routing/routes.ts
5384
- function checkBacktracking(points, source, target, diagnostics) {
5384
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5385
5385
  if (points.length < 2) return;
5386
5386
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5387
5387
  if (direct <= 0) return;
@@ -5391,7 +5391,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5391
5391
  const b = points[i + 1];
5392
5392
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5393
5393
  }
5394
- const threshold = 10;
5394
+ const threshold = maxRatio ?? 20;
5395
5395
  if (routeLen > direct * threshold) {
5396
5396
  diagnostics.push({
5397
5397
  severity: "warning",
@@ -5409,8 +5409,20 @@ function routeEdge(input) {
5409
5409
  const diagnostics = [];
5410
5410
  const softObstacles = input.obstacles ?? [];
5411
5411
  const hardObstacles = input.hardObstacles ?? [];
5412
+ let bestRejectedPath;
5413
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5412
5414
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5413
5415
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
5416
+ const recordRejected = (candidate) => {
5417
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
5418
+ return;
5419
+ }
5420
+ const crossings = countObstacleCrossings(candidate, softObstacles);
5421
+ if (crossings < bestRejectedCrossings) {
5422
+ bestRejectedCrossings = crossings;
5423
+ bestRejectedPath = candidate;
5424
+ }
5425
+ };
5414
5426
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5415
5427
  const defaultAnchors = defaultAnchorsForGeometry(
5416
5428
  input.source.box,
@@ -5470,13 +5482,14 @@ function routeEdge(input) {
5470
5482
  targetAnchor
5471
5483
  );
5472
5484
  const allObstacles = [...softObstacles, ...hardObstacles];
5485
+ const corridorMargin = input.corridorMargin ?? 32;
5473
5486
  const corridorObstacles = filterObstaclesByCorridor(
5474
5487
  source,
5475
5488
  target,
5476
5489
  allObstacles,
5477
5490
  [],
5478
5491
  // endpointObstacles passed separately via options
5479
- 32
5492
+ corridorMargin
5480
5493
  );
5481
5494
  const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5482
5495
  let cornerPath = findCornerGraphPath(
@@ -5499,7 +5512,7 @@ function routeEdge(input) {
5499
5512
  source,
5500
5513
  target,
5501
5514
  allObstacles,
5502
- { endpointObstacles, margin: 0 },
5515
+ { endpointObstacles, margin: 0, corridorMargin },
5503
5516
  diagnostics
5504
5517
  );
5505
5518
  if (path !== null && path.length >= 2) {
@@ -5516,9 +5529,16 @@ function routeEdge(input) {
5516
5529
  softObstacles,
5517
5530
  softObstacleIndex
5518
5531
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5519
- checkBacktracking(finalized, source, target, diagnostics);
5532
+ checkBacktracking(
5533
+ finalized,
5534
+ source,
5535
+ target,
5536
+ diagnostics,
5537
+ input.maxBacktrackingRatio
5538
+ );
5520
5539
  return { points: finalized, diagnostics };
5521
5540
  }
5541
+ recordRejected(finalized);
5522
5542
  if (cornerPath !== null) {
5523
5543
  const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5524
5544
  source,
@@ -5545,15 +5565,22 @@ function routeEdge(input) {
5545
5565
  hardObstacles,
5546
5566
  hardObstacleIndex
5547
5567
  )) {
5548
- checkBacktracking(fullFinalized, source, target, diagnostics);
5568
+ checkBacktracking(
5569
+ fullFinalized,
5570
+ source,
5571
+ target,
5572
+ diagnostics,
5573
+ input.maxBacktrackingRatio
5574
+ );
5549
5575
  return { points: fullFinalized, diagnostics };
5550
5576
  }
5577
+ recordRejected(fullFinalized);
5551
5578
  }
5552
5579
  const gridPath = findObstacleFreePath(
5553
5580
  source,
5554
5581
  target,
5555
5582
  allObstacles,
5556
- { endpointObstacles, margin: 0 },
5583
+ { endpointObstacles, margin: 0, corridorMargin },
5557
5584
  diagnostics
5558
5585
  );
5559
5586
  if (gridPath !== null && gridPath.length >= 2) {
@@ -5574,9 +5601,16 @@ function routeEdge(input) {
5574
5601
  hardObstacles,
5575
5602
  hardObstacleIndex
5576
5603
  )) {
5577
- checkBacktracking(gridFinalized, source, target, diagnostics);
5604
+ checkBacktracking(
5605
+ gridFinalized,
5606
+ source,
5607
+ target,
5608
+ diagnostics,
5609
+ input.maxBacktrackingRatio
5610
+ );
5578
5611
  return { points: gridFinalized, diagnostics };
5579
5612
  }
5613
+ recordRejected(gridFinalized);
5580
5614
  }
5581
5615
  }
5582
5616
  }
@@ -5640,7 +5674,8 @@ function routeEdge(input) {
5640
5674
  finalizedClean,
5641
5675
  candidate.points[0],
5642
5676
  candidate.points[candidate.points.length - 1],
5643
- diagnostics
5677
+ diagnostics,
5678
+ input.maxBacktrackingRatio
5644
5679
  );
5645
5680
  return { points: finalizedClean, diagnostics };
5646
5681
  }
@@ -5706,13 +5741,41 @@ function routeEdge(input) {
5706
5741
  code: "routing.obstacle.unavoidable",
5707
5742
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5708
5743
  });
5709
- return {
5710
- points: finalizeRoute(
5711
- bestPoints2,
5744
+ const finalizedSoftBest = finalizeRoute(
5745
+ bestPoints2,
5746
+ softObstacles,
5747
+ hardObstacles,
5748
+ diagnostics
5749
+ );
5750
+ let softFallback = finalizedSoftBest;
5751
+ if (bestRejectedPath !== void 0) {
5752
+ const finalizedRejected = finalizeRoute(
5753
+ bestRejectedPath,
5712
5754
  softObstacles,
5713
5755
  hardObstacles,
5714
5756
  diagnostics
5715
- ),
5757
+ );
5758
+ const rejectedCrossings = countObstacleCrossings(
5759
+ finalizedRejected,
5760
+ softObstacles
5761
+ );
5762
+ const heuristicCrossings = countObstacleCrossings(
5763
+ finalizedSoftBest,
5764
+ softObstacles
5765
+ );
5766
+ if (rejectedCrossings < heuristicCrossings) {
5767
+ softFallback = finalizedRejected;
5768
+ }
5769
+ }
5770
+ checkBacktracking(
5771
+ softFallback,
5772
+ softFallback[0],
5773
+ softFallback[softFallback.length - 1],
5774
+ diagnostics,
5775
+ input.maxBacktrackingRatio
5776
+ );
5777
+ return {
5778
+ points: softFallback,
5716
5779
  diagnostics
5717
5780
  };
5718
5781
  }
@@ -5744,6 +5807,22 @@ function routeEdge(input) {
5744
5807
  maxAttempts
5745
5808
  );
5746
5809
  }
5810
+ if (bestRejectedPath !== void 0) {
5811
+ diagnostics.push({
5812
+ severity: "warning",
5813
+ code: "routing.obstacle.unavoidable",
5814
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5815
+ });
5816
+ return {
5817
+ points: finalizeRoute(
5818
+ bestRejectedPath,
5819
+ softObstacles,
5820
+ hardObstacles,
5821
+ diagnostics
5822
+ ),
5823
+ diagnostics
5824
+ };
5825
+ }
5747
5826
  diagnostics.push({
5748
5827
  severity: "error",
5749
5828
  code: "routing.evidence.crossing_forbidden",
@@ -5791,13 +5870,41 @@ function routeEdge(input) {
5791
5870
  code: "routing.obstacle.unavoidable",
5792
5871
  message: "No bounded orthogonal route candidate avoided all obstacles."
5793
5872
  });
5794
- return {
5795
- points: finalizeRoute(
5796
- bestPoints,
5873
+ const finalizedBestPoints = finalizeRoute(
5874
+ bestPoints,
5875
+ softObstacles,
5876
+ hardObstacles,
5877
+ diagnostics
5878
+ );
5879
+ let fallbackPoints = finalizedBestPoints;
5880
+ if (bestRejectedPath !== void 0) {
5881
+ const finalizedRejected = finalizeRoute(
5882
+ bestRejectedPath,
5797
5883
  softObstacles,
5798
5884
  hardObstacles,
5799
5885
  diagnostics
5800
- ),
5886
+ );
5887
+ const rejectedCrossings = countObstacleCrossings(
5888
+ finalizedRejected,
5889
+ softObstacles
5890
+ );
5891
+ const heuristicCrossings = countObstacleCrossings(
5892
+ finalizedBestPoints,
5893
+ softObstacles
5894
+ );
5895
+ if (rejectedCrossings < heuristicCrossings) {
5896
+ fallbackPoints = finalizedRejected;
5897
+ }
5898
+ }
5899
+ checkBacktracking(
5900
+ fallbackPoints,
5901
+ fallbackPoints[0],
5902
+ fallbackPoints[fallbackPoints.length - 1],
5903
+ diagnostics,
5904
+ input.maxBacktrackingRatio
5905
+ );
5906
+ return {
5907
+ points: fallbackPoints,
5801
5908
  diagnostics
5802
5909
  };
5803
5910
  }
@@ -6296,6 +6403,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6296
6403
  }
6297
6404
  return false;
6298
6405
  }
6406
+ function countObstacleCrossings(points, obstacles) {
6407
+ let count = 0;
6408
+ for (const obstacle of obstacles) {
6409
+ validateBox(obstacle);
6410
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
6411
+ const a = points[pointIndex];
6412
+ const b = points[pointIndex + 1];
6413
+ if (a === void 0 || b === void 0) {
6414
+ continue;
6415
+ }
6416
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
6417
+ count += 1;
6418
+ break;
6419
+ }
6420
+ }
6421
+ }
6422
+ return count;
6423
+ }
6299
6424
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6300
6425
  for (let index = 0; index < points.length - 1; index += 1) {
6301
6426
  const a = points[index];
@@ -6741,7 +6866,8 @@ function solveDiagram(diagram, options = {}) {
6741
6866
  constrained.boxes,
6742
6867
  constrained.locks,
6743
6868
  options?.overlapSpacing ?? 40,
6744
- Math.max(0, options?.minLaneGutter ?? 0)
6869
+ Math.max(0, options?.minLaneGutter ?? 0),
6870
+ options.distributeContainedChildren ?? true
6745
6871
  );
6746
6872
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6747
6873
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6908,7 +7034,8 @@ function solveDiagram(diagram, options = {}) {
6908
7034
  diagram.direction,
6909
7035
  options,
6910
7036
  diagnostics,
6911
- coordinatedGroups
7037
+ coordinatedGroups,
7038
+ contentBounds
6912
7039
  );
6913
7040
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6914
7041
  coordinatedEdges,
@@ -7330,7 +7457,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7330
7457
  function containsCjk(value) {
7331
7458
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7332
7459
  }
7333
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7460
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7334
7461
  const layouts = /* @__PURE__ */ new Map();
7335
7462
  const diagnostics = [];
7336
7463
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7349,7 +7476,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7349
7476
  locks,
7350
7477
  diagnostics,
7351
7478
  movedChildIds,
7352
- laneGutter
7479
+ laneGutter,
7480
+ constraints,
7481
+ distributeContainedChildren
7353
7482
  );
7354
7483
  if (layout2 !== void 0) {
7355
7484
  layouts.set(swimlane.id, layout2);
@@ -7541,7 +7670,7 @@ function isStackRunaway(boxes, nodes, direction, options) {
7541
7670
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7542
7671
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7543
7672
  }
7544
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7673
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7545
7674
  const headerHeight = swimlane.headerHeight ?? 28;
7546
7675
  const padding = swimlane.padding ?? 16;
7547
7676
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7566,7 +7695,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7566
7695
  locks,
7567
7696
  diagnostics,
7568
7697
  movedChildIds,
7569
- laneGutter
7698
+ laneGutter,
7699
+ constraints,
7700
+ distributeContainedChildren
7570
7701
  );
7571
7702
  }
7572
7703
  return applyHorizontalSwimlaneContract(
@@ -7581,13 +7712,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7581
7712
  laneGutter
7582
7713
  );
7583
7714
  }
7584
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
7715
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7585
7716
  const populatedBounds = laneBounds.filter(
7586
7717
  (box) => box !== void 0
7587
7718
  );
7588
7719
  const top = Math.min(...populatedBounds.map((box) => box.y));
7589
7720
  const left = Math.min(...populatedBounds.map((box) => box.x));
7590
7721
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
7722
+ const containedChildIds = /* @__PURE__ */ new Set();
7723
+ if (distributeContainedChildren) {
7724
+ for (const c of constraints) {
7725
+ if (c.kind !== "containment") continue;
7726
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
7727
+ const distributable = c.childIds.filter((childId) => {
7728
+ if (nodeBoxes.get(childId) === void 0) return false;
7729
+ const lock = locks.get(childId);
7730
+ return lock === void 0 || lock.source === "fixed-position";
7731
+ });
7732
+ if (distributable.length < 2) continue;
7733
+ for (const childId of distributable) {
7734
+ containedChildIds.add(childId);
7735
+ }
7736
+ }
7737
+ }
7591
7738
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7592
7739
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7593
7740
  const rankStackGap = Math.max(8, padding / 2);
@@ -7604,7 +7751,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7604
7751
  nodeBoxes,
7605
7752
  flowRanks,
7606
7753
  locks,
7607
- rankStackGap
7754
+ rankStackGap,
7755
+ containedChildIds
7608
7756
  );
7609
7757
  const slotWidth = Math.max(
7610
7758
  Math.max(...populatedBounds.map((box) => box.width)),
@@ -7626,7 +7774,10 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7626
7774
  const distributable = lane.children.filter(
7627
7775
  (childId) => !locks.has(childId)
7628
7776
  );
7629
- if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7777
+ const coveredByContainment = lane.children.some(
7778
+ (childId) => containedChildIds.has(childId)
7779
+ );
7780
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7630
7781
  moveRankedVerticalLaneChildren(
7631
7782
  lane.children,
7632
7783
  nodeBoxes,
@@ -7654,6 +7805,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7654
7805
  );
7655
7806
  continue;
7656
7807
  }
7808
+ const rankedCoveredByContainment = lane.children.some(
7809
+ (childId) => containedChildIds.has(childId)
7810
+ );
7657
7811
  moveRankedVerticalLaneChildren(
7658
7812
  lane.children,
7659
7813
  nodeBoxes,
@@ -7664,7 +7818,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7664
7818
  rankSpacing,
7665
7819
  rankStackGap,
7666
7820
  { x: target.x, y: laneContentTop },
7667
- slotWidth - padding * 2
7821
+ slotWidth - padding * 2,
7822
+ rankedCoveredByContainment
7668
7823
  );
7669
7824
  }
7670
7825
  return {
@@ -7780,9 +7935,12 @@ function crossAxisSpreadWidth(items, gap) {
7780
7935
  0
7781
7936
  );
7782
7937
  }
7783
- function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7938
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7784
7939
  let maxWidth = 0;
7785
7940
  for (const lane of swimlane.lanes) {
7941
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7942
+ continue;
7943
+ }
7786
7944
  for (const stack of rankStacks(
7787
7945
  lane.children,
7788
7946
  nodeBoxes,
@@ -7795,7 +7953,7 @@ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7795
7953
  }
7796
7954
  return maxWidth;
7797
7955
  }
7798
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
7956
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7799
7957
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7800
7958
  const unlocked = [];
7801
7959
  for (const item of stack) {
@@ -7824,7 +7982,7 @@ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics,
7824
7982
  }
7825
7983
  nodeBoxes.set(childId, next);
7826
7984
  } else {
7827
- const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7985
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7828
7986
  if (!shouldSpread) {
7829
7987
  let yOffset = 0;
7830
7988
  for (const { childId, box } of unlocked) {
@@ -8953,14 +9111,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8953
9111
  }
8954
9112
  };
8955
9113
  }
8956
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
9114
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8957
9115
  const coordinated = [];
8958
9116
  const coordinatedNodeById = new Map(
8959
9117
  coordinatedNodes.map((node) => [node.id, node])
8960
9118
  );
9119
+ const corridorMarginOption = options.corridorMargin ?? "auto";
9120
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
9121
+ 200,
9122
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
9123
+ );
9124
+ const routingGutter = options.routingGutter ?? 160;
9125
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8961
9126
  const nodeObstacleIndex = createBoxSpatialIndex(
8962
9127
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8963
- options.routingGutter ?? 160
9128
+ queryGutter
8964
9129
  );
8965
9130
  for (const edge of edges) {
8966
9131
  const source = nodes.get(edge.source.nodeId);
@@ -8982,11 +9147,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8982
9147
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8983
9148
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8984
9149
  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
- );
9150
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8990
9151
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8991
9152
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8992
9153
  );
@@ -9004,7 +9165,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
9004
9165
  ...routeTextObstacles
9005
9166
  ],
9006
9167
  hardObstacles,
9007
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
9168
+ corridorMargin,
9169
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
9170
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
9008
9171
  });
9009
9172
  diagnostics.push(
9010
9173
  ...route.diagnostics.map((diagnostic) => ({