@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.
package/dist/index.cjs CHANGED
@@ -179,6 +179,28 @@ function applyLayoutConstraints(input) {
179
179
  if (input.distributeContainedChildren) {
180
180
  yieldFixedPositionLocks(input, boxes, locks);
181
181
  }
182
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
183
+ for (const swimlane of input.swimlanes) {
184
+ if (swimlane.layout === "contract") continue;
185
+ for (const lane of swimlane.lanes) {
186
+ const fixedChildren = [];
187
+ let participantCount = 0;
188
+ for (const childId of lane.children) {
189
+ const lock = locks.get(childId);
190
+ if (lock === void 0) {
191
+ participantCount += 1;
192
+ } else if (lock.source === "fixed-position") {
193
+ participantCount += 1;
194
+ fixedChildren.push(childId);
195
+ }
196
+ }
197
+ if (participantCount < 2) continue;
198
+ for (const childId of fixedChildren) {
199
+ locks.delete(childId);
200
+ }
201
+ }
202
+ }
203
+ }
182
204
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
183
205
  applyRelative(input.constraints, boxes, locks, diagnostics);
184
206
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -198,6 +220,9 @@ function applyLayoutConstraints(input) {
198
220
  applyDistributeContained(input, boxes, locks, diagnostics);
199
221
  dedupReplayDiagnostics(diagnostics, diagBefore);
200
222
  }
223
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
224
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
225
+ }
201
226
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
202
227
  reportOverlaps(
203
228
  boxes,
@@ -1037,9 +1062,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1037
1062
  }
1038
1063
  });
1039
1064
  }
1040
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1041
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1042
- }
1043
1065
  }
1044
1066
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1045
1067
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -1079,6 +1101,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1079
1101
  effectiveGap = minGap + remaining / (unlocked.length - 1);
1080
1102
  }
1081
1103
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
1104
+ reserved.sort((a, b) => a.start - b.start);
1082
1105
  let pos = contentStart;
1083
1106
  for (const child of unlocked) {
1084
1107
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4729,15 +4752,12 @@ var BinaryHeap = class {
4729
4752
  let smallestIdx = idx;
4730
4753
  const leftIdx = (idx << 1) + 1;
4731
4754
  const rightIdx = leftIdx + 1;
4732
- if (leftIdx < size && this._less(
4733
- this._data[leftIdx],
4734
- this._data[smallestIdx]
4735
- )) {
4755
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4736
4756
  smallestIdx = leftIdx;
4737
4757
  }
4738
4758
  if (rightIdx < size && this._less(
4739
4759
  this._data[rightIdx],
4740
- this._data[smallestIdx]
4760
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4741
4761
  )) {
4742
4762
  smallestIdx = rightIdx;
4743
4763
  }
@@ -4804,8 +4824,59 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4804
4824
  turnPenalty,
4805
4825
  segmentPenalty
4806
4826
  );
4807
- if (path === null) return null;
4808
- return simplifyRoute(path);
4827
+ if (path !== null) {
4828
+ const simplified = simplifyRoute(path);
4829
+ const filteredSet = new Set(filtered);
4830
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4831
+ let crossesExcluded = false;
4832
+ for (let i = 0; i < simplified.length - 1; i++) {
4833
+ const a = simplified[i];
4834
+ const b = simplified[i + 1];
4835
+ for (const obs of obstacles) {
4836
+ if (filteredSet.has(obs)) continue;
4837
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4838
+ crossesExcluded = true;
4839
+ break;
4840
+ }
4841
+ }
4842
+ if (crossesExcluded) break;
4843
+ }
4844
+ if (!crossesExcluded) return simplified;
4845
+ } else {
4846
+ return simplified;
4847
+ }
4848
+ }
4849
+ if (!useCorridor) return null;
4850
+ const xsFull = collectXs(source, target, obstacles, margin);
4851
+ const ysFull = collectYs(source, target, obstacles, margin);
4852
+ if (xsFull.length * ysFull.length > maxNodes) {
4853
+ diagnostics?.push({
4854
+ severity: "warning",
4855
+ code: "routing.astar.grid_overflow",
4856
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4857
+ detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4858
+ });
4859
+ return null;
4860
+ }
4861
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4862
+ connectHorizontalEdges(
4863
+ nodesFull,
4864
+ ysFull,
4865
+ obstacles,
4866
+ endpointObstacles,
4867
+ margin
4868
+ );
4869
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4870
+ const pathFull = aStarSearch(
4871
+ nodesFull,
4872
+ idxFull,
4873
+ source,
4874
+ target,
4875
+ turnPenalty,
4876
+ segmentPenalty
4877
+ );
4878
+ if (pathFull === null) return null;
4879
+ return simplifyRoute(pathFull);
4809
4880
  }
