kurbo/
circle.rs

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