@mark1russell7/highways 0.2.0 → 0.4.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 (66) hide show
  1. package/README.md +265 -9
  2. package/build/index.d.ts +2168 -101
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +11624 -3422
  5. package/build/index.js.map +1 -1
  6. package/package.json +4 -1
  7. package/src/bench/compare.ts +180 -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/screen/forms.ts +372 -0
  24. package/src/bench/screen/generators.ts +54 -0
  25. package/src/bench/screen/hash.ts +129 -0
  26. package/src/bench/screen/index.ts +6 -0
  27. package/src/bench/screen/measures.ts +114 -0
  28. package/src/bench/screen/screen.ts +390 -0
  29. package/src/bench/screen/set.ts +136 -0
  30. package/src/bench/suite.ts +839 -0
  31. package/src/cases.ts +41 -5
  32. package/src/curves/clothoid/index.ts +150 -0
  33. package/src/curves/corner.ts +116 -0
  34. package/src/curves/fit.ts +79 -0
  35. package/src/curves/highway/index.ts +636 -0
  36. package/src/curves/index.ts +16 -1
  37. package/src/curves/model.ts +4 -2
  38. package/src/curves/smooth/index.ts +50 -0
  39. package/src/curves/space.ts +76 -0
  40. package/src/groups/cut/index.ts +292 -0
  41. package/src/groups/index.ts +2 -0
  42. package/src/groups/kd/index.ts +207 -0
  43. package/src/highways/corridors/index.ts +305 -0
  44. package/src/highways/follow.ts +155 -0
  45. package/src/highways/index.ts +7 -0
  46. package/src/highways/keys/index.ts +15 -0
  47. package/src/highways/model.ts +111 -0
  48. package/src/highways/result.ts +34 -0
  49. package/src/highways/router.ts +40 -0
  50. package/src/highways/shared-ways/index.ts +15 -0
  51. package/src/index.ts +6 -0
  52. package/src/levels.ts +163 -5
  53. package/src/room/bends.ts +203 -0
  54. package/src/room/index.ts +28 -0
  55. package/src/room/levels/index.ts +155 -0
  56. package/src/routers/sections.ts +15 -40
  57. package/src/routers/shared.ts +7 -2
  58. package/src/routers/trunks.ts +131 -6
  59. package/src/sections.ts +47 -1
  60. package/src/segments.ts +81 -31
  61. package/src/strategies/builtins.ts +206 -9
  62. package/src/strategies/index.ts +1 -0
  63. package/src/strategies/model.ts +55 -7
  64. package/src/strategies/options.ts +296 -0
  65. package/src/strategies/schemas.ts +234 -0
  66. package/src/visibility.ts +190 -1
@@ -1,19 +1,33 @@
1
1
  import { arcCurve, type ArcOptions } from "../curves/arc/index.ts";
2
+ import { clothoidCurve, type ClothoidOptions } from "../curves/clothoid/index.ts";
3
+ import { highwayCurve, highwayCurves, type JogStyle, type LaneCurve } from "../curves/highway/index.ts";
4
+ import type { CurveSegment } from "../curves/model.ts";
2
5
  import { roundedCurve, type RoundedOptions } from "../curves/rounded/index.ts";
6
+ import type { CurveSpace } from "../curves/space.ts";
7
+ import { smoothCurve, type SmoothOptions } from "../curves/smooth/index.ts";
3
8
  import { splineCurve, type SplineOptions } from "../curves/spline/index.ts";
4
9
  import { straightCurve } from "../curves/straight/index.ts";
10
+ import { CUT_DEFAULTS, cutGroups, type CutOptions } from "../groups/cut/index.ts";
11
+ import { KD_DEFAULTS, kdGroups, type KdOptions } from "../groups/kd/index.ts";
5
12
  import { byDataGroup, keyGroups, type KeyOptions } from "../groups/keys/index.ts";
6
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";
7
18
  import type { Graph, Result, Router } from "../model.ts";
8
19
  import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
20
+ import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
9
21
  import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
10
22
  import { plugsRouter } from "../routers/plugs.ts";
11
23
  import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
12
24
  import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
13
25
  import { straightRouter } from "../routers/straight.ts";