4810
4881
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4811
4882
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -5032,7 +5103,7 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
5032
5103
  const turnPenalty = options.turnPenalty ?? 50;
5033
5104
  const segmentPenalty = options.segmentPenalty ?? 1;
5034
5105
  const endpointObstacles = options.endpointObstacles ?? [];
5035
- const maxCorners = options.maxCorners ?? 300;
5106
+ const maxCorners = options.maxCorners ?? 600;
5036
5107
  const vertices = collectCornerVertices(source, target, obstacles, margin);
5037
5108
  if (vertices.length > maxCorners) {
5038
5109
  diagnostics?.push({
@@ -5313,7 +5384,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5313
5384
  }
5314
5385
 
5315
5386
  // src/routing/routes.ts
5316
- function checkBacktracking(points, source, target, diagnostics) {
5387
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5317
5388
  if (points.length < 2) return;
5318
5389
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5319
5390
  if (direct <= 0) return;
@@ -5323,7 +5394,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5323
5394
  const b = points[i + 1];
5324
5395
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5325
5396
  }
5326
- const threshold = 10;
5397
+ const threshold = maxRatio ?? 20;
5327
5398
  if (routeLen > direct * threshold) {
5328
5399
  diagnostics.push({
5329
5400
  severity: "warning",
@@ -5341,8 +5412,20 @@ function routeEdge(input) {
5341
5412
  const diagnostics = [];
5342
5413
  const softObstacles = input.obstacles ?? [];
5343
5414
  const hardObstacles = input.hardObstacles ?? [];
5415
+ let bestRejectedPath;
5416
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5344
5417
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5345
5418
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
5419
+ const recordRejected = (candidate) => {
5420
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
5421
+ return;
5422
+ }
5423
+ const crossings = countObstacleCrossings(candidate, softObstacles);
5424
+ if (crossings < bestRejectedCrossings) {
5425
+ bestRejectedCrossings = crossings;
5426
+ bestRejectedPath = candidate;
5427
+ }
5428
+ };
5346
5429
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5347
5430
  const defaultAnchors = defaultAnchorsForGeometry(
5348
5431
  input.source.box,
@@ -5401,18 +5484,38 @@ function routeEdge(input) {
5401
5484
  input.source.center,
5402
5485
  targetAnchor
5403
5486
  );
5404
- const cornerPath = findCornerGraphPath(
5487
+ const allObstacles = [...softObstacles, ...hardObstacles];
5488
+ const corridorMargin = input.corridorMargin ?? 32;
5489
+ const corridorObstacles = filterObstaclesByCorridor(
5490
+ source,
5491
+ target,
5492
+ allObstacles,
5493
+ [],
5494
+ // endpointObstacles passed separately via options
5495
+ corridorMargin
5496
+ );
5497
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5498
+ let cornerPath = findCornerGraphPath(
5405
5499
  source,
5406
5500
  target,
5407
- [...softObstacles, ...hardObstacles],
5501
+ cornerObstacles,
5408
5502
  { endpointObstacles, margin: 2 },
5409
5503
  diagnostics
5410
5504
  );
5505
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
5506
+ cornerPath = findCornerGraphPath(
5507
+ source,
5508
+ target,
5509
+ allObstacles,
5510
+ { endpointObstacles, margin: 2 },
5511
+ diagnostics
5512
+ );
5513
+ }
5411
5514
  const path = cornerPath ?? findObstacleFreePath(
5412
5515
  source,
5413
5516
  target,
5414
- [...softObstacles, ...hardObstacles],
5415
- { endpointObstacles, margin: 0 },
5517
+ allObstacles,
5518
+ { endpointObstacles, margin: 0, corridorMargin },
5416
5519
  diagnostics
5417
5520
  );
5418
5521
  if (path !== null && path.length >= 2) {
@@ -5429,9 +5532,90 @@ function routeEdge(input) {
5429
5532
  softObstacles,
5430
5533
  softObstacleIndex
5431
5534
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5432
- checkBacktracking(finalized, source, target, diagnostics);
5535
+ checkBacktracking(
5536
+ finalized,
5537
+ source,
5538
+ target,
5539
+ diagnostics,
5540
+ input.maxBacktrackingRatio
5541
+ );
5433
5542
  return { points: finalized, diagnostics };
5434
5543
  }
5544
+ recordRejected(finalized);
5545
+ if (cornerPath !== null) {
5546
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5547
+ source,
5548
+ target,
5549
+ allObstacles,
5550
+ { endpointObstacles, margin: 2 },
5551
+ diagnostics
5552
+ ) : null;
5553
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5554
+ const fullFinalized = finalizeRoute(
5555
+ fullCornerPath,
5556
+ softObstacles,
5557
+ hardObstacles,
5558
+ diagnostics,
5559
+ softObstacleIndex,
5560
+ hardObstacleIndex
5561
+ );
5562
+ if (!routeIntersectsObstacles(
5563
+ fullFinalized,
5564
+ softObstacles,
5565
+ softObstacleIndex
5566
+ ) && !routeIntersectsObstacles(
5567
+ fullFinalized,
5568
+ hardObstacles,
5569
+ hardObstacleIndex
5570
+ )) {
5571
+ checkBacktracking(
5572
+ fullFinalized,
5573
+ source,
5574
+ target,
5575
+ diagnostics,
5576
+ input.maxBacktrackingRatio
5577
+ );
5578
+ return { points: fullFinalized, diagnostics };
5579
+ }
5580
+ recordRejected(fullFinalized);
5581
+ }
5582
+ const gridPath = findObstacleFreePath(
5583
+ source,
5584
+ target,
5585
+ allObstacles,
5586
+ { endpointObstacles, margin: 0, corridorMargin },
5587
+ diagnostics
5588
+ );
5589
+ if (gridPath !== null && gridPath.length >= 2) {
5590
+ const gridFinalized = finalizeRoute(
5591
+ gridPath,
5592
+ softObstacles,
5593
+ hardObstacles,
5594
+ diagnostics,
5595
+ softObstacleIndex,
5596
+ hardObstacleIndex
5597
+ );
5598
+ if (!routeIntersectsObstacles(
5599
+ gridFinalized,
5600
+ softObstacles,
5601
+ softObstacleIndex
5602
+ ) && !routeIntersectsObstacles(
5603
+ gridFinalized,
5604
+ hardObstacles,
5605
+ hardObstacleIndex
5606
+ )) {
5607
+ checkBacktracking(
5608
+ gridFinalized,
5609
+ source,
5610
+ target,
5611
+ diagnostics,
5612
+ input.maxBacktrackingRatio
5613
+ );
5614
+ return { points: gridFinalized, diagnostics };
5615
+ }
5616
+ recordRejected(gridFinalized);
5617
+ }
5618
+ }
5435
5619
  }
5436
5620
  }
5437
5621
  }
@@ -5493,7 +5677,8 @@ function routeEdge(input) {
5493
5677
  finalizedClean,
5494
5678
  candidate.points[0],
5495
5679
  candidate.points[candidate.points.length - 1],
5496
- diagnostics
5680
+ diagnostics,
5681
+ input.maxBacktrackingRatio
5497
5682
  );
5498
5683
  return { points: finalizedClean, diagnostics };
5499
5684
  }
@@ -5559,13 +5744,41 @@ function routeEdge(input) {
5559
5744
  code: "routing.obstacle.unavoidable",
5560
5745
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5561
5746
  });
