@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,500 @@
1
+ /*******************************************************************************
2
+ * Derived from Eclipse Layout Kernel's network-simplex node placer.
3
+ * Copyright (c) 2016, 2017 Kiel University and others.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ *******************************************************************************/
6
+
7
+ import type { EntityRect, GraphEdge } from "@statelyai/graph";
8
+ import { runNetworkSimplex } from "./network-simplex";
9
+ import type { SimplexEdge, SimplexNode } from "./network-simplex";
10
+ import { placeNodesInLayers } from "./strategies";
11
+ import type { LayerOrder, LayeredPhaseInput, NodePlacement } from "./types";
12
+ import { nodeNodeSpacing } from "./spacing";
13
+ import { setFlexiblePortPosition } from "./flexible-ports";
14
+
15
+ function crossSize(input: LayeredPhaseInput, id: string): number {
16
+ const size = input.sizes.get(id);
17
+ return input.direction === "left" || input.direction === "right"
18
+ ? (size?.height ?? 0)
19
+ : (size?.width ?? 0);
20
+ }
21
+
22
+ function makeNode(id: string, order: number): SimplexNode {
23
+ return { id, order, layer: 0, incoming: [], outgoing: [], treeNode: false };
24
+ }
25
+
26
+ function addEdge(
27
+ edges: SimplexEdge[],
28
+ source: SimplexNode,
29
+ target: SimplexNode,
30
+ delta: number,
31
+ weight: number,
32
+ ): void {
33
+ const edge: SimplexEdge = {
34
+ id: `aux:${edges.length}`,
35
+ order: edges.length,
36
+ source,
37
+ target,
38
+ delta,
39
+ weight,
40
+ treeEdge: false,
41
+ };
42
+ edges.push(edge);
43
+ source.outgoing.push(edge);
44
+ target.incoming.push(edge);
45
+ }
46
+
47
+ /** Mirrors NGraph.makeConnected(): network simplex tie-breaking observes these zero edges. */
48
+ function makeConnected(nodes: SimplexNode[], edges: SimplexEdge[]): void {
49
+ const visited = new Set<SimplexNode>();
50
+ const representatives: SimplexNode[] = [];
51
+ for (const node of nodes) {
52
+ if (visited.has(node)) continue;
53
+ representatives.push(node);
54
+ visited.add(node);
55
+ const stack = [node];
56
+ while (stack.length > 0) {
57
+ const current = stack.pop()!;
58
+ for (const edge of [...current.incoming, ...current.outgoing]) {
59
+ const other = edge.source === current ? edge.target : edge.source;
60
+ if (visited.has(other)) continue;
61
+ visited.add(other);
62
+ stack.push(other);
63
+ }
64
+ }
65
+ }
66
+ if (representatives.length < 2) return;
67
+ const root = makeNode("__network_simplex_root", nodes.length);
68
+ nodes.push(root);
69
+ for (const representative of representatives) addEdge(edges, root, representative, 0, 0);
70
+ }
71
+
72
+ function endpointAnchors(input: LayeredPhaseInput, order: LayerOrder) {
73
+ const layer = new Map<string, number>();
74
+ const index = new Map<string, number>();
75
+ order.layers.forEach((ids, layerNo) =>
76
+ ids.forEach((id, nodeNo) => {
77
+ layer.set(id, layerNo);
78
+ index.set(id, nodeNo);
79
+ }),
80
+ );
81
+ const before = new Map<string, GraphEdge[]>();
82
+ const after = new Map<string, GraphEdge[]>();
83
+ for (const id of layer.keys()) {
84
+ before.set(id, []);
85
+ after.set(id, []);
86
+ }
87
+ for (const edge of input.graph.edges) {
88
+ const sourceLayer = layer.get(edge.sourceId) ?? 0;
89
+ const targetLayer = layer.get(edge.targetId) ?? 0;
90
+ if (sourceLayer < targetLayer) {
91
+ after.get(edge.sourceId)?.push(edge);
92
+ before.get(edge.targetId)?.push(edge);
93
+ } else if (targetLayer < sourceLayer) {
94
+ before.get(edge.sourceId)?.push(edge);
95
+ after.get(edge.targetId)?.push(edge);
96
+ }
97
+ }
98
+ const other = (edge: GraphEdge, id: string) =>
99
+ edge.sourceId === id ? edge.targetId : edge.sourceId;
100
+ const anchors = new Map<string, number>();
101
+ for (const [id, edges] of after) {
102
+ const sweptOrder = order.outputPortOrderByNodeId?.get(id);
103
+ const direction =
104
+ input.direction === "left" &&
105
+ input.settings["crossingMinimization.strategy"] === "INTERACTIVE"
106
+ ? -1
107
+ : 1;
108
+ edges.sort(
109
+ (a, b) =>
110
+ direction *
111
+ (sweptOrder
112
+ ? sweptOrder.indexOf(a.id) - sweptOrder.indexOf(b.id)
113
+ : (index.get(other(a, id)) ?? 0) - (index.get(other(b, id)) ?? 0)),
114
+ );
115
+ if (
116
+ input.direction === "left" &&
117
+ input.settings["crossingMinimization.strategy"] === "INTERACTIVE" &&
118
+ edges.length >= 3
119
+ ) {
120
+ const first = edges.shift()!;
121
+ const modelOrder = new Map(input.graph.edges.map((edge, edgeIndex) => [edge.id, edgeIndex]));
122
+ edges.sort((a, b) => (modelOrder.get(a.id) ?? 0) - (modelOrder.get(b.id) ?? 0));
123
+ edges.unshift(first);
124
+ }
125
+ edges.forEach((edge, edgeNo) =>
126
+ anchors.set(
127
+ `${edge.id}:${id}`,
128
+ Math.round((crossSize(input, id) * (edgeNo + 1)) / (edges.length + 1)),
129
+ ),
130
+ );
131
+ }
132
+ for (const [id, edges] of before) {
133
+ const sweptOrder = order.inputPortOrderByNodeId?.get(id);
134
+ edges.sort((a, b) =>
135
+ sweptOrder
136
+ ? sweptOrder.indexOf(b.id) - sweptOrder.indexOf(a.id)
137
+ : (index.get(other(a, id)) ?? 0) - (index.get(other(b, id)) ?? 0),
138
+ );
139
+ const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
140
+ const layerOrdered =
141
+ sweptOrder !== undefined ||
142
+ crossingStrategy === "LAYER_SWEEP" ||
143
+ crossingStrategy === "MEDIAN_LAYER_SWEEP" ||
144
+ (crossingStrategy === "INTERACTIVE" &&
145
+ edges.some((edge) => other(edge, id).startsWith("__layout_dummy:"))) ||
146
+ (crossingStrategy === "NONE" &&
147
+ (input.direction === "left" || input.direction === "right") &&
148
+ edges.some((edge) => other(edge, id).startsWith("__layout_dummy:")));
149
+ edges.forEach((edge, edgeNo) =>
150
+ anchors.set(
151
+ `${edge.id}:${id}`,
152
+ Math.round(
153
+ (crossSize(input, id) * (layerOrdered ? edgeNo + 1 : edges.length - edgeNo)) /
154
+ (edges.length + 1),
155
+ ),
156
+ ),
157
+ );
158
+ }
159
+ return anchors;
160
+ }
161
+
162
+ /** ELK auxiliary-graph network-simplex placement for fixed-position ports. */
163
+ export function placeNodesWithNetworkSimplex(
164
+ input: LayeredPhaseInput,
165
+ order: LayerOrder,
166
+ ): NodePlacement {
167
+ const base = placeNodesInLayers(input, order);
168
+ const horizontal = input.direction === "left" || input.direction === "right";
169
+ const inputNodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
170
+ const anchors = endpointAnchors(input, order);
171
+ const favorStraightEdges =
172
+ input.settings["nodePlacement.favorStraightEdges"] ??
173
+ (input.settings.edgeRouting ?? "ORTHOGONAL") === "ORTHOGONAL";
174
+ const preferredWeightByEdgeId = new Map<string, number>();
175
+ if (favorStraightEdges) {
176
+ const incoming = new Map(input.graph.nodes.map((node) => [node.id, [] as GraphEdge[]]));
177
+ const outgoing = new Map(input.graph.nodes.map((node) => [node.id, [] as GraphEdge[]]));
178
+ for (const edge of input.graph.edges) {
179
+ if (edge.sourceId === edge.targetId) continue;
180
+ outgoing.get(edge.sourceId)?.push(edge);
181
+ incoming.get(edge.targetId)?.push(edge);
182
+ }
183
+ const junction = (id: string): boolean => {
184
+ const incomingCount = incoming.get(id)?.length ?? 0;
185
+ const outgoingCount = outgoing.get(id)?.length ?? 0;
186
+ return incomingCount > 1 || outgoingCount > 1 || incomingCount + outgoingCount === 1;
187
+ };
188
+ for (const node of input.graph.nodes) {
189
+ if (!junction(node.id)) continue;
190
+ for (const first of outgoing.get(node.id) ?? []) {
191
+ const path = [first];
192
+ let currentId = first.targetId;
193
+ const seen = new Set([first.id]);
194
+ while (!junction(currentId)) {
195
+ const next = outgoing.get(currentId)?.[0];
196
+ if (!next || seen.has(next.id)) break;
197
+ path.push(next);
198
+ seen.add(next.id);
199
+ currentId = next.targetId;
200
+ }
201
+ if (
202
+ path.length <= 2 ||
203
+ path.some(
204
+ (edge) =>
205
+ edge.sourceId.startsWith("__layout_dummy:") ||
206
+ edge.targetId.startsWith("__layout_dummy:"),
207
+ )
208
+ ) {
209
+ continue;
210
+ }
211
+ path.forEach((edge, index) => {
212
+ const weight = index === 0 || index === path.length - 1 ? 16 : 64;
213
+ preferredWeightByEdgeId.set(
214
+ edge.id,
215
+ Math.max(preferredWeightByEdgeId.get(edge.id) ?? 0, weight),
216
+ );
217
+ });
218
+ }
219
+ }
220
+ }
221
+ const nodes: SimplexNode[] = [];
222
+ const nodeById = new Map<string, SimplexNode>();
223
+ for (const layer of order.layers) {
224
+ for (const id of layer) {
225
+ const node = makeNode(id, nodes.length);
226
+ nodes.push(node);
227
+ nodeById.set(id, node);
228
+ }
229
+ }
230
+ const edges: SimplexEdge[] = [];
231
+ for (const layer of order.layers) {
232
+ for (let index = 1; index < layer.length; index++) {
233
+ const upperId = layer[index - 1]!;
234
+ const lowerId = layer[index]!;
235
+ addEdge(
236
+ edges,
237
+ nodeById.get(upperId)!,
238
+ nodeById.get(lowerId)!,
239
+ Math.ceil(
240
+ crossSize(input, upperId) +
241
+ nodeNodeSpacing(input, upperId, lowerId) +
242
+ (upperId.startsWith("__layout_dummy:") ? 1 : 0),
243
+ ),
244
+ 0,
245
+ );
246
+ }
247
+ }
248
+ const graphEdgesInLayerOrder = order.layers.flatMap((layer) =>
249
+ layer.flatMap((id) => {
250
+ const outgoing = input.graph.edges.filter((edge) => edge.sourceId === id);
251
+ const portOrder = order.outputPortOrderByNodeId?.get(id);
252
+ if (
253
+ input.direction === "left" &&
254
+ input.settings["crossingMinimization.strategy"] === "INTERACTIVE"
255
+ ) {
256
+ return outgoing.sort(
257
+ (left, right) =>
258
+ (anchors.get(`${left.id}:${id}`) ?? 0) - (anchors.get(`${right.id}:${id}`) ?? 0),
259
+ );
260
+ }
261
+ return portOrder === undefined
262
+ ? outgoing
263
+ : outgoing.sort((left, right) => portOrder.indexOf(left.id) - portOrder.indexOf(right.id));
264
+ }),
265
+ );
266
+ for (const graphEdge of graphEdgesInLayerOrder) {
267
+ if (graphEdge.sourceId === graphEdge.targetId) continue;
268
+ const source = nodeById.get(graphEdge.sourceId);
269
+ const target = nodeById.get(graphEdge.targetId);
270
+ if (!source || !target) continue;
271
+ const dummy = makeNode(`edge:${graphEdge.id}`, nodes.length);
272
+ nodes.push(dummy);
273
+ const sourceOffset = anchors.get(`${graphEdge.id}:${graphEdge.sourceId}`) ?? 0;
274
+ const targetOffset = anchors.get(`${graphEdge.id}:${graphEdge.targetId}`) ?? 0;
275
+ const priority = Math.max(
276
+ 1,
277
+ Number(input.edgeSettings?.(graphEdge)?.["priority.straightness"] ?? 1),
278
+ );
279
+ const sourceDummy = graphEdge.sourceId.startsWith("__layout_dummy:");
280
+ const targetDummy = graphEdge.targetId.startsWith("__layout_dummy:");
281
+ const typeWeight = sourceDummy && targetDummy ? 32 : sourceDummy || targetDummy ? 8 : 4;
282
+ const weight = priority * (preferredWeightByEdgeId.get(graphEdge.id) ?? typeWeight);
283
+ addEdge(edges, dummy, source, Math.max(0, targetOffset - sourceOffset), weight);
284
+ addEdge(edges, dummy, target, Math.max(0, sourceOffset - targetOffset), weight);
285
+ }
286
+ makeConnected(nodes, edges);
287
+ runNetworkSimplex(
288
+ nodes,
289
+ edges,
290
+ Number(input.settings.thoroughness ?? 7) * nodes.length,
291
+ undefined,
292
+ false,
293
+ );
294
+
295
+ const minimum = Math.min(...[...nodeById.values()].map((node) => node.layer));
296
+ const crossPadding =
297
+ input.direction === "left" || input.direction === "right"
298
+ ? input.padding.top
299
+ : input.padding.left;
300
+ const rectByNodeId = new Map<string, EntityRect>();
301
+ for (const [id, rect] of base.rectByNodeId) {
302
+ const cross = (nodeById.get(id)?.layer ?? 0) - minimum + crossPadding;
303
+ rectByNodeId.set(
304
+ id,
305
+ input.direction === "left" || input.direction === "right"
306
+ ? { ...rect, y: cross }
307
+ : { ...rect, x: cross },
308
+ );
309
+ }
310
+
311
+ if (
312
+ input.graph.edges.some(
313
+ (edge) => Number(input.edgeSettings?.(edge)?.["priority.straightness"] ?? 1) > 1,
314
+ )
315
+ ) {
316
+ const horizontal = input.direction === "left" || input.direction === "right";
317
+ const crossStart = (rect: EntityRect) => (horizontal ? rect.y : rect.x);
318
+ const setCrossStart = (rect: EntityRect, value: number): EntityRect =>
319
+ horizontal ? { ...rect, y: value } : { ...rect, x: value };
320
+ for (let pass = 0; pass < 2; pass++) {
321
+ for (const layer of order.layers) {
322
+ for (const [index, id] of layer.entries()) {
323
+ const rect = rectByNodeId.get(id);
324
+ if (!rect) continue;
325
+ const weightedTargets: Array<{ value: number; weight: number }> = [];
326
+ for (const edge of input.graph.edges) {
327
+ if (edge.sourceId !== id && edge.targetId !== id) continue;
328
+ const otherId = edge.sourceId === id ? edge.targetId : edge.sourceId;
329
+ const otherRect = rectByNodeId.get(otherId);
330
+ if (!otherRect) continue;
331
+ const ownAnchor = anchors.get(`${edge.id}:${id}`) ?? 0;
332
+ const otherAnchor = anchors.get(`${edge.id}:${otherId}`) ?? 0;
333
+ weightedTargets.push({
334
+ value: crossStart(otherRect) + otherAnchor - ownAnchor,
335
+ weight: Math.max(
336
+ 1,
337
+ Number(input.edgeSettings?.(edge)?.["priority.straightness"] ?? 1),
338
+ ),
339
+ });
340
+ }
341
+ if (weightedTargets.length === 0) continue;
342
+ weightedTargets.sort((left, right) => left.value - right.value);
343
+ const totalWeight = weightedTargets.reduce((sum, target) => sum + target.weight, 0);
344
+ let cumulative = 0;
345
+ let lowerMedian = weightedTargets[0]!.value;
346
+ let upperMedian = weightedTargets.at(-1)!.value;
347
+ let lowerMedianFound = false;
348
+ for (const target of weightedTargets) {
349
+ cumulative += target.weight;
350
+ if (cumulative >= totalWeight / 2 && !lowerMedianFound) {
351
+ lowerMedian = target.value;
352
+ lowerMedianFound = true;
353
+ }
354
+ if (cumulative > totalWeight / 2) {
355
+ upperMedian = target.value;
356
+ break;
357
+ }
358
+ }
359
+ const upperId = layer[index - 1];
360
+ const lowerId = layer[index + 1];
361
+ const upperRect = upperId ? rectByNodeId.get(upperId) : undefined;
362
+ const lowerRect = lowerId ? rectByNodeId.get(lowerId) : undefined;
363
+ const minimum = upperRect
364
+ ? crossStart(upperRect) +
365
+ (horizontal ? upperRect.height : upperRect.width) +
366
+ nodeNodeSpacing(input, upperId!, id)
367
+ : Number.NEGATIVE_INFINITY;
368
+ const maximum = lowerRect
369
+ ? crossStart(lowerRect) -
370
+ (horizontal ? rect.height : rect.width) -
371
+ nodeNodeSpacing(input, id, lowerId!)
372
+ : Number.POSITIVE_INFINITY;
373
+ const median = input.graph.edges.some((edge) => edge.sourceId === id)
374
+ ? upperMedian
375
+ : lowerMedian;
376
+ rectByNodeId.set(id, setCrossStart(rect, Math.max(minimum, Math.min(maximum, median))));
377
+ }
378
+ }
379
+ }
380
+ const minimumCross = Math.min(...[...rectByNodeId.values()].map(crossStart));
381
+ const desiredMinimum = horizontal ? input.padding.top : input.padding.left;
382
+ for (const [id, rect] of rectByNodeId) {
383
+ rectByNodeId.set(id, setCrossStart(rect, crossStart(rect) + desiredMinimum - minimumCross));
384
+ }
385
+ }
386
+
387
+ for (const node of input.graph.nodes) {
388
+ const flexibility = String(
389
+ input.nodeSettings?.(node)?.["nodePlacement.networkSimplex.nodeFlexibility"] ??
390
+ input.settings["nodePlacement.networkSimplex.nodeFlexibility.default"] ??
391
+ "NONE",
392
+ );
393
+ if (flexibility === "NONE") continue;
394
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
395
+ if (constraints === "FIXED_RATIO" || constraints === "FIXED_POS") continue;
396
+ const rect = rectByNodeId.get(node.id);
397
+ if (!rect) continue;
398
+ const desired = (node.ports ?? []).flatMap((port) => {
399
+ const connected = input.graph.edges.find(
400
+ (edge) =>
401
+ (edge.sourceId === node.id && edge.sourcePort === port.name) ||
402
+ (edge.targetId === node.id && edge.targetPort === port.name),
403
+ );
404
+ if (!connected) return [];
405
+ const oppositeId = connected.sourceId === node.id ? connected.targetId : connected.sourceId;
406
+ const oppositeRect = rectByNodeId.get(oppositeId);
407
+ if (!oppositeRect) return [];
408
+ const portSize = horizontal ? (port.height ?? 8) : (port.width ?? 8);
409
+ const center = horizontal
410
+ ? oppositeRect.y + oppositeRect.height / 2
411
+ : oppositeRect.x + oppositeRect.width / 2;
412
+ return [{ port, center, portSize }];
413
+ });
414
+ if (desired.length === 0) continue;
415
+ const minimumCenter = Math.min(...desired.map((entry) => entry.center));
416
+ const maximumCenter = Math.max(...desired.map((entry) => entry.center));
417
+ const endSize = Math.max(...desired.map((entry) => entry.portSize));
418
+ const requiredSize = maximumCenter - minimumCenter + endSize;
419
+ const mayResize =
420
+ flexibility === "NODE_SIZE" || flexibility === "NODE_SIZE_WHERE_SPACE_PERMITS";
421
+ const currentCrossSize = horizontal ? rect.height : rect.width;
422
+ if (!mayResize && requiredSize > currentCrossSize) {
423
+ const count = desired.length;
424
+ for (const [index, { port, portSize }] of desired.entries()) {
425
+ const center =
426
+ portSize / 2 +
427
+ ((index + 1) * (currentCrossSize - count * portSize)) / (count + 1) +
428
+ index * portSize;
429
+ const axis = Math.round(center - portSize / 2);
430
+ setFlexiblePortPosition(
431
+ port,
432
+ horizontal ? (port.x ?? 0) : axis,
433
+ horizontal ? axis : (port.y ?? 0),
434
+ );
435
+ }
436
+ rectByNodeId.set(
437
+ node.id,
438
+ horizontal ? { ...rect, y: rect.y + 1 } : { ...rect, x: rect.x + 1 },
439
+ );
440
+ continue;
441
+ }
442
+ const resultSize = mayResize ? Math.max(currentCrossSize, requiredSize) : currentCrossSize;
443
+ const crossStart =
444
+ requiredSize <= currentCrossSize
445
+ ? horizontal
446
+ ? rect.y
447
+ : rect.x
448
+ : minimumCenter - endSize / 2;
449
+ rectByNodeId.set(
450
+ node.id,
451
+ horizontal
452
+ ? { ...rect, y: crossStart, height: resultSize }
453
+ : { ...rect, x: crossStart, width: resultSize },
454
+ );
455
+ for (const { port, center, portSize } of desired) {
456
+ const axis = center - crossStart - portSize / 2;
457
+ setFlexiblePortPosition(
458
+ port,
459
+ horizontal ? (port.x ?? 0) : axis,
460
+ horizontal ? axis : (port.y ?? 0),
461
+ );
462
+ }
463
+ }
464
+ let flexiblePortFlowOffset = 0;
465
+ for (const [layerIndex, layer] of order.layers.entries()) {
466
+ if (flexiblePortFlowOffset > 0) {
467
+ for (const id of layer) {
468
+ const rect = rectByNodeId.get(id);
469
+ if (!rect) continue;
470
+ rectByNodeId.set(
471
+ id,
472
+ horizontal
473
+ ? { ...rect, x: rect.x + flexiblePortFlowOffset }
474
+ : { ...rect, y: rect.y + flexiblePortFlowOffset },
475
+ );
476
+ }
477
+ }
478
+ if (layerIndex === order.layers.length - 1) continue;
479
+ flexiblePortFlowOffset += Math.max(
480
+ 0,
481
+ ...layer.map((id) => {
482
+ const node = inputNodeById.get(id);
483
+ const flexibility = String(
484
+ (node && input.nodeSettings?.(node)?.["nodePlacement.networkSimplex.nodeFlexibility"]) ??
485
+ input.settings["nodePlacement.networkSimplex.nodeFlexibility.default"] ??
486
+ "NONE",
487
+ );
488
+ if (flexibility !== "PORT_POSITION") return 0;
489
+ // ELK keeps one flow-axis unit of clearance on either side of a movable port box.
490
+ return Math.max(
491
+ 0,
492
+ ...(node?.ports ?? []).map(
493
+ (port) => (horizontal ? (port.width ?? 8) : (port.height ?? 8)) + 2,
494
+ ),
495
+ );
496
+ }),
497
+ );
498
+ }
499
+ return { rectByNodeId };
500
+ }