spatial_math/
transform.rs

1// Copyright (C) 2020-2025 spatial-math authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Spatial transformations using Plücker coordinates.
16//!
17//! This module implements spatial coordinate transformations following Featherstone's
18//! spatial algebra formulation. Plücker coordinates provide an elegant way to represent
19//! both rotation and translation in a unified 6×6 transformation matrix.
20//!
21//! # Plücker Transform Theory
22//!
23//! A spatial transform X combines rotation E and translation r into a single 6×6 matrix:
24//! ```text
25//!     | E   0 |
26//! X = |       |
27//!     | -E×r E |
28//! ```
29//!
30//! where:
31//! - **E** is the 3×3 rotation matrix
32//! - **r** is the translation vector (parent frame origin to child frame origin)
33//! - **E×r** is the cross-product matrix of the translated vector
34//!
35//! # Coordinate Frame Conventions
36//!
37//! - Transforms convert from **parent/reference frame** to **child/local frame**
38//! - Translation r is expressed in parent frame coordinates
39//! - Rotation E transforms vectors from parent to child frame
40//! - These conventions follow Featherstone's spatial algebra
41//!
42//! # Key Operations
43//!
44//! - **Motion vector transformation**: `v_child = X · v_parent`
45//! - **Force vector transformation**: `f_child = X · f_parent`
46//! - **Inertia transformation**: `I_child = X · I_parent · Xᵀ`
47//! - **Transform composition**: `X_total = X_child · X_parent`
48
49use std::ops::Mul;
50
51use nalgebra::Rotation3;
52
53use super::exts::Vec3Ext;
54use super::{
55    ArticulatedBodyInertia, Mat3, SpatialForceVector, SpatialMotionVector, UnitQuat, UnitVec3, Vec3,
56};
57use crate::exts::MatrixExt;
58use crate::{Real, SMatrix, VEC3_ZERO, unit_quat};
59
60/// A Plücker rotation representing a 3D rotation from reference to local frame.
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct PlukerRotation(Rotation3<Real>);
64
65impl PlukerRotation {
66    /// Create a Plücker rotation from an axis and angle that rotates from reference to local frame.
67    ///
68    /// # Coordinate Frame Convention
69    ///
70    /// This creates a rotation that transforms vectors from the parent/reference frame
71    /// to the child/local frame, following Featherstone's spatial algebra conventions.
72    #[inline]
73    pub fn from_axis_angle(axis: UnitVec3, angle: Real) -> Self {
74        // Note: we should do inverse here because the nalgebra rotation is defined to convert a
75        // vector from local to reference frame.
76        Self(Rotation3::from_axis_angle(&axis, angle).inverse())
77    }
78
79    /// Create a Plücker rotation from a unit quaternion.
80    ///
81    /// The quaternion should represent the rotation from reference to local frame
82    /// to be consistent with spatial algebra conventions.
83    #[inline]
84    pub fn from_quat(quat: UnitQuat) -> Self {
85        Self(Rotation3::from(quat))
86    }
87
88    /// Convert the Plücker rotation to a unit quaternion.
89    #[inline]
90    pub fn into_quat(self) -> UnitQuat {
91        self.0.into()
92    }
93
94    /// Convert the Plücker rotation to a 3x3 matrix.
95    #[inline]
96    pub fn into_matrix(self) -> Mat3 {
97        self.0.into_inner()
98    }
99
100    /// Create a identity [`PlukerRotation`].
101    #[inline]
102    pub fn identity() -> Self {
103        Self(Rotation3::identity())
104    }
105
106    /// Transpose of the rotation is identical to the inverse.
107    ///
108    /// Returns the transpose of the rotation matrix, which is equivalent to the inverse
109    /// for orthogonal rotation matrices.
110    #[inline]
111    #[must_use]
112    pub fn transpose(&self) -> Self {
113        Self(self.0.transpose())
114    }
115
116    /// Convert the rotation to a 3x3 matrix.
117    ///
118    /// Returns the underlying rotation matrix representation.
119    #[inline]
120    pub fn matrix(&self) -> Mat3 {
121        self.0.into_inner()
122    }
123
124    /// Transform a vector from local frame to reference frame.
125    ///
126    /// This is the inverse operation of multiplying a vector by the rotation.
127    #[inline]
128    pub fn inverse_transform_vector(&self, v: Vec3) -> Vec3 {
129        self.0.inverse_transform_vector(&v)
130    }
131
132    /// Transform a unit vector from local frame to reference frame.
133    ///
134    /// This is the inverse operation of multiplying a unit vector by the rotation.
135    #[inline]
136    pub fn inverse_transform_unitvec(&self, v: UnitVec3) -> UnitVec3 {
137        self.0.inverse_transform_unit_vector(&v)
138    }
139
140    /// Get the axis and angle of the rotation.
141    #[inline]
142    pub fn axis_angle(&self) -> Option<(UnitVec3, Real)> {
143        self.0.axis_angle().map(|(axis, angle)| (axis, -angle))
144    }
145
146    /// Create a Plücker rotation from a 3x3 matrix without validation.
147    ///
148    /// # Safety
149    ///
150    /// The input matrix must be a valid rotation matrix (orthogonal with determinant 1).
151    /// This function does not perform validation, so invalid input may cause undefined behavior.
152    #[inline]
153    pub fn from_matrix_unchecked(matrix: Mat3) -> Self {
154        Self(Rotation3::from_matrix_unchecked(matrix))
155    }
156
157    /// Get the rotation matrix as a slice.
158    ///
159    /// Returns the 3x3 rotation matrix as a contiguous slice of 9 elements in column-major order.
160    #[inline]
161    pub fn as_slice(&self) -> &[Real] {
162        self.0.matrix().as_slice()
163    }
164}
165
166impl Mul<Vec3> for PlukerRotation {
167    type Output = Vec3;
168
169    #[inline]
170    fn mul(self, rhs: Vec3) -> Vec3 {
171        self.0 * rhs
172    }
173}
174
175impl Mul<Self> for PlukerRotation {
176    type Output = Self;
177
178    #[inline]
179    fn mul(self, rhs: Self) -> Self {
180        Self(self.0 * rhs.0)
181    }
182}
183
184impl Mul<Mat3> for PlukerRotation {
185    type Output = Mat3;
186
187    #[inline]
188    fn mul(self, rhs: Mat3) -> Mat3 {
189        self.0 * rhs
190    }
191}
192
193/// A spatial transform using Plücker coordinates.
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195#[derive(Debug, Clone, Copy, PartialEq)]
196pub struct PluckerTransform {
197    /// The rotation component of the spatial transform (E).
198    ///
199    /// This is a 3×3 rotation matrix that transforms angular components
200    /// from the parent/reference frame to the child/local frame.
201    pub rotation: PlukerRotation,
202
203    /// The translation component of the spatial transform (r).
204    ///
205    /// This is a 3D vector representing the position of the child frame
206    /// origin relative to the parent frame origin, expressed in parent frame coordinates.
207    pub translation: Vec3,
208}
209
210impl Default for PluckerTransform {
211    #[inline]
212    fn default() -> Self {
213        Self {
214            rotation: PlukerRotation::identity(),
215            translation: Vec3::zeros(),
216        }
217    }
218}
219
220impl PluckerTransform {
221    /// Create an identity `PluckerTransform`.
222    ///
223    /// Returns a transform with no rotation or translation.
224    #[inline]
225    pub fn identity() -> Self {
226        Self {
227            rotation: PlukerRotation::identity(),
228            translation: VEC3_ZERO,
229        }
230    }
231
232    /// Create a plucker transform from a pose. The returned transform converts from parent to
233    /// local.
234    #[inline]
235    pub fn from_pose(pose: Pose) -> Self {
236        Self {
237            // Note: we should do inverse here because the pose rotation is defined to convert a
238            // vector from local to reference frame, but the plucker rotation is defined to convert
239            // a vector from reference to local frame.
240            rotation: PlukerRotation::from_quat(pose.rotation.inverse()),
241            translation: pose.translation,
242        }
243    }
244
245    /// Check if the transform contains any NaN values.
246    #[inline]
247    pub fn any_nan(&self) -> bool {
248        self.rotation.matrix().any_nan() || self.translation.any_nan()
249    }
250
251    /// Multiply this transform by another transform.
252    ///
253    /// Computes the composition of two transforms: result = self * rhs
254    /// where rhs is applied first, then self.
255    ///
256    /// The formula is: X1 * X2 = plx(E1E2, r2 + E2^T r1)
257    #[inline]
258    #[must_use]
259    pub fn mul_transform(&self, rhs: &PluckerTransform) -> PluckerTransform {
260        // X1 * X2 = plx(E1E2, r2 + E2^T r1)
261        PluckerTransform {
262            rotation: self.rotation * rhs.rotation,
263            translation: rhs.translation + rhs.rotation.transpose() * self.translation,
264        }
265    }
266
267    /// Compute the inverse of the transform.
268    ///
269    /// Returns a transform that undoes this transform when applied.
270    ///
271    /// The formula is: X^-1 = plx(E^T, -Er)
272    #[inline]
273    #[must_use]
274    pub fn inverse(&self) -> PluckerTransform {
275        // X^-1 = plx(E^T, -Er)
276        PluckerTransform {
277            rotation: self.rotation.transpose(),
278            translation: -(self.rotation * self.translation),
279        }
280    }
281
282    /// Transform a spatial motion vector from parent frame to child frame.
283    ///
284    /// Transforms a 6D motion vector containing angular and linear velocity components.
285    /// The formula is: X * v^ = mv(Ew, E(v - r x w))
286    ///
287    /// # Arguments
288    /// * `v` - Spatial motion vector in parent frame coordinates
289    ///
290    /// # Returns
291    /// Spatial motion vector in child frame coordinates
292    #[inline]
293    pub fn transform_motion_vec(&self, v: SpatialMotionVector) -> SpatialMotionVector {
294        // X * v^ = mv(Ew, E(v - r x w))
295        let w = v.top;
296        let v = v.bottom;
297
298        let w1 = self.rotation * w;
299        let v1 = self.rotation * (v - self.translation.cross(&w));
300
301        SpatialMotionVector::from_pair(w1, v1)
302    }
303
304    /// Transform a spatial motion vector from child frame to parent frame.
305    ///
306    /// This is the inverse operation of `transform_motion_vec`.
307    /// The formula is: X^-1 * v^ = mv(E^T w, E^T v + r x E^T w)
308    ///
309    /// # Arguments
310    /// * `v` - Spatial motion vector in child frame coordinates
311    ///
312    /// # Returns
313    /// Spatial motion vector in parent frame coordinates
314    #[inline]
315    pub fn inverse_transform_motion_vec(&self, v: SpatialMotionVector) -> SpatialMotionVector {
316        // X^-1 * v^ = mv(E^T w, E^T v + r x E^T w)
317        let w = v.top;
318        let v = v.bottom;
319
320        let e_transpose = self.rotation.transpose();
321
322        let w = e_transpose * w;
323        let v = e_transpose * v + self.translation.cross(&w);
324
325        SpatialMotionVector::from_pair(w, v)
326    }
327
328    /// Transform a spatial force vector from parent frame to child frame.
329    ///
330    /// Transforms a 6D force vector containing moment (torque) and force components.
331    /// The formula is: X * f = fv(E(n - r x f), Ef)
332    ///
333    /// # Arguments
334    /// * `f` - Spatial force vector in parent frame coordinates
335    ///
336    /// # Returns
337    /// Spatial force vector in child frame coordinates
338    #[inline]
339    pub fn transform_force_vec(&self, f: SpatialForceVector) -> SpatialForceVector {
340        // X * f = fv(E(n - r x f), Ef)
341
342        let n = f.top;
343        let f = f.bottom;
344
345        let e = self.rotation;
346        let r = self.translation;
347
348        let n = e * (n - r.cross(&f));
349        let f = e * f;
350
351        SpatialForceVector::from_pair(n, f)
352    }
353
354    /// Transform a spatial force vector from child frame to parent frame.
355    ///
356    /// This is the inverse operation of `transform_force_vec`.
357    /// The formula is: X^-1 * f = fv(E^T n + r x E^T f, E^T f)
358    ///
359    /// # Arguments
360    /// * `f` - Spatial force vector in child frame coordinates
361    ///
362    /// # Returns
363    /// Spatial force vector in parent frame coordinates
364    #[inline]
365    pub fn inverse_transform_force_vec(&self, f: SpatialForceVector) -> SpatialForceVector {
366        // X^-1 * f = fv(E^T n + r x E^T f, E^T f)
367
368        let n = f.top;
369        let f = f.bottom;
370
371        let e_transpose = self.rotation.transpose();
372
373        let f = e_transpose * f;
374        let n = e_transpose * n + self.translation.cross(&f);
375
376        SpatialForceVector::from_pair(n, f)
377    }
378
379    /// Transform a point from reference frame to local
380    #[inline]
381    pub fn transform_point(&self, point: Vec3) -> Vec3 {
382        let v = point - self.translation;
383        self.rotation * v
384    }
385
386    /// Transform a 3D vector from reference frame to local frame.
387    ///
388    /// Applies only the rotation component of the transform, ignoring translation.
389    /// This is appropriate for transforming directions and vectors that represent
390    // quantities like velocity or force rather than positions.
391    #[inline]
392    pub fn transform_vec3(&self, vec3: Vec3) -> Vec3 {
393        self.rotation * vec3
394    }
395
396    /// Transform a point from local frame to reference frame
397    #[inline]
398    pub fn inverse_transform_point(&self, point: Vec3) -> Vec3 {
399        let v = self.rotation.inverse_transform_vector(point);
400        v + self.translation
401    }
402
403    /// Transform a 3D vector from local frame to reference frame.
404    ///
405    /// Applies only the inverse rotation component of the transform, ignoring translation.
406    /// This is the inverse operation of `transform_vec3`.
407    #[inline]
408    pub fn inverse_transform_vec3(&self, vec3: Vec3) -> Vec3 {
409        self.rotation.inverse_transform_vector(vec3)
410    }
411
412    /// Transform a unit vector from local frame to reference frame.
413    ///
414    /// Applies only the inverse rotation component of the transform, ignoring translation.
415    /// The input vector is assumed to be normalized and will remain normalized.
416    #[inline]
417    pub fn inverse_transform_unitvec3(&self, vec3: UnitVec3) -> UnitVec3 {
418        self.rotation.inverse_transform_unitvec(vec3)
419    }
420
421    /// Transform an articulated body inertia from parent frame to child frame.
422    ///
423    /// Transforms a 6×6 articulated body inertia matrix using the similarity transform:
424    /// `I_child = X * I_parent * Xᵀ`
425    ///
426    /// The formula involves three components:
427    /// - Mass matrix: M = E * M * Eᵀ
428    /// - Coriolis matrix: H = E * (H - r * M) * Eᵀ
429    /// - Rotational inertia: I = E * (I - r×Hᵀ + (H - r×M)r×) * Eᵀ
430    ///
431    /// # Arguments
432    /// * `inertia` - Articulated body inertia in parent frame coordinates
433    ///
434    /// # Returns
435    /// Articulated body inertia in child frame coordinates
436    #[inline]
437    pub fn transform_articulated_inertia(
438        &self,
439        inertia: &ArticulatedBodyInertia,
440    ) -> ArticulatedBodyInertia {
441        let rotation = self.rotation.matrix();
442        let rotation_t = rotation.transpose();
443        let m = inertia.mass.mat3();
444        let r = self.translation.into_cross_matrix();
445        let i = inertia.top_left.mat3();
446        let h = inertia.h;
447
448        // M = E * M * E^T
449        let new_m = rotation * m * rotation_t;
450        // H = E * (H - r * M) * E^T
451        let new_h = rotation * (h - r * m) * rotation_t;
452        // E(I −r×HT +(H −r×M)r×)ET
453        let new_inertia = rotation * (i - r * h.transpose() + (h - r * m) * r) * rotation_t;
454        ArticulatedBodyInertia {
455            top_left: new_inertia.into(),
456            h: new_h,
457            mass: new_m.into(),
458        }
459    }
460
461    /// Convert the Plücker transform to a 6×6 matrix representation.
462    ///
463    /// Returns the full 6×6 spatial transform matrix:
464    /// ```text
465    ///     | E   0 |
466    /// X = |       |
467    ///     | -E×r E |
468    /// ```
469    ///
470    /// where E is the 3×3 rotation matrix and r is the translation vector.
471    /// This matrix can be used for direct multiplication with 6D vectors.
472    #[inline]
473    pub fn matrix(&self) -> SMatrix<6, 6> {
474        let mut result = SMatrix::<6, 6>::zeros();
475
476        let rotation = self.rotation.matrix();
477
478        let bottom_left = -(rotation * self.translation.into_cross_matrix());
479
480        // top left
481        result.fixed_view_mut::<3, 3>(0, 0).copy_from(&rotation);
482        // right bottom
483        result.fixed_view_mut::<3, 3>(3, 3).copy_from(&rotation);
484
485        // body left
486        result.fixed_view_mut::<3, 3>(3, 0).copy_from(&bottom_left);
487
488        result
489    }
490
491    /// Convert the Plücker transform to its dual matrix representation.
492    ///
493    /// Returns the dual transform matrix where the translation component appears
494    /// in the top-right block instead of the bottom-left block:
495    /// ```text
496    ///     | E  -E×r |
497    /// X* = |        |
498    ///     | 0    E  |
499    /// ```
500    ///
501    /// The dual matrix is the transpose inverse of the regular matrix.
502    /// This representation is useful for transforming certain spatial quantities.
503    #[inline]
504    pub fn dual_matrix(&self) -> SMatrix<6, 6> {
505        let mut result = SMatrix::<6, 6>::zeros();
506
507        let rotation = self.rotation.matrix();
508        // top left
509        result.fixed_view_mut::<3, 3>(0, 0).copy_from(&rotation);
510        // right bottom
511        result.fixed_view_mut::<3, 3>(3, 3).copy_from(&rotation);
512
513        let top_right = -rotation * self.translation.into_cross_matrix();
514
515        result.fixed_view_mut::<3, 3>(0, 3).copy_from(&top_right);
516
517        result
518    }
519
520    /// Check whether the transform is close to identity.
521    /// Check whether the transform is close to identity.
522    ///
523    /// Returns true if both the rotation angle and translation magnitude
524    /// are below the given epsilon threshold.
525    ///
526    /// # Arguments
527    /// * `epsilon` - Maximum allowed deviation from identity
528    ///
529    /// # Returns
530    /// True if the transform is approximately identity
531    #[inline]
532    pub fn is_near_identity(&self, epsilon: Real) -> bool {
533        self.rotation.0.angle() < epsilon && self.translation.iter().all(|x| x.abs() < epsilon)
534    }
535}
536
537impl Mul<Self> for PluckerTransform {
538    type Output = Self;
539
540    #[inline]
541    fn mul(self, rhs: Self) -> Self {
542        self.mul_transform(&rhs)
543    }
544}
545
546impl Mul<SpatialMotionVector> for PluckerTransform {
547    type Output = SpatialMotionVector;
548
549    #[inline]
550    fn mul(self, rhs: SpatialMotionVector) -> SpatialMotionVector {
551        self.transform_motion_vec(rhs)
552    }
553}
554
555impl Mul<SpatialForceVector> for PluckerTransform {
556    type Output = SpatialForceVector;
557
558    #[inline]
559    fn mul(self, rhs: SpatialForceVector) -> SpatialForceVector {
560        self.transform_force_vec(rhs)
561    }
562}
563
564impl Mul<&ArticulatedBodyInertia> for PluckerTransform {
565    type Output = ArticulatedBodyInertia;
566
567    #[inline]
568    fn mul(self, rhs: &ArticulatedBodyInertia) -> ArticulatedBodyInertia {
569        self.transform_articulated_inertia(rhs)
570    }
571}
572
573impl PluckerTransform {
574    /// Convert the Plücker transform to a pose.
575    ///
576    /// Returns a pose that represents the same transformation but with
577    /// the rotation component inverted to match pose conventions.
578    /// This is because poses typically represent the transformation
579    /// from local to world space, while Plücker transforms represent
580    /// the transformation from parent to child frame.
581    #[inline]
582    pub fn pose(&self) -> Pose {
583        let quat: UnitQuat = self.rotation.0.into();
584        Pose {
585            rotation: quat.inverse(),
586            translation: self.translation,
587        }
588    }
589}
590
591/// A 6DOF pose represented by rotation and translation.
592///
593/// This represents a rigid body transformation in 3D space with a quaternion
594/// for rotation and a 3D vector for translation. The pose transforms points
595/// from local/object coordinates to world/reference coordinates.
596#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
597#[derive(Debug, Default, Clone, Copy)]
598pub struct Pose {
599    /// Rotation component as a unit quaternion
600    pub rotation: UnitQuat,
601    /// Translation component as a 3D vector
602    pub translation: Vec3,
603}
604
605impl Pose {
606    /// Transform a point from local/object coordinates to world/reference coordinates.
607    ///
608    /// Applies both rotation and translation to transform a point position.
609    /// The formula is: `world_point = rotation * local_point + translation`
610    ///
611    /// # Arguments
612    /// * `local` - Point in local coordinates
613    ///
614    /// # Returns
615    /// Point in world/reference coordinates
616    #[inline]
617    pub fn transform_point(&self, local: Vec3) -> Vec3 {
618        self.rotation * local + self.translation
619    }
620
621    /// Transform a vector from local/object coordinates to world/reference coordinates.
622    ///
623    /// Applies only the rotation component, ignoring translation.
624    /// This is appropriate for transforming directions, velocities, forces,
625    /// or any vector quantity that represents a direction rather than a position.
626    ///
627    /// # Arguments
628    /// * `local` - Vector in local coordinates
629    ///
630    /// # Returns
631    /// Vector in world/reference coordinates
632    #[inline]
633    pub fn transform_vec(&self, local: Vec3) -> Vec3 {
634        self.rotation * local
635    }
636
637    /// Compute the inverse of the pose.
638    ///
639    /// Returns a pose that transforms from world/reference coordinates back to
640    /// local/object coordinates. The formula is: X⁻¹ = [R⁻¹, -R⁻¹·t]
641    ///
642    /// # Returns
643    /// Inverse pose that undoes this transformation
644    #[inline]
645    #[must_use]
646    pub fn inverse(&self) -> Pose {
647        let r = self.rotation.inverse();
648        let t = -self.translation;
649        Pose {
650            rotation: r,
651            translation: r * t,
652        }
653    }
654
655    /// Check whether the pose is close to identity.
656    ///
657    /// Returns true if both the rotation angle and translation magnitude
658    /// are below the given epsilon threshold.
659    ///
660    /// # Arguments
661    /// * `epsilon` - Maximum allowed deviation from identity
662    ///
663    /// # Returns
664    /// True if the pose is approximately identity
665    #[inline]
666    pub fn is_identity(&self, epsilon: Real) -> bool {
667        self.rotation.angle() < epsilon && self.translation.iter().all(|x| x.abs() < epsilon)
668    }
669
670    /// Convert the pose to a `[Real; 7]` array with the format `[x, y, z, i, j, k, w]`.
671    #[inline]
672    pub fn into_array(self) -> [Real; 7] {
673        let p = self.translation;
674        let r = self.rotation;
675        [p.x, p.y, p.z, r.i, r.j, r.k, r.w]
676    }
677
678    /// Create a pose from a `[Real; 7]` array with the format `[x, y, z, i, j, k, w]`.
679    #[inline]
680    pub fn from_array(arr: [Real; 7]) -> Self {
681        Pose {
682            translation: Vec3::new(arr[0], arr[1], arr[2]),
683            rotation: unit_quat(arr[3], arr[4], arr[5], arr[6]),
684        }
685    }
686
687    /// Create a pose from a slice with the format `[x, y, z, i, j, k, w]`.
688    ///
689    /// # Panics
690    ///
691    /// Panics if the slice length is not 7.
692    #[inline]
693    pub fn from_slice(arr: &[Real]) -> Self {
694        assert_eq!(arr.len(), 7);
695        Pose {
696            translation: Vec3::new(arr[0], arr[1], arr[2]),
697            rotation: unit_quat(arr[3], arr[4], arr[5], arr[6]),
698        }
699    }
700}
701
702impl From<Pose> for PluckerTransform {
703    #[inline]
704    fn from(pose: Pose) -> Self {
705        PluckerTransform::from_pose(pose)
706    }
707}
708
709impl Mul<Pose> for Pose {
710    type Output = Pose;
711
712    #[inline]
713    fn mul(self, rhs: Pose) -> Pose {
714        let rotation = self.rotation * rhs.rotation;
715        let translation = self.rotation * rhs.translation + self.translation;
716        Pose {
717            rotation,
718            translation,
719        }
720    }
721}
722
723#[cfg(feature = "approx")]
724mod approx_eq {
725
726    use crate::Real;
727
728    impl approx::AbsDiffEq for super::PlukerRotation {
729        type Epsilon = Real;
730
731        #[inline]
732        fn default_epsilon() -> Self::Epsilon {
733            Real::EPSILON
734        }
735
736        #[inline]
737        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
738            self.0.abs_diff_eq(&other.0, epsilon)
739        }
740    }
741
742    impl approx::RelativeEq for super::PlukerRotation {
743        #[inline]
744        fn default_max_relative() -> Self::Epsilon {
745            Real::default_max_relative()
746        }
747
748        #[inline]
749        fn relative_eq(
750            &self,
751            other: &Self,
752            epsilon: Self::Epsilon,
753            max_relative: Self::Epsilon,
754        ) -> bool {
755            self.0.relative_eq(&other.0, epsilon, max_relative)
756        }
757    }
758
759    impl approx::AbsDiffEq for super::PluckerTransform {
760        type Epsilon = Real;
761
762        #[inline]
763        fn default_epsilon() -> Self::Epsilon {
764            Real::EPSILON
765        }
766
767        #[inline]
768        fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
769            self.rotation.abs_diff_eq(&other.rotation, epsilon)
770                & self.translation.abs_diff_eq(&other.translation, epsilon)
771        }
772    }
773
774    impl approx::RelativeEq for super::PluckerTransform {
775        #[inline]
776        fn default_max_relative() -> Self::Epsilon {
777            Real::default_max_relative()
778        }
779
780        #[inline]
781        fn relative_eq(
782            &self,
783            other: &Self,
784            epsilon: Self::Epsilon,
785            max_relative: Self::Epsilon,
786        ) -> bool {
787            self.rotation
788                .relative_eq(&other.rotation, epsilon, max_relative)
789                & self
790                    .translation
791                    .relative_eq(&other.translation, epsilon, max_relative)
792        }
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use approx::assert_relative_eq;
799
800    use super::PluckerTransform;
801    use crate::exts::Vec3Ext;
802    use crate::transform::PlukerRotation;
803    use crate::{
804        ArticulatedBodyInertia, Mat3, Real, RigidBodyInertia, SpatialMotionVector, SymmetricMat3,
805        UnitQuat, UnitVec3, Vec3, unit_vec3, vec3,
806    };
807
808    #[test]
809    fn test_transform_motion_vec() {
810        let transform = PluckerTransform {
811            rotation: PlukerRotation::from_axis_angle(Vec3::z_axis(), Real::to_radians(90.0)),
812            translation: vec3(1., 2., 3.),
813        };
814
815        let v0 = SpatialMotionVector::from_array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
816        let v1 = transform * v0;
817
818        assert_relative_eq!(v1.top, transform.rotation * v0.top);
819        assert_relative_eq!(
820            v1.bottom,
821            transform.rotation * (v0.bottom + v0.top.cross(&transform.translation))
822        );
823    }
824
825    #[test]
826    fn test_transform_mul() {
827        let a_to_b = PluckerTransform {
828            rotation: PlukerRotation::from_axis_angle(Vec3::z_axis(), Real::to_radians(90.0)),
829            translation: Vec3::zeros(),
830        };
831
832        let b_to_c = PluckerTransform {
833            rotation: PlukerRotation::identity(),
834            translation: vec3(1.0, 0.0, 0.0),
835        };
836
837        let a_to_c = b_to_c * a_to_b;
838
839        assert_relative_eq!(a_to_c.translation, vec3(0.0, 1.0, 0.0));
840    }
841
842    #[test]
843    fn test_transform_inverse() {
844        let transform = PluckerTransform {
845            rotation: PlukerRotation::from_axis_angle(Vec3::z_axis(), Real::to_radians(90.0)),
846            translation: vec3(1.0, 0.0, 0.0),
847        };
848
849        let transform_inv = transform.inverse();
850
851        let result = transform_inv * transform;
852
853        assert_relative_eq!(result.rotation.matrix(), Mat3::identity());
854        assert_relative_eq!(result.translation, Vec3::zeros());
855    }
856
857    #[test]
858    fn test_transform_to_matrix() {
859        let transform = PluckerTransform {
860            rotation: PlukerRotation::from_axis_angle(
861                UnitVec3::new_normalize(vec3(1.0, 2.0, 3.0)),
862                Real::to_radians(45.0),
863            ),
864            translation: vec3(1.0, 2.0, 3.0),
865        };
866
867        let matrix = transform.matrix();
868
869        let e = transform.rotation.matrix();
870        let rx = transform.translation.into_cross_matrix();
871
872        {
873            let top_left = matrix.fixed_view::<3, 3>(0, 0);
874            let bottom_right = matrix.fixed_view::<3, 3>(3, 3);
875            assert_relative_eq!(bottom_right, top_left);
876
877            let top_left: Vec<_> = top_left.iter().collect();
878            let e_data = e.iter().collect::<Vec<_>>();
879            assert_eq!(top_left, e_data);
880        }
881
882        {
883            let top_right = matrix.fixed_view::<3, 3>(0, 3);
884            top_right.iter().for_each(|&x| assert_relative_eq!(x, 0.0));
885        }
886
887        {
888            let bottom_left = matrix.fixed_view::<3, 3>(3, 0);
889
890            let expected = -e * rx;
891
892            bottom_left.iter().zip(expected.iter()).for_each(|(a, b)| {
893                assert_relative_eq!(a, b);
894            });
895        }
896
897        let motion_vec = SpatialMotionVector::from_pair(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0));
898
899        assert_relative_eq!(
900            (transform * motion_vec).into_vec6(),
901            matrix * motion_vec.into_vec6(),
902            epsilon = 1e-6
903        );
904    }
905
906    #[test]
907    fn test_transform_to_dual_matrix() {
908        let transform = PluckerTransform {
909            rotation: PlukerRotation::from_axis_angle(
910                UnitVec3::new_normalize(vec3(1.0, 2.0, 3.0)),
911                Real::to_radians(90.0),
912            ),
913            translation: vec3(1.0, 2.0, 3.0),
914        };
915
916        let matrix = transform.matrix();
917        let dual_matrix = transform.dual_matrix();
918
919        assert_relative_eq!(
920            matrix.transpose().try_inverse().unwrap(),
921            dual_matrix,
922            epsilon = 1e-6
923        );
924    }
925
926    #[test]
927    fn test_transform_articulated_inertia() {
928        let transform = PluckerTransform {
929            rotation: PlukerRotation::from_axis_angle(Vec3::z_axis(), Real::to_radians(90.0)),
930            translation: vec3(1.0, 0.0, 0.0),
931        };
932
933        let inertia: ArticulatedBodyInertia =
934            RigidBodyInertia::new(1.0, vec3(1.0, 2.0, 3.0), SymmetricMat3::ONE).into();
935
936        let new_inertia = transform.transform_articulated_inertia(&inertia);
937
938        let dual_x = transform.dual_matrix();
939        let inverse_x = transform.matrix().try_inverse().unwrap();
940
941        let expected_inertia = dual_x * inertia.matrix() * inverse_x;
942
943        assert_relative_eq!(new_inertia.matrix(), expected_inertia, epsilon = 1e-6);
944
945        assert_relative_eq!(
946            new_inertia.mass.mat3(),
947            transform.rotation.matrix()
948                * inertia.mass.mat3()
949                * transform.rotation.matrix().transpose(),
950            epsilon = 1e-6
951        );
952    }
953
954    #[test]
955    fn test_pose_inverse() {
956        let pose = super::Pose {
957            rotation: UnitQuat::from_axis_angle(&unit_vec3(1., 2., 3.), Real::to_radians(36.0)),
958            translation: vec3(1.0, 2.0, 3.0),
959        };
960
961        let inv = pose.inverse();
962
963        let result = inv * pose;
964
965        assert_relative_eq!(result.rotation, UnitQuat::identity());
966        assert_relative_eq!(result.translation, Vec3::zeros());
967    }
968}