@mark1russell7/highways 0.1.2 → 0.1.3

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
@@ -228,6 +228,53 @@ var Grid = class {
228
228
  }
229
229
  }
230
230
  }
231
+ /**
232
+ * This method gives each item in the buckets along the segment `ab`, widened by `margin`, to `visit`, one time each.
233
+ * The buckets are those of `#segment`, in the same order, so the items come in the same order.
234
+ * A bucket in the range of the step before has no new item, so the walk skips it.
235
+ * The walk stops when `visit` gives true, and then the method gives true.
236
+ */
237
+ #walk(a, b, margin, visit) {
238
+ const stamp = ++this.#stamp;
239
+ const c = this.#cell;
240
+ const items = this.#items;
241
+ const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (c / 2)));
242
+ let px = a.x;
243
+ let py = a.y;
244
+ let pi0 = 1;
245
+ let pi1 = 0;
246
+ let pj0 = 1;
247
+ let pj1 = 0;
248
+ for (let k = 1; k <= n; k++) {
249
+ const qx = k === n ? b.x : a.x + (b.x - a.x) * k / n;
250
+ const qy = k === n ? b.y : a.y + (b.y - a.y) * k / n;
251
+ const i0 = Math.floor((Math.min(px, qx) - margin) / c);
252
+ const j0 = Math.floor((Math.min(py, qy) - margin) / c);
253
+ const i1 = Math.floor((Math.max(px, qx) + margin) / c);
254
+ const j1 = Math.floor((Math.max(py, qy) + margin) / c);
255
+ const seen = this.#seen;
256
+ for (let i = i0; i <= i1; i++) {
257
+ const before = i >= pi0 && i <= pi1;
258
+ for (let j = j0; j <= j1; j++) {
259
+ if (before && j >= pj0 && j <= pj1) continue;
260
+ const list = this.#buckets.get(this.#key(i, j));
261
+ if (list === void 0) continue;
262
+ for (const id of list) {
263
+ if (seen[id] === stamp) continue;
264
+ seen[id] = stamp;
265
+ if (visit(items[id]) === true) return true;
266
+ }
267
+ }
268
+ }
269
+ pi0 = i0;
270
+ pi1 = i1;
271
+ pj0 = j0;
272
+ pj1 = j1;
273
+ px = qx;
274
+ py = qy;
275
+ }
276
+ return false;
277
+ }
231
278
  #segment(a, b, margin, each) {
232
279
  const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
233
280
  let px = a.x;
@@ -255,8 +302,16 @@ var Grid = class {
255
302
  }
256
303
  /** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
257
304
  querySegment(a, b, margin, visit) {
258
- const stamp = ++this.#stamp;
259
- this.#segment(a, b, margin, (x0, y0, x1, y1) => this.#take(stamp, x0, y0, x1, y1, visit));
305
+ this.#walk(a, b, margin, (item) => {
306
+ visit(item);
307
+ });
308
+ }
309
+ /**
310
+ * This method tells if `test` gives true for an item whose buckets come within `margin` of the segment `ab`.
311
+ * It gives the items in the order of `querySegment`, and it stops at the first item that passes the test.
312
+ */
313
+ someSegment(a, b, margin, test) {
314
+ return this.#walk(a, b, margin, test);
260
315
  }
261
316
  };
262
317
  /** This function gives the bounding box of the segment `ab`. */
@@ -546,6 +601,125 @@ function makeEdges(spec, nodes, clusterOf, R) {
546
601
  }
547
602
  return edges;
548
603
  }
604
+ // src/cases.ts
605
+ /** A board while it grows: the functions add nodes and edges in order. */
606
+ var Board = class {
607
+ nodes = [];
608
+ edges = [];
609
+ #pairs = /* @__PURE__ */ new Set();
610
+ node(id, x, y, w, h) {
611
+ this.nodes.push({
612
+ id,
613
+ box: {
614
+ x,
615
+ y,
616
+ w,
617
+ h
618
+ }
619
+ });
620
+ return id;
621
+ }
622
+ /** This method adds an edge. It adds nothing for a loop or for a second edge between the same two nodes. */
623
+ link(source, target) {
624
+ const key = source < target ? `${source}|${target}` : `${target}|${source}`;
625
+ if (source === target || this.#pairs.has(key)) return;
626
+ this.#pairs.add(key);
627
+ this.edges.push({
628
+ id: `e${this.edges.length}`,
629
+ source: { node: source },
630
+ target: { node: target }
631
+ });
632
+ }
633
+ graph() {
634
+ return {
635
+ nodes: this.nodes,
636
+ edges: this.edges
637
+ };
638
+ }
639
+ };
640
+ /**
641
+ * This function makes a dense board: 150 nodes in tight clusters. At the far view, the boxes of the groups overlap.
642
+ * Then a level link starts or ends in a group that another group covers.
643
+ */
644
+ function denseGroupsBoard() {
645
+ return makeFixture({
646
+ name: "dense-groups",
647
+ kind: "clustered",
648
+ nodes: 150,
649
+ edgesPerNode: 2,
650
+ seed: 3
651
+ });
652
+ }
653
+ /**
654
+ * This function makes a board of three rows of 32 cards. Each card links to the next card of its row.
655
+ * Forty long links join cards far apart. The hub of a section is near the middle of its cards.
656
+ * So a long link climbs to hubs behind its source or past its target, and comes back.
657
+ */
658
+ function hubChainBoard() {
659
+ const R = rng(3);
660
+ const b = new Board();
661
+ const rows = 3;
662
+ const cols = 32;
663
+ const id = (r, c) => `c${r}-${c}`;
664
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * 128, r * 84, 80, 36);
665
+ for (let r = 0; r < rows; r++) for (let c = 0; c + 1 < cols; c++) b.link(id(r, c), id(r, c + 1));
666
+ for (let n = 0; n < 40; n++) {
667
+ const r = Math.floor(R() * rows);
668
+ const c = Math.floor(R() * cols);
669
+ const span = Math.floor(cols / 8 + R() * cols / 2);
670
+ if (c + span < cols) b.link(id(r, c), id((r + 1) % rows, c + span));
671
+ }
672
+ return b.graph();
673
+ }
674
+ /**
675
+ * This function makes a board of 32 groups in 8 columns and 4 rows. Each group has 4 cards in a square.
676
+ * The cards of a group link in a chain, and 96 random links join cards of two groups.
677
+ * On a level with many groups, some level links go around the outer edge of the board.
678
+ */
679
+ function manyGroupsBoard() {
680
+ const R = rng(7);
681
+ const b = new Board();
682
+ const cols = 8;
683
+ const rows = 4;
684
+ const per = 4;
685
+ const w = 60;
686
+ const h = 30;
687
+ const cardGap = 12;
688
+ const side = Math.ceil(Math.sqrt(per));
689
+ Math.ceil(per / side) * h + (Math.ceil(per / side) - 1) * cardGap;
690
+ const groups = [];
691
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
692
+ const list = [];
693
+ for (let i = 0; i < per; i++) {
694
+ const x = c * 192 + i % side * 72;
695
+ const y = r * 132 + Math.floor(i / side) * 42;
696
+ list.push(b.node(`g${r}-${c}-${i}`, x, y, w, h));
697
+ }
698
+ groups.push(list);
699
+ }
700
+ for (const list of groups) for (let i = 1; i < list.length; i++) b.link(list[i - 1], list[i]);
701
+ const pick = (list) => list[Math.floor(R() * list.length)];
702
+ for (let n = 0; n < groups.length * 3; n++) b.link(pick(groups[Math.floor(R() * groups.length)]), pick(groups[Math.floor(R() * groups.length)]));
703
+ return b.graph();
704
+ }
705
+ /** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
706
+ const CASES = [
707
+ {
708
+ name: "case-dense-groups",
709
+ defect: "Groups overlap at the far view, and level links go through other groups.",
710
+ make: denseGroupsBoard
711
+ },
712
+ {
713
+ name: "case-hub-chain",
714
+ defect: "A route climbs a chain of hubs away from its target and comes back.",
715
+ make: hubChainBoard
716
+ },
717
+ {
718
+ name: "case-many-groups",
719
+ defect: "On a level with many groups, links go around the outer edge of the board.",
720
+ make: manyGroupsBoard
721
+ }
722
+ ];
549
723
  // src/plugs.ts
550
724
  /** The outward normal of each side, in a frame with the y axis down. */
