@crazyhappyone/auto-graph 0.2.8 → 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
@@ -176,6 +176,28 @@ function applyLayoutConstraints(input) {
176
176
  if (input.distributeContainedChildren) {
177
177
  yieldFixedPositionLocks(input, boxes, locks);
178
178
  }
179
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
180
+ for (const swimlane of input.swimlanes) {
181
+ if (swimlane.layout === "contract") continue;
182
+ for (const lane of swimlane.lanes) {
183
+ const fixedChildren = [];
184
+ let participantCount = 0;
185
+ for (const childId of lane.children) {
186
+ const lock = locks.get(childId);
187
+ if (lock === void 0) {
188
+ participantCount += 1;
189
+ } else if (lock.source === "fixed-position") {
190
+ participantCount += 1;
191
+ fixedChildren.push(childId);
192
+ }
193
+ }
194
+ if (participantCount < 2) continue;
195
+ for (const childId of fixedChildren) {
196
+ locks.delete(childId);
197
+ }
198
+ }
199
+ }
200
+ }
179
201
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
180
202
  applyRelative(input.constraints, boxes, locks, diagnostics);
181
203
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -195,6 +217,9 @@ function applyLayoutConstraints(input) {
195
217
  applyDistributeContained(input, boxes, locks, diagnostics);
196
218
  dedupReplayDiagnostics(diagnostics, diagBefore);
197
219
  }
220
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
221
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
222
+ }
198
223
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
199
224
  reportOverlaps(
200
225
  boxes,
@@ -1034,9 +1059,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1034
1059
  }
1035
1060
  });
1036
1061
  }
1037
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1038
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1039
- }
1040
1062
  }
1041
1063
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1042
1064
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -1076,6 +1098,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1076
1098
  effectiveGap = minGap + remaining / (unlocked.length - 1);
1077
1099
  }
1078
1100
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
1101
+ reserved.sort((a, b) => a.start - b.start);
1079
1102
  let pos = contentStart;
1080
1103
  for (const child of unlocked) {
1081
1104
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4726,15 +4749,12 @@ var BinaryHeap = class {
4726
4749
  let smallestIdx = idx;
4727
4750
  const leftIdx = (idx << 1) + 1;
4728
4751
  const rightIdx = leftIdx + 1;
4729
- if (leftIdx < size && this._less(
4730
- this._data[leftIdx],
4731
- this._data[smallestIdx]
4732
- )) {
4752
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4733
4753
  smallestIdx = leftIdx;
4734
4754
  }
4735
4755
  if (rightIdx < size && this._less(
4736
4756
  this._data[rightIdx],
4737
- this._data[smallestIdx]
4757
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4738
4758
  )) {
4739
4759
  smallestIdx = rightIdx;
4740
4760
  }
@@ -4785,7 +4805,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4785
4805
  detail: {
4786
4806
  xsCount: xs.length,
4787
4807
  ysCount: ys.length,
4788
- maxNodes
4808
+ maxNodes,
4809
+ obstacleCount: obstacles.length,
4810
+ stage: "corridor-filtered",
4811
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4789
4812
  }
4790
4813
  });
4791
4814
  return null;
@@ -4801,8 +4824,66 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4801
4824
  turnPenalty,
4802
4825
  segmentPenalty
4803
4826
  );
4804
- if (path === null) return null;
4805
- return simplifyRoute(path);
4827
+ if (path !== null) {
4828
+ const simplified = simplifyRoute(path);
4829
+ const filteredSet = new Set(filtered);
4830
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4831
+ let crossesExcluded = false;
4832
+ for (let i = 0; i < simplified.length - 1; i++) {
4833
+ const a = simplified[i];
4834
+ const b = simplified[i + 1];
4835
+ for (const obs of obstacles) {
4836
+ if (filteredSet.has(obs)) continue;
4837
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4838
+ crossesExcluded = true;
4839
+ break;
4840
+ }
4841
+ }
4842
+ if (crossesExcluded) break;
4843
+ }
4844
+ if (!crossesExcluded) return simplified;
4845
+ } else {
4846
+ return simplified;
4847
+ }
4848
+ }
4849
+ if (!useCorridor) return null;
4850
+ const xsFull = collectXs(source, target, obstacles, margin);
4851
+ const ysFull = collectYs(source, target, obstacles, margin);
4852
+ if (xsFull.length * ysFull.length > maxNodes) {
4853
+ diagnostics?.push({
4854
+ severity: "warning",
4855
+ code: "routing.astar.grid_overflow",
4856
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
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
+ }
4865
+ });
4866
+ return null;
4867
+ }
4868
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4869
+ connectHorizontalEdges(
4870
+ nodesFull,
4871
+ ysFull,
4872
+ obstacles,
4873
+ endpointObstacles,
4874
+ margin
4875
+ );
4876
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4877
+ const pathFull = aStarSearch(
4878
+ nodesFull,
4879
+ idxFull,
4880
+ source,
4881
+ target,
4882
+ turnPenalty,
4883
+ segmentPenalty
4884
+ );
4885
+ if (pathFull === null) return null;
4886
+ return simplifyRoute(pathFull);
4806
4887
  }
