@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.js CHANGED
@@ -176,6 +176,28 @@ function applyLayoutConstraints(input) {
176
176
  if (input.distributeContainedChildren) {
177
177
  yieldFixedPositionLocks(input, boxes, locks);
178
178
  }
179
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
180
+ for (const swimlane of input.swimlanes) {
181
+ if (swimlane.layout === "contract") continue;
182
+ for (const lane of swimlane.lanes) {
183
+ const fixedChildren = [];
184
+ let participantCount = 0;
185
+ for (const childId of lane.children) {
186
+ const lock = locks.get(childId);
187
+ if (lock === void 0) {
188
+ participantCount += 1;
189
+ } else if (lock.source === "fixed-position") {
190
+ participantCount += 1;
191
+ fixedChildren.push(childId);
192
+ }
193
+ }
194
+ if (participantCount < 2) continue;
195
+ for (const childId of fixedChildren) {
196
+ locks.delete(childId);
197
+ }
198
+ }
199
+ }
200
+ }
179
201
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
180
202
  applyRelative(input.constraints, boxes, locks, diagnostics);
181
203
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -195,6 +217,9 @@ function applyLayoutConstraints(input) {
195
217
  applyDistributeContained(input, boxes, locks, diagnostics);
196
218
  dedupReplayDiagnostics(diagnostics, diagBefore);
197
219
  }
220
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
221
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
222
+ }
198
223
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
199
224
  reportOverlaps(
200
225
  boxes,
@@ -1034,9 +1059,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1034
1059
  }
1035
1060
  });
1036
1061
  }
1037
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1038
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1039
- }
1040
1062
  }
1041
1063
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1042
1064
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -1076,6 +1098,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1076
1098
  effectiveGap = minGap + remaining / (unlocked.length - 1);
1077
1099
  }
1078
1100
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
1101
+ reserved.sort((a, b) => a.start - b.start);
1079
1102
  let pos = contentStart;
1080
1103
  for (const child of unlocked) {
1081
1104
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4726,15 +4749,12 @@ var BinaryHeap = class {
4726
4749
  let smallestIdx = idx;
4727
4750
  const leftIdx = (idx << 1) + 1;
4728
4751
  const rightIdx = leftIdx + 1;
4729
- if (leftIdx < size && this._less(
4730
- this._data[leftIdx],
4731
- this._data[smallestIdx]
4732
- )) {
4752
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4733
4753
  smallestIdx = leftIdx;
4734
4754
  }
4735
4755
  if (rightIdx < size && this._less(
4736
4756
  this._data[rightIdx],
4737
- this._data[smallestIdx]
4757
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4738
4758
  )) {
4739
4759
  smallestIdx = rightIdx;
4740
4760
  }
@@ -4801,8 +4821,59 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4801
4821
  turnPenalty,
4802
4822
  segmentPenalty
4803
4823
  );
4804
- if (path === null) return null;
4805
- return simplifyRoute(path);
4824
+ if (path !== null) {
4825
+ const simplified = simplifyRoute(path);
4826
+ const filteredSet = new Set(filtered);
4827
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4828
+ let crossesExcluded = false;
4829
+ for (let i = 0; i < simplified.length - 1; i++) {
4830
+ const a = simplified[i];
4831
+ const b = simplified[i + 1];
4832
+ for (const obs of obstacles) {
4833
+ if (filteredSet.has(obs)) continue;
4834
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4835
+ crossesExcluded = true;
4836
+ break;
4837
+ }
4838
+ }
4839
+ if (crossesExcluded) break;
4840
+ }
4841
+ if (!crossesExcluded) return simplified;
4842
+ } else {
4843
+ return simplified;
4844
+ }
4845
+ }
4846
+ if (!useCorridor) return null;
4847
+ const xsFull = collectXs(source, target, obstacles, margin);
4848
+ const ysFull = collectYs(source, target, obstacles, margin);
4849
+ if (xsFull.length * ysFull.length > maxNodes) {
4850
+ diagnostics?.push({
4851
+ severity: "warning",
4852
+ code: "routing.astar.grid_overflow",
4853
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4854
+ detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4855
+ });
4856
+ return null;
4857
+ }
4858
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4859
+ connectHorizontalEdges(
4860
+ nodesFull,
4861
+ ysFull,
4862
+ obstacles,
4863
+ endpointObstacles,
4864
+ margin
4865
+ );
4866
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4867
+ const pathFull = aStarSearch(
4868
+ nodesFull,
4869
+ idxFull,
4870
+ source,
4871
+ target,
4872
+ turnPenalty,
4873
+ segmentPenalty
4874
+ );
4875
+ if (pathFull === null) return null;
4876
+ return simplifyRoute(pathFull);
4806
4877
  }
