@mark1russell7/highways 0.1.3 → 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
@@ -702,6 +702,32 @@ function manyGroupsBoard() {
702
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
703
  return b.graph();
704
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
+ }
705
731
  /** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
706
732
  const CASES = [
707
733
  {
@@ -888,207 +914,786 @@ function roundCorners(p, radius, steps = 4) {
888
914
  out.push(p[p.length - 1]);
889
915
  return out;
890
916
  }
891
- // src/visibility.ts
892
- /** The nodes as obstacles, with a grid for fast tests. */
893
- var Obstacles = class {
894
- #grid;
895
- /** This constructor indexes the boxes of the nodes. */
896
- constructor(nodes, cell) {
897
- this.#grid = new Grid(cell);
898
- for (const n of nodes) this.#grid.add(n, n.box);
899
- }
900
- /** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
901
- clear(a, b, pad, skip) {
902
- return this.hit(a, b, pad, skip) === void 0;
903
- }
904
- /**
905
- * This method gives a node whose box, made larger by `pad`, the segment `ab` goes into, or `undefined` when the segment is clear.
906
- * The nodes in `skip` do not count. The test stops at the first such node.
907
- */
908
- hit(a, b, pad, skip) {
909
- let found;
910
- this.#grid.someSegment(a, b, pad, (n) => {
911
- if (skip?.has(n) || !hits(a, b, n, pad)) return false;
912
- found = n;
913
- return true;
914
- });
915
- return found;
916
- }
917
- /** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
918
- clearPath(p, pad, skip) {
919
- for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
920
- return true;
921
- }
922
- /** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
923
- query(box, visit) {
924
- this.#grid.query(box, visit);
925
- }
926
- /** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
927
- inside(p, pad, skip) {
928
- let hit = false;
929
- this.#grid.query({
930
- x: p.x,
931
- y: p.y,
932
- w: 0,
933
- h: 0
934
- }, (n) => {
935
- if (!hit && n !== skip) {
936
- const b = n.box;
937
- hit = p.x > b.x - pad && p.x < b.x + b.w + pad && p.y > b.y - pad && p.y < b.y + b.h + pad;
938
- }
939
- });
940
- return hit;
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);
941
931
  }
942
- };
943
- /** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
944
- function hits(a, b, n, pad) {
945
- const x = n.box;
946
- return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
932
+ return out;
947
933
  }
934
+ // src/bench/properties.ts
935
+ /** The tolerance of the hard rules, in units of the plane. */
936
+ const TOLERANCE = .5;
948
937
  /**
949
- * A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
950
- * Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
951
- * A path goes from corner to corner around the boxes.
952
- * The obstacle test uses half of the margin, so a path keeps a clearance of at least half of the margin.
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.
953
945
  */
