@mark1russell7/highways 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/build/index.d.ts +468 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1882 -255
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/curves.ts +98 -0
- package/src/bench/links.ts +191 -0
- package/src/bench/room.ts +122 -0
- package/src/bench/run.ts +28 -0
- package/src/cases.ts +139 -0
- package/src/curves/arc/index.ts +71 -0
- package/src/curves/index.ts +48 -0
- package/src/curves/model.ts +64 -0
- package/src/curves/rounded/index.ts +60 -0
- package/src/curves/sample.ts +73 -0
- package/src/curves/space.ts +91 -0
- package/src/curves/spline/index.ts +77 -0
- package/src/curves/straight/index.ts +7 -0
- package/src/grid.ts +61 -2
- package/src/index.ts +6 -0
- package/src/levels.ts +151 -14
- package/src/room/index.ts +334 -0
- package/src/routers/sections.ts +33 -6
- package/src/visibility.ts +334 -92
package/src/levels.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { center, dist, type Box, type Point } from "./geom.ts";
|
|
1
|
+
import { center, dist, polylineLength, type Box, type Point } from "./geom.ts";
|
|
2
2
|
import type { LevelItem, LevelLink, Node, Side } from "./model.ts";
|
|
3
3
|
import { pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "./plugs.ts";
|
|
4
4
|
import { CornerGraph, Obstacles } from "./visibility.ts";
|
|
@@ -14,6 +14,23 @@ export interface ItemPair {
|
|
|
14
14
|
/** The parts of a way finder that a link uses. */
|
|
15
15
|
type Ways = Pick<WayFinder, "obstacles" | "corners" | "margin" | "out">;
|
|
16
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
|
+
|
|
17
34
|
const EPS = 1e-6;
|
|
18
35
|
|
|
19
36
|
/**
|
|
@@ -58,23 +75,43 @@ export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair
|
|
|
58
75
|
const [fs, ft] = facingPlugs(s.box, t.box, inset);
|
|
59
76
|
let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : undefined;
|
|
60
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.
|
|
61
|
-
points ??=
|
|
78
|
+
points ??= sideWay(ways, s, t, inset);
|
|
62
79
|
out.push({ source: p.source, target: p.target, count: p.count, points });
|
|
63
80
|
}
|
|
64
81
|
return out;
|
|
65
82
|
}
|
|
66
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
|
+
|
|
67
99
|
/**
|
|
68
100
|
* This function makes the obstacles and the corner graph of the items of a level.
|
|
69
101
|
* The buckets of the obstacles grow with the items, so a long link visits few buckets.
|
|
102
|
+
* The corner graphs use `cones`, so a link goes along the corridors between the groups, not around the outer edge of the board.
|
|
70
103
|
*/
|
|
71
|
-
function levelWays(nodes: readonly Node[], options: WayOptions):
|
|
104
|
+
function levelWays(nodes: readonly Node[], options: WayOptions): LevelWays {
|
|
72
105
|
const margin = options.margin ?? 5;
|
|
106
|
+
const k = options.k ?? 8;
|
|
73
107
|
let size = 0;
|
|
74
108
|
for (const n of nodes) size += Math.max(n.box.w, n.box.h);
|
|
75
109
|
const obstacles = new Obstacles(nodes, Math.max(180, (2 * size) / Math.max(1, nodes.length)));
|
|
76
|
-
const corners = new CornerGraph(nodes, obstacles, { margin, k:
|
|
77
|
-
|
|
110
|
+
const corners = new CornerGraph(nodes, obstacles, { margin, k, cones: true });
|
|
111
|
+
const out = (p: Plug): Point => ({ x: p.point.x + p.normal.x * margin, y: p.point.y + p.normal.y * margin });
|
|
112
|
+
let tight: CornerGraph | undefined;
|
|
113
|
+
const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, TIGHT_MARGIN), k, cones: true }));
|
|
114
|
+
return { obstacles, corners, margin, out, tight: tightGraph, free: new Map() };
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
/**
|
|
@@ -129,6 +166,105 @@ function sidePlug(ways: Ways, node: Node, toward: Point): Plug {
|
|
|
129
166
|
return plug(order[0]!);
|
|
130
167
|
}
|
|
131
168
|
|
|
169
|
+
/**
|
|
170
|
+
* This function gives the free plugs of a node: on each side, the free point nearest to the middle of the side.
|
|
171
|
+
* 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.
|
|
172
|
+
*/
|
|
173
|
+
function freePlugs(ways: LevelWays, node: Node, inset: number): FreePlugs {
|
|
174
|
+
const known = ways.free.get(node);
|
|
175
|
+
if (known) return known;
|
|
176
|
+
const b = node.box;
|
|
177
|
+
const out: Plug[] = [];
|
|
178
|
+
for (const side of SIDES) {
|
|
179
|
+
const n = SIDE_NORMAL[side];
|
|
180
|
+
const across = n.x === 0;
|
|
181
|
+
// The side goes from `lo` to `hi` along its axis, at the coordinate `at` of the other axis.
|
|
182
|
+
const lo = across ? b.x : b.y;
|
|
183
|
+
const hi = across ? b.x + b.w : b.y + b.h;
|
|
184
|
+
const at = across ? (n.y > 0 ? b.y + b.h : b.y) : n.x > 0 ? b.x + b.w : b.x;
|
|
185
|
+
const far = at + (across ? n.y : n.x) * ways.margin;
|
|
186
|
+
// A box covers a point of the side when the point or its way out is inside the box, as in `isFree`.
|
|
187
|
+
const covered: [number, number][] = [];
|
|
188
|
+
const strip = across
|
|
189
|
+
? { x: lo, y: Math.min(at, far), w: hi - lo, h: Math.abs(far - at) }
|
|
190
|
+
: { x: Math.min(at, far), y: lo, w: Math.abs(far - at), h: hi - lo };
|
|
191
|
+
ways.obstacles.query(strip, (m) => {
|
|
192
|
+
if (m === node) return;
|
|
193
|
+
const o = m.box;
|
|
194
|
+
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];
|
|
195
|
+
if ((at > p0 && at < p1) || (far > p0 && far < p1)) covered.push([o0, o1]);
|
|
196
|
+
});
|
|
197
|
+
const mid = (lo + hi) / 2;
|
|
198
|
+
let best: number | undefined;
|
|
199
|
+
for (const [f0, f1] of freeSpans(lo, hi, covered)) {
|
|
200
|
+
const room = Math.min(inset, (f1 - f0) / 2);
|
|
201
|
+
const u = Math.max(f0 + room, Math.min(f1 - room, mid));
|
|
202
|
+
if (best === undefined || Math.abs(u - mid) < Math.abs(best - mid)) best = u;
|
|
203
|
+
}
|
|
204
|
+
if (best !== undefined) out.push({ point: across ? { x: best, y: at } : { x: at, y: best }, normal: n });
|
|
205
|
+
}
|
|
206
|
+
const free = { plugs: out, reach: out.map((p) => reach(ways, node, p)) };
|
|
207
|
+
ways.free.set(node, free);
|
|
208
|
+
return free;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** This function gives the indices of the plugs, and the plugs that face `toward` most come first. */
|
|
212
|
+
function facingOrder(plugs: readonly Plug[], from: Point, toward: Point): number[] {
|
|
213
|
+
const facing = (p: Plug): number => p.normal.x * (toward.x - from.x) + p.normal.y * (toward.y - from.y);
|
|
214
|
+
return plugs.map((_, i) => i).sort((u, v) => facing(plugs[v]!) - facing(plugs[u]!) || u - v);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 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. */
|
|
218
|
+
function freeSpans(lo: number, hi: number, covered: [number, number][]): [number, number][] {
|
|
219
|
+
covered.sort((u, v) => u[0] - v[0]);
|
|
220
|
+
const out: [number, number][] = [];
|
|
221
|
+
let x = lo;
|
|
222
|
+
for (const [c0, c1] of covered) {
|
|
223
|
+
if (c0 > x) out.push([x, Math.min(c0, hi)]);
|
|
224
|
+
x = Math.max(x, c1);
|
|
225
|
+
if (x >= hi) break;
|
|
226
|
+
}
|
|
227
|
+
if (x < hi) out.push([x, hi]);
|
|
228
|
+
return out.filter(([a, b]) => b - a > EPS);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
|
|
233
|
+
* 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.
|
|
234
|
+
*/
|
|
235
|
+
function freeWay(ways: LevelWays, s: Node, t: Node, inset: number): Point[] | undefined {
|
|
236
|
+
const fs = freePlugs(ways, s, inset);
|
|
237
|
+
const ft = freePlugs(ways, t, inset);
|
|
238
|
+
const cs = center(s.box);
|
|
239
|
+
const ct = center(t.box);
|
|
240
|
+
const is = facingOrder(fs.plugs, cs, ct);
|
|
241
|
+
const it = facingOrder(ft.plugs, ct, cs);
|
|
242
|
+
const from = is.map((i) => fs.plugs[i]!);
|
|
243
|
+
const to = it.map((k) => ft.plugs[k]!);
|
|
244
|
+
const ra = is.map((i) => fs.reach[i]!);
|
|
245
|
+
const rb = it.map((k) => ft.reach[k]!);
|
|
246
|
+
// A pair of plugs with one or two clear legs between them gives the short way.
|
|
247
|
+
let best: Point[] | undefined;
|
|
248
|
+
let bestLength = Infinity;
|
|
249
|
+
from.forEach((ps, i) =>
|
|
250
|
+
to.forEach((pt, k) => {
|
|
251
|
+
const way = quickWay(ways, endsAt(ps, ra[i]!, pt, rb[k]!));
|
|
252
|
+
const length = way ? polylineLength(way) : Infinity;
|
|
253
|
+
if (length < bestLength) {
|
|
254
|
+
best = way;
|
|
255
|
+
bestLength = length;
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
if (best) return best;
|
|
260
|
+
// Else one search of the second corner graph finds the shortest way from a plug of `s` to a plug of `t`.
|
|
261
|
+
// The corners of the second graph are near the boxes, so the way can go through a gap that is narrower than the margin.
|
|
262
|
+
const a = from.map((ps, i) => ({ point: along(ps, ra[i]!), cost: ra[i]! }));
|
|
263
|
+
const b = to.map((pt, k) => ({ point: along(pt, rb[k]!), cost: rb[k]! }));
|
|
264
|
+
const found = ways.tight().pathAmong(a, b);
|
|
265
|
+
return found && pulled(ways, from[found.from]!, found.points, to[found.to]!);
|
|
266
|
+
}
|
|
267
|
+
|
|
132
268
|
/** The two ends of a link: each plug, and the point where the link turns away from the normal of the plug. */
|
|
133
269
|
interface Ends {
|
|
134
270
|
readonly ps: Plug;
|
|
@@ -143,8 +279,11 @@ interface Ends {
|
|
|
143
279
|
* Two plugs that face each other across a narrow space make the link turn in the middle of that space.
|
|
144
280
|
*/
|
|
145
281
|
function ends(ways: Ways, s: Node, ps: Plug, t: Node, pt: Plug): Ends {
|
|
146
|
-
|
|
147
|
-
|
|
282
|
+
return endsAt(ps, reach(ways, s, ps), pt, reach(ways, t, pt));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** This function gives the ends of a link from the plug `ps` to the plug `pt`, with the reach of each plug. */
|
|
286
|
+
function endsAt(ps: Plug, ra: number, pt: Plug, rb: number): Ends {
|
|
148
287
|
// Two plugs that face each other turn at the same distance, so the link does not go past itself.
|
|
149
288
|
if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
|
|
150
289
|
const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
|
|
@@ -182,14 +321,12 @@ function quickWay(ways: Ways, e: Ends): Point[] | undefined {
|
|
|
182
321
|
}
|
|
183
322
|
|
|
184
323
|
/**
|
|
185
|
-
* This function gives a way from plug
|
|
186
|
-
* Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
|
|
324
|
+
* This function gives a way from the plug `ps` through the points of `path` to the plug `pt`.
|
|
325
|
+
* 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.
|
|
187
326
|
*/
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const legs = squarePull(ways.corners.path(e.a, e.b) ?? [e.a, e.b], ways.obstacles, ways.margin / 2, axisOf(e.ps.normal), axisOf(e.pt.normal));
|
|
192
|
-
return tidy([e.ps.point, ...legs, e.pt.point]);
|
|
327
|
+
function pulled(ways: Ways, ps: Plug, path: readonly Point[], pt: Plug): Point[] {
|
|
328
|
+
const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
|
|
329
|
+
return tidy([ps.point, ...legs, pt.point]);
|
|
193
330
|
}
|
|
194
331
|
|
|
195
332
|
type Axis = "x" | "y";
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { center, type Box, type Point } from "../geom.ts";
|
|
2
|
+
import { levelLinks } from "../levels.ts";
|
|
3
|
+
import type { Edge, Graph, Router } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** An item with a box: a node of a graph, an item of a level or a box of a host layout. */
|
|
6
|
+
export interface Boxed {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly box: Box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A link between two boxes. For a bundled link, `count` is the number of its edges. The default count is 1. */
|
|
12
|
+
export interface RoomLink {
|
|
13
|
+
readonly source: string;
|
|
14
|
+
readonly target: string;
|
|
15
|
+
readonly count?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A gap of a board: a boundary of the sweep over the left or top edges of the boxes.
|
|
20
|
+
* On the axis `x`, the gap is a vertical channel between two columns. On the axis `y`, it is a horizontal channel between two rows.
|
|
21
|
+
*/
|
|
22
|
+
export interface Gap {
|
|
23
|
+
readonly axis: "x" | "y";
|
|
24
|
+
/** The place of the gap in the order of the gaps on its axis, from 0. */
|
|
25
|
+
readonly index: number;
|
|
26
|
+
/** The coordinate where the boxes after the gap start, on the axis of the gap. */
|
|
27
|
+
readonly at: number;
|
|
28
|
+
/** The ids of the boxes that end at the near side of the gap. */
|
|
29
|
+
readonly before: readonly string[];
|
|
30
|
+
/** The ids of the boxes that start at the far side of the gap, at `at`. */
|
|
31
|
+
readonly after: readonly string[];
|
|
32
|
+
/** The current width of the gap: the distance from the nearest box edge before the gap to `at`. */
|
|
33
|
+
readonly width: number;
|
|
34
|
+
/** The number of lanes that the links need in the gap. */
|
|
35
|
+
readonly lanes: number;
|
|
36
|
+
/** The width that the gap needs: at least the current width, and at least `gutter + lanes × spacing`. */
|
|
37
|
+
readonly need: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The ways that `roomDemand` finds the lanes of each gap. */
|
|
41
|
+
export type RoomMethod = "routes" | "lines";
|
|
42
|
+
|
|
43
|
+
/** The options of `roomDemand` and `makeRoom`. */
|
|
44
|
+
export interface RoomOptions {
|
|
45
|
+
/** The smallest width of each gap. The default is 8. */
|
|
46
|
+
readonly gutter?: number;
|
|
47
|
+
/** The width of one lane: the distance between two lines that run side by side. The default is 4. */
|
|
48
|
+
readonly spacing?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The largest growth of the board on each axis, as a share of its size. The default is `Infinity`.
|
|
51
|
+
* When the lanes need more, each gap gets its gutter, and the lanes share the rest in proportion.
|
|
52
|
+
*/
|
|
53
|
+
readonly maxGrowth?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The way to find the lanes. The default is `routes`.
|
|
56
|
+
* With `routes`, the function routes the links one time on the board with each gap open to the gutter.
|
|
57
|
+
* The lanes of a gap are the most routes that run along the gap side by side.
|
|
58
|
+
* With `lines`, the lanes of a gap are the straight lines between the centers of the boxes that cross the gap.
|
|
59
|
+
*/
|
|
60
|
+
readonly method?: RoomMethod;
|
|
61
|
+
/** The router of the method `routes`. Without it, the links go as the links of a zoom level (`levelLinks`), with the margin `gutter / 2`. */
|
|
62
|
+
readonly router?: Router;
|
|
63
|
+
/** With `count`, a link takes one lane for each of its edges. With `link`, the default, a link takes one lane. */
|
|
64
|
+
readonly lanes?: "link" | "count";
|
|
65
|
+
/** Two box edges nearer than this distance make one boundary of the sweep. The default is 0.5. */
|
|
66
|
+
readonly snap?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The demand of a board and the board with its gaps open. */
|
|
70
|
+
export interface Room<T extends Boxed> {
|
|
71
|
+
readonly gaps: readonly Gap[];
|
|
72
|
+
readonly items: T[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DEFAULT_GUTTER = 8;
|
|
76
|
+
const DEFAULT_SPACING = 4;
|
|
77
|
+
const DEFAULT_SNAP = 0.5;
|
|
78
|
+
|
|
79
|
+
type Axis = "x" | "y";
|
|
80
|
+
|
|
81
|
+
const lo = (b: Box, a: Axis): number => (a === "x" ? b.x : b.y);
|
|
82
|
+
const len = (b: Box, a: Axis): number => (a === "x" ? b.w : b.h);
|
|
83
|
+
const other = (a: Axis): Axis => (a === "x" ? "y" : "x");
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* This function gives the gaps of a board. A sweep over each axis sorts the left (or top) edges of the boxes.
|
|
87
|
+
* Each distinct edge after the first is a gap. The `lanes` of each gap are 0, and its `need` is its current width.
|
|
88
|
+
*/
|
|
89
|
+
export function boardGaps(items: readonly Boxed[], snap: number = DEFAULT_SNAP): Gap[] {
|
|
90
|
+
return [...axisGaps(items, "x", snap), ...axisGaps(items, "y", snap)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function axisGaps(items: readonly Boxed[], axis: Axis, snap: number): Gap[] {
|
|
94
|
+
const starts = [...new Set(items.map((i) => lo(i.box, axis)))].sort((u, v) => u - v);
|
|
95
|
+
const bounds: number[] = [];
|
|
96
|
+
for (const s of starts) if (bounds.length === 0 || s - bounds[bounds.length - 1]! > snap) bounds.push(s);
|
|
97
|
+
const after: string[][] = bounds.map(() => []);
|
|
98
|
+
for (const i of items) after[lastAtOrBefore(bounds, lo(i.box, axis) + snap)]!.push(i.id);
|
|
99
|
+
const ends = items.map((i) => ({ id: i.id, end: lo(i.box, axis) + len(i.box, axis) })).sort((u, v) => u.end - v.end);
|
|
100
|
+
const gaps: Gap[] = [];
|
|
101
|
+
let k = 0;
|
|
102
|
+
for (let n = 1; n < bounds.length; n++) {
|
|
103
|
+
const at = bounds[n]!;
|
|
104
|
+
// The nearest box edge before the gap is the largest end at or before the boundary.
|
|
105
|
+
while (k < ends.length && ends[k]!.end <= at + snap) k++;
|
|
106
|
+
const near = k > 0 ? ends[k - 1]!.end : at;
|
|
107
|
+
const before: string[] = [];
|
|
108
|
+
for (let j = k - 1; j >= 0 && ends[j]!.end >= near - snap; j--) before.push(ends[j]!.id);
|
|
109
|
+
gaps.push({ axis, index: n - 1, at, before: before.reverse(), after: after[n]!, width: Math.max(0, at - near), lanes: 0, need: Math.max(0, at - near) });
|
|
110
|
+
}
|
|
111
|
+
return gaps;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This function gives the index of the last value of the sorted list `v` that is at or before `x`, or 0. */
|
|
115
|
+
function lastAtOrBefore(v: readonly number[], x: number): number {
|
|
116
|
+
let a = 0;
|
|
117
|
+
let b = v.length - 1;
|
|
118
|
+
while (a < b) {
|
|
119
|
+
const m = (a + b + 1) >> 1;
|
|
120
|
+
if (v[m]! <= x) a = m;
|
|
121
|
+
else b = m - 1;
|
|
122
|
+
}
|
|
123
|
+
return a;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* This function moves the boxes apart, so that each gap gets the width `need`. The boxes keep their size, their order and their alignment.
|
|
128
|
+
* A box moves by the sum of the growth of each gap at or before its left (or top) edge. Thus a grid stays a grid.
|
|
129
|
+
* The function moves no box nearer to another box, so it makes no overlap.
|
|
130
|
+
*/
|
|
131
|
+
export function openGaps<T extends Boxed>(items: readonly T[], gaps: readonly Gap[], snap: number = DEFAULT_SNAP): T[] {
|
|
132
|
+
const shift = (axis: Axis): ((v: number) => number) => {
|
|
133
|
+
const list = gaps.filter((g) => g.axis === axis).sort((u, v) => u.at - v.at);
|
|
134
|
+
const at = list.map((g) => g.at);
|
|
135
|
+
const sum: number[] = [];
|
|
136
|
+
let s = 0;
|
|
137
|
+
for (const g of list) sum.push((s += Math.max(0, g.need - g.width)));
|
|
138
|
+
return (v) => (list.length === 0 || at[0]! > v + snap ? 0 : sum[lastAtOrBefore(at, v + snap)]!);
|
|
139
|
+
};
|
|
140
|
+
const sx = shift("x");
|
|
141
|
+
const sy = shift("y");
|
|
142
|
+
return items.map((i) => ({ ...i, box: { ...i.box, x: i.box.x + sx(i.box.x), y: i.box.y + sy(i.box.y) } }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This function gives the room that the links need in each gap of the board.
|
|
147
|
+
* The lanes of a gap come from the `method` of the options. The need of a gap is `gutter + lanes × spacing`, and not less than its current width.
|
|
148
|
+
*/
|
|
149
|
+
export function roomDemand(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions = {}): Gap[] {
|
|
150
|
+
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
151
|
+
const spacing = options.spacing ?? DEFAULT_SPACING;
|
|
152
|
+
const snap = options.snap ?? DEFAULT_SNAP;
|
|
153
|
+
const weight = (l: RoomLink): number => (options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1);
|
|
154
|
+
const gaps = boardGaps(items, snap);
|
|
155
|
+
const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
|
|
156
|
+
const raw = gaps.map((g, i) => ({ ...g, lanes: lanes[i]!, need: Math.max(g.width, gutter + lanes[i]! * spacing) }));
|
|
157
|
+
return capGrowth(items, raw, gutter, options.maxGrowth ?? Infinity);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** This function gives the demand of the board and the board with each gap open to its need. */
|
|
161
|
+
export function makeRoom<T extends Boxed>(items: readonly T[], links: readonly RoomLink[], options: RoomOptions = {}): Room<T> {
|
|
162
|
+
const gaps = roomDemand(items, links, options);
|
|
163
|
+
return { gaps, items: openGaps(items, gaps, options.snap ?? DEFAULT_SNAP) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** This function gives the links between the nodes of a graph, with the count of the edges of each pair. A loop does not count. */
|
|
167
|
+
export function roomLinks(graph: Graph): RoomLink[] {
|
|
168
|
+
const byPair = new Map<string, { source: string; target: string; count: number }>();
|
|
169
|
+
for (const e of graph.edges) {
|
|
170
|
+
const [s, t] = [e.source.node, e.target.node];
|
|
171
|
+
if (s === t) continue;
|
|
172
|
+
const key = s < t ? `${s}\u0000${t}` : `${t}\u0000${s}`;
|
|
173
|
+
const known = byPair.get(key);
|
|
174
|
+
if (known) known.count++;
|
|
175
|
+
else byPair.set(key, { source: s, target: t, count: 1 });
|
|
176
|
+
}
|
|
177
|
+
return [...byPair.values()];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** This function counts, for each gap, the straight lines between the centers of the boxes of a link that cross the gap. */
|
|
181
|
+
function lineLanes(items: readonly Boxed[], links: readonly RoomLink[], gaps: readonly Gap[], weight: (l: RoomLink) => number): number[] {
|
|
182
|
+
const byId = new Map(items.map((i) => [i.id, center(i.box)]));
|
|
183
|
+
return gaps.map((g) => {
|
|
184
|
+
let n = 0;
|
|
185
|
+
for (const l of links) {
|
|
186
|
+
const a = byId.get(l.source);
|
|
187
|
+
const b = byId.get(l.target);
|
|
188
|
+
if (a && b && Math.min(a[g.axis], b[g.axis]) < g.at && Math.max(a[g.axis], b[g.axis]) > g.at) n += weight(l);
|
|
189
|
+
}
|
|
190
|
+
return n;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* This function routes the links one time on the board with each gap open to the gutter.
|
|
196
|
+
* The lanes of a gap are the largest number of routes that run along the gap at one place.
|
|
197
|
+
* A piece of a route goes along a gap when it is in the channel of the gap.
|
|
198
|
+
* It must also go farther along the gap than across it.
|
|
199
|
+
*/
|
|
200
|
+
function routeLanes(
|
|
201
|
+
items: readonly Boxed[],
|
|
202
|
+
links: readonly RoomLink[],
|
|
203
|
+
gaps: readonly Gap[],
|
|
204
|
+
gutter: number,
|
|
205
|
+
snap: number,
|
|
206
|
+
weight: (l: RoomLink) => number,
|
|
207
|
+
router: Router | undefined,
|
|
208
|
+
): number[] {
|
|
209
|
+
const probeGaps = gaps.map((g) => ({ ...g, need: Math.max(g.width, gutter) }));
|
|
210
|
+
const probe = openGaps(items, probeGaps, snap);
|
|
211
|
+
const ways = probeWays(probe, links, gutter, router);
|
|
212
|
+
return probeGaps.map((g) => {
|
|
213
|
+
// The channel of the gap on the probe board: its far side moves with the growth of each gap up to this one.
|
|
214
|
+
let grow = 0;
|
|
215
|
+
for (const h of probeGaps) if (h.axis === g.axis && h.at <= g.at + snap) grow += h.need - h.width;
|
|
216
|
+
const hi = g.at + grow;
|
|
217
|
+
const lo = hi - g.need;
|
|
218
|
+
const spans: { from: number; to: number; w: number }[] = [];
|
|
219
|
+
ways.forEach((points, i) => {
|
|
220
|
+
const along = alongSpans(points, g.axis, lo, hi);
|
|
221
|
+
for (const [from, to] of merge(along)) spans.push({ from, to, w: weight(links[i]!) });
|
|
222
|
+
});
|
|
223
|
+
return density(spans);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** This function gives the points of the way of each link on the probe board, in the order of the links. */
|
|
228
|
+
function probeWays(items: readonly Boxed[], links: readonly RoomLink[], gutter: number, router: Router | undefined): (readonly Point[])[] {
|
|
229
|
+
if (router) {
|
|
230
|
+
const edges: Edge[] = links.map((l, i) => ({ id: `l${i}`, source: { node: l.source }, target: { node: l.target } }));
|
|
231
|
+
const routes = new Map(router.route({ nodes: items, edges }).routes.map((r) => [r.edge, r.points]));
|
|
232
|
+
return edges.map((e) => routes.get(e.id) ?? []);
|
|
233
|
+
}
|
|
234
|
+
const level = items.map((i) => ({ id: i.id, box: i.box, nodes: [i.id] }));
|
|
235
|
+
const pairs = links.map((l) => ({ source: l.source, target: l.target, count: l.count ?? 1 }));
|
|
236
|
+
// The margin puts the corners of the two sides of a gap on its middle line.
|
|
237
|
+
const out = levelLinks(level, pairs, { margin: Math.max(1, gutter / 2) });
|
|
238
|
+
const byPair = new Map(out.map((l) => [`${l.source}\u0000${l.target}`, l.points]));
|
|
239
|
+
return links.map((l) => byPair.get(`${l.source}\u0000${l.target}`) ?? []);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This function gives the spans along the axis of a gap where the polyline goes along the channel from `lo` to `hi`.
|
|
244
|
+
* The axis `x` gap is a vertical channel, so its spans are on the y axis.
|
|
245
|
+
*/
|
|
246
|
+
function alongSpans(p: readonly Point[], axis: Axis, lo: number, hi: number): [number, number][] {
|
|
247
|
+
const across = axis;
|
|
248
|
+
const along = other(axis);
|
|
249
|
+
const out: [number, number][] = [];
|
|
250
|
+
for (let i = 1; i < p.length; i++) {
|
|
251
|
+
const a = p[i - 1]!;
|
|
252
|
+
const b = p[i]!;
|
|
253
|
+
const da = b[across] - a[across];
|
|
254
|
+
// The part of the segment inside the channel, as the parameters t0 and t1 of the segment.
|
|
255
|
+
let t0 = 0;
|
|
256
|
+
let t1 = 1;
|
|
257
|
+
if (Math.abs(da) < 1e-9) {
|
|
258
|
+
if (a[across] < lo - 1e-6 || a[across] > hi + 1e-6) continue;
|
|
259
|
+
} else {
|
|
260
|
+
const u = (lo - a[across]) / da;
|
|
261
|
+
const v = (hi - a[across]) / da;
|
|
262
|
+
t0 = Math.max(0, Math.min(u, v));
|
|
263
|
+
t1 = Math.min(1, Math.max(u, v));
|
|
264
|
+
if (t1 <= t0) continue;
|
|
265
|
+
}
|
|
266
|
+
const s = a[along] + (b[along] - a[along]) * t0;
|
|
267
|
+
const e = a[along] + (b[along] - a[along]) * t1;
|
|
268
|
+
const run = Math.abs(e - s);
|
|
269
|
+
const cross = Math.abs(da) * (t1 - t0);
|
|
270
|
+
if (run > cross && run > 1e-6) out.push([Math.min(s, e), Math.max(s, e)]);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** This function joins the spans that overlap, so that one link counts one time at each place. */
|
|
276
|
+
function merge(spans: [number, number][]): [number, number][] {
|
|
277
|
+
spans.sort((u, v) => u[0] - v[0]);
|
|
278
|
+
const out: [number, number][] = [];
|
|
279
|
+
for (const s of spans) {
|
|
280
|
+
const last = out[out.length - 1];
|
|
281
|
+
if (last && s[0] <= last[1]) last[1] = Math.max(last[1], s[1]);
|
|
282
|
+
else out.push([s[0], s[1]]);
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** This function gives the largest sum of the weights of the spans that overlap at one place. Spans that only touch do not overlap. */
|
|
288
|
+
function density(spans: readonly { from: number; to: number; w: number }[]): number {
|
|
289
|
+
const events: [number, number][] = [];
|
|
290
|
+
for (const s of spans) events.push([s.from, s.w], [s.to, -s.w]);
|
|
291
|
+
// At one coordinate, the ends come before the starts.
|
|
292
|
+
events.sort((u, v) => u[0] - v[0] || u[1] - v[1]);
|
|
293
|
+
let n = 0;
|
|
294
|
+
let best = 0;
|
|
295
|
+
for (const [, w] of events) {
|
|
296
|
+
n += w;
|
|
297
|
+
best = Math.max(best, n);
|
|
298
|
+
}
|
|
299
|
+
return best;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* This function keeps the growth of the board on each axis at most `maxGrowth` times its size.
|
|
304
|
+
* Each gap keeps its gutter. The lanes of all gaps of the axis share the rest of the growth in proportion.
|
|
305
|
+
*/
|
|
306
|
+
function capGrowth(items: readonly Boxed[], gaps: Gap[], gutter: number, maxGrowth: number): Gap[] {
|
|
307
|
+
if (!Number.isFinite(maxGrowth)) return gaps;
|
|
308
|
+
const out = [...gaps];
|
|
309
|
+
for (const axis of ["x", "y"] as const) {
|
|
310
|
+
let x0 = Infinity;
|
|
311
|
+
let x1 = -Infinity;
|
|
312
|
+
for (const i of items) {
|
|
313
|
+
x0 = Math.min(x0, lo(i.box, axis));
|
|
314
|
+
x1 = Math.max(x1, lo(i.box, axis) + len(i.box, axis));
|
|
315
|
+
}
|
|
316
|
+
const budget = Math.max(0, maxGrowth) * Math.max(0, x1 - x0);
|
|
317
|
+
let base = 0;
|
|
318
|
+
let spare = 0;
|
|
319
|
+
for (const g of gaps) {
|
|
320
|
+
if (g.axis !== axis) continue;
|
|
321
|
+
const b = Math.max(0, gutter - g.width);
|
|
322
|
+
base += b;
|
|
323
|
+
spare += Math.max(0, g.need - g.width - b);
|
|
324
|
+
}
|
|
325
|
+
if (base + spare <= budget || spare === 0) continue;
|
|
326
|
+
const f = Math.max(0, (budget - base) / spare);
|
|
327
|
+
gaps.forEach((g, i) => {
|
|
328
|
+
if (g.axis !== axis) return;
|
|
329
|
+
const b = Math.max(0, gutter - g.width);
|
|
330
|
+
out[i] = { ...g, need: g.width + b + f * Math.max(0, g.need - g.width - b) };
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
|
+
import { center, dist, pointSegmentDistance, polylineLength, type Box, type Point } from "../geom.ts";
|
|
2
3
|
import { levelLinks, type ItemPair } from "../levels.ts";
|
|
3
4
|
import { nodeIndex, type Graph, type Level, type LevelItem, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
4
5
|
import { placePlug } from "../plugs.ts";
|
|
@@ -23,12 +24,20 @@ export interface SectionsOptions extends WayOptions, SectionOptions {
|
|
|
23
24
|
|
|
24
25
|
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
|
|
25
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
|
+
|
|
26
33
|
/**
|
|
27
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.
|
|
28
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.
|
|
29
36
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
30
37
|
* The edges in one section of the lowest level go straight around the nodes.
|
|
31
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
|
+
*
|
|
32
41
|
* The result gives one zoom level for each depth of the tree.
|
|
33
42
|
* A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
|
|
34
43
|
*/
|
|
@@ -66,6 +75,20 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
66
75
|
}
|
|
67
76
|
return h;
|
|
68
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
|
+
};
|
|
69
92
|
const up = (s: Section): Section[] => {
|
|
70
93
|
const out: Section[] = [];
|
|
71
94
|
for (let x: Section | undefined = s; x; x = parent.get(x)) out.push(x);
|
|
@@ -111,20 +134,24 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
111
134
|
const lca = a.find((x) => common.has(x))!;
|
|
112
135
|
const keep = options.hubs ?? Infinity;
|
|
113
136
|
const side = (list: Section[]): Section[] => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
114
|
-
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
|
+
}
|
|
115
144
|
const out: Point[] = [ps.point];
|
|
116
145
|
const append = (p: readonly Point[]): void => {
|
|
117
146
|
for (const q of p) if (dist(q, out[out.length - 1]!) > 1e-9) out.push(q);
|
|
118
147
|
};
|
|
119
148
|
const used: string[] = [];
|
|
120
|
-
const start = ways.out(ps);
|
|
121
149
|
append(piece(at(start), start, chain[0]!.id, hub(chain[0]!), used));
|
|
122
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));
|
|
123
|
-
const end = ways.out(pt);
|
|
124
151
|
append(piece(chain[chain.length - 1]!.id, hub(chain[chain.length - 1]!), at(end), end, used));
|
|
125
152
|
append([pt.point]);
|
|
126
|
-
// A way through the hubs that is too long gives way to the direct way.
|
|
127
|
-
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)) {
|
|
128
155
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
129
156
|
continue;
|
|
130
157
|
}
|