@crazyhappyone/auto-graph 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1391,6 +1391,28 @@ function applyLayoutConstraints(input) {
1391
1391
  if (input.distributeContainedChildren) {
1392
1392
  yieldFixedPositionLocks(input, boxes, locks);
1393
1393
  }
1394
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1395
+ for (const swimlane of input.swimlanes) {
1396
+ if (swimlane.layout === "contract") continue;
1397
+ for (const lane of swimlane.lanes) {
1398
+ const fixedChildren = [];
1399
+ let participantCount = 0;
1400
+ for (const childId of lane.children) {
1401
+ const lock = locks.get(childId);
1402
+ if (lock === void 0) {
1403
+ participantCount += 1;
1404
+ } else if (lock.source === "fixed-position") {
1405
+ participantCount += 1;
1406
+ fixedChildren.push(childId);
1407
+ }
1408
+ }
1409
+ if (participantCount < 2) continue;
1410
+ for (const childId of fixedChildren) {
1411
+ locks.delete(childId);
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1394
1416
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
1395
1417
  applyRelative(input.constraints, boxes, locks, diagnostics);
1396
1418
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -1410,6 +1432,9 @@ function applyLayoutConstraints(input) {
1410
1432
  applyDistributeContained(input, boxes, locks, diagnostics);
1411
1433
  dedupReplayDiagnostics(diagnostics, diagBefore);
1412
1434
  }
1435
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1436
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1437
+ }
1413
1438
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1414
1439
  reportOverlaps(
1415
1440
  boxes,
@@ -2249,9 +2274,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2249
2274
  }
2250
2275
  });
2251
2276
  }
2252
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
2253
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
2254
- }
2255
2277
  }
2256
2278
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2257
2279
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -2291,6 +2313,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2291
2313
  effectiveGap = minGap + remaining / (unlocked.length - 1);
2292
2314
  }
2293
2315
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
2316
+ reserved.sort((a, b) => a.start - b.start);
2294
2317
  let pos = contentStart;
2295
2318
  for (const child of unlocked) {
2296
2319
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4082,15 +4105,12 @@ var BinaryHeap = class {
4082
4105
  let smallestIdx = idx;
4083
4106
  const leftIdx = (idx << 1) + 1;
4084
4107
  const rightIdx = leftIdx + 1;
4085
- if (leftIdx < size && this._less(
4086
- this._data[leftIdx],
4087
- this._data[smallestIdx]
4088
- )) {
4108
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4089
4109
  smallestIdx = leftIdx;
4090
4110
  }
4091
4111
  if (rightIdx < size && this._less(
4092
4112
  this._data[rightIdx],
4093
- this._data[smallestIdx]
4113
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4094
4114
  )) {
4095
4115
  smallestIdx = rightIdx;
4096
4116
  }
@@ -4157,8 +4177,59 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4157
4177
  turnPenalty,
4158
4178
  segmentPenalty
4159
4179
  );
4160
- if (path === null) return null;
4161
- return simplifyRoute(path);
4180
+ if (path !== null) {
4181
+ const simplified = simplifyRoute(path);
4182
+ const filteredSet = new Set(filtered);
4183
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4184
+ let crossesExcluded = false;
4185
+ for (let i = 0; i < simplified.length - 1; i++) {
4186
+ const a = simplified[i];
4187
+ const b = simplified[i + 1];
4188
+ for (const obs of obstacles) {
4189
+ if (filteredSet.has(obs)) continue;
4190
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4191
+ crossesExcluded = true;
4192
+ break;
4193
+ }
4194
+ }
4195
+ if (crossesExcluded) break;
4196
+ }
4197
+ if (!crossesExcluded) return simplified;
4198
+ } else {
4199
+ return simplified;
4200
+ }
4201
+ }
4202
+ if (!useCorridor) return null;
4203
+ const xsFull = collectXs(source, target, obstacles, margin);
4204
+ const ysFull = collectYs(source, target, obstacles, margin);
4205
+ if (xsFull.length * ysFull.length > maxNodes) {
4206
+ diagnostics?.push({
4207
+ severity: "warning",
4208
+ code: "routing.astar.grid_overflow",
4209
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4210
+ detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4211
+ });
4212
+ return null;
4213
+ }
4214
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4215
+ connectHorizontalEdges(
4216
+ nodesFull,
4217
+ ysFull,
4218
+ obstacles,
4219
+ endpointObstacles,
4220
+ margin
4221
+ );
4222
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4223
+ const pathFull = aStarSearch(
4224
+ nodesFull,
4225
+ idxFull,
4226
+ source,
4227
+ target,
4228
+ turnPenalty,
4229
+ segmentPenalty
4230
+ );
4231
+ if (pathFull === null) return null;
4232
+ return simplifyRoute(pathFull);
4162
4233
  }
