@mark1russell7/highways 0.1.3 → 0.1.5

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,6 +914,582 @@ function roundCorners(p, radius, steps = 4) {
888
914
  out.push(p[p.length - 1]);
889
915
  return out;
890
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
+ }
891
1493
  // src/visibility.ts
892
1494
  /** The nodes as obstacles, with a grid for fast tests. */
893
1495
  var Obstacles = class {
@@ -908,7 +1510,7 @@ var Obstacles = class {
908
1510
  hit(a, b, pad, skip) {
909
1511
  let found;
910
1512
  this.#grid.someSegment(a, b, pad, (n) => {
911
- if (skip?.has(n) || !hits(a, b, n, pad)) return false;
1513
+ if (skip?.has(n) || !hits$1(a, b, n, pad)) return false;
912
1514
  found = n;
913
1515
  return true;
914
1516
  });
@@ -941,7 +1543,7 @@ var Obstacles = class {
941
1543
  }
942
1544
  };
943
1545
  /** 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) {
1546
+ function hits$1(a, b, n, pad) {
945
1547
  const x = n.box;
946
1548
  return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
947
1549
  }
@@ -1022,7 +1624,7 @@ var CornerGraph = class {
1022
1624
  }
1023
1625
  }
1024
1626
  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)) {
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)) {
1026
1628
  links[i].add(j);
1027
1629
  links[j].add(i);
1028
1630
  }
@@ -1082,13 +1684,16 @@ var CornerGraph = class {
1082
1684
  * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
1083
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,14 +1732,20 @@ 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
- for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
1741
+ for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits$1(p, q, blockers[b], pad);
1131
1742
  if (blocked) continue;
1132
1743
  const hit = this.#obstacles.hit(p, q, pad);
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
  /**
@@ -1836,181 +2452,35 @@ function cornersRouter(options) {
1836
2452
  return { routes };
1837
2453
  }
1838
2454
  };
1839
- return options.maxNodes === void 0 ? router : {
1840
- ...router,
1841
- maxNodes: options.maxNodes
1842
- };
1843
- }
1844
- // src/routers/plugs.ts
1845
- /**
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.
1848
- */
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;
1871
- /**
1872
- * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
1873
- * Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
1874
- * Each link of a level starts on the box of its source and ends on the box of its target.
1875
- *
1876
- * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
1877
- * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
1878
- * The function gives at most `limit` violations.
1879
- */
1880
- function checkProperties(graph, result, limit = 100) {
1881
- const out = [];
1882
- const add = (v) => {
1883
- out.push(v);
1884
- return out.length >= limit;
1885
- };
1886
- const byId = nodeIndex(graph);
1887
- const grid = new Grid(180);
1888
- for (const n of graph.nodes) grid.add(n, n.box);
1889
- const routes = /* @__PURE__ */ new Map();
1890
- for (const r of result.routes) routes.set(r.edge, r);
1891
- for (const e of graph.edges) {
1892
- const r = routes.get(e.id);
1893
- const s = byId.get(e.source.node);
1894
- const t = byId.get(e.target.node);
1895
- if (!s || !t) continue;
1896
- if (!r || r.points.length < 2) {
1897
- if (add({
1898
- rule: "complete",
1899
- edge: e.id,
1900
- detail: "no route"
1901
- })) return out;
1902
- continue;
1903
- }
1904
- const a = r.points[0];
1905
- const b = r.points[r.points.length - 1];
1906
- if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
1907
- if (add({
1908
- rule: "ends-at-plugs",
1909
- edge: e.id,
1910
- detail: "an end of the route is away from its node"
1911
- })) return out;
1912
- }
1913
- const fs = fixedPortPoint(s, e.source);
1914
- const ft = fixedPortPoint(t, e.target);
1915
- if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
1916
- if (add({
1917
- rule: "fixed-plugs",
1918
- edge: e.id,
1919
- detail: "the route does not start or end at its fixed plug"
1920
- })) return out;
1921
- }
1922
- for (let i = 1; i < r.points.length; i++) {
1923
- const p = r.points[i - 1];
1924
- const q = r.points[i];
1925
- let hit;
1926
- grid.querySegment(p, q, 0, (n) => {
1927
- if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
1928
- });
1929
- if (hit) {
1930
- if (add({
1931
- rule: "no-pass-through",
1932
- edge: e.id,
1933
- detail: `segment ${i} goes through ${hit.id}`
1934
- })) return out;
1935
- break;
1936
- }
1937
- }
1938
- }
1939
- const bundled = /* @__PURE__ */ new Set();
1940
- for (const b of result.bundles ?? []) for (const id of b.edges) {
1941
- const r = routes.get(id);
1942
- const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
1943
- if (bundled.has(id) || !shares) {
1944
- if (add({
1945
- rule: "bundle-trunk",
1946
- edge: id,
1947
- detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
1948
- })) return out;
1949
- }
1950
- bundled.add(id);
1951
- }
1952
- for (const run of result.runs ?? []) {
1953
- if (run.edges.length < 2) {
1954
- if (add({
1955
- rule: "run-shared",
1956
- detail: `run ${run.id} has fewer than two edges`
1957
- })) return out;
1958
- }
1959
- for (const id of run.edges) {
1960
- const r = routes.get(id);
1961
- if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
1962
- if (add({
1963
- rule: "run-shared",
1964
- edge: id,
1965
- detail: `run ${run.id}: the route does not hold the points of the run in a row`
1966
- })) return out;
1967
- }
1968
- }
1969
- }
1970
- for (const level of result.levels ?? []) {
1971
- const itemOf = /* @__PURE__ */ new Map();
1972
- for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
1973
- if (itemOf.size !== graph.nodes.length) {
1974
- if (add({
1975
- rule: "counts-sum",
1976
- detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
1977
- })) return out;
1978
- }
1979
- let between = 0;
1980
- for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
1981
- const sum = level.links.reduce((s, l) => s + l.count, 0);
1982
- if (sum !== between) {
1983
- if (add({
1984
- rule: "counts-sum",
1985
- detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
1986
- })) return out;
1987
- }
1988
- const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
1989
- for (const l of level.links) {
1990
- const s = boxOf.get(l.source);
1991
- const t = boxOf.get(l.target);
1992
- const p = l.points;
1993
- if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
1994
- if (add({
1995
- rule: "link-ends",
1996
- detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
1997
- })) return out;
1998
- }
2455
+ return options.maxNodes === void 0 ? router : {
2456
+ ...router,
2457
+ maxNodes: options.maxNodes
2458
+ };
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
+ });
1999
2480
  }
