Add no_std Support to bevy_math (#15810)

# Objective

- Contributes to #15460

## Solution

- Added two new features, `std` (default) and `alloc`, gating `std` and
`alloc` behind them respectively.
- Added missing `f32` functions to `std_ops` as required. These `f32`
methods have been added to the `clippy.toml` deny list to aid in
`no_std` development.

## Testing

- CI
- `cargo clippy -p bevy_math --no-default-features --features libm
--target "x86_64-unknown-none"`
- `cargo test -p bevy_math --no-default-features --features libm`
- `cargo test -p bevy_math --no-default-features --features "libm,
alloc"`
- `cargo test -p bevy_math --no-default-features --features "libm,
alloc, std"`
- `cargo test -p bevy_math --no-default-features --features "std"`

## Notes

The following items require the `alloc` feature to be enabled:

- `CubicBSpline`
- `CubicBezier`
- `CubicCardinalSpline`
- `CubicCurve`
- `CubicGenerator`
- `CubicHermite`
- `CubicNurbs`
- `CyclicCubicGenerator`
- `RationalCurve`
- `RationalGenerator`
- `BoxedPolygon`
- `BoxedPolyline2d`
- `BoxedPolyline3d`
- `SampleCurve`
- `SampleAutoCurve`
- `UnevenSampleCurve`
- `UnevenSampleAutoCurve`
- `EvenCore`
- `UnevenCore`
- `ChunkedUnevenCore`

This requirement could be relaxed in certain cases, but I had erred on
the side of gating rather than modifying. Since `no_std` is a new set of
platforms we are adding support to, and the `alloc` feature is enabled
by default, this is not a breaking change.

---------

Co-authored-by: Benjamin Brienen <benjamin.brienen@outlook.com>
Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
This commit is contained in:
Zachary Harrold 2024-12-04 04:14:51 +11:00 committed by GitHub
parent aa600ae95e
commit a8b9c945c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 627 additions and 268 deletions

View file

@ -10,20 +10,20 @@ keywords = ["bevy"]
rust-version = "1.81.0" rust-version = "1.81.0"
[dependencies] [dependencies]
glam = { version = "0.29", features = ["bytemuck"] } glam = { version = "0.29", default-features = false, features = ["bytemuck"] }
derive_more = { version = "1", default-features = false, features = [ derive_more = { version = "1", default-features = false, features = [
"error", "error",
"from", "from",
"display", "display",
"into", "into",
] } ] }
itertools = "0.13.0" itertools = { version = "0.13.0", default-features = false }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", default-features = false, features = [
"derive",
], optional = true }
libm = { version = "0.2", optional = true } libm = { version = "0.2", optional = true }
approx = { version = "0.5", optional = true } approx = { version = "0.5", default-features = false, optional = true }
rand = { version = "0.8", features = [ rand = { version = "0.8", default-features = false, optional = true }
"alloc",
], default-features = false, optional = true }
rand_distr = { version = "0.4.3", optional = true } rand_distr = { version = "0.4.3", optional = true }
smallvec = { version = "1.11" } smallvec = { version = "1.11" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
@ -36,11 +36,30 @@ approx = "0.5"
rand = "0.8" rand = "0.8"
rand_chacha = "0.3" rand_chacha = "0.3"
# Enable the approx feature when testing. # Enable the approx feature when testing.
bevy_math = { path = ".", version = "0.15.0-dev", features = ["approx"] } bevy_math = { path = ".", version = "0.15.0-dev", default-features = false, features = [
glam = { version = "0.29", features = ["approx"] } "approx",
] }
glam = { version = "0.29", default-features = false, features = ["approx"] }
[features] [features]
default = ["rand", "bevy_reflect", "curve"] default = ["std", "rand", "bevy_reflect", "curve"]
std = [
"alloc",
"glam/std",
"derive_more/std",
"itertools/use_std",
"serde?/std",
"approx?/std",
"rand?/std",
"rand_distr?/std",
]
alloc = [
"itertools/use_alloc",
"serde?/alloc",
"rand?/alloc",
"rand_distr?/alloc",
]
serialize = ["dep:serde", "glam/serde"] serialize = ["dep:serde", "glam/serde"]
# Enable approx for glam types to approximate floating point equality comparisons and assertions # Enable approx for glam types to approximate floating point equality comparisons and assertions
approx = ["dep:approx", "glam/approx"] approx = ["dep:approx", "glam/approx"]
@ -57,6 +76,8 @@ debug_glam_assert = ["glam/debug-glam-assert"]
rand = ["dep:rand", "dep:rand_distr", "glam/rand"] rand = ["dep:rand", "dep:rand_distr", "glam/rand"]
# Include code related to the Curve trait # Include code related to the Curve trait
curve = [] curve = []
# Enable bevy_reflect (requires std)
bevy_reflect = ["dep:bevy_reflect", "std"]
[lints] [lints]
workspace = true workspace = true

View file

@ -26,4 +26,13 @@ disallowed-methods = [
{ path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" }, { path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" },
{ path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" }, { path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" },
{ path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" }, { path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" },
# These methods have defined precision, but are only available from the standard library,
# not in core. Using these substitutes allows for no_std compatibility.
{ path = "f32::rem_euclid", reason = "use ops::rem_euclid instead for no_std compatibility" },
{ path = "f32::abs", reason = "use ops::abs instead for no_std compatibility" },
{ path = "f32::sqrt", reason = "use ops::sqrt instead for no_std compatibility" },
{ path = "f32::copysign", reason = "use ops::copysign instead for no_std compatibility" },
{ path = "f32::round", reason = "use ops::round instead for no_std compatibility" },
{ path = "f32::floor", reason = "use ops::floor instead for no_std compatibility" },
{ path = "f32::fract", reason = "use ops::fract instead for no_std compatibility" },
] ]

View file

@ -2,6 +2,7 @@ mod primitive_impls;
use super::{BoundingVolume, IntersectsVolume}; use super::{BoundingVolume, IntersectsVolume};
use crate::{ use crate::{
ops,
prelude::{Mat2, Rot2, Vec2}, prelude::{Mat2, Rot2, Vec2},
FloatPow, Isometry2d, FloatPow, Isometry2d,
}; };
@ -296,12 +297,12 @@ mod aabb2d_tests {
min: Vec2::new(-1., -1.), min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.), max: Vec2::new(1., 1.),
}; };
assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON); assert!(ops::abs(aabb.visible_area() - 4.) < f32::EPSILON);
let aabb = Aabb2d { let aabb = Aabb2d {
min: Vec2::new(0., 0.), min: Vec2::new(0., 0.),
max: Vec2::new(1., 0.5), max: Vec2::new(1., 0.5),
}; };
assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON); assert!(ops::abs(aabb.visible_area() - 0.5) < f32::EPSILON);
} }
#[test] #[test]
@ -488,7 +489,7 @@ impl BoundingCircle {
} }
} }
BoundingCircle::new(isometry * center, radius_squared.sqrt()) BoundingCircle::new(isometry * center, ops::sqrt(radius_squared))
} }
/// Get the radius of the bounding circle /// Get the radius of the bounding circle
@ -539,7 +540,7 @@ impl BoundingVolume for BoundingCircle {
#[inline(always)] #[inline(always)]
fn contains(&self, other: &Self) -> bool { fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius(); let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.squared().copysign(diff) self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff)
} }
#[inline(always)] #[inline(always)]
@ -614,14 +615,14 @@ mod bounding_circle_tests {
use super::BoundingCircle; use super::BoundingCircle;
use crate::{ use crate::{
bounding::{BoundingVolume, IntersectsVolume}, bounding::{BoundingVolume, IntersectsVolume},
Vec2, ops, Vec2,
}; };
#[test] #[test]
fn area() { fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.); let circle = BoundingCircle::new(Vec2::ONE, 5.);
// Since this number is messy we check it with a higher threshold // Since this number is messy we check it with a higher threshold
assert!((circle.visible_area() - 78.5398).abs() < 0.001); assert!(ops::abs(circle.visible_area() - 78.5398) < 0.001);
} }
#[test] #[test]
@ -647,7 +648,7 @@ mod bounding_circle_tests {
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.); let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
let merged = a.merge(&b); let merged = a.merge(&b);
assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON); assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON);
assert!(merged.contains(&a)); assert!(merged.contains(&a));
assert!(merged.contains(&b)); assert!(merged.contains(&b));
assert!(!a.contains(&merged)); assert!(!a.contains(&merged));
@ -679,7 +680,7 @@ mod bounding_circle_tests {
fn grow() { fn grow() {
let a = BoundingCircle::new(Vec2::ONE, 5.); let a = BoundingCircle::new(Vec2::ONE, 5.);
let padded = a.grow(1.25); let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON);
assert!(padded.contains(&a)); assert!(padded.contains(&a));
assert!(!a.contains(&padded)); assert!(!a.contains(&padded));
} }
@ -688,7 +689,7 @@ mod bounding_circle_tests {
fn shrink() { fn shrink() {
let a = BoundingCircle::new(Vec2::ONE, 5.); let a = BoundingCircle::new(Vec2::ONE, 5.);
let shrunk = a.shrink(0.5); let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);
assert!(a.contains(&shrunk)); assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a)); assert!(!shrunk.contains(&a));
} }
@ -697,7 +698,7 @@ mod bounding_circle_tests {
fn scale_around_center() { fn scale_around_center() {
let a = BoundingCircle::new(Vec2::ONE, 5.); let a = BoundingCircle::new(Vec2::ONE, 5.);
let scaled = a.scale_around_center(2.); let scaled = a.scale_around_center(2.);
assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);
assert!(!a.contains(&scaled)); assert!(!a.contains(&scaled));
assert!(scaled.contains(&a)); assert!(scaled.contains(&a));
} }

View file