5562
- return {
5563
- points: finalizeRoute(
5564
- bestPoints2,
5747
+ const finalizedSoftBest = finalizeRoute(
5748
+ bestPoints2,
5749
+ softObstacles,
5750
+ hardObstacles,
5751
+ diagnostics
5752
+ );
5753
+ let softFallback = finalizedSoftBest;
5754
+ if (bestRejectedPath !== void 0) {
5755
+ const finalizedRejected = finalizeRoute(
5756
+ bestRejectedPath,
5565
5757
  softObstacles,
5566
5758
  hardObstacles,
5567
5759
  diagnostics
5568
- ),
5760
+ );
5761
+ const rejectedCrossings = countObstacleCrossings(
5762
+ finalizedRejected,
5763
+ softObstacles
5764
+ );
5765
+ const heuristicCrossings = countObstacleCrossings(
5766
+ finalizedSoftBest,
5767
+ softObstacles
5768
+ );
5769
+ if (rejectedCrossings < heuristicCrossings) {
5770
+ softFallback = finalizedRejected;
5771
+ }
5772
+ }
5773
+ checkBacktracking(
5774
+ softFallback,
5775
+ softFallback[0],
5776
+ softFallback[softFallback.length - 1],
5777
+ diagnostics,
5778
+ input.maxBacktrackingRatio
5779
+ );
5780
+ return {
5781
+ points: softFallback,
5569
5782
  diagnostics
5570
5783
  };
5571
5784
  }
