use crate::curve_math::analytic;
use crate::curves::Curve2D;
use crate::curves::parametrize::{self, ParametrizeError};
use crate::surfaces::Surface;
use crate::tol;
use crate::{Axis3, Frame2, Frame3, Point2, Point3, Vector2, Vector3};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircleConstructionError {
NegativeRadius,
NullNormal,
ConfusedPoints,
CollinearPoints,
}
impl fmt::Display for CircleConstructionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
CircleConstructionError::NegativeRadius => "radius is negative",
CircleConstructionError::NullNormal => "normal has zero length",
CircleConstructionError::ConfusedPoints => "the points are confused",
CircleConstructionError::CollinearPoints => "the points are collinear",
};
f.write_str(message)
}
}
impl std::error::Error for CircleConstructionError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Circle3D {
frame: Frame3,
radius: f64,
}
impl Circle3D {
pub fn new(
center: Point3,
normal: Vector3,
radius: f64,
) -> Result<Circle3D, CircleConstructionError> {
let frame =
Frame3::from_z(center, normal).map_err(|_| CircleConstructionError::NullNormal)?;
Circle3D::from_frame(frame, radius)
}
pub fn from_axis(axis: Axis3, radius: f64) -> Result<Circle3D, CircleConstructionError> {
let frame = Frame3::from_z(axis.origin(), axis.direction())
.map_err(|_| CircleConstructionError::NullNormal)?;
Circle3D::from_frame(frame, radius)
}
pub fn from_frame(frame: Frame3, radius: f64) -> Result<Circle3D, CircleConstructionError> {
if radius < 0.0 {
return Err(CircleConstructionError::NegativeRadius);
}
Ok(Circle3D { frame, radius })
}
pub fn from_three_points(
p1: Point3,
p2: Point3,
p3: Point3,
) -> Result<Circle3D, CircleConstructionError> {
if p1.distance(p2) < tol::CONFUSION
|| p2.distance(p3) < tol::CONFUSION
|| p1.distance(p3) < tol::CONFUSION
{
return Err(CircleConstructionError::ConfusedPoints);
}
let v12 = p2 - p1;
let v13 = p3 - p1;
let collinear_cross = v12.cross(v13).magnitude();
if collinear_cross <= tol::CONFUSION * v12.magnitude().max(v13.magnitude()) {
return Err(CircleConstructionError::CollinearPoints);
}
let a = p1 - p3;
let b = p2 - p3;
let n = a.cross(b);
let center = p3
+ ((a.square_magnitude() * b - b.square_magnitude() * a).cross(n))
* (1.0 / (2.0 * n.square_magnitude()));
let radius = center.distance(p1);
let normal = (p2 - p1).cross(p3 - p2);
let frame = Frame3::new(center, normal, p1 - center)
.map_err(|_| CircleConstructionError::CollinearPoints)?;
Circle3D::from_frame(frame, radius)
}
pub fn center(&self) -> Point3 {
self.frame.origin()
}
pub fn radius(&self) -> f64 {
self.radius
}
pub fn frame(&self) -> Frame3 {
self.frame
}
pub fn normal(&self) -> Vector3 {
self.frame.z_direction()
}
pub fn eval_point(&self, u: f64) -> Point3 {
analytic::circle_d0(&self.frame, self.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::circle_dn(&self.frame, self.radius, u, order),
}
}
pub fn parameter_of(&self, point: Point3) -> f64 {
analytic::circle_parameter(&self.frame, point)
}
pub fn parametrize_on(&self, surface: impl Into<Surface>) -> Result<Curve2D, ParametrizeError> {
parametrize::circle_on_surface(self, &surface.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Circle2D {
frame: Frame2,
radius: f64,
}
impl Circle2D {
pub fn new(center: Point2, radius: f64) -> Result<Circle2D, CircleConstructionError> {
let frame = Frame2::new(center, Vector2::X, Vector2::Y)
.expect("Vector2::X and Vector2::Y are orthonormal by construction");
Circle2D::from_frame(frame, radius)
}
pub fn from_frame(frame: Frame2, radius: f64) -> Result<Circle2D, CircleConstructionError> {
if radius < 0.0 {
return Err(CircleConstructionError::NegativeRadius);
}
Ok(Circle2D { frame, radius })
}
pub fn center(&self) -> Point2 {
self.frame.origin()
}
pub fn radius(&self) -> f64 {
self.radius
}
pub fn frame(&self) -> Frame2 {
self.frame
}
pub fn eval_point(&self, u: f64) -> Point2 {
analytic::circle2d_d0(&self.frame, self.radius, u)
}
pub fn eval_points(&self, us: &[f64]) -> Vec<Point2> {
us.iter().map(|&u| self.eval_point(u)).collect()
}
pub fn eval_derivative(&self, u: f64, order: u32) -> Vector2 {
match order {
0 => panic!("eval_derivative: order must be >= 1 (use eval_point for order 0)"),
_ => analytic::circle2d_dn(&self.frame, self.radius, u, order),
}
}
pub fn parameter_of(&self, point: Point2) -> f64 {
analytic::circle2d_parameter(&self.frame, point)
}
}
#[cfg(test)]
mod tests {
use crate::{
Circle2D, Circle3D, CircleConstructionError, Frame2, Frame3, Point2, Point3, Vector2,
Vector3,
};
#[test]
fn test_circle3d_new_ok() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
assert_eq!(c.center(), Point3::ORIGIN);
assert_eq!(c.radius(), 2.0);
assert_eq!(c.normal(), Vector3::Z);
}
#[test]
fn test_circle3d_new_negative_radius_errors() {
assert_eq!(
Circle3D::new(Point3::ORIGIN, Vector3::Z, -1.0),
Err(CircleConstructionError::NegativeRadius)
);
}
#[test]
fn test_circle3d_new_null_normal_errors() {
assert_eq!(
Circle3D::new(Point3::ORIGIN, Vector3::ZERO, 1.0),
Err(CircleConstructionError::NullNormal)
);
}
#[test]
fn test_circle3d_from_axis_ok() {
let axis = crate::Axis3::new(Point3::new(1.0, 2.0, 3.0), Vector3::Y).unwrap();
let c = Circle3D::from_axis(axis, 3.0).unwrap();
assert_eq!(c.center(), Point3::new(1.0, 2.0, 3.0));
assert_eq!(c.normal(), Vector3::Y);
assert_eq!(c.radius(), 3.0);
}
#[test]
fn test_circle3d_from_axis_negative_radius_errors() {
let axis = crate::Axis3::new(Point3::ORIGIN, Vector3::Z).unwrap();
assert_eq!(
Circle3D::from_axis(axis, -1.0),
Err(CircleConstructionError::NegativeRadius)
);
}
#[test]
fn test_circle3d_from_frame_ok() {
let frame = Frame3::WORLD;
let c = Circle3D::from_frame(frame, 5.0).unwrap();
assert_eq!(c.frame(), frame);
assert_eq!(c.radius(), 5.0);
}
#[test]
fn test_circle3d_from_frame_negative_radius_errors() {
assert_eq!(
Circle3D::from_frame(Frame3::WORLD, -0.1),
Err(CircleConstructionError::NegativeRadius)
);
}
#[test]
fn test_circle3d_from_three_points_ok() {
let p1 = Point3::new(1.0, 0.0, 0.0);
let p2 = Point3::new(0.0, 1.0, 0.0);
let p3 = Point3::new(-1.0, 0.0, 0.0);
let c = Circle3D::from_three_points(p1, p2, p3).unwrap();
assert!((c.radius() - 1.0).abs() < 1e-9);
}
#[test]
fn test_circle3d_from_three_points_two_coincident_errors() {
let p = Point3::new(1.0, 2.0, 3.0);
let p2 = Point3::new(4.0, 5.0, 6.0);
assert_eq!(
Circle3D::from_three_points(p, p, p2),
Err(CircleConstructionError::ConfusedPoints)
);
}
#[test]
fn test_circle3d_from_three_points_all_coincident_errors() {
let p = Point3::new(1.0, 2.0, 3.0);
assert_eq!(
Circle3D::from_three_points(p, p, p),
Err(CircleConstructionError::ConfusedPoints)
);
}
#[test]
fn test_circle3d_from_three_points_collinear_errors() {
let p1 = Point3::new(0.0, 0.0, 0.0);
let p2 = Point3::new(1.0, 0.0, 0.0);
let p3 = Point3::new(2.0, 0.0, 0.0);
assert_eq!(
Circle3D::from_three_points(p1, p2, p3),
Err(CircleConstructionError::CollinearPoints)
);
}
#[test]
fn test_circle3d_eval_point_zero() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
let p = c.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_circle3d_eval_points_matches_loop() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
let us = [0.0, 0.5, 1.5];
let expected: Vec<Point3> = us.iter().map(|&u| c.eval_point(u)).collect();
assert_eq!(c.eval_points(&us), expected);
}
#[test]
fn test_circle3d_eval_derivative_order1() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
let d1 = c.eval_derivative(0.0, 1);
assert!(d1.x.abs() < 1e-9);
assert!((d1.y - 2.0).abs() < 1e-9);
}
#[test]
#[should_panic]
fn test_circle3d_eval_derivative_order0_panics() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
c.eval_derivative(0.0, 0);
}
#[test]
fn test_circle3d_parameter_of_round_trip() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
for u in [0.3, 2.0, 5.5] {
let p = c.eval_point(u);
assert!((c.parameter_of(p) - u).abs() < 1e-9);
}
}
#[test]
fn test_circle3d_parameter_of_in_zero_to_tau() {
let c = Circle3D::new(Point3::ORIGIN, Vector3::Z, 2.0).unwrap();
for u in [-1.0, -0.1, 7.0] {
let p = c.eval_point(u);
let recovered = c.parameter_of(p);
assert!((0.0..std::f64::consts::TAU).contains(&recovered));
}
}
#[test]
fn test_circle_construction_error_display() {
assert_eq!(
CircleConstructionError::NegativeRadius.to_string(),
"radius is negative"
);
assert_eq!(
CircleConstructionError::NullNormal.to_string(),
"normal has zero length"
);
assert_eq!(
CircleConstructionError::ConfusedPoints.to_string(),
"the points are confused"
);
assert_eq!(
CircleConstructionError::CollinearPoints.to_string(),
"the points are collinear"
);
}
#[test]
fn test_circle_construction_error_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&CircleConstructionError::NegativeRadius);
}
#[test]
fn test_circle2d_new_ok() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
assert_eq!(c.center(), Point2::ORIGIN);
assert_eq!(c.radius(), 2.0);
assert_eq!(c.frame(), Frame2::WORLD);
}
#[test]
fn test_circle2d_new_negative_radius_errors() {
assert_eq!(
Circle2D::new(Point2::ORIGIN, -1.0),
Err(CircleConstructionError::NegativeRadius)
);
}
#[test]
fn test_circle2d_from_frame_ok() {
let frame = Frame2::WORLD;
let c = Circle2D::from_frame(frame, 3.0).unwrap();
assert_eq!(c.frame(), frame);
assert_eq!(c.radius(), 3.0);
}
#[test]
fn test_circle2d_from_frame_negative_radius_errors() {
assert_eq!(
Circle2D::from_frame(Frame2::WORLD, -1.0),
Err(CircleConstructionError::NegativeRadius)
);
}
#[test]
fn test_circle2d_eval_point_zero() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
let p = c.eval_point(0.0);
assert!((p.x - 2.0).abs() < 1e-9);
assert!(p.y.abs() < 1e-9);
}
#[test]
fn test_circle2d_eval_points_matches_loop() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
let us = [0.0, 0.5, 1.5];
let expected: Vec<Point2> = us.iter().map(|&u| c.eval_point(u)).collect();
assert_eq!(c.eval_points(&us), expected);
}
#[test]
fn test_circle2d_eval_derivative_order1() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
let d1 = c.eval_derivative(0.0, 1);
assert!(d1.x.abs() < 1e-9);
assert!((d1.y - 2.0).abs() < 1e-9);
}
#[test]
#[should_panic]
fn test_circle2d_eval_derivative_order0_panics() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
c.eval_derivative(0.0, 0);
}
#[test]
fn test_circle2d_parameter_of_round_trip() {
let c = Circle2D::new(Point2::ORIGIN, 2.0).unwrap();
for u in [0.3, 2.0, 5.5] {
let p = c.eval_point(u);
assert!((c.parameter_of(p) - u).abs() < 1e-9);
}
}
#[test]
fn test_circle2d_from_frame_arbitrary() {
let frame = Frame2::from_x(Point2::new(1.0, -2.0), Vector2::new(3.0, 4.0)).unwrap();
let c = Circle2D::from_frame(frame, 2.5).unwrap();
for u in [0.3, 2.0, 5.5] {
let p = c.eval_point(u);
assert!((c.parameter_of(p) - u).abs() < 1e-9);
}
}
}