@mark1russell7/highways 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -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,151 @@ 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
+ /**
706
+ * This function makes the far view of a board after a host layout packs it.
707
+ * The board has 96 boxes of one size in a grid of 12 columns and 8 rows.
708
+ * The boxes touch, with no gap between them. Each box links to a box near it, and 48 long links join boxes far apart.
709
+ * The links have no channel between the boxes. The roadmap item ROOM measures this board before and after `makeRoom`.
710
+ *
711
+ * The board is not in `CASES`, because the digests of the router fix the output on each board of `CASES`.
712
+ */
713
+ function packedGridBoard() {
714
+ const R = rng(11);
715
+ const b = new Board();
716
+ const cols = 12;
717
+ const rows = 8;
718
+ const w = 140;
719
+ const h = 90;
720
+ const id = (r, c) => `p${r}-${c}`;
721
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * w, r * h, w, h);
722
+ const step = () => Math.floor(R() * 5) - 2;
723
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
724
+ const rr = Math.max(0, Math.min(7, r + step()));
725
+ const cc = Math.max(0, Math.min(11, c + step()));
726
+ b.link(id(r, c), id(rr, cc));
727
+ }
728
+ for (let n = 0; n < 48; n++) b.link(id(Math.floor(R() * rows), Math.floor(R() * cols)), id(Math.floor(R() * rows), Math.floor(R() * cols)));
729
+ return b.graph();
730
+ }
731
+ /** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
732
+ const CASES = [
733
+ {
734
+ name: "case-dense-groups",
735
+ defect: "Groups overlap at the far view, and level links go through other groups.",
736
+ make: denseGroupsBoard
737
+ },
738
+ {
739
+ name: "case-hub-chain",
740
+ defect: "A route climbs a chain of hubs away from its target and comes back.",
741
+ make: hubChainBoard
742
+ },
743
+ {
744
+ name: "case-many-groups",
745
+ defect: "On a level with many groups, links go around the outer edge of the board.",
746
+ make: manyGroupsBoard
747
+ }
748
+ ];
549
749
  // src/plugs.ts
550
750
  /** The outward normal of each side, in a frame with the y axis down. */
