@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.
- package/README.md +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- 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
|
-
/**
|
|
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
|
|
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
|
]);
|
package/src/strategies/index.ts
CHANGED
package/src/strategies/model.ts
CHANGED
|
@@ -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
|
|
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>[] } & {
|
|
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({
|
|
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. */
|