@crazyhappyone/auto-graph 0.2.7 → 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/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({
@@ -5401,17 +5472,36 @@ function routeEdge(input) {
5401
5472
  input.source.center,
5402
5473
  targetAnchor
5403
5474
  );
5404
- const cornerPath = findCornerGraphPath(
5475
+ const allObstacles = [...softObstacles, ...hardObstacles];
5476
+ const corridorObstacles = filterObstaclesByCorridor(
5405
5477
  source,
5406
5478
  target,
5407
- [...softObstacles, ...hardObstacles],
5479
+ allObstacles,
5480
+ [],
5481
+ // endpointObstacles passed separately via options
5482
+ 32
5483
+ );
5484
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
5485
+ let cornerPath = findCornerGraphPath(
5486
+ source,
5487
+ target,
5488
+ cornerObstacles,
5408
5489
  { endpointObstacles, margin: 2 },
5409
5490
  diagnostics
5410
5491
  );
5492
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
5493
+ cornerPath = findCornerGraphPath(
5494
+ source,
5495
+ target,
5496
+ allObstacles,
5497
+ { endpointObstacles, margin: 2 },
5498
+ diagnostics
5499
+ );
5500
+ }
5411
5501
  const path = cornerPath ?? findObstacleFreePath(
5412
5502
  source,
5413
5503
  target,
5414
- [...softObstacles, ...hardObstacles],
5504
+ allObstacles,
5415
5505
  { endpointObstacles, margin: 0 },
5416
5506
  diagnostics
5417
5507
  );
@@ -5432,6 +5522,66 @@ function routeEdge(input) {
5432
5522
  checkBacktracking(finalized, source, target, diagnostics);
5433
5523
  return { points: finalized, diagnostics };
5434
5524
  }
5525
+ if (cornerPath !== null) {
5526
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
5527
+ source,
5528
+ target,
5529
+ allObstacles,
5530
+ { endpointObstacles, margin: 2 },
5531
+ diagnostics
5532
+ ) : null;
5533
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
5534
+ const fullFinalized = finalizeRoute(
5535
+ fullCornerPath,
5536
+ softObstacles,
5537
+ hardObstacles,
5538
+ diagnostics,
5539
+ softObstacleIndex,
5540
+ hardObstacleIndex
5541
+ );
5542
+ if (!routeIntersectsObstacles(
5543
+ fullFinalized,
5544
+ softObstacles,
5545
+ softObstacleIndex
5546
+ ) && !routeIntersectsObstacles(
5547
+ fullFinalized,
5548
+ hardObstacles,
5549
+ hardObstacleIndex
5550
+ )) {
5551
+ checkBacktracking(fullFinalized, source, target, diagnostics);
5552
+ return { points: fullFinalized, diagnostics };
5553
+ }
5554
+ }
5555
+ const gridPath = findObstacleFreePath(
5556
+ source,
5557
+ target,
5558
+ allObstacles,
5559
+ { endpointObstacles, margin: 0 },
5560
+ diagnostics
5561
+ );
5562
+ if (gridPath !== null && gridPath.length >= 2) {
5563
+ const gridFinalized = finalizeRoute(
5564
+ gridPath,
5565
+ softObstacles,
5566
+ hardObstacles,
5567
+ diagnostics,
5568
+ softObstacleIndex,
5569
+ hardObstacleIndex
5570
+ );
5571
+ if (!routeIntersectsObstacles(
5572
+ gridFinalized,
5573
+ softObstacles,
5574
+ softObstacleIndex
5575
+ ) && !routeIntersectsObstacles(
5576
+ gridFinalized,
5577
+ hardObstacles,
5578
+ hardObstacleIndex
5579
+ )) {
5580
+ checkBacktracking(gridFinalized, source, target, diagnostics);
5581
+ return { points: gridFinalized, diagnostics };
5582
+ }
5583
+ }
5584
+ }
5435
5585
  }
5436
5586
  }
5437
5587
  }
@@ -6532,7 +6682,7 @@ function solveDiagram(diagram, options = {}) {
6532
6682
  edges: styledEdges
6533
6683
  });
6534
6684
  diagnostics.push(...layout2.diagnostics);
6535
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
6685
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
6536
6686
  layout2.boxes,
6537
6687
  styledNodes,
6538
6688
  styledEdges,
@@ -6540,7 +6690,8 @@ function solveDiagram(diagram, options = {}) {
6540
6690
  options,
6541
6691
  diagnostics
6542
6692
  );
6543
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6693
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
6694
+ const diagCountBefore = diagnostics.length;
6544
6695
  const rewrapped = wrapHorizontalStackIfNeeded(
6545
6696
  initialNodeBoxes,
6546
6697
  styledNodes,
@@ -6551,6 +6702,20 @@ function solveDiagram(diagram, options = {}) {
6551
6702
  for (const [id, box] of rewrapped) {
6552
6703
  initialNodeBoxes.set(id, box);
6553
6704
  }
6705
+ if (diagnostics.length > diagCountBefore) {
6706
+ for (const node of styledNodes) {
6707
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
6708
+ const rwBox = rewrapped.get(node.id);
6709
+ const idx = styledNodes.indexOf(node);
6710
+ if (idx !== -1) {
6711
+ styledNodes[idx] = {
6712
+ ...node,
6713
+ position: { x: rwBox.x, y: rwBox.y }
6714
+ };
6715
+ }
6716
+ }
6717
+ }
6718
+ }
6554
6719
  }
