@woosh/meep-engine 2.163.4 → 2.163.6
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_fill_small_holes.d.ts +16 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +144 -0
- 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 +46 -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.6",
|
|
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,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fill boundary holes that are too thin for the agent to occupy.
|
|
3
|
+
*
|
|
4
|
+
* Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
|
|
5
|
+
* cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
|
|
6
|
+
* than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
|
|
7
|
+
* the boundary, the face count, and the loop count. This fills any inner loop whose mean width
|
|
8
|
+
* (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
|
|
9
|
+
* largest-area loop (the outer boundary) is never filled.
|
|
10
|
+
*
|
|
11
|
+
* @param {BinaryTopology} mesh
|
|
12
|
+
* @param {number} agent_radius
|
|
13
|
+
* @returns {number} number of holes filled
|
|
14
|
+
*/
|
|
15
|
+
export function bt_mesh_fill_small_holes(mesh: BinaryTopology, agent_radius: number): number;
|
|
16
|
+
//# sourceMappingURL=bt_mesh_fill_small_holes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bt_mesh_fill_small_holes.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js"],"names":[],"mappings":"AA+FA;;;;;;;;;;;;;GAaG;AACH,6EAHW,MAAM,GACJ,MAAM,CAoClB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { bt_mesh_walk_boundary_loops } from "../query/bt_mesh_walk_boundary_loops.js";
|
|
2
|
+
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
|
|
3
|
+
import { bt_radial_loop_add } from "./bt_radial_loop_add.js";
|
|
4
|
+
|
|
5
|
+
const _c = [0, 0, 0];
|
|
6
|
+
const _p = [0, 0, 0];
|
|
7
|
+
const _q = [0, 0, 0];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Order a boundary cycle's edges into a vertex/edge walk starting at `start_vertex`.
|
|
11
|
+
* (Same traversal as bt_mesh_close_boundary_holes.)
|
|
12
|
+
*/
|
|
13
|
+
function order_cycle_from(mesh, cycle, start_vertex, out_vertices, out_edges) {
|
|
14
|
+
const n = cycle.length;
|
|
15
|
+
const v_to_edges = new Map();
|
|
16
|
+
for (let i = 0; i < n; i++) {
|
|
17
|
+
const e = cycle[i];
|
|
18
|
+
const v1 = mesh.edge_read_vertex1(e);
|
|
19
|
+
const v2 = mesh.edge_read_vertex2(e);
|
|
20
|
+
let bucket = v_to_edges.get(v1);
|
|
21
|
+
if (bucket === undefined) { bucket = []; v_to_edges.set(v1, bucket); }
|
|
22
|
+
bucket.push(e);
|
|
23
|
+
bucket = v_to_edges.get(v2);
|
|
24
|
+
if (bucket === undefined) { bucket = []; v_to_edges.set(v2, bucket); }
|
|
25
|
+
bucket.push(e);
|
|
26
|
+
}
|
|
27
|
+
let v_curr = start_vertex;
|
|
28
|
+
let e_prev = -1;
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
out_vertices[i] = v_curr;
|
|
31
|
+
const edges_at_v = v_to_edges.get(v_curr);
|
|
32
|
+
const e_next = (edges_at_v[0] === e_prev) ? edges_at_v[1] : edges_at_v[0];
|
|
33
|
+
out_edges[i] = e_next;
|
|
34
|
+
const e_v1 = mesh.edge_read_vertex1(e_next);
|
|
35
|
+
const e_v2 = mesh.edge_read_vertex2(e_next);
|
|
36
|
+
v_curr = (e_v1 === v_curr) ? e_v2 : e_v1;
|
|
37
|
+
e_prev = e_next;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Perimeter and (Newell) 3D area of an ordered loop of vertices. */
|
|
42
|
+
function loop_perimeter_and_area(mesh, verts) {
|
|
43
|
+
const n = verts.length;
|
|
44
|
+
let perimeter = 0;
|
|
45
|
+
let nx = 0, ny = 0, nz = 0;
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
mesh.vertex_read_coordinate(_p, 0, verts[i]);
|
|
48
|
+
mesh.vertex_read_coordinate(_q, 0, verts[(i + 1) % n]);
|
|
49
|
+
perimeter += Math.hypot(_q[0] - _p[0], _q[1] - _p[1], _q[2] - _p[2]);
|
|
50
|
+
// Newell's method for polygon area normal
|
|
51
|
+
nx += (_p[1] - _q[1]) * (_p[2] + _q[2]);
|
|
52
|
+
ny += (_p[2] - _q[2]) * (_p[0] + _q[0]);
|
|
53
|
+
nz += (_p[0] - _q[0]) * (_p[1] + _q[1]);
|
|
54
|
+
}
|
|
55
|
+
const area = 0.5 * Math.hypot(nx, ny, nz);
|
|
56
|
+
return { perimeter, area };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Fan-fill a single boundary loop (n-gon face + centroid poke), as close_boundary_holes does. */
|
|
60
|
+
function fill_loop(mesh, cycle) {
|
|
61
|
+
const n = cycle.length;
|
|
62
|
+
const seed_edge = cycle[0];
|
|
63
|
+
const seed_loop = mesh.edge_read_loop(seed_edge);
|
|
64
|
+
const existing_loop_vertex = mesh.loop_read_vertex(seed_loop);
|
|
65
|
+
const seed_v1 = mesh.edge_read_vertex1(seed_edge);
|
|
66
|
+
const seed_v2 = mesh.edge_read_vertex2(seed_edge);
|
|
67
|
+
const start_vertex = (existing_loop_vertex === seed_v1) ? seed_v2 : seed_v1;
|
|
68
|
+
|
|
69
|
+
const ordered_vertices = new Array(n);
|
|
70
|
+
const ordered_edges = new Array(n);
|
|
71
|
+
order_cycle_from(mesh, cycle, start_vertex, ordered_vertices, ordered_edges);
|
|
72
|
+
|
|
73
|
+
let cx = 0, cy = 0, cz = 0;
|
|
74
|
+
for (let i = 0; i < n; i++) {
|
|
75
|
+
mesh.vertex_read_coordinate(_c, 0, ordered_vertices[i]);
|
|
76
|
+
cx += _c[0]; cy += _c[1]; cz += _c[2];
|
|
77
|
+
}
|
|
78
|
+
cx /= n; cy /= n; cz /= n;
|
|
79
|
+
|
|
80
|
+
const new_face = mesh.faces.allocate();
|
|
81
|
+
const loops = new Array(n);
|
|
82
|
+
for (let i = 0; i < n; i++) {
|
|
83
|
+
loops[i] = mesh.loop_create();
|
|
84
|
+
mesh.loop_write_vertex(loops[i], ordered_vertices[i]);
|
|
85
|
+
mesh.loop_write_face(loops[i], new_face);
|
|
86
|
+
bt_radial_loop_add(mesh, loops[i], ordered_edges[i]);
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < n; i++) {
|
|
89
|
+
mesh.loop_write_next(loops[i], loops[(i + 1) % n]);
|
|
90
|
+
mesh.loop_write_prev(loops[i], loops[(i - 1 + n) % n]);
|
|
91
|
+
}
|
|
92
|
+
mesh.face_write_loop(new_face, loops[0]);
|
|
93
|
+
bt_mesh_face_poke(mesh, new_face, cx, cy, cz);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fill boundary holes that are too thin for the agent to occupy.
|
|
98
|
+
*
|
|
99
|
+
* Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
|
|
100
|
+
* cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
|
|
101
|
+
* than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
|
|
102
|
+
* the boundary, the face count, and the loop count. This fills any inner loop whose mean width
|
|
103
|
+
* (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
|
|
104
|
+
* largest-area loop (the outer boundary) is never filled.
|
|
105
|
+
*
|
|
106
|
+
* @param {BinaryTopology} mesh
|
|
107
|
+
* @param {number} agent_radius
|
|
108
|
+
* @returns {number} number of holes filled
|
|
109
|
+
*/
|
|
110
|
+
export function bt_mesh_fill_small_holes(mesh, agent_radius) {
|
|
111
|
+
if (agent_radius <= 0) return 0;
|
|
112
|
+
|
|
113
|
+
const cycles = bt_mesh_walk_boundary_loops(mesh);
|
|
114
|
+
if (cycles.length <= 1) return 0;
|
|
115
|
+
|
|
116
|
+
// measure every loop; the largest-area one is the outer boundary and is never filled
|
|
117
|
+
const measures = cycles.map(c => {
|
|
118
|
+
const n = c.length;
|
|
119
|
+
const verts = new Array(n);
|
|
120
|
+
const edges = new Array(n);
|
|
121
|
+
const v1 = mesh.edge_read_vertex1(c[0]);
|
|
122
|
+
order_cycle_from(mesh, c, v1, verts, edges);
|
|
123
|
+
return loop_perimeter_and_area(mesh, verts);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let outer = 0;
|
|
127
|
+
for (let i = 1; i < measures.length; i++) {
|
|
128
|
+
if (measures[i].area > measures[outer].area) outer = i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let filled = 0;
|
|
132
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
133
|
+
if (i === outer) continue;
|
|
134
|
+
const { perimeter, area } = measures[i];
|
|
135
|
+
if (perimeter <= 0) continue;
|
|
136
|
+
// mean width of the hole = 2*area/perimeter; fill when the agent disc cannot fit
|
|
137
|
+
if (2 * area / perimeter < agent_radius) {
|
|
138
|
+
fill_loop(mesh, cycles[i]);
|
|
139
|
+
filled++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return filled;
|
|
144
|
+
}
|
|
@@ -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":"AAiDA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAmMxB;+BApP8B,mEAAmE"}
|
|
@@ -39,6 +39,12 @@ 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";
|
|
45
|
+
import {
|
|
46
|
+
bt_mesh_fill_small_holes
|
|
47
|
+
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js";
|
|
42
48
|
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -164,40 +170,47 @@ export function navmesh_build_topology({
|
|
|
164
170
|
// which is what the loop-based neighbour queries rely on
|
|
165
171
|
bt_mesh_fuse_duplicate_edges(mesh);
|
|
166
172
|
|
|
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);
|
|
173
|
+
// Resolve T-junctions: when faces of differing sizes abut (an L-shaped or ring floor built from
|
|
174
|
+
// rectangles, a small tile against a big one), one face's corner can land in the middle of
|
|
175
|
+
// another's edge. Those faces touch at a single vertex only, so edge-based neighbour queries
|
|
176
|
+
// treat them as disconnected. Split such edges at the stray vertex, then re-weld/fuse so the
|
|
177
|
+
// pieces share a real edge.
|
|
178
|
+
if (bt_mesh_resolve_t_junctions(mesh, 1e-4) > 0) {
|
|
179
|
+
bt_merge_verts_by_distance(mesh, 1e-6);
|
|
180
|
+
bt_mesh_fuse_duplicate_edges(mesh);
|
|
184
181
|
}
|
|
185
182
|
|
|
186
|
-
// ---
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
183
|
+
// --- Carve obstacle footprints first (no radius dilation here) ---
|
|
184
|
+
// Cull the floor directly under any overhang, opening a hole at each obstacle footprint. The
|
|
185
|
+
// agent-radius dilation is handled by the erosion below, which treats these holes as boundaries
|
|
186
|
+
// and insets them (and the outer edge) by exactly agent_radius - giving both a uniform inset and
|
|
187
|
+
// a smooth, consistent outline around obstacles from a single, robust pass.
|
|
190
188
|
if (agent_height > 0) {
|
|
191
189
|
bt_mesh_carve_height_clearance({
|
|
192
190
|
mesh,
|
|
193
191
|
source,
|
|
194
192
|
source_bvh,
|
|
195
193
|
agent_height,
|
|
196
|
-
agent_radius,
|
|
194
|
+
agent_radius: 0,
|
|
197
195
|
up,
|
|
198
196
|
});
|
|
199
197
|
|
|
200
|
-
|
|
198
|
+
bt_mesh_compact(mesh);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Then erode every boundary (outer edge + obstacle holes) by the agent radius ---
|
|
202
|
+
// The erosion uses an exact Euclidean distance field with cut-band refinement, so it stays
|
|
203
|
+
// accurate and connected even on the hole-punched mesh the carve produces.
|
|
204
|
+
{
|
|
205
|
+
const islands = bt_mesh_compute_face_islands(mesh);
|
|
206
|
+
|
|
207
|
+
bt_mesh_face_decouple_islands(mesh, islands);
|
|
208
|
+
|
|
209
|
+
for (const island of islands) {
|
|
210
|
+
bt_mesh_face_island_erode(mesh, island, agent_radius);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
bt_mesh_cleanup_faceless_references(mesh);
|
|
201
214
|
bt_mesh_compact(mesh);
|
|
202
215
|
}
|
|
203
216
|
|
|
@@ -214,6 +227,16 @@ export function navmesh_build_topology({
|
|
|
214
227
|
bt_mesh_compact(mesh);
|
|
215
228
|
}
|
|
216
229
|
|
|
230
|
+
// Fill hair-thin sliver holes left by erosion/clearance around obstacle offsets. They are
|
|
231
|
+
// narrower than the agent (it could never fit inside them), so they are numerical noise rather
|
|
232
|
+
// than real obstacles - removing them restores wrongly-culled walkable area and keeps the
|
|
233
|
+
// boundary clean (one outer loop + one loop per genuine obstacle).
|
|
234
|
+
if (agent_radius > 0) {
|
|
235
|
+
if (bt_mesh_fill_small_holes(mesh, agent_radius) > 0) {
|
|
236
|
+
bt_mesh_compact(mesh);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
217
240
|
// face normals are consumed by navigation queries (string-pulling portal normals), populate them now
|
|
218
241
|
bt_mesh_compute_face_normals(mesh);
|
|
219
242
|
|