@ -3,14 +3,16 @@
use crate::{ use crate::{
ops, ops,
primitives::{ primitives::{
Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
Rhombus, Segment2d, Triangle2d,
}, },
Dir2, Isometry2d, Mat2, Rot2, Vec2, Dir2, Isometry2d, Mat2, Rot2, Vec2,
}; };
use core::f32::consts::{FRAC_PI_2, PI, TAU}; use core::f32::consts::{FRAC_PI_2, PI, TAU};
#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};
use smallvec::SmallVec; use smallvec::SmallVec;
use super::{Aabb2d, Bounded2d, BoundingCircle}; use super::{Aabb2d, Bounded2d, BoundingCircle};
@ -41,11 +43,11 @@ fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> SmallVec<[Vec2;
// The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.
// Compute the normalized angles of the endpoints with the rotation taken into account, and then // Compute the normalized angles of the endpoints with the rotation taken into account, and then
// check if we are looking for an angle that is between or outside them. // check if we are looking for an angle that is between or outside them.
let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU); let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU);
let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU); let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);
let inverted = left_angle < right_angle; let inverted = left_angle < right_angle;
for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
let angle = extremum.to_angle().rem_euclid(TAU); let angle = ops::rem_euclid(extremum.to_angle(), TAU);
// If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.
// There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.
// But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.
@ -286,6 +288,7 @@ impl<const N: usize> Bounded2d for Polyline2d<N> {
} }
} }
#[cfg(feature = "alloc")]
impl Bounded2d for BoxedPolyline2d { impl Bounded2d for BoxedPolyline2d {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d { fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
Aabb2d::from_point_cloud(isometry, &self.vertices) Aabb2d::from_point_cloud(isometry, &self.vertices)
@ -348,7 +351,8 @@ impl Bounded2d for Rectangle {
// Compute the AABB of the rotated rectangle by transforming the half-extents // Compute the AABB of the rotated rectangle by transforming the half-extents
// by an absolute rotation matrix. // by an absolute rotation matrix.
let (sin, cos) = isometry.rotation.sin_cos(); let (sin, cos) = isometry.rotation.sin_cos();
let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]); let abs_rot_mat =
Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]);
let half_size = abs_rot_mat * self.half_size; let half_size = abs_rot_mat * self.half_size;
Aabb2d::new(isometry.translation, half_size) Aabb2d::new(isometry.translation, half_size)
@ -371,6 +375,7 @@ impl<const N: usize> Bounded2d for Polygon<N> {
} }
} }
#[cfg(feature = "alloc")]
impl Bounded2d for BoxedPolygon { impl Bounded2d for BoxedPolygon {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d { fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
Aabb2d::from_point_cloud(isometry, &self.vertices) Aabb2d::from_point_cloud(isometry, &self.vertices)
@ -470,6 +475,7 @@ mod tests {
// Arcs and circular segments have the same bounding shapes so they share test cases. // Arcs and circular segments have the same bounding shapes so they share test cases.
fn arc_and_segment() { fn arc_and_segment() {
struct TestCase { struct TestCase {
#[allow(unused)]
name: &'static str, name: &'static str,
arc: Arc2d, arc: Arc2d,
translation: Vec2, translation: Vec2,
@ -487,7 +493,7 @@ mod tests {
} }
// The apothem of an arc covering 1/6th of a circle. // The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0; let apothem = ops::sqrt(3.0) / 2.0;
let tests = [ let tests = [
// Test case: a basic minor arc // Test case: a basic minor arc
TestCase { TestCase {
@ -558,7 +564,7 @@ mod tests {
aabb_min: Vec2::ZERO, aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0), aabb_max: Vec2::splat(1.0),
bounding_circle_center: Vec2::splat(0.5), bounding_circle_center: Vec2::splat(0.5),
bounding_circle_radius: f32::sqrt(2.0) / 2.0, bounding_circle_radius: ops::sqrt(2.0) / 2.0,
}, },
// Test case: a basic major arc // Test case: a basic major arc
TestCase { TestCase {
@ -597,6 +603,7 @@ mod tests {
]; ];
for test in tests { for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name); println!("subtest case: {}", test.name);
let segment: CircularSegment = test.arc.into(); let segment: CircularSegment = test.arc.into();
@ -622,6 +629,7 @@ mod tests {
#[test] #[test]
fn circular_sector() { fn circular_sector() {
struct TestCase { struct TestCase {
#[allow(unused)]
name: &'static str, name: &'static str,
arc: Arc2d, arc: Arc2d,
translation: Vec2, translation: Vec2,
@ -639,8 +647,8 @@ mod tests {
} }
// The apothem of an arc covering 1/6th of a circle. // The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0; let apothem = ops::sqrt(3.0) / 2.0;
let inv_sqrt_3 = f32::sqrt(3.0).recip(); let inv_sqrt_3 = ops::sqrt(3.0).recip();
let tests = [ let tests = [
// Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
TestCase { TestCase {
@ -717,7 +725,7 @@ mod tests {
aabb_min: Vec2::ZERO, aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0), aabb_max: Vec2::splat(1.0),
bounding_circle_center: Vec2::splat(0.5), bounding_circle_center: Vec2::splat(0.5),
bounding_circle_radius: f32::sqrt(2.0) / 2.0, bounding_circle_radius: ops::sqrt(2.0) / 2.0,
}, },
TestCase { TestCase {
name: "5/6th circle untransformed", name: "5/6th circle untransformed",
@ -753,6 +761,7 @@ mod tests {
]; ];
for test in tests { for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name); println!("subtest case: {}", test.name);
let sector: CircularSector = test.arc.into(); let sector: CircularSector = test.arc.into();

View file

@ -6,12 +6,15 @@ use crate::{
bounding::{BoundingCircle, BoundingVolume}, bounding::{BoundingCircle, BoundingVolume},
ops, ops,
primitives::{ primitives::{
BoxedPolygon, BoxedPolyline2d, Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d,
Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
}, },
Isometry2d, Isometry3d, Quat, Rot2, Isometry2d, Isometry3d, Quat, Rot2,
}; };
#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};
use crate::{bounding::Bounded2d, primitives::Circle}; use crate::{bounding::Bounded2d, primitives::Circle};
use super::{Aabb3d, Bounded3d, BoundingSphere}; use super::{Aabb3d, Bounded3d, BoundingSphere};
@ -26,7 +29,7 @@ impl BoundedExtrusion for Circle {
let top = (segment_dir * half_depth).abs(); let top = (segment_dir * half_depth).abs();
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d { Aabb3d {
min: isometry.translation - half_size - top, min: isometry.translation - half_size - top,
@ -56,8 +59,8 @@ impl BoundedExtrusion for Ellipse {
let m = -axis.x / axis.y; let m = -axis.x / axis.y;
let signum = axis.signum(); let signum = axis.signum();
let y = signum.y * b * b / (b * b + m * m * a * a).sqrt(); let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a);
let x = signum.x * a * (1. - y * y / b / b).sqrt(); let x = signum.x * a * ops::sqrt(1. - y * y / b / b);
isometry.rotation * Vec3A::new(x, y, 0.) isometry.rotation * Vec3A::new(x, y, 0.)
}); });
@ -104,6 +107,7 @@ impl<const N: usize> BoundedExtrusion for Polyline2d<N> {
} }
} }
#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolyline2d { impl BoundedExtrusion for BoxedPolyline2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into(); let isometry = isometry.into();
@ -144,6 +148,7 @@ impl<const N: usize> BoundedExtrusion for Polygon<N> {
} }
} }
#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolygon { impl BoundedExtrusion for BoxedPolygon {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into(); let isometry = isometry.into();
@ -301,7 +306,7 @@ mod tests {
let bounding_sphere = extrusion.bounding_sphere(isometry); let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
} }
#[test] #[test]
@ -444,7 +449,7 @@ mod tests {
let bounding_sphere = extrusion.bounding_sphere(isometry); let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
} }
#[test] #[test]

View file

@ -4,7 +4,10 @@ mod primitive_impls;
use glam::Mat3; use glam::Mat3;
use super::{BoundingVolume, IntersectsVolume}; use super::{BoundingVolume, IntersectsVolume};
use crate::{ops::FloatPow, Isometry3d, Quat, Vec3A}; use crate::{
ops::{self, FloatPow},
Isometry3d, Quat, Vec3A,
};
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -297,12 +300,12 @@ mod aabb3d_tests {
min: Vec3A::new(-1., -1., -1.), min: Vec3A::new(-1., -1., -1.),
max: Vec3A::new(1., 1., 1.), max: Vec3A::new(1., 1., 1.),
}; };
assert!((aabb.visible_area() - 12.).abs() < f32::EPSILON); assert!(ops::abs(aabb.visible_area() - 12.) < f32::EPSILON);
let aabb = Aabb3d { let aabb = Aabb3d {
min: Vec3A::new(0., 0., 0.), min: Vec3A::new(0., 0., 0.),
max: Vec3A::new(1., 0.5, 0.25), max: Vec3A::new(1., 0.5, 0.25),
}; };
assert!((aabb.visible_area() - 0.875).abs() < f32::EPSILON); assert!(ops::abs(aabb.visible_area() - 0.875) < f32::EPSILON);
} }
#[test] #[test]
@ -494,7 +497,7 @@ impl BoundingSphere {
} }
} }
BoundingSphere::new(isometry * center, radius_squared.sqrt()) BoundingSphere::new(isometry * center, ops::sqrt(radius_squared))
} }
/// Get the radius of the bounding sphere /// Get the radius of the bounding sphere
@ -528,7 +531,7 @@ impl BoundingSphere {
} else { } else {
// The point is outside the sphere. // The point is outside the sphere.
// Find the closest point on the surface of the sphere. // Find the closest point on the surface of the sphere.
let dir_to_point = point / distance_squared.sqrt(); let dir_to_point = point / ops::sqrt(distance_squared);
self.center + radius * dir_to_point self.center + radius * dir_to_point
} }
} }
@ -557,7 +560,7 @@ impl BoundingVolume for BoundingSphere {
#[inline(always)] #[inline(always)]
fn contains(&self, other: &Self) -> bool { fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius(); let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.squared().copysign(diff) self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff)
} }
#[inline(always)] #[inline(always)]
@ -644,14 +647,14 @@ mod bounding_sphere_tests {
use super::BoundingSphere; use super::BoundingSphere;
use crate::{ use crate::{
bounding::{BoundingVolume, IntersectsVolume}, bounding::{BoundingVolume, IntersectsVolume},
Quat, Vec3, Vec3A, ops, Quat, Vec3, Vec3A,
}; };
#[test] #[test]
fn area() { fn area() {
let sphere = BoundingSphere::new(Vec3::ONE, 5.); let sphere = BoundingSphere::new(Vec3::ONE, 5.);
// Since this number is messy we check it with a higher threshold // Since this number is messy we check it with a higher threshold
assert!((sphere.visible_area() - 157.0796).abs() < 0.001); assert!(ops::abs(sphere.visible_area() - 157.0796) < 0.001);
} }
#[test] #[test]
@ -677,7 +680,7 @@ mod bounding_sphere_tests {
let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.); let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.);
let merged = a.merge(&b); let merged = a.merge(&b);
assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON); assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON);
assert!(merged.contains(&a)); assert!(merged.contains(&a));
assert!(merged.contains(&b)); assert!(merged.contains(&b));
assert!(!a.contains(&merged)); assert!(!a.contains(&merged));
@ -709,7 +712,7 @@ mod bounding_sphere_tests {
fn grow() { fn grow() {
let a = BoundingSphere::new(Vec3::ONE, 5.); let a = BoundingSphere::new(Vec3::ONE, 5.);
let padded = a.grow(1.25); let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON);
assert!(padded.contains(&a)); assert!(padded.contains(&a));
assert!(!a.contains(&padded)); assert!(!a.contains(&padded));
} }
@ -718,7 +721,7 @@ mod bounding_sphere_tests {
fn shrink() { fn shrink() {
let a = BoundingSphere::new(Vec3::ONE, 5.); let a = BoundingSphere::new(Vec3::ONE, 5.);
let shrunk = a.shrink(0.5); let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);
assert!(a.contains(&shrunk)); assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a)); assert!(!shrunk.contains(&a));
} }
@ -727,7 +730,7 @@ mod bounding_sphere_tests {
fn scale_around_center() { fn scale_around_center() {
let a = BoundingSphere::new(Vec3::ONE, 5.); let a = BoundingSphere::new(Vec3::ONE, 5.);
let scaled = a.scale_around_center(2.); let scaled = a.scale_around_center(2.);
assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);
assert!(!a.contains(&scaled)); assert!(!a.contains(&scaled));
assert!(scaled.contains(&a)); assert!(scaled.contains(&a));
} }

View file

