@statelyai/layout 0.4.0 → 0.4.2

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 (33) hide show
  1. package/dist/elkjs/index.cjs +2 -2
  2. package/dist/elkjs/index.mjs +2 -2
  3. package/dist/elkjs/main.cjs +1 -1
  4. package/dist/elkjs/main.mjs +1 -1
  5. package/dist/elkjs/worker.cjs +2 -2
  6. package/dist/elkjs/worker.mjs +2 -2
  7. package/dist/{elkjs-UciYx7lD.mjs → elkjs-CSi7oB5J.mjs} +181 -69
  8. package/dist/{elkjs-ggjsBuVP.cjs → elkjs-CnBvdyBu.cjs} +181 -69
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.mjs +2 -2
  11. package/dist/layered/index.cjs +1 -1
  12. package/dist/layered/index.mjs +1 -1
  13. package/dist/{layered-CBPg6_6_.cjs → layered-BJu8A21s.cjs} +474 -158
  14. package/dist/{layered-C7ERzZjR.mjs → layered-BsAdm_u8.mjs} +469 -153
  15. package/dist/{layout-engine-DAw06RhN.cjs → layout-engine-BFP1TZbF.cjs} +1 -1
  16. package/dist/{layout-engine-DC3IYkIy.mjs → layout-engine-DnlmGxHL.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/src/elkjs/index.ts +135 -33
  19. package/src/elkjs/layout-quality.ts +81 -33
  20. package/src/layered/bk-node-placement.ts +10 -7
  21. package/src/layered/crossing-counter.ts +27 -9
  22. package/src/layered/crossing-graph.ts +2 -2
  23. package/src/layered/greedy-switch-decider.ts +44 -17
  24. package/src/layered/hierarchy-sweepiness.ts +37 -41
  25. package/src/layered/index.ts +15 -21
  26. package/src/layered/loop-envelopes.ts +10 -4
  27. package/src/layered/node-lookup.ts +29 -0
  28. package/src/layered/opposing-tracks.ts +108 -8
  29. package/src/layered/orthogonal-hypersegments.ts +69 -7
  30. package/src/layered/port-distributor.ts +26 -14
  31. package/src/layered/port-edge-order.ts +2 -1
  32. package/src/layered/spacing.ts +4 -3
  33. package/src/layered/strategies.ts +288 -111
@@ -311,6 +311,12 @@ const restoreInitialModelOrder = (input, graph) => {
311
311
  //#endregion
312
312
  //#region src/layered/port-distributor.ts
313
313
  /** Stateful clockwise port distribution across successive layer sweeps. */
314
+ const sideRank = {
315
+ NORTH: 0,
316
+ EAST: 1,
317
+ SOUTH: 2,
318
+ WEST: 3
319
+ };
314
320
  var CanonicalPortDistributor = class {
315
321
  state;
316
322
  topologyGraph;
@@ -359,25 +365,33 @@ var CanonicalPortDistributor = class {
359
365
  calculatePortRanks(graph, layer, input, nodeRelative) {
360
366
  this.prepareTopology(graph);
361
367
  let consumed = 0;
368
+ const ends = input ? this.incoming : this.outgoing;
369
+ const ranked = (port) => ends.get(port.id).length > 0;
362
370
  for (const node of layer) {
363
- const ports = node.ports.filter((port) => (input ? this.incoming : this.outgoing).get(port.id).length > 0);
364
- const increment = nodeRelative ? 1 / (ports.length + 1) : 1;
371
+ let count = 0, northCount = 0;
372
+ for (const port of node.ports) if (ranked(port)) {
373
+ count++;
374
+ if (port.side === "NORTH") northCount++;
375
+ }
376
+ const increment = nodeRelative ? 1 / (count + 1) : 1;
365
377
  if (input) {
366
- let north = consumed + ports.filter((port) => port.side === "NORTH").length * increment;
367
- let rest = nodeRelative ? consumed + 1 - increment : consumed + ports.length;
368
- for (const port of ports) {
378
+ let north = consumed + northCount * increment;
379
+ let rest = nodeRelative ? consumed + 1 - increment : consumed + count;
380
+ for (const port of node.ports) {
381
+ if (!ranked(port)) continue;
369
382
  this.state.ranks[port.id] = port.side === "NORTH" ? north : rest;
370
383
  if (port.side === "NORTH") north -= increment;
371
384
  else rest -= increment;
372
385
  }
373
386
  } else {
374
387
  let position = consumed + increment;
375
- for (const port of ports) {
388
+ for (const port of node.ports) {
389
+ if (!ranked(port)) continue;
376
390
  this.state.ranks[port.id] = position;
377
391
  position += increment;
378
392
  }
379
393
  }
380
- consumed += nodeRelative ? 1 : ports.length;
394
+ consumed += nodeRelative ? 1 : count;
381
395
  }
382
396
  }
383
397
  distribute(graph, index, forward, options) {
@@ -386,7 +400,7 @@ var CanonicalPortDistributor = class {
386
400
  const degree = (port) => incoming.get(port).length + outgoing.get(port).length;
387
401
  const ranks = (layer, input) => this.calculatePortRanks(graph, layer, input, options.nodeRelative);
388
402
  const distributeNode = (node, side$1) => {
389
- if (options.fixedOrder.has(node.id)) return;
403
+ if (options.fixedOrder.has(node.id) || !node.ports.length) return;
390
404
  const layerIndex = node.ports.length ? layerByPort.get(node.ports[0].id) : index;
391
405
  const size$1 = graph.layers[layerIndex].length;
392
406
  for (const face of [
@@ -396,7 +410,8 @@ var CanonicalPortDistributor = class {
396
410
  ]) {
397
411
  let minimum = 0, maximum = 0;
398
412
  const inLayer = [];
399
- for (const port of node.ports.filter((port$1) => port$1.side === face)) {
413
+ for (const port of node.ports) {
414
+ if (port.side !== face) continue;
400
415
  let sum = 0;
401
416
  const cross = port.side === "NORTH" || port.side === "SOUTH";
402
417
  if (cross) {
@@ -414,7 +429,8 @@ var CanonicalPortDistributor = class {
414
429
  else if (output && !input) sum += position + 1;
415
430
  else if (input && output) sum += port.side === "NORTH" ? 0 : large / 2;
416
431
  } else {
417
- if ([...outgoing.get(port.id), ...incoming.get(port.id)].some((peer) => layerByPort.get(peer) === layerIndex)) {
432
+ const sameLayer = (peer) => layerByPort.get(peer) === layerIndex;
433
+ if (outgoing.get(port.id).some(sameLayer) || incoming.get(port.id).some(sameLayer)) {
418
434
  inLayer.push(port);
419
435
  continue;
420
436
  }
@@ -435,12 +451,7 @@ var CanonicalPortDistributor = class {
435
451
  this.state.barycenters[port.id] = port.side === "EAST" ? above ? minimum - value : maximum + (size$1 + 1 - value) : above ? maximum + value : minimum - (size$1 + 1 - value);
436
452
  }
437
453
  }
438
- const sideRank = {
439
- NORTH: 0,
440
- EAST: 1,
441
- SOUTH: 2,
442
- WEST: 3
443
- };
454
+ if (node.ports.length < 2) return;
444
455
  node.ports.sort((a, b) => {
445
456
  const side$2 = sideRank[a.side] - sideRank[b.side];
446
457
  if (side$2) return side$2;
@@ -506,10 +517,18 @@ function createOrthogonalHypersegments(ports, connections, listRank) {
506
517
  segmentByPort
507
518
  };
508
519
  }
509
- /** ELK's hyperedge crossing estimate; positions are distinct ordinal port ranks on each side. */
510
- function countOrthogonalHypersegmentCrossings(ports, connections) {
511
- const { segments } = createOrthogonalHypersegments(ports, connections);
512
- const spans = segments.map((s) => ({
520
+ /**
521
+ * {@link countOrthogonalHypersegmentCrossings}, optionally separating shared
522
+ * tracks. ELK routes a hyperedge on one shared track. When it joins two or
523
+ * more ports on each side, its edges would run opposite ways along that
524
+ * track, which routing never allows; `separateShared` counts such a
525
+ * hyperedge's edges one by one instead. `shared` reports whether one exists.
526
+ */
527
+ function scoreOrthogonalHypersegments(ports, connections, separateShared = false) {
528
+ const { segments: merged, segmentByPort } = createOrthogonalHypersegments(ports, connections);
529
+ const sharedSegments = new Set(merged.flatMap((s, i) => s.incoming.length > 1 && s.outgoing.length > 1 ? [i] : []));
530
+ const separating = separateShared && sharedSegments.size > 0;
531
+ const spans = merged.filter((_, i) => !separating || !sharedSegments.has(i)).map((s) => ({
513
532
  leftStart: Math.min(...s.incoming),
514
533
  leftEnd: Math.max(...s.incoming),
515
534
  rightStart: Math.min(...s.outgoing),
@@ -522,6 +541,43 @@ function countOrthogonalHypersegmentCrossings(ports, connections) {
522
541
  if (a.leftStart < b.leftEnd && b.leftStart < a.leftEnd) count++;
523
542
  if (a.rightStart < b.rightEnd && b.rightStart < a.rightEnd) count++;
524
543
  }
544
+ if (separating) {
545
+ const position = new Map(ports.map((p) => [p.id, p.position]));
546
+ const edges = connections.filter((c$1) => sharedSegments.has(segmentByPort.get(c$1.source))).map((c$1) => [position.get(c$1.source), position.get(c$1.target)]);
547
+ for (const [left, right] of edges) for (const b of spans) {
548
+ if ((left - b.leftStart) * (right - b.rightStart) < 0) count++;
549
+ if (left < b.leftEnd && b.leftStart < left) count++;
550
+ if (right < b.rightEnd && b.rightStart < right) count++;
551
+ }
552
+ count += strictInversions(edges);
553
+ }
554
+ return {
555
+ count,
556
+ shared: sharedSegments.size > 0
557
+ };
558
+ }
559
+ /** Pairs whose ends are strictly inverted: two separated edges that cross, in O(n log n). */
560
+ function strictInversions(pairs) {
561
+ const ranks = [...new Set(pairs.map(([, right]) => right))].sort((a, b) => a - b);
562
+ const rankOf = new Map(ranks.map((value, index) => [value, index + 1]));
563
+ const tree = new Array(ranks.length + 1).fill(0);
564
+ const atMost = (rank) => {
565
+ let sum = 0;
566
+ for (let i = rank; i > 0; i -= i & -i) sum += tree[i];
567
+ return sum;
568
+ };
569
+ const sorted = [...pairs].sort((a, b) => a[0] - b[0]);
570
+ let inserted = 0, count = 0;
571
+ for (let start = 0; start < sorted.length;) {
572
+ let end = start;
573
+ while (end < sorted.length && sorted[end][0] === sorted[start][0]) end++;
574
+ for (let i = start; i < end; i++) count += inserted - atMost(rankOf.get(sorted[i][1]));
575
+ for (let i = start; i < end; i++) {
576
+ for (let r = rankOf.get(sorted[i][1]); r <= ranks.length; r += r & -r) tree[r]++;
577
+ inserted++;
578
+ }
579
+ start = end;
580
+ }
525
581
  return count;
526
582
  }
527
583
 
@@ -555,7 +611,7 @@ var IndexTree = class {
555
611
  };
556
612
  /** Score a canonical candidate without changing its node or port orders. */
557
613
  function countAllCrossings(graph, boundaryMode = "hyperedges") {
558
- const nodes = new Map(graph.layers.flat().map((node) => [node.id, node]));
614
+ let nodes;
559
615
  const layerByPort = /* @__PURE__ */ new Map();
560
616
  const incoming = /* @__PURE__ */ new Map(), outgoing = /* @__PURE__ */ new Map();
561
617
  for (const [layer, members] of graph.layers.entries()) for (const node of members) for (const port of node.ports) {
@@ -567,7 +623,6 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
567
623
  outgoing.get(edge.source).push(edge.target);
568
624
  incoming.get(edge.target).push(edge.source);
569
625
  }
570
- const connected = (id) => [...incoming.get(id), ...outgoing.get(id)];
571
626
  const degree = (id) => incoming.get(id).length + outgoing.get(id).length;
572
627
  const face = (layer, side, topDown = true) => (topDown ? layer : [...layer].reverse()).flatMap((node) => {
573
628
  const ports = node.ports.filter((port) => port.side === side);
@@ -580,7 +635,9 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
580
635
  for (const [position, port] of ports.entries()) {
581
636
  tree.remove(position);
582
637
  const pending = [];
583
- for (const peer of connected(port.id)) {
638
+ const before = incoming.get(port.id), after = outgoing.get(port.id);
639
+ for (let index = 0; index < before.length + after.length; index++) {
640
+ const peer = index < before.length ? before[index] : after[index - before.length];
584
641
  if (inLayerOnly && layerByPort.get(peer) !== layerByPort.get(port.id)) {
585
642
  crossings$1 += tree.size;
586
643
  continue;
@@ -624,7 +681,7 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
624
681
  for (const [position, port] of ports.entries()) {
625
682
  tree.remove(position);
626
683
  const node = ownerByPort.get(port.id);
627
- const targets = node.type === "NORMAL" ? nodes.get(port.dummy).ports.map((peer) => ({
684
+ const targets = node.type === "NORMAL" ? (nodes ??= new Map(graph.layers.flat().map((node$1) => [node$1.id, node$1]))).get(port.dummy).ports.map((peer) => ({
628
685
  id: peer.id,
629
686
  degree: degree(peer.id)
630
687
  })) : node.type === "LONG_EDGE" ? node.ports.filter((peer) => peer.id !== port.id).slice(0, 1).map((peer) => ({
@@ -650,21 +707,22 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
650
707
  betweenLayers: 0,
651
708
  inLayer: 0,
652
709
  northSouth: 0,
653
- total: 0
710
+ total: 0,
711
+ sharedTracks: false
654
712
  };
655
713
  if (!graph.layers.length) return score;
656
714
  score.inLayer += countPorts(face(graph.layers[0], "WEST"), true);
657
715
  score.inLayer += countPorts(face(graph.layers.at(-1), "EAST"), true);
658
716
  for (const [index, layer] of graph.layers.entries()) {
659
717
  const next = graph.layers[index + 1];
660
- if (next) if ((layer.some((node) => node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1)) || next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1))) && boundaryMode === "hyperedges") {
718
+ if (next) if ((layer.some((node) => node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1)) || next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1))) && boundaryMode !== "edges") {
661
719
  const sourcePorts = layer.flatMap((node) => node.ports).filter((port) => outgoing.get(port.id).some((peer) => layerByPort.get(peer) !== index));
662
720
  const targetPorts = next.flatMap((node) => {
663
721
  const connectedPorts = node.ports.filter((port) => incoming.get(port.id).some((peer) => layerByPort.get(peer) !== index + 1));
664
722
  return [...connectedPorts.filter((port) => port.side === "NORTH").reverse(), ...connectedPorts.filter((port) => port.side !== "NORTH").reverse()];
665
723
  });
666
724
  const sources = new Set(sourcePorts.map((port) => port.id)), targets = new Set(targetPorts.map((port) => port.id));
667
- score.betweenLayers += countOrthogonalHypersegmentCrossings([...sourcePorts.map((port, position) => ({
725
+ const hyperedges = scoreOrthogonalHypersegments([...sourcePorts.map((port, position) => ({
668
726
  id: port.id,
669
727
  side: "source",
670
728
  position
@@ -672,7 +730,9 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
672
730
  id: port.id,
673
731
  side: "target",
674
732
  position
675
- }))], graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)));
733
+ }))], graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)), boundaryMode === "separated");
734
+ score.betweenLayers += hyperedges.count;
735
+ score.sharedTracks ||= hyperedges.shared;
676
736
  score.inLayer += countPorts(face(layer, "EAST"), true) + countPorts(face(next, "WEST"), true);
677
737
  } else score.betweenLayers += countPorts([...face(layer, "EAST"), ...face(next, "WEST", false)], false);
678
738
  if (layer.some((node) => node.type === "NORTH_SOUTH_PORT")) score.northSouth += countNorthSouth(layer);
@@ -1054,7 +1114,7 @@ function buildCrossingGraph(plan, layers, incoming, outgoing) {
1054
1114
  for (const member of ports.get(operation.northSouthDummy)?.values() ?? []) member.port.origin = operation.id;
1055
1115
  }
1056
1116
  }
1057
- const sideRank = {
1117
+ const sideRank$1 = {
1058
1118
  NORTH: 0,
1059
1119
  EAST: 1,
1060
1120
  SOUTH: 2,
@@ -1067,7 +1127,7 @@ function buildCrossingGraph(plan, layers, incoming, outgoing) {
1067
1127
  id,
1068
1128
  type: kind?.type ?? kindOf(id, plan.units),
1069
1129
  unit: kind ? kind.unit : plan.units?.units.get(id),
1070
- ports: [...ports.get(id)?.values() ?? []].sort((a, b) => sideRank[a.port.side] - sideRank[b.port.side] || a.rank - b.rank).map((member) => member.port)
1130
+ ports: [...ports.get(id)?.values() ?? []].sort((a, b) => sideRank$1[a.port.side] - sideRank$1[b.port.side] || a.rank - b.rank).map((member) => member.port)
1071
1131
  };
1072
1132
  })),
1073
1133
  edges: plan.edges.map((edge) => ({
@@ -1087,7 +1147,8 @@ function splitMixedCrossingPorts(graph, reversed) {
1087
1147
  const directions = /* @__PURE__ */ new Map();
1088
1148
  graph.edges.forEach((edge, index) => {
1089
1149
  const bit = reversed[index] ? 2 : 1;
1090
- for (const port of [edge.source, edge.target]) directions.set(port, (directions.get(port) ?? 0) | bit);
1150
+ directions.set(edge.source, (directions.get(edge.source) ?? 0) | bit);
1151
+ directions.set(edge.target, (directions.get(edge.target) ?? 0) | bit);
1091
1152
  });
1092
1153
  const mixed = /* @__PURE__ */ new Set();
1093
1154
  for (const layer of graph.layers) for (const node of layer) for (const port of node.ports) if (directions.get(port.id) === 3 && (port.side === "EAST" || port.side === "WEST")) mixed.add(port.id);
@@ -1527,20 +1588,46 @@ function selfLoopTracks(loops) {
1527
1588
  }));
1528
1589
  }
1529
1590
 
1591
+ //#endregion
1592
+ //#region src/layered/node-lookup.ts
1593
+ const indexByNodes = /* @__PURE__ */ new WeakMap();
1594
+ function indexNodes(nodes) {
1595
+ const positions = /* @__PURE__ */ new Map();
1596
+ for (const [index, node] of nodes.entries()) if (!positions.has(node.id)) positions.set(node.id, index);
1597
+ indexByNodes.set(nodes, {
1598
+ length: nodes.length,
1599
+ positions
1600
+ });
1601
+ return positions;
1602
+ }
1603
+ /** Same as `nodes.find((node) => node.id === id)`, indexed per list. */
1604
+ function findNode(nodes, id) {
1605
+ const cached = indexByNodes.get(nodes);
1606
+ let position = (cached && cached.length === nodes.length ? cached.positions : indexNodes(nodes)).get(id);
1607
+ if (position !== void 0 && nodes[position]?.id !== id) position = indexNodes(nodes).get(id);
1608
+ return position !== void 0 && nodes[position]?.id === id ? nodes[position] : nodes.find((node) => node.id === id);
1609
+ }
1610
+
1530
1611
  //#endregion
1531
1612
  //#region src/layered/loop-envelopes.ts
1532
1613
  /** Unconstrained implicit loop ports are restored on loop faces after the sweep. */
1533
1614
  function hasMovableLoopPorts(input, edge) {
1534
1615
  if (edge.sourceId !== edge.targetId || edge.sourcePort !== void 0 || edge.targetPort !== void 0) return false;
1535
- const node = input.graph.nodes.find((candidate) => candidate.id === edge.sourceId);
1616
+ const node = findNode(input.graph.nodes, edge.sourceId);
1536
1617
  const constraints = node && input.nodeSettings?.(node)?.portConstraints;
1537
1618
  return constraints === void 0 || constraints === "FREE" || constraints === "UNDEFINED";
1538
1619
  }
1539
1620
  function loopEnvelopes(input) {
1540
1621
  const envelopes = /* @__PURE__ */ new Map();
1541
1622
  if ((input.settings.edgeRouting ?? "ORTHOGONAL") !== "ORTHOGONAL") return envelopes;
1623
+ const loopsByNodeId = /* @__PURE__ */ new Map();
1624
+ for (const edge of input.graph.edges) if (edge.sourceId === edge.targetId) {
1625
+ const loops = loopsByNodeId.get(edge.sourceId);
1626
+ if (loops) loops.push(edge);
1627
+ else loopsByNodeId.set(edge.sourceId, [edge]);
1628
+ }
1542
1629
  for (const node of input.graph.nodes) {
1543
- const allLoops = input.graph.edges.filter((edge) => edge.sourceId === node.id && edge.targetId === node.id);
1630
+ const allLoops = loopsByNodeId.get(node.id)?.slice() ?? [];
1544
1631
  const constraints = input.nodeSettings?.(node)?.portConstraints;
1545
1632
  if (allLoops.length && constraints && constraints !== "FREE" && constraints !== "UNDEFINED" && allLoops.every((edge) => edge.sourcePort !== void 0 && edge.targetPort !== void 0 && input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true && ((edge.width ?? 0) === 0 && (edge.height ?? 0) === 0 || (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER"))) {
1546
1633
  const size$1 = input.sizes.get(node.id) ?? {
@@ -1653,7 +1740,7 @@ function preparedLoopEnvelopes(input) {
1653
1740
  //#region src/layered/port-edge-order.ts
1654
1741
  /** ELK traverses connected edges through the node's port list, preserving each port's edge order. */
1655
1742
  function connectedEdgesInPortOrder(input, nodeId) {
1656
- const node = input.graph.nodes.find((candidate) => candidate.id === nodeId);
1743
+ const node = findNode(input.graph.nodes, nodeId);
1657
1744
  const portRank = new Map(node?.ports?.map((port, index) => [port.name, index]));
1658
1745
  const explicitCount = portRank.size;
1659
1746
  return input.graph.edges.map((edge, index) => ({
@@ -2542,8 +2629,8 @@ function nodeNodeSpacing(input, firstId, secondId) {
2542
2629
  const secondBreakingPoint = secondId.startsWith("__layout_breaking:");
2543
2630
  const firstLabel = firstId.startsWith("__layout_dummy:label:");
2544
2631
  const secondLabel = secondId.startsWith("__layout_dummy:label:");
2545
- const firstNode = input.graph.nodes.find((node) => node.id === firstId);
2546
- const secondNode = input.graph.nodes.find((node) => node.id === secondId);
2632
+ const firstNode = findNode(input.graph.nodes, firstId);
2633
+ const secondNode = findNode(input.graph.nodes, secondId);
2547
2634
  const firstExternal = firstNode !== void 0 && externalPortDummyOf(firstNode) !== void 0;
2548
2635
  const secondExternal = secondNode !== void 0 && externalPortDummyOf(secondNode) !== void 0;
2549
2636
  const firstDummy = firstId.startsWith("__layout_dummy:") || firstBreakingPoint || firstExternal;
@@ -2551,7 +2638,7 @@ function nodeNodeSpacing(input, firstId, secondId) {
2551
2638
  const spacingName = firstExternal && secondExternal ? "spacing.portPort" : firstExternal && secondLabel || secondExternal && firstLabel ? "spacing.labelPortVertical" : firstLabel && secondLabel ? "spacing.edgeEdge" : firstLabel && !secondDummy || secondLabel && !firstDummy ? "spacing.node" : firstLabel && secondDummy || secondLabel && firstDummy ? "spacing.edgeNode" : firstDummy && secondDummy && firstBreakingPoint === secondBreakingPoint ? "spacing.edgeEdge" : firstDummy || secondDummy ? "spacing.edgeNode" : "spacing.node";
2552
2639
  let spacing = spacingName === "spacing.portPort" ? Number(input.settings["spacing.portPort"] ?? 10) : spacingName === "spacing.labelPortVertical" ? Number(input.settings["spacing.labelPortVertical"] ?? 1) : spacingName === "spacing.edgeEdge" ? Number(input.settings["spacing.edgeEdge"] ?? 10) : spacingName === "spacing.edgeNode" ? Number(input.settings["spacing.edgeNode"] ?? 10) : input.spacing.node;
2553
2640
  for (const id of [firstId, secondId]) {
2554
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
2641
+ const node = findNode(input.graph.nodes, id);
2555
2642
  if (!node) continue;
2556
2643
  const individual = input.nodeSettings?.(node)?.["spacing.individual"];
2557
2644
  if (individual && typeof individual === "object") {
@@ -3362,16 +3449,35 @@ function sidePorts(node, side) {
3362
3449
  const ports = node.ports.filter((port) => port.side === side);
3363
3450
  return side === "WEST" || side === "SOUTH" ? ports.reverse() : ports;
3364
3451
  }
3452
+ function greedySwitchTopology(graph) {
3453
+ const topology = {
3454
+ incoming: /* @__PURE__ */ new Map(),
3455
+ outgoing: /* @__PURE__ */ new Map(),
3456
+ layerByPort: /* @__PURE__ */ new Map(),
3457
+ ownerByPort: /* @__PURE__ */ new Map()
3458
+ };
3459
+ for (const [layer, nodes] of graph.layers.entries()) for (const node of nodes) for (const port of node.ports) {
3460
+ topology.layerByPort.set(port.id, layer);
3461
+ topology.ownerByPort.set(port.id, node);
3462
+ topology.incoming.set(port.id, []);
3463
+ topology.outgoing.set(port.id, []);
3464
+ }
3465
+ for (const edge of graph.edges) {
3466
+ topology.outgoing.get(edge.source)?.push(edge.target);
3467
+ topology.incoming.get(edge.target)?.push(edge.source);
3468
+ }
3469
+ return topology;
3470
+ }
3365
3471
  /**
3366
3472
  * Decides greedy switches in one free layer from local two-node crossing
3367
3473
  * counts, exactly as ELK does; these estimates can differ from a global recount.
3368
3474
  */
3369
3475
  var GreedySwitchDecider = class {
3370
3476
  #free;
3371
- #incoming = /* @__PURE__ */ new Map();
3372
- #outgoing = /* @__PURE__ */ new Map();
3373
- #layerByPort = /* @__PURE__ */ new Map();
3374
- #ownerByPort = /* @__PURE__ */ new Map();
3477
+ #incoming;
3478
+ #outgoing;
3479
+ #layerByPort;
3480
+ #ownerByPort;
3375
3481
  #neighbourPositions = /* @__PURE__ */ new Map();
3376
3482
  #adjacencies = /* @__PURE__ */ new Map();
3377
3483
  #matrix = /* @__PURE__ */ new Map();
@@ -3379,19 +3485,13 @@ var GreedySwitchDecider = class {
3379
3485
  #cardinality = /* @__PURE__ */ new Map();
3380
3486
  #inLayerSize = /* @__PURE__ */ new Map();
3381
3487
  #oneSided;
3382
- constructor(graph, freeLayerIndex, oneSided) {
3488
+ constructor(graph, freeLayerIndex, oneSided, topology = greedySwitchTopology(graph)) {
3383
3489
  this.#free = graph.layers[freeLayerIndex] ?? [];
3384
3490
  this.#oneSided = oneSided;
3385
- for (const [layer, nodes] of graph.layers.entries()) for (const node of nodes) for (const port of node.ports) {
3386
- this.#layerByPort.set(port.id, layer);
3387
- this.#ownerByPort.set(port.id, node);
3388
- this.#incoming.set(port.id, []);
3389
- this.#outgoing.set(port.id, []);
3390
- }
3391
- for (const edge of graph.edges) {
3392
- this.#outgoing.get(edge.source)?.push(edge.target);
3393
- this.#incoming.get(edge.target)?.push(edge.source);
3394
- }
3491
+ this.#incoming = topology.incoming;
3492
+ this.#outgoing = topology.outgoing;
3493
+ this.#layerByPort = topology.layerByPort;
3494
+ this.#ownerByPort = topology.ownerByPort;
3395
3495
  for (const [layerIndex, side] of [[freeLayerIndex - 1, "EAST"], [freeLayerIndex + 1, "WEST"]]) {
3396
3496
  let position = 0;
3397
3497
  for (const node of graph.layers[layerIndex] ?? []) for (const port of sidePorts(node, side)) this.#neighbourPositions.set(port.id, position++);
@@ -3641,6 +3741,18 @@ function cycleModelOrder(input) {
3641
3741
  const count = Math.max(1, input.graph.nodes.length);
3642
3742
  return new Map(input.graph.nodes.map((node, index) => [node.id, enforced ? Number(input.nodeSettings?.(node)?.["considerModelOrder.groupModelOrder.cycleBreakingId"] ?? 0) * count + index : index]));
3643
3743
  }
3744
+ /**
3745
+ * Internal setting: how crossing minimization models ports and tracks.
3746
+ * `mixedPorts` (default true) counts a port carrying both directions as two
3747
+ * ports and groups its neighbours by direction. `separateSharedTracks`
3748
+ * (default false) counts a hyperedge joining two or more ports on each side
3749
+ * of its gap edge by edge, since routing never lets edges run opposite ways
3750
+ * along one track. A `probe` records whether either choice can change the
3751
+ * order. Each helps on average but steers the sweeps into worse orders on
3752
+ * some graphs, so the elkjs facade also lays such graphs out with the
3753
+ * alternatives and keeps the best result.
3754
+ */
3755
+ const CROSSING_MODEL = "internal.crossingModel";
3644
3756
  const breakCyclesWithDepthFirstSearch = (input) => {
3645
3757
  const outgoing = /* @__PURE__ */ new Map();
3646
3758
  for (const node of input.graph.nodes) outgoing.set(node.id, []);
@@ -4128,8 +4240,7 @@ function applyLayerConstraintOrder(input, order) {
4128
4240
  return {
4129
4241
  ...order,
4130
4242
  layers: order.layers.map((layer) => {
4131
- const unconstrained = layer.filter((id) => !constrained.has(id));
4132
- return [...input.settings.mergeEdges === true ? [...unconstrained.filter((id) => id.startsWith("__layout_dummy:")), ...unconstrained.filter((id) => !id.startsWith("__layout_dummy:"))] : unconstrained, ...layer.filter((id) => constrained.has(id))];
4243
+ return [...layer.filter((id) => !constrained.has(id)), ...layer.filter((id) => constrained.has(id))];
4133
4244
  })
4134
4245
  };
4135
4246
  }
@@ -4252,6 +4363,7 @@ function applyGreedySwitch(input, orientation, order) {
4252
4363
  if (oneSidedBoundary !== void 0) return countBoundary(oneSidedBoundary);
4253
4364
  return (layerIndex > 0 ? countBoundary(layerIndex - 1) : 0) + (layerIndex + 1 < layers.length ? countBoundary(layerIndex) : 0);
4254
4365
  };
4366
+ let greedyTopology;
4255
4367
  const sweep = (forward$1) => {
4256
4368
  let changed = false;
4257
4369
  const indices = forward$1 ? Array.from({ length: layers.length }, (_, index) => index) : Array.from({ length: layers.length }, (_, index) => layers.length - index - 1);
@@ -4261,7 +4373,7 @@ function applyGreedySwitch(input, orientation, order) {
4261
4373
  if (greedyGraph) {
4262
4374
  const units = getCrossingUnits(input);
4263
4375
  greedyGraph.layers = layers.map((ids) => ids.map((id) => greedyNodes.get(id)));
4264
- const decider = new GreedySwitchDecider(greedyGraph, layerIndex, type === "ONE_SIDED" ? forward$1 ? "WEST" : "EAST" : void 0);
4376
+ const decider = new GreedySwitchDecider(greedyGraph, layerIndex, type === "ONE_SIDED" ? forward$1 ? "WEST" : "EAST" : void 0, greedyTopology ??= greedySwitchTopology(greedyGraph));
4265
4377
  do {
4266
4378
  improved$1 = false;
4267
4379
  for (let index = 0; index + 1 < layer.length; index++) {
@@ -4854,7 +4966,9 @@ function sortLayerByAdjacentPosition(layer, ranksByNodeId, statistic, random, pr
4854
4966
  if (units && !forcedCompare) layer.splice(0, layer.length, ...resolveCrossingConstraints(layer, adjacentPosition, units));
4855
4967
  }
4856
4968
  function clonePortOrders(orders) {
4857
- return new Map([...orders ?? []].map(([id, edges]) => [id, [...edges]]));
4969
+ const clone = /* @__PURE__ */ new Map();
4970
+ if (orders) for (const [id, edges] of orders) clone.set(id, edges.slice());
4971
+ return clone;
4858
4972
  }
4859
4973
  function minimizeCrossingsWithLayerSweep(statistic, sweeps = 7) {
4860
4974
  return (input, orientation, assignment) => createLayerSweepSession(input, orientation, assignment, statistic, sweeps).minimize();
@@ -4896,6 +5010,18 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4896
5010
  }
4897
5011
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4898
5012
  const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
5013
+ const boundaryPortIndex = (nodeId, edgeId, incoming) => {
5014
+ const edge = edgeById.get(edgeId);
5015
+ const name = incoming !== orientation.reversedEdgeIds.has(edgeId) ? edge.targetPort : edge.sourcePort;
5016
+ if (!name?.startsWith("__native_hierarchy")) return -1;
5017
+ return nodeById.get(nodeId).ports?.findIndex((port) => port.name === name) ?? -1;
5018
+ };
5019
+ for (const [orders, incoming] of [[outputPortOrder, false], [inputPortOrder, true]]) for (const [nodeId, edgeIds] of orders) {
5020
+ if (edgeIds.length < 2) continue;
5021
+ const slots = edgeIds.flatMap((id, slot) => boundaryPortIndex(nodeId, id, incoming) < 0 ? [] : [slot]);
5022
+ const sorted = slots.map((slot) => edgeIds[slot]).sort((a, b) => boundaryPortIndex(nodeId, a, incoming) - boundaryPortIndex(nodeId, b, incoming));
5023
+ slots.forEach((slot, index) => edgeIds[slot] = sorted[index]);
5024
+ }
4899
5025
  const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
4900
5026
  const hierarchicalNodes = /* @__PURE__ */ new Set();
4901
5027
  const fixedOrderNodes = new Set(input.graph.nodes.filter((node) => {
@@ -4933,10 +5059,26 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4933
5059
  if (edgeIds.length >= 4) edgeIds.splice(0, edgeIds.length - 1, ...edgeIds.slice(0, -1).reverse());
4934
5060
  }
4935
5061
  const buildCrossingGraph$1 = createCrossingGraphBuilder(input);
5062
+ const crossingModel = input.settings[CROSSING_MODEL] ?? {};
5063
+ const mixedPorts = crossingModel.mixedPorts !== false, probe = crossingModel.probe;
5064
+ const physicalSlot = /* @__PURE__ */ new Map();
4936
5065
  const countCrossings = (candidateLayers) => {
4937
5066
  if (exactPortSweep) {
4938
5067
  const graph = buildCrossingGraph$1(orientation, candidateLayers, inputPortOrder, outputPortOrder);
4939
- let total = countAllCrossings(splitMixedCrossingPorts(graph, canonicalReversed)).total;
5068
+ for (const layer of graph.layers) for (const node of layer) {
5069
+ const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : void 0;
5070
+ if (physical?.length !== node.ports.length) continue;
5071
+ physicalSlot.clear();
5072
+ for (const [index, port] of physical.entries()) physicalSlot.set(port.id, index);
5073
+ if (node.ports.every((port) => physicalSlot.has(port.id))) node.ports.sort((a, b) => physicalSlot.get(a.id) - physicalSlot.get(b.id));
5074
+ }
5075
+ const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
5076
+ const score = countAllCrossings(split, crossingModel.separateSharedTracks ? "separated" : "hyperedges");
5077
+ if (probe) {
5078
+ if (split !== graph) probe.mixedPorts = true;
5079
+ if (score.sharedTracks) probe.sharedTracks = true;
5080
+ }
5081
+ let total = score.total;
4940
5082
  const nodeInfluence = Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0);
4941
5083
  if ((input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE" && nodeInfluence !== 0) {
4942
5084
  const changes = countModelOrderChanges(input, orientation, graph);
@@ -4986,11 +5128,12 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4986
5128
  const nodes = fixedLayer.map((id) => canonicalNodes.get(id));
4987
5129
  canonicalGraph.layers[fixedIndex] = nodes;
4988
5130
  canonicalDistributor.calculatePortRanks(canonicalGraph, nodes, !forward, nodeRelativePortRanks);
4989
- const fixedIds = new Set(fixedLayer);
4990
- for (let index = 0; index < canonicalEdges.length; index++) {
5131
+ const byEnd = forward ? canonicalEdgesBySource : canonicalEdgesByTarget;
5132
+ const indices = fixedLayer.flatMap((id) => byEnd.get(id) ?? []).sort((a, b) => a - b);
5133
+ for (const index of indices) {
4991
5134
  const edge = canonicalEdges[index];
4992
5135
  const [source, target] = canonicalOrientedEndpoints[index];
4993
- if (!fixedIds.has(forward ? source : target) || !freeIds.has(forward ? target : source)) continue;
5136
+ if (!freeIds.has(forward ? target : source)) continue;
4994
5137
  const endpoint = canonicalEndpoints.get(edge.id);
4995
5138
  const rank = canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target];
4996
5139
  ranks.get(forward ? target : source).push(rank);
@@ -5029,6 +5172,17 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5029
5172
  }
5030
5173
  for (const id of freeLayer) {
5031
5174
  const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
5175
+ const visit = (edgeId) => {
5176
+ const [source, target] = getOrientedEndpoints(edgeById.get(edgeId), orientation);
5177
+ const neighbor = forward ? source : target;
5178
+ const rank$1 = edgeRanks.get(edgeId);
5179
+ if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id)) visits.get(id).push(neighbor);
5180
+ else if (rank$1 !== void 0) visits.get(id).push(rank$1);
5181
+ };
5182
+ if (order.length < 2) {
5183
+ for (const edgeId of order) visit(edgeId);
5184
+ continue;
5185
+ }
5032
5186
  const groups = /* @__PURE__ */ new Map();
5033
5187
  for (const edgeId of order) {
5034
5188
  const edge = edgeById.get(edgeId);
@@ -5037,17 +5191,11 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5037
5191
  if (group) group.push(edgeId);
5038
5192
  else groups.set(key, [edgeId]);
5039
5193
  }
5194
+ const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
5195
+ const rank = (edgeId) => listed?.get(listedEdgeId(edgeId));
5040
5196
  for (const group of groups.values()) {
5041
- const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
5042
- const rank = (edgeId) => listed?.get(listedEdgeId(edgeId));
5043
- group.sort((a, b) => (rank(a) !== void 0 && rank(b) !== void 0 ? rank(a) - rank(b) : 0) || edgeModelOrder.get(a) - edgeModelOrder.get(b));
5044
- for (const edgeId of group) {
5045
- const [source, target] = getOrientedEndpoints(edgeById.get(edgeId), orientation);
5046
- const neighbor = forward ? source : target;
5047
- const rank$1 = edgeRanks.get(edgeId);
5048
- if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id)) visits.get(id).push(neighbor);
5049
- else if (rank$1 !== void 0) visits.get(id).push(rank$1);
5050
- }
5197
+ if (group.length > 1) group.sort((a, b) => (rank(a) !== void 0 && rank(b) !== void 0 ? rank(a) - rank(b) : 0) || edgeModelOrder.get(a) - edgeModelOrder.get(b));
5198
+ for (const edgeId of group) visit(edgeId);
5051
5199
  }
5052
5200
  }
5053
5201
  return {
@@ -5072,6 +5220,14 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5072
5220
  const canonicalEdges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
5073
5221
  const canonicalReversed = canonicalEdges.map((edge) => orientation.reversedEdgeIds.has(edge.id));
5074
5222
  const canonicalOrientedEndpoints = canonicalEdges.map((edge) => getOrientedEndpoints(edge, orientation));
5223
+ const canonicalEdgesBySource = /* @__PURE__ */ new Map(), canonicalEdgesByTarget = /* @__PURE__ */ new Map();
5224
+ canonicalOrientedEndpoints.forEach(([source, target], index) => {
5225
+ for (const [byEnd, id] of [[canonicalEdgesBySource, source], [canonicalEdgesByTarget, target]]) {
5226
+ const list = byEnd.get(id);
5227
+ if (list) list.push(index);
5228
+ else byEnd.set(id, [index]);
5229
+ }
5230
+ });
5075
5231
  const canonicalEndpoints = new Map(canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]]));
5076
5232
  const canonicalFixedOrder = new Set(input.graph.nodes.filter((node) => {
5077
5233
  const constraint = input.nodeSettings?.(node)?.portConstraints;
@@ -5087,6 +5243,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5087
5243
  hierarchical: hierarchicalNodes
5088
5244
  });
5089
5245
  for (const index of touched) for (const node of canonicalGraph.layers[index]) {
5246
+ if (node.ports.length < 2 || (outputPortOrder.get(node.id)?.length ?? 0) < 2 && (inputPortOrder.get(node.id)?.length ?? 0) < 2) continue;
5090
5247
  const ordinal = new Map(node.ports.map((port, index$1) => [port.id, index$1]));
5091
5248
  for (const [orders, source] of [[outputPortOrder, true], [inputPortOrder, false]]) orders.get(node.id)?.sort((left, right) => ordinal.get(canonicalEndpoints.get(left)[source ? "source" : "target"]) - ordinal.get(canonicalEndpoints.get(right)[source ? "source" : "target"]));
5092
5249
  }
@@ -5121,6 +5278,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5121
5278
  */
5122
5279
  const constrained = new Set([...units?.units.keys() ?? [], ...[...units?.successors ?? []].flatMap(([id, after]) => [id, ...after])]);
5123
5280
  const groupMixedPortDirections = (free, fixed, beyond = []) => {
5281
+ if (!mixedPorts) return;
5124
5282
  const fixedIds = new Set(fixed);
5125
5283
  const groups = /* @__PURE__ */ new Map();
5126
5284
  for (const id of free) {
@@ -5143,6 +5301,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5143
5301
  groups.set(key, group);
5144
5302
  }
5145
5303
  if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed))) return;
5304
+ if (probe) probe.mixedPorts = true;
5146
5305
  const index = new Map(free.map((id, i) => [id, i]));
5147
5306
  const far = new Map(beyond.map((id, i) => [id, i]));
5148
5307
  const reach = (members) => {
@@ -5302,15 +5461,23 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5302
5461
  shuffleFirstLayer,
5303
5462
  sweep,
5304
5463
  countCrossings: () => countCrossings(working),
5305
- snapshot: () => ({
5306
- physicalPortOrderByNodeId: new Map([...canonicalNodes].map(([id, node]) => [id, node.ports.map((port) => port.id)])),
5307
- layers: working.map((layer) => [...layer]),
5308
- inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
5309
- outputPortOrderByNodeId: clonePortOrders(outputPortOrder)
5310
- }),
5464
+ snapshot: () => {
5465
+ const physicalPortOrderByNodeId = /* @__PURE__ */ new Map();
5466
+ for (const [id, node] of canonicalNodes) physicalPortOrderByNodeId.set(id, node.ports.map((port) => port.id));
5467
+ return {
5468
+ physicalPortOrderByNodeId,
5469
+ layers: working.map((layer) => [...layer]),
5470
+ inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
5471
+ outputPortOrderByNodeId: clonePortOrders(outputPortOrder)
5472
+ };
5473
+ },
5311
5474
  restore,
5312
5475
  finish,
5313
5476
  currentLayers: () => working,
5477
+ crossingGraph: () => ({
5478
+ layers: working.map((layer) => layer.map((id) => canonicalNodes.get(id))),
5479
+ edges: canonicalGraph.edges
5480
+ }),
5314
5481
  physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
5315
5482
  edgeOrders: (id) => ({
5316
5483
  incoming: inputPortOrder.get(id) ?? [],
@@ -5713,7 +5880,7 @@ function sumWithSpacing(ids, input, axis) {
5713
5880
  }, 0);
5714
5881
  }
5715
5882
  function nodeFlowOffset(input, id, layerFlowSize, nodeFlowSize, layerByNodeId, margins) {
5716
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
5883
+ const node = findNode(input.graph.nodes, id);
5717
5884
  const alignment = node ? input.nodeSettings?.(node)?.alignment ?? input.settings.alignment : input.settings.alignment;
5718
5885
  let ratio;
5719
5886
  if (alignment === "LEFT" || alignment === "TOP") ratio = 0;
@@ -5962,7 +6129,56 @@ function simplifyRoute$1(points) {
5962
6129
  function implicitEdgeEndpoints(input, placement, orientation) {
5963
6130
  const horizontal$1 = input.direction === "left" || input.direction === "right";
5964
6131
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
5965
- const groups = /* @__PURE__ */ new Map();
6132
+ const nodeSettingsByNode = /* @__PURE__ */ new Map();
6133
+ const nodeSettingsOf = (node) => {
6134
+ if (!nodeSettingsByNode.has(node)) nodeSettingsByNode.set(node, input.nodeSettings?.(node));
6135
+ return nodeSettingsByNode.get(node);
6136
+ };
6137
+ const portSettingsByNode = /* @__PURE__ */ new Map();
6138
+ const portSettingsOf = (port, node) => {
6139
+ let byPort = portSettingsByNode.get(node);
6140
+ if (!byPort) portSettingsByNode.set(node, byPort = /* @__PURE__ */ new Map());
6141
+ if (!byPort.has(port)) byPort.set(port, input.portSettings?.(port, node));
6142
+ return byPort.get(port);
6143
+ };
6144
+ const groups = [];
6145
+ const groupsBySide = /* @__PURE__ */ new Map();
6146
+ const modelOrderPromotion = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
6147
+ const directionReversed = input.direction === "left" || input.direction === "up";
6148
+ const endpointSide = (nodeId, portName) => {
6149
+ const node = nodeById.get(nodeId);
6150
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
6151
+ const constraints = node ? nodeSettingsOf(node)?.portConstraints : void 0;
6152
+ if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
6153
+ const configured = portSettingsOf(port, node)?.["port.side"];
6154
+ return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
6155
+ };
6156
+ const fixedImplicitSide = (nodeId, portName, source) => {
6157
+ const node = nodeById.get(nodeId);
6158
+ const constraints = node && nodeSettingsOf(node)?.portConstraints;
6159
+ if (portName !== void 0 || ![
6160
+ "FIXED_SIDE",
6161
+ "FIXED_ORDER",
6162
+ "FIXED_RATIO",
6163
+ "FIXED_POS"
6164
+ ].includes(String(constraints))) return void 0;
6165
+ return source !== directionReversed ? "after" : "before";
6166
+ };
6167
+ const addEntry = (nodeId, side, entry) => {
6168
+ let byNode = groupsBySide.get(side);
6169
+ if (!byNode) groupsBySide.set(side, byNode = /* @__PURE__ */ new Map());
6170
+ const group = byNode.get(nodeId);
6171
+ if (group) group.entries.push(entry);
6172
+ else {
6173
+ const created = {
6174
+ nodeId,
6175
+ side,
6176
+ entries: [entry]
6177
+ };
6178
+ byNode.set(nodeId, created);
6179
+ groups.push(created);
6180
+ }
6181
+ };
5966
6182
  for (const edge of input.graph.edges) {
5967
6183
  if (hasMovableLoopPorts(input, edge)) continue;
5968
6184
  const sourceRect = placement.rectByNodeId.get(edge.sourceId);
@@ -5970,50 +6186,33 @@ function implicitEdgeEndpoints(input, placement, orientation) {
5970
6186
  if (!sourceRect || !targetRect) continue;
5971
6187
  const sourceFlow = horizontal$1 ? sourceRect.x : sourceRect.y;
5972
6188
  const targetFlow = horizontal$1 ? targetRect.x : targetRect.y;
5973
- const modelOrderPromotion = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
5974
6189
  const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
5975
6190
  const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
5976
6191
  const forward = modelOrderPromotion || (sourceFlow === targetFlow && (sourceBoundary || targetBoundary) ? sourceBoundary ? sourceBoundary.port.side === "EAST" || sourceBoundary.port.side === "SOUTH" : targetBoundary.port.side === "WEST" || targetBoundary.port.side === "NORTH" : sourceFlow <= targetFlow);
5977
6192
  const feedback = input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
5978
- const directionReversed = input.direction === "left" || input.direction === "up";
5979
- const endpointSide = (nodeId, portName) => {
5980
- const node = nodeById.get(nodeId);
5981
- const port = node?.ports?.find((candidate) => candidate.name === portName);
5982
- const constraints = node ? input.nodeSettings?.(node)?.portConstraints : void 0;
5983
- if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
5984
- const configured = input.portSettings?.(port, node)?.["port.side"];
5985
- return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
5986
- };
5987
- const fixedImplicitSide = (nodeId, portName, source) => {
5988
- const node = nodeById.get(nodeId);
5989
- const constraints = node && input.nodeSettings?.(node)?.portConstraints;
5990
- if (portName !== void 0 || ![
5991
- "FIXED_SIDE",
5992
- "FIXED_ORDER",
5993
- "FIXED_RATIO",
5994
- "FIXED_POS"
5995
- ].includes(String(constraints))) return void 0;
5996
- return source !== directionReversed ? "after" : "before";
5997
- };
5998
6193
  const sourceSide = endpointSide(edge.sourceId, edge.sourcePort) ?? fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ?? (feedback ? directionReversed ? "before" : "after" : forward ? "after" : "before");
5999
6194
  const targetSide = endpointSide(edge.targetId, edge.targetPort) ?? fixedImplicitSide(edge.targetId, edge.targetPort, false) ?? (feedback ? directionReversed ? "after" : "before" : forward ? "before" : "after");
6000
- const sourceKey = `${edge.sourceId}:${sourceSide}`;
6001
- const targetKey = `${edge.targetId}:${targetSide}`;
6002
- const sourceGroup = groups.get(sourceKey) ?? [];
6003
- sourceGroup.push({
6195
+ addEntry(edge.sourceId, sourceSide, {
6004
6196
  edge,
6005
6197
  endpoint: "source"
6006
6198
  });
6007
- groups.set(sourceKey, sourceGroup);
6008
- const targetGroup = groups.get(targetKey) ?? [];
6009
- targetGroup.push({
6199
+ addEntry(edge.targetId, targetSide, {
6010
6200
  edge,
6011
6201
  endpoint: "target"
6012
6202
  });
6013
- groups.set(targetKey, targetGroup);
6014
6203
  }
6015
6204
  const result = /* @__PURE__ */ new Map();
6016
6205
  const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
6206
+ const orderIndexes = /* @__PURE__ */ new Map();
6207
+ const orderIndex = (list, id) => {
6208
+ let positions = orderIndexes.get(list);
6209
+ if (!positions) {
6210
+ positions = /* @__PURE__ */ new Map();
6211
+ for (const [index, item] of list.entries()) if (!positions.has(item)) positions.set(item, index);
6212
+ orderIndexes.set(list, positions);
6213
+ }
6214
+ return positions.get(id) ?? -1;
6215
+ };
6017
6216
  const placedPorts = /* @__PURE__ */ new Map();
6018
6217
  const portSideCounts = /* @__PURE__ */ new Map();
6019
6218
  const portSideCount = (node, side) => {
@@ -6021,22 +6220,19 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6021
6220
  if (!counts) {
6022
6221
  counts = /* @__PURE__ */ new Map();
6023
6222
  for (const port of node.ports ?? []) {
6024
- const portSide = input.portSettings?.(port, node)?.["port.side"];
6223
+ const portSide = portSettingsOf(port, node)?.["port.side"];
6025
6224
  counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
6026
6225
  }
6027
6226
  portSideCounts.set(node, counts);
6028
6227
  }
6029
6228
  return counts.get(side) ?? 0;
6030
6229
  };
6031
- for (const [key, entries] of groups) {
6032
- const split = key.lastIndexOf(":");
6033
- const nodeId = key.slice(0, split);
6034
- const side = key.slice(split + 1);
6230
+ for (const { nodeId, side, entries } of groups) {
6035
6231
  const rect = placement.rectByNodeId.get(nodeId);
6036
6232
  if (!rect) continue;
6037
6233
  const mergeEdges = input.settings.mergeEdges === true;
6038
6234
  const node = nodeById.get(nodeId);
6039
- const nodeSettings = node ? input.nodeSettings?.(node) : void 0;
6235
+ const nodeSettings = node ? nodeSettingsOf(node) : void 0;
6040
6236
  const hypernode = nodeSettings?.hypernode;
6041
6237
  const unzipping = (input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING";
6042
6238
  const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
@@ -6055,7 +6251,7 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6055
6251
  const leftIncoming = left.endpoint === "target" !== (orientation?.reversedEdgeIds.has(left.edge.id) ?? false);
6056
6252
  if (leftIncoming === (right.endpoint === "target" !== (orientation?.reversedEdgeIds.has(right.edge.id) ?? false))) {
6057
6253
  const selected = (leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId)?.get(nodeId);
6058
- const a = selected?.indexOf(left.edge.id) ?? -1, b = selected?.indexOf(right.edge.id) ?? -1;
6254
+ const a = selected ? orderIndex(selected, left.edge.id) : -1, b = selected ? orderIndex(selected, right.edge.id) : -1;
6059
6255
  if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
6060
6256
  }
6061
6257
  if (interactiveLeftSourceOrder) {
@@ -6128,9 +6324,9 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6128
6324
  };
6129
6325
  const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
6130
6326
  const explicitPort = node?.ports?.find((port) => port.name === portName);
6131
- const physicalSide = node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.side"] : void 0;
6327
+ const physicalSide = node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.side"] : void 0;
6132
6328
  const singleFixedSidePort = constraints === "FIXED_SIDE" && physicalSide !== void 0 && node?.ports !== void 0 && portSideCount(node, physicalSide) === 1;
6133
- const authoredAnchor = node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.anchor"] : void 0;
6329
+ const authoredAnchor = node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.anchor"] : void 0;
6134
6330
  if (portName !== void 0 && node && (constraints === "FIXED_POS" || constraints === "FIXED_RATIO" || constraints === "FIXED_ORDER" || singleFixedSidePort || authoredAnchor !== void 0)) point = getPortPoint(node, portName, rect, point, input.direction, input, placedPorts);
6135
6331
  const pair = result.get(edge.id) ?? {
6136
6332
  source: point,
@@ -6341,6 +6537,14 @@ function routeEdges(style) {
6341
6537
  flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
6342
6538
  }
6343
6539
  const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
6540
+ const placedIdsByFlowLayer = () => {
6541
+ const ids = flowLayers.map(() => []);
6542
+ for (const id of placement.rectByNodeId.keys()) {
6543
+ const layer = flowLayerByNodeId.get(id);
6544
+ if (layer !== void 0) ids[layer].push(id);
6545
+ }
6546
+ return ids;
6547
+ };
6344
6548
  const labelFlowLayers = new Set([...flowLayerByNodeId].filter(([id]) => id.startsWith("__layout_dummy:label:")).map(([, layer]) => layer));
6345
6549
  for (const [id, rect] of placement.rectByNodeId) {
6346
6550
  const node = nodeById.get(id);
@@ -6417,10 +6621,11 @@ function routeEdges(style) {
6417
6621
  labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
6418
6622
  }
6419
6623
  let labelShift = 0;
6624
+ const labelShiftIds = placedIdsByFlowLayer();
6420
6625
  for (const [layerNo, bounds] of flowLayers.entries()) {
6421
6626
  if (labelShift !== 0) {
6422
- for (const [id, rect] of placement.rectByNodeId) {
6423
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
6627
+ for (const id of labelShiftIds[layerNo]) {
6628
+ const rect = placement.rectByNodeId.get(id);
6424
6629
  mutableRects.set(id, horizontal$1 ? {
6425
6630
  ...rect,
6426
6631
  x: rect.x + labelShift
@@ -6753,8 +6958,9 @@ function routeEdges(style) {
6753
6958
  candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
6754
6959
  if (segment.partner !== void 0) {
6755
6960
  const partner = result.segments[segment.partner];
6756
- candidate.secondSlot = increasing ? partner.slot : maximumSlot - segment.slot;
6757
- if (!increasing) candidate.slot = maximumSlot - partner.slot;
6961
+ const [first, second] = physicalPort(candidate, "source").graphSource ? [segment, partner] : [partner, segment];
6962
+ candidate.slot = increasing ? first.slot : maximumSlot - first.slot;
6963
+ candidate.secondSlot = increasing ? second.slot : maximumSlot - second.slot;
6758
6964
  const sourcePort = portByCandidate.get(candidate).source;
6759
6965
  const offset = (increasing ? candidate.sourceCross : candidate.targetCross) - byPort.get(sourcePort).position + (runShift.get(sourcePort) ?? 0);
6760
6966
  candidate.crossover = segment.outgoing[0] + offset;
@@ -6767,18 +6973,21 @@ function routeEdges(style) {
6767
6973
  const leadingSlots = slotsByGap[0] ?? 0;
6768
6974
  const leadingWidth = leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
6769
6975
  let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
6976
+ const flowLayerIds = flowLayers.map(() => []);
6977
+ for (const [id, layer] of flowLayerByNodeId) flowLayerIds[layer].push(id);
6770
6978
  const externalFlowLayers = new Set(flowLayers.flatMap((_, layer) => {
6771
- const ids = [...flowLayerByNodeId].filter(([, index]) => index === layer);
6772
- return ids.length && ids.every(([id]) => {
6979
+ const ids = flowLayerIds[layer];
6980
+ return ids.length && ids.every((id) => {
6773
6981
  const node = nodeById.get(id);
6774
6982
  const side = node && externalPortDummyOf(node)?.side;
6775
6983
  return horizontal$1 ? side === "WEST" || side === "EAST" : side === "NORTH" || side === "SOUTH";
6776
6984
  }) ? [layer] : [];
6777
6985
  }));
6986
+ const shiftedIds = placedIdsByFlowLayer();
6778
6987
  for (const [layerNo, bounds] of flowLayers.entries()) {
6779
6988
  const shift = nextStart - bounds.start;
6780
- for (const [id, rect] of placement.rectByNodeId) {
6781
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
6989
+ for (const id of shiftedIds[layerNo]) {
6990
+ const rect = placement.rectByNodeId.get(id);
6782
6991
  mutableRects.set(id, horizontal$1 ? {
6783
6992
  ...rect,
6784
6993
  x: rect.x + shift
@@ -6992,10 +7201,11 @@ function routeEdges(style) {
6992
7201
  });
6993
7202
  }
6994
7203
  let nextStart = flowLayers[0]?.start ?? 0;
7204
+ const shiftedIds = placedIdsByFlowLayer();
6995
7205
  for (const [layerNo, bounds] of flowLayers.entries()) {
6996
7206
  const shift = nextStart - bounds.start;
6997
- for (const [id, rect] of placement.rectByNodeId) {
6998
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
7207
+ for (const id of shiftedIds[layerNo]) {
7208
+ const rect = placement.rectByNodeId.get(id);
6999
7209
  mutableRects.set(id, horizontal$1 ? {
7000
7210
  ...rect,
7001
7211
  x: rect.x + shift
@@ -7868,7 +8078,7 @@ function normalizePlacementForPortExtents(input, placement, order, orientation)
7868
8078
  let trailingBoundary = Number.NEGATIVE_INFINITY;
7869
8079
  let leadingBoundary = Number.POSITIVE_INFINITY;
7870
8080
  for (const [ids, trailing] of [[layer, true], [nextLayer, false]]) for (const id of ids) {
7871
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
8081
+ const node = findNode(input.graph.nodes, id);
7872
8082
  const rect = placement.rectByNodeId.get(id);
7873
8083
  if (!node || !rect) continue;
7874
8084
  const bodyStart = horizontal$1 ? rect.x : rect.y;
@@ -9654,6 +9864,10 @@ function crossing(s, t) {
9654
9864
  const v = h === s ? t : s;
9655
9865
  return v.a.x > Math.min(h.a.x, h.b.x) + EPS && v.a.x < Math.max(h.a.x, h.b.x) - EPS && h.a.y > Math.min(v.a.y, v.b.y) + EPS && h.a.y < Math.max(v.a.y, v.b.y) - EPS;
9656
9866
  }
9867
+ const onBorder = (p, r) => {
9868
+ const inX = p.x >= r.x - EPS && p.x <= r.x + r.width + EPS;
9869
+ return p.y >= r.y - EPS && p.y <= r.y + r.height + EPS && (Math.abs(p.x - r.x) <= EPS || Math.abs(p.x - r.x - r.width) <= EPS) || inX && (Math.abs(p.y - r.y) <= EPS || Math.abs(p.y - r.y - r.height) <= EPS);
9870
+ };
9657
9871
  const segmentsOf = (points) => points.slice(1).flatMap((b, i) => {
9658
9872
  const s = {
9659
9873
  a: points[i],
@@ -9686,7 +9900,34 @@ function separateOpposingTracks(scene) {
9686
9900
  if (!rects) terminalCache.set(key, rects = terminals(a.route, b.route));
9687
9901
  return rects;
9688
9902
  };
9903
+ const boxes = /* @__PURE__ */ new WeakMap();
9904
+ const boxOf = (segments) => {
9905
+ let box = boxes.get(segments);
9906
+ if (!box) {
9907
+ box = [
9908
+ Infinity,
9909
+ Infinity,
9910
+ -Infinity,
9911
+ -Infinity
9912
+ ];
9913
+ for (const { a, b } of segments) {
9914
+ box[0] = Math.min(box[0], a.x, b.x);
9915
+ box[1] = Math.min(box[1], a.y, b.y);
9916
+ box[2] = Math.max(box[2], a.x, b.x);
9917
+ box[3] = Math.max(box[3], a.y, b.y);
9918
+ }
9919
+ boxes.set(segments, box);
9920
+ }
9921
+ return box;
9922
+ };
9923
+ const none = {
9924
+ opposing: 0,
9925
+ overlap: 0,
9926
+ crossings: 0
9927
+ };
9689
9928
  const pair = (mine, self, other) => {
9929
+ const [ax0, ay0, ax1, ay1] = boxOf(mine), [bx0, by0, bx1, by1] = boxOf(other.measured);
9930
+ if (ax1 < bx0 - EPS || bx1 < ax0 - EPS || ay1 < by0 - EPS || by1 < ay0 - EPS) return none;
9690
9931
  const rects = terminalsOf(self, other);
9691
9932
  const as = rects.length ? clip(mine, rects, true) : mine, bs = rects.length ? clip(other.measured, rects, true) : other.measured;
9692
9933
  let opposing = 0, overlap = 0, crossings$1 = 0;
@@ -9899,8 +10140,8 @@ function separateOpposingTracks(scene) {
9899
10140
  y: p.y
9900
10141
  };
9901
10142
  if (k >= 1 && k <= raw.length - 2) {
9902
- const before = raw[k - 1], after = raw[k + 1];
9903
- if (h ? !vertical(before) || !vertical(after) : !horizontal(before) || !horizontal(after)) continue;
10143
+ const before = raw[k - 1], after$1 = raw[k + 1];
10144
+ if (h ? !vertical(before) || !vertical(after$1) : !horizontal(before) || !horizontal(after$1)) continue;
9904
10145
  for (const delta of offsets) {
9905
10146
  const points = state.points.map((p, i) => i === k || i === k + 1 ? shift(p, delta) : p);
9906
10147
  const next = [
@@ -9911,11 +10152,11 @@ function separateOpposingTracks(scene) {
9911
10152
  a: points[i],
9912
10153
  b: points[i + 1]
9913
10154
  }));
9914
- if (!keeps(before, next[0]) || !keeps(after, next[2])) continue;
10155
+ if (!keeps(before, next[0]) || !keeps(after$1, next[2])) continue;
9915
10156
  consider(points, [
9916
10157
  before,
9917
10158
  segment,
9918
- after
10159
+ after$1
9919
10160
  ], next, next[1], 0, delta);
9920
10161
  }
9921
10162
  continue;
@@ -9938,6 +10179,32 @@ function separateOpposingTracks(scene) {
9938
10179
  a: port,
9939
10180
  b: corner
9940
10181
  }], [inflate(rect, TERMINAL_MARGIN)], true).reduce((sum, part) => sum + size(part), 0) : along;
10182
+ const after = {
10183
+ a: corner,
10184
+ b: beyond
10185
+ };
10186
+ const against = (s) => states.reduce((sum, other) => other === state ? sum : other.measured.reduce((n, t) => n + (opposite(s, t) ? shared(s, t) : 0), sum), 0);
10187
+ if (state.route.ends.length === 2 && state.route.ports?.[reversed ? 1 : 0] === void 0 && rect && onBorder(port, rect) && (h ? vertical(after) : horizontal(after))) {
10188
+ const lo = h ? rect.y : rect.x, hi = lo + (h ? rect.height : rect.width);
10189
+ for (const delta of offsets) {
10190
+ const moved = shift(port, delta);
10191
+ if ((h ? moved.y : moved.x) < lo - EPS || (h ? moved.y : moved.x) > hi + EPS) continue;
10192
+ const next = [{
10193
+ a: moved,
10194
+ b: shift(corner, delta)
10195
+ }, {
10196
+ a: shift(corner, delta),
10197
+ b: beyond
10198
+ }];
10199
+ if (!keeps(after, next[1]) || against(next[1]) > against(after) + EPS) continue;
10200
+ const replaced = [
10201
+ moved,
10202
+ shift(corner, delta),
10203
+ ...path.slice(2)
10204
+ ];
10205
+ consider(reversed ? replaced.reverse() : replaced, [segment, after], next, next[0], 0, delta);
10206
+ }
10207
+ }
9941
10208
  for (const stub of new Set([along - clearance, spacing])) for (const delta of offsets) {
9942
10209
  if (stub < 1 || along - stub < spacing) continue;
9943
10210
  const q = {
@@ -9995,14 +10262,34 @@ function separateOpposingTracks(scene) {
9995
10262
  * half a spacing either side of the port's centre.
9996
10263
  */
9997
10264
  function splitSharedPorts(tolerant) {
9998
- const ports = scene.ports;
9999
- if (!ports?.size) return;
10000
10265
  const groups = /* @__PURE__ */ new Map();
10266
+ const implicit = /* @__PURE__ */ new Map();
10001
10267
  for (const state of states) for (const end of [0, 1]) {
10002
- const id = state.route.ports?.[end];
10003
- const port = id === void 0 ? void 0 : ports.get(id);
10004
- if (!port || state.points.length < 2) continue;
10268
+ if (state.points.length < 2) continue;
10005
10269
  const [p, q] = end ? [...state.points].reverse() : state.points;
10270
+ let id = state.route.ports?.[end];
10271
+ let port = id === void 0 ? void 0 : scene.ports?.get(id);
10272
+ const node = state.route.ends.length === 2 ? state.route.ends[end] : void 0;
10273
+ const rect = node === void 0 ? void 0 : nodes.get(node);
10274
+ if (id === void 0 && rect && onBorder(p, rect)) {
10275
+ const points = implicit.get(node) ?? [];
10276
+ implicit.set(node, points);
10277
+ let at = points.find((o) => Math.abs(o.x - p.x) <= EPS && Math.abs(o.y - p.y) <= EPS);
10278
+ if (!at) points.push(at = {
10279
+ x: p.x,
10280
+ y: p.y
10281
+ });
10282
+ id = `${node}\0${points.indexOf(at)}`;
10283
+ port = {
10284
+ node,
10285
+ rect: {
10286
+ ...at,
10287
+ width: 0,
10288
+ height: 0
10289
+ }
10290
+ };
10291
+ }
10292
+ if (!port) continue;
10006
10293
  const s = {
10007
10294
  a: p,
10008
10295
  b: q
@@ -10024,7 +10311,13 @@ function separateOpposingTracks(scene) {
10024
10311
  end
10025
10312
  });
10026
10313
  }
10027
- for (const { port, normal, ends } of groups.values()) {
10314
+ for (const group of groups.values()) {
10315
+ const { port, normal } = group;
10316
+ const ends = group.ends.filter(({ state, end }) => {
10317
+ if (state.route.ports?.[end] !== void 0) return true;
10318
+ const p = end ? state.points.at(-1) : state.points[0];
10319
+ return Math.abs(p.x - port.rect.x) <= EPS && Math.abs(p.y - port.rect.y) <= EPS;
10320
+ });
10028
10321
  const incoming = ends.filter((e) => e.end === 1), outgoing = ends.filter((e) => e.end === 0);
10029
10322
  if (!incoming.length || !outgoing.length) continue;
10030
10323
  const stub = ({ state, end }) => {
@@ -10043,9 +10336,25 @@ function separateOpposingTracks(scene) {
10043
10336
  const sideLo = node[lateral], sideHi = node[lateral] + (lateral === "x" ? node.width : node.height);
10044
10337
  const wide = extent$2 >= spacing - EPS;
10045
10338
  const half = spacing / 2;
10046
- const shifted = (points, end, offset) => {
10339
+ const slid = (state, points, end, offset) => {
10340
+ if (state.route.ports?.some((id) => id !== void 0)) return void 0;
10341
+ if (Math.abs(points[0][across] - points[1][across]) <= EPS) return void 0;
10342
+ const far = nodes.get(state.route.ends[1 - end] ?? "");
10343
+ const moved = points.map((p) => ({
10344
+ ...p,
10345
+ [lateral]: p[lateral] + offset
10346
+ }));
10347
+ const within = (value$1, rect) => {
10348
+ const lo = rect[lateral];
10349
+ return value$1 >= lo - EPS && value$1 <= lo + (lateral === "x" ? rect.width : rect.height) + EPS;
10350
+ };
10351
+ const value = moved[0][lateral];
10352
+ return within(value, node) && far && within(value, far) ? moved : void 0;
10353
+ };
10354
+ const shifted = (state, points, end, offset) => {
10047
10355
  if (Math.abs(offset) <= EPS) return points;
10048
10356
  const path = end ? [...points].reverse() : [...points];
10357
+ if (path.length === 2) return slid(state, points, end, offset);
10049
10358
  if (path.length < 3) return void 0;
10050
10359
  const [p, q, r] = path;
10051
10360
  const along = r[lateral] - q[lateral];
@@ -10109,7 +10418,7 @@ function separateOpposingTracks(scene) {
10109
10418
  const next = /* @__PURE__ */ new Map();
10110
10419
  let feasible = true;
10111
10420
  for (const e of ends) {
10112
- const points = shifted(next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
10421
+ const points = shifted(e.state, next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
10113
10422
  if (!points) {
10114
10423
  feasible = false;
10115
10424
  break;
@@ -14589,10 +14898,16 @@ function buildNeighbors(input, order) {
14589
14898
  return (horizontal$1 ? port?.height : port?.width) ?? 0;
14590
14899
  });
14591
14900
  };
14901
+ const loopsByNodeId = /* @__PURE__ */ new Map();
14902
+ for (const edge of input.graph.edges) if (edge.sourceId === edge.targetId) {
14903
+ const loops = loopsByNodeId.get(edge.sourceId);
14904
+ if (loops) loops.push(edge);
14905
+ else loopsByNodeId.set(edge.sourceId, [edge]);
14906
+ }
14592
14907
  const loopTerminals = (id, side) => {
14593
14908
  const node = nodeById.get(id);
14594
14909
  if (!node) return 0;
14595
- return input.graph.edges.filter((edge) => edge.sourceId === id && edge.targetId === id).flatMap((edge) => [edge.sourcePort, edge.targetPort]).filter((name) => {
14910
+ return (loopsByNodeId.get(id) ?? []).flatMap((edge) => [edge.sourcePort, edge.targetPort]).filter((name) => {
14596
14911
  const port = node.ports?.find((candidate) => candidate.name === name);
14597
14912
  return port !== void 0 && input.portSettings?.(port, node)?.["port.side"] === side.toUpperCase() && usesSharedSideSlot(input, node, name);
14598
14913
  }).length;
@@ -14703,8 +15018,8 @@ function alignBlocks(input, order, bal, neighbors, markedEdges) {
14703
15018
  const adjacent = (bal.hdir === "LEFT" ? neighbors.right : neighbors.left).get(id) ?? [];
14704
15019
  const low = Math.floor((adjacent.length + 1) / 2) - 1;
14705
15020
  const high = Math.ceil((adjacent.length + 1) / 2) - 1;
14706
- const indices = bal.vdir === "UP" ? Array.from({ length: Math.max(0, high - low + 1) }, (_, i) => high - i) : Array.from({ length: Math.max(0, high - low + 1) }, (_, i) => low + i);
14707
- for (const median of indices) {
15021
+ for (let i = 0; i < high - low + 1; i++) {
15022
+ const median = bal.vdir === "UP" ? high - i : low + i;
14708
15023
  if (bal.align.get(id) !== id) break;
14709
15024
  const neighbor = adjacent[median];
14710
15025
  if (neighbor === void 0) continue;
@@ -18533,11 +18848,14 @@ function* createLayeredScopePipeline(graph, options, context) {
18533
18848
  settings: options.settings ?? {},
18534
18849
  ...options.nodeSettings === void 0 ? {} : { nodeSettings: options.nodeSettings },
18535
18850
  ...options.edgeSettings === void 0 ? {} : { edgeSettings: options.edgeSettings },
18536
- portSettings: (port, node) => ({
18537
- ...options.portSettings?.(port, node),
18538
- ...parallelPortIndexByKey.has(`${node.id}\0${port.name}`) ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) } : {},
18539
- ...switchedSideByPort.has(`${node.id}\0${port.name}`) ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) } : {}
18540
- })
18851
+ portSettings: (port, node) => {
18852
+ const key = `${node.id}\0${port.name}`;
18853
+ return {
18854
+ ...options.portSettings?.(port, node),
18855
+ ...parallelPortIndexByKey.has(key) ? { "port.index": parallelPortIndexByKey.get(key) } : {},
18856
+ ...switchedSideByPort.has(key) ? { "port.side": switchedSideByPort.get(key) } : {}
18857
+ };
18858
+ }
18541
18859
  };
18542
18860
  const measure = (id, run) => {
18543
18861
  context?.throwIfAborted();
@@ -18683,8 +19001,10 @@ function* createLayeredScopePipeline(graph, options, context) {
18683
19001
  const northEdges = /* @__PURE__ */ new Set(), southEdges = /* @__PURE__ */ new Set();
18684
19002
  const upper = direction === "right" || direction === "left" ? "NORTH" : "WEST";
18685
19003
  const lower = direction === "right" || direction === "left" ? "SOUTH" : "EAST";
19004
+ const endpointNodes = /* @__PURE__ */ new Map();
19005
+ for (const node of expanded.input.graph.nodes) if (!endpointNodes.has(node.id)) endpointNodes.set(node.id, node);
18686
19006
  for (const edge of expanded.input.graph.edges) for (const [id, name] of [[edge.sourceId, edge.sourcePort], [edge.targetId, edge.targetPort]]) {
18687
- const node = expanded.input.graph.nodes.find((n) => n.id === id), port = node?.ports?.find((p) => p.name === name);
19007
+ const node = endpointNodes.get(id), port = node?.ports?.find((p) => p.name === name);
18688
19008
  const side = node && port ? expanded.input.portSettings?.(port, node)?.["port.side"] : void 0;
18689
19009
  if (side === upper) northEdges.add(id);
18690
19010
  if (side === lower) southEdges.add(id);
@@ -18811,10 +19131,6 @@ function* createLayeredScopePipeline(graph, options, context) {
18811
19131
  switchedSideByPort.set(`${origin.node.id}\0${origin.port.name}`, opposite$2);
18812
19132
  }
18813
19133
  }
18814
- if (expanded.input.settings.mergeEdges === true) order = {
18815
- ...order,
18816
- layers: order.layers.map((layer) => [...layer.filter((id) => id.startsWith("__layout_dummy:")), ...layer.filter((id) => !id.startsWith("__layout_dummy:"))])
18817
- };
18818
19134
  const unzippingFanIn = graph.edges.length === graph.nodes.length - 1 && graph.nodes.some((node) => graph.edges.filter((edge) => edge.targetId === node.id).length === graph.nodes.length - 1);
18819
19135
  if ((options.settings?.["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING") if (unzippingFanIn) order = applyLayerUnzipping(expanded.input, order);
18820
19136
  else {
@@ -19717,4 +20033,4 @@ const elkjs0111LayeredAlgorithm = {
19717
20033
  };
19718
20034
 
19719
20035
  //#endregion
19720
- export { createExternalPortDummy as $, breakCyclesByStronglyConnectedNodeType as A, minimizeCrossingsWithBarycenter as B, assignLayersByDepthFirstModelOrder as C, assignLayersWithCoffmanGraham as D, assignLayersInteractively as E, breakCyclesWithModelOrderBreadthFirstSearch as F, placePorts as G, minimizeCrossingsWithModelOrder as H, breakCyclesWithModelOrderDepthFirstSearch as I, routeEdgesWithSplines as J, routeEdgesOrthogonally as K, createLayerSweepSession as L, breakCyclesGreedilyByModelOrder as M, breakCyclesInteractively as N, breakCyclesByModelOrder as O, breakCyclesWithDepthFirstSearch as P, attachExternalPortDummy as Q, getPolylineMidpoint as R, assignLayersByBreadthFirstModelOrder as S, assignLayersByLongestPathToSink as T, placeNodesInLayers as U, minimizeCrossingsWithMedian as V, placeNodesInteractively as W, minimizeHierarchyCrossings as X, JavaRandom as Y, assignLayersWithNetworkSimplex as Z, statechartHints as _, fromElkLayeredOptionId as a, elkjs0111ResultPolicy as b, elkLayeredOptionDefinitions as c, placeNodesWithBrandesKoepf as d, externalPortDummyOf as et, assignLayersWithStretchWidth as f, hint as g, separateOpposingTracks as h, layeredAlgorithm as i, breakCyclesGreedily as j, breakCyclesByStronglyConnectedConnectivity as k, placeNodesWithNetworkSimplex as l, c as m, elkjs0111LayeredAlgorithm as n, createOrthogonalHypersegments as nt, toElkLayeredOptions as o, assignLayersWithMinWidth as p, routeEdgesWithPolylines as q, getLayeredLayout as r, elkLayeredEnumValues as s, createLayeredScopePipeline as t, getCrossingUnits as tt, placeNodesWithLinearSegments as u, isPerpendicularPortRestored as v, assignLayersByLongestPath as w, setElkjs0111ResultPolicy as x, hierarchicalPortSides as y, minimizeCrossingsInteractively as z };
20036
+ export { attachExternalPortDummy as $, breakCyclesByStronglyConnectedConnectivity as A, minimizeCrossingsInteractively as B, assignLayersByBreadthFirstModelOrder as C, assignLayersInteractively as D, assignLayersByLongestPathToSink as E, breakCyclesWithDepthFirstSearch as F, placeNodesInteractively as G, minimizeCrossingsWithMedian as H, breakCyclesWithModelOrderBreadthFirstSearch as I, routeEdgesWithPolylines as J, placePorts as K, breakCyclesWithModelOrderDepthFirstSearch as L, breakCyclesGreedily as M, breakCyclesGreedilyByModelOrder as N, assignLayersWithCoffmanGraham as O, breakCyclesInteractively as P, assignLayersWithNetworkSimplex as Q, createLayerSweepSession as R, CROSSING_MODEL as S, assignLayersByLongestPath as T, minimizeCrossingsWithModelOrder as U, minimizeCrossingsWithBarycenter as V, placeNodesInLayers as W, JavaRandom as X, routeEdgesWithSplines as Y, minimizeHierarchyCrossings as Z, statechartHints as _, fromElkLayeredOptionId as a, elkjs0111ResultPolicy as b, elkLayeredOptionDefinitions as c, placeNodesWithBrandesKoepf as d, createExternalPortDummy as et, assignLayersWithStretchWidth as f, hint as g, separateOpposingTracks as h, layeredAlgorithm as i, breakCyclesByStronglyConnectedNodeType as j, breakCyclesByModelOrder as k, placeNodesWithNetworkSimplex as l, c as m, elkjs0111LayeredAlgorithm as n, createOrthogonalHypersegments as nt, toElkLayeredOptions as o, assignLayersWithMinWidth as p, routeEdgesOrthogonally as q, getLayeredLayout as r, elkLayeredEnumValues as s, createLayeredScopePipeline as t, externalPortDummyOf as tt, placeNodesWithLinearSegments as u, isPerpendicularPortRestored as v, assignLayersByDepthFirstModelOrder as w, setElkjs0111ResultPolicy as x, hierarchicalPortSides as y, getPolylineMidpoint as z };