@woosh/meep-engine 2.163.7 → 2.163.9

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts +23 -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 +44 -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 +12 -13
  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/NavigationMesh.d.ts +6 -0
  28. package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
  29. package/src/engine/navigation/mesh/NavigationMesh.js +145 -234
  30. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +229 -0
  31. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
  32. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  33. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
  34. package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts +17 -0
  35. package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.d.ts.map +1 -0
  36. package/src/engine/navigation/mesh/bt_mesh_face_find_path_polyanya.js +682 -0
  37. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
  38. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +354 -138
  39. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts +21 -0
  40. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.d.ts.map +1 -0
  41. package/src/engine/navigation/mesh/bvh_segment_penetrates_mesh.js +133 -0
@@ -1 +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":"AA4IA;;;;;;;;GAQG;AACH,kDARW,MAAM,EAAE,kBACR,MAAM,wCAEN,MAAM,gBACN,MAAM,gBAEJ,MAAM,EAAE,CA4GpB"}
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"}
@@ -14,100 +14,140 @@
14
14
  * The subtraction uses the disjoint half-plane decomposition of a convex-polygon difference:
15
15
  * T \ F = U_i ( T ∩ inside(e_0..e_{i-1}) ∩ outside(e_i) )
16
16
  * each term convex, the terms disjoint, the union exactly T minus the convex (dilated) footprint F.
17
- * Clipping is in the plane perpendicular to `up`; world XYZ is carried and interpolated so kept pieces
18
- * stay on the original (possibly tilted) walkable plane. Degenerate pieces/triangles are dropped.
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.
19
27
  */
20
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";
21
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";
22
35
 
23
36
  const POINT_EPS = 1e-9;
24
37
  const AREA_EPS = 1e-9;
38
+ const HEIGHT_EPS = 1e-3;
25
39
  const CORNER_SEGMENT_ANGLE = Math.PI / 6; // ~30 deg per rounded-corner segment
26
40
 
27
- function basis_for_up(up_x, up_y, up_z) {
28
- let rx = 1, ry = 0, rz = 0;
29
- if (Math.abs(up_x) > 0.9) { rx = 0; ry = 1; rz = 0; }
30
- let ux = up_y * rz - up_z * ry;
31
- let uy = up_z * rx - up_x * rz;
32
- let uz = up_x * ry - up_y * rx;
33
- const ul = Math.hypot(ux, uy, uz) || 1;
34
- ux /= ul; uy /= ul; uz /= ul;
35
- const vx = up_y * uz - up_z * uy;
36
- const vy = up_z * ux - up_x * uz;
37
- const vz = up_x * uy - up_y * ux;
38
- return { ux, uy, uz, vx, vy, vz };
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);
39
71
  }
40
72
 
41
- function clip_halfplane(poly, ax, az, dx, dz, keep_left) {
42
- const n = poly.length;
43
- if (n === 0) return poly;
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;
44
93
 
45
- const out = [];
46
- const push = (p) => {
47
- const last = out[out.length - 1];
48
- if (last && Math.abs(last.u - p.u) < POINT_EPS && Math.abs(last.v - p.v) < POINT_EPS) return;
49
- out.push(p);
50
- };
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];
51
96
 
52
- for (let i = 0; i < n; i++) {
53
- const A = poly[i];
54
- const C = poly[(i + 1) % n];
55
- const sa = dx * (A.v - az) - dz * (A.u - ax);
56
- const sc = dx * (C.v - az) - dz * (C.u - ax);
57
- const inA = keep_left ? (sa >= -POINT_EPS) : (sa <= POINT_EPS);
58
- const inC = keep_left ? (sc >= -POINT_EPS) : (sc <= POINT_EPS);
59
- if (inA) push(A);
60
- if (inA !== inC) {
61
- const t = sa / (sa - sc);
62
- push({
63
- x: A.x + t * (C.x - A.x),
64
- y: A.y + t * (C.y - A.y),
65
- z: A.z + t * (C.z - A.z),
66
- u: A.u + t * (C.u - A.u),
67
- v: A.v + t * (C.v - A.v),
68
- });
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++;
69
107
  }
70
- }
71
- if (out.length > 1) {
72
- const f = out[0], l = out[out.length - 1];
73
- if (Math.abs(f.u - l.u) < POINT_EPS && Math.abs(f.v - l.v) < POINT_EPS) out.pop();
74
- }
75
- return out;
76
- }
77
108
 
78
- function signed_area_uv(poly) {
79
- let a = 0;
80
- for (let i = 0; i < poly.length; i++) {
81
- const A = poly[i], C = poly[(i + 1) % poly.length];
82
- a += A.u * C.v - C.u * A.v;
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
+ }
83
115
  }
84
- return a * 0.5;
85
- }
86
116
 
