@statelyai/layout 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +32 -3
  2. package/dist/elkjs/index.cjs +2 -2
  3. package/dist/elkjs/index.d.cts +1 -1
  4. package/dist/elkjs/index.d.mts +1 -1
  5. package/dist/elkjs/index.mjs +2 -2
  6. package/dist/elkjs/main.cjs +1 -1
  7. package/dist/elkjs/main.mjs +1 -1
  8. package/dist/elkjs/worker.cjs +2 -2
  9. package/dist/elkjs/worker.mjs +2 -2
  10. package/dist/{elkjs-Cj-lWiK1.cjs → elkjs-Bh_E59N_.cjs} +3 -3
  11. package/dist/{elkjs-B5kN_zS6.mjs → elkjs-Dk9yOk9U.mjs} +3 -3
  12. package/dist/{index-B6cWNrMw.d.cts → index-0YVSvMvA.d.mts} +13 -3
  13. package/dist/{index-CJQLUKD9.d.mts → index-C0J4ttZe.d.cts} +13 -3
  14. package/dist/{index-DT6lPMfS.d.cts → index-Xc4R6y3z.d.cts} +9 -2
  15. package/dist/{index-F1U-fCTu.d.mts → index-lSp0oCJC.d.mts} +9 -2
  16. package/dist/index.cjs +7 -4
  17. package/dist/index.d.cts +4 -4
  18. package/dist/index.d.mts +4 -4
  19. package/dist/index.mjs +4 -4
  20. package/dist/layered/index.cjs +1 -1
  21. package/dist/layered/index.d.cts +2 -2
  22. package/dist/layered/index.d.mts +2 -2
  23. package/dist/layered/index.mjs +1 -1
  24. package/dist/{layered-BsAdm_u8.mjs → layered-CUwft7on.mjs} +488 -91
  25. package/dist/{layered-BJu8A21s.cjs → layered-QzYtokgN.cjs} +488 -91
  26. package/dist/{layout-engine-BFP1TZbF.cjs → layout-engine-CQ5FcBPC.cjs} +1 -1
  27. package/dist/{layout-engine-DnlmGxHL.mjs → layout-engine-DvDkMQM3.mjs} +1 -1
  28. package/dist/routing/index.cjs +5 -2
  29. package/dist/routing/index.d.cts +3 -3
  30. package/dist/routing/index.d.mts +3 -3
  31. package/dist/routing/index.mjs +2 -2
  32. package/dist/routing-D-LfMXOF.mjs +5189 -0
  33. package/dist/routing-Phxu1R0x.cjs +5423 -0
  34. package/dist/{types-CT2akdK0.d.mts → types-D8pYSn8I.d.cts} +112 -2
  35. package/dist/{types-uxPlGRof.d.cts → types-MAUtJfKQ.d.mts} +112 -2
  36. package/package.json +4 -1
  37. package/src/authoring/partial.ts +1 -7
  38. package/src/authoring/routing.ts +64 -19
  39. package/src/layered/feedback-chains.ts +22 -4
  40. package/src/layered/index.ts +186 -42
  41. package/src/layered/loop-envelopes.ts +96 -26
  42. package/src/layered/north-south-ports.ts +17 -4
  43. package/src/layered/self-loop-ports.ts +158 -0
  44. package/src/layered/strategies.ts +183 -32
  45. package/src/routing/algorithms.ts +1436 -148
  46. package/src/routing/bezier-fit.ts +605 -0
  47. package/src/routing/bezier-visibility.ts +107 -0
  48. package/src/routing/bezier.ts +596 -0
  49. package/src/routing/engine.ts +65 -13
  50. package/src/routing/index.ts +14 -0
  51. package/src/routing/model.ts +185 -1
  52. package/src/routing/path.ts +1 -1
  53. package/src/routing/policy-geometry.ts +536 -0
  54. package/src/routing/search.ts +10 -2
  55. package/src/routing/types.ts +77 -1
  56. package/dist/routing-CR8flv8b.mjs +0 -2780
  57. package/dist/routing-QIdGGFBe.cjs +0 -2996
@@ -1,4 +1,4 @@
1
- import { A as routeOrthogonal, F as LayoutError, I as UnsupportedLayoutError, M as recordRouteGeometry, N as nativeGeometry, O as overlaps$1, P as worldGeometry, c as orthogonalRouting, k as routeCrosses, v as routeToPoints } from "./routing-CR8flv8b.mjs";
1
+ import { A as overlaps$1, F as recordRouteGeometry, I as nativeGeometry, L as worldGeometry, M as routeOrthogonal, R as LayoutError, c as orthogonalRouting, j as routeCrosses, y as routeToPoints, z as UnsupportedLayoutError } from "./routing-D-LfMXOF.mjs";
2
2
  import { getNodeSize } from "@statelyai/graph/layout";
3
3
  import { createGraph } from "@statelyai/graph";
4
4
 
@@ -1608,8 +1608,174 @@ function findNode(nodes, id) {
1608
1608
  return position !== void 0 && nodes[position]?.id === id ? nodes[position] : nodes.find((node) => node.id === id);
1609
1609
  }
1610
1610
 
1611
+ //#endregion
1612
+ //#region src/layered/self-loop-ports.ts
1613
+ const hiddenBySettings = /* @__PURE__ */ new WeakMap();
1614
+ /** Sizes of the ports SelfLoopPreProcessor hid from a loop, if any. */
1615
+ function hiddenLoopPorts(settings, edgeId) {
1616
+ return hiddenBySettings.get(settings)?.get(edgeId);
1617
+ }
1618
+ /**
1619
+ * ELK's SelfLoopPreProcessor removes ports that carry only self loops from
1620
+ * nodes whose port order is not fixed, and SelfLoopPortRestorer later puts
1621
+ * them on the loop's side. The layered phases then see implicit loop ports;
1622
+ * {@link restoreSelfLoopPorts} places the hidden ports at the loop ends.
1623
+ */
1624
+ function hideSelfLoopPorts(graph, options) {
1625
+ const uses = /* @__PURE__ */ new Map();
1626
+ const nonLoop = /* @__PURE__ */ new Set();
1627
+ for (const edge of graph.edges) for (const [node, port] of [[edge.sourceId, edge.sourcePort], [edge.targetId, edge.targetPort]]) {
1628
+ if (port === void 0) continue;
1629
+ const key = `${node}\0${port}`;
1630
+ uses.set(key, (uses.get(key) ?? 0) + 1);
1631
+ if (edge.sourceId !== edge.targetId) nonLoop.add(key);
1632
+ }
1633
+ const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
1634
+ const hidden = /* @__PURE__ */ new Map();
1635
+ const edges = /* @__PURE__ */ new Map();
1636
+ const hiddenPorts = /* @__PURE__ */ new Set();
1637
+ for (const edge of graph.edges) {
1638
+ if (edge.sourceId !== edge.targetId || edge.sourcePort === void 0 || edge.targetPort === void 0 || edge.sourcePort === edge.targetPort) continue;
1639
+ const node = nodeById.get(edge.sourceId);
1640
+ const constraints = node && options.nodeSettings?.(node)?.portConstraints;
1641
+ if (!node || constraints !== void 0 && constraints !== "FREE" && constraints !== "UNDEFINED") continue;
1642
+ const ports = [edge.sourcePort, edge.targetPort].map((name) => node.ports?.find((port) => port.name === name));
1643
+ if (ports.some((port) => !port || uses.get(`${node.id}\0${port.name}`) !== 1 || nonLoop.has(`${node.id}\0${port.name}`))) continue;
1644
+ const size$1 = (port) => ({
1645
+ width: port.width ?? 0,
1646
+ height: port.height ?? 0
1647
+ });
1648
+ hidden.set(edge.id, {
1649
+ source: size$1(ports[0]),
1650
+ target: size$1(ports[1])
1651
+ });
1652
+ edges.set(edge.id, edge);
1653
+ for (const port of ports) hiddenPorts.add(`${node.id}\0${port.name}`);
1654
+ }
1655
+ if (hidden.size === 0) return void 0;
1656
+ const settings = { ...options.settings };
1657
+ hiddenBySettings.set(settings, hidden);
1658
+ return {
1659
+ graph: {
1660
+ ...graph,
1661
+ nodes: graph.nodes.map((node) => node.ports?.some((port) => hiddenPorts.has(`${node.id}\0${port.name}`)) ? {
1662
+ ...node,
1663
+ ports: node.ports.filter((port) => !hiddenPorts.has(`${node.id}\0${port.name}`))
1664
+ } : node),
1665
+ edges: graph.edges.map((edge) => hidden.has(edge.id) ? {
1666
+ ...edge,
1667
+ sourcePort: void 0,
1668
+ targetPort: void 0
1669
+ } : edge)
1670
+ },
1671
+ options: {
1672
+ ...options,
1673
+ settings
1674
+ },
1675
+ edges
1676
+ };
1677
+ }
1678
+ /** Put hidden loop ports back, anchored on the side their loop leaves from. */
1679
+ function restoreSelfLoopPorts(graph, result, edges) {
1680
+ const anchors = /* @__PURE__ */ new Map();
1681
+ for (const edge of result.edges) {
1682
+ const original = edges.get(edge.id);
1683
+ const points = edge.points;
1684
+ if (!original || !points?.length) continue;
1685
+ anchors.set(`${edge.sourceId}\0${original.sourcePort}`, points[0]);
1686
+ anchors.set(`${edge.targetId}\0${original.targetPort}`, points.at(-1));
1687
+ }
1688
+ const originalById = new Map(graph.nodes.map((node) => [node.id, node]));
1689
+ return {
1690
+ ...result,
1691
+ nodes: result.nodes.map((node) => {
1692
+ const original = originalById.get(node.id)?.ports;
1693
+ if (!original?.some((port) => anchors.has(`${node.id}\0${port.name}`))) return node;
1694
+ const placed = new Map(node.ports?.map((port) => [port.name, port]));
1695
+ return {
1696
+ ...node,
1697
+ ports: original.map((port) => {
1698
+ const anchor = anchors.get(`${node.id}\0${port.name}`);
1699
+ const width = port.width ?? 0, height = port.height ?? 0;
1700
+ if (!anchor) return placed.get(port.name) ?? {
1701
+ ...port,
1702
+ width,
1703
+ height,
1704
+ x: 0,
1705
+ y: 0
1706
+ };
1707
+ const x = anchor.x - node.x, y = anchor.y - node.y;
1708
+ return {
1709
+ ...port,
1710
+ width,
1711
+ height,
1712
+ x: x <= 0 ? x : x >= node.width ? x - width : x - width / 2,
1713
+ y: x <= 0 || x >= node.width ? y - height / 2 : y <= 0 ? y : y - height
1714
+ };
1715
+ })
1716
+ };
1717
+ }),
1718
+ edges: result.edges.map((edge) => {
1719
+ const original = edges.get(edge.id);
1720
+ return original ? {
1721
+ ...edge,
1722
+ sourcePort: original.sourcePort,
1723
+ targetPort: original.targetPort
1724
+ } : edge;
1725
+ })
1726
+ };
1727
+ }
1728
+
1611
1729
  //#endregion