4163
4234
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4164
4235
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -4385,7 +4456,7 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
4385
4456
  const turnPenalty = options.turnPenalty ?? 50;
4386
4457
  const segmentPenalty = options.segmentPenalty ?? 1;
4387
4458
  const endpointObstacles = options.endpointObstacles ?? [];
4388
- const maxCorners = options.maxCorners ?? 300;
4459
+ const maxCorners = options.maxCorners ?? 600;
4389
4460
  const vertices = collectCornerVertices(source, target, obstacles, margin);
4390
4461
  if (vertices.length > maxCorners) {
4391
4462
  diagnostics?.push({
@@ -4754,17 +4825,36 @@ function routeEdge(input) {
4754
4825
  input.source.center,
4755
4826
  targetAnchor
4756
4827
  );
4757
- const cornerPath = findCornerGraphPath(
4828
+ const allObstacles = [...softObstacles, ...hardObstacles];
4829
+ const corridorObstacles = filterObstaclesByCorridor(
4758
4830
  source,
4759
4831
  target,
4760
- [...softObstacles, ...hardObstacles],
4832
+ allObstacles,
4833
+ [],
4834
+ // endpointObstacles passed separately via options
4835
+ 32
4836
+ );
4837
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4838
+ let cornerPath = findCornerGraphPath(
4839
+ source,
4840
+ target,
4841
+ cornerObstacles,
4761
4842
  { endpointObstacles, margin: 2 },
4762
4843
  diagnostics
4763
4844
  );
4845
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4846
+ cornerPath = findCornerGraphPath(
4847
+ source,
4848
+ target,
4849
+ allObstacles,
4850
+ { endpointObstacles, margin: 2 },
4851
+ diagnostics
4852
+ );
4853
+ }
4764
4854
  const path = cornerPath ?? findObstacleFreePath(
4765
4855
  source,
4766
4856
  target,
4767
- [...softObstacles, ...hardObstacles],
4857
+ allObstacles,
4768
4858
  { endpointObstacles, margin: 0 },
4769
4859
  diagnostics
4770
4860
  );
@@ -4785,6 +4875,66 @@ function routeEdge(input) {
4785
4875
  checkBacktracking(finalized, source, target, diagnostics);
4786
4876
  return { points: finalized, diagnostics };
4787
4877
  }
4878
+ if (cornerPath !== null) {
4879
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4880
+ source,
4881
+ target,
4882
+ allObstacles,
4883
+ { endpointObstacles, margin: 2 },
4884
+ diagnostics
4885
+ ) : null;
4886
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
4887
+ const fullFinalized = finalizeRoute(
4888
+ fullCornerPath,
4889
+ softObstacles,
4890
+ hardObstacles,
4891
+ diagnostics,
4892
+ softObstacleIndex,
4893
+ hardObstacleIndex
4894
+ );
4895
+ if (!routeIntersectsObstacles(
4896
+ fullFinalized,
4897
+ softObstacles,
4898
+ softObstacleIndex
4899
+ ) && !routeIntersectsObstacles(
4900
+ fullFinalized,
4901
+ hardObstacles,
4902
+ hardObstacleIndex
4903
+ )) {
4904
+ checkBacktracking(fullFinalized, source, target, diagnostics);
4905
+ return { points: fullFinalized, diagnostics };
4906
+ }
4907
+ }
4908
+ const gridPath = findObstacleFreePath(
4909
+ source,
4910
+ target,
4911
+ allObstacles,
4912
+ { endpointObstacles, margin: 0 },
4913
+ diagnostics
4914
+ );
4915
+ if (gridPath !== null && gridPath.length >= 2) {
4916
+ const gridFinalized = finalizeRoute(
4917
+ gridPath,
4918
+ softObstacles,
4919
+ hardObstacles,
4920
+ diagnostics,
4921
+ softObstacleIndex,
4922
+ hardObstacleIndex
4923
+ );
4924
+ if (!routeIntersectsObstacles(
4925
+ gridFinalized,
4926
+ softObstacles,
4927
+ softObstacleIndex
4928
+ ) && !routeIntersectsObstacles(
4929
+ gridFinalized,
4930
+ hardObstacles,
4931
+ hardObstacleIndex
4932
+ )) {
4933
+ checkBacktracking(gridFinalized, source, target, diagnostics);
4934
+ return { points: gridFinalized, diagnostics };
4935
+ }
4936
+ }
4937
+ }
4788
4938
  }
