@mark1russell7/highways 0.0.0-stage → 0.1.1

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/src/geom.ts ADDED
@@ -0,0 +1,177 @@
1
+ /** A point in the plane. */
2
+ export interface Point {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ }
6
+
7
+ /** A box with its top-left corner at (`x`, `y`). */
8
+ export interface Box {
9
+ readonly x: number;
10
+ readonly y: number;
11
+ readonly w: number;
12
+ readonly h: number;
13
+ }
14
+
15
+ /** This function gives the distance between two points. */
16
+ export function dist(a: Point, b: Point): number {
17
+ const dx = a.x - b.x;
18
+ const dy = a.y - b.y;
19
+ return Math.sqrt(dx * dx + dy * dy);
20
+ }
21
+
22
+ /** This function gives the center of a box. */
23
+ export function center(b: Box): Point {
24
+ return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
25
+ }
26
+
27
+ /** This function gives a box that is larger by `m` on each side. A negative `m` makes the box smaller. */
28
+ export function inflate(b: Box, m: number): Box {
29
+ return { x: b.x - m, y: b.y - m, w: Math.max(0, b.w + 2 * m), h: Math.max(0, b.h + 2 * m) };
30
+ }
31
+
32
+ /** This function gives the sum of the lengths of the segments of a polyline. */
33
+ export function polylineLength(p: readonly Point[]): number {
34
+ let s = 0;
35
+ for (let i = 1; i < p.length; i++) s += dist(p[i - 1]!, p[i]!);
36
+ return s;
37
+ }
38
+
39
+ const orient = (a: Point, b: Point, c: Point): number => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
40
+
41
+ /**
42
+ * This function tells if the segments `ab` and `cd` cross at one point inside both of them.
43
+ * Segments that only touch at an end, or that are collinear, do not cross.
44
+ */
45
+ export function segmentsCross(a: Point, b: Point, c: Point, d: Point): boolean {
46
+ const d1 = orient(c, d, a);
47
+ const d2 = orient(c, d, b);
48
+ const d3 = orient(a, b, c);
49
+ const d4 = orient(a, b, d);
50
+ return d1 * d2 < 0 && d3 * d4 < 0;
51
+ }
52
+
53
+ /**
54
+ * This function tells if the segment `ab` goes through the inside of the box.
55
+ * The function clips the segment to the box (Liang-Barsky) and asks for a part longer than `eps`.
56
+ */
57
+ export function segmentHitsBox(a: Point, b: Point, box: Box, eps = 1e-6): boolean {
58
+ return segmentHitsRect(a, b, box.x, box.y, box.x + box.w, box.y + box.h, eps);
59
+ }
60
+
61
+ /** This function tells if the segment `ab` goes through the inside of the box from (`x0`, `y0`) to (`x1`, `y1`). */
62
+ export function segmentHitsRect(a: Point, b: Point, x0: number, y0: number, x1: number, y1: number, eps = 1e-6): boolean {
63
+ const dx = b.x - a.x;
64
+ const dy = b.y - a.y;
65
+ let t0 = 0;
66
+ let t1 = 1;
67
+ if (dx === 0) {
68
+ if (a.x <= x0 || a.x >= x1) return false;
69
+ } else {
70
+ const ta = (x0 - a.x) / dx;
71
+ const tb = (x1 - a.x) / dx;
72
+ t0 = Math.max(t0, Math.min(ta, tb));
73
+ t1 = Math.min(t1, Math.max(ta, tb));
74
+ if (t0 > t1) return false;
75
+ }
76
+ if (dy === 0) {
77
+ if (a.y <= y0 || a.y >= y1) return false;
78
+ } else {
79
+ const ta = (y0 - a.y) / dy;
80
+ const tb = (y1 - a.y) / dy;
81
+ t0 = Math.max(t0, Math.min(ta, tb));
82
+ t1 = Math.min(t1, Math.max(ta, tb));
83
+ if (t0 > t1) return false;
84
+ }
85
+ return (t1 - t0) * Math.sqrt(dx * dx + dy * dy) > eps;
86
+ }
87
+
88
+ /** This function gives the distance from a point to a box. The distance is 0 when the point is in the box. */
89
+ export function pointBoxDistance(p: Point, b: Box): number {
90
+ const qx = Math.max(b.x, Math.min(b.x + b.w, p.x));
91
+ const qy = Math.max(b.y, Math.min(b.y + b.h, p.y));
92
+ return Math.hypot(p.x - qx, p.y - qy);
93
+ }
94
+
95
+ /** This function gives the distance from a point to the segment `ab`. */
96
+ export function pointSegmentDistance(p: Point, a: Point, b: Point): number {
97
+ const dx = b.x - a.x;
98
+ const dy = b.y - a.y;
99
+ const l2 = dx * dx + dy * dy;
100
+ const t = l2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2));
101
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
102
+ }
103
+
104
+ /** This function gives the distance from the segment `ab` to a box. The distance is 0 when they touch. */
105
+ export function segmentBoxDistance(a: Point, b: Point, box: Box): number {
106
+ if (segmentHitsBox(a, b, box, 0) || pointBoxDistance(a, box) === 0 || pointBoxDistance(b, box) === 0) return 0;
107
+ const corners: Point[] = [
108
+ { x: box.x, y: box.y },
109
+ { x: box.x + box.w, y: box.y },
110
+ { x: box.x, y: box.y + box.h },
111
+ { x: box.x + box.w, y: box.y + box.h },
112
+ ];
113
+ let d = Math.min(pointBoxDistance(a, box), pointBoxDistance(b, box));
114
+ for (const c of corners) d = Math.min(d, pointSegmentDistance(c, a, b));
115
+ return d;
116
+ }
117
+
118
+ /**
119
+ * This function gives the turn at each inner vertex of a polyline, in radians, from -π to π.
120
+ * A positive turn is counterclockwise in a frame with the y axis up. The function skips segments shorter than `eps`.
121
+ */
122
+ export function turns(p: readonly Point[], eps = 1e-9): number[] {
123
+ const q: Point[] = [];
124
+ for (const v of p) if (q.length === 0 || dist(q[q.length - 1]!, v) > eps) q.push(v);
125
+ const out: number[] = [];
126
+ for (let i = 1; i + 1 < q.length; i++) {
127
+ const a = q[i - 1]!;
128
+ const b = q[i]!;
129
+ const c = q[i + 1]!;
130
+ const cr = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
131
+ const dt = (b.x - a.x) * (c.x - b.x) + (b.y - a.y) * (c.y - b.y);
132
+ out.push(Math.atan2(cr, dt));
133
+ }
134
+ return out;
135
+ }
136
+
137
+ /** This function counts the vertices of a polyline where the direction changes by more than `minTurn` radians. */
138
+ export function bendCount(p: readonly Point[], minTurn = 0.05): number {
139
+ return turns(p).filter((t) => Math.abs(t) > minTurn).length;
140
+ }
141
+
142
+ /**
143
+ * This function counts the inflections of a polyline: the changes of the sign of the turn.
144
+ * A turn smaller than `minTurn` radians does not count. Thus a straight part between two turns keeps their signs.
145
+ */
146
+ export function inflectionCount(p: readonly Point[], minTurn = 1e-3): number {
147
+ let n = 0;
148
+ let last = 0;
149
+ for (const t of turns(p)) {
150
+ if (Math.abs(t) <= minTurn) continue;
151
+ const s = Math.sign(t);
152
+ if (last !== 0 && s !== last) n++;
153
+ last = s;
154
+ }
155
+ return n;
156
+ }
157
+
158
+ /**
159
+ * This function gives the discrete Fréchet distance between two polylines (Eiter and Mannila, 1994).
160
+ * The cost is O(|p|·|q|) in time and O(|q|) in memory.
161
+ */
162
+ export function frechet(p: readonly Point[], q: readonly Point[]): number {
163
+ if (p.length === 0 || q.length === 0) return p.length === q.length ? 0 : Infinity;
164
+ let prev = new Float64Array(q.length);
165
+ let cur = new Float64Array(q.length);
166
+ for (let i = 0; i < p.length; i++) {
167
+ for (let j = 0; j < q.length; j++) {
168
+ const d = dist(p[i]!, q[j]!);
169
+ if (i === 0 && j === 0) cur[j] = d;
170
+ else if (i === 0) cur[j] = Math.max(cur[j - 1]!, d);
171
+ else if (j === 0) cur[j] = Math.max(prev[j]!, d);
172
+ else cur[j] = Math.max(Math.min(prev[j]!, prev[j - 1]!, cur[j - 1]!), d);
173
+ }
174
+ [prev, cur] = [cur, prev];
175
+ }
176
+ return prev[q.length - 1]!;
177
+ }
package/src/grid.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { Box, Point } from "./geom.ts";
2
+
3
+ /**
4
+ * A uniform grid of buckets over the plane. A box item goes into each bucket that the box touches.
5
+ * A segment item goes only into the buckets along the segment, so a long diagonal segment stays cheap.
6
+ * A query gives each item in the buckets that it touches, one time each.
7
+ */
8
+ export class Grid<T> {
9
+ readonly #cell: number;
10
+ readonly #buckets = new Map<number, number[]>();
11
+ readonly #items: T[] = [];
12
+ readonly #ids = new Map<T, number>();
13
+ // The stamp of the last query that gave each item, by the index of the item.
14
+ #seen = new Int32Array(16);
15
+ #stamp = 0;
16
+
17
+ /** This constructor makes an empty grid with square buckets of side `cell`. */
18
+ constructor(cell: number) {
19
+ this.#cell = Math.max(1e-9, cell);
20
+ }
21
+
22
+ #key(i: number, j: number): number {
23
+ // The key packs the two bucket indices into one number. Each index stays within ±2^20.
24
+ return (i + 1_048_576) * 2_097_152 + (j + 1_048_576);
25
+ }
26
+
27
+ #id(item: T): number {
28
+ let id = this.#ids.get(item);
29
+ if (id === undefined) {
30
+ id = this.#items.length;
31
+ this.#ids.set(item, id);
32
+ this.#items.push(item);
33
+ if (id >= this.#seen.length) {
34
+ const seen = new Int32Array(this.#seen.length * 2);
35
+ seen.set(this.#seen);
36
+ this.#seen = seen;
37
+ }
38
+ }
39
+ return id;
40
+ }
41
+
42
+ #put(id: number, x0: number, y0: number, x1: number, y1: number): void {
43
+ const c = this.#cell;
44
+ const i1 = Math.floor(x1 / c);
45
+ const j1 = Math.floor(y1 / c);
46
+ for (let i = Math.floor(x0 / c); i <= i1; i++) {
47
+ for (let j = Math.floor(y0 / c); j <= j1; j++) {
48
+ const key = this.#key(i, j);
49
+ const list = this.#buckets.get(key);
50
+ if (list === undefined) this.#buckets.set(key, [id]);
51
+ else if (list[list.length - 1] !== id) list.push(id);
52
+ }
53
+ }
54
+ }
55
+
56
+ #take(stamp: number, x0: number, y0: number, x1: number, y1: number, visit: (item: T) => void): void {
57
+ const c = this.#cell;
58
+ const i1 = Math.floor(x1 / c);
59
+ const j1 = Math.floor(y1 / c);
60
+ const seen = this.#seen;
61
+ for (let i = Math.floor(x0 / c); i <= i1; i++) {
62
+ for (let j = Math.floor(y0 / c); j <= j1; j++) {
63
+ const list = this.#buckets.get(this.#key(i, j));
64
+ if (list === undefined) continue;
65
+ for (const id of list) {
66
+ if (seen[id] === stamp) continue;
67
+ seen[id] = stamp;
68
+ visit(this.#items[id]!);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // The buckets along a segment, widened by `margin`: the boxes of steps of at most half a bucket.
75
+ #segment(a: Point, b: Point, margin: number, each: (x0: number, y0: number, x1: number, y1: number) => void): void {
76
+ const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
77
+ let px = a.x;
78
+ let py = a.y;
79
+ for (let k = 1; k <= n; k++) {
80
+ const qx = k === n ? b.x : a.x + ((b.x - a.x) * k) / n;
81
+ const qy = k === n ? b.y : a.y + ((b.y - a.y) * k) / n;
82
+ each(Math.min(px, qx) - margin, Math.min(py, qy) - margin, Math.max(px, qx) + margin, Math.max(py, qy) + margin);
83
+ px = qx;
84
+ py = qy;
85
+ }
86
+ }
87
+
88
+ /** This method adds an item with its bounding box. */
89
+ add(item: T, b: Box): void {
90
+ this.#put(this.#id(item), b.x, b.y, b.x + b.w, b.y + b.h);
91
+ }
92
+
93
+ /** This method adds the segment `ab` as an item. */
94
+ addSegment(item: T, a: Point, b: Point): void {
95
+ const id = this.#id(item);
96
+ this.#segment(a, b, 0, (x0, y0, x1, y1) => this.#put(id, x0, y0, x1, y1));
97
+ }
98
+
99
+ /** This method gives each item whose buckets touch the box to `visit`, one time each. */
100
+ query(b: Box, visit: (item: T) => void): void {
101
+ this.#take(++this.#stamp, b.x, b.y, b.x + b.w, b.y + b.h, visit);
102
+ }
103
+
104
+ /** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
105
+ querySegment(a: Point, b: Point, margin: number, visit: (item: T) => void): void {
106
+ const stamp = ++this.#stamp;
107
+ this.#segment(a, b, margin, (x0, y0, x1, y1) => this.#take(stamp, x0, y0, x1, y1, visit));
108
+ }
109
+ }
110
+
111
+ /** This function gives the bounding box of the segment `ab`. */
112
+ export function segmentBounds(a: Point, b: Point): Box {
113
+ const x = Math.min(a.x, b.x);
114
+ const y = Math.min(a.y, b.y);
115
+ return { x, y, w: Math.abs(a.x - b.x), h: Math.abs(a.y - b.y) };
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./geom.ts";
2
+ export { Grid, segmentBounds } from "./grid.ts";
3
+ export * from "./model.ts";
4
+ export * from "./fixtures.ts";
5
+ export * from "./plugs.ts";
6
+ export * from "./curves.ts";
7
+ export * from "./visibility.ts";
8
+ export * from "./ways.ts";
9
+ export * from "./segments.ts";
10
+ export * from "./sections.ts";
11
+ export * from "./routers/index.ts";
12
+ export * from "./bench/properties.ts";
13
+ export * from "./bench/quality.ts";
14
+ export * from "./bench/run.ts";
package/src/model.ts ADDED
@@ -0,0 +1,138 @@
1
+ import type { Box, Point } from "./geom.ts";
2
+
3
+ /** A side of a node box. */
4
+ export type Side = "north" | "east" | "south" | "west";
5
+
6
+ /**
7
+ * A plug of a node: a place where edges go into the node.
8
+ * A plug with `at` is fixed. The router does not move it. A plug without `at` is free: the router places it.
9
+ */
10
+ export interface Port {
11
+ readonly id: string;
12
+ /** This point is the fixed place of the plug, relative to the top-left corner of the node box. */
13
+ readonly at?: Point;
14
+ /** This side is where a free plug goes. Without it, the router picks the side. */
15
+ readonly side?: Side;
16
+ }
17
+
18
+ /** A node: a box that routes must not go through. */
19
+ export interface Node {
20
+ readonly id: string;
21
+ readonly box: Box;
22
+ readonly ports?: readonly Port[];
23
+ }
24
+
25
+ /** One end of an edge: a node, and optionally one plug of that node. */
26
+ export interface End {
27
+ readonly node: string;
28
+ readonly port?: string;
29
+ }
30
+
31
+ /** An edge between two nodes. `data` is free for the grouping rules. */
32
+ export interface Edge {
33
+ readonly id: string;
34
+ readonly source: End;
35
+ readonly target: End;
36
+ readonly data?: Readonly<Record<string, unknown>>;
37
+ }
38
+
39
+ /** The input of a router. */
40
+ export interface Graph {
41
+ readonly nodes: readonly Node[];
42
+ readonly edges: readonly Edge[];
43
+ }
44
+
45
+ /** The route of one edge: a polyline from the source plug to the target plug, in absolute coordinates. */
46
+ export interface Route {
47
+ readonly edge: string;
48
+ readonly points: readonly Point[];
49
+ }
50
+
51
+ /**
52
+ * One zoom level of a result. Each shown item is a node or a group of nodes.
53
+ * Each link joins two shown items and carries the count of the edges that it stands for.
54
+ */
55
+ export interface Level {
56
+ readonly depth: number;
57
+ readonly items: readonly LevelItem[];
58
+ readonly links: readonly LevelLink[];
59
+ }
60
+
61
+ /** A shown item of a level: one node, or a group that stands for its nodes. */
62
+ export interface LevelItem {
63
+ readonly id: string;
64
+ readonly box: Box;
65
+ readonly nodes: readonly string[];
66
+ }
67
+
68
+ /** A link of a level: the edges between two shown items, drawn as one path. */
69
+ export interface LevelLink {
70
+ readonly source: string;
71
+ readonly target: string;
72
+ readonly count: number;
73
+ readonly points: readonly Point[];
74
+ }
75
+
76
+ /** The output of a router. `levels` is present when the router makes zoom levels. */
77
+ export interface Result {
78
+ readonly routes: readonly Route[];
79
+ readonly levels?: readonly Level[];
80
+ readonly bundles?: readonly Bundle[];
81
+ /** The stretches of way that several routes share, if the router gives them. */
82
+ readonly runs?: readonly SharedRun[];
83
+ }
84
+
85
+ /**
86
+ * A bundle: edges from one node that share one plug and one trunk. Then the edges fan out to their targets.
87
+ * A renderer draws a bundle as one fat edge with a count, or as one lane for each edge.
88
+ */
89
+ export interface Bundle {
90
+ readonly id: string;
91
+ /** The key that the grouping rule gave each edge of the bundle. */
92
+ readonly key: string;
93
+ /** The node where the edges share their plug. */
94
+ readonly node: string;
95
+ /** The edges in lane order. Lane 0 turns most to the left of the trunk, in a frame with the y axis down. */
96
+ readonly edges: readonly string[];
97
+ /** The shared start of each route of the bundle, from the plug to the split point. */
98
+ readonly trunk: readonly Point[];
99
+ }
100
+
101
+ /**
102
+ * A stretch of way that several routes share, with the same points. Each route of `edges` has `points` in it,
103
+ * in this order or in the reverse order. A renderer draws a run as one fat line, or as one lane for each edge.
104
+ */
105
+ export interface SharedRun {
106
+ readonly id: string;
107
+ /** The key of the grouping rule of the edges. */
108
+ readonly key: string;
109
+ readonly points: readonly Point[];
110
+ /** The edges in lane order. Lane 0 is on the left of the run, from its first point to its last. The y axis points down. */
111
+ readonly edges: readonly string[];
112
+ }
113
+
114
+ /** A grouping rule gives each edge a key. The edges from one plug with one key make a bundle. An edge without a key stays alone. */
115
+ export type GroupRule = (edge: Edge) => string | undefined;
116
+
117
+ /** A router: a strategy with a name. The same input must give the same result. */
118
+ export interface Router {
119
+ readonly name: string;
120
+ /** The benchmark skips a fixture with more nodes than this. Without it, the router takes each fixture. */
121
+ readonly maxNodes?: number;
122
+ route(graph: Graph): Result;
123
+ }
124
+
125
+ /** This function gives a map from the id of each node to the node. */
126
+ export function nodeIndex(g: Graph): Map<string, Node> {
127
+ const m = new Map<string, Node>();
128
+ for (const n of g.nodes) m.set(n.id, n);
129
+ return m;
130
+ }
131
+
132
+ /** This function gives the absolute place of a fixed plug, or `undefined` for a free plug or no plug. */
133
+ export function fixedPortPoint(node: Node, end: End): Point | undefined {
134
+ if (end.port === undefined) return undefined;
135
+ const p = node.ports?.find((q) => q.id === end.port);
136
+ if (p?.at === undefined) return undefined;
137
+ return { x: node.box.x + p.at.x, y: node.box.y + p.at.y };
138
+ }
package/src/plugs.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { center, type Box, type Point } from "./geom.ts";
2
+ import { fixedPortPoint, type End, type Node, type Side } from "./model.ts";
3
+
4
+ /** A plug: the point where a route leaves or enters a node, and the outward normal of its side. */
5
+ export interface Plug {
6
+ readonly point: Point;
7
+ readonly normal: Point;
8
+ }
9
+
10
+ /** The outward normal of each side, in a frame with the y axis down. */
11
+ export const SIDE_NORMAL: Readonly<Record<Side, Point>> = {
12
+ north: { x: 0, y: -1 },
13
+ east: { x: 1, y: 0 },
14
+ south: { x: 0, y: 1 },
15
+ west: { x: -1, y: 0 },
16
+ };
17
+
18
+ /** This function gives the side of the box that is nearest to the point. */
19
+ export function nearestSide(box: Box, p: Point): Side {
20
+ const d: [Side, number][] = [
21
+ ["north", Math.abs(p.y - box.y)],
22
+ ["east", Math.abs(box.x + box.w - p.x)],
23
+ ["south", Math.abs(box.y + box.h - p.y)],
24
+ ["west", Math.abs(p.x - box.x)],
25
+ ];
26
+ let best = d[0]!;
27
+ for (const x of d) if (x[1] < best[1]) best = x;
28
+ return best[0];
29
+ }
30
+
31
+ /**
32
+ * This function gives the side that faces `toward`: the side of the larger component of the direction from the center.
33
+ * Thus the angle between the normal of the side and that direction is 45° or less.
34
+ */
35
+ export function sideToward(box: Box, toward: Point): Side {
36
+ const c = center(box);
37
+ const dx = toward.x - c.x;
38
+ const dy = toward.y - c.y;
39
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "east" : "west";
40
+ return dy >= 0 ? "south" : "north";
41
+ }
42
+
43
+ /**
44
+ * This function gives the point on a side of the box that is nearest to the line from the center to `toward`.
45
+ * The point stays away from the corners by `inset`, or by a quarter of the side when the side is short.
46
+ */
47
+ export function pointOnSide(box: Box, side: Side, toward: Point, inset = 6): Point {
48
+ const c = center(box);
49
+ const dx = toward.x - c.x;
50
+ const dy = toward.y - c.y;
51
+ const clamp = (v: number, lo: number, len: number): number => {
52
+ const m = Math.min(inset, len / 4);
53
+ return Math.max(lo + m, Math.min(lo + len - m, v));
54
+ };
55
+ if (side === "east" || side === "west") {
56
+ const x = side === "east" ? box.x + box.w : box.x;
57
+ const y = dx === 0 ? c.y : c.y + (dy * (x - c.x)) / dx;
58
+ return { x, y: clamp(y, box.y, box.h) };
59
+ }
60
+ const y = side === "south" ? box.y + box.h : box.y;
61
+ const x = dy === 0 ? c.x : c.x + (dx * (y - c.y)) / dy;
62
+ return { x: clamp(x, box.x, box.w), y };
63
+ }
64
+
65
+ /**
66
+ * This function places the plug of one end of an edge. A fixed plug stays at its point. A plug with a side stays on that side.
67
+ * The router places a free plug on the side that faces `toward`.
68
+ */
69
+ export function placePlug(node: Node, end: End, toward: Point): Plug {
70
+ const fixed = fixedPortPoint(node, end);
71
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
72
+ if (fixed) return { point: fixed, normal: SIDE_NORMAL[port?.side ?? nearestSide(node.box, fixed)] };
73
+ const side = port?.side ?? sideToward(node.box, toward);
74
+ return { point: pointOnSide(node.box, side, toward), normal: SIDE_NORMAL[side] };
75
+ }
@@ -0,0 +1,38 @@
1
+ import { center } from "../geom.ts";
2
+ import { nodeIndex, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
+ import { placePlug } from "../plugs.ts";
4
+ import { WayFinder, type WayOptions } from "../ways.ts";
5
+
6
+ /** The options of the corner router. */
7
+ export interface CornersOptions extends WayOptions {
8
+ /** The name in the benchmark. */
9
+ readonly name: string;
10
+ /** The benchmark skips a fixture with more nodes. */
11
+ readonly maxNodes?: number;
12
+ }
13
+
14
+ /**
15
+ * This function makes a router that goes around the nodes.
16
+ * Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
17
+ * of the box corners, without the needless corners. It enters the target plug along its normal.
18
+ */
19
+ export function cornersRouter(options: CornersOptions): Router {
20
+ const router: Router = {
21
+ name: options.name,
22
+ route(graph: Graph): Result {
23
+ const byId = nodeIndex(graph);
24
+ const ways = new WayFinder(graph.nodes, options);
25
+ const routes: Route[] = [];
26
+ for (const e of graph.edges) {
27
+ const s = byId.get(e.source.node);
28
+ const t = byId.get(e.target.node);
29
+ if (!s || !t) continue;
30
+ const ps = placePlug(s, e.source, center(t.box));
31
+ const pt = placePlug(t, e.target, center(s.box));
32
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
33
+ }
34
+ return { routes };
35
+ },
36
+ };
37
+ return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
38
+ }
@@ -0,0 +1,27 @@
1
+ import type { Router } from "../model.ts";
2
+ import { cornersRouter } from "./corners.ts";
3
+ import { plugsRouter } from "./plugs.ts";
4
+ import { sectionsRouter } from "./sections.ts";
5
+ import { sharedRouter } from "./shared.ts";
6
+ import { straightRouter } from "./straight.ts";
7
+ import { byCollection, trunksRouter } from "./trunks.ts";
8
+
9
+ export { boxExit, straightRouter } from "./straight.ts";
10
+ export { plugsRouter } from "./plugs.ts";
11
+ export { cornersRouter, type CornersOptions } from "./corners.ts";
12
+ export { sharedRouter, type SharedOptions } from "./shared.ts";
13
+ export { sectionsRouter, type SectionsOptions } from "./sections.ts";
14
+ export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunksOptions } from "./trunks.ts";
15
+
16
+ /** The routers that the benchmark races. */
17
+ export const ROUTERS: readonly Router[] = [
18
+ straightRouter,
19
+ plugsRouter,
20
+ cornersRouter({ name: "corners", maxNodes: 10_000 }),
21
+ cornersRouter({ name: "corners-smooth", smooth: true, maxNodes: 10_000 }),
22
+ trunksRouter({ name: "trunks", rule: byCollection, maxNodes: 10_000 }),
23
+ sharedRouter({ name: "shared", rule: byCollection, maxNodes: 10_000 }),
24
+ sharedRouter({ name: "shared-all", maxNodes: 10_000 }),
25
+ sectionsRouter({ name: "sections", maxNodes: 100_000 }),
26
+ sectionsRouter({ name: "sections-tree", stretch: Infinity, maxNodes: 100_000 }),
27
+ ];
@@ -0,0 +1,25 @@
1
+ import { inflectionCurve } from "../curves.ts";
2
+ import { center } from "../geom.ts";
3
+ import { nodeIndex, type Graph, type Result, type Route, type Router } from "../model.ts";
4
+ import { placePlug } from "../plugs.ts";
5
+
6
+ /**
7
+ * Plugs and the inflection rule, with no obstacles. Each edge leaves its source plug at the tangent of the side.
8
+ * It goes to the target plug on the curve of the rule. This router measures the rule alone.
9
+ */
10
+ export const plugsRouter: Router = {
11
+ name: "plugs",
12
+ route(graph: Graph): Result {
13
+ const byId = nodeIndex(graph);
14
+ const routes: Route[] = [];
15
+ for (const e of graph.edges) {
16
+ const s = byId.get(e.source.node);
17
+ const t = byId.get(e.target.node);
18
+ if (!s || !t) continue;
19
+ const ps = placePlug(s, e.source, center(t.box));
20
+ const pt = placePlug(t, e.target, center(s.box));
21
+ routes.push({ edge: e.id, points: inflectionCurve(ps.point, ps.normal, pt.point) });
22
+ }
23
+ return { routes };
24
+ },
25
+ };