@statelyai/layout 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1608,174 +1608,8 @@ 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
-
1729
1611
  //#endregion
1730
1612
  //#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
- }
1779
1613
  /** Unconstrained implicit loop ports are restored on loop faces after the sweep. */
1780
1614
  function hasMovableLoopPorts(input, edge) {
1781
1615
  if (edge.sourceId !== edge.targetId || edge.sourcePort !== void 0 || edge.targetPort !== void 0) return false;
@@ -1813,7 +1647,7 @@ function loopEnvelopes(input) {
1813
1647
  };
1814
1648
  const horizontal$1 = input.direction === "right" || input.direction === "left";
1815
1649
  const reverse = input.direction === "left" || input.direction === "up";
1816
- const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1650
+ const spacing$1 = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1817
1651
  let supported = true;
1818
1652
  const tracks = selfLoopTracks(allLoops);
1819
1653
  for (const edge of allLoops) {
@@ -1832,7 +1666,7 @@ function loopEnvelopes(input) {
1832
1666
  }, input.direction, input), getPortPoint(node, edge.targetPort, rect, {
1833
1667
  x: 0,
1834
1668
  y: 0
1835
- }, input.direction, input), sourceSide, targetSide, spacing * (tracks.get(edge.id) + 1), input.direction);
1669
+ }, input.direction, input), sourceSide, targetSide, spacing$1 * (tracks.get(edge.id) + 1), input.direction);
1836
1670
  if (labeled) {
1837
1671
  const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
1838
1672
  const { width = 0, height = 0 } = edge;
@@ -1857,10 +1691,11 @@ function loopEnvelopes(input) {
1857
1691
  }
1858
1692
  }
1859
1693
  const loops = allLoops.filter((edge) => hasMovableLoopPorts(input, edge));
1860
- if (loops.length === 0) continue;
1694
+ if (loops.length === 0 || loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) continue;
1861
1695
  const settings = input.nodeSettings?.(node);
1862
1696
  const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
1863
1697
  const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
1698
+ const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
1864
1699
  const envelope = {
1865
1700
  before: 0,
1866
1701
  after: 0,
@@ -1873,16 +1708,12 @@ function loopEnvelopes(input) {
1873
1708
  "flowAfter",
1874
1709
  "flowBefore"
1875
1710
  ] : distribution === "NORTH_SOUTH" ? ["before", "after"] : ["before"];
1876
- const canonicalSides = {
1877
- before: "NORTH",
1878
- after: "SOUTH",
1879
- flowAfter: "EAST",
1880
- flowBefore: "WEST"
1881
- };
1882
1711
  for (const side of sides$1) {
1883
1712
  const selected = loops.filter((_, index) => sides$1[index % sides$1.length] === side);
1884
- const canonical = canonicalSides[side];
1885
- envelope[side] = Math.max(0, ...selected.map((edge) => movableLoopTrack(input, selected, edge, canonical, ordering) + movableLoopLabelExtent(input, edge, canonical)));
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);
1886
1717
  }
1887
1718
  envelopes.set(node.id, envelope);
1888
1719
  }
@@ -3050,7 +2881,7 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3050
2881
  if (point.x >= rect.x + rect.width - 1e-9) return "EAST";
3051
2882
  return point.y <= rect.y ? "NORTH" : "SOUTH";
3052
2883
  };
3053
- const crossSide$1 = (side) => side === "NORTH" || side === "SOUTH";
2884
+ const crossSide = (side) => side === "NORTH" || side === "SOUTH";
3054
2885
  const segments = [];