551
751
  const SIDE_NORMAL = {
@@ -714,6 +914,582 @@ function roundCorners(p, radius, steps = 4) {
714
914
  out.push(p[p.length - 1]);
715
915
  return out;
716
916
  }
917
+ // src/curves/model.ts
918
+ /** The curve types, in the order of the benchmark. */
919
+ const CURVE_TYPES = [
920
+ "straight",
921
+ "rounded",
922
+ "spline",
923
+ "arc"
924
+ ];
925
+ /** This function gives the points of a polyline without the points that are nearer than `eps` to the point before them. */
926
+ function distinctPoints(points, eps = 1e-9) {
927
+ const out = [];
928
+ for (const p of points) {
929
+ const q = out[out.length - 1];
930
+ if (q === void 0 || Math.hypot(p.x - q.x, p.y - q.y) > eps) out.push(p);
931
+ }
932
+ return out;
933
+ }
934
+ // src/bench/properties.ts
935
+ /** The tolerance of the hard rules, in units of the plane. */
936
+ const TOLERANCE = .5;
937
+ /**
938
+ * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
939
+ * 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.
940
+ * Each link of a level starts on the box of its source and ends on the box of its target.
941
+ *
942
+ * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
943
+ * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
944
+ * The function gives at most `limit` violations.
945
+ */
946
+ function checkProperties(graph, result, limit = 100) {
947
+ const out = [];
948
+ const add = (v) => {
949
+ out.push(v);
950
+ return out.length >= limit;
951
+ };
952
+ const byId = nodeIndex(graph);
953
+ const grid = new Grid(180);
954
+ for (const n of graph.nodes) grid.add(n, n.box);
955
+ const routes = /* @__PURE__ */ new Map();
956
+ for (const r of result.routes) routes.set(r.edge, r);
957
+ for (const e of graph.edges) {
958
+ const r = routes.get(e.id);
959
+ const s = byId.get(e.source.node);
960
+ const t = byId.get(e.target.node);
961
+ if (!s || !t) continue;
962
+ if (!r || r.points.length < 2) {
963
+ if (add({
964
+ rule: "complete",
965
+ edge: e.id,
966
+ detail: "no route"
967
+ })) return out;
968
+ continue;
969
+ }
970
+ const a = r.points[0];
971
+ const b = r.points[r.points.length - 1];
972
+ if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
973
+ if (add({
974
+ rule: "ends-at-plugs",
975
+ edge: e.id,
976
+ detail: "an end of the route is away from its node"
977
+ })) return out;
978
+ }
979
+ const fs = fixedPortPoint(s, e.source);
980
+ const ft = fixedPortPoint(t, e.target);
981
+ if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
982
+ if (add({
983
+ rule: "fixed-plugs",
984
+ edge: e.id,
985
+ detail: "the route does not start or end at its fixed plug"
986
+ })) return out;
987
+ }
988
+ for (let i = 1; i < r.points.length; i++) {
989
+ const p = r.points[i - 1];
990
+ const q = r.points[i];
991
+ let hit;
992
+ grid.querySegment(p, q, 0, (n) => {
993
+ if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
994
+ });
995
+ if (hit) {
996
+ if (add({
997
+ rule: "no-pass-through",
998
+ edge: e.id,
999
+ detail: `segment ${i} goes through ${hit.id}`
1000
+ })) return out;
1001
+ break;
1002
+ }
1003
+ }
1004
+ }
1005
+ const bundled = /* @__PURE__ */ new Set();
1006
+ for (const b of result.bundles ?? []) for (const id of b.edges) {
1007
+ const r = routes.get(id);
1008
+ const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
1009
+ if (bundled.has(id) || !shares) {
1010
+ if (add({
1011
+ rule: "bundle-trunk",
1012
+ edge: id,
1013
+ detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
1014
+ })) return out;
1015
+ }
1016
+ bundled.add(id);
1017
+ }
1018
+ for (const run of result.runs ?? []) {
1019
+ if (run.edges.length < 2) {
1020
+ if (add({
1021
+ rule: "run-shared",
1022
+ detail: `run ${run.id} has fewer than two edges`
1023
+ })) return out;
1024
+ }
1025
+ for (const id of run.edges) {
1026
+ const r = routes.get(id);
1027
+ if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
1028
+ if (add({
1029
+ rule: "run-shared",
1030
+ edge: id,
1031
+ detail: `run ${run.id}: the route does not hold the points of the run in a row`
1032
+ })) return out;
1033
+ }
1034
+ }
1035
+ }
1036
+ for (const level of result.levels ?? []) {
1037
+ const itemOf = /* @__PURE__ */ new Map();
1038
+ for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
1039
+ if (itemOf.size !== graph.nodes.length) {
1040
+ if (add({
1041
+ rule: "counts-sum",
1042
+ detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
1043
+ })) return out;
1044
+ }
1045
+ let between = 0;
1046
+ for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
1047
+ const sum = level.links.reduce((s, l) => s + l.count, 0);
1048
+ if (sum !== between) {
1049
+ if (add({
1050
+ rule: "counts-sum",
1051
+ detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
1052
+ })) return out;
1053
+ }
1054
+ const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
1055
+ for (const l of level.links) {
1056
+ const s = boxOf.get(l.source);
1057
+ const t = boxOf.get(l.target);
1058
+ const p = l.points;
1059
+ if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
1060
+ if (add({
1061
+ rule: "link-ends",
1062
+ detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
1063
+ })) return out;
1064
+ }
1065
+ }
1066
+ }
1067
+ return out;
1068
+ }
1069
+ /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
1070
+ function onBoundary(p, b) {
1071
+ const inner = inflate(b, -.5);
1072
+ 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;
1073
+ }
1074
+ /** This function tells if the polyline `p` holds the points of `part` in a row. */
1075
+ function holds(p, part) {
1076
+ if (part.length === 0) return true;
1077
+ for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
1078
+ return false;
1079
+ }
1080
+ // src/curves/sample.ts
1081
+ /** The largest number of points that `sampleCurve` gives for one segment. */
1082
+ const MAX_STEPS = 1024;
1083
+ /**
1084
+ * This function gives a polyline that follows the curve. The distance from each part of the curve to the polyline is `tolerance` or less.
1085
+ * The polyline starts at the point of the `move` and goes through the end of each segment.
1086
+ * Hit tests, measures and renderers that draw only lines use this polyline.
1087
+ */
1088
+ function sampleCurve(segments, tolerance = .5) {
1089
+ const tol = Math.max(1e-6, tolerance);
1090
+ const out = [];
1091
+ let at;
1092
+ for (const s of segments) {
1093
+ if (s.kind === "move" || at === void 0) {
1094
+ out.push(s.to);
1095
+ at = s.to;
1096
+ continue;
1097
+ }
1098
+ if (s.kind === "cubic") {
1099
+ const d = Math.max(Math.hypot(at.x - 2 * s.c1.x + s.c2.x, at.y - 2 * s.c1.y + s.c2.y), Math.hypot(s.c1.x - 2 * s.c2.x + s.to.x, s.c1.y - 2 * s.c2.y + s.to.y));
1100
+ const n = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.sqrt(3 * d / (4 * tol)))));
1101
+ for (let k = 1; k < n; k++) out.push(cubicAt(at, s.c1, s.c2, s.to, k / n));
1102
+ } else if (s.kind === "arc") {
1103
+ const step = s.radius <= tol ? Math.PI : 2 * Math.acos(1 - tol / s.radius);
1104
+ const n = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.abs(s.sweep) / step)));
1105
+ for (let k = 1; k < n; k++) {
1106
+ const a = s.start + s.sweep * k / n;
1107
+ out.push({
1108
+ x: s.center.x + s.radius * Math.cos(a),
1109
+ y: s.center.y + s.radius * Math.sin(a)
1110
+ });
1111
+ }
1112
+ }
1113
+ out.push(s.to);
1114
+ at = s.to;
1115
+ }
1116
+ return out;
1117
+ }
1118
+ /** This function gives the point at the parameter `t` of the cubic Bezier curve with the control points `p0`, `p1`, `p2` and `p3`. */
1119
+ function cubicAt(p0, p1, p2, p3, t) {
1120
+ const u = 1 - t;
1121
+ const a = u * u * u;
1122
+ const b = 3 * u * u * t;
1123
+ const c = 3 * u * t * t;
1124
+ const d = t * t * t;
1125
+ return {
1126
+ x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
1127
+ y: a * p0.y + b * p1.y + c * p2.y + d * p3.y
1128
+ };
1129
+ }
1130
+ /**
1131
+ * This function writes the segments as the data of an SVG path: the commands `M`, `L`, `C` and `A`.
1132
+ * Each number has at most `digits` digits after the point.
1133
+ */
1134
+ function toSvgPath(segments, digits = 2) {
1135
+ const n = (v) => {
1136
+ const s = String(Number(v.toFixed(digits)));
1137
+ return s === "-0" ? "0" : s;
1138
+ };
1139
+ const p = (q) => `${n(q.x)} ${n(q.y)}`;
1140
+ const parts = [];
1141
+ for (const s of segments) if (s.kind === "move") parts.push(`M ${p(s.to)}`);
1142
+ else if (s.kind === "line") parts.push(`L ${p(s.to)}`);
1143
+ else if (s.kind === "cubic") parts.push(`C ${p(s.c1)} ${p(s.c2)} ${p(s.to)}`);
1144
+ else parts.push(`A ${n(s.radius)} ${n(s.radius)} 0 ${Math.abs(s.sweep) > Math.PI ? 1 : 0} ${s.sweep > 0 ? 1 : 0} ${p(s.to)}`);
1145
+ return parts.join(" ");
1146
+ }
1147
+ // src/curves/space.ts
1148
+ /**
1149
+ * This function makes the free space of a board from its boxes. The boxes are the nodes, or the items of a zoom level.
1150
+ * The curves sample each piece within `tolerance` of the curve for the test.
1151
+ */
1152
+ function curveSpace(boxes, tolerance = .25) {
1153
+ const inner = boxes.map((b) => inflate(b, -.5));
1154
+ let size = 0;
1155
+ for (const b of boxes) size += Math.max(b.w, b.h);
1156
+ const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.length)));
1157
+ inner.forEach((b, i) => grid.add(i, b));
1158
+ const hits = (p) => {
1159
+ const out = /* @__PURE__ */ new Set();
1160
+ for (let i = 1; i < p.length; i++) {
1161
+ const a = p[i - 1];
1162
+ const b = p[i];
1163
+ grid.querySegment(a, b, 0, (id) => {
1164
+ if (!out.has(id) && segmentHitsBox(a, b, inner[id], .5)) out.add(id);
1165
+ });
1166
+ }
1167
+ return out;
1168
+ };
1169
+ return {
1170
+ boxes,
1171
+ tolerance,
1172
+ addsHit(q, p) {
1173
+ let before;
1174
+ for (let i = 1; i < q.length; i++) {
1175
+ const a = q[i - 1];
1176
+ const b = q[i];
1177
+ if (grid.someSegment(a, b, 0, (id) => {
1178
+ if (!segmentHitsBox(a, b, inner[id], .5)) return false;
1179
+ before ??= hits(p);
1180
+ return !before.has(id);
1181
+ })) return true;
1182
+ }
1183
+ return false;
1184
+ }
1185
+ };
1186
+ }
1187
+ /**
1188
+ * This function finds the largest size from `max` down to 0 that keeps a piece of a curve clear.
1189
+ * A clear piece goes through no box that its polyline avoids.
1190
+ *
1191
+ * The function `piece` gives the segments of the piece at a size. They start with a `move`.
1192
+ * The polyline `chord` is the part of the original polyline that the piece replaces.
1193
+ * The function tries `max` first, then it halves the interval `steps` times. The result is 0 when no size of the search is clear.
1194
+ */
1195
+ function fitSize(space, max, chord, piece, steps = 8) {
1196
+ if (space === void 0 || max <= 0) return Math.max(0, max);
1197
+ const clear = (size) => !space.addsHit(sampleCurve(piece(size), space.tolerance), chord(size));
1198
+ if (clear(max)) return max;
1199
+ let lo = 0;
1200
+ let hi = max;
1201
+ for (let i = 0; i < steps; i++) {
1202
+ const mid = (lo + hi) / 2;
1203
+ if (clear(mid)) lo = mid;
1204
+ else hi = mid;
1205
+ }
1206
+ return lo;
1207
+ }
1208
+ // src/curves/arc/index.ts
1209
+ /** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle stays sharp. */
1210
+ const FLAT = 1e-6;
1211
+ /**
1212
+ * This function puts a circular arc at each inner vertex of a polyline. The arc is tangent to the two legs of its corner.
1213
+ * The tangent points stay on the first half of each leg, so the ends of the polyline stay.
1214
+ * With `space`, the function makes the radius of a corner smaller until the arc goes through no box that the polyline avoids.
1215
+ * On a polyline with only horizontal and vertical legs, each arc is a quarter circle.
1216
+ */
1217
+ function arcCurve(points, options = {}) {
1218
+ const p = distinctPoints(points);
1219
+ if (p.length === 0) return [];
1220
+ const radius = options.radius ?? 12;
1221
+ const out = [{
1222
+ kind: "move",
1223
+ to: p[0]
1224
+ }];
1225
+ for (let i = 1; i + 1 < p.length; i++) {
1226
+ const a = p[i - 1];
1227
+ const v = p[i];
1228
+ const b = p[i + 1];
1229
+ const la = dist(a, v);
1230
+ const lb = dist(v, b);
1231
+ const ux = (v.x - a.x) / la;
1232
+ const uy = (v.y - a.y) / la;
1233
+ const wx = (b.x - v.x) / lb;
1234
+ const wy = (b.y - v.y) / lb;
1235
+ const turn = Math.atan2(ux * wy - uy * wx, ux * wx + uy * wy);
1236
+ if (Math.abs(turn) < FLAT || Math.PI - Math.abs(turn) < FLAT) {
1237
+ out.push({
1238
+ kind: "line",
1239
+ to: v
1240
+ });
1241
+ continue;
1242
+ }
1243
+ const k = Math.tan(Math.abs(turn) / 2);
1244
+ const side = Math.sign(turn);
1245
+ const ends = (r) => [{
1246
+ x: v.x - ux * r * k,
1247
+ y: v.y - uy * r * k
1248
+ }, {
1249
+ x: v.x + wx * r * k,
1250
+ y: v.y + wy * r * k
1251
+ }];
1252
+ const piece = (r) => {
1253
+ const [p1, p2] = ends(r);
1254
+ const center = {
1255
+ x: p1.x - side * uy * r,
1256
+ y: p1.y + side * ux * r
1257
+ };
1258
+ return [{
1259
+ kind: "arc",
1260
+ center,
1261
+ radius: r,
1262
+ start: Math.atan2(p1.y - center.y, p1.x - center.x),
1263
+ sweep: turn,
1264
+ to: p2
1265
+ }];
1266
+ };
1267
+ const r = fitSize(options.space, Math.min(radius, Math.min(la, lb) / (2 * k)), (s) => [
1268
+ ends(s)[0],
1269
+ v,
1270
+ ends(s)[1]
1271
+ ], (s) => [{
1272
+ kind: "move",
1273
+ to: ends(s)[0]
1274
+ }, ...piece(s)]);
1275
+ if (r <= 0) {
1276
+ out.push({
1277
+ kind: "line",
1278
+ to: v
1279
+ });
1280
+ continue;
1281
+ }
1282
+ const [p1] = ends(r);
1283
+ if (dist(p1, out[out.length - 1].to) > 1e-9) out.push({
1284
+ kind: "line",
1285
+ to: p1
1286
+ });
1287
+ out.push(...piece(r));
1288
+ }
1289
+ if (p.length > 1) out.push({
1290
+ kind: "line",
1291
+ to: p[p.length - 1]
1292
+ });
1293
+ return out;
1294
+ }
1295
+ // src/curves/rounded/index.ts
1296
+ /**
1297
+ * This function rounds each inner vertex of a polyline with a quadratic Bezier curve, as `roundCorners` does.
1298
+ * The curve of a corner starts and ends at most `radius` from the vertex, and at most half of each leg. The ends of the polyline stay.
1299
+ * The function gives each quadratic curve as the same cubic curve. With `space`, it makes the radius of a corner smaller until the curve
1300
+ * goes through no box that the polyline avoids.
1301
+ */
1302
+ function roundedCurve(points, options = {}) {
1303
+ const p = distinctPoints(points);
1304
+ if (p.length === 0) return [];
1305
+ const radius = options.radius ?? 12;
1306
+ const out = [{
1307
+ kind: "move",
1308
+ to: p[0]
1309
+ }];
1310
+ for (let i = 1; i + 1 < p.length; i++) {
1311
+ const a = p[i - 1];
1312
+ const v = p[i];
1313
+ const b = p[i + 1];
1314
+ const la = dist(a, v);
1315
+ const lb = dist(v, b);
1316
+ const ends = (r) => [{
1317
+ x: v.x + (a.x - v.x) * r / la,
1318
+ y: v.y + (a.y - v.y) * r / la
1319
+ }, {
1320
+ x: v.x + (b.x - v.x) * r / lb,
1321
+ y: v.y + (b.y - v.y) * r / lb
1322
+ }];
1323
+ const piece = (r) => {
1324
+ const [p1, p2] = ends(r);
1325
+ return [{
1326
+ kind: "cubic",
1327
+ c1: {
1328
+ x: p1.x + 2 * (v.x - p1.x) / 3,
1329
+ y: p1.y + 2 * (v.y - p1.y) / 3
1330
+ },
1331
+ c2: {
1332
+ x: p2.x + 2 * (v.x - p2.x) / 3,
1333
+ y: p2.y + 2 * (v.y - p2.y) / 3
1334
+ },
1335
+ to: p2
1336
+ }];
1337
+ };
1338
+ const r = fitSize(options.space, Math.min(radius, la / 2, lb / 2), (s) => [
1339
+ ends(s)[0],
1340
+ v,
1341
+ ends(s)[1]
1342
+ ], (s) => [{
1343
+ kind: "move",
1344
+ to: ends(s)[0]
1345
+ }, ...piece(s)]);
1346
+ if (r <= 0) {
1347
+ out.push({
1348
+ kind: "line",
1349
+ to: v
1350
+ });
1351
+ continue;
1352
+ }
1353
+ const [p1] = ends(r);
1354
+ if (dist(p1, at(out)) > 1e-9) out.push({
1355
+ kind: "line",
1356
+ to: p1
1357
+ });
1358
+ out.push(...piece(r));
1359
+ }
1360
+ if (p.length > 1) out.push({
1361
+ kind: "line",
1362
+ to: p[p.length - 1]
1363
+ });
1364
+ return out;
1365
+ }
1366
+ /** This function gives the end of the last segment. */
1367
+ function at(segments) {
1368
+ return segments[segments.length - 1].to;
1369
+ }
1370
+ // src/curves/spline/index.ts
1371
+ /**
1372
+ * This function draws a centripetal Catmull-Rom spline through the points of a polyline (Yuksel, Schaefer and Keyser, 2011).
1373
+ * Each piece between two points is one cubic Bezier curve. The centripetal knots keep the curve free of cusps and loops in a piece.
1374
+ * The curve goes through each point. Its tangent at a point keeps one direction on the two sides of the point.
1375
+ *
1376
+ * With `space`, the function makes the tangents of a piece shorter until the piece goes through no box that its leg avoids.
1377
+ * A piece with no tangent is the straight leg.
1378
+ */
1379
+ function splineCurve(points, options = {}) {
1380
+ const p = distinctPoints(points);
1381
+ if (p.length === 0) return [];
1382
+ const out = [{
1383
+ kind: "move",
1384
+ to: p[0]
1385
+ }];
1386
+ const n = p.length;
1387
+ if (n === 1) return out;
1388
+ const scale = 1 - Math.max(0, Math.min(1, options.tension ?? 0));
1389
+ const dt = Array.from({ length: n - 1 }, (_, i) => Math.sqrt(dist(p[i], p[i + 1])));
1390
+ const unit = (q) => {
1391
+ const l = Math.hypot(q.x, q.y);
1392
+ return l === 0 ? {
1393
+ x: 0,
1394
+ y: 0
1395
+ } : {
1396
+ x: q.x / l,
1397
+ y: q.y / l
1398
+ };
1399
+ };
1400
+ const m = p.map((v, i) => {
1401
+ if (i === 0) {
1402
+ const leg = {
1403
+ x: p[1].x - v.x,
1404
+ y: p[1].y - v.y
1405
+ };
1406
+ if (!options.startNormal) return {
1407
+ x: leg.x / dt[0],
1408
+ y: leg.y / dt[0]
1409
+ };
1410
+ const u = unit(options.startNormal);
1411
+ return {
1412
+ x: u.x * dt[0],
1413
+ y: u.y * dt[0]
1414
+ };
1415
+ }
1416
+ if (i === n - 1) {
1417
+ const a = p[i - 1];
1418
+ const leg = {
1419
+ x: v.x - a.x,
1420
+ y: v.y - a.y
1421
+ };
1422
+ if (!options.endNormal) return {
1423
+ x: leg.x / dt[i - 1],
1424
+ y: leg.y / dt[i - 1]
1425
+ };
1426
+ const u = unit(options.endNormal);
1427
+ return {
1428
+ x: -u.x * dt[i - 1],
1429
+ y: -u.y * dt[i - 1]
1430
+ };
1431
+ }
1432
+ const a = p[i - 1];
1433
+ const b = p[i + 1];
1434
+ const t0 = dt[i - 1];
1435
+ const t1 = dt[i];
1436
+ return {
1437
+ x: (v.x - a.x) / t0 - (b.x - a.x) / (t0 + t1) + (b.x - v.x) / t1,
1438
+ y: (v.y - a.y) / t0 - (b.y - a.y) / (t0 + t1) + (b.y - v.y) / t1
1439
+ };
1440
+ });
1441
+ for (let i = 0; i + 1 < n; i++) {
1442
+ const a = p[i];
1443
+ const b = p[i + 1];
1444
+ const h = dt[i] / 3;
1445
+ const piece = (s) => [{
1446
+ kind: "move",
1447
+ to: a
1448
+ }, {
1449
+ kind: "cubic",
1450
+ c1: {
1451
+ x: a.x + m[i].x * h * s,
1452
+ y: a.y + m[i].y * h * s
1453
+ },
1454
+ c2: {
1455
+ x: b.x - m[i + 1].x * h * s,
1456
+ y: b.y - m[i + 1].y * h * s
1457
+ },
1458
+ to: b
1459
+ }];
1460
+ const s = fitSize(options.space, scale, () => [a, b], piece);
1461
+ out.push(s > 0 ? piece(s)[1] : {
1462
+ kind: "line",
1463
+ to: b
1464
+ });
1465
+ }
1466
+ return out;
1467
+ }
1468
+ // src/curves/straight/index.ts
1469
+ /** This function gives the polyline as it is: one `move` to the first point and one `line` to each other point. */
1470
+ function straightCurve(points) {
1471
+ return points.map((to, i) => i === 0 ? {
1472
+ kind: "move",
1473
+ to
1474
+ } : {
1475
+ kind: "line",
1476
+ to
1477
+ });
1478
+ }
1479
+ // src/curves/index.ts
1480
+ /**
1481
+ * This function gives the curve of a route: a polyline of a router, a lane of `laneRoutes` or a link of a zoom level.
1482
+ * The curve starts at the first point and ends at the last point of the polyline.
1483
+ * The output is a list of segments for any renderer. `toSvgPath` writes it as SVG path data, and `sampleCurve` gives points.
1484
+ */
1485
+ function curveOf(points, options) {
1486
+ switch (options.type) {
1487
+ case "straight": return straightCurve(points);
1488
+ case "rounded": return roundedCurve(points, options);
1489
+ case "spline": return splineCurve(points, options);
1490
+ case "arc": return arcCurve(points, options);
1491
+ }
1492
+ }
717
1493
  // src/visibility.ts
