use crate::core::scalar::ControlScalar;
use crate::kinematics::forward::Transform3D;
#[derive(Debug, Clone, Copy)]
pub struct ScaraConfig<S: ControlScalar> {
pub a1: S,
pub a2: S,
pub d4: S,
pub d1: S,
pub q_min: [S; 4],
pub q_max: [S; 4],
}
impl<S: ControlScalar> ScaraConfig<S> {
pub fn desktop() -> Self {
let two_pi = S::TWO * S::PI;
Self {
a1: S::from_f64(0.2),
a2: S::from_f64(0.2),
d4: S::from_f64(0.05),
d1: S::from_f64(0.35),
q_min: [-two_pi; 4],
q_max: [two_pi; 4],
}
}
}
pub struct ScaraRobot<S: ControlScalar> {
pub config: ScaraConfig<S>,
pub q: [S; 4],
}
impl<S: ControlScalar> ScaraRobot<S> {
pub fn new(config: ScaraConfig<S>) -> Self {
Self {
config,
q: [S::ZERO; 4],
}
}
pub fn set_joints(&mut self, q: [S; 4]) {
for (i, &qi) in q.iter().enumerate() {
self.q[i] = qi.clamp_val(self.config.q_min[i], self.config.q_max[i]);
}
}
pub fn forward(&self) -> (S, S, S, S) {
self.fk_impl(&self.q)
}
fn fk_impl(&self, q: &[S; 4]) -> (S, S, S, S) {
let q1 = q[0];
let q2 = q[1];
let d3 = q[2];
let q4 = q[3];
let c1 = q1.cos();
let s1 = q1.sin();
let c12 = (q1 + q2).cos();
let s12 = (q1 + q2).sin();
let x = self.config.a1 * c1 + self.config.a2 * c12;
let y = self.config.a1 * s1 + self.config.a2 * s12;
let z = self.config.d1 - d3 + self.config.d4;
let psi = q1 + q2 + q4;
(x, y, z, psi)
}
pub fn inverse(&self, x: S, y: S, z: S, psi: S) -> Option<[S; 4]> {
let r2 = x * x + y * y;
let r = r2.sqrt();
let a1 = self.config.a1;
let a2 = self.config.a2;
let r_max = a1 + a2;
let r_min = (a1 - a2).abs();
if r > r_max || r < r_min {
return None;
}
let cos_q2 = (r2 - a1 * a1 - a2 * a2) / (S::TWO * a1 * a2);
let cos_q2_clamped = cos_q2.clamp_val(-S::ONE, S::ONE);
let q2 = -cos_q2_clamped.acos();
let sin_q2 = q2.sin();
let k1 = a1 + a2 * cos_q2_clamped;
let k2 = a2 * sin_q2;
let q1 = y.atan2(x) - k2.atan2(k1);
let d3 = self.config.d1 + self.config.d4 - z;
let q4 = psi - q1 - q2;
let q = [q1, q2, d3, q4];
for (i, &qi) in q.iter().enumerate() {
if qi < self.config.q_min[i] || qi > self.config.q_max[i] {
return None;
}
}
Some(q)
}
pub fn jacobian(&self) -> [[S; 4]; 4] {
let q1 = self.q[0];
let q2 = self.q[1];
let s1 = q1.sin();
let c1 = q1.cos();
let s12 = (q1 + q2).sin();
let c12 = (q1 + q2).cos();
let a1 = self.config.a1;
let a2 = self.config.a2;
[
[-a1 * s1 - a2 * s12, -a2 * s12, S::ZERO, S::ZERO],
[a1 * c1 + a2 * c12, a2 * c12, S::ZERO, S::ZERO],
[S::ZERO, S::ZERO, -S::ONE, S::ZERO],
[S::ONE, S::ONE, S::ZERO, S::ONE],
]
}
pub fn forward_transforms(&self) -> [Transform3D<S>; 4] {
let q = &self.q;
let t1 = Transform3D::rot_z(q[0]).compose(&Transform3D::translate(
self.config.a1,
S::ZERO,
S::ZERO,
));
let t2 = t1
.compose(&Transform3D::rot_z(q[1]))
.compose(&Transform3D::translate(self.config.a2, S::ZERO, S::ZERO));
let t3 = t2.compose(&Transform3D::translate(S::ZERO, S::ZERO, -(q[2])));
let t4 = t3.compose(&Transform3D::rot_z(q[3]));
[t1, t2, t3, t4]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_scara() -> ScaraRobot<f64> {
ScaraRobot::new(ScaraConfig {
a1: 0.3,
a2: 0.2,
d4: 0.05,
d1: 0.4,
q_min: [-core::f64::consts::PI * 2.0; 4],
q_max: [core::f64::consts::PI * 2.0; 4],
})
}
#[test]
fn fk_at_zero_config() {
let mut robot = build_scara();
robot.set_joints([0.0, 0.0, 0.0, 0.0]);
let (x, y, z, _psi) = robot.forward();
assert!((x - 0.5).abs() < 1e-10, "x={}", x);
assert!(y.abs() < 1e-10, "y={}", y);
assert!((z - 0.45).abs() < 1e-10, "z={}", z);
}
#[test]
fn ik_fk_roundtrip() {
let mut robot = build_scara();
let q_orig = [0.3_f64, -0.5, 0.05, 0.2];
robot.set_joints(q_orig);
let (x, y, z, psi) = robot.forward();
let q_sol = robot.inverse(x, y, z, psi).expect("IK should succeed");
robot.set_joints(q_sol);
let (x2, y2, z2, psi2) = robot.forward();
assert!((x2 - x).abs() < 1e-6, "x error: {} vs {}", x2, x);
assert!((y2 - y).abs() < 1e-6, "y error: {} vs {}", y2, y);
assert!((z2 - z).abs() < 1e-6, "z error: {} vs {}", z2, z);
assert!((psi2 - psi).abs() < 1e-6, "psi error: {} vs {}", psi2, psi);
}
#[test]
fn unreachable_target_returns_none() {
let robot = build_scara();
let result = robot.inverse(0.6, 0.0, 0.45, 0.0);
assert!(result.is_none());
}
#[test]
fn jacobian_shape() {
let mut robot = build_scara();
robot.set_joints([0.3, 0.5, 0.0, 0.0]);
let j = robot.jacobian();
assert!((j[2][2] - (-1.0)).abs() < 1e-10);
assert!(j[2][0].abs() < 1e-10);
}
}