@mark1russell7/highways 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -8
- package/build/index.d.ts +1774 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9307 -3090
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +145 -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/sections.ts +121 -0
- package/src/bench/suite.ts +781 -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 +4 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +7 -0
- package/src/levels.ts +178 -9
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- 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 +78 -55
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +93 -4
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +153 -7
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +29 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
package/src/sections.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, type Box } from "./geom.ts";
|
|
2
|
-
import type { Node } from "./model.ts";
|
|
2
|
+
import type { LevelItem, Node } from "./model.ts";
|
|
3
3
|
|
|
4
4
|
/** A section of the plane: a cell of a quadtree, with the nodes whose centers are in it. */
|
|
5
5
|
export interface Section {
|
|
@@ -18,15 +18,21 @@ export interface SectionOptions {
|
|
|
18
18
|
readonly cap?: number;
|
|
19
19
|
/** The tree stops at this depth. The default is 16. */
|
|
20
20
|
readonly maxDepth?: number;
|
|
21
|
+
/** A cell shrinks to one quarter at most this many times without a split. Then it stops. The default is 32. */
|
|
22
|
+
readonly collapse?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** The default values of `SectionOptions`. */
|
|
26
|
+
export const SECTION_DEFAULTS: Readonly<Required<SectionOptions>> = Object.freeze({ cap: 8, maxDepth: 16, collapse: 32 });
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
|
|
25
30
|
* When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
|
|
26
31
|
*/
|
|
27
32
|
export function buildSections(nodes: readonly Node[], options: SectionOptions = {}): Section {
|
|
28
|
-
const cap = Math.max(1, options.cap ??
|
|
29
|
-
const maxDepth = options.maxDepth ??
|
|
33
|
+
const cap = Math.max(1, options.cap ?? SECTION_DEFAULTS.cap);
|
|
34
|
+
const maxDepth = options.maxDepth ?? SECTION_DEFAULTS.maxDepth;
|
|
35
|
+
const collapse = options.collapse ?? SECTION_DEFAULTS.collapse;
|
|
30
36
|
const centers = nodes.map((n) => center(n.box));
|
|
31
37
|
let x0 = Infinity;
|
|
32
38
|
let y0 = Infinity;
|
|
@@ -41,7 +47,7 @@ export function buildSections(nodes: readonly Node[], options: SectionOptions =
|
|
|
41
47
|
const root: Box = nodes.length ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : { x: 0, y: 0, w: 0, h: 0 };
|
|
42
48
|
const make = (id: string, depth: number, cell: Box, list: readonly number[]): Section => {
|
|
43
49
|
let box = cell;
|
|
44
|
-
for (let guard = 0; list.length > cap && depth < maxDepth && guard <
|
|
50
|
+
for (let guard = 0; list.length > cap && depth < maxDepth && guard < collapse; guard++) {
|
|
45
51
|
const mx = box.x + box.w / 2;
|
|
46
52
|
const my = box.y + box.h / 2;
|
|
47
53
|
const parts: number[][] = [[], [], [], []];
|
|
@@ -80,3 +86,86 @@ export function allSections(root: Section): Section[] {
|
|
|
80
86
|
export function treeDepth(root: Section): number {
|
|
81
87
|
return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
|
|
82
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* This function examines a tree of sections from a grouping rule. It throws an error when the tree is not valid for `count` nodes.
|
|
92
|
+
* In a valid tree, each node is in exactly one leaf. The children of a section split its nodes, and a child is one level deeper than its parent.
|
|
93
|
+
*/
|
|
94
|
+
export function checkSections(root: Section, count: number): void {
|
|
95
|
+
const seen = new Array<number>(count).fill(0);
|
|
96
|
+
const ids = new Set<string>();
|
|
97
|
+
const fail = (s: Section, what: string): never => {
|
|
98
|
+
throw new Error(`The tree of sections is not valid at the section "${s.id}": ${what}.`);
|
|
99
|
+
};
|
|
100
|
+
if (root.depth !== 0) fail(root, "the root has a depth that is not 0");
|
|
101
|
+
if (root.nodes.length !== count) fail(root, `the root has ${root.nodes.length} nodes, and the graph has ${count}`);
|
|
102
|
+
const walk = (s: Section): void => {
|
|
103
|
+
if (ids.has(s.id)) fail(s, "two sections have this id");
|
|
104
|
+
ids.add(s.id);
|
|
105
|
+
if (s.children.length === 0) {
|
|
106
|
+
for (const i of s.nodes) {
|
|
107
|
+
if (!Number.isInteger(i) || i < 0 || i >= count) fail(s, `the node index ${i} is not in the graph`);
|
|
108
|
+
seen[i]!++;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let total = 0;
|
|
113
|
+
const own = new Set(s.nodes);
|
|
114
|
+
for (const c of s.children) {
|
|
115
|
+
if (c.depth !== s.depth + 1) fail(c, "a child is not one level deeper than its parent");
|
|
116
|
+
for (const i of c.nodes) if (!own.has(i)) fail(c, `the node index ${i} is not in the parent`);
|
|
117
|
+
total += c.nodes.length;
|
|
118
|
+
walk(c);
|
|
119
|
+
}
|
|
120
|
+
if (total !== s.nodes.length) fail(s, "the children do not split the nodes of the section");
|
|
121
|
+
};
|
|
122
|
+
walk(root);
|
|
123
|
+
const lost = seen.findIndex((n) => n !== 1);
|
|
124
|
+
if (lost >= 0) fail(root, `the node index ${lost} is in ${seen[lost]} leaves, not in one`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The items of one zoom level of a tree of sections, and the item of each node. */
|
|
128
|
+
export interface LevelCut {
|
|
129
|
+
/** The items of the level: a section of the depth of the level, or one node of a leaf above that depth. */
|
|
130
|
+
readonly items: LevelItem[];
|
|
131
|
+
/** The index in `items` of the item of each node, in the order of the nodes. */
|
|
132
|
+
readonly owner: readonly number[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* This function gives the items of the zoom level at `depth`. A section of that depth is one item, with the bounding box of its nodes.
|
|
137
|
+
* A node of a leaf above that depth is an item alone. The items come in the order of the walk of the tree, parents before children.
|
|
138
|
+
* A depth past the depth of the deepest section gives each node alone.
|
|
139
|
+
*/
|
|
140
|
+
export function levelCut(nodes: readonly Pick<Node, "id" | "box">[], root: Section, depth: number): LevelCut {
|
|
141
|
+
const items: LevelItem[] = [];
|
|
142
|
+
const owner: number[] = [];
|
|
143
|
+
const boxOf = (list: readonly number[]): Box => {
|
|
144
|
+
let x0 = Infinity;
|
|
145
|
+
let y0 = Infinity;
|
|
146
|
+
let x1 = -Infinity;
|
|
147
|
+
let y1 = -Infinity;
|
|
148
|
+
for (const i of list) {
|
|
149
|
+
const b = nodes[i]!.box;
|
|
150
|
+
x0 = Math.min(x0, b.x);
|
|
151
|
+
y0 = Math.min(y0, b.y);
|
|
152
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
153
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
154
|
+
}
|
|
155
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
156
|
+
};
|
|
157
|
+
const visit = (s: Section): void => {
|
|
158
|
+
if (s.depth >= depth) {
|
|
159
|
+
for (const i of s.nodes) owner[i] = items.length;
|
|
160
|
+
items.push({ id: s.id, box: boxOf(s.nodes), nodes: s.nodes.map((i) => nodes[i]!.id) });
|
|
161
|
+
} else if (s.children.length > 0) for (const c of s.children) visit(c);
|
|
162
|
+
else
|
|
163
|
+
for (const i of s.nodes) {
|
|
164
|
+
const n = nodes[i]!;
|
|
165
|
+
owner[i] = items.length;
|
|
166
|
+
items.push({ id: n.id, box: n.box, nodes: [n.id] });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (nodes.length > 0) visit(root);
|
|
170
|
+
return { items, owner };
|
|
171
|
+
}
|
package/src/segments.ts
CHANGED
|
@@ -203,13 +203,77 @@ function turn(from: Point, base: Point, to: Point): number {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* A route that does not hold the run goes last.
|
|
206
|
+
* The lane order of the shared ways of a router.
|
|
207
|
+
* With `bundle`, two edges keep one order on all the shared ways of their common stretch.
|
|
208
|
+
* With `run`, each shared way gets its order alone. Two edges can change sides from one shared way to the next.
|
|
210
209
|
*/
|
|
211
|
-
export
|
|
212
|
-
|
|
210
|
+
export type RunOrder = "bundle" | "run";
|
|
211
|
+
|
|
212
|
+
/** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
|
|
213
|
+
interface Placed {
|
|
214
|
+
readonly edge: string;
|
|
215
|
+
readonly p: readonly Point[];
|
|
216
|
+
readonly s: number;
|
|
217
|
+
readonly e: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The place where two routes part past one end of their common stretch, and the side of the first route there. */
|
|
221
|
+
interface Parting {
|
|
222
|
+
/** The last common point. */
|
|
223
|
+
readonly at: Point;
|
|
224
|
+
/** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
|
|
225
|
+
readonly side: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
|
|
229
|
+
function partAfter(P: Placed, Q: Placed): Parting | undefined {
|
|
230
|
+
for (let j = 1; ; j++) {
|
|
231
|
+
const a = P.p[P.e + j];
|
|
232
|
+
const b = Q.p[Q.e + j];
|
|
233
|
+
if (a === undefined || b === undefined) return undefined;
|
|
234
|
+
if (dist(a, b) > SAME) {
|
|
235
|
+
const from = P.p[P.e + j - 2]!;
|
|
236
|
+
const at = P.p[P.e + j - 1]!;
|
|
237
|
+
const side = turn(from, at, a) - turn(from, at, b);
|
|
238
|
+
return side === 0 ? undefined : { at, side };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
|
|
244
|
+
function partBefore(P: Placed, Q: Placed): Parting | undefined {
|
|
245
|
+
for (let j = 1; ; j++) {
|
|
246
|
+
const a = P.p[P.s - j];
|
|
247
|
+
const b = Q.p[Q.s - j];
|
|
248
|
+
if (a === undefined || b === undefined) return undefined;
|
|
249
|
+
if (dist(a, b) > SAME) {
|
|
250
|
+
const from = P.p[P.s - j + 2]!;
|
|
251
|
+
const at = P.p[P.s - j + 1]!;
|
|
252
|
+
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
253
|
+
const side = turn(from, at, b) - turn(from, at, a);
|
|
254
|
+
return side === 0 ? undefined : { at, side };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const byEdge = (P: Placed, Q: Placed): number => (P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0);
|
|
260
|
+
|
|
261
|
+
/** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
|
|
262
|
+
const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This function gives the lane order of the edges on a shared way `run` (metro-line order).
|
|
266
|
+
* The routes are polylines that hold the points of the run in a row, in either direction. A route that does not hold the run goes last.
|
|
267
|
+
*
|
|
268
|
+
* Two routes go on together past each end of the run until they part. The route that turns more to the left there gets the lane more to the left.
|
|
269
|
+
*
|
|
270
|
+
* With the order `run`, the default, the end after the last point of the run tells the side first.
|
|
271
|
+
* With the order `bundle`, two routes that part on one side at both ends keep that side.
|
|
272
|
+
* When the two ends disagree, the two routes must cross. The end with the smaller x, then the smaller y, tells the side.
|
|
273
|
+
* Thus each shared way of a common stretch gives two routes the same order, and the crossing is where they part.
|
|
274
|
+
*/
|
|
275
|
+
export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
|
|
276
|
+
const placed: Placed[] = [];
|
|
213
277
|
const lost: string[] = [];
|
|
214
278
|
for (const r of routes) {
|
|
215
279
|
let at = indexOf(r.points, run, 0);
|
|
@@ -221,30 +285,16 @@ export function orderLanes(run: readonly Point[], routes: readonly Route[]): str
|
|
|
221
285
|
else lost.push(r.edge);
|
|
222
286
|
}
|
|
223
287
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
for (let j = 1; ; j++) {
|
|
237
|
-
const a = P.p[P.s - j];
|
|
238
|
-
const b = Q.p[Q.s - j];
|
|
239
|
-
if (a === undefined || b === undefined) break;
|
|
240
|
-
if (!same(a, b)) {
|
|
241
|
-
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
242
|
-
const d = turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, b) - turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, a);
|
|
243
|
-
if (d !== 0) return d;
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
|
|
248
|
-
});
|
|
288
|
+
if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q));
|
|
289
|
+
else {
|
|
290
|
+
// The sort starts from the order of the edges, so that each run sorts the same edges the same way.
|
|
291
|
+
placed.sort(byEdge);
|
|
292
|
+
placed.sort((P, Q) => {
|
|
293
|
+
const after = partAfter(P, Q);
|
|
294
|
+
const before = partBefore(P, Q);
|
|
295
|
+
if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
|
|
296
|
+
return after?.side ?? before?.side ?? byEdge(P, Q);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
249
299
|
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
250
300
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { arcCurve, type ArcOptions } from "../curves/arc/index.ts";
|
|
2
|
+
import { clothoidCurve, type ClothoidOptions } from "../curves/clothoid/index.ts";
|
|
3
|
+
import { highwayCurve, type JogStyle } from "../curves/highway/index.ts";
|
|
2
4
|
import { roundedCurve, type RoundedOptions } from "../curves/rounded/index.ts";
|
|
5
|
+
import { smoothCurve, type SmoothOptions } from "../curves/smooth/index.ts";
|
|
3
6
|
import { splineCurve, type SplineOptions } from "../curves/spline/index.ts";
|
|
4
7
|
import { straightCurve } from "../curves/straight/index.ts";
|
|
8
|
+
import { CUT_DEFAULTS, cutGroups, type CutOptions } from "../groups/cut/index.ts";
|
|
9
|
+
import { KD_DEFAULTS, kdGroups, type KdOptions } from "../groups/kd/index.ts";
|
|
10
|
+
import { byDataGroup, keyGroups, type KeyOptions } from "../groups/keys/index.ts";
|
|
11
|
+
import { quadtreeGroups } from "../groups/quadtree/index.ts";
|
|
5
12
|
import type { Graph, Result, Router } from "../model.ts";
|
|
6
13
|
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
14
|
+
import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
7
15
|
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
8
16
|
import { plugsRouter } from "../routers/plugs.ts";
|
|
9
17
|
import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
|
|
10
18
|
import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
|
|
11
19
|
import { straightRouter } from "../routers/straight.ts";
|
|
12
20
|
import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
|
|
21
|
+
import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
|
|
13
22
|
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type Registry, type Strategy } from "./model.ts";
|
|
23
|
+
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";
|
|
14
25
|
|
|
15
26
|
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
16
27
|
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
@@ -22,13 +33,14 @@ function routerStrategy<O extends object>(
|
|
|
22
33
|
maxNodes: number,
|
|
23
34
|
defaults: O,
|
|
24
35
|
make: (options: O & { readonly name: string }) => Router,
|
|
36
|
+
schema: OptionSchema<O>,
|
|
25
37
|
): Strategy<"router", O> {
|
|
26
|
-
return defineStrategy({ step: "router", name, describe, defaults, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
|
|
38
|
+
return defineStrategy({ step: "router", name, describe, defaults, schema, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
/** This function makes a router strategy for a router without options. */
|
|
30
42
|
function fixedRouter(router: Router, describe: string): Strategy<"router", object> {
|
|
31
|
-
return defineStrategy({ step: "router", name: router.name, describe, defaults: {}, run: (graph: Graph): Result => router.route(graph) });
|
|
43
|
+
return defineStrategy({ step: "router", name: router.name, describe, defaults: {}, schema: {}, run: (graph: Graph): Result => router.route(graph) });
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
|
|
@@ -54,6 +66,7 @@ export const routerStrategies: {
|
|
|
54
66
|
10_000,
|
|
55
67
|
{},
|
|
56
68
|
cornersRouter,
|
|
69
|
+
CORNERS_SCHEMA,
|
|
57
70
|
),
|
|
58
71
|
"corners-smooth": routerStrategy<RouterPreset<CornersOptions>>(
|
|
59
72
|
"corners-smooth",
|
|
@@ -61,6 +74,7 @@ export const routerStrategies: {
|
|
|
61
74
|
10_000,
|
|
62
75
|
{ smooth: true },
|
|
63
76
|
cornersRouter,
|
|
77
|
+
CORNERS_SCHEMA,
|
|
64
78
|
),
|
|
65
79
|
trunks: routerStrategy<RouterPreset<TrunksOptions>>(
|
|
66
80
|
"trunks",
|
|
@@ -68,6 +82,7 @@ export const routerStrategies: {
|
|
|
68
82
|
10_000,
|
|
69
83
|
{ rule: byCollection },
|
|
70
84
|
trunksRouter,
|
|
85
|
+
TRUNKS_SCHEMA,
|
|
71
86
|
),
|
|
72
87
|
shared: routerStrategy<RouterPreset<SharedOptions>>(
|
|
73
88
|
"shared",
|
|
@@ -75,14 +90,16 @@ export const routerStrategies: {
|
|
|
75
90
|
10_000,
|
|
76
91
|
{ rule: byCollection },
|
|
77
92
|
sharedRouter,
|
|
93
|
+
SHARED_SCHEMA,
|
|
78
94
|
),
|
|
79
|
-
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter),
|
|
95
|
+
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter, SHARED_SCHEMA),
|
|
80
96
|
sections: routerStrategy<RouterPreset<SectionsOptions>>(
|
|
81
97
|
"sections",
|
|
82
98
|
"A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
|
|
83
99
|
100_000,
|
|
84
100
|
{},
|
|
85
101
|
sectionsRouter,
|
|
102
|
+
SECTIONS_SCHEMA,
|
|
86
103
|
),
|
|
87
104
|
"sections-tree": routerStrategy<RouterPreset<SectionsOptions>>(
|
|
88
105
|
"sections-tree",
|
|
@@ -90,6 +107,7 @@ export const routerStrategies: {
|
|
|
90
107
|
100_000,
|
|
91
108
|
{ stretch: Infinity },
|
|
92
109
|
sectionsRouter,
|
|
110
|
+
SECTIONS_SCHEMA,
|
|
93
111
|
),
|
|
94
112
|
});
|
|
95
113
|
|
|
@@ -112,19 +130,56 @@ export interface TensionOptions {
|
|
|
112
130
|
readonly tension: number;
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
/** The
|
|
133
|
+
/** The options of the curve strategy `highway`. */
|
|
134
|
+
export interface HighwayStrategyOptions {
|
|
135
|
+
/** The radius of the center line of a bundle at a bend, and of each lone corner. The default is 12. */
|
|
136
|
+
readonly radius: number;
|
|
137
|
+
/** The smallest radius of a lane at a bend of a bundle. The default is 4. */
|
|
138
|
+
readonly minRadius: number;
|
|
139
|
+
/** The shape of the S curve where a lane changes its offset. The default is `biarc`. */
|
|
140
|
+
readonly jog: JogStyle;
|
|
141
|
+
/** The number of cubic pieces of a `quintic` S curve. The default is 4. */
|
|
142
|
+
readonly jogPieces: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The options of the curve strategy `smooth`. */
|
|
146
|
+
export interface SmoothStrategyOptions {
|
|
147
|
+
/** The radius of the arc of each corner. The default is 12. */
|
|
148
|
+
readonly radius: number;
|
|
149
|
+
/** The share of the turn of a corner in its two spirals, from 0 to 1. The default is 0.6. */
|
|
150
|
+
readonly smoothing: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The options of the curve strategy `clothoid`. */
|
|
154
|
+
export interface ClothoidStrategyOptions {
|
|
155
|
+
/** The radius of the arc of each corner. The default is 12. */
|
|
156
|
+
readonly radius: number;
|
|
157
|
+
/** The share of the turn of a corner in its two clothoids, from 0 to 1. The default is 0.5. */
|
|
158
|
+
readonly spiral: number;
|
|
159
|
+
/** The largest distance from the cubic pieces to the true clothoid. The default is 0.25. */
|
|
160
|
+
readonly tolerance: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 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.
|
|
166
|
+
*/
|
|
116
167
|
export const curveStrategies: {
|
|
117
168
|
readonly straight: Strategy<"curve", object>;
|
|
118
169
|
readonly rounded: Strategy<"curve", RadiusOptions>;
|
|
119
170
|
readonly spline: Strategy<"curve", TensionOptions>;
|
|
120
171
|
readonly arc: Strategy<"curve", RadiusOptions>;
|
|
172
|
+
readonly smooth: Strategy<"curve", SmoothStrategyOptions>;
|
|
173
|
+
readonly clothoid: Strategy<"curve", ClothoidStrategyOptions>;
|
|
174
|
+
readonly highway: Strategy<"curve", HighwayStrategyOptions>;
|
|
121
175
|
} = Object.freeze({
|
|
122
|
-
straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, run: (input: CurveInput) => straightCurve(input.points) }),
|
|
176
|
+
straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, schema: {}, run: (input: CurveInput) => straightCurve(input.points) }),
|
|
123
177
|
rounded: defineStrategy({
|
|
124
178
|
step: "curve",
|
|
125
179
|
name: "rounded",
|
|
126
180
|
describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
|
|
127
181
|
defaults: { radius: 12 },
|
|
182
|
+
schema: RADIUS_SCHEMA,
|
|
128
183
|
run: (input: CurveInput, o: Readonly<RadiusOptions>) => roundedCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies RoundedOptions),
|
|
129
184
|
}),
|
|
130
185
|
spline: defineStrategy({
|
|
@@ -132,6 +187,7 @@ export const curveStrategies: {
|
|
|
132
187
|
name: "spline",
|
|
133
188
|
describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
|
|
134
189
|
defaults: { tension: 0 },
|
|
190
|
+
schema: TENSION_SCHEMA,
|
|
135
191
|
run: (input: CurveInput, o: Readonly<TensionOptions>) => splineCurve(input.points, { ...contextOf(input), tension: o.tension } satisfies SplineOptions),
|
|
136
192
|
}),
|
|
137
193
|
arc: defineStrategy({
|
|
@@ -139,27 +195,117 @@ export const curveStrategies: {
|
|
|
139
195
|
name: "arc",
|
|
140
196
|
describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
|
|
141
197
|
defaults: { radius: 12 },
|
|
198
|
+
schema: RADIUS_SCHEMA,
|
|
142
199
|
run: (input: CurveInput, o: Readonly<RadiusOptions>) => arcCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies ArcOptions),
|
|
143
200
|
}),
|
|
201
|
+
smooth: defineStrategy({
|
|
202
|
+
step: "curve",
|
|
203
|
+
name: "smooth",
|
|
204
|
+
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.",
|
|
205
|
+
defaults: { radius: 12, smoothing: 0.6 },
|
|
206
|
+
schema: SMOOTH_SCHEMA,
|
|
207
|
+
run: (input: CurveInput, o: Readonly<SmoothStrategyOptions>) =>
|
|
208
|
+
smoothCurve(input.points, { ...(input.space ? { space: input.space } : {}), radius: o.radius, smoothing: o.smoothing } satisfies SmoothOptions),
|
|
209
|
+
}),
|
|
210
|
+
clothoid: defineStrategy({
|
|
211
|
+
step: "curve",
|
|
212
|
+
name: "clothoid",
|
|
213
|
+
describe: "A clothoid, an arc and a second clothoid at each corner, as a transition curve of a road. Cubic pieces draw each clothoid.",
|
|
214
|
+
defaults: { radius: 12, spiral: 0.5, tolerance: 0.25 },
|
|
215
|
+
schema: CLOTHOID_SCHEMA,
|
|
216
|
+
run: (input: CurveInput, o: Readonly<ClothoidStrategyOptions>) =>
|
|
217
|
+
clothoidCurve(input.points, {
|
|
218
|
+
...(input.space ? { space: input.space } : {}),
|
|
219
|
+
radius: o.radius,
|
|
220
|
+
spiral: o.spiral,
|
|
221
|
+
tolerance: o.tolerance,
|
|
222
|
+
} satisfies ClothoidOptions),
|
|
223
|
+
}),
|
|
224
|
+
highway: defineStrategy({
|
|
225
|
+
step: "curve",
|
|
226
|
+
name: "highway",
|
|
227
|
+
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
|
+
defaults: { radius: 12, minRadius: 4, jog: "biarc" as JogStyle, jogPieces: 4 },
|
|
229
|
+
schema: HIGHWAY_SCHEMA,
|
|
230
|
+
run: (input: CurveInput, o: Readonly<HighwayStrategyOptions>) => highwayCurve(input.points, { radius: o.radius, ...(input.space ? { space: input.space } : {}) }),
|
|
231
|
+
}),
|
|
144
232
|
});
|
|
145
233
|
|
|
146
234
|
/** The options of a room strategy: the options of `roomDemand`, without the method. */
|
|
147
235
|
export type RoomPreset = Omit<RoomOptions, "method">;
|
|
148
236
|
|
|
149
237
|
const roomStrategy = (method: RoomMethod, describe: string): Strategy<"room", RoomPreset> =>
|
|
150
|
-
defineStrategy<"room", RoomPreset>({ step: "room", name: method, describe, defaults: {}, run: (input, o) => roomDemand(input.items, input.links, { ...o, method }) });
|
|
238
|
+
defineStrategy<"room", RoomPreset>({ step: "room", name: method, describe, defaults: {}, schema: ROOM_SCHEMA, run: (input, o) => roomDemand(input.items, input.links, { ...o, method }) });
|
|
151
239
|
|
|
152
|
-
/** The
|
|
240
|
+
/** The options of the room strategy `levels`: the options of `levelRoom` without the method, and the limits of the quadtree. */
|
|
241
|
+
export type LevelRoomPreset = Omit<LevelRoomOptions, "method"> & SectionOptions;
|
|
242
|
+
|
|
243
|
+
/** The built-in room strategies: the methods of `roomDemand`, and the room of each zoom level (`levelRoom`). */
|
|
153
244
|
export const roomStrategies: {
|
|
154
245
|
readonly routes: Strategy<"room", RoomPreset>;
|
|
155
246
|
readonly lines: Strategy<"room", RoomPreset>;
|
|
247
|
+
readonly levels: Strategy<"room", LevelRoomPreset>;
|
|
156
248
|
} = Object.freeze({
|
|
157
249
|
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."),
|
|
158
250
|
lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap."),
|
|
251
|
+
levels: defineStrategy<"room", LevelRoomPreset>({
|
|
252
|
+
step: "room",
|
|
253
|
+
name: "levels",
|
|
254
|
+
describe: "Each zoom level gets the gaps that its links need. The far levels open first, and a box moves with its parent.",
|
|
255
|
+
defaults: {},
|
|
256
|
+
schema: LEVEL_ROOM_SCHEMA,
|
|
257
|
+
run: (input, o) => {
|
|
258
|
+
const { cap, maxDepth, collapse, ...room } = o;
|
|
259
|
+
const root = input.sections ?? buildSections(input.items, { cap: cap ?? SECTION_DEFAULTS.cap, maxDepth: maxDepth ?? SECTION_DEFAULTS.maxDepth, collapse: collapse ?? SECTION_DEFAULTS.collapse });
|
|
260
|
+
return levelRoom(input.items, input.links, root, room).gaps;
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** The built-in grouping rules: the strategies of the step `group`. The router `sections` makes its hubs and its zoom levels from their tree. */
|
|
266
|
+
export const groupStrategies: {
|
|
267
|
+
readonly quadtree: Strategy<"group", Required<SectionOptions>>;
|
|
268
|
+
readonly keys: Strategy<"group", KeyOptions>;
|
|
269
|
+
readonly kd: Strategy<"group", Required<KdOptions>>;
|
|
270
|
+
readonly cut: Strategy<"group", Required<CutOptions>>;
|
|
271
|
+
} = Object.freeze({
|
|
272
|
+
quadtree: defineStrategy({
|
|
273
|
+
step: "group",
|
|
274
|
+
name: "quadtree",
|
|
275
|
+
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`.",
|
|
276
|
+
defaults: { ...SECTION_DEFAULTS },
|
|
277
|
+
schema: SECTION_SCHEMA,
|
|
278
|
+
run: (graph: Graph, o: Readonly<Required<SectionOptions>>) => quadtreeGroups(graph, o),
|
|
279
|
+
}),
|
|
280
|
+
keys: defineStrategy({
|
|
281
|
+
step: "group",
|
|
282
|
+
name: "keys",
|
|
283
|
+
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.",
|
|
284
|
+
defaults: { key: byDataGroup } as KeyOptions,
|
|
285
|
+
schema: KEY_SCHEMA,
|
|
286
|
+
run: (graph: Graph, o: Readonly<KeyOptions>) => keyGroups(graph, o),
|
|
287
|
+
}),
|
|
288
|
+
kd: defineStrategy({
|
|
289
|
+
step: "group",
|
|
290
|
+
name: "kd",
|
|
291
|
+
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.",
|
|
292
|
+
defaults: { ...KD_DEFAULTS },
|
|
293
|
+
schema: KD_SCHEMA,
|
|
294
|
+
run: (graph: Graph, o: Readonly<Required<KdOptions>>) => kdGroups(graph, o),
|
|
295
|
+
}),
|
|
296
|
+
cut: defineStrategy({
|
|
297
|
+
step: "group",
|
|
298
|
+
name: "cut",
|
|
299
|
+
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.",
|
|
300
|
+
defaults: { ...CUT_DEFAULTS },
|
|
301
|
+
schema: CUT_SCHEMA,
|
|
302
|
+
run: (graph: Graph, o: Readonly<Required<CutOptions>>) => cutGroups(graph, o),
|
|
303
|
+
}),
|
|
159
304
|
});
|
|
160
305
|
|
|
161
306
|
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
162
307
|
export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
|
|
308
|
+
...Object.values(groupStrategies),
|
|
163
309
|
...Object.values(routerStrategies),
|
|
164
310
|
...Object.values(curveStrategies),
|
|
165
311
|
...Object.values(roomStrategies),
|
package/src/strategies/index.ts
CHANGED
package/src/strategies/model.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { CurveSpace } from "../curves/space.ts";
|
|
|
3
3
|
import type { Point } from "../geom.ts";
|
|
4
4
|
import type { Graph, Result, Router } from "../model.ts";
|
|
5
5
|
import type { Boxed, Gap, RoomLink } from "../room/index.ts";
|
|
6
|
+
import type { Section } from "../sections.ts";
|
|
7
|
+
import type { OptionSchema } from "./options.ts";
|
|
6
8
|
|
|
7
9
|
/** The input of a curve strategy: one polyline and the context of its route. */
|
|
8
10
|
export interface CurveInput {
|
|
@@ -20,16 +22,23 @@ export interface CurveInput {
|
|
|
20
22
|
export interface RoomInput {
|
|
21
23
|
readonly items: readonly Boxed[];
|
|
22
24
|
readonly links: readonly RoomLink[];
|
|
25
|
+
/**
|
|
26
|
+
* 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`.
|
|
27
|
+
* Without it, such a strategy makes the quadtree of `buildSections`. The other strategies do not read it.
|
|
28
|
+
*/
|
|
29
|
+
readonly sections?: Section;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
33
|
* The steps that have strategies, with the fixed input and output type of each step.
|
|
27
34
|
*
|
|
35
|
+
* - `group`: a graph in, the tree of sections out. The router `sections` makes its hubs and its zoom levels from this tree.
|
|
28
36
|
* - `router`: a graph in, the routes (and the `levels`, `bundles` and `runs`) out.
|
|
29
37
|
* - `curve`: a polyline in, the segments of a curve out.
|
|
30
38
|
* - `room`: the boxes and links of a board in, the gaps of the board with their need out.
|
|
31
39
|
*/
|
|
32
40
|
export interface StepTypes {
|
|
41
|
+
readonly group: { readonly input: Graph; readonly output: Section };
|
|
33
42
|
readonly router: { readonly input: Graph; readonly output: Result };
|
|
34
43
|
readonly curve: { readonly input: CurveInput; readonly output: CurveSegment[] };
|
|
35
44
|
readonly room: { readonly input: RoomInput; readonly output: Gap[] };
|
|
@@ -45,7 +54,7 @@ export type StepInput<S extends Step> = StepTypes[S]["input"];
|
|
|
45
54
|
export type StepOutput<S extends Step> = StepTypes[S]["output"];
|
|
46
55
|
|
|
47
56
|
/** The steps that have strategies, in the order of the pipeline. */
|
|
48
|
-
export const STEPS: readonly Step[] = ["router", "curve", "room"];
|
|
57
|
+
export const STEPS: readonly Step[] = ["group", "router", "curve", "room"];
|
|
49
58
|
|
|
50
59
|
/**
|
|
51
60
|
* A strategy: one way to do one step, with a name and a description.
|
|
@@ -60,6 +69,8 @@ export interface Strategy<S extends Step = Step, O extends object = object> {
|
|
|
60
69
|
readonly describe: string;
|
|
61
70
|
/** The values of the options when the caller gives no value. */
|
|
62
71
|
readonly defaults: Readonly<O>;
|
|
72
|
+
/** The declarations of the options: the type, the range, the step and the meaning of each one. A strategy without it declares nothing. */
|
|
73
|
+
readonly schema?: OptionSchema<O>;
|
|
63
74
|
/** The benchmark skips an input with more nodes than this. Without it, the strategy takes each input. */
|
|
64
75
|
readonly maxNodes?: number;
|
|
65
76
|
/** This method does the step. Each option that `options` does not give gets its value from `defaults`. */
|
|
@@ -76,23 +87,33 @@ export interface StrategyDefinition<S extends Step, O extends object> {
|
|
|
76
87
|
readonly describe: string;
|
|
77
88
|
/** These values are the options when the caller gives no value. Give `{}` for a strategy without options. */
|
|
78
89
|
readonly defaults: O;
|
|
90
|
+
/** These declarations tell the type, the range, the step and the meaning of each option. `checkSchema` examines them. */
|
|
91
|
+
readonly schema?: NoInfer<OptionSchema<O>>;
|
|
79
92
|
readonly maxNodes?: number;
|
|
80
93
|
/** This function does the step. It gets each option, from the caller or from `defaults`. */
|
|
81
94
|
run(input: StepInput<S>, options: Readonly<O>): StepOutput<S>;
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
/** This function gives a frozen copy of a schema, with each declaration frozen. */
|
|
98
|
+
function freezeSchema<O>(schema: OptionSchema<O>): OptionSchema<O> {
|
|
99
|
+
const out: Record<string, unknown> = {};
|
|
100
|
+
for (const [key, d] of Object.entries(schema)) out[key] = Object.freeze({ ...(d as object) });
|
|
101
|
+
return Object.freeze(out) as OptionSchema<O>;
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
/**
|
|
85
105
|
* This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
|
|
86
106
|
* When the caller gives no option, the definition gets the `defaults` object itself.
|
|
87
107
|
*/
|
|
88
108
|
export function defineStrategy<S extends Step, O extends object>(definition: StrategyDefinition<S, O>): Strategy<S, O> {
|
|
89
|
-
const { step, name, describe, defaults, maxNodes } = definition;
|
|
109
|
+
const { step, name, describe, defaults, schema, maxNodes } = definition;
|
|
90
110
|
if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
|
|
91
111
|
const strategy: Strategy<S, O> = {
|
|
92
112
|
step,
|
|
93
113
|
name,
|
|
94
114
|
describe,
|
|
95
115
|
defaults: Object.freeze({ ...defaults }),
|
|
116
|
+
...(schema === undefined ? {} : { schema: freezeSchema(schema) }),
|
|
96
117
|
...(maxNodes === undefined ? {} : { maxNodes }),
|
|
97
118
|
run(input: StepInput<S>, options?: Partial<O>): StepOutput<S> {
|
|
98
119
|
const all = options === undefined || Object.keys(options).length === 0 ? strategy.defaults : { ...strategy.defaults, ...options };
|
|
@@ -105,29 +126,30 @@ export function defineStrategy<S extends Step, O extends object>(definition: Str
|
|
|
105
126
|
/**
|
|
106
127
|
* A registry: the strategies of each step, in order. A registry is data. No import changes it.
|
|
107
128
|
* 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.
|
|
108
130
|
*/
|
|
109
|
-
export type Registry = { readonly [S in Step]: readonly Strategy<S>[] };
|
|
131
|
+
export type Registry = { readonly [S in Exclude<Step, "group">]: readonly Strategy<S>[] } & { readonly group?: readonly Strategy<"group">[] };
|
|
110
132
|
|
|
111
133
|
/** 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. */
|
|
112
134
|
export function createRegistry(strategies: readonly AnyStrategy[]): Registry {
|
|
113
|
-
const out: { [S in Step]: Strategy<S>[] } = { router: [], curve: [], room: [] };
|
|
135
|
+
const out: { [S in Step]: Strategy<S>[] } = { group: [], router: [], curve: [], room: [] };
|
|
114
136
|
for (const s of strategies) {
|
|
115
137
|
const list = out[s.step] as AnyStrategy[] | undefined;
|
|
116
138
|
if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
|
|
117
139
|
if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
|
|
118
140
|
list.push(s);
|
|
119
141
|
}
|
|
120
|
-
return Object.freeze({ router: Object.freeze(out.router), curve: Object.freeze(out.curve), room: Object.freeze(out.room) });
|
|
142
|
+
return Object.freeze({ group: Object.freeze(out.group), router: Object.freeze(out.router), curve: Object.freeze(out.curve), room: Object.freeze(out.room) });
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
/** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
|
|
124
146
|
export function register(registry: Registry, ...strategies: readonly AnyStrategy[]): Registry {
|
|
125
|
-
return createRegistry([...registry.router, ...registry.curve, ...registry.room, ...strategies]);
|
|
147
|
+
return createRegistry([...(registry.group ?? []), ...registry.router, ...registry.curve, ...registry.room, ...strategies]);
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
/** This function gives the strategies of one step of a registry, in order. */
|
|
129
151
|
export function listStrategies<S extends Step>(registry: Registry, step: S): readonly Strategy<S>[] {
|
|
130
|
-
return registry[step];
|
|
152
|
+
return (registry[step] ?? []) as readonly Strategy<S>[];
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
/** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
|