Skip to main content

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::{Bounds, Hsla, Path, PathBuilder, Pixels, Point, Window, point, px};
6
7use crate::plot::{PathCache, ShapeKey};
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>(
73        &self,
74        arc: &ArcData<T>,
75        inner_radius: Option<f32>,
76        outer_radius: Option<f32>,
77        bounds: &Bounds<Pixels>,
78    ) -> Option<Path<Pixels>> {
79        let start_angle = arc.start_angle - HALF_PI;
80        let end_angle = arc.end_angle - HALF_PI;
81        let da = end_angle - start_angle;
82        let pad_angle = if da >= PI {
83            // Leave some pad angle for full circle.
84            // If not, the path start and end will be the same point.
85            0.0001
86        } else {
87            arc.pad_angle
88        };
89        let r0 = inner_radius.unwrap_or(self.inner_radius).max(0.);
90        let r1 = outer_radius.unwrap_or(self.outer_radius).max(0.);
91
92        // Calculate the center point.
93        let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
94        let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
95
96        // Angle difference.
97        if r1 < EPSILON || da.abs() < EPSILON {
98            return None;
99        }
100
101        // Handle pad angle.
102        let (a0_outer, a1_outer, a0_inner, a1_inner) = if r0 > EPSILON && pad_angle > 0.0 {
103            let pad_width = r1 * pad_angle;
104            let pad_angle_outer = pad_width / r1;
105            let mut pad_angle_inner = pad_width / r0;
106            let max_inner_pad = da * 0.8;
107            if pad_angle_inner > max_inner_pad {
108                pad_angle_inner = max_inner_pad;
109            }
110            (
111                start_angle + pad_angle_outer * 0.5,
112                end_angle - pad_angle_outer * 0.5,
113                start_angle + pad_angle_inner * 0.5,
114                end_angle - pad_angle_inner * 0.5,
115            )
116        } else {
117            let pad = pad_angle * 0.5;
118            (
119                start_angle + pad,
120                end_angle - pad,
121                start_angle + pad,
122                end_angle - pad,
123            )
124        };
125
126        let da_outer = a1_outer - a0_outer;
127        if da_outer <= 0. {
128            return None;
129        }
130
131        // Calculate the start and end points of the outer arc.
132        let x01 = center_x + r1 * a0_outer.cos();
133        let y01 = center_y + r1 * a0_outer.sin();
134        let x11 = center_x + r1 * a1_outer.cos();
135        let y11 = center_y + r1 * a1_outer.sin();
136
137        let mut builder = PathBuilder::fill();
138
139        // Move to the start point of the outer arc.
140        builder.move_to(point(px(x01), px(y01)));
141
142        // Draw the outer arc.
143        let large_arc = (a1_outer - a0_outer).abs() > PI;
144        builder.arc_to(
145            point(px(r1), px(r1)),
146            px(0.),
147            large_arc,
148            true,
149            point(px(x11), px(y11)),
150        );
151
152        if r0 > EPSILON {
153            // End point of the inner arc.
154            let x10 = center_x + r0 * a1_inner.cos();
155            let y10 = center_y + r0 * a1_inner.sin();
156            builder.line_to(point(px(x10), px(y10)));
157
158            // Draw the inner arc.
159            let x00 = center_x + r0 * a0_inner.cos();
160            let y00 = center_y + r0 * a0_inner.sin();
161            let large_arc_inner = (a1_inner - a0_inner).abs() > PI;
162            builder.arc_to(
163                point(px(r0), px(r0)),
164                px(0.),
165                large_arc_inner,
166                false,
167                point(px(x00), px(y00)),
168            );
169        } else {
170            // If there is no inner radius, draw a line to the center.
171            builder.line_to(point(px(center_x), px(center_y)));
172        }
173
174        builder.build().ok()
175    }
176
177    /// Whether the cursor at `position` (relative to the bounds origin) is on
178    /// this arc's slice: within its angles and between `inner_radius` and
179    /// `outer_radius` (this arc's own radii when `None`).
180    pub fn contains<T>(
181        &self,
182        arc: &ArcData<T>,
183        position: Point<f32>,
184        inner_radius: Option<f32>,
185        outer_radius: Option<f32>,
186        bounds: &Bounds<Pixels>,
187    ) -> bool {
188        let dx = position.x - bounds.size.width.as_f32() / 2.;
189        let dy = position.y - bounds.size.height.as_f32() / 2.;
190        let radius = dx.hypot(dy);
191        let r0 = inner_radius.unwrap_or(self.inner_radius).max(0.);
192        let r1 = outer_radius.unwrap_or(self.outer_radius).max(0.);
193        if radius < r0 || radius > r1 {
194            return false;
195        }
196
197        // Screen angle -> pie angle (0 at 12 o'clock, clockwise), in [0, TAU).
198        let angle = (dy.atan2(dx) + HALF_PI).rem_euclid(2. * PI);
199        (arc.start_angle..arc.end_angle).contains(&angle)
200    }
201
202    /// Paint the Arc, reusing the path tessellated by an earlier paint while its
203    /// angles, radii and the bounds size are unchanged; see
204    /// [`Line::paint_cached`](super::Line::paint_cached).
205    #[allow(clippy::too_many_arguments)]
206    pub fn paint_cached<T>(
207        &self,
208        arc: &ArcData<T>,
209        color: impl Into<Hsla>,
210        inner_radius: Option<f32>,
211        outer_radius: Option<f32>,
212        bounds: &Bounds<Pixels>,
213        cache: &mut PathCache,
214        window: &mut Window,
215    ) {
216        let key = ShapeKey::new((
217            bounds.size.width.as_f32().to_bits(),
218            bounds.size.height.as_f32().to_bits(),
219        ))
220        .f32(arc.start_angle)
221        .f32(arc.end_angle)
222        .f32(arc.pad_angle)
223        .f32(inner_radius.unwrap_or(self.inner_radius))
224        .f32(outer_radius.unwrap_or(self.outer_radius))
225        .finish();
226        let local = Bounds::new(Point::default(), bounds.size);
227        let path = cache.get(key, bounds.origin, || {
228            self.path(arc, inner_radius, outer_radius, &local)
229        });
230        if let Some(path) = path {
231            window.paint_path(path, color.into());
232        }
233    }
234
235    /// Paint the Arc.
236    pub fn paint<T>(
237        &self,
238        arc: &ArcData<T>,
239        color: impl Into<Hsla>,
240        inner_radius: Option<f32>,
241        outer_radius: Option<f32>,
242        bounds: &Bounds<Pixels>,
243        window: &mut Window,
244    ) {
245        let path = self.path(arc, inner_radius, outer_radius, bounds);
246        if let Some(path) = path {
247            window.paint_path(path, color.into());
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_arc_default() {
258        let arc = Arc::default();
259        assert_eq!(arc.inner_radius, 0.);
260        assert_eq!(arc.outer_radius, 0.);
261    }
262
263    #[test]
264    fn test_arc_builder() {
265        let arc = Arc::new().inner_radius(10.).outer_radius(20.);
266
267        assert_eq!(arc.inner_radius, 10.);
268        assert_eq!(arc.outer_radius, 20.);
269    }
270
271    #[test]
272    fn test_arc_centroid() {
273        let arc = Arc::new().inner_radius(10.).outer_radius(20.);
274
275        let arc_data = ArcData {
276            data: &(),
277            index: 0,
278            value: 1.,
279            start_angle: 0.,
280            end_angle: PI,
281            pad_angle: 0.,
282        };
283
284        let centroid = arc.centroid(&arc_data);
285        let expected_radius = (10. + 20.) / 2.;
286        let expected_angle = (0. + PI - 2. * HALF_PI) / 2.;
287
288        assert_eq!(centroid.x, expected_radius * expected_angle.cos());
289        assert_eq!(centroid.y, expected_radius * expected_angle.sin());
290    }
291
292    #[test]
293    fn test_arc_contains() {
294        use gpui::{point, px, size};
295
296        // A 100x100 plot: center (50, 50). The right half, 12 to 6 o'clock.
297        let arc = Arc::new().inner_radius(10.).outer_radius(40.);
298        let right_half = ArcData {
299            data: &(),
300            index: 0,
301            value: 1.,
302            start_angle: 0.,
303            end_angle: PI,
304            pad_angle: 0.,
305        };
306        let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
307
308        // 3 o'clock, between the radii.
309        assert!(arc.contains(&right_half, point(80., 50.), None, None, &bounds));
310        // 9 o'clock is the other half.
311        assert!(!arc.contains(&right_half, point(20., 50.), None, None, &bounds));
312        // Inside the hole and past the rim.
313        assert!(!arc.contains(&right_half, point(55., 50.), None, None, &bounds));
314        assert!(!arc.contains(&right_half, point(95., 50.), None, None, &bounds));
315        // A wider outer radius reaches the same point.
316        assert!(arc.contains(&right_half, point(95., 50.), None, Some(50.), &bounds));
317        // 12 o'clock is the start of this arc, 6 o'clock the start of the next.
318        assert!(arc.contains(&right_half, point(50., 20.), None, None, &bounds));
319        assert!(!arc.contains(&right_half, point(50., 80.), None, None, &bounds));
320    }
321}