@statelyai/layout 0.0.2 → 0.0.4

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.
@@ -29,6 +29,7 @@ import {
29
29
  breakCyclesWithModelOrderBreadthFirstSearch,
30
30
  breakCyclesWithDepthFirstSearch,
31
31
  getPolylineMidpoint,
32
+ getOrientedPortDirection,
32
33
  minimizeCrossingsWithBarycenter,
33
34
  minimizeCrossingsWithMedian,
34
35
  minimizeCrossingsInteractively,
@@ -1332,6 +1333,37 @@ function runLayeredPipeline<N, E, G, P>(
1332
1333
  left: options.padding?.left ?? 12,
1333
1334
  };
1334
1335
  const switchedSideByPort = new Map<string, "NORTH" | "SOUTH" | "WEST" | "EAST">();
1336
+ const parallelPortIndexByKey = new Map<string, number>();
1337
+ const labeledEdgesByPair = new Map<string, GraphEdge[]>();
1338
+ for (const edge of graph.edges as GraphEdge[]) {
1339
+ const settings = options.edgeSettings?.(edge);
1340
+ if (
1341
+ edge.sourcePort === undefined ||
1342
+ edge.targetPort === undefined ||
1343
+ edge.sourceId === edge.targetId ||
1344
+ (edge.width ?? 0) <= 0 ||
1345
+ (edge.height ?? 0) <= 0 ||
1346
+ (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
1347
+ settings?.["edgeLabels.inline"] !== true
1348
+ ) {
1349
+ continue;
1350
+ }
1351
+ const key = `${edge.sourceId}\0${edge.targetId}`;
1352
+ const pair = labeledEdgesByPair.get(key) ?? [];
1353
+ pair.push(edge);
1354
+ labeledEdgesByPair.set(key, pair);
1355
+ }
1356
+ for (const pair of labeledEdgesByPair.values()) {
1357
+ if (pair.length < 2) continue;
1358
+ for (const [index, edge] of pair.entries()) {
1359
+ const portIndex =
1360
+ options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
1361
+ ? index
1362
+ : pair.length - index - 1;
1363
+ parallelPortIndexByKey.set(`${edge.sourceId}\0${edge.sourcePort}`, portIndex);
1364
+ parallelPortIndexByKey.set(`${edge.targetId}\0${edge.targetPort}`, portIndex);
1365
+ }
1366
+ }
1335
1367
  const input: LayeredPhaseInput = {
1336
1368
  graph: graph as Graph<unknown, unknown, unknown, unknown>,
1337
1369
  sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
@@ -1352,6 +1384,9 @@ function runLayeredPipeline<N, E, G, P>(
1352
1384
  ...(options.edgeSettings === undefined ? {} : { edgeSettings: options.edgeSettings }),
1353
1385
  portSettings: (port, node) => ({
1354
1386
  ...options.portSettings?.(port, node),
1387
+ ...(parallelPortIndexByKey.has(`${node.id}\0${port.name}`)
1388
+ ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) }
1389
+ : {}),
1355
1390
  ...(switchedSideByPort.has(`${node.id}\0${port.name}`)
1356
1391
  ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) }
1357
1392
  : {}),
@@ -1660,7 +1695,7 @@ function runLayeredPipeline<N, E, G, P>(
1660
1695
  }
1661
1696
  }
1662
1697
  measure("port-margin-normalization", () =>
1663
- normalizePlacementForPortExtents(expanded.input, placement, order),
1698
+ normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation),
1664
1699
  );
