@mark1russell7/highways 0.2.0 → 0.3.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.
Files changed (51) hide show
  1. package/README.md +240 -8
  2. package/build/index.d.ts +1459 -78
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +9315 -3411
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/compare.ts +145 -0
  8. package/src/bench/curves.ts +514 -2
  9. package/src/bench/links.ts +28 -6
  10. package/src/bench/measures/bundles.ts +119 -0
  11. package/src/bench/measures/curves.ts +211 -0
  12. package/src/bench/measures/groups.ts +122 -0
  13. package/src/bench/measures/index.ts +9 -0
  14. package/src/bench/measures/levels.ts +103 -0
  15. package/src/bench/measures/pareto.ts +37 -0
  16. package/src/bench/measures/room.ts +43 -0
  17. package/src/bench/measures/routes.ts +226 -0
  18. package/src/bench/measures/stability.ts +104 -0
  19. package/src/bench/measures/stats.ts +114 -0
  20. package/src/bench/properties.ts +18 -2
  21. package/src/bench/room.ts +209 -2
  22. package/src/bench/run.ts +86 -11
  23. package/src/bench/suite.ts +781 -0
  24. package/src/cases.ts +41 -5
  25. package/src/curves/clothoid/index.ts +150 -0
  26. package/src/curves/corner.ts +116 -0
  27. package/src/curves/fit.ts +79 -0
  28. package/src/curves/highway/index.ts +636 -0
  29. package/src/curves/index.ts +16 -1
  30. package/src/curves/model.ts +4 -2
  31. package/src/curves/smooth/index.ts +50 -0
  32. package/src/curves/space.ts +76 -0
  33. package/src/groups/cut/index.ts +292 -0
  34. package/src/groups/index.ts +2 -0
  35. package/src/groups/kd/index.ts +207 -0
  36. package/src/index.ts +4 -0
  37. package/src/levels.ts +163 -5
  38. package/src/room/bends.ts +203 -0
  39. package/src/room/index.ts +28 -0
  40. package/src/room/levels/index.ts +155 -0
  41. package/src/routers/sections.ts +15 -40
  42. package/src/routers/shared.ts +7 -2
  43. package/src/routers/trunks.ts +131 -6
  44. package/src/sections.ts +47 -1
  45. package/src/segments.ts +81 -31
  46. package/src/strategies/builtins.ts +129 -8
  47. package/src/strategies/index.ts +1 -0
  48. package/src/strategies/model.ts +19 -1
  49. package/src/strategies/options.ts +296 -0
  50. package/src/strategies/schemas.ts +221 -0
  51. package/src/visibility.ts +190 -1
@@ -1,10 +1,13 @@
1
- import { CURVE_TYPES, curveSpace, sampleCurve, type CurveType } from "../curves/index.ts";
2
- import { polylineLength } from "../geom.ts";
1
+ import { tracedHighwayCurves, type JogStyle, type JogTrace, type LaneCurve } from "../curves/highway/index.ts";
2
+ import { CURVE_TYPES, curveSpace, distinctPoints, sampleCurve, type CubicSegment, type CurveSegment, type CurveSpace, type CurveType } from "../curves/index.ts";
3
+ import { polylineLength, segmentsCross, type Point } from "../geom.ts";
3
4
  import { nodeIndex, type Graph, type Router } from "../model.ts";
4
5
  import { nearestSide, SIDE_NORMAL } from "../plugs.ts";
6
+ import { laneRoutes, runKey, segmentsOf, type SegmentGraph } from "../segments.ts";
5
7
  import { curveStrategies } from "../strategies/builtins.ts";
6
8
  import type { Strategy } from "../strategies/model.ts";
7
9
  import { measureLinks, type MeasuredLink } from "./links.ts";
10
+ import { g1Defect } from "./measures/curves.ts";
8
11
 
9
12
  // The core runs in any host, so it reads the clock without the types of Node or the browser.
10
13
  const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