3055
2886
  for (const [edgeId, points] of canonicalRoutes) {
3056
2887
  const edge = edgeById.get(edgeId);
@@ -3060,8 +2891,8 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3060
2891
  const first = points[index];
3061
2892
  const second = points[index + 1];
3062
2893
  if (Math.abs(first.x - second.x) > 1e-9 || Math.abs(first.y - second.y) < 1e-9) 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;
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;
3065
2896
  const owner = index === 0 ? nodes.get(edge.sourceId) : index === points.length - 2 ? nodes.get(edge.targetId) : void 0;
3066
2897
  const ownerSide = owner ? endpointSide(index === 0 ? edge.sourceId : edge.targetId, index === 0 ? edge.sourcePort : edge.targetPort, index === 0 ? first : second) : void 0;
3067
2898
  const bend = index === 0 ? second : first;
@@ -3224,7 +3055,7 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3224
3055
  const points = canonicalRoutes.get(edge.id);
3225
3056
  const sourceSide = endpointSide(edge.sourceId, edge.sourcePort, points?.[0]);
3226
3057
  const targetSide = endpointSide(edge.targetId, edge.targetPort, points?.at(-1));
3227
- if (crossSide$1(sourceSide) && crossSide$1(targetSide)) continue;
3058
+ if (crossSide(sourceSide) && crossSide(targetSide)) continue;
3228
3059
  const reversed = orientation?.reversedEdgeIds.has(edge.id) ?? source.x > target.x;
3229
3060
  const from = reversed ? target : source, to = reversed ? source : target;
3230
3061
  constraints.push({
@@ -3318,8 +3149,8 @@ function compactJoinedGeometryUnchecked(input, placement, routes, orientation) {
3318
3149
  const start = pointToCanonical(originals[0]);
3319
3150
  const next = pointToCanonical(originals[1]);
3320
3151
  const previous = pointToCanonical(originals.at(-2));
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;
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;
3323
3154
  }
3324
3155
  }
3325
3156
  const extent$2 = Math.max(...items.map((item) => item.x + item.width), ...[...canonicalRoutes.values()].flatMap((points) => points.map((point) => point.x)));
@@ -6596,13 +6427,13 @@ function routeEdges(style) {
6596
6427
  "WEST"
6597
6428
  ][index % 4] : distribution === "NORTH_SOUTH" && index % 2 === 1 ? "SOUTH" : "NORTH";
6598
6429
  };
6599
- const loopDistance = (loops, edge, side, ordering) => {
6600
- const axis = side === "NORTH" || side === "SOUTH" ? "height" : "width";
6430
+ const loopDistance = (loops, edge, side, ordering, canonical = false) => {
6431
+ const axis = (side === "NORTH" || side === "SOUTH") === (!canonical || horizontal$1) ? "height" : "width";
6601
6432
  const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
6602
6433
  const index = ordered.indexOf(edge);
6603
6434
  const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
6604
6435
  const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
6605
- const size$1 = (candidate) => input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true ? candidate[axis] ?? 0 : 0;
6436
+ const size$1 = (candidate) => canonical || input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true ? candidate[axis] ?? 0 : 0;
6606
6437
  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);
6607
6438
  };
6608
6439
  const hasFixedLoopPorts = (node) => {
@@ -6657,7 +6488,7 @@ function routeEdges(style) {
6657
6488
  for (const [id, loops] of selfLoopsByNodeId) {
6658
6489
  const rect = mutableRects.get(id);
6659
6490
  const node = nodeById.get(id);
6660
- if (!rect || !node || hasPlacementLoopEnvelope(placement, id)) continue;
6491
+ if (!rect || !node) continue;
6661
6492
  const settings = input.nodeSettings?.(node);
6662
6493
  const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
6663
6494
  const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
@@ -7772,7 +7603,7 @@ function routeEdges(style) {
7772
7603
  outsideFeedbackEdgeIds.add(edge.id);
7773
7604
  continue;
7774
7605
  }
7775
- const movable = hasMovableLoopPorts(input, edge) && hasPlacementLoopEnvelope(placement, source.id);
7606
+ const movable = hasMovableLoopPorts(input, edge) && hasPlacementLoopEnvelope(placement, source.id) && input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true;
7776
7607
  const loopRect = movable ? {
7777
7608
  x: 0,
7778
7609
  y: 0,
@@ -7812,32 +7643,7 @@ function routeEdges(style) {
7812
7643
  startRatio = (countOnSide + nestingIndex + 1) / denominator;
7813
7644
  endRatio = (countOnSide - nestingIndex) / denominator;
7814
7645
  }
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);
7646
+ const distance = loopDistance(loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side), edge, side, ordering, movable);
7841
7647
  const y = side === "NORTH" ? loopRect.y - distance : loopRect.y + loopRect.height + distance;
7842
7648
  const start$2 = {
7843
7649
  x: loopRect.x + loopRect.width * startRatio,
@@ -7867,32 +7673,7 @@ function routeEdges(style) {
7867
7673
  const secondRatio = sequenced ? 1 / denominator : 2 / denominator;
7868
7674
  const startRatio = side === "EAST" ? firstRatio : secondRatio;
7869
7675
  const endRatio = side === "EAST" ? secondRatio : firstRatio;
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);
7676
+ 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);
7896
7677
  const x = side === "EAST" ? loopRect.x + loopRect.width + distance : loopRect.x - distance;
7897
7678
  const start$2 = {
7898
7679
  x: side === "EAST" ? loopRect.x + loopRect.width : loopRect.x,
@@ -19155,8 +18936,6 @@ function* createLayeredScopePipeline(graph, options, context, fallbacks = {
19155
18936
  const separated$1 = runSeparatedComponents(graph, options, context);
19156
18937
  if (separated$1) return separated$1;
19157
18938
  }
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);
19160
18939
  const direction = options.direction ?? graph.direction ?? "right";
19161
18940
  const padding = typeof options.padding === "number" ? {
19162
18941
  top: options.padding,
@@ -20238,7 +20017,7 @@ function* createLayeredScopePipeline(graph, options, context, fallbacks = {
20238
20017
  y: (points[1].y + points[2].y - height) / 2
20239
20018
  } : void 0;
20240
20019
  const movableExteriorLoopLabelPosition = (() => {
20241
- if (edgeRouting !== "ORTHOGONAL" || labelPlacement !== "CENTER" || !hasMovableLoopPorts(input, edge) || !sourceRect || points.length !== 4) return void 0;
20020
+ if (edgeRouting !== "ORTHOGONAL" || labelPlacement !== "CENTER" || inlineLabel || !hasMovableLoopPorts(input, edge) || !sourceRect || points.length !== 4) return void 0;
20242
20021
  const first = points[1], last = points[2];
20243
20022
  if (first.y === last.y) {
20244
20023
  const low = first.y < sourceRect.y;
@@ -1,4 +1,4 @@
1
- import { K as placePorts, X as JavaRandom, i as layeredAlgorithm, n as elkjs0111LayeredAlgorithm, x as setElkjs0111ResultPolicy, z as getPolylineMidpoint } from "./layered-CUwft7on.mjs";
1
+ import { K as placePorts, X as JavaRandom, i as layeredAlgorithm, n as elkjs0111LayeredAlgorithm, x as setElkjs0111ResultPolicy, z as getPolylineMidpoint } from "./layered-CmL7oynw.mjs";
2
2
  import { getNodeSize } from "@statelyai/graph/layout";
3
3
 
4
4
  //#region src/fixed.ts
@@ -1,4 +1,4 @@
1
- const require_layered = require('./layered-QzYtokgN.cjs');
1
+ const require_layered = require('./layered-SB2mlAqt.cjs');
2
2
  let _statelyai_graph_layout = require("@statelyai/graph/layout");
3
3
 
4
4
  //#region src/fixed.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/layout",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Extensible, graph-native layout algorithms for @statelyai/graph",
5
5
  "license": "EPL-2.0 OR GPL-3.0-or-later",
6
6
  "repository": {
@@ -8,7 +8,6 @@ import { countAllCrossings } from "./crossing-counter";
8
8
  import { crossingGraph } from "./crossing-graph";
9
9
  import { sortInitialModelOrder, markInitialModelOrderPrepared } from "./initial-model-order";
10
10
  import { hasMovableLoopPorts } from "./loop-envelopes";
11
- import { hideSelfLoopPorts, restoreSelfLoopPorts } from "./self-loop-ports";
12
11
  import { fixedSelfLoopLabels, fixedSelfLoopSide } from "./fixed-self-loop";
13
12
  import { compactionBounds, placementCrossBounds } from "./compaction-bounds";
14
13
  import { setElkjs0111ResultPolicy } from "../internal/elkjs-compatibility";
@@ -1425,18 +1424,6 @@ export function* createLayeredScopePipeline<N, E, G, P>(
1425
1424
  const separated = runSeparatedComponents(graph, options, context);
1426
1425
  if (separated) return separated;
1427
1426
  }
1428
- const hiddenLoopPorts = hideSelfLoopPorts(graph as Graph<N, E, G, P>, options);
1429
- if (hiddenLoopPorts)
1430
- return restoreSelfLoopPorts(
1431
- graph as Graph<N, E, G, P>,
1432
- yield* createLayeredScopePipeline(
1433
- hiddenLoopPorts.graph,
1434
- hiddenLoopPorts.options,
1435
- context,
1436
- fallbacks,
1437
- ),
1438
- hiddenLoopPorts.edges,
1439
- );
1440
1427
  const direction = options.direction ?? graph.direction ?? "right";
1441
1428
  const padding =
1442
1429
  typeof options.padding === "number"
@@ -3155,6 +3142,7 @@ export function* createLayeredScopePipeline<N, E, G, P>(
3155
3142
  if (
3156
3143
  edgeRouting !== "ORTHOGONAL" ||
3157
3144
  labelPlacement !== "CENTER" ||
3145
+ inlineLabel ||
3158
3146
  !hasMovableLoopPorts(input, edge) ||
3159
3147
  !sourceRect ||
3160
3148
  points.length !== 4
@@ -8,86 +8,6 @@ import {
8
8
  selfLoopTracks,
9
9
  } from "./fixed-self-loop";
10
10
  import { findNode } from "./node-lookup";
11
- import { hiddenLoopPorts } from "./self-loop-ports";
12
-
13
- type CanonicalSide = "NORTH" | "SOUTH" | "EAST" | "WEST";
14
- const crossSide = (side: CanonicalSide) => side === "NORTH" || side === "SOUTH";
15
-
16
- /**
17
- * Extents of a movable loop's hidden ports in the canonical rightward frame:
18
- * along its side, and protruding from it.
19
- */
20
- function loopPortExtents(input: LayeredPhaseInput, edge: GraphEdge, side: CanonicalSide) {
21
- const ports = hiddenLoopPorts(input.settings, edge.id);
22
- const horizontal = input.direction === "right" || input.direction === "left";
23
- const along = (crossSide(side) === horizontal ? "width" : "height") as "width" | "height";
24
- const normal = along === "width" ? "height" : "width";
25
- return {
26
- along: ports ? ports.source[along] + ports.target[along] : 0,
27
- normal: ports ? Math.max(ports.source[normal], ports.target[normal]) : 0,
28
- };
29
- }
30
-
31
- /** A movable loop's label extent away from its side, in the canonical frame. */
32
- export function movableLoopLabelExtent(
33
- input: LayeredPhaseInput,
34
- edge: GraphEdge,
35
- side: CanonicalSide,
36
- ): number {
37
- const horizontal = input.direction === "right" || input.direction === "left";
38
- const size = (crossSide(side) === horizontal ? edge.height : edge.width) ?? 0;
39
- return size > 0 ? size + Number(input.settings["spacing.edgeLabel"] ?? 2) : 0;
40
- }
41
-
42
- /**
43
- * ELK's OrthogonalSelfLoopRouter routing slot of a movable loop, measured
44
- * from its node side in the canonical frame: past the protruding ports and
45
- * the self-loop spacing, then one edge spacing plus any label per inner slot.
46
- */
47
- export function movableLoopTrack(
48
- input: LayeredPhaseInput,
49
- sideLoops: readonly GraphEdge[],
50
- edge: GraphEdge,
51
- side: CanonicalSide,
52
- ordering: string,
53
- ): number {
54
- const ordered = ordering === "REVERSE_STACKED" ? [...sideLoops].reverse() : sideLoops;
55
- const inner = ordering === "SEQUENCED" ? [] : ordered.slice(0, ordered.indexOf(edge));
56
- const edgeSpacing = Number(input.settings["spacing.edgeEdge"] ?? 10);
57
- return (
58
- Math.max(0, ...sideLoops.map((loop) => loopPortExtents(input, loop, side).normal)) +
59
- Number(input.settings["spacing.nodeSelfLoop"] ?? 10) +
60
- inner.reduce(
61
- (sum, loop) =>
62
- sum + edgeSpacing + (crossSide(side) ? movableLoopLabelExtent(input, loop, side) : 0),
63
- 0,
64
- )
65
- );
66
- }
67
-
68
- /**
69
- * Canonical position of a movable loop port along its side, given its
70
- * DISTRIBUTED ratio among the side's implicit ports (ELK's port placement
71
- * with equal gaps between hidden ports of their own size).
72
- */
73
- export function movableLoopPortOffset(
74
- input: LayeredPhaseInput,
75
- sideLoops: readonly GraphEdge[],
76
- side: CanonicalSide,
77
- length: number,
78
- ratio: number,
79
- ): { along: number; normal: number } {
80
- const count = sideLoops.length * 2;
81
- const extents = sideLoops.map((loop) => loopPortExtents(input, loop, side));
82
- const total = extents.reduce((sum, extent) => sum + extent.along, 0);
83
- const size = total / count;
84
- const index = Math.round(ratio * (count + 1)) - 1;
85
- const gap = (length - total) / (count + 1);
86
- return {
87
- along: gap * (index + 1) + size * index + size / 2,
88
- normal: Math.max(0, ...extents.map((extent) => extent.normal)),
89
- };
90
- }
91
11
 
92
12
  export interface LoopEnvelope {
93
13
  before: number;
@@ -205,10 +125,18 @@ export function loopEnvelopes(input: LayeredPhaseInput): ReadonlyMap<string, Loo
205
125
  }
206
126
  }
207
127
  const loops = allLoops.filter((edge) => hasMovableLoopPorts(input, edge));
208
- if (loops.length === 0) continue;
128
+ // Inline labels remain owned by the existing labeled-loop phase. Move
129
+ // the entire node together when that phase is ported, rather than mixing
130
+ // two different loop distributions and reservations on the same node.
131
+ if (
132
+ loops.length === 0 ||
133
+ loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)
134
+ )
135
+ continue;
209
136
  const settings = input.nodeSettings?.(node);
210
137
  const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
211
138
  const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
139
+ const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
212
140
  const envelope = { before: 0, after: 0, flowBefore: 0, flowAfter: 0 };
213
141
  const sides =
214
142
  distribution === "EQUALLY"
@@ -216,23 +144,25 @@ export function loopEnvelopes(input: LayeredPhaseInput): ReadonlyMap<string, Loo
216
144
  : distribution === "NORTH_SOUTH"
217
145
  ? (["before", "after"] as const)
218
146
  : (["before"] as const);
219
- const canonicalSides = {
220
- before: "NORTH",
221
- after: "SOUTH",
222
- flowAfter: "EAST",
223
- flowBefore: "WEST",
224
- } as const;
225
147
  for (const side of sides) {
226
148
  const selected = loops.filter((_, index) => sides[index % sides.length] === side);
227
- const canonical = canonicalSides[side];
228
- envelope[side] = Math.max(
229
- 0,
230
- ...selected.map(
231
- (edge) =>
232
- movableLoopTrack(input, selected, edge, canonical, ordering) +
233
- movableLoopLabelExtent(input, edge, canonical),
234
- ),
235
- );
149
+ const horizontal = input.direction === "right" || input.direction === "left";
150
+ const crossSize =
151
+ side === "before" || side === "after"
152
+ ? horizontal
153
+ ? "height"
154
+ : "width"
155
+ : horizontal
156
+ ? "width"
157
+ : "height";
158
+ const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
159
+ envelope[side] =
160
+ spacing * (ordering === "SEQUENCED" ? Math.min(1, selected.length) : selected.length) +
161
+ selected.reduce(
162
+ (sum, edge) =>
163
+ sum + ((edge[crossSize] ?? 0) > 0 ? (edge[crossSize] ?? 0) + labelSpacing : 0),
164
+ 0,
165
+ );
236
166
  }
237
167
  envelopes.set(node.id, envelope);
238
168
  }