@nodius/layouting 0.1.1 → 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.
@@ -1,7 +1,11 @@
1
1
  import { Graph } from '../graph';
2
2
  /**
3
3
  * Minimize edge crossings using barycenter heuristic with transpose improvement.
4
+ *
5
+ * @param iterations Maximum number of barycenter sweep iterations.
6
+ * @param forceSkipTranspose When true, never run the transpose pass — used by
7
+ * the 'draft' quality preset for ~3× speedup at the cost of some crossings.
4
8
  */
5
- export declare function minimizeCrossings(graph: Graph, layers: string[][], iterations: number): string[][];
9
+ export declare function minimizeCrossings(graph: Graph, layers: string[][], iterations: number, forceSkipTranspose?: boolean): string[][];
6
10
  export declare function countAllCrossings(graph: Graph, layers: string[][]): number;
7
11
  //# sourceMappingURL=crossing-minimization.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crossing-minimization.d.ts","sourceRoot":"","sources":["../../src/algorithms/crossing-minimization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAAE,EAAE,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,EAAE,CAqEZ;AAgMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAM1E"}
1
+ {"version":3,"file":"crossing-minimization.d.ts","sourceRoot":"","sources":["../../src/algorithms/crossing-minimization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAAE,EAAE,EAClB,UAAU,EAAE,MAAM,EAClB,kBAAkB,GAAE,OAAe,GAClC,MAAM,EAAE,EAAE,CAqEZ;AA8MD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAM1E"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { layout } from './layout';
2
2
  export { IncrementalLayout } from './incremental';
3
3
  export { countAllCrossings } from './algorithms/crossing-minimization';
4
4
  export { printLayout } from './debug';
5
- export type { HandleSide, HandleType, LayoutDirection, EdgeKind, EdgeWeights, Point, HandleInput, NodeInput, EdgeInput, LayoutInput, LayoutOptions, HandleOutput, NodeOutput, EdgeOutput, LayoutResult, LayoutProposal, RotateProposal, ProposalCallback, } from './types';
5
+ export type { HandleSide, HandleType, LayoutDirection, LayoutQuality, EdgeKind, EdgeWeights, Point, HandleInput, NodeInput, EdgeInput, LayoutInput, LayoutOptions, HandleOutput, NodeOutput, EdgeOutput, LayoutResult, LayoutProposal, RotateProposal, RelocateHandlesProposal, ProposalCallback, } from './types';
6
6
  export { rotateHandles } from './proposals';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,YAAY,EACV,UAAU,EACV,UAAU,EACV,eAAe,EACf,QAAQ,EACR,WAAW,EACX,KAAK,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,YAAY,EACV,UAAU,EACV,UAAU,EACV,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,KAAK,EACL,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -29,13 +29,34 @@ __export(index_exports, {
29
29
  module.exports = __toCommonJS(index_exports);
30
30
 
31
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
+ };
32
49
  function resolveOptions(options) {
50
+ const quality = options?.quality ?? "balanced";
51
+ const profile = QUALITY_PROFILES[quality];
33
52
  return {
34
53
  direction: options?.direction ?? "TB",
54
+ quality,
35
55
  nodeSpacing: options?.nodeSpacing ?? 40,
36
56
  layerSpacing: options?.layerSpacing ?? 60,
37
- crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
38
- coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
57
+ crossingMinimizationIterations: options?.crossingMinimizationIterations ?? profile.crossingMinimizationIterations,
58
+ coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? profile.coordinateOptimizationIterations,
59
+ skipTranspose: options?.skipTranspose ?? profile.skipTranspose,
39
60
  edgeMargin: options?.edgeMargin ?? 20,
40
61
  controlWeight: options?.edgeWeights?.control ?? 1,
41
62
  dataWeight: options?.edgeWeights?.data ?? 0.25,
@@ -400,11 +421,11 @@ function insertDummyNodes(graph, layers) {
400
421
  }
401
422
 
402
423
  // src/algorithms/crossing-minimization.ts
403
- function minimizeCrossings(graph, layers, iterations) {
424
+ function minimizeCrossings(graph, layers, iterations, forceSkipTranspose = false) {
404
425
  if (layers.length <= 1) return layers;
405
426
  const totalNodes = layers.reduce((s, l) => s + l.length, 0);
406
427
  const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
407
- const skipTranspose = totalNodes > 800;
428
+ const skipTranspose = forceSkipTranspose || totalNodes > 800;
408
429
  for (let l = 0; l < layers.length; l++) {
409
430
  for (let i = 0; i < layers[l].length; i++) {
410
431
  const node = graph.nodes.get(layers[l][i]);
@@ -451,45 +472,54 @@ function orderByBarycenter(graph, layers, layerIndex, direction) {
451
472
  const layer = layers[layerIndex];
452
473
  const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
453
474
  if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
454
- const adjLayerSet = new Set(layers[adjLayerIndex]);
455
- const barycenters = /* @__PURE__ */ new Map();
456
- for (let i = 0; i < layer.length; i++) {
457
- const nodeId = layer[i];
458
- let sum = 0;
459
- let count = 0;
460
- 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;
461
482
  const inEdgeIds = graph.inEdges.get(nodeId);
462
483
  if (inEdgeIds) {
463
484
  for (const eid of inEdgeIds) {
464
485
  const edge = graph.edges.get(eid);
465
- if (edge && adjLayerSet.has(edge.from)) {
466
- const neighbor = graph.nodes.get(edge.from);
467
- if (neighbor) {
468
- sum += neighbor.order;
469
- count++;
470
- }
486
+ if (!edge) continue;
487
+ const neighbor = graph.nodes.get(edge.from);
488
+ if (neighbor && neighbor.layer === adjLayerIndex) {
489
+ sum += neighbor.order;
490
+ count++;
471
491
  }
472
492
  }
473
493
  }
474
- } 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;
475
501
  const outEdgeIds = graph.outEdges.get(nodeId);
476
502
  if (outEdgeIds) {
477
503
  for (const eid of outEdgeIds) {
478
504
  const edge = graph.edges.get(eid);
479
- if (edge && adjLayerSet.has(edge.to)) {
480
- const neighbor = graph.nodes.get(edge.to);
481
- if (neighbor) {
482
- sum += neighbor.order;
483
- count++;
484
- }
505
+ if (!edge) continue;
506
+ const neighbor = graph.nodes.get(edge.to);
507
+ if (neighbor && neighbor.layer === adjLayerIndex) {
508
+ sum += neighbor.order;
509
+ count++;
485
510
  }
486
511
  }
487
512
  }
513
+ bary[i] = count > 0 ? sum / count : i;
488
514
  }
489
- barycenters.set(nodeId, count > 0 ? sum / count : i);
490
515
  }
491
- layer.sort((a, b) => barycenters.get(a) - barycenters.get(b));
492
- 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];
493
523
  const node = graph.nodes.get(layer[i]);
494
524
  if (node) node.order = i;
495
525
  }
@@ -524,65 +554,65 @@ function transposeImprove(graph, layers, layerIndex) {
524
554
  }
525
555
  }
526
556
  }
557
+ var _scratchA = [];
558
+ var _scratchB = [];
527
559
  function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
528
560
  let crossings = 0;
529
561
  if (layerIndex > 0) {
530
- const upperLayer = new Set(layers[layerIndex - 1]);
531
- const predsA = [];
532
- const predsB = [];
562
+ const upperIdx = layerIndex - 1;
563
+ _scratchA.length = 0;
564
+ _scratchB.length = 0;
533
565
  const inA = graph.inEdges.get(nodeA);
534
566
  if (inA) {
535
567
  for (const eid of inA) {
536
568
  const edge = graph.edges.get(eid);
537
- if (edge && upperLayer.has(edge.from)) {
538
- const n = graph.nodes.get(edge.from);
539
- if (n) predsA.push(n.order);
540
- }
569
+ if (!edge) continue;
570
+ const n = graph.nodes.get(edge.from);
571
+ if (n && n.layer === upperIdx) _scratchA.push(n.order);
541
572
  }
542
573
  }
543
574
  const inB = graph.inEdges.get(nodeB);
544
575
  if (inB) {
545
576
  for (const eid of inB) {
546
577
  const edge = graph.edges.get(eid);
547
- if (edge && upperLayer.has(edge.from)) {
548
- const n = graph.nodes.get(edge.from);
549
- if (n) predsB.push(n.order);
550
- }
578
+ if (!edge) continue;
579
+ const n = graph.nodes.get(edge.from);
580
+ if (n && n.layer === upperIdx) _scratchB.push(n.order);
551
581
  }
552
582
  }
553
- for (const pA of predsA) {
554
- for (const pB of predsB) {
555
- 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++;
556
587
  }
557
588
  }
558
589
  }
559
590
  if (layerIndex < layers.length - 1) {
560
- const lowerLayer = new Set(layers[layerIndex + 1]);
561
- const succsA = [];
562
- const succsB = [];
591
+ const lowerIdx = layerIndex + 1;
592
+ _scratchA.length = 0;
593
+ _scratchB.length = 0;
563
594
  const outA = graph.outEdges.get(nodeA);
564
595
  if (outA) {
565
596
  for (const eid of outA) {
566
597
  const edge = graph.edges.get(eid);
567
- if (edge && lowerLayer.has(edge.to)) {
568
- const n = graph.nodes.get(edge.to);
569
- if (n) succsA.push(n.order);
570
- }
598
+ if (!edge) continue;
599
+ const n = graph.nodes.get(edge.to);
600
+ if (n && n.layer === lowerIdx) _scratchA.push(n.order);
571
601
  }
572
602
  }
573
603
  const outB = graph.outEdges.get(nodeB);
574
604
  if (outB) {
575
605
  for (const eid of outB) {
576
606
  const edge = graph.edges.get(eid);
577
- if (edge && lowerLayer.has(edge.to)) {
578
- const n = graph.nodes.get(edge.to);
579
- if (n) succsB.push(n.order);
580
- }
607
+ if (!edge) continue;
608
+ const n = graph.nodes.get(edge.to);
609
+ if (n && n.layer === lowerIdx) _scratchB.push(n.order);
581
610
  }
582
611
  }
583
- for (const sA of succsA) {
584
- for (const sB of succsB) {
585
- 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++;
586
616
  }
587
617
  }
588
618
  }
@@ -618,22 +648,36 @@ function countLayerCrossings(graph, upperLayer, lowerLayer) {
618
648
  return mergeSortCount(lowerPositions);
619
649
  }
620
650
  function mergeSortCount(arr) {
621
- if (arr.length <= 1) return 0;
622
- const mid = arr.length >> 1;
623
- const left = arr.slice(0, mid);
624
- const right = arr.slice(mid);
625
- let count = mergeSortCount(left) + mergeSortCount(right);
626
- let i = 0, j = 0, k = 0;
627
- while (i < left.length && j < right.length) {
628
- if (left[i] <= right[j]) {
629
- arr[k++] = left[i++];
630
- } else {
631
- count += left.length - i;
632
- 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++];
633
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];
634
680
  }
635
- while (i < left.length) arr[k++] = left[i++];
636
- while (j < right.length) arr[k++] = right[j++];
637
681
  return count;
638
682
  }
639
683
 
@@ -717,37 +761,69 @@ function optimizePositions(graph, layers, options, isHorizontal) {
717
761
  }
718
762
  centerAllLayers(graph, layers, isHorizontal);
719
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
+ }
720
770
  function optimizeLayer(graph, layer, options, isHorizontal) {
721
- if (layer.length === 0) return;
722
- const desired = /* @__PURE__ */ new Map();
723
- 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];
724
783
  const node = graph.nodes.get(nodeId);
725
- const connected = [...graph.predecessors(nodeId), ...graph.successors(nodeId)];
726
- if (connected.length === 0) {
727
- 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);
728
807
  continue;
729
808
  }
730
- const positions = connected.map((cId) => {
731
- const c = graph.nodes.get(cId);
732
- return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
733
- }).sort((a, b) => a - b);
809
+ sortPrefix(_neighborBuf, nbCount);
734
810
  const nodeSize = getOrderSize(node, isHorizontal);
735
- const median = positions.length % 2 === 0 ? (positions[positions.length / 2 - 1] + positions[positions.length / 2]) / 2 : positions[Math.floor(positions.length / 2)];
736
- 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;
737
813
  }
738
- for (let i = 0; i < layer.length; i++) {
814
+ for (let i = 0; i < n; i++) {
739
815
  const nodeId = layer[i];
740
816
  const node = graph.nodes.get(nodeId);
741
- let desiredPos = desired.get(nodeId);
817
+ let desiredPos = desired[i];
742
818
  if (i > 0) {
743
819
  const prevId = layer[i - 1];
744
820
  const prev = graph.nodes.get(prevId);
745
821
  const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
746
- desiredPos = Math.max(desiredPos, prevEnd + options.nodeSpacing);
822
+ if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
747
823
  }
748
824
  setOrderPos(node, isHorizontal, desiredPos);
749
825
  }
750
- for (let i = layer.length - 2; i >= 0; i--) {
826
+ for (let i = n - 2; i >= 0; i--) {
751
827
  const nodeId = layer[i];
752
828
  const node = graph.nodes.get(nodeId);
753
829
  const nextId = layer[i + 1];
@@ -759,6 +835,17 @@ function optimizeLayer(graph, layer, options, isHorizontal) {
759
835
  }
760
836
  }
761
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
+ }
762
849
  function centerAllLayers(graph, layers, isHorizontal) {
763
850
  if (layers.length === 0) return;
764
851
  let globalMin = Infinity;
@@ -1411,6 +1498,79 @@ function score(handles, expected) {
1411
1498
  }
1412
1499
  return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
1413
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
+ }
1414
1574
  function rotateHandles(handles, rot) {
1415
1575
  const rotateSide = (s) => {
1416
1576
  if (rot === 180) {
@@ -1429,7 +1589,12 @@ var COMPOUND_HEADER = 28;
1429
1589
  function layout(input, options) {
1430
1590
  const resolved = resolveOptions(options);
1431
1591
  if (input.nodes.length === 0) return { nodes: [], edges: [] };
1432
- const adjusted = applyRotationProposals(input, resolved);
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
+ }
1433
1598
  return layoutCompound(adjusted, resolved);
1434
1599
  }
1435
1600
  function computeLayout(graph, options) {
@@ -1438,7 +1603,12 @@ function computeLayout(graph, options) {
1438
1603
  let layers = assignLayers(graph);
1439
1604
  layers = insertDummyNodes(graph, layers);
1440
1605
  let rail = railLayers(layers, graph);
1441
- rail = minimizeCrossings(graph, rail, options.crossingMinimizationIterations);
1606
+ rail = minimizeCrossings(
1607
+ graph,
1608
+ rail,
1609
+ options.crossingMinimizationIterations,
1610
+ options.skipTranspose
1611
+ );
1442
1612
  assignCoordinates(graph, rail, options);
1443
1613
  placeValueSidecars(graph, layers, options);
1444
1614
  const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);