diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 042953f263aed..e545dc91e8adf 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -18,6 +18,8 @@ seq-macro = "0.3.6" bevy_app = { path = "../crates/bevy_app" } bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } bevy_math = { path = "../crates/bevy_math" } +bevy_shape = { path = "../crates/bevy_shape" } +bevy_geometry = { path = "../crates/bevy_geometry" } bevy_picking = { path = "../crates/bevy_picking", features = ["mesh_picking"] } bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] } bevy_camera = { path = "../crates/bevy_camera" } diff --git a/benches/benches/bevy_camera/primitives.rs b/benches/benches/bevy_camera/primitives.rs index 153fe9e7ca78e..6ad671a8eddc8 100644 --- a/benches/benches/bevy_camera/primitives.rs +++ b/benches/benches/bevy_camera/primitives.rs @@ -1,8 +1,6 @@ use bevy_camera::primitives::{Aabb, Frustum, Sphere}; -use bevy_math::{ - primitives::{HalfSpace, ViewFrustum}, - Affine3A, Quat, Vec3, Vec3A, Vec4, -}; +use bevy_math::{Affine3A, Quat, Vec3, Vec3A, Vec4}; +use bevy_shape::{HalfSpace, ViewFrustum}; use core::hint::black_box; use criterion::{criterion_group, Criterion}; diff --git a/benches/benches/bevy_math/bounding.rs b/benches/benches/bevy_math/bounding.rs index 65432ea99bfdd..7f17ffebd2dd2 100644 --- a/benches/benches/bevy_math/bounding.rs +++ b/benches/benches/bevy_math/bounding.rs @@ -1,8 +1,6 @@ use benches::bench; -use bevy_math::{ - bounding::{Aabb3d, BoundingSphere, BoundingVolume}, - prelude::*, -}; +use bevy_geometry::bounding::{Aabb3d, BoundingSphere, BoundingVolume}; +use bevy_math::prelude::*; use core::hint::black_box; use criterion::{criterion_group, Criterion}; use rand::{ diff --git a/crates/bevy_camera/Cargo.toml b/crates/bevy_camera/Cargo.toml index 49b23d6aeda07..501acf2550230 100644 --- a/crates/bevy_camera/Cargo.toml +++ b/crates/bevy_camera/Cargo.toml @@ -15,6 +15,8 @@ bevy_asset = { path = "../bevy_asset", version = "0.20.0-dev" } bevy_image = { path = "../bevy_image", version = "0.20.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev", features = [ "wgpu-types", ] } diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index bb42023f9e5ab..a2ed6299224c7 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -2,13 +2,11 @@ use core::borrow::Borrow; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; -use bevy_math::{ - bounding::{Aabb3d, BoundingVolume}, - primitives::{HalfSpace, ViewFrustum}, - Affine3A, Mat3A, Vec3, Vec3A, -}; +use bevy_geometry::bounding::{Aabb3d, BoundingVolume}; +use bevy_math::{Affine3A, Mat3A, Vec3, Vec3A}; use bevy_mesh::{Mesh, VertexAttributeValues}; use bevy_reflect::prelude::*; +use bevy_shape::{HalfSpace, ViewFrustum}; pub trait MeshAabb { /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space, diff --git a/crates/bevy_camera/src/projection.rs b/crates/bevy_camera/src/projection.rs index 85d849d6557c7..99855d973e913 100644 --- a/crates/bevy_camera/src/projection.rs +++ b/crates/bevy_camera/src/projection.rs @@ -4,10 +4,9 @@ use core::ops::{Deref, DerefMut}; use crate::{primitives::Frustum, visibility::VisibilitySystems}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::prelude::*; -use bevy_math::{ - ops, primitives::ViewFrustum, proj, vec4, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4, -}; +use bevy_math::{ops, proj, vec4, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_shape::ViewFrustum; use bevy_transform::{components::GlobalTransform, TransformSystems}; use derive_more::derive::From; use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_geometry/Cargo.toml b/crates/bevy_geometry/Cargo.toml new file mode 100644 index 0000000000000..1f709b0f96e16 --- /dev/null +++ b/crates/bevy_geometry/Cargo.toml @@ -0,0 +1,98 @@ +[package] +description = "Provides geometric functionality primitive shapes for Bevy Engine" +edition = "2024" +homepage = "https://bevy.org" +keywords = ["bevy"] +license = "MIT OR Apache-2.0" +name = "bevy_geometry" +repository = "https://github.com/bevyengine/bevy" +rust-version = "1.94.0" +version = "0.20.0-dev" + +[dependencies] +approx = { default-features = false, optional = true, version = "0.5" } +arrayvec = { default-features = false, version = "0.7" } +bevy_math = { default-features = false, path = "../bevy_math" } +bevy_reflect = { default-features = false, features = [ + "glam", +], optional = true, path = "../bevy_reflect", version = "0.20.0-dev" } +bevy_shape = { default-features = false, path = "../bevy_shape" } +derive_more = { default-features = false, features = [ + "from", + "into", +], version = "2" } +itertools = { default-features = false, version = "0.14.0" } +rand = { default-features = false, optional = true, version = "0.10" } +rand_distr = { optional = true, version = "0.6" } +serde = { default-features = false, features = [ + "derive", +], optional = true, version = "1" } +thiserror = { default-features = false, version = "2" } +variadics_please = "2.0" + +[dev-dependencies] +approx = "0.5" +# Supply rngs for examples and tests +chacha20 = { default-features = false, features = ["rng"], version = "0.10.0" } +rand = "0.10" +# Enable the approx feature when testing. +bevy_math = { default-features = false, features = [ + "approx", +], path = "../bevy_math" } +bevy_shape = { default-features = false, features = [ + "approx", +], path = "../bevy_shape" } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +getrandom = { default-features = false, features = [ + "wasm_js", +], version = "0.4" } + +[features] +alloc = [ + "bevy_math/alloc", + "bevy_shape/alloc", + "itertools/use_alloc", + "rand?/alloc", + "rand_distr?/alloc", + "serde?/alloc", +] +default = ["curve", "rand", "std"] +std = [ + "alloc", + "approx?/std", + "bevy_math/std", + "bevy_reflect?/std", + "bevy_shape/std", + "derive_more/std", + "itertools/use_std", + "rand?/std", + "rand_distr?/std", + "serde?/std", +] + +serialize = ["bevy_math/serialize", "bevy_shape/serialize", "dep:serde"] +# Enable approx for glam types to approximate floating point equality comparisons and assertions +approx = ["bevy_math/approx", "bevy_shape/approx", "dep:approx"] +# Enable the rand dependency for shape_sampling +rand = ["dep:rand", "dep:rand_distr"] +# Include code related to the Curve trait +curve = [] +# Enable bevy_reflect (requires alloc) +bevy_reflect = [ + "alloc", + "bevy_math/bevy_reflect", + "bevy_shape/bevy_reflect", + "dep:bevy_reflect", +] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--generate-link-to-definition", + "--generate-macro-expansion", + "-Zunstable-options", +] diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_geometry/src/bounding/bounded2d/mod.rs similarity index 98% rename from crates/bevy_math/src/bounding/bounded2d/mod.rs rename to crates/bevy_geometry/src/bounding/bounded2d/mod.rs index 40920402ab2d9..fdbd86df765b9 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_geometry/src/bounding/bounded2d/mod.rs @@ -1,11 +1,7 @@ mod primitive_impls; -use super::{BoundingVolume, IntersectsVolume}; -use crate::{ - ops, - prelude::{Mat2, Rot2, Vec2}, - FloatPow, Isometry2d, -}; +use crate::bounding::{BoundingVolume, IntersectsVolume}; +use bevy_math::{ops, FloatPow, Isometry2d, Mat2, Rot2, Vec2}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -272,11 +268,11 @@ impl IntersectsVolume for Aabb2d { mod aabb2d_tests { use approx::assert_relative_eq; - use super::Aabb2d; - use crate::{ - bounding::{BoundingCircle, BoundingVolume, IntersectsVolume}, - ops, Vec2, + use crate::bounding::{ + bounded2d::{Aabb2d, BoundingCircle}, + BoundingVolume, IntersectsVolume, }; + use bevy_math::{ops, Vec2}; #[test] fn center() { @@ -469,7 +465,7 @@ mod aabb2d_tests { } } -use crate::primitives::Circle; +use bevy_shape::Circle; /// A bounding circle #[derive(Clone, Copy, Debug, PartialEq)] @@ -643,11 +639,8 @@ impl IntersectsVolume for BoundingCircle { #[cfg(test)] mod bounding_circle_tests { - use super::BoundingCircle; - use crate::{ - bounding::{BoundingVolume, IntersectsVolume}, - ops, Vec2, - }; + use crate::bounding::{bounded2d::BoundingCircle, BoundingVolume, IntersectsVolume}; + use bevy_math::{ops, Vec2}; #[test] fn area() { diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_geometry/src/bounding/bounded2d/primitive_impls.rs similarity index 97% rename from crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs rename to crates/bevy_geometry/src/bounding/bounded2d/primitive_impls.rs index 8e45e645bfeb5..cafd94940fc59 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_geometry/src/bounding/bounded2d/primitive_impls.rs @@ -1,23 +1,21 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). -use crate::{ - bounding::BoundingVolume, - ops, - primitives::{ - Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, - Plane2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, - }, - Dir2, Isometry2d, Mat2, Rot2, Vec2, +use crate::bounding::{ + bounded2d::{Aabb2d, Bounded2d, BoundingCircle}, + BoundingVolume, +}; +use bevy_math::{ops, Dir2, Isometry2d, Mat2, Rot2, Vec2}; +use bevy_shape::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, + Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }; use core::f32::consts::{FRAC_PI_2, PI, TAU}; #[cfg(feature = "alloc")] -use crate::primitives::{ConvexPolygon, Polygon, Polyline2d}; +use bevy_shape::{ConvexPolygon, Polygon, Polyline2d}; use arrayvec::ArrayVec; -use super::{Aabb2d, Bounded2d, BoundingCircle}; - impl Bounded2d for Circle { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { let isometry = isometry.into(); @@ -428,16 +426,6 @@ impl Bounded2d for Capsule2d { } } -impl Bounded2d for Ring

{ - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - self.outer_shape.aabb_2d(isometry) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - self.outer_shape.bounding_circle(isometry) - } -} - #[cfg(test)] #[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { @@ -445,17 +433,15 @@ mod tests { use std::println; use approx::assert_abs_diff_eq; - use glam::Vec2; - - use crate::{ - bounding::Bounded2d, + use bevy_math::{ ops::{self, FloatPow}, - primitives::{ - Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, - Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, - Triangle2d, - }, - Dir2, Isometry2d, Rot2, + Dir2, Isometry2d, Rot2, Vec2, + }; + + use crate::bounding::bounded2d::Bounded2d; + use bevy_shape::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }; #[test] diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_geometry/src/bounding/bounded3d/extrusion.rs similarity index 93% rename from crates/bevy_math/src/bounding/bounded3d/extrusion.rs rename to crates/bevy_geometry/src/bounding/bounded3d/extrusion.rs index e37783576caac..ca57b3e4cb6ec 100644 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ b/crates/bevy_geometry/src/bounding/bounded3d/extrusion.rs @@ -1,21 +1,16 @@ use core::f32::consts::FRAC_PI_2; -use glam::{Vec2, Vec3A, Vec3Swizzles}; - -use crate::{ - bounding::{BoundingCircle, BoundingVolume}, - ops, - primitives::{ - Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Primitive2d, Rectangle, - RegularPolygon, Ring, Segment2d, Triangle2d, - }, - Isometry2d, Isometry3d, Quat, Rot2, +use crate::bounding::{bounded2d::BoundingCircle, BoundingVolume}; +use bevy_math::{ops, Isometry2d, Isometry3d, Quat, Rot2, Vec2, Vec3A, Vec3Swizzles}; +use bevy_shape::{ + Capsule2d, Circle, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Primitive2d, Rectangle, + RegularPolygon, Segment2d, Triangle2d, }; #[cfg(feature = "alloc")] -use crate::primitives::{Polygon, Polyline2d}; +use bevy_shape::{Polygon, Polyline2d}; -use crate::{bounding::Bounded2d, primitives::Circle}; +use crate::bounding::Bounded2d; use super::{Aabb3d, Bounded3d, BoundingSphere}; @@ -165,21 +160,6 @@ impl BoundedExtrusion for Capsule2d { } } -impl BoundedExtrusion for Ring { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - self.outer_shape.extrusion_aabb_3d(half_depth, isometry) - } - - fn extrusion_bounding_sphere( - &self, - half_depth: f32, - isometry: impl Into, - ) -> BoundingSphere { - self.outer_shape - .extrusion_bounding_sphere(half_depth, isometry) - } -} - impl Bounded3d for Extrusion { fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { self.base_shape.extrusion_aabb_3d(self.half_depth, isometry) @@ -260,16 +240,11 @@ pub trait BoundedExtrusion: Primitive2d + Bounded2d { mod tests { use core::f32::consts::FRAC_PI_4; - use glam::{EulerRot, Quat, Vec2, Vec3, Vec3A}; - - use crate::{ - bounding::{Bounded3d, BoundingVolume}, - ops, - primitives::{ - Capsule2d, Circle, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Rectangle, - RegularPolygon, Segment2d, Triangle2d, - }, - Dir2, Isometry3d, + use crate::bounding::{bounded3d::Bounded3d, BoundingVolume}; + use bevy_math::{ops, Dir2, EulerRot, Isometry3d, Quat, Vec2, Vec3, Vec3A}; + use bevy_shape::{ + Capsule2d, Circle, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Rectangle, + RegularPolygon, Segment2d, Triangle2d, }; #[test] diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_geometry/src/bounding/bounded3d/mod.rs similarity index 98% rename from crates/bevy_math/src/bounding/bounded3d/mod.rs rename to crates/bevy_geometry/src/bounding/bounded3d/mod.rs index fa5aaf17b9f40..43b00223fb920 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_geometry/src/bounding/bounded3d/mod.rs @@ -1,14 +1,14 @@ mod extrusion; mod primitive_impls; -use glam::Mat3; - -use super::{BoundingVolume, IntersectsVolume}; -use crate::{ +use crate::bounding::{BoundingVolume, IntersectsVolume}; +use bevy_math::{ ops::{self, FloatPow}, - primitives::Cuboid, - Isometry3d, Quat, Vec3A, + Isometry3d, Mat3, Quat, Vec3A, }; +use bevy_shape::Cuboid; + +pub use extrusion::BoundedExtrusion; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -17,8 +17,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -pub use extrusion::BoundedExtrusion; - /// Computes the geometric center of the given set of points. #[inline] fn point_cloud_3d_center(points: impl Iterator>) -> Vec3A { @@ -295,10 +293,8 @@ mod aabb3d_tests { use approx::assert_relative_eq; use super::Aabb3d; - use crate::{ - bounding::{BoundingSphere, BoundingVolume, IntersectsVolume}, - ops, Quat, Vec3, Vec3A, - }; + use crate::bounding::{bounded3d::BoundingSphere, BoundingVolume, IntersectsVolume}; + use bevy_math::{ops, EulerRot, Quat, Vec3, Vec3A}; #[test] fn center() { @@ -420,7 +416,7 @@ mod aabb3d_tests { min: Vec3A::new(-2.0, -2.0, -2.0), max: Vec3A::new(2.0, 2.0, 2.0), }; - let rotation = Quat::from_euler(glam::EulerRot::XYZ, PI, PI, 0.0); + let rotation = Quat::from_euler(EulerRot::XYZ, PI, PI, 0.0); let rotated = a.rotated_by(rotation); assert_relative_eq!(rotated.min, a.min); assert_relative_eq!(rotated.max, a.max); @@ -495,7 +491,7 @@ mod aabb3d_tests { } } -use crate::primitives::Sphere; +use bevy_shape::Sphere; /// A bounding sphere #[derive(Clone, Copy, Debug, PartialEq)] @@ -695,11 +691,8 @@ impl IntersectsVolume for BoundingSphere { mod bounding_sphere_tests { use approx::assert_relative_eq; - use super::BoundingSphere; - use crate::{ - bounding::{BoundingVolume, IntersectsVolume}, - ops, Quat, Vec3, Vec3A, - }; + use crate::bounding::{bounded3d::BoundingSphere, BoundingVolume, IntersectsVolume}; + use bevy_math::{ops, Quat, Vec3, Vec3A}; #[test] fn area() { diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_geometry/src/bounding/bounded3d/primitive_impls.rs similarity index 97% rename from crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs rename to crates/bevy_geometry/src/bounding/bounded3d/primitive_impls.rs index 7cf118e4d97e8..abbc29ff78637 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_geometry/src/bounding/bounded3d/primitive_impls.rs @@ -1,19 +1,19 @@ //! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). -use crate::{ - bounding::{Bounded2d, BoundingCircle, BoundingVolume}, - ops, - primitives::{ - Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Segment3d, - Sphere, Torus, Triangle2d, Triangle3d, - }, - Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A, +use crate::bounding::{ + bounded2d::{Bounded2d, BoundingCircle}, + bounded3d::{Aabb3d, Bounded3d, BoundingSphere}, + BoundingVolume, }; -#[cfg(feature = "alloc")] -use crate::primitives::Polyline3d; +use bevy_math::{ops, Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A}; +use bevy_shape::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Segment3d, Sphere, + Torus, Triangle2d, Triangle3d, +}; -use super::{Aabb3d, Bounded3d, BoundingSphere}; +#[cfg(feature = "alloc")] +use bevy_shape::Polyline3d; impl Bounded3d for Sphere { fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { @@ -366,16 +366,11 @@ impl Bounded3d for Triangle3d { #[cfg(test)] mod tests { - use crate::{bounding::BoundingVolume, ops, Isometry3d}; - use glam::{Quat, Vec3, Vec3A}; - - use crate::{ - bounding::Bounded3d, - primitives::{ - Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, - Segment3d, Sphere, Torus, Triangle3d, - }, - Dir3, + use crate::bounding::{bounded3d::Bounded3d, BoundingVolume}; + use bevy_math::{ops, Dir3, Isometry3d, Quat, Vec3, Vec3A}; + use bevy_shape::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, + Segment3d, Sphere, Torus, Triangle3d, }; #[test] diff --git a/crates/bevy_math/src/bounding/mod.rs b/crates/bevy_geometry/src/bounding/mod.rs similarity index 97% rename from crates/bevy_math/src/bounding/mod.rs rename to crates/bevy_geometry/src/bounding/mod.rs index acd975d068932..57d089a900a11 100644 --- a/crates/bevy_math/src/bounding/mod.rs +++ b/crates/bevy_geometry/src/bounding/mod.rs @@ -106,12 +106,7 @@ pub trait IntersectsVolume { fn intersects(&self, volume: &Volume) -> bool; } -mod bounded2d; +pub(crate) mod bounded2d; pub use bounded2d::*; -mod bounded3d; +pub(crate) mod bounded3d; pub use bounded3d::*; - -mod raycast2d; -pub use raycast2d::*; -mod raycast3d; -pub use raycast3d::*; diff --git a/crates/bevy_math/src/primitives/inset.rs b/crates/bevy_geometry/src/inset.rs similarity index 94% rename from crates/bevy_math/src/primitives/inset.rs rename to crates/bevy_geometry/src/inset.rs index 7690593ad5c66..b06d114ff4f21 100644 --- a/crates/bevy_math/src/primitives/inset.rs +++ b/crates/bevy_geometry/src/inset.rs @@ -1,10 +1,6 @@ -use crate::{ - ops, - primitives::{ - Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, - Triangle2d, - }, - Vec2, +use bevy_math::{ops, Vec2}; +use bevy_shape::{ + Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, Triangle2d, }; /// A primitive that can be resized uniformly. diff --git a/crates/bevy_geometry/src/lib.rs b/crates/bevy_geometry/src/lib.rs new file mode 100644 index 0000000000000..d503450b992de --- /dev/null +++ b/crates/bevy_geometry/src/lib.rs @@ -0,0 +1,56 @@ +#![forbid(unsafe_code)] +#![cfg_attr( + any(docsrs, docsrs_dep), + expect( + internal_features, + reason = "rustdoc_internals is needed for fake_variadic" + ) +)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] +#![no_std] + +//! This module defines functionality for primitive shapes. + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "alloc")] +extern crate alloc; + +pub mod bounding; +pub mod inset; +pub mod measured; +pub mod ray; +pub mod ring; + +#[cfg(feature = "rand")] +pub mod sampling; + +/// The geometry prelude. +/// +/// This includes all geometry traits defined in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::bounding::{bounded2d::*, bounded3d::*, BoundingVolume, IntersectsVolume}; + + #[doc(hidden)] + pub use crate::inset::*; + + #[doc(hidden)] + pub use crate::measured::*; + + #[doc(hidden)] + pub use crate::ray::{raycast2d::*, raycast3d::*, Ray2dIntersection, Ray3dIntersection}; + + #[doc(hidden)] + pub use crate::ring::*; + + #[doc(hidden)] + #[cfg(feature = "rand")] + pub use crate::sampling::*; +} diff --git a/crates/bevy_geometry/src/measured.rs b/crates/bevy_geometry/src/measured.rs new file mode 100644 index 0000000000000..371e50c869587 --- /dev/null +++ b/crates/bevy_geometry/src/measured.rs @@ -0,0 +1,605 @@ +use std::f32::consts::{FRAC_PI_3, PI}; + +use bevy_math::{ops, FloatPow}; +use bevy_shape::{ + Annulus, Capsule2d, Capsule3d, Circle, CircularSector, CircularSegment, Cone, ConicalFrustum, + Cuboid, Cylinder, Ellipse, Extrusion, Plane3d, Primitive2d, Rectangle, RegularPolygon, Rhombus, + Sphere, Tetrahedron, Torus, Triangle2d, Triangle3d, +}; + +/// A trait for getting measurements of 2D shapes +pub trait Measured2d { + /// Get the perimeter of the shape + fn perimeter(&self) -> f32; + + /// Get the area of the shape + fn area(&self) -> f32; +} + +/// A trait for getting measurements of 3D shapes +pub trait Measured3d { + /// Get the surface area of the shape + fn area(&self) -> f32; + + /// Get the volume of the shape + fn volume(&self) -> f32; +} + +impl Measured2d for Circle { + /// Get the area of the circle + #[inline] + fn area(&self) -> f32 { + PI * self.radius.squared() + } + + /// Get the perimeter or circumference of the circle + #[inline] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * self.radius + } +} + +impl Measured2d for CircularSector { + #[inline] + fn area(&self) -> f32 { + self.arc.radius.squared() * self.arc.half_angle + } + + #[inline] + fn perimeter(&self) -> f32 { + if self.half_angle() >= PI { + self.arc.radius * 2.0 * PI + } else { + 2.0 * self.radius() + self.arc_length() + } + } +} + +impl Measured2d for CircularSegment { + #[inline] + fn area(&self) -> f32 { + 0.5 * self.arc.radius.squared() * (self.arc.angle() - ops::sin(self.arc.angle())) + } + + #[inline] + fn perimeter(&self) -> f32 { + self.chord_length() + self.arc_length() + } +} + +impl Measured2d for Ellipse { + /// Get the area of the ellipse + #[inline] + fn area(&self) -> f32 { + PI * self.half_size.x * self.half_size.y + } + + #[inline] + /// Get an approximation for the perimeter or circumference of the ellipse. + /// + /// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases. + fn perimeter(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + // In the case that `a == b`, the ellipse is a circle + if a / b - 1. < 1e-5 { + return PI * (a + b); + }; + + // In the case that `a` is much larger than `b`, the ellipse is a line + if a / b > 1e4 { + return 4. * a; + }; + + // These values are the result of (0.5 choose n)^2 where n is the index in the array + // They could be calculated on the fly but hardcoding them yields more accurate and faster results + // because the actual calculation for these values involves factorials and numbers > 10^23 + const BINOMIAL_COEFFICIENTS: [f32; 21] = [ + 1., + 0.25, + 0.015625, + 0.00390625, + 0.0015258789, + 0.00074768066, + 0.00042057037, + 0.00025963783, + 0.00017140154, + 0.000119028846, + 0.00008599834, + 0.00006414339, + 0.000049109784, + 0.000038430585, + 0.000030636627, + 0.000024815668, + 0.000020380836, + 0.000016942893, + 0.000014236736, + 0.000012077564, + 0.000010333865, + ]; + + // The algorithm used here is the Gauss-Kummer infinite series expansion of the elliptic integral expression for the perimeter of ellipses + // For more information see https://www.wolframalpha.com/input/?i=gauss-kummer+series + // We only use the terms up to `i == 20` for this approximation + let h = ((a - b) / (a + b)).squared(); + + PI * (a + b) + * (0..=20) + .map(|i| BINOMIAL_COEFFICIENTS[i] * ops::powf(h, i as f32)) + .sum::() + } +} + +impl Measured2d for Annulus { + /// Get the area of the annulus + #[inline] + fn area(&self) -> f32 { + PI * (self.outer_circle.radius.squared() - self.inner_circle.radius.squared()) + } + + /// Get the perimeter or circumference of the annulus, + /// which is the sum of the perimeters of the inner and outer circles. + #[inline] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) + } +} + +impl Measured2d for Rhombus { + /// Get the area of the rhombus + #[inline] + fn area(&self) -> f32 { + 2.0 * self.half_diagonals.x * self.half_diagonals.y + } + + /// Get the perimeter of the rhombus + #[inline] + fn perimeter(&self) -> f32 { + 4.0 * self.side() + } +} + +impl Measured2d for Triangle2d { + /// Get the area of the triangle + #[inline] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + 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 + #[inline] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + + let ab = a.distance(b); + let bc = b.distance(c); + let ca = c.distance(a); + + ab + bc + ca + } +} + +impl Measured2d for Rectangle { + /// Get the area of the rectangle + #[inline] + fn area(&self) -> f32 { + 4.0 * self.half_size.x * self.half_size.y + } + + /// Get the perimeter of the rectangle + #[inline] + fn perimeter(&self) -> f32 { + 4.0 * (self.half_size.x + self.half_size.y) + } +} + +impl Measured2d for RegularPolygon { + /// Get the area of the regular polygon + #[inline] + fn area(&self) -> f32 { + let angle: f32 = 2.0 * PI / (self.sides as f32); + (self.sides as f32) * self.circumradius().squared() * ops::sin(angle) / 2.0 + } + + /// Get the perimeter of the regular polygon. + /// This is the sum of its sides + #[inline] + fn perimeter(&self) -> f32 { + self.sides as f32 * self.side_length() + } +} + +impl Measured2d for Capsule2d { + /// Get the area of the capsule + #[inline] + fn area(&self) -> f32 { + // pi*r^2 + (2r)*l + PI * self.radius.squared() + self.to_inner_rectangle().area() + } + + /// Get the perimeter of the capsule + #[inline] + fn perimeter(&self) -> f32 { + // 2pi*r + 2l + 2.0 * PI * self.radius + 4.0 * self.half_length + } +} + +impl Measured3d for Sphere { + /// Get the surface area of the sphere + #[inline] + fn area(&self) -> f32 { + 4.0 * PI * self.radius.squared() + } + + /// Get the volume of the sphere + #[inline] + fn volume(&self) -> f32 { + 4.0 * FRAC_PI_3 * self.radius.cubed() + } +} + +impl Measured2d for Plane3d { + #[inline] + fn area(&self) -> f32 { + self.half_size.element_product() * 4.0 + } + + #[inline] + fn perimeter(&self) -> f32 { + self.half_size.element_sum() * 4.0 + } +} + +impl Measured3d for Cuboid { + /// Get the surface area of the cuboid + #[inline] + fn area(&self) -> f32 { + 8.0 * (self.half_size.x * self.half_size.y + + self.half_size.y * self.half_size.z + + self.half_size.x * self.half_size.z) + } + + /// Get the volume of the cuboid + #[inline] + fn volume(&self) -> f32 { + 8.0 * self.half_size.x * self.half_size.y * self.half_size.z + } +} + +impl Measured3d for Cylinder { + /// Get the total surface area of the cylinder + #[inline] + fn area(&self) -> f32 { + 2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height) + } + + /// Get the volume of the cylinder + #[inline] + fn volume(&self) -> f32 { + self.base_area() * 2.0 * self.half_height + } +} + +impl Measured3d for Capsule3d { + /// Get the surface area of the capsule + #[inline] + fn area(&self) -> f32 { + // Modified version of 2pi * r * (2r + h) + 4.0 * PI * self.radius * (self.radius + self.half_length) + } + + /// Get the volume of the capsule + #[inline] + fn volume(&self) -> f32 { + // Modified version of pi * r^2 * (4/3 * r + a) + let diameter = self.radius * 2.0; + PI * self.radius * diameter * (diameter / 3.0 + self.half_length) + } +} + +impl Measured3d for Cone { + /// Get the total surface area of the cone + #[inline] + fn area(&self) -> f32 { + self.base_area() + self.lateral_area() + } + + /// Get the volume of the cone + #[inline] + fn volume(&self) -> f32 { + (self.base_area() * self.height) / 3.0 + } +} + +impl Measured3d for ConicalFrustum { + #[inline] + fn volume(&self) -> f32 { + FRAC_PI_3 + * self.height + * (self.radius_bottom * self.radius_bottom + + self.radius_top * self.radius_top + + self.radius_top * self.radius_bottom) + } + #[inline] + fn area(&self) -> f32 { + self.bottom_base_area() + self.top_base_area() + self.lateral_area() + } +} + +impl Measured3d for Torus { + /// Get the surface area of the torus. Note that this only produces + /// the expected result when the torus has a ring and isn't self-intersecting + #[inline] + fn area(&self) -> f32 { + 4.0 * PI.squared() * self.major_radius * self.minor_radius + } + + /// Get the volume of the torus. Note that this only produces + /// the expected result when the torus has a ring and isn't self-intersecting + #[inline] + fn volume(&self) -> f32 { + 2.0 * PI.squared() * self.major_radius * self.minor_radius.squared() + } +} + +impl Measured2d for Triangle3d { + /// Get the area of the triangle. + #[inline] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() / 2.0 + } + + /// Get the perimeter of the triangle. + #[inline] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + a.distance(b) + b.distance(c) + c.distance(a) + } +} + +impl Measured3d for Tetrahedron { + /// Get the surface area of the tetrahedron. + #[inline] + fn area(&self) -> f32 { + let [a, b, c, d] = self.vertices; + let ab = b - a; + let ac = c - a; + let ad = d - a; + let bc = c - b; + let bd = d - b; + (ab.cross(ac).length() + + ab.cross(ad).length() + + ac.cross(ad).length() + + bc.cross(bd).length()) + / 2.0 + } + + /// Get the volume of the tetrahedron. + #[inline] + fn volume(&self) -> f32 { + ops::abs(self.signed_volume()) + } +} + +impl Measured3d for Extrusion { + /// Get the surface area of the extrusion + fn area(&self) -> f32 { + 2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter()) + } + + /// Get the volume of the extrusion + fn volume(&self) -> f32 { + 2. * self.base_shape.area() * self.half_depth + } +} + +#[cfg(test)] +mod tests { + // Reference values were computed by hand and/or with external tools + + use super::*; + use approx::assert_relative_eq; + use bevy_math::{Vec2, Vec3}; + + #[test] + fn circle_math() { + let circle = Circle { radius: 3.0 }; + assert_eq!(circle.area(), 28.274334, "incorrect area"); + assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); + } + + #[test] + fn capsule2d_math() { + let capsule = Capsule2d::new(2.0, 9.0); + assert_eq!(capsule.area(), 48.566371, "incorrect area"); + assert_eq!(capsule.perimeter(), 30.566371, "incorrect perimeter"); + } + + #[test] + fn annulus_math() { + let annulus = Annulus::new(2.5, 3.5); + assert_eq!(annulus.area(), 18.849556, "incorrect area"); + assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter"); + } + + #[test] + fn rhombus_math() { + let rhombus = Rhombus::new(3.0, 4.0); + assert_eq!(rhombus.area(), 6.0, "incorrect area"); + assert_eq!(rhombus.perimeter(), 10.0, "incorrect perimeter"); + let rhombus = Rhombus::new(0.0, 0.0); + assert_eq!(rhombus.area(), 0.0, "incorrect area"); + assert_eq!(rhombus.perimeter(), 0.0, "incorrect perimeter"); + } + + #[test] + fn ellipse_math() { + let ellipse = Ellipse::new(3.0, 1.0); + assert_eq!(ellipse.area(), 9.424778, "incorrect area"); + } + + #[test] + fn ellipse_perimeter() { + let circle = Ellipse::new(1., 1.); + assert_relative_eq!(circle.perimeter(), 6.2831855); + + let line = Ellipse::new(75_000., 0.5); + assert_relative_eq!(line.perimeter(), 300_000.); + + let ellipse = Ellipse::new(0.5, 2.); + assert_relative_eq!(ellipse.perimeter(), 8.578423); + + let ellipse = Ellipse::new(5., 3.); + assert_relative_eq!(ellipse.perimeter(), 25.526999); + } + + #[test] + fn triangle2d_math() { + let triangle = Triangle2d::new( + Vec2::new(-2.0, -1.0), + Vec2::new(1.0, 4.0), + Vec2::new(7.0, 0.0), + ); + assert_eq!(triangle.area(), 21.0, "incorrect area"); + assert_eq!(triangle.perimeter(), 22.097439, "incorrect perimeter"); + } + + #[test] + fn rectangle_math() { + let rectangle = Rectangle::new(3.0, 7.0); + assert_eq!(rectangle.area(), 21.0, "incorrect area"); + assert_eq!(rectangle.perimeter(), 20.0, "incorrect perimeter"); + } + + #[test] + fn regular_polygon_math() { + let polygon = RegularPolygon::new(3.0, 6); + assert_relative_eq!(polygon.area(), 23.38268, epsilon = 0.00001); + assert_eq!(polygon.perimeter(), 18.0, "incorrect perimeter"); + } + + #[test] + fn sphere_math() { + let sphere = Sphere { radius: 4.0 }; + assert_eq!(sphere.area(), 201.06193, "incorrect area"); + assert_eq!(sphere.volume(), 268.08257, "incorrect volume"); + } + + #[test] + fn cuboid_math() { + let cuboid = Cuboid::new(3.0, 7.0, 2.0); + assert_eq!(cuboid.area(), 82.0, "incorrect area"); + assert_eq!(cuboid.volume(), 42.0, "incorrect volume"); + } + + #[test] + fn cylinder_math() { + let cylinder = Cylinder::new(2.0, 9.0); + assert_relative_eq!(cylinder.area(), 138.23007); + assert_eq!(cylinder.volume(), 113.097336, "incorrect volume"); + } + + #[test] + fn capsule3d_math() { + let capsule = Capsule3d::new(2.0, 9.0); + assert_eq!(capsule.area(), 163.36282, "incorrect area"); + assert_relative_eq!(capsule.volume(), 146.60765); + } + + #[test] + fn cone_math() { + let cone = Cone { + radius: 2.0, + height: 9.0, + }; + assert_relative_eq!(cone.area(), 70.49447); + assert_eq!(cone.volume(), 37.699111, "incorrect volume"); + } + + #[test] + fn conical_frustum_math() { + let frustum = ConicalFrustum { + height: 9.0, + radius_top: 1.0, + radius_bottom: 2.0, + }; + assert_eq!(frustum.area(), 101.05296, "incorrect surface area"); + assert_eq!(frustum.volume(), 65.97345, "incorrect volume"); + } + + #[test] + fn torus_math() { + let torus = Torus { + minor_radius: 0.3, + major_radius: 2.8, + }; + assert_relative_eq!(torus.area(), 33.16187); + assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001); + } + + #[test] + fn tetrahedron_math() { + let tetrahedron = Tetrahedron { + vertices: [ + Vec3::new(0.3, 1.0, 1.7), + Vec3::new(-2.0, -1.0, 0.0), + Vec3::new(1.8, 0.5, 1.0), + Vec3::new(-1.0, -2.0, 3.5), + ], + }; + assert_eq!(tetrahedron.area(), 19.251068, "incorrect area"); + assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume"); + + assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area"); + assert_eq!( + Tetrahedron::default().volume(), + 0.33333334, + "incorrect volume" + ); + } + + #[test] + fn extrusion_math() { + let circle = Circle::new(0.75); + let cylinder = Extrusion::new(circle, 2.5); + assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); + assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); + + let annulus = Annulus::new(0.25, 1.375); + let tube = Extrusion::new(annulus, 0.333); + assert_eq!(tube.area(), 14.886437, "incorrect surface area"); + assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); + + let polygon = RegularPolygon::new(3.8, 7); + let regular_prism = Extrusion::new(polygon, 1.25); + assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); + assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); + } + + #[test] + fn triangle3d_math() { + // Default triangle tests + let default_triangle = Triangle3d::default(); + assert_eq!(default_triangle.area(), 0.5, "incorrect area"); + assert_relative_eq!( + default_triangle.perimeter(), + 1.0 + 2.0 * ops::sqrt(1.25_f32), + epsilon = 10e-9 + ); + + // Arbitrary triangle tests + let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)]; + let triangle = Triangle3d::new(a, b, c); + + assert_eq!(triangle.area(), 3.0233467, "incorrect area"); + assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter"); + } +} diff --git a/crates/bevy_geometry/src/ray/mod.rs b/crates/bevy_geometry/src/ray/mod.rs new file mode 100644 index 0000000000000..68f6ffa3ad4ba --- /dev/null +++ b/crates/bevy_geometry/src/ray/mod.rs @@ -0,0 +1,57 @@ +pub(crate) mod raycast2d; +pub use raycast2d::*; +pub(crate) mod raycast3d; +pub use raycast3d::*; + +use bevy_math::{Ray2d, Ray3d, Vec2, Vec3}; +use bevy_shape::{InfinitePlane3d, Plane2d}; + +pub trait Ray2dIntersection { + /// Returns the distance to a [`bevy_shape::Plane2d`] if the ray intersects it + /// + /// Use [`Ray2d::plane_intersection_point`] to get the intersection point directly. + fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option; + + /// Returns the intersection point of the ray with a plane, if it exists. + /// + /// Calls [`Ray2d::get_point`] on the result of [`Ray2d::intersect_plane`]. + fn plane_intersection_point(&self, plane_origin: Vec2, plane: Plane2d) -> Option; +} + +impl Ray2dIntersection for Ray2d { + #[inline] + fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option { + self.intersect_plane_normal(plane_origin, plane.normal) + } + + #[inline] + fn plane_intersection_point(&self, plane_origin: Vec2, plane: Plane2d) -> Option { + self.intersect_plane(plane_origin, plane) + .map(|distance| self.get_point(distance)) + } +} + +pub trait Ray3dIntersection { + /// Returns the distance to a [`bevy_shape::InfinitePlane3d`] if the ray intersects it + /// + /// Use [`Ray3d::plane_intersection_point`] to get the intersection point directly. + fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option; + + /// Returns the intersection point of the ray with a plane, if it exists. + /// + /// Calls [`Ray3d::get_point`] on the result of [`Ray3d::intersect_plane`]. + fn plane_intersection_point(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option; +} + +impl Ray3dIntersection for Ray3d { + #[inline] + fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option { + self.intersect_plane_normal(plane_origin, plane.normal) + } + + #[inline] + fn plane_intersection_point(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option { + self.intersect_plane(plane_origin, plane) + .map(|distance| self.get_point(distance)) + } +} diff --git a/crates/bevy_math/src/bounding/raycast2d.rs b/crates/bevy_geometry/src/ray/raycast2d.rs similarity index 83% rename from crates/bevy_math/src/bounding/raycast2d.rs rename to crates/bevy_geometry/src/ray/raycast2d.rs index aa8fcf2591dcc..ded06e46e8c59 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_geometry/src/ray/raycast2d.rs @@ -1,97 +1,32 @@ -use super::{Aabb2d, BoundingCircle, IntersectsVolume}; -use crate::{ - ops::{self, FloatPow}, - Dir2, Ray2d, Vec2, +use crate::bounding::{ + bounded2d::{Aabb2d, BoundingCircle}, + IntersectsVolume, }; - +use bevy_math::{Dir2, Ray2d, RayCast2d, Vec2}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -/// A raycast intersection test for 2D bounding volumes -#[derive(Clone, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))] -pub struct RayCast2d { - /// The ray for the test - pub ray: Ray2d, - /// The maximum distance for the ray - pub max: f32, - /// The multiplicative inverse direction of the ray - direction_recip: Vec2, +pub trait Aabb2dIntersection { + /// Get the distance of an intersection with an [`Aabb2d`], if any. + fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option; } -impl RayCast2d { - /// Construct a [`RayCast2d`] from an origin, [`Dir2`], and max distance. - pub fn new(origin: Vec2, direction: Dir2, max: f32) -> Self { - Self::from_ray(Ray2d { origin, direction }, max) - } - - /// Construct a [`RayCast2d`] from a [`Ray2d`] and max distance. - pub fn from_ray(ray: Ray2d, max: f32) -> Self { - Self { - ray, - direction_recip: ray.direction.recip(), - max, - } - } - - /// Get the cached multiplicative inverse of the direction of the ray. - pub const fn direction_recip(&self) -> Vec2 { - self.direction_recip - } +pub trait BoundingCircleIntersection { + /// Get the distance of an intersection with a [`BoundingCircle`], if any. + fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option; +} +impl Aabb2dIntersection for RayCast2d { /// Get the distance of an intersection with an [`Aabb2d`], if any. - pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { - let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() { - (aabb.min.x, aabb.max.x) - } else { - (aabb.max.x, aabb.min.x) - }; - let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() { - (aabb.min.y, aabb.max.y) - } else { - (aabb.max.y, aabb.min.y) - }; - - // Calculate the minimum/maximum time for each axis based on how much the direction goes that - // way. These values can get arbitrarily large, or even become NaN, which is handled by the - // min/max operations below - let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x; - let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y; - let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x; - let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y; - - // An axis that is not relevant to the ray direction will be NaN. When one of the arguments - // to min/max is NaN, the other argument is used. - // An axis for which the direction is the wrong way will return an arbitrarily large - // negative value. - let tmin = tmin_x.max(tmin_y).max(0.); - let tmax = tmax_y.min(tmax_x).min(self.max); - - if tmin <= tmax { - Some(tmin) - } else { - None - } + fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { + self.aabb_intersection_at_min_max(aabb.min, aabb.max) } +} +impl BoundingCircleIntersection for RayCast2d { /// Get the distance of an intersection with a [`BoundingCircle`], if any. - pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option { - let offset = self.ray.origin - circle.center; - let projected = offset.dot(*self.ray.direction); - let cross = offset.perp_dot(*self.ray.direction); - let distance_squared = circle.radius().squared() - cross.squared(); - if distance_squared < 0. - || ops::copysign(projected.squared(), -projected) < -distance_squared - { - None - } else { - let toi = -projected - ops::sqrt(distance_squared); - if toi > self.max { - None - } else { - Some(toi.max(0.)) - } - } + fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option { + self.circle_intersection_at_center_radius(circle.center, circle.radius()) } } @@ -185,6 +120,8 @@ impl IntersectsVolume for BoundingCircleCast { #[cfg(test)] mod tests { + use bevy_math::ops; + use super::*; const EPSILON: f32 = 0.001; @@ -285,7 +222,6 @@ mod tests { test.intersects(&volume), "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - let actual_distance = test.circle_intersection_at(&volume); assert_eq!( actual_distance, @@ -393,7 +329,6 @@ mod tests { test.intersects(&volume), "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", ); - let actual_distance = test.aabb_intersection_at(&volume); assert_eq!( actual_distance, diff --git a/crates/bevy_math/src/bounding/raycast3d.rs b/crates/bevy_geometry/src/ray/raycast3d.rs similarity index 84% rename from crates/bevy_math/src/bounding/raycast3d.rs rename to crates/bevy_geometry/src/ray/raycast3d.rs index ad21766c742e3..cc6980b7a4a16 100644 --- a/crates/bevy_math/src/bounding/raycast3d.rs +++ b/crates/bevy_geometry/src/ray/raycast3d.rs @@ -1,94 +1,32 @@ -use super::{Aabb3d, BoundingSphere, IntersectsVolume}; -use crate::{ - ops::{self, FloatPow}, - Dir3A, Ray3d, Vec3A, +use crate::bounding::{ + bounded3d::{Aabb3d, BoundingSphere}, + IntersectsVolume, }; - +use bevy_math::{Dir3A, Ray3d, RayCast3d, Vec3A}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -/// A raycast intersection test for 3D bounding volumes -#[derive(Clone, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))] -pub struct RayCast3d { - /// The origin of the ray. - pub origin: Vec3A, - /// The direction of the ray. - pub direction: Dir3A, - /// The maximum distance for the ray - pub max: f32, - /// The multiplicative inverse direction of the ray - direction_recip: Vec3A, +pub trait Aabb3dIntersection { + /// Get the distance of an intersection with an [`Aabb3d`], if any. + fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option; } -impl RayCast3d { - /// Construct a [`RayCast3d`] from an origin, [direction], and max distance. - /// - /// [direction]: crate::direction::Dir3 - pub fn new(origin: impl Into, direction: impl Into, max: f32) -> Self { - let direction = direction.into(); - Self { - origin: origin.into(), - direction, - direction_recip: direction.recip(), - max, - } - } - - /// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance. - pub fn from_ray(ray: Ray3d, max: f32) -> Self { - Self::new(ray.origin, ray.direction, max) - } - - /// Get the cached multiplicative inverse of the direction of the ray. - pub const fn direction_recip(&self) -> Vec3A { - self.direction_recip - } +pub trait BoundingSphereIntersection { + /// Get the distance of an intersection with a [`BoundingSphere`], if any. + fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option; +} +impl Aabb3dIntersection for RayCast3d { /// Get the distance of an intersection with an [`Aabb3d`], if any. - pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option { - let positive = self.direction.signum().cmpgt(Vec3A::ZERO); - let min = Vec3A::select(positive, aabb.min, aabb.max); - let max = Vec3A::select(positive, aabb.max, aabb.min); - - // Calculate the minimum/maximum time for each axis based on how much the direction goes that - // way. These values can get arbitrarily large, or even become NaN, which is handled by the - // min/max operations below - let tmin = (min - self.origin) * self.direction_recip; - let tmax = (max - self.origin) * self.direction_recip; - - // An axis that is not relevant to the ray direction will be NaN. When one of the arguments - // to min/max is NaN, the other argument is used. - // An axis for which the direction is the wrong way will return an arbitrarily large - // negative value. - let tmin = tmin.max_element().max(0.); - let tmax = tmax.min_element().min(self.max); - - if tmin <= tmax { - Some(tmin) - } else { - None - } + fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option { + self.aabb_intersection_at_min_max(aabb.min, aabb.max) } +} +impl BoundingSphereIntersection for RayCast3d { /// Get the distance of an intersection with a [`BoundingSphere`], if any. - pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { - let offset = self.origin - sphere.center; - 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. - || ops::copysign(projected.squared(), -projected) < -distance_squared - { - None - } else { - let toi = -projected - ops::sqrt(distance_squared); - if toi > self.max { - None - } else { - Some(toi.max(0.)) - } - } + fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { + self.sphere_intersection_at_center_radius(sphere.center, sphere.radius()) } } @@ -139,6 +77,7 @@ impl AabbCast3d { pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option { aabb.min -= self.aabb.max; aabb.max -= self.aabb.min; + self.ray.aabb_intersection_at(&aabb) } } @@ -197,7 +136,7 @@ impl IntersectsVolume for BoundingSphereCast { #[cfg(test)] mod tests { use super::*; - use crate::{Dir3, Vec3}; + use bevy_math::{ops, Dir3, Vec3}; const EPSILON: f32 = 0.001; diff --git a/crates/bevy_geometry/src/ring.rs b/crates/bevy_geometry/src/ring.rs new file mode 100644 index 0000000000000..21b96c73fe5b6 --- /dev/null +++ b/crates/bevy_geometry/src/ring.rs @@ -0,0 +1,110 @@ +use bevy_math::{Isometry2d, Isometry3d}; +use bevy_shape::Primitive2d; + +use crate::{ + bounding::{Aabb2d, Aabb3d, Bounded2d, BoundedExtrusion, BoundingCircle, BoundingSphere}, + inset::Inset, + measured::Measured2d, +}; + +/// A 2D shape representing the ring version of a base shape. +/// +/// The `inner_shape` forms the "hollow" of the `outer_shape`. +/// +/// The resulting shapes are rings or hollow shapes. +/// For example, a circle becomes an annulus. +/// +/// # Warning +/// +/// The `outer_shape` must contain the `inner_shape` for the generated meshes to be accurate. +/// +/// If there are vertices in the `inner_shape` that escape the `outer_shape` +/// (for example, if the `inner_shape` is in fact larger), +/// it may result in incorrect geometries. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Ring { + /// The outer shape + pub outer_shape: P, + /// The inner shape (the same shape of a different size) + pub inner_shape: P, +} + +impl Ring

{ + /// Create a new `Ring` from a given `outer_shape` and `inner_shape`. + /// + /// If the primitive implements [`Inset`] and you would like a uniform thickness, consider using [`ToRing::to_ring`] + pub const fn new(outer_shape: P, inner_shape: P) -> Self { + Self { + outer_shape, + inner_shape, + } + } +} + +impl Primitive2d for Ring {} + +impl Ring

{ + /// Generate a `Ring` from a given `primitive` and a `thickness`. + pub fn from_primitive_and_thickness(primitive: P, thickness: f32) -> Self { + let hollow = primitive.clone().inset(thickness); + Ring::new(primitive, hollow) + } +} + +impl Measured2d for Ring

{ + #[inline] + fn area(&self) -> f32 { + self.outer_shape.area() - self.inner_shape.area() + } + + #[inline] + fn perimeter(&self) -> f32 { + self.outer_shape.perimeter() + self.inner_shape.perimeter() + } +} + +/// Provides a convenience method for converting a primitive to a [`Ring`], with a given thickness. +/// +/// The primitive must implement [`Inset`]. +pub trait ToRing: Primitive2d + Inset +where + Self: Sized, +{ + /// Construct a `Ring` + fn to_ring(self, thickness: f32) -> Ring; +} + +impl

ToRing for P +where + P: Primitive2d + Clone + Inset, +{ + fn to_ring(self, thickness: f32) -> Ring { + Ring::from_primitive_and_thickness(self, thickness) + } +} + +impl Bounded2d for Ring

{ + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + self.outer_shape.aabb_2d(isometry) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + self.outer_shape.bounding_circle(isometry) + } +} + +impl BoundedExtrusion for Ring { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + self.outer_shape.extrusion_aabb_3d(half_depth, isometry) + } + + fn extrusion_bounding_sphere( + &self, + half_depth: f32, + isometry: impl Into, + ) -> BoundingSphere { + self.outer_shape + .extrusion_bounding_sphere(half_depth, isometry) + } +} diff --git a/crates/bevy_math/src/sampling/mesh_sampling.rs b/crates/bevy_geometry/src/sampling/mesh_sampling.rs similarity index 94% rename from crates/bevy_math/src/sampling/mesh_sampling.rs rename to crates/bevy_geometry/src/sampling/mesh_sampling.rs index 33b55a80b7d6d..8f6f22fe45526 100644 --- a/crates/bevy_math/src/sampling/mesh_sampling.rs +++ b/crates/bevy_geometry/src/sampling/mesh_sampling.rs @@ -1,10 +1,10 @@ //! Functionality related to random sampling from triangle meshes. -use crate::{ - primitives::{Measured2d, Triangle3d}, - ShapeSample, Vec3, -}; +use crate::measured::Measured2d; +use crate::sampling::ShapeSample; use alloc::vec::Vec; +use bevy_math::Vec3; +use bevy_shape::Triangle3d; use rand::RngExt; use rand_distr::{ weighted::{Error as WeightedError, WeightedAliasIndex}, diff --git a/crates/bevy_geometry/src/sampling/mod.rs b/crates/bevy_geometry/src/sampling/mod.rs new file mode 100644 index 0000000000000..a8cec0a27779c --- /dev/null +++ b/crates/bevy_geometry/src/sampling/mod.rs @@ -0,0 +1,11 @@ +//! This module contains tools related to random sampling. +//! +//! To use this, the "rand" feature must be enabled. + +#[cfg(feature = "alloc")] +pub mod mesh_sampling; +pub mod shape_sampling; + +#[cfg(feature = "alloc")] +pub use mesh_sampling::*; +pub use shape_sampling::*; diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_geometry/src/sampling/shape_sampling.rs similarity index 98% rename from crates/bevy_math/src/sampling/shape_sampling.rs rename to crates/bevy_geometry/src/sampling/shape_sampling.rs index a2e02675c9ce8..87ab9e00b03c6 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_geometry/src/sampling/shape_sampling.rs @@ -40,7 +40,11 @@ use core::f32::consts::{FRAC_PI_2, PI, TAU}; -use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3}; +use bevy_math::{ops, NormedVectorSpace, ScalarField, Vec2, Vec3}; +use bevy_shape::{ + Annulus, Capsule2d, Capsule3d, Circle, CircularSector, Cuboid, Cylinder, Extrusion, + Primitive2d, Rectangle, Rhombus, Sphere, Tetrahedron, Triangle2d, Triangle3d, +}; use rand::{ distr::{ uniform::SampleUniform, @@ -50,6 +54,8 @@ use rand::{ RngExt, }; +use crate::measured::{Measured2d, Measured3d}; + /// Exposes methods to uniformly sample a variety of primitive shapes. pub trait ShapeSample { /// The type of vector returned by the sample methods, [`Vec2`] for 2D shapes and [`Vec3`] for 3D shapes. diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 386342c6502b7..545ccb9995277 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -15,6 +15,8 @@ bevy_camera = { path = "../bevy_camera", version = "0.20.0-dev" } bevy_color = { path = "../bevy_color", version = "0.20.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.20.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 02f13609e3470..070d805f8581b 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -18,7 +18,8 @@ use bevy_ecs::{ }, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -use bevy_math::{bounding::Aabb3d, Isometry2d, Isometry3d, Vec2, Vec3}; +use bevy_geometry::bounding::Aabb3d; +use bevy_math::{Isometry2d, Isometry3d, Vec2, Vec3}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::TransformPoint; use bevy_utils::default; diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs index a8f25dcac78d8..bb10927b6f8ec 100644 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -5,13 +5,10 @@ use core::f32::consts::{FRAC_PI_2, PI}; use super::helpers::*; use bevy_color::Color; -use bevy_math::{ - primitives::{ - Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, - Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, - Triangle2d, - }, - Dir2, Isometry2d, Rot2, Vec2, +use bevy_math::{Dir2, Isometry2d, Rot2, Vec2}; +use bevy_shape::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }; use crate::{gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index b4dd923a7b009..ec9b83751f61f 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -3,12 +3,10 @@ use super::helpers::*; use bevy_color::Color; -use bevy_math::{ - primitives::{ - Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, - Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, - }, - Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3, +use bevy_math::{Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3}; +use bevy_shape::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, Primitive3d, + Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, }; use crate::{circles::SphereBuilder, gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; diff --git a/crates/bevy_gizmos_render/Cargo.toml b/crates/bevy_gizmos_render/Cargo.toml index 12c0e103446b6..03a963cbdc604 100644 --- a/crates/bevy_gizmos_render/Cargo.toml +++ b/crates/bevy_gizmos_render/Cargo.toml @@ -23,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } bevy_image = { path = "../bevy_image", version = "0.20.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.20.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.20.0-dev" } bevy_render = { path = "../bevy_render", version = "0.20.0-dev" } diff --git a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs index 51d474bce1c85..0a894728962dd 100644 --- a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs +++ b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs @@ -18,12 +18,10 @@ use bevy_ecs::{ schedule::IntoScheduleConfigs, system::{Commands, Query, Res, ResMut}, }; -use bevy_math::{ - primitives::{Cone, Cuboid, Cylinder, Torus}, - Quat, Vec3, -}; +use bevy_math::{Quat, Vec3}; use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, Meshable}; use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_shape::{Cone, Cuboid, Cylinder, Torus}; use bevy_transform::{ components::{GlobalTransform, Transform}, systems::propagate_transforms_for, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index d28bbd66012ae..f3d26baf1e7a6 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -132,6 +132,8 @@ serialize = [ "bevy_image?/serialize", "bevy_input/serialize", "bevy_math/serialize", + "bevy_shape/serialize", + "bevy_geometry/serialize", "bevy_world_serialization?/serialize", "bevy_time/serialize", "bevy_transform/serialize", @@ -418,6 +420,8 @@ std = [ "bevy_input/std", "bevy_input_focus?/std", "bevy_math/std", + "bevy_shape/std", + "bevy_geometry/std", "bevy_platform/std", "bevy_reflect/std", "bevy_state?/std", @@ -507,6 +511,12 @@ bevy_math = { path = "../bevy_math", version = "0.20.0-dev", default-features = "bevy_reflect", "nostd-libm", ] } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev", default-features = false, features = [ + "bevy_reflect", +] } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev", default-features = false, features = [ + "bevy_reflect", +] } bevy_platform = { path = "../bevy_platform", version = "0.20.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index bc278b9b4f125..230e9587e2241 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -43,6 +43,7 @@ pub use bevy_diagnostic as diagnostic; pub use bevy_ecs as ecs; #[cfg(feature = "bevy_feathers")] pub use bevy_feathers as feathers; +pub use bevy_geometry as geometry; #[cfg(feature = "bevy_gilrs")] pub use bevy_gilrs as gilrs; #[cfg(feature = "bevy_gizmos")] @@ -84,6 +85,7 @@ pub use bevy_scene as scene; pub use bevy_settings as settings; #[cfg(feature = "bevy_shader")] pub use bevy_shader as shader; +pub use bevy_shape as shape; #[cfg(feature = "bevy_solari")] pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 6b03a6adf9057..7e275e3e26d35 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -1,8 +1,8 @@ #[doc(hidden)] pub use crate::{ - app::prelude::*, ecs::prelude::*, input::prelude::*, math::prelude::*, platform::prelude::*, - reflect::prelude::*, time::prelude::*, transform::prelude::*, utils::prelude::*, - DefaultPlugins, MinimalPlugins, + app::prelude::*, ecs::prelude::*, geometry::prelude::*, input::prelude::*, math::prelude::*, + platform::prelude::*, reflect::prelude::*, shape::prelude::*, time::prelude::*, + transform::prelude::*, utils::prelude::*, DefaultPlugins, MinimalPlugins, }; #[doc(hidden)] diff --git a/crates/bevy_light/Cargo.toml b/crates/bevy_light/Cargo.toml index f27b5d7b70a8e..843628432cfde 100644 --- a/crates/bevy_light/Cargo.toml +++ b/crates/bevy_light/Cargo.toml @@ -15,6 +15,7 @@ bevy_asset = { path = "../bevy_asset", version = "0.20.0-dev" } bevy_image = { path = "../bevy_image", version = "0.20.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.20.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } diff --git a/crates/bevy_light/src/cluster/assign.rs b/crates/bevy_light/src/cluster/assign.rs index 8be51681ac313..ef8d24544f344 100644 --- a/crates/bevy_light/src/cluster/assign.rs +++ b/crates/bevy_light/src/cluster/assign.rs @@ -12,9 +12,9 @@ use bevy_ecs::{ }; use bevy_math::{ ops::{self, sin_cos}, - primitives::HalfSpace, Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _, }; +use bevy_shape::HalfSpace; use bevy_transform::components::GlobalTransform; use tracing::{error, warn}; diff --git a/crates/bevy_light/src/directional_light.rs b/crates/bevy_light/src/directional_light.rs index e65a1e1b2024e..e481cb0b29e64 100644 --- a/crates/bevy_light/src/directional_light.rs +++ b/crates/bevy_light/src/directional_light.rs @@ -7,8 +7,8 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::primitives::ViewFrustum; use bevy_reflect::prelude::*; +use bevy_shape::ViewFrustum; use bevy_transform::components::Transform; use tracing::warn; diff --git a/crates/bevy_light/src/gizmos.rs b/crates/bevy_light/src/gizmos.rs index 681082a9ea7f5..abc8037a0a530 100644 --- a/crates/bevy_light/src/gizmos.rs +++ b/crates/bevy_light/src/gizmos.rs @@ -18,12 +18,9 @@ use bevy_ecs::{ schedule::IntoScheduleConfigs, system::{Query, Res}, }; -use bevy_math::{ - ops, - primitives::{Cone, Sphere}, - Isometry3d, Quat, Vec3, -}; +use bevy_math::{ops, Isometry3d, Quat, Vec3}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_shape::{Cone, Sphere}; use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_gizmos::{ diff --git a/crates/bevy_light/src/point_light.rs b/crates/bevy_light/src/point_light.rs index 940e124f0cf04..aed3e3f790632 100644 --- a/crates/bevy_light/src/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -6,8 +6,9 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::{primitives::ViewFrustum, proj}; +use bevy_math::proj; use bevy_reflect::prelude::*; +use bevy_shape::ViewFrustum; use bevy_transform::components::{GlobalTransform, Transform}; use crate::{cluster::ClusterVisibilityClass, light_consts}; diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index 0b23653928c02..7234da9417ee3 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -6,8 +6,9 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::{primitives::ViewFrustum, proj, Affine3A, Dir3, Mat3, Mat4, Vec3}; +use bevy_math::{proj, Affine3A, Dir3, Mat3, Mat4, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::ViewFrustum; use bevy_transform::components::{GlobalTransform, Transform}; use crate::cluster::ClusterVisibilityClass; diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index 3ddbb5734f658..0cf777051618a 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -1,7 +1,4 @@ -use crate::{ - primitives::{Primitive2d, Primitive3d}, - Quat, Rot2, Vec2, Vec3, Vec3A, Vec4, -}; +use crate::{Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::f32::consts::FRAC_1_SQRT_2; use core::fmt; @@ -94,7 +91,6 @@ fn assert_is_normalized(message: &str, length_squared: f32) { )] #[doc(alias = "Direction2d")] pub struct Dir2(Vec2); -impl Primitive2d for Dir2 {} impl Dir2 { /// A unit vector pointing along the positive X axis. @@ -405,7 +401,6 @@ impl approx::UlpsEq for Dir2 { )] #[doc(alias = "Direction3d")] pub struct Dir3(Vec3); -impl Primitive3d for Dir3 {} impl Dir3 { /// A unit vector pointing along the positive X axis. @@ -809,7 +804,6 @@ impl approx::UlpsEq for Dir3 { )] #[doc(alias = "Direction3dA")] pub struct Dir3A(Vec3A); -impl Primitive3d for Dir3A {} impl Dir3A { /// A unit vector pointing along the positive X axis. diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 5495740c610d2..6ab1f0b499fdd 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -28,7 +28,6 @@ extern crate alloc; mod affine3; mod aspect_ratio; -pub mod bounding; pub mod common_traits; mod compass; pub mod cubic_splines; @@ -37,7 +36,6 @@ mod float_ord; mod isometry; mod mat3; pub mod ops; -pub mod primitives; mod ray; mod rects; mod rotation2d; @@ -58,7 +56,7 @@ pub use glam::camera::rh::proj::directx as proj; pub use isometry::{Isometry2d, Isometry3d}; pub use mat3::*; pub use ops::FloatPow; -pub use ray::{Ray2d, Ray3d}; +pub use ray::{Ray2d, Ray3d, RayCast2d, RayCast3d}; pub use rects::*; pub use rotation2d::Rot2; @@ -66,7 +64,7 @@ pub use rotation2d::Rot2; pub use curve::Curve; #[cfg(feature = "rand")] -pub use sampling::{FromRng, ShapeSample}; +pub use sampling::FromRng; /// The math prelude. /// @@ -77,12 +75,11 @@ pub mod prelude { bvec2, bvec3, bvec3a, bvec4, bvec4a, cubic_splines::{CubicNurbsError, CubicSegment, RationalSegment}, direction::{Dir2, Dir3, Dir3A}, - ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops, - primitives::*, - quat, uvec2, uvec3, uvec4, vec2, vec3, vec3a, vec4, BVec2, BVec3, BVec3A, BVec4, BVec4A, - EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, Isometry3d, Mat2, Mat3, Mat3A, - Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, - Vec2Swizzles, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, + ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops, quat, uvec2, uvec3, uvec4, vec2, vec3, + vec3a, vec4, BVec2, BVec3, BVec3A, BVec4, BVec4A, EulerRot, FloatExt, IRect, IVec2, IVec3, + IVec4, Isometry2d, Isometry3d, Mat2, Mat3, Mat3A, Mat4, Quat, Ray2d, Ray3d, RayCast2d, + RayCast3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, + Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[doc(hidden)] @@ -91,7 +88,7 @@ pub mod prelude { #[doc(hidden)] #[cfg(feature = "rand")] - pub use crate::sampling::{FromRng, ShapeSample}; + pub use crate::sampling::FromRng; #[cfg(feature = "alloc")] #[doc(hidden)] diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray/mod.rs similarity index 68% rename from crates/bevy_math/src/ray.rs rename to crates/bevy_math/src/ray/mod.rs index 0240ba2a71353..aa29acdea793f 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray/mod.rs @@ -1,8 +1,9 @@ -use crate::{ - ops, - primitives::{InfinitePlane3d, Plane2d}, - Dir2, Dir3, Vec2, Vec3, -}; +mod raycast2d; +pub use raycast2d::*; +mod raycast3d; +pub use raycast3d::*; + +use crate::{ops, Dir2, Dir3, Vec2, Vec3}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -45,10 +46,10 @@ impl Ray2d { /// /// Use [`Ray2d::plane_intersection_point`] to get the intersection point directly. #[inline] - pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option { - let denominator = plane.normal.dot(*self.direction); + pub fn intersect_plane_normal(&self, plane_origin: Vec2, plane_normal: Dir2) -> Option { + let denominator = plane_normal.dot(*self.direction); 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 { return Some(distance); } @@ -60,8 +61,12 @@ impl Ray2d { /// /// Calls [`Ray2d::get_point`] on the result of [`Ray2d::intersect_plane`]. #[inline] - pub fn plane_intersection_point(&self, plane_origin: Vec2, plane: Plane2d) -> Option { - self.intersect_plane(plane_origin, plane) + pub fn plane_normal_intersection_point( + &self, + plane_origin: Vec2, + plane_normal: Dir2, + ) -> Option { + self.intersect_plane_normal(plane_origin, plane_normal) .map(|distance| self.get_point(distance)) } } @@ -102,10 +107,10 @@ impl Ray3d { /// /// Use [`Ray3d::plane_intersection_point`] to get the intersection point directly. #[inline] - pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option { - let denominator = plane.normal.dot(*self.direction); + pub fn intersect_plane_normal(&self, plane_origin: Vec3, plane_normal: Dir3) -> Option { + let denominator = plane_normal.dot(*self.direction); 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 { return Some(distance); } @@ -117,12 +122,12 @@ impl Ray3d { /// /// Calls [`Ray3d::get_point`] on the result of [`Ray3d::intersect_plane`]. #[inline] - pub fn plane_intersection_point( + pub fn plane_normal_intersection_point( &self, plane_origin: Vec3, - plane: InfinitePlane3d, + plane_normal: Dir3, ) -> Option { - self.intersect_plane(plane_origin, plane) + self.intersect_plane_normal(plane_origin, plane_normal) .map(|distance| self.get_point(distance)) } } @@ -137,37 +142,40 @@ mod tests { // Orthogonal, and test that an inverse plane_normal has the same result assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::Y)), + ray.intersect_plane_normal(Vec2::Y, Dir2::new(Vec2::Y).unwrap()), Some(1.0) ); assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::NEG_Y)), + ray.intersect_plane_normal(Vec2::Y, Dir2::new(Vec2::NEG_Y).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::Y)) + .intersect_plane_normal(Vec2::NEG_Y, Dir2::new(Vec2::Y).unwrap()) .is_none()); assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::NEG_Y)) + .intersect_plane_normal(Vec2::NEG_Y, Dir2::new(Vec2::NEG_Y).unwrap()) .is_none()); // Diagonal assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::ONE)), + ray.intersect_plane_normal(Vec2::Y, Dir2::new(Vec2::ONE).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::ONE)) + .intersect_plane_normal(Vec2::NEG_Y, Dir2::new(Vec2::ONE).unwrap()) .is_none()); // Parallel assert!(ray - .intersect_plane(Vec2::X, Plane2d::new(Vec2::X)) + .intersect_plane_normal(Vec2::X, Dir2::new(Vec2::X).unwrap()) .is_none()); // Parallel with simulated rounding error assert!(ray - .intersect_plane(Vec2::X, Plane2d::new(Vec2::X + Vec2::Y * f32::EPSILON)) + .intersect_plane_normal( + Vec2::X, + Dir2::new(Vec2::X + Vec2::Y * f32::EPSILON).unwrap() + ) .is_none()); } @@ -177,39 +185,39 @@ mod tests { // Orthogonal, and test that an inverse plane_normal has the same result assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::Z)), + ray.intersect_plane_normal(Vec3::Z, Dir3::new(Vec3::Z).unwrap()), Some(1.0) ); assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::NEG_Z)), + ray.intersect_plane_normal(Vec3::Z, Dir3::new(Vec3::NEG_Z).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::Z)) + .intersect_plane_normal(Vec3::NEG_Z, Dir3::new(Vec3::Z).unwrap()) .is_none()); assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::NEG_Z)) + .intersect_plane_normal(Vec3::NEG_Z, Dir3::new(Vec3::NEG_Z).unwrap()) .is_none()); // Diagonal assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::ONE)), + ray.intersect_plane_normal(Vec3::Z, Dir3::new(Vec3::ONE).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::ONE)) + .intersect_plane_normal(Vec3::NEG_Z, Dir3::new(Vec3::ONE).unwrap()) .is_none()); // Parallel assert!(ray - .intersect_plane(Vec3::X, InfinitePlane3d::new(Vec3::X)) + .intersect_plane_normal(Vec3::X, Dir3::new(Vec3::X).unwrap()) .is_none()); // Parallel with simulated rounding error assert!(ray - .intersect_plane( + .intersect_plane_normal( Vec3::X, - InfinitePlane3d::new(Vec3::X + Vec3::Z * f32::EPSILON) + Dir3::new(Vec3::X + Vec3::Z * f32::EPSILON).unwrap() ) .is_none()); } diff --git a/crates/bevy_math/src/ray/raycast2d.rs b/crates/bevy_math/src/ray/raycast2d.rs new file mode 100644 index 0000000000000..b04be186c0df8 --- /dev/null +++ b/crates/bevy_math/src/ray/raycast2d.rs @@ -0,0 +1,240 @@ +use crate::{ + ops::{self, FloatPow}, + Dir2, Ray2d, Vec2, +}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// A raycast intersection test for 2D bounding volumes +#[derive(Clone, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))] +pub struct RayCast2d { + /// The ray for the test + pub ray: Ray2d, + /// The maximum distance for the ray + pub max: f32, + /// The multiplicative inverse direction of the ray + direction_recip: Vec2, +} + +impl RayCast2d { + /// Construct a [`RayCast2d`] from an origin, [`Dir2`], and max distance. + pub fn new(origin: Vec2, direction: Dir2, max: f32) -> Self { + Self::from_ray(Ray2d { origin, direction }, max) + } + + /// Construct a [`RayCast2d`] from a [`Ray2d`] and max distance. + pub fn from_ray(ray: Ray2d, max: f32) -> Self { + Self { + ray, + direction_recip: ray.direction.recip(), + max, + } + } + + /// Get the cached multiplicative inverse of the direction of the ray. + pub const fn direction_recip(&self) -> Vec2 { + self.direction_recip + } + + /// Get the distance of an intersection with an box defined by min/max, if any. + pub fn aabb_intersection_at_min_max(&self, min: Vec2, max: Vec2) -> Option { + let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() { + (min.x, max.x) + } else { + (max.x, min.x) + }; + let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() { + (min.y, max.y) + } else { + (max.y, min.y) + }; + + // Calculate the minimum/maximum time for each axis based on how much the direction goes that + // way. These values can get arbitrarily large, or even become NaN, which is handled by the + // min/max operations below + let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x; + let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y; + let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x; + let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y; + + // An axis that is not relevant to the ray direction will be NaN. When one of the arguments + // to min/max is NaN, the other argument is used. + // An axis for which the direction is the wrong way will return an arbitrarily large + // negative value. + let tmin = tmin_x.max(tmin_y).max(0.); + let tmax = tmax_y.min(tmax_x).min(self.max); + + if tmin <= tmax { + Some(tmin) + } else { + None + } + } + + /// Get the distance of an intersection with a [`BoundingCircle`], if any. + pub fn circle_intersection_at_center_radius(&self, center: Vec2, radius: f32) -> Option { + let offset = self.ray.origin - center; + let projected = offset.dot(*self.ray.direction); + let cross = offset.perp_dot(*self.ray.direction); + let distance_squared = radius.squared() - cross.squared(); + if distance_squared < 0. + || ops::copysign(projected.squared(), -projected) < -distance_squared + { + None + } else { + let toi = -projected - ops::sqrt(distance_squared); + if toi > self.max { + None + } else { + Some(toi.max(0.)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EPSILON: f32 = 0.001; + + #[test] + fn test_ray_intersection_circle_hits() { + for (test, (center, radius), expected_distance) in &[ + ( + // Hit the center of a centered bounding circle + RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.), + (Vec2::ZERO, 1.), + 4., + ), + ( + // Hit the center of a centered bounding circle, but from the other side + RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.), + (Vec2::ZERO, 1.), + 4., + ), + ( + // Hit the center of an offset circle + RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.), + (Vec2::Y * 3., 2.), + 1., + ), + ( + // Just barely hit the circle before the max distance + RayCast2d::new(Vec2::X, Dir2::Y, 1.), + (Vec2::ONE, 0.01), + 0.99, + ), + ( + // Hit a circle off-center + RayCast2d::new(Vec2::X, Dir2::Y, 90.), + (Vec2::Y * 5., 2.), + 3.268, + ), + ( + // Barely hit a circle on the side + RayCast2d::new(Vec2::X * 0.99999, Dir2::Y, 90.), + (Vec2::Y * 5., 1.), + 4.996, + ), + ] { + let actual_distance = test + .circle_intersection_at_center_radius(*center, *radius) + .unwrap(); + assert!( + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: center: {center:?}, radius: {radius}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", + ); + } + } + + #[test] + fn test_ray_intersection_circle_inside() { + let (center, radius) = (Vec2::splat(0.5), 1.); + for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] { + for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] { + for max in &[0., 1., 900.] { + let test = RayCast2d::new(*origin, *direction, *max); + + let actual_distance = test.circle_intersection_at_center_radius(center, radius); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); + } + } + } + } + + #[test] + fn test_ray_intersection_aabb_hits() { + for (test, (min, max), expected_distance) in &[ + ( + // Hit the center of a centered aabb + RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.), + (Vec2::ZERO, Vec2::ONE), + 4., + ), + ( + // Hit the center of a centered aabb, but from the other side + RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.), + (Vec2::ZERO, Vec2::ONE), + 4., + ), + ( + // Hit the center of an offset aabb + RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.), + (Vec2::Y * 3., Vec2::splat(2.)), + 1., + ), + ( + // Just barely hit the aabb before the max distance + RayCast2d::new(Vec2::X, Dir2::Y, 1.), + (Vec2::ONE, Vec2::splat(0.01)), + 0.99, + ), + ( + // Hit an aabb off-center + RayCast2d::new(Vec2::X, Dir2::Y, 90.), + (Vec2::Y * 5., Vec2::splat(2.)), + 3., + ), + ( + // Barely hit an aabb on corner + RayCast2d::new(Vec2::X * -0.001, Dir2::from_xy(1., 1.).unwrap(), 90.), + (Vec2::Y * 2., Vec2::ONE), + 1.414, + ), + ] + .map(|(a, (center, half_size), b)| (a, (center - half_size, center + half_size), b)) + { + let actual_distance = test.aabb_intersection_at_min_max(*min, *max).unwrap(); + assert!( + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: min: {min:?}, max: {max:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", + ); + } + } + + #[test] + fn test_ray_intersection_aabb_inside() { + let (min, max) = (Vec2::splat(0.5) - Vec2::ONE, Vec2::splat(0.5) + Vec2::ONE); + for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] { + for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] { + for max_dist in &[0., 1., 900.] { + let test = RayCast2d::new(*origin, *direction, *max_dist); + + let actual_distance = test.aabb_intersection_at_min_max(min, max); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); + } + } + } + } +} diff --git a/crates/bevy_math/src/ray/raycast3d.rs b/crates/bevy_math/src/ray/raycast3d.rs new file mode 100644 index 0000000000000..f8e0f689ce1ff --- /dev/null +++ b/crates/bevy_math/src/ray/raycast3d.rs @@ -0,0 +1,243 @@ +use crate::{ + ops::{self, FloatPow}, + Dir3A, Ray3d, Vec3A, +}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// A raycast intersection test for 3D bounding volumes +#[derive(Clone, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))] +pub struct RayCast3d { + /// The origin of the ray. + pub origin: Vec3A, + /// The direction of the ray. + pub direction: Dir3A, + /// The maximum distance for the ray + pub max: f32, + /// The multiplicative inverse direction of the ray + direction_recip: Vec3A, +} + +impl RayCast3d { + /// Construct a [`RayCast3d`] from an origin, [direction], and max distance. + /// + /// [direction]: crate::direction::Dir3 + pub fn new(origin: impl Into, direction: impl Into, max: f32) -> Self { + let direction = direction.into(); + Self { + origin: origin.into(), + direction, + direction_recip: direction.recip(), + max, + } + } + + /// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance. + pub fn from_ray(ray: Ray3d, max: f32) -> Self { + Self::new(ray.origin, ray.direction, max) + } + + /// Get the cached multiplicative inverse of the direction of the ray. + pub const fn direction_recip(&self) -> Vec3A { + self.direction_recip + } + + /// Get the distance of an intersection with an box defined by min/max points, if any. + #[inline] + pub fn aabb_intersection_at_min_max(&self, min: Vec3A, max: Vec3A) -> Option { + let positive = self.direction.signum().cmpgt(Vec3A::ZERO); + let min_selected = Vec3A::select(positive, min, max); + let max_selected = Vec3A::select(positive, max, min); + + // Calculate the minimum/maximum time for each axis based on how much the direction goes that + // way. These values can get arbitrarily large, or even become NaN, which is handled by the + // min/max operations below + let tmin = (min_selected - self.origin) * self.direction_recip; + let tmax = (max_selected - self.origin) * self.direction_recip; + + // An axis that is not relevant to the ray direction will be NaN. When one of the arguments + // to min/max is NaN, the other argument is used. + // An axis for which the direction is the wrong way will return an arbitrarily large + // negative value. + let tmin = tmin.max_element().max(0.); + let tmax = tmax.min_element().min(self.max); + + if tmin <= tmax { + Some(tmin) + } else { + None + } + } + + /// Get the distance of an intersection with a sphere defined by center/radius, if any. + #[inline] + pub fn sphere_intersection_at_center_radius(&self, center: Vec3A, radius: f32) -> Option { + let offset = self.origin - center; + let projected = offset.dot(*self.direction); + let closest_point = offset - projected * *self.direction; + let distance_squared = radius.squared() - closest_point.length_squared(); + if distance_squared < 0. + || ops::copysign(projected.squared(), -projected) < -distance_squared + { + None + } else { + let toi = -projected - ops::sqrt(distance_squared); + if toi > self.max { + None + } else { + Some(toi.max(0.)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Dir3, Vec3}; + + const EPSILON: f32 = 0.001; + + #[test] + fn test_ray_intersection_sphere_hits() { + for (test, (center, radius), expected_distance) in &[ + ( + // Hit the center of a centered bounding sphere + RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.), + (Vec3::ZERO, 1.), + 4., + ), + ( + // Hit the center of a centered bounding sphere, but from the other side + RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.), + (Vec3::ZERO, 1.), + 4., + ), + ( + // Hit the center of an offset sphere + RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.), + (Vec3::Y * 3., 2.), + 1., + ), + ( + // Just barely hit the sphere before the max distance + RayCast3d::new(Vec3::X, Dir3::Y, 1.), + (Vec3::new(1., 1., 0.), 0.01), + 0.99, + ), + ( + // Hit a sphere off-center + RayCast3d::new(Vec3::X, Dir3::Y, 90.), + (Vec3::Y * 5., 2.), + 3.268, + ), + ( + // Barely hit a sphere on the side + RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.), + (Vec3::Y * 5., 1.), + 4.996, + ), + ] { + let actual_distance = test + .sphere_intersection_at_center_radius(center.to_vec3a(), *radius) + .unwrap(); + assert!( + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: center: {center:?}, radius: {radius}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", + ); + } + } + + #[test] + fn test_ray_intersection_sphere_inside() { + let (center, radius) = (Vec3::splat(0.5).to_vec3a(), 1.); + for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] { + for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] { + for max in &[0., 1., 900.] { + let test = RayCast3d::new(*origin, *direction, *max); + + let actual_distance = test.sphere_intersection_at_center_radius(center, radius); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); + } + } + } + } + + #[test] + fn test_ray_intersection_aabb_hits() { + for (test, (min, max), expected_distance) in &[ + ( + // Hit the center of a centered aabb + RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.), + (Vec3::ZERO, Vec3::ONE), + 4., + ), + ( + // Hit the center of a centered aabb, but from the other side + RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.), + (Vec3::ZERO, Vec3::ONE), + 4., + ), + ( + // Hit the center of an offset aabb + RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.), + (Vec3::Y * 3., Vec3::splat(2.)), + 1., + ), + ( + // Just barely hit the aabb before the max distance + RayCast3d::new(Vec3::X, Dir3::Y, 1.), + (Vec3::new(1., 1., 0.), Vec3::splat(0.01)), + 0.99, + ), + ( + // Hit an aabb off-center + RayCast3d::new(Vec3::X, Dir3::Y, 90.), + (Vec3::Y * 5., Vec3::splat(2.)), + 3., + ), + ( + // Barely hit an aabb on corner + RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.), + (Vec3::Y * 2., Vec3::ONE), + 1.732, + ), + ] + .map(|(a, (center, half_size), b)| (a, (center - half_size, center + half_size), b)) + { + let actual_distance = test + .aabb_intersection_at_min_max(min.to_vec3a(), max.to_vec3a()) + .unwrap(); + assert!( + ops::abs(actual_distance - expected_distance) < EPSILON, + "Case:\n Test: {test:?}\n Volume: min: {min:?}, max: {max:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}", + ); + } + } + + #[test] + fn test_ray_intersection_aabb_inside() { + let (min, max) = (Vec3::splat(0.5) - Vec3::ONE, Vec3::splat(0.5) + Vec3::ONE); + for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] { + for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] { + for max_dist in &[0., 1., 900.] { + let test = RayCast3d::new(*origin, *direction, *max_dist); + + let actual_distance = + test.aabb_intersection_at_min_max(min.to_vec3a(), max.to_vec3a()); + assert_eq!( + actual_distance, + Some(0.), + "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}", + ); + } + } + } + } +} diff --git a/crates/bevy_math/src/sampling/mod.rs b/crates/bevy_math/src/sampling/mod.rs index 78db92588b1f6..ecbb44515e05e 100644 --- a/crates/bevy_math/src/sampling/mod.rs +++ b/crates/bevy_math/src/sampling/mod.rs @@ -2,12 +2,5 @@ //! //! 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::*; diff --git a/crates/bevy_math/src/sampling/standard.rs b/crates/bevy_math/src/sampling/standard.rs index fdf7d4cee781b..cc2365d5fc211 100644 --- a/crates/bevy_math/src/sampling/standard.rs +++ b/crates/bevy_math/src/sampling/standard.rs @@ -21,12 +21,9 @@ //! let many_random_directions: Vec = rng.sample_iter(StandardUniform).take(5).collect(); //! ``` -use core::f32::consts::TAU; +use core::f32::consts::{PI, TAU}; -use crate::{ - primitives::{Circle, Sphere}, - Dir2, Dir3, Dir3A, Quat, Rot2, ShapeSample, Vec3A, -}; +use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3}; use rand::{ distr::{Distribution, StandardUniform}, RngExt, @@ -56,19 +53,31 @@ where impl Distribution for StandardUniform { #[inline] fn sample(&self, rng: &mut R) -> Dir2 { - let circle = Circle::new(1.0); - let vector = circle.sample_boundary(rng); + let theta = rng.random_range(0.0..TAU); + let (sin, cos) = ops::sin_cos(theta); + let vector = Vec2::new(cos, sin); Dir2::new_unchecked(vector) } } impl FromRng for Dir2 {} +/// Boundary sampling for unit-spheres +#[inline] +fn sample_unit_sphere_boundary(rng: &mut R) -> Vec3 { + let z = rng.random_range(-1f32..=1f32); + let (a_sin, a_cos) = ops::sin_cos(rng.random_range(-PI..=PI)); + let c = ops::sqrt(1f32 - z * z); + let x = a_sin * c; + let y = a_cos * c; + + Vec3::new(x, y, z) +} + impl Distribution for StandardUniform { #[inline] fn sample(&self, rng: &mut R) -> Dir3 { - let sphere = Sphere::new(1.0); - let vector = sphere.sample_boundary(rng); + let vector = sample_unit_sphere_boundary(rng); Dir3::new_unchecked(vector) } } @@ -78,8 +87,7 @@ impl FromRng for Dir3 {} impl Distribution for StandardUniform { #[inline] fn sample(&self, rng: &mut R) -> Dir3A { - let sphere = Sphere::new(1.0); - let vector: Vec3A = sphere.sample_boundary(rng).into(); + let vector = sample_unit_sphere_boundary(rng).to_vec3a(); Dir3A::new_unchecked(vector) } } diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index bc2b8fcae1a38..2727dd4820c2c 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -14,6 +14,8 @@ bevy_app = { path = "../bevy_app", version = "0.20.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.20.0-dev" } bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.20.0-dev" } diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 1f14a8e1a25b3..ed3447a3c2e54 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -15,13 +15,11 @@ use crate::morph::MorphAttributes; use crate::SerializedMeshAttributeData; use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; -use bevy_math::{ - bounding::{Aabb2d, Aabb3d}, - primitives::Triangle3d, - *, -}; +use bevy_geometry::bounding::{Aabb2d, Aabb3d}; +use bevy_math::*; use bevy_platform::collections::{hash_map, HashMap}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_shape::Triangle3d; use bytemuck::cast_slice; use core::hash::{Hash, Hasher}; use core::ptr; @@ -3048,9 +3046,9 @@ mod tests { use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::{MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; - use bevy_math::bounding::Aabb3d; - use bevy_math::primitives::Triangle3d; + use bevy_geometry::bounding::Aabb3d; use bevy_math::{Vec3, Vec3A}; + use bevy_shape::Triangle3d; use bevy_transform::components::Transform; #[test] diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 7ad0fc3826a5d..9a68a709dacf4 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -5,17 +5,14 @@ use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment, Verte use bevy_asset::RenderAssetUsages; use super::{Extrudable, MeshBuilder, Meshable}; -use bevy_math::prelude::Polyline2d; -use bevy_math::{ - ops, - primitives::{ - Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, - Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, Triangle3d, - WindingOrder, - }, - FloatExt, Vec2, Vec3, -}; +use bevy_geometry::ring::Ring; +use bevy_math::{ops, FloatExt, Vec2, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::{ + Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, + Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, Triangle3d, + WindingOrder, +}; use wgpu_types::PrimitiveTopology; /// A builder used for creating a [`Mesh`] with a [`Circle`] shape. @@ -1543,12 +1540,9 @@ where #[cfg(test)] mod tests { - use bevy_math::{ - prelude::Annulus, - primitives::{ConvexPolygon, RegularPolygon}, - FloatOrd, Vec2, - }; + use bevy_math::{FloatOrd, Vec2}; use bevy_platform::collections::HashSet; + use bevy_shape::{Annulus, ConvexPolygon, RegularPolygon}; use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; diff --git a/crates/bevy_mesh/src/primitives/dim3/capsule.rs b/crates/bevy_mesh/src/primitives/dim3/capsule.rs index f46ebce0d15a7..04e66f4ba894f 100644 --- a/crates/bevy_mesh/src/primitives/dim3/capsule.rs +++ b/crates/bevy_mesh/src/primitives/dim3/capsule.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Capsule3d, Vec2, Vec3}; +use bevy_math::{ops, Vec2, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::Capsule3d; /// Manner in which UV coordinates are distributed vertically. #[derive(Clone, Copy, Debug, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/cone.rs b/crates/bevy_mesh/src/primitives/dim3/cone.rs index d06a57f832ea5..0e11dbd964485 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cone.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cone.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Cone, Vec3}; +use bevy_math::{ops, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::Cone; /// Anchoring options for [`ConeMeshBuilder`] #[derive(Debug, Copy, Clone, Default, Reflect)] @@ -191,7 +192,8 @@ impl From for Mesh { #[cfg(test)] mod tests { use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; - use bevy_math::{primitives::Cone, Vec2}; + use bevy_math::Vec2; + use bevy_shape::Cone; /// Rounds floats to handle floating point error in tests. fn round_floats(points: &mut [[f32; N]]) { diff --git a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs index 8c69378c01e64..0557099a6a679 100644 --- a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs +++ b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::ConicalFrustum, Vec3}; +use bevy_math::{ops, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::ConicalFrustum; /// A builder used for creating a [`Mesh`] with a [`ConicalFrustum`] shape. #[derive(Clone, Copy, Debug, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs index 40a7cd45d4433..8d353ddd7e0fc 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Cuboid, Vec3}; +use bevy_math::Vec3; use bevy_reflect::prelude::*; +use bevy_shape::Cuboid; /// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape. #[derive(Clone, Copy, Debug, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs index 7b1b45974ea62..555bf086ac2b4 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Cylinder}; +use bevy_math::ops; use bevy_reflect::prelude::*; +use bevy_shape::Cylinder; /// Anchoring options for [`CylinderMeshBuilder`] #[derive(Debug, Copy, Clone, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/plane.rs b/crates/bevy_mesh/src/primitives/dim3/plane.rs index e937905584f9a..93fb4bcf66dd6 100644 --- a/crates/bevy_mesh/src/primitives/dim3/plane.rs +++ b/crates/bevy_mesh/src/primitives/dim3/plane.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Plane3d, Dir3, Quat, Vec2, Vec3}; +use bevy_math::{Dir3, Quat, Vec2, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::Plane3d; /// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs index 4d13112579f09..3ead50ae92488 100644 --- a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs @@ -1,7 +1,7 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::Polyline3d; use bevy_reflect::prelude::*; +use bevy_shape::Polyline3d; /// A builder used for creating a [`Mesh`] with a [`Polyline3d`] shape. #[derive(Clone, Debug, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs index d032285283afb..9be3187009149 100644 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs @@ -1,7 +1,7 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::Segment3d; use bevy_reflect::prelude::*; +use bevy_shape::Segment3d; /// A builder used for creating a [`Mesh`] with a [`Segment3d`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/sphere.rs b/crates/bevy_mesh/src/primitives/dim3/sphere.rs index 5938526457992..e858030ccae3f 100644 --- a/crates/bevy_mesh/src/primitives/dim3/sphere.rs +++ b/crates/bevy_mesh/src/primitives/dim3/sphere.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Sphere}; +use bevy_math::ops; use bevy_reflect::prelude::*; +use bevy_shape::Sphere; use core::f32::consts::PI; use hexasphere::shapes::IcoSphere; use thiserror::Error; diff --git a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs index 529805d9a603f..ad0201c455cd9 100644 --- a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs +++ b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs @@ -1,8 +1,8 @@ use super::triangle3d; use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::{Tetrahedron, Triangle3d}; use bevy_reflect::prelude::*; +use bevy_shape::{Tetrahedron, Triangle3d}; /// A builder used for creating a [`Mesh`] with a [`Tetrahedron`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] diff --git a/crates/bevy_mesh/src/primitives/dim3/torus.rs b/crates/bevy_mesh/src/primitives/dim3/torus.rs index 6f370c13418ca..64572c9700efc 100644 --- a/crates/bevy_mesh/src/primitives/dim3/torus.rs +++ b/crates/bevy_mesh/src/primitives/dim3/torus.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Torus, Vec3}; +use bevy_math::{ops, Vec3}; use bevy_reflect::prelude::*; +use bevy_shape::Torus; use core::ops::RangeInclusive; /// A builder used for creating a [`Mesh`] with a [`Torus`] shape. diff --git a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs index e35f272ab9bb1..125c7a4c529fe 100644 --- a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs @@ -1,7 +1,8 @@ use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Triangle3d, Vec3}; +use bevy_math::Vec3; use bevy_reflect::prelude::*; +use bevy_shape::Triangle3d; /// A builder used for creating a [`Mesh`] with a [`Triangle3d`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] @@ -103,7 +104,7 @@ impl From for Mesh { #[cfg(test)] mod tests { use super::uv_coords; - use bevy_math::primitives::Triangle3d; + use bevy_shape::Triangle3d; #[test] fn uv_test() { diff --git a/crates/bevy_mesh/src/primitives/extrusion.rs b/crates/bevy_mesh/src/primitives/extrusion.rs index 9df4821eb392b..11cfcf1de805a 100644 --- a/crates/bevy_mesh/src/primitives/extrusion.rs +++ b/crates/bevy_mesh/src/primitives/extrusion.rs @@ -1,7 +1,5 @@ -use bevy_math::{ - primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}, - Vec2, Vec3, -}; +use bevy_math::{Vec2, Vec3}; +use bevy_shape::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}; use super::{MeshBuilder, Meshable}; use crate::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; diff --git a/crates/bevy_mesh/src/skinning.rs b/crates/bevy_mesh/src/skinning.rs index 45325021d1440..3f81e2bcbfec3 100644 --- a/crates/bevy_mesh/src/skinning.rs +++ b/crates/bevy_mesh/src/skinning.rs @@ -4,10 +4,8 @@ use bevy_ecs::{ component::Component, entity::Entity, prelude::ReflectComponent, system::Query, template::FromTemplate, }; -use bevy_math::{ - bounding::{Aabb3d, BoundingVolume}, - Affine3A, Mat4, Vec3, Vec3A, -}; +use bevy_geometry::bounding::{Aabb3d, BoundingVolume}; +use bevy_math::{Affine3A, Mat4, Vec3, Vec3A}; use bevy_reflect::prelude::*; use bevy_transform::components::GlobalTransform; use core::ops::Deref; @@ -453,7 +451,8 @@ mod tests { use super::*; use approx::assert_abs_diff_eq; use bevy_asset::RenderAssetUsages; - use bevy_math::{bounding::BoundingVolume, vec3, vec3a}; + use bevy_geometry::bounding::BoundingVolume; + use bevy_math::{vec3, vec3a}; #[test] fn aabb_accumulator() { diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index c638247e423c6..9398e20b85aab 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -1,10 +1,8 @@ use alloc::sync::Arc; use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; -use bevy_math::{ - bounding::{Aabb2d, Aabb3d, BoundingVolume}, - vec2, Vec2, Vec3, Vec3A, Vec3Swizzles, -}; +use bevy_geometry::bounding::{Aabb2d, Aabb3d, BoundingVolume}; +use bevy_math::{vec2, Vec2, Vec3, Vec3A, Vec3Swizzles}; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; use bevy_platform::collections::HashSet; diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index e7479f3e372d5..f781dd1d1281d 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -26,7 +26,7 @@ area_light_luts = ["bevy_image/ktx2", "bevy_image/zstd"] shader_format_glsl = ["bevy_shader/shader_format_glsl"] trace = ["bevy_render/trace"] # Enables the meshlet renderer for dense high-poly scenes (experimental) -meshlet = ["dep:lz4_flex", "dep:range-alloc"] +meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_geometry"] # Enables processing meshes into meshlet meshes meshlet_processor = [ "meshlet", @@ -56,6 +56,8 @@ bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev", features = [ bevy_shader = { path = "../bevy_shader", version = "0.20.0-dev" } bevy_material = { path = "../bevy_material", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } bevy_render = { path = "../bevy_render", version = "0.20.0-dev", features = [ "morph", diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index f00c667d6d2ca..1d82f8a067514 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -7,7 +7,7 @@ use bevy_asset::{Asset, Assets, Handle}; use bevy_ecs::{ component::Component, lifecycle::HookContext, resource::Resource, world::DeferredWorld, }; -use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; +use bevy_math::{Quat, Vec2, Vec3}; use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}; use bevy_reflect::{Reflect, TypePath}; use bevy_render::{ @@ -20,6 +20,7 @@ use bevy_render::{ RenderDebugFlags, }; use bevy_shader::load_shader_library; +use bevy_shape::Rectangle; /// Plugin to render [`ForwardDecal`]s. pub struct ForwardDecalPlugin; diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index d174c710be5aa..375fade753f03 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -2,11 +2,8 @@ use crate::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh}; use alloc::borrow::Cow; -use bevy_math::{ - bounding::{Aabb3d, BoundingSphere, BoundingVolume}, - ops::log2, - IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, -}; +use bevy_geometry::bounding::{Aabb3d, BoundingSphere, BoundingVolume}; +use bevy_math::{ops::log2, IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles}; use bevy_mesh::{Indices, Mesh, MeshVertexAttribute}; use bevy_platform::collections::HashMap; use bevy_render::render_resource::PrimitiveTopology; diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1f0884d054b6a..2d278d5f10bcf 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -31,11 +31,7 @@ use bevy_material::{ key::{ErasedMaterialPipelineKey, ErasedMeshPipelineKey}, MaterialProperties, }; -use bevy_math::{ - ops, - primitives::{HalfSpace, ViewFrustum}, - proj, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, -}; +use bevy_math::{ops, proj, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_mesh::{Mesh3d, MeshVertexBufferLayoutRef}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; @@ -66,6 +62,7 @@ use bevy_render::{ view::ExtractedView, Extract, }; +use bevy_shape::{HalfSpace, ViewFrustum}; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::default; use core::{any::TypeId, hash::Hash, mem, ops::Range}; diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index fc1f084c278ab..be4e61f4427de 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -37,16 +37,14 @@ use bevy_core_pipeline::{ }; use bevy_ecs::{resource::Resource, schedule::IntoScheduleConfigs as _}; use bevy_light::FogVolume; -use bevy_math::{ - primitives::{Cuboid, Plane3d}, - Vec2, Vec3, -}; +use bevy_math::{Vec2, Vec3}; use bevy_mesh::{Mesh, Meshable}; use bevy_render::{ render_resource::SpecializedRenderPipelines, sync_component::{SyncComponent, SyncComponentPlugin}, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems, }; +use bevy_shape::{Cuboid, Plane3d}; use render::{volumetric_fog, VolumetricFogPipeline, VolumetricFogUniformBuffer}; use crate::{volumetric_fog::render::init_volumetric_fog_pipeline, MeshPipelineSystems}; diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index 163cf46e041b2..b1ae2607c2d0b 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -19,6 +19,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.20.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } bevy_input = { path = "../bevy_input", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev", optional = true } bevy_camera = { path = "../bevy_camera", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 3643b719f964c..2700f01042144 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -1,4 +1,5 @@ -use bevy_math::{bounding::Aabb3d, Affine3A, Dir3, Ray3d, Vec2, Vec3, Vec3A}; +use bevy_geometry::bounding::Aabb3d; +use bevy_math::{Affine3A, Dir3, Ray3d, Vec2, Vec3, Vec3A}; use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; use bevy_reflect::Reflect; diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index 057537ea57a39..c95fabdf05d46 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -10,7 +10,8 @@ use bevy_camera::{ primitives::Aabb, visibility::{InheritedVisibility, ViewVisibility}, }; -use bevy_math::{bounding::Aabb3d, Ray3d}; +use bevy_geometry::bounding::Aabb3d; +use bevy_math::Ray3d; use bevy_mesh::{Mesh, Mesh2d, Mesh3d}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 1da50ef005ab4..d6889918a9278 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -68,6 +68,7 @@ bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.20.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.20.0-dev" } bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_geometry = { path = "../bevy_geometry", version = "0.20.0-dev" } bevy_material = { path = "../bevy_material", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } bevy_log = { path = "../bevy_log", version = "0.20.0-dev" } diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index 1cca0891594ac..107702f5a1a17 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -10,8 +10,8 @@ use bevy_ecs::{ system::{Res, ResMut}, world::{FromWorld, World}, }; +use bevy_geometry::bounding::{Aabb2d, BoundingVolume}; use bevy_log::warn; -use bevy_math::bounding::{Aabb2d, BoundingVolume}; use bevy_mesh::Indices; use glam::Vec4; use wgpu::{BufferUsages, DownlevelFlags, COPY_BUFFER_ALIGNMENT}; diff --git a/crates/bevy_shape/Cargo.toml b/crates/bevy_shape/Cargo.toml new file mode 100644 index 0000000000000..0f5a46cc97df4 --- /dev/null +++ b/crates/bevy_shape/Cargo.toml @@ -0,0 +1,60 @@ +[package] +description = "Provides primitive shape data definitions for Bevy Engine" +edition = "2024" +homepage = "https://bevy.org" +keywords = ["bevy"] +license = "MIT OR Apache-2.0" +name = "bevy_shape" +repository = "https://github.com/bevyengine/bevy" +rust-version = "1.94.0" +version = "0.20.0-dev" + +[dependencies] +approx = { default-features = false, optional = true, version = "0.5" } +bevy_math = { default-features = false, path = "../bevy_math" } +bevy_reflect = { default-features = false, features = [ + "glam", +], optional = true, path = "../bevy_reflect", version = "0.20.0-dev" } +derive_more = { default-features = false, features = [ + "from", + "into", +], version = "2" } +serde = { default-features = false, features = [ + "derive", +], optional = true, version = "1" } +thiserror = { default-features = false, version = "2" } + +[dev-dependencies] +approx = "0.5" +# Enable the approx feature when testing. +bevy_math = { default-features = false, features = [ + "approx", +], path = "../bevy_math" } + +[features] +alloc = ["bevy_math/alloc", "serde?/alloc"] +default = ["std"] +std = [ + "alloc", + "bevy_math/std", + "bevy_reflect?/std", + "derive_more/std", + "serde?/std", +] + +serialize = ["bevy_math/serialize", "dep:serde"] +# Enable approx for glam types to approximate floating point equality comparisons and assertions +approx = ["bevy_math/approx", "dep:approx"] +# Enable bevy_reflect (requires alloc) +bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--generate-link-to-definition", + "--generate-macro-expansion", + "-Zunstable-options", +] diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_shape/src/dim2.rs similarity index 86% rename from crates/bevy_math/src/primitives/dim2.rs rename to crates/bevy_shape/src/dim2.rs index b156b459221bc..1c445a133717d 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_shape/src/dim2.rs @@ -3,10 +3,9 @@ use derive_more::derive::From; #[cfg(feature = "alloc")] use thiserror::Error; -use super::{Measured2d, Primitive2d, WindingOrder}; -use crate::{ +use crate::{Primitive2d, WindingOrder}; +use bevy_math::{ ops::{self, FloatPow}, - primitives::Inset, Dir2, InvalidDirectionError, Isometry2d, Ray2d, Rot2, Vec2, }; @@ -80,21 +79,6 @@ impl Circle { } } -impl Measured2d for Circle { - /// Get the area of the circle - #[inline] - fn area(&self) -> f32 { - PI * self.radius.squared() - } - - /// Get the perimeter or circumference of the circle - #[inline] - #[doc(alias = "circumference")] - fn perimeter(&self) -> f32 { - 2.0 * PI * self.radius - } -} - /// A primitive representing an arc between two points on a circle. /// /// An arc has no area. @@ -304,22 +288,6 @@ impl Default for CircularSector { } } -impl Measured2d for CircularSector { - #[inline] - fn area(&self) -> f32 { - self.arc.radius.squared() * self.arc.half_angle - } - - #[inline] - fn perimeter(&self) -> f32 { - if self.half_angle() >= PI { - self.arc.radius * 2.0 * PI - } else { - 2.0 * self.radius() + self.arc_length() - } - } -} - impl CircularSector { /// Create a new [`CircularSector`] from a `radius` and an `angle` #[inline] @@ -456,18 +424,6 @@ impl Default for CircularSegment { } } -impl Measured2d for CircularSegment { - #[inline] - fn area(&self) -> f32 { - 0.5 * self.arc.radius.squared() * (self.arc.angle() - ops::sin(self.arc.angle())) - } - - #[inline] - fn perimeter(&self) -> f32 { - self.chord_length() + self.arc_length() - } -} - impl CircularSegment { /// Create a new [`CircularSegment`] from a `radius`, and a `half_angle` in radians. #[inline] @@ -571,7 +527,6 @@ impl CircularSegment { #[cfg(test)] mod arc_tests { use core::f32::consts::FRAC_PI_4; - use core::f32::consts::SQRT_2; use approx::assert_abs_diff_eq; @@ -593,10 +548,6 @@ mod arc_tests { sagitta: f32, is_minor: bool, is_major: bool, - sector_area: f32, - sector_perimeter: f32, - segment_area: f32, - segment_perimeter: f32, } impl ArcTestCase { @@ -628,8 +579,6 @@ mod arc_tests { assert_abs_diff_eq!(self.chord_midpoint, sector.chord_midpoint()); assert_abs_diff_eq!(self.apothem, sector.apothem()); assert_abs_diff_eq!(self.sagitta, sector.sagitta()); - assert_abs_diff_eq!(self.sector_area, sector.area()); - assert_abs_diff_eq!(self.sector_perimeter, sector.perimeter()); } fn check_segment(&self, segment: CircularSegment) { @@ -641,8 +590,6 @@ mod arc_tests { assert_abs_diff_eq!(self.chord_midpoint, segment.chord_midpoint()); assert_abs_diff_eq!(self.apothem, segment.apothem()); assert_abs_diff_eq!(self.sagitta, segment.sagitta()); - assert_abs_diff_eq!(self.segment_area, segment.area()); - assert_abs_diff_eq!(self.segment_perimeter, segment.perimeter()); } } @@ -664,10 +611,6 @@ mod arc_tests { sagitta: 0.0, is_minor: true, is_major: false, - sector_area: 0.0, - sector_perimeter: 2.0, - segment_area: 0.0, - segment_perimeter: 0.0, }; tests.check_arc(Arc2d::new(1.0, 0.0)); @@ -693,10 +636,6 @@ mod arc_tests { sagitta: 0.0, is_minor: true, is_major: false, - sector_area: 0.0, - sector_perimeter: 0.0, - segment_area: 0.0, - segment_perimeter: 0.0, }; tests.check_arc(Arc2d::new(0.0, FRAC_PI_4)); @@ -723,10 +662,6 @@ mod arc_tests { sagitta: 1.0 - sqrt_half, is_minor: true, is_major: false, - sector_area: FRAC_PI_4, - sector_perimeter: FRAC_PI_2 + 2.0, - segment_area: FRAC_PI_4 - 0.5, - segment_perimeter: FRAC_PI_2 + SQRT_2, }; tests.check_arc(Arc2d::from_turns(1.0, 0.25)); @@ -752,10 +687,6 @@ mod arc_tests { sagitta: 1.0, is_minor: true, is_major: true, - sector_area: FRAC_PI_2, - sector_perimeter: PI + 2.0, - segment_area: FRAC_PI_2, - segment_perimeter: PI + 2.0, }; tests.check_arc(Arc2d::from_radians(1.0, PI)); @@ -781,10 +712,6 @@ mod arc_tests { sagitta: 2.0, is_minor: false, is_major: true, - sector_area: PI, - sector_perimeter: 2.0 * PI, - segment_area: PI, - segment_perimeter: 2.0 * PI, }; tests.check_arc(Arc2d::from_degrees(1.0, 360.0)); @@ -794,9 +721,6 @@ mod arc_tests { } /// An ellipse primitive, which is like a circle, but the width and height can be different -/// -/// Ellipse does not implement [`Inset`] as concentric ellipses do not have parallel curves: -/// if the ellipse is not a circle, the inset shape is not actually an ellipse (although it may look like one) but can also be a lens-like shape. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -883,70 +807,6 @@ impl Ellipse { } } -impl Measured2d for Ellipse { - /// Get the area of the ellipse - #[inline] - fn area(&self) -> f32 { - PI * self.half_size.x * self.half_size.y - } - - #[inline] - /// Get an approximation for the perimeter or circumference of the ellipse. - /// - /// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases. - fn perimeter(&self) -> f32 { - let a = self.semi_major(); - let b = self.semi_minor(); - - // In the case that `a == b`, the ellipse is a circle - if a / b - 1. < 1e-5 { - return PI * (a + b); - }; - - // In the case that `a` is much larger than `b`, the ellipse is a line - if a / b > 1e4 { - return 4. * a; - }; - - // These values are the result of (0.5 choose n)^2 where n is the index in the array - // They could be calculated on the fly but hardcoding them yields more accurate and faster results - // because the actual calculation for these values involves factorials and numbers > 10^23 - const BINOMIAL_COEFFICIENTS: [f32; 21] = [ - 1., - 0.25, - 0.015625, - 0.00390625, - 0.0015258789, - 0.00074768066, - 0.00042057037, - 0.00025963783, - 0.00017140154, - 0.000119028846, - 0.00008599834, - 0.00006414339, - 0.000049109784, - 0.000038430585, - 0.000030636627, - 0.000024815668, - 0.000020380836, - 0.000016942893, - 0.000014236736, - 0.000012077564, - 0.000010333865, - ]; - - // The algorithm used here is the Gauss-Kummer infinite series expansion of the elliptic integral expression for the perimeter of ellipses - // For more information see https://www.wolframalpha.com/input/?i=gauss-kummer+series - // We only use the terms up to `i == 20` for this approximation - let h = ((a - b) / (a + b)).squared(); - - PI * (a + b) - * (0..=20) - .map(|i| BINOMIAL_COEFFICIENTS[i] * ops::powf(h, i as f32)) - .sum::() - } -} - /// A primitive shape formed by the region between two circles, also known as a ring. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -1029,22 +889,6 @@ impl Annulus { } } -impl Measured2d for Annulus { - /// Get the area of the annulus - #[inline] - fn area(&self) -> f32 { - PI * (self.outer_circle.radius.squared() - self.inner_circle.radius.squared()) - } - - /// Get the perimeter or circumference of the annulus, - /// which is the sum of the perimeters of the inner and outer circles. - #[inline] - #[doc(alias = "circumference")] - fn perimeter(&self) -> f32 { - 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) - } -} - /// A rhombus primitive, also known as a diamond shape. /// A four sided polygon, centered on the origin, where opposite sides are parallel but without /// requiring right angles. @@ -1169,20 +1013,6 @@ impl Rhombus { } } -impl Measured2d for Rhombus { - /// Get the area of the rhombus - #[inline] - fn area(&self) -> f32 { - 2.0 * self.half_diagonals.x * self.half_diagonals.y - } - - /// Get the perimeter of the rhombus - #[inline] - fn perimeter(&self) -> f32 { - 4.0 * self.side() - } -} - /// An unbounded plane in 2D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -1772,27 +1602,6 @@ impl Triangle2d { } } -impl Measured2d for Triangle2d { - /// Get the area of the triangle - #[inline] - fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - 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 - #[inline] - fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - - let ab = a.distance(b); - let bc = b.distance(c); - let ca = c.distance(a); - - ab + bc + ca - } -} - /// A rectangle primitive, which is like a square, except that the width and height can be different #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -1871,20 +1680,6 @@ impl Rectangle { } } -impl Measured2d for Rectangle { - /// Get the area of the rectangle - #[inline] - fn area(&self) -> f32 { - 4.0 * self.half_size.x * self.half_size.y - } - - /// Get the perimeter of the rectangle - #[inline] - fn perimeter(&self) -> f32 { - 4.0 * (self.half_size.x + self.half_size.y) - } -} - /// A polygon with N vertices. #[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] @@ -2157,22 +1952,6 @@ impl RegularPolygon { } } -impl Measured2d for RegularPolygon { - /// Get the area of the regular polygon - #[inline] - fn area(&self) -> f32 { - let angle: f32 = 2.0 * PI / (self.sides as f32); - (self.sides as f32) * self.circumradius().squared() * ops::sin(angle) / 2.0 - } - - /// Get the perimeter of the regular polygon. - /// This is the sum of its sides - #[inline] - fn perimeter(&self) -> f32 { - self.sides as f32 * self.side_length() - } -} - /// A 2D capsule primitive, also known as a stadium or pill shape. /// /// A two-dimensional capsule is defined as a neighborhood of points at a distance (radius) from a line @@ -2224,99 +2003,6 @@ impl Capsule2d { } } -impl Measured2d for Capsule2d { - /// Get the area of the capsule - #[inline] - fn area(&self) -> f32 { - // pi*r^2 + (2r)*l - PI * self.radius.squared() + self.to_inner_rectangle().area() - } - - /// Get the perimeter of the capsule - #[inline] - fn perimeter(&self) -> f32 { - // 2pi*r + 2l - 2.0 * PI * self.radius + 4.0 * self.half_length - } -} - -/// A 2D shape representing the ring version of a base shape. -/// -/// The `inner_shape` forms the "hollow" of the `outer_shape`. -/// -/// The resulting shapes are rings or hollow shapes. -/// For example, a circle becomes an annulus. -/// -/// # Warning -/// -/// The `outer_shape` must contain the `inner_shape` for the generated meshes to be accurate. -/// -/// If there are vertices in the `inner_shape` that escape the `outer_shape` -/// (for example, if the `inner_shape` is in fact larger), -/// it may result in incorrect geometries. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct Ring { - /// The outer shape - pub outer_shape: P, - /// The inner shape (the same shape of a different size) - pub inner_shape: P, -} - -impl Ring

{ - /// Create a new `Ring` from a given `outer_shape` and `inner_shape`. - /// - /// If the primitive implements [`Inset`] and you would like a uniform thickness, consider using [`ToRing::to_ring`] - pub const fn new(outer_shape: P, inner_shape: P) -> Self { - Self { - outer_shape, - inner_shape, - } - } -} - -impl Primitive2d for Ring {} - -impl Ring

{ - /// Generate a `Ring` from a given `primitive` and a `thickness`. - pub fn from_primitive_and_thickness(primitive: P, thickness: f32) -> Self { - let hollow = primitive.clone().inset(thickness); - Ring::new(primitive, hollow) - } -} - -impl Measured2d for Ring

{ - #[inline] - fn area(&self) -> f32 { - self.outer_shape.area() - self.inner_shape.area() - } - - #[inline] - fn perimeter(&self) -> f32 { - self.outer_shape.perimeter() + self.inner_shape.perimeter() - } -} - -/// Provides a convenience method for converting a primitive to a [`Ring`], with a given thickness. -/// -/// The primitive must implement [`Inset`]. -pub trait ToRing: Primitive2d + Inset -where - Self: Sized, -{ - /// Construct a `Ring` - fn to_ring(self, thickness: f32) -> Ring; -} - -impl

ToRing for P -where - P: Primitive2d + Clone + Inset, -{ - fn to_ring(self, thickness: f32) -> Ring { - Ring::from_primitive_and_thickness(self, thickness) - } -} - #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools @@ -2432,8 +2118,6 @@ mod tests { fn circle_math() { let circle = Circle { radius: 3.0 }; assert_eq!(circle.diameter(), 6.0, "incorrect diameter"); - assert_eq!(circle.area(), 28.274334, "incorrect area"); - assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); } #[test] @@ -2444,8 +2128,6 @@ mod tests { Rectangle::new(4.0, 9.0), "rectangle wasn't created correctly from a capsule" ); - assert_eq!(capsule.area(), 48.566371, "incorrect area"); - assert_eq!(capsule.perimeter(), 30.566371, "incorrect perimeter"); } #[test] @@ -2453,21 +2135,15 @@ mod tests { let annulus = Annulus::new(2.5, 3.5); assert_eq!(annulus.diameter(), 7.0, "incorrect diameter"); assert_eq!(annulus.thickness(), 1.0, "incorrect thickness"); - assert_eq!(annulus.area(), 18.849556, "incorrect area"); - assert_eq!(annulus.perimeter(), 37.699112, "incorrect perimeter"); } #[test] fn rhombus_math() { let rhombus = Rhombus::new(3.0, 4.0); - assert_eq!(rhombus.area(), 6.0, "incorrect area"); - assert_eq!(rhombus.perimeter(), 10.0, "incorrect perimeter"); assert_eq!(rhombus.side(), 2.5, "incorrect side"); assert_eq!(rhombus.inradius(), 1.2, "incorrect inradius"); assert_eq!(rhombus.circumradius(), 2.0, "incorrect circumradius"); let rhombus = Rhombus::new(0.0, 0.0); - assert_eq!(rhombus.area(), 0.0, "incorrect area"); - assert_eq!(rhombus.perimeter(), 0.0, "incorrect perimeter"); assert_eq!(rhombus.side(), 0.0, "incorrect side"); assert_eq!(rhombus.inradius(), 0.0, "incorrect inradius"); assert_eq!(rhombus.circumradius(), 0.0, "incorrect circumradius"); @@ -2482,7 +2158,6 @@ mod tests { #[test] fn ellipse_math() { let ellipse = Ellipse::new(3.0, 1.0); - assert_eq!(ellipse.area(), 9.424778, "incorrect area"); assert_eq!(ellipse.eccentricity(), 0.94280905, "incorrect eccentricity"); @@ -2493,31 +2168,8 @@ mod tests { assert_eq!(circle.eccentricity(), 0., "incorrect circle eccentricity"); } - #[test] - fn ellipse_perimeter() { - let circle = Ellipse::new(1., 1.); - assert_relative_eq!(circle.perimeter(), 6.2831855); - - let line = Ellipse::new(75_000., 0.5); - assert_relative_eq!(line.perimeter(), 300_000.); - - let ellipse = Ellipse::new(0.5, 2.); - assert_relative_eq!(ellipse.perimeter(), 8.578423); - - let ellipse = Ellipse::new(5., 3.); - assert_relative_eq!(ellipse.perimeter(), 25.526999); - } - #[test] fn triangle_math() { - let triangle = Triangle2d::new( - Vec2::new(-2.0, -1.0), - Vec2::new(1.0, 4.0), - Vec2::new(7.0, 0.0), - ); - assert_eq!(triangle.area(), 21.0, "incorrect area"); - assert_eq!(triangle.perimeter(), 22.097439, "incorrect perimeter"); - let degenerate_triangle = Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(0., 0.), Vec2::new(1., 0.)); assert!(degenerate_triangle.is_degenerate()); @@ -2569,8 +2221,6 @@ mod tests { rectangle, Rectangle::from_corners(Vec2::new(-1.5, -3.5), Vec2::new(1.5, 3.5)) ); - assert_eq!(rectangle.area(), 21.0, "incorrect area"); - assert_eq!(rectangle.perimeter(), 20.0, "incorrect perimeter"); } #[test] @@ -2578,8 +2228,6 @@ mod tests { let polygon = RegularPolygon::new(3.0, 6); assert_eq!(polygon.inradius(), 2.598076, "incorrect inradius"); assert_eq!(polygon.side_length(), 3.0, "incorrect side length"); - assert_relative_eq!(polygon.area(), 23.38268, epsilon = 0.00001); - assert_eq!(polygon.perimeter(), 18.0, "incorrect perimeter"); assert_eq!( polygon.internal_angle_degrees(), 120.0, diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_shape/src/dim3.rs similarity index 89% rename from crates/bevy_math/src/primitives/dim3.rs rename to crates/bevy_shape/src/dim3.rs index 8764efcff4ce5..07274a922486b 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_shape/src/dim3.rs @@ -1,16 +1,16 @@ -use core::f32::consts::{FRAC_PI_3, PI}; +use core::f32::consts::PI; -use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; -use crate::{ +use crate::dim2::Circle; +use crate::{Primitive2d, Primitive3d}; +use bevy_math::{ ops::{self, FloatPow}, - Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3, + Dir3, InvalidDirectionError, Isometry3d, Mat3, Quat, Ray3d, Vec2, Vec3, }; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use glam::Quat; #[cfg(feature = "alloc")] use alloc::vec::Vec; @@ -74,20 +74,6 @@ impl Sphere { } } -impl Measured3d for Sphere { - /// Get the surface area of the sphere - #[inline] - fn area(&self) -> f32 { - 4.0 * PI * self.radius.squared() - } - - /// Get the volume of the sphere - #[inline] - fn volume(&self) -> f32 { - 4.0 * FRAC_PI_3 * self.radius.cubed() - } -} - /// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -159,18 +145,6 @@ impl Plane3d { ) } } -impl Measured2d for Plane3d { - #[inline] - fn area(&self) -> f32 { - self.half_size.element_product() * 4.0 - } - - #[inline] - fn perimeter(&self) -> f32 { - self.half_size.element_sum() * 4.0 - } -} - /// An unbounded plane in 3D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -753,22 +727,6 @@ impl Cuboid { } } -impl Measured3d for Cuboid { - /// Get the surface area of the cuboid - #[inline] - fn area(&self) -> f32 { - 8.0 * (self.half_size.x * self.half_size.y - + self.half_size.y * self.half_size.z - + self.half_size.x * self.half_size.z) - } - - /// Get the volume of the cuboid - #[inline] - fn volume(&self) -> f32 { - 8.0 * self.half_size.x * self.half_size.y * self.half_size.z - } -} - /// A cylinder primitive centered on the origin #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -833,20 +791,6 @@ impl Cylinder { } } -impl Measured3d for Cylinder { - /// Get the total surface area of the cylinder - #[inline] - fn area(&self) -> f32 { - 2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height) - } - - /// Get the volume of the cylinder - #[inline] - fn volume(&self) -> f32 { - self.base_area() * 2.0 * self.half_height - } -} - /// A 3D capsule primitive centered on the origin /// A three-dimensional capsule is defined as a surface at a distance (radius) from a line #[derive(Clone, Copy, Debug, PartialEq)] @@ -900,23 +844,6 @@ impl Capsule3d { } } -impl Measured3d for Capsule3d { - /// Get the surface area of the capsule - #[inline] - fn area(&self) -> f32 { - // Modified version of 2pi * r * (2r + h) - 4.0 * PI * self.radius * (self.radius + self.half_length) - } - - /// Get the volume of the capsule - #[inline] - fn volume(&self) -> f32 { - // Modified version of pi * r^2 * (4/3 * r + a) - let diameter = self.radius * 2.0; - PI * self.radius * diameter * (diameter / 3.0 + self.half_length) - } -} - /// A cone primitive centered on the midpoint between the tip of the cone and the center of its base. /// /// The cone is oriented with its tip pointing towards the Y axis. @@ -986,20 +913,6 @@ impl Cone { } } -impl Measured3d for Cone { - /// Get the total surface area of the cone - #[inline] - fn area(&self) -> f32 { - self.base_area() + self.lateral_area() - } - - /// Get the volume of the cone - #[inline] - fn volume(&self) -> f32 { - (self.base_area() * self.height) / 3.0 - } -} - /// A conical frustum primitive. /// A conical frustum can be created /// by slicing off a section of a cone. @@ -1082,21 +995,6 @@ impl ConicalFrustum { } } -impl Measured3d for ConicalFrustum { - #[inline] - fn volume(&self) -> f32 { - FRAC_PI_3 - * self.height - * (self.radius_bottom * self.radius_bottom - + self.radius_top * self.radius_top - + self.radius_top * self.radius_bottom) - } - #[inline] - fn area(&self) -> f32 { - self.bottom_base_area() + self.top_base_area() + self.lateral_area() - } -} - /// The type of torus determined by the minor and major radii #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TorusKind { @@ -1212,22 +1110,6 @@ impl Torus { } } -impl Measured3d for Torus { - /// Get the surface area of the torus. Note that this only produces - /// the expected result when the torus has a ring and isn't self-intersecting - #[inline] - fn area(&self) -> f32 { - 4.0 * PI.squared() * self.major_radius * self.minor_radius - } - - /// Get the volume of the torus. Note that this only produces - /// the expected result when the torus has a ring and isn't self-intersecting - #[inline] - fn volume(&self) -> f32 { - 2.0 * PI.squared() * self.major_radius * self.minor_radius.squared() - } -} - /// A 3D triangle primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -1407,24 +1289,6 @@ impl Triangle3d { } } -impl Measured2d for Triangle3d { - /// Get the area of the triangle. - #[inline] - fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - ab.cross(ac).length() / 2.0 - } - - /// Get the perimeter of the triangle. - #[inline] - fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - a.distance(b) + b.distance(c) + c.distance(a) - } -} - /// A tetrahedron primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -1509,30 +1373,6 @@ impl Tetrahedron { } } -impl Measured3d for Tetrahedron { - /// Get the surface area of the tetrahedron. - #[inline] - fn area(&self) -> f32 { - let [a, b, c, d] = self.vertices; - let ab = b - a; - let ac = c - a; - let ad = d - a; - let bc = c - b; - let bd = d - b; - (ab.cross(ac).length() - + ab.cross(ad).length() - + ac.cross(ad).length() - + bc.cross(bd).length()) - / 2.0 - } - - /// Get the volume of the tetrahedron. - #[inline] - fn volume(&self) -> f32 { - ops::abs(self.signed_volume()) - } -} - /// A 3D shape representing an extruded 2D `base_shape`. /// /// Extruding a shape effectively "thickens" a 2D shapes, @@ -1562,25 +1402,13 @@ impl Extrusion { } } -impl Measured3d for Extrusion { - /// Get the surface area of the extrusion - fn area(&self) -> f32 { - 2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter()) - } - - /// Get the volume of the extrusion - fn volume(&self) -> f32 { - 2. * self.base_shape.area() * self.half_depth - } -} - #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools use super::*; - use crate::{InvalidDirectionError, Quat}; use approx::assert_relative_eq; + use bevy_math::{InvalidDirectionError, Quat}; #[test] fn direction_creation() { @@ -1688,8 +1516,6 @@ mod tests { fn sphere_math() { let sphere = Sphere { radius: 4.0 }; assert_eq!(sphere.diameter(), 8.0, "incorrect diameter"); - assert_eq!(sphere.area(), 201.06193, "incorrect area"); - assert_eq!(sphere.volume(), 268.08257, "incorrect volume"); } #[test] @@ -1763,8 +1589,6 @@ mod tests { Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)), "incorrect dimensions when created from corners" ); - assert_eq!(cuboid.area(), 82.0, "incorrect area"); - assert_eq!(cuboid.volume(), 42.0, "incorrect volume"); } #[test] @@ -1781,8 +1605,6 @@ mod tests { "incorrect lateral area" ); assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area"); - assert_relative_eq!(cylinder.area(), 138.23007); - assert_eq!(cylinder.volume(), 113.097336, "incorrect volume"); } #[test] @@ -1793,8 +1615,6 @@ mod tests { Cylinder::new(2.0, 9.0), "cylinder wasn't created correctly from a capsule" ); - assert_eq!(capsule.area(), 163.36282, "incorrect area"); - assert_relative_eq!(capsule.volume(), 146.60765); } #[test] @@ -1811,8 +1631,6 @@ mod tests { assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height"); assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area"); assert_eq!(cone.base_area(), 12.566371, "incorrect base area"); - assert_relative_eq!(cone.area(), 70.49447); - assert_eq!(cone.volume(), 37.699111, "incorrect volume"); } #[test] @@ -1840,8 +1658,6 @@ mod tests { "incorrect bottom base area" ); assert_eq!(frustum.top_base_area(), PI, "incorrect top base area"); - assert_eq!(frustum.area(), 101.05296, "incorrect surface area"); - assert_eq!(frustum.volume(), 65.97345, "incorrect volume"); } #[test] @@ -1868,8 +1684,6 @@ mod tests { TorusKind::Invalid, "torus should be invalid" ); - assert_relative_eq!(torus.area(), 33.16187); - assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001); } #[test] @@ -1882,8 +1696,6 @@ mod tests { Vec3::new(-1.0, -2.0, 3.5), ], }; - assert_eq!(tetrahedron.area(), 19.251068, "incorrect area"); - assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume"); assert_eq!( tetrahedron.signed_volume(), 3.2058334, @@ -1891,12 +1703,6 @@ mod tests { ); assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55)); - assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area"); - assert_eq!( - Tetrahedron::default().volume(), - 0.33333334, - "incorrect volume" - ); assert_eq!( Tetrahedron::default().signed_volume(), -0.33333334, @@ -1905,24 +1711,6 @@ mod tests { assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO); } - #[test] - fn extrusion_math() { - let circle = Circle::new(0.75); - let cylinder = Extrusion::new(circle, 2.5); - assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); - assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); - - let annulus = crate::primitives::Annulus::new(0.25, 1.375); - let tube = Extrusion::new(annulus, 0.333); - assert_eq!(tube.area(), 14.886437, "incorrect surface area"); - assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); - - let polygon = crate::primitives::RegularPolygon::new(3.8, 7); - let regular_prism = Extrusion::new(polygon, 1.25); - assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); - assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); - } - #[test] fn triangle_math() { // Default triangle tests @@ -1932,12 +1720,6 @@ mod tests { Vec3::new(-0.5, -0.5, 0.0), Vec3::new(0.0, 0.5, 0.0), ); - assert_eq!(default_triangle.area(), 0.5, "incorrect area"); - assert_relative_eq!( - default_triangle.perimeter(), - 1.0 + 2.0 * ops::sqrt(1.25_f32), - epsilon = 10e-9 - ); assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal"); assert!( !default_triangle.is_degenerate(), @@ -1994,8 +1776,6 @@ mod tests { let triangle = Triangle3d::new(a, b, c); assert!(!triangle.is_degenerate(), "incorrectly found degenerate"); - assert_eq!(triangle.area(), 3.0233467, "incorrect area"); - assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter"); assert_eq!( triangle.circumcenter(), Vec3::new(-1., 1.75, 0.75), diff --git a/crates/bevy_math/src/primitives/half_space.rs b/crates/bevy_shape/src/half_space.rs similarity index 97% rename from crates/bevy_math/src/primitives/half_space.rs rename to crates/bevy_shape/src/half_space.rs index a955b8b0a6bb9..c8ba6212f21a2 100644 --- a/crates/bevy_math/src/primitives/half_space.rs +++ b/crates/bevy_shape/src/half_space.rs @@ -1,4 +1,4 @@ -use crate::{ops, Vec3, Vec3A, Vec4, Vec4Swizzles}; +use bevy_math::{ops, Vec3, Vec3A, Vec4, Vec4Swizzles}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -108,8 +108,8 @@ mod half_space_tests { use approx::assert_relative_eq; - use super::HalfSpace; - use crate::{Vec3, Vec4}; + use crate::half_space::HalfSpace; + use bevy_math::{Vec3, Vec4}; #[test] fn intersection_point() { diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_shape/src/lib.rs similarity index 51% rename from crates/bevy_math/src/primitives/mod.rs rename to crates/bevy_shape/src/lib.rs index c2cb17f963123..30d300d22fb20 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_shape/src/lib.rs @@ -1,14 +1,35 @@ +#![forbid(unsafe_code)] +#![cfg_attr( + any(docsrs, docsrs_dep), + expect( + internal_features, + reason = "rustdoc_internals is needed for fake_variadic" + ) +)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] +#![no_std] + //! This module defines primitive shapes. //! The origin is (0, 0) for 2D primitives and (0, 0, 0) for 3D primitives, //! unless stated otherwise. +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "alloc")] +extern crate alloc; + mod dim2; pub use dim2::*; mod dim3; pub use dim3::*; -mod inset; -pub use inset::*; mod half_space; +#[cfg(feature = "alloc")] mod polygon; pub use half_space::*; mod view_frustum; @@ -20,6 +41,10 @@ pub trait Primitive2d {} /// A marker trait for 3D primitives pub trait Primitive3d {} +impl Primitive2d for bevy_math::Dir2 {} +impl Primitive3d for bevy_math::Dir3 {} +impl Primitive3d for bevy_math::Dir3A {} + /// The winding order for a set of points #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[doc(alias = "Orientation")] @@ -35,20 +60,11 @@ pub enum WindingOrder { Invalid, } -/// A trait for getting measurements of 2D shapes -pub trait Measured2d { - /// Get the perimeter of the shape - fn perimeter(&self) -> f32; - - /// Get the area of the shape - fn area(&self) -> f32; -} - -/// A trait for getting measurements of 3D shapes -pub trait Measured3d { - /// Get the surface area of the shape - fn area(&self) -> f32; - - /// Get the volume of the shape - fn volume(&self) -> f32; +/// The shape prelude. +/// +/// This includes all primitive shape types in this crate, re-exported for your convenience. +pub mod prelude { + // just re-export everything, it's just shape definitions anyways + #[doc(hidden)] + pub use crate::*; } diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_shape/src/polygon.rs similarity index 91% rename from crates/bevy_math/src/primitives/polygon.rs rename to crates/bevy_shape/src/polygon.rs index 71197d3fd8101..9c58748346c11 100644 --- a/crates/bevy_math/src/primitives/polygon.rs +++ b/crates/bevy_shape/src/polygon.rs @@ -1,14 +1,8 @@ -#[cfg(feature = "alloc")] -use { - super::{Measured2d, Triangle2d}, - alloc::{collections::BTreeMap, vec::Vec}, - core::cmp::Ordering, -}; - -use crate::Vec2; +use alloc::{collections::BTreeMap, vec::Vec}; +use bevy_math::{ops, Vec2}; +use core::cmp::Ordering; #[derive(Debug, Clone, Copy)] -#[cfg(feature = "alloc")] enum Endpoint { Left, Right, @@ -20,7 +14,6 @@ enum Endpoint { /// If `e1.position().x == e2.position().x` the events are ordered from bottom to top. /// /// This is the order expected by the [`SweepLine`]. -#[cfg(feature = "alloc")] #[derive(Debug, Clone, Copy)] struct SweepLineEvent { segment: Segment, @@ -28,7 +21,6 @@ struct SweepLineEvent { endpoint: Endpoint, } -#[cfg(feature = "alloc")] impl SweepLineEvent { const fn position(&self) -> Vec2 { match self.endpoint { @@ -38,24 +30,20 @@ impl SweepLineEvent { } } -#[cfg(feature = "alloc")] impl PartialEq for SweepLineEvent { fn eq(&self, other: &Self) -> bool { self.position() == other.position() } } -#[cfg(feature = "alloc")] impl Eq for SweepLineEvent {} -#[cfg(feature = "alloc")] impl PartialOrd for SweepLineEvent { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -#[cfg(feature = "alloc")] impl Ord for SweepLineEvent { fn cmp(&self, other: &Self) -> Ordering { xy_order(self.position(), other.position()) @@ -63,18 +51,15 @@ impl Ord for SweepLineEvent { } /// Orders 2D points according to the order expected by the sweep line and event queue from -X to +X and then -Y to Y. -#[cfg(feature = "alloc")] fn xy_order(a: Vec2, b: Vec2) -> Ordering { a.x.total_cmp(&b.x).then_with(|| a.y.total_cmp(&b.y)) } /// The event queue holds an ordered list of all events the [`SweepLine`] will encounter when checking the current polygon. -#[cfg(feature = "alloc")] #[derive(Debug, Clone)] struct EventQueue { events: Vec, } -#[cfg(feature = "alloc")] impl EventQueue { /// Initialize a new `EventQueue` with all events from the polygon represented by `vertices`. /// @@ -120,31 +105,26 @@ impl EventQueue { /// Segments are ordered from bottom to top based on their left vertices if possible. /// If their y values are identical, the segments are ordered based on the y values of their right vertices. #[derive(Debug, Clone, Copy)] -#[cfg(feature = "alloc")] struct Segment { edge_index: usize, left: Vec2, right: Vec2, } -#[cfg(feature = "alloc")] impl PartialEq for Segment { fn eq(&self, other: &Self) -> bool { self.edge_index == other.edge_index } } -#[cfg(feature = "alloc")] impl Eq for Segment {} -#[cfg(feature = "alloc")] impl PartialOrd for Segment { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -#[cfg(feature = "alloc")] impl Ord for Segment { fn cmp(&self, other: &Self) -> Ordering { self.left @@ -155,7 +135,6 @@ impl Ord for Segment { } /// Holds information about which segment is above and which is below a given [`Segment`] -#[cfg(feature = "alloc")] #[derive(Debug, Clone, Copy)] struct SegmentOrder { above: Option, @@ -167,12 +146,11 @@ struct SegmentOrder { /// It can be thought of as a vertical line sweeping from -X to +X across the polygon that keeps track of the order of the segments /// the sweep line is intersecting at any given moment. #[derive(Debug, Clone)] -#[cfg(feature = "alloc")] struct SweepLine<'a> { vertices: &'a [Vec2], tree: BTreeMap, } -#[cfg(feature = "alloc")] + impl<'a> SweepLine<'a> { const fn new(vertices: &'a [Vec2]) -> Self { Self { @@ -263,13 +241,6 @@ impl<'a> SweepLine<'a> { /// Test what side of the line through `p1` and `p2` `q` is. /// /// The result will be `0` if the `q` is on the segment, negative for one side and positive for the other. -#[cfg_attr( - not(feature = "alloc"), - expect( - dead_code, - reason = "this function is only used with the alloc feature" - ) -)] #[inline] const fn point_side(p1: Vec2, p2: Vec2, q: Vec2) -> f32 { (p2.x - p1.x) * (q.y - p1.y) - (q.x - p1.x) * (p2.y - p1.y) @@ -285,13 +256,15 @@ const fn point_side(p1: Vec2, p2: Vec2, q: Vec2) -> f32 { /// /// The algorithm used is the Shamos-Hoey algorithm, a version of the Bentley-Ottman algorithm adapted to only detect whether any intersections exist. /// This function will run in O(n * log n) -#[cfg(feature = "alloc")] pub fn is_polygon_simple(vertices: &[Vec2]) -> bool { if vertices.len() < 3 { return true; } if vertices.len() == 3 { - return Triangle2d::new(vertices[0], vertices[1], vertices[2]).area() > 0.0; + let [a, b, c] = [vertices[0], vertices[1], vertices[2]]; + let triangle_area = + ops::abs(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0; + return triangle_area > 0.0; } let event_queue = EventQueue::new(vertices); @@ -323,7 +296,8 @@ pub fn is_polygon_simple(vertices: &[Vec2]) -> bool { #[cfg(test)] mod tests { - use crate::{primitives::polygon::is_polygon_simple, Vec2}; + use super::*; + use bevy_math::Vec2; #[test] fn complex_polygon() { diff --git a/crates/bevy_math/src/primitives/view_frustum.rs b/crates/bevy_shape/src/view_frustum.rs similarity index 98% rename from crates/bevy_math/src/primitives/view_frustum.rs rename to crates/bevy_shape/src/view_frustum.rs index 604aee6f155aa..cb480f8195654 100644 --- a/crates/bevy_math/src/primitives/view_frustum.rs +++ b/crates/bevy_shape/src/view_frustum.rs @@ -1,4 +1,5 @@ -use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4}; +use crate::half_space::HalfSpace; +use bevy_math::{Mat4, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -114,7 +115,8 @@ mod view_frustum_tests { use approx::assert_relative_eq; use super::ViewFrustum; - use crate::{primitives::HalfSpace, Vec3, Vec4}; + use crate::half_space::HalfSpace; + use bevy_math::{Vec3, Vec4}; #[test] fn cuboid_frustum_corners() { diff --git a/crates/bevy_sprite_render/Cargo.toml b/crates/bevy_sprite_render/Cargo.toml index 7153220e0ad22..7d640c9aebae1 100644 --- a/crates/bevy_sprite_render/Cargo.toml +++ b/crates/bevy_sprite_render/Cargo.toml @@ -24,6 +24,7 @@ bevy_image = { path = "../bevy_image", version = "0.20.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.20.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.20.0-dev" } bevy_math = { path = "../bevy_math", version = "0.20.0-dev" } +bevy_shape = { path = "../bevy_shape", version = "0.20.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.20.0-dev" } bevy_material = { path = "../bevy_material", version = "0.20.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.20.0-dev" } diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index a8238a5a09157..2946af9cc75da 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -9,8 +9,9 @@ use bevy_ecs::{ use bevy_asset::{Assets, Handle}; use bevy_image::TextureAtlasLayout; -use bevy_math::{primitives::Rectangle, vec2}; +use bevy_math::vec2; use bevy_mesh::{Mesh, Mesh2d, MeshAttributeCompressionFlags, MeshBuilder, Meshable}; +use bevy_shape::Rectangle; use bevy_platform::collections::HashMap; use bevy_shader::load_shader_library; diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index d8353466ff7b4..bf320dc623d5c 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -15,10 +15,11 @@ use bevy_ecs::{ world::DeferredWorld, }; use bevy_image::Image; -use bevy_math::{primitives::Rectangle, UVec2}; +use bevy_math::UVec2; use bevy_mesh::{Mesh, Mesh2d}; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::*, Reflect}; +use bevy_shape::Rectangle; use bevy_transform::components::Transform; use bevy_utils::default; use tracing::warn; diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index af9c3ec5638b8..ab13cc9f47d41 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -6,10 +6,7 @@ use std::f32::consts::FRAC_PI_2; use bevy::{ color::palettes::css::{BLUE, GRAY, RED}, - math::{ - bounding::{Bounded2d, BoundingVolume}, - Isometry2d, - }, + math::Isometry2d, mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, prelude::*, }; diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 0e640fa0c7e77..2fb91642dfd00 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -13,7 +13,7 @@ use bevy::{ light::Skybox, - math::{cubic_splines::LinearSpline, primitives::Plane3d, vec2}, + math::{cubic_splines::LinearSpline, vec2}, post_process::auto_exposure::{ AutoExposure, AutoExposureCompensationCurve, AutoExposurePlugin, }, diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index ca52b4f6dbad8..86e5a14d32dfe 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -2,7 +2,6 @@ //! syntax. use bevy::ecs::{entity::SpawnError, error::warn, world::DeferredWorld}; -use bevy::math::sampling::UniformMeshSampler; use bevy::prelude::*; use chacha20::ChaCha8Rng; diff --git a/examples/math/bounding_2d.rs b/examples/math/bounding_2d.rs index aa3e611f13759..54a5c2d4111f2 100644 --- a/examples/math/bounding_2d.rs +++ b/examples/math/bounding_2d.rs @@ -2,7 +2,7 @@ use bevy::{ color::palettes::css::*, - math::{bounding::*, ops, Isometry2d}, + math::{ops, Isometry2d}, prelude::*, }; diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index 5db89f0be85f5..035161e721cca 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -11,12 +11,7 @@ use bevy::{ camera::ScalingMode, color::palettes::css::{RED, WHITE}, input::common_conditions::{input_just_pressed, input_toggle_active}, - math::{ - bounding::{ - Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume, - }, - Isometry2d, - }, + math::Isometry2d, mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment}, prelude::*, }; diff --git a/examples/showcase/breakout.rs b/examples/showcase/breakout.rs index a4a943ccaa7ed..b9bfb8b4b2cd1 100644 --- a/examples/showcase/breakout.rs +++ b/examples/showcase/breakout.rs @@ -2,10 +2,7 @@ //! //! Demonstrates Bevy's stepping capabilities if compiled with the `bevy_debug_stepping` feature. -use bevy::{ - math::bounding::{Aabb2d, BoundingCircle, BoundingVolume, IntersectsVolume}, - prelude::*, -}; +use bevy::prelude::*; mod stepping; diff --git a/examples/showcase/contributors.rs b/examples/showcase/contributors.rs index c92d3c7bded29..2f1c0d48c3613 100644 --- a/examples/showcase/contributors.rs +++ b/examples/showcase/contributors.rs @@ -1,6 +1,6 @@ //! This example displays each contributor to the bevy source code as a bouncing bevy-ball. -use bevy::{math::bounding::Aabb2d, prelude::*}; +use bevy::prelude::*; use chacha20::ChaCha8Rng; use rand::{RngExt, SeedableRng};