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