@ -4,12 +4,15 @@ use crate::{
bounding::{Bounded2d, BoundingCircle}, bounding::{Bounded2d, BoundingCircle},
ops, ops,
primitives::{ primitives::{
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
}, },
Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A, Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A,
}; };
#[cfg(feature = "alloc")]
use crate::primitives::BoxedPolyline3d;
use super::{Aabb3d, Bounded3d, BoundingSphere}; use super::{Aabb3d, Bounded3d, BoundingSphere};
impl Bounded3d for Sphere { impl Bounded3d for Sphere {
@ -98,6 +101,7 @@ impl<const N: usize> Bounded3d for Polyline3d<N> {
} }
} }
#[cfg(feature = "alloc")]
impl Bounded3d for BoxedPolyline3d { impl Bounded3d for BoxedPolyline3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d { fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied())
@ -142,7 +146,7 @@ impl Bounded3d for Cylinder {
let bottom = -top; let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d { Aabb3d {
min: isometry.translation + (top - half_size).min(bottom - half_size), min: isometry.translation + (top - half_size).min(bottom - half_size),
@ -193,7 +197,7 @@ impl Bounded3d for Cone {
let bottom = -top; let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d { Aabb3d {
min: isometry.translation + top.min(bottom - self.radius * half_extents), min: isometry.translation + top.min(bottom - self.radius * half_extents),
@ -234,7 +238,7 @@ impl Bounded3d for ConicalFrustum {
let bottom = -top; let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d { Aabb3d {
min: isometry.translation min: isometry.translation
@ -317,7 +321,8 @@ impl Bounded3d for Torus {
// Reference: http://iquilezles.org/articles/diskbbox/ // Reference: http://iquilezles.org/articles/diskbbox/
let normal = isometry.rotation * Vec3A::Y; let normal = isometry.rotation * Vec3A::Y;
let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO); let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO);
let disc_half_size = self.major_radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); let disc_half_size =
self.major_radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
// Expand the disc by the minor radius to get the torus half-size // Expand the disc by the minor radius to get the torus half-size
let half_size = disc_half_size + Vec3A::splat(self.minor_radius); let half_size = disc_half_size + Vec3A::splat(self.minor_radius);

View file

@ -1,5 +1,8 @@
use super::{Aabb2d, BoundingCircle, IntersectsVolume}; use super::{Aabb2d, BoundingCircle, IntersectsVolume};
use crate::{ops::FloatPow, Dir2, Ray2d, Vec2}; use crate::{
ops::{self, FloatPow},
Dir2, Ray2d, Vec2,
};
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -77,10 +80,12 @@ impl RayCast2d {
let projected = offset.dot(*self.ray.direction); let projected = offset.dot(*self.ray.direction);
let closest_point = offset - projected * *self.ray.direction; let closest_point = offset - projected * *self.ray.direction;
let distance_squared = circle.radius().squared() - closest_point.length_squared(); let distance_squared = circle.radius().squared() - closest_point.length_squared();
if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { if distance_squared < 0.
|| ops::copysign(projected.squared(), -projected) < -distance_squared
{
None None
} else { } else {
let toi = -projected - distance_squared.sqrt(); let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max { if toi > self.max {
None None
} else { } else {
@ -224,21 +229,21 @@ mod tests {
4.996, 4.996,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.circle_intersection_at(volume).unwrap(); let actual_distance = test.circle_intersection_at(volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max); let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -263,9 +268,7 @@ mod tests {
] { ] {
assert!( assert!(
!test.intersects(volume), !test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}", "Case:\n Test: {test:?}\n Volume: {volume:?}",
test,
volume,
); );
} }
} }
@ -278,14 +281,17 @@ mod tests {
for max in &[0., 1., 900.] { for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max); let test = RayCast2d::new(*origin, *direction, *max);
let case = format!( assert!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", test.intersects(&volume),
origin, direction, max, "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
); );
assert!(test.intersects(&volume), "{}", case);
let actual_distance = test.circle_intersection_at(&volume); let actual_distance = test.circle_intersection_at(&volume);
assert_eq!(actual_distance, Some(0.), "{}", case); assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
} }
} }
} }
@ -331,21 +337,21 @@ mod tests {
1.414, 1.414,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.aabb_intersection_at(volume).unwrap(); let actual_distance = test.aabb_intersection_at(volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max); let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -370,9 +376,7 @@ mod tests {
] { ] {
assert!( assert!(
!test.intersects(volume), !test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}", "Case:\n Test: {test:?}\n Volume: {volume:?}",
test,
volume,
); );
} }
} }
@ -385,14 +389,17 @@ mod tests {
for max in &[0., 1., 900.] { for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max); let test = RayCast2d::new(*origin, *direction, *max);
let case = format!( assert!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", test.intersects(&volume),
origin, direction, max, "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
); );
assert!(test.intersects(&volume), "{}", case);
let actual_distance = test.aabb_intersection_at(&volume); let actual_distance = test.aabb_intersection_at(&volume);
assert_eq!(actual_distance, Some(0.), "{}", case,); assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
} }
} }
} }
@ -441,22 +448,22 @@ mod tests {
3., 3.,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.aabb_collision_at(*volume).unwrap(); let actual_distance = test.aabb_collision_at(*volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = let inverted_ray =
RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -508,22 +515,22 @@ mod tests {
3.677, 3.677,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.circle_collision_at(*volume).unwrap(); let actual_distance = test.circle_collision_at(*volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = let inverted_ray =
RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
} }

View file

@ -1,5 +1,8 @@
use super::{Aabb3d, BoundingSphere, IntersectsVolume}; use super::{Aabb3d, BoundingSphere, IntersectsVolume};
use crate::{ops::FloatPow, Dir3A, Ray3d, Vec3A}; use crate::{
ops::{self, FloatPow},
Dir3A, Ray3d, Vec3A,
};
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -74,10 +77,12 @@ impl RayCast3d {
let projected = offset.dot(*self.direction); let projected = offset.dot(*self.direction);
let closest_point = offset - projected * *self.direction; let closest_point = offset - projected * *self.direction;
let distance_squared = sphere.radius().squared() - closest_point.length_squared(); let distance_squared = sphere.radius().squared() - closest_point.length_squared();
if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { if distance_squared < 0.
|| ops::copysign(projected.squared(), -projected) < -distance_squared
{
None None
} else { } else {
let toi = -projected - distance_squared.sqrt(); let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max { if toi > self.max {
None None
} else { } else {
@ -236,21 +241,21 @@ mod tests {
4.996, 4.996,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.sphere_intersection_at(volume).unwrap(); let actual_distance = test.sphere_intersection_at(volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max); let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -275,9 +280,7 @@ mod tests {
] { ] {
assert!( assert!(
!test.intersects(volume), !test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}", "Case:\n Test: {test:?}\n Volume: {volume:?}",
test,
volume,
); );
} }
} }
@ -290,14 +293,17 @@ mod tests {
for max in &[0., 1., 900.] { for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max); let test = RayCast3d::new(*origin, *direction, *max);
let case = format!( assert!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", test.intersects(&volume),
origin, direction, max, "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
); );
assert!(test.intersects(&volume), "{}", case);
let actual_distance = test.sphere_intersection_at(&volume); let actual_distance = test.sphere_intersection_at(&volume);
assert_eq!(actual_distance, Some(0.), "{}", case,); assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
} }
} }
} }
@ -343,21 +349,21 @@ mod tests {
1.732, 1.732,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.aabb_intersection_at(volume).unwrap(); let actual_distance = test.aabb_intersection_at(volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max); let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -382,9 +388,7 @@ mod tests {
] { ] {
assert!( assert!(
!test.intersects(volume), !test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}", "Case:\n Test: {test:?}\n Volume: {volume:?}",
test,
volume,
); );
} }
} }
@ -397,14 +401,17 @@ mod tests {
for max in &[0., 1., 900.] { for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max); let test = RayCast3d::new(*origin, *direction, *max);
let case = format!( assert!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}", test.intersects(&volume),
origin, direction, max, "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
); );
assert!(test.intersects(&volume), "{}", case);
let actual_distance = test.aabb_intersection_at(&volume); let actual_distance = test.aabb_intersection_at(&volume);
assert_eq!(actual_distance, Some(0.), "{}", case,); assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
} }
} }
} }
@ -453,21 +460,21 @@ mod tests {
3., 3.,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.aabb_collision_at(*volume).unwrap(); let actual_distance = test.aabb_collision_at(*volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max); let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
@ -519,21 +526,21 @@ mod tests {
3.677, 3.677,
), ),
] { ] {
let case = format!( assert!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", test.intersects(volume),
test, volume, expected_distance "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
); );
assert!(test.intersects(volume), "{}", case);
let actual_distance = test.sphere_collision_at(*volume).unwrap(); let actual_distance = test.sphere_collision_at(*volume).unwrap();
assert!( assert!(
(actual_distance - expected_distance).abs() < EPSILON, ops::abs(actual_distance - expected_distance) < EPSILON,
"{}\n Actual distance: {}", "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
case,
actual_distance
); );
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max); let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(!inverted_ray.intersects(volume), "{}", case); assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
} }
} }
} }

View file