6555
6720
  if (useRecursive && "groupBoxes" in layout2) {
6556
6721
  const recursiveLayout = layout2;
@@ -6564,7 +6729,7 @@ function solveDiagram(diagram, options = {}) {
6564
6729
  overlapSpacing: options?.overlapSpacing ?? 40,
6565
6730
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
6566
6731
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6567
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6732
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
6568
6733
  swimlanes: styledSwimlanes,
6569
6734
  boxes: initialNodeBoxes,
6570
6735
  nodes: styledNodes,
@@ -7283,9 +7448,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
7283
7448
  if (!isStackRunaway(boxes, nodes, direction, options)) {
7284
7449
  return new Map(boxes);
7285
7450
  }
7286
- const maxRowDepth = options.maxRowDepth;
7287
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7288
- return new Map(boxes);
7451
+ let maxRowDepth = options.maxRowDepth;
7452
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
7453
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
7454
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
7455
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
7456
+ } else {
7457
+ return new Map(boxes);
7458
+ }
7289
7459
  }
7290
7460
  const ordered = [...nodes].sort((a, b) => {
7291
7461
  const ba = boxes.get(a.id);
@@ -7346,10 +7516,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
7346
7516
  });
7347
7517
  }
7348
7518
  function isStackRunaway(boxes, nodes, direction, options) {
7349
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
7519
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
7350
7520
  return false;
7351
7521
  }
7352
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
7522
+ if (nodes.length < 2) {
7353
7523
  return false;
7354
7524
  }
7355
7525
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -7357,11 +7527,18 @@ function isStackRunaway(boxes, nodes, direction, options) {
7357
7527
  return false;
7358
7528
  }
7359
7529
  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) {
7530
+ const isHorizontal = direction === "TB" || direction === "BT";
7531
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
7532
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
7533
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
7363
7534
  return false;
7364
7535
  }
7536
+ if (isHorizontal) {
7537
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
7538
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
7539
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
7540
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
7541
+ }
7365
7542
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
7366
7543
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
7367
7544
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
@@ -7425,7 +7602,17 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7425
7602
  );
7426
7603
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
7427
7604
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
7428
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
7605
+ const spreadWidth = maxCrossAxisSpreadWidth(
7606
+ swimlane,
7607
+ nodeBoxes,
7608
+ flowRanks,
7609
+ locks,
7610
+ rankStackGap
7611
+ );
7612
+ const slotWidth = Math.max(
7613
+ Math.max(...populatedBounds.map((box) => box.width)),
7614
+ spreadWidth
7615
+ ) + padding * 2;
7429
7616
  const laneStep = slotWidth + laneGutter;
7430
7617
  const laneContentTop = top + headerHeight + padding;
7431
7618
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -7439,6 +7626,24 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7439
7626
  y: laneContentTop
7440
7627
  };
7441
7628
  if (maxRank === 0) {
7629
+ const distributable = lane.children.filter(
7630
+ (childId) => !locks.has(childId)
7631
+ );
7632
+ if (distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
7633
+ moveRankedVerticalLaneChildren(
7634
+ lane.children,
7635
+ nodeBoxes,
7636
+ locks,
7637
+ diagnostics,
7638
+ movedChildIds,
7639
+ flowRanks,
7640
+ rankSpacing,
7641
+ rankStackGap,
7642
+ { x: target.x, y: laneContentTop },
7643
+ slotWidth - padding * 2
7644
+ );
7645
+ continue;
7646
+ }
7442
7647
  moveLaneChildren(
7443
7648
  lane.children,
7444
7649
  nodeBoxes,
@@ -7461,10 +7666,8 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
7461
7666
  flowRanks,
7462
7667
  rankSpacing,
7463
7668
  rankStackGap,
7464
- {
7465
- x: target.x - bounds.x,
7466
- y: laneContentTop
7467
- }
7669
+ { x: target.x, y: laneContentTop },
7670
+ slotWidth - padding * 2
7468
7671
  );
7469
7672
  }
7470
7673
  return {
@@ -7573,31 +7776,99 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
7573
7776
  }
7574
7777
  return maxHeight;
7575
7778
  }
