@statelyai/layout 0.0.1 → 0.0.3

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