@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.
- package/README.md +32 -3
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +1 -1
- package/dist/elkjs/index.d.mts +1 -1
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/{elkjs-Cj-lWiK1.cjs → elkjs-Bh_E59N_.cjs} +3 -3
- package/dist/{elkjs-B5kN_zS6.mjs → elkjs-Dk9yOk9U.mjs} +3 -3
- package/dist/{index-B6cWNrMw.d.cts → index-0YVSvMvA.d.mts} +13 -3
- package/dist/{index-CJQLUKD9.d.mts → index-C0J4ttZe.d.cts} +13 -3
- package/dist/{index-DT6lPMfS.d.cts → index-Xc4R6y3z.d.cts} +9 -2
- package/dist/{index-F1U-fCTu.d.mts → index-lSp0oCJC.d.mts} +9 -2
- package/dist/index.cjs +7 -4
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-BsAdm_u8.mjs → layered-CUwft7on.mjs} +488 -91
- package/dist/{layered-BJu8A21s.cjs → layered-QzYtokgN.cjs} +488 -91
- package/dist/{layout-engine-BFP1TZbF.cjs → layout-engine-CQ5FcBPC.cjs} +1 -1
- package/dist/{layout-engine-DnlmGxHL.mjs → layout-engine-DvDkMQM3.mjs} +1 -1
- package/dist/routing/index.cjs +5 -2
- package/dist/routing/index.d.cts +3 -3
- package/dist/routing/index.d.mts +3 -3
- package/dist/routing/index.mjs +2 -2
- package/dist/routing-D-LfMXOF.mjs +5189 -0
- package/dist/routing-Phxu1R0x.cjs +5423 -0
- package/dist/{types-CT2akdK0.d.mts → types-D8pYSn8I.d.cts} +112 -2
- package/dist/{types-uxPlGRof.d.cts → types-MAUtJfKQ.d.mts} +112 -2
- package/package.json +4 -1
- package/src/authoring/partial.ts +1 -7
- package/src/authoring/routing.ts +64 -19
- package/src/layered/feedback-chains.ts +22 -4
- package/src/layered/index.ts +186 -42
- package/src/layered/loop-envelopes.ts +96 -26
- package/src/layered/north-south-ports.ts +17 -4
- package/src/layered/self-loop-ports.ts +158 -0
- package/src/layered/strategies.ts +183 -32
- package/src/routing/algorithms.ts +1436 -148
- package/src/routing/bezier-fit.ts +605 -0
- package/src/routing/bezier-visibility.ts +107 -0
- package/src/routing/bezier.ts +596 -0
- package/src/routing/engine.ts +65 -13
- package/src/routing/index.ts +14 -0
- package/src/routing/model.ts +185 -1
- package/src/routing/path.ts +1 -1
- package/src/routing/policy-geometry.ts +536 -0
- package/src/routing/search.ts +10 -2
- package/src/routing/types.ts +77 -1
- package/dist/routing-CR8flv8b.mjs +0 -2780
- package/dist/routing-QIdGGFBe.cjs +0 -2996
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* ELK self-loop port hiding and restoration. SPDX-License-Identifier: EPL-2.0 */
|
|
2
|
+
import type { Graph, GraphEdge, GraphPort, Point, VisualGraph, VisualPort } from "@statelyai/graph";
|
|
3
|
+
import type { LayeredLayoutOptions } from "./index";
|
|
4
|
+
|
|
5
|
+
interface PortSize {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface HiddenLoopPorts {
|
|
10
|
+
source: PortSize;
|
|
11
|
+
target: PortSize;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hiddenBySettings = new WeakMap<object, ReadonlyMap<string, HiddenLoopPorts>>();
|
|
15
|
+
|
|
16
|
+
/** Sizes of the ports SelfLoopPreProcessor hid from a loop, if any. */
|
|
17
|
+
export function hiddenLoopPorts(settings: object, edgeId: string): HiddenLoopPorts | undefined {
|
|
18
|
+
return hiddenBySettings.get(settings)?.get(edgeId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ELK's SelfLoopPreProcessor removes ports that carry only self loops from
|
|
23
|
+
* nodes whose port order is not fixed, and SelfLoopPortRestorer later puts
|
|
24
|
+
* them on the loop's side. The layered phases then see implicit loop ports;
|
|
25
|
+
* {@link restoreSelfLoopPorts} places the hidden ports at the loop ends.
|
|
26
|
+
*/
|
|
27
|
+
export function hideSelfLoopPorts<N, E, G, P>(
|
|
28
|
+
graph: Graph<N, E, G, P>,
|
|
29
|
+
options: LayeredLayoutOptions,
|
|
30
|
+
):
|
|
31
|
+
| {
|
|
32
|
+
graph: Graph<N, E, G, P>;
|
|
33
|
+
options: LayeredLayoutOptions;
|
|
34
|
+
edges: ReadonlyMap<string, GraphEdge<E>>;
|
|
35
|
+
}
|
|
36
|
+
| undefined {
|
|
37
|
+
const uses = new Map<string, number>();
|
|
38
|
+
const nonLoop = new Set<string>();
|
|
39
|
+
for (const edge of graph.edges) {
|
|
40
|
+
for (const [node, port] of [
|
|
41
|
+
[edge.sourceId, edge.sourcePort],
|
|
42
|
+
[edge.targetId, edge.targetPort],
|
|
43
|
+
] as const) {
|
|
44
|
+
if (port === undefined) continue;
|
|
45
|
+
const key = `${node}\0${port}`;
|
|
46
|
+
uses.set(key, (uses.get(key) ?? 0) + 1);
|
|
47
|
+
if (edge.sourceId !== edge.targetId) nonLoop.add(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
51
|
+
const hidden = new Map<string, HiddenLoopPorts>();
|
|
52
|
+
const edges = new Map<string, GraphEdge<E>>();
|
|
53
|
+
const hiddenPorts = new Set<string>();
|
|
54
|
+
for (const edge of graph.edges) {
|
|
55
|
+
if (
|
|
56
|
+
edge.sourceId !== edge.targetId ||
|
|
57
|
+
edge.sourcePort === undefined ||
|
|
58
|
+
edge.targetPort === undefined ||
|
|
59
|
+
edge.sourcePort === edge.targetPort
|
|
60
|
+
)
|
|
61
|
+
continue;
|
|
62
|
+
const node = nodeById.get(edge.sourceId);
|
|
63
|
+
const constraints = node && options.nodeSettings?.(node)?.portConstraints;
|
|
64
|
+
if (
|
|
65
|
+
!node ||
|
|
66
|
+
(constraints !== undefined && constraints !== "FREE" && constraints !== "UNDEFINED")
|
|
67
|
+
)
|
|
68
|
+
continue;
|
|
69
|
+
// One loop per port: shared ports would make a self hyperloop.
|
|
70
|
+
const ports = [edge.sourcePort, edge.targetPort].map((name) =>
|
|
71
|
+
node.ports?.find((port) => port.name === name),
|
|
72
|
+
);
|
|
73
|
+
if (
|
|
74
|
+
ports.some(
|
|
75
|
+
(port) =>
|
|
76
|
+
!port ||
|
|
77
|
+
uses.get(`${node.id}\0${port.name}`) !== 1 ||
|
|
78
|
+
nonLoop.has(`${node.id}\0${port.name}`),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
continue;
|
|
82
|
+
const size = (port: GraphPort<P>) => ({ width: port.width ?? 0, height: port.height ?? 0 });
|
|
83
|
+
hidden.set(edge.id, { source: size(ports[0]!), target: size(ports[1]!) });
|
|
84
|
+
edges.set(edge.id, edge);
|
|
85
|
+
for (const port of ports) hiddenPorts.add(`${node.id}\0${port!.name}`);
|
|
86
|
+
}
|
|
87
|
+
if (hidden.size === 0) return undefined;
|
|
88
|
+
const settings = { ...options.settings };
|
|
89
|
+
hiddenBySettings.set(settings, hidden);
|
|
90
|
+
return {
|
|
91
|
+
graph: {
|
|
92
|
+
...graph,
|
|
93
|
+
nodes: graph.nodes.map((node) =>
|
|
94
|
+
node.ports?.some((port) => hiddenPorts.has(`${node.id}\0${port.name}`))
|
|
95
|
+
? {
|
|
96
|
+
...node,
|
|
97
|
+
ports: node.ports.filter((port) => !hiddenPorts.has(`${node.id}\0${port.name}`)),
|
|
98
|
+
}
|
|
99
|
+
: node,
|
|
100
|
+
),
|
|
101
|
+
edges: graph.edges.map((edge) =>
|
|
102
|
+
hidden.has(edge.id) ? { ...edge, sourcePort: undefined, targetPort: undefined } : edge,
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
options: { ...options, settings },
|
|
106
|
+
edges,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Put hidden loop ports back, anchored on the side their loop leaves from. */
|
|
111
|
+
export function restoreSelfLoopPorts<N, E, G, P>(
|
|
112
|
+
graph: Graph<N, E, G, P>,
|
|
113
|
+
result: VisualGraph<N, E, G, P>,
|
|
114
|
+
edges: ReadonlyMap<string, GraphEdge<E>>,
|
|
115
|
+
): VisualGraph<N, E, G, P> {
|
|
116
|
+
const anchors = new Map<string, Point>();
|
|
117
|
+
for (const edge of result.edges) {
|
|
118
|
+
const original = edges.get(edge.id);
|
|
119
|
+
const points = edge.points;
|
|
120
|
+
if (!original || !points?.length) continue;
|
|
121
|
+
anchors.set(`${edge.sourceId}\0${original.sourcePort}`, points[0]!);
|
|
122
|
+
anchors.set(`${edge.targetId}\0${original.targetPort}`, points.at(-1)!);
|
|
123
|
+
}
|
|
124
|
+
const originalById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
nodes: result.nodes.map((node) => {
|
|
128
|
+
const original = originalById.get(node.id)?.ports;
|
|
129
|
+
if (!original?.some((port) => anchors.has(`${node.id}\0${port.name}`))) return node;
|
|
130
|
+
const placed = new Map(node.ports?.map((port) => [port.name, port]));
|
|
131
|
+
return {
|
|
132
|
+
...node,
|
|
133
|
+
ports: original.map((port): VisualPort<P> => {
|
|
134
|
+
const anchor = anchors.get(`${node.id}\0${port.name}`);
|
|
135
|
+
const width = port.width ?? 0,
|
|
136
|
+
height = port.height ?? 0;
|
|
137
|
+
if (!anchor) return placed.get(port.name) ?? { ...port, width, height, x: 0, y: 0 };
|
|
138
|
+
const x = anchor.x - node.x,
|
|
139
|
+
y = anchor.y - node.y;
|
|
140
|
+
// The loop leaves from the port's outer border, centered on it.
|
|
141
|
+
return {
|
|
142
|
+
...port,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
x: x <= 0 ? x : x >= node.width ? x - width : x - width / 2,
|
|
146
|
+
y: x <= 0 || x >= node.width ? y - height / 2 : y <= 0 ? y : y - height,
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
edges: result.edges.map((edge) => {
|
|
152
|
+
const original = edges.get(edge.id);
|
|
153
|
+
return original
|
|
154
|
+
? { ...edge, sourcePort: original.sourcePort, targetPort: original.targetPort }
|
|
155
|
+
: edge;
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -19,6 +19,8 @@ import { routingCoordinates } from "./routing-coordinates";
|
|
|
19
19
|
import {
|
|
20
20
|
hasMovableLoopPorts,
|
|
21
21
|
hasPlacementLoopEnvelope,
|
|
22
|
+
movableLoopPortOffset,
|
|
23
|
+
movableLoopTrack,
|
|
22
24
|
loopEnvelopes,
|
|
23
25
|
preparedLoopEnvelopes,
|
|
24
26
|
} from "./loop-envelopes";
|
|
@@ -72,6 +74,24 @@ const modelSweepInputs = new WeakSet<LayeredPhaseInput>();
|
|
|
72
74
|
|
|
73
75
|
const nodeRelativePortRanksByInput = new WeakMap<LayeredPhaseInput, boolean>();
|
|
74
76
|
|
|
77
|
+
const labelPortSideFeedbackByInput = new WeakMap<
|
|
78
|
+
LayeredPhaseInput,
|
|
79
|
+
{ exempt: ReadonlySet<string>; routed: Set<string> }
|
|
80
|
+
>();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @internal Collect the feedback segments routed to a center-label dummy's port
|
|
84
|
+
* sides. Exempt segments keep the plain feedback detour instead.
|
|
85
|
+
*/
|
|
86
|
+
export function trackLabelPortSideFeedback(
|
|
87
|
+
input: LayeredPhaseInput,
|
|
88
|
+
exempt: ReadonlySet<string>,
|
|
89
|
+
): ReadonlySet<string> {
|
|
90
|
+
const routed = new Set<string>();
|
|
91
|
+
labelPortSideFeedbackByInput.set(input, { exempt, routed });
|
|
92
|
+
return routed;
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
function getOrientedEndpoints(
|
|
76
96
|
edge: GraphEdge,
|
|
77
97
|
orientation: AcyclicOrientation,
|
|
@@ -1794,6 +1814,8 @@ function sortLayerByAdjacentPosition(
|
|
|
1794
1814
|
sameLayerNeighbors: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
1795
1815
|
orderedVisits?: ReadonlyMap<string, readonly (number | string)[]>,
|
|
1796
1816
|
forcedCompare?: (left: string, right: string) => number,
|
|
1817
|
+
/** Nodes ELK's forced model-order comparator ties with everything. */
|
|
1818
|
+
forcedTies?: ReadonlySet<string>,
|
|
1797
1819
|
): void {
|
|
1798
1820
|
const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index] as const));
|
|
1799
1821
|
const adjacentPosition = new Map<string, number | undefined>();
|
|
@@ -1870,6 +1892,7 @@ function sortLayerByAdjacentPosition(
|
|
|
1870
1892
|
}
|
|
1871
1893
|
|
|
1872
1894
|
const compare = (a: string, b: string) => {
|
|
1895
|
+
if (forcedTies?.has(a) || forcedTies?.has(b)) return 0;
|
|
1873
1896
|
return (
|
|
1874
1897
|
forcedCompare?.(a, b) ||
|
|
1875
1898
|
(adjacentPosition.get(a) ?? 0) - (adjacentPosition.get(b) ?? 0) ||
|
|
@@ -2185,6 +2208,18 @@ export function createLayerSweepSession(
|
|
|
2185
2208
|
usesInitialModelOrder && input.settings["crossingMinimization.forceNodeModelOrder"] === true
|
|
2186
2209
|
? normalModelComparator(input)
|
|
2187
2210
|
: undefined;
|
|
2211
|
+
// ELK's forced comparator returns 0 for separate-layer nodes before
|
|
2212
|
+
// consulting barycenters, so insertion sort never moves past them.
|
|
2213
|
+
const forcedTies = forcedCompare
|
|
2214
|
+
? new Set(
|
|
2215
|
+
input.graph.nodes
|
|
2216
|
+
.filter((node) => {
|
|
2217
|
+
const constraint = input.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
2218
|
+
return constraint === "FIRST_SEPARATE" || constraint === "LAST_SEPARATE";
|
|
2219
|
+
})
|
|
2220
|
+
.map((node) => node.id),
|
|
2221
|
+
)
|
|
2222
|
+
: undefined;
|
|
2188
2223
|
const usePortRanks = true;
|
|
2189
2224
|
const medianWeights = new Map<string, number>();
|
|
2190
2225
|
const listedEdgeIds = new Map<string, string>();
|
|
@@ -2440,8 +2475,10 @@ export function createLayerSweepSession(
|
|
|
2440
2475
|
(working[firstLayerIndex] ?? []).map((id) => [id, random.nextDouble()]),
|
|
2441
2476
|
);
|
|
2442
2477
|
const compare = (left: string, right: string) =>
|
|
2443
|
-
|
|
2444
|
-
|
|
2478
|
+
forcedTies?.has(left) || forcedTies?.has(right)
|
|
2479
|
+
? 0
|
|
2480
|
+
: forcedCompare?.(left, right) ||
|
|
2481
|
+
(firstLayerWeights.get(left) ?? 0) - (firstLayerWeights.get(right) ?? 0);
|
|
2445
2482
|
const first = working[firstLayerIndex];
|
|
2446
2483
|
if (first) {
|
|
2447
2484
|
if (forcedCompare)
|
|
@@ -2596,6 +2633,7 @@ export function createLayerSweepSession(
|
|
|
2596
2633
|
sameLayerNeighbors(isForward),
|
|
2597
2634
|
adjacent.visits,
|
|
2598
2635
|
forcedCompare,
|
|
2636
|
+
forcedTies,
|
|
2599
2637
|
);
|
|
2600
2638
|
}
|
|
2601
2639
|
groupMixedPortDirections(current, previous, working[layer + 1]);
|
|
@@ -2625,6 +2663,7 @@ export function createLayerSweepSession(
|
|
|
2625
2663
|
sameLayerNeighbors(isForward),
|
|
2626
2664
|
adjacent.visits,
|
|
2627
2665
|
forcedCompare,
|
|
2666
|
+
forcedTies,
|
|
2628
2667
|
);
|
|
2629
2668
|
}
|
|
2630
2669
|
groupMixedPortDirections(current, next, working[layer - 1]);
|
|
@@ -4300,16 +4339,14 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4300
4339
|
edge: GraphEdge,
|
|
4301
4340
|
side: string,
|
|
4302
4341
|
ordering: string,
|
|
4303
|
-
canonical = false,
|
|
4304
4342
|
): number => {
|
|
4305
|
-
const
|
|
4306
|
-
const axis = crossSide === (!canonical || horizontal) ? "height" : "width";
|
|
4343
|
+
const axis = side === "NORTH" || side === "SOUTH" ? "height" : "width";
|
|
4307
4344
|
const ordered = ordering === "REVERSE_STACKED" ? [...loops].reverse() : loops;
|
|
4308
4345
|
const index = ordered.indexOf(edge);
|
|
4309
4346
|
const spacing = Number(input.settings["spacing.nodeSelfLoop"] ?? 10);
|
|
4310
4347
|
const labelSpacing = Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
4311
4348
|
const size = (candidate: GraphEdge) =>
|
|
4312
|
-
|
|
4349
|
+
input.edgeSettings?.(candidate)?.["edgeLabels.inline"] === true
|
|
4313
4350
|
? (candidate[axis] ?? 0)
|
|
4314
4351
|
: 0;
|
|
4315
4352
|
return (
|
|
@@ -4405,7 +4442,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4405
4442
|
for (const [id, loops] of selfLoopsByNodeId) {
|
|
4406
4443
|
const rect = mutableRects.get(id);
|
|
4407
4444
|
const node = nodeById.get(id);
|
|
4408
|
-
if (!rect || !node) continue;
|
|
4445
|
+
if (!rect || !node || hasPlacementLoopEnvelope(placement, id)) continue;
|
|
4409
4446
|
const settings = input.nodeSettings?.(node);
|
|
4410
4447
|
const distribution = settings?.["edgeRouting.selfLoopDistribution"] ?? "NORTH";
|
|
4411
4448
|
const ordering = settings?.["edgeRouting.selfLoopOrdering"] ?? "STACKED";
|
|
@@ -5845,6 +5882,25 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5845
5882
|
input,
|
|
5846
5883
|
)
|
|
5847
5884
|
: targetFallback;
|
|
5885
|
+
// A reversed chain enters a center-label dummy through its forward-side
|
|
5886
|
+
// port and leaves through its backward-side port: route to those sides.
|
|
5887
|
+
const [forwardSide, backwardSide] = (
|
|
5888
|
+
{
|
|
5889
|
+
right: ["EAST", "WEST"],
|
|
5890
|
+
left: ["WEST", "EAST"],
|
|
5891
|
+
down: ["SOUTH", "NORTH"],
|
|
5892
|
+
up: ["NORTH", "SOUTH"],
|
|
5893
|
+
} as const
|
|
5894
|
+
)[input.direction];
|
|
5895
|
+
const labelPortSides = labelPortSideFeedbackByInput.get(input);
|
|
5896
|
+
const faces = (node: GraphNode, side: unknown, away: string) =>
|
|
5897
|
+
!fixedSideFeedback &&
|
|
5898
|
+
!labelPortSides?.exempt.has(edge.id) &&
|
|
5899
|
+
node.id.startsWith("__layout_dummy:label:") &&
|
|
5900
|
+
side === away;
|
|
5901
|
+
const sourceFace = faces(source, feedbackSourcePortSide, backwardSide) ? -1 : 1;
|
|
5902
|
+
const targetFace = faces(target, feedbackTargetPortSide, forwardSide) ? -1 : 1;
|
|
5903
|
+
if (sourceFace < 0 || targetFace < 0) labelPortSides?.routed.add(edge.id);
|
|
5848
5904
|
if (horizontal) {
|
|
5849
5905
|
const sign = fixedSideFeedback
|
|
5850
5906
|
? Math.sign(sourceRect.x - targetRect.x) || 1
|
|
@@ -5859,16 +5915,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5859
5915
|
fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
|
|
5860
5916
|
pointsByEdgeId.set(edge.id, [
|
|
5861
5917
|
start,
|
|
5862
|
-
{ x: start.x + sign * flowSpacing, y: start.y },
|
|
5863
|
-
{ x: start.x + sign * flowSpacing, y: outerCross },
|
|
5918
|
+
{ x: start.x + sourceFace * sign * flowSpacing, y: start.y },
|
|
5919
|
+
{ x: start.x + sourceFace * sign * flowSpacing, y: outerCross },
|
|
5864
5920
|
...(preserveCenters
|
|
5865
5921
|
? [
|
|
5866
5922
|
{ x: sourceRect.x + sourceRect.width / 2, y: outerCross },
|
|
5867
5923
|
{ x: targetRect.x + targetRect.width / 2, y: outerCross },
|
|
5868
5924
|
]
|
|
5869
5925
|
: []),
|
|
5870
|
-
{ x: end.x - sign * flowSpacing, y: outerCross },
|
|
5871
|
-
{ x: end.x - sign * flowSpacing, y: end.y },
|
|
5926
|
+
{ x: end.x - targetFace * sign * flowSpacing, y: outerCross },
|
|
5927
|
+
{ x: end.x - targetFace * sign * flowSpacing, y: end.y },
|
|
5872
5928
|
end,
|
|
5873
5929
|
]);
|
|
5874
5930
|
} else {
|
|
@@ -5885,16 +5941,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5885
5941
|
fixedSideFeedback && input.settings.unnecessaryBendpoints === true;
|
|
5886
5942
|
pointsByEdgeId.set(edge.id, [
|
|
5887
5943
|
start,
|
|
5888
|
-
{ x: start.x, y: start.y + sign * flowSpacing },
|
|
5889
|
-
{ x: outerCross, y: start.y + sign * flowSpacing },
|
|
5944
|
+
{ x: start.x, y: start.y + sourceFace * sign * flowSpacing },
|
|
5945
|
+
{ x: outerCross, y: start.y + sourceFace * sign * flowSpacing },
|
|
5890
5946
|
...(preserveCenters
|
|
5891
5947
|
? [
|
|
5892
5948
|
{ x: outerCross, y: sourceRect.y + sourceRect.height / 2 },
|
|
5893
5949
|
{ x: outerCross, y: targetRect.y + targetRect.height / 2 },
|
|
5894
5950
|
]
|
|
5895
5951
|
: []),
|
|
5896
|
-
{ x: outerCross, y: end.y - sign * flowSpacing },
|
|
5897
|
-
{ x: end.x, y: end.y - sign * flowSpacing },
|
|
5952
|
+
{ x: outerCross, y: end.y - targetFace * sign * flowSpacing },
|
|
5953
|
+
{ x: end.x, y: end.y - targetFace * sign * flowSpacing },
|
|
5898
5954
|
end,
|
|
5899
5955
|
]);
|
|
5900
5956
|
}
|
|
@@ -6090,9 +6146,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
6090
6146
|
continue;
|
|
6091
6147
|
}
|
|
6092
6148
|
const movable =
|
|
6093
|
-
hasMovableLoopPorts(input, edge) &&
|
|
6094
|
-
hasPlacementLoopEnvelope(placement, source.id) &&
|
|
6095
|
-
input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true;
|
|
6149
|
+
hasMovableLoopPorts(input, edge) && hasPlacementLoopEnvelope(placement, source.id);
|
|
6096
6150
|
// Route unconstrained loops in the canonical rightward system, then
|
|
6097
6151
|
// apply the same direction transform as the layer pipeline.
|
|
6098
6152
|
const loopRect = movable
|
|
@@ -6144,7 +6198,21 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
6144
6198
|
const sideLoops = loops.filter(
|
|
6145
6199
|
(candidate, i) => loopSide(distribution, i, candidate) === side,
|
|
6146
6200
|
);
|
|
6147
|
-
|
|
6201
|
+
if (movable) {
|
|
6202
|
+
const track = movableLoopTrack(input, sideLoops, edge, side, ordering);
|
|
6203
|
+
const sign = side === "NORTH" ? -1 : 1;
|
|
6204
|
+
const base = side === "NORTH" ? 0 : loopRect.height;
|
|
6205
|
+
const [first, last] = [startRatio, endRatio].map((ratio) =>
|
|
6206
|
+
movableLoopPortOffset(input, sideLoops, side, loopRect.width, ratio),
|
|
6207
|
+
);
|
|
6208
|
+
return [
|
|
6209
|
+
{ x: first!.along, y: base + sign * first!.normal },
|
|
6210
|
+
{ x: first!.along, y: base + sign * track },
|
|
6211
|
+
{ x: last!.along, y: base + sign * track },
|
|
6212
|
+
{ x: last!.along, y: base + sign * last!.normal },
|
|
6213
|
+
];
|
|
6214
|
+
}
|
|
6215
|
+
const distance = loopDistance(sideLoops, edge, side, ordering);
|
|
6148
6216
|
const y =
|
|
6149
6217
|
side === "NORTH" ? loopRect.y - distance : loopRect.y + loopRect.height + distance;
|
|
6150
6218
|
const start = {
|
|
@@ -6168,15 +6236,26 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
6168
6236
|
const secondRatio = sequenced ? 1 / denominator : 2 / denominator;
|
|
6169
6237
|
const startRatio = side === "EAST" ? firstRatio : secondRatio;
|
|
6170
6238
|
const endRatio = side === "EAST" ? secondRatio : firstRatio;
|
|
6239
|
+
const sideLoops = loops.filter(
|
|
6240
|
+
(candidate, i) => loopSide(distribution, i, candidate) === side,
|
|
6241
|
+
);
|
|
6242
|
+
if (movable) {
|
|
6243
|
+
const track = movableLoopTrack(input, sideLoops, edge, side, ordering);
|
|
6244
|
+
const sign = side === "WEST" ? -1 : 1;
|
|
6245
|
+
const base = side === "WEST" ? 0 : loopRect.width;
|
|
6246
|
+
const [first, last] = [startRatio, endRatio].map((ratio) =>
|
|
6247
|
+
movableLoopPortOffset(input, sideLoops, side, loopRect.height, ratio),
|
|
6248
|
+
);
|
|
6249
|
+
return [
|
|
6250
|
+
{ x: base + sign * first!.normal, y: first!.along },
|
|
6251
|
+
{ x: base + sign * track, y: first!.along },
|
|
6252
|
+
{ x: base + sign * track, y: last!.along },
|
|
6253
|
+
{ x: base + sign * last!.normal, y: last!.along },
|
|
6254
|
+
];
|
|
6255
|
+
}
|
|
6171
6256
|
const distance =
|
|
6172
|
-
|
|
6173
|
-
? loopDistance(
|
|
6174
|
-
loops.filter((candidate, i) => loopSide(distribution, i, candidate) === side),
|
|
6175
|
-
edge,
|
|
6176
|
-
side,
|
|
6177
|
-
ordering,
|
|
6178
|
-
movable,
|
|
6179
|
-
)
|
|
6257
|
+
input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true
|
|
6258
|
+
? loopDistance(sideLoops, edge, side, ordering)
|
|
6180
6259
|
: spacing * (indexOnSide + 1);
|
|
6181
6260
|
const x =
|
|
6182
6261
|
side === "EAST" ? loopRect.x + loopRect.width + distance : loopRect.x - distance;
|
|
@@ -6528,6 +6607,67 @@ export const routeEdgesOrthogonally = routeEdges("ORTHOGONAL");
|
|
|
6528
6607
|
export const routeEdgesWithPolylines = routeEdges("POLYLINE");
|
|
6529
6608
|
export const routeEdgesWithSplines = routeEdges("SPLINES");
|
|
6530
6609
|
|
|
6610
|
+
type PortSide = "NORTH" | "SOUTH" | "WEST" | "EAST";
|
|
6611
|
+
const sweptPortRanks = new WeakMap<
|
|
6612
|
+
readonly GraphPort<unknown>[],
|
|
6613
|
+
(port: GraphPort<unknown>, side: PortSide) => number | undefined
|
|
6614
|
+
>();
|
|
6615
|
+
|
|
6616
|
+
/**
|
|
6617
|
+
* Order movable ports along each physical side as crossing minimization left
|
|
6618
|
+
* them, as ELK places ports from its swept port lists. The lists are
|
|
6619
|
+
* clockwise per canonical side.
|
|
6620
|
+
*/
|
|
6621
|
+
export function recordSweptPortOrder(
|
|
6622
|
+
graph: Pick<LayeredPhaseInput["graph"], "nodes" | "edges">,
|
|
6623
|
+
direction: LayeredPhaseInput["direction"],
|
|
6624
|
+
order: LayerOrder,
|
|
6625
|
+
): void {
|
|
6626
|
+
// Unconnected ports carry no swept decision.
|
|
6627
|
+
const connected = new Set(
|
|
6628
|
+
graph.edges.flatMap((edge) => [
|
|
6629
|
+
JSON.stringify([edge.sourceId, edge.sourcePort]),
|
|
6630
|
+
JSON.stringify([edge.targetId, edge.targetPort]),
|
|
6631
|
+
]),
|
|
6632
|
+
);
|
|
6633
|
+
const vertical = direction === "down" || direction === "up";
|
|
6634
|
+
const reverse = direction === "up" || direction === "left";
|
|
6635
|
+
const canonical: Record<PortSide, PortSide> =
|
|
6636
|
+
direction === "right"
|
|
6637
|
+
? { NORTH: "NORTH", EAST: "EAST", SOUTH: "SOUTH", WEST: "WEST" }
|
|
6638
|
+
: direction === "left"
|
|
6639
|
+
? { NORTH: "NORTH", EAST: "WEST", SOUTH: "SOUTH", WEST: "EAST" }
|
|
6640
|
+
: direction === "down"
|
|
6641
|
+
? { NORTH: "WEST", EAST: "SOUTH", SOUTH: "EAST", WEST: "NORTH" }
|
|
6642
|
+
: { NORTH: "EAST", EAST: "SOUTH", SOUTH: "WEST", WEST: "NORTH" };
|
|
6643
|
+
for (const node of graph.nodes) {
|
|
6644
|
+
if (!node.ports) continue;
|
|
6645
|
+
const swept = order.physicalPortOrderByNodeId?.get(node.id);
|
|
6646
|
+
if (!swept) {
|
|
6647
|
+
sweptPortRanks.delete(node.ports);
|
|
6648
|
+
continue;
|
|
6649
|
+
}
|
|
6650
|
+
const index = new Map(swept.flatMap((id, i) => (connected.has(id) ? [[id, i] as const] : [])));
|
|
6651
|
+
const rankByPort = new Map(
|
|
6652
|
+
node.ports.map((port) => [port, index.get(JSON.stringify([node.id, port.name]))]),
|
|
6653
|
+
);
|
|
6654
|
+
sweptPortRanks.set(node.ports, (port, side) => {
|
|
6655
|
+
const rank = rankByPort.get(port);
|
|
6656
|
+
if (rank === undefined) return undefined;
|
|
6657
|
+
// Canonical clockwise ranks rise with the flow coordinate on NORTH,
|
|
6658
|
+
// with the cross coordinate on EAST, and fall on SOUTH and WEST.
|
|
6659
|
+
const alongFlow = (side === "NORTH" || side === "SOUTH") !== vertical;
|
|
6660
|
+
const c = canonical[side];
|
|
6661
|
+
const sign = alongFlow
|
|
6662
|
+
? (c === "NORTH" ? 1 : -1) * (reverse ? -1 : 1)
|
|
6663
|
+
: c === "EAST"
|
|
6664
|
+
? 1
|
|
6665
|
+
: -1;
|
|
6666
|
+
return sign * rank;
|
|
6667
|
+
});
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
|
|
6531
6671
|
export function placePorts<P>(
|
|
6532
6672
|
ports: readonly GraphPort<P>[] | undefined,
|
|
6533
6673
|
rect: EntityRect,
|
|
@@ -6573,13 +6713,24 @@ export function placePorts<P>(
|
|
|
6573
6713
|
group.push(port);
|
|
6574
6714
|
grouped.set(side, group);
|
|
6575
6715
|
}
|
|
6716
|
+
const swept =
|
|
6717
|
+
constraints === "FIXED_ORDER" || constraints === "FIXED_POS" || constraints === "FIXED_RATIO"
|
|
6718
|
+
? undefined
|
|
6719
|
+
: sweptPortRanks.get(ports as readonly GraphPort<unknown>[]);
|
|
6576
6720
|
for (const group of grouped.values()) {
|
|
6577
|
-
group.sort(
|
|
6578
|
-
(left, right) =>
|
|
6579
|
-
Number(portSettings?.(left)?.["port.index"] ?? ports.indexOf(left)) -
|
|
6580
|
-
Number(portSettings?.(right)?.["port.index"] ?? ports.indexOf(right)),
|
|
6581
|
-
);
|
|
6582
6721
|
const side = sideByPort.get(group[0]!);
|
|
6722
|
+
const ranks = swept && group.map((port) => swept(port as GraphPort<unknown>, side!));
|
|
6723
|
+
const ranked =
|
|
6724
|
+
ranks?.every((rank) => rank !== undefined) &&
|
|
6725
|
+
group.every((port) => portSettings?.(port)?.["port.index"] === undefined)
|
|
6726
|
+
? new Map(group.map((port, i) => [port, ranks[i]!]))
|
|
6727
|
+
: undefined;
|
|
6728
|
+
group.sort((left, right) =>
|
|
6729
|
+
ranked
|
|
6730
|
+
? ranked.get(left)! - ranked.get(right)!
|
|
6731
|
+
: Number(portSettings?.(left)?.["port.index"] ?? ports.indexOf(left)) -
|
|
6732
|
+
Number(portSettings?.(right)?.["port.index"] ?? ports.indexOf(right)),
|
|
6733
|
+
);
|
|
6583
6734
|
if (constraints === "FIXED_ORDER" && (side === "SOUTH" || side === "WEST")) {
|
|
6584
6735
|
group.reverse();
|
|
6585
6736
|
}
|