551
725
  const SIDE_NORMAL = {
@@ -725,20 +899,30 @@ var Obstacles = class {
725
899
  }
726
900
  /** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
727
901
  clear(a, b, pad, skip) {
728
- let ok = true;
729
- this.#grid.querySegment(a, b, pad, (n) => {
730
- if (ok && !skip?.has(n)) {
731
- const x = n.box;
732
- if (segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad)) ok = false;
733
- }
902
+ return this.hit(a, b, pad, skip) === void 0;
903
+ }
904
+ /**
905
+ * This method gives a node whose box, made larger by `pad`, the segment `ab` goes into, or `undefined` when the segment is clear.
906
+ * The nodes in `skip` do not count. The test stops at the first such node.
907
+ */
908
+ hit(a, b, pad, skip) {
909
+ let found;
910
+ this.#grid.someSegment(a, b, pad, (n) => {
911
+ if (skip?.has(n) || !hits(a, b, n, pad)) return false;
912
+ found = n;
913
+ return true;
734
914
  });
735
- return ok;
915
+ return found;
736
916
  }
737
917
  /** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
738
918
  clearPath(p, pad, skip) {
739
919
  for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
740
920
  return true;
741
921
  }
922
+ /** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
923
+ query(box, visit) {
924
+ this.#grid.query(box, visit);
925
+ }
742
926
  /** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
743
927
  inside(p, pad, skip) {
744
928
  let hit = false;
@@ -756,6 +940,11 @@ var Obstacles = class {
756
940
  return hit;
757
941
  }
758
942
  };
943
+ /** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
944
+ function hits(a, b, n, pad) {
945
+ const x = n.box;
946
+ return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
947
+ }
759
948
  /**
760
949
  * A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
761
950
  * Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
@@ -776,9 +965,12 @@ var CornerGraph = class {
776
965
  #open = /* @__PURE__ */ new Int32Array(0);
777
966
  #closed = /* @__PURE__ */ new Int32Array(0);
778
967
  #stamp = 0;
779
- #tested = /* @__PURE__ */ new Int32Array(0);
780
- #testStamp = 0;
968
+ #buckets = /* @__PURE__ */ new Map();
969
+ #dist = /* @__PURE__ */ new Float64Array(0);
970
+ #near = new MinHeap();
781
971
  #endCache = /* @__PURE__ */ new Map();
972
+ #fewCache = /* @__PURE__ */ new Map();
973
+ #part;
782
974
  /** This constructor makes the graph of the nodes. */
783
975
  constructor(nodes, obstacles, options) {
784
976
  this.#obstacles = obstacles;
@@ -841,12 +1033,17 @@ var CornerGraph = class {
841
1033
  });
842
1034
  }
843
1035
  #addPoint(c) {
844
- this.#pointGrid.add(this.points.length, {
1036
+ const j = this.points.length;
1037
+ this.#pointGrid.add(j, {
845
1038
  x: c.x,
846
1039
  y: c.y,
847
1040
  w: 0,
848
1041
  h: 0
849
1042
  });