@@ -5597,6 +5810,22 @@ function routeEdge(input) {
5597
5810
  maxAttempts
5598
5811
  );
5599
5812
  }
5813
+ if (bestRejectedPath !== void 0) {
5814
+ diagnostics.push({
5815
+ severity: "warning",
5816
+ code: "routing.obstacle.unavoidable",
5817
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5818
+ });
5819
+ return {
5820
+ points: finalizeRoute(
5821
+ bestRejectedPath,
5822
+ softObstacles,
5823
+ hardObstacles,
5824
+ diagnostics
5825
+ ),
5826
+ diagnostics
5827
+ };
5828
+ }
5600
5829
  diagnostics.push({
5601
5830
  severity: "error",
5602
5831
  code: "routing.evidence.crossing_forbidden",
@@ -5644,13 +5873,41 @@ function routeEdge(input) {
5644
5873
  code: "routing.obstacle.unavoidable",
5645
5874
  message: "No bounded orthogonal route candidate avoided all obstacles."
5646
5875
  });
5647
- return {
5648
- points: finalizeRoute(
5649
- bestPoints,
5876
+ const finalizedBestPoints = finalizeRoute(
5877
+ bestPoints,
5878
+ softObstacles,
5879
+ hardObstacles,
5880
+ diagnostics
5881
+ );
5882
+ let fallbackPoints = finalizedBestPoints;
5883
+ if (bestRejectedPath !== void 0) {
5884
+ const finalizedRejected = finalizeRoute(
5885
+ bestRejectedPath,
5650
5886
  softObstacles,
5651
5887
  hardObstacles,
5652
5888
  diagnostics
5653
- ),
5889
+ );
5890
+ const rejectedCrossings = countObstacleCrossings(
5891
+ finalizedRejected,
5892
+ softObstacles
5893
+ );
5894
+ const heuristicCrossings = countObstacleCrossings(
5895
+ finalizedBestPoints,
5896
+ softObstacles
5897
+ );
5898
+ if (rejectedCrossings < heuristicCrossings) {
5899
+ fallbackPoints = finalizedRejected;
5900
+ }
5901
+ }
5902
+ checkBacktracking(
5903
+ fallbackPoints,
5904
+ fallbackPoints[0],
5905
+ fallbackPoints[fallbackPoints.length - 1],
5906
+ diagnostics,
5907
+ input.maxBacktrackingRatio
5908
+ );
5909
+ return {
5910
+ points: fallbackPoints,
5654
5911
  diagnostics
5655
5912
  };
5656
5913
  }
@@ -6149,6 +6406,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6149
6406
  }
6150
6407
  return false;
6151
6408
  }
6409
+ function countObstacleCrossings(points, obstacles) {
6410
+ let count = 0;
6411
+ for (const obstacle of obstacles) {
6412
+ validateBox(obstacle);
6413
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
6414
+ const a = points[pointIndex];
6415
+ const b = points[pointIndex + 1];
6416
+ if (a === void 0 || b === void 0) {
6417
+ continue;
6418
+ }
6419
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
6420
+ count += 1;
6421
+ break;
6422
+ }
6423
+ }
6424
+ }
6425
+ return count;
6426
+ }
6152
6427
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6153
6428
  for (let index = 0; index < points.length - 1; index += 1) {
6154
6429
  const a = points[index];
@@ -6532,7 +6807,7 @@ function solveDiagram(diagram, options = {}) {
6532
6807
  edges: styledEdges
6533
6808
  });
