@crazyhappyone/auto-graph 0.2.8 → 0.2.11

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
  }
@@ -4160,8 +4180,59 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4160
4180
  turnPenalty,
4161
4181
  segmentPenalty
4162
4182
  );
4163
- if (path === null) return null;
4164
- 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: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4214
+ });
4215
+ return null;
4216
+ }
4217
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4218
+ connectHorizontalEdges(
4219
+ nodesFull,
4220
+ ysFull,
4221
+ obstacles,
4222
+ endpointObstacles,
4223
+ margin
4224
+ );
4225
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4226
+ const pathFull = aStarSearch(
4227
+ nodesFull,
4228
+ idxFull,
4229
+ source,
4230
+ target,
4231
+ turnPenalty,
4232
+ segmentPenalty
4233
+ );
4234
+ if (pathFull === null) return null;
4235
+ return simplifyRoute(pathFull);
4165
4236
  }
4166
4237
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4167
4238
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -4388,7 +4459,7 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
4388
4459
  const turnPenalty = options.turnPenalty ?? 50;
4389
4460
  const segmentPenalty = options.segmentPenalty ?? 1;
4390
4461
  const endpointObstacles = options.endpointObstacles ?? [];
4391
- const maxCorners = options.maxCorners ?? 300;
4462
+ const maxCorners = options.maxCorners ?? 600;
4392
4463
  const vertices = collectCornerVertices(source, target, obstacles, margin);
