@woosh/meep-engine 2.148.0 → 2.150.0

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.d.ts +23 -0
  3. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.d.ts.map +1 -0
  4. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.js +295 -0
  5. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  6. package/src/engine/graphics/GraphicsEngine.js +18 -8
  7. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  8. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +8 -5
  9. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +18 -10
  10. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +1 -1
  11. package/src/engine/navigation/mesh/NavigationMesh.d.ts +6 -2
  12. package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
  13. package/src/engine/navigation/mesh/NavigationMesh.js +234 -212
  14. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +7 -3
  15. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  16. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +67 -73
  17. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts +16 -5
  18. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts.map +1 -1
  19. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.js +262 -147
  20. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  21. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +33 -3
  22. package/src/engine/navigation/mesh/bvh_query_nearest_face.d.ts +4 -1
  23. package/src/engine/navigation/mesh/bvh_query_nearest_face.d.ts.map +1 -1
  24. package/src/engine/navigation/mesh/bvh_query_nearest_face.js +164 -131
  25. package/src/engine/physics/broadphase/generate_pairs.js +110 -110
  26. package/src/engine/physics/queries/raycast.js +201 -201
@@ -1,71 +1,174 @@
1
1
  import { assert } from "../../../../core/assert.js";
2
- import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
2
+ import { NULL_NODE } from "../../../../core/bvh2/bvh3/BVH.js";
3
+ import {
4
+ bvh_query_leaves_ray_segment
5
+ } from "../../../../core/bvh2/bvh3/query/bvh_query_leaves_ray_segment.js";
6
+ import { NULL_POINTER } from "../../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
3
7
  import { compute_triangle_area_3d } from "../../../../core/geom/3d/triangle/compute_triangle_area_3d.js";
4
- import { v3_compute_triangle_normal } from "../../../../core/geom/3d/triangle/v3_compute_triangle_normal.js";
8
+ import {
9
+ computeTriangleRayIntersectionBarycentric
10
+ } from "../../../../core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js";
5
11
  import { clamp } from "../../../../core/math/clamp.js";
6
- import { roundFair } from "../../../../core/math/random/roundFair.js";
7
12
 
8
- function generate_hole_shapes_barycentric(
9
- holes_positions,
10
- hole_size,
11
- ax, ay, az,
12
- bx, by, bz,
13
- cx, cy, cz,
14
- ) {
15
- throw new Error('Not implemented');
16
- }
13
+ /**
14
+ * Upper bound on the per-edge subdivision factor, so a single huge triangle cannot explode into an
15
+ * unbounded number of sub-triangles. At this cap a triangle is sampled with up to K_MAX^2 points.
16
+ * @type {number}
17
+ */
18
+ const K_MAX = 64;
19
+
20
+ /**
21
+ * Small offset (in world units) used to lift a ray origin off the surface it sits on, so the upward
22
+ * clearance ray does not immediately re-intersect the floor it starts from.
23
+ * @type {number}
24
+ */
25
+ const SURFACE_EPSILON = 1e-4;
26
+
27
+ // reused scratch (build-time, but there's no reason to churn allocations per triangle)
28
+ const ray_leaf_buffer = [];
29
+ const intersection_result = new Float32Array(6);
30
+ const tri_a = new Float32Array(3);
31
+ const tri_b = new Float32Array(3);
32
+ const tri_c = new Float32Array(3);
17
33
 
