use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point3, Scalar, Tolerance, Vec3};
use axiolid_mesh::TriMesh;
use crate::profile::Rings;
pub(crate) fn rotate(p: Point3, origin: Point3, dir: Vec3, angle: Scalar) -> Point3 {
let v = p - origin;
let (s, c) = angle.sin_cos();
origin + v * c + dir.cross(v) * s + dir * (dir.dot(v) * (1.0 - c))
}
pub(crate) fn steps(max_radius: Scalar, angle: Scalar, tol: Scalar) -> usize {
if !(max_radius.is_finite() && max_radius > 0.0 && tol.is_finite() && tol > 0.0) {
return 8;
}
let ratio = (1.0 - (tol / max_radius).min(1.0)).clamp(-1.0, 1.0);
let per = 2.0 * ratio.acos().max(1e-9);
((angle.abs() / per).ceil() as usize).clamp(2, 4096)
}
pub fn revolve(
rings: &Rings,
axis_origin: Point3,
axis_direction: Vec3,
angle: Scalar,
tolerance: Tolerance,
) -> GeomResult<TriMesh> {
if !angle.is_finite() || angle == 0.0 {
return Err(GeomError::InvalidInput(format!(
"revolution angle must be finite and non-zero, got {angle}"
)));
}
let len = axis_direction.length();
if !len.is_finite() || len <= 0.0 {
return Err(GeomError::InvalidInput(
"revolution axis must be a finite non-zero direction".to_owned(),
));
}
let dir = axis_direction / len;
let mut max_r: Scalar = 0.0;
for p in rings.outer.iter().chain(rings.holes.iter().flatten()) {
let v = Point3::new(p.x, p.y, 0.0) - axis_origin;
max_r = max_r.max((v - dir * dir.dot(v)).length());
}
let full = (angle.abs() - core::f64::consts::TAU).abs() <= 1e-9;
let n = steps(max_r, angle, tolerance.linear());
let count = if full { n } else { n + 1 };
let stations: Vec<crate::loft::Station> = (0..count)
.map(|s| {
let t = angle * (s as Scalar) / (n as Scalar);
crate::loft::place(rings, |p| {
rotate(Point3::new(p.x, p.y, 0.0), axis_origin, dir, t)
})
})
.collect();
let stations: Vec<_> = stations.into_iter().rev().collect();
crate::loft::loft(rings, &stations, full)
}