@crazyhappyone/auto-graph 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1391,6 +1391,28 @@ function applyLayoutConstraints(input) {
1391
1391
  if (input.distributeContainedChildren) {
1392
1392
  yieldFixedPositionLocks(input, boxes, locks);
1393
1393
  }
1394
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1395
+ for (const swimlane of input.swimlanes) {
1396
+ if (swimlane.layout === "contract") continue;
1397
+ for (const lane of swimlane.lanes) {
1398
+ const fixedChildren = [];
1399
+ let participantCount = 0;
1400
+ for (const childId of lane.children) {
1401
+ const lock = locks.get(childId);
1402
+ if (lock === void 0) {
1403
+ participantCount += 1;
1404
+ } else if (lock.source === "fixed-position") {
1405
+ participantCount += 1;
1406
+ fixedChildren.push(childId);
1407
+ }
1408
+ }
1409
+ if (participantCount < 2) continue;
1410
+ for (const childId of fixedChildren) {
1411
+ locks.delete(childId);
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1394
1416
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
1395
1417
  applyRelative(input.constraints, boxes, locks, diagnostics);
1396
1418
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -1410,6 +1432,9 @@ function applyLayoutConstraints(input) {
1410
1432
  applyDistributeContained(input, boxes, locks, diagnostics);
1411
1433
  dedupReplayDiagnostics(diagnostics, diagBefore);
1412
1434
  }
1435
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1436
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1437
+ }
1413
1438
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1414
1439
  reportOverlaps(
1415
1440
  boxes,
@@ -2249,9 +2274,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2249
2274
  }
2250
2275
  });
2251
2276
  }
2252
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
2253
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
2254
- }
2255
2277
  }
2256
2278
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2257
2279
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -2291,6 +2313,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2291
2313
  effectiveGap = minGap + remaining / (unlocked.length - 1);
2292
2314
  }
2293
2315
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
2316
+ reserved.sort((a, b) => a.start - b.start);
2294
2317
  let pos = contentStart;
2295
2318
  for (const child of unlocked) {
2296
2319
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4082,15 +4105,12 @@ var BinaryHeap = class {
4082
4105
  let smallestIdx = idx;
4083
4106
  const leftIdx = (idx << 1) + 1;
4084
4107
  const rightIdx = leftIdx + 1;
4085
- if (leftIdx < size && this._less(
4086
- this._data[leftIdx],
4087
- this._data[smallestIdx]
4088
- )) {
4108
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4089
4109
  smallestIdx = leftIdx;
4090
4110
  }
4091
4111
  if (rightIdx < size && this._less(
4092
4112
  this._data[rightIdx],
4093
- this._data[smallestIdx]
4113
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4094
4114
  )) {
4095
4115
  smallestIdx = rightIdx;
4096
4116
  }
@@ -4141,7 +4161,10 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4141
4161
  detail: {
4142
4162
  xsCount: xs.length,
4143
4163
  ysCount: ys.length,
4144
- maxNodes
4164
+ maxNodes,
4165
+ obstacleCount: obstacles.length,
4166
+ stage: "corridor-filtered",
4167
+ ...corridorMargin === void 0 ? {} : { corridorMargin }
4145
4168
  }
4146
4169
  });
4147
4170
  return null;
@@ -4157,8 +4180,66 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4157
4180
  turnPenalty,
4158
4181
  segmentPenalty
4159
4182
  );
4160
- if (path === null) return null;
4161
- return simplifyRoute(path);
4183
+ if (path !== null) {
4184
+ const simplified = simplifyRoute(path);
4185
+ const filteredSet = new Set(filtered);
4186
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4187
+ let crossesExcluded = false;
4188
+ for (let i = 0; i < simplified.length - 1; i++) {
4189
+ const a = simplified[i];
4190
+ const b = simplified[i + 1];
4191
+ for (const obs of obstacles) {
4192
+ if (filteredSet.has(obs)) continue;
4193
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4194
+ crossesExcluded = true;
4195
+ break;
4196
+ }
4197
+ }
4198
+ if (crossesExcluded) break;
4199
+ }
4200
+ if (!crossesExcluded) return simplified;
4201
+ } else {
4202
+ return simplified;
4203
+ }
4204
+ }
4205
+ if (!useCorridor) return null;
4206
+ const xsFull = collectXs(source, target, obstacles, margin);
4207
+ const ysFull = collectYs(source, target, obstacles, margin);
4208
+ if (xsFull.length * ysFull.length > maxNodes) {
4209
+ diagnostics?.push({
4210
+ severity: "warning",
4211
+ code: "routing.astar.grid_overflow",
4212
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
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
+ }
4221
+ });
4222
+ return null;
4223
+ }
4224
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4225
+ connectHorizontalEdges(
4226
+ nodesFull,
4227
+ ysFull,
4228
+ obstacles,
4229
+ endpointObstacles,
4230
+ margin
4231
+ );
4232
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4233
+ const pathFull = aStarSearch(
4234
+ nodesFull,
4235
+ idxFull,
4236
+ source,
4237
+ target,
4238
+ turnPenalty,
4239
+ segmentPenalty
4240
+ );
4241
+ if (pathFull === null) return null;
4242
+ return simplifyRoute(pathFull);
4162
4243
  }
