@mark1russell7/highways 0.1.4 → 0.2.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 +118 -0
- package/build/index.d.ts +438 -74
- package/build/index.d.ts.map +1 -1
- package/build/index.js +837 -68
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/curves.ts +35 -8
- package/src/bench/room.ts +83 -15
- package/src/bench/sections.ts +121 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +5 -0
- package/src/levels.ts +17 -6
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- package/src/room/index.ts +22 -2
- package/src/room/lanes.ts +175 -0
- package/src/routers/index.ts +1 -1
- package/src/routers/sections.ts +66 -18
- package/src/sections.ts +46 -3
- package/src/strategies/builtins.ts +194 -0
- package/src/strategies/index.ts +2 -0
- package/src/strategies/model.ts +161 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
2
|
+
import { Grid } from "../grid.ts";
|
|
3
|
+
import type { LevelLink } from "../model.ts";
|
|
4
|
+
import { boardGaps, type Boxed } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The lane order of the pieces of the links in one gap.
|
|
8
|
+
*
|
|
9
|
+
* - `links`: the order of the list of links. This order reads nothing of the board.
|
|
10
|
+
* - `ends`: the pieces that turn to the near side at their two ends come first. The pieces that turn to the far side come last.
|
|
11
|
+
* - `crossings`: the order with the fewest crossings between the pieces of the gap and the legs at their ends. A local search finds it.
|
|
12
|
+
*/
|
|
13
|
+
export type LaneOrder = "links" | "ends" | "crossings";
|
|
14
|
+
|
|
15
|
+
/** The lane orders of `gapLanes`, in the order of the benchmark. */
|
|
16
|
+
export const LANE_ORDERS: readonly LaneOrder[] = ["links", "ends", "crossings"];
|
|
17
|
+
|
|
18
|
+
/** The options of `gapLanes`. */
|
|
19
|
+
export interface GapLaneOptions {
|
|
20
|
+
/** The largest distance between two lanes. A narrow gap puts its lanes nearer. The default is 4. */
|
|
21
|
+
readonly spacing?: number;
|
|
22
|
+
/** The lane order in each gap. The default is `crossings`. */
|
|
23
|
+
readonly order?: LaneOrder;
|
|
24
|
+
/** Two box edges nearer than this distance make one boundary of the sweep, as in `boardGaps`. The default is 0.5. */
|
|
25
|
+
readonly snap?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A piece of a link: a leg of the link that runs along a gap. */
|
|
29
|
+
interface Piece {
|
|
30
|
+
/** The index of the link. */
|
|
31
|
+
readonly link: number;
|
|
32
|
+
/** The index of the last point of the leg. The leg goes from the point `seg - 1` to the point `seg`. */
|
|
33
|
+
readonly seg: number;
|
|
34
|
+
/** The span of the leg along the gap. */
|
|
35
|
+
readonly lo: number;
|
|
36
|
+
readonly hi: number;
|
|
37
|
+
/** For each end of the leg: its place along the gap, and the side (-1 or 1) across the gap where the link turns. */
|
|
38
|
+
readonly turns: readonly (readonly [number, number])[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EPS = 1e-6;
|
|
42
|
+
const ROUNDS = 8;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
|
|
46
|
+
* A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
|
|
47
|
+
* When a move puts a link through more boxes than before, the link keeps its points.
|
|
48
|
+
*/
|
|
49
|
+
export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): LevelLink[] {
|
|
50
|
+
const spacing = Math.max(0, options.spacing ?? 4);
|
|
51
|
+
const order = options.order ?? "crossings";
|
|
52
|
+
const moved = links.map((l) => l.points.map((p) => ({ x: p.x, y: p.y })));
|
|
53
|
+
for (const g of boardGaps(items, options.snap)) {
|
|
54
|
+
if (g.width <= EPS) continue;
|
|
55
|
+
const across = g.axis;
|
|
56
|
+
const along = g.axis === "x" ? "y" : "x";
|
|
57
|
+
const lo = g.at - g.width;
|
|
58
|
+
const hi = g.at;
|
|
59
|
+
const pieces: Piece[] = [];
|
|
60
|
+
links.forEach((l, li) => {
|
|
61
|
+
const p = l.points;
|
|
62
|
+
// The first and the last legs hold the plugs, so they do not move.
|
|
63
|
+
for (let i = 2; i <= p.length - 2; i++) {
|
|
64
|
+
const a = p[i - 1]!;
|
|
65
|
+
const b = p[i]!;
|
|
66
|
+
if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
|
|
67
|
+
if (Math.abs(a[along] - b[along]) <= EPS) continue;
|
|
68
|
+
const side = (q: Point, r: Point): number => Math.sign(r[across] - q[across]);
|
|
69
|
+
pieces.push({
|
|
70
|
+
link: li,
|
|
71
|
+
seg: i,
|
|
72
|
+
lo: Math.min(a[along], b[along]),
|
|
73
|
+
hi: Math.max(a[along], b[along]),
|
|
74
|
+
turns: [
|
|
75
|
+
[a[along], side(a, p[i - 2]!)],
|
|
76
|
+
[b[along], side(b, p[i + 1]!)],
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (pieces.length === 0) continue;
|
|
82
|
+
const list = orderPieces(pieces, order);
|
|
83
|
+
// A piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
|
|
84
|
+
const lane: number[] = [];
|
|
85
|
+
let n = 0;
|
|
86
|
+
list.forEach((P, i) => {
|
|
87
|
+
let k = 0;
|
|
88
|
+
for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
|
|
89
|
+
lane.push(k);
|
|
90
|
+
n = Math.max(n, k + 1);
|
|
91
|
+
});
|
|
92
|
+
const step = Math.min(spacing, g.width / (n + 1));
|
|
93
|
+
const mid = (lo + hi) / 2;
|
|
94
|
+
list.forEach((P, i) => {
|
|
95
|
+
const v = mid + (lane[i]! - (n - 1) / 2) * step;
|
|
96
|
+
moved[P.link]![P.seg - 1]![across] = v;
|
|
97
|
+
moved[P.link]![P.seg]![across] = v;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const boxes = new Grid<Box>(Math.max(20, meanSide(items)));
|
|
101
|
+
for (const i of items) boxes.add(i.box, i.box);
|
|
102
|
+
return links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const overlaps = (P: Piece, Q: Piece): boolean => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
|
|
109
|
+
* A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
|
|
110
|
+
*/
|
|
111
|
+
function cost(P: Piece, Q: Piece): number {
|
|
112
|
+
let c = 0;
|
|
113
|
+
for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
|
|
114
|
+
for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
|
|
115
|
+
return c;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** This function gives the pieces of one gap in lane order, from the near side to the far side. */
|
|
119
|
+
function orderPieces(pieces: readonly Piece[], order: LaneOrder): Piece[] {
|
|
120
|
+
if (order === "links") return [...pieces];
|
|
121
|
+
const sum = (P: Piece): number => P.turns.reduce((s, [, side]) => s + side, 0);
|
|
122
|
+
const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
|
|
123
|
+
if (order === "ends") return list;
|
|
124
|
+
// The local search moves one piece to the place that removes the most crossings, until no move removes one.
|
|
125
|
+
const n = list.length;
|
|
126
|
+
const index = new Map(list.map((P, i) => [P, i]));
|
|
127
|
+
const c: number[][] = list.map((P) => list.map((Q) => (P !== Q && overlaps(P, Q) ? cost(P, Q) : 0)));
|
|
128
|
+
const at = (P: Piece, Q: Piece): number => c[index.get(P)!]![index.get(Q)!]!;
|
|
129
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
130
|
+
let better = false;
|
|
131
|
+
for (let i = 0; i < n; i++) {
|
|
132
|
+
const x = list[i]!;
|
|
133
|
+
// The change of the crossings when `x` moves to the place `k`, for each `k`.
|
|
134
|
+
let best = 0;
|
|
135
|
+
let to = i;
|
|
136
|
+
let d = 0;
|
|
137
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
138
|
+
d += at(x, list[k]!) - at(list[k]!, x);
|
|
139
|
+
if (d < best) [best, to] = [d, k];
|
|
140
|
+
}
|
|
141
|
+
d = 0;
|
|
142
|
+
for (let k = i + 1; k < n; k++) {
|
|
143
|
+
d += at(list[k]!, x) - at(x, list[k]!);
|
|
144
|
+
if (d < best) [best, to] = [d, k];
|
|
145
|
+
}
|
|
146
|
+
if (to !== i) {
|
|
147
|
+
list.splice(i, 1);
|
|
148
|
+
list.splice(to, 0, x);
|
|
149
|
+
better = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!better) break;
|
|
153
|
+
}
|
|
154
|
+
return list;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** This function gives the mean of the longer side of the boxes. */
|
|
158
|
+
function meanSide(items: readonly Boxed[]): number {
|
|
159
|
+
let s = 0;
|
|
160
|
+
for (const i of items) s += Math.max(i.box.w, i.box.h);
|
|
161
|
+
return items.length ? s / items.length : 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** This function counts the legs of a polyline that go through a box, for each box. */
|
|
165
|
+
function hits(boxes: Grid<Box>, p: readonly Point[]): number {
|
|
166
|
+
let n = 0;
|
|
167
|
+
for (let i = 1; i < p.length; i++) {
|
|
168
|
+
const a = p[i - 1]!;
|
|
169
|
+
const b = p[i]!;
|
|
170
|
+
boxes.querySegment(a, b, 0, (box) => {
|
|
171
|
+
if (segmentHitsBox(a, b, box)) n++;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return n;
|
|
175
|
+
}
|
package/src/routers/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { sharedRouter, type SharedOptions } from "./shared.ts";
|
|
|
13
13
|
export { sectionsRouter, type SectionsOptions } from "./sections.ts";
|
|
14
14
|
export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunksOptions } from "./trunks.ts";
|
|
15
15
|
|
|
16
|
-
/** The routers
|
|
16
|
+
/** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
|
|
17
17
|
export const ROUTERS: readonly Router[] = [
|
|
18
18
|
straightRouter,
|
|
19
19
|
plugsRouter,
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
2
|
import { center, dist, pointSegmentDistance, polylineLength, type Box, type Point } from "../geom.ts";
|
|
3
|
-
import { levelLinks, type ItemPair } from "../levels.ts";
|
|
3
|
+
import { LEVEL_LINK_DEFAULTS, levelLinks, type ItemPair, type LevelLinkOptions } from "../levels.ts";
|
|
4
4
|
import { nodeIndex, type Graph, type Level, type LevelItem, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
5
5
|
import { placePlug } from "../plugs.ts";
|
|
6
|
-
import { allSections, buildSections, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
6
|
+
import { allSections, buildSections, checkSections, SECTION_DEFAULTS, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
7
7
|
import { orderLanes } from "../segments.ts";
|
|
8
|
-
import {
|
|
8
|
+
import type { Strategy } from "../strategies/model.ts";
|
|
9
|
+
import { WayFinder } from "../ways.ts";
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The options of the router with sections. Each option has a default, and the defaults give the output of version 0.1.5.
|
|
13
|
+
* The options `cap`, `maxDepth` and `collapse` are the limits of the default grouping rule, the quadtree of `buildSections`.
|
|
14
|
+
* The options `tightMargin` and `plugInset` are the limits of the level links (`levelLinks`).
|
|
15
|
+
*/
|
|
16
|
+
export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
12
17
|
/** The name in the benchmark. */
|
|
13
18
|
readonly name: string;
|
|
14
19
|
/**
|
|
@@ -16,19 +21,49 @@ export interface SectionsOptions extends WayOptions, SectionOptions {
|
|
|
16
21
|
* and fewer shared ways. Without it, a way passes each hub from its node up.
|
|
17
22
|
*/
|
|
18
23
|
readonly hubs?: number;
|
|
19
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The hub limit. A way through the hubs can be at most this many times as long as the line between its plugs, plus `stretchPad` margins.
|
|
26
|
+
* Else the edge goes directly. The default is 2. The value `Infinity` keeps each way through the hubs.
|
|
27
|
+
*/
|
|
20
28
|
readonly stretch?: number;
|
|
29
|
+
/** A way through the hubs can be this many margins longer than `stretch` times the line between its plugs. The default is 4. */
|
|
30
|
+
readonly stretchPad?: number;
|
|
31
|
+
/**
|
|
32
|
+
* A way skips a hub that is more than this many units farther from the target than the nearest approach of the way before it.
|
|
33
|
+
* The default is 80, two times `BACKTRACK_MIN`. The value `Infinity` keeps each hub.
|
|
34
|
+
*/
|
|
35
|
+
readonly hubSlack?: number;
|
|
36
|
+
/**
|
|
37
|
+
* The grouping rule: a strategy of the step `group` that makes the tree of sections. The hubs and the zoom levels come from this tree.
|
|
38
|
+
* Without it, the router uses the quadtree of `buildSections` with the options `cap`, `maxDepth` and `collapse`.
|
|
39
|
+
*/
|
|
40
|
+
readonly group?: Strategy<"group">;
|
|
41
|
+
/** The options of the strategy `group`. The strategy gets its `defaults` for each option that this object does not give. */
|
|
42
|
+
readonly groupOptions?: object;
|
|
43
|
+
/**
|
|
44
|
+
* The depths of the tree that get a zoom level with groups. The last level, with each node alone, always comes after them.
|
|
45
|
+
* The default is each depth, from 0 to the depth of the deepest section.
|
|
46
|
+
*/
|
|
47
|
+
readonly levels?: readonly number[];
|
|
21
48
|
/** The benchmark skips a fixture with more nodes. */
|
|
22
49
|
readonly maxNodes?: number;
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
/** The default values of the numbers of `SectionsOptions`: the limits of the hubs, of the tree and of the level links. */
|
|
53
|
+
export const SECTIONS_DEFAULTS: Readonly<Required<Pick<SectionsOptions, "cap" | "maxDepth" | "collapse" | "tightMargin" | "plugInset" | "hubs" | "stretch" | "stretchPad" | "hubSlack">>> =
|
|
54
|
+
Object.freeze({
|
|
55
|
+
cap: SECTION_DEFAULTS.cap,
|
|
56
|
+
maxDepth: SECTION_DEFAULTS.maxDepth,
|
|
57
|
+
collapse: SECTION_DEFAULTS.collapse,
|
|
58
|
+
tightMargin: LEVEL_LINK_DEFAULTS.tightMargin,
|
|
59
|
+
plugInset: LEVEL_LINK_DEFAULTS.plugInset,
|
|
60
|
+
hubs: Infinity,
|
|
61
|
+
stretch: 2,
|
|
62
|
+
stretchPad: 4,
|
|
63
|
+
hubSlack: 2 * BACKTRACK_MIN,
|
|
64
|
+
});
|
|
26
65
|
|
|
27
|
-
|
|
28
|
-
* A hub can be this many units farther from the target than the nearest approach of the way before it.
|
|
29
|
-
* A hub farther away goes out and back, so the way skips it.
|
|
30
|
-
*/
|
|
31
|
-
const HUB_SLACK = 2 * BACKTRACK_MIN;
|
|
66
|
+
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
|
|
32
67
|
|
|
33
68
|
/**
|
|
34
69
|
* This function makes a router with a tree of sections. Each section has a hub: the free corner nearest to the center of its nodes.
|
|
@@ -45,12 +80,14 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
45
80
|
const router: Router = {
|
|
46
81
|
name: options.name,
|
|
47
82
|
route(graph: Graph): Result {
|
|
48
|
-
const stretch = options.stretch ??
|
|
83
|
+
const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
|
|
84
|
+
const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
|
|
85
|
+
const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
|
|
49
86
|
const byId = nodeIndex(graph);
|
|
50
87
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
51
88
|
const ways = new WayFinder(graph.nodes, options);
|
|
52
89
|
const corners = ways.corners;
|
|
53
|
-
const root =
|
|
90
|
+
const root = groupNodes(graph, options);
|
|
54
91
|
const parent = new Map<Section, Section>();
|
|
55
92
|
const leafOf: Section[] = [];
|
|
56
93
|
for (const s of allSections(root)) {
|
|
@@ -82,7 +119,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
82
119
|
let last = from;
|
|
83
120
|
for (const c of list) {
|
|
84
121
|
const h = hub(c);
|
|
85
|
-
if (dist(h, to) > near +
|
|
122
|
+
if (dist(h, to) > near + hubSlack) continue;
|
|
86
123
|
near = Math.min(near, pointSegmentDistance(to, last, h));
|
|
87
124
|
last = h;
|
|
88
125
|
kept.push(c);
|
|
@@ -132,7 +169,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
132
169
|
const b = up(lv);
|
|
133
170
|
const common = new Set(b);
|
|
134
171
|
const lca = a.find((x) => common.has(x))!;
|
|
135
|
-
const keep = options.hubs ??
|
|
172
|
+
const keep = options.hubs ?? SECTIONS_DEFAULTS.hubs;
|
|
136
173
|
const side = (list: Section[]): Section[] => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
137
174
|
const end = ways.out(pt);
|
|
138
175
|
const start = ways.out(ps);
|
|
@@ -151,7 +188,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
151
188
|
append(piece(chain[chain.length - 1]!.id, hub(chain[chain.length - 1]!), at(end), end, used));
|
|
152
189
|
append([pt.point]);
|
|
153
190
|
// A way through the hubs that is too long, or that goes out and back, gives way to the direct way.
|
|
154
|
-
if (polylineLength(out) > stretch * dist(ps.point, pt.point) +
|
|
191
|
+
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + stretchPad * ways.margin || goesOutAndBack(out)) {
|
|
155
192
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
156
193
|
continue;
|
|
157
194
|
}
|
|
@@ -176,17 +213,28 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
176
213
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
177
214
|
}
|
|
178
215
|
|
|
216
|
+
/** This function gives the tree of sections of the grouping rule. It examines the tree of a strategy (`checkSections`). */
|
|
217
|
+
function groupNodes(graph: Graph, options: SectionsOptions): Section {
|
|
218
|
+
if (!options.group) return buildSections(graph.nodes, options);
|
|
219
|
+
const root = options.group.run(graph, options.groupOptions);
|
|
220
|
+
checkSections(root, graph.nodes.length);
|
|
221
|
+
return root;
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
/**
|
|
180
225
|
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
226
|
+
* With the option `levels`, only those depths get a level with groups.
|
|
181
227
|
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
182
228
|
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
183
229
|
* In the last level, the points of a link are the route of its first edge.
|
|
184
230
|
*/
|
|
185
|
-
function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options:
|
|
231
|
+
function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options: SectionsOptions): Level[] {
|
|
186
232
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
187
233
|
const levels: Level[] = [];
|
|
188
234
|
const deepest = treeDepth(root);
|
|
235
|
+
const shown = options.levels && new Set(options.levels);
|
|
189
236
|
for (let d = 0; d <= deepest + 1; d++) {
|
|
237
|
+
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
190
238
|
const items: LevelItem[] = [];
|
|
191
239
|
const itemOf: (Section | number)[] = [];
|
|
192
240
|
const boxOf = (list: readonly number[]): Box => {
|
package/src/sections.ts
CHANGED
|
@@ -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,40 @@ 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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { arcCurve, type ArcOptions } from "../curves/arc/index.ts";
|
|
2
|
+
import { roundedCurve, type RoundedOptions } from "../curves/rounded/index.ts";
|
|
3
|
+
import { splineCurve, type SplineOptions } from "../curves/spline/index.ts";
|
|
4
|
+
import { straightCurve } from "../curves/straight/index.ts";
|
|
5
|
+
import { byDataGroup, keyGroups, type KeyOptions } from "../groups/keys/index.ts";
|
|
6
|
+
import { quadtreeGroups } from "../groups/quadtree/index.ts";
|
|
7
|
+
import type { Graph, Result, Router } from "../model.ts";
|
|
8
|
+
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
9
|
+
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
10
|
+
import { plugsRouter } from "../routers/plugs.ts";
|
|
11
|
+
import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
|
|
12
|
+
import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
|
|
13
|
+
import { straightRouter } from "../routers/straight.ts";
|
|
14
|
+
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";
|
|
17
|
+
|
|
18
|
+
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
19
|
+
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
20
|
+
|
|
21
|
+
/** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
|
|
22
|
+
function routerStrategy<O extends object>(
|
|
23
|
+
name: string,
|
|
24
|
+
describe: string,
|
|
25
|
+
maxNodes: number,
|
|
26
|
+
defaults: O,
|
|
27
|
+
make: (options: O & { readonly name: string }) => Router,
|
|
28
|
+
): Strategy<"router", O> {
|
|
29
|
+
return defineStrategy({ step: "router", name, describe, defaults, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** This function makes a router strategy for a router without options. */
|
|
33
|
+
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) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
|
|
38
|
+
export const routerStrategies: {
|
|
39
|
+
readonly straight: Strategy<"router", object>;
|
|
40
|
+
readonly plugs: Strategy<"router", object>;
|
|
41
|
+
readonly corners: Strategy<"router", RouterPreset<CornersOptions>>;
|
|
42
|
+
readonly "corners-smooth": Strategy<"router", RouterPreset<CornersOptions>>;
|
|
43
|
+
readonly trunks: Strategy<"router", RouterPreset<TrunksOptions>>;
|
|
44
|
+
readonly shared: Strategy<"router", RouterPreset<SharedOptions>>;
|
|
45
|
+
readonly "shared-all": Strategy<"router", RouterPreset<SharedOptions>>;
|
|
46
|
+
readonly sections: Strategy<"router", RouterPreset<SectionsOptions>>;
|
|
47
|
+
readonly "sections-tree": Strategy<"router", RouterPreset<SectionsOptions>>;
|
|
48
|
+
} = Object.freeze({
|
|
49
|
+
straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
|
|
50
|
+
plugs: fixedRouter(
|
|
51
|
+
plugsRouter,
|
|
52
|
+
"Each edge leaves its plug along the normal of the side and follows the curve of the inflection rule. It does not go around the nodes.",
|
|
53
|
+
),
|
|
54
|
+
corners: routerStrategy<RouterPreset<CornersOptions>>(
|
|
55
|
+
"corners",
|
|
56
|
+
"Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.",
|
|
57
|
+
10_000,
|
|
58
|
+
{},
|
|
59
|
+
cornersRouter,
|
|
60
|
+
),
|
|
61
|
+
"corners-smooth": routerStrategy<RouterPreset<CornersOptions>>(
|
|
62
|
+
"corners-smooth",
|
|
63
|
+
"The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.",
|
|
64
|
+
10_000,
|
|
65
|
+
{ smooth: true },
|
|
66
|
+
cornersRouter,
|
|
67
|
+
),
|
|
68
|
+
trunks: routerStrategy<RouterPreset<TrunksOptions>>(
|
|
69
|
+
"trunks",
|
|
70
|
+
"A rule puts edges in groups. The edges of a group leave one plug of one node on one trunk. At a split point, each edge goes on to its target.",
|
|
71
|
+
10_000,
|
|
72
|
+
{ rule: byCollection },
|
|
73
|
+
trunksRouter,
|
|
74
|
+
),
|
|
75
|
+
shared: routerStrategy<RouterPreset<SharedOptions>>(
|
|
76
|
+
"shared",
|
|
77
|
+
"The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.",
|
|
78
|
+
10_000,
|
|
79
|
+
{ rule: byCollection },
|
|
80
|
+
sharedRouter,
|
|
81
|
+
),
|
|
82
|
+
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter),
|
|
83
|
+
sections: routerStrategy<RouterPreset<SectionsOptions>>(
|
|
84
|
+
"sections",
|
|
85
|
+
"A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
|
|
86
|
+
100_000,
|
|
87
|
+
{},
|
|
88
|
+
sectionsRouter,
|
|
89
|
+
),
|
|
90
|
+
"sections-tree": routerStrategy<RouterPreset<SectionsOptions>>(
|
|
91
|
+
"sections-tree",
|
|
92
|
+
"The same as `sections`, but without the limit of the stretch.",
|
|
93
|
+
100_000,
|
|
94
|
+
{ stretch: Infinity },
|
|
95
|
+
sectionsRouter,
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** The context of a curve input, without the keys that have no value. */
|
|
100
|
+
const contextOf = (input: CurveInput): Omit<CurveInput, "points"> => ({
|
|
101
|
+
...(input.startNormal ? { startNormal: input.startNormal } : {}),
|
|
102
|
+
...(input.endNormal ? { endNormal: input.endNormal } : {}),
|
|
103
|
+
...(input.space ? { space: input.space } : {}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** The options of the curve strategies `rounded` and `arc`. */
|
|
107
|
+
export interface RadiusOptions {
|
|
108
|
+
/** The radius of the corners. The default is 12. */
|
|
109
|
+
readonly radius: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The options of the curve strategy `spline`. */
|
|
113
|
+
export interface TensionOptions {
|
|
114
|
+
/** The tension of the spline, from 0 (the spline) to 1 (the polyline). The default is 0. */
|
|
115
|
+
readonly tension: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
|
|
119
|
+
export const curveStrategies: {
|
|
120
|
+
readonly straight: Strategy<"curve", object>;
|
|
121
|
+
readonly rounded: Strategy<"curve", RadiusOptions>;
|
|
122
|
+
readonly spline: Strategy<"curve", TensionOptions>;
|
|
123
|
+
readonly arc: Strategy<"curve", RadiusOptions>;
|
|
124
|
+
} = Object.freeze({
|
|
125
|
+
straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, run: (input: CurveInput) => straightCurve(input.points) }),
|
|
126
|
+
rounded: defineStrategy({
|
|
127
|
+
step: "curve",
|
|
128
|
+
name: "rounded",
|
|
129
|
+
describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
|
|
130
|
+
defaults: { radius: 12 },
|
|
131
|
+
run: (input: CurveInput, o: Readonly<RadiusOptions>) => roundedCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies RoundedOptions),
|
|
132
|
+
}),
|
|
133
|
+
spline: defineStrategy({
|
|
134
|
+
step: "curve",
|
|
135
|
+
name: "spline",
|
|
136
|
+
describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
|
|
137
|
+
defaults: { tension: 0 },
|
|
138
|
+
run: (input: CurveInput, o: Readonly<TensionOptions>) => splineCurve(input.points, { ...contextOf(input), tension: o.tension } satisfies SplineOptions),
|
|
139
|
+
}),
|
|
140
|
+
arc: defineStrategy({
|
|
141
|
+
step: "curve",
|
|
142
|
+
name: "arc",
|
|
143
|
+
describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
|
|
144
|
+
defaults: { radius: 12 },
|
|
145
|
+
run: (input: CurveInput, o: Readonly<RadiusOptions>) => arcCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies ArcOptions),
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** The options of a room strategy: the options of `roomDemand`, without the method. */
|
|
150
|
+
export type RoomPreset = Omit<RoomOptions, "method">;
|
|
151
|
+
|
|
152
|
+
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 }) });
|
|
154
|
+
|
|
155
|
+
/** The built-in room strategies: the methods of `roomDemand`. */
|
|
156
|
+
export const roomStrategies: {
|
|
157
|
+
readonly routes: Strategy<"room", RoomPreset>;
|
|
158
|
+
readonly lines: Strategy<"room", RoomPreset>;
|
|
159
|
+
} = Object.freeze({
|
|
160
|
+
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
|
+
lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap."),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** 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
|
+
export const groupStrategies: {
|
|
166
|
+
readonly quadtree: Strategy<"group", Required<SectionOptions>>;
|
|
167
|
+
readonly keys: Strategy<"group", KeyOptions>;
|
|
168
|
+
} = Object.freeze({
|
|
169
|
+
quadtree: defineStrategy({
|
|
170
|
+
step: "group",
|
|
171
|
+
name: "quadtree",
|
|
172
|
+
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
|
+
defaults: { ...SECTION_DEFAULTS },
|
|
174
|
+
run: (graph: Graph, o: Readonly<Required<SectionOptions>>) => quadtreeGroups(graph, o),
|
|
175
|
+
}),
|
|
176
|
+
keys: defineStrategy({
|
|
177
|
+
step: "group",
|
|
178
|
+
name: "keys",
|
|
179
|
+
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
|
+
defaults: { key: byDataGroup } as KeyOptions,
|
|
181
|
+
run: (graph: Graph, o: Readonly<KeyOptions>) => keyGroups(graph, o),
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
186
|
+
export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
|
|
187
|
+
...Object.values(groupStrategies),
|
|
188
|
+
...Object.values(routerStrategies),
|
|
189
|
+
...Object.values(curveStrategies),
|
|
190
|
+
...Object.values(roomStrategies),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
/** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
|
|
194
|
+
export const STRATEGIES: Registry = createRegistry(BUILT_IN_STRATEGIES);
|