@crazyhappyone/auto-graph 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4164,7 +4164,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4164
4164
  detail: {
4165
4165
  xsCount: xs.length,
4166
4166
  ysCount: ys.length,
4167
- maxNodes
4167
+ maxNodes,
4168
+ obstacleCount: obstacles.length,
4169
+ stage: "corridor-filtered",
4170
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4168
4171
  }
4169
4172
  });
4170
4173
  return null;
@@ -4210,7 +4213,14 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4210
4213
  severity: "warning",
4211
4214
  code: "routing.astar.grid_overflow",
4212
4215
  message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4213
- detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4216
+ detail: {
4217
+ xsCount: xsFull.length,
4218
+ ysCount: ysFull.length,
4219
+ maxNodes,
4220
+ obstacleCount: obstacles.length,
4221
+ stage: "full-retry",
4222
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4223
+ }
4214
4224
  });
4215
4225
  return null;
4216
4226
  }
@@ -4453,6 +4463,64 @@ function areCollinear(a, b, c) {
4453
4463
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4454
4464
  }
4455
4465
 
4466
+ // src/routing/budget.ts
4467
+ var MIN_CORNER_BUDGET = 600;
4468
+ var MAX_CORNER_BUDGET = 3e3;
4469
+ var MIN_NODE_BUDGET = 4e3;
4470
+ var MAX_NODE_BUDGET = 64e3;
4471
+ var CORNERS_PER_OBSTACLE = 12;
4472
+ var CORNER_HEADROOM = 2;
4473
+ var GRID_SAFETY_FACTOR = 3;
4474
+ var CORRIDOR_SCALING_K = 0.5;
4475
+ var CORRIDOR_SCALING_BASE = 200;
4476
+ function computeRoutingBudget(cornerObstacles, allObstacles, corridorMargin, overrides = {}) {
4477
+ const adaptiveMaxCorners = deriveMaxCorners(
4478
+ cornerObstacles.length,
4479
+ corridorMargin
4480
+ );
4481
+ const adaptiveMaxNodes = deriveMaxNodes(
4482
+ allObstacles.length,
4483
+ corridorMargin
4484
+ );
4485
+ return {
4486
+ maxCorners: resolveBudget(
4487
+ overrides.maxCorners,
4488
+ adaptiveMaxCorners,
4489
+ MIN_CORNER_BUDGET,
4490
+ MAX_CORNER_BUDGET
4491
+ ),
4492
+ maxNodes: resolveBudget(
4493
+ overrides.maxNodes,
4494
+ adaptiveMaxNodes,
4495
+ MIN_NODE_BUDGET,
4496
+ MAX_NODE_BUDGET
4497
+ ),
4498
+ cornerObstacleCount: cornerObstacles.length,
4499
+ gridObstacleCount: allObstacles.length,
4500
+ corridorMargin
4501
+ };
4502
+ }
4503
+ function deriveMaxCorners(obstacleCount, corridorMargin) {
4504
+ const base = 2 + obstacleCount * CORNERS_PER_OBSTACLE * CORNER_HEADROOM;
4505
+ const corridorFactor = corridorScalingFactor(corridorMargin);
4506
+ return Math.ceil(base * corridorFactor);
4507
+ }
4508
+ function deriveMaxNodes(obstacleCount, corridorMargin) {
4509
+ const base = 4 * obstacleCount * obstacleCount + 4 * obstacleCount + 100;
4510
+ const corridorFactor = corridorScalingFactor(corridorMargin);
4511
+ return Math.ceil(base * GRID_SAFETY_FACTOR * corridorFactor);
4512
+ }
4513
+ function corridorScalingFactor(corridorMargin) {
4514
+ return 1 + corridorMargin / CORRIDOR_SCALING_BASE * CORRIDOR_SCALING_K;
4515
+ }
4516
+ function resolveBudget(override, adaptive, min, max) {
4517
+ const chosen = override !== void 0 && Number.isFinite(override) && override >= 1 ? override : adaptive;
4518
+ return clamp(chosen, min, max);
4519
+ }
4520
+ function clamp(value, min, max) {
4521
+ return Math.max(min, Math.min(max, value));
4522
+ }
4523
+
4456
4524
  // src/routing/visibility-router.ts
