@crazyhappyone/auto-graph 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -4161,7 +4161,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4161
4161
  detail: {
4162
4162
  xsCount: xs.length,
4163
4163
  ysCount: ys.length,
4164
- maxNodes
4164
+ maxNodes,
4165
+ obstacleCount: obstacles.length,
4166
+ stage: "corridor-filtered",
4167
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4165
4168
  }
4166
4169
  });
4167
4170
  return null;
@@ -4207,7 +4210,14 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4207
4210
  severity: "warning",
4208
4211
  code: "routing.astar.grid_overflow",
4209
4212
  message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4210
- detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4213
+ detail: {
4214
+ xsCount: xsFull.length,
4215
+ ysCount: ysFull.length,
4216
+ maxNodes,
4217
+ obstacleCount: obstacles.length,
4218
+ stage: "full-retry",
4219
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4220
+ }
4211
4221
  });
4212
4222
  return null;
4213
4223
  }
@@ -4450,6 +4460,64 @@ function areCollinear(a, b, c) {
4450
4460
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4451
4461
  }
4452
4462
 
4463
+ // src/routing/budget.ts
4464
+ var MIN_CORNER_BUDGET = 600;
4465
+ var MAX_CORNER_BUDGET = 3e3;
4466
+ var MIN_NODE_BUDGET = 4e3;
4467
+ var MAX_NODE_BUDGET = 64e3;
4468
+ var CORNERS_PER_OBSTACLE = 12;
4469
+ var CORNER_HEADROOM = 2;
4470
+ var GRID_SAFETY_FACTOR = 3;
4471
+ var CORRIDOR_SCALING_K = 0.5;
4472
+ var CORRIDOR_SCALING_BASE = 200;
4473
+ function computeRoutingBudget(cornerObstacles, allObstacles, corridorMargin, overrides = {}) {
4474
+ const adaptiveMaxCorners = deriveMaxCorners(
4475
+ cornerObstacles.length,
4476
+ corridorMargin
4477
+ );
4478
+ const adaptiveMaxNodes = deriveMaxNodes(
4479
+ allObstacles.length,
4480
+ corridorMargin
4481
+ );
4482
+ return {
4483
+ maxCorners: resolveBudget(
4484
+ overrides.maxCorners,
4485
+ adaptiveMaxCorners,
4486
+ MIN_CORNER_BUDGET,
4487
+ MAX_CORNER_BUDGET
4488
+ ),
4489
+ maxNodes: resolveBudget(
4490
+ overrides.maxNodes,
4491
+ adaptiveMaxNodes,
4492
+ MIN_NODE_BUDGET,
4493
+ MAX_NODE_BUDGET
4494
+ ),
4495
+ cornerObstacleCount: cornerObstacles.length,
4496
+ gridObstacleCount: allObstacles.length,
4497
+ corridorMargin
4498
+ };
4499
+ }
4500
+ function deriveMaxCorners(obstacleCount, corridorMargin) {
4501
+ const base = 2 + obstacleCount * CORNERS_PER_OBSTACLE * CORNER_HEADROOM;
4502
+ const corridorFactor = corridorScalingFactor(corridorMargin);
4503
+ return Math.ceil(base * corridorFactor);
4504
+ }
4505
+ function deriveMaxNodes(obstacleCount, corridorMargin) {
4506
+ const base = 4 * obstacleCount * obstacleCount + 4 * obstacleCount + 100;
4507
+ const corridorFactor = corridorScalingFactor(corridorMargin);
4508
+ return Math.ceil(base * GRID_SAFETY_FACTOR * corridorFactor);
4509
+ }
4510
+ function corridorScalingFactor(corridorMargin) {
4511
+ return 1 + corridorMargin / CORRIDOR_SCALING_BASE * CORRIDOR_SCALING_K;
4512
+ }
4513
+ function resolveBudget(override, adaptive, min, max) {
4514
+ const chosen = override !== void 0 && Number.isFinite(override) && override >= 1 ? override : adaptive;
4515
+ return clamp(chosen, min, max);
4516
+ }
4517
+ function clamp(value, min, max) {
4518
+ return Math.max(min, Math.min(max, value));
4519
+ }
4520
+
4453
4521
  // src/routing/visibility-router.ts
