@mark1russell7/highways 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/build/index.d.ts +698 -12
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2353 -55
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/compare.ts +41 -6
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +71 -13
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +2 -0
- package/src/strategies/builtins.ts +82 -6
- package/src/strategies/model.ts +36 -6
- package/src/strategies/schemas.ts +13 -0
package/build/index.js
CHANGED
|
@@ -1049,7 +1049,7 @@ function checkProperties(graph, result, limit = 100) {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
for (const id of run.edges) {
|
|
1051
1051
|
const r = routes.get(id);
|
|
1052
|
-
if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
|
|
1052
|
+
if (!r || !(holds$1(r.points, run.points) || holds$1(r.points, [...run.points].reverse()))) {
|
|
1053
1053
|
if (add({
|
|
1054
1054
|
rule: "run-shared",
|
|
1055
1055
|
edge: id,
|
|
@@ -1112,7 +1112,7 @@ function onBoundary(p, b) {
|
|
|
1112
1112
|
return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
|
|
1113
1113
|
}
|
|
1114
1114
|
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
1115
|
-
function holds(p, part) {
|
|
1115
|
+
function holds$1(p, part) {
|
|
1116
1116
|
if (part.length === 0) return true;
|
|
1117
1117
|
for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
|
|
1118
1118
|
return false;
|
|
@@ -1684,7 +1684,7 @@ function clothoidPoint(s, a2) {
|
|
|
1684
1684
|
/** The fits of the clothoids of recent corners, by the turn and the spiral share. Each fit comes when a corner first needs it. The cache empties when it gets large. */
|
|
1685
1685
|
const fits = /* @__PURE__ */ new Map();
|
|
1686
1686
|
/** The number of points of the dense polyline that measures the error of a fit. */
|
|
1687
|
-
const DENSE = 256;
|
|
1687
|
+
const DENSE$1 = 256;
|
|
1688
1688
|
/** This function gives the distance from the point `q` to the part of the polyline `p` from the index `from` to the index `to`. */
|
|
1689
1689
|
function distanceTo(q, p, from, to) {
|
|
1690
1690
|
let best = Infinity;
|
|
@@ -1725,13 +1725,13 @@ function clothoidFits(theta) {
|
|
|
1725
1725
|
};
|
|
1726
1726
|
};
|
|
1727
1727
|
const dense = [];
|
|
1728
|
-
for (let i = 0; i <= DENSE; i++) dense.push(clothoidPoint(i / DENSE * length, a2));
|
|
1728
|
+
for (let i = 0; i <= DENSE$1; i++) dense.push(clothoidPoint(i / DENSE$1 * length, a2));
|
|
1729
1729
|
const done = [];
|
|
1730
1730
|
return (n) => {
|
|
1731
1731
|
const known = done[n];
|
|
1732
1732
|
if (known) return known;
|
|
1733
1733
|
const pieces = fitPieces(at, n);
|
|
1734
|
-
const window = Math.ceil(DENSE / n / 2) + 8;
|
|
1734
|
+
const window = Math.ceil(DENSE$1 / n / 2) + 8;
|
|
1735
1735
|
let error = 0;
|
|
1736
1736
|
let from = {
|
|
1737
1737
|
x: 0,
|
|
@@ -1739,7 +1739,7 @@ function clothoidFits(theta) {
|
|
|
1739
1739
|
};
|
|
1740
1740
|
pieces.forEach((q, k) => {
|
|
1741
1741
|
for (let i = 1; i < 16; i++) {
|
|
1742
|
-
const mid = Math.round((k + i / 16) / n * DENSE);
|
|
1742
|
+
const mid = Math.round((k + i / 16) / n * DENSE$1);
|
|
1743
1743
|
error = Math.max(error, distanceTo(cubicAt(from, q.c1, q.c2, q.to, i / 16), dense, mid - window, mid + window));
|
|
1744
1744
|
}
|
|
1745
1745
|
from = q.to;
|
|
@@ -2043,12 +2043,12 @@ function straightCurve(points) {
|
|
|
2043
2043
|
});
|
|
2044
2044
|
}
|
|
2045
2045
|
// src/segments.ts
|
|
2046
|
-
const SAME$
|
|
2046
|
+
const SAME$3 = 1e-6;
|
|
2047
2047
|
/** This function gives the index where `p` holds the points of `part` in a row, from `from` on, or -1. */
|
|
2048
2048
|
function indexOf(p, part, from) {
|
|
2049
2049
|
for (let i = from; i + part.length <= p.length; i++) {
|
|
2050
2050
|
let ok = true;
|
|
2051
|
-
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME$
|
|
2051
|
+
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME$3;
|
|
2052
2052
|
if (ok) return i;
|
|
2053
2053
|
}
|
|
2054
2054
|
return -1;
|
|
@@ -2151,7 +2151,7 @@ function pathPoints(graph, path) {
|
|
|
2151
2151
|
const s = byId.get(ref.segment);
|
|
2152
2152
|
if (!s) continue;
|
|
2153
2153
|
const pts = ref.reverse ? [...s.points].reverse() : s.points;
|
|
2154
|
-
for (const q of pts) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME$
|
|
2154
|
+
for (const q of pts) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME$3) out.push(q);
|
|
2155
2155
|
}
|
|
2156
2156
|
return out;
|
|
2157
2157
|
}
|
|
@@ -2231,7 +2231,7 @@ function lanePaths(graph, spacingOf) {
|
|
|
2231
2231
|
if (!s) continue;
|
|
2232
2232
|
const spacing = spacingOf(s);
|
|
2233
2233
|
const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
|
|
2234
|
-
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME$
|
|
2234
|
+
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME$3) out.push(q);
|
|
2235
2235
|
}
|
|
2236
2236
|
if (out.length >= 2 && ends.length >= 2) {
|
|
2237
2237
|
out[0] = ends[0];
|
|
@@ -2258,7 +2258,7 @@ function partAfter(P, Q) {
|
|
|
2258
2258
|
const a = P.p[P.e + j];
|
|
2259
2259
|
const b = Q.p[Q.e + j];
|
|
2260
2260
|
if (a === void 0 || b === void 0) return void 0;
|
|
2261
|
-
if (dist(a, b) > SAME$
|
|
2261
|
+
if (dist(a, b) > SAME$3) {
|
|
2262
2262
|
const from = P.p[P.e + j - 2];
|
|
2263
2263
|
const at = P.p[P.e + j - 1];
|
|
2264
2264
|
const side = turn$1(from, at, a) - turn$1(from, at, b);
|
|
@@ -2275,7 +2275,7 @@ function partBefore(P, Q) {
|
|
|
2275
2275
|
const a = P.p[P.s - j];
|
|
2276
2276
|
const b = Q.p[Q.s - j];
|
|
2277
2277
|
if (a === void 0 || b === void 0) return void 0;
|
|
2278
|
-
if (dist(a, b) > SAME$
|
|
2278
|
+
if (dist(a, b) > SAME$3) {
|
|
2279
2279
|
const from = P.p[P.s - j + 2];
|
|
2280
2280
|
const at = P.p[P.s - j + 1];
|
|
2281
2281
|
const side = turn$1(from, at, b) - turn$1(from, at, a);
|
|
@@ -2288,7 +2288,7 @@ function partBefore(P, Q) {
|
|
|
2288
2288
|
}
|
|
2289
2289
|
const byEdge = (P, Q) => P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
|
|
2290
2290
|
/** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
|
|
2291
|
-
const firstPoint = (a, b) => a.x < b.x - SAME$
|
|
2291
|
+
const firstPoint = (a, b) => a.x < b.x - SAME$3 || Math.abs(a.x - b.x) <= SAME$3 && a.y < b.y;
|
|
2292
2292
|
/**
|
|
2293
2293
|
* This function gives the lane order of the edges on a shared way `run` (metro-line order).
|
|
2294
2294
|
* The routes are polylines that hold the points of the run in a row, in either direction. A route that does not hold the run goes last.
|
|
@@ -2338,7 +2338,7 @@ function orderLanes(run, routes, order = "run") {
|
|
|
2338
2338
|
// src/curves/highway/index.ts
|
|
2339
2339
|
/** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle stays sharp. */
|
|
2340
2340
|
const FLAT$1 = 1e-6;
|
|
2341
|
-
const SAME$
|
|
2341
|
+
const SAME$2 = 1e-6;
|
|
2342
2342
|
/** A turn nearer to a half turn than this angle, in radians, is a spike: the route goes back on itself. */
|
|
2343
2343
|
const SPIKE$1 = .01;
|
|
2344
2344
|
/** A miter point farther from its vertex than this multiple of the offset gives two corners in place of one. */
|
|
@@ -2377,7 +2377,7 @@ function miter(v, nA, nB, o) {
|
|
|
2377
2377
|
*/
|
|
2378
2378
|
function jogShape(r, shift, style = "biarc") {
|
|
2379
2379
|
const d = Math.abs(shift);
|
|
2380
|
-
if (d < SAME$
|
|
2380
|
+
if (d < SAME$2) return {
|
|
2381
2381
|
length: 0,
|
|
2382
2382
|
angle: 0
|
|
2383
2383
|
};
|
|
@@ -2419,7 +2419,7 @@ function filletPolyline(vertices, trace) {
|
|
|
2419
2419
|
const v = [];
|
|
2420
2420
|
for (const q of vertices) {
|
|
2421
2421
|
const last = v[v.length - 1];
|
|
2422
|
-
if (last && Math.hypot(q.p.x - last.p.x, q.p.y - last.p.y) <= SAME$
|
|
2422
|
+
if (last && Math.hypot(q.p.x - last.p.x, q.p.y - last.p.y) <= SAME$2) {
|
|
2423
2423
|
const jog = q.jog?.part === "start" ? q.jog : last.jog ?? q.jog;
|
|
2424
2424
|
v[v.length - 1] = jog ? {
|
|
2425
2425
|
...last,
|
|
@@ -2546,7 +2546,7 @@ function jogSegments(from, to, u, style, pieces) {
|
|
|
2546
2546
|
const D = sub(to, from);
|
|
2547
2547
|
const l = dot(D, u);
|
|
2548
2548
|
const d = cross(u, D);
|
|
2549
|
-
if (l <= SAME$
|
|
2549
|
+
if (l <= SAME$2) return [{
|
|
2550
2550
|
kind: "line",
|
|
2551
2551
|
to
|
|
2552
2552
|
}];
|
|
@@ -2654,7 +2654,7 @@ function tracedHighwayCurves(source, open, laneWidth, options = {}) {
|
|
|
2654
2654
|
const centerOf = (s) => {
|
|
2655
2655
|
let c = centers.get(s.id);
|
|
2656
2656
|
if (!c) {
|
|
2657
|
-
c = distinctPoints(s.points, SAME$
|
|
2657
|
+
c = distinctPoints(s.points, SAME$2);
|
|
2658
2658
|
centers.set(s.id, c);
|
|
2659
2659
|
}
|
|
2660
2660
|
return c;
|
|
@@ -2675,7 +2675,7 @@ function tracedHighwayCurves(source, open, laneWidth, options = {}) {
|
|
|
2675
2675
|
const a = c[from];
|
|
2676
2676
|
const b = c[to];
|
|
2677
2677
|
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
2678
|
-
if (len <= SAME$
|
|
2678
|
+
if (len <= SAME$2) continue;
|
|
2679
2679
|
const u = {
|
|
2680
2680
|
x: (b.x - a.x) / len,
|
|
2681
2681
|
y: (b.y - a.y) / len
|
|
@@ -2818,13 +2818,13 @@ function withoutSpikes(input) {
|
|
|
2818
2818
|
}
|
|
2819
2819
|
const rest = A.len - B.len;
|
|
2820
2820
|
const keep = [];
|
|
2821
|
-
if (rest > SAME$
|
|
2821
|
+
if (rest > SAME$2) keep.push({
|
|
2822
2822
|
...A,
|
|
2823
2823
|
b: B.b,
|
|
2824
2824
|
len: rest,
|
|
2825
2825
|
to: -1
|
|
2826
2826
|
});
|
|
2827
|
-
else if (-rest > SAME$
|
|
2827
|
+
else if (-rest > SAME$2) keep.push({
|
|
2828
2828
|
...B,
|
|
2829
2829
|
a: A.a,
|
|
2830
2830
|
len: -rest,
|
|
@@ -2843,11 +2843,11 @@ function laneCurve(plan, bends, polyline, { radius, minRadius, space, style: wan
|
|
|
2843
2843
|
const endJog = new Array(L).fill(void 0);
|
|
2844
2844
|
for (let j = 0; j < L; j++) {
|
|
2845
2845
|
const leg = legs[j];
|
|
2846
|
-
if (Math.abs(at[j] - leg.off) > SAME$
|
|
2846
|
+
if (Math.abs(at[j] - leg.off) > SAME$2) startJog[j] = {
|
|
2847
2847
|
from: at[j],
|
|
2848
2848
|
to: leg.off
|
|
2849
2849
|
};
|
|
2850
|
-
if (Math.abs(at[j + 1] - leg.off) > SAME$
|
|
2850
|
+
if (Math.abs(at[j + 1] - leg.off) > SAME$2) endJog[j] = {
|
|
2851
2851
|
from: leg.off,
|
|
2852
2852
|
to: at[j + 1]
|
|
2853
2853
|
};
|
|
@@ -5388,7 +5388,7 @@ const SECTIONS_DEFAULTS = Object.freeze({
|
|
|
5388
5388
|
stretchPad: 4,
|
|
5389
5389
|
hubSlack: 80
|
|
5390
5390
|
});
|
|
5391
|
-
const dedupe$
|
|
5391
|
+
const dedupe$2 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
5392
5392
|
/**
|
|
5393
5393
|
* 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.
|
|
5394
5394
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
@@ -5466,7 +5466,7 @@ function sectionsRouter(options) {
|
|
|
5466
5466
|
if (!p) {
|
|
5467
5467
|
const [from, to] = forward ? [a, b] : [b, a];
|
|
5468
5468
|
const way = corners.path(from, to);
|
|
5469
|
-
p = dedupe$
|
|
5469
|
+
p = dedupe$2(way ? corners.pull(way) : [from, to]);
|
|
5470
5470
|
pieces.set(key, p);
|
|
5471
5471
|
}
|
|
5472
5472
|
if (!used.includes(key)) used.push(key);
|
|
@@ -5932,7 +5932,7 @@ function splitPoint(start, targets, split, free, step) {
|
|
|
5932
5932
|
while (d > 0 && !free(at(d))) d -= step;
|
|
5933
5933
|
return d > 0 ? at(d) : start;
|
|
5934
5934
|
}
|
|
5935
|
-
const dedupe = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
5935
|
+
const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
5936
5936
|
/**
|
|
5937
5937
|
* This function gives the place where a branch leaves its trunk. The branch starts at the last point of the trunk and ends at the plug of the target.
|
|
5938
5938
|
*
|
|
@@ -6041,7 +6041,7 @@ function trunksRouter(options) {
|
|
|
6041
6041
|
const split = splitPoint(ways.out(plug), centers, options.split ?? .5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
|
|
6042
6042
|
const way = ways.fromPlug(plug, split);
|
|
6043
6043
|
if (!way) continue;
|
|
6044
|
-
const trunk = dedupe(way);
|
|
6044
|
+
const trunk = dedupe$1(way);
|
|
6045
6045
|
const end = trunk[trunk.length - 1];
|
|
6046
6046
|
const prev = trunk.length >= 2 ? trunk[trunk.length - 2] : plug.point;
|
|
6047
6047
|
const len = dist(prev, end);
|
|
@@ -6054,11 +6054,11 @@ function trunksRouter(options) {
|
|
|
6054
6054
|
const exits = [];
|
|
6055
6055
|
group.forEach((e, i) => {
|
|
6056
6056
|
const pt = placePlug(targets[i], e.target, end);
|
|
6057
|
-
const branch = dedupe(ways.toPlug(end, pt) ?? [end, pt.point]);
|
|
6057
|
+
const branch = dedupe$1(ways.toPlug(end, pt) ?? [end, pt.point]);
|
|
6058
6058
|
if (!part) {
|
|
6059
6059
|
own.push({
|
|
6060
6060
|
edge: e.id,
|
|
6061
|
-
points: dedupe([...trunk, ...branch.slice(1)])
|
|
6061
|
+
points: dedupe$1([...trunk, ...branch.slice(1)])
|
|
6062
6062
|
});
|
|
6063
6063
|
branches.push({
|
|
6064
6064
|
edge: e.id,
|
|
@@ -6090,7 +6090,7 @@ function trunksRouter(options) {
|
|
|
6090
6090
|
for (const { exit } of exits) if (!stops.has(exit.at)) stops.set(exit.at, exit.point);
|
|
6091
6091
|
const line = [...stops].sort((x, y) => x[0] - y[0]);
|
|
6092
6092
|
for (const { edge, exit } of exits) {
|
|
6093
|
-
const p = dedupe([...line.filter(([at]) => at <= exit.at).map(([, q]) => q), ...exit.rest]);
|
|
6093
|
+
const p = dedupe$1([...line.filter(([at]) => at <= exit.at).map(([, q]) => q), ...exit.rest]);
|
|
6094
6094
|
points.set(edge, p);
|
|
6095
6095
|
own.push({
|
|
6096
6096
|
edge,
|
|
@@ -6193,7 +6193,7 @@ const ROUTERS = [
|
|
|
6193
6193
|
/** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle is a spike, and it gets no arc. */
|
|
6194
6194
|
const FLAT = 1e-6;
|
|
6195
6195
|
const SPIKE = .01;
|
|
6196
|
-
const SAME = 1e-6;
|
|
6196
|
+
const SAME$1 = 1e-6;
|
|
6197
6197
|
/** A leg with a smaller share of its length on an axis does not grow on that axis. */
|
|
6198
6198
|
const SLANT = .2;
|
|
6199
6199
|
const turnOf = (u, w) => Math.atan2(u.x * w.y - u.y * w.x, u.x * w.x + u.y * w.y);
|
|
@@ -6203,7 +6203,7 @@ const turnOf = (u, w) => Math.atan2(u.x * w.y - u.y * w.x, u.x * w.x + u.y * w.y
|
|
|
6203
6203
|
*/
|
|
6204
6204
|
function jogLength(r, shift) {
|
|
6205
6205
|
const d = Math.abs(shift);
|
|
6206
|
-
if (d < SAME || r <= 0) return 0;
|
|
6206
|
+
if (d < SAME$1 || r <= 0) return 0;
|
|
6207
6207
|
if (r < d / 2) return 2 * r;
|
|
6208
6208
|
return 2 * r * Math.sin(Math.acos(1 - d / (2 * r)));
|
|
6209
6209
|
}
|
|
@@ -6218,7 +6218,7 @@ function legsOf(graph, spacing, maxWidth) {
|
|
|
6218
6218
|
const sp = s.count > 1 ? Math.min(spacing, maxWidth / (s.count - 1)) : 0;
|
|
6219
6219
|
const offset = ((s.count - 1) / 2 - ref.lane) * sp;
|
|
6220
6220
|
const c = [];
|
|
6221
|
-
for (const p of s.points) if (c.length === 0 || Math.hypot(p.x - c[c.length - 1].x, p.y - c[c.length - 1].y) > SAME) c.push(p);
|
|
6221
|
+
for (const p of s.points) if (c.length === 0 || Math.hypot(p.x - c[c.length - 1].x, p.y - c[c.length - 1].y) > SAME$1) c.push(p);
|
|
6222
6222
|
const n = c.length;
|
|
6223
6223
|
for (let j = 0; j + 1 < n; j++) {
|
|
6224
6224
|
const from = ref.reverse ? n - 1 - j : j;
|
|
@@ -6274,7 +6274,7 @@ function shortLegs(legs, minRadius) {
|
|
|
6274
6274
|
const start = j > 0 ? endLength(path[j - 1], X, X, minRadius) : 0;
|
|
6275
6275
|
const end = j + 1 < path.length ? endLength(X, path[j + 1], X, minRadius) : 0;
|
|
6276
6276
|
const short = 2 * Math.max(start, end) - X.len;
|
|
6277
|
-
if (short <= SAME) continue;
|
|
6277
|
+
if (short <= SAME$1) continue;
|
|
6278
6278
|
const known = out.get(X.key);
|
|
6279
6279
|
if (!known || known.short < short) out.set(X.key, {
|
|
6280
6280
|
leg: X,
|
|
@@ -7292,11 +7292,440 @@ function formatLevels(rows) {
|
|
|
7292
7292
|
].join(" | "))).map((b) => `| ${b} |`)
|
|
7293
7293
|
].join("\n");
|
|
7294
7294
|
}
|
|
7295
|
+
// src/highways/model.ts
|
|
7296
|
+
/** This function gives the point at a place on a polyline. A place outside the polyline gives its nearest end. */
|
|
7297
|
+
function pointAt(line, at) {
|
|
7298
|
+
if (line.length === 0) throw new Error("The polyline has no point.");
|
|
7299
|
+
if (at <= 0) return line[0];
|
|
7300
|
+
if (at >= line.length - 1) return line[line.length - 1];
|
|
7301
|
+
const i = Math.floor(at);
|
|
7302
|
+
const u = at - i;
|
|
7303
|
+
const a = line[i];
|
|
7304
|
+
if (u === 0) return a;
|
|
7305
|
+
const b = line[i + 1];
|
|
7306
|
+
return {
|
|
7307
|
+
x: a.x + (b.x - a.x) * u,
|
|
7308
|
+
y: a.y + (b.y - a.y) * u
|
|
7309
|
+
};
|
|
7310
|
+
}
|
|
7311
|
+
/** This function gives the length of a polyline from the place `from` to the place `to`, with `from` not larger than `to`. */
|
|
7312
|
+
function lengthBetween(line, from, to) {
|
|
7313
|
+
if (to <= from) return 0;
|
|
7314
|
+
let sum = 0;
|
|
7315
|
+
let prev = pointAt(line, from);
|
|
7316
|
+
for (let i = Math.floor(from) + 1; i < to; i++) {
|
|
7317
|
+
const p = line[i];
|
|
7318
|
+
sum += dist(prev, p);
|
|
7319
|
+
prev = p;
|
|
7320
|
+
}
|
|
7321
|
+
return sum + dist(prev, pointAt(line, to));
|
|
7322
|
+
}
|
|
7323
|
+
/** This function gives a member of a highway with its merge point and its split point at the places `on` and `off` of the spine. */
|
|
7324
|
+
function memberAt(spine, edge, on, off, seq = 0) {
|
|
7325
|
+
return {
|
|
7326
|
+
edge,
|
|
7327
|
+
on,
|
|
7328
|
+
off,
|
|
7329
|
+
merge: pointAt(spine, on),
|
|
7330
|
+
split: pointAt(spine, off),
|
|
7331
|
+
seq
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7334
|
+
/**
|
|
7335
|
+
* This function lists the problems of a plan against its graph. A good plan gives an empty list.
|
|
7336
|
+
* Each highway has a spine of two points or more, and two members or more. Each member is an edge of the graph, with `on` and `off` on the spine and apart.
|
|
7337
|
+
* An edge is a member of a highway one time at most.
|
|
7338
|
+
*/
|
|
7339
|
+
function checkPlan(graph, plan) {
|
|
7340
|
+
const edges = new Set(graph.edges.map((e) => e.id));
|
|
7341
|
+
const out = [];
|
|
7342
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7343
|
+
for (const h of plan.highways) {
|
|
7344
|
+
if (ids.has(h.id)) out.push(`Two highways have the id "${h.id}".`);
|
|
7345
|
+
ids.add(h.id);
|
|
7346
|
+
const last = h.spine.length - 1;
|
|
7347
|
+
if (h.spine.length < 2) out.push(`The highway "${h.id}" has a spine with fewer than two points.`);
|
|
7348
|
+
if (h.members.length < 2) out.push(`The highway "${h.id}" has fewer than two members.`);
|
|
7349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7350
|
+
for (const m of h.members) {
|
|
7351
|
+
if (!edges.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}", which is not an edge of the graph.`);
|
|
7352
|
+
if (seen.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}" two times.`);
|
|
7353
|
+
seen.add(m.edge);
|
|
7354
|
+
if (!(m.on >= 0 && m.on <= last && m.off >= 0 && m.off <= last)) out.push(`The member "${m.edge}" of the highway "${h.id}" has a place off the spine.`);
|
|
7355
|
+
if (m.on === m.off) out.push(`The member "${m.edge}" of the highway "${h.id}" joins and leaves the spine at one place.`);
|
|
7356
|
+
}
|
|
7357
|
+
if (h.lanes.length !== h.members.length || h.lanes.some((e) => !seen.has(e))) out.push(`The lanes of the highway "${h.id}" are not its members.`);
|
|
7358
|
+
}
|
|
7359
|
+
return out;
|
|
7360
|
+
}
|
|
7361
|
+
// src/highways/corridors/index.ts
|
|
7362
|
+
/** The default options of the strategy `corridors`. */
|
|
7363
|
+
const CORRIDOR_DEFAULTS = Object.freeze({
|
|
7364
|
+
rho: .3,
|
|
7365
|
+
minSize: 2,
|
|
7366
|
+
hashes: 64,
|
|
7367
|
+
bands: 32,
|
|
7368
|
+
near: 0,
|
|
7369
|
+
lane: 4
|
|
7370
|
+
});
|
|
7371
|
+
/**
|
|
7372
|
+
* The break-even overlap of two parallel edges for each unit of the lane gap, with the cost exponent 0.5 of the note.
|
|
7373
|
+
* A shared stretch shorter than this times the gap adds ink.
|
|
7374
|
+
*/
|
|
7375
|
+
const BREAK_EVEN = Math.sqrt((2 + Math.SQRT2) / (2 - Math.SQRT2));
|
|
7376
|
+
/** This function gives the FNV-1a hash of a string, as an unsigned 32-bit number. */
|
|
7377
|
+
function fnv1a(text) {
|
|
7378
|
+
let h = 2166136261;
|
|
7379
|
+
for (let i = 0; i < text.length; i++) {
|
|
7380
|
+
h ^= text.charCodeAt(i);
|
|
7381
|
+
h = Math.imul(h, 16777619);
|
|
7382
|
+
}
|
|
7383
|
+
return h >>> 0;
|
|
7384
|
+
}
|
|
7385
|
+
/** This function mixes the bits of a 32-bit number (the finalizer of MurmurHash3). */
|
|
7386
|
+
function mix(x) {
|
|
7387
|
+
let h = x >>> 0;
|
|
7388
|
+
h ^= h >>> 16;
|
|
7389
|
+
h = Math.imul(h, 2246822507);
|
|
7390
|
+
h ^= h >>> 13;
|
|
7391
|
+
h = Math.imul(h, 3266489909);
|
|
7392
|
+
h ^= h >>> 16;
|
|
7393
|
+
return h >>> 0;
|
|
7394
|
+
}
|
|
7395
|
+
/** This function gives the MinHash signature of a set of keys with `count` hash functions. */
|
|
7396
|
+
function minHash(keys, count) {
|
|
7397
|
+
const sig = new Array(count).fill(4294967295);
|
|
7398
|
+
const seeds = Array.from({ length: count }, (_, i) => mix(Math.imul(i + 1, 2654435769)));
|
|
7399
|
+
for (const key of keys) {
|
|
7400
|
+
const base = fnv1a(key);
|
|
7401
|
+
for (let i = 0; i < count; i++) {
|
|
7402
|
+
const v = mix(base ^ seeds[i]);
|
|
7403
|
+
if (v < sig[i]) sig[i] = v;
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
7406
|
+
return sig;
|
|
7407
|
+
}
|
|
7408
|
+
/**
|
|
7409
|
+
* This function gives the first way of each edge: the way through the corner graph, with the plugs at its ends.
|
|
7410
|
+
* The router `corners` finds the same way before it drops the needless corners.
|
|
7411
|
+
*/
|
|
7412
|
+
function cornerWays(graph, options) {
|
|
7413
|
+
const byId = nodeIndex(graph);
|
|
7414
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
7415
|
+
const out = [];
|
|
7416
|
+
for (const e of graph.edges) {
|
|
7417
|
+
const s = byId.get(e.source.node);
|
|
7418
|
+
const t = byId.get(e.target.node);
|
|
7419
|
+
if (!s || !t) continue;
|
|
7420
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
7421
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
7422
|
+
const raw = ways.corners.path(ways.out(ps), ways.out(pt));
|
|
7423
|
+
out.push({
|
|
7424
|
+
edge: e.id,
|
|
7425
|
+
points: raw ? [
|
|
7426
|
+
ps.point,
|
|
7427
|
+
...raw,
|
|
7428
|
+
pt.point
|
|
7429
|
+
] : [ps.point, pt.point]
|
|
7430
|
+
});
|
|
7431
|
+
}
|
|
7432
|
+
return out;
|
|
7433
|
+
}
|
|
7434
|
+
/** This function gives the way of a route, with a key for each link. Two links with the same ends, in either order, have one key. */
|
|
7435
|
+
function wayOf(route, near) {
|
|
7436
|
+
const snap = (p) => near > 0 ? `${Math.round(p.x / near)},${Math.round(p.y / near)}` : `${p.x},${p.y}`;
|
|
7437
|
+
const links = [];
|
|
7438
|
+
const set = /* @__PURE__ */ new Map();
|
|
7439
|
+
let length = 0;
|
|
7440
|
+
for (let i = 1; i < route.points.length; i++) {
|
|
7441
|
+
const p = route.points[i - 1];
|
|
7442
|
+
const q = route.points[i];
|
|
7443
|
+
const len = dist(p, q);
|
|
7444
|
+
if (len <= 1e-9) continue;
|
|
7445
|
+
const a = snap(p);
|
|
7446
|
+
const b = snap(q);
|
|
7447
|
+
if (a === b) continue;
|
|
7448
|
+
const key = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
7449
|
+
links.push({
|
|
7450
|
+
key,
|
|
7451
|
+
len,
|
|
7452
|
+
a,
|
|
7453
|
+
at: i - 1
|
|
7454
|
+
});
|
|
7455
|
+
if (!set.has(key)) set.set(key, len);
|
|
7456
|
+
length += len;
|
|
7457
|
+
}
|
|
7458
|
+
return {
|
|
7459
|
+
edge: route.edge,
|
|
7460
|
+
points: route.points,
|
|
7461
|
+
links,
|
|
7462
|
+
set,
|
|
7463
|
+
length
|
|
7464
|
+
};
|
|
7465
|
+
}
|
|
7466
|
+
/** This function gives the length of the common links of two ways. */
|
|
7467
|
+
function overlap(p, q) {
|
|
7468
|
+
const [small, large] = p.set.size <= q.set.size ? [p, q] : [q, p];
|
|
7469
|
+
let sum = 0;
|
|
7470
|
+
for (const [key, len] of small.set) if (large.set.has(key)) sum += len;
|
|
7471
|
+
return sum;
|
|
7472
|
+
}
|
|
7473
|
+
/** This function gives the pairs of ways with one equal band of their signatures, as `i * n + j` with `i < j`, in order. */
|
|
7474
|
+
function candidates(ways, hashes, bands) {
|
|
7475
|
+
const rows = Math.max(1, Math.floor(hashes / Math.max(1, bands)));
|
|
7476
|
+
const count = Math.max(1, Math.floor(hashes / rows));
|
|
7477
|
+
const n = ways.length;
|
|
7478
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
7479
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7480
|
+
ways.forEach((w, i) => {
|
|
7481
|
+
if (w.set.size === 0) return;
|
|
7482
|
+
const sig = minHash(w.set.keys(), rows * count);
|
|
7483
|
+
for (let b = 0; b < count; b++) {
|
|
7484
|
+
const key = `${b}:${sig.slice(b * rows, (b + 1) * rows).join(",")}`;
|
|
7485
|
+
const list = buckets.get(key);
|
|
7486
|
+
if (list) {
|
|
7487
|
+
for (const j of list) pairs.add(j * n + i);
|
|
7488
|
+
list.push(i);
|
|
7489
|
+
} else buckets.set(key, [i]);
|
|
7490
|
+
}
|
|
7491
|
+
});
|
|
7492
|
+
return [...pairs].sort((a, b) => a - b);
|
|
7493
|
+
}
|
|
7494
|
+
/** This function gives the root of an element of a union-find forest, and shortens the path. */
|
|
7495
|
+
function root(parent, i) {
|
|
7496
|
+
while (parent[i] !== i) {
|
|
7497
|
+
parent[i] = parent[parent[i]];
|
|
7498
|
+
i = parent[i];
|
|
7499
|
+
}
|
|
7500
|
+
return i;
|
|
7501
|
+
}
|
|
7502
|
+
/**
|
|
7503
|
+
* This function gives the clusters of the ways. Two ways join when their common links are long.
|
|
7504
|
+
* A cluster is a connected part of the joins.
|
|
7505
|
+
* MinHash with bands finds the candidate pairs. The function then measures the overlap of each candidate pair.
|
|
7506
|
+
* Each cluster keeps the order of the ways, and the clusters come in the order of their first way.
|
|
7507
|
+
*/
|
|
7508
|
+
function corridorClusters(ways, o) {
|
|
7509
|
+
const n = ways.length;
|
|
7510
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
7511
|
+
const floor = BREAK_EVEN * o.lane;
|
|
7512
|
+
for (const pair of candidates(ways, o.hashes, o.bands)) {
|
|
7513
|
+
const i = Math.floor(pair / n);
|
|
7514
|
+
const j = pair % n;
|
|
7515
|
+
const p = ways[i];
|
|
7516
|
+
const q = ways[j];
|
|
7517
|
+
if (overlap(p, q) >= Math.max(floor, o.rho * Math.min(p.length, q.length))) {
|
|
7518
|
+
const a = root(parent, i);
|
|
7519
|
+
const b = root(parent, j);
|
|
7520
|
+
if (a !== b) parent[Math.max(a, b)] = Math.min(a, b);
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7524
|
+
for (let i = 0; i < n; i++) {
|
|
7525
|
+
const r = root(parent, i);
|
|
7526
|
+
const list = groups.get(r);
|
|
7527
|
+
if (list) list.push(i);
|
|
7528
|
+
else groups.set(r, [i]);
|
|
7529
|
+
}
|
|
7530
|
+
return [...groups.values()].filter((g) => g.length >= 2);
|
|
7531
|
+
}
|
|
7532
|
+
/**
|
|
7533
|
+
* This function gives the highway of a pool of ways, or `undefined`. The core is the way with the most shared length.
|
|
7534
|
+
* The spine is the stretch of the core with the largest saving. Each link of the spine has two ways of the pool or more.
|
|
7535
|
+
* A member is a way of the pool whose links on the spine are long enough, all in one direction.
|
|
7536
|
+
*/
|
|
7537
|
+
function highwayOf(pool, o) {
|
|
7538
|
+
const count = /* @__PURE__ */ new Map();
|
|
7539
|
+
for (const w of pool) for (const key of w.set.keys()) count.set(key, (count.get(key) ?? 0) + 1);
|
|
7540
|
+
const gain = (key, len) => len * ((count.get(key) ?? 1) - 1);
|
|
7541
|
+
let core;
|
|
7542
|
+
let best = 0;
|
|
7543
|
+
for (const w of pool) {
|
|
7544
|
+
let g = 0;
|
|
7545
|
+
for (const [key, len] of w.set) g += gain(key, len);
|
|
7546
|
+
if (g > best) [core, best] = [w, g];
|
|
7547
|
+
}
|
|
7548
|
+
if (!core) return void 0;
|
|
7549
|
+
let from = -1;
|
|
7550
|
+
let to = -1;
|
|
7551
|
+
let top = 0;
|
|
7552
|
+
let start = 0;
|
|
7553
|
+
let sum = 0;
|
|
7554
|
+
core.links.forEach((l, i) => {
|
|
7555
|
+
if ((count.get(l.key) ?? 0) < 2) {
|
|
7556
|
+
sum = 0;
|
|
7557
|
+
start = i + 1;
|
|
7558
|
+
return;
|
|
7559
|
+
}
|
|
7560
|
+
sum += gain(l.key, l.len);
|
|
7561
|
+
if (sum > top) [top, from, to] = [
|
|
7562
|
+
sum,
|
|
7563
|
+
start,
|
|
7564
|
+
i
|
|
7565
|
+
];
|
|
7566
|
+
});
|
|
7567
|
+
if (from < 0) return void 0;
|
|
7568
|
+
const chosen = core.links.slice(from, to + 1);
|
|
7569
|
+
const spine = [core.points[chosen[0].at], ...chosen.map((l) => core.points[l.at + 1])];
|
|
7570
|
+
const index = /* @__PURE__ */ new Map();
|
|
7571
|
+
chosen.forEach((l, j) => {
|
|
7572
|
+
if (!index.has(l.key)) index.set(l.key, j);
|
|
7573
|
+
});
|
|
7574
|
+
const spineLength = chosen.reduce((s, l) => s + l.len, 0);
|
|
7575
|
+
const floor = BREAK_EVEN * o.lane;
|
|
7576
|
+
const members = [];
|
|
7577
|
+
for (const w of pool) {
|
|
7578
|
+
let first = -1;
|
|
7579
|
+
let last = -1;
|
|
7580
|
+
let forward;
|
|
7581
|
+
let ok = true;
|
|
7582
|
+
let shared = 0;
|
|
7583
|
+
for (const l of w.links) {
|
|
7584
|
+
const j = index.get(l.key);
|
|
7585
|
+
if (j === void 0) continue;
|
|
7586
|
+
const dir = l.a === chosen[j].a;
|
|
7587
|
+
if (forward === void 0) forward = dir;
|
|
7588
|
+
else if (forward !== dir) ok = false;
|
|
7589
|
+
if (first < 0) first = j;
|
|
7590
|
+
else if (forward ? j < last : j > last) ok = false;
|
|
7591
|
+
last = j;
|
|
7592
|
+
shared += l.len;
|
|
7593
|
+
}
|
|
7594
|
+
if (!ok || forward === void 0 || shared < Math.max(floor, o.rho * Math.min(w.length, spineLength))) continue;
|
|
7595
|
+
members.push(forward ? {
|
|
7596
|
+
way: w,
|
|
7597
|
+
on: first,
|
|
7598
|
+
off: last + 1
|
|
7599
|
+
} : {
|
|
7600
|
+
way: w,
|
|
7601
|
+
on: first + 1,
|
|
7602
|
+
off: last
|
|
7603
|
+
});
|
|
7604
|
+
}
|
|
7605
|
+
return {
|
|
7606
|
+
core,
|
|
7607
|
+
spine,
|
|
7608
|
+
members
|
|
7609
|
+
};
|
|
7610
|
+
}
|
|
7611
|
+
/**
|
|
7612
|
+
* This function gives the highways of corridor signatures (HWY-031). Each edge gets a first way: the route of `input.routes`, or its way through the corner graph.
|
|
7613
|
+
* The signature of a way is the set of its links. Two edges join when their common links are at least `rho` times the shorter way, and longer than the break-even overlap.
|
|
7614
|
+
* MinHash with `hashes` hash functions in `bands` bands finds the candidate pairs fast. Union-find gives the clusters.
|
|
7615
|
+
*
|
|
7616
|
+
* In each cluster, the spine is the longest common run of links of the way with the most shared length. Each edge on the spine joins it at its first common link and leaves it at its last one.
|
|
7617
|
+
* The edges of the cluster that are not on the spine make the next highway, while `minSize` of them remain.
|
|
7618
|
+
*/
|
|
7619
|
+
function corridorPlan(input, o) {
|
|
7620
|
+
const routes = input.routes?.routes ?? cornerWays(input.graph, {
|
|
7621
|
+
...o.margin === void 0 ? {} : { margin: o.margin },
|
|
7622
|
+
...o.k === void 0 ? {} : { k: o.k }
|
|
7623
|
+
});
|
|
7624
|
+
const known = new Set(input.graph.edges.map((e) => e.id));
|
|
7625
|
+
const ways = routes.filter((r) => known.has(r.edge)).map((r) => wayOf(r, o.near));
|
|
7626
|
+
const highways = [];
|
|
7627
|
+
corridorClusters(ways, o).forEach((cluster, c) => {
|
|
7628
|
+
let pool = cluster.map((i) => ways[i]);
|
|
7629
|
+
while (pool.length >= Math.max(2, o.minSize)) {
|
|
7630
|
+
const found = highwayOf(pool, o);
|
|
7631
|
+
if (!found) break;
|
|
7632
|
+
const { core, spine, members } = found;
|
|
7633
|
+
if (members.length < Math.max(2, o.minSize)) {
|
|
7634
|
+
pool = pool.filter((w) => w !== core);
|
|
7635
|
+
continue;
|
|
7636
|
+
}
|
|
7637
|
+
const lanes = orderLanes(spine, members.map((m) => ({
|
|
7638
|
+
edge: m.way.edge,
|
|
7639
|
+
points: m.way.points
|
|
7640
|
+
})), "bundle");
|
|
7641
|
+
const byEdge = new Map(members.map((m) => [m.way.edge, m]));
|
|
7642
|
+
const list = lanes.map((edge) => {
|
|
7643
|
+
const m = byEdge.get(edge);
|
|
7644
|
+
return memberAt(spine, edge, m.on, m.off);
|
|
7645
|
+
});
|
|
7646
|
+
highways.push({
|
|
7647
|
+
id: `h${highways.length}`,
|
|
7648
|
+
key: `c${c}`,
|
|
7649
|
+
spine,
|
|
7650
|
+
lanes,
|
|
7651
|
+
members: list
|
|
7652
|
+
});
|
|
7653
|
+
const used = new Set(members.map((m) => m.way));
|
|
7654
|
+
pool = pool.filter((w) => !used.has(w));
|
|
7655
|
+
}
|
|
7656
|
+
});
|
|
7657
|
+
return { highways };
|
|
7658
|
+
}
|
|
7659
|
+
// src/highways/result.ts
|
|
7660
|
+
/**
|
|
7661
|
+
* This function gives the highways of a result of a router: one highway for each shared run and for each trunk of a bundle.
|
|
7662
|
+
* The spine is the points of the run or of the trunk. Each edge of the run is a member, from one end of the spine to the other end, in the way of its route.
|
|
7663
|
+
* The member `seq` is the place of the shared stretch in the route of the edge. The lanes keep the lane order of the router.
|
|
7664
|
+
*
|
|
7665
|
+
* Thus `follow` routes each edge along the shared stretches of the router. A route that does not hold its run is not a member.
|
|
7666
|
+
*/
|
|
7667
|
+
function planOfResult(result) {
|
|
7668
|
+
const graph = segmentsOf(result);
|
|
7669
|
+
const keys = new Map([...(result.runs ?? []).map((r) => [r.id, r.key]), ...(result.bundles ?? []).map((b) => [b.id, b.key])]);
|
|
7670
|
+
const shared = graph.segments.filter((s) => s.count > 1);
|
|
7671
|
+
const ids = new Set(shared.map((s) => s.id));
|
|
7672
|
+
const place = /* @__PURE__ */ new Map();
|
|
7673
|
+
for (const path of graph.paths) {
|
|
7674
|
+
let seq = 0;
|
|
7675
|
+
for (const ref of path.segments) if (ids.has(ref.segment)) place.set(`${ref.segment}\u0000${path.edge}`, {
|
|
7676
|
+
seq: seq++,
|
|
7677
|
+
reverse: ref.reverse
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
const highways = [];
|
|
7681
|
+
for (const s of shared) {
|
|
7682
|
+
const last = s.points.length - 1;
|
|
7683
|
+
const members = [];
|
|
7684
|
+
for (const edge of s.edges) {
|
|
7685
|
+
const at = place.get(`${s.id}\u0000${edge}`);
|
|
7686
|
+
if (at) members.push(at.reverse ? memberAt(s.points, edge, last, 0, at.seq) : memberAt(s.points, edge, 0, last, at.seq));
|
|
7687
|
+
}
|
|
7688
|
+
if (members.length < 2) continue;
|
|
7689
|
+
highways.push({
|
|
7690
|
+
id: `h${highways.length}`,
|
|
7691
|
+
key: keys.get(s.id) ?? s.id,
|
|
7692
|
+
spine: s.points,
|
|
7693
|
+
lanes: members.map((m) => m.edge),
|
|
7694
|
+
members
|
|
7695
|
+
});
|
|
7696
|
+
}
|
|
7697
|
+
return { highways };
|
|
7698
|
+
}
|
|
7699
|
+
// src/highways/keys/index.ts
|
|
7700
|
+
/**
|
|
7701
|
+
* This function gives the highways that the router `trunks` decides. A rule gives each edge a key.
|
|
7702
|
+
* The edges from one plug with one key share a trunk to a split point. The edges that go on together share a run past it.
|
|
7703
|
+
* Each trunk and each run becomes one highway. The function does not change the router: it reads its bundles and its runs.
|
|
7704
|
+
*/
|
|
7705
|
+
function keysPlan(input, options) {
|
|
7706
|
+
return planOfResult(trunksRouter({
|
|
7707
|
+
...options,
|
|
7708
|
+
name: "keys"
|
|
7709
|
+
}).route(input.graph));
|
|
7710
|
+
}
|
|
7711
|
+
// src/highways/shared-ways/index.ts
|
|
7712
|
+
/**
|
|
7713
|
+
* This function gives the highways that the router `shared` decides. The edges of one key go one by one through the corner graph.
|
|
7714
|
+
* A link that the key uses already costs less, so the ways of a key come together. Each stretch that two edges or more share becomes one highway.
|
|
7715
|
+
* The function does not change the router: it reads its runs.
|
|
7716
|
+
*/
|
|
7717
|
+
function sharedWaysPlan(input, options) {
|
|
7718
|
+
return planOfResult(sharedRouter({
|
|
7719
|
+
...options,
|
|
7720
|
+
name: "shared-ways"
|
|
7721
|
+
}).route(input.graph));
|
|
7722
|
+
}
|
|
7295
7723
|
// src/strategies/model.ts
|
|
7296
7724
|
/** The steps that have strategies, in the order of the pipeline. */
|
|
7297
7725
|
const STEPS = [
|
|
7298
7726
|
"group",
|
|
7299
7727
|
"router",
|
|
7728
|
+
"highway",
|
|
7300
7729
|
"curve",
|
|
7301
7730
|
"room"
|
|
7302
7731
|
];
|
|
@@ -7335,6 +7764,7 @@ function createRegistry(strategies) {
|
|
|
7335
7764
|
const out = {
|
|
7336
7765
|
group: [],
|
|
7337
7766
|
router: [],
|
|
7767
|
+
highway: [],
|
|
7338
7768
|
curve: [],
|
|
7339
7769
|
room: []
|
|
7340
7770
|
};
|
|
@@ -7347,6 +7777,7 @@ function createRegistry(strategies) {
|
|
|
7347
7777
|
return Object.freeze({
|
|
7348
7778
|
group: Object.freeze(out.group),
|
|
7349
7779
|
router: Object.freeze(out.router),
|
|
7780
|
+
highway: Object.freeze(out.highway),
|
|
7350
7781
|
curve: Object.freeze(out.curve),
|
|
7351
7782
|
room: Object.freeze(out.room)
|
|
7352
7783
|
});
|
|
@@ -7356,6 +7787,7 @@ function register(registry, ...strategies) {
|
|
|
7356
7787
|
return createRegistry([
|
|
7357
7788
|
...registry.group ?? [],
|
|
7358
7789
|
...registry.router,
|
|
7790
|
+
...registry.highway ?? [],
|
|
7359
7791
|
...registry.curve,
|
|
7360
7792
|
...registry.room,
|
|
7361
7793
|
...strategies
|
|
@@ -7474,6 +7906,73 @@ const SHARED_SCHEMA = {
|
|
|
7474
7906
|
},
|
|
7475
7907
|
laneOrder: LANE_ORDER
|
|
7476
7908
|
};
|
|
7909
|
+
/** The declarations of the options of the highway strategy `corridors` (HWY-031). */
|
|
7910
|
+
const CORRIDOR_SCHEMA = {
|
|
7911
|
+
margin: WAY_SCHEMA.margin,
|
|
7912
|
+
k: WAY_SCHEMA.k,
|
|
7913
|
+
rho: {
|
|
7914
|
+
type: "number",
|
|
7915
|
+
min: .1,
|
|
7916
|
+
max: .8,
|
|
7917
|
+
step: .05,
|
|
7918
|
+
default: .3,
|
|
7919
|
+
describe: "Two edges join when their common links are this share of the shorter way, or longer."
|
|
7920
|
+
},
|
|
7921
|
+
minSize: {
|
|
7922
|
+
type: "integer",
|
|
7923
|
+
min: 2,
|
|
7924
|
+
max: 16,
|
|
7925
|
+
step: 1,
|
|
7926
|
+
default: 2,
|
|
7927
|
+
describe: "A highway has this number of edges or more."
|
|
7928
|
+
},
|
|
7929
|
+
hashes: {
|
|
7930
|
+
type: "integer",
|
|
7931
|
+
min: 16,
|
|
7932
|
+
max: 128,
|
|
7933
|
+
step: 16,
|
|
7934
|
+
scale: "log",
|
|
7935
|
+
marks: [
|
|
7936
|
+
16,
|
|
7937
|
+
32,
|
|
7938
|
+
64,
|
|
7939
|
+
128
|
|
7940
|
+
],
|
|
7941
|
+
default: 64,
|
|
7942
|
+
describe: "The number of hash functions of the signature of a way."
|
|
7943
|
+
},
|
|
7944
|
+
bands: {
|
|
7945
|
+
type: "integer",
|
|
7946
|
+
min: 4,
|
|
7947
|
+
max: 32,
|
|
7948
|
+
step: 4,
|
|
7949
|
+
scale: "log",
|
|
7950
|
+
marks: [
|
|
7951
|
+
4,
|
|
7952
|
+
8,
|
|
7953
|
+
16,
|
|
7954
|
+
32
|
|
7955
|
+
],
|
|
7956
|
+
default: 32,
|
|
7957
|
+
describe: "The number of bands of the signature. Two ways with one equal band are a candidate pair."
|
|
7958
|
+
},
|
|
7959
|
+
near: {
|
|
7960
|
+
type: "number",
|
|
7961
|
+
min: 0,
|
|
7962
|
+
max: 8,
|
|
7963
|
+
step: 1,
|
|
7964
|
+
default: 0,
|
|
7965
|
+
describe: "Two link ends nearer than this grid size are one point. With 0, only equal points are one point."
|
|
7966
|
+
},
|
|
7967
|
+
lane: {
|
|
7968
|
+
type: "number",
|
|
7969
|
+
min: 1,
|
|
7970
|
+
max: 16,
|
|
7971
|
+
step: 1,
|
|
7972
|
+
default: 4,
|
|
7973
|
+
describe: "The gap between two lanes. A common stretch shorter than the break-even overlap for this gap does not join two edges."
|
|
7974
|
+
}
|
|
7975
|
+
};
|
|
7477
7976
|
/** The declarations of the limits of the tree of sections (`SectionOptions`). */
|
|
7478
7977
|
const SECTION_SCHEMA = {
|
|
7479
7978
|
cap: {
|
|
@@ -7926,9 +8425,46 @@ const contextOf = (input) => ({
|
|
|
7926
8425
|
...input.endNormal ? { endNormal: input.endNormal } : {},
|
|
7927
8426
|
...input.space ? { space: input.space } : {}
|
|
7928
8427
|
});
|
|
8428
|
+
/** The curves of the lanes of each board, for the last options and the last free space. The strategy `highway` draws one lane in each call. */
|
|
8429
|
+
const laneCurves = /* @__PURE__ */ new WeakMap();
|
|
8430
|
+
/**
|
|
8431
|
+
* This function gives the curve of one lane of a board with `highwayCurves` and each option of the strategy `highway`.
|
|
8432
|
+
* The lanes of a bundle bend as one. So the function draws all the lanes of the board one time, and keeps them for the next lane.
|
|
8433
|
+
*/
|
|
8434
|
+
function highwayLane(input, o) {
|
|
8435
|
+
const { board, index } = input.lane;
|
|
8436
|
+
const key = JSON.stringify([
|
|
8437
|
+
o.radius,
|
|
8438
|
+
o.minRadius,
|
|
8439
|
+
o.jog,
|
|
8440
|
+
o.jogPieces
|
|
8441
|
+
]);
|
|
8442
|
+
let hit = laneCurves.get(board);
|
|
8443
|
+
if (!hit || hit.key !== key || hit.space !== input.space) {
|
|
8444
|
+
const curves = highwayCurves(board.graph, board.open, board.width, {
|
|
8445
|
+
radius: o.radius,
|
|
8446
|
+
minRadius: o.minRadius,
|
|
8447
|
+
jog: o.jog,
|
|
8448
|
+
jogPieces: o.jogPieces,
|
|
8449
|
+
...board.maxWidth === void 0 ? {} : { maxWidth: board.maxWidth },
|
|
8450
|
+
...input.space ? { space: input.space } : {}
|
|
8451
|
+
});
|
|
8452
|
+
hit = {
|
|
8453
|
+
key,
|
|
8454
|
+
space: input.space,
|
|
8455
|
+
curves
|
|
8456
|
+
};
|
|
8457
|
+
laneCurves.set(board, hit);
|
|
8458
|
+
}
|
|
8459
|
+
const lane = hit.curves[index];
|
|
8460
|
+
return lane ? [...lane.segments] : highwayCurve(input.points, {
|
|
8461
|
+
radius: o.radius,
|
|
8462
|
+
...input.space ? { space: input.space } : {}
|
|
8463
|
+
});
|
|
8464
|
+
}
|
|
7929
8465
|
/**
|
|
7930
8466
|
* The built-in curve strategies. The first six are in the order of `CURVE_TYPES`, and each one gives the same segments as `curveOf` with its type.
|
|
7931
|
-
* The strategy `highway` gives the curve of `arc` for one polyline. `
|
|
8467
|
+
* The strategy `highway` gives the curve of `arc` for one polyline. For a polyline with a `lane`, it bends the lanes of each bundle as one with `highwayCurves`.
|
|
7932
8468
|
*/
|
|
7933
8469
|
const curveStrategies = Object.freeze({
|
|
7934
8470
|
straight: defineStrategy({
|
|
@@ -8015,7 +8551,10 @@ const curveStrategies = Object.freeze({
|
|
|
8015
8551
|
jogPieces: 4
|
|
8016
8552
|
},
|
|
8017
8553
|
schema: HIGHWAY_SCHEMA,
|
|
8018
|
-
run: (input, o) =>
|
|
8554
|
+
run: (input, o) => input.lane ? highwayLane({
|
|
8555
|
+
...input,
|
|
8556
|
+
lane: input.lane
|
|
8557
|
+
}, o) : highwayCurve(input.points, {
|
|
8019
8558
|
radius: o.radius,
|
|
8020
8559
|
...input.space ? { space: input.space } : {}
|
|
8021
8560
|
})
|
|
@@ -8088,10 +8627,44 @@ const groupStrategies = Object.freeze({
|
|
|
8088
8627
|
run: (graph, o) => cutGroups(graph, o)
|
|
8089
8628
|
})
|
|
8090
8629
|
});
|
|
8630
|
+
/**
|
|
8631
|
+
* The built-in highway strategies: the decisions of the routers `trunks` and `shared` as plans, and corridor signatures (HWY-031).
|
|
8632
|
+
* `follow` routes the edges along a plan, and `highwayRouter` gives a router that does both.
|
|
8633
|
+
*/
|
|
8634
|
+
const highwayStrategies = Object.freeze({
|
|
8635
|
+
keys: defineStrategy({
|
|
8636
|
+
step: "highway",
|
|
8637
|
+
name: "keys",
|
|
8638
|
+
describe: "The highways of the router `trunks`. The edges from one plug with one key share a trunk, and the edges that go on together share a run past it.",
|
|
8639
|
+
defaults: { rule: byCollection },
|
|
8640
|
+
schema: TRUNKS_SCHEMA,
|
|
8641
|
+
maxNodes: 1e4,
|
|
8642
|
+
run: (input, o) => keysPlan(input, o)
|
|
8643
|
+
}),
|
|
8644
|
+
"shared-ways": defineStrategy({
|
|
8645
|
+
step: "highway",
|
|
8646
|
+
name: "shared-ways",
|
|
8647
|
+
describe: "The highways of the router `shared`. The edges of one key share their ways through the corner graph, and each shared stretch is a highway.",
|
|
8648
|
+
defaults: { rule: byCollection },
|
|
8649
|
+
schema: SHARED_SCHEMA,
|
|
8650
|
+
maxNodes: 1e4,
|
|
8651
|
+
run: (input, o) => sharedWaysPlan(input, o)
|
|
8652
|
+
}),
|
|
8653
|
+
corridors: defineStrategy({
|
|
8654
|
+
step: "highway",
|
|
8655
|
+
name: "corridors",
|
|
8656
|
+
describe: "Corridor signatures. Edges whose first ways share long links make a cluster, found with MinHash. The longest common run of each cluster is its highway.",
|
|
8657
|
+
defaults: { ...CORRIDOR_DEFAULTS },
|
|
8658
|
+
schema: CORRIDOR_SCHEMA,
|
|
8659
|
+
maxNodes: 1e4,
|
|
8660
|
+
run: (input, o) => corridorPlan(input, o)
|
|
8661
|
+
})
|
|
8662
|
+
});
|
|
8091
8663
|
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
8092
8664
|
const BUILT_IN_STRATEGIES = Object.freeze([
|
|
8093
8665
|
...Object.values(groupStrategies),
|
|
8094
8666
|
...Object.values(routerStrategies),
|
|
8667
|
+
...Object.values(highwayStrategies),
|
|
8095
8668
|
...Object.values(curveStrategies),
|
|
8096
8669
|
...Object.values(roomStrategies)
|
|
8097
8670
|
]);
|
|
@@ -10058,6 +10631,194 @@ function paretoFront(items, values, objectives) {
|
|
|
10058
10631
|
const v = items.map(values);
|
|
10059
10632
|
return items.filter((_, i) => !v.some((w, j) => j !== i && dominates(w, v[i], objectives)));
|
|
10060
10633
|
}
|
|
10634
|
+
// src/highways/follow.ts
|
|
10635
|
+
const SAME = 1e-6;
|
|
10636
|
+
const dedupe = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > SAME);
|
|
10637
|
+
/** This function tells if a polyline holds the points of `part` in a row, in this order. */
|
|
10638
|
+
function holdsInOrder(p, part) {
|
|
10639
|
+
for (let i = 0; i + part.length <= p.length; i++) {
|
|
10640
|
+
let ok = true;
|
|
10641
|
+
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME;
|
|
10642
|
+
if (ok) return true;
|
|
10643
|
+
}
|
|
10644
|
+
return false;
|
|
10645
|
+
}
|
|
10646
|
+
/** This function tells if a polyline holds the points of `part` in a row, in either direction. */
|
|
10647
|
+
const holds = (p, part) => holdsInOrder(p, part) || holdsInOrder(p, [...part].reverse());
|
|
10648
|
+
/**
|
|
10649
|
+
* This function tells if a point of a spine can be the plug of an end. The point is on the box of the node.
|
|
10650
|
+
* When the end has a fixed plug, the point is at that plug.
|
|
10651
|
+
*/
|
|
10652
|
+
function plugAt(node, end, p) {
|
|
10653
|
+
if (pointBoxDistance(p, node.box) > SAME) return false;
|
|
10654
|
+
const fixed = fixedPortPoint(node, end);
|
|
10655
|
+
return fixed === void 0 || dist(fixed, p) <= SAME;
|
|
10656
|
+
}
|
|
10657
|
+
/** This function gives the points of the stops from the place `on` to the place `off`. When `on` is larger than `off`, the points go backward. */
|
|
10658
|
+
function slice(stops, on, off) {
|
|
10659
|
+
const lo = Math.min(on, off);
|
|
10660
|
+
const hi = Math.max(on, off);
|
|
10661
|
+
const out = stops.filter(([at]) => at >= lo && at <= hi).map(([, p]) => p);
|
|
10662
|
+
return on <= off ? out : out.reverse();
|
|
10663
|
+
}
|
|
10664
|
+
/**
|
|
10665
|
+
* This function routes the edges along a highway plan.
|
|
10666
|
+
*
|
|
10667
|
+
* An edge of a highway goes from its source plug to its merge point. Then it goes along the spine to its split point, and to its target plug.
|
|
10668
|
+
* The ramps go around the boxes through the corner graph (`WayFinder.fromPlug` and `WayFinder.toPlug`).
|
|
10669
|
+
* A spine point on the box of the node is the plug itself. When the end has a fixed plug, the point is at that plug.
|
|
10670
|
+
*
|
|
10671
|
+
* An edge on several highways takes them in the order of `seq`.
|
|
10672
|
+
* A way through the corner graph goes from each split point to the next merge point.
|
|
10673
|
+
* An edge outside the plan keeps its route from `options.routes`, or goes as in the router `corners`.
|
|
10674
|
+
*
|
|
10675
|
+
* The result has a shared run for each stretch of a spine where the same two edges or more go. Each run holds the points of its routes in a row.
|
|
10676
|
+
* The same graph, plan and options give the same result.
|
|
10677
|
+
*/
|
|
10678
|
+
function follow(graph, plan, options = {}) {
|
|
10679
|
+
const byId = nodeIndex(graph);
|
|
10680
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
10681
|
+
const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
|
|
10682
|
+
const lines = plan.highways.map((h) => {
|
|
10683
|
+
const stops = new Map(h.spine.map((p, i) => [i, p]));
|
|
10684
|
+
for (const m of h.members) for (const at of [m.on, m.off]) if (!stops.has(at)) stops.set(at, pointAt(h.spine, at));
|
|
10685
|
+
return [...stops].sort((a, b) => a[0] - b[0]);
|
|
10686
|
+
});
|
|
10687
|
+
const trips = /* @__PURE__ */ new Map();
|
|
10688
|
+
plan.highways.forEach((h, hi) => {
|
|
10689
|
+
for (const m of h.members) {
|
|
10690
|
+
const list = trips.get(m.edge);
|
|
10691
|
+
if (list) list.push({
|
|
10692
|
+
h: hi,
|
|
10693
|
+
m
|
|
10694
|
+
});
|
|
10695
|
+
else trips.set(m.edge, [{
|
|
10696
|
+
h: hi,
|
|
10697
|
+
m
|
|
10698
|
+
}]);
|
|
10699
|
+
}
|
|
10700
|
+
});
|
|
10701
|
+
for (const list of trips.values()) list.sort((a, b) => a.m.seq - b.m.seq || a.h - b.h);
|
|
10702
|
+
const routes = [];
|
|
10703
|
+
for (const e of graph.edges) {
|
|
10704
|
+
const s = byId.get(e.source.node);
|
|
10705
|
+
const t = byId.get(e.target.node);
|
|
10706
|
+
if (!s || !t) continue;
|
|
10707
|
+
const trip = trips.get(e.id);
|
|
10708
|
+
if (!trip) {
|
|
10709
|
+
const known = base.get(e.id);
|
|
10710
|
+
if (known) {
|
|
10711
|
+
routes.push({
|
|
10712
|
+
edge: e.id,
|
|
10713
|
+
points: known
|
|
10714
|
+
});
|
|
10715
|
+
continue;
|
|
10716
|
+
}
|
|
10717
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
10718
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
10719
|
+
routes.push({
|
|
10720
|
+
edge: e.id,
|
|
10721
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
10722
|
+
});
|
|
10723
|
+
continue;
|
|
10724
|
+
}
|
|
10725
|
+
const out = [];
|
|
10726
|
+
let at;
|
|
10727
|
+
for (const { h, m } of trip) {
|
|
10728
|
+
const piece = slice(lines[h], m.on, m.off);
|
|
10729
|
+
const first = piece[0];
|
|
10730
|
+
if (at === void 0) {
|
|
10731
|
+
if (!plugAt(s, e.source, first)) {
|
|
10732
|
+
const ps = placePlug(s, e.source, first);
|
|
10733
|
+
out.push(...ways.fromPlug(ps, first) ?? [ps.point, first]);
|
|
10734
|
+
}
|
|
10735
|
+
} else if (dist(at, first) > SAME) {
|
|
10736
|
+
const way = ways.corners.path(at, first);
|
|
10737
|
+
out.push(...way ? ways.corners.pull(way) : [at, first]);
|
|
10738
|
+
}
|
|
10739
|
+
out.push(...piece);
|
|
10740
|
+
at = piece[piece.length - 1];
|
|
10741
|
+
}
|
|
10742
|
+
if (at !== void 0 && !plugAt(t, e.target, at)) {
|
|
10743
|
+
const pt = placePlug(t, e.target, at);
|
|
10744
|
+
out.push(...ways.toPlug(at, pt) ?? [at, pt.point]);
|
|
10745
|
+
}
|
|
10746
|
+
routes.push({
|
|
10747
|
+
edge: e.id,
|
|
10748
|
+
points: dedupe(out)
|
|
10749
|
+
});
|
|
10750
|
+
}
|
|
10751
|
+
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
10752
|
+
const runs = [];
|
|
10753
|
+
plan.highways.forEach((h, hi) => {
|
|
10754
|
+
const stops = lines[hi];
|
|
10755
|
+
const usersOf = (k) => h.members.filter((m) => routeOf.has(m.edge) && Math.min(m.on, m.off) <= stops[k][0] && Math.max(m.on, m.off) >= stops[k + 1][0]).map((m) => m.edge).sort();
|
|
10756
|
+
let k = 0;
|
|
10757
|
+
while (k < stops.length - 1) {
|
|
10758
|
+
const users = usersOf(k);
|
|
10759
|
+
let end = k + 1;
|
|
10760
|
+
while (end < stops.length - 1 && usersOf(end).join("\0") === users.join("\0")) end++;
|
|
10761
|
+
const points = dedupe(stops.slice(k, end + 1).map(([, p]) => p));
|
|
10762
|
+
k = end;
|
|
10763
|
+
if (users.length < 2 || points.length < 2) continue;
|
|
10764
|
+
const mine = users.map((id) => routeOf.get(id)).filter((r) => holds(r.points, points));
|
|
10765
|
+
if (mine.length < 2) continue;
|
|
10766
|
+
runs.push({
|
|
10767
|
+
id: `r${runs.length}`,
|
|
10768
|
+
key: h.key,
|
|
10769
|
+
points,
|
|
10770
|
+
edges: orderLanes(points, mine, options.laneOrder ?? "bundle")
|
|
10771
|
+
});
|
|
10772
|
+
}
|
|
10773
|
+
});
|
|
10774
|
+
return runs.length > 0 ? {
|
|
10775
|
+
routes,
|
|
10776
|
+
runs
|
|
10777
|
+
} : { routes };
|
|
10778
|
+
}
|
|
10779
|
+
// src/highways/router.ts
|
|
10780
|
+
/** This function gives the options of the way finder in the options of a strategy: `margin` and `k`, when they are present. */
|
|
10781
|
+
function wayOptionsOf(options) {
|
|
10782
|
+
const o = options;
|
|
10783
|
+
return {
|
|
10784
|
+
...typeof o.margin === "number" ? { margin: o.margin } : {},
|
|
10785
|
+
...typeof o.k === "number" ? { k: o.k } : {}
|
|
10786
|
+
};
|
|
10787
|
+
}
|
|
10788
|
+
/**
|
|
10789
|
+
* This function gives a router that runs a highway strategy and then `follow`. The strategy gets the graph without first routes.
|
|
10790
|
+
* The way finder of `follow` takes `margin` and `k` from the options of the strategy, and then from `followOptions`.
|
|
10791
|
+
* The router keeps the name and the `maxNodes` of the strategy.
|
|
10792
|
+
*/
|
|
10793
|
+
function highwayRouter(strategy, options, followOptions = {}) {
|
|
10794
|
+
return {
|
|
10795
|
+
name: strategy.name,
|
|
10796
|
+
...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
|
|
10797
|
+
route: (graph) => follow(graph, strategy.run({ graph }, options), {
|
|
10798
|
+
...followOptions,
|
|
10799
|
+
...wayOptionsOf({
|
|
10800
|
+
...strategy.defaults,
|
|
10801
|
+
...options
|
|
10802
|
+
})
|
|
10803
|
+
})
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10806
|
+
/**
|
|
10807
|
+
* This function gives a router strategy that runs a highway strategy and then `follow`.
|
|
10808
|
+
* The router strategy has the name, the options and the schema of the highway strategy.
|
|
10809
|
+
* The benchmark and the screen race such a router with the other routers.
|
|
10810
|
+
*/
|
|
10811
|
+
function highwayRouterStrategy(strategy) {
|
|
10812
|
+
return defineStrategy({
|
|
10813
|
+
step: "router",
|
|
10814
|
+
name: strategy.name,
|
|
10815
|
+
describe: strategy.describe,
|
|
10816
|
+
defaults: strategy.defaults,
|
|
10817
|
+
...strategy.schema === void 0 ? {} : { schema: strategy.schema },
|
|
10818
|
+
...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
|
|
10819
|
+
run: (graph, options) => follow(graph, strategy.run({ graph }, options), wayOptionsOf(options))
|
|
10820
|
+
});
|
|
10821
|
+
}
|
|
10061
10822
|
// src/bench/suite.ts
|
|
10062
10823
|
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
10063
10824
|
/** The names of the proposed hard rules of MEAS0. The suite counts them. It does not apply them as rules yet. */
|
|
@@ -10119,6 +10880,24 @@ const FRONT_OBJECTIVES = {
|
|
|
10119
10880
|
better: "min"
|
|
10120
10881
|
}
|
|
10121
10882
|
],
|
|
10883
|
+
highway: [
|
|
10884
|
+
{
|
|
10885
|
+
key: "R1.perEdge",
|
|
10886
|
+
better: "min"
|
|
10887
|
+
},
|
|
10888
|
+
{
|
|
10889
|
+
key: "R5.median",
|
|
10890
|
+
better: "min"
|
|
10891
|
+
},
|
|
10892
|
+
{
|
|
10893
|
+
key: "B1.inkSaved",
|
|
10894
|
+
better: "max"
|
|
10895
|
+
},
|
|
10896
|
+
{
|
|
10897
|
+
key: "R4.turn",
|
|
10898
|
+
better: "min"
|
|
10899
|
+
}
|
|
10900
|
+
],
|
|
10122
10901
|
room: [
|
|
10123
10902
|
{
|
|
10124
10903
|
key: "M1.area",
|
|
@@ -10210,18 +10989,23 @@ function curveRuns(graph, result, curves, options) {
|
|
|
10210
10989
|
const u = boardUnit(graph);
|
|
10211
10990
|
const routes = [];
|
|
10212
10991
|
const ids = [];
|
|
10213
|
-
|
|
10992
|
+
const board = options.lanes;
|
|
10993
|
+
(board ? laneRoutes(board.graph, board.open, board.width, board.maxWidth) : result.routes).forEach((r, i) => {
|
|
10214
10994
|
const e = edges.get(r.edge);
|
|
10215
10995
|
const s = e && byId.get(e.source.node);
|
|
10216
10996
|
const t = e && byId.get(e.target.node);
|
|
10217
|
-
if (!s || !t || r.points.length < 2)
|
|
10997
|
+
if (!s || !t || r.points.length < 2) return;
|
|
10218
10998
|
ids.push(r.edge);
|
|
10219
|
-
|
|
10220
|
-
points: r.points,
|
|
10999
|
+
const normals = {
|
|
10221
11000
|
startNormal: SIDE_NORMAL[nearestSide(s.box, r.points[0])],
|
|
10222
11001
|
endNormal: SIDE_NORMAL[nearestSide(t.box, r.points[r.points.length - 1])]
|
|
11002
|
+
};
|
|
11003
|
+
routes.push({
|
|
11004
|
+
points: r.points,
|
|
11005
|
+
...normals,
|
|
11006
|
+
...board ? { lane: i } : {}
|
|
10223
11007
|
});
|
|
10224
|
-
}
|
|
11008
|
+
});
|
|
10225
11009
|
const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
|
|
10226
11010
|
return curves.map((curve) => {
|
|
10227
11011
|
const own = options.curveOptions?.[curve.name];
|
|
@@ -10230,7 +11014,11 @@ function curveRuns(graph, result, curves, options) {
|
|
|
10230
11014
|
points: r.points,
|
|
10231
11015
|
startNormal: r.startNormal,
|
|
10232
11016
|
endNormal: r.endNormal,
|
|
10233
|
-
space
|
|
11017
|
+
space,
|
|
11018
|
+
...board && r.lane !== void 0 ? { lane: {
|
|
11019
|
+
board,
|
|
11020
|
+
index: r.lane
|
|
11021
|
+
} } : {}
|
|
10234
11022
|
}, own));
|
|
10235
11023
|
const samples = drawn.map((d) => sampleCurve(d, .25));
|
|
10236
11024
|
const ms = now() - t0;
|
|
@@ -10362,8 +11150,8 @@ function groupRun(graph, strategy, options, change, at) {
|
|
|
10362
11150
|
};
|
|
10363
11151
|
}
|
|
10364
11152
|
/**
|
|
10365
|
-
* This function gives the options of a grouping rule on a fixture. The rule `keys` gets
|
|
10366
|
-
* the collection of the first edge into the node. A node without such an edge is a group alone.
|
|
11153
|
+
* This function gives the options of a grouping rule on a fixture. The rule `keys` gets the string `data.group` of the node.
|
|
11154
|
+
* A node without it gets a key from the data of the edges: the collection of the first edge into the node. A node without such an edge is a group alone.
|
|
10367
11155
|
*/
|
|
10368
11156
|
function fixtureGroupOptions(strategy, graph) {
|
|
10369
11157
|
if (strategy !== "keys") return {};
|
|
@@ -10372,7 +11160,7 @@ function fixtureGroupOptions(strategy, graph) {
|
|
|
10372
11160
|
const c = e.data?.["collection"];
|
|
10373
11161
|
if (typeof c === "string" && !key.has(e.target.node)) key.set(e.target.node, c);
|
|
10374
11162
|
}
|
|
10375
|
-
return { key: (n) => key.get(n.id) };
|
|
11163
|
+
return { key: (n, i) => byDataGroup(n, i) ?? key.get(n.id) };
|
|
10376
11164
|
}
|
|
10377
11165
|
/**
|
|
10378
11166
|
* This function starts the measure suite of MEAS1. The suite measures each strategy of each step on each seed of each fixture family.
|
|
@@ -10400,10 +11188,11 @@ function runSuite(options = {}) {
|
|
|
10400
11188
|
]);
|
|
10401
11189
|
const normals = new Set(options.normalCurves ?? ["spline"]);
|
|
10402
11190
|
const log = options.log ?? (() => {});
|
|
10403
|
-
const fixtures = steps.has("router") || steps.has("group") ? (options.fixtures ?? FIXTURES).filter((f) => f.nodes <= max) : [];
|
|
11191
|
+
const fixtures = steps.has("router") || steps.has("group") || steps.has("highway") ? (options.fixtures ?? FIXTURES).filter((f) => f.nodes <= max) : [];
|
|
10404
11192
|
const routers = steps.has("router") ? listStrategies(registry, "router").filter((s) => options.routers?.includes(s.name) ?? true) : [];
|
|
10405
11193
|
const curves = steps.has("curve") ? listStrategies(registry, "curve") : [];
|
|
10406
11194
|
const groups = steps.has("group") ? listStrategies(registry, "group") : [];
|
|
11195
|
+
const highways = steps.has("highway") ? listStrategies(registry, "highway").map((s) => highwayRouterStrategy(s)) : [];
|
|
10407
11196
|
const rooms = listStrategies(registry, "room");
|
|
10408
11197
|
const rows = [];
|
|
10409
11198
|
const bench = [];
|
|
@@ -10455,6 +11244,38 @@ function runSuite(options = {}) {
|
|
|
10455
11244
|
normals
|
|
10456
11245
|
}).map((c) => c.row));
|
|
10457
11246
|
}
|
|
11247
|
+
for (const strategy of highways) {
|
|
11248
|
+
if (graph.nodes.length > (strategy.maxNodes ?? Infinity)) continue;
|
|
11249
|
+
const router = asRouter(strategy);
|
|
11250
|
+
const t0 = now();
|
|
11251
|
+
const { row, result } = benchRun(router, spec.name, graph, 2, cellLimit);
|
|
11252
|
+
const ms = now() - t0;
|
|
11253
|
+
const t1 = now();
|
|
11254
|
+
const values = routeMeasures(graph, result, row);
|
|
11255
|
+
const measureMs = now() - t1;
|
|
11256
|
+
let extraMs = 0;
|
|
11257
|
+
if (graph.nodes.length <= stabilityMax) {
|
|
11258
|
+
const t2 = now();
|
|
11259
|
+
const s = routeStability(graph, result, router.route(change.graph), { seed });
|
|
11260
|
+
extraMs = now() - t2;
|
|
11261
|
+
values["Z10.mean"] = s.mean;
|
|
11262
|
+
values["Z10.max"] = s.max;
|
|
11263
|
+
values["Z10.moved"] = s.moved;
|
|
11264
|
+
}
|
|
11265
|
+
rows.push({
|
|
11266
|
+
step: "highway",
|
|
11267
|
+
fixture: spec.name,
|
|
11268
|
+
seed: k,
|
|
11269
|
+
strategy: strategy.name,
|
|
11270
|
+
nodes: row.nodes,
|
|
11271
|
+
edges: row.edges,
|
|
11272
|
+
violations: row.violations + (row.deterministic ? 0 : 1),
|
|
11273
|
+
ms,
|
|
11274
|
+
measureMs,
|
|
11275
|
+
extraMs,
|
|
11276
|
+
values
|
|
11277
|
+
});
|
|
11278
|
+
}
|
|
10458
11279
|
for (const strategy of groups) {
|
|
10459
11280
|
const opts = fixtureGroupOptions(strategy.name, graph);
|
|
10460
11281
|
const changed = {
|
|
@@ -10553,7 +11374,7 @@ function runSuite(options = {}) {
|
|
|
10553
11374
|
}
|
|
10554
11375
|
/**
|
|
10555
11376
|
* This function puts the rows of the suite in the order of one run. It sorts them by fixture family, then by seed.
|
|
10556
|
-
* In one seed, the routers come first, and the curves come after the router of the curves.
|
|
11377
|
+
* In one seed, the routers come first, and the curves come after the router of the curves. Then the highway strategies come, and the grouping rules and the room come last.
|
|
10557
11378
|
* A name that is not in its list goes after the names of the list.
|
|
10558
11379
|
* The workflow `Bench` uses it to join the reports of its parts.
|
|
10559
11380
|
*/
|
|
@@ -10571,8 +11392,9 @@ function orderSuiteRows(rows, options = {}) {
|
|
|
10571
11392
|
const rank = (r) => {
|
|
10572
11393
|
if (r.step === "router") return [0, 2 * place(routers, r.strategy) * (curves.length + 1)];
|
|
10573
11394
|
if (r.step === "curve") return [0, 2 * after * (curves.length + 1) + 1 + place(curves, r.strategy)];
|
|
10574
|
-
if (r.step === "
|
|
10575
|
-
|
|
11395
|
+
if (r.step === "highway") return [1, place(names("highway"), r.strategy)];
|
|
11396
|
+
if (r.step === "group") return [2, place(names("group"), r.strategy)];
|
|
11397
|
+
return [3, r.strategy === "gutter" ? -1 : place(names("room"), r.strategy)];
|
|
10576
11398
|
};
|
|
10577
11399
|
return rows.map((row, i) => ({
|
|
10578
11400
|
row,
|
|
@@ -10695,7 +11517,7 @@ function ruleCounts(rows) {
|
|
|
10695
11517
|
}
|
|
10696
11518
|
return [...out.values()];
|
|
10697
11519
|
}
|
|
10698
|
-
const fmt = (v, d = 3) => v === void 0 || Number.isNaN(v) ? "-" : Number.isInteger(v) ? String(v) : v.toFixed(d);
|
|
11520
|
+
const fmt$1 = (v, d = 3) => v === void 0 || Number.isNaN(v) ? "-" : Number.isInteger(v) ? String(v) : v.toFixed(d);
|
|
10699
11521
|
/** The columns of the tables of `formatSuite`: the keys of the measures of each step. */
|
|
10700
11522
|
const SUITE_COLUMNS = {
|
|
10701
11523
|
router: [
|
|
@@ -10713,6 +11535,18 @@ const SUITE_COLUMNS = {
|
|
|
10713
11535
|
"Z7.position",
|
|
10714
11536
|
"G8.treeCost"
|
|
10715
11537
|
],
|
|
11538
|
+
highway: [
|
|
11539
|
+
"R1.perEdge",
|
|
11540
|
+
"R2.angle",
|
|
11541
|
+
"R2.min",
|
|
11542
|
+
"B1.inkSaved",
|
|
11543
|
+
"R5.median",
|
|
11544
|
+
"R5.q95",
|
|
11545
|
+
"R4.turn",
|
|
11546
|
+
"R11.q05",
|
|
11547
|
+
"B4.ambiguity",
|
|
11548
|
+
"Z10.moved"
|
|
11549
|
+
],
|
|
10716
11550
|
curve: [
|
|
10717
11551
|
"C1.share",
|
|
10718
11552
|
"C1.max",
|
|
@@ -10753,11 +11587,11 @@ function formatSummaries(summaries, step, columns = SUITE_COLUMNS[step]) {
|
|
|
10753
11587
|
...summaries.filter((s) => s.step === step).map((s) => {
|
|
10754
11588
|
const cells = columns.map((c) => {
|
|
10755
11589
|
const m = s.measures[c];
|
|
10756
|
-
return m && m.n > 0 ? `${fmt(m.median)} (${fmt(m.iqr)})` : "-";
|
|
11590
|
+
return m && m.n > 0 ? `${fmt$1(m.median)} (${fmt$1(m.iqr)})` : "-";
|
|
10757
11591
|
});
|
|
10758
11592
|
if (step === "router") {
|
|
10759
11593
|
const c = s.measures["R1.crossings"];
|
|
10760
|
-
cells.push(c && c.median > 0 ? fmt(c.iqr / c.median) : "-");
|
|
11594
|
+
cells.push(c && c.median > 0 ? fmt$1(c.iqr / c.median) : "-");
|
|
10761
11595
|
}
|
|
10762
11596
|
return `| ${s.strategy} | ${s.fixture} | ${s.seeds} | ${s.violations} | ${cells.join(" | ")} |`;
|
|
10763
11597
|
})
|
|
@@ -11021,6 +11855,31 @@ function medianOf(values) {
|
|
|
11021
11855
|
const [a, b] = [s[mid - 1], s[mid]];
|
|
11022
11856
|
return a === b ? a : (a + b) / 2;
|
|
11023
11857
|
}
|
|
11858
|
+
/**
|
|
11859
|
+
* This function gives the measures of the level links of a result: the links through a box (`Z3.through`) and the length of all links (`Z3.length`).
|
|
11860
|
+
* A result without a level with groups gives no measure.
|
|
11861
|
+
*/
|
|
11862
|
+
function levelLinkMeasures(result) {
|
|
11863
|
+
const levels = (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1));
|
|
11864
|
+
if (levels.length === 0) return {};
|
|
11865
|
+
const length = levels.reduce((sum, l) => sum + l.links.reduce((s, k) => s + polylineLength(k.points), 0), 0);
|
|
11866
|
+
return {
|
|
11867
|
+
"Z3.through": sumFaults(measureLevelFaults(result)).through,
|
|
11868
|
+
"Z3.length": length
|
|
11869
|
+
};
|
|
11870
|
+
}
|
|
11871
|
+
/** This function gives the lanes of a result with each shared segment open. A result without shared segments gives `undefined`. */
|
|
11872
|
+
function laneBoard(result, lanes) {
|
|
11873
|
+
const graph = segmentsOf(result);
|
|
11874
|
+
const open = new Set(graph.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
|
|
11875
|
+
if (open.size === 0) return void 0;
|
|
11876
|
+
return {
|
|
11877
|
+
graph,
|
|
11878
|
+
open,
|
|
11879
|
+
width: lanes.width,
|
|
11880
|
+
...lanes.maxWidth === void 0 ? {} : { maxWidth: lanes.maxWidth }
|
|
11881
|
+
};
|
|
11882
|
+
}
|
|
11024
11883
|
/** The steps that `compareStep` compares, in the order of the pipeline. */
|
|
11025
11884
|
const COMPARE_STEPS = [
|
|
11026
11885
|
"group",
|
|
@@ -11029,7 +11888,7 @@ const COMPARE_STEPS = [
|
|
|
11029
11888
|
];
|
|
11030
11889
|
/**
|
|
11031
11890
|
* This function runs each strategy of one step on one graph and measures it with the measures of `runSuite`. Then it gives the Pareto front.
|
|
11032
|
-
* The step `router` routes the graph with each router. The step `curve` draws the routes of `curveRouter` with each curve
|
|
11891
|
+
* The step `router` routes the graph with each router. The step `curve` draws the routes of `curveRouter` with each curve, or their lanes with the option `lanes`.
|
|
11033
11892
|
* The step `group` routes the graph with `sections` on the tree of each grouping rule.
|
|
11034
11893
|
*
|
|
11035
11894
|
* The same graph and the same options give the same measures. Only the times change.
|
|
@@ -11060,6 +11919,7 @@ function compareStep(step, graph, options = {}) {
|
|
|
11060
11919
|
const { row, result } = benchRun(router, name, graph, 2);
|
|
11061
11920
|
const values = {
|
|
11062
11921
|
...routeMeasures(graph, result, row),
|
|
11922
|
+
...levelLinkMeasures(result),
|
|
11063
11923
|
ink: row.quality.ink,
|
|
11064
11924
|
ms: row.ms
|
|
11065
11925
|
};
|
|
@@ -11081,6 +11941,7 @@ function compareStep(step, graph, options = {}) {
|
|
|
11081
11941
|
} else if (step === "curve") {
|
|
11082
11942
|
const result = asRouter(getStrategy(registry, "router", options.curveRouter ?? "corners")).route(graph);
|
|
11083
11943
|
const u = boardUnit(graph);
|
|
11944
|
+
const lanes = options.lanes && laneBoard(result, options.lanes);
|
|
11084
11945
|
rows = curveRuns(graph, result, listStrategies(registry, "curve"), {
|
|
11085
11946
|
...at,
|
|
11086
11947
|
smooth: /* @__PURE__ */ new Set([
|
|
@@ -11092,7 +11953,8 @@ function compareStep(step, graph, options = {}) {
|
|
|
11092
11953
|
"highway"
|
|
11093
11954
|
]),
|
|
11094
11955
|
normals: /* @__PURE__ */ new Set(["spline"]),
|
|
11095
|
-
curveOptions: own
|
|
11956
|
+
curveOptions: own,
|
|
11957
|
+
...lanes ? { lanes } : {}
|
|
11096
11958
|
}).map(({ row, drawn, edges }) => {
|
|
11097
11959
|
const peaks = drawn.map((d) => peakCurvature(d) * u);
|
|
11098
11960
|
const values = {
|
|
@@ -11151,6 +12013,1442 @@ function compareStep(step, graph, options = {}) {
|
|
|
11151
12013
|
excluded: rows.filter((r) => !fair.includes(r)).map((r) => r.strategy)
|
|
11152
12014
|
};
|
|
11153
12015
|
}
|
|
11154
|
-
|
|
12016
|
+
// src/bench/screen/hash.ts
|
|
12017
|
+
const K = [
|
|
12018
|
+
1116352408,
|
|
12019
|
+
1899447441,
|
|
12020
|
+
3049323471,
|
|
12021
|
+
3921009573,
|
|
12022
|
+
961987163,
|
|
12023
|
+
1508970993,
|
|
12024
|
+
2453635748,
|
|
12025
|
+
2870763221,
|
|
12026
|
+
3624381080,
|
|
12027
|
+
310598401,
|
|
12028
|
+
607225278,
|
|
12029
|
+
1426881987,
|
|
12030
|
+
1925078388,
|
|
12031
|
+
2162078206,
|
|
12032
|
+
2614888103,
|
|
12033
|
+
3248222580,
|
|
12034
|
+
3835390401,
|
|
12035
|
+
4022224774,
|
|
12036
|
+
264347078,
|
|
12037
|
+
604807628,
|
|
12038
|
+
770255983,
|
|
12039
|
+
1249150122,
|
|
12040
|
+
1555081692,
|
|
12041
|
+
1996064986,
|
|
12042
|
+
2554220882,
|
|
12043
|
+
2821834349,
|
|
12044
|
+
2952996808,
|
|
12045
|
+
3210313671,
|
|
12046
|
+
3336571891,
|
|
12047
|
+
3584528711,
|
|
12048
|
+
113926993,
|
|
12049
|
+
338241895,
|
|
12050
|
+
666307205,
|
|
12051
|
+
773529912,
|
|
12052
|
+
1294757372,
|
|
12053
|
+
1396182291,
|
|
12054
|
+
1695183700,
|
|
12055
|
+
1986661051,
|
|
12056
|
+
2177026350,
|
|
12057
|
+
2456956037,
|
|
12058
|
+
2730485921,
|
|
12059
|
+
2820302411,
|
|
12060
|
+
3259730800,
|
|
12061
|
+
3345764771,
|
|
12062
|
+
3516065817,
|
|
12063
|
+
3600352804,
|
|
12064
|
+
4094571909,
|
|
12065
|
+
275423344,
|
|
12066
|
+
430227734,
|
|
12067
|
+
506948616,
|
|
12068
|
+
659060556,
|
|
12069
|
+
883997877,
|
|
12070
|
+
958139571,
|
|
12071
|
+
1322822218,
|
|
12072
|
+
1537002063,
|
|
12073
|
+
1747873779,
|
|
12074
|
+
1955562222,
|
|
12075
|
+
2024104815,
|
|
12076
|
+
2227730452,
|
|
12077
|
+
2361852424,
|
|
12078
|
+
2428436474,
|
|
12079
|
+
2756734187,
|
|
12080
|
+
3204031479,
|
|
12081
|
+
3329325298
|
|
12082
|
+
];
|
|
12083
|
+
/** This function gives the bytes of a string in UTF-8. */
|
|
12084
|
+
function utf8(text) {
|
|
12085
|
+
const out = [];
|
|
12086
|
+
for (const ch of text) {
|
|
12087
|
+
const c = ch.codePointAt(0);
|
|
12088
|
+
if (c < 128) out.push(c);
|
|
12089
|
+
else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
|
|
12090
|
+
else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
12091
|
+
else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
|
|
12092
|
+
}
|
|
12093
|
+
return Uint8Array.from(out);
|
|
12094
|
+
}
|
|
12095
|
+
/** This function gives the SHA-256 digest of a string, as 64 lowercase hex characters. It encodes the string in UTF-8. */
|
|
12096
|
+
function sha256Hex(text) {
|
|
12097
|
+
const bytes = utf8(text);
|
|
12098
|
+
const bits = bytes.length * 8;
|
|
12099
|
+
const total = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
12100
|
+
const data = new Uint8Array(total);
|
|
12101
|
+
data.set(bytes);
|
|
12102
|
+
data[bytes.length] = 128;
|
|
12103
|
+
const view = new DataView(data.buffer);
|
|
12104
|
+
view.setUint32(total - 8, Math.floor(bits / 4294967296));
|
|
12105
|
+
view.setUint32(total - 4, bits >>> 0);
|
|
12106
|
+
const h = [
|
|
12107
|
+
1779033703,
|
|
12108
|
+
3144134277,
|
|
12109
|
+
1013904242,
|
|
12110
|
+
2773480762,
|
|
12111
|
+
1359893119,
|
|
12112
|
+
2600822924,
|
|
12113
|
+
528734635,
|
|
12114
|
+
1541459225
|
|
12115
|
+
];
|
|
12116
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
12117
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
12118
|
+
for (let off = 0; off < total; off += 64) {
|
|
12119
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
|
|
12120
|
+
for (let i = 16; i < 64; i++) {
|
|
12121
|
+
const a = w[i - 15];
|
|
12122
|
+
const b = w[i - 2];
|
|
12123
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
|
|
12124
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
|
|
12125
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
12126
|
+
}
|
|
12127
|
+
let [a, b, c, d, e, f, g, k] = h;
|
|
12128
|
+
for (let i = 0; i < 64; i++) {
|
|
12129
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
12130
|
+
const ch = e & f ^ ~e & g;
|
|
12131
|
+
const t1 = k + s1 + ch + K[i] + w[i] >>> 0;
|
|
12132
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
12133
|
+
k = g;
|
|
12134
|
+
g = f;
|
|
12135
|
+
f = e;
|
|
12136
|
+
e = d + t1 >>> 0;
|
|
12137
|
+
d = c;
|
|
12138
|
+
c = b;
|
|
12139
|
+
b = a;
|
|
12140
|
+
a = t1 + t2 >>> 0;
|
|
12141
|
+
}
|
|
12142
|
+
h[0] = h[0] + a >>> 0;
|
|
12143
|
+
h[1] = h[1] + b >>> 0;
|
|
12144
|
+
h[2] = h[2] + c >>> 0;
|
|
12145
|
+
h[3] = h[3] + d >>> 0;
|
|
12146
|
+
h[4] = h[4] + e >>> 0;
|
|
12147
|
+
h[5] = h[5] + f >>> 0;
|
|
12148
|
+
h[6] = h[6] + g >>> 0;
|
|
12149
|
+
h[7] = h[7] + k >>> 0;
|
|
12150
|
+
}
|
|
12151
|
+
return h.map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
12152
|
+
}
|
|
12153
|
+
/**
|
|
12154
|
+
* This function writes a value as canonical JSON: the keys of each object in sorted order, and no whitespace.
|
|
12155
|
+
* It drops a key whose value is `undefined`, as `JSON.stringify` does.
|
|
12156
|
+
*/
|
|
12157
|
+
function canonicalJson(value) {
|
|
12158
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
12159
|
+
if (Array.isArray(value)) return `[${value.map((v) => v === void 0 ? "null" : canonicalJson(v)).join(",")}]`;
|
|
12160
|
+
const o = value;
|
|
12161
|
+
return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
|
|
12162
|
+
}
|
|
12163
|
+
/**
|
|
12164
|
+
* This function gives the hash of a JSON document: the first 16 hex characters of the SHA-256 of its canonical JSON.
|
|
12165
|
+
* The document does not include its own field `hash`. The function removes that field before it calculates the hash.
|
|
12166
|
+
*/
|
|
12167
|
+
function documentHash(value) {
|
|
12168
|
+
const { hash: _hash, ...rest } = value;
|
|
12169
|
+
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
12170
|
+
}
|
|
12171
|
+
/** This function gives the 32-bit FNV-1a hash of the UTF-8 bytes of a text, as an unsigned integer. */
|
|
12172
|
+
function fnv1a32(text) {
|
|
12173
|
+
let h = 2166136261;
|
|
12174
|
+
for (const byte of utf8(text)) {
|
|
12175
|
+
h ^= byte;
|
|
12176
|
+
h = Math.imul(h, 16777619);
|
|
12177
|
+
}
|
|
12178
|
+
return h >>> 0;
|
|
12179
|
+
}
|
|
12180
|
+
/** This function gives the seed of one step of a generator: `fnv1a32("<seed>:<step>")`. */
|
|
12181
|
+
function stepSeed(seed, step) {
|
|
12182
|
+
return fnv1a32(`${seed}:${step}`);
|
|
12183
|
+
}
|
|
12184
|
+
/** This function gives the generator `mulberry32` for a seed. Each call gives the next number from 0 to below 1. */
|
|
12185
|
+
function mulberry32(seed) {
|
|
12186
|
+
let a = seed >>> 0;
|
|
12187
|
+
return () => {
|
|
12188
|
+
a = a + 1831565813 >>> 0;
|
|
12189
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
12190
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
12191
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
12192
|
+
};
|
|
12193
|
+
}
|
|
12194
|
+
/** This function rounds a number to 3 decimals, as the files of a corpus keep it. The value `-0` becomes `0`. */
|
|
12195
|
+
function round3(value) {
|
|
12196
|
+
const r = Math.round(value * 1e3) / 1e3;
|
|
12197
|
+
return r === 0 ? 0 : r;
|
|
12198
|
+
}
|
|
12199
|
+
// src/bench/screen/forms.ts
|
|
12200
|
+
const TO_FILE = {
|
|
12201
|
+
north: "top",
|
|
12202
|
+
east: "right",
|
|
12203
|
+
south: "bottom",
|
|
12204
|
+
west: "left"
|
|
12205
|
+
};
|
|
12206
|
+
const TO_MODEL = {
|
|
12207
|
+
top: "north",
|
|
12208
|
+
right: "east",
|
|
12209
|
+
bottom: "south",
|
|
12210
|
+
left: "west"
|
|
12211
|
+
};
|
|
12212
|
+
/** This function gives the side of a box that holds a point, relative to the top-left corner of the box. */
|
|
12213
|
+
function sideAt(p, w, h) {
|
|
12214
|
+
return [
|
|
12215
|
+
["top", Math.abs(p.y)],
|
|
12216
|
+
["right", Math.abs(w - p.x)],
|
|
12217
|
+
["bottom", Math.abs(h - p.y)],
|
|
12218
|
+
["left", Math.abs(p.x)]
|
|
12219
|
+
].reduce((a, b) => b[1] < a[1] ? b : a)[0];
|
|
12220
|
+
}
|
|
12221
|
+
/** This function gives 0 for -0, because JSON writes -0 as 0. Then a file and the graph that it comes from hold the same numbers. */
|
|
12222
|
+
const num = round3;
|
|
12223
|
+
function portToFile(p, n) {
|
|
12224
|
+
const side = p.side ? TO_FILE[p.side] : p.at ? sideAt(p.at, n.box.w, n.box.h) : void 0;
|
|
12225
|
+
return {
|
|
12226
|
+
id: p.id,
|
|
12227
|
+
...side ? { side } : {},
|
|
12228
|
+
...p.at ? {
|
|
12229
|
+
x: num(p.at.x),
|
|
12230
|
+
y: num(p.at.y)
|
|
12231
|
+
} : {}
|
|
12232
|
+
};
|
|
12233
|
+
}
|
|
12234
|
+
function portToModel(p) {
|
|
12235
|
+
if (p.x !== void 0 && p.y !== void 0) return {
|
|
12236
|
+
id: p.id,
|
|
12237
|
+
at: {
|
|
12238
|
+
x: p.x,
|
|
12239
|
+
y: p.y
|
|
12240
|
+
}
|
|
12241
|
+
};
|
|
12242
|
+
return {
|
|
12243
|
+
id: p.id,
|
|
12244
|
+
...p.side ? { side: TO_MODEL[p.side] } : {}
|
|
12245
|
+
};
|
|
12246
|
+
}
|
|
12247
|
+
/**
|
|
12248
|
+
* This function writes a graph as an input file of the form `layout-input/1`, with its hash.
|
|
12249
|
+
* A fixed plug gets `x`, `y` and the side that holds it. `inputGraph` reads the file back.
|
|
12250
|
+
*/
|
|
12251
|
+
function graphInput(graph, head) {
|
|
12252
|
+
const boxes = graph.nodes.map((n) => ({
|
|
12253
|
+
id: n.id,
|
|
12254
|
+
w: num(n.box.w),
|
|
12255
|
+
h: num(n.box.h),
|
|
12256
|
+
x: num(n.box.x),
|
|
12257
|
+
y: num(n.box.y),
|
|
12258
|
+
...n.ports ? { ports: n.ports.map((p) => portToFile(p, n)) } : {},
|
|
12259
|
+
...n.data ? { data: n.data } : {}
|
|
12260
|
+
}));
|
|
12261
|
+
const edges = graph.edges.map((e) => ({
|
|
12262
|
+
id: e.id,
|
|
12263
|
+
source: e.source.node,
|
|
12264
|
+
target: e.target.node,
|
|
12265
|
+
...e.source.port !== void 0 ? { sourcePort: e.source.port } : {},
|
|
12266
|
+
...e.target.port !== void 0 ? { targetPort: e.target.port } : {},
|
|
12267
|
+
...e.data ? { data: e.data } : {}
|
|
12268
|
+
}));
|
|
12269
|
+
const body = {
|
|
12270
|
+
schema: "layout-input/1",
|
|
12271
|
+
id: head.id,
|
|
12272
|
+
set: head.set,
|
|
12273
|
+
family: head.family,
|
|
12274
|
+
seed: head.seed,
|
|
12275
|
+
...head.base !== void 0 ? { base: head.base } : {},
|
|
12276
|
+
...head.change !== void 0 ? { change: head.change } : {},
|
|
12277
|
+
...head.step !== void 0 ? { step: head.step } : {},
|
|
12278
|
+
source: head.source ?? "made-up",
|
|
12279
|
+
generator: head.generator ?? null,
|
|
12280
|
+
...head.cell !== void 0 ? { cell: head.cell } : {},
|
|
12281
|
+
boxes,
|
|
12282
|
+
edges
|
|
12283
|
+
};
|
|
12284
|
+
return {
|
|
12285
|
+
...body,
|
|
12286
|
+
hash: documentHash(body)
|
|
12287
|
+
};
|
|
12288
|
+
}
|
|
12289
|
+
/**
|
|
12290
|
+
* This function gives the bounding box of a region: the smallest box that holds it.
|
|
12291
|
+
*/
|
|
12292
|
+
function regionBounds(region) {
|
|
12293
|
+
if (region.kind === "rect") return {
|
|
12294
|
+
x: region.x,
|
|
12295
|
+
y: region.y,
|
|
12296
|
+
w: region.w,
|
|
12297
|
+
h: region.h
|
|
12298
|
+
};
|
|
12299
|
+
if (region.kind === "circle") return {
|
|
12300
|
+
x: region.cx - region.r,
|
|
12301
|
+
y: region.cy - region.r,
|
|
12302
|
+
w: 2 * region.r,
|
|
12303
|
+
h: 2 * region.r
|
|
12304
|
+
};
|
|
12305
|
+
if (region.kind === "ellipse") return {
|
|
12306
|
+
x: region.cx - region.rx,
|
|
12307
|
+
y: region.cy - region.ry,
|
|
12308
|
+
w: 2 * region.rx,
|
|
12309
|
+
h: 2 * region.ry
|
|
12310
|
+
};
|
|
12311
|
+
const xs = region.points.map((p) => p[0]);
|
|
12312
|
+
const ys = region.points.map((p) => p[1]);
|
|
12313
|
+
const x = Math.min(...xs);
|
|
12314
|
+
const y = Math.min(...ys);
|
|
12315
|
+
return {
|
|
12316
|
+
x,
|
|
12317
|
+
y,
|
|
12318
|
+
w: Math.max(...xs) - x,
|
|
12319
|
+
h: Math.max(...ys) - y
|
|
12320
|
+
};
|
|
12321
|
+
}
|
|
12322
|
+
/**
|
|
12323
|
+
* This function reads the graph of the router from an input file. The router reads the positions, the ports, the edges and the data.
|
|
12324
|
+
* Each obstacle becomes a node without edges, with the bounding box of its region, after the boxes.
|
|
12325
|
+
* The router ignores the other fields of the packer: `container`, `gap`, `locked`, `rank`, the `shape` of a box and the `move` of an obstacle.
|
|
12326
|
+
* A box without `x` and `y` is an error.
|
|
12327
|
+
*/
|
|
12328
|
+
function inputGraph(input) {
|
|
12329
|
+
const nodes = input.boxes.map((b) => {
|
|
12330
|
+
if (b.x === void 0 || b.y === void 0) throw new Error(`The box "${b.id}" of the input "${input.id}" has no position. The router needs a placed input.`);
|
|
12331
|
+
return {
|
|
12332
|
+
id: b.id,
|
|
12333
|
+
box: {
|
|
12334
|
+
x: b.x,
|
|
12335
|
+
y: b.y,
|
|
12336
|
+
w: b.w,
|
|
12337
|
+
h: b.h
|
|
12338
|
+
},
|
|
12339
|
+
...b.ports ? { ports: b.ports.map(portToModel) } : {},
|
|
12340
|
+
...b.data ? { data: b.data } : {}
|
|
12341
|
+
};
|
|
12342
|
+
});
|
|
12343
|
+
for (const o of input.obstacles ?? []) nodes.push({
|
|
12344
|
+
id: o.id,
|
|
12345
|
+
box: regionBounds(o.region)
|
|
12346
|
+
});
|
|
12347
|
+
return {
|
|
12348
|
+
nodes,
|
|
12349
|
+
edges: input.edges.map((e) => ({
|
|
12350
|
+
id: e.id,
|
|
12351
|
+
source: {
|
|
12352
|
+
node: e.source,
|
|
12353
|
+
...e.sourcePort !== void 0 ? { port: e.sourcePort } : {}
|
|
12354
|
+
},
|
|
12355
|
+
target: {
|
|
12356
|
+
node: e.target,
|
|
12357
|
+
...e.targetPort !== void 0 ? { port: e.targetPort } : {}
|
|
12358
|
+
},
|
|
12359
|
+
...e.data ? { data: e.data } : {}
|
|
12360
|
+
}))
|
|
12361
|
+
};
|
|
12362
|
+
}
|
|
12363
|
+
/**
|
|
12364
|
+
* This function writes an input file as text: the keys in sorted order, and one box or one edge on each line.
|
|
12365
|
+
* `JSON.parse` of the text gives the input again.
|
|
12366
|
+
*/
|
|
12367
|
+
function formatInput(input) {
|
|
12368
|
+
const o = input;
|
|
12369
|
+
return `{\n${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => {
|
|
12370
|
+
const v = o[k];
|
|
12371
|
+
if (Array.isArray(v)) return ` ${JSON.stringify(k)}: [${v.length === 0 ? "]" : `\n${v.map((x) => ` ${canonical(x)}`).join(",\n")}\n ]`}`;
|
|
12372
|
+
return ` ${JSON.stringify(k)}: ${canonical(v)}`;
|
|
12373
|
+
}).join(",\n")}\n}\n`;
|
|
12374
|
+
}
|
|
12375
|
+
/** This function writes a value as canonical JSON with a space after each colon and comma, for a line of a file. */
|
|
12376
|
+
function canonical(value) {
|
|
12377
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
12378
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(", ")}]`;
|
|
12379
|
+
const o = value;
|
|
12380
|
+
return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}: ${canonical(o[k])}`).join(", ")}}`;
|
|
12381
|
+
}
|
|
12382
|
+
/** This function writes records as JSON Lines: one record on each line. */
|
|
12383
|
+
function recordLines(records) {
|
|
12384
|
+
return records.map((r) => JSON.stringify(r) + "\n").join("");
|
|
12385
|
+
}
|
|
12386
|
+
/** This function reads JSON Lines. It ignores an empty line. */
|
|
12387
|
+
function parseLines(text) {
|
|
12388
|
+
return text.split(/\r?\n/).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
12389
|
+
}
|
|
12390
|
+
/**
|
|
12391
|
+
* This function gives a copy of a value that JSON keeps.
|
|
12392
|
+
* A function becomes the text `function:<name>`, and a number that is not finite becomes its text.
|
|
12393
|
+
*/
|
|
12394
|
+
function jsonSafe(value) {
|
|
12395
|
+
if (typeof value === "function") return `function:${value.name || "anonymous"}`;
|
|
12396
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
12397
|
+
if (value === null || typeof value !== "object") return value;
|
|
12398
|
+
if (Array.isArray(value)) return value.map(jsonSafe);
|
|
12399
|
+
if (value instanceof Set) return [...value].map(jsonSafe);
|
|
12400
|
+
if (value instanceof Map) return Object.fromEntries([...value].map(([k, v]) => [String(k), jsonSafe(v)]));
|
|
12401
|
+
const out = {};
|
|
12402
|
+
for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = jsonSafe(v);
|
|
12403
|
+
return out;
|
|
12404
|
+
}
|
|
12405
|
+
/**
|
|
12406
|
+
* The measures of the records of the stage `route`, with the placement dependence of each one.
|
|
12407
|
+
* A joint harness that moves the boxes and routes the edges reads `placementDependent`.
|
|
12408
|
+
* It compares a measure with the value `true` only between runs on the same input.
|
|
12409
|
+
*/
|
|
12410
|
+
const MEASURES = Object.freeze([
|
|
12411
|
+
[
|
|
12412
|
+
"R1.crossings",
|
|
12413
|
+
"R1",
|
|
12414
|
+
"route.crossings",
|
|
12415
|
+
"count",
|
|
12416
|
+
"lower",
|
|
12417
|
+
true
|
|
12418
|
+
],
|
|
12419
|
+
[
|
|
12420
|
+
"R1.perEdge",
|
|
12421
|
+
"R1",
|
|
12422
|
+
"route.crossings.per-edge",
|
|
12423
|
+
"count for each edge",
|
|
12424
|
+
"lower",
|
|
12425
|
+
true
|
|
12426
|
+
],
|
|
12427
|
+
[
|
|
12428
|
+
"R2.angle",
|
|
12429
|
+
"R2",
|
|
12430
|
+
"route.crossing-angle.mean",
|
|
12431
|
+
"0 to 1",
|
|
12432
|
+
"higher",
|
|
12433
|
+
true
|
|
12434
|
+
],
|
|
12435
|
+
[
|
|
12436
|
+
"R2.min",
|
|
12437
|
+
"R2",
|
|
12438
|
+
"route.crossing-angle.min",
|
|
12439
|
+
"degrees",
|
|
12440
|
+
"higher",
|
|
12441
|
+
true
|
|
12442
|
+
],
|
|
12443
|
+
[
|
|
12444
|
+
"R3.bends",
|
|
12445
|
+
"R3",
|
|
12446
|
+
"route.bends",
|
|
12447
|
+
"count for each route",
|
|
12448
|
+
"lower",
|
|
12449
|
+
false
|
|
12450
|
+
],
|
|
12451
|
+
[
|
|
12452
|
+
"R4.turn",
|
|
12453
|
+
"R4",
|
|
12454
|
+
"route.turn.mean",
|
|
12455
|
+
"half turns for each route",
|
|
12456
|
+
"lower",
|
|
12457
|
+
false
|
|
12458
|
+
],
|
|
12459
|
+
[
|
|
12460
|
+
"R4.max",
|
|
12461
|
+
"R4",
|
|
12462
|
+
"route.turn.max",
|
|
12463
|
+
"degrees",
|
|
12464
|
+
"lower",
|
|
12465
|
+
false
|
|
12466
|
+
],
|
|
12467
|
+
[
|
|
12468
|
+
"R5.median",
|
|
12469
|
+
"R5",
|
|
12470
|
+
"route.detour.median",
|
|
12471
|
+
"ratio",
|
|
12472
|
+
"lower",
|
|
12473
|
+
false
|
|
12474
|
+
],
|
|
12475
|
+
[
|
|
12476
|
+
"R5.q95",
|
|
12477
|
+
"R5",
|
|
12478
|
+
"route.detour.q95",
|
|
12479
|
+
"ratio",
|
|
12480
|
+
"lower",
|
|
12481
|
+
false
|
|
12482
|
+
],
|
|
12483
|
+
[
|
|
12484
|
+
"R5.mean",
|
|
12485
|
+
"R5",
|
|
12486
|
+
"route.detour.mean",
|
|
12487
|
+
"ratio",
|
|
12488
|
+
"lower",
|
|
12489
|
+
false
|
|
12490
|
+
],
|
|
12491
|
+
[
|
|
12492
|
+
"R6.worstDetour",
|
|
12493
|
+
"R6",
|
|
12494
|
+
"route.detour.max",
|
|
12495
|
+
"ratio",
|
|
12496
|
+
"lower",
|
|
12497
|
+
false
|
|
12498
|
+
],
|
|
12499
|
+
[
|
|
12500
|
+
"R7.backtrack",
|
|
12501
|
+
"R7",
|
|
12502
|
+
"route.backtrack",
|
|
12503
|
+
"0 to 1",
|
|
12504
|
+
"lower",
|
|
12505
|
+
false
|
|
12506
|
+
],
|
|
12507
|
+
[
|
|
12508
|
+
"R8.clearance",
|
|
12509
|
+
"R8",
|
|
12510
|
+
"route.clearance.mean",
|
|
12511
|
+
"units",
|
|
12512
|
+
"higher",
|
|
12513
|
+
true
|
|
12514
|
+
],
|
|
12515
|
+
[
|
|
12516
|
+
"R8.tight",
|
|
12517
|
+
"R8",
|
|
12518
|
+
"route.clearance.tight",
|
|
12519
|
+
"0 to 1",
|
|
12520
|
+
"lower",
|
|
12521
|
+
true
|
|
12522
|
+
],
|
|
12523
|
+
[
|
|
12524
|
+
"R9.through",
|
|
12525
|
+
"R9",
|
|
12526
|
+
"route.through",
|
|
12527
|
+
"count",
|
|
12528
|
+
"lower",
|
|
12529
|
+
true
|
|
12530
|
+
],
|
|
12531
|
+
[
|
|
12532
|
+
"R10.plugAngle",
|
|
12533
|
+
"R10",
|
|
12534
|
+
"route.plug-angle",
|
|
12535
|
+
"degrees",
|
|
12536
|
+
"lower",
|
|
12537
|
+
false
|
|
12538
|
+
],
|
|
12539
|
+
[
|
|
12540
|
+
"R11.min",
|
|
12541
|
+
"R11",
|
|
12542
|
+
"route.plug-spacing.min",
|
|
12543
|
+
"0 to 1",
|
|
12544
|
+
"higher",
|
|
12545
|
+
true
|
|
12546
|
+
],
|
|
12547
|
+
[
|
|
12548
|
+
"R11.q05",
|
|
12549
|
+
"R11",
|
|
12550
|
+
"route.plug-spacing.q05",
|
|
12551
|
+
"0 to 1",
|
|
12552
|
+
"higher",
|
|
12553
|
+
true
|
|
12554
|
+
],
|
|
12555
|
+
[
|
|
12556
|
+
"R13.outside",
|
|
12557
|
+
"R13",
|
|
12558
|
+
"route.outside",
|
|
12559
|
+
"count",
|
|
12560
|
+
"lower",
|
|
12561
|
+
false
|
|
12562
|
+
],
|
|
12563
|
+
[
|
|
12564
|
+
"R15.inflections",
|
|
12565
|
+
"R15",
|
|
12566
|
+
"route.inflections",
|
|
12567
|
+
"count for each route",
|
|
12568
|
+
"lower",
|
|
12569
|
+
false
|
|
12570
|
+
],
|
|
12571
|
+
[
|
|
12572
|
+
"ink",
|
|
12573
|
+
"R1",
|
|
12574
|
+
"route.ink",
|
|
12575
|
+
"units",
|
|
12576
|
+
"lower",
|
|
12577
|
+
true
|
|
12578
|
+
],
|
|
12579
|
+
[
|
|
12580
|
+
"B1.inkSaved",
|
|
12581
|
+
"B1",
|
|
12582
|
+
"route.bundle.ink-saved",
|
|
12583
|
+
"0 to 1",
|
|
12584
|
+
"higher",
|
|
12585
|
+
false
|
|
12586
|
+
],
|
|
12587
|
+
[
|
|
12588
|
+
"B4.ambiguity",
|
|
12589
|
+
"B4",
|
|
12590
|
+
"route.bundle.ambiguity",
|
|
12591
|
+
"0 to 1",
|
|
12592
|
+
"lower",
|
|
12593
|
+
false
|
|
12594
|
+
],
|
|
12595
|
+
[
|
|
12596
|
+
"Z10.mean",
|
|
12597
|
+
"Z10",
|
|
12598
|
+
"route.stability.mean",
|
|
12599
|
+
"units of the board",
|
|
12600
|
+
"lower",
|
|
12601
|
+
false
|
|
12602
|
+
],
|
|
12603
|
+
[
|
|
12604
|
+
"Z10.max",
|
|
12605
|
+
"Z10",
|
|
12606
|
+
"route.stability.max",
|
|
12607
|
+
"units of the board",
|
|
12608
|
+
"lower",
|
|
12609
|
+
false
|
|
12610
|
+
],
|
|
12611
|
+
[
|
|
12612
|
+
"Z10.moved",
|
|
12613
|
+
"Z10",
|
|
12614
|
+
"route.stability.moved",
|
|
12615
|
+
"0 to 1",
|
|
12616
|
+
"lower",
|
|
12617
|
+
false
|
|
12618
|
+
],
|
|
12619
|
+
[
|
|
12620
|
+
"Z10.base.mean",
|
|
12621
|
+
"Z10",
|
|
12622
|
+
"route.stability.base.mean",
|
|
12623
|
+
"units of the board",
|
|
12624
|
+
"lower",
|
|
12625
|
+
false
|
|
12626
|
+
],
|
|
12627
|
+
[
|
|
12628
|
+
"Z10.base.max",
|
|
12629
|
+
"Z10",
|
|
12630
|
+
"route.stability.base.max",
|
|
12631
|
+
"units of the board",
|
|
12632
|
+
"lower",
|
|
12633
|
+
false
|
|
12634
|
+
],
|
|
12635
|
+
[
|
|
12636
|
+
"Z10.base.moved",
|
|
12637
|
+
"Z10",
|
|
12638
|
+
"route.stability.base.moved",
|
|
12639
|
+
"0 to 1",
|
|
12640
|
+
"lower",
|
|
12641
|
+
false
|
|
12642
|
+
],
|
|
12643
|
+
[
|
|
12644
|
+
"level-nesting",
|
|
12645
|
+
"Z6",
|
|
12646
|
+
"route.level.nesting",
|
|
12647
|
+
"count",
|
|
12648
|
+
"lower",
|
|
12649
|
+
false
|
|
12650
|
+
],
|
|
12651
|
+
[
|
|
12652
|
+
"Z3.through",
|
|
12653
|
+
"Z3",
|
|
12654
|
+
"route.level.through",
|
|
12655
|
+
"count",
|
|
12656
|
+
"lower",
|
|
12657
|
+
true
|
|
12658
|
+
],
|
|
12659
|
+
[
|
|
12660
|
+
"Z3.length",
|
|
12661
|
+
"Z3",
|
|
12662
|
+
"route.level.link-length",
|
|
12663
|
+
"units",
|
|
12664
|
+
"lower",
|
|
12665
|
+
true
|
|
12666
|
+
],
|
|
12667
|
+
[
|
|
12668
|
+
"Z4.farItems",
|
|
12669
|
+
"Z4",
|
|
12670
|
+
"route.level.far-items",
|
|
12671
|
+
"count",
|
|
12672
|
+
"lower",
|
|
12673
|
+
true
|
|
12674
|
+
],
|
|
12675
|
+
[
|
|
12676
|
+
"Z7.position",
|
|
12677
|
+
"Z7",
|
|
12678
|
+
"route.level.position",
|
|
12679
|
+
"no unit",
|
|
12680
|
+
"lower",
|
|
12681
|
+
true
|
|
12682
|
+
],
|
|
12683
|
+
[
|
|
12684
|
+
"Z8.order",
|
|
12685
|
+
"Z8",
|
|
12686
|
+
"route.level.order",
|
|
12687
|
+
"-1 to 1",
|
|
12688
|
+
"higher",
|
|
12689
|
+
true
|
|
12690
|
+
],
|
|
12691
|
+
[
|
|
12692
|
+
"G1.modularity",
|
|
12693
|
+
"G1",
|
|
12694
|
+
"route.group.modularity",
|
|
12695
|
+
"-0.5 to 1",
|
|
12696
|
+
"higher",
|
|
12697
|
+
true
|
|
12698
|
+
],
|
|
12699
|
+
[
|
|
12700
|
+
"G2.cut",
|
|
12701
|
+
"G2",
|
|
12702
|
+
"route.group.cut",
|
|
12703
|
+
"0 to 1",
|
|
12704
|
+
"lower",
|
|
12705
|
+
true
|
|
12706
|
+
],
|
|
12707
|
+
[
|
|
12708
|
+
"G6.balance",
|
|
12709
|
+
"G6",
|
|
12710
|
+
"route.group.balance",
|
|
12711
|
+
"0 to 1",
|
|
12712
|
+
"higher",
|
|
12713
|
+
true
|
|
12714
|
+
],
|
|
12715
|
+
[
|
|
12716
|
+
"G8.treeCost",
|
|
12717
|
+
"G8",
|
|
12718
|
+
"route.group.tree-cost",
|
|
12719
|
+
"0 to 1",
|
|
12720
|
+
"lower",
|
|
12721
|
+
true
|
|
12722
|
+
],
|
|
12723
|
+
[
|
|
12724
|
+
"G9.ari",
|
|
12725
|
+
"G9",
|
|
12726
|
+
"route.group.stability",
|
|
12727
|
+
"-1 to 1",
|
|
12728
|
+
"higher",
|
|
12729
|
+
true
|
|
12730
|
+
],
|
|
12731
|
+
[
|
|
12732
|
+
"C1.max",
|
|
12733
|
+
"C1",
|
|
12734
|
+
"route.curve.c1.max",
|
|
12735
|
+
"degrees",
|
|
12736
|
+
"lower",
|
|
12737
|
+
false
|
|
12738
|
+
],
|
|
12739
|
+
[
|
|
12740
|
+
"C1.share",
|
|
12741
|
+
"C1",
|
|
12742
|
+
"route.curve.c1.share",
|
|
12743
|
+
"0 to 1",
|
|
12744
|
+
"lower",
|
|
12745
|
+
false
|
|
12746
|
+
],
|
|
12747
|
+
[
|
|
12748
|
+
"C2.median",
|
|
12749
|
+
"C2",
|
|
12750
|
+
"route.curve.c2.median",
|
|
12751
|
+
"no unit",
|
|
12752
|
+
"lower",
|
|
12753
|
+
false
|
|
12754
|
+
],
|
|
12755
|
+
[
|
|
12756
|
+
"C2.max",
|
|
12757
|
+
"C2",
|
|
12758
|
+
"route.curve.c2.max",
|
|
12759
|
+
"no unit",
|
|
12760
|
+
"lower",
|
|
12761
|
+
false
|
|
12762
|
+
],
|
|
12763
|
+
[
|
|
12764
|
+
"C3.median",
|
|
12765
|
+
"C3",
|
|
12766
|
+
"route.curve.c3.median",
|
|
12767
|
+
"no unit",
|
|
12768
|
+
"lower",
|
|
12769
|
+
false
|
|
12770
|
+
],
|
|
12771
|
+
[
|
|
12772
|
+
"C3.q95",
|
|
12773
|
+
"C3",
|
|
12774
|
+
"route.curve.c3.q95",
|
|
12775
|
+
"no unit",
|
|
12776
|
+
"lower",
|
|
12777
|
+
false
|
|
12778
|
+
],
|
|
12779
|
+
[
|
|
12780
|
+
"C5.peak",
|
|
12781
|
+
"C5",
|
|
12782
|
+
"route.curve.c5.peak",
|
|
12783
|
+
"curvature times the unit of the board",
|
|
12784
|
+
"lower",
|
|
12785
|
+
false
|
|
12786
|
+
],
|
|
12787
|
+
[
|
|
12788
|
+
"C6.length",
|
|
12789
|
+
"C6",
|
|
12790
|
+
"route.curve.c6.length",
|
|
12791
|
+
"ratio",
|
|
12792
|
+
"lower",
|
|
12793
|
+
false
|
|
12794
|
+
],
|
|
12795
|
+
[
|
|
12796
|
+
"curve-clear",
|
|
12797
|
+
"C7",
|
|
12798
|
+
"route.curve.rule.clear",
|
|
12799
|
+
"count",
|
|
12800
|
+
"lower",
|
|
12801
|
+
false
|
|
12802
|
+
],
|
|
12803
|
+
[
|
|
12804
|
+
"curve-ends",
|
|
12805
|
+
"C10",
|
|
12806
|
+
"route.curve.rule.ends",
|
|
12807
|
+
"count",
|
|
12808
|
+
"lower",
|
|
12809
|
+
false
|
|
12810
|
+
],
|
|
12811
|
+
[
|
|
12812
|
+
"curve-g1",
|
|
12813
|
+
"C1",
|
|
12814
|
+
"route.curve.rule.g1",
|
|
12815
|
+
"count",
|
|
12816
|
+
"lower",
|
|
12817
|
+
false
|
|
12818
|
+
],
|
|
12819
|
+
[
|
|
12820
|
+
"curve-simple",
|
|
12821
|
+
"C9",
|
|
12822
|
+
"route.curve.rule.simple",
|
|
12823
|
+
"count",
|
|
12824
|
+
"lower",
|
|
12825
|
+
false
|
|
12826
|
+
]
|
|
12827
|
+
].map(([key, id, name, unit, better, placementDependent]) => Object.freeze({
|
|
12828
|
+
key,
|
|
12829
|
+
id,
|
|
12830
|
+
name,
|
|
12831
|
+
unit,
|
|
12832
|
+
better,
|
|
12833
|
+
placementDependent
|
|
12834
|
+
})));
|
|
12835
|
+
const BY_KEY = new Map(MEASURES.map((m) => [m.key, m]));
|
|
12836
|
+
const BY_NAME = new Map(MEASURES.map((m) => [m.name, m]));
|
|
12837
|
+
/** This function gives the measure of a key of `runSuite` or `compareStep`, or `undefined` for a key that is not in `MEASURES`. */
|
|
12838
|
+
function measureByKey(key) {
|
|
12839
|
+
return BY_KEY.get(key);
|
|
12840
|
+
}
|
|
12841
|
+
/** This function gives the measure of a name of a record, or `undefined` for a name that is not in `MEASURES`. */
|
|
12842
|
+
function measureByName(name) {
|
|
12843
|
+
return BY_NAME.get(name);
|
|
12844
|
+
}
|
|
12845
|
+
/** This function gives the name of a key in a record. A key that is not in `MEASURES` gets the name `route.other.<key>`. */
|
|
12846
|
+
function measureName(key) {
|
|
12847
|
+
return BY_KEY.get(key)?.name ?? `route.other.${key.toLowerCase()}`;
|
|
12848
|
+
}
|
|
12849
|
+
/** This function gives the objectives of a Pareto front by the names of a record, from objectives by the keys of `runSuite`. */
|
|
12850
|
+
function namedObjectives(objectives) {
|
|
12851
|
+
return objectives.map((o) => ({
|
|
12852
|
+
key: measureName(o.key),
|
|
12853
|
+
better: o.better
|
|
12854
|
+
}));
|
|
12855
|
+
}
|
|
12856
|
+
// src/bench/screen/set.ts
|
|
12857
|
+
const fixture = (name) => FIXTURES.find((f) => f.name === name);
|
|
12858
|
+
const DENSE = {
|
|
12859
|
+
name: "dense-groups",
|
|
12860
|
+
kind: "clustered",
|
|
12861
|
+
nodes: 150,
|
|
12862
|
+
edgesPerNode: 2,
|
|
12863
|
+
seed: 3
|
|
12864
|
+
};
|
|
12865
|
+
const PACKED = {
|
|
12866
|
+
cols: 12,
|
|
12867
|
+
rows: 8,
|
|
12868
|
+
long: 48
|
|
12869
|
+
};
|
|
12870
|
+
/** This function gives a family of `makeFixture`. The member `k` has the seed `familySeed(spec, k)`, as in `runSuite`. */
|
|
12871
|
+
function fixtureFamily(spec, shows, seeds, cell) {
|
|
12872
|
+
const { name, seed: _seed, ...params } = spec;
|
|
12873
|
+
return {
|
|
12874
|
+
name,
|
|
12875
|
+
shows,
|
|
12876
|
+
seeds,
|
|
12877
|
+
cell,
|
|
12878
|
+
make: (k) => makeFixture({
|
|
12879
|
+
...spec,
|
|
12880
|
+
seed: familySeed(spec, k)
|
|
12881
|
+
}),
|
|
12882
|
+
generator: (k) => ({
|
|
12883
|
+
name: "fixture",
|
|
12884
|
+
version: 1,
|
|
12885
|
+
params,
|
|
12886
|
+
seed: familySeed(spec, k)
|
|
12887
|
+
})
|
|
12888
|
+
};
|
|
12889
|
+
}
|
|
12890
|
+
/** The cell dimensions that each family of `screen-v1` shares. */
|
|
12891
|
+
const BASE = {
|
|
12892
|
+
containerSize: "freeAspect",
|
|
12893
|
+
containerShape: "rect",
|
|
12894
|
+
obstacles: "none",
|
|
12895
|
+
mode: "offline",
|
|
12896
|
+
groups: "none"
|
|
12897
|
+
};
|
|
12898
|
+
/**
|
|
12899
|
+
* The screen set `screen-v1`: 14 boards of made-up data from 10 to 150 nodes. Each family shows a known hard case.
|
|
12900
|
+
* The seed 0 of each family is the fixture or the case board of the benchmark. The seed `k` of a fixture family is `familySeed(spec, k)`, as in `runSuite`.
|
|
12901
|
+
* The generators wrap the fixtures and the case boards without a change to their output.
|
|
12902
|
+
* This set does not change. The tests compare it with the files in `corpus/screen-v1`.
|
|
12903
|
+
*/
|
|
12904
|
+
const SCREEN_V1 = {
|
|
12905
|
+
id: "screen-v1",
|
|
12906
|
+
families: [
|
|
12907
|
+
fixtureFamily(fixture("tiny-uniform"), "A tiny board: the cost of a strategy at its start.", 3, {
|
|
12908
|
+
...BASE,
|
|
12909
|
+
count: "10",
|
|
12910
|
+
edgeDensity: "medium",
|
|
12911
|
+
edgeShape: "random",
|
|
12912
|
+
ports: "free"
|
|
12913
|
+
}),
|
|
12914
|
+
fixtureFamily(fixture("small-clustered"), "Clusters, and fixed plugs on 20% of the nodes.", 3, {
|
|
12915
|
+
...BASE,
|
|
12916
|
+
count: "100",
|
|
12917
|
+
edgeDensity: "medium",
|
|
12918
|
+
edgeShape: "clustered",
|
|
12919
|
+
ports: "fixed"
|
|
12920
|
+
}),
|
|
12921
|
+
fixtureFamily(DENSE, "Dense groups whose boxes overlap at the far view.", 3, {
|
|
12922
|
+
...BASE,
|
|
12923
|
+
count: "100",
|
|
12924
|
+
edgeDensity: "medium",
|
|
12925
|
+
edgeShape: "clustered",
|
|
12926
|
+
ports: "free"
|
|
12927
|
+
}),
|
|
12928
|
+
{
|
|
12929
|
+
name: "hub-chain",
|
|
12930
|
+
shows: "A chain of hubs: routes climb away from their targets.",
|
|
12931
|
+
seeds: 1,
|
|
12932
|
+
cell: {
|
|
12933
|
+
...BASE,
|
|
12934
|
+
count: "100",
|
|
12935
|
+
edgeDensity: "medium",
|
|
12936
|
+
edgeShape: "hubChain",
|
|
12937
|
+
ports: "free"
|
|
12938
|
+
},
|
|
12939
|
+
make: () => hubChainBoard(),
|
|
12940
|
+
generator: () => ({
|
|
12941
|
+
name: "case.hub-chain",
|
|
12942
|
+
version: 1,
|
|
12943
|
+
params: {},
|
|
12944
|
+
seed: 3
|
|
12945
|
+
})
|
|
12946
|
+
},
|
|
12947
|
+
{
|
|
12948
|
+
name: "many-groups",
|
|
12949
|
+
shows: "Many groups: level links go around the board.",
|
|
12950
|
+
seeds: 1,
|
|
12951
|
+
cell: {
|
|
12952
|
+
...BASE,
|
|
12953
|
+
count: "100",
|
|
12954
|
+
edgeDensity: "medium",
|
|
12955
|
+
edgeShape: "clustered",
|
|
12956
|
+
ports: "free"
|
|
12957
|
+
},
|
|
12958
|
+
make: () => manyGroupsBoard(),
|
|
12959
|
+
generator: () => ({
|
|
12960
|
+
name: "case.many-groups",
|
|
12961
|
+
version: 1,
|
|
12962
|
+
params: {},
|
|
12963
|
+
seed: 7
|
|
12964
|
+
})
|
|
12965
|
+
},
|
|
12966
|
+
{
|
|
12967
|
+
name: "packed-grid",
|
|
12968
|
+
shows: "A packed grid: boxes touch, with no channel for the routes.",
|
|
12969
|
+
seeds: 3,
|
|
12970
|
+
cell: {
|
|
12971
|
+
...BASE,
|
|
12972
|
+
count: "100",
|
|
12973
|
+
edgeDensity: "medium",
|
|
12974
|
+
edgeShape: "longRange",
|
|
12975
|
+
ports: "free"
|
|
12976
|
+
},
|
|
12977
|
+
make: (k) => packedGridBoard({
|
|
12978
|
+
...PACKED,
|
|
12979
|
+
seed: 11 + 1009 * k
|
|
12980
|
+
}),
|
|
12981
|
+
generator: (k) => ({
|
|
12982
|
+
name: "case.packed-grid",
|
|
12983
|
+
version: 1,
|
|
12984
|
+
params: PACKED,
|
|
12985
|
+
seed: 11 + 1009 * k
|
|
12986
|
+
})
|
|
12987
|
+
}
|
|
12988
|
+
]
|
|
12989
|
+
};
|
|
12990
|
+
/** The screen sets by id. */
|
|
12991
|
+
const SCREEN_SETS = { [SCREEN_V1.id]: SCREEN_V1 };
|
|
12992
|
+
/** This function gives the id of the member `seed` of a family of a set: `<set>/<family>/s<seed>`. */
|
|
12993
|
+
function inputId(set, family, seed) {
|
|
12994
|
+
return `${set}/${family}/s${seed}`;
|
|
12995
|
+
}
|
|
12996
|
+
/** This function gives the input files of a screen set, in the order of its families and seeds. Each file has its hash. */
|
|
12997
|
+
function screenInputs(set = SCREEN_V1) {
|
|
12998
|
+
return set.families.flatMap((f) => Array.from({ length: f.seeds }, (_, k) => graphInput(f.make(k), {
|
|
12999
|
+
id: inputId(set.id, f.name, k),
|
|
13000
|
+
set: set.id,
|
|
13001
|
+
family: f.name,
|
|
13002
|
+
seed: k,
|
|
13003
|
+
generator: f.generator(k),
|
|
13004
|
+
cell: f.cell
|
|
13005
|
+
})));
|
|
13006
|
+
}
|
|
13007
|
+
/** This function gives the manifest of a list of inputs. The inputs of one generator, version and parameters share one spec. */
|
|
13008
|
+
function corpusManifest(corpus, inputs) {
|
|
13009
|
+
const specs = /* @__PURE__ */ new Map();
|
|
13010
|
+
for (const input of inputs) {
|
|
13011
|
+
const g = input.generator;
|
|
13012
|
+
if (!g) continue;
|
|
13013
|
+
const key = JSON.stringify([
|
|
13014
|
+
g.name,
|
|
13015
|
+
g.version,
|
|
13016
|
+
g.params
|
|
13017
|
+
]);
|
|
13018
|
+
const spec = specs.get(key) ?? {
|
|
13019
|
+
generator: g.name,
|
|
13020
|
+
version: g.version,
|
|
13021
|
+
params: g.params,
|
|
13022
|
+
seeds: []
|
|
13023
|
+
};
|
|
13024
|
+
if (!spec.seeds.includes(g.seed)) spec.seeds.push(g.seed);
|
|
13025
|
+
specs.set(key, spec);
|
|
13026
|
+
}
|
|
13027
|
+
return {
|
|
13028
|
+
schema: "layout-corpus/1",
|
|
13029
|
+
corpus,
|
|
13030
|
+
specs: [...specs.values()],
|
|
13031
|
+
inputs: inputs.map((i) => ({
|
|
13032
|
+
id: i.id,
|
|
13033
|
+
hash: i.hash,
|
|
13034
|
+
cell: i.cell ?? {}
|
|
13035
|
+
}))
|
|
13036
|
+
};
|
|
13037
|
+
}
|
|
13038
|
+
// src/bench/screen/screen.ts
|
|
13039
|
+
/** The steps of the screen, in the order of the pipeline. */
|
|
13040
|
+
const SCREEN_STEPS = [
|
|
13041
|
+
"group",
|
|
13042
|
+
"router",
|
|
13043
|
+
"highway",
|
|
13044
|
+
"curve"
|
|
13045
|
+
];
|
|
13046
|
+
/** The routers that the screen of the step `highway` also runs by default. They are the baselines of the note on highways. */
|
|
13047
|
+
const HIGHWAY_BASE_ROUTERS = [
|
|
13048
|
+
"corners",
|
|
13049
|
+
"shared",
|
|
13050
|
+
"trunks"
|
|
13051
|
+
];
|
|
13052
|
+
/** The hard rules of the routes of a router, or of a plan after `follow`. */
|
|
13053
|
+
const ROUTE_RULES = [
|
|
13054
|
+
"complete",
|
|
13055
|
+
"ends-at-plugs",
|
|
13056
|
+
"fixed-plugs",
|
|
13057
|
+
"no-pass-through",
|
|
13058
|
+
"counts-sum",
|
|
13059
|
+
"link-ends",
|
|
13060
|
+
"bundle-trunk",
|
|
13061
|
+
"run-shared",
|
|
13062
|
+
"same-twice"
|
|
13063
|
+
];
|
|
13064
|
+
/** The hard rules of each step of the screen, in the order of the records. A good run has 0 for each rule. */
|
|
13065
|
+
const SCREEN_RULES = {
|
|
13066
|
+
router: ROUTE_RULES,
|
|
13067
|
+
highway: ROUTE_RULES,
|
|
13068
|
+
group: [
|
|
13069
|
+
"sections",
|
|
13070
|
+
"counts-sum",
|
|
13071
|
+
"link-ends",
|
|
13072
|
+
"same-twice"
|
|
13073
|
+
],
|
|
13074
|
+
curve: ["same-twice"]
|
|
13075
|
+
};
|
|
13076
|
+
/** This function counts the violations of each rule, with 0 for each rule of `rules`. */
|
|
13077
|
+
function countRules(rules, violations) {
|
|
13078
|
+
const out = Object.fromEntries(rules.map((r) => [r, 0]));
|
|
13079
|
+
for (const v of violations) out[v.rule] = (out[v.rule] ?? 0) + 1;
|
|
13080
|
+
return out;
|
|
13081
|
+
}
|
|
13082
|
+
/** This function gives the median of a list of numbers, or `NaN` for an empty list. */
|
|
13083
|
+
function median(values) {
|
|
13084
|
+
return quantile(values, .5);
|
|
13085
|
+
}
|
|
13086
|
+
/** This function tells if a value meets a comparison with an expected value. */
|
|
13087
|
+
function meets(value, op, expected) {
|
|
13088
|
+
if (op === "<") return value < expected;
|
|
13089
|
+
if (op === "<=") return value <= expected;
|
|
13090
|
+
if (op === ">") return value > expected;
|
|
13091
|
+
if (op === ">=") return value >= expected;
|
|
13092
|
+
return value === expected;
|
|
13093
|
+
}
|
|
13094
|
+
/** This function makes the registry of a screen. It refuses a strategy of another step, and a new strategy with the name of a built-in strategy. */
|
|
13095
|
+
function screenRegistry(step, strategies, baseline) {
|
|
13096
|
+
const builtins = listStrategies(STRATEGIES, step);
|
|
13097
|
+
const chosen = [];
|
|
13098
|
+
for (const s of strategies) {
|
|
13099
|
+
if (typeof s === "string") {
|
|
13100
|
+
const known = builtins.find((b) => b.name === s);
|
|
13101
|
+
if (!known) throw new Error(`There is no ${step} "${s}". The registry has ${builtins.map((b) => b.name).join(", ")}.`);
|
|
13102
|
+
chosen.push(known);
|
|
13103
|
+
continue;
|
|
13104
|
+
}
|
|
13105
|
+
if (s.step !== step) throw new Error(`The strategy "${s.name}" is of the step "${s.step}", not "${step}".`);
|
|
13106
|
+
const known = builtins.find((b) => b.name === s.name);
|
|
13107
|
+
if (known && known !== s) throw new Error(`The new ${step} "${s.name}" has the name of a built-in strategy. Give it another name.`);
|
|
13108
|
+
chosen.push(s);
|
|
13109
|
+
}
|
|
13110
|
+
const candidates = new Set(chosen.map((s) => s.name));
|
|
13111
|
+
const others = STEPS.filter((x) => x !== step).flatMap((x) => listStrategies(STRATEGIES, x));
|
|
13112
|
+
const own = baseline ? [...builtins, ...chosen.filter((s) => !builtins.includes(s))] : chosen.filter((s, i) => chosen.indexOf(s) === i);
|
|
13113
|
+
return {
|
|
13114
|
+
registry: createRegistry([...others, ...own]),
|
|
13115
|
+
candidates
|
|
13116
|
+
};
|
|
13117
|
+
}
|
|
13118
|
+
/** This function tells if a step of the screen gives routes. The step `router` gives routes, and the step `highway` gives routes after `follow`. */
|
|
13119
|
+
const givesRoutes = (step) => step === "router" || step === "highway";
|
|
13120
|
+
/** This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router. */
|
|
13121
|
+
function rowValues(step, graph, row) {
|
|
13122
|
+
const values = { ...row.values };
|
|
13123
|
+
delete values["ms"];
|
|
13124
|
+
if (givesRoutes(step) && row.result) {
|
|
13125
|
+
const q = measureQuality(graph, row.result);
|
|
13126
|
+
values["R3.bends"] = q.bends;
|
|
13127
|
+
values["R8.clearance"] = q.clearance;
|
|
13128
|
+
values["R8.tight"] = q.tight;
|
|
13129
|
+
values["R10.plugAngle"] = q.plugAngle;
|
|
13130
|
+
values["R15.inflections"] = q.inflections;
|
|
13131
|
+
}
|
|
13132
|
+
return values;
|
|
13133
|
+
}
|
|
13134
|
+
/** This function gives the hard rules of a row of `compareStep`, and tells if the row gave the same output two times. */
|
|
13135
|
+
function rowRules(step, graph, row, again) {
|
|
13136
|
+
const rules = SCREEN_RULES[step];
|
|
13137
|
+
if (givesRoutes(step)) {
|
|
13138
|
+
const all = row.result ? checkProperties(graph, row.result, 1e6) : [];
|
|
13139
|
+
const deterministic = row.violations === all.length;
|
|
13140
|
+
return {
|
|
13141
|
+
hard: {
|
|
13142
|
+
...countRules(rules, all),
|
|
13143
|
+
"same-twice": deterministic ? 0 : 1
|
|
13144
|
+
},
|
|
13145
|
+
deterministic
|
|
13146
|
+
};
|
|
13147
|
+
}
|
|
13148
|
+
if (step === "group") {
|
|
13149
|
+
const all = row.result ? checkLevels(graph, row.result.levels ?? [], 1e6) : [{ rule: "sections" }];
|
|
13150
|
+
const deterministic = again !== void 0 && sameData(row.tree, again.tree) && sameData(row.result, again.result);
|
|
13151
|
+
return {
|
|
13152
|
+
hard: {
|
|
13153
|
+
...countRules(rules, all),
|
|
13154
|
+
"same-twice": deterministic ? 0 : 1
|
|
13155
|
+
},
|
|
13156
|
+
deterministic
|
|
13157
|
+
};
|
|
13158
|
+
}
|
|
13159
|
+
const deterministic = again !== void 0 && sameData(row.curves, again.curves);
|
|
13160
|
+
return {
|
|
13161
|
+
hard: {
|
|
13162
|
+
...countRules(rules, []),
|
|
13163
|
+
"same-twice": deterministic ? 0 : 1
|
|
13164
|
+
},
|
|
13165
|
+
deterministic
|
|
13166
|
+
};
|
|
13167
|
+
}
|
|
13168
|
+
/** This function keeps the finite values and gives them the names of a record. */
|
|
13169
|
+
function named(values) {
|
|
13170
|
+
const out = {};
|
|
13171
|
+
for (const [k, v] of Object.entries(values)) if (Number.isFinite(v)) out[measureName(k)] = v;
|
|
13172
|
+
return out;
|
|
13173
|
+
}
|
|
13174
|
+
/**
|
|
13175
|
+
* This function screens strategies of one step on the inputs of a screen set. It is the cheap test of a new idea before a full build.
|
|
13176
|
+
* Each strategy runs on each input with `compareStep`, and each run gives one record of the form `layout-record/1`.
|
|
13177
|
+
* A strategy can be a name of the registry or a strategy object, for example from a prototype file. The registry does not change.
|
|
13178
|
+
*
|
|
13179
|
+
* The step `router` routes each input two times for the rule "same twice". The steps `group` and `curve` run each input two times for the same rule.
|
|
13180
|
+
*
|
|
13181
|
+
* The step `highway` makes the plan of each strategy and routes it with `follow`, two times. It also runs the routers of `baseRouters`.
|
|
13182
|
+
* An input with a `base` in the inputs also gets the measures `route.stability.base.*`: the routes on the variant against the routes on its base.
|
|
13183
|
+
* The same inputs and the same options give the same records. Only the times change.
|
|
13184
|
+
*/
|
|
13185
|
+
function screen(step, strategies = [], options = {}) {
|
|
13186
|
+
const set = options.set ?? SCREEN_V1;
|
|
13187
|
+
const screened = screenRegistry(step, strategies, options.baseline ?? true);
|
|
13188
|
+
const candidates = screened.candidates;
|
|
13189
|
+
const base = step === "highway" ? (options.baseRouters ?? HIGHWAY_BASE_ROUTERS).map((name) => getStrategy(STRATEGIES, "router", name)) : [];
|
|
13190
|
+
const registry = step === "highway" ? createRegistry([...base, ...listStrategies(screened.registry, "highway").map((s) => highwayRouterStrategy(s))]) : screened.registry;
|
|
13191
|
+
const compareAs = step === "highway" ? "router" : step;
|
|
13192
|
+
const baseNames = new Set(base.map((s) => s.name));
|
|
13193
|
+
const stepOf = (name) => baseNames.has(name) ? "router" : step;
|
|
13194
|
+
const own = options.options ?? {};
|
|
13195
|
+
const seed = options.seed ?? 1;
|
|
13196
|
+
const log = options.log ?? (() => {});
|
|
13197
|
+
const families = options.families;
|
|
13198
|
+
const inputs = (options.inputs ?? screenInputs(set)).filter((i) => families?.includes(i.family) ?? true);
|
|
13199
|
+
const bases = new Set(inputs.flatMap((i) => i.base !== void 0 ? [i.base] : []));
|
|
13200
|
+
const byName = new Map(listStrategies(registry, compareAs).map((s) => [s.name, s]));
|
|
13201
|
+
const records = [];
|
|
13202
|
+
const comparisons = [];
|
|
13203
|
+
const kept = /* @__PURE__ */ new Map();
|
|
13204
|
+
const variants = [];
|
|
13205
|
+
for (const input of inputs) {
|
|
13206
|
+
const graph = inputGraph(input);
|
|
13207
|
+
const compare = {
|
|
13208
|
+
registry,
|
|
13209
|
+
options: own,
|
|
13210
|
+
seed,
|
|
13211
|
+
name: input.id,
|
|
13212
|
+
...options.curveRouter ? { curveRouter: options.curveRouter } : {}
|
|
13213
|
+
};
|
|
13214
|
+
const cmp = compareStep(compareAs, graph, compare);
|
|
13215
|
+
const again = givesRoutes(step) ? void 0 : compareStep(compareAs, graph, compare);
|
|
13216
|
+
comparisons.push(cmp);
|
|
13217
|
+
log(`${input.id}: ${graph.nodes.length} nodes, ${graph.edges.length} edges, ${cmp.rows.map((r) => `${r.strategy} ${r.ms.toFixed(0)}`).join(", ")}`);
|
|
13218
|
+
if (bases.has(input.id)) kept.set(input.id, {
|
|
13219
|
+
graph,
|
|
13220
|
+
results: new Map(cmp.rows.flatMap((r) => r.result ? [[r.strategy, r.result]] : []))
|
|
13221
|
+
});
|
|
13222
|
+
cmp.rows.forEach((row, i) => {
|
|
13223
|
+
const strategy = byName.get(row.strategy);
|
|
13224
|
+
const head = {
|
|
13225
|
+
schema: "layout-record/1",
|
|
13226
|
+
stage: "route",
|
|
13227
|
+
step: stepOf(row.strategy),
|
|
13228
|
+
strategy: row.strategy,
|
|
13229
|
+
options: jsonSafe({
|
|
13230
|
+
...strategy.defaults,
|
|
13231
|
+
...own[row.strategy]
|
|
13232
|
+
}),
|
|
13233
|
+
corpus: input.set,
|
|
13234
|
+
input: input.id,
|
|
13235
|
+
inputHash: input.hash,
|
|
13236
|
+
set: input.set,
|
|
13237
|
+
seed: input.seed,
|
|
13238
|
+
nodes: graph.nodes.length,
|
|
13239
|
+
edges: graph.edges.length
|
|
13240
|
+
};
|
|
13241
|
+
const prediction = options.predictions?.find((p) => p.strategy === row.strategy && p.set === input.set && p.input === input.id)?.id;
|
|
13242
|
+
const tail = {
|
|
13243
|
+
...prediction !== void 0 ? { prediction } : {},
|
|
13244
|
+
...options.commit !== void 0 ? { commit: options.commit } : {}
|
|
13245
|
+
};
|
|
13246
|
+
if (row.skipped !== void 0) {
|
|
13247
|
+
records.push({
|
|
13248
|
+
...head,
|
|
13249
|
+
measures: {},
|
|
13250
|
+
hard: {},
|
|
13251
|
+
tolerance: { through: TOLERANCE },
|
|
13252
|
+
deterministic: true,
|
|
13253
|
+
ms: 0,
|
|
13254
|
+
...tail,
|
|
13255
|
+
skipped: row.skipped
|
|
13256
|
+
});
|
|
13257
|
+
return;
|
|
13258
|
+
}
|
|
13259
|
+
const { hard, deterministic } = rowRules(step, graph, row, again?.rows[i]);
|
|
13260
|
+
const record = {
|
|
13261
|
+
...head,
|
|
13262
|
+
measures: named(rowValues(step, graph, row)),
|
|
13263
|
+
hard,
|
|
13264
|
+
tolerance: { through: TOLERANCE },
|
|
13265
|
+
deterministic,
|
|
13266
|
+
ms: row.ms,
|
|
13267
|
+
...tail
|
|
13268
|
+
};
|
|
13269
|
+
records.push(record);
|
|
13270
|
+
if (givesRoutes(step) && input.base !== void 0 && row.result) variants.push({
|
|
13271
|
+
record,
|
|
13272
|
+
base: input.base,
|
|
13273
|
+
result: row.result
|
|
13274
|
+
});
|
|
13275
|
+
});
|
|
13276
|
+
}
|
|
13277
|
+
for (const v of variants) {
|
|
13278
|
+
const base = kept.get(v.base);
|
|
13279
|
+
const before = base?.results.get(v.record.strategy);
|
|
13280
|
+
if (!base || !before) continue;
|
|
13281
|
+
const s = routeStability(base.graph, before, v.result, { seed });
|
|
13282
|
+
Object.assign(v.record.measures, named({
|
|
13283
|
+
"Z10.base.mean": s.mean,
|
|
13284
|
+
"Z10.base.max": s.max,
|
|
13285
|
+
"Z10.base.moved": s.moved
|
|
13286
|
+
}));
|
|
13287
|
+
}
|
|
13288
|
+
return {
|
|
13289
|
+
records,
|
|
13290
|
+
summary: summarizeScreen(records, comparisons, {
|
|
13291
|
+
set: set.id,
|
|
13292
|
+
step,
|
|
13293
|
+
candidates,
|
|
13294
|
+
...options.predictions ? { predictions: options.predictions } : {}
|
|
13295
|
+
})
|
|
13296
|
+
};
|
|
13297
|
+
}
|
|
13298
|
+
/** This function gives the summary of the records of a screen. */
|
|
13299
|
+
function summarizeScreen(records, comparisons, at) {
|
|
13300
|
+
const objectives = namedObjectives(FRONT_OBJECTIVES[at.step]);
|
|
13301
|
+
const names = [...new Set(records.map((r) => r.strategy))];
|
|
13302
|
+
const total = (r) => Object.values(r.hard).reduce((n, v) => n + v, 0);
|
|
13303
|
+
const floor = /* @__PURE__ */ new Map();
|
|
13304
|
+
const best = /* @__PURE__ */ new Map();
|
|
13305
|
+
for (const r of records) {
|
|
13306
|
+
if (r.skipped !== void 0 || at.candidates.has(r.strategy)) continue;
|
|
13307
|
+
if (total(r) >= (best.get(r.input) ?? Number.POSITIVE_INFINITY)) continue;
|
|
13308
|
+
best.set(r.input, total(r));
|
|
13309
|
+
floor.set(r.input, r.hard);
|
|
13310
|
+
}
|
|
13311
|
+
const overFloor = (r) => Object.entries(r.hard).reduce((n, [k, v]) => n + Math.max(0, v - (floor.get(r.input)?.[k] ?? 0)), 0);
|
|
13312
|
+
const strategies = names.map((strategy) => {
|
|
13313
|
+
const all = records.filter((r) => r.strategy === strategy);
|
|
13314
|
+
const ran = all.filter((r) => r.skipped === void 0);
|
|
13315
|
+
const hard = {};
|
|
13316
|
+
for (const r of ran) for (const [k, v] of Object.entries(r.hard)) hard[k] = (hard[k] ?? 0) + v;
|
|
13317
|
+
const keys = [...new Set(ran.flatMap((r) => Object.keys(r.measures)))];
|
|
13318
|
+
const medians = Object.fromEntries(keys.map((k) => [k, median(ran.flatMap((r) => r.measures[k] === void 0 ? [] : [r.measures[k]]))]));
|
|
13319
|
+
return {
|
|
13320
|
+
strategy,
|
|
13321
|
+
candidate: at.candidates.has(strategy),
|
|
13322
|
+
runs: ran.length,
|
|
13323
|
+
skipped: all.length - ran.length,
|
|
13324
|
+
deterministic: ran.every((r) => r.deterministic),
|
|
13325
|
+
hard,
|
|
13326
|
+
hardTotal: Object.values(hard).reduce((s, v) => s + v, 0),
|
|
13327
|
+
hardOverFloor: ran.reduce((n, r) => n + overFloor(r), 0),
|
|
13328
|
+
medians,
|
|
13329
|
+
ms: median(ran.map((r) => r.ms)),
|
|
13330
|
+
onFront: comparisons.filter((c) => c.front.includes(strategy)).length
|
|
13331
|
+
};
|
|
13332
|
+
});
|
|
13333
|
+
const fair = strategies.filter((s) => s.runs > 0 && s.hardOverFloor === 0 && s.deterministic);
|
|
13334
|
+
const front = paretoFront(fair, (s) => Object.fromEntries(objectives.map((o) => [o.key, s.medians[o.key] ?? NaN])), objectives).map((s) => s.strategy);
|
|
13335
|
+
const predictions = (at.predictions ?? []).map((p) => {
|
|
13336
|
+
const op = p.op ?? (measureByName(p.measure)?.better === "higher" ? ">=" : "<=");
|
|
13337
|
+
const values = records.filter((r) => r.strategy === p.strategy && r.set === p.set && r.skipped === void 0 && (p.input === void 0 || r.input === p.input)).flatMap((r) => r.measures[p.measure] === void 0 ? [] : [r.measures[p.measure]]);
|
|
13338
|
+
const measured = values.length === 0 ? void 0 : p.input !== void 0 ? values[0] : median(values);
|
|
13339
|
+
return {
|
|
13340
|
+
...p,
|
|
13341
|
+
op,
|
|
13342
|
+
...measured === void 0 ? {} : {
|
|
13343
|
+
measured,
|
|
13344
|
+
met: meets(measured, op, p.expected)
|
|
13345
|
+
}
|
|
13346
|
+
};
|
|
13347
|
+
});
|
|
13348
|
+
return {
|
|
13349
|
+
set: at.set,
|
|
13350
|
+
step: at.step,
|
|
13351
|
+
inputs: comparisons.length,
|
|
13352
|
+
objectives,
|
|
13353
|
+
strategies,
|
|
13354
|
+
front,
|
|
13355
|
+
excluded: strategies.filter((s) => !fair.includes(s)).map((s) => s.strategy),
|
|
13356
|
+
predictions
|
|
13357
|
+
};
|
|
13358
|
+
}
|
|
13359
|
+
const fmt = (v) => v === void 0 || Number.isNaN(v) ? "-" : Number.isInteger(v) ? String(v) : v.toFixed(3);
|
|
13360
|
+
/** This function writes the summary of a screen as Markdown: one row for each strategy, the front and the predictions. */
|
|
13361
|
+
function formatScreen(summary) {
|
|
13362
|
+
const axes = summary.objectives.map((o) => `${o.key} ${o.better === "min" ? "↓" : "↑"}`);
|
|
13363
|
+
const lines = [
|
|
13364
|
+
`## Screen of the step \`${summary.step}\` on \`${summary.set}\``,
|
|
13365
|
+
"",
|
|
13366
|
+
`${summary.inputs} inputs, ${summary.strategies.length} strategies. Each cell of a measure is the median over the inputs.`,
|
|
13367
|
+
"The floor of an input is the counts of the hard rules of the built-in strategy with the fewest breaks on that input.",
|
|
13368
|
+
"",
|
|
13369
|
+
`| strategy | new | runs | skipped | hard | over the floor | same twice | on the front of an input | ms | ${axes.join(" | ")} |`,
|
|
13370
|
+
`|---|---|---:|---:|---:|---:|---|---:|---:|${axes.map(() => "---:").join("|")}|`,
|
|
13371
|
+
...summary.strategies.map((s) => `| ${s.strategy} | ${s.candidate ? "yes" : ""} | ${s.runs} | ${s.skipped} | ${s.hardTotal} | ${s.hardOverFloor} | ${s.deterministic ? "yes" : "no"} | ${s.onFront} | ${fmt(s.ms)} | ${summary.objectives.map((o) => fmt(s.medians[o.key])).join(" | ")} |`),
|
|
13372
|
+
"",
|
|
13373
|
+
`Pareto front over the medians: ${summary.front.join(", ") || "-"}.`,
|
|
13374
|
+
`Not on the front because of a break over the floor, no run or two different outputs: ${summary.excluded.join(", ") || "-"}.`
|
|
13375
|
+
];
|
|
13376
|
+
if (summary.predictions.length > 0) {
|
|
13377
|
+
lines.push("", "| prediction | strategy | input | measure | expected | measured | met | reason |", "|---|---|---|---|---:|---:|---|---|");
|
|
13378
|
+
for (const p of summary.predictions) lines.push(`| ${p.id} | ${p.strategy} | ${p.input ?? "median"} | ${p.measure} | ${p.op} ${fmt(p.expected)} | ${fmt(p.measured)} | ${p.met === void 0 ? "-" : p.met ? "yes" : "no"} | ${p.reason} |`);
|
|
13379
|
+
}
|
|
13380
|
+
return lines.join("\n") + "\n";
|
|
13381
|
+
}
|
|
13382
|
+
// src/bench/screen/generators.ts
|
|
13383
|
+
/** This function gives a whole number from `lo` to `hi`, both included, from a generator of random numbers. */
|
|
13384
|
+
function randomInt(r, lo, hi) {
|
|
13385
|
+
return lo + Math.floor(r() * (hi - lo + 1));
|
|
13386
|
+
}
|
|
13387
|
+
/**
|
|
13388
|
+
* This function makes the input of the shared generator `rect-scattered`, version 1: a fixed rectangle container, scattered obstacles and boxes without positions.
|
|
13389
|
+
* The repository `packing` makes the same file, byte for byte. A test in each repository pins the hashes of the seeds 1, 2 and 3.
|
|
13390
|
+
* The steps are `container`, `obstacles` and `boxes`. Each step has its own generator from `stepSeed`.
|
|
13391
|
+
*/
|
|
13392
|
+
function rectScattered(seed, params, set = "xcheck-v1") {
|
|
13393
|
+
let r = mulberry32(stepSeed(seed, "container"));
|
|
13394
|
+
const w = randomInt(r, 600, 1e3);
|
|
13395
|
+
const h = randomInt(r, 400, 800);
|
|
13396
|
+
r = mulberry32(stepSeed(seed, "obstacles"));
|
|
13397
|
+
const n = randomInt(r, 3, 8);
|
|
13398
|
+
const obstacles = [];
|
|
13399
|
+
for (let i = 0; i < n; i++) {
|
|
13400
|
+
const ow = randomInt(r, 40, 160);
|
|
13401
|
+
const oh = randomInt(r, 40, 160);
|
|
13402
|
+
const x = randomInt(r, 0, w - ow);
|
|
13403
|
+
const y = randomInt(r, 0, h - oh);
|
|
13404
|
+
obstacles.push({
|
|
13405
|
+
id: `o${i}`,
|
|
13406
|
+
region: {
|
|
13407
|
+
kind: "rect",
|
|
13408
|
+
x,
|
|
13409
|
+
y,
|
|
13410
|
+
w: ow,
|
|
13411
|
+
h: oh
|
|
13412
|
+
}
|
|
13413
|
+
});
|
|
13414
|
+
}
|
|
13415
|
+
r = mulberry32(stepSeed(seed, "boxes"));
|
|
13416
|
+
const boxes = Array.from({ length: params.count }, (_, i) => {
|
|
13417
|
+
const bw = randomInt(r, 20, 120);
|
|
13418
|
+
const bh = randomInt(r, 20, 80);
|
|
13419
|
+
return {
|
|
13420
|
+
id: `b${i}`,
|
|
13421
|
+
w: bw,
|
|
13422
|
+
h: bh
|
|
13423
|
+
};
|
|
13424
|
+
});
|
|
13425
|
+
const body = {
|
|
13426
|
+
schema: "layout-input/1",
|
|
13427
|
+
id: `${set}/rect-scattered/s${seed}`,
|
|
13428
|
+
set,
|
|
13429
|
+
family: "rect-scattered",
|
|
13430
|
+
seed,
|
|
13431
|
+
source: "made-up",
|
|
13432
|
+
generator: {
|
|
13433
|
+
name: "rect-scattered",
|
|
13434
|
+
version: 1,
|
|
13435
|
+
params: { count: params.count },
|
|
13436
|
+
seed
|
|
13437
|
+
},
|
|
13438
|
+
container: {
|
|
13439
|
+
w,
|
|
13440
|
+
h,
|
|
13441
|
+
fixed: "size"
|
|
13442
|
+
},
|
|
13443
|
+
obstacles,
|
|
13444
|
+
boxes,
|
|
13445
|
+
edges: []
|
|
13446
|
+
};
|
|
13447
|
+
return {
|
|
13448
|
+
...body,
|
|
13449
|
+
hash: documentHash(body)
|
|
13450
|
+
};
|
|
13451
|
+
}
|
|
13452
|
+
export { BACKTRACK_MIN, BACKTRACK_SHARE, BREAK, BREAK_EVEN, BUILT_IN_STRATEGIES, CAP_SWEEP, CASES, CLEARANCE_CAP, COMPARE_STEPS, CORRIDOR_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, FIXTURES, FRONT_OBJECTIVES, GROWTH_SWEEP, Grid, HIGHWAY_BASE_ROUTERS, KD_DEFAULTS, KENDALL_EXACT, LANE_ORDERS, MEASURES, OUTSIDE_SLACK, Obstacles, PICK, PROPOSED_RULES, ROOM_FAMILIES, ROUTERS, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SECTION_DEFAULTS, SIDE_NORMAL, SLACK_SWEEP, SPACING_LIMIT, STEPS, STRATEGIES, STRETCH_SWEEP, SUITE_COLUMNS, SoftGraph, TOLERANCE, WayFinder, adjustedRandIndex, allSections, ambiguity, arcCurve, asRouter, backtrackOf, balance, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, bendCount, bendingEnergy, boardGaps, boardUnit, boxExit, boxesOverlap, buildSections, byCollection, byDataGroup, canonicalJson, capSettings, center, changeGraph, checkLevels, checkOptions, checkPlan, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, compareStep, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createRegistry, crossesItself, crossingAngle, crossingAngles, cubicAt, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, cutGroups, cutShare, defineStrategy, denseGroupsBoard, detourSpread, dist, distinctPoints, documentHash, dominates, drawnSegments, familySeed, farLevel, findStrategy, fixedPortPoint, fixtureGroupOptions, fnv1a32, follow, formatBendRoom, formatCurves, formatFaults, formatFronts, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, frechet, g1Defect, g2Defect, gapLanes, getStrategy, goesOutAndBack, graphInput, groupEdges, groupRun, groupSettings, groupStability, groupStrategies, growthSettings, hasCusp, highwayCurve, highwayCurves, highwayRouter, highwayRouterStrategy, highwayStrategies, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, inputGraph, inputId, itemSizes, jogPeaks, joinReports, jsonSafe, kdGroups, kendallTau, keyGroups, keyedSections, keysPlan, laneOrder, laneRoutes, laneSettings, leafGroups, lengthBetween, levelCut, levelGrouping, levelMeasures, levelPairs, levelRoom, listOptions, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, memberAt, minHash, modularity, mulberry32, namedObjectives, nearestSide, nestedGridBoard, nestingFaults, nodeIndex, offsetPolyline, openGaps, optionDefaults, orderChanges, orderLanes, orderRows, orderStability, orderSuiteRows, packedGridBoard, paretoFront, parseLines, passSettings, pathPoints, peakCurvature, pickLevel, placePlug, planOfResult, plugSpacing, plugsRouter, pointAt, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, positionStability, pullApart, quadtreeGroups, quantile, randomInt, recordLines, rectScattered, regionBounds, register, rng, roomDemand, roomLinks, roomOrderFaults, roomStrategies, round3, roundCorners, roundedCurve, routeMeasures, routeStability, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runSuite, sameData, sampleCurve, screen, screenInputs, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, sha256Hex, sharedRouter, sharedWaysPlan, sideAngle, sideToward, slackSettings, smoothCurve, splineCurve, splitPoint, stepSeed, straightCurve, straightRouter, stretchSettings, suiteFronts, sumFaults, summarize, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, toSvgPath, totalTurn, treeCost, treeDepth, trunksRouter, turns, utf8, withBends };
|
|
11155
13453
|
|
|
11156
13454
|
//# sourceMappingURL=index.js.map
|