@statelyai/layout 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +4 -2
- package/README.md +221 -13
- package/dist/elkjs/index.cjs +3 -2
- package/dist/elkjs/index.d.cts +13 -2
- package/dist/elkjs/index.d.mts +13 -2
- package/dist/elkjs/index.mjs +3 -3
- package/dist/elkjs/main.cjs +2 -2
- package/dist/elkjs/main.d.cts +2 -2
- package/dist/elkjs/main.d.mts +2 -2
- package/dist/elkjs/main.mjs +2 -2
- package/dist/elkjs/worker-api.cjs +1 -1
- package/dist/elkjs/worker-api.d.cts +2 -2
- package/dist/elkjs/worker-api.d.mts +2 -2
- package/dist/elkjs/worker-api.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/index-B6cWNrMw.d.cts +80 -0
- package/dist/index-CJQLUKD9.d.mts +80 -0
- package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
- package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
- package/dist/index.cjs +39 -9
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -3
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +3 -2
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
- package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
- package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +29 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-CR8flv8b.mjs +2780 -0
- package/dist/routing-QIdGGFBe.cjs +2996 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
- package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
- package/package.json +14 -3
- package/src/authoring/coordinates.ts +38 -10
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1397 -452
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/routes.ts +147 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +8 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +445 -0
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +911 -261
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +34 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +143 -0
- package/src/routing/algorithms.ts +1088 -0
- package/src/routing/coordination.ts +91 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +69 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +358 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
- package/dist/elkjs-CFlfkEBz.mjs +0 -1528
- /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
- /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
- /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
- /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
package/src/layered/index.ts
CHANGED
|
@@ -1,8 +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";
|
|
36
|
+
import { layoutCompounds, type CompoundVisualGraph } from "./compound";
|
|
37
|
+
import { separateLayoutOpposingTracks } from "./opposing-tracks";
|
|
38
|
+
import { repairFlatRouting } from "./native-routing";
|
|
1
39
|
import type { Graph, GraphEdge, GraphNode, Point, VisualGraph, VisualNode } from "@statelyai/graph";
|
|
2
40
|
import { labelReferences, runPartialLayout } from "../authoring/partial";
|
|
3
41
|
import { UnsupportedLayoutError } from "../errors";
|
|
4
42
|
import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
|
|
5
43
|
import { separateExteriorLabels } from "./separate-exterior-labels";
|
|
44
|
+
import { placeEndLabels } from "./end-labels";
|
|
45
|
+
|
|
6
46
|
import {
|
|
7
47
|
assignLayersByLongestPath,
|
|
8
48
|
assignLayersByLongestPathToSink,
|
|
@@ -11,7 +51,6 @@ import {
|
|
|
11
51
|
assignLayersInteractively,
|
|
12
52
|
assignLayersWithCoffmanGraham,
|
|
13
53
|
applyLayerConstraints,
|
|
14
|
-
applyLayerConstraintOrientation,
|
|
15
54
|
applyPartitionOrientation,
|
|
16
55
|
applyPartitions,
|
|
17
56
|
applyLayerConstraintOrder,
|
|
@@ -31,6 +70,9 @@ import {
|
|
|
31
70
|
breakCyclesWithModelOrderBreadthFirstSearch,
|
|
32
71
|
breakCyclesWithDepthFirstSearch,
|
|
33
72
|
getPolylineMidpoint,
|
|
73
|
+
getPortPoint,
|
|
74
|
+
removeRouteSpurs,
|
|
75
|
+
simplifyRoute,
|
|
34
76
|
getOrientedPortDirection,
|
|
35
77
|
minimizeCrossingsWithBarycenter,
|
|
36
78
|
minimizeCrossingsWithMedian,
|
|
@@ -44,23 +86,44 @@ import {
|
|
|
44
86
|
routeEdgesWithPolylines,
|
|
45
87
|
routeEdgesWithSplines,
|
|
46
88
|
} from "./strategies";
|
|
47
|
-
import type {
|
|
89
|
+
import type {
|
|
90
|
+
AcyclicOrientation,
|
|
91
|
+
LayerAssignment,
|
|
92
|
+
LayerOrder,
|
|
93
|
+
LayeredLayoutOptions,
|
|
94
|
+
LayeredPhaseInput,
|
|
95
|
+
NodeSize,
|
|
96
|
+
} from "./types";
|
|
48
97
|
import { assignLayersWithNetworkSimplex } from "./network-simplex";
|
|
49
98
|
import { assignLayersWithMinWidth } from "./min-width";
|
|
50
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";
|
|
51
110
|
import { joinLongEdgeRoutes, splitLongEdges } from "./long-edges";
|
|
52
111
|
import { unzipLayersAlternating } from "./layer-unzipping";
|
|
53
112
|
import { placeNodesWithBrandesKoepf } from "./bk-node-placement";
|
|
113
|
+
import { breakCyclesWithConstraints } from "./constraint-orientation";
|
|
54
114
|
import { placeNodesWithLinearSegments } from "./linear-segments-node-placement";
|
|
55
115
|
import { placeNodesWithNetworkSimplex } from "./network-simplex-node-placement";
|
|
56
116
|
import { applyHighDegreeNodeTreatment } from "./high-degree";
|
|
57
117
|
import { applyNodePromotion } from "./node-promotion";
|
|
118
|
+
import { straightenFeedbackChains } from "./feedback-chains";
|
|
58
119
|
import {
|
|
59
120
|
foldMultiEdgeBreakingPoints,
|
|
60
121
|
insertMultiEdgeBreakingPoints,
|
|
61
122
|
joinFoldedMultiEdgeRoutes,
|
|
62
123
|
} from "./multi-edge-wrapping";
|
|
63
124
|
|
|
125
|
+
export type { CompoundVisualGraph } from "./compound";
|
|
126
|
+
|
|
64
127
|
export type {
|
|
65
128
|
AcyclicOrientation,
|
|
66
129
|
CrossingMinimizer,
|
|
@@ -69,6 +132,9 @@ export type {
|
|
|
69
132
|
EdgeRoutes,
|
|
70
133
|
LayerAssigner,
|
|
71
134
|
LayerAssignment,
|
|
135
|
+
CompoundLayoutOptions,
|
|
136
|
+
CompoundLayoutGeometry,
|
|
137
|
+
CompoundEdgeAttachment,
|
|
72
138
|
LayeredLayoutOptions,
|
|
73
139
|
LayeredPhaseInput,
|
|
74
140
|
LayeredSpacing,
|
|
@@ -658,7 +724,9 @@ function runWrappedMultiEdgePipeline<N, E, G, P>(
|
|
|
658
724
|
{ layerByNodeId: rank },
|
|
659
725
|
cuts,
|
|
660
726
|
);
|
|
661
|
-
const brokenExpansion =
|
|
727
|
+
const brokenExpansion = insertInvertedPortDummies(
|
|
728
|
+
splitLongEdges(prepared.input, prepared.orientation, prepared.assignment),
|
|
729
|
+
);
|
|
662
730
|
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
663
731
|
const crossingMinimizer =
|
|
664
732
|
crossingStrategy === "MEDIAN_LAYER_SWEEP"
|
|
@@ -688,6 +756,7 @@ function runWrappedMultiEdgePipeline<N, E, G, P>(
|
|
|
688
756
|
),
|
|
689
757
|
);
|
|
690
758
|
const folded = foldMultiEdgeBreakingPoints(brokenExpansion, crossedOrder, prepared);
|
|
759
|
+
setPlacementOrientation(folded.expansion.input, folded.expansion.orientation);
|
|
691
760
|
const placementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
692
761
|
const placement =
|
|
693
762
|
placementStrategy === "INTERACTIVE"
|
|
@@ -1094,6 +1163,17 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1094
1163
|
if ((options.settings?.["considerModelOrder.components"] ?? "NONE") === "NONE") {
|
|
1095
1164
|
components.sort((left, right) => left.area - right.area || left.modelOrder - right.modelOrder);
|
|
1096
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));
|
|
1097
1177
|
const componentSpacing = Number(
|
|
1098
1178
|
options.settings?.["spacing.componentComponent"] ??
|
|
1099
1179
|
options.settings?.["spacing.baseValue"] ??
|
|
@@ -1126,6 +1206,7 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1126
1206
|
const placedShapes: Array<{ left: number; right: number; top: number; bottom: number }> = [];
|
|
1127
1207
|
const nodeResults = new Map<string, VisualNode<N, P>>();
|
|
1128
1208
|
const edgeResults = new Map<string, (typeof components)[number]["result"]["edges"][number]>();
|
|
1209
|
+
const placements: Array<{ component: (typeof components)[number]; x: number; y: number }> = [];
|
|
1129
1210
|
for (const component of components) {
|
|
1130
1211
|
if (x > 0 && x + component.bounds.width > maxRowWidth) {
|
|
1131
1212
|
x = 0;
|
|
@@ -1146,17 +1227,30 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1146
1227
|
}
|
|
1147
1228
|
compactedY = Math.min(y, requiredY);
|
|
1148
1229
|
}
|
|
1149
|
-
const
|
|
1150
|
-
const dy = padding.top + compactedY - component.bounds.top;
|
|
1151
|
-
for (const node of component.result.nodes) {
|
|
1152
|
-
nodeResults.set(node.id, { ...node, x: (node.x ?? 0) + dx, y: (node.y ?? 0) + dy });
|
|
1230
|
+
for (const node of component.result.nodes)
|
|
1153
1231
|
placedShapes.push({
|
|
1154
1232
|
left: x + (node.x ?? 0) - component.bounds.left,
|
|
1155
1233
|
right: x + (node.x ?? 0) - component.bounds.left + (node.width ?? 0),
|
|
1156
1234
|
top: compactedY + (node.y ?? 0) - component.bounds.top,
|
|
1157
1235
|
bottom: compactedY + (node.y ?? 0) - component.bounds.top + (node.height ?? 0),
|
|
1158
1236
|
});
|
|
1159
|
-
}
|
|
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 });
|
|
1160
1254
|
for (const edge of component.result.edges) {
|
|
1161
1255
|
edgeResults.set(edge.id, {
|
|
1162
1256
|
...edge,
|
|
@@ -1165,8 +1259,6 @@ function runSeparatedComponents<N, E, G, P>(
|
|
|
1165
1259
|
points: edge.points?.map((point) => ({ x: point.x + dx, y: point.y + dy })),
|
|
1166
1260
|
});
|
|
1167
1261
|
}
|
|
1168
|
-
x += component.bounds.width + componentSpacing;
|
|
1169
|
-
rowHeight = Math.max(rowHeight, component.bounds.height);
|
|
1170
1262
|
}
|
|
1171
1263
|
|
|
1172
1264
|
return {
|
|
@@ -1182,106 +1274,20 @@ function runCompoundPipeline<N, E, G, P>(
|
|
|
1182
1274
|
options: LayeredLayoutOptions,
|
|
1183
1275
|
context?: LayoutExecutionContext,
|
|
1184
1276
|
): VisualGraph<N, E, G, P> {
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
...new Set(nodes.flatMap((node) => (node.parentId == null ? [] : [node.parentId]))),
|
|
1200
|
-
].sort((left, right) => depth(nodeById.get(right)!) - depth(nodeById.get(left)!));
|
|
1201
|
-
const edgeById = new Map(graph.edges.map((edge) => [edge.id, { ...edge }]));
|
|
1202
|
-
const padding = typeof options.padding === "number" ? options.padding : 12;
|
|
1203
|
-
|
|
1204
|
-
const layoutSiblings = (parentId: string | null): void => {
|
|
1205
|
-
const siblings = nodes.filter((node) => (node.parentId ?? null) === parentId);
|
|
1206
|
-
if (siblings.length === 0) return;
|
|
1207
|
-
const siblingIds = new Set(siblings.map((node) => node.id));
|
|
1208
|
-
const siblingEdges = graph.edges.filter(
|
|
1209
|
-
(edge) => siblingIds.has(edge.sourceId) && siblingIds.has(edge.targetId),
|
|
1210
|
-
);
|
|
1211
|
-
const flatNodes = siblings.map((node) => ({ ...node, parentId: null }));
|
|
1212
|
-
const flatGraph = {
|
|
1213
|
-
...graph,
|
|
1214
|
-
nodes: flatNodes,
|
|
1215
|
-
edges: siblingEdges,
|
|
1216
|
-
} as Graph<N, E, G, P>;
|
|
1217
|
-
const result = runLayeredPipeline(flatGraph, options, context);
|
|
1218
|
-
for (const laidOut of result.nodes) {
|
|
1219
|
-
const node = nodeById.get(laidOut.id);
|
|
1220
|
-
if (!node) continue;
|
|
1221
|
-
Object.assign(node, laidOut, { parentId });
|
|
1222
|
-
}
|
|
1223
|
-
for (const edge of result.edges) edgeById.set(edge.id, edge);
|
|
1224
|
-
};
|
|
1225
|
-
|
|
1226
|
-
for (const parentId of parentIds) {
|
|
1227
|
-
layoutSiblings(parentId);
|
|
1228
|
-
const parent = nodeById.get(parentId);
|
|
1229
|
-
if (!parent) continue;
|
|
1230
|
-
const children = nodes.filter((node) => node.parentId === parentId);
|
|
1231
|
-
const right = Math.max(0, ...children.map((node) => (node.x ?? 0) + (node.width ?? 0)));
|
|
1232
|
-
const bottom = Math.max(0, ...children.map((node) => (node.y ?? 0) + (node.height ?? 0)));
|
|
1233
|
-
parent.width = Math.max(parent.width ?? 0, right + padding);
|
|
1234
|
-
parent.height = Math.max(parent.height ?? 0, bottom + padding);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const roots = nodes.filter((node) => node.parentId == null);
|
|
1238
|
-
if (roots.length === 1 && parentIds.includes(roots[0]!.id)) {
|
|
1239
|
-
Object.assign(roots[0]!, { x: 0, y: 0 });
|
|
1240
|
-
} else {
|
|
1241
|
-
layoutSiblings(null);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
const absoluteRect = (id: string): { x: number; y: number; width: number; height: number } => {
|
|
1245
|
-
const node = nodeById.get(id);
|
|
1246
|
-
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1247
|
-
let x = node.x ?? 0;
|
|
1248
|
-
let y = node.y ?? 0;
|
|
1249
|
-
let parentId = node.parentId;
|
|
1250
|
-
const seen = new Set<string>();
|
|
1251
|
-
while (parentId != null && !seen.has(parentId)) {
|
|
1252
|
-
seen.add(parentId);
|
|
1253
|
-
const parent = nodeById.get(parentId);
|
|
1254
|
-
if (!parent) break;
|
|
1255
|
-
x += parent.x ?? 0;
|
|
1256
|
-
y += parent.y ?? 0;
|
|
1257
|
-
parentId = parent.parentId;
|
|
1258
|
-
}
|
|
1259
|
-
return { x, y, width: node.width ?? 0, height: node.height ?? 0 };
|
|
1260
|
-
};
|
|
1261
|
-
for (const edge of graph.edges) {
|
|
1262
|
-
if (edgeById.get(edge.id)?.points !== undefined) continue;
|
|
1263
|
-
const source = absoluteRect(edge.sourceId);
|
|
1264
|
-
const target = absoluteRect(edge.targetId);
|
|
1265
|
-
const start = { x: source.x + source.width, y: source.y + source.height / 2 };
|
|
1266
|
-
const end = { x: target.x, y: target.y + target.height / 2 };
|
|
1267
|
-
const track = (start.x + end.x) / 2;
|
|
1268
|
-
const points = [start, { x: track, y: start.y }, { x: track, y: end.y }, end];
|
|
1269
|
-
edgeById.set(edge.id, {
|
|
1270
|
-
...edge,
|
|
1271
|
-
points,
|
|
1272
|
-
x: track,
|
|
1273
|
-
y: (start.y + end.y) / 2,
|
|
1274
|
-
width: edge.width ?? 0,
|
|
1275
|
-
height: edge.height ?? 0,
|
|
1276
|
-
routing: "orthogonal",
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
return {
|
|
1281
|
-
...graph,
|
|
1282
|
-
nodes,
|
|
1283
|
-
edges: graph.edges.map((edge) => edgeById.get(edge.id) ?? edge),
|
|
1284
|
-
} as VisualGraph<N, E, G, P>;
|
|
1277
|
+
const result = layoutCompounds(graph, options, (scope, local) =>
|
|
1278
|
+
runLayeredPipeline(scope, local, context),
|
|
1279
|
+
);
|
|
1280
|
+
for (const route of result.compoundRoutes.values())
|
|
1281
|
+
for (const diagnostic of route.diagnostics)
|
|
1282
|
+
context?.diagnostics.push({
|
|
1283
|
+
severity: "warning",
|
|
1284
|
+
code: diagnostic.code,
|
|
1285
|
+
message: diagnostic.message,
|
|
1286
|
+
entityIds: [route.edgeId],
|
|
1287
|
+
phase: "compound-routing",
|
|
1288
|
+
geometry: "routes",
|
|
1289
|
+
});
|
|
1290
|
+
return result;
|
|
1285
1291
|
}
|
|
1286
1292
|
|
|
1287
1293
|
function runLayeredPipeline<N, E, G, P>(
|
|
@@ -1289,6 +1295,52 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1289
1295
|
options: LayeredLayoutOptions,
|
|
1290
1296
|
context?: LayoutExecutionContext,
|
|
1291
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
|
+
}
|
|
1292
1344
|
if (options.settings?.noLayout) {
|
|
1293
1345
|
return {
|
|
1294
1346
|
...graph,
|
|
@@ -1426,8 +1478,11 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1426
1478
|
);
|
|
1427
1479
|
})();
|
|
1428
1480
|
const orientation = measure("cycle-breaking", () =>
|
|
1429
|
-
applyPartitionOrientation(input,
|
|
1481
|
+
applyPartitionOrientation(input, breakCyclesWithConstraints(input, cycleBreaker)),
|
|
1430
1482
|
);
|
|
1483
|
+
const portLists = simulatePortEdgeLists(input, orientation);
|
|
1484
|
+
const hyperedges = importsHyperedges(input);
|
|
1485
|
+
traceLayeredPhase(() => ({ kind: "port-lists", scope: graph.id, ports: portLists }));
|
|
1431
1486
|
const layeringStrategy = options.settings?.["layering.strategy"] ?? "NETWORK_SIMPLEX";
|
|
1432
1487
|
const layerAssigner = (() => {
|
|
1433
1488
|
if (options.strategies?.assignLayers) return options.strategies.assignLayers;
|
|
@@ -1444,20 +1499,249 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1444
1499
|
`Layering strategy ${layeringStrategy} is not implemented yet`,
|
|
1445
1500
|
);
|
|
1446
1501
|
})();
|
|
1447
|
-
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", () =>
|
|
1448
1578
|
applyHighDegreeNodeTreatment(
|
|
1449
|
-
|
|
1450
|
-
|
|
1579
|
+
phaseInput,
|
|
1580
|
+
phaseOrientation,
|
|
1451
1581
|
applyNodePromotion(
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
applyPartitions(
|
|
1582
|
+
phaseInput,
|
|
1583
|
+
phaseOrientation,
|
|
1584
|
+
applyPartitions(
|
|
1585
|
+
phaseInput,
|
|
1586
|
+
applyLayerConstraints(phaseInput, layerAssigner(layeringInput, phaseOrientation)),
|
|
1587
|
+
),
|
|
1455
1588
|
),
|
|
1456
1589
|
),
|
|
1457
1590
|
);
|
|
1458
|
-
|
|
1459
|
-
|
|
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,
|
|
1460
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
|
+
|
|
1461
1745
|
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
1462
1746
|
const crossingMinimizer = (() => {
|
|
1463
1747
|
if (options.strategies?.minimizeCrossings) return options.strategies.minimizeCrossings;
|
|
@@ -1473,23 +1757,154 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1473
1757
|
`Crossing-minimization strategy ${crossingStrategy} is not implemented`,
|
|
1474
1758
|
);
|
|
1475
1759
|
})();
|
|
1476
|
-
|
|
1477
|
-
|
|
1760
|
+
const finishCrossingOrder = (order: LayerOrder) =>
|
|
1761
|
+
applyGreedySwitch(
|
|
1478
1762
|
expanded.input,
|
|
1479
|
-
|
|
1763
|
+
expanded.orientation,
|
|
1764
|
+
applySemiInteractiveOrder(
|
|
1480
1765
|
expanded.input,
|
|
1481
|
-
expanded.orientation,
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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(
|
|
1485
1852
|
expanded.input,
|
|
1486
1853
|
expanded.orientation,
|
|
1487
|
-
|
|
1854
|
+
order.layers.map((members, index) => (index === layerIndex ? candidate : members)),
|
|
1488
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),
|
|
1489
1865
|
),
|
|
1490
|
-
)
|
|
1491
|
-
|
|
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,
|
|
1492
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
|
+
}
|
|
1493
1908
|
const unzippingFanIn =
|
|
1494
1909
|
graph.edges.length === graph.nodes.length - 1 &&
|
|
1495
1910
|
graph.nodes.some(
|
|
@@ -1506,6 +1921,19 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1506
1921
|
}
|
|
1507
1922
|
}
|
|
1508
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);
|
|
1509
1937
|
const nodePlacementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
1510
1938
|
const nodePlacer = (() => {
|
|
1511
1939
|
if (options.strategies?.placeNodes) return options.strategies.placeNodes;
|
|
@@ -1515,7 +1943,11 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1515
1943
|
if (nodePlacementStrategy === "NETWORK_SIMPLEX") return placeNodesWithNetworkSimplex;
|
|
1516
1944
|
return placeNodesInLayers;
|
|
1517
1945
|
})();
|
|
1518
|
-
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);
|
|
1519
1951
|
const mutableRects = placement.rectByNodeId as Map<
|
|
1520
1952
|
string,
|
|
1521
1953
|
{ x: number; y: number; width: number; height: number }
|
|
@@ -1663,54 +2095,6 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1663
2095
|
);
|
|
1664
2096
|
}
|
|
1665
2097
|
}
|
|
1666
|
-
{
|
|
1667
|
-
const horizontal = direction === "right" || direction === "left";
|
|
1668
|
-
const targetsBySource = new Map<string, Set<string>>();
|
|
1669
|
-
for (const edge of graph.edges) {
|
|
1670
|
-
if (edge.sourceId === edge.targetId) continue;
|
|
1671
|
-
const targets = targetsBySource.get(edge.sourceId) ?? new Set<string>();
|
|
1672
|
-
targets.add(edge.targetId);
|
|
1673
|
-
targetsBySource.set(edge.sourceId, targets);
|
|
1674
|
-
}
|
|
1675
|
-
for (const edge of graph.edges) {
|
|
1676
|
-
if (edge.sourcePort === undefined) continue;
|
|
1677
|
-
const source = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1678
|
-
const targetRect = mutableRects.get(edge.targetId);
|
|
1679
|
-
const sourceRect = mutableRects.get(edge.sourceId);
|
|
1680
|
-
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
1681
|
-
if (!source || !sourceRect || !targetRect || !port) continue;
|
|
1682
|
-
const side = input.portSettings?.(port, source)?.["port.side"];
|
|
1683
|
-
// A port side constrains its route, not the positions of an entire fan-out.
|
|
1684
|
-
// Align an isolated target only when the placement's separation survives.
|
|
1685
|
-
if (edge.sourceId === edge.targetId || (targetsBySource.get(edge.sourceId)?.size ?? 0) > 1)
|
|
1686
|
-
continue;
|
|
1687
|
-
const candidate =
|
|
1688
|
-
horizontal && side === "NORTH"
|
|
1689
|
-
? { ...targetRect, y: sourceRect.y - (port.height ?? 0) - targetRect.height }
|
|
1690
|
-
: horizontal && side === "SOUTH"
|
|
1691
|
-
? { ...targetRect, y: sourceRect.y + sourceRect.height + (port.height ?? 0) }
|
|
1692
|
-
: !horizontal && side === "WEST"
|
|
1693
|
-
? { ...targetRect, x: sourceRect.x - (port.width ?? 0) - targetRect.width }
|
|
1694
|
-
: !horizontal && side === "EAST"
|
|
1695
|
-
? { ...targetRect, x: sourceRect.x + sourceRect.width + (port.width ?? 0) }
|
|
1696
|
-
: undefined;
|
|
1697
|
-
if (
|
|
1698
|
-
!candidate ||
|
|
1699
|
-
[...mutableRects].some(
|
|
1700
|
-
([id, other]) =>
|
|
1701
|
-
id !== edge.targetId &&
|
|
1702
|
-
other.width > 0 &&
|
|
1703
|
-
other.height > 0 &&
|
|
1704
|
-
candidate.x < other.x + other.width &&
|
|
1705
|
-
candidate.x + candidate.width > other.x &&
|
|
1706
|
-
candidate.y < other.y + other.height &&
|
|
1707
|
-
candidate.y + candidate.height > other.y,
|
|
1708
|
-
)
|
|
1709
|
-
)
|
|
1710
|
-
continue;
|
|
1711
|
-
mutableRects.set(edge.targetId, candidate);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
2098
|
measure("port-margin-normalization", () =>
|
|
1715
2099
|
normalizePlacementForPortExtents(expanded.input, placement, order, expanded.orientation),
|
|
1716
2100
|
);
|
|
@@ -1772,12 +2156,70 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1772
2156
|
: edgeRouting === "SPLINES"
|
|
1773
2157
|
? routeEdgesWithSplines
|
|
1774
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));
|
|
1775
2163
|
let expandedRoutes = measure("edge-routing", () =>
|
|
1776
2164
|
edgeRouter(expanded.input, expanded.orientation, placement),
|
|
1777
2165
|
);
|
|
1778
|
-
|
|
1779
|
-
|
|
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),
|
|
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,
|
|
1780
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
|
+
);
|
|
1781
2223
|
const antiparallelLabelPositions = new Map<string, Point>();
|
|
1782
2224
|
const outerAntiparallelLabelIds = new Set<string>();
|
|
1783
2225
|
const parallelLabelPositions = new Map<string, Point>();
|
|
@@ -1991,9 +2433,20 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1991
2433
|
}
|
|
1992
2434
|
|
|
1993
2435
|
const forwardLabel = antiparallelLabelPositions.get(forward.id)!;
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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);
|
|
1997
2450
|
const alignPort = (node: GraphNode, portName: string): void => {
|
|
1998
2451
|
const rect = placement.rectByNodeId.get(node.id);
|
|
1999
2452
|
const port = node.ports?.find((candidate) => candidate.name === portName);
|
|
@@ -2030,7 +2483,8 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2030
2483
|
}
|
|
2031
2484
|
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
2032
2485
|
const pointsByEdgeId = new Map(expandedRoutes.pointsByEdgeId);
|
|
2033
|
-
|
|
2486
|
+
// Routes are keyed by long-edge and label-dummy segments, not by original edges.
|
|
2487
|
+
for (const edge of expanded.input.graph.edges) {
|
|
2034
2488
|
const points = [...(pointsByEdgeId.get(edge.id) ?? [])];
|
|
2035
2489
|
if (points.length === 0) continue;
|
|
2036
2490
|
const sourceDelta = postCompactionNodeCrossDeltas.get(edge.sourceId) ?? 0;
|
|
@@ -2052,19 +2506,62 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2052
2506
|
};
|
|
2053
2507
|
shiftEndpoint(0, 1, sourceDelta);
|
|
2054
2508
|
shiftEndpoint(points.length - 1, points.length - 2, targetDelta);
|
|
2055
|
-
|
|
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
|
+
);
|
|
2056
2524
|
}
|
|
2057
2525
|
expandedRoutes = { ...expandedRoutes, pointsByEdgeId };
|
|
2058
2526
|
}
|
|
2059
2527
|
let routes = measure("long-edge-joining", () =>
|
|
2060
2528
|
joinLongEdgeRoutes(
|
|
2061
|
-
|
|
2529
|
+
removeCenterLabelJunctions(
|
|
2530
|
+
expandedRoutes,
|
|
2531
|
+
expanded,
|
|
2532
|
+
labels.labelDummyIdByEdgeId,
|
|
2533
|
+
edgeRouting,
|
|
2534
|
+
),
|
|
2062
2535
|
expanded.segmentIdsByEdgeId,
|
|
2063
2536
|
edgeRouting === "SPLINES" || options.settings?.unnecessaryBendpoints === true,
|
|
2064
2537
|
edgeRouting === "SPLINES",
|
|
2065
2538
|
Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
2539
|
+
edgeRouting === "ORTHOGONAL",
|
|
2540
|
+
restoredJunctionCounts,
|
|
2541
|
+
expanded.orientation.reversedEdgeIds,
|
|
2066
2542
|
),
|
|
2067
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
|
+
}
|
|
2068
2565
|
if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
|
|
2069
2566
|
const horizontal = direction === "right" || direction === "left";
|
|
2070
2567
|
const pointsByEdgeId = new Map(routes.pointsByEdgeId);
|
|
@@ -2176,39 +2673,6 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2176
2673
|
};
|
|
2177
2674
|
}
|
|
2178
2675
|
|
|
2179
|
-
for (const edge of graph.edges) {
|
|
2180
|
-
if (edge.sourcePort === undefined || edge.targetPort !== undefined) continue;
|
|
2181
|
-
const source = expanded.input.graph.nodes.find((node) => node.id === edge.sourceId);
|
|
2182
|
-
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
2183
|
-
const targetRect = mutableRects.get(edge.targetId);
|
|
2184
|
-
if (!source || !port || !targetRect) continue;
|
|
2185
|
-
const settings = expanded.input.portSettings?.(port, source);
|
|
2186
|
-
const forwardSide =
|
|
2187
|
-
direction === "right"
|
|
2188
|
-
? "EAST"
|
|
2189
|
-
: direction === "left"
|
|
2190
|
-
? "WEST"
|
|
2191
|
-
: direction === "down"
|
|
2192
|
-
? "SOUTH"
|
|
2193
|
-
: "NORTH";
|
|
2194
|
-
if (settings?.["port.anchor"] === undefined || settings["port.side"] !== forwardSide) continue;
|
|
2195
|
-
const horizontal = direction === "right" || direction === "left";
|
|
2196
|
-
const protrusion = horizontal ? (port.width ?? 0) : (port.height ?? 0);
|
|
2197
|
-
const delta = (direction === "right" || direction === "down" ? 1 : -1) * protrusion;
|
|
2198
|
-
mutableRects.set(
|
|
2199
|
-
edge.targetId,
|
|
2200
|
-
horizontal
|
|
2201
|
-
? { ...targetRect, x: targetRect.x + delta }
|
|
2202
|
-
: { ...targetRect, y: targetRect.y + delta },
|
|
2203
|
-
);
|
|
2204
|
-
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
2205
|
-
const end = points.at(-1);
|
|
2206
|
-
if (end)
|
|
2207
|
-
points[points.length - 1] = horizontal
|
|
2208
|
-
? { ...end, x: end.x + delta }
|
|
2209
|
-
: { ...end, y: end.y + delta };
|
|
2210
|
-
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
|
|
2211
|
-
}
|
|
2212
2676
|
for (const pair of labeledEdgesByPair.values()) {
|
|
2213
2677
|
if (pair.length < 2) continue;
|
|
2214
2678
|
const sourceRect = mutableRects.get(pair[0]!.sourceId);
|
|
@@ -2378,6 +2842,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2378
2842
|
const maximumFeedbackNodeX = Math.max(...feedbackNodeRects.map((rect) => rect.x + rect.width));
|
|
2379
2843
|
const minimumFeedbackNodeY = Math.min(...feedbackNodeRects.map((rect) => rect.y));
|
|
2380
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));
|
|
2381
2848
|
const edges = graph.edges.map((edge) => {
|
|
2382
2849
|
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
2383
2850
|
const midpoint = getPolylineMidpoint(points);
|
|
@@ -2425,10 +2892,12 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2425
2892
|
flowDelta !== 0 &&
|
|
2426
2893
|
firstLeadDelta * flowDelta < 0 &&
|
|
2427
2894
|
lastLeadDelta * flowDelta < 0);
|
|
2895
|
+
// Retained dummy junctions do not split the physical label track.
|
|
2896
|
+
const feedbackTrackPoints = outsideFeedback ? simplifyRoute(points) : [];
|
|
2428
2897
|
const horizontalFeedbackCandidate = outsideFeedback
|
|
2429
|
-
?
|
|
2898
|
+
? feedbackTrackPoints
|
|
2430
2899
|
.flatMap((point, index) => {
|
|
2431
|
-
const next =
|
|
2900
|
+
const next = feedbackTrackPoints[index + 1];
|
|
2432
2901
|
return next !== undefined &&
|
|
2433
2902
|
point.y === next.y &&
|
|
2434
2903
|
(point.y < minimumFeedbackNodeY || point.y > maximumFeedbackNodeY)
|
|
@@ -2438,9 +2907,9 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2438
2907
|
.sort((left, right) => right.length - left.length)[0]
|
|
2439
2908
|
: undefined;
|
|
2440
2909
|
const verticalFeedbackCandidate = outsideFeedback
|
|
2441
|
-
?
|
|
2910
|
+
? feedbackTrackPoints
|
|
2442
2911
|
.flatMap((point, index) => {
|
|
2443
|
-
const next =
|
|
2912
|
+
const next = feedbackTrackPoints[index + 1];
|
|
2444
2913
|
return next !== undefined &&
|
|
2445
2914
|
point.x === next.x &&
|
|
2446
2915
|
(point.x < minimumFeedbackNodeX || point.x > maximumFeedbackNodeX)
|
|
@@ -2485,8 +2954,8 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2485
2954
|
? (horizontalFeedbackTrack.start.x + horizontalFeedbackTrack.end.x - width) / 2
|
|
2486
2955
|
: inlineLabel && verticalFeedbackTrack
|
|
2487
2956
|
? verticalFeedbackTrack.start.x > (minimumFeedbackNodeX + maximumFeedbackNodeX) / 2
|
|
2488
|
-
?
|
|
2489
|
-
:
|
|
2957
|
+
? Math.max(...points.map((point) => point.x)) + labelSpacing + 1
|
|
2958
|
+
: Math.min(...points.map((point) => point.x)) - labelSpacing - width - 1
|
|
2490
2959
|
: inlineLabel && verticalTrack
|
|
2491
2960
|
? labelBeforeTrack
|
|
2492
2961
|
? verticalTrack.x - edgeNodeSpacing - width
|
|
@@ -2595,33 +3064,157 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2595
3064
|
y: (points[1]!.y + points[2]!.y - height) / 2,
|
|
2596
3065
|
}
|
|
2597
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;
|
|
2598
3169
|
const explicitLabelPosition =
|
|
3170
|
+
fixedLoopLabelPosition ??
|
|
3171
|
+
movableExteriorLoopLabelPosition ??
|
|
2599
3172
|
selfLoopLabelPosition ??
|
|
2600
3173
|
outerAntiparallelLabelPosition ??
|
|
2601
3174
|
antiparallelLabelPosition ??
|
|
2602
|
-
parallelLabelPositions.get(edge.id)
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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;
|
|
2625
3218
|
return {
|
|
2626
3219
|
...edge,
|
|
2627
3220
|
x,
|
|
@@ -2629,6 +3222,7 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2629
3222
|
width,
|
|
2630
3223
|
height,
|
|
2631
3224
|
points,
|
|
3225
|
+
|
|
2632
3226
|
routing:
|
|
2633
3227
|
edgeRouting === "POLYLINE"
|
|
2634
3228
|
? ("polyline" as const)
|
|
@@ -2638,6 +3232,13 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2638
3232
|
};
|
|
2639
3233
|
});
|
|
2640
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
|
+
});
|
|
2641
3242
|
if (edgeRouting === "ORTHOGONAL") {
|
|
2642
3243
|
separateExteriorLabels({
|
|
2643
3244
|
edges,
|
|
@@ -2654,12 +3255,16 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2654
3255
|
});
|
|
2655
3256
|
}
|
|
2656
3257
|
|
|
2657
|
-
return
|
|
2658
|
-
...graph,
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
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
|
+
);
|
|
2663
3268
|
}
|
|
2664
3269
|
|
|
2665
3270
|
/**
|
|
@@ -2671,8 +3276,19 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2671
3276
|
export function getLayeredLayout<N, E, G, P>(
|
|
2672
3277
|
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
2673
3278
|
options: LayeredLayoutOptions = {},
|
|
2674
|
-
):
|
|
2675
|
-
|
|
3279
|
+
): CompoundVisualGraph<N, E, G, P> {
|
|
3280
|
+
const result = layoutWithHints(options.hints, (hints) =>
|
|
3281
|
+
repairFlatRouting(runLayeredPipeline(graph, { ...options, hints }), options),
|
|
3282
|
+
) as CompoundVisualGraph<N, E, G, P>;
|
|
3283
|
+
const complete = {
|
|
3284
|
+
...result,
|
|
3285
|
+
compoundGeometry: result.compoundGeometry ?? new Map(),
|
|
3286
|
+
compoundRoutes: result.compoundRoutes ?? new Map(),
|
|
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);
|
|
2676
3292
|
}
|
|
2677
3293
|
|
|
2678
3294
|
export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
@@ -2687,17 +3303,43 @@ export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
|
2687
3303
|
ports: true,
|
|
2688
3304
|
},
|
|
2689
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
|
+
);
|
|
2690
3310
|
if (context.scope.mode === "partial" || context.scope.mode === "route-only") {
|
|
2691
3311
|
return context.measurePhase("partial-layout", () =>
|
|
2692
3312
|
runPartialLayout(graph, options ?? {}, context, getLayeredLayout),
|
|
2693
3313
|
);
|
|
2694
3314
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
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 ?? {},
|
|
2698
3341
|
);
|
|
2699
|
-
|
|
2700
|
-
if (!context.constraints?.length) return result;
|
|
3342
|
+
}
|
|
2701
3343
|
return context.measurePhase("constraints", () =>
|
|
2702
3344
|
runPartialLayout(
|
|
2703
3345
|
result,
|
|
@@ -2716,3 +3358,11 @@ export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
|
2716
3358
|
);
|
|
2717
3359
|
},
|
|
2718
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
|
+
};
|