4163
4244
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4164
4245
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -4379,20 +4460,83 @@ function areCollinear(a, b, c) {
4379
4460
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4380
4461
  }
4381
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
+
4382
4521
  // src/routing/visibility-router.ts
4383
4522
  function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
4384
4523
  const margin = options.margin ?? 0;
4385
4524
  const turnPenalty = options.turnPenalty ?? 50;
4386
4525
  const segmentPenalty = options.segmentPenalty ?? 1;
4387
4526
  const endpointObstacles = options.endpointObstacles ?? [];
4388
- const maxCorners = options.maxCorners ?? 300;
4527
+ const maxCorners = options.maxCorners ?? 600;
4389
4528
  const vertices = collectCornerVertices(source, target, obstacles, margin);
4390
4529
  if (vertices.length > maxCorners) {
4391
4530
  diagnostics?.push({
4392
4531
  severity: "warning",
4393
4532
  code: "routing.visibility.corner_overflow",
4394
4533
  message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
4395
- 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
+ }
4396
4540
  });
4397
4541
  return null;
4398
4542
  }
@@ -4666,7 +4810,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4666
4810
  }
4667
4811
 
4668
4812
  // src/routing/routes.ts
4669
- function checkBacktracking(points, source, target, diagnostics) {
4813
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4670
4814
  if (points.length < 2) return;
4671
4815
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4672
4816
  if (direct <= 0) return;
@@ -4676,7 +4820,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4676
4820
  const b = points[i + 1];
4677
4821
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4678
4822
  }