1612
1730
  //#region src/layered/loop-envelopes.ts
1731
+ const crossSide = (side) => side === "NORTH" || side === "SOUTH";
1732
+ /**
1733
+ * Extents of a movable loop's hidden ports in the canonical rightward frame:
1734
+ * along its side, and protruding from it.
1735
+ */
1736
+ function loopPortExtents(input, edge, side) {
1737
+ const ports = hiddenLoopPorts(input.settings, edge.id);
1738
+ const horizontal$1 = input.direction === "right" || input.direction === "left";
1739
+ const along = crossSide(side) === horizontal$1 ? "width" : "height";
1740
+ const normal = along === "width" ? "height" : "width";
1741
+ return {
1742
+ along: ports ? ports.source[along] + ports.target[along] : 0,
1743
+ normal: ports ? Math.max(ports.source[normal], ports.target[normal]) : 0
1744
+ };
1745
+ }
1746
+ /** A movable loop's label extent away from its side, in the canonical frame. */
1747
+ function movableLoopLabelExtent(input, edge, side) {
1748
+ const horizontal$1 = input.direction === "right" || input.direction === "left";
1749
+ const size$1 = (crossSide(side) === horizontal$1 ? edge.height : edge.width) ?? 0;
1750
+ return size$1 > 0 ? size$1 + Number(input.settings["spacing.edgeLabel"] ?? 2) : 0;
1751
+ }
1752
+ /**
1753
+ * ELK's OrthogonalSelfLoopRouter routing slot of a movable loop, measured
1754
+ * from its node side in the canonical frame: past the protruding ports and
1755
+ * the self-loop spacing, then one edge spacing plus any label per inner slot.
1756
+ */
1757
+ function movableLoopTrack(input, sideLoops, edge, side, ordering) {
1758
+ const ordered = ordering === "REVERSE_STACKED" ? [...sideLoops].reverse() : sideLoops;
1759
+ const inner = ordering === "SEQUENCED" ? [] : ordered.slice(0, ordered.indexOf(edge));
1760
+ const edgeSpacing = Number(input.settings["spacing.edgeEdge"] ?? 10);
1761
+ return Math.max(0, ...sideLoops.map((loop) => loopPortExtents(input, loop, side).normal)) + Number(input.settings["spacing.nodeSelfLoop"] ?? 10) + inner.reduce((sum, loop) => sum + edgeSpacing + (crossSide(side) ? movableLoopLabelExtent(input, loop, side) : 0), 0);
1762
+ }
1763
+ /**
1764
+ * Canonical position of a movable loop port along its side, given its
1765
+ * DISTRIBUTED ratio among the side's implicit ports (ELK's port placement
1766
+ * with equal gaps between hidden ports of their own size).
1767
+ */
1768
+ function movableLoopPortOffset(input, sideLoops, side, length, ratio) {
1769
+ const count = sideLoops.length * 2;
1770
+ const extents = sideLoops.map((loop) => loopPortExtents(input, loop, side));
1771
+ const total = extents.reduce((sum, extent$2) => sum + extent$2.along, 0);
1772
+ const size$1 = total / count;
1773
+ const index = Math.round(ratio * (count + 1)) - 1;
1774
+ return {
1775
+ along: (length - total) / (count + 1) * (index + 1) + size$1 * index + size$1 / 2,
1776
+ normal: Math.max(0, ...extents.map((extent$2) => extent$2.normal))
1777
+ };
1778
+ }
1613
1779
  /** Unconstrained implicit loop ports are restored on loop faces after the sweep. */
1614
1780
  function hasMovableLoopPorts(input, edge) {
1615
1781
  if (edge.sourceId !== edge.targetId || edge.sourcePort !== void 0 || edge.targetPort !== void 0) return false;
@@ -1647,7 +1813,7 @@ function loopEnvelopes(input) {
1647
1813
  };
1648
1814
  const horizontal$1 = input.direction === "right" || input.direction === "left";
1649
1815
  const reverse = input.direction === "left" || input.direction === "up";
1650
- const spacing$1 = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1816
+ const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1651
1817
  let supported = true;
1652
1818
  const tracks = selfLoopTracks(allLoops);
1653
1819
  for (const edge of allLoops) {
@@ -1666,7 +1832,7 @@ function loopEnvelopes(input) {
1666
1832
  }, input.direction, input), getPortPoint(node, edge.targetPort, rect, {
1667
1833
  x: 0,
1668
1834
  y: 0
1669
- }, input.direction, input), sourceSide, targetSide, spacing$1 * (tracks.get(edge.id) + 1), input.direction);
1835
+ }, input.direction, input), sourceSide, targetSide, spacing * (tracks.get(edge.id) + 1), input.direction);
1670
1836
  if (labeled) {
1671
1837
  const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
1672
1838
  const { width = 0, height = 0 } = edge;
@@ -1691,11 +1857,10 @@ function loopEnvelopes(input) {
1691
1857
  }
1692
1858
  }
1693
1859
  const loops = allLoops.filter((edge) => hasMovableLoopPorts(input, edge));
1694
- if (loops.length === 0 || loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) continue;
1860
+ if (loops.length === 0) continue;
1695
1861
  const settings = input.nodeSettings?.(node);
1696
1862
  const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
1697
1863
  const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
