@mark1russell7/highways 0.1.5 → 0.3.0

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.
Files changed (56) hide show
  1. package/README.md +293 -8
  2. package/build/index.d.ts +1774 -236
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +9307 -3090
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/compare.ts +145 -0
  8. package/src/bench/curves.ts +514 -2
  9. package/src/bench/links.ts +28 -6
  10. package/src/bench/measures/bundles.ts +119 -0
  11. package/src/bench/measures/curves.ts +211 -0
  12. package/src/bench/measures/groups.ts +122 -0
  13. package/src/bench/measures/index.ts +9 -0
  14. package/src/bench/measures/levels.ts +103 -0
  15. package/src/bench/measures/pareto.ts +37 -0
  16. package/src/bench/measures/room.ts +43 -0
  17. package/src/bench/measures/routes.ts +226 -0
  18. package/src/bench/measures/stability.ts +104 -0
  19. package/src/bench/measures/stats.ts +114 -0
  20. package/src/bench/properties.ts +18 -2
  21. package/src/bench/room.ts +209 -2
  22. package/src/bench/run.ts +86 -11
  23. package/src/bench/sections.ts +121 -0
  24. package/src/bench/suite.ts +781 -0
  25. package/src/cases.ts +41 -5
  26. package/src/curves/clothoid/index.ts +150 -0
  27. package/src/curves/corner.ts +116 -0
  28. package/src/curves/fit.ts +79 -0
  29. package/src/curves/highway/index.ts +636 -0
  30. package/src/curves/index.ts +16 -1
  31. package/src/curves/model.ts +4 -2
  32. package/src/curves/smooth/index.ts +50 -0
  33. package/src/curves/space.ts +76 -0
  34. package/src/groups/cut/index.ts +292 -0
  35. package/src/groups/index.ts +4 -0
  36. package/src/groups/kd/index.ts +207 -0
  37. package/src/groups/keys/index.ts +81 -0
  38. package/src/groups/quadtree/index.ts +10 -0
  39. package/src/index.ts +7 -0
  40. package/src/levels.ts +178 -9
  41. package/src/model.ts +2 -0
  42. package/src/pick.ts +36 -0
  43. package/src/room/bends.ts +203 -0
  44. package/src/room/index.ts +28 -0
  45. package/src/room/levels/index.ts +155 -0
  46. package/src/routers/sections.ts +78 -55
  47. package/src/routers/shared.ts +7 -2
  48. package/src/routers/trunks.ts +131 -6
  49. package/src/sections.ts +93 -4
  50. package/src/segments.ts +81 -31
  51. package/src/strategies/builtins.ts +153 -7
  52. package/src/strategies/index.ts +1 -0
  53. package/src/strategies/model.ts +29 -7
  54. package/src/strategies/options.ts +296 -0
  55. package/src/strategies/schemas.ts +221 -0
  56. package/src/visibility.ts +190 -1