954
- var CornerGraph = class {
955
- points = [];
956
- #adj = [];
957
- #len = [];
958
- #obstacles;
959
- #pointGrid;
960
- #cell;
961
- #pad;
962
- #k;
963
- #g = /* @__PURE__ */ new Float64Array(0);
964
- #parent = /* @__PURE__ */ new Int32Array(0);
965
- #open = /* @__PURE__ */ new Int32Array(0);
966
- #closed = /* @__PURE__ */ new Int32Array(0);
967
- #stamp = 0;
968
- #buckets = /* @__PURE__ */ new Map();
969
- #dist = /* @__PURE__ */ new Float64Array(0);
970
- #near = new MinHeap();
971
- #endCache = /* @__PURE__ */ new Map();
972
- #fewCache = /* @__PURE__ */ new Map();
973
- #part;
974
- /** This constructor makes the graph of the nodes. */
975
- constructor(nodes, obstacles, options) {
976
- this.#obstacles = obstacles;
977
- this.#pad = options.margin / 2;
978
- this.#k = options.k;
979
- let size = 0;
980
- for (const n of nodes) size += (n.box.w + n.box.h) / 2;
981
- this.#cell = Math.max(1, size / Math.max(1, nodes.length) + 2 * options.margin);
982
- this.#pointGrid = new Grid(this.#cell);
983
- for (const n of nodes) {
984
- const b = inflate(n.box, options.margin);
985
- for (const c of [
986
- {
987
- x: b.x,
988
- y: b.y
989
- },
990
- {
991
- x: b.x + b.w,
992
- y: b.y
993
- },
994
- {
995
- x: b.x + b.w,
996
- y: b.y + b.h
997
- },
998
- {
999
- x: b.x,
1000
- y: b.y + b.h
1001
- }
1002
- ]) if (!obstacles.inside(c, this.#pad)) this.#addPoint(c);
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;
1003
969
  }
1004
- const spacing = (options.lattice ?? 3) * this.#cell;
1005
- if (spacing > 0 && nodes.length > 0) {
1006
- let x0 = Infinity;
1007
- let y0 = Infinity;
1008
- let x1 = -Infinity;
1009
- let y1 = -Infinity;
1010
- for (const n of nodes) {
1011
- x0 = Math.min(x0, n.box.x);
1012
- y0 = Math.min(y0, n.box.y);
1013
- x1 = Math.max(x1, n.box.x + n.box.w);
1014
- y1 = Math.max(y1, n.box.y + n.box.h);
1015
- }
1016
- for (let x = x0 - spacing / 2; x <= x1 + spacing; x += spacing) for (let y = y0 - spacing / 2; y <= y1 + spacing; y += spacing) {
1017
- const c = {
1018
- x,
1019
- y
1020
- };
1021
- if (!obstacles.inside(c, options.margin)) this.#addPoint(c);
1022
- }
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;
1023
978
  }
1024
- const links = this.points.map(() => /* @__PURE__ */ new Set());
1025
- for (let i = 0; i < this.points.length; i++) for (const j of this.nearestVisible(this.points[i], this.#k, i)) {
1026
- links[i].add(j);
1027
- links[j].add(i);
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;
1028
987
  }
1029
- links.forEach((s, i) => {
1030
- const adj = [...s].sort((a, b) => a - b);
1031
- this.#adj.push(adj);
1032
- this.#len.push(adj.map((j) => dist(this.points[i], this.points[j])));
1033
- });
1034
- }
1035
- #addPoint(c) {
1036
- const j = this.points.length;
1037
- this.#pointGrid.add(j, {
1038
- x: c.x,
1039
- y: c.y,
1040
- w: 0,
1041
- h: 0
1042
- });
1043
- const key = bucketKey(Math.floor(c.x / this.#cell), Math.floor(c.y / this.#cell));
1044
- const list = this.#buckets.get(key);
1045
- if (list === void 0) this.#buckets.set(key, [j]);
1046
- else list.push(j);
1047
- this.points.push(c);
1048
- }
1049
- /** This method gives the number of links of the graph. */
1050
- get linkCount() {
1051
- let n = 0;
1052
- for (const a of this.#adj) n += a.length;
1053
- return n / 2;
1054
- }
1055
- /** This method gives the index of the corner nearest to `p`, seen or not, or -1 when the graph has no corner near it. */
1056
- nearest(p) {
1057
- for (let r = this.#cell; r <= this.#cell * 64; r *= 2) {
1058
- let best = -1;
1059
- let bestD = Infinity;
1060
- this.#pointGrid.query({
1061
- x: p.x - r,
1062
- y: p.y - r,
1063
- w: 2 * r,
1064
- h: 2 * r
1065
- }, (j) => {
1066
- const d = dist(p, this.points[j]);
1067
- if (d < bestD || d === bestD && j < best) {
1068
- bestD = d;
1069
- best = j;
1070
- }
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;
1071
994
  });
1072
- if (best >= 0 && bestD <= r) return best;
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
+ }
1073
1003
  }
1074
- return -1;
1075
1004
  }
1076
- /** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
1077
- nearestVisible(p, k, self = -1) {
1078
- return this.#nearestVisible(p, k, self, this.#pad);
1079
- }
1080
- /**
1081
- * This method tests the corners in rings of radius `r` = 1, 2, 4 and 8 cells. In each ring, it tests the corners
1082
- * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
1083
- * The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
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
+ }
1493
+ // src/visibility.ts
1494
+ /** The nodes as obstacles, with a grid for fast tests. */
1495
+ var Obstacles = class {
1496
+ #grid;
1497
+ /** This constructor indexes the boxes of the nodes. */
1498
+ constructor(nodes, cell) {
1499
+ this.#grid = new Grid(cell);
1500
+ for (const n of nodes) this.#grid.add(n, n.box);
1501
+ }
1502
+ /** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
1503
+ clear(a, b, pad, skip) {
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;
1516
+ });
1517
+ return found;
1518
+ }
1519
+ /** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
1520
+ clearPath(p, pad, skip) {
1521
+ for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
1522
+ return true;
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
+ }
1528
+ /** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
1529
+ inside(p, pad, skip) {
1530
+ let hit = false;
1531
+ this.#grid.query({
1532
+ x: p.x,
1533
+ y: p.y,
1534
+ w: 0,
1535
+ h: 0
1536
+ }, (n) => {
1537
+ if (!hit && n !== skip) {
1538
+ const b = n.box;
1539
+ hit = p.x > b.x - pad && p.x < b.x + b.w + pad && p.y > b.y - pad && p.y < b.y + b.h + pad;
1540
+ }
1541
+ });
1542
+ return hit;
1543
+ }
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
+ }
1550
+ /**
1551
+ * A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
1552
+ * Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
1553
+ * A path goes from corner to corner around the boxes.
1554
+ * The obstacle test uses half of the margin, so a path keeps a clearance of at least half of the margin.
1555
+ */
1556
+ var CornerGraph = class {
1557
+ points = [];
1558
+ #adj = [];
1559
+ #len = [];
1560
+ #obstacles;
1561
+ #pointGrid;
1562
+ #cell;
1563
+ #pad;
1564
+ #k;
1565
+ #g = /* @__PURE__ */ new Float64Array(0);
1566
+ #parent = /* @__PURE__ */ new Int32Array(0);
1567
+ #open = /* @__PURE__ */ new Int32Array(0);
1568
+ #closed = /* @__PURE__ */ new Int32Array(0);
1569
+ #stamp = 0;
1570
+ #buckets = /* @__PURE__ */ new Map();
1571
+ #dist = /* @__PURE__ */ new Float64Array(0);
1572
+ #near = new MinHeap();
1573
+ #endCache = /* @__PURE__ */ new Map();
1574
+ #fewCache = /* @__PURE__ */ new Map();
1575
+ #part;
1576
+ /** This constructor makes the graph of the nodes. */
1577
+ constructor(nodes, obstacles, options) {
1578
+ this.#obstacles = obstacles;
1579
+ this.#pad = options.margin / 2;
1580
+ this.#k = options.k;
1581
+ let size = 0;
1582
+ for (const n of nodes) size += (n.box.w + n.box.h) / 2;
1583
+ this.#cell = Math.max(1, size / Math.max(1, nodes.length) + 2 * options.margin);
1584
+ this.#pointGrid = new Grid(this.#cell);
1585
+ for (const n of nodes) {
1586
+ const b = inflate(n.box, options.margin);
1587
+ for (const c of [
1588
+ {
1589
+ x: b.x,
1590
+ y: b.y
1591
+ },
1592
+ {
1593
+ x: b.x + b.w,
1594
+ y: b.y
1595
+ },
1596
+ {
1597
+ x: b.x + b.w,
1598
+ y: b.y + b.h
1599
+ },
1600
+ {
1601
+ x: b.x,
1602
+ y: b.y + b.h
1603
+ }
1604
+ ]) if (!obstacles.inside(c, this.#pad)) this.#addPoint(c);
1605
+ }
1606
+ const spacing = (options.lattice ?? 3) * this.#cell;
1607
+ if (spacing > 0 && nodes.length > 0) {
1608
+ let x0 = Infinity;
1609
+ let y0 = Infinity;
1610
+ let x1 = -Infinity;
1611
+ let y1 = -Infinity;
1612
+ for (const n of nodes) {
1613
+ x0 = Math.min(x0, n.box.x);
1614
+ y0 = Math.min(y0, n.box.y);
1615
+ x1 = Math.max(x1, n.box.x + n.box.w);
1616
+ y1 = Math.max(y1, n.box.y + n.box.h);
1617
+ }
1618
+ for (let x = x0 - spacing / 2; x <= x1 + spacing; x += spacing) for (let y = y0 - spacing / 2; y <= y1 + spacing; y += spacing) {
1619
+ const c = {
1620
+ x,
1621
+ y
1622
+ };
1623
+ if (!obstacles.inside(c, options.margin)) this.#addPoint(c);
1624
+ }
1625
+ }
1626
+ const links = this.points.map(() => /* @__PURE__ */ new Set());
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)) {
1628
+ links[i].add(j);
1629
+ links[j].add(i);
1630
+ }
1631
+ links.forEach((s, i) => {
1632
+ const adj = [...s].sort((a, b) => a - b);
1633
+ this.#adj.push(adj);
1634
+ this.#len.push(adj.map((j) => dist(this.points[i], this.points[j])));
1635
+ });
1636
+ }
1637
+ #addPoint(c) {
1638
+ const j = this.points.length;
1639
+ this.#pointGrid.add(j, {
1640
+ x: c.x,
1641
+ y: c.y,
1642
+ w: 0,
1643
+ h: 0
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);
1649
+ this.points.push(c);
1650
+ }
1651
+ /** This method gives the number of links of the graph. */
1652
+ get linkCount() {
1653
+ let n = 0;
1654
+ for (const a of this.#adj) n += a.length;
1655
+ return n / 2;
1656
+ }
1657
+ /** This method gives the index of the corner nearest to `p`, seen or not, or -1 when the graph has no corner near it. */
1658
+ nearest(p) {
1659
+ for (let r = this.#cell; r <= this.#cell * 64; r *= 2) {
1660
+ let best = -1;
1661
+ let bestD = Infinity;
1662
+ this.#pointGrid.query({
1663
+ x: p.x - r,
1664
+ y: p.y - r,
1665
+ w: 2 * r,
1666
+ h: 2 * r
1667
+ }, (j) => {
1668
+ const d = dist(p, this.points[j]);
1669
+ if (d < bestD || d === bestD && j < best) {
1670
+ bestD = d;
1671
+ best = j;
1672
+ }
1673
+ });
1674
+ if (best >= 0 && bestD <= r) return best;
1675
+ }
1676
+ return -1;
1677
+ }
1678
+ /** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
1679
+ nearestVisible(p, k, self = -1) {
1680
+ return this.#nearestVisible(p, k, self, this.#pad);
1681
+ }
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.
1084
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.
1085
1688
  */
