Coverage Report

Created: 2025-07-23 06:50

/rust/registry/src/index.crates.io-6f17d22bba15001f/kurbo-0.11.3/src/ellipse.rs
Line
Count
Source (jump to first uncovered line)
1
// Copyright 2020 the Kurbo Authors
2
// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4
//! Implementation of ellipse shape.
5
6
use core::f64::consts::PI;
7
use core::{
8
    iter,
9
    ops::{Add, Mul, Sub},
10
};
11
12
use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2};
13
14
#[cfg(not(feature = "std"))]
15
use crate::common::FloatFuncs;
16
17
/// An ellipse.
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 Ellipse {
22
    /// All ellipses can be represented as an affine map of the unit circle,
23
    /// centred at (0, 0). Therefore we can store the ellipse as an affine map,
24
    /// with the implication that it be applied to the unit circle to recover the
25
    /// actual shape.
26
    inner: Affine,
27
}
28
29
impl Ellipse {
30
    /// Create A new ellipse with a given center, radii, and rotation.
31
    ///
32
    /// The returned ellipse will be the result of taking a circle, stretching
33
    /// it by the `radii` along the x and y axes, then rotating it from the
34
    /// x axis by `rotation` radians, before finally translating the center
35
    /// to `center`.
36
    ///
37
    /// Rotation is clockwise in a y-down coordinate system. For more on
38
    /// rotation, see [`Affine::rotate`].
39
    #[inline]
40
0
    pub fn new(center: impl Into<Point>, radii: impl Into<Vec2>, x_rotation: f64) -> Ellipse {
41
0
        let Point { x: cx, y: cy } = center.into();
42
0
        let Vec2 { x: rx, y: ry } = radii.into();
43
0
        Ellipse::private_new(Vec2 { x: cx, y: cy }, rx, ry, x_rotation)
44
0
    }
45
46
    /// Returns the largest ellipse that can be bounded by this [`Rect`].
47
    ///
48
    /// This uses the absolute width and height of the rectangle.
49
    ///
50
    /// This ellipse is always axis-aligned; to apply rotation you can call
51
    /// [`with_rotation`] with the result.
52
    ///
53
    /// [`with_rotation`]: Ellipse::with_rotation
54
    #[inline]
55
0
    pub fn from_rect(rect: Rect) -> Self {
56
0
        let center = rect.center().to_vec2();
57
0
        let Size { width, height } = rect.size() / 2.0;
58
0
        Ellipse::private_new(center, width, height, 0.0)
59
0
    }
60
61
    /// Create an ellipse from an affine transformation of the unit circle.
62
    #[inline(always)]
63
0
    pub fn from_affine(affine: Affine) -> Self {
64
0
        Ellipse { inner: affine }
65
0
    }
66
67
    /// Create a new `Ellipse` centered on the provided point.
68
    #[inline]
69
    #[must_use]
70
0
    pub fn with_center(self, new_center: impl Into<Point>) -> Ellipse {
71
0
        let Point { x: cx, y: cy } = new_center.into();
72
0
        Ellipse {
73
0
            inner: self.inner.with_translation(Vec2 { x: cx, y: cy }),
74
0
        }
75
0
    }
76
77
    /// Create a new `Ellipse` with the provided radii.
78
    #[must_use]
79
0
    pub fn with_radii(self, new_radii: Vec2) -> Ellipse {
80
0
        let rotation = self.inner.svd().1;
81
0
        let translation = self.inner.translation();
82
0
        Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation)
83
0
    }
84
85
    /// Create a new `Ellipse`, with the rotation replaced by `rotation`
86
    /// radians.
87
    ///
88
    /// The rotation is clockwise, for a y-down coordinate system. For more
89
    /// on rotation, See [`Affine::rotate`].
90
    #[must_use]
91
0
    pub fn with_rotation(self, rotation: f64) -> Ellipse {
92
0
        let scale = self.inner.svd().0;
93
0
        let translation = self.inner.translation();
94
0
        Ellipse::private_new(translation, scale.x, scale.y, rotation)
95
0
    }
96
97
    #[deprecated(since = "0.7.0", note = "use with_rotation instead")]
98
    #[must_use]
99
    #[doc(hidden)]
100
0
    pub fn with_x_rotation(self, rotation_radians: f64) -> Ellipse {
101
0
        self.with_rotation(rotation_radians)
102
0
    }
103
104
    /// This gives us an internal method without any type conversions.
105
    #[inline]
