use core::f64::consts::PI;
use core::{
iter,
ops::{Add, Mul, Sub},
};
use crate::{Affine, Arc, ArcAppendIter, Circle, PathEl, Point, Rect, Shape, Size, Vec2};
#[cfg(not(feature = "std"))]
use crate::common::FloatFuncs;
#[derive(Clone, Copy, Default, Debug, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ellipse {
inner: Affine,
}
impl Ellipse {
#[inline]
pub fn new(center: impl Into<Point>, radii: impl Into<Vec2>, x_rotation: f64) -> Ellipse {
let Point { x: cx, y: cy } = center.into();
let Vec2 { x: rx, y: ry } = radii.into();
Ellipse::private_new(Vec2 { x: cx, y: cy }, rx, ry, x_rotation)
}
#[inline]
pub fn from_rect(rect: Rect) -> Self {
let center = rect.center().to_vec2();
let Size { width, height } = rect.size() / 2.0;
Ellipse::private_new(center, width, height, 0.0)
}
#[inline(always)]
pub const fn from_affine(affine: Affine) -> Self {
Ellipse { inner: affine }
}
#[inline]
#[must_use]
pub fn with_center(self, new_center: impl Into<Point>) -> Ellipse {
let Point { x: cx, y: cy } = new_center.into();
Ellipse {
inner: self.inner.with_translation(Vec2 { x: cx, y: cy }),
}
}
#[inline]
#[must_use]
pub fn with_radii(self, new_radii: Vec2) -> Ellipse {
let rotation = self.inner.svd().1;
let translation = self.inner.translation();
Ellipse::private_new(translation, new_radii.x, new_radii.y, rotation)
}
#[inline]
#[must_use]
pub fn with_rotation(self, rotation: f64) -> Ellipse {
let scale = self.inner.svd().0;
let translation = self.inner.translation();
Ellipse::private_new(translation, scale.x, scale.y, rotation)
}
#[inline]
fn private_new(center: Vec2, scale_x: f64, scale_y: f64, x_rotation: f64) -> Ellipse {
Ellipse {
inner: Affine::translate(center)
* Affine::rotate(x_rotation)
* Affine::scale_non_uniform(scale_x.abs(), scale_y.abs()),
}
}
#[inline(always)]
pub fn center(&self) -> Point {
self.inner.translation().to_point()
}
#[inline]
pub fn radii(&self) -> Vec2 {
self.inner.svd().0
}
#[inline]
pub fn major_radius(&self) -> f64 {
self.inner.svd().0.x
}
#[inline]
pub fn minor_radius(&self) -> f64 {
self.inner.svd().0.y
}
#[inline]
pub fn rotation(&self) -> f64 {
self.inner.svd().1
}
#[inline]
pub fn radii_and_rotation(&self) -> (Vec2, f64) {
self.inner.svd()
}
#[inline]
pub const fn is_finite(&self) -> bool {
self.inner.is_finite()
}
#[inline]
pub const fn is_nan(&self) -> bool {
self.inner.is_nan()
}
}
impl Add<Vec2> for Ellipse {
type Output = Ellipse;
#[inline]
#[allow(clippy::suspicious_arithmetic_impl)]
fn add(self, v: Vec2) -> Ellipse {
Ellipse {
inner: Affine::translate(v) * self.inner,
}
}
}
impl Sub<Vec2> for Ellipse {
type Output = Ellipse;
#[inline]
fn sub(self, v: Vec2) -> Ellipse {
Ellipse {
inner: Affine::translate(-v) * self.inner,
}
}
}
impl Mul<Ellipse> for Affine {
type Output = Ellipse;
#[inline]
fn mul(self, other: Ellipse) -> Self::Output {
Ellipse {
inner: self * other.inner,
}
}
}
impl From<Circle> for Ellipse {
#[inline]
fn from(circle: Circle) -> Self {
Ellipse::new(circle.center, Vec2::splat(circle.radius), 0.0)
}
}
impl Shape for Ellipse {
type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>;
fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> {
let (radii, x_rotation) = self.inner.svd();
Arc {
center: self.center(),
radii,
start_angle: 0.0,
sweep_angle: 2.0 * PI,
x_rotation,
}
.path_elements(tolerance)
}
#[inline]
fn area(&self) -> f64 {
PI * self.inner.determinant().abs()
}
#[inline]
fn perimeter(&self, accuracy: f64) -> f64 {
let radii = self.radii();
if !radii.is_finite() {
return f64::NAN;
}
if radii.x == 0. || radii.y == 0. {
return 4. * f64::max(radii.x, radii.y);
}
if kummer_elliptic_perimeter_range(radii) <= accuracy {
return kummer_elliptic_perimeter(radii);
}
agm_elliptic_perimeter(accuracy, radii)
}
#[inline]
fn winding(&self, pt: Point) -> i32 {
let inv = self.inner.inverse();
if (inv * pt).to_vec2().hypot2() < 1.0 {
1
} else {
0
}
}
#[inline]
fn bounding_box(&self) -> Rect {
let aff = self.inner.as_coeffs();
let a2 = aff[0] * aff[0];
let b2 = aff[1] * aff[1];
let c2 = aff[2] * aff[2];
let d2 = aff[3] * aff[3];
let cx = aff[4];
let cy = aff[5];
let range_x = (a2 + c2).sqrt();
let range_y = (b2 + d2).sqrt();
Rect {
x0: cx - range_x,
y0: cy - range_y,
x1: cx + range_x,
y1: cy + range_y,
}
}
}
#[inline]
fn kummer_elliptic_perimeter(radii: Vec2) -> f64 {
let Vec2 { x, y } = radii;
let h = ((x - y) / (x + y)).powi(2);
let h2 = h * h;
let h3 = h2 * h;
let h4 = h3 * h;
let h5 = h4 * h;
let h6 = h5 * h;
let lower = PI
+ h * (PI / 4.)
+ h2 * (PI / 64.)
+ h3 * (PI / 256.)
+ h4 * (PI * 25. / 16384.)
+ h5 * (PI * 49. / 65536.)
+ h6 * (PI * 441. / 1048576.);
(x + y) * lower
}
#[inline]
fn kummer_elliptic_perimeter_range(radii: Vec2) -> f64 {
let Vec2 { x, y } = radii;
let h = ((x - y) / (x + y)).powi(2);
const BINOM_SQUARED_REMAINDER: f64 = 0.00101416479131503;
PI * BINOM_SQUARED_REMAINDER * h.powi(7) * (x + y)
}
fn agm_elliptic_perimeter(accuracy: f64, radii: Vec2) -> f64 {
let Vec2 { x, y } = if radii.x >= radii.y {
radii
} else {
Vec2::new(radii.y, radii.x)
};
let accuracy = accuracy / (2. * PI * x);
let mut sum = 1.;
let mut a = 1.;
let mut g = y / x;
let mut c = (1. - g.powi(2)).sqrt();
let mut mul = 0.5;
loop {
let c2 = c.powi(2);
let term = mul * c2;
sum -= term;
if term <= accuracy * g {
sum -= term;
break;
}
mul *= 2.;
c = (a - g) / 2.;
let a_next = (a + g) / 2.;
g = (a * g).sqrt();
a = a_next;
}
2. * PI * x / a * sum
}
#[cfg(test)]
mod tests {
use crate::{Circle, Ellipse, Point, Shape};
use std::f64::consts::PI;
fn assert_approx_eq(x: f64, y: f64) {
assert!((x - y).abs() < 1e-7, "{x} != {y}");
}
#[test]
fn circular_perimeter() {
for radius in [1.0, 0., 1.5, PI, 10.0, -1.0, 1_234_567_890.1] {
let circle = Circle::new((0., 0.), radius);
let ellipse = Ellipse::new((0., 0.), (radius, radius), 0.);
let circle_p = circle.perimeter(0.);
let ellipse_p = ellipse.perimeter(0.1);
let epsilon = f64::EPSILON * 8. * circle_p.max(ellipse_p);
assert!(
(circle_p - ellipse_p).abs() <= epsilon,
"Expected circular ellipse radius {ellipse_p} to be equal to circle radius {circle_p} for radius {radius}"
);
}
}
#[test]
fn compare_perimeter_with_bez() {
const EPSILON: f64 = 0.000_002;
for radii in [
(0.5, 1.),
(2., 1.),
(0.000_000_1, 1.),
(0., 1.),
(1., 0.),
(-0.5, 1.),
(-0.000_000_1, -1.),
] {
let ellipse = Ellipse::new((0., 0.), radii, 0.);
let ellipse_p = ellipse.perimeter(0.000_001);
let bez_p = ellipse.path_segments(0.000_000_25).perimeter(0.000_000_25);
assert!(
(ellipse_p - bez_p).abs() < EPSILON,
"Numerically approximated ellipse perimeter ({ellipse_p}) does not match bezier segment perimeter length ({bez_p}) for radii {radii:?}"
);
}
}
#[test]
fn known_perimeter() {
const ACCURACY: f64 = 0.000_000_000_001;
for (radii, perimeter) in [
((0.5, 1.), 4.844_224_110_273_838),
((0.001, 1.), 4.000_015_588_104_688),
] {
let ellipse = Ellipse::new((0., 0.), radii, 0.);
let ellipse_p = ellipse.perimeter(ACCURACY);
assert!(
(ellipse_p - perimeter).abs() <= ACCURACY,
"Numerically approximated ellipse perimeter ({ellipse_p}) does not match known perimeter ({perimeter}) radii {radii:?}"
);
}
}
#[test]
fn area_sign() {
let center = Point::new(5.0, 5.0);
let e = Ellipse::new(center, (5.0, 5.0), 1.0);
assert_approx_eq(e.area(), 25.0 * PI);
let e = Ellipse::new(center, (5.0, 10.0), 1.0);
assert_approx_eq(e.area(), 50.0 * PI);
assert_eq!(e.winding(center), 1);
let p = e.to_path(1e-9);
assert_approx_eq(e.area(), p.area());
assert_eq!(e.winding(center), p.winding(center));
let e_neg_radius = Ellipse::new(center, (-5.0, 10.0), 1.0);
assert_approx_eq(e_neg_radius.area(), 50.0 * PI);
assert_eq!(e_neg_radius.winding(center), 1);
let p_neg_radius = e_neg_radius.to_path(1e-9);
assert_approx_eq(e_neg_radius.area(), p_neg_radius.area());
assert_eq!(e_neg_radius.winding(center), p_neg_radius.winding(center));
}
}