4789
4939
  }
4790
4940
  }
@@ -5674,7 +5824,7 @@ function solveDiagram(diagram, options = {}) {
5674
5824
  edges: styledEdges
5675
5825
  });
5676
5826
  diagnostics.push(...layout2.diagnostics);
5677
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
5827
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5678
5828
  layout2.boxes,
5679
5829
  styledNodes,
5680
5830
  styledEdges,
@@ -5682,7 +5832,8 @@ function solveDiagram(diagram, options = {}) {
5682
5832
  options,
5683
5833
  diagnostics
5684
5834
  );
5685
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
5835
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
5836
+ const diagCountBefore = diagnostics.length;
5686
5837
  const rewrapped = wrapHorizontalStackIfNeeded(
5687
5838
  initialNodeBoxes,
5688
5839
  styledNodes,
@@ -5693,6 +5844,20 @@ function solveDiagram(diagram, options = {}) {
5693
5844
  for (const [id, box] of rewrapped) {
5694
5845
  initialNodeBoxes.set(id, box);
5695
5846
  }
5847
+ if (diagnostics.length > diagCountBefore) {
5848
+ for (const node of styledNodes) {
5849
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
5850
+ const rwBox = rewrapped.get(node.id);
5851
+ const idx = styledNodes.indexOf(node);
5852
+ if (idx !== -1) {
5853
+ styledNodes[idx] = {
5854
+ ...node,
5855
+ position: { x: rwBox.x, y: rwBox.y }
5856
+ };
5857
+ }
5858
+ }
5859
+ }
5860
+ }
5696
5861
  }
5697
5862
  if (useRecursive && "groupBoxes" in layout2) {
5698
5863
  const recursiveLayout = layout2;
@@ -5706,7 +5871,7 @@ function solveDiagram(diagram, options = {}) {
5706
5871
  overlapSpacing: options?.overlapSpacing ?? 40,
5707
5872
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5708
5873
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5709
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
5874
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5710
5875
  swimlanes: styledSwimlanes,
5711
5876
  boxes: initialNodeBoxes,
5712
5877
  nodes: styledNodes,
@@ -6422,9 +6587,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6422
6587
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6423
6588
  return new Map(boxes);
6424
6589
  }
6425
- const maxRowDepth = options.maxRowDepth;
6426
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6427
- return new Map(boxes);
6590
+ let maxRowDepth = options.maxRowDepth;
6591
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6592
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6593
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6594
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6595
+ } else {
6596
+ return new Map(boxes);
6597
+ }
6428
6598
  }
6429
6599
  const ordered = [...nodes].sort((a, b) => {
6430
6600
  const ba = boxes.get(a.id);
@@ -6485,10 +6655,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6485
6655
  });
6486
6656
  }
6487
6657
  function isStackRunaway(boxes, nodes, direction, options) {
6488
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6658
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6489
6659
  return false;
6490
6660
  }
6491
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6661
+ if (nodes.length < 2) {
6492
6662
  return false;
6493
6663
  }
6494
6664
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6496,11 +6666,18 @@ function isStackRunaway(boxes, nodes, direction, options) {
6496
6666
  return false;
6497
6667
  }
