@mark1russell7/highways 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/build/index.d.ts +636 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2548 -0
- package/build/index.js.map +1 -0
- package/package.json +42 -3
- package/src/bench/properties.ts +103 -0
- package/src/bench/quality.ts +214 -0
- package/src/bench/run.ts +103 -0
- package/src/curves.ts +89 -0
- package/src/fixtures.ts +181 -0
- package/src/geom.ts +177 -0
- package/src/grid.ts +116 -0
- package/src/index.ts +14 -0
- package/src/model.ts +138 -0
- package/src/plugs.ts +75 -0
- package/src/routers/corners.ts +38 -0
- package/src/routers/index.ts +27 -0
- package/src/routers/plugs.ts +25 -0
- package/src/routers/sections.ts +236 -0
- package/src/routers/shared.ts +223 -0
- package/src/routers/straight.ts +37 -0
- package/src/routers/trunks.ts +134 -0
- package/src/sections.ts +82 -0
- package/src/segments.ts +250 -0
- package/src/visibility.ts +325 -0
- package/src/ways.ts +67 -0
- package/README.md +0 -3
package/src/sections.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { center, type Box } from "./geom.ts";
|
|
2
|
+
import type { Node } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
/** A section of the plane: a cell of a quadtree, with the nodes whose centers are in it. */
|
|
5
|
+
export interface Section {
|
|
6
|
+
/** The path from the root: "s", then one digit from 0 to 3 for each step down. */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly depth: number;
|
|
9
|
+
readonly cell: Box;
|
|
10
|
+
/** The indices of the nodes, in the order of the graph. */
|
|
11
|
+
readonly nodes: readonly number[];
|
|
12
|
+
readonly children: readonly Section[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The options of the tree of sections. */
|
|
16
|
+
export interface SectionOptions {
|
|
17
|
+
/** A section with more nodes splits. The default is 8. */
|
|
18
|
+
readonly cap?: number;
|
|
19
|
+
/** The tree stops at this depth. The default is 16. */
|
|
20
|
+
readonly maxDepth?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
|
|
25
|
+
* When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
|
|
26
|
+
*/
|
|
27
|
+
export function buildSections(nodes: readonly Node[], options: SectionOptions = {}): Section {
|
|
28
|
+
const cap = Math.max(1, options.cap ?? 8);
|
|
29
|
+
const maxDepth = options.maxDepth ?? 16;
|
|
30
|
+
const centers = nodes.map((n) => center(n.box));
|
|
31
|
+
let x0 = Infinity;
|
|
32
|
+
let y0 = Infinity;
|
|
33
|
+
let x1 = -Infinity;
|
|
34
|
+
let y1 = -Infinity;
|
|
35
|
+
for (const n of nodes) {
|
|
36
|
+
x0 = Math.min(x0, n.box.x);
|
|
37
|
+
y0 = Math.min(y0, n.box.y);
|
|
38
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
39
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
40
|
+
}
|
|
41
|
+
const root: Box = nodes.length ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : { x: 0, y: 0, w: 0, h: 0 };
|
|
42
|
+
const make = (id: string, depth: number, cell: Box, list: readonly number[]): Section => {
|
|
43
|
+
let box = cell;
|
|
44
|
+
for (let guard = 0; list.length > cap && depth < maxDepth && guard < 32; guard++) {
|
|
45
|
+
const mx = box.x + box.w / 2;
|
|
46
|
+
const my = box.y + box.h / 2;
|
|
47
|
+
const parts: number[][] = [[], [], [], []];
|
|
48
|
+
for (const i of list) parts[(centers[i]!.x < mx ? 0 : 1) + (centers[i]!.y < my ? 0 : 2)]!.push(i);
|
|
49
|
+
const quarter = (q: number): Box => ({
|
|
50
|
+
x: q % 2 === 0 ? box.x : mx,
|
|
51
|
+
y: q < 2 ? box.y : my,
|
|
52
|
+
w: box.w / 2,
|
|
53
|
+
h: box.h / 2,
|
|
54
|
+
});
|
|
55
|
+
const full = parts.map((p, q) => [p, q] as const).filter(([p]) => p.length > 0);
|
|
56
|
+
if (full.length === 1) {
|
|
57
|
+
box = quarter(full[0]![1]);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const children = full.map(([p, q]) => make(`${id}${q}`, depth + 1, quarter(q), p));
|
|
61
|
+
return { id, depth, cell: box, nodes: list, children };
|
|
62
|
+
}
|
|
63
|
+
return { id, depth, cell: box, nodes: list, children: [] };
|
|
64
|
+
};
|
|
65
|
+
return make("s", 0, root, nodes.map((_, i) => i));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** This function gives the sections of the tree, parents before children. */
|
|
69
|
+
export function allSections(root: Section): Section[] {
|
|
70
|
+
const out: Section[] = [];
|
|
71
|
+
const walk = (s: Section): void => {
|
|
72
|
+
out.push(s);
|
|
73
|
+
for (const c of s.children) walk(c);
|
|
74
|
+
};
|
|
75
|
+
walk(root);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** This function gives the depth of the deepest section. */
|
|
80
|
+
export function treeDepth(root: Section): number {
|
|
81
|
+
return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
|
|
82
|
+
}
|
package/src/segments.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { dist, type Point } from "./geom.ts";
|
|
2
|
+
import type { Result, Route } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
/** A piece of drawn way. A shared segment carries several edges, in lane order. A private segment carries one edge. */
|
|
5
|
+
export interface Segment {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly points: readonly Point[];
|
|
8
|
+
/** The edges in lane order. Lane 0 is on the left of the segment, from its first point to its last. The y axis points down. */
|
|
9
|
+
readonly edges: readonly string[];
|
|
10
|
+
/** The number of edges. A renderer shows it on a fat edge. */
|
|
11
|
+
readonly count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One step of a path: a segment, the way through it, and the lane of the edge in it. */
|
|
15
|
+
export interface SegmentRef {
|
|
16
|
+
readonly segment: string;
|
|
17
|
+
/** This value is true when the edge goes from the last point of the segment to its first point. */
|
|
18
|
+
readonly reverse: boolean;
|
|
19
|
+
readonly lane: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The route of one edge as a list of segments. */
|
|
23
|
+
export interface Path {
|
|
24
|
+
readonly edge: string;
|
|
25
|
+
readonly segments: readonly SegmentRef[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The routes of a result as shared geometry. A renderer can draw each segment one time, with a count. */
|
|
29
|
+
export interface SegmentGraph {
|
|
30
|
+
readonly segments: readonly Segment[];
|
|
31
|
+
readonly paths: readonly Path[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SAME = 1e-6;
|
|
35
|
+
|
|
36
|
+
/** This function gives the index where `p` holds the points of `part` in a row, from `from` on, or -1. */
|
|
37
|
+
function indexOf(p: readonly Point[], part: readonly Point[], from: number): number {
|
|
38
|
+
for (let i = from; i + part.length <= p.length; i++) {
|
|
39
|
+
let ok = true;
|
|
40
|
+
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j]!, part[j]!) <= SAME;
|
|
41
|
+
if (ok) return i;
|
|
42
|
+
}
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This function breaks each route into segments. The runs and the trunks of the bundles become shared segments.
|
|
48
|
+
* The other parts of each route become private segments. The segments of a path, in order, give the points of its route again.
|
|
49
|
+
*/
|
|
50
|
+
export function segmentsOf(result: Result): SegmentGraph {
|
|
51
|
+
const shared = [
|
|
52
|
+
...(result.runs ?? []).map((r) => ({ id: r.id, points: r.points, edges: r.edges })),
|
|
53
|
+
...(result.bundles ?? []).map((b) => ({ id: b.id, points: b.trunk, edges: b.edges })),
|
|
54
|
+
].filter((s) => s.points.length >= 2);
|
|
55
|
+
const onEdge = new Map<string, { id: string; points: readonly Point[]; lane: number }[]>();
|
|
56
|
+
for (const s of shared) {
|
|
57
|
+
s.edges.forEach((edge, lane) => {
|
|
58
|
+
const list = onEdge.get(edge);
|
|
59
|
+
if (list) list.push({ id: s.id, points: s.points, lane });
|
|
60
|
+
else onEdge.set(edge, [{ id: s.id, points: s.points, lane }]);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const segments: Segment[] = shared.map((s) => ({ id: s.id, points: s.points, edges: s.edges, count: s.edges.length }));
|
|
64
|
+
const paths: Path[] = [];
|
|
65
|
+
for (const route of result.routes) {
|
|
66
|
+
const p = route.points;
|
|
67
|
+
// The place of each shared segment in the route, in the order of the route.
|
|
68
|
+
const found: { at: number; end: number; ref: SegmentRef }[] = [];
|
|
69
|
+
for (const s of onEdge.get(route.edge) ?? []) {
|
|
70
|
+
let at = indexOf(p, s.points, 0);
|
|
71
|
+
let reverse = false;
|
|
72
|
+
if (at < 0) {
|
|
73
|
+
at = indexOf(p, [...s.points].reverse(), 0);
|
|
74
|
+
reverse = true;
|
|
75
|
+
}
|
|
76
|
+
if (at >= 0) found.push({ at, end: at + s.points.length - 1, ref: { segment: s.id, reverse, lane: s.lane } });
|
|
77
|
+
}
|
|
78
|
+
found.sort((a, b) => a.at - b.at);
|
|
79
|
+
const refs: SegmentRef[] = [];
|
|
80
|
+
let i = 0;
|
|
81
|
+
const own = (to: number): void => {
|
|
82
|
+
if (to <= i) return;
|
|
83
|
+
const id = `${route.edge}/${refs.length}`;
|
|
84
|
+
segments.push({ id, points: p.slice(i, to + 1), edges: [route.edge], count: 1 });
|
|
85
|
+
refs.push({ segment: id, reverse: false, lane: 0 });
|
|
86
|
+
};
|
|
87
|
+
for (const f of found) {
|
|
88
|
+
if (f.at < i) continue;
|
|
89
|
+
own(f.at);
|
|
90
|
+
refs.push(f.ref);
|
|
91
|
+
i = f.end;
|
|
92
|
+
}
|
|
93
|
+
own(p.length - 1);
|
|
94
|
+
paths.push({ edge: route.edge, segments: refs });
|
|
95
|
+
}
|
|
96
|
+
return { segments, paths };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** This function gives the points of a path, with the segments in the order and the way of the path. */
|
|
100
|
+
export function pathPoints(graph: SegmentGraph, path: Path): Point[] {
|
|
101
|
+
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
102
|
+
const out: Point[] = [];
|
|
103
|
+
for (const ref of path.segments) {
|
|
104
|
+
const s = byId.get(ref.segment);
|
|
105
|
+
if (!s) continue;
|
|
106
|
+
const pts = ref.reverse ? [...s.points].reverse() : s.points;
|
|
107
|
+
for (const q of pts) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This function moves each point of a polyline by `d` to the left, in a frame with the y axis down.
|
|
114
|
+
* At an inner point, it moves along the bisector, at most 2 × `d` far.
|
|
115
|
+
*/
|
|
116
|
+
export function offsetPolyline(p: readonly Point[], d: number): Point[] {
|
|
117
|
+
if (d === 0 || p.length < 2) return [...p];
|
|
118
|
+
const normals: Point[] = [];
|
|
119
|
+
for (let i = 1; i < p.length; i++) {
|
|
120
|
+
const dx = p[i]!.x - p[i - 1]!.x;
|
|
121
|
+
const dy = p[i]!.y - p[i - 1]!.y;
|
|
122
|
+
const l = Math.hypot(dx, dy);
|
|
123
|
+
normals.push(l > 0 ? { x: dy / l, y: -dx / l } : (normals[normals.length - 1] ?? { x: 0, y: 0 }));
|
|
124
|
+
}
|
|
125
|
+
return p.map((q, i) => {
|
|
126
|
+
const a = normals[Math.max(0, i - 1)]!;
|
|
127
|
+
const b = normals[Math.min(normals.length - 1, i)]!;
|
|
128
|
+
const mx = a.x + b.x;
|
|
129
|
+
const my = a.y + b.y;
|
|
130
|
+
const ml = Math.hypot(mx, my);
|
|
131
|
+
if (ml < 1e-9) return { x: q.x + a.x * d, y: q.y + a.y * d };
|
|
132
|
+
const cos = (mx * a.x + my * a.y) / ml;
|
|
133
|
+
const k = Math.min(2, 1 / Math.max(cos, 1e-9)) * d;
|
|
134
|
+
return { x: q.x + (mx / ml) * k, y: q.y + (my / ml) * k };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The options of `pullApart`. */
|
|
139
|
+
export interface PullApartOptions {
|
|
140
|
+
/** The distance between two lanes. */
|
|
141
|
+
readonly spacing: number;
|
|
142
|
+
/** The largest width of a segment, from its outer lane on the left to its outer lane on the right. */
|
|
143
|
+
readonly maxWidth: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* This function pulls the lanes of each shared segment apart, and gives a route for each edge.
|
|
148
|
+
* The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
|
|
149
|
+
*/
|
|
150
|
+
export function pullApart(graph: SegmentGraph, options: PullApartOptions): Route[] {
|
|
151
|
+
return lanePaths(graph, (s) => (s.count > 1 ? Math.min(options.spacing, options.maxWidth / (s.count - 1)) : 0));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
|
|
155
|
+
export function runKey(edges: readonly string[]): string {
|
|
156
|
+
return [...edges].sort().join(",");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
|
|
161
|
+
* The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
|
|
162
|
+
*
|
|
163
|
+
* Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
|
|
164
|
+
* The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
|
|
165
|
+
* The key of a segment is `runKey` of its edges.
|
|
166
|
+
*/
|
|
167
|
+
export function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<string>, laneWidth: number, maxWidth: number = Infinity): Route[] {
|
|
168
|
+
const graph = "paths" in source ? source : segmentsOf(source);
|
|
169
|
+
if (open.size === 0) return graph.paths.map((path) => ({ edge: path.edge, points: pathPoints(graph, path) }));
|
|
170
|
+
return lanePaths(graph, (s) => (s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
|
|
174
|
+
function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Route[] {
|
|
175
|
+
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
176
|
+
const routes: Route[] = [];
|
|
177
|
+
for (const path of graph.paths) {
|
|
178
|
+
const ends = pathPoints(graph, path);
|
|
179
|
+
const out: Point[] = [];
|
|
180
|
+
for (const ref of path.segments) {
|
|
181
|
+
const s = byId.get(ref.segment);
|
|
182
|
+
if (!s) continue;
|
|
183
|
+
const spacing = spacingOf(s);
|
|
184
|
+
const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
|
|
185
|
+
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
|
|
186
|
+
}
|
|
187
|
+
if (out.length >= 2 && ends.length >= 2) {
|
|
188
|
+
out[0] = ends[0]!;
|
|
189
|
+
out[out.length - 1] = ends[ends.length - 1]!;
|
|
190
|
+
}
|
|
191
|
+
routes.push({ edge: path.edge, points: out });
|
|
192
|
+
}
|
|
193
|
+
return routes;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
|
|
197
|
+
function turn(from: Point, base: Point, to: Point): number {
|
|
198
|
+
const dx = base.x - from.x;
|
|
199
|
+
const dy = base.y - from.y;
|
|
200
|
+
const vx = to.x - base.x;
|
|
201
|
+
const vy = to.y - base.y;
|
|
202
|
+
return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* This function gives the lane order of the edges on a shared way `run` (metro-line order). Two routes go on together
|
|
207
|
+
* 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.
|
|
208
|
+
* 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.
|
|
209
|
+
* A route that does not hold the run goes last.
|
|
210
|
+
*/
|
|
211
|
+
export function orderLanes(run: readonly Point[], routes: readonly Route[]): string[] {
|
|
212
|
+
const placed: { edge: string; p: readonly Point[]; s: number; e: number }[] = [];
|
|
213
|
+
const lost: string[] = [];
|
|
214
|
+
for (const r of routes) {
|
|
215
|
+
let at = indexOf(r.points, run, 0);
|
|
216
|
+
if (at >= 0) placed.push({ edge: r.edge, p: r.points, s: at, e: at + run.length - 1 });
|
|
217
|
+
else {
|
|
218
|
+
const p = [...r.points].reverse();
|
|
219
|
+
at = indexOf(p, run, 0);
|
|
220
|
+
if (at >= 0) placed.push({ edge: r.edge, p, s: at, e: at + run.length - 1 });
|
|
221
|
+
else lost.push(r.edge);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const same = (a: Point, b: Point): boolean => dist(a, b) <= SAME;
|
|
225
|
+
placed.sort((P, Q) => {
|
|
226
|
+
for (let j = 1; ; j++) {
|
|
227
|
+
const a = P.p[P.e + j];
|
|
228
|
+
const b = Q.p[Q.e + j];
|
|
229
|
+
if (a === undefined || b === undefined) break;
|
|
230
|
+
if (!same(a, b)) {
|
|
231
|
+
const d = turn(P.p[P.e + j - 2]!, P.p[P.e + j - 1]!, a) - turn(P.p[P.e + j - 2]!, P.p[P.e + j - 1]!, b);
|
|
232
|
+
if (d !== 0) return d;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (let j = 1; ; j++) {
|
|
237
|
+
const a = P.p[P.s - j];
|
|
238
|
+
const b = Q.p[Q.s - j];
|
|
239
|
+
if (a === undefined || b === undefined) break;
|
|
240
|
+
if (!same(a, b)) {
|
|
241
|
+
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
242
|
+
const d = turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, b) - turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, a);
|
|
243
|
+
if (d !== 0) return d;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
|
|
248
|
+
});
|
|
249
|
+
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
250
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { dist, inflate, segmentHitsRect, type Point } from "./geom.ts";
|
|
2
|
+
import { Grid } from "./grid.ts";
|
|
3
|
+
import type { Node } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
/** The nodes as obstacles, with a grid for fast tests. */
|
|
6
|
+
export class Obstacles {
|
|
7
|
+
readonly #grid: Grid<Node>;
|
|
8
|
+
|
|
9
|
+
/** This constructor indexes the boxes of the nodes. */
|
|
10
|
+
constructor(nodes: readonly Node[], cell: number) {
|
|
11
|
+
this.#grid = new Grid<Node>(cell);
|
|
12
|
+
for (const n of nodes) this.#grid.add(n, n.box);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
16
|
+
clear(a: Point, b: Point, pad: number, skip?: ReadonlySet<Node>): boolean {
|
|
17
|
+
let ok = true;
|
|
18
|
+
this.#grid.querySegment(a, b, pad, (n) => {
|
|
19
|
+
if (ok && !skip?.has(n)) {
|
|
20
|
+
const x = n.box;
|
|
21
|
+
if (segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad)) ok = false;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return ok;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
28
|
+
clearPath(p: readonly Point[], pad: number, skip?: ReadonlySet<Node>): boolean {
|
|
29
|
+
for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1]!, p[i]!, pad, skip)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
|
|
34
|
+
inside(p: Point, pad: number, skip?: Node): boolean {
|
|
35
|
+
let hit = false;
|
|
36
|
+
this.#grid.query({ x: p.x, y: p.y, w: 0, h: 0 }, (n) => {
|
|
37
|
+
if (!hit && n !== skip) {
|
|
38
|
+
const b = n.box;
|
|
39
|
+
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;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return hit;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The options of the corner graph. */
|
|
47
|
+
export interface CornerGraphOptions {
|
|
48
|
+
/** The distance from a box to its corners. */
|
|
49
|
+
readonly margin: number;
|
|
50
|
+
/** The number of visible neighbours of each corner. */
|
|
51
|
+
readonly k: number;
|
|
52
|
+
/**
|
|
53
|
+
* The spacing of a lattice of free points over the plane, in units of the mean node size. The default is 3, and 0 turns it off.
|
|
54
|
+
* The lattice links groups of nodes across empty space. Without it, the nearest corners of a group are all in that group.
|
|
55
|
+
*/
|
|
56
|
+
readonly lattice?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
|
|
61
|
+
* Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
|
|
62
|
+
* A path goes from corner to corner around the boxes.
|
|
63
|
+
* The obstacle test uses half of the margin, so a path keeps a clearance of at least half of the margin.
|
|
64
|
+
*/
|
|
65
|
+
export class CornerGraph {
|
|
66
|
+
readonly points: Point[] = [];
|
|
67
|
+
readonly #adj: number[][] = [];
|
|
68
|
+
// The length of each link, in the order of #adj.
|
|
69
|
+
readonly #len: number[][] = [];
|
|
70
|
+
readonly #obstacles: Obstacles;
|
|
71
|
+
readonly #pointGrid: Grid<number>;
|
|
72
|
+
readonly #cell: number;
|
|
73
|
+
readonly #pad: number;
|
|
74
|
+
readonly #k: number;
|
|
75
|
+
// The work arrays of A*, made once and cleared by a stamp.
|
|
76
|
+
#g = new Float64Array(0);
|
|
77
|
+
#parent = new Int32Array(0);
|
|
78
|
+
#open = new Int32Array(0);
|
|
79
|
+
#closed = new Int32Array(0);
|
|
80
|
+
#stamp = 0;
|
|
81
|
+
// The stamp of the last call of nearestVisible that tested each corner.
|
|
82
|
+
#tested = new Int32Array(0);
|
|
83
|
+
#testStamp = 0;
|
|
84
|
+
|
|
85
|
+
/** This constructor makes the graph of the nodes. */
|
|
86
|
+
constructor(nodes: readonly Node[], obstacles: Obstacles, options: CornerGraphOptions) {
|
|
87
|
+
this.#obstacles = obstacles;
|
|
88
|
+
this.#pad = options.margin / 2;
|
|
89
|
+
this.#k = options.k;
|
|
90
|
+
let size = 0;
|
|
91
|
+
for (const n of nodes) size += (n.box.w + n.box.h) / 2;
|
|
92
|
+
this.#cell = Math.max(1, size / Math.max(1, nodes.length) + 2 * options.margin);
|
|
93
|
+
this.#pointGrid = new Grid<number>(this.#cell);
|
|
94
|
+
for (const n of nodes) {
|
|
95
|
+
const b = inflate(n.box, options.margin);
|
|
96
|
+
for (const c of [
|
|
97
|
+
{ x: b.x, y: b.y },
|
|
98
|
+
{ x: b.x + b.w, y: b.y },
|
|
99
|
+
{ x: b.x + b.w, y: b.y + b.h },
|
|
100
|
+
{ x: b.x, y: b.y + b.h },
|
|
101
|
+
]) {
|
|
102
|
+
if (!obstacles.inside(c, this.#pad)) this.#addPoint(c);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const spacing = (options.lattice ?? 3) * this.#cell;
|
|
106
|
+
if (spacing > 0 && nodes.length > 0) {
|
|
107
|
+
let x0 = Infinity;
|
|
108
|
+
let y0 = Infinity;
|
|
109
|
+
let x1 = -Infinity;
|
|
110
|
+
let y1 = -Infinity;
|
|
111
|
+
for (const n of nodes) {
|
|
112
|
+
x0 = Math.min(x0, n.box.x);
|
|
113
|
+
y0 = Math.min(y0, n.box.y);
|
|
114
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
115
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
116
|
+
}
|
|
117
|
+
for (let x = x0 - spacing / 2; x <= x1 + spacing; x += spacing) {
|
|
118
|
+
for (let y = y0 - spacing / 2; y <= y1 + spacing; y += spacing) {
|
|
119
|
+
const c = { x, y };
|
|
120
|
+
if (!obstacles.inside(c, options.margin)) this.#addPoint(c);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const links: Set<number>[] = this.points.map(() => new Set<number>());
|
|
125
|
+
for (let i = 0; i < this.points.length; i++) {
|
|
126
|
+
for (const j of this.nearestVisible(this.points[i]!, this.#k, i)) {
|
|
127
|
+
links[i]!.add(j);
|
|
128
|
+
links[j]!.add(i);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
links.forEach((s, i) => {
|
|
132
|
+
const adj = [...s].sort((a, b) => a - b);
|
|
133
|
+
this.#adj.push(adj);
|
|
134
|
+
this.#len.push(adj.map((j) => dist(this.points[i]!, this.points[j]!)));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#addPoint(c: Point): void {
|
|
139
|
+
this.#pointGrid.add(this.points.length, { x: c.x, y: c.y, w: 0, h: 0 });
|
|
140
|
+
this.points.push(c);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** This method gives the number of links of the graph. */
|
|
144
|
+
get linkCount(): number {
|
|
145
|
+
let n = 0;
|
|
146
|
+
for (const a of this.#adj) n += a.length;
|
|
147
|
+
return n / 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** This method gives the index of the corner nearest to `p`, seen or not, or -1 when the graph has no corner near it. */
|
|
151
|
+
nearest(p: Point): number {
|
|
152
|
+
for (let r = this.#cell; r <= this.#cell * 64; r *= 2) {
|
|
153
|
+
let best = -1;
|
|
154
|
+
let bestD = Infinity;
|
|
155
|
+
this.#pointGrid.query({ x: p.x - r, y: p.y - r, w: 2 * r, h: 2 * r }, (j) => {
|
|
156
|
+
const d = dist(p, this.points[j]!);
|
|
157
|
+
if (d < bestD || (d === bestD && j < best)) {
|
|
158
|
+
bestD = d;
|
|
159
|
+
best = j;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// A corner beyond r can be nearer than a corner in a corner of the square. Within r, none is.
|
|
163
|
+
if (best >= 0 && bestD <= r) return best;
|
|
164
|
+
}
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
|
|
169
|
+
nearestVisible(p: Point, k: number, self = -1): number[] {
|
|
170
|
+
const out: number[] = [];
|
|
171
|
+
if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
|
|
172
|
+
const tested = this.#tested;
|
|
173
|
+
const stamp = ++this.#testStamp;
|
|
174
|
+
for (let r = this.#cell; r <= this.#cell * 8; r *= 2) {
|
|
175
|
+
const cands: [number, number][] = [];
|
|
176
|
+
this.#pointGrid.query({ x: p.x - r, y: p.y - r, w: 2 * r, h: 2 * r }, (j) => {
|
|
177
|
+
if (j !== self && tested[j] !== stamp) cands.push([dist(p, this.points[j]!), j]);
|
|
178
|
+
});
|
|
179
|
+
cands.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
180
|
+
for (const [d, j] of cands) {
|
|
181
|
+
// A corner beyond r can be nearer than a corner of the next ring, so it waits for that ring.
|
|
182
|
+
if (d > r) break;
|
|
183
|
+
tested[j] = stamp;
|
|
184
|
+
if (this.#obstacles.clear(p, this.points[j]!, this.#pad)) out.push(j);
|
|
185
|
+
if (out.length >= k) return out;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* This method gives a shortest path from `from` to `to` in the graph (A*), with the two points as its ends.
|
|
193
|
+
* It gives `undefined` when the graph links no corner near `from` to a corner near `to`.
|
|
194
|
+
* The function `cost` can change the cost of the link from corner `i` to corner `j`, of length `d`.
|
|
195
|
+
* It must give at least `floor` × `d`, so that the estimate of A* stays below the true cost.
|
|
196
|
+
*/
|
|
197
|
+
path(from: Point, to: Point, cost?: (i: number, j: number, d: number) => number, floor: number = 1): Point[] | undefined {
|
|
198
|
+
if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
|
|
199
|
+
const n = this.points.length;
|
|
200
|
+
if (this.#g.length < n) {
|
|
201
|
+
this.#g = new Float64Array(n);
|
|
202
|
+
this.#parent = new Int32Array(n);
|
|
203
|
+
this.#open = new Int32Array(n);
|
|
204
|
+
this.#closed = new Int32Array(n);
|
|
205
|
+
}
|
|
206
|
+
const stamp = ++this.#stamp;
|
|
207
|
+
const goal = new Map<number, number>();
|
|
208
|
+
for (const j of this.nearestVisible(to, this.#k * 2)) goal.set(j, dist(this.points[j]!, to));
|
|
209
|
+
if (goal.size === 0) return undefined;
|
|
210
|
+
const heap = new MinHeap();
|
|
211
|
+
const h = (i: number): number => floor * dist(this.points[i]!, to);
|
|
212
|
+
const relax = (j: number, g: number, parent: number): void => {
|
|
213
|
+
if (this.#closed[j] === stamp) return;
|
|
214
|
+
if (this.#open[j] === stamp && this.#g[j]! <= g) return;
|
|
215
|
+
this.#open[j] = stamp;
|
|
216
|
+
this.#g[j] = g;
|
|
217
|
+
this.#parent[j] = parent;
|
|
218
|
+
heap.push(g + h(j), j);
|
|
219
|
+
};
|
|
220
|
+
for (const j of this.nearestVisible(from, this.#k * 2)) relax(j, dist(from, this.points[j]!), -1);
|
|
221
|
+
let best = Infinity;
|
|
222
|
+
let bestEnd = -1;
|
|
223
|
+
while (heap.size > 0) {
|
|
224
|
+
if (heap.topKey >= best) break;
|
|
225
|
+
const i = heap.pop();
|
|
226
|
+
if (this.#closed[i] === stamp) continue;
|
|
227
|
+
this.#closed[i] = stamp;
|
|
228
|
+
const gi = this.#g[i]!;
|
|
229
|
+
const tail = goal.get(i);
|
|
230
|
+
if (tail !== undefined && gi + tail < best) {
|
|
231
|
+
best = gi + tail;
|
|
232
|
+
bestEnd = i;
|
|
233
|
+
}
|
|
234
|
+
const adj = this.#adj[i]!;
|
|
235
|
+
const len = this.#len[i]!;
|
|
236
|
+
for (let m = 0; m < adj.length; m++) {
|
|
237
|
+
const j = adj[m]!;
|
|
238
|
+
const d = len[m]!;
|
|
239
|
+
relax(j, gi + (cost ? cost(i, j, d) : d), i);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (bestEnd < 0) return undefined;
|
|
243
|
+
const out: Point[] = [to];
|
|
244
|
+
for (let i = bestEnd; i >= 0; i = this.#parent[i]!) out.push(this.points[i]!);
|
|
245
|
+
out.push(from);
|
|
246
|
+
return out.reverse();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** This method removes the vertices that a later vertex makes needless: from each vertex, it goes to the farthest vertex in view. */
|
|
250
|
+
pull(p: readonly Point[], lookahead = 16): Point[] {
|
|
251
|
+
if (p.length < 3) return [...p];
|
|
252
|
+
const out: Point[] = [p[0]!];
|
|
253
|
+
let i = 0;
|
|
254
|
+
while (i < p.length - 1) {
|
|
255
|
+
let j = Math.min(p.length - 1, i + lookahead);
|
|
256
|
+
while (j > i + 1 && !this.#obstacles.clear(p[i]!, p[j]!, this.#pad)) j--;
|
|
257
|
+
out.push(p[j]!);
|
|
258
|
+
i = j;
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A binary min-heap of (key, value) pairs. Equal keys come out by the smaller value, so the order is deterministic. */
|
|
265
|
+
class MinHeap {
|
|
266
|
+
readonly #k: number[] = [];
|
|
267
|
+
readonly #v: number[] = [];
|
|
268
|
+
|
|
269
|
+
get size(): number {
|
|
270
|
+
return this.#k.length;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#less(a: number, b: number): boolean {
|
|
274
|
+
return this.#k[a]! < this.#k[b]! || (this.#k[a] === this.#k[b] && this.#v[a]! < this.#v[b]!);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#swap(a: number, b: number): void {
|
|
278
|
+
const k = this.#k[a]!;
|
|
279
|
+
this.#k[a] = this.#k[b]!;
|
|
280
|
+
this.#k[b] = k;
|
|
281
|
+
const v = this.#v[a]!;
|
|
282
|
+
this.#v[a] = this.#v[b]!;
|
|
283
|
+
this.#v[b] = v;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The smallest key. */
|
|
287
|
+
get topKey(): number {
|
|
288
|
+
return this.#k[0]!;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
push(key: number, value: number): void {
|
|
292
|
+
this.#k.push(key);
|
|
293
|
+
this.#v.push(value);
|
|
294
|
+
let i = this.#k.length - 1;
|
|
295
|
+
while (i > 0) {
|
|
296
|
+
const p = (i - 1) >> 1;
|
|
297
|
+
if (!this.#less(i, p)) break;
|
|
298
|
+
this.#swap(i, p);
|
|
299
|
+
i = p;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** This method removes the pair with the smallest key and gives its value. */
|
|
304
|
+
pop(): number {
|
|
305
|
+
const top = this.#v[0]!;
|
|
306
|
+
const k = this.#k.pop()!;
|
|
307
|
+
const v = this.#v.pop()!;
|
|
308
|
+
if (this.#k.length > 0) {
|
|
309
|
+
this.#k[0] = k;
|
|
310
|
+
this.#v[0] = v;
|
|
311
|
+
let i = 0;
|
|
312
|
+
for (;;) {
|
|
313
|
+
const l = 2 * i + 1;
|
|
314
|
+
const r = l + 1;
|
|
315
|
+
let m = i;
|
|
316
|
+
if (l < this.#k.length && this.#less(l, m)) m = l;
|
|
317
|
+
if (r < this.#k.length && this.#less(r, m)) m = r;
|
|
318
|
+
if (m === i) break;
|
|
319
|
+
this.#swap(i, m);
|
|
320
|
+
i = m;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return top;
|
|
324
|
+
}
|
|
325
|
+
}
|