18
34
  /**
35
+ * True if there is no source geometry within `agent_height` directly above the point, measured along
36
+ * the world up direction. Casts a ray up from the point (nudged off the surface) and checks every
37
+ * source face whose AABB the ray crosses with a precise ray-triangle test.
19
38
  *
20
- * @param {number[]} output where to write new triangles
21
- * @param {number} output_offset
22
- * @param {number[][]} hole_shapes
23
- * @param {number} ax
24
- * @param {number} ay
25
- * @param {number} az
26
- * @param {number} bx
27
- * @param {number} by
28
- * @param {number} bz
29
- * @param {number} cx
30
- * @param {number} cy
31
- * @param {number} cz
32
- * @returns {number} new triangles added to the output
39
+ * @param {BVH} bvh source-geometry BVH (leaves carry source face IDs)
40
+ * @param {BinaryTopology} source source mesh, used to resolve face IDs to triangle vertices
41
+ * @param {number} px
42
+ * @param {number} py
43
+ * @param {number} pz
44
+ * @param {number} up_x normalized world up
45
+ * @param {number} up_y
46
+ * @param {number} up_z
47
+ * @param {number} agent_height
48
+ * @returns {boolean}
33
49
  */
34
- function triangle_punch_holes(
50
+ function point_has_clearance(bvh, source, px, py, pz, up_x, up_y, up_z, agent_height) {
51
+ const origin_x = px + up_x * SURFACE_EPSILON;
52
+ const origin_y = py + up_y * SURFACE_EPSILON;
53
+ const origin_z = pz + up_z * SURFACE_EPSILON;
54
+
55
+ const leaf_count = bvh_query_leaves_ray_segment(
56
+ bvh, bvh.root,
57
+ ray_leaf_buffer, 0,
58
+ origin_x, origin_y, origin_z,
59
+ up_x, up_y, up_z,
60
+ 0, agent_height
61
+ );
62
+
63
+ for (let i = 0; i < leaf_count; i++) {
64
+ const node = ray_leaf_buffer[i];
65
+ const face_id = bvh.node_get_user_data(node);
66
+
67
+ const loop_a = source.face_read_loop(face_id);
68
+
69
+ if (loop_a === NULL_POINTER) {
70
+ continue;
71
+ }
72
+
73
+ const loop_b = source.loop_read_next(loop_a);
74
+ const loop_c = source.loop_read_next(loop_b);
75
+
76
+ source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(loop_a));
77
+ source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(loop_b));
78
+ source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(loop_c));
79
+
80
+ const hit = computeTriangleRayIntersectionBarycentric(
81
+ intersection_result,
82
+ origin_x, origin_y, origin_z,
83
+ up_x, up_y, up_z,
84
+ tri_a[0], tri_a[1], tri_a[2],
85
+ tri_b[0], tri_b[1], tri_b[2],
86
+ tri_c[0], tri_c[1], tri_c[2]
87
+ );
88
+
89
+ if (!hit) {
90
+ continue;
91
+ }
92
+
93
+ const t = intersection_result[0];
94
+
95
+ if (t > 0 && t <= agent_height) {
96
+ // an overhead obstruction sits within the agent's height
97
+ return false;
98
+ }
99
+ }
100
+
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Append a triangle (9 floats) to `output`.
106
+ */
107
+ function emit_triangle(
35
108
  output,
36
- output_offset,
37
- hole_shapes,
38
109
  ax, ay, az,
39
110
  bx, by, bz,
40
- cx, cy, cz,
111
+ cx, cy, cz
41
112
  ) {
42
- throw new Error('Not implemented');
113
+ output.push(ax, ay, az, bx, by, bz, cx, cy, cz);
43
114
  }
44
115
 
45
116
  /**
117
+ * Knock holes in the walkable triangle soup wherever an agent of the given height would not fit under
118
+ * overhead geometry.
46
119
  *
47
- * @param {BVH} bvh
120
+ * Approach: subdivide each candidate triangle to a resolution tied to the agent's footprint, cast an
121
+ * upward clearance ray from each sub-triangle centroid against the source geometry, and keep only the
122
+ * sub-triangles that have clearance. Fully-clear triangles are emitted unchanged (no subdivision), so
123
+ * the common case adds no triangles; fully-blocked triangles are dropped; partially-blocked triangles
124
+ * are replaced by their clear sub-triangles. Accuracy is bounded by the sampling resolution.
125
+ *
126
+ * The `triangles` array is rewritten in place with the surviving triangles.
127
+ *
128
+ * @param {BVH} bvh source-geometry BVH (leaves carry source face IDs)
129
+ * @param {BinaryTopology} source source mesh, used to resolve face IDs to triangle vertices
48
130
  * @param {number} agent_height
49
131
  * @param {number} agent_radius
50
- * @param {number} triangle_count
51
- * @param {number[]} triangles
52
- * @param {function():number} random
132
+ * @param {number} triangle_count number of triangles currently in `triangles`
133
+ * @param {number[]} triangles flat XYZ soup, 9 floats per triangle
134
+ * @param {Vector3} up world up direction
53
135
  * @returns {number} new triangle count
54
136
  */