6498
6668
  const bounds = unionBoxes(nodeBoxes);
6499
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6500
- const preferred = options.preferredAspectRatio ?? 3;
6501
- if (aspectRatio < preferred) {
6669
+ const isHorizontal = direction === "TB" || direction === "BT";
6670
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6671
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6672
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6502
6673
  return false;
6503
6674
  }
6675
+ if (isHorizontal) {
6676
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6677
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6678
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6679
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6680
+ }
6504
6681
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6505
6682
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6506
6683
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
@@ -6564,7 +6741,17 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6564
6741
  );
6565
6742
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6566
6743
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6567
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6744
+ const spreadWidth = maxCrossAxisSpreadWidth(
6745
+ swimlane,
6746
+ nodeBoxes,
6747
+ flowRanks,
6748
+ locks,
6749
+ rankStackGap
6750
+ );
6751
+ const slotWidth = Math.max(
6752
+ Math.max(...populatedBounds.map((box) => box.width)),
6753
+ spreadWidth
6754
+ ) + padding * 2;
6568
6755
  const laneStep = slotWidth + laneGutter;
6569
6756
  const laneContentTop = top + headerHeight + padding;
6570
6757
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6578,6 +6765,24 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6578
6765
  y: laneContentTop
6579
6766
  };
6580
6767
  if (maxRank === 0) {
6768
+ const distributable = lane.children.filter(
6769
+ (childId) => !locks.has(childId)
6770
+ );
6771
+ if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6772
+ moveRankedVerticalLaneChildren(
6773
+ lane.children,
6774
+ nodeBoxes,
6775
+ locks,
6776
+ diagnostics,
6777
+ movedChildIds,
6778
+ flowRanks,
6779
+ rankSpacing,
6780
+ rankStackGap,
6781
+ { x: target.x, y: laneContentTop },
6782
+ slotWidth - padding * 2
6783
+ );
6784
+ continue;
6785
+ }
6581
6786
  moveLaneChildren(
6582
6787
  lane.children,
6583
6788
  nodeBoxes,
@@ -6600,10 +6805,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6600
6805
  flowRanks,
6601
6806
  rankSpacing,
6602
6807
  rankStackGap,
6603
- {
6604
- x: target.x - bounds.x,
6605
- y: laneContentTop
6606
- }
6808
+ { x: target.x, y: laneContentTop },
6809
+ slotWidth - padding * 2
6607
6810
  );
6608
6811
  }
6609
6812
  return {
@@ -6712,31 +6915,99 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6712
6915
  }
6713
6916
  return maxHeight;
6714
6917
  }
