@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
package/src/layered/index.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
|
+
import { inheritCycleRandom } from "./cycle-random";
|
|
2
|
+
import {
|
|
3
|
+
prepareHierarchicalPortConstraints,
|
|
4
|
+
sizeHierarchicalPortDummies,
|
|
5
|
+
} from "./hierarchical-port-phases";
|
|
6
|
+
import { restoreHierarchicalPorts } from "./hierarchical-port-restoration";
|
|
7
|
+
import { countAllCrossings } from "./crossing-counter";
|
|
8
|
+
import { crossingGraph } from "./crossing-graph";
|
|
9
|
+
import { sortInitialModelOrder, markInitialModelOrderPrepared } from "./initial-model-order";
|
|
10
|
+
import { hasMovableLoopPorts } from "./loop-envelopes";
|
|
11
|
+
import { fixedSelfLoopLabels, fixedSelfLoopSide } from "./fixed-self-loop";
|
|
12
|
+
import { compactionBounds, placementCrossBounds } from "./compaction-bounds";
|
|
13
|
+
import { setElkjs0111ResultPolicy } from "../internal/elkjs-compatibility";
|
|
14
|
+
import { traceLayeredPhase } from "../internal/layered-trace";
|
|
15
|
+
import { networkSimplexComponents } from "./network-simplex";
|
|
16
|
+
import { incomingAfterLongEdgeSplitting, withLongEdgeSplitterOrder } from "./splitter-order";
|
|
17
|
+
import {
|
|
18
|
+
edgeListRanks,
|
|
19
|
+
elkComponentOrder,
|
|
20
|
+
insertLabelDummyPorts,
|
|
21
|
+
simulatePortEdgeLists,
|
|
22
|
+
type PortEdgeList,
|
|
23
|
+
} from "./elk-port-lists";
|
|
24
|
+
import { importsHyperedges, mergeHyperedgeDummies } from "./hyperedge-dummy-merger";
|
|
25
|
+
import {
|
|
26
|
+
applyOrderHints,
|
|
27
|
+
applyPlacementHints,
|
|
28
|
+
hintLayerConstraint,
|
|
29
|
+
layoutWithHints,
|
|
30
|
+
} from "./hints";
|
|
31
|
+
import { getCrossingUnits, recordCrossingUnits } from "./crossing-constraints";
|
|
32
|
+
import { externalPortDummyOf } from "./external-port-dummy";
|
|
33
|
+
import type { EntityRect } from "@statelyai/graph";
|
|
34
|
+
import { setPlacementOrientation, setPlacementOrder } from "./placement-orientation";
|
|
35
|
+
import { replaceLayoutRouting } from "./replace-routing";
|
|
1
36
|
import { layoutCompounds, type CompoundVisualGraph } from "./compound";
|
|
37
|
+
import { separateLayoutOpposingTracks } from "./opposing-tracks";
|
|
38
|
+
import { repairFlatRouting } from "./native-routing";
|
|
2
39
|
import type { Graph, GraphEdge, GraphNode, Point, VisualGraph, VisualNode } from "@statelyai/graph";
|
|
3
40
|
import { labelReferences, runPartialLayout } from "../authoring/partial";
|
|
4
41
|
import { UnsupportedLayoutError } from "../errors";
|
|
5
42
|
import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
|
|
6
43
|
import { separateExteriorLabels } from "./separate-exterior-labels";
|
|
44
|
+
import { placeEndLabels } from "./end-labels";
|
|
45
|
+
|
|
7
46
|
import {
|
|
8
47
|
assignLayersByLongestPath,
|
|
9
48
|
assignLayersByLongestPathToSink,
|
|
@@ -12,7 +51,6 @@ import {
|
|
|
12
51
|
assignLayersInteractively,
|
|
13
52
|
assignLayersWithCoffmanGraham,
|
|
14
53
|
applyLayerConstraints,
|
|
15
|
-
applyLayerConstraintOrientation,
|
|
16
54
|
applyPartitionOrientation,
|
|
17
55
|
applyPartitions,
|
|
18
56
|
applyLayerConstraintOrder,
|
|
@@ -32,6 +70,9 @@ import {
|
|
|
32
70
|
breakCyclesWithModelOrderBreadthFirstSearch,
|
|
33
71
|
breakCyclesWithDepthFirstSearch,
|
|
34
72
|
getPolylineMidpoint,
|
|
73
|
+
getPortPoint,
|
|
74
|
+
removeRouteSpurs,
|
|
75
|
+
simplifyRoute,
|
|
35
76
|
getOrientedPortDirection,
|
|
36
77
|
minimizeCrossingsWithBarycenter,
|
|
37
78
|
minimizeCrossingsWithMedian,
|
|
@@ -45,17 +86,36 @@ import {
|
|
|
45
86
|
routeEdgesWithPolylines,
|
|
46
87
|
routeEdgesWithSplines,
|
|
47
88
|
} from "./strategies";
|
|
48
|
-
import type {
|
|
89
|
+
import type {
|
|
90
|
+
AcyclicOrientation,
|
|
91
|
+
LayerAssignment,
|
|
92
|
+
LayerOrder,
|
|
93
|
+
LayeredLayoutOptions,
|
|
94
|
+
LayeredPhaseInput,
|
|
95
|
+
NodeSize,
|
|
96
|
+
} from "./types";
|
|
49
97
|
import { assignLayersWithNetworkSimplex } from "./network-simplex";
|
|
50
98
|
import { assignLayersWithMinWidth } from "./min-width";
|
|
51
99
|
import { assignLayersWithStretchWidth } from "./stretch-width";
|
|
100
|
+
import {
|
|
101
|
+
insertCenterLabelDummies,
|
|
102
|
+
selectCenterLabelSides,
|
|
103
|
+
centerLabelPosition,
|
|
104
|
+
removeCenterLabelJunctions,
|
|
105
|
+
composeCenterLabelExpansion,
|
|
106
|
+
} from "./center-labels";
|
|
107
|
+
import { switchCenterLabelDummies } from "./label-dummy-switcher";
|
|
108
|
+
import { insertNorthSouthPortDummies, restoreNorthSouthPortRoutes } from "./north-south-ports";
|
|
109
|
+
import { insertInvertedPortDummies } from "./inverted-ports";
|
|
52
110
|
import { joinLongEdgeRoutes, splitLongEdges } from "./long-edges";
|
|
53
111
|
import { unzipLayersAlternating } from "./layer-unzipping";
|
|
54
112
|
import { placeNodesWithBrandesKoepf } from "./bk-node-placement";
|
|
113
|
+
import { breakCyclesWithConstraints } from "./constraint-orientation";
|
|
55
114
|
import { placeNodesWithLinearSegments } from "./linear-segments-node-placement";
|
|
56
115
|
import { placeNodesWithNetworkSimplex } from "./network-simplex-node-placement";
|
|
57
116
|
import { applyHighDegreeNodeTreatment } from "./high-degree";
|
|
58
117
|
import { applyNodePromotion } from "./node-promotion";
|
|
118
|
+
import { straightenFeedbackChains } from "./feedback-chains";
|
|
59
119
|
import {
|
|
60
120
|
foldMultiEdgeBreakingPoints,
|
|
61
121
|
insertMultiEdgeBreakingPoints,
|
|
@@ -664,7 +724,9 @@ function runWrappedMultiEdgePipeline<N, E, G, P>(
|
|
|
664
724
|
{ layerByNodeId: rank },
|
|
665
725
|
cuts,
|
|
666
726
|
);
|
|
667
|
-
const brokenExpansion =
|
|
727
|
+
const brokenExpansion = insertInvertedPortDummies(
|
|
728
|
+
splitLongEdges(prepared.input, prepared.orientation, prepared.assignment),
|
|
729
|
+
);
|
|
668
730
|
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
669
731
|
const crossingMinimizer =
|
|
670
732
|
crossingStrategy === "MEDIAN_LAYER_SWEEP"
|
|
@@ -694,6 +756,7 @@ function runWrappedMultiEdgePipeline<N, E, G, P>(
|
|
|
694
756
|
),
|
|
695
757
|
);
|
|
696
758
|
const folded = foldMultiEdgeBreakingPoints(brokenExpansion, crossedOrder, prepared);
|
|
759
|
+
setPlacementOrientation(folded.expansion.input, folded.expansion.orientation);
|
|
697
760
|
const placementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
698
761
|
const placement =
|
|
699
762
|
placementStrategy === "INTERACTIVE"
|
|
@@ -1100,6 +1163,17 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1100
1163
|
if ((options.settings?.["considerModelOrder.components"] ?? "NONE") === "NONE") {
|
|
1101
1164
|
components.sort((left, right) => left.area - right.area || left.modelOrder - right.modelOrder);
|
|
1102
1165
|
}
|
|
1166
|
+
// The component holding the flow's start (a first-layer node, else the largest)
|
|
1167
|
+
// packs first, so no other component precedes that start along the flow.
|
|
1168
|
+
const constrainedFirst = (component: (typeof components)[number]) =>
|
|
1169
|
+
component.result.nodes.some((node) => {
|
|
1170
|
+
const constraint = options.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
1171
|
+
return constraint === "FIRST" || constraint === "FIRST_SEPARATE";
|
|
1172
|
+
});
|
|
1173
|
+
const start =
|
|
1174
|
+
components.find(constrainedFirst) ??
|
|
1175
|
+
components.toSorted((left, right) => right.result.nodes.length - left.result.nodes.length)[0];
|
|
1176
|
+
if (start) components.unshift(...components.splice(components.indexOf(start), 1));
|
|
1103
1177
|
const componentSpacing = Number(
|
|
1104
1178
|
options.settings?.["spacing.componentComponent"] ??
|
|
1105
1179
|
options.settings?.["spacing.baseValue"] ??
|
|
@@ -1132,6 +1206,7 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1132
1206
|
const placedShapes: Array<{ left: number; right: number; top: number; bottom: number }> = [];
|
|
1133
1207
|
const nodeResults = new Map<string, VisualNode<N, P>>();
|
|
1134
1208
|
const edgeResults = new Map<string, (typeof components)[number]["result"]["edges"][number]>();
|
|
1209
|
+
const placements: Array<{ component: (typeof components)[number]; x: number; y: number }> = [];
|
|
1135
1210
|
for (const component of components) {
|
|
1136
1211
|
if (x > 0 && x + component.bounds.width > maxRowWidth) {
|
|
1137
1212
|
x = 0;
|
|
@@ -1152,17 +1227,30 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1152
1227
|
}
|
|
1153
1228
|
compactedY = Math.min(y, requiredY);
|
|
1154
1229
|
}
|
|
1155
|
-
const
|
|
1156
|
-
const dy = padding.top + compactedY - component.bounds.top;
|
|
1157
|
-
for (const node of component.result.nodes) {
|
|
1158
|
-
nodeResults.set(node.id, { ...node, x: (node.x ?? 0) + dx, y: (node.y ?? 0) + dy });
|
|
1230
|
+
for (const node of component.result.nodes)
|
|
1159
1231
|
placedShapes.push({
|
|
1160
1232
|
left: x + (node.x ?? 0) - component.bounds.left,
|
|
1161
1233
|
right: x + (node.x ?? 0) - component.bounds.left + (node.width ?? 0),
|
|
1162
1234
|
top: compactedY + (node.y ?? 0) - component.bounds.top,
|
|
1163
1235
|
bottom: compactedY + (node.y ?? 0) - component.bounds.top + (node.height ?? 0),
|
|
1164
1236
|
});
|
|
1165
|
-
}
|
|
1237
|
+
placements.push({ component, x, y: compactedY });
|
|
1238
|
+
x += component.bounds.width + componentSpacing;
|
|
1239
|
+
rowHeight = Math.max(rowHeight, component.bounds.height);
|
|
1240
|
+
}
|
|
1241
|
+
// Components are packed in reading order. For UP and LEFT layouts, mirror the
|
|
1242
|
+
// packing along the flow so later components follow the first one's flow
|
|
1243
|
+
// instead of preceding it (e.g. an isolated state below an UP statechart's start).
|
|
1244
|
+
const direction = options.direction ?? graph.direction ?? "right";
|
|
1245
|
+
const packedWidth = Math.max(0, ...placements.map((p) => p.x + p.component.bounds.width));
|
|
1246
|
+
const packedHeight = Math.max(0, ...placements.map((p) => p.y + p.component.bounds.height));
|
|
1247
|
+
for (const { component, x: packedX, y: packedY } of placements) {
|
|
1248
|
+
const left = direction === "left" ? packedWidth - packedX - component.bounds.width : packedX;
|
|
1249
|
+
const top = direction === "up" ? packedHeight - packedY - component.bounds.height : packedY;
|
|
1250
|
+
const dx = padding.left + left - component.bounds.left;
|
|
1251
|
+
const dy = padding.top + top - component.bounds.top;
|
|
1252
|
+
for (const node of component.result.nodes)
|
|
1253
|
+
nodeResults.set(node.id, { ...node, x: (node.x ?? 0) + dx, y: (node.y ?? 0) + dy });
|
|
1166
1254
|
for (const edge of component.result.edges) {
|
|
1167
1255
|
edgeResults.set(edge.id, {
|
|
1168
1256
|
...edge,
|
|
@@ -1171,8 +1259,6 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1171
1259
|
points: edge.points?.map((point) => ({ x: point.x + dx, y: point.y + dy })),
|
|
1172
1260
|
});
|
|
1173
1261
|
}
|
|
1174
|
-
x += component.bounds.width + componentSpacing;
|
|
1175
|
-
rowHeight = Math.max(rowHeight, component.bounds.height);
|
|
1176
1262
|
}
|
|
1177
1263
|
|
|
1178
1264
|
return {
|
|
@@ -1209,6 +1295,52 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1209
1295
|
options: LayeredLayoutOptions,
|
|
1210
1296
|
context?: LayoutExecutionContext,
|
|
1211
1297
|
): VisualGraph<N, E, G, P> {
|
|
1298
|
+
const pipeline = createLayeredScopePipeline(graph, options, context);
|
|
1299
|
+
const prepared = pipeline.next();
|
|
1300
|
+
if (prepared.done) return prepared.value;
|
|
1301
|
+
const completed = pipeline.next(prepared.value.minimize());
|
|
1302
|
+
if (!completed.done) throw new Error("Layered scope yielded more than one crossing phase");
|
|
1303
|
+
return completed.value;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** @internal A prepared native scope, before crossing, placement or routing. */
|
|
1307
|
+
export interface LayeredCrossingPhase {
|
|
1308
|
+
input: LayeredPhaseInput;
|
|
1309
|
+
orientation: AcyclicOrientation;
|
|
1310
|
+
assignment: LayerAssignment;
|
|
1311
|
+
/** Refresh compound body dimensions after child placement, before this scope resumes. */
|
|
1312
|
+
updateNodeSize(id: string, size: NodeSize): void;
|
|
1313
|
+
finishOrder(order: LayerOrder): LayerOrder;
|
|
1314
|
+
/** Run this scope's standalone minimizer, including authored ordering policies. */
|
|
1315
|
+
minimize(): LayerOrder;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* @internal Suspend a native scope at crossing minimization. A hierarchy
|
|
1320
|
+
* coordinator can prepare every scope, share sweep state, then resume each
|
|
1321
|
+
* scope with its final order. The ordinary entry point drains this same pipeline.
|
|
1322
|
+
*/
|
|
1323
|
+
export function* createLayeredScopePipeline<N, E, G, P>(
|
|
1324
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
1325
|
+
options: LayeredLayoutOptions,
|
|
1326
|
+
context?: LayoutExecutionContext,
|
|
1327
|
+
): Generator<LayeredCrossingPhase, VisualGraph<N, E, G, P>, LayerOrder> {
|
|
1328
|
+
if ((options.settings?.["spacing.edgeEdge"] ?? 10) < 2) {
|
|
1329
|
+
options = { ...options, settings: { ...options.settings, "spacing.edgeEdge": 2 } };
|
|
1330
|
+
}
|
|
1331
|
+
if (options.hints?.length) {
|
|
1332
|
+
const { hints, nodeSettings } = options;
|
|
1333
|
+
options = {
|
|
1334
|
+
...options,
|
|
1335
|
+
nodeSettings: (node) => {
|
|
1336
|
+
const base = nodeSettings?.(node);
|
|
1337
|
+
const constraint = hintLayerConstraint(hints, node);
|
|
1338
|
+
return constraint && base?.["layering.layerConstraint"] === undefined
|
|
1339
|
+
? { ...base, "layering.layerConstraint": constraint }
|
|
1340
|
+
: base;
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1212
1344
|
if (options.settings?.noLayout) {
|
|
1213
1345
|
return {
|
|
1214
1346
|
...graph,
|
|
@@ -1346,8 +1478,11 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1346
1478
|
);
|
|
1347
1479
|
})();
|
|
1348
1480
|
const orientation = measure("cycle-breaking", () =>
|
|
1349
|
-
applyPartitionOrientation(input,
|
|
1481
|
+
applyPartitionOrientation(input, breakCyclesWithConstraints(input, cycleBreaker)),
|
|
1350
1482
|
);
|
|
1483
|
+
const portLists = simulatePortEdgeLists(input, orientation);
|
|
1484
|
+
const hyperedges = importsHyperedges(input);
|
|
1485
|
+
traceLayeredPhase(() => ({ kind: "port-lists", scope: graph.id, ports: portLists }));
|
|
1351
1486
|
const layeringStrategy = options.settings?.["layering.strategy"] ?? "NETWORK_SIMPLEX";
|
|
1352
1487
|
const layerAssigner = (() => {
|
|
1353
1488
|
if (options.strategies?.assignLayers) return options.strategies.assignLayers;
|
|
@@ -1364,20 +1499,249 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1364
1499
|
`Layering strategy ${layeringStrategy} is not implemented yet`,
|
|
1365
1500
|
);
|
|
1366
1501
|
})();
|
|
1367
|
-
const
|
|
1502
|
+
const labels = measure("center-label-preprocessing", () =>
|
|
1503
|
+
insertCenterLabelDummies(input, orientation),
|
|
1504
|
+
);
|
|
1505
|
+
let phaseInput = labels.input;
|
|
1506
|
+
const phaseOrientation = labels.orientation;
|
|
1507
|
+
// ELK's layer-constraint preprocessor detaches separate external boundaries.
|
|
1508
|
+
// Their incident edges must not stretch unrelated internal components.
|
|
1509
|
+
const separateBoundaryIds = new Set(
|
|
1510
|
+
phaseInput.graph.nodes
|
|
1511
|
+
.filter((node) => {
|
|
1512
|
+
if (!externalPortDummyOf(node)) return false;
|
|
1513
|
+
const constraint = phaseInput.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
1514
|
+
return constraint === "FIRST_SEPARATE" || constraint === "LAST_SEPARATE";
|
|
1515
|
+
})
|
|
1516
|
+
.map((node) => node.id),
|
|
1517
|
+
);
|
|
1518
|
+
const boundaryConnections = new Map<string, Set<string>>();
|
|
1519
|
+
for (const edge of phaseInput.graph.edges) {
|
|
1520
|
+
const hiddenId = separateBoundaryIds.has(edge.sourceId)
|
|
1521
|
+
? edge.sourceId
|
|
1522
|
+
: separateBoundaryIds.has(edge.targetId)
|
|
1523
|
+
? edge.targetId
|
|
1524
|
+
: undefined;
|
|
1525
|
+
if (hiddenId === undefined) continue;
|
|
1526
|
+
const oppositeId = edge.sourceId === hiddenId ? edge.targetId : edge.sourceId;
|
|
1527
|
+
if (separateBoundaryIds.has(oppositeId)) continue;
|
|
1528
|
+
const hiddenNode = phaseInput.graph.nodes.find((node) => node.id === hiddenId)!;
|
|
1529
|
+
const connections = boundaryConnections.get(oppositeId) ?? new Set<string>();
|
|
1530
|
+
connections.add(String(phaseInput.nodeSettings?.(hiddenNode)?.["layering.layerConstraint"]));
|
|
1531
|
+
boundaryConnections.set(oppositeId, connections);
|
|
1532
|
+
}
|
|
1533
|
+
const inferredConstraints = new Map<string, "FIRST" | "LAST">();
|
|
1534
|
+
for (const [id, connections] of boundaryConnections) {
|
|
1535
|
+
const node = phaseInput.graph.nodes.find((node) => node.id === id)!;
|
|
1536
|
+
if (
|
|
1537
|
+
phaseInput.nodeSettings?.(node)?.["layering.layerConstraint"] !== undefined ||
|
|
1538
|
+
connections.size !== 1
|
|
1539
|
+
)
|
|
1540
|
+
continue;
|
|
1541
|
+
if (
|
|
1542
|
+
phaseInput.graph.edges.some(
|
|
1543
|
+
(edge) =>
|
|
1544
|
+
(edge.sourceId === id && !separateBoundaryIds.has(edge.targetId)) ||
|
|
1545
|
+
(edge.targetId === id && !separateBoundaryIds.has(edge.sourceId)),
|
|
1546
|
+
)
|
|
1547
|
+
)
|
|
1548
|
+
continue;
|
|
1549
|
+
inferredConstraints.set(id, connections.has("FIRST_SEPARATE") ? "FIRST" : "LAST");
|
|
1550
|
+
}
|
|
1551
|
+
if (inferredConstraints.size > 0) {
|
|
1552
|
+
const authoredSettings = phaseInput.nodeSettings;
|
|
1553
|
+
phaseInput = {
|
|
1554
|
+
...phaseInput,
|
|
1555
|
+
nodeSettings: (node) => {
|
|
1556
|
+
const inferred = inferredConstraints.get(node.id);
|
|
1557
|
+
return inferred
|
|
1558
|
+
? { ...authoredSettings?.(node), "layering.layerConstraint": inferred }
|
|
1559
|
+
: authoredSettings?.(node);
|
|
1560
|
+
},
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
const layeringInput =
|
|
1564
|
+
separateBoundaryIds.size === 0
|
|
1565
|
+
? phaseInput
|
|
1566
|
+
: {
|
|
1567
|
+
...phaseInput,
|
|
1568
|
+
graph: {
|
|
1569
|
+
...phaseInput.graph,
|
|
1570
|
+
nodes: phaseInput.graph.nodes.filter((node) => !separateBoundaryIds.has(node.id)),
|
|
1571
|
+
edges: phaseInput.graph.edges.filter(
|
|
1572
|
+
(edge) =>
|
|
1573
|
+
!separateBoundaryIds.has(edge.sourceId) && !separateBoundaryIds.has(edge.targetId),
|
|
1574
|
+
),
|
|
1575
|
+
},
|
|
1576
|
+
};
|
|
1577
|
+
let assignment = measure("layer-assignment", () =>
|
|
1368
1578
|
applyHighDegreeNodeTreatment(
|
|
1369
|
-
|
|
1370
|
-
|
|
1579
|
+
phaseInput,
|
|
1580
|
+
phaseOrientation,
|
|
1371
1581
|
applyNodePromotion(
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
applyPartitions(
|
|
1582
|
+
phaseInput,
|
|
1583
|
+
phaseOrientation,
|
|
1584
|
+
applyPartitions(
|
|
1585
|
+
phaseInput,
|
|
1586
|
+
applyLayerConstraints(phaseInput, layerAssigner(layeringInput, phaseOrientation)),
|
|
1587
|
+
),
|
|
1375
1588
|
),
|
|
1376
1589
|
),
|
|
1377
1590
|
);
|
|
1378
|
-
|
|
1379
|
-
|
|
1591
|
+
// Preserve network-simplex component order before restoring detached boundary
|
|
1592
|
+
// nodes; recomputing components after splitting changes the initial sweep.
|
|
1593
|
+
// ELK restores FIRST/LAST nodes before LongEdgeSplitter appends dummies.
|
|
1594
|
+
// Changing that order changes the initial sweep and selected port order.
|
|
1595
|
+
if (
|
|
1596
|
+
separateBoundaryIds.size > 0 ||
|
|
1597
|
+
phaseInput.graph.nodes.some((node) => {
|
|
1598
|
+
const constraint = phaseInput.nodeSettings?.(node)?.["layering.layerConstraint"];
|
|
1599
|
+
return constraint === "FIRST" || constraint === "LAST";
|
|
1600
|
+
})
|
|
1601
|
+
) {
|
|
1602
|
+
const layers = Array.from(
|
|
1603
|
+
{ length: Math.max(0, ...assignment.layerByNodeId.values()) + 1 },
|
|
1604
|
+
() => [] as string[],
|
|
1605
|
+
);
|
|
1606
|
+
const seed =
|
|
1607
|
+
assignment.seedOrder ??
|
|
1608
|
+
((phaseInput.settings["layering.strategy"] ?? "NETWORK_SIMPLEX") === "NETWORK_SIMPLEX"
|
|
1609
|
+
? networkSimplexComponents(layeringInput).flat()
|
|
1610
|
+
: phaseInput.graph.nodes.map((node) => node.id));
|
|
1611
|
+
const seeded = new Set(seed);
|
|
1612
|
+
for (const id of [
|
|
1613
|
+
...seed,
|
|
1614
|
+
...phaseInput.graph.nodes.flatMap((node) => (seeded.has(node.id) ? [] : [node.id])),
|
|
1615
|
+
]) {
|
|
1616
|
+
layers[assignment.layerByNodeId.get(id) ?? 0]!.push(id);
|
|
1617
|
+
}
|
|
1618
|
+
assignment = {
|
|
1619
|
+
...assignment,
|
|
1620
|
+
seedOrder: applyLayerConstraintOrder(phaseInput, { layers }).layers.flat(),
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const hierarchicalPorts = prepareHierarchicalPortConstraints(
|
|
1625
|
+
phaseInput,
|
|
1626
|
+
phaseOrientation,
|
|
1627
|
+
assignment,
|
|
1380
1628
|
);
|
|
1629
|
+
phaseInput = hierarchicalPorts.input;
|
|
1630
|
+
assignment = hierarchicalPorts.assignment;
|
|
1631
|
+
const labeledPortLists = insertLabelDummyPorts(portLists, orientation, labels.segmentIdsByEdgeId);
|
|
1632
|
+
let incomingLists: ReadonlyMap<string, readonly PortEdgeList[]> = labeledPortLists;
|
|
1633
|
+
let expanded = measure("long-edge-splitting", () => {
|
|
1634
|
+
const split = withLongEdgeSplitterOrder(
|
|
1635
|
+
phaseInput,
|
|
1636
|
+
splitLongEdges(phaseInput, phaseOrientation, assignment),
|
|
1637
|
+
assignment.seedOrder ??
|
|
1638
|
+
((phaseInput.settings["layering.strategy"] ?? "NETWORK_SIMPLEX") === "NETWORK_SIMPLEX"
|
|
1639
|
+
? elkComponentOrder(layeringInput, labeledPortLists)
|
|
1640
|
+
: phaseInput.graph.nodes.map((node) => node.id)),
|
|
1641
|
+
portLists,
|
|
1642
|
+
);
|
|
1643
|
+
incomingLists = incomingAfterLongEdgeSplitting(labeledPortLists, split);
|
|
1644
|
+
return composeCenterLabelExpansion(labels, insertInvertedPortDummies(split, incomingLists));
|
|
1645
|
+
});
|
|
1646
|
+
// ELK sorts before north/south helpers exist. Carry both node and physical
|
|
1647
|
+
// port order into the crossing session, including newly generated helpers.
|
|
1648
|
+
const prepareModelOrder =
|
|
1649
|
+
(expanded.input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE" &&
|
|
1650
|
+
(expanded.input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP") === "LAYER_SWEEP" &&
|
|
1651
|
+
!options.strategies?.minimizeCrossings;
|
|
1652
|
+
let preparedPorts: ReadonlyMap<string, readonly string[]> | undefined;
|
|
1653
|
+
if (prepareModelOrder) {
|
|
1654
|
+
const count =
|
|
1655
|
+
expanded.assignment.layerCount ?? Math.max(...expanded.assignment.layerByNodeId.values()) + 1;
|
|
1656
|
+
const layers: string[][] = Array.from({ length: count }, () => []);
|
|
1657
|
+
const seeded = expanded.assignment.seedOrder ?? [];
|
|
1658
|
+
const initialIds = [
|
|
1659
|
+
...seeded,
|
|
1660
|
+
...expanded.input.graph.nodes.map((node) => node.id).filter((id) => !seeded.includes(id)),
|
|
1661
|
+
];
|
|
1662
|
+
for (const id of initialIds) layers[expanded.assignment.layerByNodeId.get(id) ?? 0]!.push(id);
|
|
1663
|
+
const initial = crossingGraph(expanded.input, expanded.orientation, layers);
|
|
1664
|
+
for (const node of initial.layers.flat())
|
|
1665
|
+
if (node.id.startsWith("__layout_dummy:"))
|
|
1666
|
+
node.type = node.id.startsWith("__layout_dummy:label:") ? "LABEL" : "LONG_EDGE";
|
|
1667
|
+
const adjacency = new Map(
|
|
1668
|
+
[...(expanded.incomingEdgeOrderByNodeId ?? [])].map(([id, edges]) => [
|
|
1669
|
+
id,
|
|
1670
|
+
edges.flatMap((edge) => expanded.segmentIdsByEdgeId.get(edge) ?? [edge]),
|
|
1671
|
+
]),
|
|
1672
|
+
);
|
|
1673
|
+
sortInitialModelOrder(expanded.input, expanded.orientation, initial, adjacency);
|
|
1674
|
+
preparedPorts = new Map(
|
|
1675
|
+
initial.layers.flat().map((node) => [node.id, node.ports.map((port) => port.id)]),
|
|
1676
|
+
);
|
|
1677
|
+
expanded = {
|
|
1678
|
+
...expanded,
|
|
1679
|
+
assignment: {
|
|
1680
|
+
...expanded.assignment,
|
|
1681
|
+
seedOrder: initial.layers.flatMap((layer) => layer.map((node) => node.id)),
|
|
1682
|
+
},
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const northSouth = insertNorthSouthPortDummies(expanded);
|
|
1686
|
+
expanded = northSouth.expansion;
|
|
1687
|
+
if (prepareModelOrder) markInitialModelOrderPrepared(expanded.input, preparedPorts!);
|
|
1688
|
+
const associates = new Map<string, string[]>(),
|
|
1689
|
+
unitMembers = new Map<string, string[]>();
|
|
1690
|
+
for (const [dummy, origin] of northSouth.originsByDummyId)
|
|
1691
|
+
associates.set(origin.node.id, [...(associates.get(origin.node.id) ?? []), dummy]);
|
|
1692
|
+
for (const id of expanded.assignment.seedOrder ?? expanded.input.graph.nodes.map((n) => n.id)) {
|
|
1693
|
+
const owner = northSouth.layoutUnitByNodeId.get(id);
|
|
1694
|
+
if (owner) unitMembers.set(owner, [...(unitMembers.get(owner) ?? []), id]);
|
|
1695
|
+
}
|
|
1696
|
+
const northEdges = new Set<string>(),
|
|
1697
|
+
southEdges = new Set<string>();
|
|
1698
|
+
const upper = direction === "right" || direction === "left" ? "NORTH" : "WEST";
|
|
1699
|
+
const lower = direction === "right" || direction === "left" ? "SOUTH" : "EAST";
|
|
1700
|
+
for (const edge of expanded.input.graph.edges)
|
|
1701
|
+
for (const [id, name] of [
|
|
1702
|
+
[edge.sourceId, edge.sourcePort],
|
|
1703
|
+
[edge.targetId, edge.targetPort],
|
|
1704
|
+
]) {
|
|
1705
|
+
const node = expanded.input.graph.nodes.find((n) => n.id === id),
|
|
1706
|
+
port = node?.ports?.find((p) => p.name === name);
|
|
1707
|
+
const side =
|
|
1708
|
+
node && port ? expanded.input.portSettings?.(port, node)?.["port.side"] : undefined;
|
|
1709
|
+
if (side === upper) northEdges.add(id!);
|
|
1710
|
+
if (side === lower) southEdges.add(id!);
|
|
1711
|
+
}
|
|
1712
|
+
recordCrossingUnits(expanded.input, {
|
|
1713
|
+
edgeListRanks: {
|
|
1714
|
+
incoming: edgeListRanks(incomingLists, "incoming"),
|
|
1715
|
+
outgoing: edgeListRanks(labeledPortLists, "outgoing"),
|
|
1716
|
+
},
|
|
1717
|
+
northSouthOrigins: northSouth.originsByDummyId,
|
|
1718
|
+
incomingEdgeOrderByDummyId: northSouth.incomingEdgeOrderByDummyId,
|
|
1719
|
+
successors: northSouth.successorsByNodeId,
|
|
1720
|
+
units: northSouth.layoutUnitByNodeId,
|
|
1721
|
+
unitMembers,
|
|
1722
|
+
associates,
|
|
1723
|
+
northEdges,
|
|
1724
|
+
southEdges,
|
|
1725
|
+
normalNodes: new Set(
|
|
1726
|
+
expanded.input.graph.nodes
|
|
1727
|
+
.filter(
|
|
1728
|
+
(n) => !n.id.startsWith("__layout_dummy:") && !n.id.startsWith("__layout_breaking:"),
|
|
1729
|
+
)
|
|
1730
|
+
.map((n) => n.id),
|
|
1731
|
+
),
|
|
1732
|
+
longEdgeNodes: new Set(
|
|
1733
|
+
expanded.input.graph.nodes
|
|
1734
|
+
.filter(
|
|
1735
|
+
(n) =>
|
|
1736
|
+
(n.id.startsWith("__layout_dummy:") &&
|
|
1737
|
+
!n.id.startsWith("__layout_dummy:label:") &&
|
|
1738
|
+
!n.id.startsWith("__layout_dummy:north-south:")) ||
|
|
1739
|
+
n.id.startsWith("__layout_breaking:"),
|
|
1740
|
+
)
|
|
1741
|
+
.map((n) => n.id),
|
|
1742
|
+
),
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1381
1745
|
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
1382
1746
|
const crossingMinimizer = (() => {
|
|
1383
1747
|
if (options.strategies?.minimizeCrossings) return options.strategies.minimizeCrossings;
|
|
@@ -1393,23 +1757,154 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1393
1757
|
`Crossing-minimization strategy ${crossingStrategy} is not implemented`,
|
|
1394
1758
|
);
|
|
1395
1759
|
})();
|
|
1396
|
-
|
|
1397
|
-
|
|
1760
|
+
const finishCrossingOrder = (order: LayerOrder) =>
|
|
1761
|
+
applyGreedySwitch(
|
|
1398
1762
|
expanded.input,
|
|
1399
|
-
|
|
1763
|
+
expanded.orientation,
|
|
1764
|
+
applySemiInteractiveOrder(
|
|
1400
1765
|
expanded.input,
|
|
1401
|
-
expanded.orientation,
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1766
|
+
applyForcedModelOrder(expanded.input, expanded.orientation, order),
|
|
1767
|
+
),
|
|
1768
|
+
);
|
|
1769
|
+
const minimizeScope = () =>
|
|
1770
|
+
measure("crossing-minimization", () =>
|
|
1771
|
+
finishCrossingOrder(
|
|
1772
|
+
crossingMinimizer(expanded.input, expanded.orientation, expanded.assignment),
|
|
1773
|
+
),
|
|
1774
|
+
);
|
|
1775
|
+
let order = yield {
|
|
1776
|
+
input: expanded.input,
|
|
1777
|
+
orientation: expanded.orientation,
|
|
1778
|
+
assignment: expanded.assignment,
|
|
1779
|
+
updateNodeSize: (id, size) => {
|
|
1780
|
+
for (const holder of [input, labels.input, phaseInput, expanded.input]) {
|
|
1781
|
+
(holder.sizes as Map<string, NodeSize>).set(id, { ...size });
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
minimize: minimizeScope,
|
|
1785
|
+
finishOrder: finishCrossingOrder,
|
|
1786
|
+
};
|
|
1787
|
+
traceLayeredPhase(() => ({
|
|
1788
|
+
kind: "crossing-order",
|
|
1789
|
+
scope: graph.id,
|
|
1790
|
+
edgeIds: phaseInput.graph.edges.map((edge) => edge.id),
|
|
1791
|
+
reversedEdgeIds: [...phaseOrientation.reversedEdgeIds],
|
|
1792
|
+
layerByNodeId: assignment.layerByNodeId,
|
|
1793
|
+
layers: order.layers,
|
|
1794
|
+
}));
|
|
1795
|
+
// ELK's InLayerConstraintProcessor: northern hierarchical port helpers lead
|
|
1796
|
+
// their layer and southern ones end it, keeping their relative order.
|
|
1797
|
+
const inLayerSide = new Map(
|
|
1798
|
+
[...hierarchicalPorts.replacements.values()].map((record) => [record.helper.id, record.side]),
|
|
1799
|
+
);
|
|
1800
|
+
if (inLayerSide.size)
|
|
1801
|
+
order = {
|
|
1802
|
+
...order,
|
|
1803
|
+
layers: order.layers.map((layer) => [
|
|
1804
|
+
...layer.filter((id) => inLayerSide.get(id) === "NORTH"),
|
|
1805
|
+
...layer.filter((id) => !inLayerSide.has(id)),
|
|
1806
|
+
...layer.filter((id) => inLayerSide.get(id) === "SOUTH"),
|
|
1807
|
+
]),
|
|
1808
|
+
};
|
|
1809
|
+
// Forced model order can move other nodes between a node and its
|
|
1810
|
+
// north/south port dummies, whose edges would then cross those nodes.
|
|
1811
|
+
// Rejoin them, moving either the dummies or the intervening nodes,
|
|
1812
|
+
// whichever crosses fewer edges.
|
|
1813
|
+
const dummiesByOwner = new Map<string, Set<string>>();
|
|
1814
|
+
for (const [dummy, origin] of northSouth.originsByDummyId)
|
|
1815
|
+
dummiesByOwner.set(
|
|
1816
|
+
origin.node.id,
|
|
1817
|
+
(dummiesByOwner.get(origin.node.id) ?? new Set()).add(dummy),
|
|
1818
|
+
);
|
|
1819
|
+
const separated = (layer: readonly string[], owner: string, own: ReadonlySet<string>) => {
|
|
1820
|
+
const at = layer.indexOf(owner);
|
|
1821
|
+
const span = layer.flatMap((id, index) => (own.has(id) ? [index] : []));
|
|
1822
|
+
const [from, to] = [Math.min(at, ...span), Math.max(at, ...span)];
|
|
1823
|
+
return {
|
|
1824
|
+
at,
|
|
1825
|
+
from,
|
|
1826
|
+
to,
|
|
1827
|
+
between: layer.slice(from, to + 1).filter((id) => id !== owner && !own.has(id)),
|
|
1828
|
+
};
|
|
1829
|
+
};
|
|
1830
|
+
const intrudes = (between: readonly string[]) => between.some((id) => !id.startsWith("__"));
|
|
1831
|
+
for (let layerIndex = 0; layerIndex < order.layers.length; layerIndex++)
|
|
1832
|
+
for (const [owner, own] of dummiesByOwner) {
|
|
1833
|
+
const current = order.layers[layerIndex]!;
|
|
1834
|
+
if (!current.includes(owner)) continue;
|
|
1835
|
+
const { at, from, to, between } = separated(current, owner, own);
|
|
1836
|
+
if (!intrudes(between)) continue;
|
|
1837
|
+
const before = current.slice(from, at).filter((id) => own.has(id)),
|
|
1838
|
+
after = current.slice(at + 1, to + 1).filter((id) => own.has(id));
|
|
1839
|
+
const near = between.filter((id) => current.indexOf(id) < at),
|
|
1840
|
+
far = between.filter((id) => current.indexOf(id) > at);
|
|
1841
|
+
const head = current.slice(0, from),
|
|
1842
|
+
tail = current.slice(to + 1);
|
|
1843
|
+
const unit = [...before, owner, ...after];
|
|
1844
|
+
// Intervening nodes stay on their side of the owner, or pass it.
|
|
1845
|
+
const candidates = [
|
|
1846
|
+
[...head, ...near, ...unit, ...far, ...tail],
|
|
1847
|
+
[...head, ...far, ...unit, ...near, ...tail],
|
|
1848
|
+
];
|
|
1849
|
+
const score = (candidate: string[]) =>
|
|
1850
|
+
countAllCrossings(
|
|
1851
|
+
crossingGraph(
|
|
1405
1852
|
expanded.input,
|
|
1406
1853
|
expanded.orientation,
|
|
1407
|
-
|
|
1854
|
+
order.layers.map((members, index) => (index === layerIndex ? candidate : members)),
|
|
1408
1855
|
),
|
|
1856
|
+
).total;
|
|
1857
|
+
// Never separate another node from its own dummies.
|
|
1858
|
+
const valid = candidates.filter((candidate) =>
|
|
1859
|
+
[...dummiesByOwner].every(
|
|
1860
|
+
([other, dummies]) =>
|
|
1861
|
+
other === owner ||
|
|
1862
|
+
!candidate.includes(other) ||
|
|
1863
|
+
intrudes(separated(candidate, other, dummies).between) ===
|
|
1864
|
+
intrudes(separated(current, other, dummies).between),
|
|
1409
1865
|
),
|
|
1410
|
-
)
|
|
1411
|
-
|
|
1866
|
+
);
|
|
1867
|
+
const best = (valid.length ? valid : candidates.slice(0, 1)).reduce((winner, candidate) =>
|
|
1868
|
+
score(candidate) < score(winner) ? candidate : winner,
|
|
1869
|
+
);
|
|
1870
|
+
order = {
|
|
1871
|
+
...order,
|
|
1872
|
+
layers: order.layers.map((members, index) => (index === layerIndex ? best : members)),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
order = applyOrderHints(
|
|
1876
|
+
options.hints,
|
|
1877
|
+
order,
|
|
1878
|
+
(id) => [...(dummiesByOwner.get(id) ?? [])],
|
|
1879
|
+
(layers) =>
|
|
1880
|
+
countAllCrossings(crossingGraph(expanded.input, expanded.orientation, layers)).total,
|
|
1881
|
+
context?.diagnostics,
|
|
1412
1882
|
);
|
|
1883
|
+
// ELK's SweepCopy.assertCorrectPortSides: a north/south port whose dummy
|
|
1884
|
+
// ended on the other side of its node switches to that side.
|
|
1885
|
+
for (const layer of order.layers) {
|
|
1886
|
+
const position = new Map(layer.map((id, index) => [id, index]));
|
|
1887
|
+
for (const [dummy, origin] of northSouth.originsByDummyId) {
|
|
1888
|
+
const at = position.get(dummy),
|
|
1889
|
+
owner = position.get(origin.node.id);
|
|
1890
|
+
if (at === undefined || owner === undefined || at < owner === origin.beforeOwner) continue;
|
|
1891
|
+
const opposite = ({ NORTH: "SOUTH", SOUTH: "NORTH", WEST: "EAST", EAST: "WEST" } as const)[
|
|
1892
|
+
origin.side
|
|
1893
|
+
];
|
|
1894
|
+
switchedSideByPort.set(`${origin.node.id}\0${origin.port.name}`, opposite);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
// Preserve the existing merged-edge dummy policy without moving FIRST/LAST
|
|
1898
|
+
// nodes after crossing minimization.
|
|
1899
|
+
if (expanded.input.settings.mergeEdges === true) {
|
|
1900
|
+
order = {
|
|
1901
|
+
...order,
|
|
1902
|
+
layers: order.layers.map((layer) => [
|
|
1903
|
+
...layer.filter((id) => id.startsWith("__layout_dummy:")),
|
|
1904
|
+
...layer.filter((id) => !id.startsWith("__layout_dummy:")),
|
|
1905
|
+
]),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1413
1908
|
const unzippingFanIn =
|
|
1414
1909
|
graph.edges.length === graph.nodes.length - 1 &&
|
|
1415
1910
|
graph.nodes.some(
|
|
@@ -1426,6 +1921,19 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1426
1921
|
}
|
|
1427
1922
|
}
|
|
1428
1923
|
order = applyDirectionCongruency(expanded.input, order);
|
|
1924
|
+
const crossingUnits = getCrossingUnits(expanded.input);
|
|
1925
|
+
const switchedLabels = switchCenterLabelDummies(expanded, labels, order);
|
|
1926
|
+
expanded = switchedLabels.expansion;
|
|
1927
|
+
order = switchedLabels.order;
|
|
1928
|
+
const labelSelection = selectCenterLabelSides(expanded, labels, order);
|
|
1929
|
+
expanded = labelSelection.expansion;
|
|
1930
|
+
if (crossingUnits) recordCrossingUnits(expanded.input, crossingUnits);
|
|
1931
|
+
if (hyperedges) order = mergeHyperedgeDummies(expanded, order);
|
|
1932
|
+
expanded = {
|
|
1933
|
+
...expanded,
|
|
1934
|
+
input: sizeHierarchicalPortDummies(expanded.input, order, hierarchicalPorts),
|
|
1935
|
+
};
|
|
1936
|
+
setPlacementOrder(expanded.input, order);
|
|
1429
1937
|
const nodePlacementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
1430
1938
|
const nodePlacer = (() => {
|
|
1431
1939
|
if (options.strategies?.placeNodes) return options.strategies.placeNodes;
|
|
@@ -1435,7 +1943,11 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1435
1943
|
if (nodePlacementStrategy === "NETWORK_SIMPLEX") return placeNodesWithNetworkSimplex;
|
|
1436
1944
|
return placeNodesInLayers;
|
|
1437
1945
|
})();
|
|
1438
|
-
const placement = measure("node-placement", () =>
|
|
1946
|
+
const placement = measure("node-placement", () => {
|
|
1947
|
+
setPlacementOrientation(expanded.input, expanded.orientation);
|
|
1948
|
+
return nodePlacer(expanded.input, order);
|
|
1949
|
+
});
|
|
1950
|
+
applyPlacementHints(options.hints, expanded.input, order, placement, context?.diagnostics);
|
|
1439
1951
|
const mutableRects = placement.rectByNodeId as Map<
|
|
1440
1952
|
string,
|
|
1441
1953
|
{ x: number; y: number; width: number; height: number }
|
|
@@ -1583,54 +2095,6 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1583
2095
|
);
|
|
1584
2096
|
}
|
|
1585
2097
|
}
|
|
1586
|
-
{
|
|
1587
|
-
const horizontal = direction === "right" || direction === "left";
|
|
1588
|
-
const targetsBySource = new Map<string, Set<string>>();
|
|
1589
|
-
for (const edge of graph.edges) {
|
|
1590
|
-
if (edge.sourceId === edge.targetId) continue;
|
|
1591
|
-
const targets = targetsBySource.get(edge.sourceId) ?? new Set<string>();
|
|
1592
|
-
targets.add(edge.targetId);
|
|
1593
|
-
targetsBySource.set(edge.sourceId, targets);
|
|
1594
|
-
}
|
|
1595
|
-
for (const edge of graph.edges) {
|
|
1596
|
-
if (edge.sourcePort === undefined) continue;
|
|
1597
|
-
const source = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1598
|
-
const targetRect = mutableRects.get(edge.targetId);
|
|
1599
|
-
const sourceRect = mutableRects.get(edge.sourceId);
|
|
1600
|
-
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
1601
|
-
if (!source || !sourceRect || !targetRect || !port) continue;
|
|
1602
|
-
const side = input.portSettings?.(port, source)?.["port.side"];
|
|
1603
|
-
// A port side constrains its route, not the positions of an entire fan-out.
|
|
1604
|
-
// Align an isolated target only when the placement's separation survives.
|
|
1605
|
-
if (edge.sourceId === edge.targetId || (targetsBySource.get(edge.sourceId)?.size ?? 0) > 1)
|
|
1606
|
-
continue;
|
|
1607
|
-
const candidate =
|
|
1608
|
-
horizontal && side === "NORTH"
|
|
1609
|
-
? { ...targetRect, y: sourceRect.y - (port.height ?? 0) - targetRect.height }
|
|
1610
|
-
: horizontal && side === "SOUTH"
|
|
1611
|
-
? { ...targetRect, y: sourceRect.y + sourceRect.height + (port.height ?? 0) }
|
|
1612
|
-
: !horizontal && side === "WEST"
|
|
1613
|
-
? { ...targetRect, x: sourceRect.x - (port.width ?? 0) - targetRect.width }
|
|
1614
|
-
: !horizontal && side === "EAST"
|
|
1615
|
-
? { ...targetRect, x: sourceRect.x + sourceRect.width + (port.width ?? 0) }
|
|
1616
|
-
: undefined;
|
|
1617
|
-
if (
|
|
1618
|
-
!candidate ||
|
|
1619
|
-
[...mutableRects].some(
|
|
1620
|
-
([id, other]) =>
|
|
1621
|
-
id !== edge.targetId &&
|
|
1622
|
-
other.width > 0 &&
|
|
1623
|
-
other.height > 0 &&
|
|
1624
|
-
candidate.x < other.x + other.width &&
|
|
1625
|
-
candidate.x + candidate.width > other.x &&
|
|
1626
|
-
candidate.y < other.y + other.height &&
|
|
1627
|
-
candidate.y + candidate.height > other.y,
|
|
1628
|
-
)
|
|
1629
|
-
)
|
|
1630
|
-
continue;
|
|
1631
|
-
mutableRects.set(edge.targetId, candidate);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
2098
|
measure("port-margin-normalization", () =>
|
|
1635
2099
|
normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation),
|
|
1636
2100
|
);
|
|
@@ -1692,12 +2156,70 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1692
2156
|
: edgeRouting === "SPLINES"
|
|
1693
2157
|
? routeEdgesWithSplines
|
|
1694
2158
|
: routeEdgesOrthogonally);
|
|
2159
|
+
// A custom router owns its output. Compact placement before invoking it,
|
|
2160
|
+
// rather than moving or orthogonalizing its routes afterwards.
|
|
2161
|
+
if (options.strategies?.routeEdges)
|
|
2162
|
+
measure("post-compaction", () => applyPostCompaction(expanded.input, placement));
|
|
1695
2163
|
let expandedRoutes = measure("edge-routing", () =>
|
|
1696
2164
|
edgeRouter(expanded.input, expanded.orientation, placement),
|
|
1697
2165
|
);
|
|
1698
|
-
|
|
1699
|
-
|
|
2166
|
+
const restoredJunctionCounts = new Map<string, number>();
|
|
2167
|
+
expandedRoutes = restoreNorthSouthPortRoutes(
|
|
2168
|
+
{ ...northSouth, expansion: expanded },
|
|
2169
|
+
placement,
|
|
2170
|
+
expandedRoutes,
|
|
2171
|
+
(origin) => {
|
|
2172
|
+
const rect = placement.rectByNodeId.get(origin.node.id)!;
|
|
2173
|
+
return getPortPoint(
|
|
2174
|
+
origin.node,
|
|
2175
|
+
origin.port.name,
|
|
2176
|
+
rect,
|
|
2177
|
+
{ x: rect.x, y: rect.y },
|
|
2178
|
+
direction,
|
|
2179
|
+
expanded.input,
|
|
2180
|
+
);
|
|
2181
|
+
},
|
|
2182
|
+
restoredJunctionCounts,
|
|
2183
|
+
);
|
|
2184
|
+
const restoredNodes = expanded.input.graph.nodes.filter(
|
|
2185
|
+
(node) => !northSouth.originsByDummyId.has(node.id),
|
|
1700
2186
|
);
|
|
2187
|
+
const restoredEdges = expanded.input.graph.edges.map((edge) => {
|
|
2188
|
+
const source = northSouth.originsByDummyId.get(edge.sourceId),
|
|
2189
|
+
target = northSouth.originsByDummyId.get(edge.targetId);
|
|
2190
|
+
return source || target
|
|
2191
|
+
? {
|
|
2192
|
+
...edge,
|
|
2193
|
+
sourceId: source?.node.id ?? edge.sourceId,
|
|
2194
|
+
sourcePort: source?.port.name ?? edge.sourcePort,
|
|
2195
|
+
targetId: target?.node.id ?? edge.targetId,
|
|
2196
|
+
targetPort: target?.port.name ?? edge.targetPort,
|
|
2197
|
+
}
|
|
2198
|
+
: edge;
|
|
2199
|
+
});
|
|
2200
|
+
for (const dummy of northSouth.originsByDummyId.keys())
|
|
2201
|
+
(placement.rectByNodeId as Map<string, EntityRect>).delete(dummy);
|
|
2202
|
+
expanded = {
|
|
2203
|
+
...expanded,
|
|
2204
|
+
input: inheritCycleRandom(expanded.input, {
|
|
2205
|
+
...expanded.input,
|
|
2206
|
+
graph: { ...expanded.input.graph, nodes: restoredNodes, edges: restoredEdges },
|
|
2207
|
+
}),
|
|
2208
|
+
};
|
|
2209
|
+
const hierarchicalRestoration = restoreHierarchicalPorts(
|
|
2210
|
+
expanded.input,
|
|
2211
|
+
order,
|
|
2212
|
+
hierarchicalPorts,
|
|
2213
|
+
placement,
|
|
2214
|
+
expandedRoutes,
|
|
2215
|
+
);
|
|
2216
|
+
expanded = { ...expanded, input: hierarchicalRestoration.input };
|
|
2217
|
+
expandedRoutes = hierarchicalRestoration.routes;
|
|
2218
|
+
setPlacementOrientation(expanded.input, expanded.orientation);
|
|
2219
|
+
if (!options.strategies?.routeEdges)
|
|
2220
|
+
measure("post-compaction", () =>
|
|
2221
|
+
applyPostCompaction(expanded.input, placement, expandedRoutes, graph.edges),
|
|
2222
|
+
);
|
|
1701
2223
|
const antiparallelLabelPositions = new Map<string, Point>();
|
|
1702
2224
|
const outerAntiparallelLabelIds = new Set<string>();
|
|
1703
2225
|
const parallelLabelPositions = new Map<string, Point>();
|
|
@@ -1911,9 +2433,20 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1911
2433
|
}
|
|
1912
2434
|
|
|
1913
2435
|
const forwardLabel = antiparallelLabelPositions.get(forward.id)!;
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2436
|
+
// BK keeps the forward chain straight through its center label dummy.
|
|
2437
|
+
const forwardDummy = expanded.input.graph.nodes.find(
|
|
2438
|
+
(node) => node.id === expanded.labelDummyIdByEdgeId.get(forward.id),
|
|
2439
|
+
);
|
|
2440
|
+
const forwardDummyRect = forwardDummy && placement.rectByNodeId.get(forwardDummy.id);
|
|
2441
|
+
const forwardDummyPort = forwardDummy?.ports?.[0];
|
|
2442
|
+
const forwardCross =
|
|
2443
|
+
forwardDummyRect && forwardDummyPort
|
|
2444
|
+
? horizontal
|
|
2445
|
+
? forwardDummyRect.y + (forwardDummyPort.y ?? 0)
|
|
2446
|
+
: forwardDummyRect.x + (forwardDummyPort.x ?? 0)
|
|
2447
|
+
: horizontal
|
|
2448
|
+
? Math.round(forwardLabel.y + (forward.height ?? 0) / 2)
|
|
2449
|
+
: Math.round(forwardLabel.x + (forward.width ?? 0) / 2);
|
|
1917
2450
|
const alignPort = (node: GraphNode, portName: string): void => {
|
|
1918
2451
|
const rect = placement.rectByNodeId.get(node.id);
|
|
1919
2452
|
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
@@ -1950,7 +2483,8 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1950
2483
|
}
|
|
1951
2484
|
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
1952
2485
|
const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
|
|
1953
|
-
|
|
2486
|
+
// Routes are keyed by long-edge and label-dummy segments, not by original edges.
|
|
2487
|
+
for (const edge of expanded.input.graph.edges) {
|
|
1954
2488
|
const points = [...(pointsByEdgeId.get(edge.id) ?? [])];
|
|
1955
2489
|
if (points.length === 0) continue;
|
|
1956
2490
|
const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
|
|
@@ -1972,19 +2506,62 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1972
2506
|
};
|
|
1973
2507
|
shiftEndpoint(0, 1, sourceDelta);
|
|
1974
2508
|
shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
|
|
1975
|
-
|
|
2509
|
+
// A shift that straightens the jog leaves collinear bends behind.
|
|
2510
|
+
pointsByEdgeId.set(
|
|
2511
|
+
edge.id,
|
|
2512
|
+
sourceDelta === 0 && targetDelta === 0
|
|
2513
|
+
? points
|
|
2514
|
+
: points.filter(
|
|
2515
|
+
(point, index) =>
|
|
2516
|
+
index === 0 ||
|
|
2517
|
+
index === points.length - 1 ||
|
|
2518
|
+
!(
|
|
2519
|
+
(point.x === points[index - 1]!.x && point.x === points[index + 1]!.x) ||
|
|
2520
|
+
(point.y === points[index - 1]!.y && point.y === points[index + 1]!.y)
|
|
2521
|
+
),
|
|
2522
|
+
),
|
|
2523
|
+
);
|
|
1976
2524
|
}
|
|
1977
2525
|
expandedRoutes = { ...expandedRoutes, pointsByEdgeId };
|
|
1978
2526
|
}
|
|
1979
2527
|
let routes = measure("long-edge-joining", () =>
|
|
1980
2528
|
joinLongEdgeRoutes(
|
|
1981
|
-
|
|
2529
|
+
removeCenterLabelJunctions(
|
|
2530
|
+
expandedRoutes,
|
|
2531
|
+
expanded,
|
|
2532
|
+
labels.labelDummyIdByEdgeId,
|
|
2533
|
+
edgeRouting,
|
|
2534
|
+
),
|
|
1982
2535
|
expanded.segmentIdsByEdgeId,
|
|
1983
2536
|
edgeRouting === "SPLINES" || options.settings?.unnecessaryBendpoints === true,
|
|
1984
2537
|
edgeRouting === "SPLINES",
|
|
1985
2538
|
Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
2539
|
+
edgeRouting === "ORTHOGONAL",
|
|
2540
|
+
restoredJunctionCounts,
|
|
2541
|
+
expanded.orientation.reversedEdgeIds,
|
|
1986
2542
|
),
|
|
1987
2543
|
);
|
|
2544
|
+
if (options.settings?.feedbackEdges === true && edgeRouting === "ORTHOGONAL") {
|
|
2545
|
+
const horizontal = direction === "right" || direction === "left";
|
|
2546
|
+
const reversed = expanded.orientation.reversedEdgeIds;
|
|
2547
|
+
straightenFeedbackChains(
|
|
2548
|
+
[...expanded.segmentIdsByEdgeId].flatMap(([edgeId, ids]) => {
|
|
2549
|
+
if (ids.length < 2 || !ids.every((id) => reversed.has(id))) return [];
|
|
2550
|
+
if (routes.junctionPointsByEdgeId?.get(edgeId)?.length) return [];
|
|
2551
|
+
const labelId = labels.labelDummyIdByEdgeId.get(edgeId);
|
|
2552
|
+
if (labelId === undefined) return [{ edgeId }];
|
|
2553
|
+
const into = expanded.input.graph.edges.find(
|
|
2554
|
+
(edge) => ids.includes(edge.id) && edge.targetId === labelId,
|
|
2555
|
+
);
|
|
2556
|
+
const anchor = into && expandedRoutes.pointsByEdgeId.get(into.id)?.at(-1);
|
|
2557
|
+
if (!anchor) return [];
|
|
2558
|
+
return [{ edgeId, label: { id: labelId, cross: horizontal ? anchor.y : anchor.x } }];
|
|
2559
|
+
}),
|
|
2560
|
+
routes.pointsByEdgeId as Map<string, readonly Point[]>,
|
|
2561
|
+
placement.rectByNodeId as Map<string, EntityRect>,
|
|
2562
|
+
horizontal,
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
1988
2565
|
if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
|
|
1989
2566
|
const horizontal = direction === "right" || direction === "left";
|
|
1990
2567
|
const pointsByEdgeId = new Map(routes.pointsByEdgeId);
|
|
@@ -2096,39 +2673,6 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2096
2673
|
};
|
|
2097
2674
|
}
|
|
2098
2675
|
|
|
2099
|
-
for (const edge of graph.edges) {
|
|
2100
|
-
if (edge.sourcePort === undefined || edge.targetPort !== undefined) continue;
|
|
2101
|
-
const source = expanded.input.graph.nodes.find((node) => node.id === edge.sourceId);
|
|
2102
|
-
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
2103
|
-
const targetRect = mutableRects.get(edge.targetId);
|
|
2104
|
-
if (!source || !port || !targetRect) continue;
|
|
2105
|
-
const settings = expanded.input.portSettings?.(port, source);
|
|
2106
|
-
const forwardSide =
|
|
2107
|
-
direction === "right"
|
|
2108
|
-
? "EAST"
|
|
2109
|
-
: direction === "left"
|
|
2110
|
-
? "WEST"
|
|
2111
|
-
: direction === "down"
|
|
2112
|
-
? "SOUTH"
|
|
2113
|
-
: "NORTH";
|
|
2114
|
-
if (settings?.["port.anchor"] === undefined || settings["port.side"] !== forwardSide) continue;
|
|
2115
|
-
const horizontal = direction === "right" || direction === "left";
|
|
2116
|
-
const protrusion = horizontal ? (port.width ?? 0) : (port.height ?? 0);
|
|
2117
|
-
const delta = (direction === "right" || direction === "down" ? 1 : -1) * protrusion;
|
|
2118
|
-
mutableRects.set(
|
|
2119
|
-
edge.targetId,
|
|
2120
|
-
horizontal
|
|
2121
|
-
? { ...targetRect, x: targetRect.x + delta }
|
|
2122
|
-
: { ...targetRect, y: targetRect.y + delta },
|
|
2123
|
-
);
|
|
2124
|
-
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
2125
|
-
const end = points.at(-1);
|
|
2126
|
-
if (end)
|
|
2127
|
-
points[points.length - 1] = horizontal
|
|
2128
|
-
? { ...end, x: end.x + delta }
|
|
2129
|
-
: { ...end, y: end.y + delta };
|
|
2130
|
-
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
|
|
2131
|
-
}
|
|
2132
2676
|
for (const pair of labeledEdgesByPair.values()) {
|
|
2133
2677
|
if (pair.length < 2) continue;
|
|
2134
2678
|
const sourceRect = mutableRects.get(pair[0]!.sourceId);
|
|
@@ -2298,6 +2842,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2298
2842
|
const maximumFeedbackNodeX = Math.max(...feedbackNodeRects.map((rect) => rect.x + rect.width));
|
|
2299
2843
|
const minimumFeedbackNodeY = Math.min(...feedbackNodeRects.map((rect) => rect.y));
|
|
2300
2844
|
const maximumFeedbackNodeY = Math.max(...feedbackNodeRects.map((rect) => rect.y + rect.height));
|
|
2845
|
+
if (edgeRouting === "ORTHOGONAL")
|
|
2846
|
+
for (const [id, points] of routes.pointsByEdgeId)
|
|
2847
|
+
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(id, removeRouteSpurs(points));
|
|
2301
2848
|
const edges = graph.edges.map((edge) => {
|
|
2302
2849
|
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
2303
2850
|
const midpoint = getPolylineMidpoint(points);
|
|
@@ -2345,10 +2892,12 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2345
2892
|
flowDelta !== 0 &&
|
|
2346
2893
|
firstLeadDelta * flowDelta < 0 &&
|
|
2347
2894
|
lastLeadDelta * flowDelta < 0);
|
|
2895
|
+
// Retained dummy junctions do not split the physical label track.
|
|
2896
|
+
const feedbackTrackPoints = outsideFeedback ? simplifyRoute(points) : [];
|
|
2348
2897
|
const horizontalFeedbackCandidate = outsideFeedback
|
|
2349
|
-
?
|
|
2898
|
+
? feedbackTrackPoints
|
|
2350
2899
|
.flatMap((point, index) => {
|
|
2351
|
-
const next =
|
|
2900
|
+
const next = feedbackTrackPoints[index + 1];
|
|
2352
2901
|
return next !== undefined &&
|
|
2353
2902
|
point.y === next.y &&
|
|
2354
2903
|
(point.y < minimumFeedbackNodeY || point.y > maximumFeedbackNodeY)
|
|
@@ -2358,9 +2907,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2358
2907
|
.sort((left, right) => right.length - left.length)[0]
|
|
2359
2908
|
: undefined;
|
|
2360
2909
|
const verticalFeedbackCandidate = outsideFeedback
|
|
2361
|
-
?
|
|
2910
|
+
? feedbackTrackPoints
|
|
2362
2911
|
.flatMap((point, index) => {
|
|
2363
|
-
const next =
|
|
2912
|
+
const next = feedbackTrackPoints[index + 1];
|
|
2364
2913
|
return next !== undefined &&
|
|
2365
2914
|
point.x === next.x &&
|
|
2366
2915
|
(point.x < minimumFeedbackNodeX || point.x > maximumFeedbackNodeX)
|
|
@@ -2405,8 +2954,8 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2405
2954
|
? (horizontalFeedbackTrack.start.x + horizontalFeedbackTrack.end.x - width) / 2
|
|
2406
2955
|
: inlineLabel && verticalFeedbackTrack
|
|
2407
2956
|
? verticalFeedbackTrack.start.x > (minimumFeedbackNodeX + maximumFeedbackNodeX) / 2
|
|
2408
|
-
?
|
|
2409
|
-
:
|
|
2957
|
+
? Math.max(...points.map((point) => point.x)) + labelSpacing + 1
|
|
2958
|
+
: Math.min(...points.map((point) => point.x)) - labelSpacing - width - 1
|
|
2410
2959
|
: inlineLabel && verticalTrack
|
|
2411
2960
|
? labelBeforeTrack
|
|
2412
2961
|
? verticalTrack.x - edgeNodeSpacing - width
|
|
@@ -2515,33 +3064,157 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2515
3064
|
y: (points[1]!.y + points[2]!.y - height) / 2,
|
|
2516
3065
|
}
|
|
2517
3066
|
: undefined;
|
|
3067
|
+
const movableExteriorLoopLabelPosition = (() => {
|
|
3068
|
+
if (
|
|
3069
|
+
edgeRouting !== "ORTHOGONAL" ||
|
|
3070
|
+
labelPlacement !== "CENTER" ||
|
|
3071
|
+
inlineLabel ||
|
|
3072
|
+
!hasMovableLoopPorts(input, edge) ||
|
|
3073
|
+
!sourceRect ||
|
|
3074
|
+
points.length !== 4
|
|
3075
|
+
)
|
|
3076
|
+
return undefined;
|
|
3077
|
+
const first = points[1]!,
|
|
3078
|
+
last = points[2]!;
|
|
3079
|
+
// Cross-axis loop labels are centered on the node. Flow-side loop
|
|
3080
|
+
// labels align with the first physical port, as ELK's label placer does.
|
|
3081
|
+
if (first.y === last.y) {
|
|
3082
|
+
const low = first.y < sourceRect.y;
|
|
3083
|
+
return {
|
|
3084
|
+
x: horizontal ? sourceRect.x + (sourceRect.width - width) / 2 : Math.min(first.x, last.x),
|
|
3085
|
+
y: low ? first.y - labelSpacing - height : first.y + labelSpacing,
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
if (first.x === last.x) {
|
|
3089
|
+
const low = first.x < sourceRect.x;
|
|
3090
|
+
return {
|
|
3091
|
+
x: low ? first.x - labelSpacing - width : first.x + labelSpacing,
|
|
3092
|
+
y: horizontal
|
|
3093
|
+
? Math.min(first.y, last.y)
|
|
3094
|
+
: sourceRect.y + (sourceRect.height - height) / 2,
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
return undefined;
|
|
3098
|
+
})();
|
|
3099
|
+
// A self-loop label whose default spot covers a node or another route
|
|
3100
|
+
// sits beside one of the loop's segments instead, longest first, on the
|
|
3101
|
+
// side away from its node, at the first spot clear of both.
|
|
3102
|
+
const clearLabelSpot = (box: { x: number; y: number }) =>
|
|
3103
|
+
[...placement.rectByNodeId.values()].every(
|
|
3104
|
+
(rect) =>
|
|
3105
|
+
box.x >= rect.x + rect.width ||
|
|
3106
|
+
box.x + width <= rect.x ||
|
|
3107
|
+
box.y >= rect.y + rect.height ||
|
|
3108
|
+
box.y + height <= rect.y,
|
|
3109
|
+
) &&
|
|
3110
|
+
[...routes.pointsByEdgeId].every(
|
|
3111
|
+
([id, route]) =>
|
|
3112
|
+
id === edge.id ||
|
|
3113
|
+
route.every((a, index) => {
|
|
3114
|
+
const b = route[index + 1];
|
|
3115
|
+
if (!b) return true;
|
|
3116
|
+
return (
|
|
3117
|
+
Math.max(a.x, b.x) <= box.x ||
|
|
3118
|
+
Math.min(a.x, b.x) >= box.x + width ||
|
|
3119
|
+
Math.max(a.y, b.y) <= box.y ||
|
|
3120
|
+
Math.min(a.y, b.y) >= box.y + height
|
|
3121
|
+
);
|
|
3122
|
+
}),
|
|
3123
|
+
);
|
|
3124
|
+
const outerLoopLabelPosition = (() => {
|
|
3125
|
+
if (edge.sourceId !== edge.targetId || !sourceRect || points.length < 3) return undefined;
|
|
3126
|
+
const centerX = sourceRect.x + sourceRect.width / 2,
|
|
3127
|
+
centerY = sourceRect.y + sourceRect.height / 2;
|
|
3128
|
+
const segments = points
|
|
3129
|
+
.slice(1)
|
|
3130
|
+
.map((b, index) => [points[index]!, b] as const)
|
|
3131
|
+
.sort(
|
|
3132
|
+
([a, b], [c, d]) =>
|
|
3133
|
+
Math.abs(c.x - d.x) + Math.abs(c.y - d.y) - (Math.abs(a.x - b.x) + Math.abs(a.y - b.y)),
|
|
3134
|
+
);
|
|
3135
|
+
for (const [a, b] of segments) {
|
|
3136
|
+
const box =
|
|
3137
|
+
a.y === b.y
|
|
3138
|
+
? {
|
|
3139
|
+
x: (a.x + b.x - width) / 2,
|
|
3140
|
+
y: a.y < centerY ? a.y - labelSpacing - height : a.y + labelSpacing,
|
|
3141
|
+
}
|
|
3142
|
+
: {
|
|
3143
|
+
x: a.x < centerX ? a.x - labelSpacing - width : a.x + labelSpacing,
|
|
3144
|
+
y: (a.y + b.y - height) / 2,
|
|
3145
|
+
};
|
|
3146
|
+
if (clearLabelSpot(box)) return box;
|
|
3147
|
+
}
|
|
3148
|
+
return undefined;
|
|
3149
|
+
})();
|
|
3150
|
+
// A fixed same-side loop is a small square beside its port; its label
|
|
3151
|
+
// sits beyond that square, inside the space the loop envelope reserved.
|
|
3152
|
+
const fixedLoopSide = fixedSelfLoopSide(sourcePortSide);
|
|
3153
|
+
const fixedLoopConstraints = sourceNode && options.nodeSettings?.(sourceNode)?.portConstraints;
|
|
3154
|
+
const fixedLoopLabelPosition =
|
|
3155
|
+
edgeRouting === "ORTHOGONAL" &&
|
|
3156
|
+
labelPlacement === "CENTER" &&
|
|
3157
|
+
!inlineLabel &&
|
|
3158
|
+
edge.sourceId === edge.targetId &&
|
|
3159
|
+
fixedLoopSide &&
|
|
3160
|
+
targetPortSide === sourcePortSide &&
|
|
3161
|
+
fixedLoopConstraints &&
|
|
3162
|
+
fixedLoopConstraints !== "FREE" &&
|
|
3163
|
+
fixedLoopConstraints !== "UNDEFINED"
|
|
3164
|
+
? (() => {
|
|
3165
|
+
const spots = fixedSelfLoopLabels(points, fixedLoopSide, width, height, labelSpacing);
|
|
3166
|
+
return spots.find(clearLabelSpot) ?? spots[0];
|
|
3167
|
+
})()
|
|
3168
|
+
: undefined;
|
|
2518
3169
|
const explicitLabelPosition =
|
|
3170
|
+
fixedLoopLabelPosition ??
|
|
3171
|
+
movableExteriorLoopLabelPosition ??
|
|
2519
3172
|
selfLoopLabelPosition ??
|
|
2520
3173
|
outerAntiparallelLabelPosition ??
|
|
2521
3174
|
antiparallelLabelPosition ??
|
|
2522
|
-
parallelLabelPositions.get(edge.id)
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
3175
|
+
parallelLabelPositions.get(edge.id) ??
|
|
3176
|
+
(width > 0 && height > 0 && !clearLabelSpot({ x: routeX, y: routeY })
|
|
3177
|
+
? outerLoopLabelPosition
|
|
3178
|
+
: undefined);
|
|
3179
|
+
const centerLabelDummyId = labels.labelDummyIdByEdgeId.get(edge.id);
|
|
3180
|
+
const centerLabelInfo = centerLabelDummyId
|
|
3181
|
+
? labels.dummyById.get(centerLabelDummyId)
|
|
3182
|
+
: undefined;
|
|
3183
|
+
const centerLabelRect =
|
|
3184
|
+
labelDummyRect && centerLabelInfo
|
|
3185
|
+
? centerLabelPosition(
|
|
3186
|
+
labelDummyRect,
|
|
3187
|
+
centerLabelInfo,
|
|
3188
|
+
labelSelection.sides.get(centerLabelDummyId!),
|
|
3189
|
+
direction,
|
|
3190
|
+
)
|
|
3191
|
+
: undefined;
|
|
3192
|
+
const x = centerLabelRect
|
|
3193
|
+
? centerLabelRect.x
|
|
3194
|
+
: explicitLabelPosition
|
|
3195
|
+
? explicitLabelPosition.x
|
|
3196
|
+
: flexibleFeedbackLabel
|
|
3197
|
+
? horizontal
|
|
3198
|
+
? (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2
|
|
3199
|
+
: targetRect.x + (targetRect.width - width) / 2
|
|
3200
|
+
: centerFlowPosition !== undefined && horizontal
|
|
3201
|
+
? centerFlowPosition
|
|
3202
|
+
: labelPlacement === "CENTER" && horizontal && labelDummyRect
|
|
3203
|
+
? labelDummyRect.x
|
|
3204
|
+
: routeX;
|
|
3205
|
+
const y = centerLabelRect
|
|
3206
|
+
? centerLabelRect.y
|
|
3207
|
+
: explicitLabelPosition
|
|
3208
|
+
? explicitLabelPosition.y
|
|
3209
|
+
: flexibleFeedbackLabel
|
|
3210
|
+
? horizontal
|
|
3211
|
+
? targetRect.y + (targetRect.height - height) / 2
|
|
3212
|
+
: (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2
|
|
3213
|
+
: centerFlowPosition !== undefined && !horizontal
|
|
3214
|
+
? centerFlowPosition
|
|
3215
|
+
: labelPlacement === "CENTER" && !horizontal && labelDummyRect
|
|
3216
|
+
? labelDummyRect.y
|
|
3217
|
+
: routeY;
|
|
2545
3218
|
return {
|
|
2546
3219
|
...edge,
|
|
2547
3220
|
x,
|
|
@@ -2549,6 +3222,7 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2549
3222
|
width,
|
|
2550
3223
|
height,
|
|
2551
3224
|
points,
|
|
3225
|
+
|
|
2552
3226
|
routing:
|
|
2553
3227
|
edgeRouting === "POLYLINE"
|
|
2554
3228
|
? ("polyline" as const)
|
|
@@ -2558,6 +3232,13 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2558
3232
|
};
|
|
2559
3233
|
});
|
|
2560
3234
|
|
|
3235
|
+
placeEndLabels({
|
|
3236
|
+
edges,
|
|
3237
|
+
nodeRects: feedbackNodeRects,
|
|
3238
|
+
spacing: Number(options.settings?.["spacing.edgeLabel"] ?? 2),
|
|
3239
|
+
placement: (edge) => options.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER",
|
|
3240
|
+
inline: (edge) => options.edgeSettings?.(edge)?.["edgeLabels.inline"] === true,
|
|
3241
|
+
});
|
|
2561
3242
|
if (edgeRouting === "ORTHOGONAL") {
|
|
2562
3243
|
separateExteriorLabels({
|
|
2563
3244
|
edges,
|
|
@@ -2574,12 +3255,16 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2574
3255
|
});
|
|
2575
3256
|
}
|
|
2576
3257
|
|
|
2577
|
-
return
|
|
2578
|
-
...graph,
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
3258
|
+
return setElkjs0111ResultPolicy(
|
|
3259
|
+
{ ...graph, direction, nodes, edges },
|
|
3260
|
+
{
|
|
3261
|
+
providerBounds: hierarchicalRestoration.bounds,
|
|
3262
|
+
skipBoundsNormalization: !!hierarchicalRestoration.bounds,
|
|
3263
|
+
normalizationBounds: compactionBounds(placement),
|
|
3264
|
+
placementCrossBounds: placementCrossBounds(placement),
|
|
3265
|
+
junctionPointsByEdgeId: routes.junctionPointsByEdgeId,
|
|
3266
|
+
},
|
|
3267
|
+
);
|
|
2583
3268
|
}
|
|
2584
3269
|
|
|
2585
3270
|
/**
|
|
@@ -2592,12 +3277,18 @@ export function getLayeredLayout<N, E, G, P>(
|
|
|
2592
3277
|
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
2593
3278
|
options: LayeredLayoutOptions = {},
|
|
2594
3279
|
): CompoundVisualGraph<N, E, G, P> {
|
|
2595
|
-
const result =
|
|
2596
|
-
|
|
3280
|
+
const result = layoutWithHints(options.hints, (hints) =>
|
|
3281
|
+
repairFlatRouting(runLayeredPipeline(graph, { ...options, hints }), options),
|
|
3282
|
+
) as CompoundVisualGraph<N, E, G, P>;
|
|
3283
|
+
const complete = {
|
|
2597
3284
|
...result,
|
|
2598
3285
|
compoundGeometry: result.compoundGeometry ?? new Map(),
|
|
2599
3286
|
compoundRoutes: result.compoundRoutes ?? new Map(),
|
|
2600
3287
|
};
|
|
3288
|
+
// A replacement router owns its routes; otherwise opposite directions never share a track.
|
|
3289
|
+
return options.routing
|
|
3290
|
+
? replaceLayoutRouting(complete, options)
|
|
3291
|
+
: separateLayoutOpposingTracks(complete, options);
|
|
2601
3292
|
}
|
|
2602
3293
|
|
|
2603
3294
|
export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
@@ -2612,17 +3303,43 @@ export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
|
2612
3303
|
ports: true,
|
|
2613
3304
|
},
|
|
2614
3305
|
layout(graph, options, context) {
|
|
3306
|
+
if (options?.routing && (context.scope.mode !== "full" || context.constraints?.length))
|
|
3307
|
+
throw new UnsupportedLayoutError(
|
|
3308
|
+
"Post-layout route replacement requires unconstrained full layout; run routing separately for constrained or scoped layouts",
|
|
3309
|
+
);
|
|
2615
3310
|
if (context.scope.mode === "partial" || context.scope.mode === "route-only") {
|
|
2616
3311
|
return context.measurePhase("partial-layout", () =>
|
|
2617
3312
|
runPartialLayout(graph, options ?? {}, context, getLayeredLayout),
|
|
2618
3313
|
);
|
|
2619
3314
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
3315
|
+
const result = layoutWithHints(
|
|
3316
|
+
options?.hints,
|
|
3317
|
+
(hints, diagnostics) =>
|
|
3318
|
+
repairFlatRouting(
|
|
3319
|
+
runLayeredPipeline(graph, { ...options, hints }, { ...context, diagnostics }),
|
|
3320
|
+
options ?? {},
|
|
3321
|
+
),
|
|
3322
|
+
context.diagnostics,
|
|
3323
|
+
);
|
|
3324
|
+
if (!context.constraints?.length) {
|
|
3325
|
+
if (!options?.routing)
|
|
3326
|
+
return separateLayoutOpposingTracks(
|
|
3327
|
+
{
|
|
3328
|
+
...result,
|
|
3329
|
+
compoundGeometry: (result as CompoundVisualGraph).compoundGeometry ?? new Map(),
|
|
3330
|
+
compoundRoutes: (result as CompoundVisualGraph).compoundRoutes ?? new Map(),
|
|
3331
|
+
},
|
|
3332
|
+
options ?? {},
|
|
3333
|
+
);
|
|
3334
|
+
return replaceLayoutRouting(
|
|
3335
|
+
{
|
|
3336
|
+
...result,
|
|
3337
|
+
compoundGeometry: (result as CompoundVisualGraph).compoundGeometry ?? new Map(),
|
|
3338
|
+
compoundRoutes: (result as CompoundVisualGraph).compoundRoutes ?? new Map(),
|
|
3339
|
+
},
|
|
3340
|
+
options ?? {},
|
|
2623
3341
|
);
|
|
2624
|
-
|
|
2625
|
-
if (!context.constraints?.length) return result;
|
|
3342
|
+
}
|
|
2626
3343
|
return context.measurePhase("constraints", () =>
|
|
2627
3344
|
runPartialLayout(
|
|
2628
3345
|
result,
|
|
@@ -2641,3 +3358,11 @@ export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
|
2641
3358
|
);
|
|
2642
3359
|
},
|
|
2643
3360
|
};
|
|
3361
|
+
|
|
3362
|
+
/** The pinned ELK adapter retains its established placement/routing contract. */
|
|
3363
|
+
export const elkjs0111LayeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
3364
|
+
...layeredAlgorithm,
|
|
3365
|
+
layout(graph, options, context) {
|
|
3366
|
+
return runLayeredPipeline(graph, options ?? {}, context);
|
|
3367
|
+
},
|
|
3368
|
+
};
|