4807
4888
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4808
4889
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -5023,20 +5104,83 @@ function areCollinear(a, b, c) {
5023
5104
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
5024
5105
  }
5025
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
+
5026
5165
  // src/routing/visibility-router.ts
5027
5166
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
5028
5167
  const margin = options.margin ?? 0;
5029
5168
  const turnPenalty = options.turnPenalty ?? 50;
5030
5169
  const segmentPenalty = options.segmentPenalty ?? 1;
5031
5170
  const endpointObstacles = options.endpointObstacles ?? [];
5032
- const maxCorners = options.maxCorners ?? 300;
5171
+ const maxCorners = options.maxCorners ?? 600;
5033
5172
  const vertices = collectCornerVertices(source, target, obstacles, margin);
5034
5173
  if (vertices.length > maxCorners) {
5035
5174
  diagnostics?.push({
5036
5175
  severity: "warning",
5037
5176
  code: "routing.visibility.corner_overflow",
5038
5177
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
5039
- 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
+ }
5040
5184
  });
5041
5185
  return null;
5042
5186
  }
@@ -5310,7 +5454,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5310
5454
  }
5311
5455
 
5312
5456
  // src/routing/routes.ts
5313
- function checkBacktracking(points, source, target, diagnostics) {
5457
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5314
5458
  if (points.length < 2) return;
5315
5459
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5316
5460
  if (direct <= 0) return;
@@ -5320,7 +5464,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5320
5464
  const b = points[i + 1];
5321
5465
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5322
5466
  }
