dsh-model-everything 0.5.0 → 0.5.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.
- package/README.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2437 -216
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +764 -67
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +143 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +42 -9
- package/lib/types/render/place.d.ts +35 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +84 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -290
- package/src/render/place.ts +527 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/wrap.ts +103 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum-length ranking: the network simplex of Gansner, Koutsofios, North and Vo (1993).
|
|
3
|
+
*
|
|
4
|
+
* The ranking problem: give every node a layer such that each edge spans at least its minimum length, and make the
|
|
5
|
+
* total of all the slack - the layers every edge spans beyond its minimum - as small as it can be. Slack is what a
|
|
6
|
+
* reader pays for: an edge that spans three layers has to be drawn through two of them.
|
|
7
|
+
*
|
|
8
|
+
* Longest paths are one feasible answer and they are not the best one. They put every node as high as it can
|
|
9
|
+
* possibly go, which leaves a node whose edges leave it with nothing to do above the boxes it points at. The best
|
|
10
|
+
* answer is the solution of a linear program, and network simplex is how the mature layering implementations solve
|
|
11
|
+
* it: hold a *feasible tree* - a spanning tree every edge of which is tight - and a ranking that tightens it, attach
|
|
12
|
+
* a *cut value* to each tree edge (the weight flowing one way across that cut minus the weight flowing the other),
|
|
13
|
+
* and while some tree edge has a negative cut value, take it out, let the cheapest edge across its cut in, and move
|
|
14
|
+
* the ranks so the new edge is tight. The cut value is the rate at which the total length falls as the tree edge is
|
|
15
|
+
* left out, so every exchange shortens the drawing, and a tree with no negative cut value is the optimum: no single
|
|
16
|
+
* exchange is left that would shorten it, and for this program that is the definition of optimal.
|
|
17
|
+
*
|
|
18
|
+
* Three things are done differently from the paper, each because this graph is not the paper's graph:
|
|
19
|
+
*
|
|
20
|
+
* - **Components are solved one at a time.** The ranking is translation invariant, so two components that share no
|
|
21
|
+
* edge can be laid out apart from each other; each is solved on its own and its top row is then numbered zero.
|
|
22
|
+
* - **A node may be pinned to the top.** A state machine has an initial state and a reader expects it first, so the
|
|
23
|
+
* stage can name one node that must be on the topmost layer: a zero-length, zero-weight constraint edge runs from
|
|
24
|
+
* it to every other node of its component (`rank(v) >= rank(pinned)`), and the ranking is minimised subject to that.
|
|
25
|
+
* The constraint costs length when the free optimum would have put the pinned node below another source, and that
|
|
26
|
+
* price is what "the initial state is first" means.
|
|
27
|
+
* - **The iteration is capped.** A degenerate exchange (one that moves nothing) is legal and in theory can go round
|
|
28
|
+
* in circles. The cap turns that into a ranking that is feasible and no worse than where it started.
|
|
29
|
+
*
|
|
30
|
+
* Pure, and generic in the ranking program: no ids, no sizes, no drawing.
|
|
31
|
+
*/
|
|
32
|
+
/** How many layers this edge spans beyond its minimum: at least zero in a feasible ranking. */
|
|
33
|
+
function slackOf(edge, rank) {
|
|
34
|
+
return rank[edge.to] - rank[edge.from] - edge.minLength;
|
|
35
|
+
}
|
|
36
|
+
/** The nodes of each weakly connected component, in index order, each list in index order. */
|
|
37
|
+
function componentsOf(count, edges) {
|
|
38
|
+
const neighbours = [];
|
|
39
|
+
for (let node = 0; node < count; node += 1)
|
|
40
|
+
neighbours.push([]);
|
|
41
|
+
for (const edge of edges) {
|
|
42
|
+
neighbours[edge.from].push(edge.to);
|
|
43
|
+
neighbours[edge.to].push(edge.from);
|
|
44
|
+
}
|
|
45
|
+
const seen = new Array(count).fill(false);
|
|
46
|
+
const components = [];
|
|
47
|
+
for (let node = 0; node < count; node += 1) {
|
|
48
|
+
if (seen[node])
|
|
49
|
+
continue;
|
|
50
|
+
seen[node] = true;
|
|
51
|
+
const members = [node];
|
|
52
|
+
for (let head = 0; head < members.length; head += 1) {
|
|
53
|
+
for (const next of neighbours[members[head]]) {
|
|
54
|
+
if (seen[next])
|
|
55
|
+
continue;
|
|
56
|
+
seen[next] = true;
|
|
57
|
+
members.push(next);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
members.sort((left, right) => left - right);
|
|
61
|
+
components.push(members);
|
|
62
|
+
}
|
|
63
|
+
return components;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A spanning tree of tight edges, built one edge at a time.
|
|
67
|
+
*
|
|
68
|
+
* A tight edge is one of the tree's to grow on: it already spans exactly its minimum, so it constrains the ranking
|
|
69
|
+
* without costing anything. When no tight edge reaches out of the tree, no tree can be grown from here without
|
|
70
|
+
* moving something: the nodes outside the tree are shifted as one block until a crossing edge is tight. The block
|
|
71
|
+
* moves by the largest amount that keeps every crossing edge feasible, which is the smaller of the two bounds the
|
|
72
|
+
* crossing edges impose - one per direction - so one edge becomes tight and none goes negative.
|
|
73
|
+
*/
|
|
74
|
+
function buildTightTree(members, edges, rank, tree) {
|
|
75
|
+
const inside = new Set([members[0]]);
|
|
76
|
+
while (inside.size < members.length) {
|
|
77
|
+
let tight = -1;
|
|
78
|
+
for (const [index, edge] of edges.entries()) {
|
|
79
|
+
if (tree.has(index))
|
|
80
|
+
continue;
|
|
81
|
+
if (inside.has(edge.from) === inside.has(edge.to))
|
|
82
|
+
continue;
|
|
83
|
+
if (slackOf(edge, rank) === 0) {
|
|
84
|
+
tight = index;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (tight >= 0) {
|
|
89
|
+
tree.add(tight);
|
|
90
|
+
inside.add(edges[tight].from);
|
|
91
|
+
inside.add(edges[tight].to);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Nothing tight leaves the tree: slide everything outside it until something does. An edge leaving the tree
|
|
95
|
+
// gains a layer of slack per unit of the slide and an edge entering it loses one, so the feasible slides are the
|
|
96
|
+
// interval between the two bounds, and the bound that is not zero is where an edge becomes tight.
|
|
97
|
+
let lowest = -Infinity;
|
|
98
|
+
let highest = Infinity;
|
|
99
|
+
for (const edge of edges) {
|
|
100
|
+
const from = inside.has(edge.from);
|
|
101
|
+
const to = inside.has(edge.to);
|
|
102
|
+
if (from === to)
|
|
103
|
+
continue;
|
|
104
|
+
const slack = slackOf(edge, rank);
|
|
105
|
+
if (from)
|
|
106
|
+
lowest = Math.max(lowest, -slack);
|
|
107
|
+
else
|
|
108
|
+
highest = Math.min(highest, slack);
|
|
109
|
+
}
|
|
110
|
+
// A connected component has at least one edge across the tree's cut, so one of the two bounds is finite.
|
|
111
|
+
const step = lowest > -Infinity ? lowest : highest;
|
|
112
|
+
for (const node of members)
|
|
113
|
+
if (!inside.has(node))
|
|
114
|
+
rank[node] += step;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function walkTree(members, edges, tree) {
|
|
118
|
+
const adjacency = new Map();
|
|
119
|
+
const link = (node, other) => {
|
|
120
|
+
adjacency.set(node, [...(adjacency.get(node) ?? []), { node: other }]);
|
|
121
|
+
};
|
|
122
|
+
for (const index of tree) {
|
|
123
|
+
const edge = edges[index];
|
|
124
|
+
link(edge.from, edge.to);
|
|
125
|
+
link(edge.to, edge.from);
|
|
126
|
+
}
|
|
127
|
+
const parent = new Map();
|
|
128
|
+
const first = new Map();
|
|
129
|
+
const size = new Map();
|
|
130
|
+
const order = [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const root = members[0];
|
|
133
|
+
const stack = [root];
|
|
134
|
+
seen.add(root);
|
|
135
|
+
parent.set(root, -1);
|
|
136
|
+
while (stack.length > 0) {
|
|
137
|
+
const node = stack.pop();
|
|
138
|
+
first.set(node, order.length);
|
|
139
|
+
order.push(node);
|
|
140
|
+
for (const step of adjacency.get(node)) {
|
|
141
|
+
if (seen.has(step.node))
|
|
142
|
+
continue;
|
|
143
|
+
seen.add(step.node);
|
|
144
|
+
parent.set(step.node, node);
|
|
145
|
+
stack.push(step.node);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (let index = order.length - 1; index >= 0; index -= 1) {
|
|
149
|
+
const node = order[index];
|
|
150
|
+
size.set(node, (size.get(node) ?? 0) + 1);
|
|
151
|
+
const above = parent.get(node);
|
|
152
|
+
if (above !== undefined && above >= 0)
|
|
153
|
+
size.set(above, (size.get(above) ?? 0) + size.get(node));
|
|
154
|
+
}
|
|
155
|
+
return { parent, first, size, order };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Solve one component in place: grow a tight tree, exchange until no cut value is negative, then number its top row
|
|
159
|
+
* zero.
|
|
160
|
+
*/
|
|
161
|
+
function solveComponent(size, members, edges, rank) {
|
|
162
|
+
if (members.length < 2) {
|
|
163
|
+
if (members.length === 1)
|
|
164
|
+
rank[members[0]] = 0;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const tree = new Set();
|
|
168
|
+
buildTightTree(members, edges, rank, tree);
|
|
169
|
+
// A tree that does not span cannot be exchanged on; the start ranking is still feasible, so it is what is kept.
|
|
170
|
+
const spanning = tree.size === members.length - 1;
|
|
171
|
+
if (spanning)
|
|
172
|
+
improve(size, members, edges, tree, rank);
|
|
173
|
+
let top = Infinity;
|
|
174
|
+
for (const node of members)
|
|
175
|
+
top = Math.min(top, rank[node]);
|
|
176
|
+
for (const node of members)
|
|
177
|
+
rank[node] -= top;
|
|
178
|
+
}
|
|
179
|
+
function improve(size, members, edges, tree, rank) {
|
|
180
|
+
const incoming = new Float64Array(size);
|
|
181
|
+
const outgoing = new Float64Array(size);
|
|
182
|
+
for (const edge of edges) {
|
|
183
|
+
outgoing[edge.from] += edge.weight;
|
|
184
|
+
incoming[edge.to] += edge.weight;
|
|
185
|
+
}
|
|
186
|
+
const cap = Math.max(50, 8 * members.length + 4 * edges.length);
|
|
187
|
+
for (let iteration = 0; iteration < cap; iteration += 1) {
|
|
188
|
+
const walk = walkTree(members, edges, tree);
|
|
189
|
+
// A subtree's cut value is what enters it minus what leaves it, or the other way round: which of the two is the
|
|
190
|
+
// tail side of the tree edge decides the sign. Removing a tree edge splits the tree in two, and the side holding
|
|
191
|
+
// the edge's tail is the one the value is read from.
|
|
192
|
+
const entering = new Map();
|
|
193
|
+
const leaving = new Map();
|
|
194
|
+
for (let index = walk.order.length - 1; index >= 0; index -= 1) {
|
|
195
|
+
const node = walk.order[index];
|
|
196
|
+
entering.set(node, incoming[node] + (entering.get(node) ?? 0));
|
|
197
|
+
leaving.set(node, outgoing[node] + (leaving.get(node) ?? 0));
|
|
198
|
+
const above = walk.parent.get(node);
|
|
199
|
+
if (above === undefined || above < 0)
|
|
200
|
+
continue;
|
|
201
|
+
entering.set(above, (entering.get(above) ?? 0) + entering.get(node));
|
|
202
|
+
leaving.set(above, (leaving.get(above) ?? 0) + leaving.get(node));
|
|
203
|
+
}
|
|
204
|
+
const holds = (node, child) => {
|
|
205
|
+
const start = walk.first.get(child);
|
|
206
|
+
const at = walk.first.get(node);
|
|
207
|
+
return at >= start && at < start + walk.size.get(child);
|
|
208
|
+
};
|
|
209
|
+
let worst = -1;
|
|
210
|
+
let worstEdge = -1;
|
|
211
|
+
let worstChild = -1;
|
|
212
|
+
for (const index of tree) {
|
|
213
|
+
const edge = edges[index];
|
|
214
|
+
const child = walk.parent.get(edge.from) === edge.to ? edge.from : edge.to;
|
|
215
|
+
const tailInside = holds(edge.from, child);
|
|
216
|
+
const value = tailInside
|
|
217
|
+
? leaving.get(child) - entering.get(child)
|
|
218
|
+
: entering.get(child) - leaving.get(child);
|
|
219
|
+
if (value < 0 && (worstEdge < 0 || value < worst)) {
|
|
220
|
+
worst = value;
|
|
221
|
+
worstEdge = index;
|
|
222
|
+
worstChild = child;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (worstEdge < 0)
|
|
226
|
+
return;
|
|
227
|
+
// The head side of the leaving edge is moved down, which shortens every edge running from it to the tail side.
|
|
228
|
+
// The move is as far as the first of those edges allows, and that edge becomes the one that enters the tree.
|
|
229
|
+
const leavingEdge = edges[worstEdge];
|
|
230
|
+
const headInside = !holds(leavingEdge.from, worstChild);
|
|
231
|
+
const inHead = (node) => (headInside ? holds(node, worstChild) : !holds(node, worstChild));
|
|
232
|
+
let enteringEdge = -1;
|
|
233
|
+
let step = Infinity;
|
|
234
|
+
for (const [index, edge] of edges.entries()) {
|
|
235
|
+
if (tree.has(index))
|
|
236
|
+
continue;
|
|
237
|
+
if (!inHead(edge.from) || inHead(edge.to))
|
|
238
|
+
continue;
|
|
239
|
+
const slack = slackOf(edge, rank);
|
|
240
|
+
if (slack < step || (slack === step && (enteringEdge < 0 || index < enteringEdge))) {
|
|
241
|
+
step = slack;
|
|
242
|
+
enteringEdge = index;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (enteringEdge < 0 || step === Infinity)
|
|
246
|
+
return;
|
|
247
|
+
if (step !== 0)
|
|
248
|
+
for (const node of members)
|
|
249
|
+
if (inHead(node))
|
|
250
|
+
rank[node] += step;
|
|
251
|
+
tree.delete(worstEdge);
|
|
252
|
+
tree.add(enteringEdge);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function minimumLength(problem) {
|
|
256
|
+
const rank = problem.start.slice();
|
|
257
|
+
for (const members of componentsOf(problem.count, problem.edges)) {
|
|
258
|
+
const inside = new Set(members);
|
|
259
|
+
const local = problem.edges.filter((edge) => inside.has(edge.from) && inside.has(edge.to));
|
|
260
|
+
const pinned = problem.pinned !== undefined && inside.has(problem.pinned) ? problem.pinned : undefined;
|
|
261
|
+
// The pin is a set of zero-length constraints from the pinned node to the rest of its component. It is only
|
|
262
|
+
// feasible when the start ranking already holds the pinned node at the top of its component.
|
|
263
|
+
const pinnedRank = pinned === undefined ? undefined : rank[pinned];
|
|
264
|
+
if (pinned !== undefined && pinnedRank !== undefined && members.every((node) => rank[node] >= pinnedRank)) {
|
|
265
|
+
for (const node of members)
|
|
266
|
+
if (node !== pinned)
|
|
267
|
+
local.push({ from: pinned, to: node, minLength: 0, weight: 0 });
|
|
268
|
+
}
|
|
269
|
+
solveComponent(problem.count, members, local, rank);
|
|
270
|
+
}
|
|
271
|
+
return rank;
|
|
272
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering: the left-to-right order of each layer.
|
|
3
|
+
*
|
|
4
|
+
* The layer a box is in is fixed by now; the only freedom left is the order inside each layer, and that order decides
|
|
5
|
+
* how many edges cross. A crossing is counted on a pair of adjacent layers: two edges between the same two layers
|
|
6
|
+
* cross when their end points are in opposite order on the top and the bottom.
|
|
7
|
+
*
|
|
8
|
+
* The method is the one every layered layout uses. A median sweep takes each layer in turn and sorts it by the median
|
|
9
|
+
* position of its neighbours in the layer just visited - down the drawing, then up. A down sweep alone settles each
|
|
10
|
+
* layer against the one above it and can be undone by the next; alternating is what makes the passes converge.
|
|
11
|
+
*
|
|
12
|
+
* The median is a good guess, not a search. Greedy switch is the search: try swapping two neighbours in a layer, keep
|
|
13
|
+
* the swap if the total crossing count falls, and stop when a whole pass finds nothing. That is where a crossing the
|
|
14
|
+
* median cannot see gets removed.
|
|
15
|
+
*
|
|
16
|
+
* Pure: layers of ids in, layers of ids out, plus the crossing count that justified them.
|
|
17
|
+
*/
|
|
18
|
+
/** Where each id sits, by layer and by position within it. */
|
|
19
|
+
function locate(layers) {
|
|
20
|
+
const found = new Map();
|
|
21
|
+
layers.forEach((layer, depth) => layer.forEach((id, index) => found.set(id, { layer: depth, index })));
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
/** The median position of an id's neighbours in the layer that was just visited; its own when it has none there. */
|
|
25
|
+
function medianOf(id, neighbours, reference, positions) {
|
|
26
|
+
const found = (neighbours.get(id) ?? [])
|
|
27
|
+
.filter((other) => positions.get(other)?.layer === reference)
|
|
28
|
+
.map((other) => positions.get(other).index)
|
|
29
|
+
.sort((left, right) => left - right);
|
|
30
|
+
if (found.length === 0)
|
|
31
|
+
return positions.get(id).index;
|
|
32
|
+
const middle = Math.floor(found.length / 2);
|
|
33
|
+
return found.length % 2 === 1 ? found[middle] : (found[middle - 1] + found[middle]) / 2;
|
|
34
|
+
}
|
|
35
|
+
/** How many pairs of edges between adjacent layers cross. */
|
|
36
|
+
export function countCrossings(layers, chains) {
|
|
37
|
+
const positions = locate(layers);
|
|
38
|
+
const segments = new Map();
|
|
39
|
+
for (const chain of chains) {
|
|
40
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
41
|
+
const top = positions.get(chain.path[cursor]);
|
|
42
|
+
const bottom = positions.get(chain.path[cursor + 1]);
|
|
43
|
+
if (top === undefined || bottom === undefined || bottom.layer !== top.layer + 1)
|
|
44
|
+
continue;
|
|
45
|
+
const list = segments.get(top.layer) ?? [];
|
|
46
|
+
list.push({ top: top.index, bottom: bottom.index });
|
|
47
|
+
segments.set(top.layer, list);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let crossings = 0;
|
|
51
|
+
for (const list of segments.values()) {
|
|
52
|
+
for (let left = 0; left < list.length; left += 1) {
|
|
53
|
+
for (let right = left + 1; right < list.length; right += 1) {
|
|
54
|
+
if ((list[left].top - list[right].top) * (list[left].bottom - list[right].bottom) < 0)
|
|
55
|
+
crossings += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return crossings;
|
|
60
|
+
}
|
|
61
|
+
export function order(seedLayers, chains, seed) {
|
|
62
|
+
// The first guess: the order the source's own reading suggests. A dummy has no such order and starts last in its
|
|
63
|
+
// layer; the median sweep is what moves it to the lane its edge crosses.
|
|
64
|
+
const layers = seedLayers.map((layer) => layer
|
|
65
|
+
.map((id, index) => ({ id, index }))
|
|
66
|
+
.sort((left, right) => (seed?.get(left.id) ?? Infinity) - (seed?.get(right.id) ?? Infinity) || left.index - right.index)
|
|
67
|
+
.map((entry) => entry.id));
|
|
68
|
+
const down = new Map();
|
|
69
|
+
const up = new Map();
|
|
70
|
+
for (const chain of chains) {
|
|
71
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
72
|
+
const top = chain.path[cursor];
|
|
73
|
+
const bottom = chain.path[cursor + 1];
|
|
74
|
+
down.set(top, [...(down.get(top) ?? []), bottom]);
|
|
75
|
+
up.set(bottom, [...(up.get(bottom) ?? []), top]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let positions = locate(layers);
|
|
79
|
+
for (let round = 0; round < 4; round += 1) {
|
|
80
|
+
for (let depth = 1; depth < layers.length; depth += 1) {
|
|
81
|
+
const layer = layers[depth];
|
|
82
|
+
const sorted = layer
|
|
83
|
+
.map((id, index) => ({ id, index, median: medianOf(id, up, depth - 1, positions) }))
|
|
84
|
+
.sort((left, right) => left.median - right.median || left.index - right.index)
|
|
85
|
+
.map((entry) => entry.id);
|
|
86
|
+
layers[depth] = sorted;
|
|
87
|
+
positions = locate(layers);
|
|
88
|
+
}
|
|
89
|
+
for (let depth = layers.length - 2; depth >= 0; depth -= 1) {
|
|
90
|
+
const layer = layers[depth];
|
|
91
|
+
const sorted = layer
|
|
92
|
+
.map((id, index) => ({ id, index, median: medianOf(id, down, depth + 1, positions) }))
|
|
93
|
+
.sort((left, right) => left.median - right.median || left.index - right.index)
|
|
94
|
+
.map((entry) => entry.id);
|
|
95
|
+
layers[depth] = sorted;
|
|
96
|
+
positions = locate(layers);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let crossings = countCrossings(layers, chains);
|
|
100
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
101
|
+
let improved = false;
|
|
102
|
+
for (const layer of layers) {
|
|
103
|
+
for (let index = 0; index + 1 < layer.length; index += 1) {
|
|
104
|
+
const before = layer[index];
|
|
105
|
+
layer[index] = layer[index + 1];
|
|
106
|
+
layer[index + 1] = before;
|
|
107
|
+
const next = countCrossings(layers, chains);
|
|
108
|
+
if (next < crossings) {
|
|
109
|
+
crossings = next;
|
|
110
|
+
improved = true;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
layer[index + 1] = layer[index];
|
|
114
|
+
layer[index] = before;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!improved)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
return { layers, crossings };
|
|
122
|
+
}
|