ranim_items/mesh/
sphere.rs1#[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#[derive(Debug, Clone, PartialEq)]
20pub struct Sphere {
21 pub radius: f64,
23 pub resolution: (u32, u32),
25 pub fill_rgba: AlphaColor<Srgb>,
27}
28
29impl Sphere {
30 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 pub fn unit() -> Self {
40 Self::new(1.0)
41 }
42 pub fn with_resolution(mut self, resolution: (u32, u32)) -> Self {
44 self.resolution = resolution;
45 self
46 }
47 pub fn with_fill_color(mut self, color: AlphaColor<Srgb>) -> Self {
49 self.fill_rgba = color;
50 self
51 }
52 pub fn points_uv_func(u: f64, v: f64, r: f64) -> DVec3 {
54 Self::normals_uv_func(u, v) * r
55 }
56 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}