55
137
  export function enforce_agent_height_clearance({
56
138
  bvh,
139
+ source,
57
140
  agent_height,
58
141
  agent_radius,
59
142
  triangle_count,
60
143
  triangles,
61
- random
144
+ up,
62
145
  }) {
63
- console.warn('enforce_agent_height_clearance() is not implemented');
64
146
 
65
- return triangle_count;
147
+ assert.defined(bvh, 'bvh');
148
+ assert.defined(source, 'source');
149
+ assert.greaterThan(agent_height, 0, 'agent_height');
150
+
151
+ if (bvh.root === NULL_NODE) {
152
+ // no source geometry to obstruct anything
153
+ return triangle_count;
154
+ }
155
+
156
+ // normalize up so ray-intersection distances are world distances
157
+ let up_x = up.x;
158
+ let up_y = up.y;
159
+ let up_z = up.z;
160
+
161
+ const up_length = Math.sqrt(up_x * up_x + up_y * up_y + up_z * up_z);
162
+
163
+ assert.greaterThan(up_length, 0, 'up vector length');
164
+
165
+ up_x /= up_length;
166
+ up_y /= up_length;
167
+ up_z /= up_length;
66
168
 
67
169
  /**
68
- * Spatial resolution for performing clearance checks
170
+ * Resolution at which clearance is sampled. Tied to the agent footprint so the sampling grid is
171
+ * fine enough to resolve gaps the agent could (or could not) fit through.
69
172
  * @type {number}
70
173
  */
71
174
  const sampling_resolution = Math.min(
@@ -73,132 +176,144 @@ export function enforce_agent_height_clearance({
73
176
  Math.max(agent_radius / 2, 0.001)
74
177
  );
75
178
 
76
- assert.notNaN(sampling_resolution, 'sampling_resolution');
77
179
  assert.isFinite(sampling_resolution, 'sampling_resolution');
180
+ assert.greaterThan(sampling_resolution, 0, 'sampling_resolution');
78
181
 
79
- const sample_area = sampling_resolution * sampling_resolution;
80
-
81
- const triangle_normal = new Float32Array(3);
82
- const ray = new Ray3();
83
- ray.tMax = agent_height;
84
-
85
- let current_triangle_count = triangle_count;
86
-
87
- for (let i = 0; i < current_triangle_count; i++) {
88
- const triangle_address = i * 9;
182
+ /**
183
+ * Surviving triangles. Built separately because the count can grow (subdivision), then copied back.
184
+ * @type {number[]}
185
+ */
186
+ const output = [];
89
187
 
90
- const ax = triangles[triangle_address];
91
- const ay = triangles[triangle_address + 1];
92
- const az = triangles[triangle_address + 2];
188
+ for (let i = 0; i < triangle_count; i++) {
189
+ const address = i * 9;
93
190
 
94
- const bx = triangles[triangle_address + 3];
95
- const by = triangles[triangle_address + 4];
96
- const bz = triangles[triangle_address + 5];
191
+ const ax = triangles[address];
192
+ const ay = triangles[address + 1];
193
+ const az = triangles[address + 2];
97
194
 
98
- const cx = triangles[triangle_address + 6];
99
- const cy = triangles[triangle_address + 7];
100
- const cz = triangles[triangle_address + 8];
195
+ const bx = triangles[address + 3];
196
+ const by = triangles[address + 4];
197
+ const bz = triangles[address + 5];
101
198
 
102
- // TODO we can accelerate this by performing a clipping volume query first instead
103
- // majority of the triangles are expected to fully pass clearance, and a single clipping volume test will be able to give us "Not obstructed" answer.
199
+ const cx = triangles[address + 6];
200
+ const cy = triangles[address + 7];
201
+ const cz = triangles[address + 8];
104
202
 
105
- const triangle_area = compute_triangle_area_3d(
106
- ax, ay, az,
107
- bx, by, bz,
108
- cx, cy, cz
109
- );
203
+ const area = compute_triangle_area_3d(ax, ay, az, bx, by, bz, cx, cy, cz);
110
204
 
111
- if (triangle_area === 0) {
112
- // a degenerate triangle, should not happen but no point in sampling
205
+ if (area === 0) {
206
+ // degenerate, nothing meaningful to sample - keep as-is
207
+ emit_triangle(output, ax, ay, az, bx, by, bz, cx, cy, cz);
113
208
  continue;
114
209
  }
115
210
 
116
- // construct normal
117
- v3_compute_triangle_normal(
118
- triangle_normal, 0,
119
- ax, ay, az,
120
- bx, by, bz,
121
- cx, cy, cz
122
- );
123
-
124
- const sample_count_desired = triangle_area / sample_area;
125
- const sample_count_rounded = roundFair(sample_count_desired, random);
126
-
127
- // Guard against degenerate cases
128
- const SAMPLE_COUNT_MAX = 32000;
129
-
130
- const sample_count = clamp(sample_count_rounded, 1, SAMPLE_COUNT_MAX);
131
-
132
- // perform sampling
133
- const normal_x = triangle_normal[0];
134
- const normal_y = triangle_normal[1];
135
- const normal_z = triangle_normal[2];
136
-
137
- ray.setDirection(normal_x, normal_y, normal_z);
138
-
139
- // construct edges for sampling
140
- const e0x = bx - ax;
141
- const e0y = by - ay;
142
- const e0z = bz - az;
143
-
144
- const e1x = cx - bx;
145
- const e1y = cy - by;
146
- const e1z = cz - bz;
147
-
148
- /**
149
- * Barycentric coordinates of where the holes are
150
- * @type {number[]}
151
- */
152
- const holes = [];
153
-
154
- for (let j = 0; j < sample_count; j++) {
155
-
156
- // perform initial sample
157
- const r0 = random();
158
- const r1 = random() * (1 - r0);
159
-
160
- const x = ax + r0 * e0x + r1 * e1x;
161
- const y = ay + r0 * e0y + r1 * e1y;
162
- const z = az + r0 * e0z + r1 * e1z;
163
-
164
- ray.setOrigin(x, y, z);
165
-
166
- // tiny offset to avoid self-occlusion
167
- ray.shiftForward(1e-7);
168
-
169
- // TODO actual raycast
170
-
171
- // if we got a hit - create a hole triangle, if no hit - continue
172
- holes.push(r0, r1);
211
+ // edges, used both to size the subdivision and to place sub-triangle vertices
212
+ const e0x = bx - ax, e0y = by - ay, e0z = bz - az; // a -> b
213
+ const e1x = cx - ax, e1y = cy - ay, e1z = cz - az; // a -> c
214
+
215
+ const len_ab = Math.sqrt(e0x * e0x + e0y * e0y + e0z * e0z);
216
+ const len_ac = Math.sqrt(e1x * e1x + e1y * e1y + e1z * e1z);
217
+ const dbcx = cx - bx, dbcy = cy - by, dbcz = cz - bz;
218
+ const len_bc = Math.sqrt(dbcx * dbcx + dbcy * dbcy + dbcz * dbcz);
219
+
220
+ const longest_edge = Math.max(len_ab, len_ac, len_bc);
221
+
222
+ const k = clamp(Math.ceil(longest_edge / sampling_resolution), 1, K_MAX);
223
+
224
+ const inv_k = 1 / k;
225
+
226
+ // Sub-triangle vertices are P(i,j) = a + (i/k)*(b-a) + (j/k)*(c-a), for i+j <= k.
227
+ // Walk the k^2 sub-triangles (k*(k+... )/...) testing each centroid for clearance.
228
+ let any_blocked = false;
229
+ let any_clear = false;
230
+
231
+ // reuse a per-triangle scratch list of clear sub-triangles only when we actually subdivide
232
+ const clear_subtriangles = [];
233
+
234
+ for (let row = 0; row < k; row++) {
235
+ for (let col = 0; col < k - row; col++) {
236
+ // "upward" sub-triangle: P(col,row), P(col+1,row), P(col,row+1)
237
+ emit_subtriangle_if_clear(
238
+ bvh, source, agent_height, up_x, up_y, up_z,
239
+ ax, ay, az, e0x, e0y, e0z, e1x, e1y, e1z, inv_k,
240
+ col, row, col + 1, row, col, row + 1,
241
+ clear_subtriangles
242
+ ) ? (any_clear = true) : (any_blocked = true);
243
+
244
+ // "downward" sub-triangle: P(col+1,row), P(col+1,row+1), P(col,row+1)
245
+ if (col < k - row - 1) {
246
+ emit_subtriangle_if_clear(
247
+ bvh, source, agent_height, up_x, up_y, up_z,
248
+ ax, ay, az, e0x, e0y, e0z, e1x, e1y, e1z, inv_k,
249
+ col + 1, row, col + 1, row + 1, col, row + 1,
250
+ clear_subtriangles
251
+ ) ? (any_clear = true) : (any_blocked = true);
252
+ }
253
+ }
173
254
  }
174
255
 
175
- if (holes.length === 0) {
176
- // clearance OK
177
- continue;
256
+ if (!any_blocked) {
257
+ // fully clear - keep the original triangle, no subdivision, no bloat
258
+ emit_triangle(output, ax, ay, az, bx, by, bz, cx, cy, cz);
259
+ } else if (any_clear) {
260
+ // partially blocked - replace with the surviving sub-triangles
261
+ for (let s = 0; s < clear_subtriangles.length; s++) {
262
+ output.push(clear_subtriangles[s]);
263
+ }
178
264
  }
265
+ // else fully blocked - drop the triangle entirely
266
+ }
179
267
 
180
- const hole_shapes = generate_hole_shapes_barycentric(
181
- holes,
182
- sampling_resolution,
183
- ax, ay, az,
184
- bx, by, bz,
185
- cx, cy, cz
186
- );
268
+ // rewrite the soup in place with the survivors
269
+ for (let i = 0; i < output.length; i++) {
270
+ triangles[i] = output[i];
271
+ }
272
+ triangles.length = output.length;
187
273
 
188
- const added_triangle_count = triangle_punch_holes(
189
- triangles,
190
- triangle_count,
191
- hole_shapes,
192
- ax, ay, az,
193
- bx, by, bz,
194
- cx, cy, cz
195
- );
274
+ return (output.length / 9) | 0;
275
+ }
196
276
 
197
- triangle_count += added_triangle_count;
277
+ /**
278
+ * Build sub-triangle vertices P(i,j) = a + (i/k)*e0 + (j/k)*e1, test its centroid for clearance, and
279
+ * push its 9 coordinates into `out` when clear. Returns whether it was clear.
280
+ */
281
+ function emit_subtriangle_if_clear(
282
+ bvh, source, agent_height, up_x, up_y, up_z,
283
+ ax, ay, az, e0x, e0y, e0z, e1x, e1y, e1z, inv_k,
284
+ i0, j0, i1, j1, i2, j2,
285
+ out
286
+ ) {
287
+ const s0 = i0 * inv_k, t0 = j0 * inv_k;
288
+ const s1 = i1 * inv_k, t1 = j1 * inv_k;
289
+ const s2 = i2 * inv_k, t2 = j2 * inv_k;
290
+
291
+ const p0x = ax + s0 * e0x + t0 * e1x;
292
+ const p0y = ay + s0 * e0y + t0 * e1y;
293
+ const p0z = az + s0 * e0z + t0 * e1z;
294
+
295
+ const p1x = ax + s1 * e0x + t1 * e1x;
296
+ const p1y = ay + s1 * e0y + t1 * e1y;
297
+ const p1z = az + s1 * e0z + t1 * e1z;
298
+
299
+ const p2x = ax + s2 * e0x + t2 * e1x;
300
+ const p2y = ay + s2 * e0y + t2 * e1y;
301
+ const p2z = az + s2 * e0z + t2 * e1z;
302
+
303
+ const centroid_x = (p0x + p1x + p2x) / 3;
304
+ const centroid_y = (p0y + p1y + p2y) / 3;
305
+ const centroid_z = (p0z + p1z + p2z) / 3;
306
+
307
+ const clear = point_has_clearance(
308
+ bvh, source,
309
+ centroid_x, centroid_y, centroid_z,
310
+ up_x, up_y, up_z,
311
+ agent_height
312
+ );
198
313
 
199
- // TODO remove the original triangle, cut out the holes and add new triangles back
314
+ if (clear) {
315
+ out.push(p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z);
200
316
  }
201
317
 
202
- //
203
- return current_triangle_count;
204
- }
318
+ return clear;
319
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AAsCA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QA2JxB;+BAjM8B,mEAAmE"}
1
+ {"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AA2CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAoLxB;+BA/N8B,mEAAmE"}
@@ -4,6 +4,9 @@ import { BinaryTopology } from "../../../../core/geom/3d/topology/struct/binary/
4
4
  import {
5
5
  bt_mesh_cleanup_faceless_references
6
6
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_cleanup_faceless_references.js";
7
+ import {
8
+ bt_mesh_compact
9
+ } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_compact.js";
7
10
  import {
8
11
  bt_mesh_compute_face_normals
9
12
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.js";
@@ -25,12 +28,14 @@ import {
25
28
  bt_merge_verts_by_distance
26
29
  } from "../../../../core/geom/3d/topology/struct/binary/io/vertex/bt_merge_verts_by_distance.js";
27
30
  import { bt_face_is_degenerate } from "../../../../core/geom/3d/topology/struct/binary/query/bt_face_is_degenerate.js";
31
+ import {
32
+ bt_mesh_bridge_islands
33
+ } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.js";
28
34
  import {
29
35
  bt_mesh_compute_face_islands
30
36
  } from "../../../../core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js";
31
37
  import { v3_angle_between } from "../../../../core/geom/vec3/v3_angle_between.js";
32
38
  import Vector3 from "../../../../core/geom/Vector3.js";
33
- import { seededRandom } from "../../../../core/math/random/seededRandom.js";
34
39
  import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
35
40
  import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
36
41
  import { enforce_agent_height_clearance } from "./enforce_agent_height_clearance.js";
@@ -67,7 +72,12 @@ export function navmesh_build_topology({
67
72
  assert.greaterThanOrEqual(agent_max_step_height, 0, 'agent_max_step_height');
68
73
  assert.greaterThanOrEqual(agent_max_step_distance, 0, 'agent_max_step_distance');
69
74
 
70
- const random = seededRandom();
75
+ // The walkable-angle filter below reads each source face's normal. Compute them defensively rather
76
+ // than relying on an undocumented "caller already populated normals" precondition - normals are
77
+ // derived data, so this is idempotent. NOTE: a single face normal cannot distinguish a correctly
78
+ // wound floor from a flipped one (or a ceiling), so the source is still required to have consistent
79
+ // outward winding; that remains an input contract we cannot recover from here.
80
+ bt_mesh_compute_face_normals(source);
71
81
 
72
82
  const source_bvh = new BVH();
73
83
 
@@ -135,11 +145,12 @@ export function navmesh_build_topology({
135
145
 
136
146
  triangle_count = enforce_agent_height_clearance({
137
147
  bvh: source_bvh,
148
+ source: source,
138
149
  agent_height: agent_height,
139
150
  agent_radius: agent_radius,
140
151
  triangle_count: triangle_count,
141
152
  triangles: raw_triangles,
142
- random: random
153
+ up: up,
143
154
  });
144
155
 
145
156
  }
@@ -186,6 +197,25 @@ export function navmesh_build_topology({
186
197
  // remove dangling references
187
198
  bt_mesh_cleanup_faceless_references(mesh);
188
199
 
200
+ // Island erosion (above) kills faces via the pool's free-list, leaving holes in the ID space.
201
+ // `faces.size` is a high-water mark, not a live count, so consumers that iterate `0..faces.size`
202
+ // (e.g. bvh_build_from_bt_mesh) would touch freed slots. Compact so every consumer sees a dense,
203
+ // hole-free topology - this is the single invariant downstream code relies on.
204
+ bt_mesh_compact(mesh);
205
+
206
+ // bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
207
+ // Opt-in: with both step params 0 (the default) the topology is left untouched.
208
+ if (agent_max_step_height > 0 || agent_max_step_distance > 0) {
209
+ bt_mesh_bridge_islands(mesh, {
210
+ max_step_height: agent_max_step_height,
211
+ max_step_distance: agent_max_step_distance,
212
+ up,
213
+ });
214
+
215
+ // bridging rebuilds via vertex-merge / edge-fuse, which can leave holes; re-densify
216
+ bt_mesh_compact(mesh);
217
+ }
218
+
189
219
  // face normals are consumed by navigation queries (string-pulling portal normals), populate them now
190
220
  bt_mesh_compute_face_normals(mesh);
191
221
 
@@ -8,8 +8,11 @@
8
8
  * @param {number} x
9
9
  * @param {number} y
10
10
  * @param {number} z
11
+ * @param {Float32Array|number[]} out_point the closest point ON the winning face's surface (the snapped
12
+ * contact point) is written here when a face is found. Required, must not be null.
13
+ * @param {number} [out_point_offset=0] offset into `out_point` to write the snapped XYZ triple
11
14
  * @param {number} [max_distance=Infinity] optional cutoff, only faces within this distance are considered
12
15
  * @returns {number} face ID of the nearest face, or {@link NULL_POINTER} if no face was found within the cutoff
13
16
  */
14
- export function bvh_query_nearest_face(bvh: BVH, mesh: BinaryTopology, x: number, y: number, z: number, max_distance?: number): number;
17
+ export function bvh_query_nearest_face(bvh: BVH, mesh: BinaryTopology, x: number, y: number, z: number, out_point: Float32Array | number[], out_point_offset?: number, max_distance?: number): number;
15
18
  //# sourceMappingURL=bvh_query_nearest_face.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bvh_query_nearest_face.d.ts","sourceRoot":"","sources":["../../../../../src/engine/navigation/mesh/bvh_query_nearest_face.js"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;GAYG;AACH,0EANW,MAAM,KACN,MAAM,KACN,MAAM,iBACN,MAAM,GACJ,MAAM,CA+FlB"}
1
+ {"version":3,"file":"bvh_query_nearest_face.d.ts","sourceRoot":"","sources":["../../../../../src/engine/navigation/mesh/bvh_query_nearest_face.js"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;GAeG;AACH,0EATW,MAAM,KACN,MAAM,KACN,MAAM,aACN,YAAY,GAAC,MAAM,EAAE,qBAErB,MAAM,iBACN,MAAM,GACJ,MAAM,CA+HlB"}