Skip to main content

oxiphysics_geometry/csg/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::ImplicitSurface;
6use super::functions::*;
7
8/// An infinite plane: `dot(p, normal) - d = 0`.
9pub struct SdfPlane {
10    /// Unit outward normal.
11    pub normal: [f64; 3],
12    /// Plane offset: points with `dot(p, normal) > d` are outside.
13    pub d: f64,
14}
15impl SdfPlane {
16    /// Create a new plane.
17    pub fn new(normal: [f64; 3], d: f64) -> Self {
18        Self {
19            normal: normalize(normal),
20            d,
21        }
22    }
23}
24/// Smooth CSG difference.
25///
26/// SDF = smooth_max(sdf_a, -sdf_b, k).
27pub struct CsgSmoothDifference {
28    /// The base shape.
29    pub a: Box<dyn ImplicitSurface>,
30    /// The shape to subtract.
31    pub b: Box<dyn ImplicitSurface>,
32    /// Smoothing factor.
33    pub k: f64,
34}
35impl CsgSmoothDifference {
36    /// Create a new smooth difference.
37    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>, k: f64) -> Self {
38        Self { a, b, k }
39    }
40}
41/// A cone (infinite half-cone) opening upward from the origin.
42///
43/// Defined by its half-angle in radians. SDF is exact on the surface.
44pub struct SdfCone {
45    /// Apex position.
46    pub apex: [f64; 3],
47    /// Half-angle in radians.
48    pub half_angle: f64,
49    /// Height of the finite cone.
50    pub height: f64,
51}
52impl SdfCone {
53    /// Create a new cone.
54    pub fn new(apex: [f64; 3], half_angle: f64, height: f64) -> Self {
55        Self {
56            apex,
57            half_angle,
58            height,
59        }
60    }
61}
62/// A sphere defined by centre and radius.
63pub struct SdfSphere {
64    /// World-space centre.
65    pub center: [f64; 3],
66    /// Sphere radius (must be positive).
67    pub radius: f64,
68}
69impl SdfSphere {
70    /// Create a new sphere.
71    pub fn new(center: [f64; 3], radius: f64) -> Self {
72        Self { center, radius }
73    }
74}
75/// CSG intersection: the region inside both `a` and `b`.
76///
77/// SDF = max(sdf_a, sdf_b).
78pub struct CsgIntersection {
79    /// First operand.
80    pub a: Box<dyn ImplicitSurface>,
81    /// Second operand.
82    pub b: Box<dyn ImplicitSurface>,
83}
84impl CsgIntersection {
85    /// Create a new intersection.
86    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>) -> Self {
87        Self { a, b }
88    }
89}
90/// A node in a CSG tree.
91///
92/// Each node is either a leaf (wrapping an `ImplicitSurface`) or an interior
93/// node combining two subtrees with a `CsgOp`.
94pub enum CsgTree {
95    /// A leaf primitive.
96    Leaf(Box<dyn ImplicitSurface>),
97    /// An interior combination node.
98    Node {
99        /// The boolean operation to apply.
100        op: CsgOp,
101        /// Left operand.
102        left: Box<CsgTree>,
103        /// Right operand.
104        right: Box<CsgTree>,
105    },
106}
107impl CsgTree {
108    /// Construct a leaf node from any `ImplicitSurface`.
109    pub fn leaf(s: impl ImplicitSurface + 'static) -> Self {
110        CsgTree::Leaf(Box::new(s))
111    }
112    /// Construct a union node.
113    pub fn union(left: CsgTree, right: CsgTree) -> Self {
114        CsgTree::Node {
115            op: CsgOp::Union,
116            left: Box::new(left),
117            right: Box::new(right),
118        }
119    }
120    /// Construct an intersection node.
121    pub fn intersection(left: CsgTree, right: CsgTree) -> Self {
122        CsgTree::Node {
123            op: CsgOp::Intersection,
124            left: Box::new(left),
125            right: Box::new(right),
126        }
127    }
128    /// Construct a difference node (left minus right).
129    pub fn difference(left: CsgTree, right: CsgTree) -> Self {
130        CsgTree::Node {
131            op: CsgOp::Difference,
132            left: Box::new(left),
133            right: Box::new(right),
134        }
135    }
136}
137/// CSG union: the region inside either `a` or `b`.
138///
139/// SDF = min(sdf_a, sdf_b).
140pub struct CsgUnion {
141    /// First operand.
142    pub a: Box<dyn ImplicitSurface>,
143    /// Second operand.
144    pub b: Box<dyn ImplicitSurface>,
145}
146impl CsgUnion {
147    /// Create a new union.
148    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>) -> Self {
149        Self { a, b }
150    }
151}
152/// A torus centred at the origin lying in the XZ plane.
153pub struct SdfTorus {
154    /// Major radius (distance from centre to tube centre).
155    pub major_radius: f64,
156    /// Minor radius (tube radius).
157    pub minor_radius: f64,
158}
159impl SdfTorus {
160    /// Create a new torus.
161    pub fn new(major_radius: f64, minor_radius: f64) -> Self {
162        Self {
163            major_radius,
164            minor_radius,
165        }
166    }
167}
168/// CSG boolean operation type.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum CsgOp {
171    /// Union: region inside either child.
172    Union,
173    /// Intersection: region inside both children.
174    Intersection,
175    /// Difference: region inside the left child but outside the right child.
176    Difference,
177}
178/// A capsule aligned with the Y axis.
179pub struct SdfCapsule {
180    /// Center of the capsule.
181    pub center: [f64; 3],
182    /// Capsule radius.
183    pub radius: f64,
184    /// Half-height of the cylindrical section.
185    pub half_height: f64,
186}
187impl SdfCapsule {
188    /// Create a new capsule.
189    pub fn new(center: [f64; 3], radius: f64, half_height: f64) -> Self {
190        Self {
191            center,
192            radius,
193            half_height,
194        }
195    }
196}
197/// Offset a `CsgTree` surface inward (negative `offset`) or outward (positive).
198///
199/// Returns a wrapper `SDF f(p) = original_sdf(p) - offset` so that the
200/// zero-level set is shifted by `offset` in the normal direction.
201///
202/// The returned object implements `ImplicitSurface`.
203pub struct CsgOffsetSurface {
204    /// The inner SDF function evaluated at any point.
205    inner_sdf: Box<dyn Fn([f64; 3]) -> f64 + Send + Sync>,
206    /// Offset value (positive = expand outward, negative = shrink inward).
207    pub offset: f64,
208}
209impl CsgOffsetSurface {
210    /// Create an offset surface from a closure SDF and an offset distance.
211    pub fn new(sdf: Box<dyn Fn([f64; 3]) -> f64 + Send + Sync>, offset: f64) -> Self {
212        Self {
213            inner_sdf: sdf,
214            offset,
215        }
216    }
217    /// Evaluate the offset SDF: `inner_sdf(p) - offset`.
218    pub fn eval(&self, p: [f64; 3]) -> f64 {
219        (self.inner_sdf)(p) - self.offset
220    }
221    /// Gradient via central finite differences (eps = 1e-4).
222    pub fn finite_diff_gradient(&self, p: [f64; 3]) -> [f64; 3] {
223        const EPS: f64 = 1e-4;
224        [
225            (self.eval([p[0] + EPS, p[1], p[2]]) - self.eval([p[0] - EPS, p[1], p[2]]))
226                / (2.0 * EPS),
227            (self.eval([p[0], p[1] + EPS, p[2]]) - self.eval([p[0], p[1] - EPS, p[2]]))
228                / (2.0 * EPS),
229            (self.eval([p[0], p[1], p[2] + EPS]) - self.eval([p[0], p[1], p[2] - EPS]))
230                / (2.0 * EPS),
231        ]
232    }
233}
234impl ImplicitSurface for CsgOffsetSurface {
235    fn sdf(&self, p: [f64; 3]) -> f64 {
236        self.eval(p)
237    }
238    fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
239        self.finite_diff_gradient(p)
240    }
241}
242/// A cell in a marching-cubes grid.
243#[derive(Debug, Clone)]
244pub struct MarchingCell {
245    /// Min corner of the cell.
246    pub min: [f64; 3],
247    /// Cell side length.
248    pub step: f64,
249    /// SDF values at the 8 corners (index = ix*4 + iy*2 + iz).
250    pub corner_values: [f64; 8],
251}
252impl MarchingCell {
253    /// Return `true` if any corner is inside (negative SDF) and any is outside.
254    pub fn has_surface(&self) -> bool {
255        let has_inside = self.corner_values.iter().any(|&v| v < 0.0);
256        let has_outside = self.corner_values.iter().any(|&v| v >= 0.0);
257        has_inside && has_outside
258    }
259    /// Linear interpolation of position along edge between corner `i0` and `i1`.
260    fn edge_vertex(&self, i0: usize, i1: usize) -> [f64; 3] {
261        let offsets: [[f64; 3]; 8] = [
262            [0.0, 0.0, 0.0],
263            [self.step, 0.0, 0.0],
264            [self.step, self.step, 0.0],
265            [0.0, self.step, 0.0],
266            [0.0, 0.0, self.step],
267            [self.step, 0.0, self.step],
268            [self.step, self.step, self.step],
269            [0.0, self.step, self.step],
270        ];
271        let v0 = self.corner_values[i0];
272        let v1 = self.corner_values[i1];
273        let t = if (v1 - v0).abs() > 1e-14 {
274            v0 / (v0 - v1)
275        } else {
276            0.5
277        };
278        let p0 = add(self.min, offsets[i0]);
279        let p1 = add(self.min, offsets[i1]);
280        lerp3(p0, p1, t)
281    }
282    /// Extract surface vertices from this cell (one per crossing edge).
283    pub fn extract_vertices(&self) -> Vec<[f64; 3]> {
284        if !self.has_surface() {
285            return vec![];
286        }
287        let edges: [(usize, usize); 12] = [
288            (0, 1),
289            (1, 2),
290            (2, 3),
291            (3, 0),
292            (4, 5),
293            (5, 6),
294            (6, 7),
295            (7, 4),
296            (0, 4),
297            (1, 5),
298            (2, 6),
299            (3, 7),
300        ];
301        edges
302            .iter()
303            .filter_map(|&(i0, i1)| {
304                let v0 = self.corner_values[i0];
305                let v1 = self.corner_values[i1];
306                if v0.signum() != v1.signum() {
307                    Some(self.edge_vertex(i0, i1))
308                } else {
309                    None
310                }
311            })
312            .collect()
313    }
314}
315/// An axis-aligned box defined by centre and half-extents.
316pub struct SdfBox {
317    /// World-space centre.
318    pub center: [f64; 3],
319    /// Half-extents along each axis.
320    pub half_extents: [f64; 3],
321}
322impl SdfBox {
323    /// Create a new box.
324    pub fn new(center: [f64; 3], half_extents: [f64; 3]) -> Self {
325        Self {
326            center,
327            half_extents,
328        }
329    }
330}
331/// An infinite cylinder along the Y axis.
332pub struct SdfCylinder {
333    /// World-space centre (on the axis).
334    pub center: [f64; 3],
335    /// Cylinder radius.
336    pub radius: f64,
337}
338impl SdfCylinder {
339    /// Create a new cylinder.
340    pub fn new(center: [f64; 3], radius: f64) -> Self {
341        Self { center, radius }
342    }
343}
344/// CSG difference: region inside `a` but outside `b`.
345///
346/// SDF = max(sdf_a, -sdf_b).
347pub struct CsgDifference {
348    /// The base shape.
349    pub a: Box<dyn ImplicitSurface>,
350    /// The shape to subtract.
351    pub b: Box<dyn ImplicitSurface>,
352}
353impl CsgDifference {
354    /// Create a new difference.
355    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>) -> Self {
356        Self { a, b }
357    }
358}
359/// Smooth CSG union using the polynomial smooth-min kernel.
360///
361/// SDF = smooth_min(sdf_a, sdf_b, k).
362pub struct CsgSmoothUnion {
363    /// First operand.
364    pub a: Box<dyn ImplicitSurface>,
365    /// Second operand.
366    pub b: Box<dyn ImplicitSurface>,
367    /// Smoothing factor (larger -> smoother blend).
368    pub k: f64,
369}
370impl CsgSmoothUnion {
371    /// Create a new smooth union.
372    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>, k: f64) -> Self {
373        Self { a, b, k }
374    }
375}
376/// Smooth CSG intersection.
377///
378/// SDF = smooth_max(sdf_a, sdf_b, k).
379pub struct CsgSmoothIntersection {
380    /// First operand.
381    pub a: Box<dyn ImplicitSurface>,
382    /// Second operand.
383    pub b: Box<dyn ImplicitSurface>,
384    /// Smoothing factor.
385    pub k: f64,
386}
387impl CsgSmoothIntersection {
388    /// Create a new smooth intersection.
389    pub fn new(a: Box<dyn ImplicitSurface>, b: Box<dyn ImplicitSurface>, k: f64) -> Self {
390        Self { a, b, k }
391    }
392}
393/// Classification of a point relative to a plane.
394#[derive(Debug, Clone, Copy, PartialEq)]
395pub enum PlaneSide {
396    /// Point is on the front (positive) side.
397    Front,
398    /// Point is on the back (negative) side.
399    Back,
400    /// Point is on the plane (within tolerance).
401    OnPlane,
402}
403/// A finite capped cylinder aligned with the Y axis.
404pub struct SdfCappedCylinder {
405    /// World-space center.
406    pub center: [f64; 3],
407    /// Cylinder radius.
408    pub radius: f64,
409    /// Half-height (distance from centre to cap).
410    pub half_height: f64,
411}
412impl SdfCappedCylinder {
413    /// Create a new capped cylinder.
414    pub fn new(center: [f64; 3], radius: f64, half_height: f64) -> Self {
415        Self {
416            center,
417            radius,
418            half_height,
419        }
420    }
421}
422/// Rounded box: axis-aligned box with rounded edges.
423///
424/// SDF = SdfBox.sdf(p) - rounding_radius
425pub struct SdfRoundedBox {
426    /// World-space centre.
427    pub center: [f64; 3],
428    /// Half-extents (before rounding).
429    pub half_extents: [f64; 3],
430    /// Corner rounding radius.
431    pub radius: f64,
432}
433impl SdfRoundedBox {
434    /// Create a new rounded box.
435    pub fn new(center: [f64; 3], half_extents: [f64; 3], radius: f64) -> Self {
436        Self {
437            center,
438            half_extents,
439            radius,
440        }
441    }
442}
443
444#[cfg(test)]
445mod csg_offset_tests {
446    use super::super::functions::ImplicitSurface;
447    use super::CsgOffsetSurface;
448
449    fn unit_sphere_sdf(p: [f64; 3]) -> f64 {
450        (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt() - 1.0
451    }
452
453    #[test]
454    fn test_offset_sphere_eval_inside() {
455        let s = CsgOffsetSurface::new(Box::new(unit_sphere_sdf), 1.0);
456        assert!(s.eval([1.5, 0.0, 0.0]) < 0.0);
457    }
458
459    #[test]
460    fn test_offset_sphere_eval_outside() {
461        let s = CsgOffsetSurface::new(Box::new(unit_sphere_sdf), 1.0);
462        assert!(s.eval([3.5, 0.0, 0.0]) > 0.0);
463    }
464
465    #[test]
466    fn test_offset_sphere_on_surface() {
467        let s = CsgOffsetSurface::new(Box::new(unit_sphere_sdf), 1.0);
468        assert!(s.eval([2.0, 0.0, 0.0]).abs() < 1e-10);
469    }
470
471    #[test]
472    fn test_offset_sphere_gradient_outward() {
473        let s = CsgOffsetSurface::new(Box::new(unit_sphere_sdf), 1.0);
474        let g = s.finite_diff_gradient([2.0, 0.0, 0.0]);
475        assert!(g[0] > 0.5 && g[1].abs() < 0.01 && g[2].abs() < 0.01);
476    }
477
478    #[test]
479    fn test_implicit_surface_trait() {
480        let s = CsgOffsetSurface::new(Box::new(unit_sphere_sdf), 1.0);
481        let t: &dyn ImplicitSurface = &s;
482        assert!(t.sdf([2.5, 0.0, 0.0]) > 0.0);
483        assert!(t.sdf([1.5, 0.0, 0.0]) < 0.0);
484    }
485}