@mark1russell7/highways 0.1.1 → 0.1.3
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/build/index.d.ts +153 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1383 -374
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/links.ts +191 -0
- package/src/bench/properties.ts +27 -2
- package/src/bench/run.ts +28 -0
- package/src/cases.ts +111 -0
- package/src/grid.ts +61 -2
- package/src/index.ts +2 -0
- package/src/levels.ts +379 -0
- package/src/routers/sections.ts +50 -43
- package/src/visibility.ts +325 -86
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://mark1russell7.github.io/highways/",
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.3",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./build/index.js",
|
|
15
15
|
"types": "./build/index.d.ts",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"bench": "node scripts/bench.ts",
|
|
41
41
|
"profile": "node scripts/profile.ts",
|
|
42
|
+
"levels": "node scripts/levels.ts",
|
|
42
43
|
"test:watch": "vitest",
|
|
43
44
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
44
45
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { dist, inflate, pointSegmentDistance, polylineLength, segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
2
|
+
import { Grid } from "../grid.ts";
|
|
3
|
+
import type { Graph, Level, Result } from "../model.ts";
|
|
4
|
+
import { TOLERANCE } from "./properties.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The faults of a set of links: the routes of the edges, or the links of one zoom level.
|
|
8
|
+
* Each value counts links, except `worstDetour`. Lower is better for each value.
|
|
9
|
+
*/
|
|
10
|
+
export interface LinkFaults {
|
|
11
|
+
/** The number of links that the measure examined. */
|
|
12
|
+
readonly links: number;
|
|
13
|
+
/**
|
|
14
|
+
* The number of links that go through the inside of a box that is not one of their two ends.
|
|
15
|
+
* A segment goes through a box when a part of it longer than `TOLERANCE` is inside the box made smaller by `TOLERANCE`.
|
|
16
|
+
*/
|
|
17
|
+
readonly through: number;
|
|
18
|
+
/**
|
|
19
|
+
* The number of links that go out and back. The distance from a point of the link to its last point grows after the nearest approach.
|
|
20
|
+
* A link goes out and back when this growth is more than `BACKTRACK_SHARE` of the distance between its ends, and more than `BACKTRACK_MIN` units.
|
|
21
|
+
*/
|
|
22
|
+
readonly backtrack: number;
|
|
23
|
+
/** The largest ratio of the length of a link to the distance between its ends. Links with ends nearer than 1 unit do not count. */
|
|
24
|
+
readonly worstDetour: number;
|
|
25
|
+
/**
|
|
26
|
+
* The number of links with a point outside the bounding box of all the boxes, made larger by `slack`.
|
|
27
|
+
* Both ends of a link are always in that bounding box, so such a link goes around the outer edge of the board.
|
|
28
|
+
*/
|
|
29
|
+
readonly outside: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The faults of the links of one zoom level. */
|
|
33
|
+
export interface LevelFaults extends LinkFaults {
|
|
34
|
+
readonly depth: number;
|
|
35
|
+
/** The number of items of the level. */
|
|
36
|
+
readonly items: number;
|
|
37
|
+
/** The number of items that hold more than one node. */
|
|
38
|
+
readonly groups: number;
|
|
39
|
+
/** The number of pairs of items whose boxes overlap by more than `TOLERANCE` on each axis. */
|
|
40
|
+
readonly overlaps: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The options of the link measures. */
|
|
44
|
+
export interface LinkFaultOptions {
|
|
45
|
+
/** A link that goes farther than this distance outside the bounding box of all the boxes counts in `outside`. The default is 24. */
|
|
46
|
+
readonly slack?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A link goes out and back when its distance to its target grows by more than this share of the distance between its ends. */
|
|
50
|
+
export const BACKTRACK_SHARE = 0.25;
|
|
51
|
+
/** A link goes out and back only when its distance to its target grows by more than this number of units. */
|
|
52
|
+
export const BACKTRACK_MIN = 40;
|
|
53
|
+
/** The default `slack` of `LinkFaultOptions`. */
|
|
54
|
+
export const OUTSIDE_SLACK = 24;
|
|
55
|
+
|
|
56
|
+
/** A link from one box to another box, as `measureLinks` sees it. */
|
|
57
|
+
export interface MeasuredLink {
|
|
58
|
+
readonly source: string;
|
|
59
|
+
readonly target: string;
|
|
60
|
+
readonly points: readonly Point[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* This function gives the largest growth of the distance to the last point of a polyline.
|
|
65
|
+
* The growth at a vertex is its distance to the last point, less the nearest approach of the polyline before that vertex.
|
|
66
|
+
*/
|
|
67
|
+
export function backtrackOf(p: readonly Point[]): number {
|
|
68
|
+
if (p.length < 3) return 0;
|
|
69
|
+
const end = p[p.length - 1]!;
|
|
70
|
+
let near = dist(p[0]!, end);
|
|
71
|
+
let worst = 0;
|
|
72
|
+
for (let i = 1; i < p.length; i++) {
|
|
73
|
+
worst = Math.max(worst, dist(p[i]!, end) - near);
|
|
74
|
+
near = Math.min(near, pointSegmentDistance(end, p[i - 1]!, p[i]!));
|
|
75
|
+
}
|
|
76
|
+
return worst;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** This function tells if the polyline `p` goes out and back: `backtrackOf(p)` is more than `BACKTRACK_MIN` and more than `BACKTRACK_SHARE` of the span. */
|
|
80
|
+
export function goesOutAndBack(p: readonly Point[]): boolean {
|
|
81
|
+
if (p.length < 3) return false;
|
|
82
|
+
const span = dist(p[0]!, p[p.length - 1]!);
|
|
83
|
+
return backtrackOf(p) > Math.max(BACKTRACK_MIN, BACKTRACK_SHARE * span);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This function measures the faults of links between boxes. The map `boxes` gives the box of each end.
|
|
88
|
+
* The bounding box of `outside` is the bounding box of all the boxes of the map.
|
|
89
|
+
*/
|
|
90
|
+
export function measureLinks(boxes: ReadonlyMap<string, Box>, links: readonly MeasuredLink[], options: LinkFaultOptions = {}): LinkFaults {
|
|
91
|
+
const slack = options.slack ?? OUTSIDE_SLACK;
|
|
92
|
+
let size = 0;
|
|
93
|
+
let x0 = Infinity;
|
|
94
|
+
let y0 = Infinity;
|
|
95
|
+
let x1 = -Infinity;
|
|
96
|
+
let y1 = -Infinity;
|
|
97
|
+
for (const b of boxes.values()) {
|
|
98
|
+
size += Math.max(b.w, b.h);
|
|
99
|
+
x0 = Math.min(x0, b.x);
|
|
100
|
+
y0 = Math.min(y0, b.y);
|
|
101
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
102
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
103
|
+
}
|
|
104
|
+
// Large items cover many buckets of a small grid, so the buckets grow with the items.
|
|
105
|
+
const grid = new Grid<string>(Math.max(180, (2 * size) / Math.max(1, boxes.size)));
|
|
106
|
+
for (const [id, b] of boxes) grid.add(id, b);
|
|
107
|
+
let through = 0;
|
|
108
|
+
let backtrack = 0;
|
|
109
|
+
let worstDetour = 0;
|
|
110
|
+
let outside = 0;
|
|
111
|
+
for (const l of links) {
|
|
112
|
+
const p = l.points;
|
|
113
|
+
if (p.length === 0) continue;
|
|
114
|
+
let hit = false;
|
|
115
|
+
for (let i = 1; i < p.length && !hit; i++) {
|
|
116
|
+
const a = p[i - 1]!;
|
|
117
|
+
const b = p[i]!;
|
|
118
|
+
grid.querySegment(a, b, 0, (id) => {
|
|
119
|
+
if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id)!, -TOLERANCE), TOLERANCE)) hit = true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (hit) through++;
|
|
123
|
+
if (goesOutAndBack(p)) backtrack++;
|
|
124
|
+
const span = dist(p[0]!, p[p.length - 1]!);
|
|
125
|
+
if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
|
|
126
|
+
if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
|
|
127
|
+
}
|
|
128
|
+
return { links: links.length, through, backtrack, worstDetour, outside };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
|
|
132
|
+
export function measureRouteFaults(graph: Graph, result: Result, options: LinkFaultOptions = {}): LinkFaults {
|
|
133
|
+
const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
134
|
+
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
135
|
+
const links: MeasuredLink[] = [];
|
|
136
|
+
for (const r of result.routes) {
|
|
137
|
+
const e = edges.get(r.edge);
|
|
138
|
+
if (e) links.push({ source: e.source.node, target: e.target.node, points: r.points });
|
|
139
|
+
}
|
|
140
|
+
return measureLinks(boxes, links, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** This function counts the pairs of boxes that overlap by more than `TOLERANCE` on each axis. */
|
|
144
|
+
export function countOverlaps(boxes: readonly Box[]): number {
|
|
145
|
+
let size = 0;
|
|
146
|
+
for (const b of boxes) size += Math.max(b.w, b.h);
|
|
147
|
+
const grid = new Grid<number>(Math.max(180, (2 * size) / Math.max(1, boxes.length)));
|
|
148
|
+
boxes.forEach((b, i) => grid.add(i, b));
|
|
149
|
+
let n = 0;
|
|
150
|
+
boxes.forEach((a, i) => {
|
|
151
|
+
grid.query(a, (j) => {
|
|
152
|
+
const b = boxes[j]!;
|
|
153
|
+
if (j > i && Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > TOLERANCE && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > TOLERANCE) n++;
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return n;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** This function measures the faults of the links of one zoom level. The items of the level are the boxes. */
|
|
160
|
+
export function measureLevel(level: Level, options: LinkFaultOptions = {}): LevelFaults {
|
|
161
|
+
const boxes = new Map(level.items.map((i) => [i.id, i.box]));
|
|
162
|
+
return {
|
|
163
|
+
depth: level.depth,
|
|
164
|
+
items: level.items.length,
|
|
165
|
+
groups: level.items.filter((i) => i.nodes.length > 1).length,
|
|
166
|
+
overlaps: countOverlaps(level.items.map((i) => i.box)),
|
|
167
|
+
...measureLinks(boxes, level.links, options),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* This function measures the faults of each zoom level of a result that has groups: the levels of the far view.
|
|
173
|
+
* A level of single nodes only is the near view. Its links are routes, and `measureRouteFaults` measures them.
|
|
174
|
+
*/
|
|
175
|
+
export function measureLevelFaults(result: Result, options: LinkFaultOptions = {}): LevelFaults[] {
|
|
176
|
+
return (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1)).map((l) => measureLevel(l, options));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** This function adds the faults of several levels: it sums the counts and keeps the largest `worstDetour`. */
|
|
180
|
+
export function sumFaults(list: readonly LinkFaults[]): LinkFaults {
|
|
181
|
+
return list.reduce<LinkFaults>(
|
|
182
|
+
(s, f) => ({
|
|
183
|
+
links: s.links + f.links,
|
|
184
|
+
through: s.through + f.through,
|
|
185
|
+
backtrack: s.backtrack + f.backtrack,
|
|
186
|
+
worstDetour: Math.max(s.worstDetour, f.worstDetour),
|
|
187
|
+
outside: s.outside + f.outside,
|
|
188
|
+
}),
|
|
189
|
+
{ links: 0, through: 0, backtrack: 0, worstDetour: 0, outside: 0 },
|
|
190
|
+
);
|
|
191
|
+
}
|
package/src/bench/properties.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { dist, inflate, pointBoxDistance, segmentHitsBox, type Point } from "../geom.ts";
|
|
1
|
+
import { dist, inflate, pointBoxDistance, segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
2
2
|
import { Grid } from "../grid.ts";
|
|
3
3
|
import { fixedPortPoint, nodeIndex, type Graph, type Node, type Result } from "../model.ts";
|
|
4
4
|
|
|
5
5
|
/** A broken hard rule. A correct router has none. */
|
|
6
6
|
export interface Violation {
|
|
7
|
-
readonly rule:
|
|
7
|
+
readonly rule:
|
|
8
|
+
| "complete"
|
|
9
|
+
| "ends-at-plugs"
|
|
10
|
+
| "fixed-plugs"
|
|
11
|
+
| "no-pass-through"
|
|
12
|
+
| "counts-sum"
|
|
13
|
+
| "link-ends"
|
|
14
|
+
| "bundle-trunk"
|
|
15
|
+
| "run-shared";
|
|
8
16
|
readonly edge?: string;
|
|
9
17
|
readonly detail: string;
|
|
10
18
|
}
|
|
@@ -15,6 +23,7 @@ export const TOLERANCE = 0.5;
|
|
|
15
23
|
/**
|
|
16
24
|
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
17
25
|
* Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
|
|
26
|
+
* Each link of a level starts on the box of its source and ends on the box of its target.
|
|
18
27
|
*
|
|
19
28
|
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
20
29
|
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
|
@@ -91,10 +100,26 @@ export function checkProperties(graph: Graph, result: Result, limit = 100): Viol
|
|
|
91
100
|
const sum = level.links.reduce((s, l) => s + l.count, 0);
|
|
92
101
|
if (sum !== between)
|
|
93
102
|
if (add({ rule: "counts-sum", detail: `level ${level.depth}: the links count ${sum} edges, not ${between}` })) return out;
|
|
103
|
+
const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
|
|
104
|
+
for (const l of level.links) {
|
|
105
|
+
const s = boxOf.get(l.source);
|
|
106
|
+
const t = boxOf.get(l.target);
|
|
107
|
+
const p = l.points;
|
|
108
|
+
if (!s || !t || p.length < 2 || !onBoundary(p[0]!, s) || !onBoundary(p[p.length - 1]!, t))
|
|
109
|
+
if (add({ rule: "link-ends", detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items` }))
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
return out;
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
/** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
|
|
117
|
+
function onBoundary(p: Point, b: Box): boolean {
|
|
118
|
+
const inner = inflate(b, -TOLERANCE);
|
|
119
|
+
const deep = p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h;
|
|
120
|
+
return !deep && pointBoxDistance(p, b) <= TOLERANCE;
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
99
124
|
function holds(p: readonly Point[], part: readonly Point[]): boolean {
|
|
100
125
|
if (part.length === 0) return true;
|
package/src/bench/run.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { makeFixture, type FixtureSpec } from "../fixtures.ts";
|
|
2
2
|
import type { Graph, Result, Router } from "../model.ts";
|
|
3
|
+
import { measureLevelFaults, measureRouteFaults, sumFaults, type LevelFaults, type LinkFaults } from "./links.ts";
|
|
3
4
|
import { checkProperties, type Violation } from "./properties.ts";
|
|
4
5
|
import { measureQuality, type Quality } from "./quality.ts";
|
|
5
6
|
|
|
@@ -20,6 +21,10 @@ export interface BenchRow {
|
|
|
20
21
|
/** The first violations, for the report. */
|
|
21
22
|
readonly sample: readonly Violation[];
|
|
22
23
|
readonly quality: Quality;
|
|
24
|
+
/** The faults of the routes of the edges (`measureRouteFaults`). */
|
|
25
|
+
readonly faults: LinkFaults;
|
|
26
|
+
/** The faults of each zoom level with groups (`measureLevelFaults`). The list is empty for a router without levels. */
|
|
27
|
+
readonly levelFaults: readonly LevelFaults[];
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/** The options of a benchmark run. */
|
|
@@ -87,6 +92,8 @@ export function benchOne(router: Router, fixture: string, graph: Graph, repeat =
|
|
|
87
92
|
violations: all.length,
|
|
88
93
|
sample: all.slice(0, 5),
|
|
89
94
|
quality: measureQuality(graph, result),
|
|
95
|
+
faults: measureRouteFaults(graph, result),
|
|
96
|
+
levelFaults: measureLevelFaults(result),
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
@@ -101,3 +108,24 @@ export function formatTable(rows: readonly BenchRow[]): string {
|
|
|
101
108
|
});
|
|
102
109
|
return [head, line, ...body].join("\n");
|
|
103
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This function writes the faults of the rows as a Markdown table: the routes first, then the sum of the levels with groups.
|
|
114
|
+
* A router without such levels gets a dash in the columns of the levels.
|
|
115
|
+
*/
|
|
116
|
+
export function formatFaults(rows: readonly BenchRow[]): string {
|
|
117
|
+
const head =
|
|
118
|
+
"| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level out and back | level worst detour | level outside |";
|
|
119
|
+
const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
120
|
+
const body = rows.map((r) => {
|
|
121
|
+
const f = r.faults;
|
|
122
|
+
const cells = [r.router, r.fixture, f.through, f.backtrack, f.worstDetour.toFixed(2), f.outside];
|
|
123
|
+
if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
|
|
124
|
+
else {
|
|
125
|
+
const l = sumFaults(r.levelFaults);
|
|
126
|
+
cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.backtrack, l.worstDetour.toFixed(2), l.outside);
|
|
127
|
+
}
|
|
128
|
+
return `| ${cells.join(" | ")} |`;
|
|
129
|
+
});
|
|
130
|
+
return [head, line, ...body].join("\n");
|
|
131
|
+
}
|
package/src/cases.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { makeFixture, rng } from "./fixtures.ts";
|
|
2
|
+
import type { Edge, Graph, Node } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A board of made-up data that shows one known defect of the routes or of the zoom levels.
|
|
6
|
+
* The benchmark races the routers on each case, and the measures of `measureLinks` give a number for the defect.
|
|
7
|
+
*/
|
|
8
|
+
export interface CaseBoard {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** The defect that the board shows, in a few words. */
|
|
11
|
+
readonly defect: string;
|
|
12
|
+
/** This function makes the graph. The same function always gives the same graph. */
|
|
13
|
+
readonly make: () => Graph;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A board while it grows: the functions add nodes and edges in order. */
|
|
17
|
+
class Board {
|
|
18
|
+
readonly nodes: Node[] = [];
|
|
19
|
+
readonly edges: Edge[] = [];
|
|
20
|
+
readonly #pairs = new Set<string>();
|
|
21
|
+
|
|
22
|
+
node(id: string, x: number, y: number, w: number, h: number): string {
|
|
23
|
+
this.nodes.push({ id, box: { x, y, w, h } });
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** This method adds an edge. It adds nothing for a loop or for a second edge between the same two nodes. */
|
|
28
|
+
link(source: string, target: string): void {
|
|
29
|
+
const key = source < target ? `${source}|${target}` : `${target}|${source}`;
|
|
30
|
+
if (source === target || this.#pairs.has(key)) return;
|
|
31
|
+
this.#pairs.add(key);
|
|
32
|
+
this.edges.push({ id: `e${this.edges.length}`, source: { node: source }, target: { node: target } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
graph(): Graph {
|
|
36
|
+
return { nodes: this.nodes, edges: this.edges };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This function makes a dense board: 150 nodes in tight clusters. At the far view, the boxes of the groups overlap.
|
|
42
|
+
* Then a level link starts or ends in a group that another group covers.
|
|
43
|
+
*/
|
|
44
|
+
export function denseGroupsBoard(): Graph {
|
|
45
|
+
return makeFixture({ name: "dense-groups", kind: "clustered", nodes: 150, edgesPerNode: 2, seed: 3 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This function makes a board of three rows of 32 cards. Each card links to the next card of its row.
|
|
50
|
+
* Forty long links join cards far apart. The hub of a section is near the middle of its cards.
|
|
51
|
+
* So a long link climbs to hubs behind its source or past its target, and comes back.
|
|
52
|
+
*/
|
|
53
|
+
export function hubChainBoard(): Graph {
|
|
54
|
+
const R = rng(3);
|
|
55
|
+
const b = new Board();
|
|
56
|
+
const rows = 3;
|
|
57
|
+
const cols = 32;
|
|
58
|
+
const gap = 48;
|
|
59
|
+
const id = (r: number, c: number): string => `c${r}-${c}`;
|
|
60
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * (80 + gap), r * (36 + gap), 80, 36);
|
|
61
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c + 1 < cols; c++) b.link(id(r, c), id(r, c + 1));
|
|
62
|
+
for (let n = 0; n < 40; n++) {
|
|
63
|
+
const r = Math.floor(R() * rows);
|
|
64
|
+
const c = Math.floor(R() * cols);
|
|
65
|
+
const span = Math.floor(cols / 8 + (R() * cols) / 2);
|
|
66
|
+
if (c + span < cols) b.link(id(r, c), id((r + 1) % rows, c + span));
|
|
67
|
+
}
|
|
68
|
+
return b.graph();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This function makes a board of 32 groups in 8 columns and 4 rows. Each group has 4 cards in a square.
|
|
73
|
+
* The cards of a group link in a chain, and 96 random links join cards of two groups.
|
|
74
|
+
* On a level with many groups, some level links go around the outer edge of the board.
|
|
75
|
+
*/
|
|
76
|
+
export function manyGroupsBoard(): Graph {
|
|
77
|
+
const R = rng(7);
|
|
78
|
+
const b = new Board();
|
|
79
|
+
const cols = 8;
|
|
80
|
+
const rows = 4;
|
|
81
|
+
const per = 4;
|
|
82
|
+
const w = 60;
|
|
83
|
+
const h = 30;
|
|
84
|
+
const cardGap = 12;
|
|
85
|
+
const groupGap = 60;
|
|
86
|
+
const side = Math.ceil(Math.sqrt(per));
|
|
87
|
+
const gw = side * w + (side - 1) * cardGap;
|
|
88
|
+
const gh = Math.ceil(per / side) * h + (Math.ceil(per / side) - 1) * cardGap;
|
|
89
|
+
const groups: string[][] = [];
|
|
90
|
+
for (let r = 0; r < rows; r++)
|
|
91
|
+
for (let c = 0; c < cols; c++) {
|
|
92
|
+
const list: string[] = [];
|
|
93
|
+
for (let i = 0; i < per; i++) {
|
|
94
|
+
const x = c * (gw + groupGap) + (i % side) * (w + cardGap);
|
|
95
|
+
const y = r * (gh + groupGap) + Math.floor(i / side) * (h + cardGap);
|
|
96
|
+
list.push(b.node(`g${r}-${c}-${i}`, x, y, w, h));
|
|
97
|
+
}
|
|
98
|
+
groups.push(list);
|
|
99
|
+
}
|
|
100
|
+
for (const list of groups) for (let i = 1; i < list.length; i++) b.link(list[i - 1]!, list[i]!);
|
|
101
|
+
const pick = (list: readonly string[]): string => list[Math.floor(R() * list.length)]!;
|
|
102
|
+
for (let n = 0; n < groups.length * 3; n++) b.link(pick(groups[Math.floor(R() * groups.length)]!), pick(groups[Math.floor(R() * groups.length)]!));
|
|
103
|
+
return b.graph();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
|
|
107
|
+
export const CASES: readonly CaseBoard[] = [
|
|
108
|
+
{ name: "case-dense-groups", defect: "Groups overlap at the far view, and level links go through other groups.", make: denseGroupsBoard },
|
|
109
|
+
{ name: "case-hub-chain", defect: "A route climbs a chain of hubs away from its target and comes back.", make: hubChainBoard },
|
|
110
|
+
{ name: "case-many-groups", defect: "On a level with many groups, links go around the outer edge of the board.", make: manyGroupsBoard },
|
|
111
|
+
];
|
package/src/grid.ts
CHANGED
|
@@ -71,6 +71,56 @@ export class Grid<T> {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* This method gives each item in the buckets along the segment `ab`, widened by `margin`, to `visit`, one time each.
|
|
76
|
+
* The buckets are those of `#segment`, in the same order, so the items come in the same order.
|
|
77
|
+
* A bucket in the range of the step before has no new item, so the walk skips it.
|
|
78
|
+
* The walk stops when `visit` gives true, and then the method gives true.
|
|
79
|
+
*/
|
|
80
|
+
#walk(a: Point, b: Point, margin: number, visit: (item: T) => boolean | void): boolean {
|
|
81
|
+
const stamp = ++this.#stamp;
|
|
82
|
+
const c = this.#cell;
|
|
83
|
+
const items = this.#items;
|
|
84
|
+
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (c / 2)));
|
|
85
|
+
let px = a.x;
|
|
86
|
+
let py = a.y;
|
|
87
|
+
// The bucket range of the step before. The first step has none.
|
|
88
|
+
let pi0 = 1;
|
|
89
|
+
let pi1 = 0;
|
|
90
|
+
let pj0 = 1;
|
|
91
|
+
let pj1 = 0;
|
|
92
|
+
for (let k = 1; k <= n; k++) {
|
|
93
|
+
const qx = k === n ? b.x : a.x + ((b.x - a.x) * k) / n;
|
|
94
|
+
const qy = k === n ? b.y : a.y + ((b.y - a.y) * k) / n;
|
|
95
|
+
const i0 = Math.floor((Math.min(px, qx) - margin) / c);
|
|
96
|
+
const j0 = Math.floor((Math.min(py, qy) - margin) / c);
|
|
97
|
+
const i1 = Math.floor((Math.max(px, qx) + margin) / c);
|
|
98
|
+
const j1 = Math.floor((Math.max(py, qy) + margin) / c);
|
|
99
|
+
// A visit can add an item and so replace the array of stamps. Each step reads it again, as `#take` does.
|
|
100
|
+
const seen = this.#seen;
|
|
101
|
+
for (let i = i0; i <= i1; i++) {
|
|
102
|
+
const before = i >= pi0 && i <= pi1;
|
|
103
|
+
for (let j = j0; j <= j1; j++) {
|
|
104
|
+
if (before && j >= pj0 && j <= pj1) continue;
|
|
105
|
+
const list = this.#buckets.get(this.#key(i, j));
|
|
106
|
+
if (list === undefined) continue;
|
|
107
|
+
for (const id of list) {
|
|
108
|
+
if (seen[id] === stamp) continue;
|
|
109
|
+
seen[id] = stamp;
|
|
110
|
+
if (visit(items[id]!) === true) return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
pi0 = i0;
|
|
115
|
+
pi1 = i1;
|
|
116
|
+
pj0 = j0;
|
|
117
|
+
pj1 = j1;
|
|
118
|
+
px = qx;
|
|
119
|
+
py = qy;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
74
124
|
// The buckets along a segment, widened by `margin`: the boxes of steps of at most half a bucket.
|
|
75
125
|
#segment(a: Point, b: Point, margin: number, each: (x0: number, y0: number, x1: number, y1: number) => void): void {
|
|
76
126
|
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
|
|
@@ -103,8 +153,17 @@ export class Grid<T> {
|
|
|
103
153
|
|
|
104
154
|
/** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
|
|
105
155
|
querySegment(a: Point, b: Point, margin: number, visit: (item: T) => void): void {
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
this.#walk(a, b, margin, (item) => {
|
|
157
|
+
visit(item);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* This method tells if `test` gives true for an item whose buckets come within `margin` of the segment `ab`.
|
|
163
|
+
* It gives the items in the order of `querySegment`, and it stops at the first item that passes the test.
|
|
164
|
+
*/
|
|
165
|
+
someSegment(a: Point, b: Point, margin: number, test: (item: T) => boolean): boolean {
|
|
166
|
+
return this.#walk(a, b, margin, test);
|
|
108
167
|
}
|
|
109
168
|
}
|
|
110
169
|
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./geom.ts";
|
|
|
2
2
|
export { Grid, segmentBounds } from "./grid.ts";
|
|
3
3
|
export * from "./model.ts";
|
|
4
4
|
export * from "./fixtures.ts";
|
|
5
|
+
export * from "./cases.ts";
|
|
5
6
|
export * from "./plugs.ts";
|
|
6
7
|
export * from "./curves.ts";
|
|
7
8
|
export * from "./visibility.ts";
|
|
@@ -11,4 +12,5 @@ export * from "./sections.ts";
|
|
|
11
12
|
export * from "./routers/index.ts";
|
|
12
13
|
export * from "./bench/properties.ts";
|
|
13
14
|
export * from "./bench/quality.ts";
|
|
15
|
+
export * from "./bench/links.ts";
|
|
14
16
|
export * from "./bench/run.ts";
|