@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
|
@@ -1,82 +1,564 @@
|
|
|
1
1
|
import { assert } from "../../../core/assert.js";
|
|
2
2
|
import { Uint32Heap } from "../../../core/collection/heap/Uint32Heap.js";
|
|
3
|
-
import {
|
|
3
|
+
import { NULL_POINTER } from "../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
|
|
4
4
|
import {
|
|
5
5
|
bt_face_get_neighbour_faces
|
|
6
6
|
} from "../../../core/geom/3d/topology/struct/binary/query/bt_face_get_neighbour_faces.js";
|
|
7
|
+
import {
|
|
8
|
+
bt_mesh_compute_edge_distance_eikonal
|
|
9
|
+
} from "../../../core/geom/3d/topology/struct/binary/query/bt_mesh_compute_edge_distance_eikonal.js";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Note that we limit the supported number of neighbors, a reasonable mesh will fit this criteria
|
|
13
|
+
* @type {Uint32Array}
|
|
14
|
+
*/
|
|
15
|
+
const neighbors = new Uint32Array(256);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Geodesic distance (along the surface) from the goal face to every reached vertex. Rebuilt per
|
|
19
|
+
* query; reused to avoid re-allocation.
|
|
20
|
+
* @type {Map<number, number>}
|
|
21
|
+
*/
|
|
22
|
+
const geodesic_distance = new Map();
|
|
9
23
|
|
|
10
24
|
/**
|
|
25
|
+
* Vertices whose geodesic distance has been finalised by the Fast Marching pass.
|
|
11
26
|
* @type {Set<number>}
|
|
12
27
|
*/
|
|
13
|
-
const
|
|
28
|
+
const frozen_vertices = new Set();
|
|
14
29
|
|
|
15
30
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @type {
|
|
31
|
+
* Faces already settled by the corridor trace / fallback search. Reused across passes.
|
|
32
|
+
* @type {Set<number>}
|
|
33
|
+
*/
|
|
34
|
+
const visited_faces = new Set();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Priority queue, reused by the Fast Marching fill (ordered by f = distance + heuristic) and by the
|
|
38
|
+
* fallback corridor search (ordered by distance to goal).
|
|
39
|
+
* @type {Uint32Heap}
|
|
18
40
|
*/
|
|
19
|
-
const
|
|
41
|
+
const open = new Uint32Heap();
|
|
20
42
|
|
|
21
43
|
/**
|
|
22
|
-
* Reconstruction parent pointers: face -> the face we reached it from
|
|
44
|
+
* Reconstruction parent pointers for the fallback search: face -> the face we reached it from.
|
|
23
45
|
* @type {Map<number, number>}
|
|
24
46
|
*/
|
|
25
47
|
const came_from = new Map();
|
|
26
48
|
|
|
49
|
+
const scratch_face_vertices = [0, 0, 0];
|
|
50
|
+
|
|
51
|
+
// scratch vertex position used by the Fast Marching pass (kept apart from the trace's pos_* scratch)
|
|
52
|
+
const scratch_vertex_pos = new Float64Array(3);
|
|
53
|
+
|
|
54
|
+
// the three vertex positions of the face currently being traced
|
|
55
|
+
const pos_a = new Float64Array(3);
|
|
56
|
+
const pos_b = new Float64Array(3);
|
|
57
|
+
const pos_c = new Float64Array(3);
|
|
58
|
+
|
|
59
|
+
// in-plane orthonormal basis of the current face (origin at vertex A)
|
|
60
|
+
const basis_u = new Float64Array(3);
|
|
61
|
+
const basis_v = new Float64Array(3);
|
|
62
|
+
|
|
63
|
+
// the goal-distance field value at each vertex of the current face
|
|
64
|
+
const scratch_field = new Float64Array(3);
|
|
65
|
+
|
|
66
|
+
const scratch_centroid = new Float64Array(3);
|
|
67
|
+
|
|
68
|
+
// communicates the ray parameter of the chosen exit edge out of pick_exit_edge (avoids allocation)
|
|
69
|
+
let last_exit_t = 0;
|
|
70
|
+
|
|
71
|
+
// number of vertices the last Fast Marching pass froze; bounds the corridor trace's iteration count
|
|
72
|
+
let frozen_count = 0;
|
|
73
|
+
|
|
27
74
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
75
|
+
* Read the three vertex IDs of a triangular face into `out` (length 3). Faces are assumed triangular.
|
|
76
|
+
*
|
|
77
|
+
* @param {number[]} out
|
|
78
|
+
* @param {BinaryTopology} topology
|
|
79
|
+
* @param {number} face
|
|
80
|
+
* @returns {boolean} false if the face has no loop (uninitialised / out of bounds)
|
|
30
81
|
*/
|
|
31
|
-
|
|
82
|
+
function face_read_vertices(out, topology, face) {
|
|
83
|
+
const loop_a = topology.face_read_loop(face);
|
|
84
|
+
|
|
85
|
+
if (loop_a === NULL_POINTER) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
32
88
|
|
|
33
|
-
const
|
|
89
|
+
const loop_b = topology.loop_read_next(loop_a);
|
|
90
|
+
const loop_c = topology.loop_read_next(loop_b);
|
|
91
|
+
|
|
92
|
+
out[0] = topology.loop_read_vertex(loop_a);
|
|
93
|
+
out[1] = topology.loop_read_vertex(loop_b);
|
|
94
|
+
out[2] = topology.loop_read_vertex(loop_c);
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Straight-line distance between two vertices.
|
|
101
|
+
*/
|
|
102
|
+
function vertex_distance(topology, a, b) {
|
|
103
|
+
topology.vertex_read_coordinate(scratch_vertex_pos, 0, a);
|
|
104
|
+
const ax = scratch_vertex_pos[0], ay = scratch_vertex_pos[1], az = scratch_vertex_pos[2];
|
|
105
|
+
|
|
106
|
+
topology.vertex_read_coordinate(scratch_vertex_pos, 0, b);
|
|
107
|
+
const dx = ax - scratch_vertex_pos[0], dy = ay - scratch_vertex_pos[1], dz = az - scratch_vertex_pos[2];
|
|
108
|
+
|
|
109
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Straight-line distance from a vertex to a point. Admissible/consistent heuristic for the geodesic
|
|
114
|
+
* (true geodesic distance is never shorter than the straight line), used to make the fill goal-directed.
|
|
115
|
+
*/
|
|
116
|
+
function vertex_distance_to_point(topology, vertex, px, py, pz) {
|
|
117
|
+
topology.vertex_read_coordinate(scratch_vertex_pos, 0, vertex);
|
|
118
|
+
const dx = scratch_vertex_pos[0] - px, dy = scratch_vertex_pos[1] - py, dz = scratch_vertex_pos[2] - pz;
|
|
119
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The vertex of `face` that is neither `a` nor `b`, or -1 if the face does not have one.
|
|
124
|
+
*/
|
|
125
|
+
function third_vertex(topology, face, a, b) {
|
|
126
|
+
let loop = topology.face_read_loop(face);
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < 3; i++) {
|
|
129
|
+
const v = topology.loop_read_vertex(loop);
|
|
130
|
+
|
|
131
|
+
if (v !== a && v !== b) {
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
loop = topology.loop_read_next(loop);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
34
140
|
|
|
35
141
|
/**
|
|
36
|
-
*
|
|
142
|
+
* Solve the geodesic distance field outward from the goal face using Fast Marching (the exact Eikonal
|
|
143
|
+
* triangle update). The field is goal-directed: the heap is ordered by `distance + h`, where `h` is the
|
|
144
|
+
* straight-line distance to the start centroid, so the wavefront sweeps toward the start (exploring the
|
|
145
|
+
* start<->goal "ellipse" rather than the whole component). With `early_terminate` it stops the instant
|
|
146
|
+
* all three start-face vertices are finalised -- by then every vertex on the geodesic corridor (all of
|
|
147
|
+
* which are nearer the goal, hence finalised earlier) is resolved.
|
|
37
148
|
*
|
|
38
|
-
*
|
|
39
|
-
* the goal). Because, by the triangle inequality, the straight-line distance from any face to the
|
|
40
|
-
* goal is never greater than the summed centroid-to-centroid distance of an actual face path between
|
|
41
|
-
* them, the heuristic is admissible AND consistent. A* therefore returns a shortest-distance corridor
|
|
42
|
-
* (under the centroid metric) rather than a fewest-faces one.
|
|
149
|
+
* The heap uses lazy deletion (stale entries are skipped on pop) instead of an O(n) decrease-key.
|
|
43
150
|
*
|
|
44
|
-
* @param {number} a
|
|
45
|
-
* @param {number} b
|
|
46
151
|
* @param {BinaryTopology} topology
|
|
47
|
-
* @
|
|
152
|
+
* @param {number} goal_face seed of the wavefront (distance 0)
|
|
153
|
+
* @param {number} start_face heuristic target / early-termination target
|
|
154
|
+
* @param {boolean} early_terminate
|
|
155
|
+
* @returns {boolean} true if the start face was reached (a path exists), false otherwise
|
|
48
156
|
*/
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
157
|
+
function solve_geodesic_field(topology, goal_face, start_face, early_terminate) {
|
|
158
|
+
geodesic_distance.clear();
|
|
159
|
+
frozen_vertices.clear();
|
|
160
|
+
open.clear();
|
|
161
|
+
|
|
162
|
+
// start-face vertices (early-termination target) and centroid (heuristic target)
|
|
163
|
+
face_read_vertices(scratch_face_vertices, topology, start_face);
|
|
164
|
+
const start_v0 = scratch_face_vertices[0];
|
|
165
|
+
const start_v1 = scratch_face_vertices[1];
|
|
166
|
+
const start_v2 = scratch_face_vertices[2];
|
|
167
|
+
|
|
168
|
+
topology.vertex_read_coordinate(pos_a, 0, start_v0);
|
|
169
|
+
topology.vertex_read_coordinate(pos_b, 0, start_v1);
|
|
170
|
+
topology.vertex_read_coordinate(pos_c, 0, start_v2);
|
|
171
|
+
const start_x = (pos_a[0] + pos_b[0] + pos_c[0]) / 3;
|
|
172
|
+
const start_y = (pos_a[1] + pos_b[1] + pos_c[1]) / 3;
|
|
173
|
+
const start_z = (pos_a[2] + pos_b[2] + pos_c[2]) / 3;
|
|
174
|
+
|
|
175
|
+
// seed the goal-face vertices at distance 0
|
|
176
|
+
face_read_vertices(scratch_face_vertices, topology, goal_face);
|
|
177
|
+
for (let i = 0; i < 3; i++) {
|
|
178
|
+
const gv = scratch_face_vertices[i];
|
|
179
|
+
if (!geodesic_distance.has(gv)) {
|
|
180
|
+
geodesic_distance.set(gv, 0);
|
|
181
|
+
open.insert(gv, vertex_distance_to_point(topology, gv, start_x, start_y, start_z));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
52
184
|
|
|
53
|
-
|
|
54
|
-
const dy = scratch_array_f32[1] - scratch_array_f32[4];
|
|
55
|
-
const dz = scratch_array_f32[2] - scratch_array_f32[5];
|
|
185
|
+
frozen_count = 0;
|
|
56
186
|
|
|
57
|
-
|
|
187
|
+
while (!open.is_empty()) {
|
|
188
|
+
const v = open.pop_min();
|
|
189
|
+
|
|
190
|
+
if (frozen_vertices.has(v)) {
|
|
191
|
+
continue; // stale heap entry (lazy deletion)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
frozen_vertices.add(v);
|
|
195
|
+
frozen_count++;
|
|
196
|
+
|
|
197
|
+
if (early_terminate
|
|
198
|
+
&& (v === start_v0 || v === start_v1 || v === start_v2)
|
|
199
|
+
&& frozen_vertices.has(start_v0) && frozen_vertices.has(start_v1) && frozen_vertices.has(start_v2)
|
|
200
|
+
) {
|
|
201
|
+
return true; // start face fully resolved; the whole corridor is finalised
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const g_v = geodesic_distance.get(v);
|
|
205
|
+
|
|
206
|
+
// walk v's disk cycle (every edge incident to v)
|
|
207
|
+
const edge_first = topology.vertex_read_edge(v);
|
|
208
|
+
if (edge_first === NULL_POINTER) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let edge = edge_first;
|
|
213
|
+
do {
|
|
214
|
+
const ev1 = topology.edge_read_vertex1(edge);
|
|
215
|
+
const ev2 = topology.edge_read_vertex2(edge);
|
|
216
|
+
const v_is_first = ev1 === v;
|
|
217
|
+
const w = v_is_first ? ev2 : ev1;
|
|
218
|
+
|
|
219
|
+
if (frozen_vertices.has(w)) {
|
|
220
|
+
// both ends of this edge are finalised -> propagate the wavefront across each incident
|
|
221
|
+
// triangle to its third vertex with the exact Eikonal update
|
|
222
|
+
const g_w = geodesic_distance.get(w);
|
|
223
|
+
|
|
224
|
+
const loop_first = topology.edge_read_loop(edge);
|
|
225
|
+
if (loop_first !== NULL_POINTER) {
|
|
226
|
+
let loop = loop_first;
|
|
227
|
+
do {
|
|
228
|
+
const face = topology.loop_read_face(loop);
|
|
229
|
+
const t = third_vertex(topology, face, ev1, ev2);
|
|
230
|
+
|
|
231
|
+
if (t !== -1 && !frozen_vertices.has(t)) {
|
|
232
|
+
const d = bt_mesh_compute_edge_distance_eikonal(topology, v, w, g_v, g_w, t);
|
|
233
|
+
const existing = geodesic_distance.get(t);
|
|
234
|
+
|
|
235
|
+
if (existing === undefined || d < existing) {
|
|
236
|
+
geodesic_distance.set(t, d);
|
|
237
|
+
open.insert(t, d + vertex_distance_to_point(topology, t, start_x, start_y, start_z));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
loop = topology.loop_read_radial_next(loop);
|
|
242
|
+
} while (loop !== loop_first && loop !== NULL_POINTER);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// graph-distance relaxation, so w gets a value even before a finalised front forms across
|
|
246
|
+
// it; a later Eikonal update lowers it to the true geodesic before w itself is frozen
|
|
247
|
+
const d = g_v + vertex_distance(topology, v, w);
|
|
248
|
+
const existing = geodesic_distance.get(w);
|
|
249
|
+
|
|
250
|
+
if (existing === undefined || d < existing) {
|
|
251
|
+
geodesic_distance.set(w, d);
|
|
252
|
+
open.insert(w, d + vertex_distance_to_point(topology, w, start_x, start_y, start_z));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
edge = v_is_first ? topology.edge_read_v1_disk_next(edge) : topology.edge_read_v2_disk_next(edge);
|
|
257
|
+
} while (edge !== edge_first && edge !== NULL_POINTER);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// the heap drained: if we were early-terminating, the start was never reached. Either way, report
|
|
261
|
+
// whether the start face's vertices ended up finalised (a full solve still answers reachability).
|
|
262
|
+
return frozen_vertices.has(start_v0) && frozen_vertices.has(start_v1) && frozen_vertices.has(start_v2);
|
|
58
263
|
}
|
|
59
264
|
|
|
60
265
|
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
266
|
+
* The face on the far side of `loop`'s edge, or {@link NULL_POINTER} if the edge is a boundary.
|
|
267
|
+
*/
|
|
268
|
+
function loop_opposite_face(topology, loop, face) {
|
|
269
|
+
let radial = topology.loop_read_radial_next(loop);
|
|
270
|
+
|
|
271
|
+
while (radial !== loop) {
|
|
272
|
+
const f = topology.loop_read_face(radial);
|
|
273
|
+
|
|
274
|
+
if (f !== face) {
|
|
275
|
+
return f;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
radial = topology.loop_read_radial_next(radial);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return NULL_POINTER;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Average of a face's three vertices' distances to the goal. The average (rather than the minimum)
|
|
286
|
+
* keeps a single near-goal vertex of an oversized triangle from making the whole face look close.
|
|
287
|
+
* Returns Infinity if any vertex was not reached by the fill.
|
|
288
|
+
*/
|
|
289
|
+
function face_geodesic_distance(topology, face) {
|
|
290
|
+
if (!face_read_vertices(scratch_face_vertices, topology, face)) {
|
|
291
|
+
return Infinity;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const da = geodesic_distance.get(scratch_face_vertices[0]);
|
|
295
|
+
const db = geodesic_distance.get(scratch_face_vertices[1]);
|
|
296
|
+
const dc = geodesic_distance.get(scratch_face_vertices[2]);
|
|
297
|
+
|
|
298
|
+
if (da === undefined || db === undefined || dc === undefined) {
|
|
299
|
+
return Infinity;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return (da + db + dc) / 3;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Load the geometry of `face` into module scratch: the three vertex positions, the goal-distance field
|
|
307
|
+
* value at each vertex, and an in-plane orthonormal basis (origin at vertex A, x-axis along A->B).
|
|
308
|
+
*/
|
|
309
|
+
function load_face_geometry(topology, face) {
|
|
310
|
+
face_read_vertices(scratch_face_vertices, topology, face);
|
|
311
|
+
|
|
312
|
+
topology.vertex_read_coordinate(pos_a, 0, scratch_face_vertices[0]);
|
|
313
|
+
topology.vertex_read_coordinate(pos_b, 0, scratch_face_vertices[1]);
|
|
314
|
+
topology.vertex_read_coordinate(pos_c, 0, scratch_face_vertices[2]);
|
|
315
|
+
|
|
316
|
+
scratch_field[0] = geodesic_distance.get(scratch_face_vertices[0]);
|
|
317
|
+
scratch_field[1] = geodesic_distance.get(scratch_face_vertices[1]);
|
|
318
|
+
scratch_field[2] = geodesic_distance.get(scratch_face_vertices[2]);
|
|
319
|
+
|
|
320
|
+
const e1x = pos_b[0] - pos_a[0], e1y = pos_b[1] - pos_a[1], e1z = pos_b[2] - pos_a[2];
|
|
321
|
+
const e2x = pos_c[0] - pos_a[0], e2y = pos_c[1] - pos_a[1], e2z = pos_c[2] - pos_a[2];
|
|
322
|
+
|
|
323
|
+
const e1_len = Math.hypot(e1x, e1y, e1z);
|
|
324
|
+
basis_u[0] = e1x / e1_len;
|
|
325
|
+
basis_u[1] = e1y / e1_len;
|
|
326
|
+
basis_u[2] = e1z / e1_len;
|
|
327
|
+
|
|
328
|
+
// n = e1 x e2 (face normal); v = normalize(n x u) completes the in-plane right-handed basis
|
|
329
|
+
const nx = e1y * e2z - e1z * e2y;
|
|
330
|
+
const ny = e1z * e2x - e1x * e2z;
|
|
331
|
+
const nz = e1x * e2y - e1y * e2x;
|
|
332
|
+
|
|
333
|
+
const vx = ny * basis_u[2] - nz * basis_u[1];
|
|
334
|
+
const vy = nz * basis_u[0] - nx * basis_u[2];
|
|
335
|
+
const vz = nx * basis_u[1] - ny * basis_u[0];
|
|
336
|
+
const v_len = Math.hypot(vx, vy, vz);
|
|
337
|
+
basis_v[0] = vx / v_len;
|
|
338
|
+
basis_v[1] = vy / v_len;
|
|
339
|
+
basis_v[2] = vz / v_len;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Project a world point onto the current face basis x-axis (origin = vertex A). */
|
|
343
|
+
function project_x(px, py, pz) {
|
|
344
|
+
return (px - pos_a[0]) * basis_u[0] + (py - pos_a[1]) * basis_u[1] + (pz - pos_a[2]) * basis_u[2];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Project a world point onto the current face basis y-axis (origin = vertex A). */
|
|
348
|
+
function project_y(px, py, pz) {
|
|
349
|
+
return (px - pos_a[0]) * basis_v[0] + (py - pos_a[1]) * basis_v[1] + (pz - pos_a[2]) * basis_v[2];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Parameter `t` at which ray (ox,oy)+t*(dx,dy) crosses segment (e0)->(e1) within the segment, or
|
|
354
|
+
* Infinity if it does not cross ahead of the origin.
|
|
355
|
+
*/
|
|
356
|
+
function ray_segment_t(ox, oy, dx, dy, e0x, e0y, e1x, e1y) {
|
|
357
|
+
const sx = e1x - e0x;
|
|
358
|
+
const sy = e1y - e0y;
|
|
359
|
+
|
|
360
|
+
const denom = dx * sy - dy * sx;
|
|
361
|
+
|
|
362
|
+
if (denom === 0) {
|
|
363
|
+
return Infinity; // parallel
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const wx = e0x - ox;
|
|
367
|
+
const wy = e0y - oy;
|
|
368
|
+
|
|
369
|
+
const t = (wx * sy - wy * sx) / denom; // distance along the ray
|
|
370
|
+
const u = (wx * dy - wy * dx) / denom; // position along the segment
|
|
371
|
+
|
|
372
|
+
if (t > 1e-9 && u >= -1e-9 && u <= 1 + 1e-9) {
|
|
373
|
+
return t;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return Infinity;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Among the three edges of a triangle (in the face's 2D basis), find the one the ray (ox,oy)+t*(dx,dy)
|
|
381
|
+
* exits through (smallest t > 0), and return the face on its far side. Stores the chosen t in
|
|
382
|
+
* {@link last_exit_t}.
|
|
383
|
+
*/
|
|
384
|
+
function pick_exit_edge(
|
|
385
|
+
ox, oy, dx, dy,
|
|
386
|
+
bx, cx, cy,
|
|
387
|
+
topology, face, loop_ab, loop_bc, loop_ca
|
|
388
|
+
) {
|
|
389
|
+
let best_t = Infinity;
|
|
390
|
+
let best_face = NULL_POINTER;
|
|
391
|
+
|
|
392
|
+
let t = ray_segment_t(ox, oy, dx, dy, 0, 0, bx, 0); // edge AB
|
|
393
|
+
if (t < best_t) { best_t = t; best_face = loop_opposite_face(topology, loop_ab, face); }
|
|
394
|
+
|
|
395
|
+
t = ray_segment_t(ox, oy, dx, dy, bx, 0, cx, cy); // edge BC
|
|
396
|
+
if (t < best_t) { best_t = t; best_face = loop_opposite_face(topology, loop_bc, face); }
|
|
397
|
+
|
|
398
|
+
t = ray_segment_t(ox, oy, dx, dy, cx, cy, 0, 0); // edge CA
|
|
399
|
+
if (t < best_t) { best_t = t; best_face = loop_opposite_face(topology, loop_ca, face); }
|
|
400
|
+
|
|
401
|
+
last_exit_t = best_t;
|
|
402
|
+
return best_face;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The unvisited neighbour face with the smallest average goal-distance. Used as a robust fallback when
|
|
407
|
+
* the gradient march cannot pick a forward exit edge.
|
|
408
|
+
*/
|
|
409
|
+
function lowest_unvisited_neighbour(topology, face) {
|
|
410
|
+
const count = bt_face_get_neighbour_faces(neighbors, 0, topology, face);
|
|
411
|
+
|
|
412
|
+
let best_face = NULL_POINTER;
|
|
413
|
+
let best_distance = Infinity;
|
|
414
|
+
|
|
415
|
+
for (let i = 0; i < count; i++) {
|
|
416
|
+
const neighbor = neighbors[i];
|
|
417
|
+
|
|
418
|
+
if (visited_faces.has(neighbor)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const distance = face_geodesic_distance(topology, neighbor);
|
|
423
|
+
|
|
424
|
+
if (distance < best_distance) {
|
|
425
|
+
best_distance = distance;
|
|
426
|
+
best_face = neighbor;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return best_face;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Write the centroid of a triangular face into {@link scratch_centroid}. */
|
|
434
|
+
function face_centroid(topology, face) {
|
|
435
|
+
face_read_vertices(scratch_face_vertices, topology, face);
|
|
436
|
+
|
|
437
|
+
topology.vertex_read_coordinate(pos_a, 0, scratch_face_vertices[0]);
|
|
438
|
+
topology.vertex_read_coordinate(pos_b, 0, scratch_face_vertices[1]);
|
|
439
|
+
topology.vertex_read_coordinate(pos_c, 0, scratch_face_vertices[2]);
|
|
440
|
+
|
|
441
|
+
scratch_centroid[0] = (pos_a[0] + pos_b[0] + pos_c[0]) / 3;
|
|
442
|
+
scratch_centroid[1] = (pos_a[1] + pos_b[1] + pos_c[1]) / 3;
|
|
443
|
+
scratch_centroid[2] = (pos_a[2] + pos_b[2] + pos_c[2]) / 3;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Trace the steepest descent of the geodesic field from the start face to the goal face, writing the
|
|
448
|
+
* face corridor into `output` in START -> GOAL order. Following the in-plane gradient downhill crosses
|
|
449
|
+
* exactly the faces the true geodesic does, so the corridor admits the shortest path. Returns 0 if the
|
|
450
|
+
* descent cannot complete (e.g. it has to wind around a hole, where the field's gradient is no longer
|
|
451
|
+
* a reliable guide) -- the caller then falls back to a graph search.
|
|
64
452
|
*
|
|
65
|
-
* @
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
453
|
+
* @returns {number} faces written, or 0 on failure / if the corridor exceeds `max_path_length`
|
|
454
|
+
*/
|
|
455
|
+
function trace_corridor(output, topology, start_face, goal_face, max_path_length) {
|
|
456
|
+
visited_faces.clear();
|
|
457
|
+
|
|
458
|
+
face_centroid(topology, start_face);
|
|
459
|
+
let world_x = scratch_centroid[0];
|
|
460
|
+
let world_y = scratch_centroid[1];
|
|
461
|
+
let world_z = scratch_centroid[2];
|
|
462
|
+
|
|
463
|
+
let current = start_face;
|
|
464
|
+
let previous = NULL_POINTER;
|
|
465
|
+
let length = 0;
|
|
466
|
+
|
|
467
|
+
// each step adds a fresh face; the count of finalised vertices bounds the reachable faces
|
|
468
|
+
const iteration_cap = frozen_count * 2 + 8;
|
|
469
|
+
|
|
470
|
+
for (let iteration = 0; iteration < iteration_cap; iteration++) {
|
|
471
|
+
if (length >= max_path_length) {
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
output[length] = current;
|
|
476
|
+
length++;
|
|
477
|
+
|
|
478
|
+
if (current === goal_face) {
|
|
479
|
+
return length;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
visited_faces.add(current);
|
|
483
|
+
|
|
484
|
+
load_face_geometry(topology, current);
|
|
485
|
+
|
|
486
|
+
const ga = scratch_field[0], gb = scratch_field[1], gc = scratch_field[2];
|
|
487
|
+
|
|
488
|
+
const px = project_x(world_x, world_y, world_z);
|
|
489
|
+
const py = project_y(world_x, world_y, world_z);
|
|
490
|
+
|
|
491
|
+
const bx = project_x(pos_b[0], pos_b[1], pos_b[2]); // B.y == 0 by construction
|
|
492
|
+
const cx = project_x(pos_c[0], pos_c[1], pos_c[2]);
|
|
493
|
+
const cy = project_y(pos_c[0], pos_c[1], pos_c[2]);
|
|
494
|
+
|
|
495
|
+
// in-plane gradient of the (linear) field; descend it (negative gradient points at the goal)
|
|
496
|
+
const grad_x = (gb - ga) / bx;
|
|
497
|
+
const grad_y = (gc - ga - grad_x * cx) / cy;
|
|
498
|
+
|
|
499
|
+
let dir_x = -grad_x;
|
|
500
|
+
let dir_y = -grad_y;
|
|
501
|
+
const dir_len = Math.hypot(dir_x, dir_y);
|
|
502
|
+
|
|
503
|
+
let next = NULL_POINTER;
|
|
504
|
+
|
|
505
|
+
if (dir_len > 1e-12) {
|
|
506
|
+
dir_x /= dir_len;
|
|
507
|
+
dir_y /= dir_len;
|
|
508
|
+
|
|
509
|
+
const loop_a = topology.face_read_loop(current);
|
|
510
|
+
const loop_b = topology.loop_read_next(loop_a);
|
|
511
|
+
const loop_c = topology.loop_read_next(loop_b);
|
|
512
|
+
|
|
513
|
+
next = pick_exit_edge(px, py, dir_x, dir_y, bx, cx, cy, topology, current, loop_a, loop_b, loop_c);
|
|
514
|
+
|
|
515
|
+
if (next !== NULL_POINTER && next !== previous && !visited_faces.has(next)) {
|
|
516
|
+
// follow the gradient: re-enter the next face at the exit point (on the shared edge)
|
|
517
|
+
const exit_x = px + dir_x * last_exit_t;
|
|
518
|
+
const exit_y = py + dir_y * last_exit_t;
|
|
519
|
+
|
|
520
|
+
world_x = pos_a[0] + exit_x * basis_u[0] + exit_y * basis_v[0];
|
|
521
|
+
world_y = pos_a[1] + exit_x * basis_u[1] + exit_y * basis_v[1];
|
|
522
|
+
world_z = pos_a[2] + exit_x * basis_u[2] + exit_y * basis_v[2];
|
|
523
|
+
|
|
524
|
+
previous = current;
|
|
525
|
+
current = next;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// the gradient gave no usable forward edge (a boundary/hole, a vertex hit, or it pointed into
|
|
531
|
+
// an already-traced face). Fall back to the lowest-field unvisited neighbour, re-entering at
|
|
532
|
+
// its centroid. If that happens we are near a hole, so the caller will likely re-run as a
|
|
533
|
+
// graph search; here we just keep the corridor connected.
|
|
534
|
+
next = lowest_unvisited_neighbour(topology, current);
|
|
535
|
+
|
|
536
|
+
if (next === NULL_POINTER) {
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
face_centroid(topology, next);
|
|
541
|
+
world_x = scratch_centroid[0];
|
|
542
|
+
world_y = scratch_centroid[1];
|
|
543
|
+
world_z = scratch_centroid[2];
|
|
544
|
+
|
|
545
|
+
previous = current;
|
|
546
|
+
current = next;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Walk parent pointers from goal back to start, writing the face sequence into `output` in
|
|
554
|
+
* START -> GOAL order, or 0 if the corridor exceeds `max_path_length`.
|
|
555
|
+
*/
|
|
556
|
+
function construct_path(output, start_face, goal_face, max_path_length) {
|
|
73
557
|
let length = 0;
|
|
74
558
|
let current = goal_face;
|
|
75
559
|
|
|
76
|
-
// walk back to the start, writing in reverse (goal -> start)
|
|
77
560
|
while (true) {
|
|
78
561
|
if (length >= max_path_length) {
|
|
79
|
-
// corridor does not fit the output buffer; refuse rather than writing out of bounds
|
|
80
562
|
return 0;
|
|
81
563
|
}
|
|
82
564
|
|
|
@@ -90,7 +572,6 @@ function construct_path(output, start_face, goal_face, came_from, max_path_lengt
|
|
|
90
572
|
current = came_from.get(current);
|
|
91
573
|
}
|
|
92
574
|
|
|
93
|
-
// reverse in place to get START -> GOAL order
|
|
94
575
|
const half_length = length >> 1;
|
|
95
576
|
|
|
96
577
|
for (let i = 0; i < half_length; i++) {
|
|
@@ -104,11 +585,84 @@ function construct_path(output, start_face, goal_face, came_from, max_path_lengt
|
|
|
104
585
|
return length;
|
|
105
586
|
}
|
|
106
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Fallback corridor finder: a best-first walk over faces, always expanding the one currently closest
|
|
590
|
+
* to the goal on the geodesic field. Always finds a corridor when one exists within the resolved field
|
|
591
|
+
* (it routes around holes the gradient trace could not), at the cost of not necessarily hugging the
|
|
592
|
+
* exact geodesic.
|
|
593
|
+
*
|
|
594
|
+
* @returns {number} faces written, or 0 if no path / it exceeds `max_path_length`
|
|
595
|
+
*/
|
|
596
|
+
function search_corridor(output, topology, start_face, goal_face, max_path_length) {
|
|
597
|
+
open.clear();
|
|
598
|
+
came_from.clear();
|
|
599
|
+
visited_faces.clear();
|
|
600
|
+
|
|
601
|
+
open.insert(start_face, face_geodesic_distance(topology, start_face));
|
|
602
|
+
visited_faces.add(start_face);
|
|
603
|
+
|
|
604
|
+
while (!open.is_empty()) {
|
|
605
|
+
const current = open.pop_min();
|
|
606
|
+
|
|
607
|
+
if (current === goal_face) {
|
|
608
|
+
return construct_path(output, start_face, goal_face, max_path_length);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const count = bt_face_get_neighbour_faces(neighbors, 0, topology, current);
|
|
612
|
+
|
|
613
|
+
for (let i = 0; i < count; i++) {
|
|
614
|
+
const neighbor = neighbors[i];
|
|
615
|
+
|
|
616
|
+
if (visited_faces.has(neighbor)) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const distance = face_geodesic_distance(topology, neighbor);
|
|
621
|
+
|
|
622
|
+
if (distance === Infinity) {
|
|
623
|
+
continue; // outside the resolved field
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
visited_faces.add(neighbor);
|
|
627
|
+
came_from.set(neighbor, current);
|
|
628
|
+
open.insert(neighbor, distance);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Extract the corridor from the resolved field: gradient trace first (hugs the geodesic), graph-walk
|
|
637
|
+
* fallback for winding around holes.
|
|
638
|
+
*
|
|
639
|
+
* @returns {number} faces written, or 0 if neither method produced a corridor
|
|
640
|
+
*/
|
|
641
|
+
function extract_corridor(output, topology, start_face, goal_face, max_path_length) {
|
|
642
|
+
const traced = trace_corridor(output, topology, start_face, goal_face, max_path_length);
|
|
643
|
+
|
|
644
|
+
if (traced !== 0) {
|
|
645
|
+
return traced;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return search_corridor(output, topology, start_face, goal_face, max_path_length);
|
|
649
|
+
}
|
|
107
650
|
|
|
108
651
|
/**
|
|
109
652
|
* Find a shortest path through topology faces.
|
|
110
653
|
* If a path is found - the result will contain start and goal faces.
|
|
111
654
|
*
|
|
655
|
+
* The corridor is steered by an exact-geodesic distance field rather than a face-graph metric. A
|
|
656
|
+
* goal-directed Fast Marching pass (exact Eikonal triangle update) solves the true along-surface
|
|
657
|
+
* distance-to-goal at each vertex, sweeping toward the start and stopping as soon as the start face is
|
|
658
|
+
* resolved. Because the field measures the real walked distance (not centroid-to-centroid hops), the
|
|
659
|
+
* corridor heads straight at the goal and crosses a finely tessellated patch instead of skirting around
|
|
660
|
+
* it. The corridor is then extracted by descending the field's gradient from the start (which hugs the
|
|
661
|
+
* geodesic, so the downstream funnel/string-pull recovers the exact shortest path); where the gradient
|
|
662
|
+
* is an unreliable guide -- winding around a hole -- a best-first graph walk on the same field takes
|
|
663
|
+
* over. A full (non-early-terminated) solve is used as a backstop for the rare case the bounded fill
|
|
664
|
+
* left the corridor under-resolved.
|
|
665
|
+
*
|
|
112
666
|
* NOTE: if either start or goal faces are not part of the topology - an empty path will be produced.
|
|
113
667
|
*
|
|
114
668
|
* @param {number[]|Uint32Array} output path will be written here as a sequence of face IDs
|
|
@@ -135,73 +689,42 @@ export function bt_mesh_face_find_path(
|
|
|
135
689
|
assert.defined(topology, 'topology');
|
|
136
690
|
assert.equal(topology.isBinaryTopology, true, 'topology.isBinaryTopology !== true');
|
|
137
691
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
g_score.set(start_face_id, 0);
|
|
144
|
-
|
|
145
|
-
open.insert(start_face_id, face_centroid_distance(start_face_id, goal_face_id, topology));
|
|
146
|
-
|
|
147
|
-
while (!open.is_empty()) {
|
|
148
|
-
|
|
149
|
-
const current_node = open.pop_min();
|
|
150
|
-
|
|
151
|
-
// Lazy deletion: a face may sit in the heap multiple times if its g-score improved after an
|
|
152
|
-
// earlier push. The lowest-f entry is popped first; later popped duplicates land on an
|
|
153
|
-
// already-closed face and must be skipped.
|
|
154
|
-
if (closed.has(current_node)) {
|
|
155
|
-
continue;
|
|
692
|
+
if (start_face_id === goal_face_id) {
|
|
693
|
+
// trivial path; still validate the face actually exists
|
|
694
|
+
if (!topology.faces.is_allocated(start_face_id)) {
|
|
695
|
+
return 0;
|
|
156
696
|
}
|
|
157
697
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
return construct_path(output, start_face_id, goal_face_id, came_from, max_path_length);
|
|
698
|
+
if (max_path_length < 1) {
|
|
699
|
+
return 0;
|
|
161
700
|
}
|
|
162
701
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const neighbor_count = bt_face_get_neighbour_faces(neighbors, 0, topology, current_node);
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < neighbor_count; i++) {
|
|
170
|
-
|
|
171
|
-
const neighbor = neighbors[i];
|
|
172
|
-
|
|
173
|
-
if (closed.has(neighbor)) {
|
|
174
|
-
// already closed
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// step cost is the real geometric distance between adjacent face centroids, so the
|
|
179
|
-
// search minimises corridor length rather than the number of triangles crossed
|
|
180
|
-
const traversal_cost = face_centroid_distance(current_node, neighbor, topology);
|
|
181
|
-
|
|
182
|
-
const cost_so_far = current_g_score + traversal_cost;
|
|
183
|
-
|
|
184
|
-
if (!g_score.has(neighbor) || cost_so_far < g_score.get(neighbor)) {
|
|
185
|
-
// better path to this neighbour
|
|
186
|
-
|
|
187
|
-
g_score.set(neighbor, cost_so_far);
|
|
188
|
-
came_from.set(neighbor, current_node);
|
|
189
|
-
|
|
190
|
-
const remaining_heuristic = face_centroid_distance(neighbor, goal_face_id, topology);
|
|
191
|
-
|
|
192
|
-
const f_score = cost_so_far + remaining_heuristic;
|
|
702
|
+
output[0] = start_face_id;
|
|
703
|
+
return 1;
|
|
704
|
+
}
|
|
193
705
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
706
|
+
// both faces must be part of the topology
|
|
707
|
+
if (!topology.faces.is_allocated(start_face_id) || !topology.faces.is_allocated(goal_face_id)) {
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
198
710
|
|
|
199
|
-
|
|
711
|
+
// Fast path: goal-directed fill bounded to the start<->goal region. When the start is unreachable
|
|
712
|
+
// this drains exhaustively (it never early-exits), so a false return is a definitive "no path" --
|
|
713
|
+
// no second solve needed.
|
|
714
|
+
if (!solve_geodesic_field(topology, goal_face_id, start_face_id, true)) {
|
|
715
|
+
return 0; // no path between the two faces
|
|
716
|
+
}
|
|
200
717
|
|
|
201
|
-
|
|
718
|
+
const corridor = extract_corridor(output, topology, start_face_id, goal_face_id, max_path_length);
|
|
202
719
|
|
|
720
|
+
if (corridor !== 0) {
|
|
721
|
+
return corridor;
|
|
203
722
|
}
|
|
204
723
|
|
|
205
|
-
//
|
|
206
|
-
|
|
724
|
+
// Backstop: the bounded fill resolved enough to reach the start but left the corridor
|
|
725
|
+
// under-resolved for the extractor (rare; a corridor that winds far around a hole). A full solve
|
|
726
|
+
// resolves the whole reachable component, then the extractor cannot fail for a connected pair.
|
|
727
|
+
solve_geodesic_field(topology, goal_face_id, start_face_id, false);
|
|
728
|
+
|
|
729
|
+
return extract_corridor(output, topology, start_face_id, goal_face_id, max_path_length);
|
|
207
730
|
}
|