@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,43 +0,0 @@
1
- import { type Blockers } from './blockers.js';
2
- import type { Segment, Vec2 } from './geom/types.js';
3
- /**
4
- * The longest stretch of an edge with a person's width of free strip outside it, computed exactly from
5
- * the blockers crossing that strip. The destination's own outline is ignored.
6
- * @param edge The edge.
7
- * @param inward Its inward unit normal.
8
- * @param blockers What stands around it.
9
- * @param ownId Destination the edge belongs to.
10
- * @returns `[from, to]` in `[0, 1]`; empty when nothing is free.
11
- */
12
- export declare function freeRunOf(edge: Segment, inward: Vec2, blockers: Blockers, ownId: string): [number, number];
13
- /**
14
- * How far the aisle in front of an edge reaches: the height, along the outward normal, at which what
15
- * stands across — other destinations' outlines, and wall lines (their doorways included) that a
16
- * destination entered through doorways lines within a person's width behind them, a walled booth's or
17
- * a room row's — covers half the edge's width or more, each counted at its nearest point. A room
18
- * corner or a gap in the row across before a part of the edge does not set the aisle, and a hall wall
19
- * has no row across, whatever stands with its back to it.
20
- * @param edge The edge.
21
- * @param outward Its outward unit normal.
22
- * @param blockers What stands around it.
23
- * @param ownId Destination the edge belongs to.
24
- * @param limit How far in front to look; the strip ends there.
25
- * @returns The height and the destinations across, or `Infinity` and none when nothing stands across
26
- * half the edge within `limit`.
27
- */
28
- export declare function frontDepth(edge: Segment, outward: Vec2, blockers: Blockers, ownId: string, limit: number): {
29
- depth: number;
30
- across: string[];
31
- };
32
- /**
33
- * How far the strip in front of an edge is free: to the nearest wall, outline or doorway gap before any
34
- * part of it.
35
- * @param edge The edge.
36
- * @param outward Its outward unit normal.
37
- * @param blockers What stands around it.
38
- * @param ownId Destination the edge belongs to; empty for none.
39
- * @param limit How far in front to look.
40
- * @returns The height, or `Infinity` when the strip is free within `limit`.
41
- */
42
- export declare function stripDepth(edge: Segment, outward: Vec2, blockers: Blockers, ownId: string, limit: number): number;
43
- //# sourceMappingURL=strip.d.ts.map
package/dist/lib/strip.js DELETED
@@ -1,200 +0,0 @@
1
- // The strip in front of an edge: how far it is free, and what stands across it.
2
- import { blockerGrid, BlockerKind } from './blockers.js';
3
- import { POINT_EPS } from './geom/point-key.js';
4
- import { boundsOf } from './geom/primitives.js';
5
- import { NODE_SNAP } from './graph-types.js';
6
- import { MIN_PASS } from './network-sizes.js';
7
- /**
8
- * The longest stretch of an edge with a person's width of free strip outside it, computed exactly from
9
- * the blockers crossing that strip. The destination's own outline is ignored.
10
- * @param edge The edge.
11
- * @param inward Its inward unit normal.
12
- * @param blockers What stands around it.
13
- * @param ownId Destination the edge belongs to.
14
- * @returns `[from, to]` in `[0, 1]`; empty when nothing is free.
15
- */
16
- export function freeRunOf(edge, inward, blockers, ownId) {
17
- const ex = edge[2] - edge[0];
18
- const ey = edge[3] - edge[1];
19
- const span = ex * ex + ey * ey;
20
- if (!span)
21
- return [0, 0];
22
- const reach = 2 * MIN_PASS;
23
- // Stretches of the edge (as fractions) shaded by a blocker.
24
- const shaded = [];
25
- // Only blockers within the strip's box can shade the edge.
26
- const bx0 = Math.min(edge[0], edge[2]) - reach;
27
- const bx1 = Math.max(edge[0], edge[2]) + reach;
28
- const by0 = Math.min(edge[1], edge[3]) - reach;
29
- const by1 = Math.max(edge[1], edge[3]) + reach;
30
- const shade = (seg) => {
31
- const ta = ((seg[0] - edge[0]) * ex + (seg[1] - edge[1]) * ey) / span;
32
- const tb = ((seg[2] - edge[0]) * ex + (seg[3] - edge[1]) * ey) / span;
33
- const ha = -((seg[0] - edge[0]) * inward[0] + (seg[1] - edge[1]) * inward[1]);
34
- const hb = -((seg[2] - edge[0]) * inward[0] + (seg[3] - edge[1]) * inward[1]);
35
- if ((ha < 0 && hb < 0) || (ha > reach && hb > reach))
36
- return;
37
- let ua = ta;
38
- let ub = tb;
39
- // Clip the blocker to the strip.
40
- if (ha !== hb) {
41
- const at = (h) => ta + ((tb - ta) * (h - ha)) / (hb - ha);
42
- ua = at(Math.max(0, Math.min(ha, hb)));
43
- ub = at(Math.min(reach, Math.max(ha, hb)));
44
- }
45
- const lo = Math.max(0, Math.min(ua, ub));
46
- const hi = Math.min(1, Math.max(ua, ub));
47
- if (hi > lo)
48
- shaded.push([lo, hi]);
49
- };
50
- const grid = blockerGrid(blockers);
51
- for (const i of grid.near(bx0, by0, bx1, by1))
52
- if (grid.from[i] === BlockerKind.Wall ||
53
- (grid.from[i] === BlockerKind.Solid && grid.owner[i] !== ownId))
54
- shade(grid.segs[i]);
55
- shaded.sort((a, b) => a[0] - b[0]);
56
- let walked = 0;
57
- let best = [0, 0];
58
- for (const [lo, hi] of shaded) {
59
- if (lo > walked && lo - walked > best[1] - best[0])
60
- best = [walked, lo];
61
- walked = Math.max(walked, hi);
62
- }
63
- if (1 - walked > best[1] - best[0])
64
- best = [walked, 1];
65
- return best;
66
- }
67
- /**
68
- * The blockers in the strip in front of an edge, each as the share of the edge's width it stands before
69
- * and its nearest height along the outward normal, nearest first. Exact, from the segments; the edge's
70
- * own destination's outline, and anything on the edge or behind it, is left out.
71
- * @param edge The edge.
72
- * @param outward Its outward unit normal.
73
- * @param blockers What stands around it.
74
- * @param ownId Destination the edge belongs to.
75
- * @param far How far in front the strip reaches.
76
- * @returns The parts.
77
- */
78
- function stripParts(edge, outward, blockers, ownId, far) {
79
- const ex = edge[2] - edge[0];
80
- const ey = edge[3] - edge[1];
81
- const span = ex * ex + ey * ey;
82
- if (!span || !(far > 0))
83
- return [];
84
- const reach = Math.min(far, 1e6);
85
- const box = boundsOf([
86
- [edge[0], edge[1]],
87
- [edge[2], edge[3]],
88
- [edge[0] + outward[0] * reach, edge[1] + outward[1] * reach],
89
- [edge[2] + outward[0] * reach, edge[3] + outward[1] * reach],
90
- ]);
91
- const grid = blockerGrid(blockers);
92
- const parts = [];
93
- for (const i of grid.near(...box)) {
94
- if (grid.from[i] === BlockerKind.Solid && grid.owner[i] === ownId)
95
- continue;
96
- const seg = grid.segs[i];
97
- // Along the edge (as a share of its length) and in front of it (as a height), at both ends.
98
- const ta = ((seg[0] - edge[0]) * ex + (seg[1] - edge[1]) * ey) / span;
99
- const tb = ((seg[2] - edge[0]) * ex + (seg[3] - edge[1]) * ey) / span;
100
- const ha = (seg[0] - edge[0]) * outward[0] + (seg[1] - edge[1]) * outward[1];
101
- const hb = (seg[2] - edge[0]) * outward[0] + (seg[3] - edge[1]) * outward[1];
102
- // Square to the edge, it stands before none of its width.
103
- if (ta === tb)
104
- continue;
105
- // The share of the edge the segment stands before, and the segment's own parameters there:
106
- // heights are read over that visible part alone.
107
- const s0 = Math.max(0, Math.min(ta, tb));
108
- const s1 = Math.min(1, Math.max(ta, tb));
109
- if (s0 >= s1)
110
- continue;
111
- const at = (t) => (t - ta) / (tb - ta);
112
- const h0 = ha + (hb - ha) * at(s0);
113
- const h1 = ha + (hb - ha) * at(s1);
114
- const low = Math.min(h0, h1);
115
- // On the edge or behind it, or reaching down to it: the edge of a neighbor, not a stand ahead.
116
- if (low <= NODE_SNAP)
117
- continue;
118
- parts.push({ s0, s1, h: low, from: grid.from[i], owner: grid.owner[i] });
119
- }
120
- return parts.sort((a, b) => a.h - b.h);
121
- }
122
- /**
123
- * How far the aisle in front of an edge reaches: the height, along the outward normal, at which what
124
- * stands across — other destinations' outlines, and wall lines (their doorways included) that a
125
- * destination entered through doorways lines within a person's width behind them, a walled booth's or
126
- * a room row's — covers half the edge's width or more, each counted at its nearest point. A room
127
- * corner or a gap in the row across before a part of the edge does not set the aisle, and a hall wall
128
- * has no row across, whatever stands with its back to it.
129
- * @param edge The edge.
130
- * @param outward Its outward unit normal.
131
- * @param blockers What stands around it.
132
- * @param ownId Destination the edge belongs to.
133
- * @param limit How far in front to look; the strip ends there.
134
- * @returns The height and the destinations across, or `Infinity` and none when nothing stands across
135
- * half the edge within `limit`.
136
- */
137
- export function frontDepth(edge, outward, blockers, ownId, limit) {
138
- const lining = 2 * MIN_PASS;
139
- const parts = stripParts(edge, outward, blockers, ownId, limit + lining);
140
- const liner = (wall) => parts.find((p) => p.from === BlockerKind.Solid &&
141
- blockers.walled.has(p.owner) &&
142
- p.h >= wall.h &&
143
- p.h - wall.h <= lining &&
144
- p.s0 < wall.s1 &&
145
- wall.s0 < p.s1);
146
- const across = [];
147
- for (const p of parts) {
148
- // The wall the free run stopped at lies exactly at the limit: a hair of float noise may not
149
- // hide the aisle's own far side.
150
- if (p.h > limit + POINT_EPS)
151
- continue;
152
- if (p.from === BlockerKind.Solid)
153
- across.push(p);
154
- else {
155
- // A wall line and its doorways: the far side of a corridor of rooms.
156
- const room = liner(p);
157
- if (room)
158
- across.push({ ...p, owner: room.owner });
159
- }
160
- }
161
- // Nearest first, until what stands across covers half the edge.
162
- const covered = [];
163
- const owners = new Set();
164
- for (const part of across) {
165
- owners.add(part.owner);
166
- const kept = [];
167
- let lo = part.s0;
168
- let hi = part.s1;
169
- let total = 0;
170
- for (const span of covered) {
171
- if (span[1] < lo || span[0] > hi) {
172
- kept.push(span);
173
- total += span[1] - span[0];
174
- }
175
- else {
176
- lo = Math.min(lo, span[0]);
177
- hi = Math.max(hi, span[1]);
178
- }
179
- }
180
- kept.push([lo, hi]);
181
- covered.splice(0, covered.length, ...kept);
182
- if (total + hi - lo >= 0.5)
183
- return { depth: part.h, across: [...owners] };
184
- }
185
- return { depth: Infinity, across: [] };
186
- }
187
- /**
188
- * How far the strip in front of an edge is free: to the nearest wall, outline or doorway gap before any
189
- * part of it.
190
- * @param edge The edge.
191
- * @param outward Its outward unit normal.
192
- * @param blockers What stands around it.
193
- * @param ownId Destination the edge belongs to; empty for none.
194
- * @param limit How far in front to look.
195
- * @returns The height, or `Infinity` when the strip is free within `limit`.
196
- */
197
- export function stripDepth(edge, outward, blockers, ownId, limit) {
198
- const [nearest] = stripParts(edge, outward, blockers, ownId, limit);
199
- return nearest ? nearest.h : Infinity;
200
- }
@@ -1,84 +0,0 @@
1
- import { type AisleAxis } from './axes.js';
2
- import type { DoorSide } from './doorways.js';
3
- import { type WayIn, type Whisker } from './entries.js';
4
- import { type Frame } from './geom/frames.js';
5
- import type { RouteNetworkDestination } from './graph-types.js';
6
- import type { MaskCtx } from './mask.js';
7
- import type { NetworkBuild } from './network-build.js';
8
- /** A stub planned for a way in: a whisker before it is attached, with the way it grew from. */
9
- export interface Stub extends Whisker {
10
- /** The way in. */
11
- readonly way: WayIn;
12
- /** How far the stub runs from its foot: to its aisle's axis, else to the middle of the aisle. */
13
- reach: number;
14
- /** Half the aisle's width ahead, when the face stands on an aisle: where the stub first ends. */
15
- readonly middle?: number;
16
- }
17
- /** What planning the stubs reads. */
18
- interface StubPlanning extends NetworkBuild {
19
- /** The layer's destinations. */
20
- readonly destinations: readonly RouteNetworkDestination[];
21
- /** Doorway sides by the destination they open into. */
22
- readonly owners: ReadonlyMap<string, readonly DoorSide[]>;
23
- /** The plan's mask: whether a point is on the floor. */
24
- readonly ctx: MaskCtx;
25
- /** The direction a destination is drawn in, when it has one. */
26
- readonly frameOfDest: (d: RouteNetworkDestination) => Frame | undefined;
27
- /** Entrances with openings: reached through their doorways alone, never from a face. */
28
- readonly opened: ReadonlySet<string>;
29
- }
30
- /** The stubs planned, and the planning a retry goes on from once a stub fails to attach. */
31
- export interface StubPlan {
32
- /** Every destination's ways in, by id. */
33
- readonly ways: ReadonlyMap<string, readonly WayIn[]>;
34
- /** The stubs planned, in destination order. */
35
- readonly stubs: Stub[];
36
- /**
37
- * Whether a stub across the aisle may run that far: within the stretch its row's pitch allows,
38
- * or its face's width when it has no row.
39
- */
40
- readonly mayRunAcross: (run: number, way: WayIn) => boolean;
41
- /**
42
- * The stub of the next way to try once one failed to attach: the destination's ranked ways go on
43
- * where the planning stopped, a face only onto a side no whisker serves.
44
- */
45
- readonly retry: (failed: Stub, servedSides: ReadonlyMap<string, ReadonlySet<string>>, whiskers: readonly Whisker[]) => Stub | null;
46
- }
47
- /**
48
- * The side of its destination a way in leaves from, by its normal's main axis.
49
- * @param way The way in.
50
- * @returns The side's key.
51
- */
52
- export declare function sideOf(way: WayIn): string;
53
- /**
54
- * Plans one stub per open side of each destination, ending mid-aisle: doorways first (a stub from
55
- * another face would cross the wall), then the most open face; one face stub per side, and a
56
- * terminal destination gets only its best way in.
57
- * @param planning What the planning reads.
58
- * @returns The stubs and the plan a retry goes on from.
59
- */
60
- export declare function planStubs(planning: StubPlanning): StubPlan;
61
- /**
62
- * The aisle axes the stubs define, per direction and orientation: the stubs that end mid-aisle
63
- * define the aisles' lines first; a stub with open floor ahead runs to such a line ahead of it when
64
- * no corridor fits between the two or its row may run that far, and the rest line up among
65
- * themselves.
66
- * @param plan The stubs planned.
67
- * @param build The layer as the build reads it.
68
- * @param through Destinations people walk through: they frame nothing.
69
- * @param floorSpan The floor's own size: nothing on the layer reaches farther.
70
- * @returns The axes, and each stub's axis.
71
- */
72
- export declare function defineAxes(plan: StubPlan, build: NetworkBuild, through: ReadonlySet<string>, floorSpan: number): {
73
- axes: AisleAxis[];
74
- axisOf: Map<Stub, AisleAxis>;
75
- };
76
- /**
77
- * Runs each stub out to its aisle's axis when its dock stays lawful, over floor only.
78
- * @param axisOf Each stub's axis.
79
- * @param build The layer as the build reads it.
80
- * @param ctx The plan's mask: whether a point is on the floor.
81
- */
82
- export declare function runStubsToAxes(axisOf: ReadonlyMap<Stub, AisleAxis>, build: NetworkBuild, ctx: MaskCtx): void;
83
- export {};
84
- //# sourceMappingURL=stubs.d.ts.map
package/dist/lib/stubs.js DELETED
@@ -1,316 +0,0 @@
1
- // The stubs: a whisker planned for each way into a destination, before it is attached to the
2
- // network. A stub ends in the middle of the aisle ahead, where the stubs from both sides meet on one
3
- // line, when the rows on both sides may run that far; else it is half the free run and its own
4
- // length at most. The tips that end mid-aisle then define the aisles' axes, and each stub runs out
5
- // to its aisle's axis.
6
- import { aisleAxes, tipInFrame } from './axes.js';
7
- import { dockFor, stubLength, waysInto } from './entries.js';
8
- import { toFrame } from './geom/frames.js';
9
- import { POINT_EPS } from './geom/point-key.js';
10
- import { MAX_STRETCH, MIN_PASS } from './network-sizes.js';
11
- import { stripDepth } from './strip.js';
12
- /**
13
- * The side of its destination a way in leaves from, by its normal's main axis.
14
- * @param way The way in.
15
- * @returns The side's key.
16
- */
17
- export function sideOf(way) {
18
- return Math.abs(way.nx) >= Math.abs(way.ny) ? `x${Math.sign(way.nx)}` : `y${Math.sign(way.ny)}`;
19
- }
20
- /**
21
- * Plans one stub per open side of each destination, ending mid-aisle: doorways first (a stub from
22
- * another face would cross the wall), then the most open face; one face stub per side, and a
23
- * terminal destination gets only its best way in.
24
- * @param planning What the planning reads.
25
- * @returns The stubs and the plan a retry goes on from.
26
- */
27
- export function planStubs(planning) {
28
- const { destinations, destinationOf, owners, blockers, ctx, cfg, frames, frameOfDest, opened, note, } = planning;
29
- const ways = new Map(destinations.map((d) => [
30
- d.id,
31
- waysInto(d, owners, blockers, cfg, (x, y) => ctx.freeAt(x, y)).filter((way) => way.kind === 'door' || !opened.has(d.id)),
32
- ]));
33
- const stubs = [];
34
- // Each way in's row: the ways of one direction whose aisle MIDDLES lie on one line — the stands
35
- // along one side of one aisle, however their fronts step (staggered fronts share the middle, the
36
- // next aisle's stands lie a stand and an aisle away). Two at least: a lone face is no row. The
37
- // pitch is the median distance between neighbors along: the walk between two of them.
38
- const pitchOf = new Map();
39
- {
40
- const lines = new Map();
41
- for (const d of destinations) {
42
- const frame = frameOfDest(d) ?? frames[0];
43
- const sides = new Set();
44
- for (const way of ways.get(d.id) ?? []) {
45
- if (way.front === undefined)
46
- continue;
47
- const { horiz, c, along } = tipInFrame(frame, way.mx, way.my, way.nx, way.ny);
48
- const [nu, nv] = toFrame(frame, way.nx, way.ny);
49
- const sign = Math.sign(horiz ? nv : nu);
50
- const key = `${frames.indexOf(frame)}|${horiz}|${sign}`;
51
- // One foot per destination and side: a doorway beside a face is the same neighbor.
52
- if (sides.has(key))
53
- continue;
54
- sides.add(key);
55
- lines.set(key, [...(lines.get(key) ?? []), { way, c: c + (sign * way.front) / 2, along }]);
56
- }
57
- }
58
- for (const feet of lines.values())
59
- for (const row of aisleAxes(feet, 2 * cfg.clear, 0, () => 0)) {
60
- const gaps = row.tips
61
- .slice(1)
62
- .map((foot, i) => foot.along - row.tips[i].along)
63
- .sort((a, b) => a - b);
64
- const pitch = gaps[Math.floor((gaps.length - 1) / 2)];
65
- if (pitch !== undefined)
66
- for (const { way } of row.tips)
67
- pitchOf.set(way, pitch);
68
- }
69
- }
70
- // A stub may run past its own length to the aisle's line ahead while it is still a stub — at
71
- // most MAX_STRETCH more of its own length — and while the detour that adds for two neighbors
72
- // along the row, out and back, stays within MAX_STRETCH of the walk along their fronts: the
73
- // row's pitch. A lone face has no row to share a line with; the side ACROSS the aisle answers
74
- // by its face alone when it has no row — its row is not what is asked.
75
- const fits = (run, pitch) => run <= (1 + MAX_STRETCH) * cfg.stub && 2 * (run - cfg.stub) <= (MAX_STRETCH - 1) * pitch;
76
- const mayRun = (run, way) => {
77
- const pitch = pitchOf.get(way);
78
- return pitch !== undefined && fits(run, pitch);
79
- };
80
- const mayRunAcross = (run, way) => fits(run, pitchOf.get(way) ?? way.width);
81
- /** Each destination's ways in, best first; the attach loop retries down this order. */
82
- const rankedOf = new Map();
83
- /** Ways already planned or found unlawful; a retry never returns to one. */
84
- const tried = new Set();
85
- /**
86
- * Plans the stub of one way in. It ends in the middle of the aisle ahead, where the stubs from
87
- * both sides meet on one line, when the rows on both sides may run that far — one line serves the
88
- * aisle or none does; wider floor ahead is open floor, and an aisle narrower than a person is
89
- * none: its own length at most.
90
- * @param d The destination.
91
- * @param way Its way in.
92
- * @returns The stub, or null when the way in is unlawful (noted).
93
- */
94
- const planStub = (d, way) => {
95
- const tipAt = (run) => [
96
- +(way.mx + way.nx * run).toFixed(2),
97
- +(way.my + way.ny * run).toFixed(2),
98
- ];
99
- const facing = (id) => ways.get(id)?.find((other) => other.nx * way.nx + other.ny * way.ny < 0);
100
- const middle = way.front !== undefined &&
101
- way.front >= 2 * MIN_PASS &&
102
- mayRun(way.front / 2, way) &&
103
- (way.across ?? []).every((id) => {
104
- const other = facing(id);
105
- return other !== undefined && mayRunAcross(way.front / 2, other);
106
- })
107
- ? +(way.front / 2).toFixed(2)
108
- : undefined;
109
- const onAisle = middle !== undefined &&
110
- middle <= way.depth - MIN_PASS &&
111
- ctx.freeAt(...tipAt(middle)) &&
112
- dockFor(d, way, tipAt(middle), blockers) !== null;
113
- const reach = onAisle ? middle : stubLength(way, cfg);
114
- const tip = tipAt(reach);
115
- // Under half a person of room ahead, or no lawful dock: no way in. The noted point is where
116
- // the way really ends: the blocker in front when there is no room, else the tip.
117
- if (way.depth - MIN_PASS <= 0 || (!onAisle && !dockFor(d, way, tip, blockers))) {
118
- note(d.id, 'way-in-blocked', {
119
- at: way.depth - MIN_PASS <= 0 ? tipAt(Math.max(way.depth, 0)) : tip,
120
- });
121
- return null;
122
- }
123
- return {
124
- id: d.id,
125
- kind: way.kind,
126
- mx: way.mx,
127
- my: way.my,
128
- tx: tip[0],
129
- ty: tip[1],
130
- nx: way.nx,
131
- ny: way.ny,
132
- to: way.to,
133
- edge: way.edge,
134
- frame: frameOfDest(d),
135
- way,
136
- reach,
137
- ...(onAisle && { middle }),
138
- };
139
- };
140
- for (const d of destinations) {
141
- const list = ways.get(d.id) ?? [];
142
- if (!list.length) {
143
- note(d.id, 'no-way-in');
144
- continue;
145
- }
146
- // Doorways first (a stub from another face would cross the wall), then the most open face.
147
- const ranked = list
148
- .map((way, order) => ({ way, order }))
149
- .sort((p, q) => Number(q.way.kind !== 'face') - Number(p.way.kind !== 'face') ||
150
- q.way.depth - p.way.depth ||
151
- p.order - q.order)
152
- .map(({ way }) => way);
153
- rankedOf.set(d.id, ranked);
154
- // One face stub per side (the best-ranked claims it): a many-edged shape gets at most four.
155
- const claimed = new Set();
156
- for (const way of ranked) {
157
- // Doorways never compete for a side.
158
- if (way.kind === 'face' && claimed.has(sideOf(way)))
159
- continue;
160
- tried.add(way);
161
- const stub = planStub(d, way);
162
- if (!stub)
163
- continue;
164
- stubs.push(stub);
165
- if (way.kind === 'face')
166
- claimed.add(sideOf(way));
167
- // A terminal destination gets only its best way in.
168
- if (d.terminal)
169
- break;
170
- }
171
- }
172
- /**
173
- * The stub of the next way to try once one failed to attach: the destination's ranked ways go on
174
- * where the planning stopped, a face only onto a side no whisker serves.
175
- * @param failed The stub that could not attach.
176
- * @param servedSides Sides of each destination a whisker already serves.
177
- * @param whiskers The stubs attached so far.
178
- * @returns The next stub, or null when no way in is left.
179
- */
180
- const retry = (failed, servedSides, whiskers) => {
181
- const d = destinationOf.get(failed.id);
182
- if (d.terminal && whiskers.some((w) => w.id === d.id))
183
- return null;
184
- for (const way of rankedOf.get(d.id) ?? []) {
185
- if (tried.has(way))
186
- continue;
187
- if (way.kind === 'face' && servedSides.get(d.id)?.has(sideOf(way)))
188
- continue;
189
- tried.add(way);
190
- const stub = planStub(d, way);
191
- if (stub)
192
- return stub;
193
- }
194
- return null;
195
- };
196
- return { ways, stubs, mayRunAcross, retry };
197
- }
198
- /**
199
- * The aisle axes the stubs define, per direction and orientation: the stubs that end mid-aisle
200
- * define the aisles' lines first; a stub with open floor ahead runs to such a line ahead of it when
201
- * no corridor fits between the two or its row may run that far, and the rest line up among
202
- * themselves.
203
- * @param plan The stubs planned.
204
- * @param build The layer as the build reads it.
205
- * @param through Destinations people walk through: they frame nothing.
206
- * @param floorSpan The floor's own size: nothing on the layer reaches farther.
207
- * @returns The axes, and each stub's axis.
208
- */
209
- export function defineAxes(plan, build, through, floorSpan) {
210
- const { stubs, mayRunAcross } = plan;
211
- const { frames, blockers, cfg } = build;
212
- const axes = [];
213
- const axisOf = new Map();
214
- const groups = new Map();
215
- for (const stub of stubs) {
216
- if (!stub.frame || through.has(stub.id))
217
- continue;
218
- const { horiz, c, along } = tipInFrame(stub.frame, stub.tx, stub.ty, stub.nx, stub.ny);
219
- const key = `${frames.indexOf(stub.frame)}|${horiz}`;
220
- const group = groups.get(key) ?? { frame: stub.frame, horiz, tips: [] };
221
- // Where the tip's line may lie across: a comfortable clearance from its face and from what
222
- // stands across; in an aisle too narrow for that, where the tip is.
223
- const [nu, nv] = toFrame(stub.frame, stub.nx, stub.ny);
224
- const out = Math.sign(horiz ? nv : nu);
225
- const foot = tipInFrame(stub.frame, stub.mx, stub.my, stub.nx, stub.ny).c;
226
- const room = stub.way.depth - cfg.clear;
227
- const may = room >= cfg.clear
228
- ? [
229
- Math.min(foot + out * cfg.clear, foot + out * room),
230
- Math.max(foot + out * cfg.clear, foot + out * room),
231
- ]
232
- : [c, c];
233
- group.tips.push({ stub, c, along, may });
234
- groups.set(key, group);
235
- }
236
- for (const { frame, horiz, tips } of groups.values()) {
237
- // How far a person walks along the row's line from a tip: the line reaches that far, and no
238
- // farther than the floor. The search window grows only as needed — the answer is the same,
239
- // a rotated strip's bounding box stays small.
240
- const [dx, dy] = horiz ? [frame.cos, frame.sin] : [-frame.sin, frame.cos];
241
- const reach = (tip, sign) => {
242
- const strip = [
243
- tip.stub.tx + dy * MIN_PASS,
244
- tip.stub.ty - dx * MIN_PASS,
245
- tip.stub.tx - dy * MIN_PASS,
246
- tip.stub.ty + dx * MIN_PASS,
247
- ];
248
- for (let window = 8 * cfg.stub;; window *= 4) {
249
- const met = stripDepth(strip, [sign * dx, sign * dy], blockers, '', Math.min(window, floorSpan));
250
- if (isFinite(met))
251
- return met;
252
- if (window >= floorSpan)
253
- return floorSpan;
254
- }
255
- };
256
- const settle = (list) => aisleAxes(list, cfg.cs, 2 * cfg.clear, reach).map((axis) => {
257
- const full = { frame, horiz, c: axis.c, lo: axis.lo, hi: axis.hi };
258
- axes.push(full);
259
- for (const { stub } of axis.tips)
260
- axisOf.set(stub, full);
261
- return full;
262
- });
263
- // The aisles' lines come from the stubs that end mid-aisle. A stub with open floor ahead runs
264
- // to such a line ahead of it when no corridor fits between the two, or within the bound its
265
- // own face gives (it has no row of its own to ask); the rest line up among themselves.
266
- const lines = settle(tips.filter((tip) => tip.stub.middle !== undefined));
267
- const loose = [];
268
- for (const tip of tips) {
269
- if (tip.stub.middle !== undefined)
270
- continue;
271
- const [nu, nv] = toFrame(frame, tip.stub.nx, tip.stub.ny);
272
- const sign = Math.sign(horiz ? nv : nu);
273
- let best = null;
274
- for (const axis of lines) {
275
- if (axis.lo > tip.along || axis.hi < tip.along)
276
- continue;
277
- if (axis.c < tip.may[0] || axis.c > tip.may[1])
278
- continue;
279
- const move = (axis.c - tip.c) * sign;
280
- const run = tip.stub.reach + move;
281
- if (run < MIN_PASS || (Math.abs(move) > 2 * cfg.clear && !mayRunAcross(run, tip.stub.way)))
282
- continue;
283
- if (!best || Math.abs(move) < Math.abs(best.move))
284
- best = { axis, move };
285
- }
286
- if (best)
287
- axisOf.set(tip.stub, best.axis);
288
- else
289
- loose.push(tip);
290
- }
291
- settle(loose);
292
- }
293
- return { axes, axisOf };
294
- }
295
- /**
296
- * Runs each stub out to its aisle's axis when its dock stays lawful, over floor only.
297
- * @param axisOf Each stub's axis.
298
- * @param build The layer as the build reads it.
299
- * @param ctx The plan's mask: whether a point is on the floor.
300
- */
301
- export function runStubsToAxes(axisOf, build, ctx) {
302
- const { destinationOf, blockers } = build;
303
- for (const [stub, axis] of axisOf) {
304
- const d = destinationOf.get(stub.id);
305
- const [nu, nv] = toFrame(axis.frame, stub.nx, stub.ny);
306
- const foot = tipInFrame(axis.frame, stub.mx, stub.my, stub.nx, stub.ny).c;
307
- const t = +((axis.c - foot) / (axis.horiz ? nv : nu)).toFixed(2);
308
- if (Math.abs(t - stub.reach) <= POINT_EPS || t < MIN_PASS || t > stub.way.depth - MIN_PASS)
309
- continue;
310
- const tip = [+(stub.mx + stub.nx * t).toFixed(2), +(stub.my + stub.ny * t).toFixed(2)];
311
- if (!ctx.freeAt(tip[0], tip[1]) || !dockFor(d, stub.way, tip, blockers))
312
- continue;
313
- stub.reach = t;
314
- [stub.tx, stub.ty] = tip;
315
- }
316
- }