@woosh/meep-engine 2.163.6 → 2.163.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts +21 -0
  3. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts.map +1 -0
  4. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +42 -0
  5. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts +2 -2
  6. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts.map +1 -1
  7. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +120 -179
  8. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts +9 -10
  9. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
  10. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +20 -14
  11. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts +17 -0
  12. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts.map +1 -0
  13. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +45 -0
  14. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts +40 -0
  15. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts.map +1 -0
  16. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.js +84 -0
  17. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.d.ts.map +1 -1
  18. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js +53 -78
  19. package/src/core/geom/vec3/v3_matrix3_rotate.d.ts +16 -0
  20. package/src/core/geom/vec3/v3_matrix3_rotate.d.ts.map +1 -0
  21. package/src/core/geom/vec3/v3_matrix3_rotate.js +49 -0
  22. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts +2 -2
  23. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -1
  24. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.js +46 -46
  25. package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.d.ts.map +1 -1
  26. package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.js +6 -28
  27. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +185 -0
  28. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
  29. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  30. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
  31. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts +11 -0
  32. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -0
  33. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +472 -0
  34. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  35. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +36 -39
  36. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts +17 -0
  37. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts.map +1 -0
  38. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.js +613 -0
  39. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts +0 -28
  40. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts.map +0 -1
  41. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js +0 -358
  42. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts +0 -23
  43. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts.map +0 -1
  44. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.js +0 -319
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @param {number[]} soup flat XYZ triangle soup (9 floats per triangle)
3
+ * @param {number} triangle_count
4
+ * @param {BinaryTopology} source source mesh (face normals must be populated)
5
+ * @param {number} agent_height
6
+ * @param {number} agent_radius dilation applied to each footprint
7
+ * @param {Vector3} up
8
+ * @returns {number[]} new soup with (dilated) obstacle footprints removed
9
+ */
10
+ export function clip_soup_against_overhangs(soup: number[], triangle_count: number, source: BinaryTopology, agent_height: number, agent_radius: number, up: Vector3): number[];
11
+ //# sourceMappingURL=clip_soup_against_overhangs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip_soup_against_overhangs.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/clip_soup_against_overhangs.js"],"names":[],"mappings":"AA+MA;;;;;;;;GAQG;AACH,kDARW,MAAM,EAAE,kBACR,MAAM,wCAEN,MAAM,gBACN,MAAM,gBAEJ,MAAM,EAAE,CAkOpB"}
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Exact obstacle carving at the triangle-soup level, with a faithful agent-radius offset.
3
+ *
4
+ * Each overhead obstacle's footprint is DILATED by the agent radius (Minkowski sum with a disc: edges
5
+ * pushed out by r, convex corners replaced by a rounded arc), and that dilated polygon is subtracted
6
+ * from the walkable triangles. The resulting hole boundary is therefore the true offset of the
7
+ * obstacle - straight along edges, a clean arc at corners - rather than a wavy/mitred contour produced
8
+ * by sampling-then-eroding. (Run AFTER the outer boundary has been eroded by r, so the obstacle holes
9
+ * are not eroded a second time.)
10
+ *
11
+ * Overhead obstacles are the DOWNWARD-facing source faces; a face carves a walkable triangle only where
12
+ * it sits above it, within `agent_height`.
13
+ *
14
+ * The subtraction uses the disjoint half-plane decomposition of a convex-polygon difference:
15
+ * T \ F = U_i ( T ∩ inside(e_0..e_{i-1}) ∩ outside(e_i) )
16
+ * each term convex, the terms disjoint, the union exactly T minus the convex (dilated) footprint F.
17
+ *
18
+ * Everything is data-oriented: clipping happens in the 2D plane perpendicular to `up`, with polygons
19
+ * held as flat `Float64Array`s of (u,v) pairs and a small set of reused scratch buffers. World XYZ is
20
+ * recovered for each surviving vertex by barycentric reconstruction on its (planar) source triangle, so
21
+ * kept pieces stay exactly on the original - possibly tilted - walkable plane. Degenerate triangles are
22
+ * dropped, and triangles that no footprint overlaps are copied straight through without allocation.
23
+ *
24
+ * The footprints are indexed in a BVH (keyed by their (u,v) extent and carve height band), so each soup
25
+ * triangle only tests the obstacles that can actually overlap it - O(m log k) rather than O(m k) for m
26
+ * soup triangles and k obstacles.
27
+ */
28
+
29
+ import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
30
+ import { bvh_query_user_data_overlaps_aabb } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
31
+ import { v3_dot } from "../../../../core/geom/vec3/v3_dot.js";
32
+ import { v3_orthonormal_matrix_from_normal } from "../../../../core/geom/vec3/v3_orthonormal_matrix_from_normal.js";
33
+ import { convex_hull_monotone_2d } from "../../../../core/geom/2d/convex-hull/convex_hull_monotone_2d.js";
34
+ import { compute_polygon_area_2d } from "../../../../core/geom/2d/compute_polygon_area_2d.js";
35
+
36
+ const POINT_EPS = 1e-9;
37
+ const AREA_EPS = 1e-9;
38
+ const HEIGHT_EPS = 1e-3;
39
+ const CORNER_SEGMENT_ANGLE = Math.PI / 6; // ~30 deg per rounded-corner segment
40
+
41
+ // Vertex coincidence key. Quantize each projected coordinate to QUANTIZE (1mm) and pack the two integer
42
+ // coordinates into one safe-integer key: cheaper than a string and with no per-probe allocation. Each
43
+ // axis occupies a biased 26-bit field, so the key stays unique while |u|,|v| < 2^25 / QUANTIZE
44
+ // (~33,000 units). A 16-bit-per-axis pack would only reach ~3.3 units and silently collide - navmesh
45
+ // coordinates routinely exceed that (e.g. geometry built 10,000 units from the origin). Coincident
46
+ // vertices are the same source vertex projected identically, so they match at any quantization; the
47
+ // precision only needs to keep genuinely distinct obstacle corners apart (the smallest is ~0.3 units).
48
+ const QUANTIZE = 1e3;
49
+ const KEY_BIAS = 1 << 25; // 33,554,432
50
+ const KEY_STRIDE = 1 << 26; // 67,108,864
51
+
52
+ // Reused clip scratch (stride 2: u,v). Grown on demand so a single allocation serves the whole soup.
53
+ let _cur = new Float64Array(64);
54
+ let _tmp = new Float64Array(64);
55
+ let _side = new Float64Array(64);
56
+
57
+ /**
58
+ * Ensure `buf` can hold `vertex_count` (u,v) pairs, returning a large-enough buffer (the same one when
59
+ * it already fits). Contents are not preserved - callers grow before writing.
60
+ *
61
+ * @param {Float64Array} buf
62
+ * @param {number} vertex_count
63
+ * @returns {Float64Array}
64
+ */
65
+ function ensure_capacity(buf, vertex_count) {
66
+ const needed = vertex_count * 2;
67
+ if (buf.length >= needed) return buf;
68
+ let size = buf.length;
69
+ while (size < needed) size *= 2;
70
+ return new Float64Array(size);
71
+ }
72
+
73
+ /**
74
+ * Sutherland-Hodgman clip of a 2D polygon against one half-plane of a directed edge - the general-edge
75
+ * analogue of {@link polygon2_clip_axis_halfplane}. The edge passes through (ex, ez) with direction
76
+ * (dx, dz); `keep_left` keeps the side with `cross(dir, P - e) >= 0`, otherwise the right side.
77
+ *
78
+ * @param {Float64Array} in_pts input vertices (stride 2)
79
+ * @param {number} in_n input vertex count
80
+ * @param {Float64Array} out_pts output buffer (stride 2), sized for at least in_n + 1 vertices
81
+ * @param {number} ex
82
+ * @param {number} ez a point on the edge
83
+ * @param {number} dx
84
+ * @param {number} dz the edge direction
85
+ * @param {boolean} keep_left keep the left half-plane when true
86
+ * @returns {number} surviving vertex count
87
+ */
88
+ function clip_halfplane(in_pts, in_n, out_pts, ex, ez, dx, dz, keep_left) {
89
+ let out_n = 0;
90
+
91
+ for (let i = 0; i < in_n; i++) {
92
+ const j = (i + 1) % in_n;
93
+
94
+ const a_u = in_pts[i * 2], a_v = in_pts[i * 2 + 1];
95
+ const b_u = in_pts[j * 2], b_v = in_pts[j * 2 + 1];
96
+
97
+ const sa = dx * (a_v - ez) - dz * (a_u - ex);
98
+ const sb = dx * (b_v - ez) - dz * (b_u - ex);
99
+
100
+ const in_a = keep_left ? (sa >= -POINT_EPS) : (sa <= POINT_EPS);
101
+ const in_b = keep_left ? (sb >= -POINT_EPS) : (sb <= POINT_EPS);
102
+
103
+ if (in_a) {
104
+ out_pts[out_n * 2] = a_u;
105
+ out_pts[out_n * 2 + 1] = a_v;
106
+ out_n++;
107
+ }
108
+
109
+ if (in_a !== in_b) {
110
+ const t = sa / (sa - sb);
111
+ out_pts[out_n * 2] = a_u + t * (b_u - a_u);
112
+ out_pts[out_n * 2 + 1] = a_v + t * (b_v - a_v);
113
+ out_n++;
114
+ }
115
+ }
116
+
117
+ return out_n;
118
+ }
119
+
120
+ /**
121
+ * Outward Minkowski-with-disc offset of a convex CCW polygon by `r`: each edge is pushed out by `r` and
122
+ * each convex corner is replaced by a rounded arc. Input/output are flat (u,v) pairs.
123
+ *
124
+ * @param {Float64Array} pts convex CCW polygon (stride 2)
125
+ * @param {number} n vertex count
126
+ * @param {number} r offset radius (> 0)
127
+ * @returns {Float64Array} the offset polygon (CCW, stride 2)
128
+ */
129
+ function offset_convex_ccw(pts, n, r) {
130
+ // outward edge normals (right of each CCW edge)
131
+ const enx = new Float64Array(n);
132
+ const enz = new Float64Array(n);
133
+ for (let i = 0; i < n; i++) {
134
+ const j = (i + 1) % n;
135
+ let dx = pts[j * 2] - pts[i * 2];
136
+ let dz = pts[j * 2 + 1] - pts[i * 2 + 1];
137
+ const len = Math.hypot(dx, dz) || 1;
138
+ dx /= len; dz /= len;
139
+ enx[i] = dz; // (dir.v, -dir.u) = right/outward of a CCW edge
140
+ enz[i] = -dx;
141
+ }
142
+
143
+ const out = [];
144
+ for (let i = 0; i < n; i++) {
145
+ const prev_edge = (i - 1 + n) % n; // edge ending at vertex i
146
+ const next_edge = i; // edge starting at vertex i
147
+ const vu = pts[i * 2], vv = pts[i * 2 + 1];
148
+
149
+ const a1 = Math.atan2(enz[prev_edge], enx[prev_edge]);
150
+ const a2 = Math.atan2(enz[next_edge], enx[next_edge]);
151
+ let da = a2 - a1;
152
+ while (da < 0) da += 2 * Math.PI;
153
+ while (da > 2 * Math.PI) da -= 2 * Math.PI;
154
+
155
+ const steps = Math.max(1, Math.ceil(da / CORNER_SEGMENT_ANGLE));
156
+ for (let s = 0; s <= steps; s++) {
157
+ const a = a1 + da * (s / steps);
158
+ out.push(vu + r * Math.cos(a), vv + r * Math.sin(a));
159
+ }
160
+ }
161
+
162
+ return Float64Array.from(out);
163
+ }
164
+
165
+ /**
166
+ * Subtract convex footprint `fp` from a single convex polygon, appending the convex, disjoint pieces of
167
+ * (polygon \ fp) to `out_pieces` as right-sized `Float64Array`s. `fp` must be CCW (interior on the left
168
+ * of each edge). The polygon's interior remainder (the part inside `fp`) is discarded.
169
+ *
170
+ * @param {Float64Array} poly polygon vertices (stride 2)
171
+ * @param {number} poly_n polygon vertex count
172
+ * @param {Float64Array} fp footprint vertices (stride 2, CCW)
173
+ * @param {number} fp_n footprint vertex count
174
+ * @param {Float64Array[]} out_pieces destination list; surviving outside pieces are pushed
175
+ */
176
+ function subtract_footprint(poly, poly_n, fp, fp_n, out_pieces) {
177
+ // current = the running intersection with the inside half-planes processed so far
178
+ _cur = ensure_capacity(_cur, poly_n + fp_n);
179
+ _tmp = ensure_capacity(_tmp, poly_n + fp_n);
180
+ _side = ensure_capacity(_side, poly_n + fp_n);
181
+
182
+ let cur = _cur;
183
+ let tmp = _tmp;
184
+ for (let i = 0; i < poly_n * 2; i++) cur[i] = poly[i];
185
+ let cur_n = poly_n;
186
+
187
+ for (let i = 0; i < fp_n; i++) {
188
+ if (cur_n < 3) return; // fully consumed - nothing outside remains
189
+
190
+ const j = (i + 1) % fp_n;
191
+ const ex = fp[i * 2], ez = fp[i * 2 + 1];
192
+ const dx = fp[j * 2] - ex;
193
+ const dz = fp[j * 2 + 1] - ez;
194
+ if (Math.abs(dx) < POINT_EPS && Math.abs(dz) < POINT_EPS) continue; // degenerate edge
195
+
196
+ // piece outside this edge (and inside all earlier edges) leaves the footprint here -> it survives
197
+ const side_n = clip_halfplane(cur, cur_n, _side, ex, ez, dx, dz, false);
198
+ if (side_n >= 3 && Math.abs(compute_polygon_area_2d(_side, side_n)) > AREA_EPS) {
199
+ out_pieces.push(_side.slice(0, side_n * 2));
200
+ }
201
+
202
+ // keep marching with the part still inside this edge
203
+ cur_n = clip_halfplane(cur, cur_n, tmp, ex, ez, dx, dz, true);
204
+ const swap = cur; cur = tmp; tmp = swap;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * @param {number[]} soup flat XYZ triangle soup (9 floats per triangle)
210
+ * @param {number} triangle_count
211
+ * @param {BinaryTopology} source source mesh (face normals must be populated)
212
+ * @param {number} agent_height
213
+ * @param {number} agent_radius dilation applied to each footprint
214
+ * @param {Vector3} up
215
+ * @returns {number[]} new soup with (dilated) obstacle footprints removed
216
+ */
217
+ export function clip_soup_against_overhangs(soup, triangle_count, source, agent_height, agent_radius, up) {
218
+ let upx = up.x, upy = up.y, upz = up.z;
219
+ const ul = Math.hypot(upx, upy, upz);
220
+ if (ul === 0) return soup.slice(0, triangle_count * 9);
221
+ upx /= ul; upy /= ul; upz /= ul;
222
+
223
+ // orthonormal basis: rows 0,1 span the plane perpendicular to up (our u,v axes), row 2 is up itself
224
+ const basis = new Float64Array(9);
225
+ v3_orthonormal_matrix_from_normal(basis, 0, upx, upy, upz);
226
+ const u_x = basis[0], u_y = basis[1], u_z = basis[2];
227
+ const v_x = basis[3], v_y = basis[4], v_z = basis[5];
228
+
229
+ // collect downward-facing (overhead) source triangles as flat (u,v) triples plus their mean height
230
+ const overhead_uv = []; // stride 6: u0,v0,u1,v1,u2,v2
231
+ const overhead_h = [];
232
+ const fn = new Float32Array(3);
233
+ const fc = new Float32Array(3);
234
+ const face_count = source.faces.size;
235
+ for (let f = 0; f < face_count; f++) {
236
+ if (!source.faces.is_allocated(f)) continue;
237
+ source.face_read_normal(fn, 0, f);
238
+ if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -HEIGHT_EPS) continue; // not downward-facing
239
+
240
+ let l = source.face_read_loop(f);
241
+ let hsum = 0;
242
+ for (let k = 0; k < 3; k++) {
243
+ source.vertex_read_coordinate(fc, 0, source.loop_read_vertex(l));
244
+ const x = fc[0], y = fc[1], z = fc[2];
245
+ overhead_uv.push(v3_dot(x, y, z, u_x, u_y, u_z), v3_dot(x, y, z, v_x, v_y, v_z));
246
+ hsum += v3_dot(x, y, z, upx, upy, upz);
247
+ l = source.loop_read_next(l);
248
+ }
249
+ overhead_h.push(hsum / 3);
250
+ }
251
+
252
+ const overhead_count = overhead_h.length;
253
+ if (overhead_count === 0) return soup.slice(0, triangle_count * 9);
254
+
255
+ // Group overhead triangles that share a (coincident) vertex into one obstacle. Each obstacle's
256
+ // footprint is the convex hull of its vertices, dilated by the agent radius. Treating the obstacle
257
+ // as one polygon (rather than per-triangle) avoids self-touching/pinched union boundaries.
258
+ const parent = new Int32Array(overhead_count);
259
+ for (let i = 0; i < overhead_count; i++) parent[i] = i;
260
+ const find = (a) => { while (parent[a] !== a) { parent[a] = parent[parent[a]]; a = parent[a]; } return a; };
261
+
262
+ const key_to_tri = new Map();
263
+ for (let i = 0; i < overhead_count; i++) {
264
+ for (let k = 0; k < 3; k++) {
265
+ const ku = Math.round(overhead_uv[i * 6 + k * 2] * QUANTIZE);
266
+ const kv = Math.round(overhead_uv[i * 6 + k * 2 + 1] * QUANTIZE);
267
+ const key = (ku + KEY_BIAS) * KEY_STRIDE + (kv + KEY_BIAS);
268
+ const seen = key_to_tri.get(key);
269
+ if (seen !== undefined) parent[find(i)] = find(seen); else key_to_tri.set(key, i);
270
+ }
271
+ }
272
+
273
+ /** @type {Map<number, {pts: number[], hsum: number, n: number}>} */
274
+ const groups = new Map();
275
+ for (let i = 0; i < overhead_count; i++) {
276
+ const root = find(i);
277
+ let g = groups.get(root);
278
+ if (g === undefined) { g = { pts: [], hsum: 0, n: 0 }; groups.set(root, g); }
279
+ for (let k = 0; k < 6; k++) g.pts.push(overhead_uv[i * 6 + k]);
280
+ g.hsum += overhead_h[i];
281
+ g.n++;
282
+ }
283
+
284
+ // each footprint: a CCW (dilated) convex polygon in (u,v) plus the obstacle's mean height
285
+ /** @type {Float64Array[]} */
286
+ const footprint_poly = [];
287
+ const footprint_h = [];
288
+ for (const g of groups.values()) {
289
+ const hull = convex_hull_monotone_2d(g.pts, g.pts.length / 2);
290
+ if (hull.length < 3) continue;
291
+
292
+ let poly = new Float64Array(hull.length * 2);
293
+ for (let i = 0; i < hull.length; i++) {
294
+ poly[i * 2] = g.pts[hull[i] * 2];
295
+ poly[i * 2 + 1] = g.pts[hull[i] * 2 + 1];
296
+ }
297
+ // ensure CCW (interior to the left of each edge) for the subtraction
298
+ if (compute_polygon_area_2d(poly, hull.length) < 0) {
299
+ const rev = new Float64Array(poly.length);
300
+ for (let i = 0; i < hull.length; i++) {
301
+ rev[i * 2] = poly[(hull.length - 1 - i) * 2];
302
+ rev[i * 2 + 1] = poly[(hull.length - 1 - i) * 2 + 1];
303
+ }
304
+ poly = rev;
305
+ }
306
+ if (agent_radius > 0) poly = offset_convex_ccw(poly, poly.length / 2, agent_radius);
307
+
308
+ footprint_poly.push(poly);
309
+ footprint_h.push(g.hsum / g.n);
310
+ }
311
+
312
+ const footprint_count = footprint_poly.length;
313
+ if (footprint_count === 0) return soup.slice(0, triangle_count * 9);
314
+
315
+ // Broad-phase index: one leaf per footprint, in (u, v, height) space. The first two axes are the
316
+ // dilated footprint's (u,v) extent; the third is the band of soup heights it can carve - a triangle
317
+ // at mean height `th` is carved by an obstacle at height `h` iff 0 < h - th <= agent_height. A soup
318
+ // triangle then queries this BVH with its own (u,v) extent at `th` to get just the candidate
319
+ // obstacles, instead of scanning all of them.
320
+ const footprint_bvh = new BVH();
321
+ footprint_bvh.release_all();
322
+ for (let fi = 0; fi < footprint_count; fi++) {
323
+ const poly = footprint_poly[fi];
324
+ let min_u = Infinity, min_v = Infinity, max_u = -Infinity, max_v = -Infinity;
325
+ for (let i = 0; i < poly.length; i += 2) {
326
+ const u = poly[i], v = poly[i + 1];
327
+ if (u < min_u) min_u = u;
328
+ if (u > max_u) max_u = u;
329
+ if (v < min_v) min_v = v;
330
+ if (v > max_v) max_v = v;
331
+ }
332
+ const h = footprint_h[fi];
333
+ const node = footprint_bvh.allocate_node();
334
+ footprint_bvh.node_set_user_data(node, fi);
335
+ footprint_bvh.node_set_aabb_primitive(
336
+ node,
337
+ min_u, min_v, h - agent_height - HEIGHT_EPS,
338
+ max_u, max_v, h - HEIGHT_EPS
339
+ );
340
+ footprint_bvh.insert_leaf(node);
341
+ }
342
+
343
+ const out = [];
344
+
345
+ // per-triangle reusable storage
346
+ const tri_uv = new Float64Array(6); // u0,v0,u1,v1,u2,v2 of the source triangle
347
+ const px = new Float64Array(3), py = new Float64Array(3), pz = new Float64Array(3);
348
+ const query_aabb = new Float64Array(6);
349
+ const query_hits = new Int32Array(footprint_count);
350
+ /** @type {Float64Array[]} */
351
+ let pieces = [];
352
+ /** @type {Float64Array[]} */
353
+ let next_pieces = [];
354
+
355
+ for (let t = 0; t < triangle_count; t++) {
356
+ const o = t * 9;
357
+
358
+ let th = 0;
359
+ for (let k = 0; k < 3; k++) {
360
+ const x = soup[o + k * 3], y = soup[o + k * 3 + 1], z = soup[o + k * 3 + 2];
361
+ px[k] = x; py[k] = y; pz[k] = z;
362
+ tri_uv[k * 2] = v3_dot(x, y, z, u_x, u_y, u_z);
363
+ tri_uv[k * 2 + 1] = v3_dot(x, y, z, v_x, v_y, v_z);
364
+ th += v3_dot(x, y, z, upx, upy, upz);
365
+ }
366
+ th /= 3;
367
+
368
+ // barycentric denominator (twice the signed uv area of the triangle)
369
+ const e1u = tri_uv[2] - tri_uv[0], e1v = tri_uv[3] - tri_uv[1];
370
+ const e2u = tri_uv[4] - tri_uv[0], e2v = tri_uv[5] - tri_uv[1];
371
+ const det = e1u * e2v - e2u * e1v;
372
+
373
+ if (Math.abs(det) <= AREA_EPS) {
374
+ // degenerate in (u,v) (a near-vertical triangle): cannot carve meaningfully, pass through
375
+ for (let i = 0; i < 9; i++) out.push(soup[o + i]);
376
+ continue;
377
+ }
378
+
379
+ // broad-phase: only the obstacles whose (u,v) extent and height band overlap this triangle
380
+ let min_u = tri_uv[0], max_u = tri_uv[0], min_v = tri_uv[1], max_v = tri_uv[1];
381
+ for (let k = 1; k < 3; k++) {
382
+ const u = tri_uv[k * 2], v = tri_uv[k * 2 + 1];
383
+ if (u < min_u) min_u = u;
384
+ if (u > max_u) max_u = u;
385
+ if (v < min_v) min_v = v;
386
+ if (v > max_v) max_v = v;
387
+ }
388
+ query_aabb[0] = min_u; query_aabb[1] = min_v; query_aabb[2] = th;
389
+ query_aabb[3] = max_u; query_aabb[4] = max_v; query_aabb[5] = th;
390
+
391
+ const hit_count = bvh_query_user_data_overlaps_aabb(query_hits, 0, footprint_bvh, query_aabb);
392
+ if (hit_count === 0) {
393
+ // no obstacle overlaps here - copy straight through
394
+ for (let i = 0; i < 9; i++) out.push(soup[o + i]);
395
+ continue;
396
+ }
397
+
398
+ // start with the whole triangle as one piece, then subtract each overlapping footprint
399
+ pieces.length = 0;
400
+ pieces.push(Float64Array.of(tri_uv[0], tri_uv[1], tri_uv[2], tri_uv[3], tri_uv[4], tri_uv[5]));
401
+
402
+ for (let qi = 0; qi < hit_count; qi++) {
403
+ const fi = query_hits[qi];
404
+ const dh = footprint_h[fi] - th;
405
+ if (dh <= HEIGHT_EPS || dh > agent_height + HEIGHT_EPS) continue; // exact band test
406
+
407
+ const fp = footprint_poly[fi];
408
+ const fp_n = fp.length / 2;
409
+
410
+ next_pieces.length = 0;
411
+ for (let pi = 0; pi < pieces.length; pi++) {
412
+ subtract_footprint(pieces[pi], pieces[pi].length / 2, fp, fp_n, next_pieces);
413
+ }
414
+
415
+ const swap = pieces; pieces = next_pieces; next_pieces = swap;
416
+ if (pieces.length === 0) break;
417
+ }
418
+
419
+ const inv_det = 1 / det;
420
+ for (let pi = 0; pi < pieces.length; pi++) {
421
+ const poly = pieces[pi];
422
+ const poly_n = poly.length / 2;
423
+
424
+ // fan-triangulate the surviving piece and reconstruct world XYZ per vertex
425
+ for (let i = 1; i + 1 < poly_n; i++) {
426
+ const u0 = poly[0], v0 = poly[1];
427
+ const u1 = poly[i * 2], v1 = poly[i * 2 + 1];
428
+ const u2 = poly[(i + 1) * 2], v2 = poly[(i + 1) * 2 + 1];
429
+
430
+ const area2 = Math.abs((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0));
431
+ if (area2 < 2 * AREA_EPS) continue;
432
+
433
+ emit_xyz(out, u0, v0, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
434
+ emit_xyz(out, u1, v1, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
435
+ emit_xyz(out, u2, v2, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det);
436
+ }
437
+ }
438
+ }
439
+
440
+ return out;
441
+ }
442
+
443
+ /**
444
+ * Reconstruct the world XYZ of a (u,v) point lying in a source triangle (barycentric on the triangle's
445
+ * planar uv coordinates) and append it to `out`.
446
+ *
447
+ * @param {number[]} out destination soup
448
+ * @param {number} u
449
+ * @param {number} v the point in (u,v)
450
+ * @param {Float64Array} tri_uv the triangle's uv corners (stride 2, vertex 0 is the origin)
451
+ * @param {Float64Array} px
452
+ * @param {Float64Array} py
453
+ * @param {Float64Array} pz the triangle's world corners
454
+ * @param {number} e1u
455
+ * @param {number} e1v edge (v1 - v0) in uv
456
+ * @param {number} e2u
457
+ * @param {number} e2v edge (v2 - v0) in uv
458
+ * @param {number} inv_det reciprocal of the uv area determinant
459
+ */
460
+ function emit_xyz(out, u, v, tri_uv, px, py, pz, e1u, e1v, e2u, e2v, inv_det) {
461
+ const du = u - tri_uv[0];
462
+ const dv = v - tri_uv[1];
463
+ const w1 = (du * e2v - e2u * dv) * inv_det;
464
+ const w2 = (e1u * dv - du * e1v) * inv_det;
465
+ const w0 = 1 - w1 - w2;
466
+
467
+ out.push(
468
+ w0 * px[0] + w1 * px[1] + w2 * px[2],
469
+ w0 * py[0] + w1 * py[1] + w2 * py[2],
470
+ w0 * pz[0] + w1 * pz[1] + w2 * pz[2]
471
+ );
472
+ }
@@ -1 +1 @@
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"}
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,QAmMxB;+BAlP8B,mEAAmE"}
@@ -1,5 +1,4 @@
1
1
  import { assert } from "../../../../core/assert.js";
2
- import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
3
2
  import { BinaryTopology } from "../../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
4
3
  import {
5
4
  bt_mesh_cleanup_faceless_references
@@ -36,15 +35,13 @@ import {
36
35
  } from "../../../../core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js";
37
36
  import { v3_angle_between } from "../../../../core/geom/vec3/v3_angle_between.js";
38
37
  import Vector3 from "../../../../core/geom/Vector3.js";
39
- import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
40
- import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
41
- import { bt_mesh_carve_height_clearance } from "./bt_mesh_carve_height_clearance.js";
42
38
  import {
43
39
  bt_mesh_resolve_t_junctions
44
40
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js";
45
41
  import {
46
42
  bt_mesh_fill_small_holes
47
43
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js";
44
+ import { clip_soup_against_overhangs } from "./clip_soup_against_overhangs.js";
48
45
 
49
46
 
50
47
  /**
@@ -85,11 +82,6 @@ export function navmesh_build_topology({
85
82
  // outward winding; that remains an input contract we cannot recover from here.
86
83
  bt_mesh_compute_face_normals(source);
87
84
 
88
- const source_bvh = new BVH();
89
-
90
- // prepare a BVH, we'll need it for height queries later on
91
- bvh_build_from_bt_mesh(source_bvh, source);
92
-
93
85
  const scratch_normal = new Float32Array(3);
94
86
 
95
87
  // unpack topology into triangle soup
@@ -103,7 +95,7 @@ export function navmesh_build_topology({
103
95
  *
104
96
  * @type {number[]}
105
97
  */
106
- const raw_triangles = [];
98
+ let raw_triangles = [];
107
99
  let triangle_count = 0;
108
100
 
109
101
  for (let face_id = 0; face_id < source_face_count; face_id++) {
@@ -147,14 +139,6 @@ export function navmesh_build_topology({
147
139
  }
148
140
 
149
141
  {
150
- const bvh = new BVH();
151
-
152
- // first dump all triangles into a BVH for speed of access
153
- bvh_build_from_unindexed_triangles(bvh, raw_triangles, triangle_count);
154
-
155
- // find possible triangle edge connections, that is - where edges of 2 triangles touch, but don't match exactly.
156
- // the touching edges will need to be cut and affected triangles split accordingly
157
-
158
142
  const mesh = new BinaryTopology();
159
143
 
160
144
  // raw_triangles may have stale tail data from filtering steps above (e.g. degenerate/steep);
@@ -180,27 +164,10 @@ export function navmesh_build_topology({
180
164
  bt_mesh_fuse_duplicate_edges(mesh);
181
165
  }
182
166
 
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.
188
- if (agent_height > 0) {
189
- bt_mesh_carve_height_clearance({
190
- mesh,
191
- source,
192
- source_bvh,
193
- agent_height,
194
- agent_radius: 0,
195
- up,
196
- });
197
-
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.
167
+ // --- Erode the outer boundary by the agent radius (no obstacles carved yet) ---
168
+ // The erosion uses an exact Euclidean distance field with cut-band refinement, so the inset is
169
+ // uniform. Obstacles are carved afterwards (below) as pre-dilated footprints, so they are not
170
+ // eroded here a second time.
204
171
  {
205
172
  const islands = bt_mesh_compute_face_islands(mesh);
206
173
 
@@ -214,6 +181,36 @@ export function navmesh_build_topology({
214
181
  bt_mesh_compact(mesh);
215
182
  }
216
183
 
184
+ // --- Carve obstacle footprints, pre-dilated by the agent radius with rounded corners ---
185
+ // Subtract each overhead obstacle's r-dilated footprint from the (already outer-eroded) surface.
186
+ // The hole boundary is then the true offset of the obstacle - straight along edges, a clean arc
187
+ // at corners - instead of the wavy / mitred contour that sampling-then-eroding produced.
188
+ if (agent_height > 0) {
189
+ const soup = [];
190
+ const sv = [0, 0, 0];
191
+ for (let f = 0; f < mesh.faces.size; f++) {
192
+ if (!mesh.faces.is_allocated(f)) continue;
193
+ let l = mesh.face_read_loop(f);
194
+ for (let i = 0; i < 3; i++) {
195
+ mesh.vertex_read_coordinate(sv, 0, mesh.loop_read_vertex(l));
196
+ soup.push(sv[0], sv[1], sv[2]);
197
+ l = mesh.loop_read_next(l);
198
+ }
199
+ }
200
+
201
+ const carved = clip_soup_against_overhangs(soup, (soup.length / 9) | 0, source, agent_height, agent_radius, up);
202
+
203
+ const carved_mesh = new BinaryTopology();
204
+ bt_mesh_from_unindexed_geometry(carved_mesh, carved);
205
+ bt_merge_verts_by_distance(carved_mesh, 1e-6);
206
+ bt_mesh_fuse_duplicate_edges(carved_mesh);
207
+ if (bt_mesh_resolve_t_junctions(carved_mesh, 1e-4) > 0) {
208
+ bt_merge_verts_by_distance(carved_mesh, 1e-6);
209
+ bt_mesh_fuse_duplicate_edges(carved_mesh);
210
+ }
211
+ mesh.copy(carved_mesh);
212
+ }
213
+
217
214
  // bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
218
215
  // Opt-in: with both step params 0 (the default) the topology is left untouched.
219
216
  if (agent_max_step_height > 0 || agent_max_step_distance > 0) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Find the exact shortest path from (sx,sy,sz) in `start_face` to (gx,gy,gz) in `goal_face`.
3
+ *
4
+ * @param {number[]|Float64Array|Float32Array} output path written as flat [x0,y0,z0, x1,y1,z1, ...]
5
+ * @param {BinaryTopology} topology triangle mesh (3D vertex coordinates)
6
+ * @param {number} sx
7
+ * @param {number} sy
8
+ * @param {number} sz start point (assumed on/near `start_face`)
9
+ * @param {number} start_face triangle containing the start point
10
+ * @param {number} gx
11
+ * @param {number} gy
12
+ * @param {number} gz goal point (assumed on/near `goal_face`)
13
+ * @param {number} goal_face triangle containing the goal point
14
+ * @returns {number} number of path POINTS written (3 numbers each), 0 if no path
15
+ */
16
+ export function navmesh_polyanya_find_path(output: number[] | Float64Array | Float32Array, topology: BinaryTopology, sx: number, sy: number, sz: number, start_face: number, gx: number, gy: number, gz: number, goal_face: number): number;
17
+ //# sourceMappingURL=navmesh_polyanya_find_path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navmesh_polyanya_find_path.d.ts","sourceRoot":"","sources":["../../../../../src/engine/navigation/mesh/navmesh_polyanya_find_path.js"],"names":[],"mappings":"AAqTA;;;;;;;;;;;;;;GAcG;AACH,mDAZW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,gCAElC,MAAM,MACN,MAAM,MACN,MAAM,cACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,aACN,MAAM,GACJ,MAAM,CAmFlB"}