1665
1700
  {
1666
1701
  const horizontal = direction === "left" || direction === "right";
@@ -1720,10 +1755,288 @@ function runLayeredPipeline<N, E, G, P>(
1720
1755
  : edgeRouting === "SPLINES"
1721
1756
  ? routeEdgesWithSplines
1722
1757
  : routeEdgesOrthogonally);
1723
- const expandedRoutes = measure("edge-routing", () =>
1758
+ let expandedRoutes = measure("edge-routing", () =>
1724
1759
  edgeRouter(expanded.input, expanded.orientation, placement),
1725
1760
  );
1726
1761
  measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
1762
+ const antiparallelLabelPositions = new Map<string, Point>();
1763
+ const outerAntiparallelLabelIds = new Set<string>();
1764
+ const parallelLabelPositions = new Map<string, Point>();
1765
+ const postCompactionNodeCrossDeltas = new Map<string, number>();
1766
+ const horizontalAntiparallelFlow = direction === "left" || direction === "right";
1767
+ const usesVizInteractivePlacementProfile =
1768
+ options.strategies?.routeEdges === undefined &&
1769
+ options.settings?.["cycleBreaking.strategy"] === "MODEL_ORDER" &&
1770
+ options.settings?.["layering.strategy"] === "INTERACTIVE" &&
1771
+ options.settings?.["crossingMinimization.forceNodeModelOrder"] === true &&
1772
+ options.settings?.["nodePlacement.strategy"] === "BRANDES_KOEPF" &&
1773
+ options.settings?.["nodePlacement.favorStraightEdges"] === true &&
1774
+ options.settings?.["compaction.postCompaction.strategy"] === "LEFT" &&
1775
+ options.settings?.["compaction.postCompaction.constraints"] === "SCANLINE";
1776
+ if (usesVizInteractivePlacementProfile) {
1777
+ const neighbors = new Map(graph.nodes.map((node) => [node.id, new Set<string>()]));
1778
+ for (const edge of graph.edges) {
1779
+ if (edge.sourceId === edge.targetId) continue;
1780
+ neighbors.get(edge.sourceId)?.add(edge.targetId);
1781
+ neighbors.get(edge.targetId)?.add(edge.sourceId);
1782
+ }
1783
+ const visited = new Set<string>();
1784
+ for (const node of graph.nodes) {
1785
+ if (visited.has(node.id)) continue;
1786
+ const component: GraphNode[] = [];
1787
+ const pending = [node.id];
1788
+ while (pending.length > 0) {
1789
+ const id = pending.pop()!;
1790
+ if (visited.has(id)) continue;
1791
+ visited.add(id);
1792
+ const member = graph.nodes.find((candidate) => candidate.id === id);
1793
+ if (member) component.push(member);
1794
+ for (const neighbor of neighbors.get(id) ?? []) pending.push(neighbor);
1795
+ }
1796
+ if (component.length < 2) continue;
1797
+ const componentIds = new Set(component.map((member) => member.id));
1798
+ if (
1799
+ !graph.edges.some(
1800
+ (edge) =>
1801
+ componentIds.has(edge.sourceId) &&
1802
+ componentIds.has(edge.targetId) &&
1803
+ expanded.orientation.reversedEdgeIds.has(edge.id),
1804
+ )
1805
+ ) {
1806
+ continue;
1807
+ }
1808
+ const directedPairCounts = new Map<string, number>();
1809
+ for (const edge of graph.edges) {
1810
+ if (!componentIds.has(edge.sourceId) || !componentIds.has(edge.targetId)) continue;
1811
+ const key = `${edge.sourceId}\0${edge.targetId}`;
1812
+ directedPairCounts.set(key, (directedPairCounts.get(key) ?? 0) + 1);
1813
+ }
1814
+ if ([...directedPairCounts.values()].some((count) => count > 1)) continue;
1815
+ if (
1816
+ component.some((member) => {
1817
+ const constraints = options.nodeSettings?.(member)?.portConstraints;
1818
+ return constraints !== undefined && constraints !== "UNDEFINED" && constraints !== "FREE";
1819
+ })
1820
+ ) {
1821
+ continue;
1822
+ }
1823
+ const layerCounts = new Map<number, number>();
1824
+ for (const member of component) {
1825
+ const layer = expanded.assignment.layerByNodeId.get(member.id);
1826
+ if (layer === undefined) continue;
1827
+ layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
1828
+ }
1829
+ if ([...layerCounts.values()].some((count) => count !== 1)) continue;
1830
+ const rects = component.flatMap((member) => {
1831
+ const rect = mutableRects.get(member.id);
1832
+ return rect ? [{ member, rect }] : [];
1833
+ });
1834
+ if (rects.length !== component.length) continue;
1835
+ const minimumCross = Math.min(
1836
+ ...rects.map(({ rect }) => (horizontalAntiparallelFlow ? rect.y : rect.x)),
1837
+ );
1838
+ const maximumCross = Math.max(
1839
+ ...rects.map(({ rect }) =>
1840
+ horizontalAntiparallelFlow ? rect.y + rect.height : rect.x + rect.width,
1841
+ ),
1842
+ );
1843
+ const center = (minimumCross + maximumCross) / 2;
1844
+ for (const { member, rect } of rects) {
1845
+ const currentCross = horizontalAntiparallelFlow ? rect.y : rect.x;
1846
+ const nextCross = horizontalAntiparallelFlow
1847
+ ? center - rect.height / 2
1848
+ : center - rect.width / 2;
1849
+ postCompactionNodeCrossDeltas.set(member.id, nextCross - currentCross);
1850
+ mutableRects.set(
1851
+ member.id,
1852
+ horizontalAntiparallelFlow ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
1853
+ );
1854
+ }
1855
+ }
1856
+ }
1857
+ {
1858
+ const horizontal = horizontalAntiparallelFlow;
1859
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
1860
+ const pairEdges = new Map<string, GraphEdge[]>();
1861
+ for (const edge of graph.edges) {
1862
+ if (edge.sourceId === edge.targetId || (edge.width ?? 0) <= 0 || (edge.height ?? 0) <= 0) {
1863
+ continue;
1864
+ }
1865
+ const key = [edge.sourceId, edge.targetId].sort().join("\0");
1866
+ const pair = pairEdges.get(key) ?? [];
1867
+ pair.push(edge);
1868
+ pairEdges.set(key, pair);
1869
+ }
1870
+ for (const pair of pairEdges.values()) {
1871
+ if (
1872
+ pair.length !== 2 ||
1873
+ pair[0]!.sourceId !== pair[1]!.targetId ||
1874
+ pair[0]!.targetId !== pair[1]!.sourceId ||
1875
+ pair.some((edge) => {
1876
+ const settings = options.edgeSettings?.(edge);
1877
+ return (
1878
+ (settings?.["edgeLabels.placement"] ?? "CENTER") !== "CENTER" ||
1879
+ settings?.["edgeLabels.inline"] !== true
1880
+ );
1881
+ })
1882
+ ) {
1883
+ continue;
1884
+ }
1885
+ const firstNode = nodesById.get(pair[0]!.sourceId);
1886
+ const secondNode = nodesById.get(pair[0]!.targetId);
1887
+ const firstRect = placement.rectByNodeId.get(pair[0]!.sourceId);
1888
+ const secondRect = placement.rectByNodeId.get(pair[0]!.targetId);
1889
+ if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
1890
+ const nonSelfNeighbors = (nodeId: string) =>
1891
+ new Set(
1892
+ graph.edges.flatMap((edge) =>
1893
+ edge.sourceId === edge.targetId
1894
+ ? []
1895
+ : edge.sourceId === nodeId
1896
+ ? [edge.targetId]
1897
+ : edge.targetId === nodeId
1898
+ ? [edge.sourceId]
1899
+ : [],
1900
+ ),
1901
+ );
1902
+ const isolatedAntiparallelPair =
1903
+ nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
1904
+ const hasFlexiblePorts = (node: GraphNode): boolean => {
1905
+ const constraints = options.nodeSettings?.(node)?.portConstraints;
1906
+ return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
1907
+ };
1908
+ const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
1909
+ if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
1910
+
1911
+ const firstBeforeSecond = horizontal
1912
+ ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2
1913
+ : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
1914
+ const beforeRect = firstBeforeSecond ? firstRect : secondRect;
1915
+ const afterRect = firstBeforeSecond ? secondRect : firstRect;
1916
+ const forward = pair.find((edge) =>
1917
+ firstBeforeSecond
1918
+ ? edge.sourceId === pair[0]!.sourceId
1919
+ : edge.sourceId === pair[0]!.targetId,
1920
+ );
1921
+ if (!forward?.sourcePort || !forward.targetPort) continue;
1922
+
1923
+ let cross = Math.min(
1924
+ horizontal ? firstRect.y : firstRect.x,
1925
+ horizontal ? secondRect.y : secondRect.x,
1926
+ );
1927
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
1928
+ for (const edge of pair) {
1929
+ const width = edge.width ?? 0;
1930
+ const height = edge.height ?? 0;
1931
+ antiparallelLabelPositions.set(
1932
+ edge.id,
1933
+ isolatedAntiparallelPair
1934
+ ? horizontal
1935
+ ? {
1936
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
1937
+ y: cross,
1938
+ }
1939
+ : {
1940
+ x: cross,
1941
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
1942
+ }
1943
+ : horizontal
1944
+ ? {
1945
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
1946
+ y:
1947
+ edge === forward
1948
+ ? (firstRect.y +
1949
+ firstRect.height / 2 +
1950
+ secondRect.y +
1951
+ secondRect.height / 2) /
1952
+ 2 -
1953
+ height / 2
1954
+ : Math.max(firstRect.y + firstRect.height, secondRect.y + secondRect.height) +
1955
+ edgeSpacing,
1956
+ }
1957
+ : {
1958
+ x:
1959
+ edge === forward
1960
+ ? (firstRect.x + firstRect.width / 2 + secondRect.x + secondRect.width / 2) /
1961
+ 2 -
1962
+ width / 2
1963
+ : Math.max(firstRect.x + firstRect.width, secondRect.x + secondRect.width) +
1964
+ edgeSpacing,
1965
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
1966
+ },
1967
+ );
1968
+ if (!isolatedAntiparallelPair && edge !== forward) {
1969
+ outerAntiparallelLabelIds.add(edge.id);
1970
+ }
1971
+ cross += (horizontal ? height : width) + edgeSpacing;
1972
+ }
1973
+
1974
+ const forwardLabel = antiparallelLabelPositions.get(forward.id)!;
1975
+ const forwardCross = horizontal
1976
+ ? Math.round(forwardLabel.y + (forward.height ?? 0) / 2)
1977
+ : Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
1978
+ const alignPort = (node: GraphNode, portName: string): void => {
1979
+ const rect = placement.rectByNodeId.get(node.id);
1980
+ const port = node.ports?.find((candidate) => candidate.name === portName);
1981
+ if (!rect || !port) return;
1982
+ const placedPort = placePorts(
1983
+ node.ports,
1984
+ rect,
1985
+ direction,
1986
+ (candidate) => input.portSettings?.(candidate, node),
1987
+ { ...options.settings, ...options.nodeSettings?.(node) },
1988
+ (candidate) =>
1989
+ getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
1990
+ )?.find((candidate) => candidate.name === portName);
1991
+ if (!placedPort) return;
1992
+ const localCross = horizontal
1993
+ ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
1994
+ : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
1995
+ const nextCross = forwardCross - localCross;
1996
+ const currentCross = horizontal ? rect.y : rect.x;
1997
+ postCompactionNodeCrossDeltas.set(
1998
+ node.id,
1999
+ (postCompactionNodeCrossDeltas.get(node.id) ?? 0) + nextCross - currentCross,
2000
+ );
2001
+ mutableRects.set(
2002
+ node.id,
2003
+ horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
2004
+ );
2005
+ };
2006
+ if (isolatedAntiparallelPair) {
2007
+ alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
2008
+ alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
2009
+ }
2010
+ }
2011
+ }
2012
+ if (postCompactionNodeCrossDeltas.size > 0) {
2013
+ const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
2014
+ for (const edge of graph.edges) {
2015
+ const points = [...(pointsByEdgeId.get(edge.id) ?? [])];
2016
+ if (points.length === 0) continue;
2017
+ const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
2018
+ const targetDelta = postCompactionNodeCrossDeltas.get(edge.targetId) ?? 0;
2019
+ const shiftEndpoint = (endpointIndex: number, adjacentIndex: number, delta: number): void => {
2020
+ if (delta === 0) return;
2021
+ const endpoint = points[endpointIndex];
2022
+ const adjacent = points[adjacentIndex];
2023
+ if (!endpoint) return;
2024
+ const originalCross = horizontalAntiparallelFlow ? endpoint.y : endpoint.x;
2025
+ points[endpointIndex] = horizontalAntiparallelFlow
2026
+ ? { ...endpoint, y: endpoint.y + delta }
2027
+ : { ...endpoint, x: endpoint.x + delta };
2028
+ if (adjacent && (horizontalAntiparallelFlow ? adjacent.y : adjacent.x) === originalCross) {
2029
+ points[adjacentIndex] = horizontalAntiparallelFlow
2030
+ ? { ...adjacent, y: adjacent.y + delta }
2031
+ : { ...adjacent, x: adjacent.x + delta };
2032
+ }
2033
+ };
2034
+ shiftEndpoint(0, 1, sourceDelta);
2035
+ shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
2036
+ pointsByEdgeId.set(edge.id, points);
2037
+ }
2038
+ expandedRoutes = { ...expandedRoutes, pointsByEdgeId };
2039
+ }
1727
2040
  let routes = measure("long-edge-joining", () =>
1728
2041
  joinLongEdgeRoutes(
1729
2042
  expandedRoutes,
@@ -1877,6 +2190,99 @@ function runLayeredPipeline<N, E, G, P>(
1877
2190
  : { ...end, y: end.y + delta };
1878
2191
  (routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
1879
2192
  }
2193
+ for (const pair of labeledEdgesByPair.values()) {
2194
+ if (pair.length < 2) continue;
2195
+ const sourceRect = mutableRects.get(pair[0]!.sourceId);
2196
+ const targetRect = mutableRects.get(pair[0]!.targetId);
2197
+ if (!sourceRect || !targetRect) continue;
2198
+ const horizontal = direction === "left" || direction === "right";
2199
+ const beforeRect = horizontal
2200
+ ? sourceRect.x <= targetRect.x
2201
+ ? sourceRect
2202
+ : targetRect
2203
+ : sourceRect.y <= targetRect.y
2204
+ ? sourceRect
2205
+ : targetRect;
2206
+ const afterRect = beforeRect === sourceRect ? targetRect : sourceRect;
2207
+ const edgeSpacing = Number(options.settings?.["spacing.edgeEdge"] ?? 10);
2208
+ let cross = horizontal ? padding.top : padding.left;
2209
+ const orderedPair =
2210
+ options.settings?.["considerModelOrder.strategy"] === "PREFER_NODES"
2211
+ ? pair
2212
+ : [...pair].reverse();
2213
+ for (const edge of orderedPair) {
2214
+ const width = edge.width ?? 0;
2215
+ const height = edge.height ?? 0;
2216
+ parallelLabelPositions.set(
2217
+ edge.id,
2218
+ horizontal
2219
+ ? {
2220
+ x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
2221
+ y: cross,
2222
+ }
2223
+ : {
2224
+ x: cross,
2225
+ y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
2226
+ },
2227
+ );
2228
+ cross += (horizontal ? height : width) + edgeSpacing;
2229
+ }
2230
+
2231
+ const anchorEdge = orderedPair[0]!;
2232
+ const anchorLabel = parallelLabelPositions.get(anchorEdge.id)!;
2233
+ const desiredCross = Math.round(
2234
+ horizontal
2235
+ ? anchorLabel.y + (anchorEdge.height ?? 0) / 2
2236
+ : anchorLabel.x + (anchorEdge.width ?? 0) / 2,
2237
+ );
2238
+ const alignEndpoint = (nodeId: string, portName: string): void => {
2239
+ const node = graph.nodes.find((candidate) => candidate.id === nodeId);
2240
+ const rect = mutableRects.get(nodeId);
2241
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
2242
+ if (!node || !rect || !port) return;
2243
+ const placedPort = placePorts(
2244
+ node.ports,
2245
+ rect,
2246
+ direction,
2247
+ (candidate) => input.portSettings?.(candidate, node),
2248
+ { ...options.settings, ...options.nodeSettings?.(node) },
2249
+ (candidate) =>
2250
+ getOrientedPortDirection(expanded.input, expanded.orientation, node, candidate),
2251
+ )?.find((candidate) => candidate.name === portName);
2252
+ if (!placedPort) return;
2253
+ const localCross = horizontal
2254
+ ? (placedPort.y ?? 0) + (placedPort.height ?? 0) / 2
2255
+ : (placedPort.x ?? 0) + (placedPort.width ?? 0) / 2;
2256
+ const currentCross = horizontal ? rect.y : rect.x;
2257
+ const nextCross = desiredCross - localCross;
2258
+ const delta = nextCross - currentCross;
2259
+ mutableRects.set(nodeId, horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross });
2260
+ for (const edge of graph.edges) {
2261
+ const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
2262
+ if (points.length === 0) continue;
2263
+ const index =
2264
+ edge.sourceId === nodeId ? 0 : edge.targetId === nodeId ? points.length - 1 : undefined;
2265
+ if (index === undefined) continue;
2266
+ const endpoint = points[index]!;
2267
+ const originalCross = horizontal ? endpoint.y : endpoint.x;
2268
+ points[index] = horizontal
2269
+ ? { ...endpoint, y: endpoint.y + delta }
2270
+ : { ...endpoint, x: endpoint.x + delta };
2271
+ const adjacentIndex = index === 0 ? 1 : points.length - 2;
2272
+ const adjacent = points[adjacentIndex];
2273
+ if (adjacent && (horizontal ? adjacent.y : adjacent.x) === originalCross) {
2274
+ points[adjacentIndex] = horizontal
2275
+ ? { ...adjacent, y: adjacent.y + delta }
2276
+ : { ...adjacent, x: adjacent.x + delta };
2277
+ }
2278
+ (routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
2279
+ }
2280
+ };
2281
+ if (anchorEdge.sourcePort && anchorEdge.targetPort) {
2282
+ alignEndpoint(anchorEdge.sourceId, anchorEdge.sourcePort);
2283
+ alignEndpoint(anchorEdge.targetId, anchorEdge.targetPort);
2284
+ }
2285
+ }
1880
2286
  const routedPortAnchors = new Map<string, Point>();
1881
2287
  for (const edge of graph.edges) {
1882
2288
  const points = routes.pointsByEdgeId.get(edge.id);
@@ -1914,6 +2320,7 @@ function runLayeredPipeline<N, E, G, P>(
1914
2320
  direction,
1915
2321
  (port) => input.portSettings?.(port, node),
1916
2322
  { ...options.settings, ...options.nodeSettings?.(node) },
2323
+ (port) => getOrientedPortDirection(expanded.input, expanded.orientation, node, port),
1917
2324
  );
1918
2325
  if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") {
1919
2326
  ports = ports?.map((port) => {
@@ -2024,8 +2431,9 @@ function runLayeredPipeline<N, E, G, P>(
2024
2431
  .sort((left, right) => right.length - left.length)[0]
2025
2432
  : undefined;
2026
2433
  const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
2434
+ const targetNode = graph.nodes.find((node) => node.id === edge.targetId);
2027
2435
  const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
2028
- const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
2436
+ const targetPort = targetNode?.ports?.find((port) => port.name === edge.targetPort);
2029
2437
  const sourcePortSide =
2030
2438
  sourceNode && sourcePort
2031
2439
  ? options.portSettings?.(sourcePort, sourceNode)?.["port.side"]
@@ -2081,10 +2489,83 @@ function runLayeredPipeline<N, E, G, P>(
2081
2489
  : (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) +
2082
2490
  labelSpacing +
2083
2491
  Math.round(edgeThickness / 2);
2084
- const x =
2085
- labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
2086
- const y =
2087
- labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
2492
+ const sourceRect = placement.rectByNodeId.get(edge.sourceId);
2493
+ const targetRect = placement.rectByNodeId.get(edge.targetId);
2494
+ const hasFlexiblePorts = (node: GraphNode | undefined): boolean => {
2495
+ const constraints = node ? options.nodeSettings?.(node)?.portConstraints : undefined;
2496
+ return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
2497
+ };
2498
+ const flexibleFeedbackLabel =
2499
+ labelPlacement === "CENTER" &&
2500
+ inlineLabel &&
2501
+ expanded.orientation.reversedEdgeIds.has(edge.id) &&
2502
+ hasFlexiblePorts(sourceNode) &&
2503
+ hasFlexiblePorts(targetNode) &&
2504
+ sourceRect !== undefined &&
2505
+ targetRect !== undefined;
2506
+ const [beforeFlowRect, afterFlowRect] =
2507
+ sourceRect && targetRect
2508
+ ? horizontal
2509
+ ? sourceRect.x <= targetRect.x
2510
+ ? [sourceRect, targetRect]
2511
+ : [targetRect, sourceRect]
2512
+ : sourceRect.y <= targetRect.y
2513
+ ? [sourceRect, targetRect]
2514
+ : [targetRect, sourceRect]
2515
+ : [undefined, undefined];
2516
+ const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
2517
+ const outerAntiparallelLabelPosition = (() => {
2518
+ if (!outerAntiparallelLabelIds.has(edge.id)) return undefined;
2519
+ const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNode"] ?? 10);
2520
+ if (horizontal) {
2521
+ const x = (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2;
2522
+ const blockers = [
2523
+ sourceRect!,
2524
+ targetRect!,
2525
+ ...[...placement.rectByNodeId.values()].filter(
2526
+ (rect) => rect.x < x + width && rect.x + rect.width > x,
2527
+ ),
2528
+ ];
2529
+ return {
2530
+ x,
2531
+ y: Math.max(...blockers.map((rect) => rect.y + rect.height)) + edgeNodeSpacing,
2532
+ };
2533
+ }
2534
+ const y = (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2;
2535
+ const blockers = [
2536
+ sourceRect!,
2537
+ targetRect!,
2538
+ ...[...placement.rectByNodeId.values()].filter(
2539
+ (rect) => rect.y < y + height && rect.y + rect.height > y,
2540
+ ),
2541
+ ];
2542
+ return {
2543
+ x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing,
2544
+ y,
2545
+ };
2546
+ })();
2547
+ const explicitLabelPosition =
2548
+ outerAntiparallelLabelPosition ??
2549
+ antiparallelLabelPosition ??
2550
+ parallelLabelPositions.get(edge.id);
2551
+ const x = explicitLabelPosition
2552
+ ? explicitLabelPosition.x
2553
+ : flexibleFeedbackLabel
2554
+ ? horizontal
2555
+ ? (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2
2556
+ : targetRect.x + (targetRect.width - width) / 2
2557
+ : labelPlacement === "CENTER" && horizontal && labelDummyRect
2558
+ ? labelDummyRect.x
2559
+ : routeX;
2560
+ const y = explicitLabelPosition
2561
+ ? explicitLabelPosition.y
2562
+ : flexibleFeedbackLabel
2563
+ ? horizontal
2564
+ ? targetRect.y + (targetRect.height - height) / 2
2565
+ : (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2
2566
+ : labelPlacement === "CENTER" && !horizontal && labelDummyRect
2567
+ ? labelDummyRect.y
2568
+ : routeY;
2088
2569
  return {
2089
2570
  ...edge,
2090
2571
  x,
@@ -71,14 +71,15 @@ export function splitLongEdges(
71
71
  source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : undefined;
72
72
  const targetSide =
73
73
  target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : undefined;
74
+ const hasFixedPortSide = (node: GraphNode | undefined): boolean => {
75
+ if (!node) return false;
76
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
77
+ return constraints !== "UNDEFINED" && constraints !== "FREE";
78
+ };
74
79
  const fixedSideFeedback =
75
80
  sourceLayer > targetLayer &&
76
- ((source !== undefined &&
77
- input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" &&
78
- sourceSide === forwardSourceSide) ||
79
- (target !== undefined &&
80
- input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" &&
81
- targetSide === forwardTargetSide));
81
+ ((hasFixedPortSide(source) && sourceSide === forwardSourceSide) ||
82
+ (hasFixedPortSide(target) && targetSide === forwardTargetSide));
82
83
  if (
83
84
  span <= 1 ||
84
85
  edge.sourceId === edge.targetId ||