@woosh/meep-engine 2.163.9 → 2.163.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/package.json +1 -1
  2. package/src/core/collection/heap/IndexedFloatMaxHeap.d.ts.map +1 -0
  3. package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.js +1 -1
  4. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  5. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +2 -26
  6. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts +19 -0
  7. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.d.ts.map +1 -0
  8. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js +555 -0
  9. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts +13 -0
  10. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.d.ts.map +1 -0
  11. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.js +28 -0
  12. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts +1 -1
  13. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts.map +1 -1
  14. package/src/core/graph/metis/native/bisection/BisectionScratch.js +1 -1
  15. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts +1 -1
  16. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts.map +1 -1
  17. package/src/core/graph/metis/native/refine/RefinementScratch.js +1 -1
  18. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  19. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +25 -0
  20. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.d.ts.map +0 -1
  21. /package/src/core/{graph/metis/native/refine → collection/heap}/IndexedFloatMaxHeap.d.ts +0 -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.9",
9
+ "version": "2.163.10",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IndexedFloatMaxHeap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/heap/IndexedFloatMaxHeap.js"],"names":[],"mappings":"AA6BA;IACI;;;OAGG;IACH,yBAHW,MAAM,0BACN,MAAM,EAsBhB;IAbG,wBAA4C;IAC5C,eAAe;IAEf,2BAA4E;IAC5E,2BAAwD;IACxD,6BAA0D;IAE1D;;;OAGG;IACH,cAFU,WAAW,CAE2B;IAIpD,mBAEC;IAED;;;OAGG;IACH,cAQC;IAED,2BAEC;IAED;;;OAGG;IACH,qBAkBC;IAED;;;;OAIG;IACH,oBAsBC;IAED;;;;OAIG;IACH,kBAYC;IAED,wBAUC;IAED;;;OAGG;IACH,WAHW,MAAM,SACN,MAAM,QAehB;IAED;;OAEG;IACH,WAFa,MAAM,CAuBlB;IAED;;;OAGG;IACH,WAHW,MAAM,aACN,MAAM,QAehB;IAED;;;OAGG;IACH,WAHW,MAAM,GACJ,OAAO,CA+BnB;CACJ"}
@@ -1,4 +1,4 @@
1
- import { assert } from "../../../../assert.js";
1
+ import { assert } from "../../assert.js";
2
2
 