@@ -123,3 +126,512 @@ export function formatCurves(rows: readonly CurveBenchRow<string>[]): string {
123
126
  const body = rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.type} | ${r.links} | ${r.through} | ${r.free} | ${r.clamped} | ${r.length.toFixed(3)} | ${r.ms.toFixed(1)} |`);
124
127
  return [head, line, ...body].join("\n");
125
128
  }
129
+
130
+ /** The options of `measureHighways`. */
131
+ export interface HighwayMeasureOptions {
132
+ /** The largest width of an open segment, as in `laneRoutes`. The default is 16. */
133
+ readonly maxWidth?: number;
134
+ /** A piece of a curve with a radius under this value counts in `underMin`. The default is 4. */
135
+ readonly minRadius?: number;
136
+ /** The length at each end of a segment that the spacing does not sample, for the merges and the splits. The default is 12. */
137
+ readonly ends?: number;
138
+ /** The free space of the board. With it, the measure counts the lanes that go through a box that their polyline avoids. */
139
+ readonly space?: CurveSpace;
140
+ }
141
+
142
+ /**
143
+ * The measures of the lanes of the open segments of a board, after a curve.
144
+ * The spacing errors are shares of the spacing of the segment: 0.05 is 5% of the spacing.
145
+ */
146
+ export interface HighwayMeasures {
147
+ /** The number of open segments with two lanes or more. */
148
+ readonly bundles: number;
149
+ /** The number of curves of edges in an open segment. */
150
+ readonly lanes: number;
151
+ /** The number of sample points of the spacing. */
152
+ readonly samples: number;
153
+ /** The largest spacing error at a sample point. */
154
+ readonly worstSpacing: number;
155
+ /** The mean spacing error of the sample points. */
156
+ readonly meanSpacing: number;
157
+ /** The share of the sample points with a spacing error over `SPACING_LIMIT`. */
158
+ readonly overLimit: number;
159
+ /** The number of points where two lanes of one open segment cross, in the body of the segment: its bends and its straight parts. */
160
+ readonly crossings: number;
161
+ /** The number of points where two lanes of one open segment cross near an end of the segment, in the option `ends`. These are the merges and splits. */
162
+ readonly endCrossings: number;
163
+ /** The number of breaks of the tangent (G1 faults) near the open segments, by more than `BREAK` radians. */
164
+ readonly breaks: number;
165
+ /** The smallest radius of an arc or a cubic piece near the open segments. It is `Infinity` when there is no such piece. */
166
+ readonly minRadius: number;
167
+ /** The number of arcs and cubic pieces near the open segments with a radius under the option `minRadius`. */
168
+ readonly underMin: number;
169
+ /** The number of lanes that go through a box that their polyline of `laneRoutes` avoids. */
170
+ readonly through: number;
171
+ }
172
+
173
+ /** A break of the tangent larger than this angle, in radians, counts in `breaks`. */
174
+ export const BREAK = 0.01;
175
+
176
+ /** A spacing error over this share of the spacing counts in `overLimit`. */
177
+ export const SPACING_LIMIT = 0.05;
178
+
179
+ /** A lane goes along a segment for the spacing where the cosine of the angle between their directions is at least this value. */
180
+ const ALONG = Math.cos(Math.PI / 6);
181
+ /** A lane is in a segment for the crossings where the cosine of the angle between their directions is at least this value. */
182
+ const FORWARD = Math.cos((5 * Math.PI) / 12);
183
+
184
+ /** This function gives the unit tangent at the start and at the end of a segment that starts at `from`. */
185
+ function tangents(from: Point, s: CurveSegment): [Point, Point] | undefined {
186
+ const unit = (a: Point, b: Point): Point | undefined => {
187
+ const l = Math.hypot(b.x - a.x, b.y - a.y);
188
+ return l > 1e-9 ? { x: (b.x - a.x) / l, y: (b.y - a.y) / l } : undefined;
189
+ };
190
+ if (s.kind === "move") return undefined;
191
+ if (s.kind === "line") {
192
+ const u = unit(from, s.to);
193
+ return u && [u, u];
194
+ }
195
+ if (s.kind === "cubic") {
196
+ const a = unit(from, s.c1) ?? unit(from, s.c2) ?? unit(from, s.to);
197
+ const b = unit(s.c2, s.to) ?? unit(s.c1, s.to) ?? unit(from, s.to);
198
+ return a && b && [a, b];
199
+ }
200
+ const d = Math.sign(s.sweep);
201
+ const end = s.start + s.sweep;
202
+ return [
203
+ { x: -d * Math.sin(s.start), y: d * Math.cos(s.start) },
204
+ { x: -d * Math.sin(end), y: d * Math.cos(end) },
205
+ ];
206
+ }
207
+
208
+ /** This function gives the smallest radius of curvature of a cubic piece from `p0`, from 33 sample points. */
209
+ function cubicRadius(p0: Point, c: CubicSegment): number {
210
+ let min = Infinity;
211
+ for (let i = 0; i <= 32; i++) {
212
+ const t = i / 32;
213
+ const u = 1 - t;
214
+ const dx = 3 * (u * u * (c.c1.x - p0.x) + 2 * u * t * (c.c2.x - c.c1.x) + t * t * (c.to.x - c.c2.x));
215
+ const dy = 3 * (u * u * (c.c1.y - p0.y) + 2 * u * t * (c.c2.y - c.c1.y) + t * t * (c.to.y - c.c2.y));
216
+ const ddx = 6 * (u * (c.c2.x - 2 * c.c1.x + p0.x) + t * (c.to.x - 2 * c.c2.x + c.c1.x));
217
+ const ddy = 6 * (u * (c.c2.y - 2 * c.c1.y + p0.y) + t * (c.to.y - 2 * c.c2.y + c.c1.y));
218
+ const k = Math.abs(dx * ddy - dy * ddx);
219
+ if (k > 1e-12) min = Math.min(min, Math.hypot(dx, dy) ** 3 / k);
220
+ }
221
+ return min;
222
+ }
223
+
224
+ /** This function gives points along a polyline, `step` apart, with the first point and the last point. */
225
+ function resample(p: readonly Point[], step: number): Point[] {
226
+ const out: Point[] = p.length > 0 ? [p[0]!] : [];
227
+ let next = step;
228
+ let s = 0;
229
+ for (let i = 1; i < p.length; i++) {
230
+ const a = p[i - 1]!;
231
+ const b = p[i]!;
232
+ const l = Math.hypot(b.x - a.x, b.y - a.y);
233
+ while (next < s + l) {
234
+ const t = (next - s) / l;
235
+ out.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
236
+ next += step;
237
+ }
238
+ s += l;
239
+ }
240
+ if (p.length > 1) out.push(p[p.length - 1]!);
241
+ return out;
242
+ }
243
+
244
+ /** The nearest point of a polyline to a point. */
245
+ interface Nearest {
246
+ /** The distance to the polyline. */
247
+ readonly d: number;
248
+ /** The place of the nearest point along the polyline. */
249
+ readonly s: number;
250
+ /** The unit direction of the piece of the polyline with the nearest point. */
251
+ readonly u: Point;
252
+ }
253
+
254
+ /** This function gives the nearest point of a polyline to `q`. */
255
+ function nearest(q: Point, p: readonly Point[]): Nearest {
256
+ let best = Infinity;
257
+ let at = 0;
258
+ let u: Point = { x: 1, y: 0 };
259
+ let s = 0;
260
+ for (let i = 1; i < p.length; i++) {
261
+ const a = p[i - 1]!;
262
+ const b = p[i]!;
263
+ const dx = b.x - a.x;
264
+ const dy = b.y - a.y;
265
+ const l = Math.hypot(dx, dy);
266
+ const t = l === 0 ? 0 : Math.max(0, Math.min(1, ((q.x - a.x) * dx + (q.y - a.y) * dy) / (l * l)));
267
+ const d = Math.hypot(q.x - (a.x + t * dx), q.y - (a.y + t * dy));
268
+ if (d < best && l > 0) {
269
+ best = d;
270
+ at = s + t * l;
271
+ u = { x: dx / l, y: dy / l };
272
+ }
273
+ s += l;
274
+ }
275
+ return { d: best, s: at, u };
276
+ }
277
+
278
+ /** One open segment with two lanes or more, as the measure sees it. */
279
+ interface Bundle {
280
+ /** The center line, with no repeated points. */
281
+ readonly c: readonly Point[];
282
+ readonly edges: readonly string[];
283
+ /** The direction of each lane along the center line: 1 forward, -1 back. */
284
+ readonly sign: readonly number[];
285
+ readonly spacing: number;
286
+ /** The largest distance from the center line of a point of a lane in the segment. */
287
+ readonly band: number;
288
+ readonly total: number;
289
+ }
290
+
291
+ /**
292
+ * This function gives the parts of a sampled lane that go along a bundle. Each part is near its center line, between the places `from` and `to`.
293
+ * Each part goes in the direction of the lane in the bundle. A lane that passes near the bundle in another direction is not in it there.
294
+ */
295
+ function partsAlong(p: readonly Point[], b: Bundle, sign: number, from: number, to: number, cos: number): Point[][] {
296
+ const xs = b.c.map((q) => q.x);
297
+ const ys = b.c.map((q) => q.y);
298
+ const x0 = Math.min(...xs) - b.band;
299
+ const x1 = Math.max(...xs) + b.band;
300
+ const y0 = Math.min(...ys) - b.band;
301
+ const y1 = Math.max(...ys) + b.band;
302
+ const parts: Point[][] = [];
303
+ let cur: Point[] = [];
304
+ for (let i = 0; i < p.length; i++) {
305
+ const q = p[i]!;
306
+ let ok = q.x >= x0 && q.x <= x1 && q.y >= y0 && q.y <= y1;
307
+ if (ok) {
308
+ const m = nearest(q, b.c);
309
+ const a = p[Math.max(0, i - 1)]!;
310
+ const z = p[Math.min(p.length - 1, i + 1)]!;
311
+ const l = Math.hypot(z.x - a.x, z.y - a.y);
312
+ ok = m.d <= b.band && m.s >= from && m.s <= to && l > 0 && sign * ((z.x - a.x) * m.u.x + (z.y - a.y) * m.u.y) >= cos * l;
313
+ }
314
+ if (ok) cur.push(q);
315
+ else if (cur.length > 0) {
316
+ parts.push(cur);
317
+ cur = [];
318
+ }
319
+ }
320
+ if (cur.length > 0) parts.push(cur);
321
+ return parts;
322
+ }
323
+
324
+ /**
325
+ * This function measures the lanes of the open segments of a board after a curve. It measures the spacing, the crossings,
326
+ * the breaks of the tangent, the radii and the lanes that go through a box.
327
+ *
328
+ * A lane is in a segment where it goes near the center line of the segment, in its direction along the segment.
329
+ * The spacing samples each lane `spacing / 4` apart, from the middle of the first leg to the middle of the last leg.
330
+ * It does not sample the lane in the option `ends`. The ends of a segment are for the merges and the splits.
331
+ *
332
+ * A sample compares the distance to the next lane with the spacing. It counts when the next lane is in the band of the segment.
333
+ * The crossings, the breaks and the radii count along the full segment, and `ends` past each end of it.
334
+ */
335
+ export function measureHighways(
336
+ graph: SegmentGraph,
337
+ open: ReadonlySet<string>,
338
+ laneWidth: number,
339
+ curves: readonly LaneCurve[],
340
+ options: HighwayMeasureOptions = {},
341
+ ): HighwayMeasures {
342
+ const maxWidth = options.maxWidth ?? 16;
343
+ const minRadius = options.minRadius ?? 4;
344
+ const ends = options.ends ?? 12;
345
+ const signs = new Map<string, number>();
346
+ for (const path of graph.paths) for (const ref of path.segments) signs.set(`${ref.segment}|${path.edge}`, ref.reverse ? -1 : 1);
347
+ const bundles: Bundle[] = [];
348
+ for (const s of graph.segments) {
349
+ const c = distinctPoints(s.points);
350
+ if (s.count < 2 || !open.has(runKey(s.edges)) || c.length < 2) continue;
351
+ const spacing = Math.min(laneWidth, maxWidth / (s.count - 1));
352
+ const sign = s.edges.map((e) => signs.get(`${s.id}|${e}`) ?? 1);
353
+ bundles.push({ c, edges: s.edges, sign, spacing, band: ((s.count - 1) / 2) * spacing + spacing, total: polylineLength(c) });
354
+ }
355
+ const inBundle = new Set(bundles.flatMap((s) => s.edges));
356
+ const lanes = curves.filter((c) => inBundle.has(c.edge));
357
+ // The samples of the curves are within 0.5% of the spacing of the curve, so the samples add almost no error.
358
+ const fine = Math.max(1e-3, Math.min(laneWidth, maxWidth) * 0.005);
359
+ const sampled = new Map(lanes.map((c) => [c.edge, sampleCurve(c.segments, fine)]));
360
+ let samples = 0;
361
+ let worst = 0;
362
+ let sum = 0;
363
+ let over = 0;
364
+ const crossings = new Set<string>();
365
+ const endCrossings = new Set<string>();
366
+ for (const b of bundles) {
367
+ const sp = b.spacing;
368
+ const c = b.c;
369
+ const from = Math.max(ends, Math.hypot(c[1]!.x - c[0]!.x, c[1]!.y - c[0]!.y) / 2);
370
+ const to = b.total - Math.max(ends, Math.hypot(c[c.length - 1]!.x - c[c.length - 2]!.x, c[c.length - 1]!.y - c[c.length - 2]!.y) / 2);
371
+ const body = b.edges.map((e, k) => partsAlong(sampled.get(e) ?? [], b, b.sign[k]!, from, to, ALONG));
372
+ const whole = b.edges.map((e, k) => partsAlong(sampled.get(e) ?? [], b, b.sign[k]!, 0, b.total, ALONG));
373
+ const crossing = b.edges.map((e, k) => partsAlong(sampled.get(e) ?? [], b, b.sign[k]!, 0, b.total, FORWARD));
374
+ for (let k = 0; k + 1 < b.edges.length; k++)
375
+ for (const [x, y] of [
376
+ [k, k + 1],
377
+ [k + 1, k],
378
+ ] as const)
379
+ for (const part of body[x]!)
380
+ for (const q of resample(part, sp / 4)) {
381
+ let d = Infinity;
382
+ for (const other of whole[y]!) d = Math.min(d, nearest(q, other).d);
383
+ if (d > b.band) continue;
384
+ const err = Math.abs(d - sp) / sp;
385
+ samples += 1;
386
+ sum += err;
387
+ if (err > SPACING_LIMIT) over += 1;
388
+ worst = Math.max(worst, err);
389
+ }
390
+ for (let i = 0; i < b.edges.length; i++)
391
+ for (let j = i + 1; j < b.edges.length; j++)
392
+ for (const P of crossing[i]!)
393
+ for (const Q of crossing[j]!)
394
+ for (let x = 1; x < P.length; x++)
395
+ for (let y = 1; y < Q.length; y++) {
396
+ if (!segmentsCross(P[x - 1]!, P[x]!, Q[y - 1]!, Q[y]!)) continue;
397
+ const q = { x: (P[x - 1]!.x + P[x]!.x) / 2, y: (P[x - 1]!.y + P[x]!.y) / 2 };
398
+ const pair = [b.edges[i]!, b.edges[j]!].sort().join("|");
399
+ const at = nearest(q, c).s;
400
+ (at >= from && at <= to ? crossings : endCrossings).add(`${pair}@${Math.round(q.x)},${Math.round(q.y)}`);
401
+ }
402
+ }
403
+ // A piece of a lane is near a segment of the lane when its start is near the center line, or past an end by no more than `ends`.
404
+ const near = new Map<string, Bundle[]>();
405
+ for (const b of bundles) for (const e of b.edges) near.set(e, [...(near.get(e) ?? []), b]);
406
+ const isNear = (edge: string, q: Point): boolean => (near.get(edge) ?? []).some((b) => nearest(q, b.c).d <= b.band + ends);
407
+ let breaks = 0;
408
+ let least = Infinity;
409
+ let underMin = 0;
410
+ let through = 0;
411
+ const polylines = options.space ? new Map(laneRoutes(graph, open, laneWidth, maxWidth).map((r) => [r.edge, r.points])) : undefined;
412
+ for (const lane of lanes) {
413
+ let at: Point | undefined;
414
+ let before: Point | undefined;
415
+ for (const s of lane.segments) {
416
+ if (s.kind === "move" || at === undefined) {
417
+ at = s.to;
418
+ before = undefined;
419
+ continue;
420
+ }
421
+ const t = tangents(at, s);
422
+ const close = isNear(lane.edge, at);
423
+ if (close && t && before && Math.acos(Math.max(-1, Math.min(1, before.x * t[0].x + before.y * t[0].y))) > BREAK) breaks += 1;
424
+ const r = s.kind === "arc" ? s.radius : s.kind === "cubic" ? cubicRadius(at, s) : Infinity;
425
+ if (close && r < Infinity) {
426
+ least = Math.min(least, r);
427
+ if (r < minRadius - 1e-9) underMin += 1;
428
+ }
429
+ if (t) before = t[1];
430
+ at = s.to;
431
+ }
432
+ const p = polylines?.get(lane.edge);
433
+ if (options.space && p && options.space.addsHit(sampleCurve(lane.segments, options.space.tolerance), p)) through += 1;
434
+ }
435
+ return {
436
+ bundles: bundles.length,
437
+ lanes: lanes.length,
438
+ samples,
439
+ worstSpacing: worst,
440
+ meanSpacing: samples > 0 ? sum / samples : 0,
441
+ overLimit: samples > 0 ? over / samples : 0,
442
+ crossings: crossings.size,
443
+ endCrossings: endCrossings.size,
444
+ breaks,
445
+ minRadius: least,
446
+ underMin,
447
+ through,
448
+ };
449
+ }
450
+
451
+ /** One row of `benchHighways`: the measures of the lanes of one router on one board, after one way to curve them. */
452
+ export interface HighwayBenchRow extends HighwayMeasures {
453
+ readonly router: string;
454
+ readonly fixture: string;
455
+ /** The way to curve the lanes: `rounded` curves each lane alone, and `highway` curves the lanes of a bundle together. */
456
+ readonly curve: "rounded" | "highway";
457
+ /** The shape of the S curves of a row `highway`. */
458
+ readonly jog?: JogStyle;
459
+ /** The measures of the S curves of a row `highway`. */
460
+ readonly jogs?: JogMeasures;
461
+ /** The time to make all curves, in milliseconds. */
462
+ readonly ms: number;
463
+ }
464
+
465
+ /** The measures of the S curves where the lanes join or leave a bundle. Each curvature is in 1 / units. */
466
+ export interface JogMeasures {
467
+ /** The number of S curves. */
468
+ readonly count: number;
469
+ /** The number of S curves that are a biarc. A polynomial S curve that does not fit on its leg is a biarc. */
470
+ readonly biarcs: number;
471
+ /** The median and the largest of the peak curvature of each S curve with the shape of the row. */
472
+ readonly peakMedian: number;
473
+ readonly peakMax: number;
474
+ /** The median ratio of the peak curvature of each S curve of the row to the peak curvature of the same biarc S curve. */
475
+ readonly peakRatio?: number;
476
+ /** The largest curvature at an end of an S curve with the shape of the row. The straight leg next to it has the curvature 0, so it is the jump at the end. */
477
+ readonly endJump: number;
478
+ /** The largest jump of the curvature at a joint inside an S curve with the shape of the row. */
479
+ readonly innerJump: number;
480
+ }
481
+
482
+ /** This function gives the median of the values, or 0 when there are none. */
483
+ function medianOf(values: number[]): number {
484
+ values.sort((a, b) => a - b);
485
+ const n = values.length;
486
+ return n === 0 ? 0 : n % 2 === 1 ? values[(n - 1) / 2]! : (values[n / 2 - 1]! + values[n / 2]!) / 2;
487
+ }
488
+
489
+ /** This function gives the curvature of the cubic `(p0, c1, c2, p3)` at `t`. */
490
+ function cubicCurvature(p0: Point, s: CubicSegment, t: number): number {
491
+ const u = 1 - t;
492
+ const [c1, c2, p3] = [s.c1, s.c2, s.to];
493
+ const dx = 3 * u * u * (c1.x - p0.x) + 6 * u * t * (c2.x - c1.x) + 3 * t * t * (p3.x - c2.x);
494
+ const dy = 3 * u * u * (c1.y - p0.y) + 6 * u * t * (c2.y - c1.y) + 3 * t * t * (p3.y - c2.y);
495
+ const ddx = 6 * u * (c2.x - 2 * c1.x + p0.x) + 6 * t * (p3.x - 2 * c2.x + c1.x);
496
+ const ddy = 6 * u * (c2.y - 2 * c1.y + p0.y) + 6 * t * (p3.y - 2 * c2.y + c1.y);
497
+ const speed = Math.hypot(dx, dy);
498
+ return speed > 1e-12 ? (dx * ddy - dy * ddx) / speed ** 3 : 0;
499
+ }
500
+
501
+ /** This function gives the signed curvature at the start (`t = 0`) and at the end (`t = 1`) of a segment, and its largest absolute curvature. */
502
+ function curvatureOf(from: Point, s: CurveSegment): { readonly start: number; readonly end: number; readonly peak: number } {
503
+ if (s.kind === "arc") {
504
+ const k = s.radius > 0 ? Math.sign(s.sweep) / s.radius : 0;
505
+ return { start: k, end: k, peak: Math.abs(k) };
506
+ }
507
+ if (s.kind !== "cubic") return { start: 0, end: 0, peak: 0 };
508
+ let peak = 0;
509
+ for (let i = 0; i <= 64; i++) peak = Math.max(peak, Math.abs(cubicCurvature(from, s, i / 64)));
510
+ return { start: cubicCurvature(from, s, 0), end: cubicCurvature(from, s, 1), peak };
511
+ }
512
+
513
+ /** This function gives the peak curvature of each S curve, by lane. `jogs` has the first and the last index of each S curve in the segments of its lane. */
514
+ export function jogPeaks(curves: readonly LaneCurve[], jogs: JogTrace): number[][] {
515
+ return curves.map((c, i) =>
516
+ (jogs[i] ?? []).map(([first, last]) => {
517
+ let peak = 0;
518
+ for (let k = first; k <= last; k++) peak = Math.max(peak, curvatureOf(c.segments[k - 1]?.to ?? c.segments[k]!.to, c.segments[k]!).peak);
519
+ return peak;
520
+ }),
521
+ );
522
+ }
523
+
524
+ /**
525
+ * This function gives the peak curvature of one curve, in 1 / units: the largest curvature of its lines, cubic pieces and arcs (C5 of MEAS0 as `1 / rmin`).
526
+ * A joint with a tangent break of more than 0.5 degrees is a sharp corner, so such a curve gets `Infinity`. A curve of lines only gives 0.
527
+ */
528
+ export function peakCurvature(segments: readonly CurveSegment[]): number {
529
+ if (g1Defect(segments) > 0.5) return Infinity;
530
+ let peak = 0;
531
+ for (let k = 1; k < segments.length; k++) peak = Math.max(peak, curvatureOf(segments[k - 1]!.to, segments[k]!).peak);
532
+ return peak;
533
+ }
534
+
535
+ /**
536
+ * This function measures the S curves of the lanes that have the shape `style`. `jogs` has the first and the last index of each S curve in the segments of its lane.
537
+ * With `biarcPeaks`, the peak curvatures of the same S curves as biarcs, it also gives the median ratio of the peaks.
538
+ */
539
+ export function measureJogs(curves: readonly LaneCurve[], jogs: JogTrace, style: JogStyle = "biarc", biarcPeaks?: readonly (readonly number[])[]): JogMeasures {
540
+ const peaks: number[] = [];
541
+ const ratios: number[] = [];
542
+ let endJump = 0;
543
+ let innerJump = 0;
544
+ let biarcs = 0;
545
+ let count = 0;
546
+ curves.forEach((c, i) => {
547
+ (jogs[i] ?? []).forEach(([first, last, shape], n) => {
548
+ count++;
549
+ if (shape === "biarc") biarcs++;
550
+ if (shape !== style) return;
551
+ let peak = 0;
552
+ let before: number | undefined;
553
+ for (let k = first; k <= last; k++) {
554
+ const s = c.segments[k]!;
555
+ const m = curvatureOf(c.segments[k - 1]?.to ?? s.to, s);
556
+ peak = Math.max(peak, m.peak);
557
+ if (k === first) endJump = Math.max(endJump, Math.abs(m.start));
558
+ if (k === last) endJump = Math.max(endJump, Math.abs(m.end));
559
+ if (before !== undefined) innerJump = Math.max(innerJump, Math.abs(before - m.start));
560
+ before = m.end;
561
+ }
562
+ peaks.push(peak);
563
+ const b = biarcPeaks?.[i]?.[n];
564
+ if (b !== undefined && b > 0) ratios.push(peak / b);
565
+ });
566
+ });
567
+ const peakMax = peaks.reduce((a, b) => Math.max(a, b), 0);
568
+ return { count, biarcs, peakMedian: medianOf(peaks), peakMax, ...(biarcPeaks ? { peakRatio: medianOf(ratios) } : {}), endJump, innerJump };
569
+ }
570
+
571
+ /** The options of `benchHighways`. */
572
+ export interface HighwayBenchOptions {
573
+ /** The spacing of the lanes. The default is 4. */
574
+ readonly spacing?: number;
575
+ /** The largest width of an open segment. The default is 16. */
576
+ readonly maxWidth?: number;
577
+ /** The radius of each curve. The default is 12. */
578
+ readonly radius?: number;
579
+ /** The smallest radius of `highwayCurves`, and the limit of `underMin`. The default is 4. */
580
+ readonly minRadius?: number;
581
+ /** The shapes of the S curves. Each shape gives one row `highway`. The default is `["biarc"]`. */
582
+ readonly jogs?: readonly JogStyle[];
583
+ /** The number of cubic pieces of a `quintic` S curve. The default is 4. */
584
+ readonly jogPieces?: number;
585
+ }
586
+
587
+ /**
588
+ * This function routes a graph, opens each shared segment, and curves the lanes in two ways.
589
+ * The row `rounded` curves each lane of `laneRoutes` alone with the curve `rounded`. Each row `highway` curves them with `highwayCurves` and one shape of S curve.
590
+ * Both ways get the free space of the nodes.
591
+ */
592
+ export function benchHighways(router: Router, fixture: string, graph: Graph, options: HighwayBenchOptions = {}): HighwayBenchRow[] {
593
+ const spacing = options.spacing ?? 4;
594
+ const maxWidth = options.maxWidth ?? 16;
595
+ const radius = options.radius ?? 12;
596
+ const minRadius = options.minRadius ?? 4;
597
+ const segments = segmentsOf(router.route(graph));
598
+ const open = new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
599
+ const space = curveSpace(graph.nodes.map((n) => n.box));
600
+ const measure = { maxWidth, minRadius, ends: radius, space };
601
+ const t0 = now();
602
+ const alone = laneRoutes(segments, open, spacing, maxWidth).map((r) => ({ edge: r.edge, segments: curveStrategies.rounded.run({ points: r.points, space }, { radius }) }));
603
+ const t1 = now();
604
+ const rows: HighwayBenchRow[] = [{ router: router.name, fixture, curve: "rounded", ...measureHighways(segments, open, spacing, alone, measure), ms: t1 - t0 }];
605
+ // The rows of the other shapes compare their peaks with the S curves of the row `biarc` before them.
606
+ let biarcPeaks: number[][] | undefined;
607
+ for (const jog of options.jogs ?? ["biarc"]) {
608
+ const t2 = now();
609
+ const together = tracedHighwayCurves(segments, open, spacing, { maxWidth, radius, minRadius, space, jog, jogPieces: options.jogPieces ?? 4 });
610
+ const t3 = now();
611
+ if (jog === "biarc") biarcPeaks = jogPeaks(together.curves, together.jogs);
612
+ rows.push({
613
+ router: router.name,
614
+ fixture,
615
+ curve: "highway",
616
+ jog,
617
+ jogs: measureJogs(together.curves, together.jogs, jog, jog === "biarc" ? undefined : biarcPeaks),
618
+ ...measureHighways(segments, open, spacing, together.curves, measure),
619
+ ms: t3 - t2,
620
+ });
621
+ }
622
+ return rows;
623
+ }
624
+
625
+ /** This function writes the rows of `benchHighways` as a Markdown table. The spacing errors are percents of the spacing. */
626
+ export function formatHighways(rows: readonly HighwayBenchRow[]): string {
627
+ const head =
628
+ "| router | fixture | curve | jog | bundles | lanes | worst spacing | mean spacing | over 5% | crossings | end crossings | tangent breaks | min radius | under min | through | S curves | S biarcs | S peak κ median | S peak κ max | S peak ratio | S end κ | S inner jump | ms |";
629
+ const line = "|---|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
630
+ const pct = (v: number): string => `${(v * 100).toFixed(1)}%`;
631
+ const k = (v: number | undefined): string => (v === undefined ? "-" : v.toFixed(4));
632
+ const body = rows.map(
633
+ (r) =>
634
+ `| ${r.router} | ${r.fixture} | ${r.curve} | ${r.jog ?? "-"} | ${r.bundles} | ${r.lanes} | ${pct(r.worstSpacing)} | ${pct(r.meanSpacing)} | ${pct(r.overLimit)} | ${r.crossings} | ${r.endCrossings} | ${r.breaks} | ${r.minRadius === Infinity ? "-" : r.minRadius.toFixed(2)} | ${r.underMin} | ${r.through} | ${r.jogs?.count ?? "-"} | ${r.jogs?.biarcs ?? "-"} | ${k(r.jogs?.peakMedian)} | ${k(r.jogs?.peakMax)} | ${k(r.jogs?.peakRatio)} | ${k(r.jogs?.endJump)} | ${k(r.jogs?.innerJump)} | ${r.ms.toFixed(1)} |`,
635
+ );
636
+ return [head, line, ...body].join("\n");
637
+ }
@@ -15,6 +15,13 @@ export interface LinkFaults {
15
15
  * A segment goes through a box when a part of it longer than `TOLERANCE` is inside the box made smaller by `TOLERANCE`.
16
16
  */
17
17
  readonly through: number;
18
+ /**
19
+ * The number of links of `through` that no route can keep out of the other boxes.
20
+ * Each box that such a link goes through overlaps the box of its source or of its target, as `countOverlaps` sees it.
21
+ */
22
+ readonly unavoidable: number;
23
+ /** The number of links of `through` that go through at least one box that overlaps neither end. `through` is `unavoidable` plus `avoidable`. */
24
+ readonly avoidable: number;
18
25
  /**
19
26
  * The number of links that go out and back. The distance from a point of the link to its last point grows after the nearest approach.
20
27
  * A link goes out and back when this growth is more than `BACKTRACK_SHARE` of the distance between its ends, and more than `BACKTRACK_MIN` units.
@@ -105,27 +112,41 @@ export function measureLinks(boxes: ReadonlyMap<string, Box>, links: readonly Me
105
112
  const grid = new Grid<string>(Math.max(180, (2 * size) / Math.max(1, boxes.size)));
106
113
  for (const [id, b] of boxes) grid.add(id, b);
107
114
  let through = 0;
115
+ let avoidable = 0;
108
116
  let backtrack = 0;
109
117
  let worstDetour = 0;
110
118
  let outside = 0;
111
119
  for (const l of links) {
112
120
  const p = l.points;
113
121
  if (p.length === 0) continue;
122
+ // The test stops at the first box that overlaps neither end. Before it, a box that overlaps an end only marks the link.
114
123
  let hit = false;
115
- for (let i = 1; i < p.length && !hit; i++) {
124
+ let far = false;
125
+ const ends = [boxes.get(l.source), boxes.get(l.target)].filter((b): b is Box => b !== undefined);
126
+ for (let i = 1; i < p.length && !far; i++) {
116
127
  const a = p[i - 1]!;
117
128
  const b = p[i]!;
118
129
  grid.querySegment(a, b, 0, (id) => {
119
- if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id)!, -TOLERANCE), TOLERANCE)) hit = true;
130
+ if (far || id === l.source || id === l.target) return;
131
+ const box = boxes.get(id)!;
132
+ if (!segmentHitsBox(a, b, inflate(box, -TOLERANCE), TOLERANCE)) return;
133
+ hit = true;
134
+ if (!ends.some((e) => boxesOverlap(e, box))) far = true;
120
135
  });
121
136
  }
122
137
  if (hit) through++;
138
+ if (far) avoidable++;
123
139
  if (goesOutAndBack(p)) backtrack++;
124
140
  const span = dist(p[0]!, p[p.length - 1]!);
125
141
  if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
126
142
  if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
127
143
  }
128
- return { links: links.length, through, backtrack, worstDetour, outside };
144
+ return { links: links.length, through, unavoidable: through - avoidable, avoidable, backtrack, worstDetour, outside };
145
+ }
146
+
147
+ /** This function tells if two boxes overlap by more than `TOLERANCE` on each axis, as `countOverlaps` counts them. */
148
+ export function boxesOverlap(a: Box, b: Box): boolean {
149
+ return Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > TOLERANCE && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > TOLERANCE;
129
150
  }
130
151
 
131
152
  /** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
@@ -149,8 +170,7 @@ export function countOverlaps(boxes: readonly Box[]): number {
149
170
  let n = 0;
150
171
  boxes.forEach((a, i) => {
151
172
  grid.query(a, (j) => {
152
- const b = boxes[j]!;
153
- if (j > i && Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > TOLERANCE && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > TOLERANCE) n++;
173
+ if (j > i && boxesOverlap(a, boxes[j]!)) n++;
154
174
  });
155
175
  });
156
176
  return n;
@@ -182,10 +202,12 @@ export function sumFaults(list: readonly LinkFaults[]): LinkFaults {
182
202
  (s, f) => ({
183
203
  links: s.links + f.links,
184
204
  through: s.through + f.through,
205
+ unavoidable: s.unavoidable + f.unavoidable,
206
+ avoidable: s.avoidable + f.avoidable,
185
207
  backtrack: s.backtrack + f.backtrack,
186
208
  worstDetour: Math.max(s.worstDetour, f.worstDetour),
187
209
  outside: s.outside + f.outside,
188
210
  }),
189
- { links: 0, through: 0, backtrack: 0, worstDetour: 0, outside: 0 },
211
+ { links: 0, through: 0, unavoidable: 0, avoidable: 0, backtrack: 0, worstDetour: 0, outside: 0 },
190
212
  );
191
213
  }