1086
- #nearestVisible(p, k, self, pad) {
1689
+ #nearestVisible(p, k, self, pad, cones = false) {
1087
1690
  const out = [];
1691
+ let full = 0;
1088
1692
  if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
1089
1693
  const dd = this.#dist;
1090
1694
  const points = this.points;
1091
1695
  const cell = this.#cell;
1696
+ const reach = CONE_CELLS * cell;
1092
1697
  const blockers = [];
1093
1698
  const now = this.#near;
1094
1699
  let wait = [];
@@ -1097,6 +1702,7 @@ var CornerGraph = class {
1097
1702
  let pj0 = 1;
1098
1703
  let pj1 = 0;
1099
1704
  for (let r = cell; r <= cell * 8; r *= 2) {
1705
+ if (out.length >= k && r / 2 >= reach) return out;
1100
1706
  const x0 = p.x - r;
1101
1707
  const y0 = p.y - r;
1102
1708
  const i0 = Math.floor(x0 / cell);
@@ -1126,6 +1732,11 @@ var CornerGraph = class {
1126
1732
  while (now.size > 0) {
1127
1733
  const j = now.pop();
1128
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
+ }
1129
1740
  let blocked = false;
1130
1741
  for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
1131
1742
  if (blocked) continue;
@@ -1133,7 +1744,8 @@ var CornerGraph = class {
1133
1744
  if (hit !== void 0) blockers.push(hit);
1134
1745
  else {
1135
1746
  out.push(j);
1136
- if (out.length >= k) return out;
1747
+ full |= cone;
1748
+ if (out.length >= k && (!cones || full === 15)) return out;
1137
1749
  }
1138
1750
  }
1139
1751
  wait = later;
@@ -1321,6 +1933,10 @@ var CornerGraph = class {
1321
1933
  return out;
1322
1934
  }
1323
1935
  };
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;
1324
1940
  /** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
1325
1941
  const bucketKey = (i, j) => (i + 1048576) * 2097152 + (j + 1048576);
1326
1942
  /**
@@ -1602,415 +2218,269 @@ function offsetPolyline(p, d) {
1602
2218
  };
1603
2219
  const cos = (mx * a.x + my * a.y) / ml;
1604
2220
  const k = Math.min(2, 1 / Math.max(cos, 1e-9)) * d;
1605
- return {
1606
- x: q.x + mx / ml * k,
1607
- y: q.y + my / ml * k
1608
- };
1609
- });
1610
- }
1611
- /**
1612
- * This function pulls the lanes of each shared segment apart, and gives a route for each edge.
1613
- * The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
1614
- */
1615
- function pullApart(graph, options) {
1616
- return lanePaths(graph, (s) => s.count > 1 ? Math.min(options.spacing, options.maxWidth / (s.count - 1)) : 0);
1617
- }
1618
- /** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
1619
- function runKey(edges) {
1620
- return [...edges].sort().join(",");
1621
- }
1622
- /**
1623
- * This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
1624
- * The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
1625
- *
1626
- * Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
1627
- * The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
1628
- * The key of a segment is `runKey` of its edges.
1629
- */
1630
- function laneRoutes(source, open, laneWidth, maxWidth = Infinity) {
1631
- const graph = "paths" in source ? source : segmentsOf(source);
1632
- if (open.size === 0) return graph.paths.map((path) => ({
1633
- edge: path.edge,
1634
- points: pathPoints(graph, path)
1635
- }));
1636
- return lanePaths(graph, (s) => s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0);
1637
- }
1638
- /** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
1639
- function lanePaths(graph, spacingOf) {
1640
- const byId = new Map(graph.segments.map((s) => [s.id, s]));
1641
- const routes = [];
1642
- for (const path of graph.paths) {
1643
- const ends = pathPoints(graph, path);
1644
- const out = [];
1645
- for (const ref of path.segments) {
1646
- const s = byId.get(ref.segment);
1647
- if (!s) continue;
1648
- const spacing = spacingOf(s);
1649
- const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
1650
- for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME) out.push(q);
1651
- }
1652
- if (out.length >= 2 && ends.length >= 2) {
1653
- out[0] = ends[0];
1654
- out[out.length - 1] = ends[ends.length - 1];
1655
- }
1656
- routes.push({
1657
- edge: path.edge,
1658
- points: out
1659
- });
1660
- }
1661
- return routes;
1662
- }
1663
- /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
1664
- function turn$1(from, base, to) {
1665
- const dx = base.x - from.x;
1666
- const dy = base.y - from.y;
1667
- const vx = to.x - base.x;
1668
- const vy = to.y - base.y;
1669
- return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
1670
- }
1671
- /**
1672
- * This function gives the lane order of the edges on a shared way `run` (metro-line order). Two routes go on together
1673
- * past the last point of the run until they part. The route that turns more to the left there gets the lane more to the left.
1674
- * Two routes can part only before the first point. Then the route that comes from more to the left gets the lane more to the left.
1675
- * A route that does not hold the run goes last.
1676
- */
1677
- function orderLanes(run, routes) {
1678
- const placed = [];
1679
- const lost = [];
1680
- for (const r of routes) {
1681
- let at = indexOf(r.points, run, 0);
1682
- if (at >= 0) placed.push({
1683
- edge: r.edge,
1684
- p: r.points,
1685
- s: at,
1686
- e: at + run.length - 1
1687
- });
1688
- else {
1689
- const p = [...r.points].reverse();
1690
- at = indexOf(p, run, 0);
1691
- if (at >= 0) placed.push({
1692
- edge: r.edge,
1693
- p,
1694
- s: at,
1695
- e: at + run.length - 1
1696
- });
1697
- else lost.push(r.edge);
1698
- }
1699
- }
1700
- const same = (a, b) => dist(a, b) <= SAME;
1701
- placed.sort((P, Q) => {
1702
- for (let j = 1;; j++) {
1703
- const a = P.p[P.e + j];
1704
- const b = Q.p[Q.e + j];
1705
- if (a === void 0 || b === void 0) break;
1706
- if (!same(a, b)) {
1707
- const d = turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], a) - turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], b);
1708
- if (d !== 0) return d;
1709
- break;
1710
- }
1711
- }
1712
- for (let j = 1;; j++) {
1713
- const a = P.p[P.s - j];
1714
- const b = Q.p[Q.s - j];
1715
- if (a === void 0 || b === void 0) break;
1716
- if (!same(a, b)) {
1717
- const d = turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], b) - turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], a);
1718
- if (d !== 0) return d;
1719
- break;
1720
- }
1721
- }
1722
- return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
1723
- });
1724
- return [...placed.map((x) => x.edge), ...lost.sort()];
1725
- }
1726
- // src/sections.ts
1727
- /**
1728
- * This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
1729
- * When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
1730
- */
1731
- function buildSections(nodes, options = {}) {
1732
- const cap = Math.max(1, options.cap ?? 8);
1733
- const maxDepth = options.maxDepth ?? 16;
1734
- const centers = nodes.map((n) => center(n.box));
1735
- let x0 = Infinity;
1736
- let y0 = Infinity;
1737
- let x1 = -Infinity;
1738
- let y1 = -Infinity;
1739
- for (const n of nodes) {
1740
- x0 = Math.min(x0, n.box.x);
1741
- y0 = Math.min(y0, n.box.y);
1742
- x1 = Math.max(x1, n.box.x + n.box.w);
1743
- y1 = Math.max(y1, n.box.y + n.box.h);
1744
- }
1745
- const root = nodes.length ? {
1746
- x: x0,
1747
- y: y0,
1748
- w: x1 - x0,
1749
- h: y1 - y0
1750
- } : {
1751
- x: 0,
1752
- y: 0,
1753
- w: 0,
1754
- h: 0
1755
- };
1756
- const make = (id, depth, cell, list) => {
1757
- let box = cell;
1758
- for (let guard = 0; list.length > cap && depth < maxDepth && guard < 32; guard++) {
1759
- const mx = box.x + box.w / 2;
1760
- const my = box.y + box.h / 2;
1761
- const parts = [
1762
- [],
1763
- [],
1764
- [],
1765
- []
1766
- ];
1767
- for (const i of list) parts[(centers[i].x < mx ? 0 : 1) + (centers[i].y < my ? 0 : 2)].push(i);
1768
- const quarter = (q) => ({
1769
- x: q % 2 === 0 ? box.x : mx,
1770
- y: q < 2 ? box.y : my,
1771
- w: box.w / 2,
1772
- h: box.h / 2
1773
- });
1774
- const full = parts.map((p, q) => [p, q]).filter(([p]) => p.length > 0);
1775
- if (full.length === 1) {
1776
- box = quarter(full[0][1]);
1777
- continue;
1778
- }
1779
- const children = full.map(([p, q]) => make(`${id}${q}`, depth + 1, quarter(q), p));
1780
- return {
1781
- id,
1782
- depth,
1783
- cell: box,
1784
- nodes: list,
1785
- children
1786
- };
1787
- }
1788
- return {
1789
- id,
1790
- depth,
1791
- cell: box,
1792
- nodes: list,
1793
- children: []
1794
- };
1795
- };
1796
- return make("s", 0, root, nodes.map((_, i) => i));
1797
- }
1798
- /** This function gives the sections of the tree, parents before children. */
1799
- function allSections(root) {
1800
- const out = [];
1801
- const walk = (s) => {
1802
- out.push(s);
1803
- for (const c of s.children) walk(c);
1804
- };
1805
- walk(root);
1806
- return out;
1807
- }
1808
- /** This function gives the depth of the deepest section. */
1809
- function treeDepth(root) {
1810
- return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
1811
- }
1812
- // src/routers/corners.ts
1813
- /**
1814
- * This function makes a router that goes around the nodes.
1815
- * Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
1816
- * of the box corners, without the needless corners. It enters the target plug along its normal.
1817
- */
1818
- function cornersRouter(options) {
1819
- const router = {
1820
- name: options.name,
1821
- route(graph) {
1822
- const byId = nodeIndex(graph);
1823
- const ways = new WayFinder(graph.nodes, options);
1824
- const routes = [];
1825
- for (const e of graph.edges) {
1826
- const s = byId.get(e.source.node);
1827
- const t = byId.get(e.target.node);
1828
- if (!s || !t) continue;
1829
- const ps = placePlug(s, e.source, center(t.box));
1830
- const pt = placePlug(t, e.target, center(s.box));
1831
- routes.push({
1832
- edge: e.id,
1833
- points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
1834
- });
1835
- }
1836
- return { routes };
1837
- }
1838
- };
1839
- return options.maxNodes === void 0 ? router : {
1840
- ...router,
1841
- maxNodes: options.maxNodes
1842
- };
2221
+ return {
2222
+ x: q.x + mx / ml * k,
2223
+ y: q.y + my / ml * k
2224
+ };
2225
+ });
1843
2226
  }