4807
4878
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4808
4879
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -5029,7 +5100,7 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
5029
5100
  const turnPenalty = options.turnPenalty ?? 50;
5030
5101
  const segmentPenalty = options.segmentPenalty ?? 1;
5031
5102
  const endpointObstacles = options.endpointObstacles ?? [];
5032
- const maxCorners = options.maxCorners ?? 300;
5103
+ const maxCorners = options.maxCorners ?? 600;
5033
5104
  const vertices = collectCornerVertices(source, target, obstacles, margin);
5034
5105
  if (vertices.length > maxCorners) {
5035
5106
  diagnostics?.push({
@@ -5310,7 +5381,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
5310
5381
  }
5311
5382
 
5312
5383
  // src/routing/routes.ts
5313
- function checkBacktracking(points, source, target, diagnostics) {
5384
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
5314
5385
  if (points.length < 2) return;
5315
5386
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
5316
5387
  if (direct <= 0) return;
@@ -5320,7 +5391,7 @@ function checkBacktracking(points, source, target, diagnostics) {
5320
5391
  const b = points[i + 1];
5321
5392
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
5322
5393
  }
5323
- const threshold = 10;
5394
+ const threshold = maxRatio ?? 20;
5324
5395
  if (routeLen > direct * threshold) {
5325
5396
  diagnostics.push({
5326
5397
  severity: "warning",
@@ -5338,8 +5409,20 @@ function routeEdge(input) {
5338
5409
  const diagnostics = [];
5339
5410
  const softObstacles = input.obstacles ?? [];
5340
5411
  const hardObstacles = input.hardObstacles ?? [];
5412
+ let bestRejectedPath;
5413
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
5341
5414
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
5342
5415
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
5416
+ const recordRejected = (candidate) => {
5417
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
5418
+ return;
5419
+ }
5420
+ const crossings = countObstacleCrossings(candidate, softObstacles);
5421
+ if (crossings < bestRejectedCrossings) {
5422
+ bestRejectedCrossings = crossings;
5423
+ bestRejectedPath = candidate;
5424
+ }
5425
+ };
5343
5426
  const maxAttempts = input.maxRoutingAttempts ?? 5;
5344
5427
  const defaultAnchors = defaultAnchorsForGeometry(
5345
5428
  input.source.box,
@@ -5398,18 +5481,38 @@ function routeEdge(input) {
5398
5481
  input.source.center,
5399
5482
  targetAnchor
5400
5483
  );
5401
- const cornerPath = findCornerGraphPath(
5484
+ const allObstacles = [...softObstacles, ...hardObstacles];
5485
+ const corridorMargin = input.corridorMargin ?? 32;
5486
+ const corridorObstacles = filterObstaclesByCorridor(
5487
+ source,
5488
+ target,
5489
+ allObstacles,
5490
+ [],
5491
+ // endpointObstacles passed separately via options
5492
+ corridorMargin
5493
+ );
5494
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5495
+ let cornerPath = findCornerGraphPath(
5402
5496
  source,
5403
5497
  target,
5404
- [...softObstacles, ...hardObstacles],
5498
+ cornerObstacles,
5405
5499
  { endpointObstacles, margin: 2 },
5406
5500
  diagnostics
5407
5501
  );
5502
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
5503
+ cornerPath = findCornerGraphPath(
5504
+ source,
5505
+ target,
5506
+ allObstacles,
5507
+ { endpointObstacles, margin: 2 },
5508
+ diagnostics
5509
+ );
5510
+ }
5408
5511
  const path = cornerPath ?? findObstacleFreePath(
5409
5512
  source,
5410
5513
  target,
5411
- [...softObstacles, ...hardObstacles],
5412
- { endpointObstacles, margin: 0 },
5514
+ allObstacles,
5515
+ { endpointObstacles, margin: 0, corridorMargin },
5413
5516
  diagnostics
5414
5517
  );
5415
5518
  if (path !== null && path.length >= 2) {
@@ -5426,9 +5529,90 @@ function routeEdge(input) {
5426
5529
  softObstacles,
5427
5530
  softObstacleIndex
5428
5531
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
5429
- checkBacktracking(finalized, source, target, diagnostics);
5532
+ checkBacktracking(
5533
+ finalized,
5534
+ source,
5535
+ target,
5536
+ diagnostics,
5537
+ input.maxBacktrackingRatio
5538
+ );
5430
5539
  return { points: finalized, diagnostics };
5431
5540
  }
5541
+ recordRejected(finalized);
5542
+ if (cornerPath !== null) {
5543
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5544
+ source,
5545
+ target,
5546
+ allObstacles,
5547
+ { endpointObstacles, margin: 2 },
5548
+ diagnostics
5549
+ ) : null;
5550
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5551
+ const fullFinalized = finalizeRoute(
5552
+ fullCornerPath,
5553
+ softObstacles,
5554
+ hardObstacles,
5555
+ diagnostics,
5556
+ softObstacleIndex,
5557
+ hardObstacleIndex
5558
+ );
5559
+ if (!routeIntersectsObstacles(
5560
+ fullFinalized,
5561
+ softObstacles,
5562
+ softObstacleIndex
5563
+ ) && !routeIntersectsObstacles(
5564
+ fullFinalized,
5565
+ hardObstacles,
5566
+ hardObstacleIndex
5567
+ )) {
5568
+ checkBacktracking(
5569
+ fullFinalized,
5570
+ source,
5571
+ target,
5572
+ diagnostics,
5573
+ input.maxBacktrackingRatio
5574
+ );
5575
+ return { points: fullFinalized, diagnostics };
5576
+ }
5577
+ recordRejected(fullFinalized);
5578
+ }
5579
+ const gridPath = findObstacleFreePath(
5580
+ source,
5581
+ target,
5582
+ allObstacles,
5583
+ { endpointObstacles, margin: 0, corridorMargin },
5584
+ diagnostics
5585
+ );
5586
+ if (gridPath !== null && gridPath.length >= 2) {
5587
+ const gridFinalized = finalizeRoute(
5588
+ gridPath,
5589
+ softObstacles,
5590
+ hardObstacles,
5591
+ diagnostics,
5592
+ softObstacleIndex,
5593
+ hardObstacleIndex
5594
+ );
5595
+ if (!routeIntersectsObstacles(
5596
+ gridFinalized,
5597
+ softObstacles,
5598
+ softObstacleIndex
5599
+ ) && !routeIntersectsObstacles(
5600
+ gridFinalized,
5601
+ hardObstacles,
5602
+ hardObstacleIndex
5603
+ )) {
5604
+ checkBacktracking(
5605
+ gridFinalized,
5606
+ source,
5607
+ target,
5608
+ diagnostics,
5609
+ input.maxBacktrackingRatio
5610
+ );
5611
+ return { points: gridFinalized, diagnostics };
5612
+ }
5613
+ recordRejected(gridFinalized);
5614
+ }
5615
+ }
5432
5616
  }
5433
5617
  }
5434
5618
  }
@@ -5490,7 +5674,8 @@ function routeEdge(input) {
5490
5674
  finalizedClean,
5491
5675
  candidate.points[0],
5492
5676
  candidate.points[candidate.points.length - 1],
5493
- diagnostics
5677
+ diagnostics,
5678
+ input.maxBacktrackingRatio
5494
5679
  );
5495
5680
  return { points: finalizedClean, diagnostics };
5496
5681
  }
@@ -5556,13 +5741,41 @@ function routeEdge(input) {
5556
5741
  code: "routing.obstacle.unavoidable",
5557
5742
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
5558
5743
  });
5559
- return {
5560
- points: finalizeRoute(
5561
- bestPoints2,
5744
+ const finalizedSoftBest = finalizeRoute(
5745
+ bestPoints2,
5746
+ softObstacles,
5747
+ hardObstacles,
5748
+ diagnostics
5749
+ );
5750
+ let softFallback = finalizedSoftBest;
5751
+ if (bestRejectedPath !== void 0) {
5752
+ const finalizedRejected = finalizeRoute(
5753
+ bestRejectedPath,
5562
5754
  softObstacles,
5563
5755
  hardObstacles,
5564
5756
  diagnostics
5565
- ),
5757
+ );
5758
+ const rejectedCrossings = countObstacleCrossings(
5759
+ finalizedRejected,
5760
+ softObstacles
5761
+ );
5762
+ const heuristicCrossings = countObstacleCrossings(
5763
+ finalizedSoftBest,
5764
+ softObstacles
5765
+ );
5766
+ if (rejectedCrossings < heuristicCrossings) {
5767
+ softFallback = finalizedRejected;
5768
+ }
5769
+ }
5770
+ checkBacktracking(
5771
+ softFallback,
5772
+ softFallback[0],
5773
+ softFallback[softFallback.length - 1],
5774
+ diagnostics,
5775
+ input.maxBacktrackingRatio
5776
+ );
5777
+ return {
5778
+ points: softFallback,
5566
5779
  diagnostics
5567
5780
  };
5568
5781
  }
@@ -5594,6 +5807,22 @@ function routeEdge(input) {
5594
5807
  maxAttempts
5595
5808
  );
5596
5809
  }
