@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,141 @@
1
+ /*******************************************************************************
2
+ * Derived from ELK v0.11.0 EdgeAndLayerConstraintEdgeReverser.java.
3
+ * Copyright (c) 2010, 2017 Kiel University and others.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ *******************************************************************************/
6
+ import { createGraph } from "@statelyai/graph";
7
+ import type { GraphEdge } from "@statelyai/graph";
8
+ import type { AcyclicOrientation, CycleBreaker, LayeredPhaseInput } from "./types";
9
+ import { inheritCycleRandom } from "./cycle-random";
10
+
11
+ /** ELK handles outer constraints and whole feedback nodes before cycle breaking. */
12
+ export function orientConstrainedEdges(
13
+ input: LayeredPhaseInput,
14
+ initial: ReadonlySet<string>,
15
+ ): AcyclicOrientation & { reversedNodeIds: readonly string[] } {
16
+ const reversedEdgeIds = new Set(initial);
17
+ const reversedNodeIds: string[] = [];
18
+ const constraints = new Map(
19
+ input.graph.nodes.map((n) => [n.id, input.nodeSettings?.(n)?.["layering.layerConstraint"]]),
20
+ );
21
+ const forward =
22
+ input.direction === "right"
23
+ ? "EAST"
24
+ : input.direction === "left"
25
+ ? "WEST"
26
+ : input.direction === "down"
27
+ ? "SOUTH"
28
+ : "NORTH";
29
+ const backward =
30
+ input.direction === "right"
31
+ ? "WEST"
32
+ : input.direction === "left"
33
+ ? "EAST"
34
+ : input.direction === "down"
35
+ ? "NORTH"
36
+ : "SOUTH";
37
+ const reverse = (nodeId: string, kind: "incoming" | "outgoing" | "all") => {
38
+ for (const edge of input.graph.edges) {
39
+ if (reversedEdgeIds.has(edge.id)) continue;
40
+ if (
41
+ edge.sourceId === nodeId &&
42
+ kind !== "incoming" &&
43
+ constraints.get(edge.targetId) !== "LAST_SEPARATE"
44
+ )
45
+ reversedEdgeIds.add(edge.id);
46
+ else if (
47
+ edge.targetId === nodeId &&
48
+ kind !== "outgoing" &&
49
+ constraints.get(edge.sourceId) !== "FIRST_SEPARATE"
50
+ )
51
+ reversedEdgeIds.add(edge.id);
52
+ }
53
+ };
54
+ for (const node of input.graph.nodes) {
55
+ const c = constraints.get(node.id);
56
+ if (c === "FIRST" || c === "FIRST_SEPARATE") reverse(node.id, "incoming");
57
+ else if (c === "LAST" || c === "LAST_SEPARATE") reverse(node.id, "outgoing");
58
+ }
59
+ for (const node of input.graph.nodes) {
60
+ if (constraints.get(node.id) && constraints.get(node.id) !== "NONE") continue;
61
+ const fixed = input.nodeSettings?.(node)?.portConstraints;
62
+ if (
63
+ !["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(fixed)) ||
64
+ !node.ports?.length
65
+ )
66
+ continue;
67
+ const ports = new Map(
68
+ node.ports.map((p) => [
69
+ p.name,
70
+ { side: input.portSettings?.(p, node)?.["port.side"], net: 0 },
71
+ ]),
72
+ );
73
+ let protectedNeighbor = false;
74
+ for (const edge of input.graph.edges) {
75
+ const reversed = reversedEdgeIds.has(edge.id);
76
+ const source = reversed ? edge.targetId : edge.sourceId;
77
+ const target = reversed ? edge.sourceId : edge.targetId;
78
+ for (const incoming of [false, true]) {
79
+ if ((incoming ? target : source) !== node.id) continue;
80
+ const name = incoming
81
+ ? reversed
82
+ ? edge.sourcePort
83
+ : edge.targetPort
84
+ : reversed
85
+ ? edge.targetPort
86
+ : edge.sourcePort;
87
+ const key = name ?? `__implicit:${edge.id}:${incoming}`;
88
+ const port = ports.get(key) ?? { side: incoming ? backward : forward, net: 0 };
89
+ port.net += incoming ? 1 : -1;
90
+ ports.set(key, port);
91
+ const otherConstraint = constraints.get(incoming ? source : target);
92
+ if (
93
+ incoming
94
+ ? otherConstraint === "FIRST" || otherConstraint === "FIRST_SEPARATE"
95
+ : otherConstraint === "LAST" || otherConstraint === "LAST_SEPARATE"
96
+ )
97
+ protectedNeighbor = true;
98
+ }
99
+ }
100
+ // Fixed-side UNDEFINED ports are assigned the side opposite their flow.
101
+ // An unused or non-flow port prevents whole-node feedback reversal.
102
+ const allReversed = [...ports.values()].every((p) => {
103
+ const side = p.side === "UNDEFINED" ? (p.net > 0 ? forward : backward) : p.side;
104
+ return (side === forward && p.net > 0) || (side === backward && p.net < 0);
105
+ });
106
+ if (allReversed && !protectedNeighbor) {
107
+ reversedNodeIds.push(node.id);
108
+ reverse(node.id, "all");
109
+ }
110
+ }
111
+ return { reversedEdgeIds, reversedNodeIds };
112
+ }
113
+
114
+ /** Preserve original edge identities and carry the actual post-cycle RNG back. */
115
+ export function breakCyclesWithConstraints(
116
+ input: LayeredPhaseInput,
117
+ breaker: CycleBreaker,
118
+ ): AcyclicOrientation {
119
+ const prepared = orientConstrainedEdges(input, new Set()).reversedEdgeIds;
120
+ if (!prepared.size) return breaker(input);
121
+ const edges: GraphEdge[] = input.graph.edges.map((e) =>
122
+ prepared.has(e.id)
123
+ ? {
124
+ ...e,
125
+ sourceId: e.targetId,
126
+ targetId: e.sourceId,
127
+ sourcePort: e.targetPort,
128
+ targetPort: e.sourcePort,
129
+ }
130
+ : e,
131
+ );
132
+ const phaseInput: LayeredPhaseInput = { ...input, graph: createGraph({ ...input.graph, edges }) };
133
+ const cycles = breaker(phaseInput);
134
+ inheritCycleRandom(phaseInput, input);
135
+ const reversedEdgeIds = new Set(prepared);
136
+ for (const id of cycles.reversedEdgeIds) {
137
+ if (reversedEdgeIds.has(id)) reversedEdgeIds.delete(id);
138
+ else reversedEdgeIds.add(id);
139
+ }
140
+ return { reversedEdgeIds };
141
+ }
@@ -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,233 @@
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 { scoreOrthogonalHypersegments } 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
+ /** Some hyperedge joins two or more ports on each side of its gap. */
33
+ sharedTracks: boolean;
34
+ }
35
+
36
+ class IndexTree {
37
+ private readonly sums: number[];
38
+ private readonly entries: number[];
39
+ size = 0;
40
+ constructor(size: number) {
41
+ this.sums = Array(size + 1).fill(0);
42
+ this.entries = Array(size).fill(0);
43
+ }
44
+ add(position: number): void {
45
+ this.entries[position]!++;
46
+ this.size++;
47
+ for (let i = position + 1; i < this.sums.length; i += i & -i) this.sums[i]!++;
48
+ }
49
+ remove(position: number): void {
50
+ const count = this.entries[position]!;
51
+ if (!count) return;
52
+ this.entries[position] = 0;
53
+ this.size -= count;
54
+ for (let i = position + 1; i < this.sums.length; i += i & -i) this.sums[i]! -= count;
55
+ }
56
+ rank(position: number): number {
57
+ let value = 0;
58
+ for (let i = position; i > 0; i -= i & -i) value += this.sums[i]!;
59
+ return value;
60
+ }
61
+ }
62
+
63
+ /** Score a canonical candidate without changing its node or port orders. */
64
+ export function countAllCrossings(
65
+ graph: CrossingGraph,
66
+ /** "separated" counts hyperedges like ELK unless they would share a track (see scoreOrthogonalHypersegments). */
67
+ boundaryMode: "hyperedges" | "separated" | "edges" = "hyperedges",
68
+ ): CrossingScore {
69
+ const nodes = new Map(graph.layers.flat().map((node) => [node.id, node]));
70
+ const layerByPort = new Map<string, number>();
71
+ const incoming = new Map<string, string[]>(),
72
+ outgoing = new Map<string, string[]>();
73
+ for (const [layer, members] of graph.layers.entries())
74
+ for (const node of members)
75
+ for (const port of node.ports) {
76
+ layerByPort.set(port.id, layer);
77
+ incoming.set(port.id, []);
78
+ outgoing.set(port.id, []);
79
+ }
80
+ for (const edge of graph.edges) {
81
+ outgoing.get(edge.source)!.push(edge.target);
82
+ incoming.get(edge.target)!.push(edge.source);
83
+ }
84
+ const connected = (id: string) => [...incoming.get(id)!, ...outgoing.get(id)!];
85
+ const degree = (id: string) => incoming.get(id)!.length + outgoing.get(id)!.length;
86
+ const face = (layer: readonly CrossingNode[], side: "EAST" | "WEST", topDown = true) =>
87
+ (topDown ? layer : [...layer].reverse()).flatMap((node) => {
88
+ const ports = node.ports.filter((port) => port.side === side);
89
+ return (side === "EAST") === topDown ? ports : ports.reverse();
90
+ });
91
+ const countPorts = (ports: readonly CrossingPort[], inLayerOnly: boolean): number => {
92
+ const positions = new Map(ports.map((port, index) => [port.id, index]));
93
+ const tree = new IndexTree(ports.length);
94
+ let crossings = 0;
95
+ for (const [position, port] of ports.entries()) {
96
+ tree.remove(position);
97
+ const pending: number[] = [];
98
+ for (const peer of connected(port.id)) {
99
+ if (inLayerOnly && layerByPort.get(peer) !== layerByPort.get(port.id)) {
100
+ crossings += tree.size;
101
+ continue;
102
+ }
103
+ const end = positions.get(peer);
104
+ if (end !== undefined && end > position) {
105
+ crossings += tree.rank(end);
106
+ pending.push(end);
107
+ }
108
+ }
109
+ for (const end of pending) tree.add(end);
110
+ }
111
+ return crossings;
112
+ };
113
+ const countNorthSouth = (layer: readonly CrossingNode[]): number => {
114
+ const ports: CrossingPort[] = [],
115
+ stack: CrossingPort[] = [];
116
+ const flush = () => {
117
+ ports.push(...stack);
118
+ stack.length = 0;
119
+ };
120
+ let lastUnit: string | undefined;
121
+ for (const node of layer) {
122
+ if (lastUnit && lastUnit !== node.id && node.unit && node.unit !== lastUnit) flush();
123
+ if (node.unit) lastUnit = node.unit;
124
+ if (node.type === "NORMAL") {
125
+ ports.push(...node.ports.filter((port) => port.side === "NORTH" && port.dummy));
126
+ flush();
127
+ ports.push(...node.ports.filter((port) => port.side === "SOUTH" && port.dummy));
128
+ } else if (node.type === "NORTH_SOUTH_PORT" || node.type === "LONG_EDGE") {
129
+ const west = node.ports.find((port) => port.side === "WEST");
130
+ const east = node.ports.find((port) => port.side === "EAST");
131
+ if (west) ports.push(west);
132
+ if (east) stack.unshift(east);
133
+ }
134
+ }
135
+ flush();
136
+ const positions = new Map(ports.map((port, index) => [port.id, index]));
137
+ const ownerByPort = new Map(
138
+ layer.flatMap((node) => node.ports.map((port) => [port.id, node] as const)),
139
+ );
140
+ const tree = new IndexTree(ports.length);
141
+ let crossings = 0;
142
+ for (const [position, port] of ports.entries()) {
143
+ tree.remove(position);
144
+ const node = ownerByPort.get(port.id)!;
145
+ const targets =
146
+ node.type === "NORMAL"
147
+ ? nodes.get(port.dummy!)!.ports.map((peer) => ({ id: peer.id, degree: degree(peer.id) }))
148
+ : node.type === "LONG_EDGE"
149
+ ? node.ports
150
+ .filter((peer) => peer.id !== port.id)
151
+ .slice(0, 1)
152
+ .map((peer) => ({ id: peer.id, degree: degree(peer.id) }))
153
+ : port.origin
154
+ ? [{ id: port.origin, degree: degree(port.id) }]
155
+ : [];
156
+ const pending: number[] = [];
157
+ for (const target of targets) {
158
+ const end = positions.get(target.id);
159
+ if (end !== undefined && end > position) {
160
+ crossings += tree.rank(end) * target.degree;
161
+ pending.push(end);
162
+ }
163
+ }
164
+ for (const end of pending) tree.add(end);
165
+ }
166
+ return crossings;
167
+ };
168
+ const score: CrossingScore = {
169
+ betweenLayers: 0,
170
+ inLayer: 0,
171
+ northSouth: 0,
172
+ total: 0,
173
+ sharedTracks: false,
174
+ };
175
+ if (!graph.layers.length) return score;
176
+ score.inLayer += countPorts(face(graph.layers[0]!, "WEST"), true);
177
+ score.inLayer += countPorts(face(graph.layers.at(-1)!, "EAST"), true);
178
+ for (const [index, layer] of graph.layers.entries()) {
179
+ const next = graph.layers[index + 1];
180
+ if (next) {
181
+ const hyper =
182
+ layer.some((node) =>
183
+ node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1),
184
+ ) ||
185
+ next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1));
186
+ if (hyper && boundaryMode !== "edges") {
187
+ const sourcePorts = layer
188
+ .flatMap((node) => node.ports)
189
+ .filter((port) => outgoing.get(port.id)!.some((peer) => layerByPort.get(peer) !== index));
190
+ const targetPorts = next.flatMap((node) => {
191
+ const connectedPorts = node.ports.filter((port) =>
192
+ incoming.get(port.id)!.some((peer) => layerByPort.get(peer) !== index + 1),
193
+ );
194
+ return [
195
+ ...connectedPorts.filter((port) => port.side === "NORTH").reverse(),
196
+ ...connectedPorts.filter((port) => port.side !== "NORTH").reverse(),
197
+ ];
198
+ });
199
+ const sources = new Set(sourcePorts.map((port) => port.id)),
200
+ targets = new Set(targetPorts.map((port) => port.id));
201
+ const hyperedges = scoreOrthogonalHypersegments(
202
+ [
203
+ ...sourcePorts.map((port, position) => ({
204
+ id: port.id,
205
+ side: "source" as const,
206
+ position,
207
+ })),
208
+ ...targetPorts.map((port, position) => ({
209
+ id: port.id,
210
+ side: "target" as const,
211
+ position,
212
+ })),
213
+ ],
214
+ graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)),
215
+ boundaryMode === "separated",
216
+ );
217
+ score.betweenLayers += hyperedges.count;
218
+ score.sharedTracks ||= hyperedges.shared;
219
+ score.inLayer +=
220
+ countPorts(face(layer, "EAST"), true) + countPorts(face(next, "WEST"), true);
221
+ } else {
222
+ score.betweenLayers += countPorts(
223
+ [...face(layer, "EAST"), ...face(next, "WEST", false)],
224
+ false,
225
+ );
226
+ }
227
+ }
228
+ if (layer.some((node) => node.type === "NORTH_SOUTH_PORT"))
229
+ score.northSouth += countNorthSouth(layer);
230
+ }
231
+ score.total = score.betweenLayers + score.inLayer + score.northSouth;
232
+ return score;
233
+ }