@statelyai/layout 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/NOTICE.md +9 -1
  2. package/README.md +50 -25
  3. package/dist/elkjs/index.d.mts +3 -0
  4. package/dist/elkjs/index.mjs +904 -97
  5. package/dist/index-8xYkohbz.d.mts +1212 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +3 -3
  8. package/dist/layered/index.d.mts +2 -2
  9. package/dist/layered/index.mjs +2 -2
  10. package/dist/layered-QwJn2gb2.mjs +8978 -0
  11. package/dist/{spore-cihb_Aht.mjs → spore-D15xIQKj.mjs} +1 -31
  12. package/package.json +59 -26
  13. package/src/box.ts +135 -0
  14. package/src/elkjs/index.ts +1825 -0
  15. package/src/elkjs/types.ts +103 -0
  16. package/src/errors.ts +16 -0
  17. package/src/fixed.ts +80 -0
  18. package/src/index.ts +92 -0
  19. package/src/java-random.ts +46 -0
  20. package/src/layered/bk-node-placement.ts +715 -0
  21. package/src/layered/elk-enum-values.ts +171 -0
  22. package/src/layered/elk-options.generated.ts +315 -0
  23. package/src/layered/elk-options.ts +98 -0
  24. package/src/layered/flexible-ports.ts +11 -0
  25. package/src/layered/high-degree.ts +127 -0
  26. package/src/layered/index.ts +2138 -0
  27. package/src/layered/layer-unzipping.ts +217 -0
  28. package/src/layered/linear-segments-node-placement.ts +447 -0
  29. package/src/layered/long-edges.ts +405 -0
  30. package/src/layered/min-width.ts +159 -0
  31. package/src/layered/multi-edge-wrapping.ts +460 -0
  32. package/src/layered/network-simplex-node-placement.ts +500 -0
  33. package/src/layered/network-simplex.ts +346 -0
  34. package/src/layered/node-promotion.ts +197 -0
  35. package/src/layered/spacing.ts +37 -0
  36. package/src/layered/spline-bezier.ts +102 -0
  37. package/src/layered/strategies.ts +5343 -0
  38. package/src/layered/stretch-width.ts +136 -0
  39. package/src/layered/types.ts +111 -0
  40. package/src/layout.ts +202 -0
  41. package/src/packing.ts +74 -0
  42. package/src/random.ts +142 -0
  43. package/src/spore.ts +103 -0
  44. package/src/types.ts +84 -0
  45. package/dist/index-v0P1Ake8.d.mts +0 -149
  46. package/dist/layered-ByNCZQgJ.mjs +0 -439
