/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 | | } |