1698
- const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1699
1864
  const envelope = {
1700
1865
  before: 0,
1701
1866
  after: 0,
@@ -1708,12 +1873,16 @@ function loopEnvelopes(input) {
1708
1873
  "flowAfter",
1709
1874
  "flowBefore"
1710
1875
  ] : distribution === "NORTH_SOUTH" ? ["before", "after"] : ["before"];
1876
+ const canonicalSides = {
1877
+ before: "NORTH",
1878
+ after: "SOUTH",
1879
+ flowAfter: "EAST",
1880
+ flowBefore: "WEST"
1881
+ };
1711
1882
  for (const side of sides$1) {
1712
1883
  const selected = loops.filter((_, index) => sides$1[index % sides$1.length] === side);
1713
- const horizontal$1 = input.direction === "right" || input.direction === "left";
1714
- const crossSize$3 = side === "before" || side === "after" ? horizontal$1 ? "height" : "width" : horizontal$1 ? "width" : "height";
1715
- const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
1716
- envelope[side] = spacing * (ordering === "SEQUENCED" ? Math.min(1, selected.length) : selected.length) + selected.reduce((sum, edge) => sum + ((edge[crossSize$3] ?? 0) > 0 ? (edge[crossSize$3] ?? 0) + labelSpacing : 0), 0);
1884
+ const canonical = canonicalSides[side];
1885
+ envelope[side] = Math.max(0, ...selected.map((edge) => movableLoopTrack(input, selected, edge, canonical, ordering) + movableLoopLabelExtent(input, edge, canonical)));
1717
1886
  }
1718
1887
  envelopes.set(node.id, envelope);
1719
1888
  }
@@ -2881,7 +3050,7 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
2881
3050
  if (point.x >= rect.x + rect.width - 1e-9) return "EAST";
2882
3051
  return point.y <= rect.y ? "NORTH" : "SOUTH";
2883
3052
  };
2884
- const crossSide = (side) => side === "NORTH" || side === "SOUTH";
3053
+ const crossSide$1 = (side) => side === "NORTH" || side === "SOUTH";
2885
3054
  const segments = [];
2886
3055
  for (const [edgeId, points] of canonicalRoutes) {
2887
3056
  const edge = edgeById.get(edgeId);
@@ -2891,8 +3060,8 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
2891
3060
  const first = points[index];
2892
3061
  const second = points[index + 1];
2893
3062
  if (Math.abs(first.x - second.x) > 1e-9 || Math.abs(first.y - second.y) < 1e-9) continue;
2894
- if (index === 0 && !crossSide(endpointSide(edge.sourceId, edge.sourcePort, first))) continue;
2895
- if (index === points.length - 2 && !crossSide(endpointSide(edge.targetId, edge.targetPort, second))) continue;
3063
+ if (index === 0 && !crossSide$1(endpointSide(edge.sourceId, edge.sourcePort, first))) continue;
3064
+ if (index === points.length - 2 && !crossSide$1(endpointSide(edge.targetId, edge.targetPort, second))) continue;
2896
3065
  const owner = index === 0 ? nodes.get(edge.sourceId) : index === points.length - 2 ? nodes.get(edge.targetId) : void 0;
2897
3066
  const ownerSide = owner ? endpointSide(index === 0 ? edge.sourceId : edge.targetId, index === 0 ? edge.sourcePort : edge.targetPort, index === 0 ? first : second) : void 0;
2898
3067
  const bend = index === 0 ? second : first;
@@ -3055,7 +3224,7 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3055
3224
  const points = canonicalRoutes.get(edge.id);
3056
3225
  const sourceSide = endpointSide(edge.sourceId, edge.sourcePort, points?.[0]);
3057
3226
  const targetSide = endpointSide(edge.targetId, edge.targetPort, points?.at(-1));
3058
- if (crossSide(sourceSide) && crossSide(targetSide)) continue;
3227
+ if (crossSide$1(sourceSide) && crossSide$1(targetSide)) continue;
3059
3228
  const reversed = orientation?.reversedEdgeIds.has(edge.id) ?? source.x > target.x;
3060
3229
  const from = reversed ? target : source, to = reversed ? source : target;
3061
3230
  constraints.push({
@@ -3149,8 +3318,8 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3149
3318
  const start = pointToCanonical(originals[0]);
3150
3319
  const next = pointToCanonical(originals[1]);
3151
3320
  const previous = pointToCanonical(originals.at(-2));
3152
- if (!crossSide(endpointSide(edge.sourceId, edge.sourcePort, start)) && Math.abs(start.x - next.x) < 1e-9) points[1].x = points[0].x;
3153
- if (!crossSide(endpointSide(edge.targetId, edge.targetPort, end)) && Math.abs(end.x - previous.x) < 1e-9) points[points.length - 2].x = points.at(-1).x;
3321
+ if (!crossSide$1(endpointSide(edge.sourceId, edge.sourcePort, start)) && Math.abs(start.x - next.x) < 1e-9) points[1].x = points[0].x;
3322
+ if (!crossSide$1(endpointSide(edge.targetId, edge.targetPort, end)) && Math.abs(end.x - previous.x) < 1e-9) points[points.length - 2].x = points.at(-1).x;
3154
3323
  }
3155
3324
  }
3156
3325
  const extent$2 = Math.max(...items.map((item) => item.x + item.width), ...[...canonicalRoutes.values()].flatMap((points) => points.map((point) => point.x)));
@@ -3719,6 +3888,19 @@ function assertNorthSouthPortSides(graph) {
3719
3888
  //#region src/layered/strategies.ts
3720
3889
  const modelSweepInputs = /* @__PURE__ */ new WeakSet();
3721
3890
  const nodeRelativePortRanksByInput = /* @__PURE__ */ new WeakMap();
3891
+ const labelPortSideFeedbackByInput = /* @__PURE__ */ new WeakMap();
3892
+ /**
3893
+ * @internal Collect the feedback segments routed to a center-label dummy's port
3894
+ * sides. Exempt segments keep the plain feedback detour instead.
3895
+ */
3896
+ function trackLabelPortSideFeedback(input, exempt) {
3897
+ const routed = /* @__PURE__ */ new Set();
3898
+ labelPortSideFeedbackByInput.set(input, {
3899
+ exempt,
3900
+ routed
3901
+ });
3902
+ return routed;
3903
+ }
3722
3904
  function getOrientedEndpoints(edge, orientation) {
3723
3905
  return orientation.reversedEdgeIds.has(edge.id) ? [edge.targetId, edge.sourceId] : [edge.sourceId, edge.targetId];
3724
3906
  }
@@ -4923,7 +5105,7 @@ const assignLayersWithCoffmanGraham = (input, orientation) => {
4923
5105
  seedOrder: [...layerMembers].reverse().flat()
4924
5106
  };
4925
5107
  };
4926
- function sortLayerByAdjacentPosition(layer, ranksByNodeId, statistic, random, preOrdered = true, sourceUnknownPlacement = true, perturbSummedWeight = true, units, sameLayerNeighbors = /* @__PURE__ */ new Map(), orderedVisits, forcedCompare) {
5108
+ function sortLayerByAdjacentPosition(layer, ranksByNodeId, statistic, random, preOrdered = true, sourceUnknownPlacement = true, perturbSummedWeight = true, units, sameLayerNeighbors = /* @__PURE__ */ new Map(), orderedVisits, forcedCompare, forcedTies) {
4927
5109
  const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index]));
4928
5110
  const adjacentPosition = /* @__PURE__ */ new Map();
4929
5111
  if (units && statistic === "mean") for (const [id, score] of associatedBarycenters(layer, ranksByNodeId, sameLayerNeighbors, units.associates, random, orderedVisits)) adjacentPosition.set(id, score);
@@ -4959,6 +5141,7 @@ function sortLayerByAdjacentPosition(layer, ranksByNodeId, statistic, random, pr
4959
5141
  }
4960
5142
  else for (const nodeId of layer) if (adjacentPosition.get(nodeId) === void 0) adjacentPosition.set(nodeId, originalIndex.get(nodeId) ?? 0);
4961
5143
  const compare = (a, b) => {
5144
+ if (forcedTies?.has(a) || forcedTies?.has(b)) return 0;
4962
5145
  return forcedCompare?.(a, b) || (adjacentPosition.get(a) ?? 0) - (adjacentPosition.get(b) ?? 0) || (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0);
4963
5146
  };
4964
5147
  if (forcedCompare) insertionSort(layer, transitiveComparator((id) => id, compare));
@@ -5110,6 +5293,10 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5110
5293
  let initialOrderAttempt = usesInitialModelOrder;
5111
5294
  const sourceUnknownPlacement = true;
5112
5295
  const forcedCompare = usesInitialModelOrder && input.settings["crossingMinimization.forceNodeModelOrder"] === true ? normalModelComparator(input) : void 0;
5296
+ const forcedTies = forcedCompare ? new Set(input.graph.nodes.filter((node) => {
5297
+ const constraint = input.nodeSettings?.(node)?.["layering.layerConstraint"];
5298
+ return constraint === "FIRST_SEPARATE" || constraint === "LAST_SEPARATE";
5299
+ }).map((node) => node.id)) : void 0;
5113
5300
  const medianWeights = /* @__PURE__ */ new Map();
5114
5301
  const listedEdgeIds = /* @__PURE__ */ new Map();
