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 asix 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 | pub fn new(center: impl Into<Point>, radii: impl Into<Vec2>, x_rotation: f64) -> Ellipse {
|
41 | let Point { x: cx, y: cy } = center.into();
|
42 | let Vec2 { x: rx, y: ry } = radii.into();
|
43 | Ellipse::private_new(Vec2 { x: cx, y: cy }, rx, ry, x_rotation)
|
44 | }
|
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 | pub fn from_rect(rect: Rect) -> Self {
|
56 | let center = rect.center().to_vec2();
|
57 | let Size { width, height } = rect.size() / 2.0;
|
58 | Ellipse::private_new(center, width, height, 0.0)
|
59 | }
|
60 |
|
61 | /// Create an ellipse from an affine transformation of the unit circle.
|
62 | #[inline ]
|
63 | pub fn from_affine(affine: Affine) -> Self {
|
64 | Ellipse { inner: affine }
|
65 | }
|
66 |
|
67 | /// Create a new `Ellipse` centered on the provided point.
|
68 | #[inline ]
|
69 | #[must_use ]
|
70 | pub fn with_center(self, new_center: Point) -> Ellipse {
|
71 | let Point { x: cx, y: cy } = new_center;
|
72 | Ellipse {
|
73 | inner: self.inner.with_translation(Vec2 { x: cx, y: cy }),
|
74 | }
|
75 | }
|
76 |
|
77 | /// Create a new `Ellipse` with the provided radii.
|
78 | #[must_use ]
|
79 | pub fn with_radii(self, new_radii: Vec2) -> Ellipse {
|
80 | let rotation = self.inner.svd().1;
|
81 | let translation = self.inner.translation();
|
82 | Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation)
|
83 | }
|
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 | pub fn with_rotation(self, rotation: f64) -> Ellipse {
|
92 | let scale = self.inner.svd().0;
|
93 | let translation = self.inner.translation();
|
94 | Ellipse::private_new(translation, scale.x, scale.y, rotation)
|
95 | }
|
96 |
|
97 | #[deprecated (since = "0.7.0" , note = "use with_rotation instead" )]
|
98 | #[must_use ]
|
99 | #[doc (hidden)]
|
100 | pub fn with_x_rotation(self, rotation_radians: f64) -> Ellipse {
|
101 | self.with_rotation(rotation_radians)
|
102 | }
|
103 |
|
104 | /// This gives us an internal method without any type conversions.
|
105 | #[inline ]
|
106 | fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse {
|
107 | // Since the circle is symmetric about the x and y axes, using absolute values for the
|
108 | // radii results in the same ellipse. For simplicity we make this change here.
|
109 | Ellipse {
|
110 | inner: Affine::translate(center)
|
111 | * Affine::rotate(x_rotation)
|
112 | * Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()),
|
113 | }
|
114 | }
|
115 |
|
116 | // Getters and setters.
|
117 |
|
118 | /// Returns the center of this ellipse.
|
119 | #[inline ]
|
120 | pub fn center(&self) -> Point {
|
121 | let Vec2 { x: cx, y: cy } = self.inner.translation();
|
122 | Point { x: cx, y: cy }
|
123 | }
|
124 |
|
125 | /// Returns the two radii of this ellipse.
|
126 | ///
|
127 | /// The first number is the horizontal radius and the second is the vertical
|
128 | /// radius, before rotation.
|
129 | pub fn radii(&self) -> Vec2 {
|
130 | self.inner.svd().0
|
131 | }
|
132 |
|
133 | /// The ellipse's rotation, in radians.
|
134 | ///
|
135 | /// This allows all possible ellipses to be drawn by always starting with
|
136 | /// an ellipse with the two radii on the x and y axes.
|
137 | pub fn rotation(&self) -> f64 {
|
138 | self.inner.svd().1
|
139 | }
|
140 |
|
141 | /// Returns the radii and the rotation of this ellipse.
|
142 | ///
|
143 | /// Equivalent to `(self.radii(), self.rotation())` but more efficient.
|
144 | pub fn radii_and_rotation(&self) -> (Vec2, f64) {
|
145 | self.inner.svd()
|
146 | }
|
147 |
|
148 | /// Is this ellipse finite?
|
149 | #[inline ]
|
150 | pub fn is_finite(&self) -> bool {
|
151 | self.inner.is_finite()
|
152 | }
|
153 |
|
154 | /// Is this ellipse NaN?
|
155 | #[inline ]
|
156 | pub fn is_nan(&self) -> bool {
|
157 | self.inner.is_nan()
|
158 | }
|
159 |
|
160 | #[doc (hidden)]
|
161 | #[deprecated (since = "0.7.0" , note = "use rotation() instead" )]
|
162 | pub fn x_rotation(&self) -> f64 {
|
163 | self.rotation()
|
164 | }
|
165 | }
|
166 |
|
167 | impl Add<Vec2> for Ellipse {
|
168 | type Output = Ellipse;
|
169 |
|
170 | /// In this context adding a `Vec2` applies the corresponding translation to the ellipse.
|
171 | #[inline ]
|
172 | #[allow (clippy::suspicious_arithmetic_impl)]
|
173 | fn add(self, v: Vec2) -> Ellipse {
|
174 | Ellipse {
|
175 | inner: Affine::translate(v) * self.inner,
|
176 | }
|
177 | }
|
178 | }
|
179 |
|
180 | impl Sub<Vec2> for Ellipse {
|
181 | type Output = Ellipse;
|
182 |
|
183 | /// In this context subtracting a `Vec2` applies the corresponding translation to the ellipse.
|
184 | #[inline ]
|
185 | fn sub(self, v: Vec2) -> Ellipse {
|
186 | Ellipse {
|
187 | inner: Affine::translate(-v) * self.inner,
|
188 | }
|
189 | }
|
190 | }
|
191 |
|
192 | impl Mul<Ellipse> for Affine {
|
193 | type Output = Ellipse;
|
194 | fn mul(self, other: Ellipse) -> Self::Output {
|
195 | Ellipse {
|
196 | inner: self * other.inner,
|
197 | }
|
198 | }
|
199 | }
|
200 |
|
201 | impl From<Circle> for Ellipse {
|
202 | fn from(circle: Circle) -> Self {
|
203 | Ellipse::new(circle.center, radii:Vec2::splat(circle.radius), x_rotation:0.0)
|
204 | }
|
205 | }
|
206 |
|
207 | impl Shape for Ellipse {
|
208 | type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
|
209 |
|
210 | fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
|
211 | let (radii, x_rotation) = self.inner.svd();
|
212 | Arc {
|
213 | center: self.center(),
|
214 | radii,
|
215 | start_angle: 0.0,
|
216 | sweep_angle: 2.0 * PI,
|
217 | x_rotation,
|
218 | }
|
219 | .path_elements(tolerance)
|
220 | }
|
221 |
|
222 | #[inline ]
|
223 | fn area(&self) -> f64 {
|
224 | let Vec2 { x, y } = self.radii();
|
225 | PI * x * y
|
226 | }
|
227 |
|
228 | #[inline ]
|
229 | fn perimeter(&self, accuracy: f64) -> f64 {
|
230 | // TODO rather than delegate to the bezier path, it is possible to use various series
|
231 | // expansions to compute the perimeter to any accuracy. I believe Ramanujan authored the
|
232 | // quickest to converge. See
|
233 | // https://www.mathematica-journal.com/2009/11/23/on-the-perimeter-of-an-ellipse/
|
234 | // and https://en.wikipedia.org/wiki/Ellipse#Circumference
|
235 | //
|
236 | self.path_segments(0.1).perimeter(accuracy)
|
237 | }
|
238 |
|
239 | fn winding(&self, pt: Point) -> i32 {
|
240 | // Strategy here is to apply the inverse map to the point and see if it is in the unit
|
241 | // circle.
|
242 | let inv = self.inner.inverse();
|
243 | if (inv * pt).to_vec2().hypot2() < 1.0 {
|
244 | 1
|
245 | } else {
|
246 | 0
|
247 | }
|
248 | }
|
249 |
|
250 | // Compute a tight bounding box of the ellipse.
|
251 | //
|
252 | // See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
|
253 | // radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
|
254 | // (a, b) and (c, d) if the affine map is
|
255 | //
|
256 | // a | c | e
|
257 | // -----------
|
258 | // b | d | f
|
259 | //
|
260 | // We can then use the method in the link with the translation to get the bounding box.
|
261 | #[inline ]
|
262 | fn bounding_box(&self) -> Rect {
|
263 | let aff = self.inner.as_coeffs();
|
264 | let a2 = aff[0] * aff[0];
|
265 | let b2 = aff[1] * aff[1];
|
266 | let c2 = aff[2] * aff[2];
|
267 | let d2 = aff[3] * aff[3];
|
268 | let cx = aff[4];
|
269 | let cy = aff[5];
|
270 | let range_x = (a2 + c2).sqrt();
|
271 | let range_y = (b2 + d2).sqrt();
|
272 | Rect {
|
273 | x0: cx - range_x,
|
274 | y0: cy - range_y,
|
275 | x1: cx + range_x,
|
276 | y1: cy + range_y,
|
277 | }
|
278 | }
|
279 | }
|
280 |
|
281 | #[cfg (test)]
|
282 | mod tests {
|
283 | use crate::{Ellipse, Point, Shape};
|
284 | use std::f64::consts::PI;
|
285 |
|
286 | fn assert_approx_eq(x: f64, y: f64) {
|
287 | // Note: we might want to be more rigorous in testing the accuracy
|
288 | // of the conversion into Béziers. But this seems good enough.
|
289 | assert!((x - y).abs() < 1e-7, " {x} != {y}" );
|
290 | }
|
291 |
|
292 | #[test ]
|
293 | fn area_sign() {
|
294 | let center = Point::new(5.0, 5.0);
|
295 | let e = Ellipse::new(center, (5.0, 5.0), 1.0);
|
296 | assert_approx_eq(e.area(), 25.0 * PI);
|
297 | let e = Ellipse::new(center, (5.0, 10.0), 1.0);
|
298 | assert_approx_eq(e.area(), 50.0 * PI);
|
299 |
|
300 | assert_eq!(e.winding(center), 1);
|
301 |
|
302 | let p = e.to_path(1e-9);
|
303 | assert_approx_eq(e.area(), p.area());
|
304 | assert_eq!(e.winding(center), p.winding(center));
|
305 |
|
306 | let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
|
307 | assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
|
308 |
|
309 | assert_eq!(e_neg_radius.winding(center), 1);
|
310 |
|
311 | let p_neg_radius = e_neg_radius.to_path(1e-9);
|
312 | assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
|
313 | assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
|
314 | }
|
315 | }
|
316 | |