@woosh/meep-engine 2.163.7 → 2.163.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts +21 -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 +42 -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/PATHFINDING_PLAN.md +185 -0
  28. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
  29. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  30. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
  31. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
  32. package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +354 -138
  33. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts +17 -0
  34. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts.map +1 -0
  35. package/src/engine/navigation/mesh/navmesh_polyanya_find_path.js +613 -0
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.7",
9
+ "version": "2.163.8",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Fraction `t` in [0,1] along the segment (s0 -> s1) at which the infinite line through (l0, l1)
3
+ * crosses it. Returns NaN when the segment lies entirely on one side of the line (no crossing within
4
+ * the segment) or the two are parallel/collinear.
5
+ *
6
+ * Unlike {@link line_segment_compute_line_segment_intersection_array_2d}, the first pair of points
7
+ * defines an unbounded line (not a segment) and the result is the parametric position on the segment
8
+ * rather than the intersection point.
9
+ *
10
+ * @param {number} l0_x line point A x
11
+ * @param {number} l0_y line point A y
12
+ * @param {number} l1_x line point B x
13
+ * @param {number} l1_y line point B y
14
+ * @param {number} s0_x segment start x
15
+ * @param {number} s0_y segment start y
16
+ * @param {number} s1_x segment end x
17
+ * @param {number} s1_y segment end y
18
+ * @returns {number}
19
+ */
20
+ export function line_segment_intersection_fraction_2d(l0_x: number, l0_y: number, l1_x: number, l1_y: number, s0_x: number, s0_y: number, s1_x: number, s1_y: number): number;
21
+ //# sourceMappingURL=line_segment_intersection_fraction_2d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line_segment_intersection_fraction_2d.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/2d/line/line_segment_intersection_fraction_2d.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,4DAVW,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,GACJ,MAAM,CAsBlB"}
@@ -0,0 +1,42 @@
1
+ import { v2_cross_product } from "../../vec2/v2_cross_product.js";
2
+
3
+ /**
4
+ * Fraction `t` in [0,1] along the segment (s0 -> s1) at which the infinite line through (l0, l1)
5
+ * crosses it. Returns NaN when the segment lies entirely on one side of the line (no crossing within
6
+ * the segment) or the two are parallel/collinear.
7
+ *
8
+ * Unlike {@link line_segment_compute_line_segment_intersection_array_2d}, the first pair of points
9
+ * defines an unbounded line (not a segment) and the result is the parametric position on the segment
10
+ * rather than the intersection point.
11
+ *
12
+ * @param {number} l0_x line point A x
13
+ * @param {number} l0_y line point A y
14
+ * @param {number} l1_x line point B x
15
+ * @param {number} l1_y line point B y
16
+ * @param {number} s0_x segment start x
17
+ * @param {number} s0_y segment start y
18
+ * @param {number} s1_x segment end x
19
+ * @param {number} s1_y segment end y
20
+ * @returns {number}
21
+ */
22
+ export function line_segment_intersection_fraction_2d(
23
+ l0_x, l0_y, l1_x, l1_y,
24
+ s0_x, s0_y, s1_x, s1_y
25
+ ) {
26
+ const dir_x = l1_x - l0_x;
27
+ const dir_y = l1_y - l0_y;
28
+
29
+ const side_0 = v2_cross_product(dir_x, dir_y, s0_x - l0_x, s0_y - l0_y);
30
+ const side_1 = v2_cross_product(dir_x, dir_y, s1_x - l0_x, s1_y - l0_y);
31
+
32
+ if ((side_0 > 0 && side_1 > 0) || (side_0 < 0 && side_1 < 0)) {
33
+ return NaN; // both endpoints on the same side -> no crossing within the segment
34
+ }
35
+
36
+ const denom = side_0 - side_1;
37
+ if (denom === 0) {
38
+ return NaN; // parallel / collinear
39
+ }
40
+
41
+ return side_0 / denom;
42
+ }
@@ -8,8 +8,8 @@
8
8
  * The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
