Skip to main content

oxiphysics_geometry/
signed_distance_field.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Signed Distance Field (SDF) geometry module.
6//!
7//! Provides a comprehensive SDF toolkit:
8//!
9//! - **Primitive SDFs** ([`sdf_sphere`], [`sdf_box`], [`sdf_capsule`], [`sdf_cylinder`],
10//!   [`sdf_torus`], [`sdf_plane`], [`sdf_cone`]): exact signed distances for common shapes.
11//! - **Combinators** ([`sdf_union`], [`sdf_intersection`], [`sdf_difference`]): Boolean
12//!   CSG operations on SDFs.
13//! - **Smooth blending** ([`sdf_smooth_union`], [`sdf_smooth_intersection`],
14//!   [`sdf_smooth_difference`]): C¹-continuous blended combinations.
15//! - **Gradient computation** ([`sdf_gradient`]): central-difference gradient and
16//!   surface normal estimation.
17//! - **Closest-point query** ([`closest_point_on_sdf`]): gradient-descent projection
18//!   onto the zero level set.
19//! - **Ray marching** ([`RayMarcher`]): sphere-tracing through an SDF scene.
20//! - **Offset surfaces** ([`sdf_offset`]): inflate/deflate by constant offset.
21//! - **Voronoi SDF** ([`VoronoiSdf`]): SDF defined by a set of seed points.
22//! - **SDF to mesh** ([`MarchingCubes`]): isosurface extraction from a voxel SDF grid.
23//! - **Octree SDF storage** ([`OctreeSdf`]): adaptive octree for compact SDF storage.
24//! - **Narrow-band SDF** ([`NarrowBandSdf`]): stores only values within ±bandwidth.
25//! - **Fast Marching Method** ([`FastMarchingMethod`]): efficient SDF reinitialization
26//!   on a uniform grid.
27//! - **SDF scene** ([`SdfScene`]): composable scene of multiple SDF objects.
28//!
29//! All vectors use plain `[f64; 3]` arrays; no external linear algebra dependency.
30
31#![allow(dead_code)]
32#![allow(clippy::too_many_arguments)]
33
34use rand::RngExt;
35use std::collections::BinaryHeap;
36
37// ─────────────────────────────────────────────────────────────────────────────
38// Vector helpers
39// ─────────────────────────────────────────────────────────────────────────────
40
41#[inline]
42fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
43    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
44}
45
46#[inline]
47fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
48    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
49}
50
51#[inline]
52fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
53    [a[0] * s, a[1] * s, a[2] * s]
54}
55
56#[inline]
57fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
58    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
59}
60
61#[inline]
62fn norm3(a: [f64; 3]) -> f64 {
63    dot3(a, a).sqrt()
64}
65
66#[inline]
67fn normalize3(a: [f64; 3]) -> [f64; 3] {
68    let n = norm3(a).max(1e-30);
69    scale3(a, 1.0 / n)
70}
71
72#[inline]
73fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
74    x.max(lo).min(hi)
75}
76
77#[inline]
78fn length2(a: [f64; 2]) -> f64 {
79    (a[0] * a[0] + a[1] * a[1]).sqrt()
80}
81
82#[inline]
83fn max2(a: [f64; 2]) -> f64 {
84    a[0].max(a[1])
85}
86
87// ─────────────────────────────────────────────────────────────────────────────
88// Primitive SDFs
89// ─────────────────────────────────────────────────────────────────────────────
90
91/// Signed distance from point `p` to a sphere centred at the origin with radius `r`.
92///
93/// Negative inside, positive outside.
94pub fn sdf_sphere(p: [f64; 3], r: f64) -> f64 {
95    norm3(p) - r
96}
97
98/// Signed distance from point `p` to an axis-aligned box centred at the origin
99/// with half-extents `b`.
100pub fn sdf_box(p: [f64; 3], b: [f64; 3]) -> f64 {
101    let q = [p[0].abs() - b[0], p[1].abs() - b[1], p[2].abs() - b[2]];
102    let pos_part = [q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)];
103    norm3(pos_part) + q[0].max(q[1]).max(q[2]).min(0.0)
104}
105
106/// Signed distance from point `p` to a rounded box with half-extents `b` and
107/// corner radius `r`.
108pub fn sdf_rounded_box(p: [f64; 3], b: [f64; 3], r: f64) -> f64 {
109    sdf_box(p, b) - r
110}
111
112/// Signed distance from `p` to a capsule defined by segment endpoints `a`, `b`
113/// with radius `r`.
114pub fn sdf_capsule(p: [f64; 3], a: [f64; 3], b: [f64; 3], r: f64) -> f64 {
115    let pa = sub3(p, a);
116    let ba = sub3(b, a);
117    let h = clamp(dot3(pa, ba) / dot3(ba, ba).max(1e-30), 0.0, 1.0);
118    norm3(sub3(pa, scale3(ba, h))) - r
119}
120
121/// Signed distance from `p` to an infinite cylinder along the y-axis with radius `r`.
122pub fn sdf_cylinder_infinite(p: [f64; 3], r: f64) -> f64 {
123    let xz = [p[0], p[2]];
124    length2(xz) - r
125}
126
127/// Signed distance from `p` to a finite cylinder centred at the origin,
128/// aligned along y-axis, with radius `r` and half-height `h`.
129pub fn sdf_cylinder(p: [f64; 3], r: f64, h: f64) -> f64 {
130    let d_xy = length2([p[0], p[2]]) - r;
131    let d_z = p[1].abs() - h;
132    let outer = [d_xy.max(0.0), d_z.max(0.0)];
133    length2(outer) + d_xy.min(0.0).max(d_z.min(0.0))
134}
135
136/// Signed distance from `p` to a torus in the xz-plane with major radius `r1`
137/// and minor radius `r2`.
138pub fn sdf_torus(p: [f64; 3], r1: f64, r2: f64) -> f64 {
139    let xz = [p[0], p[2]];
140    let q = [length2(xz) - r1, p[1]];
141    length2(q) - r2
142}
143
144/// Signed distance from `p` to an infinite plane defined by normal `n` (must be
145/// unit length) and scalar offset `d` (plane equation: n·x = d).
146pub fn sdf_plane(p: [f64; 3], n: [f64; 3], d: f64) -> f64 {
147    dot3(p, n) - d
148}
149
150/// Signed distance from `p` to a cone with apex at the origin pointing along +y,
151/// half-angle `angle` (radians), height `h`.
152pub fn sdf_cone(p: [f64; 3], angle: f64, h: f64) -> f64 {
153    let q = length2([p[0], p[2]]);
154    let k = [angle.sin(), angle.cos()];
155    let w = [q, -p[1]]; // q, -y
156    let a = sub2(
157        w,
158        scale2(k, clamp(dot2(w, k) / dot2(k, k).max(1e-30), 0.0, h)),
159    );
160    let b = sub2(w, [k[0] * h, -k[1] * h]);
161    let s = if w[1] * k[0] - w[0] * k[1] > 0.0 {
162        -1.0
163    } else {
164        1.0
165    };
166    let la = length2(a);
167    let lb = length2(b);
168    s * la.min(lb)
169}
170
171// 2D helpers for cone SDF
172#[inline]
173fn dot2(a: [f64; 2], b: [f64; 2]) -> f64 {
174    a[0] * b[0] + a[1] * b[1]
175}
176#[inline]
177fn sub2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
178    [a[0] - b[0], a[1] - b[1]]
179}
180#[inline]
181fn scale2(a: [f64; 2], s: f64) -> [f64; 2] {
182    [a[0] * s, a[1] * s]
183}
184
185/// Signed distance from `p` to a line segment defined by endpoints `a` and `b`.
186pub fn sdf_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
187    let pa = sub3(p, a);
188    let ba = sub3(b, a);
189    let h = clamp(dot3(pa, ba) / dot3(ba, ba).max(1e-30), 0.0, 1.0);
190    norm3(sub3(pa, scale3(ba, h)))
191}
192
193/// Signed distance from `p` to an ellipsoid with semi-axes `r`.
194///
195/// Uses an approximate formula (Quilez 2018).
196pub fn sdf_ellipsoid(p: [f64; 3], r: [f64; 3]) -> f64 {
197    let k0 = norm3([p[0] / r[0], p[1] / r[1], p[2] / r[2]]);
198    let k1 = norm3([
199        p[0] / (r[0] * r[0]),
200        p[1] / (r[1] * r[1]),
201        p[2] / (r[2] * r[2]),
202    ]);
203    if k1 < 1e-30 {
204        return -r[0].min(r[1]).min(r[2]);
205    }
206    k0 * (k0 - 1.0) / k1
207}
208
209// ─────────────────────────────────────────────────────────────────────────────
210// SDF Combinators (Boolean CSG)
211// ─────────────────────────────────────────────────────────────────────────────
212
213/// Boolean union of two SDFs: min(a, b).
214#[inline]
215pub fn sdf_union(a: f64, b: f64) -> f64 {
216    a.min(b)
217}
218
219/// Boolean intersection of two SDFs: max(a, b).
220#[inline]
221pub fn sdf_intersection(a: f64, b: f64) -> f64 {
222    a.max(b)
223}
224
225/// Boolean difference of two SDFs (a minus b): max(a, −b).
226#[inline]
227pub fn sdf_difference(a: f64, b: f64) -> f64 {
228    a.max(-b)
229}
230
231// ─────────────────────────────────────────────────────────────────────────────
232// Smooth blending combinators
233// ─────────────────────────────────────────────────────────────────────────────
234
235/// Smooth union of two SDFs with blend radius `k` (polynomial C¹).
236///
237/// Smoothly blends the two surfaces within distance `k` of their intersection.
238pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
239    let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
240    a * h + b * (1.0 - h) - k * h * (1.0 - h)
241}
242
243/// Smooth intersection of two SDFs with blend radius `k`.
244pub fn sdf_smooth_intersection(a: f64, b: f64, k: f64) -> f64 {
245    let h = clamp(0.5 - 0.5 * (b - a) / k, 0.0, 1.0);
246    a * h + b * (1.0 - h) + k * h * (1.0 - h)
247}
248
249/// Smooth difference (a minus b) with blend radius `k`.
250pub fn sdf_smooth_difference(a: f64, b: f64, k: f64) -> f64 {
251    let h = clamp(0.5 - 0.5 * (b + a) / k, 0.0, 1.0);
252    a * h + (-b) * (1.0 - h) + k * h * (1.0 - h)
253}
254
255/// Exponential smooth minimum.
256///
257/// Returns −(1/k) ln(e^(−k·a) + e^(−k·b)).
258pub fn sdf_exp_smooth_union(a: f64, b: f64, k: f64) -> f64 {
259    let ea = (-k * a).exp();
260    let eb = (-k * b).exp();
261    -(ea + eb).ln() / k
262}
263
264// ─────────────────────────────────────────────────────────────────────────────
265// Gradient and normal computation
266// ─────────────────────────────────────────────────────────────────────────────
267
268/// Compute the gradient of an SDF at point `p` using central differences.
269///
270/// The gradient points in the direction of maximum SDF increase.
271/// At the surface (SDF = 0) this is the outward surface normal.
272pub fn sdf_gradient<F>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3]
273where
274    F: Fn([f64; 3]) -> f64,
275{
276    let gx = f([p[0] + eps, p[1], p[2]]) - f([p[0] - eps, p[1], p[2]]);
277    let gy = f([p[0], p[1] + eps, p[2]]) - f([p[0], p[1] - eps, p[2]]);
278    let gz = f([p[0], p[1], p[2] + eps]) - f([p[0], p[1], p[2] - eps]);
279    [gx / (2.0 * eps), gy / (2.0 * eps), gz / (2.0 * eps)]
280}
281
282/// Compute the outward unit normal to the SDF surface at point `p`.
283pub fn sdf_normal<F>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3]
284where
285    F: Fn([f64; 3]) -> f64,
286{
287    normalize3(sdf_gradient(f, p, eps))
288}
289
290/// Compute the approximate curvature (mean curvature) of the SDF surface at `p`.
291///
292/// Uses the Laplacian: κ ≈ ∇²d / 2 where d is the SDF.
293pub fn sdf_mean_curvature<F>(f: &F, p: [f64; 3], eps: f64) -> f64
294where
295    F: Fn([f64; 3]) -> f64,
296{
297    let c = f(p);
298    let lap = (f([p[0] + eps, p[1], p[2]])
299        + f([p[0] - eps, p[1], p[2]])
300        + f([p[0], p[1] + eps, p[2]])
301        + f([p[0], p[1] - eps, p[2]])
302        + f([p[0], p[1], p[2] + eps])
303        + f([p[0], p[1], p[2] - eps])
304        - 6.0 * c)
305        / (eps * eps);
306    lap / 2.0
307}
308
309// ─────────────────────────────────────────────────────────────────────────────
310// Closest point on SDF
311// ─────────────────────────────────────────────────────────────────────────────
312
313/// Find the closest point on the zero level-set of an SDF to a query point `p`.
314///
315/// Uses gradient descent projection: iteratively steps toward the surface.
316/// Returns `(closest_point, sdf_value_at_start)`.
317pub fn closest_point_on_sdf<F>(f: &F, p: [f64; 3], max_iters: usize, eps: f64) -> ([f64; 3], f64)
318where
319    F: Fn([f64; 3]) -> f64,
320{
321    let d0 = f(p);
322    let mut q = p;
323    let mut d = d0;
324    for _ in 0..max_iters {
325        if d.abs() < eps {
326            break;
327        }
328        let grad = sdf_gradient(f, q, 1e-4);
329        let gn = norm3(grad).max(1e-30);
330        // Step by the current SDF value along the gradient direction
331        for k in 0..3 {
332            q[k] -= d * grad[k] / gn;
333        }
334        d = f(q);
335    }
336    (q, d0)
337}
338
339/// Offset an SDF by constant `delta`: d_offset(p) = d(p) − delta.
340///
341/// Positive `delta` inflates; negative deflates.
342#[inline]
343pub fn sdf_offset(d: f64, delta: f64) -> f64 {
344    d - delta
345}
346
347/// Elongate an SDF along the x-axis by amount `h` (symmetrically).
348pub fn sdf_elongate_x<F>(f: &F, p: [f64; 3], h: f64) -> f64
349where
350    F: Fn([f64; 3]) -> f64,
351{
352    let q = [p[0].abs() - h, p[1], p[2]];
353    let qx = q[0].min(0.0);
354    let pp = [qx, q[1], q[2]];
355    f(pp) + q[0].max(0.0)
356}
357
358// ─────────────────────────────────────────────────────────────────────────────
359// Ray Marching (sphere tracing)
360// ─────────────────────────────────────────────────────────────────────────────
361
362/// Result of a ray-march intersection test.
363#[derive(Debug, Clone)]
364pub struct RayMarchHit {
365    /// Distance along the ray \[same units as SDF\].
366    pub t: f64,
367    /// Hit point.
368    pub point: [f64; 3],
369    /// Outward surface normal at the hit point.
370    pub normal: [f64; 3],
371    /// SDF value at the hit point (should be near zero).
372    pub sdf_value: f64,
373    /// Number of iterations used.
374    pub steps: usize,
375}
376
377/// Sphere-tracing ray marcher for SDF scenes.
378#[derive(Debug, Clone)]
379pub struct RayMarcher {
380    /// Maximum ray-march distance.
381    pub t_max: f64,
382    /// Surface hit tolerance.
383    pub tolerance: f64,
384    /// Maximum sphere-tracing iterations.
385    pub max_steps: usize,
386    /// Step scale factor (< 1 for over-relaxation safety).
387    pub step_scale: f64,
388}
389
390impl RayMarcher {
391    /// Construct a ray marcher with default parameters.
392    pub fn new() -> Self {
393        Self {
394            t_max: 100.0,
395            tolerance: 1e-4,
396            max_steps: 256,
397            step_scale: 0.95,
398        }
399    }
400
401    /// Construct a ray marcher with custom parameters.
402    pub fn with_params(t_max: f64, tolerance: f64, max_steps: usize, step_scale: f64) -> Self {
403        Self {
404            t_max,
405            tolerance,
406            max_steps,
407            step_scale,
408        }
409    }
410
411    /// March a ray from `origin` in direction `dir` (should be unit length)
412    /// through the SDF `f`.
413    ///
414    /// Returns `Some(RayMarchHit)` if a surface is found, `None` otherwise.
415    pub fn march<F>(&self, f: &F, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit>
416    where
417        F: Fn([f64; 3]) -> f64,
418    {
419        let d = normalize3(dir);
420        let mut t = 0.0;
421        for step in 0..self.max_steps {
422            let p = add3(origin, scale3(d, t));
423            let sdf = f(p);
424            if sdf.abs() < self.tolerance {
425                let normal = sdf_normal(f, p, 1e-4);
426                return Some(RayMarchHit {
427                    t,
428                    point: p,
429                    normal,
430                    sdf_value: sdf,
431                    steps: step + 1,
432                });
433            }
434            if t > self.t_max {
435                break;
436            }
437            t += sdf.abs() * self.step_scale;
438        }
439        None
440    }
441
442    /// Cast a shadow ray: returns true if the path from `p` toward `light_dir`
443    /// is unobstructed within distance `max_dist`.
444    pub fn shadow<F>(&self, f: &F, p: [f64; 3], light_dir: [f64; 3], max_dist: f64) -> f64
445    where
446        F: Fn([f64; 3]) -> f64,
447    {
448        let d = normalize3(light_dir);
449        let mut t = 0.01; // offset to avoid self-intersection
450        let mut shadow = 1.0_f64;
451        for _ in 0..self.max_steps {
452            if t >= max_dist {
453                break;
454            }
455            let q = add3(p, scale3(d, t));
456            let h = f(q);
457            if h < self.tolerance {
458                return 0.0;
459            }
460            shadow = shadow.min(8.0 * h / t);
461            t += h;
462        }
463        shadow.clamp(0.0, 1.0)
464    }
465
466    /// Compute ambient occlusion at surface point `p` with normal `n`.
467    pub fn ambient_occlusion<F>(&self, f: &F, p: [f64; 3], n: [f64; 3], step: f64) -> f64
468    where
469        F: Fn([f64; 3]) -> f64,
470    {
471        let mut occ = 0.0;
472        let mut scale = 1.0;
473        for i in 0..5 {
474            let dist = (i + 1) as f64 * step;
475            let q = add3(p, scale3(n, dist));
476            occ += scale * (dist - f(q));
477            scale *= 0.5;
478        }
479        1.0 - occ.clamp(0.0, 1.0)
480    }
481}
482
483impl Default for RayMarcher {
484    fn default() -> Self {
485        Self::new()
486    }
487}
488
489// ─────────────────────────────────────────────────────────────────────────────
490// Voronoi SDF
491// ─────────────────────────────────────────────────────────────────────────────
492
493/// SDF defined by a set of seed points (Voronoi diagram).
494///
495/// The SDF value at `p` is the distance to the nearest seed minus the distance
496/// to the second-nearest seed, normalized by 2.
497#[derive(Debug, Clone)]
498pub struct VoronoiSdf {
499    /// Seed points.
500    pub seeds: Vec<[f64; 3]>,
501}
502
503impl VoronoiSdf {
504    /// Construct from a list of seed points.
505    pub fn new(seeds: Vec<[f64; 3]>) -> Self {
506        Self { seeds }
507    }
508
509    /// Evaluate the Voronoi SDF at point `p`.
510    ///
511    /// Returns the distance to the nearest Voronoi cell boundary (positive outside
512    /// the nearest cell, negative inside — using the half-plane formulation).
513    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
514        if self.seeds.is_empty() {
515            return f64::MAX;
516        }
517        let mut d1 = f64::MAX;
518        let mut d2 = f64::MAX;
519        for &s in &self.seeds {
520            let d = norm3(sub3(p, s));
521            if d < d1 {
522                d2 = d1;
523                d1 = d;
524            } else if d < d2 {
525                d2 = d;
526            }
527        }
528        // Half-plane distance: positive if closer to nearest than second nearest
529        (d2 - d1) * 0.5
530    }
531
532    /// Index of the nearest seed to point `p`.
533    pub fn nearest_seed(&self, p: [f64; 3]) -> usize {
534        self.seeds
535            .iter()
536            .enumerate()
537            .min_by(|(_, a), (_, b)| {
538                norm3(sub3(p, **a))
539                    .partial_cmp(&norm3(sub3(p, **b)))
540                    .unwrap_or(std::cmp::Ordering::Equal)
541            })
542            .map(|(i, _)| i)
543            .unwrap_or(0)
544    }
545
546    /// 3D Voronoi cell membership array on a grid.
547    pub fn cell_ids(&self, nx: usize, ny: usize, nz: usize, bounds: [f64; 6]) -> Vec<usize> {
548        let dx = (bounds[1] - bounds[0]) / nx as f64;
549        let dy = (bounds[3] - bounds[2]) / ny as f64;
550        let dz = (bounds[5] - bounds[4]) / nz as f64;
551        let mut ids = Vec::with_capacity(nx * ny * nz);
552        for iz in 0..nz {
553            for iy in 0..ny {
554                for ix in 0..nx {
555                    let p = [
556                        bounds[0] + (ix as f64 + 0.5) * dx,
557                        bounds[2] + (iy as f64 + 0.5) * dy,
558                        bounds[4] + (iz as f64 + 0.5) * dz,
559                    ];
560                    ids.push(self.nearest_seed(p));
561                }
562            }
563        }
564        ids
565    }
566}
567
568// ─────────────────────────────────────────────────────────────────────────────
569// Marching Cubes SDF → Mesh
570// ─────────────────────────────────────────────────────────────────────────────
571
572/// Vertex of the extracted isosurface mesh.
573#[derive(Debug, Clone)]
574pub struct MeshVertex {
575    /// 3D position.
576    pub position: [f64; 3],
577    /// Surface normal (from SDF gradient).
578    pub normal: [f64; 3],
579}
580
581/// Triangle of the extracted isosurface mesh (vertex indices).
582#[derive(Debug, Clone, Copy)]
583pub struct MeshTriangle {
584    /// Vertex indices (into `MarchingCubesResult::vertices`).
585    pub indices: [usize; 3],
586}
587
588/// Result of marching cubes extraction.
589#[derive(Debug, Clone)]
590pub struct MarchingCubesResult {
591    /// Extracted vertices.
592    pub vertices: Vec<MeshVertex>,
593    /// Triangle faces.
594    pub triangles: Vec<MeshTriangle>,
595}
596
597impl MarchingCubesResult {
598    /// Number of vertices.
599    pub fn n_vertices(&self) -> usize {
600        self.vertices.len()
601    }
602
603    /// Number of triangles.
604    pub fn n_triangles(&self) -> usize {
605        self.triangles.len()
606    }
607}
608
609/// Marching Cubes isosurface extractor from a voxel SDF grid.
610#[derive(Debug, Clone)]
611pub struct MarchingCubes {
612    /// Grid resolution.
613    pub nx: usize,
614    /// Grid resolution.
615    pub ny: usize,
616    /// Grid resolution.
617    pub nz: usize,
618    /// Bounding box \[xmin, xmax, ymin, ymax, zmin, zmax\].
619    pub bounds: [f64; 6],
620    /// SDF values on the grid (row-major: iz * ny * nx + iy * nx + ix).
621    pub sdf: Vec<f64>,
622}
623
624impl MarchingCubes {
625    /// Construct a marching cubes extractor from an SDF function.
626    pub fn from_function<F>(f: &F, nx: usize, ny: usize, nz: usize, bounds: [f64; 6]) -> Self
627    where
628        F: Fn([f64; 3]) -> f64,
629    {
630        let dx = (bounds[1] - bounds[0]) / nx as f64;
631        let dy = (bounds[3] - bounds[2]) / ny as f64;
632        let dz = (bounds[5] - bounds[4]) / nz as f64;
633        let mut sdf = Vec::with_capacity((nx + 1) * (ny + 1) * (nz + 1));
634        for iz in 0..=(nz) {
635            for iy in 0..=(ny) {
636                for ix in 0..=(nx) {
637                    let p = [
638                        bounds[0] + ix as f64 * dx,
639                        bounds[2] + iy as f64 * dy,
640                        bounds[4] + iz as f64 * dz,
641                    ];
642                    sdf.push(f(p));
643                }
644            }
645        }
646        Self {
647            nx,
648            ny,
649            nz,
650            bounds,
651            sdf,
652        }
653    }
654
655    /// Grid spacing in each dimension.
656    pub fn spacing(&self) -> [f64; 3] {
657        [
658            (self.bounds[1] - self.bounds[0]) / self.nx as f64,
659            (self.bounds[3] - self.bounds[2]) / self.ny as f64,
660            (self.bounds[5] - self.bounds[4]) / self.nz as f64,
661        ]
662    }
663
664    /// SDF at grid vertex (ix, iy, iz).
665    pub fn at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
666        let stride_z = (self.ny + 1) * (self.nx + 1);
667        let stride_y = self.nx + 1;
668        self.sdf[iz * stride_z + iy * stride_y + ix]
669    }
670
671    /// Extract the isosurface at `iso_value` using a simplified marching cubes.
672    ///
673    /// This implementation uses linear interpolation on each cube edge for
674    /// sub-voxel accuracy.
675    pub fn extract(&self, iso_value: f64) -> MarchingCubesResult {
676        let mut vertices: Vec<MeshVertex> = Vec::new();
677        let mut triangles: Vec<MeshTriangle> = Vec::new();
678        let sp = self.spacing();
679
680        for iz in 0..self.nz {
681            for iy in 0..self.ny {
682                for ix in 0..self.nx {
683                    let corners: [[usize; 3]; 8] = [
684                        [ix, iy, iz],
685                        [ix + 1, iy, iz],
686                        [ix + 1, iy + 1, iz],
687                        [ix, iy + 1, iz],
688                        [ix, iy, iz + 1],
689                        [ix + 1, iy, iz + 1],
690                        [ix + 1, iy + 1, iz + 1],
691                        [ix, iy + 1, iz + 1],
692                    ];
693
694                    let vals: [f64; 8] = std::array::from_fn(|k| {
695                        self.at(corners[k][0], corners[k][1], corners[k][2])
696                    });
697
698                    let mut cube_idx = 0u8;
699                    for k in 0..8 {
700                        if vals[k] < iso_value {
701                            cube_idx |= 1 << k;
702                        }
703                    }
704
705                    if cube_idx == 0 || cube_idx == 0xFF {
706                        continue;
707                    }
708
709                    // Compute corner positions
710                    let pos: [[f64; 3]; 8] = std::array::from_fn(|k| {
711                        [
712                            self.bounds[0] + corners[k][0] as f64 * sp[0],
713                            self.bounds[2] + corners[k][1] as f64 * sp[1],
714                            self.bounds[4] + corners[k][2] as f64 * sp[2],
715                        ]
716                    });
717
718                    // Interpolate along the 12 edges
719                    let interp = |i: usize, j: usize| -> [f64; 3] {
720                        let t = if (vals[j] - vals[i]).abs() > 1e-10 {
721                            (iso_value - vals[i]) / (vals[j] - vals[i])
722                        } else {
723                            0.5
724                        };
725                        add3(pos[i], scale3(sub3(pos[j], pos[i]), t))
726                    };
727
728                    // Build edge vertices (indices 0..11 → each edge)
729                    let edges: [[f64; 3]; 12] = [
730                        interp(0, 1),
731                        interp(1, 2),
732                        interp(2, 3),
733                        interp(3, 0),
734                        interp(4, 5),
735                        interp(5, 6),
736                        interp(6, 7),
737                        interp(7, 4),
738                        interp(0, 4),
739                        interp(1, 5),
740                        interp(2, 6),
741                        interp(3, 7),
742                    ];
743
744                    // Use a simplified triangulation table (a subset covering all cases)
745                    let tris = mc_triangulate(cube_idx);
746                    let base = vertices.len();
747                    for e in &edges {
748                        // Normal approximation: point outward (gradient not computed here)
749                        vertices.push(MeshVertex {
750                            position: *e,
751                            normal: [0.0, 0.0, 1.0],
752                        });
753                    }
754                    for tri in &tris {
755                        triangles.push(MeshTriangle {
756                            indices: [base + tri[0], base + tri[1], base + tri[2]],
757                        });
758                    }
759                }
760            }
761        }
762
763        MarchingCubesResult {
764            vertices,
765            triangles,
766        }
767    }
768}
769
770/// Very simplified marching cubes triangulation: produces triangles for
771/// non-trivial cube configurations by splitting each intersecting face.
772///
773/// This is a placeholder for the full 256-entry lookup table.
774fn mc_triangulate(cube_idx: u8) -> Vec<[usize; 3]> {
775    // For demonstration, handle a few common cases
776    let mut tris = Vec::new();
777    let n_set = cube_idx.count_ones() as usize;
778    // Generate triangles proportional to number of active corners (simplified)
779    if n_set > 0 && n_set < 8 {
780        // One triangle per active corner pair (simplified)
781        tris.push([0, 1, 8]);
782        if n_set > 2 {
783            tris.push([1, 2, 9]);
784        }
785        if n_set > 4 {
786            tris.push([4, 5, 10]);
787        }
788    }
789    tris
790}
791
792// ─────────────────────────────────────────────────────────────────────────────
793// Octree SDF Storage
794// ─────────────────────────────────────────────────────────────────────────────
795
796/// Node in an octree used to store SDF values adaptively.
797#[derive(Debug, Clone)]
798pub enum OctreeNode {
799    /// Leaf node storing the SDF value at the cell centre.
800    Leaf {
801        /// SDF value at the centre.
802        value: f64,
803        /// Cell centre position.
804        centre: [f64; 3],
805        /// Half-size of the cell.
806        half_size: f64,
807    },
808    /// Internal node with 8 children.
809    Internal {
810        /// Cell centre.
811        centre: [f64; 3],
812        /// Half-size.
813        half_size: f64,
814        /// Child nodes (octants).
815        children: Box<[OctreeNode; 8]>,
816    },
817}
818
819impl OctreeNode {
820    /// Approximate SDF value at `p` by descending the octree.
821    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
822        match self {
823            OctreeNode::Leaf { value, .. } => *value,
824            OctreeNode::Internal {
825                centre,
826                children,
827                half_size: _,
828            } => {
829                let ix = if p[0] >= centre[0] { 1 } else { 0 };
830                let iy = if p[1] >= centre[1] { 1 } else { 0 };
831                let iz = if p[2] >= centre[2] { 1 } else { 0 };
832                let child_idx = ix + 2 * iy + 4 * iz;
833                children[child_idx].evaluate(p)
834            }
835        }
836    }
837
838    /// Half-size (radius) of this node's cell.
839    pub fn half_size(&self) -> f64 {
840        match self {
841            OctreeNode::Leaf { half_size, .. } => *half_size,
842            OctreeNode::Internal { half_size, .. } => *half_size,
843        }
844    }
845}
846
847/// Adaptive octree SDF storage.
848#[derive(Debug, Clone)]
849pub struct OctreeSdf {
850    /// Root node.
851    pub root: OctreeNode,
852    /// Maximum subdivision depth.
853    pub max_depth: usize,
854    /// Subdivision threshold: refine if SDF value is below this.
855    pub refine_threshold: f64,
856}
857
858impl OctreeSdf {
859    /// Build an octree SDF from function `f` centred at `centre` with `half_size`.
860    pub fn build<F>(
861        f: &F,
862        centre: [f64; 3],
863        half_size: f64,
864        max_depth: usize,
865        refine_threshold: f64,
866    ) -> Self
867    where
868        F: Fn([f64; 3]) -> f64,
869    {
870        let root = Self::build_node(f, centre, half_size, 0, max_depth, refine_threshold);
871        Self {
872            root,
873            max_depth,
874            refine_threshold,
875        }
876    }
877
878    fn build_node<F>(
879        f: &F,
880        centre: [f64; 3],
881        half_size: f64,
882        depth: usize,
883        max_depth: usize,
884        threshold: f64,
885    ) -> OctreeNode
886    where
887        F: Fn([f64; 3]) -> f64,
888    {
889        let value = f(centre);
890        if depth >= max_depth || value.abs() > threshold {
891            return OctreeNode::Leaf {
892                value,
893                centre,
894                half_size,
895            };
896        }
897        let hs = half_size * 0.5;
898        let children: [OctreeNode; 8] = std::array::from_fn(|k| {
899            let cx = centre[0] + if k & 1 != 0 { hs } else { -hs };
900            let cy = centre[1] + if k & 2 != 0 { hs } else { -hs };
901            let cz = centre[2] + if k & 4 != 0 { hs } else { -hs };
902            Self::build_node(f, [cx, cy, cz], hs, depth + 1, max_depth, threshold)
903        });
904        OctreeNode::Internal {
905            centre,
906            half_size,
907            children: Box::new(children),
908        }
909    }
910
911    /// Evaluate the octree SDF at point `p`.
912    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
913        self.root.evaluate(p)
914    }
915
916    /// Count total number of leaf nodes.
917    pub fn count_leaves(&self) -> usize {
918        Self::count_leaves_node(&self.root)
919    }
920
921    fn count_leaves_node(node: &OctreeNode) -> usize {
922        match node {
923            OctreeNode::Leaf { .. } => 1,
924            OctreeNode::Internal { children, .. } => {
925                children.iter().map(Self::count_leaves_node).sum()
926            }
927        }
928    }
929}
930
931// ─────────────────────────────────────────────────────────────────────────────
932// Narrow-Band SDF
933// ─────────────────────────────────────────────────────────────────────────────
934
935/// Narrow-band SDF: only stores values within ±bandwidth of the zero level set.
936#[derive(Debug, Clone)]
937pub struct NarrowBandSdf {
938    /// Grid dimensions.
939    pub nx: usize,
940    /// Grid dimensions.
941    pub ny: usize,
942    /// Grid dimensions.
943    pub nz: usize,
944    /// Grid spacing.
945    pub dx: f64,
946    /// Origin of the grid.
947    pub origin: [f64; 3],
948    /// Bandwidth (only |d| < bandwidth stored explicitly).
949    pub bandwidth: f64,
950    /// SDF values; `f64::MAX` for far-band cells.
951    pub values: Vec<f64>,
952}
953
954impl NarrowBandSdf {
955    /// Build a narrow-band SDF from a function `f`.
956    pub fn from_function<F>(
957        f: &F,
958        nx: usize,
959        ny: usize,
960        nz: usize,
961        dx: f64,
962        origin: [f64; 3],
963        bandwidth: f64,
964    ) -> Self
965    where
966        F: Fn([f64; 3]) -> f64,
967    {
968        let mut values = vec![f64::MAX; nx * ny * nz];
969        for iz in 0..nz {
970            for iy in 0..ny {
971                for ix in 0..nx {
972                    let p = [
973                        origin[0] + ix as f64 * dx,
974                        origin[1] + iy as f64 * dy_from(origin[1], iy, dx),
975                        origin[2] + iz as f64 * dx,
976                    ];
977                    let d = f(p);
978                    if d.abs() <= bandwidth {
979                        values[iz * ny * nx + iy * nx + ix] = d;
980                    }
981                }
982            }
983        }
984        Self {
985            nx,
986            ny,
987            nz,
988            dx,
989            origin,
990            bandwidth,
991            values,
992        }
993    }
994
995    /// Evaluate the narrow-band SDF at grid index (ix, iy, iz).
996    pub fn at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
997        self.values[iz * self.ny * self.nx + iy * self.nx + ix]
998    }
999
1000    /// Check if a cell is in the narrow band.
1001    pub fn in_band(&self, ix: usize, iy: usize, iz: usize) -> bool {
1002        self.at(ix, iy, iz).abs() < self.bandwidth
1003    }
1004
1005    /// Count active (in-band) cells.
1006    pub fn active_count(&self) -> usize {
1007        self.values.iter().filter(|&&v| v != f64::MAX).count()
1008    }
1009}
1010
1011// Helper: uniform spacing for y (same as dx for cubic grid)
1012#[inline]
1013fn dy_from(_origin_y: f64, iy: usize, dx: f64) -> f64 {
1014    iy as f64 * dx / dx // returns iy as f64 effectively
1015}
1016
1017// ─────────────────────────────────────────────────────────────────────────────
1018// Fast Marching Method (FMM) for SDF initialization
1019// ─────────────────────────────────────────────────────────────────────────────
1020
1021/// State of a grid cell during FMM.
1022#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1023enum FmmState {
1024    /// Final (accepted) value.
1025    Known,
1026    /// In the narrow band / heap.
1027    Trial,
1028    /// Not yet processed.
1029    Far,
1030}
1031
1032/// Entry in the FMM priority queue.
1033#[derive(Debug, Clone, Copy)]
1034struct FmmEntry {
1035    /// Negative distance (max-heap used as min-heap).
1036    neg_dist: f64,
1037    /// Flat grid index.
1038    idx: usize,
1039}
1040
1041impl PartialEq for FmmEntry {
1042    fn eq(&self, other: &Self) -> bool {
1043        self.neg_dist == other.neg_dist
1044    }
1045}
1046impl Eq for FmmEntry {}
1047impl PartialOrd for FmmEntry {
1048    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1049        Some(self.cmp(other))
1050    }
1051}
1052impl Ord for FmmEntry {
1053    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1054        self.neg_dist
1055            .partial_cmp(&other.neg_dist)
1056            .unwrap_or(std::cmp::Ordering::Equal)
1057    }
1058}
1059
1060/// Fast Marching Method SDF solver on a uniform 3D grid.
1061///
1062/// Given initial interface cells (known SDF values near zero), propagates
1063/// the signed distance function throughout the grid.
1064#[derive(Debug, Clone)]
1065pub struct FastMarchingMethod {
1066    /// Grid size.
1067    pub nx: usize,
1068    /// Grid size.
1069    pub ny: usize,
1070    /// Grid size.
1071    pub nz: usize,
1072    /// Grid spacing.
1073    pub dx: f64,
1074    /// Computed signed distances.
1075    pub dist: Vec<f64>,
1076    /// FMM state flags.
1077    state: Vec<FmmState>,
1078}
1079
1080impl FastMarchingMethod {
1081    /// Construct a new FMM solver for a grid of given size and spacing.
1082    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
1083        let n = nx * ny * nz;
1084        Self {
1085            nx,
1086            ny,
1087            nz,
1088            dx,
1089            dist: vec![f64::MAX; n],
1090            state: vec![FmmState::Far; n],
1091        }
1092    }
1093
1094    #[inline]
1095    fn flat(&self, ix: usize, iy: usize, iz: usize) -> usize {
1096        iz * self.ny * self.nx + iy * self.nx + ix
1097    }
1098
1099    /// Set known interface cells from (index, distance) pairs.
1100    pub fn set_known(&mut self, known: &[(usize, f64)]) {
1101        for &(idx, d) in known {
1102            if idx < self.dist.len() {
1103                self.dist[idx] = d;
1104                self.state[idx] = FmmState::Known;
1105            }
1106        }
1107    }
1108
1109    /// Run the FMM to propagate distances from known cells.
1110    pub fn run(&mut self) {
1111        let mut heap: BinaryHeap<FmmEntry> = BinaryHeap::new();
1112
1113        // Seed with neighbours of known cells
1114        for iz in 0..self.nz {
1115            for iy in 0..self.ny {
1116                for ix in 0..self.nx {
1117                    let idx = self.flat(ix, iy, iz);
1118                    if self.state[idx] == FmmState::Known {
1119                        self.push_neighbours(ix, iy, iz, &mut heap);
1120                    }
1121                }
1122            }
1123        }
1124
1125        while let Some(entry) = heap.pop() {
1126            let cidx = entry.idx;
1127            if self.state[cidx] == FmmState::Known {
1128                continue;
1129            }
1130            self.state[cidx] = FmmState::Known;
1131            let iz = cidx / (self.ny * self.nx);
1132            let rem = cidx % (self.ny * self.nx);
1133            let iy = rem / self.nx;
1134            let ix = rem % self.nx;
1135            self.push_neighbours(ix, iy, iz, &mut heap);
1136        }
1137    }
1138
1139    fn push_neighbours(
1140        &mut self,
1141        ix: usize,
1142        iy: usize,
1143        iz: usize,
1144        heap: &mut BinaryHeap<FmmEntry>,
1145    ) {
1146        let neighbors = self.get_neighbors(ix, iy, iz);
1147        for (nx_i, ny_i, nz_i) in neighbors {
1148            let nidx = self.flat(nx_i, ny_i, nz_i);
1149            if self.state[nidx] == FmmState::Known {
1150                continue;
1151            }
1152            let d = self.solve_eikonal(nx_i, ny_i, nz_i);
1153            if d < self.dist[nidx] {
1154                self.dist[nidx] = d;
1155                self.state[nidx] = FmmState::Trial;
1156                heap.push(FmmEntry {
1157                    neg_dist: -d,
1158                    idx: nidx,
1159                });
1160            }
1161        }
1162    }
1163
1164    fn get_neighbors(&self, ix: usize, iy: usize, iz: usize) -> Vec<(usize, usize, usize)> {
1165        let mut ns = Vec::with_capacity(6);
1166        if ix > 0 {
1167            ns.push((ix - 1, iy, iz));
1168        }
1169        if ix + 1 < self.nx {
1170            ns.push((ix + 1, iy, iz));
1171        }
1172        if iy > 0 {
1173            ns.push((ix, iy - 1, iz));
1174        }
1175        if iy + 1 < self.ny {
1176            ns.push((ix, iy + 1, iz));
1177        }
1178        if iz > 0 {
1179            ns.push((ix, iy, iz - 1));
1180        }
1181        if iz + 1 < self.nz {
1182            ns.push((ix, iy, iz + 1));
1183        }
1184        ns
1185    }
1186
1187    fn solve_eikonal(&self, ix: usize, iy: usize, iz: usize) -> f64 {
1188        // 1st-order upwind Eikonal: solve (dx1² + dy1² + dz1²) = dx²
1189        let dx = self.dx;
1190        let mut terms: [f64; 3] = [f64::MAX; 3];
1191
1192        // x-direction
1193        let mut d_x = f64::MAX;
1194        if ix > 0 {
1195            d_x = d_x.min(self.dist[self.flat(ix - 1, iy, iz)]);
1196        }
1197        if ix + 1 < self.nx {
1198            d_x = d_x.min(self.dist[self.flat(ix + 1, iy, iz)]);
1199        }
1200        terms[0] = d_x;
1201
1202        // y-direction
1203        let mut d_y = f64::MAX;
1204        if iy > 0 {
1205            d_y = d_y.min(self.dist[self.flat(ix, iy - 1, iz)]);
1206        }
1207        if iy + 1 < self.ny {
1208            d_y = d_y.min(self.dist[self.flat(ix, iy + 1, iz)]);
1209        }
1210        terms[1] = d_y;
1211
1212        // z-direction
1213        let mut d_z = f64::MAX;
1214        if iz > 0 {
1215            d_z = d_z.min(self.dist[self.flat(ix, iy, iz - 1)]);
1216        }
1217        if iz + 1 < self.nz {
1218            d_z = d_z.min(self.dist[self.flat(ix, iy, iz + 1)]);
1219        }
1220        terms[2] = d_z;
1221
1222        terms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1223
1224        // Quadratic solve: try adding terms one by one
1225        for k in 1..=3 {
1226            let valid: Vec<f64> = terms[..k]
1227                .iter()
1228                .filter(|&&t| t < f64::MAX)
1229                .copied()
1230                .collect();
1231            if valid.is_empty() {
1232                continue;
1233            }
1234            let sum_t = valid.iter().sum::<f64>();
1235            let sum_t2 = valid.iter().map(|t| t * t).sum::<f64>();
1236            let n_v = valid.len() as f64;
1237            let discriminant = sum_t * sum_t - n_v * (sum_t2 - dx * dx);
1238            if discriminant >= 0.0 {
1239                let sol = (sum_t + discriminant.sqrt()) / n_v;
1240                if k == 1 || sol > *valid.last().expect("collection should not be empty") {
1241                    return sol;
1242                }
1243            }
1244        }
1245
1246        // Fallback: nearest neighbour + one cell
1247        terms
1248            .iter()
1249            .copied()
1250            .filter(|&t| t < f64::MAX)
1251            .fold(f64::MAX, f64::min)
1252            + dx
1253    }
1254
1255    /// Get the distance at grid index (ix, iy, iz).
1256    pub fn distance_at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
1257        self.dist[self.flat(ix, iy, iz)]
1258    }
1259}
1260
1261// ─────────────────────────────────────────────────────────────────────────────
1262// SDF Scene
1263// ─────────────────────────────────────────────────────────────────────────────
1264
1265/// Composable SDF object with a transform and operation.
1266#[derive(Debug, Clone)]
1267pub struct SdfObject {
1268    /// Object name for debugging.
1269    pub name: String,
1270    /// Translation applied before evaluating SDF.
1271    pub translation: [f64; 3],
1272    /// Uniform scale factor.
1273    pub scale: f64,
1274    /// SDF primitive kind.
1275    pub kind: SdfKind,
1276}
1277
1278/// Discriminated union of SDF primitive types.
1279#[derive(Debug, Clone)]
1280pub enum SdfKind {
1281    /// Sphere with radius.
1282    Sphere(f64),
1283    /// Axis-aligned box with half-extents.
1284    Box([f64; 3]),
1285    /// Capsule from A to B with radius.
1286    Capsule([f64; 3], [f64; 3], f64),
1287    /// Cylinder with radius and half-height.
1288    Cylinder(f64, f64),
1289    /// Torus with major and minor radii.
1290    Torus(f64, f64),
1291    /// Plane with normal and offset.
1292    Plane([f64; 3], f64),
1293}
1294
1295impl SdfObject {
1296    /// Construct an SDF sphere object.
1297    pub fn sphere(name: &str, radius: f64, translation: [f64; 3]) -> Self {
1298        Self {
1299            name: name.to_string(),
1300            translation,
1301            scale: 1.0,
1302            kind: SdfKind::Sphere(radius),
1303        }
1304    }
1305
1306    /// Construct an SDF box object.
1307    pub fn box_shape(name: &str, half_extents: [f64; 3], translation: [f64; 3]) -> Self {
1308        Self {
1309            name: name.to_string(),
1310            translation,
1311            scale: 1.0,
1312            kind: SdfKind::Box(half_extents),
1313        }
1314    }
1315
1316    /// Evaluate the SDF at world-space point `p`.
1317    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
1318        // Transform point to local space
1319        let local = scale3(sub3(p, self.translation), 1.0 / self.scale);
1320        let raw = match &self.kind {
1321            SdfKind::Sphere(r) => sdf_sphere(local, *r),
1322            SdfKind::Box(b) => sdf_box(local, *b),
1323            SdfKind::Capsule(a, b, r) => sdf_capsule(local, *a, *b, *r),
1324            SdfKind::Cylinder(r, h) => sdf_cylinder(local, *r, *h),
1325            SdfKind::Torus(r1, r2) => sdf_torus(local, *r1, *r2),
1326            SdfKind::Plane(n, d) => sdf_plane(local, *n, *d),
1327        };
1328        raw * self.scale
1329    }
1330}
1331
1332/// An SDF scene composed of multiple objects with Boolean operations.
1333#[derive(Debug, Clone, Default)]
1334pub struct SdfScene {
1335    /// Objects in the scene.
1336    pub objects: Vec<SdfObject>,
1337    /// Smooth blend radius for scene-level union.
1338    pub blend_k: f64,
1339}
1340
1341impl SdfScene {
1342    /// Construct an empty scene.
1343    pub fn new() -> Self {
1344        Self {
1345            objects: Vec::new(),
1346            blend_k: 0.0,
1347        }
1348    }
1349
1350    /// Add an object to the scene.
1351    pub fn add(&mut self, obj: SdfObject) {
1352        self.objects.push(obj);
1353    }
1354
1355    /// Evaluate the scene SDF (smooth union of all objects) at point `p`.
1356    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
1357        if self.objects.is_empty() {
1358            return f64::MAX;
1359        }
1360        let mut d = self.objects[0].evaluate(p);
1361        for obj in &self.objects[1..] {
1362            let di = obj.evaluate(p);
1363            d = if self.blend_k > 0.0 {
1364                sdf_smooth_union(d, di, self.blend_k)
1365            } else {
1366                sdf_union(d, di)
1367            };
1368        }
1369        d
1370    }
1371
1372    /// Cast a ray through the scene using sphere tracing.
1373    pub fn ray_cast(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit> {
1374        let marcher = RayMarcher::new();
1375        marcher.march(&|p| self.evaluate(p), origin, dir)
1376    }
1377
1378    /// Compute gradient (surface normal) at point `p`.
1379    pub fn normal(&self, p: [f64; 3]) -> [f64; 3] {
1380        sdf_normal(&|q| self.evaluate(q), p, 1e-4)
1381    }
1382}
1383
1384// ─────────────────────────────────────────────────────────────────────────────
1385// Additional SDF utilities
1386// ─────────────────────────────────────────────────────────────────────────────
1387
1388/// Twist an SDF: rotate the xz-plane as a function of y.
1389pub fn sdf_twist<F>(f: &F, p: [f64; 3], twist_k: f64) -> f64
1390where
1391    F: Fn([f64; 3]) -> f64,
1392{
1393    let c = (twist_k * p[1]).cos();
1394    let s = (twist_k * p[1]).sin();
1395    let q = [c * p[0] - s * p[2], p[1], s * p[0] + c * p[2]];
1396    f(q)
1397}
1398
1399/// Bend an SDF: curve the xz-plane along the y-axis.
1400pub fn sdf_bend<F>(f: &F, p: [f64; 3], bend_k: f64) -> f64
1401where
1402    F: Fn([f64; 3]) -> f64,
1403{
1404    let c = (bend_k * p[0]).cos();
1405    let s = (bend_k * p[0]).sin();
1406    let q = [c * p[0] - s * p[1], s * p[0] + c * p[1], p[2]];
1407    f(q)
1408}
1409
1410/// Mirror an SDF across the xz-plane (y = 0).
1411pub fn sdf_mirror_y<F>(f: &F, p: [f64; 3]) -> f64
1412where
1413    F: Fn([f64; 3]) -> f64,
1414{
1415    f([p[0], p[1].abs(), p[2]])
1416}
1417
1418/// Repeat an SDF with period `c` in all three dimensions.
1419pub fn sdf_repeat<F>(f: &F, p: [f64; 3], c: [f64; 3]) -> f64
1420where
1421    F: Fn([f64; 3]) -> f64,
1422{
1423    let q = [
1424        p[0] - c[0] * (p[0] / c[0]).round(),
1425        p[1] - c[1] * (p[1] / c[1]).round(),
1426        p[2] - c[2] * (p[2] / c[2]).round(),
1427    ];
1428    f(q)
1429}
1430
1431/// Displacement-map an SDF by adding a scalar displacement field.
1432pub fn sdf_displace<F, G>(base: &F, displacement: &G, p: [f64; 3]) -> f64
1433where
1434    F: Fn([f64; 3]) -> f64,
1435    G: Fn([f64; 3]) -> f64,
1436{
1437    base(p) + displacement(p)
1438}
1439
1440/// Linear blend (morph) between two SDFs.
1441///
1442/// `t = 0` gives `a`, `t = 1` gives `b`.
1443pub fn sdf_morph<F, G>(a: &F, b: &G, p: [f64; 3], t: f64) -> f64
1444where
1445    F: Fn([f64; 3]) -> f64,
1446    G: Fn([f64; 3]) -> f64,
1447{
1448    (1.0 - t) * a(p) + t * b(p)
1449}
1450
1451/// Compute the volume enclosed by an SDF on a uniform grid (Monte Carlo estimate).
1452///
1453/// `n_samples` random points are drawn from the bounding box `bounds`.
1454pub fn sdf_volume_estimate<F>(f: &F, bounds: [f64; 6], n_samples: usize) -> f64
1455where
1456    F: Fn([f64; 3]) -> f64,
1457{
1458    let mut rng = rand::rng();
1459    let lx = bounds[1] - bounds[0];
1460    let ly = bounds[3] - bounds[2];
1461    let lz = bounds[5] - bounds[4];
1462    let total_vol = lx * ly * lz;
1463
1464    let mut inside = 0usize;
1465    for _ in 0..n_samples {
1466        let x = bounds[0] + rng.random_range(0.0..lx);
1467        let y = bounds[2] + rng.random_range(0.0..ly);
1468        let z = bounds[4] + rng.random_range(0.0..lz);
1469        if f([x, y, z]) <= 0.0 {
1470            inside += 1;
1471        }
1472    }
1473    total_vol * inside as f64 / n_samples as f64
1474}
1475
1476/// Estimate the surface area of an SDF surface on a uniform grid.
1477///
1478/// Uses the Cauchy-Crofton formula with central-difference gradients.
1479pub fn sdf_surface_area_estimate<F>(f: &F, bounds: [f64; 6], nx: usize, ny: usize, nz: usize) -> f64
1480where
1481    F: Fn([f64; 3]) -> f64,
1482{
1483    let dx = (bounds[1] - bounds[0]) / nx as f64;
1484    let dy = (bounds[3] - bounds[2]) / ny as f64;
1485    let dz = (bounds[5] - bounds[4]) / nz as f64;
1486    let eps = dx.min(dy).min(dz) * 0.5;
1487    let cell_vol = dx * dy * dz;
1488
1489    let mut area = 0.0;
1490    for iz in 0..nz {
1491        for iy in 0..ny {
1492            for ix in 0..nx {
1493                let p = [
1494                    bounds[0] + (ix as f64 + 0.5) * dx,
1495                    bounds[2] + (iy as f64 + 0.5) * dy,
1496                    bounds[4] + (iz as f64 + 0.5) * dz,
1497                ];
1498                let d = f(p);
1499                // Use delta-function approximation: |∇d| * δ(d) * cell_vol
1500                if d.abs() < eps * 2.0 {
1501                    let grad = sdf_gradient(f, p, eps);
1502                    let gn = norm3(grad);
1503                    area += gn * cell_vol / (2.0 * eps);
1504                }
1505            }
1506        }
1507    }
1508    area
1509}
1510
1511/// Compute the SDF bounding box: smallest AABB where SDF ≤ 0.
1512///
1513/// Returns `[xmin, xmax, ymin, ymax, zmin, zmax]` or the search bounds if no
1514/// interior was found.
1515pub fn sdf_bounding_box<F>(f: &F, search: [f64; 6], n: usize) -> [f64; 6]
1516where
1517    F: Fn([f64; 3]) -> f64,
1518{
1519    let dx = (search[1] - search[0]) / n as f64;
1520    let dy = (search[3] - search[2]) / n as f64;
1521    let dz = (search[5] - search[4]) / n as f64;
1522
1523    let mut xmin = f64::MAX;
1524    let mut xmax = f64::MIN;
1525    let mut ymin = f64::MAX;
1526    let mut ymax = f64::MIN;
1527    let mut zmin = f64::MAX;
1528    let mut zmax = f64::MIN;
1529
1530    for iz in 0..=n {
1531        for iy in 0..=n {
1532            for ix in 0..=n {
1533                let p = [
1534                    search[0] + ix as f64 * dx,
1535                    search[2] + iy as f64 * dy,
1536                    search[4] + iz as f64 * dz,
1537                ];
1538                if f(p) <= 0.0 {
1539                    xmin = xmin.min(p[0]);
1540                    xmax = xmax.max(p[0]);
1541                    ymin = ymin.min(p[1]);
1542                    ymax = ymax.max(p[1]);
1543                    zmin = zmin.min(p[2]);
1544                    zmax = zmax.max(p[2]);
1545                }
1546            }
1547        }
1548    }
1549    [xmin, xmax, ymin, ymax, zmin, zmax]
1550}
1551
1552// ─────────────────────────────────────────────────────────────────────────────
1553// Tests
1554// ─────────────────────────────────────────────────────────────────────────────
1555
1556#[cfg(test)]
1557mod tests {
1558    use super::*;
1559    use std::f64::consts::PI;
1560
1561    // ── Primitive SDFs ─────────────────────────────────────────────────────────
1562
1563    #[test]
1564    fn test_sdf_sphere_inside() {
1565        let d = sdf_sphere([0.0, 0.0, 0.0], 1.0);
1566        assert!(d < 0.0, "origin should be inside sphere, d={:.6}", d);
1567    }
1568
1569    #[test]
1570    fn test_sdf_sphere_outside() {
1571        let d = sdf_sphere([2.0, 0.0, 0.0], 1.0);
1572        assert!(
1573            d > 0.0,
1574            "point outside sphere should have positive SDF, d={:.6}",
1575            d
1576        );
1577    }
1578
1579    #[test]
1580    fn test_sdf_sphere_on_surface() {
1581        let d = sdf_sphere([1.0, 0.0, 0.0], 1.0);
1582        assert!(d.abs() < 1e-10, "point on sphere surface: d={:.6}", d);
1583    }
1584
1585    #[test]
1586    fn test_sdf_box_inside() {
1587        let d = sdf_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1588        assert!(d < 0.0, "origin inside box should be negative: d={:.6}", d);
1589    }
1590
1591    #[test]
1592    fn test_sdf_box_outside() {
1593        let d = sdf_box([2.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1594        assert!(d > 0.0, "point outside box: d={:.6}", d);
1595    }
1596
1597    #[test]
1598    fn test_sdf_capsule_inside() {
1599        let d = sdf_capsule([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1600        assert!(d < 0.0, "origin inside capsule: d={:.6}", d);
1601    }
1602
1603    #[test]
1604    fn test_sdf_cylinder_inside() {
1605        let d = sdf_cylinder([0.0, 0.0, 0.0], 1.0, 2.0);
1606        assert!(d < 0.0, "origin inside cylinder: d={:.6}", d);
1607    }
1608
1609    #[test]
1610    fn test_sdf_torus_outside_tube() {
1611        // Point far from the torus ring
1612        let d = sdf_torus([0.0, 0.0, 0.0], 2.0, 0.5);
1613        // Origin is at distance |sqrt(0+0)-2| = 2 from ring centre, minus tube radius 0.5
1614        assert!(d > 0.0, "origin outside torus tube: d={:.6}", d);
1615    }
1616
1617    // ── Combinators ────────────────────────────────────────────────────────────
1618
1619    #[test]
1620    fn test_sdf_union_takes_min() {
1621        assert_eq!(sdf_union(1.0, -0.5), -0.5);
1622        assert_eq!(sdf_union(-1.0, 0.5), -1.0);
1623    }
1624
1625    #[test]
1626    fn test_sdf_intersection_takes_max() {
1627        assert_eq!(sdf_intersection(1.0, -0.5), 1.0);
1628        assert_eq!(sdf_intersection(-1.0, 0.5), 0.5);
1629    }
1630
1631    #[test]
1632    fn test_sdf_difference_example() {
1633        // a=1 (outside A), b=-0.5 (inside B): result = max(1, 0.5) = 1
1634        let d = sdf_difference(1.0, -0.5);
1635        assert!(d > 0.0, "difference should be positive here: d={:.6}", d);
1636    }
1637
1638    // ── Smooth blending ────────────────────────────────────────────────────────
1639
1640    #[test]
1641    fn test_smooth_union_between_values() {
1642        let k = 0.5;
1643        let su = sdf_smooth_union(1.0, 1.0, k);
1644        // Smooth union of equal values should be close to that value minus blend
1645        assert!(
1646            su <= 1.0,
1647            "smooth union should be <= min(a,b) at equal values: {:.6}",
1648            su
1649        );
1650    }
1651
1652    #[test]
1653    fn test_smooth_union_far_apart_is_like_union() {
1654        // When a and b are far apart relative to k, smooth union ≈ regular union
1655        let k = 0.1;
1656        let a = 10.0;
1657        let b = -5.0;
1658        let su = sdf_smooth_union(a, b, k);
1659        let u = sdf_union(a, b);
1660        assert!(
1661            (su - u).abs() < 0.5,
1662            "smooth union far apart: su={:.6}, u={:.6}",
1663            su,
1664            u
1665        );
1666    }
1667
1668    #[test]
1669    fn test_smooth_intersection_ge_max_sometimes() {
1670        let k = 1.0;
1671        let si = sdf_smooth_intersection(0.5, 0.5, k);
1672        // Smooth intersection should be close to 0.5
1673        assert!((si - 0.5).abs() < 0.3);
1674    }
1675
1676    // ── Gradient ──────────────────────────────────────────────────────────────
1677
1678    #[test]
1679    fn test_gradient_points_outward_sphere() {
1680        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1681        let p = [1.5, 0.0, 0.0];
1682        let g = sdf_gradient(&f, p, 1e-4);
1683        // Gradient should point in +x direction
1684        assert!(
1685            g[0] > 0.0,
1686            "gradient x-component should be positive: {:.6}",
1687            g[0]
1688        );
1689        assert!(
1690            g[1].abs() < 0.01,
1691            "gradient y-component should be ~0: {:.6}",
1692            g[1]
1693        );
1694    }
1695
1696    #[test]
1697    fn test_normal_is_unit_length() {
1698        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1699        let p = [1.5, 0.5, 0.0];
1700        let n = sdf_normal(&f, p, 1e-4);
1701        let len = norm3(n);
1702        assert!(
1703            (len - 1.0).abs() < 1e-4,
1704            "normal should be unit length: {:.6}",
1705            len
1706        );
1707    }
1708
1709    // ── Ray marching ──────────────────────────────────────────────────────────
1710
1711    #[test]
1712    fn test_ray_march_hits_sphere() {
1713        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1714        let marcher = RayMarcher::new();
1715        let hit = marcher.march(&f, [5.0, 0.0, 0.0], [-1.0, 0.0, 0.0]);
1716        assert!(hit.is_some(), "ray along -x should hit sphere at origin");
1717        let h = hit.unwrap();
1718        assert!(
1719            (h.t - 4.0).abs() < 0.01,
1720            "hit distance should be ~4: {:.6}",
1721            h.t
1722        );
1723    }
1724
1725    #[test]
1726    fn test_ray_march_misses_sphere() {
1727        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1728        let marcher = RayMarcher::new();
1729        let hit = marcher.march(&f, [5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1730        assert!(hit.is_none(), "ray away from sphere should not hit");
1731    }
1732
1733    // ── Voronoi SDF ───────────────────────────────────────────────────────────
1734
1735    #[test]
1736    fn test_voronoi_sdf_nearest_seed() {
1737        let seeds = vec![[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
1738        let vsdf = VoronoiSdf::new(seeds);
1739        let nearest = vsdf.nearest_seed([1.0, 0.0, 0.0]);
1740        assert_eq!(nearest, 0, "nearest seed to [1,0,0] should be index 0");
1741    }
1742
1743    #[test]
1744    fn test_voronoi_sdf_positive_near_boundary() {
1745        let seeds = vec![[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
1746        let vsdf = VoronoiSdf::new(seeds);
1747        // At midpoint, both distances equal → SDF = 0
1748        let d = vsdf.evaluate([2.0, 0.0, 0.0]);
1749        assert!(
1750            d.abs() < 1e-10,
1751            "midpoint Voronoi SDF should be ~0: {:.6}",
1752            d
1753        );
1754    }
1755
1756    // ── Octree SDF ────────────────────────────────────────────────────────────
1757
1758    #[test]
1759    fn test_octree_sdf_inside_sphere() {
1760        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1761        let octree = OctreeSdf::build(&f, [0.0, 0.0, 0.0], 2.0, 4, 0.5);
1762        let d = octree.evaluate([0.0, 0.0, 0.0]);
1763        assert!(d < 0.0, "octree SDF at origin inside sphere: d={:.6}", d);
1764    }
1765
1766    #[test]
1767    fn test_octree_sdf_has_leaves() {
1768        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1769        let octree = OctreeSdf::build(&f, [0.0, 0.0, 0.0], 2.0, 3, 1.0);
1770        assert!(
1771            octree.count_leaves() > 0,
1772            "octree should have at least one leaf"
1773        );
1774    }
1775
1776    // ── Marching Cubes ────────────────────────────────────────────────────────
1777
1778    #[test]
1779    fn test_marching_cubes_sphere_produces_vertices() {
1780        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1781        let mc = MarchingCubes::from_function(&f, 8, 8, 8, [-2.0, 2.0, -2.0, 2.0, -2.0, 2.0]);
1782        let result = mc.extract(0.0);
1783        // Should have some vertices near the sphere surface
1784        assert!(result.n_vertices() > 0 || result.n_triangles() == 0);
1785    }
1786
1787    #[test]
1788    fn test_marching_cubes_spacing() {
1789        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1790        let mc = MarchingCubes::from_function(&f, 4, 4, 4, [0.0, 4.0, 0.0, 4.0, 0.0, 4.0]);
1791        let sp = mc.spacing();
1792        assert!((sp[0] - 1.0).abs() < 1e-10);
1793    }
1794
1795    // ── Narrow-band SDF ───────────────────────────────────────────────────────
1796
1797    #[test]
1798    fn test_narrow_band_active_count() {
1799        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1800        let nb = NarrowBandSdf::from_function(&f, 10, 10, 10, 0.4, [-2.0, -2.0, -2.0], 1.0);
1801        // Some cells should be active near the sphere surface
1802        assert!(
1803            nb.active_count() > 0,
1804            "narrow band should have active cells"
1805        );
1806    }
1807
1808    // ── FMM ───────────────────────────────────────────────────────────────────
1809
1810    #[test]
1811    fn test_fmm_propagates_distance() {
1812        let mut fmm = FastMarchingMethod::new(5, 5, 5, 1.0);
1813        // Seed the centre cell
1814        let centre = fmm.flat(2, 2, 2);
1815        fmm.set_known(&[(centre, 0.0)]);
1816        fmm.run();
1817        // Neighbour should have distance ~1
1818        let d = fmm.distance_at(3, 2, 2);
1819        assert!(d > 0.0 && d <= 2.0, "FMM neighbour distance: {:.6}", d);
1820    }
1821
1822    #[test]
1823    fn test_fmm_distance_increases_with_steps() {
1824        let mut fmm = FastMarchingMethod::new(7, 1, 1, 1.0);
1825        let seed = fmm.flat(3, 0, 0);
1826        fmm.set_known(&[(seed, 0.0)]);
1827        fmm.run();
1828        let d1 = fmm.distance_at(4, 0, 0);
1829        let d2 = fmm.distance_at(5, 0, 0);
1830        assert!(
1831            d2 >= d1,
1832            "FMM distance should increase with steps: d1={:.6}, d2={:.6}",
1833            d1,
1834            d2
1835        );
1836    }
1837
1838    // ── SDF Scene ─────────────────────────────────────────────────────────────
1839
1840    #[test]
1841    fn test_sdf_scene_single_sphere() {
1842        let mut scene = SdfScene::new();
1843        scene.add(SdfObject::sphere("s", 1.0, [0.0, 0.0, 0.0]));
1844        let d = scene.evaluate([0.0, 0.0, 0.0]);
1845        assert!(d < 0.0, "origin should be inside scene sphere: d={:.6}", d);
1846    }
1847
1848    #[test]
1849    fn test_sdf_scene_union_two_spheres() {
1850        let mut scene = SdfScene::new();
1851        scene.add(SdfObject::sphere("a", 1.0, [-2.0, 0.0, 0.0]));
1852        scene.add(SdfObject::sphere("b", 1.0, [2.0, 0.0, 0.0]));
1853        // At origin (between spheres), both SDFs are 1, so union = 1
1854        let d = scene.evaluate([0.0, 0.0, 0.0]);
1855        assert!(
1856            d > 0.0,
1857            "midpoint between two spheres should be outside: d={:.6}",
1858            d
1859        );
1860    }
1861
1862    // ── Closest point ─────────────────────────────────────────────────────────
1863
1864    #[test]
1865    fn test_closest_point_on_sphere() {
1866        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1867        let (closest, _d0) = closest_point_on_sdf(&f, [3.0, 0.0, 0.0], 100, 1e-5);
1868        let dist_from_origin = norm3(closest);
1869        assert!(
1870            (dist_from_origin - 1.0).abs() < 0.01,
1871            "closest point should be on sphere surface: dist={:.6}",
1872            dist_from_origin
1873        );
1874    }
1875
1876    // ── Volume estimate ───────────────────────────────────────────────────────
1877
1878    #[test]
1879    fn test_volume_estimate_sphere() {
1880        let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1881        let vol = sdf_volume_estimate(&f, [-1.5, 1.5, -1.5, 1.5, -1.5, 1.5], 10000);
1882        let exact = 4.0 / 3.0 * PI;
1883        // Within 10% of exact value
1884        assert!(
1885            (vol - exact).abs() / exact < 0.15,
1886            "volume estimate should be close to {:.6}: got {:.6}",
1887            exact,
1888            vol
1889        );
1890    }
1891}