@statelyai/layout 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +221 -13
  3. package/dist/elkjs/index.cjs +3 -2
  4. package/dist/elkjs/index.d.cts +13 -2
  5. package/dist/elkjs/index.d.mts +13 -2
  6. package/dist/elkjs/index.mjs +3 -3
  7. package/dist/elkjs/main.cjs +2 -2
  8. package/dist/elkjs/main.d.cts +2 -2
  9. package/dist/elkjs/main.d.mts +2 -2
  10. package/dist/elkjs/main.mjs +2 -2
  11. package/dist/elkjs/worker-api.cjs +1 -1
  12. package/dist/elkjs/worker-api.d.cts +2 -2
  13. package/dist/elkjs/worker-api.d.mts +2 -2
  14. package/dist/elkjs/worker-api.mjs +1 -1
  15. package/dist/elkjs/worker.cjs +2 -2
  16. package/dist/elkjs/worker.mjs +2 -2
  17. package/dist/elkjs-UciYx7lD.mjs +2973 -0
  18. package/dist/elkjs-ggjsBuVP.cjs +3002 -0
  19. package/dist/index-B6cWNrMw.d.cts +80 -0
  20. package/dist/index-CJQLUKD9.d.mts +80 -0
  21. package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
  22. package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
  23. package/dist/index.cjs +39 -9
  24. package/dist/index.d.cts +4 -3
  25. package/dist/index.d.mts +4 -3
  26. package/dist/index.mjs +4 -3
  27. package/dist/layered/index.cjs +3 -1
  28. package/dist/layered/index.d.cts +3 -2
  29. package/dist/layered/index.d.mts +3 -2
  30. package/dist/layered/index.mjs +2 -2
  31. package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
  32. package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
  33. package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
  34. package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
  35. package/dist/routing/index.cjs +29 -0
  36. package/dist/routing/index.d.cts +3 -0
  37. package/dist/routing/index.d.mts +3 -0
  38. package/dist/routing/index.mjs +3 -0
  39. package/dist/routing-CR8flv8b.mjs +2780 -0
  40. package/dist/routing-QIdGGFBe.cjs +2996 -0
  41. package/dist/types-CT2akdK0.d.mts +133 -0
  42. package/dist/types-uxPlGRof.d.cts +133 -0
  43. package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
  44. package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
  45. package/package.json +14 -3
  46. package/src/authoring/coordinates.ts +38 -10
  47. package/src/authoring/partial.ts +7 -2
  48. package/src/authoring/routing.ts +4 -1
  49. package/src/constraints.ts +174 -6
  50. package/src/elkjs/index.ts +1397 -452
  51. package/src/elkjs/layout-quality.ts +219 -0
  52. package/src/elkjs/opposing-tracks.ts +161 -0
  53. package/src/elkjs/orthogonal-junctions.ts +174 -0
  54. package/src/elkjs/routes.ts +147 -0
  55. package/src/elkjs/statechart.ts +9 -3
  56. package/src/elkjs/types.ts +2 -0
  57. package/src/index.ts +8 -0
  58. package/src/internal/elkjs-compatibility.ts +4 -1
  59. package/src/internal/layered-trace.ts +48 -0
  60. package/src/internal/layout-engine.ts +2 -1
  61. package/src/java-random.ts +6 -0
  62. package/src/layered/bk-node-placement.ts +403 -92
  63. package/src/layered/center-labels.ts +398 -0
  64. package/src/layered/compaction-bounds.ts +26 -0
  65. package/src/layered/compaction-errors.ts +2 -0
  66. package/src/layered/compaction-scanline.ts +63 -0
  67. package/src/layered/compound-boundaries.ts +99 -0
  68. package/src/layered/compound.ts +445 -0
  69. package/src/layered/constraint-orientation.ts +141 -0
  70. package/src/layered/crossing-constraints.ts +243 -0
  71. package/src/layered/crossing-counter.ts +221 -0
  72. package/src/layered/crossing-graph.ts +401 -0
  73. package/src/layered/cycle-random.ts +22 -0
  74. package/src/layered/elk-port-lists.ts +243 -0
  75. package/src/layered/end-labels.ts +124 -0
  76. package/src/layered/external-port-dummy.ts +137 -0
  77. package/src/layered/feedback-chains.ts +114 -0
  78. package/src/layered/fixed-self-loop.ts +150 -0
  79. package/src/layered/greedy-switch-decider.ts +356 -0
  80. package/src/layered/grouped-compaction.ts +787 -0
  81. package/src/layered/gwt-sort.ts +53 -0
  82. package/src/layered/hierarchical-port-phases.ts +242 -0
  83. package/src/layered/hierarchical-port-restoration.ts +233 -0
  84. package/src/layered/hierarchical-port-routing.ts +179 -0
  85. package/src/layered/hierarchy-crossing.ts +121 -0
  86. package/src/layered/hierarchy-sweepiness.ts +83 -0
  87. package/src/layered/hints.ts +371 -0
  88. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  89. package/src/layered/index.ts +911 -261
  90. package/src/layered/initial-model-order.ts +404 -0
  91. package/src/layered/inverted-ports.ts +207 -0
  92. package/src/layered/label-dummy-switcher.ts +200 -0
  93. package/src/layered/long-edges.ts +135 -44
  94. package/src/layered/loop-envelopes.ts +184 -0
  95. package/src/layered/native-routing.ts +275 -0
  96. package/src/layered/network-simplex.ts +11 -15
  97. package/src/layered/node-margins.ts +59 -0
  98. package/src/layered/north-south-ports.ts +271 -0
  99. package/src/layered/opposing-tracks.ts +944 -0
  100. package/src/layered/orthogonal-cycle-order.ts +106 -0
  101. package/src/layered/orthogonal-hypersegments.ts +92 -0
  102. package/src/layered/orthogonal-junctions.ts +202 -0
  103. package/src/layered/orthogonal-segments.ts +243 -0
  104. package/src/layered/placement-orientation.ts +22 -0
  105. package/src/layered/port-distributor.ts +192 -0
  106. package/src/layered/port-edge-order.ts +20 -0
  107. package/src/layered/replace-routing.ts +45 -0
  108. package/src/layered/routing-coordinates.ts +32 -0
  109. package/src/layered/separate-exterior-labels.ts +24 -6
  110. package/src/layered/spacing.ts +34 -12
  111. package/src/layered/splitter-order.ts +106 -0
  112. package/src/layered/strategies.ts +2024 -883
  113. package/src/layered/types.ts +34 -0
  114. package/src/layered/weighted-compaction.ts +84 -0
  115. package/src/routing/adapters.ts +143 -0
  116. package/src/routing/algorithms.ts +1088 -0
  117. package/src/routing/coordination.ts +91 -0
  118. package/src/routing/engine.ts +473 -0
  119. package/src/routing/index.ts +69 -0
  120. package/src/routing/layout-cache.ts +65 -0
  121. package/src/routing/model.ts +218 -0
  122. package/src/routing/path.ts +324 -0
  123. package/src/routing/persistent.ts +117 -0
  124. package/src/routing/search.ts +358 -0
  125. package/src/routing/spatial.ts +78 -0
  126. package/src/routing/types.ts +146 -0
  127. package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
  128. package/dist/elkjs-CFlfkEBz.mjs +0 -1528
  129. /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
  130. /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
  131. /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
  132. /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