14
26
  import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
15
- import { SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
16
- import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type Registry, type Strategy } from "./model.ts";
27
+ import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
28
+ import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
29
+ import type { OptionSchema } from "./options.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";
17
31
 
18
32
  /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
19
33
  export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
@@ -25,13 +39,14 @@ function routerStrategy<O extends object>(
25
39
  maxNodes: number,
26
40
  defaults: O,
27
41
  make: (options: O & { readonly name: string }) => Router,
42
+ schema: OptionSchema<O>,
28
43
  ): Strategy<"router", O> {
29
- return defineStrategy({ step: "router", name, describe, defaults, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
44
+ return defineStrategy({ step: "router", name, describe, defaults, schema, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
30
45
  }
31
46
 
32
47
  /** This function makes a router strategy for a router without options. */
33
48
  function fixedRouter(router: Router, describe: string): Strategy<"router", object> {
34
- return defineStrategy({ step: "router", name: router.name, describe, defaults: {}, run: (graph: Graph): Result => router.route(graph) });
49
+ return defineStrategy({ step: "router", name: router.name, describe, defaults: {}, schema: {}, run: (graph: Graph): Result => router.route(graph) });
35
50
  }
36
51
 
37
52
  /** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
@@ -57,6 +72,7 @@ export const routerStrategies: {
57
72
  10_000,
58
73
  {},
59
74
  cornersRouter,
75
+ CORNERS_SCHEMA,
60
76
  ),
61
77
  "corners-smooth": routerStrategy<RouterPreset<CornersOptions>>(
62
78
  "corners-smooth",
@@ -64,6 +80,7 @@ export const routerStrategies: {
64
80
  10_000,
65
81
  { smooth: true },
66
82
  cornersRouter,
83
+ CORNERS_SCHEMA,
67
84
  ),
68
85
  trunks: routerStrategy<RouterPreset<TrunksOptions>>(
69
86
  "trunks",
@@ -71,6 +88,7 @@ export const routerStrategies: {
71
88
  10_000,
72
89
  { rule: byCollection },
73
90
  trunksRouter,
91
+ TRUNKS_SCHEMA,
74
92
  ),
75
93
  shared: routerStrategy<RouterPreset<SharedOptions>>(
76
94
  "shared",
@@ -78,14 +96,16 @@ export const routerStrategies: {
78
96
  10_000,
79
97
  { rule: byCollection },
80
98
  sharedRouter,
99
+ SHARED_SCHEMA,
81
100
  ),
82
- "shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter),
101
+ "shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter, SHARED_SCHEMA),
83
102
  sections: routerStrategy<RouterPreset<SectionsOptions>>(
84
103
  "sections",
85
104
  "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
86
105
  100_000,
87
106
  {},
88
107
  sectionsRouter,
108
+ SECTIONS_SCHEMA,
89
109
  ),
90
110
  "sections-tree": routerStrategy<RouterPreset<SectionsOptions>>(
91
111
  "sections-tree",
@@ -93,6 +113,7 @@ export const routerStrategies: {
93
113
  100_000,
94
114
  { stretch: Infinity },
95
115
  sectionsRouter,
116
+ SECTIONS_SCHEMA,
96
117
  ),
97
118
  });
98
119
 
@@ -115,19 +136,86 @@ export interface TensionOptions {
115
136
  readonly tension: number;
116
137
  }
117
138
 
118
- /** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
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
+ */
143
+ export interface HighwayStrategyOptions {
144
+ /** The radius of the center line of a bundle at a bend, and of each lone corner. The default is 12. */
145
+ readonly radius: number;
146
+ /** The smallest radius of a lane at a bend of a bundle. The default is 4. */
147
+ readonly minRadius: number;
148
+ /** The shape of the S curve where a lane changes its offset. The default is `biarc`. */
149
+ readonly jog: JogStyle;
150
+ /** The number of cubic pieces of a `quintic` S curve. The default is 4. */
151
+ readonly jogPieces: number;
152
+ }
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
+
181
+ /** The options of the curve strategy `smooth`. */
182
+ export interface SmoothStrategyOptions {
183
+ /** The radius of the arc of each corner. The default is 12. */
184
+ readonly radius: number;
185
+ /** The share of the turn of a corner in its two spirals, from 0 to 1. The default is 0.6. */
186
+ readonly smoothing: number;
187
+ }
188
+
189
+ /** The options of the curve strategy `clothoid`. */
190
+ export interface ClothoidStrategyOptions {
191
+ /** The radius of the arc of each corner. The default is 12. */
192
+ readonly radius: number;
193
+ /** The share of the turn of a corner in its two clothoids, from 0 to 1. The default is 0.5. */
194
+ readonly spiral: number;
195
+ /** The largest distance from the cubic pieces to the true clothoid. The default is 0.25. */
196
+ readonly tolerance: number;
197
+ }
198
+
199
+ /**
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.
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`.
202
+ */
119
203
  export const curveStrategies: {
120
204
  readonly straight: Strategy<"curve", object>;
121
205
  readonly rounded: Strategy<"curve", RadiusOptions>;
122
206
  readonly spline: Strategy<"curve", TensionOptions>;
123
207
  readonly arc: Strategy<"curve", RadiusOptions>;
208
+ readonly smooth: Strategy<"curve", SmoothStrategyOptions>;
209
+ readonly clothoid: Strategy<"curve", ClothoidStrategyOptions>;
210
+ readonly highway: Strategy<"curve", HighwayStrategyOptions>;
124
211
  } = Object.freeze({
125
- straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, run: (input: CurveInput) => straightCurve(input.points) }),
212
+ straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, schema: {}, run: (input: CurveInput) => straightCurve(input.points) }),
126
213
  rounded: defineStrategy({
127
214
  step: "curve",
128
215
  name: "rounded",
129
216
  describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
130
217
  defaults: { radius: 12 },
218
+ schema: RADIUS_SCHEMA,
131
219
  run: (input: CurveInput, o: Readonly<RadiusOptions>) => roundedCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies RoundedOptions),
132
220
  }),
133
221
  spline: defineStrategy({
@@ -135,6 +223,7 @@ export const curveStrategies: {
135
223
  name: "spline",
136
224
  describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
137
225
  defaults: { tension: 0 },
226
+ schema: TENSION_SCHEMA,
138
227
  run: (input: CurveInput, o: Readonly<TensionOptions>) => splineCurve(input.points, { ...contextOf(input), tension: o.tension } satisfies SplineOptions),
139
228
  }),
140
229
  arc: defineStrategy({
@@ -142,35 +231,87 @@ export const curveStrategies: {
142
231
  name: "arc",
143
232
  describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
144
233
  defaults: { radius: 12 },
234
+ schema: RADIUS_SCHEMA,
145
235
  run: (input: CurveInput, o: Readonly<RadiusOptions>) => arcCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies ArcOptions),
146
236
  }),
237
+ smooth: defineStrategy({
238
+ step: "curve",
239
+ name: "smooth",
240
+ describe: "A cubic spiral, an arc and a second cubic spiral at each corner, as the corner smoothing of Figma. The curvature has no jump.",
241
+ defaults: { radius: 12, smoothing: 0.6 },
242
+ schema: SMOOTH_SCHEMA,
243
+ run: (input: CurveInput, o: Readonly<SmoothStrategyOptions>) =>
244
+ smoothCurve(input.points, { ...(input.space ? { space: input.space } : {}), radius: o.radius, smoothing: o.smoothing } satisfies SmoothOptions),
245
+ }),
246
+ clothoid: defineStrategy({
247
+ step: "curve",
248
+ name: "clothoid",
249
+ describe: "A clothoid, an arc and a second clothoid at each corner, as a transition curve of a road. Cubic pieces draw each clothoid.",
250
+ defaults: { radius: 12, spiral: 0.5, tolerance: 0.25 },
251
+ schema: CLOTHOID_SCHEMA,
252
+ run: (input: CurveInput, o: Readonly<ClothoidStrategyOptions>) =>
253
+ clothoidCurve(input.points, {
254
+ ...(input.space ? { space: input.space } : {}),
255
+ radius: o.radius,
256
+ spiral: o.spiral,
257
+ tolerance: o.tolerance,
258
+ } satisfies ClothoidOptions),
259
+ }),
260
+ highway: defineStrategy({
261
+ step: "curve",
262
+ name: "highway",
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`.",
264
+ defaults: { radius: 12, minRadius: 4, jog: "biarc" as JogStyle, jogPieces: 4 },
265
+ schema: HIGHWAY_SCHEMA,
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 } : {}) }),
268
+ }),
147
269
  });
148
270
 
149
271
  /** The options of a room strategy: the options of `roomDemand`, without the method. */
150
272
  export type RoomPreset = Omit<RoomOptions, "method">;
151
273
 
152
274
  const roomStrategy = (method: RoomMethod, describe: string): Strategy<"room", RoomPreset> =>
153
- defineStrategy<"room", RoomPreset>({ step: "room", name: method, describe, defaults: {}, run: (input, o) => roomDemand(input.items, input.links, { ...o, method }) });
275
+ defineStrategy<"room", RoomPreset>({ step: "room", name: method, describe, defaults: {}, schema: ROOM_SCHEMA, run: (input, o) => roomDemand(input.items, input.links, { ...o, method }) });
154
276
 
155
- /** The built-in room strategies: the methods of `roomDemand`. */
277
+ /** The options of the room strategy `levels`: the options of `levelRoom` without the method, and the limits of the quadtree. */
278
+ export type LevelRoomPreset = Omit<LevelRoomOptions, "method"> & SectionOptions;
279
+
280
+ /** The built-in room strategies: the methods of `roomDemand`, and the room of each zoom level (`levelRoom`). */
156
281
  export const roomStrategies: {
157
282
  readonly routes: Strategy<"room", RoomPreset>;
158
283
  readonly lines: Strategy<"room", RoomPreset>;
284
+ readonly levels: Strategy<"room", LevelRoomPreset>;
159
285
  } = Object.freeze({
160
286
  routes: roomStrategy("routes", "The lanes of a gap are the most routes that run along it side by side, with each gap open to the gutter."),
161
287
  lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap."),
288
+ levels: defineStrategy<"room", LevelRoomPreset>({
289
+ step: "room",
290
+ name: "levels",
291
+ describe: "Each zoom level gets the gaps that its links need. The far levels open first, and a box moves with its parent.",
292
+ defaults: {},
293
+ schema: LEVEL_ROOM_SCHEMA,
294
+ run: (input, o) => {
295
+ const { cap, maxDepth, collapse, ...room } = o;
296
+ const root = input.sections ?? buildSections(input.items, { cap: cap ?? SECTION_DEFAULTS.cap, maxDepth: maxDepth ?? SECTION_DEFAULTS.maxDepth, collapse: collapse ?? SECTION_DEFAULTS.collapse });
297
+ return levelRoom(input.items, input.links, root, room).gaps;
298
+ },
299
+ }),
162
300
  });
163
301
 
164
302
  /** The built-in grouping rules: the strategies of the step `group`. The router `sections` makes its hubs and its zoom levels from their tree. */
165
303
  export const groupStrategies: {
166
304
  readonly quadtree: Strategy<"group", Required<SectionOptions>>;
167
305
  readonly keys: Strategy<"group", KeyOptions>;
306
+ readonly kd: Strategy<"group", Required<KdOptions>>;
307
+ readonly cut: Strategy<"group", Required<CutOptions>>;
168
308
  } = Object.freeze({
169
309
  quadtree: defineStrategy({
170
310
  step: "group",
171
311
  name: "quadtree",
172
312
  describe: "The cells of a quadtree of the plane. A cell with more than `cap` nodes splits in four. It is the default rule of the router `sections`.",
173
313
  defaults: { ...SECTION_DEFAULTS },
314
+ schema: SECTION_SCHEMA,
174
315
  run: (graph: Graph, o: Readonly<Required<SectionOptions>>) => quadtreeGroups(graph, o),
175
316
  }),
176
317
  keys: defineStrategy({
@@ -178,14 +319,70 @@ export const groupStrategies: {
178
319
  name: "keys",
179
320
  describe: "One group for each key of the nodes. The default key is the string `data.group` of the node. A node without a key is a group alone.",
180
321
  defaults: { key: byDataGroup } as KeyOptions,
322
+ schema: KEY_SCHEMA,
181
323
  run: (graph: Graph, o: Readonly<KeyOptions>) => keyGroups(graph, o),
182
324
  }),
325
+ kd: defineStrategy({
326
+ step: "group",
327
+ name: "kd",
328
+ describe: "The cells of a k-d tree of the plane. A cell with more than `cap` nodes splits in two with a line across one axis, and each half splits again.",
329
+ defaults: { ...KD_DEFAULTS },
330
+ schema: KD_SCHEMA,
331
+ run: (graph: Graph, o: Readonly<Required<KdOptions>>) => kdGroups(graph, o),
332
+ }),
333
+ cut: defineStrategy({
334
+ step: "group",
335
+ name: "cut",
336
+ describe: "A guillotine cut that reads the edges. Each line goes where it cuts the fewest edges for its balance, so the far view has fewer links.",
337
+ defaults: { ...CUT_DEFAULTS },
338
+ schema: CUT_SCHEMA,
339
+ run: (graph: Graph, o: Readonly<Required<CutOptions>>) => cutGroups(graph, o),
340
+ }),
341
+ });
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
+ }),
183
379
  });
184
380
 
185
381
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
186
382
  export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
187
383
  ...Object.values(groupStrategies),
188
384
  ...Object.values(routerStrategies),
385
+ ...Object.values(highwayStrategies),
189
386
  ...Object.values(curveStrategies),
190
387
  ...Object.values(roomStrategies),
191
388
  ]);
@@ -1,2 +1,3 @@
1
1
  export * from "./model.ts";
2
2
  export * from "./builtins.ts";
3
+ export * from "./options.ts";
@@ -1,9 +1,12 @@
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";
9
+ import type { OptionSchema } from "./options.ts";
7
10
 
8
11
  /** The input of a curve strategy: one polyline and the context of its route. */
9
12
  export interface CurveInput {
@@ -15,12 +18,34 @@ export interface CurveInput {
15
18
  readonly endNormal?: Point;
16
19
  /** The boxes that the curve must not go through, from `curveSpace`. */
17
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;
18
38
  }
19
39
 
20
40
  /** The input of a room strategy: the boxes of a board and the links between them. */
21
41
  export interface RoomInput {
22
42
  readonly items: readonly Boxed[];
23
43
  readonly links: readonly RoomLink[];
44
+ /**
45
+ * The tree of sections of the items, for a strategy that gives room to each zoom level. The indices of its nodes are the indices of `items`.
46
+ * Without it, such a strategy makes the quadtree of `buildSections`. The other strategies do not read it.
47
+ */
48
+ readonly sections?: Section;
24
49
  }
25
50
 
26
51
  /**
@@ -28,12 +53,14 @@ export interface RoomInput {
28
53
  *
29
54
  * - `group`: a graph in, the tree of sections out. The router `sections` makes its hubs and its zoom levels from this tree.
30
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.
31
57
  * - `curve`: a polyline in, the segments of a curve out.
32
58
  * - `room`: the boxes and links of a board in, the gaps of the board with their need out.
33
59
  */
34
60
  export interface StepTypes {
35
61
  readonly group: { readonly input: Graph; readonly output: Section };
36
62
  readonly router: { readonly input: Graph; readonly output: Result };
63
+ readonly highway: { readonly input: HighwayInput; readonly output: HighwayPlan };
37
64
  readonly curve: { readonly input: CurveInput; readonly output: CurveSegment[] };
38
65
  readonly room: { readonly input: RoomInput; readonly output: Gap[] };
39
66
  }
@@ -48,7 +75,7 @@ export type StepInput<S extends Step> = StepTypes[S]["input"];
48
75
  export type StepOutput<S extends Step> = StepTypes[S]["output"];
49
76
 
50
77
  /** The steps that have strategies, in the order of the pipeline. */
51
- export const STEPS: readonly Step[] = ["group", "router", "curve", "room"];
78
+ export const STEPS: readonly Step[] = ["group", "router", "highway", "curve", "room"];
52
79
 
53
80
  /**
54
81
  * A strategy: one way to do one step, with a name and a description.
@@ -63,6 +90,8 @@ export interface Strategy<S extends Step = Step, O extends object = object> {
63
90
  readonly describe: string;
64
91
  /** The values of the options when the caller gives no value. */
65
92
  readonly defaults: Readonly<O>;
93
+ /** The declarations of the options: the type, the range, the step and the meaning of each one. A strategy without it declares nothing. */
94
+ readonly schema?: OptionSchema<O>;
66
95
  /** The benchmark skips an input with more nodes than this. Without it, the strategy takes each input. */
67
96
  readonly maxNodes?: number;
68
97
  /** This method does the step. Each option that `options` does not give gets its value from `defaults`. */
@@ -79,23 +108,33 @@ export interface StrategyDefinition<S extends Step, O extends object> {
79
108
  readonly describe: string;
80
109
  /** These values are the options when the caller gives no value. Give `{}` for a strategy without options. */
81
110
  readonly defaults: O;
111
+ /** These declarations tell the type, the range, the step and the meaning of each option. `checkSchema` examines them. */
112
+ readonly schema?: NoInfer<OptionSchema<O>>;
82
113
  readonly maxNodes?: number;
83
114
  /** This function does the step. It gets each option, from the caller or from `defaults`. */
84
115
  run(input: StepInput<S>, options: Readonly<O>): StepOutput<S>;
85
116
  }
86
117
 
118
+ /** This function gives a frozen copy of a schema, with each declaration frozen. */
119
+ function freezeSchema<O>(schema: OptionSchema<O>): OptionSchema<O> {
120
+ const out: Record<string, unknown> = {};
121
+ for (const [key, d] of Object.entries(schema)) out[key] = Object.freeze({ ...(d as object) });
122
+ return Object.freeze(out) as OptionSchema<O>;
123
+ }
124
+
87
125
  /**
88
126
  * This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
89
127
  * When the caller gives no option, the definition gets the `defaults` object itself.
90
128
  */
91
129
  export function defineStrategy<S extends Step, O extends object>(definition: StrategyDefinition<S, O>): Strategy<S, O> {
92
- const { step, name, describe, defaults, maxNodes } = definition;
130
+ const { step, name, describe, defaults, schema, maxNodes } = definition;
93
131
  if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
94
132
  const strategy: Strategy<S, O> = {
95
133
  step,
96
134
  name,
97
135
  describe,
98
136
  defaults: Object.freeze({ ...defaults }),
137
+ ...(schema === undefined ? {} : { schema: freezeSchema(schema) }),
99
138
  ...(maxNodes === undefined ? {} : { maxNodes }),
100
139
  run(input: StepInput<S>, options?: Partial<O>): StepOutput<S> {
101
140
  const all = options === undefined || Object.keys(options).length === 0 ? strategy.defaults : { ...strategy.defaults, ...options };
@@ -108,25 +147,34 @@ export function defineStrategy<S extends Step, O extends object>(definition: Str
108
147
  /**
109
148
  * A registry: the strategies of each step, in order. A registry is data. No import changes it.
110
149
  * A consumer makes its own registry with `createRegistry`, and adds a strategy with `register`.
111
- * 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.
112
151
  */
113
- 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
+ };
114
156
 
115
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. */
116
158
  export function createRegistry(strategies: readonly AnyStrategy[]): Registry {
117
- const out: { [S in Step]: Strategy<S>[] } = { group: [], router: [], curve: [], room: [] };
159
+ const out: { [S in Step]: Strategy<S>[] } = { group: [], router: [], highway: [], curve: [], room: [] };
118
160
  for (const s of strategies) {
119
161
  const list = out[s.step] as AnyStrategy[] | undefined;
120
162
  if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
121
163
  if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
122
164
  list.push(s);
123
165
  }
124
- 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
+ });
125
173
  }
126
174
 
127
175
  /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
128
176
  export function register(registry: Registry, ...strategies: readonly AnyStrategy[]): Registry {
129
- 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]);
130
178
  }
131
179
 
132
180
  /** This function gives the strategies of one step of a registry, in order. */