4393
4464
  if (vertices.length > maxCorners) {
4394
4465
  diagnostics?.push({
@@ -4669,7 +4740,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4669
4740
  }
4670
4741
 
4671
4742
  // src/routing/routes.ts
4672
- function checkBacktracking(points, source, target, diagnostics) {
4743
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4673
4744
  if (points.length < 2) return;
4674
4745
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4675
4746
  if (direct <= 0) return;
@@ -4679,7 +4750,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4679
4750
  const b = points[i + 1];
4680
4751
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4681
4752
  }
4682
- const threshold = 10;
4753
+ const threshold = maxRatio ?? 20;
4683
4754
  if (routeLen > direct * threshold) {
4684
4755
  diagnostics.push({
4685
4756
  severity: "warning",
@@ -4697,8 +4768,20 @@ function routeEdge(input) {
4697
4768
  const diagnostics = [];
4698
4769
  const softObstacles = input.obstacles ?? [];
4699
4770
  const hardObstacles = input.hardObstacles ?? [];
4771
+ let bestRejectedPath;
4772
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4700
4773
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4701
4774
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
4775
+ const recordRejected = (candidate) => {
4776
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
4777
+ return;
4778
+ }
4779
+ const crossings = countObstacleCrossings(candidate, softObstacles);
4780
+ if (crossings < bestRejectedCrossings) {
4781
+ bestRejectedCrossings = crossings;
4782
+ bestRejectedPath = candidate;
4783
+ }
4784
+ };
4702
4785
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4703
4786
  const defaultAnchors = defaultAnchorsForGeometry(
4704
4787
  input.source.box,
@@ -4757,18 +4840,38 @@ function routeEdge(input) {
4757
4840
  input.source.center,
4758
4841
  targetAnchor
4759
4842
  );
4760
- const cornerPath = findCornerGraphPath(
4843
+ const allObstacles = [...softObstacles, ...hardObstacles];
4844
+ const corridorMargin = input.corridorMargin ?? 32;
4845
+ const corridorObstacles = filterObstaclesByCorridor(
4846
+ source,
4847
+ target,
4848
+ allObstacles,
4849
+ [],
4850
+ // endpointObstacles passed separately via options
4851
+ corridorMargin
4852
+ );
4853
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4854
+ let cornerPath = findCornerGraphPath(
4761
4855
  source,
4762
4856
  target,
4763
- [...softObstacles, ...hardObstacles],
4857
+ cornerObstacles,
4764
4858
  { endpointObstacles, margin: 2 },
4765
4859
  diagnostics
4766
4860
  );
4861
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4862
+ cornerPath = findCornerGraphPath(
4863
+ source,
4864
+ target,
4865
+ allObstacles,
4866
+ { endpointObstacles, margin: 2 },
4867
+ diagnostics
4868
+ );
4869
+ }
4767
4870
  const path = cornerPath ?? findObstacleFreePath(
4768
4871
  source,
4769
4872
  target,
4770
- [...softObstacles, ...hardObstacles],
4771
- { endpointObstacles, margin: 0 },
4873
+ allObstacles,
4874
+ { endpointObstacles, margin: 0, corridorMargin },
4772
4875
  diagnostics
4773
4876
  );
4774
4877
  if (path !== null && path.length >= 2) {
@@ -4785,9 +4888,90 @@ function routeEdge(input) {
4785
4888
  softObstacles,
4786
4889
  softObstacleIndex
4787
4890
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4788
- checkBacktracking(finalized, source, target, diagnostics);
4891
+ checkBacktracking(
4892
+ finalized,
4893
+ source,
4894
+ target,
4895
+ diagnostics,
4896
+ input.maxBacktrackingRatio
4897
+ );
4789
4898
  return { points: finalized, diagnostics };
4790
4899
  }
4900
+ recordRejected(finalized);
4901
+ if (cornerPath !== null) {
4902
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4903
+ source,
4904
+ target,
4905
+ allObstacles,
4906
+ { endpointObstacles, margin: 2 },
4907
+ diagnostics
4908
+ ) : null;
4909
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
4910
+ const fullFinalized = finalizeRoute(
4911
+ fullCornerPath,
4912
+ softObstacles,
4913
+ hardObstacles,
4914
+ diagnostics,
4915
+ softObstacleIndex,
4916
+ hardObstacleIndex
4917
+ );
4918
+ if (!routeIntersectsObstacles(
4919
+ fullFinalized,
4920
+ softObstacles,
4921
+ softObstacleIndex
4922
+ ) && !routeIntersectsObstacles(
4923
+ fullFinalized,
4924
+ hardObstacles,
4925
+ hardObstacleIndex
4926
+ )) {
4927
+ checkBacktracking(
4928
+ fullFinalized,
4929
+ source,
4930
+ target,
4931
+ diagnostics,
4932
+ input.maxBacktrackingRatio
4933
+ );
4934
+ return { points: fullFinalized, diagnostics };
4935
+ }
4936
+ recordRejected(fullFinalized);
4937
+ }
4938
+ const gridPath = findObstacleFreePath(
4939
+ source,
4940
+ target,
4941
+ allObstacles,
4942
+ { endpointObstacles, margin: 0, corridorMargin },
4943
+ diagnostics
4944
+ );
4945
+ if (gridPath !== null && gridPath.length >= 2) {
4946
+ const gridFinalized = finalizeRoute(
4947
+ gridPath,
4948
+ softObstacles,
4949
+ hardObstacles,
4950
+ diagnostics,
4951
+ softObstacleIndex,
4952
+ hardObstacleIndex
4953
+ );
4954
+ if (!routeIntersectsObstacles(
4955
+ gridFinalized,
4956
+ softObstacles,
4957
+ softObstacleIndex
4958
+ ) && !routeIntersectsObstacles(
4959
+ gridFinalized,
4960
+ hardObstacles,
4961
+ hardObstacleIndex
4962
+ )) {
4963
+ checkBacktracking(
4964
+ gridFinalized,
4965
+ source,
4966
+ target,
4967
+ diagnostics,
4968
+ input.maxBacktrackingRatio
4969
+ );
4970
+ return { points: gridFinalized, diagnostics };
4971
+ }
4972
+ recordRejected(gridFinalized);
4973
+ }
4974
+ }
4791
4975
  }
4792
4976
  }
4793
4977
  }
@@ -4849,7 +5033,8 @@ function routeEdge(input) {
4849
5033
  finalizedClean,
4850
5034
  candidate.points[0],
4851
5035
  candidate.points[candidate.points.length - 1],
4852
- diagnostics
5036
+ diagnostics,
5037
+ input.maxBacktrackingRatio
4853
5038
  );
4854
5039
  return { points: finalizedClean, diagnostics };
4855
5040
  }
@@ -4915,13 +5100,41 @@ function routeEdge(input) {
4915
5100
  code: "routing.obstacle.unavoidable",
4916
5101
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
4917
5102
  });
