use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point3, Scalar, Tolerance, Vec3};
use axiolid_mesh::TriMesh;
use crate::profile::Rings;
pub(crate) const MAX_STEPS: usize = 4096;
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) -> GeomResult<usize> {
if !(tol.is_finite() && tol > 0.0) {
return Err(GeomError::InvalidInput(format!(
"chord budget must be positive and finite, got {tol}"
)));
}
if !(max_radius.is_finite() && max_radius > 0.0) {
return Ok(8);
}
let ratio = (1.0 - (tol / max_radius).min(1.0)).clamp(-1.0, 1.0);
let per = 2.0 * ratio.acos();
let n = (angle.abs() / per).ceil();
if n.is_nan() || n > MAX_STEPS as Scalar {
return Err(too_many_steps());
}
Ok((n as usize).max(2))
}
pub(crate) fn too_many_steps() -> GeomError {
GeomError::BudgetExceeded {
resource: "revolution angular steps",
}
}
pub(crate) fn grow(n: usize) -> GeomResult<usize> {
let next = n + n / 4 + 1;
if next > MAX_STEPS {
return Err(too_many_steps());
}
Ok(next)
}
fn bounded_steps(
rings: &Rings,
axis_origin: Point3,
dir: Vec3,
angle: Scalar,
max_r: Scalar,
budget: Scalar,
) -> GeomResult<usize> {
let at = |t: Scalar| {
crate::loft::place(rings, |p| {
rotate(Point3::new(p.x, p.y, 0.0), axis_origin, dir, t)
})
};
let mut n = steps(max_r, angle, budget)?;
loop {
let h = angle / n as Scalar;
let sagitta = max_r * (1.0 - (0.5 * h).cos());
let twist = crate::loft::span_deviation(&at(h), &at(0.0));
if sagitta + twist <= budget {
return Ok(n);
}
n = grow(n)?;
}
}
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 = bounded_steps(rings, axis_origin, dir, angle, max_r, tolerance.linear())?;
let count = if full { n } else { n + 1 };
let stations: Vec<crate::loft::Station> = (0..count)
.rev()
.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 mut mesh = crate::loft::loft(rings, &stations, full)?;
if matches!(crate::extrude::outward_orientation(&mesh), Some(false)) {
for triangle in mesh.indices.chunks_exact_mut(3) {
triangle.swap(1, 2);
}
}
Ok(mesh)
}