@mark1russell7/highways 0.3.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +66 -2
  2. package/build/index.d.ts +1009 -12
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +4408 -195
  5. package/build/index.js.map +1 -1
  6. package/package.json +4 -1
  7. package/src/bench/compare.ts +41 -6
  8. package/src/bench/measures/bundles.ts +101 -2
  9. package/src/bench/measures/index.ts +1 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/forms.ts +372 -0
  13. package/src/bench/screen/generators.ts +54 -0
  14. package/src/bench/screen/hash.ts +129 -0
  15. package/src/bench/screen/index.ts +6 -0
  16. package/src/bench/screen/measures.ts +126 -0
  17. package/src/bench/screen/screen.ts +454 -0
  18. package/src/bench/screen/set.ts +136 -0
  19. package/src/bench/suite.ts +71 -13
  20. package/src/highways/corridors/index.ts +305 -0
  21. package/src/highways/follow.ts +155 -0
  22. package/src/highways/index.ts +7 -0
  23. package/src/highways/keys/index.ts +15 -0
  24. package/src/highways/model.ts +111 -0
  25. package/src/highways/result.ts +34 -0
  26. package/src/highways/router.ts +40 -0
  27. package/src/highways/shared-ways/index.ts +15 -0
  28. package/src/index.ts +4 -0
  29. package/src/order/index.ts +65 -0
  30. package/src/plugs/justify.ts +48 -0
  31. package/src/plugs/side.ts +138 -0
  32. package/src/repair/index.ts +377 -0
  33. package/src/repair/walk.ts +220 -0
  34. package/src/room/lanes.ts +109 -10
  35. package/src/routers/corners.ts +73 -6
  36. package/src/routers/fan.ts +145 -0
  37. package/src/routers/index.ts +2 -2
  38. package/src/routers/shared.ts +107 -8
  39. package/src/routers/snell.ts +140 -0
  40. package/src/routers/trunks.ts +43 -2
  41. package/src/strategies/builtins.ts +82 -6
  42. package/src/strategies/model.ts +36 -6
  43. package/src/strategies/schemas.ts +66 -2
