gpui_component/plot/shape/
arc.rs

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