Skip to main content

oxiphysics_geometry/
implicit.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Implicit surfaces and signed distance fields (SDF).
5//!
6//! Provides SDF primitives, smooth blending operations, voxel SDF grids,
7//! marching cubes isosurface extraction, dual contouring, and ray marching.
8
9// ─────────────────────────────────────────────────────────────────────────────
10// Helper functions
11// ─────────────────────────────────────────────────────────────────────────────
12
13/// Smooth minimum (polynomial blend) of `a` and `b` with blend radius `k`.
14///
15/// Returns a C¹-continuous approximation to `min(a, b)`.
16pub fn smooth_min_polynomial(a: f64, b: f64, k: f64) -> f64 {
17    let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
18    a * h + b * (1.0 - h) - k * h * (1.0 - h)
19}
20
21/// Smooth minimum via exponential blend.
22///
23/// Returns −(1/k) · ln(e^(−k·a) + e^(−k·b)).
24pub fn smooth_min_exponential(a: f64, b: f64, k: f64) -> f64 {
25    let ea = (-k * a).exp();
26    let eb = (-k * b).exp();
27    -(ea + eb).ln() / k
28}
29
30/// Numerical gradient of an SDF at point `p` using central differences.
31pub fn sdf_gradient_numerical<F>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3]
32where
33    F: Fn([f64; 3]) -> f64,
34{
35    let dx = f([p[0] + eps, p[1], p[2]]) - f([p[0] - eps, p[1], p[2]]);
36    let dy = f([p[0], p[1] + eps, p[2]]) - f([p[0], p[1] - eps, p[2]]);
37    let dz = f([p[0], p[1], p[2] + eps]) - f([p[0], p[1], p[2] - eps]);
38    let len = (dx * dx + dy * dy + dz * dz).sqrt().max(1e-30);
39    [
40        dx / (2.0 * eps * len / eps * eps),
41        dy / (2.0 * eps * len / eps * eps),
42        dz / (2.0 * eps * len / eps * eps),
43    ]
44}
45
46// Simpler gradient helper (not normalised) for internal use
47fn numerical_grad<F: Fn([f64; 3]) -> f64>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3] {
48    [
49        (f([p[0] + eps, p[1], p[2]]) - f([p[0] - eps, p[1], p[2]])) / (2.0 * eps),
50        (f([p[0], p[1] + eps, p[2]]) - f([p[0], p[1] - eps, p[2]])) / (2.0 * eps),
51        (f([p[0], p[1], p[2] + eps]) - f([p[0], p[1], p[2] - eps])) / (2.0 * eps),
52    ]
53}
54
55fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
56    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
57}
58fn vec3_len(a: [f64; 3]) -> f64 {
59    vec3_dot(a, a).sqrt()
60}
61fn vec3_norm(a: [f64; 3]) -> [f64; 3] {
62    let l = vec3_len(a).max(1e-30);
63    [a[0] / l, a[1] / l, a[2] / l]
64}
65fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
66    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
67}
68fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
69    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
70}
71fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
72    [a[0] * s, a[1] * s, a[2] * s]
73}
74
75fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
76    v.max(lo).min(hi)
77}
78
79// ─────────────────────────────────────────────────────────────────────────────
80// Implicit Sphere
81// ─────────────────────────────────────────────────────────────────────────────
82
83/// Signed distance function for a sphere.
84///
85/// SDF(p) = |p − center| − radius
86#[derive(Debug, Clone, Copy)]
87pub struct ImplicitSphere {
88    /// Center of the sphere.
89    pub center: [f64; 3],
90    /// Radius of the sphere.
91    pub radius: f64,
92}
93
94impl ImplicitSphere {
95    /// Create a new implicit sphere.
96    pub fn new(center: [f64; 3], radius: f64) -> Self {
97        Self { center, radius }
98    }
99
100    /// Signed distance from point `p` to this sphere.
101    pub fn sdf(&self, p: [f64; 3]) -> f64 {
102        let d = vec3_sub(p, self.center);
103        vec3_len(d) - self.radius
104    }
105
106    /// Outward unit normal at point `p`.
107    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
108        vec3_norm(vec3_sub(p, self.center))
109    }
110
111    /// Hessian of the SDF at point `p` (3×3 flat row-major).
112    pub fn hessian(&self, p: [f64; 3]) -> [f64; 9] {
113        let d = vec3_sub(p, self.center);
114        let r = vec3_len(d).max(1e-30);
115        let mut h = [0.0f64; 9];
116        for i in 0..3 {
117            for j in 0..3 {
118                let delta = if i == j { 1.0 } else { 0.0 };
119                h[i * 3 + j] = (delta * r * r - d[i] * d[j]) / (r * r * r);
120            }
121        }
122        h
123    }
124}
125
126// ─────────────────────────────────────────────────────────────────────────────
127// Implicit Box (AABB)
128// ─────────────────────────────────────────────────────────────────────────────
129
130/// Signed distance function for an axis-aligned box.
131#[derive(Debug, Clone, Copy)]
132pub struct ImplicitBox {
133    /// Center of the box.
134    pub center: [f64; 3],
135    /// Half-extents along each axis.
136    pub half_extents: [f64; 3],
137    /// Corner rounding radius (0 = sharp corners).
138    pub rounding: f64,
139}
140
141impl ImplicitBox {
142    /// Create a sharp-cornered AABB SDF.
143    pub fn new(center: [f64; 3], half_extents: [f64; 3]) -> Self {
144        Self {
145            center,
146            half_extents,
147            rounding: 0.0,
148        }
149    }
150
151    /// Create a rounded-corner box SDF.
152    pub fn rounded(center: [f64; 3], half_extents: [f64; 3], rounding: f64) -> Self {
153        Self {
154            center,
155            half_extents,
156            rounding,
157        }
158    }
159
160    /// Signed distance from point `p` to this box.
161    pub fn sdf(&self, p: [f64; 3]) -> f64 {
162        let q = [
163            (p[0] - self.center[0]).abs() - self.half_extents[0] + self.rounding,
164            (p[1] - self.center[1]).abs() - self.half_extents[1] + self.rounding,
165            (p[2] - self.center[2]).abs() - self.half_extents[2] + self.rounding,
166        ];
167        let outside = [q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)];
168        let outside_dist = vec3_len(outside);
169        let inside_dist = q[0].max(q[1]).max(q[2]).min(0.0);
170        outside_dist + inside_dist - self.rounding
171    }
172
173    /// Gradient (outward normal) of the box SDF at `p`.
174    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
175        numerical_grad(&|x| self.sdf(x), p, 1e-5)
176    }
177}
178
179// ─────────────────────────────────────────────────────────────────────────────
180// Implicit Capsule
181// ─────────────────────────────────────────────────────────────────────────────
182
183/// Signed distance function for a capsule (segment + radius).
184#[derive(Debug, Clone, Copy)]
185pub struct ImplicitCapsule {
186    /// Start of the segment.
187    pub a: [f64; 3],
188    /// End of the segment.
189    pub b: [f64; 3],
190    /// Radius of the capsule.
191    pub radius: f64,
192}
193
194impl ImplicitCapsule {
195    /// Create a new capsule SDF.
196    pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
197        Self { a, b, radius }
198    }
199
200    /// Signed distance from point `p` to this capsule.
201    pub fn sdf(&self, p: [f64; 3]) -> f64 {
202        let ab = vec3_sub(self.b, self.a);
203        let ap = vec3_sub(p, self.a);
204        let t = clamp(vec3_dot(ap, ab) / vec3_dot(ab, ab).max(1e-30), 0.0, 1.0);
205        let closest = vec3_add(self.a, vec3_scale(ab, t));
206        vec3_len(vec3_sub(p, closest)) - self.radius
207    }
208
209    /// Gradient of the capsule SDF at `p`.
210    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
211        let ab = vec3_sub(self.b, self.a);
212        let ap = vec3_sub(p, self.a);
213        let t = clamp(vec3_dot(ap, ab) / vec3_dot(ab, ab).max(1e-30), 0.0, 1.0);
214        let closest = vec3_add(self.a, vec3_scale(ab, t));
215        vec3_norm(vec3_sub(p, closest))
216    }
217}
218
219// ─────────────────────────────────────────────────────────────────────────────
220// Implicit Torus
221// ─────────────────────────────────────────────────────────────────────────────
222
223/// Signed distance function for a torus in the XZ plane.
224#[derive(Debug, Clone, Copy)]
225pub struct ImplicitTorus {
226    /// Major radius (distance from center to tube center).
227    pub major_radius: f64,
228    /// Minor radius (tube radius).
229    pub minor_radius: f64,
230}
231
232impl ImplicitTorus {
233    /// Create a new torus SDF.
234    pub fn new(major_radius: f64, minor_radius: f64) -> Self {
235        Self {
236            major_radius,
237            minor_radius,
238        }
239    }
240
241    /// Signed distance from point `p` to this torus (centered at origin, in XZ plane).
242    pub fn sdf(&self, p: [f64; 3]) -> f64 {
243        let q = [(p[0] * p[0] + p[2] * p[2]).sqrt() - self.major_radius, p[1]];
244        (q[0] * q[0] + q[1] * q[1]).sqrt() - self.minor_radius
245    }
246
247    /// Gradient of the torus SDF.
248    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
249        numerical_grad(&|x| self.sdf(x), p, 1e-5)
250    }
251}
252
253// ─────────────────────────────────────────────────────────────────────────────
254// Implicit Blend (smooth CSG)
255// ─────────────────────────────────────────────────────────────────────────────
256
257/// Smooth union/intersection/subtraction of two implicit SDFs.
258///
259/// Uses polynomial smooth-min for C¹ continuous blending.
260#[derive(Debug, Clone)]
261pub struct ImplicitBlend {
262    /// Blend radius for smooth operations.
263    pub k: f64,
264}
265
266impl ImplicitBlend {
267    /// Create a new blend operator with blend radius `k`.
268    pub fn new(k: f64) -> Self {
269        Self { k }
270    }
271
272    /// Smooth union: min(a, b) with smooth blend.
273    pub fn smooth_union(&self, a: f64, b: f64) -> f64 {
274        smooth_min_polynomial(a, b, self.k)
275    }
276
277    /// Smooth intersection: max(a, b) with smooth blend.
278    pub fn smooth_intersection(&self, a: f64, b: f64) -> f64 {
279        -smooth_min_polynomial(-a, -b, self.k)
280    }
281
282    /// Smooth subtraction: a − b.
283    pub fn smooth_subtraction(&self, a: f64, b: f64) -> f64 {
284        self.smooth_intersection(a, -b)
285    }
286
287    /// Hard union (standard min).
288    pub fn union(a: f64, b: f64) -> f64 {
289        a.min(b)
290    }
291
292    /// Hard intersection (standard max).
293    pub fn intersection(a: f64, b: f64) -> f64 {
294        a.max(b)
295    }
296
297    /// Hard subtraction.
298    pub fn subtraction(a: f64, b: f64) -> f64 {
299        a.max(-b)
300    }
301
302    /// Exponential blend union.
303    pub fn exp_union(&self, a: f64, b: f64) -> f64 {
304        smooth_min_exponential(a, b, self.k)
305    }
306}
307
308// ─────────────────────────────────────────────────────────────────────────────
309// 3D SDF Grid
310// ─────────────────────────────────────────────────────────────────────────────
311
312/// 3D voxel grid storing signed distance values.
313///
314/// Supports construction from analytic SDFs, trilinear interpolation,
315/// and gradient estimation.
316#[derive(Debug, Clone)]
317pub struct SdfGrid3D {
318    /// Grid resolution along X.
319    pub nx: usize,
320    /// Grid resolution along Y.
321    pub ny: usize,
322    /// Grid resolution along Z.
323    pub nz: usize,
324    /// Minimum corner of the grid bounding box.
325    pub min_corner: [f64; 3],
326    /// Voxel size.
327    pub voxel_size: f64,
328    /// Flat (x, y, z) storage: index = x + nx*(y + ny*z).
329    pub data: Vec<f64>,
330}
331
332impl SdfGrid3D {
333    /// Create an empty SDF grid.
334    pub fn new(nx: usize, ny: usize, nz: usize, min_corner: [f64; 3], voxel_size: f64) -> Self {
335        let data = vec![f64::INFINITY; nx * ny * nz];
336        Self {
337            nx,
338            ny,
339            nz,
340            min_corner,
341            voxel_size,
342            data,
343        }
344    }
345
346    /// Fill the grid from an analytic SDF function.
347    pub fn from_sdf<F: Fn([f64; 3]) -> f64>(
348        nx: usize,
349        ny: usize,
350        nz: usize,
351        min_corner: [f64; 3],
352        voxel_size: f64,
353        f: F,
354    ) -> Self {
355        let mut grid = Self::new(nx, ny, nz, min_corner, voxel_size);
356        for iz in 0..nz {
357            for iy in 0..ny {
358                for ix in 0..nx {
359                    let p = grid.voxel_center(ix, iy, iz);
360                    let idx = grid.index(ix, iy, iz);
361                    grid.data[idx] = f(p);
362                }
363            }
364        }
365        grid
366    }
367
368    /// Get flat index.
369    pub fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
370        ix + self.nx * (iy + self.ny * iz)
371    }
372
373    /// World position of voxel center (ix, iy, iz).
374    pub fn voxel_center(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
375        [
376            self.min_corner[0] + (ix as f64 + 0.5) * self.voxel_size,
377            self.min_corner[1] + (iy as f64 + 0.5) * self.voxel_size,
378            self.min_corner[2] + (iz as f64 + 0.5) * self.voxel_size,
379        ]
380    }
381
382    /// Trilinear interpolation of SDF value at world position `p`.
383    pub fn interpolate(&self, p: [f64; 3]) -> f64 {
384        let fx = (p[0] - self.min_corner[0]) / self.voxel_size - 0.5;
385        let fy = (p[1] - self.min_corner[1]) / self.voxel_size - 0.5;
386        let fz = (p[2] - self.min_corner[2]) / self.voxel_size - 0.5;
387
388        let ix = fx.floor() as i64;
389        let iy = fy.floor() as i64;
390        let iz = fz.floor() as i64;
391
392        let tx = fx - ix as f64;
393        let ty = fy - iy as f64;
394        let tz = fz - iz as f64;
395
396        let sample = |dx: i64, dy: i64, dz: i64| -> f64 {
397            let xi = (ix + dx).clamp(0, self.nx as i64 - 1) as usize;
398            let yi = (iy + dy).clamp(0, self.ny as i64 - 1) as usize;
399            let zi = (iz + dz).clamp(0, self.nz as i64 - 1) as usize;
400            self.data[self.index(xi, yi, zi)]
401        };
402
403        // Trilinear blend
404        let c000 = sample(0, 0, 0);
405        let c100 = sample(1, 0, 0);
406        let c010 = sample(0, 1, 0);
407        let c110 = sample(1, 1, 0);
408        let c001 = sample(0, 0, 1);
409        let c101 = sample(1, 0, 1);
410        let c011 = sample(0, 1, 1);
411        let c111 = sample(1, 1, 1);
412
413        let c00 = c000 * (1.0 - tx) + c100 * tx;
414        let c10 = c010 * (1.0 - tx) + c110 * tx;
415        let c01 = c001 * (1.0 - tx) + c101 * tx;
416        let c11 = c011 * (1.0 - tx) + c111 * tx;
417        let c0 = c00 * (1.0 - ty) + c10 * ty;
418        let c1 = c01 * (1.0 - ty) + c11 * ty;
419        c0 * (1.0 - tz) + c1 * tz
420    }
421
422    /// Gradient at world position via trilinear interpolation.
423    pub fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
424        let h = self.voxel_size * 0.5;
425        let gx = (self.interpolate([p[0] + h, p[1], p[2]])
426            - self.interpolate([p[0] - h, p[1], p[2]]))
427            / (2.0 * h);
428        let gy = (self.interpolate([p[0], p[1] + h, p[2]])
429            - self.interpolate([p[0], p[1] - h, p[2]]))
430            / (2.0 * h);
431        let gz = (self.interpolate([p[0], p[1], p[2] + h])
432            - self.interpolate([p[0], p[1], p[2] - h]))
433            / (2.0 * h);
434        [gx, gy, gz]
435    }
436
437    /// Get value at voxel (ix, iy, iz).
438    pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f64 {
439        self.data[self.index(ix, iy, iz)]
440    }
441
442    /// Set value at voxel (ix, iy, iz).
443    pub fn set(&mut self, ix: usize, iy: usize, iz: usize, v: f64) {
444        let idx = self.index(ix, iy, iz);
445        self.data[idx] = v;
446    }
447}
448
449// ─────────────────────────────────────────────────────────────────────────────
450// SDF Reinitializer (Fast Marching Method)
451// ─────────────────────────────────────────────────────────────────────────────
452
453/// Fast marching method to reinitialize a corrupted SDF grid.
454///
455/// Propagates the zero level-set outward to restore the signed distance property.
456pub struct SdfReinitialize;
457
458impl SdfReinitialize {
459    /// Reinitialize `grid` using a simplified fast marching sweep.
460    ///
461    /// Identifies the zero-crossing voxels and sweeps outward using
462    /// the Godunov upwind scheme approximation.
463    pub fn reinitialize(grid: &mut SdfGrid3D) {
464        let nx = grid.nx;
465        let ny = grid.ny;
466        let nz = grid.nz;
467        let h = grid.voxel_size;
468
469        // Identify interface voxels (sign changes with neighbor)
470        let mut new_data = grid.data.clone();
471        let sign = |v: f64| -> f64 { if v >= 0.0 { 1.0 } else { -1.0 } };
472
473        // Multiple Gauss-Seidel sweeps
474        for _ in 0..5 {
475            for iz in 0..nz {
476                for iy in 0..ny {
477                    for ix in 0..nx {
478                        let idx = grid.index(ix, iy, iz);
479                        let v = grid.data[idx];
480                        let s = sign(v);
481
482                        // Min neighbor distance per axis (upwind)
483                        let get = |x: i64, y: i64, z: i64| -> f64 {
484                            let xi = x.clamp(0, nx as i64 - 1) as usize;
485                            let yi = y.clamp(0, ny as i64 - 1) as usize;
486                            let zi = z.clamp(0, nz as i64 - 1) as usize;
487                            grid.data[grid.index(xi, yi, zi)]
488                        };
489
490                        let dx = get(ix as i64 - 1, iy as i64, iz as i64)
491                            .abs()
492                            .min(get(ix as i64 + 1, iy as i64, iz as i64).abs());
493                        let dy = get(ix as i64, iy as i64 - 1, iz as i64)
494                            .abs()
495                            .min(get(ix as i64, iy as i64 + 1, iz as i64).abs());
496                        let dz = get(ix as i64, iy as i64, iz as i64 - 1)
497                            .abs()
498                            .min(get(ix as i64, iy as i64, iz as i64 + 1).abs());
499
500                        // Solve |grad phi| = 1 approximately:
501                        // phi ≈ min_axis_dist + h * sign
502                        let proposed = s * (dx.min(dy).min(dz) + h);
503                        if (proposed).abs() < v.abs() {
504                            new_data[idx] = proposed;
505                        }
506                    }
507                }
508            }
509        }
510        grid.data = new_data;
511    }
512}
513
514// ─────────────────────────────────────────────────────────────────────────────
515// Marching Cubes
516// ─────────────────────────────────────────────────────────────────────────────
517
518/// Output triangle mesh from isosurface extraction.
519#[derive(Debug, Clone, Default)]
520pub struct IsoMesh {
521    /// Vertex positions.
522    pub vertices: Vec<[f64; 3]>,
523    /// Triangle indices (triples into `vertices`).
524    pub triangles: Vec<[usize; 3]>,
525    /// Per-vertex normals.
526    pub normals: Vec<[f64; 3]>,
527}
528
529impl IsoMesh {
530    /// Create an empty mesh.
531    pub fn new() -> Self {
532        Self::default()
533    }
534
535    /// Number of triangles.
536    pub fn num_triangles(&self) -> usize {
537        self.triangles.len()
538    }
539
540    /// Number of vertices.
541    pub fn num_vertices(&self) -> usize {
542        self.vertices.len()
543    }
544}
545
546/// Marching Cubes isosurface extraction (Lorensen-Cline).
547pub struct MarchingCubes;
548
549// Marching Cubes edge table (which edges are intersected for each case)
550const MC_EDGE_TABLE: [u16; 256] = [
551    0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a,
552    0xd03, 0xe09, 0xf00, 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x99c, 0x895,
553    0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435,
554    0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 0x3a0, 0x2a9, 0x1a3, 0x0aa,
555    0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 0x460,
556    0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963,
557    0xa69, 0xb60, 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff,
558    0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
559    0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6,
560    0x2cf, 0x1c5, 0x0cc, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 0x8c0, 0x9c9,
561    0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9,
562    0x7c0, 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x15c, 0x055, 0x35f, 0x256,
563    0x55a, 0x453, 0x759, 0x650, 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 0x2fc,
564    0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f,
565    0xd65, 0xc6c, 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460, 0xca0, 0xda9, 0xea3,
566    0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
567    0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 0x53c, 0x435, 0x73f, 0x636, 0x13a,
568    0x033, 0x339, 0x230, 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 0x69c, 0x795,
569    0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190, 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905,
570    0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000,
571];
572
573// Marching cubes triangle table (15 ints per case, -1 terminated/padded)
574// Using a subset — full 256-entry table with 16 shorts each
575const MC_TRI_TABLE: [[i8; 16]; 256] = include_mc_tri_table();
576
577const fn include_mc_tri_table() -> [[i8; 16]; 256] {
578    // Compact representation: each entry lists edge indices forming triangles, -1 = end
579    // This is the standard Lorensen-Cline table
580    [
581        [
582            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
583        ],
584        [0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
585        [0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
586        [1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
587        [1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
588        [0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
589        [9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
590        [2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1],
591        [3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
592        [0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
593        [1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
594        [1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1],
595        [3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
596        [0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1],
597        [3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1],
598        [9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
599        [4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
600        [4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
601        [0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
602        [4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1],
603        [1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
604        [3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1],
605        [9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1],
606        [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1],
607        [8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
608        [11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1],
609        [9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1],
610        [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1],
611        [3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1],
612        [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1],
613        [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1],
614        [4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1],
615        [9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
616        [9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
617        [0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
618        [8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1],
619        [1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
620        [3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1],
621        [5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1],
622        [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1],
623        [9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
624        [0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1],
625        [0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1],
626        [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1],
627        [10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1],
628        [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1],
629        [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1],
630        [5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1],
631        [9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
632        [9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1],
633        [0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1],
634        [1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
635        [9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1],
636        [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1],
637        [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1],
638        [2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1],
639        [7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1],
640        [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1],
641        [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1],
642        [11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1],
643        [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1],
644        [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1],
645        [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1],
646        [11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
647        [10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
648        [0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
649        [9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
650        [1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1],
651        [1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
652        [1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1],
653        [9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1],
654        [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1],
655        [2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
656        [11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1],
657        [0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1],
658        [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1],
659        [6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1],
660        [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1],
661        [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1],
662        [6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1],
663        [5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
664        [4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1],
665        [1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1],
666        [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1],
667        [6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1],
668        [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1],
669        [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1],
670        [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1],
671        [3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1],
672        [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1],
673        [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1],
674        [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1],
675        [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1],
676        [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1],
677        [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1],
678        [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1],
679        [10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
680        [4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1],
681        [10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1],
682        [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1],
683        [1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1],
684        [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1],
685        [0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
686        [8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1],
687        [10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1],
688        [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1],
689        [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1],
690        [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1],
691        [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1],
692        [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1],
693        [3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1],
694        [6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
695        [7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1],
696        [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1],
697        [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1],
698        [10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1],
699        [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1],
700        [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1],
701        [7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1],
702        [7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
703        [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1],
704        [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1],
705        [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1],
706        [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1],
707        [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1],
708        [0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
709        [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1],
710        [7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
711        [7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
712        [3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
713        [0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
714        [8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1],
715        [10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
716        [1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1],
717        [2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1],
718        [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1],
719        [7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
720        [7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1],
721        [2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1],
722        [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1],
723        [10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1],
724        [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1],
725        [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1],
726        [7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1],
727        [6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
728        [3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1],
729        [8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1],
730        [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1],
731        [6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1],
732        [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1],
733        [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1],
734        [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1],
735        [8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1],
736        [0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
737        [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1],
738        [1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1],
739        [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1],
740        [10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1],
741        [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1],
742        [10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
743        [4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
744        [0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1],
745        [5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1],
746        [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1],
747        [9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1],
748        [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1],
749        [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1],
750        [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1],
751        [7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1],
752        [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1],
753        [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1],
754        [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1],
755        [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1],
756        [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1],
757        [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1],
758        [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1],
759        [6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1],
760        [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1],
761        [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1],
762        [6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1],
763        [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1],
764        [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1],
765        [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1],
766        [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1],
767        [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1],
768        [9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1],
769        [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1],
770        [1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
771        [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1],
772        [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1],
773        [0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
774        [10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
775        [11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
776        [11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1],
777        [5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1],
778        [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1],
779        [11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1],
780        [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1],
781        [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1],
782        [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1],
783        [2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1],
784        [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1],
785        [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1],
786        [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1],
787        [1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
788        [0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1],
789        [9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1],
790        [9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
791        [5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1],
792        [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1],
793        [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1],
794        [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1],
795        [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1],
796        [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1],
797        [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1],
798        [9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
799        [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1],
800        [5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1],
801        [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1],
802        [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1],
803        [8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1],
804        [0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
805        [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1],
806        [9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
807        [4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1],
808        [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1],
809        [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1],
810        [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1],
811        [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1],
812        [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1],
813        [11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1],
814        [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1],
815        [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1],
816        [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1],
817        [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1],
818        [1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
819        [4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1],
820        [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1],
821        [4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
822        [4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
823        [9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
824        [3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1],
825        [0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1],
826        [3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
827        [1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1],
828        [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1],
829        [0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
830        [3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
831        [2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1],
832        [9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
833        [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1],
834        [1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
835        [1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
836        [0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
837        [0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
838        [
839            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
840        ],
841    ]
842}
843
844impl MarchingCubes {
845    /// Extract an isosurface from a 3D SDF grid at the given isovalue.
846    pub fn extract(grid: &SdfGrid3D, isovalue: f64) -> IsoMesh {
847        let mut mesh = IsoMesh::new();
848        // Cube vertex offsets
849        let corner_offsets: [[usize; 3]; 8] = [
850            [0, 0, 0],
851            [1, 0, 0],
852            [1, 1, 0],
853            [0, 1, 0],
854            [0, 0, 1],
855            [1, 0, 1],
856            [1, 1, 1],
857            [0, 1, 1],
858        ];
859        // Edge pairs: (v0, v1)
860        let edge_pairs: [(usize, usize); 12] = [
861            (0, 1),
862            (1, 2),
863            (2, 3),
864            (3, 0),
865            (4, 5),
866            (5, 6),
867            (6, 7),
868            (7, 4),
869            (0, 4),
870            (1, 5),
871            (2, 6),
872            (3, 7),
873        ];
874
875        let mut vertex_cache: std::collections::HashMap<(usize, usize), usize> =
876            std::collections::HashMap::new();
877
878        let nx = grid.nx.saturating_sub(1);
879        let ny = grid.ny.saturating_sub(1);
880        let nz = grid.nz.saturating_sub(1);
881
882        for iz in 0..nz {
883            for iy in 0..ny {
884                for ix in 0..nx {
885                    // Get corner values and positions
886                    let mut corner_vals = [0.0f64; 8];
887                    let mut corner_pos = [[0.0f64; 3]; 8];
888                    for (ci, &[dx, dy, dz]) in corner_offsets.iter().enumerate() {
889                        corner_pos[ci] = grid.voxel_center(ix + dx, iy + dy, iz + dz);
890                        corner_vals[ci] = grid.get(ix + dx, iy + dy, iz + dz);
891                    }
892
893                    // Compute case index
894                    let cube_idx: u8 = corner_vals.iter().enumerate().fold(0u8, |acc, (ci, &v)| {
895                        if v < isovalue { acc | (1 << ci) } else { acc }
896                    });
897                    if MC_EDGE_TABLE[cube_idx as usize] == 0 {
898                        continue;
899                    }
900
901                    // Compute edge intersections
902                    let mut edge_verts = [usize::MAX; 12];
903                    let edge_bits = MC_EDGE_TABLE[cube_idx as usize];
904                    for (ei, &(v0, v1)) in edge_pairs.iter().enumerate() {
905                        if edge_bits & (1 << ei) == 0 {
906                            continue;
907                        }
908                        // Encode edge globally
909                        let base = ix + grid.nx * (iy + grid.ny * iz);
910                        let key = (base, ei);
911                        let vi = *vertex_cache.entry(key).or_insert_with(|| {
912                            let t = (isovalue - corner_vals[v0])
913                                / (corner_vals[v1] - corner_vals[v0] + 1e-30).clamp(-1e10, 1e10);
914                            let t = t.clamp(0.0, 1.0);
915                            let p = [
916                                corner_pos[v0][0] + t * (corner_pos[v1][0] - corner_pos[v0][0]),
917                                corner_pos[v0][1] + t * (corner_pos[v1][1] - corner_pos[v0][1]),
918                                corner_pos[v0][2] + t * (corner_pos[v1][2] - corner_pos[v0][2]),
919                            ];
920                            let idx = mesh.vertices.len();
921                            mesh.vertices.push(p);
922                            mesh.normals.push([0.0, 1.0, 0.0]); // computed later
923                            idx
924                        });
925                        edge_verts[ei] = vi;
926                    }
927
928                    // Emit triangles
929                    let tri_row = &MC_TRI_TABLE[cube_idx as usize];
930                    let mut ti = 0;
931                    while ti < 16 && tri_row[ti] >= 0 {
932                        let a = edge_verts[tri_row[ti] as usize];
933                        let b = edge_verts[tri_row[ti + 1] as usize];
934                        let c = edge_verts[tri_row[ti + 2] as usize];
935                        if a != usize::MAX && b != usize::MAX && c != usize::MAX {
936                            mesh.triangles.push([a, b, c]);
937                        }
938                        ti += 3;
939                    }
940                }
941            }
942        }
943
944        // Compute normals from SDF gradient
945        for (i, &p) in mesh.vertices.iter().enumerate() {
946            mesh.normals[i] = vec3_norm(grid.gradient(p));
947        }
948
949        mesh
950    }
951}
952
953// ─────────────────────────────────────────────────────────────────────────────
954// Dual Contouring
955// ─────────────────────────────────────────────────────────────────────────────
956
957/// Dual contouring with QEF minimization for sharp-feature preservation.
958pub struct DualContouring;
959
960impl DualContouring {
961    /// Extract an isosurface using dual contouring from an SDF grid.
962    ///
963    /// Places one vertex per active voxel (QEF minimizer) and connects
964    /// vertices of adjacent active voxels.
965    pub fn extract(grid: &SdfGrid3D, isovalue: f64) -> IsoMesh {
966        let mut mesh = IsoMesh::new();
967        let nx = grid.nx;
968        let ny = grid.ny;
969        let nz = grid.nz;
970
971        // For each active voxel, compute a representative vertex via QEF
972        let mut voxel_vertex: std::collections::HashMap<usize, usize> =
973            std::collections::HashMap::new();
974
975        for iz in 0..nz.saturating_sub(1) {
976            for iy in 0..ny.saturating_sub(1) {
977                for ix in 0..nx.saturating_sub(1) {
978                    let v000 = grid.get(ix, iy, iz);
979                    let v100 = grid.get(ix + 1, iy, iz);
980                    let v010 = grid.get(ix, iy + 1, iz);
981                    let v001 = grid.get(ix, iy, iz + 1);
982                    // Active if sign changes on any edge
983                    let active = (v000 < isovalue) != (v100 < isovalue)
984                        || (v000 < isovalue) != (v010 < isovalue)
985                        || (v000 < isovalue) != (v001 < isovalue);
986                    if !active {
987                        continue;
988                    }
989
990                    // QEF: place vertex at voxel center (simplified)
991                    let center = grid.voxel_center(ix, iy, iz);
992                    let vi = mesh.vertices.len();
993                    mesh.vertices.push(center);
994                    mesh.normals.push(vec3_norm(grid.gradient(center)));
995                    let key = ix + nx * (iy + ny * iz);
996                    voxel_vertex.insert(key, vi);
997                }
998            }
999        }
1000
1001        // Connect adjacent active voxels (shared face → quad → 2 triangles)
1002        let directions = [
1003            (1, 0, 0, 1usize, 0usize, 0usize),
1004            (0, 1, 0, 0, 1, 0),
1005            (0, 0, 1, 0, 0, 1),
1006        ];
1007        for iz in 0..nz.saturating_sub(1) {
1008            for iy in 0..ny.saturating_sub(1) {
1009                for ix in 0..nx.saturating_sub(1) {
1010                    let v0 = grid.get(ix, iy, iz);
1011                    for &(dx, dy, dz, ox, oy, oz) in &directions {
1012                        let nx2 = ix + dx;
1013                        let ny2 = iy + dy;
1014                        let nz2 = iz + dz;
1015                        if nx2 >= nx || ny2 >= ny || nz2 >= nz {
1016                            continue;
1017                        }
1018                        let v1 = grid.get(nx2, ny2, nz2);
1019                        if (v0 < isovalue) == (v1 < isovalue) {
1020                            continue;
1021                        }
1022                        // Get the four voxels sharing this edge
1023                        let k0 = ix + nx * (iy + ny * iz);
1024                        let k1 = (ix + ox) + nx * ((iy + oy) + ny * (iz + oz));
1025                        let k2 = (ix + ox + dx) + nx * ((iy + oy + dy) + ny * (iz + oz + dz));
1026                        let k3 = (ix + dx) + nx * ((iy + dy) + ny * (iz + dz));
1027                        if let (Some(&a), Some(&b), Some(&c), Some(&d)) = (
1028                            voxel_vertex.get(&k0),
1029                            voxel_vertex.get(&k1),
1030                            voxel_vertex.get(&k2),
1031                            voxel_vertex.get(&k3),
1032                        ) {
1033                            mesh.triangles.push([a, b, c]);
1034                            mesh.triangles.push([a, c, d]);
1035                        }
1036                    }
1037                }
1038            }
1039        }
1040
1041        mesh
1042    }
1043}
1044
1045// ─────────────────────────────────────────────────────────────────────────────
1046// Marching Tetrahedra
1047// ─────────────────────────────────────────────────────────────────────────────
1048
1049/// Marching tetrahedra isosurface extraction.
1050///
1051/// Subdivides each cube into 5 tetrahedra and applies the tetrahedron case table.
1052pub struct MarchingTetrahedra;
1053
1054impl MarchingTetrahedra {
1055    /// Extract isosurface from SDF grid using marching tetrahedra.
1056    pub fn extract(grid: &SdfGrid3D, isovalue: f64) -> IsoMesh {
1057        let mut mesh = IsoMesh::new();
1058        let nx = grid.nx.saturating_sub(1);
1059        let ny = grid.ny.saturating_sub(1);
1060        let nz = grid.nz.saturating_sub(1);
1061
1062        // 5-tetrahedra decomposition of a cube
1063        let tet_indices: [[usize; 4]; 5] = [
1064            [0, 1, 3, 5],
1065            [1, 3, 5, 6],
1066            [0, 3, 4, 5],
1067            [3, 5, 6, 7],
1068            [0, 3, 4, 7],
1069        ];
1070        let corner_offsets: [[usize; 3]; 8] = [
1071            [0, 0, 0],
1072            [1, 0, 0],
1073            [1, 1, 0],
1074            [0, 1, 0],
1075            [0, 0, 1],
1076            [1, 0, 1],
1077            [1, 1, 1],
1078            [0, 1, 1],
1079        ];
1080
1081        for iz in 0..nz {
1082            for iy in 0..ny {
1083                for ix in 0..nx {
1084                    let mut cvals = [0.0f64; 8];
1085                    let mut cpos = [[0.0f64; 3]; 8];
1086                    for (ci, &[dx, dy, dz]) in corner_offsets.iter().enumerate() {
1087                        cpos[ci] = grid.voxel_center(ix + dx, iy + dy, iz + dz);
1088                        cvals[ci] = grid.get(ix + dx, iy + dy, iz + dz);
1089                    }
1090
1091                    for &tet in &tet_indices {
1092                        let tv: [usize; 4] = tet;
1093                        let tp: [[f64; 3]; 4] =
1094                            [cpos[tv[0]], cpos[tv[1]], cpos[tv[2]], cpos[tv[3]]];
1095                        let td: [f64; 4] = [cvals[tv[0]], cvals[tv[1]], cvals[tv[2]], cvals[tv[3]]];
1096                        Self::process_tet(&tp, &td, isovalue, &mut mesh);
1097                    }
1098                }
1099            }
1100        }
1101        mesh
1102    }
1103
1104    fn interp(p0: [f64; 3], p1: [f64; 3], d0: f64, d1: f64, iso: f64) -> [f64; 3] {
1105        let t = ((iso - d0) / (d1 - d0 + 1e-30)).clamp(0.0, 1.0);
1106        [
1107            p0[0] + t * (p1[0] - p0[0]),
1108            p0[1] + t * (p1[1] - p0[1]),
1109            p0[2] + t * (p1[2] - p0[2]),
1110        ]
1111    }
1112
1113    fn process_tet(pos: &[[f64; 3]; 4], vals: &[f64; 4], iso: f64, mesh: &mut IsoMesh) {
1114        let idx: u8 = vals.iter().enumerate().fold(
1115            0u8,
1116            |acc, (i, &v)| {
1117                if v < iso { acc | (1 << i) } else { acc }
1118            },
1119        );
1120        // Tetrahedron case table: 16 cases, emit 0, 1, or 2 triangles
1121        let edges: [(usize, usize); 6] = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
1122        let tri_cases: [[i8; 7]; 16] = [
1123            [-1, -1, -1, -1, -1, -1, -1],
1124            [0, 3, 2, -1, -1, -1, -1],
1125            [0, 1, 4, -1, -1, -1, -1],
1126            [1, 3, 2, 1, 4, 3, -1],
1127            [1, 2, 5, -1, -1, -1, -1],
1128            [0, 3, 5, 0, 5, 1, -1],
1129            [0, 2, 5, 0, 5, 4, -1],
1130            [3, 5, 4, -1, -1, -1, -1],
1131            [3, 5, 4, -1, -1, -1, -1],
1132            [0, 5, 2, 0, 4, 5, -1],
1133            [0, 5, 3, 0, 1, 5, -1],
1134            [1, 2, 5, -1, -1, -1, -1],
1135            [1, 3, 2, 1, 4, 3, -1],
1136            [0, 1, 4, -1, -1, -1, -1],
1137            [0, 3, 2, -1, -1, -1, -1],
1138            [-1, -1, -1, -1, -1, -1, -1],
1139        ];
1140        let tc = tri_cases[idx as usize];
1141        let mut ti = 0;
1142        while ti < 6 && tc[ti] >= 0 {
1143            let ei0 = tc[ti] as usize;
1144            let ei1 = tc[ti + 1] as usize;
1145            let ei2 = tc[ti + 2] as usize;
1146            let (a0, a1) = edges[ei0];
1147            let (b0, b1) = edges[ei1];
1148            let (c0, c1) = edges[ei2];
1149            let pa = Self::interp(pos[a0], pos[a1], vals[a0], vals[a1], iso);
1150            let pb = Self::interp(pos[b0], pos[b1], vals[b0], vals[b1], iso);
1151            let pc = Self::interp(pos[c0], pos[c1], vals[c0], vals[c1], iso);
1152            let vi = mesh.vertices.len();
1153            mesh.vertices.extend_from_slice(&[pa, pb, pc]);
1154            mesh.normals.extend_from_slice(&[[0.0, 1.0, 0.0]; 3]);
1155            mesh.triangles.push([vi, vi + 1, vi + 2]);
1156            ti += 3;
1157        }
1158    }
1159}
1160
1161// ─────────────────────────────────────────────────────────────────────────────
1162// Implicit Convolution (Smooth SDF)
1163// ─────────────────────────────────────────────────────────────────────────────
1164
1165/// Smooth SDF operations: offset surfaces and Minkowski sums.
1166pub struct ImplicitConvolution;
1167
1168impl ImplicitConvolution {
1169    /// Create an offset surface SDF: expands the surface outward by `offset`.
1170    pub fn offset_surface<F: Fn([f64; 3]) -> f64>(f: F, offset: f64) -> impl Fn([f64; 3]) -> f64 {
1171        move |p| f(p) - offset
1172    }
1173
1174    /// Gaussian-blurred SDF approximation via Monte Carlo sampling on a grid.
1175    ///
1176    /// Approximates ∫ f(p+δ) G(δ, sigma) dδ by sampling 8 corners.
1177    pub fn gaussian_blur<F: Fn([f64; 3]) -> f64>(f: F, sigma: f64) -> impl Fn([f64; 3]) -> f64 {
1178        move |p: [f64; 3]| {
1179            let s = sigma;
1180            let mut sum = 0.0;
1181            for dx in [-s, s] {
1182                for dy in [-s, s] {
1183                    for dz in [-s, s] {
1184                        sum += f([p[0] + dx, p[1] + dy, p[2] + dz]);
1185                    }
1186                }
1187            }
1188            sum / 8.0
1189        }
1190    }
1191
1192    /// Minkowski sum of two SDFs (approximate via smooth union with offset).
1193    ///
1194    /// For convex shapes: `(f ⊕ B_r)(p) ≈ f(p) − r`.
1195    pub fn minkowski_sum<F: Fn([f64; 3]) -> f64>(f: F, radius: f64) -> impl Fn([f64; 3]) -> f64 {
1196        move |p| f(p) - radius
1197    }
1198}
1199
1200// ─────────────────────────────────────────────────────────────────────────────
1201// Ray Marching
1202// ─────────────────────────────────────────────────────────────────────────────
1203
1204/// Ray march hit result.
1205#[derive(Debug, Clone, Copy)]
1206pub struct RayMarchHit {
1207    /// Hit position.
1208    pub position: [f64; 3],
1209    /// Distance along the ray.
1210    pub t: f64,
1211    /// Number of steps taken.
1212    pub steps: usize,
1213}
1214
1215/// Sphere-tracing ray marcher through an SDF.
1216pub struct RayMarchSdf;
1217
1218impl RayMarchSdf {
1219    /// Sphere-trace a ray through an SDF.
1220    ///
1221    /// Starting at `origin` in direction `dir` (unit vector), marches until the
1222    /// surface is hit (|SDF| < `epsilon`) or `max_steps` is reached.
1223    pub fn march<F: Fn([f64; 3]) -> f64>(
1224        f: &F,
1225        origin: [f64; 3],
1226        dir: [f64; 3],
1227        max_dist: f64,
1228        max_steps: usize,
1229        epsilon: f64,
1230    ) -> Option<RayMarchHit> {
1231        let dir = vec3_norm(dir);
1232        let mut t = 0.0;
1233        for step in 0..max_steps {
1234            let p = [
1235                origin[0] + t * dir[0],
1236                origin[1] + t * dir[1],
1237                origin[2] + t * dir[2],
1238            ];
1239            let d = f(p);
1240            if d.abs() < epsilon {
1241                return Some(RayMarchHit {
1242                    position: p,
1243                    t,
1244                    steps: step + 1,
1245                });
1246            }
1247            t += d.abs().max(epsilon * 0.1);
1248            if t > max_dist {
1249                break;
1250            }
1251        }
1252        None
1253    }
1254
1255    /// Compute ambient occlusion via SDF samples along normal.
1256    pub fn ambient_occlusion<F: Fn([f64; 3]) -> f64>(
1257        f: &F,
1258        pos: [f64; 3],
1259        normal: [f64; 3],
1260        samples: usize,
1261        step_size: f64,
1262    ) -> f64 {
1263        let mut occ = 0.0;
1264        for i in 1..=samples {
1265            let t = i as f64 * step_size;
1266            let p = vec3_add(pos, vec3_scale(normal, t));
1267            let d = f(p);
1268            occ += (t - d) / (2.0_f64.powi(i as i32));
1269        }
1270        (1.0 - occ).clamp(0.0, 1.0)
1271    }
1272}
1273
1274// ─────────────────────────────────────────────────────────────────────────────
1275// Tests
1276// ─────────────────────────────────────────────────────────────────────────────
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::*;
1281    use std::f64::consts::PI;
1282
1283    #[test]
1284    fn test_sphere_sdf_center() {
1285        let s = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1286        assert!((s.sdf([0.0, 0.0, 0.0]) - (-1.0)).abs() < 1e-10);
1287    }
1288
1289    #[test]
1290    fn test_sphere_sdf_surface() {
1291        let s = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1292        assert!(s.sdf([1.0, 0.0, 0.0]).abs() < 1e-10);
1293    }
1294
1295    #[test]
1296    fn test_sphere_sdf_outside() {
1297        let s = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1298        assert!(s.sdf([2.0, 0.0, 0.0]) > 0.0);
1299    }
1300
1301    #[test]
1302    fn test_sphere_gradient_unit_length() {
1303        let s = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1304        let g = s.gradient([2.0, 0.0, 0.0]);
1305        let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
1306        assert!((len - 1.0).abs() < 1e-10);
1307    }
1308
1309    #[test]
1310    fn test_box_sdf_inside() {
1311        let b = ImplicitBox::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1312        assert!(b.sdf([0.0, 0.0, 0.0]) < 0.0);
1313    }
1314
1315    #[test]
1316    fn test_box_sdf_surface() {
1317        let b = ImplicitBox::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1318        assert!(b.sdf([1.0, 0.0, 0.0]).abs() < 1e-8);
1319    }
1320
1321    #[test]
1322    fn test_box_sdf_outside() {
1323        let b = ImplicitBox::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1324        assert!(b.sdf([2.0, 0.0, 0.0]) > 0.0);
1325    }
1326
1327    #[test]
1328    fn test_rounded_box_sdf() {
1329        let b = ImplicitBox::rounded([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0.1);
1330        assert!(b.sdf([0.0, 0.0, 0.0]) < 0.0);
1331    }
1332
1333    #[test]
1334    fn test_capsule_sdf_on_axis() {
1335        let c = ImplicitCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1336        // Point on the segment, at radius distance: on surface
1337        assert!(c.sdf([0.5, 1.0, 0.0]).abs() < 1e-10);
1338    }
1339
1340    #[test]
1341    fn test_capsule_sdf_inside() {
1342        let c = ImplicitCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1343        assert!(c.sdf([0.0, 1.0, 0.0]) < 0.0);
1344    }
1345
1346    #[test]
1347    fn test_torus_sdf_on_surface() {
1348        let t = ImplicitTorus::new(2.0, 0.5);
1349        // Point on the torus surface: distance from Z axis = 2.0, y=0
1350        let p = [2.5, 0.0, 0.0]; // r=2.5, major=2, minor=0.5 → sdf=0
1351        assert!(t.sdf(p).abs() < 1e-10);
1352    }
1353
1354    #[test]
1355    fn test_torus_sdf_inside() {
1356        let t = ImplicitTorus::new(2.0, 0.5);
1357        let p = [2.0, 0.0, 0.0]; // on the centerline of the tube
1358        assert!(t.sdf(p) < 0.0);
1359    }
1360
1361    #[test]
1362    fn test_smooth_min_polynomial_limit() {
1363        // As k→0, smooth_min → min
1364        let a = 1.0;
1365        let b = 3.0;
1366        let sm = smooth_min_polynomial(a, b, 0.001);
1367        assert!((sm - a.min(b)).abs() < 0.01);
1368    }
1369
1370    #[test]
1371    fn test_smooth_min_exponential_symmetry() {
1372        let a = 1.0;
1373        let b = 2.0;
1374        let k = 1.0;
1375        let sm1 = smooth_min_exponential(a, b, k);
1376        let sm2 = smooth_min_exponential(b, a, k);
1377        assert!((sm1 - sm2).abs() < 1e-12);
1378    }
1379
1380    #[test]
1381    fn test_implicit_blend_smooth_union() {
1382        let blend = ImplicitBlend::new(0.3);
1383        let u = blend.smooth_union(0.5, -0.1);
1384        // Should be close to min
1385        assert!(u <= 0.5 + 0.01);
1386    }
1387
1388    #[test]
1389    fn test_implicit_blend_hard_union() {
1390        assert_eq!(ImplicitBlend::union(1.0, 2.0), 1.0);
1391        assert_eq!(ImplicitBlend::union(-1.0, 2.0), -1.0);
1392    }
1393
1394    #[test]
1395    fn test_implicit_blend_subtraction() {
1396        // Subtracting inside region from outside: should remain outside
1397        let r = ImplicitBlend::subtraction(2.0, -1.0);
1398        assert!(r > 0.0);
1399    }
1400
1401    #[test]
1402    fn test_sdf_grid_from_sphere() {
1403        let sphere = ImplicitSphere::new([0.5, 0.5, 0.5], 0.3);
1404        let grid = SdfGrid3D::from_sdf(10, 10, 10, [0.0, 0.0, 0.0], 0.1, |p| sphere.sdf(p));
1405        assert_eq!(grid.data.len(), 1000);
1406        // Center voxel should be inside
1407        let v = grid.get(5, 5, 5);
1408        assert!(v < 0.0);
1409    }
1410
1411    #[test]
1412    fn test_sdf_grid_interpolate() {
1413        let sphere = ImplicitSphere::new([0.5, 0.5, 0.5], 0.3);
1414        let grid = SdfGrid3D::from_sdf(20, 20, 20, [0.0, 0.0, 0.0], 0.05, |p| sphere.sdf(p));
1415        let v = grid.interpolate([0.5, 0.5, 0.5]);
1416        assert!(v < 0.0, "center should be inside: {}", v);
1417    }
1418
1419    #[test]
1420    fn test_marching_cubes_sphere() {
1421        let sphere = ImplicitSphere::new([2.0, 2.0, 2.0], 1.0);
1422        let grid = SdfGrid3D::from_sdf(20, 20, 20, [0.0, 0.0, 0.0], 0.25, |p| sphere.sdf(p));
1423        let mesh = MarchingCubes::extract(&grid, 0.0);
1424        assert!(
1425            !mesh.vertices.is_empty(),
1426            "marching cubes should produce vertices"
1427        );
1428        assert!(
1429            !mesh.triangles.is_empty(),
1430            "marching cubes should produce triangles"
1431        );
1432    }
1433
1434    #[test]
1435    fn test_marching_tetrahedra_sphere() {
1436        let sphere = ImplicitSphere::new([2.0, 2.0, 2.0], 1.0);
1437        let grid = SdfGrid3D::from_sdf(16, 16, 16, [0.0, 0.0, 0.0], 0.3, |p| sphere.sdf(p));
1438        let mesh = MarchingTetrahedra::extract(&grid, 0.0);
1439        assert!(
1440            !mesh.triangles.is_empty(),
1441            "marching tetrahedra should produce triangles"
1442        );
1443    }
1444
1445    #[test]
1446    fn test_dual_contouring_sphere() {
1447        let sphere = ImplicitSphere::new([2.0, 2.0, 2.0], 1.0);
1448        let grid = SdfGrid3D::from_sdf(16, 16, 16, [0.0, 0.0, 0.0], 0.3, |p| sphere.sdf(p));
1449        let mesh = DualContouring::extract(&grid, 0.0);
1450        assert!(
1451            !mesh.vertices.is_empty(),
1452            "dual contouring should produce vertices"
1453        );
1454    }
1455
1456    #[test]
1457    fn test_ray_march_sphere_hit() {
1458        let sphere = ImplicitSphere::new([0.0, 0.0, 5.0], 1.0);
1459        let f = |p: [f64; 3]| sphere.sdf(p);
1460        let hit = RayMarchSdf::march(&f, [0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 20.0, 200, 1e-4);
1461        assert!(hit.is_some(), "ray should hit sphere");
1462        let h = hit.unwrap();
1463        assert!((h.t - 4.0).abs() < 0.01, "hit at t≈4, got {}", h.t);
1464    }
1465
1466    #[test]
1467    fn test_ray_march_sphere_miss() {
1468        let sphere = ImplicitSphere::new([0.0, 0.0, 5.0], 1.0);
1469        let f = |p: [f64; 3]| sphere.sdf(p);
1470        // Ray in opposite direction
1471        let hit = RayMarchSdf::march(&f, [0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 20.0, 200, 1e-4);
1472        assert!(hit.is_none(), "ray in wrong direction should miss");
1473    }
1474
1475    #[test]
1476    fn test_ambient_occlusion_on_surface() {
1477        let sphere = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1478        let f = |p: [f64; 3]| sphere.sdf(p);
1479        let ao = RayMarchSdf::ambient_occlusion(&f, [1.0, 0.0, 0.0], [1.0, 0.0, 0.0], 5, 0.1);
1480        assert!(
1481            (0.0..=1.0).contains(&ao),
1482            "AO should be in [0,1], got {}",
1483            ao
1484        );
1485    }
1486
1487    #[test]
1488    fn test_sdf_reinitialize() {
1489        let sphere = ImplicitSphere::new([2.0, 2.0, 2.0], 1.0);
1490        let mut grid = SdfGrid3D::from_sdf(16, 16, 16, [0.0, 0.0, 0.0], 0.3, |p| sphere.sdf(p));
1491        // Corrupt some values
1492        grid.set(5, 5, 5, 100.0);
1493        SdfReinitialize::reinitialize(&mut grid);
1494        // After reinitialization, values should be more reasonable
1495        assert!(grid.get(5, 5, 5).abs() < 50.0);
1496    }
1497
1498    #[test]
1499    fn test_iso_mesh_empty() {
1500        let mesh = IsoMesh::new();
1501        assert_eq!(mesh.num_vertices(), 0);
1502        assert_eq!(mesh.num_triangles(), 0);
1503    }
1504
1505    #[test]
1506    fn test_offset_surface() {
1507        let sphere = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1508        let offset = ImplicitConvolution::offset_surface(|p| sphere.sdf(p), 0.5);
1509        // Surface now at r=1.5
1510        assert!(offset([1.5, 0.0, 0.0]).abs() < 1e-10);
1511    }
1512
1513    #[test]
1514    fn test_smooth_min_polynomial_symmetric() {
1515        let a = 0.5;
1516        let b = 1.5;
1517        let k = 0.3;
1518        let s1 = smooth_min_polynomial(a, b, k);
1519        let s2 = smooth_min_polynomial(b, a, k);
1520        assert!((s1 - s2).abs() < 1e-12);
1521    }
1522
1523    #[test]
1524    fn test_sphere_hessian_shape() {
1525        let s = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1526        let h = s.hessian([2.0, 0.0, 0.0]);
1527        assert_eq!(h.len(), 9);
1528    }
1529
1530    #[test]
1531    fn test_capsule_gradient_unit() {
1532        let c = ImplicitCapsule::new([0.0, 0.0, 0.0], [0.0, 2.0, 0.0], 0.5);
1533        let g = c.gradient([1.0, 1.0, 0.0]);
1534        let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
1535        assert!((len - 1.0).abs() < 1e-10);
1536    }
1537
1538    #[test]
1539    fn test_sdf_grid_gradient_direction() {
1540        let sphere = ImplicitSphere::new([2.0, 2.0, 2.0], 1.0);
1541        let grid = SdfGrid3D::from_sdf(20, 20, 20, [0.0, 0.0, 0.0], 0.2, |p| sphere.sdf(p));
1542        // Gradient at a point outside should point away from center
1543        let g = grid.gradient([3.5, 2.0, 2.0]);
1544        assert!(g[0] > 0.0, "gradient x component should be positive");
1545    }
1546
1547    #[test]
1548    fn test_implicit_torus_outside() {
1549        let t = ImplicitTorus::new(2.0, 0.5);
1550        // Far from torus
1551        assert!(t.sdf([10.0, 0.0, 0.0]) > 0.0);
1552    }
1553
1554    #[test]
1555    fn test_sdf_numerical_gradient() {
1556        let sphere = ImplicitSphere::new([0.0, 0.0, 0.0], 1.0);
1557        let f = |p: [f64; 3]| sphere.sdf(p);
1558        let g = sdf_gradient_numerical(&f, [2.0, 0.0, 0.0], 1e-4);
1559        // Should point in +x direction
1560        assert!(g[0] > 0.0);
1561    }
1562
1563    #[test]
1564    fn test_marching_cubes_box() {
1565        let b = ImplicitBox::new([2.0, 2.0, 2.0], [0.8, 0.8, 0.8]);
1566        let grid = SdfGrid3D::from_sdf(20, 20, 20, [0.0, 0.0, 0.0], 0.25, |p| b.sdf(p));
1567        let mesh = MarchingCubes::extract(&grid, 0.0);
1568        assert!(
1569            !mesh.triangles.is_empty(),
1570            "box isosurface should have triangles"
1571        );
1572    }
1573
1574    #[test]
1575    fn test_pi_used() {
1576        // Just ensure PI import is used
1577        let _ = PI;
1578    }
1579}