@mark1russell7/highways 0.1.4 → 0.1.5

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
@@ -1510,7 +1510,7 @@ var Obstacles = class {
1510
1510
  hit(a, b, pad, skip) {
1511
1511
  let found;
1512
1512
  this.#grid.someSegment(a, b, pad, (n) => {
1513
- if (skip?.has(n) || !hits(a, b, n, pad)) return false;
1513
+ if (skip?.has(n) || !hits$1(a, b, n, pad)) return false;
1514
1514
  found = n;
1515
1515
  return true;
1516
1516
  });
@@ -1543,7 +1543,7 @@ var Obstacles = class {
1543
1543
  }
1544
1544
  };
1545
1545
  /** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
1546
- function hits(a, b, n, pad) {
1546
+ function hits$1(a, b, n, pad) {
1547
1547
  const x = n.box;
1548
1548
  return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
1549
1549
  }
@@ -1738,7 +1738,7 @@ var CornerGraph = class {
1738
1738
  if ((full & cone) !== 0) continue;
1739
1739
  }
1740
1740
  let blocked = false;
1741
- for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
1741
+ for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits$1(p, q, blockers[b], pad);
1742
1742
  if (blocked) continue;
1743
1743
  const hit = this.#obstacles.hit(p, q, pad);
1744
1744
  if (hit !== void 0) blockers.push(hit);
@@ -2625,7 +2625,7 @@ function sumFaults(list) {
2625
2625
  // src/levels.ts
2626
2626
  /** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
2627
2627
  const TIGHT_MARGIN = 1;
2628
- const EPS = 1e-6;
2628
+ const EPS$1 = 1e-6;
2629
2629
  /**
2630
2630
  * This function removes the repeated points, the points in the middle of a straight run,
2631
2631
  * and the points where the way turns back on itself.
@@ -2633,15 +2633,15 @@ const EPS = 1e-6;
2633
2633
  function tidy(p) {
2634
2634
  const out = [];
2635
2635
  for (const q of p) {
2636
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2636
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2637
2637
  while (out.length >= 2) {
2638
2638
  const a = out[out.length - 2];
2639
2639
  const b = out[out.length - 1];
2640
2640
  const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
2641
- if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
2641
+ if (Math.abs(cross) > EPS$1 * (1 + dist(a, b) * dist(b, q))) break;
2642
2642
  out.pop();
2643
2643
  }
2644
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2644
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2645
2645
  out.push(q);
2646
2646
  }
2647
2647
  return out;
@@ -2881,7 +2881,7 @@ function freeSpans(lo, hi, covered) {
2881
2881
  if (x >= hi) break;
2882
2882
  }
2883
2883
  if (x < hi) out.push([x, hi]);
2884
- return out.filter(([a, b]) => b - a > EPS);
2884
+ return out.filter(([a, b]) => b - a > EPS$1);
2885
2885
  }
2886
2886
  /**
2887
2887
  * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
@@ -3013,7 +3013,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
3013
3013
  out.push(...step.legs);
3014
3014
  const end = out[out.length - 1];
3015
3015
  const before = out[out.length - 2];
3016
- axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : void 0;
3016
+ axis = Math.abs(end.y - before.y) < EPS$1 ? "x" : Math.abs(end.x - before.x) < EPS$1 ? "y" : void 0;
3017
3017
  i = step.j;
3018
3018
  }
3019
3019
  return out;
@@ -3023,7 +3023,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
3023
3023
  * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
3024
3024
  */