7576
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7779
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7780
+ function crossAxisSpreadWidth(items, gap) {
7781
+ return items.reduce(
7782
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7783
+ 0
7784
+ );
7785
+ }
7786
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap) {
7787
+ let maxWidth = 0;
7788
+ for (const lane of swimlane.lanes) {
7789
+ for (const stack of rankStacks(
7790
+ lane.children,
7791
+ nodeBoxes,
7792
+ flowRanks
7793
+ ).values()) {
7794
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7795
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7796
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7797
+ }
7798
+ }
7799
+ return maxWidth;
7800
+ }
7801
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth) {
7577
7802
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
7578
- let yOffset = 0;
7803
+ const unlocked = [];
7579
7804
  for (const item of stack) {
7580
- const { childId, box } = item;
7581
- if (locks.has(childId)) {
7805
+ if (locks.has(item.childId)) {
7582
7806
  diagnostics.push({
7583
7807
  severity: "warning",
7584
7808
  code: "constraints.locked-target-not-moved",
7585
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7809
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
7586
7810
  path: ["swimlanes"],
7587
- detail: { nodeId: childId }
7811
+ detail: { nodeId: item.childId }
7588
7812
  });
7589
- continue;
7813
+ } else {
7814
+ unlocked.push(item);
7590
7815
  }
7816
+ }
7817
+ if (unlocked.length === 0) continue;
7818
+ if (unlocked.length === 1) {
7819
+ const { childId, box } = unlocked[0];
7591
7820
  const next = {
7592
7821
  ...box,
7593
- x: box.x + target.x,
7594
- y: target.y + rank * rankSpacing + yOffset
7822
+ x: target.x + (contentWidth - box.width) / 2,
7823
+ y: target.y + rank * rankSpacing
7595
7824
  };
7596
7825
  if (next.x !== box.x || next.y !== box.y) {
7597
7826
  movedChildIds.add(childId);
7598
7827
  }
7599
7828
  nodeBoxes.set(childId, next);
7600
- yOffset += box.height + rankStackGap;
7829
+ } else {
7830
+ const shouldSpread = unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7831
+ if (!shouldSpread) {
7832
+ let yOffset = 0;
7833
+ for (const { childId, box } of unlocked) {
7834
+ const next = {
7835
+ ...box,
7836
+ x: target.x + (contentWidth - box.width) / 2,
7837
+ y: target.y + rank * rankSpacing + yOffset
7838
+ };
7839
+ if (next.x !== box.x || next.y !== box.y) {
7840
+ movedChildIds.add(childId);
7841
+ }
7842
+ nodeBoxes.set(childId, next);
7843
+ yOffset += box.height + rankStackGap;
7844
+ }
7845
+ } else {
7846
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
7847
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
7848
+ for (const { childId, box } of unlocked) {
7849
+ const next = {
7850
+ ...box,
7851
+ x: xCursor,
7852
+ y: target.y + rank * rankSpacing
7853
+ };
7854
+ if (next.x !== box.x || next.y !== box.y) {
7855
+ movedChildIds.add(childId);
7856
+ }
7857
+ nodeBoxes.set(childId, next);
7858
+ xCursor += box.width + rankStackGap;
7859
+ }
7860
+ diagnostics.push({
7861
+ severity: "info",
7862
+ code: "swimlane_contract.cross_axis_distributed",
7863
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7864
+ path: ["swimlanes"],
7865
+ detail: {
7866
+ rank,
7867
+ childCount: unlocked.length,
7868
+ contentWidth
7869
+ }
7870
+ });
7871
+ }
7601
7872
  }
7602
7873
  }
7603
7874
  }
@@ -7936,7 +8207,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7936
8207
  });
7937
8208
  continue;
7938
8209
  }
7939
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
8210
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7940
8211
  const geometry = computeShapeGeometry({
7941
8212
  shape: node.shape,
7942
8213
  box,
@@ -8030,7 +8301,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
8030
8301
  }
8031
8302
  }
8032
8303
  }
8033
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8304
+ function coordinatePorts(node, nodeBox, portShifting) {
8034
8305
  const portsBySide = /* @__PURE__ */ new Map();
8035
8306
  for (const port of node.ports ?? []) {
8036
8307
  const ports = portsBySide.get(port.side) ?? [];
@@ -8053,9 +8324,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8053
8324
  side,
8054
8325
  index,
8055
8326
  sorted.length,
8056
- portShifting,
8057
- diagnostics,
8058
- node.id
8327
+ portShifting
8059
8328
  );
8060
8329
  const box = portBox(anchor);
8061
8330
  coordinated.push({ ...port, box, anchor });
@@ -8063,32 +8332,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
8063
8332
  }
8064
8333
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
8065
8334
  }
8066
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
8335
+ function portAnchor(nodeBox, side, index, count, portShifting) {
8067
8336
  const shiftingEnabled = portShifting?.enabled ?? true;
8068
8337
  const requestedSpacing = portShifting?.spacing ?? 24;
8069
8338
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
8070
8339
  const availableSpan = 2 * maxOffset;
8071
8340
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
8072
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
8341
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
8073
8342
  Math.min(requestedSpacing, availableSpan / (count - 1)),
8074
8343
  minSpacing
8075
8344
  ) : 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
8345
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
8093
8346
  switch (side) {
8094
8347
  case "left":