@@ -0,0 +1,636 @@
1
+ import type { Point } from "../../geom.ts";
2
+ import type { Result } from "../../model.ts";
3
+ import { laneRoutes, runKey, segmentsOf, type Segment, type SegmentGraph } from "../../segments.ts";
4
+ import { arcCurve } from "../arc/index.ts";
5
+ import { fitPieces } from "../fit.ts";
6
+ import { distinctPoints, type CurveSegment } from "../model.ts";
7
+ import { sampleCurve } from "../sample.ts";
8
+ import type { CurveSpace } from "../space.ts";
9
+
10
+ /** The options of `highwayCurves`. */
11
+ export interface HighwayOptions {
12
+ /** The radius at a bend of the center line of a bundle, at a lone corner, and at a merge or a split. The default is 12. */
13
+ readonly radius?: number;
14
+ /** The smallest radius of each lane at a bend, a merge or a split. A bend of a bundle grows to keep its inner lane at this radius. The default is 4. */
15
+ readonly minRadius?: number;
16
+ /** The largest width of an open segment, from its outer lane on the left to its outer lane on the right, as in `laneRoutes`. The default is `Infinity`. */
17
+ readonly maxWidth?: number;
18
+ /** The boxes that the lanes must not go through, from `curveSpace`. With it, no lane goes through a box that its lane of `laneRoutes` avoids. */
19
+ readonly space?: CurveSpace;
20
+ /** The shape of the S curve where a lane joins or leaves a bundle. The default is `biarc`. */
21
+ readonly jog?: JogStyle;
22
+ /** The number of cubic pieces of a `quintic` S curve, from 1 to 8. The default is 4. */
23
+ readonly jogPieces?: number;
24
+ }
25
+
26
+ /**
27
+ * The shape of the S curve where a lane changes its offset.
28
+ *
29
+ * - `biarc`: two circular arcs. The curvature jumps at each end and at the inflection.
30
+ * - `cubic`: one cubic Bezier curve with its handles along the leg. The curvature jumps at each end.
31
+ * - `quintic`: the quintic `y = d (10 t³ - 15 t⁴ + 6 t⁵)`, in G2 cubic pieces. The curvature is 0 at each end and has no jump.
32
+ *
33
+ * Each shape keeps its curvature at `1 / radius` or less. The polynomial shapes are longer than the biarc for this reason.
34
+ */
35
+ export type JogStyle = "biarc" | "cubic" | "quintic";
36
+
37
+ /**
38
+ * The S curves of the lanes, by the order of `laneRoutes`. Each S curve has the first and the last index of its segments in the curve of its lane, and its shape.
39
+ * A polynomial S curve that does not fit on its leg is a biarc.
40
+ */
41
+ export type JogTrace = (readonly [number, number, JogStyle])[][];
42
+
43
+ /** The curve of the lane of one edge: a list of segments for any renderer, as `curveOf` gives. */
44
+ export interface LaneCurve {
45
+ readonly edge: string;
46
+ readonly segments: CurveSegment[];
47
+ }
48
+
49
+ /** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle stays sharp. */
50
+ const FLAT = 1e-6;
51
+ const SAME = 1e-6;
52
+ /** A turn nearer to a half turn than this angle, in radians, is a spike: the route goes back on itself. */
53
+ const SPIKE = 0.01;
54
+ /** A miter point farther from its vertex than this multiple of the offset gives two corners in place of one. */
55
+ const BEVEL = 4;
56
+ /** The number of halvings of the search for a clear size. */
57
+ const STEPS = 8;
58
+
59
+ /** One leg of the route of an edge, in the order of the route, with the offset of the lane on it. */
60
+ interface Leg {
61
+ readonly a: Point;
62
+ readonly b: Point;
63
+ /** The unit direction. */
64
+ readonly u: Point;
65
+ /** The unit normal on the left, in a frame with the y axis down. */
66
+ readonly n: Point;
67
+ readonly len: number;
68
+ /** The offset of the lane to the left of the leg. */
69
+ readonly off: number;
70
+ /** The number of lanes of the segment of the leg. A leg of a closed or private segment has 1. */
71
+ readonly lanes: number;
72
+ readonly segment: string;
73
+ /** The indexes of the ends of the leg in the distinct points of its segment. */
74
+ readonly from: number;
75
+ readonly to: number;
76
+ }
77
+
78
+ /** A vertex of the lane polyline, with the radius of its arc. A radius of 0 keeps the vertex sharp. */
79
+ interface LaneVertex {
80
+ readonly p: Point;
81
+ readonly r: number;
82
+ /** A fixed arc keeps its radius when an arc next to it on a short leg must shrink. The bends of a bundle are fixed. */
83
+ readonly fixed?: boolean;
84
+ /** The mark of an S curve. The first vertex of an S curve has the part `start`, and the last vertex has the part `end`. */
85
+ readonly jog?: JogMark;
86
+ }
87
+
88
+ /**
89
+ * The mark of a vertex of an S curve. The two vertices of a biarc have arcs.
90
+ * The first vertex of a polynomial S curve has the direction `u` of the leg. The curve goes from it to the next vertex along `u`.
91
+ */
92
+ interface JogMark {
93
+ readonly part: "start" | "end";
94
+ readonly style: JogStyle;
95
+ readonly u?: Point;
96
+ readonly pieces?: number;
97
+ }
98
+
99
+ /** The ratio `l² / (d r)` of a quintic S curve with the length `l`, the shift `d` and the smallest radius `r`. It is the peak of `h''`. */
100
+ const QUINTIC_ROOM = 10 / Math.sqrt(3);
101
+ /** The ratio `l² / (d r)` of a cubic S curve with its handles at half of the length. A numeric search gives the peak curvature. */
102
+ const CUBIC_ROOM = 5.5212;
103
+
104
+ /** A shift of the lane from one offset to another on one end of a leg: two arcs in an S. */
105
+ interface Jog {
106
+ /** The offset before the shift and after it. */
107
+ readonly from: number;
108
+ readonly to: number;
109
+ }
110
+
111
+ const sub = (a: Point, b: Point): Point => ({ x: a.x - b.x, y: a.y - b.y });
112
+ const dot = (a: Point, b: Point): number => a.x * b.x + a.y * b.y;
113
+ const cross = (a: Point, b: Point): number => a.x * b.y - a.y * b.x;
114
+ const along = (p: Point, u: Point, s: number, n: Point, o: number): Point => ({ x: p.x + u.x * s + n.x * o, y: p.y + u.y * s + n.y * o });
115
+
116
+ /** The signed turn from the direction `u` to the direction `w`, from -π to π. A positive turn goes from the x axis to the y axis. */
117
+ const turnOf = (u: Point, w: Point): number => Math.atan2(cross(u, w), dot(u, w));
118
+
119
+ /** The point where the lines at the offset `o` of two legs meet: the miter point of the vertex `v`. */
120
+ function miter(v: Point, nA: Point, nB: Point, o: number): Point {
121
+ const c = 1 + dot(nA, nB);
122
+ if (c < FLAT) return along(v, nA, 0, nA, o);
123
+ return { x: v.x + ((nA.x + nB.x) * o) / c, y: v.y + ((nA.y + nB.y) * o) / c };
124
+ }
125
+
126
+ /**
127
+ * This function gives the length along the leg of an S with the smallest radius `r` that shifts a lane by `shift`.
128
+ * For a biarc, it gives also the angle of each arc. A radius smaller than half of the shift makes two quarter circles and a straight piece.
129
+ * A polynomial S curve has the length `sqrt(c d r)`, so that its curvature is `1 / r` or less.
130
+ */
131
+ function jogShape(r: number, shift: number, style: JogStyle = "biarc"): { readonly length: number; readonly angle: number } {
132
+ const d = Math.abs(shift);
133
+ if (d < SAME) return { length: 0, angle: 0 };
134
+ if (r <= 0) return { length: 0, angle: Math.PI / 2 };
135
+ if (style !== "biarc") return { length: Math.sqrt((style === "quintic" ? QUINTIC_ROOM : CUBIC_ROOM) * d * r), angle: 0 };
136
+ if (r < d / 2) return { length: 2 * r, angle: Math.PI / 2 };
137
+ const angle = Math.acos(1 - d / (2 * r));
138
+ return { length: 2 * r * Math.sin(angle), angle };
139
+ }
140
+
141
+ /** This function finds the largest size from `max` down to 0 for which `clear` is true. It halves the interval `STEPS` times. */
142
+ function largestClear(max: number, clear: (size: number) => boolean): number {
143
+ if (max <= 0 || clear(max)) return Math.max(0, max);
144
+ let lo = 0;
145
+ let hi = max;
146
+ for (let i = 0; i < STEPS; i++) {
147
+ const mid = (lo + hi) / 2;
148
+ if (clear(mid)) lo = mid;
149
+ else hi = mid;
150
+ }
151
+ return lo;
152
+ }
153
+
154
+ /**
155
+ * This function gives the segments of a polyline with an arc at each vertex. The arc of a vertex has the radius of the vertex.
156
+ * The arc is tangent to the two legs, so the curve has no break of its tangent. A vertex with the radius 0 stays sharp.
157
+ */
158
+ function filletPolyline(vertices: readonly LaneVertex[], trace?: [number, number, JogStyle][]): CurveSegment[] {
159
+ const v: LaneVertex[] = [];
160
+ for (const q of vertices) {
161
+ const last = v[v.length - 1];
162
+ if (last && Math.hypot(q.p.x - last.p.x, q.p.y - last.p.y) <= SAME) {
163
+ const jog = q.jog?.part === "start" ? q.jog : (last.jog ?? q.jog);
164
+ v[v.length - 1] = jog ? { ...last, r: Math.max(last.r, q.r), jog } : { ...last, r: Math.max(last.r, q.r) };
165
+ } else v.push(q);
166
+ }
167
+ if (v.length === 0) return [];
168
+ const out: CurveSegment[] = [{ kind: "move", to: v[0]!.p }];
169
+ if (v.length === 1) return out;
170
+ const unit = (a: Point, b: Point): Point => {
171
+ const l = Math.hypot(b.x - a.x, b.y - a.y);
172
+ return { x: (b.x - a.x) / l, y: (b.y - a.y) / l };
173
+ };
174
+ const len = (i: number): number => Math.hypot(v[i + 1]!.p.x - v[i]!.p.x, v[i + 1]!.p.y - v[i]!.p.y);
175
+ // The tangent length of each inner vertex. Two arcs on one leg must not overlap, so a pair that is too long shrinks.
176
+ const tan: number[] = v.map((q, i) => {
177
+ if (i === 0 || i === v.length - 1 || q.r <= 0) return 0;
178
+ const t = Math.abs(turnOf(unit(v[i - 1]!.p, q.p), unit(q.p, v[i + 1]!.p)));
179
+ if (t < FLAT || Math.PI - t < FLAT) return 0;
180
+ return q.r * Math.tan(t / 2);
181
+ });
182
+ for (let i = 0; i + 1 < v.length; i++) {
183
+ const l = len(i);
184
+ const sum = tan[i]! + tan[i + 1]!;
185
+ if (sum <= l + 1e-9) continue;
186
+ const a = v[i]!.fixed === true;
187
+ const b = v[i + 1]!.fixed === true;
188
+ // A fixed arc keeps its tangent when the arc on the other end of the leg can shrink.
189
+ // The other arc keeps at least a quarter of the leg, so that it has a radius and the curve has no break.
190
+ if (a && !b) {
191
+ tan[i + 1] = Math.min(tan[i + 1]!, Math.max(l - tan[i]!, l / 4));
192
+ tan[i] = Math.min(tan[i]!, l - tan[i + 1]!);
193
+ } else if (b && !a) {
194
+ tan[i] = Math.min(tan[i]!, Math.max(l - tan[i + 1]!, l / 4));
195
+ tan[i + 1] = Math.min(tan[i + 1]!, l - tan[i]!);
196
+ }
197
+ if (tan[i]! + tan[i + 1]! > l + 1e-9) {
198
+ const k = l / (tan[i]! + tan[i + 1]!);
199
+ tan[i] = tan[i]! * k;
200
+ tan[i + 1] = tan[i + 1]! * k;
201
+ }
202
+ }
203
+ const at = (): Point => out[out.length - 1]!.to;
204
+ // The start of the biarc that the pen draws now: the index of its first segment. A polynomial S curve has its trace in `reach`.
205
+ let open = -1;
206
+ const begin = (i: number): void => {
207
+ if (v[i]!.jog?.part === "start" && v[i]!.jog?.style === "biarc") open = out.length;
208
+ };
209
+ const finish = (i: number): void => {
210
+ if (open >= 0 && v[i]!.jog?.part === "end" && v[i]!.jog?.style === "biarc") {
211
+ trace?.push([open, out.length - 1, "biarc"]);
212
+ open = -1;
213
+ }
214
+ };
215
+ // The pen goes from the vertex `i - 1` to the point `to`. After the first vertex of a polynomial S curve, it draws the S curve to the vertex `i`.
216
+ const reach = (i: number, to: Point): void => {
217
+ const mark = v[i - 1]!.jog;
218
+ const from = at();
219
+ if (mark?.part !== "start" || mark.style === "biarc" || mark.u === undefined || to !== v[i]!.p || Math.hypot(from.x - v[i - 1]!.p.x, from.y - v[i - 1]!.p.y) > 1e-9) {
220
+ out.push({ kind: "line", to });
221
+ return;
222
+ }
223
+ const first = out.length;
224
+ out.push(...jogSegments(from, to, mark.u, mark.style, mark.pieces ?? 4));
225
+ trace?.push([first, out.length - 1, mark.style]);
226
+ };
227
+ for (let i = 1; i + 1 < v.length; i++) {
228
+ const q = v[i]!.p;
229
+ const t = tan[i]!;
230
+ if (t <= 0) {
231
+ reach(i, q);
232
+ finish(i);
233
+ begin(i);
234
+ continue;
235
+ }
236
+ const u = unit(v[i - 1]!.p, q);
237
+ const w = unit(q, v[i + 1]!.p);
238
+ const turn = turnOf(u, w);
239
+ const k = Math.tan(Math.abs(turn) / 2);
240
+ const r = t / k;
241
+ const side = Math.sign(turn);
242
+ const p1 = along(q, u, -t, u, 0);
243
+ const p2 = along(q, w, t, w, 0);
244
+ // The center is on the side of the turn, at the distance r from the first leg.
245
+ const center = { x: p1.x - side * u.y * r, y: p1.y + side * u.x * r };
246
+ if (Math.hypot(p1.x - at().x, p1.y - at().y) > 1e-9) reach(i, p1);
247
+ begin(i);
248
+ out.push({ kind: "arc", center, radius: r, start: Math.atan2(p1.y - center.y, p1.x - center.x), sweep: turn, to: p2 });
249
+ finish(i);
250
+ }
251
+ reach(v.length - 1, v[v.length - 1]!.p);
252
+ return out;
253
+ }
254
+
255
+ /**
256
+ * This function gives the segments of a polynomial S curve from `from` to `to`, with the tangent `u` at each end.
257
+ * The curve goes `l` along `u` and `d` to the side, with `l` and `d` from the chord.
258
+ * A `cubic` S curve is one cubic Bezier curve with each handle at half of `l`. A `quintic` S curve is `y = d h(t)` with `h(t) = 10 t³ - 15 t⁴ + 6 t⁵`, in `pieces` G2 cubic pieces.
259
+ */
260
+ function jogSegments(from: Point, to: Point, u: Point, style: JogStyle, pieces: number): CurveSegment[] {
261
+ const D = sub(to, from);
262
+ const l = dot(D, u);
263
+ const d = cross(u, D);
264
+ if (l <= SAME) return [{ kind: "line", to }];
265
+ const side = { x: -u.y, y: u.x };
266
+ if (style === "cubic") return [{ kind: "cubic", c1: along(from, u, l / 2, u, 0), c2: along(to, u, -l / 2, u, 0), to }];
267
+ const at = (t: number): { p: Point; d: Point; dd: Point } => {
268
+ const h = t * t * t * (10 - 15 * t + 6 * t * t);
269
+ const h1 = 30 * t * t * (1 - t) * (1 - t);
270
+ const h2 = 60 * t * (1 - t) * (1 - 2 * t);
271
+ return {
272
+ p: { x: from.x + u.x * l * t + side.x * d * h, y: from.y + u.y * l * t + side.y * d * h },
273
+ d: { x: u.x * l + side.x * d * h1, y: u.y * l + side.y * d * h1 },
274
+ dd: { x: side.x * d * h2, y: side.y * d * h2 },
275
+ };
276
+ };
277
+ const out: CurveSegment[] = fitPieces(at, Math.max(1, Math.min(8, Math.round(pieces)))).map((q) => ({ kind: "cubic", c1: q.c1, c2: q.c2, to: q.to }));
278
+ // The last piece ends at the exact point of the vertex.
279
+ const last = out[out.length - 1]!;
280
+ if (last.kind === "cubic") out[out.length - 1] = { ...last, to };
281
+ return out;
282
+ }
283
+
284
+ /** The radius of the center line at each bend of the bundles, by the key of the bend from `bendKey`. */
285
+ type Bends = ReadonlyMap<string, number>;
286
+
287
+ /** The key of the bend between two legs. The lanes that go from one leg to the other bend with one center. */
288
+ const bendKey = (A: Leg, B: Leg): string => `${A.segment}:${A.to}>${B.segment}:${B.from}@${B.a.x},${B.a.y}`;
289
+
290
+ /** One bend of the lanes of bundles: the leg before it, the leg after it, and the offset and the polyline of each lane. */
291
+ interface BendGroup {
292
+ readonly A: Leg;
293
+ readonly B: Leg;
294
+ readonly lanes: { readonly offset: number; readonly polyline: readonly Point[] }[];
295
+ }
296
+
297
+ /** The legs of the route of one lane, with no spike, and the offset of the lane at each vertex of the route. */
298
+ interface LanePlan {
299
+ readonly legs: readonly Leg[];
300
+ readonly at: readonly number[];
301
+ }
302
+
303
+ /**
304
+ * This function gives the offset of the lane at each vertex of its route. At a merge or a split, it is the offset on the leg with more lanes.
305
+ * So a bundle keeps its spacing up to the vertex, and the lane shifts on the other leg.
306
+ */
307
+ function offsetsAt(legs: readonly Leg[]): number[] {
308
+ const at: number[] = new Array<number>(legs.length + 1).fill(0);
309
+ for (let j = 1; j < legs.length; j++) {
310
+ const A = legs[j - 1]!;
311
+ const B = legs[j]!;
312
+ at[j] = A.lanes < B.lanes ? B.off : A.off;
313
+ }
314
+ return at;
315
+ }
316
+
317
+ /**
318
+ * This function gives the curve of one polyline as a lane alone: an arc at each corner, as `arcCurve` gives.
319
+ * A lane alone has no merge and no split, so the curve strategy `highway` gives this curve for one polyline.
320
+ */
321
+ export function highwayCurve(points: readonly Point[], options: Pick<HighwayOptions, "radius" | "space"> = {}): CurveSegment[] {
322
+ return arcCurve(points, { radius: options.radius ?? 12, ...(options.space ? { space: options.space } : {}) });
323
+ }
324
+
325
+ /**
326
+ * This function gives the curve of each lane of `laneRoutes`. The lanes of one open segment bend as one highway.
327
+ *
328
+ * At each bend of an open segment, the lanes are arcs with one center. The radius of a lane is the radius of the center line,
329
+ * plus or minus its offset. So the lanes stay at the lane spacing through the bend. The bend grows to keep the inner lane at `minRadius`,
330
+ * and it shrinks to fit on the legs and in the free space.
331
+ *
332
+ * A lane joins (merges) or leaves (splits) a bundle on an S curve on the leg with fewer lanes. The option `jog` gives its shape.
333
+ * The smallest radius of the S curve is `radius` or less.
334
+ * Each curve has no break of its tangent where each radius is more than 0.
335
+ * A lane that is in no open segment gets the curve of `arcCurve`.
336
+ *
337
+ * The function takes the same input as `laneRoutes`, and it gives the curves in the order of `laneRoutes`.
338
+ * The output is a list of segments for any renderer. `toSvgPath` writes it as SVG path data, and `sampleCurve` gives points.
339
+ */
340
+ export function highwayCurves(source: Result | SegmentGraph, open: ReadonlySet<string>, laneWidth: number, options: HighwayOptions = {}): LaneCurve[] {
341
+ return tracedHighwayCurves(source, open, laneWidth, options).curves;
342
+ }
343
+
344
+ /**
345
+ * This function gives the curves of `highwayCurves` and the S curves of each lane. The benchmark measures the S curves.
346
+ * The package does not export this function.
347
+ */
348
+ export function tracedHighwayCurves(
349
+ source: Result | SegmentGraph,
350
+ open: ReadonlySet<string>,
351
+ laneWidth: number,
352
+ options: HighwayOptions = {},
353
+ ): { readonly curves: LaneCurve[]; readonly jogs: JogTrace } {
354
+ const graph = "paths" in source ? source : segmentsOf(source);
355
+ const radius = Math.max(0, options.radius ?? 12);
356
+ const minRadius = Math.max(0, options.minRadius ?? 4);
357
+ const maxWidth = options.maxWidth ?? Infinity;
358
+ const space = options.space;
359
+ const style: JogStyle = options.jog ?? "biarc";
360
+ const pieces = options.jogPieces ?? 4;
361
+ const byId = new Map(graph.segments.map((s) => [s.id, s]));
362
+ const spacingOf = (s: Segment): number => (s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0);
363
+ // The polylines of `laneRoutes`: the free space keeps each curve out of the boxes that its polyline avoids.
364
+ const polylines = laneRoutes(graph, open, laneWidth, maxWidth);
365
+ const centers = new Map<string, Point[]>();
366
+ const centerOf = (s: Segment): Point[] => {
367
+ let c = centers.get(s.id);
368
+ if (!c) {
369
+ c = distinctPoints(s.points, SAME);
370
+ centers.set(s.id, c);
371
+ }
372
+ return c;
373
+ };
374
+ const plans = graph.paths.map((path): LanePlan | undefined => {
375
+ const legs: Leg[] = [];
376
+ for (const ref of path.segments) {
377
+ const s = byId.get(ref.segment);
378
+ if (!s) continue;
379
+ const sp = spacingOf(s);
380
+ const c = centerOf(s);
381
+ const offset = ((s.count - 1) / 2 - ref.lane) * sp;
382
+ const off = ref.reverse ? -offset : offset;
383
+ const n = c.length;
384
+ for (let j = 0; j + 1 < n; j++) {
385
+ const from = ref.reverse ? n - 1 - j : j;
386
+ const to = ref.reverse ? n - 2 - j : j + 1;
387
+ const a = c[from]!;
388
+ const b = c[to]!;
389
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
390
+ if (len <= SAME) continue;
391
+ const u = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };
392
+ legs.push({ a, b, u, n: { x: u.y, y: -u.x }, len, off, lanes: sp > 0 ? s.count : 1, segment: s.id, from, to });
393
+ }
394
+ }
395
+ if (legs.every((l) => l.lanes < 2)) return undefined;
396
+ const kept = withoutSpikes(legs);
397
+ return { legs: kept, at: offsetsAt(kept) };
398
+ });
399
+ // The lanes that go from one leg of a bundle to the next leg of a bundle bend about one center.
400
+ const groups = new Map<string, BendGroup>();
401
+ plans.forEach((plan, i) => {
402
+ if (!plan) return;
403
+ for (let j = 1; j < plan.legs.length; j++) {
404
+ const A = plan.legs[j - 1]!;
405
+ const B = plan.legs[j]!;
406
+ if (A.lanes < 2 || B.lanes < 2) continue;
407
+ const key = bendKey(A, B);
408
+ const group = groups.get(key) ?? { A, B, lanes: [] };
409
+ // A lane that goes the other way through the bend has its offset on the other side of its own direction.
410
+ const same = dot(A.u, group.A.u) > 0;
411
+ group.lanes.push({ offset: same ? plan.at[j]! : -plan.at[j]!, polyline: polylines[i]!.points });
412
+ groups.set(key, group);
413
+ }
414
+ });
415
+ const bends = bendRadii(groups, radius, minRadius, space);
416
+ const jogs: JogTrace = [];
417
+ const curves = graph.paths.map((path, i): LaneCurve => {
418
+ const polyline = polylines[i]!.points;
419
+ const plan = plans[i];
420
+ const trace: [number, number, JogStyle][] = [];
421
+ jogs.push(trace);
422
+ if (!plan) return { edge: path.edge, segments: highwayCurve(polyline, { radius, ...(space ? { space } : {}) }) };
423
+ if (plan.legs.length === 0) return { edge: path.edge, segments: [{ kind: "move", to: polyline[0]! }] };
424
+ return { edge: path.edge, segments: laneCurve(plan, bends, polyline, { radius, minRadius, space, style, pieces }, trace) };
425
+ });
426
+ return { curves, jogs };
427
+ }
428
+
429
+ /**
430
+ * This function gives the radius of the center line at each bend of the bundles.
431
+ * The radius is `radius`, or more to keep the inner lane at `minRadius`. It shrinks so that each tangent point stays on the half of its leg.
432
+ * It also shrinks so that no lane goes through a box that its polyline avoids.
433
+ */
434
+ function bendRadii(groups: ReadonlyMap<string, BendGroup>, radius: number, minRadius: number, space: CurveSpace | undefined): Map<string, number> {
435
+ const out = new Map<string, number>();
436
+ for (const [key, { A, B, lanes }] of groups) {
437
+ const turn = turnOf(A.u, B.u);
438
+ if (Math.abs(turn) < FLAT || Math.PI - Math.abs(turn) < FLAT) continue;
439
+ const k = Math.tan(Math.abs(turn) / 2);
440
+ const side = Math.sign(turn);
441
+ const v = B.a;
442
+ const half = Math.max(...lanes.map((l) => Math.abs(l.offset)));
443
+ const max = Math.min(Math.max(radius, half + minRadius), A.len / (2 * k), B.len / (2 * k));
444
+ const clear = (R: number): boolean => {
445
+ if (!space) return true;
446
+ return lanes.every(({ offset, polyline }) => {
447
+ const r = R + side * offset;
448
+ if (r <= 0) return true;
449
+ const W = miter(v, A.n, B.n, offset);
450
+ const piece = filletPolyline([
451
+ { p: along(W, A.u, -r * k, A.u, 0), r: 0 },
452
+ { p: W, r },
453
+ { p: along(W, B.u, r * k, B.u, 0), r: 0 },
454
+ ]);
455
+ return !space.addsHit(sampleCurve(piece, space.tolerance), polyline);
456
+ });
457
+ };
458
+ out.set(key, largestClear(max, clear));
459
+ }
460
+ return out;
461
+ }
462
+
463
+ /**
464
+ * This function removes each place where the route goes back on itself: a leg, then a leg in the opposite direction.
465
+ * Such a spike is a split point past the turn of a route. A lane cannot follow it with no break of its tangent.
466
+ */
467
+ function withoutSpikes(input: readonly Leg[]): Leg[] {
468
+ const legs = [...input];
469
+ for (let i = 1; i < legs.length; ) {
470
+ const A = legs[i - 1]!;
471
+ const B = legs[i]!;
472
+ if (Math.PI - Math.abs(turnOf(A.u, B.u)) >= SPIKE) {
473
+ i++;
474
+ continue;
475
+ }
476
+ // The route goes from A.a to the tip and back to B.b. The rest is the part of the longer leg past B.b or past A.a.
477
+ const rest = A.len - B.len;
478
+ const keep: Leg[] = [];
479
+ if (rest > SAME) keep.push({ ...A, b: B.b, len: rest, to: -1 });
480
+ else if (-rest > SAME) keep.push({ ...B, a: A.a, len: -rest, from: -1 });
481
+ legs.splice(i - 1, 2, ...keep);
482
+ i = Math.max(1, i - 1);
483
+ }
484
+ return legs;
485
+ }
486
+
487
+ /** This function gives the curve of the lane of one edge from its legs. */
488
+ function laneCurve(
489
+ plan: LanePlan,
490
+ bends: Bends,
491
+ polyline: readonly Point[],
492
+ { radius, minRadius, space, style: wanted, pieces }: { radius: number; minRadius: number; space: CurveSpace | undefined; style: JogStyle; pieces: number },
493
+ trace?: [number, number, JogStyle][],
494
+ ): CurveSegment[] {
495
+ const { legs, at } = plan;
496
+ const L = legs.length;
497
+ // The shifts on the ends of each leg, from the offset at a vertex to the offset of the leg.
498
+ const startJog: (Jog | undefined)[] = new Array<Jog | undefined>(L).fill(undefined);
499
+ const endJog: (Jog | undefined)[] = new Array<Jog | undefined>(L).fill(undefined);
500
+ for (let j = 0; j < L; j++) {
501
+ const leg = legs[j]!;
502
+ if (Math.abs(at[j]! - leg.off) > SAME) startJog[j] = { from: at[j]!, to: leg.off };
503
+ if (Math.abs(at[j + 1]! - leg.off) > SAME) endJog[j] = { from: leg.off, to: at[j + 1]! };
504
+ }
505
+ // The fixed radius of a lane at a bend of bundles: the radius of the center line, plus or minus the offset.
506
+ const fixed = (j: number): number | undefined => {
507
+ const A = legs[j - 1]!;
508
+ const B = legs[j]!;
509
+ if (A.lanes < 2 || B.lanes < 2) return undefined;
510
+ const R = bends.get(bendKey(A, B)) ?? 0;
511
+ const r = R + Math.sign(turnOf(A.u, B.u)) * at[j]!;
512
+ return R > 0 && r > 0 ? r : undefined;
513
+ };
514
+
515
+ /** The shape of the lane near one vertex of the route. */
516
+ interface Near {
517
+ /** The vertices of the shift at the end of the leg before the vertex, the corner, and the shift on the leg after it. */
518
+ readonly vertices: readonly LaneVertex[];
519
+ /** The distance along each leg that the shape uses. */
520
+ readonly back: number;
521
+ readonly ahead: number;
522
+ }
523
+ // The shape near the vertex `j` for the scale `s` of the wanted radius. The radius of each arc is at least `floor`.
524
+ // The arcs of a short leg shrink in `filletPolyline`, so a large floor keeps each tangent and does not make an arc too large for its leg.
525
+ const near = (j: number, s: number, floor: number, loose: boolean, style: JogStyle): Near => {
526
+ const A = j > 0 ? legs[j - 1] : undefined;
527
+ const B = j < L ? legs[j] : undefined;
528
+ const v = B ? B.a : A!.b;
529
+ const c = at[j]!;
530
+ const want = Math.max(radius * s, floor);
531
+ const corner: LaneVertex[] = [];
532
+ let xA = 0;
533
+ let xB = 0;
534
+ let t = 0;
535
+ if (!A || !B) corner.push({ p: v, r: 0 });
536
+ else {
537
+ const turn = turnOf(A.u, B.u);
538
+ const f = loose ? undefined : fixed(j);
539
+ const W = miter(v, A.n, B.n, c);
540
+ if (Math.abs(turn) < FLAT) {
541
+ // A straight vertex has no corner.
542
+ } else if (Math.hypot(W.x - v.x, W.y - v.y) > BEVEL * Math.abs(c)) {
543
+ // A turn near a half turn puts the miter point far away. The lane turns at two points on the offset lines.
544
+ corner.push({ p: along(v, A.n, 0, A.n, c), r: want }, { p: along(v, B.n, 0, B.n, c), r: want });
545
+ } else {
546
+ xA = dot(sub(W, v), A.u);
547
+ xB = dot(sub(W, v), B.u);
548
+ const r = f ?? want;
549
+ corner.push(f === undefined ? { p: W, r } : { p: W, r, fixed: true });
550
+ t = r * Math.tan(Math.abs(turn) / 2);
551
+ }
552
+ }
553
+ // The arcs of a shift have the wanted radius too. So the radius of each arc of the lane is at least `floor`.
554
+ const shift = (leg: Leg, jog: Jog | undefined, start: number, sign: 1 | -1): LaneVertex[] => {
555
+ if (!jog) return [];
556
+ const { length, angle } = jogShape(want, jog.to - jog.from, style);
557
+ if (style !== "biarc") {
558
+ // The S curve goes along the leg from the offset `jog.from` to the offset `jog.to`, in the order of the route.
559
+ const s0 = sign === 1 ? start : -(start + length);
560
+ const p0 = along(v, leg.u, s0, leg.n, jog.from);
561
+ const p3 = along(v, leg.u, s0 + length, leg.n, jog.to);
562
+ return [
563
+ { p: p0, r: 0, jog: { part: "start", style, u: leg.u, pieces } },
564
+ { p: p3, r: 0, jog: { part: "end", style } },
565
+ ];
566
+ }
567
+ const k = want < Math.abs(jog.to - jog.from) / 2 ? want : want * Math.tan(angle / 2);
568
+ // On the leg after the vertex, the shift goes forward from `start`. On the leg before it, it goes back from `start`.
569
+ const p1 = along(v, leg.u, sign * (start + k), leg.n, sign === 1 ? jog.from : jog.to);
570
+ const p2 = along(v, leg.u, sign * (start + length - k), leg.n, sign === 1 ? jog.to : jog.from);
571
+ return sign === 1
572
+ ? [{ p: p1, r: want, jog: { part: "start", style } }, { p: p2, r: want, jog: { part: "end", style } }]
573
+ : [{ p: p2, r: want, jog: { part: "start", style } }, { p: p1, r: want, jog: { part: "end", style } }];
574
+ };
575
+ const lenOf = (jog: Jog | undefined): number => (jog ? jogShape(want, jog.to - jog.from, style).length : 0);
576
+ return {
577
+ vertices: [...(A ? shift(A, endJog[j - 1], t - xA, -1) : []), ...corner, ...(B ? shift(B, startJog[j], xB + t, 1) : [])],
578
+ back: A ? Math.max(0, t - xA) + lenOf(endJog[j - 1]) : 0,
579
+ ahead: B ? Math.max(0, xB + t) + lenOf(startJog[j]) : 0,
580
+ };
581
+ };
582
+ // The vertices of the lane near `j`, between the middles of its two legs. The free space tests this piece.
583
+ const piece = (j: number, s: number, floor: number, loose: boolean, style: JogStyle): LaneVertex[] => {
584
+ const A = j > 0 ? legs[j - 1] : undefined;
585
+ const B = j < L ? legs[j] : undefined;
586
+ const v = B ? B.a : A!.b;
587
+ const head = A ? [{ p: along(v, A.u, -A.len / 2, A.n, A.off), r: 0 }] : [];
588
+ const tail = B ? [{ p: along(v, B.u, B.len / 2, B.n, B.off), r: 0 }] : [];
589
+ return [...head, ...near(j, s, floor, loose, style).vertices, ...tail];
590
+ };
591
+ // Each vertex gets the half of each leg next to it. The scale of the radii is the largest that fits, then the largest that is clear.
592
+ // A bend of bundles keeps its fixed radius. It is loose, with the radii of a lane alone, only when the fixed arc does not fit or is not clear.
593
+ // The smallest radius shrinks only as far as a short leg or the free space needs. A radius of 0 makes a sharp corner.
594
+ // A polynomial S curve is longer than a biarc. Where it does not fit on its leg, is not clear, or needs a smaller radius than the biarc, the vertex takes the biarc.
595
+ const solve = (j: number, style: JogStyle): { readonly vertices: readonly LaneVertex[]; readonly ok: boolean; readonly want: number } => {
596
+ const fits = (s: number, f: number, loose: boolean): boolean => {
597
+ const q = near(j, s, f, loose, style);
598
+ return (j === 0 || q.back <= legs[j - 1]!.len / 2 + 1e-9) && (j === L || q.ahead <= legs[j]!.len / 2 + 1e-9);
599
+ };
600
+ let floor = minRadius;
601
+ let loose = !fits(0, floor, false);
602
+ const fitFloor = (): void => {
603
+ if (!fits(0, floor, loose) && fits(0, 0, loose)) floor = largestClear(floor, (f) => fits(0, f, loose));
604
+ };
605
+ const most = (): number => (fits(1, floor, loose) ? 1 : largestClear(1, (x) => fits(x, floor, loose)));
606
+ fitFloor();
607
+ let s = most();
608
+ let clearAtEnd = true;
609
+ if (space) {
610
+ const clear = (x: number, f: number): boolean => !space.addsHit(sampleCurve(filletPolyline(piece(j, x, f, loose, style)), space.tolerance), polyline);
611
+ s = largestClear(s, (x) => clear(x, floor));
612
+ if (!loose && !clear(s, floor)) {
613
+ loose = true;
614
+ fitFloor();
615
+ s = largestClear(most(), (x) => clear(x, floor));
616
+ }
617
+ if (!clear(s, floor)) {
618
+ const f = largestClear(floor, (x) => clear(s, x));
619
+ if (clear(s, f)) floor = f;
620
+ }
621
+ clearAtEnd = clear(s, floor);
622
+ }
623
+ return { vertices: near(j, s, floor, loose, style).vertices, ok: fits(s, floor, loose) && clearAtEnd, want: Math.max(radius * s, floor) };
624
+ };
625
+ const vertices: LaneVertex[] = [];
626
+ for (let j = 0; j <= L; j++) {
627
+ const first = solve(j, wanted);
628
+ if (wanted === "biarc" || (startJog[j] === undefined && (j === 0 || endJog[j - 1] === undefined))) {
629
+ vertices.push(...first.vertices);
630
+ continue;
631
+ }
632
+ const biarc = solve(j, "biarc");
633
+ vertices.push(...(first.ok && first.want >= biarc.want - 1e-9 ? first : biarc).vertices);
634
+ }
635
+ return filletPolyline(vertices, trace);
636
+ }
@@ -1,7 +1,9 @@
1
1
  import type { Point } from "../geom.ts";
