@crazyhappyone/auto-graph 0.2.4 → 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.
@@ -2252,6 +2252,69 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2252
2252
  }
2253
2253
  });
2254
2254
  }
2255
+ if (input.swimlanes !== void 0 && input.swimlanes.length > 0 && input.distributeSwimlaneChildren) {
2256
+ distributeSwimlaneChildren(input, boxes, locks, diagnostics);
2257
+ }
2258
+ }
2259
+ function distributeSwimlaneChildren(input, boxes, locks, diagnostics) {
2260
+ const spread = input.distributeSwimlaneChildren === "spread";
2261
+ const minGap = input.minSiblingGap ?? 8;
2262
+ for (const swimlane of input.swimlanes) {
2263
+ if (swimlane.layout === "contract") continue;
2264
+ const isVertical = swimlane.orientation !== "horizontal";
2265
+ const axis = isVertical ? "x" : "y";
2266
+ const mainSize = isVertical ? "width" : "height";
2267
+ for (const lane of swimlane.lanes) {
2268
+ if (lane.children.length < 2) continue;
2269
+ const unlocked = [];
2270
+ const reserved = [];
2271
+ for (const childId of lane.children) {
2272
+ const box = boxes.get(childId);
2273
+ if (box === void 0) continue;
2274
+ if (locks.has(childId)) {
2275
+ const lock = locks.get(childId);
2276
+ if (lock.source === "fixed-position") {
2277
+ unlocked.push({ id: childId, box });
2278
+ continue;
2279
+ }
2280
+ reserved.push(intervalForBox(box, axis, mainSize));
2281
+ continue;
2282
+ }
2283
+ unlocked.push({ id: childId, box });
2284
+ }
2285
+ if (unlocked.length < 2) continue;
2286
+ const contentStart = isVertical ? Math.min(...unlocked.map((c) => c.box.x)) : Math.min(...unlocked.map((c) => c.box.y));
2287
+ 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));
2288
+ const contentSpan = contentEnd - contentStart;
2289
+ const totalChildSpan = unlocked.reduce((s, c) => s + c.box[mainSize], 0);
2290
+ const reservedSpan = reserved.reduce((s, r) => s + (r.end - r.start), 0);
2291
+ let effectiveGap = minGap;
2292
+ const remaining = contentSpan - totalChildSpan - reservedSpan - minGap * (unlocked.length - 1);
2293
+ if (spread && remaining > 0) {
2294
+ effectiveGap = minGap + remaining / (unlocked.length - 1);
2295
+ }
2296
+ unlocked.sort((a, b) => a.box[axis] - b.box[axis]);
2297
+ let pos = contentStart;
2298
+ for (const child of unlocked) {
2299
+ pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
2300
+ const newBox = { ...child.box };
2301
+ if (axis === "x") {
2302
+ newBox.x = pos;
2303
+ } else {
2304
+ newBox.y = pos;
2305
+ }
2306
+ boxes.set(child.id, newBox);
2307
+ locks.delete(child.id);
2308
+ pos += child.box[mainSize] + effectiveGap;
2309
+ }
2310
+ diagnostics.push({
2311
+ severity: "info",
2312
+ code: "intra_container_distributed",
2313
+ message: `Distributed ${unlocked.length} children in swimlane ${lane.id} along ${axis}.`,
2314
+ detail: { containerId: lane.id, count: unlocked.length, axis }
2315
+ });
2316
+ }
2317
+ }
2255
2318
  }
2256
2319
  function intervalForBox(box, axis, mainSize) {
2257
2320
  return { start: box[axis], end: box[axis] + box[mainSize] };
@@ -3664,6 +3727,397 @@ function connectedComponents(nodes, edges) {
3664
3727
  });
3665
3728
  }
3666
3729
 