2481
+ return { routes };
2000
2482
  }
2001
- return out;
2002
- }
2003
- /** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
2004
- function onBoundary(p, b) {
2005
- const inner = inflate(b, -.5);
2006
- return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
2007
- }
2008
- /** This function tells if the polyline `p` holds the points of `part` in a row. */
2009
- function holds(p, part) {
2010
- if (part.length === 0) return true;
2011
- for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
2012
- return false;
2013
- }
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;
@@ -2155,7 +2625,7 @@ function sumFaults(list) {
2155
2625
  // src/levels.ts
2156
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. */
2157
2627
  const TIGHT_MARGIN = 1;
2158
- const EPS = 1e-6;
2628
+ const EPS$1 = 1e-6;
2159
2629
  /**
2160
2630
  * This function removes the repeated points, the points in the middle of a straight run,
2161
2631
  * and the points where the way turns back on itself.
@@ -2163,15 +2633,15 @@ const EPS = 1e-6;
2163
2633
  function tidy(p) {
2164
2634
  const out = [];
2165
2635
  for (const q of p) {
2166
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2636
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2167
2637
  while (out.length >= 2) {
2168
2638
  const a = out[out.length - 2];
2169
2639
  const b = out[out.length - 1];
2170
2640
  const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
2171
- if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
2641
+ if (Math.abs(cross) > EPS$1 * (1 + dist(a, b) * dist(b, q))) break;
2172
2642
  out.pop();
2173
2643
  }
2174
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
2644
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
2175
2645
  out.push(q);
2176
2646
  }
2177
2647
  return out;
@@ -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,
@@ -2408,7 +2881,7 @@ function freeSpans(lo, hi, covered) {
2408
2881
  if (x >= hi) break;
2409
2882
  }
2410
2883
  if (x < hi) out.push([x, hi]);
2411
- return out.filter(([a, b]) => b - a > EPS);
2884
+ return out.filter(([a, b]) => b - a > EPS$1);
2412
2885
  }
2413
2886
  /**
2414
2887
  * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
@@ -2540,7 +3013,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
2540
3013
  out.push(...step.legs);
2541
3014
  const end = out[out.length - 1];
2542
3015
  const before = out[out.length - 2];
2543
- axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : void 0;
3016
+ axis = Math.abs(end.y - before.y) < EPS$1 ? "x" : Math.abs(end.x - before.x) < EPS$1 ? "y" : void 0;
2544
3017
  i = step.j;
2545
3018
  }
2546
3019
  return out;
@@ -2550,7 +3023,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
2550
3023
  * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
2551
3024
  */
2552
3025
  function square(a, b, obstacles, pad, from, to) {
2553
- if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : void 0;
3026
+ if (Math.abs(a.x - b.x) < EPS$1 || Math.abs(a.y - b.y) < EPS$1) return obstacles.clear(a, b, pad) ? [b] : void 0;
2554
3027
  const options = [{
2555
3028
  c: {
2556
3029
  x: b.x,
@@ -3227,7 +3700,7 @@ function trunksRouter(options) {
3227
3700
  };
3228
3701
  }
3229
3702
  // src/routers/index.ts
3230
- /** The routers that the benchmark races. */
3703
+ /** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
3231
3704
  const ROUTERS = [
3232
3705
  straightRouter,
3233
3706
  plugsRouter,
@@ -3264,6 +3737,439 @@ 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 snap = options.snap ?? DEFAULT_SNAP;
3828
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
3829
+ let total = demandOnce(items, links, options);
3830
+ for (let p = 1; p < passes; p++) {
3831
+ const next = demandOnce(openGaps(items, total, snap), links, options);
3832
+ if (next.length !== total.length || next.some((g, i) => g.axis !== total[i].axis)) break;
3833
+ total = total.map((g, i) => ({
3834
+ ...g,
3835
+ lanes: Math.max(g.lanes, next[i].lanes),
3836
+ need: g.need + Math.max(0, next[i].need - next[i].width)
3837
+ }));
3838
+ }
3839
+ return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
3840
+ }
3841
+ /** This function gives the demand of one pass, without the limit `maxGrowth`. */
3842
+ function demandOnce(items, links, options) {
3843
+ const gutter = options.gutter ?? DEFAULT_GUTTER;
3844
+ const spacing = options.spacing ?? DEFAULT_SPACING;
3845
+ const snap = options.snap ?? DEFAULT_SNAP;
3846
+ const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
3847
+ const gaps = boardGaps(items, snap);
3848
+ const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
3849
+ return gaps.map((g, i) => ({
3850
+ ...g,
3851
+ lanes: lanes[i],
3852
+ need: Math.max(g.width, gutter + lanes[i] * spacing)
3853
+ }));
3854
+ }
3855
+ /** This function gives the demand of the board and the board with each gap open to its need. */
3856
+ function makeRoom(items, links, options = {}) {
3857
+ const gaps = roomDemand(items, links, options);
3858
+ return {
3859
+ gaps,
3860
+ items: openGaps(items, gaps, options.snap ?? DEFAULT_SNAP)
3861
+ };
3862
+ }
3863
+ /** 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. */
3864
+ function roomLinks(graph) {
3865
+ const byPair = /* @__PURE__ */ new Map();
3866
+ for (const e of graph.edges) {
3867
+ const [s, t] = [e.source.node, e.target.node];
3868
+ if (s === t) continue;
3869
+ const key = s < t ? `${s}\u0000${t}` : `${t}\u0000${s}`;
3870
+ const known = byPair.get(key);
3871
+ if (known) known.count++;
3872
+ else byPair.set(key, {
3873
+ source: s,
3874
+ target: t,
3875
+ count: 1
3876
+ });
3877
+ }
3878
+ return [...byPair.values()];
3879
+ }
3880
+ /** This function counts, for each gap, the straight lines between the centers of the boxes of a link that cross the gap. */
3881
+ function lineLanes(items, links, gaps, weight) {
3882
+ const byId = new Map(items.map((i) => [i.id, center(i.box)]));
3883
+ return gaps.map((g) => {
3884
+ let n = 0;
3885
+ for (const l of links) {
3886
+ const a = byId.get(l.source);
3887
+ const b = byId.get(l.target);
3888
+ 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);
3889
+ }
3890
+ return n;
3891
+ });
3892
+ }
3893
+ /**
3894
+ * This function routes the links one time on the board with each gap open to the gutter.
3895
+ * The lanes of a gap are the largest number of routes that run along the gap at one place.
3896
+ * A piece of a route goes along a gap when it is in the channel of the gap.
3897
+ * It must also go farther along the gap than across it.
3898
+ */
3899
+ function routeLanes(items, links, gaps, gutter, snap, weight, router) {
3900
+ const probeGaps = gaps.map((g) => ({
3901
+ ...g,
3902
+ need: Math.max(g.width, gutter)
3903
+ }));
3904
+ const ways = probeWays(openGaps(items, probeGaps, snap), links, gutter, router);
3905
+ return probeGaps.map((g) => {
3906
+ let grow = 0;
3907
+ for (const h of probeGaps) if (h.axis === g.axis && h.at <= g.at + snap) grow += h.need - h.width;
3908
+ const hi = g.at + grow;
3909
+ const lo = hi - g.need;
3910
+ const spans = [];
3911
+ ways.forEach((points, i) => {
3912
+ const along = alongSpans(points, g.axis, lo, hi);
3913
+ for (const [from, to] of merge(along)) spans.push({
3914
+ from,
3915
+ to,
3916
+ w: weight(links[i])
3917
+ });
3918
+ });
3919
+ return density(spans);
3920
+ });
3921
+ }
3922
+ /** This function gives the points of the way of each link on the probe board, in the order of the links. */
3923
+ function probeWays(items, links, gutter, router) {
3924
+ if (router) {
3925
+ const edges = links.map((l, i) => ({
3926
+ id: `l${i}`,
3927
+ source: { node: l.source },
3928
+ target: { node: l.target }
3929
+ }));
3930
+ const routes = new Map(router.route({
3931
+ nodes: items,
3932
+ edges
3933
+ }).routes.map((r) => [r.edge, r.points]));
3934
+ return edges.map((e) => routes.get(e.id) ?? []);
3935
+ }
3936
+ const out = levelLinks(items.map((i) => ({
3937
+ id: i.id,
3938
+ box: i.box,
3939
+ nodes: [i.id]
3940
+ })), links.map((l) => ({
3941
+ source: l.source,
3942
+ target: l.target,
3943
+ count: l.count ?? 1
3944
+ })), { margin: Math.max(1, gutter / 2) });
3945
+ const byPair = new Map(out.map((l) => [`${l.source}\u0000${l.target}`, l.points]));
3946
+ return links.map((l) => byPair.get(`${l.source}\u0000${l.target}`) ?? []);
3947
+ }
3948
+ /**
3949
+ * This function gives the spans along the axis of a gap where the polyline goes along the channel from `lo` to `hi`.
3950
+ * The axis `x` gap is a vertical channel, so its spans are on the y axis.
3951
+ */
3952
+ function alongSpans(p, axis, lo, hi) {
3953
+ const across = axis;
3954
+ const along = other(axis);
3955
+ const out = [];
3956
+ for (let i = 1; i < p.length; i++) {
3957
+ const a = p[i - 1];
3958
+ const b = p[i];
3959
+ const da = b[across] - a[across];
3960
+ let t0 = 0;
3961
+ let t1 = 1;
3962
+ if (Math.abs(da) < 1e-9) {
3963
+ if (a[across] < lo - 1e-6 || a[across] > hi + 1e-6) continue;
3964
+ } else {
3965
+ const u = (lo - a[across]) / da;
3966
+ const v = (hi - a[across]) / da;
3967
+ t0 = Math.max(0, Math.min(u, v));
3968
+ t1 = Math.min(1, Math.max(u, v));
3969
+ if (t1 <= t0) continue;
3970
+ }
3971
+ const s = a[along] + (b[along] - a[along]) * t0;
3972
+ const e = a[along] + (b[along] - a[along]) * t1;
3973
+ const run = Math.abs(e - s);
3974
+ if (run > Math.abs(da) * (t1 - t0) && run > 1e-6) out.push([Math.min(s, e), Math.max(s, e)]);
3975
+ }
3976
+ return out;
3977
+ }
3978
+ /** This function joins the spans that overlap, so that one link counts one time at each place. */
3979
+ function merge(spans) {
3980
+ spans.sort((u, v) => u[0] - v[0]);
3981
+ const out = [];
3982
+ for (const s of spans) {
3983
+ const last = out[out.length - 1];
3984
+ if (last && s[0] <= last[1]) last[1] = Math.max(last[1], s[1]);
3985
+ else out.push([s[0], s[1]]);
3986
+ }
3987
+ return out;
3988
+ }
3989
+ /** This function gives the largest sum of the weights of the spans that overlap at one place. Spans that only touch do not overlap. */
3990
+ function density(spans) {
3991
+ const events = [];
3992
+ for (const s of spans) events.push([s.from, s.w], [s.to, -s.w]);
3993
+ events.sort((u, v) => u[0] - v[0] || u[1] - v[1]);
3994
+ let n = 0;
3995
+ let best = 0;
3996
+ for (const [, w] of events) {
3997
+ n += w;
3998
+ best = Math.max(best, n);
3999
+ }
4000
+ return best;
4001
+ }
4002
+ /**
4003
+ * This function keeps the growth of the board on each axis at most `maxGrowth` times its size.
4004
+ * Each gap keeps its gutter. The lanes of all gaps of the axis share the rest of the growth in proportion.
4005
+ */
4006
+ function capGrowth(items, gaps, gutter, maxGrowth) {
4007
+ if (!Number.isFinite(maxGrowth)) return gaps;
4008
+ const out = [...gaps];
4009
+ for (const axis of ["x", "y"]) {
4010
+ let x0 = Infinity;
4011
+ let x1 = -Infinity;
4012
+ for (const i of items) {
4013
+ x0 = Math.min(x0, lo(i.box, axis));
4014
+ x1 = Math.max(x1, lo(i.box, axis) + len(i.box, axis));
4015
+ }
4016
+ const budget = Math.max(0, maxGrowth) * Math.max(0, x1 - x0);
4017
+ let base = 0;
4018
+ let spare = 0;
4019
+ for (const g of gaps) {
4020
+ if (g.axis !== axis) continue;
4021
+ const b = Math.max(0, gutter - g.width);
4022
+ base += b;
4023
+ spare += Math.max(0, g.need - g.width - b);
4024
+ }
4025
+ if (base + spare <= budget || spare === 0) continue;
4026
+ const f = Math.max(0, (budget - base) / spare);
4027
+ gaps.forEach((g, i) => {
4028
+ if (g.axis !== axis) return;
4029
+ const b = Math.max(0, gutter - g.width);
4030
+ out[i] = {
4031
+ ...g,
4032
+ need: g.width + b + f * Math.max(0, g.need - g.width - b)
4033
+ };
4034
+ });
4035
+ }
4036
+ return out;
4037
+ }
4038
+ // src/room/lanes.ts
4039
+ /** The lane orders of `gapLanes`, in the order of the benchmark. */
4040
+ const LANE_ORDERS = [
4041
+ "links",
4042
+ "ends",
4043
+ "crossings"
4044
+ ];
4045
+ const EPS = 1e-6;
4046
+ const ROUNDS = 8;
4047
+ /**
4048
+ * This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
4049
+ * A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
4050
+ * When a move puts a link through more boxes than before, the link keeps its points.
4051
+ */
4052
+ function gapLanes(items, links, options = {}) {
4053
+ const spacing = Math.max(0, options.spacing ?? 4);
4054
+ const order = options.order ?? "crossings";
4055
+ const moved = links.map((l) => l.points.map((p) => ({
4056
+ x: p.x,
4057
+ y: p.y
4058
+ })));
4059
+ for (const g of boardGaps(items, options.snap)) {
4060
+ if (g.width <= EPS) continue;
4061
+ const across = g.axis;
4062
+ const along = g.axis === "x" ? "y" : "x";
4063
+ const lo = g.at - g.width;
4064
+ const hi = g.at;
4065
+ const pieces = [];
4066
+ links.forEach((l, li) => {
4067
+ const p = l.points;
4068
+ for (let i = 2; i <= p.length - 2; i++) {
4069
+ const a = p[i - 1];
4070
+ const b = p[i];
4071
+ if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
4072
+ if (Math.abs(a[along] - b[along]) <= EPS) continue;
4073
+ const side = (q, r) => Math.sign(r[across] - q[across]);
4074
+ pieces.push({
4075
+ link: li,
4076
+ seg: i,
4077
+ lo: Math.min(a[along], b[along]),
4078
+ hi: Math.max(a[along], b[along]),
4079
+ turns: [[a[along], side(a, p[i - 2])], [b[along], side(b, p[i + 1])]]
4080
+ });
4081
+ }
4082
+ });
4083
+ if (pieces.length === 0) continue;
4084
+ const list = orderPieces(pieces, order);
4085
+ const lane = [];
4086
+ let n = 0;
4087
+ list.forEach((P, i) => {
4088
+ let k = 0;
4089
+ for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
4090
+ lane.push(k);
4091
+ n = Math.max(n, k + 1);
4092
+ });
4093
+ const step = Math.min(spacing, g.width / (n + 1));
4094
+ const mid = (lo + hi) / 2;
4095
+ list.forEach((P, i) => {
4096
+ const v = mid + (lane[i] - (n - 1) / 2) * step;
4097
+ moved[P.link][P.seg - 1][across] = v;
4098
+ moved[P.link][P.seg][across] = v;
4099
+ });
4100
+ }
4101
+ const boxes = new Grid(Math.max(20, meanSide(items)));
4102
+ for (const i of items) boxes.add(i.box, i.box);
4103
+ return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
4104
+ ...l,
4105
+ points: moved[i]
4106
+ });
4107
+ }
4108
+ const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
4109
+ /**
4110
+ * This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
4111
+ * A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
4112
+ */
4113
+ function cost(P, Q) {
4114
+ let c = 0;
4115
+ for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
4116
+ for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
4117
+ return c;
4118
+ }
4119
+ /** This function gives the pieces of one gap in lane order, from the near side to the far side. */
4120
+ function orderPieces(pieces, order) {
4121
+ if (order === "links") return [...pieces];
4122
+ const sum = (P) => P.turns.reduce((s, [, side]) => s + side, 0);
4123
+ const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
4124
+ if (order === "ends") return list;
4125
+ const n = list.length;
4126
+ const index = new Map(list.map((P, i) => [P, i]));
4127
+ const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost(P, Q) : 0));
4128
+ const at = (P, Q) => c[index.get(P)][index.get(Q)];
4129
+ for (let round = 0; round < ROUNDS; round++) {
4130
+ let better = false;
4131
+ for (let i = 0; i < n; i++) {
4132
+ const x = list[i];
4133
+ let best = 0;
4134
+ let to = i;
4135
+ let d = 0;
4136
+ for (let k = i - 1; k >= 0; k--) {
4137
+ d += at(x, list[k]) - at(list[k], x);
4138
+ if (d < best) [best, to] = [d, k];
4139
+ }
4140
+ d = 0;
4141
+ for (let k = i + 1; k < n; k++) {
4142
+ d += at(list[k], x) - at(x, list[k]);
4143
+ if (d < best) [best, to] = [d, k];
4144
+ }
4145
+ if (to !== i) {
4146
+ list.splice(i, 1);
4147
+ list.splice(to, 0, x);
4148
+ better = true;
4149
+ }
4150
+ }
4151
+ if (!better) break;
4152
+ }
4153
+ return list;
4154
+ }
4155
+ /** This function gives the mean of the longer side of the boxes. */
4156
+ function meanSide(items) {
4157
+ let s = 0;
4158
+ for (const i of items) s += Math.max(i.box.w, i.box.h);
4159
+ return items.length ? s / items.length : 1;
4160
+ }
4161
+ /** This function counts the legs of a polyline that go through a box, for each box. */
4162
+ function hits(boxes, p) {
4163
+ let n = 0;
4164
+ for (let i = 1; i < p.length; i++) {
4165
+ const a = p[i - 1];
4166
+ const b = p[i];
4167
+ boxes.querySegment(a, b, 0, (box) => {
4168
+ if (segmentHitsBox(a, b, box)) n++;
4169
+ });
4170
+ }
4171
+ return n;
4172
+ }
3267
4173
  // src/bench/quality.ts