4918
- return {
4919
- points: finalizeRoute(
4920
- bestPoints2,
5103
+ const finalizedSoftBest = finalizeRoute(
5104
+ bestPoints2,
5105
+ softObstacles,
5106
+ hardObstacles,
5107
+ diagnostics
5108
+ );
5109
+ let softFallback = finalizedSoftBest;
5110
+ if (bestRejectedPath !== void 0) {
5111
+ const finalizedRejected = finalizeRoute(
5112
+ bestRejectedPath,
4921
5113
  softObstacles,
4922
5114
  hardObstacles,
4923
5115
  diagnostics
4924
- ),
5116
+ );
5117
+ const rejectedCrossings = countObstacleCrossings(
5118
+ finalizedRejected,
5119
+ softObstacles
5120
+ );
5121
+ const heuristicCrossings = countObstacleCrossings(
5122
+ finalizedSoftBest,
5123
+ softObstacles
5124
+ );
5125
+ if (rejectedCrossings < heuristicCrossings) {
5126
+ softFallback = finalizedRejected;
5127
+ }
5128
+ }
5129
+ checkBacktracking(
5130
+ softFallback,
5131
+ softFallback[0],
5132
+ softFallback[softFallback.length - 1],
5133
+ diagnostics,
5134
+ input.maxBacktrackingRatio
5135
+ );
5136
+ return {
5137
+ points: softFallback,
4925
5138
  diagnostics
4926
5139
  };
4927
5140
  }
@@ -4953,6 +5166,22 @@ function routeEdge(input) {
4953
5166
  maxAttempts
4954
5167
  );
4955
5168
  }
5169
+ if (bestRejectedPath !== void 0) {
5170
+ diagnostics.push({
5171
+ severity: "warning",
5172
+ code: "routing.obstacle.unavoidable",
5173
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5174
+ });
5175
+ return {
5176
+ points: finalizeRoute(
5177
+ bestRejectedPath,
5178
+ softObstacles,
5179
+ hardObstacles,
5180
+ diagnostics
5181
+ ),
5182
+ diagnostics
5183
+ };
5184
+ }
4956
5185
  diagnostics.push({
4957
5186
  severity: "error",
4958
5187
  code: "routing.evidence.crossing_forbidden",
@@ -5000,13 +5229,41 @@ function routeEdge(input) {
5000
5229
  code: "routing.obstacle.unavoidable",
5001
5230
  message: "No bounded orthogonal route candidate avoided all obstacles."
5002
5231
  });
5003
- return {
5004
- points: finalizeRoute(
5005
- bestPoints,
5232
+ const finalizedBestPoints = finalizeRoute(
5233
+ bestPoints,
5234
+ softObstacles,
5235
+ hardObstacles,
5236
+ diagnostics
5237
+ );
5238
+ let fallbackPoints = finalizedBestPoints;
5239
+ if (bestRejectedPath !== void 0) {
5240
+ const finalizedRejected = finalizeRoute(
5241
+ bestRejectedPath,
5006
5242
  softObstacles,
5007
5243
  hardObstacles,
5008
5244
  diagnostics
5009
- ),
5245
+ );
5246
+ const rejectedCrossings = countObstacleCrossings(
5247
+ finalizedRejected,
5248
+ softObstacles
5249
+ );
5250
+ const heuristicCrossings = countObstacleCrossings(
5251
+ finalizedBestPoints,
5252
+ softObstacles
5253
+ );
5254
+ if (rejectedCrossings < heuristicCrossings) {
5255
+ fallbackPoints = finalizedRejected;
5256
+ }
5257
+ }
5258
+ checkBacktracking(
5259
+ fallbackPoints,
5260
+ fallbackPoints[0],
5261
+ fallbackPoints[fallbackPoints.length - 1],
5262
+ diagnostics,
5263
+ input.maxBacktrackingRatio
5264
+ );
5265
+ return {
5266
+ points: fallbackPoints,
5010
5267
  diagnostics
5011
5268
  };
5012
5269
  }
@@ -5505,6 +5762,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5505
5762
  }
