@woosh/meep-engine 2.163.8 → 2.163.10

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/core/collection/heap/IndexedFloatMaxHeap.d.ts.map +1 -0
  3. package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.js +1 -1
  4. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts +4 -2
  5. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts.map +1 -1
  6. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +6 -4
  7. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  8. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +2 -26
  9. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts +19 -0
  10. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts.map +1 -0
  11. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js +555 -0
  12. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts +13 -0
  13. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts.map +1 -0
  14. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.js +28 -0
  15. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts +1 -1
  16. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts.map +1 -1
  17. package/src/core/graph/metis/native/bisection/BisectionScratch.js +1 -1
  18. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts +1 -1
  19. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts.map +1 -1
  20. package/src/core/graph/metis/native/refine/RefinementScratch.js +1 -1
  21. package/src/engine/navigation/mesh/NavigationMesh.d.ts +6 -0
  22. package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
  23. package/src/engine/navigation/mesh/NavigationMesh.js +145 -234
  24. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +61 -17
  25. package/src/engine/navigation/mesh/{navmesh_polyanya_find_path.d.ts → bt_mesh_face_find_path_polyanya.d.ts} +2 -2
  26. package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts.map +1 -0
  27. package/src/engine/navigation/mesh/{navmesh_polyanya_find_path.js → bt_mesh_face_find_path_polyanya.js} +117 -48
  28. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  29. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +25 -0
  30. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts +21 -0
  31. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts.map +1 -0
  32. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js +133 -0
  33. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.d.ts.map +0 -1
  34. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts.map +0 -1
  35. /package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.d.ts +0 -0
@@ -1,7 +1,6 @@
1
1
  import { Uint32Heap4 } from "../../../core/collection/heap/Uint32Heap4.js";
2
2
  import { line_segment_intersection_fraction_2d } from "../../../core/geom/2d/line/line_segment_intersection_fraction_2d.js";
3
3
  import { triangle2d_compute_area } from "../../../core/geom/2d/triangle2d_compute_area.js";
4
- import { BinaryElementPool } from "../../../core/geom/3d/topology/struct/binary/BinaryElementPool.js";
5
4
  import { NULL_POINTER } from "../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
6
5
  import { v2_distance } from "../../../core/geom/vec2/v2_distance.js";
7
6
 
@@ -28,13 +27,18 @@ import { v2_distance } from "../../../core/geom/vec2/v2_distance.js";
28
27
  * shortest root->interval->goal measured with straight 3D chords (a lower bound on the geodesic, hence
29
28
  * admissible); the terminal node's *exact* cost unfolds the goal into the node's frame instead.
30
29
  *
31
- * The output path is lifted back to 3D from vertex ids: turning corners are mesh vertices (exact
32
- * positions), the start/goal are the given 3D points, and a goal-edge bend is the exact point on that
33
- * edge. Standalone -- does not (yet) replace the bt_mesh_face_find_path + funnel pipeline in NavigationMesh.
30
+ * The output path FOLLOWS THE SURFACE: it is subdivided at every face-boundary edge the geodesic
31
+ * crosses, so each emitted segment lies within a single triangle -- it never flies over a convex crease
32
+ * nor tunnels through a concave fold. Turning corners are mesh vertices (exact positions); each edge
33
+ * crossing is the exact line-vs-edge intersection lifted to 3D by lerping the edge's two endpoints; the
34
+ * start/goal are the given 3D points. Collinear runs (a flat stretch with no real bend) are collapsed to
35
+ * their endpoints, so on a planar mesh the output is just the minimal corner polyline -- the subdivision
36
+ * only adds points where the surface actually folds. Wired into NavigationMesh.find_path.
34
37
  */
35
38
 
36
39
  const EPS = 1e-9; // area-sign slack (cone inside/outside tests)
37
40
  const DEDUP_EPS = 1e-7; // merge consecutive output points equal to within f32 reconstruction error
41
+ const COLLINEAR_EPS = 1e-4; // drop a path point whose perpendicular offset from its neighbours / span is below this
38
42
 
39
43
  // Parameter-space tolerance for edge fractions, interval coverage and vertex snapping. Vertices are
40
44
  // stored as float32, so a point that is geometrically on an edge endpoint or already-covered carries
@@ -66,7 +70,7 @@ function touch_distance(rx, ry, i0x, i0y, i1x, i1y, gx, gy) {
66
70
  tx = _reflect[0]; ty = _reflect[1];
67
71
  }
