@woosh/meep-engine 2.163.4 → 2.163.5
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/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 +163 -3
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js +95 -0
- package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js +4 -5
- package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/navmesh_build_topology.js +33 -23
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "Pure JavaScript game engine. Fully featured and production ready.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"author": "Alexander Goldring",
|
|
9
|
-
"version": "2.163.
|
|
9
|
+
"version": "2.163.5",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;GAYG;AACH,uEAHW,MAAM,EAAE,kBACR,MAAM,QAgehB"}
|
|
@@ -6,13 +6,34 @@ import {
|
|
|
6
6
|
} from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
|
|
7
7
|
import { NULL_POINTER } from "../BinaryTopology.js";
|
|
8
8
|
import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
|
|
9
|
-
import { bt_mesh_build_boundary_distance_field } from "../query/bt_mesh_build_boundary_distance_field.js";
|
|
10
9
|
import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
|
|
11
10
|
import { bt_edge_split } from "./edge/bt_edge_split.js";
|
|
12
11
|
import { bt_face_kill } from "./face/bt_face_kill.js";
|
|
13
12
|
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
|
|
14
13
|
import { bt_face_get_neighbour_faces } from "../query/bt_face_get_neighbour_faces.js";
|
|
15
14
|
|
|
15
|
+
const _eb_a = new Float32Array(3);
|
|
16
|
+
const _eb_b = new Float32Array(3);
|
|
17
|
+
const _eb_p = new Float32Array(3);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Squared straight-line distance from point p to segment a-b in 3D.
|
|
21
|
+
*/
|
|
22
|
+
function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
|
|
23
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
24
|
+
const apx = px - ax, apy = py - ay, apz = pz - az;
|
|
25
|
+
const ab_len_sq = abx * abx + aby * aby + abz * abz;
|
|
26
|
+
|
|
27
|
+
let t = ab_len_sq > 0 ? (apx * abx + apy * aby + apz * abz) / ab_len_sq : 0;
|
|
28
|
+
if (t < 0) t = 0; else if (t > 1) t = 1;
|
|
29
|
+
|
|
30
|
+
const cx = ax + t * abx - px;
|
|
31
|
+
const cy = ay + t * aby - py;
|
|
32
|
+
const cz = az + t * abz - pz;
|
|
33
|
+
|
|
34
|
+
return cx * cx + cy * cy + cz * cz;
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Shrinks an island of faces by a given distance.
|
|
18
39
|
* Conceptually, this moves the outline of the island inwards.
|
|
@@ -160,13 +181,152 @@ export function bt_mesh_face_island_erode(
|
|
|
160
181
|
}
|
|
161
182
|
}
|
|
162
183
|
|
|
163
|
-
// 3.
|
|
184
|
+
// 3. Distance field: EXACT straight-line distance from each island vertex to the nearest boundary
|
|
185
|
+
// edge. Agent clearance is a Euclidean (line-of-sight) distance to the nearest wall, NOT a geodesic
|
|
186
|
+
// distance measured along the surface. The previous eikonal field was both semantically wrong and
|
|
187
|
+
// sensitive to the triangulation - it produced a lumpy, non-uniform inset (e.g. a 14x14 platform
|
|
188
|
+
// built from strips eroded by 0.45-0.65 instead of a uniform 0.4). Exact distance is uniform.
|
|
164
189
|
/**
|
|
165
190
|
* Vertex -> Distance from boundary
|
|
166
191
|
* @type {Map<number, number>}
|
|
167
192
|
*/
|
|
168
193
|
const vertex_boundary_distances = new Map();
|
|
169
|
-
|
|
194
|
+
|
|
195
|
+
// boundary edge segments, cached by compute_field for reuse in the deviation test below
|
|
196
|
+
let boundary_segments = [];
|
|
197
|
+
|
|
198
|
+
// (re)compute the exact distance field for the current island vertices/boundary
|
|
199
|
+
function compute_field() {
|
|
200
|
+
vertex_boundary_distances.clear();
|
|
201
|
+
|
|
202
|
+
boundary_segments = [];
|
|
203
|
+
for (const e of boundary_edges_set) {
|
|
204
|
+
const bv1 = mesh.edge_read_vertex1(e);
|
|
205
|
+
const bv2 = mesh.edge_read_vertex2(e);
|
|
206
|
+
mesh.vertex_read_coordinate(_eb_a, 0, bv1);
|
|
207
|
+
mesh.vertex_read_coordinate(_eb_b, 0, bv2);
|
|
208
|
+
boundary_segments.push(_eb_a[0], _eb_a[1], _eb_a[2], _eb_b[0], _eb_b[1], _eb_b[2]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const v of island_vertices) {
|
|
212
|
+
mesh.vertex_read_coordinate(_eb_p, 0, v);
|
|
213
|
+
vertex_boundary_distances.set(v, dist_to_boundary(_eb_p[0], _eb_p[1], _eb_p[2]));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// exact straight-line distance from a point to the nearest boundary edge
|
|
218
|
+
function dist_to_boundary(px, py, pz) {
|
|
219
|
+
const seg = boundary_segments;
|
|
220
|
+
let best_sq = Infinity;
|
|
221
|
+
for (let i = 0; i < seg.length; i += 6) {
|
|
222
|
+
const d_sq = point_segment_distance_sq(
|
|
223
|
+
px, py, pz,
|
|
224
|
+
seg[i], seg[i + 1], seg[i + 2],
|
|
225
|
+
seg[i + 3], seg[i + 4], seg[i + 5]
|
|
226
|
+
);
|
|
227
|
+
if (d_sq < best_sq) best_sq = d_sq;
|
|
228
|
+
}
|
|
229
|
+
return best_sq === Infinity ? Infinity : Math.sqrt(best_sq);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
compute_field();
|
|
233
|
+
|
|
234
|
+
// 3a. CUT-BAND REFINEMENT. The inset contour is reconstructed piecewise-linearly from the faces the
|
|
235
|
+
// erosion threshold passes through, so on a coarse/skewed triangulation the cut chord deviates from
|
|
236
|
+
// the true offset (a strip-built platform eroded unevenly, 0.4 on one side and 0.6 on another). For
|
|
237
|
+
// each straddling face, place the two cut points and test whether their midpoint really lies at the
|
|
238
|
+
// erode distance; only refine (split the longest edge) when the chord deviates. Already-accurate
|
|
239
|
+
// cuts (e.g. a plain quad's straight sides) are left coarse, so detail is added only where needed.
|
|
240
|
+
{
|
|
241
|
+
const fine = Math.max(erode_distance * 0.5, 0.1);
|
|
242
|
+
const fine_sq = fine * fine;
|
|
243
|
+
const tol = Math.max(erode_distance * 0.15, 0.04);
|
|
244
|
+
let rounds = 0;
|
|
245
|
+
|
|
246
|
+
const ca = new Float32Array(3), cb = new Float32Array(3), cc = new Float32Array(3);
|
|
247
|
+
|
|
248
|
+
while (rounds < 48) {
|
|
249
|
+
rounds++;
|
|
250
|
+
|
|
251
|
+
const targets = new Set();
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < island_faces.length; i++) {
|
|
254
|
+
const f = island_faces[i];
|
|
255
|
+
if (!mesh.faces.is_allocated(f)) continue;
|
|
256
|
+
|
|
257
|
+
const la = mesh.face_read_loop(f);
|
|
258
|
+
const lb = mesh.loop_read_next(la);
|
|
259
|
+
const lc = mesh.loop_read_next(lb);
|
|
260
|
+
const v1 = mesh.loop_read_vertex(la);
|
|
261
|
+
const v2 = mesh.loop_read_vertex(lb);
|
|
262
|
+
const v3 = mesh.loop_read_vertex(lc);
|
|
263
|
+
|
|
264
|
+
const d1 = vertex_boundary_distances.get(v1);
|
|
265
|
+
const d2 = vertex_boundary_distances.get(v2);
|
|
266
|
+
const d3 = vertex_boundary_distances.get(v3);
|
|
267
|
+
|
|
268
|
+
const dmin = Math.min(d1, d2, d3);
|
|
269
|
+
const dmax = Math.max(d1, d2, d3);
|
|
270
|
+
|
|
271
|
+
// does the erode threshold pass through this face?
|
|
272
|
+
if (!(dmin < erode_distance && dmax > erode_distance)) continue;
|
|
273
|
+
|
|
274
|
+
mesh.vertex_read_coordinate(ca, 0, v1);
|
|
275
|
+
mesh.vertex_read_coordinate(cb, 0, v2);
|
|
276
|
+
mesh.vertex_read_coordinate(cc, 0, v3);
|
|
277
|
+
|
|
278
|
+
const e_ab = (ca[0] - cb[0]) ** 2 + (ca[1] - cb[1]) ** 2 + (ca[2] - cb[2]) ** 2;
|
|
279
|
+
const e_bc = (cb[0] - cc[0]) ** 2 + (cb[1] - cc[1]) ** 2 + (cb[2] - cc[2]) ** 2;
|
|
280
|
+
const e_ca = (cc[0] - ca[0]) ** 2 + (cc[1] - ca[1]) ** 2 + (cc[2] - ca[2]) ** 2;
|
|
281
|
+
const longest = Math.max(e_ab, e_bc, e_ca);
|
|
282
|
+
|
|
283
|
+
if (longest <= fine_sq) continue;
|
|
284
|
+
|
|
285
|
+
// the two cut points where the threshold crosses this face's edges
|
|
286
|
+
let mx = 0, my = 0, mz = 0, crossings = 0;
|
|
287
|
+
// edge (v1,v2)
|
|
288
|
+
if ((d1 < erode_distance) !== (d2 < erode_distance)) {
|
|
289
|
+
const t = (erode_distance - d1) / (d2 - d1);
|
|
290
|
+
mx += ca[0] + t * (cb[0] - ca[0]); my += ca[1] + t * (cb[1] - ca[1]); mz += ca[2] + t * (cb[2] - ca[2]); crossings++;
|
|
291
|
+
}
|
|
292
|
+
// edge (v2,v3)
|
|
293
|
+
if ((d2 < erode_distance) !== (d3 < erode_distance)) {
|
|
294
|
+
const t = (erode_distance - d2) / (d3 - d2);
|
|
295
|
+
mx += cb[0] + t * (cc[0] - cb[0]); my += cb[1] + t * (cc[1] - cb[1]); mz += cb[2] + t * (cc[2] - cb[2]); crossings++;
|
|
296
|
+
}
|
|
297
|
+
// edge (v3,v1)
|
|
298
|
+
if ((d3 < erode_distance) !== (d1 < erode_distance)) {
|
|
299
|
+
const t = (erode_distance - d3) / (d1 - d3);
|
|
300
|
+
mx += cc[0] + t * (ca[0] - cc[0]); my += cc[1] + t * (ca[1] - cc[1]); mz += cc[2] + t * (ca[2] - cc[2]); crossings++;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (crossings === 2) {
|
|
304
|
+
mx *= 0.5; my *= 0.5; mz *= 0.5;
|
|
305
|
+
const true_d = dist_to_boundary(mx, my, mz);
|
|
306
|
+
if (Math.abs(true_d - erode_distance) <= tol) {
|
|
307
|
+
// the straight cut here is already accurate - leave this face coarse
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sl = (longest === e_ab) ? la : (longest === e_bc) ? lb : lc;
|
|
313
|
+
targets.add(mesh.loop_read_edge(sl));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (targets.size === 0) break;
|
|
317
|
+
|
|
318
|
+
for (const e of targets) {
|
|
319
|
+
if (mesh.edges.is_allocated(e)) bt_edge_split(mesh, e, 0.5);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
island_faces = recompute_island_faces(faces);
|
|
323
|
+
({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
|
|
324
|
+
|
|
325
|
+
compute_field();
|
|
326
|
+
|
|
327
|
+
if (mesh.faces.size > 200000) break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
170
330
|
// ---------------------------------------------------------
|
|
171
331
|
// 3.5. PEAK RESCUE PASS (The Fix)
|
|
172
332
|
// Handle cases where a face is "submerged" by vertex values
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
|
|
3
|
+
* its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
|
|
4
|
+
* shared edge.
|
|
5
|
+
*
|
|
6
|
+
* This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
|
|
7
|
+
* L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
|
|
8
|
+
* neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
|
|
9
|
+
* queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
|
|
10
|
+
*
|
|
11
|
+
* Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
|
|
12
|
+
* onto the T-junction vertices and the resulting duplicate edges fuse.
|
|
13
|
+
*
|
|
14
|
+
* @param {BinaryTopology} mesh
|
|
15
|
+
* @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
|
|
16
|
+
* @returns {number} number of splits performed
|
|
17
|
+
*/
|
|
18
|
+
export function bt_mesh_resolve_t_junctions(mesh: BinaryTopology, tolerance?: number): number;
|
|
19
|
+
//# sourceMappingURL=bt_mesh_resolve_t_junctions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bt_mesh_resolve_t_junctions.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AACH,8EAHW,MAAM,GACJ,MAAM,CAuElB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NULL_POINTER } from "../BinaryTopology.js";
|
|
2
|
+
import { bt_edge_split } from "./edge/bt_edge_split.js";
|
|
3
|
+
|
|
4
|
+
const _a = new Float32Array(3);
|
|
5
|
+
const _b = new Float32Array(3);
|
|
6
|
+
const _p = new Float32Array(3);
|
|
7
|
+
const _hit = new Float32Array(3);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve T-junctions: where a vertex lies on the interior of a boundary edge (rather than at one of
|
|
11
|
+
* its endpoints), split that edge at the vertex so the two pieces can later be welded/fused into a
|
|
12
|
+
* shared edge.
|
|
13
|
+
*
|
|
14
|
+
* This is required for meshes assembled from independently-authored faces of differing sizes - e.g. an
|
|
15
|
+
* L-shaped or ring floor built from rectangles, where one rectangle's corner falls in the middle of a
|
|
16
|
+
* neighbour's edge. Without it those faces touch only at a single vertex, so edge-based neighbour
|
|
17
|
+
* queries (island detection, erosion, path-finding) treat them as DISCONNECTED.
|
|
18
|
+
*
|
|
19
|
+
* Call AFTER an initial vertex-merge/edge-fuse, then merge + fuse again so the new split vertices weld
|
|
20
|
+
* onto the T-junction vertices and the resulting duplicate edges fuse.
|
|
21
|
+
*
|
|
22
|
+
* @param {BinaryTopology} mesh
|
|
23
|
+
* @param {number} [tolerance] max distance from a vertex to an edge for it to count as "on" the edge
|
|
24
|
+
* @returns {number} number of splits performed
|
|
25
|
+
*/
|
|
26
|
+
export function bt_mesh_resolve_t_junctions(mesh, tolerance = 1e-4) {
|
|
27
|
+
const tol_sq = tolerance * tolerance;
|
|
28
|
+
|
|
29
|
+
let total = 0;
|
|
30
|
+
let rounds = 0;
|
|
31
|
+
let changed = true;
|
|
32
|
+
|
|
33
|
+
while (changed && rounds < 256) {
|
|
34
|
+
changed = false;
|
|
35
|
+
rounds++;
|
|
36
|
+
|
|
37
|
+
const edge_count = mesh.edges.size;
|
|
38
|
+
|
|
39
|
+
for (let e = 0; e < edge_count; e++) {
|
|
40
|
+
if (!mesh.edges.is_allocated(e)) continue;
|
|
41
|
+
|
|
42
|
+
// only boundary edges (a single radial loop) can carry a T-junction
|
|
43
|
+
const l = mesh.edge_read_loop(e);
|
|
44
|
+
if (l === NULL_POINTER) continue;
|
|
45
|
+
if (mesh.loop_read_radial_next(l) !== l) continue;
|
|
46
|
+
|
|
47
|
+
const v1 = mesh.edge_read_vertex1(e);
|
|
48
|
+
const v2 = mesh.edge_read_vertex2(e);
|
|
49
|
+
|
|
50
|
+
mesh.vertex_read_coordinate(_a, 0, v1);
|
|
51
|
+
mesh.vertex_read_coordinate(_b, 0, v2);
|
|
52
|
+
|
|
53
|
+
const abx = _b[0] - _a[0], aby = _b[1] - _a[1], abz = _b[2] - _a[2];
|
|
54
|
+
const ab_sq = abx * abx + aby * aby + abz * abz;
|
|
55
|
+
if (ab_sq <= tol_sq) continue;
|
|
56
|
+
|
|
57
|
+
const t_margin = tolerance / Math.sqrt(ab_sq);
|
|
58
|
+
|
|
59
|
+
let split_t = -1;
|
|
60
|
+
let split_v = -1;
|
|
61
|
+
|
|
62
|
+
const vertex_count = mesh.vertices.size;
|
|
63
|
+
for (let v = 0; v < vertex_count; v++) {
|
|
64
|
+
if (v === v1 || v === v2) continue;
|
|
65
|
+
if (!mesh.vertices.is_allocated(v)) continue;
|
|
66
|
+
|
|
67
|
+
mesh.vertex_read_coordinate(_p, 0, v);
|
|
68
|
+
|
|
69
|
+
const t = ((_p[0] - _a[0]) * abx + (_p[1] - _a[1]) * aby + (_p[2] - _a[2]) * abz) / ab_sq;
|
|
70
|
+
if (t <= t_margin || t >= 1 - t_margin) continue;
|
|
71
|
+
|
|
72
|
+
const cx = _a[0] + t * abx - _p[0];
|
|
73
|
+
const cy = _a[1] + t * aby - _p[1];
|
|
74
|
+
const cz = _a[2] + t * abz - _p[2];
|
|
75
|
+
if (cx * cx + cy * cy + cz * cz > tol_sq) continue;
|
|
76
|
+
|
|
77
|
+
split_t = t;
|
|
78
|
+
split_v = v;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (split_t >= 0) {
|
|
83
|
+
// snapshot the on-edge vertex coordinate, split, then snap the new vertex exactly onto it
|
|
84
|
+
mesh.vertex_read_coordinate(_hit, 0, split_v);
|
|
85
|
+
const nv = bt_edge_split(mesh, e, split_t);
|
|
86
|
+
mesh.vertex_write_coordinate(nv, _hit, 0);
|
|
87
|
+
|
|
88
|
+
total++;
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return total;
|
|
95
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bt_mesh_carve_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js"],"names":[],"mappings":"AA2NA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAPkC,IAAI;IACJ,MAAM;IACjB,UAAU;IACP,YAAY,EAA3B,MAAM;IACS,YAAY,EAA3B,MAAM;IACU,EAAE;
|
|
1
|
+
{"version":3,"file":"bt_mesh_carve_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js"],"names":[],"mappings":"AA2NA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAPkC,IAAI;IACJ,MAAM;IACjB,UAAU;IACP,YAAY,EAA3B,MAAM;IACS,YAAY,EAA3B,MAAM;IACU,EAAE;SAyH5B"}
|
|
@@ -254,9 +254,7 @@ export function bt_mesh_carve_height_clearance({
|
|
|
254
254
|
|
|
255
255
|
const r = Math.max(agent_radius, 0);
|
|
256
256
|
const resolution = Math.max(agent_radius > 0 ? agent_radius : agent_height / 4, 0.3);
|
|
257
|
-
const coarse_guard = Math.max(2 * agent_radius, agent_height / 2, 0.8);
|
|
258
257
|
const res_sq = resolution * resolution;
|
|
259
|
-
const guard_sq = coarse_guard * coarse_guard;
|
|
260
258
|
|
|
261
259
|
// The agent fits at p iff nothing overhangs directly above it AND nothing overhangs within its body
|
|
262
260
|
// radius. (For r == 0 this is just the exact overhead ray.)
|
|
@@ -310,9 +308,10 @@ export function bt_mesh_carve_height_clearance({
|
|
|
310
308
|
let refine = false;
|
|
311
309
|
if (n_block > 0 && n_clear > 0) {
|
|
312
310
|
refine = true; // (dilated) outline crosses this face -> hug it at `resolution`
|
|
313
|
-
} else if (n_block === 0
|
|
314
|
-
// all clear by sampling, but
|
|
315
|
-
//
|
|
311
|
+
} else if (n_block === 0) {
|
|
312
|
+
// all clear by sampling, but a small obstacle can hide entirely between the samples of a
|
|
313
|
+
// coarse face. If any downward-facing overhang sits under the face footprint, keep
|
|
314
|
+
// refining down to the contour resolution so even sub-sample obstacles get caught.
|
|
316
315
|
if (footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, r)) {
|
|
317
316
|
refine = true;
|
|
318
317
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAyLxB;+BAvO8B,mEAAmE"}
|
|
@@ -39,6 +39,9 @@ import Vector3 from "../../../../core/geom/Vector3.js";
|
|
|
39
39
|
import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
|
|
40
40
|
import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
|
|
41
41
|
import { bt_mesh_carve_height_clearance } from "./bt_mesh_carve_height_clearance.js";
|
|
42
|
+
import {
|
|
43
|
+
bt_mesh_resolve_t_junctions
|
|
44
|
+
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js";
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -164,40 +167,47 @@ export function navmesh_build_topology({
|
|
|
164
167
|
// which is what the loop-based neighbour queries rely on
|
|
165
168
|
bt_mesh_fuse_duplicate_edges(mesh);
|
|
166
169
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
bt_mesh_face_decouple_islands(mesh, islands);
|
|
176
|
-
|
|
177
|
-
for (const island of islands) {
|
|
178
|
-
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// remove dangling references, then compact (erosion frees faces, leaving holes in the ID space)
|
|
182
|
-
bt_mesh_cleanup_faceless_references(mesh);
|
|
183
|
-
bt_mesh_compact(mesh);
|
|
170
|
+
// Resolve T-junctions: when faces of differing sizes abut (an L-shaped or ring floor built from
|
|
171
|
+
// rectangles, a small tile against a big one), one face's corner can land in the middle of
|
|
172
|
+
// another's edge. Those faces touch at a single vertex only, so edge-based neighbour queries
|
|
173
|
+
// treat them as disconnected. Split such edges at the stray vertex, then re-weld/fuse so the
|
|
174
|
+
// pieces share a real edge.
|
|
175
|
+
if (bt_mesh_resolve_t_junctions(mesh, 1e-4) > 0) {
|
|
176
|
+
bt_merge_verts_by_distance(mesh, 1e-6);
|
|
177
|
+
bt_mesh_fuse_duplicate_edges(mesh);
|
|
184
178
|
}
|
|
185
179
|
|
|
186
|
-
// ---
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
180
|
+
// --- Carve obstacle footprints first (no radius dilation here) ---
|
|
181
|
+
// Cull the floor directly under any overhang, opening a hole at each obstacle footprint. The
|
|
182
|
+
// agent-radius dilation is handled by the erosion below, which treats these holes as boundaries
|
|
183
|
+
// and insets them (and the outer edge) by exactly agent_radius - giving both a uniform inset and
|
|
184
|
+
// a smooth, consistent outline around obstacles from a single, robust pass.
|
|
190
185
|
if (agent_height > 0) {
|
|
191
186
|
bt_mesh_carve_height_clearance({
|
|
192
187
|
mesh,
|
|
193
188
|
source,
|
|
194
189
|
source_bvh,
|
|
195
190
|
agent_height,
|
|
196
|
-
agent_radius,
|
|
191
|
+
agent_radius: 0,
|
|
197
192
|
up,
|
|
198
193
|
});
|
|
199
194
|
|
|
200
|
-
|
|
195
|
+
bt_mesh_compact(mesh);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Then erode every boundary (outer edge + obstacle holes) by the agent radius ---
|
|
199
|
+
// The erosion uses an exact Euclidean distance field with cut-band refinement, so it stays
|
|
200
|
+
// accurate and connected even on the hole-punched mesh the carve produces.
|
|
201
|
+
{
|
|
202
|
+
const islands = bt_mesh_compute_face_islands(mesh);
|
|
203
|
+
|
|
204
|
+
bt_mesh_face_decouple_islands(mesh, islands);
|
|
205
|
+
|
|
206
|
+
for (const island of islands) {
|
|
207
|
+
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
bt_mesh_cleanup_faceless_references(mesh);
|
|
201
211
|
bt_mesh_compact(mesh);
|
|
202
212
|
}
|
|
203
213
|
|