use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct DeltaConfig<S: ControlScalar> {
pub r_f: S,
pub r_e: S,
pub l1: S,
pub l2: S,
pub theta_min: S,
pub theta_max: S,
}
impl<S: ControlScalar> DeltaConfig<S> {
pub fn desktop() -> Self {
Self {
r_f: S::from_f64(0.15),
r_e: S::from_f64(0.05),
l1: S::from_f64(0.2),
l2: S::from_f64(0.35),
theta_min: -S::PI / S::TWO,
theta_max: S::PI / S::TWO,
}
}
}
pub struct DeltaRobot<S: ControlScalar> {
pub config: DeltaConfig<S>,
pub theta: [S; 3],
}
impl<S: ControlScalar> DeltaRobot<S> {
pub fn new(config: DeltaConfig<S>) -> Self {
Self {
config,
theta: [S::ZERO; 3],
}
}
pub fn inverse(&self, x: S, y: S, z: S) -> Option<[S; 3]> {
let mut angles = [S::ZERO; 3];
let two_pi = S::TWO * S::PI;
let third = two_pi / S::from_f64(3.0);
for (i, angle) in angles.iter_mut().enumerate() {
let delta = S::from_f64(i as f64) * third;
let cos_d = delta.cos();
let sin_d = delta.sin();
let pi_proj = x * cos_d + y * sin_d;
let qi_proj = -x * sin_d + y * cos_d;
let xm = (self.config.r_f - self.config.r_e) - pi_proj;
let l1 = self.config.l1;
let l2 = self.config.l2;
let g = xm * xm + z * z + qi_proj * qi_proj + l1 * l1 - l2 * l2;
let c_val = -g / (S::TWO * l1);
let r = (xm * xm + z * z).sqrt();
if r < S::from_f64(1e-10) {
return None;
}
let phi = (-z).atan2(xm); let ratio = c_val / r;
if ratio.abs() > S::ONE {
return None;
}
let theta_i = phi - ratio.acos();
if theta_i < self.config.theta_min || theta_i > self.config.theta_max {
return None;
}
*angle = theta_i;
}
Some(angles)
}
pub fn forward(&self) -> Option<(S, S, S)> {
self.fk_impl(&self.theta)
}
fn fk_impl(&self, theta: &[S; 3]) -> Option<(S, S, S)> {
let two_pi = S::TWO * S::PI;
let third = two_pi / S::from_f64(3.0);
let rf_re = self.config.r_f - self.config.r_e;
let l1 = self.config.l1;
let mut ex = [S::ZERO; 3];
let mut ey = [S::ZERO; 3];
let mut ez = [S::ZERO; 3];
for i in 0..3 {
let delta = S::from_f64(i as f64) * third;
let r_i = rf_re + l1 * theta[i].cos();
ex[i] = r_i * delta.cos();
ey[i] = r_i * delta.sin();
ez[i] = l1 * theta[i].sin();
}
let norm0 = ex[0] * ex[0] + ey[0] * ey[0] + ez[0] * ez[0];
let norm1 = ex[1] * ex[1] + ey[1] * ey[1] + ez[1] * ez[1];
let norm2 = ex[2] * ex[2] + ey[2] * ey[2] + ez[2] * ez[2];
let p1 = S::TWO * (ex[0] - ex[1]);
let q1 = S::TWO * (ey[0] - ey[1]);
let r1 = S::TWO * (ez[0] - ez[1]);
let s1 = norm0 - norm1;
let p2 = S::TWO * (ex[0] - ex[2]);
let q2 = S::TWO * (ey[0] - ey[2]);
let r2 = S::TWO * (ez[0] - ez[2]);
let s2 = norm0 - norm2;
let det = p1 * q2 - p2 * q1;
if det.abs() < S::from_f64(1e-10) {
return None; }
let det_inv = S::ONE / det;
let ax = (s1 * q2 - s2 * q1) * det_inv;
let bx = (r2 * q1 - r1 * q2) * det_inv; let ay = (p1 * s2 - p2 * s1) * det_inv;
let by_coeff = (p2 * r1 - p1 * r2) * det_inv;
let cx = ax - ex[0];
let cy = ay - ey[0];
let cz = -ez[0];
let a_coeff = bx * bx + by_coeff * by_coeff + S::ONE;
let b_coeff = S::TWO * (cx * bx + cy * by_coeff + cz);
let c_coeff = cx * cx + cy * cy + cz * cz - self.config.l2 * self.config.l2;
let discriminant = b_coeff * b_coeff - S::from_f64(4.0) * a_coeff * c_coeff;
if discriminant < S::ZERO {
return None;
}
let sqrt_disc = discriminant.sqrt();
let z1 = (-b_coeff + sqrt_disc) / (S::TWO * a_coeff);
let z2 = (-b_coeff - sqrt_disc) / (S::TWO * a_coeff);
let z = if z1 < z2 { z1 } else { z2 };
let x = ax + bx * z;
let y = ay + by_coeff * z;
Some((x, y, z))
}
pub fn set_joints(&mut self, theta: [S; 3]) {
for (i, &ti) in theta.iter().enumerate() {
self.theta[i] = ti.clamp_val(self.config.theta_min, self.config.theta_max);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_delta() -> DeltaRobot<f64> {
DeltaRobot::new(DeltaConfig::desktop())
}
#[test]
fn fk_at_zero_angles_gives_center_below_base() {
let mut robot = build_delta();
robot.set_joints([0.0, 0.0, 0.0]);
let (x, y, z) = robot.forward().expect("FK should succeed at zero angles");
assert!(x.abs() < 1e-6, "x={:.6} should be ~0", x);
assert!(y.abs() < 1e-6, "y={:.6} should be ~0", y);
assert!(z < 0.0, "z={:.4} should be negative (EE below base)", z);
}
#[test]
fn ik_fk_roundtrip() {
let mut robot = build_delta();
let (xr, yr, zr) = (0.0_f64, 0.0, -0.25);
let angles = robot.inverse(xr, yr, zr).expect("IK should succeed");
robot.set_joints(angles);
let (x2, y2, z2) = robot.forward().expect("FK should succeed");
assert!((x2 - xr).abs() < 1e-4, "x error: {} vs {}", x2, xr);
assert!((y2 - yr).abs() < 1e-4, "y error: {} vs {}", y2, yr);
assert!((z2 - zr).abs() < 1e-4, "z error: {} vs {}", z2, zr);
}
#[test]
fn unreachable_returns_none() {
let robot = build_delta();
assert!(robot.inverse(0.0, 0.0, -1.5).is_none());
}
#[test]
fn off_center_ik_fk_roundtrip() {
let mut robot = build_delta();
let (xr, yr, zr) = (0.05_f64, 0.03, -0.22);
let angles = match robot.inverse(xr, yr, zr) {
Some(a) => a,
None => return, };
robot.set_joints(angles);
let (x2, y2, z2) = robot.forward().expect("FK should succeed");
assert!((x2 - xr).abs() < 1e-3, "x: {} vs {}", x2, xr);
assert!((y2 - yr).abs() < 1e-3, "y: {} vs {}", y2, yr);
assert!((z2 - zr).abs() < 1e-3, "z: {} vs {}", z2, zr);
}
}