3730
+ // src/layout/recursive.ts
3731
+ function buildContainerTree(groups, constraints, edges) {
3732
+ const childrenOf = /* @__PURE__ */ new Map();
3733
+ const parentOf = /* @__PURE__ */ new Map();
3734
+ for (const group of groups) {
3735
+ const children = [];
3736
+ for (const nodeId of group.nodeIds) {
3737
+ children.push(nodeId);
3738
+ parentOf.set(nodeId, group.id);
3739
+ }
3740
+ for (const childGroupId of group.groupIds) {
3741
+ children.push(childGroupId);
3742
+ parentOf.set(childGroupId, group.id);
3743
+ }
3744
+ childrenOf.set(group.id, children);
3745
+ }
3746
+ for (const c of constraints) {
3747
+ if (c.kind !== "containment") continue;
3748
+ for (const childId of c.childIds) {
3749
+ const existing = parentOf.get(childId);
3750
+ if (existing !== void 0) {
3751
+ if (existing === c.containerId) continue;
3752
+ const oldSiblings = childrenOf.get(existing) ?? [];
3753
+ const pruned = oldSiblings.filter((id) => id !== childId);
3754
+ if (pruned.length === 0) {
3755
+ childrenOf.delete(existing);
3756
+ } else {
3757
+ childrenOf.set(existing, pruned);
3758
+ }
3759
+ const newSiblings = childrenOf.get(c.containerId) ?? [];
3760
+ newSiblings.push(childId);
3761
+ childrenOf.set(c.containerId, newSiblings);
3762
+ parentOf.set(childId, c.containerId);
3763
+ } else {
3764
+ const list = childrenOf.get(c.containerId) ?? [];
3765
+ list.push(childId);
3766
+ childrenOf.set(c.containerId, list);
3767
+ parentOf.set(childId, c.containerId);
3768
+ }
3769
+ }
3770
+ }
3771
+ const rootIds = /* @__PURE__ */ new Set();
3772
+ for (const group of groups) {
3773
+ if (!parentOf.has(group.id)) {
3774
+ rootIds.add(group.id);
3775
+ }
3776
+ }
3777
+ const edgesInGroup = /* @__PURE__ */ new Map();
3778
+ for (const edge of edges) {
3779
+ const srcParent = parentOf.get(edge.source.nodeId);
3780
+ const tgtParent = parentOf.get(edge.target.nodeId);
3781
+ if (srcParent !== void 0 && srcParent === tgtParent) {
3782
+ const list = edgesInGroup.get(srcParent) ?? [];
3783
+ list.push(edge);
3784
+ edgesInGroup.set(srcParent, list);
3785
+ }
3786
+ }
3787
+ const treeDiagnostics = [];
3788
+ return { childrenOf, rootIds, edgesInGroup, diagnostics: treeDiagnostics };
3789
+ }
3790
+ function runRecursiveContainerLayout(input) {
3791
+ const diagnostics = [];
3792
+ const boxes = /* @__PURE__ */ new Map();
3793
+ const groupBoxes = /* @__PURE__ */ new Map();
3794
+ const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
3795
+ const groupById = new Map(input.groups.map((g) => [g.id, g]));
3796
+ const groupIdSet = new Set(input.groups.map((g) => g.id));
3797
+ const {
3798
+ childrenOf,
3799
+ rootIds,
3800
+ edgesInGroup,
3801
+ diagnostics: treeDiagnostics
3802
+ } = buildContainerTree(input.groups, input.constraints, input.edges);
3803
+ diagnostics.push(...treeDiagnostics);
3804
+ if (input.groups.length === 0) {
3805
+ const flat = runComponentAwareDagreInitialLayout({
3806
+ direction: input.direction,
3807
+ nodes: input.nodes.map((n) => ({ id: n.id, size: n.size })),
3808
+ edges: input.edges.map((e) => ({
3809
+ id: e.id,
3810
+ sourceId: e.source.nodeId,
3811
+ targetId: e.target.nodeId
3812
+ })),
3813
+ ...input.options === void 0 ? {} : { options: input.options }
3814
+ });
3815
+ diagnostics.push(...flat.diagnostics);
3816
+ for (const [id, box] of flat.boxes) {
3817
+ boxes.set(id, box);
3818
+ }
3819
+ return { boxes, groupBoxes, diagnostics };
3820
+ }
3821
+ const descendants = /* @__PURE__ */ new Map();
3822
+ function collectDescendants(groupId) {
3823
+ const cached = descendants.get(groupId);
3824
+ if (cached !== void 0) return cached;
3825
+ const result = /* @__PURE__ */ new Set();
3826
+ const children = childrenOf.get(groupId) ?? [];
3827
+ for (const childId of children) {
3828
+ result.add(childId);
3829
+ const childDesc = collectDescendants(childId);
3830
+ for (const d of childDesc) result.add(d);
3831
+ }
3832
+ descendants.set(groupId, result);
3833
+ return result;
3834
+ }
3835
+ const groupOrder = topologicalSort(input.groups, childrenOf, groupIdSet);
3836
+ for (const groupId of groupOrder) {
3837
+ const group = groupById.get(groupId);
3838
+ if (group === void 0) continue;
3839
+ const children = childrenOf.get(groupId) ?? [];
3840
+ if (children.length === 0) {
3841
+ const box = {
3842
+ x: 0,
3843
+ y: 0,
3844
+ width: (group.padding?.left ?? 8) + (group.padding?.right ?? 8) + 40,
3845
+ height: (group.padding?.top ?? 8) + (group.padding?.bottom ?? 8) + 20
3846
+ };
3847
+ groupBoxes.set(groupId, box);
3848
+ boxes.set(groupId, box);
3849
+ continue;
3850
+ }
3851
+ const leafNodeIds = [];
3852
+ const nestedGroupIds = [];
3853
+ for (const childId of children) {
3854
+ if (groupIdSet.has(childId)) {
3855
+ nestedGroupIds.push(childId);
3856
+ } else {
3857
+ leafNodeIds.push(childId);
3858
+ }
3859
+ }
3860
+ const childSizes = /* @__PURE__ */ new Map();
3861
+ for (const nodeId of leafNodeIds) {
3862
+ const node = nodeById.get(nodeId);
3863
+ if (node !== void 0) {
3864
+ childSizes.set(nodeId, {
3865
+ width: node.size.width,
3866
+ height: node.size.height
3867
+ });
3868
+ }
3869
+ }
3870
+ for (const nestedId of nestedGroupIds) {
3871
+ const nestedBox = groupBoxes.get(nestedId);
3872
+ if (nestedBox !== void 0) {
3873
+ childSizes.set(nestedId, {
3874
+ width: nestedBox.width,
3875
+ height: nestedBox.height
3876
+ });
3877
+ }
3878
+ }
3879
+ const groupEdges = edgesInGroup.get(groupId) ?? [];
3880
+ const childLayout = runDagreInitialLayout({
3881
+ direction: input.direction,
3882
+ nodes: children.flatMap((childId) => {
3883
+ const size = childSizes.get(childId);
3884
+ return size === void 0 ? [] : [{ id: childId, size }];
3885
+ }),
3886
+ edges: groupEdges.map((e) => ({
3887
+ id: e.id,
3888
+ sourceId: e.source.nodeId,
3889
+ targetId: e.target.nodeId
3890
+ })),
3891
+ options: {
3892
+ ...input.options ?? {},
3893
+ ranksep: (input.options?.ranksep ?? 100) * 0.6,
3894
+ // tighter inside containers
3895
+ nodesep: (input.options?.nodesep ?? 80) * 0.6
3896
+ }
3897
+ });
3898
+ diagnostics.push(...childLayout.diagnostics);
3899
+ if (childLayout.boxes.size === 0) continue;
3900
+ const childBoxes = [...childLayout.boxes.values()];
3901
+ const contentBounds = unionBoxes(childBoxes);
3902
+ const padding = group.padding ?? {
3903
+ top: 8,
3904
+ right: 8,
3905
+ bottom: 8,
3906
+ left: 8
3907
+ };
3908
+ const containerBox = {
3909
+ x: 0,
3910
+ y: 0,
3911
+ width: contentBounds.width + (padding.left ?? 8) + (padding.right ?? 8),
3912
+ height: contentBounds.height + (padding.top ?? 8) + (padding.bottom ?? 8)
3913
+ };
3914
+ const offsetX = padding.left ?? 8;
3915
+ const offsetY = padding.top ?? 8;
3916
+ for (const [childId, childBox] of childLayout.boxes) {
3917
+ boxes.set(childId, {
3918
+ ...childBox,
3919
+ x: childBox.x + offsetX,
3920
+ y: childBox.y + offsetY
3921
+ });
3922
+ }
3923
+ groupBoxes.set(groupId, containerBox);
3924
+ boxes.set(groupId, containerBox);
3925
+ }
3926
+ const allContainedIds = /* @__PURE__ */ new Set();
3927
+ for (const [, childIds] of childrenOf) {
3928
+ for (const cid of childIds) allContainedIds.add(cid);
3929
+ }
3930
+ const topLevelNodeIds = /* @__PURE__ */ new Set();
3931
+ for (const node of input.nodes) {
3932
+ if (!allContainedIds.has(node.id)) {
3933
+ topLevelNodeIds.add(node.id);
3934
+ }
3935
+ }
3936
+ const globalNodes = [];
3937
+ for (const nodeId of topLevelNodeIds) {
3938
+ const node = nodeById.get(nodeId);
3939
+ if (node !== void 0) {
3940
+ globalNodes.push({ id: nodeId, size: node.size });
3941
+ }
3942
+ }
3943
+ for (const rootId of rootIds) {
3944
+ const gb = groupBoxes.get(rootId);
3945
+ if (gb !== void 0) {
3946
+ globalNodes.push({
3947
+ id: rootId,
3948
+ size: { width: gb.width, height: gb.height }
3949
+ });
3950
+ }
3951
+ }
3952
+ function rootContainerOf(id) {
3953
+ for (const rootId of rootIds) {
3954
+ const desc = collectDescendants(rootId);
3955
+ if (desc.has(id)) return rootId;
3956
+ }
3957
+ return void 0;
3958
+ }
3959
+ const globalEdges = input.edges.filter((e) => {
3960
+ for (const group of input.groups) {
3961
+ const desc = collectDescendants(group.id);
3962
+ if (desc.has(e.source.nodeId) && desc.has(e.target.nodeId)) {
3963
+ return false;
3964
+ }
3965
+ }
3966
+ return true;
3967
+ }).map((e) => ({
3968
+ id: e.id,
3969
+ sourceId: rootContainerOf(e.source.nodeId) ?? e.source.nodeId,
3970
+ targetId: rootContainerOf(e.target.nodeId) ?? e.target.nodeId
3971
+ }));
3972
+ if (globalNodes.length > 0) {
3973
+ const globalLayout = runDagreInitialLayout({
3974
+ direction: input.direction,
3975
+ nodes: globalNodes,
3976
+ edges: globalEdges,
3977
+ ...input.options === void 0 ? {} : { options: input.options }
3978
+ });
3979
+ diagnostics.push(...globalLayout.diagnostics);
3980
+ for (const [id, box] of globalLayout.boxes) {
3981
+ if (groupBoxes.has(id)) {
3982
+ groupBoxes.set(id, box);
3983
+ boxes.set(id, box);
3984
+ } else if (topLevelNodeIds.has(id)) {
3985
+ boxes.set(id, box);
3986
+ }
3987
+ }
3988
+ for (const groupId of groupOrder) {
3989
+ const containerBox = groupBoxes.get(groupId);
3990
+ if (containerBox === void 0) continue;
3991
+ const offsetX = containerBox.x;
3992
+ const offsetY = containerBox.y;
3993
+ const children = childrenOf.get(groupId) ?? [];
3994
+ for (const childId of children) {
3995
+ const childBox = boxes.get(childId);
3996
+ if (childBox !== void 0) {
3997
+ boxes.set(childId, {
3998
+ ...childBox,
3999
+ x: childBox.x + offsetX,
4000
+ y: childBox.y + offsetY
4001
+ });
4002
+ }
4003
+ translateDescendants(childId, offsetX, offsetY, boxes, childrenOf);
4004
+ }
4005
+ }
4006
+ }
4007
+ return { boxes, groupBoxes, diagnostics };
4008
+ }
4009
+ function translateDescendants(groupId, dx, dy, boxes, childrenOf) {
4010
+ const children = childrenOf.get(groupId) ?? [];
4011
+ for (const childId of children) {
4012
+ const box = boxes.get(childId);
4013
+ if (box !== void 0) {
4014
+ boxes.set(childId, { ...box, x: box.x + dx, y: box.y + dy });
4015
+ }
4016
+ translateDescendants(childId, dx, dy, boxes, childrenOf);
4017
+ }
4018
+ }
4019
+ function topologicalSort(groups, childrenOf, groupIdSet) {
4020
+ const visited = /* @__PURE__ */ new Set();
4021
+ const result = [];
4022
+ function visit(id) {
4023
+ if (visited.has(id)) return;
4024
+ visited.add(id);
4025
+ const children = childrenOf.get(id) ?? [];
4026
+ for (const childId of children) {
4027
+ if (groupIdSet.has(childId)) {
4028
+ visit(childId);
4029
+ }
4030
+ }
4031
+ result.push(id);
4032
+ }
4033
+ for (const group of groups) {
4034
+ visit(group.id);
4035
+ }
4036
+ return result;
4037
+ }
4038
+
4039
+ // src/routing/binary-heap.ts
4040
+ var BinaryHeap = class {
4041
+ _data = [];
4042
+ _nextOrder = 0;
4043
+ get size() {
4044
+ return this._data.length;
4045
+ }
4046
+ push(value, priority) {
4047
+ const entry = { value, priority, order: this._nextOrder++ };
4048
+ this._data.push(entry);
4049
+ this._siftUp(this._data.length - 1);
4050
+ }
4051
+ pop() {
4052
+ if (this._data.length === 0) return void 0;
4053
+ const top = this._data[0];
4054
+ const last = this._data.pop();
4055
+ if (this._data.length > 0) {
4056
+ this._data[0] = last;
4057
+ this._siftDown(0);
4058
+ }
4059
+ return top.value;
4060
+ }
4061
+ peek() {
4062
+ return this._data.length > 0 ? this._data[0].value : void 0;
4063
+ }
4064
+ // -----------------------------------------------------------------------
4065
+ // Internals
4066
+ // -----------------------------------------------------------------------
4067
+ _siftUp(idx) {
4068
+ const entry = this._data[idx];
4069
+ while (idx > 0) {
4070
+ const parentIdx = idx - 1 >> 1;
4071
+ const parent = this._data[parentIdx];
4072
+ if (this._less(entry, parent)) {
4073
+ this._data[idx] = parent;
4074
+ idx = parentIdx;
4075
+ } else {
4076
+ break;
4077
+ }
4078
+ }
4079
+ this._data[idx] = entry;
4080
+ }
4081
+ _siftDown(idx) {
4082
+ const entry = this._data[idx];
4083
+ const size = this._data.length;
4084
+ while (true) {
4085
+ let smallestIdx = idx;
4086
+ const leftIdx = (idx << 1) + 1;
4087
+ const rightIdx = leftIdx + 1;
4088
+ if (leftIdx < size && this._less(
4089
+ this._data[leftIdx],
4090
+ this._data[smallestIdx]
4091
+ )) {
4092
+ smallestIdx = leftIdx;
4093
+ }
4094
+ if (rightIdx < size && this._less(
4095
+ this._data[rightIdx],
4096
+ this._data[smallestIdx]
4097
+ )) {
4098
+ smallestIdx = rightIdx;
4099
+ }
4100
+ if (smallestIdx !== idx) {
4101
+ this._data[idx] = this._data[smallestIdx];
4102
+ idx = smallestIdx;
4103
+ } else {
4104
+ break;
4105
+ }
4106
+ }
4107
+ this._data[idx] = entry;
4108
+ }
4109
+ /**
4110
+ * Two entries are compared first by priority, then by insertion order
4111
+ * when priorities are equal. The insertion-order tie-break makes the
4112
+ * heap deterministic: for a given sequence of {value, priority} pushes,
4113
+ * the extraction order is always the same.
4114
+ */
4115
+ _less(a, b) {
4116
+ if (a.priority !== b.priority) return a.priority < b.priority;
4117
+ return a.order < b.order;
4118
+ }
4119
+ };
4120
+
3667
4121
  // src/routing/astar.ts
