@expofp/wayfinding-compiler 0.0.0-experimental.3b0db16 → 0.0.0-stage

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.
Files changed (100) hide show
  1. package/README.md +2 -445
  2. package/package.json +3 -34
  3. package/dist/index.d.ts +0 -14
  4. package/dist/index.js +0 -3
  5. package/dist/lib/attach.d.ts +0 -43
  6. package/dist/lib/attach.js +0 -199
  7. package/dist/lib/axes.d.ts +0 -76
  8. package/dist/lib/axes.js +0 -103
  9. package/dist/lib/blockers.d.ts +0 -134
  10. package/dist/lib/blockers.js +0 -289
  11. package/dist/lib/cell-array.d.ts +0 -14
  12. package/dist/lib/cell-array.js +0 -25
  13. package/dist/lib/compile-layer.d.ts +0 -41
  14. package/dist/lib/compile-layer.js +0 -241
  15. package/dist/lib/compile-wayfinding.d.ts +0 -90
  16. package/dist/lib/compile-wayfinding.js +0 -203
  17. package/dist/lib/connect.d.ts +0 -35
  18. package/dist/lib/connect.js +0 -206
  19. package/dist/lib/diagnostics.d.ts +0 -190
  20. package/dist/lib/diagnostics.js +0 -634
  21. package/dist/lib/doorways.d.ts +0 -78
  22. package/dist/lib/doorways.js +0 -331
  23. package/dist/lib/emit-network.d.ts +0 -47
  24. package/dist/lib/emit-network.js +0 -105
  25. package/dist/lib/emit-v1.d.ts +0 -31
  26. package/dist/lib/emit-v1.js +0 -56
  27. package/dist/lib/entries.d.ts +0 -123
  28. package/dist/lib/entries.js +0 -448
  29. package/dist/lib/floor-grids.d.ts +0 -48
  30. package/dist/lib/floor-grids.js +0 -185
  31. package/dist/lib/framing.d.ts +0 -20
  32. package/dist/lib/framing.js +0 -187
  33. package/dist/lib/geom/boolean-ops.d.ts +0 -21
  34. package/dist/lib/geom/boolean-ops.js +0 -1
  35. package/dist/lib/geom/clipper2-ops.d.ts +0 -7
  36. package/dist/lib/geom/clipper2-ops.js +0 -50
  37. package/dist/lib/geom/footprint.d.ts +0 -26
  38. package/dist/lib/geom/footprint.js +0 -203
  39. package/dist/lib/geom/frames.d.ts +0 -116
  40. package/dist/lib/geom/frames.js +0 -233
  41. package/dist/lib/geom/mesh-outline.d.ts +0 -13
  42. package/dist/lib/geom/mesh-outline.js +0 -90
  43. package/dist/lib/geom/normalize.d.ts +0 -28
  44. package/dist/lib/geom/normalize.js +0 -118
  45. package/dist/lib/geom/point-key.d.ts +0 -16
  46. package/dist/lib/geom/point-key.js +0 -15
  47. package/dist/lib/geom/primitives.d.ts +0 -105
  48. package/dist/lib/geom/primitives.js +0 -184
  49. package/dist/lib/geom/split-crossings.d.ts +0 -14
  50. package/dist/lib/geom/split-crossings.js +0 -171
  51. package/dist/lib/geom/types.d.ts +0 -9
  52. package/dist/lib/geom/types.js +0 -1
  53. package/dist/lib/geom/walls.d.ts +0 -31
  54. package/dist/lib/geom/walls.js +0 -71
  55. package/dist/lib/graph-invariants.d.ts +0 -49
  56. package/dist/lib/graph-invariants.js +0 -129
  57. package/dist/lib/graph-types.d.ts +0 -143
  58. package/dist/lib/graph-types.js +0 -7
  59. package/dist/lib/mask.d.ts +0 -54
  60. package/dist/lib/mask.js +0 -210
  61. package/dist/lib/min-heap.d.ts +0 -24
  62. package/dist/lib/min-heap.js +0 -73
  63. package/dist/lib/network-build.d.ts +0 -21
  64. package/dist/lib/network-build.js +0 -4
  65. package/dist/lib/network-graph.d.ts +0 -80
  66. package/dist/lib/network-graph.js +0 -467
  67. package/dist/lib/network-sizes.d.ts +0 -29
  68. package/dist/lib/network-sizes.js +0 -22
  69. package/dist/lib/network-store.d.ts +0 -173
  70. package/dist/lib/network-store.js +0 -430
  71. package/dist/lib/openings.d.ts +0 -54
  72. package/dist/lib/openings.js +0 -387
  73. package/dist/lib/place-path.d.ts +0 -47
  74. package/dist/lib/place-path.js +0 -346
  75. package/dist/lib/post-passes.d.ts +0 -31
  76. package/dist/lib/post-passes.js +0 -305
  77. package/dist/lib/reachability.d.ts +0 -58
  78. package/dist/lib/reachability.js +0 -220
  79. package/dist/lib/report.d.ts +0 -49
  80. package/dist/lib/report.js +0 -374
  81. package/dist/lib/router.d.ts +0 -111
  82. package/dist/lib/router.js +0 -388
  83. package/dist/lib/runs.d.ts +0 -92
  84. package/dist/lib/runs.js +0 -232
  85. package/dist/lib/scale.d.ts +0 -58
  86. package/dist/lib/scale.js +0 -185
  87. package/dist/lib/steiner-network.d.ts +0 -19
  88. package/dist/lib/steiner-network.js +0 -183
  89. package/dist/lib/strip.d.ts +0 -43
  90. package/dist/lib/strip.js +0 -200
  91. package/dist/lib/stubs.d.ts +0 -84
  92. package/dist/lib/stubs.js +0 -316
  93. package/dist/lib/transitions.d.ts +0 -72
  94. package/dist/lib/transitions.js +0 -191
  95. package/dist/lib/types.d.ts +0 -126
  96. package/dist/lib/types.js +0 -1
  97. package/dist/lib/validate-source.d.ts +0 -41
  98. package/dist/lib/validate-source.js +0 -133
  99. package/dist/lib/walkable.d.ts +0 -104
  100. package/dist/lib/walkable.js +0 -140
