@nodius/layouting 0.1.0 → 0.1.3

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 +1092 -122
  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 +11 -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 +1159 -234
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1156 -233
  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 +44 -0
  34. package/dist/proposals.d.ts.map +1 -0
  35. package/dist/types.d.ts +214 -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,19 +22,47 @@ 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
 
29
31
  // src/types.ts
32
+ var QUALITY_PROFILES = {
33
+ draft: {
34
+ crossingMinimizationIterations: 6,
35
+ coordinateOptimizationIterations: 2,
36
+ skipTranspose: true
37
+ },
38
+ balanced: {
39
+ crossingMinimizationIterations: 24,
40
+ coordinateOptimizationIterations: 8,
41
+ skipTranspose: false
42
+ },
43
+ high: {
44
+ crossingMinimizationIterations: 48,
45
+ coordinateOptimizationIterations: 16,
46
+ skipTranspose: false
47
+ }
48
+ };
30
49
  function resolveOptions(options) {
50
+ const quality = options?.quality ?? "balanced";
51
+ const profile = QUALITY_PROFILES[quality];
31
52
  return {
32
53
  direction: options?.direction ?? "TB",
54
+ quality,
33
55
  nodeSpacing: options?.nodeSpacing ?? 40,
34
56
  layerSpacing: options?.layerSpacing ?? 60,
35
- crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
36
- coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
37
- edgeMargin: options?.edgeMargin ?? 20
57
+ crossingMinimizationIterations: options?.crossingMinimizationIterations ?? profile.crossingMinimizationIterations,
58
+ coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? profile.coordinateOptimizationIterations,
59
+ skipTranspose: options?.skipTranspose ?? profile.skipTranspose,
60
+ edgeMargin: options?.edgeMargin ?? 20,
61
+ controlWeight: options?.edgeWeights?.control ?? 1,
62
+ dataWeight: options?.edgeWeights?.data ?? 0.25,
63
+ packComponents: options?.packComponents ?? true,
64
+ compoundPadding: options?.compoundPadding ?? 24,
65
+ onProposal: options?.onProposal
38
66
  };
39
67
  }
40
68
 
@@ -97,7 +125,7 @@ var Graph = class {
97
125
  return result;
98
126
  }
99
127
  };