5323
- const threshold = 10;
5467
+ const threshold = maxRatio ?? 20;
5324
5468
  if (routeLen > direct * threshold) {
5325
5469
  diagnostics.push({
5326
5470
  severity: "warning",
@@ -5338,8 +5482,20 @@ function routeEdge(input) {
5338
5482
  const diagnostics = [];
5339
5483
  const softObstacles = input.obstacles ?? [];
5340
5484
  const hardObstacles = input.hardObstacles ?? [];
5485
+ let bestRejectedPath;
5486
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5341
5487
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5342
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
+ };
5343
5499
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5344
5500
  const defaultAnchors = defaultAnchorsForGeometry(
5345
5501
  input.source.box,
@@ -5398,18 +5554,59 @@ function routeEdge(input) {
5398
5554
  input.source.center,
5399
5555
  targetAnchor
5400
5556
  );
5401
- const cornerPath = findCornerGraphPath(
5557
+ const allObstacles = [...softObstacles, ...hardObstacles];
5558
+ const corridorMargin = input.corridorMargin ?? 32;
5559
+ const corridorObstacles = filterObstaclesByCorridor(
5402
5560
  source,
5403
5561
  target,
5404
- [...softObstacles, ...hardObstacles],
5405
- { endpointObstacles, margin: 2 },
5562
+ allObstacles,
5563
+ [],
5564
+ // endpointObstacles passed separately via options
5565
+ corridorMargin
5566
+ );
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
+ );
5574
+ let cornerPath = findCornerGraphPath(
5575
+ source,
5576
+ target,
5577
+ cornerObstacles,
5578
+ {
5579
+ endpointObstacles,
5580
+ margin: 2,
5581
+ maxCorners: budget.maxCorners,
5582
+ corridorMargin
5583
+ },
5406
5584
  diagnostics
5407
5585
  );
5586
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
5587
+ cornerPath = findCornerGraphPath(
5588
+ source,
5589
+ target,
5590
+ allObstacles,
5591
+ {
5592
+ endpointObstacles,
5593
+ margin: 2,
5594
+ maxCorners: budget.maxCorners,
5595
+ corridorMargin
5596
+ },
5597
+ diagnostics
5598
+ );
5599
+ }
5408
5600
  const path = cornerPath ?? findObstacleFreePath(
5409
5601
  source,
5410
5602
  target,
5411
- [...softObstacles, ...hardObstacles],
5412
- { endpointObstacles, margin: 0 },
5603
+ allObstacles,
5604
+ {
5605
+ endpointObstacles,
5606
+ margin: 0,
5607
+ corridorMargin,
5608
+ maxNodes: budget.maxNodes
5609
+ },
5413
5610
  diagnostics
5414
5611
  );
5415
5612
  if (path !== null && path.length >= 2) {
@@ -5426,9 +5623,100 @@ function routeEdge(input) {
5426
5623
  softObstacles,
5427
5624
  softObstacleIndex
5428
5625
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5429
- checkBacktracking(finalized, source, target, diagnostics);
5626
+ checkBacktracking(
5627
+ finalized,
5628
+ source,
5629
+ target,
5630
+ diagnostics,
5631
+ input.maxBacktrackingRatio
5632
+ );
5430
5633
  return { points: finalized, diagnostics };
5431
5634
  }
5635
+ recordRejected(finalized);
5636
+ if (cornerPath !== null) {
5637
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5638
+ source,
5639
+ target,
5640
+ allObstacles,
5641
+ {
5642
+ endpointObstacles,
5643
+ margin: 2,
5644
+ maxCorners: budget.maxCorners,
5645
+ corridorMargin
5646
+ },
5647
+ diagnostics
5648
+ ) : null;
5649
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5650
+ const fullFinalized = finalizeRoute(
5651
+ fullCornerPath,
5652
+ softObstacles,
5653
+ hardObstacles,
5654
+ diagnostics,
5655
+ softObstacleIndex,
5656
+ hardObstacleIndex
5657
+ );
5658
+ if (!routeIntersectsObstacles(
5659
+ fullFinalized,
5660
+ softObstacles,
5661
+ softObstacleIndex
5662
+ ) && !routeIntersectsObstacles(
5663
+ fullFinalized,
5664
+ hardObstacles,
5665
+ hardObstacleIndex
5666
+ )) {
5667
+ checkBacktracking(
5668
+ fullFinalized,
5669
+ source,
5670
+ target,
5671
+ diagnostics,
5672
+ input.maxBacktrackingRatio
5673
+ );
5674
+ return { points: fullFinalized, diagnostics };
5675
+ }
5676
+ recordRejected(fullFinalized);
5677
+ }
5678
+ const gridPath = findObstacleFreePath(
5679
+ source,
5680
+ target,
5681
+ allObstacles,
5682
+ {
5683
+ endpointObstacles,
5684
+ margin: 0,
5685
+ corridorMargin,
5686
+ maxNodes: budget.maxNodes
5687
+ },
5688
+ diagnostics
5689
+ );
5690
+ if (gridPath !== null && gridPath.length >= 2) {
5691
+ const gridFinalized = finalizeRoute(
5692
+ gridPath,
5693
+ softObstacles,
5694
+ hardObstacles,
5695
+ diagnostics,
5696
+ softObstacleIndex,
5697
+ hardObstacleIndex
5698
+ );
5699
+ if (!routeIntersectsObstacles(
5700
+ gridFinalized,
5701
+ softObstacles,
5702
+ softObstacleIndex
5703
+ ) && !routeIntersectsObstacles(
5704
+ gridFinalized,
5705
+ hardObstacles,
5706
+ hardObstacleIndex
5707
+ )) {
5708
+ checkBacktracking(
5709
+ gridFinalized,
5710
+ source,
5711
+ target,
5712
+ diagnostics,
5713
+ input.maxBacktrackingRatio
5714
+ );
5715
+ return { points: gridFinalized, diagnostics };
5716
+ }
5717
+ recordRejected(gridFinalized);
5718
+ }
5719
+ }
5432
5720
  }
5433
5721
  }
5434
5722
  }
@@ -5490,7 +5778,8 @@ function routeEdge(input) {
5490
5778
  finalizedClean,
5491
5779
  candidate.points[0],
5492
5780
  candidate.points[candidate.points.length - 1],
5493
- diagnostics
5781
+ diagnostics,
5782
+ input.maxBacktrackingRatio
5494
5783
  );
5495
5784
  return { points: finalizedClean, diagnostics };
