@nodius/layouting 0.1.0 → 0.1.1

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.
Files changed (38) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +280 -126
  3. package/dist/algorithms/component-packing.d.ts +9 -0
  4. package/dist/algorithms/component-packing.d.ts.map +1 -0
  5. package/dist/algorithms/coordinate-assignment.d.ts +7 -0
  6. package/dist/algorithms/coordinate-assignment.d.ts.map +1 -0
  7. package/dist/algorithms/crossing-minimization.d.ts +7 -0
  8. package/dist/algorithms/crossing-minimization.d.ts.map +1 -0
  9. package/dist/algorithms/cycle-breaking.d.ts +8 -0
  10. package/dist/algorithms/cycle-breaking.d.ts.map +1 -0
  11. package/dist/algorithms/edge-routing.d.ts +17 -0
  12. package/dist/algorithms/edge-routing.d.ts.map +1 -0
  13. package/dist/algorithms/layer-assignment.d.ts +20 -0
  14. package/dist/algorithms/layer-assignment.d.ts.map +1 -0
  15. package/dist/algorithms/value-cluster.d.ts +15 -0
  16. package/dist/algorithms/value-cluster.d.ts.map +1 -0
  17. package/dist/algorithms/value-placement.d.ts +25 -0
  18. package/dist/algorithms/value-placement.d.ts.map +1 -0
  19. package/dist/debug.d.ts +20 -0
  20. package/dist/debug.d.ts.map +1 -0
  21. package/dist/graph.d.ts +50 -0
  22. package/dist/graph.d.ts.map +1 -0
  23. package/dist/incremental.d.ts +33 -0
  24. package/dist/incremental.d.ts.map +1 -0
  25. package/dist/index.d.ts +7 -176
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +904 -149
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +901 -148
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/layout.d.ts +10 -0
  32. package/dist/layout.d.ts.map +1 -0
  33. package/dist/proposals.d.ts +31 -0
  34. package/dist/proposals.d.ts.map +1 -0
  35. package/dist/types.d.ts +155 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/package.json +5 -4
  38. package/dist/index.d.mts +0 -176
package/dist/index.mjs CHANGED
@@ -6,7 +6,12 @@ function resolveOptions(options) {
6
6
  layerSpacing: options?.layerSpacing ?? 60,
7
7
  crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
8
8
  coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
9
- edgeMargin: options?.edgeMargin ?? 20
9
+ edgeMargin: options?.edgeMargin ?? 20,
10
+ controlWeight: options?.edgeWeights?.control ?? 1,
11
+ dataWeight: options?.edgeWeights?.data ?? 0.25,
12
+ packComponents: options?.packComponents ?? true,
13
+ compoundPadding: options?.compoundPadding ?? 24,
14
+ onProposal: options?.onProposal
10
15
  };
11
16
  }
12
17
 
@@ -69,7 +74,7 @@ var Graph = class {
69
74
  return result;
70
75
  }
71
76
  };