5810
+ if (bestRejectedPath !== void 0) {
5811
+ diagnostics.push({
5812
+ severity: "warning",
5813
+ code: "routing.obstacle.unavoidable",
5814
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5815
+ });
5816
+ return {
5817
+ points: finalizeRoute(
5818
+ bestRejectedPath,
5819
+ softObstacles,
5820
+ hardObstacles,
5821
+ diagnostics
5822
+ ),
5823
+ diagnostics
5824
+ };
5825
+ }
5597
5826
  diagnostics.push({
5598
5827
  severity: "error",
5599
5828
  code: "routing.evidence.crossing_forbidden",
@@ -5641,13 +5870,41 @@ function routeEdge(input) {
5641
5870
  code: "routing.obstacle.unavoidable",
5642
5871
  message: "No bounded orthogonal route candidate avoided all obstacles."
5643
5872
  });
5644
- return {
5645
- points: finalizeRoute(
5646
- bestPoints,
5873
+ const finalizedBestPoints = finalizeRoute(
5874
+ bestPoints,
5875
+ softObstacles,
5876
+ hardObstacles,
5877
+ diagnostics
5878
+ );
5879
+ let fallbackPoints = finalizedBestPoints;
5880
+ if (bestRejectedPath !== void 0) {
5881
+ const finalizedRejected = finalizeRoute(
5882
+ bestRejectedPath,
5647
5883
  softObstacles,
5648
5884
  hardObstacles,
5649
5885
  diagnostics
5650
- ),
5886
+ );
5887
+ const rejectedCrossings = countObstacleCrossings(
5888
+ finalizedRejected,
5889
+ softObstacles
5890
+ );
5891
+ const heuristicCrossings = countObstacleCrossings(
5892
+ finalizedBestPoints,
5893
+ softObstacles
5894
+ );
5895
+ if (rejectedCrossings < heuristicCrossings) {
5896
+ fallbackPoints = finalizedRejected;
5897
+ }
5898
+ }
5899
+ checkBacktracking(
5900
+ fallbackPoints,
5901
+ fallbackPoints[0],
5902
+ fallbackPoints[fallbackPoints.length - 1],
5903
+ diagnostics,
5904
+ input.maxBacktrackingRatio
5905
+ );
5906
+ return {
5907
+ points: fallbackPoints,
5651
5908
  diagnostics
5652
5909
  };
5653
5910
  }
@@ -6146,6 +6403,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
6146
6403
  }
