diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index a60cd3d61956f..bed4198538b82 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,16 +1,30 @@ -use super::{Primitive2d, WindingOrder}; +use super::{InvalidDirectionError, Primitive2d, WindingOrder}; use crate::Vec2; /// A normalized vector pointing in a direction in 2D space -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Direction2d(Vec2); impl Direction2d { /// Create a direction from a finite, nonzero [`Vec2`]. /// - /// Returns `None` if the input is zero (or very close to zero), or non-finite. - pub fn new(value: Vec2) -> Option { - value.try_normalize().map(Self) + /// 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 { + value.try_normalize().map(Self).map_or_else( + || { + if value.is_nan() { + Err(InvalidDirectionError::NaN) + } else if !value.is_finite() { + // If the direction is non-finite but also not NaN, it must be infinite + Err(InvalidDirectionError::Infinite) + } else { + // If the direction is invalid but neither NaN nor infinite, it must be zero + Err(InvalidDirectionError::Zero) + } + }, + Ok, + ) } /// Create a direction from a [`Vec2`] that is already normalized @@ -20,6 +34,14 @@ impl Direction2d { } } +impl TryFrom for Direction2d { + type Error = InvalidDirectionError; + + fn try_from(value: Vec2) -> Result { + Self::new(value) + } +} + impl std::ops::Deref for Direction2d { type Target = Vec2; fn deref(&self) -> &Self::Target { @@ -324,6 +346,30 @@ impl RegularPolygon { mod tests { use super::*; + #[test] + fn direction_creation() { + assert_eq!( + Direction2d::new(Vec2::X * 12.5), + Ok(Direction2d::from_normalized(Vec2::X)) + ); + assert_eq!( + Direction2d::new(Vec2::new(0.0, 0.0)), + Err(InvalidDirectionError::Zero) + ); + assert_eq!( + Direction2d::new(Vec2::new(std::f32::INFINITY, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Direction2d::new(Vec2::new(std::f32::NEG_INFINITY, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Direction2d::new(Vec2::new(std::f32::NAN, 0.0)), + Err(InvalidDirectionError::NaN) + ); + } + #[test] fn triangle_winding_order() { let mut cw_triangle = Triangle2d::new( diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 60594c8824dfa..66b3e38301259 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,16 +1,30 @@ -use super::Primitive3d; +use super::{InvalidDirectionError, Primitive3d}; use crate::Vec3; /// A normalized vector pointing in a direction in 3D space -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Direction3d(Vec3); impl Direction3d { /// Create a direction from a finite, nonzero [`Vec3`]. /// - /// Returns `None` if the input is zero (or very close to zero), or non-finite. - pub fn new(value: Vec3) -> Option { - value.try_normalize().map(Self) + /// 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 { + value.try_normalize().map(Self).map_or_else( + || { + if value.is_nan() { + Err(InvalidDirectionError::NaN) + } else if !value.is_finite() { + // If the direction is non-finite but also not NaN, it must be infinite + Err(InvalidDirectionError::Infinite) + } else { + // If the direction is invalid but neither NaN nor infinite, it must be zero + Err(InvalidDirectionError::Zero) + } + }, + Ok, + ) } /// Create a direction from a [`Vec3`] that is already normalized @@ -20,6 +34,14 @@ impl Direction3d { } } +impl TryFrom for Direction3d { + type Error = InvalidDirectionError; + + fn try_from(value: Vec3) -> Result { + Self::new(value) + } +} + impl std::ops::Deref for Direction3d { type Target = Vec3; fn deref(&self) -> &Self::Target { @@ -332,3 +354,32 @@ impl Torus { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn direction_creation() { + assert_eq!( + Direction3d::new(Vec3::X * 12.5), + Ok(Direction3d::from_normalized(Vec3::X)) + ); + assert_eq!( + Direction3d::new(Vec3::new(0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Zero) + ); + assert_eq!( + Direction3d::new(Vec3::new(std::f32::INFINITY, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Direction3d::new(Vec3::new(std::f32::NEG_INFINITY, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Direction3d::new(Vec3::new(std::f32::NAN, 0.0, 0.0)), + Err(InvalidDirectionError::NaN) + ); + } +} diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs index da3532397b6ea..d66b161172636 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_math/src/primitives/mod.rs @@ -13,6 +13,26 @@ pub trait Primitive2d {} /// A marker trait for 3D primitives pub trait Primitive3d {} +/// 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 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." + ) + } +} + /// The winding order for a set of points #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindingOrder {