@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.
@@ -1394,6 +1394,28 @@ function applyLayoutConstraints(input) {
1394
1394
  if (input.distributeContainedChildren) {
1395
1395
  yieldFixedPositionLocks(input, boxes, locks);
1396
1396
  }
1397
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1398
+ for (const swimlane of input.swimlanes) {
1399
+ if (swimlane.layout === "contract") continue;
1400
+ for (const lane of swimlane.lanes) {
1401
+ const fixedChildren = [];
1402
+ let participantCount = 0;
1403
+ for (const childId of lane.children) {
1404
+ const lock = locks.get(childId);
1405
+ if (lock === void 0) {
1406
+ participantCount += 1;
1407
+ } else if (lock.source === "fixed-position") {
1408
+ participantCount += 1;
1409
+ fixedChildren.push(childId);
1410
+ }
1411
+ }
1412
+ if (participantCount < 2) continue;
1413
+ for (const childId of fixedChildren) {
1414
+ locks.delete(childId);
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1397
1419
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
1398
1420
  applyRelative(input.constraints, boxes, locks, diagnostics);
1399
1421
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -1413,6 +1435,9 @@ function applyLayoutConstraints(input) {
1413
1435
  applyDistributeContained(input, boxes, locks, diagnostics);
1414
1436
  dedupReplayDiagnostics(diagnostics, diagBefore);
1415
1437
  }
1438
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1439
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1440
+ }
1416
1441
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1417
1442
  reportOverlaps(
1418
1443
  boxes,
@@ -2252,9 +2277,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2252
2277
  }
2253
2278
  });
2254
2279
  }
2255
- if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
2256
- distributeSwimlaneChildren(input, boxes, locks, diagnostics);
2257
- }
2258
2280
  }
2259
2281
  function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2260
2282
  const spread = input.distributeSwimlaneChildren === "spread";
@@ -2294,6 +2316,7 @@ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2294
2316
  effectiveGap = minGap + remaining / (unlocked.length - 1);
2295
2317
  }
2296
2318
  unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
2319
+ reserved.sort((a, b) => a.start - b.start);
2297
2320
  let pos = contentStart;