1043
+ const key = bucketKey(Math.floor(c.x / this.#cell), Math.floor(c.y / this.#cell));
1044
+ const list = this.#buckets.get(key);
1045
+ if (list === void 0) this.#buckets.set(key, [j]);
1046
+ else list.push(j);
850
1047
  this.points.push(c);
851
1048
  }
852
1049
  /** This method gives the number of links of the graph. */
@@ -880,28 +1077,70 @@ var CornerGraph = class {
880
1077
  nearestVisible(p, k, self = -1) {
881
1078
  return this.#nearestVisible(p, k, self, this.#pad);
882
1079
  }
1080
+ /**
1081
+ * This method tests the corners in rings of radius `r` = 1, 2, 4 and 8 cells. In each ring, it tests the corners
1082
+ * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
1083
+ * The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
1084
+ * A corner of these buckets beyond `r` waits for the next ring. So the method visits each bucket one time.
1085
+ */
883
1086
  #nearestVisible(p, k, self, pad) {
884
1087
  const out = [];
885
- if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
886
- const tested = this.#tested;
887
- const stamp = ++this.#testStamp;
888
- for (let r = this.#cell; r <= this.#cell * 8; r *= 2) {
889
- const cands = [];
890
- this.#pointGrid.query({
891
- x: p.x - r,
892
- y: p.y - r,
893
- w: 2 * r,
894
- h: 2 * r
895
- }, (j) => {
896
- if (j !== self && tested[j] !== stamp) cands.push([dist(p, this.points[j]), j]);
897
- });
898
- cands.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
899
- for (const [d, j] of cands) {
900
- if (d > r) break;
901
- tested[j] = stamp;
902
- if (this.#obstacles.clear(p, this.points[j], pad)) out.push(j);
903
- if (out.length >= k) return out;
1088
+ if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
1089
+ const dd = this.#dist;
1090
+ const points = this.points;
1091
+ const cell = this.#cell;
1092
+ const blockers = [];
1093
+ const now = this.#near;
1094
+ let wait = [];
1095
+ let pi0 = 1;
1096
+ let pi1 = 0;
1097
+ let pj0 = 1;
1098
+ let pj1 = 0;
1099
+ for (let r = cell; r <= cell * 8; r *= 2) {
1100
+ const x0 = p.x - r;
1101
+ const y0 = p.y - r;
1102
+ const i0 = Math.floor(x0 / cell);
1103
+ const j0 = Math.floor(y0 / cell);
1104
+ const i1 = Math.floor((x0 + 2 * r) / cell);
1105
+ const j1 = Math.floor((y0 + 2 * r) / cell);
1106
+ const later = [];
1107
+ now.clear();
1108
+ for (const j of wait) if (dd[j] > r) later.push(j);
1109
+ else now.add(dd[j], j);
1110
+ for (let bi = i0; bi <= i1; bi++) {
1111
+ const before = bi >= pi0 && bi <= pi1;
1112
+ for (let bj = j0; bj <= j1; bj++) {
1113
+ if (before && bj >= pj0 && bj <= pj1) continue;
1114
+ const list = this.#buckets.get(bucketKey(bi, bj));
1115
+ if (list === void 0) continue;
1116
+ for (const j of list) {
1117
+ if (j === self) continue;
1118
+ const d = dist(p, points[j]);
1119
+ dd[j] = d;
1120
+ if (d > r) later.push(j);
1121
+ else now.add(d, j);
1122
+ }
1123
+ }
1124
+ }
1125
+ now.build();
1126
+ while (now.size > 0) {
1127
+ const j = now.pop();
1128
+ const q = points[j];
1129
+ let blocked = false;
1130
+ for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
1131
+ if (blocked) continue;
1132
+ const hit = this.#obstacles.hit(p, q, pad);
1133
+ if (hit !== void 0) blockers.push(hit);
1134
+ else {
1135
+ out.push(j);
1136
+ if (out.length >= k) return out;
1137
+ }
904
1138
  }
1139
+ wait = later;
1140
+ pi0 = i0;
1141
+ pi1 = i1;
1142
+ pj0 = j0;
1143
+ pj1 = j1;
905
1144
  }
906
1145
  return out;
907
1146
  }
@@ -913,6 +1152,35 @@ var CornerGraph = class {
913
1152
  */
914
1153
  path(from, to, cost, floor = 1) {
915
1154
  if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
1155
+ return this.#search([{
1156
+ point: from,
1157
+ cost: 0
1158
+ }], [{
1159
+ point: to,
1160
+ cost: 0
1161
+ }], cost, floor, this.#k * 2)?.points;
1162
+ }
1163
+ /**
1164
+ * This method gives the shortest path from a point of `from` to a point of `to`, or `undefined` when the graph has none.
1165
+ * The cost of a path includes the costs of its two points. One search finds the path, so this method is faster than a call of `path` for each pair.
1166
+ */
1167
+ pathAmong(from, to) {
1168
+ const found = this.#search(from, to, void 0, 1, this.#k);
1169
+ const lines = [];
1170
+ from.forEach((a, i) => to.forEach((b, k) => {
1171
+ const c = a.cost + dist(a.point, b.point) + b.cost;
1172
+ if (!found || c < found.cost) lines.push({
1173
+ points: [a.point, b.point],
1174
+ from: i,
1175
+ to: k,
1176
+ cost: c
1177
+ });
1178
+ }));
1179
+ lines.sort((u, v) => u.cost - v.cost);
1180
+ return lines.find((l) => this.#obstacles.clear(l.points[0], l.points[1], this.#pad)) ?? found;
1181
+ }
1182
+ /** This method is the search (A*) of `path` and `pathAmong`. */
1183
+ #search(from, to, cost, floor, count) {
916
1184
  const n = this.points.length;
917
1185
  if (this.#g.length < n) {
918
1186
  this.#g = new Float64Array(n);
@@ -921,28 +1189,51 @@ var CornerGraph = class {
921
1189
  this.#closed = new Int32Array(n);
922
1190
  }
923
1191
  const stamp = ++this.#stamp;
1192
+ const points = this.points;
924
1193
  const goal = /* @__PURE__ */ new Map();
925
- for (const j of this.#ends(to)) goal.set(j, dist(this.points[j], to));
1194
+ const goalOf = /* @__PURE__ */ new Map();
1195
+ to.forEach((b, k) => {
1196
+ for (const j of this.#ends(b.point, count)) {
1197
+ const tail = dist(points[j], b.point) + b.cost;
1198
+ const now = goal.get(j);
1199
+ if (now === void 0 || tail < now) {
1200
+ goal.set(j, tail);
1201
+ goalOf.set(j, k);
1202
+ }
1203
+ }
1204
+ });
926
1205
  if (goal.size === 0) return void 0;
1206
+ const starts = from.map((a) => this.#ends(a.point, count));
1207
+ if (!starts.some((list) => this.#joined(list, goal))) return void 0;
927
1208
  const heap = new MinHeap();
928
- const h = (i) => floor * dist(this.points[i], to);
1209
+ const G = this.#g;
1210
+ const parentOf = this.#parent;
1211
+ const open = this.#open;
1212
+ const closed = this.#closed;
1213
+ const estimate = (p) => {
1214
+ let h = Infinity;
1215
+ for (const b of to) h = Math.min(h, dist(p, b.point));
1216
+ return floor * h;
1217
+ };
929
1218
  const relax = (j, g, parent) => {
930
- if (this.#closed[j] === stamp) return;
931
- if (this.#open[j] === stamp && this.#g[j] <= g) return;
932
- this.#open[j] = stamp;
933
- this.#g[j] = g;
934
- this.#parent[j] = parent;
935
- heap.push(g + h(j), j);
1219
+ if (closed[j] === stamp) return;
1220
+ if (open[j] === stamp && G[j] <= g) return;
1221
+ open[j] = stamp;
1222
+ G[j] = g;
1223
+ parentOf[j] = parent;
1224
+ heap.push(g + estimate(points[j]), j);
936
1225
  };
937
- for (const j of this.#ends(from)) relax(j, dist(from, this.points[j]), -1);
1226
+ from.forEach((a, i) => {
1227
+ for (const j of starts[i]) relax(j, a.cost + dist(a.point, points[j]), -1 - i);
1228
+ });
938
1229
  let best = Infinity;
939
1230
  let bestEnd = -1;
940
1231
  while (heap.size > 0) {
941
1232
  if (heap.topKey >= best) break;
942
1233
  const i = heap.pop();
943
- if (this.#closed[i] === stamp) continue;
944
- this.#closed[i] = stamp;
945
- const gi = this.#g[i];
1234
+ if (closed[i] === stamp) continue;
1235
+ closed[i] = stamp;
1236
+ const gi = G[i];
946
1237
  const tail = goal.get(i);
947
1238
  if (tail !== void 0 && gi + tail < best) {
948
1239
  best = gi + tail;
@@ -952,28 +1243,67 @@ var CornerGraph = class {
952
1243
  const len = this.#len[i];
953
1244
  for (let m = 0; m < adj.length; m++) {
954
1245
  const j = adj[m];
1246
+ if (closed[j] === stamp) continue;
955
1247
  const d = len[m];
956
1248
  relax(j, gi + (cost ? cost(i, j, d) : d), i);
957
1249
  }
958
1250
  }
959
1251
  if (bestEnd < 0) return void 0;
960
- const out = [to];
961
- for (let i = bestEnd; i >= 0; i = this.#parent[i]) out.push(this.points[i]);
962
- out.push(from);
963
- return out.reverse();
1252
+ const k = goalOf.get(bestEnd);
1253
+ const out = [to[k].point];
1254
+ for (let i = bestEnd;; i = parentOf[i]) {
1255
+ out.push(points[i]);
1256
+ if (parentOf[i] < 0) {
1257
+ const f = -1 - parentOf[i];
1258
+ out.push(from[f].point);
1259
+ return {
1260
+ points: out.reverse(),
1261
+ from: f,
1262
+ to: k,
1263
+ cost: best
1264
+ };
1265
+ }
1266
+ }
1267
+ }
1268
+ /** This method tells if a corner of `starts` and a corner of `goal` are in one connected part of the graph. */
1269
+ #joined(starts, goal) {
1270
+ const part = this.#part ??= this.#parts();
1271
+ const goals = /* @__PURE__ */ new Set();
1272
+ for (const j of goal.keys()) goals.add(part[j]);
1273
+ for (const j of starts) if (goals.has(part[j])) return true;
1274
+ return false;
1275
+ }
1276
+ /** This method gives the number of the connected part of each corner. */
1277
+ #parts() {
1278
+ const part = new Int32Array(this.points.length).fill(-1);
1279
+ const stack = [];
1280
+ let count = 0;
1281
+ for (let i = 0; i < part.length; i++) {
1282
+ if (part[i] !== -1) continue;
1283
+ part[i] = count;
1284
+ stack.push(i);
1285
+ while (stack.length > 0) for (const j of this.#adj[stack.pop()]) {
1286
+ if (part[j] !== -1) continue;
1287
+ part[j] = count;
1288
+ stack.push(j);
1289
+ }
1290
+ count++;
1291
+ }
1292
+ return part;
964
1293
  }
965
1294
  /**
966
- * This method gives the corners that an end of a path can see. An end near a box sees no corner with the clearance.
1295
+ * This method gives at most `count` corners that an end of a path can see. An end near a box sees no corner with the clearance.
967
1296
  * Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
968
1297
  * The graph does not change, so the method keeps the corners of each end.
969
1298
  */
970
- #ends(p) {
1299
+ #ends(p, count) {
1300
+ const cache = count === this.#k * 2 ? this.#endCache : this.#fewCache;
971
1301
  const key = `${p.x},${p.y}`;
972
- let out = this.#endCache.get(key);
1302
+ let out = cache.get(key);
973
1303
  if (!out) {
974
- out = this.nearestVisible(p, this.#k * 2);
975
- if (out.length === 0) out = this.#nearestVisible(p, this.#k * 2, -1, 0);
976
- this.#endCache.set(key, out);
1304
+ out = this.nearestVisible(p, count);
1305
+ if (out.length === 0) out = this.#nearestVisible(p, count, -1, 0);
1306
+ cache.set(key, out);
977
1307
  }
978
1308
  return out;
979
1309
  }
@@ -991,61 +1321,85 @@ var CornerGraph = class {
991
1321
  return out;
992
1322
  }
993
1323
  };
994
- /** A binary min-heap of (key, value) pairs. Equal keys come out by the smaller value, so the order is deterministic. */
1324
+ /** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
1325
+ const bucketKey = (i, j) => (i + 1048576) * 2097152 + (j + 1048576);
1326
+ /**
1327
+ * A binary min-heap of (key, value) pairs in typed arrays. The pairs come out by key, and equal keys by value, so the order is deterministic.
1328
+ * The method `push` adds a pair to the heap. The method `add` only appends a pair, and the method `build` then makes the heap of all the pairs.
1329
+ */
995
1330
  var MinHeap = class {
996
- #k = [];
997
- #v = [];
998
- get size() {
999
- return this.#k.length;
1000
- }
1001
- #less(a, b) {
1002
- return this.#k[a] < this.#k[b] || this.#k[a] === this.#k[b] && this.#v[a] < this.#v[b];
1003
- }
1004
- #swap(a, b) {
1005
- const k = this.#k[a];
1006
- this.#k[a] = this.#k[b];
1007
- this.#k[b] = k;
1008
- const v = this.#v[a];
1009
- this.#v[a] = this.#v[b];
1010
- this.#v[b] = v;
1011
- }
1331
+ #d = /* @__PURE__ */ new Float64Array(64);
1332
+ #j = /* @__PURE__ */ new Int32Array(64);
1333
+ size = 0;
1012
1334
  /** The smallest key. */
1013
1335
  get topKey() {
1014
- return this.#k[0];
1336
+ return this.#d[0];
1015
1337
  }
1016
1338
  push(key, value) {
1017
- this.#k.push(key);
1018
- this.#v.push(value);
1019
- let i = this.#k.length - 1;
1339
+ this.add(key, value);
1340
+ const d = this.#d;
1341
+ const jj = this.#j;
1342
+ let i = this.size - 1;
1020
1343
  while (i > 0) {
1021
1344
  const p = i - 1 >> 1;
1022
- if (!this.#less(i, p)) break;
1023
- this.#swap(i, p);
1345
+ if (!(key < d[p] || key === d[p] && value < jj[p])) break;
1346
+ d[i] = d[p];
1347
+ jj[i] = jj[p];
1024
1348
  i = p;
1025
1349
  }
1350
+ d[i] = key;
1351
+ jj[i] = value;
1352
+ }
1353
+ clear() {
1354
+ this.size = 0;
1355
+ }
1356
+ add(d, j) {
1357
+ if (this.size === this.#d.length) {
1358
+ const dd = new Float64Array(2 * this.size);
1359
+ dd.set(this.#d);
1360
+ this.#d = dd;
1361
+ const jj = new Int32Array(2 * this.size);
1362
+ jj.set(this.#j);
1363
+ this.#j = jj;
1364
+ }
1365
+ this.#d[this.size] = d;
1366
+ this.#j[this.size] = j;
1367
+ this.size++;
1368
+ }
1369
+ build() {
1370
+ for (let i = (this.size >> 1) - 1; i >= 0; i--) this.#down(i);
1026
1371
  }
1027
1372
  /** This method removes the pair with the smallest key and gives its value. */
1028
1373
  pop() {
1029
- const top = this.#v[0];
1030
- const k = this.#k.pop();
1031
- const v = this.#v.pop();
1032
- if (this.#k.length > 0) {
1033
- this.#k[0] = k;
1034
- this.#v[0] = v;
1035
- let i = 0;
1036
- for (;;) {
1037
- const l = 2 * i + 1;
1038
- const r = l + 1;
1039
- let m = i;
1040
- if (l < this.#k.length && this.#less(l, m)) m = l;
1041
- if (r < this.#k.length && this.#less(r, m)) m = r;
1042
- if (m === i) break;
1043
- this.#swap(i, m);
1044
- i = m;
1045
- }
1374
+ const top = this.#j[0];
1375
+ this.size--;
1376
+ if (this.size > 0) {
1377
+ this.#d[0] = this.#d[this.size];
1378
+ this.#j[0] = this.#j[this.size];
1379
+ this.#down(0);
1046
1380
  }
1047
1381
  return top;
1048
1382
  }
1383
+ #down(i) {
1384
+ const d = this.#d;
1385
+ const jj = this.#j;
1386
+ const n = this.size;
1387
+ const di = d[i];
1388
+ const ji = jj[i];
1389
+ for (;;) {
1390
+ const l = 2 * i + 1;
1391
+ if (l >= n) break;
1392
+ const r = l + 1;
1393
+ let m = l;
1394
+ if (r < n && (d[r] < d[l] || d[r] === d[l] && jj[r] < jj[l])) m = r;
1395
+ if (!(d[m] < di || d[m] === di && jj[m] < ji)) break;
1396
+ d[i] = d[m];
1397
+ jj[i] = jj[m];
1398
+ i = m;
1399
+ }
1400
+ d[i] = di;
1401
+ jj[i] = ji;
1402
+ }
1049
1403
  };
1050
1404
  // src/ways.ts
1051
1405
  /**
@@ -1511,7 +1865,296 @@ const plugsRouter = {
1511
1865
  return { routes };
1512
1866
  }
1513
1867
  };
1868
+ // src/bench/properties.ts
1869
+ /** The tolerance of the hard rules, in units of the plane. */
1870
+ const TOLERANCE = .5;
1871
+ /**
1872
+ * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
1873
+ * Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
1874
+ * Each link of a level starts on the box of its source and ends on the box of its target.
1875
+ *
1876
+ * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
1877
+ * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
1878
+ * The function gives at most `limit` violations.
1879
+ */
1880
+ function checkProperties(graph, result, limit = 100) {
1881
+ const out = [];
1882
+ const add = (v) => {
1883
+ out.push(v);
1884
+ return out.length >= limit;
1885
+ };
1886
+ const byId = nodeIndex(graph);
1887
+ const grid = new Grid(180);
1888
+ for (const n of graph.nodes) grid.add(n, n.box);
1889
+ const routes = /* @__PURE__ */ new Map();
1890
+ for (const r of result.routes) routes.set(r.edge, r);
1891
+ for (const e of graph.edges) {
1892
+ const r = routes.get(e.id);
1893
+ const s = byId.get(e.source.node);
1894
+ const t = byId.get(e.target.node);
1895
+ if (!s || !t) continue;
1896
+ if (!r || r.points.length < 2) {
1897
+ if (add({
1898
+ rule: "complete",
1899
+ edge: e.id,
1900
+ detail: "no route"
1901
+ })) return out;
1902
+ continue;
1903
+ }
1904
+ const a = r.points[0];
1905
+ const b = r.points[r.points.length - 1];
1906
+ if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
1907
+ if (add({
1908
+ rule: "ends-at-plugs",
1909
+ edge: e.id,
1910
+ detail: "an end of the route is away from its node"
1911
+ })) return out;
1912
+ }
1913
+ const fs = fixedPortPoint(s, e.source);
1914
+ const ft = fixedPortPoint(t, e.target);
1915
+ if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
1916
+ if (add({
1917
+ rule: "fixed-plugs",
1918
+ edge: e.id,
1919
+ detail: "the route does not start or end at its fixed plug"
1920
+ })) return out;
1921
+ }
1922
+ for (let i = 1; i < r.points.length; i++) {
1923
+ const p = r.points[i - 1];
1924
+ const q = r.points[i];
1925
+ let hit;
1926
+ grid.querySegment(p, q, 0, (n) => {
1927
+ if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
1928
+ });
1929
+ if (hit) {
1930
+ if (add({
1931
+ rule: "no-pass-through",
1932
+ edge: e.id,
1933
+ detail: `segment ${i} goes through ${hit.id}`
1934
+ })) return out;
1935
+ break;
1936
+ }
1937
+ }
1938
+ }
1939
+ const bundled = /* @__PURE__ */ new Set();
1940
+ for (const b of result.bundles ?? []) for (const id of b.edges) {
1941
+ const r = routes.get(id);
1942
+ const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
1943
+ if (bundled.has(id) || !shares) {
1944
+ if (add({
1945
+ rule: "bundle-trunk",
1946
+ edge: id,
1947
+ detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
1948
+ })) return out;
1949
+ }
1950
+ bundled.add(id);
1951
+ }
1952
+ for (const run of result.runs ?? []) {
1953
+ if (run.edges.length < 2) {
1954
+ if (add({
1955
+ rule: "run-shared",
1956
+ detail: `run ${run.id} has fewer than two edges`
1957
+ })) return out;
1958
+ }
1959
+ for (const id of run.edges) {
1960
+ const r = routes.get(id);
1961
+ if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
1962
+ if (add({
1963
+ rule: "run-shared",
1964
+ edge: id,
1965
+ detail: `run ${run.id}: the route does not hold the points of the run in a row`
1966
+ })) return out;
1967
+ }
1968
+ }
1969
+ }
1970
+ for (const level of result.levels ?? []) {
1971
+ const itemOf = /* @__PURE__ */ new Map();
1972
+ for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
1973
+ if (itemOf.size !== graph.nodes.length) {
1974
+ if (add({
1975
+ rule: "counts-sum",
1976
+ detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
1977
+ })) return out;
1978
+ }
1979
+ let between = 0;
1980
+ for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
1981
+ const sum = level.links.reduce((s, l) => s + l.count, 0);
1982
+ if (sum !== between) {
1983
+ if (add({
1984
+ rule: "counts-sum",
1985
+ detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
1986
+ })) return out;
1987
+ }
1988
+ const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
1989
+ for (const l of level.links) {
1990
+ const s = boxOf.get(l.source);
1991
+ const t = boxOf.get(l.target);
1992
+ const p = l.points;
1993
+ if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
1994
+ if (add({
1995
+ rule: "link-ends",
1996
+ detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
1997
+ })) return out;
1998
+ }
1999
+ }
2000
+ }
2001
+ return out;
2002
+ }
2003
+ /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
2004
+ function onBoundary(p, b) {
2005
+ const inner = inflate(b, -.5);
2006
+ 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;
2007
+ }
2008
+ /** This function tells if the polyline `p` holds the points of `part` in a row. */
2009
+ function holds(p, part) {
2010
+ if (part.length === 0) return true;
2011
+ for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
2012
+ return false;
2013
+ }
2014
+ // src/bench/links.ts
2015
+ /** A link goes out and back when its distance to its target grows by more than this share of the distance between its ends. */
2016
+ const BACKTRACK_SHARE = .25;
2017
+ /** A link goes out and back only when its distance to its target grows by more than this number of units. */
2018
+ const BACKTRACK_MIN = 40;
2019
+ /** The default `slack` of `LinkFaultOptions`. */
2020
+ const OUTSIDE_SLACK = 24;
2021
+ /**
2022
+ * This function gives the largest growth of the distance to the last point of a polyline.
2023
+ * The growth at a vertex is its distance to the last point, less the nearest approach of the polyline before that vertex.
2024
+ */
2025
+ function backtrackOf(p) {
2026
+ if (p.length < 3) return 0;
2027
+ const end = p[p.length - 1];
2028
+ let near = dist(p[0], end);
2029
+ let worst = 0;
2030
+ for (let i = 1; i < p.length; i++) {
2031
+ worst = Math.max(worst, dist(p[i], end) - near);
2032
+ near = Math.min(near, pointSegmentDistance(end, p[i - 1], p[i]));
2033
+ }
2034
+ return worst;
2035
+ }
2036
+ /** This function tells if the polyline `p` goes out and back: `backtrackOf(p)` is more than `BACKTRACK_MIN` and more than `BACKTRACK_SHARE` of the span. */
2037
+ function goesOutAndBack(p) {
2038
+ if (p.length < 3) return false;
2039
+ const span = dist(p[0], p[p.length - 1]);
2040
+ return backtrackOf(p) > Math.max(40, BACKTRACK_SHARE * span);
2041
+ }
2042
+ /**
2043
+ * This function measures the faults of links between boxes. The map `boxes` gives the box of each end.
2044
+ * The bounding box of `outside` is the bounding box of all the boxes of the map.
2045
+ */
2046
+ function measureLinks(boxes, links, options = {}) {
2047
+ const slack = options.slack ?? 24;
2048
+ let size = 0;
2049
+ let x0 = Infinity;
2050
+ let y0 = Infinity;
2051
+ let x1 = -Infinity;
2052
+ let y1 = -Infinity;
2053
+ for (const b of boxes.values()) {
2054
+ size += Math.max(b.w, b.h);
2055
+ x0 = Math.min(x0, b.x);
2056
+ y0 = Math.min(y0, b.y);
2057
+ x1 = Math.max(x1, b.x + b.w);
2058
+ y1 = Math.max(y1, b.y + b.h);
2059
+ }
2060
+ const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.size)));
2061
+ for (const [id, b] of boxes) grid.add(id, b);
2062
+ let through = 0;
2063
+ let backtrack = 0;
2064
+ let worstDetour = 0;
2065
+ let outside = 0;
2066
+ for (const l of links) {
2067
+ const p = l.points;
2068
+ if (p.length === 0) continue;
2069
+ let hit = false;
2070
+ for (let i = 1; i < p.length && !hit; i++) {
2071
+ const a = p[i - 1];
2072
+ const b = p[i];
2073
+ grid.querySegment(a, b, 0, (id) => {
2074
+ if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id), -.5), .5)) hit = true;
2075
+ });
2076
+ }
2077
+ if (hit) through++;
2078
+ if (goesOutAndBack(p)) backtrack++;
2079
+ const span = dist(p[0], p[p.length - 1]);
2080
+ if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
2081
+ if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
2082
+ }
2083
+ return {
2084
+ links: links.length,
2085
+ through,
2086
+ backtrack,
2087
+ worstDetour,
2088
+ outside
2089
+ };
2090
+ }
2091
+ /** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
2092
+ function measureRouteFaults(graph, result, options = {}) {
2093
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
2094
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
2095
+ const links = [];
2096
+ for (const r of result.routes) {
2097
+ const e = edges.get(r.edge);
2098
+ if (e) links.push({
2099
+ source: e.source.node,
2100
+ target: e.target.node,
2101
+ points: r.points
2102
+ });
2103
+ }
2104
+ return measureLinks(boxes, links, options);
2105
+ }
2106
+ /** This function counts the pairs of boxes that overlap by more than `TOLERANCE` on each axis. */
2107
+ function countOverlaps(boxes) {
2108
+ let size = 0;
2109
+ for (const b of boxes) size += Math.max(b.w, b.h);
2110
+ const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.length)));
2111
+ boxes.forEach((b, i) => grid.add(i, b));
2112
+ let n = 0;
2113
+ boxes.forEach((a, i) => {
2114
+ grid.query(a, (j) => {
2115
+ const b = boxes[j];
2116
+ if (j > i && Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > .5 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > .5) n++;
2117
+ });
2118
+ });
2119
+ return n;
2120
+ }
2121
+ /** This function measures the faults of the links of one zoom level. The items of the level are the boxes. */
2122
+ function measureLevel(level, options = {}) {
2123
+ const boxes = new Map(level.items.map((i) => [i.id, i.box]));
2124
+ return {
2125
+ depth: level.depth,
2126
+ items: level.items.length,
2127
+ groups: level.items.filter((i) => i.nodes.length > 1).length,
2128
+ overlaps: countOverlaps(level.items.map((i) => i.box)),
2129
+ ...measureLinks(boxes, level.links, options)
2130
+ };
2131
+ }
2132
+ /**
2133
+ * This function measures the faults of each zoom level of a result that has groups: the levels of the far view.
2134
+ * A level of single nodes only is the near view. Its links are routes, and `measureRouteFaults` measures them.
2135
+ */
2136
+ function measureLevelFaults(result, options = {}) {
2137
+ return (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1)).map((l) => measureLevel(l, options));
2138
+ }
2139
+ /** This function adds the faults of several levels: it sums the counts and keeps the largest `worstDetour`. */
2140
+ function sumFaults(list) {
2141
+ return list.reduce((s, f) => ({
2142
+ links: s.links + f.links,
2143
+ through: s.through + f.through,
2144
+ backtrack: s.backtrack + f.backtrack,
2145
+ worstDetour: Math.max(s.worstDetour, f.worstDetour),
2146
+ outside: s.outside + f.outside
2147
+ }), {
2148
+ links: 0,
2149
+ through: 0,
2150
+ backtrack: 0,
2151
+ worstDetour: 0,
2152
+ outside: 0
2153
+ });
2154
+ }
1514
2155
  // src/levels.ts
