@crazyhappyone/auto-graph 0.2.5 → 0.2.6

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
@@ -1037,6 +1037,69 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1037
1037
  }
1038
1038
  });
1039
1039
  }
1040
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
1041
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
1042
+ }
1043
+ }
1044
+ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
1045
+ const spread = input.distributeSwimlaneChildren === "spread";
1046
+ const minGap = input.minSiblingGap ?? 8;
1047
+ for (const swimlane of input.swimlanes) {
1048
+ if (swimlane.layout === "contract") continue;
1049
+ const isVertical = swimlane.orientation !== "horizontal";
1050
+ const axis = isVertical ? "x" : "y";
1051
+ const mainSize = isVertical ? "width" : "height";
1052
+ for (const lane of swimlane.lanes) {
1053
+ if (lane.children.length < 2) continue;
1054
+ const unlocked = [];
1055
+ const reserved = [];
1056
+ for (const childId of lane.children) {
1057
+ const box = boxes.get(childId);
1058
+ if (box === void 0) continue;
1059
+ if (locks.has(childId)) {
1060
+ const lock = locks.get(childId);
1061
+ if (lock.source === "fixed-position") {
1062
+ unlocked.push({ id: childId, box });
1063
+ continue;
1064
+ }
1065
+ reserved.push(intervalForBox(box, axis, mainSize));
1066
+ continue;
1067
+ }
1068
+ unlocked.push({ id: childId, box });
1069
+ }
1070
+ if (unlocked.length < 2) continue;
1071
+ const contentStart = isVertical ? Math.min(...unlocked.map((c) => c.box.x)) : Math.min(...unlocked.map((c) => c.box.y));
1072
+ const contentEnd = isVertical ? Math.max(...unlocked.map((c) => c.box.x + c.box.width)) : Math.max(...unlocked.map((c) => c.box.y + c.box.height));
1073
+ const contentSpan = contentEnd - contentStart;
1074
+ const totalChildSpan = unlocked.reduce((s, c) => s + c.box[mainSize], 0);
1075
+ const reservedSpan = reserved.reduce((s, r) => s + (r.end - r.start), 0);
1076
+ let effectiveGap = minGap;
1077
+ const remaining = contentSpan - totalChildSpan - reservedSpan - minGap * (unlocked.length - 1);
1078
+ if (spread && remaining > 0) {
1079
+ effectiveGap = minGap + remaining / (unlocked.length - 1);
1080
+ }
1081
+ unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
1082
+ let pos = contentStart;
1083
+ for (const child of unlocked) {
1084
+ pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
1085
+ const newBox = { ...child.box };
1086
+ if (axis === "x") {
1087
+ newBox.x = pos;
1088
+ } else {
1089
+ newBox.y = pos;
1090
+ }
1091
+ boxes.set(child.id, newBox);
1092
+ locks.delete(child.id);
1093
+ pos += child.box[mainSize] + effectiveGap;
1094
+ }
1095
+ diagnostics.push({
1096
+ severity: "info",
1097
+ code: "intra_container_distributed",
1098
+ message: `Distributed ${unlocked.length} children in swimlane ${lane.id} along ${axis}.`,
1099
+ detail: { containerId: lane.id, count: unlocked.length, axis }
1100
+ });
1101
+ }
1102
+ }
1040
1103
  }
1041
1104
  function intervalForBox(box, axis, mainSize) {
1042
1105
  return { start: box[axis], end: box[axis] + box[mainSize] };
@@ -4308,6 +4371,397 @@ function connectedComponents(nodes, edges) {
4308
4371
  });
4309
4372
  }
4310
4373
 