4679
- const threshold = 10;
4823
+ const threshold = maxRatio ?? 20;
4680
4824
  if (routeLen > direct * threshold) {
4681
4825
  diagnostics.push({
4682
4826
  severity: "warning",
@@ -4694,8 +4838,20 @@ function routeEdge(input) {
4694
4838
  const diagnostics = [];
4695
4839
  const softObstacles = input.obstacles ?? [];
4696
4840
  const hardObstacles = input.hardObstacles ?? [];
4841
+ let bestRejectedPath;
4842
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4697
4843
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4698
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
+ };
4699
4855
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4700
4856
  const defaultAnchors = defaultAnchorsForGeometry(
4701
4857
  input.source.box,
@@ -4754,18 +4910,59 @@ function routeEdge(input) {
4754
4910
  input.source.center,
4755
4911
  targetAnchor
4756
4912
  );
4757
- const cornerPath = findCornerGraphPath(
4913
+ const allObstacles = [...softObstacles, ...hardObstacles];
4914
+ const corridorMargin = input.corridorMargin ?? 32;
4915
+ const corridorObstacles = filterObstaclesByCorridor(
4758
4916
  source,
4759
4917
  target,
4760
- [...softObstacles, ...hardObstacles],
4761
- { endpointObstacles, margin: 2 },
4918
+ allObstacles,
4919
+ [],
4920
+ // endpointObstacles passed separately via options
4921
+ corridorMargin
4922
+ );
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
+ );
4930
+ let cornerPath = findCornerGraphPath(
4931
+ source,
4932
+ target,
4933
+ cornerObstacles,
4934
+ {
4935
+ endpointObstacles,
4936
+ margin: 2,
4937
+ maxCorners: budget.maxCorners,
4938
+ corridorMargin
4939
+ },
4762
4940
  diagnostics
4763
4941
  );
4942
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4943
+ cornerPath = findCornerGraphPath(
4944
+ source,
4945
+ target,
4946
+ allObstacles,
4947
+ {
4948
+ endpointObstacles,
4949
+ margin: 2,
4950
+ maxCorners: budget.maxCorners,
4951
+ corridorMargin
4952
+ },
4953
+ diagnostics
4954
+ );
4955
+ }
4764
4956
  const path = cornerPath ?? findObstacleFreePath(
4765
4957
  source,
4766
4958
  target,
4767
- [...softObstacles, ...hardObstacles],
4768
- { endpointObstacles, margin: 0 },
4959
+ allObstacles,
4960
+ {
4961
+ endpointObstacles,
4962
+ margin: 0,
4963
+ corridorMargin,
4964
+ maxNodes: budget.maxNodes
4965
+ },
4769
4966
  diagnostics
4770
4967
  );
4771
4968
  if (path !== null && path.length >= 2) {
@@ -4782,9 +4979,100 @@ function routeEdge(input) {
4782
4979
  softObstacles,
4783
4980
  softObstacleIndex
4784
4981
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4785
- checkBacktracking(finalized, source, target, diagnostics);
4982
+ checkBacktracking(
4983
+ finalized,
4984
+ source,
4985
+ target,
4986
+ diagnostics,
4987
+ input.maxBacktrackingRatio
4988
+ );
4786
4989
  return { points: finalized, diagnostics };
4787
4990
  }
4991
+ recordRejected(finalized);
4992
+ if (cornerPath !== null) {
4993
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4994
+ source,
4995
+ target,
4996
+ allObstacles,
4997
+ {
4998
+ endpointObstacles,
4999
+ margin: 2,
5000
+ maxCorners: budget.maxCorners,
5001
+ corridorMargin
5002
+ },
5003
+ diagnostics
5004
+ ) : null;
5005
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5006
+ const fullFinalized = finalizeRoute(
5007
+ fullCornerPath,
5008
+ softObstacles,
5009
+ hardObstacles,
5010
+ diagnostics,
5011
+ softObstacleIndex,
5012
+ hardObstacleIndex
5013
+ );
5014
+ if (!routeIntersectsObstacles(
5015
+ fullFinalized,
5016
+ softObstacles,
5017
+ softObstacleIndex
5018
+ ) && !routeIntersectsObstacles(
5019
+ fullFinalized,
5020
+ hardObstacles,
5021
+ hardObstacleIndex
5022
+ )) {
5023
+ checkBacktracking(
5024
+ fullFinalized,
5025
+ source,
5026
+ target,
5027
+ diagnostics,
5028
+ input.maxBacktrackingRatio
5029
+ );
5030
+ return { points: fullFinalized, diagnostics };
5031
+ }
5032
+ recordRejected(fullFinalized);
5033
+ }
5034
+ const gridPath = findObstacleFreePath(
5035
+ source,
5036
+ target,
5037
+ allObstacles,
5038
+ {
5039
+ endpointObstacles,
5040
+ margin: 0,
5041
+ corridorMargin,
5042
+ maxNodes: budget.maxNodes
5043
+ },
5044
+ diagnostics
5045
+ );
5046
+ if (gridPath !== null && gridPath.length >= 2) {
5047
+ const gridFinalized = finalizeRoute(
5048
+ gridPath,
5049
+ softObstacles,
5050
+ hardObstacles,
5051
+ diagnostics,
5052
+ softObstacleIndex,
5053
+ hardObstacleIndex
5054
+ );
5055
+ if (!routeIntersectsObstacles(
5056
+ gridFinalized,
5057
+ softObstacles,
5058
+ softObstacleIndex
5059
+ ) && !routeIntersectsObstacles(
5060
+ gridFinalized,
5061
+ hardObstacles,
5062
+ hardObstacleIndex
5063
+ )) {
5064
+ checkBacktracking(
5065
+ gridFinalized,
5066
+ source,
5067
+ target,
5068
+ diagnostics,
5069
+ input.maxBacktrackingRatio
5070
+ );
5071
+ return { points: gridFinalized, diagnostics };
5072
+ }
5073
+ recordRejected(gridFinalized);
5074
+ }
5075
+ }
4788
5076
  }
4789
5077
  }
4790
5078
  }
@@ -4846,7 +5134,8 @@ function routeEdge(input) {
4846
5134
  finalizedClean,
4847
5135
  candidate.points[0],
4848
5136
  candidate.points[candidate.points.length - 1],
4849
- diagnostics
5137
+ diagnostics,
5138
+ input.maxBacktrackingRatio
4850
5139
  );
