@mark1russell7/highways 0.8.0 → 0.9.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 +74 -8
- package/build/index.d.ts +705 -183
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3458 -1979
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bench/compare.ts +174 -5
- package/src/bench/exact/index.ts +4 -0
- package/src/bench/properties.ts +8 -2
- package/src/bench/room.ts +1 -1
- package/src/bench/run.ts +38 -3
- package/src/bench/screen/generators.ts +11 -41
- package/src/bench/screen/screen.ts +16 -10
- package/src/bench/screen/set.ts +156 -49
- package/src/bench/suite.ts +110 -12
- package/src/cases.ts +64 -18
- package/src/families/cases.ts +108 -0
- package/src/families/fixtures.ts +101 -0
- package/src/families/index.ts +36 -0
- package/src/families/model.ts +55 -0
- package/src/fixtures.ts +8 -2
- package/src/highways/soft-keys/index.ts +8 -4
- package/src/highways/stretches.ts +28 -7
- package/src/index.ts +8 -0
- package/src/lane-order.ts +42 -2
- package/src/lane-orders/stretch/index.ts +486 -0
- package/src/lane-orders/stretch/router.ts +22 -0
- package/src/levels/depths/index.ts +57 -0
- package/src/plugs/facing/index.ts +22 -0
- package/src/portfolio/router.ts +44 -0
- package/src/routers/corners.ts +27 -3
- package/src/routers/sections.ts +22 -40
- package/src/routers/shared.ts +51 -19
- package/src/routers/trunks.ts +21 -3
- package/src/segments.ts +8 -26
- package/src/strategies/builtins.ts +54 -2
- package/src/strategies/model.ts +55 -10
- package/src/strategies/options.ts +36 -4
- package/src/strategies/schemas.ts +68 -5
- package/src/whole/pipeline/index.ts +66 -0
package/src/routers/corners.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { nodeIndex, type Edge, type Graph, type Node, type Result, type Route, t
|
|
|
3
3
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
4
|
import { justifyPlugs } from "../plugs/justify.ts";
|
|
5
5
|
import { sidePlugs } from "../plugs/side.ts";
|
|
6
|
+
import type { PlugPair, Strategy } from "../strategies/model.ts";
|
|
6
7
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
7
8
|
|
|
8
9
|
/** The ways to place a free plug of the corner router. */
|
|
@@ -24,6 +25,13 @@ export interface CornersOptions extends WayOptions {
|
|
|
24
25
|
* Above 0, the free plugs of each side keep their order and move the least distance (`justifyPlugs`, WILD-002).
|
|
25
26
|
*/
|
|
26
27
|
readonly plugGap?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The plug rule: a strategy of the step `plugs` that places the plugs of each edge. It takes the place of the option `plugs`.
|
|
30
|
+
* Without it, the router places the plugs with the option `plugs`. An edge without plugs from the strategy gets the plugs of `facing`.
|
|
31
|
+
*/
|
|
32
|
+
readonly plug?: Strategy<"plugs">;
|
|
33
|
+
/** The options of the strategy `plug`. The strategy gets its `defaults` for each option that this object does not give. */
|
|
34
|
+
readonly plugOptions?: object;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -38,7 +46,7 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
38
46
|
const byId = nodeIndex(graph);
|
|
39
47
|
const ways = new WayFinder(graph.nodes, options);
|
|
40
48
|
const routes: Route[] = [];
|
|
41
|
-
if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
|
|
49
|
+
if (!options.plug && (options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
|
|
42
50
|
for (const e of graph.edges) {
|
|
43
51
|
const s = byId.get(e.source.node);
|
|
44
52
|
const t = byId.get(e.target.node);
|
|
@@ -51,12 +59,17 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
51
59
|
}
|
|
52
60
|
// First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
|
|
53
61
|
const ends: [Edge, Node, Node, Plug, Plug][] = [];
|
|
62
|
+
const placed = options.plug ? plugsByEdge(options.plug.run({ graph, ways }, options.plugOptions)) : undefined;
|
|
54
63
|
for (const e of graph.edges) {
|
|
55
64
|
const s = byId.get(e.source.node);
|
|
56
65
|
const t = byId.get(e.target.node);
|
|
57
66
|
if (!s || !t) continue;
|
|
58
|
-
const
|
|
59
|
-
|
|
67
|
+
const own = placed?.get(e.id)?.shift();
|
|
68
|
+
const [ps, pt]: [Plug, Plug] = own
|
|
69
|
+
? [own.source, own.target]
|
|
70
|
+
: options.plugs === "side" && !placed
|
|
71
|
+
? sidePlugs(ways, s, e.source, t, e.target)
|
|
72
|
+
: [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
|
|
60
73
|
ends.push([e, s, t, ps, pt]);
|
|
61
74
|
}
|
|
62
75
|
if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
|
|
@@ -67,6 +80,17 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
67
80
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
/** This function gives the plugs of a plug strategy by the edge id. Two edges with one id take the entries of that id in order. */
|
|
84
|
+
function plugsByEdge(list: readonly PlugPair[]): Map<string, PlugPair[]> {
|
|
85
|
+
const out = new Map<string, PlugPair[]>();
|
|
86
|
+
for (const p of list) {
|
|
87
|
+
const own = out.get(p.edge);
|
|
88
|
+
if (own) own.push(p);
|
|
89
|
+
else out.set(p.edge, [p]);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
72
96
|
* The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
2
|
import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
|
|
3
|
-
import { LEVEL_LINK_DEFAULTS,
|
|
3
|
+
import { LEVEL_LINK_DEFAULTS, type LevelLinkOptions } from "../levels.ts";
|
|
4
|
+
import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
|
|
4
5
|
import { nodeIndex, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
5
6
|
import { placePlug } from "../plugs.ts";
|
|
6
|
-
import { allSections, buildSections, checkSections,
|
|
7
|
+
import { allSections, buildSections, checkSections, SECTION_DEFAULTS, type Section, type SectionOptions } from "../sections.ts";
|
|
7
8
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
8
9
|
import type { Strategy } from "../strategies/model.ts";
|
|
9
10
|
import { WayFinder } from "../ways.ts";
|
|
@@ -45,6 +46,14 @@ export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
|
45
46
|
* The default is each depth, from 0 to the depth of the deepest section.
|
|
46
47
|
*/
|
|
47
48
|
readonly levels?: readonly number[];
|
|
49
|
+
/**
|
|
50
|
+
* The level rule: a strategy of the step `levels` that makes the zoom levels from the tree and the routes.
|
|
51
|
+
* Without it, the router uses the strategy `depths` (`depthLevels`). The strategy gets the options `levels`, `margin`, `k`, `tightMargin`, `plugInset` and `throughCost` of the router,
|
|
52
|
+
* and then `levelOptions`.
|
|
53
|
+
*/
|
|
54
|
+
readonly level?: Strategy<"levels">;
|
|
55
|
+
/** The options of the strategy `level`. They go over the level options of the router. */
|
|
56
|
+
readonly levelOptions?: object;
|
|
48
57
|
/** The lane order of the runs. With `run`, the default, each run gets its order alone. With `bundle`, two edges keep one order on each run of their common stretch. */
|
|
49
58
|
readonly laneOrder?: RunOrder;
|
|
50
59
|
/** The benchmark skips a fixture with more nodes. */
|
|
@@ -210,7 +219,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
210
219
|
if (list.length < 2 || p.length < 2) continue;
|
|
211
220
|
runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!), options.laneOrder) });
|
|
212
221
|
}
|
|
213
|
-
return { routes, runs, levels: makeLevels(graph, root,
|
|
222
|
+
return { routes, runs, levels: makeLevels(graph, root, routes, options) };
|
|
214
223
|
},
|
|
215
224
|
};
|
|
216
225
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
@@ -224,43 +233,16 @@ function groupNodes(graph: Graph, options: SectionsOptions): Section {
|
|
|
224
233
|
return root;
|
|
225
234
|
}
|
|
226
235
|
|
|
236
|
+
/** The keys of the router options that the level rule gets. */
|
|
237
|
+
const LEVEL_KEYS = ["levels", "margin", "k", "tightMargin", "plugInset", "throughCost"] as const;
|
|
238
|
+
|
|
227
239
|
/**
|
|
228
|
-
* This function gives
|
|
229
|
-
*
|
|
230
|
-
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
231
|
-
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
232
|
-
* In the last level, the points of a link are the route of its first edge.
|
|
240
|
+
* This function gives the zoom levels of the router: the levels of the strategy `level`, or else the levels of `depthLevels`.
|
|
241
|
+
* The strategy gets the level options of the router that have a value, and then `levelOptions`.
|
|
233
242
|
*/
|
|
234
|
-
function makeLevels(graph: Graph, root: Section,
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
for (let d = 0; d <= deepest + 1; d++) {
|
|
240
|
-
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
241
|
-
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
242
|
-
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
243
|
-
for (const e of graph.edges) {
|
|
244
|
-
const iu = indexOfNode.get(e.source.node);
|
|
245
|
-
const iv = indexOfNode.get(e.target.node);
|
|
246
|
-
if (iu === undefined || iv === undefined) continue;
|
|
247
|
-
const a = owner[iu]!;
|
|
248
|
-
const b = owner[iv]!;
|
|
249
|
-
if (a === b) continue;
|
|
250
|
-
const ida = items[a]!.id;
|
|
251
|
-
const idb = items[b]!.id;
|
|
252
|
-
const key = `${ida}\u0000${idb}`;
|
|
253
|
-
const link = links.get(key);
|
|
254
|
-
if (link) {
|
|
255
|
-
link.count++;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
|
|
259
|
-
}
|
|
260
|
-
const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
|
|
261
|
-
if (d > deepest)
|
|
262
|
-
levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
|
|
263
|
-
else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
|
|
264
|
-
}
|
|
265
|
-
return levels;
|
|
243
|
+
function makeLevels(graph: Graph, root: Section, routes: readonly Route[], options: SectionsOptions): Level[] {
|
|
244
|
+
if (!options.level) return depthLevels({ graph, root, routes }, options);
|
|
245
|
+
const own: Record<string, unknown> = {};
|
|
246
|
+
for (const key of LEVEL_KEYS) if (options[key] !== undefined) own[key] = options[key];
|
|
247
|
+
return options.level.run({ graph, root, routes }, { ...(own as DepthLevelOptions), ...options.levelOptions });
|
|
266
248
|
}
|
package/src/routers/shared.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { center, dist, type Point } from "../geom.ts";
|
|
|
2
2
|
import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
3
3
|
import { channelWidth } from "../channels.ts";
|
|
4
4
|
import { orderRun } from "../lane-order.ts";
|
|
5
|
+
import { stretchLaneOrders, type LaneStretchOrder } from "../lane-orders/stretch/index.ts";
|
|
5
6
|
import { byText } from "../compare.ts";
|
|
6
7
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
7
8
|
import { buildSections, type Section } from "../sections.ts";
|
|
8
9
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
9
10
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
10
11
|
import { snellMerges } from "./snell.ts";
|
|
12
|
+
import { groupRuleOf, type GroupRuleName } from "./trunks.ts";
|
|
11
13
|
|
|
12
14
|
/** The cost of a link that edges of one key share: a fixed discount, or the flow cost `len * f^alpha` (EDA-029). */
|
|
13
15
|
export type ShareCost = "fixed" | "flow";
|
|
@@ -22,12 +24,22 @@ export type ShareMerge = "corner" | "snell";
|
|
|
22
24
|
export interface SharedOptions extends WayOptions {
|
|
23
25
|
/** The name in the benchmark. */
|
|
24
26
|
readonly name: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The edges with one key share their ways. The option takes a rule or the name of a built-in rule (`GROUP_RULE_NAMES`).
|
|
29
|
+
* Without a rule, or with `all`, all edges share. An edge without a key shares with no edge.
|
|
30
|
+
*/
|
|
31
|
+
readonly rule?: GroupRule | GroupRuleName;
|
|
27
32
|
/** The cost of a link that an edge with the same key uses already, as a share of its length. The default is 0.5. */
|
|
28
33
|
readonly discount?: number;
|
|
29
34
|
/** The lane order of the runs. With `bundle`, the default, two edges keep one order on each run of their common stretch. With `run`, each run gets its order alone. */
|
|
30
35
|
readonly laneOrder?: RunOrder;
|
|
36
|
+
/**
|
|
37
|
+
* The lane order of each stretch of runs (LANE-STRETCH). With `none`, the default, each run gets its order alone.
|
|
38
|
+
* With `consensus`, `periphery` or `sifting`, the runs that follow each other on the routes share one order. Only the lane order `bundle` reads it.
|
|
39
|
+
*/
|
|
40
|
+
readonly stretchOrder?: LaneStretchOrder;
|
|
41
|
+
/** The number of passes of the stretch order `sifting`, from 1 to 4. The default is 2. */
|
|
42
|
+
readonly siftPasses?: number;
|
|
31
43
|
/**
|
|
32
44
|
* The cost of a shared link. With `fixed`, the default, a link that the key uses already costs `discount` times its length.
|
|
33
45
|
* With `flow`, one more edge on a link with `f` edges costs `len * ((f + 1)^alpha - f^alpha)` (EDA-029).
|
|
@@ -113,9 +125,11 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
113
125
|
const reuseExponent = Math.max(0, options.reuseExponent ?? 1);
|
|
114
126
|
const capacity = Math.max(0, options.capacity ?? 0);
|
|
115
127
|
const laneWidth = Math.max(0, options.laneWidth ?? 4);
|
|
128
|
+
// The name `all` is the same as no rule.
|
|
129
|
+
const rule = options.rule === undefined || options.rule === "all" ? undefined : groupRuleOf(options.rule);
|
|
116
130
|
const keyOf = (e: Edge): string => {
|
|
117
|
-
if (!
|
|
118
|
-
return
|
|
131
|
+
if (!rule) return "*";
|
|
132
|
+
return rule(e) ?? `\u0000${e.id}`;
|
|
119
133
|
};
|
|
120
134
|
const router: Router = {
|
|
121
135
|
name: options.name,
|
|
@@ -238,20 +252,38 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
238
252
|
}
|
|
239
253
|
|
|
240
254
|
// The users of each link, with the links to the ends. Two edges from one plug share the link from it.
|
|
241
|
-
const
|
|
255
|
+
const N = corners.points.length + ends.size + 1;
|
|
256
|
+
const pairOf = (a: number, b: number): number => (a < b ? a * N + b : b * N + a);
|
|
257
|
+
const users = new Map<string, Map<number, string[]>>();
|
|
242
258
|
for (const w of order) {
|
|
243
259
|
if (!w.found) continue;
|
|
244
260
|
let mine = users.get(w.key);
|
|
245
261
|
if (!mine) users.set(w.key, (mine = new Map()));
|
|
246
262
|
for (let i = 1; i < w.ids.length; i++) {
|
|
247
|
-
const
|
|
248
|
-
const b = w.ids[i]!;
|
|
249
|
-
const k = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
263
|
+
const k = pairOf(w.ids[i - 1]!, w.ids[i]!);
|
|
250
264
|
const list = mine.get(k);
|
|
251
265
|
if (list) list.push(w.edge.id);
|
|
252
266
|
else mine.set(k, [w.edge.id]);
|
|
253
267
|
}
|
|
254
268
|
}
|
|
269
|
+
// The signature of each link (ROUTE-SCALE): 0 for a link with fewer than 2 users, else the number of the sorted list of its users.
|
|
270
|
+
// Each link gets its signature one time. Two links get the same number only when they have the same sorted list, so the runs do not change.
|
|
271
|
+
const texts = new Map<string, number>();
|
|
272
|
+
const sigs = new Map<string, Map<number, number>>();
|
|
273
|
+
for (const [key, mine] of users) {
|
|
274
|
+
const own = new Map<number, number>();
|
|
275
|
+
for (const [k, list] of mine) {
|
|
276
|
+
if (list.length < 2) {
|
|
277
|
+
own.set(k, 0);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const text = [...list].sort(byText).join("\u0001");
|
|
281
|
+
let n = texts.get(text);
|
|
282
|
+
if (n === undefined) texts.set(text, (n = texts.size + 1));
|
|
283
|
+
own.set(k, n);
|
|
284
|
+
}
|
|
285
|
+
sigs.set(key, own);
|
|
286
|
+
}
|
|
255
287
|
|
|
256
288
|
// Each way breaks into stretches where the set of edges on the links stays the same.
|
|
257
289
|
// A shared stretch loses its needless corners one time, so it has the same points in each of its routes.
|
|
@@ -259,13 +291,8 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
259
291
|
const points = new Map<string, Point[]>();
|
|
260
292
|
for (const w of all) {
|
|
261
293
|
if (!w.found) continue;
|
|
262
|
-
const
|
|
263
|
-
const sig = (i: number):
|
|
264
|
-
const a = w.ids[i]!;
|
|
265
|
-
const b = w.ids[i + 1]!;
|
|
266
|
-
const list = used.get(a < b ? `${a}:${b}` : `${b}:${a}`)!;
|
|
267
|
-
return list.length < 2 ? "" : [...list].sort(byText).join("\u0001");
|
|
268
|
-
};
|
|
294
|
+
const own = sigs.get(w.key)!;
|
|
295
|
+
const sig = (i: number): number => own.get(pairOf(w.ids[i]!, w.ids[i + 1]!))!;
|
|
269
296
|
const out: Point[] = [w.ps.point];
|
|
270
297
|
const append = (piece: readonly Point[]): void => {
|
|
271
298
|
for (const p of piece) if (dist(p, out[out.length - 1]!) > 1e-9) out.push(p);
|
|
@@ -275,7 +302,7 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
275
302
|
const s = sig(i);
|
|
276
303
|
if (i + 1 < w.seq.length - 1 && sig(i + 1) === s) continue;
|
|
277
304
|
const raw = w.seq.slice(start, i + 2);
|
|
278
|
-
if (s ===
|
|
305
|
+
if (s === 0) append(corners.pull(raw));
|
|
279
306
|
else {
|
|
280
307
|
const a = w.ids[start]!;
|
|
281
308
|
const b = w.ids[i + 1]!;
|
|
@@ -298,10 +325,15 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
298
325
|
for (const w of all) routes.push({ edge: w.edge.id, points: points.get(w.edge.id) ?? [w.ps.point, w.pt.point] });
|
|
299
326
|
const shared: SharedRun[] = [];
|
|
300
327
|
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
301
|
-
|
|
302
|
-
|
|
328
|
+
const list = [...runs.values()];
|
|
329
|
+
const routesOf = (run: Run): Route[] => run.ways.map((w) => routeOf.get(w.edge.id)!);
|
|
330
|
+
// With a stretch order, the runs that follow each other on the routes share one lane order (LANE-STRETCH).
|
|
331
|
+
const method = options.laneOrder === "run" ? "none" : (options.stretchOrder ?? "none");
|
|
332
|
+
const stretch = method === "none" ? undefined : stretchLaneOrders(list.map((run) => ({ points: run.points, routes: routesOf(run) })), { method, passes: options.siftPasses ?? 2 });
|
|
333
|
+
list.forEach((run, i) => {
|
|
334
|
+
const edges = options.laneOrder === "run" ? laneOrderOfRun(run) : (stretch?.[i] ?? orderLanes(run.points, routesOf(run), "bundle"));
|
|
303
335
|
shared.push({ id: `r${shared.length}`, key: run.key, points: run.points, edges });
|
|
304
|
-
}
|
|
336
|
+
});
|
|
305
337
|
return { routes, runs: shared };
|
|
306
338
|
},
|
|
307
339
|
};
|
package/src/routers/trunks.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { balancedFan, fanPath, gilbertPoint } from "./fan.ts";
|
|
|
9
9
|
export interface TrunksOptions extends WayOptions {
|
|
10
10
|
/** The name in the benchmark. */
|
|
11
11
|
readonly name: string;
|
|
12
|
-
/** The grouping rule. The edges from one plug with one key share a trunk. */
|
|
13
|
-
readonly rule: GroupRule;
|
|
12
|
+
/** The grouping rule, or the name of a built-in rule (`GROUP_RULE_NAMES`). The edges from one plug with one key share a trunk. */
|
|
13
|
+
readonly rule: GroupRule | GroupRuleName;
|
|
14
14
|
/** The split point is at this share of the way to the center of the targets. The default is 0.5. */
|
|
15
15
|
readonly split?: number;
|
|
16
16
|
/** A group with fewer edges stays unbundled. The default is 2. */
|
|
@@ -55,6 +55,23 @@ export const byCollection: GroupRule = (e) => {
|
|
|
55
55
|
return typeof c === "string" ? c : undefined;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/** The names of the built-in grouping rules of the edges. With `collection`, the rule is `byCollection`. With `all`, each edge has one key. */
|
|
59
|
+
export type GroupRuleName = "collection" | "all";
|
|
60
|
+
|
|
61
|
+
/** The names of the built-in grouping rules, in the order of a control. */
|
|
62
|
+
export const GROUP_RULE_NAMES: readonly GroupRuleName[] = ["collection", "all"];
|
|
63
|
+
|
|
64
|
+
/** The rule `all`: each edge has the same key. */
|
|
65
|
+
const allEdges: GroupRule = () => "*";
|
|
66
|
+
|
|
67
|
+
/** This function gives the rule of a name of `GROUP_RULE_NAMES`. It gives a rule of the caller as it is. It throws a `RangeError` for an unknown name. */
|
|
68
|
+
export function groupRuleOf(rule: GroupRule | GroupRuleName): GroupRule {
|
|
69
|
+
if (typeof rule === "function") return rule;
|
|
70
|
+
if (rule === "collection") return byCollection;
|
|
71
|
+
if (rule === "all") return allEdges;
|
|
72
|
+
throw new RangeError(`There is no grouping rule "${String(rule)}". The names are ${GROUP_RULE_NAMES.join(" and ")}.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
/**
|
|
59
76
|
* This function groups the edges by their source plug and their key. Each group keeps the order of the edges.
|
|
60
77
|
* A group with fewer than `minSize` edges does not appear.
|
|
@@ -189,6 +206,7 @@ export function sharedWays(routes: readonly Route[], from: number): { points: Po
|
|
|
189
206
|
* The edges outside the groups go as in the corner router.
|
|
190
207
|
*/
|
|
191
208
|
export function trunksRouter(options: TrunksOptions): Router {
|
|
209
|
+
const rule = groupRuleOf(options.rule);
|
|
192
210
|
const router: Router = {
|
|
193
211
|
name: options.name,
|
|
194
212
|
route(graph: Graph): Result {
|
|
@@ -200,7 +218,7 @@ export function trunksRouter(options: TrunksOptions): Router {
|
|
|
200
218
|
const part = (options.leave ?? "part") === "part";
|
|
201
219
|
// A branch leaves the trunk early only on a line that keeps out of the boxes as far as the corner graph does.
|
|
202
220
|
const clear = (a: Point, b: Point): boolean => ways.obstacles.clear(a, b, ways.margin / 2);
|
|
203
|
-
for (const [id, group] of groupEdges(graph.edges,
|
|
221
|
+
for (const [id, group] of groupEdges(graph.edges, rule, options.minSize ?? 2)) {
|
|
204
222
|
const first = group[0]!;
|
|
205
223
|
const s = byId.get(first.source.node);
|
|
206
224
|
const targets = group.map((e) => byId.get(e.target.node));
|
package/src/segments.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dist, type Point } from "./geom.ts";
|
|
2
|
-
import { orderPlaced,
|
|
2
|
+
import { findRun, orderPlaced, placeRoutes } from "./lane-order.ts";
|
|
3
3
|
import type { Result, Route } from "./model.ts";
|
|
4
4
|
|
|
5
5
|
/** A piece of drawn way. A shared segment carries several edges, in lane order. A private segment carries one edge. */
|
|
@@ -34,16 +34,6 @@ export interface SegmentGraph {
|
|
|
34
34
|
|
|
35
35
|
const SAME = 1e-6;
|
|
36
36
|
|
|
37
|
-
/** This function gives the index where `p` holds the points of `part` in a row, from `from` on, or -1. */
|
|
38
|
-
function indexOf(p: readonly Point[], part: readonly Point[], from: number): number {
|
|
39
|
-
for (let i = from; i + part.length <= p.length; i++) {
|
|
40
|
-
let ok = true;
|
|
41
|
-
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j]!, part[j]!) <= SAME;
|
|
42
|
-
if (ok) return i;
|
|
43
|
-
}
|
|
44
|
-
return -1;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
/**
|
|
48
38
|
* This function breaks each route into segments. The runs and the trunks of the bundles become shared segments.
|
|
49
39
|
* The other parts of each route become private segments. The segments of a path, in order, give the points of its route again.
|
|
@@ -68,10 +58,10 @@ export function segmentsOf(result: Result): SegmentGraph {
|
|
|
68
58
|
// The place of each shared segment in the route, in the order of the route.
|
|
69
59
|
const found: { at: number; end: number; ref: SegmentRef }[] = [];
|
|
70
60
|
for (const s of onEdge.get(route.edge) ?? []) {
|
|
71
|
-
let at =
|
|
61
|
+
let at = findRun(p, s.points, 0);
|
|
72
62
|
let reverse = false;
|
|
73
63
|
if (at < 0) {
|
|
74
|
-
at =
|
|
64
|
+
at = findRun(p, [...s.points].reverse(), 0);
|
|
75
65
|
reverse = true;
|
|
76
66
|
}
|
|
77
67
|
if (at >= 0) found.push({ at, end: at + s.points.length - 1, ref: { segment: s.id, reverse, lane: s.lane } });
|
|
@@ -174,6 +164,8 @@ export function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<stri
|
|
|
174
164
|
/** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
|
|
175
165
|
function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Route[] {
|
|
176
166
|
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
167
|
+
// Each segment gets its spacing one time (ROUTE-SCALE). A key of `runKey` sorts the edges of the segment, and a busy segment is on many paths.
|
|
168
|
+
const spacings = new Map<Segment, number>();
|
|
177
169
|
const routes: Route[] = [];
|
|
178
170
|
for (const path of graph.paths) {
|
|
179
171
|
const ends = pathPoints(graph, path);
|
|
@@ -181,7 +173,8 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
|
|
|
181
173
|
for (const ref of path.segments) {
|
|
182
174
|
const s = byId.get(ref.segment);
|
|
183
175
|
if (!s) continue;
|
|
184
|
-
|
|
176
|
+
let spacing = spacings.get(s);
|
|
177
|
+
if (spacing === undefined) spacings.set(s, (spacing = spacingOf(s)));
|
|
185
178
|
const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
|
|
186
179
|
for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
|
|
187
180
|
}
|
|
@@ -218,18 +211,7 @@ export type RunOrder = "bundle" | "run";
|
|
|
218
211
|
* On a run with a cycle, the order depends on the order of `routes`. The item LANE-STRETCH is to remove the cycles.
|
|
219
212
|
*/
|
|
220
213
|
export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
|
|
221
|
-
const placed
|
|
222
|
-
const lost: string[] = [];
|
|
223
|
-
for (const r of routes) {
|
|
224
|
-
let at = indexOf(r.points, run, 0);
|
|
225
|
-
if (at >= 0) placed.push({ edge: r.edge, p: r.points, s: at, e: at + run.length - 1 });
|
|
226
|
-
else {
|
|
227
|
-
const p = [...r.points].reverse();
|
|
228
|
-
at = indexOf(p, run, 0);
|
|
229
|
-
if (at >= 0) placed.push({ edge: r.edge, p, s: at, e: at + run.length - 1 });
|
|
230
|
-
else lost.push(r.edge);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
214
|
+
const { placed, lost } = placeRoutes(run, routes);
|
|
233
215
|
const lanes = orderPlaced(placed, order);
|
|
234
216
|
return [...lanes.map((x) => x.edge), ...lost.sort()];
|
|
235
217
|
}
|
|
@@ -66,7 +66,9 @@ import { TRACLUS_DEFAULTS, traclusPlan, type TraclusOptions } from "../highways/
|
|
|
66
66
|
import { WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "../highways/weiszfeld/index.ts";
|
|
67
67
|
import { WSPD_DEFAULTS, wspdPlan, type WspdOptions } from "../highways/wspd/index.ts";
|
|
68
68
|
import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zipper/index.ts";
|
|
69
|
+
import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
|
|
69
70
|
import type { Graph, Result, Router } from "../model.ts";
|
|
71
|
+
import { facingEnds } from "../plugs/facing/index.ts";
|
|
70
72
|
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
71
73
|
import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
72
74
|
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
@@ -76,10 +78,12 @@ import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
|
|
|
76
78
|
import { straightRouter } from "../routers/straight.ts";
|
|
77
79
|
import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
|
|
78
80
|
import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
|
|
81
|
+
import { runPipeline, type PipelineOptions } from "../whole/pipeline/index.ts";
|
|
79
82
|
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
|
|
80
83
|
import type { OptionSchema } from "./options.ts";
|
|
81
84
|
import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
|
|
82
|
-
import {
|
|
85
|
+
import { DEPTHS_SCHEMA, PIPELINE_SCHEMA } from "./schemas.ts";
|
|
86
|
+
import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA } from "./schemas.ts";
|
|
83
87
|
|
|
84
88
|
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
85
89
|
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
@@ -150,7 +154,7 @@ export const routerStrategies: {
|
|
|
150
154
|
sharedRouter,
|
|
151
155
|
SHARED_SCHEMA,
|
|
152
156
|
),
|
|
153
|
-
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter,
|
|
157
|
+
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter, SHARED_ALL_SCHEMA),
|
|
154
158
|
sections: routerStrategy<RouterPreset<SectionsOptions>>(
|
|
155
159
|
"sections",
|
|
156
160
|
"A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
|
|
@@ -895,6 +899,51 @@ export const highwayStrategies: {
|
|
|
895
899
|
"corridors+ids": gateStrategy(corridors, stableIdGate),
|
|
896
900
|
});
|
|
897
901
|
|
|
902
|
+
/** The built-in plug rules: the strategies of the step `plugs`. The router `corners` routes between their plugs with its option `plug`. */
|
|
903
|
+
export const plugStrategies: {
|
|
904
|
+
readonly facing: Strategy<"plugs", object>;
|
|
905
|
+
} = Object.freeze({
|
|
906
|
+
facing: defineStrategy({
|
|
907
|
+
step: "plugs",
|
|
908
|
+
name: "facing",
|
|
909
|
+
describe: "Each free plug faces the other end of its edge, on the side of the larger component of the direction. It is the default of the router `corners`.",
|
|
910
|
+
defaults: {},
|
|
911
|
+
schema: {},
|
|
912
|
+
run: (input) => facingEnds(input),
|
|
913
|
+
}),
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
/** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
|
|
917
|
+
export const levelStrategies: {
|
|
918
|
+
readonly depths: Strategy<"levels", DepthLevelOptions>;
|
|
919
|
+
} = Object.freeze({
|
|
920
|
+
depths: defineStrategy<"levels", DepthLevelOptions>({
|
|
921
|
+
step: "levels",
|
|
922
|
+
name: "depths",
|
|
923
|
+
describe: "One zoom level for each depth of the tree, and a last level with each node alone. A link of a level goes around the other items. It is the default of the router `sections`.",
|
|
924
|
+
defaults: {},
|
|
925
|
+
schema: DEPTHS_SCHEMA,
|
|
926
|
+
run: (input, o) => depthLevels(input, o),
|
|
927
|
+
}),
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* The built-in whole strategies: the strategies of the step `whole`. The strategy `pipeline` runs one strategy of each step, as the demo of the site does.
|
|
932
|
+
* Without options, it routes the graph with the router `corners`.
|
|
933
|
+
*/
|
|
934
|
+
export const wholeStrategies: {
|
|
935
|
+
readonly pipeline: Strategy<"whole", PipelineOptions>;
|
|
936
|
+
} = Object.freeze({
|
|
937
|
+
pipeline: defineStrategy<"whole", PipelineOptions>({
|
|
938
|
+
step: "whole",
|
|
939
|
+
name: "pipeline",
|
|
940
|
+
describe: "One setting of the steps. The room opens the board, then the router or the highway plan routes it with the rules that the router reads.",
|
|
941
|
+
defaults: { router: routerStrategies.corners as Strategy<"router"> },
|
|
942
|
+
schema: PIPELINE_SCHEMA,
|
|
943
|
+
run: (graph, o) => runPipeline(graph, o),
|
|
944
|
+
}),
|
|
945
|
+
});
|
|
946
|
+
|
|
898
947
|
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
899
948
|
export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
|
|
900
949
|
...Object.values(groupStrategies),
|
|
@@ -902,6 +951,9 @@ export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
|
|
|
902
951
|
...Object.values(highwayStrategies),
|
|
903
952
|
...Object.values(curveStrategies),
|
|
904
953
|
...Object.values(roomStrategies),
|
|
954
|
+
...Object.values(plugStrategies),
|
|
955
|
+
...Object.values(levelStrategies),
|
|
956
|
+
...Object.values(wholeStrategies),
|
|
905
957
|
]);
|
|
906
958
|
|
|
907
959
|
/** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
|