@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/src/levels.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { center, dist, polylineLength, type Box, type Point } from "./geom.ts";
|
|
2
|
+
import type { LevelItem, LevelLink, Node, Side } from "./model.ts";
|
|
3
|
+
import { pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "./plugs.ts";
|
|
4
|
+
import { CornerGraph, Obstacles } from "./visibility.ts";
|
|
5
|
+
import type { WayFinder, WayOptions } from "./ways.ts";
|
|
6
|
+
|
|
7
|
+
/** Two items of a level that a link joins, and the count of the edges between them. */
|
|
8
|
+
export interface ItemPair {
|
|
9
|
+
readonly source: string;
|
|
10
|
+
readonly target: string;
|
|
11
|
+
readonly count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The parts of a way finder that a link uses. */
|
|
15
|
+
type Ways = Pick<WayFinder, "obstacles" | "corners" | "margin" | "out">;
|
|
16
|
+
|
|
17
|
+
/** The free plugs of a node, and the reach of each plug. */
|
|
18
|
+
interface FreePlugs {
|
|
19
|
+
readonly plugs: readonly Plug[];
|
|
20
|
+
readonly reach: readonly number[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The ways of a level, with a second corner graph and the free plugs of the nodes. */
|
|
24
|
+
interface LevelWays extends Ways {
|
|
25
|
+
/** This function gives a second corner graph with a small margin. The first link that needs it makes it. */
|
|
26
|
+
readonly tight: () => CornerGraph;
|
|
27
|
+
/** The free plugs of each node that a link needed. */
|
|
28
|
+
readonly free: Map<Node, FreePlugs>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
|
|
32
|
+
const TIGHT_MARGIN = 1;
|
|
33
|
+
|
|
34
|
+
const EPS = 1e-6;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function removes the repeated points, the points in the middle of a straight run,
|
|
38
|
+
* and the points where the way turns back on itself.
|
|
39
|
+
*/
|
|
40
|
+
function tidy(p: readonly Point[]): Point[] {
|
|
41
|
+
const out: Point[] = [];
|
|
42
|
+
for (const q of p) {
|
|
43
|
+
if (out.length > 0 && dist(q, out[out.length - 1]!) <= EPS) continue;
|
|
44
|
+
while (out.length >= 2) {
|
|
45
|
+
const a = out[out.length - 2]!;
|
|
46
|
+
const b = out[out.length - 1]!;
|
|
47
|
+
const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
|
|
48
|
+
if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
|
|
49
|
+
// The point b is on the line through a and q: in the middle of a straight run, or at the tip of a turn back.
|
|
50
|
+
out.pop();
|
|
51
|
+
}
|
|
52
|
+
if (out.length > 0 && dist(q, out[out.length - 1]!) <= EPS) continue;
|
|
53
|
+
out.push(q);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This function gives the links of one level. The items of the level are the obstacles.
|
|
60
|
+
* A link starts on the boundary of the box of its source and ends on the boundary of the box of its target.
|
|
61
|
+
* It goes around the boxes of the other items. Where there is space, its legs are parallel to the axes.
|
|
62
|
+
*/
|
|
63
|
+
export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair[], options: WayOptions = {}): LevelLink[] {
|
|
64
|
+
if (pairs.length === 0) return [];
|
|
65
|
+
const nodes: Node[] = items.map((i) => ({ id: i.id, box: i.box }));
|
|
66
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
67
|
+
const ways = levelWays(nodes, options);
|
|
68
|
+
const inset = Math.min(6, ways.margin);
|
|
69
|
+
const out: LevelLink[] = [];
|
|
70
|
+
for (const p of pairs) {
|
|
71
|
+
const s = byId.get(p.source);
|
|
72
|
+
const t = byId.get(p.target);
|
|
73
|
+
if (!s || !t) continue;
|
|
74
|
+
// Two items with a clear way between their facing sides get the short way.
|
|
75
|
+
const [fs, ft] = facingPlugs(s.box, t.box, inset);
|
|
76
|
+
let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : undefined;
|
|
77
|
+
// Else the link goes from the middle of a side. Each item has at most four such plugs, so the corner graph finds their ends one time.
|
|
78
|
+
points ??= sideWay(ways, s, t, inset);
|
|
79
|
+
out.push({ source: p.source, target: p.target, count: p.count, points });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* This function gives a way from plug to plug between the plugs at the middle of a side of the two nodes.
|
|
86
|
+
* Without a way in the corner graph, the link goes straight between the ends, with legs parallel to the axes where they are clear.
|
|
87
|
+
* When that line goes through another box, the link goes between the free plugs of the two nodes (`freeWay`) instead.
|
|
88
|
+
*/
|
|
89
|
+
function sideWay(ways: LevelWays, s: Node, t: Node, inset: number): Point[] {
|
|
90
|
+
const e = ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box)));
|
|
91
|
+
const quick = quickWay(ways, e);
|
|
92
|
+
if (quick) return quick;
|
|
93
|
+
const path = ways.corners.path(e.a, e.b);
|
|
94
|
+
const way = pulled(ways, e.ps, path ?? [e.a, e.b], e.pt);
|
|
95
|
+
if (path || ways.obstacles.clearPath(way, 0, new Set([s, t]))) return way;
|
|
96
|
+
return freeWay(ways, s, t, inset) ?? way;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* This function makes the obstacles and the corner graph of the items of a level.
|
|
101
|
+
* The buckets of the obstacles grow with the items, so a long link visits few buckets.
|
|
102
|
+
*/
|
|
103
|
+
function levelWays(nodes: readonly Node[], options: WayOptions): LevelWays {
|
|
104
|
+
const margin = options.margin ?? 5;
|
|
105
|
+
const k = options.k ?? 8;
|
|
106
|
+
let size = 0;
|
|
107
|
+
for (const n of nodes) size += Math.max(n.box.w, n.box.h);
|
|
108
|
+
const obstacles = new Obstacles(nodes, Math.max(180, (2 * size) / Math.max(1, nodes.length)));
|
|
109
|
+
const corners = new CornerGraph(nodes, obstacles, { margin, k });
|
|
110
|
+
const out = (p: Plug): Point => ({ x: p.point.x + p.normal.x * margin, y: p.point.y + p.normal.y * margin });
|
|
111
|
+
let tight: CornerGraph | undefined;
|
|
112
|
+
const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, TIGHT_MARGIN), k }));
|
|
113
|
+
return { obstacles, corners, margin, out, tight: tightGraph, free: new Map() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* This function gives the plugs of a link between two boxes. Each plug is on the side that faces the other box.
|
|
118
|
+
* When the two sides face each other and their spans overlap, both plugs are at the middle of the overlap, so the link is straight.
|
|
119
|
+
*/
|
|
120
|
+
export function facingPlugs(a: Box, b: Box, inset: number): [Plug, Plug] {
|
|
121
|
+
const sa = sideToward(a, center(b));
|
|
122
|
+
const sb = sideToward(b, center(a));
|
|
123
|
+
const plug = (box: Box, side: Side, toward: Point): Plug => ({ point: pointOnSide(box, side, toward, inset), normal: SIDE_NORMAL[side] });
|
|
124
|
+
const across = (sa === "east" && sb === "west") || (sa === "west" && sb === "east");
|
|
125
|
+
const down = (sa === "south" && sb === "north") || (sa === "north" && sb === "south");
|
|
126
|
+
if (across || down) {
|
|
127
|
+
const lo = across ? Math.max(a.y, b.y) + inset : Math.max(a.x, b.x) + inset;
|
|
128
|
+
const hi = across ? Math.min(a.y + a.h, b.y + b.h) - inset : Math.min(a.x + a.w, b.x + b.w) - inset;
|
|
129
|
+
if (lo <= hi) {
|
|
130
|
+
const ca = center(a);
|
|
131
|
+
const cb = center(b);
|
|
132
|
+
const m = Math.max(lo, Math.min(hi, across ? (ca.y + cb.y) / 2 : (ca.x + cb.x) / 2));
|
|
133
|
+
const at = (box: Box, side: Side): Point =>
|
|
134
|
+
across
|
|
135
|
+
? { x: side === "east" ? box.x + box.w : box.x, y: m }
|
|
136
|
+
: { x: m, y: side === "south" ? box.y + box.h : box.y };
|
|
137
|
+
return [
|
|
138
|
+
{ point: at(a, sa), normal: SIDE_NORMAL[sa] },
|
|
139
|
+
{ point: at(b, sb), normal: SIDE_NORMAL[sb] },
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [plug(a, sa, center(b)), plug(b, sb, center(a))];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SIDES: readonly Side[] = ["north", "east", "south", "west"];
|
|
147
|
+
|
|
148
|
+
/** This function tells if the way out of a plug is free. The plug and the point at `margin` from it are in no other box. */
|
|
149
|
+
function isFree(ways: Ways, node: Node, plug: Plug): boolean {
|
|
150
|
+
return !ways.obstacles.inside(ways.out(plug), 0, node) && !ways.obstacles.inside(plug.point, 0, node);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** This function gives the plug at the middle of a side of the node: the side with a free way out that faces `toward` most. */
|
|
154
|
+
function sidePlug(ways: Ways, node: Node, toward: Point): Plug {
|
|
155
|
+
const b = node.box;
|
|
156
|
+
const c = center(b);
|
|
157
|
+
const d = { x: toward.x - c.x, y: toward.y - c.y };
|
|
158
|
+
const facing = (s: Side): number => SIDE_NORMAL[s].x * d.x + SIDE_NORMAL[s].y * d.y;
|
|
159
|
+
const order = [...SIDES].sort((u, v) => facing(v) - facing(u));
|
|
160
|
+
const plug = (s: Side): Plug => {
|
|
161
|
+
const n = SIDE_NORMAL[s];
|
|
162
|
+
return { point: { x: c.x + (n.x * b.w) / 2, y: c.y + (n.y * b.h) / 2 }, normal: n };
|
|
163
|
+
};
|
|
164
|
+
for (const s of order) if (isFree(ways, node, plug(s))) return plug(s);
|
|
165
|
+
return plug(order[0]!);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* This function gives the free plugs of a node: on each side, the free point nearest to the middle of the side.
|
|
170
|
+
* A point is free when it and the point at `margin` from it are in no other box. The function keeps the plugs of each node.
|
|
171
|
+
*/
|
|
172
|
+
function freePlugs(ways: LevelWays, node: Node, inset: number): FreePlugs {
|
|
173
|
+
const known = ways.free.get(node);
|
|
174
|
+
if (known) return known;
|
|
175
|
+
const b = node.box;
|
|
176
|
+
const out: Plug[] = [];
|
|
177
|
+
for (const side of SIDES) {
|
|
178
|
+
const n = SIDE_NORMAL[side];
|
|
179
|
+
const across = n.x === 0;
|
|
180
|
+
// The side goes from `lo` to `hi` along its axis, at the coordinate `at` of the other axis.
|
|
181
|
+
const lo = across ? b.x : b.y;
|
|
182
|
+
const hi = across ? b.x + b.w : b.y + b.h;
|
|
183
|
+
const at = across ? (n.y > 0 ? b.y + b.h : b.y) : n.x > 0 ? b.x + b.w : b.x;
|
|
184
|
+
const far = at + (across ? n.y : n.x) * ways.margin;
|
|
185
|
+
// A box covers a point of the side when the point or its way out is inside the box, as in `isFree`.
|
|
186
|
+
const covered: [number, number][] = [];
|
|
187
|
+
const strip = across
|
|
188
|
+
? { x: lo, y: Math.min(at, far), w: hi - lo, h: Math.abs(far - at) }
|
|
189
|
+
: { x: Math.min(at, far), y: lo, w: Math.abs(far - at), h: hi - lo };
|
|
190
|
+
ways.obstacles.query(strip, (m) => {
|
|
191
|
+
if (m === node) return;
|
|
192
|
+
const o = m.box;
|
|
193
|
+
const [o0, o1, p0, p1] = across ? [o.x, o.x + o.w, o.y, o.y + o.h] : [o.y, o.y + o.h, o.x, o.x + o.w];
|
|
194
|
+
if ((at > p0 && at < p1) || (far > p0 && far < p1)) covered.push([o0, o1]);
|
|
195
|
+
});
|
|
196
|
+
const mid = (lo + hi) / 2;
|
|
197
|
+
let best: number | undefined;
|
|
198
|
+
for (const [f0, f1] of freeSpans(lo, hi, covered)) {
|
|
199
|
+
const room = Math.min(inset, (f1 - f0) / 2);
|
|
200
|
+
const u = Math.max(f0 + room, Math.min(f1 - room, mid));
|
|
201
|
+
if (best === undefined || Math.abs(u - mid) < Math.abs(best - mid)) best = u;
|
|
202
|
+
}
|
|
203
|
+
if (best !== undefined) out.push({ point: across ? { x: best, y: at } : { x: at, y: best }, normal: n });
|
|
204
|
+
}
|
|
205
|
+
const free = { plugs: out, reach: out.map((p) => reach(ways, node, p)) };
|
|
206
|
+
ways.free.set(node, free);
|
|
207
|
+
return free;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** This function gives the indices of the plugs, and the plugs that face `toward` most come first. */
|
|
211
|
+
function facingOrder(plugs: readonly Plug[], from: Point, toward: Point): number[] {
|
|
212
|
+
const facing = (p: Plug): number => p.normal.x * (toward.x - from.x) + p.normal.y * (toward.y - from.y);
|
|
213
|
+
return plugs.map((_, i) => i).sort((u, v) => facing(plugs[v]!) - facing(plugs[u]!) || u - v);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** This function gives the parts of the span from `lo` to `hi` outside each open interval of `covered`. A part of zero length does not count. */
|
|
217
|
+
function freeSpans(lo: number, hi: number, covered: [number, number][]): [number, number][] {
|
|
218
|
+
covered.sort((u, v) => u[0] - v[0]);
|
|
219
|
+
const out: [number, number][] = [];
|
|
220
|
+
let x = lo;
|
|
221
|
+
for (const [c0, c1] of covered) {
|
|
222
|
+
if (c0 > x) out.push([x, Math.min(c0, hi)]);
|
|
223
|
+
x = Math.max(x, c1);
|
|
224
|
+
if (x >= hi) break;
|
|
225
|
+
}
|
|
226
|
+
if (x < hi) out.push([x, hi]);
|
|
227
|
+
return out.filter(([a, b]) => b - a > EPS);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
|
|
232
|
+
* A link uses it when its plugs at the middle of a side find no way. Other boxes can cover such a plug, or close the space in front of it.
|
|
233
|
+
*/
|
|
234
|
+
function freeWay(ways: LevelWays, s: Node, t: Node, inset: number): Point[] | undefined {
|
|
235
|
+
const fs = freePlugs(ways, s, inset);
|
|
236
|
+
const ft = freePlugs(ways, t, inset);
|
|
237
|
+
const cs = center(s.box);
|
|
238
|
+
const ct = center(t.box);
|
|
239
|
+
const is = facingOrder(fs.plugs, cs, ct);
|
|
240
|
+
const it = facingOrder(ft.plugs, ct, cs);
|
|
241
|
+
const from = is.map((i) => fs.plugs[i]!);
|
|
242
|
+
const to = it.map((k) => ft.plugs[k]!);
|
|
243
|
+
const ra = is.map((i) => fs.reach[i]!);
|
|
244
|
+
const rb = it.map((k) => ft.reach[k]!);
|
|
245
|
+
// A pair of plugs with one or two clear legs between them gives the short way.
|
|
246
|
+
let best: Point[] | undefined;
|
|
247
|
+
let bestLength = Infinity;
|
|
248
|
+
from.forEach((ps, i) =>
|
|
249
|
+
to.forEach((pt, k) => {
|
|
250
|
+
const way = quickWay(ways, endsAt(ps, ra[i]!, pt, rb[k]!));
|
|
251
|
+
const length = way ? polylineLength(way) : Infinity;
|
|
252
|
+
if (length < bestLength) {
|
|
253
|
+
best = way;
|
|
254
|
+
bestLength = length;
|
|
255
|
+
}
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
if (best) return best;
|
|
259
|
+
// Else one search of the second corner graph finds the shortest way from a plug of `s` to a plug of `t`.
|
|
260
|
+
// The corners of the second graph are near the boxes, so the way can go through a gap that is narrower than the margin.
|
|
261
|
+
const a = from.map((ps, i) => ({ point: along(ps, ra[i]!), cost: ra[i]! }));
|
|
262
|
+
const b = to.map((pt, k) => ({ point: along(pt, rb[k]!), cost: rb[k]! }));
|
|
263
|
+
const found = ways.tight().pathAmong(a, b);
|
|
264
|
+
return found && pulled(ways, from[found.from]!, found.points, to[found.to]!);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The two ends of a link: each plug, and the point where the link turns away from the normal of the plug. */
|
|
268
|
+
interface Ends {
|
|
269
|
+
readonly ps: Plug;
|
|
270
|
+
readonly pt: Plug;
|
|
271
|
+
readonly a: Point;
|
|
272
|
+
readonly b: Point;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* This function gives the ends of a link. The link leaves each plug along its normal for the margin.
|
|
277
|
+
* A box can be nearer than twice the margin in front of a plug. Then the link turns in the middle of the space before that box.
|
|
278
|
+
* Two plugs that face each other across a narrow space make the link turn in the middle of that space.
|
|
279
|
+
*/
|
|
280
|
+
function ends(ways: Ways, s: Node, ps: Plug, t: Node, pt: Plug): Ends {
|
|
281
|
+
return endsAt(ps, reach(ways, s, ps), pt, reach(ways, t, pt));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** This function gives the ends of a link from the plug `ps` to the plug `pt`, with the reach of each plug. */
|
|
285
|
+
function endsAt(ps: Plug, ra: number, pt: Plug, rb: number): Ends {
|
|
286
|
+
// Two plugs that face each other turn at the same distance, so the link does not go past itself.
|
|
287
|
+
if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
|
|
288
|
+
const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
|
|
289
|
+
if (d > 0 && ra + rb > d) ra = rb = d / 2;
|
|
290
|
+
}
|
|
291
|
+
return { ps, pt, a: along(ps, ra), b: along(pt, rb) };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const along = (p: Plug, d: number): Point => ({ x: p.point.x + p.normal.x * d, y: p.point.y + p.normal.y * d });
|
|
295
|
+
|
|
296
|
+
/** This function gives the distance that a link goes along the normal of a plug before it turns. */
|
|
297
|
+
function reach(ways: Ways, node: Node, plug: Plug): number {
|
|
298
|
+
const skip = new Set([node]);
|
|
299
|
+
const far = 2 * ways.margin;
|
|
300
|
+
if (ways.obstacles.clear(plug.point, along(plug, far), 0, skip)) return ways.margin;
|
|
301
|
+
let lo = 0;
|
|
302
|
+
let hi = far;
|
|
303
|
+
for (let i = 0; i < 10; i++) {
|
|
304
|
+
const m = (lo + hi) / 2;
|
|
305
|
+
if (ways.obstacles.clear(plug.point, along(plug, m), 0, skip)) lo = m;
|
|
306
|
+
else hi = m;
|
|
307
|
+
}
|
|
308
|
+
return lo / 2;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* This function gives the way between the ends when one or two legs parallel to the axes are clear, or `undefined`.
|
|
313
|
+
* The legs keep half the margin from the boxes, or less when an end turns nearer to a box.
|
|
314
|
+
*/
|
|
315
|
+
function quickWay(ways: Ways, e: Ends): Point[] | undefined {
|
|
316
|
+
const near = Math.min(dist(e.ps.point, e.a), dist(e.pt.point, e.b));
|
|
317
|
+
const pad = Math.max(0, Math.min(ways.margin / 2, near - 1));
|
|
318
|
+
const legs = square(e.a, e.b, ways.obstacles, pad, axisOf(e.ps.normal), axisOf(e.pt.normal));
|
|
319
|
+
return legs && tidy([e.ps.point, e.a, ...legs, e.pt.point]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* This function gives a way from the plug `ps` through the points of `path` to the plug `pt`.
|
|
324
|
+
* The way leaves and enters each plug along its normal. Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
|
|
325
|
+
*/
|
|
326
|
+
function pulled(ways: Ways, ps: Plug, path: readonly Point[], pt: Plug): Point[] {
|
|
327
|
+
const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
|
|
328
|
+
return tidy([ps.point, ...legs, pt.point]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type Axis = "x" | "y";
|
|
332
|
+
|
|
333
|
+
const axisOf = (n: Point): Axis => (Math.abs(n.x) > Math.abs(n.y) ? "x" : "y");
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* This function pulls a path tight. From each vertex, it goes to the farthest vertex that one or two legs can reach.
|
|
337
|
+
* These legs are parallel to the axes and clear of the obstacles by `pad`. It keeps a diagonal leg only where no such legs are clear.
|
|
338
|
+
* `first` is the axis of the way into the first vertex, and `last` is the axis of the way out of the last vertex.
|
|
339
|
+
*/
|
|
340
|
+
function squarePull(p: readonly Point[], obstacles: Obstacles, pad: number, first: Axis, last: Axis, lookahead = 16): Point[] {
|
|
341
|
+
if (p.length === 0) return [];
|
|
342
|
+
const out: Point[] = [p[0]!];
|
|
343
|
+
let axis: Axis | undefined = first;
|
|
344
|
+
let i = 0;
|
|
345
|
+
while (i < p.length - 1) {
|
|
346
|
+
let step: { j: number; legs: Point[] } | undefined;
|
|
347
|
+
for (let j = Math.min(p.length - 1, i + lookahead); j > i && !step; j--) {
|
|
348
|
+
const legs = square(p[i]!, p[j]!, obstacles, pad, axis, j === p.length - 1 ? last : undefined);
|
|
349
|
+
if (legs) step = { j, legs };
|
|
350
|
+
}
|
|
351
|
+
// With no clear legs to the next vertex, the legs may touch a box, but they do not go through it.
|
|
352
|
+
step ??= {
|
|
353
|
+
j: i + 1,
|
|
354
|
+
legs: square(p[i]!, p[i + 1]!, obstacles, 0, axis, i + 1 === p.length - 1 ? last : undefined) ?? [p[i + 1]!],
|
|
355
|
+
};
|
|
356
|
+
out.push(...step.legs);
|
|
357
|
+
const end = out[out.length - 1]!;
|
|
358
|
+
const before = out[out.length - 2]!;
|
|
359
|
+
axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : undefined;
|
|
360
|
+
i = step.j;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* This function gives one or two legs parallel to the axes from `a` to `b`, clear of the obstacles by `pad`, or `undefined`.
|
|
367
|
+
* Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
|
|
368
|
+
*/
|
|
369
|
+
function square(a: Point, b: Point, obstacles: Obstacles, pad: number, from: Axis | undefined, to: Axis | undefined): Point[] | undefined {
|
|
370
|
+
if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : undefined;
|
|
371
|
+
// The corner of `x` goes along the x axis first, and the corner of `y` along the y axis first.
|
|
372
|
+
const options: { c: Point; bends: number }[] = [
|
|
373
|
+
{ c: { x: b.x, y: a.y }, bends: (from === "y" ? 1 : 0) + (to === "x" ? 1 : 0) },
|
|
374
|
+
{ c: { x: a.x, y: b.y }, bends: (from === "x" ? 1 : 0) + (to === "y" ? 1 : 0) },
|
|
375
|
+
];
|
|
376
|
+
options.sort((u, v) => u.bends - v.bends);
|
|
377
|
+
for (const { c } of options) if (obstacles.clear(a, c, pad) && obstacles.clear(c, b, pad)) return [c, b];
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type Level,
|
|
6
|
-
type LevelItem,
|
|
7
|
-
type LevelLink,
|
|
8
|
-
type Node,
|
|
9
|
-
type Result,
|
|
10
|
-
type Route,
|
|
11
|
-
type Router,
|
|
12
|
-
type SharedRun,
|
|
13
|
-
} from "../model.ts";
|
|
1
|
+
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
|
+
import { center, dist, pointSegmentDistance, polylineLength, type Box, type Point } from "../geom.ts";
|
|
3
|
+
import { levelLinks, type ItemPair } from "../levels.ts";
|
|
4
|
+
import { nodeIndex, type Graph, type Level, type LevelItem, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
14
5
|
import { placePlug } from "../plugs.ts";
|
|
15
6
|
import { allSections, buildSections, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
16
7
|
import { orderLanes } from "../segments.ts";
|
|
@@ -33,11 +24,22 @@ export interface SectionsOptions extends WayOptions, SectionOptions {
|
|
|
33
24
|
|
|
34
25
|
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
|
|
35
26
|
|
|
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;
|
|
32
|
+
|
|
36
33
|
/**
|
|
37
34
|
* 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.
|
|
38
35
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
39
36
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
40
|
-
* The edges in one section of the lowest level go straight around the nodes.
|
|
37
|
+
* The edges in one section of the lowest level go straight around the nodes.
|
|
38
|
+
*
|
|
39
|
+
* An edge skips a hub that takes it away from its target. A way that still goes out and back gives way to the direct way.
|
|
40
|
+
*
|
|
41
|
+
* The result gives one zoom level for each depth of the tree.
|
|
42
|
+
* A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
|
|
41
43
|
*/
|
|
42
44
|
export function sectionsRouter(options: SectionsOptions): Router {
|
|
43
45
|
const router: Router = {
|
|
@@ -73,6 +75,20 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
73
75
|
}
|
|
74
76
|
return h;
|
|
75
77
|
};
|
|
78
|
+
// The sections of the chain whose hubs do not take the way away from its target. The test uses the straight lines from hub to hub.
|
|
79
|
+
const forward = (list: readonly Section[], from: Point, to: Point): Section[] => {
|
|
80
|
+
const kept: Section[] = [];
|
|
81
|
+
let near = dist(from, to);
|
|
82
|
+
let last = from;
|
|
83
|
+
for (const c of list) {
|
|
84
|
+
const h = hub(c);
|
|
85
|
+
if (dist(h, to) > near + HUB_SLACK) continue;
|
|
86
|
+
near = Math.min(near, pointSegmentDistance(to, last, h));
|
|
87
|
+
last = h;
|
|
88
|
+
kept.push(c);
|
|
89
|
+
}
|
|
90
|
+
return kept;
|
|
91
|
+
};
|
|
76
92
|
const up = (s: Section): Section[] => {
|
|
77
93
|
const out: Section[] = [];
|
|
78
94
|
for (let x: Section | undefined = s; x; x = parent.get(x)) out.push(x);
|
|
@@ -98,8 +114,6 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
98
114
|
const at = (p: Point): string => `${p.x},${p.y}`;
|
|
99
115
|
|
|
100
116
|
const routes: Route[] = [];
|
|
101
|
-
// The index in the route of each hub that the route passes, by section.
|
|
102
|
-
const marks = new Map<string, Map<Section, number>>();
|
|
103
117
|
for (const e of graph.edges) {
|
|
104
118
|
const s = byId.get(e.source.node);
|
|
105
119
|
const t = byId.get(e.target.node);
|
|
@@ -120,25 +134,24 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
120
134
|
const lca = a.find((x) => common.has(x))!;
|
|
121
135
|
const keep = options.hubs ?? Infinity;
|
|
122
136
|
const side = (list: Section[]): Section[] => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
123
|
-
const
|
|
137
|
+
const end = ways.out(pt);
|
|
138
|
+
const start = ways.out(ps);
|
|
139
|
+
const chain = forward([...side(a), ...side(b).reverse()], start, pt.point);
|
|
140
|
+
if (chain.length === 0) {
|
|
141
|
+
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
124
144
|
const out: Point[] = [ps.point];
|
|
125
|
-
const mark = new Map<Section, number>();
|
|
126
145
|
const append = (p: readonly Point[]): void => {
|
|
127
146
|
for (const q of p) if (dist(q, out[out.length - 1]!) > 1e-9) out.push(q);
|
|
128
147
|
};
|
|
129
148
|
const used: string[] = [];
|
|
130
|
-
const start = ways.out(ps);
|
|
131
149
|
append(piece(at(start), start, chain[0]!.id, hub(chain[0]!), used));
|
|
132
|
-
|
|
133
|
-
for (let i = 1; i < chain.length; i++) {
|
|
134
|
-
append(piece(chain[i - 1]!.id, hub(chain[i - 1]!), chain[i]!.id, hub(chain[i]!), used));
|
|
135
|
-
mark.set(chain[i]!, out.length - 1);
|
|
136
|
-
}
|
|
137
|
-
const end = ways.out(pt);
|
|
150
|
+
for (let i = 1; i < chain.length; i++) append(piece(chain[i - 1]!.id, hub(chain[i - 1]!), chain[i]!.id, hub(chain[i]!), used));
|
|
138
151
|
append(piece(chain[chain.length - 1]!.id, hub(chain[chain.length - 1]!), at(end), end, used));
|
|
139
152
|
append([pt.point]);
|
|
140
|
-
// A way through the hubs that is too long gives way to the direct way.
|
|
141
|
-
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
|
|
153
|
+
// 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) + 4 * ways.margin || goesOutAndBack(out)) {
|
|
142
155
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
143
156
|
continue;
|
|
144
157
|
}
|
|
@@ -148,7 +161,6 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
148
161
|
else users.set(key, [e.id]);
|
|
149
162
|
}
|
|
150
163
|
routes.push({ edge: e.id, points: out });
|
|
151
|
-
marks.set(e.id, mark);
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
@@ -158,7 +170,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
158
170
|
if (list.length < 2 || p.length < 2) continue;
|
|
159
171
|
runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!)) });
|
|
160
172
|
}
|
|
161
|
-
return { routes, runs, levels: makeLevels(graph, root, routeOf,
|
|
173
|
+
return { routes, runs, levels: makeLevels(graph, root, routeOf, options) };
|
|
162
174
|
},
|
|
163
175
|
};
|
|
164
176
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
@@ -167,14 +179,10 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
167
179
|
/**
|
|
168
180
|
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
169
181
|
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
170
|
-
*
|
|
182
|
+
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
183
|
+
* In the last level, the points of a link are the route of its first edge.
|
|
171
184
|
*/
|
|
172
|
-
function makeLevels(
|
|
173
|
-
graph: Graph,
|
|
174
|
-
root: Section,
|
|
175
|
-
routeOf: ReadonlyMap<string, Route>,
|
|
176
|
-
marks: ReadonlyMap<string, ReadonlyMap<Section, number>>,
|
|
177
|
-
): Level[] {
|
|
185
|
+
function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options: WayOptions): Level[] {
|
|
178
186
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
179
187
|
const levels: Level[] = [];
|
|
180
188
|
const deepest = treeDepth(root);
|
|
@@ -208,7 +216,7 @@ function makeLevels(
|
|
|
208
216
|
}
|
|
209
217
|
};
|
|
210
218
|
if (graph.nodes.length > 0) visit(root);
|
|
211
|
-
const links = new Map<string, { source: string; target: string; count: number;
|
|
219
|
+
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
212
220
|
for (const e of graph.edges) {
|
|
213
221
|
const iu = indexOfNode.get(e.source.node);
|
|
214
222
|
const iv = indexOfNode.get(e.target.node);
|
|
@@ -224,13 +232,12 @@ function makeLevels(
|
|
|
224
232
|
link.count++;
|
|
225
233
|
continue;
|
|
226
234
|
}
|
|
227
|
-
|
|
228
|
-
const mark = marks.get(e.id);
|
|
229
|
-
const from = typeof a === "number" ? 0 : (mark?.get(a) ?? 0);
|
|
230
|
-
const to = typeof b === "number" ? route.length - 1 : (mark?.get(b) ?? route.length - 1);
|
|
231
|
-
links.set(key, { source: ida, target: idb, count: 1, points: route.slice(from, to + 1) });
|
|
235
|
+
links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
|
|
232
236
|
}
|
|
233
|
-
|
|
237
|
+
const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
|
|
238
|
+
if (d > deepest)
|
|
239
|
+
levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
|
|
240
|
+
else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
|
|
234
241
|
}
|
|
235
242
|
return levels;
|
|
236
243
|
}
|