5496
5785
  }
@@ -5556,13 +5845,41 @@ function routeEdge(input) {
5556
5845
  code: "routing.obstacle.unavoidable",
5557
5846
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5558
5847
  });
5559
- return {
5560
- points: finalizeRoute(
5561
- 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,
5562
5858
  softObstacles,
5563
5859
  hardObstacles,
5564
5860
  diagnostics
5565
- ),
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,
5566
5883
  diagnostics
5567
5884
  };
5568
5885
  }
@@ -5594,6 +5911,22 @@ function routeEdge(input) {
5594
5911
  maxAttempts
5595
5912
  );
5596
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
+ }
5597
5930
  diagnostics.push({
5598
5931
  severity: "error",
5599
5932
  code: "routing.evidence.crossing_forbidden",
@@ -5641,13 +5974,41 @@ function routeEdge(input) {
5641
5974
  code: "routing.obstacle.unavoidable",
5642
5975
  message: "No bounded orthogonal route candidate avoided all obstacles."
5643
5976
  });
5644
- return {
5645
- points: finalizeRoute(
5646
- 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,
5647
5987
  softObstacles,
5648
5988
  hardObstacles,
5649
5989
  diagnostics
5650
- ),
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,
5651
6012
  diagnostics
5652
6013
  };
5653
6014
  }
@@ -6146,6 +6507,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6146
6507
  }
6147
6508
  return false;
6148
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
+ }
6149
6528
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6150
6529
  for (let index = 0; index < points.length - 1; index += 1) {
6151
6530
  const a = points[index];
@@ -6529,7 +6908,7 @@ function solveDiagram(diagram, options = {}) {
6529
6908
  edges: styledEdges
6530
6909
  });
6531
6910
  diagnostics.push(...layout2.diagnostics);
6532
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6911
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
6533
6912
  layout2.boxes,
6534
6913
  styledNodes,
6535
6914
  styledEdges,
@@ -6537,7 +6916,8 @@ function solveDiagram(diagram, options = {}) {
6537
6916
  options,
6538
6917
  diagnostics
6539
6918
  );
6540
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6919
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6920
+ const diagCountBefore = diagnostics.length;
6541
6921
  const rewrapped = wrapHorizontalStackIfNeeded(
6542
6922
  initialNodeBoxes,
6543
6923
  styledNodes,
@@ -6548,6 +6928,20 @@ function solveDiagram(diagram, options = {}) {
6548
6928
  for (const [id, box] of rewrapped) {
6549
6929
  initialNodeBoxes.set(id, box);
6550
6930
  }
6931
+ if (diagnostics.length > diagCountBefore) {
6932
+ for (const node of styledNodes) {
6933
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6934
+ const rwBox = rewrapped.get(node.id);
6935
+ const idx = styledNodes.indexOf(node);
6936
+ if (idx !== -1) {
6937
+ styledNodes[idx] = {
6938
+ ...node,
6939
+ position: { x: rwBox.x, y: rwBox.y }
6940
+ };
6941
+ }
6942
+ }
6943
+ }
6944
+ }
6551
6945
  }
6552
6946
  if (useRecursive && "groupBoxes" in layout2) {
6553
6947
  const recursiveLayout = layout2;
@@ -6561,7 +6955,7 @@ function solveDiagram(diagram, options = {}) {
6561
6955
  overlapSpacing: options?.overlapSpacing ?? 40,
6562
6956
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
6563
6957
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6564
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6958
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
6565
6959
  swimlanes: styledSwimlanes,
6566
6960
  boxes: initialNodeBoxes,
6567
6961
  nodes: styledNodes,
@@ -6576,7 +6970,8 @@ function solveDiagram(diagram, options = {}) {
6576
6970
  constrained.boxes,
6577
6971
  constrained.locks,
6578
6972
  options?.overlapSpacing ?? 40,
6579
- Math.max(0, options?.minLaneGutter ?? 0)
6973
+ Math.max(0, options?.minLaneGutter ?? 0),
6974
+ options.distributeContainedChildren ?? true
6580
6975
  );
6581
6976
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6582
6977
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6743,7 +7138,8 @@ function solveDiagram(diagram, options = {}) {
6743
7138
  diagram.direction,
6744
7139
  options,
6745
7140
  diagnostics,
6746
- coordinatedGroups
7141
+ coordinatedGroups,
7142
+ contentBounds
6747
7143
  );
6748
7144
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6749
7145
  coordinatedEdges,
@@ -7165,7 +7561,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7165
7561
  function containsCjk(value) {
7166
7562
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7167
7563
  }
7168
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7564
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7169
7565
  const layouts = /* @__PURE__ */ new Map();
7170
7566
  const diagnostics = [];
7171
7567
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7184,7 +7580,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7184
7580
  locks,
7185
7581
  diagnostics,
7186
7582
  movedChildIds,
7187
- laneGutter
7583
+ laneGutter,
7584
+ constraints,
7585
+ distributeContainedChildren
7188
7586
  );
7189
7587
  if (layout2 !== void 0) {
7190
7588
  layouts.set(swimlane.id, layout2);
@@ -7280,9 +7678,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
7280
7678
  if (!isStackRunaway(boxes, nodes, direction, options)) {
7281
7679
  return new Map(boxes);
7282
7680
  }
7283
- const maxRowDepth = options.maxRowDepth;
7284
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7285
- return new Map(boxes);
7681
+ let maxRowDepth = options.maxRowDepth;
7682
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
7683
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
7684
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
7685
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
7686
+ } else {
7687
+ return new Map(boxes);
7688
+ }
7286
7689
  }
