@statelyai/layout 0.2.0 → 0.4.0
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/NOTICE.md +4 -2
- package/README.md +221 -13
- package/dist/elkjs/index.cjs +3 -2
- package/dist/elkjs/index.d.cts +13 -2
- package/dist/elkjs/index.d.mts +13 -2
- package/dist/elkjs/index.mjs +3 -3
- package/dist/elkjs/main.cjs +2 -2
- package/dist/elkjs/main.d.cts +2 -2
- package/dist/elkjs/main.d.mts +2 -2
- package/dist/elkjs/main.mjs +2 -2
- package/dist/elkjs/worker-api.cjs +1 -1
- package/dist/elkjs/worker-api.d.cts +2 -2
- package/dist/elkjs/worker-api.d.mts +2 -2
- package/dist/elkjs/worker-api.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/index-B6cWNrMw.d.cts +80 -0
- package/dist/index-CJQLUKD9.d.mts +80 -0
- package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
- package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
- package/dist/index.cjs +39 -9
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -3
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +3 -2
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
- package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
- package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +29 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-CR8flv8b.mjs +2780 -0
- package/dist/routing-QIdGGFBe.cjs +2996 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
- package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
- package/package.json +14 -3
- package/src/authoring/coordinates.ts +38 -10
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1397 -452
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/routes.ts +147 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +8 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +445 -0
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +911 -261
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +34 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +143 -0
- package/src/routing/algorithms.ts +1088 -0
- package/src/routing/coordination.ts +91 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +69 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +358 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
- package/dist/elkjs-CFlfkEBz.mjs +0 -1528
- /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
- /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
- /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
- /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2016, 2020 Kiel University and others.
|
|
3
|
+
* Native adaptation of ELK AbstractBarycenterPortDistributor,
|
|
4
|
+
* NodeRelativePortDistributor and LayerTotalPortDistributor.
|
|
5
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
6
|
+
*/
|
|
7
|
+
import type { CrossingGraph, CrossingNode, CrossingPort } from "./crossing-counter";
|
|
8
|
+
|
|
9
|
+
export interface PortDistributionState {
|
|
10
|
+
ranks: Record<string, number>;
|
|
11
|
+
barycenters: Record<string, number>;
|
|
12
|
+
positions: Record<string, number>;
|
|
13
|
+
}
|
|
14
|
+
export interface PortDistributionOptions {
|
|
15
|
+
nodeRelative: boolean;
|
|
16
|
+
fixedOrder: ReadonlySet<string>;
|
|
17
|
+
hierarchical: ReadonlySet<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Stateful clockwise port distribution across successive layer sweeps. */
|
|
21
|
+
export class CanonicalPortDistributor {
|
|
22
|
+
readonly state: PortDistributionState;
|
|
23
|
+
private topologyGraph?: CrossingGraph;
|
|
24
|
+
private owner = new Map<string, CrossingNode>();
|
|
25
|
+
private layerByPort = new Map<string, number>();
|
|
26
|
+
private nodes = new Map<string, CrossingNode>();
|
|
27
|
+
private incoming = new Map<string, string[]>();
|
|
28
|
+
private outgoing = new Map<string, string[]>();
|
|
29
|
+
constructor(graph: CrossingGraph, state?: PortDistributionState) {
|
|
30
|
+
this.state = state ? structuredClone(state) : { ranks: {}, barycenters: {}, positions: {} };
|
|
31
|
+
for (const layer of graph.layers)
|
|
32
|
+
for (const [position, node] of layer.entries()) {
|
|
33
|
+
this.state.positions[node.id] ??= position;
|
|
34
|
+
for (const port of node.ports) {
|
|
35
|
+
this.state.ranks[port.id] ??= 0;
|
|
36
|
+
this.state.barycenters[port.id] ??= 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private prepareTopology(graph: CrossingGraph): void {
|
|
42
|
+
// Sweeps reorder nodes within their layers; connectivity stays constant.
|
|
43
|
+
// A restored graph object invalidates the topology once, not per layer.
|
|
44
|
+
if (this.topologyGraph !== graph) {
|
|
45
|
+
this.topologyGraph = graph;
|
|
46
|
+
this.owner.clear();
|
|
47
|
+
this.layerByPort.clear();
|
|
48
|
+
this.nodes.clear();
|
|
49
|
+
this.incoming.clear();
|
|
50
|
+
this.outgoing.clear();
|
|
51
|
+
for (const [layer, members] of graph.layers.entries())
|
|
52
|
+
for (const node of members) {
|
|
53
|
+
this.nodes.set(node.id, node);
|
|
54
|
+
for (const port of node.ports) {
|
|
55
|
+
this.owner.set(port.id, node);
|
|
56
|
+
this.layerByPort.set(port.id, layer);
|
|
57
|
+
this.incoming.set(port.id, []);
|
|
58
|
+
this.outgoing.set(port.id, []);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const edge of graph.edges) {
|
|
62
|
+
this.incoming.get(edge.target)!.push(edge.source);
|
|
63
|
+
this.outgoing.get(edge.source)!.push(edge.target);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
calculatePortRanks(
|
|
69
|
+
graph: CrossingGraph,
|
|
70
|
+
layer: readonly CrossingNode[],
|
|
71
|
+
input: boolean,
|
|
72
|
+
nodeRelative: boolean,
|
|
73
|
+
): void {
|
|
74
|
+
this.prepareTopology(graph);
|
|
75
|
+
let consumed = 0;
|
|
76
|
+
for (const node of layer) {
|
|
77
|
+
const ports = node.ports.filter(
|
|
78
|
+
(port) => (input ? this.incoming : this.outgoing).get(port.id)!.length > 0,
|
|
79
|
+
);
|
|
80
|
+
const increment = nodeRelative ? 1 / (ports.length + 1) : 1;
|
|
81
|
+
if (input) {
|
|
82
|
+
let north = consumed + ports.filter((port) => port.side === "NORTH").length * increment;
|
|
83
|
+
let rest = nodeRelative ? consumed + 1 - increment : consumed + ports.length;
|
|
84
|
+
for (const port of ports) {
|
|
85
|
+
this.state.ranks[port.id] = port.side === "NORTH" ? north : rest;
|
|
86
|
+
if (port.side === "NORTH") north -= increment;
|
|
87
|
+
else rest -= increment;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
let position = consumed + increment;
|
|
91
|
+
for (const port of ports) {
|
|
92
|
+
this.state.ranks[port.id] = position;
|
|
93
|
+
position += increment;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
consumed += nodeRelative ? 1 : ports.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
distribute(
|
|
101
|
+
graph: CrossingGraph,
|
|
102
|
+
index: number,
|
|
103
|
+
forward: boolean,
|
|
104
|
+
options: PortDistributionOptions,
|
|
105
|
+
): void {
|
|
106
|
+
this.prepareTopology(graph);
|
|
107
|
+
const { owner, layerByPort, nodes, incoming, outgoing } = this;
|
|
108
|
+
const degree = (port: string) => incoming.get(port)!.length + outgoing.get(port)!.length;
|
|
109
|
+
const ranks = (layer: CrossingNode[], input: boolean) =>
|
|
110
|
+
this.calculatePortRanks(graph, layer, input, options.nodeRelative);
|
|
111
|
+
const distributeNode = (node: CrossingNode, side: "EAST" | "WEST") => {
|
|
112
|
+
if (options.fixedOrder.has(node.id)) return;
|
|
113
|
+
const layerIndex = node.ports.length ? layerByPort.get(node.ports[0]!.id)! : index;
|
|
114
|
+
const size = graph.layers[layerIndex]!.length;
|
|
115
|
+
for (const face of [side, "SOUTH", "NORTH"] as const) {
|
|
116
|
+
let minimum = 0,
|
|
117
|
+
maximum = 0;
|
|
118
|
+
const inLayer: CrossingPort[] = [];
|
|
119
|
+
for (const port of node.ports.filter((port) => port.side === face)) {
|
|
120
|
+
let sum = 0;
|
|
121
|
+
const cross = port.side === "NORTH" || port.side === "SOUTH";
|
|
122
|
+
if (cross) {
|
|
123
|
+
if (!port.dummy) continue;
|
|
124
|
+
const dummy = nodes.get(port.dummy)!;
|
|
125
|
+
let input = false,
|
|
126
|
+
output = false;
|
|
127
|
+
for (const peer of dummy.ports) {
|
|
128
|
+
if (peer.origin !== port.id) continue;
|
|
129
|
+
if (outgoing.get(peer.id)!.length) output = true;
|
|
130
|
+
else if (incoming.get(peer.id)!.length) input = true;
|
|
131
|
+
}
|
|
132
|
+
const position = this.state.positions[dummy.id]!;
|
|
133
|
+
const large = 2 * size + 1;
|
|
134
|
+
if (input && !output) sum += port.side === "NORTH" ? -position : large - position;
|
|
135
|
+
else if (output && !input) sum += position + 1;
|
|
136
|
+
else if (input && output) sum += port.side === "NORTH" ? 0 : large / 2;
|
|
137
|
+
} else {
|
|
138
|
+
const peers = [...outgoing.get(port.id)!, ...incoming.get(port.id)!];
|
|
139
|
+
if (peers.some((peer) => layerByPort.get(peer) === layerIndex)) {
|
|
140
|
+
inLayer.push(port);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
for (const peer of outgoing.get(port.id)!) sum += this.state.ranks[peer]!;
|
|
144
|
+
for (const peer of incoming.get(port.id)!) sum -= this.state.ranks[peer]!;
|
|
145
|
+
}
|
|
146
|
+
if (degree(port.id)) {
|
|
147
|
+
const value = sum / degree(port.id);
|
|
148
|
+
this.state.barycenters[port.id] = value;
|
|
149
|
+
minimum = Math.min(minimum, value);
|
|
150
|
+
maximum = Math.max(maximum, value);
|
|
151
|
+
} else if (cross) this.state.barycenters[port.id] = sum;
|
|
152
|
+
}
|
|
153
|
+
for (const port of inLayer) {
|
|
154
|
+
const peers = [...incoming.get(port.id)!, ...outgoing.get(port.id)!].filter(
|
|
155
|
+
(peer) => layerByPort.get(peer) === layerIndex,
|
|
156
|
+
);
|
|
157
|
+
const value =
|
|
158
|
+
peers.reduce((sum, peer) => sum + this.state.positions[owner.get(peer)!.id]! + 1, 0) /
|
|
159
|
+
peers.length;
|
|
160
|
+
const above = value < this.state.positions[node.id]! + 1;
|
|
161
|
+
this.state.barycenters[port.id] =
|
|
162
|
+
port.side === "EAST"
|
|
163
|
+
? above
|
|
164
|
+
? minimum - value
|
|
165
|
+
: maximum + (size + 1 - value)
|
|
166
|
+
: above
|
|
167
|
+
? maximum + value
|
|
168
|
+
: minimum - (size + 1 - value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const sideRank = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 };
|
|
172
|
+
node.ports.sort((a, b) => {
|
|
173
|
+
const side = sideRank[a.side] - sideRank[b.side];
|
|
174
|
+
if (side) return side;
|
|
175
|
+
const left = this.state.barycenters[a.id]!,
|
|
176
|
+
right = this.state.barycenters[b.id]!;
|
|
177
|
+
return left === 0 && right === 0 ? 0 : left === 0 ? -1 : right === 0 ? 1 : left - right;
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const free = graph.layers[index]!;
|
|
181
|
+
for (const [position, node] of free.entries()) this.state.positions[node.id] = position;
|
|
182
|
+
const side = forward ? "WEST" : "EAST";
|
|
183
|
+
const fixed = graph.layers[index + (forward ? -1 : 1)];
|
|
184
|
+
if (fixed) {
|
|
185
|
+
ranks(fixed, !forward);
|
|
186
|
+
for (const node of free) distributeNode(node, side);
|
|
187
|
+
ranks(free, forward);
|
|
188
|
+
for (const node of fixed)
|
|
189
|
+
if (!options.hierarchical.has(node.id)) distributeNode(node, forward ? "EAST" : "WEST");
|
|
190
|
+
} else for (const node of free) distributeNode(node, side);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GraphEdge } from "@statelyai/graph";
|
|
2
|
+
import type { LayeredPhaseInput } from "./types";
|
|
3
|
+
|
|
4
|
+
/** ELK traverses connected edges through the node's port list, preserving each port's edge order. */
|
|
5
|
+
export function connectedEdgesInPortOrder(input: LayeredPhaseInput, nodeId: string): GraphEdge[] {
|
|
6
|
+
const node = input.graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
7
|
+
const portRank = new Map(node?.ports?.map((port, index) => [port.name, index]));
|
|
8
|
+
const explicitCount = portRank.size;
|
|
9
|
+
return input.graph.edges
|
|
10
|
+
.map((edge, index) => ({ edge, index }))
|
|
11
|
+
.filter(({ edge }) => edge.sourceId === nodeId || edge.targetId === nodeId)
|
|
12
|
+
.sort((a, b) => {
|
|
13
|
+
const rank = ({ edge, index }: { edge: GraphEdge; index: number }) => {
|
|
14
|
+
const port = edge.sourceId === nodeId ? edge.sourcePort : edge.targetPort;
|
|
15
|
+
return (port === undefined ? undefined : portRank.get(port)) ?? explicitCount + index;
|
|
16
|
+
};
|
|
17
|
+
return rank(a) - rank(b) || a.index - b.index;
|
|
18
|
+
})
|
|
19
|
+
.map(({ edge }) => edge);
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { worldGeometry } from "../authoring/coordinates";
|
|
2
|
+
import { routeToPoints } from "../routing/adapters";
|
|
3
|
+
import { recordRouteGeometry } from "../routing/layout-cache";
|
|
4
|
+
import type { CompoundVisualGraph } from "./compound";
|
|
5
|
+
import type { LayeredLayoutOptions } from "./types";
|
|
6
|
+
|
|
7
|
+
/** Run a replacement on finalized geometry, with no initial route hints. */
|
|
8
|
+
export function replaceLayoutRouting<N, E, G, P>(
|
|
9
|
+
graph: CompoundVisualGraph<N, E, G, P>,
|
|
10
|
+
options: LayeredLayoutOptions,
|
|
11
|
+
): CompoundVisualGraph<N, E, G, P> {
|
|
12
|
+
if (!options.routing) return graph;
|
|
13
|
+
const { compoundRoutes: _routes, compoundGeometry: _geometry, ...geometry } = graph;
|
|
14
|
+
const world = worldGeometry(geometry);
|
|
15
|
+
const input = {
|
|
16
|
+
...geometry,
|
|
17
|
+
nodes: world.nodes.map((n) => ({ ...n, ports: n.ports?.map((p) => ({ ...p })) })),
|
|
18
|
+
edges: world.edges.map(({ points: _points, routing: _routing, ...e }) => e),
|
|
19
|
+
};
|
|
20
|
+
const snapshot = options.routing.strategy.route(input, {
|
|
21
|
+
...options.routing.settings,
|
|
22
|
+
coordinateSpace: "world",
|
|
23
|
+
});
|
|
24
|
+
if (snapshot instanceof Promise)
|
|
25
|
+
throw new TypeError(
|
|
26
|
+
"Layered route replacement must be synchronous; run async routing separately",
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
snapshot.routes.size !== graph.edges.length ||
|
|
30
|
+
graph.edges.some((e) => !snapshot.routes.has(e.id))
|
|
31
|
+
)
|
|
32
|
+
throw new Error("Replacement router must return exactly one route for every edge");
|
|
33
|
+
const result = {
|
|
34
|
+
...graph,
|
|
35
|
+
edgeCoordinateSpace: "world" as const,
|
|
36
|
+
edges: graph.edges.map((edge) => ({
|
|
37
|
+
...edge,
|
|
38
|
+
points: routeToPoints(snapshot.routes.get(edge.id)!),
|
|
39
|
+
routing: "polyline" as const,
|
|
40
|
+
})),
|
|
41
|
+
compoundRoutes: snapshot.routes,
|
|
42
|
+
};
|
|
43
|
+
recordRouteGeometry(result, result.compoundRoutes);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { NodePlacement } from "./types";
|
|
2
|
+
|
|
3
|
+
// ELK routes in the node placer's coordinate space, before graph-padding normalization.
|
|
4
|
+
const coordinateSpaces = new WeakMap<
|
|
5
|
+
NodePlacement,
|
|
6
|
+
{ raw: NodePlacement; normalized: NodePlacement }
|
|
7
|
+
>();
|
|
8
|
+
|
|
9
|
+
export function recordRoutingCoordinates(placement: NodePlacement, raw: NodePlacement): void {
|
|
10
|
+
coordinateSpaces.set(placement, {
|
|
11
|
+
raw,
|
|
12
|
+
normalized: { rectByNodeId: new Map(placement.rectByNodeId) },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function routingCoordinates(placement: NodePlacement): NodePlacement {
|
|
17
|
+
const spaces = coordinateSpaces.get(placement);
|
|
18
|
+
if (!spaces) return placement;
|
|
19
|
+
return {
|
|
20
|
+
rectByNodeId: new Map(
|
|
21
|
+
[...placement.rectByNodeId].map(([id, rect]) => {
|
|
22
|
+
const original = spaces.normalized.rectByNodeId.get(id),
|
|
23
|
+
raw = spaces.raw.rectByNodeId.get(id);
|
|
24
|
+
if (!original || !raw) return [id, rect];
|
|
25
|
+
return [
|
|
26
|
+
id,
|
|
27
|
+
{ ...rect, x: raw.x + (rect.x - original.x), y: raw.y + (rect.y - original.y) },
|
|
28
|
+
];
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -108,10 +108,31 @@ export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
|
108
108
|
right.length - left.length,
|
|
109
109
|
)[0];
|
|
110
110
|
if (!routeTrack) return [];
|
|
111
|
+
// Long-edge joining can retain several collinear points through a label
|
|
112
|
+
// dummy. They form one track; moving only one fragment creates diagonals
|
|
113
|
+
// at the retained junctions.
|
|
114
|
+
const pointCross = (point: Point) => (horizontalFlow ? point.y : point.x);
|
|
115
|
+
const pointFlow = (point: Point) => (horizontalFlow ? point.x : point.y);
|
|
116
|
+
let startIndex = routeTrack.startIndex;
|
|
117
|
+
let endIndex = startIndex + 1;
|
|
118
|
+
while (startIndex > 0 && pointCross(edge.points[startIndex - 1]!) === routeTrack.cross)
|
|
119
|
+
startIndex--;
|
|
120
|
+
while (
|
|
121
|
+
endIndex + 1 < edge.points.length &&
|
|
122
|
+
pointCross(edge.points[endIndex + 1]!) === routeTrack.cross
|
|
123
|
+
)
|
|
124
|
+
endIndex++;
|
|
125
|
+
const trackPoints = edge.points.slice(startIndex, endIndex + 1);
|
|
111
126
|
return [
|
|
112
127
|
{
|
|
113
128
|
edge,
|
|
114
|
-
exteriorTrack:
|
|
129
|
+
exteriorTrack: {
|
|
130
|
+
...routeTrack,
|
|
131
|
+
startIndex,
|
|
132
|
+
endIndex,
|
|
133
|
+
flowStart: Math.min(...trackPoints.map(pointFlow)),
|
|
134
|
+
flowEnd: Math.max(...trackPoints.map(pointFlow)),
|
|
135
|
+
},
|
|
115
136
|
preserveAnchors: exteriorTrack === undefined,
|
|
116
137
|
labelRect,
|
|
117
138
|
lowSide:
|
|
@@ -222,17 +243,14 @@ export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
|
222
243
|
else edge.x += totalDelta;
|
|
223
244
|
if (!exteriorTrack) continue;
|
|
224
245
|
edge.points = edge.points.flatMap((point, index) => {
|
|
225
|
-
if (index
|
|
226
|
-
return [point];
|
|
246
|
+
if (index < exteriorTrack.startIndex || index > exteriorTrack.endIndex) return [point];
|
|
227
247
|
const shifted = horizontalFlow
|
|
228
248
|
? { ...point, y: point.y + totalDelta }
|
|
229
249
|
: { ...point, x: point.x + totalDelta };
|
|
230
250
|
// Keep authored endpoint anchors attached when the chosen track ends there.
|
|
231
251
|
return preserveAnchors && index === exteriorTrack.startIndex && index === 0
|
|
232
252
|
? [point, shifted]
|
|
233
|
-
: preserveAnchors &&
|
|
234
|
-
index === exteriorTrack.startIndex + 1 &&
|
|
235
|
-
index === edge.points.length - 1
|
|
253
|
+
: preserveAnchors && index === exteriorTrack.endIndex && index === edge.points.length - 1
|
|
236
254
|
? [shifted, point]
|
|
237
255
|
: [shifted];
|
|
238
256
|
});
|
package/src/layered/spacing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { externalPortDummyOf } from "./external-port-dummy";
|
|
1
2
|
import type { LayeredPhaseInput } from "./types";
|
|
2
3
|
|
|
3
4
|
export type IndividualSpacing = Readonly<Record<string, number>>;
|
|
@@ -10,20 +11,41 @@ export function nodeNodeSpacing(
|
|
|
10
11
|
): number {
|
|
11
12
|
const firstBreakingPoint = firstId.startsWith("__layout_breaking:");
|
|
12
13
|
const secondBreakingPoint = secondId.startsWith("__layout_breaking:");
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const firstLabel = firstId.startsWith("__layout_dummy:label:");
|
|
15
|
+
const secondLabel = secondId.startsWith("__layout_dummy:label:");
|
|
16
|
+
const firstNode = input.graph.nodes.find((node) => node.id === firstId);
|
|
17
|
+
const secondNode = input.graph.nodes.find((node) => node.id === secondId);
|
|
18
|
+
const firstExternal = firstNode !== undefined && externalPortDummyOf(firstNode) !== undefined;
|
|
19
|
+
const secondExternal = secondNode !== undefined && externalPortDummyOf(secondNode) !== undefined;
|
|
20
|
+
const firstDummy = firstId.startsWith("__layout_dummy:") || firstBreakingPoint || firstExternal;
|
|
21
|
+
const secondDummy =
|
|
22
|
+
secondId.startsWith("__layout_dummy:") || secondBreakingPoint || secondExternal;
|
|
15
23
|
const spacingName =
|
|
16
|
-
|
|
17
|
-
? "spacing.
|
|
18
|
-
:
|
|
19
|
-
? "spacing.
|
|
20
|
-
:
|
|
24
|
+
firstExternal && secondExternal
|
|
25
|
+
? "spacing.portPort"
|
|
26
|
+
: (firstExternal && secondLabel) || (secondExternal && firstLabel)
|
|
27
|
+
? "spacing.labelPortVertical"
|
|
28
|
+
: firstLabel && secondLabel
|
|
29
|
+
? "spacing.edgeEdge"
|
|
30
|
+
: (firstLabel && !secondDummy) || (secondLabel && !firstDummy)
|
|
31
|
+
? "spacing.node"
|
|
32
|
+
: (firstLabel && secondDummy) || (secondLabel && firstDummy)
|
|
33
|
+
? "spacing.edgeNode"
|
|
34
|
+
: firstDummy && secondDummy && firstBreakingPoint === secondBreakingPoint
|
|
35
|
+
? "spacing.edgeEdge"
|
|
36
|
+
: firstDummy || secondDummy
|
|
37
|
+
? "spacing.edgeNode"
|
|
38
|
+
: "spacing.node";
|
|
21
39
|
let spacing =
|
|
22
|
-
spacingName === "spacing.
|
|
23
|
-
? Number(input.settings["spacing.
|
|
24
|
-
: spacingName === "spacing.
|
|
25
|
-
? Number(input.settings["spacing.
|
|
26
|
-
:
|
|
40
|
+
spacingName === "spacing.portPort"
|
|
41
|
+
? Number(input.settings["spacing.portPort"] ?? 10)
|
|
42
|
+
: spacingName === "spacing.labelPortVertical"
|
|
43
|
+
? Number(input.settings["spacing.labelPortVertical"] ?? 1)
|
|
44
|
+
: spacingName === "spacing.edgeEdge"
|
|
45
|
+
? Number(input.settings["spacing.edgeEdge"] ?? 10)
|
|
46
|
+
: spacingName === "spacing.edgeNode"
|
|
47
|
+
? Number(input.settings["spacing.edgeNode"] ?? 10)
|
|
48
|
+
: input.spacing.node;
|
|
27
49
|
for (const id of [firstId, secondId]) {
|
|
28
50
|
const node = input.graph.nodes.find((candidate) => candidate.id === id);
|
|
29
51
|
if (!node) continue;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { GraphEdge } from "@statelyai/graph";
|
|
2
|
+
import type { PortEdgeList } from "./elk-port-lists";
|
|
3
|
+
import type { LongEdgeExpansion } from "./long-edges";
|
|
4
|
+
import type { LayeredPhaseInput } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ELK's LongEdgeSplitter appends each dummy to the next layer while visiting
|
|
8
|
+
* the current layer's nodes, their ports and each port's outgoing edges. That
|
|
9
|
+
* insertion order is the initial crossing-minimization order, so seed it here.
|
|
10
|
+
*/
|
|
11
|
+
export function withLongEdgeSplitterOrder(
|
|
12
|
+
unsplit: LayeredPhaseInput,
|
|
13
|
+
expansion: LongEdgeExpansion,
|
|
14
|
+
baseOrder: readonly string[],
|
|
15
|
+
portLists: ReadonlyMap<string, readonly PortEdgeList[]>,
|
|
16
|
+
): LongEdgeExpansion {
|
|
17
|
+
const { input, orientation, assignment } = expansion;
|
|
18
|
+
const authoredIds = new Set(unsplit.graph.nodes.map((node) => node.id));
|
|
19
|
+
const dummyIds = new Set(
|
|
20
|
+
input.graph.nodes.map((node) => node.id).filter((id) => !authoredIds.has(id)),
|
|
21
|
+
);
|
|
22
|
+
if (dummyIds.size === 0) return expansion;
|
|
23
|
+
const layerOf = (id: string) => assignment.layerByNodeId.get(id) ?? 0;
|
|
24
|
+
const placed = new Set<string>();
|
|
25
|
+
const layers: string[][] = [];
|
|
26
|
+
for (const id of [...baseOrder, ...authoredIds]) {
|
|
27
|
+
if (placed.has(id) || !authoredIds.has(id)) continue;
|
|
28
|
+
placed.add(id);
|
|
29
|
+
(layers[layerOf(id)] ??= []).push(id);
|
|
30
|
+
}
|
|
31
|
+
const reversed = (edge: GraphEdge) => orientation.reversedEdgeIds.has(edge.id);
|
|
32
|
+
const outgoing = new Map<string, GraphEdge[]>();
|
|
33
|
+
for (const edge of input.graph.edges) {
|
|
34
|
+
const source = reversed(edge) ? edge.targetId : edge.sourceId;
|
|
35
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
36
|
+
if (!outgoing.has(source)) outgoing.set(source, []);
|
|
37
|
+
outgoing.get(source)!.push(edge);
|
|
38
|
+
}
|
|
39
|
+
// Visit ports and their outgoing edges in ELK's simulated list order, which
|
|
40
|
+
// records reversal history. Expanded segment ids keep the original edge id.
|
|
41
|
+
const original = (id: string) => id.split("::")[0]!;
|
|
42
|
+
const targetsInPortOrder = (id: string): string[] => {
|
|
43
|
+
const edges = outgoing.get(id) ?? [];
|
|
44
|
+
const lists = portLists.get(id);
|
|
45
|
+
if (!lists) return edges.map((edge) => (reversed(edge) ? edge.sourceId : edge.targetId));
|
|
46
|
+
const rank = new Map<string, number>();
|
|
47
|
+
for (const port of lists) for (const edgeId of port.outgoing) rank.set(edgeId, rank.size);
|
|
48
|
+
return edges
|
|
49
|
+
.map((edge, index) => ({
|
|
50
|
+
edge,
|
|
51
|
+
index,
|
|
52
|
+
rank: rank.get(original(edge.id)) ?? rank.size + index,
|
|
53
|
+
}))
|
|
54
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
55
|
+
.map(({ edge }) => (reversed(edge) ? edge.sourceId : edge.targetId));
|
|
56
|
+
};
|
|
57
|
+
for (let index = 0; index < layers.length; index++) {
|
|
58
|
+
const layer = layers[index] ?? [];
|
|
59
|
+
for (let position = 0; position < layer.length; position++) {
|
|
60
|
+
for (const target of targetsInPortOrder(layer[position]!)) {
|
|
61
|
+
if (!dummyIds.has(target) || placed.has(target) || layerOf(target) !== index + 1) continue;
|
|
62
|
+
placed.add(target);
|
|
63
|
+
(layers[index + 1] ??= []).push(target);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const seedOrder = layers.flatMap((layer) => layer ?? []);
|
|
68
|
+
for (const id of dummyIds) if (!placed.has(id)) seedOrder.push(id);
|
|
69
|
+
return { ...expansion, assignment: { ...assignment, seedOrder } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Each split re-targets a long edge through a new segment appended to the
|
|
74
|
+
* target port's incoming edges. Long edges therefore follow retained short
|
|
75
|
+
* edges, in the order of their final split.
|
|
76
|
+
*/
|
|
77
|
+
export function incomingAfterLongEdgeSplitting(
|
|
78
|
+
lists: ReadonlyMap<string, readonly PortEdgeList[]>,
|
|
79
|
+
expansion: LongEdgeExpansion,
|
|
80
|
+
): Map<string, PortEdgeList[]> {
|
|
81
|
+
const { input, orientation, assignment } = expansion;
|
|
82
|
+
const seed = new Map((assignment.seedOrder ?? []).map((id, index) => [id, index]));
|
|
83
|
+
const listed = (id: string) => id.replace(/::segment:\d+$/, "");
|
|
84
|
+
const finalSplit = new Map<string, number>();
|
|
85
|
+
for (const edge of input.graph.edges) {
|
|
86
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
87
|
+
const [source, target] = reversed
|
|
88
|
+
? [edge.targetId, edge.sourceId]
|
|
89
|
+
: [edge.sourceId, edge.targetId];
|
|
90
|
+
const at = seed.get(source);
|
|
91
|
+
if (source.startsWith("__layout_dummy:") && at !== undefined)
|
|
92
|
+
finalSplit.set(`${target}\0${listed(edge.id)}`, at);
|
|
93
|
+
}
|
|
94
|
+
return new Map(
|
|
95
|
+
[...lists].map(([id, ports]) => [
|
|
96
|
+
id,
|
|
97
|
+
ports.map((port) => ({
|
|
98
|
+
...port,
|
|
99
|
+
incoming: port.incoming
|
|
100
|
+
.map((edgeId, index) => ({ edgeId, index, split: finalSplit.get(`${id}\0${edgeId}`) }))
|
|
101
|
+
.sort((a, b) => (a.split ?? -1) - (b.split ?? -1) || a.index - b.index)
|
|
102
|
+
.map(({ edgeId }) => edgeId),
|
|
103
|
+
})),
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
}
|