4851
5140
  return { points: finalizedClean, diagnostics };
4852
5141
  }
@@ -4912,13 +5201,41 @@ function routeEdge(input) {
4912
5201
  code: "routing.obstacle.unavoidable",
4913
5202
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
4914
5203
  });
4915
- return {
4916
- points: finalizeRoute(
4917
- 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,
4918
5214
  softObstacles,
4919
5215
  hardObstacles,
4920
5216
  diagnostics
4921
- ),
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,
4922
5239
  diagnostics
4923
5240
  };
4924
5241
  }
@@ -4950,6 +5267,22 @@ function routeEdge(input) {
4950
5267
  maxAttempts
4951
5268
  );
4952
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
+ }
4953
5286
  diagnostics.push({
4954
5287
  severity: "error",
4955
5288
  code: "routing.evidence.crossing_forbidden",
@@ -4997,13 +5330,41 @@ function routeEdge(input) {
4997
5330
  code: "routing.obstacle.unavoidable",
4998
5331
  message: "No bounded orthogonal route candidate avoided all obstacles."
4999
5332
  });
5000
- return {
5001
- points: finalizeRoute(
5002
- 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,
5003
5343
  softObstacles,
5004
5344
  hardObstacles,
5005
5345
  diagnostics
5006
- ),
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,
5007
5368
  diagnostics
5008
5369
  };
5009
5370
  }
@@ -5502,6 +5863,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5502
5863
  }
5503
5864
  return false;
5504
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
+ }
5505
5884
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5506
5885
  for (let index = 0; index < points.length - 1; index += 1) {
5507
5886
  const a = points[index];
@@ -5674,7 +6053,7 @@ function solveDiagram(diagram, options = {}) {
5674
6053
  edges: styledEdges
5675
6054
  });
5676
6055
  diagnostics.push(...layout2.diagnostics);
5677
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6056
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5678
6057
  layout2.boxes,
5679
6058
  styledNodes,
5680
6059
  styledEdges,
@@ -5682,7 +6061,8 @@ function solveDiagram(diagram, options = {}) {
5682
6061
  options,
5683
6062
  diagnostics
5684
6063
  );
5685
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6064
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6065
+ const diagCountBefore = diagnostics.length;
5686
6066
  const rewrapped = wrapHorizontalStackIfNeeded(
5687
6067
  initialNodeBoxes,
5688
6068
  styledNodes,
@@ -5693,6 +6073,20 @@ function solveDiagram(diagram, options = {}) {
5693
6073
  for (const [id, box] of rewrapped) {
5694
6074
  initialNodeBoxes.set(id, box);
5695
6075
  }
6076
+ if (diagnostics.length > diagCountBefore) {
6077
+ for (const node of styledNodes) {
6078
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6079
+ const rwBox = rewrapped.get(node.id);
6080
+ const idx = styledNodes.indexOf(node);
6081
+ if (idx !== -1) {
6082
+ styledNodes[idx] = {
6083
+ ...node,
6084
+ position: { x: rwBox.x, y: rwBox.y }
6085
+ };
6086
+ }
6087
+ }
6088
+ }
6089
+ }
5696
6090
  }
