@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.
Files changed (110) hide show
  1. package/README.md +446 -0
  2. package/dist/index.d.ts +14 -0
  3. package/dist/index.js +3 -0
  4. package/dist/lib/attach.d.ts +43 -0
  5. package/dist/lib/attach.js +199 -0
  6. package/dist/lib/axes.d.ts +76 -0
  7. package/dist/lib/axes.js +103 -0
  8. package/dist/lib/blockers.d.ts +134 -0
  9. package/dist/lib/blockers.js +289 -0
  10. package/dist/lib/cell-array.d.ts +14 -0
  11. package/dist/lib/cell-array.js +25 -0
  12. package/dist/lib/centering.d.ts +14 -0
  13. package/dist/lib/centering.js +88 -0
  14. package/dist/lib/compile-layer.d.ts +41 -0
  15. package/dist/lib/compile-layer.js +241 -0
  16. package/dist/lib/compile-wayfinding.d.ts +90 -0
  17. package/dist/lib/compile-wayfinding.js +203 -0
  18. package/dist/lib/connect.d.ts +35 -0
  19. package/dist/lib/connect.js +206 -0
  20. package/dist/lib/diagnostics.d.ts +190 -0
  21. package/dist/lib/diagnostics.js +634 -0
  22. package/dist/lib/doorways.d.ts +78 -0
  23. package/dist/lib/doorways.js +331 -0
  24. package/dist/lib/emit-network.d.ts +47 -0
  25. package/dist/lib/emit-network.js +105 -0
  26. package/dist/lib/emit-v1.d.ts +31 -0
  27. package/dist/lib/emit-v1.js +56 -0
  28. package/dist/lib/entries.d.ts +123 -0
  29. package/dist/lib/entries.js +448 -0
  30. package/dist/lib/floor-grids.d.ts +48 -0
  31. package/dist/lib/floor-grids.js +185 -0
  32. package/dist/lib/framing.d.ts +20 -0
  33. package/dist/lib/framing.js +187 -0
  34. package/dist/lib/geom/boolean-ops.d.ts +21 -0
  35. package/dist/lib/geom/boolean-ops.js +1 -0
  36. package/dist/lib/geom/clipper2-ops.d.ts +7 -0
  37. package/dist/lib/geom/clipper2-ops.js +50 -0
  38. package/dist/lib/geom/footprint.d.ts +26 -0
  39. package/dist/lib/geom/footprint.js +203 -0
  40. package/dist/lib/geom/frames.d.ts +116 -0
  41. package/dist/lib/geom/frames.js +233 -0
  42. package/dist/lib/geom/mesh-outline.d.ts +13 -0
  43. package/dist/lib/geom/mesh-outline.js +90 -0
  44. package/dist/lib/geom/normalize.d.ts +28 -0
  45. package/dist/lib/geom/normalize.js +118 -0
  46. package/dist/lib/geom/point-key.d.ts +16 -0
  47. package/dist/lib/geom/point-key.js +15 -0
  48. package/dist/lib/geom/primitives.d.ts +105 -0
  49. package/dist/lib/geom/primitives.js +184 -0
  50. package/dist/lib/geom/split-crossings.d.ts +14 -0
  51. package/dist/lib/geom/split-crossings.js +171 -0
  52. package/dist/lib/geom/types.d.ts +9 -0
  53. package/dist/lib/geom/types.js +1 -0
  54. package/dist/lib/geom/walls.d.ts +31 -0
  55. package/dist/lib/geom/walls.js +71 -0
  56. package/dist/lib/graph-invariants.d.ts +49 -0
  57. package/dist/lib/graph-invariants.js +129 -0
  58. package/dist/lib/graph-types.d.ts +143 -0
  59. package/dist/lib/graph-types.js +7 -0
  60. package/dist/lib/grid-network.d.ts +20 -0
  61. package/dist/lib/grid-network.js +349 -0
  62. package/dist/lib/lattice-map.d.ts +19 -0
  63. package/dist/lib/lattice-map.js +92 -0
  64. package/dist/lib/lattice-network.d.ts +32 -0
  65. package/dist/lib/lattice-network.js +289 -0
  66. package/dist/lib/mask.d.ts +54 -0
  67. package/dist/lib/mask.js +210 -0
  68. package/dist/lib/min-heap.d.ts +24 -0
  69. package/dist/lib/min-heap.js +73 -0
  70. package/dist/lib/network-build.d.ts +21 -0
  71. package/dist/lib/network-build.js +4 -0
  72. package/dist/lib/network-graph.d.ts +80 -0
  73. package/dist/lib/network-graph.js +467 -0
  74. package/dist/lib/network-setup.d.ts +50 -0
  75. package/dist/lib/network-setup.js +76 -0
  76. package/dist/lib/network-sizes.d.ts +29 -0
  77. package/dist/lib/network-sizes.js +22 -0
  78. package/dist/lib/network-store.d.ts +173 -0
  79. package/dist/lib/network-store.js +430 -0
  80. package/dist/lib/openings.d.ts +54 -0
  81. package/dist/lib/openings.js +387 -0
  82. package/dist/lib/place-path.d.ts +47 -0
  83. package/dist/lib/place-path.js +346 -0
  84. package/dist/lib/post-passes.d.ts +31 -0
  85. package/dist/lib/post-passes.js +305 -0
  86. package/dist/lib/reachability.d.ts +58 -0
  87. package/dist/lib/reachability.js +220 -0
  88. package/dist/lib/report.d.ts +49 -0
  89. package/dist/lib/report.js +374 -0
  90. package/dist/lib/router.d.ts +111 -0
  91. package/dist/lib/router.js +388 -0
  92. package/dist/lib/runs.d.ts +92 -0
  93. package/dist/lib/runs.js +232 -0
  94. package/dist/lib/scale.d.ts +58 -0
  95. package/dist/lib/scale.js +185 -0
  96. package/dist/lib/steiner-network.d.ts +19 -0
  97. package/dist/lib/steiner-network.js +183 -0
  98. package/dist/lib/strip.d.ts +43 -0
  99. package/dist/lib/strip.js +200 -0
  100. package/dist/lib/stubs.d.ts +84 -0
  101. package/dist/lib/stubs.js +316 -0
  102. package/dist/lib/transitions.d.ts +72 -0
  103. package/dist/lib/transitions.js +191 -0
  104. package/dist/lib/types.d.ts +126 -0
  105. package/dist/lib/types.js +1 -0
  106. package/dist/lib/validate-source.d.ts +41 -0
  107. package/dist/lib/validate-source.js +133 -0
  108. package/dist/lib/walkable.d.ts +104 -0
  109. package/dist/lib/walkable.js +140 -0
  110. package/package.json +37 -0
