@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.
@@ -1394,6 +1394,28 @@ function applyLayoutConstraints(input) {
1394
1394
  if (input.distributeContainedChildren) {
1395
1395
  yieldFixedPositionLocks(input, boxes, locks);
1396
1396
  }
1397
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1398
+ for (const swimlane of input.swimlanes) {
1399
+ if (swimlane.layout === "contract") continue;
1400
+ for (const lane of swimlane.lanes) {
1401
+ const fixedChildren = [];
1402
+ let participantCount = 0;
1403
+ for (const childId of lane.children) {
1404
+ const lock = locks.get(childId);
1405
+ if (lock === void 0) {
1406
+ participantCount += 1;
1407
+ } else if (lock.source === "fixed-position") {
1408
+ participantCount += 1;
1409
+ fixedChildren.push(childId);
1410
+ }
1411
+ }
1412
+ if (participantCount < 2) continue;
1413
+ for (const childId of fixedChildren) {
1414
+ locks.delete(childId);
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1397
1419
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
1398
1420
  applyRelative(input.constraints, boxes, locks, diagnostics);
1399
1421
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -1413,6 +1435,9 @@ function applyLayoutConstraints(input) {
1413
1435
  applyDistributeContained(input, boxes, locks, diagnostics);
1414
1436
  dedupReplayDiagnostics(diagnostics, diagBefore);
1415
1437
  }
1438
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1439
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1440
+ }
1416
1441
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1417
1442
  reportOverlaps(
1418
1443
  boxes,
@@ -2252,9 +2277,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2252
2277
  }
2253
2278
  });
2254
2279
  }
2255
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
2256
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
2257
- }
2258
2280
  }
2259
2281
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2260
2282
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -2294,6 +2316,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2294
2316
  effectiveGap = minGap + remaining / (unlocked.length - 1);
2295
2317
  }
2296
2318
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
2319
+ reserved.sort((a, b) => a.start - b.start);
2297
2320
  let pos = contentStart;
2298
2321
  for (const child of unlocked) {
2299
2322
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4085,15 +4108,12 @@ var BinaryHeap = class {
4085
4108
  let smallestIdx = idx;
4086
4109
  const leftIdx = (idx << 1) + 1;
4087
4110
  const rightIdx = leftIdx + 1;
4088
- if (leftIdx < size && this._less(
4089
- this._data[leftIdx],
4090
- this._data[smallestIdx]
4091
- )) {
4111
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4092
4112
  smallestIdx = leftIdx;
4093
4113
  }
4094
4114
  if (rightIdx < size && this._less(
4095
4115
  this._data[rightIdx],
4096
- this._data[smallestIdx]
4116
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4097
4117
  )) {
4098
4118
  smallestIdx = rightIdx;
4099
4119
  }
@@ -4144,7 +4164,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4144
4164
  detail: {
4145
4165
  xsCount: xs.length,
4146
4166
  ysCount: ys.length,
4147
- maxNodes
4167
+ maxNodes,
4168
+ obstacleCount: obstacles.length,
4169
+ stage: "corridor-filtered",
4170
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4148
4171
  }
4149
4172
  });
4150
4173
  return null;
@@ -4160,8 +4183,66 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4160
4183
  turnPenalty,
4161
4184
  segmentPenalty
4162
4185
  );
4163
- if (path === null) return null;
4164
- return simplifyRoute(path);
4186
+ if (path !== null) {
4187
+ const simplified = simplifyRoute(path);
4188
+ const filteredSet = new Set(filtered);
4189
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4190
+ let crossesExcluded = false;
4191
+ for (let i = 0; i < simplified.length - 1; i++) {
4192
+ const a = simplified[i];
4193
+ const b = simplified[i + 1];
4194
+ for (const obs of obstacles) {
4195
+ if (filteredSet.has(obs)) continue;
4196
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4197
+ crossesExcluded = true;
4198
+ break;
4199
+ }
4200
+ }
4201
+ if (crossesExcluded) break;
4202
+ }
4203
+ if (!crossesExcluded) return simplified;
4204
+ } else {
4205
+ return simplified;
4206
+ }
4207
+ }
4208
+ if (!useCorridor) return null;
4209
+ const xsFull = collectXs(source, target, obstacles, margin);
4210
+ const ysFull = collectYs(source, target, obstacles, margin);
4211
+ if (xsFull.length * ysFull.length > maxNodes) {
4212
+ diagnostics?.push({
4213
+ severity: "warning",
4214
+ code: "routing.astar.grid_overflow",
4215
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
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
+ }
4224
+ });
4225
+ return null;
4226
+ }
4227
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4228
+ connectHorizontalEdges(
4229
+ nodesFull,
4230
+ ysFull,
4231
+ obstacles,
4232
+ endpointObstacles,
4233
+ margin
4234
+ );
4235
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4236
+ const pathFull = aStarSearch(
4237
+ nodesFull,
4238
+ idxFull,
4239
+ source,
4240
+ target,
4241
+ turnPenalty,
4242
+ segmentPenalty
4243
+ );
4244
+ if (pathFull === null) return null;
4245
+ return simplifyRoute(pathFull);
4165
4246
  }
