@@ -94,9 +94,9 @@ use crate::{engineering, systems::FrdLike};
9494pub struct Rotation < From , To > {
9595 /// This is _actually_ the rotation from `To` into `From`, because that's a much more natural
9696 /// constructor. This means that what we store here is equivalent to a rotation matrix whose
97- /// rows are `From` and columns are `To` (rather than the other way around). If we want convert
98- /// a vector `x` from `From` to `To`, we can't do a straight matrix multiplication of the
99- /// (equivalent) rotation matrix in `inner` with `x`, because that would yield (From x To) *
97+ /// rows are `From` and columns are `To` (rather than the other way around). If we want to
98+ /// convert a vector `x` from `From` to `To`, we can't do a straight matrix multiplication of
99+ /// the (equivalent) rotation matrix in `inner` with `x`, because that would yield (From x To) *
100100 /// (From x 1), which doesn't math. Instead, we need to matrix multiply the _inverse_ (which is
101101 /// transpose for a rotation matrix) of `inner` with `x`, which does work:
102102 ///
@@ -625,6 +625,92 @@ impl<From, To> Rotation<From, To> {
625625 )
626626 }
627627
628+ /// Constructs a rotation from the components of a [versor] / unit quaternion.
629+ ///
630+ /// The components are given as `w` (the scalar/real part) and `[i, j, k]` (the
631+ /// vector/imaginary part).
632+ ///
633+ /// The quaternion should represent the rotation that transforms points in `From` into points
634+ /// in `To`. This is the intuitive "forward" direction matching the type parameters. The
635+ /// quaternion is normalized internally, so the input does not need to be exactly unit length.
636+ ///
637+ /// # Examples
638+ ///
639+ /// ```rust
640+ /// use approx::assert_relative_eq;
641+ /// use sguaba::{system, math::Rotation};
642+ /// use uom::si::{f64::Angle, angle::degree};
643+ ///
644+ /// system!(struct PlaneNed using NED);
645+ /// system!(struct PlaneFrd using FRD);
646+ ///
647+ /// // construct from Tait-Bryan angles
648+ /// let from_angles = unsafe {
649+ /// Rotation::<PlaneNed, PlaneFrd>::tait_bryan_builder()
650+ /// .yaw(Angle::new::<degree>(90.0))
651+ /// .pitch(Angle::new::<degree>(45.0))
652+ /// .roll(Angle::new::<degree>(5.0))
653+ /// .build()
654+ /// };
655+ ///
656+ /// // extract quaternion and reconstruct
657+ /// let (w, i, j, k) = from_angles.to_quaternion();
658+ /// // SAFETY: We know that this quaternion is valid, as we got it from the rotation above.
659+ /// let from_quat = unsafe {
660+ /// Rotation::<PlaneNed, PlaneFrd>::from_quaternion(w, i, j, k)
661+ /// };
662+ ///
663+ /// assert_relative_eq!(from_angles, from_quat);
664+ /// ```
665+ ///
666+ /// # Safety
667+ ///
668+ /// The caller must uphold two invariants:
669+ ///
670+ /// 1. The provided quaternion correctly rotates from `From` to `To` (and crucially, that no
671+ /// translation is needed). If this is _not_ the correct transform, then this allows moving
672+ /// values between different coordinate system types without adjusting the values correctly,
673+ /// leading to a defeat of their type safety.
674+ ///
675+ /// 2. The quaternion must be non-zero (ie, not `(0, [0, 0, 0])`).
676+ /// A zero quaternion has no meaningful rotation associated with it.
677+ /// Non-unit quaternions (which don't represent rotations or orientations) are normalized
678+ /// internally and may change their meaning.
679+ ///
680+ /// [versor]: https://en.wikipedia.org/wiki/Versor
681+ #[ doc( alias = "from_versor" ) ]
682+ #[ must_use]
683+ pub unsafe fn from_quaternion ( w : f64 , i : f64 , j : f64 , k : f64 ) -> Self {
684+ debug_assert_ne ! (
685+ [ w, i, j, k] ,
686+ [ 0.0 , 0.0 , 0.0 , 0.0 ] ,
687+ "Quaternion must be non-zero"
688+ ) ;
689+ let from_to = UnitQuaternion :: new_normalize ( nalgebra:: Quaternion :: new ( w, i, j, k) ) ;
690+ Self {
691+ inner : from_to. inverse ( ) ,
692+ from : PhantomData :: < From > ,
693+ to : PhantomData :: < To > ,
694+ }
695+ }
696+
697+ /// Returns the components of the [unit quaternion] that rotates from `From` to `To` as
698+ /// `(w, [i, j, k])` where `w` is the scalar/real part and `[i, j, k]` is the vector / imaginary
699+ /// part.
700+ ///
701+ /// The returned quaternion represents the "forward" direction of this rotation, (ie, rotation
702+ /// required to go from `From` to `To`), matching the type parameters and the convention of
703+ /// [`Rotation::from_quaternion`].
704+ ///
705+ /// [unit quaternion]: https://en.wikipedia.org/wiki/Versor
706+ #[ doc( alias = "to_versor" ) ]
707+ #[ must_use]
708+ pub fn to_quaternion ( & self ) -> ( f64 , f64 , f64 , f64 ) {
709+ let from_to = self . inner . inverse ( ) ;
710+ let q = from_to. quaternion ( ) ;
711+ ( q. w , q. i , q. j , q. k )
712+ }
713+
628714 /// Returns the [Euler angles] describing this rotation.
629715 ///
630716 /// The use of this method is discouraged as Eulers angles are both hard to work with and do
@@ -2871,4 +2957,108 @@ mod tests {
28712957 assert ! ( debug_str. contains( "pitch" ) ) ;
28722958 assert ! ( debug_str. contains( "roll" ) ) ;
28732959 }
2960+
2961+ #[ test]
2962+ fn rotation_quaternion_identity ( ) {
2963+ let identity =
2964+ unsafe { Rotation :: < PlaneNed , PlaneFrd > :: from_quaternion ( 1.0 , 0.0 , 0.0 , 0.0 ) } ;
2965+ let expected = unsafe { Rotation :: < PlaneNed , PlaneFrd > :: identity ( ) } ;
2966+ assert_relative_eq ! ( identity, expected) ;
2967+ }
2968+
2969+ #[ test]
2970+ fn rotation_quaternion_normalization ( ) {
2971+ let from_unit =
2972+ unsafe { Rotation :: < PlaneNed , PlaneFrd > :: from_quaternion ( 1.0 , 0.0 , 0.0 , 0.0 ) } ;
2973+ let from_scaled =
2974+ unsafe { Rotation :: < PlaneNed , PlaneFrd > :: from_quaternion ( 5.0 , 0.0 , 0.0 , 0.0 ) } ;
2975+ assert_relative_eq ! ( from_unit, from_scaled) ;
2976+ }
2977+
2978+ #[ test]
2979+ fn rotation_quaternion_transform_equivalence ( ) {
2980+ let yaw = d ( 90. ) ;
2981+ let pitch = d ( 45. ) ;
2982+ let roll = d ( 5. ) ;
2983+
2984+ let from_angles = unsafe {
2985+ Rotation :: < PlaneNed , PlaneFrd > :: tait_bryan_builder ( )
2986+ . yaw ( yaw)
2987+ . pitch ( pitch)
2988+ . roll ( roll)
2989+ . build ( )
2990+ } ;
2991+
2992+ let ( w, i, j, k) = from_angles. to_quaternion ( ) ;
2993+ let from_quat = unsafe { Rotation :: < PlaneNed , PlaneFrd > :: from_quaternion ( w, i, j, k) } ;
2994+
2995+ let point = coordinate ! ( n = m( 100. ) , e = m( 50. ) , d = m( 25. ) ; in PlaneNed ) ;
2996+
2997+ assert_relative_eq ! ( from_angles. transform( point) , from_quat. transform( point) , ) ;
2998+ }
2999+
3000+ #[ test]
3001+ fn manual_quaternion_construction_compound_rotate ( ) {
3002+ // Create a rotation from the unit quaternion (1/2, 1/2, 1/2, 1/2), which cyclically maps
3003+ // the (1,0,0), (0,1,0), (0,0,1) vectors to (0,1,0), (0,0,1), and (1,0,0), respectively.
3004+ let rotation = unsafe { Rotation :: < Ecef , Ecef > :: from_quaternion ( 0.5 , 0.5 , 0.5 , 0.5 ) } ;
3005+
3006+ let a = coordinate ! ( x = m( 1.0 ) , y = m( 0.0 ) , z = m( 0.0 ) ; in Ecef ) ;
3007+ let b = coordinate ! ( x = m( 0.0 ) , y = m( 1.0 ) , z = m( 0.0 ) ; in Ecef ) ;
3008+ let c = coordinate ! ( x = m( 0.0 ) , y = m( 0.0 ) , z = m( 1.0 ) ; in Ecef ) ;
3009+
3010+ let a_rot = rotation. transform ( a) ;
3011+ let b_rot = rotation. transform ( b) ;
3012+ let c_rot = rotation. transform ( c) ;
3013+
3014+ assert_relative_eq ! ( a_rot, b) ;
3015+ assert_relative_eq ! ( b_rot, c) ;
3016+ assert_relative_eq ! ( c_rot, a) ;
3017+ }
3018+
3019+ #[ rstest]
3020+ #[ case( ( 1.0 , 0.0 , 0.0 , 0.0 ) , ( d( 0.0 ) , d( 0.0 ) , d( 0.0 ) ) ) ]
3021+ #[ case( ( 0.0 , 1.0 , 0.0 , 0.0 ) , ( d( 0.0 ) , d( 0.0 ) , d( 180.0 ) ) ) ]
3022+ #[ case( ( 0.0 , 0.0 , 1.0 , 0.0 ) , ( d( 0.0 ) , d( 180.0 ) , d( 0.0 ) ) ) ]
3023+ #[ case( ( 0.0 , 0.0 , 0.0 , 1.0 ) , ( d( 180.0 ) , d( 0.0 ) , d( 0.0 ) ) ) ]
3024+ #[ case( ( std:: f64 :: consts:: FRAC_1_SQRT_2 , std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , 0.0 ) , ( d( 0.0 ) , d( 0.0 ) , d( 90.0 ) ) ) ]
3025+ #[ case( ( std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 ) , ( d( 0.0 ) , d( 90.0 ) , d( 0.0 ) ) ) ]
3026+ #[ case( ( std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , 0.0 , std:: f64 :: consts:: FRAC_1_SQRT_2 ) , ( d( 90.0 ) , d( 0.0 ) , d( 0.0 ) ) ) ]
3027+ #[ case( ( -std:: f64 :: consts:: FRAC_1_SQRT_2 , std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , 0.0 ) , ( d( 0.0 ) , d( 0.0 ) , d( -90.0 ) ) ) ]
3028+ #[ case( ( -std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 ) , ( d( 0.0 ) , d( -90.0 ) , d( 0.0 ) ) ) ]
3029+ #[ case( ( -std:: f64 :: consts:: FRAC_1_SQRT_2 , 0.0 , 0.0 , std:: f64 :: consts:: FRAC_1_SQRT_2 ) , ( d( -90.0 ) , d( 0.0 ) , d( 0.0 ) ) ) ]
3030+ /// Checks that the parameters are handled in the correct order.
3031+ ///
3032+ /// We're rotating in a right-handed cartesian coordinate system.
3033+ /// Tests 2-4 rotate by 180° around the x, y, and z axes.
3034+ /// Tests 5-7 rotate by 90° around these axes, tests 8-10 by -90° around these axes.
3035+ ///
3036+ /// We provide the quaternions and the expected rotations as Tait-Bryan angles (in
3037+ /// yaw-pitch-roll order).
3038+ /// To verify that the rotations are indeed correct we map the three basis vectors (1, 0, 0),
3039+ /// (0, 1, 0), and (0, 0, 1) and having them mapped by the quaternion and the equivalent
3040+ /// rotation given by Tait-Bryan angles.
3041+ fn manual_quaternion_construction (
3042+ #[ case] q : ( f64 , f64 , f64 , f64 ) ,
3043+ #[ case] ypr : ( Angle , Angle , Angle ) ,
3044+ ) {
3045+ let q_rotation = unsafe { Rotation :: < Ecef , Ecef > :: from_quaternion ( q. 0 , q. 1 , q. 2 , q. 3 ) } ;
3046+ // Negate the angles, as the rotation is a rotation _of_ the transformed vector _in_ the
3047+ // rotated system, see the `from_tait_bryan_angles` documentation for details.
3048+ let ypr_rotation = unsafe {
3049+ Rotation :: < Ecef , Ecef > :: tait_bryan_builder ( )
3050+ . yaw ( -ypr. 0 )
3051+ . pitch ( -ypr. 1 )
3052+ . roll ( -ypr. 2 )
3053+ . build ( )
3054+ } ;
3055+
3056+ let x = coordinate ! ( x = m( 1.0 ) , y = m( 0.0 ) , z = m( 0.0 ) ; in Ecef ) ;
3057+ let y = coordinate ! ( x = m( 0.0 ) , y = m( 1.0 ) , z = m( 0.0 ) ; in Ecef ) ;
3058+ let z = coordinate ! ( x = m( 0.0 ) , y = m( 0.0 ) , z = m( 1.0 ) ; in Ecef ) ;
3059+
3060+ assert_relative_eq ! ( q_rotation. transform( x) , ypr_rotation. transform( x) ) ;
3061+ assert_relative_eq ! ( q_rotation. transform( y) , ypr_rotation. transform( y) ) ;
3062+ assert_relative_eq ! ( q_rotation. transform( z) , ypr_rotation. transform( z) ) ;
3063+ }
28743064}
0 commit comments