@mark1russell7/highways 0.1.1 → 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`. */
@@ -372,7 +427,7 @@ const COLLECTIONS = [
372
427
  "scores",
373
428
  "teams"
374
429
  ];
375
- const SIDES = [
430
+ const SIDES$1 = [
376
431
  "north",
377
432
  "east",
378
433
  "south",
@@ -441,7 +496,7 @@ function makeFixture(spec) {
441
496
  clusterOf.push(cluster);
442
497
  const id = `n${nodes.length}`;
443
498
  if (R() < (spec.fixedPorts ?? 0)) {
444
- const s = SIDES[Math.floor(R() * 4)];
499
+ const s = SIDES$1[Math.floor(R() * 4)];
445
500
  const t = .2 + .6 * R();
446
501
  const at = s === "north" ? {
447
502
  x: w * t,
@@ -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,8 +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();
971
+ #endCache = /* @__PURE__ */ new Map();
972
+ #fewCache = /* @__PURE__ */ new Map();
973
+ #part;
781
974
  /** This constructor makes the graph of the nodes. */
782
975
  constructor(nodes, obstacles, options) {
783
976
  this.#obstacles = obstacles;
@@ -840,12 +1033,17 @@ var CornerGraph = class {
840
1033
  });
841
1034
  }
842
1035
  #addPoint(c) {
843
- this.#pointGrid.add(this.points.length, {
1036
+ const j = this.points.length;
1037
+ this.#pointGrid.add(j, {
844
1038
  x: c.x,
845
1039
  y: c.y,
846
1040
  w: 0,
847
1041
  h: 0
848
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);
849
1047
  this.points.push(c);
850
1048
  }
851
1049
  /** This method gives the number of links of the graph. */
@@ -877,27 +1075,72 @@ var CornerGraph = class {
877
1075
  }
878
1076
  /** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
879
1077
  nearestVisible(p, k, self = -1) {
1078
+ return this.#nearestVisible(p, k, self, this.#pad);
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
+ */
1086
+ #nearestVisible(p, k, self, pad) {
880
1087
  const out = [];
881
- if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
882
- const tested = this.#tested;
883
- const stamp = ++this.#testStamp;
884
- for (let r = this.#cell; r <= this.#cell * 8; r *= 2) {
885
- const cands = [];
886
- this.#pointGrid.query({
887
- x: p.x - r,
888
- y: p.y - r,
889
- w: 2 * r,
890
- h: 2 * r
891
- }, (j) => {
892
- if (j !== self && tested[j] !== stamp) cands.push([dist(p, this.points[j]), j]);
893
- });
894
- cands.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
895
- for (const [d, j] of cands) {
896
- if (d > r) break;
897
- tested[j] = stamp;
898
- if (this.#obstacles.clear(p, this.points[j], this.#pad)) out.push(j);
899
- 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
+ }
900
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
+ }
1138
+ }
1139
+ wait = later;
1140
+ pi0 = i0;
1141
+ pi1 = i1;
1142
+ pj0 = j0;
1143
+ pj1 = j1;
901
1144
  }
902
1145
  return out;
903
1146
  }
@@ -909,6 +1152,35 @@ var CornerGraph = class {
909
1152
  */
910
1153
  path(from, to, cost, floor = 1) {
911
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) {
912
1184
  const n = this.points.length;
913
1185
  if (this.#g.length < n) {
914
1186
  this.#g = new Float64Array(n);
@@ -917,28 +1189,51 @@ var CornerGraph = class {
917
1189
  this.#closed = new Int32Array(n);
918
1190
  }
919
1191
  const stamp = ++this.#stamp;
1192
+ const points = this.points;
920
1193
  const goal = /* @__PURE__ */ new Map();
921
- for (const j of this.nearestVisible(to, this.#k * 2)) 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
+ });
922
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;
923
1208
  const heap = new MinHeap();
924
- 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
+ };
925
1218
  const relax = (j, g, parent) => {
926
- if (this.#closed[j] === stamp) return;
927
- if (this.#open[j] === stamp && this.#g[j] <= g) return;
928
- this.#open[j] = stamp;
929
- this.#g[j] = g;
930
- this.#parent[j] = parent;
931
- 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);
932
1225
  };
933
- for (const j of this.nearestVisible(from, this.#k * 2)) 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
+ });
934
1229
  let best = Infinity;
935
1230
  let bestEnd = -1;
936
1231
  while (heap.size > 0) {
937
1232
  if (heap.topKey >= best) break;
938
1233
  const i = heap.pop();
939
- if (this.#closed[i] === stamp) continue;
940
- this.#closed[i] = stamp;
941
- const gi = this.#g[i];
1234
+ if (closed[i] === stamp) continue;
1235
+ closed[i] = stamp;
1236
+ const gi = G[i];
942
1237
  const tail = goal.get(i);
943
1238
  if (tail !== void 0 && gi + tail < best) {
944
1239
  best = gi + tail;
@@ -948,15 +1243,69 @@ var CornerGraph = class {
948
1243
  const len = this.#len[i];
949
1244
  for (let m = 0; m < adj.length; m++) {
950
1245
  const j = adj[m];
1246
+ if (closed[j] === stamp) continue;
951
1247
  const d = len[m];
952
1248
  relax(j, gi + (cost ? cost(i, j, d) : d), i);
953
1249
  }
954
1250
  }
955
1251
  if (bestEnd < 0) return void 0;
956
- const out = [to];
957
- for (let i = bestEnd; i >= 0; i = this.#parent[i]) out.push(this.points[i]);
958
- out.push(from);
959
- 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;
1293
+ }
1294
+ /**
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.
1296
+ * Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
1297
+ * The graph does not change, so the method keeps the corners of each end.
1298
+ */
1299
+ #ends(p, count) {
1300
+ const cache = count === this.#k * 2 ? this.#endCache : this.#fewCache;
1301
+ const key = `${p.x},${p.y}`;
1302
+ let out = cache.get(key);
1303
+ if (!out) {
1304
+ out = this.nearestVisible(p, count);
1305
+ if (out.length === 0) out = this.#nearestVisible(p, count, -1, 0);
1306
+ cache.set(key, out);
1307
+ }
1308
+ return out;
960
1309
  }
961
1310
  /** This method removes the vertices that a later vertex makes needless: from each vertex, it goes to the farthest vertex in view. */
962
1311
  pull(p, lookahead = 16) {
@@ -972,61 +1321,85 @@ var CornerGraph = class {
972
1321
  return out;
973
1322
  }
974
1323
  };
975
- /** 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
+ */
976
1330
  var MinHeap = class {
977
- #k = [];
978
- #v = [];
979
- get size() {
980
- return this.#k.length;
981
- }
982
- #less(a, b) {
983
- return this.#k[a] < this.#k[b] || this.#k[a] === this.#k[b] && this.#v[a] < this.#v[b];
984
- }
985
- #swap(a, b) {
986
- const k = this.#k[a];
987
- this.#k[a] = this.#k[b];
988
- this.#k[b] = k;
989
- const v = this.#v[a];
990
- this.#v[a] = this.#v[b];
991
- this.#v[b] = v;
992
- }
1331
+ #d = /* @__PURE__ */ new Float64Array(64);
1332
+ #j = /* @__PURE__ */ new Int32Array(64);
1333
+ size = 0;
993
1334
  /** The smallest key. */
994
1335
  get topKey() {
995
- return this.#k[0];
1336
+ return this.#d[0];
996
1337
  }
997
1338
  push(key, value) {
998
- this.#k.push(key);
999
- this.#v.push(value);
1000
- 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;
1001
1343
  while (i > 0) {
1002
1344
  const p = i - 1 >> 1;
1003
- if (!this.#less(i, p)) break;
1004
- 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];
1005
1348
  i = p;
1006
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);
1007
1371
  }
1008
1372
  /** This method removes the pair with the smallest key and gives its value. */
1009
1373
  pop() {
1010
- const top = this.#v[0];
1011
- const k = this.#k.pop();
1012
- const v = this.#v.pop();
1013
- if (this.#k.length > 0) {
1014
- this.#k[0] = k;
1015
- this.#v[0] = v;
1016
- let i = 0;
1017
- for (;;) {
1018
- const l = 2 * i + 1;
1019
- const r = l + 1;
1020
- let m = i;
1021
- if (l < this.#k.length && this.#less(l, m)) m = l;
1022
- if (r < this.#k.length && this.#less(r, m)) m = r;
1023
- if (m === i) break;
1024
- this.#swap(i, m);
1025
- i = m;
1026
- }
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);
1027
1380
  }
1028
1381
  return top;
1029
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
+ }
1030
1403
  };
1031
1404
  // src/ways.ts
1032
1405
  /**
@@ -1492,162 +1865,888 @@ const plugsRouter = {
1492
1865
  return { routes };
1493
1866
  }
1494
1867
  };
1495
- // src/routers/sections.ts
1496
- const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
1868
+ // src/bench/properties.ts
1869
+ /** The tolerance of the hard rules, in units of the plane. */
1870
+ const TOLERANCE = .5;
1497
1871
  /**
1498
- * 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.
1499
- * An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
1500
- * Each way from hub to hub is found one time, so all the edges on it share its points.
1501
- * The edges in one section of the lowest level go straight around the nodes. The result gives one zoom level for each depth of the tree.
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.
1502
1879
  */
1503
- function sectionsRouter(options) {
1504
- const router = {
1505
- name: options.name,
1506
- route(graph) {
1507
- const stretch = options.stretch ?? 2;
1508
- const byId = nodeIndex(graph);
1509
- const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
1510
- const ways = new WayFinder(graph.nodes, options);
1511
- const corners = ways.corners;
1512
- const root = buildSections(graph.nodes, options);
1513
- const parent = /* @__PURE__ */ new Map();
1514
- const leafOf = [];
1515
- for (const s of allSections(root)) {
1516
- for (const c of s.children) parent.set(c, s);
1517
- if (s.children.length === 0) for (const i of s.nodes) leafOf[i] = s;
1518
- }
1519
- const hubs = /* @__PURE__ */ new Map();
1520
- const hub = (s) => {
1521
- let h = hubs.get(s);
1522
- if (!h) {
1523
- let x = 0;
1524
- let y = 0;
1525
- for (const i of s.nodes) {
1526
- const c = center(graph.nodes[i].box);
1527
- x += c.x;
1528
- y += c.y;
1529
- }
1530
- const c = {
1531
- x: x / Math.max(1, s.nodes.length),
1532
- y: y / Math.max(1, s.nodes.length)
1533
- };
1534
- const j = corners.nearest(c);
1535
- h = j >= 0 ? corners.points[j] : c;
1536
- hubs.set(s, h);
1537
- }
1538
- return h;
1539
- };
1540
- const up = (s) => {
1541
- const out = [];
1542
- for (let x = s; x; x = parent.get(x)) out.push(x);
1543
- return out;
1544
- };
1545
- const pieces = /* @__PURE__ */ new Map();
1546
- const users = /* @__PURE__ */ new Map();
1547
- const piece = (ka, a, kb, b, used) => {
1548
- const forward = ka <= kb;
1549
- const key = forward ? `${ka}>${kb}` : `${kb}>${ka}`;
1550
- let p = pieces.get(key);
1551
- if (!p) {
1552
- const [from, to] = forward ? [a, b] : [b, a];
1553
- const way = corners.path(from, to);
1554
- p = dedupe$1(way ? corners.pull(way) : [from, to]);
1555
- pieces.set(key, p);
1556
- }
1557
- if (!used.includes(key)) used.push(key);
1558
- return forward ? p : [...p].reverse();
1559
- };
1560
- const at = (p) => `${p.x},${p.y}`;
1561
- const routes = [];
1562
- const marks = /* @__PURE__ */ new Map();
1563
- for (const e of graph.edges) {
1564
- const s = byId.get(e.source.node);
1565
- const t = byId.get(e.target.node);
1566
- const iu = indexOfNode.get(e.source.node);
1567
- const iv = indexOfNode.get(e.target.node);
1568
- if (!s || !t || iu === void 0 || iv === void 0) continue;
1569
- const ps = placePlug(s, e.source, center(t.box));
1570
- const pt = placePlug(t, e.target, center(s.box));
1571
- const lu = leafOf[iu];
1572
- const lv = leafOf[iv];
1573
- if (lu === lv) {
1574
- routes.push({
1575
- edge: e.id,
1576
- points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
1577
- });
1578
- continue;
1579
- }
1580
- const a = up(lu);
1581
- const b = up(lv);
1582
- const common = new Set(b);
1583
- const lca = a.find((x) => common.has(x));
1584
- const keep = options.hubs ?? Infinity;
1585
- const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
1586
- const chain = [...side(a), ...side(b).reverse()];
1587
- const out = [ps.point];
1588
- const mark = /* @__PURE__ */ new Map();
1589
- const append = (p) => {
1590
- for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
1591
- };
1592
- const used = [];
1593
- const start = ways.out(ps);
1594
- append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
1595
- mark.set(chain[0], out.length - 1);
1596
- for (let i = 1; i < chain.length; i++) {
1597
- append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
1598
- mark.set(chain[i], out.length - 1);
1599
- }
1600
- const end = ways.out(pt);
1601
- append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
1602
- append([pt.point]);
1603
- if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
1604
- routes.push({
1605
- edge: e.id,
1606
- points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
1607
- });
1608
- continue;
1609
- }
1610
- for (const key of used) {
1611
- const list = users.get(key);
1612
- if (list) list.push(e.id);
1613
- else users.set(key, [e.id]);
1614
- }
1615
- routes.push({
1616
- edge: e.id,
1617
- points: out
1618
- });
1619
- marks.set(e.id, mark);
1620
- }
1621
- const routeOf = new Map(routes.map((r) => [r.edge, r]));
1622
- const runs = [];
1623
- for (const [key, p] of pieces) {
1624
- const list = users.get(key) ?? [];
1625
- if (list.length < 2 || p.length < 2) continue;
1626
- runs.push({
1627
- id: `r${runs.length}`,
1628
- key,
1629
- points: p,
1630
- edges: orderLanes(p, list.map((id) => routeOf.get(id)))
1631
- });
1632
- }
1633
- return {
1634
- routes,
1635
- runs,
1636
- levels: makeLevels(graph, root, routeOf, marks)
1637
- };
1638
- }
1639
- };
1640
- return options.maxNodes === void 0 ? router : {
1641
- ...router,
1642
- maxNodes: options.maxNodes
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
+ }
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;
2158
+ const EPS = 1e-6;
2159
+ /**
2160
+ * This function removes the repeated points, the points in the middle of a straight run,
2161
+ * and the points where the way turns back on itself.
2162
+ */
2163
+ function tidy(p) {
2164
+ const out = [];
2165
+ for (const q of p) {
2166
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2167
+ while (out.length >= 2) {
2168
+ const a = out[out.length - 2];
2169
+ const b = out[out.length - 1];
2170
+ const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
2171
+ if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
2172
+ out.pop();
2173
+ }
2174
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2175
+ out.push(q);
2176
+ }
2177
+ return out;
2178
+ }
2179
+ /**
2180
+ * This function gives the links of one level. The items of the level are the obstacles.
2181
+ * A link starts on the boundary of the box of its source and ends on the boundary of the box of its target.
2182
+ * It goes around the boxes of the other items. Where there is space, its legs are parallel to the axes.
2183
+ */
2184
+ function levelLinks(items, pairs, options = {}) {
2185
+ if (pairs.length === 0) return [];
2186
+ const nodes = items.map((i) => ({
2187
+ id: i.id,
2188
+ box: i.box
2189
+ }));
2190
+ const byId = new Map(nodes.map((n) => [n.id, n]));
2191
+ const ways = levelWays(nodes, options);
2192
+ const inset = Math.min(6, ways.margin);
2193
+ const out = [];
2194
+ for (const p of pairs) {
2195
+ const s = byId.get(p.source);
2196
+ const t = byId.get(p.target);
2197
+ if (!s || !t) continue;
2198
+ const [fs, ft] = facingPlugs(s.box, t.box, inset);
2199
+ let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : void 0;
2200
+ points ??= sideWay(ways, s, t, inset);
2201
+ out.push({
2202
+ source: p.source,
2203
+ target: p.target,
2204
+ count: p.count,
2205
+ points
2206
+ });
2207
+ }
2208
+ return out;
2209
+ }
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
+ /**
2225
+ * This function makes the obstacles and the corner graph of the items of a level.
2226
+ * The buckets of the obstacles grow with the items, so a long link visits few buckets.
2227
+ */
2228
+ function levelWays(nodes, options) {
2229
+ const margin = options.margin ?? 5;
2230
+ const k = options.k ?? 8;
2231
+ let size = 0;
2232
+ for (const n of nodes) size += Math.max(n.box.w, n.box.h);
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
+ });
2247
+ return {
2248
+ obstacles,
2249
+ corners,
2250
+ margin,
2251
+ out,
2252
+ tight: tightGraph,
2253
+ free: /* @__PURE__ */ new Map()
2254
+ };
2255
+ }
2256
+ /**
2257
+ * This function gives the plugs of a link between two boxes. Each plug is on the side that faces the other box.
2258
+ * When the two sides face each other and their spans overlap, both plugs are at the middle of the overlap, so the link is straight.
2259
+ */
2260
+ function facingPlugs(a, b, inset) {
2261
+ const sa = sideToward(a, center(b));
2262
+ const sb = sideToward(b, center(a));
2263
+ const plug = (box, side, toward) => ({
2264
+ point: pointOnSide(box, side, toward, inset),
2265
+ normal: SIDE_NORMAL[side]
2266
+ });
2267
+ const across = sa === "east" && sb === "west" || sa === "west" && sb === "east";
2268
+ if (across || sa === "south" && sb === "north" || sa === "north" && sb === "south") {
2269
+ const lo = across ? Math.max(a.y, b.y) + inset : Math.max(a.x, b.x) + inset;
2270
+ const hi = across ? Math.min(a.y + a.h, b.y + b.h) - inset : Math.min(a.x + a.w, b.x + b.w) - inset;
2271
+ if (lo <= hi) {
2272
+ const ca = center(a);
2273
+ const cb = center(b);
2274
+ const m = Math.max(lo, Math.min(hi, across ? (ca.y + cb.y) / 2 : (ca.x + cb.x) / 2));
2275
+ const at = (box, side) => across ? {
2276
+ x: side === "east" ? box.x + box.w : box.x,
2277
+ y: m
2278
+ } : {
2279
+ x: m,
2280
+ y: side === "south" ? box.y + box.h : box.y
2281
+ };
2282
+ return [{
2283
+ point: at(a, sa),
2284
+ normal: SIDE_NORMAL[sa]
2285
+ }, {
2286
+ point: at(b, sb),
2287
+ normal: SIDE_NORMAL[sb]
2288
+ }];
2289
+ }
2290
+ }
2291
+ return [plug(a, sa, center(b)), plug(b, sb, center(a))];
2292
+ }
2293
+ const SIDES = [
2294
+ "north",
2295
+ "east",
2296
+ "south",
2297
+ "west"
2298
+ ];
2299
+ /** This function tells if the way out of a plug is free. The plug and the point at `margin` from it are in no other box. */
2300
+ function isFree(ways, node, plug) {
2301
+ return !ways.obstacles.inside(ways.out(plug), 0, node) && !ways.obstacles.inside(plug.point, 0, node);
2302
+ }
2303
+ /** This function gives the plug at the middle of a side of the node: the side with a free way out that faces `toward` most. */
2304
+ function sidePlug(ways, node, toward) {
2305
+ const b = node.box;
2306
+ const c = center(b);
2307
+ const d = {
2308
+ x: toward.x - c.x,
2309
+ y: toward.y - c.y
2310
+ };
2311
+ const facing = (s) => SIDE_NORMAL[s].x * d.x + SIDE_NORMAL[s].y * d.y;
2312
+ const order = [...SIDES].sort((u, v) => facing(v) - facing(u));
2313
+ const plug = (s) => {
2314
+ const n = SIDE_NORMAL[s];
2315
+ return {
2316
+ point: {
2317
+ x: c.x + n.x * b.w / 2,
2318
+ y: c.y + n.y * b.h / 2
2319
+ },
2320
+ normal: n
2321
+ };
2322
+ };
2323
+ for (const s of order) if (isFree(ways, node, plug(s))) return plug(s);
2324
+ return plug(order[0]);
2325
+ }
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
+ /**
2451
+ * This function gives the ends of a link. The link leaves each plug along its normal for the margin.
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.
2453
+ * Two plugs that face each other across a narrow space make the link turn in the middle of that space.
2454
+ */
2455
+ function ends(ways, s, ps, 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) {
2460
+ if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
2461
+ const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
2462
+ if (d > 0 && ra + rb > d) ra = rb = d / 2;
2463
+ }
2464
+ return {
2465
+ ps,
2466
+ pt,
2467
+ a: along(ps, ra),
2468
+ b: along(pt, rb)
2469
+ };
2470
+ }
2471
+ const along = (p, d) => ({
2472
+ x: p.point.x + p.normal.x * d,
2473
+ y: p.point.y + p.normal.y * d
2474
+ });
2475
+ /** This function gives the distance that a link goes along the normal of a plug before it turns. */
2476
+ function reach(ways, node, plug) {
2477
+ const skip = /* @__PURE__ */ new Set([node]);
2478
+ const far = 2 * ways.margin;
2479
+ if (ways.obstacles.clear(plug.point, along(plug, far), 0, skip)) return ways.margin;
2480
+ let lo = 0;
2481
+ let hi = far;
2482
+ for (let i = 0; i < 10; i++) {
2483
+ const m = (lo + hi) / 2;
2484
+ if (ways.obstacles.clear(plug.point, along(plug, m), 0, skip)) lo = m;
2485
+ else hi = m;
2486
+ }
2487
+ return lo / 2;
2488
+ }
2489
+ /**
2490
+ * This function gives the way between the ends when one or two legs parallel to the axes are clear, or `undefined`.
2491
+ * The legs keep half the margin from the boxes, or less when an end turns nearer to a box.
2492
+ */
2493
+ function quickWay(ways, e) {
2494
+ const near = Math.min(dist(e.ps.point, e.a), dist(e.pt.point, e.b));
2495
+ const pad = Math.max(0, Math.min(ways.margin / 2, near - 1));
2496
+ const legs = square(e.a, e.b, ways.obstacles, pad, axisOf(e.ps.normal), axisOf(e.pt.normal));
2497
+ return legs && tidy([
2498
+ e.ps.point,
2499
+ e.a,
2500
+ ...legs,
2501
+ e.pt.point
2502
+ ]);
2503
+ }
2504
+ /**
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.
2507
+ */
2508
+ function pulled(ways, ps, path, pt) {
2509
+ const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
2510
+ return tidy([
2511
+ ps.point,
2512
+ ...legs,
2513
+ pt.point
2514
+ ]);
2515
+ }
2516
+ const axisOf = (n) => Math.abs(n.x) > Math.abs(n.y) ? "x" : "y";
2517
+ /**
2518
+ * This function pulls a path tight. From each vertex, it goes to the farthest vertex that one or two legs can reach.
2519
+ * These legs are parallel to the axes and clear of the obstacles by `pad`. It keeps a diagonal leg only where no such legs are clear.
2520
+ * `first` is the axis of the way into the first vertex, and `last` is the axis of the way out of the last vertex.
2521
+ */
2522
+ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
2523
+ if (p.length === 0) return [];
2524
+ const out = [p[0]];
2525
+ let axis = first;
2526
+ let i = 0;
2527
+ while (i < p.length - 1) {
2528
+ let step;
2529
+ for (let j = Math.min(p.length - 1, i + lookahead); j > i && !step; j--) {
2530
+ const legs = square(p[i], p[j], obstacles, pad, axis, j === p.length - 1 ? last : void 0);
2531
+ if (legs) step = {
2532
+ j,
2533
+ legs
2534
+ };
2535
+ }
2536
+ step ??= {
2537
+ j: i + 1,
2538
+ legs: square(p[i], p[i + 1], obstacles, 0, axis, i + 1 === p.length - 1 ? last : void 0) ?? [p[i + 1]]
2539
+ };
2540
+ out.push(...step.legs);
2541
+ const end = out[out.length - 1];
2542
+ const before = out[out.length - 2];
2543
+ axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : void 0;
2544
+ i = step.j;
2545
+ }
2546
+ return out;
2547
+ }
2548
+ /**
2549
+ * This function gives one or two legs parallel to the axes from `a` to `b`, clear of the obstacles by `pad`, or `undefined`.
2550
+ * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
2551
+ */
2552
+ function square(a, b, obstacles, pad, from, to) {
2553
+ if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : void 0;
2554
+ const options = [{
2555
+ c: {
2556
+ x: b.x,
2557
+ y: a.y
2558
+ },
2559
+ bends: (from === "y" ? 1 : 0) + (to === "x" ? 1 : 0)
2560
+ }, {
2561
+ c: {
2562
+ x: a.x,
2563
+ y: b.y
2564
+ },
2565
+ bends: (from === "x" ? 1 : 0) + (to === "y" ? 1 : 0)
2566
+ }];
2567
+ options.sort((u, v) => u.bends - v.bends);
2568
+ for (const { c } of options) if (obstacles.clear(a, c, pad) && obstacles.clear(c, b, pad)) return [c, b];
2569
+ }
2570
+ // src/routers/sections.ts
2571
+ const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
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
+ /**
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.
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.
2580
+ * Each way from hub to hub is found one time, so all the edges on it share its points.
2581
+ * The edges in one section of the lowest level go straight around the nodes.
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
+ *
2585
+ * The result gives one zoom level for each depth of the tree.
2586
+ * A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
2587
+ */
2588
+ function sectionsRouter(options) {
2589
+ const router = {
2590
+ name: options.name,
2591
+ route(graph) {
2592
+ const stretch = options.stretch ?? 2;
2593
+ const byId = nodeIndex(graph);
2594
+ const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
2595
+ const ways = new WayFinder(graph.nodes, options);
2596
+ const corners = ways.corners;
2597
+ const root = buildSections(graph.nodes, options);
2598
+ const parent = /* @__PURE__ */ new Map();
2599
+ const leafOf = [];
2600
+ for (const s of allSections(root)) {
2601
+ for (const c of s.children) parent.set(c, s);
2602
+ if (s.children.length === 0) for (const i of s.nodes) leafOf[i] = s;
2603
+ }
2604
+ const hubs = /* @__PURE__ */ new Map();
2605
+ const hub = (s) => {
2606
+ let h = hubs.get(s);
2607
+ if (!h) {
2608
+ let x = 0;
2609
+ let y = 0;
2610
+ for (const i of s.nodes) {
2611
+ const c = center(graph.nodes[i].box);
2612
+ x += c.x;
2613
+ y += c.y;
2614
+ }
2615
+ const c = {
2616
+ x: x / Math.max(1, s.nodes.length),
2617
+ y: y / Math.max(1, s.nodes.length)
2618
+ };
2619
+ const j = corners.nearest(c);
2620
+ h = j >= 0 ? corners.points[j] : c;
2621
+ hubs.set(s, h);
2622
+ }
2623
+ return h;
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
+ };
2638
+ const up = (s) => {
2639
+ const out = [];
2640
+ for (let x = s; x; x = parent.get(x)) out.push(x);
2641
+ return out;
2642
+ };
2643
+ const pieces = /* @__PURE__ */ new Map();
2644
+ const users = /* @__PURE__ */ new Map();
2645
+ const piece = (ka, a, kb, b, used) => {
2646
+ const forward = ka <= kb;
2647
+ const key = forward ? `${ka}>${kb}` : `${kb}>${ka}`;
2648
+ let p = pieces.get(key);
2649
+ if (!p) {
2650
+ const [from, to] = forward ? [a, b] : [b, a];
2651
+ const way = corners.path(from, to);
2652
+ p = dedupe$1(way ? corners.pull(way) : [from, to]);
2653
+ pieces.set(key, p);
2654
+ }
2655
+ if (!used.includes(key)) used.push(key);
2656
+ return forward ? p : [...p].reverse();
2657
+ };
2658
+ const at = (p) => `${p.x},${p.y}`;
2659
+ const routes = [];
2660
+ for (const e of graph.edges) {
2661
+ const s = byId.get(e.source.node);
2662
+ const t = byId.get(e.target.node);
2663
+ const iu = indexOfNode.get(e.source.node);
2664
+ const iv = indexOfNode.get(e.target.node);
2665
+ if (!s || !t || iu === void 0 || iv === void 0) continue;
2666
+ const ps = placePlug(s, e.source, center(t.box));
2667
+ const pt = placePlug(t, e.target, center(s.box));
2668
+ const lu = leafOf[iu];
2669
+ const lv = leafOf[iv];
2670
+ if (lu === lv) {
2671
+ routes.push({
2672
+ edge: e.id,
2673
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
2674
+ });
2675
+ continue;
2676
+ }
2677
+ const a = up(lu);
2678
+ const b = up(lv);
2679
+ const common = new Set(b);
2680
+ const lca = a.find((x) => common.has(x));
2681
+ const keep = options.hubs ?? Infinity;
2682
+ const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
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
+ }
2693
+ const out = [ps.point];
2694
+ const append = (p) => {
2695
+ for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
2696
+ };
2697
+ const used = [];
2698
+ append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
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));
2700
+ append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
2701
+ append([pt.point]);
2702
+ if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin || goesOutAndBack(out)) {
2703
+ routes.push({
2704
+ edge: e.id,
2705
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
2706
+ });
2707
+ continue;
2708
+ }
2709
+ for (const key of used) {
2710
+ const list = users.get(key);
2711
+ if (list) list.push(e.id);
2712
+ else users.set(key, [e.id]);
2713
+ }
2714
+ routes.push({
2715
+ edge: e.id,
2716
+ points: out
2717
+ });
2718
+ }
2719
+ const routeOf = new Map(routes.map((r) => [r.edge, r]));
2720
+ const runs = [];
2721
+ for (const [key, p] of pieces) {
2722
+ const list = users.get(key) ?? [];
2723
+ if (list.length < 2 || p.length < 2) continue;
2724
+ runs.push({
2725
+ id: `r${runs.length}`,
2726
+ key,
2727
+ points: p,
2728
+ edges: orderLanes(p, list.map((id) => routeOf.get(id)))
2729
+ });
2730
+ }
2731
+ return {
2732
+ routes,
2733
+ runs,
2734
+ levels: makeLevels(graph, root, routeOf, options)
2735
+ };
2736
+ }
2737
+ };
2738
+ return options.maxNodes === void 0 ? router : {
2739
+ ...router,
2740
+ maxNodes: options.maxNodes
1643
2741
  };
1644
2742
  }
1645
2743
  /**
1646
2744
  * This function gives one zoom level for each depth of the tree, and a last level with each node alone.
1647
2745
  * At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
1648
- * Its points are the part of the route of its first edge between the two items.
2746
+ * In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
2747
+ * In the last level, the points of a link are the route of its first edge.
1649
2748
  */
1650
- function makeLevels(graph, root, routeOf, marks) {
2749
+ function makeLevels(graph, root, routeOf, options) {
1651
2750
  const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
1652
2751
  const levels = [];
1653
2752
  const deepest = treeDepth(root);
@@ -1709,21 +2808,30 @@ function makeLevels(graph, root, routeOf, marks) {
1709
2808
  link.count++;
1710
2809
  continue;
1711
2810
  }
1712
- const route = routeOf.get(e.id)?.points ?? [];
1713
- const mark = marks.get(e.id);
1714
- const from = typeof a === "number" ? 0 : mark?.get(a) ?? 0;
1715
- const to = typeof b === "number" ? route.length - 1 : mark?.get(b) ?? route.length - 1;
1716
2811
  links.set(key, {
1717
2812
  source: ida,
1718
2813
  target: idb,
1719
2814
  count: 1,
1720
- points: route.slice(from, to + 1)
2815
+ edge: e.id
1721
2816
  });
1722
2817
  }
1723
- levels.push({
2818
+ const pairs = [...links.values()].map(({ source, target, count }) => ({
2819
+ source,
2820
+ target,
2821
+ count
2822
+ }));
2823
+ if (d > deepest) levels.push({
1724
2824
  depth: d,
1725
2825
  items,
1726
- links: [...links.values()]
2826
+ links: [...links.values()].map(({ edge, ...l }) => ({
2827
+ ...l,
2828
+ points: routeOf.get(edge)?.points ?? []
2829
+ }))
2830
+ });
2831
+ else levels.push({
2832
+ depth: d,
2833
+ items,
2834
+ links: levelLinks(items, pairs, options)
1727
2835
  });
1728
2836
  }
1729
2837
  return levels;
@@ -2156,134 +3264,6 @@ const ROUTERS = [
2156
3264
  maxNodes: 1e5
2157
3265
  })
2158
3266
  ];
2159
- // src/bench/properties.ts
2160
- /** The tolerance of the hard rules, in units of the plane. */
2161
- const TOLERANCE = .5;
2162
- /**
2163
- * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
2164
- * 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.
2165
- *
2166
- * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
2167
- * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
2168
- * The function gives at most `limit` violations.
2169
- */
2170
- function checkProperties(graph, result, limit = 100) {
2171
- const out = [];
2172
- const add = (v) => {
2173
- out.push(v);
2174
- return out.length >= limit;
2175
- };
2176
- const byId = nodeIndex(graph);
2177
- const grid = new Grid(180);
2178
- for (const n of graph.nodes) grid.add(n, n.box);
2179
- const routes = /* @__PURE__ */ new Map();
2180
- for (const r of result.routes) routes.set(r.edge, r);
2181
- for (const e of graph.edges) {
2182
- const r = routes.get(e.id);
2183
- const s = byId.get(e.source.node);
2184
- const t = byId.get(e.target.node);
2185
- if (!s || !t) continue;
2186
- if (!r || r.points.length < 2) {
2187
- if (add({
2188
- rule: "complete",
2189
- edge: e.id,
2190
- detail: "no route"
2191
- })) return out;
2192
- continue;
2193
- }
2194
- const a = r.points[0];
2195
- const b = r.points[r.points.length - 1];
2196
- if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
2197
- if (add({
2198
- rule: "ends-at-plugs",
2199
- edge: e.id,
2200
- detail: "an end of the route is away from its node"
2201
- })) return out;
2202
- }
2203
- const fs = fixedPortPoint(s, e.source);
2204
- const ft = fixedPortPoint(t, e.target);
2205
- if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
2206
- if (add({
2207
- rule: "fixed-plugs",
2208
- edge: e.id,
2209
- detail: "the route does not start or end at its fixed plug"
2210
- })) return out;
2211
- }
2212
- for (let i = 1; i < r.points.length; i++) {
2213
- const p = r.points[i - 1];
2214
- const q = r.points[i];
2215
- let hit;
2216
- grid.querySegment(p, q, 0, (n) => {
2217
- if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
2218
- });
2219
- if (hit) {
2220
- if (add({
2221
- rule: "no-pass-through",
2222
- edge: e.id,
2223
- detail: `segment ${i} goes through ${hit.id}`
2224
- })) return out;
2225
- break;
2226
- }
2227
- }
2228
- }
2229
- const bundled = /* @__PURE__ */ new Set();
2230
- for (const b of result.bundles ?? []) for (const id of b.edges) {
2231
- const r = routes.get(id);
2232
- const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
2233
- if (bundled.has(id) || !shares) {
2234
- if (add({
2235
- rule: "bundle-trunk",
2236
- edge: id,
2237
- detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
2238
- })) return out;
2239
- }
2240
- bundled.add(id);
2241
- }
2242
- for (const run of result.runs ?? []) {
2243
- if (run.edges.length < 2) {
2244
- if (add({
2245
- rule: "run-shared",
2246
- detail: `run ${run.id} has fewer than two edges`
2247
- })) return out;
2248
- }
2249
- for (const id of run.edges) {
2250
- const r = routes.get(id);
2251
- if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
2252
- if (add({
2253
- rule: "run-shared",
2254
- edge: id,
2255
- detail: `run ${run.id}: the route does not hold the points of the run in a row`
2256
- })) return out;
2257
- }
2258
- }
2259
- }
2260
- for (const level of result.levels ?? []) {
2261
- const itemOf = /* @__PURE__ */ new Map();
2262
- for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
2263
- if (itemOf.size !== graph.nodes.length) {
2264
- if (add({
2265
- rule: "counts-sum",
2266
- detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
2267
- })) return out;
2268
- }
2269
- let between = 0;
2270
- for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
2271
- const sum = level.links.reduce((s, l) => s + l.count, 0);
2272
- if (sum !== between) {
2273
- if (add({
2274
- rule: "counts-sum",
2275
- detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
2276
- })) return out;
2277
- }
2278
- }
2279
- return out;
2280
- }
2281
- /** This function tells if the polyline `p` holds the points of `part` in a row. */
2282
- function holds(p, part) {
2283
- if (part.length === 0) return true;
2284
- for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
2285
- return false;
2286
- }
2287
3267
  // src/bench/quality.ts
2288
3268
  /** The clearance stops at this distance. */
2289
3269
  const CLEARANCE_CAP = 40;
@@ -2526,7 +3506,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
2526
3506
  deterministic,
2527
3507
  violations: all.length,
2528
3508
  sample: all.slice(0, 5),
2529
- quality: measureQuality(graph, result)
3509
+ quality: measureQuality(graph, result),
3510
+ faults: measureRouteFaults(graph, result),
3511
+ levelFaults: measureLevelFaults(result)
2530
3512
  };
2531
3513
  }
2532
3514
  /** This function writes the rows as a Markdown table. */
@@ -2543,6 +3525,33 @@ function formatTable(rows) {
2543
3525
  })
2544
3526
  ].join("\n");
2545
3527
  }
2546
- 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 };
2547
3556
 
2548
3557
  //# sourceMappingURL=index.js.map