@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.cjs CHANGED
@@ -179,6 +179,28 @@ function applyLayoutConstraints(input) {
179
179
  if (input.distributeContainedChildren) {
180
180
  yieldFixedPositionLocks(input, boxes, locks);
181
181
  }
182
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
183
+ for (const swimlane of input.swimlanes) {
184
+ if (swimlane.layout === "contract") continue;
185
+ for (const lane of swimlane.lanes) {
186
+ const fixedChildren = [];
187
+ let participantCount = 0;
188
+ for (const childId of lane.children) {
189
+ const lock = locks.get(childId);
190
+ if (lock === void 0) {
191
+ participantCount += 1;
192
+ } else if (lock.source === "fixed-position") {
193
+ participantCount += 1;
194
+ fixedChildren.push(childId);
195
+ }
196
+ }
197
+ if (participantCount < 2) continue;
198
+ for (const childId of fixedChildren) {
199
+ locks.delete(childId);
200
+ }
201
+ }
202
+ }
203
+ }
182
204
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
183
205
  applyRelative(input.constraints, boxes, locks, diagnostics);
184
206
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -198,6 +220,9 @@ function applyLayoutConstraints(input) {
198
220
  applyDistributeContained(input, boxes, locks, diagnostics);
199
221
  dedupReplayDiagnostics(diagnostics, diagBefore);
200
222
  }
223
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
224
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
225
+ }
201
226
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
202
227
  reportOverlaps(
203
228
  boxes,
@@ -1037,9 +1062,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1037
1062
  }
1038
1063
  });
1039
1064
  }
1040
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1041
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1042
- }
1043
1065
  }
1044
1066
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1045
1067
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -1079,6 +1101,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1079
1101
  effectiveGap = minGap + remaining / (unlocked.length - 1);
1080
1102
  }
1081
1103
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
1104
+ reserved.sort((a, b) => a.start - b.start);
1082
1105
  let pos = contentStart;
1083
1106
  for (const child of unlocked) {
1084
1107
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4729,15 +4752,12 @@ var BinaryHeap = class {
4729
4752
  let smallestIdx = idx;
4730
4753
  const leftIdx = (idx << 1) + 1;
4731
4754
  const rightIdx = leftIdx + 1;
4732
- if (leftIdx < size && this._less(
4733
- this._data[leftIdx],
4734
- this._data[smallestIdx]
4735
- )) {
4755
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4736
4756
  smallestIdx = leftIdx;
4737
4757
  }
4738
4758
  if (rightIdx < size && this._less(
4739
4759
  this._data[rightIdx],
4740
- this._data[smallestIdx]
4760
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4741
4761
  )) {
4742
4762
  smallestIdx = rightIdx;
4743
4763
  }
@@ -4788,7 +4808,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4788
4808
  detail: {
4789
4809
  xsCount: xs.length,
4790
4810
  ysCount: ys.length,
4791
- maxNodes
4811
+ maxNodes,
4812
+ obstacleCount: obstacles.length,
4813
+ stage: "corridor-filtered",
4814
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4792
4815
  }
4793
4816
  });
4794
4817
  return null;
@@ -4804,8 +4827,66 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4804
4827
  turnPenalty,
4805
4828
  segmentPenalty
4806
4829
  );
4807
- if (path === null) return null;
4808
- return simplifyRoute(path);
4830
+ if (path !== null) {
4831
+ const simplified = simplifyRoute(path);
4832
+ const filteredSet = new Set(filtered);
4833
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4834
+ let crossesExcluded = false;
4835
+ for (let i = 0; i < simplified.length - 1; i++) {
4836
+ const a = simplified[i];
4837
+ const b = simplified[i + 1];
4838
+ for (const obs of obstacles) {
4839
+ if (filteredSet.has(obs)) continue;
4840
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4841
+ crossesExcluded = true;
4842
+ break;
4843
+ }
4844
+ }
4845
+ if (crossesExcluded) break;
4846
+ }
4847
+ if (!crossesExcluded) return simplified;
4848
+ } else {
4849
+ return simplified;
4850
+ }
4851
+ }
4852
+ if (!useCorridor) return null;
4853
+ const xsFull = collectXs(source, target, obstacles, margin);
4854
+ const ysFull = collectYs(source, target, obstacles, margin);
4855
+ if (xsFull.length * ysFull.length > maxNodes) {
4856
+ diagnostics?.push({
4857
+ severity: "warning",
4858
+ code: "routing.astar.grid_overflow",
4859
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
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
+ }
4868
+ });
4869
+ return null;
4870
+ }
4871
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4872
+ connectHorizontalEdges(
4873
+ nodesFull,
4874
+ ysFull,
4875
+ obstacles,
4876
+ endpointObstacles,
4877
+ margin
4878
+ );
4879
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4880
+ const pathFull = aStarSearch(
4881
+ nodesFull,
4882
+ idxFull,
4883
+ source,
4884
+ target,
4885
+ turnPenalty,
4886
+ segmentPenalty
4887
+ );
4888
+ if (pathFull === null) return null;
4889
+ return simplifyRoute(pathFull);
4809
4890
  }
4810
4891
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4811
4892
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -5026,20 +5107,83 @@ function areCollinear(a, b, c) {
5026
5107
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
5027
5108
  }
5028
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
+
5029
5168
  // src/routing/visibility-router.ts
5030
5169
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
5031
5170
  const margin = options.margin ?? 0;
5032
5171
  const turnPenalty = options.turnPenalty ?? 50;
5033
5172
  const segmentPenalty = options.segmentPenalty ?? 1;
5034
5173
  const endpointObstacles = options.endpointObstacles ?? [];
5035
- const maxCorners = options.maxCorners ?? 300;
5174
+ const maxCorners = options.maxCorners ?? 600;
5036
5175
  const vertices = collectCornerVertices(source, target, obstacles, margin);
5037
5176
  if (vertices.length > maxCorners) {
5038
5177
  diagnostics?.push({
5039
5178
  severity: "warning",
5040
5179
  code: "routing.visibility.corner_overflow",
5041
5180
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
5042
- 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
+ }
5043
5187
  });
5044
5188
  return null;
5045
5189
  }
@@ -5313,7 +5457,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5313
5457
  }
5314
5458
 
5315
5459
  // src/routing/routes.ts
5316
- function checkBacktracking(points, source, target, diagnostics) {
5460
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5317
5461
  if (points.length < 2) return;
5318
5462
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5319
5463
  if (direct <= 0) return;
@@ -5323,7 +5467,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5323
5467
  const b = points[i + 1];
5324
5468
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5325
5469
  }
5326
- const threshold = 10;
5470
+ const threshold = maxRatio ?? 20;
5327
5471
  if (routeLen > direct * threshold) {
5328
5472
  diagnostics.push({
5329
5473
  severity: "warning",
@@ -5341,8 +5485,20 @@ function routeEdge(input) {
5341
5485
  const diagnostics = [];
5342
5486
  const softObstacles = input.obstacles ?? [];
5343
5487
  const hardObstacles = input.hardObstacles ?? [];
5488
+ let bestRejectedPath;
5489
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5344
5490
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5345
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
+ };
5346
5502
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5347
5503
  const defaultAnchors = defaultAnchorsForGeometry(
5348
5504
  input.source.box,
@@ -5401,18 +5557,59 @@ function routeEdge(input) {
5401
5557
  input.source.center,
5402
5558
  targetAnchor
5403
5559
  );
5404
- const cornerPath = findCornerGraphPath(
5560
+ const allObstacles = [...softObstacles, ...hardObstacles];
5561
+ const corridorMargin = input.corridorMargin ?? 32;
5562
+ const corridorObstacles = filterObstaclesByCorridor(
5405
5563
  source,
5406
5564
  target,
5407
- [...softObstacles, ...hardObstacles],
5408
- { endpointObstacles, margin: 2 },
5565
+ allObstacles,
5566
+ [],
5567
+ // endpointObstacles passed separately via options
5568
+ corridorMargin
5569
+ );
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
+ );
5577
+ let cornerPath = findCornerGraphPath(
5578
+ source,
5579
+ target,
5580
+ cornerObstacles,
5581
+ {
5582
+ endpointObstacles,
5583
+ margin: 2,
5584
+ maxCorners: budget.maxCorners,
5585
+ corridorMargin
5586
+ },
5409
5587
  diagnostics
5410
5588
  );
5589
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
5590
+ cornerPath = findCornerGraphPath(
5591
+ source,
5592
+ target,
5593
+ allObstacles,
5594
+ {
5595
+ endpointObstacles,
5596
+ margin: 2,
5597
+ maxCorners: budget.maxCorners,
5598
+ corridorMargin
5599
+ },
5600
+ diagnostics
5601
+ );
5602
+ }
5411
5603
  const path = cornerPath ?? findObstacleFreePath(
5412
5604
  source,
5413
5605
  target,
5414
- [...softObstacles, ...hardObstacles],
5415
- { endpointObstacles, margin: 0 },
5606
+ allObstacles,
5607
+ {
5608
+ endpointObstacles,
5609
+ margin: 0,
5610
+ corridorMargin,
5611
+ maxNodes: budget.maxNodes
5612
+ },
5416
5613
  diagnostics
5417
5614
  );
5418
5615
  if (path !== null && path.length >= 2) {
@@ -5429,9 +5626,100 @@ function routeEdge(input) {
5429
5626
  softObstacles,
5430
5627
  softObstacleIndex
5431
5628
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5432
- checkBacktracking(finalized, source, target, diagnostics);
5629
+ checkBacktracking(
5630
+ finalized,
5631
+ source,
5632
+ target,
5633
+ diagnostics,
5634
+ input.maxBacktrackingRatio
5635
+ );
5433
5636
  return { points: finalized, diagnostics };
5434
5637
  }
5638
+ recordRejected(finalized);
5639
+ if (cornerPath !== null) {
5640
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5641
+ source,
5642
+ target,
5643
+ allObstacles,
5644
+ {
5645
+ endpointObstacles,
5646
+ margin: 2,
5647
+ maxCorners: budget.maxCorners,
5648
+ corridorMargin
5649
+ },
5650
+ diagnostics
5651
+ ) : null;
5652
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5653
+ const fullFinalized = finalizeRoute(
5654
+ fullCornerPath,
5655
+ softObstacles,
5656
+ hardObstacles,
5657
+ diagnostics,
5658
+ softObstacleIndex,
5659
+ hardObstacleIndex
5660
+ );
5661
+ if (!routeIntersectsObstacles(
5662
+ fullFinalized,
5663
+ softObstacles,
5664
+ softObstacleIndex
5665
+ ) && !routeIntersectsObstacles(
5666
+ fullFinalized,
5667
+ hardObstacles,
5668
+ hardObstacleIndex
5669
+ )) {
5670
+ checkBacktracking(
5671
+ fullFinalized,
5672
+ source,
5673
+ target,
5674
+ diagnostics,
5675
+ input.maxBacktrackingRatio
5676
+ );
5677
+ return { points: fullFinalized, diagnostics };
5678
+ }
5679
+ recordRejected(fullFinalized);
5680
+ }
5681
+ const gridPath = findObstacleFreePath(
5682
+ source,
5683
+ target,
5684
+ allObstacles,
5685
+ {
5686
+ endpointObstacles,
5687
+ margin: 0,
5688
+ corridorMargin,
5689
+ maxNodes: budget.maxNodes
5690
+ },
5691
+ diagnostics
5692
+ );
5693
+ if (gridPath !== null && gridPath.length >= 2) {
5694
+ const gridFinalized = finalizeRoute(
5695
+ gridPath,
5696
+ softObstacles,
5697
+ hardObstacles,
5698
+ diagnostics,
5699
+ softObstacleIndex,
5700
+ hardObstacleIndex
5701
+ );
5702
+ if (!routeIntersectsObstacles(
5703
+ gridFinalized,
5704
+ softObstacles,
5705
+ softObstacleIndex
5706
+ ) && !routeIntersectsObstacles(
5707
+ gridFinalized,
5708
+ hardObstacles,
5709
+ hardObstacleIndex
5710
+ )) {
5711
+ checkBacktracking(
5712
+ gridFinalized,
5713
+ source,
5714
+ target,
5715
+ diagnostics,
5716
+ input.maxBacktrackingRatio
5717
+ );
5718
+ return { points: gridFinalized, diagnostics };
5719
+ }
5720
+ recordRejected(gridFinalized);
5721
+ }
5722
+ }
5435
5723
  }
5436
5724
  }
5437
5725
  }
@@ -5493,7 +5781,8 @@ function routeEdge(input) {
5493
5781
  finalizedClean,
5494
5782
  candidate.points[0],
5495
5783
  candidate.points[candidate.points.length - 1],
5496
- diagnostics
5784
+ diagnostics,
5785
+ input.maxBacktrackingRatio
5497
5786
  );
5498
5787
  return { points: finalizedClean, diagnostics };
5499
5788
  }
@@ -5559,13 +5848,41 @@ function routeEdge(input) {
5559
5848
  code: "routing.obstacle.unavoidable",
5560
5849
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5561
5850
  });
5562
- return {
5563
- points: finalizeRoute(
5564
- 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,
5565
5861
  softObstacles,
5566
5862
  hardObstacles,
5567
5863
  diagnostics
5568
- ),
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,
5569
5886
  diagnostics
5570
5887
  };
5571
5888
  }
@@ -5597,6 +5914,22 @@ function routeEdge(input) {
5597
5914
  maxAttempts
5598
5915
  );
5599
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
+ }
5600
5933
  diagnostics.push({
5601
5934
  severity: "error",
5602
5935
  code: "routing.evidence.crossing_forbidden",
@@ -5644,13 +5977,41 @@ function routeEdge(input) {
5644
5977
  code: "routing.obstacle.unavoidable",
5645
5978
  message: "No bounded orthogonal route candidate avoided all obstacles."
5646
5979
  });
5647
- return {
5648
- points: finalizeRoute(
5649
- 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,
5650
5990
  softObstacles,
5651
5991
  hardObstacles,
5652
5992
  diagnostics
5653
- ),
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,
5654
6015
  diagnostics
5655
6016
  };
5656
6017
  }
@@ -6149,6 +6510,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6149
6510
  }
6150
6511
  return false;
6151
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
+ }
6152
6531
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6153
6532
  for (let index = 0; index < points.length - 1; index += 1) {
6154
6533
  const a = points[index];
@@ -6532,7 +6911,7 @@ function solveDiagram(diagram, options = {}) {
6532
6911
  edges: styledEdges
6533
6912
  });
6534
6913
  diagnostics.push(...layout2.diagnostics);
6535
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6914
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
6536
6915
  layout2.boxes,
6537
6916
  styledNodes,
6538
6917
  styledEdges,
@@ -6540,7 +6919,8 @@ function solveDiagram(diagram, options = {}) {
6540
6919
  options,
6541
6920
  diagnostics
6542
6921
  );
6543
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6922
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6923
+ const diagCountBefore = diagnostics.length;
6544
6924
  const rewrapped = wrapHorizontalStackIfNeeded(
6545
6925
  initialNodeBoxes,
6546
6926
  styledNodes,
@@ -6551,6 +6931,20 @@ function solveDiagram(diagram, options = {}) {
6551
6931
  for (const [id, box] of rewrapped) {
6552
6932
  initialNodeBoxes.set(id, box);
6553
6933
  }
6934
+ if (diagnostics.length > diagCountBefore) {
6935
+ for (const node of styledNodes) {
6936
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6937
+ const rwBox = rewrapped.get(node.id);
6938
+ const idx = styledNodes.indexOf(node);
6939
+ if (idx !== -1) {
6940
+ styledNodes[idx] = {
6941
+ ...node,
6942
+ position: { x: rwBox.x, y: rwBox.y }
6943
+ };
6944
+ }
6945
+ }
6946
+ }
6947
+ }
6554
6948
  }
6555
6949
  if (useRecursive && "groupBoxes" in layout2) {
6556
6950
  const recursiveLayout = layout2;
@@ -6564,7 +6958,7 @@ function solveDiagram(diagram, options = {}) {
6564
6958
  overlapSpacing: options?.overlapSpacing ?? 40,
6565
6959
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
6566
6960
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6567
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6961
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
6568
6962
  swimlanes: styledSwimlanes,
6569
6963
  boxes: initialNodeBoxes,
6570
6964
  nodes: styledNodes,
@@ -6579,7 +6973,8 @@ function solveDiagram(diagram, options = {}) {
6579
6973
  constrained.boxes,
6580
6974
  constrained.locks,
6581
6975
  options?.overlapSpacing ?? 40,
6582
- Math.max(0, options?.minLaneGutter ?? 0)
6976
+ Math.max(0, options?.minLaneGutter ?? 0),
6977
+ options.distributeContainedChildren ?? true
6583
6978
  );
6584
6979
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6585
6980
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6746,7 +7141,8 @@ function solveDiagram(diagram, options = {}) {
6746
7141
  diagram.direction,
6747
7142
  options,
6748
7143
  diagnostics,
6749
- coordinatedGroups
7144
+ coordinatedGroups,
7145
+ contentBounds
6750
7146
  );
6751
7147
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6752
7148
  coordinatedEdges,
@@ -7168,7 +7564,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7168
7564
  function containsCjk(value) {
7169
7565
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7170
7566
  }
7171
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7567
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7172
7568
  const layouts = /* @__PURE__ */ new Map();
7173
7569
  const diagnostics = [];
7174
7570
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7187,7 +7583,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7187
7583
  locks,
7188
7584
  diagnostics,
7189
7585
  movedChildIds,
7190
- laneGutter
7586
+ laneGutter,
7587
+ constraints,
7588
+ distributeContainedChildren
7191
7589
  );
7192
7590
  if (layout2 !== void 0) {
7193
7591
  layouts.set(swimlane.id, layout2);
@@ -7283,9 +7681,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
7283
7681
  if (!isStackRunaway(boxes, nodes, direction, options)) {
7284
7682
  return new Map(boxes);
7285
7683
  }
7286
- const maxRowDepth = options.maxRowDepth;
7287
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7288
- return new Map(boxes);
7684
+ let maxRowDepth = options.maxRowDepth;
7685
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
7686
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
7687
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
7688
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
7689
+ } else {
7690
+ return new Map(boxes);
7691
+ }
7289
7692
  }
7290
7693
  const ordered = [...nodes].sort((a, b) => {
7291
7694
  const ba = boxes.get(a.id);
@@ -7346,10 +7749,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
7346
7749
  });
7347
7750
  }
7348
7751
  function isStackRunaway(boxes, nodes, direction, options) {
7349
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
7752
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
7350
7753
  return false;
7351
7754
  }
7352
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
7755
+ if (nodes.length < 2) {
7353
7756
  return false;
7354
7757
  }
7355
7758
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -7357,17 +7760,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
7357
7760
  return false;
7358
7761
  }
7359
7762
  const bounds = unionBoxes(nodeBoxes);
7360
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7361
- const preferred = options.preferredAspectRatio ?? 3;
7362
- if (aspectRatio < preferred) {
7763
+ const isHorizontal = direction === "TB" || direction === "BT";
7764
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7765
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
7766
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
7363
7767
  return false;
7364
7768
  }
7769
+ if (isHorizontal) {
7770
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
7771
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
7772
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
7773
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
7774
+ }
7365
7775
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
7366
7776
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
7367
7777
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7368
7778
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7369
7779
  }
7370
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7780
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7371
7781
  const headerHeight = swimlane.headerHeight ?? 28;
7372
7782
  const padding = swimlane.padding ?? 16;
7373
7783
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7392,7 +7802,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7392
7802
  locks,
7393
7803
  diagnostics,
7394
7804
  movedChildIds,
7395
- laneGutter
7805
+ laneGutter,
7806
+ constraints,
7807
+ distributeContainedChildren
7396
7808
  );
7397
7809
  }
7398
7810
  return applyHorizontalSwimlaneContract(
@@ -7407,13 +7819,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7407
7819
  laneGutter
7408
7820
  );
7409
7821
  }
7410
- 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) {
7411
7823
  const populatedBounds = laneBounds.filter(
7412
7824
  (box) => box !== void 0
7413
7825
  );
7414
7826
  const top = Math.min(...populatedBounds.map((box) => box.y));
7415
7827
  const left = Math.min(...populatedBounds.map((box) => box.x));
7416
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
+ }
7417
7845
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7418
7846
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7419
7847
  const rankStackGap = Math.max(8, padding / 2);
@@ -7425,7 +7853,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7425
7853
  );
7426
7854
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
7427
7855
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
7428
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
7856
+ const spreadWidth = maxCrossAxisSpreadWidth(
7857
+ swimlane,
7858
+ nodeBoxes,
7859
+ flowRanks,
7860
+ locks,
7861
+ rankStackGap,
7862
+ containedChildIds
7863
+ );
7864
+ const slotWidth = Math.max(
7865
+ Math.max(...populatedBounds.map((box) => box.width)),
7866
+ spreadWidth
7867
+ ) + padding * 2;
7429
7868
  const laneStep = slotWidth + laneGutter;
7430
7869
  const laneContentTop = top + headerHeight + padding;
7431
7870
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -7439,6 +7878,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7439
7878
  y: laneContentTop
7440
7879
  };
