@rse/gradia 1.4.2 → 1.4.4
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/dst/gradia-api-render-edge.js +116 -30
- package/dst/gradia-api-render-svg.js +118 -11
- package/dst/gradia-api-type-graph.js +12 -2
- package/dst/gradia-api.umd.js +3 -3
- package/package.json +9 -9
|
@@ -149,20 +149,43 @@ const fars = (u) => u.hairpin ? (u.mirror ? [u.posIn, u.posOut] : []) : [u.mirro
|
|
|
149
149
|
entering one if mirrored) cross the track-following segment of b,
|
|
150
150
|
and the near-side stubs of b cross the one of a (a stub touching
|
|
151
151
|
the end of a segment counts too, as it would overlap a corner) */
|
|
152
|
-
const trackCrossings = (a, b) => {
|
|
152
|
+
const trackCrossings = (a, b, farsA, nearsB) => {
|
|
153
153
|
const within = (u, pos) => pos >= Math.min(u.posIn, u.posOut) && pos <= Math.max(u.posIn, u.posOut);
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
let n = 0;
|
|
155
|
+
for (const pos of farsA)
|
|
156
|
+
if (within(b, pos))
|
|
157
|
+
n++;
|
|
158
|
+
for (const pos of nearsB)
|
|
159
|
+
if (within(a, pos))
|
|
160
|
+
n++;
|
|
161
|
+
return n;
|
|
156
162
|
};
|
|
157
163
|
/* the number of collinear overlaps caused by placing user a on a
|
|
158
164
|
nearer track than user b: a far-side stub of a and a near-side stub
|
|
159
165
|
of b at the very same cross-axis position run on top of each other
|
|
160
166
|
between the two tracks (unlike a crossing, which is still readable
|
|
161
167
|
through its hop, an overlap merges the two edges visually) */
|
|
162
|
-
const trackOverlaps = (
|
|
168
|
+
const trackOverlaps = (farsA, nearsB) => {
|
|
169
|
+
let n = 0;
|
|
170
|
+
for (const pos of farsA)
|
|
171
|
+
for (const near of nearsB)
|
|
172
|
+
if (near === pos) {
|
|
173
|
+
n++;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
return n;
|
|
177
|
+
};
|
|
163
178
|
/* the maximum number of users for which the crossing-minimal track
|
|
164
179
|
order is determined exactly (the effort doubles with every user) */
|
|
165
180
|
const TRACK_EXACT_MAX = 14;
|
|
181
|
+
/* the maximum number of users for which the local search tries blocks
|
|
182
|
+
of any length and restarts after every improving move (beyond it,
|
|
183
|
+
the search is bounded to blocks of at most TRACK_BLOCK_MAX users and
|
|
184
|
+
at most TRACK_PASSES_MAX passes, as the restarting search grows too
|
|
185
|
+
fast with hundreds of users) */
|
|
186
|
+
const TRACK_SEARCH_MAX = 100;
|
|
187
|
+
const TRACK_BLOCK_MAX = 8;
|
|
188
|
+
const TRACK_PASSES_MAX = 100;
|
|
166
189
|
/* reorder the seeded track users to the order causing the fewest
|
|
167
190
|
overlaps and then the fewest crossings, with ties broken toward the
|
|
168
191
|
seed order: for small user sets exactly, by a dynamic programming
|
|
@@ -176,8 +199,18 @@ const minimizeCrossings = (seed) => {
|
|
|
176
199
|
/* the weighted cost of placing seed user i before seed user j: the
|
|
177
200
|
overlaps dominate (one outweighs all crossings possible among the
|
|
178
201
|
users, at most three per pair), then the crossings, and finally
|
|
179
|
-
a seed order inversion breaks ties
|
|
180
|
-
|
|
202
|
+
a seed order inversion breaks ties (the near-side and far-side
|
|
203
|
+
stubs of every user are determined once, as the matrix grows
|
|
204
|
+
quadratically with the users, and the flat matrix is a plain
|
|
205
|
+
array, so its mostly small integers stay unboxed, unlike in a
|
|
206
|
+
"Float64Array", whose doubles slow down the local search) */
|
|
207
|
+
const nearsOf = seed.map(nears);
|
|
208
|
+
const farsOf = seed.map(fars);
|
|
209
|
+
const cost = [];
|
|
210
|
+
for (let i = 0; i < n; i++)
|
|
211
|
+
for (let j = 0; j < n; j++)
|
|
212
|
+
cost.push((trackOverlaps(farsOf[i], nearsOf[j]) * 2 * n * n +
|
|
213
|
+
trackCrossings(seed[i], seed[j], farsOf[i], nearsOf[j])) * n * n + (i > j ? 1 : 0));
|
|
181
214
|
let order;
|
|
182
215
|
if (n <= TRACK_EXACT_MAX) {
|
|
183
216
|
const best = new Float64Array(1 << n).fill(Infinity);
|
|
@@ -191,7 +224,7 @@ const minimizeCrossings = (seed) => {
|
|
|
191
224
|
let total = best[rest];
|
|
192
225
|
for (let v = 0; v < n; v++)
|
|
193
226
|
if ((rest & (1 << v)) !== 0)
|
|
194
|
-
total += cost[v
|
|
227
|
+
total += cost[v * n + u];
|
|
195
228
|
if (total < best[s]) {
|
|
196
229
|
best[s] = total;
|
|
197
230
|
last[s] = u;
|
|
@@ -210,31 +243,68 @@ const minimizeCrossings = (seed) => {
|
|
|
210
243
|
run), until no such move remains (a single-user move is
|
|
211
244
|
the block move of length one, while the longer blocks let
|
|
212
245
|
groups of users pass an obstacle which none of its users
|
|
213
|
-
could pass on its own)
|
|
246
|
+
could pass on its own); for large user sets the search is
|
|
247
|
+
bounded instead: per block, the best move over the runs
|
|
248
|
+
behind and ahead of it is applied, and the search continues
|
|
249
|
+
with the next block instead of restarting */
|
|
214
250
|
order = seed.map((_, i) => i);
|
|
215
|
-
|
|
251
|
+
/* precompute the cost change of swapping two users once:
|
|
252
|
+
diff[a * n + b] is the cost of placing user a before user b
|
|
253
|
+
minus the cost of the reverse placement */
|
|
254
|
+
const diff = [];
|
|
255
|
+
for (let a = 0; a < n; a++)
|
|
256
|
+
for (let b = 0; b < n; b++)
|
|
257
|
+
diff.push(cost[a * n + b] - cost[b * n + a]);
|
|
258
|
+
const bounded = n > TRACK_SEARCH_MAX;
|
|
259
|
+
const maxLen = bounded ? Math.min(n - 1, TRACK_BLOCK_MAX) : n - 1;
|
|
260
|
+
const passes = bounded ? TRACK_PASSES_MAX : Infinity;
|
|
261
|
+
for (let improved = true, pass = 0; improved && pass < passes; pass++) {
|
|
216
262
|
improved = false;
|
|
217
|
-
for (let len = 1; len
|
|
218
|
-
for (let i = 0; i + len <= n
|
|
263
|
+
for (let len = 1; len <= maxLen; len++) {
|
|
264
|
+
for (let i = 0; i + len <= n; i++) {
|
|
265
|
+
let best = 0;
|
|
266
|
+
let target = -1;
|
|
267
|
+
let forward = true;
|
|
219
268
|
let delta = 0;
|
|
220
|
-
for (let k = i + len; k < n
|
|
269
|
+
for (let k = i + len; k < n; k++) {
|
|
270
|
+
const row = order[k] * n;
|
|
221
271
|
for (let b = i; b < i + len; b++)
|
|
222
|
-
delta +=
|
|
223
|
-
if (delta <
|
|
224
|
-
|
|
225
|
-
|
|
272
|
+
delta += diff[row + order[b]];
|
|
273
|
+
if (delta < best) {
|
|
274
|
+
best = delta;
|
|
275
|
+
target = k;
|
|
276
|
+
forward = true;
|
|
277
|
+
if (!bounded)
|
|
278
|
+
break;
|
|
226
279
|
}
|
|
227
280
|
}
|
|
228
281
|
delta = 0;
|
|
229
|
-
for (let k = i - 1; k >= 0
|
|
282
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
283
|
+
if (target >= 0 && !bounded)
|
|
284
|
+
break;
|
|
285
|
+
const other = order[k];
|
|
230
286
|
for (let b = i; b < i + len; b++)
|
|
231
|
-
delta +=
|
|
232
|
-
if (delta <
|
|
233
|
-
|
|
234
|
-
|
|
287
|
+
delta += diff[order[b] * n + other];
|
|
288
|
+
if (delta < best) {
|
|
289
|
+
best = delta;
|
|
290
|
+
target = k;
|
|
291
|
+
forward = false;
|
|
292
|
+
if (!bounded)
|
|
293
|
+
break;
|
|
235
294
|
}
|
|
236
295
|
}
|
|
296
|
+
if (target >= 0) {
|
|
297
|
+
if (forward)
|
|
298
|
+
order.splice(target - len + 1, 0, ...order.splice(i, len));
|
|
299
|
+
else
|
|
300
|
+
order.splice(target, 0, ...order.splice(i, len));
|
|
301
|
+
improved = true;
|
|
302
|
+
if (!bounded)
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
237
305
|
}
|
|
306
|
+
if (improved && !bounded)
|
|
307
|
+
break;
|
|
238
308
|
}
|
|
239
309
|
}
|
|
240
310
|
}
|
|
@@ -298,9 +368,14 @@ export const computeHops = (polys) => {
|
|
|
298
368
|
const [c, d] = [poly[m], poly[m + 1]];
|
|
299
369
|
if (c[0] !== d[0])
|
|
300
370
|
continue;
|
|
301
|
-
verts.push({ owner: j, x: c[0], y1: Math.min(c[1], d[1]), y2: Math.max(c[1], d[1]) });
|
|
371
|
+
verts.push({ idx: verts.length, owner: j, x: c[0], y1: Math.min(c[1], d[1]), y2: Math.max(c[1], d[1]) });
|
|
302
372
|
}
|
|
303
373
|
});
|
|
374
|
+
/* sweep the vertical segments ordered by their position, so every
|
|
375
|
+
horizontal segment only visits the ones within its own span (found
|
|
376
|
+
by a binary search), and report the crossing ones in their original
|
|
377
|
+
order of collection */
|
|
378
|
+
const byX = verts.slice().sort((a, b) => a.x - b.x);
|
|
304
379
|
return polys.map((poly, i) => {
|
|
305
380
|
const result = new Map();
|
|
306
381
|
for (let k = 0; k < poly.length - 1; k++) {
|
|
@@ -308,14 +383,25 @@ export const computeHops = (polys) => {
|
|
|
308
383
|
if (a[1] !== b[1])
|
|
309
384
|
continue;
|
|
310
385
|
const hy = a[1];
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
386
|
+
const lo = Math.min(a[0], b[0]) + 1;
|
|
387
|
+
const hi = Math.max(a[0], b[0]) - 1;
|
|
388
|
+
let l = 0;
|
|
389
|
+
let h = byX.length;
|
|
390
|
+
while (l < h) {
|
|
391
|
+
const m = (l + h) >>> 1;
|
|
392
|
+
if (byX[m].x > lo)
|
|
393
|
+
h = m;
|
|
394
|
+
else
|
|
395
|
+
l = m + 1;
|
|
396
|
+
}
|
|
397
|
+
const found = [];
|
|
398
|
+
for (let m = l; m < byX.length && byX[m].x < hi; m++) {
|
|
399
|
+
const v = byX[m];
|
|
400
|
+
if (v.owner !== i && hy > v.y1 + 1 && hy < v.y2 - 1)
|
|
401
|
+
found.push(v);
|
|
402
|
+
}
|
|
403
|
+
if (found.length > 0)
|
|
404
|
+
result.set(k, found.sort((a, b) => a.idx - b.idx).map((v) => v.x));
|
|
319
405
|
}
|
|
320
406
|
return result;
|
|
321
407
|
});
|
|
@@ -43,6 +43,66 @@ const ARITY_BACK = 3;
|
|
|
43
43
|
segment of its route, as a long segment would otherwise offer far
|
|
44
44
|
more candidate positions than a placement can ever need */
|
|
45
45
|
const SLIDE_MAX = 16;
|
|
46
|
+
/* a uniform grid of buckets indexing areas, so a query only visits
|
|
47
|
+
the areas of the buckets it covers instead of all areas (an area
|
|
48
|
+
spanning several buckets is visited once only, as every visit
|
|
49
|
+
stamps it, and the bucket indices are clamped, so even far-off
|
|
50
|
+
areas stay indexed, merely sharing the outermost buckets) */
|
|
51
|
+
const BUCKET = 64;
|
|
52
|
+
const GRID_OFF = 1 << 20;
|
|
53
|
+
const GRID_SPAN = 2 * GRID_OFF;
|
|
54
|
+
const areaGrid = () => {
|
|
55
|
+
const areas = [];
|
|
56
|
+
const buckets = new Map();
|
|
57
|
+
let stamps = new Uint32Array(0);
|
|
58
|
+
let stamp = 0;
|
|
59
|
+
const bucketOf = (v) => Math.max(-GRID_OFF, Math.min(GRID_OFF - 1, Math.floor(v / BUCKET)));
|
|
60
|
+
/* index an area and return its identifier */
|
|
61
|
+
const add = (area) => {
|
|
62
|
+
const id = areas.length;
|
|
63
|
+
areas.push(area);
|
|
64
|
+
for (let by = bucketOf(area[1]); by <= bucketOf(area[3]); by++) {
|
|
65
|
+
for (let bx = bucketOf(area[0]); bx <= bucketOf(area[2]); bx++) {
|
|
66
|
+
const bucket = buckets.get(bx * GRID_SPAN + by);
|
|
67
|
+
if (bucket === undefined)
|
|
68
|
+
buckets.set(bx * GRID_SPAN + by, [id]);
|
|
69
|
+
else
|
|
70
|
+
bucket.push(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
};
|
|
75
|
+
/* drop an indexed area */
|
|
76
|
+
const remove = (id) => {
|
|
77
|
+
areas[id] = undefined;
|
|
78
|
+
};
|
|
79
|
+
/* visit every indexed area sharing a bucket with the queried area
|
|
80
|
+
once, until the visiting function signals to stop */
|
|
81
|
+
const visit = (x1, y1, x2, y2, fn) => {
|
|
82
|
+
if (stamps.length < areas.length) {
|
|
83
|
+
const grown = new Uint32Array(Math.max(areas.length, 2 * stamps.length));
|
|
84
|
+
grown.set(stamps);
|
|
85
|
+
stamps = grown;
|
|
86
|
+
}
|
|
87
|
+
stamp++;
|
|
88
|
+
for (let by = bucketOf(y1); by <= bucketOf(y2); by++) {
|
|
89
|
+
for (let bx = bucketOf(x1); bx <= bucketOf(x2); bx++) {
|
|
90
|
+
const bucket = buckets.get(bx * GRID_SPAN + by);
|
|
91
|
+
if (bucket === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
for (const id of bucket) {
|
|
94
|
+
if (stamps[id] === stamp)
|
|
95
|
+
continue;
|
|
96
|
+
stamps[id] = stamp;
|
|
97
|
+
const area = areas[id];
|
|
98
|
+
if (area !== undefined && fn(id, area))
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return { add, remove, visit };
|
|
105
|
+
};
|
|
46
106
|
/* track occupied areas (node boxes and already placed labels) to
|
|
47
107
|
let subsequent labels dodge into a collision-free position, plus the
|
|
48
108
|
areas merely reserved for the labels still to be placed, and the
|
|
@@ -77,6 +137,19 @@ const labelPlacer = (layout, hops, hopRadius, config) => {
|
|
|
77
137
|
return boxes;
|
|
78
138
|
});
|
|
79
139
|
const lineCount = lines.reduce((n, boxes) => n + boxes.length, 0);
|
|
140
|
+
/* index the edge line boxes, the occupied areas, and the
|
|
141
|
+
reserved areas, as the placement of every label queries them
|
|
142
|
+
for all of its candidate boxes */
|
|
143
|
+
const lineGrid = areaGrid();
|
|
144
|
+
const owner = [];
|
|
145
|
+
lines.forEach((boxes, i) => {
|
|
146
|
+
for (const o of boxes)
|
|
147
|
+
owner[lineGrid.add(o)] = i;
|
|
148
|
+
});
|
|
149
|
+
const occupiedGrid = areaGrid();
|
|
150
|
+
for (const o of occupied)
|
|
151
|
+
occupiedGrid.add(o);
|
|
152
|
+
const reservedGrid = areaGrid();
|
|
80
153
|
/* claim the candidate box colliding with the fewest occupied
|
|
81
154
|
areas and, among those, with the fewest edge lines (earlier
|
|
82
155
|
candidates win ties, so the first collision-free one is taken),
|
|
@@ -87,14 +160,41 @@ const labelPlacer = (layout, hops, hopRadius, config) => {
|
|
|
87
160
|
"dodge" unset weighs the lines not at all, as its candidates are
|
|
88
161
|
ordered by a proximity which matters more than a crossed line,
|
|
89
162
|
which the halo of the label keeps readable anyway */
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
163
|
+
const hit = (o, x1, y1, x2, y2) => x1 < o[2] && x2 > o[0] && y1 < o[3] && y2 > o[1];
|
|
164
|
+
const collisions = (grid, box) => {
|
|
165
|
+
let n = 0;
|
|
166
|
+
grid.visit(box[0], box[1], box[2], box[3], (_, o) => {
|
|
167
|
+
if (hit(o, box[0], box[1], box[2], box[3]))
|
|
168
|
+
n++;
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
return n;
|
|
172
|
+
};
|
|
173
|
+
/* count the edge line boxes a candidate box crosses, but stop
|
|
174
|
+
counting once the given limit is reached, as the candidate then
|
|
175
|
+
cannot win anymore (the own lines are tested against the box
|
|
176
|
+
itself, the foreign ones against the box grown by the clearance,
|
|
177
|
+
which hence also bounds the buckets to visit) */
|
|
178
|
+
const crossings = (box, edge, limit) => {
|
|
179
|
+
const [x1, y1, x2, y2] = [box[0] - LINE_GAP, box[1] - LINE_GAP, box[2] + LINE_GAP, box[3] + LINE_GAP];
|
|
180
|
+
let n = 0;
|
|
181
|
+
lineGrid.visit(x1, y1, x2, y2, (id, o) => {
|
|
182
|
+
if (owner[id] === edge ? hit(o, box[0], box[1], box[2], box[3]) : hit(o, x1, y1, x2, y2))
|
|
183
|
+
n++;
|
|
184
|
+
return n >= limit;
|
|
185
|
+
});
|
|
186
|
+
return n;
|
|
94
187
|
};
|
|
95
|
-
|
|
188
|
+
/* reserve an area, indexed under its very object, so the claim of
|
|
189
|
+
the label it is reserved for can release it again */
|
|
190
|
+
const reserved = new Map();
|
|
96
191
|
const reserve = (box) => {
|
|
97
|
-
|
|
192
|
+
const id = reservedGrid.add(box);
|
|
193
|
+
const ids = reserved.get(box);
|
|
194
|
+
if (ids === undefined)
|
|
195
|
+
reserved.set(box, [id]);
|
|
196
|
+
else
|
|
197
|
+
ids.push(id);
|
|
98
198
|
};
|
|
99
199
|
/* a claim counts the areas reserved for the labels still to come
|
|
100
200
|
like the occupied ones, so an early label yields the position a
|
|
@@ -103,15 +203,21 @@ const labelPlacer = (layout, hops, hopRadius, config) => {
|
|
|
103
203
|
released, as that is the very area it is about to take */
|
|
104
204
|
const claim = (candidates, edge, dodge = true, own) => {
|
|
105
205
|
if (own !== undefined) {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
206
|
+
const id = reserved.get(own)?.shift();
|
|
207
|
+
if (id !== undefined)
|
|
208
|
+
reservedGrid.remove(id);
|
|
109
209
|
}
|
|
110
210
|
let box = candidates[0];
|
|
111
211
|
let lowest = Infinity;
|
|
112
212
|
for (const c of candidates) {
|
|
113
|
-
|
|
114
|
-
|
|
213
|
+
/* a candidate whose collisions alone already reach the lowest
|
|
214
|
+
score is skipped, as its line crossings, of which there are
|
|
215
|
+
at most "lineCount", can never let it win anymore */
|
|
216
|
+
let score = (collisions(occupiedGrid, c) + collisions(reservedGrid, c)) * (lineCount + 1);
|
|
217
|
+
if (score >= lowest)
|
|
218
|
+
continue;
|
|
219
|
+
if (dodge)
|
|
220
|
+
score += crossings(c, edge, lowest - score);
|
|
115
221
|
if (score < lowest) {
|
|
116
222
|
box = c;
|
|
117
223
|
lowest = score;
|
|
@@ -120,6 +226,7 @@ const labelPlacer = (layout, hops, hopRadius, config) => {
|
|
|
120
226
|
break;
|
|
121
227
|
}
|
|
122
228
|
occupied.push(box);
|
|
229
|
+
occupiedGrid.add(box);
|
|
123
230
|
return box;
|
|
124
231
|
};
|
|
125
232
|
return { claim, reserve, occupied };
|
|
@@ -23,6 +23,7 @@ const LOOP_TOP = 52; /* detour of a self-loop above its node box */
|
|
|
23
23
|
/* node nudging constants */
|
|
24
24
|
const NUDGE_GAP = 24; /* min port distance of a nudged node from its gutter tracks */
|
|
25
25
|
const NUDGE_PASSES = 3; /* max number of node nudging passes */
|
|
26
|
+
const NUDGE_EDGES = 300; /* max number of edges for which nodes are nudged at all */
|
|
26
27
|
/* snap raw node positions onto a discrete grid by clustering the
|
|
27
28
|
distinct X coordinates into columns and Y coordinates into rows */
|
|
28
29
|
const snapToGrid = (nodes, rawX, rawY) => {
|
|
@@ -488,7 +489,11 @@ const routeEdges = (edges, plans, sides, boxW, boxH, cx, cy, grid, config, level
|
|
|
488
489
|
trackPre.gutY(plans[edge].guts[0], edge) : undefined, level.fixedPort);
|
|
489
490
|
const { chanX, gutY } = assignTrackCoords(edges, plans, portPos, grid, config);
|
|
490
491
|
const polys = routePolys(edges, plans, portPos, boxH, cy, chanX, gutY);
|
|
491
|
-
|
|
492
|
+
let count;
|
|
493
|
+
const hops = () => {
|
|
494
|
+
count ??= computeHops(polys).reduce((sum, segs) => sum + Array.from(segs.values()).reduce((n, xs) => n + xs.length, 0), 0);
|
|
495
|
+
return count;
|
|
496
|
+
};
|
|
492
497
|
return { portPos, gutY, polys, hops };
|
|
493
498
|
};
|
|
494
499
|
/* nudge nodes vertically off their row centers to straighten the
|
|
@@ -530,12 +535,17 @@ const nudgeNodes = (nodes, edges, plans, col, row, boxH, grid, nrows, dy, route)
|
|
|
530
535
|
&& row.get(edge.source) === row.get(edge.target));
|
|
531
536
|
const jogs = (routing) => edges.filter((_, i) => direct[i]
|
|
532
537
|
&& routing.portPos.get(`${i}:s`).y !== routing.portPos.get(`${i}:t`).y).length;
|
|
533
|
-
const better = (a, b) => a.hops < b.hops || (a.hops === b.hops && jogs(a) < jogs(b));
|
|
538
|
+
const better = (a, b) => a.hops() < b.hops() || (a.hops() === b.hops() && jogs(a) < jogs(b));
|
|
534
539
|
/* try, in a few passes, for every node the shifts aligning one of
|
|
535
540
|
its direct edges, and keep the best of the shifts improving the
|
|
536
541
|
routing (all shifts are relative to the current position, which
|
|
537
542
|
already includes the shifts of the previous passes) */
|
|
538
543
|
let current = route();
|
|
544
|
+
/* skip the nudging on large graphs, as every tried shift re-routes
|
|
545
|
+
all edges, so its effort grows with the node count times the
|
|
546
|
+
edge count, while it only straightens a few direct edges */
|
|
547
|
+
if (edges.length > NUDGE_EDGES)
|
|
548
|
+
return current;
|
|
539
549
|
for (let pass = 0; pass < NUDGE_PASSES; pass++) {
|
|
540
550
|
let changed = false;
|
|
541
551
|
for (const node of nodes) {
|