6147
6404
  return false;
6148
6405
  }
6406
+ function countObstacleCrossings(points, obstacles) {
6407
+ let count = 0;
6408
+ for (const obstacle of obstacles) {
6409
+ validateBox(obstacle);
6410
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
6411
+ const a = points[pointIndex];
6412
+ const b = points[pointIndex + 1];
6413
+ if (a === void 0 || b === void 0) {
6414
+ continue;
6415
+ }
6416
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
6417
+ count += 1;
6418
+ break;
6419
+ }
6420
+ }
6421
+ }
6422
+ return count;
6423
+ }
6149
6424
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
6150
6425
  for (let index = 0; index < points.length - 1; index += 1) {
6151
6426
  const a = points[index];
@@ -6529,7 +6804,7 @@ function solveDiagram(diagram, options = {}) {
6529
6804
  edges: styledEdges
6530
6805
  });
6531
6806
  diagnostics.push(...layout2.diagnostics);
6532
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6807
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
6533
6808
  layout2.boxes,
6534
6809
  styledNodes,
6535
6810
  styledEdges,
@@ -6537,7 +6812,8 @@ function solveDiagram(diagram, options = {}) {
6537
6812
  options,
6538
6813
  diagnostics
6539
6814
  );
6540
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6815
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6816
+ const diagCountBefore = diagnostics.length;
6541
6817
  const rewrapped = wrapHorizontalStackIfNeeded(
6542
6818
  initialNodeBoxes,
6543
6819
  styledNodes,
@@ -6548,6 +6824,20 @@ function solveDiagram(diagram, options = {}) {
6548
6824
  for (const [id, box] of rewrapped) {
6549
6825
  initialNodeBoxes.set(id, box);
6550
6826
  }
6827
+ if (diagnostics.length > diagCountBefore) {
6828
+ for (const node of styledNodes) {
6829
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6830
+ const rwBox = rewrapped.get(node.id);
6831
+ const idx = styledNodes.indexOf(node);
6832
+ if (idx !== -1) {
6833
+ styledNodes[idx] = {
6834
+ ...node,
6835
+ position: { x: rwBox.x, y: rwBox.y }
6836
+ };
6837
+ }
6838
+ }
6839
+ }
6840
+ }
6551
6841
  }
