use crate::surface_math::analytic;
use crate::{Frame3, Point3, Vector3};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TorusConstructionError {
NegativeRadius,
NullNormal,
}
impl fmt::Display for TorusConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
TorusConstructionError::NegativeRadius => "radius is negative",
TorusConstructionError::NullNormal => "normal has zero length",
};
f.write_str(message)
}
}
impl std::error::Error for TorusConstructionError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Torus {
frame: Frame3,
major_radius: f64,
minor_radius: f64,
}
impl Torus {
pub fn new(
center: Point3,
normal: Vector3,
major_radius: f64,
minor_radius: f64,
) -> Result<Torus, TorusConstructionError> {
let frame =
Frame3::from_z(center, normal).map_err(|_| TorusConstructionError::NullNormal)?;
Torus::from_frame(frame, major_radius, minor_radius)
}
pub fn from_frame(
frame: Frame3,
major_radius: f64,
minor_radius: f64,
) -> Result<Torus, TorusConstructionError> {
if major_radius < 0.0 || minor_radius < 0.0 {
return Err(TorusConstructionError::NegativeRadius);
}
Ok(Torus {
frame,
major_radius,
minor_radius,
})
}
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, v: f64) -> Point3 {
analytic::torus_d0(&self.frame, self.major_radius, self.minor_radius, u, v)
}
pub fn eval_points(&self, uvs: &[(f64, f64)]) -> Vec<Point3> {
uvs.iter().map(|&(u, v)| self.eval_point(u, v)).collect()
}
pub fn eval_derivative(&self, u: f64, v: f64, du: u32, dv: u32) -> Vector3 {
match du + dv {
0 => panic!(
"eval_derivative: du + dv must be >= 1 (use eval_point for the (0, 0) order)"
),
1..=2 => analytic::torus_derivative(
&self.frame,
self.major_radius,
self.minor_radius,
u,
v,
du,
dv,
),
_ => panic!(
"eval_derivative: order du={du}, dv={dv} is not supported (du + dv must be <= 2)"
),
}
}
pub fn parameters_of(&self, point: Point3) -> (f64, f64) {
analytic::torus_parameters(&self.frame, self.major_radius, self.minor_radius, point)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_ok() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
assert_eq!(t.major_radius(), 5.0);
assert_eq!(t.minor_radius(), 1.5);
assert_eq!(t.center(), Point3::ORIGIN);
}
#[test]
fn test_new_null_normal_errors() {
assert_eq!(
Torus::new(Point3::ORIGIN, Vector3::ZERO, 5.0, 1.5),
Err(TorusConstructionError::NullNormal)
);
}
#[test]
fn test_new_negative_major_radius_errors() {
assert_eq!(
Torus::new(Point3::ORIGIN, Vector3::Z, -5.0, 1.5),
Err(TorusConstructionError::NegativeRadius)
);
}
#[test]
fn test_new_negative_minor_radius_errors() {
assert_eq!(
Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, -1.5),
Err(TorusConstructionError::NegativeRadius)
);
}
#[test]
fn test_from_frame_ok() {
let t = Torus::from_frame(Frame3::WORLD, 5.0, 1.5).unwrap();
assert_eq!(t.frame(), Frame3::WORLD);
}
#[test]
fn test_from_frame_negative_radius_errors() {
assert_eq!(
Torus::from_frame(Frame3::WORLD, -5.0, 1.5),
Err(TorusConstructionError::NegativeRadius)
);
assert_eq!(
Torus::from_frame(Frame3::WORLD, 5.0, -1.5),
Err(TorusConstructionError::NegativeRadius)
);
}
#[test]
fn test_eval_point() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
assert_eq!(t.eval_point(0.0, 0.0), Point3::new(6.5, 0.0, 0.0));
}
#[test]
fn test_eval_points_matches_loop() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
let uvs = [(0.0, 0.0), (0.5, 1.0)];
let expected: Vec<Point3> = uvs.iter().map(|&(u, v)| t.eval_point(u, v)).collect();
assert_eq!(t.eval_points(&uvs), expected);
}
#[test]
#[should_panic(expected = "du + dv must be >= 1")]
fn test_eval_derivative_zero_order_panics() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
t.eval_derivative(0.0, 0.0, 0, 0);
}
#[test]
#[should_panic(expected = "du + dv must be <= 2")]
fn test_eval_derivative_order_too_high_panics() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
t.eval_derivative(0.0, 0.0, 2, 1);
}
#[test]
fn test_parameters_of_round_trip() {
let t = Torus::new(Point3::ORIGIN, Vector3::Z, 5.0, 1.5).unwrap();
let (u, v) = t.parameters_of(Point3::new(6.5, 0.0, 0.0));
assert_eq!((u, v), (0.0, 0.0));
}
#[test]
fn test_error_display() {
assert_eq!(
TorusConstructionError::NegativeRadius.to_string(),
"radius is negative"
);
assert_eq!(
TorusConstructionError::NullNormal.to_string(),
"normal has zero length"
);
}
#[test]
fn test_error_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&TorusConstructionError::NegativeRadius);
}
}