@mark1russell7/highways 0.1.3 → 0.1.5

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.
@@ -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
+ }
@@ -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 that the benchmark races. */
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,
@@ -0,0 +1,169 @@
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 type { Graph, Result, Router } from "../model.ts";
6
+ import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
7
+ import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
8
+ import { plugsRouter } from "../routers/plugs.ts";
9
+ import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
10
+ import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
11
+ import { straightRouter } from "../routers/straight.ts";
12
+ import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
13
+ import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type Registry, type Strategy } from "./model.ts";
14
+
15
+ /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
16
+ export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
17
+
18
+ /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
19
+ function routerStrategy<O extends object>(
20
+ name: string,
21
+ describe: string,
22
+ maxNodes: number,
23
+ defaults: O,
24
+ make: (options: O & { readonly name: string }) => Router,
25
+ ): Strategy<"router", O> {
26
+ return defineStrategy({ step: "router", name, describe, defaults, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
27
+ }
28
+
29
+ /** This function makes a router strategy for a router without options. */
30
+ 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) });
32
+ }
33
+
34
+ /** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
35
+ export const routerStrategies: {
36
+ readonly straight: Strategy<"router", object>;
37
+ readonly plugs: Strategy<"router", object>;
38
+ readonly corners: Strategy<"router", RouterPreset<CornersOptions>>;
39
+ readonly "corners-smooth": Strategy<"router", RouterPreset<CornersOptions>>;
40
+ readonly trunks: Strategy<"router", RouterPreset<TrunksOptions>>;
41
+ readonly shared: Strategy<"router", RouterPreset<SharedOptions>>;
42
+ readonly "shared-all": Strategy<"router", RouterPreset<SharedOptions>>;
43
+ readonly sections: Strategy<"router", RouterPreset<SectionsOptions>>;
44
+ readonly "sections-tree": Strategy<"router", RouterPreset<SectionsOptions>>;
45
+ } = Object.freeze({
46
+ straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
47
+ plugs: fixedRouter(
48
+ plugsRouter,
49
+ "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.",
50
+ ),
51
+ corners: routerStrategy<RouterPreset<CornersOptions>>(
52
+ "corners",
53
+ "Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.",
54
+ 10_000,
55
+ {},
56
+ cornersRouter,
57
+ ),
58
+ "corners-smooth": routerStrategy<RouterPreset<CornersOptions>>(
59
+ "corners-smooth",
60
+ "The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.",
61
+ 10_000,
62
+ { smooth: true },
63
+ cornersRouter,
64
+ ),
65
+ trunks: routerStrategy<RouterPreset<TrunksOptions>>(
66
+ "trunks",
67
+ "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.",
68
+ 10_000,
69
+ { rule: byCollection },
70
+ trunksRouter,
71
+ ),
72
+ shared: routerStrategy<RouterPreset<SharedOptions>>(
73
+ "shared",
74
+ "The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.",
75
+ 10_000,
76
+ { rule: byCollection },
77
+ sharedRouter,
78
+ ),
79
+ "shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter),
80
+ sections: routerStrategy<RouterPreset<SectionsOptions>>(
81
+ "sections",
82
+ "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
83
+ 100_000,
84
+ {},
85
+ sectionsRouter,
86
+ ),
87
+ "sections-tree": routerStrategy<RouterPreset<SectionsOptions>>(
88
+ "sections-tree",
89
+ "The same as `sections`, but without the limit of the stretch.",
90
+ 100_000,
91
+ { stretch: Infinity },
92
+ sectionsRouter,
93
+ ),
94
+ });
95
+
96
+ /** The context of a curve input, without the keys that have no value. */
97
+ const contextOf = (input: CurveInput): Omit<CurveInput, "points"> => ({
98
+ ...(input.startNormal ? { startNormal: input.startNormal } : {}),
99
+ ...(input.endNormal ? { endNormal: input.endNormal } : {}),
100
+ ...(input.space ? { space: input.space } : {}),
101
+ });
102
+
103
+ /** The options of the curve strategies `rounded` and `arc`. */
104
+ export interface RadiusOptions {
105
+ /** The radius of the corners. The default is 12. */
106
+ readonly radius: number;
107
+ }
108
+
109
+ /** The options of the curve strategy `spline`. */
110
+ export interface TensionOptions {
111
+ /** The tension of the spline, from 0 (the spline) to 1 (the polyline). The default is 0. */
112
+ readonly tension: number;
113
+ }
114
+
115
+ /** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
116
+ export const curveStrategies: {
117
+ readonly straight: Strategy<"curve", object>;
118
+ readonly rounded: Strategy<"curve", RadiusOptions>;
119
+ readonly spline: Strategy<"curve", TensionOptions>;
120
+ readonly arc: Strategy<"curve", RadiusOptions>;
121
+ } = Object.freeze({
122
+ straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, run: (input: CurveInput) => straightCurve(input.points) }),
123
+ rounded: defineStrategy({
124
+ step: "curve",
125
+ name: "rounded",
126
+ describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
127
+ defaults: { radius: 12 },
128
+ run: (input: CurveInput, o: Readonly<RadiusOptions>) => roundedCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies RoundedOptions),
129
+ }),
130
+ spline: defineStrategy({
131
+ step: "curve",
132
+ name: "spline",
133
+ describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
134
+ defaults: { tension: 0 },
135
+ run: (input: CurveInput, o: Readonly<TensionOptions>) => splineCurve(input.points, { ...contextOf(input), tension: o.tension } satisfies SplineOptions),
136
+ }),
137
+ arc: defineStrategy({
138
+ step: "curve",
139
+ name: "arc",
140
+ describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
141
+ defaults: { radius: 12 },
142
+ run: (input: CurveInput, o: Readonly<RadiusOptions>) => arcCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies ArcOptions),
143
+ }),
144
+ });
145
+
146
+ /** The options of a room strategy: the options of `roomDemand`, without the method. */
147
+ export type RoomPreset = Omit<RoomOptions, "method">;
148
+
149
+ 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 }) });
151
+
152
+ /** The built-in room strategies: the methods of `roomDemand`. */
153
+ export const roomStrategies: {
154
+ readonly routes: Strategy<"room", RoomPreset>;
155
+ readonly lines: Strategy<"room", RoomPreset>;
156
+ } = Object.freeze({
157
+ 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
+ lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap."),
159
+ });
160
+
161
+ /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
162
+ export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
163
+ ...Object.values(routerStrategies),
164
+ ...Object.values(curveStrategies),
165
+ ...Object.values(roomStrategies),
166
+ ]);
167
+
168
+ /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
169
+ export const STRATEGIES: Registry = createRegistry(BUILT_IN_STRATEGIES);
@@ -0,0 +1,2 @@
1
+ export * from "./model.ts";
2
+ export * from "./builtins.ts";
@@ -0,0 +1,157 @@
1
+ import type { CurveSegment } from "../curves/model.ts";
2
+ import type { CurveSpace } from "../curves/space.ts";
3
+ import type { Point } from "../geom.ts";
4
+ import type { Graph, Result, Router } from "../model.ts";
5
+ import type { Boxed, Gap, RoomLink } from "../room/index.ts";
6
+
7
+ /** The input of a curve strategy: one polyline and the context of its route. */
8
+ export interface CurveInput {
9
+ /** These points are the polyline: a route of a router, a lane of `laneRoutes` or a link of a zoom level. */
10
+ readonly points: readonly Point[];
11
+ /** The curve leaves the first point along this outward normal of its plug, if the strategy reads it. */
12
+ readonly startNormal?: Point;
13
+ /** The curve comes into the last point against this outward normal of its plug, if the strategy reads it. */
14
+ readonly endNormal?: Point;
15
+ /** The boxes that the curve must not go through, from `curveSpace`. */
16
+ readonly space?: CurveSpace;
17
+ }
18
+
19
+ /** The input of a room strategy: the boxes of a board and the links between them. */
20
+ export interface RoomInput {
21
+ readonly items: readonly Boxed[];
22
+ readonly links: readonly RoomLink[];
23
+ }
24
+
25
+ /**
26
+ * The steps that have strategies, with the fixed input and output type of each step.
27
+ *
28
+ * - `router`: a graph in, the routes (and the `levels`, `bundles` and `runs`) out.
29
+ * - `curve`: a polyline in, the segments of a curve out.
30
+ * - `room`: the boxes and links of a board in, the gaps of the board with their need out.
31
+ */
32
+ export interface StepTypes {
33
+ readonly router: { readonly input: Graph; readonly output: Result };
34
+ readonly curve: { readonly input: CurveInput; readonly output: CurveSegment[] };
35
+ readonly room: { readonly input: RoomInput; readonly output: Gap[] };
36
+ }
37
+
38
+ /** The name of a step that has strategies. */
39
+ export type Step = keyof StepTypes;
40
+
41
+ /** The input type of a step. */
42
+ export type StepInput<S extends Step> = StepTypes[S]["input"];
43
+
44
+ /** The output type of a step. */
45
+ export type StepOutput<S extends Step> = StepTypes[S]["output"];
46
+
47
+ /** The steps that have strategies, in the order of the pipeline. */
48
+ export const STEPS: readonly Step[] = ["router", "curve", "room"];
49
+
50
+ /**
51
+ * A strategy: one way to do one step, with a name and a description.
52
+ * The same input and the same options must give the same output.
53
+ */
54
+ export interface Strategy<S extends Step = Step, O extends object = object> {
55
+ /** The step of the strategy. It fixes the input and the output type. */
56
+ readonly step: S;
57
+ /** The name of the strategy. Each name is unique in its step of a registry. */
58
+ readonly name: string;
59
+ /** One or two sentences that tell what the strategy does. The site and the documents show them. */
60
+ readonly describe: string;
61
+ /** The values of the options when the caller gives no value. */
62
+ readonly defaults: Readonly<O>;
63
+ /** The benchmark skips an input with more nodes than this. Without it, the strategy takes each input. */
64
+ readonly maxNodes?: number;
65
+ /** This method does the step. Each option that `options` does not give gets its value from `defaults`. */
66
+ run(input: StepInput<S>, options?: Partial<O>): StepOutput<S>;
67
+ }
68
+
69
+ /** A strategy of any step. A registry holds a list of these. */
70
+ export type AnyStrategy = { [S in Step]: Strategy<S> }[Step];
71
+
72
+ /** The definition of a strategy, for `defineStrategy`. */
73
+ export interface StrategyDefinition<S extends Step, O extends object> {
74
+ readonly step: S;
75
+ readonly name: string;
76
+ readonly describe: string;
77
+ /** These values are the options when the caller gives no value. Give `{}` for a strategy without options. */
78
+ readonly defaults: O;
79
+ readonly maxNodes?: number;
80
+ /** This function does the step. It gets each option, from the caller or from `defaults`. */
81
+ run(input: StepInput<S>, options: Readonly<O>): StepOutput<S>;
82
+ }
83
+
84
+ /**
85
+ * This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
86
+ * When the caller gives no option, the definition gets the `defaults` object itself.
87
+ */
88
+ export function defineStrategy<S extends Step, O extends object>(definition: StrategyDefinition<S, O>): Strategy<S, O> {
89
+ const { step, name, describe, defaults, maxNodes } = definition;
90
+ if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
91
+ const strategy: Strategy<S, O> = {
92
+ step,
93
+ name,
94
+ describe,
95
+ defaults: Object.freeze({ ...defaults }),
96
+ ...(maxNodes === undefined ? {} : { maxNodes }),
97
+ run(input: StepInput<S>, options?: Partial<O>): StepOutput<S> {
98
+ const all = options === undefined || Object.keys(options).length === 0 ? strategy.defaults : { ...strategy.defaults, ...options };
99
+ return definition.run(input, all as Readonly<O>);
100
+ },
101
+ };
102
+ return Object.freeze(strategy);
103
+ }
104
+
105
+ /**
106
+ * A registry: the strategies of each step, in order. A registry is data. No import changes it.
107
+ * A consumer makes its own registry with `createRegistry`, and adds a strategy with `register`.
108
+ */
109
+ export type Registry = { readonly [S in Step]: readonly Strategy<S>[] };
110
+
111
+ /** 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
+ export function createRegistry(strategies: readonly AnyStrategy[]): Registry {
113
+ const out: { [S in Step]: Strategy<S>[] } = { router: [], curve: [], room: [] };
114
+ for (const s of strategies) {
115
+ const list = out[s.step] as AnyStrategy[] | undefined;
116
+ if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
117
+ if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
118
+ list.push(s);
119
+ }
120
+ return Object.freeze({ router: Object.freeze(out.router), curve: Object.freeze(out.curve), room: Object.freeze(out.room) });
121
+ }
122
+
123
+ /** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
124
+ export function register(registry: Registry, ...strategies: readonly AnyStrategy[]): Registry {
125
+ return createRegistry([...registry.router, ...registry.curve, ...registry.room, ...strategies]);
126
+ }
127
+
128
+ /** This function gives the strategies of one step of a registry, in order. */
129
+ export function listStrategies<S extends Step>(registry: Registry, step: S): readonly Strategy<S>[] {
130
+ return registry[step];
131
+ }
132
+
133
+ /** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
134
+ export function findStrategy<S extends Step>(registry: Registry, step: S, name: string): Strategy<S> | undefined {
135
+ return listStrategies(registry, step).find((s) => s.name === name);
136
+ }
137
+
138
+ /** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
139
+ export function getStrategy<S extends Step>(registry: Registry, step: S, name: string): Strategy<S> {
140
+ const s = findStrategy(registry, step, name);
141
+ if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
142
+ return s;
143
+ }
144
+
145
+ /** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
146
+ export function asRouter<O extends object>(strategy: Strategy<"router", O>, options?: Partial<O>): Router {
147
+ return {
148
+ name: strategy.name,
149
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
150
+ route: (graph: Graph): Result => strategy.run(graph, options),
151
+ };
152
+ }
153
+
154
+ /** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
155
+ export function routersOf(registry: Registry): Router[] {
156
+ return listStrategies(registry, "router").map((s) => asRouter(s));
157
+ }
package/src/visibility.ts CHANGED
@@ -86,6 +86,11 @@ export interface CornerGraphOptions {
86
86
  * The lattice links groups of nodes across empty space. Without it, the nearest corners of a group are all in that group.
87
87
  */