7287
7690
  const ordered = [...nodes].sort((a, b) => {
7288
7691
  const ba = boxes.get(a.id);
@@ -7343,10 +7746,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
7343
7746
  });
7344
7747
  }
7345
7748
  function isStackRunaway(boxes, nodes, direction, options) {
7346
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
7749
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
7347
7750
  return false;
7348
7751
  }
7349
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
7752
+ if (nodes.length < 2) {
7350
7753
  return false;
7351
7754
  }
7352
7755
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -7354,17 +7757,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
7354
7757
  return false;
7355
7758
  }
7356
7759
  const bounds = unionBoxes(nodeBoxes);
7357
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7358
- const preferred = options.preferredAspectRatio ?? 3;
7359
- if (aspectRatio < preferred) {
7760
+ const isHorizontal = direction === "TB" || direction === "BT";
7761
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7762
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
7763
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
7360
7764
  return false;
7361
7765
  }
7766
+ if (isHorizontal) {
7767
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
7768
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
7769
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
7770
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
7771
+ }
7362
7772
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
7363
7773
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
7364
7774
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7365
7775
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7366
7776
  }
7367
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7777
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7368
7778
  const headerHeight = swimlane.headerHeight ?? 28;
7369
7779
  const padding = swimlane.padding ?? 16;
7370
7780
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7389,7 +7799,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7389
7799
  locks,
7390
7800
  diagnostics,
7391
7801
  movedChildIds,
7392
- laneGutter
7802
+ laneGutter,
7803
+ constraints,
7804
+ distributeContainedChildren
7393
7805
  );
7394
7806
  }
7395
7807
  return applyHorizontalSwimlaneContract(
@@ -7404,13 +7816,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7404
7816
  laneGutter
7405
7817
  );
7406
7818
  }
7407
- 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) {
7408
7820
  const populatedBounds = laneBounds.filter(
7409
7821
  (box) => box !== void 0
7410
7822
  );
7411
7823
  const top = Math.min(...populatedBounds.map((box) => box.y));
7412
7824
  const left = Math.min(...populatedBounds.map((box) => box.x));
7413
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
+ }
7414
7842
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7415
7843
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7416
7844
  const rankStackGap = Math.max(8, padding / 2);
@@ -7422,7 +7850,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7422
7850
  );
7423
7851
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
7424
7852
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
7425
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
7853
+ const spreadWidth = maxCrossAxisSpreadWidth(
7854
+ swimlane,
7855
+ nodeBoxes,
7856
+ flowRanks,
7857
+ locks,
7858
+ rankStackGap,
7859
+ containedChildIds
7860
+ );
7861
+ const slotWidth = Math.max(
7862
+ Math.max(...populatedBounds.map((box) => box.width)),
7863
+ spreadWidth
7864
+ ) + padding * 2;
7426
7865
  const laneStep = slotWidth + laneGutter;
7427
7866
  const laneContentTop = top + headerHeight + padding;
7428
7867
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -7436,6 +7875,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7436
7875
  y: laneContentTop
7437
7876
  };