106
0
    fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse {
107
0
        // Since the circle is symmetric about the x and y axes, using absolute values for the
108
0
        // radii results in the same ellipse. For simplicity we make this change here.
109
0
        Ellipse {
110
0
            inner: Affine::translate(center)
111
0
                * Affine::rotate(x_rotation)
112
0
                * Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()),
113
0
        }
114
0
    }
115
116
    // Getters and setters.
117
118
    /// Returns the center of this ellipse.
119
    #[inline(always)]
120
0
    pub fn center(&self) -> Point {
121
0
        self.inner.translation().to_point()
122
0
    }
123
124
    /// Returns the two radii of this ellipse.
125
    ///
126
    /// The first number is the horizontal radius and the second is the vertical
127
    /// radius, before rotation.
128
0
    pub fn radii(&self) -> Vec2 {
129
0
        self.inner.svd().0
130
0
    }
131
132
    /// The ellipse's rotation, in radians.
133
    ///
134
    /// This allows all possible ellipses to be drawn by always starting with
135
    /// an ellipse with the two radii on the x and y axes.
136
0
    pub fn rotation(&self) -> f64 {
137
0
        self.inner.svd().1
138
0
    }
139
140
    /// Returns the radii and the rotation of this ellipse.
141
    ///
142
    /// Equivalent to `(self.radii(), self.rotation())` but more efficient.
143
0
    pub fn radii_and_rotation(&self) -> (Vec2, f64) {
144
0
        self.inner.svd()
145
0
    }
146
147
    /// Is this ellipse [finite]?
148
    ///
149
    /// [finite]: f64::is_finite
150
    #[inline]
151
0
    pub fn is_finite(&self) -> bool {
152
0
        self.inner.is_finite()
153
0
    }
154
155
    /// Is this ellipse [NaN]?
156
    ///
157
    /// [NaN]: f64::is_nan
158
    #[inline]
159
0
    pub fn is_nan(&self) -> bool {
160
0
        self.inner.is_nan()
161
0
    }
162
163
    #[doc(hidden)]
164
    #[deprecated(since = "0.7.0", note = "use rotation() instead")]
165
0
    pub fn x_rotation(&self) -> f64 {
166
0
        self.rotation()
167
0
    }
168
}
169
170
impl Add<Vec2> for Ellipse {
171
    type Output = Ellipse;
172
173
    /// In this context adding a `Vec2` applies the corresponding translation to the ellipse.
174
    #[inline]
175
    #[allow(clippy::suspicious_arithmetic_impl)]
176
0
    fn add(self, v: Vec2) -> Ellipse {
177
0
        Ellipse {
178
0
            inner: Affine::translate(v) * self.inner,
179
0
        }
180
0
    }
181
}
182
183
impl Sub<Vec2> for Ellipse {
184
    type Output = Ellipse;
185
186
    /// In this context subtracting a `Vec2` applies the corresponding translation to the ellipse.
187
    #[inline]
188
0
    fn sub(self, v: Vec2) -> Ellipse {
189
0
        Ellipse {
190
0
            inner: Affine::translate(-v) * self.inner,
191
0
        }
192
0
    }
193
}
194
195
impl Mul<Ellipse> for Affine {
196
    type Output = Ellipse;
197
0
    fn mul(self, other: Ellipse) -> Self::Output {
198
0
        Ellipse {
199
0
            inner: self * other.inner,
200
0
        }
201
0
    }
202
}
203
204
impl From<Circle> for Ellipse {
205
0
    fn from(circle: Circle) -> Self {
206
0
        Ellipse::new(circle.center, Vec2::splat(circle.radius), 0.0)
207
0
    }
208
}
209
210
impl Shape for Ellipse {
211
    type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
212
213
0
    fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
214
0
        let (radii, x_rotation) = self.inner.svd();
215
0
        Arc {
216
0
            center: self.center(),
217
0
            radii,
218
0
            start_angle: 0.0,
219
0
            sweep_angle: 2.0 * PI,
220
0
            x_rotation,
221
0
        }
222
0
        .path_elements(tolerance)
223
0
    }
224
225
    #[inline]
226
0
    fn area(&self) -> f64 {
227
0
        let Vec2 { x, y } = self.radii();
228
0
        PI * x * y
229
0
    }
230
231
    /// Approximate the ellipse perimeter.
232
    ///
233
    /// This uses a numerical approximation. The absolute error between the calculated perimeter
234
    /// and the true perimeter is bounded by `accuracy` (modulo floating point rounding errors).
235
    ///