6552
6842
  if (useRecursive && "groupBoxes" in layout2) {
6553
6843
  const recursiveLayout = layout2;
@@ -6561,7 +6851,7 @@ function solveDiagram(diagram, options = {}) {
6561
6851
  overlapSpacing: options?.overlapSpacing ?? 40,
6562
6852
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
6563
6853
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6564
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6854
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
6565
6855
  swimlanes: styledSwimlanes,
6566
6856
  boxes: initialNodeBoxes,
6567
6857
  nodes: styledNodes,
@@ -6576,7 +6866,8 @@ function solveDiagram(diagram, options = {}) {
6576
6866
  constrained.boxes,
6577
6867
  constrained.locks,
6578
6868
  options?.overlapSpacing ?? 40,
6579
- Math.max(0, options?.minLaneGutter ?? 0)
6869
+ Math.max(0, options?.minLaneGutter ?? 0),
6870
+ options.distributeContainedChildren ?? true
6580
6871
  );
6581
6872
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
6582
6873
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -6743,7 +7034,8 @@ function solveDiagram(diagram, options = {}) {
6743
7034
  diagram.direction,
6744
7035
  options,
6745
7036
  diagnostics,
6746
- coordinatedGroups
7037
+ coordinatedGroups,
7038
+ contentBounds
6747
7039
  );
6748
7040
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
6749
7041
  coordinatedEdges,
@@ -7165,7 +7457,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
7165
7457
  function containsCjk(value) {
7166
7458
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
7167
7459
  }
7168
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
7460
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
7169
7461
  const layouts = /* @__PURE__ */ new Map();
7170
7462
  const diagnostics = [];
7171
7463
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -7184,7 +7476,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
7184
7476
  locks,
7185
7477
  diagnostics,
7186
7478
  movedChildIds,
7187
- laneGutter
7479
+ laneGutter,
7480
+ constraints,
7481
+ distributeContainedChildren
7188
7482
  );
7189
7483
  if (layout2 !== void 0) {
7190
7484
  layouts.set(swimlane.id, layout2);
@@ -7280,9 +7574,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
7280
7574
  if (!isStackRunaway(boxes, nodes, direction, options)) {
7281
7575
  return new Map(boxes);
7282
7576
  }
7283
- const maxRowDepth = options.maxRowDepth;
7284
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7285
- return new Map(boxes);
7577
+ let maxRowDepth = options.maxRowDepth;
7578
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
7579
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
7580
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
7581
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
7582
+ } else {
7583
+ return new Map(boxes);
7584
+ }
7286
7585
  }
7287
7586
  const ordered = [...nodes].sort((a, b) => {
7288
7587
  const ba = boxes.get(a.id);
@@ -7343,10 +7642,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
7343
7642
  });
