Skip to main content

ranim_items/mesh/
sphere.rs

1//! Sphere — a sphere mesh item.
2
3#[cfg(feature = "render")]
4use ranim_render::extract::{Extract, ExtractContext};
5
6use std::f64::consts::{PI, TAU};
7
8use ranim_core::{
9    anchor::Aabb,
10    color::{self, AlphaColor, Srgb},
11    glam::DVec3,
12    traits::{FillColor, Interpolatable, Opacity, With},
13};
14
15use super::Surface;
16use crate::mesh::MeshItem;
17
18/// A sphere primitive centered at the origin.
19#[derive(Debug, Clone, PartialEq)]
20pub struct Sphere {
21    /// Sphere radius.
22    pub radius: f64,
23    /// UV mesh resolution `(u, v)`.
24    pub resolution: (u32, u32),
25    /// Sphere fill color.
26    pub fill_rgba: AlphaColor<Srgb>,
27}
28
29impl Sphere {
30    /// Creates a sphere centered at the origin.
31    pub fn new(radius: f64) -> Self {
32        Self {
33            radius,
34            resolution: (101, 51),
35            fill_rgba: color::palette::css::BLUE.with_alpha(1.0),
36        }
37    }
38    /// Creates a unit sphere.
39    pub fn unit() -> Self {
40        Self::new(1.0)
41    }
42    /// Sets the UV mesh resolution.
43    pub fn with_resolution(mut self, resolution: (u32, u32)) -> Self {
44        self.resolution = resolution;
45        self
46    }
47    /// Sets the fill color.
48    pub fn with_fill_color(mut self, color: AlphaColor<Srgb>) -> Self {
49        self.fill_rgba = color;
50        self
51    }
52    /// Returns a point at spherical UV coordinates and radius `r`.
53    pub fn points_uv_func(u: f64, v: f64, r: f64) -> DVec3 {
54        Self::normals_uv_func(u, v) * r
55    }
56    /// Returns the unit normal at spherical UV coordinates.
57    pub fn normals_uv_func(u: f64, v: f64) -> DVec3 {
58        DVec3::new(u.cos() * v.sin(), u.sin() * v.sin(), -v.cos())
59    }
60}
61impl From<Sphere> for MeshItem {
62    fn from(value: Sphere) -> Self {
63        Surface::from(value).into()
64    }
65}
66impl From<Sphere> for Surface {
67    fn from(value: Sphere) -> Self {
68        Surface::from_uv_func(
69            |u, v| Sphere::points_uv_func(u, v, value.radius),
70            (0.0, TAU),
71            (0.0, PI),
72            value.resolution,
73        )
74        .with(|x| {
75            x.set_fill_color(value.fill_rgba);
76        })
77    }
78}
79impl Interpolatable for Sphere {
80    fn lerp(&self, target: &Self, t: f64) -> Self {
81        Self {
82            radius: self.radius.lerp(&target.radius, t),
83            resolution: if t < 0.5 {
84                self.resolution
85            } else {
86                target.resolution
87            },
88            fill_rgba: Interpolatable::lerp(&self.fill_rgba, &target.fill_rgba, t),
89        }
90    }
91}
92impl FillColor for Sphere {
93    fn fill_color(&self) -> AlphaColor<Srgb> {
94        self.fill_rgba
95    }
96    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
97        self.fill_rgba = color;
98        self
99    }
100    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
101        self.fill_rgba = self.fill_rgba.with_alpha(opacity);
102        self
103    }
104}
105impl Opacity for Sphere {
106    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
107        self.fill_rgba = self.fill_rgba.with_alpha(opacity);
108        self
109    }
110}
111impl Aabb for Sphere {
112    fn aabb(&self) -> [DVec3; 2] {
113        let r = DVec3::splat(self.radius);
114        [-r, r]
115    }
116}
117#[cfg(feature = "render")]
118impl Extract for Sphere {
119    fn extract(&self, ctx: &mut ExtractContext<'_>) {
120        MeshItem::from(self.clone()).extract(ctx);
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use ranim_core::{glam::dvec3, prelude::TransformedExt, traits::Translation};
128    #[test]
129    fn sphere_surface_uses_canonical_local_vertices() {
130        let surface = Surface::from(Sphere::new(1.0).with_resolution((5, 5)));
131        assert_eq!(surface.vertices.len(), 25);
132        assert_eq!(surface.resolution, (5, 5));
133        assert!(surface.vertices[0].abs_diff_eq(Sphere::points_uv_func(0.0, 0.0, 1.0), 1e-10));
134    }
135    #[test]
136    fn sphere_aabb_tracks_placement() {
137        let [min, max] = Sphere::new(1.0).aabb();
138        assert_eq!(min, dvec3(-1.0, -1.0, -1.0));
139        assert_eq!(max, dvec3(1.0, 1.0, 1.0));
140
141        let sphere = Sphere::new(1.0).transformed(Translation(dvec3(1.0, 2.0, 3.0)));
142        let [min, max] = sphere.aabb();
143        assert_eq!(min, dvec3(0.0, 1.0, 2.0));
144        assert_eq!(max, dvec3(2.0, 3.0, 4.0));
145    }
146    #[test]
147    fn test_sphere_interpolation() {
148        let mid = Sphere::new(1.0).lerp(&Sphere::new(3.0), 0.5);
149        assert!((mid.radius - 2.0).abs() < 1e-10);
150    }
151}