@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,346 @@
|
|
|
1
|
+
// Laying one raster path into the network: its runs placed on the lines their places define
|
|
2
|
+
// (`runs.ts`), and each end tied to what it joins at a node — the tip standing there, or the
|
|
3
|
+
// network line claiming the cell.
|
|
4
|
+
import { legsBetween, linesMeet, linkOnto, placeIn, toFrame } from './geom/frames.js';
|
|
5
|
+
import { POINT_EPS, snapCm } from './geom/point-key.js';
|
|
6
|
+
import { segmentParam } from './geom/primitives.js';
|
|
7
|
+
import { NODE_SNAP } from './graph-types.js';
|
|
8
|
+
import { onRun, placeRun, pullOf, runCoords, runDirection, runLine, runsOf, } from './runs.js';
|
|
9
|
+
/**
|
|
10
|
+
* Builds the function that lays one path.
|
|
11
|
+
* @param shared What it shares with the growth.
|
|
12
|
+
* @returns The function.
|
|
13
|
+
*/
|
|
14
|
+
export function createPathPlacer(shared) {
|
|
15
|
+
const { store, router, cfg, place, tipAt, onNetwork, add, claim } = shared;
|
|
16
|
+
const { frames } = place;
|
|
17
|
+
const pull = pullOf(cfg);
|
|
18
|
+
// The rule a path's pieces and ties keep; a caller may tighten it for one path.
|
|
19
|
+
let mayLay = place.mayLay;
|
|
20
|
+
/**
|
|
21
|
+
* Ties a run's end at a cell to the tip standing there: the run passes through the tip, or ends
|
|
22
|
+
* at its cell with an elbow to the tip.
|
|
23
|
+
* @param run The run; its line may move to the tip.
|
|
24
|
+
* @param tip The tip.
|
|
25
|
+
* @param end The run's end along: its first or last cell's center.
|
|
26
|
+
* @param hold Whether the run's line is settled and may not move.
|
|
27
|
+
* @returns The tie, or null when no free way joins them.
|
|
28
|
+
*/
|
|
29
|
+
const tieTip = (run, tip, end, hold = false) => {
|
|
30
|
+
const [, c] = runCoords(run, tip.at[0], tip.at[1]);
|
|
31
|
+
if (Math.abs(c - run.c) <= NODE_SNAP || (!hold && mayLay(runLine(run, c)))) {
|
|
32
|
+
run.c = c;
|
|
33
|
+
return { at: tip.at, legs: [] };
|
|
34
|
+
}
|
|
35
|
+
// The run cannot pass the tip: it ends at its cell, and an elbow brings the tip to it.
|
|
36
|
+
const at = onRun(run, end, run.c);
|
|
37
|
+
const legs = legsBetween(run.frame, tip.at, at, mayLay);
|
|
38
|
+
return legs && { at, legs };
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The point of a segment nearest to a point, on the centimeter grid.
|
|
42
|
+
* @param seg The segment.
|
|
43
|
+
* @param p The point.
|
|
44
|
+
* @returns The nearest point.
|
|
45
|
+
*/
|
|
46
|
+
const nearestOn = (seg, p) => {
|
|
47
|
+
const t = segmentParam(p[0], p[1], seg[0], seg[1], seg[2], seg[3]);
|
|
48
|
+
return [snapCm(seg[0] + t * (seg[2] - seg[0])), snapCm(seg[1] + t * (seg[3] - seg[1]))];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Ties a run's end at a cell to the network there: square across the line the run's own line
|
|
52
|
+
* meets within the cell, along a line running the same way (the run ends where it meets it, or
|
|
53
|
+
* at its end), else by an elbow to the nearest line a free way reaches.
|
|
54
|
+
* @param run The run; its line may move onto a line running the same way.
|
|
55
|
+
* @param end The run's end along: its first or last cell's center.
|
|
56
|
+
* @param hold Whether the run's line is settled and may not move.
|
|
57
|
+
* @returns The tie, or null when nothing near can be reached.
|
|
58
|
+
*/
|
|
59
|
+
const tieNetwork = (run, end, hold = false) => {
|
|
60
|
+
const at = onRun(run, end, run.c);
|
|
61
|
+
const dir = runDirection(run);
|
|
62
|
+
const near = [];
|
|
63
|
+
store.forEachNear(at[0] - cfg.pitch, at[1] - cfg.pitch, at[0] + cfg.pitch, at[1] + cfg.pitch, (_, x0, y0, x1, y1) => near.push([x0, y0, x1, y1]));
|
|
64
|
+
let best = null;
|
|
65
|
+
const offer = (tie, c) => {
|
|
66
|
+
const away = Math.hypot(tie.at[0] - at[0], tie.at[1] - at[1]);
|
|
67
|
+
if (away <= cfg.pitch + NODE_SNAP && (!best || away < best.away))
|
|
68
|
+
best = { tie, away, c };
|
|
69
|
+
};
|
|
70
|
+
for (const held of near) {
|
|
71
|
+
const [x0, y0, x1, y1] = held;
|
|
72
|
+
const len = Math.hypot(x1 - x0, y1 - y0);
|
|
73
|
+
if (len < NODE_SNAP)
|
|
74
|
+
continue;
|
|
75
|
+
// Which way the segment runs, as the raster can tell over its length: square across the
|
|
76
|
+
// run, along it, or neither.
|
|
77
|
+
const [su, sv] = toFrame(run.frame, (x1 - x0) / len, (y1 - y0) / len);
|
|
78
|
+
const tell = cfg.cs / len;
|
|
79
|
+
const across = (run.horiz ? Math.abs(su) : Math.abs(sv)) <= tell;
|
|
80
|
+
const alongRun = (run.horiz ? Math.abs(sv) : Math.abs(su)) <= tell;
|
|
81
|
+
if (across) {
|
|
82
|
+
// Square across: where the run's line meets the segment, or its end within a node of it.
|
|
83
|
+
const met = linesMeet(at, dir, [x0, y0], [(x1 - x0) / len, (y1 - y0) / len]);
|
|
84
|
+
if (!met)
|
|
85
|
+
continue;
|
|
86
|
+
const on = nearestOn(held, met.at);
|
|
87
|
+
if (Math.hypot(on[0] - met.at[0], on[1] - met.at[1]) <= NODE_SNAP)
|
|
88
|
+
offer({ at: on, legs: [], cut: held }, run.c);
|
|
89
|
+
}
|
|
90
|
+
else if (alongRun) {
|
|
91
|
+
// Along: the run goes onto the segment's line and ends where it meets the segment.
|
|
92
|
+
const [, c] = runCoords(run, x0, y0);
|
|
93
|
+
if (Math.abs(c - run.c) > NODE_SNAP &&
|
|
94
|
+
(hold || Math.abs(c - run.walked) > pull || !mayLay(runLine(run, c))))
|
|
95
|
+
continue;
|
|
96
|
+
const on = onRun(run, end, c);
|
|
97
|
+
const t = segmentParam(on[0], on[1], x0, y0, x1, y1);
|
|
98
|
+
const nearest = nearestOn(held, on);
|
|
99
|
+
const inside = t > 0 && t < 1 && Math.hypot(nearest[0] - on[0], nearest[1] - on[1]) <= NODE_SNAP;
|
|
100
|
+
offer(inside ? { at: on, legs: [], cut: held } : { at: nearest, legs: [] }, c);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (best) {
|
|
104
|
+
const { tie, c } = best;
|
|
105
|
+
run.c = c;
|
|
106
|
+
return tie;
|
|
107
|
+
}
|
|
108
|
+
// An elbow to the nearest line a free way reaches, nearest first.
|
|
109
|
+
const byDistance = near
|
|
110
|
+
.map((held) => ({ held, on: nearestOn(held, at) }))
|
|
111
|
+
.sort((p, q) => Math.hypot(p.on[0] - at[0], p.on[1] - at[1]) -
|
|
112
|
+
Math.hypot(q.on[0] - at[0], q.on[1] - at[1]));
|
|
113
|
+
for (const { held, on } of byDistance) {
|
|
114
|
+
const link = linkOnto(run.frame, at, held, on, mayLay);
|
|
115
|
+
if (link)
|
|
116
|
+
return { at, legs: link.legs, cut: held };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Cuts a segment where a tie lands, so the meeting is a node.
|
|
122
|
+
* @param tie The tie.
|
|
123
|
+
*/
|
|
124
|
+
const cutAt = (tie) => {
|
|
125
|
+
if (!tie.cut)
|
|
126
|
+
return;
|
|
127
|
+
const at = tie.legs.length ? lastPoint(tie.legs) : tie.at;
|
|
128
|
+
const id = store.segmentAt(at[0], at[1]);
|
|
129
|
+
if (id >= 0)
|
|
130
|
+
store.splitAt(id, at[0], at[1]);
|
|
131
|
+
};
|
|
132
|
+
const lastPoint = (legs) => {
|
|
133
|
+
const [, , x, y] = legs[legs.length - 1];
|
|
134
|
+
return [x, y];
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* The corner between two consecutive runs: where their lines meet.
|
|
138
|
+
* @param a The first run.
|
|
139
|
+
* @param b The next.
|
|
140
|
+
* @returns The corner, or null when the lines meet nowhere near.
|
|
141
|
+
*/
|
|
142
|
+
const cornerOf = (a, b) => {
|
|
143
|
+
if (a.frame === b.frame) {
|
|
144
|
+
if (a.horiz === b.horiz)
|
|
145
|
+
return null;
|
|
146
|
+
return a.horiz ? placeIn(a.frame, b.c, a.c) : placeIn(a.frame, a.c, b.c);
|
|
147
|
+
}
|
|
148
|
+
const aEnd = onRun(a, a.sign > 0 ? a.hi : a.lo, a.c);
|
|
149
|
+
const bEnd = onRun(b, b.sign > 0 ? b.lo : b.hi, b.c);
|
|
150
|
+
const met = linesMeet(aEnd, runDirection(a), bEnd, runDirection(b));
|
|
151
|
+
if (!met)
|
|
152
|
+
return null;
|
|
153
|
+
const nearA = Math.hypot(...diff(met.at, aEnd));
|
|
154
|
+
const nearB = Math.hypot(...diff(met.at, bEnd));
|
|
155
|
+
return nearA <= cfg.pitch && nearB <= cfg.pitch ? met.at : null;
|
|
156
|
+
};
|
|
157
|
+
const diff = (p, q) => [p[0] - q[0], p[1] - q[1]];
|
|
158
|
+
/**
|
|
159
|
+
* What stands at a path's end cell: the tip there, or the network when a line runs within a
|
|
160
|
+
* pitch of the cell.
|
|
161
|
+
* @param cell The cell.
|
|
162
|
+
* @returns The tip, `'network'`, or null.
|
|
163
|
+
*/
|
|
164
|
+
const endAt = (cell) => {
|
|
165
|
+
const tip = tipAt.get(cell);
|
|
166
|
+
if (tip)
|
|
167
|
+
return tip;
|
|
168
|
+
if (onNetwork[cell])
|
|
169
|
+
return 'network';
|
|
170
|
+
const [px, py] = router.worldOf(cell);
|
|
171
|
+
const near = store.nearest(px, py);
|
|
172
|
+
return near && near.away <= router.pitch ? 'network' : null;
|
|
173
|
+
};
|
|
174
|
+
const isTip = (end) => end !== 'network';
|
|
175
|
+
const layPlaced = (cells, head) => {
|
|
176
|
+
const runs = runsOf(router, cells);
|
|
177
|
+
const first = cells[0];
|
|
178
|
+
const last = cells[cells.length - 1];
|
|
179
|
+
const headEnd = head ?? endAt(first);
|
|
180
|
+
const tailEnd = endAt(last);
|
|
181
|
+
if (!headEnd || !tailEnd)
|
|
182
|
+
return false;
|
|
183
|
+
// No run: the two ends stand in one cell, joined by a leg.
|
|
184
|
+
if (!runs.length) {
|
|
185
|
+
const a = isTip(headEnd) ? headEnd.at : null;
|
|
186
|
+
const b = isTip(tailEnd) ? tailEnd.at : null;
|
|
187
|
+
if (a && b) {
|
|
188
|
+
const legs = legsBetween(frames[0], a, b, mayLay);
|
|
189
|
+
if (!legs)
|
|
190
|
+
return false;
|
|
191
|
+
for (const leg of legs)
|
|
192
|
+
add(leg);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
const tie = elbow(a ?? b ?? router.worldOf(first), router.frameOf(first));
|
|
196
|
+
if (!tie)
|
|
197
|
+
return false;
|
|
198
|
+
layElbow(tie);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
for (const run of runs)
|
|
202
|
+
placeRun(run, place, cfg, mayLay);
|
|
203
|
+
const headRun = runs[0];
|
|
204
|
+
const tailRun = runs[runs.length - 1];
|
|
205
|
+
const headEndAlong = headRun.sign > 0 ? headRun.lo : headRun.hi;
|
|
206
|
+
const tailEndAlong = tailRun.sign > 0 ? tailRun.hi : tailRun.lo;
|
|
207
|
+
// Both ties share a single run's line: the first tie settles it and the second holds it, a
|
|
208
|
+
// tip's tie first — the tip is what the path exists for, so its line wins.
|
|
209
|
+
const single = runs.length === 1;
|
|
210
|
+
const tieHead = (hold) => isTip(headEnd)
|
|
211
|
+
? tieTip(headRun, headEnd, headEndAlong, hold)
|
|
212
|
+
: tieNetwork(headRun, headEndAlong, hold);
|
|
213
|
+
const tieTail = (hold) => isTip(tailEnd)
|
|
214
|
+
? tieTip(tailRun, tailEnd, tailEndAlong, hold)
|
|
215
|
+
: tieNetwork(tailRun, tailEndAlong, hold);
|
|
216
|
+
const headFirst = !single || isTip(headEnd) || !isTip(tailEnd);
|
|
217
|
+
const headTie = headFirst ? tieHead(false) : null;
|
|
218
|
+
const tailTie = headFirst ? headTie && tieTail(single) : tieTail(false);
|
|
219
|
+
const heldHead = headFirst ? headTie : tailTie && tieHead(true);
|
|
220
|
+
if (!heldHead || !tailTie)
|
|
221
|
+
return false;
|
|
222
|
+
// Each run goes from its start to its end: the head tie, a corner between each two runs, the
|
|
223
|
+
// tail tie. Runs whose lines meet nowhere near end at their own cells, a jog between them.
|
|
224
|
+
const headTied = heldHead;
|
|
225
|
+
const starts = [headTied.at];
|
|
226
|
+
const ends = [];
|
|
227
|
+
const jogs = [];
|
|
228
|
+
for (let i = 0; i + 1 < runs.length; i++) {
|
|
229
|
+
const corner = cornerOf(runs[i], runs[i + 1]);
|
|
230
|
+
if (corner) {
|
|
231
|
+
ends.push(corner);
|
|
232
|
+
starts.push(corner);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const a = onRun(runs[i], runs[i].sign > 0 ? runs[i].hi : runs[i].lo, runs[i].c);
|
|
236
|
+
const b = onRun(runs[i + 1], runs[i + 1].sign > 0 ? runs[i + 1].lo : runs[i + 1].hi, runs[i + 1].c);
|
|
237
|
+
ends.push(a);
|
|
238
|
+
starts.push(b);
|
|
239
|
+
jogs.push(...(legsBetween(runs[i].frame, a, b, mayLay) ?? [[a[0], a[1], b[0], b[1]]]));
|
|
240
|
+
}
|
|
241
|
+
ends.push(tailTie.at);
|
|
242
|
+
// A run that turned back on itself is a path the placement bent double.
|
|
243
|
+
const pieces = [];
|
|
244
|
+
for (const [i, run] of runs.entries()) {
|
|
245
|
+
const [px, py] = starts[i];
|
|
246
|
+
const [qx, qy] = ends[i];
|
|
247
|
+
const dir = runDirection(run);
|
|
248
|
+
if ((qx - px) * dir[0] + (qy - py) * dir[1] < -NODE_SNAP)
|
|
249
|
+
return false;
|
|
250
|
+
if (Math.hypot(qx - px, qy - py) >= POINT_EPS)
|
|
251
|
+
pieces.push({ seg: [px, py, qx, qy], run });
|
|
252
|
+
}
|
|
253
|
+
if (![...pieces.map((p) => p.seg), ...jogs].every(mayLay))
|
|
254
|
+
return false;
|
|
255
|
+
cutAt(headTied);
|
|
256
|
+
cutAt(tailTie);
|
|
257
|
+
for (const leg of [...headTied.legs, ...tailTie.legs, ...jogs])
|
|
258
|
+
add(leg);
|
|
259
|
+
for (const { seg, run } of pieces) {
|
|
260
|
+
add(seg);
|
|
261
|
+
claim(seg, run.cells);
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* An elbow from a point to the nearest line a free way reaches.
|
|
267
|
+
* @param from The point.
|
|
268
|
+
* @param frame The frame the legs run along.
|
|
269
|
+
* @returns The elbow, or null when no line can be reached.
|
|
270
|
+
*/
|
|
271
|
+
const elbow = (from, frame) => {
|
|
272
|
+
const found = store.nearestLawful(from[0], from[1], (_, qx, qy) => mayLay([from[0], from[1], qx, qy]));
|
|
273
|
+
if (!found)
|
|
274
|
+
return null;
|
|
275
|
+
const link = linkOnto(frame, from, store.coords(found.id), found.at, mayLay);
|
|
276
|
+
return link && { legs: link.legs, index: found.id, at: link.at };
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Lays an elbow: cuts the line it lands on and adds its legs.
|
|
280
|
+
* @param tie The elbow.
|
|
281
|
+
*/
|
|
282
|
+
const layElbow = (tie) => {
|
|
283
|
+
if (tie.index !== undefined && tie.at)
|
|
284
|
+
store.splitAt(tie.index, tie.at[0], tie.at[1]);
|
|
285
|
+
for (const leg of tie.legs)
|
|
286
|
+
add(leg);
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Lays a path where it was walked, its ends tied by elbows: the fallback when placement fails.
|
|
290
|
+
* @param cells The path.
|
|
291
|
+
* @param head The tip the path starts at, when it starts at one.
|
|
292
|
+
* @returns False when an end could not be tied; nothing is laid then.
|
|
293
|
+
*/
|
|
294
|
+
const layPlain = (cells, head) => {
|
|
295
|
+
const walked = router.segmentsAlong(cells);
|
|
296
|
+
if (!walked.length)
|
|
297
|
+
return false;
|
|
298
|
+
// The fallback keeps the placed path's law: a walked segment that clips something blocked is
|
|
299
|
+
// replaced by an elbow around it, or the path is not laid at all.
|
|
300
|
+
const segs = [];
|
|
301
|
+
for (const seg of walked) {
|
|
302
|
+
if (mayLay(seg)) {
|
|
303
|
+
segs.push(seg);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const legs = legsBetween(router.frameOf(cells[0]), [seg[0], seg[1]], [seg[2], seg[3]], mayLay);
|
|
307
|
+
if (!legs)
|
|
308
|
+
return false;
|
|
309
|
+
segs.push(...legs);
|
|
310
|
+
}
|
|
311
|
+
const first = [segs[0][0], segs[0][1]];
|
|
312
|
+
const last = [segs[segs.length - 1][2], segs[segs.length - 1][3]];
|
|
313
|
+
const headEnd = head ?? endAt(cells[0]);
|
|
314
|
+
const tailEnd = endAt(cells[cells.length - 1]);
|
|
315
|
+
if (!headEnd || !tailEnd)
|
|
316
|
+
return false;
|
|
317
|
+
// Both ends first: an end that cannot be tied leaves nothing behind.
|
|
318
|
+
const ties = [];
|
|
319
|
+
for (const [point, end, cell] of [
|
|
320
|
+
[first, headEnd, cells[0]],
|
|
321
|
+
[last, tailEnd, cells[cells.length - 1]],
|
|
322
|
+
]) {
|
|
323
|
+
const frame = router.frameOf(cell);
|
|
324
|
+
const legs = isTip(end) ? legsBetween(frame, end.at, point, mayLay) : null;
|
|
325
|
+
const tie = isTip(end) ? legs && { legs } : elbow(point, frame);
|
|
326
|
+
if (!tie)
|
|
327
|
+
return false;
|
|
328
|
+
ties.push(tie);
|
|
329
|
+
}
|
|
330
|
+
for (const tie of ties)
|
|
331
|
+
layElbow(tie);
|
|
332
|
+
for (const seg of segs)
|
|
333
|
+
add(seg);
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
336
|
+
const layPath = (cells, head, free = place.mayLay) => {
|
|
337
|
+
mayLay = free;
|
|
338
|
+
try {
|
|
339
|
+
return layPlaced(cells, head) || layPlain(cells, head);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
mayLay = place.mayLay;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return layPath;
|
|
346
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Connector } from './connect.js';
|
|
2
|
+
import { type Frame } from './geom/frames.js';
|
|
3
|
+
import type { Segment } from './geom/types.js';
|
|
4
|
+
import type { NetworkStore } from './network-store.js';
|
|
5
|
+
import type { Router } from './router.js';
|
|
6
|
+
/**
|
|
7
|
+
* Cuts shortcut links into the network where the tree forces a detour.
|
|
8
|
+
* @param store The network; extended in place.
|
|
9
|
+
* @param router The raster router that lays each link.
|
|
10
|
+
* @param connector Lays each link on the lines of its way.
|
|
11
|
+
* @returns How many links were cut in.
|
|
12
|
+
*/
|
|
13
|
+
export declare function shortcutDetours(store: NetworkStore, router: Router, connector: Connector): number;
|
|
14
|
+
/**
|
|
15
|
+
* Removes every line that ends nowhere: a dangling end that is no tip goes, and the end it uncovers
|
|
16
|
+
* is judged the same way, back to the first junction or tip. The one line a tip has stays, whatever
|
|
17
|
+
* its other end: the tip needs it.
|
|
18
|
+
* @param store The network; trimmed in place.
|
|
19
|
+
* @param tips Keys of the tips: the ends a line may stop at.
|
|
20
|
+
*/
|
|
21
|
+
export declare function dropDeadEnds(store: NetworkStore, tips: ReadonlySet<string>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Extends each axis-aligned dangling end to the first line straight ahead, until no end is left to
|
|
24
|
+
* extend.
|
|
25
|
+
* @param store The network; extended in place.
|
|
26
|
+
* @param connector Lays the extensions.
|
|
27
|
+
* @param mayLay Whether an extension may be laid.
|
|
28
|
+
* @param frames The directions the layer is drawn in.
|
|
29
|
+
*/
|
|
30
|
+
export declare function completeLines(store: NetworkStore, connector: Connector, mayLay: (seg: Segment) => boolean, frames: readonly Frame[]): void;
|
|
31
|
+
//# sourceMappingURL=post-passes.d.ts.map
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Passes over the laid network: dead ends dropped, dangling ends run on to the line ahead, and links
|
|
2
|
+
// cut across where the network would send a visitor the long way round.
|
|
3
|
+
import { cellArray } from './cell-array.js';
|
|
4
|
+
import { nearestFrame, toFrame } from './geom/frames.js';
|
|
5
|
+
import { pointKey } from './geom/point-key.js';
|
|
6
|
+
import { rayVsSegment } from './geom/primitives.js';
|
|
7
|
+
import { NODE_SNAP } from './graph-types.js';
|
|
8
|
+
import { MinHeap } from './min-heap.js';
|
|
9
|
+
import { chainSegments, chainsFromEdges, graphOf } from './network-graph.js';
|
|
10
|
+
import { MAX_STRETCH } from './network-sizes.js';
|
|
11
|
+
/** Links one pass may cut in, so open floor cannot turn the tree into a mesh. */
|
|
12
|
+
const MAX_SHORTCUTS = 24;
|
|
13
|
+
/** Rounds per pass; each reads the whole floor once (the costly part) and cuts in all it finds. */
|
|
14
|
+
const SHORTCUT_ROUNDS = 3;
|
|
15
|
+
/** Links one round proposes; no two share an endpoint. */
|
|
16
|
+
const SHORTCUTS_PER_ROUND = 8;
|
|
17
|
+
/** Network-walk steps per round, so a dense plan cannot stall the compile. */
|
|
18
|
+
const SHORTCUT_BUDGET = 200_000;
|
|
19
|
+
/**
|
|
20
|
+
* Network distance from `source` to `target`, abandoned past `limit` so near pairs stay cheap.
|
|
21
|
+
* @param graph The network graph.
|
|
22
|
+
* @param source Where to walk from.
|
|
23
|
+
* @param target Where to walk to.
|
|
24
|
+
* @param limit Distance past which the answer stops mattering.
|
|
25
|
+
* @param budget Relaxations left to the whole pass; spent here.
|
|
26
|
+
* @param budget.left Relaxations left.
|
|
27
|
+
* @returns The distance, or `Infinity` when past `limit`, on another fragment, or over budget.
|
|
28
|
+
*/
|
|
29
|
+
function walkWithin(graph, source, target, limit, budget) {
|
|
30
|
+
const dist = new Map([[source, 0]]);
|
|
31
|
+
const heap = new MinHeap();
|
|
32
|
+
heap.push(0, source);
|
|
33
|
+
while (heap.size) {
|
|
34
|
+
const u = heap.pop();
|
|
35
|
+
const du = heap.poppedKey;
|
|
36
|
+
if (du > limit)
|
|
37
|
+
return Infinity;
|
|
38
|
+
if (u === target)
|
|
39
|
+
return du;
|
|
40
|
+
if (du > (dist.get(u) ?? Infinity))
|
|
41
|
+
continue;
|
|
42
|
+
for (const [v, len] of graph.adj[u]) {
|
|
43
|
+
if (budget.left-- <= 0)
|
|
44
|
+
return Infinity;
|
|
45
|
+
const d = du + len;
|
|
46
|
+
if (d >= (dist.get(v) ?? Infinity))
|
|
47
|
+
continue;
|
|
48
|
+
dist.set(v, d);
|
|
49
|
+
heap.push(d, v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Infinity;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Where the network sends a visitor the long way round. Each free cell is claimed by its nearest
|
|
56
|
+
* network vertex across the floor; where two claims meet, the floor walk is compared with the
|
|
57
|
+
* network walk.
|
|
58
|
+
* @param soup The network segments.
|
|
59
|
+
* @param router The raster router, for the walk across open floor.
|
|
60
|
+
* @param taken Points a link already leaves from (one link per point); picks are added.
|
|
61
|
+
* @returns Pairs of network points worth joining; empty when every walk is direct enough.
|
|
62
|
+
*/
|
|
63
|
+
function worstDetours(soup, router, taken) {
|
|
64
|
+
const graph = graphOf(soup);
|
|
65
|
+
if (graph.nodes.length < 2)
|
|
66
|
+
return [];
|
|
67
|
+
const cells = router.cells;
|
|
68
|
+
const cost = cellArray(Float64Array, cells).fill(Infinity);
|
|
69
|
+
const label = cellArray(Int32Array, cells).fill(-1);
|
|
70
|
+
const heap = new MinHeap();
|
|
71
|
+
for (const [x0, y0, x1, y1] of soup)
|
|
72
|
+
for (const c of router.cellsAlong(x0, y0, x1, y1)) {
|
|
73
|
+
if (!router.ok[c])
|
|
74
|
+
continue;
|
|
75
|
+
const [cx, cy] = router.worldOf(c);
|
|
76
|
+
const da = Math.hypot(cx - x0, cy - y0);
|
|
77
|
+
const db = Math.hypot(cx - x1, cy - y1);
|
|
78
|
+
const near = da <= db ? pointKey(x0, y0) : pointKey(x1, y1);
|
|
79
|
+
const d = Math.min(da, db);
|
|
80
|
+
if (d >= cost[c])
|
|
81
|
+
continue;
|
|
82
|
+
cost[c] = d;
|
|
83
|
+
label[c] = graph.nodeId.get(near) ?? -1;
|
|
84
|
+
heap.push(d, c);
|
|
85
|
+
}
|
|
86
|
+
while (heap.size) {
|
|
87
|
+
const c = heap.pop();
|
|
88
|
+
if (heap.poppedKey > cost[c])
|
|
89
|
+
continue;
|
|
90
|
+
for (let nd = 0; nd < 4; nd++) {
|
|
91
|
+
const nc = router.stepTo(c, nd);
|
|
92
|
+
if (nc < 0)
|
|
93
|
+
continue;
|
|
94
|
+
const d = cost[c] + router.pitch;
|
|
95
|
+
if (d >= cost[nc])
|
|
96
|
+
continue;
|
|
97
|
+
cost[nc] = d;
|
|
98
|
+
label[nc] = label[c];
|
|
99
|
+
heap.push(d, nc);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Each claim boundary is one candidate; right and down only, so a pair is weighed once.
|
|
103
|
+
const meeting = new Map();
|
|
104
|
+
for (let c = 0; c < cells; c++) {
|
|
105
|
+
if (label[c] < 0)
|
|
106
|
+
continue;
|
|
107
|
+
for (const nd of [0, 2]) {
|
|
108
|
+
const nc = router.stepTo(c, nd);
|
|
109
|
+
if (nc < 0 || label[nc] < 0 || label[nc] === label[c])
|
|
110
|
+
continue;
|
|
111
|
+
const u = Math.min(label[c], label[nc]);
|
|
112
|
+
const v = Math.max(label[c], label[nc]);
|
|
113
|
+
const free = cost[c] + cost[nc] + router.pitch;
|
|
114
|
+
const key = `${u}|${v}`;
|
|
115
|
+
const known = meeting.get(key);
|
|
116
|
+
if (!known || free < known.free)
|
|
117
|
+
meeting.set(key, { u, v, free });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Longest floor walk first: an accepted pair saves more than its floor walk, so this ranks by
|
|
121
|
+
// saving, not by ratio (lines half a meter apart have a huge ratio and save nothing).
|
|
122
|
+
const ranked = [...meeting.values()]
|
|
123
|
+
.filter(({ free }) => free > 0)
|
|
124
|
+
.sort((a, b) => b.free - a.free);
|
|
125
|
+
const budget = { left: SHORTCUT_BUDGET };
|
|
126
|
+
const picks = [];
|
|
127
|
+
// The network's direction at a vertex: along its first line.
|
|
128
|
+
const along = (u) => {
|
|
129
|
+
const [v] = graph.adj[u][0];
|
|
130
|
+
return [graph.nodes[v][0] - graph.nodes[u][0], graph.nodes[v][1] - graph.nodes[u][1]];
|
|
131
|
+
};
|
|
132
|
+
for (const { u, v, free } of ranked) {
|
|
133
|
+
if (picks.length >= SHORTCUTS_PER_ROUND || budget.left <= 0)
|
|
134
|
+
break;
|
|
135
|
+
const ku = pointKey(graph.nodes[u][0], graph.nodes[u][1]);
|
|
136
|
+
const kv = pointKey(graph.nodes[v][0], graph.nodes[v][1]);
|
|
137
|
+
if (taken.has(ku) || taken.has(kv))
|
|
138
|
+
continue;
|
|
139
|
+
const limit = MAX_STRETCH * free;
|
|
140
|
+
if (walkWithin(graph, u, v, limit, budget) <= limit)
|
|
141
|
+
continue;
|
|
142
|
+
taken.add(ku);
|
|
143
|
+
taken.add(kv);
|
|
144
|
+
picks.push({
|
|
145
|
+
from: graph.nodes[u],
|
|
146
|
+
to: graph.nodes[v],
|
|
147
|
+
fromAlong: along(u),
|
|
148
|
+
toAlong: along(v),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return picks;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Cuts shortcut links into the network where the tree forces a detour.
|
|
155
|
+
* @param store The network; extended in place.
|
|
156
|
+
* @param router The raster router that lays each link.
|
|
157
|
+
* @param connector Lays each link on the lines of its way.
|
|
158
|
+
* @returns How many links were cut in.
|
|
159
|
+
*/
|
|
160
|
+
export function shortcutDetours(store, router, connector) {
|
|
161
|
+
const isTarget = cellArray(Uint8Array, router.cells);
|
|
162
|
+
const taken = new Set();
|
|
163
|
+
let cut = 0;
|
|
164
|
+
for (let round = 0; round < SHORTCUT_ROUNDS && cut < MAX_SHORTCUTS; round++) {
|
|
165
|
+
const fixes = worstDetours(store.toArray(), router, taken);
|
|
166
|
+
if (!fixes.length)
|
|
167
|
+
return cut;
|
|
168
|
+
for (const fix of fixes) {
|
|
169
|
+
if (cut >= MAX_SHORTCUTS)
|
|
170
|
+
return cut;
|
|
171
|
+
cut += cutIn(router, connector, isTarget, fix) ? 1 : 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return cut;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Lays one link across the floor into the network.
|
|
178
|
+
* @param router The raster router.
|
|
179
|
+
* @param connector Lays the link.
|
|
180
|
+
* @param isTarget Scratch target mask, reused between links.
|
|
181
|
+
* @param fix The two network points to join.
|
|
182
|
+
* @returns True when the link was laid.
|
|
183
|
+
*/
|
|
184
|
+
function cutIn(router, connector, isTarget, fix) {
|
|
185
|
+
// Each end on the grid of the network's direction there, so the link meets it square.
|
|
186
|
+
const from = router.cellOf(fix.from[0], fix.from[1], fix.fromAlong[0], fix.fromAlong[1]);
|
|
187
|
+
const to = router.cellOf(fix.to[0], fix.to[1], fix.toAlong[0], fix.toAlong[1]);
|
|
188
|
+
if (from < 0 || to < 0 || !router.ok[from] || !router.ok[to])
|
|
189
|
+
return false;
|
|
190
|
+
isTarget.fill(0);
|
|
191
|
+
isTarget[to] = 1;
|
|
192
|
+
const cells = router.route([from], isTarget);
|
|
193
|
+
return cells !== null && cells.length >= 2 && connector.layPath(cells, null);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Removes every line that ends nowhere: a dangling end that is no tip goes, and the end it uncovers
|
|
197
|
+
* is judged the same way, back to the first junction or tip. The one line a tip has stays, whatever
|
|
198
|
+
* its other end: the tip needs it.
|
|
199
|
+
* @param store The network; trimmed in place.
|
|
200
|
+
* @param tips Keys of the tips: the ends a line may stop at.
|
|
201
|
+
*/
|
|
202
|
+
export function dropDeadEnds(store, tips) {
|
|
203
|
+
for (;;) {
|
|
204
|
+
const degree = new Map();
|
|
205
|
+
const at = new Map();
|
|
206
|
+
const other = new Map();
|
|
207
|
+
store.forEach((id, x0, y0, x1, y1) => {
|
|
208
|
+
const keys = [pointKey(x0, y0), pointKey(x1, y1)];
|
|
209
|
+
other.set(id, keys);
|
|
210
|
+
for (const key of keys) {
|
|
211
|
+
degree.set(key, (degree.get(key) ?? 0) + 1);
|
|
212
|
+
const list = at.get(key);
|
|
213
|
+
if (list)
|
|
214
|
+
list.push(id);
|
|
215
|
+
else
|
|
216
|
+
at.set(key, [id]);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
let dropped = 0;
|
|
220
|
+
for (const [key, count] of degree) {
|
|
221
|
+
if (count !== 1 || tips.has(key))
|
|
222
|
+
continue;
|
|
223
|
+
for (const id of at.get(key) ?? []) {
|
|
224
|
+
const far = other.get(id).find((k) => k !== key);
|
|
225
|
+
if (tips.has(far) && degree.get(far) <= 1)
|
|
226
|
+
continue;
|
|
227
|
+
store.remove(id);
|
|
228
|
+
degree.set(far, degree.get(far) - 1);
|
|
229
|
+
degree.set(key, 0);
|
|
230
|
+
dropped++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!dropped)
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Extends each axis-aligned dangling end to the first line straight ahead, until no end is left to
|
|
239
|
+
* extend.
|
|
240
|
+
* @param store The network; extended in place.
|
|
241
|
+
* @param connector Lays the extensions.
|
|
242
|
+
* @param mayLay Whether an extension may be laid.
|
|
243
|
+
* @param frames The directions the layer is drawn in.
|
|
244
|
+
*/
|
|
245
|
+
export function completeLines(store, connector, mayLay, frames) {
|
|
246
|
+
const meet = [0, 0];
|
|
247
|
+
const raySeg = (px, py, dx, dy, seg) => {
|
|
248
|
+
if (!rayVsSegment(meet, px, py, dx, dy, seg[0], seg[1], seg[2], seg[3]))
|
|
249
|
+
return Infinity;
|
|
250
|
+
const [s, u] = meet;
|
|
251
|
+
return s > 0.1 && u >= 0 && u <= 1 ? s : Infinity;
|
|
252
|
+
};
|
|
253
|
+
for (;;) {
|
|
254
|
+
const chains = chainsFromEdges(store.toArray());
|
|
255
|
+
const graph = graphOf(chainSegments(chains));
|
|
256
|
+
const segs = chainSegments(chains);
|
|
257
|
+
let added = 0;
|
|
258
|
+
for (let n = 0; n < graph.nodes.length; n++) {
|
|
259
|
+
if (graph.adj[n].length !== 1)
|
|
260
|
+
continue;
|
|
261
|
+
const nb = graph.adj[n][0][0];
|
|
262
|
+
const [nx, ny] = graph.nodes[n];
|
|
263
|
+
const [bx, by] = graph.nodes[nb];
|
|
264
|
+
const len = Math.hypot(nx - bx, ny - by);
|
|
265
|
+
if (len < 0.05)
|
|
266
|
+
continue;
|
|
267
|
+
// Continue along the nearest axis of the nearest direction.
|
|
268
|
+
const frame = frames[nearestFrame(frames, nx - bx, ny - by)];
|
|
269
|
+
const [du, dv] = toFrame(frame, (nx - bx) / len, (ny - by) / len);
|
|
270
|
+
if (Math.abs(du) > 0.1 && Math.abs(dv) > 0.1)
|
|
271
|
+
continue;
|
|
272
|
+
const [dx, dy] = Math.abs(du) > Math.abs(dv)
|
|
273
|
+
? [frame.cos * Math.sign(du), frame.sin * Math.sign(du)]
|
|
274
|
+
: [-frame.sin * Math.sign(dv), frame.cos * Math.sign(dv)];
|
|
275
|
+
let hit = Infinity;
|
|
276
|
+
let hitSi = -1;
|
|
277
|
+
for (let si = 0; si < segs.length; si++) {
|
|
278
|
+
const s = raySeg(nx, ny, dx, dy, segs[si]);
|
|
279
|
+
if (s < hit) {
|
|
280
|
+
hit = s;
|
|
281
|
+
hitSi = si;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (hitSi < 0)
|
|
285
|
+
continue;
|
|
286
|
+
// The ray hits a chain chord whose pruned vertices may sit a snap off the stored lines: the
|
|
287
|
+
// extension ends on the nearest real segment, or nowhere — a chord anchors no line.
|
|
288
|
+
const near = store.nearest(nx + dx * hit, ny + dy * hit);
|
|
289
|
+
if (!near || near.away > NODE_SNAP)
|
|
290
|
+
continue;
|
|
291
|
+
const qx = +near.at[0].toFixed(2);
|
|
292
|
+
const qy = +near.at[1].toFixed(2);
|
|
293
|
+
if (Math.hypot(qx - nx, qy - ny) <= NODE_SNAP || !mayLay([nx, ny, qx, qy]))
|
|
294
|
+
continue;
|
|
295
|
+
// A chain may span several stored segments: cut the one carrying the hit, if any.
|
|
296
|
+
const carrier = store.segmentAt(qx, qy);
|
|
297
|
+
if (carrier >= 0)
|
|
298
|
+
store.splitAt(carrier, qx, qy);
|
|
299
|
+
connector.add([nx, ny, qx, qy]);
|
|
300
|
+
added++;
|
|
301
|
+
}
|
|
302
|
+
if (!added)
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|