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

View file

@ -26,4 +26,13 @@ disallowed-methods = [
{ path = "f32::asinh", reason = "use ops::asinh 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" },
# 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 crate::{
ops,
prelude::{Mat2, Rot2, Vec2},
FloatPow, Isometry2d,
};
@ -296,12 +297,12 @@ mod aabb2d_tests {
min: 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 {
min: Vec2::new(0., 0.),
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]
@ -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
@ -539,7 +540,7 @@ impl BoundingVolume for BoundingCircle {
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
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)]
@ -614,14 +615,14 @@ mod bounding_circle_tests {
use super::BoundingCircle;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
Vec2,
ops, Vec2,
};
#[test]
fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.);
// 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]
@ -647,7 +648,7 @@ mod bounding_circle_tests {
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
let merged = a.merge(&b);
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(&b));
assert!(!a.contains(&merged));
@ -679,7 +680,7 @@ mod bounding_circle_tests {
fn grow() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
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!(!a.contains(&padded));
}
@ -688,7 +689,7 @@ mod bounding_circle_tests {
fn shrink() {
let a = BoundingCircle::new(Vec2::ONE, 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!(!shrunk.contains(&a));
}
@ -697,7 +698,7 @@ mod bounding_circle_tests {
fn scale_around_center() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
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!(scaled.contains(&a));
}

View file

@ -3,14 +3,16 @@
use crate::{
ops,
primitives::{
Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector,
CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon,
Rhombus, Segment2d, Triangle2d,
Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
},
Dir2, Isometry2d, Mat2, Rot2, Vec2,
};
use core::f32::consts::{FRAC_PI_2, PI, TAU};
#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};
use smallvec::SmallVec;
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.
// 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.
let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
let right_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 = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);
let inverted = left_angle < right_angle;
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.
// 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.
@ -286,6 +288,7 @@ impl<const N: usize> Bounded2d for Polyline2d<N> {
}
}
#[cfg(feature = "alloc")]
impl Bounded2d for BoxedPolyline2d {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
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
// by an absolute rotation matrix.
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;
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 {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
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.
fn arc_and_segment() {
struct TestCase {
#[allow(unused)]
name: &'static str,
arc: Arc2d,
translation: Vec2,
@ -487,7 +493,7 @@ mod tests {
}
// 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 = [
// Test case: a basic minor arc
TestCase {
@ -558,7 +564,7 @@ mod tests {
aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0),
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
TestCase {
@ -597,6 +603,7 @@ mod tests {
];
for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name);
let segment: CircularSegment = test.arc.into();
@ -622,6 +629,7 @@ mod tests {
#[test]
fn circular_sector() {
struct TestCase {
#[allow(unused)]
name: &'static str,
arc: Arc2d,
translation: Vec2,
@ -639,8 +647,8 @@ mod tests {
}
// The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0;
let inv_sqrt_3 = f32::sqrt(3.0).recip();
let apothem = ops::sqrt(3.0) / 2.0;
let inv_sqrt_3 = ops::sqrt(3.0).recip();
let tests = [
// Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
TestCase {
@ -717,7 +725,7 @@ mod tests {
aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0),
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 {
name: "5/6th circle untransformed",
@ -753,6 +761,7 @@ mod tests {
];
for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name);
let sector: CircularSector = test.arc.into();

View file

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

View file

@ -4,7 +4,10 @@ mod primitive_impls;
use glam::Mat3;
use super::{BoundingVolume, IntersectsVolume};
use crate::{ops::FloatPow, Isometry3d, Quat, Vec3A};
use crate::{
ops::{self, FloatPow},
Isometry3d, Quat, Vec3A,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
@ -297,12 +300,12 @@ mod aabb3d_tests {
min: 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 {
min: Vec3A::new(0., 0., 0.),
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]
@ -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
@ -528,7 +531,7 @@ impl BoundingSphere {
} else {
// The point is outside 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
}
}
@ -557,7 +560,7 @@ impl BoundingVolume for BoundingSphere {
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
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)]
@ -644,14 +647,14 @@ mod bounding_sphere_tests {
use super::BoundingSphere;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
Quat, Vec3, Vec3A,
ops, Quat, Vec3, Vec3A,
};
#[test]
fn area() {
let sphere = BoundingSphere::new(Vec3::ONE, 5.);
// 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]
@ -677,7 +680,7 @@ mod bounding_sphere_tests {
let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.);
let merged = a.merge(&b);
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(&b));
assert!(!a.contains(&merged));
@ -709,7 +712,7 @@ mod bounding_sphere_tests {
fn grow() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
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!(!a.contains(&padded));
}
@ -718,7 +721,7 @@ mod bounding_sphere_tests {
fn shrink() {
let a = BoundingSphere::new(Vec3::ONE, 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!(!shrunk.contains(&a));
}
@ -727,7 +730,7 @@ mod bounding_sphere_tests {
fn scale_around_center() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
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!(scaled.contains(&a));
}

View file

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

View file

@ -1,5 +1,8 @@
use super::{Aabb2d, BoundingCircle, IntersectsVolume};
use crate::{ops::FloatPow, Dir2, Ray2d, Vec2};
use crate::{
ops::{self, FloatPow},
Dir2, Ray2d, Vec2,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
@ -77,10 +80,12 @@ impl RayCast2d {
let projected = offset.dot(*self.ray.direction);
let closest_point = offset - projected * *self.ray.direction;
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
} else {
let toi = -projected - distance_squared.sqrt();
let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max {
None
} else {
@ -224,21 +229,21 @@ mod tests {
4.996,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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!(
!test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}",
test,
volume,
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
@ -278,14 +281,17 @@ mod tests {
for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max);
let case = format!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
origin, direction, max,
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
assert!(test.intersects(&volume), "{}", case);
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,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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!(
!test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}",
test,
volume,
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
@ -385,14 +389,17 @@ mod tests {
for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max);
let case = format!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
origin, direction, max,
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
assert!(test.intersects(&volume), "{}", case);
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.,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray =
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,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray =
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 crate::{ops::FloatPow, Dir3A, Ray3d, Vec3A};
use crate::{
ops::{self, FloatPow},
Dir3A, Ray3d, Vec3A,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
@ -74,10 +77,12 @@ impl RayCast3d {
let projected = offset.dot(*self.direction);
let closest_point = offset - projected * *self.direction;
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
} else {
let toi = -projected - distance_squared.sqrt();
let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max {
None
} else {
@ -236,21 +241,21 @@ mod tests {
4.996,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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!(
!test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}",
test,
volume,
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
@ -290,14 +293,17 @@ mod tests {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
let case = format!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
origin, direction, max,
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
assert!(test.intersects(&volume), "{}", case);
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,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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!(
!test.intersects(volume),
"Case:\n Test: {:?}\n Volume: {:?}",
test,
volume,
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
@ -397,14 +401,17 @@ mod tests {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
let case = format!(
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
origin, direction, max,
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
assert!(test.intersects(&volume), "{}", case);
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.,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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,
),
] {
let case = format!(
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
test, volume, expected_distance
assert!(
test.intersects(volume),
"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();
assert!(
(actual_distance - expected_distance).abs() < EPSILON,
"{}\n Actual distance: {}",
case,
actual_distance
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
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 {
#[inline]
fn norm(self) -> f32 {
self.abs()
ops::abs(self)
}
#[inline]

View file

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

View file

@ -1,11 +1,16 @@
//! 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 itertools::Itertools;
#[cfg(feature = "alloc")]
use {alloc::vec, alloc::vec::Vec, core::iter::once, itertools::Itertools};
#[cfg(feature = "curve")]
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 positions: Vec<_> = bezier.iter_positions(100).collect();
/// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBezier<P: VectorSpace> {
@ -54,6 +60,7 @@ pub struct CubicBezier<P: VectorSpace> {
pub control_points: Vec<[P; 4]>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBezier<P> {
/// Create a new cubic Bezier curve from sets of control points.
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> {
type Error = CubicBezierError;
@ -140,12 +149,15 @@ pub struct CubicBezierError;
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicHermite<P: VectorSpace> {
/// The control points of the Hermite curve.
pub control_points: Vec<(P, P)>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicHermite<P> {
/// Create a new Hermite curve from sets of control points.
pub fn new(
@ -172,6 +184,8 @@ impl<P: VectorSpace> CubicHermite<P> {
]
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicHermite<P> {
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> {
type Error = InsufficientDataError;
@ -258,6 +274,7 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicHermite<P> {
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicCardinalSpline<P: VectorSpace> {
@ -267,6 +284,7 @@ pub struct CubicCardinalSpline<P: VectorSpace> {
pub control_points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicCardinalSpline<P> {
/// Build a new Cardinal spline.
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> {
type Error = InsufficientDataError;
@ -335,6 +355,8 @@ impl<P: VectorSpace> CubicGenerator<P> for CubicCardinalSpline<P> {
Ok(CubicCurve { segments })
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
type Error = InsufficientDataError;
@ -411,12 +433,15 @@ impl<P: VectorSpace> CyclicCubicGenerator<P> for CubicCardinalSpline<P> {
/// ```
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicBSpline<P: VectorSpace> {
/// The control points of the spline
pub control_points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicBSpline<P> {
/// Build a new B-Spline.
pub fn new(control_points: impl Into<Vec<P>>) -> Self {
@ -448,6 +473,8 @@ impl<P: VectorSpace> CubicBSpline<P> {
char_matrix
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicGenerator<P> for CubicBSpline<P> {
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> {
type Error = InsufficientDataError;
@ -578,6 +606,7 @@ pub enum CubicNurbsError {
/// .unwrap();
/// let positions: Vec<_> = nurbs.iter_positions(100).collect();
/// ```
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct CubicNurbs<P: VectorSpace> {
@ -588,6 +617,8 @@ pub struct CubicNurbs<P: VectorSpace> {
/// Knots
pub knots: Vec<f32>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicNurbs<P> {
/// 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> {
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.
///
/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
pub struct LinearSpline<P: VectorSpace> {
/// The control points of the linear spline.
pub points: Vec<P>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> LinearSpline<P> {
/// Create a new linear spline from a list of points to be interpolated.
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> {
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> {
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.
#[cfg(feature = "alloc")]
pub trait CubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
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.
///
/// This makes sense only when the control data can be interpreted cyclically.
#[cfg(feature = "alloc")]
pub trait CyclicCubicGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
type Error;
@ -937,6 +979,7 @@ impl<P: VectorSpace> CubicSegment<P> {
}
/// Calculate polynomial coefficients for the cubic curve using a characteristic matrix.
#[allow(unused)]
#[inline]
fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self {
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
/// 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 {
let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]])
@ -984,9 +1028,12 @@ impl CubicSegment<Vec2> {
///
/// ```
/// # use bevy_math::prelude::*;
/// # #[cfg(feature = "alloc")]
/// # {
/// 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(1.0), 1.0);
/// # }
/// ```
///
/// # How cubic easing works
@ -1050,7 +1097,7 @@ impl CubicSegment<Vec2> {
for _ in 0..Self::MAX_ITERS {
pos_guess = self.position(t_guess);
let error = pos_guess.x - x;
if error.abs() <= Self::MAX_ERROR {
if ops::abs(error) <= Self::MAX_ERROR {
break;
}
// 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
/// [`CubicBezier`].
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
@ -1087,6 +1135,7 @@ pub struct CubicCurve<P: VectorSpace> {
segments: Vec<CubicSegment<P>>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> CubicCurve<P> {
/// 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.
@ -1193,13 +1242,13 @@ impl<P: VectorSpace> CubicCurve<P> {
if self.segments.len() == 1 {
(&self.segments[0], t)
} 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)
}
}
}
#[cfg(feature = "curve")]
#[cfg(all(feature = "curve", feature = "alloc"))]
impl<P: VectorSpace> Curve<P> for CubicCurve<P> {
#[inline]
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> {
fn extend<T: IntoIterator<Item = CubicSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for CubicCurve<P> {
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.
#[cfg(feature = "alloc")]
pub trait RationalGenerator<P: VectorSpace> {
/// An error type indicating why construction might fail.
type Error;
@ -1334,6 +1386,7 @@ impl<P: VectorSpace> RationalSegment<P> {
}
/// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix.
#[allow(unused)]
#[inline]
fn coefficients(
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
/// [`CubicNurbs`], or convert [`CubicCurve`] using `into/from`.
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
@ -1397,6 +1451,7 @@ pub struct RationalCurve<P: VectorSpace> {
segments: Vec<RationalSegment<P>>,
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> RationalCurve<P> {
/// 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.
@ -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> {
#[inline]
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> {
fn extend<T: IntoIterator<Item = RationalSegment<P>>>(&mut self, iter: T) {
self.segments.extend(iter);
}
}
#[cfg(feature = "alloc")]
impl<P: VectorSpace> IntoIterator for RationalCurve<P> {
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> {
fn from(value: CubicCurve<P>) -> Self {
Self {
@ -1577,10 +1635,9 @@ impl<P: VectorSpace> From<CubicCurve<P>> for RationalCurve<P> {
}
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use glam::{vec2, Vec2};
use crate::{
cubic_splines::{
CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve,
@ -1588,6 +1645,8 @@ mod tests {
},
ops::{self, FloatPow},
};
use alloc::vec::Vec;
use glam::{vec2, Vec2};
/// How close two floats can be and still be considered equal
const FLOAT_EQ: f32 = 1e-5;
@ -1760,7 +1819,7 @@ mod tests {
let curve = spline.to_curve().unwrap();
for (i, point) in curve.iter_positions(10).enumerate() {
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.length()
);

View file

@ -3,6 +3,7 @@
use super::interval::*;
use super::Curve;
use crate::ops;
use crate::VectorSpace;
use core::any::type_name;
use core::fmt::{self, Debug};
@ -621,7 +622,7 @@ where
fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction
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 {
d.end()
} else {
@ -669,7 +670,7 @@ where
fn sample_unchecked(&self, t: f32) -> T {
// the domain is bounded by construction
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 {
d.end()
} else {

View file

@ -6,10 +6,14 @@
//! provided methods all maintain the invariants, so this is only a concern if you manually mutate
//! the fields.
use crate::ops;
use super::interval::Interval;
use core::fmt::Debug;
use derive_more::derive::{Display, Error};
use itertools::Itertools;
#[cfg(feature = "alloc")]
use {alloc::vec::Vec, itertools::Itertools};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
@ -112,6 +116,7 @@ impl<T> InterpolationDatum<T> {
/// }
/// }
/// ```
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -146,6 +151,7 @@ pub enum EvenCoreError {
UnboundedDomain,
}
#[cfg(feature = "alloc")]
impl<T> EvenCore<T> {
/// 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
@ -243,11 +249,11 @@ pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDat
// To the right side of all the samples
InterpolationDatum::RightTail(samples - 1)
} 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
// strictly less than `samples - 1`, so its floor is at most `samples - 2`
let upper_index = lower_index + 1;
let s = steps_taken.fract();
let s = ops::fract(steps_taken);
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
/// [`sample_with`]: UnevenCore::sample_with
/// [the provided constructor]: UnevenCore::new
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -346,6 +353,7 @@ pub enum UnevenCoreError {
},
}
#[cfg(feature = "alloc")]
impl<T> UnevenCore<T> {
/// 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
@ -453,6 +461,7 @@ impl<T> UnevenCore<T> {
/// if the sample type can effectively be encoded as a fixed-length slice of values.
///
/// [sampling width]: ChunkedUnevenCore::width
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
@ -507,6 +516,7 @@ pub enum ChunkedUnevenCoreError {
},
}
#[cfg(feature = "alloc")]
impl<T> ChunkedUnevenCore<T> {
/// 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.
@ -644,6 +654,7 @@ impl<T> ChunkedUnevenCore<T> {
}
/// 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> {
// 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();
@ -682,7 +693,7 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum<usize> {
}
}
#[cfg(test)]
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::{ChunkedUnevenCore, EvenCore, UnevenCore};
use crate::curve::{cores::InterpolationDatum, interval};

View file

@ -309,18 +309,18 @@ mod easing_functions {
#[inline]
pub(crate) fn circular_in(t: f32) -> f32 {
1.0 - (1.0 - t.squared()).sqrt()
1.0 - ops::sqrt(1.0 - t.squared())
}
#[inline]
pub(crate) fn circular_out(t: f32) -> f32 {
(1.0 - (t - 1.0).squared()).sqrt()
ops::sqrt(1.0 - (t - 1.0).squared())
}
#[inline]
pub(crate) fn circular_in_out(t: f32) -> f32 {
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 {
((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]
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]

View file

@ -198,6 +198,8 @@ pub fn interval(start: f32, end: f32) -> Result<Interval, InvalidIntervalError>
#[cfg(test)]
mod tests {
use crate::ops;
use super::*;
use approx::{assert_abs_diff_eq, AbsDiffEq};
@ -237,10 +239,10 @@ mod tests {
#[test]
fn lengths() {
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();
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();
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
//! 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.
///
@ -39,6 +42,7 @@ pub trait IterableCurve<T> {
}
}
#[cfg(feature = "alloc")]
impl<T> IterableCurve<T> for ConstantCurve<Vec<T>>
where
T: Clone,

View file

@ -290,21 +290,27 @@ pub mod cores;
pub mod easing;
pub mod interval;
pub mod iterable;
#[cfg(feature = "alloc")]
pub mod sample_curves;
// bevy_math::curve re-exports all commonly-needed curve-related items.
pub use adaptors::*;
pub use easing::*;
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 derive_more::derive::{Display, Error};
use interval::InvalidIntervalError;
use itertools::Itertools;
/// 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:
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
/// ```
#[cfg(feature = "alloc")]
fn resample<I>(
&self,
segments: usize,
@ -743,6 +750,7 @@ pub trait Curve<T> {
/// domain, then a [`ResamplingError`] is returned.
///
/// [automatic interpolation]: crate::common_traits::StableInterpolate
#[cfg(feature = "alloc")]
fn resample_auto(&self, segments: usize) -> Result<SampleAutoCurve<T>, ResamplingError>
where
Self: Sized,
@ -794,6 +802,7 @@ pub trait Curve<T> {
/// 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
/// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively.
#[cfg(feature = "alloc")]
fn resample_uneven<I>(
&self,
sample_times: impl IntoIterator<Item = f32>,
@ -832,6 +841,7 @@ pub trait Curve<T> {
/// sample times of the iterator.
///
/// [automatic interpolation]: crate::common_traits::StableInterpolate
#[cfg(feature = "alloc")]
fn resample_uneven_auto(
&self,
sample_times: impl IntoIterator<Item = f32>,

View file

@ -356,6 +356,7 @@ impl<T> UnevenSampleAutoCurve<T> {
}
#[cfg(test)]
#[cfg(feature = "bevy_reflect")]
mod tests {
//! These tests should guarantee (by even compiling) that `SampleCurve` and `UnevenSampleCurve`
//! 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.
#[cfg(debug_assertions)]
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.
if length_error_squared > 2e-2 || length_error_squared.is_nan() {
// Length error is approximately 1e-2 or more.
panic!("Error: {message} The length is {}.", length_squared.sqrt());
panic!(
"Error: {message} The length is {}.",
ops::sqrt(length_squared)
);
} else if length_error_squared > 2e-4 {
// Length error is approximately 1e-4 or more.
#[cfg(feature = "std")]
eprintln!(
"Warning: {message} The length is {}.",
length_squared.sqrt()
ops::sqrt(length_squared)
);
}
}
@ -886,17 +892,17 @@ mod tests {
fn dir2_slerp() {
assert_relative_eq!(
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_relative_eq!(Dir2::X.slerp(Dir2::Y, 1.0), Dir2::Y);
assert_relative_eq!(
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!(
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() {
assert_relative_eq!(
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::Z.slerp(Dir3::X, 1.0), Dir3::X, epsilon = 0.000001);
assert_relative_eq!(
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
);
assert_relative_eq!(
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() {
assert_relative_eq!(
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::Z.slerp(Dir3A::X, 1.0), Dir3A::X, epsilon = 0.000001);
assert_relative_eq!(
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
);
assert_relative_eq!(
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)]
mod tests {
use std::hash::DefaultHasher;
use super::*;
const NAN: FloatOrd = FloatOrd(f32::NAN);
@ -157,10 +155,11 @@ mod tests {
assert!(ONE >= ZERO);
}
#[cfg(feature = "std")]
#[test]
fn float_ord_hash() {
let hash = |num| {
let mut h = DefaultHasher::new();
let mut h = std::hash::DefaultHasher::new();
FloatOrd(num).hash(&mut h);
h.finish()
};

View file

@ -6,6 +6,7 @@
html_logo_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.
//!
@ -13,6 +14,9 @@
//! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations
//! like [`Quat`].
#[cfg(feature = "alloc")]
extern crate alloc;
mod affine3;
mod aspect_ratio;
pub mod bounding;
@ -59,11 +63,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
bvec2, bvec3, bvec3a, bvec4, bvec4a,
cubic_splines::{
CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator,
CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator,
RationalCurve, RationalGenerator, RationalSegment,
},
cubic_splines::{CubicNurbsError, CubicSegment, RationalSegment},
direction::{Dir2, Dir3, Dir3A},
ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops,
primitives::*,
@ -80,6 +80,13 @@ pub mod prelude {
#[doc(hidden)]
#[cfg(feature = "rand")]
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::*;

View file

@ -4,6 +4,9 @@
//!
//! All the functions here are named according to their versions in the standard
//! 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(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")]
pub use libm_ops::*;
#[cfg(not(feature = "libm"))]
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
/// to `f32::powi`. Use this for the common small exponents.
pub trait FloatPow {

View file

@ -12,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
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
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -64,7 +67,7 @@ impl Circle {
} else {
// The point is outside 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
}
}
@ -230,7 +233,7 @@ impl Arc2d {
// used by Wolfram MathWorld, which is the distance rather than the segment.
pub fn apothem(&self) -> f32 {
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,
@ -280,7 +283,7 @@ impl Arc2d {
)]
pub struct CircularSector {
/// The arc defining the sector
#[cfg_attr(feature = "serialize", serde(flatten))]
#[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))]
pub arc: Arc2d,
}
impl Primitive2d for CircularSector {}
@ -423,7 +426,7 @@ impl CircularSector {
)]
pub struct CircularSegment {
/// The arc defining the segment
#[cfg_attr(feature = "serialize", serde(flatten))]
#[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))]
pub arc: Arc2d,
}
impl Primitive2d for CircularSegment {}
@ -676,7 +679,7 @@ mod arc_tests {
#[test]
fn quarter_circle() {
let sqrt_half: f32 = f32::sqrt(0.5);
let sqrt_half: f32 = ops::sqrt(0.5);
let tests = ArcTestCase {
radius: 1.0,
half_angle: FRAC_PI_4,
@ -687,7 +690,7 @@ mod arc_tests {
endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)],
midpoint: Vec2::Y,
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),
apothem: sqrt_half,
sagitta: 1.0 - sqrt_half,
@ -822,7 +825,7 @@ impl Ellipse {
let a = self.semi_major();
let b = self.semi_minor();
(a * a - b * b).sqrt() / a
ops::sqrt(a * a - b * b) / a
}
#[inline(always)]
@ -833,7 +836,7 @@ impl Ellipse {
let a = self.semi_major();
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.
@ -982,13 +985,13 @@ impl Annulus {
} else {
// The point is outside the annulus and closer to the outer perimeter.
// 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
}
} else {
// The point is outside the annulus and closer to the inner perimeter.
// 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
}
}
@ -1301,14 +1304,18 @@ impl<const N: usize> Polyline2d<N> {
/// in a `Box<[Vec2]>`.
///
/// For a version without alloc: [`Polyline2d`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolyline2d {
/// The vertices of the polyline
pub vertices: Box<[Vec2]>,
}
#[cfg(feature = "alloc")]
impl Primitive2d for BoxedPolyline2d {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec2> for BoxedPolyline2d {
fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self {
let vertices: Vec<Vec2> = iter.into_iter().collect();
@ -1318,6 +1325,7 @@ impl FromIterator<Vec2> for BoxedPolyline2d {
}
}
#[cfg(feature = "alloc")]
impl BoxedPolyline2d {
/// Create a new `BoxedPolyline2d` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self {
@ -1480,7 +1488,7 @@ impl Measured2d for Triangle2d {
#[inline(always)]
fn area(&self) -> f32 {
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
@ -1709,14 +1717,18 @@ impl<const N: usize> TryFrom<Polygon<N>> for ConvexPolygon<N> {
/// in a `Box<[Vec2]>`.
///
/// For a version without alloc: [`Polygon`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolygon {
/// The vertices of the `BoxedPolygon`
pub vertices: Box<[Vec2]>,
}
#[cfg(feature = "alloc")]
impl Primitive2d for BoxedPolygon {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec2> for BoxedPolygon {
fn from_iter<I: IntoIterator<Item = Vec2>>(iter: I) -> Self {
let vertices: Vec<Vec2> = iter.into_iter().collect();
@ -1726,6 +1738,7 @@ impl FromIterator<Vec2> for BoxedPolygon {
}
}
#[cfg(feature = "alloc")]
impl BoxedPolygon {
/// Create a new `BoxedPolygon` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec2>) -> Self {

View file

@ -1,7 +1,10 @@
use core::f32::consts::{FRAC_PI_3, PI};
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")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
@ -9,6 +12,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
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
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
@ -61,7 +67,7 @@ impl Sphere {
} else {
// The point is outside 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
}
}
@ -440,14 +446,18 @@ impl<const N: usize> Polyline3d<N> {
/// in a `Box<[Vec3]>`.
///
/// For a version without alloc: [`Polyline3d`]
#[cfg(feature = "alloc")]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct BoxedPolyline3d {
/// The vertices of the polyline
pub vertices: Box<[Vec3]>,
}
#[cfg(feature = "alloc")]
impl Primitive3d for BoxedPolyline3d {}
#[cfg(feature = "alloc")]
impl FromIterator<Vec3> for BoxedPolyline3d {
fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
let vertices: Vec<Vec3> = iter.into_iter().collect();
@ -457,6 +467,7 @@ impl FromIterator<Vec3> for BoxedPolyline3d {
}
}
#[cfg(feature = "alloc")]
impl BoxedPolyline3d {
/// Create a new `BoxedPolyline3d` from its vertices
pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
@ -1246,7 +1257,7 @@ impl Measured3d for Tetrahedron {
/// Get the volume of the tetrahedron.
#[inline(always)]
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_relative_eq!(
default_triangle.perimeter(),
1.0 + 2.0 * 1.25_f32.sqrt(),
1.0 + 2.0 * ops::sqrt(1.25_f32),
epsilon = 10e-9
);
assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");

View file

@ -31,7 +31,7 @@ pub(crate) mod array {
type Value = [T; N];
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]
@ -39,17 +39,21 @@ pub(crate) mod array {
where
A: SeqAccess<'de>,
{
let mut data = Vec::with_capacity(N);
for _ in 0..N {
let mut data = [const { Option::<T>::None }; N];
for element in data.iter_mut() {
match (seq.next_element())? {
Some(val) => data.push(val),
Some(val) => *element = Some(val),
None => return Err(serde::de::Error::invalid_length(N, &self)),
}
}
match data.try_into() {
Ok(arr) => Ok(arr),
Err(_) => unreachable!(),
}
let data = data.map(|value| match value {
Some(value) => value,
None => unreachable!(),
});
Ok(data)
}
}

View file

@ -1,4 +1,5 @@
use crate::{
ops,
primitives::{InfinitePlane3d, Plane2d},
Dir2, Dir3, Vec2, Vec3,
};
@ -40,7 +41,7 @@ impl Ray2d {
#[inline]
pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option<f32> {
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;
if distance > f32::EPSILON {
return Some(distance);
@ -82,7 +83,7 @@ impl Ray3d {
#[inline]
pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option<f32> {
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;
if distance > f32::EPSILON {
return Some(distance);

View file

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

View file

@ -318,7 +318,7 @@ impl Rot2 {
// The allowed length is 1 +/- 1e-4, so the largest allowed
// squared length is (1 + 1e-4)^2 = 1.00020001, which makes
// 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`].
@ -326,7 +326,7 @@ impl Rot2 {
pub fn is_near_identity(self) -> bool {
// 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;
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.
@ -520,7 +520,7 @@ mod tests {
use approx::assert_relative_eq;
use crate::{Dir2, Rot2, Vec2};
use crate::{ops, Dir2, Rot2, Vec2};
#[test]
fn creation() {
@ -589,7 +589,7 @@ mod tests {
assert_eq!(rotation.length_squared(), 125.0);
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]
@ -702,7 +702,7 @@ mod tests {
assert!(rot1.nlerp(rot2, 0.0).is_near_identity());
// 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, 1.0).as_degrees().abs(), 180.0);
assert_eq!(ops::abs(rot1.nlerp(rot2, 1.0).as_degrees()), 180.0);
}
#[test]
@ -718,9 +718,9 @@ mod tests {
let rot1 = Rot2::IDENTITY;
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_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},
ShapeSample, Vec3,
};
use alloc::vec::Vec;
use rand::Rng;
use rand_distr::{Distribution, WeightedAliasIndex, WeightedError};

View file

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

View file

@ -154,7 +154,7 @@ impl ShapeSample for Circle {
// https://mathworld.wolfram.com/DiskPointPicking.html
let theta = rng.gen_range(0.0..TAU);
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);
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 {
let z = rng.gen_range(-1f32..=1f32);
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 y = a_cos * c;
@ -202,7 +202,7 @@ impl ShapeSample for Annulus {
// 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 = r_squared.sqrt();
let r = ops::sqrt(r_squared);
let theta = rng.gen_range(0.0..TAU);
let (sin, cos) = ops::sin_cos(theta);
@ -627,7 +627,7 @@ mod tests {
let point = circle.sample_boundary(&mut rng);
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;
}

View file

@ -62,6 +62,14 @@ impl Prepare for CompileCheckNoStdCommand {
"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
}
}