6715
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
6918
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
6919
+ function crossAxisSpreadWidth(items, gap) {
6920
+ return items.reduce(
6921
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
6922
+ 0
6923
+ );
6924
+ }
6925
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
6926
+ let maxWidth = 0;
6927
+ for (const lane of swimlane.lanes) {
6928
+ for (const stack of rankStacks(
6929
+ lane.children,
6930
+ nodeBoxes,
6931
+ flowRanks
6932
+ ).values()) {
6933
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
6934
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
6935
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
6936
+ }
6937
+ }
6938
+ return maxWidth;
6939
+ }
6940
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
6716
6941
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6717
- let yOffset = 0;
6942
+ const unlocked = [];
6718
6943
  for (const item of stack) {
6719
- const { childId, box } = item;
6720
- if (locks.has(childId)) {
6944
+ if (locks.has(item.childId)) {
6721
6945
  diagnostics.push({
6722
6946
  severity: "warning",
6723
6947
  code: "constraints.locked-target-not-moved",
6724
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
6948
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6725
6949
  path: ["swimlanes"],
6726
- detail: { nodeId: childId }
6950
+ detail: { nodeId: item.childId }
6727
6951
  });
6728
- continue;
6952
+ } else {
6953
+ unlocked.push(item);
6729
6954
  }
6955
+ }
6956
+ if (unlocked.length === 0) continue;
6957
+ if (unlocked.length === 1) {
6958
+ const { childId, box } = unlocked[0];
6730
6959
  const next = {
6731
6960
  ...box,
6732
- x: box.x + target.x,
6733
- y: target.y + rank * rankSpacing + yOffset
6961
+ x: target.x + (contentWidth - box.width) / 2,
6962
+ y: target.y + rank * rankSpacing
6734
6963
  };
6735
6964
  if (next.x !== box.x || next.y !== box.y) {
6736
6965
  movedChildIds.add(childId);
6737
6966
  }
6738
6967
  nodeBoxes.set(childId, next);
6739
- yOffset += box.height + rankStackGap;
6968
+ } else {
6969
+ const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
6970
+ if (!shouldSpread) {
6971
+ let yOffset = 0;
6972
+ for (const { childId, box } of unlocked) {
6973
+ const next = {
6974
+ ...box,
6975
+ x: target.x + (contentWidth - box.width) / 2,
6976
+ y: target.y + rank * rankSpacing + yOffset
6977
+ };
6978
+ if (next.x !== box.x || next.y !== box.y) {
6979
+ movedChildIds.add(childId);
6980
+ }
6981
+ nodeBoxes.set(childId, next);
6982
+ yOffset += box.height + rankStackGap;
6983
+ }
6984
+ } else {
6985
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
6986
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
6987
+ for (const { childId, box } of unlocked) {
6988
+ const next = {
6989
+ ...box,
6990
+ x: xCursor,
6991
+ y: target.y + rank * rankSpacing
6992
+ };
6993
+ if (next.x !== box.x || next.y !== box.y) {
6994
+ movedChildIds.add(childId);
6995
+ }
6996
+ nodeBoxes.set(childId, next);
6997
+ xCursor += box.width + rankStackGap;
6998
+ }
6999
+ diagnostics.push({
7000
+ severity: "info",
7001
+ code: "swimlane_contract.cross_axis_distributed",
7002
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7003
+ path: ["swimlanes"],
7004
+ detail: {
7005
+ rank,
7006
+ childCount: unlocked.length,
7007
+ contentWidth
7008
+ }
7009
+ });
7010
+ }
6740
7011
  }
6741
7012
  }
6742
7013
  }
@@ -7075,7 +7346,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7075
7346
  });
7076
7347
  continue;
7077
7348
  }
7078
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7349
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7079
7350
  const geometry = computeShapeGeometry({
7080
7351
  shape: node.shape,
7081
7352
  box,
@@ -7169,7 +7440,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7169
7440
  }
7170
7441
  }
7171
7442
  }
7172
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7443
+ function coordinatePorts(node, nodeBox, portShifting) {
7173
7444
  const portsBySide = /* @__PURE__ */ new Map();
7174
7445
  for (const port of node.ports ?? []) {
7175
7446
  const ports = portsBySide.get(port.side) ?? [];
@@ -7192,9 +7463,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7192
7463
  side,
7193
7464
  index,
7194
7465
  sorted.length,
7195
- portShifting,
7196
- diagnostics,
7197
- node.id
7466
+ portShifting
7198
7467
  );
7199
7468
  const box = portBox(anchor);
7200
7469
  coordinated.push({ ...port, box, anchor });
@@ -7202,32 +7471,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7202
7471
  }
7203
7472
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7204
7473
  }
7205
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7474
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7206
7475
  const shiftingEnabled = portShifting?.enabled ?? true;
7207
7476
  const requestedSpacing = portShifting?.spacing ?? 24;
7208
7477
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7209
7478
  const availableSpan = 2 * maxOffset;
7210
7479
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7211
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7480
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7212
7481
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7213
7482
  minSpacing
7214
7483
  ) : requestedSpacing;
7215
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
7216
- diagnostics.push({
7217
- severity: "warning",
7218
- code: "port_constraint_overlap",
7219
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
7220
- path: ["nodes", nodeId, "ports"],
7221
- detail: {
7222
- nodeId,
7223
- side,
7224
- requestedSpacing,
7225
- effectiveSpacing: Math.round(effectiveSpacing),
7226
- portCount: count
7227
- }
7228
- });
7229
- }
7230
- const spacing = effectiveSpacing;
7231
7484
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7232
7485
  switch (side) {
7233
7486
  case "left":