Skip to main content

oxiphysics_geometry/implicit_geometry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6
7/// Smooth intersection using polynomial blend.
8#[derive(Debug, Clone)]
9pub struct SdfSmoothIntersection<A, B> {
10    /// First operand.
11    pub a: A,
12    /// Second operand.
13    pub b: B,
14    /// Blend radius.
15    pub k: f64,
16}
17impl<A: Sdf, B: Sdf> SdfSmoothIntersection<A, B> {
18    /// Create a smooth intersection.
19    pub fn new(a: A, b: B, k: f64) -> Self {
20        Self { a, b, k }
21    }
22}
23/// Smooth union using the polynomial C¹ blend of Quilez.
24///
25/// `k` controls the blend radius; `k = 0` degenerates to hard union.
26#[derive(Debug, Clone)]
27pub struct SdfSmoothUnion<A, B> {
28    /// First operand.
29    pub a: A,
30    /// Second operand.
31    pub b: B,
32    /// Blend radius (≥ 0).
33    pub k: f64,
34}
35impl<A: Sdf, B: Sdf> SdfSmoothUnion<A, B> {
36    /// Create a smooth union.
37    pub fn new(a: A, b: B, k: f64) -> Self {
38        Self { a, b, k }
39    }
40}
41/// SDF for a hexagonal prism aligned with the Y axis.
42///
43/// `radius` is the circumscribed circle radius; `half_height` is along Y.
44#[derive(Debug, Clone, Copy)]
45pub struct SdfHexagonalPrism {
46    /// Circumscribed radius of the hexagonal cross-section \[m\].
47    pub radius: f64,
48    /// Half-height along Y \[m\].
49    pub half_height: f64,
50}
51impl SdfHexagonalPrism {
52    /// Create a hexagonal prism SDF.
53    pub fn new(radius: f64, half_height: f64) -> Self {
54        Self {
55            radius,
56            half_height,
57        }
58    }
59}
60/// Boolean operation type for the SDF expression tree.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum BoolOp {
63    /// Hard union.
64    Union,
65    /// Hard intersection.
66    Intersection,
67    /// Difference (left minus right).
68    Difference,
69    /// Smooth union (blend radius stored separately).
70    SmoothUnion,
71    /// Smooth intersection.
72    SmoothIntersection,
73    /// Smooth difference.
74    SmoothDifference,
75}
76/// A single contact point between two bodies.
77#[derive(Debug, Clone, Copy)]
78pub struct ContactPoint {
79    /// Contact position in world space.
80    pub position: [f64; 3],
81    /// Contact normal (from B toward A).
82    pub normal: [f64; 3],
83    /// Penetration depth.
84    pub depth: f64,
85}
86/// Translate an SDF by a constant offset.
87#[derive(Debug, Clone)]
88pub struct SdfTranslate<S> {
89    /// Wrapped SDF.
90    pub inner: S,
91    /// Translation vector.
92    pub offset: [f64; 3],
93}
94impl<S: Sdf> SdfTranslate<S> {
95    /// Create a translated SDF.
96    pub fn new(inner: S, offset: [f64; 3]) -> Self {
97        Self { inner, offset }
98    }
99}
100/// Infinite repetition of an SDF along all three axes.
101#[derive(Debug, Clone)]
102pub struct SdfRepeat<S> {
103    /// Wrapped SDF.
104    pub inner: S,
105    /// Cell period in X.
106    pub period_x: f64,
107    /// Cell period in Y.
108    pub period_y: f64,
109    /// Cell period in Z.
110    pub period_z: f64,
111}
112impl<S: Sdf> SdfRepeat<S> {
113    /// Create an infinitely repeated SDF.
114    pub fn new(inner: S, period_x: f64, period_y: f64, period_z: f64) -> Self {
115        Self {
116            inner,
117            period_x,
118            period_y,
119            period_z,
120        }
121    }
122}
123/// Scale an SDF uniformly (preserves exact signed distance).
124#[derive(Debug, Clone)]
125pub struct SdfScale<S> {
126    /// Wrapped SDF.
127    pub inner: S,
128    /// Uniform scale factor (> 0).
129    pub factor: f64,
130}
131impl<S: Sdf> SdfScale<S> {
132    /// Create a scaled SDF.
133    pub fn new(inner: S, factor: f64) -> Self {
134        Self { inner, factor }
135    }
136}
137/// SDF for a torus in the XZ plane centred at the origin.
138#[derive(Debug, Clone, Copy)]
139pub struct SdfTorus {
140    /// Major radius (centre to tube centre) \[m\].
141    pub major: f64,
142    /// Minor radius (tube radius) \[m\].
143    pub minor: f64,
144}
145impl SdfTorus {
146    /// Create a torus SDF.
147    pub fn new(major: f64, minor: f64) -> Self {
148        Self { major, minor }
149    }
150}
151/// SDF for an infinite plane defined by `n·p = d` where `n` is the unit normal.
152///
153/// Points satisfying `n·p > d` are *outside* (positive side).
154#[derive(Debug, Clone, Copy)]
155pub struct SdfPlane {
156    /// Unit outward normal.
157    pub normal: [f64; 3],
158    /// Signed distance of the plane from the origin along `normal`.
159    pub offset: f64,
160}
161impl SdfPlane {
162    /// Create a plane SDF from a (not necessarily unit) normal and offset.
163    pub fn new(normal: [f64; 3], offset: f64) -> Self {
164        Self {
165            normal: norm(normal),
166            offset,
167        }
168    }
169}
170/// Smooth difference (subtract `B` from `A`) with polynomial blend.
171#[derive(Debug, Clone)]
172pub struct SdfSmoothDifference<A, B> {
173    /// Shape to subtract from.
174    pub a: A,
175    /// Shape to subtract.
176    pub b: B,
177    /// Blend radius.
178    pub k: f64,
179}
180impl<A: Sdf, B: Sdf> SdfSmoothDifference<A, B> {
181    /// Create a smooth difference.
182    pub fn new(a: A, b: B, k: f64) -> Self {
183        Self { a, b, k }
184    }
185}
186/// Apply torsion (twist around Y axis) to an SDF.
187///
188/// `strength` is twist angle per unit of Y \[radians/m\].
189#[derive(Debug, Clone)]
190pub struct SdfTwist<S> {
191    /// Wrapped SDF.
192    pub inner: S,
193    /// Twist rate \[rad/m\].
194    pub strength: f64,
195}
196impl<S: Sdf> SdfTwist<S> {
197    /// Create a twisted SDF.
198    pub fn new(inner: S, strength: f64) -> Self {
199        Self { inner, strength }
200    }
201}
202/// A dense 3-D grid of SDF samples.
203#[derive(Debug, Clone)]
204pub struct SdfGrid {
205    /// Number of grid cells along X.
206    pub nx: usize,
207    /// Number of cells along Y.
208    pub ny: usize,
209    /// Number of cells along Z.
210    pub nz: usize,
211    /// World-space coordinates of cell (0,0,0).
212    pub origin: [f64; 3],
213    /// Uniform voxel spacing.
214    pub spacing: f64,
215    /// Flat SDF samples in (z, y, x) major order.
216    pub data: Vec<f64>,
217}
218impl SdfGrid {
219    /// Create an empty grid of given resolution and spacing.
220    pub fn new(nx: usize, ny: usize, nz: usize, origin: [f64; 3], spacing: f64) -> Self {
221        Self {
222            nx,
223            ny,
224            nz,
225            origin,
226            spacing,
227            data: vec![f64::INFINITY; nx * ny * nz],
228        }
229    }
230    /// Sample an SDF and fill the grid.
231    pub fn from_sdf<S: Sdf>(
232        sdf: &S,
233        nx: usize,
234        ny: usize,
235        nz: usize,
236        origin: [f64; 3],
237        spacing: f64,
238    ) -> Self {
239        let mut grid = Self::new(nx, ny, nz, origin, spacing);
240        for iz in 0..nz {
241            for iy in 0..ny {
242                for ix in 0..nx {
243                    let p = grid.cell_center(ix, iy, iz);
244                    grid.data[iz * ny * nx + iy * nx + ix] = sdf.dist(p);
245                }
246            }
247        }
248        grid
249    }
250    /// World-space position of cell centre `(ix, iy, iz)`.
251    pub fn cell_center(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
252        [
253            self.origin[0] + ix as f64 * self.spacing,
254            self.origin[1] + iy as f64 * self.spacing,
255            self.origin[2] + iz as f64 * self.spacing,
256        ]
257    }
258    /// Get the SDF value at integer index `(ix, iy, iz)`.
259    pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f64 {
260        self.data[iz * self.ny * self.nx + iy * self.nx + ix]
261    }
262    /// Set the SDF value at integer index `(ix, iy, iz)`.
263    pub fn set(&mut self, ix: usize, iy: usize, iz: usize, v: f64) {
264        self.data[iz * self.ny * self.nx + iy * self.nx + ix] = v;
265    }
266    /// Trilinearly interpolate the SDF at world-space point `p`.
267    pub fn interpolate(&self, p: [f64; 3]) -> f64 {
268        let fx = (p[0] - self.origin[0]) / self.spacing;
269        let fy = (p[1] - self.origin[1]) / self.spacing;
270        let fz = (p[2] - self.origin[2]) / self.spacing;
271        let ix = (fx as usize).min(self.nx.saturating_sub(2));
272        let iy = (fy as usize).min(self.ny.saturating_sub(2));
273        let iz = (fz as usize).min(self.nz.saturating_sub(2));
274        let tx = (fx - ix as f64).clamp(0.0, 1.0);
275        let ty = (fy - iy as f64).clamp(0.0, 1.0);
276        let tz = (fz - iz as f64).clamp(0.0, 1.0);
277        let v000 = self.get(ix, iy, iz);
278        let v100 = self.get(ix + 1, iy, iz);
279        let v010 = self.get(ix, iy + 1, iz);
280        let v110 = self.get(ix + 1, iy + 1, iz);
281        let v001 = self.get(ix, iy, iz + 1);
282        let v101 = self.get(ix + 1, iy, iz + 1);
283        let v011 = self.get(ix, iy + 1, iz + 1);
284        let v111 = self.get(ix + 1, iy + 1, iz + 1);
285        let c00 = v000 * (1.0 - tx) + v100 * tx;
286        let c10 = v010 * (1.0 - tx) + v110 * tx;
287        let c01 = v001 * (1.0 - tx) + v101 * tx;
288        let c11 = v011 * (1.0 - tx) + v111 * tx;
289        let c0 = c00 * (1.0 - ty) + c10 * ty;
290        let c1 = c01 * (1.0 - ty) + c11 * ty;
291        c0 * (1.0 - tz) + c1 * tz
292    }
293    /// Apply morphological dilation (grow surface by `delta`).
294    pub fn dilate(&mut self, delta: f64) {
295        for v in self.data.iter_mut() {
296            *v -= delta;
297        }
298    }
299    /// Apply morphological erosion (shrink surface by `delta`).
300    pub fn erode(&mut self, delta: f64) {
301        for v in self.data.iter_mut() {
302            *v += delta;
303        }
304    }
305}
306/// SDF for a rounded cylinder aligned with the Y axis.
307///
308/// Cylinder of `radius` and `half_height`, with hemisphere caps of the same
309/// radius (i.e. a capsule-like shape but with flat lateral surface).
310#[derive(Debug, Clone, Copy)]
311pub struct SdfRoundedCylinder {
312    /// Cylinder radius \[m\].
313    pub radius: f64,
314    /// Half-height of the cylindrical body \[m\].
315    pub half_height: f64,
316    /// Corner rounding radius \[m\].
317    pub rounding: f64,
318}
319impl SdfRoundedCylinder {
320    /// Create a rounded cylinder SDF.
321    pub fn new(radius: f64, half_height: f64, rounding: f64) -> Self {
322        Self {
323            radius,
324            half_height,
325            rounding,
326        }
327    }
328}
329/// SDF intersection of two shapes.
330#[derive(Debug, Clone)]
331pub struct SdfIntersection<A, B> {
332    /// First operand.
333    pub a: A,
334    /// Second operand.
335    pub b: B,
336}
337impl<A: Sdf, B: Sdf> SdfIntersection<A, B> {
338    /// Create an intersection.
339    pub fn new(a: A, b: B) -> Self {
340        Self { a, b }
341    }
342}
343/// Extrude a 2-D XZ-plane profile along the Y axis.
344///
345/// The wrapped function takes `[x, z]` and returns the 2-D signed distance.
346pub struct SdfExtrude<F> {
347    /// 2-D profile function: `f([x,z]) -> signed_distance`.
348    pub profile: F,
349    /// Half-extent along the Y axis.
350    pub half_height: f64,
351}
352impl<F: Fn([f64; 2]) -> f64 + Send + Sync> SdfExtrude<F> {
353    /// Create an extruded SDF.
354    pub fn new(profile: F, half_height: f64) -> Self {
355        Self {
356            profile,
357            half_height,
358        }
359    }
360}
361/// Revolve a 2-D profile curve (given as a function of r and y) around the Y axis.
362///
363/// The wrapped function takes `(r, y)` where `r = sqrt(x²+z²)` and returns
364/// the signed distance in the 2-D profile plane.
365pub struct SdfRevolution<F> {
366    /// 2-D profile function: `f(r, y) -> signed_distance`.
367    pub profile: F,
368}
369impl<F: Fn(f64, f64) -> f64 + Send + Sync> SdfRevolution<F> {
370    /// Create a solid-of-revolution SDF.
371    pub fn new(profile: F) -> Self {
372        Self { profile }
373    }
374}
375/// SDF union of two shapes.
376#[derive(Debug, Clone)]
377pub struct SdfUnion<A, B> {
378    /// First operand.
379    pub a: A,
380    /// Second operand.
381    pub b: B,
382}
383impl<A: Sdf, B: Sdf> SdfUnion<A, B> {
384    /// Create a union.
385    pub fn new(a: A, b: B) -> Self {
386        Self { a, b }
387    }
388}
389/// SDF for a finite cone with apex at origin pointing along +Y.
390///
391/// `half_angle` is the half-apex angle in radians; `height` is the cone height.
392#[derive(Debug, Clone, Copy)]
393pub struct SdfCone {
394    /// Half-apex angle \[rad\].
395    pub half_angle: f64,
396    /// Cone height (distance from apex to base) \[m\].
397    pub height: f64,
398}
399impl SdfCone {
400    /// Create a cone SDF.
401    pub fn new(half_angle: f64, height: f64) -> Self {
402        Self { half_angle, height }
403    }
404}
405/// Expand or contract a shape by a constant offset.
406///
407/// Positive `offset` grows the shape; negative shrinks it.
408#[derive(Debug, Clone)]
409pub struct SdfOffset<S> {
410    /// Wrapped SDF.
411    pub inner: S,
412    /// Offset distance \[m\].
413    pub offset: f64,
414}
415impl<S: Sdf> SdfOffset<S> {
416    /// Create an offset SDF.
417    pub fn new(inner: S, offset: f64) -> Self {
418        Self { inner, offset }
419    }
420}
421/// Isosurface mesh extracted from a grid.
422#[derive(Debug, Clone, Default)]
423pub struct IsoMeshResult {
424    /// Triangle list (each entry is one triangle).
425    pub triangles: Vec<Triangle>,
426    /// Total vertex count (3 × triangle count without sharing).
427    pub vertex_count: usize,
428}
429/// SDF for a square-base pyramid with apex at `(0, height, 0)`.
430///
431/// Base is centred at origin in the XZ plane with half-side `half_base`.
432#[derive(Debug, Clone, Copy)]
433pub struct SdfPyramid {
434    /// Half-side of the square base \[m\].
435    pub half_base: f64,
436    /// Height of the pyramid \[m\].
437    pub height: f64,
438}
439impl SdfPyramid {
440    /// Create a pyramid SDF.
441    pub fn new(half_base: f64, height: f64) -> Self {
442        Self { half_base, height }
443    }
444}
445/// SDF difference: `A` minus `B`.
446#[derive(Debug, Clone)]
447pub struct SdfDifference<A, B> {
448    /// Shape to subtract from.
449    pub a: A,
450    /// Shape to subtract.
451    pub b: B,
452}
453impl<A: Sdf, B: Sdf> SdfDifference<A, B> {
454    /// Create a difference.
455    pub fn new(a: A, b: B) -> Self {
456        Self { a, b }
457    }
458}
459/// Displace an SDF surface with procedural value noise.
460///
461/// The displacement `amplitude` controls how far the surface is pushed;
462/// `scale` controls the noise spatial frequency.
463#[derive(Debug, Clone)]
464pub struct SdfNoiseDisplace<S> {
465    /// Wrapped SDF.
466    pub inner: S,
467    /// Noise spatial frequency scale.
468    pub noise_scale: f64,
469    /// Maximum displacement amplitude.
470    pub amplitude: f64,
471    /// Number of fBm octaves.
472    pub octaves: u32,
473}
474impl<S: Sdf> SdfNoiseDisplace<S> {
475    /// Create a noise-displaced SDF.
476    pub fn new(inner: S, noise_scale: f64, amplitude: f64, octaves: u32) -> Self {
477        Self {
478            inner,
479            noise_scale,
480            amplitude,
481            octaves,
482        }
483    }
484}
485/// SDF approximation for an axis-aligned ellipsoid with semi-axes `radii`.
486///
487/// Uses the Quilez approximation: exact on the boundary, approximate elsewhere.
488#[derive(Debug, Clone, Copy)]
489pub struct SdfEllipsoid {
490    /// Semi-axis lengths (rx, ry, rz).
491    pub radii: [f64; 3],
492}
493impl SdfEllipsoid {
494    /// Create an ellipsoid SDF.
495    pub fn new(rx: f64, ry: f64, rz: f64) -> Self {
496        Self {
497            radii: [rx, ry, rz],
498        }
499    }
500}
501/// A triangle in 3-D space (three vertices).
502#[derive(Debug, Clone, Copy)]
503pub struct Triangle {
504    /// First vertex.
505    pub v0: [f64; 3],
506    /// Second vertex.
507    pub v1: [f64; 3],
508    /// Third vertex.
509    pub v2: [f64; 3],
510}
511impl Triangle {
512    /// Compute the triangle's outward normal (unnormalized).
513    pub fn normal(&self) -> [f64; 3] {
514        cross(sub(self.v1, self.v0), sub(self.v2, self.v0))
515    }
516}
517/// Dynamic SDF expression tree node.
518///
519/// Allows building complex SDF scenes at runtime without generic type nesting.
520pub enum SdfNode {
521    /// A leaf: arbitrary boxed SDF.
522    Leaf(Box<dyn Sdf>),
523    /// A binary combination of two nodes.
524    Binary {
525        /// Binary operation.
526        op: BoolOp,
527        /// Left child.
528        left: Box<SdfNode>,
529        /// Right child.
530        right: Box<SdfNode>,
531        /// Blend radius (used for smooth ops).
532        k: f64,
533    },
534}
535impl SdfNode {
536    /// Create a leaf node from any `Sdf` implementor.
537    pub fn leaf<S: Sdf + 'static>(s: S) -> Self {
538        Self::Leaf(Box::new(s))
539    }
540    /// Combine two nodes with a boolean operation.
541    pub fn combine(op: BoolOp, left: Self, right: Self, k: f64) -> Self {
542        Self::Binary {
543            op,
544            left: Box::new(left),
545            right: Box::new(right),
546            k,
547        }
548    }
549    /// Evaluate the SDF tree at point `p`.
550    pub fn eval(&self, p: [f64; 3]) -> f64 {
551        match self {
552            Self::Leaf(s) => s.dist(p),
553            Self::Binary { op, left, right, k } => {
554                let a = left.eval(p);
555                let b = right.eval(p);
556                match op {
557                    BoolOp::Union => a.min(b),
558                    BoolOp::Intersection => a.max(b),
559                    BoolOp::Difference => a.max(-b),
560                    BoolOp::SmoothUnion => sdf_smooth_union(a, b, *k),
561                    BoolOp::SmoothIntersection => sdf_smooth_intersection(a, b, *k),
562                    BoolOp::SmoothDifference => sdf_smooth_difference(a, b, *k),
563                }
564            }
565        }
566    }
567}
568/// SDF for a triangular prism (equilateral cross-section) aligned with Y axis.
569///
570/// `side` is the equilateral triangle side length; `half_height` is along Y.
571#[derive(Debug, Clone, Copy)]
572pub struct SdfTriangularPrism {
573    /// Triangle side length \[m\].
574    pub side: f64,
575    /// Half-height along Y \[m\].
576    pub half_height: f64,
577}
578impl SdfTriangularPrism {
579    /// Create a triangular prism SDF.
580    pub fn new(side: f64, half_height: f64) -> Self {
581        Self { side, half_height }
582    }
583}
584/// SDF approximation for the Schoen gyroid minimal surface.
585///
586/// The gyroid is an infinitely periodic surface: `sin x cos y + sin y cos z + sin z cos x = 0`.
587/// This is an approximation; the exact gyroid has no closed-form SDF.
588#[derive(Debug, Clone, Copy)]
589pub struct SdfGyroid {
590    /// Spatial frequency scale.
591    pub scale: f64,
592    /// Thickness of the gyroid sheet.
593    pub thickness: f64,
594}
595impl SdfGyroid {
596    /// Create a gyroid SDF.
597    pub fn new(scale: f64, thickness: f64) -> Self {
598        Self { scale, thickness }
599    }
600}
601/// Hollow (shell) version of an SDF: the region between two offset surfaces.
602///
603/// Produces a thin shell of thickness `thickness` around the surface.
604#[derive(Debug, Clone)]
605pub struct SdfShell<S> {
606    /// Wrapped SDF.
607    pub inner: S,
608    /// Shell thickness \[m\] (half on each side of the zero isosurface).
609    pub thickness: f64,
610}
611impl<S: Sdf> SdfShell<S> {
612    /// Create a shell SDF of given `thickness`.
613    pub fn new(inner: S, thickness: f64) -> Self {
614        Self { inner, thickness }
615    }
616}
617/// Apply a bend deformation around the Y axis to an SDF.
618///
619/// `strength` controls how much bending occurs per unit of X.
620#[derive(Debug, Clone)]
621pub struct SdfBend<S> {
622    /// Wrapped SDF.
623    pub inner: S,
624    /// Bend rate \[rad/m\].
625    pub strength: f64,
626}
627impl<S: Sdf> SdfBend<S> {
628    /// Create a bent SDF.
629    pub fn new(inner: S, strength: f64) -> Self {
630        Self { inner, strength }
631    }
632}
633/// SDF for a line-segment tube: a capsule with flat ends (cylinder).
634///
635/// Computes distance to the axis segment `[a, b]` minus `radius`.
636#[derive(Debug, Clone, Copy)]
637pub struct SdfLineSegment {
638    /// Start point of the segment.
639    pub a: [f64; 3],
640    /// End point of the segment.
641    pub b: [f64; 3],
642    /// Tube radius \[m\].
643    pub radius: f64,
644}
645impl SdfLineSegment {
646    /// Create a line-segment SDF.
647    pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
648        Self { a, b, radius }
649    }
650}
651/// Result of a ray-march query.
652#[derive(Debug, Clone, Copy)]
653pub struct RayMarchResult {
654    /// Whether the ray hit the surface.
655    pub hit: bool,
656    /// Distance along the ray to the hit point (if `hit` is true).
657    pub t: f64,
658    /// Hit point in world space.
659    pub point: [f64; 3],
660    /// Number of steps taken.
661    pub steps: u32,
662}
663/// Physics collision proxy that first checks a bounding sphere, then the detailed SDF.
664///
665/// Avoids expensive SDF evaluation for clearly non-colliding points.
666#[derive(Debug, Clone)]
667pub struct SdfBoundedProxy<S> {
668    /// The detailed SDF.
669    pub inner: S,
670    /// Centre of the bounding sphere.
671    pub bsphere_center: [f64; 3],
672    /// Radius of the bounding sphere.
673    pub bsphere_radius: f64,
674}
675impl<S: Sdf> SdfBoundedProxy<S> {
676    /// Create a bounded proxy.
677    pub fn new(inner: S, bsphere_center: [f64; 3], bsphere_radius: f64) -> Self {
678        Self {
679            inner,
680            bsphere_center,
681            bsphere_radius,
682        }
683    }
684    /// Returns true if point `p` is definitely outside the bounding sphere.
685    pub fn outside_bounds(&self, p: [f64; 3]) -> bool {
686        len(sub(p, self.bsphere_center)) > self.bsphere_radius
687    }
688}
689/// SDF for an axis-aligned box centred at the origin.
690///
691/// This is a standalone, self-contained version with `half_extents` per axis.
692#[derive(Debug, Clone, Copy)]
693pub struct SdfBox {
694    /// Half-extents along each axis.
695    pub half_extents: [f64; 3],
696}
697impl SdfBox {
698    /// Create a box SDF.
699    pub fn new(hx: f64, hy: f64, hz: f64) -> Self {
700        Self {
701            half_extents: [hx, hy, hz],
702        }
703    }
704}
705/// Result of an SDF collision query.
706#[derive(Debug, Clone, Copy)]
707pub struct SdfCollisionResult {
708    /// Penetration depth (positive = overlapping).
709    pub depth: f64,
710    /// Contact normal pointing from B toward A (unit vector).
711    pub normal: [f64; 3],
712    /// Contact point (on or near surface).
713    pub contact_point: [f64; 3],
714}
715/// SDF for a sphere of given `radius` centred at the origin.
716#[derive(Debug, Clone, Copy)]
717pub struct SdfSphere {
718    /// Sphere radius \[m\].
719    pub radius: f64,
720}
721impl SdfSphere {
722    /// Create a new sphere SDF with `radius`.
723    pub fn new(radius: f64) -> Self {
724        Self { radius }
725    }
726}