6534
6809
  diagnostics.push(...layout2.diagnostics);
6535
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6810
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
6536
6811
  layout2.boxes,
6537
6812
  styledNodes,
6538
6813
  styledEdges,
@@ -6540,7 +6815,8 @@ function solveDiagram(diagram, options = {}) {
6540
6815
  options,
6541
6816
  diagnostics
6542
6817
  );
6543
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6818
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6819
+ const diagCountBefore = diagnostics.length;
6544
6820
  const rewrapped = wrapHorizontalStackIfNeeded(
6545
6821
  initialNodeBoxes,
6546
6822
  styledNodes,
@@ -6551,6 +6827,20 @@ function solveDiagram(diagram, options = {}) {
6551
6827
  for (const [id, box] of rewrapped) {
6552
6828
  initialNodeBoxes.set(id, box);
6553
6829
  }
6830
+ if (diagnostics.length > diagCountBefore) {
6831
+ for (const node of styledNodes) {
6832
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6833
+ const rwBox = rewrapped.get(node.id);
6834
+ const idx = styledNodes.indexOf(node);
6835
+ if (idx !== -1) {
6836
+ styledNodes[idx] = {
6837
+ ...node,
6838
+ position: { x: rwBox.x, y: rwBox.y }
6839
+ };
6840
+ }
6841
+ }
6842
+ }
6843
+ }
6554
6844
  }
6555
6845
  if (useRecursive && "groupBoxes" in layout2) {
6556
6846
  const recursiveLayout = layout2;
@@ -6564,7 +6854,7 @@ function solveDiagram(diagram, options = {}) {
6564
6854
  overlapSpacing: options?.overlapSpacing ?? 40,
6565
6855
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
6566
6856
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6567
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6857
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
6568
6858
  swimlanes: styledSwimlanes,
6569
6859
  boxes: initialNodeBoxes,
6570
6860
  nodes: styledNodes,
@@ -6579,7 +6869,8 @@ function solveDiagram(diagram, options = {}) {
6579
6869
  constrained.boxes,
6580
6870
  constrained.locks,
6581
6871
  options?.overlapSpacing ?? 40,
6582
- Math.max(0, options?.minLaneGutter ?? 0)
6872
+ Math.max(0, options?.minLaneGutter ?? 0),
6873
+ options.distributeContainedChildren ?? true
6583
6874
  );
6584
6875
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6585
6876
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6746,7 +7037,8 @@ function solveDiagram(diagram, options = {}) {
6746
7037
  diagram.direction,
6747
7038
  options,
6748
7039
  diagnostics,
6749
- coordinatedGroups
7040
+ coordinatedGroups,
7041
+ contentBounds
6750
7042
  );
6751
7043
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6752
7044
  coordinatedEdges,
@@ -7168,7 +7460,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7168
7460
  function containsCjk(value) {
7169
7461
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7170
7462
  }
7171
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7463
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7172
7464
  const layouts = /* @__PURE__ */ new Map();
7173
7465
  const diagnostics = [];
7174
7466
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7187,7 +7479,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7187
7479
  locks,
7188
7480
  diagnostics,
7189
7481
  movedChildIds,
7190
- laneGutter
7482
+ laneGutter,
7483
+ constraints,
7484
+ distributeContainedChildren
7191
7485
  );
7192
7486
  if (layout2 !== void 0) {
7193
7487
  layouts.set(swimlane.id, layout2);
@@ -7283,9 +7577,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
7283
7577
  if (!isStackRunaway(boxes, nodes, direction, options)) {
7284
7578
  return new Map(boxes);
7285
7579
  }
7286
- const maxRowDepth = options.maxRowDepth;
7287
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7288
- return new Map(boxes);
7580
+ let maxRowDepth = options.maxRowDepth;
7581
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
7582
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
7583
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
7584
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
7585
+ } else {
7586
+ return new Map(boxes);
7587
+ }
7289
7588
  }
7290
7589
  const ordered = [...nodes].sort((a, b) => {
7291
7590
  const ba = boxes.get(a.id);
@@ -7346,10 +7645,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
7346
7645
  });
