use crate::kinematics::{Carrier, Chain};
use crate::model::{Link, Motion, MotionKind, Point3, RigidTransform};
use crate::numeric::Tolerance;
#[derive(Clone, Debug)]
pub struct RotaryFrame<'a> {
chain: Option<&'a Chain>,
angles: Vec<f64>,
loose: Vec<(String, f64)>,
unknown: Option<String>,
polar: bool,
cylindrical: bool,
}
impl<'a> RotaryFrame<'a> {
pub fn new(chain: Option<&'a Chain>) -> Self {
let angles = chain.map_or(Vec::new(), |c| vec![0.0; c.joints.len()]);
Self { chain, angles, loose: Vec::new(), unknown: None, polar: false, cylindrical: false }
}
fn target(&self, axes: &[crate::model::RotaryWord]) -> Result<Vec<f64>, String> {
match self.chain {
Some(chain) => {
let mut angles = self.angles.clone();
for word in axes {
let k = chain
.joints
.iter()
.position(|j| j.name.eq_ignore_ascii_case(&word.name))
.ok_or_else(|| format!("rotary axis {:?} is not on the machine", word.name))?;
if !word.angle_deg.is_finite() {
return Err(format!("rotary axis {} angle is not finite", word.name));
}
angles[k] = word.angle_deg;
}
Ok(angles)
}
None => Ok(Vec::new()),
}
}
pub fn commit(&mut self, kind: &MotionKind) {
match kind {
MotionKind::RotaryPosition { axes } | MotionKind::LinearRotary { axes, .. } => {
if self.chain.is_some() {
match self.target(axes) {
Ok(angles) => {
self.angles = angles;
self.unknown = None;
}
Err(reason) => self.unknown = Some(reason),
}
} else {
for word in axes {
match self.loose.iter_mut().find(|(n, _)| n.eq_ignore_ascii_case(&word.name)) {
Some(entry) => entry.1 = word.angle_deg,
None => self.loose.push((word.name.clone(), word.angle_deg)),
}
}
self.unknown = self
.loose
.iter()
.find(|(_, a)| *a != 0.0)
.map(|(n, a)| format!("rotary axis {n} at {a} degrees needs the machine's kinematics to simulate"));
}
}
MotionKind::PolarInterpolation { enabled } => self.polar = *enabled,
MotionKind::CylindricalInterpolation { radius } => self.cylindrical = radius.is_some(),
_ => {}
}
}
fn part_from_machine(&self, angles: &[f64], tool_axis: Point3, tol: &Tolerance) -> Result<RigidTransform, String> {
let Some(chain) = self.chain else {
if tool_axis.distance(Point3::Z) > tol.angle_eps {
return Err("the tool axis is not vertical (multi-axis motion needs multi-axis simulation)".into());
}
return Ok(RigidTransform::identity());
};
for (k, j) in chain.joints.iter().enumerate() {
if j.carrier == Carrier::Head && angles[k] != 0.0 {
return Err(format!("head axis {} at {} degrees tilts the tool", j.name, angles[k]));
}
}
let place = chain.link_transform(Link::Part, angles, Point3::ZERO).map_err(|e| format!("{e}"))?;
let to_part = place.inverse();
let axis = to_part.apply_vector(tool_axis);
if axis.distance(Point3::Z) > tol.angle_eps {
return Err("the tool axis is not vertical in the part frame (indexed or radial milling needs multi-axis simulation)".into());
}
Ok(to_part)
}
pub fn to_part(
&self,
motion: &Motion,
segments: &[(Point3, Point3)],
arc_chord: f64,
tol: &Tolerance,
) -> Result<Vec<(Point3, Point3)>, String> {
if let Some(reason) = &self.unknown {
return Err(reason.clone());
}
if self.cylindrical {
return Err("cylindrical interpolation programs unwrapped coordinates; not simulated in a Z-dexel stock".into());
}
let tool_axis = motion.tool_axis.and_then(|a| a.normalized()).unwrap_or(Point3::Z);
if self.polar {
if tool_axis.distance(Point3::Z) > tol.angle_eps {
return Err("polar interpolation with a non-vertical tool axis".into());
}
return Ok(segments.to_vec());
}
match &motion.kind {
MotionKind::LinearRotary { axes, .. } => {
if self.chain.is_none() {
return Err("a synchronized linear+rotary move needs the machine's kinematics to simulate".into());
}
let target = self.target(axes)?;
let mut out = Vec::new();
for &(a, b) in segments {
let sweep = self.angles.iter().zip(&target).map(|(p, q)| (q - p).abs()).fold(0.0, f64::max).to_radians();
let pivots = self.chain.map_or(0.0, |c| c.joints.iter().map(|j| j.origin.length()).fold(0.0, f64::max));
let reach = a.length().max(b.length()) + pivots + 1.0;
let step = 2.0 * (1.0 - (arc_chord / reach).min(1.0)).acos();
let n = if sweep > 0.0 && step > 0.0 { ((sweep / step).ceil() as usize).clamp(1, 1 << 16) } else { 1 };
let mut prev: Option<Point3> = None;
for i in 0..=n {
let f = i as f64 / n as f64;
let angles: Vec<f64> = self.angles.iter().zip(&target).map(|(p, q)| p + (q - p) * f).collect();
let to_part = self.part_from_machine(&angles, tool_axis, tol)?;
let p = to_part.apply_point(a.lerp(b, f));
if let Some(q) = prev {
out.push((q, p));
}
prev = Some(p);
}
}
Ok(out)
}
_ => {
let to_part = self.part_from_machine(&self.angles, tool_axis, tol)?;
Ok(segments.iter().map(|&(a, b)| (to_part.apply_point(a), to_part.apply_point(b))).collect())
}
}
}
}