7344
7643
  }
7345
7644
  function isStackRunaway(boxes, nodes, direction, options) {
7346
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
7645
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
7347
7646
  return false;
7348
7647
  }
7349
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
7648
+ if (nodes.length < 2) {
7350
7649
  return false;
7351
7650
  }
7352
7651
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -7354,17 +7653,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
7354
7653
  return false;
7355
7654
  }
7356
7655
  const bounds = unionBoxes(nodeBoxes);
7357
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7358
- const preferred = options.preferredAspectRatio ?? 3;
7359
- if (aspectRatio < preferred) {
7656
+ const isHorizontal = direction === "TB" || direction === "BT";
7657
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7658
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
7659
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
7360
7660
  return false;
7361
7661
  }
7662
+ if (isHorizontal) {
7663
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
7664
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
7665
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
7666
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
7667
+ }
7362
7668
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
7363
7669
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
7364
7670
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
7365
7671
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
7366
7672
  }
7367
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
7673
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7368
7674
  const headerHeight = swimlane.headerHeight ?? 28;
7369
7675
  const padding = swimlane.padding ?? 16;
7370
7676
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -7389,7 +7695,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7389
7695
  locks,
7390
7696
  diagnostics,
7391
7697
  movedChildIds,
7392
- laneGutter
7698
+ laneGutter,
7699
+ constraints,
7700
+ distributeContainedChildren
7393
7701
  );
7394
7702
  }
7395
7703
  return applyHorizontalSwimlaneContract(
@@ -7404,13 +7712,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
7404
7712
  laneGutter
7405
7713
  );
7406
7714
  }
7407
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
7715
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
7408
7716
  const populatedBounds = laneBounds.filter(
7409
7717
  (box) => box !== void 0
7410
7718
  );
7411
7719
  const top = Math.min(...populatedBounds.map((box) => box.y));
7412
7720
  const left = Math.min(...populatedBounds.map((box) => box.x));
7413
7721
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
7722
+ const containedChildIds = /* @__PURE__ */ new Set();
7723
+ if (distributeContainedChildren) {
7724
+ for (const c of constraints) {
7725
+ if (c.kind !== "containment") continue;
7726
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
7727
+ const distributable = c.childIds.filter((childId) => {
7728
+ if (nodeBoxes.get(childId) === void 0) return false;
7729
+ const lock = locks.get(childId);
7730
+ return lock === void 0 || lock.source === "fixed-position";
7731
+ });
7732
+ if (distributable.length < 2) continue;
7733
+ for (const childId of distributable) {
7734
+ containedChildIds.add(childId);
7735
+ }
7736
+ }
7737
+ }
7414
7738
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
7415
7739
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
7416
7740
  const rankStackGap = Math.max(8, padding / 2);
@@ -7422,7 +7746,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7422
7746
  );
7423
7747
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
7424
7748
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
7425
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
7749
+ const spreadWidth = maxCrossAxisSpreadWidth(
7750
+ swimlane,
7751
+ nodeBoxes,
7752
+ flowRanks,
7753
+ locks,
7754
+ rankStackGap,
7755
+ containedChildIds
7756
+ );
7757
+ const slotWidth = Math.max(
7758
+ Math.max(...populatedBounds.map((box) => box.width)),
7759
+ spreadWidth
7760
+ ) + padding * 2;
7426
7761
  const laneStep = slotWidth + laneGutter;
7427
7762
  const laneContentTop = top + headerHeight + padding;
7428
7763
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -7436,6 +7771,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7436
7771
  y: laneContentTop
7437
7772
  };
7438
7773
  if (maxRank === 0) {
7774
+ const distributable = lane.children.filter(
7775
+ (childId) => !locks.has(childId)
7776
+ );
7777
+ const coveredByContainment = lane.children.some(
7778
+ (childId) => containedChildIds.has(childId)
7779
+ );
7780
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7781
+ moveRankedVerticalLaneChildren(
7782
+ lane.children,
7783
+ nodeBoxes,
7784
+ locks,
7785
+ diagnostics,
7786
+ movedChildIds,
7787
+ flowRanks,
7788
+ rankSpacing,
7789
+ rankStackGap,
7790
+ { x: target.x, y: laneContentTop },
7791
+ slotWidth - padding * 2
7792
+ );
7793
+ continue;
7794
+ }
7439
7795
  moveLaneChildren(
7440
7796
  lane.children,
7441
7797
  nodeBoxes,
@@ -7449,6 +7805,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7449
7805
  );
