Skip to main content

oxiphysics_geometry/
implicit.rs

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