718
1494
  /** The nodes as obstacles, with a grid for fast tests. */
719
1495
  var Obstacles = class {
@@ -725,20 +1501,30 @@ var Obstacles = class {
725
1501
  }
726
1502
  /** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
727
1503
  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
- }
1504
+ return this.hit(a, b, pad, skip) === void 0;
1505
+ }
1506
+ /**
1507
+ * This method gives a node whose box, made larger by `pad`, the segment `ab` goes into, or `undefined` when the segment is clear.
1508
+ * The nodes in `skip` do not count. The test stops at the first such node.
1509
+ */
1510
+ hit(a, b, pad, skip) {
1511
+ let found;
1512
+ this.#grid.someSegment(a, b, pad, (n) => {
1513
+ if (skip?.has(n) || !hits(a, b, n, pad)) return false;
1514
+ found = n;
1515
+ return true;
734
1516
  });
735
- return ok;
1517
+ return found;
736
1518
  }
737
1519
  /** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
738
1520
  clearPath(p, pad, skip) {
739
1521
  for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
740
1522
  return true;
741
1523
  }
1524
+ /** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
1525
+ query(box, visit) {
1526
+ this.#grid.query(box, visit);
1527
+ }
742
1528
  /** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
743
1529
  inside(p, pad, skip) {
744
1530
  let hit = false;
@@ -756,6 +1542,11 @@ var Obstacles = class {
756
1542
  return hit;
757
1543
  }
758
1544
  };
1545
+ /** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
1546
+ function hits(a, b, n, pad) {
1547
+ const x = n.box;
1548
+ return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
1549
+ }
759
1550
  /**
760
1551
  * A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
761
1552
  * Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
@@ -776,9 +1567,12 @@ var CornerGraph = class {
776
1567
  #open = /* @__PURE__ */ new Int32Array(0);
777
1568
  #closed = /* @__PURE__ */ new Int32Array(0);
778
1569
  #stamp = 0;
779
- #tested = /* @__PURE__ */ new Int32Array(0);
780
- #testStamp = 0;
1570
+ #buckets = /* @__PURE__ */ new Map();
1571
+ #dist = /* @__PURE__ */ new Float64Array(0);
1572
+ #near = new MinHeap();
781
1573
  #endCache = /* @__PURE__ */ new Map();
1574
+ #fewCache = /* @__PURE__ */ new Map();
1575
+ #part;
782
1576
  /** This constructor makes the graph of the nodes. */
783
1577
  constructor(nodes, obstacles, options) {
784
1578
  this.#obstacles = obstacles;
@@ -830,7 +1624,7 @@ var CornerGraph = class {
830
1624
  }
831
1625
  }
832
1626
  const links = this.points.map(() => /* @__PURE__ */ new Set());
833
- for (let i = 0; i < this.points.length; i++) for (const j of this.nearestVisible(this.points[i], this.#k, i)) {
1627
+ for (let i = 0; i < this.points.length; i++) for (const j of this.#nearestVisible(this.points[i], this.#k, i, this.#pad, options.cones ?? false)) {
834
1628
  links[i].add(j);
835
1629
  links[j].add(i);
836
1630
  }
@@ -841,12 +1635,17 @@ var CornerGraph = class {
841
1635
  });
842
1636
  }
843
1637
  #addPoint(c) {
844
- this.#pointGrid.add(this.points.length, {
1638
+ const j = this.points.length;
1639
+ this.#pointGrid.add(j, {
845
1640
  x: c.x,
846
1641
  y: c.y,
847
1642
  w: 0,
848
1643
  h: 0
849
1644
  });