100
- function buildGraph(nodes, edges) {
128
+ function buildGraph(nodes, edges, ctx = { controlWeight: 1, dataWeight: 0.25 }) {
101
129
  const graph = new Graph();
102
130
  for (const node of nodes) {
103
131
  graph.addNode({
@@ -109,10 +137,15 @@ function buildGraph(nodes, edges) {
109
137
  layer: -1,
110
138
  order: -1,
111
139
  x: 0,
112
- y: 0
140
+ y: 0,
141
+ parentId: node.parentId,
142
+ isCompound: false,
143
+ isValue: false
113
144
  });
114
145
  }
115
146
  for (const edge of edges) {
147
+ const kind = edge.kind ?? "control";
148
+ const defaultWeight = kind === "control" ? ctx.controlWeight : ctx.dataWeight;
116
149
  graph.addEdge({
117
150
  id: edge.id,
118
151
  from: edge.from,
@@ -120,9 +153,34 @@ function buildGraph(nodes, edges) {
120
153
  fromHandle: edge.fromHandle,
121
154
  toHandle: edge.toHandle,
122
155
  reversed: false,
123
- originalId: edge.id
156
+ originalId: edge.id,
157
+ kind,
158
+ weight: edge.weight ?? defaultWeight
124
159
  });
125
160
  }
161
+ for (const [nodeId, node] of graph.nodes) {
162
+ const ins = graph.inEdges.get(nodeId);
163
+ const outs = graph.outEdges.get(nodeId);
164
+ let hasControl = false;
165
+ if (ins) {
166
+ for (const eid of ins) {
167
+ if (graph.edges.get(eid)?.kind === "control") {
168
+ hasControl = true;
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ if (!hasControl && outs) {
174
+ for (const eid of outs) {
175
+ if (graph.edges.get(eid)?.kind === "control") {
176
+ hasControl = true;
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ const incidentCount = (ins?.size ?? 0) + (outs?.size ?? 0);
182
+ node.isValue = !hasControl && incidentCount > 0;
183
+ }
126
184
  return graph;
127
185
  }
128
186
  function getHandlePosition(node, handleId) {
@@ -210,28 +268,88 @@ function breakCycles(graph) {
210
268
  function assignLayers(graph) {
211
269
  const layers = /* @__PURE__ */ new Map();
212
270
  const visiting = /* @__PURE__ */ new Set();
213
- function computeLayer(nodeId) {
271
+ function controlLayer(nodeId) {
214
272
  if (layers.has(nodeId)) return layers.get(nodeId);
215
273
  if (visiting.has(nodeId)) return 0;
274
+ const node = graph.nodes.get(nodeId);
275
+ if (!node || node.isValue) return -1;
216
276
  visiting.add(nodeId);
217
- const preds = graph.predecessors(nodeId);
277
+ const inEdgeIds = graph.inEdges.get(nodeId);
218
278
  let maxPredLayer = -1;
219
- for (const pred of preds) {
220
- maxPredLayer = Math.max(maxPredLayer, computeLayer(pred));
279
+ if (inEdgeIds) {
280
+ for (const eid of inEdgeIds) {
281
+ const edge = graph.edges.get(eid);
282
+ if (!edge || edge.kind !== "control") continue;
283
+ const pred = graph.nodes.get(edge.from);
284
+ if (!pred || pred.isValue) continue;
285
+ maxPredLayer = Math.max(maxPredLayer, controlLayer(edge.from));
286
+ }
221
287
  }
222
288
  const layer = maxPredLayer + 1;
223
289
  layers.set(nodeId, layer);
224
- const node = graph.nodes.get(nodeId);
225
- if (node) node.layer = layer;
290
+ node.layer = layer;
226
291
  visiting.delete(nodeId);
227
292
  return layer;
228
293
  }
229
- for (const nodeId of graph.nodes.keys()) {
230
- computeLayer(nodeId);
294
+ for (const [nodeId, node] of graph.nodes) {
295
+ if (!node.isValue) controlLayer(nodeId);
231
296
  }
232
- let maxLayer = 0;
233
- for (const layer of layers.values()) {
234
- maxLayer = Math.max(maxLayer, layer);
297
+ for (const [nodeId, node] of graph.nodes) {
298
+ if (!node.isValue) continue;
299
+ const neighborLayers = [];
300
+ const inEdgeIds = graph.inEdges.get(nodeId);
301
+ if (inEdgeIds) {
302
+ for (const eid of inEdgeIds) {
303
+ const edge = graph.edges.get(eid);
304
+ if (!edge) continue;
305
+ const nbr = graph.nodes.get(edge.from);
306
+ if (nbr && !nbr.isValue && layers.has(edge.from)) {
307
+ neighborLayers.push(layers.get(edge.from));
308
+ }
309
+ }
310
+ }
311
+ const outEdgeIds = graph.outEdges.get(nodeId);
312
+ if (outEdgeIds) {
313
+ for (const eid of outEdgeIds) {
314
+ const edge = graph.edges.get(eid);
315
+ if (!edge) continue;
316
+ const nbr = graph.nodes.get(edge.to);
317
+ if (nbr && !nbr.isValue && layers.has(edge.to)) {
318
+ neighborLayers.push(layers.get(edge.to));
319
+ }
320
+ }
321
+ }
322
+ let layer;
323
+ if (neighborLayers.length > 0) {
324
+ neighborLayers.sort((a, b) => a - b);
325
+ layer = neighborLayers[Math.floor(neighborLayers.length / 2)];
326
+ } else {
327
+ layer = 0;
328
+ }
329
+ layers.set(nodeId, layer);
330
+ node.layer = layer;
331
+ }
332
+ for (const [nodeId, node] of graph.nodes) {
333
+ if (!layers.has(nodeId)) {
334
+ layers.set(nodeId, 0);
335
+ node.layer = 0;
336
+ }
337
+ }
338
+ let minLayer = Infinity;
339
+ let maxLayer = -Infinity;
340
+ for (const l of layers.values()) {
341
+ if (l < minLayer) minLayer = l;
342
+ if (l > maxLayer) maxLayer = l;
343
+ }
344
+ if (!isFinite(minLayer)) return [];
345
+ if (minLayer !== 0) {
346
+ for (const [id, l] of layers) {
347
+ const adj = l - minLayer;
348
+ layers.set(id, adj);
349
+ const n = graph.nodes.get(id);
350
+ if (n) n.layer = adj;
351
+ }
352
+ maxLayer -= minLayer;
235
353
  }
236
354
  const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
237
355
  for (const [nodeId, layer] of layers) {
@@ -243,6 +361,7 @@ function insertDummyNodes(graph, layers) {
243
361
  let dummyCounter = 0;
244
362
  const edgesToProcess = [...graph.edges.values()];
245
363
  for (const edge of edgesToProcess) {
364
+ if (edge.kind !== "control") continue;
246
365
  const fromNode = graph.nodes.get(edge.from);
247
366
  const toNode = graph.nodes.get(edge.to);
248
367
  if (!fromNode || !toNode) continue;
@@ -267,7 +386,9 @@ function insertDummyNodes(graph, layers) {
267
386
  layer: l,
268
387
  order: -1,
269
388
  x: 0,
270
- y: 0
389
+ y: 0,
390
+ isCompound: false,
391
+ isValue: false
271
392
  });
272
393
  layers[l].push(dummyId);
273
394
  graph.addEdge({
@@ -277,7 +398,9 @@ function insertDummyNodes(graph, layers) {
277
398
  fromHandle: prevHandleId,
278
399
  toHandle: "in",
279
400
  reversed: edge.reversed,
280
- originalId: edge.originalId
401
+ originalId: edge.originalId,
402
+ kind: edge.kind,
403
+ weight: edge.weight
281
404
  });
282
405
  prevNodeId = dummyId;
283
406
  prevHandleId = "out";
@@ -289,18 +412,20 @@ function insertDummyNodes(graph, layers) {
289
412
  fromHandle: prevHandleId,
290
413
  toHandle: edge.toHandle,
291
414
  reversed: edge.reversed,
292
- originalId: edge.originalId
415
+ originalId: edge.originalId,
416
+ kind: edge.kind,
417
+ weight: edge.weight
293
418
  });
294
419
  }
295
420
  return layers;
296
421
  }
297
422
 
298
423
  // src/algorithms/crossing-minimization.ts
299
- function minimizeCrossings(graph, layers, iterations) {
424
+ function minimizeCrossings(graph, layers, iterations, forceSkipTranspose = false) {
300
425
  if (layers.length <= 1) return layers;
301
426
  const totalNodes = layers.reduce((s, l) => s + l.length, 0);
302
427
  const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
303
- const skipTranspose = totalNodes > 800;
428
+ const skipTranspose = forceSkipTranspose || totalNodes > 800;
304
429
  for (let l = 0; l < layers.length; l++) {
305
430
  for (let i = 0; i < layers[l].length; i++) {
306
431
  const node = graph.nodes.get(layers[l][i]);
@@ -347,45 +472,54 @@ function orderByBarycenter(graph, layers, layerIndex, direction) {
347
472
  const layer = layers[layerIndex];
348
473
  const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
349
474
  if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
350
- const adjLayerSet = new Set(layers[adjLayerIndex]);
351
- const barycenters = /* @__PURE__ */ new Map();
352
- for (let i = 0; i < layer.length; i++) {
353
- const nodeId = layer[i];
354
- let sum = 0;
355
- let count = 0;
356
- if (direction === "up") {
475
+ const n = layer.length;
476
+ const bary = new Float64Array(n);
477
+ if (direction === "up") {
478
+ for (let i = 0; i < n; i++) {
479
+ const nodeId = layer[i];
480
+ let sum = 0;
481
+ let count = 0;
357
482
  const inEdgeIds = graph.inEdges.get(nodeId);
358
483
  if (inEdgeIds) {
359
484
  for (const eid of inEdgeIds) {
360
485
  const edge = graph.edges.get(eid);
361
- if (edge && adjLayerSet.has(edge.from)) {
362
- const neighbor = graph.nodes.get(edge.from);
363
- if (neighbor) {
364
- sum += neighbor.order;
365
- count++;
366
- }
486
+ if (!edge) continue;
487
+ const neighbor = graph.nodes.get(edge.from);
488
+ if (neighbor && neighbor.layer === adjLayerIndex) {
489
+ sum += neighbor.order;
490
+ count++;
367
491
  }
368
492
  }
369
493
  }
370
- } else {
494
+ bary[i] = count > 0 ? sum / count : i;
495
+ }
496
+ } else {
497
+ for (let i = 0; i < n; i++) {
498
+ const nodeId = layer[i];
499
+ let sum = 0;
500
+ let count = 0;
371
501
  const outEdgeIds = graph.outEdges.get(nodeId);
372
502
  if (outEdgeIds) {
373
503
  for (const eid of outEdgeIds) {
374
504
  const edge = graph.edges.get(eid);
375
- if (edge && adjLayerSet.has(edge.to)) {
376
- const neighbor = graph.nodes.get(edge.to);
377
- if (neighbor) {
378
- sum += neighbor.order;
379
- count++;
380
- }
505
+ if (!edge) continue;
506
+ const neighbor = graph.nodes.get(edge.to);
507
+ if (neighbor && neighbor.layer === adjLayerIndex) {
508
+ sum += neighbor.order;
509
+ count++;
381
510
  }
382
511
  }
383
512
  }
513
+ bary[i] = count > 0 ? sum / count : i;
384
514
  }
385
- barycenters.set(nodeId, count > 0 ? sum / count : i);
386
515
  }
387
- layer.sort((a, b) => barycenters.get(a) - barycenters.get(b));
388
- for (let i = 0; i < layer.length; i++) {
516
+ const indices = new Array(n);
517
+ for (let i = 0; i < n; i++) indices[i] = i;
518
+ indices.sort((a, b) => bary[a] - bary[b]);
519
+ const reordered = new Array(n);
520
+ for (let i = 0; i < n; i++) reordered[i] = layer[indices[i]];
521
+ for (let i = 0; i < n; i++) {
522
+ layer[i] = reordered[i];
389
523
  const node = graph.nodes.get(layer[i]);
390
524
  if (node) node.order = i;
391
525
  }
@@ -420,65 +554,65 @@ function transposeImprove(graph, layers, layerIndex) {
420
554
  }
421
555
  }
422
556
  }
557
+ var _scratchA = [];
558
+ var _scratchB = [];
423
559
  function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
424
560
  let crossings = 0;
425
561
  if (layerIndex > 0) {
426
- const upperLayer = new Set(layers[layerIndex - 1]);
427
- const predsA = [];
428
- const predsB = [];
562
+ const upperIdx = layerIndex - 1;
563
+ _scratchA.length = 0;
564
+ _scratchB.length = 0;
429
565
  const inA = graph.inEdges.get(nodeA);
430
566
  if (inA) {
431
567
  for (const eid of inA) {
432
568
  const edge = graph.edges.get(eid);
433
- if (edge && upperLayer.has(edge.from)) {
434
- const n = graph.nodes.get(edge.from);
435
- if (n) predsA.push(n.order);
436
- }
569
+ if (!edge) continue;
570
+ const n = graph.nodes.get(edge.from);
571
+ if (n && n.layer === upperIdx) _scratchA.push(n.order);
437
572
  }
438
573
  }
439
574
  const inB = graph.inEdges.get(nodeB);
440
575
  if (inB) {
441
576
  for (const eid of inB) {
442
577
  const edge = graph.edges.get(eid);
443
- if (edge && upperLayer.has(edge.from)) {
444
- const n = graph.nodes.get(edge.from);
445
- if (n) predsB.push(n.order);
446
- }
578
+ if (!edge) continue;
579
+ const n = graph.nodes.get(edge.from);
580
+ if (n && n.layer === upperIdx) _scratchB.push(n.order);
447
581
  }
448
582
  }
449
- for (const pA of predsA) {
450
- for (const pB of predsB) {
451
- if (pA > pB) crossings++;
583
+ for (let i = 0; i < _scratchA.length; i++) {
584
+ const pA = _scratchA[i];
585
+ for (let j = 0; j < _scratchB.length; j++) {
586
+ if (pA > _scratchB[j]) crossings++;
452
587
  }
453
588
  }
454
589
  }
455
590
  if (layerIndex < layers.length - 1) {
456
- const lowerLayer = new Set(layers[layerIndex + 1]);
457
- const succsA = [];
458
- const succsB = [];
591
+ const lowerIdx = layerIndex + 1;
592
+ _scratchA.length = 0;
593
+ _scratchB.length = 0;
459
594
  const outA = graph.outEdges.get(nodeA);
460
595
  if (outA) {
461
596
  for (const eid of outA) {
462
597
  const edge = graph.edges.get(eid);
463
- if (edge && lowerLayer.has(edge.to)) {
464
- const n = graph.nodes.get(edge.to);
465
- if (n) succsA.push(n.order);
466
- }
598
+ if (!edge) continue;
599
+ const n = graph.nodes.get(edge.to);
600
+ if (n && n.layer === lowerIdx) _scratchA.push(n.order);
467
601
  }
468
602
  }
469
603
  const outB = graph.outEdges.get(nodeB);
470
604
  if (outB) {
471
605
  for (const eid of outB) {
472
606
  const edge = graph.edges.get(eid);
473
- if (edge && lowerLayer.has(edge.to)) {
474
- const n = graph.nodes.get(edge.to);
475
- if (n) succsB.push(n.order);
476
- }
607
+ if (!edge) continue;
608
+ const n = graph.nodes.get(edge.to);
609
+ if (n && n.layer === lowerIdx) _scratchB.push(n.order);
477
610
  }
478
611
  }
479
- for (const sA of succsA) {
480
- for (const sB of succsB) {
481
- if (sA > sB) crossings++;
612
+ for (let i = 0; i < _scratchA.length; i++) {
613
+ const sA = _scratchA[i];
614
+ for (let j = 0; j < _scratchB.length; j++) {
615
+ if (sA > _scratchB[j]) crossings++;
482
616
  }
483
617
  }
484
618
  }
@@ -514,22 +648,36 @@ function countLayerCrossings(graph, upperLayer, lowerLayer) {
514
648
  return mergeSortCount(lowerPositions);
515
649
  }
516
650
  function mergeSortCount(arr) {
517
- if (arr.length <= 1) return 0;
518
- const mid = arr.length >> 1;
519
- const left = arr.slice(0, mid);
520
- const right = arr.slice(mid);
521
- let count = mergeSortCount(left) + mergeSortCount(right);
522
- let i = 0, j = 0, k = 0;
523
- while (i < left.length && j < right.length) {
524
- if (left[i] <= right[j]) {
525
- arr[k++] = left[i++];
526
- } else {
527
- count += left.length - i;
528
- arr[k++] = right[j++];
651
+ const n = arr.length;
652
+ if (n <= 1) return 0;
653
+ let buf = new Array(n);
654
+ let src = arr;
655
+ let dst = buf;
656
+ let count = 0;
657
+ for (let width = 1; width < n; width <<= 1) {
658
+ for (let i = 0; i < n; i += width << 1) {
659
+ const left = i;
660
+ const mid = Math.min(i + width, n);
661
+ const right = Math.min(i + (width << 1), n);
662
+ let a = left, b = mid, k = left;
663
+ while (a < mid && b < right) {
664
+ if (src[a] <= src[b]) {
665
+ dst[k++] = src[a++];
666
+ } else {
667
+ count += mid - a;
668
+ dst[k++] = src[b++];
669
+ }
670
+ }
671
+ while (a < mid) dst[k++] = src[a++];
672
+ while (b < right) dst[k++] = src[b++];
529
673
  }
674
+ const tmp = src;
675
+ src = dst;
676
+ dst = tmp;
677
+ }
678
+ if (src !== arr) {
679
+ for (let i = 0; i < n; i++) arr[i] = src[i];
530
680
  }
531
- while (i < left.length) arr[k++] = left[i++];
532
- while (j < right.length) arr[k++] = right[j++];
533
681
  return count;
534
682
  }
535
683
 
@@ -613,37 +761,69 @@ function optimizePositions(graph, layers, options, isHorizontal) {
613
761
  }
614
762
  centerAllLayers(graph, layers, isHorizontal);
615
763
  }
764
+ var _neighborBuf = new Float64Array(64);
765
+ var _desiredBuf = new Float64Array(64);
766
+ function ensureBufSize(size) {
767
+ if (_neighborBuf.length < size) _neighborBuf = new Float64Array(size * 2);
768
+ if (_desiredBuf.length < size) _desiredBuf = new Float64Array(size * 2);
769
+ }
616
770
  function optimizeLayer(graph, layer, options, isHorizontal) {
617
- if (layer.length === 0) return;
618
- const desired = /* @__PURE__ */ new Map();
619
- for (const nodeId of layer) {
771
+ const n = layer.length;
772
+ if (n === 0) return;
773
+ let maxDegree = 0;
774
+ for (let i = 0; i < n; i++) {
775
+ const id = layer[i];
776
+ const d = (graph.inEdges.get(id)?.size ?? 0) + (graph.outEdges.get(id)?.size ?? 0);
777
+ if (d > maxDegree) maxDegree = d;
778
+ }
779
+ ensureBufSize(Math.max(n, maxDegree));
780
+ const desired = _desiredBuf;
781
+ for (let i = 0; i < n; i++) {
782
+ const nodeId = layer[i];
620
783
  const node = graph.nodes.get(nodeId);
621
- const connected = [...graph.predecessors(nodeId), ...graph.successors(nodeId)];
622
- if (connected.length === 0) {
623
- desired.set(nodeId, getOrderPos(node, isHorizontal));
784
+ let nbCount = 0;
785
+ const inEdgeIds = graph.inEdges.get(nodeId);
786
+ if (inEdgeIds) {
787
+ for (const eid of inEdgeIds) {
788
+ const edge = graph.edges.get(eid);
789
+ if (!edge) continue;
790
+ const c = graph.nodes.get(edge.from);
791
+ if (!c) continue;
792
+ _neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
793
+ }
794
+ }
795
+ const outEdgeIds = graph.outEdges.get(nodeId);
796
+ if (outEdgeIds) {
797
+ for (const eid of outEdgeIds) {
798
+ const edge = graph.edges.get(eid);
799
+ if (!edge) continue;
800
+ const c = graph.nodes.get(edge.to);
801
+ if (!c) continue;
802
+ _neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
803
+ }
804
+ }
805
+ if (nbCount === 0) {
806
+ desired[i] = getOrderPos(node, isHorizontal);
624
807
  continue;
625
808
  }
626
- const positions = connected.map((cId) => {
627
- const c = graph.nodes.get(cId);
628
- return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
629
- }).sort((a, b) => a - b);
809
+ sortPrefix(_neighborBuf, nbCount);
630
810
  const nodeSize = getOrderSize(node, isHorizontal);
631
- const median = positions.length % 2 === 0 ? (positions[positions.length / 2 - 1] + positions[positions.length / 2]) / 2 : positions[Math.floor(positions.length / 2)];
632
- desired.set(nodeId, median - nodeSize / 2);
811
+ const median = (nbCount & 1) === 0 ? (_neighborBuf[(nbCount >> 1) - 1] + _neighborBuf[nbCount >> 1]) / 2 : _neighborBuf[nbCount >> 1];
812
+ desired[i] = median - nodeSize / 2;
633
813
  }
634
- for (let i = 0; i < layer.length; i++) {
814
+ for (let i = 0; i < n; i++) {
635
815
  const nodeId = layer[i];
636
816
  const node = graph.nodes.get(nodeId);
637
- let desiredPos = desired.get(nodeId);
817
+ let desiredPos = desired[i];
638
818
  if (i > 0) {
639
819
  const prevId = layer[i - 1];
640
820
  const prev = graph.nodes.get(prevId);
641
821
  const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
642
- desiredPos = Math.max(desiredPos, prevEnd + options.nodeSpacing);
822
+ if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
643
823
  }
644
824
  setOrderPos(node, isHorizontal, desiredPos);
645
825
  }
646
- for (let i = layer.length - 2; i >= 0; i--) {
826
+ for (let i = n - 2; i >= 0; i--) {
647
827
  const nodeId = layer[i];
648
828
  const node = graph.nodes.get(nodeId);
649
829
  const nextId = layer[i + 1];
@@ -655,6 +835,17 @@ function optimizeLayer(graph, layer, options, isHorizontal) {
655
835
  }
656
836
  }
657
837
  }
838
+ function sortPrefix(arr, count) {
839
+ for (let i = 1; i < count; i++) {
840
+ const x = arr[i];
841
+ let j = i - 1;
842
+ while (j >= 0 && arr[j] > x) {
843
+ arr[j + 1] = arr[j];
844
+ j--;
845
+ }
846
+ arr[j + 1] = x;
847
+ }
848
+ }
658
849
  function centerAllLayers(graph, layers, isHorizontal) {
659
850
  if (layers.length === 0) return;
660
851
  let globalMin = Infinity;
@@ -710,6 +901,176 @@ function getOrderSize(node, isHorizontal) {
710
901
  return isHorizontal ? node.height : node.width;
711
902
  }
712
903
 
904
+ // src/algorithms/value-placement.ts
905
+ function placeValueSidecars(graph, layers, options) {
906
+ const isHorizontal = options.direction === "LR" || options.direction === "RL";
907
+ const attachments = [];
908
+ const orphanValues = [];
909
+ for (const [nodeId, node] of graph.nodes) {
910
+ if (!node.isValue || node.isDummy) continue;
911
+ const consumer = findDominantConsumer(graph, nodeId);
912
+ if (consumer) {
913
+ attachments.push({ valueId: nodeId, consumerId: consumer });
914
+ } else {
915
+ orphanValues.push(nodeId);
916
+ }
917
+ }
918
+ const byConsumer = /* @__PURE__ */ new Map();
919
+ for (const { valueId, consumerId } of attachments) {
920
+ const arr = byConsumer.get(consumerId) ?? [];
921
+ arr.push(valueId);
922
+ byConsumer.set(consumerId, arr);
923
+ }
924
+ for (const [consumerId, vIds] of byConsumer) {
925
+ const consumer = graph.nodes.get(consumerId);
926
+ if (!consumer) continue;
927
+ const { beforeSide, afterSide } = splitValuesBySide(graph, consumerId, vIds, isHorizontal);
928
+ if (isHorizontal) {
929
+ const cxCenter = consumer.x + consumer.width / 2;
930
+ let topEdge = consumer.y;
931
+ for (const vid of beforeSide) {
932
+ const v = graph.nodes.get(vid);
933
+ if (!v) continue;
934
+ v.y = topEdge - options.nodeSpacing - v.height;
935
+ v.x = cxCenter - v.width / 2;
936
+ topEdge = v.y;
937
+ }
938
+ let bottomEdge = consumer.y + consumer.height;
939
+ for (const vid of afterSide) {
940
+ const v = graph.nodes.get(vid);
941
+ if (!v) continue;
942
+ v.y = bottomEdge + options.nodeSpacing;
943
+ v.x = cxCenter - v.width / 2;
944
+ bottomEdge = v.y + v.height;
945
+ }
946
+ } else {
947
+ const cyCenter = consumer.y + consumer.height / 2;
948
+ let leftEdge = consumer.x;
949
+ for (const vid of beforeSide) {
950
+ const v = graph.nodes.get(vid);
951
+ if (!v) continue;
952
+ v.x = leftEdge - options.nodeSpacing - v.width;
953
+ v.y = cyCenter - v.height / 2;
954
+ leftEdge = v.x;
955
+ }
956
+ let rightEdge = consumer.x + consumer.width;
957
+ for (const vid of afterSide) {
958
+ const v = graph.nodes.get(vid);
959
+ if (!v) continue;
960
+ v.x = rightEdge + options.nodeSpacing;
961
+ v.y = cyCenter - v.height / 2;
962
+ rightEdge = v.x + v.width;
963
+ }
964
+ }
965
+ }
966
+ let orphanX = 0, orphanY = 0;
967
+ for (const orphanId of orphanValues) {
968
+ const v = graph.nodes.get(orphanId);
969
+ if (!v) continue;
970
+ v.x = orphanX;
971
+ v.y = orphanY;
972
+ if (isHorizontal) orphanX += v.width + options.nodeSpacing;
973
+ else orphanY += v.height + options.nodeSpacing;
974
+ }
975
+ }
976
+ function findDominantConsumer(graph, valueId) {
977
+ const counts = /* @__PURE__ */ new Map();
978
+ const outs = graph.outEdges.get(valueId);
979
+ if (outs) {
980
+ for (const eid of outs) {
981
+ const edge = graph.edges.get(eid);
982
+ if (!edge) continue;
983
+ const other = graph.nodes.get(edge.to);
984
+ if (other && !other.isValue && !other.isDummy) {
985
+ counts.set(edge.to, (counts.get(edge.to) ?? 0) + 1);
986
+ }
987
+ }
988
+ }
989
+ const ins = graph.inEdges.get(valueId);
990
+ if (ins) {
991
+ for (const eid of ins) {
992
+ const edge = graph.edges.get(eid);
993
+ if (!edge) continue;
994
+ const other = graph.nodes.get(edge.from);
995
+ if (other && !other.isValue && !other.isDummy) {
996
+ counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);
997
+ }
998
+ }
999
+ }
1000
+ let bestId = null;
1001
+ let bestCount = -1;
1002
+ for (const [id, count] of counts) {
1003
+ if (count > bestCount || count === bestCount && bestId !== null && id < bestId) {
1004
+ bestId = id;
1005
+ bestCount = count;
1006
+ }
1007
+ }
1008
+ return bestId;
1009
+ }
1010
+ function splitValuesBySide(graph, consumerId, valueIds, isHorizontal) {
1011
+ const consumer = graph.nodes.get(consumerId);
1012
+ if (!consumer) return { beforeSide: [...valueIds].sort(), afterSide: [] };
1013
+ const slots = [];
1014
+ for (const vId of valueIds) {
1015
+ let side = "neutral";
1016
+ let offset = 0.5;
1017
+ const inspectHandle = (handleId) => {
1018
+ if (!handleId) return;
1019
+ const handle = consumer.handles.find((h) => h.id === handleId);
1020
+ if (!handle) return;
1021
+ offset = handle.offset ?? 0.5;
1022
+ if (isHorizontal) {
1023
+ if (handle.position === "top") side = "before";
1024
+ else if (handle.position === "bottom") side = "after";
1025
+ } else {
1026
+ if (handle.position === "left") side = "before";
1027
+ else if (handle.position === "right") side = "after";
1028
+ }
1029
+ };
1030
+ const outs = graph.outEdges.get(vId);
1031
+ if (outs) for (const eid of outs) {
1032
+ const e = graph.edges.get(eid);
1033
+ if (e && e.to === consumerId) {
1034
+ inspectHandle(e.toHandle);
1035
+ break;
1036
+ }
1037
+ }
1038
+ if (side === "neutral") {
1039
+ const ins = graph.inEdges.get(vId);
1040
+ if (ins) for (const eid of ins) {
1041
+ const e = graph.edges.get(eid);
1042
+ if (e && e.from === consumerId) {
1043
+ inspectHandle(e.fromHandle);
1044
+ break;
1045
+ }
1046
+ }
1047
+ }
1048
+ slots.push({ id: vId, side, offset });
1049
+ }
1050
+ const before = slots.filter((s) => s.side === "before");
1051
+ const after = slots.filter((s) => s.side === "after");
1052
+ const neutral = slots.filter((s) => s.side === "neutral");
1053
+ for (const n of neutral) {
1054
+ if (before.length <= after.length) before.push(n);
1055
+ else after.push(n);
1056
+ }
1057
+ const cmp = (a, b) => a.offset - b.offset || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1058
+ before.sort(cmp);
1059
+ after.sort(cmp);
1060
+ return {
1061
+ beforeSide: before.map((s) => s.id),
1062
+ afterSide: after.map((s) => s.id)
1063
+ };
1064
+ }
1065
+ function railLayers(layers, graph) {
1066
+ return layers.map(
1067
+ (layer) => layer.filter((id) => {
1068
+ const n = graph.nodes.get(id);
1069
+ return n && !n.isValue;
1070
+ })
1071
+ );
1072
+ }
1073
+
713
1074
  // src/algorithms/edge-routing.ts
714
1075
  function routeEdges(graph, direction, edgeMargin) {
715
1076
  const chains = collectEdgeChains(graph);
@@ -760,7 +1121,8 @@ function collectEdgeChains(graph) {
760
1121
  fromHandle: edge.fromHandle,
761
1122
  toHandle: currentEdge.toHandle,
762
1123
  dummyNodes,
763
- reversed: edge.reversed
1124
+ reversed: edge.reversed,
1125
+ kind: edge.kind
764
1126
  });
765
1127
  }
766
1128
  return chains;
@@ -805,7 +1167,8 @@ function routeChain(graph, chain, direction, margin) {
805
1167
  to: actualTo,
806
1168
  fromHandle: actualFromHandle,
807
1169
  toHandle: actualToHandle,
808
- points
1170
+ points,
1171
+ kind: chain.kind
809
1172
  };
810
1173
  }
811
1174
  function makeOrthogonal(points, direction) {
@@ -879,21 +1242,375 @@ function getDefaultTargetSide(direction) {
879
1242
  }
880
1243
  }
881
1244
 
1245
+ // src/algorithms/component-packing.ts
1246
+ function packComponents(nodes, edges, options) {
1247
+ if (nodes.length === 0) return;
1248
+ const idToNode = /* @__PURE__ */ new Map();
1249
+ for (const n of nodes) idToNode.set(n.id, n);
1250
+ const rootOf = /* @__PURE__ */ new Map();
1251
+ function topAncestor(id) {
1252
+ if (rootOf.has(id)) return rootOf.get(id);
1253
+ const n = idToNode.get(id);
1254
+ if (!n || !n.parentId || !idToNode.has(n.parentId)) {
1255
+ rootOf.set(id, id);
1256
+ return id;
1257
+ }
1258
+ const r = topAncestor(n.parentId);
1259
+ rootOf.set(id, r);
1260
+ return r;
1261
+ }
1262
+ for (const n of nodes) topAncestor(n.id);
1263
+ const parent = /* @__PURE__ */ new Map();
1264
+ function find(x) {
1265
+ let cur = x;
1266
+ while (parent.get(cur) !== cur) {
1267
+ const p = parent.get(cur);
1268
+ parent.set(cur, parent.get(p));
1269
+ cur = parent.get(cur);
1270
+ }
1271
+ return cur;
1272
+ }
1273
+ function union(a, b) {
1274
+ const ra = find(a);
1275
+ const rb = find(b);
1276
+ if (ra !== rb) parent.set(ra, rb);
1277
+ }
1278
+ for (const n of nodes) parent.set(topAncestor(n.id), topAncestor(n.id));
1279
+ for (const e of edges) {
1280
+ const ra = topAncestor(e.from);
1281
+ const rb = topAncestor(e.to);
1282
+ if (idToNode.has(ra) && idToNode.has(rb)) union(ra, rb);
1283
+ }
1284
+ const groups = /* @__PURE__ */ new Map();
1285
+ for (const n of nodes) {
1286
+ const root = topAncestor(n.id);
1287
+ const comp = find(root);
1288
+ const arr = groups.get(comp) ?? [];
1289
+ arr.push(n);
1290
+ groups.set(comp, arr);
1291
+ }
1292
+ if (groups.size <= 1) return;
1293
+ const isHorizontal = options.direction === "LR" || options.direction === "RL";
1294
+ const edgesByComp = /* @__PURE__ */ new Map();
1295
+ for (const e of edges) {
1296
+ const root = topAncestor(e.from);
1297
+ const comp = find(root);
1298
+ const arr = edgesByComp.get(comp) ?? [];
1299
+ arr.push(e);
1300
+ edgesByComp.set(comp, arr);
1301
+ }
1302
+ const boxes = [];
1303
+ for (const [compId, group] of groups) {
1304
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1305
+ for (const n of group) {
1306
+ if (n.x < minX) minX = n.x;
1307
+ if (n.y < minY) minY = n.y;
1308
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1309
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1310
+ }
1311
+ const compEdges = edgesByComp.get(compId) ?? [];
1312
+ for (const e of compEdges) {
1313
+ for (const p of e.points) {
1314
+ if (p.x < minX) minX = p.x;
1315
+ if (p.y < minY) minY = p.y;
1316
+ if (p.x > maxX) maxX = p.x;
1317
+ if (p.y > maxY) maxY = p.y;
1318
+ }
1319
+ }
1320
+ boxes.push({ id: compId, nodes: group, edges: compEdges, minX, minY, maxX, maxY });
1321
+ }
1322
+ boxes.sort((a, b) => {
1323
+ const sizeA = isHorizontal ? a.maxX - a.minX : a.maxY - a.minY;
1324
+ const sizeB = isHorizontal ? b.maxX - b.minX : b.maxY - b.minY;
1325
+ return sizeB - sizeA;
1326
+ });
1327
+ const gap = options.layerSpacing;
1328
+ let cursor = 0;
1329
+ for (const box of boxes) {
1330
+ if (isHorizontal) {
1331
+ const dy = cursor - box.minY;
1332
+ const dx = -box.minX;
1333
+ for (const n of box.nodes) {
1334
+ n.x += dx;
1335
+ n.y += dy;
1336
+ for (const h of n.handles) {
1337
+ h.x += dx;
1338
+ h.y += dy;
1339
+ }
1340
+ }
1341
+ for (const e of box.edges) {
1342
+ for (const p of e.points) {
1343
+ p.x += dx;
1344
+ p.y += dy;
1345
+ }
1346
+ }
1347
+ cursor += box.maxY - box.minY + gap;
1348
+ } else {
1349
+ const dx = cursor - box.minX;
1350
+ const dy = -box.minY;
1351
+ for (const n of box.nodes) {
1352
+ n.x += dx;
1353
+ n.y += dy;
1354
+ for (const h of n.handles) {
1355
+ h.x += dx;
1356
+ h.y += dy;
1357
+ }
1358
+ }
1359
+ for (const e of box.edges) {
1360
+ for (const p of e.points) {
1361
+ p.x += dx;
1362
+ p.y += dy;
1363
+ }
1364
+ }
1365
+ cursor += box.maxX - box.minX + gap;
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ // src/proposals.ts
1371
+ function applyRotationProposals(input, options) {
1372
+ if (!options.onProposal) return input;
1373
+ const nodeIndex = new Map(input.nodes.map((n) => [n.id, n]));
1374
+ const newNodes = input.nodes.map((node) => {
1375
+ const role = classifyNode(node, input.edges);
1376
+ const expected = expectedSidesFor(node, role, input.edges, nodeIndex, options.direction);
1377
+ const proposal = computeProposal(node, expected, options.direction, role);
1378
+ if (!proposal) return node;
1379
+ const accepted = options.onProposal(proposal);
1380
+ return accepted ?? node;
1381
+ });
1382
+ return { nodes: newNodes, edges: input.edges };
1383
+ }
1384
+ function classifyNode(node, edges) {
1385
+ const incident = edges.filter((e) => e.from === node.id || e.to === node.id);
1386
+ if (incident.length === 0) return { kind: "isolated" };
1387
+ const allData = incident.every((e) => (e.kind ?? "control") === "data");
1388
+ if (!allData) return { kind: "rail" };
1389
+ for (const e of incident) {
1390
+ const consumerId = e.from === node.id ? e.to : e.from;
1391
+ const consumerHandleId = e.from === node.id ? e.toHandle : e.fromHandle;
1392
+ const sideHint = sideHintFromHandle(
1393
+ consumerId,
1394
+ consumerHandleId,
1395
+ edges,
1396
+ /* recursing */
1397
+ false
1398
+ );
1399
+ if (sideHint) return { kind: "value", consumerSide: sideHint };
1400
+ }
1401
+ return { kind: "value", consumerSide: null };
1402
+ }
1403
+ function sideHintFromHandle(_consumerId, _handleId, _edges, _recursing) {
1404
+ return null;
1405
+ }
1406
+ function expectedSidesFor(node, role, edges, index, direction) {
1407
+ if (role.kind === "isolated" || role.kind === "rail") {
1408
+ return { input: inputSideFor(direction), output: outputSideFor(direction) };
1409
+ }
1410
+ const consumerHandleSide = findConsumerHandleSide(node, edges, index);
1411
+ if (!consumerHandleSide) {
1412
+ return { input: inputSideFor(direction), output: outputSideFor(direction) };
1413
+ }
1414
+ const isVerticalFlow = direction === "TB" || direction === "BT";
1415
+ let outputSide;
1416
+ if (isVerticalFlow) {
1417
+ if (consumerHandleSide === "left") outputSide = "right";
1418
+ else if (consumerHandleSide === "right") outputSide = "left";
1419
+ else outputSide = "right";
1420
+ } else {
1421
+ if (consumerHandleSide === "top") outputSide = "bottom";
1422
+ else if (consumerHandleSide === "bottom") outputSide = "top";
1423
+ else outputSide = "bottom";
1424
+ }
1425
+ return { input: outputSide, output: outputSide };
1426
+ }
1427
+ function findConsumerHandleSide(value, edges, index) {
1428
+ for (const e of edges) {
1429
+ const isFromValue = e.from === value.id;
1430
+ const isToValue = e.to === value.id;
1431
+ if (!isFromValue && !isToValue) continue;
1432
+ const consumerId = isFromValue ? e.to : e.from;
1433
+ const consumer = index.get(consumerId);
1434
+ if (!consumer) continue;
1435
+ const consumerHandleId = isFromValue ? e.toHandle : e.fromHandle;
1436
+ const consumerHandle = consumer.handles.find((h) => h.id === consumerHandleId);
1437
+ if (consumerHandle) return consumerHandle.position;
1438
+ }
1439
+ return null;
1440
+ }
1441
+ function computeProposal(node, expected, direction, role) {
1442
+ if (node.handles.length === 0) return null;
1443
+ const currentScore = score(node.handles, expected);
1444
+ if (currentScore.matchRatio >= 1) return null;
1445
+ const candidates = [];
1446
+ for (const rot of [90, -90, 180]) {
1447
+ const rotated = rotateHandles(node.handles, rot);
1448
+ candidates.push({ rotation: rot, score: score(rotated, expected) });
1449
+ }
1450
+ candidates.sort((a, b) => b.score.matchRatio - a.score.matchRatio);
1451
+ const best = candidates[0];
1452
+ if (best.score.matchRatio <= currentScore.matchRatio) return null;
1453
+ return buildProposal(node, best.rotation, currentScore, best.score, direction, expected, role);
1454
+ }
1455
+ function buildProposal(node, rotation, current, proposedScore, direction, expected, role) {
1456
+ const proposed = { ...node, handles: rotateHandles(node.handles, rotation) };
1457
+ 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}`;
1458
+ return {
1459
+ type: "rotate",
1460
+ nodeId: node.id,
1461
+ current: node,
1462
+ proposed,
1463
+ rotation,
1464
+ reason: `${roleDesc}; ${current.matched}/${current.total} handles match, ${proposedScore.matched}/${proposedScore.total} after rotation`
1465
+ };
1466
+ }
1467
+ function inputSideFor(direction) {
1468
+ switch (direction) {
1469
+ case "TB":
1470
+ return "top";
1471
+ case "BT":
1472
+ return "bottom";
1473
+ case "LR":
1474
+ return "left";
1475
+ case "RL":
1476
+ return "right";
1477
+ }
1478
+ }
1479
+ function outputSideFor(direction) {
1480
+ switch (direction) {
1481
+ case "TB":
1482
+ return "bottom";
1483
+ case "BT":
1484
+ return "top";
1485
+ case "LR":
1486
+ return "right";
1487
+ case "RL":
1488
+ return "left";
1489
+ }
1490
+ }
1491
+ function score(handles, expected) {
1492
+ let matched = 0;
1493
+ let total = 0;
1494
+ for (const h of handles) {
1495
+ total++;
1496
+ if (h.type === "input" && h.position === expected.input) matched++;
1497
+ else if (h.type === "output" && h.position === expected.output) matched++;
1498
+ }
1499
+ return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
1500
+ }
1501
+ function applyRelocateProposals(input, preview, options) {
1502
+ if (!options.onProposal) return input;
1503
+ const previewIndex = new Map(preview.nodes.map((n) => [n.id, n]));
1504
+ const inputIndex = new Map(input.nodes.map((n) => [n.id, n]));
1505
+ const newNodes = input.nodes.map((node) => {
1506
+ const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex);
1507
+ if (!proposal) return node;
1508
+ const accepted = options.onProposal(proposal);
1509
+ return accepted ?? node;
1510
+ });
1511
+ return { nodes: newNodes, edges: input.edges };
1512
+ }
1513
+ function computeRelocateProposal(node, edges, inputIndex, previewIndex) {
1514
+ const selfPreview = previewIndex.get(node.id);
1515
+ if (!selfPreview) return null;
1516
+ if (node.handles.length === 0) return null;
1517
+ const usedHandleIds = /* @__PURE__ */ new Set();
1518
+ for (const e of edges) {
1519
+ if (e.from === node.id) usedHandleIds.add(e.fromHandle);
1520
+ if (e.to === node.id) usedHandleIds.add(e.toHandle);
1521
+ }
1522
+ if (usedHandleIds.size === 0) return null;
1523
+ const votesByHandle = /* @__PURE__ */ new Map();
1524
+ const selfCenter = {
1525
+ x: selfPreview.x + selfPreview.width / 2,
1526
+ y: selfPreview.y + selfPreview.height / 2
1527
+ };
1528
+ for (const e of edges) {
1529
+ const isFrom = e.from === node.id;
1530
+ const isTo = e.to === node.id;
1531
+ if (!isFrom && !isTo) continue;
1532
+ const handleId = isFrom ? e.fromHandle : e.toHandle;
1533
+ const neighborId = isFrom ? e.to : e.from;
1534
+ const neighborPreview = previewIndex.get(neighborId);
1535
+ if (!neighborPreview) continue;
1536
+ const neighborCenter = {
1537
+ x: neighborPreview.x + neighborPreview.width / 2,
1538
+ y: neighborPreview.y + neighborPreview.height / 2
1539
+ };
1540
+ const dx = neighborCenter.x - selfCenter.x;
1541
+ const dy = neighborCenter.y - selfCenter.y;
1542
+ const side = Math.abs(dx) >= Math.abs(dy) ? dx >= 0 ? "right" : "left" : dy >= 0 ? "bottom" : "top";
1543
+ const dist = Math.hypot(dx, dy);
1544
+ const weight = dist > 0 ? 1 / dist : 1;
1545
+ const arr = votesByHandle.get(handleId) ?? [];
1546
+ arr.push({ side, weight });
1547
+ votesByHandle.set(handleId, arr);
1548
+ }
1549
+ const changes = {};
1550
+ const newHandles = node.handles.map((h) => {
1551
+ const votes = votesByHandle.get(h.id);
1552
+ if (!votes || votes.length === 0) return h;
1553
+ const tallies = { top: 0, right: 0, bottom: 0, left: 0 };
1554
+ for (const v of votes) tallies[v.side] += v.weight;
1555
+ const best = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
1556
+ if (best !== h.position) {
1557
+ changes[h.id] = { from: h.position, to: best };
1558
+ return { ...h, position: best };
1559
+ }
1560
+ return h;
1561
+ });
1562
+ if (Object.keys(changes).length === 0) return null;
1563
+ const proposed = { ...node, handles: newHandles };
1564
+ const summary = Object.entries(changes).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
1565
+ return {
1566
+ type: "relocate-handles",
1567
+ nodeId: node.id,
1568
+ current: node,
1569
+ proposed,
1570
+ changes,
1571
+ reason: `${Object.keys(changes).length} handle(s) point away from their neighbor (${summary})`
1572
+ };
1573
+ }
1574
+ function rotateHandles(handles, rot) {
1575
+ const rotateSide = (s) => {
1576
+ if (rot === 180) {
1577
+ return s === "top" ? "bottom" : s === "bottom" ? "top" : s === "left" ? "right" : "left";
1578
+ }
1579
+ if (rot === 90) {
1580
+ return s === "top" ? "right" : s === "right" ? "bottom" : s === "bottom" ? "left" : "top";
1581
+ }
1582
+ return s === "top" ? "left" : s === "left" ? "bottom" : s === "bottom" ? "right" : "top";
1583
+ };
1584
+ return handles.map((h) => ({ ...h, position: rotateSide(h.position) }));
1585
+ }
1586
+
882
1587
  // src/layout.ts
1588
+ var COMPOUND_HEADER = 28;
883
1589
  function layout(input, options) {
884
1590
  const resolved = resolveOptions(options);
885
- const graph = buildGraph(input.nodes, input.edges);
886
- return computeLayout(graph, resolved);
1591
+ if (input.nodes.length === 0) return { nodes: [], edges: [] };
1592
+ let adjusted = applyRotationProposals(input, resolved);
1593
+ if (resolved.onProposal) {
1594
+ const previewOptions = { ...resolved, onProposal: void 0 };
1595
+ const preview = layoutCompound(adjusted, previewOptions);
1596
+ adjusted = applyRelocateProposals(adjusted, preview, resolved);
1597
+ }
1598
+ return layoutCompound(adjusted, resolved);
887
1599
  }
888
1600
  function computeLayout(graph, options) {
889
- if (graph.nodes.size === 0) {
890
- return { nodes: [], edges: [] };
891
- }
1601
+ if (graph.nodes.size === 0) return { nodes: [], edges: [] };
892
1602
  breakCycles(graph);
893
1603
  let layers = assignLayers(graph);
894
1604
  layers = insertDummyNodes(graph, layers);
895
- layers = minimizeCrossings(graph, layers, options.crossingMinimizationIterations);
896
- assignCoordinates(graph, layers, options);
1605
+ let rail = railLayers(layers, graph);
1606
+ rail = minimizeCrossings(
1607
+ graph,
1608
+ rail,
1609
+ options.crossingMinimizationIterations,
1610
+ options.skipTranspose
1611
+ );
1612
+ assignCoordinates(graph, rail, options);
1613
+ placeValueSidecars(graph, layers, options);
897
1614
  const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
898
1615
  return buildResult(graph, routedEdges);
899
1616
  }
@@ -904,13 +1621,7 @@ function buildResult(graph, routedEdges) {
904
1621
  if (node.isDummy) continue;
905
1622
  const handles = node.handles.map((h) => {
906
1623
  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
- };
1624
+ return { id: h.id, type: h.type, position: h.position, x: pos.x, y: pos.y };
914
1625
  });
915
1626
  nodes.push({
916
1627
  id: node.id,
@@ -918,7 +1629,8 @@ function buildResult(graph, routedEdges) {
918
1629
  y: node.y,
919
1630
  width: node.width,
920
1631
  height: node.height,
921
- handles
1632
+ handles,
1633
+ parentId: node.parentId
922
1634
  });
923
1635
  }
924
1636
  for (const route of routedEdges) {
@@ -928,11 +1640,146 @@ function buildResult(graph, routedEdges) {
928
1640
  to: route.to,
929
1641
  fromHandle: route.fromHandle,
930
1642
  toHandle: route.toHandle,
931
- points: route.points
1643
+ points: route.points,
1644
+ kind: route.kind
932
1645
  });
933
1646
  }
934
1647
  return { nodes, edges };
935
1648
  }
1649
+ function layoutCompound(input, options) {
1650
+ const nodeMap = new Map(input.nodes.map((n) => [n.id, n]));
1651
+ const childrenByParent = /* @__PURE__ */ new Map();
1652
+ const rootNodes = [];
1653
+ for (const n of input.nodes) {
1654
+ if (n.parentId && nodeMap.has(n.parentId)) {
1655
+ const arr = childrenByParent.get(n.parentId) ?? [];
1656
+ arr.push(n);
1657
+ childrenByParent.set(n.parentId, arr);
1658
+ } else {
1659
+ rootNodes.push(n);
1660
+ }
1661
+ }
1662
+ const compoundIds = new Set(childrenByParent.keys());
1663
+ const depthCache = /* @__PURE__ */ new Map();
1664
+ function depthOf(id) {
1665
+ if (depthCache.has(id)) return depthCache.get(id);
1666
+ const n = nodeMap.get(id);
1667
+ const d = n?.parentId && nodeMap.has(n.parentId) ? depthOf(n.parentId) + 1 : 0;
1668
+ depthCache.set(id, d);
1669
+ return d;
1670
+ }
1671
+ for (const n of input.nodes) depthOf(n.id);
1672
+ const sortedCompounds = [...compoundIds].sort((a, b) => depthOf(b) - depthOf(a));
1673
+ const subLayouts = /* @__PURE__ */ new Map();
1674
+ const compoundSize = /* @__PURE__ */ new Map();
1675
+ for (const compoundId of sortedCompounds) {
1676
+ const children = childrenByParent.get(compoundId) ?? [];
1677
+ if (children.length === 0) continue;
1678
+ const childIdSet = new Set(children.map((c) => c.id));
1679
+ const subEdges = input.edges.filter((e) => childIdSet.has(e.from) && childIdSet.has(e.to));
1680
+ const sizedChildren = children.map((c) => {
1681
+ const sz = compoundSize.get(c.id);
1682
+ return sz ? { ...c, width: sz.width, height: sz.height } : c;
1683
+ });
1684
+ const subOptions = { ...options, packComponents: false };
1685
+ const subInput = { nodes: sizedChildren.map(stripParent), edges: subEdges };
1686
+ const subResult = layoutFlat(subInput, subOptions);
1687
+ subLayouts.set(compoundId, subResult);
1688
+ const bbox = computeBoundingBox(subResult.nodes);
1689
+ const innerW = bbox.width + options.compoundPadding * 2;
1690
+ const innerH = bbox.height + options.compoundPadding * 2 + COMPOUND_HEADER;
1691
+ const original = nodeMap.get(compoundId);
1692
+ compoundSize.set(compoundId, {
1693
+ width: Math.max(innerW, original.width),
1694
+ height: Math.max(innerH, original.height)
1695
+ });
1696
+ }
1697
+ const sizedRootNodes = rootNodes.map((n) => {
1698
+ const sz = compoundSize.get(n.id);
1699
+ return sz ? { ...n, width: sz.width, height: sz.height } : n;
1700
+ });
1701
+ const rootIdSet = new Set(rootNodes.map((n) => n.id));
1702
+ const rootEdges = input.edges.filter((e) => {
1703
+ return rootIdSet.has(e.from) && rootIdSet.has(e.to);
1704
+ });
1705
+ const rootResult = layoutFlat({ nodes: sizedRootNodes.map(stripParent), edges: rootEdges }, options);
1706
+ const finalNodes = [];
1707
+ const finalEdges = [];
1708
+ for (const n of rootResult.nodes) {
1709
+ const wasCompound = compoundIds.has(n.id);
1710
+ finalNodes.push({
1711
+ ...n,
1712
+ parentId: nodeMap.get(n.id)?.parentId
1713
+ });
1714
+ if (wasCompound) {
1715
+ placeCompoundChildren(n, (compoundId) => subLayouts.get(compoundId), finalNodes, finalEdges, options, nodeMap);
1716
+ }
1717
+ }
1718
+ finalEdges.push(...rootResult.edges);
1719
+ if (options.packComponents) {
1720
+ packComponents(finalNodes, finalEdges, options);
1721
+ }
1722
+ return { nodes: finalNodes, edges: finalEdges };
1723
+ }
1724
+ function placeCompoundChildren(parent, getSub, finalNodes, finalEdges, options, nodeMap) {
1725
+ const sub = getSub(parent.id);
1726
+ if (!sub) return;
1727
+ const bbox = computeBoundingBox(sub.nodes);
1728
+ const dx = parent.x + options.compoundPadding - bbox.minX;
1729
+ const dy = parent.y + options.compoundPadding + COMPOUND_HEADER - bbox.minY;
1730
+ const availableW = parent.width - options.compoundPadding * 2;
1731
+ const slackX = (availableW - bbox.width) / 2;
1732
+ const availableH = parent.height - options.compoundPadding * 2 - COMPOUND_HEADER;
1733
+ const slackY = (availableH - bbox.height) / 2;
1734
+ const cx = dx + Math.max(0, slackX);
1735
+ const cy = dy + Math.max(0, slackY);
1736
+ for (const child of sub.nodes) {
1737
+ const placed = {
1738
+ ...child,
1739
+ x: child.x + cx,
1740
+ y: child.y + cy,
1741
+ handles: child.handles.map((h) => ({ ...h, x: h.x + cx, y: h.y + cy })),
1742
+ parentId: nodeMap.get(child.id)?.parentId ?? parent.id
1743
+ };
1744
+ finalNodes.push(placed);
1745
+ if (getSub(child.id)) {
1746
+ placeCompoundChildren(placed, getSub, finalNodes, finalEdges, options, nodeMap);
1747
+ }
1748
+ }
1749
+ for (const edge of sub.edges) {
1750
+ finalEdges.push({
1751
+ ...edge,
1752
+ points: edge.points.map((p) => ({ x: p.x + cx, y: p.y + cy }))
1753
+ });
1754
+ }
1755
+ }
1756
+ function stripParent(n) {
1757
+ const { parentId: _ignored, ...rest } = n;
1758
+ return rest;
1759
+ }
1760
+ function layoutFlat(input, options) {
1761
+ const graph = buildGraph(input.nodes, input.edges, {
1762
+ controlWeight: options.controlWeight,
1763
+ dataWeight: options.dataWeight
1764
+ });
1765
+ return computeLayout(graph, options);
1766
+ }
1767
+ function computeBoundingBox(nodes) {
1768
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1769
+ for (const n of nodes) {
1770
+ if (n.x < minX) minX = n.x;
1771
+ if (n.y < minY) minY = n.y;
1772
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1773
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1774
+ }
1775
+ if (!isFinite(minX)) {
1776
+ minX = 0;
1777
+ minY = 0;
1778
+ maxX = 0;
1779
+ maxY = 0;
1780
+ }
1781
+ return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
1782
+ }
936
1783
 
937
1784
  // src/incremental.ts
938
1785
  var IncrementalLayout = class {
@@ -941,82 +1788,40 @@ var IncrementalLayout = class {
941
1788
  this.inputEdges = /* @__PURE__ */ new Map();
942
1789
  this.lastResult = null;
943
1790
  this.nodePositions = /* @__PURE__ */ new Map();
944
- this.options = resolveOptions(options);
1791
+ this.options = options;
1792
+ this.resolvedOptions = resolveOptions(options);
945
1793
  }
946
- /**
947
- * Set the full graph and compute a complete layout.
948
- */
949
1794
  setGraph(input) {
950
1795
  this.inputNodes.clear();
951
1796
  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
- }
1797
+ for (const node of input.nodes) this.inputNodes.set(node.id, node);
1798
+ for (const edge of input.edges) this.inputEdges.set(edge.id, edge);
958
1799
  return this.recompute();
959
1800
  }
960
- /**
961
- * Add nodes and edges incrementally.
962
- * Attempts to minimize layout changes for existing nodes.
963
- */
964
1801
  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
- );
1802
+ for (const node of nodes) this.inputNodes.set(node.id, node);
1803
+ if (edges) for (const edge of edges) this.inputEdges.set(edge.id, edge);
1804
+ return this.recomputeWithStability(new Set(nodes.map((n) => n.id)));
977
1805
  }
978
- /**
979
- * Remove nodes (and their connected edges) from the layout.
980
- */
981
1806
  removeNodes(nodeIds) {
982
1807
  const removedSet = new Set(nodeIds);
983
- for (const id of nodeIds) {
984
- this.inputNodes.delete(id);
985
- }
1808
+ for (const id of nodeIds) this.inputNodes.delete(id);
986
1809
  for (const [edgeId, edge] of this.inputEdges) {
987
1810
  if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
988
1811
  this.inputEdges.delete(edgeId);
989
1812
  }
990
1813
  }
991
- for (const id of nodeIds) {
992
- this.nodePositions.delete(id);
993
- }
1814
+ for (const id of nodeIds) this.nodePositions.delete(id);
994
1815
  return this.recompute();
995
1816
  }
996
- /**
997
- * Add edges between existing nodes.
998
- */
999
1817
  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
- );
1818
+ for (const edge of edges) this.inputEdges.set(edge.id, edge);
1819
+ return this.recomputeWithStability(/* @__PURE__ */ new Set());
1007
1820
  }
1008
- /**
1009
- * Remove edges from the layout.
1010
- */
1011
1821
  removeEdges(edgeIds) {
1012
- for (const id of edgeIds) {
1013
- this.inputEdges.delete(id);
1014
- }
1822
+ for (const id of edgeIds) this.inputEdges.delete(id);
1015
1823
  return this.recompute();
1016
1824
  }
1017
- /**
1018
- * Get the current layout result.
1019
- */
1020
1825
  getResult() {
1021
1826
  return this.lastResult;
1022
1827
  }
@@ -1025,42 +1830,48 @@ var IncrementalLayout = class {
1025
1830
  nodes: [...this.inputNodes.values()],
1026
1831
  edges: [...this.inputEdges.values()]
1027
1832
  };
1028
- const graph = buildGraph(input.nodes, input.edges);
1029
- this.lastResult = computeLayout(graph, this.options);
1833
+ this.lastResult = layout(input, this.options);
1030
1834
  this.cachePositions();
1031
1835
  return this.lastResult;
1032
1836
  }
1033
- recomputeIncremental(newNodeIds, newEdgeIds) {
1837
+ recomputeWithStability(newNodeIds) {
1034
1838
  const input = {
1035
1839
  nodes: [...this.inputNodes.values()],
1036
1840
  edges: [...this.inputEdges.values()]
1037
1841
  };
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);
1842
+ const fresh = layout(input, this.options);
1843
+ this.lastResult = this.applyStability(fresh, newNodeIds);
1047
1844
  this.cachePositions();
1048
1845
  return this.lastResult;
1049
1846
  }
1050
1847
  /**
1051
- * Apply stability: blend new positions with old positions for existing nodes.
1052
- * This reduces visual disruption when adding new nodes.
1848
+ * Blend the freshly computed positions with the previous ones, so existing
1849
+ * nodes don't jump too far when a small change happens.
1053
1850
  */
1054
- applyStability(graph, newNodeIds) {
1055
- if (this.nodePositions.size === 0) return;
1851
+ applyStability(fresh, newNodeIds) {
1852
+ if (this.nodePositions.size === 0) return fresh;
1056
1853
  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
- }
1854
+ const blended = {
1855
+ nodes: fresh.nodes.map((n) => {
1856
+ if (newNodeIds.has(n.id)) return n;
1857
+ const old = this.nodePositions.get(n.id);
1858
+ if (!old) return n;
1859
+ const dx = old.x - n.x;
1860
+ const dy = old.y - n.y;
1861
+ const x = n.x + dx * STABILITY_WEIGHT;
1862
+ const y = n.y + dy * STABILITY_WEIGHT;
1863
+ const ddx = x - n.x;
1864
+ const ddy = y - n.y;
1865
+ return {
1866
+ ...n,
1867
+ x,
1868
+ y,
1869
+ handles: n.handles.map((h) => ({ ...h, x: h.x + ddx, y: h.y + ddy }))
1870
+ };
1871
+ }),
1872
+ edges: fresh.edges
1873
+ };
1874
+ return blended;
1064
1875
  }
1065
1876
  cachePositions() {
1066
1877
  if (!this.lastResult) return;
@@ -1069,47 +1880,161 @@ var IncrementalLayout = class {
1069
1880
  this.nodePositions.set(node.id, { x: node.x, y: node.y });
1070
1881
  }
1071
1882
  }
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
- });
1883
+ };
1884
+
1885
+ // src/debug.ts
1886
+ function printLayout(result, options = {}) {
1887
+ const lines = [];
1888
+ const { nodes, edges } = result;
1889
+ if (nodes.length === 0) return "(empty layout)";
1890
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1891
+ for (const n of nodes) {
1892
+ if (n.x < minX) minX = n.x;
1893
+ if (n.y < minY) minY = n.y;
1894
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1895
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1896
+ }
1897
+ lines.push(`=== layout ${nodes.length} nodes, ${edges.length} edges ===`);
1898
+ 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)})`);
1899
+ const byY = [...nodes].sort((a, b) => a.y - b.y || a.x - b.x);
1900
+ const bands = [];
1901
+ for (const n of byY) {
1902
+ const placed = bands.find((b) => Math.abs(b[0].y - n.y) < 1);
1903
+ if (placed) placed.push(n);
1904
+ else bands.push([n]);
1905
+ }
1906
+ lines.push("");
1907
+ lines.push("--- Y bands ---");
1908
+ for (const band of bands) {
1909
+ const sorted = [...band].sort((a, b) => a.x - b.x);
1910
+ const summary = sorted.map((n) => {
1911
+ const p = n.parentId ? `[${n.parentId}/]` : "";
1912
+ return `${p}${n.id}@(${n.x.toFixed(0)},${n.y.toFixed(0)} ${n.width}x${n.height})`;
1913
+ }).join(" ");
1914
+ lines.push(` y=${band[0].y.toFixed(0).padStart(4)} : ${summary}`);
1915
+ }
1916
+ const byParent = /* @__PURE__ */ new Map();
1917
+ for (const n of nodes) {
1918
+ const key = n.parentId;
1919
+ const arr = byParent.get(key) ?? [];
1920
+ arr.push(n);
1921
+ byParent.set(key, arr);
1922
+ }
1923
+ const compoundIds = /* @__PURE__ */ new Set();
1924
+ for (const k of byParent.keys()) {
1925
+ if (k && nodes.find((n) => n.id === k)) compoundIds.add(k);
1926
+ }
1927
+ if (compoundIds.size > 0) {
1928
+ let printSubtree2 = function(id, depth) {
1929
+ const children = byParent.get(id) ?? [];
1930
+ for (const c of children) {
1931
+ const prefix = " ".repeat(depth);
1932
+ const tag = compoundIds.has(c.id) ? " (compound)" : "";
1933
+ 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)})`);
1934
+ if (compoundIds.has(c.id)) printSubtree2(c.id, depth + 1);
1935
+ }
1936
+ };
1937
+ var printSubtree = printSubtree2;
1938
+ lines.push("");
1939
+ lines.push("--- hierarchy ---");
1940
+ printSubtree2(void 0, 0);
1941
+ }
1942
+ lines.push("");
1943
+ lines.push("--- edges ---");
1944
+ for (const e of edges) {
1945
+ const head = e.points[0];
1946
+ const tail = e.points[e.points.length - 1];
1947
+ 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`);
1948
+ }
1949
+ const overlaps = findOverlaps(nodes);
1950
+ if (overlaps.length > 0) {
1951
+ lines.push("");
1952
+ lines.push("--- OVERLAPS (problem!) ---");
1953
+ for (const o of overlaps) {
1954
+ lines.push(` ${o.a} overlaps ${o.b}`);
1095
1955
  }
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
- });
1956
+ }
1957
+ if (options.grid !== false) {
1958
+ lines.push("");
1959
+ lines.push("--- ASCII grid ---");
1960
+ lines.push(asciiGrid(nodes, edges, options));
1961
+ }
1962
+ return lines.join("\n");
1963
+ }
1964
+ function findOverlaps(nodes) {
1965
+ const out = [];
1966
+ const compoundIds = /* @__PURE__ */ new Set();
1967
+ for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
1968
+ for (let i = 0; i < nodes.length; i++) {
1969
+ for (let j = i + 1; j < nodes.length; j++) {
1970
+ const a = nodes[i];
1971
+ const b = nodes[j];
1972
+ if (a.id === b.parentId || b.id === a.parentId) continue;
1973
+ const overlapX = a.x < b.x + b.width && b.x < a.x + a.width;
1974
+ const overlapY = a.y < b.y + b.height && b.y < a.y + a.height;
1975
+ if (overlapX && overlapY) {
1976
+ out.push({ a: a.id, b: b.id });
1977
+ }
1105
1978
  }
1106
- return { nodes, edges };
1107
1979
  }
1108
- };
1980
+ return out;
1981
+ }
1982
+ function asciiGrid(nodes, edges, options) {
1983
+ if (nodes.length === 0) return "";
1984
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1985
+ for (const n of nodes) {
1986
+ if (n.x < minX) minX = n.x;
1987
+ if (n.y < minY) minY = n.y;
1988
+ if (n.x + n.width > maxX) maxX = n.x + n.width;
1989
+ if (n.y + n.height > maxY) maxY = n.y + n.height;
1990
+ }
1991
+ for (const e of edges) for (const p of e.points) {
1992
+ if (p.x < minX) minX = p.x;
1993
+ if (p.y < minY) minY = p.y;
1994
+ if (p.x > maxX) maxX = p.x;
1995
+ if (p.y > maxY) maxY = p.y;
1996
+ }
1997
+ const targetW = options.gridWidth ?? 80;
1998
+ const layoutW = Math.max(1, maxX - minX);
1999
+ const layoutH = Math.max(1, maxY - minY);
2000
+ const scaleX = options.gridScale ?? targetW / layoutW;
2001
+ const scaleY = scaleX * 0.5;
2002
+ const w = Math.max(1, Math.ceil(layoutW * scaleX) + 1);
2003
+ const h = Math.max(1, Math.ceil(layoutH * scaleY) + 1);
2004
+ if (h > 80) return "(grid suppressed: too tall \u2014 pass {grid:false} to skip)";
2005
+ const grid = Array.from({ length: h }, () => Array.from({ length: w }, () => " "));
2006
+ const compoundIds = /* @__PURE__ */ new Set();
2007
+ for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
2008
+ for (const n of nodes) {
2009
+ const x0 = Math.round((n.x - minX) * scaleX);
2010
+ const y0 = Math.round((n.y - minY) * scaleY);
2011
+ const x1 = Math.max(x0, Math.round((n.x + n.width - minX) * scaleX) - 1);
2012
+ const y1 = Math.max(y0, Math.round((n.y + n.height - minY) * scaleY) - 1);
2013
+ const isCompound = compoundIds.has(n.id);
2014
+ const ch = isCompound ? "." : "#";
2015
+ for (let y = y0; y <= y1 && y < h; y++) {
2016
+ for (let x = x0; x <= x1 && x < w; x++) {
2017
+ if (y < 0 || x < 0) continue;
2018
+ if (y === y0 || y === y1 || x === x0 || x === x1) grid[y][x] = ch;
2019
+ else if (isCompound) {
2020
+ } else grid[y][x] = ch;
2021
+ }
2022
+ }
2023
+ const label = n.id.slice(0, Math.min(n.id.length, Math.max(2, x1 - x0 - 1)));
2024
+ const lx = Math.max(0, x0 + 1);
2025
+ const ly = Math.max(0, y0);
2026
+ for (let i = 0; i < label.length && lx + i < w; i++) {
2027
+ if (ly < h) grid[ly][lx + i] = label[i];
2028
+ }
2029
+ }
2030
+ return grid.map((row) => row.join("")).join("\n");
2031
+ }
1109
2032
  // Annotate the CommonJS export names for ESM import in node:
1110
2033
  0 && (module.exports = {
1111
2034
  IncrementalLayout,
1112
2035
  countAllCrossings,
1113
- layout
2036
+ layout,
2037
+ printLayout,
2038
+ rotateHandles
1114
2039
  });
1115
2040
  //# sourceMappingURL=index.js.map