2156
+ /** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
2157
+ const TIGHT_MARGIN = 1;
1515
2158
  const EPS = 1e-6;
1516
2159
  /**
1517
2160
  * This function removes the repeated points, the points in the middle of a straight run,
@@ -1554,7 +2197,7 @@ function levelLinks(items, pairs, options = {}) {
1554
2197
  if (!s || !t) continue;
1555
2198
  const [fs, ft] = facingPlugs(s.box, t.box, inset);
1556
2199
  let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : void 0;
1557
- points ??= squareWay(ways, ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box))));
2200
+ points ??= sideWay(ways, s, t, inset);
1558
2201
  out.push({
1559
2202
  source: p.source,
1560
2203
  target: p.target,
@@ -1565,25 +2208,49 @@ function levelLinks(items, pairs, options = {}) {
1565
2208
  return out;
1566
2209
  }
1567
2210
  /**
2211
+ * This function gives a way from plug to plug between the plugs at the middle of a side of the two nodes.
2212
+ * Without a way in the corner graph, the link goes straight between the ends, with legs parallel to the axes where they are clear.
2213
+ * When that line goes through another box, the link goes between the free plugs of the two nodes (`freeWay`) instead.
2214
+ */
2215
+ function sideWay(ways, s, t, inset) {
2216
+ const e = ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box)));
2217
+ const quick = quickWay(ways, e);
2218
+ if (quick) return quick;
2219
+ const path = ways.corners.path(e.a, e.b);
2220
+ const way = pulled(ways, e.ps, path ?? [e.a, e.b], e.pt);
2221
+ if (path || ways.obstacles.clearPath(way, 0, /* @__PURE__ */ new Set([s, t]))) return way;
2222
+ return freeWay(ways, s, t, inset) ?? way;
2223
+ }
2224
+ /**
1568
2225
  * This function makes the obstacles and the corner graph of the items of a level.
1569
2226
  * The buckets of the obstacles grow with the items, so a long link visits few buckets.
1570
2227
  */
