@woosh/meep-engine 2.163.7 → 2.163.9
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/package.json +1 -1
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts +23 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts.map +1 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts +2 -2
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +120 -179
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts +9 -10
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +12 -13
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +45 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.js +84 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js +53 -78
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts +16 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.js +49 -0
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts +2 -2
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.js +46 -46
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.d.ts.map +1 -1
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.js +6 -28
- package/src/engine/navigation/mesh/NavigationMesh.d.ts +6 -0
- package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
- package/src/engine/navigation/mesh/NavigationMesh.js +145 -234
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +229 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts +17 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts.map +1 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.js +682 -0
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +354 -138
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts +21 -0
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts.map +1 -0
- package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js +133 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { Uint32Heap4 } from "../../../core/collection/heap/Uint32Heap4.js";
|
|
2
|
+
import { line_segment_intersection_fraction_2d } from "../../../core/geom/2d/line/line_segment_intersection_fraction_2d.js";
|
|
3
|
+
import { triangle2d_compute_area } from "../../../core/geom/2d/triangle2d_compute_area.js";
|
|
4
|
+
import { NULL_POINTER } from "../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
|
|
5
|
+
import { v2_distance } from "../../../core/geom/vec2/v2_distance.js";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Polyanya: exact any-angle shortest path on a triangle navmesh.
|
|
9
|
+
*
|
|
10
|
+
* Cui, Harabor, Grastien -- "Compromise-free Pathfinding on a Navigation Mesh" (IJCAI 2017).
|
|
11
|
+
*
|
|
12
|
+
* Search nodes are (root, interval) pairs: an interval [i0, i1] on a mesh edge, seen from a `root`
|
|
13
|
+
* point such that the start->root sub-path is already taut/optimal and the whole interval is visible
|
|
14
|
+
* from root. Expanding a node projects the visibility cone (root through the interval) into the
|
|
15
|
+
* triangle on the far side of the edge and clips it against that triangle's two far edges. Parts still
|
|
16
|
+
* visible from root keep `root` (observable); parts hidden behind a boundary CORNER at an interval
|
|
17
|
+
* endpoint are reached by turning at that corner, which becomes the new root (non-observable). The
|
|
18
|
+
* result is the exact shortest path, turning only at obstacle corners.
|
|
19
|
+
*
|
|
20
|
+
* 3D / intrinsic: the search is planar, but it does NOT assume any global "up" -- it follows the surface
|
|
21
|
+
* by UNFOLDING. Each node carries the 2D positions of its entry edge in a frame accumulated along its
|
|
22
|
+
* own corridor; expanding a node flattens the next triangle into that frame by placing the apex from the
|
|
23
|
+
* two 3D edge lengths |edge-vertex .. apex| (an isometry of the triangle), on the side fixed by the
|
|
24
|
+
* face's winding. So distances in the frame are true geodesic distances and the cost `g` is exact on a
|
|
25
|
+
* curved/folded surface, not just a planar one. Turning corners are detected from the 3D incident-angle
|
|
26
|
+
* sum (also intrinsic). The goal has no fixed position across frames, so the A* heuristic is the exact
|
|
27
|
+
* shortest root->interval->goal measured with straight 3D chords (a lower bound on the geodesic, hence
|
|
28
|
+
* admissible); the terminal node's *exact* cost unfolds the goal into the node's frame instead.
|
|
29
|
+
*
|
|
30
|
+
* The output path FOLLOWS THE SURFACE: it is subdivided at every face-boundary edge the geodesic
|
|
31
|
+
* crosses, so each emitted segment lies within a single triangle -- it never flies over a convex crease
|
|
32
|
+
* nor tunnels through a concave fold. Turning corners are mesh vertices (exact positions); each edge
|
|
33
|
+
* crossing is the exact line-vs-edge intersection lifted to 3D by lerping the edge's two endpoints; the
|
|
34
|
+
* start/goal are the given 3D points. Collinear runs (a flat stretch with no real bend) are collapsed to
|
|
35
|
+
* their endpoints, so on a planar mesh the output is just the minimal corner polyline -- the subdivision
|
|
36
|
+
* only adds points where the surface actually folds. Wired into NavigationMesh.find_path.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const EPS = 1e-9; // area-sign slack (cone inside/outside tests)
|
|
40
|
+
const DEDUP_EPS = 1e-7; // merge consecutive output points equal to within f32 reconstruction error
|
|
41
|
+
const COLLINEAR_EPS = 1e-4; // drop a path point whose perpendicular offset from its neighbours / span is below this
|
|
42
|
+
|
|
43
|
+
// Parameter-space tolerance for edge fractions, interval coverage and vertex snapping. Vertices are
|
|
44
|
+
// stored as float32, so a point that is geometrically on an edge endpoint or already-covered carries
|
|
45
|
+
// ~coord*2^-23 quantization noise in its edge fraction -- far above 1e-9. Tolerating less re-covers the
|
|
46
|
+
// same edge stretches as slivers and the wavefront floods without converging. 1e-4 covers coordinates
|
|
47
|
+
// into the thousands while staying far below any real feature size, and never moves a waypoint (corners
|
|
48
|
+
// are exact vertex positions).
|
|
49
|
+
const PEPS = 1e-4;
|
|
50
|
+
|
|
51
|
+
// ---- 2D primitives in the unfolded frame -----------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const _reflect = new Float64Array(2);
|
|
54
|
+
|
|
55
|
+
/** Reflect (px,py) across the line through (ax,ay),(bx,by) into {@link _reflect}. */
|
|
56
|
+
function reflect(px, py, ax, ay, bx, by) {
|
|
57
|
+
const abx = bx - ax, aby = by - ay;
|
|
58
|
+
const len2 = abx * abx + aby * aby;
|
|
59
|
+
if (len2 < 1e-18) { _reflect[0] = px; _reflect[1] = py; return; }
|
|
60
|
+
const t = ((px - ax) * abx + (py - ay) * aby) / len2;
|
|
61
|
+
_reflect[0] = 2 * (ax + t * abx) - px;
|
|
62
|
+
_reflect[1] = 2 * (ay + t * aby) - py;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Exact root->interval->goal distance in the unfolded frame (the terminal node's true final cost). */
|
|
66
|
+
function touch_distance(rx, ry, i0x, i0y, i1x, i1y, gx, gy) {
|
|
67
|
+
let tx = gx, ty = gy;
|
|
68
|
+
if (triangle2d_compute_area(i0x, i0y, i1x, i1y, rx, ry) * triangle2d_compute_area(i0x, i0y, i1x, i1y, gx, gy) > 0) {
|
|
69
|
+
reflect(gx, gy, i0x, i0y, i1x, i1y);
|
|
70
|
+
tx = _reflect[0]; ty = _reflect[1];
|
|
71
|
+
}
|
|
72
|
+
const t = line_segment_intersection_fraction_2d(rx, ry, tx, ty, i0x, i0y, i1x, i1y);
|
|
73
|
+
if (t >= -PEPS && t <= 1 + PEPS) { // -1 (no crossing) falls below -PEPS, so the range test rejects it
|
|
74
|
+
return v2_distance(rx, ry, tx, ty);
|
|
75
|
+
}
|
|
76
|
+
return Math.min(
|
|
77
|
+
v2_distance(rx, ry, i0x, i0y) + v2_distance(i0x, i0y, gx, gy),
|
|
78
|
+
v2_distance(rx, ry, i1x, i1y) + v2_distance(i1x, i1y, gx, gy)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Shortest root->interval->goal measured with straight 3D chords (admissible heuristic: it is the
|
|
84
|
+
* 3D-space unfolding of the two segments across the interval's line, always <= the on-surface geodesic).
|
|
85
|
+
*/
|
|
86
|
+
function touch_distance_3d(rx, ry, rz, i0x, i0y, i0z, i1x, i1y, i1z, gx, gy, gz) {
|
|
87
|
+
const dx = i1x - i0x, dy = i1y - i0y, dz = i1z - i0z;
|
|
88
|
+
const dlen2 = dx * dx + dy * dy + dz * dz;
|
|
89
|
+
const viaI0 = Math.hypot(rx - i0x, ry - i0y, rz - i0z) + Math.hypot(gx - i0x, gy - i0y, gz - i0z);
|
|
90
|
+
if (dlen2 < 1e-18) return viaI0;
|
|
91
|
+
|
|
92
|
+
const dlen = Math.sqrt(dlen2), ux = dx / dlen, uy = dy / dlen, uz = dz / dlen;
|
|
93
|
+
const rt = (rx - i0x) * ux + (ry - i0y) * uy + (rz - i0z) * uz;
|
|
94
|
+
const rperp = Math.hypot(rx - i0x - rt * ux, ry - i0y - rt * uy, rz - i0z - rt * uz);
|
|
95
|
+
const gt = (gx - i0x) * ux + (gy - i0y) * uy + (gz - i0z) * uz;
|
|
96
|
+
const gperp = Math.hypot(gx - i0x - gt * ux, gy - i0y - gt * uy, gz - i0z - gt * uz);
|
|
97
|
+
|
|
98
|
+
const denom = rperp + gperp;
|
|
99
|
+
const xs = denom < 1e-12 ? rt : rt + (gt - rt) * rperp / denom; // edge crossing of the unfolded line
|
|
100
|
+
if (xs >= 0 && xs <= dlen) return Math.hypot(gt - rt, denom);
|
|
101
|
+
|
|
102
|
+
const viaI1 = Math.hypot(rx - i1x, ry - i1y, rz - i1z) + Math.hypot(gx - i1x, gy - i1y, gz - i1z);
|
|
103
|
+
return Math.min(viaI0, viaI1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- mesh helpers ----------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const _va = new Float64Array(3), _vb = new Float64Array(3), _vc = new Float64Array(3);
|
|
109
|
+
const _qa = new Float64Array(3), _qb = new Float64Array(3); // distance reads, kept off _va/_vb/_vc
|
|
110
|
+
|
|
111
|
+
const _tri = [0, 0, 0];
|
|
112
|
+
function triangle_vertices(topology, face) {
|
|
113
|
+
const l0 = topology.face_read_loop(face);
|
|
114
|
+
const l1 = topology.loop_read_next(l0);
|
|
115
|
+
const l2 = topology.loop_read_next(l1);
|
|
116
|
+
_tri[0] = topology.loop_read_vertex(l0);
|
|
117
|
+
_tri[1] = topology.loop_read_vertex(l1);
|
|
118
|
+
_tri[2] = topology.loop_read_vertex(l2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 3D distance between two vertices. */
|
|
122
|
+
function vid_distance_3d(topology, va, vb) {
|
|
123
|
+
topology.vertex_read_coordinate(_qa, 0, va);
|
|
124
|
+
topology.vertex_read_coordinate(_qb, 0, vb);
|
|
125
|
+
return Math.hypot(_qa[0] - _qb[0], _qa[1] - _qb[1], _qa[2] - _qb[2]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The face across edge (va,vb) other than `face`, or NULL_POINTER if that edge is a boundary. */
|
|
129
|
+
function neighbour_across(topology, face, va, vb) {
|
|
130
|
+
let loop = topology.face_read_loop(face);
|
|
131
|
+
for (let i = 0; i < 3; i++) {
|
|
132
|
+
const a = topology.loop_read_vertex(loop);
|
|
133
|
+
const b = topology.loop_read_vertex(topology.loop_read_next(loop));
|
|
134
|
+
if ((a === va && b === vb) || (a === vb && b === va)) {
|
|
135
|
+
let radial = topology.loop_read_radial_next(loop);
|
|
136
|
+
while (radial !== loop) {
|
|
137
|
+
const f = topology.loop_read_face(radial);
|
|
138
|
+
if (f !== face) return f;
|
|
139
|
+
radial = topology.loop_read_radial_next(radial);
|
|
140
|
+
}
|
|
141
|
+
return NULL_POINTER;
|
|
142
|
+
}
|
|
143
|
+
loop = topology.loop_read_next(loop);
|
|
144
|
+
}
|
|
145
|
+
return NULL_POINTER;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Unfold the apex of `face` into the 2D frame, given the entry edge (e0v,e1v) already placed at
|
|
150
|
+
* (e0x,e0y)-(e1x,e1y). The apex sits at its true 3D distances from the two edge vertices (an isometry of
|
|
151
|
+
* the triangle); the side is fixed by the face winding -- left of the edge as the loop traverses it, so
|
|
152
|
+
* the unfolded frame stays consistently oriented across faces with no reference to any global up. Writes
|
|
153
|
+
* the 2D apex into {@link _apex} and returns the apex vertex id.
|
|
154
|
+
*/
|
|
155
|
+
const _apex = new Float64Array(2);
|
|
156
|
+
function unfold_apex(topology, face, e0v, e1v, e0x, e0y, e1x, e1y) {
|
|
157
|
+
triangle_vertices(topology, face);
|
|
158
|
+
let apex = -1, e0_before_e1 = false;
|
|
159
|
+
for (let i = 0; i < 3; i++) {
|
|
160
|
+
if (_tri[i] === e0v && _tri[(i + 1) % 3] === e1v) e0_before_e1 = true;
|
|
161
|
+
if (_tri[i] !== e0v && _tri[i] !== e1v) apex = _tri[i];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const dac = vid_distance_3d(topology, e0v, apex);
|
|
165
|
+
const dbc = vid_distance_3d(topology, e1v, apex);
|
|
166
|
+
const ex = e1x - e0x, ey = e1y - e0y;
|
|
167
|
+
const d = Math.hypot(ex, ey);
|
|
168
|
+
const a = (dac * dac - dbc * dbc + d * d) / (2 * d); // foot of the apex along the edge
|
|
169
|
+
let h2 = dac * dac - a * a; if (h2 < 0) h2 = 0;
|
|
170
|
+
const h = Math.sqrt(h2);
|
|
171
|
+
const ux = ex / d, uy = ey / d;
|
|
172
|
+
const sign = e0_before_e1 ? 1 : -1; // apex left of e0->e1 when the loop runs e0->e1
|
|
173
|
+
_apex[0] = e0x + a * ux + h * (-uy) * sign; // (-uy,ux) is the left normal of (ux,uy)
|
|
174
|
+
_apex[1] = e0y + a * uy + h * ux * sign;
|
|
175
|
+
return apex;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Barycentric weights (for va,vb,vc, in order) of a 3D point projected onto their plane, into `out`. */
|
|
179
|
+
function barycentric_3d(topology, va, vb, vc, px, py, pz, out) {
|
|
180
|
+
topology.vertex_read_coordinate(_va, 0, va);
|
|
181
|
+
topology.vertex_read_coordinate(_vb, 0, vb);
|
|
182
|
+
topology.vertex_read_coordinate(_vc, 0, vc);
|
|
183
|
+
const e0x = _vb[0] - _va[0], e0y = _vb[1] - _va[1], e0z = _vb[2] - _va[2];
|
|
184
|
+
const e1x = _vc[0] - _va[0], e1y = _vc[1] - _va[1], e1z = _vc[2] - _va[2];
|
|
185
|
+
const v2x = px - _va[0], v2y = py - _va[1], v2z = pz - _va[2];
|
|
186
|
+
const d00 = e0x * e0x + e0y * e0y + e0z * e0z, d01 = e0x * e1x + e0y * e1y + e0z * e1z, d11 = e1x * e1x + e1y * e1y + e1z * e1z;
|
|
187
|
+
const d20 = v2x * e0x + v2y * e0y + v2z * e0z, d21 = v2x * e1x + v2y * e1y + v2z * e1z;
|
|
188
|
+
const denom = d00 * d11 - d01 * d01;
|
|
189
|
+
if (Math.abs(denom) < 1e-18) { out[0] = 1; out[1] = 0; out[2] = 0; return; }
|
|
190
|
+
const v = (d11 * d20 - d01 * d21) / denom, w = (d00 * d21 - d01 * d20) / denom;
|
|
191
|
+
out[0] = 1 - v - w; out[1] = v; out[2] = w;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* True if `vid` is a turning corner: a boundary vertex where the *free space* is reflex (incident
|
|
196
|
+
* triangle angles, measured intrinsically in 3D, sum to > 180deg). The path only ever bends around these
|
|
197
|
+
* -- flat boundary vertices (180deg) and convex outer corners (<180deg) are not turning points, and
|
|
198
|
+
* admitting them as roots makes the search blow up combinatorially.
|
|
199
|
+
*/
|
|
200
|
+
const _corner_cache = new Map();
|
|
201
|
+
const _corner_faces = new Set();
|
|
202
|
+
function is_corner(topology, vid) {
|
|
203
|
+
const cached = _corner_cache.get(vid);
|
|
204
|
+
if (cached !== undefined) return cached;
|
|
205
|
+
|
|
206
|
+
let boundary = false;
|
|
207
|
+
_corner_faces.clear();
|
|
208
|
+
|
|
209
|
+
const e_first = topology.vertex_read_edge(vid);
|
|
210
|
+
if (e_first !== NULL_POINTER) {
|
|
211
|
+
let e = e_first;
|
|
212
|
+
do {
|
|
213
|
+
const loop = topology.edge_read_loop(e);
|
|
214
|
+
if (loop === NULL_POINTER || topology.loop_read_radial_next(loop) === loop) {
|
|
215
|
+
boundary = true;
|
|
216
|
+
}
|
|
217
|
+
if (loop !== NULL_POINTER) {
|
|
218
|
+
let rl = loop;
|
|
219
|
+
do { _corner_faces.add(topology.loop_read_face(rl)); rl = topology.loop_read_radial_next(rl); }
|
|
220
|
+
while (rl !== loop);
|
|
221
|
+
}
|
|
222
|
+
e = topology.edge_read_vertex1(e) === vid
|
|
223
|
+
? topology.edge_read_v1_disk_next(e)
|
|
224
|
+
: topology.edge_read_v2_disk_next(e);
|
|
225
|
+
} while (e !== e_first && e !== NULL_POINTER);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let result = false;
|
|
229
|
+
if (boundary) {
|
|
230
|
+
topology.vertex_read_coordinate(_va, 0, vid);
|
|
231
|
+
const ox = _va[0], oy = _va[1], oz = _va[2];
|
|
232
|
+
let angle = 0;
|
|
233
|
+
for (const f of _corner_faces) {
|
|
234
|
+
triangle_vertices(topology, f);
|
|
235
|
+
let a = -1, b = -1;
|
|
236
|
+
for (let i = 0; i < 3; i++) {
|
|
237
|
+
if (_tri[i] !== vid) { if (a === -1) a = _tri[i]; else b = _tri[i]; }
|
|
238
|
+
}
|
|
239
|
+
topology.vertex_read_coordinate(_vb, 0, a);
|
|
240
|
+
topology.vertex_read_coordinate(_vc, 0, b);
|
|
241
|
+
const ax = _vb[0] - ox, ay = _vb[1] - oy, az = _vb[2] - oz;
|
|
242
|
+
const bx = _vc[0] - ox, by = _vc[1] - oy, bz = _vc[2] - oz;
|
|
243
|
+
const cx = ay * bz - az * by, cy = az * bx - ax * bz, cz = ax * by - ay * bx;
|
|
244
|
+
angle += Math.atan2(Math.hypot(cx, cy, cz), ax * bx + ay * by + az * bz);
|
|
245
|
+
}
|
|
246
|
+
result = angle > Math.PI + 1e-6;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_corner_cache.set(vid, result);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- node storage ----------------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
// Search nodes are fixed 16-word records packed into one flat ArrayBuffer, addressed by id*NODE_WORDS,
|
|
256
|
+
// so the open list is a Uint32Heap4 (id + score) rather than an object heap -- NO per-node object
|
|
257
|
+
// allocation, so the hot path produces no garbage and never triggers a GC pause (which would otherwise
|
|
258
|
+
// jitter the rest of the engine). Allocation is a monotonic bump counter reset per query; nodes are
|
|
259
|
+
// never released mid-query, so there is no free list and no occupancy bookkeeping. The buffer only ever
|
|
260
|
+
// grows (by doubling, copying the existing records over) and the two typed-array views are re-cached
|
|
261
|
+
// after a grow. (A pooled allocator with a free list + occupancy bitset degenerates to exactly this
|
|
262
|
+
// under a never-free, bump-allocated access pattern, so a raw buffer is the honest, lighter shape.)
|
|
263
|
+
//
|
|
264
|
+
// A node stores its entry edge as vertex ids (E0V,E1V) AND its 2D positions in the node's unfolded frame
|
|
265
|
+
// (E0X,E0Y,E1X,E1Y), the root (ROOTVID + RX,RY in the frame; ROOTVID==NULL_ID is the start point), and
|
|
266
|
+
// the interval as the fractions [T0,T1] along the edge. 3D is recovered at reconstruction from vertex
|
|
267
|
+
// ids; a terminal node stores the goal edge (E0V,E1V) and the final bend's fraction along it (N_FCT,
|
|
268
|
+
// -1 == a straight shot, since a real bend fraction is in [0,1]).
|
|
269
|
+
const N_FACE = 0, N_E0V = 1, N_E1V = 2;
|
|
270
|
+
const N_E0X = 3, N_E0Y = 4, N_E1X = 5, N_E1Y = 6;
|
|
271
|
+
const N_ROOTVID = 7, N_RX = 8, N_RY = 9;
|
|
272
|
+
const N_T0 = 10, N_T1 = 11;
|
|
273
|
+
const N_G = 12, N_PARENT = 13, N_TERMINAL = 14, N_FCT = 15;
|
|
274
|
+
const NODE_WORDS = 16;
|
|
275
|
+
const NULL_ID = 0xFFFFFFFF;
|
|
276
|
+
|
|
277
|
+
const _open = new Uint32Heap4();
|
|
278
|
+
|
|
279
|
+
// node records: uint32 and float32 views overlaid on one ArrayBuffer (_node_df aliases _node_du's bytes)
|
|
280
|
+
let _node_capacity = 1024; // capacity in nodes; grows by doubling
|
|
281
|
+
let _node_du = new Uint32Array(_node_capacity * NODE_WORDS);
|
|
282
|
+
let _node_df = new Float32Array(_node_du.buffer);
|
|
283
|
+
let _node_count = 0;
|
|
284
|
+
|
|
285
|
+
/** Reserve the next node slot, growing (and re-caching the views) only when the buffer is full. */
|
|
286
|
+
function alloc_node() {
|
|
287
|
+
const id = _node_count++;
|
|
288
|
+
if (id >= _node_capacity) {
|
|
289
|
+
_node_capacity *= 2;
|
|
290
|
+
const grown = new Uint32Array(_node_capacity * NODE_WORDS);
|
|
291
|
+
grown.set(_node_du); // copy the existing records into the larger buffer
|
|
292
|
+
_node_du = grown;
|
|
293
|
+
_node_df = new Float32Array(grown.buffer);
|
|
294
|
+
}
|
|
295
|
+
return id;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// the start/goal 3D points (for the heuristic and for lifting the path back to 3D)
|
|
299
|
+
let _sx = 0, _sy = 0, _sz = 0;
|
|
300
|
+
let _gx = 0, _gy = 0, _gz = 0;
|
|
301
|
+
|
|
302
|
+
// goal-face vertices + barycentric weights of the goal point (to place the goal in any node's frame)
|
|
303
|
+
let _goal_v0 = 0, _goal_v1 = 0, _goal_v2 = 0;
|
|
304
|
+
let _goal_w0 = 0, _goal_w1 = 0, _goal_w2 = 0;
|
|
305
|
+
const _bw = new Float64Array(3);
|
|
306
|
+
|
|
307
|
+
// edge-crossing points (flat x,y,z triples) gathered along the parent chain during reconstruction
|
|
308
|
+
const _recon = [];
|
|
309
|
+
|
|
310
|
+
function goal_weight(vid) {
|
|
311
|
+
return vid === _goal_v0 ? _goal_w0 : (vid === _goal_v1 ? _goal_w1 : _goal_w2);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---- search ----------------------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Find the exact shortest path from (sx,sy,sz) in `start_face` to (gx,gy,gz) in `goal_face`.
|
|
318
|
+
*
|
|
319
|
+
* @param {number[]|Float64Array|Float32Array} output path written as flat [x0,y0,z0, x1,y1,z1, ...]
|
|
320
|
+
* @param {BinaryTopology} topology triangle mesh (3D vertex coordinates)
|
|
321
|
+
* @param {number} sx
|
|
322
|
+
* @param {number} sy
|
|
323
|
+
* @param {number} sz start point (assumed on/near `start_face`)
|
|
324
|
+
* @param {number} start_face triangle containing the start point
|
|
325
|
+
* @param {number} gx
|
|
326
|
+
* @param {number} gy
|
|
327
|
+
* @param {number} gz goal point (assumed on/near `goal_face`)
|
|
328
|
+
* @param {number} goal_face triangle containing the goal point
|
|
329
|
+
* @returns {number} number of path POINTS written (3 numbers each), 0 if no path
|
|
330
|
+
*/
|
|
331
|
+
export function bt_mesh_face_find_path_polyanya(output, topology, sx, sy, sz, start_face, gx, gy, gz, goal_face) {
|
|
332
|
+
_sx = sx; _sy = sy; _sz = sz;
|
|
333
|
+
_gx = gx; _gy = gy; _gz = gz;
|
|
334
|
+
|
|
335
|
+
_corner_cache.clear();
|
|
336
|
+
_covered.clear();
|
|
337
|
+
_node_count = 0;
|
|
338
|
+
_open.clear();
|
|
339
|
+
|
|
340
|
+
if (start_face === goal_face) {
|
|
341
|
+
output[0] = sx; output[1] = sy; output[2] = sz;
|
|
342
|
+
output[3] = gx; output[4] = gy; output[5] = gz;
|
|
343
|
+
return 2;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// goal barycentric, so the goal can be unfolded into any frame that reaches the goal face
|
|
347
|
+
triangle_vertices(topology, goal_face);
|
|
348
|
+
_goal_v0 = _tri[0]; _goal_v1 = _tri[1]; _goal_v2 = _tri[2];
|
|
349
|
+
barycentric_3d(topology, _goal_v0, _goal_v1, _goal_v2, gx, gy, gz, _bw);
|
|
350
|
+
_goal_w0 = _bw[0]; _goal_w1 = _bw[1]; _goal_w2 = _bw[2];
|
|
351
|
+
|
|
352
|
+
// lay the start face out in 2D (CCW: apex on the +y/left side of edge a->b) and place the start point
|
|
353
|
+
triangle_vertices(topology, start_face);
|
|
354
|
+
const a = _tri[0], b = _tri[1], c = _tri[2];
|
|
355
|
+
const dab = vid_distance_3d(topology, a, b);
|
|
356
|
+
const dac = vid_distance_3d(topology, a, c);
|
|
357
|
+
const dbc = vid_distance_3d(topology, b, c);
|
|
358
|
+
const cx = (dac * dac - dbc * dbc + dab * dab) / (2 * dab);
|
|
359
|
+
let ch2 = dac * dac - cx * cx; if (ch2 < 0) ch2 = 0;
|
|
360
|
+
const cy = Math.sqrt(ch2); // p_a=(0,0), p_b=(dab,0), p_c=(cx,cy)
|
|
361
|
+
|
|
362
|
+
barycentric_3d(topology, a, b, c, sx, sy, sz, _bw);
|
|
363
|
+
const su = _bw[1] * dab + _bw[2] * cx;
|
|
364
|
+
const sv = _bw[2] * cy;
|
|
365
|
+
|
|
366
|
+
// the start point sees all three edges of its triangle in full
|
|
367
|
+
start_edge(topology, su, sv, start_face, a, b, 0, 0, dab, 0, goal_face);
|
|
368
|
+
start_edge(topology, su, sv, start_face, b, c, dab, 0, cx, cy, goal_face);
|
|
369
|
+
start_edge(topology, su, sv, start_face, c, a, cx, cy, 0, 0, goal_face);
|
|
370
|
+
|
|
371
|
+
let best = NULL_ID;
|
|
372
|
+
let guard = 0;
|
|
373
|
+
while (!_open.is_empty()) {
|
|
374
|
+
const id = _open.pop_min();
|
|
375
|
+
if (_node_du[id * NODE_WORDS + N_TERMINAL] === 1) { best = id; break; }
|
|
376
|
+
if (++guard > 5_000_000) break;
|
|
377
|
+
expand(topology, id, goal_face);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (best === NULL_ID) return 0;
|
|
381
|
+
|
|
382
|
+
// Reconstruct a SURFACE-FOLLOWING path. Every node in the parent chain shares one continuous unfolded
|
|
383
|
+
// frame (each node's frame extends its parent's), so the whole route -- start, turning corners, goal --
|
|
384
|
+
// lays out as a single 2D polyline in that frame, and the point where the route crosses each face's
|
|
385
|
+
// entry edge is just the intersection of that polyline with the edge. Lifting each crossing back onto
|
|
386
|
+
// its 3D edge keeps every output segment inside one face (i.e. on the surface). Crossings that fall on
|
|
387
|
+
// a straight (flat) stretch collapse away in push_point, so flat meshes still yield minimal paths.
|
|
388
|
+
const du = _node_du, df = _node_df;
|
|
389
|
+
const bb = best * NODE_WORDS;
|
|
390
|
+
|
|
391
|
+
// the goal's position in the shared frame: unfold the goal-face apex from the terminal's entry edge
|
|
392
|
+
const t_e0v = du[bb + N_E0V], t_e1v = du[bb + N_E1V];
|
|
393
|
+
const t_e0x = df[bb + N_E0X], t_e0y = df[bb + N_E0Y], t_e1x = df[bb + N_E1X], t_e1y = df[bb + N_E1Y];
|
|
394
|
+
const g_apex = unfold_apex(topology, goal_face, t_e0v, t_e1v, t_e0x, t_e0y, t_e1x, t_e1y);
|
|
395
|
+
const goal_u = goal_weight(t_e0v) * t_e0x + goal_weight(t_e1v) * t_e1x + goal_weight(g_apex) * _apex[0];
|
|
396
|
+
const goal_v = goal_weight(t_e0v) * t_e0y + goal_weight(t_e1v) * t_e1y + goal_weight(g_apex) * _apex[1];
|
|
397
|
+
const t_fct = df[bb + N_FCT];
|
|
398
|
+
|
|
399
|
+
// walk the chain (goal end -> start), recording each entry-edge crossing in reverse path order
|
|
400
|
+
_recon.length = 0;
|
|
401
|
+
let next_u = goal_u, next_v = goal_v; // the next waypoint toward the goal, in the shared frame
|
|
402
|
+
let prev_root = du[bb + N_ROOTVID];
|
|
403
|
+
let prev_rx = df[bb + N_RX], prev_ry = df[bb + N_RY];
|
|
404
|
+
|
|
405
|
+
for (let n = best; n !== NULL_ID; n = du[n * NODE_WORDS + N_PARENT]) {
|
|
406
|
+
const base = n * NODE_WORDS;
|
|
407
|
+
const rootVid = du[base + N_ROOTVID];
|
|
408
|
+
const rx = df[base + N_RX], ry = df[base + N_RY];
|
|
409
|
+
|
|
410
|
+
// a change of root means the route turned at the previous corridor's root: emit that corner
|
|
411
|
+
// (an exact mesh vertex -- the turning point) and aim subsequent crossings at it
|
|
412
|
+
if (rootVid !== prev_root) {
|
|
413
|
+
if (prev_root !== NULL_ID) {
|
|
414
|
+
topology.vertex_read_coordinate(_va, 0, prev_root);
|
|
415
|
+
_recon.push(_va[0], _va[1], _va[2]);
|
|
416
|
+
}
|
|
417
|
+
next_u = prev_rx; next_v = prev_ry;
|
|
418
|
+
prev_root = rootVid;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const e0v = du[base + N_E0V], e1v = du[base + N_E1V];
|
|
422
|
+
const e0x = df[base + N_E0X], e0y = df[base + N_E0Y], e1x = df[base + N_E1X], e1y = df[base + N_E1Y];
|
|
423
|
+
|
|
424
|
+
// fraction along the entry edge where the route root->next crosses it (the terminal's goal-edge
|
|
425
|
+
// bend is exact and pre-computed as N_FCT)
|
|
426
|
+
let u;
|
|
427
|
+
if (n === best && t_fct !== -1) {
|
|
428
|
+
u = t_fct;
|
|
429
|
+
} else {
|
|
430
|
+
u = line_segment_intersection_fraction_2d(rx, ry, next_u, next_v, e0x, e0y, e1x, e1y);
|
|
431
|
+
if (u === -1) {
|
|
432
|
+
// the route line grazes a shared vertex (passes through / just outside an edge endpoint,
|
|
433
|
+
// common when the geodesic runs along a row of grid vertices): snap to the nearer endpoint
|
|
434
|
+
const dx = next_u - rx, dy = next_v - ry;
|
|
435
|
+
const s0 = Math.abs(dx * (e0y - ry) - dy * (e0x - rx));
|
|
436
|
+
const s1 = Math.abs(dx * (e1y - ry) - dy * (e1x - rx));
|
|
437
|
+
u = s0 <= s1 ? 0 : 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
topology.vertex_read_coordinate(_va, 0, e0v);
|
|
442
|
+
topology.vertex_read_coordinate(_vb, 0, e1v);
|
|
443
|
+
_recon.push(_va[0] + (_vb[0] - _va[0]) * u, _va[1] + (_vb[1] - _va[1]) * u, _va[2] + (_vb[2] - _va[2]) * u);
|
|
444
|
+
|
|
445
|
+
prev_rx = rx; prev_ry = ry;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// start, the crossings in path order (reverse of how they were collected), goal
|
|
449
|
+
let count = 0;
|
|
450
|
+
count = push_point(output, count, _sx, _sy, _sz);
|
|
451
|
+
for (let i = _recon.length - 3; i >= 0; i -= 3) {
|
|
452
|
+
count = push_point(output, count, _recon[i], _recon[i + 1], _recon[i + 2]);
|
|
453
|
+
}
|
|
454
|
+
count = push_point(output, count, _gx, _gy, _gz);
|
|
455
|
+
return count / 3;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Append a 3D point, collapsing collinear runs: if the previous point lies (within tolerance) on the
|
|
460
|
+
* segment between the one before it and the new point, it is redundant and gets replaced rather than
|
|
461
|
+
* kept. Flat stretches of a surface-following path thus reduce to their endpoints; genuine bends (turning
|
|
462
|
+
* corners, folds in the surface) survive.
|
|
463
|
+
*/
|
|
464
|
+
function push_point(output, count, x, y, z) {
|
|
465
|
+
if (count >= 6) {
|
|
466
|
+
const ax = output[count - 6], ay = output[count - 5], az = output[count - 4];
|
|
467
|
+
const bx = output[count - 3], by = output[count - 2], bz = output[count - 1];
|
|
468
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
469
|
+
const acx = x - ax, acy = y - ay, acz = z - az;
|
|
470
|
+
const cross_x = aby * acz - abz * acy, cross_y = abz * acx - abx * acz, cross_z = abx * acy - aby * acx;
|
|
471
|
+
const ac_len = Math.hypot(acx, acy, acz);
|
|
472
|
+
if (ac_len < DEDUP_EPS || Math.hypot(cross_x, cross_y, cross_z) <= COLLINEAR_EPS * ac_len) {
|
|
473
|
+
output[count - 3] = x; output[count - 2] = y; output[count - 1] = z;
|
|
474
|
+
return count;
|
|
475
|
+
}
|
|
476
|
+
} else if (count >= 3 &&
|
|
477
|
+
Math.abs(output[count - 3] - x) <= DEDUP_EPS &&
|
|
478
|
+
Math.abs(output[count - 2] - y) <= DEDUP_EPS &&
|
|
479
|
+
Math.abs(output[count - 1] - z) <= DEDUP_EPS) {
|
|
480
|
+
return count;
|
|
481
|
+
}
|
|
482
|
+
output[count] = x; output[count + 1] = y; output[count + 2] = z;
|
|
483
|
+
return count + 3;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function start_edge(topology, su, sv, start_face, ea, eb, eax, eay, ebx, eby, goal_face) {
|
|
487
|
+
const neighbour = neighbour_across(topology, start_face, ea, eb);
|
|
488
|
+
add_successor(topology, neighbour, start_face, ea, eb, eax, eay, ebx, eby, su, sv, -1, 0, NULL_ID, 0, 1, goal_face);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function expand(topology, id, goal_face) {
|
|
492
|
+
const du = _node_du, df = _node_df;
|
|
493
|
+
const base = id * NODE_WORDS;
|
|
494
|
+
|
|
495
|
+
const face = du[base + N_FACE];
|
|
496
|
+
const e0v = du[base + N_E0V], e1v = du[base + N_E1V];
|
|
497
|
+
const e0x = df[base + N_E0X], e0y = df[base + N_E0Y];
|
|
498
|
+
const e1x = df[base + N_E1X], e1y = df[base + N_E1Y];
|
|
499
|
+
const rv = du[base + N_ROOTVID]; const rootVid = rv === NULL_ID ? -1 : rv;
|
|
500
|
+
const rx = df[base + N_RX], ry = df[base + N_RY];
|
|
501
|
+
const t0 = df[base + N_T0], t1 = df[base + N_T1];
|
|
502
|
+
const g = df[base + N_G];
|
|
503
|
+
|
|
504
|
+
// interval endpoints + their edge vertex ids, ordered so i1 is left of the ray root->i0
|
|
505
|
+
let i0x = e0x + t0 * (e1x - e0x), i0y = e0y + t0 * (e1y - e0y), i0v = t0 <= PEPS ? e0v : (t0 >= 1 - PEPS ? e1v : -1);
|
|
506
|
+
let i1x = e0x + t1 * (e1x - e0x), i1y = e0y + t1 * (e1y - e0y), i1v = t1 <= PEPS ? e0v : (t1 >= 1 - PEPS ? e1v : -1);
|
|
507
|
+
if (triangle2d_compute_area(rx, ry, i0x, i0y, i1x, i1y) < 0) {
|
|
508
|
+
const sxx = i0x, syy = i0y, svv = i0v;
|
|
509
|
+
i0x = i1x; i0y = i1y; i0v = i1v; i1x = sxx; i1y = syy; i1v = svv;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// unfold the apex into this node's frame, giving the two far edges (e0v,apex) and (apex,e1v)
|
|
513
|
+
const apex = unfold_apex(topology, face, e0v, e1v, e0x, e0y, e1x, e1y);
|
|
514
|
+
const apx = _apex[0], apy = _apex[1];
|
|
515
|
+
|
|
516
|
+
process_far_edge(topology, face, e0v, e0x, e0y, apex, apx, apy, rx, ry, rootVid, g, id, i0x, i0y, i0v, i1x, i1y, i1v, goal_face);
|
|
517
|
+
process_far_edge(topology, face, apex, apx, apy, e1v, e1x, e1y, rx, ry, rootVid, g, id, i0x, i0y, i0v, i1x, i1y, i1v, goal_face);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const _split = new Float64Array(2);
|
|
521
|
+
|
|
522
|
+
function process_far_edge(topology, from_face, Pv, Px, Py, Qv, Qx, Qy, rx, ry, rootVid, g, parent, i0x, i0y, i0v, i1x, i1y, i1v, goal_face) {
|
|
523
|
+
const neighbour = neighbour_across(topology, from_face, Pv, Qv);
|
|
524
|
+
|
|
525
|
+
let m = 0;
|
|
526
|
+
const tR = line_segment_intersection_fraction_2d(rx, ry, i0x, i0y, Px, Py, Qx, Qy);
|
|
527
|
+
const tL = line_segment_intersection_fraction_2d(rx, ry, i1x, i1y, Px, Py, Qx, Qy);
|
|
528
|
+
// -1 (no crossing) fails `> PEPS`, so the interior-fraction test rejects it without a NaN check
|
|
529
|
+
if (tR > PEPS && tR < 1 - PEPS) _split[m++] = tR;
|
|
530
|
+
if (tL > PEPS && tL < 1 - PEPS) _split[m++] = tL;
|
|
531
|
+
if (m === 2 && _split[0] > _split[1]) { const t = _split[0]; _split[0] = _split[1]; _split[1] = t; }
|
|
532
|
+
|
|
533
|
+
let prev = 0;
|
|
534
|
+
for (let k = 0; k <= m; k++) {
|
|
535
|
+
const next = k < m ? _split[k] : 1;
|
|
536
|
+
if (next - prev < PEPS) { prev = next; continue; }
|
|
537
|
+
|
|
538
|
+
const mx = Px + (Qx - Px) * (prev + next) * 0.5, my = Py + (Qy - Py) * (prev + next) * 0.5;
|
|
539
|
+
const sR = triangle2d_compute_area(rx, ry, i0x, i0y, mx, my); // >=0: left of right-ray (inside cone)
|
|
540
|
+
const sL = triangle2d_compute_area(rx, ry, i1x, i1y, mx, my); // <=0: right of left-ray (inside cone)
|
|
541
|
+
|
|
542
|
+
if (sR >= -EPS && sL <= EPS) {
|
|
543
|
+
add_successor(topology, neighbour, from_face, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid, g, parent, prev, next, goal_face);
|
|
544
|
+
} else if (sR < -EPS) {
|
|
545
|
+
if (i0v !== -1 && is_corner(topology, i0v)) {
|
|
546
|
+
add_successor(topology, neighbour, from_face, Pv, Qv, Px, Py, Qx, Qy, i0x, i0y, i0v, g + v2_distance(rx, ry, i0x, i0y), parent, prev, next, goal_face);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
if (i1v !== -1 && is_corner(topology, i1v)) {
|
|
550
|
+
add_successor(topology, neighbour, from_face, Pv, Qv, Px, Py, Qx, Qy, i1x, i1y, i1v, g + v2_distance(rx, ry, i1x, i1y), parent, prev, next, goal_face);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
prev = next;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Per (entered edge, root) interval coverage. A fixed root implies a fixed g, so the first node to
|
|
559
|
+
* cover a stretch of an edge is optimal there -- later nodes are clipped to the still-uncovered part
|
|
560
|
+
* (or dropped). This is what makes the wavefront terminate instead of re-flooding the same edges.
|
|
561
|
+
*/
|
|
562
|
+
const _covered = new Map();
|
|
563
|
+
const ROOT_STRIDE = 0x1000000;
|
|
564
|
+
const _uncovered = new Float64Array(256);
|
|
565
|
+
|
|
566
|
+
function reserve(from_face, neighbour, root_vid, lo, hi) {
|
|
567
|
+
let by_root = _covered.get(from_face);
|
|
568
|
+
if (by_root === undefined) { by_root = new Map(); _covered.set(from_face, by_root); }
|
|
569
|
+
|
|
570
|
+
const inner_key = neighbour * ROOT_STRIDE + (root_vid + 1);
|
|
571
|
+
let ranges = by_root.get(inner_key);
|
|
572
|
+
if (ranges === undefined) { ranges = []; by_root.set(inner_key, ranges); }
|
|
573
|
+
|
|
574
|
+
let out = 0;
|
|
575
|
+
let cur = lo;
|
|
576
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
577
|
+
const r_lo = ranges[i], r_hi = ranges[i + 1];
|
|
578
|
+
if (r_hi <= cur) continue;
|
|
579
|
+
if (r_lo >= hi) break;
|
|
580
|
+
if (r_lo > cur) { _uncovered[out++] = cur; _uncovered[out++] = r_lo < hi ? r_lo : hi; }
|
|
581
|
+
if (r_hi > cur) cur = r_hi;
|
|
582
|
+
if (cur >= hi) break;
|
|
583
|
+
}
|
|
584
|
+
if (cur < hi) { _uncovered[out++] = cur; _uncovered[out++] = hi; }
|
|
585
|
+
|
|
586
|
+
let i = 0;
|
|
587
|
+
while (i < ranges.length && ranges[i + 1] < lo - PEPS) i += 2;
|
|
588
|
+
let merge_lo = lo, merge_hi = hi, j = i;
|
|
589
|
+
while (j < ranges.length && ranges[j] <= hi + PEPS) {
|
|
590
|
+
if (ranges[j] < merge_lo) merge_lo = ranges[j];
|
|
591
|
+
if (ranges[j + 1] > merge_hi) merge_hi = ranges[j + 1];
|
|
592
|
+
j += 2;
|
|
593
|
+
}
|
|
594
|
+
ranges.splice(i, j - i, merge_lo, merge_hi);
|
|
595
|
+
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Clip the interval (edge fractions [lo,hi] along Pv->Qv) against what has already been reached on this
|
|
601
|
+
* edge from `rootVid`, then emit a search node (or terminal, if the entered face holds the goal) for
|
|
602
|
+
* each still-uncovered piece.
|
|
603
|
+
*/
|
|
604
|
+
function add_successor(topology, neighbour, from_face, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid, g, parent, lo, hi, goal_face) {
|
|
605
|
+
if (neighbour === NULL_POINTER) return; // boundary far edge -- nothing to enter
|
|
606
|
+
if (hi - lo < PEPS) return;
|
|
607
|
+
|
|
608
|
+
const out = reserve(from_face, neighbour, rootVid, lo, hi);
|
|
609
|
+
for (let i = 0; i < out; i += 2) {
|
|
610
|
+
const ulo = _uncovered[i], uhi = _uncovered[i + 1];
|
|
611
|
+
if (uhi - ulo < PEPS) continue;
|
|
612
|
+
emit_node(topology, neighbour, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid, g, parent, ulo, uhi, goal_face);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function emit_node(topology, neighbour, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid, g, parent, t0, t1, goal_face) {
|
|
617
|
+
if (neighbour === goal_face) {
|
|
618
|
+
// unfold the goal into this frame for the exact final cost: place the apex, then barycentric
|
|
619
|
+
const apex = unfold_apex(topology, goal_face, Pv, Qv, Px, Py, Qx, Qy);
|
|
620
|
+
const ggx = goal_weight(Pv) * Px + goal_weight(Qv) * Qx + goal_weight(apex) * _apex[0];
|
|
621
|
+
const ggy = goal_weight(Pv) * Py + goal_weight(Qv) * Qy + goal_weight(apex) * _apex[1];
|
|
622
|
+
|
|
623
|
+
let i0x = Px + t0 * (Qx - Px), i0y = Py + t0 * (Qy - Py), i0p = t0;
|
|
624
|
+
let i1x = Px + t1 * (Qx - Px), i1y = Py + t1 * (Qy - Py), i1p = t1;
|
|
625
|
+
if (triangle2d_compute_area(rx, ry, i0x, i0y, i1x, i1y) < 0) {
|
|
626
|
+
const sxx = i0x, syy = i0y, sp = i0p; i0x = i1x; i0y = i1y; i0p = i1p; i1x = sxx; i1y = syy; i1p = sp;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const fcost = g + touch_distance(rx, ry, i0x, i0y, i1x, i1y, ggx, ggy);
|
|
630
|
+
|
|
631
|
+
let fct = -1; // -1 = straight shot (no forced bend at an interval endpoint); a real bend is in [0,1]
|
|
632
|
+
let tx = ggx, ty = ggy;
|
|
633
|
+
if (triangle2d_compute_area(i0x, i0y, i1x, i1y, rx, ry) * triangle2d_compute_area(i0x, i0y, i1x, i1y, ggx, ggy) > 0) {
|
|
634
|
+
reflect(ggx, ggy, i0x, i0y, i1x, i1y); tx = _reflect[0]; ty = _reflect[1];
|
|
635
|
+
}
|
|
636
|
+
const t = line_segment_intersection_fraction_2d(rx, ry, tx, ty, i0x, i0y, i1x, i1y);
|
|
637
|
+
if (t < -PEPS || t > 1 + PEPS) { // -1 (no crossing) is caught by `< -PEPS`
|
|
638
|
+
const d0 = v2_distance(rx, ry, i0x, i0y) + v2_distance(i0x, i0y, ggx, ggy);
|
|
639
|
+
const d1 = v2_distance(rx, ry, i1x, i1y) + v2_distance(i1x, i1y, ggx, ggy);
|
|
640
|
+
fct = d0 <= d1 ? i0p : i1p;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const id = alloc_node();
|
|
644
|
+
const du = _node_du, df = _node_df;
|
|
645
|
+
const b = id * NODE_WORDS;
|
|
646
|
+
du[b + N_TERMINAL] = 1;
|
|
647
|
+
du[b + N_ROOTVID] = rootVid === -1 ? NULL_ID : rootVid;
|
|
648
|
+
du[b + N_E0V] = Pv; du[b + N_E1V] = Qv;
|
|
649
|
+
df[b + N_E0X] = Px; df[b + N_E0Y] = Py; df[b + N_E1X] = Qx; df[b + N_E1Y] = Qy;
|
|
650
|
+
df[b + N_RX] = rx; df[b + N_RY] = ry;
|
|
651
|
+
du[b + N_PARENT] = parent;
|
|
652
|
+
df[b + N_FCT] = fct;
|
|
653
|
+
_open.insert(id, fcost);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// admissible heuristic in 3D: exact straight-chord root -> interval -> goal
|
|
658
|
+
let r3x, r3y, r3z;
|
|
659
|
+
if (rootVid === -1) { r3x = _sx; r3y = _sy; r3z = _sz; }
|
|
660
|
+
else { topology.vertex_read_coordinate(_qa, 0, rootVid); r3x = _qa[0]; r3y = _qa[1]; r3z = _qa[2]; }
|
|
661
|
+
topology.vertex_read_coordinate(_va, 0, Pv);
|
|
662
|
+
topology.vertex_read_coordinate(_vb, 0, Qv);
|
|
663
|
+
const h = touch_distance_3d(
|
|
664
|
+
r3x, r3y, r3z,
|
|
665
|
+
_va[0] + t0 * (_vb[0] - _va[0]), _va[1] + t0 * (_vb[1] - _va[1]), _va[2] + t0 * (_vb[2] - _va[2]),
|
|
666
|
+
_va[0] + t1 * (_vb[0] - _va[0]), _va[1] + t1 * (_vb[1] - _va[1]), _va[2] + t1 * (_vb[2] - _va[2]),
|
|
667
|
+
_gx, _gy, _gz);
|
|
668
|
+
|
|
669
|
+
const id = alloc_node();
|
|
670
|
+
const du = _node_du, df = _node_df;
|
|
671
|
+
const b = id * NODE_WORDS;
|
|
672
|
+
du[b + N_TERMINAL] = 0;
|
|
673
|
+
du[b + N_FACE] = neighbour;
|
|
674
|
+
du[b + N_E0V] = Pv; du[b + N_E1V] = Qv;
|
|
675
|
+
df[b + N_E0X] = Px; df[b + N_E0Y] = Py; df[b + N_E1X] = Qx; df[b + N_E1Y] = Qy;
|
|
676
|
+
du[b + N_ROOTVID] = rootVid === -1 ? NULL_ID : rootVid;
|
|
677
|
+
df[b + N_RX] = rx; df[b + N_RY] = ry;
|
|
678
|
+
df[b + N_T0] = t0; df[b + N_T1] = t1;
|
|
679
|
+
df[b + N_G] = g;
|
|
680
|
+
du[b + N_PARENT] = parent;
|
|
681
|
+
_open.insert(id, g + h);
|
|
682
|
+
}
|