72
- function buildGraph(nodes, edges) {
77
+ function buildGraph(nodes, edges, ctx = { controlWeight: 1, dataWeight: 0.25 }) {
73
78
  const graph = new Graph();
74
79
  for (const node of nodes) {
75
80
  graph.addNode({
@@ -81,10 +86,15 @@ function buildGraph(nodes, edges) {
81
86
  layer: -1,
82
87
  order: -1,
83
88
  x: 0,
84
- y: 0
89
+ y: 0,
90
+ parentId: node.parentId,
91
+ isCompound: false,
92
+ isValue: false
85
93
  });
86
94
  }
87
95
  for (const edge of edges) {
96
+ const kind = edge.kind ?? "control";
97
+ const defaultWeight = kind === "control" ? ctx.controlWeight : ctx.dataWeight;
88
98
  graph.addEdge({
89
99
  id: edge.id,
90
100
  from: edge.from,
@@ -92,9 +102,34 @@ function buildGraph(nodes, edges) {
92
102
  fromHandle: edge.fromHandle,
93
103
  toHandle: edge.toHandle,
94
104
  reversed: false,
95
- originalId: edge.id
105
+ originalId: edge.id,
106
+ kind,
107
+ weight: edge.weight ?? defaultWeight
96
108
  });
97
109
  }
110
+ for (const [nodeId, node] of graph.nodes) {
111
+ const ins = graph.inEdges.get(nodeId);
112
+ const outs = graph.outEdges.get(nodeId);
113
+ let hasControl = false;
114
+ if (ins) {
115
+ for (const eid of ins) {
116
+ if (graph.edges.get(eid)?.kind === "control") {
117
+ hasControl = true;
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ if (!hasControl && outs) {
123
+ for (const eid of outs) {
124
+ if (graph.edges.get(eid)?.kind === "control") {
125
+ hasControl = true;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ const incidentCount = (ins?.size ?? 0) + (outs?.size ?? 0);
131
+ node.isValue = !hasControl && incidentCount > 0;
132
+ }
98
133
  return graph;
99
134
  }
100
135
  function getHandlePosition(node, handleId) {
@@ -182,28 +217,88 @@ function breakCycles(graph) {
182
217
  function assignLayers(graph) {
183
218
  const layers = /* @__PURE__ */ new Map();
184
219
  const visiting = /* @__PURE__ */ new Set();
185
- function computeLayer(nodeId) {
220
+ function controlLayer(nodeId) {
186
221
  if (layers.has(nodeId)) return layers.get(nodeId);
187
222
  if (visiting.has(nodeId)) return 0;
223
+ const node = graph.nodes.get(nodeId);
224
+ if (!node || node.isValue) return -1;
188
225
  visiting.add(nodeId);
189
- const preds = graph.predecessors(nodeId);
226
+ const inEdgeIds = graph.inEdges.get(nodeId);
190
227
  let maxPredLayer = -1;
191
- for (const pred of preds) {
192
- maxPredLayer = Math.max(maxPredLayer, computeLayer(pred));
228
+ if (inEdgeIds) {
229
+ for (const eid of inEdgeIds) {
230
+ const edge = graph.edges.get(eid);
231
+ if (!edge || edge.kind !== "control") continue;
232
+ const pred = graph.nodes.get(edge.from);
233
+ if (!pred || pred.isValue) continue;
234
+ maxPredLayer = Math.max(maxPredLayer, controlLayer(edge.from));
235
+ }
193
236
  }
194
237
  const layer = maxPredLayer + 1;
195
238
  layers.set(nodeId, layer);
196
- const node = graph.nodes.get(nodeId);
197
- if (node) node.layer = layer;
239
+ node.layer = layer;
198
240
  visiting.delete(nodeId);
199
241
  return layer;
200
242
  }
201
- for (const nodeId of graph.nodes.keys()) {
202
- computeLayer(nodeId);
243
+ for (const [nodeId, node] of graph.nodes) {
244
+ if (!node.isValue) controlLayer(nodeId);
245
+ }
246
+ for (const [nodeId, node] of graph.nodes) {
247
+ if (!node.isValue) continue;
248
+ const neighborLayers = [];
249
+ const inEdgeIds = graph.inEdges.get(nodeId);
250
+ if (inEdgeIds) {
251
+ for (const eid of inEdgeIds) {
252
+ const edge = graph.edges.get(eid);
253
+ if (!edge) continue;
254
+ const nbr = graph.nodes.get(edge.from);
255
+ if (nbr && !nbr.isValue && layers.has(edge.from)) {
256
+ neighborLayers.push(layers.get(edge.from));
257
+ }
258
+ }
259
+ }
260
+ const outEdgeIds = graph.outEdges.get(nodeId);
261
+ if (outEdgeIds) {
262
+ for (const eid of outEdgeIds) {
263
+ const edge = graph.edges.get(eid);
264
+ if (!edge) continue;
265
+ const nbr = graph.nodes.get(edge.to);
266
+ if (nbr && !nbr.isValue && layers.has(edge.to)) {
267
+ neighborLayers.push(layers.get(edge.to));
268
+ }
269
+ }
270
+ }
271
+ let layer;
272
+ if (neighborLayers.length > 0) {
273
+ neighborLayers.sort((a, b) => a - b);
274
+ layer = neighborLayers[Math.floor(neighborLayers.length / 2)];
275
+ } else {
276
+ layer = 0;
277
+ }
278
+ layers.set(nodeId, layer);
279
+ node.layer = layer;
203
280
  }
204
- let maxLayer = 0;
205
- for (const layer of layers.values()) {
206
- maxLayer = Math.max(maxLayer, layer);
281
+ for (const [nodeId, node] of graph.nodes) {
282
+ if (!layers.has(nodeId)) {
283
+ layers.set(nodeId, 0);
284
+ node.layer = 0;
285
+ }
286
+ }
287
+ let minLayer = Infinity;
288
+ let maxLayer = -Infinity;
289
+ for (const l of layers.values()) {
290
+ if (l < minLayer) minLayer = l;
291
+ if (l > maxLayer) maxLayer = l;
292
+ }
293
+ if (!isFinite(minLayer)) return [];
294
+ if (minLayer !== 0) {
295
+ for (const [id, l] of layers) {
296
+ const adj = l - minLayer;
297
+ layers.set(id, adj);
298
+ const n = graph.nodes.get(id);
299
+ if (n) n.layer = adj;
300
+ }
301
+ maxLayer -= minLayer;
207
302
  }
208
303
  const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
209
304
  for (const [nodeId, layer] of layers) {
@@ -215,6 +310,7 @@ function insertDummyNodes(graph, layers) {
215
310
  let dummyCounter = 0;
216
311
  const edgesToProcess = [...graph.edges.values()];
217
312
  for (const edge of edgesToProcess) {
313
+ if (edge.kind !== "control") continue;
218
314
  const fromNode = graph.nodes.get(edge.from);
219
315
  const toNode = graph.nodes.get(edge.to);
220
316
  if (!fromNode || !toNode) continue;
@@ -239,7 +335,9 @@ function insertDummyNodes(graph, layers) {
239
335
  layer: l,
240
336
  order: -1,
241
337
  x: 0,
242
- y: 0
338
+ y: 0,
339
+ isCompound: false,
340
+ isValue: false
243
341
  });
244
342
  layers[l].push(dummyId);
245
343
  graph.addEdge({
@@ -249,7 +347,9 @@ function insertDummyNodes(graph, layers) {
249
347
  fromHandle: prevHandleId,
250
348
  toHandle: "in",
251
349
  reversed: edge.reversed,
252
- originalId: edge.originalId
350
+ originalId: edge.originalId,
351
+ kind: edge.kind,
352
+ weight: edge.weight
253
353
  });
254
354
  prevNodeId = dummyId;
255
355
  prevHandleId = "out";
@@ -261,7 +361,9 @@ function insertDummyNodes(graph, layers) {
261
361
  fromHandle: prevHandleId,
262
362
  toHandle: edge.toHandle,
263
363
  reversed: edge.reversed,
264
- originalId: edge.originalId
364
+ originalId: edge.originalId,
365
+ kind: edge.kind,
366
+ weight: edge.weight
265
367
  });
266
368
  }
267
369
  return layers;
@@ -682,6 +784,176 @@ function getOrderSize(node, isHorizontal) {
682
784
  return isHorizontal ? node.height : node.width;
683
785
  }
684
786
 
787
+ // src/algorithms/value-placement.ts
788
+ function placeValueSidecars(graph, layers, options) {
789
+ const isHorizontal = options.direction === "LR" || options.direction === "RL";
790
+ const attachments = [];
791
+ const orphanValues = [];
792
+ for (const [nodeId, node] of graph.nodes) {
793
+ if (!node.isValue || node.isDummy) continue;
794
+ const consumer = findDominantConsumer(graph, nodeId);
795
+ if (consumer) {
796
+ attachments.push({ valueId: nodeId, consumerId: consumer });
797
+ } else {
798
+ orphanValues.push(nodeId);
799
+ }
800
+ }
801
+ const byConsumer = /* @__PURE__ */ new Map();
802
+ for (const { valueId, consumerId } of attachments) {
803
+ const arr = byConsumer.get(consumerId) ?? [];
804
+ arr.push(valueId);
805
+ byConsumer.set(consumerId, arr);
806
+ }
807
+ for (const [consumerId, vIds] of byConsumer) {
808
+ const consumer = graph.nodes.get(consumerId);
809
+ if (!consumer) continue;
810
+ const { beforeSide, afterSide } = splitValuesBySide(graph, consumerId, vIds, isHorizontal);
811
+ if (isHorizontal) {
812
+ const cxCenter = consumer.x + consumer.width / 2;
813
+ let topEdge = consumer.y;
814
+ for (const vid of beforeSide) {
815
+ const v = graph.nodes.get(vid);
816
+ if (!v) continue;
817
+ v.y = topEdge - options.nodeSpacing - v.height;
818
+ v.x = cxCenter - v.width / 2;
819
+ topEdge = v.y;
820
+ }
821
+ let bottomEdge = consumer.y + consumer.height;
822
+ for (const vid of afterSide) {
823
+ const v = graph.nodes.get(vid);
824
+ if (!v) continue;
825
+ v.y = bottomEdge + options.nodeSpacing;
826
+ v.x = cxCenter - v.width / 2;
827
+ bottomEdge = v.y + v.height;
828
+ }
829
+ } else {
830
+ const cyCenter = consumer.y + consumer.height / 2;
831
+ let leftEdge = consumer.x;
832
+ for (const vid of beforeSide) {
833
+ const v = graph.nodes.get(vid);
834
+ if (!v) continue;
835
+ v.x = leftEdge - options.nodeSpacing - v.width;
836
+ v.y = cyCenter - v.height / 2;
837
+ leftEdge = v.x;
838
+ }
839
+ let rightEdge = consumer.x + consumer.width;
840
+ for (const vid of afterSide) {
841
+ const v = graph.nodes.get(vid);
842
+ if (!v) continue;
843
+ v.x = rightEdge + options.nodeSpacing;
844
+ v.y = cyCenter - v.height / 2;
845
+ rightEdge = v.x + v.width;
846
+ }
847
+ }
848
+ }
849
+ let orphanX = 0, orphanY = 0;
850
+ for (const orphanId of orphanValues) {
851
+ const v = graph.nodes.get(orphanId);
852
+ if (!v) continue;
853
+ v.x = orphanX;
854
+ v.y = orphanY;
855
+ if (isHorizontal) orphanX += v.width + options.nodeSpacing;
856
+ else orphanY += v.height + options.nodeSpacing;
857
+ }
858
+ }
859
+ function findDominantConsumer(graph, valueId) {
860
+ const counts = /* @__PURE__ */ new Map();
861
+ const outs = graph.outEdges.get(valueId);
862
+ if (outs) {
863
+ for (const eid of outs) {
864
+ const edge = graph.edges.get(eid);
865
+ if (!edge) continue;
866
+ const other = graph.nodes.get(edge.to);
867
+ if (other && !other.isValue && !other.isDummy) {
868
+ counts.set(edge.to, (counts.get(edge.to) ?? 0) + 1);
869
+ }
870
+ }
871
+ }
872
+ const ins = graph.inEdges.get(valueId);
873
+ if (ins) {
874
+ for (const eid of ins) {
875
+ const edge = graph.edges.get(eid);
876
+ if (!edge) continue;
877
+ const other = graph.nodes.get(edge.from);
878
+ if (other && !other.isValue && !other.isDummy) {
879
+ counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);
880
+ }
881
+ }
882
+ }
883
+ let bestId = null;
884
+ let bestCount = -1;
885
+ for (const [id, count] of counts) {
886
+ if (count > bestCount || count === bestCount && bestId !== null && id < bestId) {
887
+ bestId = id;
888
+ bestCount = count;
889
+ }
890
+ }
891
+ return bestId;
892
+ }
893
+ function splitValuesBySide(graph, consumerId, valueIds, isHorizontal) {
894
+ const consumer = graph.nodes.get(consumerId);
895
+ if (!consumer) return { beforeSide: [...valueIds].sort(), afterSide: [] };
896
+ const slots = [];
897
+ for (const vId of valueIds) {
898
+ let side = "neutral";
899
+ let offset = 0.5;
900
+ const inspectHandle = (handleId) => {
901
+ if (!handleId) return;
902
+ const handle = consumer.handles.find((h) => h.id === handleId);
903
+ if (!handle) return;
904
+ offset = handle.offset ?? 0.5;
905
+ if (isHorizontal) {
906
+ if (handle.position === "top") side = "before";
907
+ else if (handle.position === "bottom") side = "after";
908
+ } else {
909
+ if (handle.position === "left") side = "before";
910
+ else if (handle.position === "right") side = "after";
911
+ }
912
+ };
913
+ const outs = graph.outEdges.get(vId);
914
+ if (outs) for (const eid of outs) {
915
+ const e = graph.edges.get(eid);
916
+ if (e && e.to === consumerId) {
917
+ inspectHandle(e.toHandle);
918
+ break;
919
+ }
920
+ }
921
+ if (side === "neutral") {
922
+ const ins = graph.inEdges.get(vId);
923
+ if (ins) for (const eid of ins) {
924
+ const e = graph.edges.get(eid);
925
+ if (e && e.from === consumerId) {
926
+ inspectHandle(e.fromHandle);
927
+ break;
928
+ }
929
+ }
930
+ }
931
+ slots.push({ id: vId, side, offset });
932
+ }
933
+ const before = slots.filter((s) => s.side === "before");
934
+ const after = slots.filter((s) => s.side === "after");
935
+ const neutral = slots.filter((s) => s.side === "neutral");
936
+ for (const n of neutral) {
937
+ if (before.length <= after.length) before.push(n);
938
+ else after.push(n);
939
+ }
940
+ const cmp = (a, b) => a.offset - b.offset || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
941
+ before.sort(cmp);
942
+ after.sort(cmp);
943
+ return {
944
+ beforeSide: before.map((s) => s.id),
945
+ afterSide: after.map((s) => s.id)
946
+ };
947
+ }
948
+ function railLayers(layers, graph) {
949
+ return layers.map(
950
+ (layer) => layer.filter((id) => {
951
+ const n = graph.nodes.get(id);
952
+ return n && !n.isValue;
953
+ })
954
+ );
955
+ }
956
+
685
957
  // src/algorithms/edge-routing.ts
686
958
  function routeEdges(graph, direction, edgeMargin) {
687
959
  const chains = collectEdgeChains(graph);
@@ -732,7 +1004,8 @@ function collectEdgeChains(graph) {
732
1004
  fromHandle: edge.fromHandle,
733
1005
  toHandle: currentEdge.toHandle,
734
1006
  dummyNodes,
735
- reversed: edge.reversed
1007
+ reversed: edge.reversed,
1008
+ kind: edge.kind
736
1009
  });
737
1010
  }
738
1011
  return chains;
@@ -777,7 +1050,8 @@ function routeChain(graph, chain, direction, margin) {
777
1050
  to: actualTo,
778
1051
  fromHandle: actualFromHandle,
779
1052
  toHandle: actualToHandle,
780
- points
1053
+ points,
1054
+ kind: chain.kind
781
1055
  };
782
1056
  }
783
1057
  function makeOrthogonal(points, direction) {
@@ -851,21 +1125,292 @@ function getDefaultTargetSide(direction) {
851
1125
  }
852
1126
  }
853
1127
 
1128
+ // src/algorithms/component-packing.ts
1129
+ function packComponents(nodes, edges, options) {
1130
+ if (nodes.length === 0) return;
1131
+ const idToNode = /* @__PURE__ */ new Map();
1132
+ for (const n of nodes) idToNode.set(n.id, n);
1133
+ const rootOf = /* @__PURE__ */ new Map();
1134
+ function topAncestor(id) {
1135
+ if (rootOf.has(id)) return rootOf.get(id);
1136
+ const n = idToNode.get(id);
1137
+ if (!n || !n.parentId || !idToNode.has(n.parentId)) {
1138
+ rootOf.set(id, id);
1139
+ return id;
1140
+ }
1141
+ const r = topAncestor(n.parentId);
1142
+ rootOf.set(id, r);
1143
+ return r;
1144
+ }
1145
+ for (const n of nodes) topAncestor(n.id);
1146
+ const parent = /* @__PURE__ */ new Map();
1147
+ function find(x) {
1148
+ let cur = x;
1149
+ while (parent.get(cur) !== cur) {
1150
+ const p = parent.get(cur);
1151
+ parent.set(cur, parent.get(p));
1152
+ cur = parent.get(cur);
1153
+ }
1154
+ return cur;
1155
+ }
1156
+ function union(a, b) {
1157
+ const ra = find(a);
1158
+ const rb = find(b);
1159
+ if (ra !== rb) parent.set(ra, rb);
1160
+ }
1161
+ for (const n of nodes) parent.set(topAncestor(n.id), topAncestor(n.id));
1162
+ for (const e of edges) {
1163
+ const ra = topAncestor(e.from);
1164
+ const rb = topAncestor(e.to);
1165
+ if (idToNode.has(ra) && idToNode.has(rb)) union(ra, rb);
1166
+ }
1167
+ const groups = /* @__PURE__ */ new Map();
1168
+ for (const n of nodes) {
1169
+ const root = topAncestor(n.id);
1170
+ const comp = find(root);
1171
+ const arr = groups.get(comp) ?? [];
1172
+ arr.push(n);
1173
+ groups.set(comp, arr);
1174
+ }
1175
+ if (groups.size <= 1) return;
1176
+ const isHorizontal = options.direction === "LR" || options.direction === "RL";
1177
+ const edgesByComp = /* @__PURE__ */ new Map();
1178
+ for (const e of edges) {
1179
+ const root = topAncestor(e.from);
1180
+ const comp = find(root);
1181
+ const arr = edgesByComp.get(comp) ?? [];
1182
+ arr.push(e);
1183
+ edgesByComp.set(comp, arr);
1184
+ }
1185
+ const boxes = [];
1186
+ for (const [compId, group] of groups) {
1187
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1188
+ for (const n of group) {
1189
+ if (n.x < minX) minX = n.x;
1190
+ if (n.y < minY) minY = n.y;
1191
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1192
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1193
+ }
1194
+ const compEdges = edgesByComp.get(compId) ?? [];
1195
+ for (const e of compEdges) {
1196
+ for (const p of e.points) {
1197
+ if (p.x < minX) minX = p.x;
1198
+ if (p.y < minY) minY = p.y;
1199
+ if (p.x > maxX) maxX = p.x;
1200
+ if (p.y > maxY) maxY = p.y;
1201
+ }
1202
+ }
1203
+ boxes.push({ id: compId, nodes: group, edges: compEdges, minX, minY, maxX, maxY });
1204
+ }
1205
+ boxes.sort((a, b) => {
1206
+ const sizeA = isHorizontal ? a.maxX - a.minX : a.maxY - a.minY;
1207
+ const sizeB = isHorizontal ? b.maxX - b.minX : b.maxY - b.minY;
1208
+ return sizeB - sizeA;
1209
+ });
1210
+ const gap = options.layerSpacing;
1211
+ let cursor = 0;
1212
+ for (const box of boxes) {
1213
+ if (isHorizontal) {
1214
+ const dy = cursor - box.minY;
1215
+ const dx = -box.minX;
1216
+ for (const n of box.nodes) {
1217
+ n.x += dx;
1218
+ n.y += dy;
1219
+ for (const h of n.handles) {
1220
+ h.x += dx;
1221
+ h.y += dy;
1222
+ }
1223
+ }
1224
+ for (const e of box.edges) {
1225
+ for (const p of e.points) {
1226
+ p.x += dx;
1227
+ p.y += dy;
1228
+ }
1229
+ }
1230
+ cursor += box.maxY - box.minY + gap;
1231
+ } else {
1232
+ const dx = cursor - box.minX;
1233
+ const dy = -box.minY;
1234
+ for (const n of box.nodes) {
1235
+ n.x += dx;
1236
+ n.y += dy;
1237
+ for (const h of n.handles) {
1238
+ h.x += dx;
1239
+ h.y += dy;
1240
+ }
1241
+ }
1242
+ for (const e of box.edges) {
1243
+ for (const p of e.points) {
1244
+ p.x += dx;
1245
+ p.y += dy;
1246
+ }
1247
+ }
1248
+ cursor += box.maxX - box.minX + gap;
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ // src/proposals.ts
1254
+ function applyRotationProposals(input, options) {
1255
+ if (!options.onProposal) return input;
1256
+ const nodeIndex = new Map(input.nodes.map((n) => [n.id, n]));
1257
+ const newNodes = input.nodes.map((node) => {
1258
+ const role = classifyNode(node, input.edges);
1259
+ const expected = expectedSidesFor(node, role, input.edges, nodeIndex, options.direction);
1260
+ const proposal = computeProposal(node, expected, options.direction, role);
1261
+ if (!proposal) return node;
1262
+ const accepted = options.onProposal(proposal);
1263
+ return accepted ?? node;
1264
+ });
1265
+ return { nodes: newNodes, edges: input.edges };
1266
+ }
1267
+ function classifyNode(node, edges) {
1268
+ const incident = edges.filter((e) => e.from === node.id || e.to === node.id);
1269
+ if (incident.length === 0) return { kind: "isolated" };
1270
+ const allData = incident.every((e) => (e.kind ?? "control") === "data");
1271
+ if (!allData) return { kind: "rail" };
1272
+ for (const e of incident) {
1273
+ const consumerId = e.from === node.id ? e.to : e.from;
1274
+ const consumerHandleId = e.from === node.id ? e.toHandle : e.fromHandle;
1275
+ const sideHint = sideHintFromHandle(
1276
+ consumerId,
1277
+ consumerHandleId,
1278
+ edges,
1279
+ /* recursing */
1280
+ false
1281
+ );
1282
+ if (sideHint) return { kind: "value", consumerSide: sideHint };
1283
+ }
1284
+ return { kind: "value", consumerSide: null };
1285
+ }
1286
+ function sideHintFromHandle(_consumerId, _handleId, _edges, _recursing) {
1287
+ return null;
1288
+ }
1289
+ function expectedSidesFor(node, role, edges, index, direction) {
1290
+ if (role.kind === "isolated" || role.kind === "rail") {
1291
+ return { input: inputSideFor(direction), output: outputSideFor(direction) };
1292
+ }
1293
+ const consumerHandleSide = findConsumerHandleSide(node, edges, index);
1294
+ if (!consumerHandleSide) {
1295
+ return { input: inputSideFor(direction), output: outputSideFor(direction) };
1296
+ }
1297
+ const isVerticalFlow = direction === "TB" || direction === "BT";
1298
+ let outputSide;
1299
+ if (isVerticalFlow) {
1300
+ if (consumerHandleSide === "left") outputSide = "right";
1301
+ else if (consumerHandleSide === "right") outputSide = "left";
1302
+ else outputSide = "right";
1303
+ } else {
1304
+ if (consumerHandleSide === "top") outputSide = "bottom";
1305
+ else if (consumerHandleSide === "bottom") outputSide = "top";
1306
+ else outputSide = "bottom";
1307
+ }
1308
+ return { input: outputSide, output: outputSide };
1309
+ }
1310
+ function findConsumerHandleSide(value, edges, index) {
1311
+ for (const e of edges) {
1312
+ const isFromValue = e.from === value.id;
1313
+ const isToValue = e.to === value.id;
1314
+ if (!isFromValue && !isToValue) continue;
1315
+ const consumerId = isFromValue ? e.to : e.from;
1316
+ const consumer = index.get(consumerId);
1317
+ if (!consumer) continue;
1318
+ const consumerHandleId = isFromValue ? e.toHandle : e.fromHandle;
1319
+ const consumerHandle = consumer.handles.find((h) => h.id === consumerHandleId);
1320
+ if (consumerHandle) return consumerHandle.position;
1321
+ }
1322
+ return null;
1323
+ }
1324
+ function computeProposal(node, expected, direction, role) {
1325
+ if (node.handles.length === 0) return null;
1326
+ const currentScore = score(node.handles, expected);
1327
+ if (currentScore.matchRatio >= 1) return null;
1328
+ const candidates = [];
1329
+ for (const rot of [90, -90, 180]) {
1330
+ const rotated = rotateHandles(node.handles, rot);
1331
+ candidates.push({ rotation: rot, score: score(rotated, expected) });
1332
+ }
1333
+ candidates.sort((a, b) => b.score.matchRatio - a.score.matchRatio);
1334
+ const best = candidates[0];
1335
+ if (best.score.matchRatio <= currentScore.matchRatio) return null;
1336
+ return buildProposal(node, best.rotation, currentScore, best.score, direction, expected, role);
1337
+ }
1338
+ function buildProposal(node, rotation, current, proposedScore, direction, expected, role) {
1339
+ const proposed = { ...node, handles: rotateHandles(node.handles, rotation) };
1340
+ const roleDesc = role.kind === "value" ? `value node should face ${expected.output} (its sidecar lands opposite the consumer's handle)` : `direction ${direction} expects inputs on ${expected.input} and outputs on ${expected.output}`;
1341
+ return {
1342
+ type: "rotate",
1343
+ nodeId: node.id,
1344
+ current: node,
1345
+ proposed,
1346
+ rotation,
1347
+ reason: `${roleDesc}; ${current.matched}/${current.total} handles match, ${proposedScore.matched}/${proposedScore.total} after rotation`
1348
+ };
1349
+ }
1350
+ function inputSideFor(direction) {
1351
+ switch (direction) {
1352
+ case "TB":
1353
+ return "top";
1354
+ case "BT":
1355
+ return "bottom";
1356
+ case "LR":
1357
+ return "left";
1358
+ case "RL":
1359
+ return "right";
1360
+ }
1361
+ }
1362
+ function outputSideFor(direction) {
1363
+ switch (direction) {
1364
+ case "TB":
1365
+ return "bottom";
1366
+ case "BT":
1367
+ return "top";
1368
+ case "LR":
1369
+ return "right";
1370
+ case "RL":
1371
+ return "left";
1372
+ }
1373
+ }
1374
+ function score(handles, expected) {
1375
+ let matched = 0;
1376
+ let total = 0;
1377
+ for (const h of handles) {
1378
+ total++;
1379
+ if (h.type === "input" && h.position === expected.input) matched++;
1380
+ else if (h.type === "output" && h.position === expected.output) matched++;
1381
+ }
1382
+ return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
1383
+ }
1384
+ function rotateHandles(handles, rot) {
1385
+ const rotateSide = (s) => {
1386
+ if (rot === 180) {
1387
+ return s === "top" ? "bottom" : s === "bottom" ? "top" : s === "left" ? "right" : "left";
1388
+ }
1389
+ if (rot === 90) {
1390
+ return s === "top" ? "right" : s === "right" ? "bottom" : s === "bottom" ? "left" : "top";
1391
+ }
1392
+ return s === "top" ? "left" : s === "left" ? "bottom" : s === "bottom" ? "right" : "top";
1393
+ };
1394
+ return handles.map((h) => ({ ...h, position: rotateSide(h.position) }));
1395
+ }
1396
+
854
1397
  // src/layout.ts
1398
+ var COMPOUND_HEADER = 28;
855
1399
  function layout(input, options) {
856
1400
  const resolved = resolveOptions(options);
857
- const graph = buildGraph(input.nodes, input.edges);
858
- return computeLayout(graph, resolved);
1401
+ if (input.nodes.length === 0) return { nodes: [], edges: [] };
1402
+ const adjusted = applyRotationProposals(input, resolved);
1403
+ return layoutCompound(adjusted, resolved);
859
1404
  }
860
1405
  function computeLayout(graph, options) {
861
- if (graph.nodes.size === 0) {
862
- return { nodes: [], edges: [] };
863
- }
1406
+ if (graph.nodes.size === 0) return { nodes: [], edges: [] };
864
1407
  breakCycles(graph);
865
1408
  let layers = assignLayers(graph);
866
1409
  layers = insertDummyNodes(graph, layers);
867
- layers = minimizeCrossings(graph, layers, options.crossingMinimizationIterations);
868
- assignCoordinates(graph, layers, options);
1410
+ let rail = railLayers(layers, graph);
1411
+ rail = minimizeCrossings(graph, rail, options.crossingMinimizationIterations);
1412
+ assignCoordinates(graph, rail, options);
1413
+ placeValueSidecars(graph, layers, options);
869
1414
  const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
870
1415
  return buildResult(graph, routedEdges);
871
1416
  }
@@ -876,13 +1421,7 @@ function buildResult(graph, routedEdges) {
876
1421
  if (node.isDummy) continue;
877
1422
  const handles = node.handles.map((h) => {
878
1423
  const pos = getHandlePosition(node, h.id);
879
- return {
880
- id: h.id,
881
- type: h.type,
882
- position: h.position,
883
- x: pos.x,
884
- y: pos.y
885
- };
1424
+ return { id: h.id, type: h.type, position: h.position, x: pos.x, y: pos.y };
886
1425
  });
887
1426
  nodes.push({
888
1427
  id: node.id,
@@ -890,7 +1429,8 @@ function buildResult(graph, routedEdges) {
890
1429
  y: node.y,
891
1430
  width: node.width,
892
1431
  height: node.height,
893
- handles
1432
+ handles,
1433
+ parentId: node.parentId
894
1434
  });
895
1435
  }
896
1436
  for (const route of routedEdges) {
@@ -900,11 +1440,146 @@ function buildResult(graph, routedEdges) {
900
1440
  to: route.to,
901
1441
  fromHandle: route.fromHandle,
902
1442
  toHandle: route.toHandle,
903
- points: route.points
1443
+ points: route.points,
1444
+ kind: route.kind
904
1445
  });
905
1446
  }
906
1447
  return { nodes, edges };
907
1448
  }
1449
+ function layoutCompound(input, options) {
1450
+ const nodeMap = new Map(input.nodes.map((n) => [n.id, n]));
1451
+ const childrenByParent = /* @__PURE__ */ new Map();
1452
+ const rootNodes = [];
1453
+ for (const n of input.nodes) {
1454
+ if (n.parentId && nodeMap.has(n.parentId)) {
1455
+ const arr = childrenByParent.get(n.parentId) ?? [];
1456
+ arr.push(n);
1457
+ childrenByParent.set(n.parentId, arr);
1458
+ } else {
1459
+ rootNodes.push(n);
1460
+ }
1461
+ }
1462
+ const compoundIds = new Set(childrenByParent.keys());
1463
+ const depthCache = /* @__PURE__ */ new Map();
1464
+ function depthOf(id) {
1465
+ if (depthCache.has(id)) return depthCache.get(id);
1466
+ const n = nodeMap.get(id);
1467
+ const d = n?.parentId && nodeMap.has(n.parentId) ? depthOf(n.parentId) + 1 : 0;
1468
+ depthCache.set(id, d);
1469
+ return d;
1470
+ }
1471
+ for (const n of input.nodes) depthOf(n.id);
1472
+ const sortedCompounds = [...compoundIds].sort((a, b) => depthOf(b) - depthOf(a));
1473
+ const subLayouts = /* @__PURE__ */ new Map();
1474
+ const compoundSize = /* @__PURE__ */ new Map();
1475
+ for (const compoundId of sortedCompounds) {
1476
+ const children = childrenByParent.get(compoundId) ?? [];
1477
+ if (children.length === 0) continue;
1478
+ const childIdSet = new Set(children.map((c) => c.id));
1479
+ const subEdges = input.edges.filter((e) => childIdSet.has(e.from) && childIdSet.has(e.to));
1480
+ const sizedChildren = children.map((c) => {
1481
+ const sz = compoundSize.get(c.id);
1482
+ return sz ? { ...c, width: sz.width, height: sz.height } : c;
1483
+ });
1484
+ const subOptions = { ...options, packComponents: false };
1485
+ const subInput = { nodes: sizedChildren.map(stripParent), edges: subEdges };
1486
+ const subResult = layoutFlat(subInput, subOptions);
1487
+ subLayouts.set(compoundId, subResult);
1488
+ const bbox = computeBoundingBox(subResult.nodes);
1489
+ const innerW = bbox.width + options.compoundPadding * 2;
1490
+ const innerH = bbox.height + options.compoundPadding * 2 + COMPOUND_HEADER;
1491
+ const original = nodeMap.get(compoundId);
1492
+ compoundSize.set(compoundId, {
1493
+ width: Math.max(innerW, original.width),
1494
+ height: Math.max(innerH, original.height)
1495
+ });
1496
+ }
1497
+ const sizedRootNodes = rootNodes.map((n) => {
1498
+ const sz = compoundSize.get(n.id);
1499
+ return sz ? { ...n, width: sz.width, height: sz.height } : n;
1500
+ });
1501
+ const rootIdSet = new Set(rootNodes.map((n) => n.id));
1502
+ const rootEdges = input.edges.filter((e) => {
1503
+ return rootIdSet.has(e.from) && rootIdSet.has(e.to);
1504
+ });
1505
+ const rootResult = layoutFlat({ nodes: sizedRootNodes.map(stripParent), edges: rootEdges }, options);
1506
+ const finalNodes = [];
1507
+ const finalEdges = [];
1508
+ for (const n of rootResult.nodes) {
1509
+ const wasCompound = compoundIds.has(n.id);
1510
+ finalNodes.push({
1511
+ ...n,
1512
+ parentId: nodeMap.get(n.id)?.parentId
1513
+ });
1514
+ if (wasCompound) {
1515
+ placeCompoundChildren(n, (compoundId) => subLayouts.get(compoundId), finalNodes, finalEdges, options, nodeMap);
1516
+ }
1517
+ }
1518
+ finalEdges.push(...rootResult.edges);
1519
+ if (options.packComponents) {
1520
+ packComponents(finalNodes, finalEdges, options);
1521
+ }
1522
+ return { nodes: finalNodes, edges: finalEdges };
1523
+ }
1524
+ function placeCompoundChildren(parent, getSub, finalNodes, finalEdges, options, nodeMap) {
1525
+ const sub = getSub(parent.id);
1526
+ if (!sub) return;
1527
+ const bbox = computeBoundingBox(sub.nodes);
1528
+ const dx = parent.x + options.compoundPadding - bbox.minX;
1529
+ const dy = parent.y + options.compoundPadding + COMPOUND_HEADER - bbox.minY;
1530
+ const availableW = parent.width - options.compoundPadding * 2;
1531
+ const slackX = (availableW - bbox.width) / 2;
1532
+ const availableH = parent.height - options.compoundPadding * 2 - COMPOUND_HEADER;
1533
+ const slackY = (availableH - bbox.height) / 2;
1534
+ const cx = dx + Math.max(0, slackX);
1535
+ const cy = dy + Math.max(0, slackY);
1536
+ for (const child of sub.nodes) {
1537
+ const placed = {
1538
+ ...child,
1539
+ x: child.x + cx,
1540
+ y: child.y + cy,
1541
+ handles: child.handles.map((h) => ({ ...h, x: h.x + cx, y: h.y + cy })),
1542
+ parentId: nodeMap.get(child.id)?.parentId ?? parent.id
1543
+ };
1544
+ finalNodes.push(placed);
1545
+ if (getSub(child.id)) {
1546
+ placeCompoundChildren(placed, getSub, finalNodes, finalEdges, options, nodeMap);
1547
+ }
1548
+ }
1549
+ for (const edge of sub.edges) {
1550
+ finalEdges.push({
1551
+ ...edge,
1552
+ points: edge.points.map((p) => ({ x: p.x + cx, y: p.y + cy }))
1553
+ });
1554
+ }
1555
+ }
1556
+ function stripParent(n) {
1557
+ const { parentId: _ignored, ...rest } = n;
1558
+ return rest;
1559
+ }
1560
+ function layoutFlat(input, options) {
1561
+ const graph = buildGraph(input.nodes, input.edges, {
1562
+ controlWeight: options.controlWeight,
1563
+ dataWeight: options.dataWeight
1564
+ });
1565
+ return computeLayout(graph, options);
1566
+ }
1567
+ function computeBoundingBox(nodes) {
1568
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1569
+ for (const n of nodes) {
1570
+ if (n.x < minX) minX = n.x;
1571
+ if (n.y < minY) minY = n.y;
1572
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1573
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1574
+ }
1575
+ if (!isFinite(minX)) {
1576
+ minX = 0;
1577
+ minY = 0;
1578
+ maxX = 0;
1579
+ maxY = 0;
1580
+ }
1581
+ return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
1582
+ }
908
1583
 
909
1584
  // src/incremental.ts
910
1585
  var IncrementalLayout = class {
@@ -913,82 +1588,40 @@ var IncrementalLayout = class {
913
1588
  this.inputEdges = /* @__PURE__ */ new Map();
914
1589
  this.lastResult = null;
915
1590
  this.nodePositions = /* @__PURE__ */ new Map();
916
- this.options = resolveOptions(options);
1591
+ this.options = options;
1592
+ this.resolvedOptions = resolveOptions(options);
917
1593
  }
918
- /**
919
- * Set the full graph and compute a complete layout.
920
- */
921
1594
  setGraph(input) {
922
1595
  this.inputNodes.clear();
923
1596
  this.inputEdges.clear();
924
- for (const node of input.nodes) {
925
- this.inputNodes.set(node.id, node);
926
- }
927
- for (const edge of input.edges) {
928
- this.inputEdges.set(edge.id, edge);
929
- }
1597
+ for (const node of input.nodes) this.inputNodes.set(node.id, node);
1598
+ for (const edge of input.edges) this.inputEdges.set(edge.id, edge);
930
1599
  return this.recompute();
931
1600
  }
932
- /**
933
- * Add nodes and edges incrementally.
934
- * Attempts to minimize layout changes for existing nodes.
935
- */
936
1601
  addNodes(nodes, edges) {
937
- for (const node of nodes) {
938
- this.inputNodes.set(node.id, node);
939
- }
940
- if (edges) {
941
- for (const edge of edges) {
942
- this.inputEdges.set(edge.id, edge);
943
- }
944
- }
945
- return this.recomputeIncremental(
946
- new Set(nodes.map((n) => n.id)),
947
- new Set(edges?.map((e) => e.id) || [])
948
- );
1602
+ for (const node of nodes) this.inputNodes.set(node.id, node);
1603
+ if (edges) for (const edge of edges) this.inputEdges.set(edge.id, edge);
1604
+ return this.recomputeWithStability(new Set(nodes.map((n) => n.id)));
949
1605
  }
950
- /**
951
- * Remove nodes (and their connected edges) from the layout.
952
- */
953
1606
  removeNodes(nodeIds) {
954
1607
  const removedSet = new Set(nodeIds);
955
- for (const id of nodeIds) {
956
- this.inputNodes.delete(id);
957
- }
1608
+ for (const id of nodeIds) this.inputNodes.delete(id);
958
1609
  for (const [edgeId, edge] of this.inputEdges) {
959
1610
  if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
960
1611
  this.inputEdges.delete(edgeId);
961
1612
  }
962
1613
  }
963
- for (const id of nodeIds) {
964
- this.nodePositions.delete(id);
965
- }
1614
+ for (const id of nodeIds) this.nodePositions.delete(id);
966
1615
  return this.recompute();
967
1616
  }
968
- /**
969
- * Add edges between existing nodes.
970
- */
971
1617
  addEdges(edges) {
972
- for (const edge of edges) {
973
- this.inputEdges.set(edge.id, edge);
974
- }
975
- return this.recomputeIncremental(
976
- /* @__PURE__ */ new Set(),
977
- new Set(edges.map((e) => e.id))
978
- );
1618
+ for (const edge of edges) this.inputEdges.set(edge.id, edge);
1619
+ return this.recomputeWithStability(/* @__PURE__ */ new Set());
979
1620
  }
980
- /**
981
- * Remove edges from the layout.
982
- */
983
1621
  removeEdges(edgeIds) {
984
- for (const id of edgeIds) {
985
- this.inputEdges.delete(id);
986
- }
1622
+ for (const id of edgeIds) this.inputEdges.delete(id);
987
1623
  return this.recompute();
988
1624
  }
989
- /**
990
- * Get the current layout result.
991
- */
992
1625
  getResult() {
993
1626
  return this.lastResult;
994
1627
  }
@@ -997,42 +1630,48 @@ var IncrementalLayout = class {
997
1630
  nodes: [...this.inputNodes.values()],
998
1631
  edges: [...this.inputEdges.values()]
999
1632
  };
1000
- const graph = buildGraph(input.nodes, input.edges);
1001
- this.lastResult = computeLayout(graph, this.options);
1633
+ this.lastResult = layout(input, this.options);
1002
1634
  this.cachePositions();
1003
1635
  return this.lastResult;
1004
1636
  }
1005
- recomputeIncremental(newNodeIds, newEdgeIds) {
1637
+ recomputeWithStability(newNodeIds) {
1006
1638
  const input = {
1007
1639
  nodes: [...this.inputNodes.values()],
1008
1640
  edges: [...this.inputEdges.values()]
1009
1641
  };
1010
- const graph = buildGraph(input.nodes, input.edges);
1011
- breakCycles(graph);
1012
- let layers = assignLayers(graph);
1013
- layers = insertDummyNodes(graph, layers);
1014
- layers = minimizeCrossings(graph, layers, this.options.crossingMinimizationIterations);
1015
- assignCoordinates(graph, layers, this.options);
1016
- this.applyStability(graph, newNodeIds);
1017
- const routedEdges = routeEdges(graph, this.options.direction, this.options.edgeMargin);
1018
- this.lastResult = this.buildResult(graph, routedEdges);
1642
+ const fresh = layout(input, this.options);
1643
+ this.lastResult = this.applyStability(fresh, newNodeIds);
1019
1644
  this.cachePositions();
1020
1645
  return this.lastResult;
1021
1646
  }
1022
1647
  /**
1023
- * Apply stability: blend new positions with old positions for existing nodes.
1024
- * This reduces visual disruption when adding new nodes.
1648
+ * Blend the freshly computed positions with the previous ones, so existing
1649
+ * nodes don't jump too far when a small change happens.
1025
1650
  */
1026
- applyStability(graph, newNodeIds) {
1027
- if (this.nodePositions.size === 0) return;
1651
+ applyStability(fresh, newNodeIds) {
1652
+ if (this.nodePositions.size === 0) return fresh;
1028
1653
  const STABILITY_WEIGHT = 0.3;
1029
- for (const [nodeId, node] of graph.nodes) {
1030
- if (node.isDummy || newNodeIds.has(nodeId)) continue;
1031
- const oldPos = this.nodePositions.get(nodeId);
1032
- if (!oldPos) continue;
1033
- node.x = node.x * (1 - STABILITY_WEIGHT) + oldPos.x * STABILITY_WEIGHT;
1034
- node.y = node.y * (1 - STABILITY_WEIGHT) + oldPos.y * STABILITY_WEIGHT;
1035
- }
1654
+ const blended = {
1655
+ nodes: fresh.nodes.map((n) => {
1656
+ if (newNodeIds.has(n.id)) return n;
1657
+ const old = this.nodePositions.get(n.id);
1658
+ if (!old) return n;
1659
+ const dx = old.x - n.x;
1660
+ const dy = old.y - n.y;
1661
+ const x = n.x + dx * STABILITY_WEIGHT;
1662
+ const y = n.y + dy * STABILITY_WEIGHT;
1663
+ const ddx = x - n.x;
1664
+ const ddy = y - n.y;
1665
+ return {
1666
+ ...n,
1667
+ x,
1668
+ y,
1669
+ handles: n.handles.map((h) => ({ ...h, x: h.x + ddx, y: h.y + ddy }))
1670
+ };
1671
+ }),
1672
+ edges: fresh.edges
1673
+ };
1674
+ return blended;
1036
1675
  }
1037
1676
  cachePositions() {
1038
1677
  if (!this.lastResult) return;
@@ -1041,46 +1680,160 @@ var IncrementalLayout = class {
1041
1680
  this.nodePositions.set(node.id, { x: node.x, y: node.y });
1042
1681
  }
1043
1682
  }
1044
- buildResult(graph, routedEdges) {
1045
- const nodes = [];
1046
- const edges = [];
1047
- for (const [, node] of graph.nodes) {
1048
- if (node.isDummy) continue;
1049
- const handles = node.handles.map((h) => {
1050
- const pos = getHandlePosition(node, h.id);
1051
- return {
1052
- id: h.id,
1053
- type: h.type,
1054
- position: h.position,
1055
- x: pos.x,
1056
- y: pos.y
1057
- };
1058
- });
1059
- nodes.push({
1060
- id: node.id,
1061
- x: node.x,
1062
- y: node.y,
1063
- width: node.width,
1064
- height: node.height,
1065
- handles
1066
- });
1683
+ };
1684
+
1685
+ // src/debug.ts
1686
+ function printLayout(result, options = {}) {
1687
+ const lines = [];
1688
+ const { nodes, edges } = result;
1689
+ if (nodes.length === 0) return "(empty layout)";
1690
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1691
+ for (const n of nodes) {
1692
+ if (n.x < minX) minX = n.x;
1693
+ if (n.y < minY) minY = n.y;
1694
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1695
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1696
+ }
1697
+ lines.push(`=== layout ${nodes.length} nodes, ${edges.length} edges ===`);
1698
+ lines.push(`bbox: x=[${minX.toFixed(0)}..${maxX.toFixed(0)}] y=[${minY.toFixed(0)}..${maxY.toFixed(0)}] (${(maxX - minX).toFixed(0)} x ${(maxY - minY).toFixed(0)})`);
1699
+ const byY = [...nodes].sort((a, b) => a.y - b.y || a.x - b.x);
1700
+ const bands = [];
1701
+ for (const n of byY) {
1702
+ const placed = bands.find((b) => Math.abs(b[0].y - n.y) < 1);
1703
+ if (placed) placed.push(n);
1704
+ else bands.push([n]);
1705
+ }
1706
+ lines.push("");
1707
+ lines.push("--- Y bands ---");
1708
+ for (const band of bands) {
1709
+ const sorted = [...band].sort((a, b) => a.x - b.x);
1710
+ const summary = sorted.map((n) => {
1711
+ const p = n.parentId ? `[${n.parentId}/]` : "";
1712
+ return `${p}${n.id}@(${n.x.toFixed(0)},${n.y.toFixed(0)} ${n.width}x${n.height})`;
1713
+ }).join(" ");
1714
+ lines.push(` y=${band[0].y.toFixed(0).padStart(4)} : ${summary}`);
1715
+ }
1716
+ const byParent = /* @__PURE__ */ new Map();
1717
+ for (const n of nodes) {
1718
+ const key = n.parentId;
1719
+ const arr = byParent.get(key) ?? [];
1720
+ arr.push(n);
1721
+ byParent.set(key, arr);
1722
+ }
1723
+ const compoundIds = /* @__PURE__ */ new Set();
1724
+ for (const k of byParent.keys()) {
1725
+ if (k && nodes.find((n) => n.id === k)) compoundIds.add(k);
1726
+ }
1727
+ if (compoundIds.size > 0) {
1728
+ let printSubtree2 = function(id, depth) {
1729
+ const children = byParent.get(id) ?? [];
1730
+ for (const c of children) {
1731
+ const prefix = " ".repeat(depth);
1732
+ const tag = compoundIds.has(c.id) ? " (compound)" : "";
1733
+ lines.push(`${prefix}- ${c.id}${tag} bbox=(${c.x.toFixed(0)},${c.y.toFixed(0)})..(${(c.x + c.width).toFixed(0)},${(c.y + c.height).toFixed(0)})`);
1734
+ if (compoundIds.has(c.id)) printSubtree2(c.id, depth + 1);
1735
+ }
1736
+ };
1737
+ var printSubtree = printSubtree2;
1738
+ lines.push("");
1739
+ lines.push("--- hierarchy ---");
1740
+ printSubtree2(void 0, 0);
1741
+ }
1742
+ lines.push("");
1743
+ lines.push("--- edges ---");
1744
+ for (const e of edges) {
1745
+ const head = e.points[0];
1746
+ const tail = e.points[e.points.length - 1];
1747
+ lines.push(` [${e.kind}] ${e.from}.${e.fromHandle} \u2192 ${e.to}.${e.toHandle} (${head.x.toFixed(0)},${head.y.toFixed(0)}) \u2192 (${tail.x.toFixed(0)},${tail.y.toFixed(0)}) via ${e.points.length} pts`);
1748
+ }
1749
+ const overlaps = findOverlaps(nodes);
1750
+ if (overlaps.length > 0) {
1751
+ lines.push("");
1752
+ lines.push("--- OVERLAPS (problem!) ---");
1753
+ for (const o of overlaps) {
1754
+ lines.push(` ${o.a} overlaps ${o.b}`);
1067
1755
  }
1068
- for (const route of routedEdges) {
1069
- edges.push({
1070
- id: route.id,
1071
- from: route.from,
1072
- to: route.to,
1073
- fromHandle: route.fromHandle,
1074
- toHandle: route.toHandle,
1075
- points: route.points
1076
- });
1756
+ }
1757
+ if (options.grid !== false) {
1758
+ lines.push("");
1759
+ lines.push("--- ASCII grid ---");
1760
+ lines.push(asciiGrid(nodes, edges, options));
1761
+ }
1762
+ return lines.join("\n");
1763
+ }
1764
+ function findOverlaps(nodes) {
1765
+ const out = [];
1766
+ const compoundIds = /* @__PURE__ */ new Set();
1767
+ for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
1768
+ for (let i = 0; i < nodes.length; i++) {
1769
+ for (let j = i + 1; j < nodes.length; j++) {
1770
+ const a = nodes[i];
1771
+ const b = nodes[j];
1772
+ if (a.id === b.parentId || b.id === a.parentId) continue;
1773
+ const overlapX = a.x < b.x + b.width && b.x < a.x + a.width;
1774
+ const overlapY = a.y < b.y + b.height && b.y < a.y + a.height;
1775
+ if (overlapX && overlapY) {
1776
+ out.push({ a: a.id, b: b.id });
1777
+ }
1077
1778
  }
1078
- return { nodes, edges };
1079
1779
  }
1080
- };
1780
+ return out;
1781
+ }
1782
+ function asciiGrid(nodes, edges, options) {
1783
+ if (nodes.length === 0) return "";
1784
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1785
+ for (const n of nodes) {
1786
+ if (n.x < minX) minX = n.x;
1787
+ if (n.y < minY) minY = n.y;
1788
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1789
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1790
+ }
1791
+ for (const e of edges) for (const p of e.points) {
1792
+ if (p.x < minX) minX = p.x;
1793
+ if (p.y < minY) minY = p.y;
1794
+ if (p.x > maxX) maxX = p.x;
1795
+ if (p.y > maxY) maxY = p.y;
1796
+ }
1797
+ const targetW = options.gridWidth ?? 80;
1798
+ const layoutW = Math.max(1, maxX - minX);
1799
+ const layoutH = Math.max(1, maxY - minY);
1800
+ const scaleX = options.gridScale ?? targetW / layoutW;
1801
+ const scaleY = scaleX * 0.5;
1802
+ const w = Math.max(1, Math.ceil(layoutW * scaleX) + 1);
1803
+ const h = Math.max(1, Math.ceil(layoutH * scaleY) + 1);
1804
+ if (h > 80) return "(grid suppressed: too tall \u2014 pass {grid:false} to skip)";
1805
+ const grid = Array.from({ length: h }, () => Array.from({ length: w }, () => " "));
1806
+ const compoundIds = /* @__PURE__ */ new Set();
1807
+ for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
1808
+ for (const n of nodes) {
1809
+ const x0 = Math.round((n.x - minX) * scaleX);
1810
+ const y0 = Math.round((n.y - minY) * scaleY);
1811
+ const x1 = Math.max(x0, Math.round((n.x + n.width - minX) * scaleX) - 1);
1812
+ const y1 = Math.max(y0, Math.round((n.y + n.height - minY) * scaleY) - 1);
1813
+ const isCompound = compoundIds.has(n.id);
1814
+ const ch = isCompound ? "." : "#";
1815
+ for (let y = y0; y <= y1 && y < h; y++) {
1816
+ for (let x = x0; x <= x1 && x < w; x++) {
1817
+ if (y < 0 || x < 0) continue;
1818
+ if (y === y0 || y === y1 || x === x0 || x === x1) grid[y][x] = ch;
1819
+ else if (isCompound) {
1820
+ } else grid[y][x] = ch;
1821
+ }
1822
+ }
1823
+ const label = n.id.slice(0, Math.min(n.id.length, Math.max(2, x1 - x0 - 1)));
1824
+ const lx = Math.max(0, x0 + 1);
1825
+ const ly = Math.max(0, y0);
1826
+ for (let i = 0; i < label.length && lx + i < w; i++) {
1827
+ if (ly < h) grid[ly][lx + i] = label[i];
1828
+ }
1829
+ }
1830
+ return grid.map((row) => row.join("")).join("\n");
1831
+ }
1081
1832
  export {
1082
1833
  IncrementalLayout,
1083
1834
  countAllCrossings,
1084
- layout
1835
+ layout,
1836
+ printLayout,
1837
+ rotateHandles
1085
1838
  };
1086
1839
  //# sourceMappingURL=index.mjs.map