use crate::error::{Error, Result};
use crate::geom::{cross, normalize, Aabb, Point3, Vector3};
use crate::mesh::TriMesh;
use crate::motion::{MotionSegment, Pose};
use crate::tool::Tool;
use std::f64::consts::{PI, TAU};
const MAX_AXIS_CHANGE: f64 = 150.0 * PI / 180.0;
const MAX_SUB_STEPS: usize = 256;
const MAX_ARC_STEPS: usize = 4096;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum SweptVolume {
ConvexHull(Vec<Point3>),
HullUnion(Vec<Vec<Point3>>),
Mesh(TriMesh),
}
impl SweptVolume {
pub fn bounds(&self) -> Aabb {
match self {
SweptVolume::ConvexHull(points) => Aabb::from_points(points.iter().copied()),
SweptVolume::HullUnion(hulls) => Aabb::from_points(hulls.iter().flatten().copied()),
SweptVolume::Mesh(mesh) => mesh.bounds(),
}
}
pub fn piece_count(&self) -> usize {
match self {
SweptVolume::ConvexHull(_) | SweptVolume::Mesh(_) => 1,
SweptVolume::HullUnion(hulls) => hulls.len(),
}
}
pub fn piece_bounds(&self) -> Vec<Aabb> {
match self {
SweptVolume::HullUnion(hulls) => hulls.iter().map(|h| Aabb::from_points(h.iter().copied())).collect(),
_ => vec![self.bounds()],
}
}
}
pub fn circle_segments(radius: f64, chord_tolerance: f64) -> usize {
let tol = chord_tolerance.max(1e-9).min(radius * 0.5);
let step = 2.0 * (1.0 - tol / radius).acos();
let n = (TAU / step).ceil();
if n.is_finite() {
(n as usize).clamp(8, 1024)
} else {
1024
}
}
pub fn area_equivalent_radius(radius: f64, segments: usize) -> f64 {
let n = segments as f64;
let a = TAU / n;
radius * (TAU / (n * a.sin())).sqrt()
}
pub fn mean_width_radius(radius: f64, segments: usize) -> f64 {
let n = segments as f64;
radius * PI / (n * (PI / n).sin())
}
fn chord_steps(angle: f64, reach: f64, chord_tolerance: f64, max_steps: usize) -> usize {
if angle <= 1e-12 || reach <= 0.0 {
return 1;
}
let ratio = (chord_tolerance.max(1e-12) / reach).min(1.0);
let step = 2.0 * (1.0 - ratio).acos();
if step <= 0.0 || !step.is_finite() {
return max_steps;
}
((angle / step).ceil().max(1.0) as usize).min(max_steps)
}
pub fn rotation_steps(angle: f64, reach: f64, chord_tolerance: f64) -> usize {
chord_steps(angle, reach, chord_tolerance, MAX_SUB_STEPS)
}
fn frame(axis: Vector3) -> Option<(Vector3, Vector3)> {
let axis = normalize(axis)?;
let helper = if axis[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let u = normalize(cross(axis, helper))?;
let v = cross(axis, u);
Some((u, v))
}
fn revolve_points(profile: &[(f64, f64)], pose: &Pose, segments: usize, radial_scale: f64) -> Result<Vec<Point3>> {
let axis = normalize(pose.axis)
.ok_or_else(|| Error::InvalidInput(format!("pose axis is not a valid direction: {:?}", pose.axis)))?;
let (u, v) = frame(axis).ok_or_else(|| Error::InvalidInput("degenerate tool axis".into()))?;
let mut points = Vec::with_capacity(profile.len() * segments);
for &(radial, height) in profile {
let c = [
pose.tip[0] + axis[0] * height,
pose.tip[1] + axis[1] * height,
pose.tip[2] + axis[2] * height,
];
let r = radial * radial_scale;
if r <= 1e-12 {
points.push(c);
continue;
}
for k in 0..segments {
let (s, cs) = (TAU * k as f64 / segments as f64).sin_cos();
points.push([
c[0] + r * (cs * u[0] + s * v[0]),
c[1] + r * (cs * u[1] + s * v[1]),
c[2] + r * (cs * u[2] + s * v[2]),
]);
}
}
Ok(points)
}
pub fn tool_instance_points(tool: &Tool, pose: &Pose, chord_tolerance: f64) -> Result<Vec<Point3>> {
let n = circle_segments(tool.radius(), chord_tolerance);
let scale = mean_width_radius(tool.radius(), n) / tool.radius();
revolve_points(&tool.cutting_profile(chord_tolerance), pose, n, scale)
}
pub fn non_cutting_instance_points(tool: &Tool, pose: &Pose, chord_tolerance: f64) -> Result<Vec<Vec<Point3>>> {
tool.non_cutting_cylinders()
.iter()
.map(|&(radius, h0, h1)| {
let n = circle_segments(radius, chord_tolerance);
let scale = mean_width_radius(radius, n) / radius;
revolve_points(&[(radius, h0), (radius, h1)], pose, n, scale)
})
.collect()
}
pub fn sample_poses(tool: &Tool, segment: &MotionSegment, chord_tolerance: f64) -> Result<Vec<Pose>> {
let (from, to) = segment.endpoints();
let axis_angle = from.axis_angle_to(to);
if axis_angle > MAX_AXIS_CHANGE {
return Err(Error::Unsupported(format!(
"tool axis changes by {:.1} degrees within one segment; split the segment",
axis_angle.to_degrees()
)));
}
let reach = tool.reach();
let mut steps = rotation_steps(axis_angle, reach, chord_tolerance);
if let Some(arc) = segment.arc_geometry()? {
let arc_steps = chord_steps(arc.angle, arc.radius + reach, chord_tolerance, MAX_ARC_STEPS);
steps = steps.max(arc_steps);
}
Ok((0..=steps).map(|k| segment.pose_at(k as f64 / steps as f64)).collect())
}
fn hulls_between(poses: &[Pose], mut points_at: impl FnMut(&Pose) -> Result<Vec<Point3>>) -> Result<SweptVolume> {
let mut prev = points_at(&poses[0])?;
if poses.len() == 2 {
let mut hull = prev;
hull.extend(points_at(&poses[1])?);
return Ok(SweptVolume::ConvexHull(hull));
}
let mut hulls = Vec::with_capacity(poses.len() - 1);
for pose in &poses[1..] {
let next = points_at(pose)?;
let mut hull = prev.clone();
hull.extend(next.iter().copied());
hulls.push(hull);
prev = next;
}
Ok(SweptVolume::HullUnion(hulls))
}
pub fn swept_volume(tool: &Tool, segment: &MotionSegment, chord_tolerance: f64) -> Result<SweptVolume> {
tool.validate()?;
segment.validate()?;
let poses = sample_poses(tool, segment, chord_tolerance)?;
hulls_between(&poses, |pose| tool_instance_points(tool, pose, chord_tolerance))
}
pub fn swept_pieces(
tool: &Tool,
segment: &MotionSegment,
chord_tolerance: f64,
) -> Result<Vec<(Pose, Pose, Vec<Point3>)>> {
tool.validate()?;
segment.validate()?;
let poses = sample_poses(tool, segment, chord_tolerance)?;
let mut prev = tool_instance_points(tool, &poses[0], chord_tolerance)?;
let mut out = Vec::with_capacity(poses.len() - 1);
for k in 1..poses.len() {
let next = tool_instance_points(tool, &poses[k], chord_tolerance)?;
let mut hull = prev.clone();
hull.extend(next.iter().copied());
out.push((poses[k - 1], poses[k], hull));
prev = next;
}
Ok(out)
}
pub fn non_cutting_volumes(tool: &Tool, segment: &MotionSegment, chord_tolerance: f64) -> Result<Vec<SweptVolume>> {
tool.validate()?;
segment.validate()?;
let count = tool.non_cutting_cylinders().len();
if count == 0 {
return Ok(Vec::new());
}
let poses = sample_poses(tool, segment, chord_tolerance)?;
(0..count)
.map(|i| {
hulls_between(&poses, |pose| {
Ok(non_cutting_instance_points(tool, pose, chord_tolerance)?.swap_remove(i))
})
})
.collect()
}