4374
+ // src/layout/recursive.ts
4375
+ function buildContainerTree(groups, constraints, edges) {
4376
+ const childrenOf = /* @__PURE__ */ new Map();
4377
+ const parentOf = /* @__PURE__ */ new Map();
4378
+ for (const group of groups) {
4379
+ const children = [];
4380
+ for (const nodeId of group.nodeIds) {
4381
+ children.push(nodeId);
4382
+ parentOf.set(nodeId, group.id);
4383
+ }
4384
+ for (const childGroupId of group.groupIds) {
4385
+ children.push(childGroupId);
4386
+ parentOf.set(childGroupId, group.id);
4387
+ }
4388
+ childrenOf.set(group.id, children);
4389
+ }
4390
+ for (const c of constraints) {
4391
+ if (c.kind !== "containment") continue;
4392
+ for (const childId of c.childIds) {
4393
+ const existing = parentOf.get(childId);
4394
+ if (existing !== void 0) {
4395
+ if (existing === c.containerId) continue;
4396
+ const oldSiblings = childrenOf.get(existing) ?? [];
4397
+ const pruned = oldSiblings.filter((id) => id !== childId);
4398
+ if (pruned.length === 0) {
4399
+ childrenOf.delete(existing);
4400
+ } else {
4401
+ childrenOf.set(existing, pruned);
4402
+ }
4403
+ const newSiblings = childrenOf.get(c.containerId) ?? [];
4404
+ newSiblings.push(childId);
4405
+ childrenOf.set(c.containerId, newSiblings);
4406
+ parentOf.set(childId, c.containerId);
4407
+ } else {
4408
+ const list = childrenOf.get(c.containerId) ?? [];
4409
+ list.push(childId);
4410
+ childrenOf.set(c.containerId, list);
4411
+ parentOf.set(childId, c.containerId);
4412
+ }
4413
+ }
4414
+ }
4415
+ const rootIds = /* @__PURE__ */ new Set();
4416
+ for (const group of groups) {
4417
+ if (!parentOf.has(group.id)) {
4418
+ rootIds.add(group.id);
4419
+ }
4420
+ }
4421
+ const edgesInGroup = /* @__PURE__ */ new Map();
4422
+ for (const edge of edges) {
4423
+ const srcParent = parentOf.get(edge.source.nodeId);
4424
+ const tgtParent = parentOf.get(edge.target.nodeId);
4425
+ if (srcParent !== void 0 && srcParent === tgtParent) {
4426
+ const list = edgesInGroup.get(srcParent) ?? [];
4427
+ list.push(edge);
4428
+ edgesInGroup.set(srcParent, list);
4429
+ }
4430
+ }
4431
+ const treeDiagnostics = [];
4432
+ return { childrenOf, rootIds, edgesInGroup, diagnostics: treeDiagnostics };
4433
+ }
4434
+ function runRecursiveContainerLayout(input) {
4435
+ const diagnostics = [];
4436
+ const boxes = /* @__PURE__ */ new Map();
4437
+ const groupBoxes = /* @__PURE__ */ new Map();
4438
+ const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
4439
+ const groupById = new Map(input.groups.map((g) => [g.id, g]));
4440
+ const groupIdSet = new Set(input.groups.map((g) => g.id));
4441
+ const {
4442
+ childrenOf,
4443
+ rootIds,
4444
+ edgesInGroup,
4445
+ diagnostics: treeDiagnostics
4446
+ } = buildContainerTree(input.groups, input.constraints, input.edges);
4447
+ diagnostics.push(...treeDiagnostics);
4448
+ if (input.groups.length === 0) {
4449
+ const flat = runComponentAwareDagreInitialLayout({
4450
+ direction: input.direction,
4451
+ nodes: input.nodes.map((n) => ({ id: n.id, size: n.size })),
4452
+ edges: input.edges.map((e) => ({
4453
+ id: e.id,
4454
+ sourceId: e.source.nodeId,
4455
+ targetId: e.target.nodeId
4456
+ })),
4457
+ ...input.options === void 0 ? {} : { options: input.options }
4458
+ });
4459
+ diagnostics.push(...flat.diagnostics);
4460
+ for (const [id, box] of flat.boxes) {
4461
+ boxes.set(id, box);
4462
+ }
4463
+ return { boxes, groupBoxes, diagnostics };
4464
+ }
4465
+ const descendants = /* @__PURE__ */ new Map();
4466
+ function collectDescendants(groupId) {
4467
+ const cached = descendants.get(groupId);
4468
+ if (cached !== void 0) return cached;
4469
+ const result = /* @__PURE__ */ new Set();
4470
+ const children = childrenOf.get(groupId) ?? [];
4471
+ for (const childId of children) {
4472
+ result.add(childId);
4473
+ const childDesc = collectDescendants(childId);
4474
+ for (const d of childDesc) result.add(d);
4475
+ }
4476
+ descendants.set(groupId, result);
4477
+ return result;
4478
+ }
4479
+ const groupOrder = topologicalSort(input.groups, childrenOf, groupIdSet);
4480
+ for (const groupId of groupOrder) {
4481
+ const group = groupById.get(groupId);
4482
+ if (group === void 0) continue;
4483
+ const children = childrenOf.get(groupId) ?? [];
4484
+ if (children.length === 0) {
4485
+ const box = {
4486
+ x: 0,
4487
+ y: 0,
4488
+ width: (group.padding?.left ?? 8) + (group.padding?.right ?? 8) + 40,
4489
+ height: (group.padding?.top ?? 8) + (group.padding?.bottom ?? 8) + 20
4490
+ };
4491
+ groupBoxes.set(groupId, box);
4492
+ boxes.set(groupId, box);
4493
+ continue;
4494
+ }
4495
+ const leafNodeIds = [];
4496
+ const nestedGroupIds = [];
4497
+ for (const childId of children) {
4498
+ if (groupIdSet.has(childId)) {
4499
+ nestedGroupIds.push(childId);
4500
+ } else {
4501
+ leafNodeIds.push(childId);
4502
+ }
4503
+ }
4504
+ const childSizes = /* @__PURE__ */ new Map();
4505
+ for (const nodeId of leafNodeIds) {
4506
+ const node = nodeById.get(nodeId);
4507
+ if (node !== void 0) {
4508
+ childSizes.set(nodeId, {
4509
+ width: node.size.width,
4510
+ height: node.size.height
4511
+ });
4512
+ }
4513
+ }
4514
+ for (const nestedId of nestedGroupIds) {
4515
+ const nestedBox = groupBoxes.get(nestedId);
4516
+ if (nestedBox !== void 0) {
4517
+ childSizes.set(nestedId, {
4518
+ width: nestedBox.width,
4519
+ height: nestedBox.height
4520
+ });
4521
+ }
4522
+ }
4523
+ const groupEdges = edgesInGroup.get(groupId) ?? [];
4524
+ const childLayout = runDagreInitialLayout({
4525
+ direction: input.direction,
4526
+ nodes: children.flatMap((childId) => {
4527
+ const size = childSizes.get(childId);
4528
+ return size === void 0 ? [] : [{ id: childId, size }];
4529
+ }),
4530
+ edges: groupEdges.map((e) => ({
4531
+ id: e.id,
4532
+ sourceId: e.source.nodeId,
4533
+ targetId: e.target.nodeId
4534
+ })),
4535
+ options: {
4536
+ ...input.options ?? {},
4537
+ ranksep: (input.options?.ranksep ?? 100) * 0.6,
4538
+ // tighter inside containers
4539
+ nodesep: (input.options?.nodesep ?? 80) * 0.6
4540
+ }
4541
+ });
4542
+ diagnostics.push(...childLayout.diagnostics);
4543
+ if (childLayout.boxes.size === 0) continue;
4544
+ const childBoxes = [...childLayout.boxes.values()];
4545
+ const contentBounds = unionBoxes(childBoxes);
4546
+ const padding = group.padding ?? {
4547
+ top: 8,
4548
+ right: 8,
4549
+ bottom: 8,
4550
+ left: 8
4551
+ };
4552
+ const containerBox = {
4553
+ x: 0,
4554
+ y: 0,
4555
+ width: contentBounds.width + (padding.left ?? 8) + (padding.right ?? 8),
4556
+ height: contentBounds.height + (padding.top ?? 8) + (padding.bottom ?? 8)
4557
+ };
4558
+ const offsetX = padding.left ?? 8;
4559
+ const offsetY = padding.top ?? 8;
4560
+ for (const [childId, childBox] of childLayout.boxes) {
4561
+ boxes.set(childId, {
4562
+ ...childBox,
4563
+ x: childBox.x + offsetX,
4564
+ y: childBox.y + offsetY
4565
+ });
4566
+ }
4567
+ groupBoxes.set(groupId, containerBox);
4568
+ boxes.set(groupId, containerBox);
4569
+ }
4570
+ const allContainedIds = /* @__PURE__ */ new Set();
4571
+ for (const [, childIds] of childrenOf) {
4572
+ for (const cid of childIds) allContainedIds.add(cid);
4573
+ }
4574
+ const topLevelNodeIds = /* @__PURE__ */ new Set();
4575
+ for (const node of input.nodes) {
4576
+ if (!allContainedIds.has(node.id)) {
4577
+ topLevelNodeIds.add(node.id);
4578
+ }
4579
+ }
4580
+ const globalNodes = [];
4581
+ for (const nodeId of topLevelNodeIds) {
4582
+ const node = nodeById.get(nodeId);
4583
+ if (node !== void 0) {
4584
+ globalNodes.push({ id: nodeId, size: node.size });
4585
+ }
4586
+ }
4587
+ for (const rootId of rootIds) {
4588
+ const gb = groupBoxes.get(rootId);
4589
+ if (gb !== void 0) {
4590
+ globalNodes.push({
4591
+ id: rootId,
4592
+ size: { width: gb.width, height: gb.height }
4593
+ });
4594
+ }
4595
+ }
4596
+ function rootContainerOf(id) {
4597
+ for (const rootId of rootIds) {
4598
+ const desc = collectDescendants(rootId);
4599
+ if (desc.has(id)) return rootId;
4600
+ }
4601
+ return void 0;
4602
+ }
4603
+ const globalEdges = input.edges.filter((e) => {
4604
+ for (const group of input.groups) {
4605
+ const desc = collectDescendants(group.id);
4606
+ if (desc.has(e.source.nodeId) && desc.has(e.target.nodeId)) {
4607
+ return false;
4608
+ }
4609
+ }
4610
+ return true;
4611
+ }).map((e) => ({
4612
+ id: e.id,
4613
+ sourceId: rootContainerOf(e.source.nodeId) ?? e.source.nodeId,
4614
+ targetId: rootContainerOf(e.target.nodeId) ?? e.target.nodeId
4615
+ }));
4616
+ if (globalNodes.length > 0) {
4617
+ const globalLayout = runDagreInitialLayout({
4618
+ direction: input.direction,
4619
+ nodes: globalNodes,
4620
+ edges: globalEdges,
4621
+ ...input.options === void 0 ? {} : { options: input.options }
4622
+ });
4623
+ diagnostics.push(...globalLayout.diagnostics);
4624
+ for (const [id, box] of globalLayout.boxes) {
4625
+ if (groupBoxes.has(id)) {
4626
+ groupBoxes.set(id, box);
4627
+ boxes.set(id, box);
4628
+ } else if (topLevelNodeIds.has(id)) {
4629
+ boxes.set(id, box);
4630
+ }
4631
+ }
4632
+ for (const groupId of groupOrder) {
4633
+ const containerBox = groupBoxes.get(groupId);
4634
+ if (containerBox === void 0) continue;
4635
+ const offsetX = containerBox.x;
4636
+ const offsetY = containerBox.y;
4637
+ const children = childrenOf.get(groupId) ?? [];
4638
+ for (const childId of children) {
4639
+ const childBox = boxes.get(childId);
4640
+ if (childBox !== void 0) {
4641
+ boxes.set(childId, {
4642
+ ...childBox,
4643
+ x: childBox.x + offsetX,
4644
+ y: childBox.y + offsetY
4645
+ });
4646
+ }
4647
+ translateDescendants(childId, offsetX, offsetY, boxes, childrenOf);
4648
+ }
4649
+ }
4650
+ }
4651
+ return { boxes, groupBoxes, diagnostics };
4652
+ }
4653
+ function translateDescendants(groupId, dx, dy, boxes, childrenOf) {
4654
+ const children = childrenOf.get(groupId) ?? [];
4655
+ for (const childId of children) {
4656
+ const box = boxes.get(childId);
4657
+ if (box !== void 0) {
4658
+ boxes.set(childId, { ...box, x: box.x + dx, y: box.y + dy });
4659
+ }
4660
+ translateDescendants(childId, dx, dy, boxes, childrenOf);
4661
+ }
4662
+ }
4663
+ function topologicalSort(groups, childrenOf, groupIdSet) {
4664
+ const visited = /* @__PURE__ */ new Set();
4665
+ const result = [];
4666
+ function visit(id) {
4667
+ if (visited.has(id)) return;
4668
+ visited.add(id);
4669
+ const children = childrenOf.get(id) ?? [];
4670
+ for (const childId of children) {
4671
+ if (groupIdSet.has(childId)) {
4672
+ visit(childId);
4673
+ }
4674
+ }
4675
+ result.push(id);
4676
+ }
4677
+ for (const group of groups) {
4678
+ visit(group.id);
4679
+ }
4680
+ return result;
4681
+ }
4682
+
4683
+ // src/routing/binary-heap.ts
4684
+ var BinaryHeap = class {
4685
+ _data = [];
4686
+ _nextOrder = 0;
4687
+ get size() {
4688
+ return this._data.length;
4689
+ }
4690
+ push(value, priority) {
4691
+ const entry = { value, priority, order: this._nextOrder++ };
4692
+ this._data.push(entry);
4693
+ this._siftUp(this._data.length - 1);
4694
+ }
4695
+ pop() {
4696
+ if (this._data.length === 0) return void 0;
4697
+ const top = this._data[0];
4698
+ const last = this._data.pop();
4699
+ if (this._data.length > 0) {
4700
+ this._data[0] = last;
4701
+ this._siftDown(0);
4702
+ }
4703
+ return top.value;
4704
+ }
4705
+ peek() {
4706
+ return this._data.length > 0 ? this._data[0].value : void 0;
4707
+ }
4708
+ // -----------------------------------------------------------------------
4709
+ // Internals
4710
+ // -----------------------------------------------------------------------
4711
+ _siftUp(idx) {
4712
+ const entry = this._data[idx];
4713
+ while (idx > 0) {
4714
+ const parentIdx = idx - 1 >> 1;
4715
+ const parent = this._data[parentIdx];
4716
+ if (this._less(entry, parent)) {
4717
+ this._data[idx] = parent;
4718
+ idx = parentIdx;
4719
+ } else {
4720
+ break;
4721
+ }
4722
+ }
4723
+ this._data[idx] = entry;
4724
+ }
4725
+ _siftDown(idx) {
4726
+ const entry = this._data[idx];
4727
+ const size = this._data.length;
4728
+ while (true) {
4729
+ let smallestIdx = idx;
4730
+ const leftIdx = (idx << 1) + 1;
4731
+ const rightIdx = leftIdx + 1;
4732
+ if (leftIdx < size && this._less(
4733
+ this._data[leftIdx],
4734
+ this._data[smallestIdx]
4735
+ )) {
4736
+ smallestIdx = leftIdx;
4737
+ }
4738
+ if (rightIdx < size && this._less(
4739
+ this._data[rightIdx],
4740
+ this._data[smallestIdx]
4741
+ )) {
4742
+ smallestIdx = rightIdx;
4743
+ }
4744
+ if (smallestIdx !== idx) {
4745
+ this._data[idx] = this._data[smallestIdx];
4746
+ idx = smallestIdx;
4747
+ } else {
4748
+ break;
4749
+ }
4750
+ }
4751
+ this._data[idx] = entry;
4752
+ }
4753
+ /**
4754
+ * Two entries are compared first by priority, then by insertion order
4755
+ * when priorities are equal. The insertion-order tie-break makes the
4756
+ * heap deterministic: for a given sequence of {value, priority} pushes,
4757
+ * the extraction order is always the same.
4758
+ */
4759
+ _less(a, b) {
4760
+ if (a.priority !== b.priority) return a.priority < b.priority;
4761
+ return a.order < b.order;
4762
+ }
4763
+ };
4764
+
4311
4765
  // src/routing/astar.ts
