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