@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/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$2 = 1e-6;
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$2;
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$2) out.push(q);
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$2) out.push(q);
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$2) {
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$2) {
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$2 || Math.abs(a.x - b.x) <= SAME$2 && a.y < b.y;
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$1 = 1e-6;
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$1) return {
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$1) {
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$1) return [{
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$1);
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$1) continue;
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$1) keep.push({
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$1) keep.push({
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$1) startJog[j] = {
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$1) endJog[j] = {
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$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
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$1(way ? corners.pull(way) : [from, to]);
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. `highwayCurves` bends the lanes of a bundle as one.
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) => highwayCurve(input.points, {
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
- for (const r of result.routes) {
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) continue;
10997
+ if (!s || !t || r.points.length < 2) return;
10218
10998
  ids.push(r.edge);
10219
- routes.push({
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 a key from the data of the edges:
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. The grouping rules and the room come last.
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 === "group") return [1, place(names("group"), r.strategy)];
10575
- return [2, r.strategy === "gutter" ? -1 : place(names("room"), r.strategy)];
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
- export { BACKTRACK_MIN, BACKTRACK_SHARE, BREAK, BUILT_IN_STRATEGIES, CAP_SWEEP, CASES, CLEARANCE_CAP, COMPARE_STEPS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, FIXTURES, FRONT_OBJECTIVES, GROWTH_SWEEP, Grid, KD_DEFAULTS, KENDALL_EXACT, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, PICK, PROPOSED_RULES, ROOM_FAMILIES, ROUTERS, 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, capSettings, center, changeGraph, checkLevels, checkOptions, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, compareStep, cornersRouter, countCrossings, countOverlaps, createRegistry, crossesItself, crossingAngle, crossingAngles, cubicAt, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, cutGroups, cutShare, defineStrategy, denseGroupsBoard, detourSpread, dist, distinctPoints, dominates, drawnSegments, familySeed, farLevel, findStrategy, fixedPortPoint, fixtureGroupOptions, formatBendRoom, formatCurves, formatFaults, formatFronts, formatHighways, formatLevelRoom, formatLevels, formatMeasureTime, formatRoom, formatRules, formatSections, formatSummaries, formatTable, frechet, g1Defect, g2Defect, gapLanes, getStrategy, goesOutAndBack, groupEdges, groupRun, groupSettings, groupStability, groupStrategies, growthSettings, hasCusp, highwayCurve, highwayCurves, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, itemSizes, jogPeaks, joinReports, kdGroups, kendallTau, keyGroups, keyedSections, laneOrder, laneRoutes, laneSettings, leafGroups, levelCut, levelGrouping, levelMeasures, levelPairs, levelRoom, listOptions, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, modularity, nearestSide, nestedGridBoard, nestingFaults, nodeIndex, offsetPolyline, openGaps, optionDefaults, orderChanges, orderLanes, orderRows, orderStability, orderSuiteRows, packedGridBoard, paretoFront, passSettings, pathPoints, peakCurvature, pickLevel, placePlug, plugSpacing, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, positionStability, pullApart, quadtreeGroups, quantile, register, rng, roomDemand, roomLinks, roomOrderFaults, roomStrategies, roundCorners, roundedCurve, routeMeasures, routeStability, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runSuite, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, slackSettings, smoothCurve, splineCurve, splitPoint, straightCurve, straightRouter, stretchSettings, suiteFronts, sumFaults, summarize, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, toSvgPath, totalTurn, treeCost, treeDepth, trunksRouter, turns, withBends };
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