Skip to main content

ranim_items/vitem/geometry/
arc.rs

1#[cfg(feature = "render")]
2use ranim_render::extract::{Extract, ExtractContext};
3
4use ranim_core::{
5    anchor::{Aabb, AabbPoint, Locate},
6    traits::{ApplyTransform, Opacity, ScaleTransform, ShiftTransform, StrokeColor},
7};
8use ranim_core::{
9    color::{AlphaColor, Srgb},
10    glam::{self, DVec3},
11};
12
13use crate::vitem::geometry::EllipticArc;
14use crate::vitem::{DEFAULT_STROKE_WIDTH, VItem};
15
16/// An arc centered at the origin in the XY plane.
17#[derive(Clone, Debug, ranim_macros::Interpolatable)]
18pub struct Arc {
19    /// Radius.
20    pub radius: f64,
21    /// Span angle in radians.
22    pub angle: f64,
23    /// Stroke rgba.
24    pub stroke_rgba: AlphaColor<Srgb>,
25    /// Stroke width.
26    pub stroke_width: f32,
27}
28
29impl Arc {
30    /// Creates an arc with the given angle and radius.
31    pub fn new(angle: f64, radius: f64) -> Self {
32        Self {
33            radius,
34            angle,
35            stroke_rgba: AlphaColor::WHITE,
36            stroke_width: DEFAULT_STROKE_WIDTH,
37        }
38    }
39
40    /// Scales the intrinsic radius.
41    pub fn scale(&mut self, scale: f64) -> &mut Self {
42        self.radius *= scale;
43        self
44    }
45
46    /// The start point in canonical local coordinates.
47    pub fn start(&self) -> DVec3 {
48        DVec3::X * self.radius
49    }
50
51    /// The end point in canonical local coordinates.
52    pub fn end(&self) -> DVec3 {
53        DVec3::new(self.angle.cos(), self.angle.sin(), 0.0) * self.radius
54    }
55}
56
57impl Aabb for Arc {
58    fn aabb(&self) -> [DVec3; 2] {
59        VItem::from(self.clone()).aabb()
60    }
61}
62
63impl Opacity for Arc {
64    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
65        self.stroke_rgba = self.stroke_rgba.with_alpha(opacity);
66        self
67    }
68}
69
70impl StrokeColor for Arc {
71    fn stroke_color(&self) -> AlphaColor<Srgb> {
72        self.stroke_rgba
73    }
74    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
75        self.stroke_rgba = color;
76        self
77    }
78    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
79        self.stroke_rgba = self.stroke_rgba.with_alpha(opacity);
80        self
81    }
82}
83
84impl From<Arc> for VItem {
85    fn from(value: Arc) -> Self {
86        EllipticArc::from(value).into()
87    }
88}
89
90#[cfg(feature = "render")]
91impl Extract for Arc {
92    fn extract(&self, ctx: &mut ExtractContext<'_>) {
93        VItem::from(self.clone()).extract(ctx);
94    }
95}
96
97/// An arc whose local geometry is defined by its start and end points.
98#[derive(Clone, Debug, ranim_macros::Interpolatable)]
99pub struct ArcBetweenPoints {
100    /// Start point.
101    pub start: DVec3,
102    /// End point.
103    pub end: DVec3,
104    /// Arc angle.
105    pub angle: f64,
106    /// Stroke rgba.
107    pub stroke_rgba: AlphaColor<Srgb>,
108    /// Stroke width.
109    pub stroke_width: f32,
110}
111
112impl ArcBetweenPoints {
113    /// Creates an arc between two local points.
114    pub fn new(start: DVec3, end: DVec3, angle: f64) -> Self {
115        Self {
116            start,
117            end,
118            angle,
119            stroke_rgba: AlphaColor::WHITE,
120            stroke_width: DEFAULT_STROKE_WIDTH,
121        }
122    }
123
124    /// Returns the circle center in local coordinates.
125    pub fn center(&self) -> DVec3 {
126        let chord = self.end - self.start;
127        let midpoint = (self.start + self.end) / 2.0;
128        let perpendicular = DVec3::Z.cross(chord).normalize_or_zero();
129        midpoint + perpendicular * (chord.length() / (2.0 * (self.angle / 2.0).tan()))
130    }
131
132    /// Scales the intrinsic start and end points about their AABB center.
133    pub fn scale(&mut self, scale: f64) -> &mut Self {
134        self.scale_at(scale, AabbPoint::CENTER)
135    }
136
137    /// Scales the intrinsic start and end points about an anchor.
138    pub fn scale_at<T>(&mut self, scale: f64, anchor: T) -> &mut Self
139    where
140        T: Locate<Self>,
141    {
142        let point = anchor.locate(self);
143        self.start
144            .shift(-point)
145            .scale(DVec3::splat(scale))
146            .shift(point);
147        self.end
148            .shift(-point)
149            .scale(DVec3::splat(scale))
150            .shift(point);
151        self
152    }
153}
154
155impl Aabb for ArcBetweenPoints {
156    fn aabb(&self) -> [DVec3; 2] {
157        VItem::from(self.clone()).aabb()
158    }
159}
160
161impl<G: Into<ranim_core::prelude::Similarity>> ApplyTransform<G> for ArcBetweenPoints {
162    fn apply(&mut self, transform: G) -> &mut Self {
163        let transform = transform.into();
164        self.start = transform.transform_point(self.start);
165        self.end = transform.transform_point(self.end);
166        self
167    }
168}
169
170impl Opacity for ArcBetweenPoints {
171    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
172        self.stroke_rgba = self.stroke_rgba.with_alpha(opacity);
173        self
174    }
175}
176
177impl StrokeColor for ArcBetweenPoints {
178    fn stroke_color(&self) -> AlphaColor<Srgb> {
179        self.stroke_rgba
180    }
181    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
182        self.stroke_rgba = color;
183        self
184    }
185    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
186        self.stroke_rgba = self.stroke_rgba.with_alpha(opacity);
187        self
188    }
189}
190
191impl From<ArcBetweenPoints> for VItem {
192    fn from(value: ArcBetweenPoints) -> Self {
193        let center = value.center();
194        let radius = value.start.distance(center);
195        let start = value.start - center;
196        let start_angle = start.y.atan2(start.x);
197        let mut item = VItem::from(EllipticArc {
198            radius: glam::DVec2::splat(radius),
199            start_angle,
200            angle: value.angle,
201            stroke_rgba: value.stroke_rgba,
202            stroke_width: value.stroke_width,
203        });
204        item.shift(center);
205        item
206    }
207}
208
209#[cfg(feature = "render")]
210impl Extract for ArcBetweenPoints {
211    fn extract(&self, ctx: &mut ExtractContext<'_>) {
212        VItem::from(self.clone()).extract(ctx);
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use std::f64::consts::PI;
219
220    use assert_float_eq::assert_float_absolute_eq;
221    use glam::dvec3;
222    use ranim_core::traits::{RotateTransform, ShiftTransform};
223
224    use super::*;
225
226    #[test]
227    fn arcs_preserve_their_endpoints() {
228        let arc = Arc::new(PI / 2.0, 2.0);
229        assert_float_absolute_eq!(
230            arc.start().distance_squared(dvec3(2.0, 0.0, 0.0)),
231            0.0,
232            1e-10
233        );
234        assert_float_absolute_eq!(arc.end().distance_squared(dvec3(0.0, 2.0, 0.0)), 0.0, 1e-10);
235
236        let angle = PI * 2.0 / 3.0;
237        let mut placed = VItem::from(Arc::new(angle, 2.0));
238        placed
239            .rotate_on_axis(DVec3::Z, PI / 2.0 - angle / 2.0)
240            .shift(dvec3(2.0, 2.0, 0.0));
241        assert!(placed.vpoints[0].abs_diff_eq(dvec3(3.732050807568877, 3.0, 0.0), 1e-10));
242        assert!(
243            placed
244                .vpoints
245                .last()
246                .unwrap()
247                .abs_diff_eq(dvec3(0.2679491924311228, 3.0, 0.0), 1e-10)
248        );
249
250        let arc = ArcBetweenPoints::new(dvec3(2.0, 0.0, 0.0), dvec3(0.0, 2.0, 0.0), PI / 2.0);
251        assert_float_absolute_eq!(arc.center().distance_squared(DVec3::ZERO), 0.0, 1e-10);
252        let item = VItem::from(arc);
253        assert_float_absolute_eq!(
254            item.vpoints[0].distance_squared(dvec3(2.0, 0.0, 0.0)),
255            0.0,
256            1e-10
257        );
258        assert_float_absolute_eq!(
259            item.vpoints[item.vpoints.len() - 1].distance_squared(dvec3(0.0, 2.0, 0.0)),
260            0.0,
261            1e-10
262        );
263    }
264}