@mark1russell7/highways 0.1.4 → 0.2.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
@@ -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);
@@ -2340,13 +2340,20 @@ function orderLanes(run, routes) {
2340
2340
  return [...placed.map((x) => x.edge), ...lost.sort()];
2341
2341
  }
2342
2342
  // src/sections.ts
2343
+ /** The default values of `SectionOptions`. */
2344
+ const SECTION_DEFAULTS = Object.freeze({
2345
+ cap: 8,
2346
+ maxDepth: 16,
2347
+ collapse: 32
2348
+ });
2343
2349
  /**
2344
2350
  * This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
2345
2351
  * When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
2346
2352
  */
2347
2353
  function buildSections(nodes, options = {}) {
2348
- const cap = Math.max(1, options.cap ?? 8);
2349
- const maxDepth = options.maxDepth ?? 16;
2354
+ const cap = Math.max(1, options.cap ?? SECTION_DEFAULTS.cap);
2355
+ const maxDepth = options.maxDepth ?? SECTION_DEFAULTS.maxDepth;
2356
+ const collapse = options.collapse ?? SECTION_DEFAULTS.collapse;
2350
2357
  const centers = nodes.map((n) => center(n.box));
2351
2358
  let x0 = Infinity;
2352
2359
  let y0 = Infinity;
@@ -2371,7 +2378,7 @@ function buildSections(nodes, options = {}) {
2371
2378
  };
2372
2379
  const make = (id, depth, cell, list) => {
2373
2380
  let box = cell;
2374
- for (let guard = 0; list.length > cap && depth < maxDepth && guard < 32; guard++) {
2381
+ for (let guard = 0; list.length > cap && depth < maxDepth && guard < collapse; guard++) {
2375
2382
  const mx = box.x + box.w / 2;
2376
2383
  const my = box.y + box.h / 2;
2377
2384
  const parts = [
@@ -2425,6 +2432,156 @@ function allSections(root) {
2425
2432
  function treeDepth(root) {
2426
2433
  return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
2427
2434
  }
2435
+ /**
2436
+ * This function examines a tree of sections from a grouping rule. It throws an error when the tree is not valid for `count` nodes.
2437
+ * In a valid tree, each node is in exactly one leaf. The children of a section split its nodes, and a child is one level deeper than its parent.
2438
+ */
2439
+ function checkSections(root, count) {
2440
+ const seen = new Array(count).fill(0);
2441
+ const ids = /* @__PURE__ */ new Set();
2442
+ const fail = (s, what) => {
2443
+ throw new Error(`The tree of sections is not valid at the section "${s.id}": ${what}.`);
2444
+ };
2445
+ if (root.depth !== 0) fail(root, "the root has a depth that is not 0");
2446
+ if (root.nodes.length !== count) fail(root, `the root has ${root.nodes.length} nodes, and the graph has ${count}`);
2447
+ const walk = (s) => {
2448
+ if (ids.has(s.id)) fail(s, "two sections have this id");
2449
+ ids.add(s.id);
2450
+ if (s.children.length === 0) {
2451
+ for (const i of s.nodes) {
2452
+ if (!Number.isInteger(i) || i < 0 || i >= count) fail(s, `the node index ${i} is not in the graph`);
2453
+ seen[i]++;
2454
+ }
2455
+ return;
2456
+ }
2457
+ let total = 0;
2458
+ const own = new Set(s.nodes);
2459
+ for (const c of s.children) {
2460
+ if (c.depth !== s.depth + 1) fail(c, "a child is not one level deeper than its parent");
2461
+ for (const i of c.nodes) if (!own.has(i)) fail(c, `the node index ${i} is not in the parent`);
2462
+ total += c.nodes.length;
2463
+ walk(c);
2464
+ }
2465
+ if (total !== s.nodes.length) fail(s, "the children do not split the nodes of the section");
2466
+ };
2467
+ walk(root);
2468
+ const lost = seen.findIndex((n) => n !== 1);
2469
+ if (lost >= 0) fail(root, `the node index ${lost} is in ${seen[lost]} leaves, not in one`);
2470
+ }
2471
+ // src/groups/quadtree/index.ts
2472
+ /**
2473
+ * This function gives the tree of sections of the grouping rule `quadtree`: the cells of a quadtree of the plane (`buildSections`).
2474
+ * It is the default grouping rule of the router `sections`.
2475
+ */
2476
+ function quadtreeGroups(graph, options = {}) {
2477
+ return buildSections(graph.nodes, options);
2478
+ }
2479
+ // src/groups/keys/index.ts
2480
+ /** This key rule gives the string `data.group` of the node, or `undefined` when the node has no such string. */
2481
+ const byDataGroup = (node) => {
2482
+ const group = node.data?.["group"];
2483
+ return typeof group === "string" ? group : void 0;
2484
+ };
2485
+ /** This function gives the bounding box of the boxes of the nodes of the list. */
2486
+ function boundsOf(nodes, list) {
2487
+ let x0 = Infinity;
2488
+ let y0 = Infinity;
2489
+ let x1 = -Infinity;
2490
+ let y1 = -Infinity;
2491
+ for (const i of list) {
2492
+ const b = nodes[i].box;
2493
+ x0 = Math.min(x0, b.x);
2494
+ y0 = Math.min(y0, b.y);
2495
+ x1 = Math.max(x1, b.x + b.w);
2496
+ y1 = Math.max(y1, b.y + b.h);
2497
+ }
2498
+ return list.length ? {
2499
+ x: x0,
2500
+ y: y0,
2501
+ w: x1 - x0,
2502
+ h: y1 - y0
2503
+ } : {
2504
+ x: 0,
2505
+ y: 0,
2506
+ w: 0,
2507
+ h: 0
2508
+ };
2509
+ }
2510
+ /**
2511
+ * This function gives the tree of sections of the grouping rule `keys`. The root holds each node. Each child of the root holds the nodes of one key,
2512
+ * in the order of their first node. The id of a group is `s:` and its key. A node without a key is a group alone, with the id `s#` and its index.
2513
+ * Thus the far view of the router `sections` shows one item for each group.
2514
+ */
2515
+ function keyedSections(nodes, options = {}) {
2516
+ const key = options.key ?? byDataGroup;
2517
+ const groups = /* @__PURE__ */ new Map();
2518
+ nodes.forEach((n, i) => {
2519
+ const k = key(n, i);
2520
+ const id = k === void 0 ? `s#${i}` : `s:${k}`;
2521
+ const list = groups.get(id);
2522
+ if (list) list.push(i);
2523
+ else groups.set(id, [i]);
2524
+ });
2525
+ const children = [...groups].map(([id, list]) => group(nodes, id, list, options.split));
2526
+ return {
2527
+ id: "s",
2528
+ depth: 0,
2529
+ cell: boundsOf(nodes, nodes.map((_, i) => i)),
2530
+ nodes: nodes.map((_, i) => i),
2531
+ children
2532
+ };
2533
+ }
2534
+ /** This function gives the section of one group at depth 1. With `split`, the section has the quadtree of its nodes below it. */
2535
+ function group(nodes, id, list, split) {
2536
+ if (!split) return {
2537
+ id,
2538
+ depth: 1,
2539
+ cell: boundsOf(nodes, list),
2540
+ nodes: list,
2541
+ children: []
2542
+ };
2543
+ const tree = buildSections(list.map((i) => nodes[i]), split);
2544
+ const rebase = (s) => ({
2545
+ id: s.id === "s" ? id : `${id}/${s.id.slice(1)}`,
2546
+ depth: s.depth + 1,
2547
+ cell: s.cell,
2548
+ nodes: s.nodes.map((j) => list[j]),
2549
+ children: s.children.map(rebase)
2550
+ });
2551
+ return rebase(tree);
2552
+ }
2553
+ /** This function gives the tree of sections of the grouping rule `keys` for a graph. */
2554
+ function keyGroups(graph, options = {}) {
2555
+ return keyedSections(graph.nodes, options);
2556
+ }
2557
+ // src/pick.ts
2558
+ /** This function gives the median width of the shown items of each level, in units of the plane. */
2559
+ function itemSizes(levels) {
2560
+ return levels.map((level) => {
2561
+ const sizes = level.items.map((i) => Math.max(i.box.w, i.box.h)).sort((a, b) => a - b);
2562
+ return sizes.length ? sizes[Math.floor(sizes.length / 2)] : 0;
2563
+ });
2564
+ }
2565
+ /** The default thresholds: a node of 22 pixels shows its routes, and a group of 120 pixels shows its parts. */
2566
+ const PICK = Object.freeze({
2567
+ routesAt: 22,
2568
+ itemAt: 120
2569
+ });
2570
+ /**
2571
+ * This function picks the level that a camera shows at a scale, in pixels for each unit of the plane. The sizes come from `itemSizes`.
2572
+ * The value `sizes.length` stands for the routes of each edge. The last level holds the nodes alone.
2573
+ * Each threshold that `options` does not give gets its value from `PICK`.
2574
+ */
2575
+ function pickLevel(sizes, scale, options = PICK) {
2576
+ const routesAt = options.routesAt ?? PICK.routesAt;
2577
+ const itemAt = options.itemAt ?? PICK.itemAt;
2578
+ const n = sizes.length;
2579
+ if (n === 0) return 0;
2580
+ if (sizes[n - 1] * scale >= routesAt) return n;
2581
+ let pick = 0;
2582
+ for (let d = 0; d < n - 1; d++) if (sizes[d] * scale >= itemAt) pick = d;
2583
+ return pick;
2584
+ }
2428
2585
  // src/routers/corners.ts
2429
2586
  /**
2430
2587
  * This function makes a router that goes around the nodes.
@@ -2623,9 +2780,12 @@ function sumFaults(list) {
2623
2780
  });
2624
2781
  }
2625
2782
  // src/levels.ts
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
- const TIGHT_MARGIN = 1;
2628
- const EPS = 1e-6;
2783
+ /** The default values of the two limits of `LevelLinkOptions`. */
2784
+ const LEVEL_LINK_DEFAULTS = Object.freeze({
2785
+ tightMargin: 1,
2786
+ plugInset: 6
2787
+ });
2788
+ const EPS$1 = 1e-6;
2629
2789
  /**
2630
2790
  * This function removes the repeated points, the points in the middle of a straight run,
2631
2791
  * and the points where the way turns back on itself.
@@ -2633,15 +2793,15 @@ const EPS = 1e-6;
2633
2793
  function tidy(p) {
2634
2794
  const out = [];
2635
2795
  for (const q of p) {
2636
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2796
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2637
2797
  while (out.length >= 2) {
2638
2798
  const a = out[out.length - 2];
2639
2799
  const b = out[out.length - 1];
2640
2800
  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;
2801
+ if (Math.abs(cross) > EPS$1 * (1 + dist(a, b) * dist(b, q))) break;
2642
2802
  out.pop();
2643
2803
  }
2644
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2804
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2645
2805
  out.push(q);
2646
2806
  }
2647
2807
  return out;
@@ -2659,7 +2819,7 @@ function levelLinks(items, pairs, options = {}) {
2659
2819
  }));
2660
2820
  const byId = new Map(nodes.map((n) => [n.id, n]));
2661
2821
  const ways = levelWays(nodes, options);
2662
- const inset = Math.min(6, ways.margin);
2822
+ const inset = Math.min(options.plugInset ?? LEVEL_LINK_DEFAULTS.plugInset, ways.margin);
2663
2823
  const out = [];
2664
2824
  for (const p of pairs) {
2665
2825
  const s = byId.get(p.source);
@@ -2713,7 +2873,7 @@ function levelWays(nodes, options) {
2713
2873
  });
2714
2874
  let tight;
2715
2875
  const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
2716
- margin: Math.min(margin, TIGHT_MARGIN),
2876
+ margin: Math.min(margin, options.tightMargin ?? LEVEL_LINK_DEFAULTS.tightMargin),
2717
2877
  k,
2718
2878
  cones: true
2719
2879
  });
@@ -2881,7 +3041,7 @@ function freeSpans(lo, hi, covered) {
2881
3041
  if (x >= hi) break;
2882
3042
  }
2883
3043
  if (x < hi) out.push([x, hi]);
2884
- return out.filter(([a, b]) => b - a > EPS);
3044
+ return out.filter(([a, b]) => b - a > EPS$1);
2885
3045
  }
2886
3046
  /**
2887
3047
  * 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 +3173,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
3013
3173
  out.push(...step.legs);
3014
3174
  const end = out[out.length - 1];
3015
3175
  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;
3176
+ axis = Math.abs(end.y - before.y) < EPS$1 ? "x" : Math.abs(end.x - before.x) < EPS$1 ? "y" : void 0;
3017
3177
  i = step.j;
3018
3178
  }
3019
3179
  return out;
@@ -3023,7 +3183,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
3023
3183
  * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
3024
3184
  */
3025
3185
  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;
3186
+ 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
3187
  const options = [{
3028
3188
  c: {
3029
3189
  x: b.x,
@@ -3041,13 +3201,20 @@ function square(a, b, obstacles, pad, from, to) {
3041
3201
  for (const { c } of options) if (obstacles.clear(a, c, pad) && obstacles.clear(c, b, pad)) return [c, b];
3042
3202
  }
3043
3203
  // src/routers/sections.ts
3204
+ /** The default values of the numbers of `SectionsOptions`: the limits of the hubs, of the tree and of the level links. */
3205
+ const SECTIONS_DEFAULTS = Object.freeze({
3206
+ cap: SECTION_DEFAULTS.cap,
3207
+ maxDepth: SECTION_DEFAULTS.maxDepth,
3208
+ collapse: SECTION_DEFAULTS.collapse,
3209
+ tightMargin: LEVEL_LINK_DEFAULTS.tightMargin,
3210
+ plugInset: LEVEL_LINK_DEFAULTS.plugInset,
3211
+ hubs: Infinity,
3212
+ stretch: 2,
3213
+ stretchPad: 4,
3214
+ hubSlack: 80
3215
+ });
3044
3216
  const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
3045
3217
  /**
3046
- * A hub can be this many units farther from the target than the nearest approach of the way before it.
3047
- * A hub farther away goes out and back, so the way skips it.
3048
- */
3049
- const HUB_SLACK = 80;
3050
- /**
3051
3218
  * 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.
3052
3219
  * An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
3053
3220
  * Each way from hub to hub is found one time, so all the edges on it share its points.
@@ -3062,12 +3229,14 @@ function sectionsRouter(options) {
3062
3229
  const router = {
3063
3230
  name: options.name,
3064
3231
  route(graph) {
3065
- const stretch = options.stretch ?? 2;
3232
+ const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
3233
+ const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
3234
+ const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
3066
3235
  const byId = nodeIndex(graph);
3067
3236
  const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
3068
3237
  const ways = new WayFinder(graph.nodes, options);
3069
3238
  const corners = ways.corners;
3070
- const root = buildSections(graph.nodes, options);
3239
+ const root = groupNodes(graph, options);
3071
3240
  const parent = /* @__PURE__ */ new Map();
3072
3241
  const leafOf = [];
3073
3242
  for (const s of allSections(root)) {
@@ -3101,7 +3270,7 @@ function sectionsRouter(options) {
3101
3270
  let last = from;
3102
3271
  for (const c of list) {
3103
3272
  const h = hub(c);
3104
- if (dist(h, to) > near + HUB_SLACK) continue;
3273
+ if (dist(h, to) > near + hubSlack) continue;
3105
3274
  near = Math.min(near, pointSegmentDistance(to, last, h));
3106
3275
  last = h;
3107
3276
  kept.push(c);
@@ -3151,7 +3320,7 @@ function sectionsRouter(options) {
3151
3320
  const b = up(lv);
3152
3321
  const common = new Set(b);
3153
3322
  const lca = a.find((x) => common.has(x));
3154
- const keep = options.hubs ?? Infinity;
3323
+ const keep = options.hubs ?? SECTIONS_DEFAULTS.hubs;
3155
3324
  const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
3156
3325
  const end = ways.out(pt);
3157
3326
  const start = ways.out(ps);
@@ -3172,7 +3341,7 @@ function sectionsRouter(options) {
3172
3341
  for (let i = 1; i < chain.length; i++) append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
3173
3342
  append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
3174
3343
  append([pt.point]);
3175
- if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin || goesOutAndBack(out)) {
3344
+ if (polylineLength(out) > stretch * dist(ps.point, pt.point) + stretchPad * ways.margin || goesOutAndBack(out)) {
3176
3345
  routes.push({
3177
3346
  edge: e.id,
3178
3347
  points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
@@ -3213,8 +3382,16 @@ function sectionsRouter(options) {
3213
3382
  maxNodes: options.maxNodes
3214
3383
  };
3215
3384
  }
3385
+ /** This function gives the tree of sections of the grouping rule. It examines the tree of a strategy (`checkSections`). */
3386
+ function groupNodes(graph, options) {
3387
+ if (!options.group) return buildSections(graph.nodes, options);
3388
+ const root = options.group.run(graph, options.groupOptions);
3389
+ checkSections(root, graph.nodes.length);
3390
+ return root;
3391
+ }
3216
3392
  /**
3217
3393
  * This function gives one zoom level for each depth of the tree, and a last level with each node alone.
3394
+ * With the option `levels`, only those depths get a level with groups.
3218
3395
  * At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
3219
3396
  * In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
3220
3397
  * In the last level, the points of a link are the route of its first edge.
@@ -3223,7 +3400,9 @@ function makeLevels(graph, root, routeOf, options) {
3223
3400
  const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
3224
3401
  const levels = [];
3225
3402
  const deepest = treeDepth(root);
3403
+ const shown = options.levels && new Set(options.levels);
3226
3404
  for (let d = 0; d <= deepest + 1; d++) {
3405
+ if (shown && d <= deepest && !shown.has(d)) continue;
3227
3406
  const items = [];
3228
3407
  const itemOf = [];
3229
3408
  const boxOf = (list) => {
@@ -3700,7 +3879,7 @@ function trunksRouter(options) {
3700
3879
  };
3701
3880
  }
3702
3881
  // src/routers/index.ts
3703
- /** The routers that the benchmark races. */
3882
+ /** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
3704
3883
  const ROUTERS = [
3705
3884
  straightRouter,
3706
3885
  plugsRouter,
@@ -3823,17 +4002,34 @@ function openGaps(items, gaps, snap = DEFAULT_SNAP) {
3823
4002
  * 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
4003
  */
3825
4004
  function roomDemand(items, links, options = {}) {
4005
+ const gutter = options.gutter ?? DEFAULT_GUTTER;
4006
+ const snap = options.snap ?? DEFAULT_SNAP;
4007
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
4008
+ let total = demandOnce(items, links, options);
4009
+ for (let p = 1; p < passes; p++) {
4010
+ const next = demandOnce(openGaps(items, total, snap), links, options);
4011
+ if (next.length !== total.length || next.some((g, i) => g.axis !== total[i].axis)) break;
4012
+ total = total.map((g, i) => ({
4013
+ ...g,
4014
+ lanes: Math.max(g.lanes, next[i].lanes),
4015
+ need: g.need + Math.max(0, next[i].need - next[i].width)
4016
+ }));
4017
+ }
4018
+ return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
4019
+ }
4020
+ /** This function gives the demand of one pass, without the limit `maxGrowth`. */
4021
+ function demandOnce(items, links, options) {
3826
4022
  const gutter = options.gutter ?? DEFAULT_GUTTER;
3827
4023
  const spacing = options.spacing ?? DEFAULT_SPACING;
3828
4024
  const snap = options.snap ?? DEFAULT_SNAP;
3829
4025
  const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
3830
4026
  const gaps = boardGaps(items, snap);
3831
4027
  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) => ({
4028
+ return gaps.map((g, i) => ({
3833
4029
  ...g,
3834
4030
  lanes: lanes[i],
3835
4031
  need: Math.max(g.width, gutter + lanes[i] * spacing)
3836
- })), gutter, options.maxGrowth ?? Infinity);
4032
+ }));
3837
4033
  }
3838
4034
  /** This function gives the demand of the board and the board with each gap open to its need. */
3839
4035
  function makeRoom(items, links, options = {}) {
@@ -4018,6 +4214,141 @@ function capGrowth(items, gaps, gutter, maxGrowth) {
4018
4214
  }
4019
4215
  return out;
4020
4216
  }
4217
+ // src/room/lanes.ts
4218
+ /** The lane orders of `gapLanes`, in the order of the benchmark. */
4219
+ const LANE_ORDERS = [
4220
+ "links",
4221
+ "ends",
4222
+ "crossings"
4223
+ ];
4224
+ const EPS = 1e-6;
4225
+ const ROUNDS = 8;
4226
+ /**
4227
+ * 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.
4228
+ * A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
4229
+ * When a move puts a link through more boxes than before, the link keeps its points.
4230
+ */
4231
+ function gapLanes(items, links, options = {}) {
4232
+ const spacing = Math.max(0, options.spacing ?? 4);
4233
+ const order = options.order ?? "crossings";
4234
+ const moved = links.map((l) => l.points.map((p) => ({
4235
+ x: p.x,
4236
+ y: p.y
4237
+ })));
4238
+ for (const g of boardGaps(items, options.snap)) {
4239
+ if (g.width <= EPS) continue;
4240
+ const across = g.axis;
4241
+ const along = g.axis === "x" ? "y" : "x";
4242
+ const lo = g.at - g.width;
4243
+ const hi = g.at;
4244
+ const pieces = [];
4245
+ links.forEach((l, li) => {
4246
+ const p = l.points;
4247
+ for (let i = 2; i <= p.length - 2; i++) {
4248
+ const a = p[i - 1];
4249
+ const b = p[i];
4250
+ if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
4251
+ if (Math.abs(a[along] - b[along]) <= EPS) continue;
4252
+ const side = (q, r) => Math.sign(r[across] - q[across]);
4253
+ pieces.push({
4254
+ link: li,
4255
+ seg: i,
4256
+ lo: Math.min(a[along], b[along]),
4257
+ hi: Math.max(a[along], b[along]),
4258
+ turns: [[a[along], side(a, p[i - 2])], [b[along], side(b, p[i + 1])]]
4259
+ });
4260
+ }
4261
+ });
4262
+ if (pieces.length === 0) continue;
4263
+ const list = orderPieces(pieces, order);
4264
+ const lane = [];
4265
+ let n = 0;
4266
+ list.forEach((P, i) => {
4267
+ let k = 0;
4268
+ for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
4269
+ lane.push(k);
4270
+ n = Math.max(n, k + 1);
4271
+ });
4272
+ const step = Math.min(spacing, g.width / (n + 1));
4273
+ const mid = (lo + hi) / 2;
4274
+ list.forEach((P, i) => {
4275
+ const v = mid + (lane[i] - (n - 1) / 2) * step;
4276
+ moved[P.link][P.seg - 1][across] = v;
4277
+ moved[P.link][P.seg][across] = v;
4278
+ });
4279
+ }
4280
+ const boxes = new Grid(Math.max(20, meanSide(items)));
4281
+ for (const i of items) boxes.add(i.box, i.box);
4282
+ return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
4283
+ ...l,
4284
+ points: moved[i]
4285
+ });
4286
+ }
4287
+ const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
4288
+ /**
4289
+ * This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
4290
+ * 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.
4291
+ */
4292
+ function cost(P, Q) {
4293
+ let c = 0;
4294
+ for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
4295
+ for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
4296
+ return c;
4297
+ }
4298
+ /** This function gives the pieces of one gap in lane order, from the near side to the far side. */
4299
+ function orderPieces(pieces, order) {
4300
+ if (order === "links") return [...pieces];
4301
+ const sum = (P) => P.turns.reduce((s, [, side]) => s + side, 0);
4302
+ const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
4303
+ if (order === "ends") return list;
4304
+ const n = list.length;
4305
+ const index = new Map(list.map((P, i) => [P, i]));
4306
+ const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost(P, Q) : 0));
4307
+ const at = (P, Q) => c[index.get(P)][index.get(Q)];
4308
+ for (let round = 0; round < ROUNDS; round++) {
4309
+ let better = false;
4310
+ for (let i = 0; i < n; i++) {
4311
+ const x = list[i];
4312
+ let best = 0;
4313
+ let to = i;
4314
+ let d = 0;
4315
+ for (let k = i - 1; k >= 0; k--) {
4316
+ d += at(x, list[k]) - at(list[k], x);
4317
+ if (d < best) [best, to] = [d, k];
4318
+ }
4319
+ d = 0;
4320
+ for (let k = i + 1; k < n; k++) {
4321
+ d += at(list[k], x) - at(x, list[k]);
4322
+ if (d < best) [best, to] = [d, k];
4323
+ }
4324
+ if (to !== i) {
4325
+ list.splice(i, 1);
4326
+ list.splice(to, 0, x);
4327
+ better = true;
4328
+ }
4329
+ }
4330
+ if (!better) break;
4331
+ }
4332
+ return list;
4333
+ }
4334
+ /** This function gives the mean of the longer side of the boxes. */
4335
+ function meanSide(items) {
4336
+ let s = 0;
4337
+ for (const i of items) s += Math.max(i.box.w, i.box.h);
4338
+ return items.length ? s / items.length : 1;
4339
+ }
4340
+ /** This function counts the legs of a polyline that go through a box, for each box. */
4341
+ function hits(boxes, p) {
4342
+ let n = 0;
4343
+ for (let i = 1; i < p.length; i++) {
4344
+ const a = p[i - 1];
4345
+ const b = p[i];
4346
+ boxes.querySegment(a, b, 0, (box) => {
4347
+ if (segmentHitsBox(a, b, box)) n++;
4348
+ });
4349
+ }
4350
+ return n;
4351
+ }
4021
4352
  // src/bench/quality.ts
4022
4353
  /** The clearance stops at this distance. */
4023
4354
  const CLEARANCE_CAP = 40;
@@ -4203,7 +4534,7 @@ function measureStability(before, after) {
4203
4534
  };
4204
4535
  }
4205
4536
  // src/bench/run.ts
4206
- const now$2 = () => globalThis.performance?.now() ?? Date.now();
4537
+ const now$3 = () => globalThis.performance?.now() ?? Date.now();
4207
4538
  /** This function routes each fixture with each router, checks the hard rules and measures the quality. */
4208
4539
  function runBenchmark(routers, fixtures, options = {}) {
4209
4540
  const rows = [];
@@ -4242,9 +4573,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
4242
4573
  let first;
4243
4574
  let deterministic = true;
4244
4575
  for (let i = 0; i < Math.max(2, repeat); i++) {
4245
- const t0 = now$2();
4576
+ const t0 = now$3();
4246
4577
  const r = router.route(graph);
4247
- times.push(now$2() - t0);
4578
+ times.push(now$3() - t0);
4248
4579
  if (!first) first = r;
4249
4580
  else if (i === 1 && !sameData(r, first)) deterministic = false;
4250
4581
  }
@@ -4306,14 +4637,233 @@ function formatFaults(rows) {
4306
4637
  })
4307
4638
  ].join("\n");
4308
4639
  }
4640
+ // src/strategies/model.ts
4641
+ /** The steps that have strategies, in the order of the pipeline. */
4642
+ const STEPS = [
4643
+ "group",
4644
+ "router",
4645
+ "curve",
4646
+ "room"
4647
+ ];
4648
+ /**
4649
+ * This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
4650
+ * When the caller gives no option, the definition gets the `defaults` object itself.
4651
+ */
4652
+ function defineStrategy(definition) {
4653
+ const { step, name, describe, defaults, maxNodes } = definition;
4654
+ if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
4655
+ const strategy = {
4656
+ step,
4657
+ name,
4658
+ describe,
4659
+ defaults: Object.freeze({ ...defaults }),
4660
+ ...maxNodes === void 0 ? {} : { maxNodes },
4661
+ run(input, options) {
4662
+ const all = options === void 0 || Object.keys(options).length === 0 ? strategy.defaults : {
4663
+ ...strategy.defaults,
4664
+ ...options
4665
+ };
4666
+ return definition.run(input, all);
4667
+ }
4668
+ };
4669
+ return Object.freeze(strategy);
4670
+ }
4671
+ /** 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. */
4672
+ function createRegistry(strategies) {
4673
+ const out = {
4674
+ group: [],
4675
+ router: [],
4676
+ curve: [],
4677
+ room: []
4678
+ };
4679
+ for (const s of strategies) {
4680
+ const list = out[s.step];
4681
+ if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
4682
+ if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
4683
+ list.push(s);
4684
+ }
4685
+ return Object.freeze({
4686
+ group: Object.freeze(out.group),
4687
+ router: Object.freeze(out.router),
4688
+ curve: Object.freeze(out.curve),
4689
+ room: Object.freeze(out.room)
4690
+ });
4691
+ }
4692
+ /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
4693
+ function register(registry, ...strategies) {
4694
+ return createRegistry([
4695
+ ...registry.group ?? [],
4696
+ ...registry.router,
4697
+ ...registry.curve,
4698
+ ...registry.room,
4699
+ ...strategies
4700
+ ]);
4701
+ }
4702
+ /** This function gives the strategies of one step of a registry, in order. */
4703
+ function listStrategies(registry, step) {
4704
+ return registry[step] ?? [];
4705
+ }
4706
+ /** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
4707
+ function findStrategy(registry, step, name) {
4708
+ return listStrategies(registry, step).find((s) => s.name === name);
4709
+ }
4710
+ /** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
4711
+ function getStrategy(registry, step, name) {
4712
+ const s = findStrategy(registry, step, name);
4713
+ if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
4714
+ return s;
4715
+ }
4716
+ /** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
4717
+ function asRouter(strategy, options) {
4718
+ return {
4719
+ name: strategy.name,
4720
+ ...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
4721
+ route: (graph) => strategy.run(graph, options)
4722
+ };
4723
+ }
4724
+ /** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
4725
+ function routersOf(registry) {
4726
+ return listStrategies(registry, "router").map((s) => asRouter(s));
4727
+ }
4728
+ // src/strategies/builtins.ts
4729
+ /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
4730
+ function routerStrategy(name, describe, maxNodes, defaults, make) {
4731
+ return defineStrategy({
4732
+ step: "router",
4733
+ name,
4734
+ describe,
4735
+ defaults,
4736
+ maxNodes,
4737
+ run: (graph, options) => make({
4738
+ ...options,
4739
+ name
4740
+ }).route(graph)
4741
+ });
4742
+ }
4743
+ /** This function makes a router strategy for a router without options. */
4744
+ function fixedRouter(router, describe) {
4745
+ return defineStrategy({
4746
+ step: "router",
4747
+ name: router.name,
4748
+ describe,
4749
+ defaults: {},
4750
+ run: (graph) => router.route(graph)
4751
+ });
4752
+ }
4753
+ /** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
4754
+ const routerStrategies = Object.freeze({
4755
+ straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
4756
+ 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."),
4757
+ 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),
4758
+ "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),
4759
+ 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),
4760
+ 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),
4761
+ "shared-all": routerStrategy("shared-all", "The same as `shared`, but all edges share their ways.", 1e4, {}, sharedRouter),
4762
+ 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),
4763
+ "sections-tree": routerStrategy("sections-tree", "The same as `sections`, but without the limit of the stretch.", 1e5, { stretch: Infinity }, sectionsRouter)
4764
+ });
4765
+ /** The context of a curve input, without the keys that have no value. */
4766
+ const contextOf = (input) => ({
4767
+ ...input.startNormal ? { startNormal: input.startNormal } : {},
4768
+ ...input.endNormal ? { endNormal: input.endNormal } : {},
4769
+ ...input.space ? { space: input.space } : {}
4770
+ });
4771
+ /** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
4772
+ const curveStrategies = Object.freeze({
4773
+ straight: defineStrategy({
4774
+ step: "curve",
4775
+ name: "straight",
4776
+ describe: "The polyline as it is.",
4777
+ defaults: {},
4778
+ run: (input) => straightCurve(input.points)
4779
+ }),
4780
+ rounded: defineStrategy({
4781
+ step: "curve",
4782
+ name: "rounded",
4783
+ describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
4784
+ defaults: { radius: 12 },
4785
+ run: (input, o) => roundedCurve(input.points, {
4786
+ ...contextOf(input),
4787
+ radius: o.radius
4788
+ })
4789
+ }),
4790
+ spline: defineStrategy({
4791
+ step: "curve",
4792
+ name: "spline",
4793
+ describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
4794
+ defaults: { tension: 0 },
4795
+ run: (input, o) => splineCurve(input.points, {
4796
+ ...contextOf(input),
4797
+ tension: o.tension
4798
+ })
4799
+ }),
4800
+ arc: defineStrategy({
4801
+ step: "curve",
4802
+ name: "arc",
4803
+ describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
4804
+ defaults: { radius: 12 },
4805
+ run: (input, o) => arcCurve(input.points, {
4806
+ ...contextOf(input),
4807
+ radius: o.radius
4808
+ })
4809
+ })
4810
+ });
4811
+ const roomStrategy = (method, describe) => defineStrategy({
4812
+ step: "room",
4813
+ name: method,
4814
+ describe,
4815
+ defaults: {},
4816
+ run: (input, o) => roomDemand(input.items, input.links, {
4817
+ ...o,
4818
+ method
4819
+ })
4820
+ });
4821
+ /** The built-in room strategies: the methods of `roomDemand`. */
4822
+ const roomStrategies = Object.freeze({
4823
+ 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."),
4824
+ lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap.")
4825
+ });
4826
+ /** The built-in grouping rules: the strategies of the step `group`. The router `sections` makes its hubs and its zoom levels from their tree. */
4827
+ const groupStrategies = Object.freeze({
4828
+ quadtree: defineStrategy({
4829
+ step: "group",
4830
+ name: "quadtree",
4831
+ describe: "The cells of a quadtree of the plane. A cell with more than `cap` nodes splits in four. It is the default rule of the router `sections`.",
4832
+ defaults: { ...SECTION_DEFAULTS },
4833
+ run: (graph, o) => quadtreeGroups(graph, o)
4834
+ }),
4835
+ keys: defineStrategy({
4836
+ step: "group",
4837
+ name: "keys",
4838
+ describe: "One group for each key of the nodes. The default key is the string `data.group` of the node. A node without a key is a group alone.",
4839
+ defaults: { key: byDataGroup },
4840
+ run: (graph, o) => keyGroups(graph, o)
4841
+ })
4842
+ });
4843
+ /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
4844
+ const BUILT_IN_STRATEGIES = Object.freeze([
4845
+ ...Object.values(groupStrategies),
4846
+ ...Object.values(routerStrategies),
4847
+ ...Object.values(curveStrategies),
4848
+ ...Object.values(roomStrategies)
4849
+ ]);
4850
+ /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
4851
+ const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
4309
4852
  // src/bench/curves.ts
4310
- const now$1 = () => globalThis.performance?.now() ?? Date.now();
4853
+ const now$2 = () => globalThis.performance?.now() ?? Date.now();
4311
4854
  /**
4312
4855
  * This function routes a graph with a router and makes each curve type of each route.
4313
4856
  * It counts the curves that go through a box, with and without the free space, and it compares their length to the polylines.
4314
4857
  * The type `spline` leaves each plug along the normal of the nearest side of its node.
4315
4858
  */
4316
4859
  function benchCurves(router, fixture, graph, options = {}) {
4860
+ return benchCurveStrategies(router, fixture, graph, CURVE_TYPES.map((t) => curveStrategies[t]), options);
4861
+ }
4862
+ /**
4863
+ * This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
4864
+ * The option `radius` goes to each strategy. A strategy without such an option does not read it.
4865
+ */
4866
+ function benchCurveStrategies(router, fixture, graph, curves, options = {}) {
4317
4867
  const tolerance = options.tolerance ?? .25;
4318
4868
  const result = router.route(graph);
4319
4869
  const byId = nodeIndex(graph);
@@ -4337,27 +4887,27 @@ function benchCurves(router, fixture, graph, options = {}) {
4337
4887
  });
4338
4888
  const base = measureLinks(boxes, routes);
4339
4889
  const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
4340
- return CURVE_TYPES.map((type) => {
4890
+ const radius = { radius: options.radius ?? 12 };
4891
+ return curves.map((curve) => {
4341
4892
  const make = (withSpace) => routes.map((r) => ({
4342
4893
  source: r.source,
4343
4894
  target: r.target,
4344
- points: sampleCurve(curveOf(r.points, {
4345
- type,
4346
- radius: options.radius ?? 12,
4895
+ points: sampleCurve(curve.run({
4896
+ points: r.points,
4347
4897
  startNormal: r.startNormal,
4348
4898
  endNormal: r.endNormal,
4349
4899
  ...withSpace ? { space } : {}
4350
- }), tolerance)
4900
+ }, radius), tolerance)
4351
4901
  }));
4352
4902
  const free = measureLinks(boxes, make(false));
4353
- const t0 = now$1();
4903
+ const t0 = now$2();
4354
4904
  const clamped = make(true);
4355
- const ms = now$1() - t0;
4905
+ const ms = now$2() - t0;
4356
4906
  const length = clamped.reduce((sum, l) => sum + polylineLength(l.points), 0);
4357
4907
  return {
4358
4908
  router: router.name,
4359
4909
  fixture,
4360
- type,
4910
+ type: curve.name,
4361
4911
  links: routes.length,
4362
4912
  through: base.through,
4363
4913
  free: free.through,
@@ -4376,7 +4926,7 @@ function formatCurves(rows) {
4376
4926
  ].join("\n");
4377
4927
  }
4378
4928
  // src/bench/room.ts
4379
- const now = () => globalThis.performance?.now() ?? Date.now();
4929
+ const now$1 = () => globalThis.performance?.now() ?? Date.now();
4380
4930
  /** This function gives the area of the bounding box of the nodes. */
4381
4931
  function areaOf(nodes) {
4382
4932
  let x0 = Infinity;
@@ -4393,11 +4943,13 @@ function areaOf(nodes) {
4393
4943
  }
4394
4944
  /**
4395
4945
  * 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`.
4946
+ * With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
4947
+ * Without it, the links of a gap share the lines of the level links.
4948
+ * The function also measures the lane routes of the router `sections` at the lane width `spacing`.
4397
4949
  */
4398
- function measureBoard(graph, spacing) {
4950
+ function measureBoard(graph, spacing, order) {
4399
4951
  const links = roomLinks(graph);
4400
- const drawn = levelLinks(graph.nodes.map((n) => ({
4952
+ const level = levelLinks(graph.nodes.map((n) => ({
4401
4953
  id: n.id,
4402
4954
  box: n.box,
4403
4955
  nodes: [n.id]
@@ -4406,6 +4958,10 @@ function measureBoard(graph, spacing) {
4406
4958
  target: l.target,
4407
4959
  count: l.count ?? 1
4408
4960
  })));
4961
+ const drawn = order ? gapLanes(graph.nodes, level, {
4962
+ spacing,
4963
+ order
4964
+ }) : level;
4409
4965
  const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4410
4966
  const edgeOf = /* @__PURE__ */ new Map();
4411
4967
  for (const e of graph.edges) {
@@ -4427,10 +4983,10 @@ function measureBoard(graph, spacing) {
4427
4983
  };
4428
4984
  }
4429
4985
  /**
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`.
4986
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
4987
+ * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
4432
4988
  */
4433
- function benchRoom(fixture, graph, options = {}) {
4989
+ function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
4434
4990
  const spacing = options.spacing ?? 4;
4435
4991
  const area0 = areaOf(graph.nodes);
4436
4992
  const links = roomLinks(graph);
@@ -4445,28 +5001,129 @@ function benchRoom(fixture, graph, options = {}) {
4445
5001
  }, spacing)
4446
5002
  });
4447
5003
  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,
5004
+ const t0 = now$1();
5005
+ const gutter = makeRoom(graph.nodes, links, {
5006
+ ...options,
5007
+ spacing: 0,
5008
+ method: "routes"
5009
+ });
5010
+ rows.push(row("gutter", gutter.items, now$1() - t0));
5011
+ const { method: _method, ...preset } = options;
5012
+ for (const s of methods) {
5013
+ const t1 = now$1();
5014
+ const gaps = s.run({
5015
+ items: graph.nodes,
5016
+ links
5017
+ }, preset);
5018
+ const nodes = openGaps(graph.nodes, gaps, options.snap);
5019
+ rows.push(row(s.name, nodes, now$1() - t1));
5020
+ }
5021
+ return rows;
5022
+ }
5023
+ /**
5024
+ * This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
5025
+ * 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`.
5026
+ */
5027
+ function sweepRoom(fixture, graph, settings, baseline = {
5028
+ gutter: 8,
5029
+ spacing: 4
5030
+ }) {
5031
+ const area0 = areaOf(graph.nodes);
5032
+ const links = roomLinks(graph);
5033
+ const row = (board, nodes, ms, spacing, lanes) => ({
5034
+ fixture,
5035
+ board,
5036
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
5037
+ ms,
5038
+ ...measureBoard({
5039
+ nodes,
5040
+ edges: graph.edges
5041
+ }, spacing, lanes)
5042
+ });
5043
+ const rows = [row("gutter", makeRoom(graph.nodes, links, {
5044
+ ...baseline,
5045
+ spacing: 0,
5046
+ method: "routes",
5047
+ maxGrowth: Infinity,
5048
+ passes: 1
5049
+ }).items, 0, baseline.spacing ?? 4)];
5050
+ for (const s of settings) {
5051
+ const t0 = now$1();
5052
+ const open = makeRoom(graph.nodes, links, {
5053
+ ...s.options,
4456
5054
  method: "routes"
4457
- }],
4458
- ["lines", {
4459
- ...options,
4460
- method: "lines"
4461
- }]
4462
- ];
4463
- for (const [board, o] of states) {
4464
- const t0 = now();
4465
- const room = makeRoom(graph.nodes, links, o);
4466
- rows.push(row(board, room.items, now() - t0));
5055
+ });
5056
+ rows.push(row(s.name, open.items, now$1() - t0, s.options.spacing ?? 4, s.lanes));
4467
5057
  }
4468
5058
  return rows;
4469
5059
  }
5060
+ /** The values of `maxGrowth` of the sweep of ROOM-A. */
5061
+ const GROWTH_SWEEP = [
5062
+ 0,
5063
+ .06,
5064
+ .09,
5065
+ .12,
5066
+ .15,
5067
+ .18,
5068
+ .21,
5069
+ .25,
5070
+ .3,
5071
+ .35,
5072
+ .5,
5073
+ Infinity
5074
+ ];
5075
+ /** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
5076
+ function growthSettings(values = GROWTH_SWEEP) {
5077
+ return values.map((m) => ({
5078
+ name: `maxGrowth ${m}`,
5079
+ options: {
5080
+ gutter: 8,
5081
+ spacing: 4,
5082
+ maxGrowth: m
5083
+ }
5084
+ }));
5085
+ }
5086
+ /**
5087
+ * This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
5088
+ * The order `shared` is the far view without lanes, as before ROOM-C.
5089
+ */
5090
+ function laneSettings(spacings = [
5091
+ 2,
5092
+ 4,
5093
+ 6,
5094
+ 8
5095
+ ], orders = LANE_ORDERS) {
5096
+ return spacings.flatMap((spacing) => [{
5097
+ name: `shared spacing ${spacing}`,
5098
+ options: {
5099
+ gutter: 8,
5100
+ spacing
5101
+ }
5102
+ }, ...orders.map((lanes) => ({
5103
+ name: `${lanes} spacing ${spacing}`,
5104
+ options: {
5105
+ gutter: 8,
5106
+ spacing
5107
+ },
5108
+ lanes
5109
+ }))]);
5110
+ }
5111
+ /** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
5112
+ function passSettings(gutters = [
5113
+ 4,
5114
+ 8,
5115
+ 12,
5116
+ 16
5117
+ ], passes = [1, 2]) {
5118
+ return gutters.flatMap((gutter) => passes.map((p) => ({
5119
+ name: `gutter ${gutter} passes ${p}`,
5120
+ options: {
5121
+ gutter,
5122
+ spacing: 4,
5123
+ passes: p
5124
+ }
5125
+ })));
5126
+ }
4470
5127
  /** This function writes the rows of `benchRoom` as a Markdown table. */
4471
5128
  function formatRoom(rows) {
4472
5129
  return [
@@ -4478,6 +5135,118 @@ function formatRoom(rows) {
4478
5135
  })
4479
5136
  ].join("\n");
4480
5137
  }
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 };
5138
+ // src/bench/sections.ts
5139
+ const now = () => globalThis.performance?.now() ?? Date.now();
5140
+ /** This function gives the number of items of the first level with more than one item, or 0 for a result without levels. */
5141
+ function farItems(result) {
5142
+ return result.levels?.find((l) => l.items.length > 1)?.items.length ?? 0;
5143
+ }
5144
+ /**
5145
+ * This function measures the router `sections` on one board with each setting. The first row is the fair baseline: the defaults, with no option.
5146
+ * The direct routes come from the hub limit 0, so `hubRoutes` counts the routes that use the hubs.
5147
+ */
5148
+ function sweepSections(fixture, graph, settings) {
5149
+ return [{
5150
+ name: "defaults",
5151
+ options: {}
5152
+ }, ...settings].map((s) => {
5153
+ const direct = sectionsRouter({
5154
+ ...s.options,
5155
+ name: "direct",
5156
+ stretch: 0,
5157
+ stretchPad: 0
5158
+ }).route(graph);
5159
+ const t0 = now();
5160
+ const result = sectionsRouter({
5161
+ ...s.options,
5162
+ name: s.name
5163
+ }).route(graph);
5164
+ const ms = now() - t0;
5165
+ const levelFaults = measureLevelFaults(result);
5166
+ return {
5167
+ fixture,
5168
+ setting: s.name,
5169
+ faults: measureRouteFaults(graph, result),
5170
+ levelFaults: sumFaults(levelFaults),
5171
+ overlaps: levelFaults.reduce((n, f) => n + f.overlaps, 0),
5172
+ hubRoutes: result.routes.filter((r, i) => !sameData(r.points, direct.routes[i]?.points)).length,
5173
+ runs: result.runs?.length ?? 0,
5174
+ levels: result.levels?.length ?? 0,
5175
+ farItems: farItems(result),
5176
+ ms
5177
+ };
5178
+ });
5179
+ }
5180
+ /** The values of the hub limit (`stretch`) of the sweep of CUSTOM. The default is 2. */
5181
+ const STRETCH_SWEEP = [
5182
+ 1.25,
5183
+ 1.5,
5184
+ 2,
5185
+ 3,
5186
+ Infinity
5187
+ ];
5188
+ /** This function gives the settings of the sweep of the hub limit: one setting for each value of `stretch`. */
5189
+ function stretchSettings(values = STRETCH_SWEEP) {
5190
+ return values.map((stretch) => ({
5191
+ name: `stretch ${stretch}`,
5192
+ options: { stretch }
5193
+ }));
5194
+ }
5195
+ /** The values of `hubSlack` of the sweep of CUSTOM. The default is 80. */
5196
+ const SLACK_SWEEP = [
5197
+ 0,
5198
+ 40,
5199
+ 80,
5200
+ 160,
5201
+ Infinity
5202
+ ];
5203
+ /** This function gives the settings of the sweep of the slack of the hubs: one setting for each value of `hubSlack`. */
5204
+ function slackSettings(values = SLACK_SWEEP) {
5205
+ return values.map((hubSlack) => ({
5206
+ name: `hubSlack ${hubSlack}`,
5207
+ options: { hubSlack }
5208
+ }));
5209
+ }
5210
+ /** The values of the section limit (`cap`) of the sweep of CUSTOM. The default is 8. */
5211
+ const CAP_SWEEP = [
5212
+ 4,
5213
+ 6,
5214
+ 8,
5215
+ 12,
5216
+ 16
5217
+ ];
5218
+ /** This function gives the settings of the sweep of the section limit: one setting for each value of `cap`. */
5219
+ function capSettings(values = CAP_SWEEP) {
5220
+ return values.map((cap) => ({
5221
+ name: `cap ${cap}`,
5222
+ options: { cap }
5223
+ }));
5224
+ }
5225
+ /**
5226
+ * This function gives one setting for each grouping rule: the router `sections` with the strategy as its option `group`.
5227
+ * The map `options` gives the options of a strategy by its name.
5228
+ */
5229
+ function groupSettings(strategies = Object.values(groupStrategies), options = {}) {
5230
+ return strategies.map((group) => ({
5231
+ name: `group ${group.name}`,
5232
+ options: {
5233
+ group,
5234
+ ...options[group.name] ? { groupOptions: options[group.name] } : {}
5235
+ }
5236
+ }));
5237
+ }
5238
+ /** This function writes the rows of `sweepSections` as a Markdown table. */
5239
+ function formatSections(rows) {
5240
+ return [
5241
+ "| fixture | setting | through | out and back | worst detour | outside | hub routes | runs | levels | far items | level through | level out and back | level outside | level overlaps | ms |",
5242
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
5243
+ ...rows.map((r) => {
5244
+ const f = r.faults;
5245
+ const l = r.levelFaults;
5246
+ return `| ${r.fixture} | ${r.setting} | ${f.through} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${f.outside} | ${r.hubRoutes} | ${r.runs} | ${r.levels} | ${r.farItems} | ${l.through} | ${l.backtrack} | ${l.outside} | ${r.overlaps} | ${r.ms.toFixed(1)} |`;
5247
+ })
5248
+ ].join("\n");
5249
+ }
5250
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, BUILT_IN_STRATEGIES, CAP_SWEEP, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, GROWTH_SWEEP, Grid, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, PICK, ROUTERS, SECTION_DEFAULTS, SIDE_NORMAL, SLACK_SWEEP, STEPS, STRATEGIES, STRETCH_SWEEP, TOLERANCE, WayFinder, allSections, arcCurve, asRouter, backtrackOf, benchCurveStrategies, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, byDataGroup, capSettings, center, checkProperties, checkSections, cornersRouter, countCrossings, countOverlaps, createRegistry, cubicAt, curveOf, curveSpace, curveStrategies, defineStrategy, denseGroupsBoard, dist, distinctPoints, findStrategy, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatSections, formatTable, frechet, gapLanes, getStrategy, goesOutAndBack, groupEdges, groupSettings, groupStrategies, growthSettings, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, itemSizes, keyGroups, keyedSections, laneOrder, laneRoutes, laneSettings, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, passSettings, pathPoints, pickLevel, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, quadtreeGroups, register, rng, roomDemand, roomLinks, roomStrategies, roundCorners, roundedCurve, routerStrategies, routersOf, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, slackSettings, splineCurve, splitPoint, straightCurve, straightRouter, stretchSettings, sumFaults, sweepRoom, sweepSections, toSvgPath, treeDepth, trunksRouter, turns };
4482
5251
 
4483
5252
  //# sourceMappingURL=index.js.map