1645
+ const key = bucketKey(Math.floor(c.x / this.#cell), Math.floor(c.y / this.#cell));
1646
+ const list = this.#buckets.get(key);
1647
+ if (list === void 0) this.#buckets.set(key, [j]);
1648
+ else list.push(j);
850
1649
  this.points.push(c);
851
1650
  }
852
1651
  /** This method gives the number of links of the graph. */
@@ -880,28 +1679,80 @@ var CornerGraph = class {
880
1679
  nearestVisible(p, k, self = -1) {
881
1680
  return this.#nearestVisible(p, k, self, this.#pad);
882
1681
  }
883
- #nearestVisible(p, k, self, pad) {
1682
+ /**
1683
+ * This method tests the corners in rings of radius `r` = 1, 2, 4 and 8 cells. In each ring, it tests the corners
1684
+ * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
1685
+ * The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
1686
+ * A corner of these buckets beyond `r` waits for the next ring. So the method visits each bucket one time.
1687
+ * With `cones`, the method also gives the nearest visible corner of each of four cones around the axes that the `k` nearest corners leave empty.
1688
+ */
1689
+ #nearestVisible(p, k, self, pad, cones = false) {
884
1690
  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;
1691
+ let full = 0;
1692
+ if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
1693
+ const dd = this.#dist;
1694
+ const points = this.points;
1695
+ const cell = this.#cell;
1696
+ const reach = CONE_CELLS * cell;
1697
+ const blockers = [];
1698
+ const now = this.#near;
1699
+ let wait = [];
1700
+ let pi0 = 1;
1701
+ let pi1 = 0;
1702
+ let pj0 = 1;
1703
+ let pj1 = 0;
1704
+ for (let r = cell; r <= cell * 8; r *= 2) {
1705
+ if (out.length >= k && r / 2 >= reach) return out;
1706
+ const x0 = p.x - r;
1707
+ const y0 = p.y - r;
1708
+ const i0 = Math.floor(x0 / cell);
1709
+ const j0 = Math.floor(y0 / cell);
1710
+ const i1 = Math.floor((x0 + 2 * r) / cell);
1711
+ const j1 = Math.floor((y0 + 2 * r) / cell);
1712
+ const later = [];
1713
+ now.clear();
1714
+ for (const j of wait) if (dd[j] > r) later.push(j);
1715
+ else now.add(dd[j], j);
1716
+ for (let bi = i0; bi <= i1; bi++) {
1717
+ const before = bi >= pi0 && bi <= pi1;
1718
+ for (let bj = j0; bj <= j1; bj++) {
1719
+ if (before && bj >= pj0 && bj <= pj1) continue;
1720
+ const list = this.#buckets.get(bucketKey(bi, bj));
1721
+ if (list === void 0) continue;
1722
+ for (const j of list) {
1723
+ if (j === self) continue;
1724
+ const d = dist(p, points[j]);
1725
+ dd[j] = d;
1726
+ if (d > r) later.push(j);
1727
+ else now.add(d, j);
1728
+ }
1729
+ }
904
1730
  }
1731
+ now.build();
1732
+ while (now.size > 0) {
1733
+ const j = now.pop();
1734
+ const q = points[j];
1735
+ const cone = cones ? coneOf(q.x - p.x, q.y - p.y) : 0;
1736
+ if (out.length >= k) {
1737
+ if (dd[j] > reach) return out;
1738
+ if ((full & cone) !== 0) continue;
1739
+ }
1740
+ let blocked = false;
1741
+ for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
1742
+ if (blocked) continue;
1743
+ const hit = this.#obstacles.hit(p, q, pad);
1744
+ if (hit !== void 0) blockers.push(hit);
1745
+ else {
1746
+ out.push(j);
1747
+ full |= cone;
1748
+ if (out.length >= k && (!cones || full === 15)) return out;
1749
+ }
1750
+ }
1751
+ wait = later;
1752
+ pi0 = i0;
1753
+ pi1 = i1;
1754
+ pj0 = j0;
1755
+ pj1 = j1;
905
1756
  }
906
1757
  return out;
907
1758
  }
@@ -913,6 +1764,35 @@ var CornerGraph = class {
913
1764
  */
914
1765
  path(from, to, cost, floor = 1) {
915
1766
  if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
1767
+ return this.#search([{
1768
+ point: from,
1769
+ cost: 0
1770
+ }], [{
1771
+ point: to,
1772
+ cost: 0
1773
+ }], cost, floor, this.#k * 2)?.points;
1774
+ }
1775
+ /**
1776
+ * This method gives the shortest path from a point of `from` to a point of `to`, or `undefined` when the graph has none.
1777
+ * 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.
1778
+ */
1779
+ pathAmong(from, to) {
1780
+ const found = this.#search(from, to, void 0, 1, this.#k);
1781
+ const lines = [];
1782
+ from.forEach((a, i) => to.forEach((b, k) => {
1783
+ const c = a.cost + dist(a.point, b.point) + b.cost;
1784
+ if (!found || c < found.cost) lines.push({
1785
+ points: [a.point, b.point],
1786
+ from: i,
1787
+ to: k,
1788
+ cost: c
1789
+ });
1790
+ }));
1791
+ lines.sort((u, v) => u.cost - v.cost);
1792
+ return lines.find((l) => this.#obstacles.clear(l.points[0], l.points[1], this.#pad)) ?? found;
1793
+ }
1794
+ /** This method is the search (A*) of `path` and `pathAmong`. */
1795
+ #search(from, to, cost, floor, count) {
916
1796
  const n = this.points.length;
917
1797
  if (this.#g.length < n) {
918
1798
  this.#g = new Float64Array(n);
@@ -921,28 +1801,51 @@ var CornerGraph = class {
921
1801
  this.#closed = new Int32Array(n);
922
1802
  }
923
1803
  const stamp = ++this.#stamp;
1804
+ const points = this.points;
924
1805
  const goal = /* @__PURE__ */ new Map();
925
- for (const j of this.#ends(to)) goal.set(j, dist(this.points[j], to));
1806
+ const goalOf = /* @__PURE__ */ new Map();
1807
+ to.forEach((b, k) => {
1808
+ for (const j of this.#ends(b.point, count)) {
1809
+ const tail = dist(points[j], b.point) + b.cost;
1810
+ const now = goal.get(j);
1811
+ if (now === void 0 || tail < now) {
1812
+ goal.set(j, tail);
1813
+ goalOf.set(j, k);
1814
+ }
1815
+ }
1816
+ });
926
1817
  if (goal.size === 0) return void 0;
1818
+ const starts = from.map((a) => this.#ends(a.point, count));
1819
+ if (!starts.some((list) => this.#joined(list, goal))) return void 0;
927
1820
  const heap = new MinHeap();
928
- const h = (i) => floor * dist(this.points[i], to);
1821
+ const G = this.#g;
1822
+ const parentOf = this.#parent;
1823
+ const open = this.#open;
1824
+ const closed = this.#closed;
1825
+ const estimate = (p) => {
1826
+ let h = Infinity;
1827
+ for (const b of to) h = Math.min(h, dist(p, b.point));
1828
+ return floor * h;
1829
+ };
929
1830
  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);
1831
+ if (closed[j] === stamp) return;
1832
+ if (open[j] === stamp && G[j] <= g) return;
1833
+ open[j] = stamp;
1834
+ G[j] = g;
1835
+ parentOf[j] = parent;
1836
+ heap.push(g + estimate(points[j]), j);
936
1837
  };
937
- for (const j of this.#ends(from)) relax(j, dist(from, this.points[j]), -1);
1838
+ from.forEach((a, i) => {
1839
+ for (const j of starts[i]) relax(j, a.cost + dist(a.point, points[j]), -1 - i);
1840
+ });
938
1841
  let best = Infinity;
939
1842
  let bestEnd = -1;
940
1843
  while (heap.size > 0) {
941
1844
  if (heap.topKey >= best) break;
942
1845
  const i = heap.pop();
943
- if (this.#closed[i] === stamp) continue;
944
- this.#closed[i] = stamp;
945
- const gi = this.#g[i];
1846
+ if (closed[i] === stamp) continue;
1847
+ closed[i] = stamp;
1848
+ const gi = G[i];
946
1849
  const tail = goal.get(i);
947
1850
  if (tail !== void 0 && gi + tail < best) {
948
1851
  best = gi + tail;
@@ -952,28 +1855,67 @@ var CornerGraph = class {
952
1855
  const len = this.#len[i];
953
1856
  for (let m = 0; m < adj.length; m++) {
954
1857
  const j = adj[m];
1858
+ if (closed[j] === stamp) continue;
955
1859
  const d = len[m];
956
1860
  relax(j, gi + (cost ? cost(i, j, d) : d), i);
957
1861
  }
958
1862
  }
959
1863
  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();
1864
+ const k = goalOf.get(bestEnd);
1865
+ const out = [to[k].point];
1866
+ for (let i = bestEnd;; i = parentOf[i]) {
1867
+ out.push(points[i]);
1868
+ if (parentOf[i] < 0) {
1869
+ const f = -1 - parentOf[i];
1870
+ out.push(from[f].point);
1871
+ return {
1872
+ points: out.reverse(),
1873
+ from: f,
1874
+ to: k,
1875
+ cost: best
1876
+ };
1877
+ }
1878
+ }
1879
+ }
1880
+ /** This method tells if a corner of `starts` and a corner of `goal` are in one connected part of the graph. */
1881
+ #joined(starts, goal) {
1882
+ const part = this.#part ??= this.#parts();
1883
+ const goals = /* @__PURE__ */ new Set();
1884
+ for (const j of goal.keys()) goals.add(part[j]);
1885
+ for (const j of starts) if (goals.has(part[j])) return true;
1886
+ return false;
1887
+ }
1888
+ /** This method gives the number of the connected part of each corner. */
1889
+ #parts() {
1890
+ const part = new Int32Array(this.points.length).fill(-1);
1891
+ const stack = [];
1892
+ let count = 0;
1893
+ for (let i = 0; i < part.length; i++) {
1894
+ if (part[i] !== -1) continue;
1895
+ part[i] = count;
1896
+ stack.push(i);
1897
+ while (stack.length > 0) for (const j of this.#adj[stack.pop()]) {
1898
+ if (part[j] !== -1) continue;
1899
+ part[j] = count;
1900
+ stack.push(j);
1901
+ }
1902
+ count++;
1903
+ }
1904
+ return part;
964
1905
  }
965
1906
  /**
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.
1907
+ * 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
1908
  * Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
968
1909
  * The graph does not change, so the method keeps the corners of each end.
969
1910
  */
970
- #ends(p) {
1911
+ #ends(p, count) {
1912
+ const cache = count === this.#k * 2 ? this.#endCache : this.#fewCache;
971
1913
  const key = `${p.x},${p.y}`;
972
- let out = this.#endCache.get(key);
1914
+ let out = cache.get(key);
973
1915
  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);
1916
+ out = this.nearestVisible(p, count);
1917
+ if (out.length === 0) out = this.#nearestVisible(p, count, -1, 0);
1918
+ cache.set(key, out);
977
1919
  }
978
1920
  return out;
979
1921
  }
@@ -991,61 +1933,89 @@ var CornerGraph = class {
991
1933
  return out;
992
1934
  }
993
1935
  };
994
- /** A binary min-heap of (key, value) pairs. Equal keys come out by the smaller value, so the order is deterministic. */
1936
+ /** A link of a cone is at most this number of cells long. A longer search costs time on a dense level and finds few new links. */
1937
+ const CONE_CELLS = 3;
1938
+ /** This function gives the bit of the cone of the offset (`dx`, `dy`): 1 is +x, 2 is +y, 4 is -x and 8 is -y. A diagonal offset is in a cone of the x axis. */
1939
+ const coneOf = (dx, dy) => Math.abs(dx) >= Math.abs(dy) ? dx > 0 ? 1 : 4 : dy > 0 ? 2 : 8;
1940
+ /** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
1941
+ const bucketKey = (i, j) => (i + 1048576) * 2097152 + (j + 1048576);
1942
+ /**
1943
+ * 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.
1944
+ * 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.
1945
+ */
995
1946
  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
- }
1947
+ #d = /* @__PURE__ */ new Float64Array(64);
1948
+ #j = /* @__PURE__ */ new Int32Array(64);
1949
+ size = 0;
1012
1950
  /** The smallest key. */
1013
1951
  get topKey() {
1014
- return this.#k[0];
1952
+ return this.#d[0];
1015
1953
  }
1016
1954
  push(key, value) {
1017
- this.#k.push(key);
1018
- this.#v.push(value);
1019
- let i = this.#k.length - 1;
1955
+ this.add(key, value);
1956
+ const d = this.#d;
1957
+ const jj = this.#j;
1958
+ let i = this.size - 1;
1020
1959
  while (i > 0) {
1021
1960
  const p = i - 1 >> 1;
1022
- if (!this.#less(i, p)) break;
1023
- this.#swap(i, p);
1961
+ if (!(key < d[p] || key === d[p] && value < jj[p])) break;
1962
+ d[i] = d[p];
1963
+ jj[i] = jj[p];
1024
1964
  i = p;
1025
1965
  }
1966
+ d[i] = key;
1967
+ jj[i] = value;
1968
+ }
1969
+ clear() {
1970
+ this.size = 0;
1971
+ }
1972
+ add(d, j) {
1973
+ if (this.size === this.#d.length) {
1974
+ const dd = new Float64Array(2 * this.size);
1975
+ dd.set(this.#d);
1976
+ this.#d = dd;
1977
+ const jj = new Int32Array(2 * this.size);
1978
+ jj.set(this.#j);
1979
+ this.#j = jj;
1980
+ }
1981
+ this.#d[this.size] = d;
1982
+ this.#j[this.size] = j;
1983
+ this.size++;
1984
+ }
1985
+ build() {
1986
+ for (let i = (this.size >> 1) - 1; i >= 0; i--) this.#down(i);
1026
1987
  }
1027
1988
  /** This method removes the pair with the smallest key and gives its value. */
1028
1989
  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
- }
1990
+ const top = this.#j[0];
1991
+ this.size--;
1992
+ if (this.size > 0) {
1993
+ this.#d[0] = this.#d[this.size];
1994
+ this.#j[0] = this.#j[this.size];
1995
+ this.#down(0);
1046
1996
  }
1047
1997
  return top;
1048
1998
  }
1999
+ #down(i) {
2000
+ const d = this.#d;
2001
+ const jj = this.#j;
2002
+ const n = this.size;
2003
+ const di = d[i];
2004
+ const ji = jj[i];
2005
+ for (;;) {
2006
+ const l = 2 * i + 1;
2007
+ if (l >= n) break;
2008
+ const r = l + 1;
2009
+ let m = l;
2010
+ if (r < n && (d[r] < d[l] || d[r] === d[l] && jj[r] < jj[l])) m = r;
2011
+ if (!(d[m] < di || d[m] === di && jj[m] < ji)) break;
2012
+ d[i] = d[m];
2013
+ jj[i] = jj[m];
2014
+ i = m;
2015
+ }
2016
+ d[i] = di;
2017
+ jj[i] = ji;
2018
+ }
1049
2019
  };