4312
4766
  function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
4313
4767
  const margin = options.margin ?? 0;
@@ -4315,8 +4769,17 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4315
4769
  const segmentPenalty = options.segmentPenalty ?? 1;
4316
4770
  const endpointObstacles = options.endpointObstacles ?? [];
4317
4771
  const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
4318
- const xs = collectXs(source, target, obstacles, margin);
4319
- const ys = collectYs(source, target, obstacles, margin);
4772
+ const useCorridor = options.corridorPrefilter ?? true;
4773
+ const corridorMargin = options.corridorMargin ?? 32;
4774
+ const filtered = useCorridor ? filterObstaclesByCorridor(
4775
+ source,
4776
+ target,
4777
+ obstacles,
4778
+ endpointObstacles,
4779
+ corridorMargin
4780
+ ) : obstacles;
4781
+ const xs = collectXs(source, target, filtered, margin);
4782
+ const ys = collectYs(source, target, filtered, margin);
4320
4783
  if (xs.length * ys.length > maxNodes) {
4321
4784
  diagnostics?.push({
4322
4785
  severity: "warning",
@@ -4331,8 +4794,8 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4331
4794
  return null;
4332
4795
  }
4333
4796
  const { nodes, nodeIndex } = buildGraph(xs, ys);
4334
- connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
4335
- connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4797
+ connectHorizontalEdges(nodes, ys, filtered, endpointObstacles, margin);
4798
+ connectVerticalEdges(nodes, xs, filtered, endpointObstacles, margin);
4336
4799
  const path = aStarSearch(
4337
4800
  nodes,
4338
4801
  nodeIndex,
@@ -4344,6 +4807,22 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
4344
4807
  if (path === null) return null;
4345
4808
  return simplifyRoute(path);
4346
4809
  }
4810
+ function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4811
+ const cx1 = Math.min(source.x, target.x) - margin;
4812
+ const cx2 = Math.max(source.x, target.x) + margin;
4813
+ const cy1 = Math.min(source.y, target.y) - margin;
4814
+ const cy2 = Math.max(source.y, target.y) + margin;
4815
+ const result = [];
4816
+ for (const obs of obstacles) {
4817
+ if (obs.x + obs.width >= cx1 && obs.x <= cx2 && obs.y + obs.height >= cy1 && obs.y <= cy2) {
4818
+ result.push(obs);
4819
+ }
4820
+ }
4821
+ for (const ep of endpointObstacles) {
4822
+ result.push(ep);
4823
+ }
4824
+ return result;
4825
+ }
4347
4826
  function collectXs(source, target, obstacles, margin) {
4348
4827
  const raw = [];
4349
4828
  for (const obs of obstacles) {
@@ -4487,25 +4966,17 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
4487
4966
  gScore.set(startId, 0);
4488
4967
  const cameFrom = /* @__PURE__ */ new Map();
4489
4968
  const cameFromDir = /* @__PURE__ */ new Map();
4490
- const openSet = [];
4491
- openSet.push({
4492
- id: startId,
4493
- f: manhattan(source, target)
4494
- });
4495
- while (openSet.length > 0) {
4496
- let bestIdx = 0;
4497
- for (let i = 1; i < openSet.length; i++) {
4498
- if (openSet[i].f < openSet[bestIdx].f) {
4499
- bestIdx = i;
4500
- }
4501
- }
4502
- const current = openSet.splice(bestIdx, 1)[0];
4503
- if (current.id === goalId) {
4969
+ const openSet = new BinaryHeap();
4970
+ openSet.push(startId, manhattan(source, target));
4971
+ while (openSet.size > 0) {
4972
+ const currentId = openSet.pop();
4973
+ const currentG = gScore.get(currentId);
4974
+ if (currentG === void 0) continue;
4975
+ if (currentId === goalId) {
4504
4976
  return reconstructPath(nodes, cameFrom, goalId);
4505
4977
  }
4506
- const node = nodes[current.id];
4507
- const currentG = gScore.get(current.id);
4508
- const prevDir = cameFromDir.get(current.id);
4978
+ const node = nodes[currentId];
4979
+ const prevDir = cameFromDir.get(currentId);
4509
4980
  for (const [neighborId, edgeCost] of node.neighbors) {
4510
4981
  const neighbor = nodes[neighborId];
4511
4982
  const tentativeG = currentG + edgeCost * segmentPenalty;
@@ -4515,10 +4986,10 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
4515
4986
  const existingG = gScore.get(neighborId);
4516
4987
  if (existingG === void 0 || totalG < existingG) {
4517
4988
  gScore.set(neighborId, totalG);
4518
- cameFrom.set(neighborId, current.id);
4989
+ cameFrom.set(neighborId, currentId);
4519
4990
  cameFromDir.set(neighborId, newDir);
4520
4991
  const f = totalG + manhattan(neighbor, target);
4521
- openSet.push({ id: neighborId, f });
4992
+ openSet.push(neighborId, f);
4522
4993
  }
4523
4994
  }
4524
4995
  }
@@ -4555,6 +5026,292 @@ function areCollinear(a, b, c) {
4555
5026
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4556
5027
  }
4557
5028
 
5029
+ // src/routing/visibility-router.ts
5030
+ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
5031
+ const margin = options.margin ?? 0;
5032
+ const turnPenalty = options.turnPenalty ?? 50;
5033
+ const segmentPenalty = options.segmentPenalty ?? 1;
5034
+ const endpointObstacles = options.endpointObstacles ?? [];
5035
+ const maxCorners = options.maxCorners ?? 300;
5036
+ const vertices = collectCornerVertices(source, target, obstacles, margin);
5037
+ if (vertices.length > maxCorners) {
5038
+ diagnostics?.push({
5039
+ severity: "warning",
5040
+ code: "routing.visibility.corner_overflow",
5041
+ message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
5042
+ detail: { vertexCount: vertices.length, maxCorners }
5043
+ });
5044
+ return null;
5045
+ }
5046
+ if (obstacles.length === 0) {
5047
+ return simplifyRoute2([source, target]);
5048
+ }
5049
+ const expandedObstacles = margin === 0 ? obstacles : obstacles.map((o) => expandBox2(o, margin));
5050
+ const edges = buildVisibilityEdges(
5051
+ vertices,
5052
+ expandedObstacles,
5053
+ endpointObstacles
5054
+ );
5055
+ const path = aStarSearch2(
5056
+ vertices,
5057
+ edges,
5058
+ source,
5059
+ target,
5060
+ segmentPenalty,
5061
+ turnPenalty
5062
+ );
5063
+ if (path === null) return null;
5064
+ return simplifyRoute2(path, expandedObstacles);
5065
+ }
5066
+ function collectCornerVertices(source, target, obstacles, margin = 0) {
5067
+ const vertices = [
5068
+ { point: { x: source.x, y: source.y }, obstacleIndex: -1 },
5069
+ { point: { x: target.x, y: target.y }, obstacleIndex: -1 }
5070
+ ];
5071
+ const seen = /* @__PURE__ */ new Set();
5072
+ seen.add(`${source.x},${source.y}`);
5073
+ seen.add(`${target.x},${target.y}`);
5074
+ const addVertex = (p, obstacleIndex) => {
5075
+ const key = `${p.x},${p.y}`;
5076
+ if (seen.has(key)) return;
5077
+ seen.add(key);
5078
+ vertices.push({ point: p, obstacleIndex });
5079
+ };
5080
+ for (let i = 0; i < obstacles.length; i++) {
5081
+ const obs = obstacles[i];
5082
+ const c0 = { x: obs.x - margin, y: obs.y - margin };
5083
+ const c1 = { x: obs.x + obs.width + margin, y: obs.y - margin };
5084
+ const c2 = {
5085
+ x: obs.x + obs.width + margin,
5086
+ y: obs.y + obs.height + margin
5087
+ };
5088
+ const c3 = { x: obs.x - margin, y: obs.y + obs.height + margin };
5089
+ for (const c of [c0, c1, c2, c3]) {
5090
+ addVertex(c, i);
5091
+ }
5092
+ }
5093
+ for (const p of [source, target]) {
5094
+ for (let i = 0; i < obstacles.length; i++) {
5095
+ const obs = obstacles[i];
5096
+ addVertex({ x: obs.x - margin, y: p.y }, i);
5097
+ addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
5098
+ addVertex({ x: p.x, y: obs.y - margin }, i);
5099
+ addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
5100
+ if (p.x > obs.x && p.x < obs.x + obs.width) {
5101
+ addVertex({ x: p.x, y: obs.y - margin }, i);
5102
+ addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
5103
+ }
5104
+ if (p.y > obs.y && p.y < obs.y + obs.height) {
5105
+ addVertex({ x: obs.x - margin, y: p.y }, i);
5106
+ addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
5107
+ }
5108
+ }
5109
+ }
5110
+ return vertices;
5111
+ }
5112
+ function buildVisibilityEdges(vertices, obstacles, endpointObstacles) {
5113
+ const edges = [];
5114
+ for (let i = 0; i < vertices.length; i++) {
5115
+ const v = vertices[i];
5116
+ const right = visibleInDirection(
5117
+ v,
5118
+ vertices,
5119
+ obstacles,
5120
+ endpointObstacles,
5121
+ "right"
5122
+ );
5123
+ const left = visibleInDirection(
5124
+ v,
5125
+ vertices,
5126
+ obstacles,
5127
+ endpointObstacles,
5128
+ "left"
5129
+ );
5130
+ const down = visibleInDirection(
5131
+ v,
5132
+ vertices,
5133
+ obstacles,
5134
+ endpointObstacles,
5135
+ "down"
5136
+ );
5137
+ const up = visibleInDirection(
5138
+ v,
5139
+ vertices,
5140
+ obstacles,
5141
+ endpointObstacles,
5142
+ "up"
5143
+ );
5144
+ for (const neighbor of [right, left, down, up]) {
5145
+ if (neighbor !== null && neighbor.index > i) {
5146
+ edges.push({
5147
+ from: i,
5148
+ to: neighbor.index,
5149
+ cost: neighbor.distance
5150
+ });
5151
+ }
5152
+ }
5153
+ }
5154
+ return edges;
5155
+ }
5156
+ function visibleInDirection(origin, vertices, obstacles, endpointObstacles, dir) {
5157
+ const candidates = [];
5158
+ for (let i = 0; i < vertices.length; i++) {
5159
+ const v = vertices[i];
5160
+ const dx = v.point.x - origin.point.x;
5161
+ const dy = v.point.y - origin.point.y;
5162
+ switch (dir) {
5163
+ case "right":
5164
+ if (dx > 0 && dy === 0) candidates.push({ index: i, dist: dx });
5165
+ break;
5166
+ case "left":
5167
+ if (dx < 0 && dy === 0) candidates.push({ index: i, dist: -dx });
5168
+ break;
5169
+ case "down":
5170
+ if (dy > 0 && dx === 0) candidates.push({ index: i, dist: dy });
5171
+ break;
5172
+ case "up":
5173
+ if (dy < 0 && dx === 0) candidates.push({ index: i, dist: -dy });
5174
+ break;
5175
+ }
5176
+ }
5177
+ candidates.sort((a, b) => a.dist - b.dist);
5178
+ for (const c of candidates) {
5179
+ if (isSegmentVisible(
5180
+ origin.point,
5181
+ vertices[c.index].point,
5182
+ obstacles,
5183
+ endpointObstacles,
5184
+ origin.obstacleIndex,
5185
+ vertices[c.index].obstacleIndex
5186
+ )) {
5187
+ return { index: c.index, distance: c.dist };
5188
+ }
5189
+ }
5190
+ return null;
5191
+ }
5192
+ function isSegmentVisible(a, b, obstacles, endpointObstacles, _aObsIdx, _bObsIdx) {
5193
+ for (let i = 0; i < obstacles.length; i++) {
5194
+ if (segmentEntersBox(a, b, obstacles[i])) return false;
5195
+ }
5196
+ for (const ep of endpointObstacles) {
5197
+ if (segmentEntersBox(a, b, ep)) return false;
5198
+ }
5199
+ return true;
5200
+ }
5201
+ function segmentEntersBox(start, end, box) {
5202
+ const left = box.x;
5203
+ const right = box.x + box.width;
5204
+ const top = box.y;
5205
+ const bottom = box.y + box.height;
5206
+ const inside = (p) => p.x > left && p.x < right && p.y > top && p.y < bottom;
5207
+ if (inside(start) || inside(end)) return true;
5208
+ if (start.x === end.x) {
5209
+ return start.x > left && start.x < right && Math.max(start.y, end.y) > top && Math.min(start.y, end.y) < bottom;
5210
+ }
5211
+ if (start.y === end.y) {
5212
+ return start.y > top && start.y < bottom && Math.max(start.x, end.x) > left && Math.min(start.x, end.x) < right;
5213
+ }
5214
+ return false;
5215
+ }
5216
+ function expandBox2(box, margin) {
5217
+ return {
5218
+ x: box.x - margin,
5219
+ y: box.y - margin,
5220
+ width: box.width + margin * 2,
5221
+ height: box.height + margin * 2
5222
+ };
5223
+ }
5224
+ function aStarSearch2(vertices, edges, source, target, segmentPenalty, turnPenalty) {
5225
+ const startId = 0;
5226
+ const goalId = 1;
5227
+ const gScore = /* @__PURE__ */ new Map();
5228
+ gScore.set(startId, 0);
5229
+ const cameFrom = /* @__PURE__ */ new Map();
5230
+ const cameFromDir = /* @__PURE__ */ new Map();
5231
+ const openSet = new BinaryHeap();
5232
+ openSet.push(startId, manhattan2(source, target));
5233
+ const neighborMap = /* @__PURE__ */ new Map();
5234
+ for (const e of edges) {
5235
+ let list = neighborMap.get(e.from);
5236
+ if (list === void 0) {
5237
+ list = [];
5238
+ neighborMap.set(e.from, list);
5239
+ }
5240
+ list.push({ to: e.to, cost: e.cost });
5241
+ list = neighborMap.get(e.to);
5242
+ if (list === void 0) {
5243
+ list = [];
5244
+ neighborMap.set(e.to, list);
5245
+ }
5246
+ list.push({ to: e.from, cost: e.cost });
5247
+ }
5248
+ while (openSet.size > 0) {
5249
+ const currentId = openSet.pop();
5250
+ const currentG = gScore.get(currentId);
5251
+ if (currentG === void 0) continue;
5252
+ if (currentId === goalId) {
5253
+ return reconstructPath2(vertices, cameFrom, goalId);
5254
+ }
5255
+ const prevDir = cameFromDir.get(currentId);
5256
+ const neighbors = neighborMap.get(currentId) ?? [];
5257
+ for (const { to, cost } of neighbors) {
5258
+ const tentativeG = currentG + cost * segmentPenalty;
5259
+ const toV = vertices[to];
5260
+ const curV = vertices[currentId];
5261
+ const newDir = toV.point.y === curV.point.y ? "h" : "v";
5262
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
5263
+ const totalG = tentativeG + turnCost;
5264
+ const existingG = gScore.get(to);
5265
+ if (existingG === void 0 || totalG < existingG) {
5266
+ gScore.set(to, totalG);
5267
+ cameFrom.set(to, currentId);
5268
+ cameFromDir.set(to, newDir);
5269
+ const f = totalG + manhattan2(toV.point, target);
5270
+ openSet.push(to, f);
5271
+ }
5272
+ }
5273
+ }
5274
+ return null;
5275
+ }
5276
+ function manhattan2(a, b) {
5277
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
5278
+ }
5279
+ function reconstructPath2(vertices, cameFrom, goalId) {
5280
+ const path = [];
5281
+ let current = goalId;
5282
+ while (current !== void 0) {
5283
+ const v = vertices[current];
5284
+ path.unshift({ x: v.point.x, y: v.point.y });
5285
+ current = cameFrom.get(current);
5286
+ }
5287
+ return path;
5288
+ }
5289
+ function simplifyRoute2(points, obstacles = []) {
5290
+ if (points.length <= 2) return [...points];
5291
+ const result = [points[0]];
5292
+ for (let i = 1; i < points.length - 1; i++) {
5293
+ const prev = result[result.length - 1];
5294
+ const curr = points[i];
5295
+ const next = points[i + 1];
5296
+ const collinear = prev.x === curr.x && curr.x === next.x || prev.y === curr.y && curr.y === next.y;
5297
+ if (!collinear) {
5298
+ result.push(curr);
5299
+ continue;
5300
+ }
5301
+ if (segmentCrossesAnyObstacle(prev, next, obstacles)) {
5302
+ result.push(curr);
5303
+ }
5304
+ }
5305
+ result.push(points[points.length - 1]);
5306
+ return result;
5307
+ }
5308
+ function segmentCrossesAnyObstacle(a, b, obstacles) {
5309
+ for (const obs of obstacles) {
5310
+ if (segmentEntersBox(a, b, obs)) return true;
5311
+ }
5312
+ return false;
5313
+ }
5314
+
4558
5315
  // src/routing/routes.ts
4559
5316
  function checkBacktracking(points, source, target, diagnostics) {
4560
5317
  if (points.length < 2) return;
@@ -4644,13 +5401,18 @@ function routeEdge(input) {
4644
5401
  input.source.center,
4645
5402
  targetAnchor
4646
5403
  );
4647
- const path = findObstacleFreePath(
5404
+ const cornerPath = findCornerGraphPath(
4648
5405
  source,
4649
5406
  target,
4650
5407
  [...softObstacles, ...hardObstacles],
4651
- {
4652
- endpointObstacles
4653
- },
5408
+ { endpointObstacles, margin: 2 },
5409
+ diagnostics
5410
+ );
5411
+ const path = cornerPath ?? findObstacleFreePath(
5412
+ source,
5413
+ target,
5414
+ [...softObstacles, ...hardObstacles],
5415
+ { endpointObstacles, margin: 0 },
4654
5416
  diagnostics
4655
5417
  );
4656
5418
  if (path !== null && path.length >= 2) {
@@ -4893,7 +5655,7 @@ function routeEdge(input) {
4893
5655
  };
4894
5656
  }
4895
5657
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
4896
- const simplified = simplifyRoute2(points);
5658
+ const simplified = simplifyRoute3(points);
4897
5659
  if (simplified.length >= 3) {
4898
5660
  return simplified;
4899
5661
  }
@@ -5197,7 +5959,7 @@ function squaredDistance2(a, b) {
5197
5959
  const dy = a.y - b.y;
5198
5960
  return dx * dx + dy * dy;
5199
5961
  }
5200
- function simplifyRoute2(points) {
5962
+ function simplifyRoute3(points) {
5201
5963
  const withoutDuplicates = [];
5202
5964
  for (const point2 of points) {
5203
5965
  const previous = withoutDuplicates.at(-1);
@@ -5748,7 +6510,21 @@ function solveDiagram(diagram, options = {}) {
5748
6510
  );
5749
6511
  const constraints = stableByConstraintId(diagram.constraints);
5750
6512
  const initialLayoutMode = options.initialLayout ?? "dagre";
5751
- const layout2 = runInitialLayout({
6513
+ const useRecursive = options.recursiveLayout === true;
6514
+ if (useRecursive && initialLayoutMode === "positions") {
6515
+ diagnostics.push({
6516
+ severity: "warning",
6517
+ code: "layout.recursive-ignores-positions",
6518
+ message: 'recursiveLayout overrides initialLayout "positions" \u2014 seed positions are ignored for bottom-up container layout.'
6519
+ });
6520
+ }
6521
+ const layout2 = useRecursive ? runRecursiveContainerLayout({
6522
+ direction: diagram.direction,
6523
+ nodes: styledNodes,
6524
+ groups: styledGroups,
6525
+ edges: styledEdges,
6526
+ constraints
6527
+ }) : runInitialLayout({
5752
6528
  mode: initialLayoutMode,
5753
6529
  componentAware: options.maxStackDepth === void 0,
5754
6530
  direction: diagram.direction,
@@ -5764,12 +6540,32 @@ function solveDiagram(diagram, options = {}) {
5764
6540
  options,
5765
6541
  diagnostics
5766
6542
  );
6543
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
6544
+ const rewrapped = wrapHorizontalStackIfNeeded(
6545
+ initialNodeBoxes,
6546
+ styledNodes,
6547
+ diagram.direction,
6548
+ options,
6549
+ diagnostics
6550
+ );
6551
+ for (const [id, box] of rewrapped) {
6552
+ initialNodeBoxes.set(id, box);
6553
+ }
6554
+ }
6555
+ if (useRecursive && "groupBoxes" in layout2) {
6556
+ const recursiveLayout = layout2;
6557
+ for (const [groupId, groupBox] of recursiveLayout.groupBoxes) {
6558
+ initialNodeBoxes.set(groupId, groupBox);
6559
+ }
6560
+ }
5767
6561
  expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
5768
6562
  const constrained = applyLayoutConstraints({
5769
6563
  direction: diagram.direction,
5770
6564
  overlapSpacing: options?.overlapSpacing ?? 40,
5771
6565
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
5772
6566
  distributeContainedChildren: options.distributeContainedChildren ?? true,
6567
+ distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
6568
+ swimlanes: styledSwimlanes,
5773
6569
  boxes: initialNodeBoxes,
5774
6570
  nodes: styledNodes,
5775
6571
  constraints
@@ -6432,7 +7228,7 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
6432
7228
  );
6433
7229
  return wrapped;
6434
7230
  }
6435
- if (edges.length > 0 || !isVerticalRunaway(wrapped, nodes, direction, options)) {
7231
+ if (edges.length > 0 || !isStackRunaway(wrapped, nodes, direction, options)) {
6436
7232
  reportVerticalRunaway(
6437
7233
  wrapped,
6438
7234
  nodes,
@@ -6483,8 +7279,57 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
6483
7279
  });
6484
7280
  return wrapped;
6485
7281
  }
7282
+ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnostics) {
7283
+ if (!isStackRunaway(boxes, nodes, direction, options)) {
7284
+ return new Map(boxes);
7285
+ }
7286
+ const maxRowDepth = options.maxRowDepth;
7287
+ if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
7288
+ return new Map(boxes);
7289
+ }
7290
+ const ordered = [...nodes].sort((a, b) => {
7291
+ const ba = boxes.get(a.id);
7292
+ const bb = boxes.get(b.id);
7293
+ if (ba === void 0 || bb === void 0) return 0;
7294
+ const dx = ba.x - bb.x;
7295
+ return dx !== 0 ? dx : ba.y - bb.y;
7296
+ });
7297
+ const rows = Math.ceil(ordered.length / maxRowDepth);
7298
+ const wrapped = new Map(boxes);
7299
+ const rowSpacing = options.overlapSpacing ?? 40;
7300
+ let minX = Infinity;
7301
+ let minY = Infinity;
7302
+ let maxH = 0;
7303
+ for (const n of ordered) {
7304
+ const b = boxes.get(n.id);
7305
+ if (b !== void 0) {
7306
+ minX = Math.min(minX, b.x);
7307
+ minY = Math.min(minY, b.y);
7308
+ maxH = Math.max(maxH, b.height);
7309
+ }
7310
+ }
7311
+ for (let ri = 0; ri < rows; ri++) {
7312
+ const rowNodes = ordered.slice(ri * maxRowDepth, (ri + 1) * maxRowDepth);
7313
+ let x = minX;
7314
+ const y = minY + ri * (maxH + rowSpacing);
7315
+ for (const node of rowNodes) {
7316
+ const box = boxes.get(node.id);
7317
+ if (box === void 0) continue;
7318
+ wrapped.set(node.id, { ...box, x, y });
7319
+ x += box.width + rowSpacing;
7320
+ }
7321
+ }
7322
+ diagnostics.push({
7323
+ severity: "warning",
7324
+ code: "horizontal_runaway",
7325
+ message: `Single-row layout exceeded maxRowDepth ${maxRowDepth}; wrapped into ${rows} rows.`,
7326
+ path: ["nodes"],
7327
+ detail: { nodeCount: ordered.length, maxRowDepth, rows }
7328
+ });
7329
+ return wrapped;
7330
+ }
6486
7331
  function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
6487
- if (!isVerticalRunaway(boxes, nodes, direction, options)) {
7332
+ if (!isStackRunaway(boxes, nodes, direction, options)) {
6488
7333
  return;
6489
7334
  }
6490
7335
  diagnostics.push({
@@ -6500,7 +7345,7 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
6500
7345
  }
6501
7346
  });
6502
7347
  }
6503
- function isVerticalRunaway(boxes, nodes, direction, options) {
7348
+ function isStackRunaway(boxes, nodes, direction, options) {
6504
7349
  if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
6505
7350
  return false;
6506
7351
  }
@@ -7091,7 +7936,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
7091
7936
  });
7092
7937
  continue;
7093
7938
  }
7094
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7939
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
7095
7940
  const geometry = computeShapeGeometry({
7096
7941
  shape: node.shape,
7097
7942
  box,
@@ -7185,7 +8030,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
7185
8030
  }
7186
8031
  }
7187
8032
  }
7188
- function coordinatePorts(node, nodeBox, portShifting) {
8033
+ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
7189
8034
  const portsBySide = /* @__PURE__ */ new Map();
7190
8035
  for (const port of node.ports ?? []) {
7191
8036
  const ports = portsBySide.get(port.side) ?? [];
@@ -7208,7 +8053,9 @@ function coordinatePorts(node, nodeBox, portShifting) {
7208
8053
  side,
7209
8054
  index,
7210
8055
  sorted.length,
7211
- portShifting
8056
+ portShifting,
8057
+ diagnostics,
8058
+ node.id
7212
8059
  );
7213
8060
  const box = portBox(anchor);
7214
8061
  coordinated.push({ ...port, box, anchor });
@@ -7216,16 +8063,32 @@ function coordinatePorts(node, nodeBox, portShifting) {
7216
8063
  }
7217
8064
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
7218
8065
  }
7219
- function portAnchor(nodeBox, side, index, count, portShifting) {
8066
+ function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
7220
8067
  const shiftingEnabled = portShifting?.enabled ?? true;
7221
8068
  const requestedSpacing = portShifting?.spacing ?? 24;
7222
8069
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
7223
8070
  const availableSpan = 2 * maxOffset;
7224
8071
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
7225
- const spacing = shiftingEnabled && count > 1 ? Math.max(
8072
+ const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
7226
8073
  Math.min(requestedSpacing, availableSpan / (count - 1)),
7227
8074
  minSpacing
7228
8075
  ) : 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;
7229
8092
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
7230
8093
  switch (side) {
7231
8094
  case "left":
@@ -9036,6 +9899,7 @@ exports.applyLayoutConstraints = applyLayoutConstraints;
9036
9899
  exports.assertFiniteNonNegative = assertFiniteNonNegative;
9037
9900
  exports.assertFinitePositive = assertFinitePositive;
9038
9901
  exports.boxCenter = boxCenter;
9902
+ exports.buildContainerTree = buildContainerTree;
9039
9903
  exports.canonicalize = canonicalize;
9040
9904
  exports.computeArrowhead = computeArrowhead;
9041
9905
  exports.computeContainerGeometry = computeContainerGeometry;
@@ -9065,7 +9929,8 @@ exports.resolveOutputFormat = resolveOutputFormat;
9065
9929
  exports.routeEdge = routeEdge;
9066
9930
  exports.runComponentAwareDagreInitialLayout = runComponentAwareDagreInitialLayout;
9067
9931
  exports.runDagreInitialLayout = runDagreInitialLayout;
9068
- exports.simplifyRoute = simplifyRoute2;
9932
+ exports.runRecursiveContainerLayout = runRecursiveContainerLayout;
9933
+ exports.simplifyRoute = simplifyRoute3;
9069
9934
  exports.solveDiagram = solveDiagram;
9070
9935
  exports.solveDiagramSafe = solveDiagramSafe;
9071
9936
  exports.sortDslDiagnostics = sortDslDiagnostics;