@mark1russell7/highways 0.4.0 → 0.4.1

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
@@ -427,7 +427,7 @@ const COLLECTIONS = [
427
427
  "scores",
428
428
  "teams"
429
429
  ];
430
- const SIDES$1 = [
430
+ const SIDES$2 = [
431
431
  "north",
432
432
  "east",
433
433
  "south",
@@ -496,7 +496,7 @@ function makeFixture(spec) {
496
496
  clusterOf.push(cluster);
497
497
  const id = `n${nodes.length}`;
498
498
  if (R() < (spec.fixedPorts ?? 0)) {
499
- const s = SIDES$1[Math.floor(R() * 4)];
499
+ const s = SIDES$2[Math.floor(R() * 4)];
500
500
  const t = .2 + .6 * R();
501
501
  const at = s === "north" ? {
502
502
  x: w * t,
@@ -1049,7 +1049,7 @@ function checkProperties(graph, result, limit = 100) {
1049
1049
  }
1050
1050
  for (const id of run.edges) {
1051
1051
  const r = routes.get(id);
1052
- if (!r || !(holds$1(r.points, run.points) || holds$1(r.points, [...run.points].reverse()))) {
1052
+ if (!r || !(holds$2(r.points, run.points) || holds$2(r.points, [...run.points].reverse()))) {
1053
1053
  if (add({
1054
1054
  rule: "run-shared",
1055
1055
  edge: id,
@@ -1096,7 +1096,7 @@ function checkLevels(graph, levels, limit = 100) {
1096
1096
  const s = boxOf.get(l.source);
1097
1097
  const t = boxOf.get(l.target);
1098
1098
  const p = l.points;
1099
- if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
1099
+ if (!s || !t || p.length < 2 || !onBoundary$1(p[0], s) || !onBoundary$1(p[p.length - 1], t)) {
1100
1100
  if (add({
1101
1101
  rule: "link-ends",
1102
1102
  detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
@@ -1107,12 +1107,12 @@ function checkLevels(graph, levels, limit = 100) {
1107
1107
  return out;
1108
1108
  }
1109
1109
  /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
1110
- function onBoundary(p, b) {
1110
+ function onBoundary$1(p, b) {
1111
1111
  const inner = inflate(b, -.5);
1112
1112
  return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
1113
1113
  }
1114
1114
  /** This function tells if the polyline `p` holds the points of `part` in a row. */
1115
- function holds$1(p, part) {
1115
+ function holds$2(p, part) {
1116
1116
  if (part.length === 0) return true;
1117
1117
  for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
1118
1118
  return false;
@@ -2286,7 +2286,7 @@ function partBefore(P, Q) {
2286
2286
  }
2287
2287
  }
2288
2288
  }
2289
- const byEdge = (P, Q) => P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
2289
+ const byEdge$1 = (P, Q) => P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
2290
2290
  /** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
2291
2291
  const firstPoint = (a, b) => a.x < b.x - SAME$3 || Math.abs(a.x - b.x) <= SAME$3 && a.y < b.y;
2292
2292
  /**
@@ -2323,14 +2323,14 @@ function orderLanes(run, routes, order = "run") {
2323
2323
  else lost.push(r.edge);
2324
2324
  }
2325
2325
  }
2326
- if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q));
2326
+ if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge$1(P, Q));
2327
2327
  else {
2328
- placed.sort(byEdge);
2328
+ placed.sort(byEdge$1);
2329
2329
  placed.sort((P, Q) => {
2330
2330
  const after = partAfter(P, Q);
2331
2331
  const before = partBefore(P, Q);
2332
2332
  if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
2333
- return after?.side ?? before?.side ?? byEdge(P, Q);
2333
+ return after?.side ?? before?.side ?? byEdge$1(P, Q);
2334
2334
  });
2335
2335
  }
2336
2336
  return [...placed.map((x) => x.edge), ...lost.sort()];
@@ -4598,6 +4598,201 @@ function pickLevel(sizes, scale, options = PICK) {
4598
4598
  for (let d = 0; d < n - 1; d++) if (sizes[d] * scale >= itemAt) pick = d;
4599
4599
  return pick;
4600
4600
  }
4601
+ // src/plugs/justify.ts
4602
+ /**
4603
+ * Plug justification (WILD-002, with NAT-031): the plugs of one side keep their order and a minimum gap.
4604
+ * They move the least distance from their ideal places.
4605
+ */
4606
+ /**
4607
+ * This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
4608
+ * The points keep the order of their ideal places. Two neighbours are at least `gap` apart, or the side divided evenly when it is too short.
4609
+ * The sum of the squares of the moves is the least. The answer is exact: a fit by pool adjacent violators.
4610
+ *
4611
+ * The function `tie` orders two points with the same ideal place. The result is in the order of `ideal`.
4612
+ */
4613
+ function justifyPlugs(ideal, lo, hi, gap, tie) {
4614
+ const k = ideal.length;
4615
+ if (k === 0) return [];
4616
+ const order = ideal.map((_, i) => i).sort((a, b) => ideal[a] - ideal[b] || (tie ? tie(a) - tie(b) : 0) || a - b);
4617
+ const s = k > 1 ? Math.max(0, Math.min(gap, (hi - lo) / (k - 1))) : 0;
4618
+ const y = isotonic(order.map((j, i) => ideal[j] - i * s));
4619
+ const top = Math.max(lo, hi - (k - 1) * s);
4620
+ const out = new Array(k);
4621
+ order.forEach((j, i) => {
4622
+ out[j] = Math.max(lo, Math.min(top, y[i])) + i * s;
4623
+ });
4624
+ return out;
4625
+ }
4626
+ /** This function gives the non-decreasing sequence nearest to `v` in the sum of squares (pool adjacent violators). */
4627
+ function isotonic(v) {
4628
+ const mean = [];
4629
+ const size = [];
4630
+ for (const x of v) {
4631
+ mean.push(x);
4632
+ size.push(1);
4633
+ while (mean.length > 1 && mean[mean.length - 2] > mean[mean.length - 1]) {
4634
+ const m = mean.pop();
4635
+ const n = size.pop();
4636
+ const i = mean.length - 1;
4637
+ mean[i] = (mean[i] * size[i] + m * n) / (size[i] + n);
4638
+ size[i] += n;
4639
+ }
4640
+ }
4641
+ const out = [];
4642
+ mean.forEach((m, i) => {
4643
+ for (let j = 0; j < size[i]; j++) out.push(m);
4644
+ });
4645
+ return out;
4646
+ }
4647
+ // src/plugs/side.ts
4648
+ /**
4649
+ * Free plugs on a side (WILD-001): the search of a route starts from the whole free side, not from one point of it.
4650
+ * Thus the first leg goes straight out to the first corner of the way, and the plug is the foot of that leg.
4651
+ */
4652
+ /** This function gives the free side of an end, or `undefined` when the plug of the end is fixed. */
4653
+ function freeSide(node, end, toward, inset) {
4654
+ if (fixedPortPoint(node, end)) return void 0;
4655
+ const side = (end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port))?.side ?? sideToward(node.box, toward);
4656
+ const b = node.box;
4657
+ const along = side === "east" || side === "west" ? "y" : "x";
4658
+ const start = along === "y" ? b.y : b.x;
4659
+ const len = along === "y" ? b.h : b.w;
4660
+ const m = Math.min(inset, len / 4);
4661
+ const at = side === "east" ? b.x + b.w : side === "west" ? b.x : side === "south" ? b.y + b.h : b.y;
4662
+ const point = pointOnSide(b, side, toward, inset);
4663
+ return {
4664
+ node,
4665
+ side,
4666
+ normal: SIDE_NORMAL[side],
4667
+ along,
4668
+ at,
4669
+ lo: start + m,
4670
+ hi: start + len - m,
4671
+ facing: {
4672
+ point,
4673
+ normal: SIDE_NORMAL[side]
4674
+ }
4675
+ };
4676
+ }
4677
+ /** This function gives the point of the side at `v` on the axis along the side. */
4678
+ function footAt(f, v) {
4679
+ return f.along === "y" ? {
4680
+ x: f.at,
4681
+ y: v
4682
+ } : {
4683
+ x: v,
4684
+ y: f.at
4685
+ };
4686
+ }
4687
+ /**
4688
+ * This function gives the places that the search tries on one free side. A place is the out point of a plug on the side.
4689
+ * A place is also a corner in front of the side. The plug of a corner is the foot of the leg to it.
4690
+ */
4691
+ function startsOf(ways, f, others, bend, most) {
4692
+ const margin = ways.margin;
4693
+ const pad = margin / 2;
4694
+ const own = /* @__PURE__ */ new Set([f.node]);
4695
+ const starts = [];
4696
+ const out = (p) => ({
4697
+ x: p.x + f.normal.x * margin,
4698
+ y: p.y + f.normal.y * margin
4699
+ });
4700
+ const plugAt = (v) => ({
4701
+ point: footAt(f, v),
4702
+ normal: f.normal
4703
+ });
4704
+ starts.push({
4705
+ plug: f.facing,
4706
+ point: out(f.facing.point),
4707
+ cost: margin
4708
+ });
4709
+ const mid = (f.lo + f.hi) / 2;
4710
+ for (const v of [
4711
+ f.lo,
4712
+ mid,
4713
+ f.hi,
4714
+ ...others
4715
+ ]) {
4716
+ if (v < f.lo - 1e-9 || v > f.hi + 1e-9) continue;
4717
+ const p = out(footAt(f, v));
4718
+ if (!ways.obstacles.inside(p, 0, f.node)) starts.push({
4719
+ plug: plugAt(v),
4720
+ point: p,
4721
+ cost: margin
4722
+ });
4723
+ }
4724
+ const seen = /* @__PURE__ */ new Set();
4725
+ for (const v of [
4726
+ f.lo,
4727
+ mid,
4728
+ f.hi,
4729
+ f.facing.point[f.along]
4730
+ ]) for (const c of ways.corners.nearestVisible(out(footAt(f, v)), 16)) seen.add(c);
4731
+ for (const c of seen) {
4732
+ const q = ways.corners.points[c];
4733
+ const across = f.along === "y" ? "x" : "y";
4734
+ const depth = (q[across] - f.at) * f.normal[across];
4735
+ if (depth < margin - 1e-9) continue;
4736
+ const v = Math.max(f.lo, Math.min(f.hi, q[f.along]));
4737
+ const foot = footAt(f, v);
4738
+ const angle = Math.atan2(Math.abs(q[f.along] - v), depth);
4739
+ if (angle > Math.PI / 4 + 1e-9) continue;
4740
+ if (!ways.obstacles.clear(foot, q, pad, own)) continue;
4741
+ starts.push({
4742
+ plug: plugAt(v),
4743
+ point: q,
4744
+ cost: dist(foot, q) * (1 + bend * angle)
4745
+ });
4746
+ }
4747
+ const best = /* @__PURE__ */ new Map();
4748
+ for (const s of starts) {
4749
+ const key = Math.round(s.plug.point[f.along] * 2);
4750
+ const old = best.get(key);
4751
+ if (!old || s.cost < old.cost) best.set(key, s);
4752
+ }
4753
+ const facing = f.facing.point[f.along];
4754
+ return [...best.values()].sort((a, b) => Math.abs(a.plug.point[f.along] - facing) - Math.abs(b.plug.point[f.along] - facing)).slice(0, Math.max(1, most));
4755
+ }
4756
+ /**
4757
+ * This function places the two plugs of an edge with a free side. A fixed plug stays at its point.
4758
+ * The search (A*) starts from each place of the free side of the source. It ends at each place of the free side of the target.
4759
+ * The plugs are the places of the shortest way. Without a way, the plugs face the other end, as `placePlug` places them.
4760
+ */
4761
+ function sidePlugs(ways, s, es, t, et, options = {}) {
4762
+ const inset = options.inset ?? 6;
4763
+ const ps = placePlug(s, es, center(t.box));
4764
+ const pt = placePlug(t, et, center(s.box));
4765
+ if (s === t) return [ps, pt];
4766
+ const fs = freeSide(s, es, center(t.box), inset);
4767
+ const ft = freeSide(t, et, center(s.box), inset);
4768
+ if (!fs && !ft) return [ps, pt];
4769
+ const bend = options.bend ?? 1;
4770
+ const most = options.most ?? 16;
4771
+ const fixed = (p) => [{
4772
+ plug: p,
4773
+ point: ways.out(p),
4774
+ cost: ways.margin
4775
+ }];
4776
+ const facingPair = fs && ft && fs.along === ft.along && fs.normal.x === -ft.normal.x && fs.normal.y === -ft.normal.y;
4777
+ const shared = (a, b, p) => {
4778
+ if (!a) return [];
4779
+ if (facingPair && b) {
4780
+ const lo = Math.max(a.lo, b.lo);
4781
+ const hi = Math.min(a.hi, b.hi);
4782
+ return lo <= hi ? [
4783
+ lo,
4784
+ (lo + hi) / 2,
4785
+ hi
4786
+ ] : [];
4787
+ }
4788
+ return [p.point[a.along]];
4789
+ };
4790
+ const from = fs ? startsOf(ways, fs, shared(fs, ft, pt), bend, most) : fixed(ps);
4791
+ const to = ft ? startsOf(ways, ft, shared(ft, fs, ps), bend, most) : fixed(pt);
4792
+ const way = ways.corners.pathAmong(from, to);
4793
+ if (!way) return [ps, pt];
4794
+ return [from[way.from].plug, to[way.to].plug];
4795
+ }
4601
4796
  // src/routers/corners.ts
4602
4797
  /**
4603
4798
  * This function makes a router that goes around the nodes.
@@ -4611,17 +4806,39 @@ function cornersRouter(options) {
4611
4806
  const byId = nodeIndex(graph);
4612
4807
  const ways = new WayFinder(graph.nodes, options);
4613
4808
  const routes = [];
4809
+ if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
4810
+ for (const e of graph.edges) {
4811
+ const s = byId.get(e.source.node);
4812
+ const t = byId.get(e.target.node);
4813
+ if (!s || !t) continue;
4814
+ const ps = placePlug(s, e.source, center(t.box));
4815
+ const pt = placePlug(t, e.target, center(s.box));
4816
+ routes.push({
4817
+ edge: e.id,
4818
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
4819
+ });
4820
+ }
4821
+ return { routes };
4822
+ }
4823
+ const ends = [];
4614
4824
  for (const e of graph.edges) {
4615
4825
  const s = byId.get(e.source.node);
4616
4826
  const t = byId.get(e.target.node);
4617
4827
  if (!s || !t) continue;
4618
- const ps = placePlug(s, e.source, center(t.box));
4619
- const pt = placePlug(t, e.target, center(s.box));
4620
- routes.push({
4621
- edge: e.id,
4622
- points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
4623
- });
4828
+ const [ps, pt] = options.plugs === "side" ? sidePlugs(ways, s, e.source, t, e.target) : [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
4829
+ ends.push([
4830
+ e,
4831
+ s,
4832
+ t,
4833
+ ps,
4834
+ pt
4835
+ ]);
4624
4836
  }
4837
+ if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap);
4838
+ for (const [e, s, t, ps, pt] of ends) routes.push({
4839
+ edge: e.id,
4840
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
4841
+ });
4625
4842
  return { routes };
4626
4843
  }
4627
4844
  };
@@ -4630,6 +4847,69 @@ function cornersRouter(options) {
4630
4847
  maxNodes: options.maxNodes
4631
4848
  };
4632
4849
  }
4850
+ /**
4851
+ * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
4852
+ * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
4853
+ */
4854
+ function spacePlugs(ends, gap) {
4855
+ const inset = 6;
4856
+ const sides = /* @__PURE__ */ new Map();
4857
+ ends.forEach(([e, s, t, ps, pt], i) => {
4858
+ for (const [end, node, plug, other, k] of [[
4859
+ e.source,
4860
+ s,
4861
+ ps,
4862
+ pt,
4863
+ 3
4864
+ ], [
4865
+ e.target,
4866
+ t,
4867
+ pt,
4868
+ ps,
4869
+ 4
4870
+ ]]) {
4871
+ if ((end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port))?.at !== void 0) continue;
4872
+ const key = JSON.stringify([
4873
+ node.id,
4874
+ plug.normal.x,
4875
+ plug.normal.y
4876
+ ]);
4877
+ let side = sides.get(key);
4878
+ if (!side) sides.set(key, side = {
4879
+ node,
4880
+ normal: plug.normal,
4881
+ items: []
4882
+ });
4883
+ side.items.push([
4884
+ i,
4885
+ k,
4886
+ other.point
4887
+ ]);
4888
+ }
4889
+ });
4890
+ for (const { node, normal, items } of sides.values()) {
4891
+ if (items.length < 2) continue;
4892
+ const along = normal.x !== 0 ? "y" : "x";
4893
+ const b = node.box;
4894
+ const start = along === "y" ? b.y : b.x;
4895
+ const len = along === "y" ? b.h : b.w;
4896
+ const m = Math.min(inset, len / 4);
4897
+ const placed = justifyPlugs(items.map(([i, k]) => ends[i][k].point[along]), start + m, start + len - m, gap, (j) => items[j][2][along]);
4898
+ items.forEach(([i, k], j) => {
4899
+ const old = ends[i][k];
4900
+ ends[i][k] = {
4901
+ point: along === "y" ? {
4902
+ x: old.point.x,
4903
+ y: placed[j]
4904
+ } : {
4905
+ x: placed[j],
4906
+ y: old.point.y
4907
+ },
4908
+ normal: old.normal
4909
+ };
4910
+ });
4911
+ }
4912
+ }
4633
4913
  // src/routers/plugs.ts
4634
4914
  /**
4635
4915
  * Plugs and the inflection rule, with no obstacles. Each edge leaves its source plug at the tangent of the side.
@@ -4819,7 +5099,7 @@ const LEVEL_LINK_DEFAULTS = Object.freeze({
4819
5099
  plugInset: 6,
4820
5100
  throughCost: 8
4821
5101
  });
4822
- const EPS$2 = 1e-6;
5102
+ const EPS$3 = 1e-6;
4823
5103
  /**
4824
5104
  * This function removes the repeated points, the points in the middle of a straight run,
4825
5105
  * and the points where the way turns back on itself.
@@ -4834,15 +5114,15 @@ function tidy(p) {
4834
5114
  function tidyPoints(p) {
4835
5115
  const out = [];
4836
5116
  for (const q of p) {
4837
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$2) continue;
5117
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$3) continue;
4838
5118
  while (out.length >= 2) {
4839
5119
  const a = out[out.length - 2];
4840
5120
  const b = out[out.length - 1];
4841
5121
  const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
4842
- if (Math.abs(cross) > EPS$2 * (1 + dist(a, b) * dist(b, q))) break;
5122
+ if (Math.abs(cross) > EPS$3 * (1 + dist(a, b) * dist(b, q))) break;
4843
5123
  out.pop();
4844
5124
  }
4845
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$2) continue;
5125
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$3) continue;
4846
5126
  out.push(q);
4847
5127
  }
4848
5128
  return out;
@@ -5097,7 +5377,7 @@ function facingPlugs(a, b, inset) {
5097
5377
  }
5098
5378
  return [plug(a, sa, center(b)), plug(b, sb, center(a))];
5099
5379
  }
5100
- const SIDES = [
5380
+ const SIDES$1 = [
5101
5381
  "north",
5102
5382
  "east",
5103
5383
  "south",
@@ -5116,7 +5396,7 @@ function sidePlug(ways, node, toward) {
5116
5396
  y: toward.y - c.y
5117
5397
  };
5118
5398
  const facing = (s) => SIDE_NORMAL[s].x * d.x + SIDE_NORMAL[s].y * d.y;
5119
- const order = [...SIDES].sort((u, v) => facing(v) - facing(u));
5399
+ const order = [...SIDES$1].sort((u, v) => facing(v) - facing(u));
5120
5400
  const plug = (s) => {
5121
5401
  const n = SIDE_NORMAL[s];
5122
5402
  return {
@@ -5139,7 +5419,7 @@ function freePlugs(ways, node, inset) {
5139
5419
  if (known) return known;
5140
5420
  const b = node.box;
5141
5421
  const out = [];
5142
- for (const side of SIDES) {
5422
+ for (const side of SIDES$1) {
5143
5423
  const n = SIDE_NORMAL[side];
5144
5424
  const across = n.x === 0;
5145
5425
  const lo = across ? b.x : b.y;
@@ -5215,7 +5495,7 @@ function freeSpans(lo, hi, covered) {
5215
5495
  if (x >= hi) break;
5216
5496
  }
5217
5497
  if (x < hi) out.push([x, hi]);
5218
- return out.filter(([a, b]) => b - a > EPS$2);
5498
+ return out.filter(([a, b]) => b - a > EPS$3);
5219
5499
  }
5220
5500
  /**
5221
5501
  * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
@@ -5347,7 +5627,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
5347
5627
  out.push(...step.legs);
5348
5628
  const end = out[out.length - 1];
5349
5629
  const before = out[out.length - 2];
5350
- axis = Math.abs(end.y - before.y) < EPS$2 ? "x" : Math.abs(end.x - before.x) < EPS$2 ? "y" : void 0;
5630
+ axis = Math.abs(end.y - before.y) < EPS$3 ? "x" : Math.abs(end.x - before.x) < EPS$3 ? "y" : void 0;
5351
5631
  i = step.j;
5352
5632
  }
5353
5633
  return out;
@@ -5357,7 +5637,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
5357
5637
  * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
5358
5638
  */
5359
5639
  function square(a, b, obstacles, pad, from, to) {
5360
- if (Math.abs(a.x - b.x) < EPS$2 || Math.abs(a.y - b.y) < EPS$2) return obstacles.clear(a, b, pad) ? [b] : void 0;
5640
+ if (Math.abs(a.x - b.x) < EPS$3 || Math.abs(a.y - b.y) < EPS$3) return obstacles.clear(a, b, pad) ? [b] : void 0;
5361
5641
  const options = [{
5362
5642
  c: {
5363
5643
  x: b.x,
@@ -5388,7 +5668,7 @@ const SECTIONS_DEFAULTS = Object.freeze({
5388
5668
  stretchPad: 4,
5389
5669
  hubSlack: 80
5390
5670
  });
5391
- const dedupe$2 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
5671
+ const dedupe$3 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
5392
5672
  /**
5393
5673
  * This function makes a router with a tree of sections. Each section has a hub: the free corner nearest to the center of its nodes.
5394
5674
  * An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
@@ -5466,7 +5746,7 @@ function sectionsRouter(options) {
5466
5746
  if (!p) {
5467
5747
  const [from, to] = forward ? [a, b] : [b, a];
5468
5748
  const way = corners.path(from, to);
5469
- p = dedupe$2(way ? corners.pull(way) : [from, to]);
5749
+ p = dedupe$3(way ? corners.pull(way) : [from, to]);
5470
5750
  pieces.set(key, p);
5471
5751
  }
5472
5752
  if (!used.includes(key)) used.push(key);
@@ -5623,6 +5903,136 @@ function makeLevels(graph, root, routeOf, options) {
5623
5903
  }
5624
5904
  return levels;
5625
5905
  }
5906
+ // src/routers/snell.ts
5907
+ /**
5908
+ * This function gives the Snell merge point of a route (NAT-035).
5909
+ * The route comes from `from`, reaches a run at `at` and goes on along the run to `next`.
5910
+ * A unit of length on the run costs `d` against 1 off it. The cheapest route meets the run at the angle `acos(d)`.
5911
+ * The function gives the distance from `at` toward `next` of that merge point. A value of 0 or less tells that the turn at `at` is already at most `acos(d)`.
5912
+ */
5913
+ function snellOffset(from, at, next, d) {
5914
+ const len = dist(at, next);
5915
+ if (len <= 0) return 0;
5916
+ const ux = (next.x - at.x) / len;
5917
+ const uy = (next.y - at.y) / len;
5918
+ const vx = from.x - at.x;
5919
+ const vy = from.y - at.y;
5920
+ const along = vx * ux + vy * uy;
5921
+ const h = Math.abs(vx * uy - vy * ux);
5922
+ const c = Math.min(.999, Math.max(0, d));
5923
+ return along + h * c / Math.sqrt(1 - c * c);
5924
+ }
5925
+ /** The turn, in radians from 0 to π, at `b` on the way from `a` through `b` to `c`. */
5926
+ function turnAt(a, b, c) {
5927
+ const ux = b.x - a.x;
5928
+ const uy = b.y - a.y;
5929
+ const vx = c.x - b.x;
5930
+ const vy = c.y - b.y;
5931
+ return Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
5932
+ }
5933
+ /**
5934
+ * This function moves the merge points of the ways of the router `shared` to the Snell angle (NAT-035). It changes the ways in place.
5935
+ *
5936
+ * A way joins a run at a corner `J` when it comes alone from the corner before.
5937
+ * The link from `J` to the next corner has other edges of its key.
5938
+ * When the way turns at `J` by more than `acos(d)`, it goes directly to a new point `J*` on that link.
5939
+ * At `J*`, the way meets the link at the angle `acos(d)`.
5940
+ * Each way on the link gets `J*` as a new corner, so the run keeps the same points in each route. Then the same rule moves the exits, on the reversed ways.
5941
+ *
5942
+ * `firstId` is the first free id of a corner. `clear` tells if a line keeps out of the boxes. `delta` gives the cost share `d` of a link with `f` other edges.
5943
+ */
5944
+ function snellMerges(ways, firstId, clear, delta) {
5945
+ let next = firstId;
5946
+ const keys = /* @__PURE__ */ new Map();
5947
+ for (const w of ways) {
5948
+ if (!w.found) continue;
5949
+ const list = keys.get(w.key);
5950
+ if (list) list.push(w);
5951
+ else keys.set(w.key, [w]);
5952
+ }
5953
+ for (const group of keys.values()) {
5954
+ if (group.length < 2) continue;
5955
+ const linkOf = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
5956
+ const on = /* @__PURE__ */ new Map();
5957
+ const add = (k, w) => {
5958
+ const set = on.get(k);
5959
+ if (set) set.add(w);
5960
+ else on.set(k, /* @__PURE__ */ new Set([w]));
5961
+ };
5962
+ const drop = (k, w) => {
5963
+ const set = on.get(k);
5964
+ if (!set) return;
5965
+ set.delete(w);
5966
+ if (set.size === 0) on.delete(k);
5967
+ };
5968
+ for (const w of group) for (let i = 1; i < w.ids.length; i++) add(linkOf(w.ids[i - 1], w.ids[i]), w);
5969
+ for (const back of [false, true]) for (const w of group) {
5970
+ if (back) {
5971
+ w.seq.reverse();
5972
+ w.ids.reverse();
5973
+ }
5974
+ for (let i = 1; i < w.ids.length - 1; i++) {
5975
+ const before = on.get(linkOf(w.ids[i - 1], w.ids[i]));
5976
+ const after = on.get(linkOf(w.ids[i], w.ids[i + 1]));
5977
+ if (!before || !after || before.size !== 1 || after.size < 2) continue;
5978
+ const P = w.seq[i - 1];
5979
+ const J = w.seq[i];
5980
+ const N = w.seq[i + 1];
5981
+ const d = delta(after.size - 1);
5982
+ if (turnAt(P, J, N) <= Math.acos(d) + .001) continue;
5983
+ const len = dist(J, N);
5984
+ let t = Math.min(len, snellOffset(P, J, N, d));
5985
+ const pointAt = (s) => ({
5986
+ x: J.x + (N.x - J.x) * s / len,
5987
+ y: J.y + (N.y - J.y) * s / len
5988
+ });
5989
+ let tries = 0;
5990
+ while (t >= 1 && !clear(P, pointAt(t)) && tries < 3) {
5991
+ t /= 2;
5992
+ tries++;
5993
+ }
5994
+ if (t < 1 || !clear(P, pointAt(t))) continue;
5995
+ const a = w.ids[i - 1];
5996
+ const j = w.ids[i];
5997
+ const n = w.ids[i + 1];
5998
+ if (t >= len - 1e-6) {
5999
+ drop(linkOf(a, j), w);
6000
+ drop(linkOf(j, n), w);
6001
+ w.seq.splice(i, 1);
6002
+ w.ids.splice(i, 1);
6003
+ add(linkOf(a, n), w);
6004
+ continue;
6005
+ }
6006
+ const star = pointAt(t);
6007
+ const id = next++;
6008
+ const users = [...after];
6009
+ on.delete(linkOf(j, n));
6010
+ for (const o of users) {
6011
+ for (let k = 0; k < o.ids.length - 1; k++) {
6012
+ const x = o.ids[k];
6013
+ const y = o.ids[k + 1];
6014
+ if (x === j && y === n || x === n && y === j) {
6015
+ o.seq.splice(k + 1, 0, star);
6016
+ o.ids.splice(k + 1, 0, id);
6017
+ break;
6018
+ }
6019
+ }
6020
+ add(linkOf(j, id), o);
6021
+ add(linkOf(id, n), o);
6022
+ }
6023
+ drop(linkOf(a, j), w);
6024
+ drop(linkOf(j, id), w);
6025
+ w.seq.splice(i, 1);
6026
+ w.ids.splice(i, 1);
6027
+ add(linkOf(a, id), w);
6028
+ }
6029
+ if (back) {
6030
+ w.seq.reverse();
6031
+ w.ids.reverse();
6032
+ }
6033
+ }
6034
+ }
6035
+ }
5626
6036
  // src/routers/shared.ts
5627
6037
  const byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
5628
6038
  /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
@@ -5641,6 +6051,10 @@ function turn(from, base, to) {
5641
6051
  */
5642
6052
  function sharedRouter(options) {
5643
6053
  const discount = Math.min(1, Math.max(.05, options.discount ?? .5));
6054
+ const flow = options.cost === "flow";
6055
+ const alpha = Math.min(1, Math.max(.1, options.alpha ?? .6));
6056
+ const share = (f) => f <= 0 ? 1 : flow ? (f + 1) ** alpha - f ** alpha : discount;
6057
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
5644
6058
  const keyOf = (e) => {
5645
6059
  if (!options.rule) return "*";
5646
6060
  return options.rule(e) ?? `\u0000${e.id}`;
@@ -5680,13 +6094,16 @@ function sharedRouter(options) {
5680
6094
  });
5681
6095
  }
5682
6096
  const discounted = /* @__PURE__ */ new Map();
5683
- const order = [...all].sort((p, q) => byText(p.key, q.key) || dist(q.ps.point, q.pt.point) - dist(p.ps.point, p.pt.point) || byText(p.edge.id, q.edge.id));
5684
- for (const w of order) {
6097
+ const order = orderWays(all, options.order ?? "span", graph);
6098
+ const top = /* @__PURE__ */ new Map();
6099
+ const route = (w) => {
5685
6100
  let used = discounted.get(w.key);
5686
6101
  if (!used) discounted.set(w.key, used = /* @__PURE__ */ new Map());
5687
6102
  const mine = used;
5688
- const way = corners.path(ways.out(w.ps), ways.out(w.pt), (i, j, d) => mine.has(link(i, j)) ? d * discount : d, discount);
5689
- if (!way) continue;
6103
+ const floor = flow ? share(top.get(w.key) ?? 0) : discount;
6104
+ const cost = flow ? (i, j, d) => d * share(mine.get(link(i, j))?.length ?? 0) : (i, j, d) => mine.has(link(i, j)) ? d * discount : d;
6105
+ const way = corners.path(ways.out(w.ps), ways.out(w.pt), cost, floor);
6106
+ if (!way) return;
5690
6107
  w.found = true;
5691
6108
  w.seq = way;
5692
6109
  w.ids = way.map((p, i) => i === 0 || i === way.length - 1 ? endId(p) : index.get(p));
@@ -5697,7 +6114,32 @@ function sharedRouter(options) {
5697
6114
  const list = mine.get(link(a, b));
5698
6115
  if (list) list.push(w.edge.id);
5699
6116
  else mine.set(link(a, b), [w.edge.id]);
6117
+ if (flow) top.set(w.key, Math.max(top.get(w.key) ?? 0, mine.get(link(a, b)).length));
6118
+ }
6119
+ };
6120
+ for (const w of order) route(w);
6121
+ for (let pass = 1; pass < passes; pass++) for (const w of order) {
6122
+ if (!w.found) continue;
6123
+ const mine = discounted.get(w.key);
6124
+ for (let i = 1; i < w.ids.length; i++) {
6125
+ const a = w.ids[i - 1];
6126
+ const b = w.ids[i];
6127
+ if (a >= corners.points.length || b >= corners.points.length) continue;
6128
+ const k = link(a, b);
6129
+ const list = mine.get(k);
6130
+ if (!list) continue;
6131
+ const at = list.indexOf(w.edge.id);
6132
+ if (at >= 0) list.splice(at, 1);
6133
+ if (list.length === 0) mine.delete(k);
5700
6134
  }
6135
+ w.found = false;
6136
+ w.seq = [];
6137
+ w.ids = [];
6138
+ route(w);
6139
+ }
6140
+ if (options.merge === "snell") {
6141
+ const pad = ways.margin / 2;
6142
+ snellMerges(order, corners.points.length + ends.size, (a, b) => ways.obstacles.clear(a, b, pad), (f) => Math.min(.95, share(f)));
5701
6143
  }
5702
6144
  const users = /* @__PURE__ */ new Map();
5703
6145
  for (const w of order) {
@@ -5785,6 +6227,33 @@ function sharedRouter(options) {
5785
6227
  };
5786
6228
  }
5787
6229
  /**
6230
+ * This function gives the order of the ways for the router `shared`. The ways of one key stay together, in the order of the keys.
6231
+ * In a key, the order is `order`, and the edge id breaks each tie. The order `load` counts the edges of the key between the same two sections.
6232
+ */
6233
+ function orderWays(all, order, graph) {
6234
+ const span = (w) => dist(w.ps.point, w.pt.point);
6235
+ if (order === "input") {
6236
+ const at = new Map(all.map((w, i) => [w, i]));
6237
+ return [...all].sort((p, q) => byText(p.key, q.key) || at.get(p) - at.get(q));
6238
+ }
6239
+ if (order === "shortest") return [...all].sort((p, q) => byText(p.key, q.key) || span(p) - span(q) || byText(p.edge.id, q.edge.id));
6240
+ if (order === "span") return [...all].sort((p, q) => byText(p.key, q.key) || span(q) - span(p) || byText(p.edge.id, q.edge.id));
6241
+ const leafOf = /* @__PURE__ */ new Map();
6242
+ const walk = (s) => {
6243
+ if (s.children.length === 0) for (const i of s.nodes) leafOf.set(graph.nodes[i].id, s.id);
6244
+ for (const c of s.children) walk(c);
6245
+ };
6246
+ walk(buildSections(graph.nodes));
6247
+ const pair = (w) => {
6248
+ const a = leafOf.get(w.edge.source.node) ?? "";
6249
+ const b = leafOf.get(w.edge.target.node) ?? "";
6250
+ return `${w.key}\u0000${a < b ? a : b}\u0000${a < b ? b : a}`;
6251
+ };
6252
+ const load = /* @__PURE__ */ new Map();
6253
+ for (const w of all) load.set(pair(w), (load.get(pair(w)) ?? 0) + 1);
6254
+ return [...all].sort((p, q) => byText(p.key, q.key) || load.get(pair(q)) - load.get(pair(p)) || span(q) - span(p) || byText(p.edge.id, q.edge.id));
6255
+ }
6256
+ /**
5788
6257
  * This function gives the lane order of a run with the option `laneOrder: "run"` (metro-line order). Two edges of the run go together past its last corner
5789
6258
  * until they part. The edge that turns more to the left there gets the lane more to the left.
5790
6259
  * Two edges can part only before the first corner. Then the edge that comes from more to the left gets the lane more to the left.
@@ -5874,6 +6343,162 @@ const straightRouter = {
5874
6343
  return { routes };
5875
6344
  }
5876
6345
  };
6346
+ // src/routers/fan.ts
6347
+ /**
6348
+ * This function gives the Gilbert split point of a trunk with `targets.length` edges (EDA-030). The point makes the flow cost least:
6349
+ * `len(start, s) * k^alpha + Σ_j len(s, t_j)`, with `k` the number of targets. Weighted Weiszfeld steps find it.
6350
+ * The distance from `start` stays from `lo` to `hi` times the distance to the center of the targets.
6351
+ * It is also at most half of the distance to the nearest target.
6352
+ * The point moves back toward `start` out of the boxes, in steps of `step`.
6353
+ */
6354
+ function gilbertPoint(start, targets, alpha, free, step, lo = .2, hi = .8) {
6355
+ if (targets.length === 0) return start;
6356
+ const k = targets.length;
6357
+ const c = {
6358
+ x: targets.reduce((s, p) => s + p.x, 0) / k,
6359
+ y: targets.reduce((s, p) => s + p.y, 0) / k
6360
+ };
6361
+ const L = dist(start, c);
6362
+ if (L === 0) return start;
6363
+ const w0 = k ** Math.min(1, Math.max(0, alpha));
6364
+ let s = {
6365
+ x: start.x + (c.x - start.x) * .5,
6366
+ y: start.y + (c.y - start.y) * .5
6367
+ };
6368
+ for (let it = 0; it < 32; it++) {
6369
+ let sx = 0;
6370
+ let sy = 0;
6371
+ let sw = 0;
6372
+ const pull = (p, w) => {
6373
+ const d = Math.max(1e-6, dist(s, p));
6374
+ sx += w * p.x / d;
6375
+ sy += w * p.y / d;
6376
+ sw += w / d;
6377
+ };
6378
+ pull(start, w0);
6379
+ for (const t of targets) pull(t, 1);
6380
+ const n = {
6381
+ x: sx / sw,
6382
+ y: sy / sw
6383
+ };
6384
+ const moved = dist(n, s);
6385
+ s = n;
6386
+ if (moved < .001) break;
6387
+ }
6388
+ const r = dist(start, s);
6389
+ const most = Math.min(hi * L, .5 * Math.min(...targets.map((p) => dist(start, p))));
6390
+ const want = Math.min(most, Math.max(lo * L, r));
6391
+ const dir = r > 1e-9 ? {
6392
+ x: (s.x - start.x) / r,
6393
+ y: (s.y - start.y) / r
6394
+ } : {
6395
+ x: (c.x - start.x) / L,
6396
+ y: (c.y - start.y) / L
6397
+ };
6398
+ const at = (t) => ({
6399
+ x: start.x + dir.x * t,
6400
+ y: start.y + dir.y * t
6401
+ });
6402
+ let d = want;
6403
+ while (d > 0 && !free(at(d))) d -= step;
6404
+ return d > 0 ? at(d) : start;
6405
+ }
6406
+ /**
6407
+ * This function grows a balancing-factor tree from `root` to the targets (NAT-017, after Cuntz and others, 2010).
6408
+ * At each step, the target `t` joins the place `v` of the tree with the least `|v - goal(t)| + bf * PL(v)`.
6409
+ * `PL(v)` is the length of the tree from the root to `v`.
6410
+ * A place is a point of the tree, or the foot of `goal(t)` on a link of the tree. `bf` 0 gives the least ink, and a large `bf` gives direct routes.
6411
+ * `branch(v, t)` gives the way from `v` to the plug of the target `t`: its first point is `v`, and its last point is the plug.
6412
+ */
6413
+ function balancedFan(root, goals, bf, branch) {
6414
+ const points = [{
6415
+ point: root,
6416
+ parent: -1,
6417
+ pl: 0,
6418
+ open: true
6419
+ }];
6420
+ const leaf = goals.map(() => -1);
6421
+ const left = new Set(goals.map((_, i) => i));
6422
+ while (left.size > 0) {
6423
+ let best = Infinity;
6424
+ let pick = -1;
6425
+ let at = -1;
6426
+ let foot;
6427
+ for (const t of left) {
6428
+ const g = goals[t];
6429
+ for (let v = 0; v < points.length; v++) {
6430
+ const p = points[v];
6431
+ if (!p.open) continue;
6432
+ const c = dist(p.point, g) + bf * p.pl;
6433
+ if (c < best - 1e-9) {
6434
+ best = c;
6435
+ pick = t;
6436
+ at = v;
6437
+ foot = void 0;
6438
+ }
6439
+ if (p.parent < 0) continue;
6440
+ const a = points[p.parent].point;
6441
+ const b = p.point;
6442
+ const L = dist(a, b);
6443
+ if (L <= 1e-9) continue;
6444
+ const u = ((g.x - a.x) * (b.x - a.x) + (g.y - a.y) * (b.y - a.y)) / (L * L);
6445
+ if (u <= 1e-6 || u >= 1 - 1e-6) continue;
6446
+ const f = {
6447
+ x: a.x + u * (b.x - a.x),
6448
+ y: a.y + u * (b.y - a.y)
6449
+ };
6450
+ const pl = points[p.parent].pl + u * L;
6451
+ const cf = dist(f, g) + bf * pl;
6452
+ if (cf < best - 1e-9) {
6453
+ best = cf;
6454
+ pick = t;
6455
+ at = v;
6456
+ foot = {
6457
+ point: f,
6458
+ pl
6459
+ };
6460
+ }
6461
+ }
6462
+ }
6463
+ left.delete(pick);
6464
+ let from = at;
6465
+ if (foot) {
6466
+ points.push({
6467
+ point: foot.point,
6468
+ parent: points[at].parent,
6469
+ pl: foot.pl,
6470
+ open: true
6471
+ });
6472
+ from = points.length - 1;
6473
+ points[at].parent = from;
6474
+ }
6475
+ const way = branch(points[from].point, pick);
6476
+ let prev = from;
6477
+ for (let i = 1; i < way.length; i++) {
6478
+ const q = way[i];
6479
+ const base = points[prev];
6480
+ if (dist(q, base.point) <= 1e-9) continue;
6481
+ points.push({
6482
+ point: q,
6483
+ parent: prev,
6484
+ pl: base.pl + dist(base.point, q),
6485
+ open: i < way.length - 1
6486
+ });
6487
+ prev = points.length - 1;
6488
+ }
6489
+ leaf[pick] = prev;
6490
+ }
6491
+ return {
6492
+ points,
6493
+ leaf
6494
+ };
6495
+ }
6496
+ /** This function gives the points of the tree from the root to the point `i`. */
6497
+ function fanPath(fan, i) {
6498
+ const out = [];
6499
+ for (let j = i; j >= 0; j = fan.points[j].parent) out.push(fan.points[j].point);
6500
+ return out.reverse();
6501
+ }
5877
6502
  // src/routers/trunks.ts
5878
6503
  /** The rule that groups the edges by the collection of their target, in `data.collection`. */
5879
6504
  const byCollection = (e) => {
@@ -5932,7 +6557,7 @@ function splitPoint(start, targets, split, free, step) {
5932
6557
  while (d > 0 && !free(at(d))) d -= step;
5933
6558
  return d > 0 ? at(d) : start;
5934
6559
  }
5935
- const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
6560
+ const dedupe$2 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
5936
6561
  /**
5937
6562
  * This function gives the place where a branch leaves its trunk. The branch starts at the last point of the trunk and ends at the plug of the target.
5938
6563
  *
@@ -6038,10 +6663,42 @@ function trunksRouter(options) {
6038
6663
  y: centers.reduce((a, p) => a + p.y, 0) / centers.length
6039
6664
  };
6040
6665
  const plug = placePlug(s, first.source, c);
6041
- const split = splitPoint(ways.out(plug), centers, options.split ?? .5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
6666
+ const start = ways.out(plug);
6667
+ const key = id.split("\0")[2];
6668
+ if (options.fan === "tree") {
6669
+ const pts = group.map((e, i) => placePlug(targets[i], e.target, start));
6670
+ const bf = Math.min(1, Math.max(0, options.bf ?? .4));
6671
+ const fan = balancedFan(start, pts.map((pt) => ways.out(pt)), bf, (from, t) => ways.toPlug(from, pts[t]) ?? [from, pts[t].point]);
6672
+ const own = group.map((e, i) => ({
6673
+ edge: e.id,
6674
+ points: dedupe$2([plug.point, ...fanPath(fan, fan.leaf[i])])
6675
+ }));
6676
+ for (const r of own) points.set(r.edge, r.points);
6677
+ const order = options.laneOrder ?? "bundle";
6678
+ const common = own[0].points.slice(0, commonStart(own, 0));
6679
+ bundles.push({
6680
+ id: `b${bundles.length}`,
6681
+ key,
6682
+ node: s.id,
6683
+ edges: orderLanes(common, own, order),
6684
+ trunk: common
6685
+ });
6686
+ for (const way of sharedWays(own, common.length)) {
6687
+ const users = own.filter((r) => way.edges.includes(r.edge));
6688
+ runs.push({
6689
+ id: `r${runs.length}`,
6690
+ key,
6691
+ points: way.points,
6692
+ edges: orderLanes(way.points, users, order)
6693
+ });
6694
+ }
6695
+ continue;
6696
+ }
6697
+ const free = (p) => !ways.obstacles.inside(p, ways.margin);
6698
+ const split = options.splitAt === "gilbert" ? gilbertPoint(start, centers, options.alpha ?? .5, free, ways.margin) : splitPoint(start, centers, options.split ?? .5, free, ways.margin);
6042
6699
  const way = ways.fromPlug(plug, split);
6043
6700
  if (!way) continue;
6044
- const trunk = dedupe$1(way);
6701
+ const trunk = dedupe$2(way);
6045
6702
  const end = trunk[trunk.length - 1];
6046
6703
  const prev = trunk.length >= 2 ? trunk[trunk.length - 2] : plug.point;
6047
6704
  const len = dist(prev, end);
@@ -6054,11 +6711,11 @@ function trunksRouter(options) {
6054
6711
  const exits = [];
6055
6712
  group.forEach((e, i) => {
6056
6713
  const pt = placePlug(targets[i], e.target, end);
6057
- const branch = dedupe$1(ways.toPlug(end, pt) ?? [end, pt.point]);
6714
+ const branch = dedupe$2(ways.toPlug(end, pt) ?? [end, pt.point]);
6058
6715
  if (!part) {
6059
6716
  own.push({
6060
6717
  edge: e.id,
6061
- points: dedupe$1([...trunk, ...branch.slice(1)])
6718
+ points: dedupe$2([...trunk, ...branch.slice(1)])
6062
6719
  });
6063
6720
  branches.push({
6064
6721
  edge: e.id,
@@ -6072,7 +6729,6 @@ function trunksRouter(options) {
6072
6729
  exit
6073
6730
  });
6074
6731
  });
6075
- const key = id.split("\0")[2];
6076
6732
  if (!part) {
6077
6733
  for (const r of own) points.set(r.edge, r.points);
6078
6734
  const edges = options.laneOrder === "run" || trunk.length < 2 ? laneOrder(dir, end, branches) : orderLanes(trunk, own, "bundle");
@@ -6090,7 +6746,7 @@ function trunksRouter(options) {
6090
6746
  for (const { exit } of exits) if (!stops.has(exit.at)) stops.set(exit.at, exit.point);
6091
6747
  const line = [...stops].sort((x, y) => x[0] - y[0]);
6092
6748
  for (const { edge, exit } of exits) {
6093
- const p = dedupe$1([...line.filter(([at]) => at <= exit.at).map(([, q]) => q), ...exit.rest]);
6749
+ const p = dedupe$2([...line.filter(([at]) => at <= exit.at).map(([, q]) => q), ...exit.rest]);
6094
6750
  points.set(edge, p);
6095
6751
  own.push({
6096
6752
  edge,
@@ -6564,7 +7220,7 @@ function routeLanes(items, links, gaps, gutter, snap, weight, router) {
6564
7220
  w: weight(links[i])
6565
7221
  });
6566
7222
  });
6567
- return density(spans);
7223
+ return density$1(spans);
6568
7224
  });
6569
7225
  }
6570
7226
  /** This function gives the points of the way of each link on the probe board, in the order of the links. */
@@ -6635,7 +7291,7 @@ function merge(spans) {
6635
7291
  return out;
6636
7292
  }
6637
7293
  /** This function gives the largest sum of the weights of the spans that overlap at one place. Spans that only touch do not overlap. */
6638
- function density(spans) {
7294
+ function density$1(spans) {
6639
7295
  const events = [];
6640
7296
  for (const s of spans) events.push([s.from, s.w], [s.to, -s.w]);
6641
7297
  events.sort((u, v) => u[0] - v[0] || u[1] - v[1]);
@@ -6690,7 +7346,13 @@ const LANE_ORDERS = [
6690
7346
  "ends",
6691
7347
  "crossings"
6692
7348
  ];
6693
- const EPS$1 = 1e-6;
7349
+ /** The lane rules of `gapLanes`, in the order of the benchmark. */
7350
+ const LANE_TRACKS = [
7351
+ "stack",
7352
+ "left-edge",
7353
+ "free"
7354
+ ];
7355
+ const EPS$2 = 1e-6;
6694
7356
  const ROUNDS = 8;
6695
7357
  /**
6696
7358
  * This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
@@ -6698,14 +7360,24 @@ const ROUNDS = 8;
6698
7360
  * When a move puts a link through more boxes than before, the link keeps its points.
6699
7361
  */
6700
7362
  function gapLanes(items, links, options = {}) {
7363
+ return laneGaps(items, links, options).links;
7364
+ }
7365
+ /** This function gives the lanes of each gap that holds a piece of a link, with the options of `gapLanes`. A gap without a piece is not in the list. */
7366
+ function gapLaneCounts(items, links, options = {}) {
7367
+ return laneGaps(items, links, options).counts;
7368
+ }
7369
+ /** This function does the work of `gapLanes` and `gapLaneCounts`. It gives the moved links and the lanes of each gap. */
7370
+ function laneGaps(items, links, options) {
6701
7371
  const spacing = Math.max(0, options.spacing ?? 4);
6702
7372
  const order = options.order ?? "crossings";
7373
+ const tracks = options.tracks ?? "stack";
6703
7374
  const moved = links.map((l) => l.points.map((p) => ({
6704
7375
  x: p.x,
6705
7376
  y: p.y
6706
7377
  })));
7378
+ const counts = [];
6707
7379
  for (const g of boardGaps(items, options.snap)) {
6708
- if (g.width <= EPS$1) continue;
7380
+ if (g.width <= EPS$2) continue;
6709
7381
  const across = g.axis;
6710
7382
  const along = g.axis === "x" ? "y" : "x";
6711
7383
  const lo = g.at - g.width;
@@ -6716,8 +7388,8 @@ function gapLanes(items, links, options = {}) {
6716
7388
  for (let i = 2; i <= p.length - 2; i++) {
6717
7389
  const a = p[i - 1];
6718
7390
  const b = p[i];
6719
- if (Math.abs(a[across] - b[across]) > EPS$1 || a[across] <= lo + EPS$1 || a[across] >= hi - EPS$1) continue;
6720
- if (Math.abs(a[along] - b[along]) <= EPS$1) continue;
7391
+ if (Math.abs(a[across] - b[across]) > EPS$2 || a[across] <= lo + EPS$2 || a[across] >= hi - EPS$2) continue;
7392
+ if (Math.abs(a[along] - b[along]) <= EPS$2) continue;
6721
7393
  const side = (q, r) => Math.sign(r[across] - q[across]);
6722
7394
  pieces.push({
6723
7395
  link: li,
@@ -6730,13 +7402,15 @@ function gapLanes(items, links, options = {}) {
6730
7402
  });
6731
7403
  if (pieces.length === 0) continue;
6732
7404
  const list = orderPieces(pieces, order);
6733
- const lane = [];
6734
- let n = 0;
6735
- list.forEach((P, i) => {
6736
- let k = 0;
6737
- for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
6738
- lane.push(k);
6739
- n = Math.max(n, k + 1);
7405
+ const lane = tracks === "stack" ? stackLanes(list) : leftEdgeLanes(list, tracks === "left-edge");
7406
+ const n = lane.reduce((m, k) => Math.max(m, k + 1), 0);
7407
+ counts.push({
7408
+ axis: g.axis,
7409
+ at: g.at,
7410
+ width: g.width,
7411
+ lanes: n,
7412
+ pieces: list.length,
7413
+ density: density(list)
6740
7414
  });
6741
7415
  const step = Math.min(spacing, g.width / (n + 1));
6742
7416
  const mid = (lo + hi) / 2;
@@ -6748,20 +7422,81 @@ function gapLanes(items, links, options = {}) {
6748
7422
  }
6749
7423
  const boxes = new Grid(Math.max(20, meanSide(items)));
6750
7424
  for (const i of items) boxes.add(i.box, i.box);
6751
- return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
6752
- ...l,
6753
- points: moved[i]
7425
+ return {
7426
+ links: links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
7427
+ ...l,
7428
+ points: moved[i]
7429
+ }),
7430
+ counts
7431
+ };
7432
+ }
7433
+ /** This function gives the lanes of the rule `stack`. A piece goes one lane past each piece before it in the order that it overlaps. */
7434
+ function stackLanes(list) {
7435
+ const lane = [];
7436
+ list.forEach((P, i) => {
7437
+ let k = 0;
7438
+ for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
7439
+ lane.push(k);
6754
7440
  });
7441
+ return lane;
7442
+ }
7443
+ /**
7444
+ * This function gives the lanes of the left-edge rule (Hashimoto and Stevens, 1971) for the pieces in lane order.
7445
+ * With `constrained`, a piece stays below a later piece that it overlaps when the swap of the two adds crossings (`cost`).
7446
+ * These constraints follow the lane order, so they have no cycle. Each pass fills one lane, from the near side.
7447
+ * A pass takes the free pieces whose constraints are met, in the order of their start. It puts each piece that does not overlap the last piece of the lane.
7448
+ */
7449
+ function leftEdgeLanes(list, constrained) {
7450
+ const n = list.length;
7451
+ const below = list.map(() => []);
7452
+ const wait = new Array(n).fill(0);
7453
+ if (constrained) for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
7454
+ const P = list[i];
7455
+ const Q = list[j];
7456
+ if (overlaps(P, Q) && cost$1(Q, P) > cost$1(P, Q)) {
7457
+ below[i].push(j);
7458
+ wait[j]++;
7459
+ }
7460
+ }
7461
+ const byStart = list.map((_, i) => i).sort((a, b) => list[a].lo - list[b].lo || a - b);
7462
+ const lane = new Array(n).fill(-1);
7463
+ let placed = 0;
7464
+ for (let track = 0; placed < n; track++) {
7465
+ const now = [];
7466
+ let end = -Infinity;
7467
+ for (const i of byStart) {
7468
+ if (lane[i] >= 0 || wait[i] > 0 || list[i].lo < end - EPS$2) continue;
7469
+ now.push(i);
7470
+ end = list[i].hi;
7471
+ }
7472
+ for (const i of now) lane[i] = track;
7473
+ for (const i of now) for (const j of below[i]) wait[j]--;
7474
+ placed += now.length;
7475
+ }
7476
+ return lane;
7477
+ }
7478
+ /** This function gives the largest number of pieces that overlap at one place. Pieces that only touch do not overlap. */
7479
+ function density(list) {
7480
+ const events = [];
7481
+ for (const P of list) events.push([P.lo, 1], [P.hi, -1]);
7482
+ events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
7483
+ let open = 0;
7484
+ let most = 0;
7485
+ for (const [, d] of events) {
7486
+ open += d;
7487
+ most = Math.max(most, open);
7488
+ }
7489
+ return most;
6755
7490
  }
6756
- const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS$1;
7491
+ const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS$2;
6757
7492
  /**
6758
7493
  * This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
6759
7494
  * A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
6760
7495
  */
6761
7496
  function cost$1(P, Q) {
6762
7497
  let c = 0;
6763
- for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS$1 && y < Q.hi - EPS$1) c++;
6764
- for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS$1 && y < P.hi - EPS$1) c++;
7498
+ for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS$2 && y < Q.hi - EPS$2) c++;
7499
+ for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS$2 && y < P.hi - EPS$2) c++;
6765
7500
  return c;
6766
7501
  }
6767
7502
  /** This function gives the pieces of one gap in lane order, from the near side to the far side. */
@@ -7857,7 +8592,23 @@ const RUN_LANE_ORDER = {
7857
8592
  default: "run"
7858
8593
  };
7859
8594
  /** The declarations of the options of the routers `corners` and `corners-smooth`. */
7860
- const CORNERS_SCHEMA = WAY_SCHEMA;
8595
+ const CORNERS_SCHEMA = {
8596
+ ...WAY_SCHEMA,
8597
+ plugs: {
8598
+ type: "enum",
8599
+ values: ["facing", "side"],
8600
+ default: "facing",
8601
+ describe: "With `facing`, a free plug faces the other end. With `side`, the search starts from the whole free side, and the plug goes where the way leaves (WILD-001)."
8602
+ },
8603
+ plugGap: {
8604
+ type: "number",
8605
+ min: 0,
8606
+ max: 24,
8607
+ step: 1,
8608
+ default: 0,
8609
+ describe: "With a gap above 0, the free plugs of a side keep their order and at least this gap (WILD-002). 0 keeps each plug where it is."
8610
+ }
8611
+ };
7861
8612
  /** The declarations of the options of the router `trunks`. */
7862
8613
  const TRUNKS_SCHEMA = {
7863
8614
  ...WAY_SCHEMA,
@@ -7887,6 +8638,34 @@ const TRUNKS_SCHEMA = {
7887
8638
  values: ["part", "split"],
7888
8639
  default: "part",
7889
8640
  describe: "With `part`, a lane leaves the trunk where its way parts from the other ways. With `split`, each lane leaves at the split point."
8641
+ },
8642
+ splitAt: {
8643
+ type: "enum",
8644
+ values: ["share", "gilbert"],
8645
+ default: "share",
8646
+ describe: "With `share`, the split point is at `split` of the way. With `gilbert`, it is where the flow cost is least (EDA-030)."
8647
+ },
8648
+ alpha: {
8649
+ type: "number",
8650
+ min: 0,
8651
+ max: 1,
8652
+ step: .05,
8653
+ default: .5,
8654
+ describe: "The exponent of the flow cost of the split `gilbert`. A smaller value moves the split toward the targets."
8655
+ },
8656
+ fan: {
8657
+ type: "enum",
8658
+ values: ["split", "tree"],
8659
+ default: "split",
8660
+ describe: "With `split`, the edges of a group share a trunk to a split point. With `tree`, they grow a balancing-factor tree (NAT-017)."
8661
+ },
8662
+ bf: {
8663
+ type: "number",
8664
+ min: 0,
8665
+ max: 1,
8666
+ step: .05,
8667
+ default: .4,
8668
+ describe: "The balancing factor of the fan `tree`. 0 gives the least ink, and a larger factor gives more direct routes."
7890
8669
  }
7891
8670
  };
7892
8671
  /** The declarations of the options of the routers `shared` and `shared-all`. */
@@ -7904,7 +8683,46 @@ const SHARED_SCHEMA = {
7904
8683
  default: .5,
7905
8684
  describe: "The cost of a link that an edge with the same key uses already, as a share of its length."
7906
8685
  },
7907
- laneOrder: LANE_ORDER
8686
+ laneOrder: LANE_ORDER,
8687
+ cost: {
8688
+ type: "enum",
8689
+ values: ["fixed", "flow"],
8690
+ default: "fixed",
8691
+ describe: "With `fixed`, a shared link costs `discount` of its length. With `flow`, a link with `f` edges costs `len * f^alpha` (EDA-029)."
8692
+ },
8693
+ alpha: {
8694
+ type: "number",
8695
+ min: .1,
8696
+ max: 1,
8697
+ step: .05,
8698
+ default: .6,
8699
+ describe: "The exponent of the cost `flow`. A smaller value gives more sharing."
8700
+ },
8701
+ passes: {
8702
+ type: "integer",
8703
+ min: 1,
8704
+ max: 5,
8705
+ step: 1,
8706
+ default: 1,
8707
+ describe: "In each pass after the first, each edge finds its way again with the ways of the other edges fixed (HWY-039)."
8708
+ },
8709
+ order: {
8710
+ type: "enum",
8711
+ values: [
8712
+ "span",
8713
+ "shortest",
8714
+ "load",
8715
+ "input"
8716
+ ],
8717
+ default: "span",
8718
+ describe: "The order of the edges of a key: the longest first, the shortest first, the most edges between two sections first, or the input order (NAT-024)."
8719
+ },
8720
+ merge: {
8721
+ type: "enum",
8722
+ values: ["corner", "snell"],
8723
+ default: "corner",
8724
+ describe: "With `corner`, a route joins a run at a corner of the graph. With `snell`, it joins at the angle `acos(d)` (NAT-035)."
8725
+ }
7908
8726
  };
7909
8727
  /** The declarations of the options of the highway strategy `corridors` (HWY-031). */
7910
8728
  const CORRIDOR_SCHEMA = {
@@ -8671,11 +9489,11 @@ const BUILT_IN_STRATEGIES = Object.freeze([
8671
9489
  /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
8672
9490
  const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
8673
9491
  // src/bench/measures/curves.ts
8674
- const EPS = 1e-9;
9492
+ const EPS$1 = 1e-9;
8675
9493
  const DEG = 180 / Math.PI;
8676
9494
  const unit = (x, y) => {
8677
9495
  const l = Math.hypot(x, y);
8678
- return l > EPS ? {
9496
+ return l > EPS$1 ? {
8679
9497
  x: x / l,
8680
9498
  y: y / l
8681
9499
  } : void 0;
@@ -8694,7 +9512,7 @@ function frame(from, s, t) {
8694
9512
  x: -Math.sin(a) * sign,
8695
9513
  y: Math.cos(a) * sign
8696
9514
  },
8697
- curvature: s.radius > EPS ? sign / s.radius : 0
9515
+ curvature: s.radius > EPS$1 ? sign / s.radius : 0
8698
9516
  };
8699
9517
  }
8700
9518
  const [p0, p1, p2, p3] = [
@@ -8718,7 +9536,7 @@ function frame(from, s, t) {
8718
9536
  y: 6 * (p1.y - 2 * p2.y + p3.y)
8719
9537
  };
8720
9538
  const speed = Math.hypot(d.x, d.y);
8721
- if (speed > EPS) return {
9539
+ if (speed > EPS$1) return {
8722
9540
  tangent: {
8723
9541
  x: d.x / speed,
8724
9542
  y: d.y / speed
@@ -8787,13 +9605,13 @@ function g2Defect(segments, u) {
8787
9605
  */
8788
9606
  function bendingEnergy(points, tolerance = .25) {
8789
9607
  const q = [];
8790
- for (const v of points) if (q.length === 0 || dist(q[q.length - 1], v) > EPS) q.push(v);
9608
+ for (const v of points) if (q.length === 0 || dist(q[q.length - 1], v) > EPS$1) q.push(v);
8791
9609
  if (q.length < 3) return 0;
8792
9610
  const phi = turns(q);
8793
9611
  let sum = 0;
8794
9612
  for (let i = 1; i + 1 < q.length; i++) {
8795
9613
  const t = Math.abs(phi[i - 1]);
8796
- if (t < EPS) continue;
9614
+ if (t < EPS$1) continue;
8797
9615
  const l = Math.min((dist(q[i - 1], q[i]) + dist(q[i], q[i + 1])) / 2, 8 * tolerance / t);
8798
9616
  sum += t * t / l;
8799
9617
  }
@@ -9420,15 +10238,7 @@ function measureBoard(graph, spacing, order) {
9420
10238
  order
9421
10239
  }) : level;
9422
10240
  const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
9423
- const edgeOf = /* @__PURE__ */ new Map();
9424
- for (const e of graph.edges) {
9425
- const key = `${e.source.node}\u0000${e.target.node}`;
9426
- if (!edgeOf.has(key)) edgeOf.set(key, e.id);
9427
- }
9428
- const q = measureQuality(graph, { routes: drawn.map((l) => ({
9429
- edge: edgeOf.get(`${l.source}\u0000${l.target}`),
9430
- points: l.points
9431
- })) });
10241
+ const q = farQuality(graph, drawn);
9432
10242
  const segments = segmentsOf(sectionsRouter({ name: "sections" }).route(graph));
9433
10243
  const lanes = laneRoutes(segments, new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges))), spacing);
9434
10244
  return {
@@ -9439,22 +10249,46 @@ function measureBoard(graph, spacing, order) {
9439
10249
  laneThrough: measureRouteFaults(graph, { routes: lanes }).through
9440
10250
  };
9441
10251
  }
9442
- /**
9443
- * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
9444
- * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
9445
- */
9446
- function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
9447
- const spacing = options.spacing ?? 4;
9448
- const area0 = areaOf$1(graph.nodes);
9449
- const links = roomLinks(graph);
9450
- const row = (board, nodes, ms) => ({
9451
- fixture,
9452
- board,
9453
- area: area0 > 0 ? areaOf$1(nodes) / area0 : 1,
9454
- ms,
9455
- ...measureBoard({
9456
- nodes,
9457
- edges: graph.edges
10252
+ /** This function gives the links of the far view of a board: `levelLinks` with each node as one item. */
10253
+ function farLinks(graph) {
10254
+ return levelLinks(graph.nodes.map((n) => ({
10255
+ id: n.id,
10256
+ box: n.box,
10257
+ nodes: [n.id]
10258
+ })), roomLinks(graph).map((l) => ({
10259
+ source: l.source,
10260
+ target: l.target,
10261
+ count: l.count ?? 1
10262
+ })));
10263
+ }
10264
+ /** This function measures the links of the far view with `measureQuality`. Each link stands for the first edge of its pair, because `measureQuality` reads routes of edges. */
10265
+ function farQuality(graph, drawn) {
10266
+ const edgeOf = /* @__PURE__ */ new Map();
10267
+ for (const e of graph.edges) {
10268
+ const key = `${e.source.node}\u0000${e.target.node}`;
10269
+ if (!edgeOf.has(key)) edgeOf.set(key, e.id);
10270
+ }
10271
+ return measureQuality(graph, { routes: drawn.map((l) => ({
10272
+ edge: edgeOf.get(`${l.source}\u0000${l.target}`),
10273
+ points: l.points
10274
+ })) });
10275
+ }
10276
+ /**
10277
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
10278
+ * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
10279
+ */
10280
+ function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
10281
+ const spacing = options.spacing ?? 4;
10282
+ const area0 = areaOf$1(graph.nodes);
10283
+ const links = roomLinks(graph);
10284
+ const row = (board, nodes, ms) => ({
10285
+ fixture,
10286
+ board,
10287
+ area: area0 > 0 ? areaOf$1(nodes) / area0 : 1,
10288
+ ms,
10289
+ ...measureBoard({
10290
+ nodes,
10291
+ edges: graph.edges
9458
10292
  }, spacing)
9459
10293
  });
9460
10294
  const rows = [row("packed", graph.nodes, 0)];
@@ -9779,6 +10613,94 @@ function formatBendRoom(rows) {
9779
10613
  ...rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.board} | ${r.area.toFixed(3)} | ${r.bundles} | ${(r.worstSpacing * 100).toFixed(1)}% | ${r.minRadius === Infinity ? "-" : r.minRadius.toFixed(2)} | ${r.underMin} | ${r.through} |`)
9780
10614
  ].join("\n");
9781
10615
  }
10616
+ /**
10617
+ * This function gives R12 (lane separation) of the links. Two legs of two links are near when they are parallel and nearer than `near`.
10618
+ * R12 is the length of the near legs, as a share of the ink.
10619
+ * It reads only the legs on an axis. The links of the far view have no other legs.
10620
+ */
10621
+ function laneSeparation(links, near) {
10622
+ let ink = 0;
10623
+ let close = 0;
10624
+ for (const across of ["x", "y"]) {
10625
+ const along = across === "x" ? "y" : "x";
10626
+ const legs = [];
10627
+ links.forEach((l, li) => {
10628
+ for (let i = 1; i < l.points.length; i++) {
10629
+ const a = l.points[i - 1];
10630
+ const b = l.points[i];
10631
+ if (Math.abs(a[across] - b[across]) > 1e-6 || Math.abs(a[along] - b[along]) <= 1e-6) continue;
10632
+ legs.push({
10633
+ link: li,
10634
+ at: a[across],
10635
+ lo: Math.min(a[along], b[along]),
10636
+ hi: Math.max(a[along], b[along])
10637
+ });
10638
+ }
10639
+ });
10640
+ legs.sort((p, q) => p.at - q.at);
10641
+ for (let i = 0; i < legs.length; i++) {
10642
+ const P = legs[i];
10643
+ ink += P.hi - P.lo;
10644
+ for (let j = i + 1; j < legs.length && legs[j].at - P.at < near; j++) {
10645
+ const Q = legs[j];
10646
+ if (Q.link !== P.link) close += Math.max(0, Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo));
10647
+ }
10648
+ }
10649
+ }
10650
+ return ink > 0 ? close / ink : 0;
10651
+ }
10652
+ /**
10653
+ * This function measures each lane rule of `gapLanes` (EDA-004) on the board as it is, and on the board that `makeRoom` opens.
10654
+ * The lane order is `crossings` for each rule. The lanes are `spacing` apart where the gap has room.
10655
+ */
10656
+ function benchTracks(fixture, graph, spacing = 4, rules = LANE_TRACKS) {
10657
+ const open = makeRoom(graph.nodes, roomLinks(graph), {
10658
+ gutter: 8,
10659
+ spacing,
10660
+ method: "routes"
10661
+ }).items;
10662
+ const rows = [];
10663
+ for (const [board, nodes] of [["given", graph.nodes], ["open", open]]) {
10664
+ const g = {
10665
+ nodes,
10666
+ edges: graph.edges
10667
+ };
10668
+ const level = farLinks(g);
10669
+ const boxes = new Map(nodes.map((n) => [n.id, n.box]));
10670
+ for (const tracks of rules) {
10671
+ const options = {
10672
+ spacing,
10673
+ order: "crossings",
10674
+ tracks
10675
+ };
10676
+ const counts = gapLaneCounts(nodes, level, options);
10677
+ const drawn = gapLanes(nodes, level, options);
10678
+ const q = farQuality(g, drawn);
10679
+ rows.push({
10680
+ fixture,
10681
+ board,
10682
+ tracks,
10683
+ gaps: counts.length,
10684
+ lanes: counts.reduce((s, c) => s + c.lanes, 0),
10685
+ density: counts.reduce((s, c) => s + c.density, 0),
10686
+ squeezed: counts.filter((c) => c.width / (c.lanes + 1) < spacing - 1e-9).length,
10687
+ crossings: q.crossings,
10688
+ ink: q.ink,
10689
+ separation: laneSeparation(drawn, spacing),
10690
+ through: measureLinks(boxes, drawn).through
10691
+ });
10692
+ }
10693
+ }
10694
+ return rows;
10695
+ }
10696
+ /** This function writes the rows of `benchTracks` as a Markdown table. */
10697
+ function formatTracks(rows) {
10698
+ return [
10699
+ "| fixture | board | tracks | gaps | lanes | density | squeezed | crossings | ink | R12 | through |",
10700
+ "|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|",
10701
+ ...rows.map((r) => `| ${r.fixture} | ${r.board} | ${r.tracks} | ${r.gaps} | ${r.lanes} | ${r.density} | ${r.squeezed} | ${r.crossings} | ${Math.round(r.ink)} | ${r.separation.toFixed(4)} | ${r.through} |`)
10702
+ ].join("\n");
10703
+ }
9782
10704
  // src/bench/sections.ts
9783
10705
  const now$1 = () => globalThis.performance?.now() ?? Date.now();
9784
10706
  /** This function gives the number of items of the first level with more than one item, or 0 for a result without levels. */
@@ -10268,6 +11190,104 @@ function ambiguity(graph, routes, options = {}) {
10268
11190
  estimated: !exact
10269
11191
  };
10270
11192
  }
11193
+ /** The drawn segments of each route, as keys, in the order of its points. A segment shorter than the grain of `segmentKey` has no key. */
11194
+ function routeKeys(routes) {
11195
+ const keys = /* @__PURE__ */ new Map();
11196
+ const users = /* @__PURE__ */ new Map();
11197
+ for (const r of routes) {
11198
+ const own = [];
11199
+ for (let i = 1; i < r.points.length; i++) {
11200
+ const k = segmentKey(r.points[i - 1], r.points[i]);
11201
+ if (k === void 0) continue;
11202
+ own.push(k);
11203
+ let set = users.get(k);
11204
+ if (!set) users.set(k, set = /* @__PURE__ */ new Set());
11205
+ set.add(r.edge);
11206
+ }
11207
+ keys.set(r.edge, own);
11208
+ }
11209
+ return {
11210
+ keys,
11211
+ users
11212
+ };
11213
+ }
11214
+ /**
11215
+ * This function gives the bundled share of a result (B2 of MEAS0): `count(e : e is in a run or a bundle) / m`.
11216
+ * An edge is also in a bundle when its route shares a drawn segment with the route of another edge. Thus a router without `runs` and `bundles` gets a value too.
11217
+ * The scale is 0 to 1. A higher value is better only in context. The cost is linear.
11218
+ */
11219
+ function bundledShare(result) {
11220
+ const routes = result.routes;
11221
+ if (routes.length === 0) return 0;
11222
+ const shared = /* @__PURE__ */ new Set();
11223
+ for (const run of result.runs ?? []) for (const e of run.edges) shared.add(e);
11224
+ for (const b of result.bundles ?? []) if (b.edges.length > 1) for (const e of b.edges) shared.add(e);
11225
+ const { keys, users } = routeKeys(routes);
11226
+ for (const [edge, own] of keys) if (own.some((k) => (users.get(k)?.size ?? 0) > 1)) shared.add(edge);
11227
+ let count = 0;
11228
+ for (const r of routes) if (shared.has(r.edge)) count++;
11229
+ return count / routes.length;
11230
+ }
11231
+ /**
11232
+ * This function gives the bundle stretch (B3 of MEAS0): `st(e) = L(r_e) / L(r*_e)` for each edge, with `r*_e` the base route of the edge.
11233
+ * The base routes are the routes of `corners` on the same input. An edge without a base route, or with a base route of 1 unit or less, does not count.
11234
+ * The cost is an extra run of the base router, and then linear.
11235
+ */
11236
+ function bundleStretch(routes, base) {
11237
+ const baseLength = /* @__PURE__ */ new Map();
11238
+ for (const r of base) baseLength.set(r.edge, polylineLength(r.points));
11239
+ const st = [];
11240
+ for (const r of routes) {
11241
+ const b = baseLength.get(r.edge);
11242
+ if (b === void 0 || b <= 1 || r.points.length < 2) continue;
11243
+ st.push(polylineLength(r.points) / b);
11244
+ }
11245
+ if (st.length === 0) return {
11246
+ median: 1,
11247
+ q95: 1,
11248
+ edges: 0
11249
+ };
11250
+ return {
11251
+ median: quantile(st, .5),
11252
+ q95: quantile(st, .95),
11253
+ edges: st.length
11254
+ };
11255
+ }
11256
+ /**
11257
+ * This function gives the merge angle of the routes (B6 of MEAS0): the largest turn of a lane where it joins or leaves a run, in degrees.
11258
+ * A run is a drawn segment that the routes of two edges or more share. A route joins or leaves a run at a vertex where the edges on its segment change.
11259
+ * One of the two segments at that vertex has two edges or more. The turn is the angle between the two segments.
11260
+ * Routes without a shared segment give 0. The scale is 0 to 180 degrees, and lower is better. The cost is linear.
11261
+ */
11262
+ function mergeAngle(routes) {
11263
+ const { users } = routeKeys(routes);
11264
+ const sig = /* @__PURE__ */ new Map();
11265
+ for (const [k, set] of users) sig.set(k, [...set].sort().join("\0"));
11266
+ let max = 0;
11267
+ for (const r of routes) {
11268
+ let prev;
11269
+ for (let i = 1; i < r.points.length; i++) {
11270
+ const a = r.points[i - 1];
11271
+ const b = r.points[i];
11272
+ const k = segmentKey(a, b);
11273
+ if (k === void 0) continue;
11274
+ if (prev && sig.get(prev.k) !== sig.get(k) && ((users.get(prev.k)?.size ?? 0) > 1 || (users.get(k)?.size ?? 0) > 1)) {
11275
+ const ux = prev.b.x - prev.a.x;
11276
+ const uy = prev.b.y - prev.a.y;
11277
+ const vx = b.x - a.x;
11278
+ const vy = b.y - a.y;
11279
+ const turn = Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
11280
+ max = Math.max(max, turn * 180 / Math.PI);
11281
+ }
11282
+ prev = {
11283
+ k,
11284
+ a,
11285
+ b
11286
+ };
11287
+ }
11288
+ }
11289
+ return max;
11290
+ }
10271
11291
  // src/bench/measures/levels.ts
10272
11292
  /** This function gives the levels in the order of their depth, from the far view to the near view. */
10273
11293
  function byDepth(levels) {
@@ -10631,27 +11651,337 @@ function paretoFront(items, values, objectives) {
10631
11651
  const v = items.map(values);
10632
11652
  return items.filter((_, i) => !v.some((w, j) => j !== i && dominates(w, v[i], objectives)));
10633
11653
  }
10634
- // src/highways/follow.ts
10635
- const SAME = 1e-6;
10636
- const dedupe = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > SAME);
10637
- /** This function tells if a polyline holds the points of `part` in a row, in this order. */
10638
- function holdsInOrder(p, part) {
10639
- for (let i = 0; i + part.length <= p.length; i++) {
10640
- let ok = true;
10641
- for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME;
10642
- if (ok) return true;
11654
+ // src/bench/screen/hash.ts
11655
+ const K = [
11656
+ 1116352408,
11657
+ 1899447441,
11658
+ 3049323471,
11659
+ 3921009573,
11660
+ 961987163,
11661
+ 1508970993,
11662
+ 2453635748,
11663
+ 2870763221,
11664
+ 3624381080,
11665
+ 310598401,
11666
+ 607225278,
11667
+ 1426881987,
11668
+ 1925078388,
11669
+ 2162078206,
11670
+ 2614888103,
11671
+ 3248222580,
11672
+ 3835390401,
11673
+ 4022224774,
11674
+ 264347078,
11675
+ 604807628,
11676
+ 770255983,
11677
+ 1249150122,
11678
+ 1555081692,
11679
+ 1996064986,
11680
+ 2554220882,
11681
+ 2821834349,
11682
+ 2952996808,
11683
+ 3210313671,
11684
+ 3336571891,
11685
+ 3584528711,
11686
+ 113926993,
11687
+ 338241895,
11688
+ 666307205,
11689
+ 773529912,
11690
+ 1294757372,
11691
+ 1396182291,
11692
+ 1695183700,
11693
+ 1986661051,
11694
+ 2177026350,
11695
+ 2456956037,
11696
+ 2730485921,
11697
+ 2820302411,
11698
+ 3259730800,
11699
+ 3345764771,
11700
+ 3516065817,
11701
+ 3600352804,
11702
+ 4094571909,
11703
+ 275423344,
11704
+ 430227734,
11705
+ 506948616,
11706
+ 659060556,
11707
+ 883997877,
11708
+ 958139571,
11709
+ 1322822218,
11710
+ 1537002063,
11711
+ 1747873779,
11712
+ 1955562222,
11713
+ 2024104815,
11714
+ 2227730452,
11715
+ 2361852424,
11716
+ 2428436474,
11717
+ 2756734187,
11718
+ 3204031479,
11719
+ 3329325298
11720
+ ];
11721
+ /** This function gives the bytes of a string in UTF-8. */
11722
+ function utf8(text) {
11723
+ const out = [];
11724
+ for (const ch of text) {
11725
+ const c = ch.codePointAt(0);
11726
+ if (c < 128) out.push(c);
11727
+ else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
11728
+ else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
11729
+ else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
10643
11730
  }
10644
- return false;
11731
+ return Uint8Array.from(out);
11732
+ }
11733
+ /** This function gives the SHA-256 digest of a string, as 64 lowercase hex characters. It encodes the string in UTF-8. */
11734
+ function sha256Hex(text) {
11735
+ const bytes = utf8(text);
11736
+ const bits = bytes.length * 8;
11737
+ const total = Math.ceil((bytes.length + 9) / 64) * 64;
11738
+ const data = new Uint8Array(total);
11739
+ data.set(bytes);
11740
+ data[bytes.length] = 128;
11741
+ const view = new DataView(data.buffer);
11742
+ view.setUint32(total - 8, Math.floor(bits / 4294967296));
11743
+ view.setUint32(total - 4, bits >>> 0);
11744
+ const h = [
11745
+ 1779033703,
11746
+ 3144134277,
11747
+ 1013904242,
11748
+ 2773480762,
11749
+ 1359893119,
11750
+ 2600822924,
11751
+ 528734635,
11752
+ 1541459225
11753
+ ];
11754
+ const w = /* @__PURE__ */ new Uint32Array(64);
11755
+ const rotr = (x, n) => x >>> n | x << 32 - n;
11756
+ for (let off = 0; off < total; off += 64) {
11757
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
11758
+ for (let i = 16; i < 64; i++) {
11759
+ const a = w[i - 15];
11760
+ const b = w[i - 2];
11761
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
11762
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
11763
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
11764
+ }
11765
+ let [a, b, c, d, e, f, g, k] = h;
11766
+ for (let i = 0; i < 64; i++) {
11767
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
11768
+ const ch = e & f ^ ~e & g;
11769
+ const t1 = k + s1 + ch + K[i] + w[i] >>> 0;
11770
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
11771
+ k = g;
11772
+ g = f;
11773
+ f = e;
11774
+ e = d + t1 >>> 0;
11775
+ d = c;
11776
+ c = b;
11777
+ b = a;
11778
+ a = t1 + t2 >>> 0;
11779
+ }
11780
+ h[0] = h[0] + a >>> 0;
11781
+ h[1] = h[1] + b >>> 0;
11782
+ h[2] = h[2] + c >>> 0;
11783
+ h[3] = h[3] + d >>> 0;
11784
+ h[4] = h[4] + e >>> 0;
11785
+ h[5] = h[5] + f >>> 0;
11786
+ h[6] = h[6] + g >>> 0;
11787
+ h[7] = h[7] + k >>> 0;
11788
+ }
11789
+ return h.map((x) => x.toString(16).padStart(8, "0")).join("");
10645
11790
  }
10646
- /** This function tells if a polyline holds the points of `part` in a row, in either direction. */
10647
- const holds = (p, part) => holdsInOrder(p, part) || holdsInOrder(p, [...part].reverse());
10648
11791
  /**
10649
- * This function tells if a point of a spine can be the plug of an end. The point is on the box of the node.
10650
- * When the end has a fixed plug, the point is at that plug.
11792
+ * This function writes a value as canonical JSON: the keys of each object in sorted order, and no whitespace.
11793
+ * It drops a key whose value is `undefined`, as `JSON.stringify` does.
10651
11794
  */
10652
- function plugAt(node, end, p) {
10653
- if (pointBoxDistance(p, node.box) > SAME) return false;
10654
- const fixed = fixedPortPoint(node, end);
11795
+ function canonicalJson(value) {
11796
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
11797
+ if (Array.isArray(value)) return `[${value.map((v) => v === void 0 ? "null" : canonicalJson(v)).join(",")}]`;
11798
+ const o = value;
11799
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
11800
+ }
11801
+ /**
11802
+ * This function gives the hash of a JSON document: the first 16 hex characters of the SHA-256 of its canonical JSON.
11803
+ * The document does not include its own field `hash`. The function removes that field before it calculates the hash.
11804
+ */
11805
+ function documentHash(value) {
11806
+ const { hash: _hash, ...rest } = value;
11807
+ return sha256Hex(canonicalJson(rest)).slice(0, 16);
11808
+ }
11809
+ /** This function gives the 32-bit FNV-1a hash of the UTF-8 bytes of a text, as an unsigned integer. */
11810
+ function fnv1a32(text) {
11811
+ let h = 2166136261;
11812
+ for (const byte of utf8(text)) {
11813
+ h ^= byte;
11814
+ h = Math.imul(h, 16777619);
11815
+ }
11816
+ return h >>> 0;
11817
+ }
11818
+ /** This function gives the seed of one step of a generator: `fnv1a32("<seed>:<step>")`. */
11819
+ function stepSeed(seed, step) {
11820
+ return fnv1a32(`${seed}:${step}`);
11821
+ }
11822
+ /** This function gives the generator `mulberry32` for a seed. Each call gives the next number from 0 to below 1. */
11823
+ function mulberry32(seed) {
11824
+ let a = seed >>> 0;
11825
+ return () => {
11826
+ a = a + 1831565813 >>> 0;
11827
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
11828
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
11829
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
11830
+ };
11831
+ }
11832
+ /** This function rounds a number to 3 decimals, as the files of a corpus keep it. The value `-0` becomes `0`. */
11833
+ function round3(value) {
11834
+ const r = Math.round(value * 1e3) / 1e3;
11835
+ return r === 0 ? 0 : r;
11836
+ }
11837
+ // src/order/index.ts
11838
+ /** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
11839
+ function byHash(a, b) {
11840
+ if (a.h !== b.h) return a.h - b.h;
11841
+ return a.e.id < b.e.id ? -1 : a.e.id > b.e.id ? 1 : 0;
11842
+ }
11843
+ /**
11844
+ * This function gives the graph with its edges in the stable order of WILD-053: by the FNV-1a hash of the id, then by the id.
11845
+ * The nodes and the edges do not change. Two orders of the same edges give the same graph.
11846
+ * The hash spreads the edges, so the stable order does not favor the edges of one node or one collection.
11847
+ */
11848
+ function stableOrder(graph) {
11849
+ const edges = graph.edges.map((e) => ({
11850
+ h: fnv1a32(e.id),
11851
+ e
11852
+ })).sort(byHash).map((x) => x.e);
11853
+ return {
11854
+ ...graph,
11855
+ edges
11856
+ };
11857
+ }
11858
+ /** This function gives the graph with its edges in a random order of the seed. The same graph and seed always give the same order. */
11859
+ function shuffleEdges(graph, seed) {
11860
+ const edges = [...graph.edges];
11861
+ const r = rng(seed);
11862
+ for (let i = edges.length - 1; i > 0; i--) {
11863
+ const j = Math.floor(r() * (i + 1));
11864
+ [edges[i], edges[j]] = [edges[j], edges[i]];
11865
+ }
11866
+ return {
11867
+ ...graph,
11868
+ edges
11869
+ };
11870
+ }
11871
+ /**
11872
+ * This function routes the graph in the stable order of `stableOrder`, with the function `route`.
11873
+ * The routes come back in the order of the input edges, so the caller sees no change of order.
11874
+ * The bundles, the runs and the levels come from the stable order. Thus each order of the input gives the same data.
11875
+ */
11876
+ function routeInStableOrder(graph, route) {
11877
+ const result = route(stableOrder(graph));
11878
+ const index = new Map(graph.edges.map((e, i) => [e.id, i]));
11879
+ const at = (edge) => index.get(edge) ?? Number.MAX_SAFE_INTEGER;
11880
+ const routes = [...result.routes].sort((a, b) => at(a.edge) - at(b.edge));
11881
+ return {
11882
+ ...result,
11883
+ routes
11884
+ };
11885
+ }
11886
+ /** This function gives a router that routes each graph in the stable order of WILD-053. It keeps the name and the `maxNodes` of the router. */
11887
+ function withStableOrder(router) {
11888
+ return {
11889
+ ...router,
11890
+ route: (graph) => routeInStableOrder(graph, (g) => router.route(g))
11891
+ };
11892
+ }
11893
+ /**
11894
+ * This function gives a router strategy that runs `strategy` in the stable order of WILD-053. The output does not depend on the order of the input edges.
11895
+ * The new strategy has the name `<name>+stable`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
11896
+ */
11897
+ function stableOrderStrategy(strategy) {
11898
+ return defineStrategy({
11899
+ step: "router",
11900
+ name: `${strategy.name}+stable`,
11901
+ describe: `${strategy.describe} The edges go in a stable order: by a hash of their ids.`,
11902
+ defaults: { ...strategy.defaults },
11903
+ ...strategy.schema === void 0 ? {} : { schema: strategy.schema },
11904
+ ...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
11905
+ run: (graph, options) => routeInStableOrder(graph, (g) => strategy.run(g, options))
11906
+ });
11907
+ }
11908
+ // src/bench/measures/order.ts
11909
+ /**
11910
+ * This function gives `IQR / median` of the values. Values with a median of 0 give `IQR / mean`, and values that are all 0 give 0.
11911
+ * An IQR below `1e-9` of the median is rounding noise of the sums, so it gives 0.
11912
+ */
11913
+ function spread(values) {
11914
+ const iqr = quantile(values, .75) - quantile(values, .25);
11915
+ const mid = Math.abs(quantile(values, .5));
11916
+ if (iqr <= 1e-9 * Math.max(1, mid)) return 0;
11917
+ if (mid > 0) return iqr / mid;
11918
+ const mean = values.reduce((s, v) => s + Math.abs(v), 0) / values.length;
11919
+ return mean > 0 ? iqr / mean : 0;
11920
+ }
11921
+ /** This function gives the routes by the id of the edge, in the order of the ids, so two orders of the input compare. */
11922
+ function byEdge(routes) {
11923
+ return [...routes].sort((a, b) => a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0);
11924
+ }
11925
+ /**
11926
+ * This function measures the order fragility of a router on one graph (W3, from WILD-056).
11927
+ * It routes the graph with `orders` random orders of the edges.
11928
+ * It gives the spread `IQR / median` of the crossings, the detour, the ink saved and the ink.
11929
+ * It also gives the share of orders that change a route, and the price of anarchy of the crossings.
11930
+ * The cost is `orders` extra runs of the router.
11931
+ */
11932
+ function orderFragility(graph, route, options = {}) {
11933
+ const orders = Math.max(1, Math.floor(options.orders ?? 20));
11934
+ const seed = options.seed ?? 1;
11935
+ const baseRoutes = byEdge((options.base ?? route(graph)).routes);
11936
+ const crossings = [];
11937
+ const detour = [];
11938
+ const saved = [];
11939
+ const ink = [];
11940
+ let changed = 0;
11941
+ for (let k = 0; k < orders; k++) {
11942
+ const routes = byEdge(route(shuffleEdges(graph, seed + k)).routes);
11943
+ const segs = drawnSegments(routes);
11944
+ crossings.push(crossingAngles(segs).crossings);
11945
+ detour.push(detourSpread(routes).median);
11946
+ saved.push(inkSaved(routes));
11947
+ let length = 0;
11948
+ for (const [a, b] of segs) length += Math.hypot(b.x - a.x, b.y - a.y);
11949
+ ink.push(length);
11950
+ if (!sameData(routes, baseRoutes)) changed++;
11951
+ }
11952
+ const own = crossingAngles(drawnSegments(baseRoutes)).crossings;
11953
+ const best = Math.min(...crossings);
11954
+ return {
11955
+ orders,
11956
+ crossings: spread(crossings),
11957
+ detour: spread(detour),
11958
+ inkSaved: spread(saved),
11959
+ ink: spread(ink),
11960
+ changed: changed / orders,
11961
+ poa: best > 0 ? own / best : own === 0 ? 1 : NaN
11962
+ };
11963
+ }
11964
+ // src/highways/follow.ts
11965
+ const SAME = 1e-6;
11966
+ const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > SAME);
11967
+ /** This function tells if a polyline holds the points of `part` in a row, in this order. */
11968
+ function holdsInOrder(p, part) {
11969
+ for (let i = 0; i + part.length <= p.length; i++) {
11970
+ let ok = true;
11971
+ for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j], part[j]) <= SAME;
11972
+ if (ok) return true;
11973
+ }
11974
+ return false;
11975
+ }
11976
+ /** This function tells if a polyline holds the points of `part` in a row, in either direction. */
11977
+ const holds$1 = (p, part) => holdsInOrder(p, part) || holdsInOrder(p, [...part].reverse());
11978
+ /**
11979
+ * This function tells if a point of a spine can be the plug of an end. The point is on the box of the node.
11980
+ * When the end has a fixed plug, the point is at that plug.
11981
+ */
11982
+ function plugAt(node, end, p) {
11983
+ if (pointBoxDistance(p, node.box) > SAME) return false;
11984
+ const fixed = fixedPortPoint(node, end);
10655
11985
  return fixed === void 0 || dist(fixed, p) <= SAME;
10656
11986
  }
10657
11987
  /** This function gives the points of the stops from the place `on` to the place `off`. When `on` is larger than `off`, the points go backward. */
@@ -10745,7 +12075,7 @@ function follow(graph, plan, options = {}) {
10745
12075
  }
10746
12076
  routes.push({
10747
12077
  edge: e.id,
10748
- points: dedupe(out)
12078
+ points: dedupe$1(out)
10749
12079
  });
10750
12080
  }
10751
12081
  const routeOf = new Map(routes.map((r) => [r.edge, r]));
@@ -10758,10 +12088,10 @@ function follow(graph, plan, options = {}) {
10758
12088
  const users = usersOf(k);
10759
12089
  let end = k + 1;
10760
12090
  while (end < stops.length - 1 && usersOf(end).join("\0") === users.join("\0")) end++;
10761
- const points = dedupe(stops.slice(k, end + 1).map(([, p]) => p));
12091
+ const points = dedupe$1(stops.slice(k, end + 1).map(([, p]) => p));
10762
12092
  k = end;
10763
12093
  if (users.length < 2 || points.length < 2) continue;
10764
- const mine = users.map((id) => routeOf.get(id)).filter((r) => holds(r.points, points));
12094
+ const mine = users.map((id) => routeOf.get(id)).filter((r) => holds$1(r.points, points));
10765
12095
  if (mine.length < 2) continue;
10766
12096
  runs.push({
10767
12097
  id: `r${runs.length}`,
@@ -12013,230 +13343,47 @@ function compareStep(step, graph, options = {}) {
12013
13343
  excluded: rows.filter((r) => !fair.includes(r)).map((r) => r.strategy)
12014
13344
  };
12015
13345
  }
12016
- // src/bench/screen/hash.ts
12017
- const K = [
12018
- 1116352408,
12019
- 1899447441,
12020
- 3049323471,
12021
- 3921009573,
12022
- 961987163,
12023
- 1508970993,
12024
- 2453635748,
12025
- 2870763221,
12026
- 3624381080,
12027
- 310598401,
12028
- 607225278,
12029
- 1426881987,
12030
- 1925078388,
12031
- 2162078206,
12032
- 2614888103,
12033
- 3248222580,
12034
- 3835390401,
12035
- 4022224774,
12036
- 264347078,
12037
- 604807628,
12038
- 770255983,
12039
- 1249150122,
12040
- 1555081692,
12041
- 1996064986,
12042
- 2554220882,
12043
- 2821834349,
12044
- 2952996808,
12045
- 3210313671,
12046
- 3336571891,
12047
- 3584528711,
12048
- 113926993,
12049
- 338241895,
12050
- 666307205,
12051
- 773529912,
12052
- 1294757372,
12053
- 1396182291,
12054
- 1695183700,
12055
- 1986661051,
12056
- 2177026350,
12057
- 2456956037,
12058
- 2730485921,
12059
- 2820302411,
12060
- 3259730800,
12061
- 3345764771,
12062
- 3516065817,
12063
- 3600352804,
12064
- 4094571909,
12065
- 275423344,
12066
- 430227734,
12067
- 506948616,
12068
- 659060556,
12069
- 883997877,
12070
- 958139571,
12071
- 1322822218,
12072
- 1537002063,
12073
- 1747873779,
12074
- 1955562222,
12075
- 2024104815,
12076
- 2227730452,
12077
- 2361852424,
12078
- 2428436474,
12079
- 2756734187,
12080
- 3204031479,
12081
- 3329325298
12082
- ];
12083
- /** This function gives the bytes of a string in UTF-8. */
12084
- function utf8(text) {
12085
- const out = [];
12086
- for (const ch of text) {
12087
- const c = ch.codePointAt(0);
12088
- if (c < 128) out.push(c);
12089
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
12090
- else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
12091
- else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
12092
- }
12093
- return Uint8Array.from(out);
13346
+ // src/bench/screen/forms.ts
13347
+ const TO_FILE = {
13348
+ north: "top",
13349
+ east: "right",
13350
+ south: "bottom",
13351
+ west: "left"
13352
+ };
13353
+ const TO_MODEL = {
13354
+ top: "north",
13355
+ right: "east",
13356
+ bottom: "south",
13357
+ left: "west"
13358
+ };
13359
+ /** This function gives the side of a box that holds a point, relative to the top-left corner of the box. */
13360
+ function sideAt(p, w, h) {
13361
+ return [
13362
+ ["top", Math.abs(p.y)],
13363
+ ["right", Math.abs(w - p.x)],
13364
+ ["bottom", Math.abs(h - p.y)],
13365
+ ["left", Math.abs(p.x)]
13366
+ ].reduce((a, b) => b[1] < a[1] ? b : a)[0];
12094
13367
  }
12095
- /** This function gives the SHA-256 digest of a string, as 64 lowercase hex characters. It encodes the string in UTF-8. */
12096
- function sha256Hex(text) {
12097
- const bytes = utf8(text);
12098
- const bits = bytes.length * 8;
12099
- const total = Math.ceil((bytes.length + 9) / 64) * 64;
12100
- const data = new Uint8Array(total);
12101
- data.set(bytes);
12102
- data[bytes.length] = 128;
12103
- const view = new DataView(data.buffer);
12104
- view.setUint32(total - 8, Math.floor(bits / 4294967296));
12105
- view.setUint32(total - 4, bits >>> 0);
12106
- const h = [
12107
- 1779033703,
12108
- 3144134277,
12109
- 1013904242,
12110
- 2773480762,
12111
- 1359893119,
12112
- 2600822924,
12113
- 528734635,
12114
- 1541459225
12115
- ];
12116
- const w = /* @__PURE__ */ new Uint32Array(64);
12117
- const rotr = (x, n) => x >>> n | x << 32 - n;
12118
- for (let off = 0; off < total; off += 64) {
12119
- for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
12120
- for (let i = 16; i < 64; i++) {
12121
- const a = w[i - 15];
12122
- const b = w[i - 2];
12123
- const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
12124
- const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
12125
- w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
12126
- }
12127
- let [a, b, c, d, e, f, g, k] = h;
12128
- for (let i = 0; i < 64; i++) {
12129
- const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
12130
- const ch = e & f ^ ~e & g;
12131
- const t1 = k + s1 + ch + K[i] + w[i] >>> 0;
12132
- const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
12133
- k = g;
12134
- g = f;
12135
- f = e;
12136
- e = d + t1 >>> 0;
12137
- d = c;
12138
- c = b;
12139
- b = a;
12140
- a = t1 + t2 >>> 0;
12141
- }
12142
- h[0] = h[0] + a >>> 0;
12143
- h[1] = h[1] + b >>> 0;
12144
- h[2] = h[2] + c >>> 0;
12145
- h[3] = h[3] + d >>> 0;
12146
- h[4] = h[4] + e >>> 0;
12147
- h[5] = h[5] + f >>> 0;
12148
- h[6] = h[6] + g >>> 0;
12149
- h[7] = h[7] + k >>> 0;
12150
- }
12151
- return h.map((x) => x.toString(16).padStart(8, "0")).join("");
12152
- }
12153
- /**
12154
- * This function writes a value as canonical JSON: the keys of each object in sorted order, and no whitespace.
12155
- * It drops a key whose value is `undefined`, as `JSON.stringify` does.
12156
- */
12157
- function canonicalJson(value) {
12158
- if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
12159
- if (Array.isArray(value)) return `[${value.map((v) => v === void 0 ? "null" : canonicalJson(v)).join(",")}]`;
12160
- const o = value;
12161
- return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
12162
- }
12163
- /**
12164
- * This function gives the hash of a JSON document: the first 16 hex characters of the SHA-256 of its canonical JSON.
12165
- * The document does not include its own field `hash`. The function removes that field before it calculates the hash.
12166
- */
12167
- function documentHash(value) {
12168
- const { hash: _hash, ...rest } = value;
12169
- return sha256Hex(canonicalJson(rest)).slice(0, 16);
12170
- }
12171
- /** This function gives the 32-bit FNV-1a hash of the UTF-8 bytes of a text, as an unsigned integer. */
12172
- function fnv1a32(text) {
12173
- let h = 2166136261;
12174
- for (const byte of utf8(text)) {
12175
- h ^= byte;
12176
- h = Math.imul(h, 16777619);
12177
- }
12178
- return h >>> 0;
12179
- }
12180
- /** This function gives the seed of one step of a generator: `fnv1a32("<seed>:<step>")`. */
12181
- function stepSeed(seed, step) {
12182
- return fnv1a32(`${seed}:${step}`);
12183
- }
12184
- /** This function gives the generator `mulberry32` for a seed. Each call gives the next number from 0 to below 1. */
12185
- function mulberry32(seed) {
12186
- let a = seed >>> 0;
12187
- return () => {
12188
- a = a + 1831565813 >>> 0;
12189
- let t = Math.imul(a ^ a >>> 15, 1 | a);
12190
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
12191
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
12192
- };
12193
- }
12194
- /** This function rounds a number to 3 decimals, as the files of a corpus keep it. The value `-0` becomes `0`. */
12195
- function round3(value) {
12196
- const r = Math.round(value * 1e3) / 1e3;
12197
- return r === 0 ? 0 : r;
12198
- }
12199
- // src/bench/screen/forms.ts
12200
- const TO_FILE = {
12201
- north: "top",
12202
- east: "right",
12203
- south: "bottom",
12204
- west: "left"
12205
- };
12206
- const TO_MODEL = {
12207
- top: "north",
12208
- right: "east",
12209
- bottom: "south",
12210
- left: "west"
12211
- };
12212
- /** This function gives the side of a box that holds a point, relative to the top-left corner of the box. */
12213
- function sideAt(p, w, h) {
12214
- return [
12215
- ["top", Math.abs(p.y)],
12216
- ["right", Math.abs(w - p.x)],
12217
- ["bottom", Math.abs(h - p.y)],
12218
- ["left", Math.abs(p.x)]
12219
- ].reduce((a, b) => b[1] < a[1] ? b : a)[0];
12220
- }
12221
- /** This function gives 0 for -0, because JSON writes -0 as 0. Then a file and the graph that it comes from hold the same numbers. */
12222
- const num = round3;
12223
- function portToFile(p, n) {
12224
- const side = p.side ? TO_FILE[p.side] : p.at ? sideAt(p.at, n.box.w, n.box.h) : void 0;
12225
- return {
12226
- id: p.id,
12227
- ...side ? { side } : {},
12228
- ...p.at ? {
12229
- x: num(p.at.x),
12230
- y: num(p.at.y)
12231
- } : {}
12232
- };
12233
- }
12234
- function portToModel(p) {
12235
- if (p.x !== void 0 && p.y !== void 0) return {
12236
- id: p.id,
12237
- at: {
12238
- x: p.x,
12239
- y: p.y
13368
+ /** This function gives 0 for -0, because JSON writes -0 as 0. Then a file and the graph that it comes from hold the same numbers. */
13369
+ const num = round3;
13370
+ function portToFile(p, n) {
13371
+ const side = p.side ? TO_FILE[p.side] : p.at ? sideAt(p.at, n.box.w, n.box.h) : void 0;
13372
+ return {
13373
+ id: p.id,
13374
+ ...side ? { side } : {},
13375
+ ...p.at ? {
13376
+ x: num(p.at.x),
13377
+ y: num(p.at.y)
13378
+ } : {}
13379
+ };
13380
+ }
13381
+ function portToModel(p) {
13382
+ if (p.x !== void 0 && p.y !== void 0) return {
13383
+ id: p.id,
13384
+ at: {
13385
+ x: p.x,
13386
+ y: p.y
12240
13387
  }
12241
13388
  };
12242
13389
  return {
@@ -12592,6 +13739,86 @@ const MEASURES = Object.freeze([
12592
13739
  "lower",
12593
13740
  false
12594
13741
  ],
13742
+ [
13743
+ "B2.share",
13744
+ "B2",
13745
+ "route.bundle.share",
13746
+ "0 to 1",
13747
+ "higher",
13748
+ false
13749
+ ],
13750
+ [
13751
+ "B3.median",
13752
+ "B3",
13753
+ "route.bundle.stretch.median",
13754
+ "ratio",
13755
+ "lower",
13756
+ false
13757
+ ],
13758
+ [
13759
+ "B3.q95",
13760
+ "B3",
13761
+ "route.bundle.stretch.q95",
13762
+ "ratio",
13763
+ "lower",
13764
+ false
13765
+ ],
13766
+ [
13767
+ "B6.max",
13768
+ "B6",
13769
+ "route.bundle.merge-angle.max",
13770
+ "degrees",
13771
+ "lower",
13772
+ false
13773
+ ],
13774
+ [
13775
+ "W3.crossings",
13776
+ "W3",
13777
+ "route.order.crossings",
13778
+ "IQR over the median",
13779
+ "lower",
13780
+ true
13781
+ ],
13782
+ [
13783
+ "W3.detour",
13784
+ "W3",
13785
+ "route.order.detour",
13786
+ "IQR over the median",
13787
+ "lower",
13788
+ true
13789
+ ],
13790
+ [
13791
+ "W3.inkSaved",
13792
+ "W3",
13793
+ "route.order.ink-saved",
13794
+ "IQR over the median",
13795
+ "lower",
13796
+ true
13797
+ ],
13798
+ [
13799
+ "W3.ink",
13800
+ "W3",
13801
+ "route.order.ink",
13802
+ "IQR over the median",
13803
+ "lower",
13804
+ true
13805
+ ],
13806
+ [
13807
+ "W3.changed",
13808
+ "W3",
13809
+ "route.order.changed",
13810
+ "0 to 1",
13811
+ "lower",
13812
+ true
13813
+ ],
13814
+ [
13815
+ "W3.poa",
13816
+ "W3",
13817
+ "route.order.poa",
13818
+ "ratio",
13819
+ "lower",
13820
+ true
13821
+ ],
12595
13822
  [
12596
13823
  "Z10.mean",
12597
13824
  "Z10",
@@ -13035,6 +14262,639 @@ function corpusManifest(corpus, inputs) {
13035
14262
  }))
13036
14263
  };
13037
14264
  }
14265
+ // src/repair/walk.ts
14266
+ /** The sides of a box, as the indexes of two corners: north, east, south and west. */
14267
+ const SIDES = [
14268
+ [0, 1],
14269
+ [1, 2],
14270
+ [3, 2],
14271
+ [0, 3]
14272
+ ];
14273
+ const SIDE_INDEX = {
14274
+ north: 0,
14275
+ east: 1,
14276
+ south: 2,
14277
+ west: 3
14278
+ };
14279
+ /** This function gives the four corners of a box: top left, top right, bottom right and bottom left. */
14280
+ function corners(b) {
14281
+ return [
14282
+ {
14283
+ x: b.x,
14284
+ y: b.y
14285
+ },
14286
+ {
14287
+ x: b.x + b.w,
14288
+ y: b.y
14289
+ },
14290
+ {
14291
+ x: b.x + b.w,
14292
+ y: b.y + b.h
14293
+ },
14294
+ {
14295
+ x: b.x,
14296
+ y: b.y + b.h
14297
+ }
14298
+ ];
14299
+ }
14300
+ const EPS = 1e-6;
14301
+ const keyOf = (p) => `${Math.round(p.x * 1e3)},${Math.round(p.y * 1e3)}`;
14302
+ /** This function tells if the point `c` is on the axis-parallel segment `pq`. */
14303
+ function onSide(c, p, q) {
14304
+ if (Math.abs(p.y - q.y) <= EPS) return Math.abs(c.y - p.y) <= EPS && c.x >= Math.min(p.x, q.x) - EPS && c.x <= Math.max(p.x, q.x) + EPS;
14305
+ return Math.abs(c.x - p.x) <= EPS && c.y >= Math.min(p.y, q.y) - EPS && c.y <= Math.max(p.y, q.y) + EPS;
14306
+ }
14307
+ /** This function drops each point that is on the straight line between the point before it and the point after it. */
14308
+ function straighten(p) {
14309
+ const out = [];
14310
+ for (const v of p) {
14311
+ if (out.length > 0 && dist(out[out.length - 1], v) <= EPS) continue;
14312
+ while (out.length >= 2) {
14313
+ const a = out[out.length - 2];
14314
+ const b = out[out.length - 1];
14315
+ const cross = (b.x - a.x) * (v.y - b.y) - (b.y - a.y) * (v.x - b.x);
14316
+ const dot = (b.x - a.x) * (v.x - b.x) + (b.y - a.y) * (v.y - b.y);
14317
+ if (Math.abs(cross) > EPS || dot < 0) break;
14318
+ out.pop();
14319
+ }
14320
+ out.push(v);
14321
+ }
14322
+ return out;
14323
+ }
14324
+ /**
14325
+ * The boundary walk of the repair step (RULE-046): a graph of the sides of the boxes.
14326
+ * The vertices are the corners of the boxes. The edges are the parts of each side between two corners on it.
14327
+ * A walk on this graph goes along the boundaries of the boxes, and not through a box. Thus it joins two boxes also when the boxes touch.
14328
+ * The graph drops each part of a side that goes through another box, so overlapping boxes do not open a way.
14329
+ */
14330
+ var BoundaryWalk = class {
14331
+ #points = [];
14332
+ #index = /* @__PURE__ */ new Map();
14333
+ #next = [];
14334
+ /** The vertices on each side of each node, in the order along the side. */
14335
+ #sides = /* @__PURE__ */ new Map();
14336
+ /** This constructor makes the graph of the sides of the nodes. `grid` holds the nodes by their boxes. */
14337
+ constructor(nodes, grid) {
14338
+ for (const n of nodes) for (const c of corners(n.box)) this.#vertex(c);
14339
+ for (const n of nodes) {
14340
+ const cs = corners(n.box);
14341
+ const lists = [];
14342
+ for (const [i, j] of SIDES) {
14343
+ const p = cs[i];
14344
+ const q = cs[j];
14345
+ const on = /* @__PURE__ */ new Set([this.#vertex(p), this.#vertex(q)]);
14346
+ const area = {
14347
+ x: Math.min(p.x, q.x) - 1,
14348
+ y: Math.min(p.y, q.y) - 1,
14349
+ w: Math.abs(q.x - p.x) + 2,
14350
+ h: Math.abs(q.y - p.y) + 2
14351
+ };
14352
+ grid.query(area, (m) => {
14353
+ for (const c of corners(m.box)) if (onSide(c, p, q)) on.add(this.#vertex(c));
14354
+ });
14355
+ const list = [...on].sort((u, v) => dist(p, this.#points[u]) - dist(p, this.#points[v]) || u - v);
14356
+ lists.push(list);
14357
+ for (let k = 1; k < list.length; k++) {
14358
+ const u = list[k - 1];
14359
+ const v = list[k];
14360
+ const a = this.#points[u];
14361
+ const b = this.#points[v];
14362
+ if (dist(a, b) <= EPS || this.#blocked(grid, a, b)) continue;
14363
+ this.#link(u, v);
14364
+ }
14365
+ }
14366
+ this.#sides.set(n, lists);
14367
+ }
14368
+ }
14369
+ /** This method gives the index of the vertex at the point, and adds the vertex if it is new. */
14370
+ #vertex(p) {
14371
+ const k = keyOf(p);
14372
+ let i = this.#index.get(k);
14373
+ if (i === void 0) {
14374
+ i = this.#points.length;
14375
+ this.#points.push(p);
14376
+ this.#next.push([]);
14377
+ this.#index.set(k, i);
14378
+ }
14379
+ return i;
14380
+ }
14381
+ #link(u, v) {
14382
+ const d = dist(this.#points[u], this.#points[v]);
14383
+ if (this.#next[u].some((e) => e.to === v)) return;
14384
+ this.#next[u].push({
14385
+ to: v,
14386
+ d
14387
+ });
14388
+ this.#next[v].push({
14389
+ to: u,
14390
+ d
14391
+ });
14392
+ }
14393
+ /** This method tells if the segment goes through a box, as the rule `no-pass-through` tests it. */
14394
+ #blocked(grid, a, b) {
14395
+ return grid.someSegment(a, b, 0, (n) => segmentHitsBox(a, b, inflate(n.box, -.5)));
14396
+ }
14397
+ /** This method finds the side of `node` that is nearest to `p`. It gives the two vertices of that side that are next to `p`. */
14398
+ #around(node, p) {
14399
+ const list = this.#sides.get(node)?.[SIDE_INDEX[nearestSide(node.box, p)]];
14400
+ if (!list || list.length === 0) return [];
14401
+ const from = this.#points[list[0]];
14402
+ const at = dist(from, p);
14403
+ let k = 0;
14404
+ while (k < list.length && dist(from, this.#points[list[k]]) < at) k++;
14405
+ return [list[k - 1], list[k]].filter((v) => v !== void 0);
14406
+ }
14407
+ /**
14408
+ * This method gives a way along the boundaries of the boxes. The way goes from the point `a` on the box of `s` to the point `b` on the box of `t`.
14409
+ * A point that is not on its box goes first to the nearest point of the boundary.
14410
+ * The method gives `undefined` when no way joins the two boxes.
14411
+ */
14412
+ path(a, s, b, t) {
14413
+ const a1 = nearestOnBoundary(s.box, a);
14414
+ const b1 = nearestOnBoundary(t.box, b);
14415
+ const n = this.#points.length;
14416
+ const src = n;
14417
+ const dst = n + 1;
14418
+ const extra = /* @__PURE__ */ new Map();
14419
+ const add = (u, v, d) => {
14420
+ if (!extra.has(u)) extra.set(u, []);
14421
+ extra.get(u).push({
14422
+ to: v,
14423
+ d
14424
+ });
14425
+ };
14426
+ for (const v of this.#around(s, a1)) add(src, v, dist(a1, this.#points[v]));
14427
+ for (const v of this.#around(t, b1)) add(v, dst, dist(b1, this.#points[v]));
14428
+ if (!extra.has(src)) return void 0;
14429
+ const pointOf = (i) => i === src ? a1 : i === dst ? b1 : this.#points[i];
14430
+ const best = new Float64Array(n + 2).fill(Infinity);
14431
+ const prev = new Int32Array(n + 2).fill(-1);
14432
+ best[src] = 0;
14433
+ const heap = [[0, src]];
14434
+ const less = (x, y) => x[0] < y[0] || x[0] === y[0] && x[1] < y[1];
14435
+ const pop = () => {
14436
+ const top = heap[0];
14437
+ const last = heap.pop();
14438
+ if (heap.length > 0) {
14439
+ heap[0] = last;
14440
+ let i = 0;
14441
+ for (;;) {
14442
+ const l = 2 * i + 1;
14443
+ const r = l + 1;
14444
+ let m = i;
14445
+ if (l < heap.length && less(heap[l], heap[m])) m = l;
14446
+ if (r < heap.length && less(heap[r], heap[m])) m = r;
14447
+ if (m === i) break;
14448
+ [heap[i], heap[m]] = [heap[m], heap[i]];
14449
+ i = m;
14450
+ }
14451
+ }
14452
+ return top;
14453
+ };
14454
+ const push = (x) => {
14455
+ heap.push(x);
14456
+ let i = heap.length - 1;
14457
+ while (i > 0) {
14458
+ const p = i - 1 >> 1;
14459
+ if (!less(heap[i], heap[p])) break;
14460
+ [heap[i], heap[p]] = [heap[p], heap[i]];
14461
+ i = p;
14462
+ }
14463
+ };
14464
+ while (heap.length > 0) {
14465
+ const [d, u] = pop();
14466
+ if (d > best[u]) continue;
14467
+ if (u === dst) break;
14468
+ const edges = [...u < n ? this.#next[u] : [], ...extra.get(u) ?? []];
14469
+ for (const e of edges) {
14470
+ const nd = d + e.d;
14471
+ if (nd < best[e.to]) {
14472
+ best[e.to] = nd;
14473
+ prev[e.to] = u;
14474
+ push([nd, e.to]);
14475
+ }
14476
+ }
14477
+ }
14478
+ if (prev[dst] === -1) return void 0;
14479
+ const way = [];
14480
+ for (let i = dst; i !== -1; i = prev[i]) way.push(pointOf(i));
14481
+ way.reverse();
14482
+ return straighten([
14483
+ a,
14484
+ ...way,
14485
+ b
14486
+ ]);
14487
+ }
14488
+ };
14489
+ /** This function gives the point of the boundary of the box that is nearest to `p`. A point on the boundary gives itself. */
14490
+ function nearestOnBoundary(box, p) {
14491
+ const x0 = box.x;
14492
+ const y0 = box.y;
14493
+ const x1 = box.x + box.w;
14494
+ const y1 = box.y + box.h;
14495
+ if (!(p.x > x0 && p.x < x1 && p.y > y0 && p.y < y1)) return {
14496
+ x: Math.min(x1, Math.max(x0, p.x)),
14497
+ y: Math.min(y1, Math.max(y0, p.y))
14498
+ };
14499
+ const side = nearestSide(box, p);
14500
+ if (side === "north") return {
14501
+ x: p.x,
14502
+ y: y0
14503
+ };
14504
+ if (side === "south") return {
14505
+ x: p.x,
14506
+ y: y1
14507
+ };
14508
+ if (side === "west") return {
14509
+ x: x0,
14510
+ y: p.y
14511
+ };
14512
+ return {
14513
+ x: x1,
14514
+ y: p.y
14515
+ };
14516
+ }
14517
+ // src/repair/index.ts
14518
+ /** The repair step makes at most this number of fixes for one route. A route that needs more keeps its base route. */
14519
+ const MAX_FIXES = 64;
14520
+ /** This function tells if the point is on the boundary of the box, within `TOLERANCE`, as the rule `link-ends` tests it. */
14521
+ function onBoundary(p, b) {
14522
+ const inner = inflate(b, -.5);
14523
+ return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
14524
+ }
14525
+ /** This function tells if the polyline `p` holds the points of `part` in a row, within `TOLERANCE`. */
14526
+ function holds(p, part) {
14527
+ if (part.length === 0) return true;
14528
+ for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
14529
+ return false;
14530
+ }
14531
+ /** This function drops each point that is at the same place as the point before it. */
14532
+ function dedupe(p) {
14533
+ const out = [];
14534
+ for (const v of p) if (out.length === 0 || dist(out[out.length - 1], v) > 1e-9) out.push(v);
14535
+ return out;
14536
+ }
14537
+ /**
14538
+ * This function repairs the breaks of the hard rules in the routes of any router (RULE-046). The input result does not change.
14539
+ *
14540
+ * - It adds a route through the corner graph for each edge without a route (`complete`).
14541
+ * - It moves each end back to its fixed plug, or to the nearest point of the box of its node (`ends-at-plugs` and `fixed-plugs`).
14542
+ * - It pushes each inner leg out of a box that it goes through, to the near side of the box (`no-pass-through`).
14543
+ * - It replaces each other leg through a box with a detour through the corner graph.
14544
+ * - When a leg does not repair, it routes the whole route again through the corner graph.
14545
+ * Else it walks along the boundaries of the boxes (`BoundaryWalk`).
14546
+ * - A route that still breaks a rule keeps its base route.
14547
+ * - It takes an edge out of a run or a bundle when its new route does not hold the run or the trunk (`run-shared` and `bundle-trunk`).
14548
+ * - It moves the ends of each level link to the boundaries of its items (`link-ends`).
14549
+ *
14550
+ * A route without a break does not change. The same input gives the same output.
14551
+ */
14552
+ function repairResult(graph, result, options = {}) {
14553
+ const margin = options.margin ?? 5;
14554
+ const doPush = options.push ?? true;
14555
+ const doDetour = options.detour ?? true;
14556
+ const doReroute = options.reroute ?? true;
14557
+ const doWalk = options.walk ?? true;
14558
+ const byId = nodeIndex(graph);
14559
+ const grid = new Grid(180);
14560
+ for (const n of graph.nodes) grid.add(n, n.box);
14561
+ let ways;
14562
+ let walker;
14563
+ const walkerOf = () => walker ??= new BoundaryWalk(graph.nodes, grid);
14564
+ const waysOf = () => ways ??= new WayFinder(graph.nodes, {
14565
+ margin,
14566
+ ...options.k === void 0 ? {} : { k: options.k }
14567
+ });
14568
+ const counts = {
14569
+ routes: 0,
14570
+ faulty: 0,
14571
+ snapped: 0,
14572
+ pushed: 0,
14573
+ detoured: 0,
14574
+ rerouted: 0,
14575
+ walked: 0,
14576
+ added: 0,
14577
+ kept: 0,
14578
+ unbundled: 0,
14579
+ linkEnds: 0
14580
+ };
14581
+ const hitOf = (p, q, s, t) => {
14582
+ let hit;
14583
+ grid.querySegment(p, q, 0, (n) => {
14584
+ if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
14585
+ });
14586
+ return hit;
14587
+ };
14588
+ const clear = (p, s, t) => {
14589
+ for (let i = 1; i < p.length; i++) if (hitOf(p[i - 1], p[i], s, t)) return false;
14590
+ return true;
14591
+ };
14592
+ const leave = (p) => {
14593
+ let on;
14594
+ grid.query({
14595
+ x: p.x - TOLERANCE,
14596
+ y: p.y - TOLERANCE,
14597
+ w: 2 * TOLERANCE,
14598
+ h: 2 * TOLERANCE
14599
+ }, (n) => {
14600
+ if (!on && pointBoxDistance(p, n.box) <= .5) on = n;
14601
+ });
14602
+ if (!on) return p;
14603
+ const at = nearestOnBoundary(on.box, p);
14604
+ const nrm = SIDE_NORMAL[nearestSide(on.box, at)];
14605
+ return {
14606
+ x: at.x + nrm.x * margin,
14607
+ y: at.y + nrm.y * margin
14608
+ };
14609
+ };
14610
+ const push = (p, i, box, s, t) => {
14611
+ const a = p[i - 1];
14612
+ const b = p[i];
14613
+ const len = dist(a, b);
14614
+ if (len <= 0) return void 0;
14615
+ const nx = -(b.y - a.y) / len;
14616
+ const ny = (b.x - a.x) / len;
14617
+ const big = inflate(box, margin / 2);
14618
+ const proj = [
14619
+ {
14620
+ x: big.x,
14621
+ y: big.y
14622
+ },
14623
+ {
14624
+ x: big.x + big.w,
14625
+ y: big.y
14626
+ },
14627
+ {
14628
+ x: big.x,
14629
+ y: big.y + big.h
14630
+ },
14631
+ {
14632
+ x: big.x + big.w,
14633
+ y: big.y + big.h
14634
+ }
14635
+ ].map((c) => (c.x - a.x) * nx + (c.y - a.y) * ny);
14636
+ const shifts = [Math.max(...proj), Math.min(...proj)].sort((u, v) => Math.abs(u) - Math.abs(v));
14637
+ for (const d of shifts) {
14638
+ const moved = [...p];
14639
+ moved[i - 1] = {
14640
+ x: a.x + nx * d,
14641
+ y: a.y + ny * d
14642
+ };
14643
+ moved[i] = {
14644
+ x: b.x + nx * d,
14645
+ y: b.y + ny * d
14646
+ };
14647
+ if (clear([
14648
+ p[i - 2],
14649
+ moved[i - 1],
14650
+ moved[i],
14651
+ p[i + 1]
14652
+ ], s, t)) return moved;
14653
+ }
14654
+ };
14655
+ const detour = (p, i, s, t) => {
14656
+ const a = p[i - 1];
14657
+ const b = p[i];
14658
+ const finder = waysOf();
14659
+ const way = finder.corners.path(leave(a), leave(b));
14660
+ if (!way) return void 0;
14661
+ const mid = dedupe([
14662
+ a,
14663
+ ...finder.corners.pull(way),
14664
+ b
14665
+ ]);
14666
+ if (!clear(mid, s, t)) return void 0;
14667
+ return [
14668
+ ...p.slice(0, i - 1),
14669
+ ...mid,
14670
+ ...p.slice(i + 1)
14671
+ ];
14672
+ };
14673
+ const changed = /* @__PURE__ */ new Set();
14674
+ const repairOne = (r, s, t, e) => {
14675
+ let p = dedupe(r.points);
14676
+ if (p.length < 2) p = [...r.points];
14677
+ let snapped = 0;
14678
+ const first = p[0];
14679
+ const last = p[p.length - 1];
14680
+ const wantA = fixedPortPoint(s, e.source) ?? (pointBoxDistance(first, s.box) > .5 ? nearestOnBoundary(s.box, first) : void 0);
14681
+ const wantB = fixedPortPoint(t, e.target) ?? (pointBoxDistance(last, t.box) > .5 ? nearestOnBoundary(t.box, last) : void 0);
14682
+ if (wantA && dist(wantA, first) > .5) {
14683
+ p[0] = wantA;
14684
+ snapped++;
14685
+ }
14686
+ if (wantB && dist(wantB, last) > .5) {
14687
+ p[p.length - 1] = wantB;
14688
+ snapped++;
14689
+ }
14690
+ let fault = snapped > 0;
14691
+ let pushed = 0;
14692
+ let detoured = 0;
14693
+ let fixes = 0;
14694
+ let failed = false;
14695
+ for (let i = 1; i < p.length;) {
14696
+ const hit = hitOf(p[i - 1], p[i], s, t);
14697
+ if (!hit) {
14698
+ i++;
14699
+ continue;
14700
+ }
14701
+ fault = true;
14702
+ if (++fixes > MAX_FIXES) {
14703
+ failed = true;
14704
+ break;
14705
+ }
14706
+ const moved = doPush && i >= 2 && i + 1 < p.length ? push(p, i, hit.box, s, t) : void 0;
14707
+ if (moved) {
14708
+ p = moved;
14709
+ pushed++;
14710
+ i++;
14711
+ continue;
14712
+ }
14713
+ const around = doDetour ? detour(p, i, s, t) : void 0;
14714
+ if (!around) {
14715
+ failed = true;
14716
+ break;
14717
+ }
14718
+ i += around.length - p.length + 1;
14719
+ p = around;
14720
+ detoured++;
14721
+ }
14722
+ if (!fault) return r;
14723
+ counts.faulty++;
14724
+ let whole;
14725
+ if (failed) {
14726
+ const a = p[0];
14727
+ const b = p[p.length - 1];
14728
+ const finder = doReroute ? waysOf() : void 0;
14729
+ const way = finder?.corners.path(leave(a), leave(b));
14730
+ const again = way ? dedupe([
14731
+ a,
14732
+ ...finder.corners.pull(way),
14733
+ b
14734
+ ]) : void 0;
14735
+ if (again && clear(again, s, t)) {
14736
+ p = again;
14737
+ whole = "rerouted";
14738
+ } else {
14739
+ const walk = doWalk ? walkerOf().path(a, s, b, t) : void 0;
14740
+ if (walk && clear(walk, s, t)) {
14741
+ p = walk;
14742
+ whole = "walked";
14743
+ }
14744
+ }
14745
+ if (!whole) {
14746
+ counts.kept++;
14747
+ return r;
14748
+ }
14749
+ counts[whole]++;
14750
+ }
14751
+ counts.snapped += snapped;
14752
+ if (!whole) {
14753
+ counts.pushed += pushed;
14754
+ counts.detoured += detoured;
14755
+ }
14756
+ changed.add(r.edge);
14757
+ return {
14758
+ edge: r.edge,
14759
+ points: p
14760
+ };
14761
+ };
14762
+ const routeOf = /* @__PURE__ */ new Map();
14763
+ for (const r of result.routes) if (!routeOf.has(r.edge)) routeOf.set(r.edge, r);
14764
+ const repaired = /* @__PURE__ */ new Map();
14765
+ for (const e of graph.edges) {
14766
+ const s = byId.get(e.source.node);
14767
+ const t = byId.get(e.target.node);
14768
+ if (!s || !t || repaired.has(e.id)) continue;
14769
+ counts.routes++;
14770
+ const r = routeOf.get(e.id);
14771
+ if (r && r.points.length >= 2) {
14772
+ repaired.set(e.id, repairOne(r, s, t, e));
14773
+ continue;
14774
+ }
14775
+ counts.faulty++;
14776
+ const ps = placePlug(s, e.source, center(t.box));
14777
+ const pt = placePlug(t, e.target, center(s.box));
14778
+ let way = waysOf().between(ps, pt, /* @__PURE__ */ new Set([s, t]));
14779
+ if (!clear(way, s, t)) way = doWalk ? walkerOf().path(ps.point, s, pt.point, t) : void 0;
14780
+ if (!way || !clear(way, s, t)) {
14781
+ counts.kept++;
14782
+ continue;
14783
+ }
14784
+ counts.added++;
14785
+ changed.add(e.id);
14786
+ repaired.set(e.id, {
14787
+ edge: e.id,
14788
+ points: way
14789
+ });
14790
+ }
14791
+ const out = [];
14792
+ const placed = /* @__PURE__ */ new Set();
14793
+ for (const r of result.routes) {
14794
+ const first = !placed.has(r.edge);
14795
+ placed.add(r.edge);
14796
+ out.push(first ? repaired.get(r.edge) ?? r : r);
14797
+ }
14798
+ for (const e of graph.edges) if (!placed.has(e.id) && repaired.has(e.id)) out.push(repaired.get(e.id));
14799
+ const final = /* @__PURE__ */ new Map();
14800
+ for (const r of out) if (!final.has(r.edge)) final.set(r.edge, r);
14801
+ let runs;
14802
+ if (result.runs) {
14803
+ runs = [];
14804
+ for (const run of result.runs) {
14805
+ const edges = run.edges.filter((id) => {
14806
+ if (!changed.has(id)) return true;
14807
+ const r = final.get(id);
14808
+ const ok = r !== void 0 && (holds(r.points, run.points) || holds(r.points, [...run.points].reverse()));
14809
+ if (!ok) counts.unbundled++;
14810
+ return ok;
14811
+ });
14812
+ if (edges.length === run.edges.length) runs.push(run);
14813
+ else if (edges.length >= 2) runs.push({
14814
+ ...run,
14815
+ edges
14816
+ });
14817
+ else counts.unbundled += edges.length;
14818
+ }
14819
+ }
14820
+ let bundles;
14821
+ if (result.bundles) {
14822
+ bundles = [];
14823
+ for (const b of result.bundles) {
14824
+ const edges = b.edges.filter((id) => {
14825
+ if (!changed.has(id)) return true;
14826
+ const r = final.get(id);
14827
+ const ok = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((q, i) => dist(q, r.points[i]) <= .5);
14828
+ if (!ok) counts.unbundled++;
14829
+ return ok;
14830
+ });
14831
+ if (edges.length === b.edges.length) bundles.push(b);
14832
+ else if (edges.length > 0) bundles.push({
14833
+ ...b,
14834
+ edges
14835
+ });
14836
+ }
14837
+ }
14838
+ let levels;
14839
+ if (result.levels) levels = result.levels.map((level) => {
14840
+ const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
14841
+ let touched = false;
14842
+ const links = level.links.map((l) => {
14843
+ const sb = boxOf.get(l.source);
14844
+ const tb = boxOf.get(l.target);
14845
+ const p = l.points;
14846
+ if (!sb || !tb || p.length < 2) return l;
14847
+ const a = onBoundary(p[0], sb) ? void 0 : nearestOnBoundary(sb, p[0]);
14848
+ const b = onBoundary(p[p.length - 1], tb) ? void 0 : nearestOnBoundary(tb, p[p.length - 1]);
14849
+ if (!a && !b) return l;
14850
+ touched = true;
14851
+ counts.linkEnds += (a ? 1 : 0) + (b ? 1 : 0);
14852
+ const points = [...p];
14853
+ if (a) points[0] = a;
14854
+ if (b) points[points.length - 1] = b;
14855
+ return {
14856
+ ...l,
14857
+ points
14858
+ };
14859
+ });
14860
+ return touched ? {
14861
+ ...level,
14862
+ links
14863
+ } : level;
14864
+ });
14865
+ return {
14866
+ result: {
14867
+ ...result,
14868
+ routes: out,
14869
+ ...runs ? { runs } : {},
14870
+ ...bundles ? { bundles } : {},
14871
+ ...levels ? { levels } : {}
14872
+ },
14873
+ counts
14874
+ };
14875
+ }
14876
+ /** This function gives a router that repairs the routes of `router` with `repairResult`. It keeps the name and the `maxNodes` of the router. */
14877
+ function withRepair(router, options = {}) {
14878
+ return {
14879
+ ...router,
14880
+ route: (graph) => repairResult(graph, router.route(graph), options).result
14881
+ };
14882
+ }
14883
+ /**
14884
+ * This function gives a router strategy that runs `strategy` and then the repair step (RULE-046) with `repair`.
14885
+ * The new strategy has the name `<name>+repair`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
14886
+ */
14887
+ function repairStrategy(strategy, repair = {}) {
14888
+ return defineStrategy({
14889
+ step: "router",
14890
+ name: `${strategy.name}+repair`,
14891
+ describe: `${strategy.describe} Then one repair step fixes the breaks of the hard rules.`,
14892
+ defaults: { ...strategy.defaults },
14893
+ ...strategy.schema === void 0 ? {} : { schema: strategy.schema },
14894
+ ...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
14895
+ run: (graph, options) => repairResult(graph, strategy.run(graph, options), repair).result
14896
+ });
14897
+ }
13038
14898
  // src/bench/screen/screen.ts
13039
14899
  /** The steps of the screen, in the order of the pipeline. */
13040
14900
  const SCREEN_STEPS = [
@@ -13049,6 +14909,8 @@ const HIGHWAY_BASE_ROUTERS = [
13049
14909
  "shared",
13050
14910
  "trunks"
13051
14911
  ];
14912
+ /** The wrappers of `ScreenWrap`. */
14913
+ const SCREEN_WRAPS = ["stable", "repair"];
13052
14914
  /** The hard rules of the routes of a router, or of a plan after `follow`. */
13053
14915
  const ROUTE_RULES = [
13054
14916
  "complete",
@@ -13117,8 +14979,11 @@ function screenRegistry(step, strategies, baseline) {
13117
14979
  }
13118
14980
  /** This function tells if a step of the screen gives routes. The step `router` gives routes, and the step `highway` gives routes after `follow`. */
13119
14981
  const givesRoutes = (step) => step === "router" || step === "highway";
13120
- /** This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router. */
13121
- function rowValues(step, graph, row) {
14982
+ /**
14983
+ * This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
14984
+ * A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
14985
+ */
14986
+ function rowValues(step, graph, row, base) {
13122
14987
  const values = { ...row.values };
13123
14988
  delete values["ms"];
13124
14989
  if (givesRoutes(step) && row.result) {
@@ -13128,6 +14993,11 @@ function rowValues(step, graph, row) {
13128
14993
  values["R8.tight"] = q.tight;
13129
14994
  values["R10.plugAngle"] = q.plugAngle;
13130
14995
  values["R15.inflections"] = q.inflections;
14996
+ values["B2.share"] = bundledShare(row.result);
14997
+ const st = bundleStretch(row.result.routes, base());
14998
+ values["B3.median"] = st.median;
14999
+ values["B3.q95"] = st.q95;
15000
+ values["B6.max"] = mergeAngle(row.result.routes);
13131
15001
  }
13132
15002
  return values;
13133
15003
  }
@@ -13165,6 +15035,21 @@ function rowRules(step, graph, row, again) {
13165
15035
  deterministic
13166
15036
  };
13167
15037
  }
15038
+ /** This function adds the router `<name>+<w>` for each router of the registry and each wrapper `w`. It gives the new registry and the new names. */
15039
+ function wrapRouters(registry, wraps) {
15040
+ const routers = listStrategies(registry, "router");
15041
+ const added = [];
15042
+ for (const w of wraps) for (const s of routers) added.push(w === "stable" ? stableOrderStrategy(s) : repairStrategy(s));
15043
+ return {
15044
+ registry: added.length === 0 ? registry : register(registry, ...added),
15045
+ added: added.map((s) => s.name)
15046
+ };
15047
+ }
15048
+ /** This function gives the name of a router without the suffix of a wrapper, for example `shared` for `shared+repair`. */
15049
+ function unwrappedName(name) {
15050
+ for (const w of SCREEN_WRAPS) if (name.endsWith(`+${w}`)) return name.slice(0, -(w.length + 1));
15051
+ return name;
15052
+ }
13168
15053
  /** This function keeps the finite values and gives them the names of a record. */
13169
15054
  function named(values) {
13170
15055
  const out = {};
@@ -13187,11 +15072,22 @@ function screen(step, strategies = [], options = {}) {
13187
15072
  const screened = screenRegistry(step, strategies, options.baseline ?? true);
13188
15073
  const candidates = screened.candidates;
13189
15074
  const base = step === "highway" ? (options.baseRouters ?? HIGHWAY_BASE_ROUTERS).map((name) => getStrategy(STRATEGIES, "router", name)) : [];
13190
- const registry = step === "highway" ? createRegistry([...base, ...listStrategies(screened.registry, "highway").map((s) => highwayRouterStrategy(s))]) : screened.registry;
15075
+ const unwrapped = step === "highway" ? createRegistry([...base, ...listStrategies(screened.registry, "highway").map((s) => highwayRouterStrategy(s))]) : screened.registry;
15076
+ const wrapped = givesRoutes(step) ? wrapRouters(unwrapped, options.wrap ?? []) : {
15077
+ registry: unwrapped,
15078
+ added: []
15079
+ };
15080
+ const registry = wrapped.registry;
15081
+ for (const name of wrapped.added) candidates.add(name);
13191
15082
  const compareAs = step === "highway" ? "router" : step;
13192
15083
  const baseNames = new Set(base.map((s) => s.name));
13193
- const stepOf = (name) => baseNames.has(name) ? "router" : step;
13194
- const own = options.options ?? {};
15084
+ const stepOf = (name) => baseNames.has(unwrappedName(name)) ? "router" : step;
15085
+ const own = { ...options.options };
15086
+ for (const name of wrapped.added) {
15087
+ const from = options.options?.[unwrappedName(name)];
15088
+ if (own[name] === void 0 && from !== void 0) own[name] = from;
15089
+ }
15090
+ const shuffles = givesRoutes(step) ? Math.max(0, Math.floor(options.shuffles ?? 0)) : 0;
13195
15091
  const seed = options.seed ?? 1;
13196
15092
  const log = options.log ?? (() => {});
13197
15093
  const families = options.families;
@@ -13219,6 +15115,8 @@ function screen(step, strategies = [], options = {}) {
13219
15115
  graph,
13220
15116
  results: new Map(cmp.rows.flatMap((r) => r.result ? [[r.strategy, r.result]] : []))
13221
15117
  });
15118
+ let cornersRoutes;
15119
+ const cornersBase = () => cornersRoutes ??= (cmp.rows.find((r) => r.strategy === "corners" && r.result)?.result ?? asRouter(getStrategy(STRATEGIES, "router", "corners")).route(graph)).routes;
13222
15120
  cmp.rows.forEach((row, i) => {
13223
15121
  const strategy = byName.get(row.strategy);
13224
15122
  const head = {
@@ -13257,9 +15155,26 @@ function screen(step, strategies = [], options = {}) {
13257
15155
  return;
13258
15156
  }
13259
15157
  const { hard, deterministic } = rowRules(step, graph, row, again?.rows[i]);
15158
+ const values = rowValues(step, graph, row, cornersBase);
15159
+ if (shuffles > 0 && row.result) {
15160
+ const router = asRouter(strategy, own[row.strategy]);
15161
+ const f = orderFragility(graph, (g) => router.route(g), {
15162
+ orders: shuffles,
15163
+ seed,
15164
+ base: row.result
15165
+ });
15166
+ Object.assign(values, {
15167
+ "W3.crossings": f.crossings,
15168
+ "W3.detour": f.detour,
15169
+ "W3.inkSaved": f.inkSaved,
15170
+ "W3.ink": f.ink,
15171
+ "W3.changed": f.changed,
15172
+ "W3.poa": f.poa
15173
+ });
15174
+ }
13260
15175
  const record = {
13261
15176
  ...head,
13262
- measures: named(rowValues(step, graph, row)),
15177
+ measures: named(values),
13263
15178
  hard,
13264
15179
  tolerance: { through: TOLERANCE },
13265
15180
  deterministic,
@@ -13449,6 +15364,6 @@ function rectScattered(seed, params, set = "xcheck-v1") {
13449
15364
  hash: documentHash(body)
13450
15365
  };
13451
15366
  }
13452
- export { BACKTRACK_MIN, BACKTRACK_SHARE, BREAK, BREAK_EVEN, BUILT_IN_STRATEGIES, CAP_SWEEP, CASES, CLEARANCE_CAP, COMPARE_STEPS, CORRIDOR_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, FIXTURES, FRONT_OBJECTIVES, GROWTH_SWEEP, Grid, HIGHWAY_BASE_ROUTERS, KD_DEFAULTS, KENDALL_EXACT, LANE_ORDERS, MEASURES, OUTSIDE_SLACK, Obstacles, PICK, PROPOSED_RULES, ROOM_FAMILIES, ROUTERS, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SECTION_DEFAULTS, SIDE_NORMAL, SLACK_SWEEP, SPACING_LIMIT, STEPS, STRATEGIES, STRETCH_SWEEP, SUITE_COLUMNS, SoftGraph, TOLERANCE, WayFinder, adjustedRandIndex, allSections, ambiguity, arcCurve, asRouter, backtrackOf, balance, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, bendCount, bendingEnergy, boardGaps, boardUnit, boxExit, boxesOverlap, buildSections, byCollection, byDataGroup, canonicalJson, capSettings, center, changeGraph, checkLevels, checkOptions, checkPlan, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, compareStep, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createRegistry, crossesItself, crossingAngle, crossingAngles, cubicAt, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, cutGroups, cutShare, defineStrategy, denseGroupsBoard, detourSpread, dist, distinctPoints, documentHash, dominates, drawnSegments, familySeed, farLevel, findStrategy, fixedPortPoint, fixtureGroupOptions, fnv1a32, follow, formatBendRoom, formatCurves, formatFaults, formatFronts, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, frechet, g1Defect, g2Defect, gapLanes, getStrategy, goesOutAndBack, graphInput, groupEdges, groupRun, groupSettings, groupStability, groupStrategies, growthSettings, hasCusp, highwayCurve, highwayCurves, highwayRouter, highwayRouterStrategy, highwayStrategies, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, inputGraph, inputId, itemSizes, jogPeaks, joinReports, jsonSafe, kdGroups, kendallTau, keyGroups, keyedSections, keysPlan, laneOrder, laneRoutes, laneSettings, leafGroups, lengthBetween, levelCut, levelGrouping, levelMeasures, levelPairs, levelRoom, listOptions, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, memberAt, minHash, modularity, mulberry32, namedObjectives, nearestSide, nestedGridBoard, nestingFaults, nodeIndex, offsetPolyline, openGaps, optionDefaults, orderChanges, orderLanes, orderRows, orderStability, orderSuiteRows, packedGridBoard, paretoFront, parseLines, passSettings, pathPoints, peakCurvature, pickLevel, placePlug, planOfResult, plugSpacing, plugsRouter, pointAt, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, positionStability, pullApart, quadtreeGroups, quantile, randomInt, recordLines, rectScattered, regionBounds, register, rng, roomDemand, roomLinks, roomOrderFaults, roomStrategies, round3, roundCorners, roundedCurve, routeMeasures, routeStability, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runSuite, sameData, sampleCurve, screen, screenInputs, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, sha256Hex, sharedRouter, sharedWaysPlan, sideAngle, sideToward, slackSettings, smoothCurve, splineCurve, splitPoint, stepSeed, straightCurve, straightRouter, stretchSettings, suiteFronts, sumFaults, summarize, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, toSvgPath, totalTurn, treeCost, treeDepth, trunksRouter, turns, utf8, withBends };
15367
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, BREAK, BREAK_EVEN, BUILT_IN_STRATEGIES, BoundaryWalk, CAP_SWEEP, CASES, CLEARANCE_CAP, COMPARE_STEPS, CORRIDOR_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, FIXTURES, FRONT_OBJECTIVES, GROWTH_SWEEP, Grid, HIGHWAY_BASE_ROUTERS, KD_DEFAULTS, KENDALL_EXACT, LANE_ORDERS, LANE_TRACKS, MEASURES, OUTSIDE_SLACK, Obstacles, PICK, PROPOSED_RULES, ROOM_FAMILIES, ROUTERS, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SCREEN_WRAPS, SECTION_DEFAULTS, SIDE_NORMAL, SLACK_SWEEP, SPACING_LIMIT, STEPS, STRATEGIES, STRETCH_SWEEP, SUITE_COLUMNS, SoftGraph, TOLERANCE, WayFinder, adjustedRandIndex, allSections, ambiguity, arcCurve, asRouter, backtrackOf, balance, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, benchTracks, bendCount, bendingEnergy, boardGaps, boardUnit, boxExit, boxesOverlap, buildSections, bundleStretch, bundledShare, byCollection, byDataGroup, canonicalJson, capSettings, center, changeGraph, checkLevels, checkOptions, checkPlan, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, compareStep, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createRegistry, crossesItself, crossingAngle, crossingAngles, cubicAt, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, cutGroups, cutShare, defineStrategy, denseGroupsBoard, detourSpread, dist, distinctPoints, documentHash, dominates, drawnSegments, familySeed, farLevel, findStrategy, fixedPortPoint, fixtureGroupOptions, fnv1a32, follow, formatBendRoom, formatCurves, formatFaults, formatFronts, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, formatTracks, frechet, g1Defect, g2Defect, gapLaneCounts, gapLanes, getStrategy, goesOutAndBack, graphInput, groupEdges, groupRun, groupSettings, groupStability, groupStrategies, growthSettings, hasCusp, highwayCurve, highwayCurves, highwayRouter, highwayRouterStrategy, highwayStrategies, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, inputGraph, inputId, itemSizes, jogPeaks, joinReports, jsonSafe, kdGroups, kendallTau, keyGroups, keyedSections, keysPlan, laneOrder, laneRoutes, laneSeparation, laneSettings, leafGroups, lengthBetween, levelCut, levelGrouping, levelMeasures, levelPairs, levelRoom, listOptions, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, memberAt, mergeAngle, minHash, modularity, mulberry32, namedObjectives, nearestOnBoundary, nearestSide, nestedGridBoard, nestingFaults, nodeIndex, offsetPolyline, openGaps, optionDefaults, orderChanges, orderFragility, orderLanes, orderRows, orderStability, orderSuiteRows, packedGridBoard, paretoFront, parseLines, passSettings, pathPoints, peakCurvature, pickLevel, placePlug, planOfResult, plugSpacing, plugsRouter, pointAt, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, positionStability, pullApart, quadtreeGroups, quantile, randomInt, recordLines, rectScattered, regionBounds, register, repairResult, repairStrategy, rng, roomDemand, roomLinks, roomOrderFaults, roomStrategies, round3, roundCorners, roundedCurve, routeInStableOrder, routeMeasures, routeStability, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runSuite, sameData, sampleCurve, screen, screenInputs, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, sha256Hex, sharedRouter, sharedWaysPlan, shuffleEdges, sideAngle, sideToward, slackSettings, smoothCurve, splineCurve, splitPoint, stableOrder, stableOrderStrategy, stepSeed, straightCurve, straightRouter, stretchSettings, suiteFronts, sumFaults, summarize, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, toSvgPath, totalTurn, treeCost, treeDepth, trunksRouter, turns, unwrappedName, utf8, withBends, withRepair, withStableOrder };
13453
15368
 
13454
15369
  //# sourceMappingURL=index.js.map