rgpui_component/plot/shape/
arc.rs1use 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 pub fn inner_radius(mut self, inner_radius: f32) -> Self {
50 self.inner_radius = inner_radius;
51 self
52 }
53
54 pub fn outer_radius(mut self, outer_radius: f32) -> Self {
56 self.outer_radius = outer_radius;
57 self
58 }
59
60 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 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 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 if r1 < EPSILON || da.abs() < EPSILON {
96 return None;
97 }
98
99 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 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 builder.move_to(point(px(x01), px(y01)));
139
140 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 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 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 builder.line_to(point(px(center_x), px(center_y)));
170 }
171
172 builder.build().ok()
173 }
174
175 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}