@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,243 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2016, 2020 Kiel University and others.
3
+ * Adapted from ELK v0.11.0 ForsterConstraintResolver and BarycenterHeuristic.
4
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
5
+ * SPDX-License-Identifier: EPL-2.0
6
+ *******************************************************************************/
7
+ import type { JavaRandom } from "../java-random";
8
+ import type { LayeredPhaseInput } from "./types";
9
+ export interface CrossingUnits {
10
+ /** Edge positions in ELK's port edge lists at crossing minimization. */
11
+ edgeListRanks?: {
12
+ incoming: ReadonlyMap<string, ReadonlyMap<string, number>>;
13
+ outgoing: ReadonlyMap<string, ReadonlyMap<string, number>>;
14
+ };
15
+ northSouthOrigins?: ReadonlyMap<string, import("./north-south-ports").CrossPortOrigin>;
16
+ successors: ReadonlyMap<string, readonly string[]>;
17
+ units: ReadonlyMap<string, string>;
18
+ unitMembers?: ReadonlyMap<string, readonly string[]>;
19
+ associates: ReadonlyMap<string, readonly string[]>;
20
+ normalNodes: ReadonlySet<string>;
21
+ incomingEdgeOrderByDummyId?: ReadonlyMap<string, readonly string[]>;
22
+ longEdgeNodes?: ReadonlySet<string>;
23
+ northEdges?: ReadonlySet<string>;
24
+ southEdges?: ReadonlySet<string>;
25
+ constraintsBetweenNormalNodes?: boolean;
26
+ }
27
+ const unitState = new WeakMap<LayeredPhaseInput, CrossingUnits>();
28
+ export const recordCrossingUnits = (input: LayeredPhaseInput, state: CrossingUnits): void => {
29
+ unitState.set(input, state);
30
+ };
31
+ export const getCrossingUnits = (input: LayeredPhaseInput): CrossingUnits | undefined =>
32
+ unitState.get(input);
33
+
34
+ /** Recursively include same-layer neighbors and owner associates before perturbation. */
35
+ export function associatedBarycenters(
36
+ layer: readonly string[],
37
+ ranks: ReadonlyMap<string, readonly number[]>,
38
+ sameLayerNeighbors: ReadonlyMap<string, readonly string[]>,
39
+ associates: ReadonlyMap<string, readonly string[]>,
40
+ random?: JavaRandom,
41
+ orderedVisits?: ReadonlyMap<string, readonly (number | string)[]>,
42
+ ): Map<string, number | undefined> {
43
+ const states = new Map(
44
+ layer.map((id) => [
45
+ id,
46
+ { visited: false, degree: 0, weight: 0, barycenter: undefined as number | undefined },
47
+ ]),
48
+ );
49
+ const calculate = (id: string): void => {
50
+ const state = states.get(id);
51
+ if (!state || state.visited) return;
52
+ state.visited = true;
53
+ const includeNeighbor = (neighbor: string): void => {
54
+ calculate(neighbor);
55
+ const other = states.get(neighbor);
56
+ if (other) {
57
+ state.degree += other.degree;
58
+ state.weight += other.weight;
59
+ }
60
+ };
61
+ // ELK walks ports, then their incident edges. Same-layer recursion must
62
+ // occur at that exact position, before any later fixed-layer rank.
63
+ const visits = orderedVisits?.get(id) ?? [
64
+ ...(ranks.get(id) ?? []),
65
+ ...(sameLayerNeighbors.get(id) ?? []),
66
+ ];
67
+ for (const visit of visits) {
68
+ if (typeof visit === "number") {
69
+ state.degree++;
70
+ state.weight += visit;
71
+ } else if (visit !== id) includeNeighbor(visit);
72
+ }
73
+ for (const associate of associates.get(id) ?? []) includeNeighbor(associate);
74
+ if (state.degree > 0) {
75
+ state.weight += random ? random.nextFloat() * Math.fround(0.07) - Math.fround(0.07) / 2 : 0;
76
+ state.barycenter = state.weight / state.degree;
77
+ }
78
+ };
79
+ for (const id of layer) calculate(id);
80
+ return new Map([...states].map(([id, s]) => [id, s.barycenter]));
81
+ }
82
+
83
+ interface Group {
84
+ nodes: string[];
85
+ outgoing: Group[];
86
+ incoming: Group[];
87
+ incomingCount: number;
88
+ }
89
+ /** Resolve successor and layout-unit constraints by merging violated groups in barycenter order. */
90
+ export function resolveCrossingConstraints(
91
+ order: readonly string[],
92
+ barycenters: Map<string, number | undefined>,
93
+ state: CrossingUnits,
94
+ ): string[] {
95
+ if (state.constraintsBetweenNormalNodes) {
96
+ const normalOnly = new Map(
97
+ [...state.successors]
98
+ .filter(([id]) => state.normalNodes.has(id))
99
+ .map(([id, targets]) => [id, targets.filter((target) => state.normalNodes.has(target))]),
100
+ );
101
+ const first = resolveCrossingConstraints(order, barycenters, {
102
+ ...state,
103
+ successors: normalOnly,
104
+ units: new Map(),
105
+ unitMembers: new Map(),
106
+ constraintsBetweenNormalNodes: false,
107
+ });
108
+ return resolveCrossingConstraints(first, barycenters, {
109
+ ...state,
110
+ constraintsBetweenNormalNodes: false,
111
+ });
112
+ }
113
+ const groups: Group[] = order.map((id) => ({
114
+ nodes: [id],
115
+ outgoing: [],
116
+ incoming: [],
117
+ incomingCount: 0,
118
+ }));
119
+ const byId = new Map(groups.map((g) => [g.nodes[0]!, g]));
120
+ const score = (g: Group) => barycenters.get(g.nodes[0]!) ?? 0;
121
+ const units = new Map<string, string[]>();
122
+ if (state.unitMembers)
123
+ for (const [owner, members] of state.unitMembers)
124
+ units.set(
125
+ owner,
126
+ members.filter((id) => byId.has(id)),
127
+ );
128
+ else
129
+ for (const [id, owner] of state.units)
130
+ if (byId.has(id)) units.set(owner, [...(units.get(owner) ?? []), id]);
131
+ const add = (from: Group, to: Group) => {
132
+ from.outgoing.push(to);
133
+ to.incomingCount++;
134
+ };
135
+ let previous: string | undefined;
136
+ for (const id of order) {
137
+ const group = byId.get(id)!;
138
+ for (const successor of state.successors.get(id) ?? []) {
139
+ const target = byId.get(successor);
140
+ if (target) add(group, target);
141
+ }
142
+ if (state.normalNodes.has(id)) {
143
+ if (previous !== undefined)
144
+ for (const left of units.get(previous) ?? [])
145
+ for (const right of units.get(id) ?? []) add(byId.get(left)!, byId.get(right)!);
146
+ previous = id;
147
+ }
148
+ }
149
+ const violation = (): [Group, Group] | undefined => {
150
+ const active: Group[] = [];
151
+ for (const g of groups) {
152
+ g.incoming = [];
153
+ if (g.outgoing.length && g.incomingCount === 0) active.push(g);
154
+ }
155
+ for (let index = 0; index < active.length; index++) {
156
+ const group = active[index]!;
157
+ for (const predecessor of group.incoming) {
158
+ // elkjs Double.floatValue is an identity conversion, not float32 rounding.
159
+ if (
160
+ score(predecessor) === score(group)
161
+ ? groups.indexOf(predecessor) > groups.indexOf(group)
162
+ : score(predecessor) > score(group)
163
+ )
164
+ return [predecessor, group];
165
+ }
166
+ for (const target of group.outgoing) {
167
+ target.incoming.unshift(group);
168
+ if (target.incoming.length === target.incomingCount) active.push(target);
169
+ }
170
+ }
171
+ return undefined;
172
+ };
173
+ const removeOne = (values: Group[], target: Group): boolean => {
174
+ const index = values.indexOf(target);
175
+ if (index < 0) return false;
176
+ values.splice(index, 1);
177
+ return true;
178
+ };
179
+ let pair: [Group, Group] | undefined;
180
+ while ((pair = violation())) {
181
+ const [first, second] = pair;
182
+ // ConstraintGroup degrees/weights start at zero in ELK: merged scores use
183
+ // the mean of two group scores, not their node or edge counts.
184
+ const value = (score(first) + score(second)) / 2;
185
+ const merged: Group = {
186
+ nodes: [...first.nodes, ...second.nodes],
187
+ outgoing: [...first.outgoing],
188
+ incoming: [],
189
+ incomingCount: 0,
190
+ };
191
+ removeOne(merged.outgoing, second);
192
+ for (const target of second.outgoing)
193
+ if (target !== first) {
194
+ if (merged.outgoing.includes(target)) target.incomingCount--;
195
+ else merged.outgoing.push(target);
196
+ }
197
+ for (const id of merged.nodes) barycenters.set(id, value);
198
+ const survivors: Group[] = [];
199
+ let inserted = false;
200
+ for (const group of groups) {
201
+ if (group === first || group === second) continue;
202
+ if (!inserted && score(group) > value) {
203
+ survivors.push(merged);
204
+ inserted = true;
205
+ }
206
+ if (group.outgoing.length) {
207
+ const hadFirst = removeOne(group.outgoing, first),
208
+ hadSecond = removeOne(group.outgoing, second);
209
+ if (hadFirst || hadSecond) {
210
+ group.outgoing.push(merged);
211
+ merged.incomingCount++;
212
+ }
213
+ }
214
+ survivors.push(group);
215
+ }
216
+ if (!inserted) survivors.push(merged);
217
+ groups.splice(0, groups.length, ...survivors);
218
+ }
219
+ return groups.flatMap((g) => g.nodes);
220
+ }
221
+
222
+ /** ELK greedy switches must preserve port-unit and explicit successor constraints. */
223
+ export function canSwapCrossingUnits(upper: string, lower: string, state: CrossingUnits): boolean {
224
+ if (state.successors.get(upper)?.includes(lower)) return false;
225
+ const up = state.units.get(upper),
226
+ down = state.units.get(lower);
227
+ const upperDummy = up !== undefined && up !== upper,
228
+ lowerDummy = down !== undefined && down !== lower;
229
+ const different = up !== down;
230
+ const constrained =
231
+ upperDummy || lowerDummy || state.southEdges?.has(upper) || state.northEdges?.has(lower);
232
+ if (state.northEdges?.has(upper) || state.southEdges?.has(lower)) return false;
233
+ if (
234
+ !state.longEdgeNodes?.has(upper) &&
235
+ !state.longEdgeNodes?.has(lower) &&
236
+ constrained &&
237
+ different
238
+ )
239
+ return false;
240
+ if ((upperDummy && state.normalNodes.has(lower)) || (lowerDummy && state.normalNodes.has(upper)))
241
+ return false;
242
+ return true;
243
+ }
@@ -0,0 +1,221 @@
1
+ /*
2
+ * Copyright (c) 2016, 2020 Kiel University and others.
3
+ * Native adaptation of ELK AllCrossingsCounter and CrossingsCounter.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ */
6
+ import { countOrthogonalHypersegmentCrossings } from "./orthogonal-hypersegments";
7
+
8
+ export interface CrossingPort {
9
+ id: string;
10
+ side: "NORTH" | "EAST" | "SOUTH" | "WEST";
11
+ /** Normal cross-axis port's detached dummy. */
12
+ dummy?: string;
13
+ /** Detached cross-axis dummy port's original normal port. */
14
+ origin?: string;
15
+ }
16
+ export interface CrossingNode {
17
+ id: string;
18
+ type: "NORMAL" | "LONG_EDGE" | "NORTH_SOUTH_PORT" | "LABEL" | "EXTERNAL_PORT";
19
+ /** Clockwise canonical port order. */
20
+ ports: CrossingPort[];
21
+ unit?: string;
22
+ }
23
+ export interface CrossingGraph {
24
+ layers: CrossingNode[][];
25
+ edges: Array<{ source: string; target: string }>;
26
+ }
27
+ export interface CrossingScore {
28
+ betweenLayers: number;
29
+ inLayer: number;
30
+ northSouth: number;
31
+ total: number;
32
+ }
33
+
34
+ class IndexTree {
35
+ private readonly sums: number[];
36
+ private readonly entries: number[];
37
+ size = 0;
38
+ constructor(size: number) {
39
+ this.sums = Array(size + 1).fill(0);
40
+ this.entries = Array(size).fill(0);
41
+ }
42
+ add(position: number): void {
43
+ this.entries[position]!++;
44
+ this.size++;
45
+ for (let i = position + 1; i < this.sums.length; i += i & -i) this.sums[i]!++;
46
+ }
47
+ remove(position: number): void {
48
+ const count = this.entries[position]!;
49
+ if (!count) return;
50
+ this.entries[position] = 0;
51
+ this.size -= count;
52
+ for (let i = position + 1; i < this.sums.length; i += i & -i) this.sums[i]! -= count;
53
+ }
54
+ rank(position: number): number {
55
+ let value = 0;
56
+ for (let i = position; i > 0; i -= i & -i) value += this.sums[i]!;
57
+ return value;
58
+ }
59
+ }
60
+
61
+ /** Score a canonical candidate without changing its node or port orders. */
62
+ export function countAllCrossings(
63
+ graph: CrossingGraph,
64
+ boundaryMode: "hyperedges" | "edges" = "hyperedges",
65
+ ): CrossingScore {
66
+ const nodes = new Map(graph.layers.flat().map((node) => [node.id, node]));
67
+ const layerByPort = new Map<string, number>();
68
+ const incoming = new Map<string, string[]>(),
69
+ outgoing = new Map<string, string[]>();
70
+ for (const [layer, members] of graph.layers.entries())
71
+ for (const node of members)
72
+ for (const port of node.ports) {
73
+ layerByPort.set(port.id, layer);
74
+ incoming.set(port.id, []);
75
+ outgoing.set(port.id, []);
76
+ }
77
+ for (const edge of graph.edges) {
78
+ outgoing.get(edge.source)!.push(edge.target);
79
+ incoming.get(edge.target)!.push(edge.source);
80
+ }
81
+ const connected = (id: string) => [...incoming.get(id)!, ...outgoing.get(id)!];
82
+ const degree = (id: string) => incoming.get(id)!.length + outgoing.get(id)!.length;
83
+ const face = (layer: readonly CrossingNode[], side: "EAST" | "WEST", topDown = true) =>
84
+ (topDown ? layer : [...layer].reverse()).flatMap((node) => {
85
+ const ports = node.ports.filter((port) => port.side === side);
86
+ return (side === "EAST") === topDown ? ports : ports.reverse();
87
+ });
88
+ const countPorts = (ports: readonly CrossingPort[], inLayerOnly: boolean): number => {
89
+ const positions = new Map(ports.map((port, index) => [port.id, index]));
90
+ const tree = new IndexTree(ports.length);
91
+ let crossings = 0;
92
+ for (const [position, port] of ports.entries()) {
93
+ tree.remove(position);
94
+ const pending: number[] = [];
95
+ for (const peer of connected(port.id)) {
96
+ if (inLayerOnly && layerByPort.get(peer) !== layerByPort.get(port.id)) {
97
+ crossings += tree.size;
98
+ continue;
99
+ }
100
+ const end = positions.get(peer);
101
+ if (end !== undefined && end > position) {
102
+ crossings += tree.rank(end);
103
+ pending.push(end);
104
+ }
105
+ }
106
+ for (const end of pending) tree.add(end);
107
+ }
108
+ return crossings;
109
+ };
110
+ const countNorthSouth = (layer: readonly CrossingNode[]): number => {
111
+ const ports: CrossingPort[] = [],
112
+ stack: CrossingPort[] = [];
113
+ const flush = () => {
114
+ ports.push(...stack);
115
+ stack.length = 0;
116
+ };
117
+ let lastUnit: string | undefined;
118
+ for (const node of layer) {
119
+ if (lastUnit && lastUnit !== node.id && node.unit && node.unit !== lastUnit) flush();
120
+ if (node.unit) lastUnit = node.unit;
121
+ if (node.type === "NORMAL") {
122
+ ports.push(...node.ports.filter((port) => port.side === "NORTH" && port.dummy));
123
+ flush();
124
+ ports.push(...node.ports.filter((port) => port.side === "SOUTH" && port.dummy));
125
+ } else if (node.type === "NORTH_SOUTH_PORT" || node.type === "LONG_EDGE") {
126
+ const west = node.ports.find((port) => port.side === "WEST");
127
+ const east = node.ports.find((port) => port.side === "EAST");
128
+ if (west) ports.push(west);
129
+ if (east) stack.unshift(east);
130
+ }
131
+ }
132
+ flush();
133
+ const positions = new Map(ports.map((port, index) => [port.id, index]));
134
+ const ownerByPort = new Map(
135
+ layer.flatMap((node) => node.ports.map((port) => [port.id, node] as const)),
136
+ );
137
+ const tree = new IndexTree(ports.length);
138
+ let crossings = 0;
139
+ for (const [position, port] of ports.entries()) {
140
+ tree.remove(position);
141
+ const node = ownerByPort.get(port.id)!;
142
+ const targets =
143
+ node.type === "NORMAL"
144
+ ? nodes.get(port.dummy!)!.ports.map((peer) => ({ id: peer.id, degree: degree(peer.id) }))
145
+ : node.type === "LONG_EDGE"
146
+ ? node.ports
147
+ .filter((peer) => peer.id !== port.id)
148
+ .slice(0, 1)
149
+ .map((peer) => ({ id: peer.id, degree: degree(peer.id) }))
150
+ : port.origin
151
+ ? [{ id: port.origin, degree: degree(port.id) }]
152
+ : [];
153
+ const pending: number[] = [];
154
+ for (const target of targets) {
155
+ const end = positions.get(target.id);
156
+ if (end !== undefined && end > position) {
157
+ crossings += tree.rank(end) * target.degree;
158
+ pending.push(end);
159
+ }
160
+ }
161
+ for (const end of pending) tree.add(end);
162
+ }
163
+ return crossings;
164
+ };
165
+ const score: CrossingScore = { betweenLayers: 0, inLayer: 0, northSouth: 0, total: 0 };
166
+ if (!graph.layers.length) return score;
167
+ score.inLayer += countPorts(face(graph.layers[0]!, "WEST"), true);
168
+ score.inLayer += countPorts(face(graph.layers.at(-1)!, "EAST"), true);
169
+ for (const [index, layer] of graph.layers.entries()) {
170
+ const next = graph.layers[index + 1];
171
+ if (next) {
172
+ const hyper =
173
+ layer.some((node) =>
174
+ node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1),
175
+ ) ||
176
+ next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1));
177
+ if (hyper && boundaryMode === "hyperedges") {
178
+ const sourcePorts = layer
179
+ .flatMap((node) => node.ports)
180
+ .filter((port) => outgoing.get(port.id)!.some((peer) => layerByPort.get(peer) !== index));
181
+ const targetPorts = next.flatMap((node) => {
182
+ const connectedPorts = node.ports.filter((port) =>
183
+ incoming.get(port.id)!.some((peer) => layerByPort.get(peer) !== index + 1),
184
+ );
185
+ return [
186
+ ...connectedPorts.filter((port) => port.side === "NORTH").reverse(),
187
+ ...connectedPorts.filter((port) => port.side !== "NORTH").reverse(),
188
+ ];
189
+ });
190
+ const sources = new Set(sourcePorts.map((port) => port.id)),
191
+ targets = new Set(targetPorts.map((port) => port.id));
192
+ score.betweenLayers += countOrthogonalHypersegmentCrossings(
193
+ [
194
+ ...sourcePorts.map((port, position) => ({
195
+ id: port.id,
196
+ side: "source" as const,
197
+ position,
198
+ })),
199
+ ...targetPorts.map((port, position) => ({
200
+ id: port.id,
201
+ side: "target" as const,
202
+ position,
203
+ })),
204
+ ],
205
+ graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)),
206
+ );
207
+ score.inLayer +=
208
+ countPorts(face(layer, "EAST"), true) + countPorts(face(next, "WEST"), true);
209
+ } else {
210
+ score.betweenLayers += countPorts(
211
+ [...face(layer, "EAST"), ...face(next, "WEST", false)],
212
+ false,
213
+ );
214
+ }
215
+ }
216
+ if (layer.some((node) => node.type === "NORTH_SOUTH_PORT"))
217
+ score.northSouth += countNorthSouth(layer);
218
+ }
219
+ score.total = score.betweenLayers + score.inLayer + score.northSouth;
220
+ return score;
221
+ }