4454
4522
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
4455
4523
  const margin = options.margin ?? 0;
@@ -4463,7 +4531,12 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
4463
4531
  severity: "warning",
4464
4532
  code: "routing.visibility.corner_overflow",
4465
4533
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
4466
- detail: { vertexCount: vertices.length, maxCorners }
4534
+ detail: {
4535
+ vertexCount: vertices.length,
4536
+ maxCorners,
4537
+ obstacleCount: obstacles.length,
4538
+ ...options.corridorMargin === void 0 ? {} : { corridorMargin: options.corridorMargin }
4539
+ }
4467
4540
  });
4468
4541
  return null;
4469
4542
  }
@@ -4737,7 +4810,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4737
4810
  }
4738
4811
 
4739
4812
  // src/routing/routes.ts
4740
- function checkBacktracking(points, source, target, diagnostics) {
4813
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4741
4814
  if (points.length < 2) return;
4742
4815
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4743
4816
  if (direct <= 0) return;
@@ -4747,7 +4820,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4747
4820
  const b = points[i + 1];
4748
4821
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4749
4822
  }
4750
- const threshold = 10;
4823
+ const threshold = maxRatio ?? 20;
4751
4824
  if (routeLen > direct * threshold) {
4752
4825
  diagnostics.push({
4753
4826
  severity: "warning",
@@ -4765,8 +4838,20 @@ function routeEdge(input) {
4765
4838
  const diagnostics = [];
4766
4839
  const softObstacles = input.obstacles ?? [];
4767
4840
  const hardObstacles = input.hardObstacles ?? [];
4841
+ let bestRejectedPath;
4842
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4768
4843
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4769
4844
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
4845
+ const recordRejected = (candidate) => {
4846
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
4847
+ return;
4848
+ }
4849
+ const crossings = countObstacleCrossings(candidate, softObstacles);
4850
+ if (crossings < bestRejectedCrossings) {
4851
+ bestRejectedCrossings = crossings;
4852
+ bestRejectedPath = candidate;
4853
+ }
4854
+ };
4770
4855
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4771
4856
  const defaultAnchors = defaultAnchorsForGeometry(
4772
4857
  input.source.box,
@@ -4826,20 +4911,32 @@ function routeEdge(input) {
4826
4911
  targetAnchor
4827
4912
  );
4828
4913
  const allObstacles = [...softObstacles, ...hardObstacles];
4914
+ const corridorMargin = input.corridorMargin ?? 32;
4829
4915
  const corridorObstacles = filterObstaclesByCorridor(
4830
4916
  source,
4831
4917
  target,
4832
4918
  allObstacles,
4833
4919
  [],
4834
4920
  // endpointObstacles passed separately via options
4835
- 32
4921
+ corridorMargin
4836
4922
  );
4837
4923
  const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4924
+ const budget = computeRoutingBudget(
4925
+ cornerObstacles,
4926
+ allObstacles,
4927
+ corridorMargin,
4928
+ { maxCorners: input.maxCorners, maxNodes: input.maxNodes }
4929
+ );
4838
4930
  let cornerPath = findCornerGraphPath(
4839
4931
  source,
4840
4932
  target,
4841
4933
  cornerObstacles,
4842
- { endpointObstacles, margin: 2 },
4934
+ {
4935
+ endpointObstacles,
4936
+ margin: 2,
4937
+ maxCorners: budget.maxCorners,
4938
+ corridorMargin
4939
+ },
4843
4940
  diagnostics
4844
4941
  );
4845
4942
  if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
@@ -4847,7 +4944,12 @@ function routeEdge(input) {
4847
4944
  source,
4848
4945
  target,
4849
4946
  allObstacles,
4850
- { endpointObstacles, margin: 2 },
4947
+ {
4948
+ endpointObstacles,
4949
+ margin: 2,
4950
+ maxCorners: budget.maxCorners,
4951
+ corridorMargin
4952
+ },
4851
4953
  diagnostics
4852
4954
  );
4853
4955
  }
@@ -4855,7 +4957,12 @@ function routeEdge(input) {
4855
4957
  source,
4856
4958
  target,
4857
4959
  allObstacles,
4858
- { endpointObstacles, margin: 0 },
4960
+ {
4961
+ endpointObstacles,
4962
+ margin: 0,
4963
+ corridorMargin,
4964
+ maxNodes: budget.maxNodes
4965
+ },
4859
4966
  diagnostics
4860
4967
  );
4861
4968
  if (path !== null && path.length >= 2) {
@@ -4872,15 +4979,27 @@ function routeEdge(input) {
4872
4979
  softObstacles,
4873
4980
  softObstacleIndex
4874
4981
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4875
- checkBacktracking(finalized, source, target, diagnostics);
4982
+ checkBacktracking(
4983
+ finalized,
4984
+ source,
4985
+ target,
4986
+ diagnostics,
4987
+ input.maxBacktrackingRatio
4988
+ );
4876
4989
  return { points: finalized, diagnostics };
4877
4990
  }
4991
+ recordRejected(finalized);
4878
4992
  if (cornerPath !== null) {
4879
4993
  const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4880
4994
  source,
4881
4995
  target,
4882
4996
  allObstacles,
4883
- { endpointObstacles, margin: 2 },
4997
+ {
4998
+ endpointObstacles,
4999
+ margin: 2,
5000
+ maxCorners: budget.maxCorners,
5001
+ corridorMargin
5002
+ },
4884
5003
  diagnostics
4885
5004
  ) : null;
4886
5005
  if (fullCornerPath !== null && fullCornerPath.length >= 2) {
@@ -4901,15 +5020,27 @@ function routeEdge(input) {
4901
5020
  hardObstacles,
4902
5021
  hardObstacleIndex
4903
5022
  )) {
4904
- checkBacktracking(fullFinalized, source, target, diagnostics);
5023
+ checkBacktracking(
5024
+ fullFinalized,
5025
+ source,
5026
+ target,
5027
+ diagnostics,
5028
+ input.maxBacktrackingRatio
5029
+ );
4905
5030
  return { points: fullFinalized, diagnostics };
4906
5031
  }
5032
+ recordRejected(fullFinalized);
4907
5033
  }
4908
5034
  const gridPath = findObstacleFreePath(
4909
5035
  source,
4910
5036
  target,
4911
5037
  allObstacles,
4912
- { endpointObstacles, margin: 0 },
5038
+ {
5039
+ endpointObstacles,
5040
+ margin: 0,
5041
+ corridorMargin,
5042
+ maxNodes: budget.maxNodes
5043
+ },
4913
5044
  diagnostics
4914
5045
  );
4915
5046
  if (gridPath !== null && gridPath.length >= 2) {
@@ -4930,9 +5061,16 @@ function routeEdge(input) {
4930
5061
  hardObstacles,
4931
5062
  hardObstacleIndex
4932
5063
  )) {
4933
- checkBacktracking(gridFinalized, source, target, diagnostics);
5064
+ checkBacktracking(
5065
+ gridFinalized,
5066
+ source,
5067
+ target,
5068
+ diagnostics,
5069
+ input.maxBacktrackingRatio
5070
+ );
4934
5071
  return { points: gridFinalized, diagnostics };
4935
5072
  }
5073
+ recordRejected(gridFinalized);
4936
5074
  }
4937
5075
  }
4938
5076
  }
