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