3268
4174
  /** The clearance stops at this distance. */
3269
4175
  const CLEARANCE_CAP = 40;
@@ -3449,7 +4355,7 @@ function measureStability(before, after) {
3449
4355
  };
3450
4356
  }
3451
4357
  // src/bench/run.ts
3452
- const now = () => globalThis.performance?.now() ?? Date.now();
4358
+ const now$2 = () => globalThis.performance?.now() ?? Date.now();
3453
4359
  /** This function routes each fixture with each router, checks the hard rules and measures the quality. */
3454
4360
  function runBenchmark(routers, fixtures, options = {}) {
3455
4361
  const rows = [];
@@ -3488,9 +4394,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
3488
4394
  let first;
3489
4395
  let deterministic = true;
3490
4396
  for (let i = 0; i < Math.max(2, repeat); i++) {
3491
- const t0 = now();
4397
+ const t0 = now$2();
3492
4398
  const r = router.route(graph);
3493
- times.push(now() - t0);
4399
+ times.push(now$2() - t0);
3494
4400
  if (!first) first = r;
3495
4401
  else if (i === 1 && !sameData(r, first)) deterministic = false;
3496
4402
  }
@@ -3552,6 +4458,482 @@ function formatFaults(rows) {
3552
4458
  })
3553
4459
  ].join("\n");