9
9
  *
10
10
  * @param {BinaryTopology} mesh
11
- * @param {number[]} faces
12
- * @param {number} erode_distance
11
+ * @param {number[]} faces seed faces of the island to erode
12
+ * @param {number} erode_distance distance to move the outline inwards by
13
13
  */
14
14
  export function bt_mesh_face_island_erode(mesh: BinaryTopology, faces: number[], erode_distance: number): void;
15
15
  //# sourceMappingURL=bt_mesh_face_island_erode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;GAYG;AACH,uEAHW,MAAM,EAAE,kBACR,MAAM,QAgehB"}
1
+ {"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;GAYG;AACH,uEAHW,MAAM,EAAE,kBACR,MAAM,QAqbhB"}
@@ -5,34 +5,18 @@ import {
5
5
  line3_compute_segment_point_distance_eikonal
6
6
  } from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
7
7
  import { NULL_POINTER } from "../BinaryTopology.js";
8
+ import { BitSet } from "../../../../../../binary/BitSet.js";
8
9
  import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
10
+ import { bt_face_island_flood_fill } from "../query/bt_face_island_flood_fill.js";
11
+ import {
12
+ bt_collect_boundary_segments,
13
+ bt_point_distance_to_segments,
14
+ bt_mesh_build_boundary_euclidean_distance_field
15
+ } from "../query/bt_mesh_build_boundary_euclidean_distance_field.js";
9
16
  import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
10
17
  import { bt_edge_split } from "./edge/bt_edge_split.js";
11
18
  import { bt_face_kill } from "./face/bt_face_kill.js";
12
19
  import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
13
- import { bt_face_get_neighbour_faces } from "../query/bt_face_get_neighbour_faces.js";
14
-
15
- const _eb_a = new Float32Array(3);
16
- const _eb_b = new Float32Array(3);
17
- const _eb_p = new Float32Array(3);
18
-
19
- /**
20
- * Squared straight-line distance from point p to segment a-b in 3D.
21
- */
22
- function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
23
- const abx = bx - ax, aby = by - ay, abz = bz - az;
24
- const apx = px - ax, apy = py - ay, apz = pz - az;
25
- const ab_len_sq = abx * abx + aby * aby + abz * abz;
26
-
27
- let t = ab_len_sq > 0 ? (apx * abx + apy * aby + apz * abz) / ab_len_sq : 0;
28
- if (t < 0) t = 0; else if (t > 1) t = 1;
29
-
30
- const cx = ax + t * abx - px;
31
- const cy = ay + t * aby - py;
32
- const cz = az + t * abz - pz;
33
-
34
- return cx * cx + cy * cy + cz * cz;
35
- }
36
20
 
37
21
  /**
38
22
  * Shrinks an island of faces by a given distance.
@@ -44,8 +28,8 @@ function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
44
28
  * The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
45
29
  *
46
30
  * @param {BinaryTopology} mesh
47
- * @param {number[]} faces
48
- * @param {number} erode_distance
31
+ * @param {number[]} faces seed faces of the island to erode
32
+ * @param {number} erode_distance distance to move the outline inwards by
49
33
  */
