@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/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({
@@ -4666,7 +4737,7 @@ function segmentCrossesAnyObstacle(a, b, obstacles) {
4666
4737
  }
4667
4738
 
4668
4739
  // src/routing/routes.ts
4669
- function checkBacktracking(points, source, target, diagnostics) {
4740
+ function checkBacktracking(points, source, target, diagnostics, maxRatio) {
4670
4741
  if (points.length < 2) return;
4671
4742
  const direct = Math.hypot(target.x - source.x, target.y - source.y);
4672
4743
  if (direct <= 0) return;
@@ -4676,7 +4747,7 @@ function checkBacktracking(points, source, target, diagnostics) {
4676
4747
  const b = points[i + 1];
4677
4748
  routeLen += Math.hypot(b.x - a.x, b.y - a.y);
4678
4749
  }
4679
- const threshold = 10;
4750
+ const threshold = maxRatio ?? 20;
4680
4751
  if (routeLen > direct * threshold) {
4681
4752
  diagnostics.push({
4682
4753
  severity: "warning",
@@ -4694,8 +4765,20 @@ function routeEdge(input) {
4694
4765
  const diagnostics = [];
4695
4766
  const softObstacles = input.obstacles ?? [];
4696
4767
  const hardObstacles = input.hardObstacles ?? [];
4768
+ let bestRejectedPath;
4769
+ let bestRejectedCrossings = Number.POSITIVE_INFINITY;
4697
4770
  const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
4698
4771
  const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
4772
+ const recordRejected = (candidate) => {
4773
+ if (routeIntersectsObstacles(candidate, hardObstacles, hardObstacleIndex)) {
4774
+ return;
4775
+ }
4776
+ const crossings = countObstacleCrossings(candidate, softObstacles);
4777
+ if (crossings < bestRejectedCrossings) {
4778
+ bestRejectedCrossings = crossings;
4779
+ bestRejectedPath = candidate;
4780
+ }
4781
+ };
4699
4782
  const maxAttempts = input.maxRoutingAttempts ?? 5;
4700
4783
  const defaultAnchors = defaultAnchorsForGeometry(
4701
4784
  input.source.box,
@@ -4754,18 +4837,38 @@ function routeEdge(input) {
4754
4837
  input.source.center,
4755
4838
  targetAnchor
4756
4839
  );
4757
- const cornerPath = findCornerGraphPath(
4840
+ const allObstacles = [...softObstacles, ...hardObstacles];
4841
+ const corridorMargin = input.corridorMargin ?? 32;
4842
+ const corridorObstacles = filterObstaclesByCorridor(
4843
+ source,
4844
+ target,
4845
+ allObstacles,
4846
+ [],
4847
+ // endpointObstacles passed separately via options
4848
+ corridorMargin
4849
+ );
4850
+ const cornerObstacles = corridorObstacles.length === 0 && allObstacles.length > 0 ? allObstacles : corridorObstacles;
4851
+ let cornerPath = findCornerGraphPath(
4758
4852
  source,
4759
4853
  target,
4760
- [...softObstacles, ...hardObstacles],
4854
+ cornerObstacles,
4761
4855
  { endpointObstacles, margin: 2 },
4762
4856
  diagnostics
4763
4857
  );
4858
+ if (cornerPath === null && cornerObstacles.length < allObstacles.length) {
4859
+ cornerPath = findCornerGraphPath(
4860
+ source,
4861
+ target,
4862
+ allObstacles,
4863
+ { endpointObstacles, margin: 2 },
4864
+ diagnostics
4865
+ );
4866
+ }
4764
4867
  const path = cornerPath ?? findObstacleFreePath(
4765
4868
  source,
4766
4869
  target,
4767
- [...softObstacles, ...hardObstacles],
4768
- { endpointObstacles, margin: 0 },
4870
+ allObstacles,
4871
+ { endpointObstacles, margin: 0, corridorMargin },
4769
4872
  diagnostics
4770
4873
  );
4771
4874
  if (path !== null && path.length >= 2) {
@@ -4782,9 +4885,90 @@ function routeEdge(input) {
4782
4885
  softObstacles,
4783
4886
  softObstacleIndex
4784
4887
  ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4785
- checkBacktracking(finalized, source, target, diagnostics);
4888
+ checkBacktracking(
4889
+ finalized,
4890
+ source,
4891
+ target,
4892
+ diagnostics,
4893
+ input.maxBacktrackingRatio
4894
+ );
4786
4895
  return { points: finalized, diagnostics };
4787
4896
  }
4897
+ recordRejected(finalized);
4898
+ if (cornerPath !== null) {
4899
+ const fullCornerPath = cornerObstacles.length < allObstacles.length ? findCornerGraphPath(
4900
+ source,
4901
+ target,
4902
+ allObstacles,
4903
+ { endpointObstacles, margin: 2 },
4904
+ diagnostics
4905
+ ) : null;
4906
+ if (fullCornerPath !== null && fullCornerPath.length >= 2) {
4907
+ const fullFinalized = finalizeRoute(
4908
+ fullCornerPath,
4909
+ softObstacles,
4910
+ hardObstacles,
4911
+ diagnostics,
4912
+ softObstacleIndex,
4913
+ hardObstacleIndex
4914
+ );
4915
+ if (!routeIntersectsObstacles(
4916
+ fullFinalized,
4917
+ softObstacles,
4918
+ softObstacleIndex
4919
+ ) && !routeIntersectsObstacles(
4920
+ fullFinalized,
4921
+ hardObstacles,
4922
+ hardObstacleIndex
4923
+ )) {
4924
+ checkBacktracking(
4925
+ fullFinalized,
4926
+ source,
4927
+ target,
4928
+ diagnostics,
4929
+ input.maxBacktrackingRatio
4930
+ );
4931
+ return { points: fullFinalized, diagnostics };
4932
+ }
4933
+ recordRejected(fullFinalized);
4934
+ }
4935
+ const gridPath = findObstacleFreePath(
4936
+ source,
4937
+ target,
4938
+ allObstacles,
4939
+ { endpointObstacles, margin: 0, corridorMargin },
4940
+ diagnostics
4941
+ );
4942
+ if (gridPath !== null && gridPath.length >= 2) {
4943
+ const gridFinalized = finalizeRoute(
4944
+ gridPath,
4945
+ softObstacles,
4946
+ hardObstacles,
4947
+ diagnostics,
4948
+ softObstacleIndex,
4949
+ hardObstacleIndex
4950
+ );
4951
+ if (!routeIntersectsObstacles(
4952
+ gridFinalized,
4953
+ softObstacles,
4954
+ softObstacleIndex
4955
+ ) && !routeIntersectsObstacles(
4956
+ gridFinalized,
4957
+ hardObstacles,
4958
+ hardObstacleIndex
4959
+ )) {
4960
+ checkBacktracking(
4961
+ gridFinalized,
4962
+ source,
4963
+ target,
4964
+ diagnostics,
4965
+ input.maxBacktrackingRatio
4966
+ );
4967
+ return { points: gridFinalized, diagnostics };
4968
+ }
4969
+ recordRejected(gridFinalized);
4970
+ }
4971
+ }
4788
4972
  }
4789
4973
  }
4790
4974
  }
@@ -4846,7 +5030,8 @@ function routeEdge(input) {
4846
5030
  finalizedClean,
4847
5031
  candidate.points[0],
4848
5032
  candidate.points[candidate.points.length - 1],
4849
- diagnostics
5033
+ diagnostics,
5034
+ input.maxBacktrackingRatio
4850
5035
  );
4851
5036
  return { points: finalizedClean, diagnostics };
4852
5037
  }
@@ -4912,13 +5097,41 @@ function routeEdge(input) {
4912
5097
  code: "routing.obstacle.unavoidable",
4913
5098
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
4914
5099
  });
4915
- return {
4916
- points: finalizeRoute(
4917
- bestPoints2,
5100
+ const finalizedSoftBest = finalizeRoute(
5101
+ bestPoints2,
5102
+ softObstacles,
5103
+ hardObstacles,
5104
+ diagnostics
5105
+ );
5106
+ let softFallback = finalizedSoftBest;
5107
+ if (bestRejectedPath !== void 0) {
5108
+ const finalizedRejected = finalizeRoute(
5109
+ bestRejectedPath,
4918
5110
  softObstacles,
4919
5111
  hardObstacles,
4920
5112
  diagnostics
4921
- ),
5113
+ );
5114
+ const rejectedCrossings = countObstacleCrossings(
5115
+ finalizedRejected,
5116
+ softObstacles
5117
+ );
5118
+ const heuristicCrossings = countObstacleCrossings(
5119
+ finalizedSoftBest,
5120
+ softObstacles
5121
+ );
5122
+ if (rejectedCrossings < heuristicCrossings) {
5123
+ softFallback = finalizedRejected;
5124
+ }
5125
+ }
5126
+ checkBacktracking(
5127
+ softFallback,
5128
+ softFallback[0],
5129
+ softFallback[softFallback.length - 1],
5130
+ diagnostics,
5131
+ input.maxBacktrackingRatio
5132
+ );
5133
+ return {
5134
+ points: softFallback,
4922
5135
  diagnostics
4923
5136
  };
4924
5137
  }
@@ -4950,6 +5163,22 @@ function routeEdge(input) {
4950
5163
  maxAttempts
4951
5164
  );
4952
5165
  }
5166
+ if (bestRejectedPath !== void 0) {
5167
+ diagnostics.push({
5168
+ severity: "warning",
5169
+ code: "routing.obstacle.unavoidable",
5170
+ message: "Using A* route with minor soft-obstacle crossings to avoid hard evidence obstacles."
5171
+ });
5172
+ return {
5173
+ points: finalizeRoute(
5174
+ bestRejectedPath,
5175
+ softObstacles,
5176
+ hardObstacles,
5177
+ diagnostics
5178
+ ),
5179
+ diagnostics
5180
+ };
5181
+ }
4953
5182
  diagnostics.push({
4954
5183
  severity: "error",
4955
5184
  code: "routing.evidence.crossing_forbidden",
@@ -4997,13 +5226,41 @@ function routeEdge(input) {
4997
5226
  code: "routing.obstacle.unavoidable",
4998
5227
  message: "No bounded orthogonal route candidate avoided all obstacles."
4999
5228
  });
5000
- return {
5001
- points: finalizeRoute(
5002
- bestPoints,
5229
+ const finalizedBestPoints = finalizeRoute(
5230
+ bestPoints,
5231
+ softObstacles,
5232
+ hardObstacles,
5233
+ diagnostics
5234
+ );
5235
+ let fallbackPoints = finalizedBestPoints;
5236
+ if (bestRejectedPath !== void 0) {
5237
+ const finalizedRejected = finalizeRoute(
5238
+ bestRejectedPath,
5003
5239
  softObstacles,
5004
5240
  hardObstacles,
5005
5241
  diagnostics
5006
- ),
5242
+ );
5243
+ const rejectedCrossings = countObstacleCrossings(
5244
+ finalizedRejected,
5245
+ softObstacles
5246
+ );
5247
+ const heuristicCrossings = countObstacleCrossings(
5248
+ finalizedBestPoints,
5249
+ softObstacles
5250
+ );
5251
+ if (rejectedCrossings < heuristicCrossings) {
5252
+ fallbackPoints = finalizedRejected;
5253
+ }
5254
+ }
5255
+ checkBacktracking(
5256
+ fallbackPoints,
5257
+ fallbackPoints[0],
5258
+ fallbackPoints[fallbackPoints.length - 1],
5259
+ diagnostics,
5260
+ input.maxBacktrackingRatio
5261
+ );
5262
+ return {
5263
+ points: fallbackPoints,
5007
5264
  diagnostics
5008
5265
  };
5009
5266
  }
@@ -5502,6 +5759,24 @@ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
5502
5759
  }
5503
5760
  return false;
5504
5761
  }
5762
+ function countObstacleCrossings(points, obstacles) {
5763
+ let count = 0;
5764
+ for (const obstacle of obstacles) {
5765
+ validateBox(obstacle);
5766
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
5767
+ const a = points[pointIndex];
5768
+ const b = points[pointIndex + 1];
5769
+ if (a === void 0 || b === void 0) {
5770
+ continue;
5771
+ }
5772
+ if (intersectsAabb(segmentBox2(a, b), obstacle)) {
5773
+ count += 1;
5774
+ break;
5775
+ }
5776
+ }
5777
+ }
5778
+ return count;
5779
+ }
5505
5780
  function routeIntersectsEndpointInteriors(points, endpointInteriors) {
5506
5781
  for (let index = 0; index < points.length - 1; index += 1) {
5507
5782
  const a = points[index];
@@ -5674,7 +5949,7 @@ function solveDiagram(diagram, options = {}) {
5674
5949
  edges: styledEdges
5675
5950
  });
5676
5951
  diagnostics.push(...layout2.diagnostics);
5677
- const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
5952
+ const initialNodeBoxes = initialLayoutMode === "positions" || diagram.direction !== "LR" && diagram.direction !== "RL" ? layout2.boxes : wrapVerticalStackIfNeeded(
5678
5953
  layout2.boxes,
5679
5954
  styledNodes,
5680
5955
  styledEdges,
@@ -5682,7 +5957,8 @@ function solveDiagram(diagram, options = {}) {
5682
5957
  options,
5683
5958
  diagnostics
5684
5959
  );
5685
- if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
5960
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && (options.maxRowDepth !== void 0 || options.targetAspectRatio !== void 0)) {
5961
+ const diagCountBefore = diagnostics.length;
5686
5962
  const rewrapped = wrapHorizontalStackIfNeeded(
5687
5963
  initialNodeBoxes,
5688
5964
  styledNodes,
@@ -5693,6 +5969,20 @@ function solveDiagram(diagram, options = {}) {
5693
5969
  for (const [id, box] of rewrapped) {
5694
5970
  initialNodeBoxes.set(id, box);
5695
5971
  }
5972
+ if (diagnostics.length > diagCountBefore) {
5973
+ for (const node of styledNodes) {
5974
+ if (node.position !== void 0 && rewrapped.has(node.id)) {
5975
+ const rwBox = rewrapped.get(node.id);
5976
+ const idx = styledNodes.indexOf(node);
5977
+ if (idx !== -1) {
5978
+ styledNodes[idx] = {
5979
+ ...node,
5980
+ position: { x: rwBox.x, y: rwBox.y }
5981
+ };
5982
+ }
5983
+ }
5984
+ }
5985
+ }
5696
5986
  }
5697
5987
  if (useRecursive && "groupBoxes" in layout2) {
5698
5988
  const recursiveLayout = layout2;
@@ -5706,7 +5996,7 @@ function solveDiagram(diagram, options = {}) {
5706
5996
  overlapSpacing: options?.overlapSpacing ?? 40,
5707
5997
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5708
5998
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5709
- distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
5999
+ ...options.distributeSwimlaneChildren !== void 0 ? { distributeSwimlaneChildren: options.distributeSwimlaneChildren } : {},
5710
6000
  swimlanes: styledSwimlanes,
5711
6001
  boxes: initialNodeBoxes,
5712
6002
  nodes: styledNodes,
@@ -5721,7 +6011,8 @@ function solveDiagram(diagram, options = {}) {
5721
6011
  constrained.boxes,
5722
6012
  constrained.locks,
5723
6013
  options?.overlapSpacing ?? 40,
5724
- Math.max(0, options?.minLaneGutter ?? 0)
6014
+ Math.max(0, options?.minLaneGutter ?? 0),
6015
+ options.distributeContainedChildren ?? true
5725
6016
  );
5726
6017
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
5727
6018
  diagnostics.push(...swimlaneContracts.diagnostics);
@@ -5888,7 +6179,8 @@ function solveDiagram(diagram, options = {}) {
5888
6179
  diagram.direction,
5889
6180
  options,
5890
6181
  diagnostics,
5891
- coordinatedGroups
6182
+ coordinatedGroups,
6183
+ contentBounds
5892
6184
  );
5893
6185
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5894
6186
  coordinatedEdges,
@@ -6307,7 +6599,7 @@ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnos
6307
6599
  function containsCjk(value) {
6308
6600
  return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
6309
6601
  }
6310
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
6602
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter, distributeContainedChildren) {
6311
6603
  const layouts = /* @__PURE__ */ new Map();
6312
6604
  const diagnostics = [];
6313
6605
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -6326,7 +6618,9 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
6326
6618
  locks,
6327
6619
  diagnostics,
6328
6620
  movedChildIds,
6329
- laneGutter
6621
+ laneGutter,
6622
+ constraints,
6623
+ distributeContainedChildren
6330
6624
  );
6331
6625
  if (layout2 !== void 0) {
6332
6626
  layouts.set(swimlane.id, layout2);
@@ -6422,9 +6716,14 @@ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnosti
6422
6716
  if (!isStackRunaway(boxes, nodes, direction, options)) {
6423
6717
  return new Map(boxes);
6424
6718
  }
6425
- const maxRowDepth = options.maxRowDepth;
6426
- if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6427
- return new Map(boxes);
6719
+ let maxRowDepth = options.maxRowDepth;
6720
+ if (maxRowDepth === void 0 || maxRowDepth <= 0 || nodes.length <= maxRowDepth) {
6721
+ if (maxRowDepth === void 0 && options.targetAspectRatio !== void 0) {
6722
+ maxRowDepth = Math.ceil(Math.sqrt(nodes.length));
6723
+ if (nodes.length <= maxRowDepth) return new Map(boxes);
6724
+ } else {
6725
+ return new Map(boxes);
6726
+ }
6428
6727
  }
6429
6728
  const ordered = [...nodes].sort((a, b) => {
6430
6729
  const ba = boxes.get(a.id);
@@ -6485,10 +6784,10 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6485
6784
  });
6486
6785
  }
6487
6786
  function isStackRunaway(boxes, nodes, direction, options) {
6488
- if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6787
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0 && options.targetAspectRatio === void 0 && options.maxRowDepth === void 0) {
6489
6788
  return false;
6490
6789
  }
6491
- if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
6790
+ if (nodes.length < 2) {
6492
6791
  return false;
6493
6792
  }
6494
6793
  const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
@@ -6496,17 +6795,24 @@ function isStackRunaway(boxes, nodes, direction, options) {
6496
6795
  return false;
6497
6796
  }
6498
6797
  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) {
6798
+ const isHorizontal = direction === "TB" || direction === "BT";
6799
+ const aspectRatio = isHorizontal ? bounds.height <= 0 ? Number.POSITIVE_INFINITY : bounds.width / bounds.height : bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
6800
+ const preferred = isHorizontal ? options.targetAspectRatio ?? options.preferredAspectRatio ?? 3 : options.preferredAspectRatio ?? 3;
6801
+ if ((options.preferredAspectRatio !== void 0 || options.targetAspectRatio !== void 0) && aspectRatio < preferred) {
6502
6802
  return false;
6503
6803
  }
6804
+ if (isHorizontal) {
6805
+ const yCenters = nodeBoxes.map((box) => box.y + box.height / 2);
6806
+ const ySpread = Math.max(...yCenters) - Math.min(...yCenters);
6807
+ const maxHeight = Math.max(...nodeBoxes.map((box) => box.height));
6808
+ return ySpread <= Math.max(maxHeight, options.overlapSpacing ?? 40);
6809
+ }
6504
6810
  const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
6505
6811
  const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
6506
6812
  const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
6507
6813
  return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
6508
6814
  }
6509
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
6815
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6510
6816
  const headerHeight = swimlane.headerHeight ?? 28;
6511
6817
  const padding = swimlane.padding ?? 16;
6512
6818
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -6531,7 +6837,9 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6531
6837
  locks,
6532
6838
  diagnostics,
6533
6839
  movedChildIds,
6534
- laneGutter
6840
+ laneGutter,
6841
+ constraints,
6842
+ distributeContainedChildren
6535
6843
  );
6536
6844
  }
6537
6845
  return applyHorizontalSwimlaneContract(
@@ -6546,13 +6854,29 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
6546
6854
  laneGutter
6547
6855
  );
6548
6856
  }
6549
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
6857
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter, constraints, distributeContainedChildren) {
6550
6858
  const populatedBounds = laneBounds.filter(
6551
6859
  (box) => box !== void 0
6552
6860
  );
6553
6861
  const top = Math.min(...populatedBounds.map((box) => box.y));
6554
6862
  const left = Math.min(...populatedBounds.map((box) => box.x));
6555
6863
  const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
6864
+ const containedChildIds = /* @__PURE__ */ new Set();
6865
+ if (distributeContainedChildren) {
6866
+ for (const c of constraints) {
6867
+ if (c.kind !== "containment") continue;
6868
+ if (nodeBoxes.get(c.containerId) === void 0) continue;
6869
+ const distributable = c.childIds.filter((childId) => {
6870
+ if (nodeBoxes.get(childId) === void 0) return false;
6871
+ const lock = locks.get(childId);
6872
+ return lock === void 0 || lock.source === "fixed-position";
6873
+ });
6874
+ if (distributable.length < 2) continue;
6875
+ for (const childId of distributable) {
6876
+ containedChildIds.add(childId);
6877
+ }
6878
+ }
6879
+ }
6556
6880
  const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
6557
6881
  const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
6558
6882
  const rankStackGap = Math.max(8, padding / 2);
@@ -6564,7 +6888,18 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6564
6888
  );
6565
6889
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
6566
6890
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
6567
- const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
6891
+ const spreadWidth = maxCrossAxisSpreadWidth(
6892
+ swimlane,
6893
+ nodeBoxes,
6894
+ flowRanks,
6895
+ locks,
6896
+ rankStackGap,
6897
+ containedChildIds
6898
+ );
6899
+ const slotWidth = Math.max(
6900
+ Math.max(...populatedBounds.map((box) => box.width)),
6901
+ spreadWidth
6902
+ ) + padding * 2;
6568
6903
  const laneStep = slotWidth + laneGutter;
6569
6904
  const laneContentTop = top + headerHeight + padding;
6570
6905
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
@@ -6578,6 +6913,27 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6578
6913
  y: laneContentTop
6579
6914
  };
6580
6915
  if (maxRank === 0) {
6916
+ const distributable = lane.children.filter(
6917
+ (childId) => !locks.has(childId)
6918
+ );
6919
+ const coveredByContainment = lane.children.some(
6920
+ (childId) => containedChildIds.has(childId)
6921
+ );
6922
+ if (!coveredByContainment && distributable.length >= CROSS_AXIS_SPREAD_THRESHOLD) {
6923
+ moveRankedVerticalLaneChildren(
6924
+ lane.children,
6925
+ nodeBoxes,
6926
+ locks,
6927
+ diagnostics,
6928
+ movedChildIds,
6929
+ flowRanks,
6930
+ rankSpacing,
6931
+ rankStackGap,
6932
+ { x: target.x, y: laneContentTop },
6933
+ slotWidth - padding * 2
6934
+ );
6935
+ continue;
6936
+ }
6581
6937
  moveLaneChildren(
6582
6938
  lane.children,
6583
6939
  nodeBoxes,
@@ -6591,6 +6947,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6591
6947
  );
6592
6948
  continue;
6593
6949
  }
6950
+ const rankedCoveredByContainment = lane.children.some(
6951
+ (childId) => containedChildIds.has(childId)
6952
+ );
6594
6953
  moveRankedVerticalLaneChildren(
6595
6954
  lane.children,
6596
6955
  nodeBoxes,
@@ -6600,10 +6959,9 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
6600
6959
  flowRanks,
6601
6960
  rankSpacing,
6602
6961
  rankStackGap,
6603
- {
6604
- x: target.x - bounds.x,
6605
- y: laneContentTop
6606
- }
6962
+ { x: target.x, y: laneContentTop },
6963
+ slotWidth - padding * 2,
6964
+ rankedCoveredByContainment
6607
6965
  );
6608
6966
  }
