@woosh/meep-engine 2.163.1 → 2.163.3

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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.d.ts +12 -0
  3. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.d.ts.map +1 -0
  4. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.js +18 -0
  5. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.d.ts +14 -0
  6. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.d.ts.map +1 -0
  7. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.js +24 -0
  8. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts.map +1 -1
  9. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +368 -290
  10. package/src/core/geom/vec3/v3_uniform_sample_cone.d.ts +11 -0
  11. package/src/core/geom/vec3/v3_uniform_sample_cone.d.ts.map +1 -0
  12. package/src/core/geom/vec3/v3_uniform_sample_cone.js +21 -0
  13. package/src/core/math/physics/brdf/cone_cosine_from_roughness.d.ts +13 -0
  14. package/src/core/math/physics/brdf/cone_cosine_from_roughness.d.ts.map +1 -0
  15. package/src/core/math/physics/brdf/cone_cosine_from_roughness.js +28 -0
  16. package/src/core/math/physics/brdf/reflection_sample_weight.d.ts +18 -0
  17. package/src/core/math/physics/brdf/reflection_sample_weight.d.ts.map +1 -0
  18. package/src/core/math/physics/brdf/reflection_sample_weight.js +48 -0
  19. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  20. package/src/engine/graphics/GraphicsEngine.js +52 -0
  21. package/src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.d.ts.map +1 -1
  22. package/src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.js +306 -226
  23. package/src/engine/graphics/ecs/path/tube/build/make_cap.d.ts.map +1 -1
  24. package/src/engine/graphics/ecs/path/tube/build/make_cap.js +26 -17
  25. package/src/engine/graphics/sh3/sky/hosek/make_environment_sky_hosek.d.ts +26 -0
  26. package/src/engine/graphics/sh3/sky/hosek/make_environment_sky_hosek.d.ts.map +1 -0
  27. package/src/engine/graphics/sh3/sky/hosek/make_environment_sky_hosek.js +49 -0
  28. package/src/engine/graphics/sh3/sky/hosek/render_hosek_sky_to_equirectangular.d.ts +26 -0
  29. package/src/engine/graphics/sh3/sky/hosek/render_hosek_sky_to_equirectangular.d.ts.map +1 -0
  30. package/src/engine/graphics/sh3/sky/hosek/render_hosek_sky_to_equirectangular.js +70 -0
  31. package/src/engine/graphics/sh3/sky/hosek/setup_environment_sky_from_ecd.d.ts +24 -0
  32. package/src/engine/graphics/sh3/sky/hosek/setup_environment_sky_from_ecd.d.ts.map +1 -0
  33. package/src/engine/graphics/sh3/sky/hosek/setup_environment_sky_from_ecd.js +51 -0
  34. package/src/engine/graphics/texture/EnvironmentTextureProjection.d.ts +9 -0
  35. package/src/engine/graphics/texture/EnvironmentTextureProjection.d.ts.map +1 -0
  36. package/src/engine/graphics/texture/EnvironmentTextureProjection.js +15 -0
  37. package/src/engine/graphics/texture/reflection/convolve_equirectangular_reflection.d.ts +44 -0
  38. package/src/engine/graphics/texture/reflection/convolve_equirectangular_reflection.d.ts.map +1 -0
  39. package/src/engine/graphics/texture/reflection/convolve_equirectangular_reflection.js +189 -0
  40. package/src/engine/graphics/texture/reflection/equirectangular_reflection_roughness.d.ts +25 -0
  41. package/src/engine/graphics/texture/reflection/equirectangular_reflection_roughness.d.ts.map +1 -0
  42. package/src/engine/graphics/texture/reflection/equirectangular_reflection_roughness.js +51 -0
  43. package/src/engine/graphics/texture/sampler/sampler2d_sample_equirectangular_direction.d.ts +15 -0
  44. package/src/engine/graphics/texture/sampler/sampler2d_sample_equirectangular_direction.d.ts.map +1 -0
  45. package/src/engine/graphics/texture/sampler/sampler2d_sample_equirectangular_direction.js +63 -0
  46. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts +27 -0
  47. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.d.ts.map +1 -0
  48. package/src/engine/navigation/mesh/build/bt_mesh_carve_height_clearance.js +323 -0
  49. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  50. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +223 -226
  51. package/src/engine/.fuse_hidden0000001500000001 +0 -581