236
    /// For circular ellipses (equal horizontal and vertical radii), the calculated perimeter is
237
    /// exact.
238
    #[inline]
239
0
    fn perimeter(&self, accuracy: f64) -> f64 {
240
0
        let radii = self.radii();
241
0
242
0
        // Note: the radii are calculated from an inner affine map (`self.inner`), and may be NaN.
243
0
        // Currently, constructing an ellipse with infinite radii will produce an ellipse whose
244
0
        // calculated radii are NaN.
245
0
        if !self.radii().is_finite() {
246
0
            return f64::NAN;
247
0
        }
248
0
249
0
        // Check for the trivial case where the ellipse has one of its radii equal to 0, i.e.,
250
0
        // where it describes a line, as the numerical method used breaks down with this extreme.
251
0
        if radii.x == 0. || radii.y == 0. {
252
0
            return 4. * f64::max(radii.x, radii.y);
253
0
        }
254
0
255
0
        // Evaluate an approximation based on a truncated infinite series. If it returns a good
256
0
        // enough value, we do not need to iterate.
257
0
        if kummer_elliptic_perimeter_range(radii) <= accuracy {
258
0
            return kummer_elliptic_perimeter(radii);
259
0
        }
260
0
261
0
        agm_elliptic_perimeter(accuracy, radii)
262
0
    }
263
264
0
    fn winding(&self, pt: Point) -> i32 {
265
0
        // Strategy here is to apply the inverse map to the point and see if it is in the unit
266
0
        // circle.
267
0
        let inv = self.inner.inverse();
268
0
        if (inv * pt).to_vec2().hypot2() < 1.0 {
269
0
            1
270
        } else {
271
0
            0
272
        }
273
0
    }
274
275
    // Compute a tight bounding box of the ellipse.
276
    //
277
    // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
278
    // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
279
    // (a, b) and (c, d) if the affine map is
280
    //
281
    //  a | c | e
282
    // -----------
283
    //  b | d | f
284
    //
285
    //  We can then use the method in the link with the translation to get the bounding box.
286
    #[inline]
287
0
    fn bounding_box(&self) -> Rect {
288
0
        let aff = self.inner.as_coeffs();
289
0
        let a2 = aff[0] * aff[0];
290
0
        let b2 = aff[1] * aff[1];
291
0
        let c2 = aff[2] * aff[2];
292
0
        let d2 = aff[3] * aff[3];
293
0
        let cx = aff[4];
294
0
        let cy = aff[5];
295
0
        let range_x = (a2 + c2).sqrt();
296
0
        let range_y = (b2 + d2).sqrt();
297
0
        Rect {
298
0
            x0: cx - range_x,
299
0
            y0: cy - range_y,
300
0
            x1: cx + range_x,
301
0
            y1: cy + range_y,
302
0
        }
303
0
    }