4166
4247
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4167
4248
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -4382,20 +4463,83 @@ function areCollinear(a, b, c) {
4382
4463
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4383
4464
  }
4384
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
+
4385
4524
  // src/routing/visibility-router.ts
4386
4525
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
4387
4526
  const margin = options.margin ?? 0;
4388
4527
  const turnPenalty = options.turnPenalty ?? 50;
4389
4528
  const segmentPenalty = options.segmentPenalty ?? 1;
4390
4529
  const endpointObstacles = options.endpointObstacles ?? [];
4391
- const maxCorners = options.maxCorners ?? 300;
4530
+ const maxCorners = options.maxCorners ?? 600;
4392
4531
  const vertices = collectCornerVertices(source, target, obstacles, margin);
4393
4532
  if (vertices.length > maxCorners) {
4394
4533
  diagnostics?.push({
4395
4534
  severity: "warning",
4396
4535
  code: "routing.visibility.corner_overflow",
4397
4536
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
4398
- 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
+ }
4399
4543
  });
4400
4544
  return null;
4401
4545
  }
@@ -4669,7 +4813,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4669
4813
  }
4670
4814
 
4671
4815
  // src/routing/routes.ts
4672
- function checkBacktracking(points, source, target, diagnostics) {
4816
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4673
4817
  if (points.length < 2) return;
4674
4818
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4675
4819
  if (direct <= 0) return;
@@ -4679,7 +4823,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4679
4823
  const b = points[i + 1];
4680
4824
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4681
4825
  }