1050
2020
  // src/ways.ts
1051
2021
  /**
@@ -1511,7 +2481,150 @@ const plugsRouter = {
1511
2481
  return { routes };
1512
2482
  }
1513
2483
  };
2484
+ // src/bench/links.ts
2485
+ /** A link goes out and back when its distance to its target grows by more than this share of the distance between its ends. */
2486
+ const BACKTRACK_SHARE = .25;
2487
+ /** A link goes out and back only when its distance to its target grows by more than this number of units. */
2488
+ const BACKTRACK_MIN = 40;
2489
+ /** The default `slack` of `LinkFaultOptions`. */
2490
+ const OUTSIDE_SLACK = 24;
2491
+ /**
2492
+ * This function gives the largest growth of the distance to the last point of a polyline.
2493
+ * The growth at a vertex is its distance to the last point, less the nearest approach of the polyline before that vertex.
2494
+ */
2495
+ function backtrackOf(p) {
2496
+ if (p.length < 3) return 0;
2497
+ const end = p[p.length - 1];
2498
+ let near = dist(p[0], end);
2499
+ let worst = 0;
2500
+ for (let i = 1; i < p.length; i++) {
2501
+ worst = Math.max(worst, dist(p[i], end) - near);
2502
+ near = Math.min(near, pointSegmentDistance(end, p[i - 1], p[i]));
2503
+ }
2504
+ return worst;
2505
+ }
2506
+ /** 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. */
2507
+ function goesOutAndBack(p) {
2508
+ if (p.length < 3) return false;
2509
+ const span = dist(p[0], p[p.length - 1]);
2510
+ return backtrackOf(p) > Math.max(40, BACKTRACK_SHARE * span);
2511
+ }
2512
+ /**
2513
+ * This function measures the faults of links between boxes. The map `boxes` gives the box of each end.
2514
+ * The bounding box of `outside` is the bounding box of all the boxes of the map.
2515
+ */
2516
+ function measureLinks(boxes, links, options = {}) {
2517
+ const slack = options.slack ?? 24;
2518
+ let size = 0;
2519
+ let x0 = Infinity;
2520
+ let y0 = Infinity;
2521
+ let x1 = -Infinity;
2522
+ let y1 = -Infinity;
2523
+ for (const b of boxes.values()) {
2524
+ size += Math.max(b.w, b.h);
2525
+ x0 = Math.min(x0, b.x);
2526
+ y0 = Math.min(y0, b.y);
2527
+ x1 = Math.max(x1, b.x + b.w);
2528
+ y1 = Math.max(y1, b.y + b.h);
2529
+ }
2530
+ const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.size)));
2531
+ for (const [id, b] of boxes) grid.add(id, b);
2532
+ let through = 0;
2533
+ let backtrack = 0;
2534
+ let worstDetour = 0;
2535
+ let outside = 0;
2536
+ for (const l of links) {
2537
+ const p = l.points;
2538
+ if (p.length === 0) continue;
2539
+ let hit = false;
2540
+ for (let i = 1; i < p.length && !hit; i++) {
2541
+ const a = p[i - 1];
2542
+ const b = p[i];
2543
+ grid.querySegment(a, b, 0, (id) => {
2544
+ if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id), -.5), .5)) hit = true;
2545
+ });
2546
+ }
2547
+ if (hit) through++;
2548
+ if (goesOutAndBack(p)) backtrack++;
2549
+ const span = dist(p[0], p[p.length - 1]);
2550
+ if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
2551
+ if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
2552
+ }
2553
+ return {
2554
+ links: links.length,
2555
+ through,
2556
+ backtrack,
2557
+ worstDetour,
2558
+ outside
2559
+ };
2560
+ }
2561
+ /** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
2562
+ function measureRouteFaults(graph, result, options = {}) {
2563
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
2564
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
2565
+ const links = [];
2566
+ for (const r of result.routes) {
2567
+ const e = edges.get(r.edge);
2568
+ if (e) links.push({
2569
+ source: e.source.node,
2570
+ target: e.target.node,
2571
+ points: r.points
2572
+ });
2573
+ }
2574
+ return measureLinks(boxes, links, options);
2575
+ }
2576
+ /** This function counts the pairs of boxes that overlap by more than `TOLERANCE` on each axis. */
2577
+ function countOverlaps(boxes) {
2578
+ let size = 0;
2579
+ for (const b of boxes) size += Math.max(b.w, b.h);
2580
+ const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.length)));
2581
+ boxes.forEach((b, i) => grid.add(i, b));
2582
+ let n = 0;
2583
+ boxes.forEach((a, i) => {
2584
+ grid.query(a, (j) => {
2585
+ const b = boxes[j];
2586
+ 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++;
2587
+ });
2588
+ });
2589
+ return n;
2590
+ }
2591
+ /** This function measures the faults of the links of one zoom level. The items of the level are the boxes. */
2592
+ function measureLevel(level, options = {}) {
2593
+ const boxes = new Map(level.items.map((i) => [i.id, i.box]));
2594
+ return {
2595
+ depth: level.depth,
2596
+ items: level.items.length,
2597
+ groups: level.items.filter((i) => i.nodes.length > 1).length,
2598
+ overlaps: countOverlaps(level.items.map((i) => i.box)),
2599
+ ...measureLinks(boxes, level.links, options)
2600
+ };
2601
+ }
2602
+ /**
2603
+ * This function measures the faults of each zoom level of a result that has groups: the levels of the far view.
2604
+ * A level of single nodes only is the near view. Its links are routes, and `measureRouteFaults` measures them.
2605
+ */
2606
+ function measureLevelFaults(result, options = {}) {
2607
+ return (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1)).map((l) => measureLevel(l, options));
2608
+ }
2609
+ /** This function adds the faults of several levels: it sums the counts and keeps the largest `worstDetour`. */
2610
+ function sumFaults(list) {
2611
+ return list.reduce((s, f) => ({
2612
+ links: s.links + f.links,
2613
+ through: s.through + f.through,
2614
+ backtrack: s.backtrack + f.backtrack,
2615
+ worstDetour: Math.max(s.worstDetour, f.worstDetour),
2616
+ outside: s.outside + f.outside
2617
+ }), {
2618
+ links: 0,
2619
+ through: 0,
2620
+ backtrack: 0,
2621
+ worstDetour: 0,
2622
+ outside: 0
2623
+ });
2624
+ }
1514
2625
  // src/levels.ts
2626
+ /** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
2627
+ const TIGHT_MARGIN = 1;
1515
2628
  const EPS = 1e-6;
1516
2629
  /**
1517
2630
  * This function removes the repeated points, the points in the middle of a straight run,
@@ -1554,7 +2667,7 @@ function levelLinks(items, pairs, options = {}) {
1554
2667
  if (!s || !t) continue;
1555
2668
  const [fs, ft] = facingPlugs(s.box, t.box, inset);
1556
2669
  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))));
2670
+ points ??= sideWay(ways, s, t, inset);
1558
2671
  out.push({
1559
2672
  source: p.source,
1560
2673
  target: p.target,
@@ -1565,25 +2678,52 @@ function levelLinks(items, pairs, options = {}) {
1565
2678
  return out;
1566
2679
  }
1567
2680
  /**
2681
+ * This function gives a way from plug to plug between the plugs at the middle of a side of the two nodes.
2682
+ * Without a way in the corner graph, the link goes straight between the ends, with legs parallel to the axes where they are clear.
2683
+ * When that line goes through another box, the link goes between the free plugs of the two nodes (`freeWay`) instead.
2684
+ */
2685
+ function sideWay(ways, s, t, inset) {
2686
+ const e = ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box)));
2687
+ const quick = quickWay(ways, e);
2688
+ if (quick) return quick;
2689
+ const path = ways.corners.path(e.a, e.b);
2690
+ const way = pulled(ways, e.ps, path ?? [e.a, e.b], e.pt);
2691
+ if (path || ways.obstacles.clearPath(way, 0, /* @__PURE__ */ new Set([s, t]))) return way;
2692
+ return freeWay(ways, s, t, inset) ?? way;
2693
+ }
2694
+ /**
1568
2695
  * This function makes the obstacles and the corner graph of the items of a level.
1569
2696
  * The buckets of the obstacles grow with the items, so a long link visits few buckets.
2697
+ * The corner graphs use `cones`, so a link goes along the corridors between the groups, not around the outer edge of the board.
1570
2698
  */
1571
2699
  function levelWays(nodes, options) {
1572
2700
  const margin = options.margin ?? 5;
2701
+ const k = options.k ?? 8;
1573
2702
  let size = 0;
1574
2703
  for (const n of nodes) size += Math.max(n.box.w, n.box.h);
1575
2704
  const obstacles = new Obstacles(nodes, Math.max(180, 2 * size / Math.max(1, nodes.length)));
2705
+ const corners = new CornerGraph(nodes, obstacles, {
2706
+ margin,
2707
+ k,
2708
+ cones: true
2709
+ });
2710
+ const out = (p) => ({
2711
+ x: p.point.x + p.normal.x * margin,
2712
+ y: p.point.y + p.normal.y * margin
2713
+ });
2714
+ let tight;
2715
+ const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
2716
+ margin: Math.min(margin, TIGHT_MARGIN),
2717
+ k,
2718
+ cones: true
2719
+ });
1576
2720
  return {
1577
2721
  obstacles,
1578
- corners: new CornerGraph(nodes, obstacles, {
1579
- margin,
1580
- k: options.k ?? 8
1581
- }),
2722
+ corners,
1582
2723
  margin,
1583
- out: (p) => ({
1584
- x: p.point.x + p.normal.x * margin,
1585
- y: p.point.y + p.normal.y * margin
1586
- })
2724
+ out,
2725
+ tight: tightGraph,
2726
+ free: /* @__PURE__ */ new Map()
1587
2727
  };
1588
2728
  }
