1use nabled_core::scalar::NabledReal;
4use nabled_linalg::geometry::{Rotation3, Transform3, se3, so3};
5use ndarray::{Array1, Array2, arr1};
6
7use crate::ModelError;
8
9pub fn transform_from_xyz_rpy<T: NabledReal>(
13 xyz: [T; 3],
14 rpy: [T; 3],
15) -> Result<Transform3<T>, ModelError> {
16 let rotation = rotation_from_urdf_rpy(rpy)?;
17 let translation = arr1(&[xyz[0], xyz[1], xyz[2]]);
18 Ok(se3::from_rotation_translation(&rotation, &translation))
19}
20
21pub fn joint_origin_from_dh_scalars<T: NabledReal>(
23 a: T,
24 alpha: T,
25 d: T,
26 theta: T,
27) -> Result<Transform3<T>, ModelError> {
28 transform_from_xyz_rpy([a, T::zero(), d], [alpha, theta, T::zero()])
29}
30
31#[must_use]
33pub fn identity_transform<T: NabledReal>() -> Transform3<T> {
34 se3::from_rotation_translation(
35 &Rotation3 { matrix: Array2::<T>::eye(3) },
36 &Array1::<T>::zeros(3),
37 )
38}
39
40fn rotation_from_urdf_rpy<T: NabledReal>(rpy: [T; 3]) -> Result<Rotation3<T>, ModelError> {
41 let rx = so3::exp(&arr1(&[rpy[0], T::zero(), T::zero()]).view())
42 .map_err(|_| ModelError::InvalidInput("invalid roll angle".to_string()))?;
43 let ry = so3::exp(&arr1(&[T::zero(), rpy[1], T::zero()]).view())
44 .map_err(|_| ModelError::InvalidInput("invalid pitch angle".to_string()))?;
45 let rz = so3::exp(&arr1(&[T::zero(), T::zero(), rpy[2]]).view())
46 .map_err(|_| ModelError::InvalidInput("invalid yaw angle".to_string()))?;
47 let ry_rx = so3::compose(&ry, &rx)
48 .map_err(|_| ModelError::InvalidInput("invalid origin rotation".to_string()))?;
49 so3::compose(&rz, &ry_rx)
50 .map_err(|_| ModelError::InvalidInput("invalid origin rotation".to_string()))
51}
52
53#[cfg(test)]
54mod tests {
55 use approx::assert_relative_eq;
56
57 use super::*;
58
59 #[test]
60 fn pure_translation_origin() {
61 let tf = transform_from_xyz_rpy([1.0_f64, 0.0, 0.0], [0.0, 0.0, 0.0]).unwrap();
62 assert_relative_eq!(tf.translation[0], 1.0, epsilon = 1e-12);
63 }
64
65 #[test]
66 fn identity_transform_is_identity() {
67 let tf = identity_transform::<f64>();
68 assert_relative_eq!(tf.translation[0], 0.0, epsilon = 1e-12);
69 assert_relative_eq!(tf.rotation.matrix[[0, 0]], 1.0, epsilon = 1e-12);
70 }
71
72 #[test]
73 fn joint_origin_from_dh_scalars_matches_translation() {
74 let tf = joint_origin_from_dh_scalars(1.0_f64, 0.0, 0.5, 0.0).unwrap();
75 assert_relative_eq!(tf.translation[0], 1.0, epsilon = 1e-12);
76 assert_relative_eq!(tf.translation[2], 0.5, epsilon = 1e-12);
77 }
78
79 #[test]
80 fn yaw_rotation_rotates_x_into_y() {
81 let tf =
82 transform_from_xyz_rpy([0.0_f64, 0.0, 0.0], [0.0, 0.0, std::f64::consts::FRAC_PI_2])
83 .unwrap();
84 let point = se3::transform_point(&tf, &arr1(&[1.0_f64, 0.0, 0.0]).view());
85 assert_relative_eq!(point[0], 0.0, epsilon = 1e-10);
86 assert_relative_eq!(point[1], 1.0, epsilon = 1e-10);
87 }
88}