use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
use axiolid_guarantees::Sign;
use axiolid_mesh::TriMesh;
use axiolid_predicates::arithmetic::{expansion_sign, expansion_sum};
use axiolid_predicates::two_product;
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)?;
}
}
fn side_of_axis(a: Point2, d: Vec3, p: Point2) -> Sign {
let term = |x: Scalar, y: Scalar, negate: bool| {
let (product, error) = two_product(x, y);
if negate {
[-error, -product]
} else {
[error, product]
}
};
let mut sum = expansion_sum(&term(d.x, p.y, false), &term(d.x, a.y, true));
sum = expansion_sum(&sum, &term(d.y, p.x, true));
sum = expansion_sum(&sum, &term(d.y, a.x, false));
expansion_sign(&sum)
}
pub(crate) fn check_axis_side(
rings: &Rings,
axis_origin: Point3,
axis_direction: Vec3,
tolerance: Tolerance,
full: bool,
) -> GeomResult<Vec<bool>> {
let count = rings.outer.len() + rings.holes.iter().map(Vec::len).sum::<usize>();
if axis_direction.z.abs() > tolerance.linear() * axis_direction.length() {
return Ok(vec![false; count]);
}
let offset = axis_origin.z.abs() > tolerance.linear();
let exact = axis_origin.z == 0.0 && axis_direction.z == 0.0;
let a = Point2::new(axis_origin.x, axis_origin.y);
let (mut positive, mut negative, mut noisy_touch) = (false, false, false);
let mut on_axis = Vec::with_capacity(count);
for p in rings.outer.iter().chain(rings.holes.iter().flatten()) {
if !(p.x.is_finite() && p.y.is_finite()) {
return Err(GeomError::InvalidInput(
"revolved profile must have finite vertices".to_owned(),
));
}
let side = side_of_axis(a, axis_direction, *p);
match side {
Sign::Positive => positive = true,
Sign::Negative => negative = true,
_ => {}
}
noisy_touch |= side == Sign::Zero && !offset && !exact;
on_axis.push(side == Sign::Zero && !offset);
}
if positive && negative {
return Err(GeomError::InvalidInput(
"revolved profile crosses the axis: its rings lie on both sides".to_owned(),
));
}
if noisy_touch {
return Err(touching_unsupported());
}
if !full {
return Ok(on_axis);
}
let mut start = 0;
for len in core::iter::once(rings.outer.len()).chain(rings.holes.iter().map(Vec::len)) {
let ring = &on_axis[start..start + len];
for k in 0..len {
if ring[k] && !ring[(k + len - 1) % len] && !ring[(k + 1) % len] {
return Err(GeomError::UnsupportedInput {
backend: crate::BACKEND_ID,
operation: Operation::Sweep,
input: "revolution of a profile that touches the axis at an isolated \
point: the solid pinches there",
});
}
}
start += len;
}
Ok(on_axis)
}
pub(crate) fn touching_unsupported() -> GeomError {
GeomError::UnsupportedInput {
backend: crate::BACKEND_ID,
operation: Operation::Sweep,
input: "revolution of a profile touching the axis",
}
}
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 full = (angle.abs() - core::f64::consts::TAU).abs() <= 1e-9;
let on_axis = check_axis_side(rings, axis_origin, axis_direction, tolerance, full)?;
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 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_welded(rings, &stations, full, &on_axis)?;
if matches!(crate::extrude::outward_orientation(&mesh), Some(false)) {
for triangle in mesh.indices.as_chunks_mut::<3>().0 {
triangle.swap(1, 2);
}
}
Ok(mesh)
}