Skip to content

Commit

Permalink
Add new_and_length method to Direction2d and Direction3d (#11172)
Browse files Browse the repository at this point in the history
# Objective

When creating a normalized direction from a vector, it can be useful to
get both the direction *and* the original length of the vector.

This came up when I was recreating some Parry APIs using bevy_math, and
doing it manually is quite painful. Nalgebra calls this method
[`Unit::try_new_and_get`](https://docs.rs/nalgebra/latest/nalgebra/base/struct.Unit.html#method.try_new_and_get).

## Solution

Add a `new_and_length` method to `Direction2d` and `Direction3d`.

Usage:

```rust
if let Ok((direction, length)) = Direction2d::new_and_length(Vec2::X * 10.0) {
    assert_eq!(direction, Vec2::X);
    assert_eq!(length, 10.0);
}
```

I'm open to different names, couldn't come up with a perfectly clear one
that isn't too long. My reasoning with the current name is that it's
like using `new` and calling `length` on the original vector.
  • Loading branch information
Jondolf authored Jan 8, 2024
1 parent 42e9908 commit bcbb7bb
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 28 deletions.
32 changes: 18 additions & 14 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ impl Direction2d {
/// 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> {
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,
)
Self::new_and_length(value).map(|(dir, _)| dir)
}

/// 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))
.map_or(Err(InvalidDirectionError::from_length(length)), Ok)
}

/// Create a direction from its `x` and `y` components.
Expand Down Expand Up @@ -404,6 +404,10 @@ mod tests {
Direction2d::new(Vec2::new(f32::NAN, 0.0)),
Err(InvalidDirectionError::NaN)
);
assert_eq!(
Direction2d::new_and_length(Vec2::X * 6.5),
Ok((Direction2d::from_normalized(Vec2::X), 6.5))
);
}

#[test]
Expand Down
32 changes: 18 additions & 14 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ impl Direction3d {
/// 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> {
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,
)
Self::new_and_length(value).map(|(dir, _)| dir)
}

/// 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))
.map_or(Err(InvalidDirectionError::from_length(length)), Ok)
}

/// Create a direction from its `x`, `y`, and `z` components.
Expand Down Expand Up @@ -421,5 +421,9 @@ mod test {
Direction3d::new(Vec3::new(f32::NAN, 0.0, 0.0)),
Err(InvalidDirectionError::NaN)
);
assert_eq!(
Direction3d::new_and_length(Vec3::X * 6.5),
Ok((Direction3d::from_normalized(Vec3::X), 6.5))
);
}
}
15 changes: 15 additions & 0 deletions crates/bevy_math/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ pub enum InvalidDirectionError {
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!(
Expand Down

0 comments on commit bcbb7bb

Please sign in to comment.