7441
7880
  if (maxRank === 0) {
7881
+ const distributable = lane.children.filter(
7882
+ (childId) => !locks.has(childId)
7883
+ );
7884
+ const coveredByContainment = lane.children.some(
7885
+ (childId) => containedChildIds.has(childId)
7886
+ );
7887
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7888
+ moveRankedVerticalLaneChildren(
7889
+ lane.children,
7890
+ nodeBoxes,
7891
+ locks,
7892
+ diagnostics,
7893
+ movedChildIds,
7894
+ flowRanks,
7895
+ rankSpacing,
7896
+ rankStackGap,
7897
+ { x: target.x, y: laneContentTop },
7898
+ slotWidth - padding * 2
7899
+ );
7900
+ continue;
7901
+ }
7442
7902
  moveLaneChildren(
7443
7903
  lane.children,
7444
7904
  nodeBoxes,
@@ -7452,6 +7912,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7452
7912
  );
7453
7913
  continue;
7454
7914
  }
7915
+ const rankedCoveredByContainment = lane.children.some(
7916
+ (childId) => containedChildIds.has(childId)
7917
+ );
7455
7918
  moveRankedVerticalLaneChildren(
7456
7919
  lane.children,
7457
7920
  nodeBoxes,
@@ -7461,10 +7924,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7461
7924
  flowRanks,
7462
7925
  rankSpacing,
7463
7926
  rankStackGap,
7464
- {
7465
- x: target.x - bounds.x,
7466
- y: laneContentTop
7467
- }
7927
+ { x: target.x, y: laneContentTop },
7928
+ slotWidth - padding * 2,
7929
+ rankedCoveredByContainment
7468
7930
  );
7469
7931
  }
7470
7932
  return {
@@ -7573,31 +8035,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
7573
8035
  }
7574
8036
  return maxHeight;
7575
8037
  }
7576
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
8038
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
8039
+ function crossAxisSpreadWidth(items, gap) {
8040
+ return items.reduce(
8041
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
8042
+ 0
8043
+ );
8044
+ }
8045
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
8046
+ let maxWidth = 0;
8047
+ for (const lane of swimlane.lanes) {
8048
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
8049
+ continue;
8050
+ }
8051
+ for (const stack of rankStacks(
8052
+ lane.children,
8053
+ nodeBoxes,
8054
+ flowRanks
8055
+ ).values()) {
8056
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
8057
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
8058
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
8059
+ }
8060
+ }
8061
+ return maxWidth;
8062
+ }
8063
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7577
8064
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7578
- let yOffset = 0;
8065
+ const unlocked = [];
7579
8066
  for (const item of stack) {
7580
- const { childId, box } = item;
7581
- if (locks.has(childId)) {
8067
+ if (locks.has(item.childId)) {
7582
8068
  diagnostics.push({
7583
8069
  severity: "warning",
7584
8070
  code: "constraints.locked-target-not-moved",
7585
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
8071
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
7586
8072
  path: ["swimlanes"],
7587
- detail: { nodeId: childId }
8073
+ detail: { nodeId: item.childId }
7588
8074
  });
7589
- continue;
8075
+ } else {
8076
+ unlocked.push(item);
7590
8077
  }
8078
+ }
8079
+ if (unlocked.length === 0) continue;
8080
+ if (unlocked.length === 1) {
8081
+ const { childId, box } = unlocked[0];
7591
8082
  const next = {
7592
8083
  ...box,
7593
- x: box.x + target.x,
7594
- y: target.y + rank * rankSpacing + yOffset
8084
+ x: target.x + (contentWidth - box.width) / 2,
8085
+ y: target.y + rank * rankSpacing
7595
8086
  };
7596
8087
  if (next.x !== box.x || next.y !== box.y) {
7597
8088
  movedChildIds.add(childId);
7598
8089
  }
7599
8090
  nodeBoxes.set(childId, next);
7600
- yOffset += box.height + rankStackGap;
8091
+ } else {
8092
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
8093
+ if (!shouldSpread) {
8094
+ let yOffset = 0;
8095
+ for (const { childId, box } of unlocked) {
8096
+ const next = {
8097
+ ...box,
8098
+ x: target.x + (contentWidth - box.width) / 2,
8099
+ y: target.y + rank * rankSpacing + yOffset
8100
+ };
8101
+ if (next.x !== box.x || next.y !== box.y) {
8102
+ movedChildIds.add(childId);
8103
+ }
8104
+ nodeBoxes.set(childId, next);
8105
+ yOffset += box.height + rankStackGap;
8106
+ }
8107
+ } else {
8108
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
8109
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
8110
+ for (const { childId, box } of unlocked) {
8111
+ const next = {
8112
+ ...box,
8113
+ x: xCursor,
8114
+ y: target.y + rank * rankSpacing
8115
+ };
8116
+ if (next.x !== box.x || next.y !== box.y) {
8117
+ movedChildIds.add(childId);
8118
+ }
8119
+ nodeBoxes.set(childId, next);
8120
+ xCursor += box.width + rankStackGap;
8121
+ }
8122
+ diagnostics.push({
8123
+ severity: "info",
8124
+ code: "swimlane_contract.cross_axis_distributed",
8125
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
8126
+ path: ["swimlanes"],
8127
+ detail: {
8128
+ rank,
8129
+ childCount: unlocked.length,
8130
+ contentWidth
8131
+ }
8132
+ });
8133
+ }
7601
8134
  }
7602
8135
  }
