bevy/crates/bevy_math/src/direction.rs
Joona Aalto f89af0567b
Add Rotation2d (#11658)
# 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>
2024-03-11 19:11:57 +00:00

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)
);
}
}