gpui_component/plot/shape/
arc.rs1use 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 pub fn inner_radius(mut self, inner_radius: f32) -> Self {
52 self.inner_radius = inner_radius;
53 self
54 }
55
56 pub fn outer_radius(mut self, outer_radius: f32) -> Self {
58 self.outer_radius = outer_radius;
59 self
60 }
61
62 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 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 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 if r1 < EPSILON || da.abs() < EPSILON {
92 return None;
93 }
94
95 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 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 builder.move_to(point(px(x01), px(y01)));
135
136 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 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 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 builder.line_to(point(px(center_x), px(center_y)));
166 }
167
168 builder.build().ok()
169 }
170
171 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}