7438
7877
  if (maxRank === 0) {
7878
+ const distributable = lane.children.filter(
7879
+ (childId) => !locks.has(childId)
7880
+ );
7881
+ const coveredByContainment = lane.children.some(
7882
+ (childId) => containedChildIds.has(childId)
7883
+ );
7884
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7885
+ moveRankedVerticalLaneChildren(
7886
+ lane.children,
7887
+ nodeBoxes,
7888
+ locks,
7889
+ diagnostics,
7890
+ movedChildIds,
7891
+ flowRanks,
7892
+ rankSpacing,
7893
+ rankStackGap,
7894
+ { x: target.x, y: laneContentTop },
7895
+ slotWidth - padding * 2
7896
+ );
7897
+ continue;
7898
+ }
7439
7899
  moveLaneChildren(
7440
7900
  lane.children,
7441
7901
  nodeBoxes,
@@ -7449,6 +7909,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7449
7909
  );
7450
7910
  continue;
7451
7911
  }
7912
+ const rankedCoveredByContainment = lane.children.some(
7913
+ (childId) => containedChildIds.has(childId)
7914
+ );
7452
7915
  moveRankedVerticalLaneChildren(
7453
7916
  lane.children,
7454
7917
  nodeBoxes,
@@ -7458,10 +7921,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7458
7921
  flowRanks,
7459
7922
  rankSpacing,
7460
7923
  rankStackGap,
7461
- {
7462
- x: target.x - bounds.x,
7463
- y: laneContentTop
7464
- }
7924
+ { x: target.x, y: laneContentTop },
7925
+ slotWidth - padding * 2,
7926
+ rankedCoveredByContainment
7465
7927
  );
7466
7928
  }
7467
7929
  return {
@@ -7570,31 +8032,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
7570
8032
  }
7571
8033
  return maxHeight;
7572
8034
  }
7573
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
8035
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
8036
+ function crossAxisSpreadWidth(items, gap) {
8037
+ return items.reduce(
8038
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
8039
+ 0
8040
+ );
8041
+ }
8042
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
8043
+ let maxWidth = 0;
8044
+ for (const lane of swimlane.lanes) {
8045
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
8046
+ continue;
8047
+ }
8048
+ for (const stack of rankStacks(
8049
+ lane.children,
8050
+ nodeBoxes,
8051
+ flowRanks
8052
+ ).values()) {
8053
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
8054
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
8055
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
8056
+ }
8057
+ }
8058
+ return maxWidth;
8059
+ }
8060
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7574
8061
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7575
- let yOffset = 0;
8062
+ const unlocked = [];
7576
8063
  for (const item of stack) {
7577
- const { childId, box } = item;
7578
- if (locks.has(childId)) {
8064
+ if (locks.has(item.childId)) {
7579
8065
  diagnostics.push({
7580
8066
  severity: "warning",
7581
8067
  code: "constraints.locked-target-not-moved",
7582
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
8068
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
7583
8069
  path: ["swimlanes"],
7584
- detail: { nodeId: childId }
8070
+ detail: { nodeId: item.childId }
7585
8071
  });
7586
- continue;
8072
+ } else {
8073
+ unlocked.push(item);
7587
8074
  }
8075
+ }
8076
+ if (unlocked.length === 0) continue;
8077
+ if (unlocked.length === 1) {
8078
+ const { childId, box } = unlocked[0];
7588
8079
  const next = {
7589
8080
  ...box,
7590
- x: box.x + target.x,
7591
- y: target.y + rank * rankSpacing + yOffset
8081
+ x: target.x + (contentWidth - box.width) / 2,
8082
+ y: target.y + rank * rankSpacing
7592
8083
  };
7593
8084
  if (next.x !== box.x || next.y !== box.y) {
7594
8085
  movedChildIds.add(childId);
7595
8086
  }
7596
8087
  nodeBoxes.set(childId, next);
7597
- yOffset += box.height + rankStackGap;
8088
+ } else {
8089
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
8090
+ if (!shouldSpread) {
8091
+ let yOffset = 0;
8092
+ for (const { childId, box } of unlocked) {
8093
+ const next = {
8094
+ ...box,
8095
+ x: target.x + (contentWidth - box.width) / 2,
8096
+ y: target.y + rank * rankSpacing + yOffset
8097
+ };
8098
+ if (next.x !== box.x || next.y !== box.y) {
8099
+ movedChildIds.add(childId);
8100
+ }
8101
+ nodeBoxes.set(childId, next);
8102
+ yOffset += box.height + rankStackGap;
8103
+ }
8104
+ } else {
8105
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
8106
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
8107
+ for (const { childId, box } of unlocked) {
8108
+ const next = {
8109
+ ...box,
8110
+ x: xCursor,
8111
+ y: target.y + rank * rankSpacing
8112
+ };
8113
+ if (next.x !== box.x || next.y !== box.y) {
8114
+ movedChildIds.add(childId);
8115
+ }
8116
+ nodeBoxes.set(childId, next);
8117
+ xCursor += box.width + rankStackGap;
8118
+ }
8119
+ diagnostics.push({
8120
+ severity: "info",
8121
+ code: "swimlane_contract.cross_axis_distributed",
8122
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
8123
+ path: ["swimlanes"],
8124
+ detail: {
8125
+ rank,
8126
+ childCount: unlocked.length,
8127
+ contentWidth
8128
+ }
8129
+ });
8130
+ }
7598
8131
  }
7599
8132
  }