@@ -1,290 +1,368 @@
1
- import { assert } from "../../../../../../assert.js";
2
- import { clamp01 } from "../../../../../../math/clamp01.js";
3
- import { inverseLerp } from "../../../../../../math/inverseLerp.js";
4
- import {
5
- line3_compute_segment_point_distance_eikonal
6
- } from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
7
- import { NULL_POINTER } from "../BinaryTopology.js";
8
- import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
9
- import { bt_mesh_build_boundary_distance_field } from "../query/bt_mesh_build_boundary_distance_field.js";
10
- import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
11
- import { bt_edge_split } from "./edge/bt_edge_split.js";
12
- import { bt_face_kill } from "./face/bt_face_kill.js";
13
- import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
14
-
15
- /**
16
- * Shrinks an island of faces by a given distance.
17
- * Conceptually, this moves the outline of the island inwards.
18
- * Assumes the input mesh is triangulated, see {@link bt_mesh_triangulate}.
19
- *
20
- * It is assumed that the island does not share any edges/vertices with any other part of the mesh. If this assumption is violated - expect undefined behavior.
21
- * An island is not restricted to all faces being co-planar, it may curve.
22
- * The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
23
- *
24
- * @param {BinaryTopology} mesh
25
- * @param {number[]} faces
26
- * @param {number} erode_distance
27
- */
28
- export function bt_mesh_face_island_erode(
29
- mesh,
30
- faces,
31
- erode_distance
32
- ) {
33
-
34
- if (faces.length === 0 || erode_distance <= 0) {
35
- // no work to do
36
- return;
37
- }
38
-
39
- /**
40
- *
41
- * @type {Set<number>}
42
- */
43
- const island_vertices = new Set();
44
-
45
- /**
46
- *
47
- * @type {Set<number>}
48
- */
49
- const island_edges = new Set();
50
-
51
- // 1. Map all vertices and edges of the island
52
- for (let i = 0; i < faces.length; i++) {
53
- const f = faces[i];
54
- const l_first = mesh.face_read_loop(f);
55
- if (l_first === NULL_POINTER) continue;
56
-
57
- let l_curr = l_first;
58
- do {
59
- island_vertices.add(mesh.loop_read_vertex(l_curr));
60
- island_edges.add(mesh.loop_read_edge(l_curr));
61
- l_curr = mesh.loop_read_next(l_curr);
62
- } while (l_curr !== l_first);
63
- }
64
-
65
- // 2. Identify boundary vertices (edges with only one loop)
66
- const boundary_vertices = new Set();
67
- const boundary_edges_set = new Set();
68
-
69
- for (const e of island_edges) {
70
- const l = mesh.edge_read_loop(e);
71
- if (mesh.loop_read_radial_next(l) === l) {
72
- boundary_vertices.add(mesh.edge_read_vertex1(e));
73
- boundary_vertices.add(mesh.edge_read_vertex2(e));
74
-
75
- boundary_edges_set.add(e);
76
- }
77
- }
78
-
79
- if (boundary_vertices.size === 0) {
80
- // no boundary vertices found
81
- // should never happen if the mesh is valid
82
- return;
83
- }
84
-
85
- // 3. Compute Geodesic Distance Field
86
- /**
87
- * Vertex -> Distance from boundary
88
- * @type {Map<number, number>}
89
- */
90
- const vertex_boundary_distances = new Map();
91
- bt_mesh_build_boundary_distance_field(vertex_boundary_distances, mesh, island_vertices, boundary_vertices);
92
- // ---------------------------------------------------------
93
- // 3.5. PEAK RESCUE PASS (The Fix)
94
- // Handle cases where a face is "submerged" by vertex values
95
- // but geometrically contains a safe zone (e.g. single triangle).
96
- // ---------------------------------------------------------
97
-
98
- // We snapshot the faces list because we might modify topology (poke)
99
- const faces_to_check = [...faces];
100
- const centroid = [];
101
- const scratch_v1 = [];
102
- const scratch_v2 = [];
103
-
104
- for (const f of faces_to_check) {
105
- // Collect face vertices and check if they are all "dead"
106
- let l_curr = mesh.face_read_loop(f);
107
- const l_end = l_curr;
108
- let all_dead = true;
109
-
110
- do {
111
- const v = mesh.loop_read_vertex(l_curr);
112
- if ((vertex_boundary_distances.get(v) || 0) >= erode_distance) {
113
- all_dead = false;
114
- }
115
- l_curr = mesh.loop_read_next(l_curr);
116
- } while (l_curr !== l_end);
117
-
118
- if (all_dead) {
119
- // This face is slated for total deletion.
120
- // Check if we should rescue it by adding a peak vertex.
121
-
122
- // 1. Calculate Centroid (incenter only for triangle, falls back to centroid for other N-gons)
123
- bt_face_get_incenter(centroid, 0, mesh, f);
124
-
125
- // 2. Calculate Centroid Distance to Boundary
126
- // (For a single island face, min dist to its own boundary edges)
127
- let min_dist = Infinity;
128
- // Iterate edges of this face
129
- l_curr = mesh.face_read_loop(f);
130
- do {
131
- const e = mesh.loop_read_edge(l_curr);
132
-
133
- const v1 = mesh.edge_read_vertex1(e);
134
- const v2 = mesh.edge_read_vertex2(e);
135
-
136
- if (island_vertices.has(v1) && island_vertices.has(v2)) {
137
-
138
- mesh.vertex_read_coordinate(scratch_v1, 0, v1);
139
- mesh.vertex_read_coordinate(scratch_v2, 0, v2);
140
-
141
- const d1 = vertex_boundary_distances.get(v1);
142
- const d2 = vertex_boundary_distances.get(v2);
143
-
144
- if (d1 < erode_distance || d2 < erode_distance) {
145
- // only consider edges that could cross the erosion threshold
146
-
147
- const d = line3_compute_segment_point_distance_eikonal(
148
- d1, d2,
149
- scratch_v1[0], scratch_v1[1], scratch_v1[2],
150
- scratch_v2[0], scratch_v2[1], scratch_v2[2],
151
- centroid[0], centroid[1], centroid[2]
152
- );
153
-
154
- if (d < min_dist) {
155
- min_dist = d;
156
- }
157
-
158
- }
159
-
160
- }
161
-
162
- l_curr = mesh.loop_read_next(l_curr);
163
- } while (l_curr !== l_end);
164
-
165
- // If min_dist is Infinity, it means this face has NO boundary edges
166
- // (it's internal). It should have inherited values from neighbors.
167
- // We ignore it, or assume linear interpolation was correct.
168
- // We only rescue faces touching the boundary that collapsed.
169
-
170
- if (min_dist !== Infinity && min_dist > erode_distance) {
171
- // RESCUE: The center is safe!
172
- // Poke the face to create a vertex at the centroid.
173
- const new_vert = bt_mesh_face_poke(mesh, f, centroid[0], centroid[1], centroid[2]);
174
-
175
- // Update Data
176
- island_vertices.add(new_vert);
177
- vertex_boundary_distances.set(new_vert, min_dist);
178
-
179
- // Read the new edges connected to the center and add to processing set
180
- let e_first = mesh.vertex_read_edge(new_vert);
181
-
182
- let e_scan = e_first;
183
- do {
184
- island_edges.add(e_scan);
185
-
186
- const v1 = mesh.edge_read_vertex1(e_scan);
187
- const is_v1 = v1 === new_vert;
188
-
189
- // DEBUG checks
190
- const v2 = mesh.edge_read_vertex2(e_scan);
191
- if (is_v1) {
192
- assert.equal(v1, new_vert, 'edge vertex does not match new vertex');
193
- assert.notEqual(v2, new_vert, 'edge vertex does not match new vertex');
194
- } else {
195
- assert.equal(v2, new_vert, 'edge vertex does not match new vertex');
196
- assert.notEqual(v1, new_vert, 'edge vertex does not match new vertex');
197
- }
198
-
199
- e_scan = is_v1 ? mesh.edge_read_v1_disk_next(e_scan) : mesh.edge_read_v2_disk_next(e_scan);
200
- } while (e_scan !== e_first);
201
- }
202
- }
203
- }
204
- // ---------------------------------------------------------
205
-
206
- // 4. Trace the Contour & Split Edges
207
-
208
- /**
209
- *
210
- * @type {{e: number, t: number}[]}
211
- */
212
- const edges_to_split = [];
213
-
214
- for (const e of island_edges) {
215
-
216
- assert.equal(mesh.edges.is_allocated(e), true, 'edge is not allocated');
217
-
218
- const v1 = mesh.edge_read_vertex1(e);
219
- const v2 = mesh.edge_read_vertex2(e);
220
-
221
- const d1 = vertex_boundary_distances.get(v1);
222
- const d2 = vertex_boundary_distances.get(v2);
223
-
224
- // If the edge strictly crosses the erosion threshold
225
- const crosses_erosion_boundary = (d1 < erode_distance && d2 > erode_distance) || (d2 < erode_distance && d1 > erode_distance);
226
-
227
- if (crosses_erosion_boundary) {
228
- // Calculate exact interpolation parameter
229
- const t = clamp01(inverseLerp(d1, d2, erode_distance));
230
- edges_to_split.push({ e, t });
231
- }
232
- }
233
-
234
- // Execute splits
235
- for (const { e, t } of edges_to_split) {
236
- // splits the edge as well as adjacent faces.
237
- bt_edge_split(mesh, e, t);
238
- }
239
-
240
- // Cull Dead Geometry
241
- // Any vertex that had a distance < erode_distance is dead.
242
- // Any face connected to a dead vertex must be destroyed.
243
- /**
244
- *
245
- * @type {Set<number>}
246
- */
247
- const faces_to_kill = new Set();
248
-
249
- for (const [vertex, distance] of vertex_boundary_distances.entries()) {
250
- const EPSILON = 1e-17;
251
-
252
- if (distance >= erode_distance - EPSILON) {
253
- // too far, survives
254
- continue;
255
- }
256
-
257
- const e_first = mesh.vertex_read_edge(vertex);
258
-
259
- if (e_first === NULL_POINTER) {
260
- continue;
261
- }
262
-
263
- let e_curr = e_first;
264
- do {
265
- // Traverse radial loops to get all connected faces
266
- const l_first = mesh.edge_read_loop(e_curr);
267
- if (l_first !== NULL_POINTER) {
268
- let l_curr = l_first;
269
- do {
270
- faces_to_kill.add(mesh.loop_read_face(l_curr));
271
- l_curr = mesh.loop_read_radial_next(l_curr);
272
- } while (l_curr !== l_first);
273
- }
274
-
275
- // Move around disk cycle
276
- const v1 = mesh.edge_read_vertex1(e_curr);
277
- e_curr = (v1 === vertex) ? mesh.edge_read_v1_disk_next(e_curr) : mesh.edge_read_v2_disk_next(e_curr);
278
- } while (e_curr !== e_first && e_curr !== NULL_POINTER);
279
- }
280
-
281
- // Eradicate the dead faces
282
- for (const f of faces_to_kill) {
283
- bt_face_kill(mesh, f);
284
- }
285
-
286
- // 6. Cleanup floating vertices and edges
287
- if (faces_to_kill.size > 0) {
288
- bt_mesh_cleanup_faceless_references(mesh);
289
- }
290
- }
1
+ import { assert } from "../../../../../../assert.js";
2
+ import { clamp01 } from "../../../../../../math/clamp01.js";
3
+ import { inverseLerp } from "../../../../../../math/inverseLerp.js";
4
+ import {
5
+ line3_compute_segment_point_distance_eikonal
6
+ } from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
7
+ import { NULL_POINTER } from "../BinaryTopology.js";
8
+ import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
9
+ import { bt_mesh_build_boundary_distance_field } from "../query/bt_mesh_build_boundary_distance_field.js";
10
+ import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
11
+ import { bt_edge_split } from "./edge/bt_edge_split.js";
12
+ import { bt_face_kill } from "./face/bt_face_kill.js";
13
+ import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
14
+ import { bt_face_get_neighbour_faces } from "../query/bt_face_get_neighbour_faces.js";
15
+
16
+ /**
17
+ * Shrinks an island of faces by a given distance.
18
+ * Conceptually, this moves the outline of the island inwards.
19
+ * Assumes the input mesh is triangulated, see {@link bt_mesh_triangulate}.
20
+ *
21
+ * It is assumed that the island does not share any edges/vertices with any other part of the mesh. If this assumption is violated - expect undefined behavior.
22
+ * An island is not restricted to all faces being co-planar, it may curve.
23
+ * The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
24
+ *
25
+ * @param {BinaryTopology} mesh
26
+ * @param {number[]} faces
27
+ * @param {number} erode_distance
28
+ */
29
+ export function bt_mesh_face_island_erode(
30
+ mesh,
31
+ faces,
32
+ erode_distance
33
+ ) {
34
+
35
+ if (faces.length === 0 || erode_distance <= 0) {
36
+ // no work to do
37
+ return;
38
+ }
39
+
40
+ // 1. + 2. Map the island's vertices/edges and detect boundary (single radial loop) edges/vertices.
41
+ // Extracted into a helper so it can be recomputed after the connectivity pre-pass mutates the mesh.
42
+ function collect_island_sets(island_faces) {
43
+ const iv = new Set();
44
+ const ie = new Set();
45
+
46
+ for (let i = 0; i < island_faces.length; i++) {
47
+ const f = island_faces[i];
48
+ const l_first = mesh.face_read_loop(f);
49
+ if (l_first === NULL_POINTER) continue;
50
+
51
+ let l_curr = l_first;
52
+ do {
53
+ iv.add(mesh.loop_read_vertex(l_curr));
54
+ ie.add(mesh.loop_read_edge(l_curr));
55
+ l_curr = mesh.loop_read_next(l_curr);
56
+ } while (l_curr !== l_first);
57
+ }
58
+
59
+ const bv = new Set();
60
+ const be = new Set();
61
+
62
+ for (const e of ie) {
63
+ const l = mesh.edge_read_loop(e);
64
+ if (mesh.loop_read_radial_next(l) === l) {
65
+ bv.add(mesh.edge_read_vertex1(e));
66
+ bv.add(mesh.edge_read_vertex2(e));
67
+ be.add(e);
68
+ }
69
+ }
70
+
71
+ return { island_vertices: iv, island_edges: ie, boundary_vertices: bv, boundary_edges_set: be };
72
+ }
73
+
74
+ // Re-derive the (now larger) face list of this island after edge splits by flood-filling
75
+ // edge-adjacency from the original faces. bt_edge_split reuses the original face ids for one half
76
+ // of each split, so they remain valid, allocated seeds.
77
+ function recompute_island_faces(seed_faces) {
78
+ const seen = new Set();
79
+ const stack = [];
80
+ const result = [];
81
+ const nbr = [];
82
+
83
+ for (let i = 0; i < seed_faces.length; i++) {
84
+ const f = seed_faces[i];
85
+ if (mesh.faces.is_allocated(f)) {
86
+ stack.push(f);
87
+ }
88
+ }
89
+
90
+ while (stack.length > 0) {
91
+ const f = stack.pop();
92
+ if (seen.has(f)) continue;
93
+ seen.add(f);
94
+ result.push(f);
95
+
96
+ const n = bt_face_get_neighbour_faces(nbr, 0, mesh, f);
97
+ for (let i = 0; i < n; i++) {
98
+ const g = nbr[i];
99
+ if (!seen.has(g)) {
100
+ stack.push(g);
101
+ }
102
+ }
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ let island_faces = faces;
109
+
110
+ let { island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces);
111
+
112
+ if (boundary_vertices.size === 0) {
113
+ // no boundary vertices found
114
+ // should never happen if the mesh is valid
115
+ return;
116
+ }
117
+
118
+ // ---------------------------------------------------------
119
+ // 2.5. CONNECTIVITY PRE-PASS (fix for erosion fragmentation)
120
+ // ---------------------------------------------------------
121
+ // The boundary distance field is sampled only at vertices and interpolated linearly along edges.
122
+ // An *interior* edge whose BOTH endpoints lie on the boundary therefore reads as ~0 along its
123
+ // entire length, even where its middle sits deep inside the island (the shared diagonal of a coarse
124
+ // quad, or the seam between two large coplanar tiles). Erosion then culls a band straddling that
125
+ // edge and severs one connected island into disconnected pieces.
126
+ //
127
+ // Fix: insert an interior vertex at the midpoint of every such edge. The new vertex is genuinely
128
+ // interior, so the eikonal field gives it the correct (large) clearance and the contour keeps the
129
+ // deep interior alive, preserving connectivity. A single pass suffices: splitting a
130
+ // boundary-boundary interior edge yields only boundary-interior edges, creating no new offenders.
131
+ {
132
+ const split_targets = [];
133
+
134
+ for (const e of island_edges) {
135
+ if (boundary_edges_set.has(e)) {
136
+ // a true boundary edge defines the outline - never split it
137
+ continue;
138
+ }
139
+
140
+ const v1 = mesh.edge_read_vertex1(e);
141
+ const v2 = mesh.edge_read_vertex2(e);
142
+
143
+ if (boundary_vertices.has(v1) && boundary_vertices.has(v2)) {
144
+ split_targets.push(e);
145
+ }
146
+ }
147
+
148
+ if (split_targets.length > 0) {
149
+ for (let i = 0; i < split_targets.length; i++) {
150
+ bt_edge_split(mesh, split_targets[i], 0.5);
151
+ }
152
+
153
+ island_faces = recompute_island_faces(faces);
154
+
155
+ ({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
156
+
157
+ if (boundary_vertices.size === 0) {
158
+ return;
159
+ }
160
+ }
161
+ }
162
+
163
+ // 3. Compute Geodesic Distance Field
164
+ /**
165
+ * Vertex -> Distance from boundary
166
+ * @type {Map<number, number>}
167
+ */
168
+ const vertex_boundary_distances = new Map();
169
+ bt_mesh_build_boundary_distance_field(vertex_boundary_distances, mesh, island_vertices, boundary_vertices);
170
+ // ---------------------------------------------------------
171
+ // 3.5. PEAK RESCUE PASS (The Fix)
172
+ // Handle cases where a face is "submerged" by vertex values
173
+ // but geometrically contains a safe zone (e.g. single triangle).
174
+ // ---------------------------------------------------------
175
+
176
+ // We snapshot the faces list because we might modify topology (poke)
177
+ const faces_to_check = [...island_faces];
178
+ const centroid = [];
179
+ const scratch_v1 = [];
180
+ const scratch_v2 = [];
181
+
182
+ for (const f of faces_to_check) {
183
+ // Collect face vertices and check if they are all "dead"
184
+ let l_curr = mesh.face_read_loop(f);
185
+ const l_end = l_curr;
186
+ let all_dead = true;
187
+
188
+ do {
189
+ const v = mesh.loop_read_vertex(l_curr);
190
+ if ((vertex_boundary_distances.get(v) || 0) >= erode_distance) {
191
+ all_dead = false;
192
+ }
193
+ l_curr = mesh.loop_read_next(l_curr);
194
+ } while (l_curr !== l_end);
195
+
196
+ if (all_dead) {
197
+ // This face is slated for total deletion.
198
+ // Check if we should rescue it by adding a peak vertex.
199
+
200
+ // 1. Calculate Centroid (incenter only for triangle, falls back to centroid for other N-gons)
201
+ bt_face_get_incenter(centroid, 0, mesh, f);
202
+
203
+ // 2. Calculate Centroid Distance to Boundary
204
+ // (For a single island face, min dist to its own boundary edges)
205
+ let min_dist = Infinity;
206
+ // Iterate edges of this face
207
+ l_curr = mesh.face_read_loop(f);
208
+ do {
209
+ const e = mesh.loop_read_edge(l_curr);
210
+
211
+ const v1 = mesh.edge_read_vertex1(e);
212
+ const v2 = mesh.edge_read_vertex2(e);
213
+
214
+ if (island_vertices.has(v1) && island_vertices.has(v2)) {
215
+
216
+ mesh.vertex_read_coordinate(scratch_v1, 0, v1);
217
+ mesh.vertex_read_coordinate(scratch_v2, 0, v2);
218
+
219
+ const d1 = vertex_boundary_distances.get(v1);
220
+ const d2 = vertex_boundary_distances.get(v2);
221
+
222
+ if (d1 < erode_distance || d2 < erode_distance) {
223
+ // only consider edges that could cross the erosion threshold
224
+
225
+ const d = line3_compute_segment_point_distance_eikonal(
226
+ d1, d2,
227
+ scratch_v1[0], scratch_v1[1], scratch_v1[2],
228
+ scratch_v2[0], scratch_v2[1], scratch_v2[2],
229
+ centroid[0], centroid[1], centroid[2]
230
+ );
231
+
232
+ if (d < min_dist) {
233
+ min_dist = d;
234
+ }
235
+
236
+ }
237
+
238
+ }
239
+
240
+ l_curr = mesh.loop_read_next(l_curr);
241
+ } while (l_curr !== l_end);
242
+
243
+ // If min_dist is Infinity, it means this face has NO boundary edges
244
+ // (it's internal). It should have inherited values from neighbors.
245
+ // We ignore it, or assume linear interpolation was correct.
246
+ // We only rescue faces touching the boundary that collapsed.
247
+
248
+ if (min_dist !== Infinity && min_dist > erode_distance) {
249
+ // RESCUE: The center is safe!
250
+ // Poke the face to create a vertex at the centroid.
251
+ const new_vert = bt_mesh_face_poke(mesh, f, centroid[0], centroid[1], centroid[2]);
252
+
253
+ // Update Data
254
+ island_vertices.add(new_vert);
255
+ vertex_boundary_distances.set(new_vert, min_dist);
256
+
257
+ // Read the new edges connected to the center and add to processing set
258
+ let e_first = mesh.vertex_read_edge(new_vert);
259
+
260
+ let e_scan = e_first;
261
+ do {
262
+ island_edges.add(e_scan);
263
+
264
+ const v1 = mesh.edge_read_vertex1(e_scan);
265
+ const is_v1 = v1 === new_vert;
266
+
267
+ // DEBUG checks
268
+ const v2 = mesh.edge_read_vertex2(e_scan);
269
+ if (is_v1) {
270
+ assert.equal(v1, new_vert, 'edge vertex does not match new vertex');
271
+ assert.notEqual(v2, new_vert, 'edge vertex does not match new vertex');
272
+ } else {
273
+ assert.equal(v2, new_vert, 'edge vertex does not match new vertex');
274
+ assert.notEqual(v1, new_vert, 'edge vertex does not match new vertex');
275
+ }
276
+
277
+ e_scan = is_v1 ? mesh.edge_read_v1_disk_next(e_scan) : mesh.edge_read_v2_disk_next(e_scan);
278
+ } while (e_scan !== e_first);
279
+ }
280
+ }
281
+ }
282
+ // ---------------------------------------------------------
283
+
284
+ // 4. Trace the Contour & Split Edges
285
+
286
+ /**
287
+ *
288
+ * @type {{e: number, t: number}[]}
289
+ */
290
+ const edges_to_split = [];
291
+
292
+ for (const e of island_edges) {
293
+
294
+ assert.equal(mesh.edges.is_allocated(e), true, 'edge is not allocated');
295
+
296
+ const v1 = mesh.edge_read_vertex1(e);
297
+ const v2 = mesh.edge_read_vertex2(e);
298
+
299
+ const d1 = vertex_boundary_distances.get(v1);
300
+ const d2 = vertex_boundary_distances.get(v2);
301
+
302
+ // If the edge strictly crosses the erosion threshold
303
+ const crosses_erosion_boundary = (d1 < erode_distance && d2 > erode_distance) || (d2 < erode_distance && d1 > erode_distance);
304
+
305
+ if (crosses_erosion_boundary) {
306
+ // Calculate exact interpolation parameter
307
+ const t = clamp01(inverseLerp(d1, d2, erode_distance));
308
+ edges_to_split.push({ e, t });
309
+ }
310
+ }
311
+
312
+ // Execute splits
313
+ for (const { e, t } of edges_to_split) {
314
+ // splits the edge as well as adjacent faces.
315
+ bt_edge_split(mesh, e, t);
316
+ }
317
+
318
+ // Cull Dead Geometry
319
+ // Any vertex that had a distance < erode_distance is dead.
320
+ // Any face connected to a dead vertex must be destroyed.
321
+ /**
322
+ *
323
+ * @type {Set<number>}
324
+ */
325
+ const faces_to_kill = new Set();
326
+
327
+ for (const [vertex, distance] of vertex_boundary_distances.entries()) {
328
+ const EPSILON = 1e-17;
329
+
330
+ if (distance >= erode_distance - EPSILON) {
331
+ // too far, survives
332
+ continue;
333
+ }
334
+
335
+ const e_first = mesh.vertex_read_edge(vertex);
336
+
337
+ if (e_first === NULL_POINTER) {
338
+ continue;
339
+ }
340
+
341
+ let e_curr = e_first;
342
+ do {
343
+ // Traverse radial loops to get all connected faces
344
+ const l_first = mesh.edge_read_loop(e_curr);
345
+ if (l_first !== NULL_POINTER) {
346
+ let l_curr = l_first;
347
+ do {
348
+ faces_to_kill.add(mesh.loop_read_face(l_curr));
349
+ l_curr = mesh.loop_read_radial_next(l_curr);
350
+ } while (l_curr !== l_first);
351
+ }
352
+
353
+ // Move around disk cycle
354
+ const v1 = mesh.edge_read_vertex1(e_curr);
355
+ e_curr = (v1 === vertex) ? mesh.edge_read_v1_disk_next(e_curr) : mesh.edge_read_v2_disk_next(e_curr);
356
+ } while (e_curr !== e_first && e_curr !== NULL_POINTER);
357
+ }
358
+
359
+ // Eradicate the dead faces
360
+ for (const f of faces_to_kill) {
361
+ bt_face_kill(mesh, f);
362
+ }
363
+
364
+ // 6. Cleanup floating vertices and edges
365
+ if (faces_to_kill.size > 0) {
366
+ bt_mesh_cleanup_faceless_references(mesh);
367
+ }
368
+ }