@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,634 @@
|
|
|
1
|
+
import { planDecimals } from './scale.js';
|
|
2
|
+
/**
|
|
3
|
+
* A stage point in plan units, rounded as the emit rounds.
|
|
4
|
+
* @param at The point, in meters.
|
|
5
|
+
* @param unit Plan units per meter.
|
|
6
|
+
* @returns The point in plan units.
|
|
7
|
+
*/
|
|
8
|
+
function planPoint(at, unit) {
|
|
9
|
+
const decimals = planDecimals(unit);
|
|
10
|
+
return [+(at[0] * unit).toFixed(decimals), +(at[1] * unit).toFixed(decimals)];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One POI's key among the facts: its floor and its place in that floor's source list.
|
|
14
|
+
* @param layer The floor.
|
|
15
|
+
* @param index The source position.
|
|
16
|
+
* @returns The key.
|
|
17
|
+
*/
|
|
18
|
+
export function poiKey(layer, index) {
|
|
19
|
+
return `${layer}\u0000${index}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The kind of source object a validator code is about, read off the code's own prefix.
|
|
23
|
+
* @param code The validator code.
|
|
24
|
+
* @returns The kind of object it names.
|
|
25
|
+
*/
|
|
26
|
+
function refKindOf(code) {
|
|
27
|
+
if (code.startsWith('destination-'))
|
|
28
|
+
return 'destination';
|
|
29
|
+
if (code.startsWith('transition-'))
|
|
30
|
+
return 'transition';
|
|
31
|
+
return 'layer';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The validator's findings as facts of the input stage.
|
|
35
|
+
* @param issues What `validateWayfindingSource` returned.
|
|
36
|
+
* @returns One diagnostic per issue, in the validator's order.
|
|
37
|
+
*/
|
|
38
|
+
export function fromSourceIssues(issues) {
|
|
39
|
+
return issues.map((issue) => {
|
|
40
|
+
const ref = {
|
|
41
|
+
kind: refKindOf(issue.code),
|
|
42
|
+
...(issue.layer !== undefined && { layer: issue.layer }),
|
|
43
|
+
...(issue.index !== undefined && { index: issue.index }),
|
|
44
|
+
...(issue.id !== undefined && { id: issue.id }),
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
code: issue.code,
|
|
48
|
+
severity: issue.severity,
|
|
49
|
+
stage: 'input',
|
|
50
|
+
message: issue.message,
|
|
51
|
+
...(issue.layer !== undefined && { layer: issue.layer }),
|
|
52
|
+
...(issue.layer !== undefined || issue.id !== undefined || issue.index !== undefined
|
|
53
|
+
? { ref }
|
|
54
|
+
: {}),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The scale the plan was read at, when the plan did not state one.
|
|
60
|
+
* @param unitsPerMeter The source's `unitsPerMeter`.
|
|
61
|
+
* @returns A fact when the plan was read as meters by default.
|
|
62
|
+
*/
|
|
63
|
+
export function fromScale(unitsPerMeter) {
|
|
64
|
+
if (unitsPerMeter !== undefined)
|
|
65
|
+
return [];
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
code: 'units-assumed-meters',
|
|
69
|
+
severity: 'info',
|
|
70
|
+
stage: 'scale',
|
|
71
|
+
message: 'the plan states no units per meter; it is read as meters',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
/** What each way of reading a footprint's shape says, one sentence per {@link FootprintShape}. */
|
|
76
|
+
const SHAPE_MESSAGES = {
|
|
77
|
+
traced: 'the footprint outline is traced from its drawn pieces',
|
|
78
|
+
hull: 'the drawn pieces fill their convex hull, which stands in for the outline',
|
|
79
|
+
rectilinear: 'the drawn pieces join into a short axis-aligned outline',
|
|
80
|
+
bounds: 'the drawn pieces trace no simple outline, so their bounding box stands in',
|
|
81
|
+
'bounds-oversized': 'the mesh is too fine to trace, so its bounding box stands in for the shape',
|
|
82
|
+
dock: 'the drawn pieces enclose no area, so the dock stands in for the footprint',
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What normalization did to one layer's drawing before any geometry was built off it.
|
|
86
|
+
* @param source The layer as given.
|
|
87
|
+
* @param facts What the layer's normalization returned.
|
|
88
|
+
* @param ids The id each source position compiled under.
|
|
89
|
+
* @param unit Plan units per meter, for the facts' geometry.
|
|
90
|
+
* @returns Wall facts in entry order, then POI facts in source order.
|
|
91
|
+
*/
|
|
92
|
+
export function fromNormalize(source, facts, ids, unit = 1) {
|
|
93
|
+
const layer = source.layer;
|
|
94
|
+
const out = [];
|
|
95
|
+
const openings = new Map(facts.openings.map((o) => [o.id, o]));
|
|
96
|
+
const unopened = new Set(facts.entrancesUnopened);
|
|
97
|
+
for (const { entry, runs, oddTail } of facts.wallEntries) {
|
|
98
|
+
if (runs === 0)
|
|
99
|
+
out.push({
|
|
100
|
+
code: 'wall-entry-empty',
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
stage: 'normalize',
|
|
103
|
+
message: 'the wall entry cleans to no wall piece and is dropped',
|
|
104
|
+
layer,
|
|
105
|
+
ref: { kind: 'wall', layer, index: entry },
|
|
106
|
+
});
|
|
107
|
+
if (runs > 1)
|
|
108
|
+
out.push({
|
|
109
|
+
code: 'wall-entry-split',
|
|
110
|
+
severity: 'info',
|
|
111
|
+
stage: 'normalize',
|
|
112
|
+
message: `the wall entry describes ${runs} separate runs, not one polyline`,
|
|
113
|
+
layer,
|
|
114
|
+
ref: { kind: 'wall', layer, index: entry },
|
|
115
|
+
facts: { runs },
|
|
116
|
+
});
|
|
117
|
+
if (oddTail)
|
|
118
|
+
out.push({
|
|
119
|
+
code: 'wall-entry-odd-length',
|
|
120
|
+
severity: 'warning',
|
|
121
|
+
stage: 'normalize',
|
|
122
|
+
message: 'the wall entry holds an odd count of numbers; the last one names no point and is dropped',
|
|
123
|
+
layer,
|
|
124
|
+
ref: { kind: 'wall', layer, index: entry },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const shapes = new Map(facts.shapes.map((shape) => [shape.id, shape]));
|
|
128
|
+
const clipped = new Set(facts.clipped);
|
|
129
|
+
const crossable = new Map(facts.crossable.map((cross) => [cross.id, cross]));
|
|
130
|
+
source.destinations.forEach((destination, index) => {
|
|
131
|
+
const id = ids[index];
|
|
132
|
+
const ref = { kind: 'destination', layer, index, id: destination.id };
|
|
133
|
+
const shape = shapes.get(id);
|
|
134
|
+
const cross = crossable.get(id);
|
|
135
|
+
if (shape)
|
|
136
|
+
out.push({
|
|
137
|
+
code: 'footprint-shape',
|
|
138
|
+
severity: shape.how === 'bounds-oversized' ? 'warning' : 'info',
|
|
139
|
+
stage: 'normalize',
|
|
140
|
+
message: SHAPE_MESSAGES[shape.how],
|
|
141
|
+
layer,
|
|
142
|
+
ref,
|
|
143
|
+
facts: { how: shape.how, parts: shape.parts },
|
|
144
|
+
});
|
|
145
|
+
if (clipped.has(id))
|
|
146
|
+
out.push({
|
|
147
|
+
code: 'footprint-clipped',
|
|
148
|
+
severity: 'info',
|
|
149
|
+
stage: 'normalize',
|
|
150
|
+
message: 'the footprint ran over its walls and is cut back to the largest piece they leave',
|
|
151
|
+
layer,
|
|
152
|
+
ref,
|
|
153
|
+
});
|
|
154
|
+
const opening = openings.get(id);
|
|
155
|
+
if (opening)
|
|
156
|
+
out.push({
|
|
157
|
+
code: 'entrance-openings',
|
|
158
|
+
severity: 'info',
|
|
159
|
+
stage: 'normalize',
|
|
160
|
+
message: opening.by === 'crossing'
|
|
161
|
+
? `routes reach the entrance through the wall inside its bounds, at ${opening.at.length} place(s)`
|
|
162
|
+
: opening.by === 'doorway'
|
|
163
|
+
? `the entrance stands ${opening.distance ?? 0} m outside the building; routes reach it through the doorway the walls leave`
|
|
164
|
+
: `the entrance stands ${opening.distance ?? 0} m outside the building; routes reach it through the nearest wall with floor behind it`,
|
|
165
|
+
layer,
|
|
166
|
+
ref,
|
|
167
|
+
geometry: opening.at.map((at) => ({ type: 'point', layer, at: planPoint(at, unit) })),
|
|
168
|
+
facts: {
|
|
169
|
+
by: opening.by,
|
|
170
|
+
openings: opening.at.length,
|
|
171
|
+
...(opening.distance !== undefined && { distance: opening.distance }),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (unopened.has(id))
|
|
175
|
+
out.push({
|
|
176
|
+
code: 'entrance-no-wall',
|
|
177
|
+
severity: 'info',
|
|
178
|
+
stage: 'normalize',
|
|
179
|
+
message: 'the entrance stands across no wall and is reached like any POI',
|
|
180
|
+
layer,
|
|
181
|
+
ref,
|
|
182
|
+
});
|
|
183
|
+
if (cross)
|
|
184
|
+
out.push({
|
|
185
|
+
code: 'crossable-inferred',
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
stage: 'normalize',
|
|
188
|
+
message: cross.by === 'holds-another'
|
|
189
|
+
? 'the footprint holds other destinations, so it is walked in, not around'
|
|
190
|
+
: 'the footprint stands before the only way into another destination, so routes may cross it',
|
|
191
|
+
layer,
|
|
192
|
+
ref,
|
|
193
|
+
facts: { by: cross.by, ...(cross.victims && { victims: cross.victims }) },
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A floor too large to build routes over: it is left unbuilt rather than run the compile out of memory.
|
|
200
|
+
* @param layer The layer id.
|
|
201
|
+
* @param width The floor's extent across, in meters.
|
|
202
|
+
* @param height The floor's extent down, in meters.
|
|
203
|
+
* @param destinations How many POI the source puts on this floor.
|
|
204
|
+
* @returns One fact about the floor.
|
|
205
|
+
*/
|
|
206
|
+
export function fromOversize(layer, width, height, destinations) {
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
code: 'floor-too-large',
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
stage: 'walkable',
|
|
212
|
+
message: 'the floor spans more than routes are built over, so no floor is built',
|
|
213
|
+
layer,
|
|
214
|
+
ref: { kind: 'layer', layer },
|
|
215
|
+
facts: { widthM: +width.toFixed(1), heightM: +height.toFixed(1), destinations },
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* What the walkable stage made of one floor, decided by the stage's counts rather than the rounded area.
|
|
221
|
+
* @param layer The layer id.
|
|
222
|
+
* @param stats The walkable stage's counts.
|
|
223
|
+
* @param destinations How many POI the source puts on this floor.
|
|
224
|
+
* @returns One fact about the floor.
|
|
225
|
+
*/
|
|
226
|
+
export function fromWalkable(layer, stats, destinations) {
|
|
227
|
+
const ref = { kind: 'layer', layer };
|
|
228
|
+
const counts = { walls: stats.walls, outerContours: stats.outerContours, rings: stats.rings };
|
|
229
|
+
if (stats.walls === 0) {
|
|
230
|
+
// A floor with nothing drawn on it at all is an empty draft, not a failure.
|
|
231
|
+
if (destinations === 0)
|
|
232
|
+
return [];
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
code: 'layer-no-walls',
|
|
236
|
+
severity: 'warning',
|
|
237
|
+
stage: 'walkable',
|
|
238
|
+
message: 'the layer has POI but no walls, so no floor is built',
|
|
239
|
+
layer,
|
|
240
|
+
ref,
|
|
241
|
+
facts: { ...counts, destinations },
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
if (stats.outerContours === 0)
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
code: 'layer-no-outer-contour',
|
|
249
|
+
severity: 'warning',
|
|
250
|
+
stage: 'walkable',
|
|
251
|
+
message: 'no wall run closes into an outer contour, so no floor is built',
|
|
252
|
+
layer,
|
|
253
|
+
ref,
|
|
254
|
+
facts: { ...counts, destinations },
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
if (stats.rings === 0)
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
code: 'floor-carved-away',
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
stage: 'walkable',
|
|
263
|
+
message: 'the outer contour closes, but walls and footprints cover all of it',
|
|
264
|
+
layer,
|
|
265
|
+
ref,
|
|
266
|
+
facts: { ...counts, destinations },
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
code: 'floor-built',
|
|
272
|
+
severity: 'info',
|
|
273
|
+
stage: 'walkable',
|
|
274
|
+
message: 'the floor is built',
|
|
275
|
+
layer,
|
|
276
|
+
ref,
|
|
277
|
+
facts: { ...counts, areaM2: stats.areaExact ?? stats.area },
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* What the route-network stage reported for one floor: POI left without a dock, and the part count
|
|
283
|
+
* before the emit.
|
|
284
|
+
* @param source The layer as given.
|
|
285
|
+
* @param stats The network stage's counts.
|
|
286
|
+
* @param unit Plan units per meter, for the facts' geometry.
|
|
287
|
+
* @param ids The id each source position compiled under; the source ids by default.
|
|
288
|
+
* @returns The facts, POI in source order.
|
|
289
|
+
*/
|
|
290
|
+
export function fromNetwork(source, stats, unit = 1, ids = source.destinations.map(({ id }) => id)) {
|
|
291
|
+
const layer = source.layer;
|
|
292
|
+
const unreached = new Map(stats.unreached.map((u) => [u.id, u]));
|
|
293
|
+
const out = [];
|
|
294
|
+
// Matched by the id each POI compiled under. Transition pads are not in the source: their loss is
|
|
295
|
+
// reported as the shaft's orphan.
|
|
296
|
+
source.destinations.forEach((destination, index) => {
|
|
297
|
+
const fact = unreached.get(ids[index]);
|
|
298
|
+
if (fact === undefined)
|
|
299
|
+
return;
|
|
300
|
+
// The stub tip, in plan units like every emitted point.
|
|
301
|
+
const at = fact.at && planPoint(fact.at, unit);
|
|
302
|
+
out.push({
|
|
303
|
+
code: 'destination-unreached',
|
|
304
|
+
severity: 'warning',
|
|
305
|
+
stage: 'network',
|
|
306
|
+
message: `the POI got no dock a route can end at (${fact.reason})`,
|
|
307
|
+
layer,
|
|
308
|
+
ref: { kind: 'destination', layer, index, id: destination.id },
|
|
309
|
+
...(at && { geometry: [{ type: 'point', layer, at }] }),
|
|
310
|
+
facts: {
|
|
311
|
+
reason: fact.reason,
|
|
312
|
+
...(fact.outside !== undefined && { outside: fact.outside }),
|
|
313
|
+
...(fact.nearestWall !== undefined && { nearestWall: fact.nearestWall }),
|
|
314
|
+
...(fact.floorBehind !== undefined && { floorBehind: fact.floorBehind }),
|
|
315
|
+
...(fact.by !== undefined && { by: fact.by }),
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
const layerRef = { kind: 'layer', layer };
|
|
320
|
+
if (stats.doorways !== undefined)
|
|
321
|
+
out.push({
|
|
322
|
+
code: 'doorways-found',
|
|
323
|
+
severity: 'info',
|
|
324
|
+
stage: 'network',
|
|
325
|
+
message: `the walls leave ${stats.doorways} doorway(s) on the layer`,
|
|
326
|
+
layer,
|
|
327
|
+
ref: layerRef,
|
|
328
|
+
facts: { count: stats.doorways },
|
|
329
|
+
});
|
|
330
|
+
if (stats.shortcuts !== undefined && stats.shortcuts > 0)
|
|
331
|
+
out.push({
|
|
332
|
+
code: 'network-shortcuts',
|
|
333
|
+
severity: 'info',
|
|
334
|
+
stage: 'network',
|
|
335
|
+
message: `${stats.shortcuts} shortcut link(s) are cut in where the network sent a route the long way round`,
|
|
336
|
+
layer,
|
|
337
|
+
ref: layerRef,
|
|
338
|
+
facts: { count: stats.shortcuts },
|
|
339
|
+
});
|
|
340
|
+
if (stats.untiedPieces !== undefined && stats.untiedPieces > 0)
|
|
341
|
+
out.push({
|
|
342
|
+
code: 'network-pieces-untied',
|
|
343
|
+
severity: 'warning',
|
|
344
|
+
stage: 'network',
|
|
345
|
+
message: `the layer's network is left in ${stats.untiedPieces + 1} pieces: the growth found no lawful link and no floor path between them`,
|
|
346
|
+
layer,
|
|
347
|
+
ref: layerRef,
|
|
348
|
+
facts: { pieces: stats.untiedPieces + 1 },
|
|
349
|
+
});
|
|
350
|
+
if (stats.unservedFloorPieces !== undefined && stats.unservedFloorPieces > 0)
|
|
351
|
+
out.push({
|
|
352
|
+
code: 'floor-pieces-unserved',
|
|
353
|
+
severity: 'info',
|
|
354
|
+
stage: 'network',
|
|
355
|
+
message: `${stats.unservedFloorPieces} walkable floor piece(s) hold no destination and no doorway, so no network is built over them`,
|
|
356
|
+
layer,
|
|
357
|
+
ref: layerRef,
|
|
358
|
+
facts: { count: stats.unservedFloorPieces },
|
|
359
|
+
});
|
|
360
|
+
if (stats.components !== undefined)
|
|
361
|
+
out.push({
|
|
362
|
+
code: 'network-parts-before-emit',
|
|
363
|
+
severity: 'info',
|
|
364
|
+
stage: 'network',
|
|
365
|
+
message: `the layer's network has ${stats.components} part(s) before the emit, shafts not counted`,
|
|
366
|
+
layer,
|
|
367
|
+
ref: { kind: 'layer', layer },
|
|
368
|
+
facts: { count: stats.components },
|
|
369
|
+
});
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* What collapsed on the centimeter grid as one layer's network was emitted as v1 edges.
|
|
374
|
+
* @param source The layer as given.
|
|
375
|
+
* @param collapsed What `emitLayerV1` returned.
|
|
376
|
+
* @param collapsed.collapsedSegments Path segments skipped: both ends snap to one node.
|
|
377
|
+
* @param collapsed.collapsedDocks Docks whose line is lost while their `lineEnd` still stands.
|
|
378
|
+
* @param ids The id each source position compiled under; the source ids by default.
|
|
379
|
+
* @returns One fact for the segments, then one per collapsed dock, in dock order.
|
|
380
|
+
*/
|
|
381
|
+
export function fromEmit(source, collapsed, ids = source.destinations.map(({ id }) => id)) {
|
|
382
|
+
const layer = source.layer;
|
|
383
|
+
const out = [];
|
|
384
|
+
if (collapsed.collapsedSegments > 0)
|
|
385
|
+
out.push({
|
|
386
|
+
code: 'path-segment-collapsed',
|
|
387
|
+
severity: 'info',
|
|
388
|
+
stage: 'emit',
|
|
389
|
+
message: `${collapsed.collapsedSegments} path segment(s) collapse to a point on the centimeter grid and emit no line`,
|
|
390
|
+
layer,
|
|
391
|
+
ref: { kind: 'layer', layer },
|
|
392
|
+
facts: { count: collapsed.collapsedSegments },
|
|
393
|
+
});
|
|
394
|
+
// Matched by the id each POI compiled under, like `fromNetwork`; a transition pad has no source
|
|
395
|
+
// position and keeps the compile id alone.
|
|
396
|
+
for (const id of collapsed.collapsedDocks) {
|
|
397
|
+
const index = ids.indexOf(id);
|
|
398
|
+
const destination = source.destinations[index];
|
|
399
|
+
out.push({
|
|
400
|
+
code: 'dock-collapsed',
|
|
401
|
+
severity: 'warning',
|
|
402
|
+
stage: 'emit',
|
|
403
|
+
message: 'the dock collapses to a point on the centimeter grid: its line end is emitted with no line of its own',
|
|
404
|
+
layer,
|
|
405
|
+
ref: {
|
|
406
|
+
kind: 'destination',
|
|
407
|
+
layer,
|
|
408
|
+
...(index >= 0 && { index }),
|
|
409
|
+
id: destination?.id ?? id,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The stops of a shaft that never reached the graph, and the shafts left joining nothing.
|
|
417
|
+
* @param orphans The orphans, in plan units.
|
|
418
|
+
* @param singleFloor Shafts whose attached stops all stand on one floor.
|
|
419
|
+
* @param unit Plan units per meter, for the shafts' geometry.
|
|
420
|
+
* @returns One fact per stop, then one per such shaft.
|
|
421
|
+
*/
|
|
422
|
+
export function fromTransitions(orphans, singleFloor = [], unit = 1) {
|
|
423
|
+
const stops = orphans.map((orphan) => {
|
|
424
|
+
const known = orphan.reason === 'no-dock';
|
|
425
|
+
return {
|
|
426
|
+
code: known ? 'transition-stop-no-dock' : 'transition-stop-unknown-layer',
|
|
427
|
+
severity: 'warning',
|
|
428
|
+
stage: 'transitions',
|
|
429
|
+
message: known
|
|
430
|
+
? 'a transition stop got no dock on its layer, so the shaft does not reach that floor'
|
|
431
|
+
: 'a transition stop names a layer the plan does not have',
|
|
432
|
+
layer: orphan.layer,
|
|
433
|
+
ref: {
|
|
434
|
+
kind: 'transition-stop',
|
|
435
|
+
layer: orphan.layer,
|
|
436
|
+
...(orphan.transitionId !== undefined && { id: orphan.transitionId }),
|
|
437
|
+
},
|
|
438
|
+
...(known && {
|
|
439
|
+
geometry: [
|
|
440
|
+
{ type: 'point', layer: orphan.layer, at: [orphan.x, orphan.y] },
|
|
441
|
+
],
|
|
442
|
+
}),
|
|
443
|
+
facts: {
|
|
444
|
+
reason: orphan.reason,
|
|
445
|
+
...(orphan.pointId !== undefined && { pointId: orphan.pointId }),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
const shafts = singleFloor.map(({ transition, layer, at }) => ({
|
|
450
|
+
code: 'transition-single-floor',
|
|
451
|
+
severity: 'warning',
|
|
452
|
+
stage: 'transitions',
|
|
453
|
+
message: 'the shaft docked on one floor only, so it connects nothing',
|
|
454
|
+
layer,
|
|
455
|
+
ref: {
|
|
456
|
+
kind: 'transition',
|
|
457
|
+
layer,
|
|
458
|
+
...(transition.id !== undefined && { id: transition.id }),
|
|
459
|
+
},
|
|
460
|
+
geometry: at.map((tip) => ({ type: 'point', layer, at: planPoint(tip, unit) })),
|
|
461
|
+
facts: { kind: transition.kind },
|
|
462
|
+
}));
|
|
463
|
+
return [...stops, ...shafts];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Where each transition stop rides in: on the dock of the destination it stands in, or on a pad of its
|
|
467
|
+
* own; and a stop standing in several destinations, which rides in on the smallest.
|
|
468
|
+
* @param plan The stops as `planTransitions` resolved them, in meters.
|
|
469
|
+
* @param unit Plan units per meter, for the geometry.
|
|
470
|
+
* @returns One fact per stop, and a warning per ambiguous one.
|
|
471
|
+
*/
|
|
472
|
+
export function fromStops(plan, unit) {
|
|
473
|
+
// A repeated name compiles under a suffixed key; the plan knows it by the name.
|
|
474
|
+
const named = (id) => id.split('\u0000')[0];
|
|
475
|
+
return plan.stops.flatMap(({ transition, point, destinationId, pad, alsoIn }) => {
|
|
476
|
+
const where = {
|
|
477
|
+
layer: point.layer,
|
|
478
|
+
ref: {
|
|
479
|
+
kind: 'transition-stop',
|
|
480
|
+
layer: point.layer,
|
|
481
|
+
...(transition.id !== undefined && { id: transition.id }),
|
|
482
|
+
},
|
|
483
|
+
geometry: [
|
|
484
|
+
{
|
|
485
|
+
type: 'point',
|
|
486
|
+
layer: point.layer,
|
|
487
|
+
at: [point.x * unit, point.y * unit],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
const pointId = point.id !== undefined ? { pointId: point.id } : {};
|
|
492
|
+
const bound = {
|
|
493
|
+
code: 'transition-stop-bound',
|
|
494
|
+
severity: 'info',
|
|
495
|
+
stage: 'transitions',
|
|
496
|
+
message: pad
|
|
497
|
+
? 'the transition stop stands in the open and gets a pad of its own'
|
|
498
|
+
: `the transition stop rides in on the dock of "${named(destinationId)}"`,
|
|
499
|
+
...where,
|
|
500
|
+
facts: { ...pointId, pad, ...(!pad && { destination: named(destinationId) }) },
|
|
501
|
+
};
|
|
502
|
+
if (!alsoIn.length)
|
|
503
|
+
return [bound];
|
|
504
|
+
return [
|
|
505
|
+
bound,
|
|
506
|
+
{
|
|
507
|
+
code: 'transition-stop-ambiguous',
|
|
508
|
+
severity: 'warning',
|
|
509
|
+
stage: 'transitions',
|
|
510
|
+
message: `the transition stop stands in ${alsoIn.length + 1} destinations and rides in on the smallest`,
|
|
511
|
+
...where,
|
|
512
|
+
facts: { ...pointId, destinations: [destinationId, ...alsoIn].map(named) },
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* What the emitted graph connects, as facts: parts, POI off the main part, POI bound to nothing, and the
|
|
519
|
+
* ends the binding cannot settle.
|
|
520
|
+
* @param reachability What `readReachability` read off the graph.
|
|
521
|
+
* @param facts The input and stage facts so far, to check the binding against.
|
|
522
|
+
* @returns The facts, in part order and then POI order.
|
|
523
|
+
*/
|
|
524
|
+
export function fromReachability(reachability, facts) {
|
|
525
|
+
const out = [];
|
|
526
|
+
const unreached = new Set(facts
|
|
527
|
+
.filter(({ code }) => code === 'destination-unreached')
|
|
528
|
+
.map(({ ref }) => poiKey(ref?.layer ?? '', ref?.index ?? -1)));
|
|
529
|
+
// Floors the network stage never ran on report no POI either way — over size, or over a
|
|
530
|
+
// layer-scoped input error: the same rule the compile skips them by.
|
|
531
|
+
const skipped = new Set(facts
|
|
532
|
+
.filter(({ code, stage, severity, layer }) => code === 'floor-too-large' ||
|
|
533
|
+
(stage === 'input' && severity === 'error' && layer !== undefined))
|
|
534
|
+
.map(({ layer }) => layer));
|
|
535
|
+
// POI the compile left out over a coordinate that is not a number: the input fact says it all.
|
|
536
|
+
const excluded = new Set(facts
|
|
537
|
+
.filter(({ code }) => code === 'destination-coordinate-not-finite')
|
|
538
|
+
.map(({ ref }) => poiKey(ref?.layer ?? '', ref?.index ?? -1)));
|
|
539
|
+
if (reachability.parts.length > 1)
|
|
540
|
+
out.push({
|
|
541
|
+
code: 'network-split',
|
|
542
|
+
severity: 'warning',
|
|
543
|
+
stage: 'reachability',
|
|
544
|
+
message: `the emitted graph falls into ${reachability.parts.length} parts`,
|
|
545
|
+
facts: {
|
|
546
|
+
nodes: reachability.parts.map((part) => part.nodes),
|
|
547
|
+
poi: reachability.parts.map((part) => part.poi.length),
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
for (const { ref, part, split } of reachability.poi) {
|
|
551
|
+
const where = { layer: ref.layer, ref: { kind: 'destination', ...ref } };
|
|
552
|
+
const reported = unreached.has(poiKey(ref.layer, ref.index));
|
|
553
|
+
if (part > 0)
|
|
554
|
+
out.push({
|
|
555
|
+
code: 'destination-on-detached-part',
|
|
556
|
+
severity: 'warning',
|
|
557
|
+
stage: 'reachability',
|
|
558
|
+
message: 'the POI is bound to a part of the graph the main part does not reach',
|
|
559
|
+
...where,
|
|
560
|
+
facts: { part },
|
|
561
|
+
});
|
|
562
|
+
if (part < 0)
|
|
563
|
+
out.push({
|
|
564
|
+
code: 'destination-unbound',
|
|
565
|
+
severity: 'warning',
|
|
566
|
+
stage: 'reachability',
|
|
567
|
+
message: 'no line end lies inside the POI',
|
|
568
|
+
...where,
|
|
569
|
+
});
|
|
570
|
+
if (split)
|
|
571
|
+
out.push({
|
|
572
|
+
code: 'destination-split-across-parts',
|
|
573
|
+
severity: 'warning',
|
|
574
|
+
stage: 'reachability',
|
|
575
|
+
message: 'the POI has line ends on more than one part',
|
|
576
|
+
...where,
|
|
577
|
+
});
|
|
578
|
+
// The stage decides by name and the binding by ring: when they disagree, one of them is wrong.
|
|
579
|
+
if (reported !== part < 0 &&
|
|
580
|
+
!skipped.has(ref.layer) &&
|
|
581
|
+
!excluded.has(poiKey(ref.layer, ref.index)))
|
|
582
|
+
out.push({
|
|
583
|
+
code: 'binding-mismatch',
|
|
584
|
+
severity: 'warning',
|
|
585
|
+
stage: 'reachability',
|
|
586
|
+
message: reported
|
|
587
|
+
? 'the network stage reports the POI unreached, yet a line end lies inside it'
|
|
588
|
+
: 'the network stage reports the POI reached, yet no line end lies inside it',
|
|
589
|
+
...where,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (reachability.unownedEnds.length)
|
|
593
|
+
out.push({
|
|
594
|
+
code: 'line-end-unowned',
|
|
595
|
+
severity: 'warning',
|
|
596
|
+
stage: 'reachability',
|
|
597
|
+
message: `${reachability.unownedEnds.length} line end(s) lie inside no POI and start no shaft`,
|
|
598
|
+
facts: { ends: reachability.unownedEnds },
|
|
599
|
+
});
|
|
600
|
+
if (reachability.sharedEnds.length)
|
|
601
|
+
out.push({
|
|
602
|
+
code: 'line-end-shared',
|
|
603
|
+
severity: 'warning',
|
|
604
|
+
stage: 'reachability',
|
|
605
|
+
message: `${reachability.sharedEnds.length} line end(s) lie inside more than one POI`,
|
|
606
|
+
facts: { ends: reachability.sharedEnds.map(({ end }) => end) },
|
|
607
|
+
});
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
/** What each breached invariant says, one sentence per {@link InvariantFinding}'s code. */
|
|
611
|
+
const INVARIANT_MESSAGES = {
|
|
612
|
+
'line-crosses-wall': 'the line crosses a wall of its layer',
|
|
613
|
+
'line-crosses-poi': 'the line passes through a destination it is not serving',
|
|
614
|
+
'dock-outside-ring': 'the dock ends inside no ring a destination is bound by',
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* The emitted graph's breached invariants as facts of the graph stage; debug mode only.
|
|
618
|
+
* @param findings What `checkGraphInvariants` found.
|
|
619
|
+
* @returns One diagnostic per finding, in the checker's order.
|
|
620
|
+
*/
|
|
621
|
+
export function fromGraphInvariants(findings) {
|
|
622
|
+
return findings.map(({ code, layer, lines, at }) => ({
|
|
623
|
+
code,
|
|
624
|
+
severity: 'warning',
|
|
625
|
+
stage: 'graph',
|
|
626
|
+
message: INVARIANT_MESSAGES[code],
|
|
627
|
+
layer,
|
|
628
|
+
ref: { kind: 'layer', layer },
|
|
629
|
+
geometry: [
|
|
630
|
+
{ type: 'lines', layer, lines },
|
|
631
|
+
...(at ? [{ type: 'point', layer, at }] : []),
|
|
632
|
+
],
|
|
633
|
+
}));
|
|
634
|
+
}
|