5697
6091
  if (useRecursive && "groupBoxes" in layout2) {
5698
6092
  const recursiveLayout = layout2;
@@ -5706,7 +6100,7 @@ function solveDiagram(diagram, options = {}) {
5706
6100
  overlapSpacing: options?.overlapSpacing ?? 40,
5707
6101
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5708
6102
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5709
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6103
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5710
6104
  swimlanes: styledSwimlanes,
5711
6105
  boxes: initialNodeBoxes,
5712
6106
  nodes: styledNodes,
@@ -5721,7 +6115,8 @@ function solveDiagram(diagram, options = {}) {
5721
6115
  constrained.boxes,
5722
6116
  constrained.locks,
5723
6117
  options?.overlapSpacing ?? 40,
5724
- Math.max(0, options?.minLaneGutter ?? 0)
6118
+ Math.max(0, options?.minLaneGutter ?? 0),
6119
+ options.distributeContainedChildren ?? true
5725
6120
  );
5726
6121
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5727
6122
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -5888,7 +6283,8 @@ function solveDiagram(diagram, options = {}) {
5888
6283
  diagram.direction,
5889
6284
  options,
5890
6285
  diagnostics,
5891
- coordinatedGroups
6286
+ coordinatedGroups,
6287
+ contentBounds
5892
6288
  );
5893
6289
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5894
6290
  coordinatedEdges,
@@ -6307,7 +6703,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6307
6703
  function containsCjk(value) {
6308
6704
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6309
6705
  }
6310
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6706
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6311
6707
  const layouts = /* @__PURE__ */ new Map();
6312
6708
  const diagnostics = [];
6313
6709
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6326,7 +6722,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6326
6722
  locks,
6327
6723
  diagnostics,
6328
6724
  movedChildIds,
6329
- laneGutter
6725
+ laneGutter,
6726
+ constraints,
6727
+ distributeContainedChildren
6330
6728
  );
6331
6729
  if (layout2 !== void 0) {
6332
6730
  layouts.set(swimlane.id, layout2);
@@ -6422,9 +6820,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6422
6820
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6423
6821
  return new Map(boxes);
6424
6822
  }
6425
- const maxRowDepth = options.maxRowDepth;
6426
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6427
- return new Map(boxes);
6823
+ let maxRowDepth = options.maxRowDepth;
6824
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6825
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6826
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6827
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6828
+ } else {
6829
+ return new Map(boxes);
6830
+ }
6428
6831
  }
6429
6832
  const ordered = [...nodes].sort((a, b) => {
6430
6833
  const ba = boxes.get(a.id);
@@ -6485,10 +6888,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6485
6888
  });
6486
6889
  }
6487
6890
  function isStackRunaway(boxes, nodes, direction, options) {
6488
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6891
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6489
6892
  return false;
6490
6893
  }
6491
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6894
+ if (nodes.length < 2) {
6492
6895
  return false;
6493
6896
  }
6494
6897
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6496,17 +6899,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
6496
6899
  return false;
6497
6900
  }
6498
6901
  const bounds = unionBoxes(nodeBoxes);
6499
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6500
- const preferred = options.preferredAspectRatio ?? 3;
6501
- if (aspectRatio < preferred) {
6902
+ const isHorizontal = direction === "TB" || direction === "BT";
6903
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6904
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6905
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6502
6906
  return false;
6503
6907
  }
6908
+ if (isHorizontal) {
6909
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6910
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6911
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6912
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6913
+ }
6504
6914
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6505
6915
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6506
6916
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6507
6917
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6508
6918
  }
6509
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6919
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6510
6920
  const headerHeight = swimlane.headerHeight ?? 28;
6511
6921
  const padding = swimlane.padding ?? 16;
6512
6922
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6531,7 +6941,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6531
6941
  locks,
6532
6942
  diagnostics,
6533
6943
  movedChildIds,
6534
- laneGutter
6944
+ laneGutter,
6945
+ constraints,
6946
+ distributeContainedChildren
6535
6947
  );
6536
6948
  }
6537
6949
  return applyHorizontalSwimlaneContract(
@@ -6546,13 +6958,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6546
6958
  laneGutter
6547
6959
  );
6548
6960
  }
6549
- 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) {
6550
6962
  const populatedBounds = laneBounds.filter(
6551
6963
  (box) => box !== void 0
6552
6964
  );
6553
6965
  const top = Math.min(...populatedBounds.map((box) => box.y));
6554
6966
  const left = Math.min(...populatedBounds.map((box) => box.x));
6555
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
+ }
6556
6984
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6557
6985
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6558
6986
  const rankStackGap = Math.max(8, padding / 2);
@@ -6564,7 +6992,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6564
6992
  );
6565
6993
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6566
6994
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6567
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6995
+ const spreadWidth = maxCrossAxisSpreadWidth(
6996
+ swimlane,
6997
+ nodeBoxes,
6998
+ flowRanks,
6999
+ locks,
7000
+ rankStackGap,
7001
+ containedChildIds
7002
+ );
7003
+ const slotWidth = Math.max(
7004
+ Math.max(...populatedBounds.map((box) => box.width)),
7005
+ spreadWidth
7006
+ ) + padding * 2;
6568
7007
  const laneStep = slotWidth + laneGutter;
6569
7008
  const laneContentTop = top + headerHeight + padding;
6570
7009
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6578,6 +7017,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6578
7017
  y: laneContentTop
6579
7018
  };
