@expofp/wayfinding-compiler 0.0.0-bootstrap.0 → 0.0.0-experimental.3b0db16
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/package.json +2 -2
- package/dist/lib/centering.d.ts +0 -14
- package/dist/lib/centering.js +0 -88
- package/dist/lib/grid-network.d.ts +0 -20
- package/dist/lib/grid-network.js +0 -349
- package/dist/lib/lattice-map.d.ts +0 -19
- package/dist/lib/lattice-map.js +0 -92
- package/dist/lib/lattice-network.d.ts +0 -32
- package/dist/lib/lattice-network.js +0 -289
- package/dist/lib/network-setup.d.ts +0 -50
- package/dist/lib/network-setup.js +0 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/wayfinding-compiler",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-experimental.3b0db16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ExpoFP SDK internal: compiles raw plan geometry into a wayfinding routing graph",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"clipper2-ts": "2.0.1-18",
|
|
26
26
|
"tslib": "^2.3.0",
|
|
27
|
-
"@expofp/wayfinding-geometry": "0.0.0-
|
|
27
|
+
"@expofp/wayfinding-geometry": "0.0.0-experimental.3b0db16"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
package/dist/lib/centering.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How far off the middle of its corridor each cell stands: 0 on the middle row, rising to 1 at the
|
|
3
|
-
* walls — `1 - d / dmax`, where `d` is the cell's clearance and `dmax` the clearance of the ridge the
|
|
4
|
-
* cell climbs to, which is half the corridor's width there. A corridor an even number of cells wide
|
|
5
|
-
* has two middle rows; the one with the greater index is charged half the step to the next row, so
|
|
6
|
-
* the minimum is one row.
|
|
7
|
-
* @param width Grid width in cells.
|
|
8
|
-
* @param height Grid height in cells.
|
|
9
|
-
* @param clearance Meters from each cell to the nearest blocker, row-major.
|
|
10
|
-
* @param ok 1 for a walkable cell.
|
|
11
|
-
* @returns The offset per cell; 0 for a cell that is not walkable.
|
|
12
|
-
*/
|
|
13
|
-
export declare function offCenter(width: number, height: number, clearance: Float32Array, ok: Uint8Array): Float32Array;
|
|
14
|
-
//# sourceMappingURL=centering.d.ts.map
|
package/dist/lib/centering.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// Centering: how far off the middle of its corridor each router cell stands, read off the clearance
|
|
2
|
-
// field alone. The middle is one row of cells, so two paths along one corridor lie on one line.
|
|
3
|
-
/** Clearances are compared by the millimeter: chamfer sums differ by float noise only below it. */
|
|
4
|
-
const QUANTUM = 1000;
|
|
5
|
-
/**
|
|
6
|
-
* How far off the middle of its corridor each cell stands: 0 on the middle row, rising to 1 at the
|
|
7
|
-
* walls — `1 - d / dmax`, where `d` is the cell's clearance and `dmax` the clearance of the ridge the
|
|
8
|
-
* cell climbs to, which is half the corridor's width there. A corridor an even number of cells wide
|
|
9
|
-
* has two middle rows; the one with the greater index is charged half the step to the next row, so
|
|
10
|
-
* the minimum is one row.
|
|
11
|
-
* @param width Grid width in cells.
|
|
12
|
-
* @param height Grid height in cells.
|
|
13
|
-
* @param clearance Meters from each cell to the nearest blocker, row-major.
|
|
14
|
-
* @param ok 1 for a walkable cell.
|
|
15
|
-
* @returns The offset per cell; 0 for a cell that is not walkable.
|
|
16
|
-
*/
|
|
17
|
-
export function offCenter(width, height, clearance, ok) {
|
|
18
|
-
const cells = width * height;
|
|
19
|
-
const level = new Int32Array(cells);
|
|
20
|
-
let top = 0;
|
|
21
|
-
for (let cell = 0; cell < cells; cell++) {
|
|
22
|
-
if (!ok[cell])
|
|
23
|
-
continue;
|
|
24
|
-
level[cell] = Math.round(clearance[cell] * QUANTUM);
|
|
25
|
-
top = Math.max(top, level[cell]);
|
|
26
|
-
}
|
|
27
|
-
// Cells from the highest clearance down, index order among equals: a counting sort.
|
|
28
|
-
const start = new Int32Array(top + 2);
|
|
29
|
-
for (let cell = 0; cell < cells; cell++)
|
|
30
|
-
if (ok[cell])
|
|
31
|
-
start[top - level[cell] + 1]++;
|
|
32
|
-
for (let i = 1; i < start.length; i++)
|
|
33
|
-
start[i] += start[i - 1];
|
|
34
|
-
const order = new Int32Array(start[top + 1]);
|
|
35
|
-
for (let cell = 0; cell < cells; cell++)
|
|
36
|
-
if (ok[cell])
|
|
37
|
-
order[start[top - level[cell]]++] = cell;
|
|
38
|
-
// Each cell climbs to its highest neighbor, and takes the ridge that one reached.
|
|
39
|
-
const ridge = new Int32Array(cells);
|
|
40
|
-
for (const cell of order) {
|
|
41
|
-
const x = cell % width;
|
|
42
|
-
const y = (cell - x) / width;
|
|
43
|
-
let up = -1;
|
|
44
|
-
for (let dy = -1; dy <= 1; dy++)
|
|
45
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
46
|
-
const nx = x + dx;
|
|
47
|
-
const ny = y + dy;
|
|
48
|
-
if ((!dx && !dy) || nx < 0 || ny < 0 || nx >= width || ny >= height)
|
|
49
|
-
continue;
|
|
50
|
-
const near = ny * width + nx;
|
|
51
|
-
if (ok[near] && level[near] > level[cell] && (up < 0 || level[near] > level[up]))
|
|
52
|
-
up = near;
|
|
53
|
-
}
|
|
54
|
-
ridge[cell] = up < 0 ? level[cell] : ridge[up];
|
|
55
|
-
}
|
|
56
|
-
const off = new Float32Array(cells);
|
|
57
|
-
// Whether the cell `step` away from `cell`, at `at` along its row or column, stands lower or is no floor.
|
|
58
|
-
const lower = (cell, at, limit, step) => at < 0 || at >= limit || !ok[cell + step] || level[cell + step] < level[cell];
|
|
59
|
-
/**
|
|
60
|
-
* What the second of two equal rows across a corridor is charged: half the step to the next row.
|
|
61
|
-
* @param cell The cell.
|
|
62
|
-
* @param at Its place along the axis across the corridor.
|
|
63
|
-
* @param limit The grid's size along that axis.
|
|
64
|
-
* @param step The index step along that axis.
|
|
65
|
-
* @returns The charge, 0 when the cell is not such a row's.
|
|
66
|
-
*/
|
|
67
|
-
const secondRow = (cell, at, limit, step) => {
|
|
68
|
-
const twin = cell - step;
|
|
69
|
-
if (at < 1 || !ok[twin] || level[twin] !== level[cell])
|
|
70
|
-
return 0;
|
|
71
|
-
if (!lower(cell, at + 1, limit, step) || !lower(twin, at - 2, limit, -step))
|
|
72
|
-
return 0;
|
|
73
|
-
const next = at + 1 < limit && ok[cell + step] ? level[cell + step] : 0;
|
|
74
|
-
return (0.5 * (level[cell] - next)) / ridge[cell];
|
|
75
|
-
};
|
|
76
|
-
for (const cell of order) {
|
|
77
|
-
if (!ridge[cell])
|
|
78
|
-
continue;
|
|
79
|
-
const x = cell % width;
|
|
80
|
-
const y = (cell - x) / width;
|
|
81
|
-
off[cell] =
|
|
82
|
-
1 -
|
|
83
|
-
level[cell] / ridge[cell] +
|
|
84
|
-
secondRow(cell, x, width, 1) +
|
|
85
|
-
secondRow(cell, y, height, width);
|
|
86
|
-
}
|
|
87
|
-
return off;
|
|
88
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Ring } from './geom/types.js';
|
|
2
|
-
import type { PreparedWalls } from './geom/walls.js';
|
|
3
|
-
import type { RouteNetwork, RouteNetworkDestination } from './graph-types.js';
|
|
4
|
-
import type { Opening } from './openings.js';
|
|
5
|
-
/**
|
|
6
|
-
* Builds the grid route network for one layer.
|
|
7
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
8
|
-
* @param destinations Footprints the network must reach.
|
|
9
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
10
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
11
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
12
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
13
|
-
* @param shortcuts Whether links are cut into the tree where it sends a route the long way round.
|
|
14
|
-
* @returns Paths, docks and stats for `emit-v1`.
|
|
15
|
-
*/
|
|
16
|
-
export declare function buildGridNetwork(walkableRings: readonly Ring[], destinations: readonly RouteNetworkDestination[], walls: readonly (readonly number[])[], prepared?: PreparedWalls, drawn?: readonly {
|
|
17
|
-
readonly id: string;
|
|
18
|
-
readonly ring: Ring;
|
|
19
|
-
}[], openings?: readonly Opening[], shortcuts?: boolean): RouteNetwork;
|
|
20
|
-
//# sourceMappingURL=grid-network.d.ts.map
|
package/dist/lib/grid-network.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
// The "grid" route-network generator: one tree of cheapest paths over the plan's raster. A stub per
|
|
2
|
-
// open face ends on a router cell; a single growing search then joins the tips, nearest first, each
|
|
3
|
-
// path laid along the cell centers and free to walk from then on. What shapes the lines is the cost
|
|
4
|
-
// of a cell alone — a corridor's middle row is the cheapest, a turn costs a length — and a staircase
|
|
5
|
-
// along one corridor is cut to a straight line. On request, links are cut into the tree where it
|
|
6
|
-
// sends a route the long way round. Produces the {@link RouteNetwork} for `emit-v1`.
|
|
7
|
-
import { crossesBlocker } from './blockers.js';
|
|
8
|
-
import { createConnector } from './connect.js';
|
|
9
|
-
import { emitNetwork, turnDoorwayDocks } from './emit-network.js';
|
|
10
|
-
import { dockFor, followTips, tipPoints } from './entries.js';
|
|
11
|
-
import { floorGrids } from './floor-grids.js';
|
|
12
|
-
import { POINT_EPS, pointKey } from './geom/point-key.js';
|
|
13
|
-
import { pointSegmentDistance } from './geom/primitives.js';
|
|
14
|
-
import { chainSegments, chainsFromEdges } from './network-graph.js';
|
|
15
|
-
import { setUpNetwork } from './network-setup.js';
|
|
16
|
-
import { MIN_PASS } from './network-sizes.js';
|
|
17
|
-
import { NetworkStore } from './network-store.js';
|
|
18
|
-
import { shortcutDetours } from './post-passes.js';
|
|
19
|
-
import { makeRouter } from './router.js';
|
|
20
|
-
import { planStubs, sideOf } from './stubs.js';
|
|
21
|
-
/** What a cell at a corridor's wall costs over one on its middle row, as a share of its length. */
|
|
22
|
-
const CENTERING = 1;
|
|
23
|
-
/** The sine of the angle under which a stub counts as running along a line rather than across it. */
|
|
24
|
-
const ALONG = 0.2;
|
|
25
|
-
/**
|
|
26
|
-
* Cuts each staircase to a straight line: along a chain, the farthest vertex a free straight line
|
|
27
|
-
* reaches while every vertex between stays within a lane of it. A stub's tip on the staircase moves
|
|
28
|
-
* along the stub onto the line, so a corridor between two meetings is one line however many stubs
|
|
29
|
-
* end on it; where a dock from there would be unlawful the tip stays, and the line ends at it. A
|
|
30
|
-
* corner with legs longer than the lane stays a corner.
|
|
31
|
-
* @param store The network, every meeting a node; rebuilt in place.
|
|
32
|
-
* @param whiskers The attached stubs; a tip on a cut staircase moves onto the line.
|
|
33
|
-
* @param lane How far off the straight line a staircase's vertices may stand.
|
|
34
|
-
* @param free Whether a straight line may be laid.
|
|
35
|
-
* @param lawful Whether a stub may dock from a point.
|
|
36
|
-
*/
|
|
37
|
-
function cutStairs(store, whiskers, lane, free, lawful) {
|
|
38
|
-
const tipsAt = new Map();
|
|
39
|
-
for (const stub of whiskers) {
|
|
40
|
-
const key = pointKey(stub.tx, stub.ty);
|
|
41
|
-
tipsAt.set(key, [...(tipsAt.get(key) ?? []), stub]);
|
|
42
|
-
}
|
|
43
|
-
const chains = chainsFromEdges(store.toArray(), new Set(tipsAt.keys()));
|
|
44
|
-
for (const chain of chains) {
|
|
45
|
-
const { pts } = chain;
|
|
46
|
-
const kept = [pts[0]];
|
|
47
|
-
/**
|
|
48
|
-
* Reads the vertices between two of the chain's as a staircase.
|
|
49
|
-
* @param from Index of the vertex the line starts at.
|
|
50
|
-
* @param to Index of the vertex it ends at.
|
|
51
|
-
* @returns The tips between them moved onto the line, or null when a line may not replace them.
|
|
52
|
-
*/
|
|
53
|
-
const stairs = (from, to) => {
|
|
54
|
-
const [ax, ay] = pts[from];
|
|
55
|
-
const [bx, by] = pts[to];
|
|
56
|
-
const dx = bx - ax;
|
|
57
|
-
const dy = by - ay;
|
|
58
|
-
const length = Math.hypot(dx, dy);
|
|
59
|
-
const moved = [];
|
|
60
|
-
// A chain that closes on itself has no line between its ends.
|
|
61
|
-
if (length < POINT_EPS)
|
|
62
|
-
return null;
|
|
63
|
-
for (let k = from + 1; k < to; k++)
|
|
64
|
-
if (pointSegmentDistance(pts[k][0], pts[k][1], ax, ay, bx, by) > lane)
|
|
65
|
-
return null;
|
|
66
|
-
for (let k = from + 1; k < to; k++) {
|
|
67
|
-
const [px, py] = pts[k];
|
|
68
|
-
for (const stub of tipsAt.get(pointKey(px, py)) ?? []) {
|
|
69
|
-
// Where the stub's own line meets the straight line; a stub running along the line keeps
|
|
70
|
-
// its place along it.
|
|
71
|
-
const cross = dx * stub.ny - dy * stub.nx;
|
|
72
|
-
const along = Math.abs(cross) > ALONG * length
|
|
73
|
-
? ((stub.mx - ax) * stub.ny - (stub.my - ay) * stub.nx) / cross
|
|
74
|
-
: ((px - ax) * dx + (py - ay) * dy) / (length * length);
|
|
75
|
-
const at = [+(ax + along * dx).toFixed(2), +(ay + along * dy).toFixed(2)];
|
|
76
|
-
const run = (at[0] - stub.mx) * stub.nx + (at[1] - stub.my) * stub.ny;
|
|
77
|
-
if (along < 0 || along > 1 || run < MIN_PASS || !lawful(stub, at))
|
|
78
|
-
return null;
|
|
79
|
-
moved.push({ stub, along, at });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return free([ax, ay, bx, by]) ? moved : null;
|
|
83
|
-
};
|
|
84
|
-
// The edge to the next vertex is the network's own: it stays as it is.
|
|
85
|
-
const cut = (from, to) => to > from + 1 ? stairs(from, to) : [];
|
|
86
|
-
let from = 0;
|
|
87
|
-
while (from < pts.length - 1) {
|
|
88
|
-
let to = pts.length - 1;
|
|
89
|
-
let moved = cut(from, to);
|
|
90
|
-
while (!moved)
|
|
91
|
-
moved = cut(from, --to);
|
|
92
|
-
for (const { stub, at } of moved.sort((a, b) => a.along - b.along)) {
|
|
93
|
-
[stub.tx, stub.ty] = at;
|
|
94
|
-
kept.push([at[0], at[1]]);
|
|
95
|
-
}
|
|
96
|
-
kept.push(pts[to]);
|
|
97
|
-
from = to;
|
|
98
|
-
}
|
|
99
|
-
chain.pts = kept;
|
|
100
|
-
}
|
|
101
|
-
store.reset(chainSegments(chains));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Builds the grid route network for one layer.
|
|
105
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
106
|
-
* @param destinations Footprints the network must reach.
|
|
107
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
108
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
109
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
110
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
111
|
-
* @param shortcuts Whether links are cut into the tree where it sends a route the long way round.
|
|
112
|
-
* @returns Paths, docks and stats for `emit-v1`.
|
|
113
|
-
*/
|
|
114
|
-
export function buildGridNetwork(walkableRings, destinations, walls, prepared, drawn = [], openings = [], shortcuts = false) {
|
|
115
|
-
const { cfg, wallLines, doors, owners, opened, floor, unservedFloorPieces, doorSides, blockers, destinationOf, failed, note, } = setUpNetwork(walkableRings, destinations, walls, prepared, drawn, openings);
|
|
116
|
-
// One grid, the plan's: a hall turned its own way is walked on it too.
|
|
117
|
-
const { grids, frames, frameOfDest } = floorGrids(floor, doorSides, destinations, wallLines, doors, cfg, false);
|
|
118
|
-
const ctx = grids[0].ctx;
|
|
119
|
-
const router = makeRouter(grids, cfg, CENTERING);
|
|
120
|
-
const store = new NetworkStore();
|
|
121
|
-
const build = { cfg, frames, blockers, destinationOf, note };
|
|
122
|
-
const mayLay = (seg) => ctx.lineFree(seg[0], seg[1], seg[2], seg[3]) && !crossesBlocker(blockers, seg);
|
|
123
|
-
/* ---- stubs: one per open side of each destination, as the steiner network plans them ---- */
|
|
124
|
-
const plan = planStubs({ ...build, destinations, owners, ctx, frameOfDest, opened });
|
|
125
|
-
const whiskers = [];
|
|
126
|
-
const anchors = [];
|
|
127
|
-
/** Sides of each destination a whisker already serves. */
|
|
128
|
-
const servedSides = new Map();
|
|
129
|
-
const attach = (stub) => {
|
|
130
|
-
whiskers.push(stub);
|
|
131
|
-
if (stub.kind === 'face')
|
|
132
|
-
servedSides.set(stub.id, (servedSides.get(stub.id) ?? new Set()).add(sideOf(stub.way)));
|
|
133
|
-
};
|
|
134
|
-
/**
|
|
135
|
-
* Moves a stub's tip onto a router cell: the planned tip's, or one a step nearer or farther along
|
|
136
|
-
* the stub — the cheapest to stand on from which the dock is lawful, the planned one among equals.
|
|
137
|
-
* Where the floor around the planned tip is too tight to walk, the nearest such cell along the
|
|
138
|
-
* stub, toward the foot first.
|
|
139
|
-
* @param stub The stub; its tip moves to the cell's center.
|
|
140
|
-
* @returns The cell, or -1 when the stub has none (noted).
|
|
141
|
-
*/
|
|
142
|
-
const seat = (stub) => {
|
|
143
|
-
const destination = destinationOf.get(stub.id);
|
|
144
|
-
const farthest = Math.min(stub.way.depth - MIN_PASS, stub.reach + 2 * cfg.stub);
|
|
145
|
-
let onFloor = false;
|
|
146
|
-
/**
|
|
147
|
-
* The cell a number of steps along the stub from its planned tip, when a dock from it is lawful.
|
|
148
|
-
* @param steps Cells along the stub, toward the foot when negative.
|
|
149
|
-
* @returns The cell, or -1.
|
|
150
|
-
*/
|
|
151
|
-
const cellAt = (steps) => {
|
|
152
|
-
const run = steps * router.pitch;
|
|
153
|
-
if (stub.reach + run < MIN_PASS || stub.reach + run > farthest)
|
|
154
|
-
return -1;
|
|
155
|
-
const cell = router.cellIn(stub.tx + stub.nx * run, stub.ty + stub.ny * run, frames[0]);
|
|
156
|
-
if (cell < 0 || !router.ok[cell])
|
|
157
|
-
return -1;
|
|
158
|
-
onFloor = true;
|
|
159
|
-
return dockFor(destination, stub.way, router.worldOf(cell), blockers) ? cell : -1;
|
|
160
|
-
};
|
|
161
|
-
let best = cellAt(0);
|
|
162
|
-
for (const steps of [-1, 1]) {
|
|
163
|
-
const cell = cellAt(steps);
|
|
164
|
-
if (cell >= 0 && (best < 0 || router.stepCost[cell] < router.stepCost[best]))
|
|
165
|
-
best = cell;
|
|
166
|
-
}
|
|
167
|
-
for (const sign of [-1, 1])
|
|
168
|
-
for (let steps = 2; best < 0 && Math.abs(steps * router.pitch) <= farthest; steps++)
|
|
169
|
-
best = cellAt(sign * steps);
|
|
170
|
-
if (best < 0) {
|
|
171
|
-
note(stub.id, onFloor ? 'way-in-blocked' : 'no-floor', { at: [stub.tx, stub.ty] });
|
|
172
|
-
return -1;
|
|
173
|
-
}
|
|
174
|
-
[stub.tx, stub.ty] = router.worldOf(best);
|
|
175
|
-
anchors.push({ id: stub.id, at: [stub.tx, stub.ty] });
|
|
176
|
-
return best;
|
|
177
|
-
};
|
|
178
|
-
const around = Math.round(cfg.stub / router.pitch);
|
|
179
|
-
/**
|
|
180
|
-
* How much floor lies around a cell: the walkable cells within a stub's length of it.
|
|
181
|
-
* @param cell The cell.
|
|
182
|
-
* @returns The count.
|
|
183
|
-
*/
|
|
184
|
-
const roomAround = (cell) => {
|
|
185
|
-
const [x, y] = router.worldOf(cell);
|
|
186
|
-
let room = 0;
|
|
187
|
-
for (let dy = -around; dy <= around; dy++)
|
|
188
|
-
for (let dx = -around; dx <= around; dx++) {
|
|
189
|
-
const near = router.cellIn(x + dx * router.pitch, y + dy * router.pitch, frames[0]);
|
|
190
|
-
if (near >= 0 && router.ok[near])
|
|
191
|
-
room++;
|
|
192
|
-
}
|
|
193
|
-
return room;
|
|
194
|
-
};
|
|
195
|
-
/* ---- the tree: every tip joined to the network laid so far, nearest first ---- */
|
|
196
|
-
/** 1 for each cell a laid path passes. */
|
|
197
|
-
const laid = new Uint8Array(router.cells);
|
|
198
|
-
const network = [];
|
|
199
|
-
/** Stubs whose tip stands on floor no other tip shares. */
|
|
200
|
-
const stranded = [];
|
|
201
|
-
let trees = 0;
|
|
202
|
-
/**
|
|
203
|
-
* One growth over the stubs: each tip is joined by the cheapest path from the network laid so
|
|
204
|
-
* far; with no network, or floor it cannot reach, a tree starts from the roomiest tip left.
|
|
205
|
-
* @param stubs The stubs to join.
|
|
206
|
-
* @returns The stubs that could not be: no cell to stand on, or alone on their floor.
|
|
207
|
-
*/
|
|
208
|
-
const grow = (stubs) => {
|
|
209
|
-
const group = new Int32Array(router.cells).fill(-1);
|
|
210
|
-
const targets = [];
|
|
211
|
-
const lost = [];
|
|
212
|
-
for (const stub of stubs) {
|
|
213
|
-
const cell = seat(stub);
|
|
214
|
-
if (cell < 0) {
|
|
215
|
-
lost.push(stub);
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
// Already on a laid line: the tip becomes a node of it.
|
|
219
|
-
if (laid[cell]) {
|
|
220
|
-
store.splitAt(store.segmentAt(stub.tx, stub.ty), stub.tx, stub.ty);
|
|
221
|
-
attach(stub);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (group[cell] < 0) {
|
|
225
|
-
group[cell] = targets.length;
|
|
226
|
-
targets.push({ cell, stubs: [], room: roomAround(cell) });
|
|
227
|
-
}
|
|
228
|
-
targets[group[cell]].stubs.push(stub);
|
|
229
|
-
}
|
|
230
|
-
// Trees start from the tip with most floor around it; the stubs' own order among equals.
|
|
231
|
-
const order = targets
|
|
232
|
-
.map((_, index) => index)
|
|
233
|
-
.sort((a, b) => targets[b].room - targets[a].room || a - b);
|
|
234
|
-
/** The target a tree started from, by its cell, while nothing has joined it. */
|
|
235
|
-
const lone = new Map();
|
|
236
|
-
let next = 0;
|
|
237
|
-
const reseed = () => {
|
|
238
|
-
while (next < order.length && group[targets[order[next]].cell] < 0)
|
|
239
|
-
next++;
|
|
240
|
-
if (next >= order.length)
|
|
241
|
-
return null;
|
|
242
|
-
const { cell } = targets[order[next]];
|
|
243
|
-
group[cell] = -1;
|
|
244
|
-
lone.set(cell, order[next]);
|
|
245
|
-
return [cell];
|
|
246
|
-
};
|
|
247
|
-
router.grow(network.length ? network : (reseed() ?? []), group, reseed, (cells, reached) => {
|
|
248
|
-
const root = cells[cells.length - 1];
|
|
249
|
-
const seed = lone.get(root);
|
|
250
|
-
// The first path of a tree: the tip it started from is on the network now.
|
|
251
|
-
if (seed !== undefined) {
|
|
252
|
-
lone.delete(root);
|
|
253
|
-
trees++;
|
|
254
|
-
targets[seed].stubs.forEach(attach);
|
|
255
|
-
}
|
|
256
|
-
for (const [x0, y0, x1, y1] of router.segmentsAlong(cells))
|
|
257
|
-
store.add(x0, y0, x1, y1);
|
|
258
|
-
for (const cell of cells) {
|
|
259
|
-
if (laid[cell])
|
|
260
|
-
continue;
|
|
261
|
-
laid[cell] = 1;
|
|
262
|
-
network.push(cell);
|
|
263
|
-
}
|
|
264
|
-
targets[reached].stubs.forEach(attach);
|
|
265
|
-
return [];
|
|
266
|
-
});
|
|
267
|
-
for (const target of lone.values())
|
|
268
|
-
for (const stub of targets[target].stubs) {
|
|
269
|
-
note(stub.id, 'cut-off', { at: [stub.tx, stub.ty] });
|
|
270
|
-
stranded.push(stub);
|
|
271
|
-
lost.push(stub);
|
|
272
|
-
}
|
|
273
|
-
return lost;
|
|
274
|
-
};
|
|
275
|
-
let pending = plan.stubs;
|
|
276
|
-
// A stub that could not be joined is retried from its destination's next way in.
|
|
277
|
-
while (pending.length)
|
|
278
|
-
pending = grow(pending).flatMap((stub) => plan.retry(stub, servedSides, whiskers) ?? []);
|
|
279
|
-
// No network at all (a plan with one destination): a stub's own run is its line.
|
|
280
|
-
if (!store.size) {
|
|
281
|
-
const own = stranded.find((stub) => mayLay([stub.mx, stub.my, stub.tx, stub.ty]));
|
|
282
|
-
if (own) {
|
|
283
|
-
store.add(own.mx, own.my, own.tx, own.ty);
|
|
284
|
-
attach(own);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
followTips(whiskers, store.node(tipPoints(whiskers)));
|
|
288
|
-
/* ---- shortcuts: links across where the tree sends a route the long way round ---- */
|
|
289
|
-
let linked = 0;
|
|
290
|
-
if (shortcuts) {
|
|
291
|
-
const connector = createConnector(store, router, cfg, {
|
|
292
|
-
frames,
|
|
293
|
-
axes: [],
|
|
294
|
-
doors,
|
|
295
|
-
blockers,
|
|
296
|
-
mayLay,
|
|
297
|
-
});
|
|
298
|
-
const tree = store.toArray();
|
|
299
|
-
// Through the connector, so a link ends on the line that claims its last cell.
|
|
300
|
-
store.reset([]);
|
|
301
|
-
for (const seg of tree)
|
|
302
|
-
connector.add(seg);
|
|
303
|
-
linked = shortcutDetours(store, router, connector);
|
|
304
|
-
followTips(whiskers, store.node(tipPoints(whiskers)));
|
|
305
|
-
}
|
|
306
|
-
/* ---- straighten: every meeting a node, then each staircase one line ---- */
|
|
307
|
-
cutStairs(store, whiskers, cfg.stub / 2,
|
|
308
|
-
// Over cells the router walks, so the line keeps a person's clearance.
|
|
309
|
-
(seg) => mayLay(seg) && router.cellsAlong(...seg).every((cell) => router.ok[cell] === 1), (stub, at) => dockFor(destinationOf.get(stub.id), stub.way, at, blockers) !== null);
|
|
310
|
-
turnDoorwayDocks(whiskers, build, store);
|
|
311
|
-
/* ---- emit the RouteNetwork artifact ---- */
|
|
312
|
-
const emitted = emitNetwork({ ...build, store, whiskers, destinations, failed, doors });
|
|
313
|
-
const ways = [];
|
|
314
|
-
for (const [id, list] of plan.ways)
|
|
315
|
-
for (const way of list)
|
|
316
|
-
ways.push({ id, kind: way.kind, foot: [way.mx, way.my], normal: [way.nx, way.ny] });
|
|
317
|
-
return {
|
|
318
|
-
paths: emitted.paths,
|
|
319
|
-
docks: emitted.docks,
|
|
320
|
-
anatomy: {
|
|
321
|
-
doors: doors.map((door) => ({
|
|
322
|
-
gap: [
|
|
323
|
-
[door.gap[0], door.gap[1]],
|
|
324
|
-
[door.gap[2], door.gap[3]],
|
|
325
|
-
],
|
|
326
|
-
mid: [door.mid[0], door.mid[1]],
|
|
327
|
-
})),
|
|
328
|
-
anchors,
|
|
329
|
-
ways,
|
|
330
|
-
whiskers: whiskers.map((stub) => ({
|
|
331
|
-
id: stub.id,
|
|
332
|
-
kind: stub.kind,
|
|
333
|
-
foot: [stub.mx, stub.my],
|
|
334
|
-
tip: [stub.tx, stub.ty],
|
|
335
|
-
})),
|
|
336
|
-
frames: [],
|
|
337
|
-
loose: [],
|
|
338
|
-
axes: [],
|
|
339
|
-
},
|
|
340
|
-
stats: {
|
|
341
|
-
unreached: emitted.unreached,
|
|
342
|
-
components: emitted.components,
|
|
343
|
-
doorways: doors.length,
|
|
344
|
-
shortcuts: linked,
|
|
345
|
-
...(trees > 1 && { untiedPieces: trees - 1 }),
|
|
346
|
-
...(unservedFloorPieces > 0 && { unservedFloorPieces }),
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Lattice, RouteNetworkDock } from './graph-types.js';
|
|
2
|
-
import type { WfGridFloor } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Bytes packed as runs: the value, then the run's length as a little-endian base-128 number, and
|
|
5
|
-
* the whole as base64. A floor's map is long stretches of one value.
|
|
6
|
-
* @param bytes The bytes.
|
|
7
|
-
* @returns The packed text.
|
|
8
|
-
*/
|
|
9
|
-
export declare function packRuns(bytes: Uint8Array): string;
|
|
10
|
-
/**
|
|
11
|
-
* One floor's lattice and docks as the map handed to the runtime, in plan units.
|
|
12
|
-
* @param layer The floor's layer id.
|
|
13
|
-
* @param lattice The lattice, in meters.
|
|
14
|
-
* @param docks The docks leaving it, in meters, in the lattice's own order.
|
|
15
|
-
* @param unitsPerMeter Plan coordinates in one meter.
|
|
16
|
-
* @returns The floor's map.
|
|
17
|
-
*/
|
|
18
|
-
export declare function latticeMap(layer: string, lattice: Lattice, docks: readonly RouteNetworkDock[], unitsPerMeter: number): WfGridFloor;
|
|
19
|
-
//# sourceMappingURL=lattice-map.d.ts.map
|
package/dist/lib/lattice-map.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// The lattice as it is handed out: a map of each floor in the plan's own units — where the grid
|
|
2
|
-
// stands, how its cells are spaced, a byte of links per cell — and the docks that leave it.
|
|
3
|
-
import { PLAN_DECIMALS } from './scale.js';
|
|
4
|
-
/** How near a blocker a route starts to pay more to walk, in meters: a wide aisle's half. */
|
|
5
|
-
const NEAR = 1.5;
|
|
6
|
-
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
7
|
-
/**
|
|
8
|
-
* Bytes as base64 text.
|
|
9
|
-
* @param bytes The bytes.
|
|
10
|
-
* @returns The text, padded.
|
|
11
|
-
*/
|
|
12
|
-
function toBase64(bytes) {
|
|
13
|
-
const chunks = [];
|
|
14
|
-
let chunk = '';
|
|
15
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
16
|
-
const triple = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
|
|
17
|
-
chunk +=
|
|
18
|
-
ALPHABET[triple >> 18] +
|
|
19
|
-
ALPHABET[(triple >> 12) & 63] +
|
|
20
|
-
(i + 1 < bytes.length ? ALPHABET[(triple >> 6) & 63] : '=') +
|
|
21
|
-
(i + 2 < bytes.length ? ALPHABET[triple & 63] : '=');
|
|
22
|
-
// In pieces: one string grown a character at a time over a whole floor is slow to build.
|
|
23
|
-
if (chunk.length >= 8192) {
|
|
24
|
-
chunks.push(chunk);
|
|
25
|
-
chunk = '';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return chunks.join('') + chunk;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Bytes packed as runs: the value, then the run's length as a little-endian base-128 number, and
|
|
32
|
-
* the whole as base64. A floor's map is long stretches of one value.
|
|
33
|
-
* @param bytes The bytes.
|
|
34
|
-
* @returns The packed text.
|
|
35
|
-
*/
|
|
36
|
-
export function packRuns(bytes) {
|
|
37
|
-
const runs = [];
|
|
38
|
-
for (let start = 0; start < bytes.length;) {
|
|
39
|
-
let end = start + 1;
|
|
40
|
-
while (end < bytes.length && bytes[end] === bytes[start])
|
|
41
|
-
end++;
|
|
42
|
-
runs.push(bytes[start]);
|
|
43
|
-
for (let length = end - start;; length >>>= 7) {
|
|
44
|
-
if (length < 128) {
|
|
45
|
-
runs.push(length);
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
runs.push((length & 127) | 128);
|
|
49
|
-
}
|
|
50
|
-
start = end;
|
|
51
|
-
}
|
|
52
|
-
return toBase64(Uint8Array.from(runs));
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* One floor's lattice and docks as the map handed to the runtime, in plan units.
|
|
56
|
-
* @param layer The floor's layer id.
|
|
57
|
-
* @param lattice The lattice, in meters.
|
|
58
|
-
* @param docks The docks leaving it, in meters, in the lattice's own order.
|
|
59
|
-
* @param unitsPerMeter Plan coordinates in one meter.
|
|
60
|
-
* @returns The floor's map.
|
|
61
|
-
*/
|
|
62
|
-
export function latticeMap(layer, lattice, docks, unitsPerMeter) {
|
|
63
|
-
const at = (v) => +(v * unitsPerMeter).toFixed(PLAN_DECIMALS);
|
|
64
|
-
const nodeX = (cell) => lattice.origin[0] + (cell % lattice.width) * lattice.pitch;
|
|
65
|
-
const nodeY = (cell) => lattice.origin[1] + Math.floor(cell / lattice.width) * lattice.pitch;
|
|
66
|
-
const turns = [];
|
|
67
|
-
docks.forEach((dock, index) => {
|
|
68
|
-
const cell = lattice.dockCells[index];
|
|
69
|
-
// A dock that leaves from elsewhere than its node turns there on its way: a doorway's middle.
|
|
70
|
-
if (at(dock.from[0]) !== at(nodeX(cell)) || at(dock.from[1]) !== at(nodeY(cell)))
|
|
71
|
-
turns.push(index, at(dock.from[0]), at(dock.from[1]));
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
layer,
|
|
75
|
-
x: at(lattice.origin[0]),
|
|
76
|
-
y: at(lattice.origin[1]),
|
|
77
|
-
pitch: +(lattice.pitch * unitsPerMeter).toFixed(6),
|
|
78
|
-
width: lattice.width,
|
|
79
|
-
height: lattice.height,
|
|
80
|
-
links: packRuns(lattice.links),
|
|
81
|
-
near: +(NEAR * unitsPerMeter).toFixed(6),
|
|
82
|
-
...(lattice.sparseFrom !== undefined && {
|
|
83
|
-
sparseFrom: +(lattice.sparseFrom * unitsPerMeter).toFixed(6),
|
|
84
|
-
}),
|
|
85
|
-
docks: docks.flatMap((dock, index) => [
|
|
86
|
-
at(dock.to[0]),
|
|
87
|
-
at(dock.to[1]),
|
|
88
|
-
lattice.dockCells[index],
|
|
89
|
-
]),
|
|
90
|
-
...(turns.length && { turns }),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Ring } from './geom/types.js';
|
|
2
|
-
import type { PreparedWalls } from './geom/walls.js';
|
|
3
|
-
import { type NetworkVariant, type RouteNetwork, type RouteNetworkDestination } from './graph-types.js';
|
|
4
|
-
import type { Opening } from './openings.js';
|
|
5
|
-
/** How a lattice is spaced. */
|
|
6
|
-
export interface LatticeSpacing {
|
|
7
|
-
/** Distance between neighboring nodes, in meters. */
|
|
8
|
-
readonly pitch: number;
|
|
9
|
-
/**
|
|
10
|
-
* How far from the nearest cell nobody can stand in open floor begins, in meters: there the
|
|
11
|
-
* reader stands its nodes two cells apart. Absent, a node stands in every cell.
|
|
12
|
-
*/
|
|
13
|
-
readonly sparseFrom?: number;
|
|
14
|
-
}
|
|
15
|
-
/** The spacing of each lattice network. */
|
|
16
|
-
export declare const LATTICE_SPACING: Readonly<Record<Exclude<NetworkVariant, 'steiner'>, LatticeSpacing>>;
|
|
17
|
-
/**
|
|
18
|
-
* Builds the lattice route network for one layer.
|
|
19
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
20
|
-
* @param destinations Footprints the network must reach.
|
|
21
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
22
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
23
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
24
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
25
|
-
* @param spacing How the lattice is spaced.
|
|
26
|
-
* @returns The docks, the lattice they stand on, and stats.
|
|
27
|
-
*/
|
|
28
|
-
export declare function buildLatticeNetwork(walkableRings: readonly Ring[], destinations: readonly RouteNetworkDestination[], walls: readonly (readonly number[])[], prepared: PreparedWalls | undefined, drawn: readonly {
|
|
29
|
-
readonly id: string;
|
|
30
|
-
readonly ring: Ring;
|
|
31
|
-
}[], openings: readonly Opening[], spacing: LatticeSpacing): RouteNetwork;
|
|
32
|
-
//# sourceMappingURL=lattice-network.d.ts.map
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
// The "grid" route-network generators: the walkable floor itself is the graph. Every cell of the
|
|
2
|
-
// raster a person can stand in is a node, joined to its neighbors in eight directions wherever the
|
|
3
|
-
// step crosses nothing; on open floor the reader may stand its nodes two cells apart. A stub per
|
|
4
|
-
// open face ends on a node, and the dock runs from there. Nothing is laid, grown or straightened: the route is
|
|
5
|
-
// found over the lattice at run time. Produces a {@link RouteNetwork} that carries a {@link Lattice}.
|
|
6
|
-
import { crossesBlocker } from './blockers.js';
|
|
7
|
-
import { dockFor, turnsInDoorway } from './entries.js';
|
|
8
|
-
import { floorGrids } from './floor-grids.js';
|
|
9
|
-
import { snapCm } from './geom/point-key.js';
|
|
10
|
-
import { LATTICE_STEPS, } from './graph-types.js';
|
|
11
|
-
import { setUpNetwork } from './network-setup.js';
|
|
12
|
-
import { MIN_PASS } from './network-sizes.js';
|
|
13
|
-
import { makeRouter } from './router.js';
|
|
14
|
-
import { planStubs, sideOf } from './stubs.js';
|
|
15
|
-
/** The spacing of each lattice network. */
|
|
16
|
-
export const LATTICE_SPACING = {
|
|
17
|
-
grid: { pitch: 0.5, sparseFrom: 0.5 },
|
|
18
|
-
'grid-fine': { pitch: 0.5 },
|
|
19
|
-
'grid-coarse': { pitch: 1 },
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Builds the lattice route network for one layer.
|
|
23
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
24
|
-
* @param destinations Footprints the network must reach.
|
|
25
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
26
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
27
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
28
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
29
|
-
* @param spacing How the lattice is spaced.
|
|
30
|
-
* @returns The docks, the lattice they stand on, and stats.
|
|
31
|
-
*/
|
|
32
|
-
export function buildLatticeNetwork(walkableRings, destinations, walls, prepared, drawn, openings, spacing) {
|
|
33
|
-
const { cfg, wallLines, doors, owners, opened, floor, unservedFloorPieces, doorSides, blockers, destinationOf, failed, note, } = setUpNetwork(walkableRings, destinations, walls, prepared, drawn, openings);
|
|
34
|
-
const { grids, frames, frameOfDest } = floorGrids(floor, doorSides, destinations, wallLines, doors, cfg, false);
|
|
35
|
-
const ctx = grids[0].ctx;
|
|
36
|
-
const router = makeRouter(grids, { ...cfg, pitch: spacing.pitch });
|
|
37
|
-
const { pitch, clearance } = router;
|
|
38
|
-
const { width, height } = router.size;
|
|
39
|
-
const cells = width * height;
|
|
40
|
-
const [originX, originY] = router.worldOf(0);
|
|
41
|
-
const nodeAt = (cell) => [
|
|
42
|
-
snapCm(originX + (cell % width) * pitch),
|
|
43
|
-
snapCm(originY + Math.floor(cell / width) * pitch),
|
|
44
|
-
];
|
|
45
|
-
/* ---- the steps: every cell joined to the neighbors a person can step to ---- */
|
|
46
|
-
/**
|
|
47
|
-
* The cell one step from another, as the raster walks it. A diagonal step is taken only when both
|
|
48
|
-
* ways round its corner are open: a route never cuts a corner.
|
|
49
|
-
* @param cell The cell stepped from.
|
|
50
|
-
* @param step Which of {@link LATTICE_STEPS}.
|
|
51
|
-
* @returns The cell stepped onto, or -1.
|
|
52
|
-
*/
|
|
53
|
-
const stepFrom = (cell, step) => {
|
|
54
|
-
if (step < 4)
|
|
55
|
-
return router.stepTo(cell, step);
|
|
56
|
-
const [dx, dy] = LATTICE_STEPS[step];
|
|
57
|
-
const across = dx > 0 ? 0 : 1;
|
|
58
|
-
const along = dy > 0 ? 2 : 3;
|
|
59
|
-
const side = router.stepTo(cell, across);
|
|
60
|
-
const ahead = router.stepTo(cell, along);
|
|
61
|
-
if (side < 0 || ahead < 0)
|
|
62
|
-
return -1;
|
|
63
|
-
const corner = router.stepTo(side, along);
|
|
64
|
-
return corner >= 0 && router.stepTo(ahead, across) === corner ? corner : -1;
|
|
65
|
-
};
|
|
66
|
-
// A node stands where a person can: the cell is walkable and its middle is floor.
|
|
67
|
-
const stands = new Uint8Array(cells);
|
|
68
|
-
for (let cell = 0; cell < cells; cell++)
|
|
69
|
-
if (router.ok[cell] && ctx.freeAt(...nodeAt(cell)))
|
|
70
|
-
stands[cell] = 1;
|
|
71
|
-
// Far enough from anything that blocks, a step cannot cross it; nearer, the step is read against
|
|
72
|
-
// the walls and outlines as drawn.
|
|
73
|
-
const safe = pitch * (Math.SQRT2 + 1);
|
|
74
|
-
const unit = new Uint8Array(cells);
|
|
75
|
-
for (let cell = 0; cell < cells; cell++) {
|
|
76
|
-
if (!stands[cell])
|
|
77
|
-
continue;
|
|
78
|
-
// One of each pair of opposite steps: the other is the same step read from its far end.
|
|
79
|
-
for (let step = 0; step < 8; step += 2) {
|
|
80
|
-
const next = stepFrom(cell, step);
|
|
81
|
-
if (next < 0 || !stands[next])
|
|
82
|
-
continue;
|
|
83
|
-
if ((clearance[cell] < safe || clearance[next] < safe) &&
|
|
84
|
-
crossesBlocker(blockers, [...nodeAt(cell), ...nodeAt(next)]))
|
|
85
|
-
continue;
|
|
86
|
-
unit[cell] |= 1 << step;
|
|
87
|
-
unit[next] |= 1 << (step ^ 1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const offset = LATTICE_STEPS.map(([dx, dy]) => dy * width + dx);
|
|
91
|
-
// The connected parts of the lattice, by cell; -1 where no node stands.
|
|
92
|
-
const part = new Int32Array(cells).fill(-1);
|
|
93
|
-
let parts = 0;
|
|
94
|
-
for (let seed = 0; seed < cells; seed++) {
|
|
95
|
-
if (!unit[seed] || part[seed] >= 0)
|
|
96
|
-
continue;
|
|
97
|
-
const queue = [seed];
|
|
98
|
-
part[seed] = parts;
|
|
99
|
-
for (let head = 0; head < queue.length; head++) {
|
|
100
|
-
const cell = queue[head];
|
|
101
|
-
for (let step = 0; step < 8; step++) {
|
|
102
|
-
const next = cell + offset[step];
|
|
103
|
-
if (!(unit[cell] & (1 << step)) || part[next] >= 0)
|
|
104
|
-
continue;
|
|
105
|
-
part[next] = parts;
|
|
106
|
-
queue.push(next);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
parts++;
|
|
110
|
-
}
|
|
111
|
-
/* ---- stubs: one per open side of each destination, as the steiner network plans them ---- */
|
|
112
|
-
const build = { cfg, frames, blockers, destinationOf, note };
|
|
113
|
-
const plan = planStubs({ ...build, destinations, owners, ctx, frameOfDest, opened });
|
|
114
|
-
const whiskers = [];
|
|
115
|
-
const anchors = [];
|
|
116
|
-
/** Sides of each destination a whisker already serves. */
|
|
117
|
-
const servedSides = new Map();
|
|
118
|
-
/** The cells stubs stand on, by the part of the lattice they are in. */
|
|
119
|
-
const tipCells = new Map();
|
|
120
|
-
/**
|
|
121
|
-
* The node a stub ends on: the one in its planned tip's cell, else the nearest along the stub —
|
|
122
|
-
* the first a lawful dock runs from.
|
|
123
|
-
* @param stub The stub; its tip moves to the node.
|
|
124
|
-
* @returns The node's cell, or -1 when the stub has none (noted).
|
|
125
|
-
*/
|
|
126
|
-
const seat = (stub) => {
|
|
127
|
-
const destination = destinationOf.get(stub.id);
|
|
128
|
-
const farthest = Math.min(stub.way.depth - MIN_PASS, stub.reach + 2 * cfg.stub);
|
|
129
|
-
let onFloor = false;
|
|
130
|
-
for (let steps = 0; steps * pitch <= farthest + stub.reach; steps++)
|
|
131
|
-
for (const sign of steps ? [-1, 1] : [1]) {
|
|
132
|
-
const run = sign * steps * pitch;
|
|
133
|
-
if (stub.reach + run < MIN_PASS || stub.reach + run > farthest)
|
|
134
|
-
continue;
|
|
135
|
-
const cell = router.cellIn(stub.tx + stub.nx * run, stub.ty + stub.ny * run, frames[0]);
|
|
136
|
-
if (cell < 0 || !unit[cell])
|
|
137
|
-
continue;
|
|
138
|
-
onFloor = true;
|
|
139
|
-
if (!dockFor(destination, stub.way, nodeAt(cell), blockers))
|
|
140
|
-
continue;
|
|
141
|
-
[stub.tx, stub.ty] = nodeAt(cell);
|
|
142
|
-
anchors.push({ id: stub.id, at: [stub.tx, stub.ty] });
|
|
143
|
-
return cell;
|
|
144
|
-
}
|
|
145
|
-
note(stub.id, onFloor ? 'way-in-blocked' : 'no-floor', { at: [stub.tx, stub.ty] });
|
|
146
|
-
return -1;
|
|
147
|
-
};
|
|
148
|
-
let pending = plan.stubs;
|
|
149
|
-
// A stub that found no node, or stands alone on its part of the floor, is retried from its
|
|
150
|
-
// destination's next way in.
|
|
151
|
-
while (pending.length) {
|
|
152
|
-
const seated = pending.flatMap((stub) => {
|
|
153
|
-
const cell = seat(stub);
|
|
154
|
-
return cell < 0 ? [] : [{ stub, cell }];
|
|
155
|
-
});
|
|
156
|
-
const lost = pending.filter((stub) => !seated.some((entry) => entry.stub === stub));
|
|
157
|
-
const arriving = new Map();
|
|
158
|
-
for (const { cell } of seated)
|
|
159
|
-
arriving.set(part[cell], (arriving.get(part[cell]) ?? new Set()).add(cell));
|
|
160
|
-
for (const entry of seated) {
|
|
161
|
-
const { stub, cell } = entry;
|
|
162
|
-
const known = tipCells.get(part[cell]);
|
|
163
|
-
// A part of the floor only this tip stands on leads nowhere.
|
|
164
|
-
if (!known && arriving.get(part[cell]).size < 2) {
|
|
165
|
-
note(stub.id, 'cut-off', { at: [stub.tx, stub.ty] });
|
|
166
|
-
lost.push(stub);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
whiskers.push(entry);
|
|
170
|
-
if (stub.kind === 'face')
|
|
171
|
-
servedSides.set(stub.id, (servedSides.get(stub.id) ?? new Set()).add(sideOf(stub.way)));
|
|
172
|
-
}
|
|
173
|
-
for (const { cell } of whiskers)
|
|
174
|
-
tipCells.set(part[cell], (tipCells.get(part[cell]) ?? new Set()).add(cell));
|
|
175
|
-
pending = lost.flatMap((stub) => plan.retry(stub, servedSides, whiskers.map((entry) => entry.stub)) ?? []);
|
|
176
|
-
}
|
|
177
|
-
/* ---- docks: from each stub's node into its destination ---- */
|
|
178
|
-
/** Docking-ring sides already taken, per destination: one stub per side. */
|
|
179
|
-
const usedEdges = new Map();
|
|
180
|
-
const lawful = [];
|
|
181
|
-
for (const { stub, cell } of whiskers) {
|
|
182
|
-
const destination = destinationOf.get(stub.id);
|
|
183
|
-
const tip = [stub.tx, stub.ty];
|
|
184
|
-
const docked = dockFor(destination, stub.way, tip, blockers, usedEdges.get(stub.id));
|
|
185
|
-
if (!docked) {
|
|
186
|
-
note(stub.id, 'detached', { at: tip, by: 'dock-unlawful' });
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (docked.edge !== undefined)
|
|
190
|
-
usedEdges.set(stub.id, (usedEdges.get(stub.id) ?? new Set()).add(docked.edge));
|
|
191
|
-
// A doorway dock that ends toward a side of its ring turns at the doorway's middle: a line runs
|
|
192
|
-
// there from the node, and the dock leaves from the middle.
|
|
193
|
-
const middle = [stub.mx, stub.my];
|
|
194
|
-
const turns = turnsInDoorway(destination, stub, tip, docked.to);
|
|
195
|
-
lawful.push({
|
|
196
|
-
dock: { id: stub.id, from: turns ? middle : tip, to: docked.to },
|
|
197
|
-
cell,
|
|
198
|
-
turn: turns ? [tip, middle] : null,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
// The parts kept: most docks first, each next part only when it docks a destination no kept part
|
|
202
|
-
// does — a part the floor cuts off serves what nothing else can, and nothing else.
|
|
203
|
-
const dockCount = new Map();
|
|
204
|
-
for (const { cell } of lawful)
|
|
205
|
-
dockCount.set(part[cell], (dockCount.get(part[cell]) ?? 0) + 1);
|
|
206
|
-
const serving = new Set();
|
|
207
|
-
const reached = new Set();
|
|
208
|
-
for (const [label] of [...dockCount.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])) {
|
|
209
|
-
const ids = lawful.filter(({ cell }) => part[cell] === label).map(({ dock }) => dock.id);
|
|
210
|
-
if (!ids.some((id) => !reached.has(id)))
|
|
211
|
-
continue;
|
|
212
|
-
serving.add(label);
|
|
213
|
-
for (const id of ids)
|
|
214
|
-
reached.add(id);
|
|
215
|
-
}
|
|
216
|
-
const kept = lawful.filter(({ cell }) => serving.has(part[cell]));
|
|
217
|
-
/* ---- the map: the kept parts' cells, cut to the box that holds them ---- */
|
|
218
|
-
let minX = width;
|
|
219
|
-
let minY = height;
|
|
220
|
-
let maxX = -1;
|
|
221
|
-
let maxY = -1;
|
|
222
|
-
for (let cell = 0; cell < cells; cell++) {
|
|
223
|
-
if (!serving.has(part[cell]))
|
|
224
|
-
continue;
|
|
225
|
-
minX = Math.min(minX, cell % width);
|
|
226
|
-
maxX = Math.max(maxX, cell % width);
|
|
227
|
-
minY = Math.min(minY, Math.floor(cell / width));
|
|
228
|
-
maxY = Math.max(maxY, Math.floor(cell / width));
|
|
229
|
-
}
|
|
230
|
-
const across = Math.max(0, maxX - minX + 1);
|
|
231
|
-
const down = Math.max(0, maxY - minY + 1);
|
|
232
|
-
const links = new Uint8Array(across * down);
|
|
233
|
-
for (let y = 0; y < down; y++)
|
|
234
|
-
for (let x = 0; x < across; x++) {
|
|
235
|
-
const cell = (minY + y) * width + minX + x;
|
|
236
|
-
if (serving.has(part[cell]))
|
|
237
|
-
links[y * across + x] = unit[cell];
|
|
238
|
-
}
|
|
239
|
-
const cropOf = (cell) => (Math.floor(cell / width) - minY) * across + ((cell % width) - minX);
|
|
240
|
-
const labels = [...serving].sort((a, b) => a - b);
|
|
241
|
-
const ways = [];
|
|
242
|
-
for (const [id, list] of plan.ways)
|
|
243
|
-
for (const way of list)
|
|
244
|
-
ways.push({ id, kind: way.kind, foot: [way.mx, way.my], normal: [way.nx, way.ny] });
|
|
245
|
-
return {
|
|
246
|
-
paths: kept.flatMap(({ turn }) => (turn ? [turn] : [])),
|
|
247
|
-
docks: kept.map(({ dock }) => dock),
|
|
248
|
-
lattice: {
|
|
249
|
-
origin: across ? nodeAt(minY * width + minX) : [originX, originY],
|
|
250
|
-
pitch,
|
|
251
|
-
width: across,
|
|
252
|
-
height: down,
|
|
253
|
-
links,
|
|
254
|
-
...(spacing.sparseFrom !== undefined && { sparseFrom: spacing.sparseFrom }),
|
|
255
|
-
dockCells: kept.map(({ cell }) => cropOf(cell)),
|
|
256
|
-
dockParts: kept.map(({ cell }) => labels.indexOf(part[cell])),
|
|
257
|
-
},
|
|
258
|
-
anatomy: {
|
|
259
|
-
doors: doors.map((door) => ({
|
|
260
|
-
gap: [
|
|
261
|
-
[door.gap[0], door.gap[1]],
|
|
262
|
-
[door.gap[2], door.gap[3]],
|
|
263
|
-
],
|
|
264
|
-
mid: [door.mid[0], door.mid[1]],
|
|
265
|
-
})),
|
|
266
|
-
anchors,
|
|
267
|
-
ways,
|
|
268
|
-
whiskers: whiskers.map(({ stub }) => ({
|
|
269
|
-
id: stub.id,
|
|
270
|
-
kind: stub.kind,
|
|
271
|
-
foot: [stub.mx, stub.my],
|
|
272
|
-
tip: [stub.tx, stub.ty],
|
|
273
|
-
})),
|
|
274
|
-
frames: [],
|
|
275
|
-
loose: [],
|
|
276
|
-
axes: [],
|
|
277
|
-
},
|
|
278
|
-
stats: {
|
|
279
|
-
unreached: destinations
|
|
280
|
-
.filter((d) => !reached.has(d.id))
|
|
281
|
-
.map((d) => failed.get(d.id) ?? { id: d.id, reason: 'detached' }),
|
|
282
|
-
components: tipCells.size,
|
|
283
|
-
doorways: doors.length,
|
|
284
|
-
shortcuts: 0,
|
|
285
|
-
...(tipCells.size > 1 && { untiedPieces: tipCells.size - 1 }),
|
|
286
|
-
...(unservedFloorPieces > 0 && { unservedFloorPieces }),
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { type Blockers } from './blockers.js';
|
|
2
|
-
import { type DoorSide, type Doorway } from './doorways.js';
|
|
3
|
-
import type { Ring, Segment } from './geom/types.js';
|
|
4
|
-
import { type PreparedWalls } from './geom/walls.js';
|
|
5
|
-
import type { RouteNetworkDestination, Unreached } from './graph-types.js';
|
|
6
|
-
import type { NetworkBuild } from './network-build.js';
|
|
7
|
-
import { type NetworkSizes } from './network-sizes.js';
|
|
8
|
-
import type { Opening } from './openings.js';
|
|
9
|
-
/** A layer as a route-network generator finds it. */
|
|
10
|
-
export interface NetworkSetup {
|
|
11
|
-
/** Network sizes. */
|
|
12
|
-
readonly cfg: NetworkSizes;
|
|
13
|
-
/** The layer's wall segments. */
|
|
14
|
-
readonly wallLines: readonly Segment[];
|
|
15
|
-
/** The doorways the walls leave. */
|
|
16
|
-
readonly doors: readonly Doorway[];
|
|
17
|
-
/** Doorway sides by the destination they open into, the entrances' openings included. */
|
|
18
|
-
readonly owners: ReadonlyMap<string, readonly DoorSide[]>;
|
|
19
|
-
/** Entrances with openings: reached through their doorways alone, never from a face. */
|
|
20
|
-
readonly opened: ReadonlySet<string>;
|
|
21
|
-
/** The floor pieces a route can serve. */
|
|
22
|
-
readonly floor: readonly Ring[];
|
|
23
|
-
/** Walkable floor pieces no destination or doorway serves, left out of the network. */
|
|
24
|
-
readonly unservedFloorPieces: number;
|
|
25
|
-
/** The closed sides of the doorways: only a doorway's middle stays open. */
|
|
26
|
-
readonly doorSides: readonly Segment[];
|
|
27
|
-
/** What a line or a dock may not cross; a doorway's closed sides block like walls. */
|
|
28
|
-
readonly blockers: Blockers;
|
|
29
|
-
/** The layer's destinations, by id. */
|
|
30
|
-
readonly destinationOf: ReadonlyMap<string, RouteNetworkDestination>;
|
|
31
|
-
/** Why each destination without a dock has none: the most informative reason noted. */
|
|
32
|
-
readonly failed: ReadonlyMap<string, Unreached>;
|
|
33
|
-
/** Records why a destination has no dock yet; see {@link NetworkBuild.note}. */
|
|
34
|
-
readonly note: NetworkBuild['note'];
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Reads a layer for a route-network generator.
|
|
38
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
39
|
-
* @param destinations Footprints the network must reach.
|
|
40
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
41
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
42
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
43
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
44
|
-
* @returns The layer as the generators read it.
|
|
45
|
-
*/
|
|
46
|
-
export declare function setUpNetwork(walkableRings: readonly Ring[], destinations: readonly RouteNetworkDestination[], walls: readonly (readonly number[])[], prepared: PreparedWalls | undefined, drawn: readonly {
|
|
47
|
-
readonly id: string;
|
|
48
|
-
readonly ring: Ring;
|
|
49
|
-
}[], openings: readonly Opening[]): NetworkSetup;
|
|
50
|
-
//# sourceMappingURL=network-setup.d.ts.map
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// What every route-network generator reads of a layer before it lays a line: the doorways and who
|
|
2
|
-
// owns them, the openings the entrances make, the floor a route can serve, what a line may not
|
|
3
|
-
// cross, and where a destination the build could not serve is noted. Read once here, so every
|
|
4
|
-
// generator docks the same ways in.
|
|
5
|
-
import { blockersOf } from './blockers.js';
|
|
6
|
-
import { DOOR_WELD, doorOwners, doorwaySides, findDoorways, } from './doorways.js';
|
|
7
|
-
import { servedFloor } from './floor-grids.js';
|
|
8
|
-
import { prepareWalls } from './geom/walls.js';
|
|
9
|
-
import { NETWORK_SIZES } from './network-sizes.js';
|
|
10
|
-
/**
|
|
11
|
-
* Reads a layer for a route-network generator.
|
|
12
|
-
* @param walkableRings The layer's walkable polygon (outer rings + holes).
|
|
13
|
-
* @param destinations Footprints the network must reach.
|
|
14
|
-
* @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
|
|
15
|
-
* @param prepared The same walls already prepared, when the caller has them.
|
|
16
|
-
* @param drawn The rest of each destination's drawing; ways in may not cross it.
|
|
17
|
-
* @param openings The openings the entrances make: the ways in they own, through the walls.
|
|
18
|
-
* @returns The layer as the generators read it.
|
|
19
|
-
*/
|
|
20
|
-
export function setUpNetwork(walkableRings, destinations, walls, prepared, drawn, openings) {
|
|
21
|
-
const cfg = NETWORK_SIZES;
|
|
22
|
-
const prep = prepared ?? prepareWalls(walls);
|
|
23
|
-
const wallLines = prep.segments;
|
|
24
|
-
const doors = findDoorways(prep, destinations);
|
|
25
|
-
const owners = doorOwners(doors, destinations, wallLines, cfg);
|
|
26
|
-
// An entrance owns its openings and is reached through them alone. An opening on a doorway the
|
|
27
|
-
// walls already leave is that doorway; any other stands on the floor past the wall, and the dock
|
|
28
|
-
// from it crosses the wall inside the opening's span.
|
|
29
|
-
const opened = new Set();
|
|
30
|
-
const passes = new Map();
|
|
31
|
-
for (const { id, at, span, door } of openings) {
|
|
32
|
-
const same = doors.find((d) => Math.hypot(d.mid[0] - at[0], d.mid[1] - at[1]) <= DOOR_WELD);
|
|
33
|
-
const flip = same && same.nx * door.nx + same.ny * door.ny < 0 ? -1 : 1;
|
|
34
|
-
const sides = owners.get(id) ?? [];
|
|
35
|
-
sides.push(same
|
|
36
|
-
? { door: same, nx: same.nx * flip, ny: same.ny * flip }
|
|
37
|
-
: { door, nx: door.nx, ny: door.ny });
|
|
38
|
-
owners.set(id, sides);
|
|
39
|
-
opened.add(id);
|
|
40
|
-
passes.set(id, [...(passes.get(id) ?? []), span]);
|
|
41
|
-
}
|
|
42
|
-
const { rings: floor, dropped: unservedFloorPieces } = servedFloor(walkableRings, destinations, owners);
|
|
43
|
-
// Only a doorway's middle stays open, so every pass draws lines through it there.
|
|
44
|
-
const doorSides = doorwaySides(doors, cfg);
|
|
45
|
-
// A doorway's closed sides block stubs and docks like walls.
|
|
46
|
-
const blockers = blockersOf(destinations, [...wallLines, ...doorSides], doors, drawn, new Set(owners.keys()), passes);
|
|
47
|
-
/** Why a destination has no stub yet: the furthest stage any of its ways in reached. */
|
|
48
|
-
const failed = new Map();
|
|
49
|
-
// Reasons from least to most informative; a destination keeps the most informative one.
|
|
50
|
-
const REACHED = [
|
|
51
|
-
'no-way-in',
|
|
52
|
-
'no-floor',
|
|
53
|
-
'cut-off',
|
|
54
|
-
'way-in-blocked',
|
|
55
|
-
'detached',
|
|
56
|
-
];
|
|
57
|
-
const note = (id, reason, facts) => {
|
|
58
|
-
const known = failed.get(id);
|
|
59
|
-
if (known === undefined || REACHED.indexOf(reason) > REACHED.indexOf(known.reason))
|
|
60
|
-
failed.set(id, { id, reason, ...facts });
|
|
61
|
-
};
|
|
62
|
-
return {
|
|
63
|
-
cfg,
|
|
64
|
-
wallLines,
|
|
65
|
-
doors,
|
|
66
|
-
owners,
|
|
67
|
-
opened,
|
|
68
|
-
floor,
|
|
69
|
-
unservedFloorPieces,
|
|
70
|
-
doorSides,
|
|
71
|
-
blockers,
|
|
72
|
-
destinationOf: new Map(destinations.map((d) => [d.id, d])),
|
|
73
|
-
failed,
|
|
74
|
-
note,
|
|
75
|
-
};
|
|
76
|
-
}
|