1589
2729
  /**
@@ -1657,13 +2797,139 @@ function sidePlug(ways, node, toward) {
1657
2797
  return plug(order[0]);
1658
2798
  }
1659
2799
  /**
2800
+ * This function gives the free plugs of a node: on each side, the free point nearest to the middle of the side.
2801
+ * 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.
2802
+ */
2803
+ function freePlugs(ways, node, inset) {
2804
+ const known = ways.free.get(node);
2805
+ if (known) return known;
2806
+ const b = node.box;
2807
+ const out = [];
2808
+ for (const side of SIDES) {
2809
+ const n = SIDE_NORMAL[side];
2810
+ const across = n.x === 0;
2811
+ const lo = across ? b.x : b.y;
2812
+ const hi = across ? b.x + b.w : b.y + b.h;
2813
+ const at = across ? n.y > 0 ? b.y + b.h : b.y : n.x > 0 ? b.x + b.w : b.x;
2814
+ const far = at + (across ? n.y : n.x) * ways.margin;
2815
+ const covered = [];
2816
+ const strip = across ? {
2817
+ x: lo,
2818
+ y: Math.min(at, far),
2819
+ w: hi - lo,
2820
+ h: Math.abs(far - at)
2821
+ } : {
2822
+ x: Math.min(at, far),
2823
+ y: lo,
2824
+ w: Math.abs(far - at),
2825
+ h: hi - lo
2826
+ };
2827
+ ways.obstacles.query(strip, (m) => {
2828
+ if (m === node) return;
2829
+ const o = m.box;
2830
+ const [o0, o1, p0, p1] = across ? [
2831
+ o.x,
2832
+ o.x + o.w,
2833
+ o.y,
2834
+ o.y + o.h
2835
+ ] : [
2836
+ o.y,
2837
+ o.y + o.h,
2838
+ o.x,
2839
+ o.x + o.w
2840
+ ];
2841
+ if (at > p0 && at < p1 || far > p0 && far < p1) covered.push([o0, o1]);
2842
+ });
2843
+ const mid = (lo + hi) / 2;
2844
+ let best;
2845
+ for (const [f0, f1] of freeSpans(lo, hi, covered)) {
2846
+ const room = Math.min(inset, (f1 - f0) / 2);
2847
+ const u = Math.max(f0 + room, Math.min(f1 - room, mid));
2848
+ if (best === void 0 || Math.abs(u - mid) < Math.abs(best - mid)) best = u;
2849
+ }
2850
+ if (best !== void 0) out.push({
2851
+ point: across ? {
2852
+ x: best,
2853
+ y: at
2854
+ } : {
2855
+ x: at,
2856
+ y: best
2857
+ },
2858
+ normal: n
2859
+ });
2860
+ }
2861
+ const free = {
2862
+ plugs: out,
2863
+ reach: out.map((p) => reach(ways, node, p))
2864
+ };
2865
+ ways.free.set(node, free);
2866
+ return free;
2867
+ }
2868
+ /** This function gives the indices of the plugs, and the plugs that face `toward` most come first. */
2869
+ function facingOrder(plugs, from, toward) {
2870
+ const facing = (p) => p.normal.x * (toward.x - from.x) + p.normal.y * (toward.y - from.y);
2871
+ return plugs.map((_, i) => i).sort((u, v) => facing(plugs[v]) - facing(plugs[u]) || u - v);
2872
+ }
2873
+ /** 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. */
2874
+ function freeSpans(lo, hi, covered) {
2875
+ covered.sort((u, v) => u[0] - v[0]);
2876
+ const out = [];
2877
+ let x = lo;
2878
+ for (const [c0, c1] of covered) {
2879
+ if (c0 > x) out.push([x, Math.min(c0, hi)]);
2880
+ x = Math.max(x, c1);
2881
+ if (x >= hi) break;
2882
+ }
2883
+ if (x < hi) out.push([x, hi]);
2884
+ return out.filter(([a, b]) => b - a > EPS);
2885
+ }
2886
+ /**
2887
+ * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
2888
+ * 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.
2889
+ */
2890
+ function freeWay(ways, s, t, inset) {
2891
+ const fs = freePlugs(ways, s, inset);
2892
+ const ft = freePlugs(ways, t, inset);
2893
+ const cs = center(s.box);
2894
+ const ct = center(t.box);
2895
+ const is = facingOrder(fs.plugs, cs, ct);
2896
+ const it = facingOrder(ft.plugs, ct, cs);
2897
+ const from = is.map((i) => fs.plugs[i]);
2898
+ const to = it.map((k) => ft.plugs[k]);
2899
+ const ra = is.map((i) => fs.reach[i]);
2900
+ const rb = it.map((k) => ft.reach[k]);
2901
+ let best;
2902
+ let bestLength = Infinity;
2903
+ from.forEach((ps, i) => to.forEach((pt, k) => {
2904
+ const way = quickWay(ways, endsAt(ps, ra[i], pt, rb[k]));
2905
+ const length = way ? polylineLength(way) : Infinity;
2906
+ if (length < bestLength) {
2907
+ best = way;
2908
+ bestLength = length;
2909
+ }
2910
+ }));
2911
+ if (best) return best;
2912
+ const a = from.map((ps, i) => ({
2913
+ point: along(ps, ra[i]),
2914
+ cost: ra[i]
2915
+ }));
2916
+ const b = to.map((pt, k) => ({
2917
+ point: along(pt, rb[k]),
2918
+ cost: rb[k]
2919
+ }));
2920
+ const found = ways.tight().pathAmong(a, b);
2921
+ return found && pulled(ways, from[found.from], found.points, to[found.to]);
2922
+ }
2923
+ /**
1660
2924
  * This function gives the ends of a link. The link leaves each plug along its normal for the margin.
1661
2925
  * 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
2926
  * Two plugs that face each other across a narrow space make the link turn in the middle of that space.
1663
2927
  */
1664
2928
  function ends(ways, s, ps, t, pt) {
1665
- let ra = reach(ways, s, ps);
1666
- let rb = reach(ways, t, pt);
2929
+ return endsAt(ps, reach(ways, s, ps), pt, reach(ways, t, pt));
2930
+ }
2931
+ /** This function gives the ends of a link from the plug `ps` to the plug `pt`, with the reach of each plug. */
2932
+ function endsAt(ps, ra, pt, rb) {
1667
2933
  if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
1668
2934
  const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
1669
2935
  if (d > 0 && ra + rb > d) ra = rb = d / 2;
@@ -1709,17 +2975,15 @@ function quickWay(ways, e) {
1709
2975
  ]);
1710
2976
  }
1711
2977
  /**
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.
1714
- */
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));
2978
+ * This function gives a way from the plug `ps` through the points of `path` to the plug `pt`.
2979
+ * 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.
2980
+ */
2981
+ function pulled(ways, ps, path, pt) {
2982
+ const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
1719
2983
  return tidy([
1720
- e.ps.point,
2984
+ ps.point,
1721
2985
  ...legs,
1722
- e.pt.point
2986
+ pt.point
1723
2987
  ]);
1724
2988
  }
1725
2989
  const axisOf = (n) => Math.abs(n.x) > Math.abs(n.y) ? "x" : "y";
@@ -1779,11 +3043,18 @@ function square(a, b, obstacles, pad, from, to) {
1779
3043
  // src/routers/sections.ts
1780
3044
  const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
1781
3045
  /**
3046
+ * A hub can be this many units farther from the target than the nearest approach of the way before it.
3047
+ * A hub farther away goes out and back, so the way skips it.
3048
+ */
3049
+ const HUB_SLACK = 80;
3050
+ /**
1782
3051
  * 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
3052
  * 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
3053
  * Each way from hub to hub is found one time, so all the edges on it share its points.
1785
3054
  * The edges in one section of the lowest level go straight around the nodes.
1786
3055
  *
3056
+ * 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.
3057
+ *
1787
3058
  * The result gives one zoom level for each depth of the tree.
1788
3059
  * A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
1789
3060
  */
@@ -1824,6 +3095,19 @@ function sectionsRouter(options) {
1824
3095
  }
1825
3096
  return h;
1826
3097
  };