4682
- const threshold = 10;
4826
+ const threshold = maxRatio ?? 20;
4683
4827
  if (routeLen > direct * threshold) {
4684
4828
  diagnostics.push({
4685
4829
  severity: "warning",
@@ -4697,8 +4841,20 @@ function routeEdge(input) {
4697
4841
  const diagnostics = [];
4698
4842
  const softObstacles = input.obstacles ?? [];
4699
4843
  const hardObstacles = input.hardObstacles ?? [];
4844
+ let bestRejectedPath;
4845
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4700
4846
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4701
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
+ };
4702
4858
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4703
4859
  const defaultAnchors = defaultAnchorsForGeometry(
4704
4860
  input.source.box,
@@ -4757,18 +4913,59 @@ function routeEdge(input) {
4757
4913
  input.source.center,
4758
4914
  targetAnchor
4759
4915
  );
4760
- const cornerPath = findCornerGraphPath(
4916
+ const allObstacles = [...softObstacles, ...hardObstacles];
4917
+ const corridorMargin = input.corridorMargin ?? 32;
4918
+ const corridorObstacles = filterObstaclesByCorridor(
4761
4919
  source,
4762
4920
  target,
4763
- [...softObstacles, ...hardObstacles],
4764
- { endpointObstacles, margin: 2 },
4921
+ allObstacles,
4922
+ [],
4923
+ // endpointObstacles passed separately via options
4924
+ corridorMargin
4925
+ );
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
+ );
4933
+ let cornerPath = findCornerGraphPath(
4934
+ source,
4935
+ target,
4936
+ cornerObstacles,
4937
+ {
4938
+ endpointObstacles,
4939
+ margin: 2,
4940
+ maxCorners: budget.maxCorners,
4941
+ corridorMargin
4942
+ },
4765
4943
  diagnostics
4766
4944
  );
4945
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4946
+ cornerPath = findCornerGraphPath(
4947
+ source,
4948
+ target,
4949
+ allObstacles,
4950
+ {
4951
+ endpointObstacles,
4952
+ margin: 2,
4953
+ maxCorners: budget.maxCorners,
4954
+ corridorMargin
4955
+ },
4956
+ diagnostics
4957
+ );
4958
+ }
4767
4959
  const path = cornerPath ?? findObstacleFreePath(
4768
4960
  source,
4769
4961
  target,
4770
- [...softObstacles, ...hardObstacles],
4771
- { endpointObstacles, margin: 0 },
4962
+ allObstacles,
4963
+ {
4964
+ endpointObstacles,
4965
+ margin: 0,
4966
+ corridorMargin,
4967
+ maxNodes: budget.maxNodes
4968
+ },
4772
4969
  diagnostics
4773
4970
  );
4774
4971
  if (path !== null && path.length >= 2) {
@@ -4785,9 +4982,100 @@ function routeEdge(input) {
4785
4982
  softObstacles,
4786
4983
  softObstacleIndex
4787
4984
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4788
- checkBacktracking(finalized, source, target, diagnostics);
4985
+ checkBacktracking(
4986
+ finalized,
4987
+ source,
4988
+ target,
4989
+ diagnostics,
4990
+ input.maxBacktrackingRatio
4991
+ );
4789
4992
  return { points: finalized, diagnostics };
4790
4993
  }
4994
+ recordRejected(finalized);
4995
+ if (cornerPath !== null) {
4996
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4997
+ source,
4998
+ target,
4999
+ allObstacles,
5000
+ {
5001
+ endpointObstacles,
5002
+ margin: 2,
5003
+ maxCorners: budget.maxCorners,
5004
+ corridorMargin
5005
+ },
5006
+ diagnostics
5007
+ ) : null;
5008
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5009
+ const fullFinalized = finalizeRoute(
5010
+ fullCornerPath,
5011
+ softObstacles,
5012
+ hardObstacles,
5013
+ diagnostics,
5014
+ softObstacleIndex,
5015
+ hardObstacleIndex
5016
+ );
5017
+ if (!routeIntersectsObstacles(
5018
+ fullFinalized,
5019
+ softObstacles,
5020
+ softObstacleIndex
5021
+ ) && !routeIntersectsObstacles(
5022
+ fullFinalized,
5023
+ hardObstacles,
5024
+ hardObstacleIndex
5025
+ )) {
5026
+ checkBacktracking(
5027
+ fullFinalized,
5028
+ source,
5029
+ target,
5030
+ diagnostics,
5031
+ input.maxBacktrackingRatio
5032
+ );
5033
+ return { points: fullFinalized, diagnostics };
5034
+ }
5035
+ recordRejected(fullFinalized);
5036
+ }
5037
+ const gridPath = findObstacleFreePath(
5038
+ source,
5039
+ target,
5040
+ allObstacles,
5041
+ {
5042
+ endpointObstacles,
5043
+ margin: 0,
5044
+ corridorMargin,
5045
+ maxNodes: budget.maxNodes
5046
+ },
5047
+ diagnostics
5048
+ );
5049
+ if (gridPath !== null && gridPath.length >= 2) {
5050
+ const gridFinalized = finalizeRoute(
5051
+ gridPath,
5052
+ softObstacles,
5053
+ hardObstacles,
5054
+ diagnostics,
5055
+ softObstacleIndex,
5056
+ hardObstacleIndex
5057
+ );
5058
+ if (!routeIntersectsObstacles(
5059
+ gridFinalized,
5060
+ softObstacles,
5061
+ softObstacleIndex
5062
+ ) && !routeIntersectsObstacles(
5063
+ gridFinalized,
5064
+ hardObstacles,
5065
+ hardObstacleIndex
5066
+ )) {
5067
+ checkBacktracking(
5068
+ gridFinalized,
5069
+ source,
5070
+ target,
5071
+ diagnostics,
5072
+ input.maxBacktrackingRatio
5073
+ );
5074
+ return { points: gridFinalized, diagnostics };
5075
+ }
5076
+ recordRejected(gridFinalized);
5077
+ }
5078
+ }
4791
5079
  }
4792
5080
  }
4793
5081
  }
@@ -4849,7 +5137,8 @@ function routeEdge(input) {
4849
5137
  finalizedClean,
4850
5138
  candidate.points[0],
4851
5139
  candidate.points[candidate.points.length - 1],
4852
- diagnostics
5140
+ diagnostics,
5141
+ input.maxBacktrackingRatio
4853
5142
  );