2298
2321
  for (const child of unlocked) {
2299
2322
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
@@ -4085,15 +4108,12 @@ var BinaryHeap = class {
4085
4108
  let smallestIdx = idx;
4086
4109
  const leftIdx = (idx << 1) + 1;
4087
4110
  const rightIdx = leftIdx + 1;
4088
- if (leftIdx < size && this._less(
4089
- this._data[leftIdx],
4090
- this._data[smallestIdx]
4091
- )) {
4111
+ if (leftIdx < size && this._less(this._data[leftIdx], entry)) {
4092
4112
  smallestIdx = leftIdx;
4093
4113
  }
4094
4114
  if (rightIdx < size && this._less(
4095
4115
  this._data[rightIdx],
4096
- this._data[smallestIdx]
4116
+ smallestIdx === leftIdx ? this._data[leftIdx] : entry
4097
4117
  )) {
4098
4118
  smallestIdx = rightIdx;
4099
4119
  }
@@ -4160,8 +4180,59 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4160
4180
  turnPenalty,
4161
4181
  segmentPenalty
4162
4182
  );
4163
- if (path === null) return null;
4164
- return simplifyRoute(path);
4183
+ if (path !== null) {
4184
+ const simplified = simplifyRoute(path);
4185
+ const filteredSet = new Set(filtered);
4186
+ if (useCorridor && obstacles.some((o) => !filteredSet.has(o))) {
4187
+ let crossesExcluded = false;
4188
+ for (let i = 0; i < simplified.length - 1; i++) {
4189
+ const a = simplified[i];
4190
+ const b = simplified[i + 1];
4191
+ for (const obs of obstacles) {
4192
+ if (filteredSet.has(obs)) continue;
4193
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) {
4194
+ crossesExcluded = true;
4195
+ break;
4196
+ }
4197
+ }
4198
+ if (crossesExcluded) break;
4199
+ }
4200
+ if (!crossesExcluded) return simplified;
4201
+ } else {
4202
+ return simplified;
4203
+ }
4204
+ }
4205
+ if (!useCorridor) return null;
4206
+ const xsFull = collectXs(source, target, obstacles, margin);
4207
+ const ysFull = collectYs(source, target, obstacles, margin);
4208
+ if (xsFull.length * ysFull.length > maxNodes) {
4209
+ diagnostics?.push({
4210
+ severity: "warning",
4211
+ code: "routing.astar.grid_overflow",
4212
+ message: `A* full-retry grid overflow: ${xsFull.length * ysFull.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4213
+ detail: { xsCount: xsFull.length, ysCount: ysFull.length, maxNodes }
4214
+ });
4215
+ return null;
4216
+ }
4217
+ const { nodes: nodesFull, nodeIndex: idxFull } = buildGraph(xsFull, ysFull);
4218
+ connectHorizontalEdges(
4219
+ nodesFull,
4220
+ ysFull,
4221
+ obstacles,
4222
+ endpointObstacles,
4223
+ margin
4224
+ );
4225
+ connectVerticalEdges(nodesFull, xsFull, obstacles, endpointObstacles, margin);
4226
+ const pathFull = aStarSearch(
4227
+ nodesFull,
4228
+ idxFull,
4229
+ source,
4230
+ target,
4231
+ turnPenalty,
4232
+ segmentPenalty
4233
+ );
4234
+ if (pathFull === null) return null;
4235
+ return simplifyRoute(pathFull);
4165
4236
  }
4166
4237
  function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4167
4238
  const cx1 = Math.min(source.x, target.x) - margin;
@@ -4388,7 +4459,7 @@ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostic
4388
4459
  const turnPenalty = options.turnPenalty ?? 50;
4389
4460
  const segmentPenalty = options.segmentPenalty ?? 1;
4390
4461
  const endpointObstacles = options.endpointObstacles ?? [];
4391
- const maxCorners = options.maxCorners ?? 300;
4462
+ const maxCorners = options.maxCorners ?? 600;
4392
4463
  const vertices = collectCornerVertices(source, target, obstacles, margin);
4393
4464
  if (vertices.length > maxCorners) {
4394
4465
  diagnostics?.push({
@@ -4757,17 +4828,36 @@ function routeEdge(input) {
4757
4828
  input.source.center,
4758
4829
  targetAnchor
4759
4830
  );
4760
- const cornerPath = findCornerGraphPath(
4831
+ const allObstacles = [...softObstacles, ...hardObstacles];
4832
+ const corridorObstacles = filterObstaclesByCorridor(
4761
4833
  source,
4762
4834
  target,
4763
- [...softObstacles, ...hardObstacles],
4835
+ allObstacles,
4836
+ [],
4837
+ // endpointObstacles passed separately via options
4838
+ 32
4839
+ );
4840
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4841
+ let cornerPath = findCornerGraphPath(
4842
+ source,
4843
+ target,
4844
+ cornerObstacles,
4764
4845
  { endpointObstacles, margin: 2 },
4765
4846
  diagnostics
4766
4847
  );
4848
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4849
+ cornerPath = findCornerGraphPath(
4850
+ source,
4851
+ target,
4852
+ allObstacles,
4853
+ { endpointObstacles, margin: 2 },
4854
+ diagnostics
4855
+ );
4856
+ }
4767
4857
  const path = cornerPath ?? findObstacleFreePath(
4768
4858
  source,
4769
4859
  target,
4770
- [...softObstacles, ...hardObstacles],
4860
+ allObstacles,
4771
4861
  { endpointObstacles, margin: 0 },
4772
4862
  diagnostics
4773
4863
  );
@@ -4788,6 +4878,66 @@ function routeEdge(input) {
4788
4878
  checkBacktracking(finalized, source, target, diagnostics);
4789
4879
  return { points: finalized, diagnostics };
4790
4880
  }
4881
+ if (cornerPath !== null) {
4882
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4883
+ source,
4884
+ target,
4885
+ allObstacles,
4886
+ { endpointObstacles, margin: 2 },
4887
+ diagnostics
4888
+ ) : null;
4889
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
4890
+ const fullFinalized = finalizeRoute(
4891
+ fullCornerPath,
4892
+ softObstacles,
4893
+ hardObstacles,
4894
+ diagnostics,
4895
+ softObstacleIndex,
4896
+ hardObstacleIndex
4897
+ );
4898
+ if (!routeIntersectsObstacles(
4899
+ fullFinalized,
4900
+ softObstacles,
4901
+ softObstacleIndex
4902
+ ) && !routeIntersectsObstacles(
4903
+ fullFinalized,
4904
+ hardObstacles,
4905
+ hardObstacleIndex
4906
+ )) {
4907
+ checkBacktracking(fullFinalized, source, target, diagnostics);
4908
+ return { points: fullFinalized, diagnostics };
4909
+ }
4910
+ }
4911
+ const gridPath = findObstacleFreePath(
4912
+ source,
4913
+ target,
4914
+ allObstacles,
4915
+ { endpointObstacles, margin: 0 },
4916
+ diagnostics
4917
+ );
4918
+ if (gridPath !== null && gridPath.length >= 2) {
4919
+ const gridFinalized = finalizeRoute(
4920
+ gridPath,
4921
+ softObstacles,
4922
+ hardObstacles,
4923
+ diagnostics,
4924
+ softObstacleIndex,
4925
+ hardObstacleIndex
4926
+ );
4927
+ if (!routeIntersectsObstacles(
4928
+ gridFinalized,
4929
+ softObstacles,
4930
+ softObstacleIndex
4931
+ ) && !routeIntersectsObstacles(
4932
+ gridFinalized,
4933
+ hardObstacles,
4934
+ hardObstacleIndex
4935
+ )) {
4936
+ checkBacktracking(gridFinalized, source, target, diagnostics);
4937
+ return { points: gridFinalized, diagnostics };
4938
+ }
4939
+ }
4940
+ }
4791
4941
  }
4792
4942
  }
4793
4943
  }
@@ -5677,7 +5827,7 @@ function solveDiagram(diagram, options = {}) {
5677
5827
  edges: styledEdges
5678
5828
  });
5679
5829
  diagnostics.push(...layout2.diagnostics);
5680
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
5830
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5681
5831
  layout2.boxes,
5682
5832
  styledNodes,
5683
5833
  styledEdges,
@@ -5685,7 +5835,8 @@ function solveDiagram(diagram, options = {}) {
5685
5835
  options,
5686
5836
  diagnostics
5687
5837
  );
5688
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
5838
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
5839
+ const diagCountBefore = diagnostics.length;
5689
5840
  const rewrapped = wrapHorizontalStackIfNeeded(
5690
5841
  initialNodeBoxes,
5691
5842
  styledNodes,
@@ -5696,6 +5847,20 @@ function solveDiagram(diagram, options = {}) {
5696
5847
  for (const [id, box] of rewrapped) {
5697
5848
  initialNodeBoxes.set(id, box);
5698
5849
  }
5850
+ if (diagnostics.length > diagCountBefore) {
5851
+ for (const node of styledNodes) {
5852
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
5853
+ const rwBox = rewrapped.get(node.id);
5854
+ const idx = styledNodes.indexOf(node);
5855
+ if (idx !== -1) {
5856
+ styledNodes[idx] = {
5857
+ ...node,
5858
+ position: { x: rwBox.x, y: rwBox.y }
5859
+ };
5860
+ }
5861
+ }
5862
+ }
5863
+ }
5699
5864
  }
5700
5865
  if (useRecursive && "groupBoxes" in layout2) {
5701
5866
  const recursiveLayout = layout2;
@@ -5709,7 +5874,7 @@ function solveDiagram(diagram, options = {}) {
5709
5874
  overlapSpacing: options?.overlapSpacing ?? 40,
5710
5875
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5711
5876
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5712
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
5877
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5713
5878
  swimlanes: styledSwimlanes,
5714
5879
  boxes: initialNodeBoxes,
5715
5880
  nodes: styledNodes,
@@ -6425,9 +6590,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6425
6590
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6426
6591
  return new Map(boxes);
6427
6592
  }
6428
- const maxRowDepth = options.maxRowDepth;
6429
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6430
- return new Map(boxes);
6593
+ let maxRowDepth = options.maxRowDepth;
6594
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6595
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6596
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6597
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6598
+ } else {
6599
+ return new Map(boxes);
6600
+ }
6431
6601
  }
6432
6602
  const ordered = [...nodes].sort((a, b) => {
6433
6603
  const ba = boxes.get(a.id);
@@ -6488,10 +6658,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6488
6658
  });
6489
6659
  }
6490
6660
  function isStackRunaway(boxes, nodes, direction, options) {
6491
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6661
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6492
6662
  return false;
6493
6663
  }
6494
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6664
+ if (nodes.length < 2) {
6495
6665
  return false;
6496
6666
  }
6497
6667
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6499,11 +6669,18 @@ function isStackRunaway(boxes, nodes, direction, options) {
6499
6669
  return false;
6500
6670
  }
6501
6671
  const bounds = unionBoxes(nodeBoxes);
6502
- const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6503
- const preferred = options.preferredAspectRatio ?? 3;
6504
- if (aspectRatio < preferred) {
6672
+ const isHorizontal = direction === "TB" || direction === "BT";
6673
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6674
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6675
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6505
6676
  return false;
6506
6677
  }
6678
+ if (isHorizontal) {
6679
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6680
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6681
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6682
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6683
+ }
6507
6684
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6508
6685
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6509
6686
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
@@ -6567,7 +6744,17 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6567
6744
  );
6568
6745
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6569
6746
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6570
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6747
+ const spreadWidth = maxCrossAxisSpreadWidth(
6748
+ swimlane,
6749
+ nodeBoxes,
6750
+ flowRanks,
6751
+ locks,
6752
+ rankStackGap
6753
+ );
6754
+ const slotWidth = Math.max(
6755
+ Math.max(...populatedBounds.map((box) => box.width)),
6756
+ spreadWidth
6757
+ ) + padding * 2;
6571
6758
  const laneStep = slotWidth + laneGutter;
6572
6759
  const laneContentTop = top + headerHeight + padding;
6573
6760
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6581,6 +6768,24 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6581
6768
  y: laneContentTop
6582
6769
  };
6583
6770
  if (maxRank === 0) {
6771
+ const distributable = lane.children.filter(
6772
+ (childId) => !locks.has(childId)
6773
+ );
6774
+ if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6775
+ moveRankedVerticalLaneChildren(
6776
+ lane.children,
6777
+ nodeBoxes,
6778
+ locks,
6779
+ diagnostics,
6780
+ movedChildIds,
6781
+ flowRanks,
6782
+ rankSpacing,
6783
+ rankStackGap,
6784
+ { x: target.x, y: laneContentTop },
6785
+ slotWidth - padding * 2
6786
+ );
6787
+ continue;
6788
+ }
6584
6789
  moveLaneChildren(
6585
6790
  lane.children,
6586
6791
  nodeBoxes,
@@ -6603,10 +6808,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6603
6808
  flowRanks,
6604
6809
  rankSpacing,
6605
6810
  rankStackGap,
6606
- {
6607
- x: target.x - bounds.x,
6608
- y: laneContentTop
6609
- }
6811
+ { x: target.x, y: laneContentTop },
6812
+ slotWidth - padding * 2
6610
6813
  );
6611
6814
  }
6612
6815
  return {
@@ -6715,31 +6918,99 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6715
6918
  }
6716
6919
  return maxHeight;
6717
6920
  }
6718
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
6921
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
6922
+ function crossAxisSpreadWidth(items, gap) {
6923
+ return items.reduce(
6924
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
6925
+ 0
6926
+ );
6927
+ }
6928
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
6929
+ let maxWidth = 0;
6930
+ for (const lane of swimlane.lanes) {
6931
+ for (const stack of rankStacks(
6932
+ lane.children,
6933
+ nodeBoxes,
6934
+ flowRanks
6935
+ ).values()) {
6936
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
6937
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
6938
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
6939
+ }
6940
+ }
6941
+ return maxWidth;
6942
+ }
6943
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
6719
6944
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6720
- let yOffset = 0;
6945
+ const unlocked = [];
6721
6946
  for (const item of stack) {
6722
- const { childId, box } = item;
6723
- if (locks.has(childId)) {
6947
+ if (locks.has(item.childId)) {
6724
6948
  diagnostics.push({
6725
6949
  severity: "warning",
6726
6950
  code: "constraints.locked-target-not-moved",
6727
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
6951
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6728
6952
  path: ["swimlanes"],
6729
- detail: { nodeId: childId }
6953
+ detail: { nodeId: item.childId }
6730
6954
  });
6731
- continue;
6955
+ } else {
6956
+ unlocked.push(item);
6732
6957
  }
6958
+ }
6959
+ if (unlocked.length === 0) continue;
6960
+ if (unlocked.length === 1) {
6961
+ const { childId, box } = unlocked[0];
6733
6962
  const next = {
6734
6963
  ...box,
6735
- x: box.x + target.x,
6736
- y: target.y + rank * rankSpacing + yOffset
6964
+ x: target.x + (contentWidth - box.width) / 2,
6965
+ y: target.y + rank * rankSpacing
6737
6966
  };
6738
6967
  if (next.x !== box.x || next.y !== box.y) {
6739
6968
  movedChildIds.add(childId);
6740
6969
  }
6741
6970
  nodeBoxes.set(childId, next);
6742
- yOffset += box.height + rankStackGap;
6971
+ } else {
6972
+ const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
6973
+ if (!shouldSpread) {
6974
+ let yOffset = 0;
6975
+ for (const { childId, box } of unlocked) {
6976
+ const next = {
6977
+ ...box,
6978
+ x: target.x + (contentWidth - box.width) / 2,
6979
+ y: target.y + rank * rankSpacing + yOffset
6980
+ };
6981
+ if (next.x !== box.x || next.y !== box.y) {
6982
+ movedChildIds.add(childId);
6983
+ }
6984
+ nodeBoxes.set(childId, next);
6985
+ yOffset += box.height + rankStackGap;
6986
+ }
6987
+ } else {
6988
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
6989
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
6990
+ for (const { childId, box } of unlocked) {
6991
+ const next = {
6992
+ ...box,
6993
+ x: xCursor,
6994
+ y: target.y + rank * rankSpacing
6995
+ };
6996
+ if (next.x !== box.x || next.y !== box.y) {
6997
+ movedChildIds.add(childId);
6998
+ }
6999
+ nodeBoxes.set(childId, next);
7000
+ xCursor += box.width + rankStackGap;
7001
+ }
7002
+ diagnostics.push({
7003
+ severity: "info",
7004
+ code: "swimlane_contract.cross_axis_distributed",
7005
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7006
+ path: ["swimlanes"],
7007
+ detail: {
7008
+ rank,
7009
+ childCount: unlocked.length,
7010
+ contentWidth
7011
+ }
7012
+ });
7013
+ }
6743
7014
  }
6744
7015
  }
6745
7016
  }
@@ -7078,7 +7349,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7078
7349
  });
7079
7350
  continue;
7080
7351
  }
7081
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7352
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7082
7353
  const geometry = computeShapeGeometry({
7083
7354
  shape: node.shape,
7084
7355
  box,
@@ -7172,7 +7443,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7172
7443
  }
7173
7444
  }
7174
7445
  }
7175
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7446
+ function coordinatePorts(node, nodeBox, portShifting) {
7176
7447
  const portsBySide = /* @__PURE__ */ new Map();
7177
7448
  for (const port of node.ports ?? []) {
7178
7449
  const ports = portsBySide.get(port.side) ?? [];
@@ -7195,9 +7466,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7195
7466
  side,
7196
7467
  index,
7197
7468
  sorted.length,
7198
- portShifting,
7199
- diagnostics,
7200
- node.id
7469
+ portShifting
7201
7470
  );
7202
7471
  const box = portBox(anchor);
7203
7472
  coordinated.push({ ...port, box, anchor });
@@ -7205,32 +7474,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7205
7474
  }
7206
7475
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7207
7476
  }
7208
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7477
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7209
7478
  const shiftingEnabled = portShifting?.enabled ?? true;
7210
7479
  const requestedSpacing = portShifting?.spacing ?? 24;
7211
7480
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7212
7481
  const availableSpan = 2 * maxOffset;
7213
7482
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7214
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7483
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7215
7484
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7216
7485
  minSpacing
7217
7486
  ) : requestedSpacing;
7218
- if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
7219
- diagnostics.push({
7220
- severity: "warning",
7221
- code: "port_constraint_overlap",
7222
- message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
7223
- path: ["nodes", nodeId, "ports"],
7224
- detail: {
7225
- nodeId,
7226
- side,
7227
- requestedSpacing,
7228
- effectiveSpacing: Math.round(effectiveSpacing),
7229
- portCount: count
7230
- }
7231
- });
7232
- }
7233
- const spacing = effectiveSpacing;
7234
7487
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7235
7488
  switch (side) {
7236
7489
  case "left":