5506
5763
  return false;
5507
5764
  }
5765
+ function countObstacleCrossings(points, obstacles) {
5766
+ let count = 0;
5767
+ for (const obstacle of obstacles) {
5768
+ validateBox(obstacle);
5769
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
5770
+ const a = points[pointIndex];
5771
+ const b = points[pointIndex + 1];
5772
+ if (a === void 0 || b === void 0) {
5773
+ continue;
5774
+ }
5775
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
5776
+ count += 1;
5777
+ break;
5778
+ }
5779
+ }
5780
+ }
5781
+ return count;
5782
+ }
5508
5783
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5509
5784
  for (let index = 0; index < points.length - 1; index += 1) {
5510
5785
  const a = points[index];
@@ -5677,7 +5952,7 @@ function solveDiagram(diagram, options = {}) {
5677
5952
  edges: styledEdges
5678
5953
  });
5679
5954
  diagnostics.push(...layout2.diagnostics);
5680
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
5955
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5681
5956
  layout2.boxes,
5682
5957
  styledNodes,
5683
5958
  styledEdges,
@@ -5685,7 +5960,8 @@ function solveDiagram(diagram, options = {}) {
5685
5960
  options,
5686
5961
  diagnostics
5687
5962
  );
5688
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
5963
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
5964
+ const diagCountBefore = diagnostics.length;
5689
5965
  const rewrapped = wrapHorizontalStackIfNeeded(
5690
5966
  initialNodeBoxes,
5691
5967
  styledNodes,
@@ -5696,6 +5972,20 @@ function solveDiagram(diagram, options = {}) {
5696
5972
  for (const [id, box] of rewrapped) {
5697
5973
  initialNodeBoxes.set(id, box);
5698
5974
  }
5975
+ if (diagnostics.length > diagCountBefore) {
5976
+ for (const node of styledNodes) {
5977
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
5978
+ const rwBox = rewrapped.get(node.id);
5979
+ const idx = styledNodes.indexOf(node);
5980
+ if (idx !== -1) {
5981
+ styledNodes[idx] = {
5982
+ ...node,
5983
+ position: { x: rwBox.x, y: rwBox.y }
5984
+ };
5985
+ }
5986
+ }
5987
+ }
5988
+ }
5699
5989
  }
5700
5990
  if (useRecursive && "groupBoxes" in layout2) {
5701
5991
  const recursiveLayout = layout2;
@@ -5709,7 +5999,7 @@ function solveDiagram(diagram, options = {}) {
5709
5999
  overlapSpacing: options?.overlapSpacing ?? 40,
5710
6000
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5711
6001
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5712
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6002
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5713
6003
  swimlanes: styledSwimlanes,
5714
6004
  boxes: initialNodeBoxes,
5715
6005
  nodes: styledNodes,
@@ -5724,7 +6014,8 @@ function solveDiagram(diagram, options = {}) {
5724
6014
  constrained.boxes,
5725
6015
  constrained.locks,
5726
6016
  options?.overlapSpacing ?? 40,
5727
- Math.max(0, options?.minLaneGutter ?? 0)
6017
+ Math.max(0, options?.minLaneGutter ?? 0),
6018
+ options.distributeContainedChildren ?? true
5728
6019
  );
5729
6020
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5730
6021
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -5891,7 +6182,8 @@ function solveDiagram(diagram, options = {}) {
5891
6182
  diagram.direction,
5892
6183
  options,
5893
6184
  diagnostics,
5894
- coordinatedGroups
6185
+ coordinatedGroups,
6186
+ contentBounds
5895
6187
  );
5896
6188
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5897
6189
  coordinatedEdges,
@@ -6310,7 +6602,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6310
6602
  function containsCjk(value) {
6311
6603
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6312
6604
  }
6313
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6605
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6314
6606
  const layouts = /* @__PURE__ */ new Map();
6315
6607
  const diagnostics = [];
6316
6608
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6329,7 +6621,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6329
6621
  locks,
6330
6622
  diagnostics,
6331
6623
  movedChildIds,
6332
- laneGutter
6624
+ laneGutter,
6625
+ constraints,
6626
+ distributeContainedChildren
6333
6627
  );
6334
6628
  if (layout2 !== void 0) {
6335
6629
  layouts.set(swimlane.id, layout2);
@@ -6425,9 +6719,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6425
6719
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6426
6720
  return new Map(boxes);
6427
6721
  }
6428
- const maxRowDepth = options.maxRowDepth;
6429
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6430
- return new Map(boxes);
6722
+ let maxRowDepth = options.maxRowDepth;
6723
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6724
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6725
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6726
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6727
+ } else {
6728
+ return new Map(boxes);
6729
+ }
6431
6730
  }
