use crate::{make_arc, make_revolution, NurbsCurve, NurbsSurface, SurfaceProjection, Vec3};
const RECOGNITION_TOLERANCE: f64 = 1e-9;
#[derive(Clone, Debug)]
pub struct RevolutionFrame {
pub origin: Vec3,
pub axis: Vec3,
pub x_axis: Vec3,
pub y_axis: Vec3,
}
impl RevolutionFrame {
fn cylindrical(&self, point: Vec3) -> (Option<f64>, f64, f64) {
let d = point.sub(self.origin);
let axial = d.dot(self.axis);
let radial_vector = d.sub(self.axis.scale(axial));
let radius = radial_vector.length();
if radius <= 1e-14 * (1.0 + axial.abs()) {
return (None, radius, axial);
}
let mut theta = radial_vector
.dot(self.y_axis)
.atan2(radial_vector.dot(self.x_axis));
if theta < 0.0 {
theta += std::f64::consts::TAU;
}
(Some(theta), radius, axial)
}
}
#[derive(Clone, Debug)]
pub enum AnalyticSurface {
Plane {
origin: Vec3,
u_dir: Vec3,
v_dir: Vec3,
u_domain: [f64; 2],
v_domain: [f64; 2],
},
RuledRevolution {
frame: RevolutionFrame,
rho0: f64,
rho1: f64,
height: f64,
},
Sphere { frame: RevolutionFrame, radius: f64 },
Torus {
frame: RevolutionFrame,
major_radius: f64,
minor_radius: f64,
},
Revolution {
frame: RevolutionFrame,
spans: usize,
sweep: f64,
generatrix: NurbsCurve,
},
}
impl AnalyticSurface {
pub fn kind_label(&self) -> &'static str {
match self {
AnalyticSurface::Plane { .. } => "Plane",
AnalyticSurface::RuledRevolution { rho0, rho1, .. } => {
let scale = rho0.abs().max(rho1.abs()).max(1.0);
if (rho0 - rho1).abs() <= RECOGNITION_TOLERANCE * scale {
"Cylinder"
} else {
"Cone"
}
}
AnalyticSurface::Sphere { .. } => "Sphere",
AnalyticSurface::Torus { .. } => "Torus",
AnalyticSurface::Revolution { .. } => "Surface of revolution",
}
}
}
pub fn circle_angle_to_parameter(spans: usize, sweep: f64, angle: f64) -> f64 {
let segment = sweep / spans as f64;
let clamped = angle.clamp(0.0, sweep);
let mut span = (clamped / segment).floor() as usize;
if span >= spans {
span = spans - 1;
}
let local = clamped - span as f64 * segment;
let half = (0.5 * local).tan();
let s = (0.5 * segment).sin();
let c = (0.5 * segment).cos();
let t = half / (s + half * (1.0 - c));
(span as f64 + t.clamp(0.0, 1.0)) / spans as f64
}
#[path = "analytic_surface/recognition.rs"]
mod recognition;
#[path = "analytic_surface/intersect.rs"]
mod intersect;
#[path = "analytic_surface/revolution.rs"]
mod revolution;
pub use intersect::intersect_analytic_pair;
pub use recognition::recognize;
pub use revolution::{revolution_structure, RevolutionStructure};
pub(crate) use revolution::circumcenter;
use recognition::generatrix_is_meridional_half_ray;
impl AnalyticSurface {
pub fn project(&self, surface: &NurbsSurface, point: Vec3) -> Option<SurfaceProjection> {
let (u, v) = match self {
AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
u_domain,
v_domain,
} => {
let d = point.sub(*origin);
let a = u_dir.dot(*u_dir);
let b = u_dir.dot(*v_dir);
let c = v_dir.dot(*v_dir);
let determinant = a * c - b * b;
if determinant.abs() <= 1e-16 * (a * c).max(1.0) {
return None;
}
let fu = u_dir.dot(d);
let fv = v_dir.dot(d);
let u =
((fu * c - fv * b) / determinant + u_domain[0]).clamp(u_domain[0], u_domain[1]);
let v =
((fv * a - fu * b) / determinant + v_domain[0]).clamp(v_domain[0], v_domain[1]);
(u, v)
}
AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
} => {
let (theta, rho, z) = frame.cylindrical(point);
let u = theta
.map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
.unwrap_or(0.0);
let delta_rho = rho1 - rho0;
let length_squared = delta_rho * delta_rho + height * height;
let t = (((rho - rho0) * delta_rho + z * height) / length_squared).clamp(0.0, 1.0);
(u, t)
}
AnalyticSurface::Sphere { frame, radius: _ } => {
let (theta, rho, z) = frame.cylindrical(point);
let u = theta
.map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
.unwrap_or(0.0);
let beta = rho.atan2(-z);
let v = circle_angle_to_parameter(2, std::f64::consts::PI, beta);
(u, v)
}
AnalyticSurface::Torus {
frame,
major_radius,
minor_radius: _,
} => {
let (theta, rho, z) = frame.cylindrical(point);
let u = theta
.map(|angle| circle_angle_to_parameter(4, std::f64::consts::TAU, angle))
.unwrap_or(0.0);
let mut psi = z.atan2(rho - major_radius);
if psi < 0.0 {
psi += std::f64::consts::TAU;
}
let v = circle_angle_to_parameter(4, std::f64::consts::TAU, psi);
(u, v)
}
AnalyticSurface::Revolution {
frame,
spans,
sweep,
generatrix,
} => {
if !generatrix_is_meridional_half_ray(generatrix, frame) {
return None;
}
let (theta, rho, z) = frame.cylindrical(point);
let full = *sweep >= std::f64::consts::TAU - 1e-9;
let point_at_angle = |angle: f64| {
let radial = frame
.x_axis
.scale(angle.cos())
.add(frame.y_axis.scale(angle.sin()));
frame.origin.add(radial.scale(rho)).add(frame.axis.scale(z))
};
let mut candidates: Vec<(f64, Vec3)> = Vec::with_capacity(2);
match theta {
None => candidates.push((0.0, point)),
Some(angle) if full || angle <= *sweep => {
candidates.push((
circle_angle_to_parameter(*spans, *sweep, angle),
point_at_angle(0.0),
));
}
Some(angle) => {
candidates.push((0.0, point));
candidates.push((1.0, point_at_angle(angle - *sweep)));
}
}
let mut best: Option<(f64, f64, f64)> = None;
for (u, query) in candidates {
let Ok(projection) = crate::project_point_to_curve(generatrix, query) else {
return None;
};
if best
.map(|(_, _, distance)| projection.distance < distance)
.unwrap_or(true)
{
best = Some((u, projection.u, projection.distance));
}
}
let (u, v, _) = best?;
(u, v)
}
};
let projected = surface.evaluate(u, v).ok()?;
Some(SurfaceProjection {
u,
v,
point: projected,
distance: projected.sub(point).length(),
})
}
pub fn frame(&self) -> Option<&RevolutionFrame> {
match self {
AnalyticSurface::Plane { .. } => None,
AnalyticSurface::RuledRevolution { frame, .. }
| AnalyticSurface::Sphere { frame, .. }
| AnalyticSurface::Torus { frame, .. }
| AnalyticSurface::Revolution { frame, .. } => Some(frame),
}
}
}