@expofp/wayfinding-compiler 0.0.0-bootstrap.0
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 +446 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +3 -0
- package/dist/lib/attach.d.ts +43 -0
- package/dist/lib/attach.js +199 -0
- package/dist/lib/axes.d.ts +76 -0
- package/dist/lib/axes.js +103 -0
- package/dist/lib/blockers.d.ts +134 -0
- package/dist/lib/blockers.js +289 -0
- package/dist/lib/cell-array.d.ts +14 -0
- package/dist/lib/cell-array.js +25 -0
- package/dist/lib/centering.d.ts +14 -0
- package/dist/lib/centering.js +88 -0
- package/dist/lib/compile-layer.d.ts +41 -0
- package/dist/lib/compile-layer.js +241 -0
- package/dist/lib/compile-wayfinding.d.ts +90 -0
- package/dist/lib/compile-wayfinding.js +203 -0
- package/dist/lib/connect.d.ts +35 -0
- package/dist/lib/connect.js +206 -0
- package/dist/lib/diagnostics.d.ts +190 -0
- package/dist/lib/diagnostics.js +634 -0
- package/dist/lib/doorways.d.ts +78 -0
- package/dist/lib/doorways.js +331 -0
- package/dist/lib/emit-network.d.ts +47 -0
- package/dist/lib/emit-network.js +105 -0
- package/dist/lib/emit-v1.d.ts +31 -0
- package/dist/lib/emit-v1.js +56 -0
- package/dist/lib/entries.d.ts +123 -0
- package/dist/lib/entries.js +448 -0
- package/dist/lib/floor-grids.d.ts +48 -0
- package/dist/lib/floor-grids.js +185 -0
- package/dist/lib/framing.d.ts +20 -0
- package/dist/lib/framing.js +187 -0
- package/dist/lib/geom/boolean-ops.d.ts +21 -0
- package/dist/lib/geom/boolean-ops.js +1 -0
- package/dist/lib/geom/clipper2-ops.d.ts +7 -0
- package/dist/lib/geom/clipper2-ops.js +50 -0
- package/dist/lib/geom/footprint.d.ts +26 -0
- package/dist/lib/geom/footprint.js +203 -0
- package/dist/lib/geom/frames.d.ts +116 -0
- package/dist/lib/geom/frames.js +233 -0
- package/dist/lib/geom/mesh-outline.d.ts +13 -0
- package/dist/lib/geom/mesh-outline.js +90 -0
- package/dist/lib/geom/normalize.d.ts +28 -0
- package/dist/lib/geom/normalize.js +118 -0
- package/dist/lib/geom/point-key.d.ts +16 -0
- package/dist/lib/geom/point-key.js +15 -0
- package/dist/lib/geom/primitives.d.ts +105 -0
- package/dist/lib/geom/primitives.js +184 -0
- package/dist/lib/geom/split-crossings.d.ts +14 -0
- package/dist/lib/geom/split-crossings.js +171 -0
- package/dist/lib/geom/types.d.ts +9 -0
- package/dist/lib/geom/types.js +1 -0
- package/dist/lib/geom/walls.d.ts +31 -0
- package/dist/lib/geom/walls.js +71 -0
- package/dist/lib/graph-invariants.d.ts +49 -0
- package/dist/lib/graph-invariants.js +129 -0
- package/dist/lib/graph-types.d.ts +143 -0
- package/dist/lib/graph-types.js +7 -0
- package/dist/lib/grid-network.d.ts +20 -0
- package/dist/lib/grid-network.js +349 -0
- package/dist/lib/lattice-map.d.ts +19 -0
- package/dist/lib/lattice-map.js +92 -0
- package/dist/lib/lattice-network.d.ts +32 -0
- package/dist/lib/lattice-network.js +289 -0
- package/dist/lib/mask.d.ts +54 -0
- package/dist/lib/mask.js +210 -0
- package/dist/lib/min-heap.d.ts +24 -0
- package/dist/lib/min-heap.js +73 -0
- package/dist/lib/network-build.d.ts +21 -0
- package/dist/lib/network-build.js +4 -0
- package/dist/lib/network-graph.d.ts +80 -0
- package/dist/lib/network-graph.js +467 -0
- package/dist/lib/network-setup.d.ts +50 -0
- package/dist/lib/network-setup.js +76 -0
- package/dist/lib/network-sizes.d.ts +29 -0
- package/dist/lib/network-sizes.js +22 -0
- package/dist/lib/network-store.d.ts +173 -0
- package/dist/lib/network-store.js +430 -0
- package/dist/lib/openings.d.ts +54 -0
- package/dist/lib/openings.js +387 -0
- package/dist/lib/place-path.d.ts +47 -0
- package/dist/lib/place-path.js +346 -0
- package/dist/lib/post-passes.d.ts +31 -0
- package/dist/lib/post-passes.js +305 -0
- package/dist/lib/reachability.d.ts +58 -0
- package/dist/lib/reachability.js +220 -0
- package/dist/lib/report.d.ts +49 -0
- package/dist/lib/report.js +374 -0
- package/dist/lib/router.d.ts +111 -0
- package/dist/lib/router.js +388 -0
- package/dist/lib/runs.d.ts +92 -0
- package/dist/lib/runs.js +232 -0
- package/dist/lib/scale.d.ts +58 -0
- package/dist/lib/scale.js +185 -0
- package/dist/lib/steiner-network.d.ts +19 -0
- package/dist/lib/steiner-network.js +183 -0
- package/dist/lib/strip.d.ts +43 -0
- package/dist/lib/strip.js +200 -0
- package/dist/lib/stubs.d.ts +84 -0
- package/dist/lib/stubs.js +316 -0
- package/dist/lib/transitions.d.ts +72 -0
- package/dist/lib/transitions.js +191 -0
- package/dist/lib/types.d.ts +126 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/validate-source.d.ts +41 -0
- package/dist/lib/validate-source.js +133 -0
- package/dist/lib/walkable.d.ts +104 -0
- package/dist/lib/walkable.js +140 -0
- package/package.json +37 -0
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Box, Ring } from './geom/types.js';
|
|
2
|
+
import { type NetworkSizes } from './network-sizes.js';
|
|
3
|
+
/** The walkable floor as a grid of cells, with how far each of them is from anything blocked. */
|
|
4
|
+
export interface MaskCtx {
|
|
5
|
+
/** Cell size. */
|
|
6
|
+
readonly cs: number;
|
|
7
|
+
/** Width in cells. */
|
|
8
|
+
readonly w: number;
|
|
9
|
+
/** Height in cells. */
|
|
10
|
+
readonly h: number;
|
|
11
|
+
/** Grid origin x. */
|
|
12
|
+
readonly ox: number;
|
|
13
|
+
/** Grid origin y. */
|
|
14
|
+
readonly oy: number;
|
|
15
|
+
/** 1 for floor, row-major. */
|
|
16
|
+
readonly mask: Uint8Array;
|
|
17
|
+
/** Meters from each cell to the nearest blocked one. */
|
|
18
|
+
readonly dist: Float32Array;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a point is on the floor.
|
|
21
|
+
* @param x Point x.
|
|
22
|
+
* @param y Point y.
|
|
23
|
+
* @returns True on the floor.
|
|
24
|
+
*/
|
|
25
|
+
freeAt(x: number, y: number): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a straight line stays on floor (sampled at under a cell).
|
|
28
|
+
* @param ax Start x.
|
|
29
|
+
* @param ay Start y.
|
|
30
|
+
* @param bx End x.
|
|
31
|
+
* @param by End y.
|
|
32
|
+
* @returns True when every sample is floor.
|
|
33
|
+
*/
|
|
34
|
+
lineFree(ax: number, ay: number, bx: number, by: number): boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rasterize the walkable rings (walls and footprints already cut out) and their clearance field.
|
|
38
|
+
* @param rings The walkable polygon (outer rings + holes).
|
|
39
|
+
* @param cfg Network sizes.
|
|
40
|
+
* @param within Box to read; the rings' bounds by default.
|
|
41
|
+
* @returns The mask and its clearance field.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildMaskCtx(rings: readonly Ring[], cfg: NetworkSizes, within?: Box): MaskCtx;
|
|
44
|
+
/**
|
|
45
|
+
* Opens a doorway's throat to the router and to the lines laid through it: floor cells within `r`
|
|
46
|
+
* of the middle count a person's clearance, so a route can pass a doorway narrower than two
|
|
47
|
+
* clearances. Only cells already free are lifted; the jambs and walls stay closed.
|
|
48
|
+
* @param ctx The mask, in its own coordinates.
|
|
49
|
+
* @param mx Doorway middle x.
|
|
50
|
+
* @param my Doorway middle y.
|
|
51
|
+
* @param r The throat's radius.
|
|
52
|
+
*/
|
|
53
|
+
export declare function openDoorMiddle(ctx: MaskCtx, mx: number, my: number, r: number): void;
|
|
54
|
+
//# sourceMappingURL=mask.d.ts.map
|
package/dist/lib/mask.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// The raster mask the route network grows over: the walkable floor as cells, with how far each cell is
|
|
2
|
+
// from anything blocked.
|
|
3
|
+
import { cellArray } from './cell-array.js';
|
|
4
|
+
import { boundsOf } from './geom/primitives.js';
|
|
5
|
+
import { MIN_PASS } from './network-sizes.js';
|
|
6
|
+
/**
|
|
7
|
+
* Rasterize the walkable rings (walls and footprints already cut out) and their clearance field.
|
|
8
|
+
* @param rings The walkable polygon (outer rings + holes).
|
|
9
|
+
* @param cfg Network sizes.
|
|
10
|
+
* @param within Box to read; the rings' bounds by default.
|
|
11
|
+
* @returns The mask and its clearance field.
|
|
12
|
+
*/
|
|
13
|
+
export function buildMaskCtx(rings, cfg, within) {
|
|
14
|
+
let [minx, miny, maxx, maxy] = within ?? boundsOf(rings.flat());
|
|
15
|
+
if (!isFinite(minx)) {
|
|
16
|
+
minx = 0;
|
|
17
|
+
miny = 0;
|
|
18
|
+
maxx = 1;
|
|
19
|
+
maxy = 1;
|
|
20
|
+
}
|
|
21
|
+
const cs = cfg.cs;
|
|
22
|
+
const ox = minx - 1;
|
|
23
|
+
const oy = miny - 1;
|
|
24
|
+
const w = Math.max(4, Math.ceil((maxx - minx + 2) / cs));
|
|
25
|
+
const h = Math.max(4, Math.ceil((maxy - miny + 2) / cs));
|
|
26
|
+
const mask = cellArray(Uint8Array, w * h);
|
|
27
|
+
// Nonzero-winding scanline fill over an active edge list.
|
|
28
|
+
const fillPolys = (polys, value) => {
|
|
29
|
+
const edges = [];
|
|
30
|
+
for (const poly of polys)
|
|
31
|
+
for (let i = 0; i < poly.length; i++) {
|
|
32
|
+
const [ax, ay] = poly[i];
|
|
33
|
+
const [bx, by] = poly[(i + 1) % poly.length];
|
|
34
|
+
if (ay !== by)
|
|
35
|
+
edges.push({
|
|
36
|
+
ax,
|
|
37
|
+
ay,
|
|
38
|
+
bx,
|
|
39
|
+
by,
|
|
40
|
+
lo: Math.min(ay, by),
|
|
41
|
+
hi: Math.max(ay, by),
|
|
42
|
+
dir: by > ay ? 1 : -1,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
edges.sort((p, q) => p.lo - q.lo);
|
|
46
|
+
const active = [];
|
|
47
|
+
let next = 0;
|
|
48
|
+
for (let gy = 0; gy < h; gy++) {
|
|
49
|
+
const y = oy + (gy + 0.5) * cs;
|
|
50
|
+
const xs = [];
|
|
51
|
+
while (next < edges.length && edges[next].lo <= y)
|
|
52
|
+
active.push(edges[next++]);
|
|
53
|
+
for (let k = active.length - 1; k >= 0; k--)
|
|
54
|
+
if (active[k].hi <= y) {
|
|
55
|
+
active[k] = active[active.length - 1];
|
|
56
|
+
active.pop();
|
|
57
|
+
}
|
|
58
|
+
for (const { ax, ay, bx, by, dir } of active)
|
|
59
|
+
xs.push([ax + ((bx - ax) * (y - ay)) / (by - ay), dir]);
|
|
60
|
+
xs.sort((u, v) => u[0] - v[0]);
|
|
61
|
+
let wind = 0;
|
|
62
|
+
for (let k = 0; k + 1 < xs.length; k++) {
|
|
63
|
+
wind += xs[k][1];
|
|
64
|
+
if (wind === 0)
|
|
65
|
+
continue;
|
|
66
|
+
const i0 = Math.max(0, Math.ceil((xs[k][0] - ox) / cs - 0.5));
|
|
67
|
+
const i1 = Math.min(w - 1, Math.floor((xs[k + 1][0] - ox) / cs - 0.5));
|
|
68
|
+
for (let i = i0; i <= i1; i++)
|
|
69
|
+
mask[gy * w + i] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
fillPolys(rings, 1);
|
|
74
|
+
// A cell is floor only when no boundary edge passes through it, so a strip thinner than a cell still
|
|
75
|
+
// closes the cells it crosses. Cells are found by exact grid traversal of each edge.
|
|
76
|
+
const close = (gx, gy) => {
|
|
77
|
+
if (gx >= 0 && gy >= 0 && gx < w && gy < h)
|
|
78
|
+
mask[gy * w + gx] = 0;
|
|
79
|
+
};
|
|
80
|
+
const cover = (ax, ay, bx, by) => {
|
|
81
|
+
let gx = Math.floor((ax - ox) / cs);
|
|
82
|
+
let gy = Math.floor((ay - oy) / cs);
|
|
83
|
+
const ex = Math.floor((bx - ox) / cs);
|
|
84
|
+
const ey = Math.floor((by - oy) / cs);
|
|
85
|
+
const dx = bx - ax;
|
|
86
|
+
const dy = by - ay;
|
|
87
|
+
const sx = Math.sign(dx);
|
|
88
|
+
const sy = Math.sign(dy);
|
|
89
|
+
const tdx = dx ? cs / Math.abs(dx) : Infinity;
|
|
90
|
+
const tdy = dy ? cs / Math.abs(dy) : Infinity;
|
|
91
|
+
let tx = dx ? (ox + (gx + (dx > 0 ? 1 : 0)) * cs - ax) / dx : Infinity;
|
|
92
|
+
let ty = dy ? (oy + (gy + (dy > 0 ? 1 : 0)) * cs - ay) / dy : Infinity;
|
|
93
|
+
close(gx, gy);
|
|
94
|
+
for (let guard = Math.abs(ex - gx) + Math.abs(ey - gy) + 2; guard > 0; guard--) {
|
|
95
|
+
if (gx === ex && gy === ey)
|
|
96
|
+
break;
|
|
97
|
+
if (tx < ty) {
|
|
98
|
+
gx += sx;
|
|
99
|
+
tx += tdx;
|
|
100
|
+
}
|
|
101
|
+
else if (ty < tx) {
|
|
102
|
+
gy += sy;
|
|
103
|
+
ty += tdy;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Through a grid corner: both cells beside it are touched.
|
|
107
|
+
close(gx + sx, gy);
|
|
108
|
+
close(gx, gy + sy);
|
|
109
|
+
gx += sx;
|
|
110
|
+
gy += sy;
|
|
111
|
+
tx += tdx;
|
|
112
|
+
ty += tdy;
|
|
113
|
+
}
|
|
114
|
+
close(gx, gy);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
for (const ring of rings)
|
|
118
|
+
for (let i = 0; i < ring.length; i++) {
|
|
119
|
+
const a = ring[i];
|
|
120
|
+
const b = ring[(i + 1) % ring.length];
|
|
121
|
+
cover(a[0], a[1], b[0], b[1]);
|
|
122
|
+
}
|
|
123
|
+
// Chamfer distance (meters) to the nearest blocked cell.
|
|
124
|
+
const dist = cellArray(Float32Array, w * h).fill(1e9);
|
|
125
|
+
for (let i = 0; i < w * h; i++)
|
|
126
|
+
if (!mask[i])
|
|
127
|
+
dist[i] = 0;
|
|
128
|
+
const d1 = cs;
|
|
129
|
+
const d2 = cs * Math.SQRT2;
|
|
130
|
+
for (let y = 0; y < h; y++)
|
|
131
|
+
for (let x = 0; x < w; x++) {
|
|
132
|
+
const i = y * w + x;
|
|
133
|
+
if (x > 0)
|
|
134
|
+
dist[i] = Math.min(dist[i], dist[i - 1] + d1);
|
|
135
|
+
if (y > 0)
|
|
136
|
+
dist[i] = Math.min(dist[i], dist[i - w] + d1);
|
|
137
|
+
if (x > 0 && y > 0)
|
|
138
|
+
dist[i] = Math.min(dist[i], dist[i - w - 1] + d2);
|
|
139
|
+
if (x < w - 1 && y > 0)
|
|
140
|
+
dist[i] = Math.min(dist[i], dist[i - w + 1] + d2);
|
|
141
|
+
}
|
|
142
|
+
for (let y = h - 1; y >= 0; y--)
|
|
143
|
+
for (let x = w - 1; x >= 0; x--) {
|
|
144
|
+
const i = y * w + x;
|
|
145
|
+
if (x < w - 1)
|
|
146
|
+
dist[i] = Math.min(dist[i], dist[i + 1] + d1);
|
|
147
|
+
if (y < h - 1)
|
|
148
|
+
dist[i] = Math.min(dist[i], dist[i + w] + d1);
|
|
149
|
+
if (x < w - 1 && y < h - 1)
|
|
150
|
+
dist[i] = Math.min(dist[i], dist[i + w + 1] + d2);
|
|
151
|
+
if (x > 0 && y < h - 1)
|
|
152
|
+
dist[i] = Math.min(dist[i], dist[i + w - 1] + d2);
|
|
153
|
+
}
|
|
154
|
+
const cellAt = (x, y) => {
|
|
155
|
+
const gx = Math.floor((x - ox) / cs);
|
|
156
|
+
const gy = Math.floor((y - oy) / cs);
|
|
157
|
+
return gx >= 0 && gy >= 0 && gx < w && gy < h ? gy * w + gx : -1;
|
|
158
|
+
};
|
|
159
|
+
const freeAt = (x, y) => {
|
|
160
|
+
const c = cellAt(x, y);
|
|
161
|
+
return c >= 0 && mask[c] === 1;
|
|
162
|
+
};
|
|
163
|
+
const lineFree = (ax, ay, bx, by) => {
|
|
164
|
+
const len = Math.hypot(bx - ax, by - ay);
|
|
165
|
+
const n = Math.max(2, Math.ceil(len / (cs * 0.6)));
|
|
166
|
+
for (let i = 1; i < n; i++) {
|
|
167
|
+
const s = i / n;
|
|
168
|
+
if (!freeAt(ax + (bx - ax) * s, ay + (by - ay) * s))
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
cs,
|
|
175
|
+
w,
|
|
176
|
+
h,
|
|
177
|
+
ox,
|
|
178
|
+
oy,
|
|
179
|
+
mask,
|
|
180
|
+
dist,
|
|
181
|
+
freeAt,
|
|
182
|
+
lineFree,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Opens a doorway's throat to the router and to the lines laid through it: floor cells within `r`
|
|
187
|
+
* of the middle count a person's clearance, so a route can pass a doorway narrower than two
|
|
188
|
+
* clearances. Only cells already free are lifted; the jambs and walls stay closed.
|
|
189
|
+
* @param ctx The mask, in its own coordinates.
|
|
190
|
+
* @param mx Doorway middle x.
|
|
191
|
+
* @param my Doorway middle y.
|
|
192
|
+
* @param r The throat's radius.
|
|
193
|
+
*/
|
|
194
|
+
export function openDoorMiddle(ctx, mx, my, r) {
|
|
195
|
+
const { cs, ox, oy, w, h, mask, dist } = ctx;
|
|
196
|
+
const gx0 = Math.max(0, Math.floor((mx - r - ox) / cs));
|
|
197
|
+
const gx1 = Math.min(w - 1, Math.floor((mx + r - ox) / cs));
|
|
198
|
+
const gy0 = Math.max(0, Math.floor((my - r - oy) / cs));
|
|
199
|
+
const gy1 = Math.min(h - 1, Math.floor((my + r - oy) / cs));
|
|
200
|
+
for (let gy = gy0; gy <= gy1; gy++)
|
|
201
|
+
for (let gx = gx0; gx <= gx1; gx++) {
|
|
202
|
+
const cx = ox + (gx + 0.5) * cs;
|
|
203
|
+
const cy = oy + (gy + 0.5) * cs;
|
|
204
|
+
if (Math.hypot(cx - mx, cy - my) > r)
|
|
205
|
+
continue;
|
|
206
|
+
const c = gy * w + gx;
|
|
207
|
+
if (mask[c] === 1 && dist[c] < MIN_PASS)
|
|
208
|
+
dist[c] = MIN_PASS;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocation-free binary min-heap of (key, value) numbers: `pop` returns the value and leaves the key in
|
|
3
|
+
* {@link poppedKey}.
|
|
4
|
+
*/
|
|
5
|
+
export declare class MinHeap {
|
|
6
|
+
private readonly k;
|
|
7
|
+
private readonly v;
|
|
8
|
+
/** Key of the last popped entry. */
|
|
9
|
+
poppedKey: number;
|
|
10
|
+
/** Entry count. */
|
|
11
|
+
get size(): number;
|
|
12
|
+
/**
|
|
13
|
+
* Add an entry.
|
|
14
|
+
* @param key What the heap orders by.
|
|
15
|
+
* @param val The value carried with it.
|
|
16
|
+
*/
|
|
17
|
+
push(key: number, val: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Remove the smallest-key entry.
|
|
20
|
+
* @returns Its value; the key goes to {@link poppedKey}.
|
|
21
|
+
*/
|
|
22
|
+
pop(): number;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=min-heap.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocation-free binary min-heap of (key, value) numbers: `pop` returns the value and leaves the key in
|
|
3
|
+
* {@link poppedKey}.
|
|
4
|
+
*/
|
|
5
|
+
export class MinHeap {
|
|
6
|
+
k = [];
|
|
7
|
+
v = [];
|
|
8
|
+
/** Key of the last popped entry. */
|
|
9
|
+
poppedKey = 0;
|
|
10
|
+
/** Entry count. */
|
|
11
|
+
get size() {
|
|
12
|
+
return this.k.length;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Add an entry.
|
|
16
|
+
* @param key What the heap orders by.
|
|
17
|
+
* @param val The value carried with it.
|
|
18
|
+
*/
|
|
19
|
+
push(key, val) {
|
|
20
|
+
const { k, v } = this;
|
|
21
|
+
let i = k.length;
|
|
22
|
+
k.push(key);
|
|
23
|
+
v.push(val);
|
|
24
|
+
while (i > 0) {
|
|
25
|
+
const p = (i - 1) >> 1;
|
|
26
|
+
if (k[p] <= k[i])
|
|
27
|
+
break;
|
|
28
|
+
const tk = k[p];
|
|
29
|
+
const tv = v[p];
|
|
30
|
+
k[p] = k[i];
|
|
31
|
+
v[p] = v[i];
|
|
32
|
+
k[i] = tk;
|
|
33
|
+
v[i] = tv;
|
|
34
|
+
i = p;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Remove the smallest-key entry.
|
|
39
|
+
* @returns Its value; the key goes to {@link poppedKey}.
|
|
40
|
+
*/
|
|
41
|
+
pop() {
|
|
42
|
+
const { k, v } = this;
|
|
43
|
+
const topK = k[0];
|
|
44
|
+
const topV = v[0];
|
|
45
|
+
const lk = k.pop();
|
|
46
|
+
const lv = v.pop();
|
|
47
|
+
if (k.length) {
|
|
48
|
+
k[0] = lk;
|
|
49
|
+
v[0] = lv;
|
|
50
|
+
let i = 0;
|
|
51
|
+
for (;;) {
|
|
52
|
+
const l = 2 * i + 1;
|
|
53
|
+
const r = l + 1;
|
|
54
|
+
let m = i;
|
|
55
|
+
if (l < k.length && k[l] < k[m])
|
|
56
|
+
m = l;
|
|
57
|
+
if (r < k.length && k[r] < k[m])
|
|
58
|
+
m = r;
|
|
59
|
+
if (m === i)
|
|
60
|
+
break;
|
|
61
|
+
const tk = k[m];
|
|
62
|
+
const tv = v[m];
|
|
63
|
+
k[m] = k[i];
|
|
64
|
+
v[m] = v[i];
|
|
65
|
+
k[i] = tk;
|
|
66
|
+
v[i] = tv;
|
|
67
|
+
i = m;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.poppedKey = topK;
|
|
71
|
+
return topV;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Blockers } from './blockers.js';
|
|
2
|
+
import type { Frame } from './geom/frames.js';
|
|
3
|
+
import type { RouteNetworkDestination, Unreached, UnreachedReason } from './graph-types.js';
|
|
4
|
+
import type { NetworkSizes } from './network-sizes.js';
|
|
5
|
+
/** The layer as every pass of the network build reads it. */
|
|
6
|
+
export interface NetworkBuild {
|
|
7
|
+
/** Network sizes. */
|
|
8
|
+
readonly cfg: NetworkSizes;
|
|
9
|
+
/** The directions the layer is drawn in, the plan's first. */
|
|
10
|
+
readonly frames: readonly Frame[];
|
|
11
|
+
/** What a line or a dock may not cross. */
|
|
12
|
+
readonly blockers: Blockers;
|
|
13
|
+
/** The layer's destinations, by id. */
|
|
14
|
+
readonly destinationOf: ReadonlyMap<string, RouteNetworkDestination>;
|
|
15
|
+
/**
|
|
16
|
+
* Records why a destination has no dock yet, with where and by which check; the most informative
|
|
17
|
+
* reason is kept, with its facts.
|
|
18
|
+
*/
|
|
19
|
+
readonly note: (id: string, reason: UnreachedReason, facts?: Omit<Unreached, 'id' | 'reason'>) => void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=network-build.d.ts.map
|