6609
6967
  return {
@@ -6712,31 +7070,102 @@ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
6712
7070
  }
6713
7071
  return maxHeight;
6714
7072
  }
6715
- function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
7073
+ var CROSS_AXIS_SPREAD_THRESHOLD = 3;
7074
+ function crossAxisSpreadWidth(items, gap) {
7075
+ return items.reduce(
7076
+ (sum, item, index) => sum + item.box.width + (index === 0 ? 0 : gap),
7077
+ 0
7078
+ );
7079
+ }
7080
+ function maxCrossAxisSpreadWidth(swimlane, nodeBoxes, flowRanks, locks, gap, containedChildIds) {
7081
+ let maxWidth = 0;
7082
+ for (const lane of swimlane.lanes) {
7083
+ if (containedChildIds !== void 0 && lane.children.some((childId) => containedChildIds.has(childId))) {
7084
+ continue;
7085
+ }
7086
+ for (const stack of rankStacks(
7087
+ lane.children,
7088
+ nodeBoxes,
7089
+ flowRanks
7090
+ ).values()) {
7091
+ const unlocked = stack.filter((item) => !locks.has(item.childId));
7092
+ if (unlocked.length < CROSS_AXIS_SPREAD_THRESHOLD) continue;
7093
+ maxWidth = Math.max(maxWidth, crossAxisSpreadWidth(unlocked, gap));
7094
+ }
7095
+ }
7096
+ return maxWidth;
7097
+ }
7098
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target, contentWidth, suppressSpread) {
6716
7099
  for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
6717
- let yOffset = 0;
7100
+ const unlocked = [];
6718
7101
  for (const item of stack) {
6719
- const { childId, box } = item;
6720
- if (locks.has(childId)) {
7102
+ if (locks.has(item.childId)) {
6721
7103
  diagnostics.push({
6722
7104
  severity: "warning",
6723
7105
  code: "constraints.locked-target-not-moved",
6724
- message: `Locked child ${childId} was not moved into contract swimlane slot.`,
7106
+ message: `Locked child ${item.childId} was not moved into contract swimlane slot.`,
6725
7107
  path: ["swimlanes"],
6726
- detail: { nodeId: childId }
7108
+ detail: { nodeId: item.childId }
6727
7109
  });
6728
- continue;
7110
+ } else {
7111
+ unlocked.push(item);
6729
7112
  }
7113
+ }
7114
+ if (unlocked.length === 0) continue;
7115
+ if (unlocked.length === 1) {
7116
+ const { childId, box } = unlocked[0];
6730
7117
  const next = {
6731
7118
  ...box,
6732
- x: box.x + target.x,
6733
- y: target.y + rank * rankSpacing + yOffset
7119
+ x: target.x + (contentWidth - box.width) / 2,
7120
+ y: target.y + rank * rankSpacing
6734
7121
  };
6735
7122
  if (next.x !== box.x || next.y !== box.y) {
6736
7123
  movedChildIds.add(childId);
6737
7124
  }
6738
7125
  nodeBoxes.set(childId, next);
6739
- yOffset += box.height + rankStackGap;
7126
+ } else {
7127
+ const shouldSpread = !suppressSpread && unlocked.length >= CROSS_AXIS_SPREAD_THRESHOLD;
7128
+ if (!shouldSpread) {
7129
+ let yOffset = 0;
7130
+ for (const { childId, box } of unlocked) {
7131
+ const next = {
7132
+ ...box,
7133
+ x: target.x + (contentWidth - box.width) / 2,
7134
+ y: target.y + rank * rankSpacing + yOffset
7135
+ };
7136
+ if (next.x !== box.x || next.y !== box.y) {
7137
+ movedChildIds.add(childId);
7138
+ }
7139
+ nodeBoxes.set(childId, next);
7140
+ yOffset += box.height + rankStackGap;
7141
+ }
7142
+ } else {
7143
+ const packedWidth = crossAxisSpreadWidth(unlocked, rankStackGap);
7144
+ let xCursor = target.x + Math.max(0, (contentWidth - packedWidth) / 2);
7145
+ for (const { childId, box } of unlocked) {
7146
+ const next = {
7147
+ ...box,
7148
+ x: xCursor,
7149
+ y: target.y + rank * rankSpacing
7150
+ };
7151
+ if (next.x !== box.x || next.y !== box.y) {
7152
+ movedChildIds.add(childId);
7153
+ }
7154
+ nodeBoxes.set(childId, next);
7155
+ xCursor += box.width + rankStackGap;
7156
+ }
7157
+ diagnostics.push({
7158
+ severity: "info",
7159
+ code: "swimlane_contract.cross_axis_distributed",
7160
+ message: `Spread ${unlocked.length} same-rank children horizontally in contract lane (rank ${rank}).`,
7161
+ path: ["swimlanes"],
7162
+ detail: {
7163
+ rank,
7164
+ childCount: unlocked.length,
7165
+ contentWidth
7166
+ }
7167
+ });
7168
+ }
6740
7169
  }
6741
7170
  }
6742
7171
  }
