@statelyai/layout 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/dist/elkjs/index.mjs +10 -7
- package/dist/{index-D2RodsZY.d.mts → index-8xYkohbz.d.mts} +2 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/layered/index.d.mts +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-Dd868WZY.mjs → layered-QwJn2gb2.mjs} +399 -53
- package/dist/{spore-fTgSoRLP.mjs → spore-D15xIQKj.mjs} +1 -1
- package/package.json +16 -6
- package/src/box.ts +135 -0
- package/src/elkjs/index.ts +1825 -0
- package/src/elkjs/types.ts +103 -0
- package/src/errors.ts +16 -0
- package/src/fixed.ts +80 -0
- package/src/index.ts +92 -0
- package/src/java-random.ts +46 -0
- package/src/layered/bk-node-placement.ts +715 -0
- package/src/layered/elk-enum-values.ts +171 -0
- package/src/layered/elk-options.generated.ts +315 -0
- package/src/layered/elk-options.ts +98 -0
- package/src/layered/flexible-ports.ts +11 -0
- package/src/layered/high-degree.ts +127 -0
- package/src/layered/index.ts +2138 -0
- package/src/layered/layer-unzipping.ts +217 -0
- package/src/layered/linear-segments-node-placement.ts +447 -0
- package/src/layered/long-edges.ts +405 -0
- package/src/layered/min-width.ts +159 -0
- package/src/layered/multi-edge-wrapping.ts +460 -0
- package/src/layered/network-simplex-node-placement.ts +500 -0
- package/src/layered/network-simplex.ts +346 -0
- package/src/layered/node-promotion.ts +197 -0
- package/src/layered/spacing.ts +37 -0
- package/src/layered/spline-bezier.ts +102 -0
- package/src/layered/strategies.ts +5343 -0
- package/src/layered/stretch-width.ts +136 -0
- package/src/layered/types.ts +111 -0
- package/src/layout.ts +202 -0
- package/src/packing.ts +74 -0
- package/src/random.ts +142 -0
- package/src/spore.ts +103 -0
- package/src/types.ts +84 -0
|
@@ -0,0 +1,2138 @@
|
|
|
1
|
+
import type { Graph, GraphEdge, GraphNode, Point, VisualGraph, VisualNode } from "@statelyai/graph";
|
|
2
|
+
import { UnsupportedLayoutError } from "../errors";
|
|
3
|
+
import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
assignLayersByLongestPath,
|
|
6
|
+
assignLayersByLongestPathToSink,
|
|
7
|
+
assignLayersByBreadthFirstModelOrder,
|
|
8
|
+
assignLayersByDepthFirstModelOrder,
|
|
9
|
+
assignLayersInteractively,
|
|
10
|
+
assignLayersWithCoffmanGraham,
|
|
11
|
+
applyLayerConstraints,
|
|
12
|
+
applyLayerConstraintOrientation,
|
|
13
|
+
applyPartitionOrientation,
|
|
14
|
+
applyPartitions,
|
|
15
|
+
applyLayerConstraintOrder,
|
|
16
|
+
applyLayerUnzipping,
|
|
17
|
+
applyGreedySwitch,
|
|
18
|
+
applyDirectionCongruency,
|
|
19
|
+
applyForcedModelOrder,
|
|
20
|
+
applyPostCompaction,
|
|
21
|
+
applySemiInteractiveOrder,
|
|
22
|
+
breakCyclesByModelOrder,
|
|
23
|
+
breakCyclesByStronglyConnectedConnectivity,
|
|
24
|
+
breakCyclesByStronglyConnectedNodeType,
|
|
25
|
+
breakCyclesGreedily,
|
|
26
|
+
breakCyclesGreedilyByModelOrder,
|
|
27
|
+
breakCyclesInteractively,
|
|
28
|
+
breakCyclesWithModelOrderDepthFirstSearch,
|
|
29
|
+
breakCyclesWithModelOrderBreadthFirstSearch,
|
|
30
|
+
breakCyclesWithDepthFirstSearch,
|
|
31
|
+
getPolylineMidpoint,
|
|
32
|
+
minimizeCrossingsWithBarycenter,
|
|
33
|
+
minimizeCrossingsWithMedian,
|
|
34
|
+
minimizeCrossingsInteractively,
|
|
35
|
+
minimizeCrossingsWithModelOrder,
|
|
36
|
+
normalizePlacementForPortExtents,
|
|
37
|
+
placeNodesInLayers,
|
|
38
|
+
placeNodesInteractively,
|
|
39
|
+
placePorts,
|
|
40
|
+
routeEdgesOrthogonally,
|
|
41
|
+
routeEdgesWithPolylines,
|
|
42
|
+
routeEdgesWithSplines,
|
|
43
|
+
} from "./strategies";
|
|
44
|
+
import type { LayeredLayoutOptions, LayeredPhaseInput, NodeSize } from "./types";
|
|
45
|
+
import { assignLayersWithNetworkSimplex } from "./network-simplex";
|
|
46
|
+
import { assignLayersWithMinWidth } from "./min-width";
|
|
47
|
+
import { assignLayersWithStretchWidth } from "./stretch-width";
|
|
48
|
+
import { joinLongEdgeRoutes, splitLongEdges } from "./long-edges";
|
|
49
|
+
import { unzipLayersAlternating } from "./layer-unzipping";
|
|
50
|
+
import { placeNodesWithBrandesKoepf } from "./bk-node-placement";
|
|
51
|
+
import { placeNodesWithLinearSegments } from "./linear-segments-node-placement";
|
|
52
|
+
import { placeNodesWithNetworkSimplex } from "./network-simplex-node-placement";
|
|
53
|
+
import { applyHighDegreeNodeTreatment } from "./high-degree";
|
|
54
|
+
import { applyNodePromotion } from "./node-promotion";
|
|
55
|
+
import {
|
|
56
|
+
foldMultiEdgeBreakingPoints,
|
|
57
|
+
insertMultiEdgeBreakingPoints,
|
|
58
|
+
joinFoldedMultiEdgeRoutes,
|
|
59
|
+
} from "./multi-edge-wrapping";
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
AcyclicOrientation,
|
|
63
|
+
CrossingMinimizer,
|
|
64
|
+
CycleBreaker,
|
|
65
|
+
EdgeRouter,
|
|
66
|
+
EdgeRoutes,
|
|
67
|
+
LayerAssigner,
|
|
68
|
+
LayerAssignment,
|
|
69
|
+
LayeredLayoutOptions,
|
|
70
|
+
LayeredPhaseInput,
|
|
71
|
+
LayeredSpacing,
|
|
72
|
+
LayeredStrategies,
|
|
73
|
+
LayoutPadding,
|
|
74
|
+
LayerOrder,
|
|
75
|
+
NodePlacement,
|
|
76
|
+
NodePlacer,
|
|
77
|
+
NodeSize,
|
|
78
|
+
} from "./types";
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
assignLayersByLongestPath,
|
|
82
|
+
assignLayersByLongestPathToSink,
|
|
83
|
+
assignLayersByBreadthFirstModelOrder,
|
|
84
|
+
assignLayersByDepthFirstModelOrder,
|
|
85
|
+
assignLayersInteractively,
|
|
86
|
+
assignLayersWithCoffmanGraham,
|
|
87
|
+
breakCyclesByModelOrder,
|
|
88
|
+
breakCyclesByStronglyConnectedConnectivity,
|
|
89
|
+
breakCyclesByStronglyConnectedNodeType,
|
|
90
|
+
breakCyclesWithDepthFirstSearch,
|
|
91
|
+
breakCyclesGreedily,
|
|
92
|
+
breakCyclesGreedilyByModelOrder,
|
|
93
|
+
breakCyclesInteractively,
|
|
94
|
+
breakCyclesWithModelOrderDepthFirstSearch,
|
|
95
|
+
breakCyclesWithModelOrderBreadthFirstSearch,
|
|
96
|
+
minimizeCrossingsWithBarycenter,
|
|
97
|
+
minimizeCrossingsWithMedian,
|
|
98
|
+
minimizeCrossingsInteractively,
|
|
99
|
+
minimizeCrossingsWithModelOrder,
|
|
100
|
+
placeNodesInLayers,
|
|
101
|
+
placeNodesInteractively,
|
|
102
|
+
routeEdgesOrthogonally,
|
|
103
|
+
routeEdgesWithPolylines,
|
|
104
|
+
routeEdgesWithSplines,
|
|
105
|
+
} from "./strategies";
|
|
106
|
+
export { assignLayersWithNetworkSimplex } from "./network-simplex";
|
|
107
|
+
export { assignLayersWithMinWidth } from "./min-width";
|
|
108
|
+
export { assignLayersWithStretchWidth } from "./stretch-width";
|
|
109
|
+
export { placeNodesWithBrandesKoepf } from "./bk-node-placement";
|
|
110
|
+
export { placeNodesWithLinearSegments } from "./linear-segments-node-placement";
|
|
111
|
+
export { placeNodesWithNetworkSimplex } from "./network-simplex-node-placement";
|
|
112
|
+
export {
|
|
113
|
+
elkLayeredEnumValues,
|
|
114
|
+
elkLayeredOptionDefinitions,
|
|
115
|
+
fromElkLayeredOptionId,
|
|
116
|
+
toElkLayeredOptions,
|
|
117
|
+
} from "./elk-options";
|
|
118
|
+
export type {
|
|
119
|
+
CycleBreakingStrategy,
|
|
120
|
+
CrossingMinimizationStrategy,
|
|
121
|
+
EdgeRoutingStyle,
|
|
122
|
+
ElkLayeredOptionId,
|
|
123
|
+
ElkLayeredOptionName,
|
|
124
|
+
ElkLayeredOptionValueByName,
|
|
125
|
+
LayeredAdvancedOptions,
|
|
126
|
+
LayeringStrategy,
|
|
127
|
+
NodePlacementStrategy,
|
|
128
|
+
} from "./elk-options";
|
|
129
|
+
|
|
130
|
+
const DEFAULT_NODE_SIZE: NodeSize = { width: 0, height: 0 };
|
|
131
|
+
|
|
132
|
+
function adjustUnzippedSinkRoutes<N, E, G, P>(
|
|
133
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
134
|
+
routes: ReadonlyMap<string, readonly Point[]>,
|
|
135
|
+
direction: "up" | "down" | "left" | "right",
|
|
136
|
+
edgeNodeSpacing: number,
|
|
137
|
+
edgeEdgeSpacing: number,
|
|
138
|
+
): ReadonlyMap<string, readonly Point[]> {
|
|
139
|
+
const horizontal = direction === "left" || direction === "right";
|
|
140
|
+
const flow = (point: Point): number => (horizontal ? point.x : point.y);
|
|
141
|
+
const cross = (point: Point): number => (horizontal ? point.y : point.x);
|
|
142
|
+
const result = new Map(routes);
|
|
143
|
+
const incomingByTarget = new Map<string, GraphEdge[]>();
|
|
144
|
+
for (const edge of graph.edges) {
|
|
145
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
146
|
+
const incoming = incomingByTarget.get(edge.targetId) ?? [];
|
|
147
|
+
incoming.push(edge);
|
|
148
|
+
incomingByTarget.set(edge.targetId, incoming);
|
|
149
|
+
}
|
|
150
|
+
for (const incoming of incomingByTarget.values()) {
|
|
151
|
+
if (incoming.length < 3) continue;
|
|
152
|
+
const routed = incoming.flatMap((edge) => {
|
|
153
|
+
const points = result.get(edge.id);
|
|
154
|
+
return points && points.length >= 2 ? [{ edge, start: points[0]!, end: points.at(-1)! }] : [];
|
|
155
|
+
});
|
|
156
|
+
if (!routed.some(({ start, end }) => Math.abs(cross(start) - cross(end)) < 1e-9)) continue;
|
|
157
|
+
const before = routed
|
|
158
|
+
.filter(({ start, end }) => cross(start) < cross(end) - 1e-9)
|
|
159
|
+
.sort((left, right) => cross(left.start) - cross(right.start));
|
|
160
|
+
const after = routed
|
|
161
|
+
.filter(({ start, end }) => cross(start) > cross(end) + 1e-9)
|
|
162
|
+
.sort((left, right) => cross(left.start) - cross(right.start));
|
|
163
|
+
const rewrite = (
|
|
164
|
+
candidates: typeof before,
|
|
165
|
+
distance: (index: number, count: number) => number,
|
|
166
|
+
): void => {
|
|
167
|
+
for (const [index, { edge, start, end }] of candidates.entries()) {
|
|
168
|
+
const sign = flow(end) >= flow(start) ? 1 : -1;
|
|
169
|
+
const desired =
|
|
170
|
+
flow(end) -
|
|
171
|
+
sign * (edgeNodeSpacing + distance(index, candidates.length) * edgeEdgeSpacing);
|
|
172
|
+
const minimum = flow(start) + sign * edgeNodeSpacing;
|
|
173
|
+
const track = sign > 0 ? Math.max(minimum, desired) : Math.min(minimum, desired);
|
|
174
|
+
result.set(
|
|
175
|
+
edge.id,
|
|
176
|
+
horizontal
|
|
177
|
+
? [start, { x: track, y: start.y }, { x: track, y: end.y }, end]
|
|
178
|
+
: [start, { x: start.x, y: track }, { x: end.x, y: track }, end],
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
rewrite(before, (index) => index + 1);
|
|
183
|
+
rewrite(after, (index, count) => count - 1 - index);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getNodeSize(node: GraphNode, options: LayeredLayoutOptions): NodeSize {
|
|
189
|
+
const measured = options.measure?.(node);
|
|
190
|
+
if (measured) return measured;
|
|
191
|
+
return {
|
|
192
|
+
width: node.width !== undefined && node.width >= 0 ? node.width : DEFAULT_NODE_SIZE.width,
|
|
193
|
+
height: node.height !== undefined && node.height >= 0 ? node.height : DEFAULT_NODE_SIZE.height,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function hasNestedNodes(graph: Graph): boolean {
|
|
198
|
+
return graph.nodes.some((node) => node.parentId != null);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runWrappedPathPipeline<N, E, G, P>(
|
|
202
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
203
|
+
options: LayeredLayoutOptions,
|
|
204
|
+
): VisualGraph<N, E, G, P> | undefined {
|
|
205
|
+
const strategy = options.settings?.["wrapping.strategy"] ?? "OFF";
|
|
206
|
+
const improveMultiEdgeCuts = options.settings?.["wrapping.multiEdge.improveCuts"] ?? true;
|
|
207
|
+
const multiEdgeDistancePenalty = Number(
|
|
208
|
+
options.settings?.["wrapping.multiEdge.distancePenalty"] ?? 2,
|
|
209
|
+
);
|
|
210
|
+
const improveWrappedEdges = options.settings?.["wrapping.multiEdge.improveWrappedEdges"] ?? true;
|
|
211
|
+
// A path has one edge spanning every candidate cut, so all three multi-edge
|
|
212
|
+
// refinements are mathematically neutral. General graphs use them below.
|
|
213
|
+
void improveMultiEdgeCuts;
|
|
214
|
+
void multiEdgeDistancePenalty;
|
|
215
|
+
void improveWrappedEdges;
|
|
216
|
+
const direction = options.direction ?? graph.direction ?? "right";
|
|
217
|
+
if (strategy === "OFF" || direction !== "right" || graph.nodes.length < 2) return undefined;
|
|
218
|
+
if (graph.edges.length !== graph.nodes.length - 1) return undefined;
|
|
219
|
+
const outgoing = new Map(graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
220
|
+
const indegree = new Map(graph.nodes.map((node) => [node.id, 0]));
|
|
221
|
+
const edgeByPair = new Map<string, (typeof graph.edges)[number]>();
|
|
222
|
+
for (const edge of graph.edges) {
|
|
223
|
+
if (edge.sourceId === edge.targetId) return undefined;
|
|
224
|
+
outgoing.get(edge.sourceId)?.push(edge.targetId);
|
|
225
|
+
indegree.set(edge.targetId, (indegree.get(edge.targetId) ?? 0) + 1);
|
|
226
|
+
edgeByPair.set(`${edge.sourceId}\0${edge.targetId}`, edge);
|
|
227
|
+
}
|
|
228
|
+
const source = graph.nodes.find((node) => indegree.get(node.id) === 0);
|
|
229
|
+
if (!source) return undefined;
|
|
230
|
+
const orderedIds: string[] = [];
|
|
231
|
+
let currentId: string | undefined = source.id;
|
|
232
|
+
const seen = new Set<string>();
|
|
233
|
+
while (currentId !== undefined && !seen.has(currentId)) {
|
|
234
|
+
seen.add(currentId);
|
|
235
|
+
orderedIds.push(currentId);
|
|
236
|
+
const targets: string[] = outgoing.get(currentId) ?? [];
|
|
237
|
+
if (targets.length > 1) return undefined;
|
|
238
|
+
currentId = targets[0];
|
|
239
|
+
}
|
|
240
|
+
if (orderedIds.length !== graph.nodes.length) return undefined;
|
|
241
|
+
|
|
242
|
+
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
|
|
243
|
+
const maximumWidth = Math.max(...[...sizes.values()].map((size) => size.width));
|
|
244
|
+
const maximumHeight = Math.max(...[...sizes.values()].map((size) => size.height));
|
|
245
|
+
if (
|
|
246
|
+
[...sizes.values()].some((size) => size.width !== maximumWidth || size.height !== maximumHeight)
|
|
247
|
+
) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const aspectRatio = Number(options.settings?.aspectRatio ?? 1.6);
|
|
251
|
+
const correctionFactor = Number(options.settings?.["wrapping.correctionFactor"] ?? 1);
|
|
252
|
+
const layerSpacing = options.spacing?.layer ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
253
|
+
const nodeSpacing = options.spacing?.node ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
254
|
+
const additionalSpacing = Number(options.settings?.["wrapping.additionalEdgeSpacing"] ?? 10);
|
|
255
|
+
const estimatedRowStep = maximumHeight + nodeSpacing + 1 + additionalSpacing * 2;
|
|
256
|
+
const automaticColumns = Math.min(
|
|
257
|
+
orderedIds.length,
|
|
258
|
+
Math.max(1, Math.ceil(Math.sqrt(orderedIds.length * aspectRatio * correctionFactor))),
|
|
259
|
+
);
|
|
260
|
+
const cuttingStrategy = String(options.settings?.["wrapping.cutting.strategy"] ?? "MSD");
|
|
261
|
+
const initialRows = Math.ceil(orderedIds.length / automaticColumns);
|
|
262
|
+
const freedom = Math.max(0, Number(options.settings?.["wrapping.cutting.msd.freedom"] ?? 1));
|
|
263
|
+
let automaticRows = initialRows;
|
|
264
|
+
if (cuttingStrategy === "MSD") {
|
|
265
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
266
|
+
for (
|
|
267
|
+
let rows = Math.max(1, initialRows - freedom);
|
|
268
|
+
rows <= Math.min(orderedIds.length, initialRows + freedom);
|
|
269
|
+
rows++
|
|
270
|
+
) {
|
|
271
|
+
const score = Math.max(
|
|
272
|
+
Math.ceil(orderedIds.length / rows) * (maximumWidth + layerSpacing),
|
|
273
|
+
aspectRatio * correctionFactor * rows * estimatedRowStep,
|
|
274
|
+
);
|
|
275
|
+
if (score < bestScore) {
|
|
276
|
+
bestScore = score;
|
|
277
|
+
automaticRows = rows;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let cuts =
|
|
282
|
+
cuttingStrategy === "MANUAL" && Array.isArray(options.settings?.["wrapping.cutting.cuts"])
|
|
283
|
+
? (options.settings["wrapping.cutting.cuts"] as unknown[])
|
|
284
|
+
.map(Number)
|
|
285
|
+
.filter((cut) => Number.isInteger(cut) && cut > 0 && cut < orderedIds.length)
|
|
286
|
+
.sort((left, right) => left - right)
|
|
287
|
+
: Array.from({ length: Math.max(0, automaticRows - 1) }, (_, index) =>
|
|
288
|
+
cuttingStrategy === "ARD"
|
|
289
|
+
? Math.round(((index + 1) * orderedIds.length) / automaticRows)
|
|
290
|
+
: Math.min(
|
|
291
|
+
orderedIds.length - 1,
|
|
292
|
+
(index + 1) * Math.ceil(orderedIds.length / automaticRows),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
cuts = [...new Set(cuts)];
|
|
296
|
+
const forbidden = new Set(
|
|
297
|
+
Array.isArray(options.settings?.["wrapping.validify.forbiddenIndices"])
|
|
298
|
+
? (options.settings["wrapping.validify.forbiddenIndices"] as unknown[]).map(Number)
|
|
299
|
+
: [],
|
|
300
|
+
);
|
|
301
|
+
const validify = String(options.settings?.["wrapping.validify.strategy"] ?? "GREEDY");
|
|
302
|
+
if (validify !== "NO" && forbidden.size > 0) {
|
|
303
|
+
const adjusted: number[] = [];
|
|
304
|
+
let offset = 0;
|
|
305
|
+
for (const desired of cuts) {
|
|
306
|
+
const current = desired + offset;
|
|
307
|
+
let upper = current;
|
|
308
|
+
while (upper < orderedIds.length && forbidden.has(upper)) upper++;
|
|
309
|
+
let selected = upper;
|
|
310
|
+
if (validify === "LOOK_BACK") {
|
|
311
|
+
let lower = current;
|
|
312
|
+
while (lower > 0 && forbidden.has(lower)) lower--;
|
|
313
|
+
if (current - lower <= upper - current && lower > (adjusted.at(-1) ?? 0)) selected = lower;
|
|
314
|
+
}
|
|
315
|
+
if (selected >= orderedIds.length) break;
|
|
316
|
+
if (selected > (adjusted.at(-1) ?? 0)) adjusted.push(selected);
|
|
317
|
+
offset += selected - current;
|
|
318
|
+
}
|
|
319
|
+
cuts = adjusted;
|
|
320
|
+
}
|
|
321
|
+
if (cuts.length === 0) return undefined;
|
|
322
|
+
const boundaries = [0, ...cuts, orderedIds.length];
|
|
323
|
+
const rowByIndex = new Map<number, { row: number; column: number }>();
|
|
324
|
+
for (let row = 0; row + 1 < boundaries.length; row++) {
|
|
325
|
+
for (let index = boundaries[row]!; index < boundaries[row + 1]!; index++) {
|
|
326
|
+
rowByIndex.set(index, { row, column: index - boundaries[row]! });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const padding =
|
|
330
|
+
typeof options.padding === "number"
|
|
331
|
+
? {
|
|
332
|
+
top: options.padding,
|
|
333
|
+
right: options.padding,
|
|
334
|
+
bottom: options.padding,
|
|
335
|
+
left: options.padding,
|
|
336
|
+
}
|
|
337
|
+
: {
|
|
338
|
+
top: options.padding?.top ?? 12,
|
|
339
|
+
right: options.padding?.right ?? 12,
|
|
340
|
+
bottom: options.padding?.bottom ?? 12,
|
|
341
|
+
left: options.padding?.left ?? 12,
|
|
342
|
+
};
|
|
343
|
+
const structuralMargin = strategy === "MULTI_EDGE" ? 30 : 10;
|
|
344
|
+
const rowStep = estimatedRowStep;
|
|
345
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
346
|
+
const visualNodeById = new Map<string, VisualNode<N, P>>();
|
|
347
|
+
for (const [index, id] of orderedIds.entries()) {
|
|
348
|
+
const node = nodeById.get(id)!;
|
|
349
|
+
const size = sizes.get(id)!;
|
|
350
|
+
const cell = rowByIndex.get(index) ?? { row: 0, column: index };
|
|
351
|
+
visualNodeById.set(id, {
|
|
352
|
+
...node,
|
|
353
|
+
x: padding.left + structuralMargin + cell.column * (maximumWidth + layerSpacing),
|
|
354
|
+
y: padding.top + cell.row * rowStep,
|
|
355
|
+
...size,
|
|
356
|
+
} as VisualNode<N, P>);
|
|
357
|
+
}
|
|
358
|
+
const visualEdgeById = new Map<string, VisualGraph<N, E, G, P>["edges"][number]>();
|
|
359
|
+
for (let index = 0; index + 1 < orderedIds.length; index++) {
|
|
360
|
+
const sourceId = orderedIds[index]!;
|
|
361
|
+
const targetId = orderedIds[index + 1]!;
|
|
362
|
+
const edge = edgeByPair.get(`${sourceId}\0${targetId}`)!;
|
|
363
|
+
const sourceNode = visualNodeById.get(sourceId)!;
|
|
364
|
+
const targetNode = visualNodeById.get(targetId)!;
|
|
365
|
+
const start = {
|
|
366
|
+
x: (sourceNode.x ?? 0) + (sourceNode.width ?? 0),
|
|
367
|
+
y: (sourceNode.y ?? 0) + (sourceNode.height ?? 0) / 2,
|
|
368
|
+
};
|
|
369
|
+
const end = {
|
|
370
|
+
x: targetNode.x ?? 0,
|
|
371
|
+
y: (targetNode.y ?? 0) + (targetNode.height ?? 0) / 2,
|
|
372
|
+
};
|
|
373
|
+
const wraps = cuts.includes(index + 1);
|
|
374
|
+
const points = wraps
|
|
375
|
+
? [
|
|
376
|
+
start,
|
|
377
|
+
{ x: start.x + structuralMargin, y: start.y },
|
|
378
|
+
{
|
|
379
|
+
x: start.x + structuralMargin,
|
|
380
|
+
y: start.y + nodeSpacing + additionalSpacing,
|
|
381
|
+
},
|
|
382
|
+
{ x: padding.left, y: start.y + nodeSpacing + additionalSpacing },
|
|
383
|
+
{ x: padding.left, y: end.y },
|
|
384
|
+
end,
|
|
385
|
+
]
|
|
386
|
+
: [start, end];
|
|
387
|
+
visualEdgeById.set(edge.id, {
|
|
388
|
+
...edge,
|
|
389
|
+
x: (start.x + end.x) / 2,
|
|
390
|
+
y: (start.y + end.y) / 2,
|
|
391
|
+
width: edge.width ?? 0,
|
|
392
|
+
height: edge.height ?? 0,
|
|
393
|
+
points,
|
|
394
|
+
routing: "orthogonal",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
...graph,
|
|
399
|
+
direction,
|
|
400
|
+
nodes: graph.nodes.map((node) => visualNodeById.get(node.id)!),
|
|
401
|
+
edges: graph.edges.map((edge) => visualEdgeById.get(edge.id)!),
|
|
402
|
+
} as VisualGraph<N, E, G, P>;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function runWrappedMultiEdgePipeline<N, E, G, P>(
|
|
406
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
407
|
+
options: LayeredLayoutOptions,
|
|
408
|
+
): VisualGraph<N, E, G, P> | undefined {
|
|
409
|
+
if (
|
|
410
|
+
(options.settings?.["wrapping.strategy"] ?? "OFF") !== "MULTI_EDGE" ||
|
|
411
|
+
(options.direction ?? graph.direction ?? "right") !== "right" ||
|
|
412
|
+
graph.nodes.length < 2
|
|
413
|
+
) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
const nodeIndex = new Map(graph.nodes.map((node, index) => [node.id, index]));
|
|
417
|
+
const outgoing = new Map(graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
418
|
+
const incoming = new Map(graph.nodes.map((node) => [node.id, [] as string[]]));
|
|
419
|
+
const indegree = new Map(graph.nodes.map((node) => [node.id, 0]));
|
|
420
|
+
for (const edge of graph.edges) {
|
|
421
|
+
if (
|
|
422
|
+
!nodeIndex.has(edge.sourceId) ||
|
|
423
|
+
!nodeIndex.has(edge.targetId) ||
|
|
424
|
+
edge.sourceId === edge.targetId
|
|
425
|
+
)
|
|
426
|
+
return undefined;
|
|
427
|
+
outgoing.get(edge.sourceId)!.push(edge.targetId);
|
|
428
|
+
incoming.get(edge.targetId)!.push(edge.sourceId);
|
|
429
|
+
indegree.set(edge.targetId, (indegree.get(edge.targetId) ?? 0) + 1);
|
|
430
|
+
}
|
|
431
|
+
const queue = graph.nodes
|
|
432
|
+
.filter((node) => indegree.get(node.id) === 0)
|
|
433
|
+
.sort((left, right) => nodeIndex.get(left.id)! - nodeIndex.get(right.id)!)
|
|
434
|
+
.map((node) => node.id);
|
|
435
|
+
const rank = new Map(graph.nodes.map((node) => [node.id, 0]));
|
|
436
|
+
const topological: string[] = [];
|
|
437
|
+
while (queue.length > 0) {
|
|
438
|
+
const id = queue.shift()!;
|
|
439
|
+
topological.push(id);
|
|
440
|
+
for (const targetId of outgoing.get(id) ?? []) {
|
|
441
|
+
rank.set(targetId, Math.max(rank.get(targetId) ?? 0, (rank.get(id) ?? 0) + 1));
|
|
442
|
+
indegree.set(targetId, (indegree.get(targetId) ?? 1) - 1);
|
|
443
|
+
if (indegree.get(targetId) === 0) {
|
|
444
|
+
queue.push(targetId);
|
|
445
|
+
queue.sort((left, right) => nodeIndex.get(left)! - nodeIndex.get(right)!);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (topological.length !== graph.nodes.length) return undefined;
|
|
450
|
+
const maximumRank = Math.max(...rank.values());
|
|
451
|
+
const nodeIdByRank = new Map<string | number, string>();
|
|
452
|
+
for (const [id, value] of rank) {
|
|
453
|
+
if (nodeIdByRank.has(value)) return undefined;
|
|
454
|
+
nodeIdByRank.set(value, id);
|
|
455
|
+
}
|
|
456
|
+
if (nodeIdByRank.size !== maximumRank + 1) return undefined;
|
|
457
|
+
const orderedIds = Array.from({ length: maximumRank + 1 }, (_, index) =>
|
|
458
|
+
nodeIdByRank.get(index)!,
|
|
459
|
+
);
|
|
460
|
+
const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
|
|
461
|
+
const layerSpacing = options.spacing?.layer ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
462
|
+
const nodeSpacing = options.spacing?.node ?? options.settings?.["spacing.baseValue"] ?? 20;
|
|
463
|
+
const widths = orderedIds.map((id) => sizes.get(id)!.width + layerSpacing);
|
|
464
|
+
const heights = orderedIds.map((id) => sizes.get(id)!.height + nodeSpacing);
|
|
465
|
+
const aspectRatio = Number(options.settings?.aspectRatio ?? 1.6);
|
|
466
|
+
const correctionFactor = Number(options.settings?.["wrapping.correctionFactor"] ?? 1);
|
|
467
|
+
const desiredAspectRatio = aspectRatio * correctionFactor;
|
|
468
|
+
const rowCount = Math.max(
|
|
469
|
+
1,
|
|
470
|
+
Math.min(
|
|
471
|
+
orderedIds.length,
|
|
472
|
+
Math.round(
|
|
473
|
+
Math.sqrt(
|
|
474
|
+
widths.reduce((sum, width) => sum + width, 0) /
|
|
475
|
+
(desiredAspectRatio * Math.max(...heights)),
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
const cuttingStrategy = String(options.settings?.["wrapping.cutting.strategy"] ?? "MSD");
|
|
481
|
+
let cuts: number[];
|
|
482
|
+
if (cuttingStrategy === "MANUAL" && Array.isArray(options.settings?.["wrapping.cutting.cuts"])) {
|
|
483
|
+
cuts = (options.settings["wrapping.cutting.cuts"] as unknown[])
|
|
484
|
+
.map(Number)
|
|
485
|
+
.filter((cut) => Number.isInteger(cut) && cut > 0 && cut < orderedIds.length)
|
|
486
|
+
.sort((left, right) => left - right);
|
|
487
|
+
} else if (cuttingStrategy === "ARD") {
|
|
488
|
+
cuts = Array.from({ length: rowCount - 1 }, (_, index) =>
|
|
489
|
+
Math.round(((index + 1) * orderedIds.length) / rowCount),
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
const freedom = Math.max(0, Number(options.settings?.["wrapping.cutting.msd.freedom"] ?? 1));
|
|
493
|
+
const prefixWidths: number[] = [];
|
|
494
|
+
widths.reduce((sum, width, index) => (prefixWidths[index] = sum + width), 0);
|
|
495
|
+
const totalWidth = prefixWidths.at(-1)!;
|
|
496
|
+
let bestScale = Number.NEGATIVE_INFINITY;
|
|
497
|
+
cuts = [];
|
|
498
|
+
for (
|
|
499
|
+
let cutCount = Math.max(0, rowCount - 1 - freedom);
|
|
500
|
+
cutCount <= Math.min(orderedIds.length - 1, rowCount - 1 + freedom);
|
|
501
|
+
cutCount++
|
|
502
|
+
) {
|
|
503
|
+
const rowWidth = totalWidth / (cutCount + 1);
|
|
504
|
+
const candidate: number[] = [];
|
|
505
|
+
let sumSoFar = 0;
|
|
506
|
+
let lastCutWidth = 0;
|
|
507
|
+
let maximumWidth = Number.NEGATIVE_INFINITY;
|
|
508
|
+
let totalHeight = 0;
|
|
509
|
+
let rowHeight = heights[0]!;
|
|
510
|
+
if (cutCount === 0) {
|
|
511
|
+
maximumWidth = totalWidth;
|
|
512
|
+
totalHeight = Math.max(...heights);
|
|
513
|
+
} else {
|
|
514
|
+
for (let index = 1; index < orderedIds.length; index++) {
|
|
515
|
+
if (prefixWidths[index - 1]! - sumSoFar >= rowWidth) {
|
|
516
|
+
candidate.push(index);
|
|
517
|
+
maximumWidth = Math.max(maximumWidth, prefixWidths[index - 1]! - lastCutWidth);
|
|
518
|
+
totalHeight += rowHeight;
|
|
519
|
+
sumSoFar += prefixWidths[index - 1]! - sumSoFar;
|
|
520
|
+
lastCutWidth = prefixWidths[index - 1]!;
|
|
521
|
+
rowHeight = heights[index]!;
|
|
522
|
+
}
|
|
523
|
+
rowHeight = Math.max(rowHeight, heights[index]!);
|
|
524
|
+
}
|
|
525
|
+
totalHeight += rowHeight;
|
|
526
|
+
}
|
|
527
|
+
const scale = Math.min(1 / maximumWidth, 1 / desiredAspectRatio / totalHeight);
|
|
528
|
+
if (scale > bestScale) {
|
|
529
|
+
bestScale = scale;
|
|
530
|
+
cuts = candidate;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
cuts = [...new Set(cuts)];
|
|
535
|
+
if (options.settings?.["wrapping.multiEdge.improveCuts"] ?? true) {
|
|
536
|
+
const spans = Array.from({ length: orderedIds.length + 1 }, () => 0);
|
|
537
|
+
for (const edge of graph.edges) {
|
|
538
|
+
const sourceRank = rank.get(edge.sourceId)!;
|
|
539
|
+
const targetRank = rank.get(edge.targetId)!;
|
|
540
|
+
for (let index = sourceRank + 1; index <= targetRank; index++) spans[index]!++;
|
|
541
|
+
}
|
|
542
|
+
const distancePenalty = Number(options.settings?.["wrapping.multiEdge.distancePenalty"] ?? 2);
|
|
543
|
+
type Cut = {
|
|
544
|
+
index: number;
|
|
545
|
+
newIndex: number;
|
|
546
|
+
assigned: boolean;
|
|
547
|
+
previous?: Cut;
|
|
548
|
+
next?: Cut;
|
|
549
|
+
};
|
|
550
|
+
const candidates: Cut[] = cuts.map((index) => ({ index, newIndex: index, assigned: false }));
|
|
551
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
552
|
+
candidates[index]!.previous = candidates[index - 1];
|
|
553
|
+
candidates[index]!.next = candidates[index + 1];
|
|
554
|
+
}
|
|
555
|
+
const nextUnassigned = (candidate: Cut | undefined): Cut | undefined => {
|
|
556
|
+
while (candidate?.assigned) candidate = candidate.next;
|
|
557
|
+
return candidate;
|
|
558
|
+
};
|
|
559
|
+
const improved: number[] = [];
|
|
560
|
+
for (let iteration = 0; iteration < candidates.length; iteration++) {
|
|
561
|
+
let left: Cut | undefined;
|
|
562
|
+
let right = nextUnassigned(candidates[0]);
|
|
563
|
+
let best: { candidate: Cut; index: number; score: number } | undefined;
|
|
564
|
+
for (let index = 1; index < orderedIds.length; index++) {
|
|
565
|
+
const rightDistance = right
|
|
566
|
+
? Math.abs(right.index - index)
|
|
567
|
+
: Math.abs(index - left!.index) + 1;
|
|
568
|
+
const leftDistance = left ? Math.abs(index - left.index) : rightDistance + 1;
|
|
569
|
+
const candidate = leftDistance < rightDistance ? left! : right!;
|
|
570
|
+
const distance = Math.min(leftDistance, rightDistance);
|
|
571
|
+
const score = spans[index]! + Math.pow(distance, distancePenalty);
|
|
572
|
+
if (!best || score < best.score) best = { candidate, index, score };
|
|
573
|
+
if (right && index === right.index) {
|
|
574
|
+
left = right;
|
|
575
|
+
right = nextUnassigned(right.next);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (!best) break;
|
|
579
|
+
const offset = best.index - best.candidate.index;
|
|
580
|
+
best.candidate.newIndex = best.index;
|
|
581
|
+
best.candidate.assigned = true;
|
|
582
|
+
improved.push(best.index);
|
|
583
|
+
let previous = best.candidate.previous;
|
|
584
|
+
while (previous && !previous.assigned) {
|
|
585
|
+
previous.index += offset;
|
|
586
|
+
previous = previous.previous;
|
|
587
|
+
}
|
|
588
|
+
let next = best.candidate.next;
|
|
589
|
+
while (next && !next.assigned) {
|
|
590
|
+
next.index += offset;
|
|
591
|
+
next = next.next;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
cuts = improved.sort((left, right) => left - right);
|
|
595
|
+
}
|
|
596
|
+
const forbidden = new Set(
|
|
597
|
+
Array.isArray(options.settings?.["wrapping.validify.forbiddenIndices"])
|
|
598
|
+
? (options.settings["wrapping.validify.forbiddenIndices"] as unknown[]).map(Number)
|
|
599
|
+
: [],
|
|
600
|
+
);
|
|
601
|
+
const validify = String(options.settings?.["wrapping.validify.strategy"] ?? "NO");
|
|
602
|
+
if (options.settings?.["wrapping.validify.strategy"] !== undefined && validify !== "NO") {
|
|
603
|
+
cuts = cuts.flatMap((cut, cutIndex) => {
|
|
604
|
+
const allowed = (index: number) => {
|
|
605
|
+
if (forbidden.size > 0) return !forbidden.has(index);
|
|
606
|
+
const targetId = orderedIds[index];
|
|
607
|
+
if (!targetId) return false;
|
|
608
|
+
const pairs = new Set(
|
|
609
|
+
(incoming.get(targetId) ?? []).map((sourceId) => `${sourceId}\0${targetId}`),
|
|
610
|
+
);
|
|
611
|
+
return pairs.size <= 1;
|
|
612
|
+
};
|
|
613
|
+
if (allowed(cut)) return [cut];
|
|
614
|
+
if (validify === "LOOK_BACK") {
|
|
615
|
+
for (let index = cut - 1; index > (cuts[cutIndex - 1] ?? 0); index--)
|
|
616
|
+
if (allowed(index)) return [index];
|
|
617
|
+
}
|
|
618
|
+
for (let index = cut + 1; index < orderedIds.length; index++)
|
|
619
|
+
if (allowed(index)) return [index];
|
|
620
|
+
return [];
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
cuts = [...new Set(cuts)].sort((left, right) => left - right);
|
|
624
|
+
if (cuts.length === 0) return undefined;
|
|
625
|
+
|
|
626
|
+
const wrappedPadding =
|
|
627
|
+
typeof options.padding === "number"
|
|
628
|
+
? {
|
|
629
|
+
top: options.padding,
|
|
630
|
+
right: options.padding,
|
|
631
|
+
bottom: options.padding,
|
|
632
|
+
left: options.padding,
|
|
633
|
+
}
|
|
634
|
+
: {
|
|
635
|
+
top: options.padding?.top ?? 12,
|
|
636
|
+
right: options.padding?.right ?? 12,
|
|
637
|
+
bottom: options.padding?.bottom ?? 12,
|
|
638
|
+
left: options.padding?.left ?? 12,
|
|
639
|
+
};
|
|
640
|
+
const phaseInput: LayeredPhaseInput = {
|
|
641
|
+
graph: graph as Graph<unknown, unknown, unknown, unknown>,
|
|
642
|
+
sizes,
|
|
643
|
+
direction: "right",
|
|
644
|
+
spacing: { node: nodeSpacing, layer: layerSpacing },
|
|
645
|
+
padding: wrappedPadding,
|
|
646
|
+
constrainedLayerByNodeId: new Map(),
|
|
647
|
+
settings: options.settings ?? {},
|
|
648
|
+
...(options.nodeSettings === undefined ? {} : { nodeSettings: options.nodeSettings }),
|
|
649
|
+
...(options.edgeSettings === undefined ? {} : { edgeSettings: options.edgeSettings }),
|
|
650
|
+
...(options.portSettings === undefined ? {} : { portSettings: options.portSettings }),
|
|
651
|
+
};
|
|
652
|
+
const prepared = insertMultiEdgeBreakingPoints(
|
|
653
|
+
phaseInput,
|
|
654
|
+
{ reversedEdgeIds: new Set() },
|
|
655
|
+
{ layerByNodeId: rank },
|
|
656
|
+
cuts,
|
|
657
|
+
);
|
|
658
|
+
const brokenExpansion = splitLongEdges(prepared.input, prepared.orientation, prepared.assignment);
|
|
659
|
+
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
660
|
+
const crossingMinimizer =
|
|
661
|
+
crossingStrategy === "MEDIAN_LAYER_SWEEP"
|
|
662
|
+
? minimizeCrossingsWithMedian(options.crossingSweeps)
|
|
663
|
+
: crossingStrategy === "INTERACTIVE"
|
|
664
|
+
? minimizeCrossingsInteractively
|
|
665
|
+
: crossingStrategy === "NONE"
|
|
666
|
+
? minimizeCrossingsWithModelOrder
|
|
667
|
+
: minimizeCrossingsWithBarycenter(options.crossingSweeps);
|
|
668
|
+
const crossedOrder = applyLayerConstraintOrder(
|
|
669
|
+
brokenExpansion.input,
|
|
670
|
+
applyGreedySwitch(
|
|
671
|
+
brokenExpansion.input,
|
|
672
|
+
brokenExpansion.orientation,
|
|
673
|
+
applySemiInteractiveOrder(
|
|
674
|
+
brokenExpansion.input,
|
|
675
|
+
applyForcedModelOrder(
|
|
676
|
+
brokenExpansion.input,
|
|
677
|
+
brokenExpansion.orientation,
|
|
678
|
+
crossingMinimizer(
|
|
679
|
+
brokenExpansion.input,
|
|
680
|
+
brokenExpansion.orientation,
|
|
681
|
+
brokenExpansion.assignment,
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
);
|
|
687
|
+
const folded = foldMultiEdgeBreakingPoints(brokenExpansion, crossedOrder, prepared);
|
|
688
|
+
const placementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
689
|
+
const placement =
|
|
690
|
+
placementStrategy === "INTERACTIVE"
|
|
691
|
+
? placeNodesInteractively(folded.expansion.input, folded.order)
|
|
692
|
+
: placementStrategy === "LINEAR_SEGMENTS"
|
|
693
|
+
? placeNodesWithLinearSegments(folded.expansion.input, folded.order)
|
|
694
|
+
: placementStrategy === "NETWORK_SIMPLEX"
|
|
695
|
+
? placeNodesWithNetworkSimplex(folded.expansion.input, folded.order)
|
|
696
|
+
: placementStrategy === "SIMPLE"
|
|
697
|
+
? placeNodesInLayers(folded.expansion.input, folded.order)
|
|
698
|
+
: placeNodesWithBrandesKoepf(folded.expansion.input, folded.order);
|
|
699
|
+
const routing = options.settings?.edgeRouting ?? "ORTHOGONAL";
|
|
700
|
+
const router =
|
|
701
|
+
routing === "POLYLINE"
|
|
702
|
+
? routeEdgesWithPolylines
|
|
703
|
+
: routing === "SPLINES"
|
|
704
|
+
? routeEdgesWithSplines
|
|
705
|
+
: routeEdgesOrthogonally;
|
|
706
|
+
const internalRoutes = router(folded.expansion.input, folded.expansion.orientation, placement);
|
|
707
|
+
const publicPointsByEdgeId = joinFoldedMultiEdgeRoutes(
|
|
708
|
+
graph as Graph<unknown, unknown, unknown, unknown>,
|
|
709
|
+
prepared,
|
|
710
|
+
folded,
|
|
711
|
+
placement,
|
|
712
|
+
internalRoutes,
|
|
713
|
+
routing,
|
|
714
|
+
);
|
|
715
|
+
const visualNodeById = new Map<string, VisualNode<N, P>>();
|
|
716
|
+
for (const node of graph.nodes) {
|
|
717
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
718
|
+
if (!rect) return undefined;
|
|
719
|
+
visualNodeById.set(node.id, {
|
|
720
|
+
...node,
|
|
721
|
+
...rect,
|
|
722
|
+
ports: placePorts(node.ports, rect, "right", (port) => options.portSettings?.(port, node), {
|
|
723
|
+
...options.settings,
|
|
724
|
+
...options.nodeSettings?.(node),
|
|
725
|
+
}),
|
|
726
|
+
} as VisualNode<N, P>);
|
|
727
|
+
}
|
|
728
|
+
const visualEdges = graph.edges.map((edge) => {
|
|
729
|
+
const points = publicPointsByEdgeId.get(edge.id) ?? [];
|
|
730
|
+
const first = points[0] ?? { x: 0, y: 0 };
|
|
731
|
+
const last = points.at(-1) ?? first;
|
|
732
|
+
return {
|
|
733
|
+
...edge,
|
|
734
|
+
x: (first.x + last.x) / 2,
|
|
735
|
+
y: (first.y + last.y) / 2,
|
|
736
|
+
width: edge.width ?? 0,
|
|
737
|
+
height: edge.height ?? 0,
|
|
738
|
+
points,
|
|
739
|
+
routing: routing === "SPLINES" ? ("spline" as const) : ("orthogonal" as const),
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
return {
|
|
743
|
+
...graph,
|
|
744
|
+
direction: "right",
|
|
745
|
+
nodes: graph.nodes.map((node) => visualNodeById.get(node.id)!),
|
|
746
|
+
edges: visualEdges,
|
|
747
|
+
} as VisualGraph<N, E, G, P>;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function runWrappedPipeline<N, E, G, P>(
|
|
751
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
752
|
+
options: LayeredLayoutOptions,
|
|
753
|
+
): VisualGraph<N, E, G, P> | undefined {
|
|
754
|
+
return runWrappedPathPipeline(graph, options) ?? runWrappedMultiEdgePipeline(graph, options);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function runCommentBoxPipeline<N, E, G, P>(
|
|
758
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
759
|
+
options: LayeredLayoutOptions,
|
|
760
|
+
context?: LayoutExecutionContext,
|
|
761
|
+
): VisualGraph<N, E, G, P> | undefined {
|
|
762
|
+
const commentIds = new Set(
|
|
763
|
+
graph.nodes
|
|
764
|
+
.filter(
|
|
765
|
+
(node) =>
|
|
766
|
+
options.nodeSettings?.(node)?.commentBox === true &&
|
|
767
|
+
graph.edges.filter((edge) => edge.sourceId === node.id || edge.targetId === node.id)
|
|
768
|
+
.length === 1,
|
|
769
|
+
)
|
|
770
|
+
.map((node) => node.id),
|
|
771
|
+
);
|
|
772
|
+
if (commentIds.size === 0) return undefined;
|
|
773
|
+
const ordinaryNodeById = new Map(
|
|
774
|
+
graph.nodes.filter((node) => !commentIds.has(node.id)).map((node) => [node.id, node]),
|
|
775
|
+
);
|
|
776
|
+
const commentsByTargetId = new Map<string, GraphNode[]>();
|
|
777
|
+
for (const commentId of commentIds) {
|
|
778
|
+
const attachment = graph.edges.find(
|
|
779
|
+
(edge) =>
|
|
780
|
+
(edge.sourceId === commentId && ordinaryNodeById.has(edge.targetId)) ||
|
|
781
|
+
(edge.targetId === commentId && ordinaryNodeById.has(edge.sourceId)),
|
|
782
|
+
);
|
|
783
|
+
if (!attachment) continue;
|
|
784
|
+
const targetId = attachment.sourceId === commentId ? attachment.targetId : attachment.sourceId;
|
|
785
|
+
const comments = commentsByTargetId.get(targetId) ?? [];
|
|
786
|
+
const comment = graph.nodes.find((node) => node.id === commentId);
|
|
787
|
+
if (comment) comments.push(comment);
|
|
788
|
+
commentsByTargetId.set(targetId, comments);
|
|
789
|
+
}
|
|
790
|
+
if (commentsByTargetId.size !== 1) return undefined;
|
|
791
|
+
|
|
792
|
+
const [targetId, comments] = [...commentsByTargetId][0]!;
|
|
793
|
+
const target = ordinaryNodeById.get(targetId);
|
|
794
|
+
if (!target) return undefined;
|
|
795
|
+
const direction = options.direction ?? graph.direction ?? "right";
|
|
796
|
+
const horizontal = direction === "left" || direction === "right";
|
|
797
|
+
const commentNodeSpacing = Number(options.settings?.["spacing.commentNode"] ?? 10);
|
|
798
|
+
const commentCommentSpacing = Number(options.settings?.["spacing.commentComment"] ?? 10);
|
|
799
|
+
const targetSize = getNodeSize(target, options);
|
|
800
|
+
const commentSizes = comments.map((comment) => getNodeSize(comment, options));
|
|
801
|
+
const beforeIndexes = comments.flatMap((_, index) => (index % 2 === 0 ? [index] : []));
|
|
802
|
+
const afterIndexes = comments.flatMap((_, index) => (index % 2 === 1 ? [index] : []));
|
|
803
|
+
const rowFlowSize = (indexes: readonly number[]) =>
|
|
804
|
+
indexes.reduce(
|
|
805
|
+
(sum, index) => sum + (horizontal ? commentSizes[index]!.width : commentSizes[index]!.height),
|
|
806
|
+
0,
|
|
807
|
+
) +
|
|
808
|
+
Math.max(0, indexes.length - 1) * commentCommentSpacing;
|
|
809
|
+
const rowCrossSize = (indexes: readonly number[]) =>
|
|
810
|
+
Math.max(
|
|
811
|
+
0,
|
|
812
|
+
...indexes.map((index) =>
|
|
813
|
+
horizontal ? commentSizes[index]!.height : commentSizes[index]!.width,
|
|
814
|
+
),
|
|
815
|
+
);
|
|
816
|
+
const beforeFlowSize = rowFlowSize(beforeIndexes);
|
|
817
|
+
const afterFlowSize = rowFlowSize(afterIndexes);
|
|
818
|
+
const beforeCrossSize = rowCrossSize(beforeIndexes);
|
|
819
|
+
const afterCrossSize = rowCrossSize(afterIndexes);
|
|
820
|
+
const targetCrossOffset = beforeCrossSize + (beforeIndexes.length > 0 ? commentNodeSpacing : 0);
|
|
821
|
+
const groupSize = horizontal
|
|
822
|
+
? {
|
|
823
|
+
width: Math.max(targetSize.width, beforeFlowSize, afterFlowSize),
|
|
824
|
+
height:
|
|
825
|
+
targetCrossOffset +
|
|
826
|
+
targetSize.height +
|
|
827
|
+
(afterIndexes.length > 0 ? commentNodeSpacing : 0) +
|
|
828
|
+
afterCrossSize,
|
|
829
|
+
}
|
|
830
|
+
: {
|
|
831
|
+
width:
|
|
832
|
+
targetCrossOffset +
|
|
833
|
+
targetSize.width +
|
|
834
|
+
(afterIndexes.length > 0 ? commentNodeSpacing : 0) +
|
|
835
|
+
afterCrossSize,
|
|
836
|
+
height: Math.max(targetSize.height, beforeFlowSize, afterFlowSize),
|
|
837
|
+
};
|
|
838
|
+
const baseGraph = {
|
|
839
|
+
...graph,
|
|
840
|
+
nodes: graph.nodes
|
|
841
|
+
.filter((node) => !commentIds.has(node.id))
|
|
842
|
+
.map((node) => (node.id === targetId ? { ...node, ...groupSize } : node)),
|
|
843
|
+
edges: graph.edges.filter(
|
|
844
|
+
(edge) => !commentIds.has(edge.sourceId) && !commentIds.has(edge.targetId),
|
|
845
|
+
),
|
|
846
|
+
} as Graph<N, E, G, P>;
|
|
847
|
+
const base = runLayeredPipeline(
|
|
848
|
+
baseGraph,
|
|
849
|
+
{
|
|
850
|
+
...options,
|
|
851
|
+
measure: (node) =>
|
|
852
|
+
node.id === targetId ? groupSize : (options.measure?.(node) ?? getNodeSize(node, options)),
|
|
853
|
+
},
|
|
854
|
+
context,
|
|
855
|
+
);
|
|
856
|
+
const groupRect = base.nodes.find((node) => node.id === targetId);
|
|
857
|
+
if (!groupRect) return undefined;
|
|
858
|
+
const crossShift =
|
|
859
|
+
targetCrossOffset -
|
|
860
|
+
((horizontal ? groupSize.height : groupSize.width) -
|
|
861
|
+
(horizontal ? targetSize.height : targetSize.width)) /
|
|
862
|
+
2;
|
|
863
|
+
const visualNodeById = new Map<string, VisualNode<N, P>>();
|
|
864
|
+
for (const node of base.nodes) {
|
|
865
|
+
if (node.id === targetId) continue;
|
|
866
|
+
visualNodeById.set(node.id, {
|
|
867
|
+
...node,
|
|
868
|
+
x: horizontal ? node.x : (node.x ?? 0) + crossShift,
|
|
869
|
+
y: horizontal ? (node.y ?? 0) + crossShift : node.y,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const targetFlowInset =
|
|
873
|
+
((horizontal ? groupSize.width : groupSize.height) -
|
|
874
|
+
(horizontal ? targetSize.width : targetSize.height)) /
|
|
875
|
+
2;
|
|
876
|
+
const targetRect = {
|
|
877
|
+
...target,
|
|
878
|
+
x: horizontal ? (groupRect.x ?? 0) + targetFlowInset : (groupRect.x ?? 0) + targetCrossOffset,
|
|
879
|
+
y: horizontal ? (groupRect.y ?? 0) + targetCrossOffset : (groupRect.y ?? 0) + targetFlowInset,
|
|
880
|
+
...targetSize,
|
|
881
|
+
} as VisualNode<N, P>;
|
|
882
|
+
visualNodeById.set(targetId, targetRect);
|
|
883
|
+
|
|
884
|
+
for (const [indexes, before] of [
|
|
885
|
+
[beforeIndexes, true],
|
|
886
|
+
[afterIndexes, false],
|
|
887
|
+
] as const) {
|
|
888
|
+
let flowOffset = ((horizontal ? groupSize.width : groupSize.height) - rowFlowSize(indexes)) / 2;
|
|
889
|
+
for (const index of indexes) {
|
|
890
|
+
const comment = comments[index]!;
|
|
891
|
+
const size = commentSizes[index]!;
|
|
892
|
+
const visual = {
|
|
893
|
+
...comment,
|
|
894
|
+
x: horizontal
|
|
895
|
+
? (groupRect.x ?? 0) + flowOffset
|
|
896
|
+
: before
|
|
897
|
+
? (targetRect.x ?? 0) - commentNodeSpacing - size.width
|
|
898
|
+
: (targetRect.x ?? 0) + (targetRect.width ?? 0) + commentNodeSpacing,
|
|
899
|
+
y: horizontal
|
|
900
|
+
? before
|
|
901
|
+
? (targetRect.y ?? 0) - commentNodeSpacing - size.height
|
|
902
|
+
: (targetRect.y ?? 0) + (targetRect.height ?? 0) + commentNodeSpacing
|
|
903
|
+
: (groupRect.y ?? 0) + flowOffset,
|
|
904
|
+
...size,
|
|
905
|
+
} as VisualNode<N, P>;
|
|
906
|
+
visualNodeById.set(comment.id, visual);
|
|
907
|
+
flowOffset += (horizontal ? size.width : size.height) + commentCommentSpacing;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const normalEdgeById = new Map(
|
|
912
|
+
base.edges.map((edge) => {
|
|
913
|
+
const points = (edge.points ?? []).map((point) => ({
|
|
914
|
+
x: horizontal ? point.x : point.x + crossShift,
|
|
915
|
+
y: horizontal ? point.y + crossShift : point.y,
|
|
916
|
+
}));
|
|
917
|
+
const endpoint = (source: boolean) => {
|
|
918
|
+
if (horizontal) {
|
|
919
|
+
return {
|
|
920
|
+
x:
|
|
921
|
+
(direction === "right") === source
|
|
922
|
+
? (targetRect.x ?? 0) + (targetRect.width ?? 0)
|
|
923
|
+
: (targetRect.x ?? 0),
|
|
924
|
+
y: (targetRect.y ?? 0) + (targetRect.height ?? 0) / 2,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
x: (targetRect.x ?? 0) + (targetRect.width ?? 0) / 2,
|
|
929
|
+
y:
|
|
930
|
+
(direction === "down") === source
|
|
931
|
+
? (targetRect.y ?? 0) + (targetRect.height ?? 0)
|
|
932
|
+
: (targetRect.y ?? 0),
|
|
933
|
+
};
|
|
934
|
+
};
|
|
935
|
+
if (edge.sourceId === targetId && points.length > 0) points[0] = endpoint(true);
|
|
936
|
+
if (edge.targetId === targetId && points.length > 0)
|
|
937
|
+
points[points.length - 1] = endpoint(false);
|
|
938
|
+
return [
|
|
939
|
+
edge.id,
|
|
940
|
+
{
|
|
941
|
+
...edge,
|
|
942
|
+
x: horizontal ? edge.x : (edge.x ?? 0) + crossShift,
|
|
943
|
+
y: horizontal ? (edge.y ?? 0) + crossShift : edge.y,
|
|
944
|
+
points,
|
|
945
|
+
},
|
|
946
|
+
] as const;
|
|
947
|
+
}),
|
|
948
|
+
);
|
|
949
|
+
for (const edge of graph.edges) {
|
|
950
|
+
const commentId = commentIds.has(edge.sourceId)
|
|
951
|
+
? edge.sourceId
|
|
952
|
+
: commentIds.has(edge.targetId)
|
|
953
|
+
? edge.targetId
|
|
954
|
+
: undefined;
|
|
955
|
+
if (!commentId) continue;
|
|
956
|
+
const comment = visualNodeById.get(commentId)!;
|
|
957
|
+
const commentIndex = comments.findIndex((candidate) => candidate.id === commentId);
|
|
958
|
+
const sideIndexes = commentIndex % 2 === 0 ? beforeIndexes : afterIndexes;
|
|
959
|
+
const sideRank = sideIndexes.indexOf(commentIndex);
|
|
960
|
+
const targetFlowRatio = (sideRank + 1) / (sideIndexes.length + 1);
|
|
961
|
+
const before = horizontal
|
|
962
|
+
? (comment.y ?? 0) < (targetRect.y ?? 0)
|
|
963
|
+
: (comment.x ?? 0) < (targetRect.x ?? 0);
|
|
964
|
+
const start = horizontal
|
|
965
|
+
? {
|
|
966
|
+
x: (comment.x ?? 0) + (comment.width ?? 0) / 2,
|
|
967
|
+
y: before ? (comment.y ?? 0) + (comment.height ?? 0) : (comment.y ?? 0),
|
|
968
|
+
}
|
|
969
|
+
: {
|
|
970
|
+
x: before ? (comment.x ?? 0) + (comment.width ?? 0) : (comment.x ?? 0),
|
|
971
|
+
y: (comment.y ?? 0) + (comment.height ?? 0) / 2,
|
|
972
|
+
};
|
|
973
|
+
const end = horizontal
|
|
974
|
+
? {
|
|
975
|
+
x: (targetRect.x ?? 0) + (targetRect.width ?? 0) * targetFlowRatio,
|
|
976
|
+
y: before ? (targetRect.y ?? 0) : (targetRect.y ?? 0) + (targetRect.height ?? 0),
|
|
977
|
+
}
|
|
978
|
+
: {
|
|
979
|
+
x: before ? (targetRect.x ?? 0) : (targetRect.x ?? 0) + (targetRect.width ?? 0),
|
|
980
|
+
y: (targetRect.y ?? 0) + (targetRect.height ?? 0) * targetFlowRatio,
|
|
981
|
+
};
|
|
982
|
+
normalEdgeById.set(edge.id, {
|
|
983
|
+
...edge,
|
|
984
|
+
x: (start.x + end.x) / 2,
|
|
985
|
+
y: (start.y + end.y) / 2,
|
|
986
|
+
width: edge.width ?? 0,
|
|
987
|
+
height: edge.height ?? 0,
|
|
988
|
+
points: edge.sourceId === commentId ? [start, end] : [end, start],
|
|
989
|
+
routing: "orthogonal",
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
...graph,
|
|
994
|
+
direction,
|
|
995
|
+
nodes: graph.nodes.map((node) => visualNodeById.get(node.id)!),
|
|
996
|
+
edges: graph.edges.map((edge) => normalEdgeById.get(edge.id)!),
|
|
997
|
+
} as VisualGraph<N, E, G, P>;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function runSeparatedComponents<N, E, G, P>(
|
|
1001
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
1002
|
+
options: LayeredLayoutOptions,
|
|
1003
|
+
context?: LayoutExecutionContext,
|
|
1004
|
+
): VisualGraph<N, E, G, P> | undefined {
|
|
1005
|
+
if (graph.nodes.length < 2) return undefined;
|
|
1006
|
+
|
|
1007
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1008
|
+
const neighbors = new Map(graph.nodes.map((node) => [node.id, new Set<string>()]));
|
|
1009
|
+
for (const edge of graph.edges) {
|
|
1010
|
+
if (edge.sourceId === edge.targetId) continue;
|
|
1011
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) continue;
|
|
1012
|
+
neighbors.get(edge.sourceId)!.add(edge.targetId);
|
|
1013
|
+
neighbors.get(edge.targetId)!.add(edge.sourceId);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const visited = new Set<string>();
|
|
1017
|
+
const componentNodeIds: string[][] = [];
|
|
1018
|
+
for (const node of graph.nodes) {
|
|
1019
|
+
if (visited.has(node.id)) continue;
|
|
1020
|
+
const ids: string[] = [];
|
|
1021
|
+
const pending = [node.id];
|
|
1022
|
+
visited.add(node.id);
|
|
1023
|
+
while (pending.length > 0) {
|
|
1024
|
+
const id = pending.pop()!;
|
|
1025
|
+
ids.push(id);
|
|
1026
|
+
for (const neighbor of neighbors.get(id) ?? []) {
|
|
1027
|
+
if (visited.has(neighbor)) continue;
|
|
1028
|
+
visited.add(neighbor);
|
|
1029
|
+
pending.push(neighbor);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
componentNodeIds.push(ids);
|
|
1033
|
+
}
|
|
1034
|
+
if (componentNodeIds.length < 2) return undefined;
|
|
1035
|
+
|
|
1036
|
+
const boundsOf = (result: VisualGraph<N, E, G, P>) => {
|
|
1037
|
+
let left = Number.POSITIVE_INFINITY;
|
|
1038
|
+
let top = Number.POSITIVE_INFINITY;
|
|
1039
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
1040
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
1041
|
+
const include = (x: number, y: number, width = 0, height = 0): void => {
|
|
1042
|
+
left = Math.min(left, x);
|
|
1043
|
+
top = Math.min(top, y);
|
|
1044
|
+
right = Math.max(right, x + width);
|
|
1045
|
+
bottom = Math.max(bottom, y + height);
|
|
1046
|
+
};
|
|
1047
|
+
for (const node of result.nodes) {
|
|
1048
|
+
include(node.x ?? 0, node.y ?? 0, node.width ?? 0, node.height ?? 0);
|
|
1049
|
+
for (const port of node.ports ?? []) {
|
|
1050
|
+
include(
|
|
1051
|
+
(node.x ?? 0) + (port.x ?? 0),
|
|
1052
|
+
(node.y ?? 0) + (port.y ?? 0),
|
|
1053
|
+
port.width ?? 0,
|
|
1054
|
+
port.height ?? 0,
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
for (const edge of result.edges) {
|
|
1059
|
+
for (const point of edge.points ?? []) include(point.x, point.y);
|
|
1060
|
+
if ((edge.width ?? 0) > 0 || (edge.height ?? 0) > 0) {
|
|
1061
|
+
include(edge.x ?? 0, edge.y ?? 0, edge.width ?? 0, edge.height ?? 0);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
left,
|
|
1066
|
+
top,
|
|
1067
|
+
width: Math.max(0, right - left),
|
|
1068
|
+
height: Math.max(0, bottom - top),
|
|
1069
|
+
};
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const components = componentNodeIds.map((ids, modelOrder) => {
|
|
1073
|
+
const idSet = new Set(ids);
|
|
1074
|
+
const result = runLayeredPipeline(
|
|
1075
|
+
{
|
|
1076
|
+
...graph,
|
|
1077
|
+
nodes: graph.nodes.filter((node) => idSet.has(node.id)),
|
|
1078
|
+
edges: graph.edges.filter((edge) => idSet.has(edge.sourceId) && idSet.has(edge.targetId)),
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
...options,
|
|
1082
|
+
padding: 0,
|
|
1083
|
+
settings: { ...options.settings, separateConnectedComponents: false },
|
|
1084
|
+
},
|
|
1085
|
+
context,
|
|
1086
|
+
);
|
|
1087
|
+
const bounds = boundsOf(result);
|
|
1088
|
+
return { result, bounds, modelOrder, area: bounds.width * bounds.height };
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
if ((options.settings?.["considerModelOrder.components"] ?? "NONE") === "NONE") {
|
|
1092
|
+
components.sort((left, right) => left.area - right.area || left.modelOrder - right.modelOrder);
|
|
1093
|
+
}
|
|
1094
|
+
const componentSpacing = Number(
|
|
1095
|
+
options.settings?.["spacing.componentComponent"] ??
|
|
1096
|
+
options.settings?.["spacing.baseValue"] ??
|
|
1097
|
+
20,
|
|
1098
|
+
);
|
|
1099
|
+
const aspectRatio = Number(options.settings?.aspectRatio ?? 1.6);
|
|
1100
|
+
const totalArea = components.reduce((sum, component) => sum + component.area, 0);
|
|
1101
|
+
const maxRowWidth = Math.max(
|
|
1102
|
+
...components.map((component) => component.bounds.width),
|
|
1103
|
+
Math.sqrt(totalArea) * aspectRatio,
|
|
1104
|
+
);
|
|
1105
|
+
const padding =
|
|
1106
|
+
typeof options.padding === "number"
|
|
1107
|
+
? {
|
|
1108
|
+
top: options.padding,
|
|
1109
|
+
right: options.padding,
|
|
1110
|
+
bottom: options.padding,
|
|
1111
|
+
left: options.padding,
|
|
1112
|
+
}
|
|
1113
|
+
: {
|
|
1114
|
+
top: options.padding?.top ?? 12,
|
|
1115
|
+
right: options.padding?.right ?? 12,
|
|
1116
|
+
bottom: options.padding?.bottom ?? 12,
|
|
1117
|
+
left: options.padding?.left ?? 12,
|
|
1118
|
+
};
|
|
1119
|
+
let x = 0;
|
|
1120
|
+
let y = 0;
|
|
1121
|
+
let rowHeight = 0;
|
|
1122
|
+
const compactConnectedComponents = options.settings?.["compaction.connectedComponents"] === true;
|
|
1123
|
+
const placedShapes: Array<{ left: number; right: number; top: number; bottom: number }> = [];
|
|
1124
|
+
const nodeResults = new Map<string, VisualNode<N, P>>();
|
|
1125
|
+
const edgeResults = new Map<string, (typeof components)[number]["result"]["edges"][number]>();
|
|
1126
|
+
for (const component of components) {
|
|
1127
|
+
if (x > 0 && x + component.bounds.width > maxRowWidth) {
|
|
1128
|
+
x = 0;
|
|
1129
|
+
y += rowHeight + componentSpacing;
|
|
1130
|
+
rowHeight = 0;
|
|
1131
|
+
}
|
|
1132
|
+
let compactedY = y;
|
|
1133
|
+
if (compactConnectedComponents && y > 0) {
|
|
1134
|
+
let requiredY = 0;
|
|
1135
|
+
for (const node of component.result.nodes) {
|
|
1136
|
+
const localLeft = x + (node.x ?? 0) - component.bounds.left;
|
|
1137
|
+
const localRight = localLeft + (node.width ?? 0);
|
|
1138
|
+
const localTop = (node.y ?? 0) - component.bounds.top;
|
|
1139
|
+
for (const placed of placedShapes) {
|
|
1140
|
+
if (localRight <= placed.left || localLeft >= placed.right) continue;
|
|
1141
|
+
requiredY = Math.max(requiredY, placed.bottom + componentSpacing - localTop);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
compactedY = Math.min(y, requiredY);
|
|
1145
|
+
}
|
|
1146
|
+
const dx = padding.left + x - component.bounds.left;
|
|
1147
|
+
const dy = padding.top + compactedY - component.bounds.top;
|
|
1148
|
+
for (const node of component.result.nodes) {
|
|
1149
|
+
nodeResults.set(node.id, { ...node, x: (node.x ?? 0) + dx, y: (node.y ?? 0) + dy });
|
|
1150
|
+
placedShapes.push({
|
|
1151
|
+
left: x + (node.x ?? 0) - component.bounds.left,
|
|
1152
|
+
right: x + (node.x ?? 0) - component.bounds.left + (node.width ?? 0),
|
|
1153
|
+
top: compactedY + (node.y ?? 0) - component.bounds.top,
|
|
1154
|
+
bottom: compactedY + (node.y ?? 0) - component.bounds.top + (node.height ?? 0),
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
for (const edge of component.result.edges) {
|
|
1158
|
+
edgeResults.set(edge.id, {
|
|
1159
|
+
...edge,
|
|
1160
|
+
x: (edge.x ?? 0) + dx,
|
|
1161
|
+
y: (edge.y ?? 0) + dy,
|
|
1162
|
+
points: edge.points?.map((point) => ({ x: point.x + dx, y: point.y + dy })),
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
x += component.bounds.width + componentSpacing;
|
|
1166
|
+
rowHeight = Math.max(rowHeight, component.bounds.height);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
...graph,
|
|
1171
|
+
direction: options.direction ?? graph.direction ?? "right",
|
|
1172
|
+
nodes: graph.nodes.map((node) => nodeResults.get(node.id)!),
|
|
1173
|
+
edges: graph.edges.map((edge) => edgeResults.get(edge.id)!),
|
|
1174
|
+
} as VisualGraph<N, E, G, P>;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function runCompoundPipeline<N, E, G, P>(
|
|
1178
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
1179
|
+
options: LayeredLayoutOptions,
|
|
1180
|
+
context?: LayoutExecutionContext,
|
|
1181
|
+
): VisualGraph<N, E, G, P> {
|
|
1182
|
+
const nodes = graph.nodes.map((node) => ({ ...node })) as VisualNode<N, P>[];
|
|
1183
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
1184
|
+
const depth = (node: GraphNode): number => {
|
|
1185
|
+
let value = 0;
|
|
1186
|
+
let parentId = node.parentId;
|
|
1187
|
+
const seen = new Set<string>();
|
|
1188
|
+
while (parentId != null && !seen.has(parentId)) {
|
|
1189
|
+
seen.add(parentId);
|
|
1190
|
+
value++;
|
|
1191
|
+
parentId = nodeById.get(parentId)?.parentId;
|
|
1192
|
+
}
|
|
1193
|
+
return value;
|
|
1194
|
+
};
|
|
1195
|
+
const parentIds = [
|
|
1196
|
+
...new Set(nodes.flatMap((node) => (node.parentId == null ? [] : [node.parentId]))),
|
|
1197
|
+
].sort((left, right) => depth(nodeById.get(right)!) - depth(nodeById.get(left)!));
|
|
1198
|
+
const edgeById = new Map(graph.edges.map((edge) => [edge.id, { ...edge }]));
|
|
1199
|
+
const padding = typeof options.padding === "number" ? options.padding : 12;
|
|
1200
|
+
|
|
1201
|
+
const layoutSiblings = (parentId: string | null): void => {
|
|
1202
|
+
const siblings = nodes.filter((node) => (node.parentId ?? null) === parentId);
|
|
1203
|
+
if (siblings.length === 0) return;
|
|
1204
|
+
const siblingIds = new Set(siblings.map((node) => node.id));
|
|
1205
|
+
const siblingEdges = graph.edges.filter(
|
|
1206
|
+
(edge) => siblingIds.has(edge.sourceId) && siblingIds.has(edge.targetId),
|
|
1207
|
+
);
|
|
1208
|
+
const flatNodes = siblings.map((node) => ({ ...node, parentId: null }));
|
|
1209
|
+
const flatGraph = {
|
|
1210
|
+
...graph,
|
|
1211
|
+
nodes: flatNodes,
|
|
1212
|
+
edges: siblingEdges,
|
|
1213
|
+
} as Graph<N, E, G, P>;
|
|
1214
|
+
const result = runLayeredPipeline(flatGraph, options, context);
|
|
1215
|
+
for (const laidOut of result.nodes) {
|
|
1216
|
+
const node = nodeById.get(laidOut.id);
|
|
1217
|
+
if (!node) continue;
|
|
1218
|
+
Object.assign(node, laidOut, { parentId });
|
|
1219
|
+
}
|
|
1220
|
+
for (const edge of result.edges) edgeById.set(edge.id, edge);
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
for (const parentId of parentIds) {
|
|
1224
|
+
layoutSiblings(parentId);
|
|
1225
|
+
const parent = nodeById.get(parentId);
|
|
1226
|
+
if (!parent) continue;
|
|
1227
|
+
const children = nodes.filter((node) => node.parentId === parentId);
|
|
1228
|
+
const right = Math.max(0, ...children.map((node) => (node.x ?? 0) + (node.width ?? 0)));
|
|
1229
|
+
const bottom = Math.max(0, ...children.map((node) => (node.y ?? 0) + (node.height ?? 0)));
|
|
1230
|
+
parent.width = Math.max(parent.width ?? 0, right + padding);
|
|
1231
|
+
parent.height = Math.max(parent.height ?? 0, bottom + padding);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const roots = nodes.filter((node) => node.parentId == null);
|
|
1235
|
+
if (roots.length === 1 && parentIds.includes(roots[0]!.id)) {
|
|
1236
|
+
Object.assign(roots[0]!, { x: 0, y: 0 });
|
|
1237
|
+
} else {
|
|
1238
|
+
layoutSiblings(null);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const absoluteRect = (id: string): { x: number; y: number; width: number; height: number } => {
|
|
1242
|
+
const node = nodeById.get(id);
|
|
1243
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1244
|
+
let x = node.x ?? 0;
|
|
1245
|
+
let y = node.y ?? 0;
|
|
1246
|
+
let parentId = node.parentId;
|
|
1247
|
+
const seen = new Set<string>();
|
|
1248
|
+
while (parentId != null && !seen.has(parentId)) {
|
|
1249
|
+
seen.add(parentId);
|
|
1250
|
+
const parent = nodeById.get(parentId);
|
|
1251
|
+
if (!parent) break;
|
|
1252
|
+
x += parent.x ?? 0;
|
|
1253
|
+
y += parent.y ?? 0;
|
|
1254
|
+
parentId = parent.parentId;
|
|
1255
|
+
}
|
|
1256
|
+
return { x, y, width: node.width ?? 0, height: node.height ?? 0 };
|
|
1257
|
+
};
|
|
1258
|
+
for (const edge of graph.edges) {
|
|
1259
|
+
if (edgeById.get(edge.id)?.points !== undefined) continue;
|
|
1260
|
+
const source = absoluteRect(edge.sourceId);
|
|
1261
|
+
const target = absoluteRect(edge.targetId);
|
|
1262
|
+
const start = { x: source.x + source.width, y: source.y + source.height / 2 };
|
|
1263
|
+
const end = { x: target.x, y: target.y + target.height / 2 };
|
|
1264
|
+
const track = (start.x + end.x) / 2;
|
|
1265
|
+
const points = [start, { x: track, y: start.y }, { x: track, y: end.y }, end];
|
|
1266
|
+
edgeById.set(edge.id, {
|
|
1267
|
+
...edge,
|
|
1268
|
+
points,
|
|
1269
|
+
x: track,
|
|
1270
|
+
y: (start.y + end.y) / 2,
|
|
1271
|
+
width: edge.width ?? 0,
|
|
1272
|
+
height: edge.height ?? 0,
|
|
1273
|
+
routing: "orthogonal",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
...graph,
|
|
1279
|
+
nodes,
|
|
1280
|
+
edges: graph.edges.map((edge) => edgeById.get(edge.id) ?? edge),
|
|
1281
|
+
} as VisualGraph<N, E, G, P>;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function runLayeredPipeline<N, E, G, P>(
|
|
1285
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
1286
|
+
options: LayeredLayoutOptions,
|
|
1287
|
+
context?: LayoutExecutionContext,
|
|
1288
|
+
): VisualGraph<N, E, G, P> {
|
|
1289
|
+
if (options.settings?.noLayout) {
|
|
1290
|
+
return {
|
|
1291
|
+
...graph,
|
|
1292
|
+
nodes: graph.nodes.map((node) => ({
|
|
1293
|
+
...node,
|
|
1294
|
+
x: node.x ?? 0,
|
|
1295
|
+
y: node.y ?? 0,
|
|
1296
|
+
width: node.width ?? 0,
|
|
1297
|
+
height: node.height ?? 0,
|
|
1298
|
+
})),
|
|
1299
|
+
edges: graph.edges.map((edge) => ({
|
|
1300
|
+
...edge,
|
|
1301
|
+
x: edge.x ?? 0,
|
|
1302
|
+
y: edge.y ?? 0,
|
|
1303
|
+
width: edge.width ?? 0,
|
|
1304
|
+
height: edge.height ?? 0,
|
|
1305
|
+
points: edge.points ?? [],
|
|
1306
|
+
routing: edge.routing ?? "polyline",
|
|
1307
|
+
})),
|
|
1308
|
+
} as VisualGraph<N, E, G, P>;
|
|
1309
|
+
}
|
|
1310
|
+
if (hasNestedNodes(graph)) return runCompoundPipeline(graph, options, context);
|
|
1311
|
+
const wrapped = runWrappedPipeline(graph, options);
|
|
1312
|
+
if (wrapped) return wrapped;
|
|
1313
|
+
const comments = runCommentBoxPipeline(graph, options, context);
|
|
1314
|
+
if (comments) return comments;
|
|
1315
|
+
if (options.settings?.separateConnectedComponents !== false) {
|
|
1316
|
+
const separated = runSeparatedComponents(graph, options, context);
|
|
1317
|
+
if (separated) return separated;
|
|
1318
|
+
}
|
|
1319
|
+
const direction = options.direction ?? graph.direction ?? "right";
|
|
1320
|
+
const padding =
|
|
1321
|
+
typeof options.padding === "number"
|
|
1322
|
+
? {
|
|
1323
|
+
top: options.padding,
|
|
1324
|
+
right: options.padding,
|
|
1325
|
+
bottom: options.padding,
|
|
1326
|
+
left: options.padding,
|
|
1327
|
+
}
|
|
1328
|
+
: {
|
|
1329
|
+
top: options.padding?.top ?? 12,
|
|
1330
|
+
right: options.padding?.right ?? 12,
|
|
1331
|
+
bottom: options.padding?.bottom ?? 12,
|
|
1332
|
+
left: options.padding?.left ?? 12,
|
|
1333
|
+
};
|
|
1334
|
+
const switchedSideByPort = new Map<string, "NORTH" | "SOUTH" | "WEST" | "EAST">();
|
|
1335
|
+
const input: LayeredPhaseInput = {
|
|
1336
|
+
graph: graph as Graph<unknown, unknown, unknown, unknown>,
|
|
1337
|
+
sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
|
|
1338
|
+
direction,
|
|
1339
|
+
spacing: {
|
|
1340
|
+
node: options.spacing?.node ?? options.settings?.["spacing.baseValue"] ?? 20,
|
|
1341
|
+
layer: options.spacing?.layer ?? options.settings?.["spacing.baseValue"] ?? 20,
|
|
1342
|
+
},
|
|
1343
|
+
padding,
|
|
1344
|
+
constrainedLayerByNodeId: new Map(
|
|
1345
|
+
graph.nodes.flatMap((node) => {
|
|
1346
|
+
const layer = options.constraints?.layer?.(node);
|
|
1347
|
+
return layer === undefined ? [] : [[node.id, layer] as const];
|
|
1348
|
+
}),
|
|
1349
|
+
),
|
|
1350
|
+
settings: options.settings ?? {},
|
|
1351
|
+
...(options.nodeSettings === undefined ? {} : { nodeSettings: options.nodeSettings }),
|
|
1352
|
+
...(options.edgeSettings === undefined ? {} : { edgeSettings: options.edgeSettings }),
|
|
1353
|
+
portSettings: (port, node) => ({
|
|
1354
|
+
...options.portSettings?.(port, node),
|
|
1355
|
+
...(switchedSideByPort.has(`${node.id}\0${port.name}`)
|
|
1356
|
+
? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) }
|
|
1357
|
+
: {}),
|
|
1358
|
+
}),
|
|
1359
|
+
};
|
|
1360
|
+
const measure = <T>(id: string, run: () => T): T => {
|
|
1361
|
+
context?.throwIfAborted();
|
|
1362
|
+
return context ? context.measurePhase(id, run) : run();
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
const cycleBreakingStrategy = options.settings?.["cycleBreaking.strategy"] ?? "GREEDY";
|
|
1366
|
+
const cycleBreaker = (() => {
|
|
1367
|
+
if (options.strategies?.breakCycles) return options.strategies.breakCycles;
|
|
1368
|
+
if (cycleBreakingStrategy === "GREEDY") return breakCyclesGreedily;
|
|
1369
|
+
if (cycleBreakingStrategy === "DEPTH_FIRST") return breakCyclesWithDepthFirstSearch;
|
|
1370
|
+
if (cycleBreakingStrategy === "INTERACTIVE") return breakCyclesInteractively;
|
|
1371
|
+
if (cycleBreakingStrategy === "MODEL_ORDER") return breakCyclesByModelOrder;
|
|
1372
|
+
if (cycleBreakingStrategy === "GREEDY_MODEL_ORDER") {
|
|
1373
|
+
return breakCyclesGreedilyByModelOrder;
|
|
1374
|
+
}
|
|
1375
|
+
if (cycleBreakingStrategy === "DFS_NODE_ORDER") {
|
|
1376
|
+
return breakCyclesWithModelOrderDepthFirstSearch;
|
|
1377
|
+
}
|
|
1378
|
+
if (cycleBreakingStrategy === "BFS_NODE_ORDER") {
|
|
1379
|
+
return breakCyclesWithModelOrderBreadthFirstSearch;
|
|
1380
|
+
}
|
|
1381
|
+
if (cycleBreakingStrategy === "SCC_CONNECTIVITY") {
|
|
1382
|
+
return breakCyclesByStronglyConnectedConnectivity;
|
|
1383
|
+
}
|
|
1384
|
+
if (cycleBreakingStrategy === "SCC_NODE_TYPE") {
|
|
1385
|
+
return breakCyclesByStronglyConnectedNodeType;
|
|
1386
|
+
}
|
|
1387
|
+
throw new UnsupportedLayoutError(
|
|
1388
|
+
`Cycle-breaking strategy ${cycleBreakingStrategy} is not implemented yet`,
|
|
1389
|
+
);
|
|
1390
|
+
})();
|
|
1391
|
+
const orientation = measure("cycle-breaking", () =>
|
|
1392
|
+
applyPartitionOrientation(input, applyLayerConstraintOrientation(input, cycleBreaker(input))),
|
|
1393
|
+
);
|
|
1394
|
+
const layeringStrategy = options.settings?.["layering.strategy"] ?? "NETWORK_SIMPLEX";
|
|
1395
|
+
const layerAssigner = (() => {
|
|
1396
|
+
if (options.strategies?.assignLayers) return options.strategies.assignLayers;
|
|
1397
|
+
if (layeringStrategy === "LONGEST_PATH_SOURCE") return assignLayersByLongestPath;
|
|
1398
|
+
if (layeringStrategy === "LONGEST_PATH") return assignLayersByLongestPathToSink;
|
|
1399
|
+
if (layeringStrategy === "INTERACTIVE") return assignLayersInteractively;
|
|
1400
|
+
if (layeringStrategy === "BF_MODEL_ORDER") return assignLayersByBreadthFirstModelOrder;
|
|
1401
|
+
if (layeringStrategy === "DF_MODEL_ORDER") return assignLayersByDepthFirstModelOrder;
|
|
1402
|
+
if (layeringStrategy === "COFFMAN_GRAHAM") return assignLayersWithCoffmanGraham;
|
|
1403
|
+
if (layeringStrategy === "NETWORK_SIMPLEX") return assignLayersWithNetworkSimplex;
|
|
1404
|
+
if (layeringStrategy === "MIN_WIDTH") return assignLayersWithMinWidth;
|
|
1405
|
+
if (layeringStrategy === "STRETCH_WIDTH") return assignLayersWithStretchWidth;
|
|
1406
|
+
throw new UnsupportedLayoutError(
|
|
1407
|
+
`Layering strategy ${layeringStrategy} is not implemented yet`,
|
|
1408
|
+
);
|
|
1409
|
+
})();
|
|
1410
|
+
const assignment = measure("layer-assignment", () =>
|
|
1411
|
+
applyHighDegreeNodeTreatment(
|
|
1412
|
+
input,
|
|
1413
|
+
orientation,
|
|
1414
|
+
applyNodePromotion(
|
|
1415
|
+
input,
|
|
1416
|
+
orientation,
|
|
1417
|
+
applyPartitions(input, applyLayerConstraints(input, layerAssigner(input, orientation))),
|
|
1418
|
+
),
|
|
1419
|
+
),
|
|
1420
|
+
);
|
|
1421
|
+
let expanded = measure("long-edge-splitting", () =>
|
|
1422
|
+
splitLongEdges(input, orientation, assignment),
|
|
1423
|
+
);
|
|
1424
|
+
const crossingStrategy = options.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
1425
|
+
const crossingMinimizer = (() => {
|
|
1426
|
+
if (options.strategies?.minimizeCrossings) return options.strategies.minimizeCrossings;
|
|
1427
|
+
if (crossingStrategy === "LAYER_SWEEP") {
|
|
1428
|
+
return minimizeCrossingsWithBarycenter(options.crossingSweeps);
|
|
1429
|
+
}
|
|
1430
|
+
if (crossingStrategy === "MEDIAN_LAYER_SWEEP") {
|
|
1431
|
+
return minimizeCrossingsWithMedian(options.crossingSweeps);
|
|
1432
|
+
}
|
|
1433
|
+
if (crossingStrategy === "INTERACTIVE") return minimizeCrossingsInteractively;
|
|
1434
|
+
if (crossingStrategy === "NONE") return minimizeCrossingsWithModelOrder;
|
|
1435
|
+
throw new UnsupportedLayoutError(
|
|
1436
|
+
`Crossing-minimization strategy ${crossingStrategy} is not implemented`,
|
|
1437
|
+
);
|
|
1438
|
+
})();
|
|
1439
|
+
let order = measure("crossing-minimization", () =>
|
|
1440
|
+
applyLayerConstraintOrder(
|
|
1441
|
+
expanded.input,
|
|
1442
|
+
applyGreedySwitch(
|
|
1443
|
+
expanded.input,
|
|
1444
|
+
expanded.orientation,
|
|
1445
|
+
applySemiInteractiveOrder(
|
|
1446
|
+
expanded.input,
|
|
1447
|
+
applyForcedModelOrder(
|
|
1448
|
+
expanded.input,
|
|
1449
|
+
expanded.orientation,
|
|
1450
|
+
crossingMinimizer(expanded.input, expanded.orientation, expanded.assignment),
|
|
1451
|
+
),
|
|
1452
|
+
),
|
|
1453
|
+
),
|
|
1454
|
+
),
|
|
1455
|
+
);
|
|
1456
|
+
const unzippingFanIn =
|
|
1457
|
+
graph.edges.length === graph.nodes.length - 1 &&
|
|
1458
|
+
graph.nodes.some(
|
|
1459
|
+
(node) =>
|
|
1460
|
+
graph.edges.filter((edge) => edge.targetId === node.id).length === graph.nodes.length - 1,
|
|
1461
|
+
);
|
|
1462
|
+
if ((options.settings?.["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING") {
|
|
1463
|
+
if (unzippingFanIn) {
|
|
1464
|
+
order = applyLayerUnzipping(expanded.input, order);
|
|
1465
|
+
} else {
|
|
1466
|
+
const unzipped = measure("layer-unzipping", () => unzipLayersAlternating(expanded, order));
|
|
1467
|
+
expanded = unzipped.expansion;
|
|
1468
|
+
order = unzipped.order;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
order = applyDirectionCongruency(expanded.input, order);
|
|
1472
|
+
const nodePlacementStrategy = options.settings?.["nodePlacement.strategy"] ?? "BRANDES_KOEPF";
|
|
1473
|
+
const nodePlacer = (() => {
|
|
1474
|
+
if (options.strategies?.placeNodes) return options.strategies.placeNodes;
|
|
1475
|
+
if (nodePlacementStrategy === "INTERACTIVE") return placeNodesInteractively;
|
|
1476
|
+
if (nodePlacementStrategy === "BRANDES_KOEPF") return placeNodesWithBrandesKoepf;
|
|
1477
|
+
if (nodePlacementStrategy === "LINEAR_SEGMENTS") return placeNodesWithLinearSegments;
|
|
1478
|
+
if (nodePlacementStrategy === "NETWORK_SIMPLEX") return placeNodesWithNetworkSimplex;
|
|
1479
|
+
return placeNodesInLayers;
|
|
1480
|
+
})();
|
|
1481
|
+
const placement = measure("node-placement", () => nodePlacer(expanded.input, order));
|
|
1482
|
+
const mutableRects = placement.rectByNodeId as Map<
|
|
1483
|
+
string,
|
|
1484
|
+
{ x: number; y: number; width: number; height: number }
|
|
1485
|
+
>;
|
|
1486
|
+
if (
|
|
1487
|
+
(expanded.input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING" &&
|
|
1488
|
+
graph.edges.length === graph.nodes.length - 1 &&
|
|
1489
|
+
(direction === "right" || direction === "left")
|
|
1490
|
+
) {
|
|
1491
|
+
const originalNodes = graph.nodes;
|
|
1492
|
+
const sink = originalNodes.find(
|
|
1493
|
+
(node) =>
|
|
1494
|
+
graph.edges.filter((edge) => edge.targetId === node.id).length === originalNodes.length - 1,
|
|
1495
|
+
);
|
|
1496
|
+
if (sink) {
|
|
1497
|
+
const sources = originalNodes.filter((node) => node.id !== sink.id);
|
|
1498
|
+
const configuredSplits = sources.flatMap((node) => {
|
|
1499
|
+
const value = input.nodeSettings?.(node)?.["layerUnzipping.layerSplit"];
|
|
1500
|
+
return value === undefined ? [] : [Math.max(1, Number(value))];
|
|
1501
|
+
});
|
|
1502
|
+
const split = configuredSplits.length > 0 ? Math.min(...configuredSplits) : 2;
|
|
1503
|
+
const minimizeEdgeLength = sources.some(
|
|
1504
|
+
(node) => input.nodeSettings?.(node)?.["layerUnzipping.minimizeEdgeLength"] === true,
|
|
1505
|
+
);
|
|
1506
|
+
const sequence = [sources.at(-1)!, ...sources.slice(0, -1)];
|
|
1507
|
+
const sourceWidth = Math.max(...sources.map((node) => input.sizes.get(node.id)?.width ?? 0));
|
|
1508
|
+
const sourceHeight = Math.max(
|
|
1509
|
+
...sources.map((node) => input.sizes.get(node.id)?.height ?? 0),
|
|
1510
|
+
);
|
|
1511
|
+
const skipForEdgeLength =
|
|
1512
|
+
minimizeEdgeLength &&
|
|
1513
|
+
split === 2 &&
|
|
1514
|
+
(sourceWidth +
|
|
1515
|
+
Math.max(
|
|
1516
|
+
2 * Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
1517
|
+
sources.length * Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10),
|
|
1518
|
+
input.spacing.layer,
|
|
1519
|
+
)) /
|
|
1520
|
+
(sourceHeight +
|
|
1521
|
+
Math.max(input.spacing.node, Number(input.settings["spacing.edgeNode"] ?? 10))) >=
|
|
1522
|
+
sources.length / 4;
|
|
1523
|
+
if (skipForEdgeLength) {
|
|
1524
|
+
for (const [index, node] of sequence.entries()) {
|
|
1525
|
+
const rect = mutableRects.get(node.id);
|
|
1526
|
+
if (!rect) continue;
|
|
1527
|
+
mutableRects.set(node.id, {
|
|
1528
|
+
...rect,
|
|
1529
|
+
x: input.padding.left,
|
|
1530
|
+
y: input.padding.top + index * (sourceHeight + input.spacing.node),
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
const sinkRect = mutableRects.get(sink.id);
|
|
1534
|
+
if (sinkRect) {
|
|
1535
|
+
mutableRects.set(sink.id, {
|
|
1536
|
+
...sinkRect,
|
|
1537
|
+
x:
|
|
1538
|
+
input.padding.left +
|
|
1539
|
+
sourceWidth +
|
|
1540
|
+
input.spacing.layer +
|
|
1541
|
+
Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
1542
|
+
y:
|
|
1543
|
+
input.padding.top + ((sequence.length - 1) * (sourceHeight + input.spacing.node)) / 2,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
} else {
|
|
1547
|
+
const sublayerOffset = Math.ceil((sourceHeight + input.spacing.node + 1) / 2);
|
|
1548
|
+
const repeatStep = split * ((sourceHeight + input.spacing.node) / 2) + split - 1;
|
|
1549
|
+
let minimumCross = Number.POSITIVE_INFINITY;
|
|
1550
|
+
let maximumCross = Number.NEGATIVE_INFINITY;
|
|
1551
|
+
for (const [index, node] of sequence.entries()) {
|
|
1552
|
+
const sublayer = index % split;
|
|
1553
|
+
const position = Math.floor(index / split);
|
|
1554
|
+
const rect = mutableRects.get(node.id);
|
|
1555
|
+
if (!rect) continue;
|
|
1556
|
+
const x = input.padding.left + sublayer * (sourceWidth + input.spacing.layer);
|
|
1557
|
+
const y = input.padding.top + sublayer * sublayerOffset + position * repeatStep;
|
|
1558
|
+
mutableRects.set(node.id, { ...rect, x, y });
|
|
1559
|
+
minimumCross = Math.min(minimumCross, y);
|
|
1560
|
+
maximumCross = Math.max(maximumCross, y);
|
|
1561
|
+
}
|
|
1562
|
+
const sinkRect = mutableRects.get(sink.id);
|
|
1563
|
+
if (sinkRect) {
|
|
1564
|
+
mutableRects.set(sink.id, {
|
|
1565
|
+
...sinkRect,
|
|
1566
|
+
x:
|
|
1567
|
+
input.padding.left +
|
|
1568
|
+
split * (sourceWidth + input.spacing.layer) +
|
|
1569
|
+
Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
1570
|
+
y: Math.round((minimumCross + maximumCross) / 2),
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
for (const node of expanded.input.graph.nodes) {
|
|
1577
|
+
if (expanded.input.nodeSettings?.(node)?.hypernode !== true) continue;
|
|
1578
|
+
const rect = mutableRects.get(node.id);
|
|
1579
|
+
if (!rect) continue;
|
|
1580
|
+
const neighbors = expanded.input.graph.edges
|
|
1581
|
+
.filter((edge) => edge.targetId === node.id)
|
|
1582
|
+
.map((edge) => mutableRects.get(edge.sourceId))
|
|
1583
|
+
.filter((candidate) => candidate !== undefined);
|
|
1584
|
+
if (neighbors.length === 0) continue;
|
|
1585
|
+
const horizontal = direction === "left" || direction === "right";
|
|
1586
|
+
const desiredCross = Math.min(
|
|
1587
|
+
...neighbors.map((candidate) => (horizontal ? candidate.y : candidate.x)),
|
|
1588
|
+
);
|
|
1589
|
+
const delta = desiredCross - (horizontal ? rect.y : rect.x);
|
|
1590
|
+
if (Math.abs(delta) < 1e-9) continue;
|
|
1591
|
+
const pending = [node.id];
|
|
1592
|
+
const shifted = new Set<string>();
|
|
1593
|
+
while (pending.length > 0) {
|
|
1594
|
+
const id = pending.shift()!;
|
|
1595
|
+
if (shifted.has(id)) continue;
|
|
1596
|
+
shifted.add(id);
|
|
1597
|
+
const current = mutableRects.get(id);
|
|
1598
|
+
if (current) {
|
|
1599
|
+
mutableRects.set(
|
|
1600
|
+
id,
|
|
1601
|
+
horizontal ? { ...current, y: current.y + delta } : { ...current, x: current.x + delta },
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
for (const edge of expanded.input.graph.edges) {
|
|
1605
|
+
if (edge.sourceId === id && edge.targetId !== node.id) pending.push(edge.targetId);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (
|
|
1610
|
+
expanded.input.settings.directionCongruency === "ROTATION" &&
|
|
1611
|
+
(direction === "left" || direction === "down")
|
|
1612
|
+
) {
|
|
1613
|
+
const horizontal = direction === "left";
|
|
1614
|
+
for (const node of expanded.input.graph.nodes) {
|
|
1615
|
+
const incomingDegree = expanded.input.graph.edges.filter(
|
|
1616
|
+
(edge) => edge.targetId === node.id && edge.sourceId !== node.id,
|
|
1617
|
+
).length;
|
|
1618
|
+
if (incomingDegree < 2) continue;
|
|
1619
|
+
const rect = mutableRects.get(node.id);
|
|
1620
|
+
if (!rect) continue;
|
|
1621
|
+
const crossSize = horizontal ? rect.height : rect.width;
|
|
1622
|
+
const correction = crossSize / (incomingDegree + 1);
|
|
1623
|
+
mutableRects.set(
|
|
1624
|
+
node.id,
|
|
1625
|
+
horizontal ? { ...rect, y: rect.y - correction } : { ...rect, x: rect.x - correction },
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
{
|
|
1630
|
+
const horizontal = direction === "right" || direction === "left";
|
|
1631
|
+
for (const edge of graph.edges) {
|
|
1632
|
+
if (edge.sourcePort === undefined) continue;
|
|
1633
|
+
const source = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1634
|
+
const targetRect = mutableRects.get(edge.targetId);
|
|
1635
|
+
const sourceRect = mutableRects.get(edge.sourceId);
|
|
1636
|
+
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
1637
|
+
if (!source || !sourceRect || !targetRect || !port) continue;
|
|
1638
|
+
const side = input.portSettings?.(port, source)?.["port.side"];
|
|
1639
|
+
if (horizontal && side === "NORTH") {
|
|
1640
|
+
mutableRects.set(edge.targetId, {
|
|
1641
|
+
...targetRect,
|
|
1642
|
+
y: sourceRect.y - (port.height ?? 0) - targetRect.height,
|
|
1643
|
+
});
|
|
1644
|
+
} else if (horizontal && side === "SOUTH") {
|
|
1645
|
+
mutableRects.set(edge.targetId, {
|
|
1646
|
+
...targetRect,
|
|
1647
|
+
y: sourceRect.y + sourceRect.height + (port.height ?? 0),
|
|
1648
|
+
});
|
|
1649
|
+
} else if (!horizontal && side === "WEST") {
|
|
1650
|
+
mutableRects.set(edge.targetId, {
|
|
1651
|
+
...targetRect,
|
|
1652
|
+
x: sourceRect.x - (port.width ?? 0) - targetRect.width,
|
|
1653
|
+
});
|
|
1654
|
+
} else if (!horizontal && side === "EAST") {
|
|
1655
|
+
mutableRects.set(edge.targetId, {
|
|
1656
|
+
...targetRect,
|
|
1657
|
+
x: sourceRect.x + sourceRect.width + (port.width ?? 0),
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
measure("port-margin-normalization", () =>
|
|
1663
|
+
normalizePlacementForPortExtents(expanded.input, placement, order),
|
|
1664
|
+
);
|
|
1665
|
+
{
|
|
1666
|
+
const horizontal = direction === "left" || direction === "right";
|
|
1667
|
+
const crossCenter = (nodeId: string): number => {
|
|
1668
|
+
const rect = placement.rectByNodeId.get(nodeId);
|
|
1669
|
+
return rect ? (horizontal ? rect.y + rect.height / 2 : rect.x + rect.width / 2) : 0;
|
|
1670
|
+
};
|
|
1671
|
+
for (const node of graph.nodes) {
|
|
1672
|
+
for (const port of node.ports ?? []) {
|
|
1673
|
+
const settings = options.portSettings?.(port, node);
|
|
1674
|
+
if (settings?.allowNonFlowPortsToSwitchSides !== true) continue;
|
|
1675
|
+
const side = settings["port.side"];
|
|
1676
|
+
if (
|
|
1677
|
+
horizontal ? side !== "NORTH" && side !== "SOUTH" : side !== "WEST" && side !== "EAST"
|
|
1678
|
+
) {
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
const configuredSide = side as "NORTH" | "SOUTH" | "WEST" | "EAST";
|
|
1682
|
+
const outgoing = graph.edges.find(
|
|
1683
|
+
(edge) => edge.sourceId === node.id && edge.sourcePort === port.name,
|
|
1684
|
+
);
|
|
1685
|
+
const incoming = graph.edges.find(
|
|
1686
|
+
(edge) => edge.targetId === node.id && edge.targetPort === port.name,
|
|
1687
|
+
);
|
|
1688
|
+
const peers = outgoing
|
|
1689
|
+
? graph.edges
|
|
1690
|
+
.filter((edge) => edge.targetId === outgoing.targetId)
|
|
1691
|
+
.map((edge) => edge.sourceId)
|
|
1692
|
+
: incoming
|
|
1693
|
+
? graph.edges
|
|
1694
|
+
.filter((edge) => edge.sourceId === incoming.sourceId)
|
|
1695
|
+
.map((edge) => edge.targetId)
|
|
1696
|
+
: [];
|
|
1697
|
+
if (peers.length < 2) continue;
|
|
1698
|
+
const positions = peers.map(crossCenter);
|
|
1699
|
+
const own = crossCenter(node.id);
|
|
1700
|
+
const switched = horizontal
|
|
1701
|
+
? own >= Math.max(...positions)
|
|
1702
|
+
? "SOUTH"
|
|
1703
|
+
: own <= Math.min(...positions)
|
|
1704
|
+
? "NORTH"
|
|
1705
|
+
: configuredSide
|
|
1706
|
+
: own >= Math.max(...positions)
|
|
1707
|
+
? "EAST"
|
|
1708
|
+
: own <= Math.min(...positions)
|
|
1709
|
+
? "WEST"
|
|
1710
|
+
: configuredSide;
|
|
1711
|
+
switchedSideByPort.set(`${node.id}\0${port.name}`, switched);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const edgeRouting = options.settings?.edgeRouting ?? "ORTHOGONAL";
|
|
1716
|
+
const edgeRouter =
|
|
1717
|
+
options.strategies?.routeEdges ??
|
|
1718
|
+
(edgeRouting === "POLYLINE"
|
|
1719
|
+
? routeEdgesWithPolylines
|
|
1720
|
+
: edgeRouting === "SPLINES"
|
|
1721
|
+
? routeEdgesWithSplines
|
|
1722
|
+
: routeEdgesOrthogonally);
|
|
1723
|
+
const expandedRoutes = measure("edge-routing", () =>
|
|
1724
|
+
edgeRouter(expanded.input, expanded.orientation, placement),
|
|
1725
|
+
);
|
|
1726
|
+
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
1727
|
+
let routes = measure("long-edge-joining", () =>
|
|
1728
|
+
joinLongEdgeRoutes(
|
|
1729
|
+
expandedRoutes,
|
|
1730
|
+
expanded.segmentIdsByEdgeId,
|
|
1731
|
+
edgeRouting === "SPLINES" || options.settings?.unnecessaryBendpoints === true,
|
|
1732
|
+
edgeRouting === "SPLINES",
|
|
1733
|
+
Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
1734
|
+
),
|
|
1735
|
+
);
|
|
1736
|
+
if (options.settings?.["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT") {
|
|
1737
|
+
const horizontal = direction === "right" || direction === "left";
|
|
1738
|
+
const pointsByEdgeId = new Map(routes.pointsByEdgeId);
|
|
1739
|
+
for (const [edgeId, points] of pointsByEdgeId) {
|
|
1740
|
+
if (points.length <= 4) continue;
|
|
1741
|
+
const start = points[0]!;
|
|
1742
|
+
const firstBend = points[1]!;
|
|
1743
|
+
const end = points.at(-1)!;
|
|
1744
|
+
pointsByEdgeId.set(edgeId, [
|
|
1745
|
+
start,
|
|
1746
|
+
firstBend,
|
|
1747
|
+
horizontal ? { x: firstBend.x, y: end.y } : { x: end.x, y: firstBend.y },
|
|
1748
|
+
end,
|
|
1749
|
+
]);
|
|
1750
|
+
}
|
|
1751
|
+
routes = { ...routes, pointsByEdgeId };
|
|
1752
|
+
}
|
|
1753
|
+
if (edgeRouting === "ORTHOGONAL") {
|
|
1754
|
+
const horizontal = direction === "right" || direction === "left";
|
|
1755
|
+
const forwardSign = direction === "right" || direction === "down" ? 1 : -1;
|
|
1756
|
+
const mutableRouteMap = routes.pointsByEdgeId as Map<string, readonly Point[]>;
|
|
1757
|
+
for (const node of graph.nodes) {
|
|
1758
|
+
if (options.nodeSettings?.(node)?.hypernode !== true || !horizontal) continue;
|
|
1759
|
+
const incoming = graph.edges.filter((edge) => edge.targetId === node.id);
|
|
1760
|
+
const outgoing = graph.edges.filter((edge) => edge.sourceId === node.id);
|
|
1761
|
+
const moveForward = Number(incoming.length > 0) <= Number(outgoing.length > 0);
|
|
1762
|
+
const incident = moveForward ? outgoing : incoming;
|
|
1763
|
+
const candidates = incident.flatMap((edge) => {
|
|
1764
|
+
const points = [...(mutableRouteMap.get(edge.id) ?? [])];
|
|
1765
|
+
if (points.length < 3) return [];
|
|
1766
|
+
const bendIndex = moveForward ? 1 : points.length - 2;
|
|
1767
|
+
const secondIndex = moveForward ? 2 : points.length - 3;
|
|
1768
|
+
return [{ edge, points, bendIndex, secondIndex }];
|
|
1769
|
+
});
|
|
1770
|
+
if (candidates.length === 0) continue;
|
|
1771
|
+
const signedFlow = (point: Point) =>
|
|
1772
|
+
forwardSign * (horizontal ? point.x : point.y) * (moveForward ? 1 : -1);
|
|
1773
|
+
const joinFlow = Math.min(
|
|
1774
|
+
...candidates.map(({ points, bendIndex }) => signedFlow(points[bendIndex]!)),
|
|
1775
|
+
);
|
|
1776
|
+
const bendEdges = candidates.filter(
|
|
1777
|
+
({ points, bendIndex }) => Math.abs(signedFlow(points[bendIndex]!) - joinFlow) < 1e-9,
|
|
1778
|
+
);
|
|
1779
|
+
const rect = mutableRects.get(node.id);
|
|
1780
|
+
if (!rect) continue;
|
|
1781
|
+
const join = bendEdges[0]!.points[bendEdges[0]!.bendIndex]!;
|
|
1782
|
+
const second = bendEdges[0]!.points[bendEdges[0]!.secondIndex]!;
|
|
1783
|
+
const flowCenter = horizontal ? rect.x + rect.width / 2 : rect.y + rect.height / 2;
|
|
1784
|
+
const joinCoordinate = horizontal ? join.x : join.y;
|
|
1785
|
+
const crossDifference = horizontal
|
|
1786
|
+
? Math.abs(second.y - join.y)
|
|
1787
|
+
: Math.abs(second.x - join.x);
|
|
1788
|
+
const flowSize = horizontal ? rect.width : rect.height;
|
|
1789
|
+
const crossSize = horizontal ? rect.height : rect.width;
|
|
1790
|
+
if (
|
|
1791
|
+
Math.abs(joinCoordinate - flowCenter) <= flowSize / 2 ||
|
|
1792
|
+
crossDifference <= crossSize / 2
|
|
1793
|
+
) {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
const delta = joinCoordinate - flowCenter;
|
|
1797
|
+
mutableRects.set(
|
|
1798
|
+
node.id,
|
|
1799
|
+
horizontal ? { ...rect, x: rect.x + delta } : { ...rect, y: rect.y + delta },
|
|
1800
|
+
);
|
|
1801
|
+
for (const edge of [...incoming, ...outgoing]) {
|
|
1802
|
+
const points = [...(mutableRouteMap.get(edge.id) ?? [])];
|
|
1803
|
+
const endpointIndex = edge.sourceId === node.id ? 0 : points.length - 1;
|
|
1804
|
+
const endpoint = points[endpointIndex];
|
|
1805
|
+
if (!endpoint) continue;
|
|
1806
|
+
points[endpointIndex] = horizontal
|
|
1807
|
+
? { ...endpoint, x: endpoint.x + delta }
|
|
1808
|
+
: { ...endpoint, y: endpoint.y + delta };
|
|
1809
|
+
mutableRouteMap.set(edge.id, points);
|
|
1810
|
+
}
|
|
1811
|
+
const movedRect = mutableRects.get(node.id)!;
|
|
1812
|
+
for (const { edge, points: originalPoints, bendIndex, secondIndex } of bendEdges) {
|
|
1813
|
+
const points = [...(mutableRouteMap.get(edge.id) ?? originalPoints)];
|
|
1814
|
+
const originalBend = originalPoints[bendIndex]!;
|
|
1815
|
+
const originalSecond = originalPoints[secondIndex]!;
|
|
1816
|
+
points.splice(bendIndex, 1);
|
|
1817
|
+
const endpointIndex = moveForward ? 0 : points.length - 1;
|
|
1818
|
+
points[endpointIndex] = horizontal
|
|
1819
|
+
? {
|
|
1820
|
+
x: movedRect.x + movedRect.width / 2,
|
|
1821
|
+
y: originalSecond.y >= originalBend.y ? movedRect.y + movedRect.height : movedRect.y,
|
|
1822
|
+
}
|
|
1823
|
+
: {
|
|
1824
|
+
x: originalSecond.x >= originalBend.x ? movedRect.x + movedRect.width : movedRect.x,
|
|
1825
|
+
y: movedRect.y + movedRect.height / 2,
|
|
1826
|
+
};
|
|
1827
|
+
mutableRouteMap.set(edge.id, points);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (
|
|
1832
|
+
(options.settings?.["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING" &&
|
|
1833
|
+
!unzippingFanIn &&
|
|
1834
|
+
edgeRouting === "ORTHOGONAL"
|
|
1835
|
+
) {
|
|
1836
|
+
routes = {
|
|
1837
|
+
pointsByEdgeId: adjustUnzippedSinkRoutes(
|
|
1838
|
+
graph,
|
|
1839
|
+
routes.pointsByEdgeId,
|
|
1840
|
+
direction,
|
|
1841
|
+
Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10),
|
|
1842
|
+
Number(options.settings?.["spacing.edgeEdgeBetweenLayers"] ?? 10),
|
|
1843
|
+
),
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
for (const edge of graph.edges) {
|
|
1848
|
+
if (edge.sourcePort === undefined || edge.targetPort !== undefined) continue;
|
|
1849
|
+
const source = expanded.input.graph.nodes.find((node) => node.id === edge.sourceId);
|
|
1850
|
+
const port = source?.ports?.find((candidate) => candidate.name === edge.sourcePort);
|
|
1851
|
+
const targetRect = mutableRects.get(edge.targetId);
|
|
1852
|
+
if (!source || !port || !targetRect) continue;
|
|
1853
|
+
const settings = expanded.input.portSettings?.(port, source);
|
|
1854
|
+
const forwardSide =
|
|
1855
|
+
direction === "right"
|
|
1856
|
+
? "EAST"
|
|
1857
|
+
: direction === "left"
|
|
1858
|
+
? "WEST"
|
|
1859
|
+
: direction === "down"
|
|
1860
|
+
? "SOUTH"
|
|
1861
|
+
: "NORTH";
|
|
1862
|
+
if (settings?.["port.anchor"] === undefined || settings["port.side"] !== forwardSide) continue;
|
|
1863
|
+
const horizontal = direction === "right" || direction === "left";
|
|
1864
|
+
const protrusion = horizontal ? (port.width ?? 0) : (port.height ?? 0);
|
|
1865
|
+
const delta = (direction === "right" || direction === "down" ? 1 : -1) * protrusion;
|
|
1866
|
+
mutableRects.set(
|
|
1867
|
+
edge.targetId,
|
|
1868
|
+
horizontal
|
|
1869
|
+
? { ...targetRect, x: targetRect.x + delta }
|
|
1870
|
+
: { ...targetRect, y: targetRect.y + delta },
|
|
1871
|
+
);
|
|
1872
|
+
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
1873
|
+
const end = points.at(-1);
|
|
1874
|
+
if (end)
|
|
1875
|
+
points[points.length - 1] = horizontal
|
|
1876
|
+
? { ...end, x: end.x + delta }
|
|
1877
|
+
: { ...end, y: end.y + delta };
|
|
1878
|
+
(routes.pointsByEdgeId as Map<string, readonly Point[]>).set(edge.id, points);
|
|
1879
|
+
}
|
|
1880
|
+
const routedPortAnchors = new Map<string, Point>();
|
|
1881
|
+
for (const edge of graph.edges) {
|
|
1882
|
+
const points = routes.pointsByEdgeId.get(edge.id);
|
|
1883
|
+
const first = points?.[0];
|
|
1884
|
+
const last = points?.at(-1);
|
|
1885
|
+
if (
|
|
1886
|
+
edge.sourcePort !== undefined &&
|
|
1887
|
+
first &&
|
|
1888
|
+
graph.edges.filter(
|
|
1889
|
+
(candidate) =>
|
|
1890
|
+
candidate.sourceId === edge.sourceId && candidate.sourcePort === edge.sourcePort,
|
|
1891
|
+
).length === 1
|
|
1892
|
+
) {
|
|
1893
|
+
routedPortAnchors.set(`${edge.sourceId}\0${edge.sourcePort}`, first);
|
|
1894
|
+
}
|
|
1895
|
+
if (
|
|
1896
|
+
edge.targetPort !== undefined &&
|
|
1897
|
+
last &&
|
|
1898
|
+
graph.edges.filter(
|
|
1899
|
+
(candidate) =>
|
|
1900
|
+
candidate.targetId === edge.targetId && candidate.targetPort === edge.targetPort,
|
|
1901
|
+
).length === 1
|
|
1902
|
+
) {
|
|
1903
|
+
routedPortAnchors.set(`${edge.targetId}\0${edge.targetPort}`, last);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const nodes = graph.nodes.map((node): VisualNode<N, P> => {
|
|
1907
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
1908
|
+
if (!rect) {
|
|
1909
|
+
throw new Error(`Node placement missing for ${node.id}`);
|
|
1910
|
+
}
|
|
1911
|
+
let ports = placePorts(
|
|
1912
|
+
node.ports,
|
|
1913
|
+
rect,
|
|
1914
|
+
direction,
|
|
1915
|
+
(port) => input.portSettings?.(port, node),
|
|
1916
|
+
{ ...options.settings, ...options.nodeSettings?.(node) },
|
|
1917
|
+
);
|
|
1918
|
+
if (options.nodeSettings?.(node)?.portConstraints === "FIXED_SIDE") {
|
|
1919
|
+
ports = ports?.map((port) => {
|
|
1920
|
+
if ((port.width ?? 8) !== 0 || (port.height ?? 8) !== 0) return port;
|
|
1921
|
+
const anchor = routedPortAnchors.get(`${node.id}\0${port.name}`);
|
|
1922
|
+
if (!anchor || port.x === undefined || port.y === undefined) return port;
|
|
1923
|
+
const settings = input.portSettings?.(port, node);
|
|
1924
|
+
const configuredAnchor = settings?.["port.anchor"] as
|
|
1925
|
+
| { x?: number; y?: number }
|
|
1926
|
+
| undefined;
|
|
1927
|
+
const width = port.width ?? 0;
|
|
1928
|
+
const height = port.height ?? 0;
|
|
1929
|
+
const defaultAnchorX = port.x >= rect.width ? width : port.x + width <= 0 ? 0 : width / 2;
|
|
1930
|
+
const defaultAnchorY =
|
|
1931
|
+
port.y >= rect.height ? height : port.y + height <= 0 ? 0 : height / 2;
|
|
1932
|
+
const x = anchor.x - rect.x - (configuredAnchor?.x ?? defaultAnchorX);
|
|
1933
|
+
const y = anchor.y - rect.y - (configuredAnchor?.y ?? defaultAnchorY);
|
|
1934
|
+
return {
|
|
1935
|
+
...port,
|
|
1936
|
+
x: x === 0 && Object.is(port.x, -0) ? port.x : x,
|
|
1937
|
+
y: y === 0 && Object.is(port.y, -0) ? port.y : y,
|
|
1938
|
+
};
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
return {
|
|
1942
|
+
...node,
|
|
1943
|
+
...rect,
|
|
1944
|
+
...(ports === undefined ? {} : { ports }),
|
|
1945
|
+
} as VisualNode<N, P>;
|
|
1946
|
+
});
|
|
1947
|
+
const feedbackNodeRects = graph.nodes.flatMap((node) => {
|
|
1948
|
+
const rect = placement.rectByNodeId.get(node.id);
|
|
1949
|
+
return rect ? [rect] : [];
|
|
1950
|
+
});
|
|
1951
|
+
const minimumFeedbackNodeX = Math.min(...feedbackNodeRects.map((rect) => rect.x));
|
|
1952
|
+
const maximumFeedbackNodeX = Math.max(...feedbackNodeRects.map((rect) => rect.x + rect.width));
|
|
1953
|
+
const minimumFeedbackNodeY = Math.min(...feedbackNodeRects.map((rect) => rect.y));
|
|
1954
|
+
const maximumFeedbackNodeY = Math.max(...feedbackNodeRects.map((rect) => rect.y + rect.height));
|
|
1955
|
+
const edges = graph.edges.map((edge) => {
|
|
1956
|
+
const points = [...(routes.pointsByEdgeId.get(edge.id) ?? [])];
|
|
1957
|
+
const midpoint = getPolylineMidpoint(points);
|
|
1958
|
+
const width = edge.width ?? 0;
|
|
1959
|
+
const height = edge.height ?? 0;
|
|
1960
|
+
const edgeSettings = options.edgeSettings?.(edge);
|
|
1961
|
+
const labelPlacement = edgeSettings?.["edgeLabels.placement"] ?? "CENTER";
|
|
1962
|
+
const inlineLabel = edgeSettings?.["edgeLabels.inline"] === true;
|
|
1963
|
+
const firstPoint = points[0] ?? midpoint;
|
|
1964
|
+
const lastPoint = points.at(-1) ?? midpoint;
|
|
1965
|
+
const labelSpacing = Number(options.settings?.["spacing.edgeLabel"] ?? 2);
|
|
1966
|
+
const edgeThickness = Number(edgeSettings?.["edge.thickness"] ?? 1);
|
|
1967
|
+
const labelDummyRect = placement.rectByNodeId.get(
|
|
1968
|
+
expanded.labelDummyIdByEdgeId.get(edge.id) ?? "",
|
|
1969
|
+
);
|
|
1970
|
+
const edgeLabelSideSelection = options.settings?.["edgeLabels.sideSelection"] ?? "SMART_DOWN";
|
|
1971
|
+
const placeLabelUp =
|
|
1972
|
+
edgeLabelSideSelection === "ALWAYS_UP" ||
|
|
1973
|
+
edgeLabelSideSelection === "SMART_UP" ||
|
|
1974
|
+
edgeLabelSideSelection === "DIRECTION_UP";
|
|
1975
|
+
const horizontal = direction === "left" || direction === "right";
|
|
1976
|
+
const verticalTrack = horizontal
|
|
1977
|
+
? points.find((point, index) => {
|
|
1978
|
+
const next = points[index + 1];
|
|
1979
|
+
return next !== undefined && point.x === next.x && point.y !== next.y;
|
|
1980
|
+
})
|
|
1981
|
+
: undefined;
|
|
1982
|
+
const secondPoint = points[1];
|
|
1983
|
+
const beforeLastPoint = points.at(-2);
|
|
1984
|
+
const flowDelta = horizontal ? lastPoint.x - firstPoint.x : lastPoint.y - firstPoint.y;
|
|
1985
|
+
const firstLeadDelta = secondPoint
|
|
1986
|
+
? horizontal
|
|
1987
|
+
? secondPoint.x - firstPoint.x
|
|
1988
|
+
: secondPoint.y - firstPoint.y
|
|
1989
|
+
: 0;
|
|
1990
|
+
const lastLeadDelta = beforeLastPoint
|
|
1991
|
+
? horizontal
|
|
1992
|
+
? lastPoint.x - beforeLastPoint.x
|
|
1993
|
+
: lastPoint.y - beforeLastPoint.y
|
|
1994
|
+
: 0;
|
|
1995
|
+
const outsideFeedback =
|
|
1996
|
+
routes.outsideFeedbackEdgeIds?.has(edge.id) === true ||
|
|
1997
|
+
(secondPoint !== undefined &&
|
|
1998
|
+
beforeLastPoint !== undefined &&
|
|
1999
|
+
flowDelta !== 0 &&
|
|
2000
|
+
firstLeadDelta * flowDelta < 0 &&
|
|
2001
|
+
lastLeadDelta * flowDelta < 0);
|
|
2002
|
+
const horizontalFeedbackCandidate = outsideFeedback
|
|
2003
|
+
? points
|
|
2004
|
+
.flatMap((point, index) => {
|
|
2005
|
+
const next = points[index + 1];
|
|
2006
|
+
return next !== undefined &&
|
|
2007
|
+
point.y === next.y &&
|
|
2008
|
+
(point.y < minimumFeedbackNodeY || point.y > maximumFeedbackNodeY)
|
|
2009
|
+
? [{ start: point, end: next, length: Math.abs(next.x - point.x) }]
|
|
2010
|
+
: [];
|
|
2011
|
+
})
|
|
2012
|
+
.sort((left, right) => right.length - left.length)[0]
|
|
2013
|
+
: undefined;
|
|
2014
|
+
const verticalFeedbackCandidate = outsideFeedback
|
|
2015
|
+
? points
|
|
2016
|
+
.flatMap((point, index) => {
|
|
2017
|
+
const next = points[index + 1];
|
|
2018
|
+
return next !== undefined &&
|
|
2019
|
+
point.x === next.x &&
|
|
2020
|
+
(point.x < minimumFeedbackNodeX || point.x > maximumFeedbackNodeX)
|
|
2021
|
+
? [{ start: point, end: next, length: Math.abs(next.y - point.y) }]
|
|
2022
|
+
: [];
|
|
2023
|
+
})
|
|
2024
|
+
.sort((left, right) => right.length - left.length)[0]
|
|
2025
|
+
: undefined;
|
|
2026
|
+
const sourceNode = graph.nodes.find((node) => node.id === edge.sourceId);
|
|
2027
|
+
const sourcePort = sourceNode?.ports?.find((port) => port.name === edge.sourcePort);
|
|
2028
|
+
const targetPort = sourceNode?.ports?.find((port) => port.name === edge.targetPort);
|
|
2029
|
+
const sourcePortSide =
|
|
2030
|
+
sourceNode && sourcePort
|
|
2031
|
+
? options.portSettings?.(sourcePort, sourceNode)?.["port.side"]
|
|
2032
|
+
: undefined;
|
|
2033
|
+
const targetPortSide =
|
|
2034
|
+
sourceNode && targetPort
|
|
2035
|
+
? options.portSettings?.(targetPort, sourceNode)?.["port.side"]
|
|
2036
|
+
: undefined;
|
|
2037
|
+
const sameSideHorizontalPortSelfLoop =
|
|
2038
|
+
edge.sourceId === edge.targetId &&
|
|
2039
|
+
(sourcePortSide === "EAST" || sourcePortSide === "WEST") &&
|
|
2040
|
+
targetPortSide === sourcePortSide;
|
|
2041
|
+
const horizontalFeedbackTrack =
|
|
2042
|
+
sameSideHorizontalPortSelfLoop ||
|
|
2043
|
+
(horizontalFeedbackCandidate?.length ?? -1) >= (verticalFeedbackCandidate?.length ?? -1)
|
|
2044
|
+
? horizontalFeedbackCandidate
|
|
2045
|
+
: undefined;
|
|
2046
|
+
const verticalFeedbackTrack = horizontalFeedbackTrack ? undefined : verticalFeedbackCandidate;
|
|
2047
|
+
const trackNearTarget =
|
|
2048
|
+
verticalTrack !== undefined &&
|
|
2049
|
+
Math.abs(lastPoint.x - verticalTrack.x) < Math.abs(verticalTrack.x - firstPoint.x);
|
|
2050
|
+
const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
2051
|
+
const labelBeforeTrack = direction === "right" ? trackNearTarget : !trackNearTarget;
|
|
2052
|
+
const routeX =
|
|
2053
|
+
labelPlacement === "TAIL"
|
|
2054
|
+
? firstPoint.x + labelSpacing
|
|
2055
|
+
: labelPlacement === "HEAD"
|
|
2056
|
+
? lastPoint.x - width - labelSpacing
|
|
2057
|
+
: inlineLabel && horizontalFeedbackTrack
|
|
2058
|
+
? (horizontalFeedbackTrack.start.x + horizontalFeedbackTrack.end.x - width) / 2
|
|
2059
|
+
: inlineLabel && verticalFeedbackTrack
|
|
2060
|
+
? verticalFeedbackTrack.start.x > (minimumFeedbackNodeX + maximumFeedbackNodeX) / 2
|
|
2061
|
+
? verticalFeedbackTrack.start.x + labelSpacing + 1
|
|
2062
|
+
: verticalFeedbackTrack.start.x - labelSpacing - width - 1
|
|
2063
|
+
: inlineLabel && verticalTrack
|
|
2064
|
+
? labelBeforeTrack
|
|
2065
|
+
? verticalTrack.x - edgeNodeSpacing - width
|
|
2066
|
+
: verticalTrack.x + edgeNodeSpacing
|
|
2067
|
+
: inlineLabel
|
|
2068
|
+
? horizontal
|
|
2069
|
+
? Math.floor(midpoint.x - width / 2)
|
|
2070
|
+
: Math.ceil(midpoint.x - width / 2)
|
|
2071
|
+
: midpoint.x - width / 2;
|
|
2072
|
+
const routeY =
|
|
2073
|
+
labelPlacement === "CENTER" && inlineLabel && horizontalFeedbackTrack
|
|
2074
|
+
? horizontalFeedbackTrack.start.y - height / 2 - 0.5
|
|
2075
|
+
: labelPlacement === "CENTER" && inlineLabel && verticalFeedbackTrack
|
|
2076
|
+
? (verticalFeedbackTrack.start.y + verticalFeedbackTrack.end.y - height) / 2
|
|
2077
|
+
: labelPlacement === "CENTER" && inlineLabel
|
|
2078
|
+
? midpoint.y - height / 2 - 0.5
|
|
2079
|
+
: labelPlacement === "CENTER" && placeLabelUp
|
|
2080
|
+
? midpoint.y - height - labelSpacing - Math.round(edgeThickness / 2)
|
|
2081
|
+
: (labelPlacement === "CENTER" ? midpoint.y : (firstPoint.y + lastPoint.y) / 2) +
|
|
2082
|
+
labelSpacing +
|
|
2083
|
+
Math.round(edgeThickness / 2);
|
|
2084
|
+
const x =
|
|
2085
|
+
labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
|
|
2086
|
+
const y =
|
|
2087
|
+
labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
|
|
2088
|
+
return {
|
|
2089
|
+
...edge,
|
|
2090
|
+
x,
|
|
2091
|
+
y,
|
|
2092
|
+
width,
|
|
2093
|
+
height,
|
|
2094
|
+
points,
|
|
2095
|
+
routing:
|
|
2096
|
+
edgeRouting === "POLYLINE"
|
|
2097
|
+
? ("polyline" as const)
|
|
2098
|
+
: edgeRouting === "SPLINES"
|
|
2099
|
+
? ("splines" as const)
|
|
2100
|
+
: ("orthogonal" as const),
|
|
2101
|
+
};
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
return {
|
|
2105
|
+
...graph,
|
|
2106
|
+
direction,
|
|
2107
|
+
nodes,
|
|
2108
|
+
edges,
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/**
|
|
2113
|
+
* Deterministic native layered layout for an `@statelyai/graph` graph.
|
|
2114
|
+
*
|
|
2115
|
+
* Supports flat and nested graphs, cycles, ports, self-loops, four directions,
|
|
2116
|
+
* custom phase strategies, and all layered edge-routing styles.
|
|
2117
|
+
*/
|
|
2118
|
+
export function getLayeredLayout<N, E, G, P>(
|
|
2119
|
+
graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
|
|
2120
|
+
options: LayeredLayoutOptions = {},
|
|
2121
|
+
): VisualGraph<N, E, G, P> {
|
|
2122
|
+
return runLayeredPipeline(graph, options);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
|
|
2126
|
+
id: "layered",
|
|
2127
|
+
capabilities: {
|
|
2128
|
+
full: true,
|
|
2129
|
+
incremental: false,
|
|
2130
|
+
partial: false,
|
|
2131
|
+
routeOnly: false,
|
|
2132
|
+
hierarchy: true,
|
|
2133
|
+
ports: true,
|
|
2134
|
+
},
|
|
2135
|
+
layout(graph, options, context) {
|
|
2136
|
+
return runLayeredPipeline(graph, options ?? {}, context);
|
|
2137
|
+
},
|
|
2138
|
+
};
|