@@ -0,0 +1,140 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+ import type { Way } from "./shared.ts";
3
+
4
+ /**
5
+ * This function gives the Snell merge point of a route (NAT-035).
6
+ * The route comes from `from`, reaches a run at `at` and goes on along the run to `next`.
7
+ * A unit of length on the run costs `d` against 1 off it. The cheapest route meets the run at the angle `acos(d)`.
8
+ * The function gives the distance from `at` toward `next` of that merge point. A value of 0 or less tells that the turn at `at` is already at most `acos(d)`.
9
+ */
10
+ export function snellOffset(from: Point, at: Point, next: Point, d: number): number {
11
+ const len = dist(at, next);
12
+ if (len <= 0) return 0;
13
+ const ux = (next.x - at.x) / len;
14
+ const uy = (next.y - at.y) / len;
15
+ const vx = from.x - at.x;
16
+ const vy = from.y - at.y;
17
+ // The foot of `from` on the line of the run is at `along` from `at`, and `from` is `h` from the line.
18
+ const along = vx * ux + vy * uy;
19
+ const h = Math.abs(vx * uy - vy * ux);
20
+ const c = Math.min(0.999, Math.max(0, d));
21
+ return along + (h * c) / Math.sqrt(1 - c * c);
22
+ }
23
+
24
+ /** The turn, in radians from 0 to π, at `b` on the way from `a` through `b` to `c`. */
25
+ function turnAt(a: Point, b: Point, c: Point): number {
26
+ const ux = b.x - a.x;
27
+ const uy = b.y - a.y;
28
+ const vx = c.x - b.x;
29
+ const vy = c.y - b.y;
30
+ return Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
31
+ }
32
+
33
+ /**
34
+ * This function moves the merge points of the ways of the router `shared` to the Snell angle (NAT-035). It changes the ways in place.
35
+ *
36
+ * A way joins a run at a corner `J` when it comes alone from the corner before.
37
+ * The link from `J` to the next corner has other edges of its key.
38
+ * When the way turns at `J` by more than `acos(d)`, it goes directly to a new point `J*` on that link.
39
+ * At `J*`, the way meets the link at the angle `acos(d)`.
40
+ * Each way on the link gets `J*` as a new corner, so the run keeps the same points in each route. Then the same rule moves the exits, on the reversed ways.
41
+ *
42
+ * `firstId` is the first free id of a corner. `clear` tells if a line keeps out of the boxes. `delta` gives the cost share `d` of a link with `f` other edges.
43
+ */
44
+ export function snellMerges(ways: readonly Way[], firstId: number, clear: (a: Point, b: Point) => boolean, delta: (f: number) => number): void {
45
+ let next = firstId;
46
+ const keys = new Map<string, Way[]>();
47
+ for (const w of ways) {
48
+ if (!w.found) continue;
49
+ const list = keys.get(w.key);
50
+ if (list) list.push(w);
51
+ else keys.set(w.key, [w]);
52
+ }
53
+ for (const group of keys.values()) {
54
+ if (group.length < 2) continue;
55
+ const linkOf = (a: number, b: number): string => (a < b ? `${a}:${b}` : `${b}:${a}`);
56
+ const on = new Map<string, Set<Way>>();
57
+ const add = (k: string, w: Way): void => {
58
+ const set = on.get(k);
59
+ if (set) set.add(w);
60
+ else on.set(k, new Set([w]));
61
+ };
62
+ const drop = (k: string, w: Way): void => {
63
+ const set = on.get(k);
64
+ if (!set) return;
65
+ set.delete(w);
66
+ if (set.size === 0) on.delete(k);
67
+ };
68
+ for (const w of group) for (let i = 1; i < w.ids.length; i++) add(linkOf(w.ids[i - 1]!, w.ids[i]!), w);
69
+ // First the merges, then the exits: an exit is a merge of the reversed way.
70
+ for (const back of [false, true]) {
71
+ for (const w of group) {
72
+ if (back) {
73
+ w.seq.reverse();
74
+ w.ids.reverse();
75
+ }
76
+ for (let i = 1; i < w.ids.length - 1; i++) {
77
+ const before = on.get(linkOf(w.ids[i - 1]!, w.ids[i]!));
78
+ const after = on.get(linkOf(w.ids[i]!, w.ids[i + 1]!));
79
+ if (!before || !after || before.size !== 1 || after.size < 2) continue;
80
+ const P = w.seq[i - 1]!;
81
+ const J = w.seq[i]!;
82
+ const N = w.seq[i + 1]!;
83
+ const d = delta(after.size - 1);
84
+ const turn = turnAt(P, J, N);
85
+ if (turn <= Math.acos(d) + 1e-3) continue;
86
+ const len = dist(J, N);
87
+ let t = Math.min(len, snellOffset(P, J, N, d));
88
+ // A line from P to the merge point that goes through a box moves the point back toward J, at most three times.
89
+ const pointAt = (s: number): Point => ({ x: J.x + ((N.x - J.x) * s) / len, y: J.y + ((N.y - J.y) * s) / len });
90
+ let tries = 0;
91
+ while (t >= 1 && !clear(P, pointAt(t)) && tries < 3) {
92
+ t /= 2;
93
+ tries++;
94
+ }
95
+ if (t < 1 || !clear(P, pointAt(t))) continue;
96
+ const a = w.ids[i - 1]!;
97
+ const j = w.ids[i]!;
98
+ const n = w.ids[i + 1]!;
99
+ if (t >= len - 1e-6) {
100
+ // The merge point is the next corner: the way skips J.
101
+ drop(linkOf(a, j), w);
102
+ drop(linkOf(j, n), w);
103
+ w.seq.splice(i, 1);
104
+ w.ids.splice(i, 1);
105
+ add(linkOf(a, n), w);
106
+ continue;
107
+ }
108
+ const star = pointAt(t);
109
+ const id = next++;
110
+ const users = [...after];
111
+ on.delete(linkOf(j, n));
112
+ // Each way on the link from J to N gets the new corner between them.
113
+ for (const o of users) {
114
+ for (let k = 0; k < o.ids.length - 1; k++) {
115
+ const x = o.ids[k]!;
116
+ const y = o.ids[k + 1]!;
117
+ if ((x === j && y === n) || (x === n && y === j)) {
118
+ o.seq.splice(k + 1, 0, star);
119
+ o.ids.splice(k + 1, 0, id);
120
+ break;
121
+ }
122
+ }
123
+ add(linkOf(j, id), o);
124
+ add(linkOf(id, n), o);
125
+ }
126
+ // The way itself goes from P directly to the new corner.
127
+ drop(linkOf(a, j), w);
128
+ drop(linkOf(j, id), w);
129
+ w.seq.splice(i, 1);
130
+ w.ids.splice(i, 1);
131
+ add(linkOf(a, id), w);
132
+ }
133
+ if (back) {
134
+ w.seq.reverse();
135
+ w.ids.reverse();
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
@@ -3,6 +3,7 @@ import { nodeIndex, type Bundle, type Edge, type Graph, type GroupRule, type Nod
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
4
  import { orderLanes, type RunOrder } from "../segments.ts";
5
5
  import { WayFinder, type WayOptions } from "../ways.ts";
6
+ import { balancedFan, fanPath, gilbertPoint } from "./fan.ts";
6
7
 
7
8
  /** The options of the trunk router. */
8
9
  export interface TrunksOptions extends WayOptions {
@@ -21,6 +22,20 @@ export interface TrunksOptions extends WayOptions {
21
22
  * A lane that turns back over the trunk leaves at an earlier point of the trunk. With `split`, each lane leaves at the split point, as in version 0.2.0.
22
23
  */
23
24
  readonly leave?: TrunkLeave;
25
+ /**
26
+ * The place of the split point. With `share`, the default, it is at `split` of the way to the center of the targets.
27
+ * With `gilbert`, it is where the flow cost of the trunk and its branches is least, with the exponent `alpha` (EDA-030).
28
+ */
29
+ readonly splitAt?: TrunkSplit;
30
+ /** The exponent of the flow cost of the split `gilbert`, from 0 to 1. The default is 0.5. */
31
+ readonly alpha?: number;
32
+ /**
33
+ * The shape of each group. With `split`, the default, the edges share a trunk to a split point.
34
+ * With `tree`, the edges grow a balancing-factor tree from the plug, with the factor `bf` (NAT-017).
35
+ */
36
+ readonly fan?: TrunkFan;
37
+ /** The balancing factor of the fan `tree`, from 0 to 1. 0 gives the least ink, and a larger factor gives more direct routes. The default is 0.4. */
38
+ readonly bf?: number;
24
39
  /** The benchmark skips a fixture with more nodes. */
25
40
  readonly maxNodes?: number;
26
41
  }
@@ -28,6 +43,12 @@ export interface TrunksOptions extends WayOptions {
28
43
  /** The place where the lanes of a trunk leave it: where their ways part, or at the split point. */
29
44
  export type TrunkLeave = "part" | "split";
30
45
 
46
+ /** The place of the split point of a trunk: at a share of the way, or at the Gilbert point of the flow cost (EDA-030). */
47
+ export type TrunkSplit = "share" | "gilbert";
48
+
49
+ /** The shape of a group of the router `trunks`: a trunk and a split point, or a balancing-factor tree (NAT-017). */
50
+ export type TrunkFan = "split" | "tree";
51
+
31
52
  /** The rule that groups the edges by the collection of their target, in `data.collection`. */
32
53
  export const byCollection: GroupRule = (e) => {
33
54
  const c = e.data?.["collection"];
@@ -188,7 +209,28 @@ export function trunksRouter(options: TrunksOptions): Router {
188
209
  const c = { x: centers.reduce((a, p) => a + p.x, 0) / centers.length, y: centers.reduce((a, p) => a + p.y, 0) / centers.length };
189
210
  const plug: Plug = placePlug(s, first.source, c);
190
211
  const start = ways.out(plug);
191
- const split = splitPoint(start, centers, options.split ?? 0.5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
212
+ const key = id.split("\u0000")[2]!;
213
+ if (options.fan === "tree") {
214
+ // Each edge follows the tree from the plug to its target. The routes share the points of the tree.
215
+ const pts = group.map((e, i) => placePlug(targets[i]!, e.target, start));
216
+ const bf = Math.min(1, Math.max(0, options.bf ?? 0.4));
217
+ const fan = balancedFan(start, pts.map((pt) => ways.out(pt)), bf, (from, t) => ways.toPlug(from, pts[t]!) ?? [from, pts[t]!.point]);
218
+ const own: Route[] = group.map((e, i) => ({ edge: e.id, points: dedupe([plug.point, ...fanPath(fan, fan.leaf[i]!)]) }));
219
+ for (const r of own) points.set(r.edge, r.points);
220
+ const order = options.laneOrder ?? "bundle";
221
+ const common = own[0]!.points.slice(0, commonStart(own, 0));
222
+ bundles.push({ id: `b${bundles.length}`, key, node: s.id, edges: orderLanes(common, own, order), trunk: common });
223
+ for (const way of sharedWays(own, common.length)) {
224
+ const users = own.filter((r) => way.edges.includes(r.edge));
225
+ runs.push({ id: `r${runs.length}`, key, points: way.points, edges: orderLanes(way.points, users, order) });
226
+ }
227
+ continue;
228
+ }
229
+ const free = (p: Point): boolean => !ways.obstacles.inside(p, ways.margin);
230
+ const split =
231
+ options.splitAt === "gilbert"
232
+ ? gilbertPoint(start, centers, options.alpha ?? 0.5, free, ways.margin)
233
+ : splitPoint(start, centers, options.split ?? 0.5, free, ways.margin);
192
234
  const way = ways.fromPlug(plug, split);
193
235
  if (!way) continue;
194
236
  const trunk = dedupe(way);
@@ -211,7 +253,6 @@ export function trunksRouter(options: TrunksOptions): Router {
211
253
  const exit = leaveTrunk(trunk, branch, clear, 2 * ways.margin);
212
254
  if (exit) exits.push({ edge: e.id, exit });
213
255
  });
214
- const key = id.split("\u0000")[2]!;
215
256
  if (!part) {
216
257
  for (const r of own) points.set(r.edge, r.points);
217
258
  const edges = options.laneOrder === "run" || trunk.length < 2 ? laneOrder(dir, end, branches) : orderLanes(trunk, own, "bundle");
@@ -1,7 +1,9 @@
1
1
  import { arcCurve, type ArcOptions } from "../curves/arc/index.ts";
2
2
  import { clothoidCurve, type ClothoidOptions } from "../curves/clothoid/index.ts";
3
- import { highwayCurve, type JogStyle } from "../curves/highway/index.ts";
3
+ import { highwayCurve, highwayCurves, type JogStyle, type LaneCurve } from "../curves/highway/index.ts";
4
+ import type { CurveSegment } from "../curves/model.ts";
4
5
  import { roundedCurve, type RoundedOptions } from "../curves/rounded/index.ts";
6
+ import type { CurveSpace } from "../curves/space.ts";
5
7
  import { smoothCurve, type SmoothOptions } from "../curves/smooth/index.ts";
6
8
  import { splineCurve, type SplineOptions } from "../curves/spline/index.ts";
7
9
  import { straightCurve } from "../curves/straight/index.ts";
@@ -9,6 +11,10 @@ import { CUT_DEFAULTS, cutGroups, type CutOptions } from "../groups/cut/index.ts
9
11
  import { KD_DEFAULTS, kdGroups, type KdOptions } from "../groups/kd/index.ts";
10
12
  import { byDataGroup, keyGroups, type KeyOptions } from "../groups/keys/index.ts";
11
13
  import { quadtreeGroups } from "../groups/quadtree/index.ts";
14
+ import { CORRIDOR_DEFAULTS, corridorPlan, type CorridorOptions } from "../highways/corridors/index.ts";
15
+ import { keysPlan, type KeysOptions } from "../highways/keys/index.ts";
16
+ import type { HighwayInput } from "../highways/model.ts";
17
+ import { sharedWaysPlan, type SharedWaysOptions } from "../highways/shared-ways/index.ts";
12
18
  import type { Graph, Result, Router } from "../model.ts";
13
19
  import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
14
20
  import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
@@ -19,9 +25,9 @@ import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
19
25
  import { straightRouter } from "../routers/straight.ts";
20
26
  import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
21
27
  import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
22
- import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type Registry, type Strategy } from "./model.ts";
28
+ import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
23
29
  import type { OptionSchema } from "./options.ts";
24
- import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CUT_SCHEMA, HIGHWAY_SCHEMA, KD_SCHEMA, KEY_SCHEMA, LEVEL_ROOM_SCHEMA, RADIUS_SCHEMA, ROOM_SCHEMA, SECTION_SCHEMA, SECTIONS_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, TENSION_SCHEMA, TRUNKS_SCHEMA } from "./schemas.ts";
30
+ import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CUT_SCHEMA, HIGHWAY_SCHEMA, KD_SCHEMA, KEY_SCHEMA, LEVEL_ROOM_SCHEMA, RADIUS_SCHEMA, ROOM_SCHEMA, SECTION_SCHEMA, SECTIONS_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, TENSION_SCHEMA, TRUNKS_SCHEMA } from "./schemas.ts";
25
31
 
26
32
  /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
27
33
  export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
@@ -130,7 +136,10 @@ export interface TensionOptions {
130
136
  readonly tension: number;
131
137
  }
132
138
 
133
- /** The options of the curve strategy `highway`. */
139
+ /**
140
+ * The options of the curve strategy `highway`. The options `minRadius`, `jog` and `jogPieces` change only the lanes of a bundle.
141
+ * So they change the curve only when the input has a `lane` on a board with open segments.
142
+ */
134
143
  export interface HighwayStrategyOptions {
135
144
  /** The radius of the center line of a bundle at a bend, and of each lone corner. The default is 12. */
136
145
  readonly radius: number;
@@ -142,6 +151,33 @@ export interface HighwayStrategyOptions {
142
151
  readonly jogPieces: number;
143
152
  }
144
153
 
154
+ /** The curves of the lanes of each board, for the last options and the last free space. The strategy `highway` draws one lane in each call. */
155
+ const laneCurves = new WeakMap<LaneBoard, { readonly key: string; readonly space: CurveSpace | undefined; readonly curves: LaneCurve[] }>();
156
+
157
+ /**
158
+ * This function gives the curve of one lane of a board with `highwayCurves` and each option of the strategy `highway`.
159
+ * The lanes of a bundle bend as one. So the function draws all the lanes of the board one time, and keeps them for the next lane.
160
+ */
161
+ function highwayLane(input: CurveInput & Required<Pick<CurveInput, "lane">>, o: Readonly<HighwayStrategyOptions>): CurveSegment[] {
162
+ const { board, index } = input.lane;
163
+ const key = JSON.stringify([o.radius, o.minRadius, o.jog, o.jogPieces]);
164
+ let hit = laneCurves.get(board);
165
+ if (!hit || hit.key !== key || hit.space !== input.space) {
166
+ const curves = highwayCurves(board.graph, board.open, board.width, {
167
+ radius: o.radius,
168
+ minRadius: o.minRadius,
169
+ jog: o.jog,
170
+ jogPieces: o.jogPieces,
171
+ ...(board.maxWidth === undefined ? {} : { maxWidth: board.maxWidth }),
172
+ ...(input.space ? { space: input.space } : {}),
173
+ });
174
+ hit = { key, space: input.space, curves };
175
+ laneCurves.set(board, hit);
176
+ }
177
+ const lane = hit.curves[index];
178
+ return lane ? [...lane.segments] : highwayCurve(input.points, { radius: o.radius, ...(input.space ? { space: input.space } : {}) });
179
+ }
180
+
145
181
  /** The options of the curve strategy `smooth`. */
146
182
  export interface SmoothStrategyOptions {
147
183
  /** The radius of the arc of each corner. The default is 12. */
@@ -162,7 +198,7 @@ export interface ClothoidStrategyOptions {
162
198
 
163
199
  /**
164
200
  * The built-in curve strategies. The first six are in the order of `CURVE_TYPES`, and each one gives the same segments as `curveOf` with its type.
165
- * The strategy `highway` gives the curve of `arc` for one polyline. `highwayCurves` bends the lanes of a bundle as one.
201
+ * The strategy `highway` gives the curve of `arc` for one polyline. For a polyline with a `lane`, it bends the lanes of each bundle as one with `highwayCurves`.
166
202
  */
167
203
  export const curveStrategies: {
168
204
  readonly straight: Strategy<"curve", object>;
@@ -227,7 +263,8 @@ export const curveStrategies: {
227
263
  describe: "The lanes of a bundle bend as one, with concentric arcs at the lane spacing. One lane alone gets the curve of `arc`.",
228
264
  defaults: { radius: 12, minRadius: 4, jog: "biarc" as JogStyle, jogPieces: 4 },
229
265
  schema: HIGHWAY_SCHEMA,
230
- run: (input: CurveInput, o: Readonly<HighwayStrategyOptions>) => highwayCurve(input.points, { radius: o.radius, ...(input.space ? { space: input.space } : {}) }),
266
+ run: (input: CurveInput, o: Readonly<HighwayStrategyOptions>) =>
267
+ input.lane ? highwayLane({ ...input, lane: input.lane }, o) : highwayCurve(input.points, { radius: o.radius, ...(input.space ? { space: input.space } : {}) }),
231
268
  }),
232
269
  });
233
270
 
@@ -303,10 +340,49 @@ export const groupStrategies: {
303
340
  }),
304
341
  });
305
342
 
343
+ /**
344
+ * The built-in highway strategies: the decisions of the routers `trunks` and `shared` as plans, and corridor signatures (HWY-031).
345
+ * `follow` routes the edges along a plan, and `highwayRouter` gives a router that does both.
346
+ */
347
+ export const highwayStrategies: {
348
+ readonly keys: Strategy<"highway", KeysOptions>;
349
+ readonly "shared-ways": Strategy<"highway", SharedWaysOptions>;
350
+ readonly corridors: Strategy<"highway", CorridorOptions>;
351
+ } = Object.freeze({
352
+ keys: defineStrategy({
353
+ step: "highway",
354
+ name: "keys",
355
+ describe: "The highways of the router `trunks`. The edges from one plug with one key share a trunk, and the edges that go on together share a run past it.",
356
+ defaults: { rule: byCollection } as KeysOptions,
357
+ schema: TRUNKS_SCHEMA,
358
+ maxNodes: 10_000,
359
+ run: (input: HighwayInput, o: Readonly<KeysOptions>) => keysPlan(input, o),
360
+ }),
361
+ "shared-ways": defineStrategy({
362
+ step: "highway",
363
+ name: "shared-ways",
364
+ describe: "The highways of the router `shared`. The edges of one key share their ways through the corner graph, and each shared stretch is a highway.",
365
+ defaults: { rule: byCollection } as SharedWaysOptions,
366
+ schema: SHARED_SCHEMA,
367
+ maxNodes: 10_000,
368
+ run: (input: HighwayInput, o: Readonly<SharedWaysOptions>) => sharedWaysPlan(input, o),
369
+ }),
370
+ corridors: defineStrategy({
371
+ step: "highway",
372
+ name: "corridors",
373
+ describe: "Corridor signatures. Edges whose first ways share long links make a cluster, found with MinHash. The longest common run of each cluster is its highway.",
374
+ defaults: { ...CORRIDOR_DEFAULTS } as CorridorOptions,
375
+ schema: CORRIDOR_SCHEMA,
376
+ maxNodes: 10_000,
377
+ run: (input: HighwayInput, o: Readonly<CorridorOptions>) => corridorPlan(input, o),
378
+ }),
379
+ });
380
+
306
381
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
307
382
  export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
308
383
  ...Object.values(groupStrategies),
309
384
  ...Object.values(routerStrategies),
385
+ ...Object.values(highwayStrategies),
310
386
  ...Object.values(curveStrategies),
311
387
  ...Object.values(roomStrategies),
312
388
  ]);
@@ -1,9 +1,11 @@
1
1
  import type { CurveSegment } from "../curves/model.ts";
2
2
  import type { CurveSpace } from "../curves/space.ts";
3
3
  import type { Point } from "../geom.ts";
4
+ import type { HighwayInput, HighwayPlan } from "../highways/model.ts";
4
5
  import type { Graph, Result, Router } from "../model.ts";
5
6
  import type { Boxed, Gap, RoomLink } from "../room/index.ts";
6
7
  import type { Section } from "../sections.ts";
8
+ import type { SegmentGraph } from "../segments.ts";
7
9
  import type { OptionSchema } from "./options.ts";
8
10
 
9
11
  /** The input of a curve strategy: one polyline and the context of its route. */
@@ -16,6 +18,23 @@ export interface CurveInput {
16
18
  readonly endNormal?: Point;
17
19
  /** The boxes that the curve must not go through, from `curveSpace`. */
18
20
  readonly space?: CurveSpace;
21
+ /**
22
+ * The lane of the polyline on its board, for a strategy that bends the lanes of a bundle as one. Then `points` is the route of `laneRoutes` with the index `index`.
23
+ * The other strategies do not read it.
24
+ */
25
+ readonly lane?: { readonly board: LaneBoard; readonly index: number };
26
+ }
27
+
28
+ /** The lanes of the bundles of one board: the input of `laneRoutes` and `highwayCurves`. */
29
+ export interface LaneBoard {
30
+ /** The shared segments and the paths of the routes, from `segmentsOf`. */
31
+ readonly graph: SegmentGraph;
32
+ /** The keys of the open segments, from `runKey`. Each edge of an open segment gets its own lane. */
33
+ readonly open: ReadonlySet<string>;
34
+ /** The distance between two lanes of an open segment. */
35
+ readonly width: number;
36
+ /** The largest width of an open segment, from its outer lane to its outer lane. The default is `Infinity`. */
37
+ readonly maxWidth?: number;
19
38
  }
20
39
 
21
40
  /** The input of a room strategy: the boxes of a board and the links between them. */
@@ -34,12 +53,14 @@ export interface RoomInput {
34
53
  *
35
54
  * - `group`: a graph in, the tree of sections out. The router `sections` makes its hubs and its zoom levels from this tree.
36
55
  * - `router`: a graph in, the routes (and the `levels`, `bundles` and `runs`) out.
56
+ * - `highway`: a graph and optionally its first routes in, a highway plan out. `follow` routes the edges along the plan.
37
57
  * - `curve`: a polyline in, the segments of a curve out.
38
58
  * - `room`: the boxes and links of a board in, the gaps of the board with their need out.
39
59
  */
40
60
  export interface StepTypes {
41
61
  readonly group: { readonly input: Graph; readonly output: Section };
42
62
  readonly router: { readonly input: Graph; readonly output: Result };
63
+ readonly highway: { readonly input: HighwayInput; readonly output: HighwayPlan };
43
64
  readonly curve: { readonly input: CurveInput; readonly output: CurveSegment[] };
44
65
  readonly room: { readonly input: RoomInput; readonly output: Gap[] };
45
66
  }
@@ -54,7 +75,7 @@ export type StepInput<S extends Step> = StepTypes[S]["input"];
54
75
  export type StepOutput<S extends Step> = StepTypes[S]["output"];
55
76
 
56
77
  /** The steps that have strategies, in the order of the pipeline. */
57
- export const STEPS: readonly Step[] = ["group", "router", "curve", "room"];
78
+ export const STEPS: readonly Step[] = ["group", "router", "highway", "curve", "room"];
58
79
 
59
80
  /**
60
81
  * A strategy: one way to do one step, with a name and a description.
@@ -126,25 +147,34 @@ export function defineStrategy<S extends Step, O extends object>(definition: Str
126
147
  /**
127
148
  * A registry: the strategies of each step, in order. A registry is data. No import changes it.
128
149
  * A consumer makes its own registry with `createRegistry`, and adds a strategy with `register`.
129
- * The list `group` is optional in the type, so a registry from version 0.1.5 stays a registry. `createRegistry` always gives it.
150
+ * The lists `group` and `highway` are optional in the type, so a registry from version 0.1.5 or 0.3.0 stays a registry. `createRegistry` always gives them.
130
151
  */
131
- export type Registry = { readonly [S in Exclude<Step, "group">]: readonly Strategy<S>[] } & { readonly group?: readonly Strategy<"group">[] };
152
+ export type Registry = { readonly [S in Exclude<Step, "group" | "highway">]: readonly Strategy<S>[] } & {
153
+ readonly group?: readonly Strategy<"group">[];
154
+ readonly highway?: readonly Strategy<"highway">[];
155
+ };
132
156
 
133
157
  /** This function makes a registry from a list of strategies. It keeps their order. Two strategies with one step and one name are an error. */
134
158
  export function createRegistry(strategies: readonly AnyStrategy[]): Registry {
135
- const out: { [S in Step]: Strategy<S>[] } = { group: [], router: [], curve: [], room: [] };
159
+ const out: { [S in Step]: Strategy<S>[] } = { group: [], router: [], highway: [], curve: [], room: [] };
136
160
  for (const s of strategies) {
137
161
  const list = out[s.step] as AnyStrategy[] | undefined;
138
162
  if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
139
163
  if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
140
164
  list.push(s);
141
165
  }
142
- return Object.freeze({ group: Object.freeze(out.group), router: Object.freeze(out.router), curve: Object.freeze(out.curve), room: Object.freeze(out.room) });
166
+ return Object.freeze({
167
+ group: Object.freeze(out.group),
168
+ router: Object.freeze(out.router),
169
+ highway: Object.freeze(out.highway),
170
+ curve: Object.freeze(out.curve),
171
+ room: Object.freeze(out.room),
172
+ });
143
173
  }
144
174
 
145
175
  /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
146
176
  export function register(registry: Registry, ...strategies: readonly AnyStrategy[]): Registry {
147
- return createRegistry([...(registry.group ?? []), ...registry.router, ...registry.curve, ...registry.room, ...strategies]);
177
+ return createRegistry([...(registry.group ?? []), ...registry.router, ...(registry.highway ?? []), ...registry.curve, ...registry.room, ...strategies]);
148
178
  }
149
179
 
150
180
  /** This function gives the strategies of one step of a registry, in order. */
@@ -1,11 +1,13 @@
1
1
  import type { CutOptions } from "../groups/cut/index.ts";
2
2
  import type { KdOptions } from "../groups/kd/index.ts";
3
3
  import type { KeyOptions } from "../groups/keys/index.ts";
4
+ import type { CorridorOptions } from "../highways/corridors/index.ts";
4
5
  import type { CornersOptions } from "../routers/corners.ts";
5
6
  import type { SectionsOptions } from "../routers/sections.ts";
6
7
  import type { SharedOptions } from "../routers/shared.ts";
7
8
  import type { TrunksOptions } from "../routers/trunks.ts";
8
9
  import type { SectionOptions } from "../sections.ts";
10
+ import type { WayOptions } from "../ways.ts";
9
11
  import type { RunOrder } from "../segments.ts";
10
12
  import type { ClothoidStrategyOptions, HighwayStrategyOptions, LevelRoomPreset, RadiusOptions, RoomPreset, RouterPreset, SmoothStrategyOptions, TensionOptions } from "./builtins.ts";
11
13
  import type { EnumOption, OptionSchema } from "./options.ts";
@@ -14,7 +16,7 @@ import type { EnumOption, OptionSchema } from "./options.ts";
14
16
  // The test of the defaults runs each strategy with `optionDefaults` and with no option, and compares the output.
15
17
 
16
18
  /** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
17
- const WAY_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
19
+ const WAY_SCHEMA: OptionSchema<WayOptions> = {
18
20
  margin: { type: "number", min: 1, max: 20, step: 1, default: 5, describe: "The distance from a box to the corners of the graph." },
19
21
  k: { type: "integer", min: 2, max: 16, step: 1, default: 8, describe: "The number of visible neighbours of each corner." },
20
22
  smooth: { type: "boolean", default: false, describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners." },
@@ -32,7 +34,23 @@ const LANE_ORDER: EnumOption<RunOrder> = {
32
34
  const RUN_LANE_ORDER: EnumOption<RunOrder> = { ...LANE_ORDER, default: "run" };
33
35
 
34
36
  /** The declarations of the options of the routers `corners` and `corners-smooth`. */
35
- export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = WAY_SCHEMA;
37
+ export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
38
+ ...WAY_SCHEMA,
39
+ plugs: {
40
+ type: "enum",
41
+ values: ["facing", "side"],
42
+ default: "facing",
43
+ describe: "With `facing`, a free plug faces the other end. With `side`, the search starts from the whole free side, and the plug goes where the way leaves (WILD-001).",
44
+ },
45
+ plugGap: {
46
+ type: "number",
47
+ min: 0,
48
+ max: 24,
49
+ step: 1,
50
+ default: 0,
51
+ describe: "With a gap above 0, the free plugs of a side keep their order and at least this gap (WILD-002). 0 keeps each plug where it is.",
52
+ },
53
+ };
36
54
 
37
55
  /** The declarations of the options of the router `trunks`. */
38
56
  export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
@@ -47,6 +65,20 @@ export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
47
65
  default: "part",
48
66
  describe: "With `part`, a lane leaves the trunk where its way parts from the other ways. With `split`, each lane leaves at the split point.",
49
67
  },
68
+ splitAt: {
69
+ type: "enum",
70
+ values: ["share", "gilbert"],
71
+ default: "share",
72
+ describe: "With `share`, the split point is at `split` of the way. With `gilbert`, it is where the flow cost is least (EDA-030).",
73
+ },
74
+ alpha: { type: "number", min: 0, max: 1, step: 0.05, default: 0.5, describe: "The exponent of the flow cost of the split `gilbert`. A smaller value moves the split toward the targets." },
75
+ fan: {
76
+ type: "enum",
77
+ values: ["split", "tree"],
78
+ default: "split",
79
+ describe: "With `split`, the edges of a group share a trunk to a split point. With `tree`, they grow a balancing-factor tree (NAT-017).",
80
+ },
81
+ bf: { type: "number", min: 0, max: 1, step: 0.05, default: 0.4, describe: "The balancing factor of the fan `tree`. 0 gives the least ink, and a larger factor gives more direct routes." },
50
82
  };
51
83
 
52
84
  /** The declarations of the options of the routers `shared` and `shared-all`. */
@@ -62,6 +94,38 @@ export const SHARED_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
62
94
  describe: "The cost of a link that an edge with the same key uses already, as a share of its length.",
63
95
  },
64
96
  laneOrder: LANE_ORDER,
97
+ cost: {
98
+ type: "enum",
99
+ values: ["fixed", "flow"],
100
+ default: "fixed",
101
+ describe: "With `fixed`, a shared link costs `discount` of its length. With `flow`, a link with `f` edges costs `len * f^alpha` (EDA-029).",
102
+ },
103
+ alpha: { type: "number", min: 0.1, max: 1, step: 0.05, default: 0.6, describe: "The exponent of the cost `flow`. A smaller value gives more sharing." },
104
+ passes: { type: "integer", min: 1, max: 5, step: 1, default: 1, describe: "In each pass after the first, each edge finds its way again with the ways of the other edges fixed (HWY-039)." },
105
+ order: {
106
+ type: "enum",
107
+ values: ["span", "shortest", "load", "input"],
108
+ default: "span",
109
+ describe: "The order of the edges of a key: the longest first, the shortest first, the most edges between two sections first, or the input order (NAT-024).",
110
+ },
111
+ merge: {
112
+ type: "enum",
113
+ values: ["corner", "snell"],
114
+ default: "corner",
115
+ describe: "With `corner`, a route joins a run at a corner of the graph. With `snell`, it joins at the angle `acos(d)` (NAT-035).",
116
+ },
117
+ };
118
+
119
+ /** The declarations of the options of the highway strategy `corridors` (HWY-031). */
120
+ export const CORRIDOR_SCHEMA: OptionSchema<CorridorOptions> = {
121
+ margin: WAY_SCHEMA.margin,
122
+ k: WAY_SCHEMA.k,
123
+ rho: { type: "number", min: 0.1, max: 0.8, step: 0.05, default: 0.3, describe: "Two edges join when their common links are this share of the shorter way, or longer." },
124
+ minSize: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A highway has this number of edges or more." },
125
+ hashes: { type: "integer", min: 16, max: 128, step: 16, scale: "log", marks: [16, 32, 64, 128], default: 64, describe: "The number of hash functions of the signature of a way." },
126
+ bands: { type: "integer", min: 4, max: 32, step: 4, scale: "log", marks: [4, 8, 16, 32], default: 32, describe: "The number of bands of the signature. Two ways with one equal band are a candidate pair." },
127
+ near: { type: "number", min: 0, max: 8, step: 1, default: 0, describe: "Two link ends nearer than this grid size are one point. With 0, only equal points are one point." },
128
+ lane: { type: "number", min: 1, max: 16, step: 1, default: 4, describe: "The gap between two lanes. A common stretch shorter than the break-even overlap for this gap does not join two edges." },
65
129
  };
66
130
 
67
131
  /** The declarations of the limits of the tree of sections (`SectionOptions`). */