4457
4525
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
4458
4526
  const margin = options.margin ?? 0;
@@ -4466,7 +4534,12 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
4466
4534
  severity: "warning",
4467
4535
  code: "routing.visibility.corner_overflow",
4468
4536
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
4469
- detail: { vertexCount: vertices.length, maxCorners }
4537
+ detail: {
4538
+ vertexCount: vertices.length,
4539
+ maxCorners,
4540
+ obstacleCount: obstacles.length,
4541
+ ...options.corridorMargin === void 0 ? {} : { corridorMargin: options.corridorMargin }
4542
+ }
4470
4543
  });
4471
4544
  return null;
4472
4545
  }
@@ -4740,7 +4813,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4740
4813
  }
4741
4814
 
4742
4815
  // src/routing/routes.ts
4743
- function checkBacktracking(points, source, target, diagnostics) {
4816
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4744
4817
  if (points.length < 2) return;
4745
4818
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4746
4819
  if (direct <= 0) return;
@@ -4750,7 +4823,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4750
4823
  const b = points[i + 1];
4751
4824
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4752
4825
  }
4753
- const threshold = 10;
4826
+ const threshold = maxRatio ?? 20;
4754
4827
  if (routeLen > direct * threshold) {
4755
4828
  diagnostics.push({
4756
4829
  severity: "warning",
@@ -4768,8 +4841,20 @@ function routeEdge(input) {
4768
4841
  const diagnostics = [];
4769
4842
  const softObstacles = input.obstacles ?? [];
4770
4843
  const hardObstacles = input.hardObstacles ?? [];
4844
+ let bestRejectedPath;
4845
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4771
4846
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4772
4847
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
4848
+ const recordRejected = (candidate) => {
4849
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
4850
+ return;
4851
+ }
4852
+ const crossings = countObstacleCrossings(candidate, softObstacles);
4853
+ if (crossings < bestRejectedCrossings) {
4854
+ bestRejectedCrossings = crossings;
4855
+ bestRejectedPath = candidate;
4856
+ }
4857
+ };
4773
4858
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4774
4859
  const defaultAnchors = defaultAnchorsForGeometry(
4775
4860
  input.source.box,
@@ -4829,20 +4914,32 @@ function routeEdge(input) {
4829
4914
  targetAnchor
4830
4915
  );
4831
4916
  const allObstacles = [...softObstacles, ...hardObstacles];
4917
+ const corridorMargin = input.corridorMargin ?? 32;
4832
4918
  const corridorObstacles = filterObstaclesByCorridor(
4833
4919
  source,
4834
4920
  target,
4835
4921
  allObstacles,
4836
4922
  [],
4837
4923
  // endpointObstacles passed separately via options
4838
- 32
4924
+ corridorMargin
4839
4925
  );
4840
4926
  const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4927
+ const budget = computeRoutingBudget(
4928
+ cornerObstacles,
4929
+ allObstacles,
4930
+ corridorMargin,
4931
+ { maxCorners: input.maxCorners, maxNodes: input.maxNodes }
4932
+ );
4841
4933
  let cornerPath = findCornerGraphPath(
4842
4934
  source,
4843
4935
  target,
4844
4936
  cornerObstacles,
4845
- { endpointObstacles, margin: 2 },
4937
+ {
4938
+ endpointObstacles,
4939
+ margin: 2,
4940
+ maxCorners: budget.maxCorners,
4941
+ corridorMargin
4942
+ },
4846
4943
  diagnostics
4847
4944
  );
4848
4945
  if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
@@ -4850,7 +4947,12 @@ function routeEdge(input) {
4850
4947
  source,
4851
4948
  target,
4852
4949
  allObstacles,
4853
- { endpointObstacles, margin: 2 },
4950
+ {
4951
+ endpointObstacles,
4952
+ margin: 2,
4953
+ maxCorners: budget.maxCorners,
4954
+ corridorMargin
4955
+ },
4854
4956
  diagnostics
4855
4957
  );
4856
4958
  }
@@ -4858,7 +4960,12 @@ function routeEdge(input) {
4858
4960
  source,
4859
4961
  target,
4860
4962
  allObstacles,
4861
- { endpointObstacles, margin: 0 },
4963
+ {
4964
+ endpointObstacles,
4965
+ margin: 0,
4966
+ corridorMargin,
4967
+ maxNodes: budget.maxNodes
4968
+ },
4862
4969
  diagnostics
4863
4970
  );
4864
4971
  if (path !== null && path.length >= 2) {
@@ -4875,15 +4982,27 @@ function routeEdge(input) {
4875
4982
  softObstacles,
4876
4983
  softObstacleIndex
4877
4984
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4878
- checkBacktracking(finalized, source, target, diagnostics);
4985
+ checkBacktracking(
4986
+ finalized,
4987
+ source,
4988
+ target,
4989
+ diagnostics,
4990
+ input.maxBacktrackingRatio
4991
+ );
4879
4992
  return { points: finalized, diagnostics };
4880
4993
  }
4994
+ recordRejected(finalized);
4881
4995
  if (cornerPath !== null) {
4882
4996
  const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4883
4997
  source,
4884
4998
  target,
4885
4999
  allObstacles,
4886
- { endpointObstacles, margin: 2 },
5000
+ {
5001
+ endpointObstacles,
5002
+ margin: 2,
5003
+ maxCorners: budget.maxCorners,
5004
+ corridorMargin
5005
+ },
4887
5006
  diagnostics
4888
5007
  ) : null;
4889
5008
  if (fullCornerPath !== null && fullCornerPath.length >= 2) {
@@ -4904,15 +5023,27 @@ function routeEdge(input) {
4904
5023
  hardObstacles,
4905
5024
  hardObstacleIndex
4906
5025
  )) {
4907
- checkBacktracking(fullFinalized, source, target, diagnostics);
5026
+ checkBacktracking(
5027
+ fullFinalized,
5028
+ source,
5029
+ target,
5030
+ diagnostics,
5031
+ input.maxBacktrackingRatio
5032
+ );
4908
5033
  return { points: fullFinalized, diagnostics };
4909
5034
  }
5035
+ recordRejected(fullFinalized);
4910
5036
  }
4911
5037
  const gridPath = findObstacleFreePath(
4912
5038
  source,
4913
5039
  target,
4914
5040
  allObstacles,
4915
- { endpointObstacles, margin: 0 },
5041
+ {
5042
+ endpointObstacles,
5043
+ margin: 0,
5044
+ corridorMargin,
5045
+ maxNodes: budget.maxNodes
5046
+ },
4916
5047
  diagnostics
4917
5048
  );
4918
5049
  if (gridPath !== null && gridPath.length >= 2) {
@@ -4933,9 +5064,16 @@ function routeEdge(input) {
4933
5064
  hardObstacles,
4934
5065
  hardObstacleIndex
4935
5066
  )) {
4936
- checkBacktracking(gridFinalized, source, target, diagnostics);
5067
+ checkBacktracking(
5068
+ gridFinalized,
5069
+ source,
5070
+ target,
5071
+ diagnostics,
5072
+ input.maxBacktrackingRatio
5073
+ );
4937
5074
  return { points: gridFinalized, diagnostics };
4938
5075
  }
5076
+ recordRejected(gridFinalized);
4939
5077
  }
4940
5078
  }
4941
5079
  }
