@woosh/meep-engine 2.163.5 → 2.163.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.163.5",
9
+ "version": "2.163.7",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Fill boundary holes that are too thin for the agent to occupy.
3
+ *
4
+ * Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
5
+ * cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
6
+ * than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
7
+ * the boundary, the face count, and the loop count. This fills any inner loop whose mean width
8
+ * (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
9
+ * largest-area loop (the outer boundary) is never filled.
10
+ *
11
+ * @param {BinaryTopology} mesh
12
+ * @param {number} agent_radius
13
+ * @returns {number} number of holes filled
14
+ */
15
+ export function bt_mesh_fill_small_holes(mesh: BinaryTopology, agent_radius: number): number;
16
+ //# sourceMappingURL=bt_mesh_fill_small_holes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bt_mesh_fill_small_holes.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js"],"names":[],"mappings":"AA+FA;;;;;;;;;;;;;GAaG;AACH,6EAHW,MAAM,GACJ,MAAM,CA2ClB"}
@@ -0,0 +1,151 @@
1
+ import { bt_mesh_walk_boundary_loops } from "../query/bt_mesh_walk_boundary_loops.js";
2
+ import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
3
+ import { bt_radial_loop_add } from "./bt_radial_loop_add.js";
4
+
5
+ const _c = [0, 0, 0];
6
+ const _p = [0, 0, 0];
7
+ const _q = [0, 0, 0];
8
+
9
+ /**
10
+ * Order a boundary cycle's edges into a vertex/edge walk starting at `start_vertex`.
11
+ * (Same traversal as bt_mesh_close_boundary_holes.)
12
+ */
13
+ function order_cycle_from(mesh, cycle, start_vertex, out_vertices, out_edges) {
14
+ const n = cycle.length;
15
+ const v_to_edges = new Map();
16
+ for (let i = 0; i < n; i++) {
17
+ const e = cycle[i];
18
+ const v1 = mesh.edge_read_vertex1(e);
19
+ const v2 = mesh.edge_read_vertex2(e);
20
+ let bucket = v_to_edges.get(v1);
21
+ if (bucket === undefined) { bucket = []; v_to_edges.set(v1, bucket); }
22
+ bucket.push(e);
23
+ bucket = v_to_edges.get(v2);
24
+ if (bucket === undefined) { bucket = []; v_to_edges.set(v2, bucket); }
25
+ bucket.push(e);
26
+ }
27
+ let v_curr = start_vertex;
28
+ let e_prev = -1;
29
+ for (let i = 0; i < n; i++) {
30
+ out_vertices[i] = v_curr;
31
+ const edges_at_v = v_to_edges.get(v_curr);
32
+ const e_next = (edges_at_v[0] === e_prev) ? edges_at_v[1] : edges_at_v[0];
33
+ out_edges[i] = e_next;
34
+ const e_v1 = mesh.edge_read_vertex1(e_next);
35
+ const e_v2 = mesh.edge_read_vertex2(e_next);
36
+ v_curr = (e_v1 === v_curr) ? e_v2 : e_v1;
37
+ e_prev = e_next;
38
+ }
39
+ }
40
+
41
+ /** Perimeter and (Newell) 3D area of an ordered loop of vertices. */
42
+ function loop_perimeter_and_area(mesh, verts) {
43
+ const n = verts.length;
44
+ let perimeter = 0;
45
+ let nx = 0, ny = 0, nz = 0;
46
+ for (let i = 0; i < n; i++) {
47
+ mesh.vertex_read_coordinate(_p, 0, verts[i]);
48
+ mesh.vertex_read_coordinate(_q, 0, verts[(i + 1) % n]);
49
+ perimeter += Math.hypot(_q[0] - _p[0], _q[1] - _p[1], _q[2] - _p[2]);
50
+ // Newell's method for polygon area normal
51
+ nx += (_p[1] - _q[1]) * (_p[2] + _q[2]);
52
+ ny += (_p[2] - _q[2]) * (_p[0] + _q[0]);
53
+ nz += (_p[0] - _q[0]) * (_p[1] + _q[1]);
54
+ }
55
+ const area = 0.5 * Math.hypot(nx, ny, nz);
56
+ return { perimeter, area };
57
+ }
58
+
59
+ /** Fan-fill a single boundary loop (n-gon face + centroid poke), as close_boundary_holes does. */
60
+ function fill_loop(mesh, cycle) {
61
+ const n = cycle.length;
62
+ const seed_edge = cycle[0];
63
+ const seed_loop = mesh.edge_read_loop(seed_edge);
64
+ const existing_loop_vertex = mesh.loop_read_vertex(seed_loop);
65
+ const seed_v1 = mesh.edge_read_vertex1(seed_edge);
66
+ const seed_v2 = mesh.edge_read_vertex2(seed_edge);
67
+ const start_vertex = (existing_loop_vertex === seed_v1) ? seed_v2 : seed_v1;
68
+
69
+ const ordered_vertices = new Array(n);
70
+ const ordered_edges = new Array(n);
71
+ order_cycle_from(mesh, cycle, start_vertex, ordered_vertices, ordered_edges);
72
+
73
+ let cx = 0, cy = 0, cz = 0;
74
+ for (let i = 0; i < n; i++) {
75
+ mesh.vertex_read_coordinate(_c, 0, ordered_vertices[i]);
76
+ cx += _c[0]; cy += _c[1]; cz += _c[2];
77
+ }
78
+ cx /= n; cy /= n; cz /= n;
79
+
80
+ const new_face = mesh.faces.allocate();
81
+ const loops = new Array(n);
82
+ for (let i = 0; i < n; i++) {
83
+ loops[i] = mesh.loop_create();
84
+ mesh.loop_write_vertex(loops[i], ordered_vertices[i]);
85
+ mesh.loop_write_face(loops[i], new_face);
86
+ bt_radial_loop_add(mesh, loops[i], ordered_edges[i]);
87
+ }
88
+ for (let i = 0; i < n; i++) {
89
+ mesh.loop_write_next(loops[i], loops[(i + 1) % n]);
90
+ mesh.loop_write_prev(loops[i], loops[(i - 1 + n) % n]);
91
+ }
92
+ mesh.face_write_loop(new_face, loops[0]);
93
+ bt_mesh_face_poke(mesh, new_face, cx, cy, cz);
94
+ }
95
+
96
+ /**
97
+ * Fill boundary holes that are too thin for the agent to occupy.
98
+ *
99
+ * Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
100
+ * cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
101
+ * than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
102
+ * the boundary, the face count, and the loop count. This fills any inner loop whose mean width
103
+ * (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
104
+ * largest-area loop (the outer boundary) is never filled.
105
+ *
106
+ * @param {BinaryTopology} mesh
107
+ * @param {number} agent_radius
108
+ * @returns {number} number of holes filled
109
+ */
110
+ export function bt_mesh_fill_small_holes(mesh, agent_radius) {
111
+ if (agent_radius <= 0) return 0;
112
+
113
+ let cycles;
114
+ try {
115
+ cycles = bt_mesh_walk_boundary_loops(mesh);
116
+ } catch (e) {
117
+ // a malformed (pinched) boundary can arise at float32-precision limits (e.g. geometry far from
118
+ // the origin); skip hole-filling rather than aborting the whole build
119
+ return 0;
120
+ }
121
+ if (cycles.length <= 1) return 0;
122
+
123
+ // measure every loop; the largest-area one is the outer boundary and is never filled
124
+ const measures = cycles.map(c => {
125
+ const n = c.length;
126
+ const verts = new Array(n);
127
+ const edges = new Array(n);
128
+ const v1 = mesh.edge_read_vertex1(c[0]);
129
+ order_cycle_from(mesh, c, v1, verts, edges);
130
+ return loop_perimeter_and_area(mesh, verts);
131
+ });
132
+
133
+ let outer = 0;
134
+ for (let i = 1; i < measures.length; i++) {
135
+ if (measures[i].area > measures[outer].area) outer = i;
136
+ }
137
+
138
+ let filled = 0;
139
+ for (let i = 0; i < cycles.length; i++) {
140
+ if (i === outer) continue;
141
+ const { perimeter, area } = measures[i];
142
+ if (perimeter <= 0) continue;
143
+ // mean width of the hole = 2*area/perimeter; fill when the agent disc cannot fit
144
+ if (2 * area / perimeter < agent_radius) {
145
+ fill_loop(mesh, cycles[i]);
146
+ filled++;
147
+ }
148
+ }
149
+
150
+ return filled;
151
+ }
@@ -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":"AA4IA;;;;;;;;GAQG;AACH,kDARW,MAAM,EAAE,kBACR,MAAM,wCAEN,MAAM,gBACN,MAAM,gBAEJ,MAAM,EAAE,CA4GpB"}
@@ -0,0 +1,256 @@
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
+ * 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.
19
+ */
20
+
21
+ import { convex_hull_monotone_2d } from "../../../../core/geom/2d/convex-hull/convex_hull_monotone_2d.js";
22
+
23
+ const POINT_EPS = 1e-9;
24
+ const AREA_EPS = 1e-9;
25
+ const CORNER_SEGMENT_ANGLE = Math.PI / 6; // ~30 deg per rounded-corner segment
26
+
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 };
39
+ }
40
+
41
+ function clip_halfplane(poly, ax, az, dx, dz, keep_left) {
42
+ const n = poly.length;
43
+ if (n === 0) return poly;
44
+
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
+ };
51
+
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
+ });
69
+ }
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
+
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;
83
+ }
84
+ return a * 0.5;
85
+ }
86
+
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;
91
+
92
+ // outward edge normals (right of each CCW edge)
93
+ const enx = new Array(n), enz = new Array(n);
94
+ 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;
97
+ const len = Math.hypot(dx, dz) || 1;
98
+ dx /= len; dz /= len;
99
+ enx[i] = dz; // (dir.v, -dir.u) = right/outward of a CCW edge
100
+ enz[i] = -dx;
101
+ }
102
+
103
+ const out = [];
104
+ 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];
108
+
109
+ let a1 = Math.atan2(enz[prevEdge], enx[prevEdge]);
110
+ let a2 = Math.atan2(enz[nextEdge], enx[nextEdge]);
111
+ let da = a2 - a1;
112
+ while (da < 0) da += 2 * Math.PI;
113
+ while (da > 2 * Math.PI) da -= 2 * Math.PI;
114
+
115
+ const steps = Math.max(1, Math.ceil(da / CORNER_SEGMENT_ANGLE));
116
+ for (let s = 0; s <= steps; s++) {
117
+ const a = a1 + da * (s / steps);
118
+ out.push({ u: v.u + r * Math.cos(a), v: v.v + r * Math.sin(a) });
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+
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);
137
+ }
138
+ return pieces;
139
+ }
140
+
141
+ /**
142
+ * @param {number[]} soup flat XYZ triangle soup (9 floats per triangle)
143
+ * @param {number} triangle_count
144
+ * @param {BinaryTopology} source source mesh (face normals must be populated)
145
+ * @param {number} agent_height
146
+ * @param {number} agent_radius dilation applied to each footprint
147
+ * @param {Vector3} up
148
+ * @returns {number[]} new soup with (dilated) obstacle footprints removed
149
+ */
150
+ export function clip_soup_against_overhangs(soup, triangle_count, source, agent_height, agent_radius, up) {
151
+ let upx = up.x, upy = up.y, upz = up.z;
152
+ const ul = Math.hypot(upx, upy, upz);
153
+ if (ul === 0) return soup.slice(0, triangle_count * 9);
154
+ upx /= ul; upy /= ul; upz /= ul;
155
+
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;
160
+
161
+ // collect downward-facing (overhead) source triangles in (u,v) with their along-up height
162
+ const overhead = [];
163
+ const fn = new Float32Array(3);
164
+ const fc = new Float32Array(3);
165
+ const faceCount = source.faces.size;
166
+ for (let f = 0; f < faceCount; f++) {
167
+ if (!source.faces.is_allocated(f)) continue;
168
+ source.face_read_normal(fn, 0, f);
169
+ if (fn[0] * upx + fn[1] * upy + fn[2] * upz >= -1e-3) continue;
170
+
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 = [];
175
+ let hsum = 0;
176
+ for (const l of [la, lb, lc]) {
177
+ 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]);
180
+ }
181
+ overhead.push({ uv, h: hsum / 3 });
182
+ }
183
+
184
+ if (overhead.length === 0) return soup.slice(0, triangle_count * 9);
185
+
186
+ // Group overhead triangles that share a (coincident) vertex into one obstacle. Each obstacle's
187
+ // footprint is the convex hull of its vertices, dilated by the agent radius. Treating the obstacle
188
+ // 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 = [];
192
+ 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);
199
+ }
200
+ }
201
+ 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++;
208
+ }
209
+
210
+ const footprints = [];
211
+ for (const g of groups.values()) {
212
+ const hull = convex_hull_monotone_2d(g.pts, g.pts.length / 2);
213
+ 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 });
218
+ }
219
+
220
+ if (footprints.length === 0) return soup.slice(0, triangle_count * 9);
221
+
222
+ const out = [];
223
+ for (let t = 0; t < triangle_count; t++) {
224
+ const o = t * 9;
225
+ const tri = [];
226
+ let th = 0;
227
+ for (let k = 0; k < 3; k++) {
228
+ 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);
231
+ }
232
+ th /= 3;
233
+
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);
241
+ }
242
+ pieces = next;
243
+ if (pieces.length === 0) break;
244
+ }
245
+
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));
250
+ 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);
252
+ }
253
+ }
254
+ }
255
+ return out;
256
+ }
@@ -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,QAyLxB;+BAvO8B,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,12 +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";
41
+ import {
42
+ bt_mesh_fill_small_holes
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";
45
45
 
46
46
 
47
47
  /**
@@ -82,11 +82,6 @@ export function navmesh_build_topology({
82
82
  // outward winding; that remains an input contract we cannot recover from here.
83
83
  bt_mesh_compute_face_normals(source);
84
84
 
85
- const source_bvh = new BVH();
86
-
87
- // prepare a BVH, we'll need it for height queries later on
88
- bvh_build_from_bt_mesh(source_bvh, source);
89
-
90
85
  const scratch_normal = new Float32Array(3);
91
86
 
92
87
  // unpack topology into triangle soup
@@ -100,7 +95,7 @@ export function navmesh_build_topology({
100
95
  *
101
96
  * @type {number[]}
102
97
  */
103
- const raw_triangles = [];
98
+ let raw_triangles = [];
104
99
  let triangle_count = 0;
105
100
 
106
101
  for (let face_id = 0; face_id < source_face_count; face_id++) {
@@ -144,14 +139,6 @@ export function navmesh_build_topology({
144
139
  }
145
140
 
146
141
  {
147
- const bvh = new BVH();
148
-
149
- // first dump all triangles into a BVH for speed of access
150
- bvh_build_from_unindexed_triangles(bvh, raw_triangles, triangle_count);
151
-
152
- // find possible triangle edge connections, that is - where edges of 2 triangles touch, but don't match exactly.
153
- // the touching edges will need to be cut and affected triangles split accordingly
154
-
155
142
  const mesh = new BinaryTopology();
156
143
 
157
144
  // raw_triangles may have stale tail data from filtering steps above (e.g. degenerate/steep);
@@ -177,27 +164,10 @@ export function navmesh_build_topology({
177
164
  bt_mesh_fuse_duplicate_edges(mesh);
178
165
  }
179
166
 
180
- // --- Carve obstacle footprints first (no radius dilation here) ---
181
- // Cull the floor directly under any overhang, opening a hole at each obstacle footprint. The
182
- // agent-radius dilation is handled by the erosion below, which treats these holes as boundaries
183
- // and insets them (and the outer edge) by exactly agent_radius - giving both a uniform inset and
184
- // a smooth, consistent outline around obstacles from a single, robust pass.
185
- if (agent_height > 0) {
186
- bt_mesh_carve_height_clearance({
187
- mesh,
188
- source,
189
- source_bvh,
190
- agent_height,
191
- agent_radius: 0,
192
- up,
193
- });
194
-
195
- bt_mesh_compact(mesh);
196
- }
197
-
198
- // --- Then erode every boundary (outer edge + obstacle holes) by the agent radius ---
199
- // The erosion uses an exact Euclidean distance field with cut-band refinement, so it stays
200
- // 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.
201
171
  {
202
172
  const islands = bt_mesh_compute_face_islands(mesh);
203
173
 
@@ -211,6 +181,36 @@ export function navmesh_build_topology({
211
181
  bt_mesh_compact(mesh);
212
182
  }
213
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
+
214
214
  // bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
215
215
  // Opt-in: with both step params 0 (the default) the topology is left untouched.
216
216
  if (agent_max_step_height > 0 || agent_max_step_distance > 0) {
@@ -224,6 +224,16 @@ export function navmesh_build_topology({
224
224
  bt_mesh_compact(mesh);
225
225
  }
226
226
 
227
+ // Fill hair-thin sliver holes left by erosion/clearance around obstacle offsets. They are
228
+ // narrower than the agent (it could never fit inside them), so they are numerical noise rather
229
+ // than real obstacles - removing them restores wrongly-culled walkable area and keeps the
230
+ // boundary clean (one outer loop + one loop per genuine obstacle).
231
+ if (agent_radius > 0) {
232
+ if (bt_mesh_fill_small_holes(mesh, agent_radius) > 0) {
233
+ bt_mesh_compact(mesh);
234
+ }
235
+ }
236
+
227
237
  // face normals are consumed by navigation queries (string-pulling portal normals), populate them now
228
238
  bt_mesh_compute_face_normals(mesh);
229
239
 
@@ -1,28 +0,0 @@
1
- /**
2
- * Carve holes into a (welded) walkable topology wherever an agent of the given height+radius would not
3
- * fit under overhead geometry. INTENDED TO RUN AFTER the agent-radius boundary erosion: the boundary is
4
- * already handled, so this only removes the obstacle footprints (dilated by the agent radius).
5
- *
6
- * All subdivision is conformal ({@link bt_edge_split} re-triangulates every face around a split edge),
7
- * and culling whole faces ({@link bt_face_kill}) never cracks neighbours - so this cannot disconnect a
8
- * passable region. Refinement is tight: faces straddling the (dilated) obstacle outline are refined to
9
- * `resolution`; a still-large all-clear face whose column has overhead is refined down to a coarse
10
- * guard so a small obstacle cannot hide unsampled; everything else stays coarse.
11
- *
12
- * @param {object} params
13
- * @param {BinaryTopology} params.mesh walkable topology to carve (modified in place)
14
- * @param {BinaryTopology} params.source original source mesh (walkable + overhead geometry)
15
- * @param {BVH} params.source_bvh BVH over `source`
16
- * @param {number} params.agent_height
17
- * @param {number} params.agent_radius
18
- * @param {Vector3} params.up world up direction
19
- */
20
- export function bt_mesh_carve_height_clearance({ mesh, source, source_bvh, agent_height, agent_radius, up, }: {
21
- mesh: BinaryTopology;
22
- source: BinaryTopology;
23
- source_bvh: BVH;
24
- agent_height: number;
25
- agent_radius: number;
26
- up: Vector3;
27
- }): void;
28
- //# sourceMappingURL=bt_mesh_carve_height_clearance.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bt_mesh_carve_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js"],"names":[],"mappings":"AA2NA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAPkC,IAAI;IACJ,MAAM;IACjB,UAAU;IACP,YAAY,EAA3B,MAAM;IACS,YAAY,EAA3B,MAAM;IACU,EAAE;SAyH5B"}
@@ -1,358 +0,0 @@
1
- import { NULL_NODE } from "../../../../core/bvh2/bvh3/BVH.js";
2
- import {
3
- bvh_query_leaves_ray_segment
4
- } from "../../../../core/bvh2/bvh3/query/bvh_query_leaves_ray_segment.js";
5
- import {
6
- bvh_query_user_data_overlaps_aabb
7
- } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
8
- import { NULL_POINTER } from "../../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
9
- import {
10
- bt_mesh_cleanup_faceless_references
11
- } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_cleanup_faceless_references.js";
12
- import { bt_edge_split } from "../../../../core/geom/3d/topology/struct/binary/io/edge/bt_edge_split.js";
13
- import { bt_face_kill } from "../../../../core/geom/3d/topology/struct/binary/io/face/bt_face_kill.js";
14
- import {
15
- computeTriangleRayIntersectionBarycentric
16
- } from "../../../../core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js";
17
-
18
- /** Lift a ray origin off the surface so the upward clearance ray does not re-hit the floor. */
19
- const SURFACE_EPSILON = 1e-4;
20
-
21
- /** Safety cap on the face count during refinement. */
22
- const MAX_FACES = 200000;
23
-
24
- // reused scratch
25
- const ray_leaf_buffer = [];
26
- const intersection_result = new Float32Array(6);
27
- const tri_a = new Float32Array(3);
28
- const tri_b = new Float32Array(3);
29
- const tri_c = new Float32Array(3);
30
-
31
- const va = new Float32Array(3);
32
- const vb = new Float32Array(3);
33
- const vc = new Float32Array(3);
34
-
35
- const overhead_hits = [];
36
- const query_aabb = new Float32Array(6);
37
- const scratch_normal = new Float32Array(3);
38
-
39
- /** True if nothing in `source` sits directly above the point within `agent_height` (exact ray test). */
40
- function point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height) {
41
- const origin_x = px + up_x * SURFACE_EPSILON;
42
- const origin_y = py + up_y * SURFACE_EPSILON;
43
- const origin_z = pz + up_z * SURFACE_EPSILON;
44
-
45
- const leaf_count = bvh_query_leaves_ray_segment(
46
- source_bvh, source_bvh.root,
47
- ray_leaf_buffer, 0,
48
- origin_x, origin_y, origin_z,
49
- up_x, up_y, up_z,
50
- 0, agent_height
51
- );
52
-
53
- for (let i = 0; i < leaf_count; i++) {
54
- const node = ray_leaf_buffer[i];
55
- const face_id = source_bvh.node_get_user_data(node);
56
-
57
- const loop_a = source.face_read_loop(face_id);
58
- if (loop_a === NULL_POINTER) continue;
59
-
60
- // Only DOWNWARD-facing geometry (a ceiling / overhang underside) can obstruct head clearance.
61
- // Up-facing walkable floors above (a higher tier, a step the agent climbs) must NOT block.
62
- source.face_read_normal(scratch_normal, 0, face_id);
63
- if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
64
- continue;
65
- }
66
-
67
- const loop_b = source.loop_read_next(loop_a);
68
- const loop_c = source.loop_read_next(loop_b);
69
-
70
- source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(loop_a));
71
- source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(loop_b));
72
- source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(loop_c));
73
-
74
- const hit = computeTriangleRayIntersectionBarycentric(
75
- intersection_result,
76
- origin_x, origin_y, origin_z,
77
- up_x, up_y, up_z,
78
- tri_a[0], tri_a[1], tri_a[2],
79
- tri_b[0], tri_b[1], tri_b[2],
80
- tri_c[0], tri_c[1], tri_c[2]
81
- );
82
- if (!hit) continue;
83
-
84
- const t = intersection_result[0];
85
- if (t > 0 && t <= agent_height) return false;
86
- }
87
-
88
- return true;
89
- }
90
-
91
- /**
92
- * True if any source geometry sits above the point within `agent_height`, anywhere inside a horizontal
93
- * disc of radius `r` (approximated by an axis-aligned box). Used to dilate the obstacle footprint by
94
- * the agent radius - the agent's body cannot occupy a spot whose `r`-neighbourhood is overhung.
95
- */
96
- function overhead_within_radius(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height, r) {
97
- const p_along_up = px * up_x + py * up_y + pz * up_z;
98
-
99
- // expand on the axes perpendicular to up by r, then sweep up by agent_height
100
- const ex = r * (1 - Math.abs(up_x));
101
- const ey = r * (1 - Math.abs(up_y));
102
- const ez = r * (1 - Math.abs(up_z));
103
-
104
- let min_x = px - ex, min_y = py - ey, min_z = pz - ez;
105
- let max_x = px + ex, max_y = py + ey, max_z = pz + ez;
106
-
107
- const dx = up_x * agent_height, dy = up_y * agent_height, dz = up_z * agent_height;
108
- if (dx > 0) max_x += dx; else min_x += dx;
109
- if (dy > 0) max_y += dy; else min_y += dy;
110
- if (dz > 0) max_z += dz; else min_z += dz;
111
-
112
- query_aabb[0] = min_x; query_aabb[1] = min_y; query_aabb[2] = min_z;
113
- query_aabb[3] = max_x; query_aabb[4] = max_y; query_aabb[5] = max_z;
114
-
115
- const count = bvh_query_user_data_overlaps_aabb(overhead_hits, 0, source_bvh, query_aabb);
116
-
117
- for (let i = 0; i < count; i++) {
118
- const fid = overhead_hits[i];
119
- const la = source.face_read_loop(fid);
120
- if (la === NULL_POINTER) continue;
121
-
122
- // only downward-facing geometry counts as an overhang (see point_has_clearance)
123
- source.face_read_normal(scratch_normal, 0, fid);
124
- if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
125
- continue;
126
- }
127
-
128
- const lb = source.loop_read_next(la);
129
- const lc = source.loop_read_next(lb);
130
-
131
- source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(la));
132
- source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(lb));
133
- source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(lc));
134
-
135
- const cand_min_along_up = Math.min(
136
- tri_a[0] * up_x + tri_a[1] * up_y + tri_a[2] * up_z,
137
- tri_b[0] * up_x + tri_b[1] * up_y + tri_b[2] * up_z,
138
- tri_c[0] * up_x + tri_c[1] * up_y + tri_c[2] * up_z
139
- );
140
-
141
- if (cand_min_along_up > p_along_up + 1e-3 && cand_min_along_up <= p_along_up + agent_height + 1e-3) {
142
- return true;
143
- }
144
- }
145
-
146
- return false;
147
- }
148
-
149
- /**
150
- * Does the column over this face's footprint (inflated horizontally by `pad`) contain any DOWNWARD-
151
- * facing geometry above it within `agent_height`? Footprint-wide (not just the centroid), so an
152
- * obstacle sitting anywhere under a large face is detected and the face is refined toward it.
153
- */
154
- function footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, pad) {
155
- const face_max_along_up = Math.max(
156
- va[0] * up_x + va[1] * up_y + va[2] * up_z,
157
- vb[0] * up_x + vb[1] * up_y + vb[2] * up_z,
158
- vc[0] * up_x + vc[1] * up_y + vc[2] * up_z
159
- );
160
-
161
- let min_x = Math.min(va[0], vb[0], vc[0]) - pad;
162
- let min_y = Math.min(va[1], vb[1], vc[1]) - pad;
163
- let min_z = Math.min(va[2], vb[2], vc[2]) - pad;
164
- let max_x = Math.max(va[0], vb[0], vc[0]) + pad;
165
- let max_y = Math.max(va[1], vb[1], vc[1]) + pad;
166
- let max_z = Math.max(va[2], vb[2], vc[2]) + pad;
167
-
168
- const dx = up_x * agent_height, dy = up_y * agent_height, dz = up_z * agent_height;
169
- if (dx > 0) max_x += dx; else min_x += dx;
170
- if (dy > 0) max_y += dy; else min_y += dy;
171
- if (dz > 0) max_z += dz; else min_z += dz;
172
-
173
- query_aabb[0] = min_x; query_aabb[1] = min_y; query_aabb[2] = min_z;
174
- query_aabb[3] = max_x; query_aabb[4] = max_y; query_aabb[5] = max_z;
175
-
176
- const count = bvh_query_user_data_overlaps_aabb(overhead_hits, 0, source_bvh, query_aabb);
177
-
178
- for (let i = 0; i < count; i++) {
179
- const fid = overhead_hits[i];
180
- const la = source.face_read_loop(fid);
181
- if (la === NULL_POINTER) continue;
182
-
183
- source.face_read_normal(scratch_normal, 0, fid);
184
- if (scratch_normal[0] * up_x + scratch_normal[1] * up_y + scratch_normal[2] * up_z >= -1e-3) {
185
- continue;
186
- }
187
-
188
- const lb = source.loop_read_next(la);
189
- const lc = source.loop_read_next(lb);
190
- source.vertex_read_coordinate(tri_a, 0, source.loop_read_vertex(la));
191
- source.vertex_read_coordinate(tri_b, 0, source.loop_read_vertex(lb));
192
- source.vertex_read_coordinate(tri_c, 0, source.loop_read_vertex(lc));
193
-
194
- const cand_min_along_up = Math.min(
195
- tri_a[0] * up_x + tri_a[1] * up_y + tri_a[2] * up_z,
196
- tri_b[0] * up_x + tri_b[1] * up_y + tri_b[2] * up_z,
197
- tri_c[0] * up_x + tri_c[1] * up_y + tri_c[2] * up_z
198
- );
199
-
200
- if (cand_min_along_up > face_max_along_up + 1e-3) {
201
- return true;
202
- }
203
- }
204
-
205
- return false;
206
- }
207
-
208
- function read_face_triangle(mesh, face_id) {
209
- const la = mesh.face_read_loop(face_id);
210
- const lb = mesh.loop_read_next(la);
211
- const lc = mesh.loop_read_next(lb);
212
-
213
- mesh.vertex_read_coordinate(va, 0, mesh.loop_read_vertex(la));
214
- mesh.vertex_read_coordinate(vb, 0, mesh.loop_read_vertex(lb));
215
- mesh.vertex_read_coordinate(vc, 0, mesh.loop_read_vertex(lc));
216
-
217
- return { la, lb, lc };
218
- }
219
-
220
- /**
221
- * Carve holes into a (welded) walkable topology wherever an agent of the given height+radius would not
222
- * fit under overhead geometry. INTENDED TO RUN AFTER the agent-radius boundary erosion: the boundary is
223
- * already handled, so this only removes the obstacle footprints (dilated by the agent radius).
224
- *
225
- * All subdivision is conformal ({@link bt_edge_split} re-triangulates every face around a split edge),
226
- * and culling whole faces ({@link bt_face_kill}) never cracks neighbours - so this cannot disconnect a
227
- * passable region. Refinement is tight: faces straddling the (dilated) obstacle outline are refined to
228
- * `resolution`; a still-large all-clear face whose column has overhead is refined down to a coarse
229
- * guard so a small obstacle cannot hide unsampled; everything else stays coarse.
230
- *
231
- * @param {object} params
232
- * @param {BinaryTopology} params.mesh walkable topology to carve (modified in place)
233
- * @param {BinaryTopology} params.source original source mesh (walkable + overhead geometry)
234
- * @param {BVH} params.source_bvh BVH over `source`
235
- * @param {number} params.agent_height
236
- * @param {number} params.agent_radius
237
- * @param {Vector3} params.up world up direction
238
- */
239
- export function bt_mesh_carve_height_clearance({
240
- mesh,
241
- source,
242
- source_bvh,
243
- agent_height,
244
- agent_radius,
245
- up,
246
- }) {
247
-
248
- if (agent_height <= 0 || source_bvh.root === NULL_NODE) return;
249
-
250
- let up_x = up.x, up_y = up.y, up_z = up.z;
251
- const up_len = Math.sqrt(up_x * up_x + up_y * up_y + up_z * up_z);
252
- if (up_len === 0) return;
253
- up_x /= up_len; up_y /= up_len; up_z /= up_len;
254
-
255
- const r = Math.max(agent_radius, 0);
256
- const resolution = Math.max(agent_radius > 0 ? agent_radius : agent_height / 4, 0.3);
257
- const res_sq = resolution * resolution;
258
-
259
- // The agent fits at p iff nothing overhangs directly above it AND nothing overhangs within its body
260
- // radius. (For r == 0 this is just the exact overhead ray.)
261
- function agent_fits(px, py, pz) {
262
- if (!point_has_clearance(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height)) {
263
- return false;
264
- }
265
- if (r > 0 && overhead_within_radius(source_bvh, source, px, py, pz, up_x, up_y, up_z, agent_height, r)) {
266
- return false;
267
- }
268
- return true;
269
- }
270
-
271
- // ---- Phase 1: tight, conformal refinement around the (dilated) obstacle outline ----
272
- let changed = true;
273
- let guard = 0;
274
-
275
- while (changed && guard < 64) {
276
- changed = false;
277
- guard++;
278
-
279
- const face_count = mesh.faces.size;
280
-
281
- for (let f = 0; f < face_count; f++) {
282
- if (!mesh.faces.is_allocated(f)) continue;
283
-
284
- read_face_triangle(mesh, f);
285
-
286
- const ab = (va[0] - vb[0]) ** 2 + (va[1] - vb[1]) ** 2 + (va[2] - vb[2]) ** 2;
287
- const bc = (vb[0] - vc[0]) ** 2 + (vb[1] - vc[1]) ** 2 + (vb[2] - vc[2]) ** 2;
288
- const ca = (vc[0] - va[0]) ** 2 + (vc[1] - va[1]) ** 2 + (vc[2] - va[2]) ** 2;
289
- const longest_sq = Math.max(ab, bc, ca);
290
-
291
- if (longest_sq <= res_sq) continue;
292
-
293
- const mab_x = (va[0] + vb[0]) * 0.5, mab_y = (va[1] + vb[1]) * 0.5, mab_z = (va[2] + vb[2]) * 0.5;
294
- const mbc_x = (vb[0] + vc[0]) * 0.5, mbc_y = (vb[1] + vc[1]) * 0.5, mbc_z = (vb[2] + vc[2]) * 0.5;
295
- const mca_x = (vc[0] + va[0]) * 0.5, mca_y = (vc[1] + va[1]) * 0.5, mca_z = (vc[2] + va[2]) * 0.5;
296
- const cx = (va[0] + vb[0] + vc[0]) / 3, cy = (va[1] + vb[1] + vc[1]) / 3, cz = (va[2] + vb[2] + vc[2]) / 3;
297
-
298
- const samples = [
299
- va[0], va[1], va[2], vb[0], vb[1], vb[2], vc[0], vc[1], vc[2],
300
- mab_x, mab_y, mab_z, mbc_x, mbc_y, mbc_z, mca_x, mca_y, mca_z,
301
- cx, cy, cz,
302
- ];
303
- let n_clear = 0, n_block = 0;
304
- for (let s = 0; s < samples.length; s += 3) {
305
- if (agent_fits(samples[s], samples[s + 1], samples[s + 2])) n_clear++; else n_block++;
306
- }
307
-
308
- let refine = false;
309
- if (n_block > 0 && n_clear > 0) {
310
- refine = true; // (dilated) outline crosses this face -> hug it at `resolution`
311
- } else if (n_block === 0) {
312
- // all clear by sampling, but a small obstacle can hide entirely between the samples of a
313
- // coarse face. If any downward-facing overhang sits under the face footprint, keep
314
- // refining down to the contour resolution so even sub-sample obstacles get caught.
315
- if (footprint_has_overhead(source_bvh, source, up_x, up_y, up_z, agent_height, r)) {
316
- refine = true;
317
- }
318
- }
319
- // n_clear === 0 (fully blocked): leave it for the cull phase
320
-
321
- if (!refine) continue;
322
-
323
- const { la, lb, lc } = read_face_triangle(mesh, f);
324
- let split_loop;
325
- if (longest_sq === ab) split_loop = la;
326
- else if (longest_sq === bc) split_loop = lb;
327
- else split_loop = lc;
328
-
329
- bt_edge_split(mesh, mesh.loop_read_edge(split_loop), 0.5);
330
- changed = true;
331
- }
332
-
333
- if (mesh.faces.size > MAX_FACES) break;
334
- }
335
-
336
- // ---- Phase 2: cull blocked faces (whole-face, conformal) ----
337
- const faces_to_kill = [];
338
- const face_count = mesh.faces.size;
339
-
340
- for (let f = 0; f < face_count; f++) {
341
- if (!mesh.faces.is_allocated(f)) continue;
342
-
343
- read_face_triangle(mesh, f);
344
- const cx = (va[0] + vb[0] + vc[0]) / 3;
345
- const cy = (va[1] + vb[1] + vc[1]) / 3;
346
- const cz = (va[2] + vb[2] + vc[2]) / 3;
347
-
348
- if (!agent_fits(cx, cy, cz)) faces_to_kill.push(f);
349
- }
350
-
351
- for (let i = 0; i < faces_to_kill.length; i++) {
352
- bt_face_kill(mesh, faces_to_kill[i]);
353
- }
354
-
355
- if (faces_to_kill.length > 0) {
356
- bt_mesh_cleanup_faceless_references(mesh);
357
- }
358
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Knock holes in the walkable triangle soup wherever an agent of the given height would not fit under
3
- * overhead geometry.
4
- *
5
- * Approach: subdivide each candidate triangle to a resolution tied to the agent's footprint, cast an
6
- * upward clearance ray from each sub-triangle centroid against the source geometry, and keep only the
7
- * sub-triangles that have clearance. Fully-clear triangles are emitted unchanged (no subdivision), so
8
- * the common case adds no triangles; fully-blocked triangles are dropped; partially-blocked triangles
9
- * are replaced by their clear sub-triangles. Accuracy is bounded by the sampling resolution.
10
- *
11
- * The `triangles` array is rewritten in place with the surviving triangles.
12
- *
13
- * @param {BVH} bvh source-geometry BVH (leaves carry source face IDs)
14
- * @param {BinaryTopology} source source mesh, used to resolve face IDs to triangle vertices
15
- * @param {number} agent_height
16
- * @param {number} agent_radius
17
- * @param {number} triangle_count number of triangles currently in `triangles`
18
- * @param {number[]} triangles flat XYZ soup, 9 floats per triangle
19
- * @param {Vector3} up world up direction
20
- * @returns {number} new triangle count
21
- */
22
- export function enforce_agent_height_clearance({ bvh, source, agent_height, agent_radius, triangle_count, triangles, up, }: BVH): number;
23
- //# sourceMappingURL=enforce_agent_height_clearance.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"enforce_agent_height_clearance.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/enforce_agent_height_clearance.js"],"names":[],"mappings":"AAmHA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,kIAFa,MAAM,CA4IlB"}
@@ -1,319 +0,0 @@
1
- import { assert } from "../../../../core/assert.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";
7
- import { compute_triangle_area_3d } from "../../../../core/geom/3d/triangle/compute_triangle_area_3d.js";
8
- import {
9
- computeTriangleRayIntersectionBarycentric
10
- } from "../../../../core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js";
11
- import { clamp } from "../../../../core/math/clamp.js";
12
-
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);
33
-
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.
38
- *
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}
49
- */
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(
108
- output,
109
- ax, ay, az,
110
- bx, by, bz,
111
- cx, cy, cz
112
- ) {
113
- output.push(ax, ay, az, bx, by, bz, cx, cy, cz);
114
- }
115
-
116
- /**
117
- * Knock holes in the walkable triangle soup wherever an agent of the given height would not fit under
118
- * overhead geometry.
119
- *
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
130
- * @param {number} agent_height
131
- * @param {number} agent_radius
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
135
- * @returns {number} new triangle count
136
- */
137
- export function enforce_agent_height_clearance({
138
- bvh,
139
- source,
140
- agent_height,
141
- agent_radius,
142
- triangle_count,
143
- triangles,
144
- up,
145
- }) {
146
-
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;
168
-
169
- /**
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.
172
- * @type {number}
173
- */
174
- const sampling_resolution = Math.min(
175
- agent_height / 4,
176
- Math.max(agent_radius / 2, 0.001)
177
- );
178
-
179
- assert.isFinite(sampling_resolution, 'sampling_resolution');
180
- assert.greaterThan(sampling_resolution, 0, 'sampling_resolution');
181
-
182
- /**
183
- * Surviving triangles. Built separately because the count can grow (subdivision), then copied back.
184
- * @type {number[]}
185
- */
186
- const output = [];
187
-
188
- for (let i = 0; i < triangle_count; i++) {
189
- const address = i * 9;
190
-
191
- const ax = triangles[address];
192
- const ay = triangles[address + 1];
193
- const az = triangles[address + 2];
194
-
195
- const bx = triangles[address + 3];
196
- const by = triangles[address + 4];
197
- const bz = triangles[address + 5];
198
-
199
- const cx = triangles[address + 6];
200
- const cy = triangles[address + 7];
201
- const cz = triangles[address + 8];
202
-
203
- const area = compute_triangle_area_3d(ax, ay, az, bx, by, bz, cx, cy, cz);
204
-
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);
208
- continue;
209
- }
210
-
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
- }
254
- }
255
-
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
- }
264
- }
265
- // else fully blocked - drop the triangle entirely
266
- }
267
-
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;
273
-
274
- return (output.length / 9) | 0;
275
- }
276
-
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
- );
313
-
314
- if (clear) {
315
- out.push(p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z);
316
- }
317
-
318
- return clear;
319
- }