1844
- // src/routers/plugs.ts
1845
2227
  /**
1846
- * Plugs and the inflection rule, with no obstacles. Each edge leaves its source plug at the tangent of the side.
1847
- * It goes to the target plug on the curve of the rule. This router measures the rule alone.
2228
+ * This function pulls the lanes of each shared segment apart, and gives a route for each edge.
2229
+ * The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
1848
2230
  */
1849
- const plugsRouter = {
1850
- name: "plugs",
1851
- route(graph) {
1852
- const byId = nodeIndex(graph);
1853
- const routes = [];
1854
- for (const e of graph.edges) {
1855
- const s = byId.get(e.source.node);
1856
- const t = byId.get(e.target.node);
1857
- if (!s || !t) continue;
1858
- const ps = placePlug(s, e.source, center(t.box));
1859
- const pt = placePlug(t, e.target, center(s.box));
1860
- routes.push({
1861
- edge: e.id,
1862
- points: inflectionCurve(ps.point, ps.normal, pt.point)
1863
- });
1864
- }
1865
- return { routes };
1866
- }
1867
- };
1868
- // src/bench/properties.ts
1869
- /** The tolerance of the hard rules, in units of the plane. */
1870
- const TOLERANCE = .5;
2231
+ function pullApart(graph, options) {
2232
+ return lanePaths(graph, (s) => s.count > 1 ? Math.min(options.spacing, options.maxWidth / (s.count - 1)) : 0);
2233
+ }
2234
+ /** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
2235
+ function runKey(edges) {
2236
+ return [...edges].sort().join(",");
2237
+ }
1871
2238
  /**
1872
- * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
1873
- * Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
1874
- * Each link of a level starts on the box of its source and ends on the box of its target.
2239
+ * This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
2240
+ * The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
1875
2241
  *
1876
- * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
1877
- * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
1878
- * The function gives at most `limit` violations.
2242
+ * Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
2243
+ * The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
2244
+ * The key of a segment is `runKey` of its edges.
1879
2245
  */