7450
7806
  continue;
7451
7807
  }
7808
+ const rankedCoveredByContainment = lane.children.some(
7809
+ (childId) => containedChildIds.has(childId)
7810
+ );
7452
7811
  moveRankedVerticalLaneChildren(
7453
7812
  lane.children,
7454
7813
  nodeBoxes,
@@ -7458,10 +7817,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7458
7817
  flowRanks,
7459
7818
  rankSpacing,
7460
7819
  rankStackGap,
7461
- {
7462
- x: target.x - bounds.x,
7463
- y: laneContentTop
7464
- }
7820
+ { x: target.x, y: laneContentTop },
7821
+ slotWidth - padding * 2,
7822
+ rankedCoveredByContainment
7465
7823
  );
7466
7824
  }
7467
7825
  return {
@@ -7570,31 +7928,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
7570
7928
  }
7571
7929
  return maxHeight;
7572
7930
  }
7573
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7931
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7932
+ function crossAxisSpreadWidth(items, gap) {
7933
+ return items.reduce(
7934
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7935
+ 0
7936
+ );
7937
+ }
7938
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7939
+ let maxWidth = 0;
7940
+ for (const lane of swimlane.lanes) {
7941
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7942
+ continue;
7943
+ }
7944
+ for (const stack of rankStacks(
7945
+ lane.children,
7946
+ nodeBoxes,
7947
+ flowRanks
7948
+ ).values()) {
7949
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7950
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7951
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7952
+ }
7953
+ }
7954
+ return maxWidth;
7955
+ }
7956
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
7574
7957
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7575
- let yOffset = 0;
7958
+ const unlocked = [];
7576
7959
  for (const item of stack) {
7577
- const { childId, box } = item;
7578
- if (locks.has(childId)) {
7960
+ if (locks.has(item.childId)) {
7579
7961
  diagnostics.push({
7580
7962
  severity: "warning",
7581
7963
  code: "constraints.locked-target-not-moved",
7582
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7964
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
7583
7965
  path: ["swimlanes"],
7584
- detail: { nodeId: childId }
7966
+ detail: { nodeId: item.childId }
7585
7967
  });
7586
- continue;
7968
+ } else {
7969
+ unlocked.push(item);
7587
7970
  }
7971
+ }
7972
+ if (unlocked.length === 0) continue;
7973
+ if (unlocked.length === 1) {
7974
+ const { childId, box } = unlocked[0];
7588
7975
  const next = {
7589
7976
  ...box,
7590
- x: box.x + target.x,
7591
- y: target.y + rank * rankSpacing + yOffset
7977
+ x: target.x + (contentWidth - box.width) / 2,
7978
+ y: target.y + rank * rankSpacing
7592
7979
  };
7593
7980
  if (next.x !== box.x || next.y !== box.y) {
7594
7981
  movedChildIds.add(childId);
7595
7982
  }
7596
7983
  nodeBoxes.set(childId, next);
7597
- yOffset += box.height + rankStackGap;
7984
+ } else {
7985
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7986
+ if (!shouldSpread) {
7987
+ let yOffset = 0;
7988
+ for (const { childId, box } of unlocked) {
7989
+ const next = {
7990
+ ...box,
7991
+ x: target.x + (contentWidth - box.width) / 2,
7992
+ y: target.y + rank * rankSpacing + yOffset
7993
+ };
7994
+ if (next.x !== box.x || next.y !== box.y) {
7995
+ movedChildIds.add(childId);
7996
+ }
7997
+ nodeBoxes.set(childId, next);
7998
+ yOffset += box.height + rankStackGap;
7999
+ }
8000
+ } else {
8001
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
8002
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
8003
+ for (const { childId, box } of unlocked) {
8004
+ const next = {
8005
+ ...box,
8006
+ x: xCursor,
8007
+ y: target.y + rank * rankSpacing
8008
+ };
8009
+ if (next.x !== box.x || next.y !== box.y) {
8010
+ movedChildIds.add(childId);
8011
+ }
8012
+ nodeBoxes.set(childId, next);
8013
+ xCursor += box.width + rankStackGap;
8014
+ }
8015
+ diagnostics.push({
8016
+ severity: "info",
8017
+ code: "swimlane_contract.cross_axis_distributed",
8018
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
8019
+ path: ["swimlanes"],
8020
+ detail: {
8021
+ rank,
8022
+ childCount: unlocked.length,
8023
+ contentWidth
8024
+ }
8025
+ });
8026
+ }
7598
8027
  }
7599
8028
  }