@@ -4999,7 +5137,8 @@ function routeEdge(input) {
4999
5137
  finalizedClean,
5000
5138
  candidate.points[0],
5001
5139
  candidate.points[candidate.points.length - 1],
5002
- diagnostics
5140
+ diagnostics,
5141
+ input.maxBacktrackingRatio
5003
5142
  );
5004
5143
  return { points: finalizedClean, diagnostics };
5005
5144
  }
@@ -5065,13 +5204,41 @@ function routeEdge(input) {
5065
5204
  code: "routing.obstacle.unavoidable",
5066
5205
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5067
5206
  });
5068
- return {
5069
- points: finalizeRoute(
5070
- bestPoints2,
5207
+ const finalizedSoftBest = finalizeRoute(
5208
+ bestPoints2,
5209
+ softObstacles,
5210
+ hardObstacles,
5211
+ diagnostics
5212
+ );
5213
+ let softFallback = finalizedSoftBest;
5214
+ if (bestRejectedPath !== void 0) {
5215
+ const finalizedRejected = finalizeRoute(
5216
+ bestRejectedPath,
5071
5217
  softObstacles,
5072
5218
  hardObstacles,
5073
5219
  diagnostics
5074
- ),
5220
+ );
5221
+ const rejectedCrossings = countObstacleCrossings(
5222
+ finalizedRejected,
5223
+ softObstacles
5224
+ );
5225
+ const heuristicCrossings = countObstacleCrossings(
5226
+ finalizedSoftBest,
5227
+ softObstacles
5228
+ );
5229
+ if (rejectedCrossings < heuristicCrossings) {
5230
+ softFallback = finalizedRejected;
5231
+ }
5232
+ }
5233
+ checkBacktracking(
5234
+ softFallback,
5235
+ softFallback[0],
5236
+ softFallback[softFallback.length - 1],
5237
+ diagnostics,
5238
+ input.maxBacktrackingRatio
5239
+ );
5240
+ return {
5241
+ points: softFallback,
5075
5242
  diagnostics
5076
5243
  };
5077
5244
  }