6580
7019
  if (maxRank === 0) {
7020
+ const distributable = lane.children.filter(
7021
+ (childId) => !locks.has(childId)
7022
+ );
7023
+ const coveredByContainment = lane.children.some(
7024
+ (childId) => containedChildIds.has(childId)
7025
+ );
7026
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7027
+ moveRankedVerticalLaneChildren(
7028
+ lane.children,
7029
+ nodeBoxes,
7030
+ locks,
7031
+ diagnostics,
7032
+ movedChildIds,
7033
+ flowRanks,
7034
+ rankSpacing,
7035
+ rankStackGap,
7036
+ { x: target.x, y: laneContentTop },
7037
+ slotWidth - padding * 2
7038
+ );
7039
+ continue;
7040
+ }
6581
7041
  moveLaneChildren(
6582
7042
  lane.children,
6583
7043
  nodeBoxes,
@@ -6591,6 +7051,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6591
7051
  );
6592
7052
  continue;
6593
7053
  }
7054
+ const rankedCoveredByContainment = lane.children.some(
7055
+ (childId) => containedChildIds.has(childId)
7056
+ );
6594
7057
  moveRankedVerticalLaneChildren(
6595
7058
  lane.children,
6596
7059
  nodeBoxes,
@@ -6600,10 +7063,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6600
7063
  flowRanks,
6601
7064
  rankSpacing,
6602
7065
  rankStackGap,
6603
- {
6604
- x: target.x - bounds.x,
6605
- y: laneContentTop
6606
- }
7066
+ { x: target.x, y: laneContentTop },
7067
+ slotWidth - padding * 2,
7068
+ rankedCoveredByContainment
6607
7069
  );
6608
7070
  }
6609
7071
  return {
@@ -6712,31 +7174,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6712
7174
  }
6713
7175
  return maxHeight;
6714
7176
  }
6715
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7177
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7178
+ function crossAxisSpreadWidth(items, gap) {
7179
+ return items.reduce(
7180
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7181
+ 0
7182
+ );
7183
+ }
7184
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7185
+ let maxWidth = 0;
7186
+ for (const lane of swimlane.lanes) {
7187
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7188
+ continue;
7189
+ }
7190
+ for (const stack of rankStacks(
7191
+ lane.children,
7192
+ nodeBoxes,
7193
+ flowRanks
7194
+ ).values()) {
7195
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7196
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7197
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7198
+ }
7199
+ }
7200
+ return maxWidth;
7201
+ }
7202
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6716
7203
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6717
- let yOffset = 0;
7204
+ const unlocked = [];
6718
7205
  for (const item of stack) {
6719
- const { childId, box } = item;
6720
- if (locks.has(childId)) {
7206
+ if (locks.has(item.childId)) {
6721
7207
  diagnostics.push({
6722
7208
  severity: "warning",
6723
7209
  code: "constraints.locked-target-not-moved",
6724
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7210
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6725
7211
  path: ["swimlanes"],
6726
- detail: { nodeId: childId }
7212
+ detail: { nodeId: item.childId }
6727
7213
  });
6728
- continue;
7214
+ } else {
7215
+ unlocked.push(item);
6729
7216
  }
7217
+ }
7218
+ if (unlocked.length === 0) continue;
7219
+ if (unlocked.length === 1) {
7220
+ const { childId, box } = unlocked[0];
6730
7221
  const next = {
6731
7222
  ...box,
6732
- x: box.x + target.x,
6733
- y: target.y + rank * rankSpacing + yOffset
7223
+ x: target.x + (contentWidth - box.width) / 2,
7224
+ y: target.y + rank * rankSpacing
6734
7225
  };
6735
7226
  if (next.x !== box.x || next.y !== box.y) {
6736
7227
  movedChildIds.add(childId);
6737
7228
  }
6738
7229
  nodeBoxes.set(childId, next);
6739
- yOffset += box.height + rankStackGap;
7230
+ } else {
7231
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7232
+ if (!shouldSpread) {
7233
+ let yOffset = 0;
7234
+ for (const { childId, box } of unlocked) {
7235
+ const next = {
7236
+ ...box,
7237
+ x: target.x + (contentWidth - box.width) / 2,
7238
+ y: target.y + rank * rankSpacing + yOffset
7239
+ };
7240
+ if (next.x !== box.x || next.y !== box.y) {
7241
+ movedChildIds.add(childId);
7242
+ }
7243
+ nodeBoxes.set(childId, next);
7244
+ yOffset += box.height + rankStackGap;
7245
+ }
7246
+ } else {
7247
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
7248
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
7249
+ for (const { childId, box } of unlocked) {
7250
+ const next = {
7251
+ ...box,
7252
+ x: xCursor,
7253
+ y: target.y + rank * rankSpacing
7254
+ };
7255
+ if (next.x !== box.x || next.y !== box.y) {
7256
+ movedChildIds.add(childId);
7257
+ }
7258
+ nodeBoxes.set(childId, next);
7259
+ xCursor += box.width + rankStackGap;
7260
+ }
7261
+ diagnostics.push({
7262
+ severity: "info",
7263
+ code: "swimlane_contract.cross_axis_distributed",
7264
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7265
+ path: ["swimlanes"],
7266
+ detail: {
7267
+ rank,
7268
+ childCount: unlocked.length,
7269
+ contentWidth
7270
+ }
7271
+ });
7272
+ }
6740
7273
  }
6741
7274
  }
