@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/README.md +109 -0
- package/build/index.d.ts +321 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1506 -580
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/curves.ts +98 -0
- package/src/bench/room.ts +122 -0
- package/src/cases.ts +28 -0
- package/src/curves/arc/index.ts +71 -0
- package/src/curves/index.ts +48 -0
- package/src/curves/model.ts +64 -0
- package/src/curves/rounded/index.ts +60 -0
- package/src/curves/sample.ts +73 -0
- package/src/curves/space.ts +91 -0
- package/src/curves/spline/index.ts +77 -0
- package/src/curves/straight/index.ts +7 -0
- package/src/index.ts +4 -0
- package/src/levels.ts +3 -2
- package/src/room/index.ts +334 -0
- package/src/visibility.ts +28 -3
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/
|
|
892
|
-
/** The
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
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 (
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1847
|
-
*
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
|
1873
|
-
*
|
|
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
|
-
*
|
|
1877
|
-
*
|
|
1878
|
-
* The
|
|
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
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
};
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
if (
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
}
|
|
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
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
|
2004
|
-
function
|
|
2005
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
|
|
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
|