
# Objective Rotating vectors is a very common task. It is required for a variety of things both within Bevy itself and in many third party plugins, for example all over physics and collision detection, and for things like Bevy's bounding volumes and several gizmo implementations. For 3D, we can do this using a `Quat`, but for 2D, we do not have a clear and efficient option. `Mat2` can be used for rotating vectors if created using `Mat2::from_angle`, but this is not obvious to many users, it doesn't have many rotation helpers, and the type does not give any guarantees that it represents a valid rotation. We should have a proper type for 2D rotations. In addition to allowing for potential optimization, it would allow us to have a consistent and explicitly documented representation used throughout the engine, i.e. counterclockwise and in radians. ## Representation The mathematical formula for rotating a 2D vector is the following: ``` new_x = x * cos - y * sin new_y = x * sin + y * cos ``` Here, `sin` and `cos` are the sine and cosine of the rotation angle. Computing these every time when a vector needs to be rotated can be expensive, so the rotation shouldn't be just an `f32` angle. Instead, it is often more efficient to represent the rotation using the sine and cosine of the angle instead of storing the angle itself. This can be freely passed around and reused without unnecessary computations. The two options are either a 2x2 rotation matrix or a unit complex number where the cosine is the real part and the sine is the imaginary part. These are equivalent for the most part, but the unit complex representation is a bit more memory efficient (two `f32`s instead of four), so I chose that. This is like Nalgebra's [`UnitComplex`](https://docs.rs/nalgebra/latest/nalgebra/geometry/type.UnitComplex.html) type, which can be used for the [`Rotation2`](https://docs.rs/nalgebra/latest/nalgebra/geometry/type.Rotation2.html) type. ## Implementation Add a `Rotation2d` type represented as a unit complex number: ```rust /// A counterclockwise 2D rotation in radians. /// /// The rotation angle is wrapped to be within the `]-pi, pi]` range. pub struct Rotation2d { /// The cosine of the rotation angle in radians. /// /// This is the real part of the unit complex number representing the rotation. pub cos: f32, /// The sine of the rotation angle in radians. /// /// This is the imaginary part of the unit complex number representing the rotation. pub sin: f32, } ``` Using it is similar to using `Quat`, but in 2D: ```rust let rotation = Rotation2d::radians(PI / 2.0); // Rotate vector (also works on Direction2d!) assert_eq!(rotation * Vec2::X, Vec2::Y); // Get angle as degrees assert_eq!(rotation.as_degrees(), 90.0); // Getting sin and cos is free let (sin, cos) = rotation.sin_cos(); // "Subtract" rotations let rotation2 = Rotation2d::FRAC_PI_4; // there are constants! let diff = rotation * rotation2.inverse(); assert_eq!(diff.as_radians(), PI / 4.0); // This is equivalent to the above assert_eq!(rotation2.angle_between(rotation), PI / 4.0); // Lerp let rotation1 = Rotation2d::IDENTITY; let rotation2 = Rotation2d::FRAC_PI_2; let result = rotation1.lerp(rotation2, 0.5); assert_eq!(result.as_radians(), std::f32::consts::FRAC_PI_4); // Slerp let rotation1 = Rotation2d::FRAC_PI_4); let rotation2 = Rotation2d::degrees(-180.0); // we can use degrees too! let result = rotation1.slerp(rotation2, 1.0 / 3.0); assert_eq!(result.as_radians(), std::f32::consts::FRAC_PI_2); ``` There's also a `From<f32>` implementation for `Rotation2d`, which means that methods can still accept radians as floats if the argument uses `impl Into<Rotation2d>`. This means that adding `Rotation2d` shouldn't even be a breaking change. --- ## Changelog - Added `Rotation2d` - Bounding volume methods now take an `impl Into<Rotation2d>` - Gizmo methods with rotation now take an `impl Into<Rotation2d>` ## Future use cases - Collision detection (a type like this is quite essential considering how common vector rotations are) - `Transform` helpers (e.g. return a 2D rotation about the Z axis from a `Transform`) - The rotation used for `Transform2d` (#8268) - More gizmos, maybe meshes... everything in 2D that uses rotation --------- Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> Co-authored-by: Robert Walter <robwalter96@gmail.com> Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
626 lines
19 KiB
Rust
626 lines
19 KiB
Rust
use crate::{
|
|
primitives::{Primitive2d, Primitive3d},
|
|
Quat, Rotation2d, Vec2, Vec3, Vec3A,
|
|
};
|
|
|
|
/// An error indicating that a direction is invalid.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum InvalidDirectionError {
|
|
/// The length of the direction vector is zero or very close to zero.
|
|
Zero,
|
|
/// The length of the direction vector is `std::f32::INFINITY`.
|
|
Infinite,
|
|
/// The length of the direction vector is `NaN`.
|
|
NaN,
|
|
}
|
|
|
|
impl InvalidDirectionError {
|
|
/// Creates an [`InvalidDirectionError`] from the length of an invalid direction vector.
|
|
pub fn from_length(length: f32) -> Self {
|
|
if length.is_nan() {
|
|
InvalidDirectionError::NaN
|
|
} else if !length.is_finite() {
|
|
// If the direction is non-finite but also not NaN, it must be infinite
|
|
InvalidDirectionError::Infinite
|
|
} else {
|
|
// If the direction is invalid but neither NaN nor infinite, it must be zero
|
|
InvalidDirectionError::Zero
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for InvalidDirectionError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"Direction can not be zero (or very close to zero), or non-finite."
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Checks that a vector with the given squared length is normalized.
|
|
///
|
|
/// Warns for small error with a length threshold of approximately `1e-4`,
|
|
/// and panics for large error with a length threshold of approximately `1e-2`.
|
|
///
|
|
/// The format used for the logged warning is `"Warning: {warning} The length is {length}`,
|
|
/// and similarly for the error.
|
|
#[cfg(debug_assertions)]
|
|
fn assert_is_normalized(message: &str, length_squared: f32) {
|
|
let length_error_squared = (length_squared - 1.0).abs();
|
|
|
|
// Panic for large error and warn for slight error.
|
|
if length_error_squared > 2e-2 || length_error_squared.is_nan() {
|
|
// Length error is approximately 1e-2 or more.
|
|
panic!("Error: {message} The length is {}.", length_squared.sqrt());
|
|
} else if length_error_squared > 2e-4 {
|
|
// Length error is approximately 1e-4 or more.
|
|
eprintln!(
|
|
"Warning: {message} The length is {}.",
|
|
length_squared.sqrt()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A normalized vector pointing in a direction in 2D space
|
|
#[deprecated(
|
|
since = "0.14.0",
|
|
note = "`Direction2d` has been renamed. Please use `Dir2` instead."
|
|
)]
|
|
pub type Direction2d = Dir2;
|
|
|
|
/// A normalized vector pointing in a direction in 3D space
|
|
#[deprecated(
|
|
since = "0.14.0",
|
|
note = "`Direction3d` has been renamed. Please use `Dir3` instead."
|
|
)]
|
|
pub type Direction3d = Dir3;
|
|
|
|
/// A normalized vector pointing in a direction in 2D space
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
#[doc(alias = "Direction2d")]
|
|
pub struct Dir2(Vec2);
|
|
impl Primitive2d for Dir2 {}
|
|
|
|
impl Dir2 {
|
|
/// A unit vector pointing along the positive X axis.
|
|
pub const X: Self = Self(Vec2::X);
|
|
/// A unit vector pointing along the positive Y axis.
|
|
pub const Y: Self = Self(Vec2::Y);
|
|
/// A unit vector pointing along the negative X axis.
|
|
pub const NEG_X: Self = Self(Vec2::NEG_X);
|
|
/// A unit vector pointing along the negative Y axis.
|
|
pub const NEG_Y: Self = Self(Vec2::NEG_Y);
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec2`].
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new(value: Vec2) -> Result<Self, InvalidDirectionError> {
|
|
Self::new_and_length(value).map(|(dir, _)| dir)
|
|
}
|
|
|
|
/// Create a [`Dir2`] from a [`Vec2`] that is already normalized.
|
|
///
|
|
/// # Warning
|
|
///
|
|
/// `value` must be normalized, i.e its length must be `1.0`.
|
|
pub fn new_unchecked(value: Vec2) -> Self {
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"The vector given to `Dir2::new_unchecked` is not normalized.",
|
|
value.length_squared(),
|
|
);
|
|
|
|
Self(value)
|
|
}
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec2`], also returning its original length.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new_and_length(value: Vec2) -> Result<(Self, f32), InvalidDirectionError> {
|
|
let length = value.length();
|
|
let direction = (length.is_finite() && length > 0.0).then_some(value / length);
|
|
|
|
direction
|
|
.map(|dir| (Self(dir), length))
|
|
.ok_or(InvalidDirectionError::from_length(length))
|
|
}
|
|
|
|
/// Create a direction from its `x` and `y` components.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the vector formed by the components is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn from_xy(x: f32, y: f32) -> Result<Self, InvalidDirectionError> {
|
|
Self::new(Vec2::new(x, y))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Vec2> for Dir2 {
|
|
type Error = InvalidDirectionError;
|
|
|
|
fn try_from(value: Vec2) -> Result<Self, Self::Error> {
|
|
Self::new(value)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for Dir2 {
|
|
type Target = Vec2;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Neg for Dir2 {
|
|
type Output = Self;
|
|
fn neg(self) -> Self::Output {
|
|
Self(-self.0)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<f32> for Dir2 {
|
|
type Output = Vec2;
|
|
fn mul(self, rhs: f32) -> Self::Output {
|
|
self.0 * rhs
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir2> for f32 {
|
|
type Output = Vec2;
|
|
fn mul(self, rhs: Dir2) -> Self::Output {
|
|
self * rhs.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir2> for Rotation2d {
|
|
type Output = Dir2;
|
|
|
|
/// Rotates the [`Dir2`] using a [`Rotation2d`].
|
|
fn mul(self, direction: Dir2) -> Self::Output {
|
|
let rotated = self * *direction;
|
|
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"`Dir2` is denormalized after rotation.",
|
|
rotated.length_squared(),
|
|
);
|
|
|
|
Dir2(rotated)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::AbsDiffEq for Dir2 {
|
|
type Epsilon = f32;
|
|
fn default_epsilon() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool {
|
|
self.as_ref().abs_diff_eq(other.as_ref(), epsilon)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::RelativeEq for Dir2 {
|
|
fn default_max_relative() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool {
|
|
self.as_ref()
|
|
.relative_eq(other.as_ref(), epsilon, max_relative)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::UlpsEq for Dir2 {
|
|
fn default_max_ulps() -> u32 {
|
|
4
|
|
}
|
|
fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool {
|
|
self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps)
|
|
}
|
|
}
|
|
|
|
/// A normalized vector pointing in a direction in 3D space
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
#[doc(alias = "Direction3d")]
|
|
pub struct Dir3(Vec3);
|
|
impl Primitive3d for Dir3 {}
|
|
|
|
impl Dir3 {
|
|
/// A unit vector pointing along the positive X axis.
|
|
pub const X: Self = Self(Vec3::X);
|
|
/// A unit vector pointing along the positive Y axis.
|
|
pub const Y: Self = Self(Vec3::Y);
|
|
/// A unit vector pointing along the positive Z axis.
|
|
pub const Z: Self = Self(Vec3::Z);
|
|
/// A unit vector pointing along the negative X axis.
|
|
pub const NEG_X: Self = Self(Vec3::NEG_X);
|
|
/// A unit vector pointing along the negative Y axis.
|
|
pub const NEG_Y: Self = Self(Vec3::NEG_Y);
|
|
/// A unit vector pointing along the negative Z axis.
|
|
pub const NEG_Z: Self = Self(Vec3::NEG_Z);
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec3`].
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new(value: Vec3) -> Result<Self, InvalidDirectionError> {
|
|
Self::new_and_length(value).map(|(dir, _)| dir)
|
|
}
|
|
|
|
/// Create a [`Dir3`] from a [`Vec3`] that is already normalized.
|
|
///
|
|
/// # Warning
|
|
///
|
|
/// `value` must be normalized, i.e its length must be `1.0`.
|
|
pub fn new_unchecked(value: Vec3) -> Self {
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"The vector given to `Dir3::new_unchecked` is not normalized.",
|
|
value.length_squared(),
|
|
);
|
|
|
|
Self(value)
|
|
}
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec3`], also returning its original length.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new_and_length(value: Vec3) -> Result<(Self, f32), InvalidDirectionError> {
|
|
let length = value.length();
|
|
let direction = (length.is_finite() && length > 0.0).then_some(value / length);
|
|
|
|
direction
|
|
.map(|dir| (Self(dir), length))
|
|
.ok_or(InvalidDirectionError::from_length(length))
|
|
}
|
|
|
|
/// Create a direction from its `x`, `y`, and `z` components.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the vector formed by the components is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn from_xyz(x: f32, y: f32, z: f32) -> Result<Self, InvalidDirectionError> {
|
|
Self::new(Vec3::new(x, y, z))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Vec3> for Dir3 {
|
|
type Error = InvalidDirectionError;
|
|
|
|
fn try_from(value: Vec3) -> Result<Self, Self::Error> {
|
|
Self::new(value)
|
|
}
|
|
}
|
|
|
|
impl From<Dir3> for Vec3 {
|
|
fn from(value: Dir3) -> Self {
|
|
value.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for Dir3 {
|
|
type Target = Vec3;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Neg for Dir3 {
|
|
type Output = Self;
|
|
fn neg(self) -> Self::Output {
|
|
Self(-self.0)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<f32> for Dir3 {
|
|
type Output = Vec3;
|
|
fn mul(self, rhs: f32) -> Self::Output {
|
|
self.0 * rhs
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir3> for f32 {
|
|
type Output = Vec3;
|
|
fn mul(self, rhs: Dir3) -> Self::Output {
|
|
self * rhs.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir3> for Quat {
|
|
type Output = Dir3;
|
|
|
|
/// Rotates the [`Dir3`] using a [`Quat`].
|
|
fn mul(self, direction: Dir3) -> Self::Output {
|
|
let rotated = self * *direction;
|
|
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"`Dir3` is denormalized after rotation.",
|
|
rotated.length_squared(),
|
|
);
|
|
|
|
Dir3(rotated)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::AbsDiffEq for Dir3 {
|
|
type Epsilon = f32;
|
|
fn default_epsilon() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool {
|
|
self.as_ref().abs_diff_eq(other.as_ref(), epsilon)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::RelativeEq for Dir3 {
|
|
fn default_max_relative() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool {
|
|
self.as_ref()
|
|
.relative_eq(other.as_ref(), epsilon, max_relative)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::UlpsEq for Dir3 {
|
|
fn default_max_ulps() -> u32 {
|
|
4
|
|
}
|
|
fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool {
|
|
self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps)
|
|
}
|
|
}
|
|
|
|
/// A normalized SIMD vector pointing in a direction in 3D space.
|
|
///
|
|
/// This type stores a 16 byte aligned [`Vec3A`].
|
|
/// This may or may not be faster than [`Dir3`]: make sure to benchmark!
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
|
#[doc(alias = "Direction3dA")]
|
|
pub struct Dir3A(Vec3A);
|
|
impl Primitive3d for Dir3A {}
|
|
|
|
impl Dir3A {
|
|
/// A unit vector pointing along the positive X axis.
|
|
pub const X: Self = Self(Vec3A::X);
|
|
/// A unit vector pointing along the positive Y axis.
|
|
pub const Y: Self = Self(Vec3A::Y);
|
|
/// A unit vector pointing along the positive Z axis.
|
|
pub const Z: Self = Self(Vec3A::Z);
|
|
/// A unit vector pointing along the negative X axis.
|
|
pub const NEG_X: Self = Self(Vec3A::NEG_X);
|
|
/// A unit vector pointing along the negative Y axis.
|
|
pub const NEG_Y: Self = Self(Vec3A::NEG_Y);
|
|
/// A unit vector pointing along the negative Z axis.
|
|
pub const NEG_Z: Self = Self(Vec3A::NEG_Z);
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec3A`].
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new(value: Vec3A) -> Result<Self, InvalidDirectionError> {
|
|
Self::new_and_length(value).map(|(dir, _)| dir)
|
|
}
|
|
|
|
/// Create a [`Dir3A`] from a [`Vec3A`] that is already normalized.
|
|
///
|
|
/// # Warning
|
|
///
|
|
/// `value` must be normalized, i.e its length must be `1.0`.
|
|
pub fn new_unchecked(value: Vec3A) -> Self {
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"The vector given to `Dir3A::new_unchecked` is not normalized.",
|
|
value.length_squared(),
|
|
);
|
|
|
|
Self(value)
|
|
}
|
|
|
|
/// Create a direction from a finite, nonzero [`Vec3A`], also returning its original length.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn new_and_length(value: Vec3A) -> Result<(Self, f32), InvalidDirectionError> {
|
|
let length = value.length();
|
|
let direction = (length.is_finite() && length > 0.0).then_some(value / length);
|
|
|
|
direction
|
|
.map(|dir| (Self(dir), length))
|
|
.ok_or(InvalidDirectionError::from_length(length))
|
|
}
|
|
|
|
/// Create a direction from its `x`, `y`, and `z` components.
|
|
///
|
|
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
|
|
/// of the vector formed by the components is zero (or very close to zero), infinite, or `NaN`.
|
|
pub fn from_xyz(x: f32, y: f32, z: f32) -> Result<Self, InvalidDirectionError> {
|
|
Self::new(Vec3A::new(x, y, z))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Vec3A> for Dir3A {
|
|
type Error = InvalidDirectionError;
|
|
|
|
fn try_from(value: Vec3A) -> Result<Self, Self::Error> {
|
|
Self::new(value)
|
|
}
|
|
}
|
|
|
|
impl From<Dir3A> for Vec3A {
|
|
fn from(value: Dir3A) -> Self {
|
|
value.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for Dir3A {
|
|
type Target = Vec3A;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Neg for Dir3A {
|
|
type Output = Self;
|
|
fn neg(self) -> Self::Output {
|
|
Self(-self.0)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<f32> for Dir3A {
|
|
type Output = Vec3A;
|
|
fn mul(self, rhs: f32) -> Self::Output {
|
|
self.0 * rhs
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir3A> for f32 {
|
|
type Output = Vec3A;
|
|
fn mul(self, rhs: Dir3A) -> Self::Output {
|
|
self * rhs.0
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul<Dir3A> for Quat {
|
|
type Output = Dir3A;
|
|
|
|
/// Rotates the [`Dir3A`] using a [`Quat`].
|
|
fn mul(self, direction: Dir3A) -> Self::Output {
|
|
let rotated = self * *direction;
|
|
|
|
#[cfg(debug_assertions)]
|
|
assert_is_normalized(
|
|
"`Dir3A` is denormalized after rotation.",
|
|
rotated.length_squared(),
|
|
);
|
|
|
|
Dir3A(rotated)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::AbsDiffEq for Dir3A {
|
|
type Epsilon = f32;
|
|
fn default_epsilon() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool {
|
|
self.as_ref().abs_diff_eq(other.as_ref(), epsilon)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::RelativeEq for Dir3A {
|
|
fn default_max_relative() -> f32 {
|
|
f32::EPSILON
|
|
}
|
|
fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool {
|
|
self.as_ref()
|
|
.relative_eq(other.as_ref(), epsilon, max_relative)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "approx")]
|
|
impl approx::UlpsEq for Dir3A {
|
|
fn default_max_ulps() -> u32 {
|
|
4
|
|
}
|
|
fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool {
|
|
self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::InvalidDirectionError;
|
|
|
|
#[test]
|
|
fn dir2_creation() {
|
|
assert_eq!(Dir2::new(Vec2::X * 12.5), Ok(Dir2::X));
|
|
assert_eq!(
|
|
Dir2::new(Vec2::new(0.0, 0.0)),
|
|
Err(InvalidDirectionError::Zero)
|
|
);
|
|
assert_eq!(
|
|
Dir2::new(Vec2::new(f32::INFINITY, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir2::new(Vec2::new(f32::NEG_INFINITY, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir2::new(Vec2::new(f32::NAN, 0.0)),
|
|
Err(InvalidDirectionError::NaN)
|
|
);
|
|
assert_eq!(Dir2::new_and_length(Vec2::X * 6.5), Ok((Dir2::X, 6.5)));
|
|
}
|
|
|
|
#[test]
|
|
fn dir3_creation() {
|
|
assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));
|
|
assert_eq!(
|
|
Dir3::new(Vec3::new(0.0, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Zero)
|
|
);
|
|
assert_eq!(
|
|
Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::NaN)
|
|
);
|
|
assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));
|
|
|
|
// Test rotation
|
|
assert!(
|
|
(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2) * Dir3::X)
|
|
.abs_diff_eq(Vec3::Y, 10e-6)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dir3a_creation() {
|
|
assert_eq!(Dir3A::new(Vec3A::X * 12.5), Ok(Dir3A::X));
|
|
assert_eq!(
|
|
Dir3A::new(Vec3A::new(0.0, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Zero)
|
|
);
|
|
assert_eq!(
|
|
Dir3A::new(Vec3A::new(f32::INFINITY, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir3A::new(Vec3A::new(f32::NEG_INFINITY, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::Infinite)
|
|
);
|
|
assert_eq!(
|
|
Dir3A::new(Vec3A::new(f32::NAN, 0.0, 0.0)),
|
|
Err(InvalidDirectionError::NaN)
|
|
);
|
|
assert_eq!(Dir3A::new_and_length(Vec3A::X * 6.5), Ok((Dir3A::X, 6.5)));
|
|
|
|
// Test rotation
|
|
assert!(
|
|
(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2) * Dir3A::X)
|
|
.abs_diff_eq(Vec3A::Y, 10e-6)
|
|
);
|
|
}
|
|
}
|