3668
4122
  function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
3669
4123
  const margin = options.margin ?? 0;
@@ -3671,8 +4125,17 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
3671
4125
  const segmentPenalty = options.segmentPenalty ?? 1;
3672
4126
  const endpointObstacles = options.endpointObstacles ?? [];
3673
4127
  const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
3674
- const xs = collectXs(source, target, obstacles, margin);
3675
- const ys = collectYs(source, target, obstacles, margin);
4128
+ const useCorridor = options.corridorPrefilter ?? true;
4129
+ const corridorMargin = options.corridorMargin ?? 32;
4130
+ const filtered = useCorridor ? filterObstaclesByCorridor(
4131
+ source,
4132
+ target,
4133
+ obstacles,
4134
+ endpointObstacles,
4135
+ corridorMargin
4136
+ ) : obstacles;
4137
+ const xs = collectXs(source, target, filtered, margin);
4138
+ const ys = collectYs(source, target, filtered, margin);
3676
4139
  if (xs.length * ys.length > maxNodes) {
3677
4140
  diagnostics?.push({
3678
4141
  severity: "warning",
@@ -3687,8 +4150,8 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
3687
4150
  return null;
3688
4151
  }
3689
4152
  const { nodes, nodeIndex } = buildGraph(xs, ys);
3690
- connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
3691
- connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4153
+ connectHorizontalEdges(nodes, ys, filtered, endpointObstacles, margin);
4154
+ connectVerticalEdges(nodes, xs, filtered, endpointObstacles, margin);
3692
4155
  const path = aStarSearch(
3693
4156
  nodes,
3694
4157
  nodeIndex,
@@ -3700,6 +4163,22 @@ function findObstacleFreePath(source, target, obstacles, options = {}, diagnosti
3700
4163
  if (path === null) return null;
3701
4164
  return simplifyRoute(path);
3702
4165
  }
4166
+ function filterObstaclesByCorridor(source, target, obstacles, endpointObstacles, margin) {
4167
+ const cx1 = Math.min(source.x, target.x) - margin;
4168
+ const cx2 = Math.max(source.x, target.x) + margin;
4169
+ const cy1 = Math.min(source.y, target.y) - margin;
4170
+ const cy2 = Math.max(source.y, target.y) + margin;
4171
+ const result = [];
4172
+ for (const obs of obstacles) {
4173
+ if (obs.x + obs.width >= cx1 && obs.x <= cx2 && obs.y + obs.height >= cy1 && obs.y <= cy2) {
4174
+ result.push(obs);
4175
+ }
4176
+ }
4177
+ for (const ep of endpointObstacles) {
4178
+ result.push(ep);
4179
+ }
4180
+ return result;
4181
+ }
3703
4182
  function collectXs(source, target, obstacles, margin) {
3704
4183
  const raw = [];
3705
4184
  for (const obs of obstacles) {
@@ -3843,25 +4322,17 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
3843
4322
  gScore.set(startId, 0);
3844
4323
  const cameFrom = /* @__PURE__ */ new Map();
3845
4324
  const cameFromDir = /* @__PURE__ */ new Map();
3846
- const openSet = [];
3847
- openSet.push({
3848
- id: startId,
3849
- f: manhattan(source, target)
3850
- });
3851
- while (openSet.length > 0) {
3852
- let bestIdx = 0;
3853
- for (let i = 1; i < openSet.length; i++) {
3854
- if (openSet[i].f < openSet[bestIdx].f) {
3855
- bestIdx = i;
3856
- }
3857
- }
3858
- const current = openSet.splice(bestIdx, 1)[0];
3859
- if (current.id === goalId) {
4325
+ const openSet = new BinaryHeap();
4326
+ openSet.push(startId, manhattan(source, target));
4327
+ while (openSet.size > 0) {
4328
+ const currentId = openSet.pop();
4329
+ const currentG = gScore.get(currentId);
4330
+ if (currentG === void 0) continue;
4331
+ if (currentId === goalId) {
3860
4332
  return reconstructPath(nodes, cameFrom, goalId);
3861
4333
  }
3862
- const node = nodes[current.id];
3863
- const currentG = gScore.get(current.id);
3864
- const prevDir = cameFromDir.get(current.id);
4334
+ const node = nodes[currentId];
4335
+ const prevDir = cameFromDir.get(currentId);
3865
4336
  for (const [neighborId, edgeCost] of node.neighbors) {
3866
4337
  const neighbor = nodes[neighborId];
3867
4338
  const tentativeG = currentG + edgeCost * segmentPenalty;
@@ -3871,10 +4342,10 @@ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenal
3871
4342
  const existingG = gScore.get(neighborId);
3872
4343
  if (existingG === void 0 || totalG < existingG) {
3873
4344
  gScore.set(neighborId, totalG);
3874
- cameFrom.set(neighborId, current.id);
4345
+ cameFrom.set(neighborId, currentId);
3875
4346
  cameFromDir.set(neighborId, newDir);
3876
4347
  const f = totalG + manhattan(neighbor, target);
3877
- openSet.push({ id: neighborId, f });
4348
+ openSet.push(neighborId, f);
3878
4349
  }
3879
4350
  }
3880
4351
  }
@@ -3911,6 +4382,292 @@ function areCollinear(a, b, c) {
3911
4382
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
3912
4383
  }
3913
4384
 
4385
+ // src/routing/visibility-router.ts
4386
+ function findCornerGraphPath(source, target, obstacles, options = {}, diagnostics) {
4387
+ const margin = options.margin ?? 0;
4388
+ const turnPenalty = options.turnPenalty ?? 50;
4389
+ const segmentPenalty = options.segmentPenalty ?? 1;
4390
+ const endpointObstacles = options.endpointObstacles ?? [];
4391
+ const maxCorners = options.maxCorners ?? 300;
4392
+ const vertices = collectCornerVertices(source, target, obstacles, margin);
4393
+ if (vertices.length > maxCorners) {
4394
+ diagnostics?.push({
4395
+ severity: "warning",
4396
+ code: "routing.visibility.corner_overflow",
4397
+ message: `Corner graph overflow: ${vertices.length} vertices > ${maxCorners}. Falling back to grid A*.`,
4398
+ detail: { vertexCount: vertices.length, maxCorners }
4399
+ });
4400
+ return null;
4401
+ }
4402
+ if (obstacles.length === 0) {
4403
+ return simplifyRoute2([source, target]);
4404
+ }
4405
+ const expandedObstacles = margin === 0 ? obstacles : obstacles.map((o) => expandBox2(o, margin));
4406
+ const edges = buildVisibilityEdges(
4407
+ vertices,
4408
+ expandedObstacles,
4409
+ endpointObstacles
4410
+ );
4411
+ const path = aStarSearch2(
4412
+ vertices,
4413
+ edges,
4414
+ source,
4415
+ target,
4416
+ segmentPenalty,
4417
+ turnPenalty
4418
+ );
4419
+ if (path === null) return null;
4420
+ return simplifyRoute2(path, expandedObstacles);
4421
+ }
4422
+ function collectCornerVertices(source, target, obstacles, margin = 0) {
4423
+ const vertices = [
4424
+ { point: { x: source.x, y: source.y }, obstacleIndex: -1 },
4425
+ { point: { x: target.x, y: target.y }, obstacleIndex: -1 }
4426
+ ];
4427
+ const seen = /* @__PURE__ */ new Set();
4428
+ seen.add(`${source.x},${source.y}`);
4429
+ seen.add(`${target.x},${target.y}`);
4430
+ const addVertex = (p, obstacleIndex) => {
4431
+ const key = `${p.x},${p.y}`;
4432
+ if (seen.has(key)) return;
4433
+ seen.add(key);
4434
+ vertices.push({ point: p, obstacleIndex });
4435
+ };
4436
+ for (let i = 0; i < obstacles.length; i++) {
4437
+ const obs = obstacles[i];
4438
+ const c0 = { x: obs.x - margin, y: obs.y - margin };
4439
+ const c1 = { x: obs.x + obs.width + margin, y: obs.y - margin };
4440
+ const c2 = {
4441
+ x: obs.x + obs.width + margin,
4442
+ y: obs.y + obs.height + margin
4443
+ };
4444
+ const c3 = { x: obs.x - margin, y: obs.y + obs.height + margin };
4445
+ for (const c of [c0, c1, c2, c3]) {
4446
+ addVertex(c, i);
4447
+ }
4448
+ }
4449
+ for (const p of [source, target]) {
4450
+ for (let i = 0; i < obstacles.length; i++) {
4451
+ const obs = obstacles[i];
4452
+ addVertex({ x: obs.x - margin, y: p.y }, i);
4453
+ addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
4454
+ addVertex({ x: p.x, y: obs.y - margin }, i);
4455
+ addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
4456
+ if (p.x > obs.x && p.x < obs.x + obs.width) {
4457
+ addVertex({ x: p.x, y: obs.y - margin }, i);
4458
+ addVertex({ x: p.x, y: obs.y + obs.height + margin }, i);
4459
+ }
4460
+ if (p.y > obs.y && p.y < obs.y + obs.height) {
4461
+ addVertex({ x: obs.x - margin, y: p.y }, i);
4462
+ addVertex({ x: obs.x + obs.width + margin, y: p.y }, i);
4463
+ }
4464
+ }
4465
+ }
4466
+ return vertices;
4467
+ }
4468
+ function buildVisibilityEdges(vertices, obstacles, endpointObstacles) {
4469
+ const edges = [];
4470
+ for (let i = 0; i < vertices.length; i++) {
4471
+ const v = vertices[i];
4472
+ const right = visibleInDirection(
4473
+ v,
4474
+ vertices,
4475
+ obstacles,
4476
+ endpointObstacles,
4477
+ "right"
4478
+ );
4479
+ const left = visibleInDirection(
4480
+ v,
4481
+ vertices,
4482
+ obstacles,
4483
+ endpointObstacles,
4484
+ "left"
4485
+ );
4486
+ const down = visibleInDirection(
4487
+ v,
4488
+ vertices,
4489
+ obstacles,
4490
+ endpointObstacles,
4491
+ "down"
4492
+ );
4493
+ const up = visibleInDirection(
4494
+ v,
4495
+ vertices,
4496
+ obstacles,
4497
+ endpointObstacles,
4498
+ "up"
4499
+ );
4500
+ for (const neighbor of [right, left, down, up]) {
4501
+ if (neighbor !== null && neighbor.index > i) {
4502
+ edges.push({
4503
+ from: i,
4504
+ to: neighbor.index,
4505
+ cost: neighbor.distance
4506
+ });
4507
+ }
4508
+ }
4509
+ }
4510
+ return edges;
4511
+ }
4512
+ function visibleInDirection(origin, vertices, obstacles, endpointObstacles, dir) {
4513
+ const candidates = [];
4514
+ for (let i = 0; i < vertices.length; i++) {
4515
+ const v = vertices[i];
4516
+ const dx = v.point.x - origin.point.x;
4517
+ const dy = v.point.y - origin.point.y;
4518
+ switch (dir) {
4519
+ case "right":
4520
+ if (dx > 0 && dy === 0) candidates.push({ index: i, dist: dx });
4521
+ break;
4522
+ case "left":
4523
+ if (dx < 0 && dy === 0) candidates.push({ index: i, dist: -dx });
4524
+ break;
4525
+ case "down":
4526
+ if (dy > 0 && dx === 0) candidates.push({ index: i, dist: dy });
4527
+ break;
4528
+ case "up":
4529
+ if (dy < 0 && dx === 0) candidates.push({ index: i, dist: -dy });
4530
+ break;
4531
+ }
4532
+ }
4533
+ candidates.sort((a, b) => a.dist - b.dist);
4534
+ for (const c of candidates) {
4535
+ if (isSegmentVisible(
4536
+ origin.point,
4537
+ vertices[c.index].point,
4538
+ obstacles,
4539
+ endpointObstacles,
4540
+ origin.obstacleIndex,
4541
+ vertices[c.index].obstacleIndex
4542
+ )) {
4543
+ return { index: c.index, distance: c.dist };
4544
+ }
4545
+ }
4546
+ return null;
4547
+ }
4548
+ function isSegmentVisible(a, b, obstacles, endpointObstacles, _aObsIdx, _bObsIdx) {
4549
+ for (let i = 0; i < obstacles.length; i++) {
4550
+ if (segmentEntersBox(a, b, obstacles[i])) return false;
4551
+ }
4552
+ for (const ep of endpointObstacles) {
4553
+ if (segmentEntersBox(a, b, ep)) return false;
4554
+ }
4555
+ return true;
4556
+ }
4557
+ function segmentEntersBox(start, end, box) {
4558
+ const left = box.x;
4559
+ const right = box.x + box.width;
4560
+ const top = box.y;
4561
+ const bottom = box.y + box.height;
4562
+ const inside = (p) => p.x > left && p.x < right && p.y > top && p.y < bottom;
4563
+ if (inside(start) || inside(end)) return true;
4564
+ if (start.x === end.x) {
4565
+ return start.x > left && start.x < right && Math.max(start.y, end.y) > top && Math.min(start.y, end.y) < bottom;
4566
+ }
4567
+ if (start.y === end.y) {
4568
+ return start.y > top && start.y < bottom && Math.max(start.x, end.x) > left && Math.min(start.x, end.x) < right;
4569
+ }
4570
+ return false;
4571
+ }
4572
+ function expandBox2(box, margin) {
4573
+ return {
4574
+ x: box.x - margin,
4575
+ y: box.y - margin,
4576
+ width: box.width + margin * 2,
4577
+ height: box.height + margin * 2
4578
+ };
4579
+ }
4580
+ function aStarSearch2(vertices, edges, source, target, segmentPenalty, turnPenalty) {
4581
+ const startId = 0;
4582
+ const goalId = 1;
4583
+ const gScore = /* @__PURE__ */ new Map();
4584
+ gScore.set(startId, 0);
4585
+ const cameFrom = /* @__PURE__ */ new Map();
4586
+ const cameFromDir = /* @__PURE__ */ new Map();
4587
+ const openSet = new BinaryHeap();
4588
+ openSet.push(startId, manhattan2(source, target));
4589
+ const neighborMap = /* @__PURE__ */ new Map();
4590
+ for (const e of edges) {
4591
+ let list = neighborMap.get(e.from);
4592
+ if (list === void 0) {
4593
+ list = [];
4594
+ neighborMap.set(e.from, list);
4595
+ }
4596
+ list.push({ to: e.to, cost: e.cost });
4597
+ list = neighborMap.get(e.to);
4598
+ if (list === void 0) {
4599
+ list = [];
4600
+ neighborMap.set(e.to, list);
4601
+ }
4602
+ list.push({ to: e.from, cost: e.cost });
4603
+ }
4604
+ while (openSet.size > 0) {
4605
+ const currentId = openSet.pop();
4606
+ const currentG = gScore.get(currentId);
4607
+ if (currentG === void 0) continue;
4608
+ if (currentId === goalId) {
4609
+ return reconstructPath2(vertices, cameFrom, goalId);
4610
+ }
4611
+ const prevDir = cameFromDir.get(currentId);
4612
+ const neighbors = neighborMap.get(currentId) ?? [];
4613
+ for (const { to, cost } of neighbors) {
4614
+ const tentativeG = currentG + cost * segmentPenalty;
4615
+ const toV = vertices[to];
4616
+ const curV = vertices[currentId];
4617
+ const newDir = toV.point.y === curV.point.y ? "h" : "v";
4618
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4619
+ const totalG = tentativeG + turnCost;
4620
+ const existingG = gScore.get(to);
4621
+ if (existingG === void 0 || totalG < existingG) {
4622
+ gScore.set(to, totalG);
4623
+ cameFrom.set(to, currentId);
4624
+ cameFromDir.set(to, newDir);
4625
+ const f = totalG + manhattan2(toV.point, target);
4626
+ openSet.push(to, f);
4627
+ }
4628
+ }
4629
+ }
4630
+ return null;
4631
+ }
4632
+ function manhattan2(a, b) {
4633
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4634
+ }
4635
+ function reconstructPath2(vertices, cameFrom, goalId) {
4636
+ const path = [];
4637
+ let current = goalId;
4638
+ while (current !== void 0) {
4639
+ const v = vertices[current];
4640
+ path.unshift({ x: v.point.x, y: v.point.y });
4641
+ current = cameFrom.get(current);
4642
+ }
4643
+ return path;
4644
+ }
4645
+ function simplifyRoute2(points, obstacles = []) {
4646
+ if (points.length <= 2) return [...points];
4647
+ const result = [points[0]];
4648
+ for (let i = 1; i < points.length - 1; i++) {
4649
+ const prev = result[result.length - 1];
4650
+ const curr = points[i];
4651
+ const next = points[i + 1];
4652
+ const collinear = prev.x === curr.x && curr.x === next.x || prev.y === curr.y && curr.y === next.y;
4653
+ if (!collinear) {
4654
+ result.push(curr);
4655
+ continue;
4656
+ }
4657
+ if (segmentCrossesAnyObstacle(prev, next, obstacles)) {
4658
+ result.push(curr);
4659
+ }
4660
+ }
4661
+ result.push(points[points.length - 1]);
4662
+ return result;
4663
+ }
4664
+ function segmentCrossesAnyObstacle(a, b, obstacles) {
4665
+ for (const obs of obstacles) {
4666
+ if (segmentEntersBox(a, b, obs)) return true;
4667
+ }
4668
+ return false;
4669
+ }
4670
+
3914
4671
  // src/routing/routes.ts
3915
4672
  function checkBacktracking(points, source, target, diagnostics) {
3916
4673
  if (points.length < 2) return;
@@ -4000,13 +4757,18 @@ function routeEdge(input) {
4000
4757
  input.source.center,
4001
4758
  targetAnchor
4002
4759
  );
4003
- const path = findObstacleFreePath(
4760
+ const cornerPath = findCornerGraphPath(
4004
4761
  source,
4005
4762
  target,
4006
4763
  [...softObstacles, ...hardObstacles],
4007
- {
4008
- endpointObstacles
4009
- },
4764
+ { endpointObstacles, margin: 2 },
4765
+ diagnostics
4766
+ );
4767
+ const path = cornerPath ?? findObstacleFreePath(
4768
+ source,
4769
+ target,
4770
+ [...softObstacles, ...hardObstacles],
4771
+ { endpointObstacles, margin: 0 },
4010
4772
  diagnostics
4011
4773
  );
4012
4774
  if (path !== null && path.length >= 2) {
@@ -4249,7 +5011,7 @@ function routeEdge(input) {
4249
5011
  };
4250
5012
  }
4251
5013
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
4252
- const simplified = simplifyRoute2(points);
5014
+ const simplified = simplifyRoute3(points);
4253
5015
  if (simplified.length >= 3) {
4254
5016
  return simplified;
4255
5017
  }
@@ -4553,7 +5315,7 @@ function squaredDistance2(a, b) {
4553
5315
  const dy = a.y - b.y;
4554
5316
  return dx * dx + dy * dy;
4555
5317
  }
4556
- function simplifyRoute2(points) {
5318
+ function simplifyRoute3(points) {
4557
5319
  const withoutDuplicates = [];
4558
5320
  for (const point2 of points) {
4559
5321
  const previous = withoutDuplicates.at(-1);
@@ -4893,7 +5655,21 @@ function solveDiagram(diagram, options = {}) {
4893
5655
  );
4894
5656
  const constraints = stableByConstraintId(diagram.constraints);
4895
5657
  const initialLayoutMode = options.initialLayout ?? "dagre";
4896
- const layout2 = runInitialLayout({
5658
+ const useRecursive = options.recursiveLayout === true;
5659
+ if (useRecursive && initialLayoutMode === "positions") {
5660
+ diagnostics.push({
5661
+ severity: "warning",
5662
+ code: "layout.recursive-ignores-positions",
5663
+ message: 'recursiveLayout overrides initialLayout "positions" \u2014 seed positions are ignored for bottom-up container layout.'
5664
+ });
5665
+ }
5666
+ const layout2 = useRecursive ? runRecursiveContainerLayout({
5667
+ direction: diagram.direction,
5668
+ nodes: styledNodes,
5669
+ groups: styledGroups,
5670
+ edges: styledEdges,
5671
+ constraints
5672
+ }) : runInitialLayout({
4897
5673
  mode: initialLayoutMode,
4898
5674
  componentAware: options.maxStackDepth === void 0,
4899
5675
  direction: diagram.direction,
@@ -4909,12 +5685,32 @@ function solveDiagram(diagram, options = {}) {
4909
5685
  options,
4910
5686
  diagnostics
4911
5687
  );
5688
+ if ((diagram.direction === "TB" || diagram.direction === "BT") && options.maxRowDepth !== void 0) {
5689
+ const rewrapped = wrapHorizontalStackIfNeeded(
5690
+ initialNodeBoxes,
5691
+ styledNodes,
5692
+ diagram.direction,
5693
+ options,
5694
+ diagnostics
5695
+ );
5696
+ for (const [id, box] of rewrapped) {
5697
+ initialNodeBoxes.set(id, box);
5698
+ }
5699
+ }
5700
+ if (useRecursive && "groupBoxes" in layout2) {
5701
+ const recursiveLayout = layout2;
5702
+ for (const [groupId, groupBox] of recursiveLayout.groupBoxes) {
5703
+ initialNodeBoxes.set(groupId, groupBox);
5704
+ }
5705
+ }
4912
5706
  expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4913
5707
  const constrained = applyLayoutConstraints({
4914
5708
  direction: diagram.direction,
4915
5709
  overlapSpacing: options?.overlapSpacing ?? 40,
4916
5710
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4917
5711
  distributeContainedChildren: options.distributeContainedChildren ?? true,
5712
+ distributeSwimlaneChildren: options.distributeSwimlaneChildren ?? "spread",
5713
+ swimlanes: styledSwimlanes,
4918
5714
  boxes: initialNodeBoxes,
4919
5715
  nodes: styledNodes,
4920
5716
  constraints
@@ -5574,7 +6370,7 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
5574
6370
  );
5575
6371
  return wrapped;
5576
6372
  }
5577
- if (edges.length > 0 || !isVerticalRunaway(wrapped, nodes, direction, options)) {
6373
+ if (edges.length > 0 || !isStackRunaway(wrapped, nodes, direction, options)) {
5578
6374
  reportVerticalRunaway(
5579
6375
  wrapped,
5580
6376
  nodes,
@@ -5625,8 +6421,57 @@ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diag
5625
6421
  });
5626
6422
  return wrapped;
5627
6423
  }
6424
+ function wrapHorizontalStackIfNeeded(boxes, nodes, direction, options, diagnostics) {
6425
+ if (!isStackRunaway(boxes, nodes, direction, options)) {
6426
+ return new Map(boxes);
6427
+ }
6428
+ const maxRowDepth = options.maxRowDepth;
6429
+ if (maxRowDepth === void 0 || nodes.length <= maxRowDepth) {
6430
+ return new Map(boxes);
6431
+ }
6432
+ const ordered = [...nodes].sort((a, b) => {
6433
+ const ba = boxes.get(a.id);
6434
+ const bb = boxes.get(b.id);
6435
+ if (ba === void 0 || bb === void 0) return 0;
6436
+ const dx = ba.x - bb.x;
6437
+ return dx !== 0 ? dx : ba.y - bb.y;
6438
+ });
6439
+ const rows = Math.ceil(ordered.length / maxRowDepth);
6440
+ const wrapped = new Map(boxes);
6441
+ const rowSpacing = options.overlapSpacing ?? 40;
6442
+ let minX = Infinity;
6443
+ let minY = Infinity;
6444
+ let maxH = 0;
6445
+ for (const n of ordered) {
6446
+ const b = boxes.get(n.id);
6447
+ if (b !== void 0) {
6448
+ minX = Math.min(minX, b.x);
6449
+ minY = Math.min(minY, b.y);
6450
+ maxH = Math.max(maxH, b.height);
6451
+ }
6452
+ }
6453
+ for (let ri = 0; ri < rows; ri++) {
6454
+ const rowNodes = ordered.slice(ri * maxRowDepth, (ri + 1) * maxRowDepth);
6455
+ let x = minX;
6456
+ const y = minY + ri * (maxH + rowSpacing);
6457
+ for (const node of rowNodes) {
6458
+ const box = boxes.get(node.id);
6459
+ if (box === void 0) continue;
6460
+ wrapped.set(node.id, { ...box, x, y });
6461
+ x += box.width + rowSpacing;
6462
+ }
6463
+ }
6464
+ diagnostics.push({
6465
+ severity: "warning",
6466
+ code: "horizontal_runaway",
6467
+ message: `Single-row layout exceeded maxRowDepth ${maxRowDepth}; wrapped into ${rows} rows.`,
6468
+ path: ["nodes"],
6469
+ detail: { nodeCount: ordered.length, maxRowDepth, rows }
6470
+ });
6471
+ return wrapped;
6472
+ }
5628
6473
  function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
5629
- if (!isVerticalRunaway(boxes, nodes, direction, options)) {
6474
+ if (!isStackRunaway(boxes, nodes, direction, options)) {
5630
6475
  return;
5631
6476
  }
5632
6477
  diagnostics.push({
@@ -5642,7 +6487,7 @@ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnost
5642
6487
  }
5643
6488
  });
5644
6489
  }
5645
- function isVerticalRunaway(boxes, nodes, direction, options) {
6490
+ function isStackRunaway(boxes, nodes, direction, options) {
5646
6491
  if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
5647
6492
  return false;
5648
6493
  }
@@ -6233,7 +7078,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6233
7078
  });
6234
7079
  continue;
6235
7080
  }
6236
- const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
7081
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting, diagnostics);
6237
7082
  const geometry = computeShapeGeometry({
6238
7083
  shape: node.shape,
6239
7084
  box,
@@ -6327,7 +7172,7 @@ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6327
7172
  }
6328
7173
  }
6329
7174
  }
6330
- function coordinatePorts(node, nodeBox, portShifting) {
7175
+ function coordinatePorts(node, nodeBox, portShifting, diagnostics) {
6331
7176
  const portsBySide = /* @__PURE__ */ new Map();
6332
7177
  for (const port of node.ports ?? []) {
6333
7178
  const ports = portsBySide.get(port.side) ?? [];
@@ -6350,7 +7195,9 @@ function coordinatePorts(node, nodeBox, portShifting) {
6350
7195
  side,
6351
7196
  index,
6352
7197
  sorted.length,
6353
- portShifting
7198
+ portShifting,
7199
+ diagnostics,
7200
+ node.id
6354
7201
  );
6355
7202
  const box = portBox(anchor);
6356
7203
  coordinated.push({ ...port, box, anchor });
@@ -6358,16 +7205,32 @@ function coordinatePorts(node, nodeBox, portShifting) {
6358
7205
  }
6359
7206
  return coordinated.sort((a, b) => a.id.localeCompare(b.id));
6360
7207
  }
6361
- function portAnchor(nodeBox, side, index, count, portShifting) {
7208
+ function portAnchor(nodeBox, side, index, count, portShifting, diagnostics, nodeId) {
6362
7209
  const shiftingEnabled = portShifting?.enabled ?? true;
6363
7210
  const requestedSpacing = portShifting?.spacing ?? 24;
6364
7211
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6365
7212
  const availableSpan = 2 * maxOffset;
6366
7213
  const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6367
- const spacing = shiftingEnabled && count > 1 ? Math.max(
7214
+ const effectiveSpacing = shiftingEnabled && count > 1 ? Math.max(
6368
7215
  Math.min(requestedSpacing, availableSpan / (count - 1)),
6369
7216
  minSpacing
6370
7217
  ) : requestedSpacing;
7218
+ if (shiftingEnabled && count > 1 && effectiveSpacing < requestedSpacing && diagnostics !== void 0 && nodeId !== void 0) {
7219
+ diagnostics.push({
7220
+ severity: "warning",
7221
+ code: "port_constraint_overlap",
7222
+ message: `Port spacing on ${nodeId} ${side} compressed from ${requestedSpacing}px to ${Math.round(effectiveSpacing)}px for ${count} ports.`,
7223
+ path: ["nodes", nodeId, "ports"],
7224
+ detail: {
7225
+ nodeId,
7226
+ side,
7227
+ requestedSpacing,
7228
+ effectiveSpacing: Math.round(effectiveSpacing),
7229
+ portCount: count
7230
+ }
7231
+ });
7232
+ }
7233
+ const spacing = effectiveSpacing;
6371
7234
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6372
7235
  switch (side) {
6373
7236
  case "left":