@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,29 @@
|
|
|
1
|
+
/** The sizes every route-network stage reads, all in meters. */
|
|
2
|
+
export interface NetworkSizes {
|
|
3
|
+
/** Mask cell size. */
|
|
4
|
+
readonly cs: number;
|
|
5
|
+
/** Comfortable clearance; tighter floor is passable at a steep cost. */
|
|
6
|
+
readonly clear: number;
|
|
7
|
+
/** Router cell size, coarser than {@link NetworkSizes.cs}. */
|
|
8
|
+
readonly pitch: number;
|
|
9
|
+
/** Cost of a turn, as a length. */
|
|
10
|
+
readonly turn: number;
|
|
11
|
+
/** A face is open when nothing stands within this distance in front of it. */
|
|
12
|
+
readonly open: number;
|
|
13
|
+
/** A stub's own length, with open floor ahead; across an aisle it runs to the middle. */
|
|
14
|
+
readonly stub: number;
|
|
15
|
+
}
|
|
16
|
+
/** The route-network sizes every stage reads, in meters (`scale.ts` scales the plan first). */
|
|
17
|
+
export declare const NETWORK_SIZES: NetworkSizes;
|
|
18
|
+
/**
|
|
19
|
+
* Least clearance the router passes (half a person's width): a doorway. Cells under `clear` cost ~9×, so a
|
|
20
|
+
* tight slot is taken only when no wider path exists.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MIN_PASS = 0.3;
|
|
23
|
+
/**
|
|
24
|
+
* Max ratio of a walk along the network to the walk across open floor. A tree has no way round, so
|
|
25
|
+
* above this ratio a shortcut link is cut in; and a row's stubs run out to an aisle's middle only while
|
|
26
|
+
* two neighbors along it stay within it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MAX_STRETCH = 2;
|
|
29
|
+
//# sourceMappingURL=network-sizes.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The sizes every route-network stage reads, in meters, and the one ratio the stubs and the shortcut
|
|
2
|
+
// pass share. `scale.ts` brings the plan to meters first.
|
|
3
|
+
/** The route-network sizes every stage reads, in meters (`scale.ts` scales the plan first). */
|
|
4
|
+
export const NETWORK_SIZES = {
|
|
5
|
+
cs: 0.25,
|
|
6
|
+
clear: 0.8,
|
|
7
|
+
pitch: 0.5,
|
|
8
|
+
turn: 3,
|
|
9
|
+
open: 1.5,
|
|
10
|
+
stub: 2,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Least clearance the router passes (half a person's width): a doorway. Cells under `clear` cost ~9×, so a
|
|
14
|
+
* tight slot is taken only when no wider path exists.
|
|
15
|
+
*/
|
|
16
|
+
export const MIN_PASS = 0.3;
|
|
17
|
+
/**
|
|
18
|
+
* Max ratio of a walk along the network to the walk across open floor. A tree has no way round, so
|
|
19
|
+
* above this ratio a shortcut link is cut in; and a row's stubs run out to an aisle's middle only while
|
|
20
|
+
* two neighbors along it stay within it.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_STRETCH = 2;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Segment, Vec2 } from './geom/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The route network under construction: nodes by centimeter key, segments between them.
|
|
4
|
+
*
|
|
5
|
+
* Ids are insertion order and every walk goes in id order; a cut segment's halves are added at the
|
|
6
|
+
* end. Passes break ties by this order, so it keeps the output deterministic.
|
|
7
|
+
*
|
|
8
|
+
* Invariants: no self-loops; no two live segments join the same nodes; every coordinate is on the
|
|
9
|
+
* centimeter grid, and shared ends share exact coordinates.
|
|
10
|
+
*
|
|
11
|
+
* Segments are indexed by uniform grid cells; a removed one stays in its cells, skipped, until
|
|
12
|
+
* reset.
|
|
13
|
+
*/
|
|
14
|
+
export declare class NetworkStore {
|
|
15
|
+
private readonly nodeId;
|
|
16
|
+
private readonly nodeX;
|
|
17
|
+
private readonly nodeY;
|
|
18
|
+
private readonly nodeKeys;
|
|
19
|
+
private readonly nodeSegs;
|
|
20
|
+
private readonly segA;
|
|
21
|
+
private readonly segB;
|
|
22
|
+
private readonly live;
|
|
23
|
+
private readonly edges;
|
|
24
|
+
private readonly cells;
|
|
25
|
+
private readonly visited;
|
|
26
|
+
private liveCount;
|
|
27
|
+
private visitStamp;
|
|
28
|
+
private minCx;
|
|
29
|
+
private minCy;
|
|
30
|
+
private maxCx;
|
|
31
|
+
private maxCy;
|
|
32
|
+
/** Live segments. */
|
|
33
|
+
get size(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Add a segment; its ends are snapped to the centimeter.
|
|
36
|
+
* @param x0 Start x.
|
|
37
|
+
* @param y0 Start y.
|
|
38
|
+
* @param x1 End x.
|
|
39
|
+
* @param y1 End y.
|
|
40
|
+
* @returns The segment id, or -1 when both ends share a key or the same two nodes are already
|
|
41
|
+
* joined.
|
|
42
|
+
*/
|
|
43
|
+
add(x0: number, y0: number, x1: number, y1: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* Remove a live segment; a node left without segments goes with it.
|
|
46
|
+
* @param id The segment.
|
|
47
|
+
*/
|
|
48
|
+
remove(id: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Cut a live segment at a point; its halves are added at the end, start half first. A point on
|
|
51
|
+
* one of its ends changes nothing.
|
|
52
|
+
* @param id The segment.
|
|
53
|
+
* @param x Cut x.
|
|
54
|
+
* @param y Cut y.
|
|
55
|
+
*/
|
|
56
|
+
splitAt(id: number, x: number, y: number): void;
|
|
57
|
+
/**
|
|
58
|
+
* The first live segment, in id order, that carries a point away from both its ends.
|
|
59
|
+
* @param x Point x.
|
|
60
|
+
* @param y Point y.
|
|
61
|
+
* @returns The segment id, or -1 when none does.
|
|
62
|
+
*/
|
|
63
|
+
segmentAt(x: number, y: number): number;
|
|
64
|
+
/**
|
|
65
|
+
* The nearest segment point a lawful way reaches from a point: segments come nearest first by the
|
|
66
|
+
* millimeter, ties in id order, so equidistant points come deterministically.
|
|
67
|
+
* @param x Point x.
|
|
68
|
+
* @param y Point y.
|
|
69
|
+
* @param lawful Whether the way from the point to a projection is lawful.
|
|
70
|
+
* @returns The segment, its point and the distance, or null when none is reachable.
|
|
71
|
+
*/
|
|
72
|
+
nearestLawful(x: number, y: number, lawful: (id: number, qx: number, qy: number) => boolean): {
|
|
73
|
+
id: number;
|
|
74
|
+
at: Vec2;
|
|
75
|
+
away: number;
|
|
76
|
+
} | null;
|
|
77
|
+
/**
|
|
78
|
+
* Visit live segments nearest first, by the distance to the point's projection rounded to the
|
|
79
|
+
* centimeter (where a caller splits); ties in id order.
|
|
80
|
+
* @param x Point x.
|
|
81
|
+
* @param y Point y.
|
|
82
|
+
* @param rank Maps a distance to the value compared (the distance or a coarser one).
|
|
83
|
+
* @param visit Gets the id, rounded projection and distance; returning true stops the walk.
|
|
84
|
+
*/
|
|
85
|
+
visitNearest(x: number, y: number, rank: (away: number) => number, visit: (id: number, qx: number, qy: number, away: number) => boolean): void;
|
|
86
|
+
/**
|
|
87
|
+
* The live segment nearest to a point, by rounded projection; lowest id among equals.
|
|
88
|
+
* @param x Point x.
|
|
89
|
+
* @param y Point y.
|
|
90
|
+
* @returns The segment, the rounded projection and the distance, or null for an empty store.
|
|
91
|
+
*/
|
|
92
|
+
nearest(x: number, y: number): {
|
|
93
|
+
id: number;
|
|
94
|
+
at: Vec2;
|
|
95
|
+
away: number;
|
|
96
|
+
} | null;
|
|
97
|
+
/**
|
|
98
|
+
* A segment's ends.
|
|
99
|
+
* @param id The segment.
|
|
100
|
+
* @returns A fresh `[x0, y0, x1, y1]`.
|
|
101
|
+
*/
|
|
102
|
+
coords(id: number): Segment;
|
|
103
|
+
/**
|
|
104
|
+
* Visit every live segment in id order.
|
|
105
|
+
* @param visit Called with the segment id and its ends.
|
|
106
|
+
*/
|
|
107
|
+
forEach(visit: (id: number, x0: number, y0: number, x1: number, y1: number) => void): void;
|
|
108
|
+
/**
|
|
109
|
+
* Visit the live segments in the cells a box overlaps, in id order: a superset of those whose
|
|
110
|
+
* bounds meet the box.
|
|
111
|
+
* @param x0 Box min x.
|
|
112
|
+
* @param y0 Box min y.
|
|
113
|
+
* @param x1 Box max x.
|
|
114
|
+
* @param y1 Box max y.
|
|
115
|
+
* @param visit Called with the segment id and its ends.
|
|
116
|
+
*/
|
|
117
|
+
forEachNear(x0: number, y0: number, x1: number, y1: number, visit: (id: number, x0: number, y0: number, x1: number, y1: number) => void): void;
|
|
118
|
+
/**
|
|
119
|
+
* The live segments.
|
|
120
|
+
* @returns Fresh tuples, in id order.
|
|
121
|
+
*/
|
|
122
|
+
toArray(): Segment[];
|
|
123
|
+
/**
|
|
124
|
+
* Drop everything and add the given segments in order; ids restart from 0.
|
|
125
|
+
* @param segments The new network.
|
|
126
|
+
*/
|
|
127
|
+
reset(segments: readonly Segment[]): void;
|
|
128
|
+
/**
|
|
129
|
+
* Make every meeting a node: segments that cross, end on another or run along one are cut there,
|
|
130
|
+
* and ends closer than `eps` become one.
|
|
131
|
+
* @param fixed Points that stay exactly where they are; near ends join them, and fixed points
|
|
132
|
+
* that near each other become the first of them.
|
|
133
|
+
* @param eps How close two ends or an end and a segment must be to meet.
|
|
134
|
+
* @returns Each fixed point that joined another, by its key, and where it went.
|
|
135
|
+
*/
|
|
136
|
+
node(fixed: readonly Vec2[], eps?: number): Map<string, Vec2>;
|
|
137
|
+
/**
|
|
138
|
+
* Whether a live segment ends at the point.
|
|
139
|
+
* @param x Point x.
|
|
140
|
+
* @param y Point y.
|
|
141
|
+
* @returns True when the point keys to a node.
|
|
142
|
+
*/
|
|
143
|
+
hasNode(x: number, y: number): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* The connected pieces of the network.
|
|
146
|
+
* @returns A label per live segment id: the smallest node index in its piece.
|
|
147
|
+
*/
|
|
148
|
+
components(): Map<number, number>;
|
|
149
|
+
/**
|
|
150
|
+
* Put a segment into every cell its bounds overlap.
|
|
151
|
+
* @param id The segment.
|
|
152
|
+
*/
|
|
153
|
+
private index;
|
|
154
|
+
/**
|
|
155
|
+
* Live segment ids indexed within a distance of a point, each once, in no particular order.
|
|
156
|
+
* @param x Point x.
|
|
157
|
+
* @param y Point y.
|
|
158
|
+
* @param radius How far around the point to look.
|
|
159
|
+
* @returns The ids.
|
|
160
|
+
*/
|
|
161
|
+
private idsNear;
|
|
162
|
+
/**
|
|
163
|
+
* Live segment ids indexed in the cells a box overlaps, each once, in no particular order.
|
|
164
|
+
* @param x0 Box min x.
|
|
165
|
+
* @param y0 Box min y.
|
|
166
|
+
* @param x1 Box max x.
|
|
167
|
+
* @param y1 Box max y.
|
|
168
|
+
* @returns The ids.
|
|
169
|
+
*/
|
|
170
|
+
private idsIn;
|
|
171
|
+
private createNode;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=network-store.d.ts.map
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { POINT_EPS, pointKey, snapCm } from './geom/point-key.js';
|
|
2
|
+
import { pointSegmentDistance, segmentParam } from './geom/primitives.js';
|
|
3
|
+
import { splitCrossings } from './geom/split-crossings.js';
|
|
4
|
+
/** Side of an index cell, in meters. */
|
|
5
|
+
const CELL = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Slack for the projection's centimeter rounding (up to 1 cm · √2 / 2) in the nearest-first bound.
|
|
8
|
+
*/
|
|
9
|
+
const ROUNDING = POINT_EPS;
|
|
10
|
+
/**
|
|
11
|
+
* Cell coordinates packed into one number; room for ±32768 cells either way.
|
|
12
|
+
* @param cx Cell x.
|
|
13
|
+
* @param cy Cell y.
|
|
14
|
+
* @returns The key.
|
|
15
|
+
*/
|
|
16
|
+
const cellKey = (cx, cy) => (cx + 32768) * 65536 + (cy + 32768);
|
|
17
|
+
/**
|
|
18
|
+
* The route network under construction: nodes by centimeter key, segments between them.
|
|
19
|
+
*
|
|
20
|
+
* Ids are insertion order and every walk goes in id order; a cut segment's halves are added at the
|
|
21
|
+
* end. Passes break ties by this order, so it keeps the output deterministic.
|
|
22
|
+
*
|
|
23
|
+
* Invariants: no self-loops; no two live segments join the same nodes; every coordinate is on the
|
|
24
|
+
* centimeter grid, and shared ends share exact coordinates.
|
|
25
|
+
*
|
|
26
|
+
* Segments are indexed by uniform grid cells; a removed one stays in its cells, skipped, until
|
|
27
|
+
* reset.
|
|
28
|
+
*/
|
|
29
|
+
export class NetworkStore {
|
|
30
|
+
nodeId = new Map();
|
|
31
|
+
nodeX = [];
|
|
32
|
+
nodeY = [];
|
|
33
|
+
nodeKeys = [];
|
|
34
|
+
nodeSegs = [];
|
|
35
|
+
segA = [];
|
|
36
|
+
segB = [];
|
|
37
|
+
live = [];
|
|
38
|
+
edges = new Set();
|
|
39
|
+
cells = new Map();
|
|
40
|
+
visited = [];
|
|
41
|
+
liveCount = 0;
|
|
42
|
+
visitStamp = 0;
|
|
43
|
+
minCx = Infinity;
|
|
44
|
+
minCy = Infinity;
|
|
45
|
+
maxCx = -Infinity;
|
|
46
|
+
maxCy = -Infinity;
|
|
47
|
+
/** Live segments. */
|
|
48
|
+
get size() {
|
|
49
|
+
return this.liveCount;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Add a segment; its ends are snapped to the centimeter.
|
|
53
|
+
* @param x0 Start x.
|
|
54
|
+
* @param y0 Start y.
|
|
55
|
+
* @param x1 End x.
|
|
56
|
+
* @param y1 End y.
|
|
57
|
+
* @returns The segment id, or -1 when both ends share a key or the same two nodes are already
|
|
58
|
+
* joined.
|
|
59
|
+
*/
|
|
60
|
+
add(x0, y0, x1, y1) {
|
|
61
|
+
const ka = pointKey(x0, y0);
|
|
62
|
+
const kb = pointKey(x1, y1);
|
|
63
|
+
if (ka === kb)
|
|
64
|
+
return -1;
|
|
65
|
+
const known = this.nodeId.get(ka);
|
|
66
|
+
const other = this.nodeId.get(kb);
|
|
67
|
+
if (known !== undefined && other !== undefined && this.edges.has(edgeKey(known, other)))
|
|
68
|
+
return -1;
|
|
69
|
+
const a = known ?? this.createNode(ka, x0, y0);
|
|
70
|
+
const b = other ?? this.createNode(kb, x1, y1);
|
|
71
|
+
const id = this.live.length;
|
|
72
|
+
this.segA.push(a);
|
|
73
|
+
this.segB.push(b);
|
|
74
|
+
this.live.push(true);
|
|
75
|
+
this.edges.add(edgeKey(a, b));
|
|
76
|
+
this.nodeSegs[a].add(id);
|
|
77
|
+
this.nodeSegs[b].add(id);
|
|
78
|
+
this.visited.push(0);
|
|
79
|
+
this.liveCount++;
|
|
80
|
+
this.index(id);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Remove a live segment; a node left without segments goes with it.
|
|
85
|
+
* @param id The segment.
|
|
86
|
+
*/
|
|
87
|
+
remove(id) {
|
|
88
|
+
if (!this.live[id])
|
|
89
|
+
return;
|
|
90
|
+
const a = this.segA[id];
|
|
91
|
+
const b = this.segB[id];
|
|
92
|
+
this.live[id] = false;
|
|
93
|
+
this.edges.delete(edgeKey(a, b));
|
|
94
|
+
this.liveCount--;
|
|
95
|
+
for (const node of [a, b]) {
|
|
96
|
+
this.nodeSegs[node].delete(id);
|
|
97
|
+
if (!this.nodeSegs[node].size)
|
|
98
|
+
this.nodeId.delete(this.nodeKeys[node]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Cut a live segment at a point; its halves are added at the end, start half first. A point on
|
|
103
|
+
* one of its ends changes nothing.
|
|
104
|
+
* @param id The segment.
|
|
105
|
+
* @param x Cut x.
|
|
106
|
+
* @param y Cut y.
|
|
107
|
+
*/
|
|
108
|
+
splitAt(id, x, y) {
|
|
109
|
+
if (!this.live[id])
|
|
110
|
+
return;
|
|
111
|
+
const [ax, ay, bx, by] = this.coords(id);
|
|
112
|
+
const key = pointKey(x, y);
|
|
113
|
+
if (key === pointKey(ax, ay) || key === pointKey(bx, by))
|
|
114
|
+
return;
|
|
115
|
+
this.remove(id);
|
|
116
|
+
this.add(ax, ay, x, y);
|
|
117
|
+
this.add(x, y, bx, by);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The first live segment, in id order, that carries a point away from both its ends.
|
|
121
|
+
* @param x Point x.
|
|
122
|
+
* @param y Point y.
|
|
123
|
+
* @returns The segment id, or -1 when none does.
|
|
124
|
+
*/
|
|
125
|
+
segmentAt(x, y) {
|
|
126
|
+
const key = pointKey(x, y);
|
|
127
|
+
let found = -1;
|
|
128
|
+
for (const id of this.idsNear(x, y, POINT_EPS)) {
|
|
129
|
+
if (found >= 0 && id > found)
|
|
130
|
+
continue;
|
|
131
|
+
const a = this.segA[id];
|
|
132
|
+
const b = this.segB[id];
|
|
133
|
+
if (key === this.nodeKeys[a] || key === this.nodeKeys[b])
|
|
134
|
+
continue;
|
|
135
|
+
const distance = pointSegmentDistance(x, y, this.nodeX[a], this.nodeY[a], this.nodeX[b], this.nodeY[b]);
|
|
136
|
+
if (distance < POINT_EPS)
|
|
137
|
+
found = id;
|
|
138
|
+
}
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The nearest segment point a lawful way reaches from a point: segments come nearest first by the
|
|
143
|
+
* millimeter, ties in id order, so equidistant points come deterministically.
|
|
144
|
+
* @param x Point x.
|
|
145
|
+
* @param y Point y.
|
|
146
|
+
* @param lawful Whether the way from the point to a projection is lawful.
|
|
147
|
+
* @returns The segment, its point and the distance, or null when none is reachable.
|
|
148
|
+
*/
|
|
149
|
+
nearestLawful(x, y, lawful) {
|
|
150
|
+
let found = null;
|
|
151
|
+
this.visitNearest(x, y, (away) => Math.round(away * 1000), (id, qx, qy, away) => {
|
|
152
|
+
if (!lawful(id, qx, qy))
|
|
153
|
+
return false;
|
|
154
|
+
found = { id, at: [qx, qy], away };
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Visit live segments nearest first, by the distance to the point's projection rounded to the
|
|
161
|
+
* centimeter (where a caller splits); ties in id order.
|
|
162
|
+
* @param x Point x.
|
|
163
|
+
* @param y Point y.
|
|
164
|
+
* @param rank Maps a distance to the value compared (the distance or a coarser one).
|
|
165
|
+
* @param visit Gets the id, rounded projection and distance; returning true stops the walk.
|
|
166
|
+
*/
|
|
167
|
+
visitNearest(x, y, rank, visit) {
|
|
168
|
+
if (!this.liveCount)
|
|
169
|
+
return;
|
|
170
|
+
const stamp = ++this.visitStamp;
|
|
171
|
+
const cx = Math.floor(x / CELL);
|
|
172
|
+
const cy = Math.floor(y / CELL);
|
|
173
|
+
const pending = [];
|
|
174
|
+
const take = (key) => {
|
|
175
|
+
for (const id of this.cells.get(key) ?? []) {
|
|
176
|
+
if (!this.live[id] || this.visited[id] === stamp)
|
|
177
|
+
continue;
|
|
178
|
+
this.visited[id] = stamp;
|
|
179
|
+
const a = this.segA[id];
|
|
180
|
+
const b = this.segB[id];
|
|
181
|
+
const x0 = this.nodeX[a];
|
|
182
|
+
const y0 = this.nodeY[a];
|
|
183
|
+
const x1 = this.nodeX[b];
|
|
184
|
+
const y1 = this.nodeY[b];
|
|
185
|
+
const t = segmentParam(x, y, x0, y0, x1, y1);
|
|
186
|
+
const qx = +(x0 + t * (x1 - x0)).toFixed(2);
|
|
187
|
+
const qy = +(y0 + t * (y1 - y0)).toFixed(2);
|
|
188
|
+
const away = Math.hypot(qx - x, qy - y);
|
|
189
|
+
pending.push({ id, qx, qy, away, rank: rank(away) });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
for (let r = 0;; r++) {
|
|
193
|
+
// The ring of cells r steps out, clipped to the cells anything was ever indexed in.
|
|
194
|
+
const x0 = Math.max(cx - r, this.minCx);
|
|
195
|
+
const x1 = Math.min(cx + r, this.maxCx);
|
|
196
|
+
for (const gy of [cy - r, cy + r]) {
|
|
197
|
+
if (gy < this.minCy || gy > this.maxCy || (r === 0 && gy !== cy))
|
|
198
|
+
continue;
|
|
199
|
+
for (let gx = x0; gx <= x1; gx++)
|
|
200
|
+
take(cellKey(gx, gy));
|
|
201
|
+
if (r === 0)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
for (const gx of r ? [cx - r, cx + r] : []) {
|
|
205
|
+
if (gx < this.minCx || gx > this.maxCx)
|
|
206
|
+
continue;
|
|
207
|
+
for (let gy = Math.max(cy - r + 1, this.minCy); gy <= Math.min(cy + r - 1, this.maxCy); gy++)
|
|
208
|
+
take(cellKey(gx, gy));
|
|
209
|
+
}
|
|
210
|
+
const covered = cx - r <= this.minCx &&
|
|
211
|
+
cx + r >= this.maxCx &&
|
|
212
|
+
cy - r <= this.minCy &&
|
|
213
|
+
cy + r >= this.maxCy;
|
|
214
|
+
// Every segment not met yet lies wholly outside the square read so far.
|
|
215
|
+
const reach = covered
|
|
216
|
+
? Infinity
|
|
217
|
+
: Math.min(x - (cx - r) * CELL, (cx + r + 1) * CELL - x, y - (cy - r) * CELL, (cy + r + 1) * CELL - y) - ROUNDING;
|
|
218
|
+
const limit = rank(reach);
|
|
219
|
+
pending.sort((p, q) => q.rank - p.rank || q.id - p.id);
|
|
220
|
+
while (pending.length && pending[pending.length - 1].rank < limit) {
|
|
221
|
+
const next = pending.pop();
|
|
222
|
+
if (visit(next.id, next.qx, next.qy, next.away))
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (covered)
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The live segment nearest to a point, by rounded projection; lowest id among equals.
|
|
231
|
+
* @param x Point x.
|
|
232
|
+
* @param y Point y.
|
|
233
|
+
* @returns The segment, the rounded projection and the distance, or null for an empty store.
|
|
234
|
+
*/
|
|
235
|
+
nearest(x, y) {
|
|
236
|
+
let found = null;
|
|
237
|
+
this.visitNearest(x, y, (away) => away, (id, qx, qy, away) => {
|
|
238
|
+
found = { id, at: [qx, qy], away };
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A segment's ends.
|
|
245
|
+
* @param id The segment.
|
|
246
|
+
* @returns A fresh `[x0, y0, x1, y1]`.
|
|
247
|
+
*/
|
|
248
|
+
coords(id) {
|
|
249
|
+
const a = this.segA[id];
|
|
250
|
+
const b = this.segB[id];
|
|
251
|
+
return [this.nodeX[a], this.nodeY[a], this.nodeX[b], this.nodeY[b]];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Visit every live segment in id order.
|
|
255
|
+
* @param visit Called with the segment id and its ends.
|
|
256
|
+
*/
|
|
257
|
+
forEach(visit) {
|
|
258
|
+
for (let id = 0; id < this.live.length; id++) {
|
|
259
|
+
if (!this.live[id])
|
|
260
|
+
continue;
|
|
261
|
+
const a = this.segA[id];
|
|
262
|
+
const b = this.segB[id];
|
|
263
|
+
visit(id, this.nodeX[a], this.nodeY[a], this.nodeX[b], this.nodeY[b]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Visit the live segments in the cells a box overlaps, in id order: a superset of those whose
|
|
268
|
+
* bounds meet the box.
|
|
269
|
+
* @param x0 Box min x.
|
|
270
|
+
* @param y0 Box min y.
|
|
271
|
+
* @param x1 Box max x.
|
|
272
|
+
* @param y1 Box max y.
|
|
273
|
+
* @param visit Called with the segment id and its ends.
|
|
274
|
+
*/
|
|
275
|
+
forEachNear(x0, y0, x1, y1, visit) {
|
|
276
|
+
for (const id of [...this.idsIn(x0, y0, x1, y1)].sort((a, b) => a - b)) {
|
|
277
|
+
const a = this.segA[id];
|
|
278
|
+
const b = this.segB[id];
|
|
279
|
+
visit(id, this.nodeX[a], this.nodeY[a], this.nodeX[b], this.nodeY[b]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The live segments.
|
|
284
|
+
* @returns Fresh tuples, in id order.
|
|
285
|
+
*/
|
|
286
|
+
toArray() {
|
|
287
|
+
const out = [];
|
|
288
|
+
this.forEach((_, x0, y0, x1, y1) => out.push([x0, y0, x1, y1]));
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Drop everything and add the given segments in order; ids restart from 0.
|
|
293
|
+
* @param segments The new network.
|
|
294
|
+
*/
|
|
295
|
+
reset(segments) {
|
|
296
|
+
this.nodeId.clear();
|
|
297
|
+
this.nodeX.length = 0;
|
|
298
|
+
this.nodeY.length = 0;
|
|
299
|
+
this.nodeKeys.length = 0;
|
|
300
|
+
this.nodeSegs.length = 0;
|
|
301
|
+
this.segA.length = 0;
|
|
302
|
+
this.segB.length = 0;
|
|
303
|
+
this.live.length = 0;
|
|
304
|
+
this.edges.clear();
|
|
305
|
+
this.cells.clear();
|
|
306
|
+
this.visited.length = 0;
|
|
307
|
+
this.liveCount = 0;
|
|
308
|
+
this.minCx = Infinity;
|
|
309
|
+
this.minCy = Infinity;
|
|
310
|
+
this.maxCx = -Infinity;
|
|
311
|
+
this.maxCy = -Infinity;
|
|
312
|
+
for (const [x0, y0, x1, y1] of segments)
|
|
313
|
+
this.add(x0, y0, x1, y1);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Make every meeting a node: segments that cross, end on another or run along one are cut there,
|
|
317
|
+
* and ends closer than `eps` become one.
|
|
318
|
+
* @param fixed Points that stay exactly where they are; near ends join them, and fixed points
|
|
319
|
+
* that near each other become the first of them.
|
|
320
|
+
* @param eps How close two ends or an end and a segment must be to meet.
|
|
321
|
+
* @returns Each fixed point that joined another, by its key, and where it went.
|
|
322
|
+
*/
|
|
323
|
+
node(fixed, eps = POINT_EPS) {
|
|
324
|
+
const moved = new Map();
|
|
325
|
+
this.reset(splitCrossings(this.toArray(), eps, fixed, moved));
|
|
326
|
+
return moved;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Whether a live segment ends at the point.
|
|
330
|
+
* @param x Point x.
|
|
331
|
+
* @param y Point y.
|
|
332
|
+
* @returns True when the point keys to a node.
|
|
333
|
+
*/
|
|
334
|
+
hasNode(x, y) {
|
|
335
|
+
return this.nodeId.has(pointKey(x, y));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The connected pieces of the network.
|
|
339
|
+
* @returns A label per live segment id: the smallest node index in its piece.
|
|
340
|
+
*/
|
|
341
|
+
components() {
|
|
342
|
+
const parent = this.nodeX.map((_, i) => i);
|
|
343
|
+
const find = (i) => {
|
|
344
|
+
while (parent[i] !== i) {
|
|
345
|
+
parent[i] = parent[parent[i]];
|
|
346
|
+
i = parent[i];
|
|
347
|
+
}
|
|
348
|
+
return i;
|
|
349
|
+
};
|
|
350
|
+
this.forEach((id) => {
|
|
351
|
+
const ra = find(this.segA[id]);
|
|
352
|
+
const rb = find(this.segB[id]);
|
|
353
|
+
if (ra !== rb)
|
|
354
|
+
parent[Math.max(ra, rb)] = Math.min(ra, rb);
|
|
355
|
+
});
|
|
356
|
+
const labels = new Map();
|
|
357
|
+
this.forEach((id) => labels.set(id, find(this.segA[id])));
|
|
358
|
+
return labels;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Put a segment into every cell its bounds overlap.
|
|
362
|
+
* @param id The segment.
|
|
363
|
+
*/
|
|
364
|
+
index(id) {
|
|
365
|
+
const [x0, y0, x1, y1] = this.coords(id);
|
|
366
|
+
const lx = Math.floor(Math.min(x0, x1) / CELL);
|
|
367
|
+
const hx = Math.floor(Math.max(x0, x1) / CELL);
|
|
368
|
+
const ly = Math.floor(Math.min(y0, y1) / CELL);
|
|
369
|
+
const hy = Math.floor(Math.max(y0, y1) / CELL);
|
|
370
|
+
for (let gx = lx; gx <= hx; gx++)
|
|
371
|
+
for (let gy = ly; gy <= hy; gy++) {
|
|
372
|
+
const key = cellKey(gx, gy);
|
|
373
|
+
const list = this.cells.get(key);
|
|
374
|
+
if (list)
|
|
375
|
+
list.push(id);
|
|
376
|
+
else
|
|
377
|
+
this.cells.set(key, [id]);
|
|
378
|
+
}
|
|
379
|
+
this.minCx = Math.min(this.minCx, lx);
|
|
380
|
+
this.minCy = Math.min(this.minCy, ly);
|
|
381
|
+
this.maxCx = Math.max(this.maxCx, hx);
|
|
382
|
+
this.maxCy = Math.max(this.maxCy, hy);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Live segment ids indexed within a distance of a point, each once, in no particular order.
|
|
386
|
+
* @param x Point x.
|
|
387
|
+
* @param y Point y.
|
|
388
|
+
* @param radius How far around the point to look.
|
|
389
|
+
* @returns The ids.
|
|
390
|
+
*/
|
|
391
|
+
idsNear(x, y, radius) {
|
|
392
|
+
return this.idsIn(x - radius, y - radius, x + radius, y + radius);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Live segment ids indexed in the cells a box overlaps, each once, in no particular order.
|
|
396
|
+
* @param x0 Box min x.
|
|
397
|
+
* @param y0 Box min y.
|
|
398
|
+
* @param x1 Box max x.
|
|
399
|
+
* @param y1 Box max y.
|
|
400
|
+
* @returns The ids.
|
|
401
|
+
*/
|
|
402
|
+
idsIn(x0, y0, x1, y1) {
|
|
403
|
+
// Own set, not visit stamps, so it works inside a `visitNearest` walk.
|
|
404
|
+
const ids = new Set();
|
|
405
|
+
for (let gx = Math.floor(x0 / CELL); gx <= Math.floor(x1 / CELL); gx++)
|
|
406
|
+
for (let gy = Math.floor(y0 / CELL); gy <= Math.floor(y1 / CELL); gy++)
|
|
407
|
+
for (const id of this.cells.get(cellKey(gx, gy)) ?? [])
|
|
408
|
+
if (this.live[id])
|
|
409
|
+
ids.add(id);
|
|
410
|
+
return ids;
|
|
411
|
+
}
|
|
412
|
+
createNode(key, x, y) {
|
|
413
|
+
const node = this.nodeX.length;
|
|
414
|
+
this.nodeId.set(key, node);
|
|
415
|
+
this.nodeX.push(snapCm(x));
|
|
416
|
+
this.nodeY.push(snapCm(y));
|
|
417
|
+
this.nodeKeys.push(key);
|
|
418
|
+
this.nodeSegs.push(new Set());
|
|
419
|
+
return node;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The key of an undirected edge between two nodes.
|
|
424
|
+
* @param a One node.
|
|
425
|
+
* @param b The other.
|
|
426
|
+
* @returns The key.
|
|
427
|
+
*/
|
|
428
|
+
function edgeKey(a, b) {
|
|
429
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
430
|
+
}
|