7347
7646
  }
7348
7647
  function isStackRunaway(boxes, nodes, direction, options) {
7349
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
7648
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
7350
7649
  return false;
7351
7650
  }
7352
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
7651
+ if (nodes.length < 2) {
7353
7652
  return false;
7354
7653
  }
7355
7654
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -7357,17 +7656,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
7357
7656
  return false;
7358
7657
  }
7359
7658
  const bounds = unionBoxes(nodeBoxes);
7360
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7361
- const preferred = options.preferredAspectRatio ?? 3;
7362
- if (aspectRatio < preferred) {
7659
+ const isHorizontal = direction === "TB" || direction === "BT";
7660
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7661
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
7662
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
7363
7663
  return false;
7364
7664
  }
7665
+ if (isHorizontal) {
7666
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
7667
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
7668
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
7669
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
7670
+ }
7365
7671
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
7366
7672
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
7367
7673
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7368
7674
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7369
7675
  }
7370
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7676
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7371
7677
  const headerHeight = swimlane.headerHeight ?? 28;
7372
7678
  const padding = swimlane.padding ?? 16;
7373
7679
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7392,7 +7698,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7392
7698
  locks,
7393
7699
  diagnostics,
7394
7700
  movedChildIds,
7395
- laneGutter
7701
+ laneGutter,
7702
+ constraints,
7703
+ distributeContainedChildren
7396
7704
  );
7397
7705
  }
7398
7706
  return applyHorizontalSwimlaneContract(
@@ -7407,13 +7715,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7407
7715
  laneGutter
7408
7716
  );
7409
7717
  }
7410
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
7718
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7411
7719
  const populatedBounds = laneBounds.filter(
7412
7720
  (box) => box !== void 0
7413
7721
  );
7414
7722
  const top = Math.min(...populatedBounds.map((box) => box.y));
7415
7723
  const left = Math.min(...populatedBounds.map((box) => box.x));
7416
7724
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
7725
+ const containedChildIds = /* @__PURE__ */ new Set();
7726
+ if (distributeContainedChildren) {
7727
+ for (const c of constraints) {
7728
+ if (c.kind !== "containment") continue;
7729
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
7730
+ const distributable = c.childIds.filter((childId) => {
7731
+ if (nodeBoxes.get(childId) === void 0) return false;
7732
+ const lock = locks.get(childId);
7733
+ return lock === void 0 || lock.source === "fixed-position";
7734
+ });
7735
+ if (distributable.length < 2) continue;
7736
+ for (const childId of distributable) {
7737
+ containedChildIds.add(childId);
7738
+ }
7739
+ }
7740
+ }
7417
7741
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7418
7742
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7419
7743
  const rankStackGap = Math.max(8, padding / 2);
@@ -7425,7 +7749,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7425
7749
  );
7426
7750
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
7427
7751
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
7428
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
7752
+ const spreadWidth = maxCrossAxisSpreadWidth(
7753
+ swimlane,
7754
+ nodeBoxes,
7755
+ flowRanks,
7756
+ locks,
7757
+ rankStackGap,
7758
+ containedChildIds
7759
+ );
7760
+ const slotWidth = Math.max(
7761
+ Math.max(...populatedBounds.map((box) => box.width)),
7762
+ spreadWidth
7763
+ ) + padding * 2;
7429
7764
  const laneStep = slotWidth + laneGutter;
7430
7765
  const laneContentTop = top + headerHeight + padding;
7431
7766
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -7439,6 +7774,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7439
7774
  y: laneContentTop
7440
7775
  };
7441
7776
  if (maxRank === 0) {
7777
+ const distributable = lane.children.filter(
7778
+ (childId) => !locks.has(childId)
7779
+ );
7780
+ const coveredByContainment = lane.children.some(
7781
+ (childId) => containedChildIds.has(childId)
7782
+ );
7783
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7784
+ moveRankedVerticalLaneChildren(
7785
+ lane.children,
7786
+ nodeBoxes,
7787
+ locks,
7788
+ diagnostics,
7789
+ movedChildIds,
7790
+ flowRanks,
7791
+ rankSpacing,
7792
+ rankStackGap,
7793
+ { x: target.x, y: laneContentTop },
7794
+ slotWidth - padding * 2
7795
+ );
7796
+ continue;
7797
+ }
7442
7798
  moveLaneChildren(
7443
7799
  lane.children,
7444
7800
  nodeBoxes,
@@ -7452,6 +7808,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7452
7808
  );
