@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.
- package/README.md +5 -2
- package/dist/elkjs/index.mjs +16 -12
- package/dist/index.mjs +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-QwJn2gb2.mjs → layered-Bp8MkxWK.mjs} +361 -15
- package/dist/{spore-D15xIQKj.mjs → spore-ePWz2LtF.mjs} +1 -1
- package/package.json +5 -1
- package/src/elkjs/index.ts +25 -24
- package/src/layered/index.ts +488 -7
- package/src/layered/long-edges.ts +7 -6
- package/src/layered/strategies.ts +66 -6
package/src/layered/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
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
|
|
77
|
-
|
|
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 ||
|