@mark1russell7/highways 0.9.3 → 0.9.4

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
@@ -5212,6 +5212,533 @@ function sideEnds(input, options = {}) {
5212
5212
  }
5213
5213
  return out;
5214
5214
  }
5215
+ // src/routers/gutter.ts
5216
+ /** The default cost of one bend of the gutter mode. */
5217
+ const GUTTER_BEND_COST = 16;
5218
+ const EPS$6 = 1e-6;
5219
+ /** The directions of a step: east, south, west and north, with the y axis down. */
5220
+ const DX = [
5221
+ 1,
5222
+ 0,
5223
+ -1,
5224
+ 0
5225
+ ];
5226
+ const DY = [
5227
+ 0,
5228
+ 1,
5229
+ 0,
5230
+ -1
5231
+ ];
5232
+ /**
5233
+ * This function gives the orthogonal route of each edge in the free room of a frame: the box `frame` minus the boxes of `nodes`.
5234
+ * Each route starts at the plug of the wall, and goes along the normal of the wall into the frame. It ends on a side of its end node.
5235
+ *
5236
+ * The route goes on a grid of lines. A line goes through the middle of each free band between the box edges, each plug and each entry.
5237
+ * A shortest path with a cost for each bend (`bendCost`) gives the route. Then the routes that share one gap get a lane each.
5238
+ * An edge without a path through the free room gets `undefined`.
5239
+ */
5240
+ function gutterRoutes(frame, nodes, edges, options = {}) {
5241
+ const bend = options.bendCost ?? 16;
5242
+ const boxes = nodes.map((n) => n.box).filter((b) => b.w > EPS$6 && b.h > EPS$6);
5243
+ const entries = edges.map(entriesOf);
5244
+ const xs = /* @__PURE__ */ new Set();
5245
+ const ys = /* @__PURE__ */ new Set();
5246
+ for (const [lines, lo, size, key] of [[
5247
+ xs,
5248
+ frame.x,
5249
+ frame.w,
5250
+ "x"
5251
+ ], [
5252
+ ys,
5253
+ frame.y,
5254
+ frame.h,
5255
+ "y"
5256
+ ]]) {
5257
+ const cuts = [lo, lo + size];
5258
+ for (const b of boxes) for (const v of key === "x" ? [b.x, b.x + b.w] : [b.y, b.y + b.h]) if (v > lo && v < lo + size) cuts.push(v);
5259
+ cuts.sort((a, b) => a - b);
5260
+ for (let i = 1; i < cuts.length; i++) if (cuts[i] - cuts[i - 1] > EPS$6) lines.add((cuts[i] + cuts[i - 1]) / 2);
5261
+ }
5262
+ edges.forEach((e, i) => {
5263
+ for (const { point, normal } of [{
5264
+ point: e.plug.point,
5265
+ normal: e.plug.normal
5266
+ }, ...entries[i]]) (normal.x !== 0 ? ys : xs).add(normal.x !== 0 ? point.y : point.x);
5267
+ });
5268
+ const grid = new Grid$1(frame, boxes, [...xs].sort((a, b) => a - b), [...ys].sort((a, b) => a - b));
5269
+ return lanes(frame, boxes, edges, entries, edges.map((e, i) => stubOnly(e, boxes) ?? grid.path(e.plug, e.end.box, entries[i], bend)), options.laneGap ?? 5);
5270
+ }
5271
+ /** This function gives the entries of the end node. They are its fixed plug, or the middles of its west and east sides and two points on each long side. */
5272
+ function entriesOf(e) {
5273
+ if (e.port) return [{
5274
+ point: e.port.point,
5275
+ normal: e.port.normal,
5276
+ fixed: true
5277
+ }];
5278
+ const b = e.end.box;
5279
+ const cy = b.y + b.h / 2;
5280
+ const d = Math.min(6, b.w / 4);
5281
+ return [
5282
+ ["west", {
5283
+ x: b.x,
5284
+ y: cy
5285
+ }],
5286
+ ["east", {
5287
+ x: b.x + b.w,
5288
+ y: cy
5289
+ }],
5290
+ ["north", {
5291
+ x: b.x + d,
5292
+ y: b.y
5293
+ }],
5294
+ ["north", {
5295
+ x: b.x + b.w - d,
5296
+ y: b.y
5297
+ }],
5298
+ ["south", {
5299
+ x: b.x + d,
5300
+ y: b.y + b.h
5301
+ }],
5302
+ ["south", {
5303
+ x: b.x + b.w - d,
5304
+ y: b.y + b.h
5305
+ }]
5306
+ ].filter(([s]) => e.side === void 0 || s === e.side).map(([s, point]) => ({
5307
+ point,
5308
+ normal: SIDE_NORMAL[s],
5309
+ fixed: false
5310
+ }));
5311
+ }
5312
+ /**
5313
+ * This function gives the route of an edge whose plug is level with its end node. The way straight in from the plug must meet that node first.
5314
+ * The route is the stub only: from the plug to the side of the node. A node that touches the wall gives a stub of length 0.
5315
+ */
5316
+ function stubOnly(e, boxes) {
5317
+ if (e.port) return void 0;
5318
+ const p = e.plug.point;
5319
+ const b = e.end.box;
5320
+ const n = e.plug.normal;
5321
+ let q;
5322
+ if (n.x !== 0) {
5323
+ if (!(p.y > b.y + EPS$6 && p.y < b.y + b.h - EPS$6)) return void 0;
5324
+ q = {
5325
+ x: n.x > 0 ? b.x + b.w : b.x,
5326
+ y: p.y
5327
+ };
5328
+ if ((q.x - p.x) * n.x > EPS$6) return void 0;
5329
+ } else {
5330
+ if (!(p.x > b.x + EPS$6 && p.x < b.x + b.w - EPS$6)) return void 0;
5331
+ q = {
5332
+ x: p.x,
5333
+ y: n.y > 0 ? b.y + b.h : b.y
5334
+ };
5335
+ if ((q.y - p.y) * n.y > EPS$6) return void 0;
5336
+ }
5337
+ for (const o of boxes) if (o !== b && blocks(p, q, o)) return void 0;
5338
+ return [p, q];
5339
+ }
5340
+ /** This function tells if the axis-parallel segment `ab` meets the closed box `o` along a part longer than a tiny length. A segment along a side meets the box. */
5341
+ function blocks(a, b, o) {
5342
+ if (a.y === b.y) {
5343
+ if (a.y < o.y - EPS$6 || a.y > o.y + o.h + EPS$6) return false;
5344
+ return Math.min(Math.max(a.x, b.x), o.x + o.w) - Math.max(Math.min(a.x, b.x), o.x) > EPS$6;
5345
+ }
5346
+ if (a.x < o.x - EPS$6 || a.x > o.x + o.w + EPS$6) return false;
5347
+ return Math.min(Math.max(a.y, b.y), o.y + o.h) - Math.max(Math.min(a.y, b.y), o.y) > EPS$6;
5348
+ }
5349
+ /** This function gives the direction index of a unit normal: 0 east, 1 south, 2 west, 3 north. */
5350
+ function dirOf(n) {
5351
+ return n.x > 0 ? 0 : n.y > 0 ? 1 : n.x < 0 ? 2 : 3;
5352
+ }
5353
+ /** The grid of the free room: the crossings of the lines that are strictly in the frame and not in a closed box. */
5354
+ var Grid$1 = class {
5355
+ #xs;
5356
+ #ys;
5357
+ #free;
5358
+ #boxes;
5359
+ constructor(frame, boxes, xs, ys) {
5360
+ this.#xs = xs;
5361
+ this.#ys = ys;
5362
+ this.#boxes = boxes;
5363
+ this.#free = new Uint8Array(xs.length * ys.length);
5364
+ for (let i = 0; i < ys.length; i++) {
5365
+ const y = ys[i];
5366
+ if (y <= frame.y + EPS$6 || y >= frame.y + frame.h - EPS$6) continue;
5367
+ for (let j = 0; j < xs.length; j++) {
5368
+ const x = xs[j];
5369
+ if (x <= frame.x + EPS$6 || x >= frame.x + frame.w - EPS$6) continue;
5370
+ let free = true;
5371
+ for (const b of boxes) if (x >= b.x - EPS$6 && x <= b.x + b.w + EPS$6 && y >= b.y - EPS$6 && y <= b.y + b.h + EPS$6) {
5372
+ free = false;
5373
+ break;
5374
+ }
5375
+ if (free) this.#free[i * xs.length + j] = 1;
5376
+ }
5377
+ }
5378
+ }
5379
+ #point(id) {
5380
+ const nx = this.#xs.length;
5381
+ return {
5382
+ x: this.#xs[id % nx],
5383
+ y: this.#ys[Math.floor(id / nx)]
5384
+ };
5385
+ }
5386
+ /** This method gives the neighbour of the crossing `id` in the direction `d`, or -1. Two free crossings next to each other on a line have no box between them. */
5387
+ #step(id, d) {
5388
+ const nx = this.#xs.length;
5389
+ const i = Math.floor(id / nx);
5390
+ const j = id % nx;
5391
+ const ni = i + DY[d];
5392
+ const nj = j + DX[d];
5393
+ if (ni < 0 || nj < 0 || ni >= this.#ys.length || nj >= nx) return -1;
5394
+ const k = ni * nx + nj;
5395
+ return this.#free[k] ? k : -1;
5396
+ }
5397
+ /** This method gives the first free crossing from the point `p` in the direction `d`. The way to that crossing must be clear. */
5398
+ #enter(p, d, skip) {
5399
+ const nx = this.#xs.length;
5400
+ const along = d % 2 === 0;
5401
+ const line = along ? this.#ys.indexOf(p.y) : this.#xs.indexOf(p.x);
5402
+ if (line < 0) return -1;
5403
+ const list = along ? this.#xs : this.#ys;
5404
+ const v = along ? p.x : p.y;
5405
+ const sign = d < 2 ? 1 : -1;
5406
+ let k = -1;
5407
+ if (sign > 0) {
5408
+ for (let m = 0; m < list.length; m++) if (list[m] > v + EPS$6) {
5409
+ k = m;
5410
+ break;
5411
+ }
5412
+ } else for (let m = list.length - 1; m >= 0; m--) if (list[m] < v - EPS$6) {
5413
+ k = m;
5414
+ break;
5415
+ }
5416
+ if (k < 0) return -1;
5417
+ const id = along ? line * nx + k : k * nx + line;
5418
+ if (!this.#free[id]) return -1;
5419
+ const q = this.#point(id);
5420
+ for (const b of this.#boxes) if (b !== skip && blocks(p, q, b)) return -1;
5421
+ return id;
5422
+ }
5423
+ /**
5424
+ * This method gives the shortest path from the plug into the end box, with the cost `bend` for each bend, or `undefined`.
5425
+ * The path leaves the plug along the inward normal, and it enters an entry along the inward normal of the side.
5426
+ */
5427
+ path(plug, end, entries, bend) {
5428
+ const n = this.#free.length;
5429
+ const d0 = dirOf({
5430
+ x: -plug.normal.x,
5431
+ y: -plug.normal.y
5432
+ });
5433
+ const first = this.#enter(plug.point, d0);
5434
+ if (first < 0) return void 0;
5435
+ const goals = [];
5436
+ for (const e of entries) {
5437
+ const id = this.#enter(e.point, dirOf(e.normal), end);
5438
+ if (id >= 0) goals.push({
5439
+ id,
5440
+ dir: (dirOf(e.normal) + 2) % 4,
5441
+ point: e.point
5442
+ });
5443
+ }
5444
+ if (goals.length === 0) return void 0;
5445
+ const byNode = /* @__PURE__ */ new Map();
5446
+ goals.forEach((g, k) => {
5447
+ const list = byNode.get(g.id);
5448
+ if (list) list.push(k);
5449
+ else byNode.set(g.id, [k]);
5450
+ });
5451
+ const states = 4 * n + goals.length;
5452
+ const cost = new Float64Array(states).fill(Infinity);
5453
+ const prev = new Int32Array(states).fill(-1);
5454
+ const heap = new Heap();
5455
+ const s0 = first * 4 + d0;
5456
+ cost[s0] = dist$1(plug.point, this.#point(first));
5457
+ heap.push(cost[s0], s0);
5458
+ let found = -1;
5459
+ while (heap.size > 0) {
5460
+ const [c, s] = heap.pop();
5461
+ if (c > cost[s]) continue;
5462
+ if (s >= 4 * n) {
5463
+ found = s;
5464
+ break;
5465
+ }
5466
+ const id = s >> 2;
5467
+ const d = s & 3;
5468
+ const here = this.#point(id);
5469
+ for (const k of byNode.get(id) ?? []) {
5470
+ const g = goals[k];
5471
+ if (g.dir === (d + 2) % 4) continue;
5472
+ const gs = 4 * n + k;
5473
+ const gc = c + dist$1(here, g.point) + (g.dir === d ? 0 : bend);
5474
+ if (gc < cost[gs]) {
5475
+ cost[gs] = gc;
5476
+ prev[gs] = s;
5477
+ heap.push(gc, gs);
5478
+ }
5479
+ }
5480
+ for (let nd = 0; nd < 4; nd++) {
5481
+ if (nd === (d + 2) % 4) continue;
5482
+ const next = this.#step(id, nd);
5483
+ if (next < 0) continue;
5484
+ const ns = next * 4 + nd;
5485
+ const nc = c + dist$1(here, this.#point(next)) + (nd === d ? 0 : bend);
5486
+ if (nc < cost[ns]) {
5487
+ cost[ns] = nc;
5488
+ prev[ns] = s;
5489
+ heap.push(nc, ns);
5490
+ }
5491
+ }
5492
+ }
5493
+ if (found < 0) return void 0;
5494
+ const pts = [goals[found - 4 * n].point];
5495
+ for (let s = prev[found]; s >= 0; s = prev[s]) pts.push(this.#point(s >> 2));
5496
+ pts.push(plug.point);
5497
+ return simplify(pts.reverse());
5498
+ }
5499
+ };
5500
+ function dist$1(a, b) {
5501
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
5502
+ }
5503
+ /** This function takes out each point between two collinear legs, and each repeated point. */
5504
+ function simplify(pts) {
5505
+ const out = [];
5506
+ for (const p of pts) {
5507
+ const a = out[out.length - 1];
5508
+ if (a && a.x === p.x && a.y === p.y) continue;
5509
+ const b = out[out.length - 2];
5510
+ if (a && b && (b.x === a.x && a.x === p.x || b.y === a.y && a.y === p.y)) out[out.length - 1] = p;
5511
+ else out.push(p);
5512
+ }
5513
+ return out;
5514
+ }
5515
+ /** A binary min-heap of (cost, state) pairs. Equal costs come out by the state, so the order is deterministic. */
5516
+ var Heap = class {
5517
+ #c = [];
5518
+ #s = [];
5519
+ get size() {
5520
+ return this.#c.length;
5521
+ }
5522
+ #less(i, j) {
5523
+ return this.#c[i] < this.#c[j] || this.#c[i] === this.#c[j] && this.#s[i] < this.#s[j];
5524
+ }
5525
+ #swap(i, j) {
5526
+ [this.#c[i], this.#c[j]] = [this.#c[j], this.#c[i]];
5527
+ [this.#s[i], this.#s[j]] = [this.#s[j], this.#s[i]];
5528
+ }
5529
+ push(c, s) {
5530
+ this.#c.push(c);
5531
+ this.#s.push(s);
5532
+ for (let i = this.#c.length - 1; i > 0;) {
5533
+ const p = i - 1 >> 1;
5534
+ if (!this.#less(i, p)) break;
5535
+ this.#swap(i, p);
5536
+ i = p;
5537
+ }
5538
+ }
5539
+ pop() {
5540
+ const top = [this.#c[0], this.#s[0]];
5541
+ const c = this.#c.pop();
5542
+ const s = this.#s.pop();
5543
+ if (this.#c.length > 0) {
5544
+ this.#c[0] = c;
5545
+ this.#s[0] = s;
5546
+ for (let i = 0;;) {
5547
+ const l = 2 * i + 1;
5548
+ const r = l + 1;
5549
+ let m = i;
5550
+ if (l < this.#c.length && this.#less(l, m)) m = l;
5551
+ if (r < this.#c.length && this.#less(r, m)) m = r;
5552
+ if (m === i) break;
5553
+ this.#swap(i, m);
5554
+ i = m;
5555
+ }
5556
+ }
5557
+ return top;
5558
+ }
5559
+ };
5560
+ /**
5561
+ * This function gives each leg that shares a gap with legs of other routes its own lane. The lanes are evenly spaced in the gap,
5562
+ * at most `laneGap` apart, in an order that gives no crossing where it can. A leg from a plug of the wall and a leg into a fixed plug do not move.
5563
+ */
5564
+ function lanes(frame, boxes, edges, entries, routes, laneGap) {
5565
+ const legs = [];
5566
+ routes.forEach((pts, r) => {
5567
+ if (!pts || pts.length < 2) return;
5568
+ const last = pts[pts.length - 1];
5569
+ const entry = entries[r].find((e) => e.point.x === last.x && e.point.y === last.y);
5570
+ for (let k = 0; k + 1 < pts.length; k++) {
5571
+ const a = pts[k];
5572
+ const b = pts[k + 1];
5573
+ if (a.x === b.x && a.y === b.y) continue;
5574
+ const horizontal = a.y === b.y;
5575
+ const [al, ac] = horizontal ? ["x", "y"] : ["y", "x"];
5576
+ const turn = (p) => p === void 0 ? 0 : Math.sign(p[ac] - a[ac]);
5577
+ const ta = turn(pts[k - 1]);
5578
+ const tb = turn(pts[k + 2]);
5579
+ const forward = b[al] > a[al];
5580
+ let [lo, hi] = free(frame, boxes, horizontal, a[ac], Math.min(a[al], b[al]), Math.max(a[al], b[al]));
5581
+ const end = k + 2 === pts.length;
5582
+ if (end && entry && !entry.fixed) {
5583
+ const e = edges[r].end.box;
5584
+ const [elo, size] = horizontal ? [e.y, e.h] : [e.x, e.w];
5585
+ const q = Math.min(2, size / 4);
5586
+ lo = Math.max(lo, elo + q);
5587
+ hi = Math.min(hi, elo + size - q);
5588
+ }
5589
+ legs.push({
5590
+ route: r,
5591
+ index: k,
5592
+ horizontal,
5593
+ across: a[ac],
5594
+ s0: Math.min(a[al], b[al]),
5595
+ s1: Math.max(a[al], b[al]),
5596
+ t0: forward ? ta : tb,
5597
+ t1: forward ? tb : ta,
5598
+ lo,
5599
+ hi,
5600
+ pinned: k === 0 || end && (entry === void 0 || entry.fixed)
5601
+ });
5602
+ }
5603
+ });
5604
+ const parent = legs.map((_, i) => i);
5605
+ const root = (i) => parent[i] === i ? i : parent[i] = root(parent[i]);
5606
+ for (let i = 0; i < legs.length; i++) for (let j = i + 1; j < legs.length; j++) {
5607
+ const a = legs[i];
5608
+ const b = legs[j];
5609
+ if (a.route === b.route || a.horizontal !== b.horizontal) continue;
5610
+ if (Math.min(a.s1, b.s1) - Math.max(a.s0, b.s0) <= -laneGap) continue;
5611
+ if (a.across < b.lo - EPS$6 || a.across > b.hi + EPS$6 || b.across < a.lo - EPS$6 || b.across > a.hi + EPS$6) continue;
5612
+ parent[root(i)] = root(j);
5613
+ }
5614
+ const groups = /* @__PURE__ */ new Map();
5615
+ legs.forEach((_, i) => {
5616
+ const g = root(i);
5617
+ const list = groups.get(g);
5618
+ if (list) list.push(i);
5619
+ else groups.set(g, [i]);
5620
+ });
5621
+ const out = routes.map((pts) => pts ? pts.map((p) => ({ ...p })) : void 0);
5622
+ for (const members of groups.values()) {
5623
+ if (members.length < 2) continue;
5624
+ const group = members.map((i) => legs[i]);
5625
+ const at = place(group, laneGap);
5626
+ group.forEach((leg, m) => {
5627
+ const pts = out[leg.route];
5628
+ const key = leg.horizontal ? "y" : "x";
5629
+ pts[leg.index][key] = at[m];
5630
+ pts[leg.index + 1][key] = at[m];
5631
+ });
5632
+ }
5633
+ return out;
5634
+ }
5635
+ /** This function gives the free range across a leg: the nearest box sides or walls on each side, over the span of the leg. */
5636
+ function free(frame, boxes, horizontal, across, s0, s1) {
5637
+ let lo = horizontal ? frame.y : frame.x;
5638
+ let hi = horizontal ? frame.y + frame.h : frame.x + frame.w;
5639
+ for (const b of boxes) {
5640
+ const [bs, bl, cs, cl] = horizontal ? [
5641
+ b.x,
5642
+ b.w,
5643
+ b.y,
5644
+ b.h
5645
+ ] : [
5646
+ b.y,
5647
+ b.h,
5648
+ b.x,
5649
+ b.w
5650
+ ];
5651
+ if (Math.min(s1, bs + bl) - Math.max(s0, bs) <= EPS$6) continue;
5652
+ if (cs + cl <= across + EPS$6) lo = Math.max(lo, cs + cl);
5653
+ if (cs >= across - EPS$6) hi = Math.min(hi, cs);
5654
+ }
5655
+ return [lo, hi];
5656
+ }
5657
+ /** This function tells the side of the leg `b` for the leg `a`. It gives -1 for the smaller coordinate, +1 for the larger and 0 for no rule. */
5658
+ function want(a, b) {
5659
+ const side = (sa, sb, ta, tb, inner) => {
5660
+ if (inner(sa, sb)) return ta;
5661
+ if (inner(sb, sa)) return -tb;
5662
+ return ta === tb ? 0 : Math.sign(ta - tb);
5663
+ };
5664
+ const left = side(a.s0, b.s0, a.t0, b.t0, (x, y) => x > y + EPS$6);
5665
+ const right = side(a.s1, b.s1, a.t1, b.t1, (x, y) => x < y - EPS$6);
5666
+ if (left !== 0 && right !== 0 && left !== right) return left;
5667
+ return left || right;
5668
+ }
5669
+ /**
5670
+ * This function gives the lane of each leg of a group. A pinned leg keeps its place, and the pinned legs keep their order.
5671
+ * The other legs go in the order of `want`, evenly spaced between the pinned legs and the ends of the free range.
5672
+ */
5673
+ function place(group, laneGap) {
5674
+ const n = group.length;
5675
+ const soft = group.map(() => []);
5676
+ const hard = group.map(() => []);
5677
+ const softIn = new Array(n).fill(0);
5678
+ const hardIn = new Array(n).fill(0);
5679
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
5680
+ const a = group[i];
5681
+ const b = group[j];
5682
+ if (a.pinned && b.pinned) {
5683
+ if (Math.abs(a.across - b.across) <= EPS$6) continue;
5684
+ const [x, y] = a.across < b.across ? [i, j] : [j, i];
5685
+ hard[x].push(y);
5686
+ hardIn[y]++;
5687
+ continue;
5688
+ }
5689
+ const w = want(a, b);
5690
+ if (w === 0) continue;
5691
+ const [x, y] = w < 0 ? [i, j] : [j, i];
5692
+ soft[x].push(y);
5693
+ softIn[y]++;
5694
+ }
5695
+ const order = [];
5696
+ const done = new Array(n).fill(false);
5697
+ while (order.length < n) {
5698
+ let best = -1;
5699
+ for (let i = 0; i < n; i++) {
5700
+ if (done[i] || hardIn[i] > 0) continue;
5701
+ if (best < 0 || softIn[i] < softIn[best] || softIn[i] === softIn[best] && group[i].across < group[best].across - EPS$6) best = i;
5702
+ }
5703
+ done[best] = true;
5704
+ order.push(best);
5705
+ for (const y of soft[best]) softIn[y]--;
5706
+ for (const y of hard[best]) hardIn[y]--;
5707
+ }
5708
+ for (let k = 0; k < order.length; k++) {
5709
+ const a = group[order[k]];
5710
+ if (!a.pinned) continue;
5711
+ let to = k + 1;
5712
+ for (let m = k + 1; m < order.length; m++) {
5713
+ const b = group[order[m]];
5714
+ if (b.pinned && Math.abs(b.across - a.across) <= EPS$6) order.splice(to++, 0, ...order.splice(m, 1));
5715
+ }
5716
+ k = to - 1;
5717
+ }
5718
+ const at = new Array(n).fill(0);
5719
+ let start = 0;
5720
+ let from = -Infinity;
5721
+ for (let k = 0; k <= n; k++) {
5722
+ const i = order[k];
5723
+ const pin = i !== void 0 && group[i].pinned;
5724
+ if (k < n && !pin) continue;
5725
+ const run = order.slice(start, k);
5726
+ if (run.length > 0) {
5727
+ const lo = Math.max(from, ...run.map((r) => group[r].lo));
5728
+ const hi = Math.min(k < n ? group[i].across : Infinity, ...run.map((r) => group[r].hi));
5729
+ const room = hi - lo;
5730
+ const s = Math.min(laneGap, room / (run.length + 1));
5731
+ const width = (run.length - 1) * s;
5732
+ const mean = run.reduce((t, r) => t + group[r].across, 0) / run.length;
5733
+ const first = Math.min(Math.max(mean - width / 2, lo + s), hi - s - width);
5734
+ run.forEach((r, m) => at[r] = room > EPS$6 && s > 0 ? first + m * s : group[r].across);
5735
+ }
5736
+ if (k < n) at[i] = group[i].across;
5737
+ start = k + 1;
5738
+ if (k < n) from = group[i].across;
5739
+ }
5740
+ return at;
5741
+ }
5215
5742
  // src/routers/frames.ts
5216
5743
  /** The tiny gap between the bounds and the band of a wall, so that a way along the bounds stays clear of the wall. */
5217
5744
  const EPS$5 = 1e-6;
@@ -5229,6 +5756,7 @@ function frameRoutes(graph, inner, options) {
5229
5756
  if (list) list.push(n);
5230
5757
  else children.set(n.parent, [n]);
5231
5758
  }
5759
+ if (options.inner === "gutter") return gutterFrameRoutes(inner, options, byId, children);
5232
5760
  const margin = options.margin ?? 5;
5233
5761
  const inset = options.frameInset ?? margin;
5234
5762
  const rooms = /* @__PURE__ */ new Map();
@@ -5236,8 +5764,7 @@ function frameRoutes(graph, inner, options) {
5236
5764
  for (const { edge, frame, plug } of inner) {
5237
5765
  const f = byId.get(frame);
5238
5766
  const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
5239
- const chain = [];
5240
- for (let p = byId.get(far.node).parent; p !== frame; p = byId.get(p).parent) chain.push(p);
5767
+ const chain = chainOf(byId, frame, far.node);
5241
5768
  const key = JSON.stringify([frame, ...chain]);
5242
5769
  let room = rooms.get(key);
5243
5770
  if (!room) rooms.set(key, room = makeRoom$1(f, chain, children, byId, inset, options));
@@ -5266,6 +5793,63 @@ function frameRoutes(graph, inner, options) {
5266
5793
  }
5267
5794
  return routes;
5268
5795
  }
5796
+ /** This function gives the nodes between the frame and the far end, the nearest first. Each of them holds the far end, so it is not an obstacle. */
5797
+ function chainOf(byId, frame, far) {
5798
+ const chain = [];
5799
+ for (let p = byId.get(far).parent; p !== frame; p = byId.get(p).parent) chain.push(p);
5800
+ return chain;
5801
+ }
5802
+ /**
5803
+ * This function gives the inner routes of the mode `gutter`. The inner edges of one room route together, so that they share the gaps in lanes.
5804
+ * A route is orthogonal, in the free room of the frame box. An edge without a way through the free room gets no route.
5805
+ */
5806
+ function gutterFrameRoutes(inner, options, byId, children) {
5807
+ const rooms = /* @__PURE__ */ new Map();
5808
+ const routes = inner.map(() => void 0);
5809
+ inner.forEach(({ edge, frame, plug }, at) => {
5810
+ const f = byId.get(frame);
5811
+ const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
5812
+ const chain = chainOf(byId, frame, far.node);
5813
+ const key = JSON.stringify([frame, ...chain]);
5814
+ let room = rooms.get(key);
5815
+ if (!room) rooms.set(key, room = {
5816
+ nodes: roomNodes(f, chain, children, byId, f.box, options),
5817
+ items: [],
5818
+ frame: f.box
5819
+ });
5820
+ const end = room.nodes.get(far.node);
5821
+ if (!end) return;
5822
+ const fp = placePlug(f, own, center(end.box));
5823
+ const port = fixedPortPoint(end, far) === void 0 ? void 0 : placePlug(end, far, fp.point);
5824
+ const side = far.port === void 0 ? void 0 : end.ports?.find((p) => p.id === far.port)?.side;
5825
+ room.items.push({
5826
+ at,
5827
+ edge: {
5828
+ plug: fp,
5829
+ end,
5830
+ ...port ? { port } : {},
5831
+ ...side ? { side } : {}
5832
+ }
5833
+ });
5834
+ });
5835
+ for (const room of rooms.values()) {
5836
+ const found = gutterRoutes(room.frame, [...room.nodes.values()], room.items.map((i) => i.edge), {
5837
+ laneGap: options.margin ?? 5,
5838
+ ...options.bendCost === void 0 ? {} : { bendCost: options.bendCost }
5839
+ });
5840
+ room.items.forEach(({ at }, k) => {
5841
+ const points = found[k];
5842
+ if (!points) return;
5843
+ const { edge, frame, plug } = inner[at];
5844
+ routes[at] = {
5845
+ edge: edge.id,
5846
+ points: plug === "source" ? points : points.reverse(),
5847
+ frame
5848
+ };
5849
+ });
5850
+ }
5851
+ return routes.filter((r) => r !== void 0);
5852
+ }
5269
5853
  /** This function puts the plug of the wall before a way that starts on the bounds. A first point on the line of the stub goes. */
5270
5854
  function stub(plug, way) {
5271
5855
  const [a, b] = way;
@@ -5281,31 +5865,7 @@ function stub(plug, way) {
5281
5865
  function makeRoom$1(f, chain, children, byId, inset, options) {
5282
5866
  const margin = options.margin ?? 5;
5283
5867
  const bounds = shrink(f.box, inset);
5284
- const skip = new Set(chain);
5285
- const nodes = /* @__PURE__ */ new Map();
5286
- for (const holder of [f.id, ...chain]) for (const n of children.get(holder) ?? []) {
5287
- if (skip.has(n.id)) continue;
5288
- const box = byId.get(holder).box;
5289
- if (inside(n.box, box)) {
5290
- nodes.set(n.id, n);
5291
- continue;
5292
- }
5293
- const rule = options.outside ?? "throw";
5294
- if (rule === "throw") throw new RangeError(`The box of the node "${n.id}" is not inside the box of its frame "${holder}". The option outside can skip or clamp it.`);
5295
- if (rule === "skip") continue;
5296
- const cut = cutBox(n.box, bounds);
5297
- if (cut) nodes.set(n.id, {
5298
- ...n,
5299
- box: cut,
5300
- ...n.ports ? { ports: n.ports.map((p) => p.at ? {
5301
- ...p,
5302
- at: {
5303
- x: p.at.x + n.box.x - cut.x,
5304
- y: p.at.y + n.box.y - cut.y
5305
- }
5306
- } : p) } : {}
5307
- });
5308
- }
5868
+ const nodes = roomNodes(f, chain, children, byId, bounds, options);
5309
5869
  const w = shrink(bounds, -(margin / 2 + EPS$5));
5310
5870
  const t = 2 * margin + 1;
5311
5871
  const walls = [
@@ -5342,6 +5902,38 @@ function makeRoom$1(f, chain, children, byId, inset, options) {
5342
5902
  nodes
5343
5903
  };
5344
5904
  }
5905
+ /**
5906
+ * This function gives the obstacles of a room by id. They are the children of the frame and of each node of the chain, but not the chain.
5907
+ * It applies the rule `outside` to each box outside the box of its holder. The rule `clamp` cuts such a box to `bounds`.
5908
+ */
5909
+ function roomNodes(f, chain, children, byId, bounds, options) {
5910
+ const skip = new Set(chain);
5911
+ const nodes = /* @__PURE__ */ new Map();
5912
+ for (const holder of [f.id, ...chain]) for (const n of children.get(holder) ?? []) {
5913
+ if (skip.has(n.id)) continue;
5914
+ const box = byId.get(holder).box;
5915
+ if (inside(n.box, box)) {
5916
+ nodes.set(n.id, n);
5917
+ continue;
5918
+ }
5919
+ const rule = options.outside ?? "throw";
5920
+ if (rule === "throw") throw new RangeError(`The box of the node "${n.id}" is not inside the box of its frame "${holder}". The option outside can skip or clamp it.`);
5921
+ if (rule === "skip") continue;
5922
+ const cut = cutBox(n.box, bounds);
5923
+ if (cut) nodes.set(n.id, {
5924
+ ...n,
5925
+ box: cut,
5926
+ ...n.ports ? { ports: n.ports.map((p) => p.at ? {
5927
+ ...p,
5928
+ at: {
5929
+ x: p.at.x + n.box.x - cut.x,
5930
+ y: p.at.y + n.box.y - cut.y
5931
+ }
5932
+ } : p) } : {}
5933
+ });
5934
+ }
5935
+ return nodes;
5936
+ }
5345
5937
  /** This function gives the box, smaller by `d` on each side. A negative `d` makes it larger. */
5346
5938
  function shrink(b, d) {
5347
5939
  return {
@@ -7501,7 +8093,7 @@ function turn(from, base, to) {
7501
8093
  * Thus the edges of a key come together on common ways. Each stretch that several edges share has the same points in each route.
7502
8094
  * The result gives each stretch as a run, with the order of its lanes.
7503
8095
  */
7504
- function sharedRouter(options, negotiation) {
8096
+ function sharedRouter(options, negotiation, hooks) {
7505
8097
  const discount = Math.min(1, Math.max(.05, options.discount ?? .5));
7506
8098
  const flow = options.cost === "flow";
7507
8099
  const alpha = Math.min(1, Math.max(.1, options.alpha ?? .6));
@@ -7552,7 +8144,20 @@ function sharedRouter(options, negotiation) {
7552
8144
  });
7553
8145
  }
7554
8146
  const discounted = /* @__PURE__ */ new Map();
7555
- const order = orderWays(all, options.order ?? "span", graph);
8147
+ const search = {
8148
+ graph,
8149
+ corners: corners.points.length,
8150
+ link,
8151
+ find: (w, cost, floor = 1) => {
8152
+ const way = corners.path(ways.out(w.ps), ways.out(w.pt), cost, floor);
8153
+ return way && {
8154
+ seq: way,
8155
+ ids: way.map((p, i) => i === 0 || i === way.length - 1 ? endId(p) : index.get(p))
8156
+ };
8157
+ }
8158
+ };
8159
+ const sorted = orderWays(all, options.order ?? "span", graph);
8160
+ const order = hooks?.order ? hooks.order(sorted, search) : sorted;
7556
8161
  const top = /* @__PURE__ */ new Map();
7557
8162
  const widths = /* @__PURE__ */ new Map();
7558
8163
  const widthOf = (i, j) => {
@@ -7676,6 +8281,7 @@ function sharedRouter(options, negotiation) {
7676
8281
  route(w);
7677
8282
  }
7678
8283
  }
8284
+ hooks?.refine?.(order, search);
7679
8285
  if (options.merge === "snell") {
7680
8286
  const pad = ways.margin / 2;
7681
8287
  snellMerges(order, corners.points.length + ends.size, (a, b) => ways.obstacles.clear(a, b, pad), (f) => Math.min(.95, share(f)));
@@ -8009,72 +8615,503 @@ function asRouter(strategy, options) {
8009
8615
  function routersOf(registry) {
8010
8616
  return listStrategies(registry, "router").map((s) => asRouter(s));
8011
8617
  }
8012
- // src/routers/attraction/index.ts
8013
- /** The default options of the router `attraction`. */
8014
- const ATTRACTION_DEFAULTS = Object.freeze({
8015
- passes: 3,
8016
- attraction: .5,
8017
- capacityWeight: 1,
8018
- laneWidth: 4
8618
+ // src/routers/annealing/index.ts
8619
+ /** The default options of the router `annealing`. */
8620
+ const ANNEALING_DEFAULTS = Object.freeze({
8621
+ candidates: 4,
8622
+ crossing: 4,
8623
+ temperature: 1,
8624
+ cooling: .95,
8625
+ budget: 50,
8626
+ seed: 1
8019
8627
  });
8020
- /** The declarations of the options of the router `attraction` (HWY-044). */
8021
- const ATTRACTION_SCHEMA = {
8022
- passes: {
8628
+ /** The declarations of the options of the router `annealing` (RULE-021). */
8629
+ const ANNEALING_SCHEMA = {
8630
+ candidates: {
8023
8631
  type: "integer",
8024
- min: 1,
8025
- max: 10,
8632
+ min: 3,
8633
+ max: 8,
8026
8634
  step: 1,
8027
8635
  marks: [
8028
- 2,
8029
8636
  3,
8030
- 5
8637
+ 4,
8638
+ 6
8031
8639
  ],
8032
- default: 3,
8033
- describe: "The number of passes. Each later pass routes each edge again from no ways."
8640
+ default: 4,
8641
+ describe: "The number of candidate ways of each edge."
8034
8642
  },
8035
- attraction: {
8643
+ crossing: {
8036
8644
  type: "number",
8037
8645
  min: 0,
8038
- max: 1,
8039
- step: .05,
8646
+ max: 20,
8647
+ step: .5,
8040
8648
  marks: [
8041
- .25,
8042
- .5,
8043
- .75
8649
+ 1,
8650
+ 4,
8651
+ 10
8044
8652
  ],
8045
- default: .5,
8046
- describe: "The pull of the history of a link. A busy link of the passes before costs less."
8653
+ default: 4,
8654
+ describe: "The cost of one crossing, in units of the median box side."
8047
8655
  },
8048
- capacityWeight: {
8656
+ temperature: {
8049
8657
  type: "number",
8050
- min: 0,
8658
+ min: .1,
8051
8659
  max: 10,
8052
- step: .5,
8660
+ step: .1,
8053
8661
  marks: [
8054
- 0,
8662
+ .5,
8055
8663
  1,
8056
8664
  4
8057
8665
  ],
8058
8666
  default: 1,
8059
- describe: "The cost weight of each edge over the capacity of a link."
8667
+ describe: "The start temperature, in units of the median box side."
8060
8668
  },
8061
- laneWidth: {
8669
+ cooling: {
8062
8670
  type: "number",
8063
- min: 1,
8064
- max: 12,
8065
- step: 1,
8066
- marks: [4, 6],
8067
- default: 4,
8068
- describe: "The width of one lane. The capacity of a link is its free width divided by this width."
8069
- }
8070
- };
8071
- /**
8072
- * This function makes the router `attraction` (HWY-044). It is the router `shared` with a negotiation over the passes.
8073
- * After each pass, the history `h` of each link grows by `use / m`, with `m` the edges of the key. The history stops at `0.9 / a`.
8074
- * In each pass, a link costs `len (1 - a h) (1 + b max(0, use - cap))` times the cost in `shared`.
8075
- * The busy links of the passes before thus pull the edges, and a link over its capacity pushes them away.
8076
- */
8077
- function attractionRouter(options) {
8671
+ min: .9,
8672
+ max: .99,
8673
+ step: .01,
8674
+ marks: [
8675
+ .9,
8676
+ .95,
8677
+ .98
8678
+ ],
8679
+ default: .95,
8680
+ describe: "The factor of the temperature after each sweep of the edges."
8681
+ },
8682
+ budget: {
8683
+ type: "integer",
8684
+ min: 20,
8685
+ max: 200,
8686
+ step: 10,
8687
+ marks: [
8688
+ 20,
8689
+ 50,
8690
+ 100
8691
+ ],
8692
+ default: 50,
8693
+ describe: "The budget: the number of changes for each edge."
8694
+ },
8695
+ seed: {
8696
+ type: "integer",
8697
+ min: 0,
8698
+ max: 1e6,
8699
+ step: 1,
8700
+ marks: [
8701
+ 1,
8702
+ 2,
8703
+ 3
8704
+ ],
8705
+ default: 1,
8706
+ describe: "The seed of the random choices. The same seed gives the same routes."
8707
+ }
8708
+ };
8709
+ /** The share of the cost of a link that the edges of one key use already, as in `shared` with its default discount. */
8710
+ const DISCOUNT = .5;
8711
+ /** The cost factor of the links of the earlier candidates in the search of the next candidate. */
8712
+ const PENALTY = 3;
8713
+ /** This function tells if the segments `a b` and `c d` cross at one inner point of each. A touch at an end or a common line is not a crossing. */
8714
+ function properCross(a, b, c, d) {
8715
+ const o = (p, q, r) => (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
8716
+ const d1 = o(c, d, a);
8717
+ const d2 = o(c, d, b);
8718
+ const d3 = o(a, b, c);
8719
+ const d4 = o(a, b, d);
8720
+ return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
8721
+ }
8722
+ /** This function gives the median of the smaller side of the boxes, the unit of the costs. */
8723
+ function unitOf(graph) {
8724
+ const sides = graph.nodes.map((n) => Math.min(n.box.w, n.box.h)).sort((a, b) => a - b);
8725
+ const u = sides.length === 0 ? 1 : sides[Math.floor((sides.length - 1) / 2)];
8726
+ return u > 0 ? u : 1;
8727
+ }
8728
+ /**
8729
+ * This function anneals the choice of one candidate way for each edge (RULE-021, after Davidson and Harel, 1996).
8730
+ * The candidates of an edge are its way in `shared` and its shortest way. Other candidates are the shortest ways when the links of earlier candidates cost 3 times more.
8731
+ * The energy is `(1 - 0.5) ink + 0.5 Σ L + crossing u X`: the cost of `shared` plus a cost for each crossing `X` between the ways of two edges.
8732
+ * A change takes one edge to another candidate. A worse change stays with the rational chance `1 / (1 + y + y^2 / 2)`, with `y = ΔE / T`.
8733
+ * The temperature falls by `cooling` after each sweep. The ways at the end are the ways of the lowest energy of the run.
8734
+ */
8735
+ function annealWays(ways, search, o) {
8736
+ const live = ways.filter((w) => w.found);
8737
+ const m = live.length;
8738
+ if (m < 2) return;
8739
+ const unit = unitOf(search.graph);
8740
+ const n = search.corners;
8741
+ let maxId = 0;
8742
+ for (const w of live) for (const i of w.ids) maxId = Math.max(maxId, i);
8743
+ const N = maxId + 1;
8744
+ const pairOf = (a, b) => a < b ? a * N + b : b * N + a;
8745
+ const make = (seq, ids) => {
8746
+ const links = [];
8747
+ const lengths = [];
8748
+ let length = 0;
8749
+ for (let i = 1; i < ids.length; i++) {
8750
+ const d = dist(seq[i - 1], seq[i]);
8751
+ links.push(pairOf(ids[i - 1], ids[i]));
8752
+ lengths.push(d);
8753
+ length += d;
8754
+ }
8755
+ return {
8756
+ seq,
8757
+ ids,
8758
+ links,
8759
+ lengths,
8760
+ length
8761
+ };
8762
+ };
8763
+ const k = Math.min(8, Math.max(1, Math.floor(o.candidates)));
8764
+ const cands = live.map((w) => {
8765
+ const list = [make(w.seq, w.ids)];
8766
+ const seen = /* @__PURE__ */ new Set([w.ids.join(",")]);
8767
+ const hit = /* @__PURE__ */ new Set();
8768
+ const mark = (c) => {
8769
+ for (let i = 1; i < c.ids.length; i++) if (c.ids[i - 1] < n && c.ids[i] < n) hit.add(search.link(c.ids[i - 1], c.ids[i]));
8770
+ };
8771
+ const add = (found) => {
8772
+ if (!found) return false;
8773
+ const key = found.ids.join(",");
8774
+ if (seen.has(key)) return false;
8775
+ seen.add(key);
8776
+ const c = make(found.seq, found.ids);
8777
+ list.push(c);
8778
+ mark(c);
8779
+ return true;
8780
+ };
8781
+ mark(list[0]);
8782
+ if (list.length < k) add(search.find(w));
8783
+ for (let tries = 0; list.length < k && tries < k; tries++) if (!add(search.find(w, (i, j, d) => hit.has(search.link(i, j)) ? d * PENALTY : d, 1))) break;
8784
+ return list;
8785
+ });
8786
+ const boundsOf = (seqs) => {
8787
+ let x0 = Infinity;
8788
+ let y0 = Infinity;
8789
+ let x1 = -Infinity;
8790
+ let y1 = -Infinity;
8791
+ for (const seq of seqs) for (const p of seq) {
8792
+ x0 = Math.min(x0, p.x);
8793
+ y0 = Math.min(y0, p.y);
8794
+ x1 = Math.max(x1, p.x);
8795
+ y1 = Math.max(y1, p.y);
8796
+ }
8797
+ return {
8798
+ x0,
8799
+ y0,
8800
+ x1,
8801
+ y1
8802
+ };
8803
+ };
8804
+ const apart = (A, B) => A.x1 < B.x0 || B.x1 < A.x0 || A.y1 < B.y0 || B.y1 < A.y0;
8805
+ const own = cands.map((list) => list.map((c) => boundsOf([c.seq])));
8806
+ const boxes = cands.map((list) => boundsOf(list.map((c) => c.seq)));
8807
+ const near = live.map(() => []);
8808
+ for (let e = 0; e < m; e++) for (let f = e + 1; f < m; f++) {
8809
+ if (apart(boxes[e], boxes[f])) continue;
8810
+ const ke = cands[e].length;
8811
+ const kf = cands[f].length;
8812
+ const table = new Int32Array(ke * kf);
8813
+ let any = false;
8814
+ for (let i = 0; i < ke; i++) {
8815
+ const p = cands[e][i].seq;
8816
+ for (let j = 0; j < kf; j++) {
8817
+ if (apart(own[e][i], own[f][j])) continue;
8818
+ const q = cands[f][j].seq;
8819
+ let x = 0;
8820
+ for (let s = 1; s < p.length; s++) for (let t = 1; t < q.length; t++) if (properCross(p[s - 1], p[s], q[t - 1], q[t])) x++;
8821
+ table[i * kf + j] = x;
8822
+ if (x > 0) any = true;
8823
+ }
8824
+ }
8825
+ if (!any) continue;
8826
+ near[e].push({
8827
+ f,
8828
+ table,
8829
+ mine: true
8830
+ });
8831
+ near[f].push({
8832
+ f: e,
8833
+ table,
8834
+ mine: false
8835
+ });
8836
+ }
8837
+ const keys = live.map((w) => w.key);
8838
+ const users = /* @__PURE__ */ new Map();
8839
+ const use = (e, c, step) => {
8840
+ let mine = users.get(keys[e]);
8841
+ if (!mine) users.set(keys[e], mine = /* @__PURE__ */ new Map());
8842
+ let ink = 0;
8843
+ for (let i = 0; i < c.links.length; i++) {
8844
+ const before = mine.get(c.links[i]) ?? 0;
8845
+ const after = before + step;
8846
+ if (after === 0) mine.delete(c.links[i]);
8847
+ else mine.set(c.links[i], after);
8848
+ if (before === 0 && after > 0) ink += c.lengths[i];
8849
+ if (before > 0 && after === 0) ink -= c.lengths[i];
8850
+ }
8851
+ return ink;
8852
+ };
8853
+ const choice = new Int32Array(m);
8854
+ for (let e = 0; e < m; e++) use(e, cands[e][0], 1);
8855
+ const crossCost = Math.max(0, o.crossing) * unit;
8856
+ const crossings = (e, i) => {
8857
+ let x = 0;
8858
+ for (const { f, table, mine } of near[e]) {
8859
+ const kf = cands[f].length;
8860
+ const ke = cands[e].length;
8861
+ x += mine ? table[i * kf + choice[f]] : table[choice[f] * ke + i];
8862
+ }
8863
+ return x;
8864
+ };
8865
+ const random = rng(Math.max(0, Math.floor(o.seed)));
8866
+ const cooling = Math.min(.99, Math.max(.9, o.cooling));
8867
+ let temperature = Math.max(1e-6, o.temperature) * unit;
8868
+ const moves = Math.max(1, Math.floor(o.budget)) * m;
8869
+ let energy = 0;
8870
+ let best = 0;
8871
+ let bestChoice = choice.slice();
8872
+ for (let s = 0; s < moves; s++) {
8873
+ if (s > 0 && s % m === 0) temperature *= cooling;
8874
+ const e = Math.floor(random() * m);
8875
+ const list = cands[e];
8876
+ if (list.length < 2) continue;
8877
+ const from = choice[e];
8878
+ const to = (from + 1 + Math.floor(random() * (list.length - 1))) % list.length;
8879
+ const dInk = use(e, list[from], -1) + use(e, list[to], 1);
8880
+ const dLength = list[to].length - list[from].length;
8881
+ const dCross = crossings(e, to) - crossings(e, from);
8882
+ const delta = .5 * dInk + DISCOUNT * dLength + crossCost * dCross;
8883
+ const y = delta / temperature;
8884
+ if (delta <= 0 || random() < 1 / (1 + y + y * y / 2)) {
8885
+ choice[e] = to;
8886
+ energy += delta;
8887
+ if (energy < best - 1e-9) {
8888
+ best = energy;
8889
+ bestChoice = choice.slice();
8890
+ }
8891
+ } else {
8892
+ use(e, list[to], -1);
8893
+ use(e, list[from], 1);
8894
+ }
8895
+ }
8896
+ live.forEach((w, e) => {
8897
+ const c = cands[e][bestChoice[e]];
8898
+ w.seq = c.seq;
8899
+ w.ids = c.ids;
8900
+ });
8901
+ }
8902
+ /**
8903
+ * This function makes the router `annealing` (RULE-021 with its rider WILD-028). It is the router `shared`, then the annealing of `annealWays`.
8904
+ * Each candidate is a way through the corner graph, as in `shared`, so each one keeps the hard rules.
8905
+ */
8906
+ function annealingRouter(options) {
8907
+ const o = {
8908
+ ...ANNEALING_DEFAULTS,
8909
+ ...options
8910
+ };
8911
+ return sharedRouter({
8912
+ name: o.name,
8913
+ ...o.rule === void 0 ? {} : { rule: o.rule },
8914
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes }
8915
+ }, void 0, { refine: (ways, search) => annealWays(ways, search, o) });
8916
+ }
8917
+ /**
8918
+ * The router strategy `annealing` (RULE-021), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
8919
+ * A screen module or a consumer adds it to a registry.
8920
+ */
8921
+ const annealingStrategy = defineStrategy({
8922
+ step: "router",
8923
+ name: "annealing",
8924
+ describe: "Simulated annealing over candidate routes (RULE-021, with WILD-028). Each edge picks one of a few ways, for less ink and fewer crossings.",
8925
+ defaults: { ...ANNEALING_DEFAULTS },
8926
+ schema: ANNEALING_SCHEMA,
8927
+ maxNodes: 1e4,
8928
+ run: (graph, o) => annealingRouter({
8929
+ ...o,
8930
+ name: "annealing",
8931
+ rule: byCollection
8932
+ }).route(graph)
8933
+ });
8934
+ // src/routers/tight/index.ts
8935
+ /** The values of `TightFirst`. */
8936
+ const TIGHT_FIRSTS = Object.freeze(["tight", "loose"]);
8937
+ /** The values of `TightTies`. */
8938
+ const TIGHT_TIES = Object.freeze(["id", "length"]);
8939
+ /** The default options of the router `tight`. */
8940
+ const TIGHT_DEFAULTS = Object.freeze({
8941
+ penalty: 3,
8942
+ first: "tight",
8943
+ ties: "length"
8944
+ });
8945
+ /** The declarations of the options of the router `tight` (WILD-023). */
8946
+ const TIGHT_SCHEMA = {
8947
+ penalty: {
8948
+ type: "number",
8949
+ min: 2,
8950
+ max: 10,
8951
+ step: .5,
8952
+ marks: [
8953
+ 2,
8954
+ 3,
8955
+ 5
8956
+ ],
8957
+ default: 3,
8958
+ describe: "The cost factor of the links of the first way in the search of the second way."
8959
+ },
8960
+ first: {
8961
+ type: "enum",
8962
+ values: ["tight", "loose"],
8963
+ default: "tight",
8964
+ describe: "The edges that go first: the edges with few good ways, or the edges with many good ways."
8965
+ },
8966
+ ties: {
8967
+ type: "enum",
8968
+ values: ["id", "length"],
8969
+ default: "length",
8970
+ describe: "The order of two edges with the same slack: by the edge id, or the longer edge first."
8971
+ }
8972
+ };
8973
+ /**
8974
+ * This function gives the slack of each way (WILD-023): `L2 / L1 - 1`. `L1` is the length of the shortest way.
8975
+ * `L2` is the cost of the shortest way when each link of the first way costs `penalty` times its length.
8976
+ * A way with no second way has the slack `penalty - 1`. A way with a second way of near-equal length has a slack near 0.
8977
+ */
8978
+ function waySlacks(ways, search, penalty) {
8979
+ const out = /* @__PURE__ */ new Map();
8980
+ const n = search.corners;
8981
+ for (const w of ways) {
8982
+ const first = search.find(w);
8983
+ if (!first) {
8984
+ out.set(w, 0);
8985
+ continue;
8986
+ }
8987
+ const l1 = polylineLength(first.seq);
8988
+ const own = /* @__PURE__ */ new Set();
8989
+ for (let i = 1; i < first.ids.length; i++) {
8990
+ const a = first.ids[i - 1];
8991
+ const b = first.ids[i];
8992
+ if (a < n && b < n) own.add(search.link(a, b));
8993
+ }
8994
+ const cost = (i, j, d) => own.has(search.link(i, j)) ? d * penalty : d;
8995
+ const second = search.find(w, cost, 1);
8996
+ let l2 = l1;
8997
+ if (second) {
8998
+ l2 = 0;
8999
+ for (let i = 1; i < second.ids.length; i++) {
9000
+ const a = second.ids[i - 1];
9001
+ const b = second.ids[i];
9002
+ const d = dist(second.seq[i - 1], second.seq[i]);
9003
+ l2 += a < n && b < n ? cost(a, b, d) : d;
9004
+ }
9005
+ }
9006
+ out.set(w, l1 > 1e-9 ? l2 / l1 - 1 : 0);
9007
+ }
9008
+ return out;
9009
+ }
9010
+ /**
9011
+ * This function makes the router `tight` (WILD-023). It is the router `shared` with another order of the edges of each key.
9012
+ * With `first: tight`, the edges with the largest slack value go first: their second way costs much more, so they have few good ways.
9013
+ * The edges with many ways of near-equal cost go last. The keys keep the order of `shared`.
9014
+ */
9015
+ function tightRouter(options) {
9016
+ const o = {
9017
+ ...TIGHT_DEFAULTS,
9018
+ ...options
9019
+ };
9020
+ const penalty = Math.min(10, Math.max(1, o.penalty));
9021
+ const sign = o.first === "loose" ? 1 : -1;
9022
+ return sharedRouter({
9023
+ name: o.name,
9024
+ ...o.rule === void 0 ? {} : { rule: o.rule },
9025
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes }
9026
+ }, void 0, { order: (ways, search) => {
9027
+ const slack = waySlacks(ways, search, penalty);
9028
+ const span = (w) => dist(w.ps.point, w.pt.point);
9029
+ return [...ways].sort((p, q) => byText(p.key, q.key) || sign * (slack.get(p) - slack.get(q)) || (o.ties === "length" ? span(q) - span(p) : 0) || byText(p.edge.id, q.edge.id));
9030
+ } });
9031
+ }
9032
+ /**
9033
+ * The router strategy `tight` (WILD-023), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
9034
+ * A screen module or a consumer adds it to a registry.
9035
+ */
9036
+ const tightStrategy = defineStrategy({
9037
+ step: "router",
9038
+ name: "tight",
9039
+ describe: "Tight edges first (WILD-023). The router `shared` routes first the edges whose second way costs much more than their first way.",
9040
+ defaults: { ...TIGHT_DEFAULTS },
9041
+ schema: TIGHT_SCHEMA,
9042
+ maxNodes: 1e4,
9043
+ run: (graph, o) => tightRouter({
9044
+ ...o,
9045
+ name: "tight",
9046
+ rule: byCollection
9047
+ }).route(graph)
9048
+ });
9049
+ // src/routers/attraction/index.ts
9050
+ /** The default options of the router `attraction`. */
9051
+ const ATTRACTION_DEFAULTS = Object.freeze({
9052
+ passes: 3,
9053
+ attraction: .5,
9054
+ capacityWeight: 1,
9055
+ laneWidth: 4
9056
+ });
9057
+ /** The declarations of the options of the router `attraction` (HWY-044). */
9058
+ const ATTRACTION_SCHEMA = {
9059
+ passes: {
9060
+ type: "integer",
9061
+ min: 1,
9062
+ max: 10,
9063
+ step: 1,
9064
+ marks: [
9065
+ 2,
9066
+ 3,
9067
+ 5
9068
+ ],
9069
+ default: 3,
9070
+ describe: "The number of passes. Each later pass routes each edge again from no ways."
9071
+ },
9072
+ attraction: {
9073
+ type: "number",
9074
+ min: 0,
9075
+ max: 1,
9076
+ step: .05,
9077
+ marks: [
9078
+ .25,
9079
+ .5,
9080
+ .75
9081
+ ],
9082
+ default: .5,
9083
+ describe: "The pull of the history of a link. A busy link of the passes before costs less."
9084
+ },
9085
+ capacityWeight: {
9086
+ type: "number",
9087
+ min: 0,
9088
+ max: 10,
9089
+ step: .5,
9090
+ marks: [
9091
+ 0,
9092
+ 1,
9093
+ 4
9094
+ ],
9095
+ default: 1,
9096
+ describe: "The cost weight of each edge over the capacity of a link."
9097
+ },
9098
+ laneWidth: {
9099
+ type: "number",
9100
+ min: 1,
9101
+ max: 12,
9102
+ step: 1,
9103
+ marks: [4, 6],
9104
+ default: 4,
9105
+ describe: "The width of one lane. The capacity of a link is its free width divided by this width."
9106
+ }
9107
+ };
9108
+ /**
9109
+ * This function makes the router `attraction` (HWY-044). It is the router `shared` with a negotiation over the passes.
9110
+ * After each pass, the history `h` of each link grows by `use / m`, with `m` the edges of the key. The history stops at `0.9 / a`.
9111
+ * In each pass, a link costs `len (1 - a h) (1 + b max(0, use - cap))` times the cost in `shared`.
9112
+ * The busy links of the passes before thus pull the edges, and a link over its capacity pushes them away.
9113
+ */
9114
+ function attractionRouter(options) {
8078
9115
  const o = {
8079
9116
  ...ATTRACTION_DEFAULTS,
8080
9117
  ...options
@@ -16779,41 +17816,352 @@ function interchangePlan(input, o) {
16779
17816
  });
16780
17817
  return tidyPlan({ highways });
16781
17818
  }
16782
- // src/bench/measures/bundles.ts
17819
+ // src/highways/knee/root.ts
16783
17820
  /**
16784
- * This function gives the share of the ink that shared segments save (B1 of MEAS0): `inkSaved = 1 - ink(S) / Σ_e L(r_e)`.
16785
- * `ink(S)` counts a shared segment one time, as `measureQuality` does. The scale is 0 to 1, and higher is better. The cost is `n log n`.
17821
+ * This function gives the root of the tree of one source node (NAT-018, NAT-019): the start point nearest the center of the targets.
17822
+ * It also gives the lead of each edge, the way from its own start point to the root. A lead is `undefined` when the corner graph has no way.
16786
17823
  */
16787
- function inkSaved(routes) {
16788
- let total = 0;
16789
- for (const r of routes) total += polylineLength(r.points);
16790
- if (total <= 0) return 0;
16791
- let ink = 0;
16792
- for (const [a, b] of drawnSegments(routes)) ink += dist(a, b);
16793
- return Math.max(0, 1 - ink / total);
17824
+ function rootOf(finder, starts, goals) {
17825
+ const c = {
17826
+ x: goals.reduce((s, p) => s + p.x, 0) / goals.length,
17827
+ y: goals.reduce((s, p) => s + p.y, 0) / goals.length
17828
+ };
17829
+ let root = starts[0];
17830
+ for (const s of starts) if (dist(s, c) < dist(root, c) - 1e-9) root = s;
17831
+ const leads = starts.map((s) => {
17832
+ if (dist(s, root) <= 1e-9) return [root];
17833
+ if (finder.obstacles.clearPath([s, root], finder.margin / 2)) return [s, root];
17834
+ const way = finder.corners.path(s, root);
17835
+ return way ? finder.corners.pull(way) : void 0;
17836
+ });
17837
+ return {
17838
+ root,
17839
+ leads
17840
+ };
17841
+ }
17842
+ // src/highways/knee/index.ts
17843
+ /** The values of `KneeRule`. */
17844
+ const KNEE_RULES = Object.freeze(["chord", "weights"]);
17845
+ /** The default options of the strategy `knee`. */
17846
+ const KNEE_DEFAULTS = Object.freeze({
17847
+ sweep: 11,
17848
+ rule: "chord",
17849
+ ink: .5,
17850
+ detour: 1.5,
17851
+ minSize: 2
17852
+ });
17853
+ /** This function gives the ink of a fan: the sum of the lengths of its links. */
17854
+ function fanInk(fan) {
17855
+ let sum = 0;
17856
+ for (const p of fan.points) if (p.parent >= 0) sum += dist(p.point, fan.points[p.parent].point);
17857
+ return sum;
16794
17858
  }
16795
17859
  /**
16796
- * This function measures the ambiguity of the routes (B4 of MEAS0), with one step of neighbours.
16797
- * `N(v)` holds the nodes whose routes share a drawn segment with a route of `v`.
16798
- * `amb = Σ_v count(w in N(v) : no edge v-w) / Σ_v count(N(v))`. The cost is quadratic, so a seeded sample serves above `exact` nodes.
17860
+ * This function adds a stub point at `stub` from the end of the last link of a way. The tree of NAT-017 joins a new branch only at an open point or on a link to one.
17861
+ * The last point of a way is not open, so without the stub no branch joins the last link.
16799
17862
  */
16800
- function ambiguity(graph, routes, options = {}) {
16801
- const n = graph.nodes.length;
16802
- const index = new Map(graph.nodes.map((v, i) => [v.id, i]));
16803
- const ends = /* @__PURE__ */ new Map();
16804
- for (const e of graph.edges) {
16805
- const s = index.get(e.source.node);
16806
- const t = index.get(e.target.node);
16807
- if (s !== void 0 && t !== void 0) ends.set(e.id, [s, t]);
17863
+ function withStub(way, stub) {
17864
+ if (way.length < 2) return way;
17865
+ const a = way[way.length - 2];
17866
+ const b = way[way.length - 1];
17867
+ const d = dist(a, b);
17868
+ if (d <= 2 * stub) return way;
17869
+ return [
17870
+ ...way.slice(0, -1),
17871
+ {
17872
+ x: b.x + (a.x - b.x) * stub / d,
17873
+ y: b.y + (a.y - b.y) * stub / d
17874
+ },
17875
+ b
17876
+ ];
17877
+ }
17878
+ /**
17879
+ * This function gives the index of the tree that the rule picks on the front of the trees (NAT-018).
17880
+ * The front holds the trees that no other tree beats on the ink and on the path lengths. The two values go to the range from 0 to 1.
17881
+ * With `chord`, the pick is the tree of the front farthest from the line between the two ends of the front.
17882
+ * With `weights`, the pick is the tree with the least `ink × L + (1 - ink) × S`. A tie goes to the earlier tree.
17883
+ */
17884
+ function kneeOf(trees, rule, ink) {
17885
+ if (trees.length === 0) return -1;
17886
+ const front = trees.map((_, i) => i).filter((i) => !trees.some((q, j) => j !== i && q.ink <= trees[i].ink && q.paths <= trees[i].paths && (q.ink < trees[i].ink || q.paths < trees[i].paths)));
17887
+ const lo = {
17888
+ x: Math.min(...front.map((i) => trees[i].ink)),
17889
+ y: Math.min(...front.map((i) => trees[i].paths))
17890
+ };
17891
+ const hi = {
17892
+ x: Math.max(...front.map((i) => trees[i].ink)),
17893
+ y: Math.max(...front.map((i) => trees[i].paths))
17894
+ };
17895
+ const norm = (i) => ({
17896
+ x: hi.x - lo.x > 1e-9 ? (trees[i].ink - lo.x) / (hi.x - lo.x) : 0,
17897
+ y: hi.y - lo.y > 1e-9 ? (trees[i].paths - lo.y) / (hi.y - lo.y) : 0
17898
+ });
17899
+ let best = front[0];
17900
+ let score = rule === "chord" ? -Infinity : Infinity;
17901
+ if (rule === "weights") {
17902
+ const w = Math.min(1, Math.max(0, ink));
17903
+ for (const i of front) {
17904
+ const p = norm(i);
17905
+ const s = w * p.x + (1 - w) * p.y;
17906
+ if (s < score - 1e-12) [best, score] = [i, s];
17907
+ }
17908
+ return best;
16808
17909
  }
16809
- const keyIds = /* @__PURE__ */ new Map();
16810
- const nodesOf = [];
16811
- const keysOf = /* @__PURE__ */ new Map();
16812
- for (const r of routes) {
16813
- const end = ends.get(r.edge);
16814
- if (!end) continue;
16815
- const own = [];
16816
- for (let i = 1; i < r.points.length; i++) {
17910
+ const a = front.reduce((u, i) => trees[i].ink < trees[u].ink || trees[i].ink === trees[u].ink && trees[i].paths < trees[u].paths ? i : u);
17911
+ const b = front.reduce((u, i) => trees[i].paths < trees[u].paths || trees[i].paths === trees[u].paths && trees[i].ink < trees[u].ink ? i : u);
17912
+ const A = norm(a);
17913
+ const B = norm(b);
17914
+ const len = dist(A, B);
17915
+ if (len <= 1e-12) return a;
17916
+ for (const i of front) {
17917
+ const p = norm(i);
17918
+ const s = Math.abs((B.x - A.x) * (A.y - p.y) - (A.x - p.x) * (B.y - A.y)) / len;
17919
+ if (s > score + 1e-12) [best, score] = [i, s];
17920
+ }
17921
+ return best;
17922
+ }
17923
+ /**
17924
+ * This function gives the highways of the knee trees (NAT-018). The edges from one source node get one tree. Its root is the out point of the plug nearest the center of the targets.
17925
+ * A sweep of the balancing factor of NAT-017 gives trees from the least ink to the most direct paths.
17926
+ * The tree at the knee of the front of ink and path lengths is the pick. Each edge takes its path in the tree, when that path is short enough.
17927
+ * The stretches of the tree that two edges or more share become highways.
17928
+ */
17929
+ function kneePlan(input, o) {
17930
+ const finder = new WayFinder(input.graph.nodes);
17931
+ const first = firstRoutes(input);
17932
+ const groups = /* @__PURE__ */ new Map();
17933
+ for (const p of edgePlugs(input.graph)) {
17934
+ const s = finder.out(p.ps);
17935
+ const t = finder.out(p.pt);
17936
+ const route = first.get(p.edge.id);
17937
+ const group = p.edge.source.node;
17938
+ const list = groups.get(group) ?? [];
17939
+ list.push({
17940
+ edge: p.edge.id,
17941
+ key: p.edge.source.node,
17942
+ s,
17943
+ t,
17944
+ base: route ? polylineLength(route) : dist(s, t)
17945
+ });
17946
+ groups.set(group, list);
17947
+ }
17948
+ const sweep = Math.min(21, Math.max(2, Math.floor(o.sweep)));
17949
+ const ways = [];
17950
+ for (const group of [...groups.keys()].sort()) {
17951
+ const list = groups.get(group);
17952
+ if (list.length < 2) continue;
17953
+ const goals = list.map((e) => e.t);
17954
+ const { root, leads } = rootOf(finder, list.map((e) => e.s), goals);
17955
+ const lost = /* @__PURE__ */ new Set();
17956
+ const branch = (from, t) => {
17957
+ const way = finder.corners.path(from, goals[t]);
17958
+ if (way) return withStub(finder.corners.pull(way), finder.margin);
17959
+ lost.add(t);
17960
+ return [from, goals[t]];
17961
+ };
17962
+ const trees = [];
17963
+ for (let k = 0; k < sweep; k++) {
17964
+ const a = (k + 1) / sweep;
17965
+ const bf = (1 - a) / a;
17966
+ const fan = balancedFan(root, goals, bf, branch);
17967
+ let paths = 0;
17968
+ for (const leaf of fan.leaf) paths += fan.points[leaf].pl;
17969
+ trees.push({
17970
+ bf,
17971
+ ink: fanInk(fan),
17972
+ paths,
17973
+ fan
17974
+ });
17975
+ }
17976
+ const pick = trees[kneeOf(trees, o.rule, o.ink)];
17977
+ list.forEach((e, t) => {
17978
+ const lead = leads[t];
17979
+ if (lost.has(t) || !lead) return;
17980
+ const points = [...lead, ...fanPath(pick.fan, pick.fan.leaf[t]).slice(1)];
17981
+ if (points.length < 2 || polylineLength(points) > o.detour * e.base) return;
17982
+ ways.push({
17983
+ edge: e.edge,
17984
+ key: e.key,
17985
+ points
17986
+ });
17987
+ });
17988
+ }
17989
+ return sharedStretches(ways, { minSize: Math.max(2, Math.floor(o.minSize)) });
17990
+ }
17991
+ // src/highways/bronchial/index.ts
17992
+ /** The values of `BronchialSplit`. */
17993
+ const BRONCHIAL_SPLITS = Object.freeze([
17994
+ "count",
17995
+ "weight",
17996
+ "gap"
17997
+ ]);
17998
+ /** The default options of the strategy `bronchial`. */
17999
+ const BRONCHIAL_DEFAULTS = Object.freeze({
18000
+ split: "count",
18001
+ share: .5,
18002
+ smallest: 1,
18003
+ detour: 1.5,
18004
+ minSize: 2
18005
+ });
18006
+ /**
18007
+ * This function gives the diamond angle of the direction `(dx, dy)`: a value from 0 to 4 that grows with the angle, with no trigonometry.
18008
+ * The value 0 is the direction of the positive x axis.
18009
+ */
18010
+ function diamondAngle(dx, dy) {
18011
+ const s = Math.abs(dx) + Math.abs(dy);
18012
+ if (s <= 1e-12) return 0;
18013
+ if (dy >= 0) return dx >= 0 ? dy / s : 1 - dx / s;
18014
+ return dx < 0 ? 2 - dy / s : 3 + dx / s;
18015
+ }
18016
+ /**
18017
+ * This function splits the targets of a tip in two halves (NAT-019). The targets go in the order of their angles around the tip.
18018
+ * The order starts after the widest gap of the angles, so each half is a sector. The rule of `split` then cuts the order in two.
18019
+ */
18020
+ function splitTargets(tip, targets, ids, split) {
18021
+ const sorted = [...ids].sort((a, b) => diamondAngle(targets[a].x - tip.x, targets[a].y - tip.y) - diamondAngle(targets[b].x - tip.x, targets[b].y - tip.y) || a - b);
18022
+ const angle = (i) => diamondAngle(targets[sorted[i]].x - tip.x, targets[sorted[i]].y - tip.y);
18023
+ const n = sorted.length;
18024
+ const gaps = sorted.map((_, i) => i === n - 1 ? angle(0) + 4 - angle(i) : angle(i + 1) - angle(i));
18025
+ let widest = 0;
18026
+ for (let i = 1; i < n; i++) if (gaps[i] > gaps[widest] + 1e-12) widest = i;
18027
+ const order = [...sorted.slice(widest + 1), ...sorted.slice(0, widest + 1)];
18028
+ let cut = Math.floor(n / 2);
18029
+ if (split === "weight") {
18030
+ const w = order.map((i) => dist(tip, targets[i]));
18031
+ const total = w.reduce((s, x) => s + x, 0);
18032
+ let run = 0;
18033
+ let best = Infinity;
18034
+ for (let c = 1; c < n; c++) {
18035
+ run += w[c - 1];
18036
+ const off = Math.abs(2 * run - total);
18037
+ if (off < best - 1e-9) [best, cut] = [off, c];
18038
+ }
18039
+ } else if (split === "gap") {
18040
+ let best = -Infinity;
18041
+ const at = (k) => diamondAngle(targets[order[k]].x - tip.x, targets[order[k]].y - tip.y);
18042
+ for (let c = 1; c < n; c++) {
18043
+ const g = (at(c) - at(c - 1) + 4) % 4;
18044
+ if (g > best + 1e-12) [best, cut] = [g, c];
18045
+ }
18046
+ }
18047
+ return [order.slice(0, cut), order.slice(cut)];
18048
+ }
18049
+ /**
18050
+ * This function gives the highways of the bronchial trees (NAT-019, after Kitaoka and others, 1999). The edges from one source node get one tree.
18051
+ * The root of a tree is the out point of the plug nearest the center of the targets.
18052
+ * From the tip, the tree grows a share of the way to the center of its targets. Then it splits the targets in two and grows a branch for each half.
18053
+ * A branch that hits a box grows half as far, or not at all. A small set of targets gets its own ways from the tip, through the corner graph.
18054
+ * Each edge takes its path in the tree when that path is short enough, and the stretches that two edges or more share become highways.
18055
+ */
18056
+ function bronchialPlan(input, o) {
18057
+ const finder = new WayFinder(input.graph.nodes);
18058
+ const pad = finder.margin / 2;
18059
+ const first = firstRoutes(input);
18060
+ const groups = /* @__PURE__ */ new Map();
18061
+ for (const p of edgePlugs(input.graph)) {
18062
+ const s = finder.out(p.ps);
18063
+ const t = finder.out(p.pt);
18064
+ const route = first.get(p.edge.id);
18065
+ const group = p.edge.source.node;
18066
+ const list = groups.get(group) ?? [];
18067
+ list.push({
18068
+ edge: p.edge.id,
18069
+ key: p.edge.source.node,
18070
+ s,
18071
+ t,
18072
+ base: route ? polylineLength(route) : dist(s, t)
18073
+ });
18074
+ groups.set(group, list);
18075
+ }
18076
+ const share = Math.min(.7, Math.max(.3, o.share));
18077
+ const smallest = Math.min(3, Math.max(1, Math.floor(o.smallest)));
18078
+ const ways = [];
18079
+ for (const group of [...groups.keys()].sort()) {
18080
+ const list = groups.get(group);
18081
+ if (list.length < 2) continue;
18082
+ const goals = list.map((e) => e.t);
18083
+ const { root, leads } = rootOf(finder, list.map((e) => e.s), goals);
18084
+ const paths = list.map(() => void 0);
18085
+ const grow = (trunk, ids) => {
18086
+ const tip = trunk[trunk.length - 1];
18087
+ if (ids.length <= smallest) {
18088
+ for (const i of ids) {
18089
+ const way = finder.corners.path(tip, goals[i]);
18090
+ if (way) paths[i] = [...trunk, ...finder.corners.pull(way).slice(1)];
18091
+ }
18092
+ return;
18093
+ }
18094
+ const c = {
18095
+ x: ids.reduce((s, i) => s + goals[i].x, 0) / ids.length,
18096
+ y: ids.reduce((s, i) => s + goals[i].y, 0) / ids.length
18097
+ };
18098
+ let next = trunk;
18099
+ for (let f = share; f > share / 8; f /= 2) {
18100
+ const p = {
18101
+ x: tip.x + f * (c.x - tip.x),
18102
+ y: tip.y + f * (c.y - tip.y)
18103
+ };
18104
+ if (dist(p, tip) <= 1e-6) break;
18105
+ if (!finder.obstacles.inside(p, pad) && finder.obstacles.clearPath([tip, p], pad)) {
18106
+ next = [...trunk, p];
18107
+ break;
18108
+ }
18109
+ }
18110
+ const [a, b] = splitTargets(next[next.length - 1], goals, ids, o.split);
18111
+ grow(next, a);
18112
+ grow(next, b);
18113
+ };
18114
+ grow([root], list.map((_, i) => i));
18115
+ list.forEach((e, i) => {
18116
+ const tail = paths[i];
18117
+ const lead = leads[i];
18118
+ if (!tail || !lead) return;
18119
+ const points = [...lead, ...tail.slice(1)];
18120
+ if (points.length < 2 || polylineLength(points) > o.detour * e.base) return;
18121
+ ways.push({
18122
+ edge: e.edge,
18123
+ key: e.key,
18124
+ points
18125
+ });
18126
+ });
18127
+ }
18128
+ return sharedStretches(ways, { minSize: Math.max(2, Math.floor(o.minSize)) });
18129
+ }
18130
+ // src/bench/measures/bundles.ts
18131
+ /**
18132
+ * This function gives the share of the ink that shared segments save (B1 of MEAS0): `inkSaved = 1 - ink(S) / Σ_e L(r_e)`.
18133
+ * `ink(S)` counts a shared segment one time, as `measureQuality` does. The scale is 0 to 1, and higher is better. The cost is `n log n`.
18134
+ */
18135
+ function inkSaved(routes) {
18136
+ let total = 0;
18137
+ for (const r of routes) total += polylineLength(r.points);
18138
+ if (total <= 0) return 0;
18139
+ let ink = 0;
18140
+ for (const [a, b] of drawnSegments(routes)) ink += dist(a, b);
18141
+ return Math.max(0, 1 - ink / total);
18142
+ }
18143
+ /**
18144
+ * This function measures the ambiguity of the routes (B4 of MEAS0), with one step of neighbours.
18145
+ * `N(v)` holds the nodes whose routes share a drawn segment with a route of `v`.
18146
+ * `amb = Σ_v count(w in N(v) : no edge v-w) / Σ_v count(N(v))`. The cost is quadratic, so a seeded sample serves above `exact` nodes.
18147
+ */
18148
+ function ambiguity(graph, routes, options = {}) {
18149
+ const n = graph.nodes.length;
18150
+ const index = new Map(graph.nodes.map((v, i) => [v.id, i]));
18151
+ const ends = /* @__PURE__ */ new Map();
18152
+ for (const e of graph.edges) {
18153
+ const s = index.get(e.source.node);
18154
+ const t = index.get(e.target.node);
18155
+ if (s !== void 0 && t !== void 0) ends.set(e.id, [s, t]);
18156
+ }
18157
+ const keyIds = /* @__PURE__ */ new Map();
18158
+ const nodesOf = [];
18159
+ const keysOf = /* @__PURE__ */ new Map();
18160
+ for (const r of routes) {
18161
+ const end = ends.get(r.edge);
18162
+ if (!end) continue;
18163
+ const own = [];
18164
+ for (let i = 1; i < r.points.length; i++) {
16817
18165
  const k = segmentKey(r.points[i - 1], r.points[i]);
16818
18166
  if (k === void 0) continue;
16819
18167
  let id = keyIds.get(k);
@@ -19641,6 +20989,243 @@ function zipperPlan(input, o) {
19641
20989
  }
19642
20990
  return tidyPlan(plan);
19643
20991
  }
20992
+ // src/levels/venation/index.ts
20993
+ /** The values of `VenationBackbone`. */
20994
+ const VENATION_BACKBONES = Object.freeze(["rng", "gabriel"]);
20995
+ /** The default options of the level strategy `venation`. */
20996
+ const VENATION_DEFAULTS = Object.freeze({
20997
+ backbone: "rng",
20998
+ discount: .4,
20999
+ detour: 1.5,
21000
+ maxItems: 400
21001
+ });
21002
+ /**
21003
+ * This function gives the links of the backbone of the points, as pairs of indices `i < j` (NAT-016).
21004
+ * With `rng`, the link `p q` exists when no third point `r` has `max(d(p, r), d(q, r)) < d(p, q)`.
21005
+ * With `gabriel`, the link exists when no third point is inside the circle with the diameter `p q`.
21006
+ */
21007
+ function backboneLinks(points, backbone) {
21008
+ const out = [];
21009
+ const n = points.length;
21010
+ const d2 = (a, b) => (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
21011
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
21012
+ const pq = d2(points[i], points[j]);
21013
+ let keep = true;
21014
+ for (let r = 0; r < n && keep; r++) {
21015
+ if (r === i || r === j) continue;
21016
+ const pr = d2(points[i], points[r]);
21017
+ const qr = d2(points[j], points[r]);
21018
+ if (backbone === "rng" ? Math.max(pr, qr) < pq - 1e-9 : pr + qr < pq - 1e-9) keep = false;
21019
+ }
21020
+ if (keep) out.push([i, j]);
21021
+ }
21022
+ return out;
21023
+ }
21024
+ /**
21025
+ * This function gives a way around the box `b`, inflated by `m`, from the point `a` to the point `c` of its boundary.
21026
+ * The way goes along the shorter side of the inflated box. It holds the two points at `m` out from `a` and `c`, and the corners between them.
21027
+ */
21028
+ function aroundBox(b, m, a, c) {
21029
+ const x0 = b.x - m;
21030
+ const y0 = b.y - m;
21031
+ const x1 = b.x + b.w + m;
21032
+ const y1 = b.y + b.h + m;
21033
+ const w = x1 - x0;
21034
+ const h = y1 - y0;
21035
+ const per = 2 * (w + h);
21036
+ const onBoundary = (p) => {
21037
+ const cx = Math.min(x1, Math.max(x0, p.x));
21038
+ const cy = Math.min(y1, Math.max(y0, p.y));
21039
+ const dl = cx - x0;
21040
+ const dr = x1 - cx;
21041
+ const dt = cy - y0;
21042
+ const db = y1 - cy;
21043
+ const least = Math.min(dl, dr, dt, db);
21044
+ if (least === dt) return {
21045
+ p: {
21046
+ x: cx,
21047
+ y: y0
21048
+ },
21049
+ t: cx - x0
21050
+ };
21051
+ if (least === dr) return {
21052
+ p: {
21053
+ x: x1,
21054
+ y: cy
21055
+ },
21056
+ t: w + (cy - y0)
21057
+ };
21058
+ if (least === db) return {
21059
+ p: {
21060
+ x: cx,
21061
+ y: y1
21062
+ },
21063
+ t: w + h + (x1 - cx)
21064
+ };
21065
+ return {
21066
+ p: {
21067
+ x: x0,
21068
+ y: cy
21069
+ },
21070
+ t: 2 * w + h + (y1 - cy)
21071
+ };
21072
+ };
21073
+ const corners = [
21074
+ {
21075
+ p: {
21076
+ x: x0,
21077
+ y: y0
21078
+ },
21079
+ t: 0
21080
+ },
21081
+ {
21082
+ p: {
21083
+ x: x1,
21084
+ y: y0
21085
+ },
21086
+ t: w
21087
+ },
21088
+ {
21089
+ p: {
21090
+ x: x1,
21091
+ y: y1
21092
+ },
21093
+ t: w + h
21094
+ },
21095
+ {
21096
+ p: {
21097
+ x: x0,
21098
+ y: y1
21099
+ },
21100
+ t: 2 * w + h
21101
+ }
21102
+ ];
21103
+ const A = onBoundary(a);
21104
+ const C = onBoundary(c);
21105
+ const ahead = (C.t - A.t + per) % per;
21106
+ const out = [A.p];
21107
+ if (ahead <= per / 2) {
21108
+ const list = corners.map((k) => ({
21109
+ ...k,
21110
+ s: (k.t - A.t + per) % per
21111
+ })).filter((k) => k.s > 1e-9 && k.s < ahead - 1e-9);
21112
+ for (const k of list.sort((p, q) => p.s - q.s)) out.push(k.p);
21113
+ } else {
21114
+ const back = per - ahead;
21115
+ const list = corners.map((k) => ({
21116
+ ...k,
21117
+ s: (A.t - k.t + per) % per
21118
+ })).filter((k) => k.s > 1e-9 && k.s < back - 1e-9);
21119
+ for (const k of list.sort((p, q) => p.s - q.s)) out.push(k.p);
21120
+ }
21121
+ out.push(C.p);
21122
+ return out.filter((p, i) => i === 0 || dist(p, out[i - 1]) > 1e-9);
21123
+ }
21124
+ /**
21125
+ * This function gives the links of one level along a closed venation backbone (NAT-016, after Runions and others, 2005).
21126
+ * The backbone links the items by their relative neighbourhood graph or their Gabriel graph. Each backbone link goes around the other items.
21127
+ * The level links go in the order of their counts, the largest first. Each one takes the cheapest way on the backbone, with the cost `L (1 - discount)` for a used backbone link.
21128
+ * The way goes around each item between its two ends. A way longer than `detour` times the direct link, or a direct backbone link, keeps the direct link.
21129
+ */
21130
+ function venationLinks(items, direct, o) {
21131
+ if (direct.length < 2 || items.length < 3 || items.length > o.maxItems) return [...direct];
21132
+ const index = new Map(items.map((it, i) => [it.id, i]));
21133
+ const links = backboneLinks(items.map((it) => center(it.box)), o.backbone);
21134
+ const routed = levelLinks(items, links.map(([i, j]) => ({
21135
+ source: items[i].id,
21136
+ target: items[j].id,
21137
+ count: 1
21138
+ })), o);
21139
+ const n = items.length;
21140
+ const next = Array.from({ length: n }, () => []);
21141
+ const cost = routed.map((l) => polylineLength(l.points));
21142
+ routed.forEach((l, k) => {
21143
+ const i = index.get(l.source);
21144
+ const j = index.get(l.target);
21145
+ next[i].push({
21146
+ to: j,
21147
+ link: k
21148
+ });
21149
+ next[j].push({
21150
+ to: i,
21151
+ link: k
21152
+ });
21153
+ });
21154
+ const direct2 = new Set(links.map(([i, j]) => `${i},${j}`));
21155
+ const used = new Uint8Array(routed.length);
21156
+ const share = 1 - Math.min(.8, Math.max(0, o.discount));
21157
+ const order = direct.map((_, k) => k).sort((p, q) => direct[q].count - direct[p].count || direct[p].source.localeCompare(direct[q].source) || direct[p].target.localeCompare(direct[q].target));
21158
+ const margin = o.margin ?? 5;
21159
+ const out = [...direct];
21160
+ for (const k of order) {
21161
+ const l = direct[k];
21162
+ const s = index.get(l.source);
21163
+ const t = index.get(l.target);
21164
+ if (s === void 0 || t === void 0) continue;
21165
+ if (direct2.has(`${Math.min(s, t)},${Math.max(s, t)}`)) {
21166
+ const at = next[s].find((x) => x.to === t);
21167
+ if (at) used[at.link] = 1;
21168
+ continue;
21169
+ }
21170
+ const best = new Float64Array(n).fill(Infinity);
21171
+ const from = new Int32Array(n).fill(-1);
21172
+ const via = new Int32Array(n).fill(-1);
21173
+ const done = new Uint8Array(n);
21174
+ best[s] = 0;
21175
+ for (;;) {
21176
+ let u = -1;
21177
+ for (let i = 0; i < n; i++) if (!done[i] && best[i] < Infinity && (u < 0 || best[i] < best[u])) u = i;
21178
+ if (u < 0 || u === t) break;
21179
+ done[u] = 1;
21180
+ for (const x of next[u]) {
21181
+ const c = best[u] + cost[x.link] * (used[x.link] ? share : 1);
21182
+ if (c < best[x.to] - 1e-9) {
21183
+ best[x.to] = c;
21184
+ from[x.to] = u;
21185
+ via[x.to] = x.link;
21186
+ }
21187
+ }
21188
+ }
21189
+ if (from[t] < 0) continue;
21190
+ const hops = [];
21191
+ for (let v = t; v !== s; v = from[v]) hops.push({
21192
+ link: via[v],
21193
+ forward: routed[via[v]].target === items[v].id
21194
+ });
21195
+ hops.reverse();
21196
+ const points = [];
21197
+ hops.forEach((h, i) => {
21198
+ const p = routed[h.link].points;
21199
+ const seq = h.forward ? [...p] : [...p].reverse();
21200
+ if (i > 0) {
21201
+ const item = items[index.get(h.forward ? routed[h.link].source : routed[h.link].target)];
21202
+ const around = aroundBox(item.box, margin, points[points.length - 1], seq[0]);
21203
+ if (points.length > 1) points.pop();
21204
+ points.push(...around);
21205
+ seq.shift();
21206
+ }
21207
+ for (const q of seq) if (points.length === 0 || dist(q, points[points.length - 1]) > 1e-9) points.push(q);
21208
+ });
21209
+ if (points.length < 2 || polylineLength(points) > o.detour * polylineLength(l.points)) continue;
21210
+ for (const h of hops) used[h.link] = 1;
21211
+ out[k] = {
21212
+ ...l,
21213
+ points
21214
+ };
21215
+ }
21216
+ return out;
21217
+ }
21218
+ /**
21219
+ * This function gives the zoom levels of the level strategy `venation` (NAT-016). The levels and their items are the levels of `depths`.
21220
+ * In each level with groups, the links take their ways along a closed venation backbone (`venationLinks`). The last level stays as in `depths`.
21221
+ */
21222
+ function venationLevels(input, options) {
21223
+ const levels = depthLevels(input, options);
21224
+ return levels.map((level, i) => i === levels.length - 1 ? level : {
21225
+ ...level,
21226
+ links: venationLinks(level.items, level.links, options)
21227
+ });
21228
+ }
19644
21229
  // src/strategies/options.ts
19645
21230
  /** The number of values of a sweep on a range when the call and the declaration give no number. */
19646
21231
  const SWEEP_COUNT = 5;
@@ -19906,77 +21491,347 @@ function runPipeline(graph, options) {
19906
21491
  result: asRouter(options.router, routerOptions).route(board)
19907
21492
  };
19908
21493
  }
19909
- // src/strategies/schemas.ts
19910
- /** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
19911
- const WAY_SCHEMA = {
19912
- margin: {
19913
- type: "number",
19914
- min: 1,
19915
- max: 20,
19916
- step: 1,
19917
- default: 5,
19918
- describe: "The distance from a box to the corners of the graph."
19919
- },
19920
- k: {
19921
- type: "integer",
19922
- min: 2,
19923
- max: 16,
19924
- step: 1,
19925
- default: 8,
19926
- describe: "The number of visible neighbours of each corner."
19927
- },
19928
- smooth: {
19929
- type: "boolean",
19930
- default: false,
19931
- describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners."
19932
- }
19933
- };
19934
- /** The declarations of the options of the stretch order (LANE-STRETCH): the method and the passes of `sifting`. A generator can list each value. */
19935
- const LANE_STRETCH_SCHEMA = {
19936
- method: {
19937
- type: "enum",
19938
- values: [
19939
- "none",
19940
- "consensus",
19941
- "periphery",
19942
- "sifting"
19943
- ],
19944
- default: "sifting",
19945
- describe: "The order of each stretch: the order of each run alone, the order of the pair relation, the order of the largest run, or the order of the pair relation after sifting."
19946
- },
19947
- passes: {
19948
- type: "integer",
19949
- min: 1,
19950
- max: 4,
19951
- step: 1,
19952
- default: 2,
19953
- describe: "The number of passes of the method `sifting`. In each pass, each edge moves to its best place."
19954
- }
19955
- };
19956
- /**
19957
- * The declaration of the grouping rule of the edges: a name of `GROUP_RULE_NAMES`. A new name of the list needs no change here.
19958
- * The option also takes a rule of the caller (`custom`), so the old value form stays valid.
19959
- * The `defaults` of a strategy keep the rule `byCollection`, and the declaration gives its name `collection` as its `default`.
19960
- */
19961
- const EDGE_RULE = {
19962
- type: "enum",
19963
- values: GROUP_RULE_NAMES,
19964
- custom: "function",
19965
- describe: "The grouping rule."
19966
- };
19967
- /** The declaration of the lane order of the shared ways of the routers `trunks` and `shared`. */
19968
- const LANE_ORDER = {
19969
- type: "enum",
19970
- values: ["bundle", "run"],
19971
- default: "bundle",
19972
- describe: "With `bundle`, two edges keep one lane order on each shared way of their common stretch. With `run`, each shared way gets its order alone."
19973
- };
19974
- /** The declaration of the lane order of the runs of the router `sections`. The default keeps the lane order of version 0.2.0. */
19975
- const RUN_LANE_ORDER = {
19976
- ...LANE_ORDER,
19977
- default: "run"
19978
- };
19979
- /** The declarations of the options of the routers `corners` and `corners-smooth`. */
21494
+ // src/bench/quality.ts
21495
+ /** The clearance stops at this distance. */
21496
+ const CLEARANCE_CAP = 40;
21497
+ const TIGHT = 4;
21498
+ const EXACT_CROSSINGS = 2e4;
21499
+ const SAMPLE = 2e3;
21500
+ const key = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
21501
+ /** This function measures the quality of a result. */
21502
+ function measureQuality(graph, result) {
21503
+ const byId = nodeIndex(graph);
21504
+ const nodes = new Grid(180);
21505
+ for (const n of graph.nodes) nodes.add(n, n.box);
21506
+ const segs = [];
21507
+ const seen = /* @__PURE__ */ new Set();
21508
+ let bends = 0;
21509
+ let infl = 0;
21510
+ let clear = 0;
21511
+ let tight = 0;
21512
+ let detour = 0;
21513
+ let detourN = 0;
21514
+ let angle = 0;
21515
+ let angleN = 0;
21516
+ for (const r of result.routes) {
21517
+ const p = r.points;
21518
+ for (let i = 1; i < p.length; i++) {
21519
+ const ka = key(p[i - 1]);
21520
+ const kb = key(p[i]);
21521
+ if (ka === kb) continue;
21522
+ const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
21523
+ if (seen.has(k)) continue;
21524
+ seen.add(k);
21525
+ segs.push([p[i - 1], p[i]]);
21526
+ }
21527
+ bends += bendCount(p);
21528
+ infl += inflectionCount(p);
21529
+ const ends = endNodes(graph, byId, r.edge);
21530
+ let c = 40;
21531
+ for (let i = 1; i < p.length; i++) {
21532
+ const a = p[i - 1];
21533
+ const b = p[i];
21534
+ nodes.querySegment(a, b, c, (n) => {
21535
+ if (n !== ends.source && n !== ends.target) c = Math.min(c, segmentBoxDistance(a, b, n.box));
21536
+ });
21537
+ }
21538
+ clear += c;
21539
+ if (c < TIGHT) tight++;
21540
+ if (p.length >= 2) {
21541
+ const d = dist(p[0], p[p.length - 1]);
21542
+ if (d > 1) {
21543
+ detour += polylineLength(p) / d;
21544
+ detourN++;
21545
+ }
21546
+ for (const [at, next, node] of [[
21547
+ p[0],
21548
+ p[1],
21549
+ ends.source
21550
+ ], [
21551
+ p[p.length - 1],
21552
+ p[p.length - 2],
21553
+ ends.target
21554
+ ]]) {
21555
+ if (!node) continue;
21556
+ const a = sideAngle(node.box, at, next);
21557
+ if (a !== void 0) {
21558
+ angle += a;
21559
+ angleN++;
21560
+ }
21561
+ }
21562
+ }
21563
+ }
21564
+ const n = Math.max(1, result.routes.length);
21565
+ let ink = 0;
21566
+ for (const [a, b] of segs) ink += dist(a, b);
21567
+ const [crossings, crossingsEstimated] = countCrossings(segs);
21568
+ return {
21569
+ ink,
21570
+ crossings,
21571
+ crossingsEstimated,
21572
+ bends: bends / n,
21573
+ inflections: infl / n,
21574
+ clearance: clear / n,
21575
+ tight: tight / n,
21576
+ detour: detourN ? detour / detourN : 1,
21577
+ plugAngle: angleN ? angle / angleN : 0
21578
+ };
21579
+ }
21580
+ function endNodes(graph, byId, edgeId) {
21581
+ const e = edgeIndex(graph).get(edgeId);
21582
+ return {
21583
+ source: e && byId.get(e.source.node),
21584
+ target: e && byId.get(e.target.node)
21585
+ };
21586
+ }
21587
+ const edgeIndexes = /* @__PURE__ */ new WeakMap();
21588
+ function edgeIndex(graph) {
21589
+ let m = edgeIndexes.get(graph);
21590
+ if (!m) {
21591
+ m = /* @__PURE__ */ new Map();
21592
+ for (const e of graph.edges) m.set(e.id, e);
21593
+ edgeIndexes.set(graph, m);
21594
+ }
21595
+ return m;
21596
+ }
21597
+ /**
21598
+ * This function gives an angle in degrees: the angle between the segment from `at` to `next` and the outward normal.
21599
+ * The normal is the normal of the side of the box nearest to `at`. A route that starts inside the box (an interior route) gives no angle.
21600
+ */
21601
+ function sideAngle(box, at, next) {
21602
+ const d = [
21603
+ [
21604
+ Math.abs(at.y - box.y),
21605
+ 0,
21606
+ -1
21607
+ ],
21608
+ [
21609
+ Math.abs(at.x - (box.x + box.w)),
21610
+ 1,
21611
+ 0
21612
+ ],
21613
+ [
21614
+ Math.abs(at.y - (box.y + box.h)),
21615
+ 0,
21616
+ 1
21617
+ ],
21618
+ [
21619
+ Math.abs(at.x - box.x),
21620
+ -1,
21621
+ 0
21622
+ ]
21623
+ ].sort((a, b) => a[0] - b[0])[0];
21624
+ if (d[0] > 1) return void 0;
21625
+ const vx = next.x - at.x;
21626
+ const vy = next.y - at.y;
21627
+ const l = Math.hypot(vx, vy);
21628
+ if (l === 0) return void 0;
21629
+ const cos = Math.max(-1, Math.min(1, (vx * d[1] + vy * d[2]) / l));
21630
+ return Math.acos(cos) * 180 / Math.PI;
21631
+ }
21632
+ /**
21633
+ * This function counts the crossings of a set of segments with a grid of buckets.
21634
+ * Above 20 000 segments, it counts the crossings of a seeded sample of 2 000 segments and scales the count.
21635
+ */
21636
+ function countCrossings(segs) {
21637
+ if (segs.length < 2) return [0, false];
21638
+ const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
21639
+ const grid = new Grid(Math.max(20, len));
21640
+ segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
21641
+ const exact = segs.length <= EXACT_CROSSINGS;
21642
+ const R = rng(segs.length);
21643
+ const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
21644
+ let count = 0;
21645
+ for (const i of pick) {
21646
+ const [a, b] = segs[i];
21647
+ grid.querySegment(a, b, 0, (j) => {
21648
+ if (exact && j <= i) return;
21649
+ if (j === i) return;
21650
+ const [c, d] = segs[j];
21651
+ if (segmentsCross(a, b, c, d)) count++;
21652
+ });
21653
+ }
21654
+ return exact ? [count, false] : [Math.round(count * segs.length / (2 * pick.length)), true];
21655
+ }
21656
+ /** This function measures how much the routes changed between two results. */
21657
+ function measureStability(before, after) {
21658
+ const prev = new Map(before.routes.map((r) => [r.edge, r.points]));
21659
+ let sum = 0;
21660
+ let max = 0;
21661
+ let moved = 0;
21662
+ let n = 0;
21663
+ for (const r of after.routes) {
21664
+ const p = prev.get(r.edge);
21665
+ if (!p) continue;
21666
+ const f = frechet(p, r.points);
21667
+ sum += f;
21668
+ max = Math.max(max, f);
21669
+ if (f > 1) moved++;
21670
+ n++;
21671
+ }
21672
+ return {
21673
+ mean: n ? sum / n : 0,
21674
+ max,
21675
+ moved: n ? moved / n : 0
21676
+ };
21677
+ }
21678
+ // src/whole/portfolio/index.ts
21679
+ /** The clock in milliseconds. */
21680
+ const clock = () => globalThis.performance?.now() ?? Date.now();
21681
+ /** The default options of the strategy `board-portfolio`. */
21682
+ const BOARD_PORTFOLIO_DEFAULTS = Object.freeze({
21683
+ settings: 4,
21684
+ crossings: 1,
21685
+ detour: 1,
21686
+ ink: 1,
21687
+ budget: 0
21688
+ });
21689
+ /** This function gives the rank of each value from 0 (the best) to 1 (the worst). Equal values get the mean of their ranks. One value gets 0. */
21690
+ function rankShares(values, lowerIsBetter) {
21691
+ const n = values.length;
21692
+ if (n < 2) return values.map(() => 0);
21693
+ const order = values.map((_, i) => i).sort((a, b) => lowerIsBetter ? values[a] - values[b] : values[b] - values[a]);
21694
+ const rank = new Array(n).fill(0);
21695
+ for (let i = 0; i < n;) {
21696
+ let j = i;
21697
+ while (j + 1 < n && Math.abs(values[order[j + 1]] - values[order[i]]) <= 1e-9) j++;
21698
+ for (let k = i; k <= j; k++) rank[order[k]] = (i + j) / 2 / (n - 1);
21699
+ i = j + 1;
21700
+ }
21701
+ return rank;
21702
+ }
21703
+ /**
21704
+ * This function runs the portfolio for one board (WILD-045, after Rice, 1976). It reads only the board and the outputs of its own settings.
21705
+ * A setting whose router or highway strategy has a node limit below the size of the board does not run.
21706
+ * A setting with a break of a hard rule does not count. The score of a setting is `Σ w_i × the rank share of measure_i` over the settings.
21707
+ * The measures are the crossings (R1), the median detour (R5) and the ink saved (B1). The least score wins, and a tie goes to the earlier setting.
21708
+ */
21709
+ function boardPortfolio(graph, list, o, now = clock) {
21710
+ const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
21711
+ const t0 = now();
21712
+ const entries = [];
21713
+ let fallback;
21714
+ for (const s of list.slice(0, count)) {
21715
+ if (o.budget > 0 && fallback !== void 0 && now() - t0 > o.budget * 1e3) break;
21716
+ const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
21717
+ if (limit !== void 0 && graph.nodes.length > limit) continue;
21718
+ const output = runPipeline(graph, s.pipeline);
21719
+ fallback ??= {
21720
+ name: s.name,
21721
+ output
21722
+ };
21723
+ if (checkProperties(output.graph, output.result, 1).length > 0) continue;
21724
+ entries.push({
21725
+ name: s.name,
21726
+ output,
21727
+ ...measuresOf(output.graph, output.result)
21728
+ });
21729
+ }
21730
+ if (entries.length === 0) {
21731
+ const first = fallback ?? {
21732
+ name: list[0].name,
21733
+ output: runPipeline(graph, list[0].pipeline)
21734
+ };
21735
+ return {
21736
+ output: first.output,
21737
+ pick: first.name,
21738
+ entries
21739
+ };
21740
+ }
21741
+ const r1 = rankShares(entries.map((e) => e.crossings), true);
21742
+ const r5 = rankShares(entries.map((e) => e.detour), true);
21743
+ const b1 = rankShares(entries.map((e) => e.ink), false);
21744
+ let best = 0;
21745
+ let score = Infinity;
21746
+ entries.forEach((_, i) => {
21747
+ const s = o.crossings * r1[i] + o.detour * r5[i] + o.ink * b1[i];
21748
+ if (s < score - 1e-9) [best, score] = [i, s];
21749
+ });
21750
+ return {
21751
+ output: entries[best].output,
21752
+ pick: entries[best].name,
21753
+ entries
21754
+ };
21755
+ }
21756
+ /** This function gives the three measures of the score of one output: the crossings, the median detour and the ink saved. */
21757
+ function measuresOf(graph, result) {
21758
+ return {
21759
+ crossings: measureQuality(graph, result).crossings,
21760
+ detour: detourSpread(result.routes).median,
21761
+ ink: inkSaved(result.routes)
21762
+ };
21763
+ }
21764
+ // src/strategies/schemas.ts
21765
+ /** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
21766
+ const WAY_SCHEMA = {
21767
+ margin: {
21768
+ type: "number",
21769
+ min: 1,
21770
+ max: 20,
21771
+ step: 1,
21772
+ default: 5,
21773
+ describe: "The distance from a box to the corners of the graph."
21774
+ },
21775
+ k: {
21776
+ type: "integer",
21777
+ min: 2,
21778
+ max: 16,
21779
+ step: 1,
21780
+ default: 8,
21781
+ describe: "The number of visible neighbours of each corner."
21782
+ },
21783
+ smooth: {
21784
+ type: "boolean",
21785
+ default: false,
21786
+ describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners."
21787
+ }
21788
+ };
21789
+ /** The declarations of the options of the stretch order (LANE-STRETCH): the method and the passes of `sifting`. A generator can list each value. */
21790
+ const LANE_STRETCH_SCHEMA = {
21791
+ method: {
21792
+ type: "enum",
21793
+ values: [
21794
+ "none",
21795
+ "consensus",
21796
+ "periphery",
21797
+ "sifting"
21798
+ ],
21799
+ default: "sifting",
21800
+ describe: "The order of each stretch: the order of each run alone, the order of the pair relation, the order of the largest run, or the order of the pair relation after sifting."
21801
+ },
21802
+ passes: {
21803
+ type: "integer",
21804
+ min: 1,
21805
+ max: 4,
21806
+ step: 1,
21807
+ default: 2,
21808
+ describe: "The number of passes of the method `sifting`. In each pass, each edge moves to its best place."
21809
+ }
21810
+ };
21811
+ /**
21812
+ * The declaration of the grouping rule of the edges: a name of `GROUP_RULE_NAMES`. A new name of the list needs no change here.
21813
+ * The option also takes a rule of the caller (`custom`), so the old value form stays valid.
21814
+ * The `defaults` of a strategy keep the rule `byCollection`, and the declaration gives its name `collection` as its `default`.
21815
+ */
21816
+ const EDGE_RULE = {
21817
+ type: "enum",
21818
+ values: GROUP_RULE_NAMES,
21819
+ custom: "function",
21820
+ describe: "The grouping rule."
21821
+ };
21822
+ /** The declaration of the lane order of the shared ways of the routers `trunks` and `shared`. */
21823
+ const LANE_ORDER = {
21824
+ type: "enum",
21825
+ values: ["bundle", "run"],
21826
+ default: "bundle",
21827
+ describe: "With `bundle`, two edges keep one lane order on each shared way of their common stretch. With `run`, each shared way gets its order alone."
21828
+ };
21829
+ /** The declaration of the lane order of the runs of the router `sections`. The default keeps the lane order of version 0.2.0. */
21830
+ const RUN_LANE_ORDER = {
21831
+ ...LANE_ORDER,
21832
+ default: "run"
21833
+ };
21834
+ /** The declarations of the options of the routers `corners` and `corners-smooth`. */
19980
21835
  const CORNERS_SCHEMA = {
19981
21836
  ...WAY_SCHEMA,
19982
21837
  plugs: {
@@ -20018,6 +21873,20 @@ const CORNERS_SCHEMA = {
20018
21873
  max: 20,
20019
21874
  step: 1,
20020
21875
  describe: "The distance from the walls of a frame to the bounds of its inner routes. Without a value, the inset is the margin."
21876
+ },
21877
+ inner: {
21878
+ type: "enum",
21879
+ values: ["corners", "gutter"],
21880
+ default: "corners",
21881
+ describe: "The mode of the inner routes. With `gutter`, each inner route is orthogonal in the gaps between the nodes of the frame, with a lane in each shared gap."
21882
+ },
21883
+ bendCost: {
21884
+ type: "number",
21885
+ min: 0,
21886
+ max: 100,
21887
+ step: 1,
21888
+ default: 16,
21889
+ describe: "The cost of one bend of an inner route of the mode `gutter`, in units of length. A larger cost gives fewer bends."
20021
21890
  }
20022
21891
  };
20023
21892
  /** The declarations of the options of the router `trunks`. */
@@ -23235,6 +25104,235 @@ const INTERCHANGE_SCHEMA = {
23235
25104
  describe: "An edge rides the network only when its way is this many times its first route or less."
23236
25105
  }
23237
25106
  };
25107
+ /** The declarations of the options of the highway strategy `knee` (NAT-018). */
25108
+ const KNEE_SCHEMA = {
25109
+ sweep: {
25110
+ type: "integer",
25111
+ min: 5,
25112
+ max: 21,
25113
+ step: 1,
25114
+ marks: [
25115
+ 5,
25116
+ 11,
25117
+ 21
25118
+ ],
25119
+ default: 11,
25120
+ describe: "The number of points of the sweep of the balancing factor."
25121
+ },
25122
+ rule: {
25123
+ type: "enum",
25124
+ values: ["chord", "weights"],
25125
+ default: "chord",
25126
+ describe: "The rule that picks one tree: the knee of the chord, or the least weighted sum."
25127
+ },
25128
+ ink: {
25129
+ type: "number",
25130
+ min: 0,
25131
+ max: 1,
25132
+ step: .05,
25133
+ marks: [
25134
+ .25,
25135
+ .5,
25136
+ .75
25137
+ ],
25138
+ default: .5,
25139
+ describe: "The weight of the ink in the rule `weights`. The rest of the weight goes to the path length."
25140
+ },
25141
+ detour: {
25142
+ type: "number",
25143
+ min: 1,
25144
+ max: 3,
25145
+ step: .05,
25146
+ marks: [
25147
+ 1.25,
25148
+ 1.5,
25149
+ 2
25150
+ ],
25151
+ default: 1.5,
25152
+ describe: "An edge rides the tree only when its path in the tree is this many times its first route or less."
25153
+ },
25154
+ minSize: {
25155
+ type: "integer",
25156
+ min: 2,
25157
+ max: 16,
25158
+ step: 1,
25159
+ default: 2,
25160
+ describe: "A stretch of a tree with fewer edges than this number is not a highway."
25161
+ }
25162
+ };
25163
+ /** The declarations of the options of the highway strategy `bronchial` (NAT-019). */
25164
+ const BRONCHIAL_SCHEMA = {
25165
+ split: {
25166
+ type: "enum",
25167
+ values: [
25168
+ "count",
25169
+ "weight",
25170
+ "gap"
25171
+ ],
25172
+ default: "count",
25173
+ describe: "The rule that splits the targets of a tip in two: by count, by weight, or at the widest gap."
25174
+ },
25175
+ share: {
25176
+ type: "number",
25177
+ min: .3,
25178
+ max: .7,
25179
+ step: .05,
25180
+ marks: [
25181
+ .3,
25182
+ .5,
25183
+ .7
25184
+ ],
25185
+ default: .5,
25186
+ describe: "The share of the distance to the center of the targets that a branch grows."
25187
+ },
25188
+ smallest: {
25189
+ type: "integer",
25190
+ min: 1,
25191
+ max: 3,
25192
+ step: 1,
25193
+ default: 1,
25194
+ describe: "A set with this number of targets or fewer stops the splits."
25195
+ },
25196
+ detour: {
25197
+ type: "number",
25198
+ min: 1,
25199
+ max: 3,
25200
+ step: .05,
25201
+ marks: [
25202
+ 1.25,
25203
+ 1.5,
25204
+ 2
25205
+ ],
25206
+ default: 1.5,
25207
+ describe: "An edge rides the tree only when its path in the tree is this many times its first route or less."
25208
+ },
25209
+ minSize: {
25210
+ type: "integer",
25211
+ min: 2,
25212
+ max: 16,
25213
+ step: 1,
25214
+ default: 2,
25215
+ describe: "A stretch of a tree with fewer edges than this number is not a highway."
25216
+ }
25217
+ };
25218
+ /** The declarations of the options of the level strategy `venation` (NAT-016): the options of `depths`, and the options of the backbone. */
25219
+ const VENATION_SCHEMA = {
25220
+ ...DEPTHS_SCHEMA,
25221
+ backbone: {
25222
+ type: "enum",
25223
+ values: ["rng", "gabriel"],
25224
+ default: "rng",
25225
+ describe: "The graph of the backbone: the relative neighbourhood graph, or the Gabriel graph."
25226
+ },
25227
+ discount: {
25228
+ type: "number",
25229
+ min: 0,
25230
+ max: .8,
25231
+ step: .05,
25232
+ marks: [
25233
+ 0,
25234
+ .4,
25235
+ .8
25236
+ ],
25237
+ default: .4,
25238
+ describe: "The share of the cost of a backbone link that a later level link saves when an earlier link uses it."
25239
+ },
25240
+ detour: {
25241
+ type: "number",
25242
+ min: 1,
25243
+ max: 3,
25244
+ step: .05,
25245
+ marks: [
25246
+ 1.25,
25247
+ 1.5,
25248
+ 2
25249
+ ],
25250
+ default: 1.5,
25251
+ describe: "A level link takes its way on the backbone only when that way is this many times its direct link or less."
25252
+ },
25253
+ maxItems: {
25254
+ type: "integer",
25255
+ min: 10,
25256
+ max: 2e3,
25257
+ step: 10,
25258
+ marks: [
25259
+ 100,
25260
+ 400,
25261
+ 1e3
25262
+ ],
25263
+ default: 400,
25264
+ describe: "A level with more items than this number keeps the links of `depths`."
25265
+ }
25266
+ };
25267
+ /** The declarations of the options of the whole strategy `board-portfolio` (WILD-045). */
25268
+ const BOARD_PORTFOLIO_SCHEMA = {
25269
+ settings: {
25270
+ type: "integer",
25271
+ min: 3,
25272
+ max: 12,
25273
+ step: 1,
25274
+ marks: [
25275
+ 3,
25276
+ 4,
25277
+ 8,
25278
+ 12
25279
+ ],
25280
+ default: 4,
25281
+ describe: "The number of settings that the portfolio tries, from the start of its list."
25282
+ },
25283
+ crossings: {
25284
+ type: "number",
25285
+ min: 0,
25286
+ max: 4,
25287
+ step: .25,
25288
+ marks: [
25289
+ 0,
25290
+ 1,
25291
+ 2
25292
+ ],
25293
+ default: 1,
25294
+ describe: "The weight of the crossings (R1) in the score."
25295
+ },
25296
+ detour: {
25297
+ type: "number",
25298
+ min: 0,
25299
+ max: 4,
25300
+ step: .25,
25301
+ marks: [
25302
+ 0,
25303
+ 1,
25304
+ 2
25305
+ ],
25306
+ default: 1,
25307
+ describe: "The weight of the median detour (R5) in the score."
25308
+ },
25309
+ ink: {
25310
+ type: "number",
25311
+ min: 0,
25312
+ max: 4,
25313
+ step: .25,
25314
+ marks: [
25315
+ 0,
25316
+ 1,
25317
+ 2
25318
+ ],
25319
+ default: 1,
25320
+ describe: "The weight of the ink saved (B1) in the score."
25321
+ },
25322
+ budget: {
25323
+ type: "number",
25324
+ min: 0,
25325
+ max: 10,
25326
+ step: .1,
25327
+ marks: [
25328
+ 0,
25329
+ 1,
25330
+ 10
25331
+ ],
25332
+ default: 0,
25333
+ describe: "The time budget in seconds. The value 0 sets no budget."
25334
+ }
25335
+ };
23238
25336
  // src/strategies/builtins.ts
23239
25337
  /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
23240
25338
  function routerStrategy(name, describe, maxNodes, defaults, make, schema) {
@@ -23968,6 +26066,24 @@ const highwayStrategies = Object.freeze({
23968
26066
  maxNodes: 1e4,
23969
26067
  run: (input, o) => interchangePlan(input, o)
23970
26068
  }),
26069
+ knee: defineStrategy({
26070
+ step: "highway",
26071
+ name: "knee",
26072
+ describe: "The knee of the plant front (NAT-018). A sweep of the balancing factor of NAT-017 gives trees, and the tree at the knee of ink against path length wins.",
26073
+ defaults: { ...KNEE_DEFAULTS },
26074
+ schema: KNEE_SCHEMA,
26075
+ maxNodes: 1e4,
26076
+ run: (input, o) => kneePlan(input, o)
26077
+ }),
26078
+ bronchial: defineStrategy({
26079
+ step: "highway",
26080
+ name: "bronchial",
26081
+ describe: "Bronchial splits that fill the space (NAT-019). The tree grows toward the center of its targets, then splits the targets in two halves, again and again.",
26082
+ defaults: { ...BRONCHIAL_DEFAULTS },
26083
+ schema: BRONCHIAL_SCHEMA,
26084
+ maxNodes: 1e4,
26085
+ run: (input, o) => bronchialPlan(input, o)
26086
+ }),
23971
26087
  "corridors+breakeven": gateStrategy(corridors, breakEvenGate),
23972
26088
  "corridors+stretch": gateStrategy(corridors, stretchGate),
23973
26089
  "corridors+ambiguity": gateStrategy(corridors, ambiguityGate),
@@ -24014,26 +26130,74 @@ const plugStrategies = Object.freeze({
24014
26130
  })
24015
26131
  });
24016
26132
  /** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
24017
- const levelStrategies = Object.freeze({ depths: defineStrategy({
24018
- step: "levels",
24019
- name: "depths",
24020
- describe: "One zoom level for each depth of the tree, and a last level with each node alone. A link of a level goes around the other items. It is the default of the router `sections`.",
24021
- defaults: {},
24022
- schema: DEPTHS_SCHEMA,
24023
- run: (input, o) => depthLevels(input, o)
24024
- }) });
26133
+ const levelStrategies = Object.freeze({
26134
+ depths: defineStrategy({
26135
+ step: "levels",
26136
+ name: "depths",
26137
+ describe: "One zoom level for each depth of the tree, and a last level with each node alone. A link of a level goes around the other items. It is the default of the router `sections`.",
26138
+ defaults: {},
26139
+ schema: DEPTHS_SCHEMA,
26140
+ run: (input, o) => depthLevels(input, o)
26141
+ }),
26142
+ venation: defineStrategy({
26143
+ step: "levels",
26144
+ name: "venation",
26145
+ describe: "A closed venation backbone (NAT-016). The links of each level with groups take their ways along the relative neighbourhood graph of the items.",
26146
+ defaults: { ...VENATION_DEFAULTS },
26147
+ schema: VENATION_SCHEMA,
26148
+ run: (input, o) => venationLevels(input, o)
26149
+ })
26150
+ });
26151
+ /**
26152
+ * The fixed list of settings of the whole strategy `board-portfolio` (WILD-045), in order. The option `settings` takes the first ones.
26153
+ * The list holds routers and highway strategies with their defaults. It comes from the catalog of the strategies, not from a fit to the screen.
26154
+ */
26155
+ const BOARD_PORTFOLIO_SETTINGS = Object.freeze([...[
26156
+ "shared",
26157
+ "corners",
26158
+ "trunks",
26159
+ "sections",
26160
+ "sections-tree",
26161
+ "corners-smooth"
26162
+ ].map((name) => ({
26163
+ name,
26164
+ pipeline: { router: routerStrategies[name] }
26165
+ })), ...[
26166
+ "corridors",
26167
+ "shared-ways",
26168
+ "road-classes",
26169
+ "steiner",
26170
+ "knee",
26171
+ "bronchial"
26172
+ ].map((name) => ({
26173
+ name: `highway:${name}`,
26174
+ pipeline: {
26175
+ router: routerStrategies.corners,
26176
+ highway: highwayStrategies[name]
26177
+ }
26178
+ }))].map((s) => Object.freeze(s)));
24025
26179
  /**
24026
26180
  * The built-in whole strategies: the strategies of the step `whole`. The strategy `pipeline` runs one strategy of each step, as the demo of the site does.
24027
26181
  * Without options, it routes the graph with the router `corners`.
24028
26182
  */
24029
- const wholeStrategies = Object.freeze({ pipeline: defineStrategy({
24030
- step: "whole",
24031
- name: "pipeline",
24032
- describe: "One setting of the steps. The room opens the board, then the router or the highway plan routes it with the rules that the router reads.",
24033
- defaults: { router: routerStrategies.corners },
24034
- schema: PIPELINE_SCHEMA,
24035
- run: (graph, o) => runPipeline(graph, o)
24036
- }) });
26183
+ const wholeStrategies = Object.freeze({
26184
+ pipeline: defineStrategy({
26185
+ step: "whole",
26186
+ name: "pipeline",
26187
+ describe: "One setting of the steps. The room opens the board, then the router or the highway plan routes it with the rules that the router reads.",
26188
+ defaults: { router: routerStrategies.corners },
26189
+ schema: PIPELINE_SCHEMA,
26190
+ run: (graph, o) => runPipeline(graph, o)
26191
+ }),
26192
+ "board-portfolio": defineStrategy({
26193
+ step: "whole",
26194
+ name: "board-portfolio",
26195
+ describe: "A portfolio for each board (WILD-045). The pipeline tries a fixed list of settings, and the least weighted rank of crossings, detour and ink wins.",
26196
+ defaults: { ...BOARD_PORTFOLIO_DEFAULTS },
26197
+ schema: BOARD_PORTFOLIO_SCHEMA,
26198
+ run: (graph, o) => boardPortfolio(graph, BOARD_PORTFOLIO_SETTINGS, o).output
26199
+ })
26200
+ });
24037
26201
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
24038
26202
  const BUILT_IN_STRATEGIES = Object.freeze([
24039
26203
  ...Object.values(groupStrategies),
@@ -24091,252 +26255,68 @@ function fixPorts(graph, rule, options = {}) {
24091
26255
  const added = /* @__PURE__ */ new Map();
24092
26256
  const taken = /* @__PURE__ */ new Map();
24093
26257
  const portsOf = (node) => {
24094
- let m = fixed.get(node.id);
24095
- if (!m) fixed.set(node.id, m = /* @__PURE__ */ new Map());
24096
- return m;
24097
- };
24098
- const fix = (node, plug, base) => ({
24099
- ...base,
24100
- side: sideOf(plug, node),
24101
- at: {
24102
- x: plug.point.x - node.box.x,
24103
- y: plug.point.y - node.box.y
24104
- }
24105
- });
24106
- const outerEdges = new Set(outer.edges);
24107
- const edges = graph.edges.map((e) => {
24108
- if (!outerEdges.has(e)) return e;
24109
- const pair = byEdge.get(e.id)?.shift();
24110
- if (!pair) return e;
24111
- const end = (which, plug) => {
24112
- const own = e[which];
24113
- const node = byId.get(own.node);
24114
- if (!node) return own;
24115
- if (own.port === void 0) {
24116
- if (!options.newPorts) return own;
24117
- let ids = taken.get(node.id);
24118
- if (!ids) taken.set(node.id, ids = new Set((node.ports ?? []).map((p) => p.id)));
24119
- const id = freeId(ids, `${e.id}:${which}`);
24120
- const list = added.get(node.id) ?? [];
24121
- list.push(fix(node, plug, { id }));
24122
- added.set(node.id, list);
24123
- return {
24124
- ...own,
24125
- port: id
24126
- };
24127
- }
24128
- const port = node.ports?.find((p) => p.id === own.port);
24129
- if (!port || portOffset(node.box, port) !== void 0) return own;
24130
- const m = portsOf(node);
24131
- if (!m.has(port.id)) m.set(port.id, fix(node, plug, port));
24132
- return own;
24133
- };
24134
- const source = end("source", pair.source);
24135
- const target = end("target", pair.target);
24136
- return source === e.source && target === e.target ? e : {
24137
- ...e,
24138
- source,
24139
- target
24140
- };
24141
- });
24142
- const nodes = graph.nodes.map((n) => {
24143
- const m = fixed.get(n.id);
24144
- const more = added.get(n.id);
24145
- if (!m && !more) return n;
24146
- const ports = (n.ports ?? []).map((p) => m?.get(p.id) ?? p);
24147
- return {
24148
- ...n,
24149
- ports: more ? [...ports, ...more] : ports
24150
- };
24151
- });
24152
- return {
24153
- ...graph,
24154
- nodes,
24155
- edges
24156
- };
24157
- }
24158
- // src/bench/quality.ts
24159
- /** The clearance stops at this distance. */
24160
- const CLEARANCE_CAP = 40;
24161
- const TIGHT = 4;
24162
- const EXACT_CROSSINGS = 2e4;
24163
- const SAMPLE = 2e3;
24164
- const key = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
24165
- /** This function measures the quality of a result. */
24166
- function measureQuality(graph, result) {
24167
- const byId = nodeIndex(graph);
24168
- const nodes = new Grid(180);
24169
- for (const n of graph.nodes) nodes.add(n, n.box);
24170
- const segs = [];
24171
- const seen = /* @__PURE__ */ new Set();
24172
- let bends = 0;
24173
- let infl = 0;
24174
- let clear = 0;
24175
- let tight = 0;
24176
- let detour = 0;
24177
- let detourN = 0;
24178
- let angle = 0;
24179
- let angleN = 0;
24180
- for (const r of result.routes) {
24181
- const p = r.points;
24182
- for (let i = 1; i < p.length; i++) {
24183
- const ka = key(p[i - 1]);
24184
- const kb = key(p[i]);
24185
- if (ka === kb) continue;
24186
- const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
24187
- if (seen.has(k)) continue;
24188
- seen.add(k);
24189
- segs.push([p[i - 1], p[i]]);
24190
- }
24191
- bends += bendCount(p);
24192
- infl += inflectionCount(p);
24193
- const ends = endNodes(graph, byId, r.edge);
24194
- let c = 40;
24195
- for (let i = 1; i < p.length; i++) {
24196
- const a = p[i - 1];
24197
- const b = p[i];
24198
- nodes.querySegment(a, b, c, (n) => {
24199
- if (n !== ends.source && n !== ends.target) c = Math.min(c, segmentBoxDistance(a, b, n.box));
24200
- });
24201
- }
24202
- clear += c;
24203
- if (c < TIGHT) tight++;
24204
- if (p.length >= 2) {
24205
- const d = dist(p[0], p[p.length - 1]);
24206
- if (d > 1) {
24207
- detour += polylineLength(p) / d;
24208
- detourN++;
24209
- }
24210
- for (const [at, next, node] of [[
24211
- p[0],
24212
- p[1],
24213
- ends.source
24214
- ], [
24215
- p[p.length - 1],
24216
- p[p.length - 2],
24217
- ends.target
24218
- ]]) {
24219
- if (!node) continue;
24220
- const a = sideAngle(node.box, at, next);
24221
- if (a !== void 0) {
24222
- angle += a;
24223
- angleN++;
24224
- }
24225
- }
24226
- }
24227
- }
24228
- const n = Math.max(1, result.routes.length);
24229
- let ink = 0;
24230
- for (const [a, b] of segs) ink += dist(a, b);
24231
- const [crossings, crossingsEstimated] = countCrossings(segs);
24232
- return {
24233
- ink,
24234
- crossings,
24235
- crossingsEstimated,
24236
- bends: bends / n,
24237
- inflections: infl / n,
24238
- clearance: clear / n,
24239
- tight: tight / n,
24240
- detour: detourN ? detour / detourN : 1,
24241
- plugAngle: angleN ? angle / angleN : 0
24242
- };
24243
- }
24244
- function endNodes(graph, byId, edgeId) {
24245
- const e = edgeIndex(graph).get(edgeId);
24246
- return {
24247
- source: e && byId.get(e.source.node),
24248
- target: e && byId.get(e.target.node)
24249
- };
24250
- }
24251
- const edgeIndexes = /* @__PURE__ */ new WeakMap();
24252
- function edgeIndex(graph) {
24253
- let m = edgeIndexes.get(graph);
24254
- if (!m) {
24255
- m = /* @__PURE__ */ new Map();
24256
- for (const e of graph.edges) m.set(e.id, e);
24257
- edgeIndexes.set(graph, m);
24258
- }
24259
- return m;
24260
- }
24261
- /**
24262
- * This function gives an angle in degrees: the angle between the segment from `at` to `next` and the outward normal.
24263
- * The normal is the normal of the side of the box nearest to `at`. A route that starts inside the box (an interior route) gives no angle.
24264
- */
24265
- function sideAngle(box, at, next) {
24266
- const d = [
24267
- [
24268
- Math.abs(at.y - box.y),
24269
- 0,
24270
- -1
24271
- ],
24272
- [
24273
- Math.abs(at.x - (box.x + box.w)),
24274
- 1,
24275
- 0
24276
- ],
24277
- [
24278
- Math.abs(at.y - (box.y + box.h)),
24279
- 0,
24280
- 1
24281
- ],
24282
- [
24283
- Math.abs(at.x - box.x),
24284
- -1,
24285
- 0
24286
- ]
24287
- ].sort((a, b) => a[0] - b[0])[0];
24288
- if (d[0] > 1) return void 0;
24289
- const vx = next.x - at.x;
24290
- const vy = next.y - at.y;
24291
- const l = Math.hypot(vx, vy);
24292
- if (l === 0) return void 0;
24293
- const cos = Math.max(-1, Math.min(1, (vx * d[1] + vy * d[2]) / l));
24294
- return Math.acos(cos) * 180 / Math.PI;
24295
- }
24296
- /**
24297
- * This function counts the crossings of a set of segments with a grid of buckets.
24298
- * Above 20 000 segments, it counts the crossings of a seeded sample of 2 000 segments and scales the count.
24299
- */
24300
- function countCrossings(segs) {
24301
- if (segs.length < 2) return [0, false];
24302
- const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
24303
- const grid = new Grid(Math.max(20, len));
24304
- segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
24305
- const exact = segs.length <= EXACT_CROSSINGS;
24306
- const R = rng(segs.length);
24307
- const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
24308
- let count = 0;
24309
- for (const i of pick) {
24310
- const [a, b] = segs[i];
24311
- grid.querySegment(a, b, 0, (j) => {
24312
- if (exact && j <= i) return;
24313
- if (j === i) return;
24314
- const [c, d] = segs[j];
24315
- if (segmentsCross(a, b, c, d)) count++;
24316
- });
24317
- }
24318
- return exact ? [count, false] : [Math.round(count * segs.length / (2 * pick.length)), true];
24319
- }
24320
- /** This function measures how much the routes changed between two results. */
24321
- function measureStability(before, after) {
24322
- const prev = new Map(before.routes.map((r) => [r.edge, r.points]));
24323
- let sum = 0;
24324
- let max = 0;
24325
- let moved = 0;
24326
- let n = 0;
24327
- for (const r of after.routes) {
24328
- const p = prev.get(r.edge);
24329
- if (!p) continue;
24330
- const f = frechet(p, r.points);
24331
- sum += f;
24332
- max = Math.max(max, f);
24333
- if (f > 1) moved++;
24334
- n++;
24335
- }
26258
+ let m = fixed.get(node.id);
26259
+ if (!m) fixed.set(node.id, m = /* @__PURE__ */ new Map());
26260
+ return m;
26261
+ };
26262
+ const fix = (node, plug, base) => ({
26263
+ ...base,
26264
+ side: sideOf(plug, node),
26265
+ at: {
26266
+ x: plug.point.x - node.box.x,
26267
+ y: plug.point.y - node.box.y
26268
+ }
26269
+ });
26270
+ const outerEdges = new Set(outer.edges);
26271
+ const edges = graph.edges.map((e) => {
26272
+ if (!outerEdges.has(e)) return e;
26273
+ const pair = byEdge.get(e.id)?.shift();
26274
+ if (!pair) return e;
26275
+ const end = (which, plug) => {
26276
+ const own = e[which];
26277
+ const node = byId.get(own.node);
26278
+ if (!node) return own;
26279
+ if (own.port === void 0) {
26280
+ if (!options.newPorts) return own;
26281
+ let ids = taken.get(node.id);
26282
+ if (!ids) taken.set(node.id, ids = new Set((node.ports ?? []).map((p) => p.id)));
26283
+ const id = freeId(ids, `${e.id}:${which}`);
26284
+ const list = added.get(node.id) ?? [];
26285
+ list.push(fix(node, plug, { id }));
26286
+ added.set(node.id, list);
26287
+ return {
26288
+ ...own,
26289
+ port: id
26290
+ };
26291
+ }
26292
+ const port = node.ports?.find((p) => p.id === own.port);
26293
+ if (!port || portOffset(node.box, port) !== void 0) return own;
26294
+ const m = portsOf(node);
26295
+ if (!m.has(port.id)) m.set(port.id, fix(node, plug, port));
26296
+ return own;
26297
+ };
26298
+ const source = end("source", pair.source);
26299
+ const target = end("target", pair.target);
26300
+ return source === e.source && target === e.target ? e : {
26301
+ ...e,
26302
+ source,
26303
+ target
26304
+ };
26305
+ });
26306
+ const nodes = graph.nodes.map((n) => {
26307
+ const m = fixed.get(n.id);
26308
+ const more = added.get(n.id);
26309
+ if (!m && !more) return n;
26310
+ const ports = (n.ports ?? []).map((p) => m?.get(p.id) ?? p);
26311
+ return {
26312
+ ...n,
26313
+ ports: more ? [...ports, ...more] : ports
26314
+ };
26315
+ });
24336
26316
  return {
24337
- mean: n ? sum / n : 0,
24338
- max,
24339
- moved: n ? moved / n : 0
26317
+ ...graph,
26318
+ nodes,
26319
+ edges
24340
26320
  };
24341
26321
  }
24342
26322
  // src/bench/run.ts
@@ -29659,10 +31639,12 @@ const count = (name, min, max, value, meaning) => numberDimension({
29659
31639
  default: value,
29660
31640
  meaning
29661
31641
  });
29662
- /** This function gives the numbers of the full dimension values, by name. */
31642
+ /** This function gives the numbers of the full dimension values, by name. A dimension that is not `int` or `real` is not in them. */
29663
31643
  function numbers(dimensions, dims) {
29664
- return Object.fromEntries(dimensions.map((d) => [d.name, numberOf(dims, d.name)]));
31644
+ return Object.fromEntries(dimensions.filter((d) => d.kind === "int" || d.kind === "real").map((d) => [d.name, numberOf(dims, d.name)]));
29665
31645
  }
31646
+ /** The default rule of the random numbers of a case family. */
31647
+ const CASE_SEEDING = "mulberry32 of the input seed. The fixed part of the board takes no number. Then one stream gives the random links.";
29666
31648
  /** This function gives a case family. Its stream is mulberry32 of the input seed, and it uses only the stream, `Math.floor` and `Math.ceil`. */
29667
31649
  function caseFamily(fields) {
29668
31650
  const { name, meaning, dimensions } = fields;
@@ -29673,11 +31655,11 @@ function caseFamily(fields) {
29673
31655
  owner: "highways",
29674
31656
  meaning,
29675
31657
  dimensions,
29676
- seeding: "mulberry32 of the input seed. The fixed part of the board takes no number. Then one stream gives the random links.",
31658
+ seeding: fields.seeding ?? CASE_SEEDING,
29677
31659
  crossEngine: "exact",
29678
31660
  generate(seed, dims, context) {
29679
31661
  return {
29680
- graph: fields.make(numbers(dimensions, dims), seed, context.random),
31662
+ graph: fields.make(numbers(dimensions, dims), seed, context.random, dims),
29681
31663
  generator: {
29682
31664
  name: fields.generator,
29683
31665
  version: 1,
@@ -29759,6 +31741,508 @@ const PACKED_GRID = caseFamily({
29759
31741
  reach: v["reach"]
29760
31742
  }, random)
29761
31743
  });
31744
+ // src/families/coverage.ts
31745
+ /** A board while it grows. It adds no loop, and no second edge between the same two ends. */
31746
+ var Builder = class {
31747
+ nodes = [];
31748
+ edges = [];
31749
+ #keys = /* @__PURE__ */ new Set();
31750
+ node(id, x, y, w, h, extra = {}) {
31751
+ this.nodes.push({
31752
+ id,
31753
+ box: {
31754
+ x,
31755
+ y,
31756
+ w,
31757
+ h
31758
+ },
31759
+ ...extra.parent !== void 0 ? { parent: extra.parent } : {},
31760
+ ...extra.ports ? { ports: extra.ports } : {}
31761
+ });
31762
+ return id;
31763
+ }
31764
+ /** This method adds an edge. The key of an end is its node and its plug. It adds nothing for a loop or a second edge between the same two ends. */
31765
+ link(source, target) {
31766
+ if (source.node === target.node) return;
31767
+ const a = `${source.node}:${source.port ?? ""}`;
31768
+ const b = `${target.node}:${target.port ?? ""}`;
31769
+ const key = a < b ? `${a}|${b}` : `${b}|${a}`;
31770
+ if (this.#keys.has(key)) return;
31771
+ this.#keys.add(key);
31772
+ this.edges.push({
31773
+ id: `e${this.edges.length}`,
31774
+ source,
31775
+ target
31776
+ });
31777
+ }
31778
+ graph() {
31779
+ return {
31780
+ nodes: this.nodes,
31781
+ edges: this.edges
31782
+ };
31783
+ }
31784
+ };
31785
+ /** This function gives a whole number from `lo` to `hi`, both included. */
31786
+ const int = (random, lo, hi) => lo + Math.floor(random() * (hi - lo + 1));
31787
+ /** This function gives the least whole number `s` with `s × s` equal to `v` or more. It uses no square root, so each engine gives the same value. */
31788
+ function ceilSqrt(v) {
31789
+ let lo = 0;
31790
+ let hi = Math.max(1, v);
31791
+ while (lo < hi) {
31792
+ const mid = Math.floor((lo + hi) / 2);
31793
+ if (mid * mid >= v) hi = mid;
31794
+ else lo = mid + 1;
31795
+ }
31796
+ return lo;
31797
+ }
31798
+ /** This function gives `k` of the indices 0 to `n - 1` at random, in increasing order. It uses a part of the shuffle of Fisher and Yates. */
31799
+ function choose(random, n, k) {
31800
+ const list = Array.from({ length: n }, (_, i) => i);
31801
+ const take = Math.min(k, n);
31802
+ for (let i = 0; i < take; i++) {
31803
+ const j = int(random, i, n - 1);
31804
+ [list[i], list[j]] = [list[j], list[i]];
31805
+ }
31806
+ return list.slice(0, take).sort((a, b) => a - b);
31807
+ }
31808
+ /** This function gives the indices 0 to `n - 1` in a random order, with the shuffle of Fisher and Yates. */
31809
+ function shuffle(random, n) {
31810
+ const list = Array.from({ length: n }, (_, i) => i);
31811
+ for (let i = n - 1; i > 0; i--) {
31812
+ const j = int(random, 0, i);
31813
+ [list[i], list[j]] = [list[j], list[i]];
31814
+ }
31815
+ return list;
31816
+ }
31817
+ /** The rule of the random numbers of each family of this file. */
31818
+ const SEEDING = "mulberry32 of the input seed. One stream gives the sizes and the places of the boxes, then the edges, in a fixed order.";
31819
+ /**
31820
+ * The family `frame-rows`: cards with full-width rows, as INTERIOR-ROUTE takes them. Each row has a fixed plug on the east wall and on the west wall of its card.
31821
+ * The links join a plug of a card to a plug of another card. An inner edge joins a row to a plug of its own card. The default has no inner edge.
31822
+ */
31823
+ const FRAME_ROWS = caseFamily({
31824
+ name: "frame-rows",
31825
+ meaning: "Cards with full-width rows and fixed plugs on their walls, as tables of a schema diagram.",
31826
+ dimensions: Object.freeze([
31827
+ count("cards", 1, 24, 9, "The number of cards. Each card is a frame."),
31828
+ count("rows", 1, 12, 5, "The number of full-width rows in each card."),
31829
+ count("links", 0, 8, 4, "The number of tries for a link from a wall plug of a card to a wall plug of another card, for each card."),
31830
+ count("inner", 0, 12, 0, "The number of tries for an inner edge from a row to a wall plug of its card, for each card."),
31831
+ count("gap", 24, 240, 96, "The free space between two cards, in units.")
31832
+ ]),
31833
+ generator: "family.frame-rows",
31834
+ seeding: SEEDING,
31835
+ make: (v, _seed, random) => {
31836
+ const b = new Builder();
31837
+ const cards = v["cards"];
31838
+ const rows = v["rows"];
31839
+ const gap = v["gap"];
31840
+ const cols = ceilSqrt(cards);
31841
+ const w = 220;
31842
+ const header = 28;
31843
+ const pitch = 30;
31844
+ const h = header + rows * pitch + 4;
31845
+ const ports = [];
31846
+ for (let j = 0; j < rows; j++) {
31847
+ const along = (header + j * pitch + 12) / h;
31848
+ ports.push({
31849
+ id: `e${j}`,
31850
+ side: "east",
31851
+ along
31852
+ }, {
31853
+ id: `w${j}`,
31854
+ side: "west",
31855
+ along
31856
+ });
31857
+ }
31858
+ const colOf = (i) => i % cols;
31859
+ for (let i = 0; i < cards; i++) {
31860
+ const x = colOf(i) * (w + gap);
31861
+ const y = Math.floor(i / cols) * (h + gap);
31862
+ const card = b.node(`f${i}`, x, y, w, h, { ports });
31863
+ for (let j = 0; j < rows; j++) b.node(`f${i}r${j}`, x + 10, y + header + j * pitch, 200, 24, { parent: card });
31864
+ }
31865
+ for (let n = 0; n < cards * v["links"]; n++) {
31866
+ const a = int(random, 0, cards - 1);
31867
+ const c = int(random, 0, cards - 1);
31868
+ const ra = int(random, 0, rows - 1);
31869
+ const rc = int(random, 0, rows - 1);
31870
+ const east = colOf(a) <= colOf(c);
31871
+ b.link({
31872
+ node: `f${a}`,
31873
+ port: `${east ? "e" : "w"}${ra}`
31874
+ }, {
31875
+ node: `f${c}`,
31876
+ port: `${east ? "w" : "e"}${rc}`
31877
+ });
31878
+ }
31879
+ for (let i = 0; i < cards; i++) for (let t = 0; t < v["inner"]; t++) {
31880
+ const row = int(random, 0, rows - 1);
31881
+ const side = random() < .5 ? "e" : "w";
31882
+ b.link({ node: `f${i}r${row}` }, {
31883
+ node: `f${i}`,
31884
+ port: `${side}${int(random, 0, rows - 1)}`
31885
+ });
31886
+ }
31887
+ return b.graph();
31888
+ }
31889
+ });
31890
+ /** The family `pipeline`: a long chain of stages in rows that turn at each end, with some links that skip stages. */
31891
+ const PIPELINE = caseFamily({
31892
+ name: "pipeline",
31893
+ meaning: "A long chain of stages in rows that turn at each end, with a few links that skip stages.",
31894
+ dimensions: Object.freeze([
31895
+ count("stages", 4, 240, 72, "The number of stages of the chain."),
31896
+ count("perRow", 2, 40, 12, "The number of stages in each row. The chain turns at the end of each row."),
31897
+ count("skips", 0, 80, 8, "The number of tries for a link that skips 1 to 3 stages."),
31898
+ count("gap", 16, 160, 56, "The free space between two stages, in units.")
31899
+ ]),
31900
+ generator: "family.pipeline",
31901
+ seeding: SEEDING,
31902
+ make: (v, _seed, random) => {
31903
+ const b = new Builder();
31904
+ const stages = v["stages"];
31905
+ const perRow = v["perRow"];
31906
+ const gap = v["gap"];
31907
+ const cell = 120;
31908
+ for (let i = 0; i < stages; i++) {
31909
+ const r = Math.floor(i / perRow);
31910
+ const p = i % perRow;
31911
+ const col = r % 2 === 0 ? p : perRow - 1 - p;
31912
+ const w = int(random, 72, cell);
31913
+ b.node(`s${i}`, col * (cell + gap) + Math.floor((cell - w) / 2), r * (40 + gap), w, 40);
31914
+ }
31915
+ for (let i = 0; i + 1 < stages; i++) b.link({ node: `s${i}` }, { node: `s${i + 1}` });
31916
+ for (let n = 0; n < v["skips"]; n++) {
31917
+ const s = int(random, 0, stages - 1);
31918
+ const d = int(random, 2, 4);
31919
+ if (s + d < stages) b.link({ node: `s${s}` }, { node: `s${s + d}` });
31920
+ }
31921
+ return b.graph();
31922
+ }
31923
+ });
31924
+ /** This function gives the cells of the square ring `r` around the cell (0, 0), in a fixed order. */
31925
+ function ring(r) {
31926
+ const out = [];
31927
+ for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) if (Math.max(Math.abs(dx), Math.abs(dy)) === r) out.push([dx, dy]);
31928
+ return out;
31929
+ }
31930
+ /** The family `fan-out`: one large hub in the middle, with a link to each leaf on the square rings around it. */
31931
+ const FAN_OUT = caseFamily({
31932
+ name: "fan-out",
31933
+ meaning: "A wide fan-out: one hub in the middle links to each leaf on the rings around it.",
31934
+ dimensions: Object.freeze([
31935
+ count("leaves", 4, 240, 96, "The number of leaves. The hub links to each leaf."),
31936
+ count("cross", 0, 120, 8, "The number of tries for a link between two leaves."),
31937
+ count("gap", 8, 120, 36, "The free space between two leaves, in units.")
31938
+ ]),
31939
+ generator: "family.fan-out",
31940
+ seeding: SEEDING,
31941
+ make: (v, _seed, random) => {
31942
+ const b = new Builder();
31943
+ const leaves = v["leaves"];
31944
+ const gap = v["gap"];
31945
+ const cw = 80 + gap;
31946
+ const ch = 32 + gap;
31947
+ const cells = [];
31948
+ let r = 2;
31949
+ while (cells.length < leaves + Math.floor(leaves / 4)) cells.push(...ring(r++));
31950
+ const reach = r - 1;
31951
+ const at = (dx, dy) => [(dx + reach) * cw, (dy + reach) * ch];
31952
+ const [hx, hy] = at(-1, -1);
31953
+ const hub = b.node("hub", hx, hy, 2 * cw + 80, 2 * ch + 32);
31954
+ const ids = [];
31955
+ for (const i of choose(random, cells.length, leaves)) {
31956
+ const [x, y] = at(...cells[i]);
31957
+ const w = int(random, 48, 80);
31958
+ const h = int(random, 20, 32);
31959
+ ids.push(b.node(`l${ids.length}`, x, y, w, h));
31960
+ }
31961
+ for (const id of ids) b.link({ node: hub }, { node: id });
31962
+ for (let n = 0; n < v["cross"]; n++) b.link({ node: ids[int(random, 0, ids.length - 1)] }, { node: ids[int(random, 0, ids.length - 1)] });
31963
+ return b.graph();
31964
+ }
31965
+ });
31966
+ /** The family `two-hubs`: two tall hubs with many nodes between them. Each such node has a link from each hub. A few nodes of one hub are on its far side. */
31967
+ const TWO_HUBS = caseFamily({
31968
+ name: "two-hubs",
31969
+ meaning: "Two hubs that share many targets between them: two fans that overlap.",
31970
+ dimensions: Object.freeze([
31971
+ count("shared", 0, 160, 48, "The number of nodes between the hubs. Each of them has a link from each hub."),
31972
+ count("own", 0, 80, 12, "The number of nodes on the far side of each hub, with a link from that hub only."),
31973
+ count("span", 1, 12, 5, "The number of columns of nodes between the two hubs."),
31974
+ count("gap", 8, 120, 36, "The free space between two nodes, in units.")
31975
+ ]),
31976
+ generator: "family.two-hubs",
31977
+ seeding: SEEDING,
31978
+ make: (v, _seed, random) => {
31979
+ const b = new Builder();
31980
+ const shared = v["shared"];
31981
+ const own = v["own"];
31982
+ const span = v["span"];
31983
+ const gap = v["gap"];
31984
+ const cw = 64 + gap;
31985
+ const ch = 28 + gap;
31986
+ const ceilDiv = (a, d) => Math.floor((a + d - 1) / d);
31987
+ const rows = Math.max(1, ceilDiv(shared + Math.floor(shared / 4), span), ceilDiv(own + Math.floor(own / 4), 2));
31988
+ const hubH = rows * ch - gap;
31989
+ const a = b.node("A", 2 * cw, 0, 64, hubH);
31990
+ const z = b.node("B", (span + 3) * cw, 0, 64, hubH);
31991
+ const place = (prefix, first, cols, n) => choose(random, cols * rows, n).map((c, i) => b.node(`${prefix}${i}`, (first + c % cols) * cw, Math.floor(c / cols) * ch, 64, 28));
31992
+ const mid = place("m", 3, span, shared);
31993
+ const left = place("a", 0, 2, own);
31994
+ const right = place("b", span + 4, 2, own);
31995
+ for (const id of mid) {
31996
+ b.link({ node: a }, { node: id });
31997
+ b.link({ node: z }, { node: id });
31998
+ }
31999
+ for (const id of left) b.link({ node: a }, { node: id });
32000
+ for (const id of right) b.link({ node: z }, { node: id });
32001
+ b.link({ node: a }, { node: z });
32002
+ return b.graph();
32003
+ }
32004
+ });
32005
+ /** The family `uneven-sizes`: a few large boxes and many small boxes in shelves, in a random order. Many edges end on a large box. */
32006
+ const UNEVEN_SIZES = caseFamily({
32007
+ name: "uneven-sizes",
32008
+ meaning: "Very uneven box sizes: a few large boxes among many small boxes.",
32009
+ dimensions: Object.freeze([
32010
+ count("small", 4, 300, 90, "The number of small boxes."),
32011
+ count("large", 1, 16, 4, "The number of large boxes."),
32012
+ count("ratio", 2, 10, 5, "The size of a large box, divided by the size of a small box."),
32013
+ numberDimension({
32014
+ name: "edgesPerNode",
32015
+ kind: "real",
32016
+ min: .5,
32017
+ max: 4,
32018
+ default: 1.5,
32019
+ meaning: "The number of tries for an edge, divided by the number of boxes."
32020
+ }),
32021
+ numberDimension({
32022
+ name: "toLarge",
32023
+ kind: "real",
32024
+ min: 0,
32025
+ max: 1,
32026
+ default: .5,
32027
+ meaning: "The share of the edges with a large box at one end."
32028
+ }),
32029
+ count("gap", 8, 120, 30, "The free space between two boxes, in units.")
32030
+ ]),
32031
+ generator: "family.uneven-sizes",
32032
+ seeding: SEEDING,
32033
+ make: (v, _seed, random) => {
32034
+ const b = new Builder();
32035
+ const ratio = v["ratio"];
32036
+ const gap = v["gap"];
32037
+ const boxes = [];
32038
+ for (let i = 0; i < v["large"]; i++) boxes.push({
32039
+ id: `L${i}`,
32040
+ w: ratio * int(random, 48, 72),
32041
+ h: ratio * int(random, 24, 36)
32042
+ });
32043
+ for (let i = 0; i < v["small"]; i++) boxes.push({
32044
+ id: `s${i}`,
32045
+ w: int(random, 40, 80),
32046
+ h: int(random, 20, 40)
32047
+ });
32048
+ const order = shuffle(random, boxes.length);
32049
+ const area = boxes.reduce((s, x) => s + (x.w + gap) * (x.h + gap), 0);
32050
+ const width = Math.max(...boxes.map((x) => x.w), Math.floor(3 * ceilSqrt(area) / 2));
32051
+ const places = /* @__PURE__ */ new Map();
32052
+ let x = 0;
32053
+ let y = 0;
32054
+ let shelf = 0;
32055
+ for (const i of order) {
32056
+ const box = boxes[i];
32057
+ if (x > 0 && x + box.w > width) {
32058
+ x = 0;
32059
+ y += shelf + gap;
32060
+ shelf = 0;
32061
+ }
32062
+ places.set(box.id, [x, y]);
32063
+ x += box.w + gap + int(random, 0, gap);
32064
+ shelf = Math.max(shelf, box.h);
32065
+ }
32066
+ for (const box of boxes) {
32067
+ const [px, py] = places.get(box.id);
32068
+ b.node(box.id, px, py, box.w, box.h);
32069
+ }
32070
+ const ids = boxes.map((x) => x.id);
32071
+ const large = ids.slice(0, v["large"]);
32072
+ const tries = Math.round(v["edgesPerNode"] * ids.length);
32073
+ for (let n = 0; n < tries; n++) {
32074
+ const s = ids[int(random, 0, ids.length - 1)];
32075
+ const t = random() < v["toLarge"] ? large[int(random, 0, large.length - 1)] : ids[int(random, 0, ids.length - 1)];
32076
+ b.link({ node: s }, { node: t });
32077
+ }
32078
+ return b.graph();
32079
+ }
32080
+ });
32081
+ /** The family `thin-bars`: long thin bars in lanes, as a timeline or the rows of a table. The links join bars of lanes near each other. */
32082
+ const THIN_BARS = caseFamily({
32083
+ name: "thin-bars",
32084
+ meaning: "Long thin boxes in lanes, as a timeline or the rows of a table.",
32085
+ dimensions: Object.freeze([
32086
+ count("lanes", 2, 40, 16, "The number of lanes. Each lane is a row of a timeline."),
32087
+ count("bars", 1, 12, 5, "The number of bars in each lane."),
32088
+ count("links", 0, 240, 64, "The number of tries for a link between two bars of lanes near each other."),
32089
+ count("thick", 4, 32, 12, "The height of each bar, in units."),
32090
+ count("gap", 4, 80, 16, "The free space between two lanes, in units.")
32091
+ ]),
32092
+ generator: "family.thin-bars",
32093
+ seeding: SEEDING,
32094
+ make: (v, _seed, random) => {
32095
+ const b = new Builder();
32096
+ const lanes = v["lanes"];
32097
+ const bars = v["bars"];
32098
+ const thick = v["thick"];
32099
+ for (let i = 0; i < lanes; i++) {
32100
+ let x = int(random, 0, 80);
32101
+ for (let j = 0; j < bars; j++) {
32102
+ const w = int(random, 120, 420);
32103
+ b.node(`t${i}-${j}`, x, i * (thick + v["gap"]), w, thick);
32104
+ x += w + int(random, 24, 120);
32105
+ }
32106
+ }
32107
+ for (let n = 0; n < v["links"]; n++) {
32108
+ const a = int(random, 0, lanes - 1);
32109
+ const c = Math.max(0, Math.min(lanes - 1, a + int(random, -3, 3)));
32110
+ b.link({ node: `t${a}-${int(random, 0, bars - 1)}` }, { node: `t${c}-${int(random, 0, bars - 1)}` });
32111
+ }
32112
+ return b.graph();
32113
+ }
32114
+ });
32115
+ /** The family `sparse-long`: few nodes far apart on a large board, and edges that go to far targets. */
32116
+ const SPARSE_LONG = caseFamily({
32117
+ name: "sparse-long",
32118
+ meaning: "A sparse board: few nodes far apart, with long edges across the board.",
32119
+ dimensions: Object.freeze([
32120
+ count("nodes", 4, 120, 36, "The number of nodes."),
32121
+ count("edges", 0, 160, 36, "The number of tries for a long edge."),
32122
+ count("spacing", 200, 1200, 480, "The size of the square cell of each node, in units."),
32123
+ count("far", 1, 8, 4, "The number of random targets of an edge. The edge takes the target farthest from its source.")
32124
+ ]),
32125
+ generator: "family.sparse-long",
32126
+ seeding: SEEDING,
32127
+ make: (v, _seed, random) => {
32128
+ const b = new Builder();
32129
+ const n = v["nodes"];
32130
+ const spacing = v["spacing"];
32131
+ const cols = ceilSqrt(n);
32132
+ for (let i = 0; i < n; i++) {
32133
+ const w = int(random, 80, 120);
32134
+ const h = int(random, 32, 56);
32135
+ b.node(`n${i}`, i % cols * spacing + int(random, 0, spacing - w - 40), Math.floor(i / cols) * spacing + int(random, 0, spacing - h - 40), w, h);
32136
+ }
32137
+ const dist = (i, j) => Math.abs(i % cols - j % cols) + Math.abs(Math.floor(i / cols) - Math.floor(j / cols));
32138
+ for (let e = 0; e < v["edges"]; e++) {
32139
+ const s = int(random, 0, n - 1);
32140
+ let best = -1;
32141
+ let far = -1;
32142
+ for (let f = 0; f < v["far"]; f++) {
32143
+ const t = int(random, 0, n - 1);
32144
+ if (t !== s && dist(s, t) > far) {
32145
+ best = t;
32146
+ far = dist(s, t);
32147
+ }
32148
+ }
32149
+ if (best >= 0) b.link({ node: `n${s}` }, { node: `n${best}` });
32150
+ }
32151
+ return b.graph();
32152
+ }
32153
+ });
32154
+ /** The family `local-mesh`: a grid of small nodes with many short edges, each to a node of the 8 nodes around it. */
32155
+ const LOCAL_MESH = caseFamily({
32156
+ name: "local-mesh",
32157
+ meaning: "Many short local edges: each node links to nodes next to it in a grid.",
32158
+ dimensions: Object.freeze([
32159
+ count("cols", 2, 40, 12, "The number of columns of nodes."),
32160
+ count("rows", 2, 30, 9, "The number of rows of nodes."),
32161
+ count("degree", 1, 6, 3, "The number of tries for an edge to one of the 8 nodes around a node, for each node."),
32162
+ count("gap", 8, 80, 20, "The free space between two nodes, in units."),
32163
+ count("jitter", 0, 40, 8, "The largest random move of a node in its cell, in units. The move stays 8 units or more below the gap.")
32164
+ ]),
32165
+ generator: "family.local-mesh",
32166
+ seeding: SEEDING,
32167
+ make: (v, _seed, random) => {
32168
+ const b = new Builder();
32169
+ const cols = v["cols"];
32170
+ const rows = v["rows"];
32171
+ const gap = v["gap"];
32172
+ const move = Math.min(v["jitter"], gap - 8);
32173
+ const id = (r, c) => `m${r}-${c}`;
32174
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * (48 + gap) + int(random, 0, move), r * (28 + gap) + int(random, 0, move), 48, 28);
32175
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) for (let t = 0; t < v["degree"]; t++) {
32176
+ const rr = r + int(random, -1, 1);
32177
+ const cc = c + int(random, -1, 1);
32178
+ if (rr >= 0 && rr < rows && cc >= 0 && cc < cols) b.link({ node: id(r, c) }, { node: id(rr, cc) });
32179
+ }
32180
+ return b.graph();
32181
+ }
32182
+ });
32183
+ /** The values of the dimension `layout` of `bipartite`. */
32184
+ const BIPARTITE_LAYOUTS = Object.freeze(["rows", "mixed"]);
32185
+ /**
32186
+ * The family `bipartite`: a complete bipartite part, with dense crossings that no plan can remove, and other nodes with random links.
32187
+ * At 3 nodes or more on each side, the part has no drawing without crossings.
32188
+ */
32189
+ const BIPARTITE = caseFamily({
32190
+ name: "bipartite",
32191
+ meaning: "An adversarial board: a complete bipartite part whose crossings no plan can remove.",
32192
+ dimensions: Object.freeze([
32193
+ count("left", 2, 16, 6, "The number of nodes of the first side of the complete bipartite part."),
32194
+ count("right", 2, 16, 6, "The number of nodes of the second side. Each node of the first side links to each node of the second side."),
32195
+ count("noise", 0, 120, 24, "The number of other nodes, in a band below the bipartite part."),
32196
+ count("extra", 0, 160, 24, "The number of tries for a random link between two nodes of the board."),
32197
+ enumDimension("layout", BIPARTITE_LAYOUTS, "rows", "The places of the bipartite part. `rows`: each side in one row, one row above the other. `mixed`: all the nodes at random cells of one grid."),
32198
+ count("gap", 16, 160, 64, "The free space between two nodes, in units.")
32199
+ ]),
32200
+ generator: "family.bipartite",
32201
+ seeding: SEEDING,
32202
+ make: (v, _seed, random, dims) => {
32203
+ const b = new Builder();
32204
+ const left = v["left"];
32205
+ const right = v["right"];
32206
+ const noise = v["noise"];
32207
+ const gap = v["gap"];
32208
+ const cw = 80 + gap;
32209
+ const ch = 36 + gap;
32210
+ const ids = [
32211
+ ...Array.from({ length: left }, (_, i) => `a${i}`),
32212
+ ...Array.from({ length: right }, (_, j) => `b${j}`),
32213
+ ...Array.from({ length: noise }, (_, k) => `n${k}`)
32214
+ ];
32215
+ if (dims["layout"] === "mixed") {
32216
+ const cols = ceilSqrt(ids.length + Math.floor(ids.length / 4));
32217
+ const cells = choose(random, cols * cols, ids.length);
32218
+ const order = shuffle(random, ids.length);
32219
+ ids.forEach((id, i) => {
32220
+ const c = cells[order[i]];
32221
+ b.node(id, c % cols * cw, Math.floor(c / cols) * ch, 80, 36);
32222
+ });
32223
+ } else {
32224
+ const cols = Math.max(left, right, ceilSqrt(noise));
32225
+ for (let i = 0; i < left; i++) b.node(`a${i}`, (i + Math.floor((cols - left) / 2)) * cw, 0, 80, 36);
32226
+ for (let j = 0; j < right; j++) b.node(`b${j}`, (j + Math.floor((cols - right) / 2)) * cw, 3 * ch, 80, 36);
32227
+ choose(random, cols * Math.max(1, Math.floor((noise + Math.floor(noise / 4) + cols - 1) / cols)), noise).forEach((c, k) => b.node(`n${k}`, c % cols * cw, (5 + Math.floor(c / cols)) * ch, 80, 36));
32228
+ }
32229
+ for (let i = 0; i < left; i++) for (let j = 0; j < right; j++) b.link({ node: `a${i}` }, { node: `b${j}` });
32230
+ for (let n = 0; n < v["extra"]; n++) b.link({ node: ids[int(random, 0, ids.length - 1)] }, { node: ids[int(random, 0, ids.length - 1)] });
32231
+ return b.graph();
32232
+ }
32233
+ });
32234
+ /** The 9 families of CORPUS-FAMILIES, in the order of `screen-v3`. */
32235
+ const COVERAGE_FAMILIES = Object.freeze([
32236
+ FRAME_ROWS,
32237
+ PIPELINE,
32238
+ FAN_OUT,
32239
+ TWO_HUBS,
32240
+ UNEVEN_SIZES,
32241
+ THIN_BARS,
32242
+ SPARSE_LONG,
32243
+ LOCAL_MESH,
32244
+ BIPARTITE
32245
+ ]);
29762
32246
  // src/families/fixtures.ts
29763
32247
  /** The values of the dimension `layout`: each `FixtureKind`. */
29764
32248
  const FIXTURE_LAYOUTS = Object.freeze([
@@ -29885,6 +32369,7 @@ const DENSE_GROUPS = fixtureFamily("dense-groups", "Dense clusters whose groups
29885
32369
  // src/families/index.ts
29886
32370
  /**
29887
32371
  * The corpus families of highways, in the order of the screen sets. Each family at its defaults makes the board of its screen family.
32372
+ * The first 6 families are the families of `screen-v1` and `screen-v2`. The 9 families of `COVERAGE_FAMILIES` follow them (CORPUS-FAMILIES).
29888
32373
  * An import registers nothing. A host makes a registry with `createFamilyRegistry(HIGHWAYS_FAMILIES)`, or with the lists of other engines too.
29889
32374
  */
29890
32375
  const HIGHWAYS_FAMILIES = Object.freeze([
@@ -29893,7 +32378,8 @@ const HIGHWAYS_FAMILIES = Object.freeze([
29893
32378
  DENSE_GROUPS,
29894
32379
  HUB_CHAIN,
29895
32380
  MANY_GROUPS,
29896
- PACKED_GRID
32381
+ PACKED_GRID,
32382
+ ...COVERAGE_FAMILIES
29897
32383
  ]);
29898
32384
  /** This function gives a new registry of `HIGHWAYS_FAMILIES`. */
29899
32385
  function highwaysRegistry() {
@@ -30159,13 +32645,130 @@ const SCREEN_V2 = {
30159
32645
  } },
30160
32646
  heldOut: SCREEN_V2_HELD_OUT
30161
32647
  };
32648
+ /** This function gives the 9 families of CORPUS-FAMILIES, each with `seeds` members. Their base seeds are the primes from 13 to 43. */
32649
+ function coverageFamilies(seeds) {
32650
+ return [
32651
+ registryFamily("frame-rows", 13, "Cards with full-width rows and fixed plugs on their walls.", seeds, {
32652
+ ...BASE,
32653
+ count: "100",
32654
+ edgeDensity: "sparse",
32655
+ edgeShape: "random",
32656
+ groups: "nested",
32657
+ ports: "fixed"
32658
+ }),
32659
+ registryFamily("pipeline", 17, "A long chain of stages in rows that turn.", seeds, {
32660
+ ...BASE,
32661
+ count: "100",
32662
+ edgeDensity: "sparse",
32663
+ edgeShape: "tree",
32664
+ ports: "free"
32665
+ }),
32666
+ registryFamily("fan-out", 19, "A wide fan-out from one hub to the rings around it.", seeds, {
32667
+ ...BASE,
32668
+ count: "100",
32669
+ edgeDensity: "medium",
32670
+ edgeShape: "bundled",
32671
+ ports: "free"
32672
+ }),
32673
+ registryFamily("two-hubs", 23, "Two hubs with many shared targets.", seeds, {
32674
+ ...BASE,
32675
+ count: "100",
32676
+ edgeDensity: "medium",
32677
+ edgeShape: "bundled",
32678
+ ports: "free"
32679
+ }),
32680
+ registryFamily("uneven-sizes", 29, "A few large boxes among many small boxes.", seeds, {
32681
+ ...BASE,
32682
+ sizes: "bimodal",
32683
+ count: "100",
32684
+ edgeDensity: "medium",
32685
+ edgeShape: "random",
32686
+ ports: "free"
32687
+ }),
32688
+ registryFamily("thin-bars", 31, "Long thin bars in lanes, as a timeline.", seeds, {
32689
+ ...BASE,
32690
+ sizes: "extremeAspect",
32691
+ count: "100",
32692
+ edgeDensity: "medium",
32693
+ edgeShape: "random",
32694
+ ports: "free"
32695
+ }),
32696
+ registryFamily("sparse-long", 37, "Few nodes far apart, with long edges.", seeds, {
32697
+ ...BASE,
32698
+ count: "10",
32699
+ edgeDensity: "sparse",
32700
+ edgeShape: "longRange",
32701
+ ports: "free"
32702
+ }),
32703
+ registryFamily("local-mesh", 41, "Many short edges between nodes next to each other.", seeds, {
32704
+ ...BASE,
32705
+ count: "100",
32706
+ edgeDensity: "dense",
32707
+ edgeShape: "clustered",
32708
+ ports: "free"
32709
+ }),
32710
+ registryFamily("bipartite", 43, "A complete bipartite part: crossings that no plan removes.", seeds, {
32711
+ ...BASE,
32712
+ count: "10",
32713
+ edgeDensity: "dense",
32714
+ edgeShape: "random",
32715
+ ports: "free"
32716
+ })
32717
+ ];
32718
+ }
32719
+ /** The member seeds of each family of `screen-v3` that the split `default` holds out. */
32720
+ const SCREEN_V3_HELD_OUT = [
32721
+ 7,
32722
+ 8,
32723
+ 9
32724
+ ];
32725
+ const V3_FAMILIES = [...screenFamilies(10, 10), ...coverageFamilies(10)];
32726
+ /**
32727
+ * The screen set `screen-v3` (CORPUS-FAMILIES): the 15 families of `HIGHWAYS_FAMILIES`, each with the 10 members 0 to 9, thus 150 boards.
32728
+ * The first 6 families are the families of `screen-v2` with their seeds. Thus their boards are the boards of `screen-v2`, under the ids of `screen-v3`.
32729
+ * The 9 new families cover frames with rows, a long chain, a wide fan-out and two hubs.
32730
+ * They also cover uneven sizes, thin bars, long edges, short edges and a complete bipartite part.
32731
+ *
32732
+ * The manifest declares 10 inputs for each family. The split `default` holds out the members 7 to 9 of each family.
32733
+ * This set does not change. A new family goes into a new set. The tests compare it with the files in `corpus/screen-v3`.
32734
+ */
32735
+ const SCREEN_V3 = {
32736
+ id: "screen-v3",
32737
+ families: V3_FAMILIES,
32738
+ claims: { declared: {
32739
+ minCount: 10,
32740
+ thin: [],
32741
+ counts: Object.fromEntries(V3_FAMILIES.map((f) => [f.name, f.seeds])),
32742
+ familyBy: SCREEN_FAMILY_BY,
32743
+ familyMin: 10
32744
+ } },
32745
+ heldOut: SCREEN_V3_HELD_OUT
32746
+ };
32747
+ /**
32748
+ * This function gives the power of the sign test over `families` families at the level `level`.
32749
+ * The power is the least count of families on one side that passes.
32750
+ * A family set without ties and with that count or more on one side passes the two-sided exact sign test, with the p below `level`.
32751
+ * The function gives `undefined` when no count passes, for example for 5 families or fewer at the level 0.05.
32752
+ */
32753
+ function signTestPower(families, level = .05) {
32754
+ for (let k = Math.ceil(families / 2); k <= families; k++) {
32755
+ let tail = 0;
32756
+ let c = 1;
32757
+ for (let i = 0; i <= families - k; i++) {
32758
+ tail += c;
32759
+ c = c * (families - i) / (i + 1);
32760
+ }
32761
+ if (Math.min(1, 2 * tail / 2 ** families) < level) return k;
32762
+ }
32763
+ }
30162
32764
  /**
30163
- * The screen sets by id. `screen-v1` and its larger version `screen-v2` (CORPUS-SEEDS) have the boards of the screen.
32765
+ * The screen sets by id. `screen-v1`, its larger version `screen-v2` (CORPUS-SEEDS) and `screen-v3` with 15 families (CORPUS-FAMILIES) have the boards of the screen.
30164
32766
  * `exact-v1` has small boards with a known optimum (MEAS2). `exact-v1-bounds` has larger boards with bounds (EXACT-BOUNDS).
30165
32767
  */
30166
32768
  const SCREEN_SETS = {
30167
32769
  [SCREEN_V1.id]: SCREEN_V1,
30168
32770
  [SCREEN_V2.id]: SCREEN_V2,
32771
+ [SCREEN_V3.id]: SCREEN_V3,
30169
32772
  [EXACT_V1.id]: EXACT_V1,
30170
32773
  [EXACT_V1_BOUNDS.id]: EXACT_V1_BOUNDS
30171
32774
  };
@@ -30979,7 +33582,9 @@ const MOVE_NAMES = Object.freeze({
30979
33582
  gaps: "gap",
30980
33583
  streets: "street",
30981
33584
  search: "searched",
30982
- interchanges: "networkRoad"
33585
+ interchanges: "networkRoad",
33586
+ knee: "tree",
33587
+ bronchial: "tree"
30983
33588
  });
30984
33589
  /**
30985
33590
  * The highway strategies that change the plan of another strategy (`from`). Their own move is a change of that plan, so they count no move.
@@ -33194,6 +35799,6 @@ const sharedStretchStrategy = defineStrategy({
33194
35799
  name: "shared-stretch"
33195
35800
  }).route(graph)
33196
35801
  });
33197
- export { AMBIGUITY_GUARD_DEFAULTS, ARC_SOURCES, AREA_DEFAULTS, AREA_WIDTHS, ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, BACKTRACK_MIN, BACKTRACK_SHARE, BREAK, BREAK_EVEN, BREAK_EVEN_DEFAULTS, BUILT_IN_STRATEGIES, BoundaryWalk, CANDIDATE_SOURCES, CAP_SWEEP, CASES, CHANNEL_REACH, CLEARANCE_CAP, COMPARE_ALL_STEPS, COMPARE_STEPS, CONFLUENT_DEFAULTS, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, CORRIDOR_DEFAULTS, CROSSBAR_DEFAULTS, CUBU_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, DEFAULT_RNG, DENSE_GROUPS, DENSITY_KERNELS, DIFFUSION_DEFAULTS, DISTORTION_NEIGHBOURS, DIVIDED_DEFAULTS, EDGE_DISTANCES, EDGE_PATH_DEFAULTS, EXACT_CLASSES, EXACT_EDGES_PER_NODE, EXACT_V1, EXACT_V1_BOUNDS, EXACT_V1_BOUNDS_SIZES, EXACT_V1_SEEDS, EXACT_V1_SIZES, FDEB_DEFAULTS, FIXTURES, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, FLOW_CLUSTER_DEFAULTS, FLOW_FIELD_DEFAULTS, FRECHET_DEFAULTS, FRONT_OBJECTIVES, GAP_BASELINES, GAP_BOOTSTRAP, GAP_CAPS, GAP_DEFAULTS, GAP_RULES, GAP_VIEWS, GROUP_SCOPES, GROUP_SPINES, GROWTH_DEFAULTS, GROWTH_SWEEP, Grid, HDBSCAN_DEFAULTS, HIERARCHY_DEFAULTS, HIERARCHY_GROUPS, HIGHWAYS_ENGINE, HIGHWAYS_FAMILIES, HIGHWAY_BASES, HIGHWAY_BASE_ROUTERS, HOUGH_DEFAULTS, HUB_ALLOCATIONS, HUB_ARC_DEFAULTS, HUB_CANDIDATES, HUB_CHAIN, HYSTERESIS_DEFAULTS, INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, KDEEB_DEFAULTS, KD_DEFAULTS, KENDALL_EXACT, LANES_DEFAULTS, LANE_ORDERS, LANE_STRETCH_DEFAULTS, LANE_STRETCH_ORDERS, LANE_TRACKS, LEVEL_HIGHWAY_DEFAULTS, LINKAGES, LINK_COMMUNITY_DEFAULTS, MANY_GROUPS, MEASURES, MEASURE_LANE_WIDTH, MEET_DEFAULTS, MEMBER_FORMS, MESH_DEFAULTS, MINGLE_DEFAULTS, MIN_EFFECTS, MLSEB_DEFAULTS, MLS_FITS, MODULE_RULES, MOVE_NAMES, NESTED_DEFAULTS, OD_CELL_DEFAULTS, OUTSIDE_SLACK, Obstacles, PACKED_GRID, PICK, PLAN_CHANGERS, PORTFOLIO_B1, PORTFOLIO_BANDS, PORTFOLIO_BASE_MEMBER, PORTFOLIO_CAPS, PORTFOLIO_FIT, PORTFOLIO_HASHES, PORTFOLIO_KS_API, PORTFOLIO_RULES, PORTFOLIO_SETS, POWER_DEFAULTS, PROPOSED_RULES, P_HUB_DEFAULTS, QUICKBUNDLES_DEFAULTS, RECORD_EPS, RESISTANCE_DEFAULTS, RIDGE_DEFAULTS, ROAD_CLASS_DEFAULTS, ROOM_FAMILIES, ROUTED_STEPS, ROUTERS, SAME_HIGHWAY, SBEB_DEFAULTS, SCALE_SPACE_DEFAULTS, SCREEN_FAMILY_BY, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SCREEN_V2, SCREEN_V2_HELD_OUT, SCREEN_WRAPS, SEARCH_DEFAULTS, SEARCH_MOVES, SECTION_DEFAULTS, SELECT_DEFAULTS, SIDE_NORMAL, SKELETON_GROUPS, SKELETON_TARGETS, SLACK_SWEEP, SMALL_CLUSTERED, SOFT_KEY_DEFAULTS, SPACING_LIMIT, SPECTRAL_DEFAULTS, SPIRAL_DEFAULTS, SPREAD_DEFAULTS, STABLE_ID_DEFAULTS, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, STEPS, STRATEGIES, STREET_DEFAULTS, STREET_SOURCES, STRETCH_DEFAULTS, STRETCH_SWEEP, STUB_DEFAULTS, SUITE_COLUMNS, SoftGraph, SplitTree, TERRITORY_DEFAULTS, TERRITORY_POINTS, TINY_UNIFORM, TOLERANCE, TRACLUS_DEFAULTS, TRAFFIC_RULES, WEISZFELD_DEFAULTS, WSPD_DEFAULTS, WayFinder, ZIPPER_DEFAULTS, activationBase, activationReport, adjustedRandIndex, allSections, ambiguity, ambiguityGate, ambiguityPlan, arcCurve, areaPlan, asRouter, attractionRouter, attractionStrategy, backtrackOf, balance, basePlan, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, benchTracks, bendCount, bendingEnergy, bicliqueGroups, bigGroups, boardGaps, boardUnit, boardWins, bootstrapMedian, boundsFamilyName, boxDisplacement, boxExit, boxPush, boxUnit, boxesOverlap, breakEvenGate, breakEvenPlan, buildSections, bundleStretch, bundledShare, byCollection, byDataGroup, candidateArcs, candidatePlan, candidateScore, canonicalJson, capSettings, caseSeed, cellSteps, center, chainClusters, changeGraph, changedDimensions, channelOverflows, channelWidth, characteristicPoints, checkFamilies, checkLevels, checkOptions, checkParents, checkPlan, checkPort, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, clusters1d, coAssociation, commonEdges, compareAnyStep, compareMeasure, compareSpec, compareStep, compatibility, compatibilityMatrix, confluentPlan, consensusGroups, consensusLabels, consensusPlan, controlChain, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createFamilyRegistry, createRegistry, crossbarPlan, crossesItself, crossingAngle, crossingAngles, cubicAt, cubuPlan, curvatureVariation, curveDeviation, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, curvesHash, cutGroups, cutShare, declaresFamily, defineStrategy, denseGroupsBoard, densityPlan, densityRidges, depthLevels, detourSpread, diffusionPlan, dimensionAt, dimensionUnit, dist, distanceDistortion, distinctPoints, divideHighway, dividedPlan, documentHash, dominates, drawnSegments, edgeBlocks, edgeEnds, edgeLengths, edgePathPlan, edgePaths, edgePlugs, edgeResistance, enumDimension, exactBoard, exactFamily, exactGenerator, exactSeed, exactStretch, exactStretches, exercised, facingEnds, familyCell, familyKind, familyOf, familySeed, farLevel, farLinks, fdebLines, fdebPlan, fft, findStrategy, finderOf, firstLines, fixPorts, fixedPortPoint, fixtureGroupOptions, fixtureParams, fixtureRngSeed, fixtureSpecOf, flowClusterPlan, flowClusters, flowDistance, flowFieldPlan, fnv1a32, follow, formatActivation, formatBendRoom, formatCapped, formatCurves, formatFaults, formatFronts, formatGap, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatPaired, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, formatTracks, frameRoutes, frechet, frechetBundles, frechetPlan, freeLines, g1Defect, g2Defect, gapBaselines, gapCap, gapClosed, gapLaneCounts, gapLanes, gapNames, gapPlan, gapValues, gateStrategy, getStrategy, goesOutAndBack, graphInput, greedyBicliques, greedyCrossbars, groupEdges, groupFill, groupRun, groupSettings, groupStability, groupStrategies, groupedPlan, growthPlan, growthSettings, guardBreaks, hasCusp, hdbscanClusters, hdbscanLabels, hdbscanPlan, hierarchyPlan, highwayChurn, highwayCurve, highwayCurves, highwayEdits, highwayIdKeep, highwayMoves, highwayRouter, highwayRouterStrategy, highwayRouters, highwayStrategies, highwaysRegistry, houghLines, houghPeaks, houghPlan, hubArcPlan, hubCandidates, hubChainBoard, hysteresisGate, hysteresisPlan, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, innerPoints, inputGraph, inputId, interchangeCandidates, interchangePlan, isRoutedStep, itemOverlaps, itemSizes, jaccard, jaccardIndex, jogPeaks, joinReports, jsonSafe, kMeans, kdGroups, kdeebLines, kdeebPlan, keepMembers, kendallTau, keyGroups, keyedSections, keysPlan, labelGroups, laneArea, laneCrossings, laneFidelity, laneOrder, laneRoutes, laneSeparation, laneSettings, laneStretches, lanesPlan, lanesThrough, leafGroups, lengthBetween, levelCut, levelGrouping, levelHighwayPlan, levelMeasures, levelPairs, levelRatio, levelRoom, levelStrategies, levelsOf, lineDensity, linePoint, linkAgreement, linkCommunities, linkCommunityGroups, linkCommunityPlan, linkDensity, listOptions, listStrategies, makeFixture, makeRoom, manifestHash, manyGroupsBoard, matchHighways, maxStretch, mdf, meanLine, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, meetMember, meetPlan, meetingPoints, memberAt, memberStrategy, mergeAngle, meshPlan, meshWaypoints, minEffectOf, minHash, mingleBundles, minglePlan, mixedSegments, mixesCollections, mlsPoint, mlsebLines, mlsebPlan, modularity, modulesOf, mulberry32, namedObjectives, nearestCentre, nearestOnBoundary, nearestOnLine, nearestSide, nearestSkeleton, nestedGridBoard, nestedGroups, nestedPlan, nestingFaults, nestingViolations, nextRow, nodeAmbiguity, nodeIndex, nodeLinks, numberDimension, numberOf, occupiedArea, odCellPlan, odGroups, offsetLine, offsetPolyline, oneSteiner, openGaps, openLanes, optimaFile, optimumKey, optionDefaults, orderChanges, orderFragility, orderLanes, orderRows, orderStability, orderSuiteRows, orthogonality, outOfBoxes, outerGraph, outputGroups, outputHash, pHubPlan, pMedian, packedGridBoard, paramProblem, paretoFront, parseLines, partWithin, passSettings, pathPoints, peakCurvature, pickLevel, pickMoves, placePlug, planMoves, planOfResult, plugSpacing, plugStrategies, plugsRouter, pointAt, pointBoxDistance, pointDensity, pointGroups, pointOnSide, pointSegmentDistance, polylineLength, portOffset, portfolio, portfolioB1Spec, portfolioBand, portfolioFacts, portfolioKey, portfolioMember, portfolioMembers, portfolioName, portfolioRouter, portfolioStrategy, positionStability, powerPlan, pullApart, quadtreeGroups, quantile, quickBundles, quickBundlesPlan, randomInt, rayToBox, recordLines, recordMoves, rectScattered, regionBounds, register, repairResult, repairStrategy, representative, resample, resistanceClusters, resistancePlan, resolvePorts, resultForm, resultHash, ridgeLines, ridgePlan, rng, roadClassPlan, roomDemand, roomLinks, roomOrderFaults, roomStrategies, roomValues, round3, roundCorners, roundedCurve, routeCrossings, routeInStableOrder, routeMeasures, routePortfolio, routeSeparation, routeStability, routeSymmetry, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runPipeline, runProfile, runSuite, sameData, sampleCurve, samplesOf, sbebLines, sbebPlan, scaleClusters, scaleSpaceGroups, scaleSpacePlan, screen, screenInputs, screenSplit, searchPlan, searchScore, sectionsRouter, segmentBounds, segmentBoxDistance, segmentClusters, segmentDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, selectBest, selectPlan, setManifest, sha256Hex, shapeSkeleton, sharedRouter, sharedStretchStrategy, sharedStretches, sharedWaysAll, sharedWaysPlan, shuffleEdges, sideAngle, sideToward, silhouette, skeletonGroups, skeletonPath, slackSettings, smoothCurve, snapPlan, softDistance, softKeyClusters, softKeyPlan, spanningTree, spectralClusters, spectralLabels, spectralPlan, spinePart, spiralJoin, spiralPlan, spiralTree, splineCurve, splitFrames, splitPath, splitPoint, spreadEnds, stableIdGate, stableIdPlan, stableOrder, stableOrderStrategy, steinerPlan, stepRouterStrategy, stepRouters, stepSeed, straightCurve, straightRouter, strategyKey, streetEdges, streetPlan, streetPoint, streetRide, stretchGate, stretchLaneOrders, stretchPlan, stretchSettings, stubPlan, suiteFronts, sumFaults, summarize, summarizeGap, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, symmetricEigen, territoryPlan, toSvgPath, totalTurn, tracePlan, traclusPlan, treeCost, treeDepth, trunksRouter, turns, unwrappedName, utf8, waypointWay, weberPoint, weiszfeldPlan, wholeBoard, wholeStrategies, withBends, withRepair, withStableOrder, wspdGroups, wspdPlan, zipLegs, zipperPlan };
35802
+ export { AMBIGUITY_GUARD_DEFAULTS, ANNEALING_DEFAULTS, ANNEALING_SCHEMA, ARC_SOURCES, AREA_DEFAULTS, AREA_WIDTHS, ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, BACKTRACK_MIN, BACKTRACK_SHARE, BIPARTITE, BIPARTITE_LAYOUTS, BOARD_PORTFOLIO_DEFAULTS, BOARD_PORTFOLIO_SETTINGS, BREAK, BREAK_EVEN, BREAK_EVEN_DEFAULTS, BRONCHIAL_DEFAULTS, BRONCHIAL_SPLITS, BUILT_IN_STRATEGIES, BoundaryWalk, CANDIDATE_SOURCES, CAP_SWEEP, CASES, CHANNEL_REACH, CLEARANCE_CAP, COMPARE_ALL_STEPS, COMPARE_STEPS, CONFLUENT_DEFAULTS, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, CORRIDOR_DEFAULTS, COVERAGE_FAMILIES, CROSSBAR_DEFAULTS, CUBU_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, DEFAULT_RNG, DENSE_GROUPS, DENSITY_KERNELS, DIFFUSION_DEFAULTS, DISTORTION_NEIGHBOURS, DIVIDED_DEFAULTS, EDGE_DISTANCES, EDGE_PATH_DEFAULTS, EXACT_CLASSES, EXACT_EDGES_PER_NODE, EXACT_V1, EXACT_V1_BOUNDS, EXACT_V1_BOUNDS_SIZES, EXACT_V1_SEEDS, EXACT_V1_SIZES, FAN_OUT, FDEB_DEFAULTS, FIXTURES, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, FLOW_CLUSTER_DEFAULTS, FLOW_FIELD_DEFAULTS, FRAME_ROWS, FRECHET_DEFAULTS, FRONT_OBJECTIVES, GAP_BASELINES, GAP_BOOTSTRAP, GAP_CAPS, GAP_DEFAULTS, GAP_RULES, GAP_VIEWS, GROUP_SCOPES, GROUP_SPINES, GROWTH_DEFAULTS, GROWTH_SWEEP, GUTTER_BEND_COST, Grid, HDBSCAN_DEFAULTS, HIERARCHY_DEFAULTS, HIERARCHY_GROUPS, HIGHWAYS_ENGINE, HIGHWAYS_FAMILIES, HIGHWAY_BASES, HIGHWAY_BASE_ROUTERS, HOUGH_DEFAULTS, HUB_ALLOCATIONS, HUB_ARC_DEFAULTS, HUB_CANDIDATES, HUB_CHAIN, HYSTERESIS_DEFAULTS, INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, KDEEB_DEFAULTS, KD_DEFAULTS, KENDALL_EXACT, KNEE_DEFAULTS, KNEE_RULES, LANES_DEFAULTS, LANE_ORDERS, LANE_STRETCH_DEFAULTS, LANE_STRETCH_ORDERS, LANE_TRACKS, LEVEL_HIGHWAY_DEFAULTS, LINKAGES, LINK_COMMUNITY_DEFAULTS, LOCAL_MESH, MANY_GROUPS, MEASURES, MEASURE_LANE_WIDTH, MEET_DEFAULTS, MEMBER_FORMS, MESH_DEFAULTS, MINGLE_DEFAULTS, MIN_EFFECTS, MLSEB_DEFAULTS, MLS_FITS, MODULE_RULES, MOVE_NAMES, NESTED_DEFAULTS, OD_CELL_DEFAULTS, OUTSIDE_SLACK, Obstacles, PACKED_GRID, PICK, PIPELINE, PLAN_CHANGERS, PORTFOLIO_B1, PORTFOLIO_BANDS, PORTFOLIO_BASE_MEMBER, PORTFOLIO_CAPS, PORTFOLIO_FIT, PORTFOLIO_HASHES, PORTFOLIO_KS_API, PORTFOLIO_RULES, PORTFOLIO_SETS, POWER_DEFAULTS, PROPOSED_RULES, P_HUB_DEFAULTS, QUICKBUNDLES_DEFAULTS, RECORD_EPS, RESISTANCE_DEFAULTS, RIDGE_DEFAULTS, ROAD_CLASS_DEFAULTS, ROOM_FAMILIES, ROUTED_STEPS, ROUTERS, SAME_HIGHWAY, SBEB_DEFAULTS, SCALE_SPACE_DEFAULTS, SCREEN_FAMILY_BY, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SCREEN_V2, SCREEN_V2_HELD_OUT, SCREEN_V3, SCREEN_V3_HELD_OUT, SCREEN_WRAPS, SEARCH_DEFAULTS, SEARCH_MOVES, SECTION_DEFAULTS, SELECT_DEFAULTS, SIDE_NORMAL, SKELETON_GROUPS, SKELETON_TARGETS, SLACK_SWEEP, SMALL_CLUSTERED, SOFT_KEY_DEFAULTS, SPACING_LIMIT, SPARSE_LONG, SPECTRAL_DEFAULTS, SPIRAL_DEFAULTS, SPREAD_DEFAULTS, STABLE_ID_DEFAULTS, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, STEPS, STRATEGIES, STREET_DEFAULTS, STREET_SOURCES, STRETCH_DEFAULTS, STRETCH_SWEEP, STUB_DEFAULTS, SUITE_COLUMNS, SoftGraph, SplitTree, TERRITORY_DEFAULTS, TERRITORY_POINTS, THIN_BARS, TIGHT_DEFAULTS, TIGHT_FIRSTS, TIGHT_SCHEMA, TIGHT_TIES, TINY_UNIFORM, TOLERANCE, TRACLUS_DEFAULTS, TRAFFIC_RULES, TWO_HUBS, UNEVEN_SIZES, VENATION_BACKBONES, VENATION_DEFAULTS, WEISZFELD_DEFAULTS, WSPD_DEFAULTS, WayFinder, ZIPPER_DEFAULTS, activationBase, activationReport, adjustedRandIndex, allSections, ambiguity, ambiguityGate, ambiguityPlan, annealWays, annealingRouter, annealingStrategy, arcCurve, areaPlan, aroundBox, asRouter, attractionRouter, attractionStrategy, backboneLinks, backtrackOf, balance, basePlan, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, benchTracks, bendCount, bendingEnergy, bicliqueGroups, bigGroups, boardGaps, boardPortfolio, boardUnit, boardWins, bootstrapMedian, boundsFamilyName, boxDisplacement, boxExit, boxPush, boxUnit, boxesOverlap, breakEvenGate, breakEvenPlan, bronchialPlan, buildSections, bundleStretch, bundledShare, byCollection, byDataGroup, candidateArcs, candidatePlan, candidateScore, canonicalJson, capSettings, caseSeed, cellSteps, center, chainClusters, changeGraph, changedDimensions, channelOverflows, channelWidth, characteristicPoints, checkFamilies, checkLevels, checkOptions, checkParents, checkPlan, checkPort, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, clusters1d, coAssociation, commonEdges, compareAnyStep, compareMeasure, compareSpec, compareStep, compatibility, compatibilityMatrix, confluentPlan, consensusGroups, consensusLabels, consensusPlan, controlChain, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createFamilyRegistry, createRegistry, crossbarPlan, crossesItself, crossingAngle, crossingAngles, cubicAt, cubuPlan, curvatureVariation, curveDeviation, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, curvesHash, cutGroups, cutShare, declaresFamily, defineStrategy, denseGroupsBoard, densityPlan, densityRidges, depthLevels, detourSpread, diamondAngle, diffusionPlan, dimensionAt, dimensionUnit, dist, distanceDistortion, distinctPoints, divideHighway, dividedPlan, documentHash, dominates, drawnSegments, edgeBlocks, edgeEnds, edgeLengths, edgePathPlan, edgePaths, edgePlugs, edgeResistance, enumDimension, exactBoard, exactFamily, exactGenerator, exactSeed, exactStretch, exactStretches, exercised, facingEnds, familyCell, familyKind, familyOf, familySeed, farLevel, farLinks, fdebLines, fdebPlan, fft, findStrategy, finderOf, firstLines, fixPorts, fixedPortPoint, fixtureGroupOptions, fixtureParams, fixtureRngSeed, fixtureSpecOf, flowClusterPlan, flowClusters, flowDistance, flowFieldPlan, fnv1a32, follow, formatActivation, formatBendRoom, formatCapped, formatCurves, formatFaults, formatFronts, formatGap, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatPaired, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, formatTracks, frameRoutes, frechet, frechetBundles, frechetPlan, freeLines, g1Defect, g2Defect, gapBaselines, gapCap, gapClosed, gapLaneCounts, gapLanes, gapNames, gapPlan, gapValues, gateStrategy, getStrategy, goesOutAndBack, graphInput, greedyBicliques, greedyCrossbars, groupEdges, groupFill, groupRun, groupSettings, groupStability, groupStrategies, groupedPlan, growthPlan, growthSettings, guardBreaks, gutterRoutes, hasCusp, hdbscanClusters, hdbscanLabels, hdbscanPlan, hierarchyPlan, highwayChurn, highwayCurve, highwayCurves, highwayEdits, highwayIdKeep, highwayMoves, highwayRouter, highwayRouterStrategy, highwayRouters, highwayStrategies, highwaysRegistry, houghLines, houghPeaks, houghPlan, hubArcPlan, hubCandidates, hubChainBoard, hysteresisGate, hysteresisPlan, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, innerPoints, inputGraph, inputId, interchangeCandidates, interchangePlan, isRoutedStep, itemOverlaps, itemSizes, jaccard, jaccardIndex, jogPeaks, joinReports, jsonSafe, kMeans, kdGroups, kdeebLines, kdeebPlan, keepMembers, kendallTau, keyGroups, keyedSections, keysPlan, kneeOf, kneePlan, labelGroups, laneArea, laneCrossings, laneFidelity, laneOrder, laneRoutes, laneSeparation, laneSettings, laneStretches, lanesPlan, lanesThrough, leafGroups, lengthBetween, levelCut, levelGrouping, levelHighwayPlan, levelMeasures, levelPairs, levelRatio, levelRoom, levelStrategies, levelsOf, lineDensity, linePoint, linkAgreement, linkCommunities, linkCommunityGroups, linkCommunityPlan, linkDensity, listOptions, listStrategies, makeFixture, makeRoom, manifestHash, manyGroupsBoard, matchHighways, maxStretch, mdf, meanLine, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, meetMember, meetPlan, meetingPoints, memberAt, memberStrategy, mergeAngle, meshPlan, meshWaypoints, minEffectOf, minHash, mingleBundles, minglePlan, mixedSegments, mixesCollections, mlsPoint, mlsebLines, mlsebPlan, modularity, modulesOf, mulberry32, namedObjectives, nearestCentre, nearestOnBoundary, nearestOnLine, nearestSide, nearestSkeleton, nestedGridBoard, nestedGroups, nestedPlan, nestingFaults, nestingViolations, nextRow, nodeAmbiguity, nodeIndex, nodeLinks, numberDimension, numberOf, occupiedArea, odCellPlan, odGroups, offsetLine, offsetPolyline, oneSteiner, openGaps, openLanes, optimaFile, optimumKey, optionDefaults, orderChanges, orderFragility, orderLanes, orderRows, orderStability, orderSuiteRows, orthogonality, outOfBoxes, outerGraph, outputGroups, outputHash, pHubPlan, pMedian, packedGridBoard, paramProblem, paretoFront, parseLines, partWithin, passSettings, pathPoints, peakCurvature, pickLevel, pickMoves, placePlug, planMoves, planOfResult, plugSpacing, plugStrategies, plugsRouter, pointAt, pointBoxDistance, pointDensity, pointGroups, pointOnSide, pointSegmentDistance, polylineLength, portOffset, portfolio, portfolioB1Spec, portfolioBand, portfolioFacts, portfolioKey, portfolioMember, portfolioMembers, portfolioName, portfolioRouter, portfolioStrategy, positionStability, powerPlan, pullApart, quadtreeGroups, quantile, quickBundles, quickBundlesPlan, randomInt, rankShares, rayToBox, recordLines, recordMoves, rectScattered, regionBounds, register, repairResult, repairStrategy, representative, resample, resistanceClusters, resistancePlan, resolvePorts, resultForm, resultHash, ridgeLines, ridgePlan, rng, roadClassPlan, roomDemand, roomLinks, roomOrderFaults, roomStrategies, roomValues, round3, roundCorners, roundedCurve, routeCrossings, routeInStableOrder, routeMeasures, routePortfolio, routeSeparation, routeStability, routeSymmetry, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runPipeline, runProfile, runSuite, sameData, sampleCurve, samplesOf, sbebLines, sbebPlan, scaleClusters, scaleSpaceGroups, scaleSpacePlan, screen, screenInputs, screenSplit, searchPlan, searchScore, sectionsRouter, segmentBounds, segmentBoxDistance, segmentClusters, segmentDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, selectBest, selectPlan, setManifest, sha256Hex, shapeSkeleton, sharedRouter, sharedStretchStrategy, sharedStretches, sharedWaysAll, sharedWaysPlan, shuffleEdges, sideAngle, sideToward, signTestPower, silhouette, skeletonGroups, skeletonPath, slackSettings, smoothCurve, snapPlan, softDistance, softKeyClusters, softKeyPlan, spanningTree, spectralClusters, spectralLabels, spectralPlan, spinePart, spiralJoin, spiralPlan, spiralTree, splineCurve, splitFrames, splitPath, splitPoint, splitTargets, spreadEnds, stableIdGate, stableIdPlan, stableOrder, stableOrderStrategy, steinerPlan, stepRouterStrategy, stepRouters, stepSeed, straightCurve, straightRouter, strategyKey, streetEdges, streetPlan, streetPoint, streetRide, stretchGate, stretchLaneOrders, stretchPlan, stretchSettings, stubPlan, suiteFronts, sumFaults, summarize, summarizeGap, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, symmetricEigen, territoryPlan, tightRouter, tightStrategy, toSvgPath, totalTurn, tracePlan, traclusPlan, treeCost, treeDepth, trunksRouter, turns, unwrappedName, utf8, venationLevels, venationLinks, waySlacks, waypointWay, weberPoint, weiszfeldPlan, wholeBoard, wholeStrategies, withBends, withRepair, withStableOrder, wspdGroups, wspdPlan, zipLegs, zipperPlan };
33198
35803
 
33199
35804
  //# sourceMappingURL=index.js.map