use crate::curve_math::analytic;
use crate::curves::{Curve2D, ParametrizeError};
use crate::surfaces::Surface;
use crate::tol;
use crate::{Frame3, Point3, Vector3};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EllipseConstructionError {
NegativeRadius,
InvertedRadii,
NullNormal,
NullAxis,
InvertedAxis,
}
impl fmt::Display for EllipseConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
EllipseConstructionError::NegativeRadius => "minor radius is negative",
EllipseConstructionError::InvertedRadii => "major radius is smaller than minor radius",
EllipseConstructionError::NullNormal => "normal has zero length",
EllipseConstructionError::NullAxis => "major-axis point coincides with the center",
EllipseConstructionError::InvertedAxis => {
"the two points do not determine a valid axis pair"
}
};
f.write_str(message)
}
}
impl std::error::Error for EllipseConstructionError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ellipse3D {
frame: Frame3,
major_radius: f64,
minor_radius: f64,
}
impl Ellipse3D {
pub fn new(
center: Point3,
normal: Vector3,
x_direction: Vector3,
major_radius: f64,
minor_radius: f64,
) -> Result<Ellipse3D, EllipseConstructionError> {
let frame = Frame3::new(center, normal, x_direction)
.map_err(|_| EllipseConstructionError::NullNormal)?;
Ellipse3D::from_frame(frame, major_radius, minor_radius)
}
pub fn from_frame(
frame: Frame3,
major_radius: f64,
minor_radius: f64,
) -> Result<Ellipse3D, EllipseConstructionError> {
if minor_radius < 0.0 {
return Err(EllipseConstructionError::NegativeRadius);
}
if major_radius < minor_radius {
return Err(EllipseConstructionError::InvertedRadii);
}
Ok(Ellipse3D {
frame,
major_radius,
minor_radius,
})
}
pub fn from_center_and_points(
center: Point3,
s1: Point3,
s2: Point3,
) -> Result<Ellipse3D, EllipseConstructionError> {
let v1 = s1 - center;
let d1 = v1.magnitude();
if d1 < tol::CONFUSION {
return Err(EllipseConstructionError::NullAxis);
}
let x_axis = v1 * (1.0 / d1);
let v2 = s2 - center;
let proj = v2.dot(x_axis);
let perp = v2 - proj * x_axis;
let d2 = perp.magnitude();
if d1 < d2 || d2 < 1e-7 {
return Err(EllipseConstructionError::InvertedAxis);
}
let normal = x_axis.cross(v2);
if normal.magnitude() < 1e-7 {
return Err(EllipseConstructionError::InvertedAxis);
}
let frame = Frame3::new(center, normal, x_axis)
.map_err(|_| EllipseConstructionError::InvertedAxis)?;
Ellipse3D::from_frame(frame, d1, d2)
}
pub fn center(&self) -> Point3 {
self.frame.origin()
}
pub fn frame(&self) -> Frame3 {
self.frame
}
pub fn major_radius(&self) -> f64 {
self.major_radius
}
pub fn minor_radius(&self) -> f64 {
self.minor_radius
}
pub fn eval_point(&self, u: f64) -> Point3 {
analytic::ellipse_d0(&self.frame, self.major_radius, self.minor_radius, u)
}
pub fn eval_points(&self, us: &[f64]) -> Vec<Point3> {
us.iter().map(|&u| self.eval_point(u)).collect()
}
pub fn eval_derivative(&self, u: f64, order: u32) -> Vector3 {
match order {
0 => panic!("eval_derivative: order must be >= 1 (use eval_point for order 0)"),
_ => analytic::ellipse_dn(&self.frame, self.major_radius, self.minor_radius, u, order),
}
}
pub fn parameter_of(&self, point: Point3) -> f64 {
analytic::ellipse_parameter(&self.frame, self.major_radius, self.minor_radius, point)
}
pub fn parametrize_on(&self, surface: impl Into<Surface>) -> Result<Curve2D, ParametrizeError> {
let _ = surface.into();
Err(ParametrizeError::NotAnalytic)
}
}
#[cfg(test)]
mod tests {
use crate::{Ellipse3D, EllipseConstructionError, Frame3, Point3, Vector3};
#[test]
fn test_ellipse3d_new_ok() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
assert_eq!(e.center(), Point3::ORIGIN);
assert_eq!(e.major_radius(), 3.0);
assert_eq!(e.minor_radius(), 1.5);
}
#[test]
fn test_ellipse3d_new_negative_minor_radius_errors() {
assert_eq!(
Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, -1.0),
Err(EllipseConstructionError::NegativeRadius)
);
}
#[test]
fn test_ellipse3d_new_inverted_radii_errors() {
assert_eq!(
Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 1.0, 2.0),
Err(EllipseConstructionError::InvertedRadii)
);
}
#[test]
fn test_ellipse3d_new_null_normal_errors() {
assert_eq!(
Ellipse3D::new(Point3::ORIGIN, Vector3::ZERO, Vector3::X, 3.0, 1.5),
Err(EllipseConstructionError::NullNormal)
);
}
#[test]
fn test_ellipse3d_new_parallel_x_direction_errors() {
assert_eq!(
Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::Z, 3.0, 1.5),
Err(EllipseConstructionError::NullNormal)
);
}
#[test]
fn test_ellipse3d_from_frame_ok() {
let e = Ellipse3D::from_frame(Frame3::WORLD, 3.0, 1.5).unwrap();
assert_eq!(e.frame(), Frame3::WORLD);
assert_eq!(e.major_radius(), 3.0);
assert_eq!(e.minor_radius(), 1.5);
}
#[test]
fn test_ellipse3d_from_frame_negative_radius_errors() {
assert_eq!(
Ellipse3D::from_frame(Frame3::WORLD, 3.0, -1.0),
Err(EllipseConstructionError::NegativeRadius)
);
}
#[test]
fn test_ellipse3d_from_frame_inverted_radii_errors() {
assert_eq!(
Ellipse3D::from_frame(Frame3::WORLD, 1.0, 2.0),
Err(EllipseConstructionError::InvertedRadii)
);
}
#[test]
fn test_ellipse3d_from_center_and_points_ok() {
let e = Ellipse3D::from_center_and_points(
Point3::ORIGIN,
Point3::new(3.0, 0.0, 0.0),
Point3::new(0.0, 1.5, 0.0),
)
.unwrap();
assert_eq!(e.major_radius(), 3.0);
assert_eq!(e.minor_radius(), 1.5);
}
#[test]
fn test_ellipse3d_from_center_and_points_null_axis_errors() {
assert_eq!(
Ellipse3D::from_center_and_points(
Point3::ORIGIN,
Point3::ORIGIN,
Point3::new(0.0, 1.5, 0.0),
),
Err(EllipseConstructionError::NullAxis)
);
}
#[test]
fn test_ellipse3d_from_center_and_points_inverted_axis_errors_when_minor_larger() {
assert_eq!(
Ellipse3D::from_center_and_points(
Point3::ORIGIN,
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 3.0, 0.0),
),
Err(EllipseConstructionError::InvertedAxis)
);
}
#[test]
fn test_ellipse3d_from_center_and_points_inverted_axis_errors_when_collinear() {
assert_eq!(
Ellipse3D::from_center_and_points(
Point3::ORIGIN,
Point3::new(3.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
),
Err(EllipseConstructionError::InvertedAxis)
);
}
#[test]
fn test_ellipse3d_eval_point_zero() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
let p = e.eval_point(0.0);
assert!((p.x - 3.0).abs() < 1e-9);
assert!(p.y.abs() < 1e-9);
assert!(p.z.abs() < 1e-9);
}
#[test]
fn test_ellipse3d_eval_points_matches_loop() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
let us = [0.0, 0.5, 1.5];
let expected: Vec<Point3> = us.iter().map(|&u| e.eval_point(u)).collect();
assert_eq!(e.eval_points(&us), expected);
}
#[test]
fn test_ellipse3d_eval_derivative_order1() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
let d1 = e.eval_derivative(0.0, 1);
assert!(d1.x.abs() < 1e-9);
assert!((d1.y - 1.5).abs() < 1e-9);
}
#[test]
#[should_panic]
fn test_ellipse3d_eval_derivative_order0_panics() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
e.eval_derivative(0.0, 0);
}
#[test]
fn test_ellipse3d_parameter_of_round_trip() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
for u in [0.3, 2.0, 5.5] {
let p = e.eval_point(u);
assert!((e.parameter_of(p) - u).abs() < 1e-9);
}
}
#[test]
fn test_ellipse3d_parameter_of_in_zero_to_tau() {
let e = Ellipse3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 3.0, 1.5).unwrap();
for u in [-1.0, -0.1, 7.0] {
let p = e.eval_point(u);
let recovered = e.parameter_of(p);
assert!((0.0..std::f64::consts::TAU).contains(&recovered));
}
}
#[test]
fn test_ellipse_construction_error_display() {
assert_eq!(
EllipseConstructionError::NegativeRadius.to_string(),
"minor radius is negative"
);
assert_eq!(
EllipseConstructionError::InvertedRadii.to_string(),
"major radius is smaller than minor radius"
);
assert_eq!(
EllipseConstructionError::NullNormal.to_string(),
"normal has zero length"
);
assert_eq!(
EllipseConstructionError::NullAxis.to_string(),
"major-axis point coincides with the center"
);
assert_eq!(
EllipseConstructionError::InvertedAxis.to_string(),
"the two points do not determine a valid axis pair"
);
}
#[test]
fn test_ellipse_construction_error_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&EllipseConstructionError::NegativeRadius);
}
}