@statelyai/layout 0.3.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 +150 -11
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +2 -0
- package/dist/elkjs/index.d.mts +2 -0
- 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 +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
- package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
- package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
- package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
- package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +2 -1
- package/dist/routing/index.d.cts +2 -2
- package/dist/routing/index.d.mts +2 -2
- package/dist/routing/index.mjs +2 -2
- package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
- package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
- package/package.json +8 -2
- package/src/authoring/coordinates.ts +28 -8
- 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 +1392 -449
- 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/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +2 -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 +35 -7
- 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 +886 -161
- 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 +13 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +32 -0
- package/src/routing/algorithms.ts +433 -21
- package/src/routing/coordination.ts +8 -3
- package/src/routing/index.ts +1 -0
- package/src/routing/search.ts +93 -11
- package/dist/elkjs-DaHMISnQ.mjs +0 -1677
- package/dist/elkjs-dMghiiWW.cjs +0 -1706
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
import { preparePortMargins, portCrossMargins } from "./node-margins";
|
|
1
2
|
/*******************************************************************************
|
|
2
3
|
* Derived from Eclipse Layout Kernel's Brandes-Koepf node placer.
|
|
3
4
|
* Copyright (c) 2015 Kiel University and others.
|
|
4
5
|
* SPDX-License-Identifier: EPL-2.0
|
|
5
6
|
*******************************************************************************/
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
8
|
+
import { getCrossingUnits } from "./crossing-constraints";
|
|
9
|
+
import type { EntityRect, GraphEdge, GraphNode } from "@statelyai/graph";
|
|
10
|
+
import { placeNodesInLayers, placePorts, usesSharedSideSlot } from "./strategies";
|
|
9
11
|
import type { LayerOrder, LayeredPhaseInput, NodePlacement } from "./types";
|
|
10
12
|
import { nodeNodeSpacing } from "./spacing";
|
|
13
|
+
import { prepareLoopEnvelopes, preparedLoopEnvelopes, recordLoopEnvelopes } from "./loop-envelopes";
|
|
14
|
+
|
|
15
|
+
import { recordPlacementCrossBounds } from "./compaction-bounds";
|
|
16
|
+
import { recordRoutingCoordinates } from "./routing-coordinates";
|
|
17
|
+
|
|
18
|
+
const beforeMargin = (input: LayeredPhaseInput, id: string) =>
|
|
19
|
+
Math.max(
|
|
20
|
+
preparedLoopEnvelopes(input)?.get(id)?.before ?? 0,
|
|
21
|
+
portCrossMargins(input, id)?.before ?? 0,
|
|
22
|
+
);
|
|
23
|
+
const afterMargin = (input: LayeredPhaseInput, id: string) =>
|
|
24
|
+
Math.max(
|
|
25
|
+
preparedLoopEnvelopes(input)?.get(id)?.after ?? 0,
|
|
26
|
+
portCrossMargins(input, id)?.after ?? 0,
|
|
27
|
+
);
|
|
11
28
|
|
|
12
29
|
type HDirection = "LEFT" | "RIGHT";
|
|
13
30
|
type VDirection = "UP" | "DOWN";
|
|
@@ -16,6 +33,7 @@ interface Alignment {
|
|
|
16
33
|
hdir: HDirection;
|
|
17
34
|
vdir: VDirection;
|
|
18
35
|
root: Map<string, string>;
|
|
36
|
+
onlyDummies: Map<string, boolean>;
|
|
19
37
|
align: Map<string, string>;
|
|
20
38
|
innerShift: Map<string, number>;
|
|
21
39
|
blockSize: Map<string, number>;
|
|
@@ -40,9 +58,35 @@ function anchorCrossSize(input: LayeredPhaseInput, id: string): number {
|
|
|
40
58
|
return id.startsWith("__layout_dummy:wrap:") ? 0 : actualCrossSize(input, id);
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
/** Per-run memo: one placement reads a fixed input. */
|
|
62
|
+
let placementMemo:
|
|
63
|
+
| { input: LayeredPhaseInput; crossSize: Map<string, number>; spacing: Map<string, number> }
|
|
64
|
+
| undefined;
|
|
65
|
+
|
|
43
66
|
function crossSize(input: LayeredPhaseInput, id: string): number {
|
|
67
|
+
if (placementMemo?.input !== input) return uncachedCrossSize(input, id);
|
|
68
|
+
let value = placementMemo.crossSize.get(id);
|
|
69
|
+
if (value === undefined) placementMemo.crossSize.set(id, (value = uncachedCrossSize(input, id)));
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function spacingBetween(input: LayeredPhaseInput, firstId: string, secondId: string): number {
|
|
74
|
+
if (placementMemo?.input !== input) return nodeNodeSpacing(input, firstId, secondId);
|
|
75
|
+
const key = `${firstId}\0${secondId}`;
|
|
76
|
+
let value = placementMemo.spacing.get(key);
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
placementMemo.spacing.set(key, (value = nodeNodeSpacing(input, firstId, secondId)));
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uncachedCrossSize(input: LayeredPhaseInput, id: string): number {
|
|
44
83
|
const value = actualCrossSize(input, id);
|
|
45
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
id.startsWith("__layout_breaking:") ||
|
|
86
|
+
id.startsWith("__layout_dummy:inverted:") ||
|
|
87
|
+
id.startsWith("__layout_dummy:north-south:")
|
|
88
|
+
)
|
|
89
|
+
return value;
|
|
46
90
|
if (value !== 0 || (!id.startsWith("__layout_dummy:") && !id.startsWith("__layout_breaking:"))) {
|
|
47
91
|
return value;
|
|
48
92
|
}
|
|
@@ -105,6 +149,7 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
105
149
|
index: number,
|
|
106
150
|
count: number,
|
|
107
151
|
side: "north" | "south" | "west" | "east",
|
|
152
|
+
widths: readonly number[] = [],
|
|
108
153
|
): number => {
|
|
109
154
|
const size = anchorCrossSize(input, id);
|
|
110
155
|
if (hasCenteredPorts(id)) return size / 2;
|
|
@@ -129,13 +174,42 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
129
174
|
const start = Math.max(0, Number(startValue ?? 0) - (Number(startValue ?? 0) > 0 ? 1 : 0));
|
|
130
175
|
const end = Math.max(0, Number(endValue ?? 0) - (Number(endValue ?? 0) > 0 ? 1 : 0));
|
|
131
176
|
const available = Math.max(0, size - start - end);
|
|
132
|
-
|
|
133
|
-
|
|
177
|
+
// ELK spaces the side's ports by their cross-axis sizes and anchors at their centers.
|
|
178
|
+
const width = (at: number) => widths[at] ?? 0;
|
|
179
|
+
let before = 0,
|
|
180
|
+
total = 0;
|
|
181
|
+
for (let at = 0; at < count; at++) {
|
|
182
|
+
if (at < index) before += width(at);
|
|
183
|
+
total += width(at);
|
|
184
|
+
}
|
|
185
|
+
const own = width(index) / 2;
|
|
186
|
+
if (alignment === "BEGIN") return start + before + own + index * spacing;
|
|
187
|
+
if (alignment === "END")
|
|
188
|
+
return start + available - (total - before - own) - (count - index - 1) * spacing;
|
|
134
189
|
if (alignment === "CENTER")
|
|
135
|
-
return
|
|
190
|
+
return (
|
|
191
|
+
start + (available - total - (count - 1) * spacing) / 2 + before + own + index * spacing
|
|
192
|
+
);
|
|
136
193
|
if (alignment === "JUSTIFIED")
|
|
137
|
-
return count === 1
|
|
138
|
-
|
|
194
|
+
return count === 1
|
|
195
|
+
? start + available / 2
|
|
196
|
+
: start + before + own + (index * (available - total)) / (count - 1);
|
|
197
|
+
return start + before + own + ((available - total) * (index + 1)) / (count + 1);
|
|
198
|
+
};
|
|
199
|
+
// Settings and sizes are fixed during placement: place each node's ports once.
|
|
200
|
+
const placedPortsByNode = new Map<GraphNode, ReturnType<typeof placePorts>>();
|
|
201
|
+
const portSideCounts = new Map<GraphNode, Map<unknown, number>>();
|
|
202
|
+
const portSideCount = (node: GraphNode, side: unknown): number => {
|
|
203
|
+
let counts = portSideCounts.get(node);
|
|
204
|
+
if (!counts) {
|
|
205
|
+
counts = new Map();
|
|
206
|
+
for (const port of node.ports ?? []) {
|
|
207
|
+
const portSide = input.portSettings?.(port, node)?.["port.side"];
|
|
208
|
+
counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
|
|
209
|
+
}
|
|
210
|
+
portSideCounts.set(node, counts);
|
|
211
|
+
}
|
|
212
|
+
return counts.get(side) ?? 0;
|
|
139
213
|
};
|
|
140
214
|
const explicitPortAnchor = (id: string, edgeId: string): number | undefined => {
|
|
141
215
|
const node = nodeById.get(id);
|
|
@@ -146,19 +220,51 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
146
220
|
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
147
221
|
if (!port) return undefined;
|
|
148
222
|
const size = input.sizes.get(id) ?? { width: 0, height: 0 };
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
223
|
+
if (!placedPortsByNode.has(node))
|
|
224
|
+
placedPortsByNode.set(
|
|
225
|
+
node,
|
|
226
|
+
placePorts(
|
|
227
|
+
node.ports,
|
|
228
|
+
{ x: 0, y: 0, ...size },
|
|
229
|
+
input.direction,
|
|
230
|
+
(candidate) => input.portSettings?.(candidate, node),
|
|
231
|
+
{ ...input.settings, ...input.nodeSettings?.(node) },
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
const placed = placedPortsByNode.get(node)?.find((candidate) => candidate.name === portName);
|
|
156
235
|
if (!placed) return undefined;
|
|
157
236
|
const anchor = input.portSettings?.(port, node)?.["port.anchor"] as
|
|
158
237
|
| { x?: number; y?: number }
|
|
159
238
|
| undefined;
|
|
160
239
|
const constraints = input.nodeSettings?.(node)?.portConstraints;
|
|
161
|
-
|
|
240
|
+
const sideEntries = right.get(id)?.some((entry) => entry.edgeId === edgeId)
|
|
241
|
+
? right.get(id)!
|
|
242
|
+
: (left.get(id) ?? []);
|
|
243
|
+
const singlePhysicalPort =
|
|
244
|
+
node.ports?.filter((candidate) => candidate.direction === port.direction).length === 1 &&
|
|
245
|
+
sideEntries.every((entry) => {
|
|
246
|
+
const connected = edgeById.get(entry.edgeId)!;
|
|
247
|
+
return (
|
|
248
|
+
(connected.sourceId === id ? connected.sourcePort : connected.targetPort) === portName
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
const physicalSide = input.portSettings?.(port, node)?.["port.side"];
|
|
252
|
+
const uniqueFixedSide =
|
|
253
|
+
constraints === "FIXED_SIDE" &&
|
|
254
|
+
physicalSide !== undefined &&
|
|
255
|
+
physicalSide !== "UNDEFINED" &&
|
|
256
|
+
node.ports !== undefined &&
|
|
257
|
+
portSideCount(node, physicalSide) === 1;
|
|
258
|
+
// A fan-out on one physical port has one anchor, irrespective of edge count.
|
|
259
|
+
// Multiple FIXED_SIDE ports still follow the crossing sweep's selected order.
|
|
260
|
+
if (
|
|
261
|
+
anchor === undefined &&
|
|
262
|
+
constraints !== "FIXED_RATIO" &&
|
|
263
|
+
constraints !== "FIXED_POS" &&
|
|
264
|
+
!uniqueFixedSide &&
|
|
265
|
+
constraints !== "FIXED_ORDER" &&
|
|
266
|
+
!singlePhysicalPort
|
|
267
|
+
) {
|
|
162
268
|
return undefined;
|
|
163
269
|
}
|
|
164
270
|
if (input.direction === "left" || input.direction === "right") {
|
|
@@ -200,10 +306,43 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
200
306
|
index: number,
|
|
201
307
|
count: number,
|
|
202
308
|
side: "north" | "south" | "west" | "east",
|
|
309
|
+
widths?: readonly number[],
|
|
203
310
|
): number =>
|
|
204
|
-
|
|
311
|
+
!customPortAnchors
|
|
205
312
|
? (anchorCrossSize(input, id) * (index + 1)) / (count + 1)
|
|
206
|
-
: (explicitPortAnchor(id, edgeId) ?? portAnchor(id, index, count, side));
|
|
313
|
+
: (explicitPortAnchor(id, edgeId) ?? portAnchor(id, index, count, side, widths));
|
|
314
|
+
/** Cross-axis sizes of a side's ports in order, when each edge there has its own port. */
|
|
315
|
+
const portWidths = (id: string, entries: readonly Neighbor[], count: number) => {
|
|
316
|
+
const node = nodeById.get(id);
|
|
317
|
+
if (!node?.ports || count !== entries.length) return undefined;
|
|
318
|
+
const names = entries.map(({ edgeId }) => {
|
|
319
|
+
const edge = edgeById.get(edgeId)!;
|
|
320
|
+
return edge.sourceId === id ? edge.sourcePort : edge.targetPort;
|
|
321
|
+
});
|
|
322
|
+
if (names.includes(undefined) || new Set(names).size !== names.length) return undefined;
|
|
323
|
+
const horizontal = input.direction === "left" || input.direction === "right";
|
|
324
|
+
return names.map((name) => {
|
|
325
|
+
const port = node.ports!.find((candidate) => candidate.name === name);
|
|
326
|
+
return (horizontal ? port?.height : port?.width) ?? 0;
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
// Routing gives fixed-side self-loop terminals on shared-slot ports a slot
|
|
330
|
+
// among the side's other edges (after them here), so anchors reserve it.
|
|
331
|
+
const loopTerminals = (id: string, side: string): number => {
|
|
332
|
+
const node = nodeById.get(id);
|
|
333
|
+
if (!node) return 0;
|
|
334
|
+
return input.graph.edges
|
|
335
|
+
.filter((edge) => edge.sourceId === id && edge.targetId === id)
|
|
336
|
+
.flatMap((edge) => [edge.sourcePort, edge.targetPort])
|
|
337
|
+
.filter((name) => {
|
|
338
|
+
const port = node.ports?.find((candidate) => candidate.name === name);
|
|
339
|
+
return (
|
|
340
|
+
port !== undefined &&
|
|
341
|
+
input.portSettings?.(port, node)?.["port.side"] === side.toUpperCase() &&
|
|
342
|
+
usesSharedSideSlot(input, node, name)
|
|
343
|
+
);
|
|
344
|
+
}).length;
|
|
345
|
+
};
|
|
207
346
|
for (const [id, entries] of right) {
|
|
208
347
|
const sweptOrder = order.outputPortOrderByNodeId?.get(id);
|
|
209
348
|
const interactiveForwardLongEdgeSource =
|
|
@@ -232,10 +371,20 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
232
371
|
(edgeModelOrder.get(leftEntry.edgeId) ?? 0) -
|
|
233
372
|
(edgeModelOrder.get(rightEntry.edgeId) ?? 0),
|
|
234
373
|
);
|
|
374
|
+
// ELK finds the first connecting edge by walking the source's port list.
|
|
375
|
+
// Parallel edges must follow the selected port order, not root edge order.
|
|
376
|
+
const firstByNeighbor = new Set<string>();
|
|
377
|
+
for (const entry of portOrder) {
|
|
378
|
+
if (firstByNeighbor.has(entry.id)) continue;
|
|
379
|
+
firstByNeighbor.add(entry.id);
|
|
380
|
+
edgeIdByNodePair.set(`${id}\0${entry.id}`, entry.edgeId);
|
|
381
|
+
}
|
|
382
|
+
const count = entries.length + loopTerminals(id, afterSide);
|
|
383
|
+
const widths = portWidths(id, portOrder, count);
|
|
235
384
|
portOrder.forEach((entry, index) => {
|
|
236
385
|
anchor.set(
|
|
237
386
|
`${entry.edgeId}:${id}`,
|
|
238
|
-
neighborAnchor(id, entry.edgeId, index,
|
|
387
|
+
neighborAnchor(id, entry.edgeId, index, count, afterSide, widths),
|
|
239
388
|
);
|
|
240
389
|
});
|
|
241
390
|
}
|
|
@@ -267,10 +416,20 @@ function buildNeighbors(input: LayeredPhaseInput, order: LayerOrder) {
|
|
|
267
416
|
(edgeModelOrder.get(rightEntry.edgeId) ?? 0) -
|
|
268
417
|
(edgeModelOrder.get(leftEntry.edgeId) ?? 0),
|
|
269
418
|
);
|
|
419
|
+
// getEdge(current, next) walks current's clockwise ports. The reverse
|
|
420
|
+
// sweep must choose from the target's order, independently of the source.
|
|
421
|
+
const firstByNeighbor = new Set<string>();
|
|
422
|
+
for (const entry of [...portOrder].reverse()) {
|
|
423
|
+
if (firstByNeighbor.has(entry.id)) continue;
|
|
424
|
+
firstByNeighbor.add(entry.id);
|
|
425
|
+
edgeIdByNodePair.set(`${id}\0${entry.id}`, entry.edgeId);
|
|
426
|
+
}
|
|
427
|
+
const count = entries.length + loopTerminals(id, beforeSide);
|
|
428
|
+
const widths = portWidths(id, portOrder, count);
|
|
270
429
|
portOrder.forEach((entry, index) => {
|
|
271
430
|
anchor.set(
|
|
272
431
|
`${entry.edgeId}:${id}`,
|
|
273
|
-
neighborAnchor(id, entry.edgeId, index,
|
|
432
|
+
neighborAnchor(id, entry.edgeId, index, count, beforeSide, widths),
|
|
274
433
|
);
|
|
275
434
|
});
|
|
276
435
|
}
|
|
@@ -282,6 +441,7 @@ function makeAlignment(hdir: HDirection, vdir: VDirection): Alignment {
|
|
|
282
441
|
hdir,
|
|
283
442
|
vdir,
|
|
284
443
|
root: new Map(),
|
|
444
|
+
onlyDummies: new Map(),
|
|
285
445
|
align: new Map(),
|
|
286
446
|
innerShift: new Map(),
|
|
287
447
|
blockSize: new Map(),
|
|
@@ -297,9 +457,14 @@ function markConflicts(
|
|
|
297
457
|
): Set<string> {
|
|
298
458
|
const marked = new Set<string>();
|
|
299
459
|
if (order.layers.length < 3) return marked;
|
|
300
|
-
const
|
|
460
|
+
const longEdge = (id: string): boolean =>
|
|
301
461
|
id.startsWith("__layout_dummy:") &&
|
|
302
|
-
|
|
462
|
+
!id.startsWith("__layout_dummy:north-south:") &&
|
|
463
|
+
!id.startsWith("__layout_dummy:label:");
|
|
464
|
+
// ELK only marks inner segments between LONG_EDGE nodes. Port helpers are
|
|
465
|
+
// separate node types and must not block valid Brandes-Koepf alignments.
|
|
466
|
+
const isInner = (id: string): boolean =>
|
|
467
|
+
longEdge(id) && (neighbors.left.get(id) ?? []).some((neighbor) => longEdge(neighbor.id));
|
|
303
468
|
for (let previousLayerNo = 1; previousLayerNo + 1 < order.layers.length; previousLayerNo++) {
|
|
304
469
|
const previousLayer = order.layers[previousLayerNo]!;
|
|
305
470
|
const currentLayer = order.layers[previousLayerNo + 1]!;
|
|
@@ -337,6 +502,7 @@ function alignBlocks(
|
|
|
337
502
|
for (const layer of order.layers) {
|
|
338
503
|
for (const id of layer) {
|
|
339
504
|
bal.root.set(id, id);
|
|
505
|
+
bal.onlyDummies.set(id, true);
|
|
340
506
|
bal.align.set(id, id);
|
|
341
507
|
bal.innerShift.set(id, 0);
|
|
342
508
|
}
|
|
@@ -363,6 +529,13 @@ function alignBlocks(
|
|
|
363
529
|
bal.align.set(neighbor.id, id);
|
|
364
530
|
bal.root.set(id, bal.root.get(neighbor.id) ?? neighbor.id);
|
|
365
531
|
bal.align.set(id, bal.root.get(id) ?? id);
|
|
532
|
+
const root = bal.root.get(id)!;
|
|
533
|
+
const longEdge =
|
|
534
|
+
getCrossingUnits(input)?.longEdgeNodes?.has(id) ??
|
|
535
|
+
(id.startsWith("__layout_dummy:") &&
|
|
536
|
+
!id.startsWith("__layout_dummy:label:") &&
|
|
537
|
+
!id.startsWith("__layout_dummy:north-south:"));
|
|
538
|
+
bal.onlyDummies.set(root, bal.onlyDummies.get(root)! && longEdge);
|
|
366
539
|
r = index;
|
|
367
540
|
}
|
|
368
541
|
}
|
|
@@ -371,8 +544,8 @@ function alignBlocks(
|
|
|
371
544
|
|
|
372
545
|
const roots = new Set(bal.root.values());
|
|
373
546
|
for (const root of roots) {
|
|
374
|
-
let above =
|
|
375
|
-
let below = crossSize(input, root);
|
|
547
|
+
let above = beforeMargin(input, root);
|
|
548
|
+
let below = crossSize(input, root) + afterMargin(input, root);
|
|
376
549
|
bal.innerShift.set(root, 0);
|
|
377
550
|
let current = root;
|
|
378
551
|
let next = bal.align.get(current) ?? root;
|
|
@@ -386,8 +559,8 @@ function alignBlocks(
|
|
|
386
559
|
: anchorCrossSize(input, next) / 2;
|
|
387
560
|
const nextShift = (bal.innerShift.get(current) ?? 0) + currentAnchor - nextAnchor;
|
|
388
561
|
bal.innerShift.set(next, nextShift);
|
|
389
|
-
above = Math.max(above, -nextShift);
|
|
390
|
-
below = Math.max(below, nextShift + crossSize(input, next));
|
|
562
|
+
above = Math.max(above, beforeMargin(input, next) - nextShift);
|
|
563
|
+
below = Math.max(below, nextShift + crossSize(input, next) + afterMargin(input, next));
|
|
391
564
|
current = next;
|
|
392
565
|
next = bal.align.get(current) ?? root;
|
|
393
566
|
}
|
|
@@ -423,10 +596,12 @@ function compact(
|
|
|
423
596
|
classIndegree.set(target, (classIndegree.get(target) ?? 0) + 1);
|
|
424
597
|
};
|
|
425
598
|
|
|
599
|
+
const threshold = createStraighteningThreshold(input, order, bal, neighbors);
|
|
426
600
|
const placeBlock = (root: string): void => {
|
|
427
601
|
if (bal.y.has(root)) return;
|
|
428
602
|
bal.y.set(root, 0);
|
|
429
603
|
let assigned = false;
|
|
604
|
+
let bound = bal.vdir === "UP" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
430
605
|
let current = root;
|
|
431
606
|
do {
|
|
432
607
|
const layerNo = neighbors.layerIndex.get(current) ?? 0;
|
|
@@ -436,31 +611,38 @@ function compact(
|
|
|
436
611
|
if (neighbor !== undefined) {
|
|
437
612
|
const neighborRoot = bal.root.get(neighbor) ?? neighbor;
|
|
438
613
|
placeBlock(neighborRoot);
|
|
614
|
+
bound = threshold.calculate(bound, root, current);
|
|
439
615
|
if (bal.sink.get(root) === root)
|
|
440
616
|
bal.sink.set(root, bal.sink.get(neighborRoot) ?? neighborRoot);
|
|
441
617
|
const rootSink = bal.sink.get(root) ?? root;
|
|
442
618
|
const neighborSink = bal.sink.get(neighborRoot) ?? neighborRoot;
|
|
443
|
-
const spacing =
|
|
619
|
+
const spacing = spacingBetween(input, current, neighbor);
|
|
444
620
|
if (rootSink === neighborSink) {
|
|
445
621
|
const candidate =
|
|
446
622
|
bal.vdir === "UP"
|
|
447
623
|
? (bal.y.get(neighborRoot) ?? 0) +
|
|
448
624
|
(bal.innerShift.get(neighbor) ?? 0) -
|
|
625
|
+
beforeMargin(input, neighbor) -
|
|
626
|
+
afterMargin(input, current) -
|
|
449
627
|
spacing -
|
|
450
628
|
crossSize(input, current) -
|
|
451
629
|
(bal.innerShift.get(current) ?? 0)
|
|
452
630
|
: (bal.y.get(neighborRoot) ?? 0) +
|
|
453
631
|
(bal.innerShift.get(neighbor) ?? 0) +
|
|
454
632
|
crossSize(input, neighbor) +
|
|
633
|
+
afterMargin(input, neighbor) +
|
|
634
|
+
beforeMargin(input, current) +
|
|
455
635
|
spacing -
|
|
456
636
|
(bal.innerShift.get(current) ?? 0);
|
|
457
637
|
bal.y.set(
|
|
458
638
|
root,
|
|
459
639
|
assigned
|
|
460
640
|
? bal.vdir === "UP"
|
|
461
|
-
? Math.min(bal.y.get(root) ?? 0, candidate)
|
|
462
|
-
: Math.max(bal.y.get(root) ?? 0, candidate)
|
|
463
|
-
:
|
|
641
|
+
? Math.min(bal.y.get(root) ?? 0, candidate, bound)
|
|
642
|
+
: Math.max(bal.y.get(root) ?? 0, candidate, bound)
|
|
643
|
+
: bal.vdir === "UP"
|
|
644
|
+
? Math.min(candidate, bound)
|
|
645
|
+
: Math.max(candidate, bound),
|
|
464
646
|
);
|
|
465
647
|
assigned = true;
|
|
466
648
|
} else {
|
|
@@ -472,20 +654,27 @@ function compact(
|
|
|
472
654
|
? (bal.y.get(root) ?? 0) +
|
|
473
655
|
(bal.innerShift.get(current) ?? 0) +
|
|
474
656
|
crossSize(input, current) +
|
|
657
|
+
afterMargin(input, current) +
|
|
658
|
+
beforeMargin(input, neighbor) +
|
|
475
659
|
classSpacing -
|
|
476
660
|
(bal.y.get(neighborRoot) ?? 0) -
|
|
477
661
|
(bal.innerShift.get(neighbor) ?? 0)
|
|
478
662
|
: (bal.y.get(root) ?? 0) +
|
|
479
663
|
(bal.innerShift.get(current) ?? 0) -
|
|
664
|
+
beforeMargin(input, current) -
|
|
665
|
+
afterMargin(input, neighbor) -
|
|
480
666
|
(bal.y.get(neighborRoot) ?? 0) -
|
|
481
667
|
(bal.innerShift.get(neighbor) ?? 0) -
|
|
482
668
|
crossSize(input, neighbor) -
|
|
483
669
|
classSpacing;
|
|
484
670
|
addClassEdge(rootSink, neighborSink, separation);
|
|
485
671
|
}
|
|
672
|
+
} else {
|
|
673
|
+
bound = threshold.calculate(bound, root, current);
|
|
486
674
|
}
|
|
487
675
|
current = bal.align.get(current) ?? root;
|
|
488
676
|
} while (current !== root);
|
|
677
|
+
threshold.finish(root);
|
|
489
678
|
};
|
|
490
679
|
|
|
491
680
|
const layers = bal.hdir === "LEFT" ? [...order.layers].reverse() : order.layers;
|
|
@@ -526,14 +715,15 @@ function compact(
|
|
|
526
715
|
bal.y.set(id, value + (bal.innerShift.get(id) ?? 0));
|
|
527
716
|
}
|
|
528
717
|
}
|
|
718
|
+
threshold.postProcess();
|
|
529
719
|
}
|
|
530
720
|
|
|
531
721
|
function extent(input: LayeredPhaseInput, bal: Alignment): [number, number] {
|
|
532
722
|
let min = Number.POSITIVE_INFINITY;
|
|
533
723
|
let max = Number.NEGATIVE_INFINITY;
|
|
534
724
|
for (const [id, value] of bal.y) {
|
|
535
|
-
min = Math.min(min, value);
|
|
536
|
-
max = Math.max(max, value + crossSize(input, id));
|
|
725
|
+
min = Math.min(min, value - beforeMargin(input, id));
|
|
726
|
+
max = Math.max(max, value + crossSize(input, id) + afterMargin(input, id));
|
|
537
727
|
}
|
|
538
728
|
return [min, max];
|
|
539
729
|
}
|
|
@@ -546,8 +736,9 @@ function preservesLayerOrder(
|
|
|
546
736
|
for (const layer of order.layers) {
|
|
547
737
|
let previousEnd = Number.NEGATIVE_INFINITY;
|
|
548
738
|
for (const id of layer) {
|
|
549
|
-
const
|
|
550
|
-
const
|
|
739
|
+
const position = positions.get(id) ?? 0;
|
|
740
|
+
const start = position - beforeMargin(input, id);
|
|
741
|
+
const end = position + crossSize(input, id) + afterMargin(input, id);
|
|
551
742
|
if (start <= previousEnd || end <= previousEnd) return false;
|
|
552
743
|
previousEnd = end;
|
|
553
744
|
}
|
|
@@ -555,78 +746,170 @@ function preservesLayerOrder(
|
|
|
555
746
|
return true;
|
|
556
747
|
}
|
|
557
748
|
|
|
558
|
-
|
|
749
|
+
/** ELK's compaction thresholds and deferred straightening share block state. */
|
|
750
|
+
function createStraighteningThreshold(
|
|
559
751
|
input: LayeredPhaseInput,
|
|
560
752
|
order: LayerOrder,
|
|
561
753
|
bal: Alignment,
|
|
562
754
|
neighbors: ReturnType<typeof buildNeighbors>,
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
(input.settings["nodePlacement.bk.edgeStraightening"] ?? "IMPROVE_STRAIGHTNESS")
|
|
566
|
-
"IMPROVE_STRAIGHTNESS"
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const nodesByRoot = new Map<string, string[]>();
|
|
755
|
+
) {
|
|
756
|
+
const enabled =
|
|
757
|
+
(input.settings["nodePlacement.bk.edgeStraightening"] ?? "IMPROVE_STRAIGHTNESS") ===
|
|
758
|
+
"IMPROVE_STRAIGHTNESS";
|
|
759
|
+
const invalid = bal.vdir === "UP" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
760
|
+
const finished = new Set<string>();
|
|
761
|
+
const used = new Set<string>();
|
|
762
|
+
const blocks = new Map<string, string[]>();
|
|
572
763
|
for (const id of neighbors.layerIndex.keys()) {
|
|
573
764
|
const root = bal.root.get(id) ?? id;
|
|
574
|
-
|
|
575
|
-
nodes.push(id);
|
|
576
|
-
nodesByRoot.set(root, nodes);
|
|
765
|
+
blocks.set(root, [...(blocks.get(root) ?? []), id]);
|
|
577
766
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
767
|
+
type Pending = { free: string; isRoot: boolean; edge?: GraphEdge; hasEdges?: boolean };
|
|
768
|
+
const queue: Pending[] = [],
|
|
769
|
+
stack: Pending[] = [];
|
|
770
|
+
const rootOf = (id: string) => bal.root.get(id) ?? id;
|
|
771
|
+
// Feedback segments retain model direction; BK traverses their layer direction.
|
|
772
|
+
// SelfLoopPreProcessor hides self loops until after node placement.
|
|
773
|
+
const edgesByFree = new Map<boolean, Map<string, GraphEdge[]>>();
|
|
774
|
+
for (const incoming of [true, false]) {
|
|
775
|
+
const byFree = new Map<string, GraphEdge[]>();
|
|
776
|
+
for (const edge of input.graph.edges) {
|
|
777
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
778
|
+
const reversed =
|
|
779
|
+
(neighbors.layerIndex.get(edge.sourceId) ?? 0) >
|
|
780
|
+
(neighbors.layerIndex.get(edge.targetId) ?? 0);
|
|
781
|
+
const free = incoming !== reversed ? edge.targetId : edge.sourceId;
|
|
782
|
+
const edges = byFree.get(free);
|
|
783
|
+
if (edges) edges.push(edge);
|
|
784
|
+
else byFree.set(free, [edge]);
|
|
785
|
+
}
|
|
786
|
+
edgesByFree.set(incoming, byFree);
|
|
787
|
+
}
|
|
788
|
+
const pick = (pending: Pending): Pending => {
|
|
789
|
+
const incoming = pending.isRoot ? bal.hdir === "RIGHT" : bal.hdir === "LEFT";
|
|
790
|
+
const ports =
|
|
791
|
+
(incoming
|
|
792
|
+
? getCrossingUnits(input)?.incomingEdgeOrderByDummyId?.get(pending.free)
|
|
793
|
+
: undefined) ??
|
|
794
|
+
(incoming ? order.inputPortOrderByNodeId : order.outputPortOrderByNodeId)?.get(pending.free);
|
|
795
|
+
const edges = [...(edgesByFree.get(incoming)!.get(pending.free) ?? [])]
|
|
796
|
+
// Edges missing from the port order (a merged dummy's in-layer parts)
|
|
797
|
+
// follow the listed edges.
|
|
798
|
+
.sort((a, b) => {
|
|
799
|
+
if (!ports) return 0;
|
|
800
|
+
const rank = (id: string) => (ports.includes(id) ? ports.indexOf(id) : ports.length);
|
|
801
|
+
return rank(a.id) - rank(b.id);
|
|
603
802
|
});
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const delta = desired - (bal.y.get(id) ?? 0);
|
|
803
|
+
// ELK initializes this flag true and changes it only on alignment.
|
|
804
|
+
// A singleton normal node therefore remains eligible for same-layer edges.
|
|
805
|
+
const onlyDummies = bal.onlyDummies.get(rootOf(pending.free)) ?? true;
|
|
806
|
+
pending.hasEdges = false;
|
|
807
|
+
pending.edge = undefined;
|
|
808
|
+
for (const edge of edges) {
|
|
611
809
|
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
810
|
+
!onlyDummies &&
|
|
811
|
+
neighbors.layerIndex.get(edge.sourceId) === neighbors.layerIndex.get(edge.targetId)
|
|
812
|
+
)
|
|
813
|
+
continue;
|
|
814
|
+
if (used.has(rootOf(pending.free))) continue;
|
|
815
|
+
pending.hasEdges = true;
|
|
816
|
+
const other = edge.sourceId === pending.free ? edge.targetId : edge.sourceId;
|
|
817
|
+
if (finished.has(rootOf(other))) {
|
|
818
|
+
pending.edge = edge;
|
|
819
|
+
return pending;
|
|
620
820
|
}
|
|
621
821
|
}
|
|
622
|
-
|
|
822
|
+
return pending;
|
|
823
|
+
};
|
|
824
|
+
const anchor = (edge: GraphEdge, id: string) =>
|
|
825
|
+
neighbors.anchor.get(`${edge.id}:${id}`) ?? crossSize(input, id) / 2;
|
|
826
|
+
const bound = (free: string, isRoot: boolean): number => {
|
|
827
|
+
const pending = pick({ free, isRoot });
|
|
828
|
+
if (!pending.edge) {
|
|
829
|
+
if (pending.hasEdges) queue.push(pending);
|
|
830
|
+
return invalid;
|
|
831
|
+
}
|
|
832
|
+
const edge = pending.edge;
|
|
833
|
+
const other = edge.sourceId === free ? edge.targetId : edge.sourceId;
|
|
834
|
+
const threshold =
|
|
835
|
+
(bal.y.get(rootOf(other)) ?? 0) +
|
|
836
|
+
(bal.innerShift.get(other) ?? 0) +
|
|
837
|
+
anchor(edge, other) -
|
|
838
|
+
(bal.innerShift.get(free) ?? 0) -
|
|
839
|
+
anchor(edge, free);
|
|
840
|
+
used.add(rootOf(edge.sourceId));
|
|
841
|
+
used.add(rootOf(edge.targetId));
|
|
842
|
+
return threshold;
|
|
843
|
+
};
|
|
844
|
+
const calculate = (old: number, root: string, current: string): number => {
|
|
845
|
+
if (!enabled) return invalid;
|
|
846
|
+
const isRoot = root === current,
|
|
847
|
+
isLast = bal.align.get(current) === root;
|
|
848
|
+
if (!isRoot && !isLast) return old;
|
|
849
|
+
let value = old;
|
|
850
|
+
if (isRoot) value = bound(root, true);
|
|
851
|
+
if (!Number.isFinite(value) && isLast) value = bound(current, false);
|
|
852
|
+
return value;
|
|
853
|
+
};
|
|
854
|
+
const process = (pending: Pending): boolean => {
|
|
855
|
+
const edge = pending.edge!;
|
|
856
|
+
const free = pending.free;
|
|
857
|
+
const other = edge.sourceId === free ? edge.targetId : edge.sourceId;
|
|
858
|
+
const delta =
|
|
859
|
+
(bal.y.get(other) ?? 0) + anchor(edge, other) - (bal.y.get(free) ?? 0) - anchor(edge, free);
|
|
860
|
+
if (!Number.isFinite(delta) || delta === 0) return false;
|
|
861
|
+
let available = Math.abs(delta);
|
|
862
|
+
for (const id of blocks.get(rootOf(free)) ?? []) {
|
|
863
|
+
const layer = order.layers[neighbors.layerIndex.get(id) ?? 0] ?? [];
|
|
864
|
+
const index = neighbors.nodeIndex.get(id) ?? 0;
|
|
865
|
+
const adjacent = layer[index + (delta < 0 ? -1 : 1)];
|
|
866
|
+
if (adjacent === undefined) continue;
|
|
867
|
+
const gap =
|
|
868
|
+
delta < 0
|
|
869
|
+
? (bal.y.get(id) ?? 0) - (bal.y.get(adjacent) ?? 0) - crossSize(input, adjacent)
|
|
870
|
+
: (bal.y.get(adjacent) ?? 0) - (bal.y.get(id) ?? 0) - crossSize(input, id);
|
|
871
|
+
const margins =
|
|
872
|
+
delta < 0
|
|
873
|
+
? beforeMargin(input, id) + afterMargin(input, adjacent)
|
|
874
|
+
: afterMargin(input, id) + beforeMargin(input, adjacent);
|
|
875
|
+
available = Math.min(available, gap - margins - spacingBetween(input, id, adjacent));
|
|
876
|
+
}
|
|
877
|
+
for (const id of blocks.get(rootOf(free)) ?? [])
|
|
878
|
+
bal.y.set(id, (bal.y.get(id) ?? 0) + (delta < 0 ? -available : available));
|
|
879
|
+
return available > 0;
|
|
880
|
+
};
|
|
881
|
+
return {
|
|
882
|
+
calculate,
|
|
883
|
+
finish: (root: string) => finished.add(root),
|
|
884
|
+
postProcess: () => {
|
|
885
|
+
if (!enabled) return;
|
|
886
|
+
for (const pending of queue) {
|
|
887
|
+
pick(pending);
|
|
888
|
+
if (!pending.edge) continue;
|
|
889
|
+
if (!process(pending)) stack.push(pending);
|
|
890
|
+
}
|
|
891
|
+
while (stack.length) process(stack.pop()!);
|
|
892
|
+
},
|
|
893
|
+
};
|
|
623
894
|
}
|
|
624
895
|
|
|
625
|
-
/** ELK-compatible Brandes-Koepf placement
|
|
896
|
+
/** ELK-compatible Brandes-Koepf placement with port-aware compaction. */
|
|
626
897
|
export function placeNodesWithBrandesKoepf(
|
|
627
898
|
input: LayeredPhaseInput,
|
|
628
899
|
order: LayerOrder,
|
|
629
900
|
): NodePlacement {
|
|
901
|
+
const previous = placementMemo;
|
|
902
|
+
placementMemo = { input, crossSize: new Map(), spacing: new Map() };
|
|
903
|
+
try {
|
|
904
|
+
return placeNodes(input, order);
|
|
905
|
+
} finally {
|
|
906
|
+
placementMemo = previous;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function placeNodes(input: LayeredPhaseInput, order: LayerOrder): NodePlacement {
|
|
911
|
+
preparePortMargins(input);
|
|
912
|
+
const envelopes = prepareLoopEnvelopes(input);
|
|
630
913
|
const base = placeNodesInLayers(input, order);
|
|
631
914
|
const neighbors = buildNeighbors(input, order);
|
|
632
915
|
const markedEdges = markConflicts(order, neighbors);
|
|
@@ -650,7 +933,6 @@ export function placeNodesWithBrandesKoepf(
|
|
|
650
933
|
const layout = makeAlignment(hdir, vdir);
|
|
651
934
|
alignBlocks(input, order, layout, neighbors, markedEdges);
|
|
652
935
|
compact(input, order, layout, neighbors);
|
|
653
|
-
improveEdgeStraightness(input, order, layout, neighbors);
|
|
654
936
|
return layout;
|
|
655
937
|
});
|
|
656
938
|
const smallestFeasibleLayout = (): Alignment => {
|
|
@@ -696,7 +978,7 @@ export function placeNodesWithBrandesKoepf(
|
|
|
696
978
|
} else {
|
|
697
979
|
positions = smallestFeasibleLayout().y;
|
|
698
980
|
}
|
|
699
|
-
const minimum = Math.min(...positions.
|
|
981
|
+
const minimum = Math.min(...[...positions].map(([id, value]) => value - beforeMargin(input, id)));
|
|
700
982
|
const crossPadding =
|
|
701
983
|
input.direction === "left" || input.direction === "right"
|
|
702
984
|
? input.padding.top
|
|
@@ -711,5 +993,34 @@ export function placeNodesWithBrandesKoepf(
|
|
|
711
993
|
: { ...rect, x: cross },
|
|
712
994
|
);
|
|
713
995
|
}
|
|
714
|
-
|
|
996
|
+
const placement = { rectByNodeId };
|
|
997
|
+
recordRoutingCoordinates(placement, {
|
|
998
|
+
rectByNodeId: new Map(
|
|
999
|
+
[...rectByNodeId].map(([id, rect]) => [
|
|
1000
|
+
id,
|
|
1001
|
+
input.direction === "left" || input.direction === "right"
|
|
1002
|
+
? { ...rect, y: positions.get(id) ?? 0 }
|
|
1003
|
+
: { ...rect, x: positions.get(id) ?? 0 },
|
|
1004
|
+
]),
|
|
1005
|
+
),
|
|
1006
|
+
});
|
|
1007
|
+
// ELK's layer-size phase measures the final node of each ordered layer,
|
|
1008
|
+
// including its dummy thickness and trailing margin. Retain this extent
|
|
1009
|
+
// before routing restoration removes zero-size port and long-edge helpers.
|
|
1010
|
+
const horizontal = input.direction === "left" || input.direction === "right";
|
|
1011
|
+
recordPlacementCrossBounds(placement, {
|
|
1012
|
+
axis: horizontal ? "y" : "x",
|
|
1013
|
+
maximum: Math.max(
|
|
1014
|
+
0,
|
|
1015
|
+
...order.layers.flatMap((layer) => {
|
|
1016
|
+
const id = layer.at(-1);
|
|
1017
|
+
const rect = id === undefined ? undefined : rectByNodeId.get(id);
|
|
1018
|
+
return rect && id !== undefined
|
|
1019
|
+
? [(horizontal ? rect.y : rect.x) + crossSize(input, id) + afterMargin(input, id)]
|
|
1020
|
+
: [];
|
|
1021
|
+
}),
|
|
1022
|
+
),
|
|
1023
|
+
});
|
|
1024
|
+
recordLoopEnvelopes(placement, new Set(envelopes.keys()));
|
|
1025
|
+
return placement;
|
|
715
1026
|
}
|