Coverage Report

Created: 2025-12-31 07:38

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.13.0/src/circle.rs
Line
Count
Source
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(always)]
31
0
    pub fn new(center: impl Into<Point>, radius: f64) -> Circle {
32
0
        Circle {
33
0
            center: center.into(),
34
0
            radius,
35
0
        }
36
0
    }
37
38
    /// Create a [`CircleSegment`] by cutting out parts of this circle.
39
    #[inline(always)]
40
0
    pub fn segment(self, inner_radius: f64, start_angle: f64, sweep_angle: f64) -> CircleSegment {
41
0
        CircleSegment {
42
0
            center: self.center,
43
0
            outer_radius: self.radius,
44
0
            inner_radius,
45
0
            start_angle,
46
0
            sweep_angle,
47
0
        }
48
0
    }
49
50
    /// Is this circle [finite]?
51
    ///
52
    /// [finite]: f64::is_finite
53
    #[inline]
54
0
    pub fn is_finite(&self) -> bool {
55
0
        self.center.is_finite() && self.radius.is_finite()
56
0
    }
57
58
    /// Is this circle [NaN]?
59
    ///
60
    /// [NaN]: f64::is_nan
61
    #[inline]
62
0
    pub fn is_nan(&self) -> bool {
63
0
        self.center.is_nan() || self.radius.is_nan()
64
0
    }