304
}
305
306
/// Calculates circumference C of an ellipse with radii (x, y) as the infinite series
307
///
308
/// C = pi (x+y) * ∑ binom(1/2, n)^2 * h^n from n = 0 to inf
309
/// with h = (x - y)^2 / (x + y)^2
310
/// and binom(.,.) the binomial coefficient
311
///
312
/// as described by Kummer ("Über die Hypergeometrische Reihe", 1837) and rediscovered by
313
/// Linderholm and Segal ("An Overlooked Series for the Elliptic Perimeter", 1995).
314
///
315
/// The series converges very quickly for ellipses with only moderate eccentricity (`h` not close
316
/// to 1).
317
///
318
/// The series is truncated to the sixth power, meaning a lower bound on the true value is
319
/// returned. Adding the value of [`kummer_elliptic_perimeter_range`] to the value returned by this
320
/// function calculates an upper bound on the true value.
321
#[inline]
322
0
fn kummer_elliptic_perimeter(radii: Vec2) -> f64 {
323
0
    let Vec2 { x, y } = radii;
324
0
    let h = ((x - y) / (x + y)).powi(2);
325
0
    let h2 = h * h;
326
0
    let h3 = h2 * h;
327
0
    let h4 = h3 * h;
328
0
    let h5 = h4 * h;
329
0
    let h6 = h5 * h;
330
0
331
0
    let lower = PI
332
0
        + h * (PI / 4.)
333
0
        + h2 * (PI / 64.)
334
0
        + h3 * (PI / 256.)
335
0
        + h4 * (PI * 25. / 16384.)
336
0
        + h5 * (PI * 49. / 65536.)
337
0
        + h6 * (PI * 441. / 1048576.);
338
0
339
0
    (x + y) * lower
340
0
}
341
342
/// This calculates the error range of [`kummer_elliptic_perimeter`]. That function returns a lower
343
/// bound on the true value, and though we do not know what the remainder of the infinite series
344
/// sums to, we can calculate an upper bound:
345
///
346
/// ∑ binom(1/2, n)^2 for n = 0 to inf
347
///   = 1 + (1 / 2!!)^2 + (1!! / 4!!)^2 + (3!! / 6!!)^2 + (5!! / 8!!)^2 + ..
348
///   = 4 / pi
349
///   with !! the double factorial
350
/// (equation 274 in "Summation of Series", L. B. W. Jolley, 1961).
351
///
352
/// This means the remainder of the infinite series for C, assuming the series was truncated to the
353
/// mth term and h = 1, sums to
354
/// 4 / pi - ∑ binom(1/2, n)^2 for n = 0 to m-1
355
///
356
/// As 0 ≤ h ≤ 1, this is an upper bound.
357
#[inline]
358
0
fn kummer_elliptic_perimeter_range(radii: Vec2) -> f64 {
359
0
    let Vec2 { x, y } = radii;
360
0
    let h = ((x - y) / (x + y)).powi(2);
361
362
    const BINOM_SQUARED_REMAINDER: f64 = 0.00101416479131503;
363
    // = 4. / PI - (1. + 1. / 4. + 1. / 64. + 1. / 256. + 25. / 16384. + 49. / 65536. + 441. / 1048576.)
364
365
0
    PI * BINOM_SQUARED_REMAINDER * h.powi(7) * (x + y)
366
0
}
367
368
/// Calculates circumference C of an ellipse with radii (x, y) using the arithmetic-geometric mean,
369
/// as described in equation 19.8.6 of
370
/// <https://web.archive.org/web/20240926233336/https://dlmf.nist.gov/19.8#i>.
371
0
fn agm_elliptic_perimeter(accuracy: f64, radii: Vec2) -> f64 {
372
0
    let Vec2 { x, y } = if radii.x >= radii.y {
373
0
        radii
374
    } else {
375
0
        Vec2::new(radii.y, radii.x)
376
    };
377
378
0
    let accuracy = accuracy / (2. * PI * x);
379
0
380
0
    let mut sum = 1.;
381
0
    let mut a = 1.;
382
0
    let mut g = y / x;
383
0
    let mut c = (1. - g.powi(2)).sqrt();
384
0
    let mut mul = 0.5;
385
386
    loop {
387
0
        let c2 = c.powi(2);
388
0
        // term = 2^(n-1) c_n^2
389
0
        let term = mul * c2;
390
0
        sum -= term;
391
0
392
0
        // We have c_(n+1) ≤ 1/2 c_n
393
0
        // (for a derivation, see e.g. section 2.1 of  "Elliptic integrals, the
394
0
        // arithmetic-geometric mean and the Brent-Salamin algorithm for π" by G.J.O. Jameson:
395
0
        // <https://web.archive.org/web/20241002140956/https://www.maths.lancs.ac.uk/jameson/ellagm.pdf>)
396
0
        //
397
0
        // Therefore
398
0
        // ∑ 2^(i-1) c_i^2 from i = 1 ≤ ∑ 2^(i-1) ((1/2)^i c_0)^2 from i = 1
399
0
        //                            = ∑ 2^-(i+1) c_0^2          from i = 1
400
0
        //                            = 1/2 c_0^2
401
0
        //
402
0
        // or, for arbitrary starting point i = n+1:
403
0
        // ∑ 2^(i-1) c_i^2 from i = n+1 ≤ ∑ 2^(i-1) ((1/2)^(i-n) c_n)^2 from i = n+1
404
0
        //                              = ∑ 2^(2n - i - 1) c_n^2        from i = n+1
405
0
        //                              = 2^(2n) ∑ 2^(-(i+1)) c_n^2     from i = n+1
406
0
        //                              = 2^(2n) 2^(-(n+1)) c_n^2
407
0
        //                              = 2^(n-1) c_n^2
408
0
        //
409
0
        // Therefore, the remainder of the series sums to less than or equal to 2^(n-1) c_n^2,
410
0
        // which is exactly the value of the nth term.
411
0
        //
412
0
        // Furthermore, a_m ≥ g_n, and g_n ≤ 1, for all m, n.
413
0
        if term <= accuracy * g {
414
            // `sum` currently overestimates the true value - subtract the upper bound of the
415
            // remaining series. We will then underestimate the true value, but by no more than
416
            // `accuracy`.
417
0
            sum -= term;
418
0
            break;
419
0
        }
420
0
421
0
        mul *= 2.;
422
0
        // This is equal to c_next = c^2 / (4 * a_next)
423
0
        c = (a - g) / 2.;
424
0
        let a_next = (a + g) / 2.;
425
0
        g = (a * g).sqrt();
426
0
        a = a_next;
427
    }
428
429
0
    2. * PI * x / a * sum
430
0
}
431
432
#[cfg(test)]
433
mod tests {
434
    use crate::{Circle, Ellipse, Point, Shape};
435
    use std::f64::consts::PI;
436
437
    fn assert_approx_eq(x: f64, y: f64) {
438
        // Note: we might want to be more rigorous in testing the accuracy
439
        // of the conversion into Béziers. But this seems good enough.
440
        assert!((x - y).abs() < 1e-7, "{x} != {y}");
441
    }
442
443
    #[test]
444
    fn circular_perimeter() {
445
        for radius in [1.0, 0., 1.5, PI, 10.0, -1.0, 1_234_567_890.1] {
446
            let circle = Circle::new((0., 0.), radius);
447
            let ellipse = Ellipse::new((0., 0.), (radius, radius), 0.);
448
449
            let circle_p = circle.perimeter(0.);
450
            let ellipse_p = ellipse.perimeter(0.1);
451
452
            // We expect the results to be identical, modulo potential roundoff errors. Compare
453
            // with a very small relative epsilon.
454
            let epsilon = f64::EPSILON * 8. * circle_p.max(ellipse_p);
455
            assert!(
456
                (circle_p - ellipse_p).abs() <= epsilon,
457
                "Expected circular ellipse radius {ellipse_p} to be equal to circle radius {circle_p} for radius {radius}"
458
            );
459
        }
460
    }
461
462
    #[test]
463
    fn compare_perimeter_with_bez() {
464
        const EPSILON: f64 = 0.000_002;
465
466
        for radii in [
467
            (0.5, 1.),
468
            (2., 1.),
469
            (0.000_000_1, 1.),
470
            (0., 1.),
471
            (1., 0.),
472
            (-0.5, 1.),
473
            (-0.000_000_1, -1.),
474
        ] {
475
            let ellipse = Ellipse::new((0., 0.), radii, 0.);
476
477
            let ellipse_p = ellipse.perimeter(0.000_001);
478
            let bez_p = ellipse.path_segments(0.000_000_25).perimeter(0.000_000_25);
479
480
            assert!(
481
                (ellipse_p - bez_p).abs() < EPSILON,
482
                "Numerically approximated ellipse perimeter ({ellipse_p}) does not match bezier segment perimeter length ({bez_p}) for radii {radii:?}"
483
            );
484
        }
485
    }
486
487
    #[test]
488
    fn known_perimeter() {
489
        const ACCURACY: f64 = 0.000_000_000_001;
490
491
        for (radii, perimeter) in [
492
            ((0.5, 1.), 4.844_224_110_273_838),
493
            ((0.001, 1.), 4.000_015_588_104_688),
494
        ] {
495
            let ellipse = Ellipse::new((0., 0.), radii, 0.);
496
497
            let ellipse_p = ellipse.perimeter(ACCURACY);
498
            assert!(
499
                (ellipse_p - perimeter).abs() <= ACCURACY,
500
                "Numerically approximated ellipse perimeter ({ellipse_p}) does not match known perimeter ({perimeter}) radii {radii:?}"
501
            );
502
        }
503
    }
504
505
    #[test]
506
    fn area_sign() {
507
        let center = Point::new(5.0, 5.0);
508
        let e = Ellipse::new(center, (5.0, 5.0), 1.0);
509
        assert_approx_eq(e.area(), 25.0 * PI);
510
        let e = Ellipse::new(center, (5.0, 10.0), 1.0);
511
        assert_approx_eq(e.area(), 50.0 * PI);
512
513
        assert_eq!(e.winding(center), 1);
514
515
        let p = e.to_path(1e-9);
516
        assert_approx_eq(e.area(), p.area());
517
        assert_eq!(e.winding(center), p.winding(center));
518
519
        let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
520
        assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
521
522
        assert_eq!(e_neg_radius.winding(center), 1);
523
524
        let p_neg_radius = e_neg_radius.to_path(1e-9);
525
        assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
526
        assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
527
    }
528
}