2
2
  import { arcCurve } from "./arc/index.ts";
3
+ import { clothoidCurve } from "./clothoid/index.ts";
3
4
  import type { CurveSegment, CurveType } from "./model.ts";
4
5
  import { roundedCurve } from "./rounded/index.ts";
6
+ import { smoothCurve } from "./smooth/index.ts";
5
7
  import type { CurveSpace } from "./space.ts";
6
8
  import { splineCurve } from "./spline/index.ts";
7
9
  import { straightCurve } from "./straight/index.ts";
@@ -13,14 +15,23 @@ export { straightCurve } from "./straight/index.ts";
13
15
  export { roundedCurve, type RoundedOptions } from "./rounded/index.ts";
14
16
  export { splineCurve, type SplineOptions } from "./spline/index.ts";
15
17
  export { arcCurve, type ArcOptions } from "./arc/index.ts";
18
+ export { smoothCurve, type SmoothOptions } from "./smooth/index.ts";
19
+ export { clothoidCurve, type ClothoidOptions } from "./clothoid/index.ts";
20
+ export { highwayCurve, highwayCurves, type HighwayOptions, type JogStyle, type LaneCurve } from "./highway/index.ts";
16
21
 
17
22
  /** The options of `curveOf`. Each type reads only the options that it uses. */
