Skip to main content

gpui_base/plot/shape/
arc.rs

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