@@ -4996,7 +5134,8 @@ function routeEdge(input) {
4996
5134
  finalizedClean,
4997
5135
  candidate.points[0],
4998
5136
  candidate.points[candidate.points.length - 1],
4999
- diagnostics
5137
+ diagnostics,
5138
+ input.maxBacktrackingRatio
5000
5139
  );
5001
5140
  return { points: finalizedClean, diagnostics };
5002
5141
  }
@@ -5062,13 +5201,41 @@ function routeEdge(input) {
5062
5201
  code: "routing.obstacle.unavoidable",
5063
5202
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5064
5203
  });
5065
- return {
5066
- points: finalizeRoute(
5067
- bestPoints2,
5204
+ const finalizedSoftBest = finalizeRoute(
5205
+ bestPoints2,
5206
+ softObstacles,
5207
+ hardObstacles,
5208
+ diagnostics
5209
+ );
5210
+ let softFallback = finalizedSoftBest;
5211
+ if (bestRejectedPath !== void 0) {
5212
+ const finalizedRejected = finalizeRoute(
5213
+ bestRejectedPath,
5068
5214
  softObstacles,
5069
5215
  hardObstacles,
5070
5216
  diagnostics
5071
- ),
5217
+ );
5218
+ const rejectedCrossings = countObstacleCrossings(
5219
+ finalizedRejected,
5220
+ softObstacles
5221
+ );
5222
+ const heuristicCrossings = countObstacleCrossings(
5223
+ finalizedSoftBest,
5224
+ softObstacles
5225
+ );
5226
+ if (rejectedCrossings < heuristicCrossings) {
5227
+ softFallback = finalizedRejected;
5228
+ }
5229
+ }
5230
+ checkBacktracking(
5231
+ softFallback,
5232
+ softFallback[0],
5233
+ softFallback[softFallback.length - 1],
5234
+ diagnostics,
5235
+ input.maxBacktrackingRatio
5236
+ );
5237
+ return {
5238
+ points: softFallback,
5072
5239
  diagnostics
5073
5240
  };
5074
5241
  }