@@ -0,0 +1,217 @@
1
+ /*******************************************************************************
2
+ * Derived from Eclipse Layout Kernel's AlternatingLayerUnzipper.
3
+ * Copyright (c) 2024 Kiel University and others.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ *******************************************************************************/
6
+
7
+ import type { GraphEdge, GraphNode } from "@statelyai/graph";
8
+ import type { LongEdgeExpansion } from "./long-edges";
9
+ import type { LayerOrder, LayeredPhaseInput } from "./types";
10
+
11
+ type PlaceholderType = "PLACEHOLDER" | "NONSHIFTING_PLACEHOLDER";
12
+
13
+ function isLongEdgeNode(id: string): boolean {
14
+ return id.startsWith("__layout_dummy:");
15
+ }
16
+
17
+ export function unzipLayersAlternating(
18
+ expansion: LongEdgeExpansion,
19
+ order: LayerOrder,
20
+ ): { expansion: LongEdgeExpansion; order: LayerOrder } {
21
+ const input = expansion.input;
22
+ const nodes = [...input.graph.nodes] as GraphNode[];
23
+ let edges = [...input.graph.edges] as GraphEdge[];
24
+ const sizes = new Map(input.sizes);
25
+ const usedNodeIds = new Set(nodes.map((node) => node.id));
26
+ const placeholderTypeById = new Map<string, PlaceholderType>();
27
+ const originalEdgeByCurrentId = new Map(edges.map((edge) => [edge.id, edge]));
28
+ const leavesBySegmentId = new Map(edges.map((edge) => [edge.id, [edge.id] as string[]]));
29
+ let dummySerial = 0;
30
+ let edgeSerial = 0;
31
+
32
+ const uniqueNodeId = (kind: "long" | "placeholder"): string => {
33
+ let id: string;
34
+ do id = `__layout_dummy:unzip:${kind}:${dummySerial++}`;
35
+ while (usedNodeIds.has(id));
36
+ usedNodeIds.add(id);
37
+ return id;
38
+ };
39
+ const createNode = (kind: "long" | PlaceholderType): string => {
40
+ const id = uniqueNodeId(kind === "long" ? "long" : "placeholder");
41
+ nodes.push({ type: "node", id, data: undefined, width: 0, height: 0 });
42
+ sizes.set(id, { width: 0, height: 0 });
43
+ if (kind !== "long") placeholderTypeById.set(id, kind);
44
+ return id;
45
+ };
46
+ const insertAt = (layer: string[], index: number, id: string): void => {
47
+ layer.splice(Math.min(index, layer.length), 0, id);
48
+ };
49
+ const splitEdge = (edge: GraphEdge, dummyId: string): GraphEdge => {
50
+ const original = originalEdgeByCurrentId.get(edge.id) ?? edge;
51
+ const first: GraphEdge = {
52
+ ...edge,
53
+ id: `${edge.id}::unzip:${edgeSerial++}:0`,
54
+ targetId: dummyId,
55
+ targetPort: undefined,
56
+ points: undefined,
57
+ width: 0,
58
+ height: 0,
59
+ };
60
+ const second: GraphEdge = {
61
+ ...edge,
62
+ id: `${edge.id}::unzip:${edgeSerial++}:1`,
63
+ sourceId: dummyId,
64
+ sourcePort: undefined,
65
+ points: undefined,
66
+ width: 0,
67
+ height: 0,
68
+ };
69
+ const index = edges.findIndex((candidate) => candidate.id === edge.id);
70
+ edges.splice(index, 1, first, second);
71
+ originalEdgeByCurrentId.delete(edge.id);
72
+ originalEdgeByCurrentId.set(first.id, original);
73
+ originalEdgeByCurrentId.set(second.id, original);
74
+ for (const leaves of leavesBySegmentId.values()) {
75
+ const leaf = leaves.indexOf(edge.id);
76
+ if (leaf >= 0) leaves.splice(leaf, 1, first.id, second.id);
77
+ }
78
+ return second;
79
+ };
80
+
81
+ const outputLayers: string[][] = [];
82
+ for (const originalLayer of order.layers.map((layer) => [...layer])) {
83
+ const configuredSplits = originalLayer.flatMap((id) => {
84
+ const node = nodes.find((candidate) => candidate.id === id);
85
+ const value = node && input.nodeSettings?.(node)?.["layerUnzipping.layerSplit"];
86
+ return value === undefined ? [] : [Math.max(1, Number(value))];
87
+ });
88
+ const split = configuredSplits.length > 0 ? Math.min(...configuredSplits) : 2;
89
+ const resetOnLongEdges = originalLayer.every((id) => {
90
+ const node = nodes.find((candidate) => candidate.id === id);
91
+ return !node || input.nodeSettings?.(node)?.["layerUnzipping.resetOnLongEdges"] !== false;
92
+ });
93
+ const minimizeEdgeLength = originalLayer.some((id) => {
94
+ const node = nodes.find((candidate) => candidate.id === id);
95
+ return node && input.nodeSettings?.(node)?.["layerUnzipping.minimizeEdgeLength"] === true;
96
+ });
97
+ if (minimizeEdgeLength) {
98
+ const maximumWidth = Math.max(0, ...originalLayer.map((id) => sizes.get(id)?.width ?? 0));
99
+ const averageHeight =
100
+ originalLayer.reduce((sum, id) => sum + (sizes.get(id)?.height ?? 0), 0) /
101
+ originalLayer.length;
102
+ const estimatedWidth =
103
+ maximumWidth +
104
+ Math.max(
105
+ 2 * Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10),
106
+ originalLayer.length * Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10),
107
+ input.spacing.layer,
108
+ );
109
+ const estimatedHeight =
110
+ averageHeight +
111
+ Math.max(input.spacing.node, Number(input.settings["spacing.edgeNode"] ?? 10));
112
+ if (estimatedWidth / estimatedHeight >= originalLayer.length / 4) {
113
+ outputLayers.push(originalLayer);
114
+ continue;
115
+ }
116
+ }
117
+ if (originalLayer.length <= split) {
118
+ outputLayers.push(originalLayer);
119
+ continue;
120
+ }
121
+
122
+ const sublayers = [originalLayer, ...Array.from({ length: split - 1 }, () => [] as string[])];
123
+ const nodesInLayer = originalLayer.length;
124
+ for (
125
+ let j = 0, nodeIndex = 0, targetLayer = 0;
126
+ j < nodesInLayer;
127
+ j++, nodeIndex++, targetLayer++
128
+ ) {
129
+ const nodeId = sublayers[0]![nodeIndex]!;
130
+ if (placeholderTypeById.get(nodeId) === "NONSHIFTING_PLACEHOLDER") {
131
+ j--;
132
+ targetLayer--;
133
+ continue;
134
+ }
135
+ if (targetLayer % split > 0) {
136
+ sublayers[0]!.splice(nodeIndex, 1);
137
+ sublayers[targetLayer % split]!.push(nodeId);
138
+ }
139
+
140
+ let edgeCount = 0;
141
+ const incoming = edges.filter((edge) => edge.targetId === nodeId).reverse();
142
+ for (const incomingEdge of incoming) {
143
+ let next = incomingEdge;
144
+ for (let layerIndex = 0; layerIndex < targetLayer % split; layerIndex++) {
145
+ const dummyId = createNode("long");
146
+ insertAt(sublayers[layerIndex]!, nodeIndex + edgeCount, dummyId);
147
+ next = splitEdge(next, dummyId);
148
+ }
149
+ if (targetLayer % split > 0) edgeCount++;
150
+ }
151
+ if (incoming.length === 0) {
152
+ for (let layerIndex = 0; layerIndex < targetLayer % split; layerIndex++) {
153
+ insertAt(sublayers[layerIndex]!, nodeIndex + edgeCount, createNode("PLACEHOLDER"));
154
+ }
155
+ if (targetLayer % split > 0) edgeCount++;
156
+ }
157
+
158
+ let extraEdge = false;
159
+ for (const outgoingEdge of edges.filter((edge) => edge.sourceId === nodeId)) {
160
+ let next = outgoingEdge;
161
+ for (let layerIndex = (targetLayer % split) + 1; layerIndex < split; layerIndex++) {
162
+ const dummyId = createNode("long");
163
+ sublayers[layerIndex]!.push(dummyId);
164
+ next = splitEdge(next, dummyId);
165
+ }
166
+ for (let layerIndex = 0; layerIndex <= targetLayer % split; layerIndex++) {
167
+ if (extraEdge) {
168
+ insertAt(sublayers[layerIndex]!, nodeIndex + 1, createNode("NONSHIFTING_PLACEHOLDER"));
169
+ }
170
+ }
171
+ if (extraEdge) edgeCount++;
172
+ extraEdge = true;
173
+ }
174
+ nodeIndex += Math.max(0, edgeCount - 1);
175
+ if (resetOnLongEdges && isLongEdgeNode(nodeId)) targetLayer = -1;
176
+ }
177
+ outputLayers.push(...sublayers);
178
+ }
179
+
180
+ const layers = outputLayers.map((layer) =>
181
+ layer.filter((id) => placeholderTypeById.get(id) === undefined),
182
+ );
183
+ const retainedIds = new Set(layers.flat());
184
+ const graphNodes = nodes.filter((node) => retainedIds.has(node.id));
185
+ const layerByNodeId = new Map<string, number>();
186
+ layers.forEach((layer, layerNo) => layer.forEach((id) => layerByNodeId.set(id, layerNo)));
187
+ const segmentIdsByEdgeId = new Map<string, readonly string[]>();
188
+ for (const [edgeId, segmentIds] of expansion.segmentIdsByEdgeId) {
189
+ segmentIdsByEdgeId.set(
190
+ edgeId,
191
+ segmentIds.flatMap((segmentId) => leavesBySegmentId.get(segmentId) ?? [segmentId]),
192
+ );
193
+ }
194
+ const reversedEdgeIds = new Set<string>();
195
+ for (const edge of edges) {
196
+ const original = originalEdgeByCurrentId.get(edge.id);
197
+ if (original && expansion.orientation.reversedEdgeIds.has(original.id)) {
198
+ reversedEdgeIds.add(edge.id);
199
+ }
200
+ }
201
+ const graph = { ...input.graph, nodes: graphNodes, edges } as LayeredPhaseInput["graph"];
202
+ return {
203
+ expansion: {
204
+ ...expansion,
205
+ input: {
206
+ ...input,
207
+ graph,
208
+ sizes,
209
+ edgeSettings: (edge) => input.edgeSettings?.(originalEdgeByCurrentId.get(edge.id) ?? edge),
210
+ },
211
+ orientation: { reversedEdgeIds },
212
+ assignment: { layerByNodeId },
213
+ segmentIdsByEdgeId,
214
+ },
215
+ order: { layers },
216
+ };
217
+ }
@@ -0,0 +1,447 @@
1
+ /*******************************************************************************
2
+ * Derived from Eclipse Layout Kernel's linear-segments node placer.
3
+ * Copyright (c) 2010, 2015 Kiel University and others.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ *******************************************************************************/
6
+
7
+ import type { EntityRect, GraphEdge } from "@statelyai/graph";
8
+ import { placeNodesInLayers } from "./strategies";
9
+ import type { LayerOrder, LayeredPhaseInput, NodePlacement } from "./types";
10
+ import { nodeNodeSpacing } from "./spacing";
11
+
12
+ interface Segment {
13
+ ids: string[];
14
+ deflection: number;
15
+ weight: number;
16
+ ref?: Segment;
17
+ lastLayer: number;
18
+ indexInLastLayer: number;
19
+ }
20
+
21
+ function crossSize(input: LayeredPhaseInput, id: string): number {
22
+ const size = input.sizes.get(id);
23
+ const value =
24
+ input.direction === "left" || input.direction === "right"
25
+ ? (size?.height ?? 0)
26
+ : (size?.width ?? 0);
27
+ if (value !== 0 || !id.startsWith("__layout_dummy:")) return value;
28
+ return Math.max(
29
+ 1,
30
+ ...input.graph.edges
31
+ .filter((edge) => edge.sourceId === id || edge.targetId === id)
32
+ .map((edge) => Number(input.edgeSettings?.(edge)?.["edge.thickness"] ?? 1)),
33
+ );
34
+ }
35
+
36
+ function anchorCrossSize(input: LayeredPhaseInput, id: string): number {
37
+ return id.startsWith("__layout_dummy:") ? 0 : crossSize(input, id);
38
+ }
39
+
40
+ function endpointAnchors(input: LayeredPhaseInput, order: LayerOrder) {
41
+ const layer = new Map<string, number>();
42
+ const index = new Map<string, number>();
43
+ order.layers.forEach((ids, layerNo) =>
44
+ ids.forEach((id, nodeNo) => {
45
+ layer.set(id, layerNo);
46
+ index.set(id, nodeNo);
47
+ }),
48
+ );
49
+ const before = new Map<string, GraphEdge[]>();
50
+ const after = new Map<string, GraphEdge[]>();
51
+ for (const id of layer.keys()) {
52
+ before.set(id, []);
53
+ after.set(id, []);
54
+ }
55
+ for (const edge of input.graph.edges) {
56
+ const sourceLayer = layer.get(edge.sourceId) ?? 0;
57
+ const targetLayer = layer.get(edge.targetId) ?? 0;
58
+ if (sourceLayer < targetLayer) {
59
+ after.get(edge.sourceId)?.push(edge);
60
+ before.get(edge.targetId)?.push(edge);
61
+ } else if (targetLayer < sourceLayer) {
62
+ before.get(edge.sourceId)?.push(edge);
63
+ after.get(edge.targetId)?.push(edge);
64
+ }
65
+ }
66
+ const other = (edge: GraphEdge, id: string) =>
67
+ edge.sourceId === id ? edge.targetId : edge.sourceId;
68
+ const anchors = new Map<string, number>();
69
+ for (const [id, edges] of after) {
70
+ const sweptOrder = order.outputPortOrderByNodeId?.get(id);
71
+ edges.sort((a, b) =>
72
+ sweptOrder
73
+ ? sweptOrder.indexOf(a.id) - sweptOrder.indexOf(b.id)
74
+ : (index.get(other(a, id)) ?? 0) - (index.get(other(b, id)) ?? 0),
75
+ );
76
+ edges.forEach((edge, edgeNo) =>
77
+ anchors.set(
78
+ `${edge.id}:${id}`,
79
+ (anchorCrossSize(input, id) * (edgeNo + 1)) / (edges.length + 1),
80
+ ),
81
+ );
82
+ }
83
+ for (const [id, edges] of before) {
84
+ const sweptOrder = order.inputPortOrderByNodeId?.get(id);
85
+ edges.sort((a, b) =>
86
+ sweptOrder
87
+ ? sweptOrder.indexOf(b.id) - sweptOrder.indexOf(a.id)
88
+ : (index.get(other(a, id)) ?? 0) - (index.get(other(b, id)) ?? 0),
89
+ );
90
+ const forward =
91
+ sweptOrder !== undefined ||
92
+ (input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP") !== "NONE";
93
+ edges.forEach((edge, edgeNo) =>
94
+ anchors.set(
95
+ `${edge.id}:${id}`,
96
+ (anchorCrossSize(input, id) * (forward ? edgeNo + 1 : edges.length - edgeNo)) /
97
+ (edges.length + 1),
98
+ ),
99
+ );
100
+ }
101
+ return { anchors, layer, index };
102
+ }
103
+
104
+ function region(segment: Segment): Segment {
105
+ let current = segment;
106
+ while (current.ref) current = current.ref;
107
+ return current;
108
+ }
109
+
110
+ /** ELK-compatible force-based linear-segments placement. */
111
+ export function placeNodesWithLinearSegments(
112
+ input: LayeredPhaseInput,
113
+ order: LayerOrder,
114
+ ): NodePlacement {
115
+ const base = placeNodesInLayers(input, order);
116
+ const { anchors, layer } = endpointAnchors(input, order);
117
+ const segmentById = new Map<string, Segment>();
118
+ const segments: Segment[] = [];
119
+
120
+ // Normal nodes form singleton segments. Consecutive long-edge dummies form one segment.
121
+ for (const ids of order.layers) {
122
+ for (const id of ids) {
123
+ if (segmentById.has(id)) continue;
124
+ const segment: Segment = {
125
+ ids: [id],
126
+ deflection: 0,
127
+ weight: 0,
128
+ lastLayer: -1,
129
+ indexInLastLayer: -1,
130
+ };
131
+ segmentById.set(id, segment);
132
+ if (id.startsWith("__layout_dummy:")) {
133
+ let current = id;
134
+ while (true) {
135
+ const nextEdge = input.graph.edges.find((edge) => edge.sourceId === current);
136
+ if (!nextEdge) break;
137
+ const next = nextEdge.targetId;
138
+ if (!next.startsWith("__layout_dummy:") || segmentById.has(next)) break;
139
+ segment.ids.push(next);
140
+ segmentById.set(next, segment);
141
+ current = next;
142
+ }
143
+ }
144
+ segments.push(segment);
145
+ }
146
+ }
147
+
148
+ // Build ELK's segment dependency graph, splitting a chain when its order changes
149
+ // between consecutive layers and would otherwise introduce a cycle.
150
+ const outgoing = new Map<Segment, Segment[]>(segments.map((segment) => [segment, []]));
151
+ const indegree = new Map<Segment, number>(segments.map((segment) => [segment, 0]));
152
+ const addDependency = (source: Segment, target: Segment) => {
153
+ outgoing.get(source)?.push(target);
154
+ indegree.set(target, (indegree.get(target) ?? 0) + 1);
155
+ };
156
+ const removeDependency = (source: Segment, target: Segment) => {
157
+ const targets = outgoing.get(source);
158
+ const index = targets?.indexOf(target) ?? -1;
159
+ if (index < 0 || !targets) return;
160
+ targets.splice(index, 1);
161
+ indegree.set(target, (indegree.get(target) ?? 1) - 1);
162
+ };
163
+ for (const [layerNo, ids] of order.layers.entries()) {
164
+ let previous: Segment | undefined;
165
+ for (const [indexInLayer, id] of ids.entries()) {
166
+ let current = segmentById.get(id);
167
+ if (!current) continue;
168
+ if (current.indexInLastLayer >= 0) {
169
+ const cycle = ids.slice(indexInLayer + 1).some((laterId) => {
170
+ const later = segmentById.get(laterId);
171
+ return (
172
+ later !== undefined &&
173
+ later.lastLayer === current!.lastLayer &&
174
+ later.indexInLastLayer < current!.indexInLastLayer
175
+ );
176
+ });
177
+ if (cycle) {
178
+ if (previous) removeDependency(previous, current);
179
+ const splitIndex = current.ids.indexOf(id);
180
+ const movedIds = current.ids.splice(splitIndex);
181
+ const split: Segment = {
182
+ ids: movedIds,
183
+ deflection: 0,
184
+ weight: 0,
185
+ lastLayer: -1,
186
+ indexInLastLayer: -1,
187
+ };
188
+ for (const movedId of movedIds) segmentById.set(movedId, split);
189
+ segments.push(split);
190
+ outgoing.set(split, []);
191
+ indegree.set(split, 0);
192
+ if (previous) addDependency(previous, split);
193
+ current = split;
194
+ }
195
+ }
196
+ const nextId = ids[indexInLayer + 1];
197
+ const next = nextId === undefined ? undefined : segmentById.get(nextId);
198
+ if (next) addDependency(current, next);
199
+ current.lastLayer = layerNo;
200
+ current.indexInLastLayer = indexInLayer;
201
+ previous = current;
202
+ }
203
+ }
204
+ const queue = segments.filter((segment) => indegree.get(segment) === 0);
205
+ const sorted: Segment[] = [];
206
+ while (queue.length) {
207
+ const segment = queue.shift()!;
208
+ sorted.push(segment);
209
+ for (const target of outgoing.get(segment) ?? []) {
210
+ indegree.set(target, (indegree.get(target) ?? 0) - 1);
211
+ if (indegree.get(target) === 0) queue.push(target);
212
+ }
213
+ }
214
+ for (const segment of segments) if (!sorted.includes(segment)) sorted.push(segment);
215
+
216
+ const y = new Map<string, number>();
217
+ const layerExtent = order.layers.map(() => 0);
218
+ const recent = order.layers.map(() => undefined as string | undefined);
219
+ const edgeSpacing = Number(input.settings["spacing.edgeEdge"] ?? 10);
220
+ for (const segment of sorted) {
221
+ let uppermost = 0;
222
+ for (const id of segment.ids) {
223
+ const layerNo = layer.get(id) ?? 0;
224
+ uppermost = Math.max(
225
+ uppermost,
226
+ (layerExtent[layerNo] ?? 0) +
227
+ (recent[layerNo] === undefined
228
+ ? edgeSpacing
229
+ : nodeNodeSpacing(input, recent[layerNo]!, id)),
230
+ );
231
+ }
232
+ for (const id of segment.ids) {
233
+ const layerNo = layer.get(id) ?? 0;
234
+ y.set(id, uppermost);
235
+ layerExtent[layerNo] = uppermost + crossSize(input, id);
236
+ recent[layerNo] = id;
237
+ }
238
+ }
239
+
240
+ const incident = new Map<string, GraphEdge[]>();
241
+ for (const id of layer.keys()) incident.set(id, []);
242
+ for (const edge of input.graph.edges) {
243
+ incident.get(edge.sourceId)?.push(edge);
244
+ incident.get(edge.targetId)?.push(edge);
245
+ }
246
+ const inputPriorityById = new Map<string, number>();
247
+ const outputPriorityById = new Map<string, number>();
248
+ for (const id of layer.keys()) {
249
+ const priorities = (predicate: (edge: GraphEdge) => boolean) =>
250
+ Math.max(
251
+ Number.NEGATIVE_INFINITY,
252
+ ...input.graph.edges
253
+ .filter(predicate)
254
+ .map((edge) => Number(input.edgeSettings?.(edge)?.["priority.straightness"] ?? 0)),
255
+ );
256
+ inputPriorityById.set(
257
+ id,
258
+ priorities((edge) => edge.targetId === id),
259
+ );
260
+ outputPriorityById.set(
261
+ id,
262
+ priorities((edge) => edge.sourceId === id),
263
+ );
264
+ }
265
+ const dampening = Number(
266
+ input.settings["nodePlacement.linearSegments.deflectionDampening"] ?? 0.3,
267
+ );
268
+ const thoroughness = Number(input.settings.thoroughness ?? 7);
269
+ let mode: "FORWARD" | "BACKWARD" | "RUBBER" = "FORWARD";
270
+ let pendulumIterations = 4;
271
+ let finalIterations = 3;
272
+ let ready = false;
273
+ let lastTotal = Number.POSITIVE_INFINITY;
274
+ do {
275
+ const incoming = mode !== "BACKWARD";
276
+ const outgoingEdges = mode !== "FORWARD";
277
+ let total = 0;
278
+ for (const segment of sorted) {
279
+ segment.ref = undefined;
280
+ let totalNodeDeflection = 0;
281
+ let nodeWeight = 0;
282
+ for (const id of segment.ids) {
283
+ let nodeDeflection = 0;
284
+ let edgeWeight = 0;
285
+ const minimumPriority = Math.max(
286
+ incoming
287
+ ? (inputPriorityById.get(id) ?? Number.NEGATIVE_INFINITY)
288
+ : Number.NEGATIVE_INFINITY,
289
+ outgoingEdges
290
+ ? (outputPriorityById.get(id) ?? Number.NEGATIVE_INFINITY)
291
+ : Number.NEGATIVE_INFINITY,
292
+ );
293
+ for (const edge of incident.get(id) ?? []) {
294
+ const source = edge.sourceId === id;
295
+ if ((source && !outgoingEdges) || (!source && !incoming)) continue;
296
+ const other = source ? edge.targetId : edge.sourceId;
297
+ if (segmentById.get(other) === segment) continue;
298
+ const priority = Number(input.edgeSettings?.(edge)?.["priority.straightness"] ?? 0);
299
+ const otherPriority = Math.max(
300
+ inputPriorityById.get(other) ?? Number.NEGATIVE_INFINITY,
301
+ outputPriorityById.get(other) ?? Number.NEGATIVE_INFINITY,
302
+ );
303
+ if (priority < minimumPriority || priority < otherPriority) continue;
304
+ const here =
305
+ (y.get(id) ?? 0) + (anchors.get(`${edge.id}:${id}`) ?? crossSize(input, id) / 2);
306
+ const there =
307
+ (y.get(other) ?? 0) +
308
+ (anchors.get(`${edge.id}:${other}`) ?? crossSize(input, other) / 2);
309
+ nodeDeflection += there - here;
310
+ edgeWeight++;
311
+ }
312
+ if (edgeWeight > 0) {
313
+ totalNodeDeflection += nodeDeflection / edgeWeight;
314
+ nodeWeight++;
315
+ }
316
+ }
317
+ segment.deflection = nodeWeight > 0 ? (dampening * totalNodeDeflection) / nodeWeight : 0;
318
+ segment.weight = nodeWeight;
319
+ total += Math.abs(segment.deflection);
320
+ }
321
+
322
+ let merged: boolean;
323
+ do {
324
+ merged = false;
325
+ for (const ids of order.layers) {
326
+ for (let i = 1; i < ids.length; i++) {
327
+ const upperId = ids[i - 1]!;
328
+ const lowerId = ids[i]!;
329
+ const upper = region(segmentById.get(upperId)!);
330
+ const lower = region(segmentById.get(lowerId)!);
331
+ if (upper === lower) continue;
332
+ const upperExtent =
333
+ (y.get(upperId) ?? 0) +
334
+ crossSize(input, upperId) +
335
+ upper.deflection +
336
+ nodeNodeSpacing(input, upperId, lowerId);
337
+ const lowerExtent = (y.get(lowerId) ?? 0) + lower.deflection;
338
+ if (upperExtent > lowerExtent + 0.0001 * nodeNodeSpacing(input, upperId, lowerId)) {
339
+ const weight = upper.weight + lower.weight;
340
+ if (weight > 0) {
341
+ lower.deflection =
342
+ (lower.weight * lower.deflection + upper.weight * upper.deflection) / weight;
343
+ lower.weight = weight;
344
+ upper.ref = lower;
345
+ merged = true;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ } while (merged);
351
+ for (const segment of sorted) {
352
+ const displacement = region(segment).deflection;
353
+ for (const id of segment.ids) y.set(id, (y.get(id) ?? 0) + displacement);
354
+ }
355
+
356
+ if (mode !== "RUBBER") {
357
+ pendulumIterations--;
358
+ if (pendulumIterations <= 0 && (total < lastTotal || -pendulumIterations > thoroughness)) {
359
+ mode = "RUBBER";
360
+ lastTotal = Number.POSITIVE_INFINITY;
361
+ } else {
362
+ mode = mode === "FORWARD" ? "BACKWARD" : "FORWARD";
363
+ lastTotal = total;
364
+ }
365
+ } else {
366
+ ready = total >= lastTotal || lastTotal - total < 20 / thoroughness;
367
+ lastTotal = total;
368
+ if (ready) finalIterations--;
369
+ }
370
+ } while (!(ready && finalIterations <= 0));
371
+
372
+ // ELK's correction pass snaps a segment to the nearest feasible straight incident edge.
373
+ for (const segment of sorted) {
374
+ let roomAbove = Number.POSITIVE_INFINITY;
375
+ let roomBelow = Number.POSITIVE_INFINITY;
376
+ for (const id of segment.ids) {
377
+ const layerNo = layer.get(id) ?? 0;
378
+ const ids = order.layers[layerNo] ?? [];
379
+ const index = ids.indexOf(id);
380
+ const upper = ids[index - 1];
381
+ const lower = ids[index + 1];
382
+ roomAbove = Math.min(
383
+ roomAbove,
384
+ upper === undefined
385
+ ? (y.get(id) ?? 0)
386
+ : (y.get(id) ?? 0) -
387
+ ((y.get(upper) ?? 0) + crossSize(input, upper) + nodeNodeSpacing(input, id, upper)),
388
+ );
389
+ roomBelow = Math.min(
390
+ roomBelow,
391
+ lower === undefined
392
+ ? 2 * (y.get(id) ?? 0)
393
+ : (y.get(lower) ?? 0) -
394
+ ((y.get(id) ?? 0) + crossSize(input, id) + nodeNodeSpacing(input, id, lower)),
395
+ );
396
+ }
397
+ let displacement = Number.POSITIVE_INFINITY;
398
+ const endpointEdges: Array<[string, readonly GraphEdge[]]> = [
399
+ [
400
+ segment.ids[0]!,
401
+ (incident.get(segment.ids[0]!) ?? []).filter((edge) => edge.targetId === segment.ids[0]),
402
+ ],
403
+ [
404
+ segment.ids.at(-1)!,
405
+ (incident.get(segment.ids.at(-1)!) ?? []).filter(
406
+ (edge) => edge.sourceId === segment.ids.at(-1),
407
+ ),
408
+ ],
409
+ ];
410
+ for (const [id, edges] of endpointEdges) {
411
+ for (const edge of edges) {
412
+ const other = edge.sourceId === id ? edge.targetId : edge.sourceId;
413
+ const delta =
414
+ (y.get(other) ?? 0) +
415
+ (anchors.get(`${edge.id}:${other}`) ?? crossSize(input, other) / 2) -
416
+ (y.get(id) ?? 0) -
417
+ (anchors.get(`${edge.id}:${id}`) ?? crossSize(input, id) / 2);
418
+ if (
419
+ Math.abs(delta) < Math.abs(displacement) &&
420
+ Math.abs(delta) < (delta < 0 ? roomAbove : roomBelow)
421
+ ) {
422
+ displacement = delta;
423
+ }
424
+ }
425
+ }
426
+ if (Number.isFinite(displacement) && displacement !== 0) {
427
+ for (const id of segment.ids) y.set(id, (y.get(id) ?? 0) + displacement);
428
+ }
429
+ }
430
+
431
+ const minimum = Math.min(...y.values());
432
+ const crossPadding =
433
+ input.direction === "left" || input.direction === "right"
434
+ ? input.padding.top
435
+ : input.padding.left;
436
+ const rectByNodeId = new Map<string, EntityRect>();
437
+ for (const [id, rect] of base.rectByNodeId) {
438
+ const cross = (y.get(id) ?? 0) - minimum + crossPadding;
439
+ rectByNodeId.set(
440
+ id,
441
+ input.direction === "left" || input.direction === "right"
442
+ ? { ...rect, y: cross }
443
+ : { ...rect, x: cross },
444
+ );
445
+ }
446
+ return { rectByNodeId };
447
+ }