6432
6731
  const ordered = [...nodes].sort((a, b) => {
6433
6732
  const ba = boxes.get(a.id);
@@ -6488,10 +6787,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6488
6787
  });
6489
6788
  }
6490
6789
  function isStackRunaway(boxes, nodes, direction, options) {
6491
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6790
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6492
6791
  return false;
6493
6792
  }
6494
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6793
+ if (nodes.length < 2) {
6495
6794
  return false;
6496
6795
  }
6497
6796
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6499,17 +6798,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
6499
6798
  return false;
6500
6799
  }
6501
6800
  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) {
6801
+ const isHorizontal = direction === "TB" || direction === "BT";
6802
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6803
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6804
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6505
6805
  return false;
6506
6806
  }
6807
+ if (isHorizontal) {
6808
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6809
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6810
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6811
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6812
+ }
6507
6813
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6508
6814
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6509
6815
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6510
6816
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6511
6817
  }
6512
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6818
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6513
6819
  const headerHeight = swimlane.headerHeight ?? 28;
6514
6820
  const padding = swimlane.padding ?? 16;
6515
6821
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6534,7 +6840,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6534
6840
  locks,
6535
6841
  diagnostics,
6536
6842
  movedChildIds,
6537
- laneGutter
6843
+ laneGutter,
6844
+ constraints,
6845
+ distributeContainedChildren
6538
6846
  );
6539
6847
  }
6540
6848
  return applyHorizontalSwimlaneContract(
@@ -6549,13 +6857,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6549
6857
  laneGutter
6550
6858
  );
6551
6859
  }
6552
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
6860
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6553
6861
  const populatedBounds = laneBounds.filter(
6554
6862
  (box) => box !== void 0
6555
6863
  );
6556
6864
  const top = Math.min(...populatedBounds.map((box) => box.y));
6557
6865
  const left = Math.min(...populatedBounds.map((box) => box.x));
6558
6866
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
6867
+ const containedChildIds = /* @__PURE__ */ new Set();
6868
+ if (distributeContainedChildren) {
6869
+ for (const c of constraints) {
6870
+ if (c.kind !== "containment") continue;
6871
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
6872
+ const distributable = c.childIds.filter((childId) => {
6873
+ if (nodeBoxes.get(childId) === void 0) return false;
6874
+ const lock = locks.get(childId);
6875
+ return lock === void 0 || lock.source === "fixed-position";
6876
+ });
6877
+ if (distributable.length < 2) continue;
6878
+ for (const childId of distributable) {
6879
+ containedChildIds.add(childId);
6880
+ }
6881
+ }
6882
+ }
6559
6883
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6560
6884
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6561
6885
  const rankStackGap = Math.max(8, padding / 2);
@@ -6567,7 +6891,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6567
6891
  );
6568
6892
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6569
6893
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6570
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6894
+ const spreadWidth = maxCrossAxisSpreadWidth(
6895
+ swimlane,
6896
+ nodeBoxes,
6897
+ flowRanks,
6898
+ locks,
6899
+ rankStackGap,
6900
+ containedChildIds
6901
+ );
6902
+ const slotWidth = Math.max(
6903
+ Math.max(...populatedBounds.map((box) => box.width)),
6904
+ spreadWidth
6905
+ ) + padding * 2;
6571
6906
  const laneStep = slotWidth + laneGutter;
6572
6907
  const laneContentTop = top + headerHeight + padding;
6573
6908
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6581,6 +6916,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6581
6916
  y: laneContentTop
6582
6917
  };