@@ -5100,6 +5267,22 @@ function routeEdge(input) {
5100
5267
  maxAttempts
5101
5268
  );
5102
5269
  }
5270
+ if (bestRejectedPath !== void 0) {
5271
+ diagnostics.push({
5272
+ severity: "warning",
5273
+ code: "routing.obstacle.unavoidable",
5274
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5275
+ });
5276
+ return {
5277
+ points: finalizeRoute(
5278
+ bestRejectedPath,
5279
+ softObstacles,
5280
+ hardObstacles,
5281
+ diagnostics
5282
+ ),
5283
+ diagnostics
5284
+ };
5285
+ }
5103
5286
  diagnostics.push({
5104
5287
  severity: "error",
5105
5288
  code: "routing.evidence.crossing_forbidden",
@@ -5147,13 +5330,41 @@ function routeEdge(input) {
5147
5330
  code: "routing.obstacle.unavoidable",
5148
5331
  message: "No bounded orthogonal route candidate avoided all obstacles."
5149
5332
  });
5150
- return {
5151
- points: finalizeRoute(
5152
- bestPoints,
5333
+ const finalizedBestPoints = finalizeRoute(
5334
+ bestPoints,
5335
+ softObstacles,
5336
+ hardObstacles,
5337
+ diagnostics
5338
+ );
5339
+ let fallbackPoints = finalizedBestPoints;
5340
+ if (bestRejectedPath !== void 0) {
5341
+ const finalizedRejected = finalizeRoute(
5342
+ bestRejectedPath,
5153
5343
  softObstacles,
5154
5344
  hardObstacles,
5155
5345
  diagnostics
5156
- ),
5346
+ );
5347
+ const rejectedCrossings = countObstacleCrossings(
5348
+ finalizedRejected,
5349
+ softObstacles
5350
+ );
5351
+ const heuristicCrossings = countObstacleCrossings(
5352
+ finalizedBestPoints,
5353
+ softObstacles
5354
+ );
5355
+ if (rejectedCrossings < heuristicCrossings) {
5356
+ fallbackPoints = finalizedRejected;
5357
+ }
5358
+ }
5359
+ checkBacktracking(
5360
+ fallbackPoints,
5361
+ fallbackPoints[0],
5362
+ fallbackPoints[fallbackPoints.length - 1],
5363
+ diagnostics,
5364
+ input.maxBacktrackingRatio
5365
+ );
5366
+ return {
5367
+ points: fallbackPoints,
5157
5368
  diagnostics
5158
5369
  };
5159
5370
  }
@@ -5652,6 +5863,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5652
5863
  }
5653
5864
  return false;
5654
5865
  }