1880
- function checkProperties(graph, result, limit = 100) {
1881
- const out = [];
1882
- const add = (v) => {
1883
- out.push(v);
1884
- return out.length >= limit;
1885
- };
1886
- const byId = nodeIndex(graph);
1887
- const grid = new Grid(180);
1888
- for (const n of graph.nodes) grid.add(n, n.box);
1889
- const routes = /* @__PURE__ */ new Map();
1890
- for (const r of result.routes) routes.set(r.edge, r);
1891
- for (const e of graph.edges) {
1892
- const r = routes.get(e.id);
1893
- const s = byId.get(e.source.node);
1894
- const t = byId.get(e.target.node);
1895
- if (!s || !t) continue;
1896
- if (!r || r.points.length < 2) {
1897
- if (add({
1898
- rule: "complete",
1899
- edge: e.id,
1900
- detail: "no route"
1901
- })) return out;
1902
- continue;
1903
- }
1904
- const a = r.points[0];
1905
- const b = r.points[r.points.length - 1];
1906
- if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
1907
- if (add({
1908
- rule: "ends-at-plugs",
1909
- edge: e.id,
1910
- detail: "an end of the route is away from its node"
1911
- })) return out;
1912
- }
1913
- const fs = fixedPortPoint(s, e.source);
1914
- const ft = fixedPortPoint(t, e.target);
1915
- if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
1916
- if (add({
1917
- rule: "fixed-plugs",
1918
- edge: e.id,
1919
- detail: "the route does not start or end at its fixed plug"
1920
- })) return out;
2246
+ function laneRoutes(source, open, laneWidth, maxWidth = Infinity) {
2247
+ const graph = "paths" in source ? source : segmentsOf(source);
2248
+ if (open.size === 0) return graph.paths.map((path) => ({
2249
+ edge: path.edge,
2250
+ points: pathPoints(graph, path)
2251
+ }));
2252
+ return lanePaths(graph, (s) => s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0);
2253
+ }
2254
+ /** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
2255
+ function lanePaths(graph, spacingOf) {
2256
+ const byId = new Map(graph.segments.map((s) => [s.id, s]));
2257
+ const routes = [];
2258
+ for (const path of graph.paths) {
2259
+ const ends = pathPoints(graph, path);
2260
+ const out = [];
2261
+ for (const ref of path.segments) {
2262
+ const s = byId.get(ref.segment);
2263
+ if (!s) continue;
2264
+ const spacing = spacingOf(s);
2265
+ const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
2266
+ for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME) out.push(q);
1921
2267
  }
1922
- for (let i = 1; i < r.points.length; i++) {
1923
- const p = r.points[i - 1];
1924
- const q = r.points[i];
1925
- let hit;
1926
- grid.querySegment(p, q, 0, (n) => {
1927
- if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
1928
- });
1929
- if (hit) {
1930
- if (add({
1931
- rule: "no-pass-through",
1932
- edge: e.id,
1933
- detail: `segment ${i} goes through ${hit.id}`
1934
- })) return out;
1935
- break;
1936
- }
2268
+ if (out.length >= 2 && ends.length >= 2) {
2269
+ out[0] = ends[0];
2270
+ out[out.length - 1] = ends[ends.length - 1];
1937
2271
  }
2272
+ routes.push({
2273
+ edge: path.edge,
2274
+ points: out
2275
+ });
1938
2276
  }
1939
- const bundled = /* @__PURE__ */ new Set();
1940
- for (const b of result.bundles ?? []) for (const id of b.edges) {
1941
- const r = routes.get(id);
1942
- const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
1943
- if (bundled.has(id) || !shares) {
1944
- if (add({
1945
- rule: "bundle-trunk",
1946
- edge: id,
1947
- detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
1948
- })) return out;
2277
+ return routes;
2278
+ }
2279
+ /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
2280
+ function turn$1(from, base, to) {
2281
+ const dx = base.x - from.x;
2282
+ const dy = base.y - from.y;
2283
+ const vx = to.x - base.x;
2284
+ const vy = to.y - base.y;
2285
+ return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
2286
+ }
2287
+ /**
2288
+ * This function gives the lane order of the edges on a shared way `run` (metro-line order). Two routes go on together
2289
+ * past the last point of the run until they part. The route that turns more to the left there gets the lane more to the left.
2290
+ * Two routes can part only before the first point. Then the route that comes from more to the left gets the lane more to the left.
2291
+ * A route that does not hold the run goes last.
2292
+ */
2293
+ function orderLanes(run, routes) {
2294
+ const placed = [];
2295
+ const lost = [];
2296
+ for (const r of routes) {
2297
+ let at = indexOf(r.points, run, 0);
2298
+ if (at >= 0) placed.push({
2299
+ edge: r.edge,
2300
+ p: r.points,
2301
+ s: at,
2302
+ e: at + run.length - 1
2303
+ });
2304
+ else {
2305
+ const p = [...r.points].reverse();
2306
+ at = indexOf(p, run, 0);
2307
+ if (at >= 0) placed.push({
2308
+ edge: r.edge,
2309
+ p,
2310
+ s: at,
2311
+ e: at + run.length - 1
2312
+ });
2313
+ else lost.push(r.edge);
1949
2314
  }
1950
- bundled.add(id);
1951
2315
  }
1952
- for (const run of result.runs ?? []) {
1953
- if (run.edges.length < 2) {
1954
- if (add({
1955
- rule: "run-shared",
1956
- detail: `run ${run.id} has fewer than two edges`
1957
- })) return out;
1958
- }
1959
- for (const id of run.edges) {
1960
- const r = routes.get(id);
1961
- if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
1962
- if (add({
1963
- rule: "run-shared",
1964
- edge: id,
1965
- detail: `run ${run.id}: the route does not hold the points of the run in a row`
1966
- })) return out;
2316
+ const same = (a, b) => dist(a, b) <= SAME;
2317
+ placed.sort((P, Q) => {
2318
+ for (let j = 1;; j++) {
2319
+ const a = P.p[P.e + j];
2320
+ const b = Q.p[Q.e + j];
2321
+ if (a === void 0 || b === void 0) break;
2322
+ if (!same(a, b)) {
2323
+ const d = turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], a) - turn$1(P.p[P.e + j - 2], P.p[P.e + j - 1], b);
2324
+ if (d !== 0) return d;
2325
+ break;
1967
2326
  }
1968
2327
  }
1969
- }
1970
- for (const level of result.levels ?? []) {
1971
- const itemOf = /* @__PURE__ */ new Map();
1972
- for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
1973
- if (itemOf.size !== graph.nodes.length) {
1974
- if (add({
1975
- rule: "counts-sum",
1976
- detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
1977
- })) return out;
1978
- }
1979
- let between = 0;
1980
- for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
1981
- const sum = level.links.reduce((s, l) => s + l.count, 0);
1982
- if (sum !== between) {
1983
- if (add({
1984
- rule: "counts-sum",
1985
- detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
1986
- })) return out;
2328
+ for (let j = 1;; j++) {
2329
+ const a = P.p[P.s - j];
2330
+ const b = Q.p[Q.s - j];
2331
+ if (a === void 0 || b === void 0) break;
2332
+ if (!same(a, b)) {
2333
+ const d = turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], b) - turn$1(P.p[P.s - j + 2], P.p[P.s - j + 1], a);
2334
+ if (d !== 0) return d;
2335
+ break;
2336
+ }
1987
2337
  }
1988
- const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
1989
- for (const l of level.links) {
1990
- const s = boxOf.get(l.source);
1991
- const t = boxOf.get(l.target);
1992
- const p = l.points;
1993
- if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
1994
- if (add({
1995
- rule: "link-ends",
1996
- detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
1997
- })) return out;
2338
+ return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
2339
+ });
2340
+ return [...placed.map((x) => x.edge), ...lost.sort()];
2341
+ }
2342
+ // src/sections.ts
2343
+ /**
2344
+ * This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
2345
+ * When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
2346
+ */
2347
+ function buildSections(nodes, options = {}) {
2348
+ const cap = Math.max(1, options.cap ?? 8);
2349
+ const maxDepth = options.maxDepth ?? 16;
2350
+ const centers = nodes.map((n) => center(n.box));
2351
+ let x0 = Infinity;
2352
+ let y0 = Infinity;
2353
+ let x1 = -Infinity;
2354
+ let y1 = -Infinity;
2355
+ for (const n of nodes) {
2356
+ x0 = Math.min(x0, n.box.x);
2357
+ y0 = Math.min(y0, n.box.y);
2358
+ x1 = Math.max(x1, n.box.x + n.box.w);
2359
+ y1 = Math.max(y1, n.box.y + n.box.h);
2360
+ }
2361
+ const root = nodes.length ? {
2362
+ x: x0,
2363
+ y: y0,
2364
+ w: x1 - x0,
2365
+ h: y1 - y0
2366
+ } : {
2367
+ x: 0,
2368
+ y: 0,
2369
+ w: 0,
2370
+ h: 0
2371
+ };
2372
+ const make = (id, depth, cell, list) => {
2373
+ let box = cell;
2374
+ for (let guard = 0; list.length > cap && depth < maxDepth && guard < 32; guard++) {
2375
+ const mx = box.x + box.w / 2;
2376
+ const my = box.y + box.h / 2;
2377
+ const parts = [
2378
+ [],
2379
+ [],
2380
+ [],
2381
+ []
2382
+ ];
2383
+ for (const i of list) parts[(centers[i].x < mx ? 0 : 1) + (centers[i].y < my ? 0 : 2)].push(i);
2384
+ const quarter = (q) => ({
2385
+ x: q % 2 === 0 ? box.x : mx,
2386
+ y: q < 2 ? box.y : my,
2387
+ w: box.w / 2,
2388
+ h: box.h / 2
2389
+ });
2390
+ const full = parts.map((p, q) => [p, q]).filter(([p]) => p.length > 0);
2391
+ if (full.length === 1) {
2392
+ box = quarter(full[0][1]);
2393
+ continue;
1998
2394
  }
2395
+ const children = full.map(([p, q]) => make(`${id}${q}`, depth + 1, quarter(q), p));
2396
+ return {
2397
+ id,
2398
+ depth,
2399
+ cell: box,
2400
+ nodes: list,
2401
+ children
2402
+ };
1999
2403
  }
2000
- }
2404
+ return {
2405
+ id,
2406
+ depth,
2407
+ cell: box,
2408
+ nodes: list,
2409
+ children: []
2410
+ };
2411
+ };
2412
+ return make("s", 0, root, nodes.map((_, i) => i));
2413
+ }
2414
+ /** This function gives the sections of the tree, parents before children. */
2415
+ function allSections(root) {
2416
+ const out = [];
2417
+ const walk = (s) => {
2418
+ out.push(s);
2419
+ for (const c of s.children) walk(c);
2420
+ };
2421
+ walk(root);
2001
2422
  return out;
2002
2423
  }
2003
- /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
2004
- function onBoundary(p, b) {
2005
- const inner = inflate(b, -.5);
2006
- return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
2424
+ /** This function gives the depth of the deepest section. */
2425
+ function treeDepth(root) {
2426
+ return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
2007
2427
  }
2008
- /** This function tells if the polyline `p` holds the points of `part` in a row. */
2009
- function holds(p, part) {
2010
- if (part.length === 0) return true;
2011
- for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
2012
- return false;
2428
+ // src/routers/corners.ts
2429
+ /**
2430
+ * This function makes a router that goes around the nodes.
2431
+ * Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
2432
+ * of the box corners, without the needless corners. It enters the target plug along its normal.
2433
+ */
2434
+ function cornersRouter(options) {
2435
+ const router = {
2436
+ name: options.name,
2437
+ route(graph) {
2438
+ const byId = nodeIndex(graph);
2439
+ const ways = new WayFinder(graph.nodes, options);
2440
+ const routes = [];
2441
+ for (const e of graph.edges) {
2442
+ const s = byId.get(e.source.node);
2443
+ const t = byId.get(e.target.node);
2444
+ if (!s || !t) continue;
2445
+ const ps = placePlug(s, e.source, center(t.box));
2446
+ const pt = placePlug(t, e.target, center(s.box));
2447
+ routes.push({
2448
+ edge: e.id,
2449
+ points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
2450
+ });
2451
+ }
2452
+ return { routes };
2453
+ }
2454
+ };
2455
+ return options.maxNodes === void 0 ? router : {
2456
+ ...router,
2457
+ maxNodes: options.maxNodes
2458
+ };
2013
2459
  }
2460
+ // src/routers/plugs.ts
2461
+ /**
2462
+ * Plugs and the inflection rule, with no obstacles. Each edge leaves its source plug at the tangent of the side.
2463
+ * It goes to the target plug on the curve of the rule. This router measures the rule alone.
2464
+ */
2465
+ const plugsRouter = {
2466
+ name: "plugs",
2467
+ route(graph) {
2468
+ const byId = nodeIndex(graph);
2469
+ const routes = [];
2470
+ for (const e of graph.edges) {
2471
+ const s = byId.get(e.source.node);
2472
+ const t = byId.get(e.target.node);
2473
+ if (!s || !t) continue;
2474
+ const ps = placePlug(s, e.source, center(t.box));
2475
+ const pt = placePlug(t, e.target, center(s.box));
2476
+ routes.push({
2477
+ edge: e.id,
2478
+ points: inflectionCurve(ps.point, ps.normal, pt.point)
2479
+ });
2480
+ }
2481
+ return { routes };
2482
+ }
2483
+ };
2014
2484
  // src/bench/links.ts
2015
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. */
2016
2486
  const BACKTRACK_SHARE = .25;
@@ -2224,6 +2694,7 @@ function sideWay(ways, s, t, inset) {
2224
2694
  /**
2225
2695
  * This function makes the obstacles and the corner graph of the items of a level.
2226
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.
2227
2698
  */
2228
2699
  function levelWays(nodes, options) {
2229
2700
  const margin = options.margin ?? 5;
@@ -2233,7 +2704,8 @@ function levelWays(nodes, options) {
2233
2704
  const obstacles = new Obstacles(nodes, Math.max(180, 2 * size / Math.max(1, nodes.length)));
2234
2705
  const corners = new CornerGraph(nodes, obstacles, {
2235
2706
  margin,
2236
- k
2707
+ k,
2708
+ cones: true
2237
2709
  });
2238
2710
  const out = (p) => ({
2239
2711
  x: p.point.x + p.normal.x * margin,
@@ -2242,7 +2714,8 @@ function levelWays(nodes, options) {
2242
2714
  let tight;
2243
2715
  const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
2244
2716
  margin: Math.min(margin, TIGHT_MARGIN),
2245
- k
2717
+ k,
2718
+ cones: true
2246
2719
  });
2247
2720
  return {
2248
2721
  obstacles,
@@ -3264,6 +3737,287 @@ const ROUTERS = [
3264
3737
  maxNodes: 1e5
3265
3738
  })
3266
3739
  ];
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";
3747
+ /**
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.
3750
+ */
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)];
3809
+ };
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)
3818
+ }
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);
3872
+ }
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])
3900
+ });
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) ?? []);
3918
+ }
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;
3953
+ }
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)]);
3958
+ }
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]]);
3969
+ }
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));
3998
+ }
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);
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
+ });
4018
+ }
4019
+ return out;
4020
+ }
3267
4021
  // src/bench/quality.ts