6583
6918
  if (maxRank === 0) {
6919
+ const distributable = lane.children.filter(
6920
+ (childId) => !locks.has(childId)
6921
+ );
6922
+ const coveredByContainment = lane.children.some(
6923
+ (childId) => containedChildIds.has(childId)
6924
+ );
6925
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6926
+ moveRankedVerticalLaneChildren(
6927
+ lane.children,
6928
+ nodeBoxes,
6929
+ locks,
6930
+ diagnostics,
6931
+ movedChildIds,
6932
+ flowRanks,
6933
+ rankSpacing,
6934
+ rankStackGap,
6935
+ { x: target.x, y: laneContentTop },
6936
+ slotWidth - padding * 2
6937
+ );
6938
+ continue;
6939
+ }
6584
6940
  moveLaneChildren(
6585
6941
  lane.children,
6586
6942
  nodeBoxes,
@@ -6594,6 +6950,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6594
6950
  );
6595
6951
  continue;
6596
6952
  }
6953
+ const rankedCoveredByContainment = lane.children.some(
6954
+ (childId) => containedChildIds.has(childId)
6955
+ );
6597
6956
  moveRankedVerticalLaneChildren(
6598
6957
  lane.children,
6599
6958
  nodeBoxes,
@@ -6603,10 +6962,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6603
6962
  flowRanks,
6604
6963
  rankSpacing,
6605
6964
  rankStackGap,
6606
- {
6607
- x: target.x - bounds.x,
6608
- y: laneContentTop
6609
- }
6965
+ { x: target.x, y: laneContentTop },
6966
+ slotWidth - padding * 2,
6967
+ rankedCoveredByContainment
6610
6968
  );
6611
6969
  }
6612
6970
  return {
@@ -6715,31 +7073,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6715
7073
  }
6716
7074
  return maxHeight;
6717
7075
  }
6718
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7076
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7077
+ function crossAxisSpreadWidth(items, gap) {
7078
+ return items.reduce(
7079
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7080
+ 0
7081
+ );
7082
+ }
7083
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7084
+ let maxWidth = 0;
7085
+ for (const lane of swimlane.lanes) {
7086
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7087
+ continue;
7088
+ }
7089
+ for (const stack of rankStacks(
7090
+ lane.children,
7091
+ nodeBoxes,
7092
+ flowRanks
7093
+ ).values()) {
7094
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7095
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7096
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7097
+ }
7098
+ }
7099
+ return maxWidth;
7100
+ }
7101
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6719
7102
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6720
- let yOffset = 0;
7103
+ const unlocked = [];
6721
7104
  for (const item of stack) {
6722
- const { childId, box } = item;
6723
- if (locks.has(childId)) {
7105
+ if (locks.has(item.childId)) {
6724
7106
  diagnostics.push({
6725
7107
  severity: "warning",
6726
7108
  code: "constraints.locked-target-not-moved",
6727
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7109
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6728
7110
  path: ["swimlanes"],
6729
- detail: { nodeId: childId }
7111
+ detail: { nodeId: item.childId }
6730
7112
  });
6731
- continue;
7113
+ } else {
7114
+ unlocked.push(item);
6732
7115
  }
7116
+ }
7117
+ if (unlocked.length === 0) continue;
7118
+ if (unlocked.length === 1) {
7119
+ const { childId, box } = unlocked[0];
6733
7120
  const next = {
6734
7121
  ...box,
6735
- x: box.x + target.x,
6736
- y: target.y + rank * rankSpacing + yOffset
7122
+ x: target.x + (contentWidth - box.width) / 2,
7123
+ y: target.y + rank * rankSpacing
6737
7124
  };
6738
7125
  if (next.x !== box.x || next.y !== box.y) {
6739
7126
  movedChildIds.add(childId);
6740
7127
  }
6741
7128
  nodeBoxes.set(childId, next);
6742
- yOffset += box.height + rankStackGap;
7129
+ } else {
7130
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7131
+ if (!shouldSpread) {
7132
+ let yOffset = 0;
7133
+ for (const { childId, box } of unlocked) {
7134
+ const next = {
7135
+ ...box,
7136
+ x: target.x + (contentWidth - box.width) / 2,
7137
+ y: target.y + rank * rankSpacing + yOffset
7138
+ };
7139
+ if (next.x !== box.x || next.y !== box.y) {
7140
+ movedChildIds.add(childId);
7141
+ }
7142
+ nodeBoxes.set(childId, next);
7143
+ yOffset += box.height + rankStackGap;
7144
+ }
7145
+ } else {
7146
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
7147
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
7148
+ for (const { childId, box } of unlocked) {
7149
+ const next = {
7150
+ ...box,
7151
+ x: xCursor,
7152
+ y: target.y + rank * rankSpacing
7153
+ };
7154
+ if (next.x !== box.x || next.y !== box.y) {
7155
+ movedChildIds.add(childId);
7156
+ }
7157
+ nodeBoxes.set(childId, next);
7158
+ xCursor += box.width + rankStackGap;
7159
+ }
7160
+ diagnostics.push({
7161
+ severity: "info",
7162
+ code: "swimlane_contract.cross_axis_distributed",
7163
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7164
+ path: ["swimlanes"],
7165
+ detail: {
7166
+ rank,
7167
+ childCount: unlocked.length,
7168
+ contentWidth
7169
+ }
7170
+ });
7171
+ }
6743
7172
  }
6744
7173
  }