7600
8133
  }
@@ -7933,7 +8466,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7933
8466
  });
7934
8467
  continue;
7935
8468
  }
7936
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
8469
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7937
8470
  const geometry = computeShapeGeometry({
7938
8471
  shape: node.shape,
7939
8472
  box,
@@ -8027,7 +8560,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
8027
8560
  }
8028
8561
  }
8029
8562
  }
8030
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8563
+ function coordinatePorts(node, nodeBox, portShifting) {
8031
8564
  const portsBySide = /* @__PURE__ */ new Map();
8032
8565
  for (const port of node.ports ?? []) {
8033
8566
  const ports = portsBySide.get(port.side) ?? [];
@@ -8050,9 +8583,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8050
8583
  side,
8051
8584
  index,
8052
8585
  sorted.length,
8053
- portShifting,
8054
- diagnostics,
8055
- node.id
8586
+ portShifting
8056
8587
  );
8057
8588
  const box = portBox(anchor);
8058
8589
  coordinated.push({ ...port, box, anchor });
@@ -8060,32 +8591,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8060
8591
  }
8061
8592
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
8062
8593
  }
8063
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
8594
+ function portAnchor(nodeBox, side, index, count, portShifting) {
8064
8595
  const shiftingEnabled = portShifting?.enabled ?? true;
8065
8596
  const requestedSpacing = portShifting?.spacing ?? 24;
8066
8597
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
8067
8598
  const availableSpan = 2 * maxOffset;
8068
8599
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
8069
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
8600
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
8070
8601
  Math.min(requestedSpacing, availableSpan / (count - 1)),
8071
8602
  minSpacing
8072
8603
  ) : requestedSpacing;
8073
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
8074
- diagnostics.push({
8075
- severity: "warning",
8076
- code: "port_constraint_overlap",
8077
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
8078
- path: ["nodes", nodeId, "ports"],
8079
- detail: {
8080
- nodeId,
8081
- side,
8082
- requestedSpacing,
8083
- effectiveSpacing: Math.round(effectiveSpacing),
8084
- portCount: count
8085
- }
8086
- });
8087
- }
8088
- const spacing = effectiveSpacing;
8089
8604
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
8090
8605
  switch (side) {
8091
8606
  case "left":
@@ -8700,14 +9215,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8700
9215
  }
8701
9216
  };
8702
9217
  }
8703
- 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) {
8704
9219
  const coordinated = [];
8705
9220
  const coordinatedNodeById = new Map(
8706
9221
  coordinatedNodes.map((node) => [node.id, node])
8707
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;
8708
9230
  const nodeObstacleIndex = createBoxSpatialIndex(
8709
9231
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8710
- options.routingGutter ?? 160
9232
+ queryGutter
8711
9233
  );
8712
9234
  for (const edge of edges) {
8713
9235
  const source = nodes.get(edge.source.nodeId);
@@ -8729,11 +9251,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8729
9251
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8730
9252
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8731
9253
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8732
- const corridor = edgeCorridorBox(
8733
- source.box,
8734
- target.box,
8735
- options.routingGutter ?? 160
8736
- );
9254
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8737
9255
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8738
9256
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8739
9257
  );
@@ -8751,7 +9269,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8751
9269
  ...routeTextObstacles
8752
9270
  ],
8753
9271
  hardObstacles,
8754
- ...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 }
8755
9275
  });
8756
9276
  diagnostics.push(
8757
9277
  ...route.diagnostics.map((diagnostic) => ({