@@ -0,0 +1,356 @@
1
+ /*******************************************************************************
2
+ * Derived from ELK v0.11.0 greedyswitch.SwitchDecider, CrossingMatrixFiller,
3
+ * BetweenLayerEdgeTwoNodeCrossingsCounter, NorthSouthEdgeNeighbouringNodeCrossingsCounter
4
+ * and CrossingsCounter (in-layer two-node counting).
5
+ * Copyright (c) 2015, 2020 Kiel University and others.
6
+ * SPDX-License-Identifier: EPL-2.0
7
+ *******************************************************************************/
8
+ import type { CrossingGraph, CrossingNode, CrossingPort } from "./crossing-counter";
9
+
10
+ type Side = CrossingPort["side"];
11
+
12
+ /** ELK's inNorthSouthEastWestOrder: west and south ports are listed in reverse. */
13
+ function sidePorts(node: CrossingNode, side: Side): CrossingPort[] {
14
+ const ports = node.ports.filter((port) => port.side === side);
15
+ return side === "WEST" || side === "SOUTH" ? ports.reverse() : ports;
16
+ }
17
+
18
+ interface Adjacency {
19
+ position: number;
20
+ cardinality: number;
21
+ current: number;
22
+ }
23
+
24
+ /**
25
+ * Decides greedy switches in one free layer from local two-node crossing
26
+ * counts, exactly as ELK does; these estimates can differ from a global recount.
27
+ */
28
+ export class GreedySwitchDecider {
29
+ readonly #free: readonly CrossingNode[];
30
+ readonly #incoming = new Map<string, string[]>();
31
+ readonly #outgoing = new Map<string, string[]>();
32
+ readonly #layerByPort = new Map<string, number>();
33
+ readonly #ownerByPort = new Map<string, CrossingNode>();
34
+ readonly #neighbourPositions = new Map<string, number>();
35
+ readonly #adjacencies = new Map<string, { sides: Map<Side, Adjacency[]> }>();
36
+ readonly #matrix = new Map<string, [number, number]>();
37
+ readonly #inLayerPositions = new Map<string, number>();
38
+ readonly #cardinality = new Map<Side, Map<string, number>>();
39
+ readonly #inLayerSize = new Map<Side, number>();
40
+ readonly #oneSided?: "WEST" | "EAST";
41
+
42
+ constructor(graph: CrossingGraph, freeLayerIndex: number, oneSided?: "WEST" | "EAST") {
43
+ this.#free = graph.layers[freeLayerIndex] ?? [];
44
+ this.#oneSided = oneSided;
45
+ for (const [layer, nodes] of graph.layers.entries())
46
+ for (const node of nodes)
47
+ for (const port of node.ports) {
48
+ this.#layerByPort.set(port.id, layer);
49
+ this.#ownerByPort.set(port.id, node);
50
+ this.#incoming.set(port.id, []);
51
+ this.#outgoing.set(port.id, []);
52
+ }
53
+ for (const edge of graph.edges) {
54
+ this.#outgoing.get(edge.source)?.push(edge.target);
55
+ this.#incoming.get(edge.target)?.push(edge.source);
56
+ }
57
+ for (const [layerIndex, side] of [
58
+ [freeLayerIndex - 1, "EAST"],
59
+ [freeLayerIndex + 1, "WEST"],
60
+ ] as const) {
61
+ let position = 0;
62
+ for (const node of graph.layers[layerIndex] ?? [])
63
+ for (const port of sidePorts(node, side)) this.#neighbourPositions.set(port.id, position++);
64
+ }
65
+ for (const side of ["WEST", "EAST"] as const) {
66
+ const cardinality = new Map<string, number>();
67
+ let position = 0;
68
+ for (const node of this.#free) {
69
+ const ports = sidePorts(node, side);
70
+ cardinality.set(node.id, ports.length);
71
+ for (const port of ports) this.#inLayerPositions.set(port.id, position++);
72
+ }
73
+ this.#cardinality.set(side, cardinality);
74
+ this.#inLayerSize.set(side, position);
75
+ }
76
+ }
77
+
78
+ /** Crossings with `upper` above `lower`, and after switching them. */
79
+ crossings(upper: CrossingNode, lower: CrossingNode): [number, number] {
80
+ const matrix = this.#matrixEntry(upper, lower);
81
+ const left = this.#inLayerBothOrders(upper, lower, "WEST");
82
+ const right = this.#inLayerBothOrders(upper, lower, "EAST");
83
+ const northSouth = this.#northSouth(upper, lower);
84
+ return [
85
+ matrix[0] + left[0] + right[0] + northSouth[0],
86
+ matrix[1] + left[1] + right[1] + northSouth[1],
87
+ ];
88
+ }
89
+
90
+ /** ELK's constraintsPreventSwitch. */
91
+ preventsSwitch(
92
+ upper: CrossingNode,
93
+ lower: CrossingNode,
94
+ successors: ReadonlyMap<string, readonly string[]>,
95
+ ): boolean {
96
+ if (successors.get(upper.id)?.includes(lower.id)) return true;
97
+ const hasEdgesOnSide = (node: CrossingNode, side: Side) =>
98
+ node.ports.some(
99
+ (port) =>
100
+ port.side === side && (port.dummy !== undefined || this.#connected(port.id).length > 0),
101
+ );
102
+ const neitherIsLongEdge = upper.type !== "LONG_EDGE" && lower.type !== "LONG_EDGE";
103
+ const differentUnits = upper.unit !== lower.unit;
104
+ const haveUnits =
105
+ (upper.unit !== undefined && upper.unit !== upper.id) ||
106
+ (lower.unit !== undefined && lower.unit !== lower.id) ||
107
+ hasEdgesOnSide(upper, "SOUTH") ||
108
+ hasEdgesOnSide(lower, "NORTH");
109
+ const unitConstraint =
110
+ (haveUnits && differentUnits) ||
111
+ hasEdgesOnSide(upper, "NORTH") ||
112
+ hasEdgesOnSide(lower, "SOUTH");
113
+ return (
114
+ (neitherIsLongEdge && unitConstraint) ||
115
+ (upper.type === "NORTH_SOUTH_PORT" && lower.type === "NORMAL") ||
116
+ (lower.type === "NORTH_SOUTH_PORT" && upper.type === "NORMAL")
117
+ );
118
+ }
119
+
120
+ notifySwitch(upper: CrossingNode, lower: CrossingNode): void {
121
+ this.#switchPositions(upper, lower, "WEST");
122
+ this.#switchPositions(upper, lower, "EAST");
123
+ }
124
+
125
+ #isInLayer(a: string, b: string): boolean {
126
+ return this.#layerByPort.get(a) === this.#layerByPort.get(b);
127
+ }
128
+
129
+ #matrixEntry(upper: CrossingNode, lower: CrossingNode): [number, number] {
130
+ const key = `${upper.id}\0${lower.id}`;
131
+ let entry = this.#matrix.get(key);
132
+ if (!entry) {
133
+ entry = [0, 0];
134
+ const sides: Side[] = this.#oneSided ? [this.#oneSided] : ["WEST", "EAST"];
135
+ for (const side of sides) {
136
+ const [ul, lu] = this.#mergeAdjacencies(
137
+ this.#adjacencyList(upper, side),
138
+ this.#adjacencyList(lower, side),
139
+ );
140
+ entry[0] += ul;
141
+ entry[1] += lu;
142
+ }
143
+ this.#matrix.set(key, entry);
144
+ this.#matrix.set(`${lower.id}\0${upper.id}`, [entry[1], entry[0]]);
145
+ }
146
+ return entry;
147
+ }
148
+
149
+ #adjacencyList(node: CrossingNode, side: Side): Adjacency[] {
150
+ let cached = this.#adjacencies.get(node.id);
151
+ if (!cached) this.#adjacencies.set(node.id, (cached = { sides: new Map() }));
152
+ let list = cached.sides.get(side);
153
+ if (!list) {
154
+ list = [];
155
+ for (const port of sidePorts(node, side)) {
156
+ const ends = side === "WEST" ? this.#incoming.get(port.id)! : this.#outgoing.get(port.id)!;
157
+ for (const end of ends) {
158
+ if (this.#ownerByPort.get(end) === node || this.#isInLayer(end, port.id)) continue;
159
+ const position = this.#neighbourPositions.get(end) ?? 0;
160
+ const last = list.at(-1);
161
+ // ELK merges only consecutive equal positions before its stable sort.
162
+ if (last?.position === position) last.cardinality++;
163
+ else list.push({ position, cardinality: 1, current: 1 });
164
+ }
165
+ }
166
+ list.sort((a, b) => a.position - b.position);
167
+ cached.sides.set(side, list);
168
+ }
169
+ return list;
170
+ }
171
+
172
+ #mergeAdjacencies(upperList: Adjacency[], lowerList: Adjacency[]): [number, number] {
173
+ const cursor = (list: Adjacency[]) => {
174
+ for (const adjacency of list) adjacency.current = adjacency.cardinality;
175
+ return {
176
+ list,
177
+ index: 0,
178
+ size: list.reduce((sum, adjacency) => sum + adjacency.cardinality, 0),
179
+ };
180
+ };
181
+ const upper = cursor(upperList),
182
+ lower = cursor(lowerList);
183
+ const removeFirst = (state: ReturnType<typeof cursor>) => {
184
+ if (state.size === 0) return;
185
+ const entry = state.list[state.index]!;
186
+ if (entry.current === 1) state.index++;
187
+ else entry.current--;
188
+ state.size--;
189
+ };
190
+ let upperLower = 0,
191
+ lowerUpper = 0;
192
+ while (upper.size !== 0 && lower.size !== 0) {
193
+ const u = upper.list[upper.index]!,
194
+ l = lower.list[lower.index]!;
195
+ if (u.position > l.position) {
196
+ upperLower += upper.size;
197
+ removeFirst(lower);
198
+ } else if (l.position > u.position) {
199
+ lowerUpper += lower.size;
200
+ removeFirst(upper);
201
+ } else {
202
+ upperLower += upper.size - u.current;
203
+ lowerUpper += lower.size - l.current;
204
+ removeFirst(upper);
205
+ removeFirst(lower);
206
+ }
207
+ }
208
+ return [upperLower, lowerUpper];
209
+ }
210
+
211
+ #connected(port: string): string[] {
212
+ return [...this.#incoming.get(port)!, ...this.#outgoing.get(port)!];
213
+ }
214
+
215
+ #inLayerBothOrders(upper: CrossingNode, lower: CrossingNode, side: Side): [number, number] {
216
+ const position = (port: string) => this.#inLayerPositions.get(port) ?? 0;
217
+ // A TreeSet ordered by position: equal positions collapse into one entry.
218
+ const byPosition = new Map<number, string>();
219
+ for (const node of [upper, lower])
220
+ for (const port of sidePorts(node, side))
221
+ for (const end of this.#connected(port.id)) {
222
+ if (this.#ownerByPort.get(end) === node) continue;
223
+ if (!byPosition.has(position(port.id))) byPosition.set(position(port.id), port.id);
224
+ if (this.#isInLayer(end, port.id) && !byPosition.has(position(end)))
225
+ byPosition.set(position(end), end);
226
+ }
227
+ const sorted = () => [...byPosition.values()].sort((a, b) => position(a) - position(b));
228
+ const before = this.#countInLayer(sorted(), side);
229
+ this.#switchPositions(upper, lower, side);
230
+ const after = this.#countInLayer(sorted(), side);
231
+ this.#switchPositions(lower, upper, side);
232
+ return [before, after];
233
+ }
234
+
235
+ #countInLayer(ports: readonly string[], side: Side): number {
236
+ const position = (port: string) => this.#inLayerPositions.get(port) ?? 0;
237
+ const open = Array.from({ length: Math.max(1, this.#inLayerSize.get(side)!) }, () => 0);
238
+ let size = 0,
239
+ crossings = 0;
240
+ const rank = (end: number) => {
241
+ let count = 0;
242
+ for (let index = 0; index < end && index < open.length; index++) count += open[index]!;
243
+ return count;
244
+ };
245
+ for (const port of ports) {
246
+ const at = position(port);
247
+ if (at < open.length) {
248
+ size -= open[at]!;
249
+ open[at] = 0;
250
+ }
251
+ let betweenLayers = 0;
252
+ const ends: number[] = [];
253
+ for (const end of this.#connected(port)) {
254
+ if (this.#isInLayer(end, port)) {
255
+ const endPosition = position(end);
256
+ if (endPosition > at) {
257
+ crossings += rank(endPosition);
258
+ ends.push(endPosition);
259
+ }
260
+ } else betweenLayers++;
261
+ }
262
+ crossings += size * betweenLayers;
263
+ for (const end of ends)
264
+ if (end < open.length) {
265
+ open[end]!++;
266
+ size++;
267
+ }
268
+ }
269
+ return crossings;
270
+ }
271
+
272
+ #switchPositions(wasUpper: CrossingNode, wasLower: CrossingNode, side: Side): void {
273
+ const cardinality = this.#cardinality.get(side)!;
274
+ for (const port of sidePorts(wasUpper, side))
275
+ this.#inLayerPositions.set(
276
+ port.id,
277
+ (this.#inLayerPositions.get(port.id) ?? 0) + (cardinality.get(wasLower.id) ?? 0),
278
+ );
279
+ for (const port of sidePorts(wasLower, side))
280
+ this.#inLayerPositions.set(
281
+ port.id,
282
+ (this.#inLayerPositions.get(port.id) ?? 0) - (cardinality.get(wasUpper.id) ?? 0),
283
+ );
284
+ }
285
+
286
+ /** NorthSouthEdgeNeighbouringNodeCrossingsCounter. */
287
+ #northSouth(upper: CrossingNode, lower: CrossingNode): [number, number] {
288
+ const originSide = (dummy: CrossingNode): Side | undefined => {
289
+ const origin = dummy.ports[0]?.origin;
290
+ return origin === undefined ? undefined : this.#portSide(origin);
291
+ };
292
+ const northSouthEdges = (node: CrossingNode, side: Side) =>
293
+ node.ports.filter((port) => port.side === side && port.dummy !== undefined).length;
294
+ let result: [number, number] = [0, 0];
295
+ if (
296
+ upper.type === "NORTH_SOUTH_PORT" &&
297
+ lower.type === "NORTH_SOUTH_PORT" &&
298
+ upper.unit !== undefined &&
299
+ upper.unit === lower.unit
300
+ )
301
+ result =
302
+ originSide(upper) === "NORTH"
303
+ ? this.#twoNorthSouthDummies(upper, lower)
304
+ : this.#twoNorthSouthDummies(lower, upper);
305
+ if (upper.type === "NORTH_SOUTH_PORT" && lower.type === "LONG_EDGE")
306
+ result = originSide(upper) === "NORTH" ? [1, 0] : [0, 1];
307
+ else if (lower.type === "NORTH_SOUTH_PORT" && upper.type === "LONG_EDGE")
308
+ result = originSide(lower) === "NORTH" ? [0, 1] : [1, 0];
309
+ if (upper.type === "NORMAL" && lower.type === "LONG_EDGE")
310
+ result = [northSouthEdges(upper, "SOUTH"), northSouthEdges(upper, "NORTH")];
311
+ if (lower.type === "NORMAL" && upper.type === "LONG_EDGE")
312
+ result = [northSouthEdges(lower, "NORTH"), northSouthEdges(lower, "SOUTH")];
313
+ return result;
314
+ }
315
+
316
+ #portSide(portId: string): Side | undefined {
317
+ return this.#ownerByPort.get(portId)?.ports.find((port) => port.id === portId)?.side;
318
+ }
319
+
320
+ #twoNorthSouthDummies(further: CrossingNode, closer: CrossingNode): [number, number] {
321
+ const originPosition = (dummy: CrossingNode) => {
322
+ const origin = dummy.ports[0]?.origin;
323
+ const owner = origin === undefined ? undefined : this.#ownerByPort.get(origin);
324
+ const side = origin === undefined ? undefined : this.#portSide(origin);
325
+ if (!owner || !side) return 0;
326
+ return sidePorts(owner, side).findIndex((port) => port.id === origin);
327
+ };
328
+ const degree = (node: CrossingNode, side: Side) => {
329
+ const port = node.ports.find((candidate) => candidate.side === side);
330
+ return port ? this.#connected(port.id).length : 0;
331
+ };
332
+ return originPosition(further) > originPosition(closer)
333
+ ? [degree(closer, "EAST"), degree(further, "WEST")]
334
+ : [degree(closer, "WEST"), degree(further, "EAST")];
335
+ }
336
+ }
337
+
338
+ /**
339
+ * ELK's SweepCopy.assertCorrectPortSides: once crossing minimization finishes,
340
+ * a north/south port whose dummy ended on the other side of its node changes side.
341
+ */
342
+ export function assertNorthSouthPortSides(graph: CrossingGraph): void {
343
+ for (const layer of graph.layers) {
344
+ const index = new Map(layer.map((node, position) => [node.id, position]));
345
+ for (const dummy of layer) {
346
+ if (dummy.type !== "NORTH_SOUTH_PORT") continue;
347
+ const origin = dummy.ports[0]?.origin;
348
+ const owner = layer.find((node) => node.ports.some((port) => port.id === origin));
349
+ const port = owner?.ports.find((candidate) => candidate.id === origin);
350
+ if (!owner || !port) continue;
351
+ if (port.side === "NORTH" && index.get(dummy.id)! > index.get(owner.id)!) port.side = "SOUTH";
352
+ else if (port.side === "SOUTH" && index.get(owner.id)! > index.get(dummy.id)!)
353
+ port.side = "NORTH";
354
+ }
355
+ }
356
+ }