5115
5302
  const listedEdgeId = (edgeId) => {
@@ -5251,7 +5438,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5251
5438
  const shuffleFirstLayer = (forward) => {
5252
5439
  const firstLayerIndex = forward ? 0 : Math.max(0, working.length - 1);
5253
5440
  const firstLayerWeights = new Map((working[firstLayerIndex] ?? []).map((id) => [id, random.nextDouble()]));
5254
- const compare = (left, right) => forcedCompare?.(left, right) || (firstLayerWeights.get(left) ?? 0) - (firstLayerWeights.get(right) ?? 0);
5441
+ const compare = (left, right) => forcedTies?.has(left) || forcedTies?.has(right) ? 0 : forcedCompare?.(left, right) || (firstLayerWeights.get(left) ?? 0) - (firstLayerWeights.get(right) ?? 0);
5255
5442
  const first = working[firstLayerIndex];
5256
5443
  if (first) if (forcedCompare) insertionSort(first, transitiveComparator((id) => id, compare));
5257
5444
  else first.sort(compare);
@@ -5347,7 +5534,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5347
5534
  if (statistic === "median") sortWithMedianWeights(current, previous);
5348
5535
  else {
5349
5536
  const adjacent = adjacentRanks(previous, current, true);
5350
- sortLayerByAdjacentPosition(current, adjacent.ranks, statistic, random, !firstSweep || initialOrderAttempt || canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT", sourceUnknownPlacement, true, units, sameLayerNeighbors(isForward), adjacent.visits, forcedCompare);
5537
+ sortLayerByAdjacentPosition(current, adjacent.ranks, statistic, random, !firstSweep || initialOrderAttempt || canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT", sourceUnknownPlacement, true, units, sameLayerNeighbors(isForward), adjacent.visits, forcedCompare, forcedTies);
5351
5538
  }
5352
5539
  groupMixedPortDirections(current, previous, working[layer + 1]);
5353
5540
  if (exactPortSweep) distributePorts(layer, true);
@@ -5361,7 +5548,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5361
5548
  if (statistic === "median") sortWithMedianWeights(current, next);
5362
5549
  else {
5363
5550
  const adjacent = adjacentRanks(next, current, false);
5364
- sortLayerByAdjacentPosition(current, adjacent.ranks, statistic, random, !firstSweep || initialOrderAttempt || canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT", sourceUnknownPlacement, true, units, sameLayerNeighbors(isForward), adjacent.visits, forcedCompare);
5551
+ sortLayerByAdjacentPosition(current, adjacent.ranks, statistic, random, !firstSweep || initialOrderAttempt || canonicalNodes.get(current[0] ?? "")?.type === "EXTERNAL_PORT", sourceUnknownPlacement, true, units, sameLayerNeighbors(isForward), adjacent.visits, forcedCompare, forcedTies);
5365
5552
  }
5366
5553
  groupMixedPortDirections(current, next, working[layer - 1]);
5367
5554
  if (exactPortSweep) distributePorts(layer, false);
@@ -6409,13 +6596,13 @@ function routeEdges(style) {
6409
6596
  "WEST"
6410
6597
  ][index % 4] : distribution === "NORTH_SOUTH" && index % 2 === 1 ? "SOUTH" : "NORTH";
6411
6598
  };
6412
- const loopDistance = (loops, edge, side, ordering, canonical = false) => {
6413
- const axis = (side === "NORTH" || side === "SOUTH") === (!canonical || horizontal$1) ? "height" : "width";
6599
+ const loopDistance = (loops, edge, side, ordering) => {
6600
+ const axis = side === "NORTH" || side === "SOUTH" ? "height" : "width";
6414
6601
  const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
6415
6602
  const index = ordered.indexOf(edge);
6416
6603
  const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
6417
6604
  const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
6418
- const size$1 = (candidate) => canonical || input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true ? candidate[axis] ?? 0 : 0;
6605
+ const size$1 = (candidate) => input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true ? candidate[axis] ?? 0 : 0;
6419
6606
  return spacing * (ordering === "SEQUENCED" ? 1 : index + 1) + ordered.slice(0, index).reduce((extent$2, candidate) => extent$2 + (size$1(candidate) > 0 ? size$1(candidate) + labelSpacing : 0), 0) + (size$1(edge) > 0 && input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true ? size$1(edge) / 2 + labelSpacing : 0);
6420
6607
  };
6421
6608
  const hasFixedLoopPorts = (node) => {
@@ -6470,7 +6657,7 @@ function routeEdges(style) {
6470
6657
  for (const [id, loops] of selfLoopsByNodeId) {
6471
6658
  const rect = mutableRects.get(id);
6472
6659
  const node = nodeById.get(id);
6473
- if (!rect || !node) continue;
6660
+ if (!rect || !node || hasPlacementLoopEnvelope(placement, id)) continue;
6474
6661
  const settings = input.nodeSettings?.(node);
6475
6662
  const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
6476
6663
  const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
@@ -7381,6 +7568,17 @@ function routeEdges(style) {
7381
7568
  };
7382
7569
  const start$1 = fixedSideFeedback ? getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback$1, input.direction, input) : sourceFallback$1;
7383
7570
  const end$1 = fixedSideFeedback ? getPortPoint(target, edge.targetPort, targetRect, targetFallback$1, input.direction, input) : targetFallback$1;
7571
+ const [forwardSide, backwardSide] = {
7572
+ right: ["EAST", "WEST"],
7573
+ left: ["WEST", "EAST"],
7574
+ down: ["SOUTH", "NORTH"],
7575
+ up: ["NORTH", "SOUTH"]
7576
+ }[input.direction];
7577
+ const labelPortSides = labelPortSideFeedbackByInput.get(input);
7578
+ const faces = (node, side, away) => !fixedSideFeedback && !labelPortSides?.exempt.has(edge.id) && node.id.startsWith("__layout_dummy:label:") && side === away;
7579
+ const sourceFace = faces(source, feedbackSourcePortSide, backwardSide) ? -1 : 1;
7580
+ const targetFace = faces(target, feedbackTargetPortSide, forwardSide) ? -1 : 1;
7581
+ if (sourceFace < 0 || targetFace < 0) labelPortSides?.routed.add(edge.id);
7384
7582
  if (horizontal$1) {
7385
7583
  const sign = fixedSideFeedback ? Math.sign(sourceRect.x - targetRect.x) || 1 : reverse ? -1 : 1;
7386
7584
  const outerCross = Math.max(...[...placement.rectByNodeId.values()].map((rect) => rect.y + rect.height)) + crossSpacing + (fixedSideFeedback ? .5 : 0);
@@ -7388,11 +7586,11 @@ function routeEdges(style) {
7388
7586
  pointsByEdgeId.set(edge.id, [
7389
7587
  start$1,
7390
7588
  {
7391
- x: start$1.x + sign * flowSpacing,
7589
+ x: start$1.x + sourceFace * sign * flowSpacing,
7392
7590
  y: start$1.y
7393
7591
  },
7394
7592
  {
7395
- x: start$1.x + sign * flowSpacing,
7593
+ x: start$1.x + sourceFace * sign * flowSpacing,
7396
7594
  y: outerCross
7397
7595
  },
7398
7596
  ...preserveCenters ? [{
@@ -7403,11 +7601,11 @@ function routeEdges(style) {
7403
7601
  y: outerCross
7404
7602
  }] : [],
7405
7603
  {
7406
- x: end$1.x - sign * flowSpacing,
7604
+ x: end$1.x - targetFace * sign * flowSpacing,
7407
7605
  y: outerCross
7408
7606
  },
7409
7607
  {
7410
- x: end$1.x - sign * flowSpacing,
7608
+ x: end$1.x - targetFace * sign * flowSpacing,
7411
7609
  y: end$1.y
7412
7610
  },
7413
7611
  end$1
@@ -7420,11 +7618,11 @@ function routeEdges(style) {
7420
7618
  start$1,
7421
7619
  {
7422
7620
  x: start$1.x,
7423
- y: start$1.y + sign * flowSpacing
7621
+ y: start$1.y + sourceFace * sign * flowSpacing
7424
7622
  },
7425
7623
  {
7426
7624
  x: outerCross,
7427
- y: start$1.y + sign * flowSpacing
7625
+ y: start$1.y + sourceFace * sign * flowSpacing
7428
7626
  },
7429
7627
  ...preserveCenters ? [{
7430
7628
  x: outerCross,
@@ -7435,11 +7633,11 @@ function routeEdges(style) {
7435
7633
  }] : [],
7436
7634
  {
7437
7635
  x: outerCross,
7438
- y: end$1.y - sign * flowSpacing
7636
+ y: end$1.y - targetFace * sign * flowSpacing
7439
7637
  },
7440
7638
  {
7441
7639
  x: end$1.x,
7442
- y: end$1.y - sign * flowSpacing
7640
+ y: end$1.y - targetFace * sign * flowSpacing
7443
7641
  },
7444
7642
  end$1
7445
7643
  ]);
@@ -7574,7 +7772,7 @@ function routeEdges(style) {
7574
7772
  outsideFeedbackEdgeIds.add(edge.id);
7575
7773
  continue;
7576
7774
  }
7577
- const movable = hasMovableLoopPorts(input, edge) && hasPlacementLoopEnvelope(placement, source.id) && input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true;
7775
+ const movable = hasMovableLoopPorts(input, edge) && hasPlacementLoopEnvelope(placement, source.id);
7578
7776
  const loopRect = movable ? {
7579
7777
  x: 0,
7580
7778
  y: 0,
@@ -7614,7 +7812,32 @@ function routeEdges(style) {
7614
7812
  startRatio = (countOnSide + nestingIndex + 1) / denominator;
7615
7813
  endRatio = (countOnSide - nestingIndex) / denominator;
7616
7814
  }
7617
- const distance = loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering, movable);
7815
+ const sideLoops = loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side);
7816
+ if (movable) {
7817
+ const track$1 = movableLoopTrack(input, sideLoops, edge, side, ordering);
7818
+ const sign = side === "NORTH" ? -1 : 1;
7819
+ const base = side === "NORTH" ? 0 : loopRect.height;
7820
+ const [first, last] = [startRatio, endRatio].map((ratio) => movableLoopPortOffset(input, sideLoops, side, loopRect.width, ratio));
7821
+ return [
7822
+ {
7823
+ x: first.along,
7824
+ y: base + sign * first.normal
7825
+ },
7826
+ {
7827
+ x: first.along,
7828
+ y: base + sign * track$1
7829
+ },
7830
+ {
7831
+ x: last.along,
7832
+ y: base + sign * track$1
7833
+ },
7834
+ {
7835
+ x: last.along,
7836
+ y: base + sign * last.normal
7837
+ }
7838
+ ];
7839
+ }
7840
+ const distance = loopDistance(sideLoops, edge, side, ordering);
7618
7841
  const y = side === "NORTH" ? loopRect.y - distance : loopRect.y + loopRect.height + distance;
7619
7842
  const start$2 = {
7620
7843
  x: loopRect.x + loopRect.width * startRatio,
@@ -7644,7 +7867,32 @@ function routeEdges(style) {
7644
7867
  const secondRatio = sequenced ? 1 / denominator : 2 / denominator;
7645
7868
  const startRatio = side === "EAST" ? firstRatio : secondRatio;
7646
7869
  const endRatio = side === "EAST" ? secondRatio : firstRatio;
7647
- const distance = movable || input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true ? loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering, movable) : spacing * (indexOnSide + 1);
7870
+ const sideLoops = loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side);
7871
+ if (movable) {
7872
+ const track$1 = movableLoopTrack(input, sideLoops, edge, side, ordering);
7873
+ const sign = side === "WEST" ? -1 : 1;
7874
+ const base = side === "WEST" ? 0 : loopRect.width;
7875
+ const [first, last] = [startRatio, endRatio].map((ratio) => movableLoopPortOffset(input, sideLoops, side, loopRect.height, ratio));
7876
+ return [
7877
+ {
7878
+ x: base + sign * first.normal,
7879
+ y: first.along
7880
+ },
7881
+ {
7882
+ x: base + sign * track$1,
7883
+ y: first.along
7884
+ },
7885
+ {
7886
+ x: base + sign * track$1,
7887
+ y: last.along
7888
+ },
7889
+ {
7890
+ x: base + sign * last.normal,
7891
+ y: last.along
7892
+ }
7893
+ ];
7894
+ }
7895
+ const distance = input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true ? loopDistance(sideLoops, edge, side, ordering) : spacing * (indexOnSide + 1);
7648
7896
  const x = side === "EAST" ? loopRect.x + loopRect.width + distance : loopRect.x - distance;
7649
7897
  const start$2 = {
7650
7898
  x: side === "EAST" ? loopRect.x + loopRect.width : loopRect.x,
@@ -7924,6 +8172,55 @@ function routeEdges(style) {
7924
8172
  const routeEdgesOrthogonally = routeEdges("ORTHOGONAL");
7925
8173
  const routeEdgesWithPolylines = routeEdges("POLYLINE");
7926
8174
  const routeEdgesWithSplines = routeEdges("SPLINES");
8175
+ const sweptPortRanks = /* @__PURE__ */ new WeakMap();
8176
+ /**
8177
+ * Order movable ports along each physical side as crossing minimization left
8178
+ * them, as ELK places ports from its swept port lists. The lists are
8179
+ * clockwise per canonical side.
8180
+ */
8181
+ function recordSweptPortOrder(graph, direction, order) {
8182
+ const connected = new Set(graph.edges.flatMap((edge) => [JSON.stringify([edge.sourceId, edge.sourcePort]), JSON.stringify([edge.targetId, edge.targetPort])]));
8183
+ const vertical$1 = direction === "down" || direction === "up";
8184
+ const reverse = direction === "up" || direction === "left";
8185
+ const canonical = direction === "right" ? {
8186
+ NORTH: "NORTH",
8187
+ EAST: "EAST",
8188
+ SOUTH: "SOUTH",
8189
+ WEST: "WEST"
8190
+ } : direction === "left" ? {
8191
+ NORTH: "NORTH",
8192
+ EAST: "WEST",
8193
+ SOUTH: "SOUTH",
8194
+ WEST: "EAST"
8195
+ } : direction === "down" ? {
8196
+ NORTH: "WEST",
8197
+ EAST: "SOUTH",
8198
+ SOUTH: "EAST",
8199
+ WEST: "NORTH"
8200
+ } : {
8201
+ NORTH: "EAST",
8202
+ EAST: "SOUTH",
8203
+ SOUTH: "WEST",
8204
+ WEST: "NORTH"
8205
+ };
8206
+ for (const node of graph.nodes) {
8207
+ if (!node.ports) continue;
8208
+ const swept = order.physicalPortOrderByNodeId?.get(node.id);
8209
+ if (!swept) {
8210
+ sweptPortRanks.delete(node.ports);
8211
+ continue;
8212
+ }
8213
+ const index = new Map(swept.flatMap((id, i) => connected.has(id) ? [[id, i]] : []));
8214
+ const rankByPort = new Map(node.ports.map((port) => [port, index.get(JSON.stringify([node.id, port.name]))]));
8215
+ sweptPortRanks.set(node.ports, (port, side) => {
8216
+ const rank = rankByPort.get(port);
8217
+ if (rank === void 0) return void 0;
8218
+ const alongFlow = (side === "NORTH" || side === "SOUTH") !== vertical$1;
8219
+ const c$1 = canonical[side];
8220
+ return (alongFlow ? (c$1 === "NORTH" ? 1 : -1) * (reverse ? -1 : 1) : c$1 === "EAST" ? 1 : -1) * rank;
8221
+ });
8222
+ }
8223
+ }
7927
8224
  function placePorts(ports, rect, direction, portSettings, nodeSettings, portDirection) {
7928
8225
  if (!ports) return void 0;
7929
8226
  const horizontal$1 = direction === "left" || direction === "right";
@@ -7950,9 +8247,12 @@ function placePorts(ports, rect, direction, portSettings, nodeSettings, portDire
7950
8247
  group.push(port);
7951
8248
  grouped.set(side, group);
7952
8249
  }
8250
+ const swept = constraints === "FIXED_ORDER" || constraints === "FIXED_POS" || constraints === "FIXED_RATIO" ? void 0 : sweptPortRanks.get(ports);
7953
8251
  for (const group of grouped.values()) {
7954
- group.sort((left, right) => Number(portSettings?.(left)?.["port.index"] ?? ports.indexOf(left)) - Number(portSettings?.(right)?.["port.index"] ?? ports.indexOf(right)));
7955
8252
  const side = sideByPort.get(group[0]);
8253
+ const ranks = swept && group.map((port) => swept(port, side));
8254
+ const ranked = ranks?.every((rank) => rank !== void 0) && group.every((port) => portSettings?.(port)?.["port.index"] === void 0) ? new Map(group.map((port, i) => [port, ranks[i]])) : void 0;
8255
+ group.sort((left, right) => ranked ? ranked.get(left) - ranked.get(right) : Number(portSettings?.(left)?.["port.index"] ?? ports.indexOf(left)) - Number(portSettings?.(right)?.["port.index"] ?? ports.indexOf(right)));
7956
8256
  if (constraints === "FIXED_ORDER" && (side === "SOUTH" || side === "WEST")) group.reverse();
7957
8257
  }
7958
8258
  return ports.map((port) => {
@@ -12767,13 +13067,7 @@ function runPartialLayout(input, options, context, arrange, phase = "selection")
12767
13067
  if (routeAllowed) {
12768
13068
  if (options.settings?.edgeRouting === "SPLINES" || options.strategies?.routeEdges) throw new UnsupportedLayoutError("Partial layout supports its built-in orthogonal router only");
12769
13069
  const ancestorIds = new Set([...ancestors(source.id), ...ancestors(target.id)]);
12770
- const nodeObstacles = graph.nodes.filter((n) => !ancestorIds.has(n.id)).map((n) => n.id === source.id || n.id === target.id ? n : {
12771
- ...n,
12772
- x: n.x - 1,
12773
- y: n.y - 1,
12774
- width: n.width + 2,
12775
- height: n.height + 2
12776
- });
13070
+ const nodeObstacles = graph.nodes.filter((n) => !ancestorIds.has(n.id));
12777
13071
  const labelObstacles = [...edgeById.values()].filter((e) => e.id !== edge.id && e.width > 0 && e.height > 0);
12778
13072
  const required = waypoints.filter((c$1) => (c$1.strength ?? "required") === "required");
12779
13073
  const accepted = new Set(required.map((c$1) => c$1.id));
@@ -13803,7 +14097,7 @@ function switchCenterLabelDummies(expansion, labels, order) {
13803
14097
  //#endregion
13804
14098
  //#region src/layered/north-south-ports.ts
13805
14099
  /** Detach fixed cross-axis ports into same-layer units before crossing minimization. */
13806
- function insertNorthSouthPortDummies(expansion) {
14100
+ function insertNorthSouthPortDummies(expansion, sortedPorts) {
13807
14101
  const { input, orientation } = expansion;
13808
14102
  const vertical$1 = input.direction === "down" || input.direction === "up";
13809
14103
  const forward = input.direction === "right" ? "EAST" : input.direction === "left" ? "WEST" : input.direction === "down" ? "SOUTH" : "NORTH";
@@ -13914,13 +14208,16 @@ function insertNorthSouthPortDummies(expansion) {
13914
14208
  for (const node of nodes) if (origins.has(node.id)) node.ports?.sort((a, b) => Number(a.name === "output") - Number(b.name === "output"));
13915
14209
  const roleOrder = (origin) => origin.input && origin.output ? 2 : origin.input ? 0 : 1;
13916
14210
  const negativeFlow = input.direction === "left" || input.direction === "up";
14211
+ const modelOrder = (input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE";
13917
14212
  const orderedOrigins = input.graph.nodes.flatMap((node) => {
13918
14213
  const entries = [...origins].filter(([, origin]) => origin.node.id === node.id);
13919
14214
  const constraints = input.nodeSettings?.(node)?.portConstraints;
14215
+ const sorted = sortedPorts?.get(node.id);
13920
14216
  const coordinate = (origin) => {
13921
- if (constraints === "FIXED_POS" || constraints === "FIXED_RATIO") return (vertical$1 ? origin.port.y ?? 0 : origin.port.x ?? 0) * (negativeFlow ? -1 : 1);
13922
- const index = node.ports?.indexOf(origin.port) ?? 0;
13923
- return origin.beforeOwner ? index : -index;
14217
+ const flip = modelOrder && origin.output && !origin.input ? -1 : 1;
14218
+ if (constraints === "FIXED_POS" || constraints === "FIXED_RATIO") return (vertical$1 ? origin.port.y ?? 0 : origin.port.x ?? 0) * (negativeFlow ? -1 : 1) * flip;
14219
+ const index = sorted ? sorted.indexOf(JSON.stringify([node.id, origin.port.name])) : node.ports?.indexOf(origin.port) ?? 0;
14220
+ return (origin.beforeOwner ? index : -index) * flip;
13924
14221
  };
13925
14222
  return entries.sort(([, a], [, b]) => Number(!a.beforeOwner) - Number(!b.beforeOwner) || roleOrder(a) - roleOrder(b) || coordinate(a) - coordinate(b));
13926
14223
  });
@@ -16235,9 +16532,10 @@ function opposed(a, b) {
16235
16532
  * A reversed feedback edge split by label or inverted-port dummies is routed
16236
16533
  * one segment at a time, so each segment detours to the outer feedback track
16237
16534
  * and back to the next dummy. Rejoin the chain as one track: the label's own
16238
- * track, or the outer track with the label moved onto it, whichever is clear.
16535
+ * track, or the outer track with the label moved onto it, whichever is clear;
16536
+ * failing both, a fresh track past everything else with the label on it.
16239
16537
  */
16240
- function straightenFeedbackChains(chains, pointsByEdgeId, rectByNodeId, horizontal$1) {
16538
+ function straightenFeedbackChains(chains, pointsByEdgeId, rectByNodeId, horizontal$1, spacing) {
16241
16539
  const cross = (p) => horizontal$1 ? p.y : p.x;
16242
16540
  const flow = (p) => horizontal$1 ? p.x : p.y;
16243
16541
  const at = (f, c$1) => horizontal$1 ? {
@@ -16256,7 +16554,12 @@ function straightenFeedbackChains(chains, pointsByEdgeId, rectByNodeId, horizont
16256
16554
  const tail = points.at(-2), end = points.at(-1);
16257
16555
  const outer = Math.max(...points.map(cross));
16258
16556
  const labelRect = label && rectByNodeId.get(label.id);
16259
- for (const track of label ? [label.cross, outer] : [outer]) {
16557
+ const tracks = label ? [label.cross, outer] : [outer];
16558
+ if (labelRect) {
16559
+ const farthest = Math.max(...[...pointsByEdgeId.values()].flatMap((other) => other.map(cross)), ...[...rectByNodeId.values()].map((rect) => horizontal$1 ? rect.y + rect.height : rect.x + rect.width));
16560
+ tracks.push(farthest + spacing + label.cross - (horizontal$1 ? labelRect.y : labelRect.x));
16561
+ }
16562
+ for (const track of tracks) {
16260
16563
  const route = [
16261
16564
  start,
16262
16565
  lead,
@@ -16285,7 +16588,7 @@ function straightenFeedbackChains(chains, pointsByEdgeId, rectByNodeId, horizont
16285
16588
  }
16286
16589
  if (simple.at(-1) !== end) continue;
16287
16590
  if (simple.length >= points.length) break;
16288
- if ([...pointsByEdgeId].some(([id, other]) => id !== edgeId && opposed(simple, other))) continue;
16591
+ if ([...pointsByEdgeId].some(([id, other]) => id !== edgeId && opposed(simple, other) && !opposed(points, other))) continue;
16289
16592
  pointsByEdgeId.set(edgeId, simple);
16290
16593
  if (moved) rectByNodeId.set(label.id, moved);
16291
16594
  break;
@@ -18654,53 +18957,100 @@ function runSeparatedComponents(graph, options, context) {
18654
18957
  bottom: options.padding?.bottom ?? 12,
18655
18958
  left: options.padding?.left ?? 12
18656
18959
  };
18960
+ const direction = options.direction ?? graph.direction ?? "right";
18961
+ const drawnRects = (result) => {
18962
+ const rects = [];
18963
+ for (const node of result.nodes) {
18964
+ const x$1 = node.x ?? 0, y$1 = node.y ?? 0;
18965
+ rects.push({
18966
+ x: x$1,
18967
+ y: y$1,
18968
+ width: node.width ?? 0,
18969
+ height: node.height ?? 0
18970
+ });
18971
+ for (const port of node.ports ?? []) rects.push({
18972
+ x: x$1 + (port.x ?? 0),
18973
+ y: y$1 + (port.y ?? 0),
18974
+ width: port.width ?? 0,
18975
+ height: port.height ?? 0
18976
+ });
18977
+ }
18978
+ for (const edge of result.edges) {
18979
+ const points = edge.points ?? [];
18980
+ for (let index = 1; index < points.length; index++) {
18981
+ const from = points[index - 1], to = points[index];
18982
+ const x$1 = Math.min(from.x, to.x), y$1 = Math.min(from.y, to.y);
18983
+ rects.push({
18984
+ x: x$1,
18985
+ y: y$1,
18986
+ width: Math.max(from.x, to.x) - x$1,
18987
+ height: Math.max(from.y, to.y) - y$1
18988
+ });
18989
+ }
18990
+ if ((edge.width ?? 0) > 0 || (edge.height ?? 0) > 0) rects.push({
18991
+ x: edge.x ?? 0,
18992
+ y: edge.y ?? 0,
18993
+ width: edge.width ?? 0,
18994
+ height: edge.height ?? 0
18995
+ });
18996
+ }
18997
+ return rects;
18998
+ };
18999
+ const rows = [];
18657
19000
  let x = 0;
19001
+ for (const component of components) {
19002
+ if (x > 0 && x + component.bounds.width > maxRowWidth) x = 0;
19003
+ if (x === 0) rows.push([]);
19004
+ rows.at(-1).push({
19005
+ component,
19006
+ x
19007
+ });
19008
+ x += component.bounds.width + componentSpacing;
19009
+ }
18658
19010
  let y = 0;
18659
- let rowHeight = 0;
18660
19011
  const compactConnectedComponents = options.settings?.["compaction.connectedComponents"] === true;
18661
19012
  const placedShapes = [];
18662
19013
  const nodeResults = /* @__PURE__ */ new Map();
18663
19014
  const edgeResults = /* @__PURE__ */ new Map();
18664
19015
  const placements = [];
18665
- for (const component of components) {
18666
- if (x > 0 && x + component.bounds.width > maxRowWidth) {
18667
- x = 0;
18668
- y += rowHeight + componentSpacing;
18669
- rowHeight = 0;
18670
- }
18671
- let compactedY = y;
18672
- if (compactConnectedComponents && y > 0) {
18673
- let requiredY = 0;
18674
- for (const node of component.result.nodes) {
18675
- const localLeft = x + (node.x ?? 0) - component.bounds.left;
18676
- const localRight = localLeft + (node.width ?? 0);
18677
- const localTop = (node.y ?? 0) - component.bounds.top;
18678
- for (const placed of placedShapes) {
18679
- if (localRight <= placed.left || localLeft >= placed.right) continue;
18680
- requiredY = Math.max(requiredY, placed.bottom + componentSpacing - localTop);
19016
+ for (const row of rows) {
19017
+ const rowWidth = Math.max(...row.map((entry) => entry.x + entry.component.bounds.width));
19018
+ let rowHeight = 0;
19019
+ for (const { component, x: packedX } of row) {
19020
+ const { bounds } = component;
19021
+ const left = direction === "left" ? rowWidth - packedX - bounds.width : packedX;
19022
+ const shapes = drawnRects(component.result).map((rect) => ({
19023
+ left: left + rect.x - bounds.left,
19024
+ top: direction === "up" ? bounds.top + bounds.height - rect.y - rect.height : rect.y - bounds.top,
19025
+ width: rect.width,
19026
+ height: rect.height
19027
+ }));
19028
+ let compactedY = y;
19029
+ if (compactConnectedComponents && y > 0) {
19030
+ let requiredY = 0;
19031
+ for (const shape of shapes) for (const placed of placedShapes) {
19032
+ if (shape.left + shape.width <= placed.left || shape.left >= placed.right) continue;
19033
+ requiredY = Math.max(requiredY, placed.bottom + componentSpacing - shape.top);
18681
19034
  }
19035
+ compactedY = Math.min(y, requiredY);
18682
19036
  }
18683
- compactedY = Math.min(y, requiredY);
19037
+ for (const shape of shapes) placedShapes.push({
19038
+ left: shape.left,
19039
+ right: shape.left + shape.width,
19040
+ top: compactedY + shape.top,
19041
+ bottom: compactedY + shape.top + shape.height
19042
+ });
19043
+ placements.push({
19044
+ component,
19045
+ x: left,
19046
+ y: compactedY
19047
+ });
19048
+ rowHeight = Math.max(rowHeight, bounds.height);
18684
19049
  }
18685
- for (const node of component.result.nodes) placedShapes.push({
18686
- left: x + (node.x ?? 0) - component.bounds.left,
18687
- right: x + (node.x ?? 0) - component.bounds.left + (node.width ?? 0),
18688
- top: compactedY + (node.y ?? 0) - component.bounds.top,
18689
- bottom: compactedY + (node.y ?? 0) - component.bounds.top + (node.height ?? 0)
18690
- });
18691
- placements.push({
18692
- component,
18693
- x,
18694
- y: compactedY
18695
- });
18696
- x += component.bounds.width + componentSpacing;
18697
- rowHeight = Math.max(rowHeight, component.bounds.height);
19050
+ y += rowHeight + componentSpacing;
18698
19051
  }
18699
- const direction = options.direction ?? graph.direction ?? "right";
18700
- const packedWidth = Math.max(0, ...placements.map((p) => p.x + p.component.bounds.width));
18701
19052
  const packedHeight = Math.max(0, ...placements.map((p) => p.y + p.component.bounds.height));
18702
- for (const { component, x: packedX, y: packedY } of placements) {
18703
- const left = direction === "left" ? packedWidth - packedX - component.bounds.width : packedX;
19053
+ for (const { component, x: left, y: packedY } of placements) {
18704
19054
  const top = direction === "up" ? packedHeight - packedY - component.bounds.height : packedY;
18705
19055
  const dx = padding.left + left - component.bounds.left;
18706
19056
  const dy = padding.top + top - component.bounds.top;
@@ -18751,7 +19101,11 @@ function runLayeredPipeline(graph, options, context) {
18751
19101
  * coordinator can prepare every scope, share sweep state, then resume each
18752
19102
  * scope with its final order. The ordinary entry point drains this same pipeline.
18753
19103
  */
18754
- function* createLayeredScopePipeline(graph, options, context) {
19104
+ function* createLayeredScopePipeline(graph, options, context, fallbacks = {
19105
+ antiparallelPairs: /* @__PURE__ */ new Set(),
19106
+ feedbackSegments: /* @__PURE__ */ new Set()
19107
+ }) {
19108
+ const scopeOptions = options;
18755
19109
  if ((options.settings?.["spacing.edgeEdge"] ?? 10) < 2) options = {
18756
19110
  ...options,
18757
19111
  settings: {
@@ -18801,6 +19155,8 @@ function* createLayeredScopePipeline(graph, options, context) {
18801
19155
  const separated$1 = runSeparatedComponents(graph, options, context);
18802
19156
  if (separated$1) return separated$1;
18803
19157
  }
19158
+ const hiddenLoopPorts$1 = hideSelfLoopPorts(graph, options);
19159
+ if (hiddenLoopPorts$1) return restoreSelfLoopPorts(graph, yield* createLayeredScopePipeline(hiddenLoopPorts$1.graph, hiddenLoopPorts$1.options, context, fallbacks), hiddenLoopPorts$1.edges);
18804
19160
  const direction = options.direction ?? graph.direction ?? "right";
18805
19161
  const padding = typeof options.padding === "number" ? {
18806
19162
  top: options.padding,
@@ -18989,7 +19345,7 @@ function* createLayeredScopePipeline(graph, options, context) {
18989
19345
  }
18990
19346
  };
18991
19347
  }
18992
- const northSouth = insertNorthSouthPortDummies(expanded);
19348
+ const northSouth = insertNorthSouthPortDummies(expanded, preparedPorts);
18993
19349
  expanded = northSouth.expansion;
18994
19350
  if (prepareModelOrder) markInitialModelOrderPrepared(expanded.input, preparedPorts);
18995
19351
  const associates = /* @__PURE__ */ new Map(), unitMembers = /* @__PURE__ */ new Map();
@@ -19051,6 +19407,8 @@ function* createLayeredScopePipeline(graph, options, context) {
19051
19407
  minimize: minimizeScope,
19052
19408
  finishOrder: finishCrossingOrder
19053
19409
  };
19410
+ const crossingOrder = order;
19411
+ recordSweptPortOrder(graph, direction, order);
19054
19412
  traceLayeredPhase(() => ({
19055
19413
  kind: "crossing-order",
19056
19414
  scope: graph.id,
@@ -19295,6 +19653,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19295
19653
  const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
19296
19654
  const edgeRouter = options.strategies?.routeEdges ?? (edgeRouting === "POLYLINE" ? routeEdgesWithPolylines : edgeRouting === "SPLINES" ? routeEdgesWithSplines : routeEdgesOrthogonally);
19297
19655
  if (options.strategies?.routeEdges) measure("post-compaction", () => applyPostCompaction(expanded.input, placement));
19656
+ const labelPortSideSegments = trackLabelPortSideFeedback(expanded.input, fallbacks.feedbackSegments);
19298
19657
  let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
19299
19658
  const restoredJunctionCounts = /* @__PURE__ */ new Map();
19300
19659
  expandedRoutes = restoreNorthSouthPortRoutes({
@@ -19342,6 +19701,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19342
19701
  const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
19343
19702
  const parallelLabelPositions = /* @__PURE__ */ new Map();
19344
19703
  const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
19704
+ const flowOrderedPairs = /* @__PURE__ */ new Map();
19345
19705
  const horizontalAntiparallelFlow = direction === "left" || direction === "right";
19346
19706
  if (options.strategies?.routeEdges === void 0 && options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" && options.settings?.["layering.strategy"] === "INTERACTIVE" && options.settings?.["crossingMinimization.forceNodeModelOrder"] === true && options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" && options.settings?.["nodePlacement.favorStraightEdges"] === true && options.settings?.["compaction.postCompaction.strategy"] === "LEFT" && options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE") {
19347
19707
  const neighbors = new Map(graph.nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
@@ -19411,6 +19771,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19411
19771
  const horizontal$1 = horizontalAntiparallelFlow;
19412
19772
  const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
19413
19773
  const pairEdges = /* @__PURE__ */ new Map();
19774
+ const flowOrder = direction === "left" || direction === "up";
19414
19775
  for (const edge of graph.edges) {
19415
19776
  if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) continue;
19416
19777
  const key = [edge.sourceId, edge.targetId].sort().join("\0");
@@ -19418,7 +19779,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19418
19779
  pair.push(edge);
19419
19780
  pairEdges.set(key, pair);
19420
19781
  }
19421
- for (const pair of pairEdges.values()) {
19782
+ for (const [pairKey, pair] of pairEdges) {
19422
19783
  if (pair.length !== 2 || pair[0].sourceId !== pair[1].targetId || pair[0].targetId !== pair[1].sourceId || pair.some((edge) => {
19423
19784
  const settings = options.edgeSettings?.(edge);
19424
19785
  return (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" || settings?.["edgeLabels.inline"] !== true;
@@ -19436,11 +19797,13 @@ function* createLayeredScopePipeline(graph, options, context) {
19436
19797
  };
19437
19798
  const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
19438
19799
  if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
19439
- const firstBeforeSecond = horizontal$1 ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
19800
+ const pairFlowOrder = flowOrder && !fallbacks.antiparallelPairs.has(pairKey);
19801
+ const firstBeforeSecond = (horizontal$1 ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2) !== pairFlowOrder;
19440
19802
  const beforeRect = firstBeforeSecond ? firstRect : secondRect;
19441
19803
  const afterRect = firstBeforeSecond ? secondRect : firstRect;
19442
19804
  const forward = pair.find((edge) => firstBeforeSecond ? edge.sourceId === pair[0].sourceId : edge.sourceId === pair[0].targetId);
19443
19805
  if (!forward?.sourcePort || !forward.targetPort) continue;
19806
+ if (pairFlowOrder) flowOrderedPairs.set(pairKey, [firstNode.id, secondNode.id]);
19444
19807
  let cross = Math.min(horizontal$1 ? firstRect.y : firstRect.x, horizontal$1 ? secondRect.y : secondRect.x);
19445
19808
  const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
19446
19809
  for (const edge of pair) {
@@ -19549,7 +19912,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19549
19912
  cross: horizontal$1 ? anchor.y : anchor.x
19550
19913
  }
19551
19914
  }];
19552
- }), routes.pointsByEdgeId, placement.rectByNodeId, horizontal$1);
19915
+ }), routes.pointsByEdgeId, placement.rectByNodeId, horizontal$1, Number(options.settings?.["spacing.edgeEdge"] ?? 10));
19553
19916
  }
19554
19917
  if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
19555
19918
  const horizontal$1 = direction === "right" || direction === "left";
@@ -19651,8 +20014,11 @@ function* createLayeredScopePipeline(graph, options, context) {
19651
20014
  }
19652
20015
  }
19653
20016
  if ((options.settings?.["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING" && !unzippingFanIn && edgeRouting === "ORTHOGONAL") routes = { pointsByEdgeId: adjustUnzippedSinkRoutes(graph, routes.pointsByEdgeId, direction, Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10), Number(options.settings?.["spacing.edgeEdgeBetweenLayers"] ?? 10)) };
20017
+ const connectsOnlyTo = (nodeId, otherId) => graph.edges.every((edge) => edge.sourceId === edge.targetId || edge.sourceId !== nodeId && edge.targetId !== nodeId || edge.sourceId === otherId || edge.targetId === otherId);
19654
20018
  for (const pair of labeledEdgesByPair.values()) {
19655
20019
  if (pair.length < 2) continue;
20020
+ const { sourceId, targetId } = pair[0];
20021
+ if (!connectsOnlyTo(sourceId, targetId) || !connectsOnlyTo(targetId, sourceId)) continue;
19656
20022
  const sourceRect = mutableRects.get(pair[0].sourceId);
19657
20023
  const targetRect = mutableRects.get(pair[0].targetId);
19658
20024
  if (!sourceRect || !targetRect) continue;
@@ -19872,7 +20238,7 @@ function* createLayeredScopePipeline(graph, options, context) {
19872
20238
  y: (points[1].y + points[2].y - height) / 2
19873
20239
  } : void 0;
19874
20240
  const movableExteriorLoopLabelPosition = (() => {
19875
- if (edgeRouting !== "ORTHOGONAL" || labelPlacement !== "CENTER" || inlineLabel || !hasMovableLoopPorts(input, edge) || !sourceRect || points.length !== 4) return void 0;
20241
+ if (edgeRouting !== "ORTHOGONAL" || labelPlacement !== "CENTER" || !hasMovableLoopPorts(input, edge) || !sourceRect || points.length !== 4) return void 0;
19876
20242
  const first = points[1], last = points[2];
19877
20243
  if (first.y === last.y) {
19878
20244
  const low = first.y < sourceRect.y;
@@ -19950,6 +20316,28 @@ function* createLayeredScopePipeline(graph, options, context) {
19950
20316
  };
19951
20317
  }
19952
20318
  });
20319
+ if (edgeRouting === "ORTHOGONAL") {
20320
+ const diagonalEdges = edges.filter((edge) => countDiagonals(edge.points) > 0);
20321
+ const antiparallelPairs = [...flowOrderedPairs].flatMap(([key, nodeIds]) => diagonalEdges.some((edge) => nodeIds.includes(edge.sourceId) || nodeIds.includes(edge.targetId)) ? [key] : []);
20322
+ const feedbackSegments = diagonalEdges.flatMap((edge) => {
20323
+ const segments = expanded.segmentIdsByEdgeId.get(edge.id) ?? [edge.id];
20324
+ return segments.some((id) => labelPortSideSegments.has(id)) ? segments : [];
20325
+ });
20326
+ if (antiparallelPairs.length > 0 || feedbackSegments.length > 0) {
20327
+ const retry = createLayeredScopePipeline(graph, scopeOptions, context && {
20328
+ ...context,
20329
+ diagnostics: []
20330
+ }, {
20331
+ antiparallelPairs: new Set([...fallbacks.antiparallelPairs, ...antiparallelPairs]),
20332
+ feedbackSegments: new Set([...fallbacks.feedbackSegments, ...feedbackSegments])
20333
+ });
20334
+ const prepared = retry.next();
20335
+ const resumed = prepared.done ? prepared : retry.next(crossingOrder);
20336
+ if (!resumed.done) throw new Error("Layered scope yielded more than one crossing phase");
20337
+ const diagonals = (routed) => routed.reduce((sum, edge) => sum + countDiagonals(edge.points), 0);
20338
+ if (diagonals(resumed.value.edges) < diagonals(edges)) return resumed.value;
20339
+ }
20340
+ }
19953
20341
  return setElkjs0111ResultPolicy({
19954
20342
  ...graph,
19955
20343
  direction,
@@ -19963,6 +20351,15 @@ function* createLayeredScopePipeline(graph, options, context) {
19963
20351
  junctionPointsByEdgeId: routes.junctionPointsByEdgeId
19964
20352
  });
19965
20353
  }
20354
+ function countDiagonals(points) {
20355
+ let count = 0;
20356
+ for (let index = 1; index < (points?.length ?? 0); index++) {
20357
+ const a = points[index - 1];
20358
+ const b = points[index];
20359
+ if (Math.abs(a.x - b.x) > 1e-6 && Math.abs(a.y - b.y) > 1e-6) count++;
20360
+ }
20361
+ return count;
20362
+ }
19966
20363
  /**
19967
20364
  * Deterministic native layered layout for an `@statelyai/graph` graph.
19968
20365
  *