use crate::curve_math::analytic;
use crate::curves::{Curve2D, ParametrizeError};
use crate::surfaces::Surface;
use crate::{Frame3D, Point3D, Tolerance, Vector3D};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParabolaConstructionError {
NegativeFocal,
NullNormal,
}
impl fmt::Display for ParabolaConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
ParabolaConstructionError::NegativeFocal => "focal distance is negative",
ParabolaConstructionError::NullNormal => "normal has zero length",
};
f.write_str(message)
}
}
impl std::error::Error for ParabolaConstructionError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Parabola3D {
frame: Frame3D,
focal: f64,
}
impl Parabola3D {
pub fn new(
apex: Point3D,
normal: Vector3D,
x_direction: Vector3D,
focal: f64,
) -> Result<Parabola3D, ParabolaConstructionError> {
let frame = Frame3D::new(apex, normal, x_direction)
.map_err(|_| ParabolaConstructionError::NullNormal)?;
Parabola3D::from_frame(frame, focal)
}
pub fn from_frame(frame: Frame3D, focal: f64) -> Result<Parabola3D, ParabolaConstructionError> {
if focal < 0.0 {
return Err(ParabolaConstructionError::NegativeFocal);
}
Ok(Parabola3D { frame, focal })
}
pub fn apex(&self) -> Point3D {
self.frame.origin()
}
pub fn frame(&self) -> Frame3D {
self.frame
}
pub fn focal(&self) -> f64 {
self.focal
}
pub fn eval_point(&self, u: f64) -> Point3D {
analytic::parabola_d0(&self.frame, self.focal, u)
}
pub fn eval_points(&self, us: &[f64]) -> Vec<Point3D> {
us.iter().map(|&u| self.eval_point(u)).collect()
}
pub fn eval_derivative(&self, u: f64, order: u32) -> Vector3D {
match order {
0 => panic!("eval_derivative: order must be >= 1 (use eval_point for order 0)"),
_ => analytic::parabola_dn(&self.frame, self.focal, u, order),
}
}
pub fn parameter_of(&self, point: Point3D) -> f64 {
analytic::parabola_parameter(&self.frame, point)
}
pub fn contains(&self, point: Point3D, tol: Tolerance) -> bool {
let u = self.parameter_of(point);
self.eval_point(u).distance(point) <= tol.confusion
}
pub fn parametrize_on(&self, surface: impl Into<Surface>) -> Result<Curve2D, ParametrizeError> {
let _ = surface.into();
Err(ParametrizeError::NotAnalytic)
}
}
#[cfg(test)]
mod tests {
use crate::{Frame3D, Parabola3D, ParabolaConstructionError, Point3D, Tolerance, Vector3D};
#[test]
fn test_parabola3d_new_ok() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.7).unwrap();
assert_eq!(p.apex(), Point3D::ORIGIN);
assert_eq!(p.focal(), 1.7);
}
#[test]
fn test_parabola3d_new_negative_focal_errors() {
assert_eq!(
Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, -1.0),
Err(ParabolaConstructionError::NegativeFocal)
);
}
#[test]
fn test_parabola3d_new_zero_focal_allowed() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 0.0).unwrap();
assert_eq!(p.focal(), 0.0);
}
#[test]
fn test_parabola3d_new_null_normal_errors() {
assert_eq!(
Parabola3D::new(Point3D::ORIGIN, Vector3D::ZERO, Vector3D::X, 1.0),
Err(ParabolaConstructionError::NullNormal)
);
}
#[test]
fn test_parabola3d_new_parallel_x_direction_errors() {
assert_eq!(
Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::Z, 1.0),
Err(ParabolaConstructionError::NullNormal)
);
}
#[test]
fn test_parabola3d_from_frame_ok() {
let p = Parabola3D::from_frame(Frame3D::WORLD, 1.7).unwrap();
assert_eq!(p.frame(), Frame3D::WORLD);
assert_eq!(p.focal(), 1.7);
}
#[test]
fn test_parabola3d_from_frame_negative_focal_errors() {
assert_eq!(
Parabola3D::from_frame(Frame3D::WORLD, -0.1),
Err(ParabolaConstructionError::NegativeFocal)
);
}
#[test]
fn test_parabola3d_eval_point_zero() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.0).unwrap();
let pt = p.eval_point(0.0);
assert!(pt.x.abs() < 1e-9);
assert!(pt.y.abs() < 1e-9);
assert!(pt.z.abs() < 1e-9);
}
#[test]
fn test_parabola3d_eval_points_matches_loop() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.7).unwrap();
let us = [0.0, 0.5, 1.5];
let expected: Vec<Point3D> = us.iter().map(|&u| p.eval_point(u)).collect();
assert_eq!(p.eval_points(&us), expected);
}
#[test]
fn test_parabola3d_eval_derivative_order1() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.0).unwrap();
let d1 = p.eval_derivative(0.0, 1);
assert!(d1.x.abs() < 1e-9);
assert!((d1.y - 1.0).abs() < 1e-9);
}
#[test]
fn test_parabola3d_eval_derivative_order3_is_zero() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.0).unwrap();
let d3 = p.eval_derivative(0.5, 3);
assert_eq!(d3, Vector3D::ZERO);
}
#[test]
#[should_panic]
fn test_parabola3d_eval_derivative_order0_panics() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.0).unwrap();
p.eval_derivative(0.0, 0);
}
#[test]
fn test_parabola3d_parameter_of_round_trip() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.7).unwrap();
for u in [0.3, 2.0, -5.5] {
let pt = p.eval_point(u);
assert!((p.parameter_of(pt) - u).abs() < 1e-9);
}
}
#[test]
fn test_parabola3d_parameter_of_is_unbounded() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.7).unwrap();
let pt = p.eval_point(100.0);
assert!((p.parameter_of(pt) - 100.0).abs() < 1e-6);
}
#[test]
fn test_parabola_construction_error_display() {
assert_eq!(
ParabolaConstructionError::NegativeFocal.to_string(),
"focal distance is negative"
);
assert_eq!(
ParabolaConstructionError::NullNormal.to_string(),
"normal has zero length"
);
}
#[test]
fn test_parabola_construction_error_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&ParabolaConstructionError::NegativeFocal);
}
#[test]
fn test_parabola3d_contains() {
let p = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.0).unwrap();
let tol = Tolerance::DEFAULT;
assert!(p.contains(p.eval_point(2.0), tol));
assert!(p.contains(p.eval_point(-1.5), tol));
assert!(!p.contains(Point3D::new(0.0, 2.0, 5.0), tol));
}
}