use crate::approxeq::ApproxEq;
use crate::trig::Trig;
use crate::{point2, point3, vec3, Angle, Point2D, Point3D, Vector2D, Vector3D};
use crate::{Transform2D, Transform3D, UnknownUnit};
use core::cmp::{Eq, PartialEq};
use core::fmt;
use core::hash::Hash;
use core::marker::PhantomData;
use core::ops::{Add, Mul, Neg, Sub};
#[cfg(feature = "bytemuck")]
use bytemuck::{Pod, Zeroable};
use num_traits::real::Real;
use num_traits::{NumCast, One, Zero};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[repr(C)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "T: serde::Serialize",
deserialize = "T: serde::Deserialize<'de>"
))
)]
pub struct Rotation2D<T, Src, Dst> {
pub angle: T,
#[doc(hidden)]
pub _unit: PhantomData<(Src, Dst)>,
}
impl<T: Copy, Src, Dst> Copy for Rotation2D<T, Src, Dst> {}
impl<T: Clone, Src, Dst> Clone for Rotation2D<T, Src, Dst> {
fn clone(&self) -> Self {
Rotation2D {
angle: self.angle.clone(),
_unit: PhantomData,
}
}
}
impl<T, Src, Dst> Eq for Rotation2D<T, Src, Dst> where T: Eq {}
impl<T, Src, Dst> PartialEq for Rotation2D<T, Src, Dst>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.angle == other.angle
}
}
impl<T, Src, Dst> Hash for Rotation2D<T, Src, Dst>
where
T: Hash,
{
fn hash<H: core::hash::Hasher>(&self, h: &mut H) {
self.angle.hash(h);
}
}
#[cfg(feature = "arbitrary")]
impl<'a, T, Src, Dst> arbitrary::Arbitrary<'a> for Rotation2D<T, Src, Dst>
where
T: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Rotation2D::new(arbitrary::Arbitrary::arbitrary(u)?))
}
}
#[cfg(feature = "bytemuck")]
unsafe impl<T: Zeroable, Src, Dst> Zeroable for Rotation2D<T, Src, Dst> {}
#[cfg(feature = "bytemuck")]
unsafe impl<T: Pod, Src: 'static, Dst: 'static> Pod for Rotation2D<T, Src, Dst> {}
impl<T, Src, Dst> Rotation2D<T, Src, Dst> {
#[inline]
pub fn new(angle: Angle<T>) -> Self {
Rotation2D {
angle: angle.radians,
_unit: PhantomData,
}
}
pub fn radians(angle: T) -> Self {
Self::new(Angle::radians(angle))
}
#[inline]
pub fn identity() -> Self
where
T: Zero,
{
Self::radians(T::zero())
}
}
impl<T: Copy, Src, Dst> Rotation2D<T, Src, Dst> {
#[inline]
pub fn cast_unit<Src2, Dst2>(&self) -> Rotation2D<T, Src2, Dst2> {
Rotation2D {
angle: self.angle,
_unit: PhantomData,
}
}
#[inline]
pub fn to_untyped(&self) -> Rotation2D<T, UnknownUnit, UnknownUnit> {
self.cast_unit()
}
#[inline]
pub fn from_untyped(r: &Rotation2D<T, UnknownUnit, UnknownUnit>) -> Self {
r.cast_unit()
}
}
impl<T, Src, Dst> Rotation2D<T, Src, Dst>
where
T: Copy,
{
pub fn get_angle(&self) -> Angle<T> {
Angle::radians(self.angle)
}
}
impl<T: Real, Src, Dst> Rotation2D<T, Src, Dst> {
#[inline]
pub fn to_3d(&self) -> Rotation3D<T, Src, Dst> {
Rotation3D::around_z(self.get_angle())
}
#[inline]
pub fn inverse(&self) -> Rotation2D<T, Dst, Src> {
Rotation2D::radians(-self.angle)
}
#[inline]
pub fn then<NewSrc>(&self, other: &Rotation2D<T, NewSrc, Src>) -> Rotation2D<T, NewSrc, Dst> {
Rotation2D::radians(self.angle + other.angle)
}
#[inline]
pub fn transform_point(&self, point: Point2D<T, Src>) -> Point2D<T, Dst> {
let (sin, cos) = Real::sin_cos(self.angle);
point2(point.x * cos - point.y * sin, point.y * cos + point.x * sin)
}
#[inline]
pub fn transform_vector(&self, vector: Vector2D<T, Src>) -> Vector2D<T, Dst> {
self.transform_point(vector.to_point()).to_vector()
}
}
impl<T, Src, Dst> Rotation2D<T, Src, Dst>
where
T: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Zero + Trig,
{
#[inline]
pub fn to_transform(&self) -> Transform2D<T, Src, Dst> {
Transform2D::rotation(self.get_angle())
}
}
impl<T: fmt::Debug, Src, Dst> fmt::Debug for Rotation2D<T, Src, Dst> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Rotation({:?} rad)", self.angle)
}
}
impl<T, Src, Dst> ApproxEq<T> for Rotation2D<T, Src, Dst>
where
T: Copy + Neg<Output = T> + ApproxEq<T>,
{
fn approx_epsilon() -> T {
T::approx_epsilon()
}
fn approx_eq_eps(&self, other: &Self, eps: &T) -> bool {
self.angle.approx_eq_eps(&other.angle, eps)
}
}
#[repr(C)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "T: serde::Serialize",
deserialize = "T: serde::Deserialize<'de>"
))
)]
pub struct Rotation3D<T, Src, Dst> {
pub i: T,
pub j: T,
pub k: T,
pub r: T,
#[doc(hidden)]
pub _unit: PhantomData<(Src, Dst)>,
}
impl<T: Copy, Src, Dst> Copy for Rotation3D<T, Src, Dst> {}
impl<T: Clone, Src, Dst> Clone for Rotation3D<T, Src, Dst> {
fn clone(&self) -> Self {
Rotation3D {
i: self.i.clone(),
j: self.j.clone(),
k: self.k.clone(),
r: self.r.clone(),
_unit: PhantomData,
}
}
}
impl<T, Src, Dst> Eq for Rotation3D<T, Src, Dst> where T: Eq {}
impl<T, Src, Dst> PartialEq for Rotation3D<T, Src, Dst>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.i == other.i && self.j == other.j && self.k == other.k && self.r == other.r
}
}
impl<T, Src, Dst> Hash for Rotation3D<T, Src, Dst>
where
T: Hash,
{
fn hash<H: core::hash::Hasher>(&self, h: &mut H) {
self.i.hash(h);
self.j.hash(h);
self.k.hash(h);
self.r.hash(h);
}
}
#[cfg(feature = "arbitrary")]
impl<'a, T, Src, Dst> arbitrary::Arbitrary<'a> for Rotation3D<T, Src, Dst>
where
T: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let (i, j, k, r) = arbitrary::Arbitrary::arbitrary(u)?;
Ok(Rotation3D::quaternion(i, j, k, r))
}
}
#[cfg(feature = "bytemuck")]
unsafe impl<T: Zeroable, Src, Dst> Zeroable for Rotation3D<T, Src, Dst> {}
#[cfg(feature = "bytemuck")]
unsafe impl<T: Pod, Src: 'static, Dst: 'static> Pod for Rotation3D<T, Src, Dst> {}
impl<T, Src, Dst> Rotation3D<T, Src, Dst> {
#[inline]
pub fn quaternion(a: T, b: T, c: T, r: T) -> Self {
Rotation3D {
i: a,
j: b,
k: c,
r,
_unit: PhantomData,
}
}
#[inline]
pub fn identity() -> Self
where
T: Zero + One,
{
Self::quaternion(T::zero(), T::zero(), T::zero(), T::one())
}
}
impl<T, Src, Dst> Rotation3D<T, Src, Dst>
where
T: Copy,
{
#[inline]
pub fn vector_part(&self) -> Vector3D<T, UnknownUnit> {
vec3(self.i, self.j, self.k)
}
#[inline]
pub fn cast_unit<Src2, Dst2>(&self) -> Rotation3D<T, Src2, Dst2> {
Rotation3D {
i: self.i,
j: self.j,
k: self.k,
r: self.r,
_unit: PhantomData,
}
}
#[inline]
pub fn to_untyped(&self) -> Rotation3D<T, UnknownUnit, UnknownUnit> {
self.cast_unit()
}
#[inline]
pub fn from_untyped(r: &Rotation3D<T, UnknownUnit, UnknownUnit>) -> Self {
r.cast_unit()
}
}
impl<T, Src, Dst> Rotation3D<T, Src, Dst>
where
T: Real,
{
#[inline]
pub fn unit_quaternion(i: T, j: T, k: T, r: T) -> Self {
Self::quaternion(i, j, k, r).normalize()
}
pub fn around_axis(axis: Vector3D<T, Src>, angle: Angle<T>) -> Self {
let axis = axis.normalize();
let two = T::one() + T::one();
let (sin, cos) = Angle::sin_cos(angle / two);
Self::quaternion(axis.x * sin, axis.y * sin, axis.z * sin, cos)
}
pub fn around_x(angle: Angle<T>) -> Self {
let zero = Zero::zero();
let two = T::one() + T::one();
let (sin, cos) = Angle::sin_cos(angle / two);
Self::quaternion(sin, zero, zero, cos)
}
pub fn around_y(angle: Angle<T>) -> Self {
let zero = Zero::zero();
let two = T::one() + T::one();
let (sin, cos) = Angle::sin_cos(angle / two);
Self::quaternion(zero, sin, zero, cos)
}
pub fn around_z(angle: Angle<T>) -> Self {
let zero = Zero::zero();
let two = T::one() + T::one();
let (sin, cos) = Angle::sin_cos(angle / two);
Self::quaternion(zero, zero, sin, cos)
}
pub fn euler(roll: Angle<T>, pitch: Angle<T>, yaw: Angle<T>) -> Self {
let half = T::one() / (T::one() + T::one());
let (sy, cy) = Real::sin_cos(half * yaw.get());
let (sp, cp) = Real::sin_cos(half * pitch.get());
let (sr, cr) = Real::sin_cos(half * roll.get());
Self::quaternion(
cy * sr * cp - sy * cr * sp,
cy * cr * sp + sy * sr * cp,
sy * cr * cp - cy * sr * sp,
cy * cr * cp + sy * sr * sp,
)
}
#[inline]
pub fn inverse(&self) -> Rotation3D<T, Dst, Src> {
Rotation3D::quaternion(-self.i, -self.j, -self.k, self.r)
}
#[inline]
pub fn norm(&self) -> T {
self.square_norm().sqrt()
}
#[inline]
pub fn square_norm(&self) -> T {
self.i * self.i + self.j * self.j + self.k * self.k + self.r * self.r
}
#[inline]
pub fn normalize(&self) -> Self {
self.mul(T::one() / self.norm())
}
#[inline]
pub fn is_normalized(&self) -> bool
where
T: ApproxEq<T>,
{
let eps = NumCast::from(1.0e-5).unwrap();
self.square_norm().approx_eq_eps(&T::one(), &eps)
}
pub fn slerp(&self, other: &Self, t: T) -> Self
where
T: ApproxEq<T>,
{
debug_assert!(self.is_normalized());
debug_assert!(other.is_normalized());
let r1 = *self;
let mut r2 = *other;
let mut dot = r1.i * r2.i + r1.j * r2.j + r1.k * r2.k + r1.r * r2.r;
let one = T::one();
if dot.approx_eq(&T::one()) {
return r1.lerp(&r2, t);
}
if dot < T::zero() {
r2 = r2.mul(-T::one());
dot = -dot;
}
dot = Real::min(dot, one);
let theta = Real::acos(dot) * t;
let r3 = r2.sub(r1.mul(dot)).normalize();
let (sin, cos) = Real::sin_cos(theta);
r1.mul(cos).add(r3.mul(sin))
}
#[inline]
pub fn lerp(&self, other: &Self, t: T) -> Self {
let one_t = T::one() - t;
self.mul(one_t).add(other.mul(t)).normalize()
}
pub fn transform_point3d(&self, point: Point3D<T, Src>) -> Point3D<T, Dst>
where
T: ApproxEq<T>,
{
debug_assert!(self.is_normalized());
let two = T::one() + T::one();
let cross = self.vector_part().cross(point.to_vector().to_untyped()) * two;
point3(
point.x + self.r * cross.x + self.j * cross.z - self.k * cross.y,
point.y + self.r * cross.y + self.k * cross.x - self.i * cross.z,
point.z + self.r * cross.z + self.i * cross.y - self.j * cross.x,
)
}
#[inline]
pub fn transform_point2d(&self, point: Point2D<T, Src>) -> Point2D<T, Dst>
where
T: ApproxEq<T>,
{
self.transform_point3d(point.to_3d()).xy()
}
#[inline]
pub fn transform_vector3d(&self, vector: Vector3D<T, Src>) -> Vector3D<T, Dst>
where
T: ApproxEq<T>,
{
self.transform_point3d(vector.to_point()).to_vector()
}
#[inline]
pub fn transform_vector2d(&self, vector: Vector2D<T, Src>) -> Vector2D<T, Dst>
where
T: ApproxEq<T>,
{
self.transform_vector3d(vector.to_3d()).xy()
}
#[inline]
#[rustfmt::skip]
pub fn to_transform(&self) -> Transform3D<T, Src, Dst>
where
T: ApproxEq<T>,
{
debug_assert!(self.is_normalized());
let i2 = self.i + self.i;
let j2 = self.j + self.j;
let k2 = self.k + self.k;
let ii = self.i * i2;
let ij = self.i * j2;
let ik = self.i * k2;
let jj = self.j * j2;
let jk = self.j * k2;
let kk = self.k * k2;
let ri = self.r * i2;
let rj = self.r * j2;
let rk = self.r * k2;
let one = T::one();
let zero = T::zero();
let m11 = one - (jj + kk);
let m12 = ij + rk;
let m13 = ik - rj;
let m21 = ij - rk;
let m22 = one - (ii + kk);
let m23 = jk + ri;
let m31 = ik + rj;
let m32 = jk - ri;
let m33 = one - (ii + jj);
Transform3D::new(
m11, m12, m13, zero,
m21, m22, m23, zero,
m31, m32, m33, zero,
zero, zero, zero, one,
)
}
#[inline]
pub fn then<NewDst>(&self, other: &Rotation3D<T, Dst, NewDst>) -> Rotation3D<T, Src, NewDst>
where
T: ApproxEq<T>,
{
debug_assert!(self.is_normalized());
Rotation3D::quaternion(
other.i * self.r + other.r * self.i + other.j * self.k - other.k * self.j,
other.j * self.r + other.r * self.j + other.k * self.i - other.i * self.k,
other.k * self.r + other.r * self.k + other.i * self.j - other.j * self.i,
other.r * self.r - other.i * self.i - other.j * self.j - other.k * self.k,
)
}
#[inline]
fn add(&self, other: Self) -> Self {
Self::quaternion(
self.i + other.i,
self.j + other.j,
self.k + other.k,
self.r + other.r,
)
}
#[inline]
fn sub(&self, other: Self) -> Self {
Self::quaternion(
self.i - other.i,
self.j - other.j,
self.k - other.k,
self.r - other.r,
)
}
#[inline]
fn mul(&self, factor: T) -> Self {
Self::quaternion(
self.i * factor,
self.j * factor,
self.k * factor,
self.r * factor,
)
}
}
impl<T: fmt::Debug, Src, Dst> fmt::Debug for Rotation3D<T, Src, Dst> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Quat({:?}*i + {:?}*j + {:?}*k + {:?})",
self.i, self.j, self.k, self.r
)
}
}
impl<T, Src, Dst> ApproxEq<T> for Rotation3D<T, Src, Dst>
where
T: Copy + Neg<Output = T> + ApproxEq<T>,
{
fn approx_epsilon() -> T {
T::approx_epsilon()
}
fn approx_eq_eps(&self, other: &Self, eps: &T) -> bool {
(self.i.approx_eq_eps(&other.i, eps)
&& self.j.approx_eq_eps(&other.j, eps)
&& self.k.approx_eq_eps(&other.k, eps)
&& self.r.approx_eq_eps(&other.r, eps))
|| (self.i.approx_eq_eps(&-other.i, eps)
&& self.j.approx_eq_eps(&-other.j, eps)
&& self.k.approx_eq_eps(&-other.k, eps)
&& self.r.approx_eq_eps(&-other.r, eps))
}
}
#[test]
fn simple_rotation_2d() {
use crate::default::Rotation2D;
use core::f32::consts::{FRAC_PI_2, PI};
let ri = Rotation2D::identity();
let r90 = Rotation2D::radians(FRAC_PI_2);
let rm90 = Rotation2D::radians(-FRAC_PI_2);
let r180 = Rotation2D::radians(PI);
assert!(ri
.transform_point(point2(1.0, 2.0))
.approx_eq(&point2(1.0, 2.0)));
assert!(r90
.transform_point(point2(1.0, 2.0))
.approx_eq(&point2(-2.0, 1.0)));
assert!(rm90
.transform_point(point2(1.0, 2.0))
.approx_eq(&point2(2.0, -1.0)));
assert!(r180
.transform_point(point2(1.0, 2.0))
.approx_eq(&point2(-1.0, -2.0)));
assert!(r90
.inverse()
.inverse()
.transform_point(point2(1.0, 2.0))
.approx_eq(&r90.transform_point(point2(1.0, 2.0))));
}
#[test]
fn simple_rotation_3d_in_2d() {
use crate::default::Rotation3D;
use core::f32::consts::{FRAC_PI_2, PI};
let ri = Rotation3D::identity();
let r90 = Rotation3D::around_z(Angle::radians(FRAC_PI_2));
let rm90 = Rotation3D::around_z(Angle::radians(-FRAC_PI_2));
let r180 = Rotation3D::around_z(Angle::radians(PI));
assert!(ri
.transform_point2d(point2(1.0, 2.0))
.approx_eq(&point2(1.0, 2.0)));
assert!(r90
.transform_point2d(point2(1.0, 2.0))
.approx_eq(&point2(-2.0, 1.0)));
assert!(rm90
.transform_point2d(point2(1.0, 2.0))
.approx_eq(&point2(2.0, -1.0)));
assert!(r180
.transform_point2d(point2(1.0, 2.0))
.approx_eq(&point2(-1.0, -2.0)));
assert!(r90
.inverse()
.inverse()
.transform_point2d(point2(1.0, 2.0))
.approx_eq(&r90.transform_point2d(point2(1.0, 2.0))));
}
#[test]
fn pre_post() {
use crate::default::Rotation3D;
use core::f32::consts::FRAC_PI_2;
let r1 = Rotation3D::around_x(Angle::radians(FRAC_PI_2));
let r2 = Rotation3D::around_y(Angle::radians(FRAC_PI_2));
let r3 = Rotation3D::around_z(Angle::radians(FRAC_PI_2));
let t1 = r1.to_transform();
let t2 = r2.to_transform();
let t3 = r3.to_transform();
let p = point3(1.0, 2.0, 3.0);
let p1 = r1.then(&r2).then(&r3).transform_point3d(p);
let p2 = t1.then(&t2).then(&t3).transform_point3d(p);
assert!(p1.approx_eq(&p2.unwrap()));
let p3 = t3.then(&t1).then(&t2).transform_point3d(p);
assert!(!p1.approx_eq(&p3.unwrap()));
}
#[test]
fn to_transform3d() {
use crate::default::Rotation3D;
use core::f32::consts::{FRAC_PI_2, PI};
let rotations = [
Rotation3D::identity(),
Rotation3D::around_x(Angle::radians(FRAC_PI_2)),
Rotation3D::around_x(Angle::radians(-FRAC_PI_2)),
Rotation3D::around_x(Angle::radians(PI)),
Rotation3D::around_y(Angle::radians(FRAC_PI_2)),
Rotation3D::around_y(Angle::radians(-FRAC_PI_2)),
Rotation3D::around_y(Angle::radians(PI)),
Rotation3D::around_z(Angle::radians(FRAC_PI_2)),
Rotation3D::around_z(Angle::radians(-FRAC_PI_2)),
Rotation3D::around_z(Angle::radians(PI)),
];
let points = [
point3(0.0, 0.0, 0.0),
point3(1.0, 2.0, 3.0),
point3(-5.0, 3.0, -1.0),
point3(-0.5, -1.0, 1.5),
];
for rotation in &rotations {
for &point in &points {
let p1 = rotation.transform_point3d(point);
let p2 = rotation.to_transform().transform_point3d(point);
assert!(p1.approx_eq(&p2.unwrap()));
}
}
}
#[test]
fn slerp() {
use crate::default::Rotation3D;
let q1 = Rotation3D::quaternion(1.0, 0.0, 0.0, 0.0);
let q2 = Rotation3D::quaternion(0.0, 1.0, 0.0, 0.0);
let q3 = Rotation3D::quaternion(0.0, 0.0, -1.0, 0.0);
assert!(q1.slerp(&q2, 0.0).approx_eq(&q1));
assert!(q1.slerp(&q2, 0.2).approx_eq(&Rotation3D::quaternion(
0.951056516295154,
0.309016994374947,
0.0,
0.0
)));
assert!(q1.slerp(&q2, 0.4).approx_eq(&Rotation3D::quaternion(
0.809016994374947,
0.587785252292473,
0.0,
0.0
)));
assert!(q1.slerp(&q2, 0.6).approx_eq(&Rotation3D::quaternion(
0.587785252292473,
0.809016994374947,
0.0,
0.0
)));
assert!(q1.slerp(&q2, 0.8).approx_eq(&Rotation3D::quaternion(
0.309016994374947,
0.951056516295154,
0.0,
0.0
)));
assert!(q1.slerp(&q2, 1.0).approx_eq(&q2));
assert!(q1.slerp(&q3, 0.0).approx_eq(&q1));
assert!(q1.slerp(&q3, 0.2).approx_eq(&Rotation3D::quaternion(
0.951056516295154,
0.0,
-0.309016994374947,
0.0
)));
assert!(q1.slerp(&q3, 0.4).approx_eq(&Rotation3D::quaternion(
0.809016994374947,
0.0,
-0.587785252292473,
0.0
)));
assert!(q1.slerp(&q3, 0.6).approx_eq(&Rotation3D::quaternion(
0.587785252292473,
0.0,
-0.809016994374947,
0.0
)));
assert!(q1.slerp(&q3, 0.8).approx_eq(&Rotation3D::quaternion(
0.309016994374947,
0.0,
-0.951056516295154,
0.0
)));
assert!(q1.slerp(&q3, 1.0).approx_eq(&q3));
}
#[test]
fn around_axis() {
use crate::default::Rotation3D;
use core::f32::consts::{FRAC_PI_2, PI};
let r1 = Rotation3D::around_axis(vec3(1.0, 1.0, 0.0), Angle::radians(PI));
let r2 = Rotation3D::around_axis(vec3(1.0, 1.0, 0.0), Angle::radians(FRAC_PI_2));
assert!(r1
.transform_point3d(point3(1.0, 2.0, 0.0))
.approx_eq(&point3(2.0, 1.0, 0.0)));
assert!(r2
.transform_point3d(point3(1.0, 0.0, 0.0))
.approx_eq(&point3(0.5, 0.5, -0.5.sqrt())));
let r3 = Rotation3D::around_axis(vec3(0.5, 1.0, 2.0), Angle::radians(2.291288));
assert!(r3
.transform_point3d(point3(1.0, 0.0, 0.0))
.approx_eq(&point3(-0.58071821, 0.81401868, -0.01182979)));
}
#[test]
fn from_euler() {
use crate::default::Rotation3D;
use core::f32::consts::FRAC_PI_2;
let p = point3(1.0, 2.0, 3.0);
let angle = Angle::radians(FRAC_PI_2);
let zero = Angle::radians(0.0);
let roll_re = Rotation3D::euler(angle, zero, zero);
let roll_rq = Rotation3D::around_x(angle);
let roll_pe = roll_re.transform_point3d(p);
let roll_pq = roll_rq.transform_point3d(p);
let pitch_re = Rotation3D::euler(zero, angle, zero);
let pitch_rq = Rotation3D::around_y(angle);
let pitch_pe = pitch_re.transform_point3d(p);
let pitch_pq = pitch_rq.transform_point3d(p);
let yaw_re = Rotation3D::euler(zero, zero, angle);
let yaw_rq = Rotation3D::around_z(angle);
let yaw_pe = yaw_re.transform_point3d(p);
let yaw_pq = yaw_rq.transform_point3d(p);
assert!(roll_pe.approx_eq(&roll_pq));
assert!(pitch_pe.approx_eq(&pitch_pq));
assert!(yaw_pe.approx_eq(&yaw_pq));
let ypr_e = Rotation3D::euler(angle, angle, angle);
let ypr_q = roll_rq.then(&pitch_rq).then(&yaw_rq);
let ypr_pe = ypr_e.transform_point3d(p);
let ypr_pq = ypr_q.transform_point3d(p);
assert!(ypr_pe.approx_eq(&ypr_pq));
}