87
- /** Outward (Minkowski-with-disc) offset of a convex CCW polygon by r, with rounded corners. */
88
- function offset_convex_ccw(poly, r) {
89
- const n = poly.length;
90
- if (n < 3 || r <= 0) return poly;
117
+ return out_n;
118
+ }
91
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) {
92
130
  // outward edge normals (right of each CCW edge)
93
- const enx = new Array(n), enz = new Array(n);
131
+ const enx = new Float64Array(n);
132
+ const enz = new Float64Array(n);
94
133
  for (let i = 0; i < n; i++) {
95
- const a = poly[i], b = poly[(i + 1) % n];
96
- let dx = b.u - a.u, dz = b.v - a.v;
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];
97
137
  const len = Math.hypot(dx, dz) || 1;
98
138
  dx /= len; dz /= len;
99
- enx[i] = dz; // (dir.v, -dir.u) = right/outward of a CCW edge
139
+ enx[i] = dz; // (dir.v, -dir.u) = right/outward of a CCW edge
100
140
  enz[i] = -dx;
101
141
  }
102
142
 
103
143
  const out = [];
104
144
  for (let i = 0; i < n; i++) {
105
- const prevEdge = (i - 1 + n) % n; // edge ending at vertex i
106
- const nextEdge = i; // edge starting at vertex i
107
- const v = poly[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];
108
148
 
109
- let a1 = Math.atan2(enz[prevEdge], enx[prevEdge]);
110
- let a2 = Math.atan2(enz[nextEdge], enx[nextEdge]);
149
+ const a1 = Math.atan2(enz[prev_edge], enx[prev_edge]);
150
+ const a2 = Math.atan2(enz[next_edge], enx[next_edge]);
111
151
  let da = a2 - a1;
112
152
  while (da < 0) da += 2 * Math.PI;
113
153
  while (da > 2 * Math.PI) da -= 2 * Math.PI;
@@ -115,27 +155,54 @@ function offset_convex_ccw(poly, r) {
115
155
  const steps = Math.max(1, Math.ceil(da / CORNER_SEGMENT_ANGLE));
116
156
  for (let s = 0; s <= steps; s++) {
117
157
  const a = a1 + da * (s / steps);
118
- out.push({ u: v.u + r * Math.cos(a), v: v.v + r * Math.sin(a) });
158
+ out.push(vu + r * Math.cos(a), vv + r * Math.sin(a));
119
159
  }
120
160
  }
121
- return out;
161
+
162
+ return Float64Array.from(out);
122
163
  }
123
164
 
124
- function subtract_convex(P, F) {
125
- const pieces = [];
126
- let current = P;
127
- for (let i = 0; i < F.length; i++) {
128
- if (current.length < 3) break;
129
- const a = F[i];
130
- const b = F[(i + 1) % F.length];
131
- const ax = a.u, az = a.v;
132
- const dx = b.u - a.u, dz = b.v - a.v;
133
- if (Math.abs(dx) < POINT_EPS && Math.abs(dz) < POINT_EPS) continue; // skip degenerate edge
134
- const outside = clip_halfplane(current, ax, az, dx, dz, false);
135
- if (outside.length >= 3 && Math.abs(signed_area_uv(outside)) > AREA_EPS) pieces.push(outside);
136
- current = clip_halfplane(current, ax, az, dx, dz, true);
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;
137
205
  }
138
- return pieces;
139
206
  }
140
207
 