@@ -5103,6 +5270,22 @@ function routeEdge(input) {
5103
5270
  maxAttempts
5104
5271
  );
5105
5272
  }
5273
+ if (bestRejectedPath !== void 0) {
5274
+ diagnostics.push({
5275
+ severity: "warning",
5276
+ code: "routing.obstacle.unavoidable",
5277
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5278
+ });
5279
+ return {
5280
+ points: finalizeRoute(
5281
+ bestRejectedPath,
5282
+ softObstacles,
5283
+ hardObstacles,
5284
+ diagnostics
5285
+ ),
5286
+ diagnostics
5287
+ };
5288
+ }
5106
5289
  diagnostics.push({
5107
5290
  severity: "error",
5108
5291
  code: "routing.evidence.crossing_forbidden",
@@ -5150,13 +5333,41 @@ function routeEdge(input) {
5150
5333
  code: "routing.obstacle.unavoidable",
5151
5334
  message: "No bounded orthogonal route candidate avoided all obstacles."
5152
5335
  });
5153
- return {
5154
- points: finalizeRoute(
5155
- bestPoints,
5336
+ const finalizedBestPoints = finalizeRoute(
5337
+ bestPoints,
5338
+ softObstacles,
5339
+ hardObstacles,
5340
+ diagnostics
5341
+ );
5342
+ let fallbackPoints = finalizedBestPoints;
5343
+ if (bestRejectedPath !== void 0) {
5344
+ const finalizedRejected = finalizeRoute(
5345
+ bestRejectedPath,
5156
5346
  softObstacles,
5157
5347
  hardObstacles,
5158
5348
  diagnostics
5159
- ),
5349
+ );
5350
+ const rejectedCrossings = countObstacleCrossings(
5351
+ finalizedRejected,
5352
+ softObstacles
5353
+ );
5354
+ const heuristicCrossings = countObstacleCrossings(
5355
+ finalizedBestPoints,
5356
+ softObstacles
5357
+ );
5358
+ if (rejectedCrossings < heuristicCrossings) {
5359
+ fallbackPoints = finalizedRejected;
5360
+ }
5361
+ }
5362
+ checkBacktracking(
5363
+ fallbackPoints,
5364
+ fallbackPoints[0],
5365
+ fallbackPoints[fallbackPoints.length - 1],
5366
+ diagnostics,
5367
+ input.maxBacktrackingRatio
5368
+ );
5369
+ return {
5370
+ points: fallbackPoints,
5160
5371
  diagnostics
5161
5372
  };
5162
5373
  }
@@ -5655,6 +5866,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5655
5866
  }
5656
5867
  return false;
5657
5868
  }