4854
5143
  return { points: finalizedClean, diagnostics };
4855
5144
  }
@@ -4915,13 +5204,41 @@ function routeEdge(input) {
4915
5204
  code: "routing.obstacle.unavoidable",
4916
5205
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
4917
5206
  });
4918
- return {
4919
- points: finalizeRoute(
4920
- 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,
4921
5217
  softObstacles,
4922
5218
  hardObstacles,
4923
5219
  diagnostics
4924
- ),
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,
4925
5242
  diagnostics
4926
5243
  };
4927
5244
  }
@@ -4953,6 +5270,22 @@ function routeEdge(input) {
4953
5270
  maxAttempts
4954
5271
  );
4955
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
+ }
4956
5289
  diagnostics.push({
4957
5290
  severity: "error",
4958
5291
  code: "routing.evidence.crossing_forbidden",
@@ -5000,13 +5333,41 @@ function routeEdge(input) {
5000
5333
  code: "routing.obstacle.unavoidable",
5001
5334
  message: "No bounded orthogonal route candidate avoided all obstacles."
5002
5335
  });
5003
- return {
5004
- points: finalizeRoute(
5005
- 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,
5006
5346
  softObstacles,
5007
5347
  hardObstacles,
5008
5348
  diagnostics
5009
- ),
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,
5010
5371
  diagnostics
5011
5372
  };
5012
5373
  }
@@ -5505,6 +5866,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5505
5866
  }
5506
5867
  return false;
5507
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
+ }
5508
5887
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5509
5888
  for (let index = 0; index < points.length - 1; index += 1) {
5510
5889
  const a = points[index];
@@ -5677,7 +6056,7 @@ function solveDiagram(diagram, options = {}) {
5677
6056
  edges: styledEdges
5678
6057
  });
5679
6058
  diagnostics.push(...layout2.diagnostics);
5680
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6059
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5681
6060
  layout2.boxes,
5682
6061
  styledNodes,
5683
6062
  styledEdges,
@@ -5685,7 +6064,8 @@ function solveDiagram(diagram, options = {}) {
5685
6064
  options,
5686
6065
  diagnostics
5687
6066
  );
5688
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6067
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6068
+ const diagCountBefore = diagnostics.length;
5689
6069
  const rewrapped = wrapHorizontalStackIfNeeded(
5690
6070
  initialNodeBoxes,
5691
6071
  styledNodes,
@@ -5696,6 +6076,20 @@ function solveDiagram(diagram, options = {}) {
5696
6076
  for (const [id, box] of rewrapped) {
5697
6077
  initialNodeBoxes.set(id, box);
5698
6078
  }
6079
+ if (diagnostics.length > diagCountBefore) {
6080
+ for (const node of styledNodes) {
6081
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6082
+ const rwBox = rewrapped.get(node.id);
6083
+ const idx = styledNodes.indexOf(node);
6084
+ if (idx !== -1) {
6085
+ styledNodes[idx] = {
6086
+ ...node,
6087
+ position: { x: rwBox.x, y: rwBox.y }
6088
+ };
6089
+ }
6090
+ }
6091
+ }
6092
+ }
5699
6093
  }