141
208
  /**
@@ -153,104 +220,253 @@ export function clip_soup_against_overhangs(soup, triangle_count, source, agent_
153
220
  if (ul === 0) return soup.slice(0, triangle_count * 9);
154
221
  upx /= ul; upy /= ul; upz /= ul;
155
222
 
156
- const B = basis_for_up(upx, upy, upz);
157
- const along = (x, y, z) => x * upx + y * upy + z * upz;
158
- const uOf = (x, y, z) => x * B.ux + y * B.uy + z * B.uz;
159
- const vOf = (x, y, z) => x * B.vx + y * B.vy + z * B.vz;
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];
160
228
 
161
- // collect downward-facing (overhead) source triangles in (u,v) with their along-up height
162
- const overhead = [];
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 = [];
163
232
  const fn = new Float32Array(3);
164
233
  const fc = new Float32Array(3);
165
- const faceCount = source.faces.size;
166
- for (let f = 0; f < faceCount; f++) {
234
+ const face_count = source.faces.size;
235
+ for (let f = 0; f < face_count; f++) {
167
236
  if (!source.faces.is_allocated(f)) continue;
168
237
  source.face_read_normal(fn, 0, f);
169
- if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -1e-3) continue;
238
+ if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -HEIGHT_EPS) continue; // not downward-facing
170
239
 
171
- const la = source.face_read_loop(f);
172
- const lb = source.loop_read_next(la);
173
- const lc = source.loop_read_next(lb);
174
- const uv = [];
240
+ let l = source.face_read_loop(f);
175
241
  let hsum = 0;
176
- for (const l of [la, lb, lc]) {
242
+ for (let k = 0; k < 3; k++) {
177
243
  source.vertex_read_coordinate(fc, 0, source.loop_read_vertex(l));
178
- uv.push({ u: uOf(fc[0], fc[1], fc[2]), v: vOf(fc[0], fc[1], fc[2]) });
179
- hsum += along(fc[0], fc[1], fc[2]);
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);
180
248
  }
181
- overhead.push({ uv, h: hsum / 3 });
249
+ overhead_h.push(hsum / 3);
182
250
  }
183
251
 
184
- if (overhead.length === 0) return soup.slice(0, triangle_count * 9);
252
+ const overhead_count = overhead_h.length;
253
+ if (overhead_count === 0) return soup.slice(0, triangle_count * 9);
185
254
 
186
255
  // Group overhead triangles that share a (coincident) vertex into one obstacle. Each obstacle's
187
256
  // footprint is the convex hull of its vertices, dilated by the agent radius. Treating the obstacle
188
257
  // as one polygon (rather than per-triangle) avoids self-touching/pinched union boundaries.
189
- const QK = 1e4;
190
- const vkey = (p) => Math.round(p.u * QK) + "," + Math.round(p.v * QK);
191
- const parent = [];
258
+ const parent = new Int32Array(overhead_count);
259
+ for (let i = 0; i < overhead_count; i++) parent[i] = i;
192
260
  const find = (a) => { while (parent[a] !== a) { parent[a] = parent[parent[a]]; a = parent[a]; } return a; };
193
- for (let i = 0; i < overhead.length; i++) parent[i] = i;
194
- const keyToTri = new Map();
195
- for (let i = 0; i < overhead.length; i++) {
196
- for (const p of overhead[i].uv) {
197
- const k = vkey(p);
198
- if (keyToTri.has(k)) parent[find(i)] = find(keyToTri.get(k)); else keyToTri.set(k, i);
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);
199
270
  }
200
271
  }
272
+
273
+ /** @type {Map<number, {pts: number[], hsum: number, n: number}>} */
201
274
  const groups = new Map();
202
- for (let i = 0; i < overhead.length; i++) {
203
- const r = find(i);
204
- let g = groups.get(r);
205
- if (!g) { g = { pts: [], hsum: 0, n: 0 }; groups.set(r, g); }
206
- for (const p of overhead[i].uv) g.pts.push(p.u, p.v);
207
- g.hsum += overhead[i].h; g.n++;
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++;
208
282
  }
209
283
 
210
- const footprints = [];
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 = [];
211
288
  for (const g of groups.values()) {
212
289
  const hull = convex_hull_monotone_2d(g.pts, g.pts.length / 2);
213
290
  if (hull.length < 3) continue;
214
- let poly = hull.map((idx) => ({ u: g.pts[idx * 2], v: g.pts[idx * 2 + 1] }));
215
- if (signed_area_uv(poly) < 0) poly = poly.slice().reverse();
216
- if (agent_radius > 0) poly = offset_convex_ccw(poly, agent_radius);
217
- footprints.push({ poly, h: g.hsum / g.n });
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);
218
310
  }
219
311
 
220
- if (footprints.length === 0) return soup.slice(0, triangle_count * 9);
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
+ }
221
342
 
222
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
+
223
355
  for (let t = 0; t < triangle_count; t++) {
224
356
  const o = t * 9;
225
- const tri = [];
357
+
226
358
  let th = 0;
227
359
  for (let k = 0; k < 3; k++) {
228
360
  const x = soup[o + k * 3], y = soup[o + k * 3 + 1], z = soup[o + k * 3 + 2];
229
- tri.push({ x, y, z, u: uOf(x, y, z), v: vOf(x, y, z) });
230
- th += along(x, y, z);
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);
231
365
  }
232
366
  th /= 3;
233
367
 
234
- let pieces = [tri];
235
- for (const fp of footprints) {
236
- const dh = fp.h - th;
237
- if (dh <= 1e-3 || dh > agent_height + 1e-3) continue;
238
- const next = [];
239
- for (const p of pieces) {
240
- for (const s of subtract_convex(p, fp.poly)) next.push(s);
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);
241
413
  }
242
- pieces = next;
414
+
415
+ const swap = pieces; pieces = next_pieces; next_pieces = swap;
243
416
  if (pieces.length === 0) break;
244
417
  }
245
418
 
246
- for (const p of pieces) {
247
- for (let i = 1; i + 1 < p.length; i++) {
248
- const a0 = p[0], a1 = p[i], a2 = p[i + 1];
249
- const area2 = Math.abs((a1.u - a0.u) * (a2.v - a0.v) - (a2.u - a0.u) * (a1.v - a0.v));
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));
250
431
  if (area2 < 2 * AREA_EPS) continue;
251
- out.push(a0.x, a0.y, a0.z, a1.x, a1.y, a1.z, a2.x, a2.y, a2.z);
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);
252
436
  }
253
437
  }
254
438
  }
439
+
255
440
  return out;
256
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
+ }
@@ -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"}