5869
+ function countObstacleCrossings(points, obstacles) {
5870
+ let count = 0;
5871
+ for (const obstacle of obstacles) {
5872
+ validateBox(obstacle);
5873
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
5874
+ const a = points[pointIndex];
5875
+ const b = points[pointIndex + 1];
5876
+ if (a === void 0 || b === void 0) {
5877
+ continue;
5878
+ }
5879
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
5880
+ count += 1;
5881
+ break;
5882
+ }
5883
+ }
5884
+ }
5885
+ return count;
5886
+ }
5658
5887
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5659
5888
  for (let index = 0; index < points.length - 1; index += 1) {
5660
5889
  const a = points[index];
@@ -5889,7 +6118,8 @@ function solveDiagram(diagram, options = {}) {
5889
6118
  constrained.boxes,
5890
6119
  constrained.locks,
5891
6120
  options?.overlapSpacing ?? 40,
5892
- Math.max(0, options?.minLaneGutter ?? 0)
6121
+ Math.max(0, options?.minLaneGutter ?? 0),
6122
+ options.distributeContainedChildren ?? true
5893
6123
  );
5894
6124
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5895
6125
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6056,7 +6286,8 @@ function solveDiagram(diagram, options = {}) {
6056
6286
  diagram.direction,
6057
6287
  options,
6058
6288
  diagnostics,
6059
- coordinatedGroups
6289
+ coordinatedGroups,
6290
+ contentBounds
6060
6291
  );
6061
6292
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6062
6293
  coordinatedEdges,
@@ -6475,7 +6706,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6475
6706
  function containsCjk(value) {
6476
6707
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6477
6708
  }
6478
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6709
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6479
6710
  const layouts = /* @__PURE__ */ new Map();
6480
6711
  const diagnostics = [];
6481
6712
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6494,7 +6725,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6494
6725
  locks,
6495
6726
  diagnostics,
6496
6727
  movedChildIds,
6497
- laneGutter
6728
+ laneGutter,
6729
+ constraints,
6730
+ distributeContainedChildren
6498
6731
  );
6499
6732
  if (layout2 !== void 0) {
6500
6733
  layouts.set(swimlane.id, layout2);
@@ -6686,7 +6919,7 @@ function isStackRunaway(boxes, nodes, direction, options) {
6686
6919
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6687
6920
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6688
6921
  }
6689
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6922
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6690
6923
  const headerHeight = swimlane.headerHeight ?? 28;
6691
6924
  const padding = swimlane.padding ?? 16;
6692
6925
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6711,7 +6944,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6711
6944
  locks,
6712
6945
  diagnostics,
6713
6946
  movedChildIds,
6714
- laneGutter
6947
+ laneGutter,
6948
+ constraints,
6949
+ distributeContainedChildren
6715
6950
  );
6716
6951
  }
6717
6952
  return applyHorizontalSwimlaneContract(
@@ -6726,13 +6961,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6726
6961
  laneGutter
6727
6962
  );
6728
6963
  }
6729
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
6964
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6730
6965
  const populatedBounds = laneBounds.filter(
6731
6966
  (box) => box !== void 0
6732
6967
  );
6733
6968
  const top = Math.min(...populatedBounds.map((box) => box.y));
6734
6969
  const left = Math.min(...populatedBounds.map((box) => box.x));
6735
6970
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
6971
+ const containedChildIds = /* @__PURE__ */ new Set();
6972
+ if (distributeContainedChildren) {
6973
+ for (const c of constraints) {
6974
+ if (c.kind !== "containment") continue;
6975
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
6976
+ const distributable = c.childIds.filter((childId) => {
6977
+ if (nodeBoxes.get(childId) === void 0) return false;
6978
+ const lock = locks.get(childId);
6979
+ return lock === void 0 || lock.source === "fixed-position";
6980
+ });
6981
+ if (distributable.length < 2) continue;
6982
+ for (const childId of distributable) {
6983
+ containedChildIds.add(childId);
6984
+ }
6985
+ }
6986
+ }
6736
6987
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6737
6988
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6738
6989
  const rankStackGap = Math.max(8, padding / 2);
@@ -6749,7 +7000,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6749
7000
  nodeBoxes,