@ -157,7 +157,7 @@ impl NormedVectorSpace for Vec2 {
impl NormedVectorSpace for f32 { impl NormedVectorSpace for f32 {
#[inline] #[inline]
fn norm(self) -> f32 { fn norm(self) -> f32 {
self.abs() ops::abs(self)
} }
#[inline] #[inline]

View file

@ -140,7 +140,7 @@ mod test_compass_quadrant {
#[test] #[test]
fn test_cardinal_directions() { fn test_cardinal_directions() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(1.0, 0.0)).unwrap(), Dir2::new(Vec2::new(1.0, 0.0)).unwrap(),
CompassQuadrant::East, CompassQuadrant::East,
@ -166,7 +166,7 @@ mod test_compass_quadrant {
#[test] #[test]
fn test_north_pie_slice() { fn test_north_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(), Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
CompassQuadrant::North, CompassQuadrant::North,
@ -184,7 +184,7 @@ mod test_compass_quadrant {
#[test] #[test]
fn test_east_pie_slice() { fn test_east_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), Dir2::new(Vec2::new(0.9, 0.1)).unwrap(),
CompassQuadrant::East, CompassQuadrant::East,
@ -202,7 +202,7 @@ mod test_compass_quadrant {
#[test] #[test]
fn test_south_pie_slice() { fn test_south_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(), Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
CompassQuadrant::South, CompassQuadrant::South,
@ -220,7 +220,7 @@ mod test_compass_quadrant {
#[test] #[test]
fn test_west_pie_slice() { fn test_west_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(), Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
CompassQuadrant::West, CompassQuadrant::West,
@ -243,7 +243,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_cardinal_directions() { fn test_cardinal_directions() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(), Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(),
CompassOctant::NorthWest, CompassOctant::NorthWest,
@ -282,7 +282,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_north_pie_slice() { fn test_north_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(), Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
CompassOctant::North, CompassOctant::North,
@ -300,7 +300,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_north_east_pie_slice() { fn test_north_east_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(0.4, 0.6)).unwrap(), Dir2::new(Vec2::new(0.4, 0.6)).unwrap(),
CompassOctant::NorthEast, CompassOctant::NorthEast,
@ -318,7 +318,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_east_pie_slice() { fn test_east_pie_slice() {
let tests = vec![ let tests = [
(Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East), (Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East),
( (
Dir2::new(Vec2::new(0.9, -0.1)).unwrap(), Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
@ -333,7 +333,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_south_east_pie_slice() { fn test_south_east_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(0.4, -0.6)).unwrap(), Dir2::new(Vec2::new(0.4, -0.6)).unwrap(),
CompassOctant::SouthEast, CompassOctant::SouthEast,
@ -351,7 +351,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_south_pie_slice() { fn test_south_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(), Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
CompassOctant::South, CompassOctant::South,
@ -369,7 +369,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_south_west_pie_slice() { fn test_south_west_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(), Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(),
CompassOctant::SouthWest, CompassOctant::SouthWest,
@ -387,7 +387,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_west_pie_slice() { fn test_west_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(), Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
CompassOctant::West, CompassOctant::West,
@ -405,7 +405,7 @@ mod test_compass_octant {
#[test] #[test]
fn test_north_west_pie_slice() { fn test_north_west_pie_slice() {
let tests = vec![ let tests = [
( (
Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(), Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(),
CompassOctant::NorthWest, CompassOctant::NorthWest,

View file

@ -1,11 +1,16 @@
//! Provides types for building cubic splines for rendering curves and use with animation easing. //! Provides types for building cubic splines for rendering curves and use with animation easing.
use core::{fmt::Debug, iter::once}; use core::fmt::Debug;
use crate::{ops::FloatPow, Vec2, VectorSpace}; use crate::{
ops::{self, FloatPow},
Vec2, VectorSpace,
};
use derive_more::derive::{Display, Error}; use derive_more::derive::{Display, Error};
use itertools::Itertools;
#[cfg(feature = "alloc")]
use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
#[cfg(feature = "curve")] #[cfg(feature = "curve")]
use crate::curve::{Curve, Interval}; use crate::curve::{Curve, Interval};
@ -47,6 +52,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// let bezier = CubicBezier::new(points).to_curve().unwrap(); /// let bezier = CubicBezier::new(points).to_curve().unwrap();
/// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// let positions: Vec<_> = bezier.iter_positions(100).collect();
/// ``` /// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBezier<P: VectorSpace> { pub struct CubicBezier<P: VectorSpace> {
@ -54,6 +60,7 @@ pub struct CubicBezier<P: VectorSpace> {
pub control_points: Vec<[P; 4]>, pub control_points: Vec<[P; 4]>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBezier<P> { impl<P: VectorSpace> CubicBezier<P> {
/// Create a new cubic Bezier curve from sets of control points. /// Create a new cubic Bezier curve from sets of control points.
pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self { pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self {
@ -62,6 +69,8 @@ impl<P: VectorSpace> CubicBezier<P> {
} }
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> { impl<P: VectorSpace> CubicGenerator<P> for CubicBezier<P> {
type Error = CubicBezierError; type Error = CubicBezierError;
@ -140,12 +149,15 @@ pub struct CubicBezierError;
/// ``` /// ```
/// ///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicHermite<P: VectorSpace> { pub struct CubicHermite<P: VectorSpace> {
/// The control points of the Hermite curve. /// The control points of the Hermite curve.
pub control_points: Vec<(P, P)>, pub control_points: Vec<(P, P)>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicHermite<P> { impl<P: VectorSpace> CubicHermite<P> {
/// Create a new Hermite curve from sets of control points. /// Create a new Hermite curve from sets of control points.
pub fn new( pub fn new(
@ -172,6 +184,8 @@ impl<P: VectorSpace> CubicHermite<P> {
] ]
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> { impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -196,6 +210,8 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
} }
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> { impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -258,6 +274,7 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
/// ``` /// ```
/// ///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicCardinalSpline<P: VectorSpace> { pub struct CubicCardinalSpline<P: VectorSpace> {
@ -267,6 +284,7 @@ pub struct CubicCardinalSpline<P: VectorSpace> {
pub control_points: Vec<P>, pub control_points: Vec<P>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicCardinalSpline<P> { impl<P: VectorSpace> CubicCardinalSpline<P> {
/// Build a new Cardinal spline. /// Build a new Cardinal spline.
pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self { pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self {
@ -299,6 +317,8 @@ impl<P: VectorSpace> CubicCardinalSpline<P> {
] ]
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> { impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -335,6 +355,8 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
Ok(CubicCurve { segments }) Ok(CubicCurve { segments })
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> { impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -411,12 +433,15 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
/// ``` /// ```
/// ///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBSpline<P: VectorSpace> { pub struct CubicBSpline<P: VectorSpace> {
/// The control points of the spline /// The control points of the spline
pub control_points: Vec<P>, pub control_points: Vec<P>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBSpline<P> { impl<P: VectorSpace> CubicBSpline<P> {
/// Build a new B-Spline. /// Build a new B-Spline.
pub fn new(control_points: impl Into<Vec<P>>) -> Self { pub fn new(control_points: impl Into<Vec<P>>) -> Self {
@ -448,6 +473,8 @@ impl<P: VectorSpace> CubicBSpline<P> {
char_matrix char_matrix
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> { impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -470,6 +497,7 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicBSpline<P> { impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicBSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -578,6 +606,7 @@ pub enum CubicNurbsError {
/// .unwrap(); /// .unwrap();
/// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect();
/// ``` /// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicNurbs<P: VectorSpace> { pub struct CubicNurbs<P: VectorSpace> {
@ -588,6 +617,8 @@ pub struct CubicNurbs<P: VectorSpace> {
/// Knots /// Knots
pub knots: Vec<f32>, pub knots: Vec<f32>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicNurbs<P> { impl<P: VectorSpace> CubicNurbs<P> {
/// Build a Non-Uniform Rational B-Spline. /// Build a Non-Uniform Rational B-Spline.
/// ///
@ -748,6 +779,8 @@ impl<P: VectorSpace> CubicNurbs<P> {
] ]
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> { impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -802,12 +835,15 @@ impl<P: VectorSpace> RationalGenerator<P> for CubicNurbs<P> {
/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first. /// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first.
/// ///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic /// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct LinearSpline<P: VectorSpace> { pub struct LinearSpline<P: VectorSpace> {
/// The control points of the linear spline. /// The control points of the linear spline.
pub points: Vec<P>, pub points: Vec<P>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> LinearSpline<P> { impl<P: VectorSpace> LinearSpline<P> {
/// Create a new linear spline from a list of points to be interpolated. /// Create a new linear spline from a list of points to be interpolated.
pub fn new(points: impl Into<Vec<P>>) -> Self { pub fn new(points: impl Into<Vec<P>>) -> Self {
@ -816,6 +852,8 @@ impl<P: VectorSpace> LinearSpline<P> {
} }
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> { impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -843,6 +881,8 @@ impl<P: VectorSpace> CubicGenerator<P> for LinearSpline<P> {
} }
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> { impl<P: VectorSpace> CyclicCubicGenerator<P> for LinearSpline<P> {
type Error = InsufficientDataError; type Error = InsufficientDataError;
@ -877,6 +917,7 @@ pub struct InsufficientDataError {
} }
/// Implement this on cubic splines that can generate a cubic curve from their spline parameters. /// Implement this on cubic splines that can generate a cubic curve from their spline parameters.
#[cfg(feature = "alloc")]
pub trait CubicGenerator<P: VectorSpace> { pub trait CubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail. /// An error type indicating why construction might fail.
type Error; type Error;
@ -888,6 +929,7 @@ pub trait CubicGenerator<P: VectorSpace> {
/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters. /// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters.
/// ///
/// This makes sense only when the control data can be interpreted cyclically. /// This makes sense only when the control data can be interpreted cyclically.
#[cfg(feature = "alloc")]
pub trait CyclicCubicGenerator<P: VectorSpace> { pub trait CyclicCubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail. /// An error type indicating why construction might fail.
type Error; type Error;
@ -937,6 +979,7 @@ impl<P: VectorSpace> CubicSegment<P> {
} }
/// Calculate polynomial coefficients for the cubic curve using a characteristic matrix. /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.
#[allow(unused)]
#[inline] #[inline]
fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self { fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
let [c0, c1, c2, c3] = char_matrix; let [c0, c1, c2, c3] = char_matrix;
@ -964,6 +1007,7 @@ impl CubicSegment<Vec2> {
/// ///
/// This is a very common tool for UI animations that accelerate and decelerate smoothly. For /// This is a very common tool for UI animations that accelerate and decelerate smoothly. For
/// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`.
#[cfg(feature = "alloc")]
pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self { pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
let (p0, p3) = (Vec2::ZERO, Vec2::ONE); let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]) let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]])
@ -984,9 +1028,12 @@ impl CubicSegment<Vec2> {
/// ///
/// ``` /// ```
/// # use bevy_math::prelude::*; /// # use bevy_math::prelude::*;
/// # #[cfg(feature = "alloc")]
/// # {
/// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0)); /// let cubic_bezier = CubicSegment::new_bezier((0.25, 0.1), (0.25, 1.0));
/// assert_eq!(cubic_bezier.ease(0.0), 0.0); /// assert_eq!(cubic_bezier.ease(0.0), 0.0);
/// assert_eq!(cubic_bezier.ease(1.0), 1.0); /// assert_eq!(cubic_bezier.ease(1.0), 1.0);
/// # }
/// ``` /// ```
/// ///
/// # How cubic easing works /// # How cubic easing works
@ -1050,7 +1097,7 @@ impl CubicSegment<Vec2> {
for _ in 0..Self::MAX_ITERS { for _ in 0..Self::MAX_ITERS {
pos_guess = self.position(t_guess); pos_guess = self.position(t_guess);
let error = pos_guess.x - x; let error = pos_guess.x - x;
if error.abs() <= Self::MAX_ERROR { if ops::abs(error) <= Self::MAX_ERROR {
break; break;
} }
// Using Newton's method, use the tangent line to estimate a better guess value. // Using Newton's method, use the tangent line to estimate a better guess value.
@ -1079,6 +1126,7 @@ impl<P: VectorSpace> Curve<P> for CubicSegment<P> {
/// ///
/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as /// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as
/// [`CubicBezier`]. /// [`CubicBezier`].
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
@ -1087,6 +1135,7 @@ pub struct CubicCurve<P: VectorSpace> {
segments: Vec<CubicSegment<P>>, segments: Vec<CubicSegment<P>>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicCurve<P> { impl<P: VectorSpace> CubicCurve<P> {
/// Create a new curve from a collection of segments. If the collection of segments is empty, /// Create a new curve from a collection of segments. If the collection of segments is empty,
/// a curve cannot be built and `None` will be returned instead. /// a curve cannot be built and `None` will be returned instead.
@ -1193,13 +1242,13 @@ impl<P: VectorSpace> CubicCurve<P> {
if self.segments.len() == 1 { if self.segments.len() == 1 {
(&self.segments[0], t) (&self.segments[0], t)
} else { } else {
let i = (t.floor() as usize).clamp(0, self.segments.len() - 1); let i = (ops::floor(t) as usize).clamp(0, self.segments.len() - 1);
(&self.segments[i], t - i as f32) (&self.segments[i], t - i as f32)
} }
} }
} }
#[cfg(feature = "curve")] #[cfg(all(feature = "curve", feature = "alloc"))]
impl<P: VectorSpace> Curve<P> for CubicCurve<P> { impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
#[inline] #[inline]
fn domain(&self) -> Interval { fn domain(&self) -> Interval {
@ -1214,12 +1263,14 @@ impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> { impl<P: VectorSpace> Extend<CubicSegment<P>> for CubicCurve<P> {
fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) { fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter); self.segments.extend(iter);
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for CubicCurve<P> { impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter; type IntoIter = <Vec<CubicSegment<P>> as IntoIterator>::IntoIter;
@ -1231,6 +1282,7 @@ impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
} }
/// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters. /// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters.
#[cfg(feature = "alloc")]
pub trait RationalGenerator<P: VectorSpace> { pub trait RationalGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail. /// An error type indicating why construction might fail.
type Error; type Error;
@ -1334,6 +1386,7 @@ impl<P: VectorSpace> RationalSegment<P> {
} }
/// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix. /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.
#[allow(unused)]
#[inline] #[inline]
fn coefficients( fn coefficients(
control_points: [P; 4], control_points: [P; 4],
@ -1389,6 +1442,7 @@ impl<P: VectorSpace> Curve<P> for RationalSegment<P> {
/// ///
/// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as /// Use any struct that implements the [`RationalGenerator`] trait to create a new curve, such as
/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`. /// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
@ -1397,6 +1451,7 @@ pub struct RationalCurve<P: VectorSpace> {
segments: Vec<RationalSegment<P>>, segments: Vec<RationalSegment<P>>,
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> RationalCurve<P> { impl<P: VectorSpace> RationalCurve<P> {
/// Create a new curve from a collection of segments. If the collection of segments is empty, /// Create a new curve from a collection of segments. If the collection of segments is empty,
/// a curve cannot be built and `None` will be returned instead. /// a curve cannot be built and `None` will be returned instead.
@ -1528,7 +1583,7 @@ impl<P: VectorSpace> RationalCurve<P> {
} }
} }
#[cfg(feature = "curve")] #[cfg(all(feature = "curve", feature = "alloc"))]
impl<P: VectorSpace> Curve<P> for RationalCurve<P> { impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
#[inline] #[inline]
fn domain(&self) -> Interval { fn domain(&self) -> Interval {
@ -1543,12 +1598,14 @@ impl<P: VectorSpace> Curve<P> for RationalCurve<P> {
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> { impl<P: VectorSpace> Extend<RationalSegment<P>> for RationalCurve<P> {
fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) { fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter); self.segments.extend(iter);
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for RationalCurve<P> { impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter; type IntoIter = <Vec<RationalSegment<P>> as IntoIterator>::IntoIter;
@ -1569,6 +1626,7 @@ impl<P: VectorSpace> From<CubicSegment<P>> for RationalSegment<P> {
} }
} }
#[cfg(feature = "alloc")]
impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> { impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
fn from(value: CubicCurve<P>) -> Self { fn from(value: CubicCurve<P>) -> Self {
Self { Self {
@ -1577,10 +1635,9 @@ impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
} }
} }
#[cfg(feature = "alloc")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use glam::{vec2, Vec2};
use crate::{ use crate::{
cubic_splines::{ cubic_splines::{
CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve, CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
@ -1588,6 +1645,8 @@ mod tests {
}, },
ops::{self, FloatPow}, ops::{self, FloatPow},
}; };
use alloc::vec::Vec;
use glam::{vec2, Vec2};
/// How close two floats can be and still be considered equal /// How close two floats can be and still be considered equal
const FLOAT_EQ: f32 = 1e-5; const FLOAT_EQ: f32 = 1e-5;
@ -1760,7 +1819,7 @@ mod tests {
let curve = spline.to_curve().unwrap(); let curve = spline.to_curve().unwrap();
for (i, point) in curve.iter_positions(10).enumerate() { for (i, point) in curve.iter_positions(10).enumerate() {
assert!( assert!(
f32::abs(point.length() - 1.0) < EPSILON, ops::abs(point.length() - 1.0) < EPSILON,
"Point {i} is not on the unit circle: {point:?} has length {}", "Point {i} is not on the unit circle: {point:?} has length {}",
point.length() point.length()
); );

View file

@ -3,6 +3,7 @@
use super::interval::*; use super::interval::*;
use super::Curve; use super::Curve;
use crate::ops;
use crate::VectorSpace; use crate::VectorSpace;
use core::any::type_name; use core::any::type_name;
use core::fmt::{self, Debug}; use core::fmt::{self, Debug};
@ -621,7 +622,7 @@ where
fn sample_unchecked(&self, t: f32) -> T { fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction // the domain is bounded by construction
let d = self.curve.domain(); let d = self.curve.domain();
let cyclic_t = (t - d.start()).rem_euclid(d.length()); let cyclic_t = ops::rem_euclid(t - d.start(), d.length());
let t = if t != d.start() && cyclic_t == 0.0 { let t = if t != d.start() && cyclic_t == 0.0 {
d.end() d.end()
} else { } else {
@ -669,7 +670,7 @@ where
fn sample_unchecked(&self, t: f32) -> T { fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction // the domain is bounded by construction
let d = self.curve.domain(); let d = self.curve.domain();
let cyclic_t = (t - d.start()).rem_euclid(d.length()); let cyclic_t = ops::rem_euclid(t - d.start(), d.length());
let t = if t != d.start() && cyclic_t == 0.0 { let t = if t != d.start() && cyclic_t == 0.0 {
d.end() d.end()
} else { } else {

View file

@ -6,10 +6,14 @@
//! provided methods all maintain the invariants, so this is only a concern if you manually mutate //! provided methods all maintain the invariants, so this is only a concern if you manually mutate
//! the fields. //! the fields.
use crate::ops;
use super::interval::Interval; use super::interval::Interval;
use core::fmt::Debug; use core::fmt::Debug;
use derive_more::derive::{Display, Error}; use derive_more::derive::{Display, Error};
use itertools::Itertools;
#[cfg(feature = "alloc")]
use {alloc::vec::Vec, itertools::Itertools};
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -112,6 +116,7 @@ impl<T> InterpolationDatum<T> {
/// } /// }
/// } /// }
/// ``` /// ```
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -146,6 +151,7 @@ pub enum EvenCoreError {
UnboundedDomain, UnboundedDomain,
} }
#[cfg(feature = "alloc")]
impl<T> EvenCore<T> { impl<T> EvenCore<T> {
/// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are /// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are
/// regarded to be evenly spaced within the given domain interval, so that the outermost /// regarded to be evenly spaced within the given domain interval, so that the outermost
@ -243,11 +249,11 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat
// To the right side of all the samples // To the right side of all the samples
InterpolationDatum::RightTail(samples - 1) InterpolationDatum::RightTail(samples - 1)
} else { } else {
let lower_index = steps_taken.floor() as usize; let lower_index = ops::floor(steps_taken) as usize;
// This upper index is always valid because `steps_taken` is a finite value // This upper index is always valid because `steps_taken` is a finite value
// strictly less than `samples - 1`, so its floor is at most `samples - 2` // strictly less than `samples - 1`, so its floor is at most `samples - 2`
let upper_index = lower_index + 1; let upper_index = lower_index + 1;
let s = steps_taken.fract(); let s = ops::fract(steps_taken);
InterpolationDatum::Between(lower_index, upper_index, s) InterpolationDatum::Between(lower_index, upper_index, s)
} }
} }
@ -314,6 +320,7 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat
/// [`domain`]: UnevenCore::domain /// [`domain`]: UnevenCore::domain
/// [`sample_with`]: UnevenCore::sample_with /// [`sample_with`]: UnevenCore::sample_with
/// [the provided constructor]: UnevenCore::new /// [the provided constructor]: UnevenCore::new
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -346,6 +353,7 @@ pub enum UnevenCoreError {
}, },
} }
#[cfg(feature = "alloc")]
impl<T> UnevenCore<T> { impl<T> UnevenCore<T> {
/// Create a new [`UnevenCore`]. The given samples are filtered to finite times and /// Create a new [`UnevenCore`]. The given samples are filtered to finite times and
/// sorted internally; if there are not at least 2 valid timed samples, an error will be /// sorted internally; if there are not at least 2 valid timed samples, an error will be
@ -453,6 +461,7 @@ impl<T> UnevenCore<T> {
/// if the sample type can effectively be encoded as a fixed-length slice of values. /// if the sample type can effectively be encoded as a fixed-length slice of values.
/// ///
/// [sampling width]: ChunkedUnevenCore::width /// [sampling width]: ChunkedUnevenCore::width
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -507,6 +516,7 @@ pub enum ChunkedUnevenCoreError {
}, },
} }
#[cfg(feature = "alloc")]
impl<T> ChunkedUnevenCore<T> { impl<T> ChunkedUnevenCore<T> {
/// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times, /// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times,
/// and deduplicated. See the [type-level documentation] for more information about this type. /// and deduplicated. See the [type-level documentation] for more information about this type.
@ -644,6 +654,7 @@ impl<T> ChunkedUnevenCore<T> {
} }
/// Sort the given times, deduplicate them, and filter them to only finite times. /// Sort the given times, deduplicate them, and filter them to only finite times.
#[cfg(feature = "alloc")]
fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> { fn filter_sort_dedup_times(times: impl IntoIterator<Item = f32>) -> Vec<f32> {
// Filter before sorting/deduplication so that NAN doesn't interfere with them. // Filter before sorting/deduplication so that NAN doesn't interfere with them.
let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec(); let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec();
@ -682,7 +693,7 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> {
} }
} }
#[cfg(test)] #[cfg(all(test, feature = "alloc"))]
mod tests { mod tests {
use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; use super::{ChunkedUnevenCore, EvenCore, UnevenCore};
use crate::curve::{cores::InterpolationDatum, interval}; use crate::curve::{cores::InterpolationDatum, interval};

View file

@ -309,18 +309,18 @@ mod easing_functions {
#[inline] #[inline]
pub(crate) fn circular_in(t: f32) -> f32 { pub(crate) fn circular_in(t: f32) -> f32 {
1.0 - (1.0 - t.squared()).sqrt() 1.0 - ops::sqrt(1.0 - t.squared())
} }
#[inline] #[inline]
pub(crate) fn circular_out(t: f32) -> f32 { pub(crate) fn circular_out(t: f32) -> f32 {
(1.0 - (t - 1.0).squared()).sqrt() ops::sqrt(1.0 - (t - 1.0).squared())
} }
#[inline] #[inline]
pub(crate) fn circular_in_out(t: f32) -> f32 { pub(crate) fn circular_in_out(t: f32) -> f32 {
if t < 0.5 { if t < 0.5 {
(1.0 - (1.0 - (2.0 * t).squared()).sqrt()) / 2.0 (1.0 - ops::sqrt(1.0 - (2.0 * t).squared())) / 2.0
} else { } else {
((1.0 - (-2.0 * t + 2.0).squared()).sqrt() + 1.0) / 2.0 (ops::sqrt(1.0 - (-2.0 * t + 2.0).squared()) + 1.0) / 2.0
} }
} }
@ -411,7 +411,7 @@ mod easing_functions {
#[inline] #[inline]
pub(crate) fn steps(num_steps: usize, t: f32) -> f32 { pub(crate) fn steps(num_steps: usize, t: f32) -> f32 {
(t * num_steps as f32).round() / num_steps.max(1) as f32 ops::round(t * num_steps as f32) / num_steps.max(1) as f32
} }
#[inline] #[inline]

View file

@ -198,6 +198,8 @@ pub fn interval(start: f32, end: f32) -> Result<Interval, InvalidIntervalError>
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ops;
use super::*; use super::*;
use approx::{assert_abs_diff_eq, AbsDiffEq}; use approx::{assert_abs_diff_eq, AbsDiffEq};
@ -237,10 +239,10 @@ mod tests {
#[test] #[test]
fn lengths() { fn lengths() {
let ivl = interval(-5.0, 10.0).unwrap(); let ivl = interval(-5.0, 10.0).unwrap();
assert!((ivl.length() - 15.0).abs() <= f32::EPSILON); assert!(ops::abs(ivl.length() - 15.0) <= f32::EPSILON);
let ivl = interval(5.0, 100.0).unwrap(); let ivl = interval(5.0, 100.0).unwrap();
assert!((ivl.length() - 95.0).abs() <= f32::EPSILON); assert!(ops::abs(ivl.length() - 95.0) <= f32::EPSILON);
let ivl = interval(0.0, f32::INFINITY).unwrap(); let ivl = interval(0.0, f32::INFINITY).unwrap();
assert_eq!(ivl.length(), f32::INFINITY); assert_eq!(ivl.length(), f32::INFINITY);

View file

@ -1,7 +1,10 @@
//! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like //! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like
//! output whose length cannot be known statically. //! output whose length cannot be known statically.
use super::{ConstantCurve, Interval}; use super::Interval;
#[cfg(feature = "alloc")]
use {super::ConstantCurve, alloc::vec::Vec};
/// A curve which provides samples in the form of [`Iterator`]s. /// A curve which provides samples in the form of [`Iterator`]s.
/// ///
@ -39,6 +42,7 @@ pub trait IterableCurve<T> {
} }
} }
#[cfg(feature = "alloc")]
impl<T> IterableCurve<T> for ConstantCurve<Vec<T>> impl<T> IterableCurve<T> for ConstantCurve<Vec<T>>
where where
T: Clone, T: Clone,

View file

@ -290,21 +290,27 @@ pub mod cores;
pub mod easing; pub mod easing;
pub mod interval; pub mod interval;
pub mod iterable; pub mod iterable;
#[cfg(feature = "alloc")]
pub mod sample_curves; pub mod sample_curves;
// bevy_math::curve re-exports all commonly-needed curve-related items. // bevy_math::curve re-exports all commonly-needed curve-related items.
pub use adaptors::*; pub use adaptors::*;
pub use easing::*; pub use easing::*;
pub use interval::{interval, Interval}; pub use interval::{interval, Interval};
pub use sample_curves::*;
use cores::{EvenCore, UnevenCore}; #[cfg(feature = "alloc")]
pub use {
crate::StableInterpolate,
cores::{EvenCore, UnevenCore},
itertools::Itertools,
sample_curves::*,
};
use crate::{StableInterpolate, VectorSpace}; use crate::VectorSpace;
use core::{marker::PhantomData, ops::Deref}; use core::{marker::PhantomData, ops::Deref};
use derive_more::derive::{Display, Error}; use derive_more::derive::{Display, Error};
use interval::InvalidIntervalError; use interval::InvalidIntervalError;
use itertools::Itertools;
/// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// A trait for a type that can represent values of type `T` parametrized over a fixed interval.
/// ///
@ -716,6 +722,7 @@ pub trait Curve<T> {
/// // A curve which only stores three data points and uses `nlerp` to interpolate them: /// // A curve which only stores three data points and uses `nlerp` to interpolate them:
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t)); /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
/// ``` /// ```
#[cfg(feature = "alloc")]
fn resample<I>( fn resample<I>(
&self, &self,
segments: usize, segments: usize,
@ -743,6 +750,7 @@ pub trait Curve<T> {
/// domain, then a [`ResamplingError`] is returned. /// domain, then a [`ResamplingError`] is returned.
/// ///
/// [automatic interpolation]: crate::common_traits::StableInterpolate /// [automatic interpolation]: crate::common_traits::StableInterpolate
#[cfg(feature = "alloc")]
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError> fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
where where
Self: Sized, Self: Sized,
@ -794,6 +802,7 @@ pub trait Curve<T> {
/// The interpolation takes two values by reference together with a scalar parameter and /// The interpolation takes two values by reference together with a scalar parameter and
/// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
#[cfg(feature = "alloc")]
fn resample_uneven<I>( fn resample_uneven<I>(
&self, &self,
sample_times: impl IntoIterator<Item = f32>, sample_times: impl IntoIterator<Item = f32>,
@ -832,6 +841,7 @@ pub trait Curve<T> {
/// sample times of the iterator. /// sample times of the iterator.
/// ///
/// [automatic interpolation]: crate::common_traits::StableInterpolate /// [automatic interpolation]: crate::common_traits::StableInterpolate
#[cfg(feature = "alloc")]
fn resample_uneven_auto( fn resample_uneven_auto(
&self, &self,
sample_times: impl IntoIterator<Item = f32>, sample_times: impl IntoIterator<Item = f32>,

View file

@ -356,6 +356,7 @@ impl<T> UnevenSampleAutoCurve<T> {
} }
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "bevy_reflect")]
mod tests { mod tests {
//! These tests should guarantee (by even compiling) that `SampleCurve` and `UnevenSampleCurve` //! These tests should guarantee (by even compiling) that `SampleCurve` and `UnevenSampleCurve`
//! can be `Reflect` under reasonable circumstances where their interpolation is defined by: //! can be `Reflect` under reasonable circumstances where their interpolation is defined by:

View file

@ -55,17 +55,23 @@ impl core::fmt::Display for InvalidDirectionError {
/// and similarly for the error. /// and similarly for the error.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn assert_is_normalized(message: &str, length_squared: f32) { fn assert_is_normalized(message: &str, length_squared: f32) {
let length_error_squared = (length_squared - 1.0).abs(); use crate::ops;
let length_error_squared = ops::abs(length_squared - 1.0);
// Panic for large error and warn for slight error. // Panic for large error and warn for slight error.
if length_error_squared > 2e-2 || length_error_squared.is_nan() { if length_error_squared > 2e-2 || length_error_squared.is_nan() {
// Length error is approximately 1e-2 or more. // Length error is approximately 1e-2 or more.
panic!("Error: {message} The length is {}.", length_squared.sqrt()); panic!(
"Error: {message} The length is {}.",
ops::sqrt(length_squared)
);
} else if length_error_squared > 2e-4 { } else if length_error_squared > 2e-4 {
// Length error is approximately 1e-4 or more. // Length error is approximately 1e-4 or more.
#[cfg(feature = "std")]
eprintln!( eprintln!(
"Warning: {message} The length is {}.", "Warning: {message} The length is {}.",
length_squared.sqrt() ops::sqrt(length_squared)
); );
} }
} }
@ -886,17 +892,17 @@ mod tests {
fn dir2_slerp() { fn dir2_slerp() {
assert_relative_eq!( assert_relative_eq!(
Dir2::X.slerp(Dir2::Y, 0.5), Dir2::X.slerp(Dir2::Y, 0.5),
Dir2::from_xy(0.5_f32.sqrt(), 0.5_f32.sqrt()).unwrap() Dir2::from_xy(ops::sqrt(0.5_f32), ops::sqrt(0.5_f32)).unwrap()
); );
assert_eq!(Dir2::Y.slerp(Dir2::X, 0.0), Dir2::Y); assert_eq!(Dir2::Y.slerp(Dir2::X, 0.0), Dir2::Y);
assert_relative_eq!(Dir2::X.slerp(Dir2::Y, 1.0), Dir2::Y); assert_relative_eq!(Dir2::X.slerp(Dir2::Y, 1.0), Dir2::Y);
assert_relative_eq!( assert_relative_eq!(
Dir2::Y.slerp(Dir2::X, 1.0 / 3.0), Dir2::Y.slerp(Dir2::X, 1.0 / 3.0),
Dir2::from_xy(0.5, 0.75_f32.sqrt()).unwrap() Dir2::from_xy(0.5, ops::sqrt(0.75_f32)).unwrap()
); );
assert_relative_eq!( assert_relative_eq!(
Dir2::X.slerp(Dir2::Y, 2.0 / 3.0), Dir2::X.slerp(Dir2::Y, 2.0 / 3.0),
Dir2::from_xy(0.5, 0.75_f32.sqrt()).unwrap() Dir2::from_xy(0.5, ops::sqrt(0.75_f32)).unwrap()
); );
} }
@ -967,18 +973,18 @@ mod tests {
fn dir3_slerp() { fn dir3_slerp() {
assert_relative_eq!( assert_relative_eq!(
Dir3::X.slerp(Dir3::Y, 0.5), Dir3::X.slerp(Dir3::Y, 0.5),
Dir3::from_xyz(0.5f32.sqrt(), 0.5f32.sqrt(), 0.0).unwrap() Dir3::from_xyz(ops::sqrt(0.5f32), ops::sqrt(0.5f32), 0.0).unwrap()
); );
assert_relative_eq!(Dir3::Y.slerp(Dir3::Z, 0.0), Dir3::Y); assert_relative_eq!(Dir3::Y.slerp(Dir3::Z, 0.0), Dir3::Y);
assert_relative_eq!(Dir3::Z.slerp(Dir3::X, 1.0), Dir3::X, epsilon = 0.000001); assert_relative_eq!(Dir3::Z.slerp(Dir3::X, 1.0), Dir3::X, epsilon = 0.000001);
assert_relative_eq!( assert_relative_eq!(
Dir3::X.slerp(Dir3::Z, 1.0 / 3.0), Dir3::X.slerp(Dir3::Z, 1.0 / 3.0),
Dir3::from_xyz(0.75f32.sqrt(), 0.0, 0.5).unwrap(), Dir3::from_xyz(ops::sqrt(0.75f32), 0.0, 0.5).unwrap(),
epsilon = 0.000001 epsilon = 0.000001
); );
assert_relative_eq!( assert_relative_eq!(
Dir3::Z.slerp(Dir3::Y, 2.0 / 3.0), Dir3::Z.slerp(Dir3::Y, 2.0 / 3.0),
Dir3::from_xyz(0.0, 0.75f32.sqrt(), 0.5).unwrap() Dir3::from_xyz(0.0, ops::sqrt(0.75f32), 0.5).unwrap()
); );
} }
@ -1038,18 +1044,18 @@ mod tests {
fn dir3a_slerp() { fn dir3a_slerp() {
assert_relative_eq!( assert_relative_eq!(
Dir3A::X.slerp(Dir3A::Y, 0.5), Dir3A::X.slerp(Dir3A::Y, 0.5),
Dir3A::from_xyz(0.5f32.sqrt(), 0.5f32.sqrt(), 0.0).unwrap() Dir3A::from_xyz(ops::sqrt(0.5f32), ops::sqrt(0.5f32), 0.0).unwrap()
); );
assert_relative_eq!(Dir3A::Y.slerp(Dir3A::Z, 0.0), Dir3A::Y); assert_relative_eq!(Dir3A::Y.slerp(Dir3A::Z, 0.0), Dir3A::Y);
assert_relative_eq!(Dir3A::Z.slerp(Dir3A::X, 1.0), Dir3A::X, epsilon = 0.000001); assert_relative_eq!(Dir3A::Z.slerp(Dir3A::X, 1.0), Dir3A::X, epsilon = 0.000001);
assert_relative_eq!( assert_relative_eq!(
Dir3A::X.slerp(Dir3A::Z, 1.0 / 3.0), Dir3A::X.slerp(Dir3A::Z, 1.0 / 3.0),
Dir3A::from_xyz(0.75f32.sqrt(), 0.0, 0.5).unwrap(), Dir3A::from_xyz(ops::sqrt(0.75f32), 0.0, 0.5).unwrap(),
epsilon = 0.000001 epsilon = 0.000001
); );
assert_relative_eq!( assert_relative_eq!(
Dir3A::Z.slerp(Dir3A::Y, 2.0 / 3.0), Dir3A::Z.slerp(Dir3A::Y, 2.0 / 3.0),
Dir3A::from_xyz(0.0, 0.75f32.sqrt(), 0.5).unwrap() Dir3A::from_xyz(0.0, ops::sqrt(0.75f32), 0.5).unwrap()
); );
} }

View file

@ -95,8 +95,6 @@ impl Neg for FloatOrd {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::hash::DefaultHasher;
use super::*; use super::*;
const NAN: FloatOrd = FloatOrd(f32::NAN); const NAN: FloatOrd = FloatOrd(f32::NAN);
@ -157,10 +155,11 @@ mod tests {
assert!(ONE >= ZERO); assert!(ONE >= ZERO);
} }
#[cfg(feature = "std")]
#[test] #[test]
fn float_ord_hash() { fn float_ord_hash() {
let hash = |num| { let hash = |num| {
let mut h = DefaultHasher::new(); let mut h = std::hash::DefaultHasher::new();
FloatOrd(num).hash(&mut h); FloatOrd(num).hash(&mut h);
h.finish() h.finish()
}; };

View file

@ -6,6 +6,7 @@
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevyengine.org/assets/icon.png"
)] )]
#![cfg_attr(not(feature = "std"), no_std)]
//! Provides math types and functionality for the Bevy game engine. //! Provides math types and functionality for the Bevy game engine.
//! //!
@ -13,6 +14,9 @@
//! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations //! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations
//! like [`Quat`]. //! like [`Quat`].
#[cfg(feature = "alloc")]
extern crate alloc;
mod affine3; mod affine3;
mod aspect_ratio; mod aspect_ratio;
pub mod bounding; pub mod bounding;
@ -59,11 +63,7 @@ pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{
bvec2, bvec3, bvec3a, bvec4, bvec4a, bvec2, bvec3, bvec3a, bvec4, bvec4a,
cubic_splines::{ cubic_splines::{CubicNurbsError, CubicSegment, RationalSegment},
CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator,
CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator,
RationalCurve, RationalGenerator, RationalSegment,
},
direction::{Dir2, Dir3, Dir3A}, direction::{Dir2, Dir3, Dir3A},
ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops, ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops,
primitives::*, primitives::*,
@ -80,6 +80,13 @@ pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "rand")] #[cfg(feature = "rand")]
pub use crate::sampling::{FromRng, ShapeSample}; pub use crate::sampling::{FromRng, ShapeSample};
#[cfg(feature = "alloc")]
#[doc(hidden)]
pub use crate::cubic_splines::{
CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator, CubicHermite,
CubicNurbs, CyclicCubicGenerator, RationalCurve, RationalGenerator,
};
} }
pub use glam::*; pub use glam::*;

View file

@ -4,6 +4,9 @@
//! //!
//! All the functions here are named according to their versions in the standard //! All the functions here are named according to their versions in the standard
//! library. //! library.
//!
//! It also provides `no_std` compatible alternatives to certain floating-point
//! operations which are not provided in the [`core`] library.
#![allow(dead_code)] #![allow(dead_code)]
#![allow(clippy::disallowed_methods)] #![allow(clippy::disallowed_methods)]
@ -444,12 +447,159 @@ mod libm_ops {
} }
} }
#[cfg(all(feature = "libm", not(feature = "std")))]
mod libm_ops_for_no_std {
//! Provides standardized names for [`f32`] operations which may not be
//! supported on `no_std` platforms.
//! On `no_std` platforms, this forwards to the implementations provided
//! by [`libm`].
/// Calculates the least nonnegative remainder of `self (mod rhs)`.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn rem_euclid(x: f32, y: f32) -> f32 {
let result = libm::remainderf(x, y);
// libm::remainderf has a range of -y/2 to +y/2
if result < 0. {
result + y
} else {
result
}
}
/// Computes the absolute value of x.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn abs(x: f32) -> f32 {
libm::fabsf(x)
}
/// Returns the square root of a number.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn sqrt(x: f32) -> f32 {
libm::sqrtf(x)
}
/// Returns a number composed of the magnitude of `x` and the sign of `y`.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn copysign(x: f32, y: f32) -> f32 {
libm::copysignf(x, y)
}
/// Returns the nearest integer to `x`. If a value is half-way between two integers, round away from `0.0`.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn round(x: f32) -> f32 {
libm::roundf(x)
}
/// Returns the largest integer less than or equal to `x`.
///
/// Precision is specified when the `libm` feature is enabled.
#[inline(always)]
pub fn floor(x: f32) -> f32 {
libm::floorf(x)
}
/// Returns the fractional part of `x`.
///
/// This function always returns the precise result.
#[inline(always)]
pub fn fract(x: f32) -> f32 {
libm::modff(x).0
}
}
#[cfg(feature = "std")]
mod std_ops_for_no_std {
//! Provides standardized names for [`f32`] operations which may not be
//! supported on `no_std` platforms.
//! On `std` platforms, this forwards directly to the implementations provided
//! by [`std`].
/// Calculates the least nonnegative remainder of `x (mod y)`.
///
/// The result of this operation is guaranteed to be the rounded infinite-precision result.
#[inline(always)]
pub fn rem_euclid(x: f32, y: f32) -> f32 {
f32::rem_euclid(x, y)
}
/// Computes the absolute value of x.
///
/// This function always returns the precise result.
#[inline(always)]
pub fn abs(x: f32) -> f32 {
f32::abs(x)
}
/// Returns the square root of a number.
///
/// The result of this operation is guaranteed to be the rounded infinite-precision result.
/// It is specified by IEEE 754 as `squareRoot` and guaranteed not to change.
#[inline(always)]
pub fn sqrt(x: f32) -> f32 {
f32::sqrt(x)
}
/// Returns a number composed of the magnitude of `x` and the sign of `y`.
///
/// Equal to `x` if the sign of `x` and `y` are the same, otherwise equal to `-x`. If `x` is a
/// `NaN`, then a `NaN` with the sign bit of `y` is returned. Note, however, that conserving the
/// sign bit on `NaN` across arithmetical operations is not generally guaranteed.
#[inline(always)]
pub fn copysign(x: f32, y: f32) -> f32 {
f32::copysign(x, y)
}
/// Returns the nearest integer to `x`. If a value is half-way between two integers, round away from `0.0`.
///
/// This function always returns the precise result.
#[inline(always)]
pub fn round(x: f32) -> f32 {
f32::round(x)
}
/// Returns the largest integer less than or equal to `x`.
///
/// This function always returns the precise result.
#[inline(always)]
pub fn floor(x: f32) -> f32 {
f32::floor(x)
}
/// Returns the fractional part of `x`.
///
/// This function always returns the precise result.
#[inline(always)]
pub fn fract(x: f32) -> f32 {
f32::fract(x)
}
}
#[cfg(feature = "libm")] #[cfg(feature = "libm")]
pub use libm_ops::*; pub use libm_ops::*;
#[cfg(not(feature = "libm"))] #[cfg(not(feature = "libm"))]
pub use std_ops::*; pub use std_ops::*;
#[cfg(feature = "std")]
pub use std_ops_for_no_std::*;
#[cfg(all(feature = "libm", not(feature = "std")))]
pub use libm_ops_for_no_std::*;
#[cfg(all(not(feature = "libm"), not(feature = "std")))]
compile_error!("Either the `libm` feature or the `std` feature must be enabled.");
/// This extension trait covers shortfall in determinacy from the lack of a `libm` counterpart /// This extension trait covers shortfall in determinacy from the lack of a `libm` counterpart
/// to `f32::powi`. Use this for the common small exponents. /// to `f32::powi`. Use this for the common small exponents.
pub trait FloatPow { pub trait FloatPow {

View file

@ -12,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] #[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[cfg(feature = "alloc")]
use alloc::{boxed::Box, vec::Vec};
/// A circle primitive, representing the set of points some distance from the origin /// A circle primitive, representing the set of points some distance from the origin
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -64,7 +67,7 @@ impl Circle {
} else { } else {
// The point is outside the circle. // The point is outside the circle.
// Find the closest point on the perimeter of the circle. // Find the closest point on the perimeter of the circle.
let dir_to_point = point / distance_squared.sqrt(); let dir_to_point = point / ops::sqrt(distance_squared);
self.radius * dir_to_point self.radius * dir_to_point
} }
} }
@ -230,7 +233,7 @@ impl Arc2d {
// used by Wolfram MathWorld, which is the distance rather than the segment. // used by Wolfram MathWorld, which is the distance rather than the segment.
pub fn apothem(&self) -> f32 { pub fn apothem(&self) -> f32 {
let sign = if self.is_minor() { 1.0 } else { -1.0 }; let sign = if self.is_minor() { 1.0 } else { -1.0 };
sign * f32::sqrt(self.radius.squared() - self.half_chord_length().squared()) sign * ops::sqrt(self.radius.squared() - self.half_chord_length().squared())
} }
/// Get the length of the sagitta of this arc, that is, /// Get the length of the sagitta of this arc, that is,
@ -280,7 +283,7 @@ impl Arc2d {
)] )]
pub struct CircularSector { pub struct CircularSector {
/// The arc defining the sector /// The arc defining the sector
#[cfg_attr(feature = "serialize", serde(flatten))] #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))]
pub arc: Arc2d, pub arc: Arc2d,
} }
impl Primitive2d for CircularSector {} impl Primitive2d for CircularSector {}
@ -423,7 +426,7 @@ impl CircularSector {
)] )]
pub struct CircularSegment { pub struct CircularSegment {
/// The arc defining the segment /// The arc defining the segment
#[cfg_attr(feature = "serialize", serde(flatten))] #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))]
pub arc: Arc2d, pub arc: Arc2d,
} }
impl Primitive2d for CircularSegment {} impl Primitive2d for CircularSegment {}
@ -676,7 +679,7 @@ mod arc_tests {
#[test] #[test]
fn quarter_circle() { fn quarter_circle() {
let sqrt_half: f32 = f32::sqrt(0.5); let sqrt_half: f32 = ops::sqrt(0.5);
let tests = ArcTestCase { let tests = ArcTestCase {
radius: 1.0, radius: 1.0,
half_angle: FRAC_PI_4, half_angle: FRAC_PI_4,
@ -687,7 +690,7 @@ mod arc_tests {
endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)],
midpoint: Vec2::Y, midpoint: Vec2::Y,
half_chord_length: sqrt_half, half_chord_length: sqrt_half,
chord_length: f32::sqrt(2.0), chord_length: ops::sqrt(2.0),
chord_midpoint: Vec2::new(0.0, sqrt_half), chord_midpoint: Vec2::new(0.0, sqrt_half),
apothem: sqrt_half, apothem: sqrt_half,
sagitta: 1.0 - sqrt_half, sagitta: 1.0 - sqrt_half,
@ -822,7 +825,7 @@ impl Ellipse {
let a = self.semi_major(); let a = self.semi_major();
let b = self.semi_minor(); let b = self.semi_minor();
(a * a - b * b).sqrt() / a ops::sqrt(a * a - b * b) / a
} }
#[inline(always)] #[inline(always)]
@ -833,7 +836,7 @@ impl Ellipse {
let a = self.semi_major(); let a = self.semi_major();
let b = self.semi_minor(); let b = self.semi_minor();
(a * a - b * b).sqrt() ops::sqrt(a * a - b * b)
} }
/// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse.
@ -982,13 +985,13 @@ impl Annulus {
} else { } else {
// The point is outside the annulus and closer to the outer perimeter. // The point is outside the annulus and closer to the outer perimeter.
// Find the closest point on the perimeter of the annulus. // Find the closest point on the perimeter of the annulus.
let dir_to_point = point / distance_squared.sqrt(); let dir_to_point = point / ops::sqrt(distance_squared);
self.outer_circle.radius * dir_to_point self.outer_circle.radius * dir_to_point
} }
} else { } else {
// The point is outside the annulus and closer to the inner perimeter. // The point is outside the annulus and closer to the inner perimeter.
// Find the closest point on the perimeter of the annulus. // Find the closest point on the perimeter of the annulus.
let dir_to_point = point / distance_squared.sqrt(); let dir_to_point = point / ops::sqrt(distance_squared);
self.inner_circle.radius * dir_to_point self.inner_circle.radius * dir_to_point
} }
} }
@ -1301,14 +1304,18 @@ impl<const N: usize> Polyline2d<N> {
/// in a `Box<[Vec2]>`. /// in a `Box<[Vec2]>`.
/// ///
/// For a version without alloc: [`Polyline2d`] /// For a version without alloc: [`Polyline2d`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolyline2d { pub struct BoxedPolyline2d {
/// The vertices of the polyline /// The vertices of the polyline
pub vertices: Box<[Vec2]>, pub vertices: Box<[Vec2]>,
} }
#[cfg(feature = "alloc")]
impl Primitive2d for BoxedPolyline2d {} impl Primitive2d for BoxedPolyline2d {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec2> for BoxedPolyline2d { impl FromIterator<Vec2> for BoxedPolyline2d {
fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self {
let vertices: Vec<Vec2> = iter.into_iter().collect(); let vertices: Vec<Vec2> = iter.into_iter().collect();
@ -1318,6 +1325,7 @@ impl FromIterator<Vec2> for BoxedPolyline2d {
} }
} }
#[cfg(feature = "alloc")]
impl BoxedPolyline2d { impl BoxedPolyline2d {
/// Create a new `BoxedPolyline2d` from its vertices /// Create a new `BoxedPolyline2d` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self { pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self {
@ -1480,7 +1488,7 @@ impl Measured2d for Triangle2d {
#[inline(always)] #[inline(always)]
fn area(&self) -> f32 { fn area(&self) -> f32 {
let [a, b, c] = self.vertices; let [a, b, c] = self.vertices;
(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 ops::abs(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0
} }
/// Get the perimeter of the triangle /// Get the perimeter of the triangle
@ -1709,14 +1717,18 @@ impl<const N: usize> TryFrom<Polygon<N>> for ConvexPolygon<N> {
/// in a `Box<[Vec2]>`. /// in a `Box<[Vec2]>`.
/// ///
/// For a version without alloc: [`Polygon`] /// For a version without alloc: [`Polygon`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolygon { pub struct BoxedPolygon {
/// The vertices of the `BoxedPolygon` /// The vertices of the `BoxedPolygon`
pub vertices: Box<[Vec2]>, pub vertices: Box<[Vec2]>,
} }
#[cfg(feature = "alloc")]
impl Primitive2d for BoxedPolygon {} impl Primitive2d for BoxedPolygon {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec2> for BoxedPolygon { impl FromIterator<Vec2> for BoxedPolygon {
fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self {
let vertices: Vec<Vec2> = iter.into_iter().collect(); let vertices: Vec<Vec2> = iter.into_iter().collect();
@ -1726,6 +1738,7 @@ impl FromIterator<Vec2> for BoxedPolygon {
} }
} }
#[cfg(feature = "alloc")]
impl BoxedPolygon { impl BoxedPolygon {
/// Create a new `BoxedPolygon` from its vertices /// Create a new `BoxedPolygon` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self { pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self {

View file

@ -1,7 +1,10 @@
use core::f32::consts::{FRAC_PI_3, PI}; use core::f32::consts::{FRAC_PI_3, PI};
use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};
use crate::{ops, ops::FloatPow, Dir3, InvalidDirectionError, Isometry3d, Mat3, Vec2, Vec3}; use crate::{
ops::{self, FloatPow},
Dir3, InvalidDirectionError, Isometry3d, Mat3, Vec2, Vec3,
};
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
@ -9,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use glam::Quat; use glam::Quat;
#[cfg(feature = "alloc")]
use alloc::{boxed::Box, vec::Vec};
/// A sphere primitive, representing the set of all points some distance from the origin /// A sphere primitive, representing the set of all points some distance from the origin
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -61,7 +67,7 @@ impl Sphere {
} else { } else {
// The point is outside the sphere. // The point is outside the sphere.
// Find the closest point on the surface of the sphere. // Find the closest point on the surface of the sphere.
let dir_to_point = point / distance_squared.sqrt(); let dir_to_point = point / ops::sqrt(distance_squared);
self.radius * dir_to_point self.radius * dir_to_point
} }
} }
@ -440,14 +446,18 @@ impl<const N: usize> Polyline3d<N> {
/// in a `Box<[Vec3]>`. /// in a `Box<[Vec3]>`.
/// ///
/// For a version without alloc: [`Polyline3d`] /// For a version without alloc: [`Polyline3d`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolyline3d { pub struct BoxedPolyline3d {
/// The vertices of the polyline /// The vertices of the polyline
pub vertices: Box<[Vec3]>, pub vertices: Box<[Vec3]>,
} }
#[cfg(feature = "alloc")]
impl Primitive3d for BoxedPolyline3d {} impl Primitive3d for BoxedPolyline3d {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec3> for BoxedPolyline3d { impl FromIterator<Vec3> for BoxedPolyline3d {
fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
let vertices: Vec<Vec3> = iter.into_iter().collect(); let vertices: Vec<Vec3> = iter.into_iter().collect();
@ -457,6 +467,7 @@ impl FromIterator<Vec3> for BoxedPolyline3d {
} }
} }
#[cfg(feature = "alloc")]
impl BoxedPolyline3d { impl BoxedPolyline3d {
/// Create a new `BoxedPolyline3d` from its vertices /// Create a new `BoxedPolyline3d` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self { pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
@ -1246,7 +1257,7 @@ impl Measured3d for Tetrahedron {
/// Get the volume of the tetrahedron. /// Get the volume of the tetrahedron.
#[inline(always)] #[inline(always)]
fn volume(&self) -> f32 { fn volume(&self) -> f32 {
self.signed_volume().abs() ops::abs(self.signed_volume())
} }
} }
@ -1573,7 +1584,7 @@ mod tests {
assert_eq!(default_triangle.area(), 0.5, "incorrect area"); assert_eq!(default_triangle.area(), 0.5, "incorrect area");
assert_relative_eq!( assert_relative_eq!(
default_triangle.perimeter(), default_triangle.perimeter(),
1.0 + 2.0 * 1.25_f32.sqrt(), 1.0 + 2.0 * ops::sqrt(1.25_f32),
epsilon = 10e-9 epsilon = 10e-9
); );
assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal"); assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");

View file

@ -31,7 +31,7 @@ pub(crate) mod array {
type Value = [T; N]; type Value = [T; N];
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str(&format!("an array of length {}", N)) formatter.write_fmt(format_args!("an array of length {}", N))
} }
#[inline] #[inline]
@ -39,17 +39,21 @@ pub(crate) mod array {
where where
A: SeqAccess<'de>, A: SeqAccess<'de>,
{ {
let mut data = Vec::with_capacity(N); let mut data = [const { Option::<T>::None }; N];
for _ in 0..N {
for element in data.iter_mut() {
match (seq.next_element())? { match (seq.next_element())? {
Some(val) => data.push(val), Some(val) => *element = Some(val),
None => return Err(serde::de::Error::invalid_length(N, &self)), None => return Err(serde::de::Error::invalid_length(N, &self)),
} }
} }
match data.try_into() {
Ok(arr) => Ok(arr), let data = data.map(|value| match value {
Err(_) => unreachable!(), Some(value) => value,
} None => unreachable!(),
});
Ok(data)
} }
} }

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
ops,
primitives::{InfinitePlane3d, Plane2d}, primitives::{InfinitePlane3d, Plane2d},
Dir2, Dir3, Vec2, Vec3, Dir2, Dir3, Vec2, Vec3,
}; };
@ -40,7 +41,7 @@ impl Ray2d {
#[inline] #[inline]
pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option<f32> { pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option<f32> {
let denominator = plane.normal.dot(*self.direction); let denominator = plane.normal.dot(*self.direction);
if denominator.abs() > f32::EPSILON { if ops::abs(denominator) > f32::EPSILON {
let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator;
if distance > f32::EPSILON { if distance > f32::EPSILON {
return Some(distance); return Some(distance);
@ -82,7 +83,7 @@ impl Ray3d {
#[inline] #[inline]
pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option<f32> { pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option<f32> {
let denominator = plane.normal.dot(*self.direction); let denominator = plane.normal.dot(*self.direction);
if denominator.abs() > f32::EPSILON { if ops::abs(denominator) > f32::EPSILON {
let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator;
if distance > f32::EPSILON { if distance > f32::EPSILON {
return Some(distance); return Some(distance);

View file

@ -371,6 +371,8 @@ impl Rect {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ops;
use super::*; use super::*;
#[test] #[test]
@ -382,8 +384,8 @@ mod tests {
assert!(r.center().abs_diff_eq(Vec2::new(3., -5.), 1e-5)); assert!(r.center().abs_diff_eq(Vec2::new(3., -5.), 1e-5));
assert!((r.width() - 8.).abs() <= 1e-5); assert!(ops::abs(r.width() - 8.) <= 1e-5);
assert!((r.height() - 11.).abs() <= 1e-5); assert!(ops::abs(r.height() - 11.) <= 1e-5);
assert!(r.size().abs_diff_eq(Vec2::new(8., 11.), 1e-5)); assert!(r.size().abs_diff_eq(Vec2::new(8., 11.), 1e-5));
assert!(r.half_size().abs_diff_eq(Vec2::new(4., 5.5), 1e-5)); assert!(r.half_size().abs_diff_eq(Vec2::new(4., 5.5), 1e-5));

View file

@ -318,7 +318,7 @@ impl Rot2 {
// The allowed length is 1 +/- 1e-4, so the largest allowed // The allowed length is 1 +/- 1e-4, so the largest allowed
// squared length is (1 + 1e-4)^2 = 1.00020001, which makes // squared length is (1 + 1e-4)^2 = 1.00020001, which makes
// the threshold for the squared length approximately 2e-4. // the threshold for the squared length approximately 2e-4.
(self.length_squared() - 1.0).abs() <= 2e-4 ops::abs(self.length_squared() - 1.0) <= 2e-4
} }
/// Returns `true` if the rotation is near [`Rot2::IDENTITY`]. /// Returns `true` if the rotation is near [`Rot2::IDENTITY`].
@ -326,7 +326,7 @@ impl Rot2 {
pub fn is_near_identity(self) -> bool { pub fn is_near_identity(self) -> bool {
// Same as `Quat::is_near_identity`, but using sine and cosine // Same as `Quat::is_near_identity`, but using sine and cosine
let threshold_angle_sin = 0.000_049_692_047; // let threshold_angle = 0.002_847_144_6; let threshold_angle_sin = 0.000_049_692_047; // let threshold_angle = 0.002_847_144_6;
self.cos > 0.0 && self.sin.abs() < threshold_angle_sin self.cos > 0.0 && ops::abs(self.sin) < threshold_angle_sin
} }
/// Returns the angle in radians needed to make `self` and `other` coincide. /// Returns the angle in radians needed to make `self` and `other` coincide.
@ -520,7 +520,7 @@ mod tests {
use approx::assert_relative_eq; use approx::assert_relative_eq;
use crate::{Dir2, Rot2, Vec2}; use crate::{ops, Dir2, Rot2, Vec2};
#[test] #[test]
fn creation() { fn creation() {
@ -589,7 +589,7 @@ mod tests {
assert_eq!(rotation.length_squared(), 125.0); assert_eq!(rotation.length_squared(), 125.0);
assert_eq!(rotation.length(), 11.18034); assert_eq!(rotation.length(), 11.18034);
assert!((rotation.normalize().length() - 1.0).abs() < 10e-7); assert!(ops::abs(rotation.normalize().length() - 1.0) < 10e-7);
} }
#[test] #[test]
@ -702,7 +702,7 @@ mod tests {
assert!(rot1.nlerp(rot2, 0.0).is_near_identity()); assert!(rot1.nlerp(rot2, 0.0).is_near_identity());
// At 0.5, there is no valid rotation, so the fallback is the original angle. // At 0.5, there is no valid rotation, so the fallback is the original angle.
assert_eq!(rot1.nlerp(rot2, 0.5).as_degrees(), 0.0); assert_eq!(rot1.nlerp(rot2, 0.5).as_degrees(), 0.0);
assert_eq!(rot1.nlerp(rot2, 1.0).as_degrees().abs(), 180.0); assert_eq!(ops::abs(rot1.nlerp(rot2, 1.0).as_degrees()), 180.0);
} }
#[test] #[test]
@ -718,9 +718,9 @@ mod tests {
let rot1 = Rot2::IDENTITY; let rot1 = Rot2::IDENTITY;
let rot2 = Rot2::from_sin_cos(0.0, -1.0); let rot2 = Rot2::from_sin_cos(0.0, -1.0);
assert!((rot1.slerp(rot2, 1.0 / 3.0).as_degrees() - 60.0).abs() < 10e-6); assert!(ops::abs(rot1.slerp(rot2, 1.0 / 3.0).as_degrees() - 60.0) < 10e-6);
assert!(rot1.slerp(rot2, 0.0).is_near_identity()); assert!(rot1.slerp(rot2, 0.0).is_near_identity());
assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 90.0); assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 90.0);
assert_eq!(rot1.slerp(rot2, 1.0).as_degrees().abs(), 180.0); assert_eq!(ops::abs(rot1.slerp(rot2, 1.0).as_degrees()), 180.0);
} }
} }

View file

@ -4,6 +4,7 @@ use crate::{
primitives::{Measured2d, Triangle3d}, primitives::{Measured2d, Triangle3d},
ShapeSample, Vec3, ShapeSample, Vec3,
}; };
use alloc::vec::Vec;
use rand::Rng; use rand::Rng;
use rand_distr::{Distribution, WeightedAliasIndex, WeightedError}; use rand_distr::{Distribution, WeightedAliasIndex, WeightedError};

View file

@ -2,10 +2,12 @@
//! //!
//! To use this, the "rand" feature must be enabled. //! To use this, the "rand" feature must be enabled.
#[cfg(feature = "alloc")]
pub mod mesh_sampling; pub mod mesh_sampling;
pub mod shape_sampling; pub mod shape_sampling;
pub mod standard; pub mod standard;
#[cfg(feature = "alloc")]
pub use mesh_sampling::*; pub use mesh_sampling::*;
pub use shape_sampling::*; pub use shape_sampling::*;
pub use standard::*; pub use standard::*;

View file

@ -154,7 +154,7 @@ impl ShapeSample for Circle {
// https://mathworld.wolfram.com/DiskPointPicking.html // https://mathworld.wolfram.com/DiskPointPicking.html
let theta = rng.gen_range(0.0..TAU); let theta = rng.gen_range(0.0..TAU);
let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); let r_squared = rng.gen_range(0.0..=(self.radius * self.radius));
let r = r_squared.sqrt(); let r = ops::sqrt(r_squared);
let (sin, cos) = ops::sin_cos(theta); let (sin, cos) = ops::sin_cos(theta);
Vec2::new(r * cos, r * sin) Vec2::new(r * cos, r * sin)
} }
@ -171,7 +171,7 @@ impl ShapeSample for Circle {
fn sample_unit_sphere_boundary<R: Rng + ?Sized>(rng: &mut R) -> Vec3 { fn sample_unit_sphere_boundary<R: Rng + ?Sized>(rng: &mut R) -> Vec3 {
let z = rng.gen_range(-1f32..=1f32); let z = rng.gen_range(-1f32..=1f32);
let (a_sin, a_cos) = ops::sin_cos(rng.gen_range(-PI..=PI)); let (a_sin, a_cos) = ops::sin_cos(rng.gen_range(-PI..=PI));
let c = (1f32 - z * z).sqrt(); let c = ops::sqrt(1f32 - z * z);
let x = a_sin * c; let x = a_sin * c;
let y = a_cos * c; let y = a_cos * c;
@ -202,7 +202,7 @@ impl ShapeSample for Annulus {
// Like random sampling for a circle, radius is weighted by the square. // Like random sampling for a circle, radius is weighted by the square.
let r_squared = rng.gen_range((inner_radius * inner_radius)..(outer_radius * outer_radius)); let r_squared = rng.gen_range((inner_radius * inner_radius)..(outer_radius * outer_radius));
let r = r_squared.sqrt(); let r = ops::sqrt(r_squared);
let theta = rng.gen_range(0.0..TAU); let theta = rng.gen_range(0.0..TAU);
let (sin, cos) = ops::sin_cos(theta); let (sin, cos) = ops::sin_cos(theta);
@ -627,7 +627,7 @@ mod tests {
let point = circle.sample_boundary(&mut rng); let point = circle.sample_boundary(&mut rng);
let angle = ops::atan(point.y / point.x) + PI / 2.0; let angle = ops::atan(point.y / point.x) + PI / 2.0;
let wedge = (angle * 8.0 / PI).floor() as usize; let wedge = ops::floor(angle * 8.0 / PI) as usize;
wedge_hits[wedge] += 1; wedge_hits[wedge] += 1;
} }

View file

@ -62,6 +62,14 @@ impl Prepare for CompileCheckNoStdCommand {
"Please fix compiler errors in output above for bevy_mikktspace no_std compatibility.", "Please fix compiler errors in output above for bevy_mikktspace no_std compatibility.",
)); ));
commands.push(PreparedCommand::new::<Self>(
cmd!(
sh,
"cargo check -p bevy_math --no-default-features --features libm --target {target}"
),
"Please fix compiler errors in output above for bevy_math no_std compatibility.",
));
commands commands
} }
} }