@@ -7075,7 +7504,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7075
7504
  });
7076
7505
  continue;
7077
7506
  }
7078
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7507
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7079
7508
  const geometry = computeShapeGeometry({
7080
7509
  shape: node.shape,
7081
7510
  box,
@@ -7169,7 +7598,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7169
7598
  }
7170
7599
  }
7171
7600
  }
7172
- function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7601
+ function coordinatePorts(node, nodeBox, portShifting) {
7173
7602
  const portsBySide = /* @__PURE__ */ new Map();
7174
7603
  for (const port of node.ports ?? []) {
7175
7604
  const ports = portsBySide.get(port.side) ?? [];
@@ -7192,9 +7621,7 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7192
7621
  side,
7193
7622
  index,
7194
7623
  sorted.length,
7195
- portShifting,
7196
- diagnostics,
7197
- node.id
7624
+ portShifting
7198
7625
  );
7199
7626
  const box = portBox(anchor);
7200
7627
  coordinated.push({ ...port, box, anchor });
@@ -7202,32 +7629,16 @@ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7202
7629
  }
7203
7630
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7204
7631
  }
7205
- function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7632
+ function portAnchor(nodeBox, side, index, count, portShifting) {
7206
7633
  const shiftingEnabled = portShifting?.enabled ?? true;
7207
7634
  const requestedSpacing = portShifting?.spacing ?? 24;
7208
7635
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7209
7636
  const availableSpan = 2 * maxOffset;
7210
7637
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7211
- const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7638
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
7212
7639
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7213
7640
  minSpacing
7214
7641
  ) : 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
