use std::f64::consts::{FRAC_PI_2, PI, TAU};
use std::fmt;
use crate::curve_math::analytic::{in_period, wrap_to_turn};
use crate::curves::{Circle2D, Circle3D, Curve2D, Line2D, Line3D};
use crate::surfaces::{ParametricSurface, Surface};
use crate::tol;
use crate::{Frame2D, Point2D, Point3D, Vector2D, Vector3D};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParametrizeError {
NotAnalytic,
CurveNotOnSurface,
}
impl fmt::Display for ParametrizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
ParametrizeError::NotAnalytic => {
"no closed-form 2D representation exists for this curve/surface pair"
}
ParametrizeError::CurveNotOnSurface => "the curve does not lie on the surface",
};
f.write_str(message)
}
}
impl std::error::Error for ParametrizeError {}
const CHECK_PARAMS: [f64; 3] = [0.0, 1.7, 4.1];
pub(crate) fn line_on_surface(
line: &Line3D,
surface: &Surface,
) -> Result<Curve2D, ParametrizeError> {
let pcurve = match surface {
Surface::Plane(plane) => line_on_plane(line, plane.frame()),
Surface::Cylinder(cyl) => line_on_cylinder(line, cyl.frame()),
Surface::Cone(cone) => {
line_on_cone(line, cone.frame(), cone.ref_radius(), cone.semi_angle())
}
Surface::Sphere(_) | Surface::Torus(_) | Surface::BSpline(_) => {
return Err(ParametrizeError::NotAnalytic);
}
}?;
finish_line(surface, pcurve, |t| line.eval_point(t))
}
pub(crate) fn circle_on_surface(
circle: &Circle3D,
surface: &Surface,
) -> Result<Curve2D, ParametrizeError> {
match surface {
Surface::Plane(plane) => {
let circle2d = circle_on_plane(circle, plane.frame());
verify_on_surface(surface, &Curve2D::Circle(circle2d), |t| {
circle.eval_point(t)
})?;
Ok(Curve2D::Circle(circle2d))
}
Surface::Cylinder(cyl) => {
let pcurve = circle_on_cylinder(circle, cyl.frame())?;
finish_line(surface, pcurve, |t| circle.eval_point(t))
}
Surface::Cone(cone) => {
let pcurve =
circle_on_cone(circle, cone.frame(), cone.ref_radius(), cone.semi_angle())?;
finish_line(surface, pcurve, |t| circle.eval_point(t))
}
Surface::Sphere(sphere) => {
let pcurve = circle_on_sphere(circle, sphere.frame())?;
finish_line(surface, pcurve, |t| circle.eval_point(t))
}
Surface::Torus(torus) => {
let pcurve = circle_on_torus(
circle,
torus.frame(),
torus.major_radius(),
torus.minor_radius(),
)?;
finish_line(surface, pcurve, |t| circle.eval_point(t))
}
Surface::BSpline(_) => Err(ParametrizeError::NotAnalytic),
}
}
fn finish_line(
surface: &Surface,
pcurve: Line2D,
curve_point: impl Fn(f64) -> Point3D,
) -> Result<Curve2D, ParametrizeError> {
verify_on_surface(surface, &Curve2D::Line(pcurve), curve_point)?;
Ok(Curve2D::Line(normalize_line2d_at_zero(pcurve, surface)))
}
fn verify_on_surface(
surface: &Surface,
pcurve: &Curve2D,
curve_point: impl Fn(f64) -> Point3D,
) -> Result<(), ParametrizeError> {
use crate::curves::ParametricCurve2D;
let mut scale: f64 = 1.0;
for &t in &CHECK_PARAMS {
scale = scale.max((curve_point(t) - Point3D::ORIGIN).magnitude());
}
let tolerance = 1e-6 * scale;
for &t in &CHECK_PARAMS {
let uv = pcurve.eval_point(t);
let on_surface = surface.eval_point(uv.x, uv.y);
if on_surface.distance(curve_point(t)) > tolerance {
return Err(ParametrizeError::CurveNotOnSurface);
}
}
Ok(())
}
fn line_on_plane(line: &Line3D, frame: crate::Frame3D) -> Result<Line2D, ParametrizeError> {
let origin = eval_point2d_plane(frame, line.origin());
let d = line.direction();
let dir = Vector2D::new(d.dot(frame.x_direction()), d.dot(frame.y_direction()));
let dir = dir
.normalized()
.ok_or(ParametrizeError::CurveNotOnSurface)?;
Line2D::new(origin, dir).map_err(|_| ParametrizeError::CurveNotOnSurface)
}
fn circle_on_plane(circle: &Circle3D, frame: crate::Frame3D) -> Circle2D {
let center = eval_point2d_plane(frame, circle.center());
let cf = circle.frame();
let x_dir = eval_dir2d_plane(frame, cf.x_direction());
let y_dir = eval_dir2d_plane(frame, cf.y_direction());
let frame2 = Frame2D::new(center, x_dir, y_dir)
.expect("circle frame axes stay orthonormal under a plane projection");
Circle2D::from_frame(frame2, circle.radius())
.expect("circle radius is non-negative by construction")
}
fn eval_point2d_plane(frame: crate::Frame3D, p: Point3D) -> Point2D {
let d = p - frame.origin();
Point2D::new(d.dot(frame.x_direction()), d.dot(frame.y_direction()))
}
fn eval_dir2d_plane(frame: crate::Frame3D, d: Vector3D) -> Vector2D {
Vector2D::new(d.dot(frame.x_direction()), d.dot(frame.y_direction()))
}
fn line_on_cylinder(line: &Line3D, frame: crate::Frame3D) -> Result<Line2D, ParametrizeError> {
let axis = frame.z_direction();
if line.direction().cross(axis).square_magnitude() > tol::ANGULAR * tol::ANGULAR {
return Err(ParametrizeError::NotAnalytic);
}
let (x, y, z) = frame.local_coordinates(line.origin());
let u = angle_or_zero(y, x);
let v = z;
let sign = if line.direction().dot(axis) >= 0.0 {
1.0
} else {
-1.0
};
Line2D::new(Point2D::new(u, v), Vector2D::new(0.0, sign))
.map_err(|_| ParametrizeError::NotAnalytic)
}
fn circle_on_cylinder(
circle: &Circle3D,
frame: crate::Frame3D,
) -> Result<Line2D, ParametrizeError> {
let axis = frame.z_direction();
let normal = circle.normal();
if axis.cross(normal).square_magnitude() > tol::ANGULAR * tol::ANGULAR {
return Err(ParametrizeError::NotAnalytic);
}
let u = wrap_to_turn(
frame
.x_direction()
.angle_with_ref(circle.frame().x_direction(), axis),
);
let v = (circle.center() - frame.origin()).dot(axis);
let du = if normal.dot(axis) > 0.0 { 1.0 } else { -1.0 };
Line2D::new(Point2D::new(u, v), Vector2D::new(du, 0.0))
.map_err(|_| ParametrizeError::NotAnalytic)
}
fn line_on_cone(
line: &Line3D,
frame: crate::Frame3D,
ref_r: f64,
semi_angle: f64,
) -> Result<Line2D, ParametrizeError> {
let apex = cone_apex(frame, ref_r, semi_angle);
let mut foot = line.origin();
let mut delta_v = 0.0;
if foot.distance(apex) < tol::CONFUSION {
foot = foot + line.direction();
delta_v = 1.0;
}
let (u, v) = crate::surface_math::analytic::cone_parameters(&frame, ref_r, semi_angle, foot);
let sv = crate::surface_math::analytic::cone_derivative(&frame, ref_r, semi_angle, u, v, 0, 1);
if line.direction().cross(sv).square_magnitude() > tol::ANGULAR * tol::ANGULAR {
return Err(ParametrizeError::NotAnalytic);
}
let sign = if line.direction().dot(sv) >= 0.0 {
1.0
} else {
-1.0
};
Line2D::new(
Point2D::new(u, v - delta_v * sign),
Vector2D::new(0.0, sign),
)
.map_err(|_| ParametrizeError::NotAnalytic)
}
fn circle_on_cone(
circle: &Circle3D,
frame: crate::Frame3D,
ref_r: f64,
semi_angle: f64,
) -> Result<Line2D, ParametrizeError> {
let axis = frame.z_direction();
let normal = circle.normal();
if axis.cross(normal).square_magnitude() > tol::ANGULAR * tol::ANGULAR {
return Err(ParametrizeError::NotAnalytic);
}
let cf = circle.frame();
let x = frame.x_direction().dot(cf.x_direction());
let y = frame.y_direction().dot(cf.x_direction());
let z = (circle.center() - frame.origin()).dot(axis);
let u = if x.abs() <= tol::ANGULAR && y.abs() <= tol::ANGULAR {
0.0
} else if -ref_r > z * semi_angle.tan() {
wrap_to_turn((-y).atan2(-x))
} else {
wrap_to_turn(y.atan2(x))
};
let v = z / semi_angle.cos();
let du = if normal.dot(axis) > 0.0 { 1.0 } else { -1.0 };
Line2D::new(Point2D::new(u, v), Vector2D::new(du, 0.0))
.map_err(|_| ParametrizeError::NotAnalytic)
}
fn cone_apex(frame: crate::Frame3D, ref_r: f64, semi_angle: f64) -> Point3D {
let v_apex = -ref_r / semi_angle.sin();
frame.origin() + (v_apex * semi_angle.cos()) * frame.z_direction()
}
fn circle_on_sphere(circle: &Circle3D, frame: crate::Frame3D) -> Result<Line2D, ParametrizeError> {
let z = frame.z_direction();
let cf = circle.frame();
let normal = circle.normal();
let is_iso_u = normal.dot(z).abs() <= tol::CONFUSION
&& circle.center().distance(frame.origin()) <= tol::CONFUSION;
let is_iso_v = cf.x_direction().dot(z).abs() <= tol::CONFUSION
&& cf.y_direction().dot(z).abs() <= tol::CONFUSION;
if is_iso_u {
Ok(sphere_meridian(circle, frame))
} else if is_iso_v {
sphere_parallel(circle, frame)
} else {
Err(ParametrizeError::NotAnalytic)
}
}
fn sphere_meridian(circle: &Circle3D, frame: crate::Frame3D) -> Line2D {
let cf = circle.frame();
let mut p1 = eval_point2d_sphere(frame, cf.x_direction());
let mut p2 = eval_point2d_sphere(frame, cf.y_direction());
if (p1.y - FRAC_PI_2).abs() < tol::P_CONFUSION || (p1.y + FRAC_PI_2).abs() < tol::P_CONFUSION {
p1.x = p2.x;
} else if ((p1.x - p2.x).abs() - PI).abs() < tol::P_CONFUSION {
p2.x = p1.x;
p2.y = if p2.y < 0.0 { -PI - p2.y } else { PI - p2.y };
} else {
p2.x = p1.x;
}
let dir = (p2 - p1)
.normalized()
.expect("meridian endpoints are distinct in v");
Line2D::new(p1, dir).expect("meridian direction is unit length")
}
fn sphere_parallel(circle: &Circle3D, frame: crate::Frame3D) -> Result<Line2D, ParametrizeError> {
let z = frame.z_direction();
let cf = circle.frame();
let u = wrap_to_turn(frame.x_direction().angle_with_ref(cf.x_direction(), z));
let r = circle.eval_point(0.0).distance(frame.origin());
let height = (circle.center() - frame.origin()).dot(z);
let v = (height / r).clamp(-1.0, 1.0).asin();
let du = if circle.normal().dot(z) >= 0.0 {
1.0
} else {
-1.0
};
Line2D::new(Point2D::new(u, v), Vector2D::new(du, 0.0))
.map_err(|_| ParametrizeError::NotAnalytic)
}
fn eval_point2d_sphere(frame: crate::Frame3D, dir: Vector3D) -> Point2D {
let x = dir.dot(frame.x_direction());
let y = dir.dot(frame.y_direction());
let z = dir.dot(frame.z_direction());
let u = angle_or_zero(y, x);
let v = z.clamp(-1.0, 1.0).asin();
Point2D::new(u, v)
}
fn sphere_set_in_bounds(line: Line2D) -> Line2D {
let mut line = line;
let y0 = line.eval_point(0.0).y;
let new_y = in_period(y0, -PI, PI);
line = translate_line(line, 0.0, new_y - y0);
let p = line.eval_point(0.0);
let dir = line.direction();
let north = p.y - FRAC_PI_2 > tol::CONFUSION
|| ((p.y - FRAC_PI_2).abs() < tol::CONFUSION && dir.y > 0.0);
let south = p.y + FRAC_PI_2 < -tol::CONFUSION
|| ((p.y + FRAC_PI_2).abs() < tol::CONFUSION && dir.y < 0.0);
if north {
line = mirror_about_horizontal(line, FRAC_PI_2);
line = translate_line(line, PI, 0.0);
} else if south {
line = mirror_about_horizontal(line, -FRAC_PI_2);
line = translate_line(line, PI, 0.0);
} else {
return line;
}
let x0 = line.eval_point(0.0).x;
let new_x = in_period(x0, 0.0, TAU);
translate_line(line, new_x - x0, 0.0)
}
fn mirror_about_horizontal(line: Line2D, c: f64) -> Line2D {
let o = line.origin();
let d = line.direction();
Line2D::new(Point2D::new(o.x, 2.0 * c - o.y), Vector2D::new(d.x, -d.y))
.expect("mirrored direction keeps unit length")
}
fn circle_on_torus(
circle: &Circle3D,
frame: crate::Frame3D,
maj: f64,
min: f64,
) -> Result<Line2D, ParametrizeError> {
let z = frame.z_direction();
let oc = circle.center() - frame.origin();
let normal = circle.normal();
let is_toroidal = oc.magnitude() < tol::CONFUSION
|| z.cross(normal).square_magnitude() <= tol::ANGULAR * tol::ANGULAR;
if is_toroidal {
torus_toroidal(circle, frame, maj, min)
} else {
torus_poloidal(circle, frame)
}
}
fn torus_toroidal(
circle: &Circle3D,
frame: crate::Frame3D,
maj: f64,
min: f64,
) -> Result<Line2D, ParametrizeError> {
let z = frame.z_direction();
let cf = circle.frame();
let mut p1 = torus_eval_u(frame, cf.x_direction());
let mut p2 = torus_eval_u(frame, cf.y_direction());
let oc = circle.center() - frame.origin();
let axial_fraction = oc.dot(z) / min;
let mut v = axial_fraction.clamp(-1.0, 1.0).asin();
if circle.radius() < maj {
v = PI - v;
} else if v < 0.0 {
v += TAU;
}
p1.y = v;
p2.y = v;
let mut v2d = p2 - p1;
if (p1.x - p2.x).abs() > PI {
v2d = -v2d;
}
let dir = v2d.normalized().ok_or(ParametrizeError::NotAnalytic)?;
if p1.x < 0.0 {
p1.x += TAU;
}
Line2D::new(p1, dir).map_err(|_| ParametrizeError::NotAnalytic)
}
fn torus_poloidal(circle: &Circle3D, frame: crate::Frame3D) -> Result<Line2D, ParametrizeError> {
let z = frame.z_direction();
let oc = circle.center() - frame.origin();
let cf = circle.frame();
let u = wrap_to_turn(frame.x_direction().angle_with_ref(oc, z));
let mut v1 = oc.angle_with_ref(cf.x_direction(), oc.cross(z));
if v1 < 0.0 {
v1 += TAU;
}
let mut dir = Vector2D::Y;
if oc.cross(z).dot(cf.x_direction().cross(cf.y_direction())) < 0.0 {
dir = -dir;
}
Line2D::new(Point2D::new(u, v1), dir).map_err(|_| ParametrizeError::NotAnalytic)
}
fn torus_eval_u(frame: crate::Frame3D, dir: Vector3D) -> Point2D {
let x = dir.dot(frame.x_direction());
let y = dir.dot(frame.y_direction());
Point2D::new(angle_or_zero(y, x), 0.0)
}
pub(crate) fn normalize_line2d_at_zero(line: Line2D, surface: &Surface) -> Line2D {
match surface {
Surface::Plane(_) | Surface::BSpline(_) => line,
Surface::Cylinder(_) | Surface::Cone(_) => wrap_u(line),
Surface::Sphere(_) => sphere_set_in_bounds(line),
Surface::Torus(_) => wrap_v(wrap_u(line)),
}
}
fn wrap_u(line: Line2D) -> Line2D {
let u0 = line.eval_point(0.0).x;
let new_u = in_period(u0, 0.0, TAU);
translate_line(line, new_u - u0, 0.0)
}
fn wrap_v(line: Line2D) -> Line2D {
let v0 = line.eval_point(0.0).y;
let new_v = in_period(v0, 0.0, TAU);
translate_line(line, 0.0, new_v - v0)
}
fn translate_line(line: Line2D, du: f64, dv: f64) -> Line2D {
let o = line.origin();
Line2D::new(Point2D::new(o.x + du, o.y + dv), line.direction())
.expect("translation preserves the unit direction")
}
fn angle_or_zero(y: f64, x: f64) -> f64 {
if x.abs() < tol::P_CONFUSION && y.abs() < tol::P_CONFUSION {
0.0
} else {
wrap_to_turn(y.atan2(x))
}
}
#[cfg(test)]
mod tests {
use super::{
FRAC_PI_2, PI, TAU, mirror_about_horizontal, sphere_meridian, sphere_set_in_bounds,
};
use crate::curve_math::analytic::in_period;
use crate::curves::{Curve2D, ParametricCurve2D, ParametrizeError};
use crate::tol;
use crate::{
Circle3D, Cylinder, Frame3D, Line2D, Line3D, Plane, Point2D, Point3D, Sphere, Vector2D,
Vector3D,
};
#[test]
fn test_circle_on_cylinder_radius_mismatch_is_not_on_surface() {
let cylinder = Cylinder::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
let circle = Circle3D::new(Point3D::new(0.0, 0.0, 1.0), Vector3D::Z, 1.0).unwrap();
assert_eq!(
circle.parametrize_on(cylinder),
Err(ParametrizeError::CurveNotOnSurface)
);
}
#[test]
fn test_line_skew_to_cylinder_axis_is_not_analytic() {
let cylinder = Cylinder::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
let line = Line3D::new(Point3D::new(2.0, 0.0, 0.0), Vector3D::new(1.0, 0.0, 1.0)).unwrap();
assert_eq!(
line.parametrize_on(cylinder),
Err(ParametrizeError::NotAnalytic)
);
}
#[test]
#[allow(clippy::needless_borrows_for_generic_args)]
fn test_parametrize_on_accepts_surface_by_reference() {
let cylinder = Cylinder::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
let circle = Circle3D::new(Point3D::new(0.0, 0.0, 3.0), Vector3D::Z, 2.0).unwrap();
assert!(circle.parametrize_on(&cylinder).is_ok());
}
#[test]
fn test_line_on_plane_perpendicular_is_not_on_surface() {
let plane = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
let line = Line3D::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
assert_eq!(
line.parametrize_on(plane),
Err(ParametrizeError::CurveNotOnSurface)
);
}
#[test]
fn test_parametrize_error_display_and_is_std_error() {
fn takes_error(_e: &dyn std::error::Error) {}
takes_error(&ParametrizeError::NotAnalytic);
assert!(!ParametrizeError::NotAnalytic.to_string().is_empty());
assert!(!ParametrizeError::CurveNotOnSurface.to_string().is_empty());
}
#[test]
fn test_sphere_meridian_pole_mirror_north() {
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
let eps = 2e-7_f64;
let phi = 15.0_f64.to_radians();
let x_hint = Vector3D::new(phi.cos() * eps.sin(), phi.sin() * eps.sin(), eps.cos());
let frame = Frame3D::new(Point3D::ORIGIN, Vector3D::X, x_hint).unwrap();
let circle = Circle3D::from_frame(frame, 2.0).unwrap();
let sf = sphere.frame();
let raw = sphere_meridian(&circle, sf);
assert!(FRAC_PI_2 - raw.origin().y > 0.0);
assert!(FRAC_PI_2 - raw.origin().y < tol::CONFUSION);
assert_close(raw.direction().y, 1.0);
let normalized = sphere_set_in_bounds(raw);
assert_close(
normalized.origin().x,
in_period(raw.origin().x + PI, 0.0, TAU),
);
assert_close(normalized.direction().y, -raw.direction().y);
assert!((normalized.origin().y - FRAC_PI_2).abs() < tol::CONFUSION);
assert!((0.0..TAU + 1e-9).contains(&normalized.origin().x));
let pcurve = circle.parametrize_on(sphere).unwrap();
assert_eq!(pcurve, Curve2D::Line(normalized));
for t in [0.0_f64, 0.5, 1.7, 3.0, 4.5] {
let uv = pcurve.eval_point(t);
let on_surface = sphere.eval_point(uv.x, uv.y);
let expected = circle.eval_point(t);
assert!(
on_surface.distance(expected) < 1e-9,
"t={t}: sphere.eval_point(u,v)={on_surface:?} != circle.eval_point(t)={expected:?}"
);
}
}
#[test]
fn test_sphere_meridian_pole_mirror_south() {
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
let eps = 2e-7_f64;
let phi = 15.0_f64.to_radians();
let x_hint = -Vector3D::new(phi.cos() * eps.sin(), phi.sin() * eps.sin(), eps.cos());
let frame = Frame3D::new(Point3D::ORIGIN, Vector3D::X, x_hint).unwrap();
let circle = Circle3D::from_frame(frame, 2.0).unwrap();
let sf = sphere.frame();
let raw = sphere_meridian(&circle, sf);
assert!(raw.origin().y - (-FRAC_PI_2) > 0.0);
assert!(raw.origin().y - (-FRAC_PI_2) < tol::CONFUSION);
assert_close(raw.direction().y, -1.0);
let normalized = sphere_set_in_bounds(raw);
assert_close(
normalized.origin().x,
in_period(raw.origin().x + PI, 0.0, TAU),
);
assert_close(normalized.direction().y, -raw.direction().y);
assert!((normalized.origin().y - (-FRAC_PI_2)).abs() < tol::CONFUSION);
assert!((0.0..TAU + 1e-9).contains(&normalized.origin().x));
let pcurve = circle.parametrize_on(sphere).unwrap();
assert_eq!(pcurve, Curve2D::Line(normalized));
for t in [0.0_f64, 0.5, 1.7, 3.0, 4.5] {
let uv = pcurve.eval_point(t);
let on_surface = sphere.eval_point(uv.x, uv.y);
let expected = circle.eval_point(t);
assert!(
on_surface.distance(expected) < 1e-9,
"t={t}: sphere.eval_point(u,v)={on_surface:?} != circle.eval_point(t)={expected:?}"
);
}
}
#[test]
fn test_mirror_about_horizontal() {
let line = Line2D::new(Point2D::new(1.0, 1.7), Vector2D::new(0.0, 1.0)).unwrap();
let mirrored = mirror_about_horizontal(line, FRAC_PI_2);
assert_close(mirrored.origin().x, 1.0);
assert_close(mirrored.origin().y, 2.0 * FRAC_PI_2 - 1.7);
assert_close(mirrored.direction().x, 0.0);
assert_close(mirrored.direction().y, -1.0);
}
#[test]
fn test_mirror_about_horizontal_is_involution() {
let line = Line2D::new(Point2D::new(-2.3, 0.4), Vector2D::new(0.0, -1.0)).unwrap();
let once = mirror_about_horizontal(line, FRAC_PI_2);
let twice = mirror_about_horizontal(once, FRAC_PI_2);
assert_close(twice.origin().x, line.origin().x);
assert_close(twice.origin().y, line.origin().y);
assert_close(twice.direction().x, line.direction().x);
assert_close(twice.direction().y, line.direction().y);
}
fn assert_close(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
}