7600
8029
  }
@@ -7933,7 +8362,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7933
8362
  });
7934
8363
  continue;
7935
8364
  }
7936
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
8365
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7937
8366
  const geometry = computeShapeGeometry({
7938
8367
  shape: node.shape,
7939
8368
  box,
@@ -8027,7 +8456,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
8027
8456
  }
8028
8457
  }
8029
8458
  }
8030
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8459
+ function coordinatePorts(node, nodeBox, portShifting) {
8031
8460
  const portsBySide = /* @__PURE__ */ new Map();
8032
8461
  for (const port of node.ports ?? []) {
8033
8462
  const ports = portsBySide.get(port.side) ?? [];
@@ -8050,9 +8479,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8050
8479
  side,
8051
8480
  index,
8052
8481
  sorted.length,
8053
- portShifting,
8054
- diagnostics,
8055
- node.id
8482
+ portShifting
8056
8483
  );
8057
8484
  const box = portBox(anchor);
8058
8485
  coordinated.push({ ...port, box, anchor });
@@ -8060,32 +8487,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8060
8487
  }
8061
8488
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
8062
8489
  }
8063
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
8490
+ function portAnchor(nodeBox, side, index, count, portShifting) {
8064
8491
  const shiftingEnabled = portShifting?.enabled ?? true;
8065
8492
  const requestedSpacing = portShifting?.spacing ?? 24;
8066
8493
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
8067
8494
  const availableSpan = 2 * maxOffset;
8068
8495
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
8069
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
8496
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
8070
8497
  Math.min(requestedSpacing, availableSpan / (count - 1)),
8071
8498
  minSpacing
8072
8499
  ) : requestedSpacing;
8073
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
8074
- diagnostics.push({
8075
- severity: "warning",
8076
- code: "port_constraint_overlap",
8077
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
8078
- path: ["nodes", nodeId, "ports"],
8079
- detail: {
8080
- nodeId,
8081
- side,
8082
- requestedSpacing,
8083
- effectiveSpacing: Math.round(effectiveSpacing),
8084
- portCount: count
8085
- }
8086
- });
8087
- }
8088
- const spacing = effectiveSpacing;
8089
8500
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
8090
8501
  switch (side) {
8091
8502
  case "left":
@@ -8700,14 +9111,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
8700
9111
  }
8701
9112
  };
8702
9113
  }
8703
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
9114
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
8704
9115
  const coordinated = [];
8705
9116
  const coordinatedNodeById = new Map(
8706
9117
  coordinatedNodes.map((node) => [node.id, node])
8707
9118
  );
9119
+ const corridorMarginOption = options.corridorMargin ?? "auto";
9120
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
9121
+ 200,
9122
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
9123
+ );
9124
+ const routingGutter = options.routingGutter ?? 160;
9125
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
8708
9126
  const nodeObstacleIndex = createBoxSpatialIndex(
8709
9127
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
8710
- options.routingGutter ?? 160
9128
+ queryGutter
8711
9129
  );
8712
9130
  for (const edge of edges) {
8713
9131
  const source = nodes.get(edge.source.nodeId);
@@ -8729,11 +9147,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8729
9147
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
8730
9148
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
8731
9149
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
8732
- const corridor = edgeCorridorBox(
8733
- source.box,
8734
- target.box,
8735
- options.routingGutter ?? 160
8736
- );
9150
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
8737
9151
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
8738
9152
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
8739
9153
  );
@@ -8751,7 +9165,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
8751
9165
  ...routeTextObstacles
8752
9166
  ],
8753
9167
  hardObstacles,
8754
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
9168
+ corridorMargin,
9169
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
9170
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
8755
9171
  });
8756
9172
  diagnostics.push(
8757
9173
  ...route.diagnostics.map((diagnostic) => ({