7642
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7232
7643
  switch (side) {
7233
7644
  case "left":
@@ -7842,14 +8253,21 @@ function evidenceOverlapDiagnostic(block, conflict) {
7842
8253
  }
7843
8254
  };
7844
8255
  }
7845
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
8256
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups, contentBounds) {
7846
8257
  const coordinated = [];
7847
8258
  const coordinatedNodeById = new Map(
7848
8259
  coordinatedNodes.map((node) => [node.id, node])
7849
8260
  );
8261
+ const corridorMarginOption = options.corridorMargin ?? "auto";
8262
+ const corridorMargin = typeof corridorMarginOption === "number" ? corridorMarginOption : Math.max(
8263
+ 200,
8264
+ Math.hypot(contentBounds.width, contentBounds.height) * 0.3
8265
+ );
8266
+ const routingGutter = options.routingGutter ?? 160;
8267
+ const queryGutter = (options.routeKind ?? "orthogonal") === "obstacle-avoiding" ? Math.max(routingGutter, corridorMargin) : routingGutter;
7850
8268
  const nodeObstacleIndex = createBoxSpatialIndex(
7851
8269
  obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
7852
- options.routingGutter ?? 160
8270
+ queryGutter
7853
8271
  );
7854
8272
  for (const edge of edges) {
7855
8273
  const source = nodes.get(edge.source.nodeId);
@@ -7871,11 +8289,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7871
8289
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
7872
8290
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
7873
8291
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
7874
- const corridor = edgeCorridorBox(
7875
- source.box,
7876
- target.box,
7877
- options.routingGutter ?? 160
7878
- );
8292
+ const corridor = edgeCorridorBox(source.box, target.box, queryGutter);
7879
8293
  const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7880
8294
  (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7881
8295
  );
@@ -7893,7 +8307,9 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
7893
8307
  ...routeTextObstacles
7894
8308
  ],
7895
8309
  hardObstacles,
7896
- ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
8310
+ corridorMargin,
8311
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts },
8312
+ ...options.maxBacktrackingRatio === void 0 ? {} : { maxBacktrackingRatio: options.maxBacktrackingRatio }
7897
8313
  });
7898
8314
  diagnostics.push(
7899
8315
  ...route.diagnostics.map((diagnostic) => ({