1571
2228
  function levelWays(nodes, options) {
1572
2229
  const margin = options.margin ?? 5;
2230
+ const k = options.k ?? 8;
1573
2231
  let size = 0;
1574
2232
  for (const n of nodes) size += Math.max(n.box.w, n.box.h);
1575
2233
  const obstacles = new Obstacles(nodes, Math.max(180, 2 * size / Math.max(1, nodes.length)));
2234
+ const corners = new CornerGraph(nodes, obstacles, {
2235
+ margin,
2236
+ k
2237
+ });
2238
+ const out = (p) => ({
2239
+ x: p.point.x + p.normal.x * margin,
2240
+ y: p.point.y + p.normal.y * margin
2241
+ });
2242
+ let tight;
2243
+ const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
2244
+ margin: Math.min(margin, TIGHT_MARGIN),
2245
+ k
2246
+ });
1576
2247
  return {
1577
2248
  obstacles,
1578
- corners: new CornerGraph(nodes, obstacles, {
1579
- margin,
1580
- k: options.k ?? 8
1581
- }),
2249
+ corners,
1582
2250
  margin,
1583
- out: (p) => ({
1584
- x: p.point.x + p.normal.x * margin,
1585
- y: p.point.y + p.normal.y * margin
1586
- })
2251
+ out,
2252
+ tight: tightGraph,
2253
+ free: /* @__PURE__ */ new Map()
1587
2254
  };
