1 | // Copyright 2019 the Kurbo Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | //! Implementation of circle shape. |
5 | |
6 | use core::{ |
7 | f64::consts::{FRAC_PI_2, PI}, |
8 | iter, |
9 | ops::{Add, Mul, Sub}, |
10 | }; |
11 | |
12 | use crate::{Affine, Arc, ArcAppendIter, Ellipse, PathEl, Point, Rect, Shape, Vec2}; |
13 | |
14 | #[cfg (not(feature = "std" ))] |
15 | use crate::common::FloatFuncs; |
16 | |
17 | /// A circle. |
18 | #[derive (Clone, Copy, Default, Debug, PartialEq)] |
19 | #[cfg_attr (feature = "schemars" , derive(schemars::JsonSchema))] |
20 | #[cfg_attr (feature = "serde" , derive(serde::Serialize, serde::Deserialize))] |
21 | pub struct Circle { |
22 | /// The center. |
23 | pub center: Point, |
24 | /// The radius. |
25 | pub radius: f64, |
26 | } |
27 | |
28 | impl Circle { |
29 | /// A new circle from center and radius. |
30 | #[inline ] |
31 | pub fn new(center: impl Into<Point>, radius: f64) -> Circle { |
32 | Circle { |
33 | center: center.into(), |
34 | radius, |
35 | } |
36 | } |
37 | |
38 | /// Create a [`CircleSegment`] by cutting out parts of this circle. |
39 | pub fn segment(self, inner_radius: f64, start_angle: f64, sweep_angle: f64) -> CircleSegment { |
40 | CircleSegment { |
41 | center: self.center, |
42 | outer_radius: self.radius, |
43 | inner_radius, |
44 | start_angle, |
45 | sweep_angle, |
46 | } |
47 | } |
48 | |
49 | /// Is this circle finite? |
50 | #[inline ] |
51 | pub fn is_finite(&self) -> bool { |
52 | self.center.is_finite() && self.radius.is_finite() |
53 | } |
54 | |
55 | /// Is this circle NaN? |
56 | #[inline ] |
57 | pub fn is_nan(&self) -> bool { |
58 | self.center.is_nan() || self.radius.is_nan() |
59 | } |
60 | } |
61 | |
62 | impl Add<Vec2> for Circle { |
63 | type Output = Circle; |
64 | |
65 | #[inline ] |
66 | fn add(self, v: Vec2) -> Circle { |
67 | Circle { |
68 | center: self.center + v, |
69 | radius: self.radius, |
70 | } |
71 | } |
72 | } |
73 | |
74 | impl Sub<Vec2> for Circle { |
75 | type Output = Circle; |
76 | |
77 | #[inline ] |
78 | fn sub(self, v: Vec2) -> Circle { |
79 | Circle { |
80 | center: self.center - v, |
81 | radius: self.radius, |
82 | } |
83 | } |
84 | } |
85 | |
86 | impl Mul<Circle> for Affine { |
87 | type Output = Ellipse; |
88 | fn mul(self, other: Circle) -> Self::Output { |
89 | self * Ellipse::from(other) |
90 | } |
91 | } |
92 | |
93 | #[doc (hidden)] |
94 | pub struct CirclePathIter { |
95 | circle: Circle, |
96 | delta_th: f64, |
97 | arm_len: f64, |
98 | ix: usize, |
99 | n: usize, |
100 | } |
101 | |
102 | impl Shape for Circle { |
103 | type PathElementsIter<'iter> = CirclePathIter; |
104 | |
105 | fn path_elements(&self, tolerance: f64) -> CirclePathIter { |
106 | let scaled_err = self.radius.abs() / tolerance; |
107 | let (n, arm_len) = if scaled_err < 1.0 / 1.9608e-4 { |
108 | // Solution from http://spencermortensen.com/articles/bezier-circle/ |
109 | (4, 0.551915024494) |
110 | } else { |
111 | // This is empirically determined to fall within error tolerance. |
112 | let n = (1.1163 * scaled_err).powf(1.0 / 6.0).ceil() as usize; |
113 | // Note: this isn't minimum error, but it is simple and we can easily |
114 | // estimate the error. |
115 | let arm_len = (4.0 / 3.0) * (FRAC_PI_2 / (n as f64)).tan(); |
116 | (n, arm_len) |
117 | }; |
118 | CirclePathIter { |
119 | circle: *self, |
120 | delta_th: 2.0 * PI / (n as f64), |
121 | arm_len, |
122 | ix: 0, |
123 | n, |
124 | } |
125 | } |
126 | |
127 | #[inline ] |
128 | fn area(&self) -> f64 { |
129 | PI * self.radius.powi(2) |
130 | } |
131 | |
132 | #[inline ] |
133 | fn perimeter(&self, _accuracy: f64) -> f64 { |
134 | (2.0 * PI * self.radius).abs() |
135 | } |
136 | |
137 | fn winding(&self, pt: Point) -> i32 { |
138 | if (pt - self.center).hypot2() < self.radius.powi(2) { |
139 | 1 |
140 | } else { |
141 | 0 |
142 | } |
143 | } |
144 | |
145 | #[inline ] |
146 | fn bounding_box(&self) -> Rect { |
147 | let r = self.radius.abs(); |
148 | let (x, y) = self.center.into(); |
149 | Rect::new(x - r, y - r, x + r, y + r) |
150 | } |
151 | |
152 | fn as_circle(&self) -> Option<Circle> { |
153 | Some(*self) |
154 | } |
155 | } |
156 | |
157 | impl Iterator for CirclePathIter { |
158 | type Item = PathEl; |
159 | |
160 | fn next(&mut self) -> Option<PathEl> { |
161 | let a = self.arm_len; |
162 | let r = self.circle.radius; |
163 | let (x, y) = self.circle.center.into(); |
164 | let ix = self.ix; |
165 | self.ix += 1; |
166 | if ix == 0 { |
167 | Some(PathEl::MoveTo(Point::new(x + r, y))) |
168 | } else if ix <= self.n { |
169 | let th1 = self.delta_th * (ix as f64); |
170 | let th0 = th1 - self.delta_th; |
171 | let (s0, c0) = th0.sin_cos(); |
172 | let (s1, c1) = if ix == self.n { |
173 | (0.0, 1.0) |
174 | } else { |
175 | th1.sin_cos() |
176 | }; |
177 | Some(PathEl::CurveTo( |
178 | Point::new(x + r * (c0 - a * s0), y + r * (s0 + a * c0)), |
179 | Point::new(x + r * (c1 + a * s1), y + r * (s1 - a * c1)), |
180 | Point::new(x + r * c1, y + r * s1), |
181 | )) |
182 | } else if ix == self.n + 1 { |
183 | Some(PathEl::ClosePath) |
184 | } else { |
185 | None |
186 | } |
187 | } |
188 | } |
189 | |
190 | /// A segment of a circle. |
191 | /// |
192 | /// If `inner_radius > 0`, then the shape will be a doughnut segment. |
193 | pub struct CircleSegment { |
194 | /// The center. |
195 | pub center: Point, |
196 | /// The outer radius. |
197 | pub outer_radius: f64, |
198 | /// The inner radius. |
199 | pub inner_radius: f64, |
200 | /// The angle to start drawing the segment (in radians). |
201 | pub start_angle: f64, |
202 | /// The arc length of the segment (in radians). |
203 | pub sweep_angle: f64, |
204 | } |
205 | |
206 | impl CircleSegment { |
207 | /// Create a `CircleSegment` out of its constituent parts. |
208 | pub fn new( |
209 | center: impl Into<Point>, |
210 | outer_radius: f64, |
211 | inner_radius: f64, |
212 | start_angle: f64, |
213 | sweep_angle: f64, |
214 | ) -> Self { |
215 | CircleSegment { |
216 | center: center.into(), |
217 | outer_radius, |
218 | inner_radius, |
219 | start_angle, |
220 | sweep_angle, |
221 | } |
222 | } |
223 | |
224 | /// Is this circle segment finite? |
225 | #[inline ] |
226 | pub fn is_finite(&self) -> bool { |
227 | self.center.is_finite() |
228 | && self.outer_radius.is_finite() |
229 | && self.inner_radius.is_finite() |
230 | && self.start_angle.is_finite() |
231 | && self.sweep_angle.is_finite() |
232 | } |
233 | |
234 | /// Is this circle segment NaN? |
235 | #[inline ] |
236 | pub fn is_nan(&self) -> bool { |
237 | self.center.is_nan() |
238 | || self.outer_radius.is_nan() |
239 | || self.inner_radius.is_nan() |
240 | || self.start_angle.is_nan() |
241 | || self.sweep_angle.is_nan() |
242 | } |
243 | } |
244 | |
245 | impl Add<Vec2> for CircleSegment { |
246 | type Output = CircleSegment; |
247 | |
248 | #[inline ] |
249 | fn add(self, v: Vec2) -> Self { |
250 | Self { |
251 | center: self.center + v, |
252 | ..self |
253 | } |
254 | } |
255 | } |
256 | |
257 | impl Sub<Vec2> for CircleSegment { |
258 | type Output = CircleSegment; |
259 | |
260 | #[inline ] |
261 | fn sub(self, v: Vec2) -> Self { |
262 | Self { |
263 | center: self.center - v, |
264 | ..self |
265 | } |
266 | } |
267 | } |
268 | |
269 | type CircleSegmentPathIter = iter::Chain< |
270 | iter::Chain< |
271 | iter::Chain<iter::Chain<iter::Once<PathEl>, iter::Once<PathEl>>, ArcAppendIter>, |
272 | iter::Once<PathEl>, |
273 | >, |
274 | ArcAppendIter, |
275 | >; |
276 | |
277 | impl Shape for CircleSegment { |
278 | type PathElementsIter<'iter> = CircleSegmentPathIter; |
279 | |
280 | fn path_elements(&self, tolerance: f64) -> CircleSegmentPathIter { |
281 | iter::once(PathEl::MoveTo(point_on_circle( |
282 | self.center, |
283 | self.inner_radius, |
284 | self.start_angle, |
285 | ))) |
286 | // First radius |
287 | .chain(iter::once(PathEl::LineTo(point_on_circle( |
288 | self.center, |
289 | self.outer_radius, |
290 | self.start_angle, |
291 | )))) |
292 | // outer arc |
293 | .chain( |
294 | Arc { |
295 | center: self.center, |
296 | radii: Vec2::new(self.outer_radius, self.outer_radius), |
297 | start_angle: self.start_angle, |
298 | sweep_angle: self.sweep_angle, |
299 | x_rotation: 0.0, |
300 | } |
301 | .append_iter(tolerance), |
302 | ) |
303 | // second radius |
304 | .chain(iter::once(PathEl::LineTo(point_on_circle( |
305 | self.center, |
306 | self.inner_radius, |
307 | self.start_angle + self.sweep_angle, |
308 | )))) |
309 | // inner arc |
310 | .chain( |
311 | Arc { |
312 | center: self.center, |
313 | radii: Vec2::new(self.inner_radius, self.inner_radius), |
314 | start_angle: self.start_angle + self.sweep_angle, |
315 | sweep_angle: -self.sweep_angle, |
316 | x_rotation: 0.0, |
317 | } |
318 | .append_iter(tolerance), |
319 | ) |
320 | } |
321 | |
322 | #[inline ] |
323 | fn area(&self) -> f64 { |
324 | 0.5 * (self.outer_radius.powi(2) - self.inner_radius.powi(2)).abs() * self.sweep_angle |
325 | } |
326 | |
327 | #[inline ] |
328 | fn perimeter(&self, _accuracy: f64) -> f64 { |
329 | 2.0 * (self.outer_radius - self.inner_radius).abs() |
330 | + self.sweep_angle * (self.inner_radius + self.outer_radius) |
331 | } |
332 | |
333 | fn winding(&self, pt: Point) -> i32 { |
334 | let angle = (pt - self.center).atan2(); |
335 | if angle < self.start_angle || angle > self.start_angle + self.sweep_angle { |
336 | return 0; |
337 | } |
338 | let dist2 = (pt - self.center).hypot2(); |
339 | if (dist2 < self.outer_radius.powi(2) && dist2 > self.inner_radius.powi(2)) || |
340 | // case where outer_radius < inner_radius |
341 | (dist2 < self.inner_radius.powi(2) && dist2 > self.outer_radius.powi(2)) |
342 | { |
343 | 1 |
344 | } else { |
345 | 0 |
346 | } |
347 | } |
348 | |
349 | #[inline ] |
350 | fn bounding_box(&self) -> Rect { |
351 | // todo this is currently not tight |
352 | let r = self.inner_radius.max(self.outer_radius); |
353 | let (x, y) = self.center.into(); |
354 | Rect::new(x - r, y - r, x + r, y + r) |
355 | } |
356 | } |
357 | |
358 | #[inline ] |
359 | fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point { |
360 | let (angle_sin: f64, angle_cos: f64) = angle.sin_cos(); |
361 | center |
362 | + Vec2 { |
363 | x: angle_cos * radius, |
364 | y: angle_sin * radius, |
365 | } |
366 | } |
367 | |
368 | #[cfg (test)] |
369 | mod tests { |
370 | use crate::{Circle, Point, Shape}; |
371 | use std::f64::consts::PI; |
372 | |
373 | fn assert_approx_eq(x: f64, y: f64) { |
374 | // Note: we might want to be more rigorous in testing the accuracy |
375 | // of the conversion into Béziers. But this seems good enough. |
376 | assert!((x - y).abs() < 1e-7, " {x} != {y}" ); |
377 | } |
378 | |
379 | #[test ] |
380 | fn area_sign() { |
381 | let center = Point::new(5.0, 5.0); |
382 | let c = Circle::new(center, 5.0); |
383 | assert_approx_eq(c.area(), 25.0 * PI); |
384 | |
385 | assert_eq!(c.winding(center), 1); |
386 | |
387 | let p = c.to_path(1e-9); |
388 | assert_approx_eq(c.area(), p.area()); |
389 | assert_eq!(c.winding(center), p.winding(center)); |
390 | |
391 | let c_neg_radius = Circle::new(center, -5.0); |
392 | assert_approx_eq(c_neg_radius.area(), 25.0 * PI); |
393 | |
394 | assert_eq!(c_neg_radius.winding(center), 1); |
395 | |
396 | let p_neg_radius = c_neg_radius.to_path(1e-9); |
397 | assert_approx_eq(c_neg_radius.area(), p_neg_radius.area()); |
398 | assert_eq!(c_neg_radius.winding(center), p_neg_radius.winding(center)); |
399 | } |
400 | } |
401 | |