88
88
  readonly lattice?: number;
89
+ /**
90
+ * With `cones`, each corner also links to the nearest corner that it can see in each of four cones around the axes.
91
+ * Then a path can go along a long corridor between rows or columns of boxes. The default is false.
92
+ */
93
+ readonly cones?: boolean;
89
94
  }
90
95
 
91
96
  /**
@@ -164,7 +169,8 @@ export class CornerGraph {
164
169
  }
165
170
  const links: Set<number>[] = this.points.map(() => new Set<number>());
166
171
  for (let i = 0; i < this.points.length; i++) {
167
- for (const j of this.nearestVisible(this.points[i]!, this.#k, i)) {
172
+ // The k nearest corners of a corner in a long corridor are often all on one side. The cones add a link along the corridor.
173
+ for (const j of this.#nearestVisible(this.points[i]!, this.#k, i, this.#pad, options.cones ?? false)) {
168
174
  links[i]!.add(j);
169
175
  links[j]!.add(i);
170
176
  }
@@ -221,13 +227,17 @@ export class CornerGraph {
221
227
  * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
222
228
  * The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
223
229
  * A corner of these buckets beyond `r` waits for the next ring. So the method visits each bucket one time.
230
+ * With `cones`, the method also gives the nearest visible corner of each of four cones around the axes that the `k` nearest corners leave empty.
224
231
  */
