Add new_and_length method to Direction2d and Direction3d (#11172)

# 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.
This commit is contained in:
Joona Aalto 2024-01-09 00:36:56 +02:00 committed by GitHub
parent 42e990861c
commit bcbb7bb9dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 28 deletions

View file

@ -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.
@ -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]

View file

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

View file

@ -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!(