7603
8136
  }
@@ -7936,7 +8469,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7936
8469
  });
7937
8470
  continue;
7938
8471
  }
7939
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
8472
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7940
8473
  const geometry = computeShapeGeometry({
7941
8474
  shape: node.shape,
7942
8475
  box,
@@ -8030,7 +8563,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
8030
8563
  }
8031
8564
  }
8032
8565
  }
8033
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8566
+ function coordinatePorts(node, nodeBox, portShifting) {
8034
8567
  const portsBySide = /* @__PURE__ */ new Map();
8035
8568
  for (const port of node.ports ?? []) {
8036
8569
  const ports = portsBySide.get(port.side) ?? [];
@@ -8053,9 +8586,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8053
8586
  side,
8054
8587
  index,
8055
8588
  sorted.length,
8056
- portShifting,
8057
- diagnostics,
8058
- node.id
8589
+ portShifting
8059
8590
  );
8060
8591
  const box = portBox(anchor);
8061
8592
  coordinated.push({ ...port, box, anchor });
@@ -8063,32 +8594,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8063
8594
  }
8064
8595
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
8065
8596
  }
8066
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
8597
+ function portAnchor(nodeBox, side, index, count, portShifting) {
8067
8598
  const shiftingEnabled = portShifting?.enabled ?? true;
8068
8599
  const requestedSpacing = portShifting?.spacing ?? 24;
8069
8600
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
8070
8601
  const availableSpan = 2 * maxOffset;
8071
8602
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
8072
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
8603
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
8073
8604
  Math.min(requestedSpacing, availableSpan / (count - 1)),
8074
8605
  minSpacing
8075
8606
  ) : requestedSpacing;
8076
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
8077
- diagnostics.push({
8078
- severity: "warning",
8079
- code: "port_constraint_overlap",
8080
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
8081
- path: ["nodes", nodeId, "ports"],
8082
- detail: {
8083
- nodeId,
8084
- side,
8085
- requestedSpacing,
8086
- effectiveSpacing: Math.round(effectiveSpacing),
8087
- portCount: count
8088
- }
8089
- });
8090
- }
8091
- const spacing = effectiveSpacing;
8092
8607
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
8093
8608
  switch (side) {
8094
8609
  case "left":
@@ -8703,14 +9218,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8703
9218
  }
8704
9219
  };
8705
9220
  }
8706
- 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) {
8707
9222
  const coordinated = [];
8708
9223
  const coordinatedNodeById = new Map(
8709
9224
  coordinatedNodes.map((node) => [node.id, node])
8710
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;
8711
9233
  const nodeObstacleIndex = createBoxSpatialIndex(
8712
9234
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8713
- options.routingGutter ?? 160
9235
+ queryGutter
8714
9236
  );
8715
9237
  for (const edge of edges) {
8716
9238
  const source = nodes.get(edge.source.nodeId);
@@ -8732,11 +9254,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8732
9254
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8733
9255
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8734
9256
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8735
- const corridor = edgeCorridorBox(
8736
- source.box,
8737
- target.box,
8738
- options.routingGutter ?? 160
8739
- );
9257
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8740
9258
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8741
9259
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8742
9260
  );
@@ -8754,7 +9272,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8754
9272
  ...routeTextObstacles
8755
9273
  ],
8756
9274
  hardObstacles,
8757
- ...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 }
8758
9278
  });
8759
9279
  diagnostics.push(
8760
9280
  ...route.diagnostics.map((diagnostic) => ({