5700
6094
  if (useRecursive && "groupBoxes" in layout2) {
5701
6095
  const recursiveLayout = layout2;
@@ -5709,7 +6103,7 @@ function solveDiagram(diagram, options = {}) {
5709
6103
  overlapSpacing: options?.overlapSpacing ?? 40,
5710
6104
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5711
6105
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5712
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6106
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5713
6107
  swimlanes: styledSwimlanes,
5714
6108
  boxes: initialNodeBoxes,
5715
6109
  nodes: styledNodes,
@@ -5724,7 +6118,8 @@ function solveDiagram(diagram, options = {}) {
5724
6118
  constrained.boxes,
5725
6119
  constrained.locks,
5726
6120
  options?.overlapSpacing ?? 40,
5727
- Math.max(0, options?.minLaneGutter ?? 0)
6121
+ Math.max(0, options?.minLaneGutter ?? 0),
6122
+ options.distributeContainedChildren ?? true
5728
6123
  );
5729
6124
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5730
6125
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -5891,7 +6286,8 @@ function solveDiagram(diagram, options = {}) {
5891
6286
  diagram.direction,
5892
6287
  options,
5893
6288
  diagnostics,
5894
- coordinatedGroups
6289
+ coordinatedGroups,
6290
+ contentBounds
5895
6291
  );
5896
6292
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5897
6293
  coordinatedEdges,
@@ -6310,7 +6706,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6310
6706
  function containsCjk(value) {
6311
6707
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6312
6708
  }
6313
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6709
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6314
6710
  const layouts = /* @__PURE__ */ new Map();
6315
6711
  const diagnostics = [];
6316
6712
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6329,7 +6725,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6329
6725
  locks,
6330
6726
  diagnostics,
6331
6727
  movedChildIds,
6332
- laneGutter
6728
+ laneGutter,
6729
+ constraints,
6730
+ distributeContainedChildren
6333
6731
  );
6334
6732
  if (layout2 !== void 0) {
6335
6733
  layouts.set(swimlane.id, layout2);
@@ -6425,9 +6823,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6425
6823
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6426
6824
  return new Map(boxes);
6427
6825
  }
6428
- const maxRowDepth = options.maxRowDepth;
6429
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6430
- return new Map(boxes);
6826
+ let maxRowDepth = options.maxRowDepth;
6827
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6828
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6829
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6830
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6831
+ } else {
6832
+ return new Map(boxes);
6833
+ }
6431
6834
  }
6432
6835
  const ordered = [...nodes].sort((a, b) => {
6433
6836
  const ba = boxes.get(a.id);
@@ -6488,10 +6891,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6488
6891
  });
6489
6892
  }
6490
6893
  function isStackRunaway(boxes, nodes, direction, options) {
6491
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6894
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6492
6895
  return false;
6493
6896
  }
6494
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6897
+ if (nodes.length < 2) {
6495
6898
  return false;
6496
6899
  }
6497
6900
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6499,17 +6902,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
6499
6902
  return false;
6500
6903
  }
6501
6904
  const bounds = unionBoxes(nodeBoxes);
6502
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6503
- const preferred = options.preferredAspectRatio ?? 3;
6504
- if (aspectRatio < preferred) {
6905
+ const isHorizontal = direction === "TB" || direction === "BT";
6906
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6907
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6908
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6505
6909
  return false;
6506
6910
  }
6911
+ if (isHorizontal) {
6912
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6913
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6914
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6915
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6916
+ }
6507
6917
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6508
6918
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6509
6919
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6510
6920
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6511
6921
  }
6512
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6922
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6513
6923
  const headerHeight = swimlane.headerHeight ?? 28;
6514
6924
  const padding = swimlane.padding ?? 16;
6515
6925
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6534,7 +6944,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6534
6944
  locks,
6535
6945
  diagnostics,
6536
6946
  movedChildIds,
6537
- laneGutter
6947
+ laneGutter,
6948
+ constraints,
6949
+ distributeContainedChildren
6538
6950
  );
6539
6951
  }
6540
6952
  return applyHorizontalSwimlaneContract(
@@ -6549,13 +6961,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6549
6961
  laneGutter
6550
6962
  );
6551
6963
  }
6552
- 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) {
6553
6965
  const populatedBounds = laneBounds.filter(
6554
6966
  (box) => box !== void 0
6555
6967
  );
6556
6968
  const top = Math.min(...populatedBounds.map((box) => box.y));
6557
6969
  const left = Math.min(...populatedBounds.map((box) => box.x));
6558
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
+ }
6559
6987
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6560
6988
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6561
6989
  const rankStackGap = Math.max(8, padding / 2);
@@ -6567,7 +6995,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6567
6995
  );
6568
6996
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6569
6997
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6570
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6998
+ const spreadWidth = maxCrossAxisSpreadWidth(
6999
+ swimlane,
7000
+ nodeBoxes,
7001
+ flowRanks,
7002
+ locks,
7003
+ rankStackGap,
7004
+ containedChildIds
7005
+ );
7006
+ const slotWidth = Math.max(
7007
+ Math.max(...populatedBounds.map((box) => box.width)),
7008
+ spreadWidth
7009
+ ) + padding * 2;
6571
7010
  const laneStep = slotWidth + laneGutter;
6572
7011
  const laneContentTop = top + headerHeight + padding;
6573
7012
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6581,6 +7020,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6581
7020
  y: laneContentTop
6582
7021
  };