6742
7275
  }
@@ -7075,7 +7608,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7075
7608
  });
7076
7609
  continue;
7077
7610
  }
7078
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7611
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7079
7612
  const geometry = computeShapeGeometry({
7080
7613
  shape: node.shape,
7081
7614
  box,
@@ -7169,7 +7702,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7169
7702
  }
7170
7703
  }
7171
7704
  }
7172
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7705
+ function coordinatePorts(node, nodeBox, portShifting) {
7173
7706
  const portsBySide = /* @__PURE__ */ new Map();
7174
7707
  for (const port of node.ports ?? []) {
7175
7708
  const ports = portsBySide.get(port.side) ?? [];
@@ -7192,9 +7725,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7192
7725
  side,
7193
7726
  index,
7194
7727
  sorted.length,
7195
- portShifting,
7196
- diagnostics,
7197
- node.id
7728
+ portShifting
7198
7729
  );
7199
7730
  const box = portBox(anchor);
7200
7731
  coordinated.push({ ...port, box, anchor });
@@ -7202,32 +7733,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7202
7733
  }
7203
7734
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7204
7735
  }
7205
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7736
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7206
7737
  const shiftingEnabled = portShifting?.enabled ?? true;
7207
7738
  const requestedSpacing = portShifting?.spacing ?? 24;
7208
7739
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7209
7740
  const availableSpan = 2 * maxOffset;
7210
7741
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7211
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7742
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7212
7743
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7213
7744
  minSpacing
7214
7745
  ) : requestedSpacing;
7215
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
7216
- diagnostics.push({
7217
- severity: "warning",
7218
- code: "port_constraint_overlap",
7219
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
7220
- path: ["nodes", nodeId, "ports"],
7221
- detail: {
7222
- nodeId,
7223
- side,
7224
- requestedSpacing,
7225
- effectiveSpacing: Math.round(effectiveSpacing),
7226
- portCount: count
7227
- }
7228
- });
7229
- }
7230
- const spacing = effectiveSpacing;
7231
7746
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7232
7747
  switch (side) {
7233
7748
  case "left":
@@ -7842,14 +8357,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
7842
8357
  }
7843
8358
  };
7844
8359
  }
7845
- 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) {
7846
8361
  const coordinated = [];
7847
8362
  const coordinatedNodeById = new Map(
7848
8363
  coordinatedNodes.map((node) => [node.id, node])
7849
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;
7850
8372
  const nodeObstacleIndex = createBoxSpatialIndex(
7851
8373
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
7852
- options.routingGutter ?? 160
8374
+ queryGutter
7853
8375
  );
7854
8376
  for (const edge of edges) {
7855
8377
  const source = nodes.get(edge.source.nodeId);
@@ -7871,11 +8393,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7871
8393
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
7872
8394
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
7873
8395
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
7874
- const corridor = edgeCorridorBox(
7875
- source.box,
7876
- target.box,
7877
- options.routingGutter ?? 160
7878
- );
8396
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
7879
8397
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7880
8398
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7881
8399
  );
@@ -7893,7 +8411,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7893
8411
  ...routeTextObstacles
7894
8412
  ],
7895
8413
  hardObstacles,
7896
- ...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 }
7897
8417
  });
7898
8418
  diagnostics.push(
7899
8419
  ...route.diagnostics.map((diagnostic) => ({