use crate::model::{Kinematics, Machine, MachineKind, Point3, RigidTransform, RotaryAxis};
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
pub mod library;
pub mod post;
pub mod sequence;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Carrier {
Table,
Head,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Joint {
pub name: String,
pub direction: Point3,
pub origin: Point3,
pub limits_deg: Option<[f64; 2]>,
pub carrier: Carrier,
}
pub use crate::model::Link;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChainKind {
Mill3,
Mill4,
TableTable,
HeadHead,
HeadTable,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Chain {
pub kind: ChainKind,
pub joints: Vec<Joint>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KinematicsOptions {
pub tcp: bool,
pub singular_cone_deg: f64,
pub cos_eps: f64,
}
impl Default for KinematicsOptions {
fn default() -> Self {
Self { tcp: true, singular_cone_deg: 0.5, cos_eps: 1e-6 }
}
}
impl KinematicsOptions {
pub fn validate(&self) -> Result<(), KinematicsError> {
if !self.singular_cone_deg.is_finite() || self.singular_cone_deg < 0.0 || self.singular_cone_deg >= 90.0 {
return Err(KinematicsError::InvalidInput("singular cone must be in [0, 90) degrees".into()));
}
if !self.cos_eps.is_finite() || self.cos_eps < 0.0 || self.cos_eps > 1e-3 {
return Err(KinematicsError::InvalidInput("cosine tolerance must be in [0, 1e-3]".into()));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pose {
pub linear: Point3,
pub angles_deg: Vec<f64>,
pub branch: u8,
pub singular: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum KinematicsError {
InvalidMachine(String),
InvalidInput(String),
InvalidAxis,
Unreachable { axis: Point3 },
LimitViolation { index: usize, joint: String },
}
impl std::fmt::Display for KinematicsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KinematicsError::InvalidMachine(m) => write!(f, "machine: {m}"),
KinematicsError::InvalidInput(m) => write!(f, "kinematics: {m}"),
KinematicsError::InvalidAxis => write!(f, "tool axis must be a finite non-zero vector"),
KinematicsError::Unreachable { axis } => {
write!(f, "tool axis [{}, {}, {}] is unreachable on this machine", axis.x, axis.y, axis.z)
}
KinematicsError::LimitViolation { index, joint } => {
write!(f, "CL point {index} needs rotary {joint} outside its limits")
}
}
}
}
impl std::error::Error for KinematicsError {}
impl From<KinematicsError> for crate::CamError {
fn from(e: KinematicsError) -> Self {
crate::CamError::InvalidInput(e.to_string())
}
}
pub fn rotate(d: Point3, theta: f64, v: Point3) -> Point3 {
let (s, c) = theta.sin_cos();
v.scale(c).add(d.cross(v).scale(s)).add(d.scale(d.dot(v) * (1.0 - c)))
}
pub fn rotate_about(o: Point3, d: Point3, theta: f64, x: Point3) -> Point3 {
o.add(rotate(d, theta, x.sub(o)))
}
fn signed_angle(d: Point3, x: Point3, y: Point3) -> f64 {
(x.cross(y).dot(d)).atan2(x.dot(y))
}
fn perp(d: Point3, v: Point3) -> Point3 {
v.sub(d.scale(v.dot(d)))
}
fn wrap_pi(a: f64) -> f64 {
let mut r = a % (2.0 * PI);
if r <= -PI {
r += 2.0 * PI;
} else if r > PI {
r -= 2.0 * PI;
}
r
}
pub fn wrap_deg(a: f64) -> f64 {
let mut r = a % 360.0;
if r <= -180.0 {
r += 360.0;
} else if r > 180.0 {
r -= 360.0;
}
r
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TwoAxisSolution {
pub alpha: f64,
pub beta: f64,
pub singular: bool,
}
pub fn two_axis(
d_a: Point3,
d_b: Point3,
u: Point3,
w: Point3,
prev: [f64; 2],
cone: f64,
cos_eps: f64,
) -> Vec<TwoAxisSolution> {
let ua = u.dot(d_a);
let ab = d_a.dot(d_b);
let a = u.dot(d_b) - ua * ab;
let b = d_a.cross(u).dot(d_b);
let c = ua * ab;
let k = w.dot(d_b);
let r = a.hypot(b);
let sin_cone = cone.sin().max(0.0);
let mut alphas: Vec<(f64, bool)> = Vec::new();
if r <= sin_cone.max(cos_eps) {
if (k - c).abs() > r + cos_eps {
return Vec::new();
}
alphas.push((prev[0], true));
} else {
let phi = b.atan2(a);
let ratio = (k - c) / r;
if ratio > 1.0 + cos_eps || ratio < -1.0 - cos_eps {
return Vec::new();
}
let delta = ratio.clamp(-1.0, 1.0).acos();
alphas.push((wrap_pi(phi + delta), false));
if delta > 0.0 {
alphas.push((wrap_pi(phi - delta), false));
}
}
let w_perp = perp(d_b, w);
let w_singular = w_perp.length() <= sin_cone.max(cos_eps);
let mut out = Vec::new();
for (alpha, alpha_free) in alphas {
let u1 = rotate(d_a, alpha, u);
let u_perp = perp(d_b, u1);
let (beta, beta_free) = if w_singular || u_perp.length() <= 1e-12 {
(prev[1], true)
} else {
(wrap_pi(signed_angle(d_b, u_perp, w_perp)), false)
};
out.push(TwoAxisSolution { alpha, beta, singular: alpha_free || beta_free });
}
out
}
pub fn one_axis(d: Point3, u: Point3, w: Point3, prev: f64, cone: f64, cos_eps: f64) -> Option<(f64, bool)> {
if (u.dot(d) - w.dot(d)).abs() > cos_eps {
return None;
}
let up = perp(d, u);
let wp = perp(d, w);
if wp.length() <= cone.sin().max(cos_eps) || up.length() <= cos_eps {
return Some((prev, true));
}
Some((wrap_pi(signed_angle(d, up, wp)), false))
}
fn joint(axis: &RotaryAxis, carrier: Carrier, wcs_origin: Point3) -> Result<Joint, KinematicsError> {
axis.validate().map_err(KinematicsError::InvalidMachine)?;
let direction = axis.direction.normalized().ok_or_else(|| {
KinematicsError::InvalidMachine(format!("rotary axis {} has a zero direction", axis.name))
})?;
let origin = match carrier {
Carrier::Table => axis.origin.sub(wcs_origin),
Carrier::Head => axis.origin,
};
Ok(Joint { name: axis.name.clone(), direction, origin, limits_deg: axis.limits_deg, carrier })
}
impl Chain {
pub fn from_machine(machine: &Machine, wcs_origin: Point3) -> Result<Chain, KinematicsError> {
machine.validate().map_err(KinematicsError::InvalidMachine)?;
if !wcs_origin.is_finite() {
return Err(KinematicsError::InvalidMachine("work offset origin is not finite".into()));
}
let chain = match &machine.kind {
MachineKind::Mill3 => Chain { kind: ChainKind::Mill3, joints: Vec::new() },
MachineKind::Lathe => {
return Err(KinematicsError::InvalidMachine("lathe kinematics are not a milling chain".into()))
}
MachineKind::Mill4 { rotary } => {
Chain { kind: ChainKind::Mill4, joints: vec![joint(rotary, Carrier::Table, wcs_origin)?] }
}
MachineKind::MillTurn { c_axis, .. } => {
Chain { kind: ChainKind::Mill4, joints: vec![joint(c_axis, Carrier::Table, wcs_origin)?] }
}
MachineKind::Mill5 { kinematics } => match kinematics {
Kinematics::TableTable { primary, secondary } => Chain {
kind: ChainKind::TableTable,
joints: vec![
joint(primary, Carrier::Table, wcs_origin)?,
joint(secondary, Carrier::Table, wcs_origin)?,
],
},
Kinematics::HeadHead { primary, secondary } => Chain {
kind: ChainKind::HeadHead,
joints: vec![
joint(primary, Carrier::Head, wcs_origin)?,
joint(secondary, Carrier::Head, wcs_origin)?,
],
},
Kinematics::HeadTable { head, table } => Chain {
kind: ChainKind::HeadTable,
joints: vec![joint(head, Carrier::Head, wcs_origin)?, joint(table, Carrier::Table, wcs_origin)?],
},
},
};
if chain.joints.len() == 2 {
let (a, b) = (chain.joints[0].direction, chain.joints[1].direction);
if a.cross(b).length() <= 1e-9 {
return Err(KinematicsError::InvalidMachine("the two rotary axes are parallel".into()));
}
if chain.joints[0].name == chain.joints[1].name {
return Err(KinematicsError::InvalidMachine("the two rotary axes share a name".into()));
}
}
Ok(chain)
}
pub fn mill3() -> Chain {
Chain { kind: ChainKind::Mill3, joints: Vec::new() }
}
pub fn rotary_count(&self) -> usize {
self.joints.len()
}
pub fn joint_names(&self) -> Vec<&str> {
self.joints.iter().map(|j| j.name.as_str()).collect()
}
fn check_angles(&self, angles_deg: &[f64]) -> Result<(), KinematicsError> {
if angles_deg.len() != self.joints.len() {
return Err(KinematicsError::InvalidInput(format!(
"expected {} rotary angles, got {}",
self.joints.len(),
angles_deg.len()
)));
}
if angles_deg.iter().any(|a| !a.is_finite()) {
return Err(KinematicsError::InvalidInput("rotary angle is not finite".into()));
}
Ok(())
}
fn joint_transform(j: &Joint, theta: f64) -> RigidTransform {
let r = RigidTransform::axis_angle(j.direction, theta, Point3::ZERO).unwrap_or_else(|_| RigidTransform::identity());
let shift = j.origin.sub(r.apply_vector(j.origin));
RigidTransform { rotation: r.rotation, translation: shift }
}
pub fn link_transform(&self, link: Link, angles_deg: &[f64], linear: Point3) -> Result<RigidTransform, KinematicsError> {
self.check_angles(angles_deg)?;
let th: Vec<f64> = angles_deg.iter().map(|a| a.to_radians()).collect();
let j = &self.joints;
let table_joints: Vec<usize> = (0..j.len()).filter(|&k| j[k].carrier == Carrier::Table).collect();
let head_joints: Vec<usize> = (0..j.len()).filter(|&k| j[k].carrier == Carrier::Head).collect();
let compose = |ids: &[usize]| -> RigidTransform {
let mut t = RigidTransform::identity();
for &k in ids.iter().rev() {
t = t.then(&Self::joint_transform(&j[k], th[k]));
}
t
};
let shift = RigidTransform::translation(linear);
Ok(match link {
Link::Base => RigidTransform::identity(),
Link::Part => compose(&table_joints),
Link::Tool => compose(&head_joints).then(&shift),
Link::Joint { index: k } => {
if k >= j.len() {
return Err(KinematicsError::InvalidInput(format!("link joint {k} does not exist")));
}
match j[k].carrier {
Carrier::Table => {
let upto: Vec<usize> = table_joints.iter().copied().take_while(|&i| i <= k).collect();
compose(&upto)
}
Carrier::Head => {
let upto: Vec<usize> = head_joints.iter().copied().take_while(|&i| i <= k).collect();
compose(&upto).then(&shift)
}
}
}
})
}
pub fn part_to_world(&self, angles_deg: &[f64], x: Point3) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::HeadHead => x,
ChainKind::Mill4 => rotate_about(j[0].origin, j[0].direction, angles_deg[0].to_radians(), x),
ChainKind::TableTable => {
let inner = rotate_about(j[1].origin, j[1].direction, angles_deg[1].to_radians(), x);
rotate_about(j[0].origin, j[0].direction, angles_deg[0].to_radians(), inner)
}
ChainKind::HeadTable => rotate_about(j[1].origin, j[1].direction, angles_deg[1].to_radians(), x),
}
}
pub fn world_to_part(&self, angles_deg: &[f64], x: Point3) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::HeadHead => x,
ChainKind::Mill4 => rotate_about(j[0].origin, j[0].direction, -angles_deg[0].to_radians(), x),
ChainKind::TableTable => {
let outer = rotate_about(j[0].origin, j[0].direction, -angles_deg[0].to_radians(), x);
rotate_about(j[1].origin, j[1].direction, -angles_deg[1].to_radians(), outer)
}
ChainKind::HeadTable => rotate_about(j[1].origin, j[1].direction, -angles_deg[1].to_radians(), x),
}
}
pub fn part_dir_to_world(&self, angles_deg: &[f64], v: Point3) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::HeadHead => v,
ChainKind::Mill4 => rotate(j[0].direction, angles_deg[0].to_radians(), v),
ChainKind::TableTable => {
let inner = rotate(j[1].direction, angles_deg[1].to_radians(), v);
rotate(j[0].direction, angles_deg[0].to_radians(), inner)
}
ChainKind::HeadTable => rotate(j[1].direction, angles_deg[1].to_radians(), v),
}
}
pub fn tool_axis_world(&self, angles_deg: &[f64]) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::Mill4 | ChainKind::TableTable => Point3::Z,
ChainKind::HeadHead => {
let inner = rotate(j[1].direction, angles_deg[1].to_radians(), Point3::Z);
rotate(j[0].direction, angles_deg[0].to_radians(), inner)
}
ChainKind::HeadTable => rotate(j[0].direction, angles_deg[0].to_radians(), Point3::Z),
}
}
pub fn head_offset(&self, angles_deg: &[f64]) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::Mill4 | ChainKind::TableTable => Point3::ZERO,
ChainKind::HeadHead => {
let (t1, t2) = (angles_deg[0].to_radians(), angles_deg[1].to_radians());
let o1 = j[0].origin;
let o2 = j[1].origin;
let inner = o2.sub(rotate(j[1].direction, t2, o2)).sub(o1);
o1.add(rotate(j[0].direction, t1, inner))
}
ChainKind::HeadTable => {
let o = j[0].origin;
o.sub(rotate(j[0].direction, angles_deg[0].to_radians(), o))
}
}
}
pub fn forward(&self, pose: &Pose, options: &KinematicsOptions) -> Result<(Point3, Point3), KinematicsError> {
self.check_angles(&pose.angles_deg)?;
if !pose.linear.is_finite() {
return Err(KinematicsError::InvalidInput("linear position is not finite".into()));
}
let axis_w = self.tool_axis_world(&pose.angles_deg);
let axis = self.world_dir_to_part(&pose.angles_deg, axis_w);
let tip = if options.tcp {
pose.linear
} else {
let tip_w = pose.linear.add(self.head_offset(&pose.angles_deg));
self.world_to_part(&pose.angles_deg, tip_w)
};
Ok((tip, axis))
}
pub fn world_dir_to_part(&self, angles_deg: &[f64], v: Point3) -> Point3 {
let j = &self.joints;
match self.kind {
ChainKind::Mill3 | ChainKind::HeadHead => v,
ChainKind::Mill4 => rotate(j[0].direction, -angles_deg[0].to_radians(), v),
ChainKind::TableTable => {
let outer = rotate(j[0].direction, -angles_deg[0].to_radians(), v);
rotate(j[1].direction, -angles_deg[1].to_radians(), outer)
}
ChainKind::HeadTable => rotate(j[1].direction, -angles_deg[1].to_radians(), v),
}
}
pub fn linear_for(&self, tip: Point3, angles_deg: &[f64], options: &KinematicsOptions) -> Point3 {
if options.tcp {
tip
} else {
self.part_to_world(angles_deg, tip).sub(self.head_offset(angles_deg))
}
}
pub fn inverse(
&self,
tip: Point3,
axis: Option<Point3>,
previous: Option<&[f64]>,
options: &KinematicsOptions,
) -> Result<Vec<Pose>, KinematicsError> {
options.validate()?;
if !tip.is_finite() {
return Err(KinematicsError::InvalidInput("tool tip is not finite".into()));
}
let a = match axis {
None => Point3::Z,
Some(v) => v.normalized().ok_or(KinematicsError::InvalidAxis)?,
};
let prev: Vec<f64> = match previous {
Some(p) => {
self.check_angles(p)?;
p.iter().map(|d| d.to_radians()).collect()
}
None => vec![0.0; self.joints.len()],
};
let cone = options.singular_cone_deg.to_radians();
let eps = options.cos_eps;
let unreachable = || KinematicsError::Unreachable { axis: a };
let j = &self.joints;
let mut raw: Vec<(Vec<f64>, bool)> = Vec::new();
match self.kind {
ChainKind::Mill3 => {
if 1.0 - a.dot(Point3::Z) > eps {
return Err(unreachable());
}
raw.push((Vec::new(), false));
}
ChainKind::Mill4 => {
let (theta, singular) =
one_axis(j[0].direction, a, Point3::Z, prev[0], cone, eps).ok_or_else(unreachable)?;
raw.push((vec![theta], singular));
}
ChainKind::TableTable => {
for s in two_axis(j[1].direction, j[0].direction, a, Point3::Z, [prev[1], prev[0]], cone, eps) {
raw.push((vec![s.beta, s.alpha], s.singular));
}
}
ChainKind::HeadHead => {
for s in two_axis(j[1].direction, j[0].direction, Point3::Z, a, [prev[1], prev[0]], cone, eps) {
raw.push((vec![s.beta, s.alpha], s.singular));
}
}
ChainKind::HeadTable => {
for s in two_axis(j[0].direction, j[1].direction, Point3::Z, a, [prev[0], -prev[1]], cone, eps) {
raw.push((vec![s.alpha, -s.beta], s.singular));
}
}
}
if raw.is_empty() {
return Err(unreachable());
}
let mut poses = Vec::with_capacity(raw.len());
for (branch, (angles, singular)) in raw.into_iter().enumerate() {
let angles_deg: Vec<f64> = angles.iter().map(|r| wrap_deg(r.to_degrees())).collect();
let linear = self.linear_for(tip, &angles_deg, options);
poses.push(Pose { linear, angles_deg, branch: branch as u8, singular });
}
Ok(poses)
}
}