6583
7022
  if (maxRank === 0) {
7023
+ const distributable = lane.children.filter(
7024
+ (childId) => !locks.has(childId)
7025
+ );
7026
+ const coveredByContainment = lane.children.some(
7027
+ (childId) => containedChildIds.has(childId)
7028
+ );
7029
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7030
+ moveRankedVerticalLaneChildren(
7031
+ lane.children,
7032
+ nodeBoxes,
7033
+ locks,
7034
+ diagnostics,
7035
+ movedChildIds,
7036
+ flowRanks,
7037
+ rankSpacing,
7038
+ rankStackGap,
7039
+ { x: target.x, y: laneContentTop },
7040
+ slotWidth - padding * 2
7041
+ );
7042
+ continue;
7043
+ }
6584
7044
  moveLaneChildren(
6585
7045
  lane.children,
6586
7046
  nodeBoxes,
@@ -6594,6 +7054,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6594
7054
  );
6595
7055
  continue;
6596
7056
  }
7057
+ const rankedCoveredByContainment = lane.children.some(
7058
+ (childId) => containedChildIds.has(childId)
7059
+ );
6597
7060
  moveRankedVerticalLaneChildren(
6598
7061
  lane.children,
6599
7062
  nodeBoxes,
@@ -6603,10 +7066,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6603
7066
  flowRanks,
6604
7067
  rankSpacing,
6605
7068
  rankStackGap,
6606
- {
6607
- x: target.x - bounds.x,
6608
- y: laneContentTop
6609
- }
7069
+ { x: target.x, y: laneContentTop },
7070
+ slotWidth - padding * 2,
7071
+ rankedCoveredByContainment
6610
7072
  );
6611
7073
  }
6612
7074
  return {
@@ -6715,31 +7177,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6715
7177
  }
6716
7178
  return maxHeight;
6717
7179
  }
6718
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7180
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7181
+ function crossAxisSpreadWidth(items, gap) {
7182
+ return items.reduce(
7183
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7184
+ 0
7185
+ );
7186
+ }
7187
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7188
+ let maxWidth = 0;
7189
+ for (const lane of swimlane.lanes) {
7190
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7191
+ continue;
7192
+ }
7193
+ for (const stack of rankStacks(
7194
+ lane.children,
7195
+ nodeBoxes,
7196
+ flowRanks
7197
+ ).values()) {
7198
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7199
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7200
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7201
+ }
7202
+ }
7203
+ return maxWidth;
7204
+ }
7205
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6719
7206
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6720
- let yOffset = 0;
7207
+ const unlocked = [];
6721
7208
  for (const item of stack) {
6722
- const { childId, box } = item;
6723
- if (locks.has(childId)) {
7209
+ if (locks.has(item.childId)) {
6724
7210
  diagnostics.push({
6725
7211
  severity: "warning",
6726
7212
  code: "constraints.locked-target-not-moved",
6727
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7213
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6728
7214
  path: ["swimlanes"],
6729
- detail: { nodeId: childId }
7215
+ detail: { nodeId: item.childId }
6730
7216
  });
6731
- continue;
7217
+ } else {
7218
+ unlocked.push(item);
6732
7219
  }
7220
+ }
7221
+ if (unlocked.length === 0) continue;
7222
+ if (unlocked.length === 1) {
7223
+ const { childId, box } = unlocked[0];
6733
7224
  const next = {
6734
7225
  ...box,
6735
- x: box.x + target.x,
6736
- y: target.y + rank * rankSpacing + yOffset
7226
+ x: target.x + (contentWidth - box.width) / 2,
7227
+ y: target.y + rank * rankSpacing
6737
7228
  };
6738
7229
  if (next.x !== box.x || next.y !== box.y) {
6739
7230
  movedChildIds.add(childId);
6740
7231
  }
6741
7232
  nodeBoxes.set(childId, next);
6742
- yOffset += box.height + rankStackGap;
7233
+ } else {
7234
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7235
+ if (!shouldSpread) {
7236
+ let yOffset = 0;
7237
+ for (const { childId, box } of unlocked) {
7238
+ const next = {
7239
+ ...box,
7240
+ x: target.x + (contentWidth - box.width) / 2,
7241
+ y: target.y + rank * rankSpacing + yOffset
7242
+ };
7243
+ if (next.x !== box.x || next.y !== box.y) {
7244
+ movedChildIds.add(childId);
7245
+ }
7246
+ nodeBoxes.set(childId, next);
7247
+ yOffset += box.height + rankStackGap;
7248
+ }
7249
+ } else {
7250
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
7251
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
7252
+ for (const { childId, box } of unlocked) {
7253
+ const next = {
7254
+ ...box,
7255
+ x: xCursor,
7256
+ y: target.y + rank * rankSpacing
7257
+ };
7258
+ if (next.x !== box.x || next.y !== box.y) {
7259
+ movedChildIds.add(childId);
7260
+ }
7261
+ nodeBoxes.set(childId, next);
7262
+ xCursor += box.width + rankStackGap;
7263
+ }
7264
+ diagnostics.push({
7265
+ severity: "info",
7266
+ code: "swimlane_contract.cross_axis_distributed",
7267
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7268
+ path: ["swimlanes"],
7269
+ detail: {
7270
+ rank,
7271
+ childCount: unlocked.length,
7272
+ contentWidth
7273
+ }
7274
+ });
7275
+ }
6743
7276
  }
6744
7277
  }