1588
2255
  }
1589
2256
  /**
@@ -1657,13 +2324,139 @@ function sidePlug(ways, node, toward) {
1657
2324
  return plug(order[0]);
1658
2325
  }
1659
2326
  /**
2327
+ * This function gives the free plugs of a node: on each side, the free point nearest to the middle of the side.
2328
+ * A point is free when it and the point at `margin` from it are in no other box. The function keeps the plugs of each node.
2329
+ */
2330
+ function freePlugs(ways, node, inset) {
2331
+ const known = ways.free.get(node);
2332
+ if (known) return known;
2333
+ const b = node.box;
2334
+ const out = [];
2335
+ for (const side of SIDES) {
2336
+ const n = SIDE_NORMAL[side];
2337
+ const across = n.x === 0;
2338
+ const lo = across ? b.x : b.y;
2339
+ const hi = across ? b.x + b.w : b.y + b.h;
2340
+ const at = across ? n.y > 0 ? b.y + b.h : b.y : n.x > 0 ? b.x + b.w : b.x;
2341
+ const far = at + (across ? n.y : n.x) * ways.margin;
2342
+ const covered = [];
2343
+ const strip = across ? {
2344
+ x: lo,
2345
+ y: Math.min(at, far),
2346
+ w: hi - lo,
2347
+ h: Math.abs(far - at)
2348
+ } : {
2349
+ x: Math.min(at, far),
2350
+ y: lo,
2351
+ w: Math.abs(far - at),
2352
+ h: hi - lo
2353
+ };
2354
+ ways.obstacles.query(strip, (m) => {
2355
+ if (m === node) return;
2356
+ const o = m.box;
2357
+ const [o0, o1, p0, p1] = across ? [
2358
+ o.x,
2359
+ o.x + o.w,
2360
+ o.y,
2361
+ o.y + o.h
2362
+ ] : [
2363
+ o.y,
2364
+ o.y + o.h,
2365
+ o.x,
2366
+ o.x + o.w
2367
+ ];
2368
+ if (at > p0 && at < p1 || far > p0 && far < p1) covered.push([o0, o1]);
2369
+ });
2370
+ const mid = (lo + hi) / 2;
2371
+ let best;
2372
+ for (const [f0, f1] of freeSpans(lo, hi, covered)) {
2373
+ const room = Math.min(inset, (f1 - f0) / 2);
2374
+ const u = Math.max(f0 + room, Math.min(f1 - room, mid));
2375
+ if (best === void 0 || Math.abs(u - mid) < Math.abs(best - mid)) best = u;
2376
+ }
2377
+ if (best !== void 0) out.push({
2378
+ point: across ? {
2379
+ x: best,
2380
+ y: at
2381
+ } : {
2382
+ x: at,
2383
+ y: best
2384
+ },
2385
+ normal: n
2386
+ });
2387
+ }
2388
+ const free = {
2389
+ plugs: out,
2390
+ reach: out.map((p) => reach(ways, node, p))
2391
+ };
2392
+ ways.free.set(node, free);
2393
+ return free;
2394
+ }
2395
+ /** This function gives the indices of the plugs, and the plugs that face `toward` most come first. */
2396
+ function facingOrder(plugs, from, toward) {
2397
+ const facing = (p) => p.normal.x * (toward.x - from.x) + p.normal.y * (toward.y - from.y);
2398
+ return plugs.map((_, i) => i).sort((u, v) => facing(plugs[v]) - facing(plugs[u]) || u - v);
2399
+ }
2400
+ /** This function gives the parts of the span from `lo` to `hi` outside each open interval of `covered`. A part of zero length does not count. */
2401
+ function freeSpans(lo, hi, covered) {
2402
+ covered.sort((u, v) => u[0] - v[0]);
2403
+ const out = [];
2404
+ let x = lo;
2405
+ for (const [c0, c1] of covered) {
2406
+ if (c0 > x) out.push([x, Math.min(c0, hi)]);
2407
+ x = Math.max(x, c1);
2408
+ if (x >= hi) break;
2409
+ }
2410
+ if (x < hi) out.push([x, hi]);
2411
+ return out.filter(([a, b]) => b - a > EPS);
2412
+ }
2413
+ /**
2414
+ * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
2415
+ * A link uses it when its plugs at the middle of a side find no way. Other boxes can cover such a plug, or close the space in front of it.
2416
+ */
2417
+ function freeWay(ways, s, t, inset) {
2418
+ const fs = freePlugs(ways, s, inset);
2419
+ const ft = freePlugs(ways, t, inset);
2420
+ const cs = center(s.box);
2421
+ const ct = center(t.box);
2422
+ const is = facingOrder(fs.plugs, cs, ct);
2423
+ const it = facingOrder(ft.plugs, ct, cs);
2424
+ const from = is.map((i) => fs.plugs[i]);
2425
+ const to = it.map((k) => ft.plugs[k]);
2426
+ const ra = is.map((i) => fs.reach[i]);
2427
+ const rb = it.map((k) => ft.reach[k]);
2428
+ let best;
2429
+ let bestLength = Infinity;
2430
+ from.forEach((ps, i) => to.forEach((pt, k) => {
2431
+ const way = quickWay(ways, endsAt(ps, ra[i], pt, rb[k]));
2432
+ const length = way ? polylineLength(way) : Infinity;
2433
+ if (length < bestLength) {
2434
+ best = way;
2435
+ bestLength = length;
2436
+ }
2437
+ }));
2438
+ if (best) return best;
2439
+ const a = from.map((ps, i) => ({
2440
+ point: along(ps, ra[i]),
2441
+ cost: ra[i]
2442
+ }));
2443
+ const b = to.map((pt, k) => ({
2444
+ point: along(pt, rb[k]),
2445
+ cost: rb[k]
2446
+ }));
2447
+ const found = ways.tight().pathAmong(a, b);
2448
+ return found && pulled(ways, from[found.from], found.points, to[found.to]);
2449
+ }
2450
+ /**
1660
2451
  * This function gives the ends of a link. The link leaves each plug along its normal for the margin.
1661
2452
  * A box can be nearer than twice the margin in front of a plug. Then the link turns in the middle of the space before that box.
1662
2453
  * Two plugs that face each other across a narrow space make the link turn in the middle of that space.
1663
2454
  */