6750
7001
  flowRanks,
6751
7002
  locks,
6752
- rankStackGap
7003
+ rankStackGap,
7004
+ containedChildIds
6753
7005
  );
6754
7006
  const slotWidth = Math.max(
6755
7007
  Math.max(...populatedBounds.map((box) => box.width)),
@@ -6771,7 +7023,10 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6771
7023
  const distributable = lane.children.filter(
6772
7024
  (childId) => !locks.has(childId)
6773
7025
  );
6774
- if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7026
+ const coveredByContainment = lane.children.some(
7027
+ (childId) => containedChildIds.has(childId)
7028
+ );
7029
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6775
7030
  moveRankedVerticalLaneChildren(
6776
7031
  lane.children,
6777
7032
  nodeBoxes,
@@ -6799,6 +7054,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6799
7054
  );
6800
7055
  continue;
6801
7056
  }
7057
+ const rankedCoveredByContainment = lane.children.some(
7058
+ (childId) => containedChildIds.has(childId)
7059
+ );
6802
7060
  moveRankedVerticalLaneChildren(
6803
7061
  lane.children,
6804
7062
  nodeBoxes,
@@ -6809,7 +7067,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6809
7067
  rankSpacing,
6810
7068
  rankStackGap,
6811
7069
  { x: target.x, y: laneContentTop },
6812
- slotWidth - padding * 2
7070
+ slotWidth - padding * 2,
7071
+ rankedCoveredByContainment
6813
7072
  );
6814
7073
  }
6815
7074
  return {
@@ -6925,9 +7184,12 @@ function crossAxisSpreadWidth(items, gap) {
6925
7184
  0
6926
7185
  );
6927
7186
  }
6928
- function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7187
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
6929
7188
  let maxWidth = 0;
6930
7189
  for (const lane of swimlane.lanes) {
7190
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7191
+ continue;
7192
+ }
6931
7193
  for (const stack of rankStacks(
6932
7194
  lane.children,
6933
7195
  nodeBoxes,
@@ -6940,7 +7202,7 @@ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
6940
7202
  }
6941
7203
  return maxWidth;
6942
7204
  }
6943
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
7205
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6944
7206
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6945
7207
  const unlocked = [];
6946
7208
  for (const item of stack) {
@@ -6969,7 +7231,7 @@ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics,
6969
7231
  }
6970
7232
  nodeBoxes.set(childId, next);
6971
7233
  } else {
6972
- const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7234
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
6973
7235
  if (!shouldSpread) {
6974
7236
  let yOffset = 0;
6975
7237
  for (const { childId, box } of unlocked) {
@@ -8098,14 +8360,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8098
8360
  }
8099
8361
  };
8100
8362
  }
8101
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
8363
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8102
8364
  const coordinated = [];
8103
8365
  const coordinatedNodeById = new Map(
8104
8366
  coordinatedNodes.map((node) => [node.id, node])
8105
8367
  );
8368
+ const corridorMarginOption = options.corridorMargin ?? "auto";
8369
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
8370
+ 200,
8371
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
8372
+ );
8373
+ const routingGutter = options.routingGutter ?? 160;
8374
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8106
8375
  const nodeObstacleIndex = createBoxSpatialIndex(
8107
8376
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8108
- options.routingGutter ?? 160
8377
+ queryGutter
8109
8378
  );
8110
8379
  for (const edge of edges) {
8111
8380
  const source = nodes.get(edge.source.nodeId);
@@ -8127,11 +8396,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8127
8396
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8128
8397
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8129
8398
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8130
- const corridor = edgeCorridorBox(
8131
- source.box,
8132
- target.box,
8133
- options.routingGutter ?? 160
8134
- );
8399
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8135
8400
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8136
8401
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8137
8402
  );
@@ -8149,7 +8414,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8149
8414
  ...routeTextObstacles
8150
8415
  ],
8151
8416
  hardObstacles,
8152
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
8417
+ corridorMargin,
8418
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
8419
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
8153
8420
  });
8154
8421
  diagnostics.push(
8155
8422
  ...route.diagnostics.map((diagnostic) => ({