7453
7809
  continue;
7454
7810
  }
7811
+ const rankedCoveredByContainment = lane.children.some(
7812
+ (childId) => containedChildIds.has(childId)
7813
+ );
7455
7814
  moveRankedVerticalLaneChildren(
7456
7815
  lane.children,
7457
7816
  nodeBoxes,
@@ -7461,10 +7820,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7461
7820
  flowRanks,
7462
7821
  rankSpacing,
7463
7822
  rankStackGap,
7464
- {
7465
- x: target.x - bounds.x,
7466
- y: laneContentTop
7467
- }
7823
+ { x: target.x, y: laneContentTop },
7824
+ slotWidth - padding * 2,
7825
+ rankedCoveredByContainment
7468
7826
  );
7469
7827
  }
7470
7828
  return {
@@ -7573,31 +7931,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
7573
7931
  }
7574
7932
  return maxHeight;
7575
7933
  }
7576
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7934
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7935
+ function crossAxisSpreadWidth(items, gap) {
7936
+ return items.reduce(
7937
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7938
+ 0
7939
+ );
7940
+ }
7941
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7942
+ let maxWidth = 0;
7943
+ for (const lane of swimlane.lanes) {
7944
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7945
+ continue;
7946
+ }
7947
+ for (const stack of rankStacks(
7948
+ lane.children,
7949
+ nodeBoxes,
7950
+ flowRanks
7951
+ ).values()) {
7952
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7953
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7954
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7955
+ }
7956
+ }
7957
+ return maxWidth;
7958
+ }
7959
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7577
7960
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7578
- let yOffset = 0;
7961
+ const unlocked = [];
7579
7962
  for (const item of stack) {
7580
- const { childId, box } = item;
7581
- if (locks.has(childId)) {
7963
+ if (locks.has(item.childId)) {
7582
7964
  diagnostics.push({
7583
7965
  severity: "warning",
7584
7966
  code: "constraints.locked-target-not-moved",
7585
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7967
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
7586
7968
  path: ["swimlanes"],
7587
- detail: { nodeId: childId }
7969
+ detail: { nodeId: item.childId }
7588
7970
  });
7589
- continue;
7971
+ } else {
7972
+ unlocked.push(item);
7590
7973
  }
7974
+ }
7975
+ if (unlocked.length === 0) continue;
7976
+ if (unlocked.length === 1) {
7977
+ const { childId, box } = unlocked[0];
7591
7978
  const next = {
7592
7979
  ...box,
7593
- x: box.x + target.x,
7594
- y: target.y + rank * rankSpacing + yOffset
7980
+ x: target.x + (contentWidth - box.width) / 2,
7981
+ y: target.y + rank * rankSpacing
7595
7982
  };
7596
7983
  if (next.x !== box.x || next.y !== box.y) {
7597
7984
  movedChildIds.add(childId);
7598
7985
  }
7599
7986
  nodeBoxes.set(childId, next);
7600
- yOffset += box.height + rankStackGap;
7987
+ } else {
7988
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7989
+ if (!shouldSpread) {
7990
+ let yOffset = 0;
7991
+ for (const { childId, box } of unlocked) {
7992
+ const next = {
7993
+ ...box,
7994
+ x: target.x + (contentWidth - box.width) / 2,
7995
+ y: target.y + rank * rankSpacing + yOffset
7996
+ };
7997
+ if (next.x !== box.x || next.y !== box.y) {
7998
+ movedChildIds.add(childId);
7999
+ }
8000
+ nodeBoxes.set(childId, next);
8001
+ yOffset += box.height + rankStackGap;
8002
+ }
8003
+ } else {
8004
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
8005
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
8006
+ for (const { childId, box } of unlocked) {
8007
+ const next = {
8008
+ ...box,
8009
+ x: xCursor,
8010
+ y: target.y + rank * rankSpacing
8011
+ };
8012
+ if (next.x !== box.x || next.y !== box.y) {
8013
+ movedChildIds.add(childId);
8014
+ }
8015
+ nodeBoxes.set(childId, next);
8016
+ xCursor += box.width + rankStackGap;
8017
+ }
8018
+ diagnostics.push({
8019
+ severity: "info",
8020
+ code: "swimlane_contract.cross_axis_distributed",
8021
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
8022
+ path: ["swimlanes"],
8023
+ detail: {
8024
+ rank,
8025
+ childCount: unlocked.length,
8026
+ contentWidth
8027
+ }
8028
+ });
8029
+ }
7601
8030
  }
7602
8031
  }