@@ -0,0 +1,80 @@
1
+ import { type Frame } from './geom/frames.js';
2
+ import type { Segment } from './geom/types.js';
3
+ /** A run of network points: a chain of segments through vertices no third segment meets. */
4
+ export interface Chain {
5
+ /** Its points, in order. */
6
+ pts: [number, number][];
7
+ }
8
+ /**
9
+ * Reads a segment soup as chains: every maximal run through vertices of degree two, straightened
10
+ * where it runs straight through a point.
11
+ * @param soup The segments.
12
+ * @param protect Keys of points a chain keeps as vertices however straight it runs through them.
13
+ * @returns The chains, coordinates on the centimeter grid.
14
+ */
15
+ export declare function chainsFromEdges(soup: readonly Segment[], protect?: ReadonlySet<string>): Chain[];
16
+ /** What the joining may read of the floor. */
17
+ interface JoinOptions {
18
+ /** The directions the layer is drawn in. */
19
+ readonly frames?: readonly Frame[];
20
+ /** Whether a segment lies on free floor: across no wall or stand. Everything is free by default. */
21
+ readonly free?: (seg: Segment) => boolean;
22
+ /**
23
+ * How far aside, and how far ahead, the run continuing another may start: a stub's length. Without
24
+ * it no run is pulled over.
25
+ */
26
+ readonly lane?: number;
27
+ }
28
+ /** The lines joined into straight runs, and where that moved a point. */
29
+ interface JoinedRuns {
30
+ /** The runs, each straight stretch one chord. */
31
+ readonly chains: Chain[];
32
+ /** Every vertex a chord replaced, by its key, and where it now lies on the chord. */
33
+ readonly moved: Map<string, [number, number]>;
34
+ }
35
+ /**
36
+ * Joins the chains that continue one another through a shared end, and makes each straight stretch
37
+ * one chord: an aisle is one line from end to end, the stubs meeting it end on its span, and two aisles
38
+ * cross whole. Two pieces continue one another when they run along the same axis of the same frame
39
+ * in opposite senses, or, off every axis, when the joint lies on the chord between its neighbors.
40
+ * Every vertex a chord replaces is moved onto the chord, and so are the ends of the lines that stood
41
+ * on it. A run then continues through the next run along its axis when nothing stands between them
42
+ * on the floor: that run is pulled onto its line, longest runs first, so a line never starts where the
43
+ * one before it could go on. The converter that builds wf.data.js cuts a line where another ends on
44
+ * it or crosses it.
45
+ * @param chains The chains, ends on shared vertices.
46
+ * @param options The layer's directions and its free floor.
47
+ * @returns The joined runs, and where the vertices they replaced now lie.
48
+ */
49
+ export declare function joinStraightRuns(chains: readonly Chain[], options?: JoinOptions): JoinedRuns;
50
+ /** The network as a graph of its vertices. */
51
+ export interface Graph {
52
+ /** Vertex positions, by vertex id. */
53
+ readonly nodes: [number, number][];
54
+ /** Neighbors of each vertex: vertex, length, edge id. */
55
+ readonly adj: [number, number, number][][];
56
+ /** Edge ends, by edge id. */
57
+ readonly edges: [number, number][];
58
+ /** Vertex id by point key. */
59
+ readonly nodeId: Map<string, number>;
60
+ }
61
+ /**
62
+ * Chains as segments, one per pair of neighboring points.
63
+ * @param chains The chains.
64
+ * @returns Their segments, in chain order.
65
+ */
66
+ export declare function chainSegments(chains: readonly Chain[]): Segment[];
67
+ /**
68
+ * Reads segments as a graph; vertices merge by point key.
69
+ * @param segments The segments.
70
+ * @returns The graph.
71
+ */
72
+ export declare function graphOf(segments: readonly Segment[]): Graph;
73
+ /**
74
+ * The connected pieces of a graph.
75
+ * @param graph The graph.
76
+ * @returns Each vertex's piece, by vertex id.
77
+ */
78
+ export declare function components(graph: Graph): Int32Array;
79
+ export {};
80
+ //# sourceMappingURL=network-graph.d.ts.map
@@ -0,0 +1,467 @@
1
+ // The network read as a graph: its segments joined into chains, the chains as vertices and edges,
2
+ // and the connected pieces.
3
+ import { nearestFrame, toFrame } from './geom/frames.js';
4
+ import { POINT_EPS, pointKey } from './geom/point-key.js';
5
+ /**
6
+ * Reads a segment soup as chains: every maximal run through vertices of degree two, straightened
7
+ * where it runs straight through a point.
8
+ * @param soup The segments.
9
+ * @param protect Keys of points a chain keeps as vertices however straight it runs through them.
10
+ * @returns The chains, coordinates on the centimeter grid.
11
+ */
12
+ export function chainsFromEdges(soup, protect) {
13
+ const nodePos = new Map();
14
+ const adjK = new Map();
15
+ const ek = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
16
+ const edgeSet = new Set();
17
+ for (const [x0, y0, x1, y1] of soup) {
18
+ const a = pointKey(x0, y0);
19
+ const b = pointKey(x1, y1);
20
+ if (a === b || edgeSet.has(ek(a, b)))
21
+ continue;
22
+ edgeSet.add(ek(a, b));
23
+ nodePos.set(a, [x0, y0]);
24
+ nodePos.set(b, [x1, y1]);
25
+ if (!adjK.has(a))
26
+ adjK.set(a, []);
27
+ if (!adjK.has(b))
28
+ adjK.set(b, []);
29
+ adjK.get(a).push(b);
30
+ adjK.get(b).push(a);
31
+ }
32
+ const used = new Set();
33
+ const chains = [];
34
+ const trace = (start, next) => {
35
+ const pts = [nodePos.get(start), nodePos.get(next)];
36
+ used.add(ek(start, next));
37
+ let prev = start;
38
+ let cur = next;
39
+ while (adjK.get(cur).length === 2) {
40
+ const nxt = adjK.get(cur).find((k) => k !== prev);
41
+ if (nxt === undefined || used.has(ek(cur, nxt)))
42
+ break;
43
+ used.add(ek(cur, nxt));
44
+ pts.push(nodePos.get(nxt));
45
+ prev = cur;
46
+ cur = nxt;
47
+ }
48
+ return pts;
49
+ };
50
+ for (const [k, nb] of adjK) {
51
+ if (nb.length === 2)
52
+ continue;
53
+ for (const n of nb)
54
+ if (!used.has(ek(k, n)))
55
+ chains.push({ pts: trace(k, n) });
56
+ }
57
+ for (const [k, nb] of adjK)
58
+ for (const n of nb)
59
+ if (!used.has(ek(k, n)))
60
+ chains.push({ pts: trace(k, n) });
61
+ for (const c of chains) {
62
+ const out = [c.pts[0]];
63
+ for (let i = 1; i + 1 < c.pts.length; i++) {
64
+ const [ax, ay] = out[out.length - 1];
65
+ const [bx, by] = c.pts[i];
66
+ const [cx, cy] = c.pts[i + 1];
67
+ const kept = protect?.has(pointKey(bx, by));
68
+ const cross = Math.abs((bx - ax) * (cy - ay) - (by - ay) * (cx - ax));
69
+ const len = Math.hypot(cx - ax, cy - ay) || 1;
70
+ // Drop a point only where the chain runs straight through it (within POINT_EPS): a few-cm jog
71
+ // is a corner, and a chord across it would be off every axis.
72
+ if (kept || cross / len > POINT_EPS)
73
+ out.push(c.pts[i]);
74
+ }
75
+ out.push(c.pts[c.pts.length - 1]);
76
+ c.pts = out.map((p) => [+p[0].toFixed(2), +p[1].toFixed(2)]);
77
+ }
78
+ return chains.filter((c) => c.pts.length >= 2);
79
+ }
80
+ /** How far off a frame axis a line may point and still run along it: about two degrees. */
81
+ const AXIS_TAN = 0.035;
82
+ /**
83
+ * Which frame axis a direction runs along, if any.
84
+ * @param frames The directions the layer is drawn in.
85
+ * @param dx Direction x.
86
+ * @param dy Direction y.
87
+ * @returns The axis, or null off every axis.
88
+ */
89
+ function axisOf(frames, dx, dy) {
90
+ if (!frames.length)
91
+ return null;
92
+ const frame = nearestFrame(frames, dx, dy);
93
+ const [u, v] = toFrame(frames[frame], dx, dy);
94
+ if (Math.abs(v) <= AXIS_TAN * Math.abs(u))
95
+ return { frame, axis: 0, sign: Math.sign(u) };
96
+ if (Math.abs(u) <= AXIS_TAN * Math.abs(v))
97
+ return { frame, axis: 1, sign: Math.sign(v) };
98
+ return null;
99
+ }
100
+ /**
101
+ * Joins the chains that continue one another through a shared end, and makes each straight stretch
102
+ * one chord: an aisle is one line from end to end, the stubs meeting it end on its span, and two aisles
103
+ * cross whole. Two pieces continue one another when they run along the same axis of the same frame
104
+ * in opposite senses, or, off every axis, when the joint lies on the chord between its neighbors.
105
+ * Every vertex a chord replaces is moved onto the chord, and so are the ends of the lines that stood
106
+ * on it. A run then continues through the next run along its axis when nothing stands between them
107
+ * on the floor: that run is pulled onto its line, longest runs first, so a line never starts where the
108
+ * one before it could go on. The converter that builds wf.data.js cuts a line where another ends on
109
+ * it or crosses it.
110
+ * @param chains The chains, ends on shared vertices.
111
+ * @param options The layer's directions and its free floor.
112
+ * @returns The joined runs, and where the vertices they replaced now lie.
113
+ */
114
+ export function joinStraightRuns(chains, options = {}) {
115
+ const frames = options.frames ?? [];
116
+ const free = options.free ?? (() => true);
117
+ const runs = chains.map((c) => c.pts.map((p) => [p[0], p[1]]));
118
+ const owner = runs.map((_, i) => i);
119
+ const find = (i) => {
120
+ while (owner[i] !== i)
121
+ i = owner[i];
122
+ return i;
123
+ };
124
+ const meetings = new Map();
125
+ runs.forEach((pts, i) => {
126
+ if (!pts || pts.length < 2)
127
+ return;
128
+ for (const p of [pts[0], pts[pts.length - 1]]) {
129
+ const key = pointKey(p[0], p[1]);
130
+ meetings.set(key, [...(meetings.get(key) ?? []), i]);
131
+ }
132
+ });
133
+ // A run turned to end at `key`, and its point next to that end.
134
+ const endAt = (i, key) => {
135
+ const pts = runs[i];
136
+ const last = pts[pts.length - 1];
137
+ if (pointKey(last[0], last[1]) === key)
138
+ return { pts, next: pts[pts.length - 2] };
139
+ const first = pts[0];
140
+ if (pointKey(first[0], first[1]) === key)
141
+ return { pts: [...pts].reverse(), next: pts[1] };
142
+ return null;
143
+ };
144
+ // How far from straight two pieces meeting at a joint are: the angle between them, or Infinity.
145
+ const bend = (joint, a, c) => {
146
+ const [ax, ay] = [a[0] - joint[0], a[1] - joint[1]];
147
+ const [cx, cy] = [c[0] - joint[0], c[1] - joint[1]];
148
+ const la = Math.hypot(ax, ay);
149
+ const lc = Math.hypot(cx, cy);
150
+ if (!la || !lc)
151
+ return Infinity;
152
+ const sameAxis = (p, q) => p !== null && q !== null && p.frame === q.frame && p.axis === q.axis && p.sign === -q.sign;
153
+ const angle = Math.acos(Math.max(-1, Math.min(1, -(ax * cx + ay * cy) / (la * lc))));
154
+ if (sameAxis(axisOf(frames, ax, ay), axisOf(frames, cx, cy)))
155
+ return angle;
156
+ // Off the axes: straight through within a centimeter of the chord.
157
+ const len = Math.hypot(c[0] - a[0], c[1] - a[1]) || 1;
158
+ const off = Math.abs((joint[0] - a[0]) * (c[1] - a[1]) - (joint[1] - a[1]) * (c[0] - a[0])) / len;
159
+ const between = ax * cx + ay * cy < 0;
160
+ return off <= POINT_EPS && between ? angle : Infinity;
161
+ };
162
+ for (const key of [...meetings.keys()].sort()) {
163
+ // Every continuing pair at the meeting joins, straightest first: a crossing keeps both lines whole.
164
+ for (;;) {
165
+ const incident = [...new Set(meetings.get(key).map(find))];
166
+ let best = null;
167
+ for (let a = 0; a < incident.length; a++)
168
+ for (let b = a + 1; b < incident.length; b++) {
169
+ const i = incident[a];
170
+ const j = incident[b];
171
+ const ei = endAt(i, key);
172
+ const ej = endAt(j, key);
173
+ if (!ei || !ej)
174
+ continue;
175
+ const angle = bend(ei.pts[ei.pts.length - 1], ei.next, ej.next);
176
+ if (!isFinite(angle))
177
+ continue;
178
+ if (!best || angle < best.angle)
179
+ best = { angle, i, j };
180
+ }
181
+ if (!best)
182
+ break;
183
+ const ei = endAt(best.i, key);
184
+ const ej = endAt(best.j, key);
185
+ runs[best.i] = [...ei.pts, ...[...ej.pts].reverse().slice(1)];
186
+ runs[best.j] = null;
187
+ owner[best.j] = best.i;
188
+ }
189
+ }
190
+ // Each straight stretch of a run becomes its chord; the vertices on it move onto the chord.
191
+ const moved = new Map();
192
+ const round = (p) => [+p[0].toFixed(2), +p[1].toFixed(2)];
193
+ // Each straight stretch of a run becomes its chord, a run of its own; the vertices on it move onto
194
+ // the chord, and the stretch carries their keys.
195
+ const stretches = [];
196
+ const onRun = [];
197
+ const straighten = (pts) => {
198
+ let from = 0;
199
+ for (let i = 1; i < pts.length; i++) {
200
+ const last = i === pts.length - 1;
201
+ if (!last && isFinite(bend(pts[i], pts[i - 1], pts[i + 1])))
202
+ continue;
203
+ const [ax, ay] = pts[from];
204
+ const [cx, cy] = pts[i];
205
+ const len2 = (cx - ax) ** 2 + (cy - ay) ** 2 || 1;
206
+ const keys = [];
207
+ for (let k = from + 1; k < i; k++) {
208
+ const [px, py] = pts[k];
209
+ const t = ((px - ax) * (cx - ax) + (py - ay) * (cy - ay)) / len2;
210
+ moved.set(pointKey(px, py), round([ax + t * (cx - ax), ay + t * (cy - ay)]));
211
+ keys.push(pointKey(px, py));
212
+ }
213
+ stretches.push([pts[from], pts[i]]);
214
+ onRun.push(keys);
215
+ from = i;
216
+ }
217
+ };
218
+ for (const pts of runs)
219
+ if (pts && pts.length >= 2)
220
+ straighten(pts);
221
+ const lanes = options.lane
222
+ ? mergeLanes(stretches, onRun, frames, free, options.lane, moved)
223
+ : stretches;
224
+ // A run's own ends may have stood on another run's chord; a run both ends meet at is gone.
225
+ const at = (p) => moved.get(pointKey(p[0], p[1])) ?? p;
226
+ return {
227
+ chains: lanes
228
+ .map((pts) => ({ pts: pts.map(at) }))
229
+ .filter(({ pts }) => Math.hypot(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]) > POINT_EPS),
230
+ moved,
231
+ };
232
+ }
233
+ /**
234
+ * Pulls each straight run onto the line of the run it continues: the next run along the same axis,
235
+ * when the floor between them is free. Longest runs first, so the longer line keeps its place and the
236
+ * shorter one moves, together with every point that stood on it. Runs keep their authored points as
237
+ * keys; where a point lies now is always read through `moved`.
238
+ * @param runs The straight runs, two points each.
239
+ * @param onRun The keys of the vertices each run's chords replaced.
240
+ * @param frames The directions the layer is drawn in.
241
+ * @param free Whether a segment lies on free floor.
242
+ * @param lane How far aside, and how far ahead, a continuing run may start.
243
+ * @param moved Where the vertices replaced so far lie; extended with every point pulled over.
244
+ * @returns The runs left, the pulled ones merged into the lines they continue, ends where they lie now.
245
+ */
246
+ function mergeLanes(runs, onRun, frames, free, lane, moved) {
247
+ const keyOf = (p) => pointKey(p[0], p[1]);
248
+ const at = (p) => moved.get(keyOf(p)) ?? p;
249
+ // Each run's ends: the authored point (its key) and where it lies now.
250
+ const ends = runs.map((pts) => [pts[0], pts[pts.length - 1]].map((p) => ({ key: keyOf(p), raw: p })));
251
+ const now = (i) => ends[i].map((e) => at(e.raw));
252
+ const axis = runs.map((_, i) => {
253
+ const [a, c] = now(i);
254
+ return axisOf(frames, c[0] - a[0], c[1] - a[1]);
255
+ });
256
+ const length = (i) => {
257
+ const [a, c] = now(i);
258
+ return Math.hypot(c[0] - a[0], c[1] - a[1]);
259
+ };
260
+ const alive = runs.map(() => true);
261
+ const order = runs.map((_, i) => i).sort((a, b) => length(b) - length(a) || a - b);
262
+ const round = (p) => [+p[0].toFixed(2), +p[1].toFixed(2)];
263
+ // The keys of the points on each run's chords, carried along when the run is pulled over.
264
+ const carried = onRun.map((keys) => [...keys]);
265
+ // Runs by the keys of their ends, and by the keys on their chords: what a run takes along when it
266
+ // is pulled over.
267
+ const atKey = new Map();
268
+ const carriedBy = new Map();
269
+ ends.forEach((pair, i) => {
270
+ for (const e of pair)
271
+ atKey.set(e.key, [...(atKey.get(e.key) ?? []), i]);
272
+ });
273
+ carried.forEach((keys, i) => {
274
+ for (const key of keys)
275
+ carriedBy.set(key, [...(carriedBy.get(key) ?? []), i]);
276
+ });
277
+ // Whether everything B takes along can follow it onto A's line. A run ending on one of B's points
278
+ // follows only when it meets B square, and when the move lengthens it or it carries nothing: a run
279
+ // shortened could end before the points that stand on it. A run that carries one of B's ends on
280
+ // its chord would lose that point, so B stays.
281
+ const detachable = (b, project) => {
282
+ const [b0, b1] = now(b);
283
+ const follows = (was, r) => {
284
+ const [p, q] = now(r);
285
+ const dot = (q[0] - p[0]) * (b1[0] - b0[0]) + (q[1] - p[1]) * (b1[1] - b0[1]);
286
+ const lens = Math.hypot(q[0] - p[0], q[1] - p[1]) * Math.hypot(b1[0] - b0[0], b1[1] - b0[1]);
287
+ if (lens === 0 || Math.abs(dot) > AXIS_TAN * lens)
288
+ return false;
289
+ const other = keyOf(p) === keyOf(was) ? q : p;
290
+ const to = project(was);
291
+ const [wx, wy] = [was[0] - other[0], was[1] - other[1]];
292
+ const [tx, ty] = [to[0] - other[0], to[1] - other[1]];
293
+ if (tx * wx + ty * wy >= wx * wx + wy * wy)
294
+ return true;
295
+ return carried[r].length === 0 && tx * wx + ty * wy >= 0;
296
+ };
297
+ for (const e of ends[b]) {
298
+ if ((carriedBy.get(e.key) ?? []).some((r) => r !== b && alive[r]))
299
+ return false;
300
+ if (!(atKey.get(e.key) ?? []).every((r) => r === b || !alive[r] || follows(at(e.raw), r)))
301
+ return false;
302
+ }
303
+ for (const key of carried[b]) {
304
+ const was = moved.get(key);
305
+ if (!was)
306
+ continue;
307
+ if (!(atKey.get(key) ?? []).every((r) => r === b || !alive[r] || follows(was, r)))
308
+ return false;
309
+ }
310
+ return true;
311
+ };
312
+ let settled = false;
313
+ while (!settled) {
314
+ settled = true;
315
+ for (const a of order) {
316
+ if (!alive[a] || !axis[a])
317
+ continue;
318
+ for (;;) {
319
+ let merged = false;
320
+ for (const endIdx of [0, 1]) {
321
+ const [p0, p1] = now(a);
322
+ const end = endIdx === 1 ? p1 : p0;
323
+ const start = endIdx === 1 ? p0 : p1;
324
+ const len = Math.hypot(end[0] - start[0], end[1] - start[1]) || 1;
325
+ const ux = (end[0] - start[0]) / len;
326
+ const uy = (end[1] - start[1]) / len;
327
+ const along = (p) => (p[0] - end[0]) * ux + (p[1] - end[1]) * uy;
328
+ const aside = (p) => Math.abs((p[0] - end[0]) * uy - (p[1] - end[1]) * ux);
329
+ const onLine = (s) => round([end[0] + ux * s, end[1] + uy * s]);
330
+ let best = null;
331
+ for (const b of order) {
332
+ if (b === a || !alive[b] || !axis[b])
333
+ continue;
334
+ if (axis[b].frame !== axis[a].frame || axis[b].axis !== axis[a].axis)
335
+ continue;
336
+ const [q0, q1] = now(b);
337
+ const near = Math.min(along(q0), along(q1));
338
+ const far = Math.max(along(q0), along(q1));
339
+ // B continues A: it starts at A's end or within a lane ahead, no farther aside than a lane,
340
+ // and goes on from there. A run alongside A is a twin, not a continuation.
341
+ if (near < -POINT_EPS || near > lane || far <= near)
342
+ continue;
343
+ if (Math.max(aside(q0), aside(q1)) > lane)
344
+ continue;
345
+ if (!detachable(b, (p) => onLine(along(p))))
346
+ continue;
347
+ if (!best || near < best.near)
348
+ best = { b, near, far };
349
+ }
350
+ if (!best)
351
+ continue;
352
+ const [q0, q1] = now(best.b);
353
+ const nearQ = along(q0) <= along(q1) ? q0 : q1;
354
+ const farQ = nearQ === q0 ? q1 : q0;
355
+ const nearP = onLine(Math.max(0, best.near));
356
+ const farP = onLine(best.far);
357
+ const seg = (p, q) => [
358
+ p[0],
359
+ p[1],
360
+ q[0],
361
+ q[1],
362
+ ];
363
+ // Free floor between the two: from B's ends across to A's line, along the gap, and along B
364
+ // once pulled over.
365
+ if (!free(seg(nearQ, onLine(best.near))) || !free(seg(farQ, farP)))
366
+ continue;
367
+ if (best.near > 0 && !free(seg(end, nearP)))
368
+ continue;
369
+ if (!free(seg(nearP, farP)))
370
+ continue;
371
+ // B's own ends and everything on its chord move onto A's line, keeping their keys.
372
+ for (const key of [...ends[best.b].map((e) => e.key), ...carried[best.b]]) {
373
+ const was = moved.get(key) ?? ends[best.b].find((e) => e.key === key).raw;
374
+ moved.set(key, onLine(along(was)));
375
+ carried[a].push(key);
376
+ carriedBy.set(key, [...(carriedBy.get(key) ?? []).filter((r) => r !== best.b), a]);
377
+ }
378
+ // A now reaches B's far end: that end's key becomes A's, so later moves follow it.
379
+ const farEnd = ends[best.b].find((e) => keyOf(at(e.raw)) === keyOf(farP)) ??
380
+ ends[best.b][along(ends[best.b][0].raw) >= along(ends[best.b][1].raw) ? 0 : 1];
381
+ atKey.set(ends[a][endIdx].key, (atKey.get(ends[a][endIdx].key) ?? []).filter((r) => r !== a));
382
+ ends[a][endIdx] = farEnd;
383
+ atKey.set(farEnd.key, [...(atKey.get(farEnd.key) ?? []), a]);
384
+ moved.set(farEnd.key, farP);
385
+ alive[best.b] = false;
386
+ merged = true;
387
+ settled = false;
388
+ }
389
+ if (!merged)
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ return runs.flatMap((_, i) => (alive[i] ? [now(i)] : []));
395
+ }
396
+ /**
397
+ * Chains as segments, one per pair of neighboring points.
398
+ * @param chains The chains.
399
+ * @returns Their segments, in chain order.
400
+ */
401
+ export function chainSegments(chains) {
402
+ const segs = [];
403
+ for (const ch of chains)
404
+ for (let i = 0; i + 1 < ch.pts.length; i++)
405
+ segs.push([ch.pts[i][0], ch.pts[i][1], ch.pts[i + 1][0], ch.pts[i + 1][1]]);
406
+ return segs;
407
+ }
408
+ /**
409
+ * Reads segments as a graph; vertices merge by point key.
410
+ * @param segments The segments.
411
+ * @returns The graph.
412
+ */
413
+ export function graphOf(segments) {
414
+ const nodeId = new Map();
415
+ const nodes = [];
416
+ const adj = [];
417
+ const edges = [];
418
+ const getNode = (x, y) => {
419
+ const k = pointKey(x, y);
420
+ let id = nodeId.get(k);
421
+ if (id === undefined) {
422
+ id = nodes.length;
423
+ nodeId.set(k, id);
424
+ nodes.push([x, y]);
425
+ adj.push([]);
426
+ }
427
+ return id;
428
+ };
429
+ const link = (u, v) => {
430
+ if (u === v)
431
+ return;
432
+ const d = Math.hypot(nodes[u][0] - nodes[v][0], nodes[u][1] - nodes[v][1]);
433
+ const eid = edges.length;
434
+ edges.push([u, v]);
435
+ adj[u].push([v, d, eid]);
436
+ adj[v].push([u, d, eid]);
437
+ };
438
+ for (const [x0, y0, x1, y1] of segments)
439
+ link(getNode(x0, y0), getNode(x1, y1));
440
+ return { nodes, adj, edges, nodeId };
441
+ }
442
+ /**
443
+ * The connected pieces of a graph.
444
+ * @param graph The graph.
445
+ * @returns Each vertex's piece, by vertex id.
446
+ */
447
+ export function components(graph) {
448
+ const comp = new Int32Array(graph.nodes.length).fill(-1);
449
+ const stack = [];
450
+ let c = 0;
451
+ for (let s = 0; s < graph.nodes.length; s++) {
452
+ if (comp[s] >= 0)
453
+ continue;
454
+ stack.push(s);
455
+ comp[s] = c;
456
+ while (stack.length) {
457
+ const u = stack.pop();
458
+ for (const [v] of graph.adj[u])
459
+ if (comp[v] < 0) {
460
+ comp[v] = c;
461
+ stack.push(v);
462
+ }
463
+ }
464
+ c++;
465
+ }
466
+ return comp;
467
+ }
@@ -0,0 +1,50 @@
1
+ import { type Blockers } from './blockers.js';
2
+ import { type DoorSide, type Doorway } from './doorways.js';
3
+ import type { Ring, Segment } from './geom/types.js';
4
+ import { type PreparedWalls } from './geom/walls.js';
5
+ import type { RouteNetworkDestination, Unreached } from './graph-types.js';
6
+ import type { NetworkBuild } from './network-build.js';
7
+ import { type NetworkSizes } from './network-sizes.js';
8
+ import type { Opening } from './openings.js';
9
+ /** A layer as a route-network generator finds it. */
10
+ export interface NetworkSetup {
11
+ /** Network sizes. */
12
+ readonly cfg: NetworkSizes;
13
+ /** The layer's wall segments. */
14
+ readonly wallLines: readonly Segment[];
15
+ /** The doorways the walls leave. */
16
+ readonly doors: readonly Doorway[];
17
+ /** Doorway sides by the destination they open into, the entrances' openings included. */
18
+ readonly owners: ReadonlyMap<string, readonly DoorSide[]>;
19
+ /** Entrances with openings: reached through their doorways alone, never from a face. */
20
+ readonly opened: ReadonlySet<string>;
21
+ /** The floor pieces a route can serve. */
22
+ readonly floor: readonly Ring[];
23
+ /** Walkable floor pieces no destination or doorway serves, left out of the network. */
24
+ readonly unservedFloorPieces: number;
25
+ /** The closed sides of the doorways: only a doorway's middle stays open. */
26
+ readonly doorSides: readonly Segment[];
27
+ /** What a line or a dock may not cross; a doorway's closed sides block like walls. */
28
+ readonly blockers: Blockers;
29
+ /** The layer's destinations, by id. */
30
+ readonly destinationOf: ReadonlyMap<string, RouteNetworkDestination>;
31
+ /** Why each destination without a dock has none: the most informative reason noted. */
32
+ readonly failed: ReadonlyMap<string, Unreached>;
33
+ /** Records why a destination has no dock yet; see {@link NetworkBuild.note}. */
34
+ readonly note: NetworkBuild['note'];
35
+ }
36
+ /**
37
+ * Reads a layer for a route-network generator.
38
+ * @param walkableRings The layer's walkable polygon (outer rings + holes).
39
+ * @param destinations Footprints the network must reach.
40
+ * @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
41
+ * @param prepared The same walls already prepared, when the caller has them.
42
+ * @param drawn The rest of each destination's drawing; ways in may not cross it.
43
+ * @param openings The openings the entrances make: the ways in they own, through the walls.
44
+ * @returns The layer as the generators read it.
45
+ */
46
+ export declare function setUpNetwork(walkableRings: readonly Ring[], destinations: readonly RouteNetworkDestination[], walls: readonly (readonly number[])[], prepared: PreparedWalls | undefined, drawn: readonly {
47
+ readonly id: string;
48
+ readonly ring: Ring;
49
+ }[], openings: readonly Opening[]): NetworkSetup;
50
+ //# sourceMappingURL=network-setup.d.ts.map
@@ -0,0 +1,76 @@
1
+ // What every route-network generator reads of a layer before it lays a line: the doorways and who
2
+ // owns them, the openings the entrances make, the floor a route can serve, what a line may not
3
+ // cross, and where a destination the build could not serve is noted. Read once here, so every
4
+ // generator docks the same ways in.
5
+ import { blockersOf } from './blockers.js';
6
+ import { DOOR_WELD, doorOwners, doorwaySides, findDoorways, } from './doorways.js';
7
+ import { servedFloor } from './floor-grids.js';
8
+ import { prepareWalls } from './geom/walls.js';
9
+ import { NETWORK_SIZES } from './network-sizes.js';
10
+ /**
11
+ * Reads a layer for a route-network generator.
12
+ * @param walkableRings The layer's walkable polygon (outer rings + holes).
13
+ * @param destinations Footprints the network must reach.
14
+ * @param walls Raw wall polylines as flat `[x0, y0, …]` runs, for exact thin-wall blocking.
15
+ * @param prepared The same walls already prepared, when the caller has them.
16
+ * @param drawn The rest of each destination's drawing; ways in may not cross it.
17
+ * @param openings The openings the entrances make: the ways in they own, through the walls.
18
+ * @returns The layer as the generators read it.
19
+ */
20
+ export function setUpNetwork(walkableRings, destinations, walls, prepared, drawn, openings) {
21
+ const cfg = NETWORK_SIZES;
22
+ const prep = prepared ?? prepareWalls(walls);
23
+ const wallLines = prep.segments;
24
+ const doors = findDoorways(prep, destinations);
25
+ const owners = doorOwners(doors, destinations, wallLines, cfg);
26
+ // An entrance owns its openings and is reached through them alone. An opening on a doorway the
27
+ // walls already leave is that doorway; any other stands on the floor past the wall, and the dock
28
+ // from it crosses the wall inside the opening's span.
29
+ const opened = new Set();
30
+ const passes = new Map();
31
+ for (const { id, at, span, door } of openings) {
32
+ const same = doors.find((d) => Math.hypot(d.mid[0] - at[0], d.mid[1] - at[1]) <= DOOR_WELD);
33
+ const flip = same && same.nx * door.nx + same.ny * door.ny < 0 ? -1 : 1;
34
+ const sides = owners.get(id) ?? [];
35
+ sides.push(same
36
+ ? { door: same, nx: same.nx * flip, ny: same.ny * flip }
37
+ : { door, nx: door.nx, ny: door.ny });
38
+ owners.set(id, sides);
39
+ opened.add(id);
40
+ passes.set(id, [...(passes.get(id) ?? []), span]);
41
+ }
42
+ const { rings: floor, dropped: unservedFloorPieces } = servedFloor(walkableRings, destinations, owners);
43
+ // Only a doorway's middle stays open, so every pass draws lines through it there.
44
+ const doorSides = doorwaySides(doors, cfg);
45
+ // A doorway's closed sides block stubs and docks like walls.
46
+ const blockers = blockersOf(destinations, [...wallLines, ...doorSides], doors, drawn, new Set(owners.keys()), passes);
47
+ /** Why a destination has no stub yet: the furthest stage any of its ways in reached. */
48
+ const failed = new Map();
49
+ // Reasons from least to most informative; a destination keeps the most informative one.
50
+ const REACHED = [
51
+ 'no-way-in',
52
+ 'no-floor',
53
+ 'cut-off',
54
+ 'way-in-blocked',
55
+ 'detached',
56
+ ];
57
+ const note = (id, reason, facts) => {
58
+ const known = failed.get(id);
59
+ if (known === undefined || REACHED.indexOf(reason) > REACHED.indexOf(known.reason))
60
+ failed.set(id, { id, reason, ...facts });
61
+ };
62
+ return {
63
+ cfg,
64
+ wallLines,
65
+ doors,
66
+ owners,
67
+ opened,
68
+ floor,
69
+ unservedFloorPieces,
70
+ doorSides,
71
+ blockers,
72
+ destinationOf: new Map(destinations.map((d) => [d.id, d])),
73
+ failed,
74
+ note,
75
+ };
76
+ }