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