1664
2455
  function ends(ways, s, ps, t, pt) {
1665
- let ra = reach(ways, s, ps);
1666
- let rb = reach(ways, t, pt);
2456
+ return endsAt(ps, reach(ways, s, ps), pt, reach(ways, t, pt));
2457
+ }
2458
+ /** This function gives the ends of a link from the plug `ps` to the plug `pt`, with the reach of each plug. */
2459
+ function endsAt(ps, ra, pt, rb) {
1667
2460
  if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
1668
2461
  const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
1669
2462
  if (d > 0 && ra + rb > d) ra = rb = d / 2;
@@ -1709,17 +2502,15 @@ function quickWay(ways, e) {
1709
2502
  ]);
1710
2503
  }
1711
2504
  /**
1712
- * This function gives a way from plug to plug through the corner graph. The way leaves and enters each plug along its normal.
1713
- * Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
2505
+ * This function gives a way from the plug `ps` through the points of `path` to the plug `pt`.
2506
+ * The way leaves and enters each plug along its normal. Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
1714
2507
  */
1715
- function squareWay(ways, e) {
1716
- const quick = quickWay(ways, e);
1717
- if (quick) return quick;
1718
- const legs = squarePull(ways.corners.path(e.a, e.b) ?? [e.a, e.b], ways.obstacles, ways.margin / 2, axisOf(e.ps.normal), axisOf(e.pt.normal));
2508
+ function pulled(ways, ps, path, pt) {
2509
+ const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
1719
2510
  return tidy([
1720
- e.ps.point,
2511
+ ps.point,
1721
2512
  ...legs,
1722
- e.pt.point
2513
+ pt.point
1723
2514
  ]);
1724
2515
  }
1725
2516
  const axisOf = (n) => Math.abs(n.x) > Math.abs(n.y) ? "x" : "y";
@@ -1779,11 +2570,18 @@ function square(a, b, obstacles, pad, from, to) {
1779
2570
  // src/routers/sections.ts
1780
2571
  const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
1781
2572
  /**
2573
+ * A hub can be this many units farther from the target than the nearest approach of the way before it.
2574
+ * A hub farther away goes out and back, so the way skips it.
2575
+ */
2576
+ const HUB_SLACK = 80;
2577
+ /**
1782
2578
  * 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.
1783
2579
  * An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
1784
2580
  * Each way from hub to hub is found one time, so all the edges on it share its points.
1785
2581
  * The edges in one section of the lowest level go straight around the nodes.
1786
2582
  *
2583
+ * An edge skips a hub that takes it away from its target. A way that still goes out and back gives way to the direct way.
2584
+ *
1787
2585
  * The result gives one zoom level for each depth of the tree.
1788
2586
  * A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
1789
2587
  */
@@ -1824,6 +2622,19 @@ function sectionsRouter(options) {
1824
2622
  }
1825
2623
  return h;
1826
2624
  };
2625
+ const forward = (list, from, to) => {
2626
+ const kept = [];
2627
+ let near = dist(from, to);
2628
+ let last = from;
2629
+ for (const c of list) {
2630
+ const h = hub(c);
2631
+ if (dist(h, to) > near + HUB_SLACK) continue;
2632
+ near = Math.min(near, pointSegmentDistance(to, last, h));
2633
+ last = h;
2634
+ kept.push(c);
2635
+ }
2636
+ return kept;
2637
+ };
1827
2638
  const up = (s) => {
1828
2639
  const out = [];
1829
2640
  for (let x = s; x; x = parent.get(x)) out.push(x);
@@ -1869,19 +2680,26 @@ function sectionsRouter(options) {
1869
2680
  const lca = a.find((x) => common.has(x));
1870
2681
  const keep = options.hubs ?? Infinity;
1871
2682
  const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
1872
- const chain = [...side(a), ...side(b).reverse()];
2683
+ const end = ways.out(pt);
2684
+ const start = ways.out(ps);
2685
+ const chain = forward([...side(a), ...side(b).reverse()], start, pt.point);
2686
+ if (chain.length === 0) {
2687
+ routes.push({
2688
+ edge: e.id,
2689
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
2690
+ });
2691
+ continue;
2692
+ }
1873
2693
  const out = [ps.point];
1874
2694
  const append = (p) => {
1875
2695
  for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
1876
2696
  };
1877
2697
  const used = [];
1878
- const start = ways.out(ps);
1879
2698
  append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
1880
2699
  for (let i = 1; i < chain.length; i++) append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
1881
- const end = ways.out(pt);
1882
2700
  append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
1883
2701
  append([pt.point]);
1884
- if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
2702
+ if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin || goesOutAndBack(out)) {
1885
2703
  routes.push({
1886
2704
  edge: e.id,
1887
2705
  points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
@@ -2446,152 +3264,6 @@ const ROUTERS = [
2446
3264
  maxNodes: 1e5
2447
3265
  })
2448
3266
  ];
2449
- // src/bench/properties.ts
2450
- /** The tolerance of the hard rules, in units of the plane. */
2451
- const TOLERANCE = .5;
2452
- /**
2453
- * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
2454
- * Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
2455
- * Each link of a level starts on the box of its source and ends on the box of its target.
2456
- *
2457
- * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
2458
- * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
2459
- * The function gives at most `limit` violations.
2460
- */
2461
- function checkProperties(graph, result, limit = 100) {
2462
- const out = [];
2463
- const add = (v) => {
2464
- out.push(v);
2465
- return out.length >= limit;
2466
- };
2467
- const byId = nodeIndex(graph);
2468
- const grid = new Grid(180);
2469
- for (const n of graph.nodes) grid.add(n, n.box);
2470
- const routes = /* @__PURE__ */ new Map();
2471
- for (const r of result.routes) routes.set(r.edge, r);
2472
- for (const e of graph.edges) {
2473
- const r = routes.get(e.id);
2474
- const s = byId.get(e.source.node);
2475
- const t = byId.get(e.target.node);
2476
- if (!s || !t) continue;
2477
- if (!r || r.points.length < 2) {
2478
- if (add({
2479
- rule: "complete",
2480
- edge: e.id,
2481
- detail: "no route"
2482
- })) return out;
2483
- continue;
2484
- }
2485
- const a = r.points[0];
2486
- const b = r.points[r.points.length - 1];
2487
- if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
2488
- if (add({
2489
- rule: "ends-at-plugs",
2490
- edge: e.id,
2491
- detail: "an end of the route is away from its node"
2492
- })) return out;
2493
- }
2494
- const fs = fixedPortPoint(s, e.source);
2495
- const ft = fixedPortPoint(t, e.target);
2496
- if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
2497
- if (add({
2498
- rule: "fixed-plugs",
2499
- edge: e.id,
2500
- detail: "the route does not start or end at its fixed plug"
2501
- })) return out;
2502
- }
2503
- for (let i = 1; i < r.points.length; i++) {
2504
- const p = r.points[i - 1];
2505
- const q = r.points[i];
2506
- let hit;
2507
- grid.querySegment(p, q, 0, (n) => {
2508
- if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
2509
- });
2510
- if (hit) {
2511
- if (add({
2512
- rule: "no-pass-through",
2513
- edge: e.id,
2514
- detail: `segment ${i} goes through ${hit.id}`
2515
- })) return out;
2516
- break;
2517
- }
2518
- }
2519
- }
2520
- const bundled = /* @__PURE__ */ new Set();
2521
- for (const b of result.bundles ?? []) for (const id of b.edges) {
2522
- const r = routes.get(id);
2523
- const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
2524
- if (bundled.has(id) || !shares) {
2525
- if (add({
2526
- rule: "bundle-trunk",
2527
- edge: id,
2528
- detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
2529
- })) return out;
2530
- }
2531
- bundled.add(id);
2532
- }
2533
- for (const run of result.runs ?? []) {
2534
- if (run.edges.length < 2) {
2535
- if (add({
2536
- rule: "run-shared",
2537
- detail: `run ${run.id} has fewer than two edges`
2538
- })) return out;
2539
- }
2540
- for (const id of run.edges) {
2541
- const r = routes.get(id);
2542
- if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
2543
- if (add({
2544
- rule: "run-shared",
2545
- edge: id,
2546
- detail: `run ${run.id}: the route does not hold the points of the run in a row`
2547
- })) return out;
2548
- }
2549
- }
2550
- }
2551
- for (const level of result.levels ?? []) {
2552
- const itemOf = /* @__PURE__ */ new Map();
2553
- for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
2554
- if (itemOf.size !== graph.nodes.length) {
2555
- if (add({
2556
- rule: "counts-sum",
2557
- detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
2558
- })) return out;
2559
- }
2560
- let between = 0;
2561
- for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
2562
- const sum = level.links.reduce((s, l) => s + l.count, 0);
2563
- if (sum !== between) {
2564
- if (add({
2565
- rule: "counts-sum",
2566
- detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
2567
- })) return out;
2568
- }
2569
- const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
2570
- for (const l of level.links) {
2571
- const s = boxOf.get(l.source);
2572
- const t = boxOf.get(l.target);
2573
- const p = l.points;
2574
- if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
2575
- if (add({
2576
- rule: "link-ends",
2577
- detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
2578
- })) return out;
2579
- }
2580
- }
2581
- }
2582
- return out;
2583
- }
2584
- /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
2585
- function onBoundary(p, b) {
2586
- const inner = inflate(b, -.5);
2587
- 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;
2588
- }
2589
- /** This function tells if the polyline `p` holds the points of `part` in a row. */
2590
- function holds(p, part) {
2591
- if (part.length === 0) return true;
2592
- for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
2593
- return false;
2594
- }
2595
3267
  // src/bench/quality.ts
