Skip to main content

oxiphysics_geometry/csg/
functions.rs

1//! Auto-generated module
2//!
3//! đŸ¤– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{CsgOp, CsgTree, MarchingCell, PlaneSide};
6
7/// Type alias for a mesh returned as (vertices, triangle-index-lists).
8type VertsTris = (Vec<[f64; 3]>, Vec<[usize; 3]>);
9/// Type alias for a list of (point, normal) pairs.
10type PointNormals = Vec<([f64; 3], [f64; 3])>;
11/// Type alias for an AABB of an implicit surface: (min, max) corner pair.
12type SurfaceAabb = ([f64; 3], [f64; 3]);
13
14#[inline]
15pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
16    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
17}
18#[inline]
19pub(super) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
20    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
21}
22#[inline]
23pub(super) fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
25}
26#[inline]
27pub(super) fn scale(a: [f64; 3], t: f64) -> [f64; 3] {
28    [a[0] * t, a[1] * t, a[2] * t]
29}
30#[inline]
31pub(super) fn length(a: [f64; 3]) -> f64 {
32    dot(a, a).sqrt()
33}
34#[inline]
35pub(super) fn normalize(a: [f64; 3]) -> [f64; 3] {
36    let l = length(a);
37    if l > 1e-15 {
38        scale(a, 1.0 / l)
39    } else {
40        [0.0, 0.0, 0.0]
41    }
42}
43
44#[inline]
45pub(super) fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
46    x.max(lo).min(hi)
47}
48#[inline]
49pub(super) fn mix(a: f64, b: f64, t: f64) -> f64 {
50    a * (1.0 - t) + b * t
51}
52#[inline]
53pub(super) fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
54    [
55        a[0] + t * (b[0] - a[0]),
56        a[1] + t * (b[1] - a[1]),
57        a[2] + t * (b[2] - a[2]),
58    ]
59}
60/// Signed distance field interface.
61///
62/// `sdf` returns a negative value inside the surface, 0 on the surface, and a
63/// positive value outside.  `gradient` returns the unit-outward normal (or an
64/// approximation thereof).
65pub trait ImplicitSurface: Send + Sync {
66    /// Signed distance from `p` to the nearest point on the surface.
67    fn sdf(&self, p: [f64; 3]) -> f64;
68    /// Gradient of the SDF at `p` (should be approximately unit-length on the surface).
69    fn gradient(&self, p: [f64; 3]) -> [f64; 3];
70}
71/// Sphere-march a ray against an implicit surface.
72///
73/// Advances along `ray_dir` by `sdf(current_pos)` at each step.  Returns
74/// `Some((hit_point, surface_normal))` when the ray comes within `1e-5` of the
75/// surface, or `None` if `max_dist` or `max_steps` is exceeded.
76///
77/// The normal is computed via central differences of the SDF.
78pub fn sphere_march(
79    surface: &dyn ImplicitSurface,
80    ray_origin: [f64; 3],
81    ray_dir: [f64; 3],
82    max_dist: f64,
83    max_steps: usize,
84) -> Option<([f64; 3], [f64; 3])> {
85    pub(super) const HIT_EPS: f64 = 1e-5;
86    let dir = normalize(ray_dir);
87    let mut t = 0.0_f64;
88    for _ in 0..max_steps {
89        let p = add(ray_origin, scale(dir, t));
90        let d = surface.sdf(p);
91        if d.abs() < HIT_EPS {
92            let normal = central_diff_normal(surface, p);
93            return Some((p, normal));
94        }
95        if d < 0.0 {
96            t -= d.abs();
97            let p2 = add(ray_origin, scale(dir, t));
98            let normal = central_diff_normal(surface, p2);
99            return Some((p2, normal));
100        }
101        t += d;
102        if t > max_dist {
103            return None;
104        }
105    }
106    None
107}
108/// Enhanced sphere march with over-relaxation for faster convergence.
109///
110/// Uses a relaxation factor `omega > 1` to take larger steps when safe.
111pub fn sphere_march_relaxed(
112    surface: &dyn ImplicitSurface,
113    ray_origin: [f64; 3],
114    ray_dir: [f64; 3],
115    max_dist: f64,
116    max_steps: usize,
117    omega: f64,
118) -> Option<([f64; 3], [f64; 3])> {
119    pub(super) const HIT_EPS: f64 = 1e-5;
120    let dir = normalize(ray_dir);
121    let mut t = 0.0_f64;
122    let mut prev_d = f64::MAX;
123    for _ in 0..max_steps {
124        let p = add(ray_origin, scale(dir, t));
125        let d = surface.sdf(p);
126        if d.abs() < HIT_EPS {
127            let normal = central_diff_normal(surface, p);
128            return Some((p, normal));
129        }
130        if d < 0.0 {
131            t -= d.abs();
132            let p2 = add(ray_origin, scale(dir, t));
133            let normal = central_diff_normal(surface, p2);
134            return Some((p2, normal));
135        }
136        let step = if d < prev_d { d * omega } else { d };
137        t += step;
138        prev_d = d;
139        if t > max_dist {
140            return None;
141        }
142    }
143    None
144}
145/// Compute the SDF gradient at `p` via central differences.
146pub(super) fn central_diff_normal(surface: &dyn ImplicitSurface, p: [f64; 3]) -> [f64; 3] {
147    pub(super) const EPS: f64 = 1e-4;
148    let dx = surface.sdf([p[0] + EPS, p[1], p[2]]) - surface.sdf([p[0] - EPS, p[1], p[2]]);
149    let dy = surface.sdf([p[0], p[1] + EPS, p[2]]) - surface.sdf([p[0], p[1] - EPS, p[2]]);
150    let dz = surface.sdf([p[0], p[1], p[2] + EPS]) - surface.sdf([p[0], p[1], p[2] - EPS]);
151    normalize([dx, dy, dz])
152}
153/// Test whether a ray hits a CSG object using sphere marching.
154///
155/// Returns the hit distance along the ray, or `None` on miss.
156pub fn csg_ray_test(
157    surface: &dyn ImplicitSurface,
158    ray_origin: [f64; 3],
159    ray_dir: [f64; 3],
160    max_dist: f64,
161) -> Option<f64> {
162    let dir = normalize(ray_dir);
163    sphere_march(surface, ray_origin, dir, max_dist, 256).map(|(hit, _)| {
164        let d = sub(hit, ray_origin);
165        length(d)
166    })
167}
168/// Test whether a point is inside a CSG object (SDF < 0).
169pub fn csg_contains_point(surface: &dyn ImplicitSurface, point: [f64; 3]) -> bool {
170    surface.sdf(point) < 0.0
171}
172/// Classify a point with respect to a plane defined by `normal` and `d`.
173pub fn classify_point(point: [f64; 3], normal: [f64; 3], d: f64, tolerance: f64) -> PlaneSide {
174    let dist = dot(point, normal) - d;
175    if dist > tolerance {
176        PlaneSide::Front
177    } else if dist < -tolerance {
178        PlaneSide::Back
179    } else {
180        PlaneSide::OnPlane
181    }
182}
183/// Classify an entire polygon (triangle) with respect to a plane.
184///
185/// Returns `(front_count, back_count, on_count)`.
186pub fn classify_triangle(
187    v0: [f64; 3],
188    v1: [f64; 3],
189    v2: [f64; 3],
190    normal: [f64; 3],
191    d: f64,
192    tolerance: f64,
193) -> (usize, usize, usize) {
194    let sides = [
195        classify_point(v0, normal, d, tolerance),
196        classify_point(v1, normal, d, tolerance),
197        classify_point(v2, normal, d, tolerance),
198    ];
199    let front = sides.iter().filter(|&&s| s == PlaneSide::Front).count();
200    let back = sides.iter().filter(|&&s| s == PlaneSide::Back).count();
201    let on = sides.iter().filter(|&&s| s == PlaneSide::OnPlane).count();
202    (front, back, on)
203}
204/// Clip a polygon (list of vertices) against a plane.
205///
206/// Returns the vertices on the front side (including intersections with the plane).
207/// Uses the Sutherland-Hodgman algorithm variant.
208pub fn clip_polygon_front(vertices: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
209    if vertices.is_empty() {
210        return vec![];
211    }
212    let mut output = Vec::new();
213    let n = vertices.len();
214    for i in 0..n {
215        let current = vertices[i];
216        let next = vertices[(i + 1) % n];
217        let d_current = dot(current, normal) - d;
218        let d_next = dot(next, normal) - d;
219        if d_current >= 0.0 {
220            output.push(current);
221            if d_next < 0.0 {
222                let t = d_current / (d_current - d_next);
223                output.push(lerp3(current, next, t));
224            }
225        } else if d_next >= 0.0 {
226            let t = d_current / (d_current - d_next);
227            output.push(lerp3(current, next, t));
228        }
229    }
230    output
231}
232/// Clip a polygon against a plane, keeping the back side.
233pub fn clip_polygon_back(vertices: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
234    clip_polygon_front(vertices, [-normal[0], -normal[1], -normal[2]], -d)
235}
236/// Extract surface mesh vertices and triangle indices from an SDF using a
237/// simplified marching-cubes approach on a regular grid.
238///
239/// Returns `(vertices, triangles)` where each triangle is `[i0, i1, i2]`.
240/// This is a simplified version that only detects sign changes along edges.
241pub fn extract_mesh_simple(
242    surface: &dyn ImplicitSurface,
243    bounds_min: [f64; 3],
244    bounds_max: [f64; 3],
245    resolution: usize,
246) -> VertsTris {
247    let n = resolution.max(2);
248    let step = [
249        (bounds_max[0] - bounds_min[0]) / n as f64,
250        (bounds_max[1] - bounds_min[1]) / n as f64,
251        (bounds_max[2] - bounds_min[2]) / n as f64,
252    ];
253    let mut vertices = Vec::new();
254    let mut triangles = Vec::new();
255    for ix in 0..n {
256        for iy in 0..n {
257            for iz in 0..n {
258                let x0 = bounds_min[0] + ix as f64 * step[0];
259                let y0 = bounds_min[1] + iy as f64 * step[1];
260                let z0 = bounds_min[2] + iz as f64 * step[2];
261                let corners = [
262                    [x0, y0, z0],
263                    [x0 + step[0], y0, z0],
264                    [x0 + step[0], y0 + step[1], z0],
265                    [x0, y0 + step[1], z0],
266                ];
267                let vals: Vec<f64> = corners.iter().map(|&c| surface.sdf(c)).collect();
268                let edges = [(0, 1), (1, 2), (2, 3), (3, 0)];
269                let mut edge_verts = Vec::new();
270                for &(a, b) in &edges {
271                    if vals[a].signum() != vals[b].signum() {
272                        let t = vals[a] / (vals[a] - vals[b]);
273                        let point = lerp3(corners[a], corners[b], t);
274                        let vi = vertices.len();
275                        vertices.push(point);
276                        edge_verts.push(vi);
277                    }
278                }
279                if edge_verts.len() >= 3 {
280                    for i in 1..edge_verts.len() - 1 {
281                        triangles.push([edge_verts[0], edge_verts[i], edge_verts[i + 1]]);
282                    }
283                }
284            }
285        }
286    }
287    (vertices, triangles)
288}
289/// Sample a regular grid over the given bounding box and return all grid
290/// points where `|sdf(p)| < threshold`.
291///
292/// `resolution` is the number of divisions per axis, so the total number of
293/// probed points is `(resolution + 1)^3`.
294///
295/// The threshold is chosen as half the grid spacing to capture at least one
296/// layer of points near the zero-level set.
297pub fn sample_surface_points(
298    surface: &dyn ImplicitSurface,
299    bounds_min: [f64; 3],
300    bounds_max: [f64; 3],
301    resolution: usize,
302) -> Vec<[f64; 3]> {
303    let n = resolution.max(1);
304    let step = [
305        (bounds_max[0] - bounds_min[0]) / n as f64,
306        (bounds_max[1] - bounds_min[1]) / n as f64,
307        (bounds_max[2] - bounds_min[2]) / n as f64,
308    ];
309    let threshold = step[0].max(step[1]).max(step[2]) * 0.55;
310    let mut points = Vec::new();
311    for ix in 0..=n {
312        let x = bounds_min[0] + ix as f64 * step[0];
313        for iy in 0..=n {
314            let y = bounds_min[1] + iy as f64 * step[1];
315            for iz in 0..=n {
316                let z = bounds_min[2] + iz as f64 * step[2];
317                let p = [x, y, z];
318                if surface.sdf(p).abs() < threshold {
319                    points.push(p);
320                }
321            }
322        }
323    }
324    points
325}
326/// Sample surface points with projected normals.
327///
328/// Returns `(point, normal)` pairs for all near-surface grid points.
329pub fn sample_surface_points_with_normals(
330    surface: &dyn ImplicitSurface,
331    bounds_min: [f64; 3],
332    bounds_max: [f64; 3],
333    resolution: usize,
334) -> PointNormals {
335    let points = sample_surface_points(surface, bounds_min, bounds_max, resolution);
336    points
337        .into_iter()
338        .map(|p| {
339            let n = surface.gradient(p);
340            (p, n)
341        })
342        .collect()
343}
344/// Compute the approximate volume of the interior region using grid sampling.
345///
346/// Returns the volume estimate (count of interior cells * cell_volume).
347pub fn estimate_volume(
348    surface: &dyn ImplicitSurface,
349    bounds_min: [f64; 3],
350    bounds_max: [f64; 3],
351    resolution: usize,
352) -> f64 {
353    let n = resolution.max(1);
354    let step = [
355        (bounds_max[0] - bounds_min[0]) / n as f64,
356        (bounds_max[1] - bounds_min[1]) / n as f64,
357        (bounds_max[2] - bounds_min[2]) / n as f64,
358    ];
359    let cell_volume = step[0] * step[1] * step[2];
360    let mut count = 0usize;
361    for ix in 0..=n {
362        let x = bounds_min[0] + ix as f64 * step[0];
363        for iy in 0..=n {
364            let y = bounds_min[1] + iy as f64 * step[1];
365            for iz in 0..=n {
366                let z = bounds_min[2] + iz as f64 * step[2];
367                if surface.sdf([x, y, z]) < 0.0 {
368                    count += 1;
369                }
370            }
371        }
372    }
373    count as f64 * cell_volume
374}
375/// Evaluate the SDF of a `CsgTree` at point `p`.
376///
377/// Recursively descends the tree, combining child SDF values according to the
378/// boolean operation at each interior node.
379pub fn csg_sdf(node: &CsgTree, p: [f64; 3]) -> f64 {
380    match node {
381        CsgTree::Leaf(s) => s.sdf(p),
382        CsgTree::Node { op, left, right } => {
383            let da = csg_sdf(left, p);
384            let db = csg_sdf(right, p);
385            match op {
386                CsgOp::Union => da.min(db),
387                CsgOp::Intersection => da.max(db),
388                CsgOp::Difference => da.max(-db),
389            }
390        }
391    }
392}
393/// Compute the gradient (approximate unit normal) of a `CsgTree` SDF at `p`.
394///
395/// Uses central differences of [`csg_sdf`].
396pub fn csg_gradient(node: &CsgTree, p: [f64; 3]) -> [f64; 3] {
397    pub(super) const EPS: f64 = 1e-4;
398    let dx = csg_sdf(node, [p[0] + EPS, p[1], p[2]]) - csg_sdf(node, [p[0] - EPS, p[1], p[2]]);
399    let dy = csg_sdf(node, [p[0], p[1] + EPS, p[2]]) - csg_sdf(node, [p[0], p[1] - EPS, p[2]]);
400    let dz = csg_sdf(node, [p[0], p[1], p[2] + EPS]) - csg_sdf(node, [p[0], p[1], p[2] - EPS]);
401    normalize([dx, dy, dz])
402}
403/// Sphere-march a ray against a `CsgTree`.
404///
405/// Advances the ray by `csg_sdf(node, current_pos)` at each step.
406/// Returns `Some(t)` — the ray parameter of the hit — when the SDF drops below
407/// `1e-4`, or `None` if `max_dist` or `max_steps` is exceeded.
408pub fn csg_ray_march(
409    node: &CsgTree,
410    origin: [f64; 3],
411    dir: [f64; 3],
412    max_dist: f64,
413    max_steps: usize,
414) -> Option<f64> {
415    pub(super) const HIT_EPS: f64 = 1e-4;
416    let d = normalize(dir);
417    let mut t = 0.0_f64;
418    for _ in 0..max_steps {
419        let p = add(origin, scale(d, t));
420        let dist = csg_sdf(node, p);
421        if dist.abs() < HIT_EPS {
422            return Some(t);
423        }
424        if dist < 0.0 {
425            return Some(t);
426        }
427        t += dist;
428        if t > max_dist {
429            return None;
430        }
431    }
432    None
433}
434/// Test whether a world-space point is inside a `CsgTree` (SDF < 0).
435pub fn csg_tree_contains(node: &CsgTree, p: [f64; 3]) -> bool {
436    csg_sdf(node, p) < 0.0
437}
438/// Estimate the surface area of a `CsgTree` solid by Monte-Carlo sampling.
439///
440/// Samples `n_samples` random rays from random directions and accumulates
441/// the area of the surface patches they hit.  The result is an unbiased
442/// estimator of the surface area when `n_samples` is large.
443///
444/// In practice we use a simple grid-marching approach: scan a uniform grid,
445/// detect sign changes across adjacent cells, and count the approximate
446/// surface patch area for each crossing.
447pub fn csg_surface_area(
448    node: &CsgTree,
449    bbox_min: [f64; 3],
450    bbox_max: [f64; 3],
451    n_div: usize,
452) -> f64 {
453    let n = n_div.max(2);
454    let step = [
455        (bbox_max[0] - bbox_min[0]) / n as f64,
456        (bbox_max[1] - bbox_min[1]) / n as f64,
457        (bbox_max[2] - bbox_min[2]) / n as f64,
458    ];
459    let cell_area_x = step[1] * step[2];
460    let cell_area_y = step[0] * step[2];
461    let cell_area_z = step[0] * step[1];
462    let mut area = 0.0_f64;
463    for ix in 0..n {
464        for iy in 0..n {
465            for iz in 0..n {
466                let p = [
467                    bbox_min[0] + (ix as f64 + 0.5) * step[0],
468                    bbox_min[1] + (iy as f64 + 0.5) * step[1],
469                    bbox_min[2] + (iz as f64 + 0.5) * step[2],
470                ];
471                let d = csg_sdf(node, p);
472                if ix + 1 < n {
473                    let pn = [p[0] + step[0], p[1], p[2]];
474                    let dn = csg_sdf(node, pn);
475                    if d * dn < 0.0 {
476                        area += cell_area_x;
477                    }
478                }
479                if iy + 1 < n {
480                    let pn = [p[0], p[1] + step[1], p[2]];
481                    let dn = csg_sdf(node, pn);
482                    if d * dn < 0.0 {
483                        area += cell_area_y;
484                    }
485                }
486                if iz + 1 < n {
487                    let pn = [p[0], p[1], p[2] + step[2]];
488                    let dn = csg_sdf(node, pn);
489                    if d * dn < 0.0 {
490                        area += cell_area_z;
491                    }
492                }
493            }
494        }
495    }
496    area
497}
498/// Evaluate the SDF of an offset `CsgTree` at point `p`.
499///
500/// `f_offset(p) = csg_sdf(node, p) - offset`
501pub fn csg_offset_sdf(node: &CsgTree, p: [f64; 3], offset: f64) -> f64 {
502    csg_sdf(node, p) - offset
503}
504/// Returns `true` if a point lies inside the offset surface.
505pub fn csg_offset_contains(node: &CsgTree, p: [f64; 3], offset: f64) -> bool {
506    csg_offset_sdf(node, p, offset) < 0.0
507}
508/// Compute the gradient of the offset SDF (same direction as the original,
509/// since the offset is a constant shift of the level set).
510pub fn csg_offset_gradient(node: &CsgTree, p: [f64; 3]) -> [f64; 3] {
511    csg_gradient(node, p)
512}
513/// A simplified representation of a `CsgTree` — here we perform constant
514/// folding: if the root is a Union/Intersection/Difference of two leaves
515/// whose bounding boxes do not overlap, we can determine the result
516/// analytically.
517///
518/// Returns `true` if the tree was recognized as a trivially-simplified form
519/// (the caller can then use a simpler primitive).  This is a best-effort
520/// static analysis pass.
521///
522/// Strategy:
523/// - A `Union(A, B)` where neither A nor B can overlap (conservative check
524///   via sample points) collapses to the one that is "larger".
525/// - An `Intersection(A, B)` where the two are disjoint collapses to empty.
526///
527/// For the general case this returns `false` and the original tree is used.
528pub fn csg_tree_simplify_check(node: &CsgTree, probe_center: [f64; 3]) -> bool {
529    match node {
530        CsgTree::Leaf(_) => true,
531        CsgTree::Node { op, left, right } => {
532            let da = csg_sdf(left, probe_center);
533            let db = csg_sdf(right, probe_center);
534            match op {
535                CsgOp::Union => da < 0.0 || db < 0.0,
536                CsgOp::Intersection => da < 0.0 && db < 0.0,
537                CsgOp::Difference => da < 0.0 && db >= 0.0,
538            }
539        }
540    }
541}
542/// Recursive surface area estimator for a `CsgTree` using a user-supplied bounding box.
543///
544/// Delegates to `csg_surface_area` with a default resolution of 16 divisions.
545pub fn csg_node_surface_area(node: &CsgTree, bbox_min: [f64; 3], bbox_max: [f64; 3]) -> f64 {
546    csg_surface_area(node, bbox_min, bbox_max, 16)
547}
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use crate::csg::CsgDifference;
552    use crate::csg::CsgIntersection;
553    use crate::csg::CsgSmoothDifference;
554    use crate::csg::CsgSmoothIntersection;
555    use crate::csg::CsgSmoothUnion;
556
557    use crate::csg::CsgUnion;
558    use crate::csg::SdfBox;
559    use crate::csg::SdfCylinder;
560    use crate::csg::SdfSphere;
561    use crate::csg::SdfTorus;
562
563    #[test]
564    fn sphere_sdf_center_is_minus_radius() {
565        let s = SdfSphere::new([0.0, 0.0, 0.0], 2.0);
566        assert!((s.sdf([0.0, 0.0, 0.0]) - (-2.0)).abs() < 1e-12);
567    }
568    #[test]
569    fn sphere_sdf_on_surface_is_zero() {
570        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.5);
571        assert!(s.sdf([1.5, 0.0, 0.0]).abs() < 1e-12);
572    }
573    #[test]
574    fn sphere_sdf_outside_is_positive() {
575        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
576        assert!(s.sdf([3.0, 0.0, 0.0]) > 0.0);
577    }
578    #[test]
579    fn union_inside_either_sphere_is_negative() {
580        let a = Box::new(SdfSphere::new([-2.0, 0.0, 0.0], 1.0));
581        let b = Box::new(SdfSphere::new([2.0, 0.0, 0.0], 1.0));
582        let u = CsgUnion::new(a, b);
583        assert!(u.sdf([-2.0, 0.0, 0.0]) < 0.0);
584        assert!(u.sdf([2.0, 0.0, 0.0]) < 0.0);
585        assert!(u.sdf([0.0, 0.0, 0.0]) > 0.0);
586    }
587    #[test]
588    fn intersection_only_inside_both_is_negative() {
589        let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
590        let b = Box::new(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
591        let i = CsgIntersection::new(a, b);
592        assert!(i.sdf([0.5, 0.0, 0.0]) < 0.0);
593        assert!(i.sdf([-1.5, 0.0, 0.0]) > 0.0);
594        assert!(i.sdf([2.5, 0.0, 0.0]) > 0.0);
595    }
596    #[test]
597    fn difference_carved_out_region_is_positive() {
598        let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
599        let b = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
600        let d = CsgDifference::new(a, b);
601        assert!(d.sdf([0.0, 0.0, 0.0]) > 0.0);
602        assert!(d.sdf([2.5, 0.0, 0.0]) < 0.0);
603        assert!(d.sdf([5.0, 0.0, 0.0]) > 0.0);
604    }
605    #[test]
606    fn smooth_union_blends_surfaces() {
607        let a = Box::new(SdfSphere::new([-0.5, 0.0, 0.0], 1.0));
608        let b = Box::new(SdfSphere::new([0.5, 0.0, 0.0], 1.0));
609        let su = CsgSmoothUnion::new(a, b, 0.5);
610        let sharp_a = Box::new(SdfSphere::new([-0.5, 0.0, 0.0], 1.0));
611        let sharp_b = Box::new(SdfSphere::new([0.5, 0.0, 0.0], 1.0));
612        let sharp = CsgUnion::new(sharp_a, sharp_b);
613        assert!(su.sdf([0.0, 0.0, 0.0]) <= sharp.sdf([0.0, 0.0, 0.0]) + 1e-10);
614    }
615    #[test]
616    fn smooth_intersection_blends() {
617        let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
618        let b = Box::new(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
619        let si = CsgSmoothIntersection::new(a, b, 0.3);
620        assert!(si.sdf([0.5, 0.0, 0.0]) < 0.0);
621    }
622    #[test]
623    fn smooth_difference_carves() {
624        let a = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
625        let b = Box::new(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
626        let sd = CsgSmoothDifference::new(a, b, 0.2);
627        assert!(sd.sdf([0.0, 0.0, 0.0]) > 0.0);
628        assert!(sd.sdf([2.5, 0.0, 0.0]) < 0.0);
629    }
630    #[test]
631    fn sphere_march_hits_sphere() {
632        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
633        let origin = [-5.0, 0.0, 0.0];
634        let dir = [1.0, 0.0, 0.0];
635        let result = sphere_march(&s, origin, dir, 100.0, 256);
636        assert!(result.is_some(), "expected a hit");
637        let (hit, _normal) = result.unwrap();
638        assert!(
639            s.sdf(hit).abs() < 1e-4,
640            "hit point not on surface: sdf={}",
641            s.sdf(hit)
642        );
643    }
644    #[test]
645    fn sphere_march_misses_sphere() {
646        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
647        let origin = [-5.0, 5.0, 0.0];
648        let dir = [1.0, 0.0, 0.0];
649        let result = sphere_march(&s, origin, dir, 20.0, 256);
650        assert!(result.is_none());
651    }
652    #[test]
653    fn sphere_march_relaxed_hits() {
654        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
655        let result = sphere_march_relaxed(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 256, 1.5);
656        assert!(result.is_some());
657    }
658    #[test]
659    fn box_sdf_corner_region() {
660        let b = SdfBox::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
661        assert!(b.sdf([0.0, 0.0, 0.0]) < 0.0);
662        assert!(b.sdf([1.0, 0.0, 0.0]).abs() < 1e-12);
663        let p = [2.0, 2.0, 2.0];
664        let expected = ((1.0_f64).powi(2) * 3.0).sqrt();
665        assert!((b.sdf(p) - expected).abs() < 1e-10);
666    }
667    #[test]
668    fn cylinder_sdf_on_axis() {
669        let c = SdfCylinder::new([0.0, 0.0, 0.0], 1.0);
670        assert!((c.sdf([0.0, 0.0, 0.0]) - (-1.0)).abs() < 1e-12);
671    }
672    #[test]
673    fn cylinder_sdf_on_surface() {
674        let c = SdfCylinder::new([0.0, 0.0, 0.0], 1.0);
675        assert!(c.sdf([1.0, 5.0, 0.0]).abs() < 1e-12);
676    }
677    #[test]
678    fn torus_sdf_on_surface() {
679        let t = SdfTorus::new(2.0, 0.5);
680        assert!(t.sdf([2.5, 0.0, 0.0]).abs() < 1e-12);
681        assert!(t.sdf([1.5, 0.0, 0.0]).abs() < 1e-12);
682    }
683    #[test]
684    fn sample_surface_points_returns_nonempty_for_sphere() {
685        let s = SdfSphere::new([0.0, 0.0, 0.0], 0.5);
686        let pts = sample_surface_points(&s, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0], 20);
687        assert!(!pts.is_empty(), "expected surface points, got none");
688    }
689    #[test]
690    fn sample_surface_points_with_normals_has_unit_normals() {
691        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
692        let pts = sample_surface_points_with_normals(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 10);
693        for (_, n) in &pts {
694            let len = length(*n);
695            assert!((len - 1.0).abs() < 0.1, "normal length = {len}");
696        }
697    }
698    #[test]
699    fn estimate_volume_sphere() {
700        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
701        let vol = estimate_volume(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 20);
702        let expected = 4.0 / 3.0 * std::f64::consts::PI;
703        assert!(
704            (vol - expected).abs() < 1.0,
705            "estimated volume = {vol}, expected ~ {expected}"
706        );
707    }
708    #[test]
709    fn csg_ray_test_hits() {
710        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
711        let dist = csg_ray_test(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
712        assert!(dist.is_some());
713        let d = dist.unwrap();
714        assert!((d - 4.0).abs() < 0.1, "hit distance = {d}");
715    }
716    #[test]
717    fn csg_contains_point_inside() {
718        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
719        assert!(csg_contains_point(&s, [0.0, 0.0, 0.0]));
720        assert!(!csg_contains_point(&s, [2.0, 0.0, 0.0]));
721    }
722    #[test]
723    fn classify_point_front_back() {
724        let normal = [0.0, 1.0, 0.0];
725        let d = 0.0;
726        assert_eq!(
727            classify_point([0.0, 1.0, 0.0], normal, d, 1e-6),
728            PlaneSide::Front
729        );
730        assert_eq!(
731            classify_point([0.0, -1.0, 0.0], normal, d, 1e-6),
732            PlaneSide::Back
733        );
734        assert_eq!(
735            classify_point([0.0, 0.0, 0.0], normal, d, 1e-6),
736            PlaneSide::OnPlane
737        );
738    }
739    #[test]
740    fn classify_triangle_straddling() {
741        let normal = [0.0, 1.0, 0.0];
742        let d = 0.0;
743        let (f, b, _on) = classify_triangle(
744            [0.0, 1.0, 0.0],
745            [0.0, -1.0, 0.0],
746            [1.0, 0.0, 0.0],
747            normal,
748            d,
749            1e-6,
750        );
751        assert_eq!(f, 1);
752        assert_eq!(b, 1);
753    }
754    #[test]
755    fn clip_polygon_front_basic() {
756        let tri = [[0.0, 1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, -1.0, 0.0]];
757        let result = clip_polygon_front(&tri, [0.0, 1.0, 0.0], 0.0);
758        assert!(
759            result.len() >= 3,
760            "clipped polygon has {} verts",
761            result.len()
762        );
763        for v in &result {
764            assert!(dot(*v, [0.0, 1.0, 0.0]) >= -1e-10);
765        }
766    }
767    #[test]
768    fn clip_polygon_back_basic() {
769        let tri = [[0.0, 1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, -1.0, 0.0]];
770        let result = clip_polygon_back(&tri, [0.0, 1.0, 0.0], 0.0);
771        assert!(result.len() >= 3);
772        for v in &result {
773            assert!(dot(*v, [0.0, 1.0, 0.0]) <= 1e-10);
774        }
775    }
776    #[test]
777    fn extract_mesh_simple_nonempty() {
778        let s = SdfSphere::new([0.0, 0.0, 0.0], 1.0);
779        let (verts, _tris) = extract_mesh_simple(&s, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5], 20);
780        assert!(!verts.is_empty(), "mesh has no vertices");
781    }
782}
783#[cfg(test)]
784mod tests_csg_tree {
785
786    use crate::csg::CsgTree;
787
788    use crate::csg::SdfSphere;
789
790    use crate::csg::csg_gradient;
791    use crate::csg::csg_ray_march;
792    use crate::csg::csg_sdf;
793    use crate::csg::csg_tree_contains;
794    #[test]
795    fn csg_tree_leaf_sphere_sdf() {
796        let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
797        assert!(csg_sdf(&tree, [0.0, 0.0, 0.0]) < 0.0);
798        assert!(csg_sdf(&tree, [3.0, 0.0, 0.0]) > 0.0);
799        assert!(csg_sdf(&tree, [1.0, 0.0, 0.0]).abs() < 1e-12);
800    }
801    #[test]
802    fn csg_tree_union_inside_either() {
803        let a = CsgTree::leaf(SdfSphere::new([-2.0, 0.0, 0.0], 1.0));
804        let b = CsgTree::leaf(SdfSphere::new([2.0, 0.0, 0.0], 1.0));
805        let u = CsgTree::union(a, b);
806        assert!(csg_sdf(&u, [-2.0, 0.0, 0.0]) < 0.0);
807        assert!(csg_sdf(&u, [2.0, 0.0, 0.0]) < 0.0);
808        assert!(csg_sdf(&u, [0.0, 0.0, 0.0]) > 0.0);
809    }
810    #[test]
811    fn csg_tree_intersection_only_shared_region() {
812        let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
813        let b = CsgTree::leaf(SdfSphere::new([1.0, 0.0, 0.0], 2.0));
814        let i = CsgTree::intersection(a, b);
815        assert!(csg_sdf(&i, [0.5, 0.0, 0.0]) < 0.0);
816        assert!(csg_sdf(&i, [-1.5, 0.0, 0.0]) > 0.0);
817        assert!(csg_sdf(&i, [2.5, 0.0, 0.0]) > 0.0);
818    }
819    #[test]
820    fn csg_tree_difference_carves_out() {
821        let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
822        let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
823        let d = CsgTree::difference(a, b);
824        assert!(csg_sdf(&d, [0.0, 0.0, 0.0]) > 0.0);
825        assert!(csg_sdf(&d, [2.5, 0.0, 0.0]) < 0.0);
826        assert!(csg_sdf(&d, [5.0, 0.0, 0.0]) > 0.0);
827    }
828    #[test]
829    fn csg_tree_nested_union_of_three_spheres() {
830        let a = CsgTree::leaf(SdfSphere::new([-3.0, 0.0, 0.0], 1.0));
831        let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
832        let c = CsgTree::leaf(SdfSphere::new([3.0, 0.0, 0.0], 1.0));
833        let ab = CsgTree::union(a, b);
834        let abc = CsgTree::union(ab, c);
835        assert!(csg_sdf(&abc, [-3.0, 0.0, 0.0]) < 0.0);
836        assert!(csg_sdf(&abc, [0.0, 0.0, 0.0]) < 0.0);
837        assert!(csg_sdf(&abc, [3.0, 0.0, 0.0]) < 0.0);
838        assert!(csg_sdf(&abc, [1.5, 0.0, 0.0]) > 0.0);
839    }
840    #[test]
841    fn csg_gradient_leaf_sphere_is_unit() {
842        let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
843        let g = csg_gradient(&tree, [2.0, 0.0, 0.0]);
844        let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
845        assert!((len - 1.0).abs() < 0.01, "gradient length = {len}");
846    }
847    #[test]
848    fn csg_ray_march_hits_sphere() {
849        let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
850        let t = csg_ray_march(&tree, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 256);
851        assert!(t.is_some(), "should hit sphere");
852        let t = t.unwrap();
853        assert!((t - 4.0).abs() < 0.1, "expected t≈4, got {t}");
854    }
855    #[test]
856    fn csg_ray_march_misses_sphere() {
857        let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.0));
858        let t = csg_ray_march(&tree, [-5.0, 10.0, 0.0], [1.0, 0.0, 0.0], 20.0, 256);
859        assert!(t.is_none(), "should miss sphere");
860    }
861    #[test]
862    fn csg_ray_march_union_hits_either() {
863        let a = CsgTree::leaf(SdfSphere::new([-5.0, 0.0, 0.0], 1.0));
864        let b = CsgTree::leaf(SdfSphere::new([5.0, 0.0, 0.0], 1.0));
865        let u = CsgTree::union(a, b);
866        let t = csg_ray_march(&u, [-10.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0, 512);
867        assert!(t.is_some(), "should hit union");
868    }
869    #[test]
870    fn csg_ray_march_difference_misses_carved_region() {
871        let big = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
872        let cut = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.9));
873        let shell = CsgTree::difference(big, cut);
874        let t = csg_ray_march(&shell, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 20.0, 512);
875        assert!(t.is_some(), "should hit shell");
876    }
877    #[test]
878    fn csg_tree_contains_inside() {
879        let tree = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 2.0));
880        assert!(csg_tree_contains(&tree, [0.0, 0.0, 0.0]));
881        assert!(!csg_tree_contains(&tree, [3.0, 0.0, 0.0]));
882    }
883    #[test]
884    fn csg_tree_contains_difference() {
885        let a = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 3.0));
886        let b = CsgTree::leaf(SdfSphere::new([0.0, 0.0, 0.0], 1.5));
887        let d = CsgTree::difference(a, b);
888        assert!(!csg_tree_contains(&d, [0.0, 0.0, 0.0]));
889        assert!(csg_tree_contains(&d, [2.5, 0.0, 0.0]));
890    }
891}
892/// Compute a conservative axis-aligned bounding box for a `CsgTree`.
893///
894/// For leaf nodes, the bounding box must be provided via a user-supplied
895/// `bounds` function.  For combination nodes:
896/// - **Union**: union of child bounding boxes.
897/// - **Intersection**: intersection of child bounding boxes (may be empty).
898/// - **Difference**: use the left child's bounding box (conservative).
899pub fn csg_tree_aabb<F>(node: &CsgTree, bounds: &F) -> ([f64; 3], [f64; 3])
900where
901    F: Fn(&dyn ImplicitSurface) -> SurfaceAabb,
902{
903    match node {
904        CsgTree::Leaf(s) => bounds(s.as_ref()),
905        CsgTree::Node { op, left, right } => {
906            let (l_min, l_max) = csg_tree_aabb(left, bounds);
907            let (r_min, r_max) = csg_tree_aabb(right, bounds);
908            match op {
909                CsgOp::Union => {
910                    let mn = [
911                        l_min[0].min(r_min[0]),
912                        l_min[1].min(r_min[1]),
913                        l_min[2].min(r_min[2]),
914                    ];
915                    let mx = [
916                        l_max[0].max(r_max[0]),
917                        l_max[1].max(r_max[1]),
918                        l_max[2].max(r_max[2]),
919                    ];
920                    (mn, mx)
921                }
922                CsgOp::Intersection => {
923                    let mn = [
924                        l_min[0].max(r_min[0]),
925                        l_min[1].max(r_min[1]),
926                        l_min[2].max(r_min[2]),
927                    ];
928                    let mx = [
929                        l_max[0].min(r_max[0]),
930                        l_max[1].min(r_max[1]),
931                        l_max[2].min(r_max[2]),
932                    ];
933                    (mn, mx)
934                }
935                CsgOp::Difference => (l_min, l_max),
936            }
937        }
938    }
939}
940/// Sample an SDF on a regular grid and collect all `MarchingCell`s that
941/// contain a surface crossing.
942///
943/// `resolution` is the number of cells per axis.  The total sampled cells
944/// is `resolution³`.
945pub fn marching_cubes_sample(
946    surface: &dyn ImplicitSurface,
947    bounds_min: [f64; 3],
948    bounds_max: [f64; 3],
949    resolution: usize,
950) -> Vec<MarchingCell> {
951    let n = resolution.max(2);
952    let step_x = (bounds_max[0] - bounds_min[0]) / n as f64;
953    let step_y = (bounds_max[1] - bounds_min[1]) / n as f64;
954    let step_z = (bounds_max[2] - bounds_min[2]) / n as f64;
955    let step = step_x.min(step_y).min(step_z);
956    let offsets: [[f64; 3]; 8] = [
957        [0.0, 0.0, 0.0],
958        [step, 0.0, 0.0],
959        [step, step, 0.0],
960        [0.0, step, 0.0],
961        [0.0, 0.0, step],
962        [step, 0.0, step],
963        [step, step, step],
964        [0.0, step, step],
965    ];
966    let mut cells = Vec::new();
967    for ix in 0..n {
968        let x0 = bounds_min[0] + ix as f64 * step;
969        for iy in 0..n {
970            let y0 = bounds_min[1] + iy as f64 * step;
971            for iz in 0..n {
972                let z0 = bounds_min[2] + iz as f64 * step;
973                let cell_min = [x0, y0, z0];
974                let mut corner_values = [0.0f64; 8];
975                for (k, &off) in offsets.iter().enumerate() {
976                    let p = add(cell_min, off);
977                    corner_values[k] = surface.sdf(p);
978                }
979                let cell = MarchingCell {
980                    min: cell_min,
981                    step,
982                    corner_values,
983                };
984                if cell.has_surface() {
985                    cells.push(cell);
986                }
987            }
988        }
989    }
990    cells
991}
992/// Extract a flat list of surface vertex positions from a marching-cubes grid.
993///
994/// Returns all edge-crossing positions (one per sign-change edge per cell).
995pub fn marching_cubes_vertices(
996    surface: &dyn ImplicitSurface,
997    bounds_min: [f64; 3],
998    bounds_max: [f64; 3],
999    resolution: usize,
1000) -> Vec<[f64; 3]> {
1001    let cells = marching_cubes_sample(surface, bounds_min, bounds_max, resolution);
1002    cells.iter().flat_map(|c| c.extract_vertices()).collect()
1003}