3
3
  /**
4
4
  * Max-heap of (uint32 id, float32 score) entries with an external id→slot index
@@ -1 +1 @@
1
- {"version":3,"file":"bt_mesh_simplify.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js"],"names":[],"mappings":"AAqLA;;;;;;;;;;;;;;GAcG;AACH,0EAHW,MAAM,wBACN,IAAI,MAAM,CAAC,QA6HrB"}
1
+ {"version":3,"file":"bt_mesh_simplify.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js"],"names":[],"mappings":"AA6JA;;;;;;;;;;;;;;GAcG;AACH,0EAHW,MAAM,wBACN,IAAI,MAAM,CAAC,QA6HrB"}
@@ -7,6 +7,7 @@ import { bt_mesh_compute_vertex_quadratics } from "./bt_mesh_compute_vertex_quad
7
7
  import { bt_edge_collapse } from "./edge/bt_edge_collapse.js";
8
8
  import { bt_edge_kill } from "./edge/bt_edge_kill.js";
9
9
  import { bt_edge_kill_parallels } from "./edge/bt_edge_kill_parallels.js";
10
+ import { bt_edge_swap_vertex_slots } from "./edge/bt_edge_swap_vertex_slots.js";
10
11
  import { bt_vert_fuse_duplicate_edges } from "./vertex/bt_vert_fuse_duplicate_edges.js";
11
12
  import { bt_vert_kill } from "./vertex/bt_vert_kill.js";
12
13
 
@@ -50,31 +51,6 @@ function count_edge_faces(mesh, edge_id) {
50
51
  return count;
51
52
  }
52
53
 
53
- /**
54
- * Swap the v1/v2 slots of an edge. Disk cycle pointers for the two slots are swapped
55
- * alongside so that the disk cycles on both attached vertices remain valid.
56
- *
57
- * @param {BinaryTopology} mesh
58
- * @param {number} edge_id
59
- */
60
- function swap_edge_vertex_slots(mesh, edge_id) {
61
- const v1 = mesh.edge_read_vertex1(edge_id);
62
- const v2 = mesh.edge_read_vertex2(edge_id);
63
-
64
- const v1_next = mesh.edge_read_v1_disk_next(edge_id);
65
- const v1_prev = mesh.edge_read_v1_disk_prev(edge_id);
66
- const v2_next = mesh.edge_read_v2_disk_next(edge_id);
67
- const v2_prev = mesh.edge_read_v2_disk_prev(edge_id);
68
-
69
- mesh.edge_write_vertex1(edge_id, v2);
70
- mesh.edge_write_vertex2(edge_id, v1);
71
-
72
- mesh.edge_write_v1_disk_next(edge_id, v2_next);
73
- mesh.edge_write_v1_disk_prev(edge_id, v2_prev);
74
- mesh.edge_write_v2_disk_next(edge_id, v1_next);
75
- mesh.edge_write_v2_disk_prev(edge_id, v1_prev);
76
- }
77
-
78
54
  /**
79
55
  * Evaluate a potential edge collapse: choose the optimal surviving vertex position
80
56
  * and return the quadric-based cost. Returns Infinity if the edge cannot be collapsed.
@@ -285,7 +261,7 @@ export function bt_mesh_simplify(
285
261
  // bt_edge_collapse keeps v1 as the survivor. If v2 is the pinned endpoint,
286
262
  // swap the edge's slots so the pinned vertex ends up in the v1 position.
287
263
  if (v2_restricted) {
288
- swap_edge_vertex_slots(mesh, edge_id);
264
+ bt_edge_swap_vertex_slots(mesh, edge_id);
289
265
  }
290
266
 
291
267
  const pre_survivor = mesh.edge_read_vertex1(edge_id);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Clean up a triangle mesh in place by removing only geometrically redundant vertices, so the
3
+ * surface and its boundary are preserved to within `max_error`. Unlike {@link bt_mesh_simplify}
4
+ * (quadric LoD decimation driven to a face-count target, free to relocate vertices), this is a
5
+ * conservative half-edge cleanup: it never moves a surviving vertex and never changes the
6
+ * boundary outline or hole topology by more than `max_error`. It removes the dense interior of
7
+ * flat/near-coplanar regions and collinear runs of boundary vertices, and stops once the
8
+ * cheapest remaining collapse would exceed the tolerance.
9
+ *
10
+ * Intended for navmesh post-processing, where the input is a walkable surface whose outline and
11
+ * holes (obstacle footprints) carry meaning and must not drift, but whose interior inherits far
12
+ * more triangles than navigation needs.
13
+ *
14
+ * @param {BinaryTopology} mesh Mesh to clean up in-place.
15
+ * @param {number} max_error Maximum world-space deviation allowed for any removed vertex and for
16
+ * the boundary polyline. A no-op when <= 0.
17
+ */
18
+ export function bt_mesh_simplify_by_error(mesh: BinaryTopology, max_error: number): void;
19
+ //# sourceMappingURL=bt_mesh_simplify_by_error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bt_mesh_simplify_by_error.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js"],"names":[],"mappings":"AA4aA;;;;;;;;;;;;;;;;GAgBG;AACH,2EAHW,MAAM,QAgHhB"}
@@ -0,0 +1,555 @@
1
+ import { assert } from "../../../../../../assert.js";
2
+ import { FibonacciHeap } from "../../../../../../collection/heap/FibonacciHeap.js";
3
+ import {
4
+ line3_compute_segment_point_distance_sqr
5
+ } from "../../../../line/line3_compute_segment_point_distance_sqr.js";
6
+ import { NULL_POINTER } from "../BinaryTopology.js";
7
+ import { bt_query_edge_is_boundary } from "../query/bt_query_edge_is_boundary.js";
8
+ import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
9
+ import { bt_mesh_compute_vertex_quadratics } from "./bt_mesh_compute_vertex_quadratics.js";
10
+ import { bt_edge_collapse } from "./edge/bt_edge_collapse.js";
11
+ import { bt_edge_kill_parallels } from "./edge/bt_edge_kill_parallels.js";
12
+ import { bt_edge_swap_vertex_slots } from "./edge/bt_edge_swap_vertex_slots.js";
13
+ import { bt_vert_fuse_duplicate_edges } from "./vertex/bt_vert_fuse_duplicate_edges.js";
14
+
15
+ // Module-level scratch to keep the hot loop allocation-free.
16
+ const _pos_a = new Float32Array(3);
17
+ const _pos_b = new Float32Array(3);
18
+ const _pos_c = new Float32Array(3);
19
+ const _pos_s = new Float32Array(3);
20
+ const _pos_v = new Float32Array(3);
21
+ const _pos_target = new Float32Array(3);
22
+ const _n_before = new Float32Array(3);
23
+ const _n_after = new Float32Array(3);
24
+
25
+ // Cap on the merged valence (deg(v1) + deg(v2)) of a collapse. A flat region otherwise lets the
26
+ // greedy order funnel every triangle into a single high-valence "star" vertex, which makes each
27
+ // subsequent disk walk / requeue O(degree) and the whole pass O(n^2). Bounding the merged
28
+ // neighbourhood keeps every per-vertex walk O(1), so the pass is O(n log n). It can leave a few
29
+ // extra triangles in a large flat region versus the theoretical minimum -- a good trade for
30
+ // navigation, where a balanced low-valence mesh also searches better than a sliver fan.
31
+ const DEGREE_SUM_CAP = 24;
32
+
33
+ // Per-vertex degree and boundary-membership caches, kept in plain typed arrays so choose_collapse
34
+ // reads them in O(1) instead of disk-walking on every call (BinaryTopology accessors go through a
35
+ // DataView, which is comparatively slow, and choose_collapse runs far more often than collapses do).
36
+ // They are refreshed for the survivor and its neighbours after each collapse -- the only vertices a
37
+ // collapse can change. Assigned per call to bt_mesh_simplify_by_error (the pass is not re-entrant).
38
+ let _degree = null;
39
+ let _vboundary = null;
40
+
41
+ // Result of the most recent choose_collapse: the kept and removed endpoints of the best
42
+ // (cheapest, in-tolerance) collapse direction. Only meaningful when it returned a finite cost.
43
+ let _choice_survivor = NULL_POINTER;
44
+ let _choice_victim = NULL_POINTER;
45
+
46
+ /**
47
+ * Refresh `_degree[v]` and `_vboundary[v]` from the mesh in a single disk-cycle walk: the degree is
48
+ * the cycle length and the vertex is on a boundary when any incident edge is a boundary edge.
49
+ *
50
+ * @param {BinaryTopology} mesh
51
+ * @param {number} v
52
+ */
53
+ function recompute_vertex_cache(mesh, v) {
54
+ let e = mesh.vertex_read_edge(v);
55
+
56
+ if (e === NULL_POINTER) {
57
+ _degree[v] = 0;
58
+ _vboundary[v] = 0;
59
+ return;
60
+ }
61
+
62
+ const start_e = e;
63
+ let n = 0;
64
+ let bnd = 0;
65
+
66
+ do {
67
+ n++;
68
+
69
+ if (bnd === 0 && bt_query_edge_is_boundary(mesh, e)) {
70
+ bnd = 1;
71
+ }
72
+
73
+ e = (mesh.edge_read_vertex1(e) === v)
74
+ ? mesh.edge_read_v1_disk_next(e)
75
+ : mesh.edge_read_v2_disk_next(e);
76
+ } while (e !== start_e && e !== NULL_POINTER);
77
+
78
+ _degree[v] = n;
79
+ _vboundary[v] = bnd;
80
+ }
81
+
82
+ /**
83
+ * For a boundary vertex `v`, return the far endpoint of its boundary edge other than
84
+ * `exclude_edge` -- i.e. `v`'s neighbour along the boundary polyline on the far side.
85
+ *
86
+ * Returns NULL_POINTER unless `v` has exactly one such other boundary edge (a clean
87
+ * 2-manifold boundary vertex). Dead-ends and pinch/junction vertices are declined so the
88
+ * caller conservatively skips simplifying there rather than mis-stepping the polyline.
89
+ *
90
+ * @param {BinaryTopology} mesh
91
+ * @param {number} v
92
+ * @param {number} exclude_edge
93
+ * @returns {number}
94
+ */
95
+ function other_boundary_neighbor(mesh, v, exclude_edge) {
96
+ let e = mesh.vertex_read_edge(v);
97
+
98
+ if (e === NULL_POINTER) {
99
+ return NULL_POINTER;
100
+ }
101
+
102
+ const start_e = e;
103
+
104
+ let found = NULL_POINTER;
105
+ let count = 0;
106
+
107
+ do {
108
+ if (e !== exclude_edge && bt_query_edge_is_boundary(mesh, e)) {
109
+ count++;
110
+ const v1 = mesh.edge_read_vertex1(e);
111
+ const v2 = mesh.edge_read_vertex2(e);
112
+ found = (v1 === v) ? v2 : v1;
113
+ }
114
+
115
+ e = (mesh.edge_read_vertex1(e) === v)
116
+ ? mesh.edge_read_v1_disk_next(e)
117
+ : mesh.edge_read_v2_disk_next(e);
118
+ } while (e !== start_e && e !== NULL_POINTER);
119
+
120
+ if (count !== 1) {
121
+ return NULL_POINTER;
122
+ }
123
+
124
+ return found;
125
+ }
126
+
127
+ /**
128
+ * Unnormalised triangle normal (b-a) x (c-a) written into `out`.
129
+ *
130
+ * Deliberately NOT v3_compute_triangle_normal: the flip guard needs the raw cross product. Its
131
+ * magnitude vanishing signals a degenerate (zero-area) result, which the caller's `dot <= 0` test
132
+ * then rejects. v3_compute_triangle_normal normalises (discarding that magnitude) and returns a
133
+ * fixed (0,1,0) for degenerate triangles, which would let a collapse-to-sliver slip the guard.
134
+ */
135
+ function triangle_cross(out, ax, ay, az, bx, by, bz, cx, cy, cz) {
136
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
137
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
138
+
139
+ out[0] = uy * vz - uz * vy;
140
+ out[1] = uz * vx - ux * vz;
141
+ out[2] = ux * vy - uy * vx;
142
+ }
143
+
144
+ /**
145
+ * Quadric-error cost of relocating both endpoints' supporting planes to `(tx,ty,tz)`.
146
+ * For a half-edge collapse the target is the (stationary) survivor's own position, so this
147
+ * measures how far the removed vertex's surface would deviate -- zero on a flat region.
148
+ */
149
+ function quadric_cost_at(q_survivor, q_victim, tx, ty, tz) {
150
+ return q_survivor.evaluate(tx, ty, tz) + q_victim.evaluate(tx, ty, tz);
151
+ }
152
+
153
+ /**
154
+ * Pick the best allowed collapse for edge `e` and return its quadric error. Writes the kept
155
+ * and removed vertices into `_choice_survivor` / `_choice_victim`. Returns POSITIVE_INFINITY
156
+ * when no direction is allowed.
157
+ *
158
+ * Allowability rules (cleanup, not decimation -- nothing moves, the boundary shape is kept):
159
+ * - the survivor is always an existing vertex and never relocates;
160
+ * - a *boundary* edge may collapse only by removing an endpoint that is collinear within
161
+ * `eps` with its two boundary neighbours, so the outline polyline shifts by <= eps;
162
+ * - an *interior* edge may remove only a non-boundary endpoint (the boundary endpoint, if
163
+ * any, survives), and is forbidden outright when *both* endpoints are boundary vertices
164
+ * (collapsing it would pinch a thin neck or weld two boundary loops -- a topology change).
165
+ *
166
+ * @param {BinaryTopology} mesh
167
+ * @param {number} e
168
+ * @param {Quadric3[]} quadratics
169
+ * @param {number} eps
170
+ * @returns {number}
171
+ */
172
+ function choose_collapse(mesh, e, quadratics, eps) {
173
+ const v1 = mesh.edge_read_vertex1(e);
174
+ const v2 = mesh.edge_read_vertex2(e);
175
+
176
+ if (v1 === v2) {
177
+ return Number.POSITIVE_INFINITY;
178
+ }
179
+
180
+ if (mesh.edge_read_loop(e) === NULL_POINTER) {
181
+ // wire edge with no incident face -- nothing to clean up
182
+ return Number.POSITIVE_INFINITY;
183
+ }
184
+
185
+ const q1 = quadratics[v1];
186
+ const q2 = quadratics[v2];
187
+
188
+ if (q1 == null || q2 == null) {
189
+ return Number.POSITIVE_INFINITY;
190
+ }
191
+
192
+ // Valence cap (see DEGREE_SUM_CAP). The merged neighbourhood size is the same whichever
193
+ // endpoint survives, so this gates the edge regardless of collapse direction.
194
+ if (_degree[v1] + _degree[v2] > DEGREE_SUM_CAP) {
195
+ return Number.POSITIVE_INFINITY;
196
+ }
197
+
198
+ mesh.vertex_read_coordinate(_pos_a, 0, v1);
199
+ mesh.vertex_read_coordinate(_pos_b, 0, v2);
200
+
201
+ let best = Number.POSITIVE_INFINITY;
202
+ let best_survivor = NULL_POINTER;
203
+ let best_victim = NULL_POINTER;
204
+
205
+ if (bt_query_edge_is_boundary(mesh, e)) {
206
+ // Boundary edge: both endpoints are boundary vertices. Removing a vertex replaces the two
207
+ // boundary edges meeting at it with the single segment between its two boundary neighbours,
208
+ // so the outline shifts by the removed vertex's distance to that *segment*. Allow the
209
+ // removal only when that shift is within eps. Distance must be to the segment, not the
210
+ // infinite line: a near-collinear spike whose tip projects beyond its neighbours sits on
211
+ // their line yet is far from the segment between them, and cutting it would move the
212
+ // boundary by the whole spike length.
213
+ const eps_sq = eps * eps;
214
+
215
+ // remove v2, keep v1
216
+ const w2 = other_boundary_neighbor(mesh, v2, e);
217
+ if (w2 !== NULL_POINTER) {
218
+ mesh.vertex_read_coordinate(_pos_c, 0, w2);
219
+ const dev_sqr = line3_compute_segment_point_distance_sqr(
220
+ _pos_a[0], _pos_a[1], _pos_a[2],
221
+ _pos_c[0], _pos_c[1], _pos_c[2],
222
+ _pos_b[0], _pos_b[1], _pos_b[2]
223
+ );
224
+ if (dev_sqr <= eps_sq) {
225
+ const cost = quadric_cost_at(q1, q2, _pos_a[0], _pos_a[1], _pos_a[2]);
226
+ if (cost < best) {
227
+ best = cost;
228
+ best_survivor = v1;
229
+ best_victim = v2;
230
+ }
231
+ }
232
+ }
233
+
234
+ // remove v1, keep v2
235
+ const w1 = other_boundary_neighbor(mesh, v1, e);
236
+ if (w1 !== NULL_POINTER) {
237
+ mesh.vertex_read_coordinate(_pos_c, 0, w1);
238
+ const dev_sqr = line3_compute_segment_point_distance_sqr(
239
+ _pos_b[0], _pos_b[1], _pos_b[2],
240
+ _pos_c[0], _pos_c[1], _pos_c[2],
241
+ _pos_a[0], _pos_a[1], _pos_a[2]
242
+ );
243
+ if (dev_sqr <= eps_sq) {
244
+ const cost = quadric_cost_at(q1, q2, _pos_b[0], _pos_b[1], _pos_b[2]);
245
+ if (cost < best) {
246
+ best = cost;
247
+ best_survivor = v2;
248
+ best_victim = v1;
249
+ }
250
+ }
251
+ }
252
+ } else {
253
+ // Interior edge.
254
+ const b1 = _vboundary[v1] === 1;
255
+ const b2 = _vboundary[v2] === 1;
256
+
257
+ if (b1 && b2) {
258
+ // both on the boundary, edge interior -> pinching / loop-welding, forbidden
259
+ return Number.POSITIVE_INFINITY;
260
+ }
261
+
262
+ if (!b2) {
263
+ // remove v2, keep v1
264
+ const cost = quadric_cost_at(q1, q2, _pos_a[0], _pos_a[1], _pos_a[2]);
265
+ if (cost < best) {
266
+ best = cost;
267
+ best_survivor = v1;
268
+ best_victim = v2;
269
+ }
270
+ }
271
+
272
+ if (!b1) {
273
+ // remove v1, keep v2
274
+ const cost = quadric_cost_at(q1, q2, _pos_b[0], _pos_b[1], _pos_b[2]);
275
+ if (cost < best) {
276
+ best = cost;
277
+ best_survivor = v2;
278
+ best_victim = v1;
279
+ }
280
+ }
281
+ }
282
+
283
+ _choice_survivor = best_survivor;
284
+ _choice_victim = best_victim;
285
+
286
+ return best;
287
+ }
288
+
289
+ /**
290
+ * Would removing `victim` (folding it onto the stationary `survivor`) invert or degenerate
291
+ * any of the faces it reshapes? Checks every face incident to `victim` that does not also
292
+ * touch `survivor` (those are the collapse faces, which die) by comparing each triangle's
293
+ * normal before and after the substitution. A non-positive dot means a flip or a collapse to
294
+ * zero area -- both reject the collapse.
295
+ *
296
+ * This guards the in-plane fold that the quadric error cannot see: on a flat region every
297
+ * candidate scores ~0, but a non-star-shaped one-ring would still overlap if collapsed.
298
+ *
299
+ * @param {BinaryTopology} mesh
300
+ * @param {number} victim
301
+ * @param {number} survivor
302
+ * @returns {boolean}
303
+ */
304
+ function collapse_would_fold(mesh, victim, survivor) {
305
+ mesh.vertex_read_coordinate(_pos_s, 0, survivor);
306
+ mesh.vertex_read_coordinate(_pos_v, 0, victim);
307
+
308
+ const face_pool = mesh.faces;
309
+
310
+ let e = mesh.vertex_read_edge(victim);
311
+
312
+ if (e === NULL_POINTER) {
313
+ return false;
314
+ }
315
+
316
+ const start_e = e;
317
+
318
+ do {
319
+ const l0 = mesh.edge_read_loop(e);
320
+
321
+ if (l0 !== NULL_POINTER) {
322
+ let l = l0;
323
+
324
+ do {
325
+ // each face incident to `victim` is reached exactly once via its victim-anchored loop
326
+ if (mesh.loop_read_vertex(l) === victim) {
327
+ const f = mesh.loop_read_face(l);
328
+
329
+ if (f !== NULL_POINTER && face_pool.is_allocated(f)) {
330
+ const lb = mesh.loop_read_next(l);
331
+ const lc = mesh.loop_read_next(lb);
332
+
333
+ // triangle faces only
334
+ if (mesh.loop_read_next(lc) === l) {
335
+ const vb = mesh.loop_read_vertex(lb);
336
+ const vc = mesh.loop_read_vertex(lc);
337
+
338
+ // skip the faces that die in the collapse (they touch the survivor)
339
+ if (vb !== survivor && vc !== survivor) {
340
+ mesh.vertex_read_coordinate(_pos_b, 0, vb);
341
+ mesh.vertex_read_coordinate(_pos_c, 0, vc);
342
+
343
+ triangle_cross(
344
+ _n_before,
345
+ _pos_v[0], _pos_v[1], _pos_v[2],
346
+ _pos_b[0], _pos_b[1], _pos_b[2],
347
+ _pos_c[0], _pos_c[1], _pos_c[2]
348
+ );
349
+ triangle_cross(
350
+ _n_after,
351
+ _pos_s[0], _pos_s[1], _pos_s[2],
352
+ _pos_b[0], _pos_b[1], _pos_b[2],
353
+ _pos_c[0], _pos_c[1], _pos_c[2]
354
+ );
355
+
356
+ const dot = _n_before[0] * _n_after[0]
357
+ + _n_before[1] * _n_after[1]
358
+ + _n_before[2] * _n_after[2];
359
+
360
+ if (dot <= 0) {
361
+ return true;
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ l = mesh.loop_read_radial_next(l);
369
+ } while (l !== l0 && l !== NULL_POINTER);
370
+ }
371
+
372
+ e = (mesh.edge_read_vertex1(e) === victim)
373
+ ? mesh.edge_read_v1_disk_next(e)
374
+ : mesh.edge_read_v2_disk_next(e);
375
+ } while (e !== start_e && e !== NULL_POINTER);
376
+
377
+ return false;
378
+ }
379
+
380
+ /**
381
+ * Re-cost the collapses affected by a collapse that produced survivor `s`. Re-evaluates `s`'s
382
+ * incident edges (their endpoints' valence, quadric, and -- for the boundary ones -- the outline
383
+ * shape all moved) and updates the heap in place: still-collapsible edges are inserted/updated,
384
+ * edges that left the tolerance are removed. Edges further out keep their old heap score; this only
385
+ * affects pop *order*, never correctness, because every collapse is re-validated against the live
386
+ * mesh when it is finally popped.
387
+ *
388
+ * @param {BinaryTopology} mesh
389
+ * @param {number} s
390
+ * @param {Quadric3[]} quadratics
391
+ * @param {number} eps
392
+ * @param {number} eps_sq
393
+ * @param {FibonacciHeap} heap
394
+ */
395
+ function requeue_ring(mesh, s, quadratics, eps, eps_sq, heap) {
396
+ // The survivor and its neighbours are exactly the vertices whose degree/boundary status a
397
+ // collapse can change, so refresh their cache here (before choose_collapse reads it) rather
398
+ // than walking the disk inside every choose_collapse.
399
+ recompute_vertex_cache(mesh, s);
400
+
401
+ let e = mesh.vertex_read_edge(s);
402
+
403
+ if (e === NULL_POINTER) {
404
+ return;
405
+ }
406
+
407
+ const start_e = e;
408
+
409
+ do {
410
+ const v1 = mesh.edge_read_vertex1(e);
411
+ const w = (v1 === s) ? mesh.edge_read_vertex2(e) : v1;
412
+
413
+ recompute_vertex_cache(mesh, w);
414
+
415
+ const cost = choose_collapse(mesh, e, quadratics, eps);
416
+
417
+ if (cost <= eps_sq) {
418
+ heap.insert_or_update(e, cost);
419
+ } else {
420
+ heap.remove(e);
421
+ }
422
+
423
+ e = (v1 === s)
424
+ ? mesh.edge_read_v1_disk_next(e)
425
+ : mesh.edge_read_v2_disk_next(e);
426
+ } while (e !== start_e && e !== NULL_POINTER);
427
+ }
428
+
429
+ /**
430
+ * Clean up a triangle mesh in place by removing only geometrically redundant vertices, so the
431
+ * surface and its boundary are preserved to within `max_error`. Unlike {@link bt_mesh_simplify}
432
+ * (quadric LoD decimation driven to a face-count target, free to relocate vertices), this is a
433
+ * conservative half-edge cleanup: it never moves a surviving vertex and never changes the
434
+ * boundary outline or hole topology by more than `max_error`. It removes the dense interior of
435
+ * flat/near-coplanar regions and collinear runs of boundary vertices, and stops once the
436
+ * cheapest remaining collapse would exceed the tolerance.
437
+ *
438
+ * Intended for navmesh post-processing, where the input is a walkable surface whose outline and
439
+ * holes (obstacle footprints) carry meaning and must not drift, but whose interior inherits far
440
+ * more triangles than navigation needs.
441
+ *
442
+ * @param {BinaryTopology} mesh Mesh to clean up in-place.
443
+ * @param {number} max_error Maximum world-space deviation allowed for any removed vertex and for
444
+ * the boundary polyline. A no-op when <= 0.
445
+ */
446
+ export function bt_mesh_simplify_by_error(mesh, max_error) {
447
+ assert.defined(mesh, 'mesh');
448
+ assert.notNull(mesh, 'mesh');
449
+ assert.equal(mesh.isBinaryTopology, true, 'mesh.isBinaryTopology !== true');
450
+ assert.isNumber(max_error, 'max_error');
451
+
452
+ if (!(max_error > 0)) {
453
+ return;
454
+ }
455
+
456
+ const eps = max_error;
457
+ const eps_sq = eps * eps;
458
+
459
+ const edge_pool = mesh.edges;
460
+ const edge_count = edge_pool.size;
461
+
462
+ if (edge_count === 0) {
463
+ return;
464
+ }
465
+
466
+ const quadratics = bt_mesh_compute_vertex_quadratics(mesh);
467
+
468
+ // O(1) degree/boundary caches for choose_collapse, seeded once and kept current per collapse.
469
+ const vertex_count = mesh.vertices.size;
470
+ _degree = new Int32Array(vertex_count);
471
+ _vboundary = new Uint8Array(vertex_count);
472
+ for (let v = 0; v < vertex_count; v++) {
473
+ if (mesh.vertices.is_allocated(v)) {
474
+ recompute_vertex_cache(mesh, v);
475
+ }
476
+ }
477
+
478
+ // Indexed min priority queue keyed by edge id: re-costs are in-place insert_or_update / remove
479
+ // (O(1) expected via its internal id->handle map), so there are no duplicate or stale entries to
480
+ // track -- the queue always holds exactly the currently-collapsible edges. Edges are only ever
481
+ // killed, never created, so ids stay < edge_count for the whole run.
482
+ const heap = new FibonacciHeap(edge_count);
483
+
484
+ for (let e = 0; e < edge_count; e++) {
485
+ if (!edge_pool.is_allocated(e)) {
486
+ continue;
487
+ }
488
+
489
+ const cost = choose_collapse(mesh, e, quadratics, eps);
490
+
491
+ if (cost <= eps_sq) {
492
+ heap.insert(e, cost);
493
+ }
494
+ }
495
+
496
+ // Safety bound against an unforeseen non-terminating re-queue cycle. Every accepted collapse
497
+ // removes >= 1 face; collapsed-edge and over-tolerance/fold pops also burn iterations.
498
+ let guard = (edge_count + heap.size) * 8 + 16;
499
+
500
+ while (!heap.is_empty()) {
501
+ if (--guard < 0) {
502
+ break;
503
+ }
504
+
505
+ const e = heap.pop_min();
506
+
507
+ if (!edge_pool.is_allocated(e)) {
508
+ // edge already collapsed away (its queue entry was never removed; drop it now)
509
+ continue;
510
+ }
511
+
512
+ // Re-validate against the live mesh: a 2-ring change may have raised this edge's cost
513
+ // without re-queueing it (requeue only touches the 1-ring). This also fills in the chosen
514
+ // collapse direction (_choice_survivor / _choice_victim).
515
+ const cost = choose_collapse(mesh, e, quadratics, eps);
516
+
517
+ if (cost > eps_sq) {
518
+ // no longer within tolerance; a neighbouring collapse may re-queue it later
519
+ continue;
520
+ }
521
+
522
+ const survivor = _choice_survivor;
523
+ const victim = _choice_victim;
524
+
525
+ if (survivor === NULL_POINTER || victim === NULL_POINTER) {
526
+ continue;
527
+ }
528
+
529
+ if (collapse_would_fold(mesh, victim, survivor)) {
530
+ // a neighbouring collapse may re-queue it later
531
+ continue;
532
+ }
533
+
534
+ // bt_edge_collapse keeps v1; make the survivor v1 so it is the one that stays.
535
+ if (mesh.edge_read_vertex1(e) !== survivor) {
536
+ bt_edge_swap_vertex_slots(mesh, e);
537
+ }
538
+
539
+ // The survivor keeps its exact coordinate -- nothing in the mesh moves.
540
+ mesh.vertex_read_coordinate(_pos_target, 0, survivor);
541
+
542
+ // Fold the victim's accumulated planes into the survivor before the topology changes,
543
+ // so the survivor's quadric keeps measuring deviation from the original surface.
544
+ quadratics[survivor].add(quadratics[victim]);
545
+ quadratics[victim] = null;
546
+
547
+ bt_edge_kill_parallels(mesh, e);
548
+ const s = bt_edge_collapse(mesh, e, _pos_target, 0);
549
+ bt_vert_fuse_duplicate_edges(mesh, s);
550
+
551
+ requeue_ring(mesh, s, quadratics, eps, eps_sq, heap);
552
+ }
553
+
554
+ bt_mesh_cleanup_faceless_references(mesh);
555
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Swap the v1/v2 slots of an edge. Disk cycle pointers for the two slots are swapped
3
+ * alongside so that the disk cycles on both attached vertices remain valid.
4
+ *
5
+ * Useful before a half-edge collapse (e.g. {@link bt_edge_collapse}, which keeps v1 as
6
+ * the survivor) when the caller needs a specific endpoint to survive: swap first so the
7
+ * desired survivor sits in the v1 slot.
8
+ *
9
+ * @param {BinaryTopology} mesh
10
+ * @param {number} edge_id
11
+ */
12
+ export function bt_edge_swap_vertex_slots(mesh: BinaryTopology, edge_id: number): void;
13
+ //# sourceMappingURL=bt_edge_swap_vertex_slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bt_edge_swap_vertex_slots.d.ts","sourceRoot":"","sources":["../../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_swap_vertex_slots.js"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,yEAFW,MAAM,QAkBhB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Swap the v1/v2 slots of an edge. Disk cycle pointers for the two slots are swapped
3
+ * alongside so that the disk cycles on both attached vertices remain valid.
4
+ *
5
+ * Useful before a half-edge collapse (e.g. {@link bt_edge_collapse}, which keeps v1 as
6
+ * the survivor) when the caller needs a specific endpoint to survive: swap first so the
7
+ * desired survivor sits in the v1 slot.
8
+ *
9
+ * @param {BinaryTopology} mesh
10
+ * @param {number} edge_id
11
+ */
12
+ export function bt_edge_swap_vertex_slots(mesh, edge_id) {
13
+ const v1 = mesh.edge_read_vertex1(edge_id);
14
+ const v2 = mesh.edge_read_vertex2(edge_id);
15
+
16
+ const v1_next = mesh.edge_read_v1_disk_next(edge_id);
17
+ const v1_prev = mesh.edge_read_v1_disk_prev(edge_id);
18
+ const v2_next = mesh.edge_read_v2_disk_next(edge_id);
19
+ const v2_prev = mesh.edge_read_v2_disk_prev(edge_id);
20
+
21
+ mesh.edge_write_vertex1(edge_id, v2);
22
+ mesh.edge_write_vertex2(edge_id, v1);
23
+
24
+ mesh.edge_write_v1_disk_next(edge_id, v2_next);
25
+ mesh.edge_write_v1_disk_prev(edge_id, v2_prev);
26
+ mesh.edge_write_v2_disk_next(edge_id, v1_next);
27
+ mesh.edge_write_v2_disk_prev(edge_id, v1_prev);
28
+ }
@@ -68,5 +68,5 @@ export class BisectionScratch {
68
68
  */
69
69
  release_vertex_partition_slot(): void;
70
70
  }
71
- import { IndexedFloatMaxHeap } from "../refine/IndexedFloatMaxHeap.js";
71
+ import { IndexedFloatMaxHeap } from "../../../../collection/heap/IndexedFloatMaxHeap.js";
72
72
  //# sourceMappingURL=BisectionScratch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BisectionScratch.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/bisection/BisectionScratch.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH;IACI;;OAEG;IACH,8BAFW,MAAM,EA8BhB;IA3BG,yBAAwC;IAIxC,+BAA0D;IAC1D,wCAAmE;IACnE,6BAAwD;IACxD,6BAAwD;IACxD,+BAA2C;IAC3C,qCAAgE;IAIhE,gCAGC;IACD,gCAA2D;IAE3D,6BAAwD;IACxD,gCAA2D;IAI3D,4BAA4B;IAC5B,uBADW,WAAW,EAAE,CACO;IAC/B,kCAAkC;IAGtC;;;;;;;;;;OAUG;IACH,yCAHW,MAAM,GACJ,WAAW,CAavB;IAED;;;OAGG;IACH,sCAEC;CACJ;oCApGmC,kCAAkC"}
1
+ {"version":3,"file":"BisectionScratch.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/bisection/BisectionScratch.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH;IACI;;OAEG;IACH,8BAFW,MAAM,EA8BhB;IA3BG,yBAAwC;IAIxC,+BAA0D;IAC1D,wCAAmE;IACnE,6BAAwD;IACxD,6BAAwD;IACxD,+BAA2C;IAC3C,qCAAgE;IAIhE,gCAGC;IACD,gCAA2D;IAE3D,6BAAwD;IACxD,gCAA2D;IAI3D,4BAA4B;IAC5B,uBADW,WAAW,EAAE,CACO;IAC/B,kCAAkC;IAGtC;;;;;;;;;;OAUG;IACH,yCAHW,MAAM,GACJ,WAAW,CAavB;IAED;;;OAGG;IACH,sCAEC;CACJ;oCApGmC,oDAAoD"}
@@ -1,4 +1,4 @@
1
- import { IndexedFloatMaxHeap } from "../refine/IndexedFloatMaxHeap.js";
1
+ import { IndexedFloatMaxHeap } from "../../../../collection/heap/IndexedFloatMaxHeap.js";
2
2
 
3
3
  /**
4
4
  * Pre-allocated scratch reused by every recursive `bisect_graph` call during
@@ -41,5 +41,5 @@ export class RefinementScratch {
41
41
  partition_weight_min: Uint32Array;
42
42
  partition_weight_max: Uint32Array;
43
43
  }
44
- import { IndexedFloatMaxHeap } from "./IndexedFloatMaxHeap.js";
44
+ import { IndexedFloatMaxHeap } from "../../../../collection/heap/IndexedFloatMaxHeap.js";
45
45
  //# sourceMappingURL=RefinementScratch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RefinementScratch.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/refine/RefinementScratch.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;IACI;;;OAGG;IACH,8BAHW,MAAM,mBACN,MAAM,EAkBhB;IAfG,yBAAwC;IACxC,wBAAsC;IAEtC,gCAGC;IAED,yBAAoD;IACpD,8BAAyD;IACzD,uCAAkE;IAGlE,kCAA4D;IAC5D,kCAA4D;CAEnE;oCApDmC,0BAA0B"}
1
+ {"version":3,"file":"RefinementScratch.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/refine/RefinementScratch.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;IACI;;;OAGG;IACH,8BAHW,MAAM,mBACN,MAAM,EAkBhB;IAfG,yBAAwC;IACxC,wBAAsC;IAEtC,gCAGC;IAED,yBAAoD;IACpD,8BAAyD;IACzD,uCAAkE;IAGlE,kCAA4D;IAC5D,kCAA4D;CAEnE;oCApDmC,oDAAoD"}
@@ -1,4 +1,4 @@
1
- import { IndexedFloatMaxHeap } from "./IndexedFloatMaxHeap.js";
1
+ import { IndexedFloatMaxHeap } from "../../../../collection/heap/IndexedFloatMaxHeap.js";
2
2
 
3
3
  /**
4
4
  * Pre-allocated scratch reused across every `fm_kway` call during the
@@ -1 +1 @@
1
- {"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAmMxB;+BAlP8B,mEAAmE"}
1
+ {"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AAuDA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAmNxB;+BA3Q8B,mEAAmE"}
@@ -41,8 +41,17 @@ import {
41
41
  import {
42
42
  bt_mesh_fill_small_holes
43
43
  } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js";
44
+ import {
45
+ bt_mesh_simplify_by_error
46
+ } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js";
44
47
  import { clip_soup_against_overhangs } from "./clip_soup_against_overhangs.js";
45
48
 
49
+ // Fraction of the agent radius used as the navmesh cleanup error budget (see the cleanup pass in
50
+ // navmesh_build_topology). A larger agent cannot resolve fine surface detail and the walkable
51
+ // surface was already inset by agent_radius during erosion, so a small fraction of it is a safe,
52
+ // navigation-imperceptible bound on how far cleanup may move the surface or the walkable outline.
53
+ const NAVMESH_SIMPLIFY_ERROR_FRACTION = 0.05;
54
+
46
55
 
47
56
  /**
48
57
  * Build from given scene geometry
@@ -98,6 +107,11 @@ export function navmesh_build_topology({
98
107
  let raw_triangles = [];
99
108
  let triangle_count = 0;
100
109
 
110
+ // NOTE: the inherited source tessellation is cleaned up at the end of this function by
111
+ // bt_mesh_simplify_by_error (interior coplanar fans + collinear boundary runs collapsed). A
112
+ // further win -- re-triangulating large flat regions to a minimal polygon set -- is left as a
113
+ // possible follow-up.
114
+
101
115
  for (let face_id = 0; face_id < source_face_count; face_id++) {
102
116
 
103
117
  source.face_read_normal(scratch_normal, 0, face_id);
@@ -234,6 +248,17 @@ export function navmesh_build_topology({
234
248
  }
235
249
  }
236
250
 
251
+ // --- Clean up the dense triangulation inherited from the source render geometry ---
252
+ // The navmesh arrives with far more triangles than navigation needs (a flat floor comes in
253
+ // as many coplanar triangles). Collapse only the geometrically redundant ones -- interior
254
+ // near-coplanar fans and collinear runs of boundary vertices -- without moving the surface
255
+ // or the walkable outline (and obstacle holes) by more than a small fraction of the agent
256
+ // radius. With agent_radius 0 the tolerance is 0 and this is a no-op.
257
+ if (agent_radius > 0) {
258
+ bt_mesh_simplify_by_error(mesh, agent_radius * NAVMESH_SIMPLIFY_ERROR_FRACTION);
259
+ bt_mesh_compact(mesh);
260
+ }
261
+
237
262
  // face normals are consumed by navigation queries (string-pulling portal normals), populate them now
238
263
  bt_mesh_compute_face_normals(mesh);
239
264
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"IndexedFloatMaxHeap.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/graph/metis/native/refine/IndexedFloatMaxHeap.js"],"names":[],"mappings":"AA6BA;IACI;;;OAGG;IACH,yBAHW,MAAM,0BACN,MAAM,EAsBhB;IAbG,wBAA4C;IAC5C,eAAe;IAEf,2BAA4E;IAC5E,2BAAwD;IACxD,6BAA0D;IAE1D;;;OAGG;IACH,cAFU,WAAW,CAE2B;IAIpD,mBAEC;IAED;;;OAGG;IACH,cAQC;IAED,2BAEC;IAED;;;OAGG;IACH,qBAkBC;IAED;;;;OAIG;IACH,oBAsBC;IAED;;;;OAIG;IACH,kBAYC;IAED,wBAUC;IAED;;;OAGG;IACH,WAHW,MAAM,SACN,MAAM,QAehB;IAED;;OAEG;IACH,WAFa,MAAM,CAuBlB;IAED;;;OAGG;IACH,WAHW,MAAM,aACN,MAAM,QAehB;IAED;;;OAGG;IACH,WAHW,MAAM,GACJ,OAAO,CA+BnB;CACJ"}