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 HyperbolaConstructionError {
NegativeRadius,
NullNormal,
ConfusedPoints,
CollinearPoints,
}
impl fmt::Display for HyperbolaConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
HyperbolaConstructionError::NegativeRadius => "radius is negative",
HyperbolaConstructionError::NullNormal => "normal has zero length",
HyperbolaConstructionError::ConfusedPoints => "the points are confused",
HyperbolaConstructionError::CollinearPoints => "the points are collinear",
};
f.write_str(message)
}
}
impl std::error::Error for HyperbolaConstructionError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hyperbola3D {
frame: Frame3,
major_radius: f64,
minor_radius: f64,
}
impl Hyperbola3D {
pub fn new(
center: Point3,
normal: Vector3,
x_direction: Vector3,
major_radius: f64,
minor_radius: f64,
) -> Result<Hyperbola3D, HyperbolaConstructionError> {
let frame = Frame3::new(center, normal, x_direction)
.map_err(|_| HyperbolaConstructionError::NullNormal)?;
Hyperbola3D::from_frame(frame, major_radius, minor_radius)
}
pub fn from_frame(
frame: Frame3,
major_radius: f64,
minor_radius: f64,
) -> Result<Hyperbola3D, HyperbolaConstructionError> {
if major_radius < 0.0 || minor_radius < 0.0 {
return Err(HyperbolaConstructionError::NegativeRadius);
}
Ok(Hyperbola3D {
frame,
major_radius,
minor_radius,
})
}
pub fn from_center_and_points(
center: Point3,
s1: Point3,
s2: Point3,
) -> Result<Hyperbola3D, HyperbolaConstructionError> {
if center.distance(s1) < tol::CONFUSION
|| center.distance(s2) < tol::CONFUSION
|| s1.distance(s2) < tol::CONFUSION
{
return Err(HyperbolaConstructionError::ConfusedPoints);
}
let v1 = s1 - center;
let d1 = v1.magnitude();
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 d2 < tol::CONFUSION {
return Err(HyperbolaConstructionError::CollinearPoints);
}
let normal = x_axis.cross(v2);
if normal.magnitude() < tol::CONFUSION {
return Err(HyperbolaConstructionError::CollinearPoints);
}
let frame = Frame3::new(center, normal, x_axis)
.map_err(|_| HyperbolaConstructionError::CollinearPoints)?;
Hyperbola3D::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::hyperbola_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::hyperbola_dn(&self.frame, self.major_radius, self.minor_radius, u, order)
}
}
}
pub fn parameter_of(&self, point: Point3) -> f64 {
analytic::hyperbola_parameter(&self.frame, 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::{Frame3, Hyperbola3D, HyperbolaConstructionError, Point3, Vector3};
#[test]
fn test_hyperbola3d_new_ok() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
assert_eq!(h.center(), Point3::ORIGIN);
assert_eq!(h.major_radius(), 2.0);
assert_eq!(h.minor_radius(), 1.0);
}
#[test]
fn test_hyperbola3d_new_negative_major_radius_errors() {
assert_eq!(
Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, -2.0, 1.0),
Err(HyperbolaConstructionError::NegativeRadius)
);
}
#[test]
fn test_hyperbola3d_new_negative_minor_radius_errors() {
assert_eq!(
Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, -1.0),
Err(HyperbolaConstructionError::NegativeRadius)
);
}
#[test]
fn test_hyperbola3d_new_minor_greater_than_major_allowed() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 1.0, 2.0).unwrap();
assert_eq!(h.major_radius(), 1.0);
assert_eq!(h.minor_radius(), 2.0);
}
#[test]
fn test_hyperbola3d_new_null_normal_errors() {
assert_eq!(
Hyperbola3D::new(Point3::ORIGIN, Vector3::ZERO, Vector3::X, 2.0, 1.0),
Err(HyperbolaConstructionError::NullNormal)
);
}
#[test]
fn test_hyperbola3d_new_parallel_x_direction_errors() {
assert_eq!(
Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::Z, 2.0, 1.0),
Err(HyperbolaConstructionError::NullNormal)
);
}
#[test]
fn test_hyperbola3d_from_frame_ok() {
let h = Hyperbola3D::from_frame(Frame3::WORLD, 2.0, 1.0).unwrap();
assert_eq!(h.frame(), Frame3::WORLD);
assert_eq!(h.major_radius(), 2.0);
assert_eq!(h.minor_radius(), 1.0);
}
#[test]
fn test_hyperbola3d_from_frame_negative_major_radius_errors() {
assert_eq!(
Hyperbola3D::from_frame(Frame3::WORLD, -2.0, 1.0),
Err(HyperbolaConstructionError::NegativeRadius)
);
}
#[test]
fn test_hyperbola3d_from_frame_negative_minor_radius_errors() {
assert_eq!(
Hyperbola3D::from_frame(Frame3::WORLD, 2.0, -1.0),
Err(HyperbolaConstructionError::NegativeRadius)
);
}
#[test]
fn test_hyperbola3d_from_center_and_points_ok() {
let h = Hyperbola3D::from_center_and_points(
Point3::ORIGIN,
Point3::new(2.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
)
.unwrap();
assert_eq!(h.major_radius(), 2.0);
assert_eq!(h.minor_radius(), 1.0);
}
#[test]
fn test_hyperbola3d_from_center_and_points_confused_points_errors() {
assert_eq!(
Hyperbola3D::from_center_and_points(
Point3::ORIGIN,
Point3::ORIGIN,
Point3::new(0.0, 1.0, 0.0),
),
Err(HyperbolaConstructionError::ConfusedPoints)
);
}
#[test]
fn test_hyperbola3d_from_center_and_points_collinear_points_errors() {
assert_eq!(
Hyperbola3D::from_center_and_points(
Point3::ORIGIN,
Point3::new(2.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
),
Err(HyperbolaConstructionError::CollinearPoints)
);
}
#[test]
fn test_hyperbola3d_eval_point_zero() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
let p = h.eval_point(0.0);
assert!((p.x - 2.0).abs() < 1e-9);
assert!(p.y.abs() < 1e-9);
assert!(p.z.abs() < 1e-9);
}
#[test]
fn test_hyperbola3d_eval_points_matches_loop() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
let us = [0.0, 0.5, 1.5];
let expected: Vec<Point3> = us.iter().map(|&u| h.eval_point(u)).collect();
assert_eq!(h.eval_points(&us), expected);
}
#[test]
fn test_hyperbola3d_eval_derivative_order1() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
let d1 = h.eval_derivative(0.0, 1);
assert!(d1.x.abs() < 1e-9);
assert!((d1.y - 1.0).abs() < 1e-9);
}
#[test]
#[should_panic]
fn test_hyperbola3d_eval_derivative_order0_panics() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
h.eval_derivative(0.0, 0);
}
#[test]
fn test_hyperbola3d_parameter_of_round_trip() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
for u in [0.3, 2.0, -1.5] {
let p = h.eval_point(u);
assert!((h.parameter_of(p) - u).abs() < 1e-9);
}
}
#[test]
fn test_hyperbola3d_parameter_of_is_unbounded() {
let h = Hyperbola3D::new(Point3::ORIGIN, Vector3::Z, Vector3::X, 2.0, 1.0).unwrap();
let p = h.eval_point(5.0);
assert!((h.parameter_of(p) - 5.0).abs() < 1e-6);
}
#[test]
fn test_hyperbola_construction_error_display() {
assert_eq!(
HyperbolaConstructionError::NegativeRadius.to_string(),
"radius is negative"
);
assert_eq!(
HyperbolaConstructionError::NullNormal.to_string(),
"normal has zero length"
);
assert_eq!(
HyperbolaConstructionError::ConfusedPoints.to_string(),
"the points are confused"
);
assert_eq!(
HyperbolaConstructionError::CollinearPoints.to_string(),
"the points are collinear"
);
}
#[test]
fn test_hyperbola_construction_error_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&HyperbolaConstructionError::NegativeRadius);
}
}