@@ -1,289 +0,0 @@
1
- // What a straight run may not cross — walls, outlines of destinations nobody walks through, doorway
2
- // gaps — indexed by the cells they cover, and the rays and boxes read through the index.
3
- import { rayVsSegment, ringEdges, segmentsCross } from './geom/primitives.js';
4
- import { NODE_SNAP } from './graph-types.js';
5
- /** {@link SegmentGrid} cell side, meters: a booth or two, so a query reads a few cells. */
6
- const GRID_CELL = 4;
7
- /** Padding a segment is filed with, so a hit a hair past its end or on a cell border still finds it. */
8
- const GRID_PAD = 1e-3;
9
- /** What a blocker segment is. */
10
- export var BlockerKind;
11
- (function (BlockerKind) {
12
- BlockerKind[BlockerKind["Wall"] = 0] = "Wall";
13
- BlockerKind[BlockerKind["Solid"] = 1] = "Solid";
14
- BlockerKind[BlockerKind["Gap"] = 2] = "Gap";
15
- })(BlockerKind || (BlockerKind = {}));
16
- /**
17
- * Blocker segments bucketed by the cells their bounds cover. It only narrows the candidates: a segment it
18
- * leaves out has bounds apart from the query's, so no test could hold for it.
19
- */
20
- export class SegmentGrid {
21
- /** Every blocker segment, in {@link Blockers} order: walls, then outlines, then gaps. */
22
- segs = [];
23
- /** What each segment is. */
24
- from = [];
25
- /** Owning destination of a solid edge; empty for walls and gaps. */
26
- owner = [];
27
- cells = new Map();
28
- seen;
29
- stamp = 0;
30
- /** Bounds of the filed cells, so a ray knows when it has left them all. */
31
- c0 = Infinity;
32
- c1 = -Infinity;
33
- r0 = Infinity;
34
- r1 = -Infinity;
35
- constructor(blockers) {
36
- const add = (seg, from, owner) => {
37
- this.segs.push(seg);
38
- this.from.push(from);
39
- this.owner.push(owner);
40
- };
41
- for (const wall of blockers.walls)
42
- add(wall, BlockerKind.Wall, '');
43
- for (const poi of blockers.solid)
44
- for (const edge of poi.edges)
45
- add(edge, BlockerKind.Solid, poi.id);
46
- for (const gap of blockers.gaps)
47
- add(gap, BlockerKind.Gap, '');
48
- this.seen = new Int32Array(this.segs.length);
49
- // The cells the segments reach: a query looks no farther.
50
- for (const seg of this.segs) {
51
- this.c0 = Math.min(this.c0, Math.floor((Math.min(seg[0], seg[2]) - GRID_PAD) / GRID_CELL));
52
- this.c1 = Math.max(this.c1, Math.floor((Math.max(seg[0], seg[2]) + GRID_PAD) / GRID_CELL));
53
- this.r0 = Math.min(this.r0, Math.floor((Math.min(seg[1], seg[3]) - GRID_PAD) / GRID_CELL));
54
- this.r1 = Math.max(this.r1, Math.floor((Math.max(seg[1], seg[3]) + GRID_PAD) / GRID_CELL));
55
- }
56
- this.segs.forEach((seg, i) => this.cover(Math.min(seg[0], seg[2]) - GRID_PAD, Math.min(seg[1], seg[3]) - GRID_PAD, Math.max(seg[0], seg[2]) + GRID_PAD, Math.max(seg[1], seg[3]) + GRID_PAD, (key) => {
57
- const list = this.cells.get(key);
58
- if (list)
59
- list.push(i);
60
- else
61
- this.cells.set(key, [i]);
62
- }));
63
- }
64
- cover(x0, y0, x1, y1, visit) {
65
- const c0 = Math.max(this.c0, Math.floor(x0 / GRID_CELL));
66
- const c1 = Math.min(this.c1, Math.floor(x1 / GRID_CELL));
67
- const r0 = Math.max(this.r0, Math.floor(y0 / GRID_CELL));
68
- const r1 = Math.min(this.r1, Math.floor(y1 / GRID_CELL));
69
- for (let c = c0; c <= c1; c++)
70
- for (let r = r0; r <= r1; r++)
71
- visit(c * 1_000_003 + r);
72
- }
73
- /**
74
- * Walk the cells a ray passes through, nearest first, handing each segment once.
75
- * @param ox Ray origin x.
76
- * @param oy Ray origin y.
77
- * @param nx Ray direction x.
78
- * @param ny Ray direction y.
79
- * @param from Ray parameter to start at.
80
- * @param visit Gets a cell's new segments and the parameter where the ray leaves it; true stops.
81
- */
82
- alongRay(ox, oy, nx, ny, from, visit) {
83
- const sx = ox + nx * from;
84
- const sy = oy + ny * from;
85
- let c = Math.floor(sx / GRID_CELL);
86
- let r = Math.floor(sy / GRID_CELL);
87
- const stepC = nx > 0 ? 1 : nx < 0 ? -1 : 0;
88
- const stepR = ny > 0 ? 1 : ny < 0 ? -1 : 0;
89
- // Ray parameter at the next column/row border, and its growth per cell.
90
- const border = (p, d, cell) => d > 0 ? ((cell + 1) * GRID_CELL - p) / d : d < 0 ? (cell * GRID_CELL - p) / d : Infinity;
91
- let nextC = from + border(sx, nx, c);
92
- let nextR = from + border(sy, ny, r);
93
- const perC = nx ? GRID_CELL / Math.abs(nx) : Infinity;
94
- const perR = ny ? GRID_CELL / Math.abs(ny) : Infinity;
95
- this.stamp++;
96
- // A ray going nowhere meets only what its start cell holds.
97
- const still = !stepC && !stepR;
98
- for (;;) {
99
- const fresh = [];
100
- for (const i of this.cells.get(c * 1_000_003 + r) ?? []) {
101
- if (this.seen[i] === this.stamp)
102
- continue;
103
- this.seen[i] = this.stamp;
104
- fresh.push(i);
105
- }
106
- if (visit(fresh, Math.min(nextC, nextR)) || still)
107
- return;
108
- if (nextC < nextR) {
109
- c += stepC;
110
- nextC += perC;
111
- }
112
- else {
113
- r += stepR;
114
- nextR += perR;
115
- }
116
- // Past the last filed cell the ray meets nothing more.
117
- if ((stepC > 0 && c > this.c1) || (stepC < 0 && c < this.c0))
118
- return;
119
- if ((stepR > 0 && r > this.r1) || (stepR < 0 && r < this.r0))
120
- return;
121
- if (!stepC && (c < this.c0 || c > this.c1))
122
- return;
123
- if (!stepR && (r < this.r0 || r > this.r1))
124
- return;
125
- }
126
- }
127
- /**
128
- * Segments whose bounds meet a box, each once, in {@link Blockers} order.
129
- * @param x0 Box min x.
130
- * @param y0 Box min y.
131
- * @param x1 Box max x.
132
- * @param y1 Box max y.
133
- * @returns Their positions in {@link segs}.
134
- */
135
- near(x0, y0, x1, y1) {
136
- const found = [];
137
- this.stamp++;
138
- this.cover(x0, y0, x1, y1, (key) => {
139
- for (const i of this.cells.get(key) ?? []) {
140
- if (this.seen[i] === this.stamp)
141
- continue;
142
- this.seen[i] = this.stamp;
143
- const seg = this.segs[i];
144
- if (Math.max(seg[0], seg[2]) >= x0 &&
145
- Math.min(seg[0], seg[2]) <= x1 &&
146
- Math.max(seg[1], seg[3]) >= y0 &&
147
- Math.min(seg[1], seg[3]) <= y1)
148
- found.push(i);
149
- }
150
- });
151
- return found.sort((a, b) => a - b);
152
- }
153
- /**
154
- * Whether a segment crosses any blocker of one kind (exact `segmentsCross` on the candidates).
155
- * @param seg The segment.
156
- * @param from Blocker kind.
157
- * @param except Destination whose own edges do not count.
158
- * @returns True when one crosses it.
159
- */
160
- crosses(seg, from, except) {
161
- return this.crossing(seg, from, except).length > 0;
162
- }
163
- /**
164
- * The blockers of one kind a segment crosses (exact `segmentsCross` on the candidates).
165
- * @param seg The segment.
166
- * @param from Blocker kind.
167
- * @param except Destination whose own edges do not count.
168
- * @returns The blockers crossed.
169
- */
170
- crossing(seg, from, except) {
171
- return this.near(Math.min(seg[0], seg[2]), Math.min(seg[1], seg[3]), Math.max(seg[0], seg[2]), Math.max(seg[1], seg[3]))
172
- .filter((i) => this.from[i] === from &&
173
- (except === undefined || this.owner[i] !== except) &&
174
- segmentsCross(seg, this.segs[i]))
175
- .map((i) => this.segs[i]);
176
- }
177
- }
178
- /**
179
- * Walls and outlines whose bounds meet a box.
180
- * @param blockers The blockers.
181
- * @param box The box.
182
- * @returns Each segment with its owning destination; no owner for a wall.
183
- */
184
- export function blockersNear(blockers, box) {
185
- const grid = blockerGrid(blockers);
186
- const [x0, y0, x1, y1] = box;
187
- return grid
188
- .near(x0, y0, x1, y1)
189
- .filter((i) => grid.from[i] !== BlockerKind.Gap)
190
- .map((i) => grid.from[i] === BlockerKind.Solid
191
- ? { seg: grid.segs[i], owner: grid.owner[i] }
192
- : { seg: grid.segs[i] });
193
- }
194
- /** One grid per blockers object, built lazily. */
195
- const grids = new WeakMap();
196
- /**
197
- * The index of a blockers object, built once and kept with it.
198
- * @param blockers The blockers.
199
- * @returns Their index.
200
- */
201
- export function blockerGrid(blockers) {
202
- let grid = grids.get(blockers);
203
- if (!grid) {
204
- grid = new SegmentGrid(blockers);
205
- grids.set(blockers, grid);
206
- }
207
- return grid;
208
- }
209
- /**
210
- * Whether a segment crosses a wall, a closed doorway side or the outline of a destination nobody
211
- * walks through: the rule every network line keeps.
212
- * @param blockers The blockers.
213
- * @param seg The segment.
214
- * @returns True when it crosses one.
215
- */
216
- export function crossesBlocker(blockers, seg) {
217
- const grid = blockerGrid(blockers);
218
- return grid.crosses(seg, BlockerKind.Wall) || grid.crosses(seg, BlockerKind.Solid);
219
- }
220
- /**
221
- * What a run out of a destination may not cross, read once for the layer.
222
- * @param destinations Every destination on the layer.
223
- * @param walls The layer's wall segments.
224
- * @param doors The layer's doorways.
225
- * @param drawn The rest of each destination's drawing, by the destination's id.
226
- * @param walled Destinations entered through doorways: the walls they line stand in for them.
227
- * @param passes Wall spans an entrance's dock may cross, by the entrance's id.
228
- * @returns The blockers.
229
- */
230
- export function blockersOf(destinations, walls, doors, drawn = [], walled = new Set(), passes) {
231
- return {
232
- walls,
233
- solid: [
234
- ...destinations
235
- .filter((d) => !d.crossable && d.polygon.length >= 3)
236
- .map((d) => ({ id: d.id, edges: ringEdges(d.polygon) })),
237
- ...drawn.map((block) => ({ id: block.id, edges: ringEdges(block.ring) })),
238
- ],
239
- gaps: doors.map((door) => door.gap),
240
- walled,
241
- ...(passes && { passes }),
242
- };
243
- }
244
- /**
245
- * How far a ray runs before it meets a blocker, by exact intersection, and what it met.
246
- * @param ox Ray origin x.
247
- * @param oy Ray origin y.
248
- * @param nx Ray direction x.
249
- * @param ny Ray direction y.
250
- * @param ownId Destination the ray leaves.
251
- * @param own `'skip'` ignores its outline; `'ahead'` counts only the outline ahead of the start.
252
- * @param withGaps Whether a doorway gap stops the ray.
253
- * @param blockers What the ray may meet.
254
- * @returns The distance, or `Infinity` when the ray meets nothing.
255
- */
256
- export function freeRun(ox, oy, nx, ny, ownId, own, withGaps, blockers) {
257
- let best = Infinity;
258
- const hit = [0, 0];
259
- // `from`: where hits start to count. A wall along the start face closes the ray; the own outline there
260
- // is the face itself and does not.
261
- const test = (seg, from) => {
262
- if (!rayVsSegment(hit, ox, oy, nx, ny, seg[0], seg[1], seg[2], seg[3]))
263
- return;
264
- const [t, u] = hit;
265
- if (u < -1e-9 || u > 1 + 1e-9 || t <= from)
266
- return;
267
- if (t < best)
268
- best = Math.max(0, t);
269
- };
270
- const grid = blockerGrid(blockers);
271
- // Cell by cell outward: a hit's segment is filed in the hit's cell, so once the best hit is nearer than
272
- // the cell being left, no later cell holds a nearer one.
273
- grid.alongRay(ox, oy, nx, ny, -2 * NODE_SNAP, (ids, leaves) => {
274
- for (const i of ids) {
275
- const seg = grid.segs[i];
276
- const owner = grid.owner[i];
277
- if (grid.from[i] === BlockerKind.Wall)
278
- test(seg, -NODE_SNAP);
279
- else if (grid.from[i] === BlockerKind.Gap) {
280
- if (withGaps)
281
- test(seg, NODE_SNAP);
282
- }
283
- else if (owner !== ownId || own !== 'skip')
284
- test(seg, owner === ownId ? NODE_SNAP : -NODE_SNAP);
285
- }
286
- return best < leaves - GRID_PAD;
287
- });
288
- return best;
289
- }
@@ -1,14 +0,0 @@
1
- /** The engine refused the memory a floor's raster needs: the floor is too large for this device. */
2
- export declare class CellsRefusedError extends Error {
3
- constructor(length: number);
4
- }
5
- /**
6
- * A raster-sized typed array. An engine refuses a valid length only for want of memory, whatever its
7
- * message says, so any refusal of one throws {@link CellsRefusedError}; an invalid length is a bug and
8
- * its error passes through.
9
- * @param Ctor The typed array class.
10
- * @param length Its length, in cells.
11
- * @returns The zeroed array.
12
- */
13
- export declare function cellArray<T>(Ctor: new (length: number) => T, length: number): T;
14
- //# sourceMappingURL=cell-array.d.ts.map
@@ -1,25 +0,0 @@
1
- /** The engine refused the memory a floor's raster needs: the floor is too large for this device. */
2
- export class CellsRefusedError extends Error {
3
- constructor(length) {
4
- super(`the engine refused a raster of ${length} cells`);
5
- this.name = 'CellsRefusedError';
6
- }
7
- }
8
- /**
9
- * A raster-sized typed array. An engine refuses a valid length only for want of memory, whatever its
10
- * message says, so any refusal of one throws {@link CellsRefusedError}; an invalid length is a bug and
11
- * its error passes through.
12
- * @param Ctor The typed array class.
13
- * @param length Its length, in cells.
14
- * @returns The zeroed array.
15
- */
16
- export function cellArray(Ctor, length) {
17
- try {
18
- return new Ctor(length);
19
- }
20
- catch (error) {
21
- if (Number.isSafeInteger(length) && length >= 0)
22
- throw new CellsRefusedError(length);
23
- throw error;
24
- }
25
- }
@@ -1,41 +0,0 @@
1
- import type { NormalizeFacts } from './diagnostics.js';
2
- import type { BooleanOps } from './geom/boolean-ops.js';
3
- import type { RouteNetwork } from './graph-types.js';
4
- import type { SourceDestination, WayfindingLayerSource } from './types.js';
5
- import { type WalkableArea } from './walkable.js';
6
- /** One layer's stage artifacts. */
7
- interface CompiledLayer {
8
- /** Globally unique layer id. */
9
- readonly layer: string;
10
- /** The destinations as the stages saw them, rings normalized and carved. */
11
- readonly destinations: SourceDestination[];
12
- /** Walkable area. */
13
- readonly walkable: WalkableArea;
14
- /** Route network. */
15
- readonly network: RouteNetwork;
16
- /** What normalization did to the drawing, for the diagnostics. */
17
- readonly facts: NormalizeFacts;
18
- }
19
- /** Options of one layer's compile. */
20
- interface CompileLayerOptions {
21
- /** A walkable area already built for this layer; the stage is skipped. */
22
- readonly walkable?: WalkableArea;
23
- /** Called as each stage finishes; `cached` when it was handed its artifact. */
24
- readonly onStage?: (stage: 'walkable' | 'network', cached: boolean) => void;
25
- }
26
- /**
27
- * One layer's pipeline: walkable area, then route network.
28
- * @param source One layer's walls and destinations.
29
- * @param ops Polygon boolean backend.
30
- * @param options A ready walkable area and the stage callback.
31
- * @returns The layer's walkable area and route network.
32
- */
33
- export declare function compileLayer(source: WayfindingLayerSource, ops: BooleanOps, options?: CompileLayerOptions): CompiledLayer;
34
- /**
35
- * Destinations sorted by id, then outline, so input order does not change the network.
36
- * @param destinations The destinations as given.
37
- * @returns A sorted copy.
38
- */
39
- export declare function inCanonicalOrder(destinations: readonly SourceDestination[]): SourceDestination[];
40
- export {};
41
- //# sourceMappingURL=compile-layer.d.ts.map
@@ -1,241 +0,0 @@
1
- // One layer's pipeline: the destinations read as the stages see them — rings normalized, footprints
2
- // carved back to the walls, zones and blocking stands read crossable — then the walkable area and the
3
- // route network over it.
4
- import { blockersOf } from './blockers.js';
5
- import { doorOwners, doorwaySides, findDoorways } from './doorways.js';
6
- import { dockFor, standingBefore, stubLength, waysInto } from './entries.js';
7
- import { footprintOf } from './geom/footprint.js';
8
- import { cleanRing } from './geom/normalize.js';
9
- import { boundsOf, pointInRing, signedArea, vertexMean } from './geom/primitives.js';
10
- import { prepareWalls } from './geom/walls.js';
11
- import { NETWORK_SIZES } from './network-sizes.js';
12
- import { entranceOpenings } from './openings.js';
13
- import { buildSteinerNetwork } from './steiner-network.js';
14
- import { buildWalkableArea, carveFootprints, inBuilding, outerContoursOf, WALL_HALF_WIDTH, wallStripsOf, } from './walkable.js';
15
- /**
16
- * One layer's pipeline: walkable area, then route network.
17
- * @param source One layer's walls and destinations.
18
- * @param ops Polygon boolean backend.
19
- * @param options A ready walkable area and the stage callback.
20
- * @returns The layer's walkable area and route network.
21
- */
22
- export function compileLayer(source, ops, options) {
23
- // Walls first: the building's outline is theirs alone. The walls are read and offset once and shared
24
- // by every stage below.
25
- const prepared = prepareWalls(source.walls);
26
- const contours = options?.walkable ? undefined : outerContoursOf(prepared.runs);
27
- const outline = options?.walkable?.outline ?? ops.union(contours ?? []);
28
- // A destination with neither its middle nor a corner inside the building never reaches the network:
29
- // it is reported off the floor without being traced, carved or entered.
30
- const offFloor = (d) => {
31
- const ring = cleanRing(d.dock ?? d.polygon);
32
- return outline.length > 0 && ![vertexMean(ring), ...ring].some((p) => inBuilding(outline, p));
33
- };
34
- const sorted = inCanonicalOrder(source.destinations);
35
- // Entrances go through the walls they stand across, or the nearest wall when they stand outside;
36
- // the walls stay whole. An entrance that got an opening is on the floor.
37
- const opened = entranceOpenings(prepared, outline, sorted, offFloor);
38
- const openedIds = new Set(opened.openings.map((o) => o.id));
39
- const outside = sorted.filter((d) => offFloor(d) && !openedIds.has(d.id));
40
- const shapes = [];
41
- const zones = [];
42
- let sized;
43
- // Normalize rings once so docking grows one stub per real face; a ring that normalizes below 3
44
- // vertices stays raw, visible to diagnostics. `parts` become the footprint and are dropped from
45
- // the artifacts.
46
- const destinations = sorted
47
- .filter((d) => !outside.includes(d))
48
- .map((d) => {
49
- const { parts, ...rest } = d;
50
- const shape = parts?.length
51
- ? footprintOf(parts, ops, rest.dock)
52
- : { ring: null, drawn: [] };
53
- const polygon = shape.ring ?? cleanRing(rest.polygon);
54
- const dock = rest.dock ? cleanRing(rest.dock) : undefined;
55
- const cleaned = dock && dock.length >= 3 ? { ...rest, dock } : rest;
56
- const drawn = shape.drawn.length ? { ...cleaned, drawnShape: shape.drawn } : cleaned;
57
- if (parts?.length && shape.how)
58
- shapes.push({ id: d.id, how: shape.how, parts: parts.length });
59
- return polygon.length >= 3 ? { ...drawn, polygon } : drawn;
60
- })
61
- .map((d, index, all) => {
62
- sized ??= all.map(sizeOf);
63
- if (d.crossable || !holdsAnother(sized[index], sized))
64
- return d;
65
- zones.push(d.id);
66
- return { ...d, crossable: true };
67
- });
68
- // Footprints are carved back to their walls here, and every later stage sees the carved shape.
69
- const shared = {
70
- walls: prepared,
71
- strips: wallStripsOf(ops, prepared.segments, WALL_HALF_WIDTH),
72
- ...(contours && { contours }),
73
- };
74
- // Every piece of a solid destination is carved: footprints first, so carved rings pair back by
75
- // position, then the rest of the drawing. Owners are positions, not ids: ids may repeat or be
76
- // blank.
77
- const solid = [];
78
- const rings = [];
79
- const owner = [];
80
- destinations.forEach((d, index) => {
81
- if (d.crossable)
82
- return;
83
- solid.push(index);
84
- rings.push(d.polygon);
85
- owner.push(index);
86
- });
87
- // A piece of the drawing another destination stands in is ground around it, not a block.
88
- const holdsOther = (block, index) => destinations.some((other, k) => k !== index && other.polygon.length >= 3 && pointInRing(block, vertexMean(other.polygon)));
89
- for (const index of solid)
90
- for (const block of destinations[index].drawnShape ?? []) {
91
- rings.push(block);
92
- owner.push(index);
93
- }
94
- const { rings: obstacles, clipped } = carveFootprints(rings, shared.strips, ops);
95
- // The rest of the drawing is input only; it stays off the artifacts, which are in plan units.
96
- let next = 0;
97
- const carved = destinations.map(({ drawnShape: _block, ...rest }) => rest.crossable ? rest : { ...rest, polygon: obstacles[next++] });
98
- // The rest of each drawing blocks ways in.
99
- const drawnOf = (index) => (destinations[index].drawnShape ?? [])
100
- .filter((ring) => !holdsOther(ring, index))
101
- .map((ring) => ({ id: destinations[index].id, ring }));
102
- // Read on the carved outlines, as every later stage sees them. A destination named here stops
103
- // being an obstacle: its ring leaves the floor, without re-carving.
104
- const passable = crossableByStanding(carved, prepared, NETWORK_SIZES, solid.flatMap(drawnOf));
105
- const open = carved.map((d, index) => (passable.has(index) ? { ...d, crossable: true } : d));
106
- const standing = obstacles.filter((_, index) => !passable.has(owner[index]));
107
- const walkable = options?.walkable ??
108
- buildWalkableArea({ walls: source.walls, obstacles: standing }, ops, WALL_HALF_WIDTH, shared);
109
- options?.onStage?.('walkable', options.walkable != null);
110
- // Raw walls too: doorways and ways in use their exact geometry.
111
- const drawnBlocks = solid.filter((index) => !passable.has(index)).flatMap(drawnOf);
112
- const network = buildSteinerNetwork(walkable.rings, open, source.walls, prepared, drawnBlocks, opened.openings);
113
- options?.onStage?.('network', false);
114
- const off = outside.map(({ parts: _parts, ...d }) => d);
115
- // An entrance outside that opened no wall keeps what the search found.
116
- const noFloor = new Map(opened.unreached.map((u) => [u.id, u]));
117
- const byEntrance = new Map();
118
- for (const { id, at, by, distance } of opened.openings) {
119
- const entry = byEntrance.get(id) ?? {
120
- id,
121
- by,
122
- at: [],
123
- ...(distance !== undefined && { distance }),
124
- };
125
- // A wall open to both sides is one place.
126
- const seen = entry.at.some(([x, y]) => x === at[0] && y === at[1]);
127
- byEntrance.set(id, seen ? entry : { ...entry, at: [...entry.at, at] });
128
- }
129
- const facts = {
130
- wallEntries: prepared.entries,
131
- shapes,
132
- // Footprint rings only: a clipped auxiliary drawn block is not the footprint being cut back.
133
- clipped: clipped
134
- .slice(0, solid.length)
135
- .flatMap((cut, index) => (cut ? [destinations[owner[index]].id] : [])),
136
- crossable: [
137
- ...zones.map((id) => ({ id, by: 'holds-another' })),
138
- ...[...passable].map(([index, victims]) => ({
139
- id: carved[index].id,
140
- by: 'stands-before',
141
- victims,
142
- })),
143
- ],
144
- openings: [...byEntrance.values()],
145
- entrancesUnopened: open.flatMap((d) => (d.entrance && !openedIds.has(d.id) ? [d.id] : [])),
146
- };
147
- return {
148
- layer: source.layer,
149
- destinations: [...open, ...off],
150
- walkable,
151
- network: {
152
- ...network,
153
- stats: {
154
- ...network.stats,
155
- unreached: [
156
- ...network.stats.unreached,
157
- ...off.map((d) => noFloor.get(d.id) ?? { id: d.id, reason: 'no-floor', outside: true }),
158
- ],
159
- },
160
- },
161
- facts,
162
- };
163
- }
164
- /**
165
- * Destinations a route must cross because they stand in front of another destination's only way in.
166
- * Only when that destination has no way in at all, and the one in front is reachable itself.
167
- * @param destinations The layer's destinations, footprints already carved.
168
- * @param prepared The layer's walls as drawn, already read: the doorways are theirs.
169
- * @param cfg Network sizes.
170
- * @param drawn The rest of each destination's drawing, as the network will see it.
171
- * @returns The positions of the destinations to walk through, each with the ids it walled in.
172
- */
173
- function crossableByStanding(destinations, prepared, cfg, drawn) {
174
- // Every gap in the walls, including ones a footprint stands across.
175
- const doors = findDoorways(prepared);
176
- const owners = doorOwners(doors, destinations, prepared.segments, cfg);
177
- const blockers = blockersOf(destinations, [...prepared.segments, ...doorwaySides(doors, cfg)], doors, drawn);
178
- // Usable as the network judges it: a person fits in front, and its stub docks lawfully.
179
- const usable = (d, ways) => ways.some((way) => {
180
- const run = stubLength(way, cfg);
181
- return (way.depth >= cfg.clear &&
182
- dockFor(d, way, [way.mx + way.nx * run, way.my + way.ny * run], blockers) !== null);
183
- });
184
- const ways = destinations.map((d) => waysInto(d, owners, blockers, cfg));
185
- const reachable = destinations.map((d, index) => usable(d, ways[index]));
186
- // A repeated id names none of its destinations (-1).
187
- const at = new Map();
188
- destinations.forEach((d, index) => at.set(d.id, at.has(d.id) ? -1 : index));
189
- const passable = new Map();
190
- for (const [index, victim] of destinations.entries()) {
191
- if (victim.crossable || reachable[index])
192
- continue;
193
- // Whoever stands in the strip a person needs in front of a way in closed it.
194
- for (const way of ways[index])
195
- for (const id of standingBefore(way, victim.id, blockers, cfg.clear)) {
196
- const who = at.get(id) ?? -1;
197
- if (who < 0 || !reachable[who])
198
- continue;
199
- const victims = passable.get(who) ?? [];
200
- if (!victims.includes(victim.id))
201
- passable.set(who, [...victims, victim.id]);
202
- }
203
- }
204
- return passable;
205
- }
206
- /**
207
- * Destinations sorted by id, then outline, so input order does not change the network.
208
- * @param destinations The destinations as given.
209
- * @returns A sorted copy.
210
- */
211
- export function inCanonicalOrder(destinations) {
212
- const outline = (d) => JSON.stringify([d.polygon, d.parts ?? [], d.dock ?? []]);
213
- const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
214
- return [...destinations].sort((a, b) => compare(a.id, b.id) || compare(outline(a), outline(b)));
215
- }
216
- function sizeOf({ polygon }) {
217
- return {
218
- polygon,
219
- area: polygon.length < 3 ? 0 : Math.abs(signedArea(polygon)),
220
- box: boundsOf(polygon),
221
- middle: polygon.length < 3 ? [NaN, NaN] : vertexMean(polygon),
222
- };
223
- }
224
- /**
225
- * Whether a smaller destination's centroid lies inside this one (a pavilion, a hall of stands).
226
- * Such a destination is a zone people walk in, not an obstacle.
227
- * @param d The destination.
228
- * @param all Every destination on the layer.
229
- * @returns True when `d` holds another destination.
230
- */
231
- function holdsAnother(d, all) {
232
- if (d.polygon.length < 3)
233
- return false;
234
- const [x0, y0, x1, y1] = d.box;
235
- return all.some((other) => {
236
- if (other === d || other.polygon.length < 3 || other.area >= d.area)
237
- return false;
238
- const [x, y] = other.middle;
239
- return x >= x0 && x <= x1 && y >= y0 && y <= y1 && pointInRing(d.polygon, other.middle);
240
- });
241
- }