68
72
  const t = line_segment_intersection_fraction_2d(rx, ry, tx, ty, i0x, i0y, i1x, i1y);
69
- if (!Number.isNaN(t) && t >= -PEPS && t <= 1 + PEPS) {
73
+ if (t >= -PEPS && t <= 1 + PEPS) { // -1 (no crossing) falls below -PEPS, so the range test rejects it
70
74
  return v2_distance(rx, ry, tx, ty);
71
75
  }
72
76
  return Math.min(
@@ -248,19 +252,20 @@ function is_corner(topology, vid) {
248
252
 
249
253
  // ---- node storage ----------------------------------------------------------------------------------
250
254
 
251
- // Search nodes live as fixed 16-word records in a BinaryElementPool's buffer, referenced by integer id,
255
+ // Search nodes are fixed 16-word records packed into one flat ArrayBuffer, addressed by id*NODE_WORDS,
252
256
  // so the open list is a Uint32Heap4 (id + score) rather than an object heap -- NO per-node object
253
257
  // allocation, so the hot path produces no garbage and never triggers a GC pause (which would otherwise
254
- // jitter the rest of the engine). We drive the buffer with our own monotonic counter rather than the
255
- // pool's allocate()/clear(): nodes are never released mid-query and is_allocated() is never read, so the
256
- // pool's per-node occupancy bit and O(capacity) clear() would be pure overhead. The buffer reallocates
257
- // on grow, so the cached data_* views (and the node being expanded) are refreshed accordingly.
258
+ // jitter the rest of the engine). Allocation is a monotonic bump counter reset per query; nodes are
259
+ // never released mid-query, so there is no free list and no occupancy bookkeeping. The buffer only ever
260
+ // grows (by doubling, copying the existing records over) and the two typed-array views are re-cached
261
+ // after a grow. (A pooled allocator with a free list + occupancy bitset degenerates to exactly this
262
+ // under a never-free, bump-allocated access pattern, so a raw buffer is the honest, lighter shape.)
258
263
  //
259
264
  // A node stores its entry edge as vertex ids (E0V,E1V) AND its 2D positions in the node's unfolded frame
260
265
  // (E0X,E0Y,E1X,E1Y), the root (ROOTVID + RX,RY in the frame; ROOTVID==NULL_ID is the start point), and
261
266
  // the interval as the fractions [T0,T1] along the edge. 3D is recovered at reconstruction from vertex
262
267
  // ids; a terminal node stores the goal edge (E0V,E1V) and the final bend's fraction along it (N_FCT,
263
- // NaN == a straight shot).
268
+ // -1 == a straight shot, since a real bend fraction is in [0,1]).
264
269
  const N_FACE = 0, N_E0V = 1, N_E1V = 2;
265
270
  const N_E0X = 3, N_E0Y = 4, N_E1X = 5, N_E1Y = 6;
266
271
  const N_ROOTVID = 7, N_RX = 8, N_RY = 9;
@@ -269,22 +274,23 @@ const N_G = 12, N_PARENT = 13, N_TERMINAL = 14, N_FCT = 15;
269
274
  const NODE_WORDS = 16;
270
275
  const NULL_ID = 0xFFFFFFFF;
271
276
 
272
- const _node_pool = new BinaryElementPool(NODE_WORDS * 4, 1024);
273
277
  const _open = new Uint32Heap4();
274
278
 
279
+ // node records: uint32 and float32 views overlaid on one ArrayBuffer (_node_df aliases _node_du's bytes)
280
+ let _node_capacity = 1024; // capacity in nodes; grows by doubling
281
+ let _node_du = new Uint32Array(_node_capacity * NODE_WORDS);
282
+ let _node_df = new Float32Array(_node_du.buffer);
275
283
  let _node_count = 0;
276
- let _node_capacity = _node_pool.capacity;
277
- let _node_du = _node_pool.data_uint32;
278
- let _node_df = _node_pool.data_float32;
279
284
 
280
285
  /** Reserve the next node slot, growing (and re-caching the views) only when the buffer is full. */
281
286
  function alloc_node() {
282
287
  const id = _node_count++;
283
288
  if (id >= _node_capacity) {
284
- _node_pool.ensure_capacity(id + 1);
285
- _node_capacity = _node_pool.capacity;
286
- _node_du = _node_pool.data_uint32;
287
- _node_df = _node_pool.data_float32;
289
+ _node_capacity *= 2;
290
+ const grown = new Uint32Array(_node_capacity * NODE_WORDS);
291
+ grown.set(_node_du); // copy the existing records into the larger buffer
292
+ _node_du = grown;
293
+ _node_df = new Float32Array(grown.buffer);
288
294
  }
289
295
  return id;
290
296
  }
@@ -298,8 +304,8 @@ let _goal_v0 = 0, _goal_v1 = 0, _goal_v2 = 0;
298
304
  let _goal_w0 = 0, _goal_w1 = 0, _goal_w2 = 0;
299
305
  const _bw = new Float64Array(3);
300
306
 
301
- // root vertex ids gathered along the parent chain during reconstruction (reused, never shrinks)
302
- const _recon_roots = [];
307
+ // edge-crossing points (flat x,y,z triples) gathered along the parent chain during reconstruction
308
+ const _recon = [];
303
309
 
304
310
  function goal_weight(vid) {
305
311
  return vid === _goal_v0 ? _goal_w0 : (vid === _goal_v1 ? _goal_w1 : _goal_w2);
@@ -322,7 +328,7 @@ function goal_weight(vid) {
322
328
  * @param {number} goal_face triangle containing the goal point
323
329
  * @returns {number} number of path POINTS written (3 numbers each), 0 if no path
324
330
  */
325
- export function navmesh_polyanya_find_path(output, topology, sx, sy, sz, start_face, gx, gy, gz, goal_face) {
331
+ export function bt_mesh_face_find_path_polyanya(output, topology, sx, sy, sz, start_face, gx, gy, gz, goal_face) {
326
332
  _sx = sx; _sy = sy; _sz = sz;
327
333
  _gx = gx; _gy = gy; _gz = gz;
328
334
 
@@ -373,41 +379,101 @@ export function navmesh_polyanya_find_path(output, topology, sx, sy, sz, start_f
373
379
 
374
380
  if (best === NULL_ID) return 0;
375
381
 
376
- // reconstruct in 3D: distinct roots (start .. last turning corner), final goal-edge bend, goal
382
+ // Reconstruct a SURFACE-FOLLOWING path. Every node in the parent chain shares one continuous unfolded
383
+ // frame (each node's frame extends its parent's), so the whole route -- start, turning corners, goal --
384
+ // lays out as a single 2D polyline in that frame, and the point where the route crosses each face's
385
+ // entry edge is just the intersection of that polyline with the edge. Lifting each crossing back onto
386
+ // its 3D edge keeps every output segment inside one face (i.e. on the surface). Crossings that fall on
387
+ // a straight (flat) stretch collapse away in push_point, so flat meshes still yield minimal paths.
377
388
  const du = _node_du, df = _node_df;
378
- _recon_roots.length = 0;
389
+ const bb = best * NODE_WORDS;
390
+
391
+ // the goal's position in the shared frame: unfold the goal-face apex from the terminal's entry edge
392
+ const t_e0v = du[bb + N_E0V], t_e1v = du[bb + N_E1V];
393
+ const t_e0x = df[bb + N_E0X], t_e0y = df[bb + N_E0Y], t_e1x = df[bb + N_E1X], t_e1y = df[bb + N_E1Y];
394
+ const g_apex = unfold_apex(topology, goal_face, t_e0v, t_e1v, t_e0x, t_e0y, t_e1x, t_e1y);
395
+ const goal_u = goal_weight(t_e0v) * t_e0x + goal_weight(t_e1v) * t_e1x + goal_weight(g_apex) * _apex[0];
396
+ const goal_v = goal_weight(t_e0v) * t_e0y + goal_weight(t_e1v) * t_e1y + goal_weight(g_apex) * _apex[1];
397
+ const t_fct = df[bb + N_FCT];
398
+
399
+ // walk the chain (goal end -> start), recording each entry-edge crossing in reverse path order
400
+ _recon.length = 0;
401
+ let next_u = goal_u, next_v = goal_v; // the next waypoint toward the goal, in the shared frame
402
+ let prev_root = du[bb + N_ROOTVID];
403
+ let prev_rx = df[bb + N_RX], prev_ry = df[bb + N_RY];
404
+
379
405
  for (let n = best; n !== NULL_ID; n = du[n * NODE_WORDS + N_PARENT]) {
380
- _recon_roots.push(du[n * NODE_WORDS + N_ROOTVID]);
381
- }
406
+ const base = n * NODE_WORDS;
407
+ const rootVid = du[base + N_ROOTVID];
408
+ const rx = df[base + N_RX], ry = df[base + N_RY];
409
+
410
+ // a change of root means the route turned at the previous corridor's root: emit that corner
411
+ // (an exact mesh vertex -- the turning point) and aim subsequent crossings at it
412
+ if (rootVid !== prev_root) {
413
+ if (prev_root !== NULL_ID) {
414
+ topology.vertex_read_coordinate(_va, 0, prev_root);
415
+ _recon.push(_va[0], _va[1], _va[2]);
416
+ }
417
+ next_u = prev_rx; next_v = prev_ry;
418
+ prev_root = rootVid;
419
+ }
382
420
 
383
- let count = 0;
384
- for (let i = _recon_roots.length - 1; i >= 0; i--) {
385
- const vid = _recon_roots[i];
386
- if (vid === NULL_ID) {
387
- count = push_point(output, count, _sx, _sy, _sz);
421
+ const e0v = du[base + N_E0V], e1v = du[base + N_E1V];
422
+ const e0x = df[base + N_E0X], e0y = df[base + N_E0Y], e1x = df[base + N_E1X], e1y = df[base + N_E1Y];
423
+
424
+ // fraction along the entry edge where the route root->next crosses it (the terminal's goal-edge
425
+ // bend is exact and pre-computed as N_FCT)
426
+ let u;
427
+ if (n === best && t_fct !== -1) {
428
+ u = t_fct;
388
429
  } else {
389
- topology.vertex_read_coordinate(_va, 0, vid);
390
- count = push_point(output, count, _va[0], _va[1], _va[2]);
430
+ u = line_segment_intersection_fraction_2d(rx, ry, next_u, next_v, e0x, e0y, e1x, e1y);
431
+ if (u === -1) {
432
+ // the route line grazes a shared vertex (passes through / just outside an edge endpoint,
433
+ // common when the geodesic runs along a row of grid vertices): snap to the nearer endpoint
434
+ const dx = next_u - rx, dy = next_v - ry;
435
+ const s0 = Math.abs(dx * (e0y - ry) - dy * (e0x - rx));
436
+ const s1 = Math.abs(dx * (e1y - ry) - dy * (e1x - rx));
437
+ u = s0 <= s1 ? 0 : 1;
438
+ }
391
439
  }
440
+
441
+ topology.vertex_read_coordinate(_va, 0, e0v);
442
+ topology.vertex_read_coordinate(_vb, 0, e1v);
443
+ _recon.push(_va[0] + (_vb[0] - _va[0]) * u, _va[1] + (_vb[1] - _va[1]) * u, _va[2] + (_vb[2] - _va[2]) * u);
444
+
445
+ prev_rx = rx; prev_ry = ry;
392
446
  }
393
447
 
394
- const bb = best * NODE_WORDS;
395
- const fct = df[bb + N_FCT];
396
- if (!Number.isNaN(fct)) {
397
- topology.vertex_read_coordinate(_va, 0, du[bb + N_E0V]);
398
- topology.vertex_read_coordinate(_vb, 0, du[bb + N_E1V]);
399
- count = push_point(output, count,
400
- _va[0] + (_vb[0] - _va[0]) * fct,
401
- _va[1] + (_vb[1] - _va[1]) * fct,
402
- _va[2] + (_vb[2] - _va[2]) * fct);
448
+ // start, the crossings in path order (reverse of how they were collected), goal
449
+ let count = 0;
450
+ count = push_point(output, count, _sx, _sy, _sz);
451
+ for (let i = _recon.length - 3; i >= 0; i -= 3) {
452
+ count = push_point(output, count, _recon[i], _recon[i + 1], _recon[i + 2]);
403
453
  }
404
454
  count = push_point(output, count, _gx, _gy, _gz);
405
455
  return count / 3;
406
456
  }
407
457
 
408
- /** Append a 3D point unless it duplicates the previous one (within f32 reconstruction error). */
458
+ /**
459
+ * Append a 3D point, collapsing collinear runs: if the previous point lies (within tolerance) on the
460
+ * segment between the one before it and the new point, it is redundant and gets replaced rather than
461
+ * kept. Flat stretches of a surface-following path thus reduce to their endpoints; genuine bends (turning
462
+ * corners, folds in the surface) survive.
463
+ */
409
464
  function push_point(output, count, x, y, z) {
410
- if (count >= 3 &&
465
+ if (count >= 6) {
466
+ const ax = output[count - 6], ay = output[count - 5], az = output[count - 4];
467
+ const bx = output[count - 3], by = output[count - 2], bz = output[count - 1];
468
+ const abx = bx - ax, aby = by - ay, abz = bz - az;
469
+ const acx = x - ax, acy = y - ay, acz = z - az;
470
+ const cross_x = aby * acz - abz * acy, cross_y = abz * acx - abx * acz, cross_z = abx * acy - aby * acx;
471
+ const ac_len = Math.hypot(acx, acy, acz);
472
+ if (ac_len < DEDUP_EPS || Math.hypot(cross_x, cross_y, cross_z) <= COLLINEAR_EPS * ac_len) {
473
+ output[count - 3] = x; output[count - 2] = y; output[count - 1] = z;
474
+ return count;
475
+ }
476
+ } else if (count >= 3 &&
411
477
  Math.abs(output[count - 3] - x) <= DEDUP_EPS &&
412
478
  Math.abs(output[count - 2] - y) <= DEDUP_EPS &&
413
479
  Math.abs(output[count - 1] - z) <= DEDUP_EPS) {
@@ -459,8 +525,9 @@ function process_far_edge(topology, from_face, Pv, Px, Py, Qv, Qx, Qy, rx, ry, r
459
525
  let m = 0;
460
526
  const tR = line_segment_intersection_fraction_2d(rx, ry, i0x, i0y, Px, Py, Qx, Qy);
461
527
  const tL = line_segment_intersection_fraction_2d(rx, ry, i1x, i1y, Px, Py, Qx, Qy);
462
- if (!Number.isNaN(tR) && tR > PEPS && tR < 1 - PEPS) _split[m++] = tR;
463
- if (!Number.isNaN(tL) && tL > PEPS && tL < 1 - PEPS) _split[m++] = tL;
528
+ // -1 (no crossing) fails `> PEPS`, so the interior-fraction test rejects it without a NaN check
529
+ if (tR > PEPS && tR < 1 - PEPS) _split[m++] = tR;
530
+ if (tL > PEPS && tL < 1 - PEPS) _split[m++] = tL;
464
531
  if (m === 2 && _split[0] > _split[1]) { const t = _split[0]; _split[0] = _split[1]; _split[1] = t; }
465
532
 
466
533
  let prev = 0;
@@ -561,13 +628,13 @@ function emit_node(topology, neighbour, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid,
561
628
 
562
629
  const fcost = g + touch_distance(rx, ry, i0x, i0y, i1x, i1y, ggx, ggy);
563
630
 
564
- let fct = NaN;
631
+ let fct = -1; // -1 = straight shot (no forced bend at an interval endpoint); a real bend is in [0,1]
565
632
  let tx = ggx, ty = ggy;
566
633
  if (triangle2d_compute_area(i0x, i0y, i1x, i1y, rx, ry) * triangle2d_compute_area(i0x, i0y, i1x, i1y, ggx, ggy) > 0) {
567
634
  reflect(ggx, ggy, i0x, i0y, i1x, i1y); tx = _reflect[0]; ty = _reflect[1];
568
635
  }
569
636
  const t = line_segment_intersection_fraction_2d(rx, ry, tx, ty, i0x, i0y, i1x, i1y);
570
- if (Number.isNaN(t) || t < -PEPS || t > 1 + PEPS) {
637
+ if (t < -PEPS || t > 1 + PEPS) { // -1 (no crossing) is caught by `< -PEPS`
571
638
  const d0 = v2_distance(rx, ry, i0x, i0y) + v2_distance(i0x, i0y, ggx, ggy);
572
639
  const d1 = v2_distance(rx, ry, i1x, i1y) + v2_distance(i1x, i1y, ggx, ggy);
573
640
  fct = d0 <= d1 ? i0p : i1p;
@@ -579,6 +646,8 @@ function emit_node(topology, neighbour, Pv, Qv, Px, Py, Qx, Qy, rx, ry, rootVid,
579
646
  du[b + N_TERMINAL] = 1;
580
647
  du[b + N_ROOTVID] = rootVid === -1 ? NULL_ID : rootVid;
581
648
  du[b + N_E0V] = Pv; du[b + N_E1V] = Qv;
649
+ df[b + N_E0X] = Px; df[b + N_E0Y] = Py; df[b + N_E1X] = Qx; df[b + N_E1Y] = Qy;
650
+ df[b + N_RX] = rx; df[b + N_RY] = ry;
582
651
  du[b + N_PARENT] = parent;
583
652
  df[b + N_FCT] = fct;
584
653
  _open.insert(id, fcost);
@@ -1 +1 @@
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
+ {"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AAuDA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAmNxB;+BA3Q8B,mEAAmE"}
@@ -41,8 +41,17 @@ import {
41
41
  import {
42
42
  bt_mesh_fill_small_holes
43
43
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js";
44
+ import {
45
+ bt_mesh_simplify_by_error
46
+ } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js";
44
47
  import { clip_soup_against_overhangs } from "./clip_soup_against_overhangs.js";
45
48
 
49
+ // Fraction of the agent radius used as the navmesh cleanup error budget (see the cleanup pass in
50
+ // navmesh_build_topology). A larger agent cannot resolve fine surface detail and the walkable
51
+ // surface was already inset by agent_radius during erosion, so a small fraction of it is a safe,
52
+ // navigation-imperceptible bound on how far cleanup may move the surface or the walkable outline.
53
+ const NAVMESH_SIMPLIFY_ERROR_FRACTION = 0.05;
54
+
46
55
 
47
56
  /**
48
57
  * Build from given scene geometry
@@ -98,6 +107,11 @@ export function navmesh_build_topology({
98
107
  let raw_triangles = [];
99
108
  let triangle_count = 0;
100
109
 
110
+ // NOTE: the inherited source tessellation is cleaned up at the end of this function by
111
+ // bt_mesh_simplify_by_error (interior coplanar fans + collinear boundary runs collapsed). A
112
+ // further win -- re-triangulating large flat regions to a minimal polygon set -- is left as a
113
+ // possible follow-up.
114
+
101
115
  for (let face_id = 0; face_id < source_face_count; face_id++) {
102
116
 
103
117
  source.face_read_normal(scratch_normal, 0, face_id);
@@ -234,6 +248,17 @@ export function navmesh_build_topology({
234
248
  }
235
249
  }
236
250
 
251
+ // --- Clean up the dense triangulation inherited from the source render geometry ---
252
+ // The navmesh arrives with far more triangles than navigation needs (a flat floor comes in
253
+ // as many coplanar triangles). Collapse only the geometrically redundant ones -- interior
254
+ // near-coplanar fans and collinear runs of boundary vertices -- without moving the surface
255
+ // or the walkable outline (and obstacle holes) by more than a small fraction of the agent
256
+ // radius. With agent_radius 0 the tolerance is 0 and this is a no-op.
257
+ if (agent_radius > 0) {
258
+ bt_mesh_simplify_by_error(mesh, agent_radius * NAVMESH_SIMPLIFY_ERROR_FRACTION);
259
+ bt_mesh_compact(mesh);
260
+ }
261
+
237
262
  // face normals are consumed by navigation queries (string-pulling portal normals), populate them now
238
263
  bt_mesh_compute_face_normals(mesh);
239
264
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Does the segment (ax,ay,az)->(bx,by,bz) pass THROUGH the mesh surface -- cross the interior of any
3
+ * triangle transversally (entering one side, exiting the other) strictly between its endpoints?
4
+ *
5
+ * This is the "penetration" test for a navmesh path piece. A piece that hugs the surface (coplanar with
6
+ * it) or flies over / under it -- a corner-only waypoint chord on a non-planar surface -- does NOT
7
+ * penetrate; a piece that tunnels through a wall or floor does. Endpoints lying on the surface (every
8
+ * path waypoint) are not penetrations.
9
+ *
10
+ * @param {BVH} bvh built by bvh_build_from_bt_mesh over `mesh`
11
+ * @param {BinaryTopology} mesh triangle mesh
12
+ * @param {number} ax
13
+ * @param {number} ay
14
+ * @param {number} az
15
+ * @param {number} bx
16
+ * @param {number} by
17
+ * @param {number} bz
18
+ * @returns {boolean}
19
+ */
20
+ export function bvh_segment_penetrates_mesh(bvh: BVH, mesh: BinaryTopology, ax: number, ay: number, az: number, bx: number, by: number, bz: number): boolean;
21
+ //# sourceMappingURL=bvh_segment_penetrates_mesh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh_segment_penetrates_mesh.d.ts","sourceRoot":"","sources":["../../../../../src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js"],"names":[],"mappings":"AAgEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,gFARW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,OAAO,CAmDnB"}
@@ -0,0 +1,133 @@
1
+ import {
2
+ COLUMN_CHILD_1,
3
+ COLUMN_CHILD_2,
4
+ COLUMN_USER_DATA,
5
+ ELEMENT_WORD_COUNT,
6
+ NULL_NODE
7
+ } from "../../../core/bvh2/bvh3/BVH.js";
8
+ import { NULL_POINTER } from "../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
9
+
10
+ // Plane-side slack and barycentric slack. Mesh vertices are float32, so a segment endpoint that lies ON
11
+ // a triangle sits within ~coord*2^-23 of its plane; PLANE_EPS keeps those (and grazing/coplanar pieces)
12
+ // from registering as a crossing. Only a genuinely transversal piece -- strictly on opposite sides of a
13
+ // triangle's plane and crossing its interior away from its own endpoints -- counts as a penetration.
14
+ const PLANE_EPS = 1e-3;
15
+ const BARY_EPS = 1e-6;
16
+
17
+ const _coords = new Float32Array(3);
18
+ const _stack = new Uint32Array(1024); // DFS node stack; navmesh BVHs are shallow (depth ~ log2(faces))
19
+
20
+ /** Standard slab test: does the segment p + t*d, t in [0,1], intersect the AABB [min,max]? */
21
+ function segment_intersects_aabb(minx, miny, minz, maxx, maxy, maxz, px, py, pz, dx, dy, dz) {
22
+ let tmin = 0, tmax = 1;
23
+
24
+ if (dx > -1e-12 && dx < 1e-12) { if (px < minx || px > maxx) return false; }
25
+ else { let t1 = (minx - px) / dx, t2 = (maxx - px) / dx; if (t1 > t2) { const t = t1; t1 = t2; t2 = t; } if (t1 > tmin) tmin = t1; if (t2 < tmax) tmax = t2; if (tmin > tmax) return false; }
26
+
27
+ if (dy > -1e-12 && dy < 1e-12) { if (py < miny || py > maxy) return false; }
28
+ else { let t1 = (miny - py) / dy, t2 = (maxy - py) / dy; if (t1 > t2) { const t = t1; t1 = t2; t2 = t; } if (t1 > tmin) tmin = t1; if (t2 < tmax) tmax = t2; if (tmin > tmax) return false; }
29
+
30
+ if (dz > -1e-12 && dz < 1e-12) { if (pz < minz || pz > maxz) return false; }
31
+ else { let t1 = (minz - pz) / dz, t2 = (maxz - pz) / dz; if (t1 > t2) { const t = t1; t1 = t2; t2 = t; } if (t1 > tmin) tmin = t1; if (t2 < tmax) tmax = t2; if (tmin > tmax) return false; }
32
+
33
+ return true;
34
+ }
35
+
36
+ /** True if segment (px,py,pz)->(qx,qy,qz) crosses the interior of triangle (a,b,c) transversally. */
37
+ function segment_crosses_triangle(px, py, pz, qx, qy, qz, ax, ay, az, bx, by, bz, cx, cy, cz) {
38
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
39
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
40
+ const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
41
+ const nlen = Math.hypot(nx, ny, nz);
42
+ if (nlen < 1e-12) return false; // degenerate triangle
43
+
44
+ const dP = ((px - ax) * nx + (py - ay) * ny + (pz - az) * nz) / nlen;
45
+ const dQ = ((qx - ax) * nx + (qy - ay) * ny + (qz - az) * nz) / nlen;
46
+
47
+ // must cross the plane strictly (not coplanar, not endpoint-on-plane)
48
+ if (!((dP > PLANE_EPS && dQ < -PLANE_EPS) || (dP < -PLANE_EPS && dQ > PLANE_EPS))) return false;
49
+
50
+ const t = dP / (dP - dQ);
51
+ if (t <= PLANE_EPS || t >= 1 - PLANE_EPS) return false; // crossing at/near an endpoint (on the surface)
52
+
53
+ // crossing point, tested for containment via barycentric coordinates in the triangle plane
54
+ const wx = px + (qx - px) * t - ax, wy = py + (qy - py) * t - ay, wz = pz + (qz - pz) * t - az;
55
+ const d00 = ux * ux + uy * uy + uz * uz, d01 = ux * vx + uy * vy + uz * vz, d11 = vx * vx + vy * vy + vz * vz;
56
+ const d20 = wx * ux + wy * uy + wz * uz, d21 = wx * vx + wy * vy + wz * vz;
57
+ const denom = d00 * d11 - d01 * d01;
58
+ if (denom > -1e-18 && denom < 1e-18) return false;
59
+ const s = (d11 * d20 - d01 * d21) / denom;
60
+ const w = (d00 * d21 - d01 * d20) / denom;
61
+
62
+ return s >= -BARY_EPS && w >= -BARY_EPS && s + w <= 1 + BARY_EPS;
63
+ }
64
+
65
+ /**
66
+ * Does the segment (ax,ay,az)->(bx,by,bz) pass THROUGH the mesh surface -- cross the interior of any
67
+ * triangle transversally (entering one side, exiting the other) strictly between its endpoints?
68
+ *
69
+ * This is the "penetration" test for a navmesh path piece. A piece that hugs the surface (coplanar with
70
+ * it) or flies over / under it -- a corner-only waypoint chord on a non-planar surface -- does NOT
71
+ * penetrate; a piece that tunnels through a wall or floor does. Endpoints lying on the surface (every
72
+ * path waypoint) are not penetrations.
73
+ *
74
+ * @param {BVH} bvh built by bvh_build_from_bt_mesh over `mesh`
75
+ * @param {BinaryTopology} mesh triangle mesh
76
+ * @param {number} ax
77
+ * @param {number} ay
78
+ * @param {number} az
79
+ * @param {number} bx
80
+ * @param {number} by
81
+ * @param {number} bz
82
+ * @returns {boolean}
83
+ */
84
+ export function bvh_segment_penetrates_mesh(bvh, mesh, ax, ay, az, bx, by, bz) {
85
+ const root = bvh.root;
86
+ if (root === NULL_NODE) return false;
87
+
88
+ const float32 = bvh.__data_float32;
89
+ const uint32 = bvh.__data_uint32;
90
+ const dx = bx - ax, dy = by - ay, dz = bz - az;
91
+
92
+ let sp = 0;
93
+ _stack[sp++] = root;
94
+
95
+ while (sp > 0) {
96
+ const node = _stack[--sp];
97
+ const address = node * ELEMENT_WORD_COUNT;
98
+
99
+ if (!segment_intersects_aabb(
100
+ float32[address], float32[address + 1], float32[address + 2],
101
+ float32[address + 3], float32[address + 4], float32[address + 5],
102
+ ax, ay, az, dx, dy, dz
103
+ )) {
104
+ continue;
105
+ }
106
+
107
+ const child_1 = uint32[address + COLUMN_CHILD_1];
108
+ if (child_1 !== NULL_NODE) {
109
+ _stack[sp++] = child_1;
110
+ _stack[sp++] = uint32[address + COLUMN_CHILD_2];
111
+ continue;
112
+ }
113
+
114
+ const face_id = uint32[address + COLUMN_USER_DATA];
115
+ const loop_a = mesh.face_read_loop(face_id);
116
+ if (loop_a === NULL_POINTER) continue;
117
+ const loop_b = mesh.loop_read_next(loop_a);
118
+ const loop_c = mesh.loop_read_next(loop_b);
119
+
120
+ mesh.vertex_read_coordinate(_coords, 0, mesh.loop_read_vertex(loop_a));
121
+ const tax = _coords[0], tay = _coords[1], taz = _coords[2];
122
+ mesh.vertex_read_coordinate(_coords, 0, mesh.loop_read_vertex(loop_b));
123
+ const tbx = _coords[0], tby = _coords[1], tbz = _coords[2];
124
+ mesh.vertex_read_coordinate(_coords, 0, mesh.loop_read_vertex(loop_c));
125
+ const tcx = _coords[0], tcy = _coords[1], tcz = _coords[2];
126
+
127
+ if (segment_crosses_triangle(ax, ay, az, bx, by, bz, tax, tay, taz, tbx, tby, tbz, tcx, tcy, tcz)) {
128
+ return true;
129
+ }
130
+ }
131
+
132
+ return false;
133
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"IndexedFloatMaxHeap.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/refine/IndexedFloatMaxHeap.js"],"names":[],"mappings":"AA6BA;IACI;;;OAGG;IACH,yBAHW,MAAM,0BACN,MAAM,EAsBhB;IAbG,wBAA4C;IAC5C,eAAe;IAEf,2BAA4E;IAC5E,2BAAwD;IACxD,6BAA0D;IAE1D;;;OAGG;IACH,cAFU,WAAW,CAE2B;IAIpD,mBAEC;IAED;;;OAGG;IACH,cAQC;IAED,2BAEC;IAED;;;OAGG;IACH,qBAkBC;IAED;;;;OAIG;IACH,oBAsBC;IAED;;;;OAIG;IACH,kBAYC;IAED,wBAUC;IAED;;;OAGG;IACH,WAHW,MAAM,SACN,MAAM,QAehB;IAED;;OAEG;IACH,WAFa,MAAM,CAuBlB;IAED;;;OAGG;IACH,WAHW,MAAM,aACN,MAAM,QAehB;IAED;;;OAGG;IACH,WAHW,MAAM,GACJ,OAAO,CA+BnB;CACJ"}
@@ -1 +0,0 @@
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"}