3098
+ const forward = (list, from, to) => {
3099
+ const kept = [];
3100
+ let near = dist(from, to);
3101
+ let last = from;
3102
+ for (const c of list) {
3103
+ const h = hub(c);
3104
+ if (dist(h, to) > near + HUB_SLACK) continue;
3105
+ near = Math.min(near, pointSegmentDistance(to, last, h));
3106
+ last = h;
3107
+ kept.push(c);
3108
+ }
3109
+ return kept;
3110
+ };
1827
3111
  const up = (s) => {
1828
3112
  const out = [];
1829
3113
  for (let x = s; x; x = parent.get(x)) out.push(x);
@@ -1869,19 +3153,26 @@ function sectionsRouter(options) {
1869
3153
  const lca = a.find((x) => common.has(x));
1870
3154
  const keep = options.hubs ?? Infinity;
1871
3155
  const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
1872
- const chain = [...side(a), ...side(b).reverse()];
3156
+ const end = ways.out(pt);
3157
+ const start = ways.out(ps);
3158
+ const chain = forward([...side(a), ...side(b).reverse()], start, pt.point);
3159
+ if (chain.length === 0) {
3160
+ routes.push({
3161
+ edge: e.id,
3162
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
3163
+ });
3164
+ continue;
3165
+ }
1873
3166
  const out = [ps.point];
1874
3167
  const append = (p) => {
1875
3168
  for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
1876
3169
  };
1877
3170
  const used = [];
1878
- const start = ways.out(ps);
1879
3171
  append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
1880
3172
  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
3173
  append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
1883
3174
  append([pt.point]);
1884
- if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
3175
+ if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin || goesOutAndBack(out)) {
1885
3176
  routes.push({
1886
3177
  edge: e.id,
1887
3178
  points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
@@ -2446,152 +3737,287 @@ const ROUTERS = [
2446
3737
  maxNodes: 1e5
2447
3738
  })
2448
3739
  ];
2449
- // src/bench/properties.ts
2450
- /** The tolerance of the hard rules, in units of the plane. */
2451
- const TOLERANCE = .5;
3740
+ // src/room/index.ts
3741
+ const DEFAULT_GUTTER = 8;
3742
+ const DEFAULT_SPACING = 4;
3743
+ const DEFAULT_SNAP = .5;
3744
+ const lo = (b, a) => a === "x" ? b.x : b.y;
3745
+ const len = (b, a) => a === "x" ? b.w : b.h;
3746
+ const other = (a) => a === "x" ? "y" : "x";
2452
3747
  /**
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.
3748
+ * This function gives the gaps of a board. A sweep over each axis sorts the left (or top) edges of the boxes.
3749
+ * Each distinct edge after the first is a gap. The `lanes` of each gap are 0, and its `need` is its current width.
2460
3750
  */
2461
- function checkProperties(graph, result, limit = 100) {
2462
- const out = [];
2463
- const add = (v) => {
2464
- out.push(v);
2465
- return out.length >= limit;
3751
+ function boardGaps(items, snap = DEFAULT_SNAP) {
3752
+ return [...axisGaps(items, "x", snap), ...axisGaps(items, "y", snap)];
3753
+ }
3754
+ function axisGaps(items, axis, snap) {
3755
+ const starts = [...new Set(items.map((i) => lo(i.box, axis)))].sort((u, v) => u - v);
3756
+ const bounds = [];
3757
+ for (const s of starts) if (bounds.length === 0 || s - bounds[bounds.length - 1] > snap) bounds.push(s);
3758
+ const after = bounds.map(() => []);
3759
+ for (const i of items) after[lastAtOrBefore(bounds, lo(i.box, axis) + snap)].push(i.id);
3760
+ const ends = items.map((i) => ({
3761
+ id: i.id,
3762
+ end: lo(i.box, axis) + len(i.box, axis)
3763
+ })).sort((u, v) => u.end - v.end);
3764
+ const gaps = [];
3765
+ let k = 0;
3766
+ for (let n = 1; n < bounds.length; n++) {
3767
+ const at = bounds[n];
3768
+ while (k < ends.length && ends[k].end <= at + snap) k++;
3769
+ const near = k > 0 ? ends[k - 1].end : at;
3770
+ const before = [];
3771
+ for (let j = k - 1; j >= 0 && ends[j].end >= near - snap; j--) before.push(ends[j].id);
3772
+ gaps.push({
3773
+ axis,
3774
+ index: n - 1,
3775
+ at,
3776
+ before: before.reverse(),
3777
+ after: after[n],
3778
+ width: Math.max(0, at - near),
3779
+ lanes: 0,
3780
+ need: Math.max(0, at - near)
3781
+ });
3782
+ }
3783
+ return gaps;
3784
+ }
3785
+ /** This function gives the index of the last value of the sorted list `v` that is at or before `x`, or 0. */
3786
+ function lastAtOrBefore(v, x) {
3787
+ let a = 0;
3788
+ let b = v.length - 1;
3789
+ while (a < b) {
3790
+ const m = a + b + 1 >> 1;
3791
+ if (v[m] <= x) a = m;
3792
+ else b = m - 1;
3793
+ }
3794
+ return a;
3795
+ }
3796
+ /**
3797
+ * This function moves the boxes apart, so that each gap gets the width `need`. The boxes keep their size, their order and their alignment.
3798
+ * A box moves by the sum of the growth of each gap at or before its left (or top) edge. Thus a grid stays a grid.
3799
+ * The function moves no box nearer to another box, so it makes no overlap.
3800
+ */
3801
+ function openGaps(items, gaps, snap = DEFAULT_SNAP) {
3802
+ const shift = (axis) => {
3803
+ const list = gaps.filter((g) => g.axis === axis).sort((u, v) => u.at - v.at);
3804
+ const at = list.map((g) => g.at);
3805
+ const sum = [];
3806
+ let s = 0;
3807
+ for (const g of list) sum.push(s += Math.max(0, g.need - g.width));
3808
+ return (v) => list.length === 0 || at[0] > v + snap ? 0 : sum[lastAtOrBefore(at, v + snap)];
2466
3809
  };
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;
3810
+ const sx = shift("x");
3811
+ const sy = shift("y");
3812
+ return items.map((i) => ({
3813
+ ...i,
3814
+ box: {
3815
+ ...i.box,
3816
+ x: i.box.x + sx(i.box.x),
3817
+ y: i.box.y + sy(i.box.y)
2493
3818
  }
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;
3819
+ }));
3820
+ }
3821
+ /**
3822
+ * This function gives the room that the links need in each gap of the board.
3823
+ * The lanes of a gap come from the `method` of the options. The need of a gap is `gutter + lanes × spacing`, and not less than its current width.
3824
+ */
3825
+ function roomDemand(items, links, options = {}) {
3826
+ const gutter = options.gutter ?? DEFAULT_GUTTER;
3827
+ const spacing = options.spacing ?? DEFAULT_SPACING;
3828
+ const snap = options.snap ?? DEFAULT_SNAP;
3829
+ const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
3830
+ const gaps = boardGaps(items, snap);
3831
+ const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
3832
+ return capGrowth(items, gaps.map((g, i) => ({
3833
+ ...g,
3834
+ lanes: lanes[i],
3835
+ need: Math.max(g.width, gutter + lanes[i] * spacing)
3836
+ })), gutter, options.maxGrowth ?? Infinity);
3837
+ }
3838
+ /** This function gives the demand of the board and the board with each gap open to its need. */
3839
+ function makeRoom(items, links, options = {}) {
3840
+ const gaps = roomDemand(items, links, options);
3841
+ return {
3842
+ gaps,
3843
+ items: openGaps(items, gaps, options.snap ?? DEFAULT_SNAP)
3844
+ };
3845
+ }
3846
+ /** This function gives the links between the nodes of a graph, with the count of the edges of each pair. A loop does not count. */
3847
+ function roomLinks(graph) {
3848
+ const byPair = /* @__PURE__ */ new Map();
3849
+ for (const e of graph.edges) {
3850
+ const [s, t] = [e.source.node, e.target.node];
3851
+ if (s === t) continue;
3852
+ const key = s < t ? `${s}\u0000${t}` : `${t}\u0000${s}`;
3853
+ const known = byPair.get(key);
3854
+ if (known) known.count++;
3855
+ else byPair.set(key, {
3856
+ source: s,
3857
+ target: t,
3858
+ count: 1
3859
+ });
3860
+ }
3861
+ return [...byPair.values()];
3862
+ }
3863
+ /** This function counts, for each gap, the straight lines between the centers of the boxes of a link that cross the gap. */
3864
+ function lineLanes(items, links, gaps, weight) {
3865
+ const byId = new Map(items.map((i) => [i.id, center(i.box)]));
3866
+ return gaps.map((g) => {
3867
+ let n = 0;
3868
+ for (const l of links) {
3869
+ const a = byId.get(l.source);
3870
+ const b = byId.get(l.target);
3871
+ if (a && b && Math.min(a[g.axis], b[g.axis]) < g.at && Math.max(a[g.axis], b[g.axis]) > g.at) n += weight(l);
2502
3872
  }
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;
3873
+ return n;
3874
+ });
3875
+ }
3876
+ /**
3877
+ * This function routes the links one time on the board with each gap open to the gutter.
3878
+ * The lanes of a gap are the largest number of routes that run along the gap at one place.
3879
+ * A piece of a route goes along a gap when it is in the channel of the gap.
3880
+ * It must also go farther along the gap than across it.
3881
+ */
3882
+ function routeLanes(items, links, gaps, gutter, snap, weight, router) {
3883
+ const probeGaps = gaps.map((g) => ({
3884
+ ...g,
3885
+ need: Math.max(g.width, gutter)
3886
+ }));
3887
+ const ways = probeWays(openGaps(items, probeGaps, snap), links, gutter, router);
3888
+ return probeGaps.map((g) => {
3889
+ let grow = 0;
3890
+ for (const h of probeGaps) if (h.axis === g.axis && h.at <= g.at + snap) grow += h.need - h.width;
3891
+ const hi = g.at + grow;
3892
+ const lo = hi - g.need;
3893
+ const spans = [];
3894
+ ways.forEach((points, i) => {
3895
+ const along = alongSpans(points, g.axis, lo, hi);
3896
+ for (const [from, to] of merge(along)) spans.push({
3897
+ from,
3898
+ to,
3899
+ w: weight(links[i])
2509
3900
  });
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
- }
3901
+ });
3902
+ return density(spans);
3903
+ });
3904
+ }
3905
+ /** This function gives the points of the way of each link on the probe board, in the order of the links. */
3906
+ function probeWays(items, links, gutter, router) {
3907
+ if (router) {
3908
+ const edges = links.map((l, i) => ({
3909
+ id: `l${i}`,
3910
+ source: { node: l.source },
3911
+ target: { node: l.target }
3912
+ }));
3913
+ const routes = new Map(router.route({
3914
+ nodes: items,
3915
+ edges
3916
+ }).routes.map((r) => [r.edge, r.points]));
3917
+ return edges.map((e) => routes.get(e.id) ?? []);
2519
3918
  }
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;
3919
+ const out = levelLinks(items.map((i) => ({
3920
+ id: i.id,
3921
+ box: i.box,
3922
+ nodes: [i.id]
3923
+ })), links.map((l) => ({
3924
+ source: l.source,
3925
+ target: l.target,
3926
+ count: l.count ?? 1
3927
+ })), { margin: Math.max(1, gutter / 2) });
3928
+ const byPair = new Map(out.map((l) => [`${l.source}\u0000${l.target}`, l.points]));
3929
+ return links.map((l) => byPair.get(`${l.source}\u0000${l.target}`) ?? []);
3930
+ }
3931
+ /**
3932
+ * This function gives the spans along the axis of a gap where the polyline goes along the channel from `lo` to `hi`.
3933
+ * The axis `x` gap is a vertical channel, so its spans are on the y axis.
3934
+ */
3935
+ function alongSpans(p, axis, lo, hi) {
3936
+ const across = axis;
3937
+ const along = other(axis);
3938
+ const out = [];
3939
+ for (let i = 1; i < p.length; i++) {
3940
+ const a = p[i - 1];
3941
+ const b = p[i];
3942
+ const da = b[across] - a[across];
3943
+ let t0 = 0;
3944
+ let t1 = 1;
3945
+ if (Math.abs(da) < 1e-9) {
3946
+ if (a[across] < lo - 1e-6 || a[across] > hi + 1e-6) continue;
3947
+ } else {
3948
+ const u = (lo - a[across]) / da;
3949
+ const v = (hi - a[across]) / da;
3950
+ t0 = Math.max(0, Math.min(u, v));
3951
+ t1 = Math.min(1, Math.max(u, v));
3952
+ if (t1 <= t0) continue;
2530
3953
  }
2531
- bundled.add(id);
3954
+ const s = a[along] + (b[along] - a[along]) * t0;
3955
+ const e = a[along] + (b[along] - a[along]) * t1;
3956
+ const run = Math.abs(e - s);
3957
+ if (run > Math.abs(da) * (t1 - t0) && run > 1e-6) out.push([Math.min(s, e), Math.max(s, e)]);
2532
3958
  }
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
- }
3959
+ return out;
3960
+ }
3961
+ /** This function joins the spans that overlap, so that one link counts one time at each place. */
3962
+ function merge(spans) {
3963
+ spans.sort((u, v) => u[0] - v[0]);
3964
+ const out = [];
3965
+ for (const s of spans) {
3966
+ const last = out[out.length - 1];
3967
+ if (last && s[0] <= last[1]) last[1] = Math.max(last[1], s[1]);
3968
+ else out.push([s[0], s[1]]);
2550
3969
  }
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;
3970
+ return out;
3971
+ }
3972
+ /** This function gives the largest sum of the weights of the spans that overlap at one place. Spans that only touch do not overlap. */
3973
+ function density(spans) {
3974
+ const events = [];
3975
+ for (const s of spans) events.push([s.from, s.w], [s.to, -s.w]);
3976
+ events.sort((u, v) => u[0] - v[0] || u[1] - v[1]);
3977
+ let n = 0;
3978
+ let best = 0;
3979
+ for (const [, w] of events) {
3980
+ n += w;
3981
+ best = Math.max(best, n);
3982
+ }
3983
+ return best;
3984
+ }
3985
+ /**
3986
+ * This function keeps the growth of the board on each axis at most `maxGrowth` times its size.
3987
+ * Each gap keeps its gutter. The lanes of all gaps of the axis share the rest of the growth in proportion.
3988
+ */
3989
+ function capGrowth(items, gaps, gutter, maxGrowth) {
3990
+ if (!Number.isFinite(maxGrowth)) return gaps;
3991
+ const out = [...gaps];
3992
+ for (const axis of ["x", "y"]) {
3993
+ let x0 = Infinity;
3994
+ let x1 = -Infinity;
3995
+ for (const i of items) {
3996
+ x0 = Math.min(x0, lo(i.box, axis));
3997
+ x1 = Math.max(x1, lo(i.box, axis) + len(i.box, axis));
2568
3998
  }
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
- }
3999
+ const budget = Math.max(0, maxGrowth) * Math.max(0, x1 - x0);
4000
+ let base = 0;
4001
+ let spare = 0;
4002
+ for (const g of gaps) {
4003
+ if (g.axis !== axis) continue;
4004
+ const b = Math.max(0, gutter - g.width);
4005
+ base += b;
4006
+ spare += Math.max(0, g.need - g.width - b);
2580
4007
  }
4008
+ if (base + spare <= budget || spare === 0) continue;
4009
+ const f = Math.max(0, (budget - base) / spare);
4010
+ gaps.forEach((g, i) => {
4011
+ if (g.axis !== axis) return;
4012
+ const b = Math.max(0, gutter - g.width);
4013
+ out[i] = {
4014
+ ...g,
4015
+ need: g.width + b + f * Math.max(0, g.need - g.width - b)
4016
+ };
4017
+ });
2581
4018
  }
2582
4019
  return out;
2583
4020
  }
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
4021
  // src/bench/quality.ts
2596
4022
  /** The clearance stops at this distance. */
2597
4023
  const CLEARANCE_CAP = 40;
@@ -2777,7 +4203,7 @@ function measureStability(before, after) {
2777
4203
  };
2778
4204
  }