5866
+ function countObstacleCrossings(points, obstacles) {
5867
+ let count = 0;
5868
+ for (const obstacle of obstacles) {
5869
+ validateBox(obstacle);
5870
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
5871
+ const a = points[pointIndex];
5872
+ const b = points[pointIndex + 1];
5873
+ if (a === void 0 || b === void 0) {
5874
+ continue;
5875
+ }
5876
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
5877
+ count += 1;
5878
+ break;
5879
+ }
5880
+ }
5881
+ }
5882
+ return count;
5883
+ }
5655
5884
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5656
5885
  for (let index = 0; index < points.length - 1; index += 1) {
5657
5886
  const a = points[index];
@@ -5886,7 +6115,8 @@ function solveDiagram(diagram, options = {}) {
5886
6115
  constrained.boxes,
5887
6116
  constrained.locks,
5888
6117
  options?.overlapSpacing ?? 40,
5889
- Math.max(0, options?.minLaneGutter ?? 0)
6118
+ Math.max(0, options?.minLaneGutter ?? 0),
6119
+ options.distributeContainedChildren ?? true
5890
6120
  );
5891
6121
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5892
6122
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6053,7 +6283,8 @@ function solveDiagram(diagram, options = {}) {
6053
6283
  diagram.direction,
6054
6284
  options,
6055
6285
  diagnostics,
6056
- coordinatedGroups
6286
+ coordinatedGroups,
6287
+ contentBounds
6057
6288
  );
6058
6289
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6059
6290
  coordinatedEdges,
@@ -6472,7 +6703,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6472
6703
  function containsCjk(value) {
6473
6704
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6474
6705
  }
6475
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6706
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6476
6707
  const layouts = /* @__PURE__ */ new Map();
6477
6708
  const diagnostics = [];
6478
6709
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6491,7 +6722,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6491
6722
  locks,
6492
6723
  diagnostics,
6493
6724
  movedChildIds,
6494
- laneGutter
6725
+ laneGutter,
6726
+ constraints,
6727
+ distributeContainedChildren
6495
6728
  );
6496
6729
  if (layout2 !== void 0) {
6497
6730
  layouts.set(swimlane.id, layout2);
@@ -6683,7 +6916,7 @@ function isStackRunaway(boxes, nodes, direction, options) {
6683
6916
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6684
6917
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6685
6918
  }
6686
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6919
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6687
6920
  const headerHeight = swimlane.headerHeight ?? 28;
6688
6921
  const padding = swimlane.padding ?? 16;
6689
6922
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6708,7 +6941,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6708
6941
  locks,
6709
6942
  diagnostics,
6710
6943
  movedChildIds,
6711
- laneGutter
6944
+ laneGutter,
6945
+ constraints,
6946
+ distributeContainedChildren
6712
6947
  );
6713
6948
  }
6714
6949
  return applyHorizontalSwimlaneContract(
@@ -6723,13 +6958,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6723
6958
  laneGutter
6724
6959
  );
6725
6960
  }
6726
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
6961
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6727
6962
  const populatedBounds = laneBounds.filter(
6728
6963
  (box) => box !== void 0
6729
6964
  );
6730
6965
  const top = Math.min(...populatedBounds.map((box) => box.y));
6731
6966
  const left = Math.min(...populatedBounds.map((box) => box.x));
6732
6967
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
6968
+ const containedChildIds = /* @__PURE__ */ new Set();
6969
+ if (distributeContainedChildren) {
6970
+ for (const c of constraints) {
6971
+ if (c.kind !== "containment") continue;
6972
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
6973
+ const distributable = c.childIds.filter((childId) => {
6974
+ if (nodeBoxes.get(childId) === void 0) return false;
6975
+ const lock = locks.get(childId);
6976
+ return lock === void 0 || lock.source === "fixed-position";
6977
+ });
6978
+ if (distributable.length < 2) continue;
6979
+ for (const childId of distributable) {
6980
+ containedChildIds.add(childId);
6981
+ }
6982
+ }
6983
+ }
6733
6984
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6734
6985
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6735
6986
  const rankStackGap = Math.max(8, padding / 2);
@@ -6746,7 +6997,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6746
6997
  nodeBoxes,