18
23
  export interface CurveOptions {
19
24
  readonly type: CurveType;
20
- /** The types `rounded` and `arc` read this radius of the corners. The default is 12. */
25
+ /** The types `rounded`, `arc`, `smooth` and `clothoid` read this radius of the corners. The default is 12. */
21
26
  readonly radius?: number;
22
27
  /** The type `spline` reads this tension, from 0 to 1. The default is 0. */
23
28
  readonly tension?: number;
29
+ /** The type `smooth` reads this share of the turn of a corner in its spirals, from 0 to 1. The default is 0.6. */
30
+ readonly smoothing?: number;
31
+ /** The type `clothoid` reads this share of the turn of a corner in its clothoids, from 0 to 1. The default is 0.5. */
32
+ readonly spiral?: number;
33
+ /** The type `clothoid` reads this largest distance from its cubic pieces to the true clothoid. The default is 0.25. */
34
+ readonly tolerance?: number;
24
35
  /** The type `spline` leaves the first point along this outward normal of its plug. */
25
36
  readonly startNormal?: Point;
26
37
  /** The type `spline` comes into the last point against this outward normal of its plug. */
@@ -44,5 +55,9 @@ export function curveOf(points: readonly Point[], options: CurveOptions): CurveS
44
55
  return splineCurve(points, options);
45
56
  case "arc":
46
57
  return arcCurve(points, options);
58
+ case "smooth":
59
+ return smoothCurve(points, options);
60
+ case "clothoid":
61
+ return clothoidCurve(points, options);
47
62
  }
48
63
  }