@mark1russell7/highways 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/build/index.d.ts +636 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2548 -0
- package/build/index.js.map +1 -0
- package/package.json +42 -3
- package/src/bench/properties.ts +103 -0
- package/src/bench/quality.ts +214 -0
- package/src/bench/run.ts +103 -0
- package/src/curves.ts +89 -0
- package/src/fixtures.ts +181 -0
- package/src/geom.ts +177 -0
- package/src/grid.ts +116 -0
- package/src/index.ts +14 -0
- package/src/model.ts +138 -0
- package/src/plugs.ts +75 -0
- package/src/routers/corners.ts +38 -0
- package/src/routers/index.ts +27 -0
- package/src/routers/plugs.ts +25 -0
- package/src/routers/sections.ts +236 -0
- package/src/routers/shared.ts +223 -0
- package/src/routers/straight.ts +37 -0
- package/src/routers/trunks.ts +134 -0
- package/src/sections.ts +82 -0
- package/src/segments.ts +250 -0
- package/src/visibility.ts +325 -0
- package/src/ways.ts +67 -0
- package/README.md +0 -3
package/build/index.js
ADDED
|
@@ -0,0 +1,2548 @@
|
|
|
1
|
+
// src/geom.ts
|
|
2
|
+
/** This function gives the distance between two points. */
|
|
3
|
+
function dist(a, b) {
|
|
4
|
+
const dx = a.x - b.x;
|
|
5
|
+
const dy = a.y - b.y;
|
|
6
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
7
|
+
}
|
|
8
|
+
/** This function gives the center of a box. */
|
|
9
|
+
function center(b) {
|
|
10
|
+
return {
|
|
11
|
+
x: b.x + b.w / 2,
|
|
12
|
+
y: b.y + b.h / 2
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** This function gives a box that is larger by `m` on each side. A negative `m` makes the box smaller. */
|
|
16
|
+
function inflate(b, m) {
|
|
17
|
+
return {
|
|
18
|
+
x: b.x - m,
|
|
19
|
+
y: b.y - m,
|
|
20
|
+
w: Math.max(0, b.w + 2 * m),
|
|
21
|
+
h: Math.max(0, b.h + 2 * m)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** This function gives the sum of the lengths of the segments of a polyline. */
|
|
25
|
+
function polylineLength(p) {
|
|
26
|
+
let s = 0;
|
|
27
|
+
for (let i = 1; i < p.length; i++) s += dist(p[i - 1], p[i]);
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
const orient = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
31
|
+
/**
|
|
32
|
+
* This function tells if the segments `ab` and `cd` cross at one point inside both of them.
|
|
33
|
+
* Segments that only touch at an end, or that are collinear, do not cross.
|
|
34
|
+
*/
|
|
35
|
+
function segmentsCross(a, b, c, d) {
|
|
36
|
+
const d1 = orient(c, d, a);
|
|
37
|
+
const d2 = orient(c, d, b);
|
|
38
|
+
const d3 = orient(a, b, c);
|
|
39
|
+
const d4 = orient(a, b, d);
|
|
40
|
+
return d1 * d2 < 0 && d3 * d4 < 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* This function tells if the segment `ab` goes through the inside of the box.
|
|
44
|
+
* The function clips the segment to the box (Liang-Barsky) and asks for a part longer than `eps`.
|
|
45
|
+
*/
|
|
46
|
+
function segmentHitsBox(a, b, box, eps = 1e-6) {
|
|
47
|
+
return segmentHitsRect(a, b, box.x, box.y, box.x + box.w, box.y + box.h, eps);
|
|
48
|
+
}
|
|
49
|
+
/** This function tells if the segment `ab` goes through the inside of the box from (`x0`, `y0`) to (`x1`, `y1`). */
|
|
50
|
+
function segmentHitsRect(a, b, x0, y0, x1, y1, eps = 1e-6) {
|
|
51
|
+
const dx = b.x - a.x;
|
|
52
|
+
const dy = b.y - a.y;
|
|
53
|
+
let t0 = 0;
|
|
54
|
+
let t1 = 1;
|
|
55
|
+
if (dx === 0) {
|
|
56
|
+
if (a.x <= x0 || a.x >= x1) return false;
|
|
57
|
+
} else {
|
|
58
|
+
const ta = (x0 - a.x) / dx;
|
|
59
|
+
const tb = (x1 - a.x) / dx;
|
|
60
|
+
t0 = Math.max(t0, Math.min(ta, tb));
|
|
61
|
+
t1 = Math.min(t1, Math.max(ta, tb));
|
|
62
|
+
if (t0 > t1) return false;
|
|
63
|
+
}
|
|
64
|
+
if (dy === 0) {
|
|
65
|
+
if (a.y <= y0 || a.y >= y1) return false;
|
|
66
|
+
} else {
|
|
67
|
+
const ta = (y0 - a.y) / dy;
|
|
68
|
+
const tb = (y1 - a.y) / dy;
|
|
69
|
+
t0 = Math.max(t0, Math.min(ta, tb));
|
|
70
|
+
t1 = Math.min(t1, Math.max(ta, tb));
|
|
71
|
+
if (t0 > t1) return false;
|
|
72
|
+
}
|
|
73
|
+
return (t1 - t0) * Math.sqrt(dx * dx + dy * dy) > eps;
|
|
74
|
+
}
|
|
75
|
+
/** This function gives the distance from a point to a box. The distance is 0 when the point is in the box. */
|
|
76
|
+
function pointBoxDistance(p, b) {
|
|
77
|
+
const qx = Math.max(b.x, Math.min(b.x + b.w, p.x));
|
|
78
|
+
const qy = Math.max(b.y, Math.min(b.y + b.h, p.y));
|
|
79
|
+
return Math.hypot(p.x - qx, p.y - qy);
|
|
80
|
+
}
|
|
81
|
+
/** This function gives the distance from a point to the segment `ab`. */
|
|
82
|
+
function pointSegmentDistance(p, a, b) {
|
|
83
|
+
const dx = b.x - a.x;
|
|
84
|
+
const dy = b.y - a.y;
|
|
85
|
+
const l2 = dx * dx + dy * dy;
|
|
86
|
+
const t = l2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2));
|
|
87
|
+
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
|
88
|
+
}
|
|
89
|
+
/** This function gives the distance from the segment `ab` to a box. The distance is 0 when they touch. */
|
|
90
|
+
function segmentBoxDistance(a, b, box) {
|
|
91
|
+
if (segmentHitsBox(a, b, box, 0) || pointBoxDistance(a, box) === 0 || pointBoxDistance(b, box) === 0) return 0;
|
|
92
|
+
const corners = [
|
|
93
|
+
{
|
|
94
|
+
x: box.x,
|
|
95
|
+
y: box.y
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
x: box.x + box.w,
|
|
99
|
+
y: box.y
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
x: box.x,
|
|
103
|
+
y: box.y + box.h
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
x: box.x + box.w,
|
|
107
|
+
y: box.y + box.h
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
let d = Math.min(pointBoxDistance(a, box), pointBoxDistance(b, box));
|
|
111
|
+
for (const c of corners) d = Math.min(d, pointSegmentDistance(c, a, b));
|
|
112
|
+
return d;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* This function gives the turn at each inner vertex of a polyline, in radians, from -π to π.
|
|
116
|
+
* A positive turn is counterclockwise in a frame with the y axis up. The function skips segments shorter than `eps`.
|
|
117
|
+
*/
|
|
118
|
+
function turns(p, eps = 1e-9) {
|
|
119
|
+
const q = [];
|
|
120
|
+
for (const v of p) if (q.length === 0 || dist(q[q.length - 1], v) > eps) q.push(v);
|
|
121
|
+
const out = [];
|
|
122
|
+
for (let i = 1; i + 1 < q.length; i++) {
|
|
123
|
+
const a = q[i - 1];
|
|
124
|
+
const b = q[i];
|
|
125
|
+
const c = q[i + 1];
|
|
126
|
+
const cr = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
|
|
127
|
+
const dt = (b.x - a.x) * (c.x - b.x) + (b.y - a.y) * (c.y - b.y);
|
|
128
|
+
out.push(Math.atan2(cr, dt));
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** This function counts the vertices of a polyline where the direction changes by more than `minTurn` radians. */
|
|
133
|
+
function bendCount(p, minTurn = .05) {
|
|
134
|
+
return turns(p).filter((t) => Math.abs(t) > minTurn).length;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* This function counts the inflections of a polyline: the changes of the sign of the turn.
|
|
138
|
+
* A turn smaller than `minTurn` radians does not count. Thus a straight part between two turns keeps their signs.
|
|
139
|
+
*/
|
|
140
|
+
function inflectionCount(p, minTurn = .001) {
|
|
141
|
+
let n = 0;
|
|
142
|
+
let last = 0;
|
|
143
|
+
for (const t of turns(p)) {
|
|
144
|
+
if (Math.abs(t) <= minTurn) continue;
|
|
145
|
+
const s = Math.sign(t);
|
|
146
|
+
if (last !== 0 && s !== last) n++;
|
|
147
|
+
last = s;
|
|
148
|
+
}
|
|
149
|
+
return n;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* This function gives the discrete Fréchet distance between two polylines (Eiter and Mannila, 1994).
|
|
153
|
+
* The cost is O(|p|·|q|) in time and O(|q|) in memory.
|
|
154
|
+
*/
|
|
155
|
+
function frechet(p, q) {
|
|
156
|
+
if (p.length === 0 || q.length === 0) return p.length === q.length ? 0 : Infinity;
|
|
157
|
+
let prev = new Float64Array(q.length);
|
|
158
|
+
let cur = new Float64Array(q.length);
|
|
159
|
+
for (let i = 0; i < p.length; i++) {
|
|
160
|
+
for (let j = 0; j < q.length; j++) {
|
|
161
|
+
const d = dist(p[i], q[j]);
|
|
162
|
+
if (i === 0 && j === 0) cur[j] = d;
|
|
163
|
+
else if (i === 0) cur[j] = Math.max(cur[j - 1], d);
|
|
164
|
+
else if (j === 0) cur[j] = Math.max(prev[j], d);
|
|
165
|
+
else cur[j] = Math.max(Math.min(prev[j], prev[j - 1], cur[j - 1]), d);
|
|
166
|
+
}
|
|
167
|
+
[prev, cur] = [cur, prev];
|
|
168
|
+
}
|
|
169
|
+
return prev[q.length - 1];
|
|
170
|
+
}
|
|
171
|
+
// src/grid.ts
|
|
172
|
+
/**
|
|
173
|
+
* A uniform grid of buckets over the plane. A box item goes into each bucket that the box touches.
|
|
174
|
+
* A segment item goes only into the buckets along the segment, so a long diagonal segment stays cheap.
|
|
175
|
+
* A query gives each item in the buckets that it touches, one time each.
|
|
176
|
+
*/
|
|
177
|
+
var Grid = class {
|
|
178
|
+
#cell;
|
|
179
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
180
|
+
#items = [];
|
|
181
|
+
#ids = /* @__PURE__ */ new Map();
|
|
182
|
+
#seen = /* @__PURE__ */ new Int32Array(16);
|
|
183
|
+
#stamp = 0;
|
|
184
|
+
/** This constructor makes an empty grid with square buckets of side `cell`. */
|
|
185
|
+
constructor(cell) {
|
|
186
|
+
this.#cell = Math.max(1e-9, cell);
|
|
187
|
+
}
|
|
188
|
+
#key(i, j) {
|
|
189
|
+
return (i + 1048576) * 2097152 + (j + 1048576);
|
|
190
|
+
}
|
|
191
|
+
#id(item) {
|
|
192
|
+
let id = this.#ids.get(item);
|
|
193
|
+
if (id === void 0) {
|
|
194
|
+
id = this.#items.length;
|
|
195
|
+
this.#ids.set(item, id);
|
|
196
|
+
this.#items.push(item);
|
|
197
|
+
if (id >= this.#seen.length) {
|
|
198
|
+
const seen = new Int32Array(this.#seen.length * 2);
|
|
199
|
+
seen.set(this.#seen);
|
|
200
|
+
this.#seen = seen;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return id;
|
|
204
|
+
}
|
|
205
|
+
#put(id, x0, y0, x1, y1) {
|
|
206
|
+
const c = this.#cell;
|
|
207
|
+
const i1 = Math.floor(x1 / c);
|
|
208
|
+
const j1 = Math.floor(y1 / c);
|
|
209
|
+
for (let i = Math.floor(x0 / c); i <= i1; i++) for (let j = Math.floor(y0 / c); j <= j1; j++) {
|
|
210
|
+
const key = this.#key(i, j);
|
|
211
|
+
const list = this.#buckets.get(key);
|
|
212
|
+
if (list === void 0) this.#buckets.set(key, [id]);
|
|
213
|
+
else if (list[list.length - 1] !== id) list.push(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
#take(stamp, x0, y0, x1, y1, visit) {
|
|
217
|
+
const c = this.#cell;
|
|
218
|
+
const i1 = Math.floor(x1 / c);
|
|
219
|
+
const j1 = Math.floor(y1 / c);
|
|
220
|
+
const seen = this.#seen;
|
|
221
|
+
for (let i = Math.floor(x0 / c); i <= i1; i++) for (let j = Math.floor(y0 / c); j <= j1; j++) {
|
|
222
|
+
const list = this.#buckets.get(this.#key(i, j));
|
|
223
|
+
if (list === void 0) continue;
|
|
224
|
+
for (const id of list) {
|
|
225
|
+
if (seen[id] === stamp) continue;
|
|
226
|
+
seen[id] = stamp;
|
|
227
|
+
visit(this.#items[id]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
#segment(a, b, margin, each) {
|
|
232
|
+
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
|
|
233
|
+
let px = a.x;
|
|
234
|
+
let py = a.y;
|
|
235
|
+
for (let k = 1; k <= n; k++) {
|
|
236
|
+
const qx = k === n ? b.x : a.x + (b.x - a.x) * k / n;
|
|
237
|
+
const qy = k === n ? b.y : a.y + (b.y - a.y) * k / n;
|
|
238
|
+
each(Math.min(px, qx) - margin, Math.min(py, qy) - margin, Math.max(px, qx) + margin, Math.max(py, qy) + margin);
|
|
239
|
+
px = qx;
|
|
240
|
+
py = qy;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** This method adds an item with its bounding box. */
|
|
244
|
+
add(item, b) {
|
|
245
|
+
this.#put(this.#id(item), b.x, b.y, b.x + b.w, b.y + b.h);
|
|
246
|
+
}
|
|
247
|
+
/** This method adds the segment `ab` as an item. */
|
|
248
|
+
addSegment(item, a, b) {
|
|
249
|
+
const id = this.#id(item);
|
|
250
|
+
this.#segment(a, b, 0, (x0, y0, x1, y1) => this.#put(id, x0, y0, x1, y1));
|
|
251
|
+
}
|
|
252
|
+
/** This method gives each item whose buckets touch the box to `visit`, one time each. */
|
|
253
|
+
query(b, visit) {
|
|
254
|
+
this.#take(++this.#stamp, b.x, b.y, b.x + b.w, b.y + b.h, visit);
|
|
255
|
+
}
|
|
256
|
+
/** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
|
|
257
|
+
querySegment(a, b, margin, visit) {
|
|
258
|
+
const stamp = ++this.#stamp;
|
|
259
|
+
this.#segment(a, b, margin, (x0, y0, x1, y1) => this.#take(stamp, x0, y0, x1, y1, visit));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
/** This function gives the bounding box of the segment `ab`. */
|
|
263
|
+
function segmentBounds(a, b) {
|
|
264
|
+
return {
|
|
265
|
+
x: Math.min(a.x, b.x),
|
|
266
|
+
y: Math.min(a.y, b.y),
|
|
267
|
+
w: Math.abs(a.x - b.x),
|
|
268
|
+
h: Math.abs(a.y - b.y)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// src/model.ts
|
|
272
|
+
/** This function gives a map from the id of each node to the node. */
|
|
273
|
+
function nodeIndex(g) {
|
|
274
|
+
const m = /* @__PURE__ */ new Map();
|
|
275
|
+
for (const n of g.nodes) m.set(n.id, n);
|
|
276
|
+
return m;
|
|
277
|
+
}
|
|
278
|
+
/** This function gives the absolute place of a fixed plug, or `undefined` for a free plug or no plug. */
|
|
279
|
+
function fixedPortPoint(node, end) {
|
|
280
|
+
if (end.port === void 0) return void 0;
|
|
281
|
+
const p = node.ports?.find((q) => q.id === end.port);
|
|
282
|
+
if (p?.at === void 0) return void 0;
|
|
283
|
+
return {
|
|
284
|
+
x: node.box.x + p.at.x,
|
|
285
|
+
y: node.box.y + p.at.y
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// src/fixtures.ts
|
|
289
|
+
/** This function gives a seeded random source with values in [0, 1) (mulberry32). */
|
|
290
|
+
function rng(seed) {
|
|
291
|
+
let a = seed >>> 0;
|
|
292
|
+
return () => {
|
|
293
|
+
a = a + 1831565813 >>> 0;
|
|
294
|
+
let t = a;
|
|
295
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
296
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
297
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** The fixtures of the benchmark, from 10 to 100 000 nodes. */
|
|
301
|
+
const FIXTURES = [
|
|
302
|
+
{
|
|
303
|
+
name: "tiny-uniform",
|
|
304
|
+
kind: "uniform",
|
|
305
|
+
nodes: 10,
|
|
306
|
+
edgesPerNode: 1.5,
|
|
307
|
+
seed: 1
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "small-clustered",
|
|
311
|
+
kind: "clustered",
|
|
312
|
+
nodes: 100,
|
|
313
|
+
edgesPerNode: 2,
|
|
314
|
+
seed: 2,
|
|
315
|
+
fixedPorts: .2
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "grid-400",
|
|
319
|
+
kind: "grid",
|
|
320
|
+
nodes: 400,
|
|
321
|
+
edgesPerNode: 2,
|
|
322
|
+
seed: 3
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "mid-uniform",
|
|
326
|
+
kind: "uniform",
|
|
327
|
+
nodes: 1e3,
|
|
328
|
+
edgesPerNode: 2,
|
|
329
|
+
seed: 4
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "mid-clustered",
|
|
333
|
+
kind: "clustered",
|
|
334
|
+
nodes: 1e3,
|
|
335
|
+
edgesPerNode: 3,
|
|
336
|
+
seed: 5,
|
|
337
|
+
fixedPorts: .2
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "mid-hubs",
|
|
341
|
+
kind: "hubs",
|
|
342
|
+
nodes: 1e3,
|
|
343
|
+
edgesPerNode: 2,
|
|
344
|
+
seed: 6
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "mid-fanout",
|
|
348
|
+
kind: "collections",
|
|
349
|
+
nodes: 1e3,
|
|
350
|
+
edgesPerNode: 2,
|
|
351
|
+
seed: 9
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "large-clustered",
|
|
355
|
+
kind: "clustered",
|
|
356
|
+
nodes: 1e4,
|
|
357
|
+
edgesPerNode: 3,
|
|
358
|
+
seed: 7
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "huge-clustered",
|
|
362
|
+
kind: "clustered",
|
|
363
|
+
nodes: 1e5,
|
|
364
|
+
edgesPerNode: 2,
|
|
365
|
+
seed: 8
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
const COLLECTIONS = [
|
|
369
|
+
"users",
|
|
370
|
+
"modes",
|
|
371
|
+
"speeds",
|
|
372
|
+
"scores",
|
|
373
|
+
"teams"
|
|
374
|
+
];
|
|
375
|
+
const SIDES = [
|
|
376
|
+
"north",
|
|
377
|
+
"east",
|
|
378
|
+
"south",
|
|
379
|
+
"west"
|
|
380
|
+
];
|
|
381
|
+
const GAP = 12;
|
|
382
|
+
const SPACING = 90;
|
|
383
|
+
/**
|
|
384
|
+
* This function makes the graph of a fixture. The same spec always gives the same graph.
|
|
385
|
+
* Node boxes do not overlap, and a gap of 12 units stays between them.
|
|
386
|
+
*/
|
|
387
|
+
function makeFixture(spec) {
|
|
388
|
+
const R = rng(spec.seed * 2654435761 + spec.nodes);
|
|
389
|
+
const gauss = () => Math.sqrt(-2 * Math.log(R() + 1e-12)) * Math.cos(2 * Math.PI * R());
|
|
390
|
+
const side = Math.sqrt(spec.nodes) * SPACING;
|
|
391
|
+
const grid = new Grid(SPACING);
|
|
392
|
+
const nodes = [];
|
|
393
|
+
const clusterOf = [];
|
|
394
|
+
const free = (b) => {
|
|
395
|
+
let ok = true;
|
|
396
|
+
const g = {
|
|
397
|
+
x: b.x - GAP,
|
|
398
|
+
y: b.y - GAP,
|
|
399
|
+
w: b.w + 24,
|
|
400
|
+
h: b.h + 24
|
|
401
|
+
};
|
|
402
|
+
grid.query(g, (o) => {
|
|
403
|
+
if (ok && o.x < g.x + g.w && g.x < o.x + o.w && o.y < g.y + g.h && g.y < o.y + o.h) ok = false;
|
|
404
|
+
});
|
|
405
|
+
return ok;
|
|
406
|
+
};
|
|
407
|
+
const centers = Array.from({ length: Math.max(3, Math.round(Math.sqrt(spec.nodes) / 3)) }, () => ({
|
|
408
|
+
x: side * (.1 + .8 * R()),
|
|
409
|
+
y: side * (.1 + .8 * R())
|
|
410
|
+
}));
|
|
411
|
+
const spread = side / (2.5 * Math.sqrt(centers.length));
|
|
412
|
+
const cols = Math.ceil(Math.sqrt(spec.nodes));
|
|
413
|
+
let tries = 0;
|
|
414
|
+
while (nodes.length < spec.nodes && tries++ < spec.nodes * 200) {
|
|
415
|
+
const w = 30 + Math.floor(R() * 30);
|
|
416
|
+
const h = 18 + Math.floor(R() * 12);
|
|
417
|
+
let cx;
|
|
418
|
+
let cy;
|
|
419
|
+
let cluster = -1;
|
|
420
|
+
if (spec.kind === "grid") {
|
|
421
|
+
const i = nodes.length;
|
|
422
|
+
cx = (i % cols + .5) * SPACING;
|
|
423
|
+
cy = (Math.floor(i / cols) + .5) * SPACING;
|
|
424
|
+
} else if (spec.kind === "clustered" || spec.kind === "collections") {
|
|
425
|
+
cluster = Math.floor(R() * centers.length);
|
|
426
|
+
const c = centers[cluster];
|
|
427
|
+
cx = c.x + gauss() * spread;
|
|
428
|
+
cy = c.y + gauss() * spread;
|
|
429
|
+
} else {
|
|
430
|
+
cx = R() * side;
|
|
431
|
+
cy = R() * side;
|
|
432
|
+
}
|
|
433
|
+
const box = {
|
|
434
|
+
x: Math.round(cx - w / 2),
|
|
435
|
+
y: Math.round(cy - h / 2),
|
|
436
|
+
w,
|
|
437
|
+
h
|
|
438
|
+
};
|
|
439
|
+
if (spec.kind !== "grid" && !free(box)) continue;
|
|
440
|
+
grid.add(box, box);
|
|
441
|
+
clusterOf.push(cluster);
|
|
442
|
+
const id = `n${nodes.length}`;
|
|
443
|
+
if (R() < (spec.fixedPorts ?? 0)) {
|
|
444
|
+
const s = SIDES[Math.floor(R() * 4)];
|
|
445
|
+
const t = .2 + .6 * R();
|
|
446
|
+
const at = s === "north" ? {
|
|
447
|
+
x: w * t,
|
|
448
|
+
y: 0
|
|
449
|
+
} : s === "south" ? {
|
|
450
|
+
x: w * t,
|
|
451
|
+
y: h
|
|
452
|
+
} : s === "west" ? {
|
|
453
|
+
x: 0,
|
|
454
|
+
y: h * t
|
|
455
|
+
} : {
|
|
456
|
+
x: w,
|
|
457
|
+
y: h * t
|
|
458
|
+
};
|
|
459
|
+
const ports = [{
|
|
460
|
+
id: "out",
|
|
461
|
+
at: {
|
|
462
|
+
x: Math.round(at.x),
|
|
463
|
+
y: Math.round(at.y)
|
|
464
|
+
}
|
|
465
|
+
}];
|
|
466
|
+
nodes.push({
|
|
467
|
+
id,
|
|
468
|
+
box,
|
|
469
|
+
ports
|
|
470
|
+
});
|
|
471
|
+
} else nodes.push({
|
|
472
|
+
id,
|
|
473
|
+
box
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
nodes,
|
|
478
|
+
edges: makeEdges(spec, nodes, clusterOf, R)
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function makeEdges(spec, nodes, clusterOf, R) {
|
|
482
|
+
const n = nodes.length;
|
|
483
|
+
if (n < 2) return [];
|
|
484
|
+
const locality = spec.locality ?? .65;
|
|
485
|
+
const near = new Grid(180);
|
|
486
|
+
nodes.forEach((v, i) => near.add(i, v.box));
|
|
487
|
+
const kindOf = spec.kind === "collections" ? clusterOf.map((c) => `${COLLECTIONS[c % COLLECTIONS.length]}-${c}`) : nodes.map(() => COLLECTIONS[Math.floor(R() * COLLECTIONS.length)]);
|
|
488
|
+
const pickFar = () => spec.kind === "hubs" ? Math.floor(n * R() ** 3) : Math.floor(R() * n);
|
|
489
|
+
const target = Math.round(n * spec.edgesPerNode);
|
|
490
|
+
const seen = /* @__PURE__ */ new Set();
|
|
491
|
+
const edges = [];
|
|
492
|
+
const add = (u, v) => {
|
|
493
|
+
const key = u < v ? `${u}-${v}` : `${v}-${u}`;
|
|
494
|
+
if (u === v || seen.has(key)) return;
|
|
495
|
+
seen.add(key);
|
|
496
|
+
const src = nodes[u];
|
|
497
|
+
edges.push({
|
|
498
|
+
id: `e${edges.length}`,
|
|
499
|
+
source: src.ports ? {
|
|
500
|
+
node: src.id,
|
|
501
|
+
port: "out"
|
|
502
|
+
} : { node: src.id },
|
|
503
|
+
target: { node: nodes[v].id },
|
|
504
|
+
data: { collection: kindOf[v] }
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
if (spec.kind === "collections") {
|
|
508
|
+
const members = /* @__PURE__ */ new Map();
|
|
509
|
+
clusterOf.forEach((c, i) => {
|
|
510
|
+
const list = members.get(c);
|
|
511
|
+
if (list) list.push(i);
|
|
512
|
+
else members.set(c, [i]);
|
|
513
|
+
});
|
|
514
|
+
const clusters = [...members.keys()].sort((a, c) => a - c);
|
|
515
|
+
let rounds = 0;
|
|
516
|
+
while (edges.length < target * .6 && clusters.length > 1 && rounds++ < target) {
|
|
517
|
+
const u = Math.floor(R() * n);
|
|
518
|
+
const others = clusters.filter((c) => c !== clusterOf[u]);
|
|
519
|
+
const pool = members.get(others[Math.floor(R() * others.length)]);
|
|
520
|
+
const fan = 4 + Math.floor(R() * 12);
|
|
521
|
+
for (let i = 0; i < fan; i++) add(u, pool[Math.floor(R() * pool.length)]);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
let tries = 0;
|
|
525
|
+
while (edges.length < target && tries++ < target * 30) {
|
|
526
|
+
const u = Math.floor(R() * n);
|
|
527
|
+
let v = -1;
|
|
528
|
+
if (R() < locality) {
|
|
529
|
+
const b = nodes[u].box;
|
|
530
|
+
const cand = [];
|
|
531
|
+
for (let r = SPACING; cand.length < 8 && r < 5760; r *= 2) {
|
|
532
|
+
cand.length = 0;
|
|
533
|
+
near.query({
|
|
534
|
+
x: b.x - r,
|
|
535
|
+
y: b.y - r,
|
|
536
|
+
w: b.w + 2 * r,
|
|
537
|
+
h: b.h + 2 * r
|
|
538
|
+
}, (i) => {
|
|
539
|
+
if (i !== u) cand.push(i);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
cand.sort((a, c) => a - c);
|
|
543
|
+
if (cand.length) v = cand[Math.floor(R() * cand.length)];
|
|
544
|
+
} else v = pickFar();
|
|
545
|
+
if (v >= 0) add(u, v);
|
|
546
|
+
}
|
|
547
|
+
return edges;
|
|
548
|
+
}
|
|
549
|
+
// src/plugs.ts
|
|
550
|
+
/** The outward normal of each side, in a frame with the y axis down. */
|
|
551
|
+
const SIDE_NORMAL = {
|
|
552
|
+
north: {
|
|
553
|
+
x: 0,
|
|
554
|
+
y: -1
|
|
555
|
+
},
|
|
556
|
+
east: {
|
|
557
|
+
x: 1,
|
|
558
|
+
y: 0
|
|
559
|
+
},
|
|
560
|
+
south: {
|
|
561
|
+
x: 0,
|
|
562
|
+
y: 1
|
|
563
|
+
},
|
|
564
|
+
west: {
|
|
565
|
+
x: -1,
|
|
566
|
+
y: 0
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
/** This function gives the side of the box that is nearest to the point. */
|
|
570
|
+
function nearestSide(box, p) {
|
|
571
|
+
const d = [
|
|
572
|
+
["north", Math.abs(p.y - box.y)],
|
|
573
|
+
["east", Math.abs(box.x + box.w - p.x)],
|
|
574
|
+
["south", Math.abs(box.y + box.h - p.y)],
|
|
575
|
+
["west", Math.abs(p.x - box.x)]
|
|
576
|
+
];
|
|
577
|
+
let best = d[0];
|
|
578
|
+
for (const x of d) if (x[1] < best[1]) best = x;
|
|
579
|
+
return best[0];
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* This function gives the side that faces `toward`: the side of the larger component of the direction from the center.
|
|
583
|
+
* Thus the angle between the normal of the side and that direction is 45° or less.
|
|
584
|
+
*/
|
|
585
|
+
function sideToward(box, toward) {
|
|
586
|
+
const c = center(box);
|
|
587
|
+
const dx = toward.x - c.x;
|
|
588
|
+
const dy = toward.y - c.y;
|
|
589
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "east" : "west";
|
|
590
|
+
return dy >= 0 ? "south" : "north";
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* This function gives the point on a side of the box that is nearest to the line from the center to `toward`.
|
|
594
|
+
* The point stays away from the corners by `inset`, or by a quarter of the side when the side is short.
|
|
595
|
+
*/
|
|
596
|
+
function pointOnSide(box, side, toward, inset = 6) {
|
|
597
|
+
const c = center(box);
|
|
598
|
+
const dx = toward.x - c.x;
|
|
599
|
+
const dy = toward.y - c.y;
|
|
600
|
+
const clamp = (v, lo, len) => {
|
|
601
|
+
const m = Math.min(inset, len / 4);
|
|
602
|
+
return Math.max(lo + m, Math.min(lo + len - m, v));
|
|
603
|
+
};
|
|
604
|
+
if (side === "east" || side === "west") {
|
|
605
|
+
const x = side === "east" ? box.x + box.w : box.x;
|
|
606
|
+
return {
|
|
607
|
+
x,
|
|
608
|
+
y: clamp(dx === 0 ? c.y : c.y + dy * (x - c.x) / dx, box.y, box.h)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const y = side === "south" ? box.y + box.h : box.y;
|
|
612
|
+
return {
|
|
613
|
+
x: clamp(dy === 0 ? c.x : c.x + dx * (y - c.y) / dy, box.x, box.w),
|
|
614
|
+
y
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* This function places the plug of one end of an edge. A fixed plug stays at its point. A plug with a side stays on that side.
|
|
619
|
+
* The router places a free plug on the side that faces `toward`.
|
|
620
|
+
*/
|
|
621
|
+
function placePlug(node, end, toward) {
|
|
622
|
+
const fixed = fixedPortPoint(node, end);
|
|
623
|
+
const port = end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port);
|
|
624
|
+
if (fixed) return {
|
|
625
|
+
point: fixed,
|
|
626
|
+
normal: SIDE_NORMAL[port?.side ?? nearestSide(node.box, fixed)]
|
|
627
|
+
};
|
|
628
|
+
const side = port?.side ?? sideToward(node.box, toward);
|
|
629
|
+
return {
|
|
630
|
+
point: pointOnSide(node.box, side, toward),
|
|
631
|
+
normal: SIDE_NORMAL[side]
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
// src/curves.ts
|
|
635
|
+
/**
|
|
636
|
+
* This function gives the number of inflections that the rule asks for a chord at the angle `angle` (radians).
|
|
637
|
+
* It gives 0 near an axis, 1 near a diagonal and 2 between them. The bands are `tol` wide on each side.
|
|
638
|
+
*/
|
|
639
|
+
function inflectionSector(angle, tol = 7 * Math.PI / 180) {
|
|
640
|
+
const q = Math.PI / 2;
|
|
641
|
+
const mod = (angle % q + q) % q;
|
|
642
|
+
if (mod < tol || q - mod < tol) return 0;
|
|
643
|
+
if (Math.abs(mod - q / 2) < tol) return 1;
|
|
644
|
+
return 2;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* This function draws the curve of the inflection rule from the plug `p0` to `target`.
|
|
648
|
+
* The curve leaves `p0` along `normal`. In the frame of the chord, its side offset f has f(0) = f(L) = 0 and f'(0) = tan θ.
|
|
649
|
+
* Here θ is the angle from the chord to the normal. A polynomial of degree 2, 3 or 4 gives 0, 1 or 2 inflections,
|
|
650
|
+
* at 1/2, or at 1/3 and 2/3, of the chord.
|
|
651
|
+
*/
|
|
652
|
+
function inflectionCurve(p0, normal, target, options = {}) {
|
|
653
|
+
const L = dist(p0, target);
|
|
654
|
+
if (L === 0) return [p0, target];
|
|
655
|
+
const ux = (target.x - p0.x) / L;
|
|
656
|
+
const uy = (target.y - p0.y) / L;
|
|
657
|
+
const k = inflectionSector(Math.atan2(uy, ux), options.tol);
|
|
658
|
+
const maxAngle = options.maxAngle ?? 1.35;
|
|
659
|
+
const th = -Math.atan2(normal.x * uy - normal.y * ux, normal.x * ux + normal.y * uy);
|
|
660
|
+
const d = Math.tan(Math.max(-maxAngle, Math.min(maxAngle, th)));
|
|
661
|
+
const f = k === 0 ? (s) => -d / L * s * s + d * s : k === 1 ? (s) => 12 * d / (L * L) * (s ** 3 / 6 - L * s * s / 4) + d * s : (s) => -36 * d / L ** 3 * (s ** 4 / 12 - L * s ** 3 / 6 + L * L * s * s / 9) + d * s;
|
|
662
|
+
const n = Math.max(2, options.samples ?? 24);
|
|
663
|
+
const out = [];
|
|
664
|
+
for (let i = 0; i <= n; i++) {
|
|
665
|
+
if (i === n) {
|
|
666
|
+
out.push(target);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
const s = L * i / n;
|
|
670
|
+
const o = f(s);
|
|
671
|
+
out.push({
|
|
672
|
+
x: p0.x + ux * s - uy * o,
|
|
673
|
+
y: p0.y + uy * s + ux * o
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
out[0] = p0;
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* This function rounds each inner vertex of a polyline with a quadratic Bézier curve.
|
|
681
|
+
* The curve starts and ends at most `radius` from the vertex, and at most half of each segment. The ends of the polyline stay.
|
|
682
|
+
*/
|
|
683
|
+
function roundCorners(p, radius, steps = 4) {
|
|
684
|
+
if (p.length < 3 || radius <= 0) return [...p];
|
|
685
|
+
const out = [p[0]];
|
|
686
|
+
for (let i = 1; i + 1 < p.length; i++) {
|
|
687
|
+
const a = p[i - 1];
|
|
688
|
+
const v = p[i];
|
|
689
|
+
const b = p[i + 1];
|
|
690
|
+
const la = dist(a, v);
|
|
691
|
+
const lb = dist(v, b);
|
|
692
|
+
const r = Math.min(radius, la / 2, lb / 2);
|
|
693
|
+
if (r <= 0) {
|
|
694
|
+
out.push(v);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const p1 = {
|
|
698
|
+
x: v.x + (a.x - v.x) * r / la,
|
|
699
|
+
y: v.y + (a.y - v.y) * r / la
|
|
700
|
+
};
|
|
701
|
+
const p2 = {
|
|
702
|
+
x: v.x + (b.x - v.x) * r / lb,
|
|
703
|
+
y: v.y + (b.y - v.y) * r / lb
|
|
704
|
+
};
|
|
705
|
+
for (let k = 0; k <= steps; k++) {
|
|
706
|
+
const t = k / steps;
|
|
707
|
+
const u = 1 - t;
|
|
708
|
+
out.push({
|
|
709
|
+
x: u * u * p1.x + 2 * u * t * v.x + t * t * p2.x,
|
|
710
|
+
y: u * u * p1.y + 2 * u * t * v.y + t * t * p2.y
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
out.push(p[p.length - 1]);
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
// src/visibility.ts
|
|
718
|
+
/** The nodes as obstacles, with a grid for fast tests. */
|
|
719
|
+
var Obstacles = class {
|
|
720
|
+
#grid;
|
|
721
|
+
/** This constructor indexes the boxes of the nodes. */
|
|
722
|
+
constructor(nodes, cell) {
|
|
723
|
+
this.#grid = new Grid(cell);
|
|
724
|
+
for (const n of nodes) this.#grid.add(n, n.box);
|
|
725
|
+
}
|
|
726
|
+
/** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
727
|
+
clear(a, b, pad, skip) {
|
|
728
|
+
let ok = true;
|
|
729
|
+
this.#grid.querySegment(a, b, pad, (n) => {
|
|
730
|
+
if (ok && !skip?.has(n)) {
|
|
731
|
+
const x = n.box;
|
|
732
|
+
if (segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad)) ok = false;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
return ok;
|
|
736
|
+
}
|
|
737
|
+
/** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
738
|
+
clearPath(p, pad, skip) {
|
|
739
|
+
for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
/** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
|
|
743
|
+
inside(p, pad, skip) {
|
|
744
|
+
let hit = false;
|
|
745
|
+
this.#grid.query({
|
|
746
|
+
x: p.x,
|
|
747
|
+
y: p.y,
|
|
748
|
+
w: 0,
|
|
749
|
+
h: 0
|
|
750
|
+
}, (n) => {
|
|
751
|
+
if (!hit && n !== skip) {
|
|
752
|
+
const b = n.box;
|
|
753
|
+
hit = p.x > b.x - pad && p.x < b.x + b.w + pad && p.y > b.y - pad && p.y < b.y + b.h + pad;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
return hit;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
|
|
761
|
+
* Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
|
|
762
|
+
* A path goes from corner to corner around the boxes.
|
|
763
|
+
* The obstacle test uses half of the margin, so a path keeps a clearance of at least half of the margin.
|
|
764
|
+
*/
|
|
765
|
+
var CornerGraph = class {
|
|
766
|
+
points = [];
|
|
767
|
+
#adj = [];
|
|
768
|
+
#len = [];
|
|
769
|
+
#obstacles;
|
|
770
|
+
#pointGrid;
|
|
771
|
+
#cell;
|
|
772
|
+
#pad;
|
|
773
|
+
#k;
|
|
774
|
+
#g = /* @__PURE__ */ new Float64Array(0);
|
|
775
|
+
#parent = /* @__PURE__ */ new Int32Array(0);
|
|
776
|
+
#open = /* @__PURE__ */ new Int32Array(0);
|
|
777
|
+
#closed = /* @__PURE__ */ new Int32Array(0);
|
|
778
|
+
#stamp = 0;
|
|
779
|
+
#tested = /* @__PURE__ */ new Int32Array(0);
|
|
780
|
+
#testStamp = 0;
|
|
781
|
+
/** This constructor makes the graph of the nodes. */
|
|
782
|
+
constructor(nodes, obstacles, options) {
|
|
783
|
+
this.#obstacles = obstacles;
|
|
784
|
+
this.#pad = options.margin / 2;
|
|
785
|
+
this.#k = options.k;
|
|
786
|
+
let size = 0;
|
|
787
|
+
for (const n of nodes) size += (n.box.w + n.box.h) / 2;
|
|
788
|
+
this.#cell = Math.max(1, size / Math.max(1, nodes.length) + 2 * options.margin);
|
|
789
|
+
this.#pointGrid = new Grid(this.#cell);
|
|
790
|
+
for (const n of nodes) {
|
|
791
|
+
const b = inflate(n.box, options.margin);
|
|
792
|
+
for (const c of [
|
|
793
|
+
{
|
|
794
|
+
x: b.x,
|
|
795
|
+
y: b.y
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
x: b.x + b.w,
|
|
799
|
+
y: b.y
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
x: b.x + b.w,
|
|
803
|
+
y: b.y + b.h
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
x: b.x,
|
|
807
|
+
y: b.y + b.h
|
|
808
|
+
}
|
|
809
|
+
]) if (!obstacles.inside(c, this.#pad)) this.#addPoint(c);
|
|
810
|
+
}
|
|
811
|
+
const spacing = (options.lattice ?? 3) * this.#cell;
|
|
812
|
+
if (spacing > 0 && nodes.length > 0) {
|
|
813
|
+
let x0 = Infinity;
|
|
814
|
+
let y0 = Infinity;
|
|
815
|
+
let x1 = -Infinity;
|
|
816
|
+
let y1 = -Infinity;
|
|
817
|
+
for (const n of nodes) {
|
|
818
|
+
x0 = Math.min(x0, n.box.x);
|
|
819
|
+
y0 = Math.min(y0, n.box.y);
|
|
820
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
821
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
822
|
+
}
|
|
823
|
+
for (let x = x0 - spacing / 2; x <= x1 + spacing; x += spacing) for (let y = y0 - spacing / 2; y <= y1 + spacing; y += spacing) {
|
|
824
|
+
const c = {
|
|
825
|
+
x,
|
|
826
|
+
y
|
|
827
|
+
};
|
|
828
|
+
if (!obstacles.inside(c, options.margin)) this.#addPoint(c);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const links = this.points.map(() => /* @__PURE__ */ new Set());
|
|
832
|
+
for (let i = 0; i < this.points.length; i++) for (const j of this.nearestVisible(this.points[i], this.#k, i)) {
|
|
833
|
+
links[i].add(j);
|
|
834
|
+
links[j].add(i);
|
|
835
|
+
}
|
|
836
|
+
links.forEach((s, i) => {
|
|
837
|
+
const adj = [...s].sort((a, b) => a - b);
|
|
838
|
+
this.#adj.push(adj);
|
|
839
|
+
this.#len.push(adj.map((j) => dist(this.points[i], this.points[j])));
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
#addPoint(c) {
|
|
843
|
+
this.#pointGrid.add(this.points.length, {
|
|
844
|
+
x: c.x,
|
|
845
|
+
y: c.y,
|
|
846
|
+
w: 0,
|
|
847
|
+
h: 0
|
|
848
|
+
});
|
|
849
|
+
this.points.push(c);
|
|
850
|
+
}
|
|
851
|
+
/** This method gives the number of links of the graph. */
|
|
852
|
+
get linkCount() {
|
|
853
|
+
let n = 0;
|
|
854
|
+
for (const a of this.#adj) n += a.length;
|
|
855
|
+
return n / 2;
|
|
856
|
+
}
|
|
857
|
+
/** This method gives the index of the corner nearest to `p`, seen or not, or -1 when the graph has no corner near it. */
|
|
858
|
+
nearest(p) {
|
|
859
|
+
for (let r = this.#cell; r <= this.#cell * 64; r *= 2) {
|
|
860
|
+
let best = -1;
|
|
861
|
+
let bestD = Infinity;
|
|
862
|
+
this.#pointGrid.query({
|
|
863
|
+
x: p.x - r,
|
|
864
|
+
y: p.y - r,
|
|
865
|
+
w: 2 * r,
|
|
866
|
+
h: 2 * r
|
|
867
|
+
}, (j) => {
|
|
868
|
+
const d = dist(p, this.points[j]);
|
|
869
|
+
if (d < bestD || d === bestD && j < best) {
|
|
870
|
+
bestD = d;
|
|
871
|
+
best = j;
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
if (best >= 0 && bestD <= r) return best;
|
|
875
|
+
}
|
|
876
|
+
return -1;
|
|
877
|
+
}
|
|
878
|
+
/** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
|
|
879
|
+
nearestVisible(p, k, self = -1) {
|
|
880
|
+
const out = [];
|
|
881
|
+
if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
|
|
882
|
+
const tested = this.#tested;
|
|
883
|
+
const stamp = ++this.#testStamp;
|
|
884
|
+
for (let r = this.#cell; r <= this.#cell * 8; r *= 2) {
|
|
885
|
+
const cands = [];
|
|
886
|
+
this.#pointGrid.query({
|
|
887
|
+
x: p.x - r,
|
|
888
|
+
y: p.y - r,
|
|
889
|
+
w: 2 * r,
|
|
890
|
+
h: 2 * r
|
|
891
|
+
}, (j) => {
|
|
892
|
+
if (j !== self && tested[j] !== stamp) cands.push([dist(p, this.points[j]), j]);
|
|
893
|
+
});
|
|
894
|
+
cands.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
895
|
+
for (const [d, j] of cands) {
|
|
896
|
+
if (d > r) break;
|
|
897
|
+
tested[j] = stamp;
|
|
898
|
+
if (this.#obstacles.clear(p, this.points[j], this.#pad)) out.push(j);
|
|
899
|
+
if (out.length >= k) return out;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return out;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* This method gives a shortest path from `from` to `to` in the graph (A*), with the two points as its ends.
|
|
906
|
+
* It gives `undefined` when the graph links no corner near `from` to a corner near `to`.
|
|
907
|
+
* The function `cost` can change the cost of the link from corner `i` to corner `j`, of length `d`.
|
|
908
|
+
* It must give at least `floor` × `d`, so that the estimate of A* stays below the true cost.
|
|
909
|
+
*/
|
|
910
|
+
path(from, to, cost, floor = 1) {
|
|
911
|
+
if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
|
|
912
|
+
const n = this.points.length;
|
|
913
|
+
if (this.#g.length < n) {
|
|
914
|
+
this.#g = new Float64Array(n);
|
|
915
|
+
this.#parent = new Int32Array(n);
|
|
916
|
+
this.#open = new Int32Array(n);
|
|
917
|
+
this.#closed = new Int32Array(n);
|
|
918
|
+
}
|
|
919
|
+
const stamp = ++this.#stamp;
|
|
920
|
+
const goal = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const j of this.nearestVisible(to, this.#k * 2)) goal.set(j, dist(this.points[j], to));
|
|
922
|
+
if (goal.size === 0) return void 0;
|
|
923
|
+
const heap = new MinHeap();
|
|
924
|
+
const h = (i) => floor * dist(this.points[i], to);
|
|
925
|
+
const relax = (j, g, parent) => {
|
|
926
|
+
if (this.#closed[j] === stamp) return;
|
|
927
|
+
if (this.#open[j] === stamp && this.#g[j] <= g) return;
|
|
928
|
+
this.#open[j] = stamp;
|
|
929
|
+
this.#g[j] = g;
|
|
930
|
+
this.#parent[j] = parent;
|
|
931
|
+
heap.push(g + h(j), j);
|
|
932
|
+
};
|
|
933
|
+
for (const j of this.nearestVisible(from, this.#k * 2)) relax(j, dist(from, this.points[j]), -1);
|
|
934
|
+
let best = Infinity;
|
|
935
|
+
let bestEnd = -1;
|
|
936
|
+
while (heap.size > 0) {
|
|
937
|
+
if (heap.topKey >= best) break;
|
|
938
|
+
const i = heap.pop();
|
|
939
|
+
if (this.#closed[i] === stamp) continue;
|
|
940
|
+
this.#closed[i] = stamp;
|
|
941
|
+
const gi = this.#g[i];
|
|
942
|
+
const tail = goal.get(i);
|
|
943
|
+
if (tail !== void 0 && gi + tail < best) {
|
|
944
|
+
best = gi + tail;
|
|
945
|
+
bestEnd = i;
|
|
946
|
+
}
|
|
947
|
+
const adj = this.#adj[i];
|
|
948
|
+
const len = this.#len[i];
|
|
949
|
+
for (let m = 0; m < adj.length; m++) {
|
|
950
|
+
const j = adj[m];
|
|
951
|
+
const d = len[m];
|
|
952
|
+
relax(j, gi + (cost ? cost(i, j, d) : d), i);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (bestEnd < 0) return void 0;
|
|
956
|
+
const out = [to];
|
|
957
|
+
for (let i = bestEnd; i >= 0; i = this.#parent[i]) out.push(this.points[i]);
|
|
958
|
+
out.push(from);
|
|
959
|
+
return out.reverse();
|
|
960
|
+
}
|
|
961
|
+
/** This method removes the vertices that a later vertex makes needless: from each vertex, it goes to the farthest vertex in view. */
|
|
962
|
+
pull(p, lookahead = 16) {
|
|
963
|
+
if (p.length < 3) return [...p];
|
|
964
|
+
const out = [p[0]];
|
|
965
|
+
let i = 0;
|
|
966
|
+
while (i < p.length - 1) {
|
|
967
|
+
let j = Math.min(p.length - 1, i + lookahead);
|
|
968
|
+
while (j > i + 1 && !this.#obstacles.clear(p[i], p[j], this.#pad)) j--;
|
|
969
|
+
out.push(p[j]);
|
|
970
|
+
i = j;
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
/** A binary min-heap of (key, value) pairs. Equal keys come out by the smaller value, so the order is deterministic. */
|
|
976
|
+
var MinHeap = class {
|
|
977
|
+
#k = [];
|
|
978
|
+
#v = [];
|
|
979
|
+
get size() {
|
|
980
|
+
return this.#k.length;
|
|
981
|
+
}
|
|
982
|
+
#less(a, b) {
|
|
983
|
+
return this.#k[a] < this.#k[b] || this.#k[a] === this.#k[b] && this.#v[a] < this.#v[b];
|
|
984
|
+
}
|
|
985
|
+
#swap(a, b) {
|
|
986
|
+
const k = this.#k[a];
|
|
987
|
+
this.#k[a] = this.#k[b];
|
|
988
|
+
this.#k[b] = k;
|
|
989
|
+
const v = this.#v[a];
|
|
990
|
+
this.#v[a] = this.#v[b];
|
|
991
|
+
this.#v[b] = v;
|
|
992
|
+
}
|
|
993
|
+
/** The smallest key. */
|
|
994
|
+
get topKey() {
|
|
995
|
+
return this.#k[0];
|
|
996
|
+
}
|
|
997
|
+
push(key, value) {
|
|
998
|
+
this.#k.push(key);
|
|
999
|
+
this.#v.push(value);
|
|
1000
|
+
let i = this.#k.length - 1;
|
|
1001
|
+
while (i > 0) {
|
|
1002
|
+
const p = i - 1 >> 1;
|
|
1003
|
+
if (!this.#less(i, p)) break;
|
|
1004
|
+
this.#swap(i, p);
|
|
1005
|
+
i = p;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/** This method removes the pair with the smallest key and gives its value. */
|
|
1009
|
+
pop() {
|
|
1010
|
+
const top = this.#v[0];
|
|
1011
|
+
const k = this.#k.pop();
|
|
1012
|
+
const v = this.#v.pop();
|
|
1013
|
+
if (this.#k.length > 0) {
|
|
1014
|
+
this.#k[0] = k;
|
|
1015
|
+
this.#v[0] = v;
|
|
1016
|
+
let i = 0;
|
|
1017
|
+
for (;;) {
|
|
1018
|
+
const l = 2 * i + 1;
|
|
1019
|
+
const r = l + 1;
|
|
1020
|
+
let m = i;
|
|
1021
|
+
if (l < this.#k.length && this.#less(l, m)) m = l;
|
|
1022
|
+
if (r < this.#k.length && this.#less(r, m)) m = r;
|
|
1023
|
+
if (m === i) break;
|
|
1024
|
+
this.#swap(i, m);
|
|
1025
|
+
i = m;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return top;
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
// src/ways.ts
|
|
1032
|
+
/**
|
|
1033
|
+
* The ways around the nodes of one graph. A way leaves a plug along its normal.
|
|
1034
|
+
* It goes a shortest way (A*) through the corner graph, without the needless corners. It enters the other plug along its normal.
|
|
1035
|
+
*/
|
|
1036
|
+
var WayFinder = class {
|
|
1037
|
+
obstacles;
|
|
1038
|
+
corners;
|
|
1039
|
+
margin;
|
|
1040
|
+
#smooth;
|
|
1041
|
+
/** This constructor makes the obstacles and the corner graph of the nodes. */
|
|
1042
|
+
constructor(nodes, options = {}) {
|
|
1043
|
+
this.margin = options.margin ?? 5;
|
|
1044
|
+
this.#smooth = options.smooth ?? false;
|
|
1045
|
+
this.obstacles = new Obstacles(nodes, 180);
|
|
1046
|
+
this.corners = new CornerGraph(nodes, this.obstacles, {
|
|
1047
|
+
margin: this.margin,
|
|
1048
|
+
k: options.k ?? 8
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/** This method gives the point at `margin` from the plug, along its normal. */
|
|
1052
|
+
out(p) {
|
|
1053
|
+
return {
|
|
1054
|
+
x: p.point.x + p.normal.x * this.margin,
|
|
1055
|
+
y: p.point.y + p.normal.y * this.margin
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/** This method gives a way from plug to plug. Without a way in the graph, it gives the straight line. */
|
|
1059
|
+
between(ps, pt, ends) {
|
|
1060
|
+
if (this.#smooth) {
|
|
1061
|
+
const curve = inflectionCurve(ps.point, ps.normal, pt.point);
|
|
1062
|
+
if (this.obstacles.clearPath(curve, this.margin / 2, ends)) return curve;
|
|
1063
|
+
}
|
|
1064
|
+
const way = this.corners.path(this.out(ps), this.out(pt));
|
|
1065
|
+
if (!way) return [ps.point, pt.point];
|
|
1066
|
+
const poly = [
|
|
1067
|
+
ps.point,
|
|
1068
|
+
...this.corners.pull(way),
|
|
1069
|
+
pt.point
|
|
1070
|
+
];
|
|
1071
|
+
if (this.#smooth) {
|
|
1072
|
+
const round = roundCorners(poly, this.margin);
|
|
1073
|
+
if (this.obstacles.clearPath(round, 0, ends)) return round;
|
|
1074
|
+
}
|
|
1075
|
+
return poly;
|
|
1076
|
+
}
|
|
1077
|
+
/** This method gives a way from a plug to a free point, or `undefined` when the graph has none. */
|
|
1078
|
+
fromPlug(ps, to) {
|
|
1079
|
+
const way = this.corners.path(this.out(ps), to);
|
|
1080
|
+
return way && [ps.point, ...this.corners.pull(way)];
|
|
1081
|
+
}
|
|
1082
|
+
/** This method gives a way from a free point to a plug, or `undefined` when the graph has none. */
|
|
1083
|
+
toPlug(from, pt) {
|
|
1084
|
+
const way = this.corners.path(from, this.out(pt));
|
|
1085
|
+
return way && [...this.corners.pull(way), pt.point];
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
// src/segments.ts
|
|
1089
|
+
const SAME = 1e-6;
|
|
1090
|
+
/** This function gives the index where `p` holds the points of `part` in a row, from `from` on, or -1. */
|
|
1091
|
+
function indexOf(p, part, from) {
|
|
1092
|
+
for (let i = from; i + part.length <= p.length; i++) {
|
|
1093
|
+
let ok = true;
|
|
1094
|
+
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME;
|
|
1095
|
+
if (ok) return i;
|
|
1096
|
+
}
|
|
1097
|
+
return -1;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* This function breaks each route into segments. The runs and the trunks of the bundles become shared segments.
|
|
1101
|
+
* The other parts of each route become private segments. The segments of a path, in order, give the points of its route again.
|
|
1102
|
+
*/
|
|
1103
|
+
function segmentsOf(result) {
|
|
1104
|
+
const shared = [...(result.runs ?? []).map((r) => ({
|
|
1105
|
+
id: r.id,
|
|
1106
|
+
points: r.points,
|
|
1107
|
+
edges: r.edges
|
|
1108
|
+
})), ...(result.bundles ?? []).map((b) => ({
|
|
1109
|
+
id: b.id,
|
|
1110
|
+
points: b.trunk,
|
|
1111
|
+
edges: b.edges
|
|
1112
|
+
}))].filter((s) => s.points.length >= 2);
|
|
1113
|
+
const onEdge = /* @__PURE__ */ new Map();
|
|
1114
|
+
for (const s of shared) s.edges.forEach((edge, lane) => {
|
|
1115
|
+
const list = onEdge.get(edge);
|
|
1116
|
+
if (list) list.push({
|
|
1117
|
+
id: s.id,
|
|
1118
|
+
points: s.points,
|
|
1119
|
+
lane
|
|
1120
|
+
});
|
|
1121
|
+
else onEdge.set(edge, [{
|
|
1122
|
+
id: s.id,
|
|
1123
|
+
points: s.points,
|
|
1124
|
+
lane
|
|
1125
|
+
}]);
|
|
1126
|
+
});
|
|
1127
|
+
const segments = shared.map((s) => ({
|
|
1128
|
+
id: s.id,
|
|
1129
|
+
points: s.points,
|
|
1130
|
+
edges: s.edges,
|
|
1131
|
+
count: s.edges.length
|
|
1132
|
+
}));
|
|
1133
|
+
const paths = [];
|
|
1134
|
+
for (const route of result.routes) {
|
|
1135
|
+
const p = route.points;
|
|
1136
|
+
const found = [];
|
|
1137
|
+
for (const s of onEdge.get(route.edge) ?? []) {
|
|
1138
|
+
let at = indexOf(p, s.points, 0);
|
|
1139
|
+
let reverse = false;
|
|
1140
|
+
if (at < 0) {
|
|
1141
|
+
at = indexOf(p, [...s.points].reverse(), 0);
|
|
1142
|
+
reverse = true;
|
|
1143
|
+
}
|
|
1144
|
+
if (at >= 0) found.push({
|
|
1145
|
+
at,
|
|
1146
|
+
end: at + s.points.length - 1,
|
|
1147
|
+
ref: {
|
|
1148
|
+
segment: s.id,
|
|
1149
|
+
reverse,
|
|
1150
|
+
lane: s.lane
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
found.sort((a, b) => a.at - b.at);
|
|
1155
|
+
const refs = [];
|
|
1156
|
+
let i = 0;
|
|
1157
|
+
const own = (to) => {
|
|
1158
|
+
if (to <= i) return;
|
|
1159
|
+
const id = `${route.edge}/${refs.length}`;
|
|
1160
|
+
segments.push({
|
|
1161
|
+
id,
|
|
1162
|
+
points: p.slice(i, to + 1),
|
|
1163
|
+
edges: [route.edge],
|
|
1164
|
+
count: 1
|
|
1165
|
+
});
|
|
1166
|
+
refs.push({
|
|
1167
|
+
segment: id,
|
|
1168
|
+
reverse: false,
|
|
1169
|
+
lane: 0
|
|
1170
|
+
});
|
|
1171
|
+
};
|
|
1172
|
+
for (const f of found) {
|
|
1173
|
+
if (f.at < i) continue;
|
|
1174
|
+
own(f.at);
|
|
1175
|
+
refs.push(f.ref);
|
|
1176
|
+
i = f.end;
|
|
1177
|
+
}
|
|
1178
|
+
own(p.length - 1);
|
|
1179
|
+
paths.push({
|
|
1180
|
+
edge: route.edge,
|
|
1181
|
+
segments: refs
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
segments,
|
|
1186
|
+
paths
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/** This function gives the points of a path, with the segments in the order and the way of the path. */
|
|
1190
|
+
function pathPoints(graph, path) {
|
|
1191
|
+
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
1192
|
+
const out = [];
|
|
1193
|
+
for (const ref of path.segments) {
|
|
1194
|
+
const s = byId.get(ref.segment);
|
|
1195
|
+
if (!s) continue;
|
|
1196
|
+
const pts = ref.reverse ? [...s.points].reverse() : s.points;
|
|
1197
|
+
for (const q of pts) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME) out.push(q);
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* This function moves each point of a polyline by `d` to the left, in a frame with the y axis down.
|
|
1203
|
+
* At an inner point, it moves along the bisector, at most 2 × `d` far.
|
|
1204
|
+
*/
|
|
1205
|
+
function offsetPolyline(p, d) {
|
|
1206
|
+
if (d === 0 || p.length < 2) return [...p];
|
|
1207
|
+
const normals = [];
|
|
1208
|
+
for (let i = 1; i < p.length; i++) {
|
|
1209
|
+
const dx = p[i].x - p[i - 1].x;
|
|
1210
|
+
const dy = p[i].y - p[i - 1].y;
|
|
1211
|
+
const l = Math.hypot(dx, dy);
|
|
1212
|
+
normals.push(l > 0 ? {
|
|
1213
|
+
x: dy / l,
|
|
1214
|
+
y: -dx / l
|
|
1215
|
+
} : normals[normals.length - 1] ?? {
|
|
1216
|
+
x: 0,
|
|
1217
|
+
y: 0
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
return p.map((q, i) => {
|
|
1221
|
+
const a = normals[Math.max(0, i - 1)];
|
|
1222
|
+
const b = normals[Math.min(normals.length - 1, i)];
|
|
1223
|
+
const mx = a.x + b.x;
|
|
1224
|
+
const my = a.y + b.y;
|
|
1225
|
+
const ml = Math.hypot(mx, my);
|
|
1226
|
+
if (ml < 1e-9) return {
|
|
1227
|
+
x: q.x + a.x * d,
|
|
1228
|
+
y: q.y + a.y * d
|
|
1229
|
+
};
|
|
1230
|
+
const cos = (mx * a.x + my * a.y) / ml;
|
|
1231
|
+
const k = Math.min(2, 1 / Math.max(cos, 1e-9)) * d;
|
|
1232
|
+
return {
|
|
1233
|
+
x: q.x + mx / ml * k,
|
|
1234
|
+
y: q.y + my / ml * k
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* This function pulls the lanes of each shared segment apart, and gives a route for each edge.
|
|
1240
|
+
* The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
|
|
1241
|
+
*/
|
|
1242
|
+
function pullApart(graph, options) {
|
|
1243
|
+
return lanePaths(graph, (s) => s.count > 1 ? Math.min(options.spacing, options.maxWidth / (s.count - 1)) : 0);
|
|
1244
|
+
}
|
|
1245
|
+
/** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
|
|
1246
|
+
function runKey(edges) {
|
|
1247
|
+
return [...edges].sort().join(",");
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
|
|
1251
|
+
* The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
|
|
1252
|
+
*
|
|
1253
|
+
* Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
|
|
1254
|
+
* The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
|
|
1255
|
+
* The key of a segment is `runKey` of its edges.
|
|
1256
|
+
*/
|
|
1257
|
+
function laneRoutes(source, open, laneWidth, maxWidth = Infinity) {
|
|
1258
|
+
const graph = "paths" in source ? source : segmentsOf(source);
|
|
1259
|
+
if (open.size === 0) return graph.paths.map((path) => ({
|
|
1260
|
+
edge: path.edge,
|
|
1261
|
+
points: pathPoints(graph, path)
|
|
1262
|
+
}));
|
|
1263
|
+
return lanePaths(graph, (s) => s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0);
|
|
1264
|
+
}
|
|
1265
|
+
/** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
|
|
1266
|
+
function lanePaths(graph, spacingOf) {
|
|
1267
|
+
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
1268
|
+
const routes = [];
|
|
1269
|
+
for (const path of graph.paths) {
|
|
1270
|
+
const ends = pathPoints(graph, path);
|
|
1271
|
+
const out = [];
|
|
1272
|
+
for (const ref of path.segments) {
|
|
1273
|
+
const s = byId.get(ref.segment);
|
|
1274
|
+
if (!s) continue;
|
|
1275
|
+
const spacing = spacingOf(s);
|
|
1276
|
+
const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
|
|
1277
|
+
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME) out.push(q);
|
|
1278
|
+
}
|
|
1279
|
+
if (out.length >= 2 && ends.length >= 2) {
|
|
1280
|
+
out[0] = ends[0];
|
|
1281
|
+
out[out.length - 1] = ends[ends.length - 1];
|
|
1282
|
+
}
|
|
1283
|
+
routes.push({
|
|
1284
|
+
edge: path.edge,
|
|
1285
|
+
points: out
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
return routes;
|
|
1289
|
+
}
|
|
1290
|
+
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
1291
|
+
function turn$1(from, base, to) {
|
|
1292
|
+
const dx = base.x - from.x;
|
|
1293
|
+
const dy = base.y - from.y;
|
|
1294
|
+
const vx = to.x - base.x;
|
|
1295
|
+
const vy = to.y - base.y;
|
|
1296
|
+
return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* This function gives the lane order of the edges on a shared way `run` (metro-line order). Two routes go on together
|
|
1300
|
+
* past the last point of the run until they part. The route that turns more to the left there gets the lane more to the left.
|
|
1301
|
+
* Two routes can part only before the first point. Then the route that comes from more to the left gets the lane more to the left.
|
|
1302
|
+
* A route that does not hold the run goes last.
|
|
1303
|
+
*/
|
|
1304
|
+
function orderLanes(run, routes) {
|
|
1305
|
+
const placed = [];
|
|
1306
|
+
const lost = [];
|
|
1307
|
+
for (const r of routes) {
|
|
1308
|
+
let at = indexOf(r.points, run, 0);
|
|
1309
|
+
if (at >= 0) placed.push({
|
|
1310
|
+
edge: r.edge,
|
|
1311
|
+
p: r.points,
|
|
1312
|
+
s: at,
|
|
1313
|
+
e: at + run.length - 1
|
|
1314
|
+
});
|
|
1315
|
+
else {
|
|
1316
|
+
const p = [...r.points].reverse();
|
|
1317
|
+
at = indexOf(p, run, 0);
|
|
1318
|
+
if (at >= 0) placed.push({
|
|
1319
|
+
edge: r.edge,
|
|
1320
|
+
p,
|
|
1321
|
+
s: at,
|
|
1322
|
+
e: at + run.length - 1
|
|
1323
|
+
});
|
|
1324
|
+
else lost.push(r.edge);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
const same = (a, b) => dist(a, b) <= SAME;
|
|
1328
|
+
placed.sort((P, Q) => {
|
|
1329
|
+
for (let j = 1;; j++) {
|
|
1330
|
+
const a = P.p[P.e + j];
|
|
1331
|
+
const b = Q.p[Q.e + j];
|
|
1332
|
+
if (a === void 0 || b === void 0) break;
|
|
1333
|
+
if (!same(a, b)) {
|
|
1334
|
+
const d = turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], a) - turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], b);
|
|
1335
|
+
if (d !== 0) return d;
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
for (let j = 1;; j++) {
|
|
1340
|
+
const a = P.p[P.s - j];
|
|
1341
|
+
const b = Q.p[Q.s - j];
|
|
1342
|
+
if (a === void 0 || b === void 0) break;
|
|
1343
|
+
if (!same(a, b)) {
|
|
1344
|
+
const d = turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], b) - turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], a);
|
|
1345
|
+
if (d !== 0) return d;
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
|
|
1350
|
+
});
|
|
1351
|
+
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
1352
|
+
}
|
|
1353
|
+
// src/sections.ts
|
|
1354
|
+
/**
|
|
1355
|
+
* This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
|
|
1356
|
+
* When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
|
|
1357
|
+
*/
|
|
1358
|
+
function buildSections(nodes, options = {}) {
|
|
1359
|
+
const cap = Math.max(1, options.cap ?? 8);
|
|
1360
|
+
const maxDepth = options.maxDepth ?? 16;
|
|
1361
|
+
const centers = nodes.map((n) => center(n.box));
|
|
1362
|
+
let x0 = Infinity;
|
|
1363
|
+
let y0 = Infinity;
|
|
1364
|
+
let x1 = -Infinity;
|
|
1365
|
+
let y1 = -Infinity;
|
|
1366
|
+
for (const n of nodes) {
|
|
1367
|
+
x0 = Math.min(x0, n.box.x);
|
|
1368
|
+
y0 = Math.min(y0, n.box.y);
|
|
1369
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
1370
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
1371
|
+
}
|
|
1372
|
+
const root = nodes.length ? {
|
|
1373
|
+
x: x0,
|
|
1374
|
+
y: y0,
|
|
1375
|
+
w: x1 - x0,
|
|
1376
|
+
h: y1 - y0
|
|
1377
|
+
} : {
|
|
1378
|
+
x: 0,
|
|
1379
|
+
y: 0,
|
|
1380
|
+
w: 0,
|
|
1381
|
+
h: 0
|
|
1382
|
+
};
|
|
1383
|
+
const make = (id, depth, cell, list) => {
|
|
1384
|
+
let box = cell;
|
|
1385
|
+
for (let guard = 0; list.length > cap && depth < maxDepth && guard < 32; guard++) {
|
|
1386
|
+
const mx = box.x + box.w / 2;
|
|
1387
|
+
const my = box.y + box.h / 2;
|
|
1388
|
+
const parts = [
|
|
1389
|
+
[],
|
|
1390
|
+
[],
|
|
1391
|
+
[],
|
|
1392
|
+
[]
|
|
1393
|
+
];
|
|
1394
|
+
for (const i of list) parts[(centers[i].x < mx ? 0 : 1) + (centers[i].y < my ? 0 : 2)].push(i);
|
|
1395
|
+
const quarter = (q) => ({
|
|
1396
|
+
x: q % 2 === 0 ? box.x : mx,
|
|
1397
|
+
y: q < 2 ? box.y : my,
|
|
1398
|
+
w: box.w / 2,
|
|
1399
|
+
h: box.h / 2
|
|
1400
|
+
});
|
|
1401
|
+
const full = parts.map((p, q) => [p, q]).filter(([p]) => p.length > 0);
|
|
1402
|
+
if (full.length === 1) {
|
|
1403
|
+
box = quarter(full[0][1]);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const children = full.map(([p, q]) => make(`${id}${q}`, depth + 1, quarter(q), p));
|
|
1407
|
+
return {
|
|
1408
|
+
id,
|
|
1409
|
+
depth,
|
|
1410
|
+
cell: box,
|
|
1411
|
+
nodes: list,
|
|
1412
|
+
children
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
id,
|
|
1417
|
+
depth,
|
|
1418
|
+
cell: box,
|
|
1419
|
+
nodes: list,
|
|
1420
|
+
children: []
|
|
1421
|
+
};
|
|
1422
|
+
};
|
|
1423
|
+
return make("s", 0, root, nodes.map((_, i) => i));
|
|
1424
|
+
}
|
|
1425
|
+
/** This function gives the sections of the tree, parents before children. */
|
|
1426
|
+
function allSections(root) {
|
|
1427
|
+
const out = [];
|
|
1428
|
+
const walk = (s) => {
|
|
1429
|
+
out.push(s);
|
|
1430
|
+
for (const c of s.children) walk(c);
|
|
1431
|
+
};
|
|
1432
|
+
walk(root);
|
|
1433
|
+
return out;
|
|
1434
|
+
}
|
|
1435
|
+
/** This function gives the depth of the deepest section. */
|
|
1436
|
+
function treeDepth(root) {
|
|
1437
|
+
return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
|
|
1438
|
+
}
|
|
1439
|
+
// src/routers/corners.ts
|
|
1440
|
+
/**
|
|
1441
|
+
* This function makes a router that goes around the nodes.
|
|
1442
|
+
* Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
|
|
1443
|
+
* of the box corners, without the needless corners. It enters the target plug along its normal.
|
|
1444
|
+
*/
|
|
1445
|
+
function cornersRouter(options) {
|
|
1446
|
+
const router = {
|
|
1447
|
+
name: options.name,
|
|
1448
|
+
route(graph) {
|
|
1449
|
+
const byId = nodeIndex(graph);
|
|
1450
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
1451
|
+
const routes = [];
|
|
1452
|
+
for (const e of graph.edges) {
|
|
1453
|
+
const s = byId.get(e.source.node);
|
|
1454
|
+
const t = byId.get(e.target.node);
|
|
1455
|
+
if (!s || !t) continue;
|
|
1456
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
1457
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
1458
|
+
routes.push({
|
|
1459
|
+
edge: e.id,
|
|
1460
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return { routes };
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
return options.maxNodes === void 0 ? router : {
|
|
1467
|
+
...router,
|
|
1468
|
+
maxNodes: options.maxNodes
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
// src/routers/plugs.ts
|
|
1472
|
+
/**
|
|
1473
|
+
* Plugs and the inflection rule, with no obstacles. Each edge leaves its source plug at the tangent of the side.
|
|
1474
|
+
* It goes to the target plug on the curve of the rule. This router measures the rule alone.
|
|
1475
|
+
*/
|
|
1476
|
+
const plugsRouter = {
|
|
1477
|
+
name: "plugs",
|
|
1478
|
+
route(graph) {
|
|
1479
|
+
const byId = nodeIndex(graph);
|
|
1480
|
+
const routes = [];
|
|
1481
|
+
for (const e of graph.edges) {
|
|
1482
|
+
const s = byId.get(e.source.node);
|
|
1483
|
+
const t = byId.get(e.target.node);
|
|
1484
|
+
if (!s || !t) continue;
|
|
1485
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
1486
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
1487
|
+
routes.push({
|
|
1488
|
+
edge: e.id,
|
|
1489
|
+
points: inflectionCurve(ps.point, ps.normal, pt.point)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
return { routes };
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
// src/routers/sections.ts
|
|
1496
|
+
const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
1497
|
+
/**
|
|
1498
|
+
* This function makes a router with a tree of sections. Each section has a hub: the free corner nearest to the center of its nodes.
|
|
1499
|
+
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
1500
|
+
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
1501
|
+
* The edges in one section of the lowest level go straight around the nodes. The result gives one zoom level for each depth of the tree.
|
|
1502
|
+
*/
|
|
1503
|
+
function sectionsRouter(options) {
|
|
1504
|
+
const router = {
|
|
1505
|
+
name: options.name,
|
|
1506
|
+
route(graph) {
|
|
1507
|
+
const stretch = options.stretch ?? 2;
|
|
1508
|
+
const byId = nodeIndex(graph);
|
|
1509
|
+
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
1510
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
1511
|
+
const corners = ways.corners;
|
|
1512
|
+
const root = buildSections(graph.nodes, options);
|
|
1513
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1514
|
+
const leafOf = [];
|
|
1515
|
+
for (const s of allSections(root)) {
|
|
1516
|
+
for (const c of s.children) parent.set(c, s);
|
|
1517
|
+
if (s.children.length === 0) for (const i of s.nodes) leafOf[i] = s;
|
|
1518
|
+
}
|
|
1519
|
+
const hubs = /* @__PURE__ */ new Map();
|
|
1520
|
+
const hub = (s) => {
|
|
1521
|
+
let h = hubs.get(s);
|
|
1522
|
+
if (!h) {
|
|
1523
|
+
let x = 0;
|
|
1524
|
+
let y = 0;
|
|
1525
|
+
for (const i of s.nodes) {
|
|
1526
|
+
const c = center(graph.nodes[i].box);
|
|
1527
|
+
x += c.x;
|
|
1528
|
+
y += c.y;
|
|
1529
|
+
}
|
|
1530
|
+
const c = {
|
|
1531
|
+
x: x / Math.max(1, s.nodes.length),
|
|
1532
|
+
y: y / Math.max(1, s.nodes.length)
|
|
1533
|
+
};
|
|
1534
|
+
const j = corners.nearest(c);
|
|
1535
|
+
h = j >= 0 ? corners.points[j] : c;
|
|
1536
|
+
hubs.set(s, h);
|
|
1537
|
+
}
|
|
1538
|
+
return h;
|
|
1539
|
+
};
|
|
1540
|
+
const up = (s) => {
|
|
1541
|
+
const out = [];
|
|
1542
|
+
for (let x = s; x; x = parent.get(x)) out.push(x);
|
|
1543
|
+
return out;
|
|
1544
|
+
};
|
|
1545
|
+
const pieces = /* @__PURE__ */ new Map();
|
|
1546
|
+
const users = /* @__PURE__ */ new Map();
|
|
1547
|
+
const piece = (ka, a, kb, b, used) => {
|
|
1548
|
+
const forward = ka <= kb;
|
|
1549
|
+
const key = forward ? `${ka}>${kb}` : `${kb}>${ka}`;
|
|
1550
|
+
let p = pieces.get(key);
|
|
1551
|
+
if (!p) {
|
|
1552
|
+
const [from, to] = forward ? [a, b] : [b, a];
|
|
1553
|
+
const way = corners.path(from, to);
|
|
1554
|
+
p = dedupe$1(way ? corners.pull(way) : [from, to]);
|
|
1555
|
+
pieces.set(key, p);
|
|
1556
|
+
}
|
|
1557
|
+
if (!used.includes(key)) used.push(key);
|
|
1558
|
+
return forward ? p : [...p].reverse();
|
|
1559
|
+
};
|
|
1560
|
+
const at = (p) => `${p.x},${p.y}`;
|
|
1561
|
+
const routes = [];
|
|
1562
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1563
|
+
for (const e of graph.edges) {
|
|
1564
|
+
const s = byId.get(e.source.node);
|
|
1565
|
+
const t = byId.get(e.target.node);
|
|
1566
|
+
const iu = indexOfNode.get(e.source.node);
|
|
1567
|
+
const iv = indexOfNode.get(e.target.node);
|
|
1568
|
+
if (!s || !t || iu === void 0 || iv === void 0) continue;
|
|
1569
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
1570
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
1571
|
+
const lu = leafOf[iu];
|
|
1572
|
+
const lv = leafOf[iv];
|
|
1573
|
+
if (lu === lv) {
|
|
1574
|
+
routes.push({
|
|
1575
|
+
edge: e.id,
|
|
1576
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
1577
|
+
});
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
const a = up(lu);
|
|
1581
|
+
const b = up(lv);
|
|
1582
|
+
const common = new Set(b);
|
|
1583
|
+
const lca = a.find((x) => common.has(x));
|
|
1584
|
+
const keep = options.hubs ?? Infinity;
|
|
1585
|
+
const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
1586
|
+
const chain = [...side(a), ...side(b).reverse()];
|
|
1587
|
+
const out = [ps.point];
|
|
1588
|
+
const mark = /* @__PURE__ */ new Map();
|
|
1589
|
+
const append = (p) => {
|
|
1590
|
+
for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
|
|
1591
|
+
};
|
|
1592
|
+
const used = [];
|
|
1593
|
+
const start = ways.out(ps);
|
|
1594
|
+
append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
|
|
1595
|
+
mark.set(chain[0], out.length - 1);
|
|
1596
|
+
for (let i = 1; i < chain.length; i++) {
|
|
1597
|
+
append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
|
|
1598
|
+
mark.set(chain[i], out.length - 1);
|
|
1599
|
+
}
|
|
1600
|
+
const end = ways.out(pt);
|
|
1601
|
+
append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
|
|
1602
|
+
append([pt.point]);
|
|
1603
|
+
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
|
|
1604
|
+
routes.push({
|
|
1605
|
+
edge: e.id,
|
|
1606
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
1607
|
+
});
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
for (const key of used) {
|
|
1611
|
+
const list = users.get(key);
|
|
1612
|
+
if (list) list.push(e.id);
|
|
1613
|
+
else users.set(key, [e.id]);
|
|
1614
|
+
}
|
|
1615
|
+
routes.push({
|
|
1616
|
+
edge: e.id,
|
|
1617
|
+
points: out
|
|
1618
|
+
});
|
|
1619
|
+
marks.set(e.id, mark);
|
|
1620
|
+
}
|
|
1621
|
+
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
1622
|
+
const runs = [];
|
|
1623
|
+
for (const [key, p] of pieces) {
|
|
1624
|
+
const list = users.get(key) ?? [];
|
|
1625
|
+
if (list.length < 2 || p.length < 2) continue;
|
|
1626
|
+
runs.push({
|
|
1627
|
+
id: `r${runs.length}`,
|
|
1628
|
+
key,
|
|
1629
|
+
points: p,
|
|
1630
|
+
edges: orderLanes(p, list.map((id) => routeOf.get(id)))
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
routes,
|
|
1635
|
+
runs,
|
|
1636
|
+
levels: makeLevels(graph, root, routeOf, marks)
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
return options.maxNodes === void 0 ? router : {
|
|
1641
|
+
...router,
|
|
1642
|
+
maxNodes: options.maxNodes
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
1647
|
+
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
1648
|
+
* Its points are the part of the route of its first edge between the two items.
|
|
1649
|
+
*/
|
|
1650
|
+
function makeLevels(graph, root, routeOf, marks) {
|
|
1651
|
+
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
1652
|
+
const levels = [];
|
|
1653
|
+
const deepest = treeDepth(root);
|
|
1654
|
+
for (let d = 0; d <= deepest + 1; d++) {
|
|
1655
|
+
const items = [];
|
|
1656
|
+
const itemOf = [];
|
|
1657
|
+
const boxOf = (list) => {
|
|
1658
|
+
let x0 = Infinity;
|
|
1659
|
+
let y0 = Infinity;
|
|
1660
|
+
let x1 = -Infinity;
|
|
1661
|
+
let y1 = -Infinity;
|
|
1662
|
+
for (const i of list) {
|
|
1663
|
+
const b = graph.nodes[i].box;
|
|
1664
|
+
x0 = Math.min(x0, b.x);
|
|
1665
|
+
y0 = Math.min(y0, b.y);
|
|
1666
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
1667
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
x: x0,
|
|
1671
|
+
y: y0,
|
|
1672
|
+
w: x1 - x0,
|
|
1673
|
+
h: y1 - y0
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
const visit = (s) => {
|
|
1677
|
+
if (s.depth >= d) {
|
|
1678
|
+
items.push({
|
|
1679
|
+
id: s.id,
|
|
1680
|
+
box: boxOf(s.nodes),
|
|
1681
|
+
nodes: s.nodes.map((i) => graph.nodes[i].id)
|
|
1682
|
+
});
|
|
1683
|
+
for (const i of s.nodes) itemOf[i] = s;
|
|
1684
|
+
} else if (s.children.length > 0) for (const c of s.children) visit(c);
|
|
1685
|
+
else for (const i of s.nodes) {
|
|
1686
|
+
const n = graph.nodes[i];
|
|
1687
|
+
items.push({
|
|
1688
|
+
id: n.id,
|
|
1689
|
+
box: n.box,
|
|
1690
|
+
nodes: [n.id]
|
|
1691
|
+
});
|
|
1692
|
+
itemOf[i] = i;
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
if (graph.nodes.length > 0) visit(root);
|
|
1696
|
+
const links = /* @__PURE__ */ new Map();
|
|
1697
|
+
for (const e of graph.edges) {
|
|
1698
|
+
const iu = indexOfNode.get(e.source.node);
|
|
1699
|
+
const iv = indexOfNode.get(e.target.node);
|
|
1700
|
+
if (iu === void 0 || iv === void 0) continue;
|
|
1701
|
+
const a = itemOf[iu];
|
|
1702
|
+
const b = itemOf[iv];
|
|
1703
|
+
if (a === b) continue;
|
|
1704
|
+
const ida = typeof a === "number" ? graph.nodes[a].id : a.id;
|
|
1705
|
+
const idb = typeof b === "number" ? graph.nodes[b].id : b.id;
|
|
1706
|
+
const key = `${ida}\u0000${idb}`;
|
|
1707
|
+
const link = links.get(key);
|
|
1708
|
+
if (link) {
|
|
1709
|
+
link.count++;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
const route = routeOf.get(e.id)?.points ?? [];
|
|
1713
|
+
const mark = marks.get(e.id);
|
|
1714
|
+
const from = typeof a === "number" ? 0 : mark?.get(a) ?? 0;
|
|
1715
|
+
const to = typeof b === "number" ? route.length - 1 : mark?.get(b) ?? route.length - 1;
|
|
1716
|
+
links.set(key, {
|
|
1717
|
+
source: ida,
|
|
1718
|
+
target: idb,
|
|
1719
|
+
count: 1,
|
|
1720
|
+
points: route.slice(from, to + 1)
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
levels.push({
|
|
1724
|
+
depth: d,
|
|
1725
|
+
items,
|
|
1726
|
+
links: [...links.values()]
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
return levels;
|
|
1730
|
+
}
|
|
1731
|
+
// src/routers/shared.ts
|
|
1732
|
+
const byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1733
|
+
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
1734
|
+
function turn(from, base, to) {
|
|
1735
|
+
const dx = base.x - from.x;
|
|
1736
|
+
const dy = base.y - from.y;
|
|
1737
|
+
const vx = to.x - base.x;
|
|
1738
|
+
const vy = to.y - base.y;
|
|
1739
|
+
return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* This function makes a router where edges share their ways (after Pupyrev, Nachmanson, Bereg and Holroyd, "Edge routing with ordered bundles").
|
|
1743
|
+
* The edges go one by one through the corner graph. A link that an edge with the same key uses already costs less.
|
|
1744
|
+
* Thus the edges of a key come together on common ways. Each stretch that several edges share has the same points in each route.
|
|
1745
|
+
* The result gives each stretch as a run, with the order of its lanes.
|
|
1746
|
+
*/
|
|
1747
|
+
function sharedRouter(options) {
|
|
1748
|
+
const discount = Math.min(1, Math.max(.05, options.discount ?? .5));
|
|
1749
|
+
const keyOf = (e) => {
|
|
1750
|
+
if (!options.rule) return "*";
|
|
1751
|
+
return options.rule(e) ?? `\u0000${e.id}`;
|
|
1752
|
+
};
|
|
1753
|
+
const router = {
|
|
1754
|
+
name: options.name,
|
|
1755
|
+
route(graph) {
|
|
1756
|
+
const byId = nodeIndex(graph);
|
|
1757
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
1758
|
+
const corners = ways.corners;
|
|
1759
|
+
const index = /* @__PURE__ */ new Map();
|
|
1760
|
+
corners.points.forEach((p, i) => index.set(p, i));
|
|
1761
|
+
const M = corners.points.length + 1;
|
|
1762
|
+
const link = (i, j) => i < j ? i * M + j : j * M + i;
|
|
1763
|
+
const ends = /* @__PURE__ */ new Map();
|
|
1764
|
+
const endId = (p) => {
|
|
1765
|
+
const k = `${p.x},${p.y}`;
|
|
1766
|
+
let id = ends.get(k);
|
|
1767
|
+
if (id === void 0) ends.set(k, id = corners.points.length + ends.size);
|
|
1768
|
+
return id;
|
|
1769
|
+
};
|
|
1770
|
+
const all = [];
|
|
1771
|
+
for (const e of graph.edges) {
|
|
1772
|
+
const s = byId.get(e.source.node);
|
|
1773
|
+
const t = byId.get(e.target.node);
|
|
1774
|
+
if (!s || !t) continue;
|
|
1775
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
1776
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
1777
|
+
all.push({
|
|
1778
|
+
edge: e,
|
|
1779
|
+
key: keyOf(e),
|
|
1780
|
+
ps,
|
|
1781
|
+
pt,
|
|
1782
|
+
seq: [],
|
|
1783
|
+
ids: [],
|
|
1784
|
+
found: false
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
const discounted = /* @__PURE__ */ new Map();
|
|
1788
|
+
const order = [...all].sort((p, q) => byText(p.key, q.key) || dist(q.ps.point, q.pt.point) - dist(p.ps.point, p.pt.point) || byText(p.edge.id, q.edge.id));
|
|
1789
|
+
for (const w of order) {
|
|
1790
|
+
let used = discounted.get(w.key);
|
|
1791
|
+
if (!used) discounted.set(w.key, used = /* @__PURE__ */ new Map());
|
|
1792
|
+
const mine = used;
|
|
1793
|
+
const way = corners.path(ways.out(w.ps), ways.out(w.pt), (i, j, d) => mine.has(link(i, j)) ? d * discount : d, discount);
|
|
1794
|
+
if (!way) continue;
|
|
1795
|
+
w.found = true;
|
|
1796
|
+
w.seq = way;
|
|
1797
|
+
w.ids = way.map((p, i) => i === 0 || i === way.length - 1 ? endId(p) : index.get(p));
|
|
1798
|
+
for (let i = 1; i < w.ids.length; i++) {
|
|
1799
|
+
const a = w.ids[i - 1];
|
|
1800
|
+
const b = w.ids[i];
|
|
1801
|
+
if (a >= corners.points.length || b >= corners.points.length) continue;
|
|
1802
|
+
const list = mine.get(link(a, b));
|
|
1803
|
+
if (list) list.push(w.edge.id);
|
|
1804
|
+
else mine.set(link(a, b), [w.edge.id]);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const users = /* @__PURE__ */ new Map();
|
|
1808
|
+
for (const w of order) {
|
|
1809
|
+
if (!w.found) continue;
|
|
1810
|
+
let mine = users.get(w.key);
|
|
1811
|
+
if (!mine) users.set(w.key, mine = /* @__PURE__ */ new Map());
|
|
1812
|
+
for (let i = 1; i < w.ids.length; i++) {
|
|
1813
|
+
const a = w.ids[i - 1];
|
|
1814
|
+
const b = w.ids[i];
|
|
1815
|
+
const k = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
1816
|
+
const list = mine.get(k);
|
|
1817
|
+
if (list) list.push(w.edge.id);
|
|
1818
|
+
else mine.set(k, [w.edge.id]);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1822
|
+
const points = /* @__PURE__ */ new Map();
|
|
1823
|
+
for (const w of all) {
|
|
1824
|
+
if (!w.found) continue;
|
|
1825
|
+
const used = users.get(w.key);
|
|
1826
|
+
const sig = (i) => {
|
|
1827
|
+
const a = w.ids[i];
|
|
1828
|
+
const b = w.ids[i + 1];
|
|
1829
|
+
const list = used.get(a < b ? `${a}:${b}` : `${b}:${a}`);
|
|
1830
|
+
return list.length < 2 ? "" : [...list].sort(byText).join("");
|
|
1831
|
+
};
|
|
1832
|
+
const out = [w.ps.point];
|
|
1833
|
+
const append = (piece) => {
|
|
1834
|
+
for (const p of piece) if (dist(p, out[out.length - 1]) > 1e-9) out.push(p);
|
|
1835
|
+
};
|
|
1836
|
+
let start = 0;
|
|
1837
|
+
for (let i = 0; i < w.seq.length - 1; i++) {
|
|
1838
|
+
const s = sig(i);
|
|
1839
|
+
if (i + 1 < w.seq.length - 1 && sig(i + 1) === s) continue;
|
|
1840
|
+
const raw = w.seq.slice(start, i + 2);
|
|
1841
|
+
if (s === "") append(corners.pull(raw));
|
|
1842
|
+
else {
|
|
1843
|
+
const a = w.ids[start];
|
|
1844
|
+
const b = w.ids[i + 1];
|
|
1845
|
+
const id = `${s}\u0002${Math.min(a, b)}\u0002${Math.max(a, b)}`;
|
|
1846
|
+
let run = runs.get(id);
|
|
1847
|
+
if (!run) {
|
|
1848
|
+
run = {
|
|
1849
|
+
key: w.key,
|
|
1850
|
+
first: Math.min(a, b),
|
|
1851
|
+
last: Math.max(a, b),
|
|
1852
|
+
points: corners.pull(a < b ? raw : [...raw].reverse()),
|
|
1853
|
+
ways: []
|
|
1854
|
+
};
|
|
1855
|
+
runs.set(id, run);
|
|
1856
|
+
}
|
|
1857
|
+
run.ways.push(w);
|
|
1858
|
+
append(a < b ? run.points : [...run.points].reverse());
|
|
1859
|
+
}
|
|
1860
|
+
start = i + 1;
|
|
1861
|
+
}
|
|
1862
|
+
append([w.pt.point]);
|
|
1863
|
+
points.set(w.edge.id, out);
|
|
1864
|
+
}
|
|
1865
|
+
const routes = [];
|
|
1866
|
+
for (const w of all) routes.push({
|
|
1867
|
+
edge: w.edge.id,
|
|
1868
|
+
points: points.get(w.edge.id) ?? [w.ps.point, w.pt.point]
|
|
1869
|
+
});
|
|
1870
|
+
const shared = [];
|
|
1871
|
+
for (const run of runs.values()) shared.push({
|
|
1872
|
+
id: `r${shared.length}`,
|
|
1873
|
+
key: run.key,
|
|
1874
|
+
points: run.points,
|
|
1875
|
+
edges: laneOrderOfRun(run)
|
|
1876
|
+
});
|
|
1877
|
+
return {
|
|
1878
|
+
routes,
|
|
1879
|
+
runs: shared
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
return options.maxNodes === void 0 ? router : {
|
|
1884
|
+
...router,
|
|
1885
|
+
maxNodes: options.maxNodes
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* This function gives the lane order of a run (metro-line order). Two edges of the run go together past its last corner
|
|
1890
|
+
* until they part. The edge that turns more to the left there gets the lane more to the left.
|
|
1891
|
+
* Two edges can part only before the first corner. Then the edge that comes from more to the left gets the lane more to the left.
|
|
1892
|
+
*/
|
|
1893
|
+
function laneOrderOfRun(run) {
|
|
1894
|
+
const oriented = /* @__PURE__ */ new Map();
|
|
1895
|
+
for (const w of run.ways) {
|
|
1896
|
+
const s0 = w.ids.indexOf(run.first);
|
|
1897
|
+
const e0 = w.ids.indexOf(run.last);
|
|
1898
|
+
if (s0 < e0) oriented.set(w, {
|
|
1899
|
+
seq: w.seq,
|
|
1900
|
+
ids: w.ids,
|
|
1901
|
+
s: s0,
|
|
1902
|
+
e: e0
|
|
1903
|
+
});
|
|
1904
|
+
else oriented.set(w, {
|
|
1905
|
+
seq: [...w.seq].reverse(),
|
|
1906
|
+
ids: [...w.ids].reverse(),
|
|
1907
|
+
s: w.seq.length - 1 - s0,
|
|
1908
|
+
e: w.seq.length - 1 - e0
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
const compare = (p, q) => {
|
|
1912
|
+
const P = oriented.get(p);
|
|
1913
|
+
const Q = oriented.get(q);
|
|
1914
|
+
for (let j = 1;; j++) {
|
|
1915
|
+
const a = P.seq[P.e + j];
|
|
1916
|
+
const b = Q.seq[Q.e + j];
|
|
1917
|
+
if (a === void 0 || b === void 0) break;
|
|
1918
|
+
if (P.ids[P.e + j] !== Q.ids[Q.e + j]) {
|
|
1919
|
+
const d = turn(P.seq[P.e + j - 2], P.seq[P.e + j - 1], a) - turn(P.seq[P.e + j - 2], P.seq[P.e + j - 1], b);
|
|
1920
|
+
if (d !== 0) return d;
|
|
1921
|
+
break;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
for (let j = 1;; j++) {
|
|
1925
|
+
const a = P.seq[P.s - j];
|
|
1926
|
+
const b = Q.seq[Q.s - j];
|
|
1927
|
+
if (a === void 0 || b === void 0) break;
|
|
1928
|
+
if (P.ids[P.s - j] !== Q.ids[Q.s - j]) {
|
|
1929
|
+
const d = turn(P.seq[P.s - j + 2], P.seq[P.s - j + 1], b) - turn(P.seq[P.s - j + 2], P.seq[P.s - j + 1], a);
|
|
1930
|
+
if (d !== 0) return d;
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return byText(p.edge.id, q.edge.id);
|
|
1935
|
+
};
|
|
1936
|
+
return [...run.ways].sort(compare).map((w) => w.edge.id);
|
|
1937
|
+
}
|
|
1938
|
+
// src/routers/straight.ts
|
|
1939
|
+
/** This function gives the point where the ray from the center of the box to `toward` leaves the box. */
|
|
1940
|
+
function boxExit(box, toward) {
|
|
1941
|
+
const c = center(box);
|
|
1942
|
+
const dx = toward.x - c.x;
|
|
1943
|
+
const dy = toward.y - c.y;
|
|
1944
|
+
if (dx === 0 && dy === 0) return c;
|
|
1945
|
+
const tx = dx === 0 ? Infinity : box.w / 2 / Math.abs(dx);
|
|
1946
|
+
const ty = dy === 0 ? Infinity : box.h / 2 / Math.abs(dy);
|
|
1947
|
+
const t = Math.min(tx, ty);
|
|
1948
|
+
return {
|
|
1949
|
+
x: c.x + dx * t,
|
|
1950
|
+
y: c.y + dy * t
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* The control of the benchmark: each edge is one straight line between its plugs.
|
|
1955
|
+
* A free plug goes where the line between the two centers leaves the box.
|
|
1956
|
+
*/
|
|
1957
|
+
const straightRouter = {
|
|
1958
|
+
name: "straight",
|
|
1959
|
+
route(graph) {
|
|
1960
|
+
const byId = nodeIndex(graph);
|
|
1961
|
+
const routes = [];
|
|
1962
|
+
for (const e of graph.edges) {
|
|
1963
|
+
const s = byId.get(e.source.node);
|
|
1964
|
+
const t = byId.get(e.target.node);
|
|
1965
|
+
if (!s || !t) continue;
|
|
1966
|
+
const fs = fixedPortPoint(s, e.source);
|
|
1967
|
+
const ft = fixedPortPoint(t, e.target);
|
|
1968
|
+
const a = fs ?? boxExit(s.box, ft ?? center(t.box));
|
|
1969
|
+
const b = ft ?? boxExit(t.box, fs ?? center(s.box));
|
|
1970
|
+
routes.push({
|
|
1971
|
+
edge: e.id,
|
|
1972
|
+
points: [a, b]
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
return { routes };
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
// src/routers/trunks.ts
|
|
1979
|
+
/** The rule that groups the edges by the collection of their target, in `data.collection`. */
|
|
1980
|
+
const byCollection = (e) => {
|
|
1981
|
+
const c = e.data?.["collection"];
|
|
1982
|
+
return typeof c === "string" ? c : void 0;
|
|
1983
|
+
};
|
|
1984
|
+
/**
|
|
1985
|
+
* This function groups the edges by their source plug and their key. Each group keeps the order of the edges.
|
|
1986
|
+
* A group with fewer than `minSize` edges does not appear.
|
|
1987
|
+
*/
|
|
1988
|
+
function groupEdges(edges, rule, minSize = 2) {
|
|
1989
|
+
const all = /* @__PURE__ */ new Map();
|
|
1990
|
+
for (const e of edges) {
|
|
1991
|
+
const key = rule(e);
|
|
1992
|
+
if (key === void 0) continue;
|
|
1993
|
+
const id = `${e.source.node}\u0000${e.source.port ?? ""}\u0000${key}`;
|
|
1994
|
+
const list = all.get(id);
|
|
1995
|
+
if (list) list.push(e);
|
|
1996
|
+
else all.set(id, [e]);
|
|
1997
|
+
}
|
|
1998
|
+
for (const [id, list] of all) if (list.length < minSize) all.delete(id);
|
|
1999
|
+
return all;
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* This function gives the lane order of the branches that leave the split point `from`, with the trunk along `dir`.
|
|
2003
|
+
* Lane 0 turns most to the left, in a frame with the y axis down. Thus two lanes do not cross at the split.
|
|
2004
|
+
*/
|
|
2005
|
+
function laneOrder(dir, from, branches) {
|
|
2006
|
+
const angle = (p) => {
|
|
2007
|
+
const vx = p.x - from.x;
|
|
2008
|
+
const vy = p.y - from.y;
|
|
2009
|
+
return Math.atan2(dir.x * vy - dir.y * vx, dir.x * vx + dir.y * vy);
|
|
2010
|
+
};
|
|
2011
|
+
return branches.map((b) => ({
|
|
2012
|
+
edge: b.edge,
|
|
2013
|
+
a: angle(b.next)
|
|
2014
|
+
})).sort((p, q) => p.a - q.a || (p.edge < q.edge ? -1 : p.edge > q.edge ? 1 : 0)).map((x) => x.edge);
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* This function gives the split point of a bundle, on the line from `start` to the center of the targets.
|
|
2018
|
+
* The point is at `split` of the way, but at most half of the way to the nearest target. It moves back toward `start` out of the boxes.
|
|
2019
|
+
*/
|
|
2020
|
+
function splitPoint(start, targets, split, free, step) {
|
|
2021
|
+
if (targets.length === 0) return start;
|
|
2022
|
+
const c = {
|
|
2023
|
+
x: targets.reduce((s, p) => s + p.x, 0) / targets.length,
|
|
2024
|
+
y: targets.reduce((s, p) => s + p.y, 0) / targets.length
|
|
2025
|
+
};
|
|
2026
|
+
const L = dist(start, c);
|
|
2027
|
+
if (L === 0) return start;
|
|
2028
|
+
let d = Math.min(split * L, .5 * Math.min(...targets.map((p) => dist(start, p))));
|
|
2029
|
+
const at = (t) => ({
|
|
2030
|
+
x: start.x + (c.x - start.x) * t / L,
|
|
2031
|
+
y: start.y + (c.y - start.y) * t / L
|
|
2032
|
+
});
|
|
2033
|
+
while (d > 0 && !free(at(d))) d -= step;
|
|
2034
|
+
return d > 0 ? at(d) : start;
|
|
2035
|
+
}
|
|
2036
|
+
const dedupe = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
2037
|
+
/**
|
|
2038
|
+
* This function makes a router that bundles the edges by a rule. The edges of a group share one plug and one trunk
|
|
2039
|
+
* to a split point. From there, each edge goes its own way. The router gives the bundles with their lane order.
|
|
2040
|
+
* The edges outside the groups go as in the corner router.
|
|
2041
|
+
*/
|
|
2042
|
+
function trunksRouter(options) {
|
|
2043
|
+
const router = {
|
|
2044
|
+
name: options.name,
|
|
2045
|
+
route(graph) {
|
|
2046
|
+
const byId = nodeIndex(graph);
|
|
2047
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
2048
|
+
const points = /* @__PURE__ */ new Map();
|
|
2049
|
+
const bundles = [];
|
|
2050
|
+
for (const [id, group] of groupEdges(graph.edges, options.rule, options.minSize ?? 2)) {
|
|
2051
|
+
const first = group[0];
|
|
2052
|
+
const s = byId.get(first.source.node);
|
|
2053
|
+
const targets = group.map((e) => byId.get(e.target.node));
|
|
2054
|
+
if (!s || targets.some((t) => !t)) continue;
|
|
2055
|
+
const centers = targets.map((t) => center(t.box));
|
|
2056
|
+
const c = {
|
|
2057
|
+
x: centers.reduce((a, p) => a + p.x, 0) / centers.length,
|
|
2058
|
+
y: centers.reduce((a, p) => a + p.y, 0) / centers.length
|
|
2059
|
+
};
|
|
2060
|
+
const plug = placePlug(s, first.source, c);
|
|
2061
|
+
const split = splitPoint(ways.out(plug), centers, options.split ?? .5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
|
|
2062
|
+
const way = ways.fromPlug(plug, split);
|
|
2063
|
+
if (!way) continue;
|
|
2064
|
+
const trunk = dedupe(way);
|
|
2065
|
+
const end = trunk[trunk.length - 1];
|
|
2066
|
+
const prev = trunk.length >= 2 ? trunk[trunk.length - 2] : plug.point;
|
|
2067
|
+
const len = dist(prev, end);
|
|
2068
|
+
const dir = len > 0 ? {
|
|
2069
|
+
x: (end.x - prev.x) / len,
|
|
2070
|
+
y: (end.y - prev.y) / len
|
|
2071
|
+
} : plug.normal;
|
|
2072
|
+
const branches = [];
|
|
2073
|
+
group.forEach((e, i) => {
|
|
2074
|
+
const pt = placePlug(targets[i], e.target, end);
|
|
2075
|
+
const branch = dedupe(ways.toPlug(end, pt) ?? [end, pt.point]);
|
|
2076
|
+
points.set(e.id, dedupe([...trunk, ...branch.slice(1)]));
|
|
2077
|
+
branches.push({
|
|
2078
|
+
edge: e.id,
|
|
2079
|
+
next: branch[1] ?? pt.point
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
bundles.push({
|
|
2083
|
+
id: `b${bundles.length}`,
|
|
2084
|
+
key: id.split("\0")[2],
|
|
2085
|
+
node: s.id,
|
|
2086
|
+
edges: laneOrder(dir, end, branches),
|
|
2087
|
+
trunk
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
const routes = [];
|
|
2091
|
+
for (const e of graph.edges) {
|
|
2092
|
+
const s = byId.get(e.source.node);
|
|
2093
|
+
const t = byId.get(e.target.node);
|
|
2094
|
+
if (!s || !t) continue;
|
|
2095
|
+
const known = points.get(e.id);
|
|
2096
|
+
if (known) {
|
|
2097
|
+
routes.push({
|
|
2098
|
+
edge: e.id,
|
|
2099
|
+
points: known
|
|
2100
|
+
});
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
2104
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
2105
|
+
routes.push({
|
|
2106
|
+
edge: e.id,
|
|
2107
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
routes,
|
|
2112
|
+
bundles
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
return options.maxNodes === void 0 ? router : {
|
|
2117
|
+
...router,
|
|
2118
|
+
maxNodes: options.maxNodes
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
// src/routers/index.ts
|
|
2122
|
+
/** The routers that the benchmark races. */
|
|
2123
|
+
const ROUTERS = [
|
|
2124
|
+
straightRouter,
|
|
2125
|
+
plugsRouter,
|
|
2126
|
+
cornersRouter({
|
|
2127
|
+
name: "corners",
|
|
2128
|
+
maxNodes: 1e4
|
|
2129
|
+
}),
|
|
2130
|
+
cornersRouter({
|
|
2131
|
+
name: "corners-smooth",
|
|
2132
|
+
smooth: true,
|
|
2133
|
+
maxNodes: 1e4
|
|
2134
|
+
}),
|
|
2135
|
+
trunksRouter({
|
|
2136
|
+
name: "trunks",
|
|
2137
|
+
rule: byCollection,
|
|
2138
|
+
maxNodes: 1e4
|
|
2139
|
+
}),
|
|
2140
|
+
sharedRouter({
|
|
2141
|
+
name: "shared",
|
|
2142
|
+
rule: byCollection,
|
|
2143
|
+
maxNodes: 1e4
|
|
2144
|
+
}),
|
|
2145
|
+
sharedRouter({
|
|
2146
|
+
name: "shared-all",
|
|
2147
|
+
maxNodes: 1e4
|
|
2148
|
+
}),
|
|
2149
|
+
sectionsRouter({
|
|
2150
|
+
name: "sections",
|
|
2151
|
+
maxNodes: 1e5
|
|
2152
|
+
}),
|
|
2153
|
+
sectionsRouter({
|
|
2154
|
+
name: "sections-tree",
|
|
2155
|
+
stretch: Infinity,
|
|
2156
|
+
maxNodes: 1e5
|
|
2157
|
+
})
|
|
2158
|
+
];
|
|
2159
|
+
// src/bench/properties.ts
|
|
2160
|
+
/** The tolerance of the hard rules, in units of the plane. */
|
|
2161
|
+
const TOLERANCE = .5;
|
|
2162
|
+
/**
|
|
2163
|
+
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
2164
|
+
* Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
|
|
2165
|
+
*
|
|
2166
|
+
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
2167
|
+
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
|
2168
|
+
* The function gives at most `limit` violations.
|
|
2169
|
+
*/
|
|
2170
|
+
function checkProperties(graph, result, limit = 100) {
|
|
2171
|
+
const out = [];
|
|
2172
|
+
const add = (v) => {
|
|
2173
|
+
out.push(v);
|
|
2174
|
+
return out.length >= limit;
|
|
2175
|
+
};
|
|
2176
|
+
const byId = nodeIndex(graph);
|
|
2177
|
+
const grid = new Grid(180);
|
|
2178
|
+
for (const n of graph.nodes) grid.add(n, n.box);
|
|
2179
|
+
const routes = /* @__PURE__ */ new Map();
|
|
2180
|
+
for (const r of result.routes) routes.set(r.edge, r);
|
|
2181
|
+
for (const e of graph.edges) {
|
|
2182
|
+
const r = routes.get(e.id);
|
|
2183
|
+
const s = byId.get(e.source.node);
|
|
2184
|
+
const t = byId.get(e.target.node);
|
|
2185
|
+
if (!s || !t) continue;
|
|
2186
|
+
if (!r || r.points.length < 2) {
|
|
2187
|
+
if (add({
|
|
2188
|
+
rule: "complete",
|
|
2189
|
+
edge: e.id,
|
|
2190
|
+
detail: "no route"
|
|
2191
|
+
})) return out;
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
const a = r.points[0];
|
|
2195
|
+
const b = r.points[r.points.length - 1];
|
|
2196
|
+
if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
|
|
2197
|
+
if (add({
|
|
2198
|
+
rule: "ends-at-plugs",
|
|
2199
|
+
edge: e.id,
|
|
2200
|
+
detail: "an end of the route is away from its node"
|
|
2201
|
+
})) return out;
|
|
2202
|
+
}
|
|
2203
|
+
const fs = fixedPortPoint(s, e.source);
|
|
2204
|
+
const ft = fixedPortPoint(t, e.target);
|
|
2205
|
+
if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
|
|
2206
|
+
if (add({
|
|
2207
|
+
rule: "fixed-plugs",
|
|
2208
|
+
edge: e.id,
|
|
2209
|
+
detail: "the route does not start or end at its fixed plug"
|
|
2210
|
+
})) return out;
|
|
2211
|
+
}
|
|
2212
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
2213
|
+
const p = r.points[i - 1];
|
|
2214
|
+
const q = r.points[i];
|
|
2215
|
+
let hit;
|
|
2216
|
+
grid.querySegment(p, q, 0, (n) => {
|
|
2217
|
+
if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
|
|
2218
|
+
});
|
|
2219
|
+
if (hit) {
|
|
2220
|
+
if (add({
|
|
2221
|
+
rule: "no-pass-through",
|
|
2222
|
+
edge: e.id,
|
|
2223
|
+
detail: `segment ${i} goes through ${hit.id}`
|
|
2224
|
+
})) return out;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const bundled = /* @__PURE__ */ new Set();
|
|
2230
|
+
for (const b of result.bundles ?? []) for (const id of b.edges) {
|
|
2231
|
+
const r = routes.get(id);
|
|
2232
|
+
const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
|
|
2233
|
+
if (bundled.has(id) || !shares) {
|
|
2234
|
+
if (add({
|
|
2235
|
+
rule: "bundle-trunk",
|
|
2236
|
+
edge: id,
|
|
2237
|
+
detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
|
|
2238
|
+
})) return out;
|
|
2239
|
+
}
|
|
2240
|
+
bundled.add(id);
|
|
2241
|
+
}
|
|
2242
|
+
for (const run of result.runs ?? []) {
|
|
2243
|
+
if (run.edges.length < 2) {
|
|
2244
|
+
if (add({
|
|
2245
|
+
rule: "run-shared",
|
|
2246
|
+
detail: `run ${run.id} has fewer than two edges`
|
|
2247
|
+
})) return out;
|
|
2248
|
+
}
|
|
2249
|
+
for (const id of run.edges) {
|
|
2250
|
+
const r = routes.get(id);
|
|
2251
|
+
if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
|
|
2252
|
+
if (add({
|
|
2253
|
+
rule: "run-shared",
|
|
2254
|
+
edge: id,
|
|
2255
|
+
detail: `run ${run.id}: the route does not hold the points of the run in a row`
|
|
2256
|
+
})) return out;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
for (const level of result.levels ?? []) {
|
|
2261
|
+
const itemOf = /* @__PURE__ */ new Map();
|
|
2262
|
+
for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
|
|
2263
|
+
if (itemOf.size !== graph.nodes.length) {
|
|
2264
|
+
if (add({
|
|
2265
|
+
rule: "counts-sum",
|
|
2266
|
+
detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
|
|
2267
|
+
})) return out;
|
|
2268
|
+
}
|
|
2269
|
+
let between = 0;
|
|
2270
|
+
for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
|
|
2271
|
+
const sum = level.links.reduce((s, l) => s + l.count, 0);
|
|
2272
|
+
if (sum !== between) {
|
|
2273
|
+
if (add({
|
|
2274
|
+
rule: "counts-sum",
|
|
2275
|
+
detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
|
|
2276
|
+
})) return out;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
2282
|
+
function holds(p, part) {
|
|
2283
|
+
if (part.length === 0) return true;
|
|
2284
|
+
for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
// src/bench/quality.ts
|
|
2288
|
+
/** The clearance stops at this distance. */
|
|
2289
|
+
const CLEARANCE_CAP = 40;
|
|
2290
|
+
const TIGHT = 4;
|
|
2291
|
+
const EXACT_CROSSINGS = 2e4;
|
|
2292
|
+
const SAMPLE = 2e3;
|
|
2293
|
+
const key = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
|
|
2294
|
+
/** This function measures the quality of a result. */
|
|
2295
|
+
function measureQuality(graph, result) {
|
|
2296
|
+
const byId = nodeIndex(graph);
|
|
2297
|
+
const nodes = new Grid(180);
|
|
2298
|
+
for (const n of graph.nodes) nodes.add(n, n.box);
|
|
2299
|
+
const segs = [];
|
|
2300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2301
|
+
let bends = 0;
|
|
2302
|
+
let infl = 0;
|
|
2303
|
+
let clear = 0;
|
|
2304
|
+
let tight = 0;
|
|
2305
|
+
let detour = 0;
|
|
2306
|
+
let detourN = 0;
|
|
2307
|
+
let angle = 0;
|
|
2308
|
+
let angleN = 0;
|
|
2309
|
+
for (const r of result.routes) {
|
|
2310
|
+
const p = r.points;
|
|
2311
|
+
for (let i = 1; i < p.length; i++) {
|
|
2312
|
+
const ka = key(p[i - 1]);
|
|
2313
|
+
const kb = key(p[i]);
|
|
2314
|
+
if (ka === kb) continue;
|
|
2315
|
+
const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
2316
|
+
if (seen.has(k)) continue;
|
|
2317
|
+
seen.add(k);
|
|
2318
|
+
segs.push([p[i - 1], p[i]]);
|
|
2319
|
+
}
|
|
2320
|
+
bends += bendCount(p);
|
|
2321
|
+
infl += inflectionCount(p);
|
|
2322
|
+
const ends = endNodes(graph, byId, r.edge);
|
|
2323
|
+
let c = 40;
|
|
2324
|
+
for (let i = 1; i < p.length; i++) {
|
|
2325
|
+
const a = p[i - 1];
|
|
2326
|
+
const b = p[i];
|
|
2327
|
+
nodes.querySegment(a, b, c, (n) => {
|
|
2328
|
+
if (n !== ends.source && n !== ends.target) c = Math.min(c, segmentBoxDistance(a, b, n.box));
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
clear += c;
|
|
2332
|
+
if (c < TIGHT) tight++;
|
|
2333
|
+
if (p.length >= 2) {
|
|
2334
|
+
const d = dist(p[0], p[p.length - 1]);
|
|
2335
|
+
if (d > 1) {
|
|
2336
|
+
detour += polylineLength(p) / d;
|
|
2337
|
+
detourN++;
|
|
2338
|
+
}
|
|
2339
|
+
for (const [at, next, node] of [[
|
|
2340
|
+
p[0],
|
|
2341
|
+
p[1],
|
|
2342
|
+
ends.source
|
|
2343
|
+
], [
|
|
2344
|
+
p[p.length - 1],
|
|
2345
|
+
p[p.length - 2],
|
|
2346
|
+
ends.target
|
|
2347
|
+
]]) {
|
|
2348
|
+
if (!node) continue;
|
|
2349
|
+
const a = sideAngle(node.box, at, next);
|
|
2350
|
+
if (a !== void 0) {
|
|
2351
|
+
angle += a;
|
|
2352
|
+
angleN++;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const n = Math.max(1, result.routes.length);
|
|
2358
|
+
let ink = 0;
|
|
2359
|
+
for (const [a, b] of segs) ink += dist(a, b);
|
|
2360
|
+
const [crossings, crossingsEstimated] = countCrossings(segs);
|
|
2361
|
+
return {
|
|
2362
|
+
ink,
|
|
2363
|
+
crossings,
|
|
2364
|
+
crossingsEstimated,
|
|
2365
|
+
bends: bends / n,
|
|
2366
|
+
inflections: infl / n,
|
|
2367
|
+
clearance: clear / n,
|
|
2368
|
+
tight: tight / n,
|
|
2369
|
+
detour: detourN ? detour / detourN : 1,
|
|
2370
|
+
plugAngle: angleN ? angle / angleN : 0
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function endNodes(graph, byId, edgeId) {
|
|
2374
|
+
const e = edgeIndex(graph).get(edgeId);
|
|
2375
|
+
return {
|
|
2376
|
+
source: e && byId.get(e.source.node),
|
|
2377
|
+
target: e && byId.get(e.target.node)
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
const edgeIndexes = /* @__PURE__ */ new WeakMap();
|
|
2381
|
+
function edgeIndex(graph) {
|
|
2382
|
+
let m = edgeIndexes.get(graph);
|
|
2383
|
+
if (!m) {
|
|
2384
|
+
m = /* @__PURE__ */ new Map();
|
|
2385
|
+
for (const e of graph.edges) m.set(e.id, e);
|
|
2386
|
+
edgeIndexes.set(graph, m);
|
|
2387
|
+
}
|
|
2388
|
+
return m;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* This function gives an angle in degrees: the angle between the segment from `at` to `next` and the outward normal.
|
|
2392
|
+
* The normal is the normal of the side of the box nearest to `at`. A route that starts inside the box (an interior route) gives no angle.
|
|
2393
|
+
*/
|
|
2394
|
+
function sideAngle(box, at, next) {
|
|
2395
|
+
const d = [
|
|
2396
|
+
[
|
|
2397
|
+
Math.abs(at.y - box.y),
|
|
2398
|
+
0,
|
|
2399
|
+
-1
|
|
2400
|
+
],
|
|
2401
|
+
[
|
|
2402
|
+
Math.abs(at.x - (box.x + box.w)),
|
|
2403
|
+
1,
|
|
2404
|
+
0
|
|
2405
|
+
],
|
|
2406
|
+
[
|
|
2407
|
+
Math.abs(at.y - (box.y + box.h)),
|
|
2408
|
+
0,
|
|
2409
|
+
1
|
|
2410
|
+
],
|
|
2411
|
+
[
|
|
2412
|
+
Math.abs(at.x - box.x),
|
|
2413
|
+
-1,
|
|
2414
|
+
0
|
|
2415
|
+
]
|
|
2416
|
+
].sort((a, b) => a[0] - b[0])[0];
|
|
2417
|
+
if (d[0] > 1) return void 0;
|
|
2418
|
+
const vx = next.x - at.x;
|
|
2419
|
+
const vy = next.y - at.y;
|
|
2420
|
+
const l = Math.hypot(vx, vy);
|
|
2421
|
+
if (l === 0) return void 0;
|
|
2422
|
+
const cos = Math.max(-1, Math.min(1, (vx * d[1] + vy * d[2]) / l));
|
|
2423
|
+
return Math.acos(cos) * 180 / Math.PI;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* This function counts the crossings of a set of segments with a grid of buckets.
|
|
2427
|
+
* Above 20 000 segments, it counts the crossings of a seeded sample of 2 000 segments and scales the count.
|
|
2428
|
+
*/
|
|
2429
|
+
function countCrossings(segs) {
|
|
2430
|
+
if (segs.length < 2) return [0, false];
|
|
2431
|
+
const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
|
|
2432
|
+
const grid = new Grid(Math.max(20, len));
|
|
2433
|
+
segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
|
|
2434
|
+
const exact = segs.length <= EXACT_CROSSINGS;
|
|
2435
|
+
const R = rng(segs.length);
|
|
2436
|
+
const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
|
|
2437
|
+
let count = 0;
|
|
2438
|
+
for (const i of pick) {
|
|
2439
|
+
const [a, b] = segs[i];
|
|
2440
|
+
grid.querySegment(a, b, 0, (j) => {
|
|
2441
|
+
if (exact && j <= i) return;
|
|
2442
|
+
if (j === i) return;
|
|
2443
|
+
const [c, d] = segs[j];
|
|
2444
|
+
if (segmentsCross(a, b, c, d)) count++;
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
return exact ? [count, false] : [Math.round(count * segs.length / (2 * pick.length)), true];
|
|
2448
|
+
}
|
|
2449
|
+
/** This function measures how much the routes changed between two results. */
|
|
2450
|
+
function measureStability(before, after) {
|
|
2451
|
+
const prev = new Map(before.routes.map((r) => [r.edge, r.points]));
|
|
2452
|
+
let sum = 0;
|
|
2453
|
+
let max = 0;
|
|
2454
|
+
let moved = 0;
|
|
2455
|
+
let n = 0;
|
|
2456
|
+
for (const r of after.routes) {
|
|
2457
|
+
const p = prev.get(r.edge);
|
|
2458
|
+
if (!p) continue;
|
|
2459
|
+
const f = frechet(p, r.points);
|
|
2460
|
+
sum += f;
|
|
2461
|
+
max = Math.max(max, f);
|
|
2462
|
+
if (f > 1) moved++;
|
|
2463
|
+
n++;
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
mean: n ? sum / n : 0,
|
|
2467
|
+
max,
|
|
2468
|
+
moved: n ? moved / n : 0
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
// src/bench/run.ts
|
|
2472
|
+
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
2473
|
+
/** This function routes each fixture with each router, checks the hard rules and measures the quality. */
|
|
2474
|
+
function runBenchmark(routers, fixtures, options = {}) {
|
|
2475
|
+
const rows = [];
|
|
2476
|
+
for (const spec of fixtures) {
|
|
2477
|
+
const graph = makeFixture(spec);
|
|
2478
|
+
for (const router of routers) if (graph.nodes.length <= (router.maxNodes ?? Infinity)) rows.push(benchOne(router, spec.name, graph, options.repeat ?? 3));
|
|
2479
|
+
}
|
|
2480
|
+
return rows;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* This function tells if two values hold the same data, as JSON sees it. It does not make a string,
|
|
2484
|
+
* because the result of a large graph can be longer than the longest string.
|
|
2485
|
+
*/
|
|
2486
|
+
function sameData(a, b) {
|
|
2487
|
+
if (a === b) return true;
|
|
2488
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
2489
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
2490
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
2491
|
+
if (Array.isArray(a)) {
|
|
2492
|
+
const y = b;
|
|
2493
|
+
if (a.length !== y.length) return false;
|
|
2494
|
+
for (let i = 0; i < a.length; i++) if (!sameData(a[i], y[i])) return false;
|
|
2495
|
+
return true;
|
|
2496
|
+
}
|
|
2497
|
+
const x = a;
|
|
2498
|
+
const y = b;
|
|
2499
|
+
const kx = Object.keys(x).filter((k) => x[k] !== void 0);
|
|
2500
|
+
const ky = Object.keys(y).filter((k) => y[k] !== void 0);
|
|
2501
|
+
if (kx.length !== ky.length) return false;
|
|
2502
|
+
for (const k of kx) if (!sameData(x[k], y[k])) return false;
|
|
2503
|
+
return true;
|
|
2504
|
+
}
|
|
2505
|
+
/** This function routes one graph with one router and measures the result. */
|
|
2506
|
+
function benchOne(router, fixture, graph, repeat = 3) {
|
|
2507
|
+
const times = [];
|
|
2508
|
+
let first;
|
|
2509
|
+
let deterministic = true;
|
|
2510
|
+
for (let i = 0; i < Math.max(2, repeat); i++) {
|
|
2511
|
+
const t0 = now();
|
|
2512
|
+
const r = router.route(graph);
|
|
2513
|
+
times.push(now() - t0);
|
|
2514
|
+
if (!first) first = r;
|
|
2515
|
+
else if (i === 1 && !sameData(r, first)) deterministic = false;
|
|
2516
|
+
}
|
|
2517
|
+
times.sort((a, b) => a - b);
|
|
2518
|
+
const result = first;
|
|
2519
|
+
const all = checkProperties(graph, result, 1e6);
|
|
2520
|
+
return {
|
|
2521
|
+
router: router.name,
|
|
2522
|
+
fixture,
|
|
2523
|
+
nodes: graph.nodes.length,
|
|
2524
|
+
edges: graph.edges.length,
|
|
2525
|
+
ms: times[Math.floor(times.length / 2)],
|
|
2526
|
+
deterministic,
|
|
2527
|
+
violations: all.length,
|
|
2528
|
+
sample: all.slice(0, 5),
|
|
2529
|
+
quality: measureQuality(graph, result)
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
/** This function writes the rows as a Markdown table. */
|
|
2533
|
+
function formatTable(rows) {
|
|
2534
|
+
const head = "| router | fixture | nodes | edges | ms | same twice | violations | ink | crossings | bends | inflections | clearance | tight | detour | plug angle |";
|
|
2535
|
+
const line = "|---|---|---:|---:|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
2536
|
+
const f = (v, d = 2) => Number.isInteger(v) ? String(v) : v.toFixed(d);
|
|
2537
|
+
return [
|
|
2538
|
+
head,
|
|
2539
|
+
line,
|
|
2540
|
+
...rows.map((r) => {
|
|
2541
|
+
const q = r.quality;
|
|
2542
|
+
return `| ${r.router} | ${r.fixture} | ${r.nodes} | ${r.edges} | ${f(r.ms, 1)} | ${r.deterministic ? "yes" : "NO"} | ${r.violations} | ${Math.round(q.ink)} | ${q.crossingsEstimated ? "≈" : ""}${q.crossings} | ${f(q.bends)} | ${f(q.inflections)} | ${f(q.clearance, 1)} | ${f(q.tight)} | ${f(q.detour)} | ${f(q.plugAngle, 1)} |`;
|
|
2543
|
+
})
|
|
2544
|
+
].join("\n");
|
|
2545
|
+
}
|
|
2546
|
+
export { CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, dist, fixedPortPoint, formatTable, frechet, groupEdges, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, measureQuality, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, runKey, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, treeDepth, trunksRouter, turns };
|
|
2547
|
+
|
|
2548
|
+
//# sourceMappingURL=index.js.map
|