@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,404 @@
1
+ /*
2
+ * Copyright (c) 2020 Kiel University and others.
3
+ * Native adaptation of ELK SortByInputModelProcessor and model-order comparators.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ */
6
+ import type { CrossingGraph, CrossingNode, CrossingPort } from "./crossing-counter";
7
+ import { gwtSort, insertionSort } from "./gwt-sort";
8
+ import type { AcyclicOrientation, LayeredPhaseInput } from "./types";
9
+
10
+ /** Retain transitive decisions from earlier comparisons for this one sort. */
11
+ export function transitiveComparator<T>(
12
+ key: (value: T) => string,
13
+ compare: (left: T, right: T) => number,
14
+ compareEqualKeys = false,
15
+ ) {
16
+ const follows = new Map<string, Set<string>>();
17
+ return (left: T, right: T): number => {
18
+ const a = key(left),
19
+ b = key(right);
20
+ // Shared-port helper comparisons intentionally retain ELK's nonzero tie.
21
+ if (a === b) return compareEqualKeys ? compare(left, right) : 0;
22
+ if (follows.get(a)?.has(b)) return -1;
23
+ if (follows.get(b)?.has(a)) return 1;
24
+ const result = compare(left, right);
25
+ if (result) {
26
+ const earlier = result < 0 ? a : b,
27
+ later = result < 0 ? b : a;
28
+ const descendants = new Set([later, ...(follows.get(later) ?? [])]);
29
+ for (const predecessor of [
30
+ earlier,
31
+ ...[...follows].filter(([, values]) => values.has(earlier)).map(([id]) => id),
32
+ ]) {
33
+ const values = follows.get(predecessor) ?? new Set<string>();
34
+ for (const id of descendants) values.add(id);
35
+ follows.set(predecessor, values);
36
+ }
37
+ }
38
+ return result;
39
+ };
40
+ }
41
+
42
+ export function normalModelComparator(
43
+ input: LayeredPhaseInput,
44
+ respectGroupBoundary = true,
45
+ ): (left: string, right: string) => number {
46
+ const byId = new Map(input.graph.nodes.map((node, index) => [node.id, { node, index }]));
47
+ const mode =
48
+ input.settings["considerModelOrder.groupModelOrder.cmGroupOrderStrategy"] ??
49
+ "ONLY_WITHIN_GROUP";
50
+ const configured = input.settings["considerModelOrder.groupModelOrder.cmEnforcedGroupOrders"];
51
+ const enforced = new Set<number>(
52
+ Array.isArray(configured)
53
+ ? configured.map(Number)
54
+ : typeof configured === "string"
55
+ ? (configured.match(/-?\d+/g) ?? []).map(Number)
56
+ : [1, 2, 6, 7, 10, 11],
57
+ );
58
+ return (left, right) => {
59
+ const a = byId.get(left),
60
+ b = byId.get(right);
61
+ if (!a || !b || left.startsWith("__layout_dummy:") || right.startsWith("__layout_dummy:"))
62
+ return 0;
63
+ const aa = input.nodeSettings?.(a.node),
64
+ bb = input.nodeSettings?.(b.node);
65
+ if (
66
+ aa?.["considerModelOrder.noModelOrder"] === true ||
67
+ bb?.["considerModelOrder.noModelOrder"] === true
68
+ )
69
+ return 0;
70
+ if (
71
+ [aa?.["layering.layerConstraint"], bb?.["layering.layerConstraint"]].some(
72
+ (value) => value === "FIRST_SEPARATE" || value === "LAST_SEPARATE",
73
+ )
74
+ )
75
+ return 0;
76
+ const ag = Number(aa?.["considerModelOrder.groupModelOrder.crossingMinimizationId"] ?? 0),
77
+ bg = Number(bb?.["considerModelOrder.groupModelOrder.crossingMinimizationId"] ?? 0);
78
+ if (respectGroupBoundary && mode === "ONLY_WITHIN_GROUP" && ag !== bg) return 0;
79
+ if (mode === "ENFORCED" && ag !== bg && enforced.has(ag) && enforced.has(bg)) return ag - bg;
80
+ return a.index - b.index;
81
+ };
82
+ }
83
+
84
+ /** Sort initial canonical nodes and physical ports before the first crossing attempt. */
85
+ function createModelOrderComparators(
86
+ input: LayeredPhaseInput,
87
+ orientation: AcyclicOrientation,
88
+ graph: CrossingGraph,
89
+ incomingOrder?: ReadonlyMap<string, readonly string[]>,
90
+ ) {
91
+ const strategy = input.settings["considerModelOrder.strategy"] ?? "NONE";
92
+ const owner = new Map(
93
+ graph.layers.flat().flatMap((node) => node.ports.map((port) => [port.id, node] as const)),
94
+ );
95
+ const ports = new Map(
96
+ graph.layers.flat().flatMap((node) => node.ports.map((port) => [port.id, port] as const)),
97
+ );
98
+ const layerByNode = new Map(
99
+ graph.layers.flatMap((layer, i) => layer.map((node) => [node.id, i] as const)),
100
+ );
101
+ const nativeEdges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
102
+ const incoming = new Map<string, number[]>(),
103
+ outgoing = new Map<string, number[]>();
104
+ graph.edges.forEach((edge, i) => {
105
+ incoming.set(edge.target, [...(incoming.get(edge.target) ?? []), i]);
106
+ outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), i]);
107
+ });
108
+ for (const [port, indices] of incoming) {
109
+ const order = incomingOrder?.get(owner.get(port)!.id);
110
+ if (order)
111
+ indices.sort((a, b) => order.indexOf(nativeEdges[a]!.id) - order.indexOf(nativeEdges[b]!.id));
112
+ }
113
+ const edgeOrder = (i: number) =>
114
+ input.modelOrderByEdgeId?.get(nativeEdges[i]!.id) ?? input.graph.edges.indexOf(nativeEdges[i]!);
115
+ const firstIncoming = (node: CrossingNode) =>
116
+ node.ports.flatMap((port) => incoming.get(port.id) ?? [])[0];
117
+ const sourcePort = (i: number) => ports.get(graph.edges[i]!.source)!;
118
+ const targetPort = (i: number) => ports.get(graph.edges[i]!.target)!;
119
+ const isHelper = (node: CrossingNode) => node.type !== "NORMAL";
120
+ const targetNode = (port: CrossingPort): CrossingNode | undefined => {
121
+ let i = outgoing.get(port.id)?.[0];
122
+ const seen = new Set<string>();
123
+ while (i !== undefined) {
124
+ const node = owner.get(targetPort(i).id)!;
125
+ if (!isHelper(node)) return node;
126
+ if (seen.has(node.id)) return;
127
+ seen.add(node.id);
128
+ i = node.ports.flatMap((p) => outgoing.get(p.id) ?? [])[0];
129
+ }
130
+ };
131
+ const nodeById = new Map(input.graph.nodes.map((node, index) => [node.id, { node, index }]));
132
+ const nodeModel = (node: CrossingNode): number | undefined => {
133
+ const original = nodeById.get(node.id);
134
+ return !original ||
135
+ isHelper(node) ||
136
+ input.nodeSettings?.(original.node)?.["considerModelOrder.noModelOrder"] === true
137
+ ? undefined
138
+ : original.index;
139
+ };
140
+ const normalCompare = normalModelComparator(input, false);
141
+ const sideOrder = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 };
142
+ const portModel = (port: CrossingPort) => {
143
+ const node = nodeById.get(owner.get(port.id)!.id)?.node;
144
+ const names = node?.ports ?? [];
145
+ return names.findIndex((p) => JSON.stringify([node!.id, p.name]) === port.id);
146
+ };
147
+ const portCompare = (
148
+ previous: readonly CrossingNode[],
149
+ targetOrder?: ReadonlyMap<string, number>,
150
+ ) =>
151
+ transitiveComparator<CrossingPort>(
152
+ (p) => p.id,
153
+ (a, b) => {
154
+ if (a.side !== b.side) return sideOrder[a.side] - sideOrder[b.side];
155
+ let reverse = 1;
156
+ const ai = incoming.get(a.id)?.[0],
157
+ bi = incoming.get(b.id)?.[0];
158
+ const ao = outgoing.get(a.id)?.[0],
159
+ bo = outgoing.get(b.id)?.[0];
160
+ const authored = () =>
161
+ portModel(a) >= 0 && portModel(b) >= 0 ? portModel(a) - portModel(b) : 0;
162
+ if (ai !== undefined && bi !== undefined) {
163
+ if (a.side !== "EAST") reverse = -reverse;
164
+ const ap = sourcePort(ai),
165
+ bp = sourcePort(bi),
166
+ an = owner.get(ap.id)!,
167
+ bn = owner.get(bp.id)!;
168
+ if (an === bn) return reverse * (an.ports.indexOf(ap) - an.ports.indexOf(bp));
169
+ const ar = previous.indexOf(an),
170
+ br = previous.indexOf(bn);
171
+ if (ar >= 0 || br >= 0) return reverse * (ar < 0 ? 1 : br < 0 ? -1 : ar - br);
172
+ if (input.settings["considerModelOrder.portModelOrder"] === true && authored())
173
+ return reverse * authored();
174
+ }
175
+ if (ao !== undefined && bo !== undefined) {
176
+ if (a.side === "WEST" || a.side === "SOUTH") reverse = -reverse;
177
+ const an = targetNode(a),
178
+ bn = targetNode(b);
179
+ if (
180
+ strategy === "PREFER_NODES" &&
181
+ an &&
182
+ bn &&
183
+ nodeModel(an) !== undefined &&
184
+ nodeModel(bn) !== undefined
185
+ )
186
+ return reverse * (normalCompare(an.id, bn.id) || -1);
187
+ if (input.settings["considerModelOrder.portModelOrder"] === true && authored())
188
+ return reverse * authored();
189
+ let ar = edgeOrder(ao),
190
+ br = edgeOrder(bo);
191
+ if (an && an === bn) return reverse * (ar > br ? 1 : -1);
192
+ if (an && targetOrder?.has(an.id)) ar = targetOrder.get(an.id)!;
193
+ if (bn && targetOrder?.has(bn.id)) br = targetOrder.get(bn.id)!;
194
+ return reverse * (ar > br ? 1 : -1);
195
+ }
196
+ if (ai !== undefined && bo !== undefined) return 1;
197
+ if (ao !== undefined && bi !== undefined) return -1;
198
+ if (a.side === "WEST" || a.side === "SOUTH") reverse = -reverse;
199
+ return reverse * (authored() || -1);
200
+ },
201
+ true,
202
+ );
203
+ const longMode = input.settings["considerModelOrder.longEdgeStrategy"] ?? "DUMMY_NODE_OVER";
204
+ const missingEdgeOrder =
205
+ longMode === "DUMMY_NODE_OVER" ? 2147483647 : longMode === "DUMMY_NODE_UNDER" ? -2147483648 : 0;
206
+ const connectedEdgeOrder = (node: CrossingNode) => {
207
+ const i = firstIncoming(node);
208
+ return i === undefined ? missingEdgeOrder : edgeOrder(i);
209
+ };
210
+ const nodeCompare = (previous: readonly CrossingNode[], beforePorts: boolean) => {
211
+ const compare = transitiveComparator<CrossingNode>(
212
+ (node) => node.id,
213
+ (a, b): number => {
214
+ const am = nodeModel(a),
215
+ bm = nodeModel(b);
216
+ if (strategy === "PREFER_EDGES" || am === undefined || bm === undefined) {
217
+ const previousSource = (node: CrossingNode) => {
218
+ for (const p of node.ports) {
219
+ const i = incoming.get(p.id)?.[0];
220
+ if (
221
+ i !== undefined &&
222
+ layerByNode.get(owner.get(sourcePort(i).id)!.id) === layerByNode.get(node.id)! - 1
223
+ )
224
+ return sourcePort(i);
225
+ }
226
+ };
227
+ const ap = previousSource(a),
228
+ bp = previousSource(b);
229
+ if (ap && bp) {
230
+ const an = owner.get(ap.id)!,
231
+ bn = owner.get(bp.id)!;
232
+ if (an === bn && ap !== bp) return an.ports.indexOf(ap) - an.ports.indexOf(bp);
233
+ if (an === bn) return -1;
234
+ const ar = previous.indexOf(an),
235
+ br = previous.indexOf(bn);
236
+ if (ar >= 0 || br >= 0) return ar < 0 ? 1 : br < 0 ? -1 : ar - br;
237
+ }
238
+ const endpoints = (node: CrossingNode) => {
239
+ const source = firstIncoming(node),
240
+ next = node.ports.flatMap((p) => outgoing.get(p.id) ?? [])[0];
241
+ const sourcePhysical = source === undefined ? undefined : sourcePort(source),
242
+ targetPhysical = next === undefined ? undefined : targetPort(next);
243
+ return {
244
+ source: sourcePhysical && owner.get(sourcePhysical.id),
245
+ target: targetPhysical && owner.get(targetPhysical.id),
246
+ sourcePort: sourcePhysical,
247
+ targetPort: targetPhysical,
248
+ };
249
+ };
250
+ const helperCompare = (): number => {
251
+ if (a.type === "LONG_EDGE" && b.type === "NORMAL") {
252
+ const { source, target } = endpoints(a);
253
+ const layer = layerByNode.get(a.id);
254
+ if (
255
+ layerByNode.get(source?.id ?? "") !== layer &&
256
+ layerByNode.get(target?.id ?? "") !== layer
257
+ )
258
+ return 0;
259
+ if (source === b || target === b) return 1;
260
+ return source ? compare(source, b) : 0;
261
+ }
262
+ if (a.type === "NORMAL" && b.type === "LONG_EDGE") {
263
+ const { source, target } = endpoints(b);
264
+ const layer = layerByNode.get(a.id);
265
+ if (
266
+ layerByNode.get(source?.id ?? "") !== layer &&
267
+ layerByNode.get(target?.id ?? "") !== layer
268
+ )
269
+ return 0;
270
+ if (source === a || target === a) return -1;
271
+ return source ? compare(a, source) : 0;
272
+ }
273
+ if (a.type !== "LONG_EDGE" || b.type !== "LONG_EDGE") return 0;
274
+ const ae = endpoints(a),
275
+ be = endpoints(b);
276
+ const as = ae.source && layerByNode.get(ae.source.id) === layerByNode.get(a.id),
277
+ at = !as && ae.target && layerByNode.get(ae.target.id) === layerByNode.get(a.id),
278
+ bs = be.source && layerByNode.get(be.source.id) === layerByNode.get(b.id),
279
+ bt = !bs && be.target && layerByNode.get(be.target.id) === layerByNode.get(b.id);
280
+ const ar = as ? ae.source! : at ? ae.target! : a,
281
+ br = bs ? be.source! : bt ? be.target! : b;
282
+ if (ar === a && br === b) return 0;
283
+ if (ar === br) {
284
+ if (beforePorts) {
285
+ if (as && bs && ae.sourcePort && be.sourcePort)
286
+ return portCompare(previous)(ae.sourcePort, be.sourcePort) > 0 ? 1 : -1;
287
+ if (as && bt) return 1;
288
+ if (at && bs) return -1;
289
+ if (at && bt) return 0;
290
+ } else {
291
+ for (const port of ar.ports) {
292
+ if (port === ae.sourcePort) return -1;
293
+ if (port === be.sourcePort) return 1;
294
+ }
295
+ }
296
+ }
297
+ return ar === br ? 0 : compare(ar, br);
298
+ };
299
+ if (!ap || !bp) {
300
+ const helperResult = helperCompare();
301
+ if (helperResult !== 0) return helperResult;
302
+ }
303
+ if (!!ap !== !!bp && (am === undefined || bm === undefined))
304
+ return connectedEdgeOrder(a) > connectedEdgeOrder(b) ? 1 : -1;
305
+ }
306
+ if (am !== undefined && bm !== undefined) return normalCompare(a.id, b.id) || -1;
307
+ return -1;
308
+ },
309
+ );
310
+ return compare;
311
+ };
312
+ const sort = () => {
313
+ for (let layer = 0; layer < graph.layers.length; layer++) {
314
+ const current = graph.layers[layer]!,
315
+ previous = [...graph.layers[Math.max(0, layer - 1)]!];
316
+ insertionSort(current, nodeCompare(previous, true));
317
+ for (const node of current) {
318
+ const constraint = input.nodeSettings?.(nodeById.get(node.id)!.node)?.portConstraints;
319
+ if (
320
+ constraint === "FIXED_ORDER" ||
321
+ constraint === "FIXED_POS" ||
322
+ constraint === "FIXED_RATIO"
323
+ )
324
+ continue;
325
+ const targetOrder = new Map<string, number>();
326
+ for (const p of node.ports) {
327
+ const i = outgoing.get(p.id)?.[0],
328
+ target = targetNode(p);
329
+ if (i === undefined || !target || orientation.reversedEdgeIds.has(nativeEdges[i]!.id))
330
+ continue;
331
+ targetOrder.set(
332
+ target.id,
333
+ Math.min(targetOrder.get(target.id) ?? Number.MAX_SAFE_INTEGER, edgeOrder(i)),
334
+ );
335
+ }
336
+ gwtSort(node.ports, portCompare(previous, targetOrder));
337
+ }
338
+ insertionSort(current, nodeCompare(previous, false));
339
+ }
340
+ };
341
+ const countChanges = () => {
342
+ let nodeChanges = 0,
343
+ portChanges = 0;
344
+ for (let layer = 0; layer < graph.layers.length; layer++) {
345
+ const current = graph.layers[layer]!,
346
+ previous = graph.layers[Math.max(0, layer - 1)]!;
347
+ const compare = nodeCompare(previous, false);
348
+ for (let i = 0; i < current.length; i++)
349
+ for (let j = i + 1; j < current.length; j++) {
350
+ if (
351
+ nodeModel(current[i]!) !== undefined &&
352
+ nodeModel(current[j]!) !== undefined &&
353
+ compare(current[i]!, current[j]!) > 0
354
+ )
355
+ nodeChanges++;
356
+ }
357
+ for (const node of current) {
358
+ const comparePort = portCompare(previous);
359
+ for (let i = 0; i < node.ports.length; i++)
360
+ for (let j = i + 1; j < node.ports.length; j++)
361
+ if (comparePort(node.ports[i]!, node.ports[j]!) > 0) portChanges++;
362
+ }
363
+ }
364
+ return { nodeChanges, portChanges };
365
+ };
366
+ return { sort, countChanges };
367
+ }
368
+
369
+ export function sortInitialModelOrder(
370
+ input: LayeredPhaseInput,
371
+ orientation: AcyclicOrientation,
372
+ graph: CrossingGraph,
373
+ incomingOrder?: ReadonlyMap<string, readonly string[]>,
374
+ ): void {
375
+ createModelOrderComparators(input, orientation, graph, incomingOrder).sort();
376
+ }
377
+
378
+ export function countModelOrderChanges(
379
+ input: LayeredPhaseInput,
380
+ orientation: AcyclicOrientation,
381
+ graph: CrossingGraph,
382
+ ): { nodeChanges: number; portChanges: number } {
383
+ return createModelOrderComparators(input, orientation, graph).countChanges();
384
+ }
385
+
386
+ const preparedInputs = new WeakMap<LayeredPhaseInput, ReadonlyMap<string, readonly string[]>>();
387
+ export const markInitialModelOrderPrepared = (
388
+ input: LayeredPhaseInput,
389
+ ports: ReadonlyMap<string, readonly string[]>,
390
+ ) => {
391
+ preparedInputs.set(input, ports);
392
+ };
393
+ export const restoreInitialModelOrder = (
394
+ input: LayeredPhaseInput,
395
+ graph: CrossingGraph,
396
+ ): boolean => {
397
+ const prepared = preparedInputs.get(input);
398
+ if (!prepared) return false;
399
+ for (const node of graph.layers.flat()) {
400
+ const order = prepared.get(node.id);
401
+ if (order) node.ports.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
402
+ }
403
+ return true;
404
+ };
@@ -0,0 +1,207 @@
1
+ /*
2
+ * Copyright (c) 2011, 2019 Kiel University and others.
3
+ * Native adaptation of ELK InvertedPortProcessor (v0.11.0).
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ */
6
+ import type { GraphNode, GraphEdge } from "@statelyai/graph";
7
+ import type { LongEdgeExpansion } from "./long-edges";
8
+ import { inheritCycleRandom } from "./cycle-random";
9
+ import type { PortEdgeList } from "./elk-port-lists";
10
+
11
+ /** Insert same-layer long-edge dummies before crossing minimization. */
12
+ export function insertInvertedPortDummies(
13
+ expansion: LongEdgeExpansion,
14
+ portLists?: ReadonlyMap<string, readonly PortEdgeList[]>,
15
+ ): LongEdgeExpansion {
16
+ const { input } = expansion;
17
+ const forward =
18
+ input.direction === "right"
19
+ ? "EAST"
20
+ : input.direction === "left"
21
+ ? "WEST"
22
+ : input.direction === "down"
23
+ ? "SOUTH"
24
+ : "NORTH";
25
+ const backward = ({ EAST: "WEST", WEST: "EAST", NORTH: "SOUTH", SOUTH: "NORTH" } as const)[
26
+ forward
27
+ ];
28
+ const nodes: GraphNode[] = [...input.graph.nodes];
29
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
30
+ const edges: GraphEdge[] = [];
31
+ const sizes = new Map(input.sizes);
32
+ const layers = new Map(expansion.assignment.layerByNodeId);
33
+ const reversed = new Set<string>();
34
+ const replacements = new Map<string, string[]>();
35
+ const originals = new Map<string, GraphEdge>();
36
+ const dummyIds = new Set<string>();
37
+ const creationOrder = new Map<string, number[]>();
38
+ const ownerOrder = new Map(
39
+ (expansion.assignment.seedOrder ?? input.graph.nodes.map((n) => n.id)).map((id, i) => [id, i]),
40
+ );
41
+ const usedEdges = new Set(input.graph.edges.map((e) => e.id));
42
+ const fixedSide = (
43
+ node: GraphNode | undefined,
44
+ portName: string | undefined,
45
+ source: boolean,
46
+ ) => {
47
+ if (!node || node.id.startsWith("__layout_dummy:")) return undefined;
48
+ const constraints = String(input.nodeSettings?.(node)?.portConstraints ?? "UNDEFINED");
49
+ if (constraints === "UNDEFINED" || constraints === "FREE") return undefined;
50
+ const port = node.ports?.find((p) => p.name === portName);
51
+ if (!port) return source ? forward : backward;
52
+ return input.portSettings?.(port, node)?.["port.side"];
53
+ };
54
+ const dummy = (edge: GraphEdge, at: string, end: string) => {
55
+ let id = `__layout_dummy:inverted:${edge.id}:${end}`;
56
+ while (nodeById.has(id)) id += ":";
57
+ const node: GraphNode = {
58
+ type: "node",
59
+ id,
60
+ data: undefined,
61
+ width: 0,
62
+ height: 0,
63
+ ports: [
64
+ { name: "input", direction: "in", x: 0, y: 0, width: 0, height: 0, data: undefined },
65
+ { name: "output", direction: "out", x: 0, y: 0, width: 0, height: 0, data: undefined },
66
+ ],
67
+ };
68
+ nodes.push(node);
69
+ nodeById.set(id, node);
70
+ dummyIds.add(id);
71
+ const reverse = expansion.orientation.reversedEdgeIds.has(edge.id);
72
+ const incoming = (end === "target") !== reverse;
73
+ const owner = nodeById.get(at)!;
74
+ const name = end === "source" ? edge.sourcePort : edge.targetPort;
75
+ // ELK visits the owner's ports in list order and each port's edges in the
76
+ // order reversal history left them.
77
+ const listed = edge.id.replace(/::segment:\d+$/, "");
78
+ const lists = portLists?.get(at);
79
+ const port = lists?.findIndex((p) => (incoming ? p.incoming : p.outgoing).includes(listed));
80
+ creationOrder.set(id, [
81
+ layers.get(at) ?? 0,
82
+ ownerOrder.get(at) ?? 0,
83
+ incoming ? 0 : 1,
84
+ ...(lists && port !== undefined && port >= 0
85
+ ? [port, 0, (incoming ? lists[port]!.incoming : lists[port]!.outgoing).indexOf(listed)]
86
+ : [
87
+ owner.ports?.findIndex((p) => p.name === name) ?? -1,
88
+ Number(reverse),
89
+ input.graph.edges.indexOf(edge),
90
+ ]),
91
+ ]);
92
+ sizes.set(id, { width: 0, height: 0 });
93
+ layers.set(id, layers.get(at) ?? 0);
94
+ return id;
95
+ };
96
+ for (const edge of input.graph.edges) {
97
+ const rev = expansion.orientation.reversedEdgeIds.has(edge.id);
98
+ const source = nodeById.get(edge.sourceId),
99
+ target = nodeById.get(edge.targetId);
100
+ const sourceSide = fixedSide(source, edge.sourcePort, true),
101
+ targetSide = fixedSide(target, edge.targetPort, false);
102
+ const invertSource =
103
+ edge.sourceId !== edge.targetId && sourceSide === (rev ? forward : backward);
104
+ const invertTarget =
105
+ edge.sourceId !== edge.targetId && targetSide === (rev ? backward : forward);
106
+ if (!invertSource && !invertTarget) {
107
+ edges.push(edge);
108
+ if (rev) reversed.add(edge.id);
109
+ replacements.set(edge.id, [edge.id]);
110
+ continue;
111
+ }
112
+ const chain = [edge.sourceId];
113
+ if (invertSource) chain.push(dummy(edge, edge.sourceId, "source"));
114
+ if (invertTarget) chain.push(dummy(edge, edge.targetId, "target"));
115
+ chain.push(edge.targetId);
116
+ const segments: string[] = [];
117
+ for (let i = 0; i < chain.length - 1; i++) {
118
+ let id = `${edge.id}::inverted:${i}`;
119
+ while (usedEdges.has(id)) id += ":";
120
+ usedEdges.add(id);
121
+ segments.push(id);
122
+ originals.set(id, edge);
123
+ edges.push({
124
+ ...edge,
125
+ id,
126
+ sourceId: chain[i]!,
127
+ targetId: chain[i + 1]!,
128
+ sourcePort: i === 0 ? edge.sourcePort : rev ? "input" : "output",
129
+ targetPort: i === chain.length - 2 ? edge.targetPort : rev ? "output" : "input",
130
+ // Existing center-label metadata stays on the inter-layer segment.
131
+ ...(i === (invertSource ? 1 : 0) ? {} : { width: 0, height: 0 }),
132
+ points: undefined,
133
+ });
134
+ if (rev) reversed.add(id);
135
+ }
136
+ replacements.set(edge.id, segments);
137
+ }
138
+ const incomingEdgeOrderByNodeId = new Map(
139
+ [...(expansion.incomingEdgeOrderByNodeId ?? [])].map(([id, order]) => [id, [...order]]),
140
+ );
141
+ // A source-side inversion retargets the original segment and appends a new
142
+ // continuation to its target port. Interior chain targets are temporary;
143
+ // only terminal segments change an authored node's incoming adjacency.
144
+ for (const node of [...input.graph.nodes].sort(
145
+ (a, b) => (layers.get(a.id) ?? 0) - (layers.get(b.id) ?? 0),
146
+ )) {
147
+ for (const edge of input.graph.edges) {
148
+ const rev = expansion.orientation.reversedEdgeIds.has(edge.id);
149
+ const source = rev ? edge.targetId : edge.sourceId;
150
+ const target = rev ? edge.sourceId : edge.targetId;
151
+ const sourcePort = rev ? edge.targetPort : edge.sourcePort;
152
+ if (source !== node.id || fixedSide(node, sourcePort, true) !== backward) continue;
153
+ const order = incomingEdgeOrderByNodeId.get(target);
154
+ if (!order || (replacements.get(edge.id)?.length ?? 0) < 2) continue;
155
+ const original = [...expansion.segmentIdsByEdgeId].find(([, ids]) =>
156
+ ids.includes(edge.id),
157
+ )?.[0];
158
+ if (original && order.includes(original))
159
+ incomingEdgeOrderByNodeId.set(target, [...order.filter((id) => id !== original), original]);
160
+ }
161
+ }
162
+ if (!dummyIds.size) return expansion;
163
+ // InvertedPortProcessor visits owners and their ports, rather than the
164
+ // root edge list. Cycle-reversed incident edges are appended afterward.
165
+ const orderedDummies = nodes
166
+ .filter((n) => dummyIds.has(n.id))
167
+ .sort((a, b) => {
168
+ const aa = creationOrder.get(a.id)!,
169
+ bb = creationOrder.get(b.id)!;
170
+ for (let i = 0; i < aa.length; i++) if (aa[i] !== bb[i]) return aa[i]! - bb[i]!;
171
+ return 0;
172
+ });
173
+ nodes.splice(0, nodes.length, ...input.graph.nodes, ...orderedDummies);
174
+ return {
175
+ ...expansion,
176
+ incomingEdgeOrderByNodeId,
177
+ input: inheritCycleRandom(input, {
178
+ ...input,
179
+ graph: { ...input.graph, nodes, edges },
180
+ sizes,
181
+ modelOrderByEdgeId: new Map(
182
+ edges.map((edge) => {
183
+ const original = originals.get(edge.id) ?? edge;
184
+ return [
185
+ edge.id,
186
+ input.modelOrderByEdgeId?.get(original.id) ?? input.graph.edges.indexOf(original),
187
+ ];
188
+ }),
189
+ ),
190
+ nodeSettings: (n) =>
191
+ dummyIds.has(n.id) ? { portConstraints: "FIXED_POS" } : input.nodeSettings?.(n),
192
+ portSettings: (p, n) =>
193
+ dummyIds.has(n.id)
194
+ ? { "port.side": p.name === "input" ? backward : forward }
195
+ : input.portSettings?.(p, n),
196
+ edgeSettings: (e) => input.edgeSettings?.(originals.get(e.id) ?? e),
197
+ }),
198
+ orientation: { reversedEdgeIds: reversed },
199
+ assignment: { ...expansion.assignment, layerByNodeId: layers },
200
+ segmentIdsByEdgeId: new Map(
201
+ [...expansion.segmentIdsByEdgeId].map(([id, segs]) => [
202
+ id,
203
+ segs.flatMap((s) => replacements.get(s) ?? [s]),
204
+ ]),
205
+ ),
206
+ };
207
+ }