use crate::motion::{clamp, magnitude};
use libm::{acosf, atan2f, cosf, sinf, sqrtf};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform {
pub m: [f32; 16],
}
impl Transform {
pub fn identity() -> Self {
let mut m = [0.0; 16];
m[0] = 1.0;
m[5] = 1.0;
m[10] = 1.0;
m[15] = 1.0;
Self { m }
}
pub fn multiply(&self, other: &Transform) -> Transform {
let mut m = [0.0f32; 16];
for row in 0..4 {
for col in 0..4 {
let mut sum = 0.0;
for k in 0..4 {
sum += self.m[row * 4 + k] * other.m[k * 4 + col];
}
m[row * 4 + col] = sum;
}
}
Transform { m }
}
pub fn position(&self) -> (f32, f32, f32) {
(self.m[3], self.m[7], self.m[11])
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DhParameters {
pub a: f32,
pub alpha: f32,
pub d: f32,
pub theta: f32,
}
impl DhParameters {
pub fn transform(&self) -> Transform {
let (ct, st) = (cosf(self.theta), sinf(self.theta));
let (ca, sa) = (cosf(self.alpha), sinf(self.alpha));
Transform {
m: [
ct,
-st * ca,
st * sa,
self.a * ct,
st,
ct * ca,
-ct * sa,
self.a * st,
0.0,
sa,
ca,
self.d,
0.0,
0.0,
0.0,
1.0,
],
}
}
}
pub fn forward_kinematics(joints: &[DhParameters]) -> Transform {
let mut transform = Transform::identity();
for joint in joints {
transform = transform.multiply(&joint.transform());
}
transform
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Elbow {
Up,
Down,
}
#[derive(Clone, Copy, Debug)]
pub struct TwoLinkArm {
l1: f32,
l2: f32,
}
impl TwoLinkArm {
pub fn new(l1: f32, l2: f32) -> Self {
Self {
l1: magnitude(l1),
l2: magnitude(l2),
}
}
pub fn reach(&self) -> (f32, f32) {
(magnitude(self.l1 - self.l2), self.l1 + self.l2)
}
pub fn tip(&self, shoulder: f32, elbow: f32) -> (f32, f32) {
let x = self.l1 * cosf(shoulder) + self.l2 * cosf(shoulder + elbow);
let y = self.l1 * sinf(shoulder) + self.l2 * sinf(shoulder + elbow);
(x, y)
}
pub fn joints_for(&self, x: f32, y: f32, elbow: Elbow) -> Option<(f32, f32)> {
let distance_squared = x * x + y * y;
let distance = sqrtf(distance_squared);
let (min, max) = self.reach();
let tolerance = 1e-4;
if distance > max + tolerance || distance < min - tolerance {
return None;
}
let denominator = 2.0 * self.l1 * self.l2;
if denominator == 0.0 {
return None;
}
let cos_elbow = clamp(
(distance_squared - self.l1 * self.l1 - self.l2 * self.l2) / denominator,
-1.0,
1.0,
);
let elbow_magnitude = acosf(cos_elbow);
let elbow_angle = match elbow {
Elbow::Up => elbow_magnitude,
Elbow::Down => -elbow_magnitude,
};
let shoulder = atan2f(y, x)
- atan2f(
self.l2 * sinf(elbow_angle),
self.l1 + self.l2 * cosf(elbow_angle),
);
Some((shoulder, elbow_angle))
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f32::consts::FRAC_PI_2;
#[test]
fn dh_forward_kinematics_matches_the_planar_arm() {
let (q1, q2) = (0.6_f32, -0.4_f32);
let dh = [
DhParameters {
a: 1.5,
alpha: 0.0,
d: 0.0,
theta: q1,
},
DhParameters {
a: 1.0,
alpha: 0.0,
d: 0.0,
theta: q2,
},
];
let (x, y, z) = forward_kinematics(&dh).position();
let arm = TwoLinkArm::new(1.5, 1.0);
let (px, py) = arm.tip(q1, q2);
assert!((x - px).abs() < 1e-5);
assert!((y - py).abs() < 1e-5);
assert!(z.abs() < 1e-5);
}
#[test]
fn identity_is_the_multiplicative_unit() {
let t = DhParameters {
a: 0.7,
alpha: 0.3,
d: 0.2,
theta: 1.1,
}
.transform();
let i = Transform::identity();
assert_eq!(i.multiply(&t), t);
assert_eq!(t.multiply(&i), t);
}
#[test]
fn a_z_offset_lifts_the_tool_out_of_the_plane() {
let dh = [DhParameters {
a: 0.0,
alpha: 0.0,
d: 1.0,
theta: 0.0,
}];
let (x, y, z) = forward_kinematics(&dh).position();
assert!(x.abs() < 1e-5 && y.abs() < 1e-5 && (z - 1.0).abs() < 1e-5);
}
#[test]
fn a_twist_then_offset_swings_into_the_y_axis() {
let dh = [
DhParameters {
a: 0.0,
alpha: FRAC_PI_2,
d: 0.0,
theta: 0.0,
},
DhParameters {
a: 0.0,
alpha: 0.0,
d: 1.0,
theta: 0.0,
},
];
let (x, y, z) = forward_kinematics(&dh).position();
assert!(x.abs() < 1e-5 && (y + 1.0).abs() < 1e-5 && z.abs() < 1e-5);
}
#[test]
fn two_link_inverse_round_trips_both_elbows() {
let arm = TwoLinkArm::new(1.0, 1.2);
for &(q1, q2) in &[(0.5, 0.7), (0.2, -0.9), (-0.6, 1.1)] {
let (x, y) = arm.tip(q1, q2);
let elbow = if q2 >= 0.0 { Elbow::Up } else { Elbow::Down };
let (s, e) = arm.joints_for(x, y, elbow).unwrap();
let (rx, ry) = arm.tip(s, e);
assert!((rx - x).abs() < 1e-4 && (ry - y).abs() < 1e-4);
}
}
#[test]
fn unreachable_targets_have_no_solution() {
let arm = TwoLinkArm::new(1.0, 1.0);
assert!(arm.joints_for(5.0, 0.0, Elbow::Up).is_none()); assert!(arm.joints_for(0.0, 0.0, Elbow::Up).is_some()); }
#[test]
fn reach_is_the_link_sum_and_difference() {
let arm = TwoLinkArm::new(2.0, 0.5);
assert_eq!(arm.reach(), (1.5, 2.5));
}
}