65
}
66
67
impl Add<Vec2> for Circle {
68
    type Output = Circle;
69
70
    #[inline]
71
0
    fn add(self, v: Vec2) -> Circle {
72
0
        Circle {
73
0
            center: self.center + v,
74
0
            radius: self.radius,
75
0
        }
76
0
    }
77
}
78
79
impl Sub<Vec2> for Circle {
80
    type Output = Circle;
81
82
    #[inline]
83
0
    fn sub(self, v: Vec2) -> Circle {
84
0
        Circle {
85
0
            center: self.center - v,
86
0
            radius: self.radius,
87
0
        }
88
0
    }
89
}
90
91
impl Mul<Circle> for Affine {
92
    type Output = Ellipse;
93
0
    fn mul(self, other: Circle) -> Self::Output {
94
0
        self * Ellipse::from(other)
95
0
    }
96
}
97
98
#[doc(hidden)]
99
pub struct CirclePathIter {
100
    circle: Circle,
101
    delta_th: f64,
102
    arm_len: f64,
103
    ix: usize,
104
    n: usize,
105
}
106
107
impl Shape for Circle {
108
    type PathElementsIter<'iter> = CirclePathIter;
109
110
0
    fn path_elements(&self, tolerance: f64) -> CirclePathIter {
111
0
        let scaled_err = self.radius.abs() / tolerance;
112
0
        let (n, arm_len) = if scaled_err < 1.0 / 1.9608e-4 {
113
            // Solution from http://spencermortensen.com/articles/bezier-circle/
114
0
            (4, 0.551915024494)
115
        } else {
116
            // This is empirically determined to fall within error tolerance.
117
0
            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
0
            let arm_len = (4.0 / 3.0) * (FRAC_PI_2 / (n as f64)).tan();
121
0
            (n, arm_len)
122
        };
123
0
        CirclePathIter {
124
0
            circle: *self,
125
0
            delta_th: 2.0 * PI / (n as f64),
126
0
            arm_len,
127
0
            ix: 0,
128
0
            n,
129
0
        }
130
0
    }
131
132
    #[inline]
133
0
    fn area(&self) -> f64 {
134
0
        PI * self.radius.powi(2)
135
0
    }
136
137
    #[inline]
138
0
    fn perimeter(&self, _accuracy: f64) -> f64 {
139
0
        (2.0 * PI * self.radius).abs()
140
0
    }
141
142
0
    fn winding(&self, pt: Point) -> i32 {
143
0
        if (pt - self.center).hypot2() < self.radius.powi(2) {
144
0
            1
145
        } else {
146
0
            0
147
        }
148
0
    }
149
150
    #[inline]
151
0
    fn bounding_box(&self) -> Rect {
152
0
        let r = self.radius.abs();
153
0
        let (x, y) = self.center.into();
154
0
        Rect::new(x - r, y - r, x + r, y + r)
155
0
    }
156
157
    #[inline(always)]
158
0
    fn as_circle(&self) -> Option<Circle> {
159
0
        Some(*self)
160
0
    }
161
}
162
163
impl Iterator for CirclePathIter {
164
    type Item = PathEl;
165
166
0
    fn next(&mut self) -> Option<PathEl> {
167
0
        let a = self.arm_len;
168
0
        let r = self.circle.radius;
169
0
        let (x, y) = self.circle.center.into();
170
0
        let ix = self.ix;
171
0
        self.ix += 1;
172
0
        if ix == 0 {
173
0
            Some(PathEl::MoveTo(Point::new(x + r, y)))
174
0
        } else if ix <= self.n {
175
0
            let th1 = self.delta_th * (ix as f64);
176
0
            let th0 = th1 - self.delta_th;
177
0
            let (s0, c0) = th0.sin_cos();
178
0
            let (s1, c1) = if ix == self.n {
179
0
                (0.0, 1.0)
180
            } else {
181
0
                th1.sin_cos()
182
            };
183
0
            Some(PathEl::CurveTo(
184
0
                Point::new(x + r * (c0 - a * s0), y + r * (s0 + a * c0)),
185
0
                Point::new(x + r * (c1 + a * s1), y + r * (s1 - a * c1)),
186
0
                Point::new(x + r * c1, y + r * s1),
187
0
            ))
188
0
        } else if ix == self.n + 1 {
189
0
            Some(PathEl::ClosePath)
190
        } else {
191
0
            None
192
        }
193
0
    }
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))]
202
pub 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
215
impl CircleSegment {
216
    /// Create a `CircleSegment` out of its constituent parts.
217
    #[inline(always)]
218
0
    pub fn new(
219
0
        center: impl Into<Point>,
220
0
        outer_radius: f64,
221
0
        inner_radius: f64,
222
0
        start_angle: f64,
223
0
        sweep_angle: f64,
224
0
    ) -> Self {
225
0
        CircleSegment {
226
0
            center: center.into(),
227
0
            outer_radius,
228
0
            inner_radius,
229
0
            start_angle,
230
0
            sweep_angle,
231
0
        }
232
0
    }
233
234
    /// Return an arc representing the outer radius.
235
    #[must_use]
236
    #[inline(always)]
237
0
    pub fn outer_arc(&self) -> Arc {
238
0
        Arc {
239
0
            center: self.center,
240
0
            radii: Vec2::new(self.outer_radius, self.outer_radius),
241
0
            start_angle: self.start_angle,
242
0
            sweep_angle: self.sweep_angle,
243
0
            x_rotation: 0.0,
244
0
        }
245
0
    }
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
0
    pub fn inner_arc(&self) -> Arc {
257
0
        Arc {
258
0
            center: self.center,
259
0
            radii: Vec2::new(self.inner_radius, self.inner_radius),
260
0
            start_angle: self.start_angle + self.sweep_angle,
261
0
            sweep_angle: -self.sweep_angle,
262
0
            x_rotation: 0.0,
263
0
        }
264
0
    }
265
266
    /// Is this circle segment [finite]?
267
    ///
268
    /// [finite]: f64::is_finite
269
    #[inline]
270
0
    pub const fn is_finite(&self) -> bool {
271
0
        self.center.is_finite()
272
0
            && self.outer_radius.is_finite()
273
0
            && self.inner_radius.is_finite()
274
0
            && self.start_angle.is_finite()
275
0
            && self.sweep_angle.is_finite()
276
0
    }
277
278
    /// Is this circle segment [NaN]?
279
    ///
280
    /// [NaN]: f64::is_nan
281
    #[inline]
282
0
    pub const fn is_nan(&self) -> bool {
283
0
        self.center.is_nan()
284
0
            || self.outer_radius.is_nan()
285
0
            || self.inner_radius.is_nan()
286
0
            || self.start_angle.is_nan()
287
0
            || self.sweep_angle.is_nan()
288
0
    }
289
}
290
291
impl Add<Vec2> for CircleSegment {
292
    type Output = CircleSegment;
293
294
    #[inline]
295
0
    fn add(self, v: Vec2) -> Self {
296
0
        Self {
297
0
            center: self.center + v,
298
0
            ..self
299
0
        }
300
0
    }
301
}
302
303
impl Sub<Vec2> for CircleSegment {
304
    type Output = CircleSegment;
305
306
    #[inline]
307
0
    fn sub(self, v: Vec2) -> Self {
308
0
        Self {
309
0
            center: self.center - v,
310
0
            ..self
311
0
        }
312
0
    }
313
}
314
315
type 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
323
impl Shape for CircleSegment {
324
    type PathElementsIter<'iter> = CircleSegmentPathIter;
325
326
0
    fn path_elements(&self, tolerance: f64) -> CircleSegmentPathIter {
327
0
        iter::once(PathEl::MoveTo(point_on_circle(
328
0
            self.center,
329
0
            self.inner_radius,
330
0
            self.start_angle,
331
0
        )))
332
        // First radius
333
0
        .chain(iter::once(PathEl::LineTo(point_on_circle(
334
0
            self.center,
335
0
            self.outer_radius,
336
0
            self.start_angle,
337
0
        ))))
338
        // outer arc
339
0
        .chain(self.outer_arc().append_iter(tolerance))
340
        // second radius
341
0
        .chain(iter::once(PathEl::LineTo(point_on_circle(
342
0
            self.center,
343
0
            self.inner_radius,
344
0
            self.start_angle + self.sweep_angle,
345
0
        ))))
346
        // inner arc
347
0
        .chain(self.inner_arc().append_iter(tolerance))
348
0
    }
349
350
    #[inline]
351
0
    fn area(&self) -> f64 {
352
0
        0.5 * (self.outer_radius.powi(2) - self.inner_radius.powi(2)).abs() * self.sweep_angle
353
0
    }
354
355
    #[inline]
356
0
    fn perimeter(&self, _accuracy: f64) -> f64 {
357
0
        2.0 * (self.outer_radius - self.inner_radius).abs()
358
0
            + self.sweep_angle * (self.inner_radius + self.outer_radius)
359
0
    }
360
361
0
    fn winding(&self, pt: Point) -> i32 {
362
0
        let angle = (pt - self.center).atan2();
363
0
        if angle < self.start_angle || angle > self.start_angle + self.sweep_angle {
364
0
            return 0;
365
0
        }
366
0
        let dist2 = (pt - self.center).hypot2();
367
0
        if (dist2 < self.outer_radius.powi(2) && dist2 > self.inner_radius.powi(2)) ||
368
            // case where outer_radius < inner_radius
369
0
            (dist2 < self.inner_radius.powi(2) && dist2 > self.outer_radius.powi(2))
370
        {
371
0
            1
372
        } else {
373
0
            0
374
        }
375
0
    }
376
377
    #[inline]
378
0
    fn bounding_box(&self) -> Rect {
379
        // todo this is currently not tight
380
0
        let r = self.inner_radius.max(self.outer_radius);
381
0
        let (x, y) = self.center.into();
382
0
        Rect::new(x - r, y - r, x + r, y + r)
383
0
    }
384
}
385
386
#[inline]
387
0
fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point {
388
0
    let (angle_sin, angle_cos) = angle.sin_cos();
389
0
    center
390
0
        + Vec2 {
391
0
            x: angle_cos * radius,
392
0
            y: angle_sin * radius,
393
0
        }
394
0
}
395
396
#[cfg(test)]
397
mod 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
}