@statelyai/layout 0.3.0 → 0.4.1

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 (110) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +150 -11
  3. package/dist/elkjs/index.cjs +2 -2
  4. package/dist/elkjs/index.d.cts +2 -0
  5. package/dist/elkjs/index.d.mts +2 -0
  6. package/dist/elkjs/index.mjs +2 -2
  7. package/dist/elkjs/main.cjs +1 -1
  8. package/dist/elkjs/main.mjs +1 -1
  9. package/dist/elkjs/worker.cjs +2 -2
  10. package/dist/elkjs/worker.mjs +2 -2
  11. package/dist/elkjs-BBfKN4Dw.mjs +3075 -0
  12. package/dist/elkjs-DNfXDPup.cjs +3104 -0
  13. package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
  14. package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
  15. package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
  16. package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
  17. package/dist/index.cjs +6 -3
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.mts +3 -3
  20. package/dist/index.mjs +4 -4
  21. package/dist/layered/index.cjs +3 -1
  22. package/dist/layered/index.d.cts +2 -2
  23. package/dist/layered/index.d.mts +2 -2
  24. package/dist/layered/index.mjs +2 -2
  25. package/dist/{layered-B2buJqMf.cjs → layered-5RxQhryb.cjs} +11184 -2932
  26. package/dist/{layered-DGFTeQSl.mjs → layered-DSesbctm.mjs} +12059 -3903
  27. package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-CLFySz3R.cjs} +5 -18
  28. package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-GpBIX6ng.mjs} +3 -10
  29. package/dist/routing/index.cjs +2 -1
  30. package/dist/routing/index.d.cts +2 -2
  31. package/dist/routing/index.d.mts +2 -2
  32. package/dist/routing/index.mjs +2 -2
  33. package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
  34. package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
  35. package/package.json +8 -2
  36. package/src/authoring/coordinates.ts +28 -8
  37. package/src/authoring/partial.ts +7 -2
  38. package/src/authoring/routing.ts +4 -1
  39. package/src/constraints.ts +174 -6
  40. package/src/elkjs/index.ts +1476 -449
  41. package/src/elkjs/layout-quality.ts +267 -0
  42. package/src/elkjs/opposing-tracks.ts +161 -0
  43. package/src/elkjs/orthogonal-junctions.ts +174 -0
  44. package/src/elkjs/statechart.ts +9 -3
  45. package/src/elkjs/types.ts +2 -0
  46. package/src/index.ts +2 -0
  47. package/src/internal/elkjs-compatibility.ts +4 -1
  48. package/src/internal/layered-trace.ts +48 -0
  49. package/src/internal/layout-engine.ts +2 -1
  50. package/src/java-random.ts +6 -0
  51. package/src/layered/bk-node-placement.ts +403 -92
  52. package/src/layered/center-labels.ts +398 -0
  53. package/src/layered/compaction-bounds.ts +26 -0
  54. package/src/layered/compaction-errors.ts +2 -0
  55. package/src/layered/compaction-scanline.ts +63 -0
  56. package/src/layered/compound-boundaries.ts +99 -0
  57. package/src/layered/compound.ts +35 -7
  58. package/src/layered/constraint-orientation.ts +141 -0
  59. package/src/layered/crossing-constraints.ts +243 -0
  60. package/src/layered/crossing-counter.ts +233 -0
  61. package/src/layered/crossing-graph.ts +401 -0
  62. package/src/layered/cycle-random.ts +22 -0
  63. package/src/layered/elk-port-lists.ts +243 -0
  64. package/src/layered/end-labels.ts +124 -0
  65. package/src/layered/external-port-dummy.ts +137 -0
  66. package/src/layered/feedback-chains.ts +114 -0
  67. package/src/layered/fixed-self-loop.ts +150 -0
  68. package/src/layered/greedy-switch-decider.ts +356 -0
  69. package/src/layered/grouped-compaction.ts +787 -0
  70. package/src/layered/gwt-sort.ts +53 -0
  71. package/src/layered/hierarchical-port-phases.ts +242 -0
  72. package/src/layered/hierarchical-port-restoration.ts +233 -0
  73. package/src/layered/hierarchical-port-routing.ts +179 -0
  74. package/src/layered/hierarchy-crossing.ts +121 -0
  75. package/src/layered/hierarchy-sweepiness.ts +79 -0
  76. package/src/layered/hints.ts +371 -0
  77. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  78. package/src/layered/index.ts +875 -161
  79. package/src/layered/initial-model-order.ts +404 -0
  80. package/src/layered/inverted-ports.ts +207 -0
  81. package/src/layered/label-dummy-switcher.ts +200 -0
  82. package/src/layered/long-edges.ts +135 -44
  83. package/src/layered/loop-envelopes.ts +184 -0
  84. package/src/layered/native-routing.ts +275 -0
  85. package/src/layered/network-simplex.ts +11 -15
  86. package/src/layered/node-margins.ts +59 -0
  87. package/src/layered/north-south-ports.ts +271 -0
  88. package/src/layered/opposing-tracks.ts +1044 -0
  89. package/src/layered/orthogonal-cycle-order.ts +106 -0
  90. package/src/layered/orthogonal-hypersegments.ts +154 -0
  91. package/src/layered/orthogonal-junctions.ts +202 -0
  92. package/src/layered/orthogonal-segments.ts +243 -0
  93. package/src/layered/placement-orientation.ts +22 -0
  94. package/src/layered/port-distributor.ts +192 -0
  95. package/src/layered/port-edge-order.ts +20 -0
  96. package/src/layered/replace-routing.ts +45 -0
  97. package/src/layered/routing-coordinates.ts +32 -0
  98. package/src/layered/separate-exterior-labels.ts +24 -6
  99. package/src/layered/spacing.ts +34 -12
  100. package/src/layered/splitter-order.ts +106 -0
  101. package/src/layered/strategies.ts +2124 -886
  102. package/src/layered/types.ts +13 -0
  103. package/src/layered/weighted-compaction.ts +84 -0
  104. package/src/routing/adapters.ts +32 -0
  105. package/src/routing/algorithms.ts +433 -21
  106. package/src/routing/coordination.ts +8 -3
  107. package/src/routing/index.ts +1 -0
  108. package/src/routing/search.ts +93 -11
  109. package/dist/elkjs-DaHMISnQ.mjs +0 -1677
  110. package/dist/elkjs-dMghiiWW.cjs +0 -1706
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Copyright (c) 2020 Kiel University and others.
3
+ * Native adaptation of the sorts elkjs (ELK compiled by GWT) runs.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ */
6
+
7
+ /**
8
+ * GWT's `Collections.sort` / `List.sort` / `Arrays.sort(Object[])` (elkjs): top-down
9
+ * merge sort over insertion-sorted runs shorter than 7. For a consistent comparator
10
+ * this equals any stable sort; stateful or non-antisymmetric comparators (ELK's
11
+ * model-order comparators) depend on this exact call sequence.
12
+ */
13
+ export function gwtSort<T>(items: T[], compare: (left: T, right: T) => number): void {
14
+ const sort = (temp: T[], array: T[], low: number, high: number, ofs: number): void => {
15
+ if (high - low < 7) {
16
+ for (let i = low + 1; i < high; i++)
17
+ for (let j = i; j > low && compare(array[j - 1]!, array[j]!) > 0; j--)
18
+ [array[j - 1], array[j]] = [array[j]!, array[j - 1]!];
19
+ return;
20
+ }
21
+ const tempLow = low + ofs,
22
+ tempHigh = high + ofs,
23
+ tempMid = tempLow + ((tempHigh - tempLow) >> 1);
24
+ sort(array, temp, tempLow, tempMid, -ofs);
25
+ sort(array, temp, tempMid, tempHigh, -ofs);
26
+ if (compare(temp[tempMid - 1]!, temp[tempMid]!) <= 0) {
27
+ for (let i = tempLow; low < high;) array[low++] = temp[i++]!;
28
+ return;
29
+ }
30
+ for (let left = tempLow, right = tempMid; low < high;)
31
+ array[low++] =
32
+ right >= tempHigh || (left < tempMid && compare(temp[left]!, temp[right]!) <= 0)
33
+ ? temp[left++]!
34
+ : temp[right++]!;
35
+ };
36
+ sort(items.slice(), items, 0, items.length, 0);
37
+ }
38
+
39
+ /**
40
+ * ELK's own insertion sort (SortByInputModelProcessor / ModelOrderBarycenterHeuristic),
41
+ * which preserves comparator decisions even when model and edge priorities conflict.
42
+ */
43
+ export function insertionSort<T>(items: T[], compare: (left: T, right: T) => number): void {
44
+ for (let i = 1; i < items.length; i++) {
45
+ const value = items[i]!;
46
+ let j = i;
47
+ while (j > 0 && compare(items[j - 1]!, value) > 0) {
48
+ items[j] = items[j - 1]!;
49
+ j--;
50
+ }
51
+ items[j] = value;
52
+ }
53
+ }
@@ -0,0 +1,242 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2010, 2020 Kiel University and others.
3
+ * Adapted from ELK HierarchicalPortConstraintProcessor and
4
+ * HierarchicalPortDummySizeProcessor in elkjs 0.11.1.
5
+ * SPDX-License-Identifier: EPL-2.0
6
+ *******************************************************************************/
7
+ import { inheritCycleRandom } from "./cycle-random";
8
+ import type { GraphEdge, GraphNode } from "@statelyai/graph";
9
+ import { attachExternalPortDummy, externalPortDummyOf } from "./external-port-dummy";
10
+ import type { AcyclicOrientation, LayerAssignment, LayeredPhaseInput, LayerOrder } from "./types";
11
+
12
+ export interface HierarchicalPortReplacement {
13
+ original: GraphNode;
14
+ helper: GraphNode;
15
+ side: "NORTH" | "SOUTH";
16
+ }
17
+ export interface HierarchicalPortPreparation {
18
+ input: LayeredPhaseInput;
19
+ assignment: LayerAssignment & { layerCount?: number };
20
+ replacements: ReadonlyMap<string, HierarchicalPortReplacement>;
21
+ originals: ReadonlyMap<string, GraphNode>;
22
+ }
23
+ const sides = {
24
+ right: { before: "WEST", after: "EAST", north: "NORTH", south: "SOUTH" },
25
+ left: { before: "EAST", after: "WEST", north: "NORTH", south: "SOUTH" },
26
+ down: { before: "NORTH", after: "SOUTH", north: "WEST", south: "EAST" },
27
+ up: { before: "SOUTH", after: "NORTH", north: "WEST", south: "EAST" },
28
+ } as const;
29
+
30
+ export function hierarchicalPortSides(input: Pick<LayeredPhaseInput, "direction" | "settings">) {
31
+ return input.direction === "left" && input.settings.directionCongruency === "ROTATION"
32
+ ? { ...sides.left, north: "SOUTH" as const, south: "NORTH" as const }
33
+ : input.direction === "down" && input.settings.directionCongruency === "ROTATION"
34
+ ? { ...sides.down, north: "EAST" as const, south: "WEST" as const }
35
+ : sides[input.direction];
36
+ }
37
+
38
+ /** Introduce one flow-axis helper for each original port and adjacent layer. */
39
+ export function prepareHierarchicalPortConstraints(
40
+ input: LayeredPhaseInput,
41
+ orientation: AcyclicOrientation,
42
+ assignment: LayerAssignment & { layerCount?: number },
43
+ ): HierarchicalPortPreparation {
44
+ const replacements = new Map<string, HierarchicalPortReplacement>();
45
+ const originals = new Map<string, GraphNode>();
46
+ const physical = hierarchicalPortSides(input);
47
+ if (
48
+ !["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(
49
+ String(input.settings.portConstraints),
50
+ )
51
+ )
52
+ return { input, assignment, replacements, originals };
53
+ for (const node of input.graph.nodes) {
54
+ const origin = externalPortDummyOf(node);
55
+ if (origin?.side === physical.north || origin?.side === physical.south)
56
+ originals.set(node.id, node);
57
+ }
58
+ if (!originals.size) return { input, assignment, replacements, originals };
59
+ const byOriginalId = new Map(originals);
60
+ originals.clear();
61
+ for (const id of assignment.seedOrder ?? input.graph.nodes.map((node) => node.id)) {
62
+ const node = byOriginalId.get(id);
63
+ if (node) originals.set(id, node);
64
+ }
65
+
66
+ const count = assignment.layerCount ?? Math.max(-1, ...assignment.layerByNodeId.values()) + 1;
67
+ const layers: string[][] = Array.from({ length: count }, () => []);
68
+ const seed = assignment.seedOrder ?? input.graph.nodes.map((node) => node.id);
69
+ for (const id of seed)
70
+ if (!originals.has(id)) layers[assignment.layerByNodeId.get(id) ?? 0]!.push(id);
71
+ const nodes = new Map(input.graph.nodes.map((node) => [node.id, node]));
72
+ const edges = input.graph.edges.map((edge) => ({ ...edge }));
73
+ const additions = new Map<number, string[]>();
74
+ const usedIds = new Set(nodes.keys());
75
+ const helperFor = (original: GraphNode, layer: number): GraphNode => {
76
+ const key = `${original.id}:flow:${layer}`;
77
+ let record = replacements.get(key);
78
+ if (record) return record.helper;
79
+ let id = key;
80
+ while (usedIds.has(id)) id += ":helper";
81
+ usedIds.add(id);
82
+ const origin = externalPortDummyOf(original)!;
83
+ const helper = attachExternalPortDummy(
84
+ {
85
+ ...original,
86
+ id,
87
+ width: 0,
88
+ height: 0,
89
+ ports: [
90
+ {
91
+ name: `${id}:in`,
92
+ direction: "in" as const,
93
+ data: undefined,
94
+ x: 0,
95
+ y: 0,
96
+ width: 0,
97
+ height: 0,
98
+ },
99
+ {
100
+ name: `${id}:out`,
101
+ direction: "out" as const,
102
+ data: undefined,
103
+ x: 0,
104
+ y: 0,
105
+ width: 0,
106
+ height: 0,
107
+ },
108
+ ],
109
+ },
110
+ origin,
111
+ );
112
+ record = { original, helper, side: origin.side === physical.north ? "NORTH" : "SOUTH" };
113
+ replacements.set(key, record);
114
+ additions.set(layer, [...(additions.get(layer) ?? []), id]);
115
+ return helper;
116
+ };
117
+ const source = (edge: GraphEdge) =>
118
+ orientation.reversedEdgeIds.has(edge.id) ? edge.targetId : edge.sourceId;
119
+ const target = (edge: GraphEdge) =>
120
+ orientation.reversedEdgeIds.has(edge.id) ? edge.sourceId : edge.targetId;
121
+ for (let layer = 0; layer < layers.length; layer++)
122
+ for (const id of layers[layer]!) {
123
+ for (const edge of edges.filter((edge) => target(edge) === id)) {
124
+ const original = originals.get(source(edge));
125
+ if (!original) continue;
126
+ const helper = helperFor(original, layer - 1);
127
+ if (orientation.reversedEdgeIds.has(edge.id)) {
128
+ edge.targetId = helper.id;
129
+ edge.targetPort = helper.ports![1]!.name;
130
+ } else {
131
+ edge.sourceId = helper.id;
132
+ edge.sourcePort = helper.ports![1]!.name;
133
+ }
134
+ }
135
+ for (const edge of edges.filter((edge) => source(edge) === id)) {
136
+ const original = originals.get(target(edge));
137
+ if (!original) continue;
138
+ const helper = helperFor(original, layer + 1);
139
+ if (orientation.reversedEdgeIds.has(edge.id)) {
140
+ edge.sourceId = helper.id;
141
+ edge.sourcePort = helper.ports![0]!.name;
142
+ } else {
143
+ edge.targetId = helper.id;
144
+ edge.targetPort = helper.ports![0]!.name;
145
+ }
146
+ }
147
+ }
148
+ // The worker inserts the leading layer while iterating the original
149
+ // replacement buckets. Later bucket indices address that mutated layer list.
150
+ const preparedLayers = layers.map((layer) => [...layer]);
151
+ for (let index = -1; index <= count; index++) {
152
+ const added = additions.get(index);
153
+ if (!added?.length) continue;
154
+ if (index === -1) preparedLayers.unshift([...added]);
155
+ else if (index === count) preparedLayers.push([...added]);
156
+ else preparedLayers[index]!.push(...added);
157
+ }
158
+ const layerByNodeId = new Map(
159
+ preparedLayers.flatMap((layer, i) => layer.map((id) => [id, i] as const)),
160
+ );
161
+ const helpers = [...replacements.values()].map((record) => record.helper);
162
+ const helperIds = new Set(helpers.map((node) => node.id));
163
+ const sizes = new Map(input.sizes);
164
+ for (const id of originals.keys()) sizes.delete(id);
165
+ for (const helper of helpers) sizes.set(helper.id, { width: 0, height: 0 });
166
+ const prepared: LayeredPhaseInput = inheritCycleRandom(input, {
167
+ ...input,
168
+ sizes,
169
+ graph: {
170
+ ...input.graph,
171
+ nodes: [...input.graph.nodes.filter((node) => !originals.has(node.id)), ...helpers],
172
+ edges,
173
+ },
174
+ nodeSettings: (node) =>
175
+ helperIds.has(node.id)
176
+ ? { ...input.nodeSettings?.(node), portConstraints: "FIXED_POS", alignment: "CENTER" }
177
+ : input.nodeSettings?.(node),
178
+ portSettings: (port, node) =>
179
+ helperIds.has(node.id)
180
+ ? { "port.side": port.direction === "out" ? physical.after : physical.before }
181
+ : input.portSettings?.(port, node),
182
+ });
183
+ return {
184
+ input: prepared,
185
+ assignment: {
186
+ ...assignment,
187
+ layerCount: preparedLayers.length,
188
+ layerByNodeId,
189
+ seedOrder: preparedLayers.flat(),
190
+ },
191
+ replacements,
192
+ originals,
193
+ };
194
+ }
195
+
196
+ /** ELK assigns successive double-spacing lane widths after crossing order is known. */
197
+ export function sizeHierarchicalPortDummies(
198
+ input: LayeredPhaseInput,
199
+ order: LayerOrder,
200
+ preparation: HierarchicalPortPreparation,
201
+ ): LayeredPhaseInput {
202
+ if (!preparation.replacements.size) return input;
203
+ const byId = new Map(
204
+ [...preparation.replacements.values()].map((record) => [record.helper.id, record]),
205
+ );
206
+ const horizontal = input.direction === "right" || input.direction === "left";
207
+ const delta = 2 * Number(input.settings["spacing.edgeEdgeBetweenLayers"] ?? 10);
208
+ const sizes = new Map(input.sizes);
209
+ const resized = new Map<string, GraphNode>();
210
+ for (const layer of order.layers)
211
+ for (const side of ["NORTH", "SOUTH"] as const) {
212
+ const ids = layer.filter((id) => byId.get(id)?.side === side);
213
+ for (const [index, id] of ids.entries()) {
214
+ const width = delta * (side === "NORTH" ? index : ids.length - 1 - index);
215
+ const node = input.graph.nodes.find((node) => node.id === id)!;
216
+ const size = horizontal ? { width, height: 0 } : { width: 0, height: width };
217
+ sizes.set(id, size);
218
+ resized.set(id, {
219
+ ...node,
220
+ ...size,
221
+ ports: node.ports?.map((port) => ({
222
+ ...port,
223
+ x: horizontal
224
+ ? input.portSettings?.(port, node)?.["port.side"] === "EAST"
225
+ ? width
226
+ : 0
227
+ : 0,
228
+ y: !horizontal
229
+ ? input.portSettings?.(port, node)?.["port.side"] === "SOUTH"
230
+ ? width
231
+ : 0
232
+ : 0,
233
+ })),
234
+ });
235
+ }
236
+ }
237
+ return inheritCycleRandom(input, {
238
+ ...input,
239
+ sizes,
240
+ graph: { ...input.graph, nodes: input.graph.nodes.map((node) => resized.get(node.id) ?? node) },
241
+ });
242
+ }
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) 2010, 2020 Kiel University and others.
3
+ * Native adaptation of ELK boundary restoration and direction transforms.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ */
6
+ import type { EntityRect, Point } from "@statelyai/graph";
7
+
8
+ import { phaseRandomByInput, crossingRandom, inheritCycleRandom } from "./cycle-random";
9
+ import { placementCrossBounds, compactionBounds } from "./compaction-bounds";
10
+ import { externalPortDummyOf, type ExternalPortDummy } from "./external-port-dummy";
11
+ import {
12
+ hierarchicalPortSides,
13
+ type HierarchicalPortPreparation,
14
+ } from "./hierarchical-port-phases";
15
+ import { routeHierarchicalPorts } from "./hierarchical-port-routing";
16
+ import type { LayeredPhaseInput, NodePlacement, EdgeRoutes, LayerOrder, NodeSize } from "./types";
17
+
18
+ const restoredDummies = new WeakSet<ExternalPortDummy>();
19
+ export function isPerpendicularPortRestored(node: object) {
20
+ const origin = externalPortDummyOf(node);
21
+ return !!origin && restoredDummies.has(origin);
22
+ }
23
+
24
+ /** Restore original boundary nodes and routes in the physical export frame. */
25
+ export function restoreHierarchicalPorts(
26
+ input: LayeredPhaseInput,
27
+ order: LayerOrder,
28
+ prep: HierarchicalPortPreparation,
29
+ placement: NodePlacement,
30
+ routes: EdgeRoutes,
31
+ ) {
32
+ if (!prep.originals.size) return { input, routes };
33
+ const horizontal = input.direction === "right" || input.direction === "left";
34
+ const reversed = input.direction === "left" || input.direction === "up";
35
+ const crossReversed =
36
+ input.settings.directionCongruency === "ROTATION" &&
37
+ (input.direction === "left" || input.direction === "down");
38
+ const pad = input.padding;
39
+ const physical = hierarchicalPortSides(input);
40
+ const rects = placement.rectByNodeId as Map<string, EntityRect>;
41
+ const oldBounds = compactionBounds(placement),
42
+ crossBounds = placementCrossBounds(placement);
43
+ let physicalWidth = Math.max(0, ...[...rects.values()].map((r) => r.x + r.width)) - pad.left;
44
+ let physicalHeight = Math.max(0, ...[...rects.values()].map((r) => r.y + r.height)) - pad.top;
45
+ if (oldBounds) {
46
+ physicalWidth = Math.max(physicalWidth, oldBounds.right - pad.left);
47
+ physicalHeight = Math.max(physicalHeight, oldBounds.bottom - pad.top);
48
+ }
49
+ if (crossBounds?.axis === "x")
50
+ physicalWidth = Math.max(physicalWidth, crossBounds.maximum - pad.left);
51
+ if (crossBounds?.axis === "y")
52
+ physicalHeight = Math.max(physicalHeight, crossBounds.maximum - pad.top);
53
+ // Placement already includes graph padding; retain the measured phase extents.
54
+ const initialSize = {
55
+ width: horizontal ? physicalWidth : physicalHeight,
56
+ height: horizontal ? physicalHeight : physicalWidth,
57
+ };
58
+ const canonicalPad = horizontal
59
+ ? { ...pad }
60
+ : { left: pad.top, right: pad.bottom, top: pad.left, bottom: pad.right };
61
+ if (reversed) [canonicalPad.left, canonicalPad.right] = [canonicalPad.right, canonicalPad.left];
62
+ if (crossReversed)
63
+ [canonicalPad.top, canonicalPad.bottom] = [canonicalPad.bottom, canonicalPad.top];
64
+ const toCanonical = (point: Point): Point => {
65
+ let x = horizontal ? point.x - pad.left : point.y - pad.top;
66
+ let y = horizontal ? point.y - pad.top : point.x - pad.left;
67
+ if (reversed) x = initialSize.width - x;
68
+ if (crossReversed) y = initialSize.height - y;
69
+ return { x, y };
70
+ };
71
+ const toCanonicalRect = (rect: EntityRect): EntityRect => {
72
+ const p = toCanonical(rect),
73
+ width = horizontal ? rect.width : rect.height,
74
+ height = horizontal ? rect.height : rect.width;
75
+ return {
76
+ x: p.x - (reversed ? width : 0),
77
+ y: p.y - (crossReversed ? height : 0),
78
+ width,
79
+ height,
80
+ };
81
+ };
82
+ const records = new Map([...prep.replacements.values()].map((r) => [r.helper.id, r]));
83
+ const origins = [...prep.originals.values()].map((node) => {
84
+ const o = externalPortDummyOf(node)!;
85
+ const width = horizontal ? o.width : o.height,
86
+ height = horizontal ? o.height : o.width;
87
+ let x = horizontal ? o.port.x : o.port.y,
88
+ y = horizontal ? o.port.y : o.port.x;
89
+ if (reversed) x = width - x;
90
+ if (crossReversed) y = height - y;
91
+ const side = o.side === physical.north ? ("NORTH" as const) : ("SOUTH" as const);
92
+ return {
93
+ id: node.id,
94
+ origin: {
95
+ ...o,
96
+ width,
97
+ height,
98
+ side,
99
+ port: { x, y, side: side === "NORTH" ? ("SOUTH" as const) : ("NORTH" as const) },
100
+ },
101
+ };
102
+ });
103
+ const helpers = order.layers.flat().flatMap((id) => {
104
+ const record = records.get(id),
105
+ rect = rects.get(id);
106
+ if (!record || !rect) return [];
107
+ return [
108
+ {
109
+ id,
110
+ originalId: record.original.id,
111
+ rect: toCanonicalRect(rect),
112
+ incoming: input.graph.edges.filter((e) => e.targetId === id).map((e) => e.id),
113
+ outgoing: input.graph.edges.filter((e) => e.sourceId === id).map((e) => e.id),
114
+ },
115
+ ];
116
+ });
117
+ const restored = routeHierarchicalPorts(
118
+ {
119
+ constraints: input.settings.portConstraints ?? "FIXED_SIDE",
120
+ size: initialSize,
121
+ offset: { x: 0, y: 0 },
122
+ padding: canonicalPad,
123
+ spacing: {
124
+ node: input.spacing.node,
125
+ edge: Number(input.settings["spacing.edgeEdge"] ?? 10),
126
+ port: Number(input.settings["spacing.portPort"] ?? 10),
127
+ },
128
+ originals: origins,
129
+ helpers,
130
+ bends: new Map(
131
+ [...routes.pointsByEdgeId].map(([id, points]) => [
132
+ id,
133
+ points.slice(1, -1).map(toCanonical),
134
+ ]),
135
+ ),
136
+ },
137
+ phaseRandomByInput.get(input) ?? crossingRandom(input),
138
+ );
139
+ const fromCanonical = (p: Point): Point => {
140
+ let x = p.x + restored.offset.x,
141
+ y = p.y + restored.offset.y;
142
+ if (reversed) x = restored.size.width - x;
143
+ if (crossReversed) y = restored.size.height - y;
144
+ return horizontal ? { x: x + pad.left, y: y + pad.top } : { x: y + pad.left, y: x + pad.top };
145
+ };
146
+ const fromCanonicalRect = (rect: EntityRect): EntityRect => {
147
+ const p = fromCanonical(rect);
148
+ if (reversed) {
149
+ if (horizontal) p.x -= rect.width;
150
+ else p.y -= rect.width;
151
+ }
152
+ if (crossReversed) {
153
+ if (horizontal) p.y -= rect.height;
154
+ else p.x -= rect.height;
155
+ }
156
+ return {
157
+ ...p,
158
+ width: horizontal ? rect.width : rect.height,
159
+ height: horizontal ? rect.height : rect.width,
160
+ };
161
+ };
162
+ for (const [id, rect] of rects)
163
+ if (!records.has(id)) rects.set(id, fromCanonicalRect(toCanonicalRect(rect)));
164
+ for (const id of records.keys()) rects.delete(id);
165
+ for (const [id, rect] of restored.rects) {
166
+ rects.set(id, fromCanonicalRect(rect));
167
+ restoredDummies.add(externalPortDummyOf(prep.originals.get(id)!)!);
168
+ }
169
+ const originalById = new Map(origins.map((origin) => [origin.id, origin]));
170
+ const boundaryPoint = (helperId: string): Point => {
171
+ const original = records.get(helperId)!.original;
172
+ const o = originalById.get(original.id)!.origin,
173
+ rect = restored.rects.get(original.id)!;
174
+ return fromCanonical({ x: rect.x + o.port.x, y: rect.y + o.port.y });
175
+ };
176
+ const pointsByEdgeId = new Map(
177
+ [...routes.pointsByEdgeId].map(([id, points]) => [
178
+ id,
179
+ points.map((p) => fromCanonical(toCanonical(p))),
180
+ ]),
181
+ );
182
+ for (const edge of input.graph.edges) {
183
+ const source = records.get(edge.sourceId),
184
+ target = records.get(edge.targetId);
185
+ if (!source && !target) continue;
186
+ const old = routes.pointsByEdgeId.get(edge.id);
187
+ if (!old?.length) continue;
188
+ const first = source ? boundaryPoint(edge.sourceId) : fromCanonical(toCanonical(old[0]!));
189
+ const last = target ? boundaryPoint(edge.targetId) : fromCanonical(toCanonical(old.at(-1)!));
190
+ pointsByEdgeId.set(edge.id, [
191
+ first,
192
+ ...(restored.bends.get(edge.id) ?? []).map(fromCanonical),
193
+ last,
194
+ ]);
195
+ }
196
+ const nodes = [
197
+ ...input.graph.nodes.filter((n) => !records.has(n.id)),
198
+ ...prep.originals.values(),
199
+ ];
200
+ const edges = input.graph.edges.map((edge) => {
201
+ const source = records.get(edge.sourceId)?.original,
202
+ target = records.get(edge.targetId)?.original;
203
+ return {
204
+ ...edge,
205
+ sourceId: source?.id ?? edge.sourceId,
206
+ targetId: target?.id ?? edge.targetId,
207
+ sourcePort: source?.ports?.[0]?.name ?? edge.sourcePort,
208
+ targetPort: target?.ports?.[0]?.name ?? edge.targetPort,
209
+ };
210
+ });
211
+ const finalSize: NodeSize = horizontal
212
+ ? restored.size
213
+ : { width: restored.size.height, height: restored.size.width };
214
+ return {
215
+ input: inheritCycleRandom(input, { ...input, graph: { ...input.graph, nodes, edges } }),
216
+ routes: {
217
+ ...routes,
218
+ pointsByEdgeId,
219
+ junctionPointsByEdgeId: routes.junctionPointsByEdgeId
220
+ ? new Map(
221
+ [...routes.junctionPointsByEdgeId].map(([id, points]) => [
222
+ id,
223
+ points.map((p) => fromCanonical(toCanonical(p))),
224
+ ]),
225
+ )
226
+ : undefined,
227
+ },
228
+ bounds: {
229
+ width: finalSize.width + pad.left + pad.right,
230
+ height: finalSize.height + pad.top + pad.bottom,
231
+ },
232
+ };
233
+ }