3554
4460
  }
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 };
4461
+ // src/strategies/model.ts
4462
+ /** The steps that have strategies, in the order of the pipeline. */
4463
+ const STEPS = [
4464
+ "router",
4465
+ "curve",
4466
+ "room"
4467
+ ];
4468
+ /**
4469
+ * This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
4470
+ * When the caller gives no option, the definition gets the `defaults` object itself.
4471
+ */
4472
+ function defineStrategy(definition) {
4473
+ const { step, name, describe, defaults, maxNodes } = definition;
4474
+ if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
4475
+ const strategy = {
4476
+ step,
4477
+ name,
4478
+ describe,
4479
+ defaults: Object.freeze({ ...defaults }),
4480
+ ...maxNodes === void 0 ? {} : { maxNodes },
4481
+ run(input, options) {
4482
+ const all = options === void 0 || Object.keys(options).length === 0 ? strategy.defaults : {
4483
+ ...strategy.defaults,
4484
+ ...options
4485
+ };
4486
+ return definition.run(input, all);
4487
+ }
4488
+ };
4489
+ return Object.freeze(strategy);
4490
+ }
4491
+ /** This function makes a registry from a list of strategies. It keeps their order. Two strategies with one step and one name are an error. */
4492
+ function createRegistry(strategies) {
4493
+ const out = {
4494
+ router: [],
4495
+ curve: [],
4496
+ room: []
4497
+ };
4498
+ for (const s of strategies) {
4499
+ const list = out[s.step];
4500
+ if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
4501
+ if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
4502
+ list.push(s);
4503
+ }
4504
+ return Object.freeze({
4505
+ router: Object.freeze(out.router),
4506
+ curve: Object.freeze(out.curve),
4507
+ room: Object.freeze(out.room)
4508
+ });
4509
+ }
4510
+ /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
4511
+ function register(registry, ...strategies) {
4512
+ return createRegistry([
4513
+ ...registry.router,
4514
+ ...registry.curve,
4515
+ ...registry.room,
4516
+ ...strategies
4517
+ ]);
4518
+ }
4519
+ /** This function gives the strategies of one step of a registry, in order. */
4520
+ function listStrategies(registry, step) {
4521
+ return registry[step];
4522
+ }
4523
+ /** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
4524
+ function findStrategy(registry, step, name) {
4525
+ return listStrategies(registry, step).find((s) => s.name === name);
4526
+ }
4527
+ /** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
4528
+ function getStrategy(registry, step, name) {
4529
+ const s = findStrategy(registry, step, name);
4530
+ if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
4531
+ return s;
4532
+ }
4533
+ /** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
4534
+ function asRouter(strategy, options) {
4535
+ return {
4536
+ name: strategy.name,
4537
+ ...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
4538
+ route: (graph) => strategy.run(graph, options)
4539
+ };
4540
+ }
4541
+ /** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
4542
+ function routersOf(registry) {
4543
+ return listStrategies(registry, "router").map((s) => asRouter(s));
4544
+ }
4545
+ // src/strategies/builtins.ts
4546
+ /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
4547
+ function routerStrategy(name, describe, maxNodes, defaults, make) {
4548
+ return defineStrategy({
4549
+ step: "router",
4550
+ name,
4551
+ describe,
4552
+ defaults,
4553
+ maxNodes,
4554
+ run: (graph, options) => make({
4555
+ ...options,
4556
+ name
4557
+ }).route(graph)
4558
+ });
4559
+ }
4560
+ /** This function makes a router strategy for a router without options. */
4561
+ function fixedRouter(router, describe) {
4562
+ return defineStrategy({
4563
+ step: "router",
4564
+ name: router.name,
4565
+ describe,
4566
+ defaults: {},
4567
+ run: (graph) => router.route(graph)
4568
+ });
4569
+ }
4570
+ /** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
4571
+ const routerStrategies = Object.freeze({
4572
+ straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
4573
+ plugs: fixedRouter(plugsRouter, "Each edge leaves its plug along the normal of the side and follows the curve of the inflection rule. It does not go around the nodes."),
4574
+ corners: routerStrategy("corners", "Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.", 1e4, {}, cornersRouter),
4575
+ "corners-smooth": routerStrategy("corners-smooth", "The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.", 1e4, { smooth: true }, cornersRouter),
4576
+ trunks: routerStrategy("trunks", "A rule puts edges in groups. The edges of a group leave one plug of one node on one trunk. At a split point, each edge goes on to its target.", 1e4, { rule: byCollection }, trunksRouter),
4577
+ shared: routerStrategy("shared", "The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.", 1e4, { rule: byCollection }, sharedRouter),
4578
+ "shared-all": routerStrategy("shared-all", "The same as `shared`, but all edges share their ways.", 1e4, {}, sharedRouter),
4579
+ sections: routerStrategy("sections", "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.", 1e5, {}, sectionsRouter),
4580
+ "sections-tree": routerStrategy("sections-tree", "The same as `sections`, but without the limit of the stretch.", 1e5, { stretch: Infinity }, sectionsRouter)
4581
+ });
4582
+ /** The context of a curve input, without the keys that have no value. */
4583
+ const contextOf = (input) => ({
4584
+ ...input.startNormal ? { startNormal: input.startNormal } : {},
4585
+ ...input.endNormal ? { endNormal: input.endNormal } : {},
4586
+ ...input.space ? { space: input.space } : {}
4587
+ });
4588
+ /** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
4589
+ const curveStrategies = Object.freeze({
4590
+ straight: defineStrategy({
4591
+ step: "curve",
4592
+ name: "straight",
4593
+ describe: "The polyline as it is.",
4594
+ defaults: {},
4595
+ run: (input) => straightCurve(input.points)
4596
+ }),
4597
+ rounded: defineStrategy({
4598
+ step: "curve",
4599
+ name: "rounded",
4600
+ describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
4601
+ defaults: { radius: 12 },
4602
+ run: (input, o) => roundedCurve(input.points, {
4603
+ ...contextOf(input),
4604
+ radius: o.radius
4605
+ })
4606
+ }),
4607
+ spline: defineStrategy({
4608
+ step: "curve",
4609
+ name: "spline",
4610
+ describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
4611
+ defaults: { tension: 0 },
4612
+ run: (input, o) => splineCurve(input.points, {
4613
+ ...contextOf(input),
4614
+ tension: o.tension
4615
+ })
4616
+ }),
4617
+ arc: defineStrategy({
4618
+ step: "curve",
4619
+ name: "arc",
4620
+ describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
4621
+ defaults: { radius: 12 },
4622
+ run: (input, o) => arcCurve(input.points, {
4623
+ ...contextOf(input),
4624
+ radius: o.radius
4625
+ })
4626
+ })
4627
+ });
4628
+ const roomStrategy = (method, describe) => defineStrategy({
4629
+ step: "room",
4630
+ name: method,
4631
+ describe,
4632
+ defaults: {},
4633
+ run: (input, o) => roomDemand(input.items, input.links, {
4634
+ ...o,
4635
+ method
4636
+ })
4637
+ });
4638
+ /** The built-in room strategies: the methods of `roomDemand`. */
4639
+ const roomStrategies = Object.freeze({
4640
+ routes: roomStrategy("routes", "The lanes of a gap are the most routes that run along it side by side, with each gap open to the gutter."),
4641
+ lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap.")
4642
+ });
4643
+ /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
4644
+ const BUILT_IN_STRATEGIES = Object.freeze([
4645
+ ...Object.values(routerStrategies),
4646
+ ...Object.values(curveStrategies),
4647
+ ...Object.values(roomStrategies)
4648
+ ]);
4649
+ /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
4650
+ const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
4651
+ // src/bench/curves.ts
4652
+ const now$1 = () => globalThis.performance?.now() ?? Date.now();
4653
+ /**
4654
+ * This function routes a graph with a router and makes each curve type of each route.
4655
+ * It counts the curves that go through a box, with and without the free space, and it compares their length to the polylines.
4656
+ * The type `spline` leaves each plug along the normal of the nearest side of its node.
4657
+ */
4658
+ function benchCurves(router, fixture, graph, options = {}) {
4659
+ return benchCurveStrategies(router, fixture, graph, CURVE_TYPES.map((t) => curveStrategies[t]), options);
4660
+ }
4661
+ /**
4662
+ * This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
4663
+ * The option `radius` goes to each strategy. A strategy without such an option does not read it.
4664
+ */
4665
+ function benchCurveStrategies(router, fixture, graph, curves, options = {}) {
4666
+ const tolerance = options.tolerance ?? .25;
4667
+ const result = router.route(graph);
4668
+ const byId = nodeIndex(graph);
4669
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4670
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
4671
+ const space = curveSpace(graph.nodes.map((n) => n.box), tolerance);
4672
+ const routes = result.routes.flatMap((r) => {
4673
+ const e = edges.get(r.edge);
4674
+ const s = e && byId.get(e.source.node);
4675
+ const t = e && byId.get(e.target.node);
4676
+ if (!e || !s || !t || r.points.length < 2) return [];
4677
+ const first = r.points[0];
4678
+ const last = r.points[r.points.length - 1];
4679
+ return [{
4680
+ source: s.id,
4681
+ target: t.id,
4682
+ points: r.points,
4683
+ startNormal: SIDE_NORMAL[nearestSide(s.box, first)],
4684
+ endNormal: SIDE_NORMAL[nearestSide(t.box, last)]
4685
+ }];
4686
+ });
4687
+ const base = measureLinks(boxes, routes);
4688
+ const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
4689
+ const radius = { radius: options.radius ?? 12 };
4690
+ return curves.map((curve) => {
4691
+ const make = (withSpace) => routes.map((r) => ({
4692
+ source: r.source,
4693
+ target: r.target,
4694
+ points: sampleCurve(curve.run({
4695
+ points: r.points,
4696
+ startNormal: r.startNormal,
4697
+ endNormal: r.endNormal,
4698
+ ...withSpace ? { space } : {}
4699
+ }, radius), tolerance)
4700
+ }));
4701
+ const free = measureLinks(boxes, make(false));
4702
+ const t0 = now$1();
4703
+ const clamped = make(true);
4704
+ const ms = now$1() - t0;
4705
+ const length = clamped.reduce((sum, l) => sum + polylineLength(l.points), 0);
4706
+ return {
4707
+ router: router.name,
4708
+ fixture,
4709
+ type: curve.name,
4710
+ links: routes.length,
4711
+ through: base.through,
4712
+ free: free.through,
4713
+ clamped: measureLinks(boxes, clamped).through,
4714
+ length: ink > 0 ? length / ink : 1,
4715
+ ms
4716
+ };
4717
+ });
4718
+ }
4719
+ /** This function writes the rows of `benchCurves` as a Markdown table. */
4720
+ function formatCurves(rows) {
4721
+ return [
4722
+ "| router | fixture | curve | routes | through, polyline | through, free | through, clamped | length | ms |",
4723
+ "|---|---|---|---:|---:|---:|---:|---:|---:|",
4724
+ ...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)} |`)
4725
+ ].join("\n");
4726
+ }
4727
+ // src/bench/room.ts
4728
+ const now = () => globalThis.performance?.now() ?? Date.now();
4729
+ /** This function gives the area of the bounding box of the nodes. */
4730
+ function areaOf(nodes) {
4731
+ let x0 = Infinity;
4732
+ let y0 = Infinity;
4733
+ let x1 = -Infinity;
4734
+ let y1 = -Infinity;
4735
+ for (const n of nodes) {
4736
+ x0 = Math.min(x0, n.box.x);
4737
+ y0 = Math.min(y0, n.box.y);
4738
+ x1 = Math.max(x1, n.box.x + n.box.w);
4739
+ y1 = Math.max(y1, n.box.y + n.box.h);
4740
+ }
4741
+ return nodes.length ? (x1 - x0) * (y1 - y0) : 0;
4742
+ }
4743
+ /**
4744
+ * 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.
4745
+ * With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
4746
+ * Without it, the links of a gap share the lines of the level links.
4747
+ * The function also measures the lane routes of the router `sections` at the lane width `spacing`.
4748
+ */
4749
+ function measureBoard(graph, spacing, order) {
4750
+ const links = roomLinks(graph);
4751
+ const level = levelLinks(graph.nodes.map((n) => ({
4752
+ id: n.id,
4753
+ box: n.box,
4754
+ nodes: [n.id]
4755
+ })), links.map((l) => ({
4756
+ source: l.source,
4757
+ target: l.target,
4758
+ count: l.count ?? 1
4759
+ })));
4760
+ const drawn = order ? gapLanes(graph.nodes, level, {
4761
+ spacing,
4762
+ order
4763
+ }) : level;
4764
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
4765
+ const edgeOf = /* @__PURE__ */ new Map();
4766
+ for (const e of graph.edges) {
4767
+ const key = `${e.source.node}\u0000${e.target.node}`;
4768
+ if (!edgeOf.has(key)) edgeOf.set(key, e.id);
4769
+ }
4770
+ const q = measureQuality(graph, { routes: drawn.map((l) => ({
4771
+ edge: edgeOf.get(`${l.source}\u0000${l.target}`),
4772
+ points: l.points
4773
+ })) });
4774
+ const segments = segmentsOf(sectionsRouter({ name: "sections" }).route(graph));
4775
+ const lanes = laneRoutes(segments, new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges))), spacing);
4776
+ return {
4777
+ faults: measureLinks(boxes, drawn),
4778
+ crossings: q.crossings,
4779
+ ink: q.ink,
4780
+ detour: q.detour,
4781
+ laneThrough: measureRouteFaults(graph, { routes: lanes }).through
4782
+ };
4783
+ }
4784
+ /**
4785
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
4786
+ * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
4787
+ */
4788
+ function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
4789
+ const spacing = options.spacing ?? 4;
4790
+ const area0 = areaOf(graph.nodes);
4791
+ const links = roomLinks(graph);
4792
+ const row = (board, nodes, ms) => ({
4793
+ fixture,
4794
+ board,
4795
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
4796
+ ms,
4797
+ ...measureBoard({
4798
+ nodes,
4799
+ edges: graph.edges
4800
+ }, spacing)
4801
+ });
4802
+ const rows = [row("packed", graph.nodes, 0)];
4803
+ const t0 = now();
4804
+ const gutter = makeRoom(graph.nodes, links, {
4805
+ ...options,
4806
+ spacing: 0,
4807
+ method: "routes"
4808
+ });
4809
+ rows.push(row("gutter", gutter.items, now() - t0));
4810
+ const { method: _method, ...preset } = options;
4811
+ for (const s of methods) {
4812
+ const t1 = now();
4813
+ const gaps = s.run({
4814
+ items: graph.nodes,
4815
+ links
4816
+ }, preset);
4817
+ const nodes = openGaps(graph.nodes, gaps, options.snap);
4818
+ rows.push(row(s.name, nodes, now() - t1));
4819
+ }
4820
+ return rows;
4821
+ }
4822
+ /**
4823
+ * This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
4824
+ * The first row is the fair baseline: the board with each gap open to the gutter of `baseline` only, measured at the spacing of `baseline`.
4825
+ */
4826
+ function sweepRoom(fixture, graph, settings, baseline = {
4827
+ gutter: 8,
4828
+ spacing: 4
4829
+ }) {
4830
+ const area0 = areaOf(graph.nodes);
4831
+ const links = roomLinks(graph);
4832
+ const row = (board, nodes, ms, spacing, lanes) => ({
4833
+ fixture,
4834
+ board,
4835
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
4836
+ ms,
4837
+ ...measureBoard({
4838
+ nodes,
4839
+ edges: graph.edges
4840
+ }, spacing, lanes)
4841
+ });
4842
+ const rows = [row("gutter", makeRoom(graph.nodes, links, {
4843
+ ...baseline,
4844
+ spacing: 0,
4845
+ method: "routes",
4846
+ maxGrowth: Infinity,
4847
+ passes: 1
4848
+ }).items, 0, baseline.spacing ?? 4)];
4849
+ for (const s of settings) {
4850
+ const t0 = now();
4851
+ const open = makeRoom(graph.nodes, links, {
4852
+ ...s.options,
4853
+ method: "routes"
4854
+ });
4855
+ rows.push(row(s.name, open.items, now() - t0, s.options.spacing ?? 4, s.lanes));
4856
+ }
4857
+ return rows;
4858
+ }
4859
+ /** The values of `maxGrowth` of the sweep of ROOM-A. */
4860
+ const GROWTH_SWEEP = [
4861
+ 0,
4862
+ .06,
4863
+ .09,
4864
+ .12,
4865
+ .15,
4866
+ .18,
4867
+ .21,
4868
+ .25,
4869
+ .3,
4870
+ .35,
4871
+ .5,
4872
+ Infinity
4873
+ ];
4874
+ /** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
4875
+ function growthSettings(values = GROWTH_SWEEP) {
4876
+ return values.map((m) => ({
4877
+ name: `maxGrowth ${m}`,
4878
+ options: {
4879
+ gutter: 8,
4880
+ spacing: 4,
4881
+ maxGrowth: m
4882
+ }
4883
+ }));
4884
+ }
4885
+ /**
4886
+ * This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
4887
+ * The order `shared` is the far view without lanes, as before ROOM-C.
4888
+ */
4889
+ function laneSettings(spacings = [
4890
+ 2,
4891
+ 4,
4892
+ 6,
4893
+ 8
4894
+ ], orders = LANE_ORDERS) {
4895
+ return spacings.flatMap((spacing) => [{
4896
+ name: `shared spacing ${spacing}`,
4897
+ options: {
4898
+ gutter: 8,
4899
+ spacing
4900
+ }
4901
+ }, ...orders.map((lanes) => ({
4902
+ name: `${lanes} spacing ${spacing}`,
4903
+ options: {
4904
+ gutter: 8,
4905
+ spacing
4906
+ },
4907
+ lanes
4908
+ }))]);
4909
+ }
4910
+ /** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
4911
+ function passSettings(gutters = [
4912
+ 4,
4913
+ 8,
4914
+ 12,
4915
+ 16
4916
+ ], passes = [1, 2]) {
4917
+ return gutters.flatMap((gutter) => passes.map((p) => ({
4918
+ name: `gutter ${gutter} passes ${p}`,
4919
+ options: {
4920
+ gutter,
4921
+ spacing: 4,
4922
+ passes: p
4923
+ }
4924
+ })));
4925
+ }
4926
+ /** This function writes the rows of `benchRoom` as a Markdown table. */
4927
+ function formatRoom(rows) {
4928
+ return [
4929
+ "| fixture | board | area | links | through | outside | out and back | worst detour | crossings | ink | detour | lane through | ms |",
4930
+ "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
4931
+ ...rows.map((r) => {
4932
+ const f = r.faults;
4933
+ 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)} |`;
4934
+ })
4935
+ ].join("\n");
4936
+ }
4937
+ export { BACKTRACK_MIN, BACKTRACK_SHARE, BUILT_IN_STRATEGIES, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, GROWTH_SWEEP, Grid, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, STEPS, STRATEGIES, TOLERANCE, WayFinder, allSections, arcCurve, asRouter, backtrackOf, benchCurveStrategies, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, createRegistry, cubicAt, curveOf, curveSpace, curveStrategies, defineStrategy, denseGroupsBoard, dist, distinctPoints, findStrategy, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, gapLanes, getStrategy, goesOutAndBack, groupEdges, growthSettings, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, laneSettings, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, passSettings, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, register, rng, roomDemand, roomLinks, roomStrategies, roundCorners, roundedCurve, routerStrategies, routersOf, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, sweepRoom, toSvgPath, treeDepth, trunksRouter, turns };
3556
4938
 
3557
4939
  //# sourceMappingURL=index.js.map