Skip to main content

rgpui_component/plot/shape/
arc.rs

1// @reference: https://d3js.org/d3-shape/arc
2
3use std::{f32::consts::PI, fmt::Debug};
4
5use rgpui::{Bounds, Hsla, Path, PathBuilder, Pixels, Point, Window, point, px};
6
7const EPSILON: f32 = 1e-12;
8const HALF_PI: f32 = PI / 2.;
9
10pub struct ArcData<'a, T> {
11    pub data: &'a T,
12    pub index: usize,
13    pub value: f32,
14    pub start_angle: f32,
15    pub end_angle: f32,
16    pub pad_angle: f32,
17}
18
19impl<T> Debug for ArcData<'_, T> {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(
22            f,
23            "ArcData {{ index: {}, value: {}, start_angle: {}, end_angle: {}, pad_angle: {} }}",
24            self.index, self.value, self.start_angle, self.end_angle, self.pad_angle
25        )
26    }
27}
28
29pub struct Arc {
30    inner_radius: f32,
31    outer_radius: f32,
32}
33
34impl Default for Arc {
35    fn default() -> Self {
36        Self {
37            inner_radius: 0.,
38            outer_radius: 0.,
39        }
40    }
41}
42
43impl Arc {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Set the inner radius of the Arc.
49    pub fn inner_radius(mut self, inner_radius: f32) -> Self {
50        self.inner_radius = inner_radius;
51        self
52    }
53
54    /// Set the outer radius of the Arc.
55    pub fn outer_radius(mut self, outer_radius: f32) -> Self {
56        self.outer_radius = outer_radius;
57        self
58    }
59
60    /// Get the centroid of the Arc.
61    pub fn centroid<T>(&self, arc: &ArcData<T>) -> Point<f32> {
62        let start_angle = arc.start_angle - HALF_PI;
63        let end_angle = arc.end_angle - HALF_PI;
64        let r = (self.inner_radius + self.outer_radius) / 2.;
65        let a = (start_angle + end_angle) / 2.;
66
67        point(r * a.cos(), r * a.sin())
68    }
69
70    fn path<T>(
71        &self,
72        arc: &ArcData<T>,
73        inner_radius: Option<f32>,
74        outer_radius: Option<f32>,
75        bounds: &Bounds<Pixels>,
76    ) -> Option<Path<Pixels>> {
77        let start_angle = arc.start_angle - HALF_PI;
78        let end_angle = arc.end_angle - HALF_PI;
79        let da = end_angle - start_angle;
80        let pad_angle = if da >= PI {
81            // Leave some pad angle for full circle.
82            // If not, the path start and end will be the same point.
83            0.0001
84        } else {
85            arc.pad_angle
86        };
87        let r0 = inner_radius.unwrap_or(self.inner_radius).max(0.);
88        let r1 = outer_radius.unwrap_or(self.outer_radius).max(0.);
89
90        // Calculate the center point.
91        let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
92        let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
93
94        // Angle difference.
95        if r1 < EPSILON || da.abs() < EPSILON {
96            return None;
97        }
98
99        // Handle pad angle.
100        let (a0_outer, a1_outer, a0_inner, a1_inner) = if r0 > EPSILON && pad_angle > 0.0 {
101            let pad_width = r1 * pad_angle;
102            let pad_angle_outer = pad_width / r1;
103            let mut pad_angle_inner = pad_width / r0;
104            let max_inner_pad = da * 0.8;
105            if pad_angle_inner > max_inner_pad {
106                pad_angle_inner = max_inner_pad;
107            }
108            (
109                start_angle + pad_angle_outer * 0.5,
110                end_angle - pad_angle_outer * 0.5,
111                start_angle + pad_angle_inner * 0.5,
112                end_angle - pad_angle_inner * 0.5,
113            )
114        } else {
115            let pad = pad_angle * 0.5;
116            (
117                start_angle + pad,
118                end_angle - pad,
119                start_angle + pad,
120                end_angle - pad,
121            )
122        };
123
124        let da_outer = a1_outer - a0_outer;
125        if da_outer <= 0. {
126            return None;
127        }
128
129        // Calculate the start and end points of the outer arc.
130        let x01 = center_x + r1 * a0_outer.cos();
131        let y01 = center_y + r1 * a0_outer.sin();
132        let x11 = center_x + r1 * a1_outer.cos();
133        let y11 = center_y + r1 * a1_outer.sin();
134
135        let mut builder = PathBuilder::fill();
136
137        // Move to the start point of the outer arc.
138        builder.move_to(point(px(x01), px(y01)));
139
140        // Draw the outer arc.
141        let large_arc = (a1_outer - a0_outer).abs() > PI;
142        builder.arc_to(
143            point(px(r1), px(r1)),
144            px(0.),
145            large_arc,
146            true,
147            point(px(x11), px(y11)),
148        );
149
150        if r0 > EPSILON {
151            // End point of the inner arc.
152            let x10 = center_x + r0 * a1_inner.cos();
153            let y10 = center_y + r0 * a1_inner.sin();
154            builder.line_to(point(px(x10), px(y10)));
155
156            // Draw the inner arc.
157            let x00 = center_x + r0 * a0_inner.cos();
158            let y00 = center_y + r0 * a0_inner.sin();
159            let large_arc_inner = (a1_inner - a0_inner).abs() > PI;
160            builder.arc_to(
161                point(px(r0), px(r0)),
162                px(0.),
163                large_arc_inner,
164                false,
165                point(px(x00), px(y00)),
166            );
167        } else {
168            // If there is no inner radius, draw a line to the center.
169            builder.line_to(point(px(center_x), px(center_y)));
170        }
171
172        builder.build().ok()
173    }
174
175    /// Paint the Arc.
176    pub fn paint<T>(
177        &self,
178        arc: &ArcData<T>,
179        color: impl Into<Hsla>,
180        inner_radius: Option<f32>,
181        outer_radius: Option<f32>,
182        bounds: &Bounds<Pixels>,
183        window: &mut Window,
184    ) {
185        let path = self.path(arc, inner_radius, outer_radius, bounds);
186        if let Some(path) = path {
187            window.paint_path(path, color.into());
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_arc_default() {
198        let arc = Arc::default();
199        assert_eq!(arc.inner_radius, 0.);
200        assert_eq!(arc.outer_radius, 0.);
201    }
202
203    #[test]
204    fn test_arc_builder() {
205        let arc = Arc::new().inner_radius(10.).outer_radius(20.);
206
207        assert_eq!(arc.inner_radius, 10.);
208        assert_eq!(arc.outer_radius, 20.);
209    }
210
211    #[test]
212    fn test_arc_centroid() {
213        let arc = Arc::new().inner_radius(10.).outer_radius(20.);
214
215        let arc_data = ArcData {
216            data: &(),
217            index: 0,
218            value: 1.,
219            start_angle: 0.,
220            end_angle: PI,
221            pad_angle: 0.,
222        };
223
224        let centroid = arc.centroid(&arc_data);
225        let expected_radius = (10. + 20.) / 2.;
226        let expected_angle = (0. + PI - 2. * HALF_PI) / 2.;
227
228        assert_eq!(centroid.x, expected_radius * expected_angle.cos());
229        assert_eq!(centroid.y, expected_radius * expected_angle.sin());
230    }
231}