@statelyai/layout 0.4.0 → 0.4.2
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/dist/elkjs/index.cjs +2 -2
- 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-UciYx7lD.mjs → elkjs-CSi7oB5J.mjs} +181 -69
- package/dist/{elkjs-ggjsBuVP.cjs → elkjs-CnBvdyBu.cjs} +181 -69
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-CBPg6_6_.cjs → layered-BJu8A21s.cjs} +474 -158
- package/dist/{layered-C7ERzZjR.mjs → layered-BsAdm_u8.mjs} +469 -153
- package/dist/{layout-engine-DAw06RhN.cjs → layout-engine-BFP1TZbF.cjs} +1 -1
- package/dist/{layout-engine-DC3IYkIy.mjs → layout-engine-DnlmGxHL.mjs} +1 -1
- package/package.json +1 -1
- package/src/elkjs/index.ts +135 -33
- package/src/elkjs/layout-quality.ts +81 -33
- package/src/layered/bk-node-placement.ts +10 -7
- package/src/layered/crossing-counter.ts +27 -9
- package/src/layered/crossing-graph.ts +2 -2
- package/src/layered/greedy-switch-decider.ts +44 -17
- package/src/layered/hierarchy-sweepiness.ts +37 -41
- package/src/layered/index.ts +15 -21
- package/src/layered/loop-envelopes.ts +10 -4
- package/src/layered/node-lookup.ts +29 -0
- package/src/layered/opposing-tracks.ts +108 -8
- package/src/layered/orthogonal-hypersegments.ts +69 -7
- package/src/layered/port-distributor.ts +26 -14
- package/src/layered/port-edge-order.ts +2 -1
- package/src/layered/spacing.ts +4 -3
- package/src/layered/strategies.ts +288 -111
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./initial-model-order";
|
|
8
8
|
import { insertionSort } from "./gwt-sort";
|
|
9
9
|
import { CanonicalPortDistributor } from "./port-distributor";
|
|
10
|
-
import { countAllCrossings } from "./crossing-counter";
|
|
10
|
+
import { countAllCrossings, type CrossingGraph } from "./crossing-counter";
|
|
11
11
|
import {
|
|
12
12
|
createCrossingGraphBuilder,
|
|
13
13
|
crossingGraph,
|
|
@@ -60,7 +60,13 @@ import type { ElkLayeredOptionValueByName } from "./elk-options";
|
|
|
60
60
|
import { conservativeSpline } from "./spline-bezier";
|
|
61
61
|
import { getFlexiblePortPosition } from "./flexible-ports";
|
|
62
62
|
import { traceLayeredPhase } from "../internal/layered-trace";
|
|
63
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
assertNorthSouthPortSides,
|
|
65
|
+
GreedySwitchDecider,
|
|
66
|
+
greedySwitchTopology,
|
|
67
|
+
type GreedySwitchTopology,
|
|
68
|
+
} from "./greedy-switch-decider";
|
|
69
|
+
import { findNode } from "./node-lookup";
|
|
64
70
|
|
|
65
71
|
const modelSweepInputs = new WeakSet<LayeredPhaseInput>();
|
|
66
72
|
|
|
@@ -124,6 +130,24 @@ function cycleModelOrder(input: LayeredPhaseInput): Map<string, number> {
|
|
|
124
130
|
);
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Internal setting: how crossing minimization models ports and tracks.
|
|
135
|
+
* `mixedPorts` (default true) counts a port carrying both directions as two
|
|
136
|
+
* ports and groups its neighbours by direction. `separateSharedTracks`
|
|
137
|
+
* (default false) counts a hyperedge joining two or more ports on each side
|
|
138
|
+
* of its gap edge by edge, since routing never lets edges run opposite ways
|
|
139
|
+
* along one track. A `probe` records whether either choice can change the
|
|
140
|
+
* order. Each helps on average but steers the sweeps into worse orders on
|
|
141
|
+
* some graphs, so the elkjs facade also lays such graphs out with the
|
|
142
|
+
* alternatives and keeps the best result.
|
|
143
|
+
*/
|
|
144
|
+
export const CROSSING_MODEL = "internal.crossingModel";
|
|
145
|
+
export interface CrossingModel {
|
|
146
|
+
mixedPorts?: boolean;
|
|
147
|
+
separateSharedTracks?: boolean;
|
|
148
|
+
probe?: { mixedPorts: boolean; sharedTracks: boolean };
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
export const breakCyclesWithDepthFirstSearch: CycleBreaker = (input) => {
|
|
128
152
|
const outgoing = new Map<string, GraphEdge[]>();
|
|
129
153
|
for (const node of input.graph.nodes) outgoing.set(node.id, []);
|
|
@@ -818,14 +842,7 @@ export function applyLayerConstraintOrder(input: LayeredPhaseInput, order: Layer
|
|
|
818
842
|
...order,
|
|
819
843
|
layers: order.layers.map((layer) => {
|
|
820
844
|
const unconstrained = layer.filter((id) => !constrained.has(id));
|
|
821
|
-
|
|
822
|
-
input.settings.mergeEdges === true
|
|
823
|
-
? [
|
|
824
|
-
...unconstrained.filter((id) => id.startsWith("__layout_dummy:")),
|
|
825
|
-
...unconstrained.filter((id) => !id.startsWith("__layout_dummy:")),
|
|
826
|
-
]
|
|
827
|
-
: unconstrained;
|
|
828
|
-
return [...mergedOrder, ...layer.filter((id) => constrained.has(id))];
|
|
845
|
+
return [...unconstrained, ...layer.filter((id) => constrained.has(id))];
|
|
829
846
|
}),
|
|
830
847
|
};
|
|
831
848
|
}
|
|
@@ -1022,6 +1039,8 @@ export function applyGreedySwitch(
|
|
|
1022
1039
|
(layerIndex + 1 < layers.length ? countBoundary(layerIndex) : 0)
|
|
1023
1040
|
);
|
|
1024
1041
|
};
|
|
1042
|
+
// Switches stay within layers, so port connectivity and layers are fixed.
|
|
1043
|
+
let greedyTopology: GreedySwitchTopology | undefined;
|
|
1025
1044
|
const sweep = (forward: boolean): boolean => {
|
|
1026
1045
|
let changed = false;
|
|
1027
1046
|
const indices = forward
|
|
@@ -1038,6 +1057,7 @@ export function applyGreedySwitch(
|
|
|
1038
1057
|
greedyGraph,
|
|
1039
1058
|
layerIndex,
|
|
1040
1059
|
type === "ONE_SIDED" ? (forward ? "WEST" : "EAST") : undefined,
|
|
1060
|
+
(greedyTopology ??= greedySwitchTopology(greedyGraph)),
|
|
1041
1061
|
);
|
|
1042
1062
|
do {
|
|
1043
1063
|
improved = false;
|
|
@@ -1897,6 +1917,8 @@ export interface LayerSweepSession {
|
|
|
1897
1917
|
currentLayers(): readonly (readonly string[])[];
|
|
1898
1918
|
/** Current canonical port ids of one node, as {@link snapshot} records them. */
|
|
1899
1919
|
physicalPortOrder(nodeId: string): string[];
|
|
1920
|
+
/** Canonical crossing graph in the current order; callers must not mutate it. */
|
|
1921
|
+
crossingGraph(): CrossingGraph;
|
|
1900
1922
|
/** Live incident edge orders of one node; callers must not mutate them. */
|
|
1901
1923
|
edgeOrders(nodeId: string): { incoming: readonly string[]; outgoing: readonly string[] };
|
|
1902
1924
|
/** Same as restoring a snapshot that differs only in one layer. */
|
|
@@ -1911,7 +1933,9 @@ export interface LayerSweepSession {
|
|
|
1911
1933
|
}
|
|
1912
1934
|
|
|
1913
1935
|
function clonePortOrders(orders?: ReadonlyMap<string, readonly string[]>): Map<string, string[]> {
|
|
1914
|
-
|
|
1936
|
+
const clone = new Map<string, string[]>();
|
|
1937
|
+
if (orders) for (const [id, edges] of orders) clone.set(id, edges.slice());
|
|
1938
|
+
return clone;
|
|
1915
1939
|
}
|
|
1916
1940
|
|
|
1917
1941
|
function minimizeCrossingsWithLayerSweep(
|
|
@@ -1984,6 +2008,32 @@ export function createLayerSweepSession(
|
|
|
1984
2008
|
}
|
|
1985
2009
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
1986
2010
|
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
2011
|
+
// ELK lists a compound's boundary ports in creation order: edges on them
|
|
2012
|
+
// take their slots in that order, in model order within a port. Authored
|
|
2013
|
+
// ports already arrive in ELK's sorted order and keep their edge order.
|
|
2014
|
+
const boundaryPortIndex = (nodeId: string, edgeId: string, incoming: boolean) => {
|
|
2015
|
+
const edge = edgeById.get(edgeId)!;
|
|
2016
|
+
const name =
|
|
2017
|
+
incoming !== orientation.reversedEdgeIds.has(edgeId) ? edge.targetPort : edge.sourcePort;
|
|
2018
|
+
if (!name?.startsWith("__native_hierarchy")) return -1;
|
|
2019
|
+
return nodeById.get(nodeId)!.ports?.findIndex((port) => port.name === name) ?? -1;
|
|
2020
|
+
};
|
|
2021
|
+
for (const [orders, incoming] of [
|
|
2022
|
+
[outputPortOrder, false],
|
|
2023
|
+
[inputPortOrder, true],
|
|
2024
|
+
] as const)
|
|
2025
|
+
for (const [nodeId, edgeIds] of orders) {
|
|
2026
|
+
if (edgeIds.length < 2) continue;
|
|
2027
|
+
const slots = edgeIds.flatMap((id, slot) =>
|
|
2028
|
+
boundaryPortIndex(nodeId, id, incoming) < 0 ? [] : [slot],
|
|
2029
|
+
);
|
|
2030
|
+
const sorted = slots
|
|
2031
|
+
.map((slot) => edgeIds[slot]!)
|
|
2032
|
+
.sort(
|
|
2033
|
+
(a, b) => boundaryPortIndex(nodeId, a, incoming) - boundaryPortIndex(nodeId, b, incoming),
|
|
2034
|
+
);
|
|
2035
|
+
slots.forEach((slot, index) => (edgeIds[slot] = sorted[index]!));
|
|
2036
|
+
}
|
|
1987
2037
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
1988
2038
|
const hierarchicalNodes = new Set<string>();
|
|
1989
2039
|
const fixedOrderNodes = new Set(
|
|
@@ -2037,6 +2087,12 @@ export function createLayerSweepSession(
|
|
|
2037
2087
|
}
|
|
2038
2088
|
}
|
|
2039
2089
|
const buildCrossingGraph = createCrossingGraphBuilder(input);
|
|
2090
|
+
const crossingModel: CrossingModel =
|
|
2091
|
+
(input.settings as { [CROSSING_MODEL]?: CrossingModel })[CROSSING_MODEL] ?? {};
|
|
2092
|
+
const mixedPorts = crossingModel.mixedPorts !== false,
|
|
2093
|
+
probe = crossingModel.probe;
|
|
2094
|
+
// Reused per count: physical port positions of one node.
|
|
2095
|
+
const physicalSlot = new Map<string, number>();
|
|
2040
2096
|
const countCrossings = (candidateLayers: readonly (readonly string[])[]): number => {
|
|
2041
2097
|
if (exactPortSweep) {
|
|
2042
2098
|
const graph = buildCrossingGraph(
|
|
@@ -2045,7 +2101,27 @@ export function createLayerSweepSession(
|
|
|
2045
2101
|
inputPortOrder,
|
|
2046
2102
|
outputPortOrder,
|
|
2047
2103
|
);
|
|
2048
|
-
|
|
2104
|
+
// Edge orders rank a side's ports per direction; a fixed-side port with
|
|
2105
|
+
// a reversed edge mixes both, so count the sweep's physical port order.
|
|
2106
|
+
for (const layer of graph.layers)
|
|
2107
|
+
for (const node of layer) {
|
|
2108
|
+
const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : undefined;
|
|
2109
|
+
if (physical?.length !== node.ports.length) continue;
|
|
2110
|
+
physicalSlot.clear();
|
|
2111
|
+
for (const [index, port] of physical.entries()) physicalSlot.set(port.id, index);
|
|
2112
|
+
if (node.ports.every((port) => physicalSlot.has(port.id)))
|
|
2113
|
+
node.ports.sort((a, b) => physicalSlot.get(a.id)! - physicalSlot.get(b.id)!);
|
|
2114
|
+
}
|
|
2115
|
+
const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
|
|
2116
|
+
const score = countAllCrossings(
|
|
2117
|
+
split,
|
|
2118
|
+
crossingModel.separateSharedTracks ? "separated" : "hyperedges",
|
|
2119
|
+
);
|
|
2120
|
+
if (probe) {
|
|
2121
|
+
if (split !== graph) probe.mixedPorts = true;
|
|
2122
|
+
if (score.sharedTracks) probe.sharedTracks = true;
|
|
2123
|
+
}
|
|
2124
|
+
let total = score.total;
|
|
2049
2125
|
const nodeInfluence = Number(
|
|
2050
2126
|
input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0,
|
|
2051
2127
|
);
|
|
@@ -2151,12 +2227,13 @@ export function createLayerSweepSession(
|
|
|
2151
2227
|
!forward,
|
|
2152
2228
|
nodeRelativePortRanks,
|
|
2153
2229
|
);
|
|
2154
|
-
|
|
2155
|
-
|
|
2230
|
+
// The fixed layer's edges toward the free layer, in edge order.
|
|
2231
|
+
const byEnd = forward ? canonicalEdgesBySource : canonicalEdgesByTarget;
|
|
2232
|
+
const indices = fixedLayer.flatMap((id) => byEnd.get(id) ?? []).sort((a, b) => a - b);
|
|
2233
|
+
for (const index of indices) {
|
|
2156
2234
|
const edge = canonicalEdges[index]!;
|
|
2157
2235
|
const [source, target] = canonicalOrientedEndpoints[index]!;
|
|
2158
|
-
if (!
|
|
2159
|
-
continue;
|
|
2236
|
+
if (!freeIds.has(forward ? target : source)) continue;
|
|
2160
2237
|
const endpoint = canonicalEndpoints.get(edge.id)!;
|
|
2161
2238
|
const rank =
|
|
2162
2239
|
canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target]!;
|
|
@@ -2223,6 +2300,20 @@ export function createLayerSweepSession(
|
|
|
2223
2300
|
// and same-layer recursion must not be separated into independent sums.
|
|
2224
2301
|
for (const id of freeLayer) {
|
|
2225
2302
|
const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
|
|
2303
|
+
const visit = (edgeId: string) => {
|
|
2304
|
+
const edge = edgeById.get(edgeId)!;
|
|
2305
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2306
|
+
const neighbor = forward ? source : target;
|
|
2307
|
+
const rank = edgeRanks.get(edgeId);
|
|
2308
|
+
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2309
|
+
visits.get(id)!.push(neighbor);
|
|
2310
|
+
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
2311
|
+
};
|
|
2312
|
+
// One edge forms one group; nothing to sort.
|
|
2313
|
+
if (order.length < 2) {
|
|
2314
|
+
for (const edgeId of order) visit(edgeId);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2226
2317
|
const groups = new Map<string, string[]>();
|
|
2227
2318
|
for (const edgeId of order) {
|
|
2228
2319
|
const edge = edgeById.get(edgeId)!;
|
|
@@ -2233,25 +2324,18 @@ export function createLayerSweepSession(
|
|
|
2233
2324
|
if (group) group.push(edgeId);
|
|
2234
2325
|
else groups.set(key, [edgeId]);
|
|
2235
2326
|
}
|
|
2327
|
+
// Port sorting reorders ports, not the edges on one port; those keep
|
|
2328
|
+
// ELK's edge list order. Expanded segments keep their listed edge id.
|
|
2329
|
+
const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
|
|
2330
|
+
const rank = (edgeId: string) => listed?.get(listedEdgeId(edgeId));
|
|
2236
2331
|
for (const group of groups.values()) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2245
|
-
);
|
|
2246
|
-
for (const edgeId of group) {
|
|
2247
|
-
const edge = edgeById.get(edgeId)!;
|
|
2248
|
-
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2249
|
-
const neighbor = forward ? source : target;
|
|
2250
|
-
const rank = edgeRanks.get(edgeId);
|
|
2251
|
-
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2252
|
-
visits.get(id)!.push(neighbor);
|
|
2253
|
-
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
2254
|
-
}
|
|
2332
|
+
if (group.length > 1)
|
|
2333
|
+
group.sort(
|
|
2334
|
+
(a, b) =>
|
|
2335
|
+
(rank(a) !== undefined && rank(b) !== undefined ? rank(a)! - rank(b)! : 0) ||
|
|
2336
|
+
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2337
|
+
);
|
|
2338
|
+
for (const edgeId of group) visit(edgeId);
|
|
2255
2339
|
}
|
|
2256
2340
|
}
|
|
2257
2341
|
return { ranks, visits };
|
|
@@ -2288,6 +2372,18 @@ export function createLayerSweepSession(
|
|
|
2288
2372
|
const canonicalOrientedEndpoints = canonicalEdges.map((edge) =>
|
|
2289
2373
|
getOrientedEndpoints(edge, orientation),
|
|
2290
2374
|
);
|
|
2375
|
+
const canonicalEdgesBySource = new Map<string, number[]>(),
|
|
2376
|
+
canonicalEdgesByTarget = new Map<string, number[]>();
|
|
2377
|
+
canonicalOrientedEndpoints.forEach(([source, target], index) => {
|
|
2378
|
+
for (const [byEnd, id] of [
|
|
2379
|
+
[canonicalEdgesBySource, source],
|
|
2380
|
+
[canonicalEdgesByTarget, target],
|
|
2381
|
+
] as const) {
|
|
2382
|
+
const list = byEnd.get(id);
|
|
2383
|
+
if (list) list.push(index);
|
|
2384
|
+
else byEnd.set(id, [index]);
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2291
2387
|
const canonicalEndpoints = new Map(
|
|
2292
2388
|
canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]),
|
|
2293
2389
|
);
|
|
@@ -2316,6 +2412,13 @@ export function createLayerSweepSession(
|
|
|
2316
2412
|
});
|
|
2317
2413
|
for (const index of touched)
|
|
2318
2414
|
for (const node of canonicalGraph.layers[index]!) {
|
|
2415
|
+
// One port leaves every edge order tied, and one edge has no order.
|
|
2416
|
+
if (
|
|
2417
|
+
node.ports.length < 2 ||
|
|
2418
|
+
((outputPortOrder.get(node.id)?.length ?? 0) < 2 &&
|
|
2419
|
+
(inputPortOrder.get(node.id)?.length ?? 0) < 2)
|
|
2420
|
+
)
|
|
2421
|
+
continue;
|
|
2319
2422
|
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2320
2423
|
for (const [orders, source] of [
|
|
2321
2424
|
[outputPortOrder, true],
|
|
@@ -2386,6 +2489,7 @@ export function createLayerSweepSession(
|
|
|
2386
2489
|
fixed: readonly string[],
|
|
2387
2490
|
beyond: readonly string[] = [],
|
|
2388
2491
|
) => {
|
|
2492
|
+
if (!mixedPorts) return;
|
|
2389
2493
|
const fixedIds = new Set(fixed);
|
|
2390
2494
|
const groups = new Map<string, Array<{ id: string; reversed: boolean }>>();
|
|
2391
2495
|
for (const id of free) {
|
|
@@ -2408,6 +2512,7 @@ export function createLayerSweepSession(
|
|
|
2408
2512
|
}
|
|
2409
2513
|
if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed)))
|
|
2410
2514
|
return;
|
|
2515
|
+
if (probe) probe.mixedPorts = true;
|
|
2411
2516
|
const index = new Map(free.map((id, i) => [id, i]));
|
|
2412
2517
|
// The direction whose nodes lead further toward the start of the layer
|
|
2413
2518
|
// beyond (from the previous sweep) goes first; forward edges by default.
|
|
@@ -2638,17 +2743,27 @@ export function createLayerSweepSession(
|
|
|
2638
2743
|
shuffleFirstLayer,
|
|
2639
2744
|
sweep,
|
|
2640
2745
|
countCrossings: () => countCrossings(working),
|
|
2641
|
-
snapshot: () =>
|
|
2642
|
-
physicalPortOrderByNodeId
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2746
|
+
snapshot: () => {
|
|
2747
|
+
const physicalPortOrderByNodeId = new Map<string, string[]>();
|
|
2748
|
+
for (const [id, node] of canonicalNodes)
|
|
2749
|
+
physicalPortOrderByNodeId.set(
|
|
2750
|
+
id,
|
|
2751
|
+
node.ports.map((port) => port.id),
|
|
2752
|
+
);
|
|
2753
|
+
return {
|
|
2754
|
+
physicalPortOrderByNodeId,
|
|
2755
|
+
layers: working.map((layer) => [...layer]),
|
|
2756
|
+
inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
|
|
2757
|
+
outputPortOrderByNodeId: clonePortOrders(outputPortOrder),
|
|
2758
|
+
};
|
|
2759
|
+
},
|
|
2649
2760
|
restore,
|
|
2650
2761
|
finish,
|
|
2651
2762
|
currentLayers: () => working,
|
|
2763
|
+
crossingGraph: () => ({
|
|
2764
|
+
layers: working.map((layer) => layer.map((id) => canonicalNodes.get(id)!)),
|
|
2765
|
+
edges: canonicalGraph.edges,
|
|
2766
|
+
}),
|
|
2652
2767
|
physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
|
|
2653
2768
|
edgeOrders: (id) => ({
|
|
2654
2769
|
incoming: inputPortOrder.get(id) ?? [],
|
|
@@ -3341,7 +3456,7 @@ function nodeFlowOffset(
|
|
|
3341
3456
|
layerByNodeId: ReadonlyMap<string, number>,
|
|
3342
3457
|
margins?: { leading: number; trailing: number; maxLeading: number; maxTrailing: number },
|
|
3343
3458
|
): number {
|
|
3344
|
-
const node = input.graph.nodes
|
|
3459
|
+
const node = findNode(input.graph.nodes, id);
|
|
3345
3460
|
const alignment = node
|
|
3346
3461
|
? (input.nodeSettings?.(node)?.alignment ?? input.settings.alignment)
|
|
3347
3462
|
: input.settings.alignment;
|
|
@@ -3707,7 +3822,85 @@ function implicitEdgeEndpoints(
|
|
|
3707
3822
|
): ReadonlyMap<string, { source: Point; target: Point }> {
|
|
3708
3823
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
3709
3824
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
3710
|
-
|
|
3825
|
+
// Settings stay fixed for the call; resolve each node's and port's once.
|
|
3826
|
+
const nodeSettingsByNode = new Map<
|
|
3827
|
+
GraphNode,
|
|
3828
|
+
ReturnType<NonNullable<typeof input.nodeSettings>>
|
|
3829
|
+
>();
|
|
3830
|
+
const nodeSettingsOf = (node: GraphNode) => {
|
|
3831
|
+
if (!nodeSettingsByNode.has(node)) nodeSettingsByNode.set(node, input.nodeSettings?.(node));
|
|
3832
|
+
return nodeSettingsByNode.get(node);
|
|
3833
|
+
};
|
|
3834
|
+
// Port settings depend on the owning node too, so cache them per node.
|
|
3835
|
+
const portSettingsByNode = new Map<
|
|
3836
|
+
GraphNode,
|
|
3837
|
+
Map<GraphPort, ReturnType<NonNullable<typeof input.portSettings>>>
|
|
3838
|
+
>();
|
|
3839
|
+
const portSettingsOf = (port: GraphPort, node: GraphNode) => {
|
|
3840
|
+
let byPort = portSettingsByNode.get(node);
|
|
3841
|
+
if (!byPort) portSettingsByNode.set(node, (byPort = new Map()));
|
|
3842
|
+
if (!byPort.has(port)) byPort.set(port, input.portSettings?.(port, node));
|
|
3843
|
+
return byPort.get(port);
|
|
3844
|
+
};
|
|
3845
|
+
// Endpoint groups by side and node, in first-entry order.
|
|
3846
|
+
type EndpointGroup = {
|
|
3847
|
+
nodeId: string;
|
|
3848
|
+
side: string;
|
|
3849
|
+
entries: Array<{ edge: GraphEdge; endpoint: "source" | "target" }>;
|
|
3850
|
+
};
|
|
3851
|
+
const groups: EndpointGroup[] = [];
|
|
3852
|
+
const groupsBySide = new Map<string, Map<string, EndpointGroup>>();
|
|
3853
|
+
const modelOrderPromotion =
|
|
3854
|
+
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3855
|
+
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
3856
|
+
const endpointSide = (nodeId: string, portName: string | undefined): string | undefined => {
|
|
3857
|
+
const node = nodeById.get(nodeId);
|
|
3858
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3859
|
+
const constraints = node ? nodeSettingsOf(node)?.portConstraints : undefined;
|
|
3860
|
+
if (
|
|
3861
|
+
!node ||
|
|
3862
|
+
!port ||
|
|
3863
|
+
(constraints !== "FIXED_SIDE" &&
|
|
3864
|
+
constraints !== "FIXED_ORDER" &&
|
|
3865
|
+
constraints !== "FIXED_RATIO" &&
|
|
3866
|
+
constraints !== "FIXED_POS")
|
|
3867
|
+
) {
|
|
3868
|
+
return undefined;
|
|
3869
|
+
}
|
|
3870
|
+
const configured = portSettingsOf(port, node)?.["port.side"];
|
|
3871
|
+
return configured === "WEST" || configured === "NORTH"
|
|
3872
|
+
? "before"
|
|
3873
|
+
: configured === "EAST" || configured === "SOUTH"
|
|
3874
|
+
? "after"
|
|
3875
|
+
: undefined;
|
|
3876
|
+
};
|
|
3877
|
+
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3878
|
+
const node = nodeById.get(nodeId);
|
|
3879
|
+
const constraints = node && nodeSettingsOf(node)?.portConstraints;
|
|
3880
|
+
if (
|
|
3881
|
+
portName !== undefined ||
|
|
3882
|
+
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3883
|
+
)
|
|
3884
|
+
return undefined;
|
|
3885
|
+
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3886
|
+
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3887
|
+
return source !== directionReversed ? "after" : "before";
|
|
3888
|
+
};
|
|
3889
|
+
const addEntry = (
|
|
3890
|
+
nodeId: string,
|
|
3891
|
+
side: string,
|
|
3892
|
+
entry: { edge: GraphEdge; endpoint: "source" | "target" },
|
|
3893
|
+
) => {
|
|
3894
|
+
let byNode = groupsBySide.get(side);
|
|
3895
|
+
if (!byNode) groupsBySide.set(side, (byNode = new Map()));
|
|
3896
|
+
const group = byNode.get(nodeId);
|
|
3897
|
+
if (group) group.entries.push(entry);
|
|
3898
|
+
else {
|
|
3899
|
+
const created = { nodeId, side, entries: [entry] };
|
|
3900
|
+
byNode.set(nodeId, created);
|
|
3901
|
+
groups.push(created);
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3711
3904
|
for (const edge of input.graph.edges) {
|
|
3712
3905
|
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3713
3906
|
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
@@ -3715,8 +3908,6 @@ function implicitEdgeEndpoints(
|
|
|
3715
3908
|
if (!sourceRect || !targetRect) continue;
|
|
3716
3909
|
const sourceFlow = horizontal ? sourceRect.x : sourceRect.y;
|
|
3717
3910
|
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
3718
|
-
const modelOrderPromotion =
|
|
3719
|
-
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3720
3911
|
const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
|
|
3721
3912
|
const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
|
|
3722
3913
|
// A zero-gap boundary can coincide with the node's flow origin.
|
|
@@ -3729,40 +3920,6 @@ function implicitEdgeEndpoints(
|
|
|
3729
3920
|
: sourceFlow <= targetFlow);
|
|
3730
3921
|
const feedback =
|
|
3731
3922
|
input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
|
|
3732
|
-
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
3733
|
-
const endpointSide = (nodeId: string, portName: string | undefined): string | undefined => {
|
|
3734
|
-
const node = nodeById.get(nodeId);
|
|
3735
|
-
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3736
|
-
const constraints = node ? input.nodeSettings?.(node)?.portConstraints : undefined;
|
|
3737
|
-
if (
|
|
3738
|
-
!node ||
|
|
3739
|
-
!port ||
|
|
3740
|
-
(constraints !== "FIXED_SIDE" &&
|
|
3741
|
-
constraints !== "FIXED_ORDER" &&
|
|
3742
|
-
constraints !== "FIXED_RATIO" &&
|
|
3743
|
-
constraints !== "FIXED_POS")
|
|
3744
|
-
) {
|
|
3745
|
-
return undefined;
|
|
3746
|
-
}
|
|
3747
|
-
const configured = input.portSettings?.(port, node)?.["port.side"];
|
|
3748
|
-
return configured === "WEST" || configured === "NORTH"
|
|
3749
|
-
? "before"
|
|
3750
|
-
: configured === "EAST" || configured === "SOUTH"
|
|
3751
|
-
? "after"
|
|
3752
|
-
: undefined;
|
|
3753
|
-
};
|
|
3754
|
-
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3755
|
-
const node = nodeById.get(nodeId);
|
|
3756
|
-
const constraints = node && input.nodeSettings?.(node)?.portConstraints;
|
|
3757
|
-
if (
|
|
3758
|
-
portName !== undefined ||
|
|
3759
|
-
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3760
|
-
)
|
|
3761
|
-
return undefined;
|
|
3762
|
-
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3763
|
-
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3764
|
-
return source !== directionReversed ? "after" : "before";
|
|
3765
|
-
};
|
|
3766
3923
|
const sourceSide =
|
|
3767
3924
|
endpointSide(edge.sourceId, edge.sourcePort) ??
|
|
3768
3925
|
fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ??
|
|
@@ -3771,18 +3928,24 @@ function implicitEdgeEndpoints(
|
|
|
3771
3928
|
endpointSide(edge.targetId, edge.targetPort) ??
|
|
3772
3929
|
fixedImplicitSide(edge.targetId, edge.targetPort, false) ??
|
|
3773
3930
|
(feedback ? (directionReversed ? "after" : "before") : forward ? "before" : "after");
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
const sourceGroup = groups.get(sourceKey) ?? [];
|
|
3777
|
-
sourceGroup.push({ edge, endpoint: "source" });
|
|
3778
|
-
groups.set(sourceKey, sourceGroup);
|
|
3779
|
-
const targetGroup = groups.get(targetKey) ?? [];
|
|
3780
|
-
targetGroup.push({ edge, endpoint: "target" });
|
|
3781
|
-
groups.set(targetKey, targetGroup);
|
|
3931
|
+
addEntry(edge.sourceId, sourceSide, { edge, endpoint: "source" });
|
|
3932
|
+
addEntry(edge.targetId, targetSide, { edge, endpoint: "target" });
|
|
3782
3933
|
}
|
|
3783
3934
|
|
|
3784
3935
|
const result = new Map<string, { source: Point; target: Point }>();
|
|
3785
3936
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
3937
|
+
// First positions in swept port orders, as `indexOf` finds them.
|
|
3938
|
+
const orderIndexes = new Map<readonly string[], Map<string, number>>();
|
|
3939
|
+
const orderIndex = (list: readonly string[], id: string) => {
|
|
3940
|
+
let positions = orderIndexes.get(list);
|
|
3941
|
+
if (!positions) {
|
|
3942
|
+
positions = new Map();
|
|
3943
|
+
for (const [index, item] of list.entries())
|
|
3944
|
+
if (!positions.has(item)) positions.set(item, index);
|
|
3945
|
+
orderIndexes.set(list, positions);
|
|
3946
|
+
}
|
|
3947
|
+
return positions.get(id) ?? -1;
|
|
3948
|
+
};
|
|
3786
3949
|
const placedPorts = new Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>();
|
|
3787
3950
|
const portSideCounts = new Map<GraphNode, Map<unknown, number>>();
|
|
3788
3951
|
const portSideCount = (node: GraphNode, side: unknown): number => {
|
|
@@ -3790,22 +3953,19 @@ function implicitEdgeEndpoints(
|
|
|
3790
3953
|
if (!counts) {
|
|
3791
3954
|
counts = new Map();
|
|
3792
3955
|
for (const port of node.ports ?? []) {
|
|
3793
|
-
const portSide =
|
|
3956
|
+
const portSide = portSettingsOf(port, node)?.["port.side"];
|
|
3794
3957
|
counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
|
|
3795
3958
|
}
|
|
3796
3959
|
portSideCounts.set(node, counts);
|
|
3797
3960
|
}
|
|
3798
3961
|
return counts.get(side) ?? 0;
|
|
3799
3962
|
};
|
|
3800
|
-
for (const
|
|
3801
|
-
const split = key.lastIndexOf(":");
|
|
3802
|
-
const nodeId = key.slice(0, split);
|
|
3803
|
-
const side = key.slice(split + 1);
|
|
3963
|
+
for (const { nodeId, side, entries } of groups) {
|
|
3804
3964
|
const rect = placement.rectByNodeId.get(nodeId);
|
|
3805
3965
|
if (!rect) continue;
|
|
3806
3966
|
const mergeEdges = input.settings.mergeEdges === true;
|
|
3807
3967
|
const node = nodeById.get(nodeId);
|
|
3808
|
-
const nodeSettings = node ?
|
|
3968
|
+
const nodeSettings = node ? nodeSettingsOf(node) : undefined;
|
|
3809
3969
|
const hypernode = nodeSettings?.hypernode;
|
|
3810
3970
|
const unzipping = (input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING";
|
|
3811
3971
|
const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
@@ -3860,8 +4020,8 @@ function implicitEdgeEndpoints(
|
|
|
3860
4020
|
const selected = (
|
|
3861
4021
|
leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId
|
|
3862
4022
|
)?.get(nodeId);
|
|
3863
|
-
const a = selected
|
|
3864
|
-
b = selected
|
|
4023
|
+
const a = selected ? orderIndex(selected, left.edge.id) : -1,
|
|
4024
|
+
b = selected ? orderIndex(selected, right.edge.id) : -1;
|
|
3865
4025
|
if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
|
|
3866
4026
|
}
|
|
3867
4027
|
if (interactiveLeftSourceOrder) {
|
|
@@ -4006,7 +4166,7 @@ function implicitEdgeEndpoints(
|
|
|
4006
4166
|
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
4007
4167
|
const explicitPort = node?.ports?.find((port) => port.name === portName);
|
|
4008
4168
|
const physicalSide =
|
|
4009
|
-
node && explicitPort ?
|
|
4169
|
+
node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.side"] : undefined;
|
|
4010
4170
|
const singleFixedSidePort =
|
|
4011
4171
|
constraints === "FIXED_SIDE" &&
|
|
4012
4172
|
physicalSide !== undefined &&
|
|
@@ -4015,9 +4175,7 @@ function implicitEdgeEndpoints(
|
|
|
4015
4175
|
// Movable port ordering comes from the layer sweep above. Fixed coordinates
|
|
4016
4176
|
// and authored anchors must replace the synthesized node-origin fallback.
|
|
4017
4177
|
const authoredAnchor =
|
|
4018
|
-
node && explicitPort
|
|
4019
|
-
? input.portSettings?.(explicitPort, node)?.["port.anchor"]
|
|
4020
|
-
: undefined;
|
|
4178
|
+
node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.anchor"] : undefined;
|
|
4021
4179
|
if (
|
|
4022
4180
|
portName !== undefined &&
|
|
4023
4181
|
node &&
|
|
@@ -4346,6 +4504,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4346
4504
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
4347
4505
|
}
|
|
4348
4506
|
const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
|
|
4507
|
+
// Placed node ids per flow layer, in placement order. Layer loops only
|
|
4508
|
+
// move these nodes, so a grouping stays valid while its loop runs.
|
|
4509
|
+
const placedIdsByFlowLayer = () => {
|
|
4510
|
+
const ids = flowLayers.map(() => [] as string[]);
|
|
4511
|
+
for (const id of placement.rectByNodeId.keys()) {
|
|
4512
|
+
const layer = flowLayerByNodeId.get(id);
|
|
4513
|
+
if (layer !== undefined) ids[layer]!.push(id);
|
|
4514
|
+
}
|
|
4515
|
+
return ids;
|
|
4516
|
+
};
|
|
4349
4517
|
const labelFlowLayers = new Set(
|
|
4350
4518
|
[...flowLayerByNodeId]
|
|
4351
4519
|
.filter(([id]) => id.startsWith("__layout_dummy:label:"))
|
|
@@ -4473,10 +4641,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4473
4641
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
4474
4642
|
}
|
|
4475
4643
|
let labelShift = 0;
|
|
4644
|
+
const labelShiftIds = placedIdsByFlowLayer();
|
|
4476
4645
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
4477
4646
|
if (labelShift !== 0) {
|
|
4478
|
-
for (const
|
|
4479
|
-
|
|
4647
|
+
for (const id of labelShiftIds[layerNo]!) {
|
|
4648
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
4480
4649
|
mutableRects.set(
|
|
4481
4650
|
id,
|
|
4482
4651
|
horizontal ? { ...rect, x: rect.x + labelShift } : { ...rect, y: rect.y + labelShift },
|
|
@@ -4931,9 +5100,13 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4931
5100
|
candidate.straight = Math.abs(segment.start - segment.end) < 1e-3;
|
|
4932
5101
|
candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
|
|
4933
5102
|
if (segment.partner !== undefined) {
|
|
5103
|
+
// The route leaves the graph source on the half holding that end.
|
|
4934
5104
|
const partner = result.segments[segment.partner]!;
|
|
4935
|
-
|
|
4936
|
-
|
|
5105
|
+
const [first, second] = physicalPort(candidate, "source").graphSource
|
|
5106
|
+
? [segment, partner]
|
|
5107
|
+
: [partner, segment];
|
|
5108
|
+
candidate.slot = increasing ? first.slot : maximumSlot - first.slot;
|
|
5109
|
+
candidate.secondSlot = increasing ? second.slot : maximumSlot - second.slot;
|
|
4937
5110
|
const sourcePort = portByCandidate.get(candidate)!.source;
|
|
4938
5111
|
const offset =
|
|
4939
5112
|
(increasing ? candidate.sourceCross : candidate.targetCross) -
|
|
@@ -4964,11 +5137,13 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4964
5137
|
leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
|
|
4965
5138
|
let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
|
|
4966
5139
|
// ELK omits ordinary node spacing beside an external-port-only layer.
|
|
5140
|
+
const flowLayerIds = flowLayers.map(() => [] as string[]);
|
|
5141
|
+
for (const [id, layer] of flowLayerByNodeId) flowLayerIds[layer]!.push(id);
|
|
4967
5142
|
const externalFlowLayers = new Set(
|
|
4968
5143
|
flowLayers.flatMap((_, layer) => {
|
|
4969
|
-
const ids = [
|
|
5144
|
+
const ids = flowLayerIds[layer]!;
|
|
4970
5145
|
return ids.length &&
|
|
4971
|
-
ids.every((
|
|
5146
|
+
ids.every((id) => {
|
|
4972
5147
|
const node = nodeById.get(id);
|
|
4973
5148
|
const side = node && externalPortDummyOf(node)?.side;
|
|
4974
5149
|
return horizontal
|
|
@@ -4979,10 +5154,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4979
5154
|
: [];
|
|
4980
5155
|
}),
|
|
4981
5156
|
);
|
|
5157
|
+
const shiftedIds = placedIdsByFlowLayer();
|
|
4982
5158
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
4983
5159
|
const shift = nextStart - bounds.start;
|
|
4984
|
-
for (const
|
|
4985
|
-
|
|
5160
|
+
for (const id of shiftedIds[layerNo]!) {
|
|
5161
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
4986
5162
|
mutableRects.set(
|
|
4987
5163
|
id,
|
|
4988
5164
|
horizontal ? { ...rect, x: rect.x + shift } : { ...rect, y: rect.y + shift },
|
|
@@ -5340,10 +5516,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5340
5516
|
}
|
|
5341
5517
|
}
|
|
5342
5518
|
let nextStart = flowLayers[0]?.start ?? 0;
|
|
5519
|
+
const shiftedIds = placedIdsByFlowLayer();
|
|
5343
5520
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
5344
5521
|
const shift = nextStart - bounds.start;
|
|
5345
|
-
for (const
|
|
5346
|
-
|
|
5522
|
+
for (const id of shiftedIds[layerNo]!) {
|
|
5523
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
5347
5524
|
mutableRects.set(
|
|
5348
5525
|
id,
|
|
5349
5526
|
horizontal ? { ...rect, x: rect.x + shift } : { ...rect, y: rect.y + shift },
|
|
@@ -6677,7 +6854,7 @@ export function normalizePlacementForPortExtents(
|
|
|
6677
6854
|
[nextLayer, false],
|
|
6678
6855
|
] as const) {
|
|
6679
6856
|
for (const id of ids) {
|
|
6680
|
-
const node = input.graph.nodes
|
|
6857
|
+
const node = findNode(input.graph.nodes, id);
|
|
6681
6858
|
const rect = placement.rectByNodeId.get(id);
|
|
6682
6859
|
if (!node || !rect) continue;
|
|
6683
6860
|
const bodyStart = horizontal ? rect.x : rect.y;
|