3025
3025
  function square(a, b, obstacles, pad, from, to) {
3026
- if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : void 0;
3026
+ if (Math.abs(a.x - b.x) < EPS$1 || Math.abs(a.y - b.y) < EPS$1) return obstacles.clear(a, b, pad) ? [b] : void 0;
3027
3027
  const options = [{
3028
3028
  c: {
3029
3029
  x: b.x,
@@ -3700,7 +3700,7 @@ function trunksRouter(options) {
3700
3700
  };
3701
3701
  }
3702
3702
  // src/routers/index.ts
3703
- /** The routers that the benchmark races. */
3703
+ /** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
3704
3704
  const ROUTERS = [
3705
3705
  straightRouter,
3706
3706
  plugsRouter,
@@ -3823,17 +3823,34 @@ function openGaps(items, gaps, snap = DEFAULT_SNAP) {
3823
3823
  * The lanes of a gap come from the `method` of the options. The need of a gap is `gutter + lanes × spacing`, and not less than its current width.
3824
3824
  */
3825
3825
  function roomDemand(items, links, options = {}) {
3826
+ const gutter = options.gutter ?? DEFAULT_GUTTER;
3827
+ const snap = options.snap ?? DEFAULT_SNAP;
3828
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
3829
+ let total = demandOnce(items, links, options);
3830
+ for (let p = 1; p < passes; p++) {
3831
+ const next = demandOnce(openGaps(items, total, snap), links, options);
3832
+ if (next.length !== total.length || next.some((g, i) => g.axis !== total[i].axis)) break;
3833
+ total = total.map((g, i) => ({
3834
+ ...g,
3835
+ lanes: Math.max(g.lanes, next[i].lanes),
3836
+ need: g.need + Math.max(0, next[i].need - next[i].width)
3837
+ }));
3838
+ }
3839
+ return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
3840
+ }
3841
+ /** This function gives the demand of one pass, without the limit `maxGrowth`. */
3842
+ function demandOnce(items, links, options) {
3826
3843
  const gutter = options.gutter ?? DEFAULT_GUTTER;
3827
3844
  const spacing = options.spacing ?? DEFAULT_SPACING;
3828
3845
  const snap = options.snap ?? DEFAULT_SNAP;
3829
3846
  const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
3830
3847
  const gaps = boardGaps(items, snap);
3831
3848
  const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
3832
- return capGrowth(items, gaps.map((g, i) => ({
3849
+ return gaps.map((g, i) => ({
3833
3850
  ...g,
3834
3851
  lanes: lanes[i],
3835
3852
  need: Math.max(g.width, gutter + lanes[i] * spacing)
3836
- })), gutter, options.maxGrowth ?? Infinity);
3853
+ }));
3837
3854
  }
3838
3855
  /** This function gives the demand of the board and the board with each gap open to its need. */
3839
3856
  function makeRoom(items, links, options = {}) {
@@ -4018,6 +4035,141 @@ function capGrowth(items, gaps, gutter, maxGrowth) {
4018
4035
  }
4019
4036
  return out;
4020
4037
  }
4038
+ // src/room/lanes.ts
4039
+ /** The lane orders of `gapLanes`, in the order of the benchmark. */
4040
+ const LANE_ORDERS = [
4041
+ "links",
4042
+ "ends",
4043
+ "crossings"
4044
+ ];
4045
+ const EPS = 1e-6;
4046
+ const ROUNDS = 8;
4047
+ /**
4048
+ * This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
4049
+ * A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
4050
+ * When a move puts a link through more boxes than before, the link keeps its points.
4051
+ */
4052
+ function gapLanes(items, links, options = {}) {
4053
+ const spacing = Math.max(0, options.spacing ?? 4);
4054
+ const order = options.order ?? "crossings";
4055
+ const moved = links.map((l) => l.points.map((p) => ({
4056
+ x: p.x,
4057
+ y: p.y
4058
+ })));
4059
+ for (const g of boardGaps(items, options.snap)) {
4060
+ if (g.width <= EPS) continue;
4061
+ const across = g.axis;
4062
+ const along = g.axis === "x" ? "y" : "x";
4063
+ const lo = g.at - g.width;
4064
+ const hi = g.at;
4065
+ const pieces = [];
4066
+ links.forEach((l, li) => {
4067
+ const p = l.points;
4068
+ for (let i = 2; i <= p.length - 2; i++) {
4069
+ const a = p[i - 1];
4070
+ const b = p[i];
4071
+ if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
4072
+ if (Math.abs(a[along] - b[along]) <= EPS) continue;
4073
+ const side = (q, r) => Math.sign(r[across] - q[across]);
4074
+ pieces.push({
4075
+ link: li,
4076
+ seg: i,
4077
+ lo: Math.min(a[along], b[along]),
4078
+ hi: Math.max(a[along], b[along]),
4079
+ turns: [[a[along], side(a, p[i - 2])], [b[along], side(b, p[i + 1])]]
4080
+ });
4081
+ }
4082
+ });
4083
+ if (pieces.length === 0) continue;
4084
+ const list = orderPieces(pieces, order);
4085
+ const lane = [];
4086
+ let n = 0;
4087
+ list.forEach((P, i) => {
4088
+ let k = 0;
4089
+ for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
4090
+ lane.push(k);
4091
+ n = Math.max(n, k + 1);
4092
+ });
4093
+ const step = Math.min(spacing, g.width / (n + 1));
4094
+ const mid = (lo + hi) / 2;
4095
+ list.forEach((P, i) => {
4096
+ const v = mid + (lane[i] - (n - 1) / 2) * step;
4097
+ moved[P.link][P.seg - 1][across] = v;
4098
+ moved[P.link][P.seg][across] = v;
4099
+ });
4100
+ }
4101
+ const boxes = new Grid(Math.max(20, meanSide(items)));
4102
+ for (const i of items) boxes.add(i.box, i.box);
4103
+ return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
4104
+ ...l,
4105
+ points: moved[i]
4106
+ });
4107
+ }
4108
+ const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
4109
+ /**
4110
+ * This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
4111
+ * A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
4112
+ */
4113
+ function cost(P, Q) {
4114
+ let c = 0;
4115
+ for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
4116
+ for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
4117
+ return c;
4118
+ }
4119
+ /** This function gives the pieces of one gap in lane order, from the near side to the far side. */
4120
+ function orderPieces(pieces, order) {
4121
+ if (order === "links") return [...pieces];
4122
+ const sum = (P) => P.turns.reduce((s, [, side]) => s + side, 0);
4123
+ const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
4124
+ if (order === "ends") return list;
4125
+ const n = list.length;
4126
+ const index = new Map(list.map((P, i) => [P, i]));
4127
+ const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost(P, Q) : 0));
4128
+ const at = (P, Q) => c[index.get(P)][index.get(Q)];
4129
+ for (let round = 0; round < ROUNDS; round++) {
4130
+ let better = false;
4131
+ for (let i = 0; i < n; i++) {
4132
+ const x = list[i];
4133
+ let best = 0;
4134
+ let to = i;
4135
+ let d = 0;
4136
+ for (let k = i - 1; k >= 0; k--) {
4137
+ d += at(x, list[k]) - at(list[k], x);
4138
+ if (d < best) [best, to] = [d, k];
4139
+ }
4140
+ d = 0;
4141
+ for (let k = i + 1; k < n; k++) {
4142
+ d += at(list[k], x) - at(x, list[k]);
4143
+ if (d < best) [best, to] = [d, k];
4144
+ }
4145
+ if (to !== i) {
4146
+ list.splice(i, 1);
4147
+ list.splice(to, 0, x);
4148
+ better = true;
4149
+ }
4150
+ }
4151
+ if (!better) break;
4152
+ }
4153
+ return list;
4154
+ }
4155
+ /** This function gives the mean of the longer side of the boxes. */
4156
+ function meanSide(items) {
4157
+ let s = 0;
4158
+ for (const i of items) s += Math.max(i.box.w, i.box.h);
4159
+ return items.length ? s / items.length : 1;
4160
+ }
4161
+ /** This function counts the legs of a polyline that go through a box, for each box. */
4162
+ function hits(boxes, p) {
4163
+ let n = 0;
4164
+ for (let i = 1; i < p.length; i++) {
4165
+ const a = p[i - 1];
4166
+ const b = p[i];
4167
+ boxes.querySegment(a, b, 0, (box) => {
4168
+ if (segmentHitsBox(a, b, box)) n++;
4169
+ });
4170
+ }
4171
+ return n;
4172
+ }
4021
4173
  // src/bench/quality.ts