2779
4205
  // src/bench/run.ts
2780
- const now = () => globalThis.performance?.now() ?? Date.now();
4206
+ const now$2 = () => globalThis.performance?.now() ?? Date.now();
2781
4207
  /** This function routes each fixture with each router, checks the hard rules and measures the quality. */
2782
4208
  function runBenchmark(routers, fixtures, options = {}) {
2783
4209
  const rows = [];
@@ -2816,9 +4242,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
2816
4242
  let first;
2817
4243
  let deterministic = true;
2818
4244
  for (let i = 0; i < Math.max(2, repeat); i++) {
2819
- const t0 = now();
4245
+ const t0 = now$2();
2820
4246
  const r = router.route(graph);
2821
- times.push(now() - t0);
4247
+ times.push(now$2() - t0);
2822
4248
  if (!first) first = r;
2823
4249
  else if (i === 1 && !sameData(r, first)) deterministic = false;
2824
4250
  }
@@ -2834,7 +4260,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
2834
4260
  deterministic,
2835
4261
  violations: all.length,
2836
4262
  sample: all.slice(0, 5),
2837
- quality: measureQuality(graph, result)
4263
+ quality: measureQuality(graph, result),
4264
+ faults: measureRouteFaults(graph, result),
4265
+ levelFaults: measureLevelFaults(result)
2838
4266
  };
2839
4267
  }
2840
4268
  /** This function writes the rows as a Markdown table. */
@@ -2851,6 +4279,205 @@ function formatTable(rows) {
2851
4279
  })
2852
4280
  ].join("\n");
2853
4281
  }
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 };
4282
+ /**
4283
+ * This function writes the faults of the rows as a Markdown table: the routes first, then the sum of the levels with groups.
4284
+ * A router without such levels gets a dash in the columns of the levels.
4285
+ */
4286
+ function formatFaults(rows) {
4287
+ return [
4288
+ "| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level out and back | level worst detour | level outside |",
4289
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
4290
+ ...rows.map((r) => {
4291
+ const f = r.faults;
4292
+ const cells = [
4293
+ r.router,
4294
+ r.fixture,
4295
+ f.through,
4296
+ f.backtrack,
4297
+ f.worstDetour.toFixed(2),
4298
+ f.outside
4299
+ ];
4300
+ if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
4301
+ else {
4302
+ const l = sumFaults(r.levelFaults);
4303
+ cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.backtrack, l.worstDetour.toFixed(2), l.outside);
4304
+ }
4305
+ return `| ${cells.join(" | ")} |`;
4306
+ })
4307
+ ].join("\n");
4308
+ }
4309
+ // src/bench/curves.ts
4310
+ const now$1 = () => globalThis.performance?.now() ?? Date.now();
4311
+ /**
4312
+ * This function routes a graph with a router and makes each curve type of each route.
4313
+ * It counts the curves that go through a box, with and without the free space, and it compares their length to the polylines.
4314
+ * The type `spline` leaves each plug along the normal of the nearest side of its node.
4315
+ */
4316
+ function benchCurves(router, fixture, graph, options = {}) {
4317
+ const tolerance = options.tolerance ?? .25;
4318
+ const result = router.route(graph);
4319
+ const byId = nodeIndex(graph);
4320
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4321
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
4322
+ const space = curveSpace(graph.nodes.map((n) => n.box), tolerance);
4323
+ const routes = result.routes.flatMap((r) => {
4324
+ const e = edges.get(r.edge);
4325
+ const s = e && byId.get(e.source.node);
4326
+ const t = e && byId.get(e.target.node);
4327
+ if (!e || !s || !t || r.points.length < 2) return [];
4328
+ const first = r.points[0];
4329
+ const last = r.points[r.points.length - 1];
4330
+ return [{
4331
+ source: s.id,
4332
+ target: t.id,
4333
+ points: r.points,
4334
+ startNormal: SIDE_NORMAL[nearestSide(s.box, first)],
4335
+ endNormal: SIDE_NORMAL[nearestSide(t.box, last)]
4336
+ }];
4337
+ });
4338
+ const base = measureLinks(boxes, routes);
4339
+ const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
4340
+ return CURVE_TYPES.map((type) => {
4341
+ const make = (withSpace) => routes.map((r) => ({
4342
+ source: r.source,
4343
+ target: r.target,
4344
+ points: sampleCurve(curveOf(r.points, {
4345
+ type,
4346
+ radius: options.radius ?? 12,
4347
+ startNormal: r.startNormal,
4348
+ endNormal: r.endNormal,
4349
+ ...withSpace ? { space } : {}
4350
+ }), tolerance)
4351
+ }));
4352
+ const free = measureLinks(boxes, make(false));
4353
+ const t0 = now$1();
4354
+ const clamped = make(true);
4355
+ const ms = now$1() - t0;
4356
+ const length = clamped.reduce((sum, l) => sum + polylineLength(l.points), 0);
4357
+ return {
4358
+ router: router.name,
4359
+ fixture,
4360
+ type,
4361
+ links: routes.length,
4362
+ through: base.through,
4363
+ free: free.through,
4364
+ clamped: measureLinks(boxes, clamped).through,
4365
+ length: ink > 0 ? length / ink : 1,
4366
+ ms
4367
+ };
4368
+ });
4369
+ }
4370
+ /** This function writes the rows of `benchCurves` as a Markdown table. */
4371
+ function formatCurves(rows) {
4372
+ return [
4373
+ "| router | fixture | curve | routes | through, polyline | through, free | through, clamped | length | ms |",
4374
+ "|---|---|---|---:|---:|---:|---:|---:|---:|",
4375
+ ...rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.type} | ${r.links} | ${r.through} | ${r.free} | ${r.clamped} | ${r.length.toFixed(3)} | ${r.ms.toFixed(1)} |`)
4376
+ ].join("\n");
4377
+ }
4378
+ // src/bench/room.ts
4379
+ const now = () => globalThis.performance?.now() ?? Date.now();
4380
+ /** This function gives the area of the bounding box of the nodes. */
4381
+ function areaOf(nodes) {
4382
+ let x0 = Infinity;
4383
+ let y0 = Infinity;
4384
+ let x1 = -Infinity;
4385
+ let y1 = -Infinity;
4386
+ for (const n of nodes) {
4387
+ x0 = Math.min(x0, n.box.x);
4388
+ y0 = Math.min(y0, n.box.y);
4389
+ x1 = Math.max(x1, n.box.x + n.box.w);
4390
+ y1 = Math.max(y1, n.box.y + n.box.h);
4391
+ }
4392
+ return nodes.length ? (x1 - x0) * (y1 - y0) : 0;
4393
+ }
4394
+ /**
4395
+ * This function measures the links of one board. It measures the links of the far view, with each node of the graph as one item of a level.
4396
+ * It also measures the lane routes of the router `sections` at the lane width `spacing`.
4397
+ */
4398
+ function measureBoard(graph, spacing) {
4399
+ const links = roomLinks(graph);
4400
+ const drawn = levelLinks(graph.nodes.map((n) => ({
4401
+ id: n.id,
4402
+ box: n.box,
4403
+ nodes: [n.id]
4404
+ })), links.map((l) => ({
4405
+ source: l.source,
4406
+ target: l.target,
4407
+ count: l.count ?? 1
4408
+ })));
4409
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4410
+ const edgeOf = /* @__PURE__ */ new Map();
4411
+ for (const e of graph.edges) {
4412
+ const key = `${e.source.node}\u0000${e.target.node}`;
4413
+ if (!edgeOf.has(key)) edgeOf.set(key, e.id);
4414
+ }
4415
+ const q = measureQuality(graph, { routes: drawn.map((l) => ({
4416
+ edge: edgeOf.get(`${l.source}\u0000${l.target}`),
4417
+ points: l.points
4418
+ })) });
4419
+ const segments = segmentsOf(sectionsRouter({ name: "sections" }).route(graph));
4420
+ const lanes = laneRoutes(segments, new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges))), spacing);
4421
+ return {
4422
+ faults: measureLinks(boxes, drawn),
4423
+ crossings: q.crossings,
4424
+ ink: q.ink,
4425
+ detour: q.detour,
4426
+ laneThrough: measureRouteFaults(graph, { routes: lanes }).through
4427
+ };
4428
+ }
4429
+ /**
4430
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that `makeRoom` opens with each method.
4431
+ * The options of `makeRoom` are the same for each method, except `method`.
4432
+ */
4433
+ function benchRoom(fixture, graph, options = {}) {
4434
+ const spacing = options.spacing ?? 4;
4435
+ const area0 = areaOf(graph.nodes);
4436
+ const links = roomLinks(graph);
4437
+ const row = (board, nodes, ms) => ({
4438
+ fixture,
4439
+ board,
4440
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
4441
+ ms,
4442
+ ...measureBoard({
4443
+ nodes,
4444
+ edges: graph.edges
4445
+ }, spacing)
4446
+ });
4447
+ const rows = [row("packed", graph.nodes, 0)];
4448
+ const states = [
4449
+ ["gutter", {
4450
+ ...options,
4451
+ spacing: 0,
4452
+ method: "routes"
4453
+ }],
4454
+ ["routes", {
4455
+ ...options,
4456
+ method: "routes"
4457
+ }],
4458
+ ["lines", {
4459
+ ...options,
4460
+ method: "lines"
4461
+ }]
4462
+ ];
4463
+ for (const [board, o] of states) {
4464
+ const t0 = now();
4465
+ const room = makeRoom(graph.nodes, links, o);
4466
+ rows.push(row(board, room.items, now() - t0));
4467
+ }
4468
+ return rows;
4469
+ }
4470
+ /** This function writes the rows of `benchRoom` as a Markdown table. */
4471
+ function formatRoom(rows) {
4472
+ return [
4473
+ "| fixture | board | area | links | through | outside | out and back | worst detour | crossings | ink | detour | lane through | ms |",
4474
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
4475
+ ...rows.map((r) => {
4476
+ const f = r.faults;
4477
+ return `| ${r.fixture} | ${r.board} | ${r.area.toFixed(3)} | ${f.links} | ${f.through} | ${f.outside} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${r.crossings} | ${Math.round(r.ink)} | ${r.detour.toFixed(3)} | ${r.laneThrough} | ${r.ms.toFixed(1)} |`;
4478
+ })
4479
+ ].join("\n");
4480
+ }
4481
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, arcCurve, backtrackOf, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, cubicAt, curveOf, curveSpace, denseGroupsBoard, dist, distinctPoints, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roomDemand, roomLinks, roundCorners, roundedCurve, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, toSvgPath, treeDepth, trunksRouter, turns };
2855
4482
 
2856
4483
  //# sourceMappingURL=index.js.map