2596
3268
  /** The clearance stops at this distance. */
2597
3269
  const CLEARANCE_CAP = 40;
@@ -2834,7 +3506,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
2834
3506
  deterministic,
2835
3507
  violations: all.length,
2836
3508
  sample: all.slice(0, 5),
2837
- quality: measureQuality(graph, result)
3509
+ quality: measureQuality(graph, result),
3510
+ faults: measureRouteFaults(graph, result),
3511
+ levelFaults: measureLevelFaults(result)
2838
3512
  };
2839
3513
  }
2840
3514
  /** This function writes the rows as a Markdown table. */
@@ -2851,6 +3525,33 @@ function formatTable(rows) {
2851
3525
  })
2852
3526
  ].join("\n");
2853
3527
  }
2854
- export { CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, dist, fixedPortPoint, formatTable, frechet, groupEdges, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, measureQuality, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, runKey, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, treeDepth, trunksRouter, turns };
3528
+ /**
3529
+ * This function writes the faults of the rows as a Markdown table: the routes first, then the sum of the levels with groups.
3530
+ * A router without such levels gets a dash in the columns of the levels.
3531
+ */
3532
+ function formatFaults(rows) {
3533
+ return [
3534
+ "| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level out and back | level worst detour | level outside |",
3535
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
3536
+ ...rows.map((r) => {
3537
+ const f = r.faults;
3538
+ const cells = [
3539
+ r.router,
3540
+ r.fixture,
3541
+ f.through,
3542
+ f.backtrack,
3543
+ f.worstDetour.toFixed(2),
3544
+ f.outside
3545
+ ];
3546
+ if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
3547
+ else {
3548
+ const l = sumFaults(r.levelFaults);
3549
+ cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.backtrack, l.worstDetour.toFixed(2), l.outside);
3550
+ }
3551
+ return `| ${cells.join(" | ")} |`;
3552
+ })
3553
+ ].join("\n");
3554
+ }
3555
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, backtrackOf, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, denseGroupsBoard, dist, fixedPortPoint, formatFaults, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, manyGroupsBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, runKey, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, sumFaults, treeDepth, trunksRouter, turns };
2855
3556
 
2856
3557
  //# sourceMappingURL=index.js.map