6745
7278
  }
@@ -7078,7 +7611,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7078
7611
  });
7079
7612
  continue;
7080
7613
  }
7081
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7614
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7082
7615
  const geometry = computeShapeGeometry({
7083
7616
  shape: node.shape,
7084
7617
  box,
@@ -7172,7 +7705,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7172
7705
  }
7173
7706
  }
7174
7707
  }
7175
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7708
+ function coordinatePorts(node, nodeBox, portShifting) {
7176
7709
  const portsBySide = /* @__PURE__ */ new Map();
7177
7710
  for (const port of node.ports ?? []) {
7178
7711
  const ports = portsBySide.get(port.side) ?? [];
@@ -7195,9 +7728,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7195
7728
  side,
7196
7729
  index,
7197
7730
  sorted.length,
7198
- portShifting,
7199
- diagnostics,
7200
- node.id
7731
+ portShifting
7201
7732
  );
7202
7733
  const box = portBox(anchor);
7203
7734
  coordinated.push({ ...port, box, anchor });
@@ -7205,32 +7736,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7205
7736
  }
7206
7737
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7207
7738
  }
7208
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7739
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7209
7740
  const shiftingEnabled = portShifting?.enabled ?? true;
7210
7741
  const requestedSpacing = portShifting?.spacing ?? 24;
7211
7742
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7212
7743
  const availableSpan = 2 * maxOffset;
7213
7744
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7214
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7745
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7215
7746
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7216
7747
  minSpacing
7217
7748
  ) : requestedSpacing;
7218
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
7219
- diagnostics.push({
7220
- severity: "warning",
7221
- code: "port_constraint_overlap",
7222
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
7223
- path: ["nodes", nodeId, "ports"],
7224
- detail: {
7225
- nodeId,
7226
- side,
7227
- requestedSpacing,
7228
- effectiveSpacing: Math.round(effectiveSpacing),
7229
- portCount: count
7230
- }
7231
- });
7232
- }
7233
- const spacing = effectiveSpacing;
7234
7749
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7235
7750
  switch (side) {
7236
7751
  case "left":
@@ -7845,14 +8360,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
7845
8360
  }
7846
8361
  };
7847
8362
  }
7848
- 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) {
7849
8364
  const coordinated = [];
7850
8365
  const coordinatedNodeById = new Map(
7851
8366
  coordinatedNodes.map((node) => [node.id, node])
7852
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;
7853
8375
  const nodeObstacleIndex = createBoxSpatialIndex(
7854
8376
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
7855
- options.routingGutter ?? 160
8377
+ queryGutter
7856
8378
  );
7857
8379
  for (const edge of edges) {
7858
8380
  const source = nodes.get(edge.source.nodeId);
@@ -7874,11 +8396,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7874
8396
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
7875
8397
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
7876
8398
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
7877
- const corridor = edgeCorridorBox(
7878
- source.box,
7879
- target.box,
7880
- options.routingGutter ?? 160
7881
- );
8399
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
7882
8400
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7883
8401
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7884
8402
  );
@@ -7896,7 +8414,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7896
8414
  ...routeTextObstacles
7897
8415
  ],
7898
8416
  hardObstacles,
7899
- ...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 }
7900
8420
  });
7901
8421
  diagnostics.push(
7902
8422
  ...route.diagnostics.map((diagnostic) => ({