6745
7174
  }
@@ -7078,7 +7507,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7078
7507
  });
7079
7508
  continue;
7080
7509
  }
7081
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7510
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7082
7511
  const geometry = computeShapeGeometry({
7083
7512
  shape: node.shape,
7084
7513
  box,
@@ -7172,7 +7601,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7172
7601
  }
7173
7602
  }
7174
7603
  }
7175
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7604
+ function coordinatePorts(node, nodeBox, portShifting) {
7176
7605
  const portsBySide = /* @__PURE__ */ new Map();
7177
7606
  for (const port of node.ports ?? []) {
7178
7607
  const ports = portsBySide.get(port.side) ?? [];
@@ -7195,9 +7624,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7195
7624
  side,
7196
7625
  index,
7197
7626
  sorted.length,
7198
- portShifting,
7199
- diagnostics,
7200
- node.id
7627
+ portShifting
7201
7628
  );
7202
7629
  const box = portBox(anchor);
7203
7630
  coordinated.push({ ...port, box, anchor });
@@ -7205,32 +7632,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7205
7632
  }
7206
7633
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7207
7634
  }
7208
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7635
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7209
7636
  const shiftingEnabled = portShifting?.enabled ?? true;
7210
7637
  const requestedSpacing = portShifting?.spacing ?? 24;
7211
7638
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7212
7639
  const availableSpan = 2 * maxOffset;
7213
7640
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7214
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7641
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7215
7642
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7216
7643
  minSpacing
7217
7644
  ) : 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
7645
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7235
7646
  switch (side) {
7236
7647
  case "left":
@@ -7845,14 +8256,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
7845
8256
  }
7846
8257
  };
7847
8258
  }
7848
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
8259
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
7849
8260
  const coordinated = [];
7850
8261
  const coordinatedNodeById = new Map(
7851
8262
  coordinatedNodes.map((node) => [node.id, node])
7852
8263
  );
8264
+ const corridorMarginOption = options.corridorMargin ?? "auto";
8265
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
8266
+ 200,
8267
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
8268
+ );
8269
+ const routingGutter = options.routingGutter ?? 160;
8270
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
7853
8271
  const nodeObstacleIndex = createBoxSpatialIndex(
7854
8272
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
7855
- options.routingGutter ?? 160
8273
+ queryGutter
7856
8274
  );
7857
8275
  for (const edge of edges) {
7858
8276
  const source = nodes.get(edge.source.nodeId);
@@ -7874,11 +8292,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7874
8292
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
7875
8293
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
7876
8294
  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
- );
8295
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
7882
8296
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7883
8297
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7884
8298
  );
@@ -7896,7 +8310,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7896
8310
  ...routeTextObstacles
7897
8311
  ],
7898
8312
  hardObstacles,
7899
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
8313
+ corridorMargin,
8314
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
8315
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
7900
8316
  });
7901
8317
  diagnostics.push(
7902
8318
  ...route.diagnostics.map((diagnostic) => ({