3268
4022
  /** The clearance stops at this distance. */
3269
4023
  const CLEARANCE_CAP = 40;
@@ -3449,7 +4203,7 @@ function measureStability(before, after) {
3449
4203
  };
3450
4204
  }
3451
4205
  // src/bench/run.ts
3452
- const now = () => globalThis.performance?.now() ?? Date.now();
4206
+ const now$2 = () => globalThis.performance?.now() ?? Date.now();
3453
4207
  /** This function routes each fixture with each router, checks the hard rules and measures the quality. */
3454
4208
  function runBenchmark(routers, fixtures, options = {}) {
3455
4209
  const rows = [];
@@ -3488,9 +4242,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
3488
4242
  let first;
3489
4243
  let deterministic = true;
3490
4244
  for (let i = 0; i < Math.max(2, repeat); i++) {
3491
- const t0 = now();
4245
+ const t0 = now$2();
3492
4246
  const r = router.route(graph);
3493
- times.push(now() - t0);
4247
+ times.push(now$2() - t0);
3494
4248
  if (!first) first = r;
3495
4249
  else if (i === 1 && !sameData(r, first)) deterministic = false;
3496
4250
  }
@@ -3552,6 +4306,178 @@ function formatFaults(rows) {
3552
4306
  })
3553
4307
  ].join("\n");
3554
4308
  }
3555
- export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, backtrackOf, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, denseGroupsBoard, dist, fixedPortPoint, formatFaults, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, manyGroupsBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, runKey, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, sumFaults, treeDepth, trunksRouter, turns };
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 };
3556
4482
 
3557
4483
  //# sourceMappingURL=index.js.map