4022
4174
  /** The clearance stops at this distance. */
4023
4175
  const CLEARANCE_CAP = 40;
@@ -4306,6 +4458,196 @@ function formatFaults(rows) {
4306
4458
  })
4307
4459
  ].join("\n");
4308
4460
  }
4461
+ // src/strategies/model.ts
4462
+ /** The steps that have strategies, in the order of the pipeline. */
4463
+ const STEPS = [
4464
+ "router",
4465
+ "curve",
4466
+ "room"
4467
+ ];
4468
+ /**
4469
+ * This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
4470
+ * When the caller gives no option, the definition gets the `defaults` object itself.
4471
+ */
4472
+ function defineStrategy(definition) {
4473
+ const { step, name, describe, defaults, maxNodes } = definition;
4474
+ if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
4475
+ const strategy = {
4476
+ step,
4477
+ name,
4478
+ describe,
4479
+ defaults: Object.freeze({ ...defaults }),
4480
+ ...maxNodes === void 0 ? {} : { maxNodes },
4481
+ run(input, options) {
4482
+ const all = options === void 0 || Object.keys(options).length === 0 ? strategy.defaults : {
4483
+ ...strategy.defaults,
4484
+ ...options
4485
+ };
4486
+ return definition.run(input, all);
4487
+ }
4488
+ };
4489
+ return Object.freeze(strategy);
4490
+ }
4491
+ /** This function makes a registry from a list of strategies. It keeps their order. Two strategies with one step and one name are an error. */
4492
+ function createRegistry(strategies) {
4493
+ const out = {
4494
+ router: [],
4495
+ curve: [],
4496
+ room: []
4497
+ };
4498
+ for (const s of strategies) {
4499
+ const list = out[s.step];
4500
+ if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
4501
+ if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
4502
+ list.push(s);
4503
+ }
4504
+ return Object.freeze({
4505
+ router: Object.freeze(out.router),
4506
+ curve: Object.freeze(out.curve),
4507
+ room: Object.freeze(out.room)
4508
+ });
4509
+ }
4510
+ /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
4511
+ function register(registry, ...strategies) {
4512
+ return createRegistry([
4513
+ ...registry.router,
4514
+ ...registry.curve,
4515
+ ...registry.room,
4516
+ ...strategies
4517
+ ]);
4518
+ }
4519
+ /** This function gives the strategies of one step of a registry, in order. */
4520
+ function listStrategies(registry, step) {
4521
+ return registry[step];
4522
+ }
4523
+ /** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
4524
+ function findStrategy(registry, step, name) {
4525
+ return listStrategies(registry, step).find((s) => s.name === name);
4526
+ }
4527
+ /** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
4528
+ function getStrategy(registry, step, name) {
4529
+ const s = findStrategy(registry, step, name);
4530
+ if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
4531
+ return s;
4532
+ }
4533
+ /** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
4534
+ function asRouter(strategy, options) {
4535
+ return {
4536
+ name: strategy.name,
4537
+ ...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
4538
+ route: (graph) => strategy.run(graph, options)
4539
+ };
4540
+ }
4541
+ /** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
4542
+ function routersOf(registry) {
4543
+ return listStrategies(registry, "router").map((s) => asRouter(s));
4544
+ }
4545
+ // src/strategies/builtins.ts
4546
+ /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
4547
+ function routerStrategy(name, describe, maxNodes, defaults, make) {
4548
+ return defineStrategy({
4549
+ step: "router",
4550
+ name,
4551
+ describe,
4552
+ defaults,
4553
+ maxNodes,
4554
+ run: (graph, options) => make({
4555
+ ...options,
4556
+ name
4557
+ }).route(graph)
4558
+ });
4559
+ }
4560
+ /** This function makes a router strategy for a router without options. */
4561
+ function fixedRouter(router, describe) {
4562
+ return defineStrategy({
4563
+ step: "router",
4564
+ name: router.name,
4565
+ describe,
4566
+ defaults: {},
4567
+ run: (graph) => router.route(graph)
4568
+ });
4569
+ }
4570
+ /** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
4571
+ const routerStrategies = Object.freeze({
4572
+ straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
4573
+ plugs: fixedRouter(plugsRouter, "Each edge leaves its plug along the normal of the side and follows the curve of the inflection rule. It does not go around the nodes."),
4574
+ corners: routerStrategy("corners", "Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.", 1e4, {}, cornersRouter),
4575
+ "corners-smooth": routerStrategy("corners-smooth", "The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.", 1e4, { smooth: true }, cornersRouter),
4576
+ trunks: routerStrategy("trunks", "A rule puts edges in groups. The edges of a group leave one plug of one node on one trunk. At a split point, each edge goes on to its target.", 1e4, { rule: byCollection }, trunksRouter),
4577
+ shared: routerStrategy("shared", "The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.", 1e4, { rule: byCollection }, sharedRouter),
4578
+ "shared-all": routerStrategy("shared-all", "The same as `shared`, but all edges share their ways.", 1e4, {}, sharedRouter),
4579
+ sections: routerStrategy("sections", "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.", 1e5, {}, sectionsRouter),
4580
+ "sections-tree": routerStrategy("sections-tree", "The same as `sections`, but without the limit of the stretch.", 1e5, { stretch: Infinity }, sectionsRouter)
4581
+ });
4582
+ /** The context of a curve input, without the keys that have no value. */
4583
+ const contextOf = (input) => ({
4584
+ ...input.startNormal ? { startNormal: input.startNormal } : {},
4585
+ ...input.endNormal ? { endNormal: input.endNormal } : {},
4586
+ ...input.space ? { space: input.space } : {}
4587
+ });
4588
+ /** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
4589
+ const curveStrategies = Object.freeze({
4590
+ straight: defineStrategy({
4591
+ step: "curve",
4592
+ name: "straight",
4593
+ describe: "The polyline as it is.",
4594
+ defaults: {},
4595
+ run: (input) => straightCurve(input.points)
4596
+ }),
4597
+ rounded: defineStrategy({
4598
+ step: "curve",
4599
+ name: "rounded",
4600
+ describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
4601
+ defaults: { radius: 12 },
4602
+ run: (input, o) => roundedCurve(input.points, {
4603
+ ...contextOf(input),
4604
+ radius: o.radius
4605
+ })
4606
+ }),
4607
+ spline: defineStrategy({
4608
+ step: "curve",
4609
+ name: "spline",
4610
+ describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
4611
+ defaults: { tension: 0 },
4612
+ run: (input, o) => splineCurve(input.points, {
4613
+ ...contextOf(input),
4614
+ tension: o.tension
4615
+ })
4616
+ }),
4617
+ arc: defineStrategy({
4618
+ step: "curve",
4619
+ name: "arc",
4620
+ describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
4621
+ defaults: { radius: 12 },
4622
+ run: (input, o) => arcCurve(input.points, {
4623
+ ...contextOf(input),
4624
+ radius: o.radius
4625
+ })
4626
+ })
4627
+ });
4628
+ const roomStrategy = (method, describe) => defineStrategy({
4629
+ step: "room",
4630
+ name: method,
4631
+ describe,
4632
+ defaults: {},
4633
+ run: (input, o) => roomDemand(input.items, input.links, {
4634
+ ...o,
4635
+ method
4636
+ })
4637
+ });
4638
+ /** The built-in room strategies: the methods of `roomDemand`. */
4639
+ const roomStrategies = Object.freeze({
4640
+ routes: roomStrategy("routes", "The lanes of a gap are the most routes that run along it side by side, with each gap open to the gutter."),
4641
+ lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap.")
4642
+ });
4643
+ /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
4644
+ const BUILT_IN_STRATEGIES = Object.freeze([
4645
+ ...Object.values(routerStrategies),
4646
+ ...Object.values(curveStrategies),
4647
+ ...Object.values(roomStrategies)
4648
+ ]);
4649
+ /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
4650
+ const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
4309
4651
  // src/bench/curves.ts
4310
4652
  const now$1 = () => globalThis.performance?.now() ?? Date.now();
4311
4653
  /**
@@ -4314,6 +4656,13 @@ const now$1 = () => globalThis.performance?.now() ?? Date.now();
4314
4656
  * The type `spline` leaves each plug along the normal of the nearest side of its node.
4315
4657
  */
4316
4658
  function benchCurves(router, fixture, graph, options = {}) {
4659
+ return benchCurveStrategies(router, fixture, graph, CURVE_TYPES.map((t) => curveStrategies[t]), options);
4660
+ }
4661
+ /**
4662
+ * This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
4663
+ * The option `radius` goes to each strategy. A strategy without such an option does not read it.
4664
+ */
4665
+ function benchCurveStrategies(router, fixture, graph, curves, options = {}) {
4317
4666
  const tolerance = options.tolerance ?? .25;
4318
4667
  const result = router.route(graph);
4319
4668
  const byId = nodeIndex(graph);
@@ -4337,17 +4686,17 @@ function benchCurves(router, fixture, graph, options = {}) {
4337
4686
  });
4338
4687
  const base = measureLinks(boxes, routes);
4339
4688
  const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
4340
- return CURVE_TYPES.map((type) => {
4689
+ const radius = { radius: options.radius ?? 12 };
4690
+ return curves.map((curve) => {
4341
4691
  const make = (withSpace) => routes.map((r) => ({
4342
4692
  source: r.source,
4343
4693
  target: r.target,
4344
- points: sampleCurve(curveOf(r.points, {
4345
- type,
4346
- radius: options.radius ?? 12,
4694
+ points: sampleCurve(curve.run({
4695
+ points: r.points,
4347
4696
  startNormal: r.startNormal,
4348
4697
  endNormal: r.endNormal,
4349
4698
  ...withSpace ? { space } : {}
4350
- }), tolerance)
4699
+ }, radius), tolerance)
4351
4700
  }));
4352
4701
  const free = measureLinks(boxes, make(false));
4353
4702
  const t0 = now$1();
@@ -4357,7 +4706,7 @@ function benchCurves(router, fixture, graph, options = {}) {
4357
4706
  return {
4358
4707
  router: router.name,
4359
4708
  fixture,
4360
- type,
4709
+ type: curve.name,
4361
4710
  links: routes.length,
4362
4711
  through: base.through,
4363
4712
  free: free.through,
@@ -4393,11 +4742,13 @@ function areaOf(nodes) {
4393
4742
  }
4394
4743
  /**
4395
4744
  * This function measures the links of one board. It measures the links of the far view, with each node of the graph as one item of a level.
4396
- * It also measures the lane routes of the router `sections` at the lane width `spacing`.
4745
+ * With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
4746
+ * Without it, the links of a gap share the lines of the level links.
4747
+ * The function also measures the lane routes of the router `sections` at the lane width `spacing`.
4397
4748
  */
4398
- function measureBoard(graph, spacing) {
4749
+ function measureBoard(graph, spacing, order) {
4399
4750
  const links = roomLinks(graph);
4400
- const drawn = levelLinks(graph.nodes.map((n) => ({
4751
+ const level = levelLinks(graph.nodes.map((n) => ({
4401
4752
  id: n.id,
4402
4753
  box: n.box,
4403
4754
  nodes: [n.id]
@@ -4406,6 +4757,10 @@ function measureBoard(graph, spacing) {
4406
4757
  target: l.target,
4407
4758
  count: l.count ?? 1
4408
4759
  })));
4760
+ const drawn = order ? gapLanes(graph.nodes, level, {
4761
+ spacing,
4762
+ order
4763
+ }) : level;
4409
4764
  const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4410
4765
  const edgeOf = /* @__PURE__ */ new Map();
4411
4766
  for (const e of graph.edges) {
@@ -4427,10 +4782,10 @@ function measureBoard(graph, spacing) {
4427
4782
  };
4428
4783
  }
4429
4784
  /**
4430
- * This function measures a packed board, the board with each gap open to the gutter only, and the board that `makeRoom` opens with each method.
4431
- * The options of `makeRoom` are the same for each method, except `method`.
4785
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
4786
+ * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
4432
4787
  */
4433
- function benchRoom(fixture, graph, options = {}) {
4788
+ function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
4434
4789
  const spacing = options.spacing ?? 4;
4435
4790
  const area0 = areaOf(graph.nodes);
4436
4791
  const links = roomLinks(graph);
@@ -4445,28 +4800,129 @@ function benchRoom(fixture, graph, options = {}) {
4445
4800
  }, spacing)
4446
4801
  });
4447
4802
  const rows = [row("packed", graph.nodes, 0)];
4448
- const states = [
4449
- ["gutter", {
4450
- ...options,
4451
- spacing: 0,
4452
- method: "routes"
4453
- }],
4454
- ["routes", {
4455
- ...options,
4456
- method: "routes"
4457
- }],
4458
- ["lines", {
4459
- ...options,
4460
- method: "lines"
4461
- }]
4462
- ];
4463
- for (const [board, o] of states) {
4803
+ const t0 = now();
4804
+ const gutter = makeRoom(graph.nodes, links, {
4805
+ ...options,
4806
+ spacing: 0,
4807
+ method: "routes"
4808
+ });
4809
+ rows.push(row("gutter", gutter.items, now() - t0));
4810
+ const { method: _method, ...preset } = options;
4811
+ for (const s of methods) {
4812
+ const t1 = now();
4813
+ const gaps = s.run({
4814
+ items: graph.nodes,
4815
+ links
4816
+ }, preset);
4817
+ const nodes = openGaps(graph.nodes, gaps, options.snap);
4818
+ rows.push(row(s.name, nodes, now() - t1));
4819
+ }
4820
+ return rows;
4821
+ }
4822
+ /**
4823
+ * This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
4824
+ * The first row is the fair baseline: the board with each gap open to the gutter of `baseline` only, measured at the spacing of `baseline`.
4825
+ */
4826
+ function sweepRoom(fixture, graph, settings, baseline = {
4827
+ gutter: 8,
4828
+ spacing: 4
4829
+ }) {
4830
+ const area0 = areaOf(graph.nodes);
4831
+ const links = roomLinks(graph);
4832
+ const row = (board, nodes, ms, spacing, lanes) => ({
4833
+ fixture,
4834
+ board,
4835
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
4836
+ ms,
4837
+ ...measureBoard({
4838
+ nodes,
4839
+ edges: graph.edges
4840
+ }, spacing, lanes)
4841
+ });
4842
+ const rows = [row("gutter", makeRoom(graph.nodes, links, {
4843
+ ...baseline,
4844
+ spacing: 0,
4845
+ method: "routes",
4846
+ maxGrowth: Infinity,
4847
+ passes: 1
4848
+ }).items, 0, baseline.spacing ?? 4)];
4849
+ for (const s of settings) {
4464
4850
  const t0 = now();
4465
- const room = makeRoom(graph.nodes, links, o);
4466
- rows.push(row(board, room.items, now() - t0));
4851
+ const open = makeRoom(graph.nodes, links, {
4852
+ ...s.options,
4853
+ method: "routes"
4854
+ });
4855
+ rows.push(row(s.name, open.items, now() - t0, s.options.spacing ?? 4, s.lanes));
4467
4856
  }
4468
4857
  return rows;
4469
4858
  }
4859
+ /** The values of `maxGrowth` of the sweep of ROOM-A. */
4860
+ const GROWTH_SWEEP = [
4861
+ 0,
4862
+ .06,
4863
+ .09,
4864
+ .12,
4865
+ .15,
4866
+ .18,
4867
+ .21,
4868
+ .25,
4869
+ .3,
4870
+ .35,
4871
+ .5,
4872
+ Infinity
4873
+ ];
4874
+ /** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
4875
+ function growthSettings(values = GROWTH_SWEEP) {
4876
+ return values.map((m) => ({
4877
+ name: `maxGrowth ${m}`,
4878
+ options: {
4879
+ gutter: 8,
4880
+ spacing: 4,
4881
+ maxGrowth: m
4882
+ }
4883
+ }));
4884
+ }
4885
+ /**
4886
+ * This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
4887
+ * The order `shared` is the far view without lanes, as before ROOM-C.
4888
+ */
4889
+ function laneSettings(spacings = [
4890
+ 2,
4891
+ 4,
4892
+ 6,
4893
+ 8
4894
+ ], orders = LANE_ORDERS) {
4895
+ return spacings.flatMap((spacing) => [{
4896
+ name: `shared spacing ${spacing}`,
4897
+ options: {
4898
+ gutter: 8,
4899
+ spacing
4900
+ }
4901
+ }, ...orders.map((lanes) => ({
4902
+ name: `${lanes} spacing ${spacing}`,
4903
+ options: {
4904
+ gutter: 8,
4905
+ spacing
4906
+ },
4907
+ lanes
4908
+ }))]);
4909
+ }
4910
+ /** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
4911
+ function passSettings(gutters = [
4912
+ 4,
4913
+ 8,
4914
+ 12,
4915
+ 16
4916
+ ], passes = [1, 2]) {
4917
+ return gutters.flatMap((gutter) => passes.map((p) => ({
4918
+ name: `gutter ${gutter} passes ${p}`,
4919
+ options: {
4920
+ gutter,
4921
+ spacing: 4,
4922
+ passes: p
4923
+ }
4924
+ })));
4925
+ }
4470
4926
  /** This function writes the rows of `benchRoom` as a Markdown table. */
4471
4927
  function formatRoom(rows) {
4472
4928
  return [
@@ -4478,6 +4934,6 @@ function formatRoom(rows) {
4478
4934
  })
4479
4935
  ].join("\n");
4480
4936
  }
4481
- export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, arcCurve, backtrackOf, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, cubicAt, curveOf, curveSpace, denseGroupsBoard, dist, distinctPoints, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roomDemand, roomLinks, roundCorners, roundedCurve, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, toSvgPath, treeDepth, trunksRouter, turns };
4937
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, BUILT_IN_STRATEGIES, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, GROWTH_SWEEP, Grid, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, STEPS, STRATEGIES, TOLERANCE, WayFinder, allSections, arcCurve, asRouter, backtrackOf, benchCurveStrategies, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, createRegistry, cubicAt, curveOf, curveSpace, curveStrategies, defineStrategy, denseGroupsBoard, dist, distinctPoints, findStrategy, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, gapLanes, getStrategy, goesOutAndBack, groupEdges, growthSettings, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, laneSettings, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, passSettings, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, register, rng, roomDemand, roomLinks, roomStrategies, roundCorners, roundedCurve, routerStrategies, routersOf, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, sweepRoom, toSvgPath, treeDepth, trunksRouter, turns };
4482
4938
 
4483
4939
  //# sourceMappingURL=index.js.map