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