50
34
  export function bt_mesh_face_island_erode(
51
35
  mesh,
@@ -58,11 +42,25 @@ export function bt_mesh_face_island_erode(
58
42
  return;
59
43
  }
60
44
 
61
- // 1. + 2. Map the island's vertices/edges and detect boundary (single radial loop) edges/vertices.
62
- // Extracted into a helper so it can be recomputed after the connectivity pre-pass mutates the mesh.
45
+ // Reused island working sets. collect_island_sets clears and refills these in place, so the same
46
+ // four Set objects serve the whole erosion (which re-derives them many times) without allocating.
47
+ const island_vertices = new Set();
48
+ const island_edges = new Set();
49
+ const boundary_vertices = new Set();
50
+ const boundary_edges = new Set();
51
+
52
+ /**
53
+ * Refill the four island working sets for `island_faces`: every vertex and edge the island touches,
54
+ * plus the subset of each that lies on the boundary (an edge whose radial cycle is a single loop).
55
+ * Clears the sets first so their backing storage is reused across calls.
56
+ *
57
+ * @param {number[]} island_faces allocated face IDs making up the island
58
+ */
63
59
  function collect_island_sets(island_faces) {
64
- const iv = new Set();
65
- const ie = new Set();
60
+ island_vertices.clear();
61
+ island_edges.clear();
62
+ boundary_vertices.clear();
63
+ boundary_edges.clear();
66
64
 
67
65
  for (let i = 0; i < island_faces.length; i++) {
68
66
  const f = island_faces[i];
@@ -71,64 +69,44 @@ export function bt_mesh_face_island_erode(
71
69
 
72
70
  let l_curr = l_first;
73
71
  do {
74
- iv.add(mesh.loop_read_vertex(l_curr));
75
- ie.add(mesh.loop_read_edge(l_curr));
72
+ island_vertices.add(mesh.loop_read_vertex(l_curr));
73
+ island_edges.add(mesh.loop_read_edge(l_curr));
76
74
  l_curr = mesh.loop_read_next(l_curr);
77
75
  } while (l_curr !== l_first);
78
76
  }
79
77
 
80
- const bv = new Set();
81
- const be = new Set();
82
-
83
- for (const e of ie) {
78
+ for (const e of island_edges) {
84
79
  const l = mesh.edge_read_loop(e);
85
80
  if (mesh.loop_read_radial_next(l) === l) {
86
- bv.add(mesh.edge_read_vertex1(e));
87
- bv.add(mesh.edge_read_vertex2(e));
88
- be.add(e);
81
+ boundary_vertices.add(mesh.edge_read_vertex1(e));
82
+ boundary_vertices.add(mesh.edge_read_vertex2(e));
83
+ boundary_edges.add(e);
89
84
  }
90
85
  }
91
-
92
- return { island_vertices: iv, island_edges: ie, boundary_vertices: bv, boundary_edges_set: be };
93
86
  }
94
87
 
95
- // Re-derive the (now larger) face list of this island after edge splits by flood-filling
96
- // edge-adjacency from the original faces. bt_edge_split reuses the original face ids for one half
97
- // of each split, so they remain valid, allocated seeds.
98
- function recompute_island_faces(seed_faces) {
99
- const seen = new Set();
100
- const stack = [];
101
- const result = [];
102
- const nbr = [];
103
-
104
- for (let i = 0; i < seed_faces.length; i++) {
105
- const f = seed_faces[i];
106
- if (mesh.faces.is_allocated(f)) {
107
- stack.push(f);
108
- }
109
- }
110
-
111
- while (stack.length > 0) {
112
- const f = stack.pop();
113
- if (seen.has(f)) continue;
114
- seen.add(f);
115
- result.push(f);
116
-
117
- const n = bt_face_get_neighbour_faces(nbr, 0, mesh, f);
118
- for (let i = 0; i < n; i++) {
119
- const g = nbr[i];
120
- if (!seen.has(g)) {
121
- stack.push(g);
122
- }
123
- }
124
- }
88
+ // Reused buffers for re-deriving the island's faces after edge splits.
89
+ const flood_visited = new BitSet();
90
+ const island_faces_scratch = [];
125
91
 
126
- return result;
92
+ /**
93
+ * Re-derive the (now larger) face list of this island after edge splits, by flood-filling
94
+ * edge-adjacency from the original seed faces. {@link bt_edge_split} reuses the original face IDs
95
+ * for one half of each split, so the seeds stay allocated and edge-connected to the new fragments.
96
+ * Returns a reused buffer - the caller must not retain it across calls.
97
+ *
98
+ * @param {number[]} seed_faces original island face IDs
99
+ * @returns {number[]} the island's current faces (reused buffer)
100
+ */
101
+ function recompute_island_faces(seed_faces) {
102
+ flood_visited.reset();
103
+ island_faces_scratch.length = 0;
104
+ bt_face_island_flood_fill(mesh, seed_faces, island_faces_scratch, flood_visited);
105
+ return island_faces_scratch;
127
106
  }
128
107
 
129
108
  let island_faces = faces;
130
-
131
- let { island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces);
109
+ collect_island_sets(island_faces);
132
110
 
133
111
  if (boundary_vertices.size === 0) {
134
112
  // no boundary vertices found
@@ -137,23 +115,23 @@ export function bt_mesh_face_island_erode(
137
115
  }
138
116
 
139
117
  // ---------------------------------------------------------
140
- // 2.5. CONNECTIVITY PRE-PASS (fix for erosion fragmentation)
118
+ // CONNECTIVITY PRE-PASS (fix for erosion fragmentation)
141
119
  // ---------------------------------------------------------
142
- // The boundary distance field is sampled only at vertices and interpolated linearly along edges.
143
- // An *interior* edge whose BOTH endpoints lie on the boundary therefore reads as ~0 along its
144
- // entire length, even where its middle sits deep inside the island (the shared diagonal of a coarse
145
- // quad, or the seam between two large coplanar tiles). Erosion then culls a band straddling that
146
- // edge and severs one connected island into disconnected pieces.
120
+ // The distance field is sampled only at vertices and interpolated linearly along edges. An
121
+ // *interior* edge whose BOTH endpoints lie on the boundary therefore reads as ~0 along its entire
122
+ // length, even where its middle sits deep inside the island (the shared diagonal of a coarse quad,
123
+ // or the seam between two large coplanar tiles). Erosion then culls a band straddling that edge and
124
+ // severs one connected island into disconnected pieces.
147
125
  //
148
126
  // Fix: insert an interior vertex at the midpoint of every such edge. The new vertex is genuinely
149
- // interior, so the eikonal field gives it the correct (large) clearance and the contour keeps the
150
- // deep interior alive, preserving connectivity. A single pass suffices: splitting a
151
- // boundary-boundary interior edge yields only boundary-interior edges, creating no new offenders.
127
+ // interior, so it gets the correct (large) clearance and the contour keeps the deep interior alive,
128
+ // preserving connectivity. A single pass suffices: splitting a boundary-boundary interior edge
129
+ // yields only boundary-interior edges, creating no new offenders.
152
130
  {
153
131
  const split_targets = [];
154
132
 
155
133
  for (const e of island_edges) {
156
- if (boundary_edges_set.has(e)) {
134
+ if (boundary_edges.has(e)) {
157
135
  // a true boundary edge defines the outline - never split it
158
136
  continue;
159
137
  }
@@ -172,8 +150,7 @@ export function bt_mesh_face_island_erode(
172
150
  }
173
151
 
174
152
  island_faces = recompute_island_faces(faces);
175
-
176
- ({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
153
+ collect_island_sets(island_faces);
177
154
 
178
155
  if (boundary_vertices.size === 0) {
179
156
  return;
@@ -181,57 +158,34 @@ export function bt_mesh_face_island_erode(
181
158
  }
182
159
  }
183
160
 
184
- // 3. Distance field: EXACT straight-line distance from each island vertex to the nearest boundary
161
+ // Distance field: EXACT straight-line distance from each island vertex to the nearest boundary
185
162
  // edge. Agent clearance is a Euclidean (line-of-sight) distance to the nearest wall, NOT a geodesic
186
- // distance measured along the surface. The previous eikonal field was both semantically wrong and
187
- // sensitive to the triangulation - it produced a lumpy, non-uniform inset (e.g. a 14x14 platform
188
- // built from strips eroded by 0.45-0.65 instead of a uniform 0.4). Exact distance is uniform.
163
+ // distance measured along the surface, so the eikonal field would be both semantically wrong here
164
+ // and sensitive to the triangulation (a lumpy, non-uniform inset). See
165
+ // bt_mesh_build_boundary_euclidean_distance_field.
189
166
  /**
190
- * Vertex -> Distance from boundary
167
+ * Vertex -> distance from boundary.
191
168
  * @type {Map<number, number>}
192
169
  */
193
170
  const vertex_boundary_distances = new Map();
194
171
 
195
- // boundary edge segments, cached by compute_field for reuse in the deviation test below
196
- let boundary_segments = [];
172
+ // Flat boundary segment buffer (6 numbers per segment), reused across every field rebuild.
173
+ const boundary_segments = [];
197
174
 
198
- // (re)compute the exact distance field for the current island vertices/boundary
175
+ // (Re)compute the exact distance field for the current island vertices and boundary.
199
176
  function compute_field() {
200
- vertex_boundary_distances.clear();
201
-
202
- boundary_segments = [];
203
- for (const e of boundary_edges_set) {
204
- const bv1 = mesh.edge_read_vertex1(e);
205
- const bv2 = mesh.edge_read_vertex2(e);
206
- mesh.vertex_read_coordinate(_eb_a, 0, bv1);
207
- mesh.vertex_read_coordinate(_eb_b, 0, bv2);
208
- boundary_segments.push(_eb_a[0], _eb_a[1], _eb_a[2], _eb_b[0], _eb_b[1], _eb_b[2]);
209
- }
210
-
211
- for (const v of island_vertices) {
212
- mesh.vertex_read_coordinate(_eb_p, 0, v);
213
- vertex_boundary_distances.set(v, dist_to_boundary(_eb_p[0], _eb_p[1], _eb_p[2]));
214
- }
215
- }
216
-
217
- // exact straight-line distance from a point to the nearest boundary edge
218
- function dist_to_boundary(px, py, pz) {
219
- const seg = boundary_segments;
220
- let best_sq = Infinity;
221
- for (let i = 0; i < seg.length; i += 6) {
222
- const d_sq = point_segment_distance_sq(
223
- px, py, pz,
224
- seg[i], seg[i + 1], seg[i + 2],
225
- seg[i + 3], seg[i + 4], seg[i + 5]
226
- );
227
- if (d_sq < best_sq) best_sq = d_sq;
228
- }
229
- return best_sq === Infinity ? Infinity : Math.sqrt(best_sq);
177
+ bt_collect_boundary_segments(mesh, boundary_edges, boundary_segments);
178
+ bt_mesh_build_boundary_euclidean_distance_field(
179
+ vertex_boundary_distances,
180
+ mesh,
181
+ island_vertices,
182
+ boundary_segments
183
+ );
230
184
  }
231
185
 
232
186
  compute_field();
233
187
 
234
- // 3a. CUT-BAND REFINEMENT. The inset contour is reconstructed piecewise-linearly from the faces the
188
+ // CUT-BAND REFINEMENT. The inset contour is reconstructed piecewise-linearly from the faces the
235
189
  // erosion threshold passes through, so on a coarse/skewed triangulation the cut chord deviates from
236
190
  // the true offset (a strip-built platform eroded unevenly, 0.4 on one side and 0.6 on another). For
237
191
  // each straddling face, place the two cut points and test whether their midpoint really lies at the
@@ -245,10 +199,13 @@ export function bt_mesh_face_island_erode(
245
199
 
246
200
  const ca = new Float32Array(3), cb = new Float32Array(3), cc = new Float32Array(3);
247
201
 
202
+ // reused across rounds: edges whose longest side should be split this round
203
+ const refine_targets = new Set();
204
+
248
205
  while (rounds < 48) {
249
206
  rounds++;
250
207
 
251
- const targets = new Set();
208
+ refine_targets.clear();
252
209
 
253
210
  for (let i = 0; i < island_faces.length; i++) {
254
211
  const f = island_faces[i];
@@ -302,7 +259,7 @@ export function bt_mesh_face_island_erode(
302
259
 
303
260
  if (crossings === 2) {
304
261
  mx *= 0.5; my *= 0.5; mz *= 0.5;
305
- const true_d = dist_to_boundary(mx, my, mz);
262
+ const true_d = bt_point_distance_to_segments(boundary_segments, mx, my, mz);
306
263
  if (Math.abs(true_d - erode_distance) <= tol) {
307
264
  // the straight cut here is already accurate - leave this face coarse
308
265
  continue;
@@ -310,37 +267,38 @@ export function bt_mesh_face_island_erode(
310
267
  }
311
268
 
312
269
  const sl = (longest === e_ab) ? la : (longest === e_bc) ? lb : lc;
313
- targets.add(mesh.loop_read_edge(sl));
270
+ refine_targets.add(mesh.loop_read_edge(sl));
314
271
  }
315
272
 
316
- if (targets.size === 0) break;
273
+ if (refine_targets.size === 0) break;
317
274
 
318
- for (const e of targets) {
275
+ for (const e of refine_targets) {
319
276
  if (mesh.edges.is_allocated(e)) bt_edge_split(mesh, e, 0.5);
320
277
  }
321
278
 
322
279
  island_faces = recompute_island_faces(faces);
323
- ({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
280
+ collect_island_sets(island_faces);
324
281
 
325
282
  compute_field();
326
283
 
327
284
  if (mesh.faces.size > 200000) break;
328
285
  }
329
286
  }
287
+
330
288
  // ---------------------------------------------------------
331
- // 3.5. PEAK RESCUE PASS (The Fix)
332
- // Handle cases where a face is "submerged" by vertex values
333
- // but geometrically contains a safe zone (e.g. single triangle).
289
+ // PEAK RESCUE PASS
290
+ // Handle cases where a face is "submerged" by its vertex values but geometrically contains a safe
291
+ // zone (e.g. a single triangle whose corners are all on the boundary, but whose incenter is not).
334
292
  // ---------------------------------------------------------
335
293
 
336
- // We snapshot the faces list because we might modify topology (poke)
294
+ // Snapshot the face list because we may modify topology (poke) while iterating.
337
295
  const faces_to_check = [...island_faces];
338
296
  const centroid = [];
339
297
  const scratch_v1 = [];
340
298
  const scratch_v2 = [];
341
299
 
342
300
  for (const f of faces_to_check) {
343
- // Collect face vertices and check if they are all "dead"
301
+ // Collect face vertices and check if they are all "dead".
344
302
  let l_curr = mesh.face_read_loop(f);
345
303
  const l_end = l_curr;
346
304
  let all_dead = true;
@@ -354,16 +312,14 @@ export function bt_mesh_face_island_erode(
354
312
  } while (l_curr !== l_end);
355
313
 
356
314
  if (all_dead) {
357
- // This face is slated for total deletion.
358
- // Check if we should rescue it by adding a peak vertex.
315
+ // This face is slated for total deletion. Check whether we should rescue it by adding a peak
316
+ // vertex at the incenter.
359
317
 
360
- // 1. Calculate Centroid (incenter only for triangle, falls back to centroid for other N-gons)
318
+ // 1. Incenter (for a triangle; falls back to centroid for other n-gons).
361
319
  bt_face_get_incenter(centroid, 0, mesh, f);
362
320
 
363
- // 2. Calculate Centroid Distance to Boundary
364
- // (For a single island face, min dist to its own boundary edges)
321
+ // 2. Incenter distance to boundary (min over this face's own boundary-crossing edges).
365
322
  let min_dist = Infinity;
366
- // Iterate edges of this face
367
323
  l_curr = mesh.face_read_loop(f);
368
324
  do {
369
325
  const e = mesh.loop_read_edge(l_curr);
@@ -392,30 +348,23 @@ export function bt_mesh_face_island_erode(
392
348
  if (d < min_dist) {
393
349
  min_dist = d;
394
350
  }
395
-
396
351
  }
397
-
398
352
  }
399
353
 
400
354
  l_curr = mesh.loop_read_next(l_curr);
401
355
  } while (l_curr !== l_end);
402
356
 
403
- // If min_dist is Infinity, it means this face has NO boundary edges
404
- // (it's internal). It should have inherited values from neighbors.
405
- // We ignore it, or assume linear interpolation was correct.
406
- // We only rescue faces touching the boundary that collapsed.
407
-
357
+ // min_dist === Infinity means this face has no boundary edges (it's internal); leave it to
358
+ // the interpolated field. We only rescue faces touching the boundary that collapsed.
408
359
  if (min_dist !== Infinity && min_dist > erode_distance) {
409
- // RESCUE: The center is safe!
410
- // Poke the face to create a vertex at the centroid.
360
+ // RESCUE: the center is safe - poke the face to create a vertex at the incenter.
411
361
  const new_vert = bt_mesh_face_poke(mesh, f, centroid[0], centroid[1], centroid[2]);
412
362
 
413
- // Update Data
414
363
  island_vertices.add(new_vert);
415
364
  vertex_boundary_distances.set(new_vert, min_dist);
416
365
 
417
- // Read the new edges connected to the center and add to processing set
418
- let e_first = mesh.vertex_read_edge(new_vert);
366
+ // Register the new edges fanning from the centre into the working edge set.
367
+ const e_first = mesh.vertex_read_edge(new_vert);
419
368
 
420
369
  let e_scan = e_first;
421
370
  do {
@@ -424,7 +373,7 @@ export function bt_mesh_face_island_erode(
424
373
  const v1 = mesh.edge_read_vertex1(e_scan);
425
374
  const is_v1 = v1 === new_vert;
426
375
 
427
- // DEBUG checks
376
+ // sanity: exactly one endpoint is the new vertex
428
377
  const v2 = mesh.edge_read_vertex2(e_scan);
429
378
  if (is_v1) {
430
379
  assert.equal(v1, new_vert, 'edge vertex does not match new vertex');
@@ -439,15 +388,11 @@ export function bt_mesh_face_island_erode(
439
388
  }
440
389
  }
441
390
  }
442
- // ---------------------------------------------------------
443
-
444
- // 4. Trace the Contour & Split Edges
445
391
 
446
- /**
447
- *
448
- * @type {{e: number, t: number}[]}
449
- */
450
- const edges_to_split = [];
392
+ // Trace the contour: split every island edge that strictly crosses the erosion threshold, exactly at
393
+ // the crossing. Parallel arrays (edge id + parameter) avoid one small object per crossing edge.
394
+ const split_edges = [];
395
+ const split_ts = [];
451
396
 
452
397
  for (const e of island_edges) {
453
398
 
@@ -459,34 +404,31 @@ export function bt_mesh_face_island_erode(
459
404
  const d1 = vertex_boundary_distances.get(v1);
460
405
  const d2 = vertex_boundary_distances.get(v2);
461
406
 
462
- // If the edge strictly crosses the erosion threshold
407
+ // does the edge strictly cross the erosion threshold?
463
408
  const crosses_erosion_boundary = (d1 < erode_distance && d2 > erode_distance) || (d2 < erode_distance && d1 > erode_distance);
464
409
 
465
410
  if (crosses_erosion_boundary) {
466
- // Calculate exact interpolation parameter
467
411
  const t = clamp01(inverseLerp(d1, d2, erode_distance));
468
- edges_to_split.push({ e, t });
412
+ split_edges.push(e);
413
+ split_ts.push(t);
469
414
  }
470
415
  }
471
416
 
472
- // Execute splits
473
- for (const { e, t } of edges_to_split) {
474
- // splits the edge as well as adjacent faces.
475
- bt_edge_split(mesh, e, t);
417
+ for (let i = 0; i < split_edges.length; i++) {
418
+ // splits the edge as well as the adjacent faces
419
+ bt_edge_split(mesh, split_edges[i], split_ts[i]);
476
420
  }
477
421
 
478
- // Cull Dead Geometry
479
- // Any vertex that had a distance < erode_distance is dead.
480
- // Any face connected to a dead vertex must be destroyed.
422
+ // Cull dead geometry: any vertex whose distance is < erode_distance is dead, and every face touching
423
+ // a dead vertex must be destroyed.
481
424
  /**
482
- *
483
425
  * @type {Set<number>}
484
426
  */
485
427
  const faces_to_kill = new Set();
486
428
 
487
- for (const [vertex, distance] of vertex_boundary_distances.entries()) {
488
- const EPSILON = 1e-17;
429
+ const EPSILON = 1e-17;
489
430
 
431
+ for (const [vertex, distance] of vertex_boundary_distances.entries()) {
490
432
  if (distance >= erode_distance - EPSILON) {
491
433
  // too far, survives
492
434
  continue;
@@ -500,7 +442,7 @@ export function bt_mesh_face_island_erode(
500
442
 
501
443
  let e_curr = e_first;
502
444
  do {
503
- // Traverse radial loops to get all connected faces
445
+ // traverse radial loops to gather all faces connected to this edge
504
446
  const l_first = mesh.edge_read_loop(e_curr);
505
447
  if (l_first !== NULL_POINTER) {
506
448
  let l_curr = l_first;
@@ -510,18 +452,17 @@ export function bt_mesh_face_island_erode(
510
452
  } while (l_curr !== l_first);
511
453
  }
512
454
 
513
- // Move around disk cycle
455
+ // move around the disk cycle
514
456
  const v1 = mesh.edge_read_vertex1(e_curr);
515
457
  e_curr = (v1 === vertex) ? mesh.edge_read_v1_disk_next(e_curr) : mesh.edge_read_v2_disk_next(e_curr);
516
458
  } while (e_curr !== e_first && e_curr !== NULL_POINTER);
517
459
  }
518
460
 
519
- // Eradicate the dead faces
520
461
  for (const f of faces_to_kill) {
521
462
  bt_face_kill(mesh, f);
522
463
  }
523
464
 
524
- // 6. Cleanup floating vertices and edges
465
+ // Cleanup floating vertices and edges.
525
466
  if (faces_to_kill.size > 0) {
526
467
  bt_mesh_cleanup_faceless_references(mesh);
527
468
  }
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Fill boundary holes that are too thin for the agent to occupy.
2
+ * Fill inner boundary holes whose mean width falls below `min_width`.
3
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.
4
+ * A mesh can contain hair-thin sliver holes - a strip of degenerate faces pinched off into its own
5
+ * tiny boundary loop - that are numerical noise rather than meaningful openings. Such slivers inflate
6
+ * the boundary length, the face count, and the loop count. This measures every boundary loop's mean
7
+ * width (2*area/perimeter, the inradius of the area-equivalent disc) and fills each inner loop narrower
8
+ * than `min_width`. The single largest-area loop is treated as the outer boundary and is never filled.
10
9
  *
11
- * @param {BinaryTopology} mesh
12
- * @param {number} agent_radius
10
+ * @param {BinaryTopology} mesh mesh whose boundary loops are inspected and (where too thin) filled in place
11
+ * @param {number} min_width minimum mean width an inner loop must have to be kept; thinner loops are filled
13
12
  * @returns {number} number of holes filled
14
13
  */
15
- export function bt_mesh_fill_small_holes(mesh: BinaryTopology, agent_radius: number): number;
14
+ export function bt_mesh_fill_small_holes(mesh: BinaryTopology, min_width: number): number;
16
15
  //# sourceMappingURL=bt_mesh_fill_small_holes.d.ts.map
@@ -1 +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"}
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;;;;;;;;;;;;GAYG;AACH,0EAHW,MAAM,GACJ,MAAM,CA2ClB"}