@mark1russell7/highways 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/build/index.d.ts +468 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1882 -255
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/curves.ts +98 -0
- package/src/bench/links.ts +191 -0
- package/src/bench/room.ts +122 -0
- package/src/bench/run.ts +28 -0
- package/src/cases.ts +139 -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/grid.ts +61 -2
- package/src/index.ts +6 -0
- package/src/levels.ts +151 -14
- package/src/room/index.ts +334 -0
- package/src/routers/sections.ts +33 -6
- package/src/visibility.ts +334 -92
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { distinctPoints, type CurveSegment } from "../model.ts";
|
|
3
|
+
import { fitSize, type CurveSpace } from "../space.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of `arcCurve`. */
|
|
6
|
+
export interface ArcOptions {
|
|
7
|
+
/** The largest radius of the arc at a corner. The default is 12. */
|
|
8
|
+
readonly radius?: number;
|
|
9
|
+
/** The boxes that the curve must not go through. Without it, each corner gets the largest radius that fits on its legs. */
|
|
10
|
+
readonly space?: CurveSpace;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle stays sharp. */
|
|
14
|
+
const FLAT = 1e-6;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This function puts a circular arc at each inner vertex of a polyline. The arc is tangent to the two legs of its corner.
|
|
18
|
+
* The tangent points stay on the first half of each leg, so the ends of the polyline stay.
|
|
19
|
+
* With `space`, the function makes the radius of a corner smaller until the arc goes through no box that the polyline avoids.
|
|
20
|
+
* On a polyline with only horizontal and vertical legs, each arc is a quarter circle.
|
|
21
|
+
*/
|
|
22
|
+
export function arcCurve(points: readonly Point[], options: ArcOptions = {}): CurveSegment[] {
|
|
23
|
+
const p = distinctPoints(points);
|
|
24
|
+
if (p.length === 0) return [];
|
|
25
|
+
const radius = options.radius ?? 12;
|
|
26
|
+
const out: CurveSegment[] = [{ kind: "move", to: p[0]! }];
|
|
27
|
+
for (let i = 1; i + 1 < p.length; i++) {
|
|
28
|
+
const a = p[i - 1]!;
|
|
29
|
+
const v = p[i]!;
|
|
30
|
+
const b = p[i + 1]!;
|
|
31
|
+
const la = dist(a, v);
|
|
32
|
+
const lb = dist(v, b);
|
|
33
|
+
const ux = (v.x - a.x) / la;
|
|
34
|
+
const uy = (v.y - a.y) / la;
|
|
35
|
+
const wx = (b.x - v.x) / lb;
|
|
36
|
+
const wy = (b.y - v.y) / lb;
|
|
37
|
+
const turn = Math.atan2(ux * wy - uy * wx, ux * wx + uy * wy);
|
|
38
|
+
if (Math.abs(turn) < FLAT || Math.PI - Math.abs(turn) < FLAT) {
|
|
39
|
+
out.push({ kind: "line", to: v });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// The tangent points are r·tan(|turn|/2) from the vertex.
|
|
43
|
+
const k = Math.tan(Math.abs(turn) / 2);
|
|
44
|
+
const side = Math.sign(turn);
|
|
45
|
+
const ends = (r: number): [Point, Point] => [
|
|
46
|
+
{ x: v.x - ux * r * k, y: v.y - uy * r * k },
|
|
47
|
+
{ x: v.x + wx * r * k, y: v.y + wy * r * k },
|
|
48
|
+
];
|
|
49
|
+
const piece = (r: number): CurveSegment[] => {
|
|
50
|
+
const [p1, p2] = ends(r);
|
|
51
|
+
// The center is on the side of the turn, at the distance r from the first leg.
|
|
52
|
+
const center = { x: p1.x - side * uy * r, y: p1.y + side * ux * r };
|
|
53
|
+
return [{ kind: "arc", center, radius: r, start: Math.atan2(p1.y - center.y, p1.x - center.x), sweep: turn, to: p2 }];
|
|
54
|
+
};
|
|
55
|
+
const r = fitSize(
|
|
56
|
+
options.space,
|
|
57
|
+
Math.min(radius, Math.min(la, lb) / (2 * k)),
|
|
58
|
+
(s) => [ends(s)[0], v, ends(s)[1]],
|
|
59
|
+
(s) => [{ kind: "move", to: ends(s)[0] }, ...piece(s)],
|
|
60
|
+
);
|
|
61
|
+
if (r <= 0) {
|
|
62
|
+
out.push({ kind: "line", to: v });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const [p1] = ends(r);
|
|
66
|
+
if (dist(p1, out[out.length - 1]!.to) > 1e-9) out.push({ kind: "line", to: p1 });
|
|
67
|
+
out.push(...piece(r));
|
|
68
|
+
}
|
|
69
|
+
if (p.length > 1) out.push({ kind: "line", to: p[p.length - 1]! });
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Point } from "../geom.ts";
|
|
2
|
+
import { arcCurve } from "./arc/index.ts";
|
|
3
|
+
import type { CurveSegment, CurveType } from "./model.ts";
|
|
4
|
+
import { roundedCurve } from "./rounded/index.ts";
|
|
5
|
+
import type { CurveSpace } from "./space.ts";
|
|
6
|
+
import { splineCurve } from "./spline/index.ts";
|
|
7
|
+
import { straightCurve } from "./straight/index.ts";
|
|
8
|
+
|
|
9
|
+
export * from "./model.ts";
|
|
10
|
+
export { cubicAt, sampleCurve, toSvgPath } from "./sample.ts";
|
|
11
|
+
export { curveSpace, type CurveSpace } from "./space.ts";
|
|
12
|
+
export { straightCurve } from "./straight/index.ts";
|
|
13
|
+
export { roundedCurve, type RoundedOptions } from "./rounded/index.ts";
|
|
14
|
+
export { splineCurve, type SplineOptions } from "./spline/index.ts";
|
|
15
|
+
export { arcCurve, type ArcOptions } from "./arc/index.ts";
|
|
16
|
+
|
|
17
|
+
/** The options of `curveOf`. Each type reads only the options that it uses. */
|
|
18
|
+
export interface CurveOptions {
|
|
19
|
+
readonly type: CurveType;
|
|
20
|
+
/** The types `rounded` and `arc` read this radius of the corners. The default is 12. */
|
|
21
|
+
readonly radius?: number;
|
|
22
|
+
/** The type `spline` reads this tension, from 0 to 1. The default is 0. */
|
|
23
|
+
readonly tension?: number;
|
|
24
|
+
/** The type `spline` leaves the first point along this outward normal of its plug. */
|
|
25
|
+
readonly startNormal?: Point;
|
|
26
|
+
/** The type `spline` comes into the last point against this outward normal of its plug. */
|
|
27
|
+
readonly endNormal?: Point;
|
|
28
|
+
/** The boxes that the curve must not go through, from `curveSpace`. The type `straight` does not read it. */
|
|
29
|
+
readonly space?: CurveSpace;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This function gives the curve of a route: a polyline of a router, a lane of `laneRoutes` or a link of a zoom level.
|
|
34
|
+
* The curve starts at the first point and ends at the last point of the polyline.
|
|
35
|
+
* The output is a list of segments for any renderer. `toSvgPath` writes it as SVG path data, and `sampleCurve` gives points.
|
|
36
|
+
*/
|
|
37
|
+
export function curveOf(points: readonly Point[], options: CurveOptions): CurveSegment[] {
|
|
38
|
+
switch (options.type) {
|
|
39
|
+
case "straight":
|
|
40
|
+
return straightCurve(points);
|
|
41
|
+
case "rounded":
|
|
42
|
+
return roundedCurve(points, options);
|
|
43
|
+
case "spline":
|
|
44
|
+
return splineCurve(points, options);
|
|
45
|
+
case "arc":
|
|
46
|
+
return arcCurve(points, options);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Point } from "../geom.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type of curve for a route.
|
|
5
|
+
*
|
|
6
|
+
* - `straight`: the polyline as it is.
|
|
7
|
+
* - `rounded`: a quadratic Bezier curve at each corner, as `roundCorners` draws it.
|
|
8
|
+
* - `spline`: a centripetal Catmull-Rom spline through the points. It does not overshoot or make loops.
|
|
9
|
+
* - `arc`: a circular arc at each corner, tangent to the two legs.
|
|
10
|
+
*/
|
|
11
|
+
export type CurveType = "straight" | "rounded" | "spline" | "arc";
|
|
12
|
+
|
|
13
|
+
/** The curve types, in the order of the benchmark. */
|
|
14
|
+
export const CURVE_TYPES: readonly CurveType[] = ["straight", "rounded", "spline", "arc"];
|
|
15
|
+
|
|
16
|
+
/** The start of a curve: the pen goes to `to` and draws nothing. */
|
|
17
|
+
export interface MoveSegment {
|
|
18
|
+
readonly kind: "move";
|
|
19
|
+
readonly to: Point;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A straight line from the end of the segment before to `to`. */
|
|
23
|
+
export interface LineSegment {
|
|
24
|
+
readonly kind: "line";
|
|
25
|
+
readonly to: Point;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A cubic Bezier curve from the end of the segment before to `to`, with the control points `c1` and `c2`. */
|
|
29
|
+
export interface CubicSegment {
|
|
30
|
+
readonly kind: "cubic";
|
|
31
|
+
readonly c1: Point;
|
|
32
|
+
readonly c2: Point;
|
|
33
|
+
readonly to: Point;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A circular arc from the end of the segment before to `to`. The arc has its center at `center` and the radius `radius`.
|
|
38
|
+
* It starts at the angle `start` and turns by `sweep` radians. A positive `sweep` turns from the x axis to the y axis.
|
|
39
|
+
* On a canvas, `ctx.arc(center.x, center.y, radius, start, start + sweep, sweep < 0)` draws it.
|
|
40
|
+
*/
|
|
41
|
+
export interface ArcSegment {
|
|
42
|
+
readonly kind: "arc";
|
|
43
|
+
readonly center: Point;
|
|
44
|
+
readonly radius: number;
|
|
45
|
+
readonly start: number;
|
|
46
|
+
readonly sweep: number;
|
|
47
|
+
readonly to: Point;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A segment of a curve. A curve is a list of segments: one `move` first, then lines, cubic curves and arcs.
|
|
52
|
+
* A renderer maps the list to SVG path data, to canvas calls or to the vertices of a mesh.
|
|
53
|
+
*/
|
|
54
|
+
export type CurveSegment = MoveSegment | LineSegment | CubicSegment | ArcSegment;
|
|
55
|
+
|
|
56
|
+
/** This function gives the points of a polyline without the points that are nearer than `eps` to the point before them. */
|
|
57
|
+
export function distinctPoints(points: readonly Point[], eps = 1e-9): Point[] {
|
|
58
|
+
const out: Point[] = [];
|
|
59
|
+
for (const p of points) {
|
|
60
|
+
const q = out[out.length - 1];
|
|
61
|
+
if (q === undefined || Math.hypot(p.x - q.x, p.y - q.y) > eps) out.push(p);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { distinctPoints, type CurveSegment } from "../model.ts";
|
|
3
|
+
import { fitSize, type CurveSpace } from "../space.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of `roundedCurve`. */
|
|
6
|
+
export interface RoundedOptions {
|
|
7
|
+
/** The largest distance from a corner to the start and to the end of its curve. The default is 12. */
|
|
8
|
+
readonly radius?: number;
|
|
9
|
+
/** The boxes that the curve must not go through. Without it, each corner gets the full radius. */
|
|
10
|
+
readonly space?: CurveSpace;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This function rounds each inner vertex of a polyline with a quadratic Bezier curve, as `roundCorners` does.
|
|
15
|
+
* 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.
|
|
16
|
+
* The function gives each quadratic curve as the same cubic curve. With `space`, it makes the radius of a corner smaller until the curve
|
|
17
|
+
* goes through no box that the polyline avoids.
|
|
18
|
+
*/
|
|
19
|
+
export function roundedCurve(points: readonly Point[], options: RoundedOptions = {}): CurveSegment[] {
|
|
20
|
+
const p = distinctPoints(points);
|
|
21
|
+
if (p.length === 0) return [];
|
|
22
|
+
const radius = options.radius ?? 12;
|
|
23
|
+
const out: CurveSegment[] = [{ kind: "move", to: p[0]! }];
|
|
24
|
+
for (let i = 1; i + 1 < p.length; i++) {
|
|
25
|
+
const a = p[i - 1]!;
|
|
26
|
+
const v = p[i]!;
|
|
27
|
+
const b = p[i + 1]!;
|
|
28
|
+
const la = dist(a, v);
|
|
29
|
+
const lb = dist(v, b);
|
|
30
|
+
const ends = (r: number): [Point, Point] => [
|
|
31
|
+
{ x: v.x + ((a.x - v.x) * r) / la, y: v.y + ((a.y - v.y) * r) / la },
|
|
32
|
+
{ x: v.x + ((b.x - v.x) * r) / lb, y: v.y + ((b.y - v.y) * r) / lb },
|
|
33
|
+
];
|
|
34
|
+
const piece = (r: number): CurveSegment[] => {
|
|
35
|
+
const [p1, p2] = ends(r);
|
|
36
|
+
// A quadratic curve with the control point v is the cubic curve with the control points 2/3 of the way to v.
|
|
37
|
+
return [{ kind: "cubic", c1: { x: p1.x + (2 * (v.x - p1.x)) / 3, y: p1.y + (2 * (v.y - p1.y)) / 3 }, c2: { x: p2.x + (2 * (v.x - p2.x)) / 3, y: p2.y + (2 * (v.y - p2.y)) / 3 }, to: p2 }];
|
|
38
|
+
};
|
|
39
|
+
const r = fitSize(
|
|
40
|
+
options.space,
|
|
41
|
+
Math.min(radius, la / 2, lb / 2),
|
|
42
|
+
(s) => [ends(s)[0], v, ends(s)[1]],
|
|
43
|
+
(s) => [{ kind: "move", to: ends(s)[0] }, ...piece(s)],
|
|
44
|
+
);
|
|
45
|
+
if (r <= 0) {
|
|
46
|
+
out.push({ kind: "line", to: v });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const [p1] = ends(r);
|
|
50
|
+
if (dist(p1, at(out)) > 1e-9) out.push({ kind: "line", to: p1 });
|
|
51
|
+
out.push(...piece(r));
|
|
52
|
+
}
|
|
53
|
+
if (p.length > 1) out.push({ kind: "line", to: p[p.length - 1]! });
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** This function gives the end of the last segment. */
|
|
58
|
+
function at(segments: readonly CurveSegment[]): Point {
|
|
59
|
+
return segments[segments.length - 1]!.to;
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Point } from "../geom.ts";
|
|
2
|
+
import type { CurveSegment } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
/** The largest number of points that `sampleCurve` gives for one segment. */
|
|
5
|
+
const MAX_STEPS = 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This function gives a polyline that follows the curve. The distance from each part of the curve to the polyline is `tolerance` or less.
|
|
9
|
+
* The polyline starts at the point of the `move` and goes through the end of each segment.
|
|
10
|
+
* Hit tests, measures and renderers that draw only lines use this polyline.
|
|
11
|
+
*/
|
|
12
|
+
export function sampleCurve(segments: readonly CurveSegment[], tolerance = 0.5): Point[] {
|
|
13
|
+
const tol = Math.max(1e-6, tolerance);
|
|
14
|
+
const out: Point[] = [];
|
|
15
|
+
let at: Point | undefined;
|
|
16
|
+
for (const s of segments) {
|
|
17
|
+
if (s.kind === "move" || at === undefined) {
|
|
18
|
+
out.push(s.to);
|
|
19
|
+
at = s.to;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (s.kind === "cubic") {
|
|
23
|
+
// The chord of a step of 1/n of the parameter is within 3·D/(4·n²) of the curve. D is the largest second difference of the control points.
|
|
24
|
+
const d = Math.max(
|
|
25
|
+
Math.hypot(at.x - 2 * s.c1.x + s.c2.x, at.y - 2 * s.c1.y + s.c2.y),
|
|
26
|
+
Math.hypot(s.c1.x - 2 * s.c2.x + s.to.x, s.c1.y - 2 * s.c2.y + s.to.y),
|
|
27
|
+
);
|
|
28
|
+
const n = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.sqrt((3 * d) / (4 * tol)))));
|
|
29
|
+
for (let k = 1; k < n; k++) out.push(cubicAt(at, s.c1, s.c2, s.to, k / n));
|
|
30
|
+
} else if (s.kind === "arc") {
|
|
31
|
+
// The chord of an angle φ is within r·(1 - cos(φ/2)) of the arc.
|
|
32
|
+
const step = s.radius <= tol ? Math.PI : 2 * Math.acos(1 - tol / s.radius);
|
|
33
|
+
const n = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.abs(s.sweep) / step)));
|
|
34
|
+
for (let k = 1; k < n; k++) {
|
|
35
|
+
const a = s.start + (s.sweep * k) / n;
|
|
36
|
+
out.push({ x: s.center.x + s.radius * Math.cos(a), y: s.center.y + s.radius * Math.sin(a) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
out.push(s.to);
|
|
40
|
+
at = s.to;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** This function gives the point at the parameter `t` of the cubic Bezier curve with the control points `p0`, `p1`, `p2` and `p3`. */
|
|
46
|
+
export function cubicAt(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
|
|
47
|
+
const u = 1 - t;
|
|
48
|
+
const a = u * u * u;
|
|
49
|
+
const b = 3 * u * u * t;
|
|
50
|
+
const c = 3 * u * t * t;
|
|
51
|
+
const d = t * t * t;
|
|
52
|
+
return { x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, y: a * p0.y + b * p1.y + c * p2.y + d * p3.y };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This function writes the segments as the data of an SVG path: the commands `M`, `L`, `C` and `A`.
|
|
57
|
+
* Each number has at most `digits` digits after the point.
|
|
58
|
+
*/
|
|
59
|
+
export function toSvgPath(segments: readonly CurveSegment[], digits = 2): string {
|
|
60
|
+
const n = (v: number): string => {
|
|
61
|
+
const s = String(Number(v.toFixed(digits)));
|
|
62
|
+
return s === "-0" ? "0" : s;
|
|
63
|
+
};
|
|
64
|
+
const p = (q: Point): string => `${n(q.x)} ${n(q.y)}`;
|
|
65
|
+
const parts: string[] = [];
|
|
66
|
+
for (const s of segments) {
|
|
67
|
+
if (s.kind === "move") parts.push(`M ${p(s.to)}`);
|
|
68
|
+
else if (s.kind === "line") parts.push(`L ${p(s.to)}`);
|
|
69
|
+
else if (s.kind === "cubic") parts.push(`C ${p(s.c1)} ${p(s.c2)} ${p(s.to)}`);
|
|
70
|
+
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)}`);
|
|
71
|
+
}
|
|
72
|
+
return parts.join(" ");
|
|
73
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { TOLERANCE } from "../bench/properties.ts";
|
|
2
|
+
import { inflate, segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
3
|
+
import { Grid } from "../grid.ts";
|
|
4
|
+
import type { CurveSegment } from "./model.ts";
|
|
5
|
+
import { sampleCurve } from "./sample.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The free space of a board: the boxes that a curve must not go through.
|
|
9
|
+
* Make it one time for a board with `curveSpace`, and give it to each call of `curveOf`.
|
|
10
|
+
*/
|
|
11
|
+
export interface CurveSpace {
|
|
12
|
+
/** The boxes of the board. */
|
|
13
|
+
readonly boxes: readonly Box[];
|
|
14
|
+
/** The tolerance of the samples of a curve, in units of the plane. */
|
|
15
|
+
readonly tolerance: number;
|
|
16
|
+
/**
|
|
17
|
+
* This method tells if the polyline `q` goes through a box that the polyline `p` does not go through.
|
|
18
|
+
* A polyline goes through a box when a part of it longer than `TOLERANCE` is inside the box made smaller by `TOLERANCE`.
|
|
19
|
+
*/
|
|
20
|
+
addsHit(q: readonly Point[], p: readonly Point[]): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function makes the free space of a board from its boxes. The boxes are the nodes, or the items of a zoom level.
|
|
25
|
+
* The curves sample each piece within `tolerance` of the curve for the test.
|
|
26
|
+
*/
|
|
27
|
+
export function curveSpace(boxes: readonly Box[], tolerance = 0.25): CurveSpace {
|
|
28
|
+
const inner = boxes.map((b) => inflate(b, -TOLERANCE));
|
|
29
|
+
let size = 0;
|
|
30
|
+
for (const b of boxes) size += Math.max(b.w, b.h);
|
|
31
|
+
// The buckets grow with the boxes, as the buckets of `measureLinks` do.
|
|
32
|
+
const grid = new Grid<number>(Math.max(180, (2 * size) / Math.max(1, boxes.length)));
|
|
33
|
+
inner.forEach((b, i) => grid.add(i, b));
|
|
34
|
+
const hits = (p: readonly Point[]): Set<number> => {
|
|
35
|
+
const out = new Set<number>();
|
|
36
|
+
for (let i = 1; i < p.length; i++) {
|
|
37
|
+
const a = p[i - 1]!;
|
|
38
|
+
const b = p[i]!;
|
|
39
|
+
grid.querySegment(a, b, 0, (id) => {
|
|
40
|
+
if (!out.has(id) && segmentHitsBox(a, b, inner[id]!, TOLERANCE)) out.add(id);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
boxes,
|
|
47
|
+
tolerance,
|
|
48
|
+
addsHit(q: readonly Point[], p: readonly Point[]): boolean {
|
|
49
|
+
let before: Set<number> | undefined;
|
|
50
|
+
for (let i = 1; i < q.length; i++) {
|
|
51
|
+
const a = q[i - 1]!;
|
|
52
|
+
const b = q[i]!;
|
|
53
|
+
const hit = grid.someSegment(a, b, 0, (id) => {
|
|
54
|
+
if (!segmentHitsBox(a, b, inner[id]!, TOLERANCE)) return false;
|
|
55
|
+
before ??= hits(p);
|
|
56
|
+
return !before.has(id);
|
|
57
|
+
});
|
|
58
|
+
if (hit) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This function finds the largest size from `max` down to 0 that keeps a piece of a curve clear.
|
|
67
|
+
* A clear piece goes through no box that its polyline avoids.
|
|
68
|
+
*
|
|
69
|
+
* The function `piece` gives the segments of the piece at a size. They start with a `move`.
|
|
70
|
+
* The polyline `chord` is the part of the original polyline that the piece replaces.
|
|
71
|
+
* The function tries `max` first, then it halves the interval `steps` times. The result is 0 when no size of the search is clear.
|
|
72
|
+
*/
|
|
73
|
+
export function fitSize(
|
|
74
|
+
space: CurveSpace | undefined,
|
|
75
|
+
max: number,
|
|
76
|
+
chord: (size: number) => readonly Point[],
|
|
77
|
+
piece: (size: number) => readonly CurveSegment[],
|
|
78
|
+
steps = 8,
|
|
79
|
+
): number {
|
|
80
|
+
if (space === undefined || max <= 0) return Math.max(0, max);
|
|
81
|
+
const clear = (size: number): boolean => !space.addsHit(sampleCurve(piece(size), space.tolerance), chord(size));
|
|
82
|
+
if (clear(max)) return max;
|
|
83
|
+
let lo = 0;
|
|
84
|
+
let hi = max;
|
|
85
|
+
for (let i = 0; i < steps; i++) {
|
|
86
|
+
const mid = (lo + hi) / 2;
|
|
87
|
+
if (clear(mid)) lo = mid;
|
|
88
|
+
else hi = mid;
|
|
89
|
+
}
|
|
90
|
+
return lo;
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { distinctPoints, type CurveSegment } from "../model.ts";
|
|
3
|
+
import { fitSize, type CurveSpace } from "../space.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of `splineCurve`. */
|
|
6
|
+
export interface SplineOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The tension of the spline, from 0 to 1. The value 0 gives the centripetal Catmull-Rom spline, and 1 gives the polyline.
|
|
9
|
+
* The default is 0.
|
|
10
|
+
*/
|
|
11
|
+
readonly tension?: number;
|
|
12
|
+
/** The outward normal of the plug at the first point. With it, the curve leaves the first point along this normal. */
|
|
13
|
+
readonly startNormal?: Point;
|
|
14
|
+
/** The outward normal of the plug at the last point. With it, the curve comes into the last point against this normal. */
|
|
15
|
+
readonly endNormal?: Point;
|
|
16
|
+
/** The boxes that the curve must not go through. Without it, each piece gets the full tangents. */
|
|
17
|
+
readonly space?: CurveSpace;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function draws a centripetal Catmull-Rom spline through the points of a polyline (Yuksel, Schaefer and Keyser, 2011).
|
|
22
|
+
* Each piece between two points is one cubic Bezier curve. The centripetal knots keep the curve free of cusps and loops in a piece.
|
|
23
|
+
* The curve goes through each point. Its tangent at a point keeps one direction on the two sides of the point.
|
|
24
|
+
*
|
|
25
|
+
* With `space`, the function makes the tangents of a piece shorter until the piece goes through no box that its leg avoids.
|
|
26
|
+
* A piece with no tangent is the straight leg.
|
|
27
|
+
*/
|
|
28
|
+
export function splineCurve(points: readonly Point[], options: SplineOptions = {}): CurveSegment[] {
|
|
29
|
+
const p = distinctPoints(points);
|
|
30
|
+
if (p.length === 0) return [];
|
|
31
|
+
const out: CurveSegment[] = [{ kind: "move", to: p[0]! }];
|
|
32
|
+
const n = p.length;
|
|
33
|
+
if (n === 1) return out;
|
|
34
|
+
const scale = 1 - Math.max(0, Math.min(1, options.tension ?? 0));
|
|
35
|
+
// The knot interval of each leg is the square root of its length.
|
|
36
|
+
const dt = Array.from({ length: n - 1 }, (_, i) => Math.sqrt(dist(p[i]!, p[i + 1]!)));
|
|
37
|
+
const unit = (q: Point): Point => {
|
|
38
|
+
const l = Math.hypot(q.x, q.y);
|
|
39
|
+
return l === 0 ? { x: 0, y: 0 } : { x: q.x / l, y: q.y / l };
|
|
40
|
+
};
|
|
41
|
+
// The tangent at each point, as a speed in the parameter of the knots.
|
|
42
|
+
const m: Point[] = p.map((v, i) => {
|
|
43
|
+
if (i === 0) {
|
|
44
|
+
const leg = { x: p[1]!.x - v.x, y: p[1]!.y - v.y };
|
|
45
|
+
if (!options.startNormal) return { x: leg.x / dt[0]!, y: leg.y / dt[0]! };
|
|
46
|
+
const u = unit(options.startNormal);
|
|
47
|
+
return { x: u.x * dt[0]!, y: u.y * dt[0]! };
|
|
48
|
+
}
|
|
49
|
+
if (i === n - 1) {
|
|
50
|
+
const a = p[i - 1]!;
|
|
51
|
+
const leg = { x: v.x - a.x, y: v.y - a.y };
|
|
52
|
+
if (!options.endNormal) return { x: leg.x / dt[i - 1]!, y: leg.y / dt[i - 1]! };
|
|
53
|
+
const u = unit(options.endNormal);
|
|
54
|
+
return { x: -u.x * dt[i - 1]!, y: -u.y * dt[i - 1]! };
|
|
55
|
+
}
|
|
56
|
+
const a = p[i - 1]!;
|
|
57
|
+
const b = p[i + 1]!;
|
|
58
|
+
const t0 = dt[i - 1]!;
|
|
59
|
+
const t1 = dt[i]!;
|
|
60
|
+
return {
|
|
61
|
+
x: (v.x - a.x) / t0 - (b.x - a.x) / (t0 + t1) + (b.x - v.x) / t1,
|
|
62
|
+
y: (v.y - a.y) / t0 - (b.y - a.y) / (t0 + t1) + (b.y - v.y) / t1,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
for (let i = 0; i + 1 < n; i++) {
|
|
66
|
+
const a = p[i]!;
|
|
67
|
+
const b = p[i + 1]!;
|
|
68
|
+
const h = dt[i]! / 3;
|
|
69
|
+
const piece = (s: number): CurveSegment[] => [
|
|
70
|
+
{ kind: "move", to: a },
|
|
71
|
+
{ kind: "cubic", c1: { x: a.x + m[i]!.x * h * s, y: a.y + m[i]!.y * h * s }, c2: { x: b.x - m[i + 1]!.x * h * s, y: b.y - m[i + 1]!.y * h * s }, to: b },
|
|
72
|
+
];
|
|
73
|
+
const s = fitSize(options.space, scale, () => [a, b], piece);
|
|
74
|
+
out.push(s > 0 ? piece(s)[1]! : { kind: "line", to: b });
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Point } from "../../geom.ts";
|
|
2
|
+
import type { CurveSegment } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** This function gives the polyline as it is: one `move` to the first point and one `line` to each other point. */
|
|
5
|
+
export function straightCurve(points: readonly Point[]): CurveSegment[] {
|
|
6
|
+
return points.map((to, i): CurveSegment => (i === 0 ? { kind: "move", to } : { kind: "line", to }));
|
|
7
|
+
}
|
package/src/grid.ts
CHANGED
|
@@ -71,6 +71,56 @@ export class Grid<T> {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* This method gives each item in the buckets along the segment `ab`, widened by `margin`, to `visit`, one time each.
|
|
76
|
+
* The buckets are those of `#segment`, in the same order, so the items come in the same order.
|
|
77
|
+
* A bucket in the range of the step before has no new item, so the walk skips it.
|
|
78
|
+
* The walk stops when `visit` gives true, and then the method gives true.
|
|
79
|
+
*/
|
|
80
|
+
#walk(a: Point, b: Point, margin: number, visit: (item: T) => boolean | void): boolean {
|
|
81
|
+
const stamp = ++this.#stamp;
|
|
82
|
+
const c = this.#cell;
|
|
83
|
+
const items = this.#items;
|
|
84
|
+
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (c / 2)));
|
|
85
|
+
let px = a.x;
|
|
86
|
+
let py = a.y;
|
|
87
|
+
// The bucket range of the step before. The first step has none.
|
|
88
|
+
let pi0 = 1;
|
|
89
|
+
let pi1 = 0;
|
|
90
|
+
let pj0 = 1;
|
|
91
|
+
let pj1 = 0;
|
|
92
|
+
for (let k = 1; k <= n; k++) {
|
|
93
|
+
const qx = k === n ? b.x : a.x + ((b.x - a.x) * k) / n;
|
|
94
|
+
const qy = k === n ? b.y : a.y + ((b.y - a.y) * k) / n;
|
|
95
|
+
const i0 = Math.floor((Math.min(px, qx) - margin) / c);
|
|
96
|
+
const j0 = Math.floor((Math.min(py, qy) - margin) / c);
|
|
97
|
+
const i1 = Math.floor((Math.max(px, qx) + margin) / c);
|
|
98
|
+
const j1 = Math.floor((Math.max(py, qy) + margin) / c);
|
|
99
|
+
// A visit can add an item and so replace the array of stamps. Each step reads it again, as `#take` does.
|
|
100
|
+
const seen = this.#seen;
|
|
101
|
+
for (let i = i0; i <= i1; i++) {
|
|
102
|
+
const before = i >= pi0 && i <= pi1;
|
|
103
|
+
for (let j = j0; j <= j1; j++) {
|
|
104
|
+
if (before && j >= pj0 && j <= pj1) continue;
|
|
105
|
+
const list = this.#buckets.get(this.#key(i, j));
|
|
106
|
+
if (list === undefined) continue;
|
|
107
|
+
for (const id of list) {
|
|
108
|
+
if (seen[id] === stamp) continue;
|
|
109
|
+
seen[id] = stamp;
|
|
110
|
+
if (visit(items[id]!) === true) return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
pi0 = i0;
|
|
115
|
+
pi1 = i1;
|
|
116
|
+
pj0 = j0;
|
|
117
|
+
pj1 = j1;
|
|
118
|
+
px = qx;
|
|
119
|
+
py = qy;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
74
124
|
// The buckets along a segment, widened by `margin`: the boxes of steps of at most half a bucket.
|
|
75
125
|
#segment(a: Point, b: Point, margin: number, each: (x0: number, y0: number, x1: number, y1: number) => void): void {
|
|
76
126
|
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
|
|
@@ -103,8 +153,17 @@ export class Grid<T> {
|
|
|
103
153
|
|
|
104
154
|
/** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
|
|
105
155
|
querySegment(a: Point, b: Point, margin: number, visit: (item: T) => void): void {
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
this.#walk(a, b, margin, (item) => {
|
|
157
|
+
visit(item);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* This method tells if `test` gives true for an item whose buckets come within `margin` of the segment `ab`.
|
|
163
|
+
* It gives the items in the order of `querySegment`, and it stops at the first item that passes the test.
|
|
164
|
+
*/
|
|
165
|
+
someSegment(a: Point, b: Point, margin: number, test: (item: T) => boolean): boolean {
|
|
166
|
+
return this.#walk(a, b, margin, test);
|
|
108
167
|
}
|
|
109
168
|
}
|
|
110
169
|
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,19 @@ export * from "./geom.ts";
|
|
|
2
2
|
export { Grid, segmentBounds } from "./grid.ts";
|
|
3
3
|
export * from "./model.ts";
|
|
4
4
|
export * from "./fixtures.ts";
|
|
5
|
+
export * from "./cases.ts";
|
|
5
6
|
export * from "./plugs.ts";
|
|
6
7
|
export * from "./curves.ts";
|
|
8
|
+
export * from "./curves/index.ts";
|
|
7
9
|
export * from "./visibility.ts";
|
|
8
10
|
export * from "./ways.ts";
|
|
9
11
|
export * from "./segments.ts";
|
|
10
12
|
export * from "./sections.ts";
|
|
11
13
|
export * from "./routers/index.ts";
|
|
14
|
+
export * from "./room/index.ts";
|
|
12
15
|
export * from "./bench/properties.ts";
|
|
13
16
|
export * from "./bench/quality.ts";
|
|
17
|
+
export * from "./bench/links.ts";
|
|
14
18
|
export * from "./bench/run.ts";
|
|
19
|
+
export * from "./bench/curves.ts";
|
|
20
|
+
export * from "./bench/room.ts";
|