7603
8032
  }
@@ -7936,7 +8365,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7936
8365
  });
7937
8366
  continue;
7938
8367
  }
7939
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
8368
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7940
8369
  const geometry = computeShapeGeometry({
7941
8370
  shape: node.shape,
7942
8371
  box,
@@ -8030,7 +8459,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
8030
8459
  }
8031
8460
  }
8032
8461
  }
8033
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8462
+ function coordinatePorts(node, nodeBox, portShifting) {
8034
8463
  const portsBySide = /* @__PURE__ */ new Map();
8035
8464
  for (const port of node.ports ?? []) {
8036
8465
  const ports = portsBySide.get(port.side) ?? [];
@@ -8053,9 +8482,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8053
8482
  side,
8054
8483
  index,
8055
8484
  sorted.length,
8056
- portShifting,
8057
- diagnostics,
8058
- node.id
8485
+ portShifting
8059
8486
  );
8060
8487
  const box = portBox(anchor);
8061
8488
  coordinated.push({ ...port, box, anchor });
@@ -8063,32 +8490,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8063
8490
  }
8064
8491
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
8065
8492
  }
8066
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
8493
+ function portAnchor(nodeBox, side, index, count, portShifting) {
8067
8494
  const shiftingEnabled = portShifting?.enabled ?? true;
8068
8495
  const requestedSpacing = portShifting?.spacing ?? 24;
8069
8496
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
8070
8497
  const availableSpan = 2 * maxOffset;
8071
8498
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
8072
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
8499
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
8073
8500
  Math.min(requestedSpacing, availableSpan / (count - 1)),
8074
8501
  minSpacing
8075
8502
  ) : requestedSpacing;
8076
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
8077
- diagnostics.push({
8078
- severity: "warning",
8079
- code: "port_constraint_overlap",
8080
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
8081
- path: ["nodes", nodeId, "ports"],
8082
- detail: {
8083
- nodeId,
8084
- side,
8085
- requestedSpacing,
8086
- effectiveSpacing: Math.round(effectiveSpacing),
8087
- portCount: count
8088
- }
8089
- });
8090
- }
8091
- const spacing = effectiveSpacing;
8092
8503
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
8093
8504
  switch (side) {
8094
8505
  case "left":
@@ -8703,14 +9114,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8703
9114
  }
8704
9115
  };
8705
9116
  }
8706
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
9117
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8707
9118
  const coordinated = [];
8708
9119
  const coordinatedNodeById = new Map(
8709
9120
  coordinatedNodes.map((node) => [node.id, node])
8710
9121
  );
9122
+ const corridorMarginOption = options.corridorMargin ?? "auto";
9123
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
9124
+ 200,
9125
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
9126
+ );
9127
+ const routingGutter = options.routingGutter ?? 160;
9128
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8711
9129
  const nodeObstacleIndex = createBoxSpatialIndex(
8712
9130
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8713
- options.routingGutter ?? 160
9131
+ queryGutter
8714
9132
  );
8715
9133
  for (const edge of edges) {
8716
9134
  const source = nodes.get(edge.source.nodeId);
@@ -8732,11 +9150,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8732
9150
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8733
9151
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8734
9152
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8735
- const corridor = edgeCorridorBox(
8736
- source.box,
8737
- target.box,
8738
- options.routingGutter ?? 160
8739
- );
9153
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8740
9154
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8741
9155
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8742
9156
  );
@@ -8754,7 +9168,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8754
9168
  ...routeTextObstacles
8755
9169
  ],
8756
9170
  hardObstacles,
8757
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
9171
+ corridorMargin,
9172
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
9173
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
8758
9174
  });
8759
9175
  diagnostics.push(
8760
9176
  ...route.diagnostics.map((diagnostic) => ({