225
- #nearestVisible(p: Point, k: number, self: number, pad: number): number[] {
232
+ #nearestVisible(p: Point, k: number, self: number, pad: number, cones = false): number[] {
226
233
  const out: number[] = [];
234
+ // The bits of the full cones: 1 is +x, 2 is +y, 4 is -x and 8 is -y.
235
+ let full = 0;
227
236
  if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
228
237
  const dd = this.#dist;
229
238
  const points = this.points;
230
239
  const cell = this.#cell;
240
+ const reach = CONE_CELLS * cell;
231
241
  const blockers: Node[] = [];
232
242
  const now = this.#near;
233
243
  // The corners that the rings found beyond their radius. The next ring takes them.
@@ -238,6 +248,8 @@ export class CornerGraph {
238
248
  let pj0 = 1;
239
249
  let pj1 = 0;
240
250
  for (let r = cell; r <= cell * 8; r *= 2) {
251
+ // The ring of `r` holds the corners beyond `r / 2`. With the k nearest corners, the cones take no corner beyond `reach`.
252
+ if (out.length >= k && r / 2 >= reach) return out;
241
253
  // These bounds are the bounds of a query of the box at (p.x - r, p.y - r) with the side 2r.
242
254
  const x0 = p.x - r;
243
255
  const y0 = p.y - r;
@@ -273,6 +285,12 @@ export class CornerGraph {
273
285
  // A node that stopped an earlier segment often stops this segment too, so the test takes these nodes first.
274
286
  // The order of the nodes does not change the result.
275
287
  const q = points[j]!;
288
+ const cone = cones ? coneOf(q.x - p.x, q.y - p.y) : 0;
289
+ if (out.length >= k) {
290
+ // After the k nearest corners, only a corner of an empty cone counts, up to `CONE_CELLS` cells from `p`.
291
+ if (dd[j]! > reach) return out;
292
+ if ((full & cone) !== 0) continue;
293
+ }
276
294
  let blocked = false;
277
295
  for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b]!, pad);
278
296
  if (blocked) continue;
@@ -280,7 +298,8 @@ export class CornerGraph {
280
298
  if (hit !== undefined) blockers.push(hit);
281
299
  else {
282
300
  out.push(j);
283
- if (out.length >= k) return out;
301
+ full |= cone;
302
+ if (out.length >= k && (!cones || full === 15)) return out;
284
303
  }
285
304
  }
286
305
  wait = later;
@@ -474,6 +493,12 @@ export class CornerGraph {
474
493
  }
475
494
  }
476
495
 
496
+ /** A link of a cone is at most this number of cells long. A longer search costs time on a dense level and finds few new links. */
497
+ const CONE_CELLS = 3;
498
+
499
+ /** This function gives the bit of the cone of the offset (`dx`, `dy`): 1 is +x, 2 is +y, 4 is -x and 8 is -y. A diagonal offset is in a cone of the x axis. */
500
+ const coneOf = (dx: number, dy: number): number => (Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 4) : dy > 0 ? 2 : 8);
501
+
477
502
  /** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
478
503
  const bucketKey = (i: number, j: number): number => (i + 1_048_576) * 2_097_152 + (j + 1_048_576);
479
504