6747
6998
  flowRanks,
6748
6999
  locks,
6749
- rankStackGap
7000
+ rankStackGap,
7001
+ containedChildIds
6750
7002
  );
6751
7003
  const slotWidth = Math.max(
6752
7004
  Math.max(...populatedBounds.map((box) => box.width)),
@@ -6768,7 +7020,10 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6768
7020
  const distributable = lane.children.filter(
6769
7021
  (childId) => !locks.has(childId)
6770
7022
  );
6771
- if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7023
+ const coveredByContainment = lane.children.some(
7024
+ (childId) => containedChildIds.has(childId)
7025
+ );
7026
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6772
7027
  moveRankedVerticalLaneChildren(
6773
7028
  lane.children,
6774
7029
  nodeBoxes,
@@ -6796,6 +7051,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6796
7051
  );
6797
7052
  continue;
6798
7053
  }
7054
+ const rankedCoveredByContainment = lane.children.some(
7055
+ (childId) => containedChildIds.has(childId)
7056
+ );
6799
7057
  moveRankedVerticalLaneChildren(
6800
7058
  lane.children,
6801
7059
  nodeBoxes,
@@ -6806,7 +7064,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6806
7064
  rankSpacing,
6807
7065
  rankStackGap,
6808
7066
  { x: target.x, y: laneContentTop },
6809
- slotWidth - padding * 2
7067
+ slotWidth - padding * 2,
7068
+ rankedCoveredByContainment
6810
7069
  );
6811
7070
  }
6812
7071
  return {
@@ -6922,9 +7181,12 @@ function crossAxisSpreadWidth(items, gap) {
6922
7181
  0
6923
7182
  );
6924
7183
  }
6925
- function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7184
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
6926
7185
  let maxWidth = 0;
6927
7186
  for (const lane of swimlane.lanes) {
7187
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7188
+ continue;
7189
+ }
6928
7190
  for (const stack of rankStacks(
6929
7191
  lane.children,
6930
7192
  nodeBoxes,
@@ -6937,7 +7199,7 @@ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
6937
7199
  }
6938
7200
  return maxWidth;
6939
7201
  }
6940
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
7202
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6941
7203
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6942
7204
  const unlocked = [];
6943
7205
  for (const item of stack) {
@@ -6966,7 +7228,7 @@ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics,
6966
7228
  }
6967
7229
  nodeBoxes.set(childId, next);
6968
7230
  } else {
6969
- const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7231
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
6970
7232
  if (!shouldSpread) {
6971
7233
  let yOffset = 0;
6972
7234
  for (const { childId, box } of unlocked) {
@@ -8095,14 +8357,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8095
8357
  }
8096
8358
  };
8097
8359
  }
8098
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
8360
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8099
8361
  const coordinated = [];
8100
8362
  const coordinatedNodeById = new Map(
8101
8363
  coordinatedNodes.map((node) => [node.id, node])
8102
8364
  );
8365
+ const corridorMarginOption = options.corridorMargin ?? "auto";
8366
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
8367
+ 200,
8368
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
8369
+ );
8370
+ const routingGutter = options.routingGutter ?? 160;
8371
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8103
8372
  const nodeObstacleIndex = createBoxSpatialIndex(
8104
8373
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8105
- options.routingGutter ?? 160
8374
+ queryGutter
8106
8375
  );
8107
8376
  for (const edge of edges) {
8108
8377
  const source = nodes.get(edge.source.nodeId);
@@ -8124,11 +8393,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8124
8393
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8125
8394
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8126
8395
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8127
- const corridor = edgeCorridorBox(
8128
- source.box,
8129
- target.box,
8130
- options.routingGutter ?? 160
8131
- );
8396
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8132
8397
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8133
8398
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8134
8399
  );
@@ -8146,7 +8411,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8146
8411
  ...routeTextObstacles
8147
8412
  ],
8148
8413
  hardObstacles,
8149
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
8414
+ corridorMargin,
8415
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
8416
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
8150
8417
  });
8151
8418
  diagnostics.push(
8152
8419
  ...route.diagnostics.map((diagnostic) => ({