From de76baa81fa6e536649fdbcca8ea37bf3ad51bdc Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 11:14:29 +0200 Subject: [PATCH 01/17] init bevy_shapes --- crates/bevy_shapes/Cargo.toml | 15 +++ crates/bevy_shapes/LICENSE-APACHE | 176 ++++++++++++++++++++++++++++++ crates/bevy_shapes/LICENSE-MIT | 19 ++++ crates/bevy_shapes/README.md | 7 ++ crates/bevy_shapes/clippy.toml | 39 +++++++ crates/bevy_shapes/src/lib.rs | 14 +++ 6 files changed, 270 insertions(+) create mode 100644 crates/bevy_shapes/Cargo.toml create mode 100644 crates/bevy_shapes/LICENSE-APACHE create mode 100644 crates/bevy_shapes/LICENSE-MIT create mode 100644 crates/bevy_shapes/README.md create mode 100644 crates/bevy_shapes/clippy.toml create mode 100644 crates/bevy_shapes/src/lib.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml new file mode 100644 index 0000000000000..fdd0e8eb0d333 --- /dev/null +++ b/crates/bevy_shapes/Cargo.toml @@ -0,0 +1,15 @@ +[package] +description = "Provides primitive shapes for Bevy Engine" +edition = "2024" +homepage = "https://bevy.org" +keywords = ["bevy"] +license = "MIT OR Apache-2.0" +name = "bevy_shapes" +repository = "https://github.com/bevyengine/bevy" +rust-version = "1.94.0" +version = "0.20.0-dev" + +[dependencies] + +[lints] +workspace = true diff --git a/crates/bevy_shapes/LICENSE-APACHE b/crates/bevy_shapes/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_shapes/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_shapes/LICENSE-MIT b/crates/bevy_shapes/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_shapes/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_shapes/README.md b/crates/bevy_shapes/README.md new file mode 100644 index 0000000000000..b8ee9946c6811 --- /dev/null +++ b/crates/bevy_shapes/README.md @@ -0,0 +1,7 @@ +# Bevy Shapes + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_shapes.svg)](https://crates.io/crates/bevy_shapes) +[![Downloads](https://img.shields.io/crates/d/bevy_shapes.svg)](https://crates.io/crates/bevy_shapes) +[![Docs](https://docs.rs/bevy_shapes/badge.svg)](https://docs.rs/bevy_shapes/latest/bevy_shapes/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_shapes/clippy.toml b/crates/bevy_shapes/clippy.toml new file mode 100644 index 0000000000000..c1f67e044d8ec --- /dev/null +++ b/crates/bevy_shapes/clippy.toml @@ -0,0 +1,39 @@ +disallowed-methods = [ + { path = "f32::powi", reason = "use ops::FloatPow::squared, ops::FloatPow::cubed, or ops::powf instead for libm determinism" }, + { path = "f32::log", reason = "use ops::ln, ops::log2, or ops::log10 instead for libm determinism" }, + { path = "f32::abs_sub", reason = "deprecated and deeply confusing method" }, + { path = "f32::powf", reason = "use ops::powf instead for libm determinism" }, + { path = "f32::exp", reason = "use ops::exp instead for libm determinism" }, + { path = "f32::exp2", reason = "use ops::exp2 instead for libm determinism" }, + { path = "f32::ln", reason = "use ops::ln instead for libm determinism" }, + { path = "f32::log2", reason = "use ops::log2 instead for libm determinism" }, + { path = "f32::log10", reason = "use ops::log10 instead for libm determinism" }, + { path = "f32::cbrt", reason = "use ops::cbrt instead for libm determinism" }, + { path = "f32::hypot", reason = "use ops::hypot instead for libm determinism" }, + { path = "f32::sin", reason = "use ops::sin instead for libm determinism" }, + { path = "f32::cos", reason = "use ops::cos instead for libm determinism" }, + { path = "f32::tan", reason = "use ops::tan instead for libm determinism" }, + { path = "f32::asin", reason = "use ops::asin instead for libm determinism" }, + { path = "f32::acos", reason = "use ops::acos instead for libm determinism" }, + { path = "f32::atan", reason = "use ops::atan instead for libm determinism" }, + { path = "f32::atan2", reason = "use ops::atan2 instead for libm determinism" }, + { path = "f32::sin_cos", reason = "use ops::sin_cos instead for libm determinism" }, + { path = "f32::exp_m1", reason = "use ops::exp_m1 instead for libm determinism" }, + { path = "f32::ln_1p", reason = "use ops::ln_1p instead for libm determinism" }, + { path = "f32::sinh", reason = "use ops::sinh instead for libm determinism" }, + { path = "f32::cosh", reason = "use ops::cosh instead for libm determinism" }, + { path = "f32::tanh", reason = "use ops::tanh instead for libm determinism" }, + { path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" }, + { path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" }, + { path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" }, + # These methods have defined precision, but are only available from the standard library, + # not in core. Using these substitutes allows for no_std compatibility. + { path = "f32::rem_euclid", reason = "use ops::rem_euclid instead for no_std compatibility" }, + { path = "f32::abs", reason = "use ops::abs instead for no_std compatibility" }, + { path = "f32::sqrt", reason = "use ops::sqrt instead for no_std compatibility" }, + { path = "f32::copysign", reason = "use ops::copysign instead for no_std compatibility" }, + { path = "f32::round", reason = "use ops::round instead for no_std compatibility" }, + { path = "f32::floor", reason = "use ops::floor instead for no_std compatibility" }, + { path = "f32::ceil", reason = "use ops::ceil instead for no_std compatibility" }, + { path = "f32::fract", reason = "use ops::fract instead for no_std compatibility" }, +] diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs new file mode 100644 index 0000000000000..b93cf3ffd9cc9 --- /dev/null +++ b/crates/bevy_shapes/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From d0767dec4d3046042c2bbbb2ced46bbd10aa7cee Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 11:26:12 +0200 Subject: [PATCH 02/17] move primitives from bevy_math over to the new bevy_shapes crate --- crates/bevy_shapes/Cargo.toml | 35 + crates/bevy_shapes/src/lib.rs | 33 +- crates/bevy_shapes/src/primitives/dim2.rs | 2637 +++++++++++++++++ crates/bevy_shapes/src/primitives/dim3.rs | 2063 +++++++++++++ .../bevy_shapes/src/primitives/half_space.rs | 153 + crates/bevy_shapes/src/primitives/inset.rs | 98 + crates/bevy_shapes/src/primitives/mod.rs | 54 + crates/bevy_shapes/src/primitives/polygon.rs | 376 +++ .../src/primitives/view_frustum.rs | 253 ++ 9 files changed, 5690 insertions(+), 12 deletions(-) create mode 100644 crates/bevy_shapes/src/primitives/dim2.rs create mode 100644 crates/bevy_shapes/src/primitives/dim3.rs create mode 100644 crates/bevy_shapes/src/primitives/half_space.rs create mode 100644 crates/bevy_shapes/src/primitives/inset.rs create mode 100644 crates/bevy_shapes/src/primitives/mod.rs create mode 100644 crates/bevy_shapes/src/primitives/polygon.rs create mode 100644 crates/bevy_shapes/src/primitives/view_frustum.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index fdd0e8eb0d333..514af323bfa61 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -10,6 +10,41 @@ 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] +bevy_math = { default-features = false, features = [ + "approx", +], path = "../bevy_math" } + +[features] +alloc = ["bevy_math/alloc"] +default = ["bevy_math/default", "std"] +std = ["alloc", "bevy_math/std"] + +approx = ["bevy_math/approx", "dep:approx"] +bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] +serialize = ["bevy_math/serialize", "dep:serde"] [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_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index b93cf3ffd9cc9..ff66ac1b6242d 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -1,14 +1,23 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![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] -#[cfg(test)] -mod tests { - use super::*; +#[cfg(feature = "std")] +extern crate std; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[cfg(feature = "alloc")] +extern crate alloc; + +pub mod primitives; diff --git a/crates/bevy_shapes/src/primitives/dim2.rs b/crates/bevy_shapes/src/primitives/dim2.rs new file mode 100644 index 0000000000000..58b6e5c8eefb3 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/dim2.rs @@ -0,0 +1,2637 @@ +use core::f32::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI}; +use derive_more::derive::From; +#[cfg(feature = "alloc")] +use thiserror::Error; + +use super::{Measured2d, Primitive2d, WindingOrder}; +use bevy_math::{ + ops::{self, FloatPow}, + primitives::Inset, + Dir2, InvalidDirectionError, Isometry2d, Ray2d, Rot2, Vec2, +}; + +#[cfg(feature = "alloc")] +use super::polygon::is_polygon_simple; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +/// A circle primitive, representing the set of points some distance from the origin +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Circle { + /// The radius of the circle + pub radius: f32, +} + +impl Primitive2d for Circle {} + +impl Default for Circle { + /// Returns the default [`Circle`] with a radius of `0.5`. + fn default() -> Self { + Self { radius: 0.5 } + } +} + +impl Circle { + /// Create a new [`Circle`] from a `radius` + #[inline] + pub const fn new(radius: f32) -> Self { + Self { radius } + } + + /// Get the diameter of the circle + #[inline] + pub const fn diameter(&self) -> f32 { + 2.0 * self.radius + } + + /// Finds the point on the circle that is closest to the given `point`. + /// + /// If the point is outside the circle, the returned point will be on the perimeter of the circle. + /// Otherwise, it will be inside the circle and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + let distance_squared = point.length_squared(); + + if distance_squared <= self.radius.squared() { + // The point is inside the circle. + point + } else { + // The point is outside the circle. + // Find the closest point on the perimeter of the circle. + let dir_to_point = point / ops::sqrt(distance_squared); + self.radius * dir_to_point + } + } +} + +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. +/// If you want to include the portion of a circle's area swept out by the arc, +/// use the pie-shaped [`CircularSector`]. +/// If you want to include only the space inside the convex hull of the arc, +/// use the bowl-shaped [`CircularSegment`]. +/// +/// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on +/// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this +/// means that the origin may not be within the `Arc2d`'s convex hull. +/// +/// **Warning:** Arcs with negative angle or radius, or with angle greater than an entire circle, are not officially supported. +/// It is recommended to normalize arcs to have an angle in [0, 2π]. +#[derive(Clone, Copy, Debug, PartialEq)] +#[doc(alias("CircularArc", "CircleArc"))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Arc2d { + /// The radius of the circle + pub radius: f32, + /// Half the angle defining the arc + pub half_angle: f32, +} + +impl Primitive2d for Arc2d {} + +impl Default for Arc2d { + /// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle + fn default() -> Self { + Self { + radius: 0.5, + half_angle: 2.0 * FRAC_PI_3, + } + } +} + +impl Arc2d { + /// Create a new [`Arc2d`] from a `radius` and a `half_angle` + #[inline] + pub const fn new(radius: f32, half_angle: f32) -> Self { + Self { radius, half_angle } + } + + /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians + #[inline] + pub const fn from_radians(radius: f32, angle: f32) -> Self { + Self { + radius, + half_angle: angle / 2.0, + } + } + + /// Create a new [`Arc2d`] from a `radius` and an `angle` in degrees. + #[inline] + pub const fn from_degrees(radius: f32, angle: f32) -> Self { + Self { + radius, + half_angle: angle.to_radians() / 2.0, + } + } + + /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a single turn. + /// + /// For instance, `0.5` turns is a semicircle. + #[inline] + pub const fn from_turns(radius: f32, fraction: f32) -> Self { + Self { + radius, + half_angle: fraction * PI, + } + } + + /// Get the angle of the arc + #[inline] + pub const fn angle(&self) -> f32 { + self.half_angle * 2.0 + } + + /// Get the length of the arc + #[inline] + pub const fn length(&self) -> f32 { + self.angle() * self.radius + } + + /// Get the right-hand end point of the arc + #[inline] + pub fn right_endpoint(&self) -> Vec2 { + self.radius * Vec2::from_angle(FRAC_PI_2 - self.half_angle) + } + + /// Get the left-hand end point of the arc + #[inline] + pub fn left_endpoint(&self) -> Vec2 { + self.radius * Vec2::from_angle(FRAC_PI_2 + self.half_angle) + } + + /// Get the endpoints of the arc + #[inline] + pub fn endpoints(&self) -> [Vec2; 2] { + [self.left_endpoint(), self.right_endpoint()] + } + + /// Get the midpoint of the arc + #[inline] + pub fn midpoint(&self) -> Vec2 { + self.radius * Vec2::Y + } + + /// Get half the distance between the endpoints (half the length of the chord) + #[inline] + pub fn half_chord_length(&self) -> f32 { + self.radius * ops::sin(self.half_angle) + } + + /// Get the distance between the endpoints (the length of the chord) + #[inline] + pub fn chord_length(&self) -> f32 { + 2.0 * self.half_chord_length() + } + + /// Get the midpoint of the two endpoints (the midpoint of the chord) + #[inline] + pub fn chord_midpoint(&self) -> Vec2 { + self.apothem() * Vec2::Y + } + + /// Get the length of the apothem of this arc, that is, + /// the distance from the center of the circle to the midpoint of the chord, in the direction of the midpoint of the arc. + /// Equivalently, the [`radius`](Self::radius) minus the [`sagitta`](Self::sagitta). + /// + /// Note that for a [`major`](Self::is_major) arc, the apothem will be negative. + #[inline] + // Naming note: Various sources are inconsistent as to whether the apothem is the segment between the center and the + // midpoint of a chord, or the length of that segment. Given this confusion, we've opted for the definition + // used by Wolfram MathWorld, which is the distance rather than the segment. + pub fn apothem(&self) -> f32 { + let sign = if self.is_minor() { 1.0 } else { -1.0 }; + sign * ops::sqrt(self.radius.squared() - self.half_chord_length().squared()) + } + + /// Get the length of the sagitta of this arc, that is, + /// the length of the line between the midpoints of the arc and its chord. + /// Equivalently, the height of the triangle whose base is the chord and whose apex is the midpoint of the arc. + /// + /// The sagitta is also the sum of the [`radius`](Self::radius) and the [`apothem`](Self::apothem). + pub fn sagitta(&self) -> f32 { + self.radius - self.apothem() + } + + /// Produces true if the arc is at most half a circle. + /// + /// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor. + #[inline] + pub const fn is_minor(&self) -> bool { + self.half_angle <= FRAC_PI_2 + } + + /// Produces true if the arc is at least half a circle. + /// + /// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor. + #[inline] + pub const fn is_major(&self) -> bool { + self.half_angle >= FRAC_PI_2 + } +} + +/// A primitive representing a circular sector: a pie slice of a circle. +/// +/// The segment is positioned so that it always includes [`Vec2::Y`] and is vertically symmetrical. +/// To orient the sector differently, apply a rotation. +/// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. +/// +/// **Warning:** Circular sectors with negative angle or radius, or with angle greater than an entire circle, are not officially supported. +/// We recommend normalizing circular sectors to have an angle in [0, 2π]. +#[derive(Clone, Copy, Debug, PartialEq, From)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct CircularSector { + /// The arc defining the sector + #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] + pub arc: Arc2d, +} + +impl Primitive2d for CircularSector {} + +impl Default for CircularSector { + /// Returns the default [`CircularSector`] with radius `0.5` and covering a third of a circle + fn default() -> Self { + Self::from(Arc2d::default()) + } +} + +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] + pub const fn new(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::new(radius, angle), + } + } + + /// Create a new [`CircularSector`] from a `radius` and an `angle` in radians. + #[inline] + pub const fn from_radians(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_radians(radius, angle), + } + } + + /// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees. + #[inline] + pub const fn from_degrees(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_degrees(radius, angle), + } + } + + /// Create a new [`CircularSector`] from a `radius` and a number of `turns` of a circle. + /// + /// For instance, `0.5` turns is a semicircle. + #[inline] + pub const fn from_turns(radius: f32, fraction: f32) -> Self { + Self { + arc: Arc2d::from_turns(radius, fraction), + } + } + + /// Get half the angle of the sector + #[inline] + pub const fn half_angle(&self) -> f32 { + self.arc.half_angle + } + + /// Get the angle of the sector + #[inline] + pub const fn angle(&self) -> f32 { + self.arc.angle() + } + + /// Get the radius of the sector + #[inline] + pub const fn radius(&self) -> f32 { + self.arc.radius + } + + /// Get the length of the arc defining the sector + #[inline] + pub const fn arc_length(&self) -> f32 { + self.arc.length() + } + + /// Get half the length of the chord defined by the sector + /// + /// See [`Arc2d::half_chord_length`] + #[inline] + pub fn half_chord_length(&self) -> f32 { + self.arc.half_chord_length() + } + + /// Get the length of the chord defined by the sector + /// + /// See [`Arc2d::chord_length`] + #[inline] + pub fn chord_length(&self) -> f32 { + self.arc.chord_length() + } + + /// Get the midpoint of the chord defined by the sector + /// + /// See [`Arc2d::chord_midpoint`] + #[inline] + pub fn chord_midpoint(&self) -> Vec2 { + self.arc.chord_midpoint() + } + + /// Get the length of the apothem of this sector + /// + /// See [`Arc2d::apothem`] + #[inline] + pub fn apothem(&self) -> f32 { + self.arc.apothem() + } + + /// Get the length of the sagitta of this sector + /// + /// See [`Arc2d::sagitta`] + #[inline] + pub fn sagitta(&self) -> f32 { + self.arc.sagitta() + } +} + +/// A primitive representing a circular segment: +/// the area enclosed by the arc of a circle and its chord (the line between its endpoints). +/// +/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. +/// To orient the segment differently, apply a rotation. +/// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. +/// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful. +/// +/// **Warning:** Circular segments with negative angle or radius, or with angle greater than an entire circle, are not officially supported. +/// We recommend normalizing circular segments to have an angle in [0, 2π]. +#[derive(Clone, Copy, Debug, PartialEq, From)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct CircularSegment { + /// The arc defining the segment + #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] + pub arc: Arc2d, +} + +impl Primitive2d for CircularSegment {} + +impl Default for CircularSegment { + /// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle + fn default() -> Self { + Self::from(Arc2d::default()) + } +} + +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] + pub const fn new(radius: f32, half_angle: f32) -> Self { + Self { + arc: Arc2d::new(radius, half_angle), + } + } + + /// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians. + #[inline] + pub const fn from_radians(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_radians(radius, angle), + } + } + + /// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees. + #[inline] + pub const fn from_degrees(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_degrees(radius, angle), + } + } + + /// Create a new [`CircularSegment`] from a `radius` and a number of `turns` of a circle. + /// + /// For instance, `0.5` turns is a semicircle. + #[inline] + pub const fn from_turns(radius: f32, fraction: f32) -> Self { + Self { + arc: Arc2d::from_turns(radius, fraction), + } + } + + /// Get the half-angle of the segment + #[inline] + pub const fn half_angle(&self) -> f32 { + self.arc.half_angle + } + + /// Get the angle of the segment + #[inline] + pub const fn angle(&self) -> f32 { + self.arc.angle() + } + + /// Get the radius of the segment + #[inline] + pub const fn radius(&self) -> f32 { + self.arc.radius + } + + /// Get the length of the arc defining the segment + #[inline] + pub const fn arc_length(&self) -> f32 { + self.arc.length() + } + + /// Get half the length of the segment's base, also known as its chord + #[inline] + #[doc(alias = "half_base_length")] + pub fn half_chord_length(&self) -> f32 { + self.arc.half_chord_length() + } + + /// Get the length of the segment's base, also known as its chord + #[inline] + #[doc(alias = "base_length")] + #[doc(alias = "base")] + pub fn chord_length(&self) -> f32 { + self.arc.chord_length() + } + + /// Get the midpoint of the segment's base, also known as its chord + #[inline] + #[doc(alias = "base_midpoint")] + pub fn chord_midpoint(&self) -> Vec2 { + self.arc.chord_midpoint() + } + + /// Get the length of the apothem of this segment, + /// which is the signed distance between the segment and the center of its circle + /// + /// See [`Arc2d::apothem`] + #[inline] + pub fn apothem(&self) -> f32 { + self.arc.apothem() + } + + /// Get the length of the sagitta of this segment, also known as its height + /// + /// See [`Arc2d::sagitta`] + #[inline] + #[doc(alias = "height")] + pub fn sagitta(&self) -> f32 { + self.arc.sagitta() + } +} + +#[cfg(test)] +mod arc_tests { + use core::f32::consts::FRAC_PI_4; + use core::f32::consts::SQRT_2; + + use approx::assert_abs_diff_eq; + + use super::*; + + struct ArcTestCase { + radius: f32, + half_angle: f32, + angle: f32, + length: f32, + right_endpoint: Vec2, + left_endpoint: Vec2, + endpoints: [Vec2; 2], + midpoint: Vec2, + half_chord_length: f32, + chord_length: f32, + chord_midpoint: Vec2, + apothem: f32, + sagitta: f32, + is_minor: bool, + is_major: bool, + sector_area: f32, + sector_perimeter: f32, + segment_area: f32, + segment_perimeter: f32, + } + + impl ArcTestCase { + fn check_arc(&self, arc: Arc2d) { + assert_abs_diff_eq!(self.radius, arc.radius); + assert_abs_diff_eq!(self.half_angle, arc.half_angle); + assert_abs_diff_eq!(self.angle, arc.angle()); + assert_abs_diff_eq!(self.length, arc.length()); + assert_abs_diff_eq!(self.right_endpoint, arc.right_endpoint()); + assert_abs_diff_eq!(self.left_endpoint, arc.left_endpoint()); + assert_abs_diff_eq!(self.endpoints[0], arc.endpoints()[0]); + assert_abs_diff_eq!(self.endpoints[1], arc.endpoints()[1]); + assert_abs_diff_eq!(self.midpoint, arc.midpoint()); + assert_abs_diff_eq!(self.half_chord_length, arc.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, arc.chord_length(), epsilon = 0.00001); + assert_abs_diff_eq!(self.chord_midpoint, arc.chord_midpoint()); + assert_abs_diff_eq!(self.apothem, arc.apothem()); + assert_abs_diff_eq!(self.sagitta, arc.sagitta()); + assert_eq!(self.is_minor, arc.is_minor()); + assert_eq!(self.is_major, arc.is_major()); + } + + fn check_sector(&self, sector: CircularSector) { + assert_abs_diff_eq!(self.radius, sector.radius()); + assert_abs_diff_eq!(self.half_angle, sector.half_angle()); + assert_abs_diff_eq!(self.angle, sector.angle()); + assert_abs_diff_eq!(self.half_chord_length, sector.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, sector.chord_length(), epsilon = 0.00001); + 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) { + assert_abs_diff_eq!(self.radius, segment.radius()); + assert_abs_diff_eq!(self.half_angle, segment.half_angle()); + assert_abs_diff_eq!(self.angle, segment.angle()); + assert_abs_diff_eq!(self.half_chord_length, segment.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, segment.chord_length(), epsilon = 0.00001); + 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()); + } + } + + #[test] + fn zero_angle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: 0.0, + angle: 0.0, + length: 0.0, + left_endpoint: Vec2::Y, + right_endpoint: Vec2::Y, + endpoints: [Vec2::Y, Vec2::Y], + midpoint: Vec2::Y, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::Y, + apothem: 1.0, + 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)); + tests.check_sector(CircularSector::new(1.0, 0.0)); + tests.check_segment(CircularSegment::new(1.0, 0.0)); + } + + #[test] + fn zero_radius() { + let tests = ArcTestCase { + radius: 0.0, + half_angle: FRAC_PI_4, + angle: FRAC_PI_2, + length: 0.0, + left_endpoint: Vec2::ZERO, + right_endpoint: Vec2::ZERO, + endpoints: [Vec2::ZERO, Vec2::ZERO], + midpoint: Vec2::ZERO, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::ZERO, + apothem: 0.0, + 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)); + tests.check_sector(CircularSector::new(0.0, FRAC_PI_4)); + tests.check_segment(CircularSegment::new(0.0, FRAC_PI_4)); + } + + #[test] + fn quarter_circle() { + let sqrt_half: f32 = ops::sqrt(0.5); + let tests = ArcTestCase { + radius: 1.0, + half_angle: FRAC_PI_4, + angle: FRAC_PI_2, + length: FRAC_PI_2, + left_endpoint: Vec2::new(-sqrt_half, sqrt_half), + right_endpoint: Vec2::splat(sqrt_half), + endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], + midpoint: Vec2::Y, + half_chord_length: sqrt_half, + chord_length: ops::sqrt(2.0), + chord_midpoint: Vec2::new(0.0, sqrt_half), + apothem: sqrt_half, + 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)); + tests.check_sector(CircularSector::from_turns(1.0, 0.25)); + tests.check_segment(CircularSegment::from_turns(1.0, 0.25)); + } + + #[test] + fn half_circle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: FRAC_PI_2, + angle: PI, + length: PI, + left_endpoint: Vec2::NEG_X, + right_endpoint: Vec2::X, + endpoints: [Vec2::NEG_X, Vec2::X], + midpoint: Vec2::Y, + half_chord_length: 1.0, + chord_length: 2.0, + chord_midpoint: Vec2::ZERO, + apothem: 0.0, + 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)); + tests.check_sector(CircularSector::from_radians(1.0, PI)); + tests.check_segment(CircularSegment::from_radians(1.0, PI)); + } + + #[test] + fn full_circle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: PI, + angle: 2.0 * PI, + length: 2.0 * PI, + left_endpoint: Vec2::NEG_Y, + right_endpoint: Vec2::NEG_Y, + endpoints: [Vec2::NEG_Y, Vec2::NEG_Y], + midpoint: Vec2::Y, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::NEG_Y, + apothem: -1.0, + 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)); + tests.check_sector(CircularSector::from_degrees(1.0, 360.0)); + tests.check_segment(CircularSegment::from_degrees(1.0, 360.0)); + } +} + +/// 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( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Ellipse { + /// Half of the width and height of the ellipse. + /// + /// This corresponds to the two perpendicular radii defining the ellipse. + pub half_size: Vec2, +} + +impl Primitive2d for Ellipse {} + +impl Default for Ellipse { + /// Returns the default [`Ellipse`] with a half-width of `1.0` and a half-height of `0.5`. + fn default() -> Self { + Self { + half_size: Vec2::new(1.0, 0.5), + } + } +} + +impl Ellipse { + /// Create a new `Ellipse` from half of its width and height. + /// + /// This corresponds to the two perpendicular radii defining the ellipse. + #[inline] + pub const fn new(half_width: f32, half_height: f32) -> Self { + Self { + half_size: Vec2::new(half_width, half_height), + } + } + + /// Create a new `Ellipse` from a given full size. + /// + /// `size.x` is the diameter along the X axis, and `size.y` is the diameter along the Y axis. + #[inline] + pub const fn from_size(size: Vec2) -> Self { + Self { + half_size: Vec2::new(size.x / 2.0, size.y / 2.0), + } + } + + #[inline] + /// Returns the [eccentricity](https://en.wikipedia.org/wiki/Eccentricity_(mathematics)) of the ellipse. + /// It can be thought of as a measure of how "stretched" or elongated the ellipse is. + /// + /// The value should be in the range [0, 1), where 0 represents a circle, and 1 represents a parabola. + pub fn eccentricity(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + ops::sqrt(a * a - b * b) / a + } + + #[inline] + /// Get the focal length of the ellipse. This corresponds to the distance between one of the foci and the center of the ellipse. + /// + /// The focal length of an ellipse is related to its eccentricity by `eccentricity = focal_length / semi_major` + pub fn focal_length(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + ops::sqrt(a * a - b * b) + } + + /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. + #[inline] + pub fn semi_major(&self) -> f32 { + self.half_size.max_element() + } + + /// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse. + #[inline] + pub fn semi_minor(&self) -> f32 { + self.half_size.min_element() + } +} + +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))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "Ring")] +pub struct Annulus { + /// The inner circle of the annulus + pub inner_circle: Circle, + /// The outer circle of the annulus + pub outer_circle: Circle, +} + +impl Primitive2d for Annulus {} + +impl Default for Annulus { + /// Returns the default [`Annulus`] with radii of `0.5` and `1.0`. + fn default() -> Self { + Self { + inner_circle: Circle::new(0.5), + outer_circle: Circle::new(1.0), + } + } +} + +impl Annulus { + /// Create a new [`Annulus`] from the radii of the inner and outer circle + #[inline] + pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { + Self { + inner_circle: Circle::new(inner_radius), + outer_circle: Circle::new(outer_radius), + } + } + + /// Get the diameter of the annulus + #[inline] + pub const fn diameter(&self) -> f32 { + self.outer_circle.diameter() + } + + /// Get the thickness of the annulus + #[inline] + pub const fn thickness(&self) -> f32 { + self.outer_circle.radius - self.inner_circle.radius + } + + /// Finds the point on the annulus that is closest to the given `point`: + /// + /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. + /// - If the point is inside of the inner circle (hole) of the annulus, the returned point will be on the inner perimeter. + /// - Otherwise, the returned point is overlapping the annulus and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + let distance_squared = point.length_squared(); + + if self.inner_circle.radius.squared() <= distance_squared { + if distance_squared <= self.outer_circle.radius.squared() { + // The point is inside the annulus. + point + } else { + // The point is outside the annulus and closer to the outer perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / ops::sqrt(distance_squared); + self.outer_circle.radius * dir_to_point + } + } else { + // The point is outside the annulus and closer to the inner perimeter. + // Find the closest point on the perimeter of the annulus. + let dir_to_point = point / ops::sqrt(distance_squared); + self.inner_circle.radius * dir_to_point + } + } +} + +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. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "Diamond")] +pub struct Rhombus { + /// Size of the horizontal and vertical diagonals of the rhombus + pub half_diagonals: Vec2, +} + +impl Primitive2d for Rhombus {} + +impl Default for Rhombus { + /// Returns the default [`Rhombus`] with a half-horizontal and half-vertical diagonal of `0.5`. + fn default() -> Self { + Self { + half_diagonals: Vec2::splat(0.5), + } + } +} + +impl Rhombus { + /// Create a new `Rhombus` from a vertical and horizontal diagonal sizes. + #[inline] + pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { + Self { + half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), + } + } + + /// Create a new `Rhombus` from a side length with all inner angles equal. + #[inline] + pub const fn from_side(side: f32) -> Self { + Self { + half_diagonals: Vec2::splat(side * FRAC_1_SQRT_2), + } + } + + /// Create a new `Rhombus` from a given inradius with all inner angles equal. + #[inline] + pub const fn from_inradius(inradius: f32) -> Self { + let half_diagonal = inradius * 2.0 / core::f32::consts::SQRT_2; + Self { + half_diagonals: Vec2::new(half_diagonal, half_diagonal), + } + } + + /// Get the length of each side of the rhombus + #[inline] + pub fn side(&self) -> f32 { + self.half_diagonals.length() + } + + /// Get the radius of the circumcircle on which all vertices + /// of the rhombus lie + #[inline] + pub const fn circumradius(&self) -> f32 { + self.half_diagonals.x.max(self.half_diagonals.y) + } + + /// Get the radius of the largest circle that can + /// be drawn within the rhombus + #[inline] + #[doc(alias = "apothem")] + pub fn inradius(&self) -> f32 { + let side = self.side(); + if side == 0.0 { + 0.0 + } else { + (self.half_diagonals.x * self.half_diagonals.y) / side + } + } + + /// Finds the point on the rhombus that is closest to the given `point`. + /// + /// If the point is outside the rhombus, the returned point will be on the perimeter of the rhombus. + /// Otherwise, it will be inside the rhombus and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + // Fold the problem into the positive quadrant + let point_abs = point.abs(); + let half_diagonals = self.half_diagonals.abs(); // to ensure correct sign + + // The unnormalised normal vector perpendicular to the side of the rhombus + let normal = Vec2::new(half_diagonals.y, half_diagonals.x); + let normal_magnitude_squared = normal.length_squared(); + if normal_magnitude_squared == 0.0 { + return Vec2::ZERO; // A null Rhombus has only one point anyway. + } + + // The last term corresponds to normal.dot(rhombus_vertex) + let distance_unnormalised = normal.dot(point_abs) - half_diagonals.x * half_diagonals.y; + + // The point is already inside so we simply return it. + if distance_unnormalised <= 0.0 { + return point; + } + + // Clamp the point to the edge + let mut result = point_abs - normal * distance_unnormalised / normal_magnitude_squared; + + // Clamp the point back to the positive quadrant + // if it's outside, it needs to be clamped to either vertex + if result.x <= 0.0 { + result = Vec2::new(0.0, half_diagonals.y); + } else if result.y <= 0.0 { + result = Vec2::new(half_diagonals.x, 0.0); + } + + // Finally, we restore the signs of the original vector + result.copysign(point) + } +} + +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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Plane2d { + /// The normal of the plane. The plane will be placed perpendicular to this direction + pub normal: Dir2, +} + +impl Primitive2d for Plane2d {} + +impl Default for Plane2d { + /// Returns the default [`Plane2d`] with a normal pointing in the `+Y` direction. + fn default() -> Self { + Self { normal: Dir2::Y } + } +} + +impl Plane2d { + /// Create a new `Plane2d` from a normal + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(normal: Vec2) -> Self { + Self { + normal: Dir2::new(normal).expect("normal must be nonzero and finite"), + } + } +} + +/// An infinite line going through the origin along a direction in 2D space. +/// +/// For a finite line: [`Segment2d`] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Line2d { + /// The direction of the line. The line extends infinitely in both the given direction + /// and its opposite direction + pub direction: Dir2, +} + +impl Primitive2d for Line2d {} + +/// A line segment defined by two endpoints in 2D space. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "LineSegment2d")] +pub struct Segment2d { + /// The endpoints of the line segment. + pub vertices: [Vec2; 2], +} + +impl Primitive2d for Segment2d {} + +impl Default for Segment2d { + fn default() -> Self { + Self { + vertices: [Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)], + } + } +} + +impl Segment2d { + /// Create a new `Segment2d` from its endpoints. + #[inline] + pub const fn new(point1: Vec2, point2: Vec2) -> Self { + Self { + vertices: [point1, point2], + } + } + + /// Create a new `Segment2d` centered at the origin with the given direction and length. + /// + /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. + #[inline] + pub fn from_direction_and_length(direction: Dir2, length: f32) -> Self { + let endpoint = 0.5 * length * direction; + Self { + vertices: [-endpoint, endpoint], + } + } + + /// Create a new `Segment2d` centered at the origin from a vector representing + /// the direction and length of the line segment. + /// + /// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`. + #[inline] + pub fn from_scaled_direction(scaled_direction: Vec2) -> Self { + let endpoint = 0.5 * scaled_direction; + Self { + vertices: [-endpoint, endpoint], + } + } + + /// Create a new `Segment2d` starting from the origin of the given `ray`, + /// going in the direction of the ray for the given `length`. + /// + /// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`. + #[inline] + pub fn from_ray_and_length(ray: Ray2d, length: f32) -> Self { + Self { + vertices: [ray.origin, ray.get_point(length)], + } + } + + /// Get the position of the first endpoint of the line segment. + #[inline] + pub const fn point1(&self) -> Vec2 { + self.vertices[0] + } + + /// Get the position of the second endpoint of the line segment. + #[inline] + pub const fn point2(&self) -> Vec2 { + self.vertices[1] + } + + /// Compute the midpoint between the two endpoints of the line segment. + #[inline] + #[doc(alias = "midpoint")] + pub fn center(&self) -> Vec2 { + self.point1().midpoint(self.point2()) + } + + /// Compute the length of the line segment. + #[inline] + pub fn length(&self) -> f32 { + self.point1().distance(self.point2()) + } + + /// Compute the squared length of the line segment. + #[inline] + pub fn length_squared(&self) -> f32 { + self.point1().distance_squared(self.point2()) + } + + /// Compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// For the non-panicking version, see [`Segment2d::try_direction`]. + /// + /// # Panics + /// + /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn direction(&self) -> Dir2 { + self.try_direction().unwrap_or_else(|err| { + panic!("Failed to compute the direction of a line segment: {err}") + }) + } + + /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed, + /// for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn try_direction(&self) -> Result { + Dir2::new(self.scaled_direction()) + } + + /// Compute the vector from the first endpoint to the second endpoint. + #[inline] + pub fn scaled_direction(&self) -> Vec2 { + self.point2() - self.point1() + } + + /// Compute the normalized counterclockwise normal on the left-hand side of the line segment. + /// + /// For the non-panicking version, see [`Segment2d::try_left_normal`]. + /// + /// # Panics + /// + /// Panics if a valid normal could not be computed, for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn left_normal(&self) -> Dir2 { + self.try_left_normal().unwrap_or_else(|err| { + panic!("Failed to compute the left-hand side normal of a line segment: {err}") + }) + } + + /// Try to compute the normalized counterclockwise normal on the left-hand side of the line segment. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid normal could not be computed, + /// for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn try_left_normal(&self) -> Result { + Dir2::new(self.scaled_left_normal()) + } + + /// Compute the non-normalized counterclockwise normal on the left-hand side of the line segment. + /// + /// The length of the normal is the distance between the endpoints. + #[inline] + pub fn scaled_left_normal(&self) -> Vec2 { + let scaled_direction = self.scaled_direction(); + Vec2::new(-scaled_direction.y, scaled_direction.x) + } + + /// Compute the normalized clockwise normal on the right-hand side of the line segment. + /// + /// For the non-panicking version, see [`Segment2d::try_right_normal`]. + /// + /// # Panics + /// + /// Panics if a valid normal could not be computed, for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn right_normal(&self) -> Dir2 { + self.try_right_normal().unwrap_or_else(|err| { + panic!("Failed to compute the right-hand side normal of a line segment: {err}") + }) + } + + /// Try to compute the normalized clockwise normal on the right-hand side of the line segment. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid normal could not be computed, + /// for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn try_right_normal(&self) -> Result { + Dir2::new(self.scaled_right_normal()) + } + + /// Compute the non-normalized clockwise normal on the right-hand side of the line segment. + /// + /// The length of the normal is the distance between the endpoints. + #[inline] + pub fn scaled_right_normal(&self) -> Vec2 { + let scaled_direction = self.scaled_direction(); + Vec2::new(scaled_direction.y, -scaled_direction.x) + } + + /// Compute the segment transformed by the given [`Isometry2d`]. + #[inline] + pub fn transformed(&self, isometry: impl Into) -> Self { + let isometry: Isometry2d = isometry.into(); + Self::new( + isometry.transform_point(self.point1()), + isometry.transform_point(self.point2()), + ) + } + + /// Compute the segment translated by the given vector. + #[inline] + pub fn translated(&self, translation: Vec2) -> Segment2d { + Self::new(self.point1() + translation, self.point2() + translation) + } + + /// Compute the segment rotated around the origin by the given rotation. + #[inline] + pub fn rotated(&self, rotation: Rot2) -> Segment2d { + Segment2d::new(rotation * self.point1(), rotation * self.point2()) + } + + /// Compute the segment rotated around the given point by the given rotation. + #[inline] + pub fn rotated_around(&self, rotation: Rot2, point: Vec2) -> Segment2d { + // We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back + let offset = self.translated(-point); + let rotated = offset.rotated(rotation); + rotated.translated(point) + } + + /// Compute the segment rotated around its own center. + #[inline] + pub fn rotated_around_center(&self, rotation: Rot2) -> Segment2d { + self.rotated_around(rotation, self.center()) + } + + /// Compute the segment with its center at the origin, keeping the same direction and length. + #[inline] + pub fn centered(&self) -> Segment2d { + let center = self.center(); + self.translated(-center) + } + + /// Compute the segment with a new length, keeping the same direction and center. + #[inline] + pub fn resized(&self, length: f32) -> Segment2d { + let offset_from_origin = self.center(); + let centered = self.translated(-offset_from_origin); + let ratio = length / self.length(); + let segment = Segment2d::new(centered.point1() * ratio, centered.point2() * ratio); + segment.translated(offset_from_origin) + } + + /// Reverses the direction of the line segment by swapping the endpoints. + #[inline] + pub const fn reverse(&mut self) { + let [point1, point2] = &mut self.vertices; + core::mem::swap(point1, point2); + } + + /// Returns the line segment with its direction reversed by swapping the endpoints. + #[inline] + #[must_use] + pub fn reversed(mut self) -> Self { + self.reverse(); + self + } + + /// Returns the point on the [`Segment2d`] that is closest to the specified `point`. + #[inline] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + // `point` + // x + // ^| + // / | + //`offset`/ | + // / | `segment_vector` + // x----.-------------->x + // 0 t 1 + let segment_vector = self.vertices[1] - self.vertices[0]; + let offset = point - self.vertices[0]; + // The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment. + let projection_scaled = segment_vector.dot(offset); + + // `point` is too far "left" in the picture + if projection_scaled <= 0.0 { + return self.vertices[0]; + } + + let length_squared = segment_vector.length_squared(); + // `point` is too far "right" in the picture + if projection_scaled >= length_squared { + return self.vertices[1]; + } + + // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. + let t = projection_scaled / length_squared; + self.vertices[0] + t * segment_vector + } +} + +impl From<[Vec2; 2]> for Segment2d { + #[inline] + fn from(vertices: [Vec2; 2]) -> Self { + Self { vertices } + } +} + +impl From<(Vec2, Vec2)> for Segment2d { + #[inline] + fn from((point1, point2): (Vec2, Vec2)) -> Self { + Self::new(point1, point2) + } +} + +/// A series of connected line segments in 2D space. +#[cfg(feature = "alloc")] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Polyline2d { + /// The vertices of the polyline + pub vertices: Vec, +} + +#[cfg(feature = "alloc")] +impl Primitive2d for Polyline2d {} + +#[cfg(feature = "alloc")] +impl FromIterator for Polyline2d { + fn from_iter>(iter: I) -> Self { + Self { + vertices: iter.into_iter().collect(), + } + } +} + +#[cfg(feature = "alloc")] +impl Default for Polyline2d { + fn default() -> Self { + Self { + vertices: Vec::from([Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)]), + } + } +} + +#[cfg(feature = "alloc")] +impl Polyline2d { + /// Create a new `Polyline2d` from its vertices + pub fn new(vertices: impl IntoIterator) -> Self { + Self::from_iter(vertices) + } + + /// Create a new `Polyline2d` from two endpoints with subdivision points. + /// `subdivisions = 0` creates a simple line with just start and end points. + /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. + pub fn with_subdivisions(start: Vec2, end: Vec2, subdivisions: usize) -> Self { + let total_vertices = subdivisions + 2; + let mut vertices = Vec::with_capacity(total_vertices); + + let step = (end - start) / (subdivisions + 1) as f32; + for i in 0..total_vertices { + vertices.push(start + step * i as f32); + } + + Self { vertices } + } +} + +/// A triangle in 2D space +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Triangle2d { + /// The vertices of the triangle + pub vertices: [Vec2; 3], +} + +impl Primitive2d for Triangle2d {} + +impl Default for Triangle2d { + /// Returns the default [`Triangle2d`] with the vertices `[0.0, 0.5]`, `[-0.5, -0.5]`, and `[0.5, -0.5]`. + fn default() -> Self { + Self { + vertices: [Vec2::Y * 0.5, Vec2::new(-0.5, -0.5), Vec2::new(0.5, -0.5)], + } + } +} + +impl Triangle2d { + /// Create a new `Triangle2d` from points `a`, `b`, and `c` + #[inline] + pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { + Self { + vertices: [a, b, c], + } + } + + /// Get the [`WindingOrder`] of the triangle + #[inline] + #[doc(alias = "orientation")] + pub fn winding_order(&self) -> WindingOrder { + let [a, b, c] = self.vertices; + let area = (b - a).perp_dot(c - a); + if area > f32::EPSILON { + WindingOrder::CounterClockwise + } else if area < -f32::EPSILON { + WindingOrder::Clockwise + } else { + WindingOrder::Invalid + } + } + + /// Compute the circle passing through all three vertices of the triangle. + /// The vector in the returned tuple is the circumcenter. + pub fn circumcircle(&self) -> (Circle, Vec2) { + // We treat the triangle as translated so that vertex A is at the origin. This simplifies calculations. + // + // A = (0, 0) + // * + // / \ + // / \ + // / \ + // / \ + // / U \ + // / \ + // *-------------* + // B C + + let a = self.vertices[0]; + let (b, c) = (self.vertices[1] - a, self.vertices[2] - a); + let b_length_sq = b.length_squared(); + let c_length_sq = c.length_squared(); + + // Reference: https://en.wikipedia.org/wiki/Circumcircle#Cartesian_coordinates_2 + let inv_d = (2.0 * (b.x * c.y - b.y * c.x)).recip(); + let ux = inv_d * (c.y * b_length_sq - b.y * c_length_sq); + let uy = inv_d * (b.x * c_length_sq - c.x * b_length_sq); + let u = Vec2::new(ux, uy); + + // Compute true circumcenter and circumradius, adding the tip coordinate so that + // A is translated back to its actual coordinate. + let center = u + a; + let radius = u.length(); + + (Circle { radius }, center) + } + + /// Checks if the triangle is degenerate, meaning it has zero area. + /// + /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. + /// This indicates that the three vertices are collinear or nearly collinear. + #[inline] + pub fn is_degenerate(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = (b - a).extend(0.); + let ac = (c - a).extend(0.); + ab.cross(ac).length() < 10e-7 + } + + /// Checks if the triangle is acute, meaning all angles are less than 90 degrees + #[inline] + pub fn is_acute(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 < c^2 for an acute triangle + let side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; + let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); + sum - max > max + } + + /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees + #[inline] + pub fn is_obtuse(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 > c^2 for an obtuse triangle + let side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; + let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); + sum - max < max + } + + /// Reverse the [`WindingOrder`] of the triangle + /// by swapping the first and last vertices. + #[inline] + pub const fn reverse(&mut self) { + self.vertices.swap(0, 2); + } + + /// This triangle but reversed. + #[inline] + #[must_use] + pub fn reversed(mut self) -> Self { + self.reverse(); + self + } +} + +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))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "Quad")] +pub struct Rectangle { + /// Half of the width and height of the rectangle + pub half_size: Vec2, +} + +impl Primitive2d for Rectangle {} + +impl Default for Rectangle { + /// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`. + fn default() -> Self { + Self { + half_size: Vec2::splat(0.5), + } + } +} + +impl Rectangle { + /// Create a new `Rectangle` from a full width and height + #[inline] + pub const fn new(width: f32, height: f32) -> Self { + Self::from_size(Vec2::new(width, height)) + } + + /// Create a new `Rectangle` from a given full size + #[inline] + pub const fn from_size(size: Vec2) -> Self { + Self { + half_size: Vec2::new(size.x / 2.0, size.y / 2.0), + } + } + + /// Create a new `Rectangle` from two corner points + #[inline] + pub fn from_corners(point1: Vec2, point2: Vec2) -> Self { + Self { + half_size: (point2 - point1).abs() / 2.0, + } + } + + /// Create a `Rectangle` from a single length. + /// The resulting `Rectangle` will be the same size in every direction. + #[inline] + pub const fn from_length(length: f32) -> Self { + Self { + half_size: Vec2::splat(length / 2.0), + } + } + + /// Get the size of the rectangle + #[inline] + pub fn size(&self) -> Vec2 { + 2.0 * self.half_size + } + + /// Finds the point on the rectangle that is closest to the given `point`. + /// + /// If the point is outside the rectangle, the returned point will be on the perimeter of the rectangle. + /// Otherwise, it will be inside the rectangle and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec2) -> Vec2 { + // Clamp point coordinates to the rectangle + point.clamp(-self.half_size, self.half_size) + } +} + +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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Polygon { + /// The vertices of the `Polygon` + pub vertices: Vec, +} + +#[cfg(feature = "alloc")] +impl Primitive2d for Polygon {} + +#[cfg(feature = "alloc")] +impl FromIterator for Polygon { + fn from_iter>(iter: I) -> Self { + Self { + vertices: iter.into_iter().collect(), + } + } +} + +#[cfg(feature = "alloc")] +impl Polygon { + /// Create a new `Polygon` from its vertices + pub fn new(vertices: impl IntoIterator) -> Self { + Self::from_iter(vertices) + } + + /// Tests if the polygon is simple. + /// + /// A polygon is simple if it is not self intersecting and not self tangent. + /// As such, no two edges of the polygon may cross each other and each vertex must not lie on another edge. + #[cfg(feature = "alloc")] + pub fn is_simple(&self) -> bool { + is_polygon_simple(&self.vertices) + } +} + +#[cfg(feature = "alloc")] +impl From for Polygon { + fn from(val: ConvexPolygon) -> Self { + Polygon { + vertices: val.vertices, + } + } +} + +/// A convex polygon with `N` vertices. +#[cfg(feature = "alloc")] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct ConvexPolygon { + /// The vertices of the [`ConvexPolygon`]. + vertices: Vec, +} + +#[cfg(feature = "alloc")] +impl Primitive2d for ConvexPolygon {} + +/// An error that happens when creating a [`ConvexPolygon`]. +#[cfg(feature = "alloc")] +#[derive(Error, Debug, Clone)] +pub enum ConvexPolygonError { + /// The created polygon is not convex. + #[error("The created polygon is not convex")] + Concave, +} + +#[cfg(feature = "alloc")] +impl ConvexPolygon { + fn triangle_winding_order( + &self, + a_index: usize, + b_index: usize, + c_index: usize, + ) -> WindingOrder { + let a = self.vertices[a_index]; + let b = self.vertices[b_index]; + let c = self.vertices[c_index]; + Triangle2d::new(a, b, c).winding_order() + } + + /// Create a [`ConvexPolygon`] from its `vertices`. + /// + /// # Errors + /// + /// Returns [`ConvexPolygonError::Concave`] if the `vertices` do not form a convex polygon. + pub fn new(vertices: impl IntoIterator) -> Result { + let polygon = Self::new_unchecked(vertices); + let len = polygon.vertices.len(); + let ref_winding_order = polygon.triangle_winding_order(len - 1, 0, 1); + for i in 1..len { + let winding_order = polygon.triangle_winding_order(i - 1, i, (i + 1) % len); + if winding_order != ref_winding_order { + return Err(ConvexPolygonError::Concave); + } + } + Ok(polygon) + } + + /// Create a [`ConvexPolygon`] from its `vertices`, without checks. + /// Use this version only if you know that the `vertices` make up a convex polygon. + #[inline] + pub fn new_unchecked(vertices: impl IntoIterator) -> Self { + Self { + vertices: vertices.into_iter().collect(), + } + } + + /// Get the vertices of this polygon + #[inline] + pub fn vertices(&self) -> &[Vec2] { + &self.vertices + } +} + +#[cfg(feature = "alloc")] +impl TryFrom for ConvexPolygon { + type Error = ConvexPolygonError; + + fn try_from(val: Polygon) -> Result { + ConvexPolygon::new(val.vertices) + } +} + +/// A polygon centered on the origin where all vertices lie on a circle, equally far apart. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct RegularPolygon { + /// The circumcircle on which all vertices lie + pub circumcircle: Circle, + /// The number of sides + pub sides: u32, +} + +impl Primitive2d for RegularPolygon {} + +impl Default for RegularPolygon { + /// Returns the default [`RegularPolygon`] with six sides (a hexagon) and a circumradius of `0.5`. + fn default() -> Self { + Self { + circumcircle: Circle { radius: 0.5 }, + sides: 6, + } + } +} + +impl RegularPolygon { + /// Create a new `RegularPolygon` + /// from the radius of the circumcircle and a number of sides + /// + /// # Panics + /// + /// Panics if `circumradius` is negative + #[inline] + pub const fn new(circumradius: f32, sides: u32) -> Self { + assert!( + circumradius.is_sign_positive(), + "polygon has a negative radius" + ); + assert!(sides > 2, "polygon has less than 3 sides"); + + Self { + circumcircle: Circle { + radius: circumradius, + }, + sides, + } + } + + /// Get the radius of the circumcircle on which all vertices + /// of the regular polygon lie + #[inline] + pub const fn circumradius(&self) -> f32 { + self.circumcircle.radius + } + + /// Get the inradius or apothem of the regular polygon. + /// This is the radius of the largest circle that can + /// be drawn within the polygon + #[inline] + #[doc(alias = "apothem")] + pub fn inradius(&self) -> f32 { + self.circumradius() * ops::cos(PI / self.sides as f32) + } + + /// Get the length of one side of the regular polygon + #[inline] + pub fn side_length(&self) -> f32 { + 2.0 * self.circumradius() * ops::sin(PI / self.sides as f32) + } + + /// Get the internal angle of the regular polygon in degrees. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the interior of the polygon + #[inline] + pub const fn internal_angle_degrees(&self) -> f32 { + (self.sides - 2) as f32 / self.sides as f32 * 180.0 + } + + /// Get the internal angle of the regular polygon in radians. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the interior of the polygon + #[inline] + pub const fn internal_angle_radians(&self) -> f32 { + (self.sides - 2) as f32 * PI / self.sides as f32 + } + + /// Get the external angle of the regular polygon in degrees. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the exterior of the polygon + #[inline] + pub const fn external_angle_degrees(&self) -> f32 { + 360.0 / self.sides as f32 + } + + /// Get the external angle of the regular polygon in radians. + /// + /// This is the angle formed by two adjacent sides with points + /// within the angle being in the exterior of the polygon + #[inline] + pub const fn external_angle_radians(&self) -> f32 { + 2.0 * PI / self.sides as f32 + } + + /// Returns an iterator over the vertices of the regular polygon, + /// rotated counterclockwise by the given angle in radians. + /// + /// With a rotation of 0, a vertex will be placed at the top `(0.0, circumradius)`. + pub fn vertices(self, rotation: f32) -> impl IntoIterator { + // Add pi/2 so that the polygon has a vertex at the top (sin is 1.0 and cos is 0.0) + let start_angle = rotation + FRAC_PI_2; + let step = core::f32::consts::TAU / self.sides as f32; + + (0..self.sides).map(move |i| { + let theta = start_angle + i as f32 * step; + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(cos, sin) * self.circumcircle.radius + }) + } +} + +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 +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "stadium", alias = "pill")] +pub struct Capsule2d { + /// The radius of the capsule + pub radius: f32, + /// Half the height of the capsule, excluding the semicircles + pub half_length: f32, +} + +impl Primitive2d for Capsule2d {} + +impl Default for Capsule2d { + /// Returns the default [`Capsule2d`] with a radius of `0.5` and a half-height of `0.5`, + /// excluding the semicircles. + fn default() -> Self { + Self { + radius: 0.5, + half_length: 0.5, + } + } +} + +impl Capsule2d { + /// Create a new `Capsule2d` from a radius and length + pub const fn new(radius: f32, length: f32) -> Self { + Self { + radius, + half_length: length / 2.0, + } + } + + /// Get the part connecting the semicircular ends of the capsule as a [`Rectangle`] + #[inline] + pub const fn to_inner_rectangle(&self) -> Rectangle { + Rectangle::new(self.radius * 2.0, self.half_length * 2.0) + } +} + +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 + + use super::*; + use approx::{assert_abs_diff_eq, assert_relative_eq}; + + #[test] + fn rectangle_closest_point() { + let rectangle = Rectangle::new(2.0, 2.0); + assert_eq!(rectangle.closest_point(Vec2::X * 10.0), Vec2::X); + assert_eq!(rectangle.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE); + assert_eq!( + rectangle.closest_point(Vec2::new(0.25, 0.1)), + Vec2::new(0.25, 0.1) + ); + } + + #[test] + fn circle_closest_point() { + let circle = Circle { radius: 1.0 }; + assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X); + assert_eq!( + circle.closest_point(Vec2::NEG_ONE * 10.0), + Vec2::NEG_ONE.normalize() + ); + assert_eq!( + circle.closest_point(Vec2::new(0.25, 0.1)), + Vec2::new(0.25, 0.1) + ); + } + + #[test] + fn annulus_closest_point() { + let annulus = Annulus::new(1.5, 2.0); + assert_eq!(annulus.closest_point(Vec2::X * 10.0), Vec2::X * 2.0); + assert_eq!( + annulus.closest_point(Vec2::NEG_ONE), + Vec2::NEG_ONE.normalize() * 1.5 + ); + assert_eq!( + annulus.closest_point(Vec2::new(1.55, 0.85)), + Vec2::new(1.55, 0.85) + ); + } + + #[test] + fn rhombus_closest_point() { + let rhombus = Rhombus::new(2.0, 1.0); + assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::X); + assert_eq!( + rhombus.closest_point(Vec2::NEG_ONE * 0.2), + Vec2::NEG_ONE * 0.2 + ); + assert_eq!( + rhombus.closest_point(Vec2::new(-0.55, 0.35)), + Vec2::new(-0.5, 0.25) + ); + + let rhombus = Rhombus::new(0.0, 0.0); + assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::ZERO); + assert_eq!(rhombus.closest_point(Vec2::NEG_ONE * 0.2), Vec2::ZERO); + assert_eq!(rhombus.closest_point(Vec2::new(-0.55, 0.35)), Vec2::ZERO); + } + + #[test] + fn segment_closest_point() { + assert_eq!( + Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(3.0, 0.0)) + .closest_point(Vec2::new(1.0, 6.0)), + Vec2::new(1.0, 0.0) + ); + + let segments = [ + Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0)), + Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)), + Segment2d::new(Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)), + Segment2d::new(Vec2::new(1.0, 0.0), Vec2::new(1.0, 5.0 * f32::EPSILON)), + ]; + let points = [ + Vec2::new(0.0, 0.0), + Vec2::new(1.0, 0.0), + Vec2::new(-1.0, 1.0), + Vec2::new(1.0, 1.0), + Vec2::new(-1.0, 0.0), + Vec2::new(5.0, -1.0), + Vec2::new(1.0, f32::EPSILON), + ]; + + for point in points.iter() { + for segment in segments.iter() { + let closest = segment.closest_point(*point); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.point1()), + "Closest point must always be at least as close as either vertex." + ); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.point2()), + "Closest point must always be at least as close as either vertex." + ); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.center()), + "Closest point must always be at least as close as the center." + ); + let closest_to_closest = segment.closest_point(closest); + // Closest point must already be on the segment + assert_relative_eq!(closest_to_closest, closest); + } + } + } + + #[test] + 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] + fn capsule_math() { + let capsule = Capsule2d::new(2.0, 9.0); + assert_eq!( + capsule.to_inner_rectangle(), + 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] + fn annulus_math() { + 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"); + let rhombus = Rhombus::from_side(core::f32::consts::SQRT_2); + assert_abs_diff_eq!(rhombus.half_diagonals, Vec2::new(1.0, 1.0)); + assert_abs_diff_eq!( + rhombus.half_diagonals, + Rhombus::from_inradius(FRAC_1_SQRT_2).half_diagonals + ); + } + + #[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"); + + let line = Ellipse::new(1., 0.); + assert_eq!(line.eccentricity(), 1., "incorrect line eccentricity"); + + let circle = Ellipse::new(2., 2.); + 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()); + + let acute_triangle = + Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 5.)); + let obtuse_triangle = + Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 0.5)); + + assert!(acute_triangle.is_acute()); + assert!(!acute_triangle.is_obtuse()); + assert!(!obtuse_triangle.is_acute()); + assert!(obtuse_triangle.is_obtuse()); + } + + #[test] + fn triangle_winding_order() { + let mut cw_triangle = Triangle2d::new( + Vec2::new(0.0, 2.0), + Vec2::new(-0.5, -1.2), + Vec2::new(-1.0, -1.0), + ); + assert_eq!(cw_triangle.winding_order(), WindingOrder::Clockwise); + + let ccw_triangle = Triangle2d::new( + Vec2::new(-1.0, -1.0), + Vec2::new(-0.5, -1.2), + Vec2::new(0.0, 2.0), + ); + assert_eq!(ccw_triangle.winding_order(), WindingOrder::CounterClockwise); + + // The clockwise triangle should be the same as the counterclockwise + // triangle when reversed + cw_triangle.reverse(); + assert_eq!(cw_triangle, ccw_triangle); + + let invalid_triangle = Triangle2d::new( + Vec2::new(0.0, 2.0), + Vec2::new(0.0, -1.0), + Vec2::new(0.0, -1.2), + ); + assert_eq!(invalid_triangle.winding_order(), WindingOrder::Invalid); + } + + #[test] + fn rectangle_math() { + let rectangle = Rectangle::new(3.0, 7.0); + assert_eq!( + 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] + fn regular_polygon_math() { + 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, + "incorrect internal angle" + ); + assert_eq!( + polygon.internal_angle_radians(), + 120_f32.to_radians(), + "incorrect internal angle" + ); + assert_eq!( + polygon.external_angle_degrees(), + 60.0, + "incorrect external angle" + ); + assert_eq!( + polygon.external_angle_radians(), + 60_f32.to_radians(), + "incorrect external angle" + ); + } + + #[test] + fn triangle_circumcenter() { + let triangle = Triangle2d::new( + Vec2::new(10.0, 2.0), + Vec2::new(-5.0, -3.0), + Vec2::new(2.0, -1.0), + ); + let (Circle { radius }, circumcenter) = triangle.circumcircle(); + + // Calculated with external calculator + assert_eq!(radius, 98.34887); + assert_eq!(circumcenter, Vec2::new(-28.5, 92.5)); + } + + #[test] + fn regular_polygon_vertices() { + let polygon = RegularPolygon::new(1.0, 4); + + // Regular polygons have a vertex at the top by default + let mut vertices = polygon.vertices(0.0).into_iter(); + assert!((vertices.next().unwrap() - Vec2::Y).length() < 1e-7); + + // Rotate by 45 degrees, forming an axis-aligned square + let mut rotated_vertices = polygon.vertices(core::f32::consts::FRAC_PI_4).into_iter(); + + // Distance from the origin to the middle of a side, derived using Pythagorean theorem + let side_distance = FRAC_1_SQRT_2; + assert!( + (rotated_vertices.next().unwrap() - Vec2::new(-side_distance, side_distance)).length() + < 1e-7, + ); + } +} diff --git a/crates/bevy_shapes/src/primitives/dim3.rs b/crates/bevy_shapes/src/primitives/dim3.rs new file mode 100644 index 0000000000000..2713da80a5784 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/dim3.rs @@ -0,0 +1,2063 @@ +use core::f32::consts::{FRAC_PI_3, PI}; + +use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; +use bevy_math::{ + ops::{self, FloatPow}, + Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3, +}; + +use bevy_math::Quat; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + +/// A sphere primitive, representing the set of all points some distance from the origin +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Sphere { + /// The radius of the sphere + pub radius: f32, +} + +impl Primitive3d for Sphere {} + +impl Default for Sphere { + /// Returns the default [`Sphere`] with a radius of `0.5`. + fn default() -> Self { + Self { radius: 0.5 } + } +} + +impl Sphere { + /// Create a new [`Sphere`] from a `radius` + #[inline] + pub const fn new(radius: f32) -> Self { + Self { radius } + } + + /// Get the diameter of the sphere + #[inline] + pub const fn diameter(&self) -> f32 { + 2.0 * self.radius + } + + /// Finds the point on the sphere that is closest to the given `point`. + /// + /// If the point is outside the sphere, the returned point will be on the surface of the sphere. + /// Otherwise, it will be inside the sphere and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec3) -> Vec3 { + let distance_squared = point.length_squared(); + + if distance_squared <= self.radius.squared() { + // The point is inside the sphere. + point + } else { + // The point is outside the sphere. + // Find the closest point on the surface of the sphere. + let dir_to_point = point / ops::sqrt(distance_squared); + self.radius * dir_to_point + } + } +} + +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))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Plane3d { + /// The normal of the plane. The plane will be placed perpendicular to this direction + pub normal: Dir3, + /// Half of the width and height of the plane + pub half_size: Vec2, +} + +impl Primitive3d for Plane3d {} + +impl Default for Plane3d { + /// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`. + fn default() -> Self { + Self { + normal: Dir3::Y, + half_size: Vec2::splat(0.5), + } + } +} + +impl Plane3d { + /// Create a new `Plane3d` from a normal and a half size + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(normal: Vec3, half_size: Vec2) -> Self { + Self { + normal: Dir3::new(normal).expect("normal must be nonzero and finite"), + half_size, + } + } + + /// Create a new `Plane3d` based on three points and compute the geometric center + /// of those points. + /// + /// The direction of the plane normal is determined by the winding order + /// of the triangular shape formed by the points. + /// + /// # Panics + /// + /// Panics if a valid normal can not be computed, for example when the points + /// are *collinear* and lie on the same line. + #[inline] + pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { + let normal = Dir3::new((b - a).cross(c - a)).expect( + "finite plane must be defined by three finite points that don't lie on the same line", + ); + let translation = (a + b + c) / 3.0; + + ( + Self { + normal, + ..Default::default() + }, + translation, + ) + } +} +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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct InfinitePlane3d { + /// The normal of the plane. The plane will be placed perpendicular to this direction + pub normal: Dir3, +} + +impl Primitive3d for InfinitePlane3d {} + +impl Default for InfinitePlane3d { + /// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction. + fn default() -> Self { + Self { normal: Dir3::Y } + } +} + +impl InfinitePlane3d { + /// Create a new `InfinitePlane3d` from a normal + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new>(normal: T) -> Self + where + >::Error: core::fmt::Debug, + { + Self { + normal: normal + .try_into() + .expect("normal must be nonzero and finite"), + } + } + + /// Create a new `InfinitePlane3d` based on three points and compute the geometric center + /// of those points. + /// + /// The direction of the plane normal is determined by the winding order + /// of the triangular shape formed by the points. + /// + /// # Panics + /// + /// Panics if a valid normal can not be computed, for example when the points + /// are *collinear* and lie on the same line. + #[inline] + pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { + let normal = Dir3::new((b - a).cross(c - a)).expect( + "infinite plane must be defined by three finite points that don't lie on the same line", + ); + let translation = (a + b + c) / 3.0; + + (Self { normal }, translation) + } + + /// Computes the shortest distance between a plane transformed with the given `isometry` and a + /// `point`. The result is a signed value; it's positive if the point lies in the half-space + /// that the plane's normal vector points towards. + #[inline] + pub fn signed_distance(&self, isometry: impl Into, point: Vec3) -> f32 { + let isometry = isometry.into(); + self.normal.dot(isometry.inverse() * point) + } + + /// Injects the `point` into this plane transformed with the given `isometry`. + /// + /// This projects the point orthogonally along the shortest path onto the plane. + #[inline] + pub fn project_point(&self, isometry: impl Into, point: Vec3) -> Vec3 { + point - self.normal * self.signed_distance(isometry, point) + } + + /// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given + /// `origin` to the XY-plane. + /// + /// ## Guarantees + /// + /// * the transformation is a [congruence] meaning it will preserve all distances and angles of + /// the transformed geometry + /// * uses the least rotation possible to transform the geometry + /// * if two geometries are transformed with the same isometry, then the relations between + /// them, like distances, are also preserved + /// * compared to projections, the transformation is lossless (up to floating point errors) + /// reversible + /// + /// ## Non-Guarantees + /// + /// * the rotation used is generally not unique + /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to + /// enforce some kind of alignment the user has to use an extra transformation ontop of this + /// one + /// + /// See [`isometries_xy`] for example usescases. + /// + /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) + /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` + #[inline] + pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d { + let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z); + let transformed_origin = rotation * origin; + Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation) + } + + /// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the + /// given `origin`. + /// + /// ## Guarantees + /// + /// * the transformation is a [congruence] meaning it will preserve all distances and angles of + /// the transformed geometry + /// * uses the least rotation possible to transform the geometry + /// * if two geometries are transformed with the same isometry, then the relations between + /// them, like distances, are also preserved + /// * compared to projections, the transformation is lossless (up to floating point errors) + /// reversible + /// + /// ## Non-Guarantees + /// + /// * the rotation used is generally not unique + /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to + /// enforce some kind of alignment the user has to use an extra transformation ontop of this + /// one + /// + /// See [`isometries_xy`] for example usescases. + /// + /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) + /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` + #[inline] + pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d { + self.isometry_into_xy(origin).inverse() + } + + /// Computes both [isometries] which transforms points from the plane in 3D space with the + /// given `origin` to the XY-plane and back. + /// + /// [isometries]: `Isometry3d` + /// + /// # Example + /// + /// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The + /// workflow would usually look like this: + /// + /// ``` + /// # use bevy_math::{Vec3, Dir3}; + /// # use bevy_math::primitives::InfinitePlane3d; + /// + /// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z]; + /// let center = (a + b + c) / 3.0; + /// + /// let plane = InfinitePlane3d::new(Vec3::ONE); + /// + /// let (to_xy, from_xy) = plane.isometries_xy(center); + /// + /// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate()); + /// + /// // apply some algorithm to `triangle_2d` + /// + /// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3); + /// ``` + #[inline] + pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) { + let projection = self.isometry_into_xy(origin); + (projection, projection.inverse()) + } +} + +/// An infinite line going through the origin along a direction in 3D space. +/// +/// For a finite line: [`Segment3d`] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Line3d { + /// The direction of the line + pub direction: Dir3, +} + +impl Primitive3d for Line3d {} + +/// A line segment defined by two endpoints in 3D space. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "LineSegment3d")] +pub struct Segment3d { + /// The endpoints of the line segment. + pub vertices: [Vec3; 2], +} + +impl Primitive3d for Segment3d {} + +impl Default for Segment3d { + fn default() -> Self { + Self { + vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)], + } + } +} + +impl Segment3d { + /// Create a new `Segment3d` from its endpoints. + #[inline] + pub const fn new(point1: Vec3, point2: Vec3) -> Self { + Self { + vertices: [point1, point2], + } + } + + /// Create a new `Segment3d` centered at the origin with the given direction and length. + /// + /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. + #[inline] + pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self { + let endpoint = 0.5 * length * direction; + Self { + vertices: [-endpoint, endpoint], + } + } + + /// Create a new `Segment3d` centered at the origin from a vector representing + /// the direction and length of the line segment. + /// + /// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`. + #[inline] + pub fn from_scaled_direction(scaled_direction: Vec3) -> Self { + let endpoint = 0.5 * scaled_direction; + Self { + vertices: [-endpoint, endpoint], + } + } + + /// Create a new `Segment3d` starting from the origin of the given `ray`, + /// going in the direction of the ray for the given `length`. + /// + /// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`. + #[inline] + pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self { + Self { + vertices: [ray.origin, ray.get_point(length)], + } + } + + /// Get the position of the first endpoint of the line segment. + #[inline] + pub const fn point1(&self) -> Vec3 { + self.vertices[0] + } + + /// Get the position of the second endpoint of the line segment. + #[inline] + pub const fn point2(&self) -> Vec3 { + self.vertices[1] + } + + /// Compute the midpoint between the two endpoints of the line segment. + #[inline] + #[doc(alias = "midpoint")] + pub fn center(&self) -> Vec3 { + self.point1().midpoint(self.point2()) + } + + /// Compute the length of the line segment. + #[inline] + pub fn length(&self) -> f32 { + self.point1().distance(self.point2()) + } + + /// Compute the squared length of the line segment. + #[inline] + pub fn length_squared(&self) -> f32 { + self.point1().distance_squared(self.point2()) + } + + /// Compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// For the non-panicking version, see [`Segment3d::try_direction`]. + /// + /// # Panics + /// + /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn direction(&self) -> Dir3 { + self.try_direction().unwrap_or_else(|err| { + panic!("Failed to compute the direction of a line segment: {err}") + }) + } + + /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed, + /// for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn try_direction(&self) -> Result { + Dir3::new(self.scaled_direction()) + } + + /// Compute the vector from the first endpoint to the second endpoint. + #[inline] + pub fn scaled_direction(&self) -> Vec3 { + self.point2() - self.point1() + } + + /// Compute the segment transformed by the given [`Isometry3d`]. + #[inline] + pub fn transformed(&self, isometry: impl Into) -> Self { + let isometry: Isometry3d = isometry.into(); + Self::new( + isometry.transform_point(self.point1()).into(), + isometry.transform_point(self.point2()).into(), + ) + } + + /// Compute the segment translated by the given vector. + #[inline] + pub fn translated(&self, translation: Vec3) -> Segment3d { + Self::new(self.point1() + translation, self.point2() + translation) + } + + /// Compute the segment rotated around the origin by the given rotation. + #[inline] + pub fn rotated(&self, rotation: Quat) -> Segment3d { + Segment3d::new(rotation * self.point1(), rotation * self.point2()) + } + + /// Compute the segment rotated around the given point by the given rotation. + #[inline] + pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d { + // We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back + let offset = self.translated(-point); + let rotated = offset.rotated(rotation); + rotated.translated(point) + } + + /// Compute the segment rotated around its own center. + #[inline] + pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d { + self.rotated_around(rotation, self.center()) + } + + /// Compute the segment with its center at the origin, keeping the same direction and length. + #[inline] + pub fn centered(&self) -> Segment3d { + let center = self.center(); + self.translated(-center) + } + + /// Compute the segment with a new length, keeping the same direction and center. + #[inline] + pub fn resized(&self, length: f32) -> Segment3d { + let offset_from_origin = self.center(); + let centered = self.translated(-offset_from_origin); + let ratio = length / self.length(); + let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio); + segment.translated(offset_from_origin) + } + + /// Reverses the direction of the line segment by swapping the endpoints. + #[inline] + pub const fn reverse(&mut self) { + let [point1, point2] = &mut self.vertices; + core::mem::swap(point1, point2); + } + + /// Returns the line segment with its direction reversed by swapping the endpoints. + #[inline] + #[must_use] + pub fn reversed(mut self) -> Self { + self.reverse(); + self + } + + /// Returns the point on the [`Segment3d`] that is closest to the specified `point`. + #[inline] + pub fn closest_point(&self, point: Vec3) -> Vec3 { + // `point` + // x + // ^| + // / | + //`offset`/ | + // / | `segment_vector` + // x----.-------------->x + // 0 t 1 + let segment_vector = self.vertices[1] - self.vertices[0]; + let offset = point - self.vertices[0]; + // The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment. + let projection_scaled = segment_vector.dot(offset); + + // `point` is too far "left" in the picture + if projection_scaled <= 0.0 { + return self.vertices[0]; + } + + let length_squared = segment_vector.length_squared(); + // `point` is too far "right" in the picture + if projection_scaled >= length_squared { + return self.vertices[1]; + } + + // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. + let t = projection_scaled / length_squared; + self.vertices[0] + t * segment_vector + } +} + +impl From<[Vec3; 2]> for Segment3d { + #[inline] + fn from(vertices: [Vec3; 2]) -> Self { + Self { vertices } + } +} + +impl From<(Vec3, Vec3)> for Segment3d { + #[inline] + fn from((point1, point2): (Vec3, Vec3)) -> Self { + Self::new(point1, point2) + } +} + +/// A series of connected line segments in 3D space. +#[cfg(feature = "alloc")] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Polyline3d { + /// The vertices of the polyline + pub vertices: Vec, +} + +#[cfg(feature = "alloc")] +impl Primitive3d for Polyline3d {} + +#[cfg(feature = "alloc")] +impl FromIterator for Polyline3d { + fn from_iter>(iter: I) -> Self { + Self { + vertices: iter.into_iter().collect(), + } + } +} + +#[cfg(feature = "alloc")] +impl Default for Polyline3d { + fn default() -> Self { + Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)]) + } +} + +#[cfg(feature = "alloc")] +impl Polyline3d { + /// Create a new `Polyline3d` from its vertices + pub fn new(vertices: impl IntoIterator) -> Self { + Self::from_iter(vertices) + } + + /// Create a new `Polyline3d` from two endpoints with subdivision points. + /// `subdivisions = 0` creates a simple line with just start and end points. + /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. + pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self { + let total_vertices = subdivisions + 2; + let mut vertices = Vec::with_capacity(total_vertices); + + let step = (end - start) / (subdivisions + 1) as f32; + for i in 0..total_vertices { + vertices.push(start + step * i as f32); + } + + Self { vertices } + } +} + +/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not +/// required to be the same. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Cuboid { + /// Half of the width, height and depth of the cuboid + pub half_size: Vec3, +} + +impl Primitive3d for Cuboid {} + +impl Default for Cuboid { + /// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`. + fn default() -> Self { + Self { + half_size: Vec3::splat(0.5), + } + } +} + +impl Cuboid { + /// Create a new `Cuboid` from a full x, y, and z length + #[inline] + pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self { + Self::from_size(Vec3::new(x_length, y_length, z_length)) + } + + /// Create a new `Cuboid` from a given full size + #[inline] + pub const fn from_size(size: Vec3) -> Self { + Self { + half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0), + } + } + + /// Create a new `Cuboid` from two corner points + #[inline] + pub fn from_corners(point1: Vec3, point2: Vec3) -> Self { + Self { + half_size: (point2 - point1).abs() / 2.0, + } + } + + /// Create a `Cuboid` from a single length. + /// The resulting `Cuboid` will be the same size in every direction. + #[inline] + pub const fn from_length(length: f32) -> Self { + Self { + half_size: Vec3::splat(length / 2.0), + } + } + + /// Get the size of the cuboid + #[inline] + pub fn size(&self) -> Vec3 { + 2.0 * self.half_size + } + + /// Finds the point on the cuboid that is closest to the given `point`. + /// + /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. + /// Otherwise, it will be inside the cuboid and returned as is. + #[inline] + pub fn closest_point(&self, point: Vec3) -> Vec3 { + // Clamp point coordinates to the cuboid + point.clamp(-self.half_size, self.half_size) + } +} + +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))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Cylinder { + /// The radius of the cylinder + pub radius: f32, + /// The half height of the cylinder + pub half_height: f32, +} + +impl Primitive3d for Cylinder {} + +impl Default for Cylinder { + /// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`. + fn default() -> Self { + Self { + radius: 0.5, + half_height: 0.5, + } + } +} + +impl Cylinder { + /// Create a new `Cylinder` from a radius and full height + #[inline] + pub const fn new(radius: f32, height: f32) -> Self { + Self { + radius, + half_height: height / 2.0, + } + } + + /// Get the base of the cylinder as a [`Circle`] + #[inline] + pub const fn base(&self) -> Circle { + Circle { + radius: self.radius, + } + } + + /// Get the surface area of the side of the cylinder, + /// also known as the lateral area + #[inline] + #[doc(alias = "side_area")] + pub const fn lateral_area(&self) -> f32 { + 4.0 * PI * self.radius * self.half_height + } + + /// Get the surface area of one base of the cylinder + #[inline] + pub fn base_area(&self) -> f32 { + PI * self.radius.squared() + } +} + +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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Capsule3d { + /// The radius of the capsule + pub radius: f32, + /// Half the height of the capsule, excluding the hemispheres + pub half_length: f32, +} + +impl Primitive3d for Capsule3d {} + +impl Default for Capsule3d { + /// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`. + /// The total height is `2.0`. + fn default() -> Self { + Self { + radius: 0.5, + half_length: 0.5, + } + } +} + +impl Capsule3d { + /// Create a new `Capsule3d` from a radius and length + pub const fn new(radius: f32, length: f32) -> Self { + Self { + radius, + half_length: length / 2.0, + } + } + + /// Get the part connecting the hemispherical ends + /// of the capsule as a [`Cylinder`] + #[inline] + pub const fn to_cylinder(&self) -> Cylinder { + Cylinder { + radius: self.radius, + half_height: self.half_length, + } + } +} + +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. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Cone { + /// The radius of the base + pub radius: f32, + /// The height of the cone + pub height: f32, +} + +impl Primitive3d for Cone {} + +impl Default for Cone { + /// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`. + fn default() -> Self { + Self { + radius: 0.5, + height: 1.0, + } + } +} + +impl Cone { + /// Create a new [`Cone`] from a radius and height. + pub const fn new(radius: f32, height: f32) -> Self { + Self { radius, height } + } + /// Get the base of the cone as a [`Circle`] + #[inline] + pub const fn base(&self) -> Circle { + Circle { + radius: self.radius, + } + } + + /// Get the slant height of the cone, the length of the line segment + /// connecting a point on the base to the apex + #[inline] + #[doc(alias = "side_length")] + pub fn slant_height(&self) -> f32 { + ops::hypot(self.radius, self.height) + } + + /// Get the surface area of the side of the cone, + /// also known as the lateral area + #[inline] + #[doc(alias = "side_area")] + pub fn lateral_area(&self) -> f32 { + PI * self.radius * self.slant_height() + } + + /// Get the surface area of the base of the cone + #[inline] + pub fn base_area(&self) -> f32 { + PI * self.radius.squared() + } +} + +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. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct ConicalFrustum { + /// The radius of the top of the frustum + pub radius_top: f32, + /// The radius of the base of the frustum + pub radius_bottom: f32, + /// The height of the frustum + pub height: f32, +} + +impl Primitive3d for ConicalFrustum {} + +impl Default for ConicalFrustum { + /// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`. + fn default() -> Self { + Self { + radius_top: 0.25, + radius_bottom: 0.5, + height: 0.5, + } + } +} + +impl ConicalFrustum { + /// Get the bottom base of the conical frustum as a [`Circle`] + #[inline] + pub const fn bottom_base(&self) -> Circle { + Circle { + radius: self.radius_bottom, + } + } + + /// Get the top base of the conical frustum as a [`Circle`] + #[inline] + pub const fn top_base(&self) -> Circle { + Circle { + radius: self.radius_top, + } + } + + /// Get the slant height of the conical frustum, the length of the line segment + /// connecting a point on the base to the closest point on the top + #[inline] + #[doc(alias = "side_length")] + pub fn slant_height(&self) -> f32 { + ops::hypot(self.radius_bottom - self.radius_top, self.height) + } + + /// Get the surface area of the side of the conical frustum, + /// also known as the lateral area + #[inline] + #[doc(alias = "side_area")] + pub fn lateral_area(&self) -> f32 { + PI * (self.radius_bottom + self.radius_top) * self.slant_height() + } + + /// Get the surface area of the bottom base of the conical frustum + #[inline] + pub fn bottom_base_area(&self) -> f32 { + PI * self.radius_bottom.squared() + } + + /// Get the surface area of the top base of the conical frustum + #[inline] + pub fn top_base_area(&self) -> f32 { + PI * self.radius_top.squared() + } +} + +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 { + /// A torus that has a ring. + /// The major radius is greater than the minor radius + Ring, + /// A torus that has no hole but also doesn't intersect itself. + /// The major radius is equal to the minor radius + Horn, + /// A self-intersecting torus. + /// The major radius is less than the minor radius + Spindle, + /// A torus with non-geometric properties like + /// a minor or major radius that is non-positive, + /// infinite, or `NaN` + Invalid, +} + +/// A torus primitive, often representing a ring or donut shape +/// The set of points some distance from a circle centered at the origin +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Torus { + /// The radius of the tube of the torus + #[doc( + alias = "ring_radius", + alias = "tube_radius", + alias = "cross_section_radius" + )] + pub minor_radius: f32, + /// The distance from the center of the torus to the center of the tube + #[doc(alias = "radius_of_revolution")] + pub major_radius: f32, +} + +impl Primitive3d for Torus {} + +impl Default for Torus { + /// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`. + fn default() -> Self { + Self { + minor_radius: 0.25, + major_radius: 0.75, + } + } +} + +impl Torus { + /// Create a new `Torus` from an inner and outer radius. + /// + /// The inner radius is the radius of the hole, and the outer radius + /// is the radius of the entire object + #[inline] + pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { + let minor_radius = (outer_radius - inner_radius) / 2.0; + let major_radius = outer_radius - minor_radius; + + Self { + minor_radius, + major_radius, + } + } + + /// Get the inner radius of the torus. + /// For a ring torus, this corresponds to the radius of the hole, + /// or `major_radius - minor_radius` + #[inline] + pub const fn inner_radius(&self) -> f32 { + self.major_radius - self.minor_radius + } + + /// Get the outer radius of the torus. + /// This corresponds to the overall radius of the entire object, + /// or `major_radius + minor_radius` + #[inline] + pub const fn outer_radius(&self) -> f32 { + self.major_radius + self.minor_radius + } + + /// Get the [`TorusKind`] determined by the minor and major radii. + /// + /// The torus can either be a *ring torus* that has a hole, + /// a *horn torus* that doesn't have a hole but also isn't self-intersecting, + /// or a *spindle torus* that is self-intersecting. + /// + /// If the minor or major radius is non-positive, infinite, or `NaN`, + /// [`TorusKind::Invalid`] is returned + #[inline] + pub fn kind(&self) -> TorusKind { + // Invalid if minor or major radius is non-positive, infinite, or NaN + if self.minor_radius <= 0.0 + || !self.minor_radius.is_finite() + || self.major_radius <= 0.0 + || !self.major_radius.is_finite() + { + return TorusKind::Invalid; + } + + match self.major_radius.partial_cmp(&self.minor_radius).unwrap() { + core::cmp::Ordering::Greater => TorusKind::Ring, + core::cmp::Ordering::Equal => TorusKind::Horn, + core::cmp::Ordering::Less => TorusKind::Spindle, + } + } +} + +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))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Triangle3d { + /// The vertices of the triangle. + pub vertices: [Vec3; 3], +} + +impl Primitive3d for Triangle3d {} + +impl Default for Triangle3d { + /// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`. + fn default() -> Self { + Self { + vertices: [ + Vec3::new(0.0, 0.5, 0.0), + Vec3::new(-0.5, -0.5, 0.0), + Vec3::new(0.5, -0.5, 0.0), + ], + } + } +} + +impl Triangle3d { + /// Create a new [`Triangle3d`] from points `a`, `b`, and `c`. + #[inline] + pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self { + Self { + vertices: [a, b, c], + } + } + + /// Get the normal of the triangle in the direction of the right-hand rule, assuming + /// the vertices are ordered in a counter-clockwise direction. + /// + /// The normal is computed as the cross product of the vectors `ab` and `ac`. + /// + /// # Errors + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + #[inline] + pub fn normal(&self) -> Result { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + Dir3::new(ab.cross(ac)) + } + + /// Checks if the triangle is degenerate, meaning it has zero area. + /// + /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. + /// This indicates that the three vertices are collinear or nearly collinear. + #[inline] + pub fn is_degenerate(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() < 10e-7 + } + + /// Checks if the triangle is acute, meaning all angles are less than 90 degrees + #[inline] + pub fn is_acute(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 < c^2 for an acute triangle + let side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; + let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); + sum - max > max + } + + /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees + #[inline] + pub fn is_obtuse(&self) -> bool { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + // a^2 + b^2 > c^2 for an obtuse triangle + let side_lengths = [ + ab.length_squared(), + bc.length_squared(), + ca.length_squared(), + ]; + let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; + let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); + sum - max < max + } + + /// Reverse the triangle by swapping the first and last vertices. + #[inline] + pub const fn reverse(&mut self) { + self.vertices.swap(0, 2); + } + + /// This triangle but reversed. + #[inline] + #[must_use] + pub const fn reversed(mut self) -> Triangle3d { + self.reverse(); + self + } + + /// Get the centroid of the triangle. + /// + /// This function finds the geometric center of the triangle by averaging the vertices: + /// `centroid = (a + b + c) / 3`. + #[doc(alias("center", "barycenter", "baricenter"))] + #[inline] + pub fn centroid(&self) -> Vec3 { + (self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0 + } + + /// Get the largest side of the triangle. + /// + /// Returns the two points that form the largest side of the triangle. + #[inline] + pub fn largest_side(&self) -> (Vec3, Vec3) { + let [a, b, c] = self.vertices; + let ab = b - a; + let bc = c - b; + let ca = a - c; + + let mut largest_side_points = (a, b); + let mut largest_side_length = ab.length_squared(); + + let bc_length = bc.length_squared(); + if bc_length > largest_side_length { + largest_side_points = (b, c); + largest_side_length = bc_length; + } + + let ca_length = ca.length_squared(); + if ca_length > largest_side_length { + largest_side_points = (a, c); + } + + largest_side_points + } + + /// Get the circumcenter of the triangle. + #[inline] + pub fn circumcenter(&self) -> Vec3 { + if self.is_degenerate() { + // If the triangle is degenerate, the circumcenter is the midpoint of the largest side. + let (p1, p2) = self.largest_side(); + return (p1 + p2) / 2.0; + } + + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + let n = ab.cross(ac); + + // Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d + a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac)) + / (2.0 * n.length_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) + } +} + +/// A tetrahedron primitive. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Tetrahedron { + /// The vertices of the tetrahedron. + pub vertices: [Vec3; 4], +} + +impl Primitive3d for Tetrahedron {} + +impl Default for Tetrahedron { + /// Returns the default [`Tetrahedron`] with the vertices + /// `[0.5, 0.5, 0.5]`, `[-0.5, 0.5, -0.5]`, `[-0.5, -0.5, 0.5]` and `[0.5, -0.5, -0.5]`. + fn default() -> Self { + Self { + vertices: [ + Vec3::new(0.5, 0.5, 0.5), + Vec3::new(-0.5, 0.5, -0.5), + Vec3::new(-0.5, -0.5, 0.5), + Vec3::new(0.5, -0.5, -0.5), + ], + } + } +} + +impl Tetrahedron { + /// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`. + #[inline] + pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self { + Self { + vertices: [a, b, c, d], + } + } + + /// Get the signed volume of the tetrahedron. + /// + /// If it's negative, the normal vector of the face defined by + /// the first three points using the right-hand rule points + /// away from the fourth vertex. + #[inline] + pub fn signed_volume(&self) -> f32 { + let [a, b, c, d] = self.vertices; + let ab = b - a; + let ac = c - a; + let ad = d - a; + Mat3::from_cols(ab, ac, ad).determinant() / 6.0 + } + + /// Get the centroid of the tetrahedron. + /// + /// This function finds the geometric center of the tetrahedron + /// by averaging the vertices: `centroid = (a + b + c + d) / 4`. + #[doc(alias("center", "barycenter", "baricenter"))] + #[inline] + pub fn centroid(&self) -> Vec3 { + (self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0 + } + + /// Get the triangles that form the faces of this tetrahedron. + /// + /// Note that the orientations of the faces are determined by that of the tetrahedron; if the + /// signed volume of this tetrahedron is positive, then the triangles' normals will point + /// outward, and if the signed volume is negative they will point inward. + #[inline] + pub fn faces(&self) -> [Triangle3d; 4] { + let [a, b, c, d] = self.vertices; + [ + Triangle3d::new(b, c, d), + Triangle3d::new(a, c, d).reversed(), + Triangle3d::new(a, b, d), + Triangle3d::new(a, b, c).reversed(), + ] + } +} + +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, +/// creating a shape with the same cross-section over the entire depth. +/// +/// The resulting volumes are prisms. +/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder. +#[doc(alias = "Prism")] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Extrusion { + /// The base shape of the extrusion + pub base_shape: T, + /// Half of the depth of the extrusion + pub half_depth: f32, +} + +impl Primitive3d for Extrusion {} + +impl Extrusion { + /// Create a new `Extrusion` from a given `base_shape` and `depth` + pub fn new(base_shape: T, depth: f32) -> Self { + Self { + base_shape, + half_depth: depth / 2., + } + } +} + +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::{InvalidDirectionError, Quat}; + + #[test] + fn direction_creation() { + assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X)); + assert_eq!( + Dir3::new(Vec3::new(0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Zero) + ); + assert_eq!( + Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)), + Err(InvalidDirectionError::NaN) + ); + assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5))); + + // Test rotation + assert!( + (Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X) + .abs_diff_eq(Vec3::Y, 10e-6) + ); + } + + #[test] + fn cuboid_closest_point() { + let cuboid = Cuboid::new(2.0, 2.0, 2.0); + assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X); + assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE); + assert_eq!( + cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)), + Vec3::new(0.25, 0.1, 0.3) + ); + } + + #[test] + fn sphere_closest_point() { + let sphere = Sphere { radius: 1.0 }; + assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X); + assert_eq!( + sphere.closest_point(Vec3::NEG_ONE * 10.0), + Vec3::NEG_ONE.normalize() + ); + assert_eq!( + sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)), + Vec3::new(0.25, 0.1, 0.3) + ); + } + + #[test] + fn segment_closest_point() { + assert_eq!( + Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0)) + .closest_point(Vec3::new(1.0, 6.0, -2.0)), + Vec3::new(1.0, 0.0, 0.0) + ); + + let segments = [ + Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)), + Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)), + Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)), + Segment3d::new( + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0), + ), + ]; + let points = [ + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(-1.0, 1.0, 2.0), + Vec3::new(1.0, 1.0, 1.0), + Vec3::new(-1.0, 0.0, 0.0), + Vec3::new(5.0, -1.0, 0.5), + Vec3::new(1.0, f32::EPSILON, 0.0), + ]; + + for point in points.iter() { + for segment in segments.iter() { + let closest = segment.closest_point(*point); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.point1()), + "Closest point must always be at least as close as either vertex." + ); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.point2()), + "Closest point must always be at least as close as either vertex." + ); + assert!( + point.distance_squared(closest) <= point.distance_squared(segment.center()), + "Closest point must always be at least as close as the center." + ); + let closest_to_closest = segment.closest_point(closest); + // Closest point must already be on the segment + assert_relative_eq!(closest_to_closest, closest); + } + } + } + + #[test] + 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] + fn plane_from_points() { + let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); + assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); + assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size"); + assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); + } + + #[test] + fn infinite_plane_math() { + let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); + assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); + assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation"); + + let point_in_plane = Vec3::X + Vec3::Z; + assert_eq!( + plane.signed_distance(origin, point_in_plane), + 0.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(origin, point_in_plane), + point_in_plane, + "incorrect point" + ); + + let point_outside = Vec3::Y; + assert_eq!( + plane.signed_distance(origin, point_outside), + -1.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(origin, point_outside), + Vec3::ZERO, + "incorrect point" + ); + + let point_outside = Vec3::NEG_Y; + assert_eq!( + plane.signed_distance(origin, point_outside), + 1.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(origin, point_outside), + Vec3::ZERO, + "incorrect point" + ); + + let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5; + let (proj, inj) = plane.isometries_xy(origin); + + let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO]; + assert_eq!(area_f(triangle), 0.5, "incorrect area"); + + let triangle_proj = triangle.map(|vec3| proj * vec3); + assert_relative_eq!(area_f(triangle_proj), 0.5); + + let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3); + assert_relative_eq!(area_f(triangle_proj_inj), 0.5); + } + + #[test] + fn cuboid_math() { + let cuboid = Cuboid::new(3.0, 7.0, 2.0); + assert_eq!( + cuboid, + 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] + fn cylinder_math() { + let cylinder = Cylinder::new(2.0, 9.0); + assert_eq!( + cylinder.base(), + Circle { radius: 2.0 }, + "base produces incorrect circle" + ); + assert_eq!( + cylinder.lateral_area(), + 113.097336, + "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] + fn capsule_math() { + let capsule = Capsule3d::new(2.0, 9.0); + assert_eq!( + capsule.to_cylinder(), + 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] + fn cone_math() { + let cone = Cone { + radius: 2.0, + height: 9.0, + }; + assert_eq!( + cone.base(), + Circle { radius: 2.0 }, + "base produces incorrect circle" + ); + 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] + fn conical_frustum_math() { + let frustum = ConicalFrustum { + height: 9.0, + radius_top: 1.0, + radius_bottom: 2.0, + }; + assert_eq!( + frustum.bottom_base(), + Circle { radius: 2.0 }, + "bottom base produces incorrect circle" + ); + assert_eq!( + frustum.top_base(), + Circle { radius: 1.0 }, + "top base produces incorrect circle" + ); + assert_eq!(frustum.slant_height(), 9.055386, "incorrect slant height"); + assert_eq!(frustum.lateral_area(), 85.345, "incorrect lateral area"); + assert_eq!( + frustum.bottom_base_area(), + 12.566371, + "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] + fn torus_math() { + let torus = Torus { + minor_radius: 0.3, + major_radius: 2.8, + }; + assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius"); + assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius"); + assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind"); + assert_eq!( + Torus::new(0.0, 1.0).kind(), + TorusKind::Horn, + "incorrect torus kind" + ); + assert_eq!( + Torus::new(-0.5, 1.0).kind(), + TorusKind::Spindle, + "incorrect torus kind" + ); + assert_eq!( + Torus::new(1.5, 1.0).kind(), + TorusKind::Invalid, + "torus should be invalid" + ); + 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.signed_volume(), + 3.2058334, + "incorrect signed volume" + ); + 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, + "incorrect signed volume" + ); + 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 + let mut default_triangle = Triangle3d::default(); + let reverse_default_triangle = Triangle3d::new( + Vec3::new(0.5, -0.5, 0.0), + 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(), + "incorrect degenerate check" + ); + assert_eq!( + default_triangle.centroid(), + Vec3::new(0.0, -0.16666667, 0.0), + "incorrect centroid" + ); + assert_eq!( + default_triangle.largest_side(), + (Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0)) + ); + default_triangle.reverse(); + assert_eq!( + default_triangle, reverse_default_triangle, + "incorrect reverse" + ); + assert_eq!( + default_triangle.circumcenter(), + Vec3::new(0.0, -0.125, 0.0), + "incorrect circumcenter" + ); + + // Custom triangle tests + let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y); + let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0)); + let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0)); + + assert_eq!( + right_triangle.circumcenter(), + Vec3::new(0.5, 0.5, 0.0), + "incorrect circumcenter" + ); + assert_eq!( + obtuse_triangle.circumcenter(), + Vec3::new(0.0, -4.95, 0.0), + "incorrect circumcenter" + ); + assert_eq!( + acute_triangle.circumcenter(), + Vec3::new(0.5, 2.475, 0.0), + "incorrect circumcenter" + ); + + assert!(acute_triangle.is_acute()); + assert!(!acute_triangle.is_obtuse()); + assert!(!obtuse_triangle.is_acute()); + assert!(obtuse_triangle.is_obtuse()); + + // 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!(!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), + "incorrect circumcenter" + ); + assert_eq!( + triangle.normal(), + Ok(Dir3::new_unchecked(Vec3::new( + -0.04134491, + -0.4134491, + 0.90958804 + ))), + "incorrect normal" + ); + + // Degenerate triangle tests + let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); + assert!( + zero_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + zero_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + zero_degenerate_triangle.largest_side(), + (Vec3::ZERO, Vec3::ZERO), + "incorrect largest side" + ); + + let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); + assert!( + dup_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + dup_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + dup_degenerate_triangle.largest_side(), + (Vec3::ZERO, Vec3::X), + "incorrect largest side" + ); + + let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); + assert!( + collinear_degenerate_triangle.is_degenerate(), + "incorrect degenerate check" + ); + assert_eq!( + collinear_degenerate_triangle.normal(), + Err(InvalidDirectionError::Zero), + "incorrect normal" + ); + assert_eq!( + collinear_degenerate_triangle.largest_side(), + (Vec3::NEG_X, Vec3::X), + "incorrect largest side" + ); + } +} diff --git a/crates/bevy_shapes/src/primitives/half_space.rs b/crates/bevy_shapes/src/primitives/half_space.rs new file mode 100644 index 0000000000000..52cc7fe38d292 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/half_space.rs @@ -0,0 +1,153 @@ +use bevy_math::{ops, Vec3, Vec3A, Vec4, Vec4Swizzles}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// A region of 3D space, specifically an open set whose border is a bisecting 2D plane. +/// +/// This bisecting plane partitions 3D space into two infinite regions, +/// the half-space is one of those regions and excludes the bisecting plane. +/// +/// Each instance of this type is characterized by: +/// - the bisecting plane's unit normal, normalized and pointing "inside" the half-space, +/// - the signed distance along the normal from the bisecting plane to the origin of 3D space. +/// +/// The distance can also be seen as: +/// - the distance along the inverse of the normal from the origin of 3D space to the bisecting plane, +/// - the opposite of the distance along the normal from the origin of 3D space to the bisecting plane. +/// +/// Any point `p` is considered to be within the `HalfSpace` when the length of the projection +/// of p on the normal is greater or equal than the opposite of the distance, +/// meaning: if the equation `normal.dot(p) + distance > 0.` is satisfied. +/// +/// For example, the half-space containing all the points with a z-coordinate lesser +/// or equal than `8.0` would be defined by: `HalfSpace::new(Vec3::NEG_Z.extend(-8.0))`. +/// It includes all the points from the bisecting plane towards `NEG_Z`, and the distance +/// from the plane to the origin is `-8.0` along `NEG_Z`. +/// +/// It is used to define a [`ViewFrustum`](crate::primitives::ViewFrustum), +/// but is also a useful mathematical primitive for rendering tasks such as light computation. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Clone, Debug, Default, PartialEq) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct HalfSpace { + normal_d: Vec4, +} + +impl HalfSpace { + /// Constructs a `HalfSpace` from a 4D vector whose first 3 components + /// represent the bisecting plane's unit normal, and the last component is + /// the signed distance along the normal from the plane to the origin. + /// The constructor ensures the normal vector is normalized and the distance is appropriately scaled. + #[inline] + pub fn new(normal_d: Vec4) -> Self { + Self { + normal_d: normal_d * normal_d.xyz().length_recip(), + } + } + + /// Returns the unit normal vector of the bisecting plane that characterizes the `HalfSpace`. + #[inline] + pub fn normal(&self) -> Vec3A { + Vec3A::from_vec4(self.normal_d) + } + + /// Returns the signed distance from the bisecting plane to the origin along + /// the plane's unit normal vector. + #[inline] + pub fn d(&self) -> f32 { + self.normal_d.w + } + + /// Returns the bisecting plane's unit normal vector and the signed distance + /// from the plane to the origin. + #[inline] + pub const fn normal_d(&self) -> Vec4 { + self.normal_d + } + + /// Returns the intersection point if the three halfspaces all intersect at a single point. + #[inline] + pub fn intersection_point(a: HalfSpace, b: HalfSpace, c: HalfSpace) -> Option { + let an = a.normal(); + let bn = b.normal(); + let cn = c.normal(); + + let x = Vec3A::new(an.x, bn.x, cn.x); + let y = Vec3A::new(an.y, bn.y, cn.y); + let z = Vec3A::new(an.z, bn.z, cn.z); + + let d = -Vec3A::new(a.d(), b.d(), c.d()); + + let u = y.cross(z); + let v = x.cross(d); + + let denom = x.dot(u); + + if ops::abs(denom) < f32::EPSILON { + return None; + } + + Some(Vec3::new(d.dot(u), z.dot(v), -y.dot(v)) / denom) + } +} + +#[cfg(test)] +mod half_space_tests { + use core::f32; + + use approx::assert_relative_eq; + + use super::HalfSpace; + use bevy_math::{Vec3, Vec4}; + + #[test] + fn intersection_point() { + // Intersection of shifted xy, xz, and yz planes + let xy_at_z_3 = HalfSpace { + normal_d: Vec4::new(0., 0., -1., 3.), + }; + let xz_at_y_2 = HalfSpace { + normal_d: Vec4::new(0., 1., 0., -2.), + }; + let yz_at_x_1 = HalfSpace { + normal_d: Vec4::new(1., 0., 0., -1.), + }; + assert_relative_eq!( + HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, yz_at_x_1).unwrap(), + Vec3::new(1., 2., 3.), + epsilon = 2e-7 + ); + + // Three planes that do not simultaneously intersect + let xz_at_y_3 = HalfSpace { + normal_d: Vec4::new(0., 1., 0., -3.), + }; + assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, xz_at_y_3).is_none()); + + // Three planes that intersect at a line + let other_xz_at_y_2 = HalfSpace { + normal_d: Vec4::new(0., -1., 0., 3.), + }; + assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, other_xz_at_y_2).is_none()); + + // Three identical planes + assert!(HalfSpace::intersection_point(xz_at_y_2, xz_at_y_2, other_xz_at_y_2).is_none()); + + // ill-defined halfspace + let ill_defined = HalfSpace { + normal_d: Vec4::new(0., 0., 0., f32::INFINITY), + }; + assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, ill_defined).is_none()); + } +} diff --git a/crates/bevy_shapes/src/primitives/inset.rs b/crates/bevy_shapes/src/primitives/inset.rs new file mode 100644 index 0000000000000..0cc89e58453a7 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/inset.rs @@ -0,0 +1,98 @@ +use bevy_math::{ + ops, + primitives::{ + Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, + Triangle2d, + }, + Vec2, +}; + +/// A primitive that can be resized uniformly. +/// +/// See documentation on [`Inset::inset`]. +/// +/// See also [`ToRing`](crate::primitives::ToRing). +pub trait Inset: Primitive2d { + /// Create a new version of this primitive that is resized uniformly. + /// That is, it resizes the shape inwards such that for the lines between vertices, + /// it creates new parallel lines that are `distance` inwards from the original lines. + /// + /// This is useful for creating smaller shapes or making outlines of `distance` thickness with [`Ring`](crate::primitives::Ring). + /// + /// See also [`ToRing::to_ring`](crate::primitives::ToRing::to_ring) + fn inset(self, distance: f32) -> Self; +} + +impl Inset for Circle { + fn inset(mut self, distance: f32) -> Self { + self.radius -= distance; + self + } +} + +impl Inset for Triangle2d { + fn inset(self, distance: f32) -> Self { + fn find_inset_point(a: Vec2, b: Vec2, c: Vec2, distance: f32) -> Vec2 { + let unit_vector_ab = (b - a).normalize(); + let unit_vector_ac = (c - a).normalize(); + let half_angle_bac = unit_vector_ab.angle_to(unit_vector_ac) / 2.0; + let mean = (unit_vector_ab + unit_vector_ac) / 2.0; + let direction = mean.normalize(); + let magnitude = distance / ops::sin(half_angle_bac); + a + direction * magnitude + } + + let [a, b, c] = self.vertices; + + let new_a = find_inset_point(a, b, c, distance); + let new_b = find_inset_point(b, c, a, distance); + let new_c = find_inset_point(c, a, b, distance); + + Self::new(new_a, new_b, new_c) + } +} + +impl Inset for Rhombus { + fn inset(mut self, distance: f32) -> Self { + let [half_width, half_height] = self.half_diagonals.into(); + let angle = ops::atan(half_height / half_width); + let x_offset = distance / ops::sin(angle); + let y_offset = distance / ops::cos(angle); + self.half_diagonals -= Vec2::new(x_offset, y_offset); + self + } +} + +impl Inset for Capsule2d { + fn inset(mut self, distance: f32) -> Self { + self.radius -= distance; + self + } +} + +impl Inset for Rectangle { + fn inset(mut self, distance: f32) -> Self { + self.half_size -= Vec2::splat(distance); + self + } +} + +impl Inset for CircularSegment { + fn inset(self, distance: f32) -> Self { + let old_arc = self.arc; + let radius = old_arc.radius - distance; + let apothem = old_arc.apothem() + distance; + // https://en.wikipedia.org/wiki/Circular_segment + let half_angle = ops::acos(apothem / radius); + Self::new(radius, half_angle) + } +} + +impl Inset for RegularPolygon { + fn inset(mut self, distance: f32) -> Self { + let half_angle = self.internal_angle_radians() / 2.0; + let offset = distance / ops::sin(half_angle); + self.circumcircle.radius -= offset; + self + } +} diff --git a/crates/bevy_shapes/src/primitives/mod.rs b/crates/bevy_shapes/src/primitives/mod.rs new file mode 100644 index 0000000000000..c2cb17f963123 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/mod.rs @@ -0,0 +1,54 @@ +//! This module defines primitive shapes. +//! The origin is (0, 0) for 2D primitives and (0, 0, 0) for 3D primitives, +//! unless stated otherwise. + +mod dim2; +pub use dim2::*; +mod dim3; +pub use dim3::*; +mod inset; +pub use inset::*; +mod half_space; +mod polygon; +pub use half_space::*; +mod view_frustum; +pub use view_frustum::*; + +/// A marker trait for 2D primitives +pub trait Primitive2d {} + +/// A marker trait for 3D primitives +pub trait Primitive3d {} + +/// The winding order for a set of points +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[doc(alias = "Orientation")] +pub enum WindingOrder { + /// A clockwise winding order + Clockwise, + /// A counterclockwise winding order + #[doc(alias = "AntiClockwise")] + CounterClockwise, + /// An invalid winding order indicating that it could not be computed reliably. + /// This often happens in *degenerate cases* where the points lie on the same line + #[doc(alias("Degenerate", "Collinear"))] + 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; +} diff --git a/crates/bevy_shapes/src/primitives/polygon.rs b/crates/bevy_shapes/src/primitives/polygon.rs new file mode 100644 index 0000000000000..71ef421c7251b --- /dev/null +++ b/crates/bevy_shapes/src/primitives/polygon.rs @@ -0,0 +1,376 @@ +#[cfg(feature = "alloc")] +use { + super::{Measured2d, Triangle2d}, + alloc::{collections::BTreeMap, vec::Vec}, + core::cmp::Ordering, +}; + +use bevy_math::Vec2; + +#[derive(Debug, Clone, Copy)] +#[cfg(feature = "alloc")] +enum Endpoint { + Left, + Right, +} + +/// An event in the [`EventQueue`] is either the left or right vertex of an edge of the polygon. +/// +/// Events are ordered so that any event `e1` which is to the left of another event `e2` is less than that event. +/// 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, + /// Type of the vertex (left or right) + endpoint: Endpoint, +} + +#[cfg(feature = "alloc")] +impl SweepLineEvent { + const fn position(&self) -> Vec2 { + match self.endpoint { + Endpoint::Left => self.segment.left, + Endpoint::Right => self.segment.right, + } + } +} + +#[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()) + } +} + +/// 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`. + /// + /// The events in the event queue will be ordered. + fn new(vertices: &[Vec2]) -> Self { + if vertices.is_empty() { + return Self { events: Vec::new() }; + } + + let mut events = Vec::with_capacity(vertices.len() * 2); + for i in 0..vertices.len() { + let v1 = vertices[i]; + let v2 = *vertices.get(i + 1).unwrap_or(&vertices[0]); + let (left, right) = if xy_order(v1, v2) == Ordering::Less { + (v1, v2) + } else { + (v2, v1) + }; + + let segment = Segment { + edge_index: i, + left, + right, + }; + events.push(SweepLineEvent { + segment, + endpoint: Endpoint::Left, + }); + events.push(SweepLineEvent { + segment, + endpoint: Endpoint::Right, + }); + } + + events.sort(); + + Self { events } + } +} + +/// Represents a segment or rather an edge of the polygon in the [`SweepLine`]. +/// +/// 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 + .y + .total_cmp(&other.left.y) + .then_with(|| self.right.y.total_cmp(&other.right.y)) + } +} + +/// 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, + below: Option, +} + +/// A sweep line allows for an efficient search for intersections between [segments](`Segment`). +/// +/// 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 { + vertices, + tree: BTreeMap::new(), + } + } + + /// Determine whether the given edges of the polygon intersect. + fn intersects(&self, edge1: Option, edge2: Option) -> bool { + let Some(edge1) = edge1 else { + return false; + }; + let Some(edge2) = edge2 else { + return false; + }; + + // All adjacent edges intersect at their shared vertex + // but these intersections do not count so we ignore them here. + // Likewise a segment will always intersect itself / an identical edge. + if edge1 == edge2 + || (edge1 + 1) % self.vertices.len() == edge2 + || (edge2 + 1) % self.vertices.len() == edge1 + { + return false; + } + + let s11 = self.vertices[edge1]; + let s12 = *self.vertices.get(edge1 + 1).unwrap_or(&self.vertices[0]); + let s21 = self.vertices[edge2]; + let s22 = *self.vertices.get(edge2 + 1).unwrap_or(&self.vertices[0]); + + // When both points of the second edge are on the same side of the first edge, no intersection is possible. + if point_side(s11, s12, s21) * point_side(s11, s12, s22) > 0.0 { + return false; + } + if point_side(s21, s22, s11) * point_side(s21, s22, s12) > 0.0 { + return false; + } + + true + } + + /// Add a new segment to the sweep line + fn add(&mut self, s: Segment) -> SegmentOrder { + let above = if let Some((next_s, next_ord)) = self.tree.range_mut(s..).next() { + next_ord.below.replace(s.edge_index); + Some(next_s.edge_index) + } else { + None + }; + let below = if let Some((prev_s, prev_ord)) = self.tree.range_mut(..s).next_back() { + prev_ord.above.replace(s.edge_index); + Some(prev_s.edge_index) + } else { + None + }; + + let s_ord = SegmentOrder { above, below }; + self.tree.insert(s, s_ord); + s_ord + } + + /// Get the segment order for the given segment. + /// + /// If `s` has not been added to the [`SweepLine`] `None` will be returned. + fn find(&self, s: &Segment) -> Option<&SegmentOrder> { + self.tree.get(s) + } + + /// Remove `s` from the [`SweepLine`]. + fn remove(&mut self, s: &Segment) { + let Some(s_ord) = self.tree.get(s).copied() else { + return; + }; + + if let Some((_, above_ord)) = self.tree.range_mut(s..).next() { + above_ord.below = s_ord.below; + } + if let Some((_, below_ord)) = self.tree.range_mut(..s).next_back() { + below_ord.above = s_ord.above; + } + + self.tree.remove(s); + } +} + +/// 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) +} + +/// Tests whether the `vertices` describe a simple polygon. +/// The last vertex must not be equal to the first vertex. +/// +/// A polygon is simple if it is not self intersecting and not self tangent. +/// As such, no two edges of the polygon may cross each other and each vertex must not lie on another edge. +/// +/// Any 'polygon' with less than three vertices is simple. +/// +/// 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 event_queue = EventQueue::new(vertices); + let mut sweep_line = SweepLine::new(vertices); + + for e in event_queue.events { + match e.endpoint { + Endpoint::Left => { + let s = sweep_line.add(e.segment); + if sweep_line.intersects(Some(e.segment.edge_index), s.above) + || sweep_line.intersects(Some(e.segment.edge_index), s.below) + { + return false; + } + } + Endpoint::Right => { + if let Some(s) = sweep_line.find(&e.segment) { + if sweep_line.intersects(s.above, s.below) { + return false; + } + sweep_line.remove(&e.segment); + } + } + } + } + + true +} + +#[cfg(test)] +mod tests { + use crate::primitives::polygon::is_polygon_simple; + use bevy_math::Vec2; + + #[test] + fn complex_polygon() { + // A square with one side punching through the opposite side. + let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y, Vec2::new(2.0, 0.5)]; + assert!(!is_polygon_simple(&verts)); + + // A square with a vertex from one side touching the opposite side. + let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y, Vec2::new(1.0, 0.5)]; + assert!(!is_polygon_simple(&verts)); + + // A square with one side touching the opposite side. + let verts = [ + Vec2::ZERO, + Vec2::X, + Vec2::ONE, + Vec2::Y, + Vec2::new(1.0, 0.6), + Vec2::new(1.0, 0.4), + ]; + assert!(!is_polygon_simple(&verts)); + + // Four points lying on a line + let verts = [Vec2::ONE, Vec2::new(3., 2.), Vec2::new(5., 3.), Vec2::NEG_X]; + assert!(!is_polygon_simple(&verts)); + + // Three points lying on a line + let verts = [Vec2::ONE, Vec2::new(3., 2.), Vec2::NEG_X]; + assert!(!is_polygon_simple(&verts)); + + // Two identical points and one other point + let verts = [Vec2::ONE, Vec2::ONE, Vec2::NEG_X]; + assert!(!is_polygon_simple(&verts)); + + // Two triangles with one shared side + let verts = [Vec2::ZERO, Vec2::X, Vec2::Y, Vec2::ONE, Vec2::X, Vec2::Y]; + assert!(!is_polygon_simple(&verts)); + } + + #[test] + fn simple_polygon() { + // A square + let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]; + assert!(is_polygon_simple(&verts)); + + let verts = []; + assert!(is_polygon_simple(&verts)); + } +} diff --git a/crates/bevy_shapes/src/primitives/view_frustum.rs b/crates/bevy_shapes/src/primitives/view_frustum.rs new file mode 100644 index 0000000000000..2c314cb8c7c62 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/view_frustum.rs @@ -0,0 +1,253 @@ +use crate::primitives::HalfSpace; +use bevy_math::{Mat4, Vec3, Vec4}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s. +/// +/// View Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid. +/// +/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors +/// of the half-spaces point towards the interior of the frustum. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Clone, Debug, Default, PartialEq) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct ViewFrustum { + /// The six half-spaces making up the frustum + pub half_spaces: [HalfSpace; 6], +} + +impl ViewFrustum { + /// The index for the near plane in `half_spaces` + pub const NEAR_PLANE_IDX: usize = 4; + /// The index for the far plane in `half_spaces` + pub const FAR_PLANE_IDX: usize = 5; + /// Vec4 representing an inactive half space. + /// The bisecting plane's unit normal is set to (0, 0, 0). + /// The signed distance along the normal from the plane to the origin is set to `f32::INFINITY`. + const INACTIVE_HALF_SPACE: Vec4 = Vec4::new(0.0, 0.0, 0.0, f32::INFINITY); + + /// Returns a view frustum derived from `clip_from_world`. + #[inline] + pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self { + let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world); + frustum.half_spaces[Self::FAR_PLANE_IDX] = HalfSpace::new(clip_from_world.row(2)); + frustum + } + + /// Returns a view frustum derived from `clip_from_world`, + /// but with a custom far plane. + #[inline] + pub fn from_clip_from_world_custom_far( + clip_from_world: &Mat4, + view_translation: &Vec3, + view_backward: &Vec3, + far: f32, + ) -> Self { + let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world); + let far_center = *view_translation - far * *view_backward; + frustum.half_spaces[Self::FAR_PLANE_IDX] = + HalfSpace::new(view_backward.extend(-view_backward.dot(far_center))); + frustum + } + + /// Calculates the corners of this frustum. Returns `None` if the frustum isn't properly defined. + /// + /// If `Some`, the corners are returned in the following order: + /// near top left, near top right, near bottom right, near bottom left, + /// far top left, far top right, far bottom right, far bottom left. + /// If the far plane is an inactive half space, the intersection points + /// that include the far plane will be `Vec3::NAN`. + #[inline] + pub fn corners(&self) -> Option<[Vec3; 8]> { + let [left, right, top, bottom, near, far] = self.half_spaces; + Some([ + HalfSpace::intersection_point(top, left, near)?, + HalfSpace::intersection_point(top, right, near)?, + HalfSpace::intersection_point(bottom, right, near)?, + HalfSpace::intersection_point(bottom, left, near)?, + HalfSpace::intersection_point(top, left, far)?, + HalfSpace::intersection_point(top, right, far)?, + HalfSpace::intersection_point(bottom, right, far)?, + HalfSpace::intersection_point(bottom, left, far)?, + ]) + } + + // NOTE: This approach of extracting the frustum half-space from the view + // projection matrix is from Foundations of Game Engine Development 2 + // Rendering by Lengyel. + /// Returns a view frustum derived from `view_projection`, + /// without a far plane. + fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self { + let row0 = clip_from_world.row(0); + let row1 = clip_from_world.row(1); + let row2 = clip_from_world.row(2); + let row3 = clip_from_world.row(3); + + Self { + half_spaces: [ + HalfSpace::new(row3 + row0), + HalfSpace::new(row3 - row0), + HalfSpace::new(row3 + row1), + HalfSpace::new(row3 - row1), + HalfSpace::new(row3 + row2), + HalfSpace::new(Self::INACTIVE_HALF_SPACE), + ], + } + } +} + +#[cfg(test)] +mod view_frustum_tests { + use core::f32::consts::FRAC_1_SQRT_2; + + use approx::assert_relative_eq; + + use super::ViewFrustum; + use crate::primitives::HalfSpace; + use bevy_math::{Vec3, Vec4}; + + #[test] + fn cuboid_frustum_corners() { + let cuboid_frustum = ViewFrustum { + // left: x = -5; right: x = 4 + // near: y = 0; far: y = 6 + // top: z = 3; bottom: z = -2 + half_spaces: [ + // left: yz plane at x = -5 + HalfSpace::new(Vec4::new(1., 0., 0., 5.)), + // right: yz plane at x = 4 + HalfSpace::new(Vec4::new(-1., 0., 0., 4.)), + // top: xy plane at z = 3 + HalfSpace::new(Vec4::new(0., 0., -1., 3.)), + // bottom: xy plane at z = -2 + HalfSpace::new(Vec4::new(0., 0., 1., 2.)), + // near: xz plane at origin (y = 0) + HalfSpace::new(Vec4::new(0., 1., 0., 0.)), + // far: xz plane at y = 6 + HalfSpace::new(Vec4::new(0., -1., 0., 6.)), + ], + }; + let corners = cuboid_frustum.corners().unwrap(); + // near top left + assert_relative_eq!(corners[0], Vec3::new(-5., 0., 3.), epsilon = 2e-7); + // near top right + assert_relative_eq!(corners[1], Vec3::new(4., 0., 3.), epsilon = 2e-7); + // near bottom right + assert_relative_eq!(corners[2], Vec3::new(4., 0., -2.), epsilon = 2e-7); + // near bottom left + assert_relative_eq!(corners[3], Vec3::new(-5., 0., -2.), epsilon = 2e-7); + // far top left + assert_relative_eq!(corners[4], Vec3::new(-5., 6., 3.), epsilon = 2e-7); + // far top right + assert_relative_eq!(corners[5], Vec3::new(4., 6., 3.), epsilon = 2e-7); + // far bottom right + assert_relative_eq!(corners[6], Vec3::new(4., 6., -2.), epsilon = 2e-7); + // far bottom left + assert_relative_eq!(corners[7], Vec3::new(-5., 6., -2.), epsilon = 2e-7); + } + + #[test] + fn pyramid_frustum_corners() { + // a frustum where the near plane intersects the left right top and bottom planes + // at a single point + let pyramid_frustum = ViewFrustum { + half_spaces: [ + // left + HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + // right + HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + // top + HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)), + // bottom + HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), + // near: xz plane at y = -1 + HalfSpace::new(Vec4::new(0., 1., 0., 1.)), + // far: xz plane at y = 3 + HalfSpace::new(Vec4::new(0., -1., 0., 3.)), + ], + }; + let corners = pyramid_frustum.corners().unwrap(); + // near top left + assert_relative_eq!(corners[0], Vec3::new(0., -1., 0.), epsilon = 2e-7); + // near top right + assert_relative_eq!(corners[1], Vec3::new(0., -1., 0.), epsilon = 2e-7); + // near bottom right + assert_relative_eq!(corners[2], Vec3::new(0., -1., 0.), epsilon = 2e-7); + // near bottom left + assert_relative_eq!(corners[3], Vec3::new(0., -1., 0.), epsilon = 2e-7); + // far top left + assert_relative_eq!(corners[4], Vec3::new(-4., 3., 4.), epsilon = 2e-7); + // far top right + assert_relative_eq!(corners[5], Vec3::new(4., 3., 4.), epsilon = 2e-7); + // far bottom right + assert_relative_eq!(corners[6], Vec3::new(4., 3., -4.), epsilon = 2e-7); + // far bottom left + assert_relative_eq!(corners[7], Vec3::new(-4., 3., -4.), epsilon = 2e-7); + } + + #[test] + fn frustum_with_some_nan_corners() { + // frustum with no far plane has NAN far corners + let no_far = ViewFrustum { + half_spaces: [ + // left: a yz plane rotated outwards + HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + // right: a yz plane rotated outwards + HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + // top: an xz plane rotated outwards + HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)), + // bottom: xz plane rotated outwards + HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), + // near: xz plane at origin (y = 0) + HalfSpace::new(Vec4::new(0., 1., 0., 0.)), + // far + HalfSpace::new(ViewFrustum::INACTIVE_HALF_SPACE), + ], + }; + let corners = no_far.corners().unwrap(); + // near top left + assert_relative_eq!(corners[0], Vec3::new(-1., 0., 1.), epsilon = 2e-7); + // near top right + assert_relative_eq!(corners[1], Vec3::new(1., 0., 1.), epsilon = 2e-7); + // near bottom right + assert_relative_eq!(corners[2], Vec3::new(1., 0., -1.), epsilon = 2e-7); + // near bottom left + assert_relative_eq!(corners[3], Vec3::new(-1., 0., -1.), epsilon = 2e-7); + // far top left + assert!(corners[4].is_nan()); + // far top right + assert!(corners[5].is_nan()); + // far bottom right + assert!(corners[6].is_nan()); + // far bottom left + assert!(corners[7].is_nan()); + } + + #[test] + fn invalid_frustum_corners() { + let invalid = ViewFrustum { + half_spaces: [ + // the left and the top half spaces are the same, resulting in no intersection point + HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)), + HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), + HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), + HalfSpace::new(Vec4::new(0., 1., 0., 0.)), + HalfSpace::new(Vec4::new(0., -1., 0., 3.)), + ], + }; + assert!(invalid.corners().is_none()); + } +} From 6f648042a42e882cdac738f3377046e8635918d8 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 13:57:23 +0200 Subject: [PATCH 03/17] implement meshing and extrusion in bevy_shapes --- crates/bevy_shapes/Cargo.toml | 17 +- crates/bevy_shapes/src/lib.rs | 3 + crates/bevy_shapes/src/meshing/dim2.rs | 1583 +++++++++++++++++ .../bevy_shapes/src/meshing/dim3/capsule.rs | 436 +++++ crates/bevy_shapes/src/meshing/dim3/cone.rs | 274 +++ .../src/meshing/dim3/conical_frustum.rs | 183 ++ crates/bevy_shapes/src/meshing/dim3/cuboid.rs | 98 + .../bevy_shapes/src/meshing/dim3/cylinder.rs | 221 +++ crates/bevy_shapes/src/meshing/dim3/mod.rs | 23 + crates/bevy_shapes/src/meshing/dim3/plane.rs | 205 +++ .../src/meshing/dim3/polyline3d.rs | 41 + .../bevy_shapes/src/meshing/dim3/segment3d.rs | 49 + crates/bevy_shapes/src/meshing/dim3/sphere.rs | 267 +++ .../src/meshing/dim3/tetrahedron.rs | 63 + crates/bevy_shapes/src/meshing/dim3/torus.rs | 175 ++ .../src/meshing/dim3/triangle3d.rs | 129 ++ crates/bevy_shapes/src/meshing/extrusion.rs | 432 +++++ crates/bevy_shapes/src/meshing/mod.rs | 53 + 18 files changed, 4250 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_shapes/src/meshing/dim2.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/capsule.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/cone.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/cuboid.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/cylinder.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/mod.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/plane.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/polyline3d.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/segment3d.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/sphere.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/torus.rs create mode 100644 crates/bevy_shapes/src/meshing/dim3/triangle3d.rs create mode 100644 crates/bevy_shapes/src/meshing/extrusion.rs create mode 100644 crates/bevy_shapes/src/meshing/mod.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index 514af323bfa61..91c3b43739b8f 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -11,7 +11,9 @@ version = "0.20.0-dev" [dependencies] approx = { default-features = false, optional = true, version = "0.5" } +bevy_asset = { default-features = false, optional = true, path = "../bevy_asset" } bevy_math = { default-features = false, path = "../bevy_math" } +bevy_mesh = { default-features = false, optional = true, path = "../bevy_mesh" } bevy_reflect = { default-features = false, features = [ "glam", ], optional = true, path = "../bevy_reflect", version = "0.20.0-dev" } @@ -19,24 +21,35 @@ derive_more = { default-features = false, features = [ "from", "into", ], version = "2" } +hexasphere = { default-features = false, optional = true, version = "19.0.0" } serde = { default-features = false, features = [ "derive", ], optional = true, version = "1" } thiserror = { default-features = false, version = "2" } +wgpu-types = { default-features = false, optional = true, version = "29.0.3" } [dev-dependencies] bevy_math = { default-features = false, features = [ "approx", ], path = "../bevy_math" } +bevy_platform = { default-features = false, path = "../bevy_platform" } [features] alloc = ["bevy_math/alloc"] default = ["bevy_math/default", "std"] std = ["alloc", "bevy_math/std"] -approx = ["bevy_math/approx", "dep:approx"] +approx = ["bevy_math/approx", "dep:approx"] bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] -serialize = ["bevy_math/serialize", "dep:serde"] +extrusion = ["meshing"] +meshing = [ + "bevy_reflect", + "dep:bevy_asset", + "dep:bevy_mesh", + "dep:hexasphere", + "dep:wgpu-types", +] +serialize = ["bevy_math/serialize", "dep:serde"] [lints] workspace = true diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index ff66ac1b6242d..e49b605153e26 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -21,3 +21,6 @@ extern crate std; extern crate alloc; pub mod primitives; + +#[cfg(all(feature = "meshing", feature = "alloc"))] +pub mod meshing; diff --git a/crates/bevy_shapes/src/meshing/dim2.rs b/crates/bevy_shapes/src/meshing/dim2.rs new file mode 100644 index 0000000000000..ef4686a1b4aa0 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim2.rs @@ -0,0 +1,1583 @@ +use core::f32::consts::FRAC_PI_2; +use core::mem; + +use bevy_asset::RenderAssetUsages; +use bevy_mesh::{Indices, Mesh, VertexAttributeValues}; + +use crate::{ + meshing::{extrusion::PerimeterSegment, Extrudable, MeshBuilder, Meshable}, + primitives::{ + Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, + Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, Triangle3d, + WindingOrder, + }, +}; +use bevy_math::prelude::Polyline2d; +use bevy_math::{ops, FloatExt, Vec2, Vec3}; +use bevy_reflect::prelude::*; +use wgpu_types::PrimitiveTopology; + +use alloc::vec::Vec; + +/// A builder used for creating a [`Mesh`] with a [`Circle`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct CircleMeshBuilder { + /// The [`Circle`] shape. + pub circle: Circle, + /// The number of vertices used for the circle mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: u32, +} + +impl Default for CircleMeshBuilder { + fn default() -> Self { + Self { + circle: Circle::default(), + resolution: 32, + } + } +} + +impl CircleMeshBuilder { + /// Creates a new [`CircleMeshBuilder`] from a given radius and vertex count. + #[inline] + pub const fn new(radius: f32, resolution: u32) -> Self { + Self { + circle: Circle { radius }, + resolution, + } + } + + /// Sets the number of vertices used for the circle mesh. + #[inline] + #[doc(alias = "vertices")] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl MeshBuilder for CircleMeshBuilder { + fn build(&self) -> Mesh { + Ellipse::new(self.circle.radius, self.circle.radius) + .mesh_builder() + .resolution(self.resolution) + .build() + } +} + +impl Extrudable for CircleMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (0..self.resolution).chain([0]).collect(), + }] + } +} + +impl Meshable for Circle { + type Output = CircleMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + CircleMeshBuilder { + circle: *self, + ..Default::default() + } + } +} + +/// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. +/// +/// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes +/// the entire circle, particularly the same texture will be displayed with different fractions of a +/// complete circle. +/// +/// It's expected that more will be added in the future, such as a variant that causes the texture to be +/// scaled to fit the bounding box of the shape, which would be good for packed textures only including the +/// portion of the circle that is needed to display. +#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Default, Debug, Clone)] +#[non_exhaustive] +pub enum CircularMeshUvMode { + /// Treats the shape as a mask over a circle of equal size and radius, + /// with the center of the circle at the center of the texture. + Mask { + /// Angle by which to rotate the shape when generating the UV map. + angle: f32, + }, +} + +impl Default for CircularMeshUvMode { + fn default() -> Self { + CircularMeshUvMode::Mask { angle: 0.0 } + } +} + +/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. +/// +/// The resulting mesh will have a UV-map such that the center of the circle is +/// at the center of the texture. +#[derive(Clone, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct CircularSectorMeshBuilder { + /// The sector shape. + pub sector: CircularSector, + /// The number of vertices used for the arc portion of the sector mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: u32, + /// The UV mapping mode + pub uv_mode: CircularMeshUvMode, +} + +impl Default for CircularSectorMeshBuilder { + fn default() -> Self { + Self { + sector: CircularSector::default(), + resolution: 32, + uv_mode: CircularMeshUvMode::default(), + } + } +} + +impl CircularSectorMeshBuilder { + /// Creates a new [`CircularSectorMeshBuilder`] from a given sector + #[inline] + pub fn new(sector: CircularSector) -> Self { + Self { + sector, + ..Self::default() + } + } + + /// Sets the number of vertices used for the sector mesh. + #[inline] + #[doc(alias = "vertices")] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets the uv mode used for the sector mesh + #[inline] + pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { + self.uv_mode = uv_mode; + self + } +} + +impl MeshBuilder for CircularSectorMeshBuilder { + fn build(&self) -> Mesh { + let resolution = self.resolution as usize; + let mut indices = Vec::with_capacity((resolution - 1) * 3); + let mut positions = Vec::with_capacity(resolution + 1); + let normals = alloc::vec![[0.0, 0.0, 1.0]; resolution + 1]; + let mut uvs = Vec::with_capacity(resolution + 1); + + let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; + + // Push the center of the circle. + positions.push([0.0; 3]); + uvs.push([0.5; 2]); + + let first_angle = FRAC_PI_2 - self.sector.half_angle(); + let last_angle = FRAC_PI_2 + self.sector.half_angle(); + let last_i = (self.resolution - 1) as f32; + for i in 0..self.resolution { + let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); + + // Compute the vertex + let vertex = self.sector.radius() * Vec2::from_angle(angle); + // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). + // We accomplish the Y axis flip by negating the angle. + let uv = + Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); + + positions.push([vertex.x, vertex.y, 0.0]); + uvs.push([uv.x, uv.y]); + } + + for i in 1..self.resolution { + // Index 0 is the center. + indices.extend_from_slice(&[0, i, i + 1]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for CircularSectorMeshBuilder { + fn perimeter(&self) -> Vec { + let (sin, cos) = ops::sin_cos(self.sector.arc.half_angle); + let first_normal = Vec2::new(sin, cos); + let last_normal = Vec2::new(-sin, cos); + alloc::vec![ + PerimeterSegment::Flat { + indices: alloc::vec![self.resolution, 0, 1], + }, + PerimeterSegment::Smooth { + first_normal, + last_normal, + indices: (1..=self.resolution).collect(), + }, + ] + } +} + +impl Meshable for CircularSector { + type Output = CircularSectorMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + CircularSectorMeshBuilder { + sector: *self, + ..Default::default() + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. +/// +/// The resulting mesh will have a UV-map such that the center of the circle is +/// at the center of the texture. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct CircularSegmentMeshBuilder { + /// The segment shape. + pub segment: CircularSegment, + /// The number of vertices used for the arc portion of the segment mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: u32, + /// The UV mapping mode + pub uv_mode: CircularMeshUvMode, +} + +impl Default for CircularSegmentMeshBuilder { + fn default() -> Self { + Self { + segment: CircularSegment::default(), + resolution: 32, + uv_mode: CircularMeshUvMode::default(), + } + } +} + +impl CircularSegmentMeshBuilder { + /// Creates a new [`CircularSegmentMeshBuilder`] from a given segment + #[inline] + pub fn new(segment: CircularSegment) -> Self { + Self { + segment, + ..Self::default() + } + } + + /// Sets the number of vertices used for the segment mesh. + #[inline] + #[doc(alias = "vertices")] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets the uv mode used for the segment mesh + #[inline] + pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { + self.uv_mode = uv_mode; + self + } +} + +impl MeshBuilder for CircularSegmentMeshBuilder { + fn build(&self) -> Mesh { + let resolution = self.resolution as usize; + let mut indices = Vec::with_capacity((resolution - 1) * 3); + let mut positions = Vec::with_capacity(resolution + 1); + let normals = alloc::vec![[0.0, 0.0, 1.0]; resolution + 1]; + let mut uvs = Vec::with_capacity(resolution + 1); + + let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; + + // Push the center of the chord. + let midpoint_vertex = self.segment.chord_midpoint(); + positions.push([midpoint_vertex.x, midpoint_vertex.y, 0.0]); + // Compute the UV coordinate of the midpoint vertex. + // This is similar to the computation inside the loop for the arc vertices, + // but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius + // to correctly position the vertex. + let midpoint_uv = Vec2::from_angle(-uv_angle - FRAC_PI_2).mul_add( + Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())), + Vec2::splat(0.5), + ); + uvs.push([midpoint_uv.x, midpoint_uv.y]); + + let first_angle = FRAC_PI_2 - self.segment.half_angle(); + let last_angle = FRAC_PI_2 + self.segment.half_angle(); + let last_i = (self.resolution - 1) as f32; + for i in 0..self.resolution { + let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); + + // Compute the vertex + let vertex = self.segment.radius() * Vec2::from_angle(angle); + // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). + // We accomplish the Y axis flip by negating the angle. + let uv = + Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); + + positions.push([vertex.x, vertex.y, 0.0]); + uvs.push([uv.x, uv.y]); + } + + for i in 1..self.resolution { + // Index 0 is the midpoint of the chord. + indices.extend_from_slice(&[0, i, i + 1]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for CircularSegmentMeshBuilder { + fn perimeter(&self) -> Vec { + let (sin, cos) = ops::sin_cos(self.segment.arc.half_angle); + let first_normal = Vec2::new(sin, cos); + let last_normal = Vec2::new(-sin, cos); + alloc::vec![ + PerimeterSegment::Flat { + indices: alloc::vec![self.resolution, 0, 1], + }, + PerimeterSegment::Smooth { + first_normal, + last_normal, + indices: (1..=self.resolution).collect(), + }, + ] + } +} + +impl Meshable for CircularSegment { + type Output = CircularSegmentMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + CircularSegmentMeshBuilder { + segment: *self, + ..Default::default() + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`ConvexPolygon`] shape. +/// +/// You must verify that the `vertices` are not concave when constructing this type. You can +/// guarantee this by creating a [`ConvexPolygon`] first, then calling [`ConvexPolygon::mesh()`]. +#[derive(Clone, Debug, Reflect)] +#[reflect(Debug, Clone)] +pub struct ConvexPolygonMeshBuilder { + pub vertices: Vec, +} + +impl Meshable for ConvexPolygon { + type Output = ConvexPolygonMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Self::Output { + vertices: self.vertices().to_vec(), + } + } +} + +impl MeshBuilder for ConvexPolygonMeshBuilder { + fn build(&self) -> Mesh { + let len = self.vertices.len(); + let mut indices = Vec::with_capacity((len - 2) * 3); + let mut positions = Vec::with_capacity(len); + let normals = alloc::vec![[0.0, 0.0, 1.0]; len]; + let mut uvs = Vec::with_capacity(len); + + let mut min = Vec2::splat(f32::INFINITY); + let mut max = Vec2::splat(f32::NEG_INFINITY); + + for vertex in &self.vertices { + min = min.min(*vertex); + max = max.max(*vertex); + } + + let size = (max - min).max(Vec2::splat(f32::EPSILON)); + + for vertex in &self.vertices { + positions.push([vertex.x, vertex.y, 0.0]); + let uv = (*vertex - min) / size; + // Map each axis independently into [0, 1] over the polygon's AABB. + uvs.push([uv.x, uv.y]); + } + for i in 2..len as u32 { + indices.extend_from_slice(&[0, i - 1, i]); + } + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for ConvexPolygonMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: (0..self.vertices.len() as u32).chain([0]).collect(), + }] + } +} + +/// A builder used for creating a [`Mesh`] with a [`RegularPolygon`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct RegularPolygonMeshBuilder { + circumradius: f32, + sides: u32, +} + +impl Default for RegularPolygonMeshBuilder { + /// Returns the default [`RegularPolygonMeshBuilder`] with six sides (a hexagon) and a circumradius of `0.5`. + fn default() -> Self { + Self { + circumradius: 0.5, + sides: 6, + } + } +} + +impl RegularPolygonMeshBuilder { + /// Creates a new [`RegularPolygonMeshBuilder`] from the radius of a circumcircle and a number + /// of sides. + /// + /// # Panics + /// + /// Panics in debug mode if `circumradius` is negative, or if `sides` is less than 3. + pub const fn new(circumradius: f32, sides: u32) -> Self { + debug_assert!( + circumradius.is_sign_positive(), + "polygon has a negative radius" + ); + debug_assert!(sides > 2, "polygon has less than 3 sides"); + + Self { + circumradius, + sides, + } + } +} + +impl Meshable for RegularPolygon { + type Output = RegularPolygonMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Self::Output { + circumradius: self.circumcircle.radius, + sides: self.sides, + } + } +} + +impl MeshBuilder for RegularPolygonMeshBuilder { + fn build(&self) -> Mesh { + // The ellipse mesh is just a regular polygon with two radii + Ellipse::new(self.circumradius, self.circumradius) + .mesh_builder() + .resolution(self.sides) + .build() + } +} + +impl Extrudable for RegularPolygonMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: (0..self.sides).chain([0]).collect(), + }] + } +} + +/// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct EllipseMeshBuilder { + /// The [`Ellipse`] shape. + pub ellipse: Ellipse, + /// The number of vertices used for the ellipse mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: u32, +} + +impl Default for EllipseMeshBuilder { + fn default() -> Self { + Self { + ellipse: Ellipse::default(), + resolution: 32, + } + } +} + +impl EllipseMeshBuilder { + /// Creates a new [`EllipseMeshBuilder`] from a given half width and half height and a vertex count. + #[inline] + pub const fn new(half_width: f32, half_height: f32, resolution: u32) -> Self { + Self { + ellipse: Ellipse::new(half_width, half_height), + resolution, + } + } + + /// Sets the number of vertices used for the ellipse mesh. + #[inline] + #[doc(alias = "vertices")] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl MeshBuilder for EllipseMeshBuilder { + fn build(&self) -> Mesh { + let resolution = self.resolution as usize; + let mut indices = Vec::with_capacity((resolution - 2) * 3); + let mut positions = Vec::with_capacity(resolution); + let normals = alloc::vec![[0.0, 0.0, 1.0]; resolution]; + let mut uvs = Vec::with_capacity(resolution); + + // Add pi/2 so that there is a vertex at the top (sin is 1.0 and cos is 0.0) + let start_angle = FRAC_PI_2; + let step = core::f32::consts::TAU / self.resolution as f32; + + for i in 0..self.resolution { + // Compute vertex position at angle theta + let theta = start_angle + i as f32 * step; + let (sin, cos) = ops::sin_cos(theta); + let x = cos * self.ellipse.half_size.x; + let y = sin * self.ellipse.half_size.y; + + positions.push([x, y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[0, i, i + 1]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for EllipseMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (0..self.resolution).chain([0]).collect(), + }] + } +} + +impl Meshable for Ellipse { + type Output = EllipseMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + EllipseMeshBuilder { + ellipse: *self, + ..Default::default() + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`Segment2d`]. +pub struct Segment2dMeshBuilder { + /// The [`Segment2d`] shape. + pub segment: Segment2d, +} + +impl Segment2dMeshBuilder { + /// Creates a new [`Segment2dMeshBuilder`] from a given segment. + #[inline] + pub const fn new(line: Segment2d) -> Self { + Self { segment: line } + } +} + +impl MeshBuilder for Segment2dMeshBuilder { + fn build(&self) -> Mesh { + let positions = self.segment.vertices.map(|v| v.extend(0.0)).to_vec(); + let indices = Indices::U32(alloc::vec![0, 1]); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_indices(indices) + } +} + +impl Meshable for Segment2d { + type Output = Segment2dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Segment2dMeshBuilder::new(*self) + } +} + +/// A builder used for creating a [`Mesh`] with a [`Polyline2d`] shape. +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Polyline2dMeshBuilder { + polyline: Polyline2d, +} + +impl MeshBuilder for Polyline2dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self + .polyline + .vertices + .iter() + .map(|v| v.extend(0.0)) + .collect(); + + let indices = Indices::U32( + (0..self.polyline.vertices.len() as u32 - 1) + .flat_map(|i| [i, i + 1]) + .collect(), + ); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + } +} + +impl Meshable for Polyline2d { + type Output = Polyline2dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Polyline2dMeshBuilder { + polyline: self.clone(), + } + } +} + +/// A builder for creating a [`Mesh`] with an [`Annulus`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct AnnulusMeshBuilder { + /// The [`Annulus`] shape. + pub annulus: Annulus, + + /// The number of vertices used in constructing each concentric circle of the annulus mesh. + /// The default is `32`. + pub resolution: u32, +} + +impl Default for AnnulusMeshBuilder { + fn default() -> Self { + Self { + annulus: Annulus::default(), + resolution: 32, + } + } +} + +impl AnnulusMeshBuilder { + /// Create an [`AnnulusMeshBuilder`] with the given inner radius, outer radius, and angular vertex count. + #[inline] + pub fn new(inner_radius: f32, outer_radius: f32, resolution: u32) -> Self { + Self { + annulus: Annulus::new(inner_radius, outer_radius), + resolution, + } + } + + /// Sets the number of vertices used in constructing the concentric circles of the annulus mesh. + #[inline] + pub fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl MeshBuilder for AnnulusMeshBuilder { + fn build(&self) -> Mesh { + let inner_radius = self.annulus.inner_circle.radius; + let outer_radius = self.annulus.outer_circle.radius; + + let num_vertices = (self.resolution as usize + 1) * 2; + let mut indices = Vec::with_capacity(self.resolution as usize * 6); + let mut positions = Vec::with_capacity(num_vertices); + let mut uvs = Vec::with_capacity(num_vertices); + let normals = alloc::vec![[0.0, 0.0, 1.0]; num_vertices]; + + // We have one more set of vertices than might be naïvely expected; + // the vertices at `start_angle` are duplicated for the purposes of UV + // mapping. Here, each iteration places a pair of vertices at a fixed + // angle from the center of the annulus. + let start_angle = FRAC_PI_2; + let step = core::f32::consts::TAU / self.resolution as f32; + for i in 0..=self.resolution { + let theta = start_angle + (i % self.resolution) as f32 * step; + let (sin, cos) = ops::sin_cos(theta); + let inner_pos = [cos * inner_radius, sin * inner_radius, 0.]; + let outer_pos = [cos * outer_radius, sin * outer_radius, 0.]; + positions.push(inner_pos); + positions.push(outer_pos); + + // The first UV direction is radial and the second is angular; + // i.e., a single UV rectangle is stretched around the annulus, with + // its top and bottom meeting as the circle closes. Lines of constant + // U map to circles, and lines of constant V map to radial line segments. + let inner_uv = [0., i as f32 / self.resolution as f32]; + let outer_uv = [1., i as f32 / self.resolution as f32]; + uvs.push(inner_uv); + uvs.push(outer_uv); + } + + // Adjacent pairs of vertices form two triangles with each other; here, + // we are just making sure that they both have the right orientation, + // which is the CCW order of + // `inner_vertex` -> `outer_vertex` -> `next_outer` -> `next_inner` + for i in 0..self.resolution { + let inner_vertex = 2 * i; + let outer_vertex = 2 * i + 1; + let next_inner = inner_vertex + 2; + let next_outer = outer_vertex + 2; + indices.extend_from_slice(&[inner_vertex, outer_vertex, next_outer]); + indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]); + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for AnnulusMeshBuilder { + fn perimeter(&self) -> Vec { + let vert_count = 2 * self.resolution; + alloc::vec![ + PerimeterSegment::Smooth { + first_normal: Vec2::NEG_Y, + last_normal: Vec2::NEG_Y, + indices: (0..vert_count).step_by(2).chain([0]).rev().collect(), // Inner hole + }, + PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (1..vert_count).step_by(2).chain([1]).collect(), // Outer perimeter + }, + ] + } +} + +impl Meshable for Annulus { + type Output = AnnulusMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + AnnulusMeshBuilder { + annulus: *self, + ..Default::default() + } + } +} + +/// A builder for creating a [`Mesh`] with an [`Rhombus`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct RhombusMeshBuilder { + half_diagonals: Vec2, +} + +impl Default for RhombusMeshBuilder { + /// Returns the default [`RhombusMeshBuilder`] with a half-horizontal and half-vertical diagonal of `0.5`. + fn default() -> Self { + Self { + half_diagonals: Vec2::splat(0.5), + } + } +} + +impl RhombusMeshBuilder { + /// Creates a new [`RhombusMeshBuilder`] from a horizontal and vertical diagonal size. + /// + /// # Panics + /// + /// Panics in debug mode if `horizontal_diagonal` or `vertical_diagonal` is negative. + pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { + debug_assert!( + horizontal_diagonal >= 0.0, + "rhombus has a negative horizontal size", + ); + debug_assert!( + vertical_diagonal >= 0.0, + "rhombus has a negative vertical size" + ); + + Self { + half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), + } + } +} + +impl MeshBuilder for RhombusMeshBuilder { + fn build(&self) -> Mesh { + let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y]; + let positions = alloc::vec![ + [hhd, 0.0, 0.0], + [0.0, vhd, 0.0], + [-hhd, 0.0, 0.0], + [0.0, -vhd, 0.0], + ]; + let normals = alloc::vec![[0.0, 0.0, 1.0]; 4]; + let uvs = alloc::vec![[1.0, 0.5], [0.5, 0.0], [0.0, 0.5], [0.5, 1.0]]; + let indices = Indices::U32(alloc::vec![2, 0, 1, 2, 3, 0]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Extrudable for RhombusMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 3, 0], + }] + } +} + +impl Meshable for Rhombus { + type Output = RhombusMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Self::Output { + half_diagonals: self.half_diagonals, + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`Triangle2d`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Triangle2dMeshBuilder { + triangle: Triangle2d, +} + +impl Triangle2dMeshBuilder { + /// Creates a new [`Triangle2dMeshBuilder`] from the points `a`, `b`, and `c`. + pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { + Self { + triangle: Triangle2d::new(a, b, c), + } + } +} + +impl Meshable for Triangle2d { + type Output = Triangle2dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Self::Output { triangle: *self } + } +} + +impl MeshBuilder for Triangle2dMeshBuilder { + fn build(&self) -> Mesh { + let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.)); + + let positions: Vec<_> = vertices_3d.into(); + let normals = alloc::vec![[0.0, 0.0, 1.0]; 3]; + + let uvs: Vec<_> = crate::meshing::triangle3d::uv_coords(&Triangle3d::new( + vertices_3d[0], + vertices_3d[1], + vertices_3d[2], + )) + .into(); + + let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; + let indices = if is_ccw { + Indices::U32(alloc::vec![0, 1, 2]) + } else { + Indices::U32(alloc::vec![2, 1, 0]) + }; + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Extrudable for Triangle2dMeshBuilder { + fn perimeter(&self) -> Vec { + let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; + if is_ccw { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 0], + }] + } else { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![2, 1, 0, 2], + }] + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`Rectangle`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct RectangleMeshBuilder { + half_size: Vec2, +} + +impl Default for RectangleMeshBuilder { + /// Returns the default [`RectangleMeshBuilder`] with a half-width and half-height of `0.5`. + fn default() -> Self { + Self { + half_size: Vec2::splat(0.5), + } + } +} + +impl RectangleMeshBuilder { + /// Creates a new [`RectangleMeshBuilder`] from a full width and height. + /// + /// # Panics + /// + /// Panics in debug mode if `width` or `height` is negative. + pub const fn new(width: f32, height: f32) -> Self { + debug_assert!(width >= 0.0, "rectangle has a negative width"); + debug_assert!(height >= 0.0, "rectangle has a negative height"); + + Self { + half_size: Vec2::new(width / 2.0, height / 2.0), + } + } +} + +impl MeshBuilder for RectangleMeshBuilder { + fn build(&self) -> Mesh { + let [hw, hh] = [self.half_size.x, self.half_size.y]; + let positions = alloc::vec![ + [hw, hh, 0.0], + [-hw, hh, 0.0], + [-hw, -hh, 0.0], + [hw, -hh, 0.0], + ]; + let normals = alloc::vec![[0.0, 0.0, 1.0]; 4]; + let uvs = alloc::vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; + let indices = Indices::U32(alloc::vec![0, 1, 2, 0, 2, 3]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Extrudable for RectangleMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 3, 0], + }] + } +} + +impl Meshable for Rectangle { + type Output = RectangleMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + RectangleMeshBuilder { + half_size: self.half_size, + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Capsule2dMeshBuilder { + /// The [`Capsule2d`] shape. + pub capsule: Capsule2d, + /// The number of vertices used for one hemicircle. + /// The total number of vertices for the capsule mesh will be two times the resolution. + /// + /// The default is `16`. + pub resolution: u32, +} + +impl Default for Capsule2dMeshBuilder { + fn default() -> Self { + Self { + capsule: Capsule2d::default(), + resolution: 16, + } + } +} + +impl Capsule2dMeshBuilder { + /// Creates a new [`Capsule2dMeshBuilder`] from a given radius, length, and the number of vertices + /// used for one hemicircle. The total number of vertices for the capsule mesh will be two times the resolution. + #[inline] + pub fn new(radius: f32, length: f32, resolution: u32) -> Self { + Self { + capsule: Capsule2d::new(radius, length), + resolution, + } + } + + /// Sets the number of vertices used for one hemicircle. + /// The total number of vertices for the capsule mesh will be two times the resolution. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl MeshBuilder for Capsule2dMeshBuilder { + fn build(&self) -> Mesh { + // The resolution is the number of vertices for one semicircle + let resolution = self.resolution; + let vertex_count = 2 * resolution; + + // Six extra indices for the two triangles between the semicircles + let mut indices = Vec::with_capacity((resolution as usize - 2) * 2 * 3 + 6); + let mut positions = Vec::with_capacity(vertex_count as usize); + let normals = alloc::vec![[0.0, 0.0, 1.0]; vertex_count as usize]; + let mut uvs = Vec::with_capacity(vertex_count as usize); + + let radius = self.capsule.radius; + let step = core::f32::consts::TAU / vertex_count as f32; + + // If the vertex count is even, offset starting angle of top semicircle by half a step + // to position the vertices evenly. + let start_angle = if vertex_count.is_multiple_of(2) { + step / 2.0 + } else { + 0.0 + }; + + // How much the hemicircle radius is of the total half-height of the capsule. + // This is used to prevent the UVs from stretching between the semicircles. + let radius_frac = self.capsule.radius / (self.capsule.half_length + self.capsule.radius); + + // Create top semicircle + for i in 0..resolution { + // Compute vertex position at angle theta + let theta = start_angle + i as f32 * step; + let (sin, cos) = ops::sin_cos(theta); + let (x, y) = (cos * radius, sin * radius + self.capsule.half_length); + + positions.push([x, y, 0.0]); + uvs.push([0.5 * (cos + 1.0), radius_frac * (1.0 - 0.5 * (sin + 1.0))]); + } + + // Add top semicircle indices + for i in 1..resolution - 1 { + indices.extend_from_slice(&[0, i, i + 1]); + } + + // Add indices for top left triangle of the part between the semicircles + indices.extend_from_slice(&[0, resolution - 1, resolution]); + + // Create bottom semicircle + for i in resolution..vertex_count { + // Compute vertex position at angle theta + let theta = start_angle + i as f32 * step; + let (sin, cos) = ops::sin_cos(theta); + let (x, y) = (cos * radius, sin * radius - self.capsule.half_length); + + positions.push([x, y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - radius_frac * 0.5 * (sin + 1.0)]); + } + + // Add bottom semicircle indices + for i in 1..resolution - 1 { + indices.extend_from_slice(&[resolution, resolution + i, resolution + i + 1]); + } + + // Add indices for bottom right triangle of the part between the semicircles + indices.extend_from_slice(&[resolution, vertex_count - 1, 0]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } +} + +impl Extrudable for Capsule2dMeshBuilder { + fn perimeter(&self) -> Vec { + let resolution = self.resolution; + let top_semi_indices = (0..resolution).collect(); + let bottom_semi_indices = (resolution..(2 * resolution)).collect(); + alloc::vec![ + PerimeterSegment::Smooth { + first_normal: Vec2::X, + last_normal: Vec2::NEG_X, + indices: top_semi_indices, + }, // Top semi-circle + PerimeterSegment::Flat { + indices: alloc::vec![resolution - 1, resolution], + }, // Left edge + PerimeterSegment::Smooth { + first_normal: Vec2::NEG_X, + last_normal: Vec2::X, + indices: bottom_semi_indices, + }, // Bottom semi-circle + PerimeterSegment::Flat { + indices: alloc::vec![2 * resolution - 1, 0], + }, // Right edge + ] + } +} + +impl Meshable for Capsule2d { + type Output = Capsule2dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Capsule2dMeshBuilder { + capsule: *self, + ..Default::default() + } + } +} + +/// A builder used for creating a [`Mesh`] with a [`Ring`] shape. +pub struct RingMeshBuilder

+where + P: Primitive2d + Meshable, +{ + pub outer_shape_builder: P::Output, + pub inner_shape_builder: P::Output, +} + +impl

RingMeshBuilder

+where + P: Primitive2d + Meshable, +{ + /// Create a new `RingMeshBuilder

` from a given `Ring

` shape. + pub fn new(ring: &Ring

) -> Self { + Self { + outer_shape_builder: ring.outer_shape.mesh_builder(), + inner_shape_builder: ring.inner_shape.mesh_builder(), + } + } + + /// Apply a function to the inner builders + pub fn with_inner(mut self, func: impl Fn(P::Output) -> P::Output) -> Self { + self.outer_shape_builder = func(self.outer_shape_builder); + self.inner_shape_builder = func(self.inner_shape_builder); + self + } + + fn get_vertex_attributes(&self) -> Option { + fn get_positions(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { + if let VertexAttributeValues::Float32x3(data) = + mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)? + { + Some(data) + } else { + None + } + } + + fn get_uvs(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 2]>> { + if let VertexAttributeValues::Float32x2(data) = + mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0)? + { + Some(data) + } else { + None + } + } + + fn get_normals(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { + if let VertexAttributeValues::Float32x3(data) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL)? + { + Some(data) + } else { + None + } + } + + let mut outer = self.outer_shape_builder.build(); + let mut inner = self.inner_shape_builder.build(); + + assert_eq!( + outer.primitive_topology(), + PrimitiveTopology::TriangleList, + "PrimitiveTopology must be a TriangleList, mesh builder not compatible" + ); + assert_eq!( + inner.primitive_topology(), + PrimitiveTopology::TriangleList, + "PrimitiveTopology must be a TriangleList, mesh builder not compatible" + ); + + Some(RingMeshBuilderVertexAttributes { + outer_positions: mem::take(get_positions(&mut outer)?), + inner_positions: mem::take(get_positions(&mut inner)?), + outer_normals: mem::take(get_normals(&mut outer)?), + inner_normals: mem::take(get_normals(&mut inner)?), + outer_uvs: mem::take(get_uvs(&mut outer)?), + inner_uvs: mem::take(get_uvs(&mut inner)?), + }) + } +} + +struct RingMeshBuilderVertexAttributes { + outer_positions: Vec<[f32; 3]>, + inner_positions: Vec<[f32; 3]>, + outer_normals: Vec<[f32; 3]>, + inner_normals: Vec<[f32; 3]>, + outer_uvs: Vec<[f32; 2]>, + inner_uvs: Vec<[f32; 2]>, +} + +impl

MeshBuilder for RingMeshBuilder

+where + P: Primitive2d + Meshable, +{ + /// Builds a [`Mesh`] based on the configuration in `self`. + /// + /// # Panics + /// + /// Panics if the following assumptions are not met. + /// + /// It is assumed that the inner and outer meshes have the same number of vertices. + /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated + /// a different number of vertices for the inner and outer instances of the primitive. + /// + /// It is assumed that the `primitive_topology` of the mesh returned by + /// the underlying builder is [`PrimitiveTopology::TriangleList`] + /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. + fn build(&self) -> Mesh { + if let Some(RingMeshBuilderVertexAttributes { + outer_uvs, + inner_uvs, + outer_positions, + inner_positions, + outer_normals, + inner_normals, + }) = self.get_vertex_attributes() + && outer_uvs.len() == inner_uvs.len() + && outer_positions.len() == inner_positions.len() + && outer_normals.len() == inner_normals.len() + { + let mut uvs = outer_uvs; + let inner_uvs = inner_positions + .iter() + .zip(&outer_positions) + .zip(&inner_uvs) + .map(|((inner_position, outer_position), inner_uv)| -> [f32; 2] { + const UV_CENTER: Vec2 = Vec2::splat(0.5); + let ip = Vec3::from(*inner_position).truncate(); + let op = Vec3::from(*outer_position).truncate(); + let uv = Vec2::from(*inner_uv) - UV_CENTER; + (uv * ip.length() / op.length() + UV_CENTER).into() + }); + uvs.extend(inner_uvs); + + let mut normals = outer_normals; + normals.extend(inner_normals); + + let points = outer_positions.len() as u32; + let mut indices = Vec::with_capacity(outer_positions.len() * 6); + for i in 0..points { + // for five points: + indices.push(i); // 0 1 2 3 4 + indices.push(i + 1); // 1 2 3 4 0 <- + indices.push(points + i); // 0' 1' 2' 3' 4' + indices.push(points + i); // 0' 1' 2' 3' 4' + indices.push(i + 1); // 1 2 3 4 0 <- + indices.push(points + i + 1); // 1' 2' 3' 4' 0' <- + } + let indices_length = indices.len(); + // Fix up the last pair of triangles (return to start) + if let (_, [_, b, _, _, e, f]) = indices.split_at_mut(indices_length.saturating_sub(6)) + { + *b = 0; + *e = 0; + *f = points; + } + + let mut positions = outer_positions; + positions.extend_from_slice(&inner_positions); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(Indices::U32(indices)) + } else { + panic!("The inner and outer meshes should have the same number of vertices, and have required attributes"); + } + } +} + +impl

Extrudable for RingMeshBuilder

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + /// A list of the indices each representing a part of the perimeter of the mesh. + /// + /// # Panics + /// + /// Panics if the following assumptions are not met. + /// + /// It is assumed that the inner and outer meshes have the same number of vertices. + /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated + /// a different number of vertices for the inner and outer instances of the primitive. + /// + /// It is assumed that the `primitive_topology` of the mesh returned by + /// the underlying builder is [`PrimitiveTopology::TriangleList`] + /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. + fn perimeter(&self) -> Vec { + let outer_vertex_count = self + .get_vertex_attributes() + .filter(|r| r.outer_positions.len() == r.inner_positions.len()) + .expect("The inner and outer meshes should have the same number of vertices, and have required attributes") + .outer_positions + .len(); + + let mut outer_perimeter = self.outer_shape_builder.perimeter(); + let inner_perimeter = + self.inner_shape_builder + .perimeter() + .into_iter() + .rev() + .map(|segment| match segment { + PerimeterSegment::Smooth { + first_normal, + last_normal, + mut indices, + } => PerimeterSegment::Smooth { + first_normal: -last_normal, + last_normal: -first_normal, + indices: { + let outer_perimeter_vertex_count = outer_vertex_count as u32; + indices.reverse(); + for i in &mut indices { + *i += outer_perimeter_vertex_count; + } + indices + }, + }, + PerimeterSegment::Flat { mut indices } => PerimeterSegment::Flat { + indices: { + let outer_perimeter_vertex_count = outer_vertex_count as u32; + indices.reverse(); + for i in &mut indices { + *i += outer_perimeter_vertex_count; + } + indices + }, + }, + }); + + outer_perimeter.extend(inner_perimeter); + outer_perimeter + } +} + +impl

Meshable for Ring

+where + P: Primitive2d + Meshable, +{ + type Output = RingMeshBuilder

; + + fn mesh_builder(&self) -> Self::Output { + RingMeshBuilder::new(self) + } +} + +#[cfg(test)] +mod tests { + use crate::meshing::{MeshBuilder, Meshable}; + use crate::primitives::{Annulus, ConvexPolygon, RegularPolygon}; + use bevy_math::{FloatOrd, Vec2}; + use bevy_platform::collections::HashSet; + + use bevy_mesh::{Mesh, VertexAttributeValues}; + + fn count_distinct_positions(points: &[[f32; 3]]) -> usize { + let mut map = >::default(); + for point in points { + map.insert(point.map(FloatOrd)); + } + map.len() + } + + #[test] + fn test_annulus() { + let mesh = Annulus::new(1.0, 1.2).mesh_builder().resolution(16).build(); + + assert_eq!( + 32, + count_distinct_positions( + mesh.attribute(Mesh::ATTRIBUTE_POSITION) + .unwrap() + .as_float3() + .unwrap() + ) + ); + } + + /// Sin/cos and multiplication computations result in numbers like 0.4999999. + /// Round these to numbers we expect like 0.5. + fn fix_floats(points: &mut [[f32; N]]) { + for point in points.iter_mut() { + for coord in point.iter_mut() { + let round = (*coord * 2.).round() / 2.; + if (*coord - round).abs() < 0.00001 { + *coord = round; + } + } + } + } + + #[test] + fn test_regular_polygon() { + let mut mesh = RegularPolygon::new(7.0, 4).mesh(); + + let Some(VertexAttributeValues::Float32x3(mut positions)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) + else { + panic!("Expected positions f32x3"); + }; + let Some(VertexAttributeValues::Float32x2(mut uvs)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_UV_0) + else { + panic!("Expected uvs f32x2"); + }; + let Some(VertexAttributeValues::Float32x3(normals)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) + else { + panic!("Expected normals f32x3"); + }; + + fix_floats(&mut positions); + fix_floats(&mut uvs); + + assert_eq!( + [ + [0.0, 7.0, 0.0], + [-7.0, 0.0, 0.0], + [0.0, -7.0, 0.0], + [7.0, 0.0, 0.0], + ], + &positions[..] + ); + + // Note V coordinate increases in the opposite direction to the Y coordinate. + assert_eq!([[0.5, 0.0], [0.0, 0.5], [0.5, 1.0], [1.0, 0.5],], &uvs[..]); + + assert_eq!(&[[0.0, 0.0, 1.0]; 4], &normals[..]); + } + + #[test] + fn test_convex_polygon() { + let polygon = ConvexPolygon::new(alloc::vec![ + Vec2::new(-2.0, -1.0), + Vec2::new(2.0, -1.0), + Vec2::new(1.0, 3.0), + Vec2::new(-1.0, 2.0), + ]) + .unwrap(); + + let mut mesh = polygon.mesh(); + + let Some(VertexAttributeValues::Float32x3(mut positions)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) + else { + panic!("Expected positions f32x3"); + }; + let Some(VertexAttributeValues::Float32x2(mut uvs)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_UV_0) + else { + panic!("Expected uvs f32x2"); + }; + let Some(VertexAttributeValues::Float32x3(normals)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) + else { + panic!("Expected normals f32x3"); + }; + + fix_floats(&mut positions); + fix_floats(&mut uvs); + + assert_eq!( + [ + [-2.0, -1.0, 0.0], + [2.0, -1.0, 0.0], + [1.0, 3.0, 0.0], + [-1.0, 2.0, 0.0], + ], + &positions[..] + ); + + assert_eq!( + [[0.0, 0.0], [1.0, 0.0], [0.75, 1.0], [0.25, 0.75]], + &uvs[..] + ); + + assert_eq!(&[[0.0, 0.0, 1.0]; 4], &normals[..]); + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/capsule.rs b/crates/bevy_shapes/src/meshing/dim3/capsule.rs new file mode 100644 index 0000000000000..083fdf4710ed6 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/capsule.rs @@ -0,0 +1,436 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Capsule3d, +}; +use bevy_asset::RenderAssetUsages; +use bevy_math::{ops, Vec2, Vec3}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +use alloc::vec::Vec; + +/// Manner in which UV coordinates are distributed vertically. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub enum CapsuleUvProfile { + /// UV space is distributed by how much of the capsule consists of the hemispheres. + #[default] + Aspect, + /// Hemispheres get UV space according to the ratio of latitudes to rings. + Uniform, + /// Upper third of the texture goes to the northern hemisphere, middle third to the cylinder + /// and lower third to the southern one. + Fixed, +} + +/// A builder used for creating a [`Mesh`] with a [`Capsule3d`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Capsule3dMeshBuilder { + /// The [`Capsule3d`] shape. + pub capsule: Capsule3d, + /// The number of horizontal lines subdividing the cylindrical part of the capsule. + /// The default is `0`. + pub rings: u32, + /// The number of vertical lines subdividing the hemispheres of the capsule. + /// The default is `32`. + pub longitudes: u32, + /// The number of horizontal lines subdividing the hemispheres of the capsule. + /// The default is `16`. + pub latitudes: u32, + /// The manner in which UV coordinates are distributed vertically. + /// The default is [`CapsuleUvProfile::Aspect`]. + pub uv_profile: CapsuleUvProfile, +} + +impl Default for Capsule3dMeshBuilder { + fn default() -> Self { + Self { + capsule: Capsule3d::default(), + rings: 0, + longitudes: 32, + latitudes: 16, + uv_profile: CapsuleUvProfile::default(), + } + } +} + +impl Capsule3dMeshBuilder { + /// Creates a new [`Capsule3dMeshBuilder`] from a given radius, height, longitudes, and latitudes. + /// + /// Note that `height` is the distance between the centers of the hemispheres. + /// `radius` will be added to both ends to get the real height of the mesh. + #[inline] + pub fn new(radius: f32, height: f32, longitudes: u32, latitudes: u32) -> Self { + Self { + capsule: Capsule3d::new(radius, height), + longitudes, + latitudes, + ..Default::default() + } + } + + /// Sets the number of horizontal lines subdividing the cylindrical part of the capsule. + #[inline] + pub const fn rings(mut self, rings: u32) -> Self { + self.rings = rings; + self + } + + /// Sets the number of vertical lines subdividing the hemispheres of the capsule. + #[inline] + pub const fn longitudes(mut self, longitudes: u32) -> Self { + self.longitudes = longitudes; + self + } + + /// Sets the number of horizontal lines subdividing the hemispheres of the capsule. + #[inline] + pub const fn latitudes(mut self, latitudes: u32) -> Self { + self.latitudes = latitudes; + self + } + + /// Sets the manner in which UV coordinates are distributed vertically. + #[inline] + pub const fn uv_profile(mut self, uv_profile: CapsuleUvProfile) -> Self { + self.uv_profile = uv_profile; + self + } +} + +impl MeshBuilder for Capsule3dMeshBuilder { + fn build(&self) -> Mesh { + // code adapted from https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db + let Capsule3dMeshBuilder { + capsule, + rings, + longitudes, + latitudes, + uv_profile, + } = *self; + let Capsule3d { + radius, + half_length, + } = capsule; + + let calc_middle = rings > 0; + let half_lats = latitudes / 2; + let half_latsn1 = half_lats - 1; + let half_latsn2 = half_lats - 2; + let ringsp1 = rings + 1; + let lonsp1 = longitudes + 1; + let summit = half_length + radius; + + // Vertex index offsets. + let vert_offset_north_hemi = longitudes; + let vert_offset_north_equator = vert_offset_north_hemi + lonsp1 * half_latsn1; + let vert_offset_cylinder = vert_offset_north_equator + lonsp1; + let vert_offset_south_equator = if calc_middle { + vert_offset_cylinder + lonsp1 * rings + } else { + vert_offset_cylinder + }; + let vert_offset_south_hemi = vert_offset_south_equator + lonsp1; + let vert_offset_south_polar = vert_offset_south_hemi + lonsp1 * half_latsn2; + let vert_offset_south_cap = vert_offset_south_polar + lonsp1; + + // Initialize arrays. + let vert_len = (vert_offset_south_cap + longitudes) as usize; + + let mut vs: Vec = alloc::vec![Vec3::ZERO; vert_len]; + let mut vts: Vec = alloc::vec![Vec2::ZERO; vert_len]; + let mut vns: Vec = alloc::vec![Vec3::ZERO; vert_len]; + + let to_theta = 2.0 * core::f32::consts::PI / longitudes as f32; + let to_phi = core::f32::consts::PI / latitudes as f32; + let to_tex_horizontal = 1.0 / longitudes as f32; + let to_tex_vertical = 1.0 / half_lats as f32; + + let vt_aspect_ratio = match uv_profile { + CapsuleUvProfile::Aspect => radius / (2.0 * half_length + radius + radius), + CapsuleUvProfile::Uniform => half_lats as f32 / (ringsp1 + latitudes) as f32, + CapsuleUvProfile::Fixed => 1.0 / 3.0, + }; + let vt_aspect_north = 1.0 - vt_aspect_ratio; + let vt_aspect_south = vt_aspect_ratio; + + let mut theta_cartesian: Vec = alloc::vec![Vec2::ZERO; longitudes as usize]; + let mut rho_theta_cartesian: Vec = alloc::vec![Vec2::ZERO; longitudes as usize]; + let mut s_texture_cache: Vec = alloc::vec![0.0; lonsp1 as usize]; + + for j in 0..longitudes as usize { + let jf = j as f32; + let s_texture_polar = 1.0 - ((jf + 0.5) * to_tex_horizontal); + let theta = jf * to_theta; + + theta_cartesian[j] = Vec2::from_angle(theta); + rho_theta_cartesian[j] = radius * theta_cartesian[j]; + + // North. + vs[j] = Vec3::new(0.0, summit, 0.0); + vts[j] = Vec2::new(s_texture_polar, 1.0); + vns[j] = Vec3::Y; + + // South. + let idx = vert_offset_south_cap as usize + j; + vs[idx] = Vec3::new(0.0, -summit, 0.0); + vts[idx] = Vec2::new(s_texture_polar, 0.0); + vns[idx] = Vec3::new(0.0, -1.0, 0.0); + } + + // Equatorial vertices. + for (j, s_texture_cache_j) in s_texture_cache.iter_mut().enumerate().take(lonsp1 as usize) { + let s_texture = 1.0 - j as f32 * to_tex_horizontal; + *s_texture_cache_j = s_texture; + + // Wrap to first element upon reaching last. + let j_mod = j % longitudes as usize; + let tc = theta_cartesian[j_mod]; + let rtc = rho_theta_cartesian[j_mod]; + + // North equator. + let idxn = vert_offset_north_equator as usize + j; + vs[idxn] = Vec3::new(rtc.x, half_length, -rtc.y); + vts[idxn] = Vec2::new(s_texture, vt_aspect_north); + vns[idxn] = Vec3::new(tc.x, 0.0, -tc.y); + + // South equator. + let idxs = vert_offset_south_equator as usize + j; + vs[idxs] = Vec3::new(rtc.x, -half_length, -rtc.y); + vts[idxs] = Vec2::new(s_texture, vt_aspect_south); + vns[idxs] = Vec3::new(tc.x, 0.0, -tc.y); + } + + // Hemisphere vertices. + for i in 0..half_latsn1 { + let ip1f = i as f32 + 1.0; + let phi = ip1f * to_phi; + + // For coordinates. + let (sin_phi_south, cos_phi_south) = ops::sin_cos(phi); + + // Symmetrical hemispheres mean cosine and sine only needs + // to be calculated once. + let cos_phi_north = sin_phi_south; + let sin_phi_north = -cos_phi_south; + + let rho_cos_phi_north = radius * cos_phi_north; + let rho_sin_phi_north = radius * sin_phi_north; + let z_offset_north = half_length - rho_sin_phi_north; + + let rho_cos_phi_south = radius * cos_phi_south; + let rho_sin_phi_south = radius * sin_phi_south; + let z_offset_sout = -half_length - rho_sin_phi_south; + + // For texture coordinates. + let t_tex_fac = ip1f * to_tex_vertical; + let cmpl_tex_fac = 1.0 - t_tex_fac; + let t_tex_north = cmpl_tex_fac + vt_aspect_north * t_tex_fac; + let t_tex_south = cmpl_tex_fac * vt_aspect_south; + + let i_lonsp1 = i * lonsp1; + let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1; + let vert_curr_lat_south = vert_offset_south_hemi + i_lonsp1; + + for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1 as usize) { + let j_mod = j % longitudes as usize; + + let tc = theta_cartesian[j_mod]; + + // North hemisphere. + let idxn = vert_curr_lat_north as usize + j; + vs[idxn] = Vec3::new( + rho_cos_phi_north * tc.x, + z_offset_north, + -rho_cos_phi_north * tc.y, + ); + vts[idxn] = Vec2::new(*s_texture, t_tex_north); + vns[idxn] = Vec3::new(cos_phi_north * tc.x, -sin_phi_north, -cos_phi_north * tc.y); + + // South hemisphere. + let idxs = vert_curr_lat_south as usize + j; + vs[idxs] = Vec3::new( + rho_cos_phi_south * tc.x, + z_offset_sout, + -rho_cos_phi_south * tc.y, + ); + vts[idxs] = Vec2::new(*s_texture, t_tex_south); + vns[idxs] = Vec3::new(cos_phi_south * tc.x, -sin_phi_south, -cos_phi_south * tc.y); + } + } + + // Cylinder vertices. + if calc_middle { + // Exclude both origin and destination edges + // (North and South equators) from the interpolation. + let to_fac = 1.0 / ringsp1 as f32; + let mut idx_cyl_lat = vert_offset_cylinder as usize; + + for h in 1..ringsp1 { + let fac = h as f32 * to_fac; + let cmpl_fac = 1.0 - fac; + let t_texture = cmpl_fac * vt_aspect_north + fac * vt_aspect_south; + let z = half_length - 2.0 * half_length * fac; + + for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1 as usize) { + let j_mod = j % longitudes as usize; + let tc = theta_cartesian[j_mod]; + let rtc = rho_theta_cartesian[j_mod]; + + vs[idx_cyl_lat] = Vec3::new(rtc.x, z, -rtc.y); + vts[idx_cyl_lat] = Vec2::new(*s_texture, t_texture); + vns[idx_cyl_lat] = Vec3::new(tc.x, 0.0, -tc.y); + + idx_cyl_lat += 1; + } + } + } + + // Triangle indices. + + // Stride is 3 for polar triangles; + // stride is 6 for two triangles forming a quad. + let lons3 = longitudes * 3; + let lons6 = longitudes * 6; + let hemi_lons = half_latsn1 * lons6; + + let tri_offset_north_hemi = lons3; + let tri_offset_cylinder = tri_offset_north_hemi + hemi_lons; + let tri_offset_south_hemi = tri_offset_cylinder + ringsp1 * lons6; + let tri_offset_south_cap = tri_offset_south_hemi + hemi_lons; + + let fs_len = tri_offset_south_cap + lons3; + let mut tris: Vec = alloc::vec![0; fs_len as usize]; + + // Polar caps. + let mut i = 0; + let mut k = 0; + let mut m = tri_offset_south_cap as usize; + while i < longitudes { + // North. + tris[k] = i; + tris[k + 1] = vert_offset_north_hemi + i; + tris[k + 2] = vert_offset_north_hemi + i + 1; + + // South. + tris[m] = vert_offset_south_cap + i; + tris[m + 1] = vert_offset_south_polar + i + 1; + tris[m + 2] = vert_offset_south_polar + i; + + i += 1; + k += 3; + m += 3; + } + + // Hemispheres. + + let mut i = 0; + let mut k = tri_offset_north_hemi as usize; + let mut m = tri_offset_south_hemi as usize; + + while i < half_latsn1 { + let i_lonsp1 = i * lonsp1; + + let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1; + let vert_next_lat_north = vert_curr_lat_north + lonsp1; + + let vert_curr_lat_south = vert_offset_south_equator + i_lonsp1; + let vert_next_lat_south = vert_curr_lat_south + lonsp1; + + let mut j = 0; + while j < longitudes { + // North. + let north00 = vert_curr_lat_north + j; + let north01 = vert_next_lat_north + j; + let north11 = vert_next_lat_north + j + 1; + let north10 = vert_curr_lat_north + j + 1; + + tris[k] = north00; + tris[k + 1] = north11; + tris[k + 2] = north10; + + tris[k + 3] = north00; + tris[k + 4] = north01; + tris[k + 5] = north11; + + // South. + let south00 = vert_curr_lat_south + j; + let south01 = vert_next_lat_south + j; + let south11 = vert_next_lat_south + j + 1; + let south10 = vert_curr_lat_south + j + 1; + + tris[m] = south00; + tris[m + 1] = south11; + tris[m + 2] = south10; + + tris[m + 3] = south00; + tris[m + 4] = south01; + tris[m + 5] = south11; + + j += 1; + k += 6; + m += 6; + } + + i += 1; + } + + // Cylinder. + let mut i = 0; + let mut k = tri_offset_cylinder as usize; + + while i < ringsp1 { + let vert_curr_lat = vert_offset_north_equator + i * lonsp1; + let vert_next_lat = vert_curr_lat + lonsp1; + + let mut j = 0; + while j < longitudes { + let cy00 = vert_curr_lat + j; + let cy01 = vert_next_lat + j; + let cy11 = vert_next_lat + j + 1; + let cy10 = vert_curr_lat + j + 1; + + tris[k] = cy00; + tris[k + 1] = cy11; + tris[k + 2] = cy10; + + tris[k + 3] = cy00; + tris[k + 4] = cy01; + tris[k + 5] = cy11; + + j += 1; + k += 6; + } + + i += 1; + } + + let vs: Vec<[f32; 3]> = vs.into_iter().map(Into::into).collect(); + let vns: Vec<[f32; 3]> = vns.into_iter().map(Into::into).collect(); + let vts: Vec<[f32; 2]> = vts.into_iter().map(Into::into).collect(); + + assert_eq!(vs.len(), vert_len); + assert_eq!(tris.len(), fs_len as usize); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts) + .with_inserted_indices(Indices::U32(tris)) + } +} + +impl Meshable for Capsule3d { + type Output = Capsule3dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Capsule3dMeshBuilder { + capsule: *self, + ..Default::default() + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cone.rs b/crates/bevy_shapes/src/meshing/dim3/cone.rs new file mode 100644 index 0000000000000..31672b1dbe27d --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/cone.rs @@ -0,0 +1,274 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Cone, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::{ops, Vec3}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// Anchoring options for [`ConeMeshBuilder`] +#[derive(Debug, Copy, Clone, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub enum ConeAnchor { + #[default] + /// Midpoint between the tip of the cone and the center of its base. + MidPoint, + /// The Tip of the triangle + Tip, + /// The center of the base circle + Base, +} + +/// A builder used for creating a [`Mesh`] with a [`Cone`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct ConeMeshBuilder { + /// The [`Cone`] shape. + pub cone: Cone, + /// The number of vertices used for the base of the cone. + /// + /// The default is `32`. + pub resolution: u32, + /// The anchor point for the cone mesh, defaults to the midpoint between + /// the tip of the cone and the center of its base + pub anchor: ConeAnchor, +} + +impl Default for ConeMeshBuilder { + fn default() -> Self { + Self { + cone: Cone::default(), + resolution: 32, + anchor: ConeAnchor::default(), + } + } +} + +impl ConeMeshBuilder { + /// Creates a new [`ConeMeshBuilder`] from a given radius, height, + /// and number of vertices used for the base of the cone. + #[inline] + pub const fn new(radius: f32, height: f32, resolution: u32) -> Self { + Self { + cone: Cone { radius, height }, + resolution, + anchor: ConeAnchor::MidPoint, + } + } + + /// Sets the number of vertices used for the base of the cone. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets a custom anchor point for the mesh + #[inline] + pub const fn anchor(mut self, anchor: ConeAnchor) -> Self { + self.anchor = anchor; + self + } +} + +impl MeshBuilder for ConeMeshBuilder { + fn build(&self) -> Mesh { + let half_height = self.cone.height / 2.0; + + // `resolution` vertices for the base, `resolution` vertices for the bottom of the lateral surface, + // and one vertex for the tip. + let num_vertices = self.resolution as usize * 2 + 1; + let num_indices = self.resolution as usize * 6 - 6; + + let mut positions = Vec::with_capacity(num_vertices); + let mut normals = Vec::with_capacity(num_vertices); + let mut uvs = Vec::with_capacity(num_vertices); + let mut indices = Vec::with_capacity(num_indices); + + // Tip + positions.push([0.0, half_height, 0.0]); + + // The tip doesn't have a singular normal that works correctly. + // We use an invalid normal here so that it becomes NaN in the fragment shader + // and doesn't affect the overall shading. This might seem hacky, but it's one of + // the only ways to get perfectly smooth cones without creases or other shading artifacts. + // + // Note that this requires that normals are not normalized in the vertex shader, + // as that would make the entire triangle invalid and make the cone appear as black. + normals.push([0.0, 0.0, 0.0]); + + // The UVs of the cone are in polar coordinates, so it's like projecting a circle texture from above. + // The center of the texture is at the center of the lateral surface, at the tip of the cone. + uvs.push([0.5, 0.5]); + + // Now we build the lateral surface, the side of the cone. + + // The vertex normals will be perpendicular to the surface. + // + // Here we get the slope of a normal and use it for computing + // the multiplicative inverse of the length of a vector in the direction + // of the normal. This allows us to normalize vertex normals efficiently. + let normal_slope = self.cone.radius / self.cone.height; + // Equivalent to Vec2::new(1.0, slope).length().recip() + let normalization_factor = (1.0 + normal_slope * normal_slope).sqrt().recip(); + + // How much the angle changes at each step + let step_theta = core::f32::consts::TAU / self.resolution as f32; + + // Add vertices for the bottom of the lateral surface. + for segment in 0..self.resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + // The vertex normal perpendicular to the side + let normal = Vec3::new(cos, normal_slope, sin) * normalization_factor; + + positions.push([self.cone.radius * cos, -half_height, self.cone.radius * sin]); + normals.push(normal.to_array()); + uvs.push([0.5 + cos * 0.5, 0.5 + sin * 0.5]); + } + + // Add indices for the lateral surface. Each triangle is formed by the tip + // and two vertices at the base. + for j in 1..self.resolution { + indices.extend_from_slice(&[0, j + 1, j]); + } + + // Close the surface with a triangle between the tip, first base vertex, and last base vertex. + indices.extend_from_slice(&[0, 1, self.resolution]); + + // Now we build the actual base of the cone. + + let index_offset = positions.len() as u32; + + // Add base vertices. + for i in 0..self.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + positions.push([cos * self.cone.radius, -half_height, sin * self.cone.radius]); + normals.push([0.0, -1.0, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + // Add base indices. + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[index_offset, index_offset + i, index_offset + i + 1]); + } + + // Offset the vertex positions Y axis to match the anchor + match self.anchor { + ConeAnchor::Tip => positions.iter_mut().for_each(|p| p[1] -= half_height), + ConeAnchor::Base => positions.iter_mut().for_each(|p| p[1] += half_height), + ConeAnchor::MidPoint => (), + }; + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Cone { + type Output = ConeMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + ConeMeshBuilder { + cone: *self, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Cone, + }; + use bevy_math::Vec2; + use bevy_mesh::{Mesh, VertexAttributeValues}; + + /// Rounds floats to handle floating point error in tests. + fn round_floats(points: &mut [[f32; N]]) { + for point in points.iter_mut() { + for coord in point.iter_mut() { + let round = (*coord * 100.0).round() / 100.0; + if (*coord - round).abs() < 0.00001 { + *coord = round; + } + } + } + } + + #[test] + fn cone_mesh() { + let mut mesh = Cone { + radius: 0.5, + height: 1.0, + } + .mesh_builder() + .resolution(4) + .build(); + + let Some(VertexAttributeValues::Float32x3(mut positions)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) + else { + panic!("Expected positions f32x3"); + }; + let Some(VertexAttributeValues::Float32x3(mut normals)) = + mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) + else { + panic!("Expected normals f32x3"); + }; + + round_floats(&mut positions); + round_floats(&mut normals); + + // Vertex positions + assert_eq!( + [ + // Tip + [0.0, 0.5, 0.0], + // Lateral surface + [0.5, -0.5, 0.0], + [0.0, -0.5, 0.5], + [-0.5, -0.5, 0.0], + [0.0, -0.5, -0.5], + // Base + [0.5, -0.5, 0.0], + [0.0, -0.5, 0.5], + [-0.5, -0.5, 0.0], + [0.0, -0.5, -0.5], + ], + &positions[..] + ); + + // Vertex normals + let [x, y] = Vec2::new(0.5, -1.0).perp().normalize().to_array(); + assert_eq!( + &[ + // Tip + [0.0, 0.0, 0.0], + // Lateral surface + [x, y, 0.0], + [0.0, y, x], + [-x, y, 0.0], + [0.0, y, -x], + // Base + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + ], + &normals[..] + ); + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs new file mode 100644 index 0000000000000..8677cbaed3de1 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs @@ -0,0 +1,183 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::ConicalFrustum, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::{ops, Vec3}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`ConicalFrustum`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct ConicalFrustumMeshBuilder { + /// The [`ConicalFrustum`] shape. + pub frustum: ConicalFrustum, + /// The number of vertices used for the top and bottom of the conical frustum. + /// + /// The default is `32`. + pub resolution: u32, + /// The number of horizontal lines subdividing the lateral surface of the conical frustum. + /// + /// The default is `1`. + pub segments: u32, +} + +impl Default for ConicalFrustumMeshBuilder { + fn default() -> Self { + Self { + frustum: ConicalFrustum::default(), + resolution: 32, + segments: 1, + } + } +} + +impl ConicalFrustumMeshBuilder { + /// Creates a new [`ConicalFrustumMeshBuilder`] from the given top and bottom radii, a height, + /// and a resolution used for the top and bottom. + #[inline] + pub const fn new(radius_top: f32, radius_bottom: f32, height: f32, resolution: u32) -> Self { + Self { + frustum: ConicalFrustum { + radius_top, + radius_bottom, + height, + }, + resolution, + segments: 1, + } + } + + /// Sets the number of vertices used for the top and bottom of the conical frustum. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets the number of horizontal lines subdividing the lateral surface of the conical frustum. + #[inline] + pub const fn segments(mut self, segments: u32) -> Self { + self.segments = segments; + self + } +} + +impl MeshBuilder for ConicalFrustumMeshBuilder { + fn build(&self) -> Mesh { + debug_assert!(self.resolution > 2); + debug_assert!(self.segments > 0); + + let ConicalFrustum { + radius_top, + radius_bottom, + height, + } = self.frustum; + let half_height = height / 2.0; + + let num_rings = self.segments + 1; + let num_vertices = (self.resolution * 2 + num_rings * (self.resolution + 1)) as usize; + let num_faces = self.resolution * (num_rings - 2); + let num_indices = ((2 * num_faces + 2 * (self.resolution - 1) * 2) * 3) as usize; + + let mut positions = Vec::with_capacity(num_vertices); + let mut normals = Vec::with_capacity(num_vertices); + let mut uvs = Vec::with_capacity(num_vertices); + let mut indices = Vec::with_capacity(num_indices); + + let step_theta = core::f32::consts::TAU / self.resolution as f32; + let step_y = height / self.segments as f32; + let step_radius = (radius_top - radius_bottom) / self.segments as f32; + + // Rings + for ring in 0..num_rings { + let y = -half_height + ring as f32 * step_y; + let radius = radius_bottom + ring as f32 * step_radius; + + for segment in 0..=self.resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + positions.push([radius * cos, y, radius * sin]); + normals.push( + Vec3::new(cos, (radius_bottom - radius_top) / height, sin) + .normalize() + .to_array(), + ); + uvs.push([ + segment as f32 / self.resolution as f32, + ring as f32 / self.segments as f32, + ]); + } + } + + // Lateral surface + for i in 0..self.segments { + let ring = i * (self.resolution + 1); + let next_ring = (i + 1) * (self.resolution + 1); + + for j in 0..self.resolution { + indices.extend_from_slice(&[ + ring + j, + next_ring + j, + ring + j + 1, + next_ring + j, + next_ring + j + 1, + ring + j + 1, + ]); + } + } + + // Caps + let mut build_cap = |top: bool, radius: f32| { + let offset = positions.len() as u32; + let (y, normal_y, winding) = if top { + (half_height, 1.0, (1, 0)) + } else { + (-half_height, -1.0, (0, 1)) + }; + + for i in 0..self.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + positions.push([cos * radius, y, sin * radius]); + normals.push([0.0, normal_y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[ + offset, + offset + i + winding.0, + offset + i + winding.1, + ]); + } + }; + + build_cap(true, radius_top); + build_cap(false, radius_bottom); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for ConicalFrustum { + type Output = ConicalFrustumMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + ConicalFrustumMeshBuilder { + frustum: *self, + ..Default::default() + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs new file mode 100644 index 0000000000000..51d9af01c5db1 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs @@ -0,0 +1,98 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Cuboid, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::Vec3; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct CuboidMeshBuilder { + half_size: Vec3, +} + +impl Default for CuboidMeshBuilder { + /// Returns the default [`CuboidMeshBuilder`] with a width, height, and depth of `1.0`. + fn default() -> Self { + Self { + half_size: Vec3::splat(0.5), + } + } +} + +impl MeshBuilder for CuboidMeshBuilder { + fn build(&self) -> Mesh { + let min = -self.half_size; + let max = self.half_size; + + // Suppose Y-up right hand, and camera look from +Z to -Z + let vertices = &[ + // Front + ([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]), + ([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]), + ([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]), + ([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]), + // Back + ([min.x, max.y, min.z], [0.0, 0.0, -1.0], [1.0, 0.0]), + ([max.x, max.y, min.z], [0.0, 0.0, -1.0], [0.0, 0.0]), + ([max.x, min.y, min.z], [0.0, 0.0, -1.0], [0.0, 1.0]), + ([min.x, min.y, min.z], [0.0, 0.0, -1.0], [1.0, 1.0]), + // Right + ([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]), + ([max.x, max.y, min.z], [1.0, 0.0, 0.0], [1.0, 0.0]), + ([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]), + // Left + ([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]), + ([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]), + ([min.x, max.y, min.z], [-1.0, 0.0, 0.0], [0.0, 1.0]), + ([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]), + // Top + ([max.x, max.y, min.z], [0.0, 1.0, 0.0], [1.0, 0.0]), + ([min.x, max.y, min.z], [0.0, 1.0, 0.0], [0.0, 0.0]), + ([min.x, max.y, max.z], [0.0, 1.0, 0.0], [0.0, 1.0]), + ([max.x, max.y, max.z], [0.0, 1.0, 0.0], [1.0, 1.0]), + // Bottom + ([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]), + ([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]), + ([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]), + ]; + + let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect(); + let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); + let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); + + let indices = Indices::U32(alloc::vec![ + 0, 1, 2, 2, 3, 0, // front + 4, 5, 6, 6, 7, 4, // back + 8, 9, 10, 10, 11, 8, // right + 12, 13, 14, 14, 15, 12, // left + 16, 17, 18, 18, 19, 16, // top + 20, 21, 22, 22, 23, 20, // bottom + ]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(indices) + } +} + +impl Meshable for Cuboid { + type Output = CuboidMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + CuboidMeshBuilder { + half_size: self.half_size, + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cylinder.rs b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs new file mode 100644 index 0000000000000..8f48753cfa390 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs @@ -0,0 +1,221 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Cylinder, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::ops; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// Anchoring options for [`CylinderMeshBuilder`] +#[derive(Debug, Copy, Clone, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub enum CylinderAnchor { + #[default] + /// Midpoint between the top and bottom caps of the cylinder + MidPoint, + /// The center of the top circle cap + Top, + /// The center of the bottom circle cap + Bottom, +} + +/// A builder used for creating a [`Mesh`] with a [`Cylinder`] shape. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct CylinderMeshBuilder { + /// The [`Cylinder`] shape. + pub cylinder: Cylinder, + /// The number of vertices used for the top and bottom of the cylinder. + /// + /// The default is `32`. + pub resolution: u32, + /// The number of segments along the height of the cylinder. + /// Must be greater than `0` for geometry to be generated. + /// + /// The default is `1`. + pub segments: u32, + /// If set to `true`, the cylinder caps (flat circle faces) are built, + /// otherwise the mesh will be a shallow tube + pub caps: bool, + /// The anchor point for the cylinder mesh, defaults to the midpoint between + /// the top and bottom caps + pub anchor: CylinderAnchor, +} + +impl Default for CylinderMeshBuilder { + fn default() -> Self { + Self { + cylinder: Cylinder::default(), + resolution: 32, + segments: 1, + caps: true, + anchor: CylinderAnchor::default(), + } + } +} + +impl CylinderMeshBuilder { + /// Creates a new [`CylinderMeshBuilder`] from the given radius, a height, + /// and a resolution used for the top and bottom. + #[inline] + pub fn new(radius: f32, height: f32, resolution: u32) -> Self { + Self { + cylinder: Cylinder::new(radius, height), + resolution, + ..Default::default() + } + } + + /// Sets the number of vertices used for the top and bottom of the cylinder. + #[inline] + pub const fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } + + /// Sets the number of segments along the height of the cylinder. + /// Must be greater than `0` for geometry to be generated. + #[inline] + pub const fn segments(mut self, segments: u32) -> Self { + self.segments = segments; + self + } + + /// Ignore the cylinder caps, making the mesh a shallow tube instead + #[inline] + pub const fn without_caps(mut self) -> Self { + self.caps = false; + self + } + + /// Sets a custom anchor point for the mesh + #[inline] + pub const fn anchor(mut self, anchor: CylinderAnchor) -> Self { + self.anchor = anchor; + self + } +} + +impl MeshBuilder for CylinderMeshBuilder { + fn build(&self) -> Mesh { + let resolution = self.resolution; + let segments = self.segments; + + debug_assert!(resolution > 2); + debug_assert!(segments > 0); + + let num_rings = segments + 1; + let num_vertices = resolution * 2 + num_rings * (resolution + 1); + let num_faces = resolution * (num_rings - 2); + let num_indices = (2 * num_faces + 2 * (resolution - 1) * 2) * 3; + + let mut positions = Vec::with_capacity(num_vertices as usize); + let mut normals = Vec::with_capacity(num_vertices as usize); + let mut uvs = Vec::with_capacity(num_vertices as usize); + let mut indices = Vec::with_capacity(num_indices as usize); + + let step_theta = core::f32::consts::TAU / resolution as f32; + let step_y = 2.0 * self.cylinder.half_height / segments as f32; + + // rings + + for ring in 0..num_rings { + let y = -self.cylinder.half_height + ring as f32 * step_y; + + for segment in 0..=resolution { + let theta = segment as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + positions.push([self.cylinder.radius * cos, y, self.cylinder.radius * sin]); + normals.push([cos, 0., sin]); + uvs.push([ + segment as f32 / resolution as f32, + ring as f32 / segments as f32, + ]); + } + } + + // barrel skin + + for i in 0..segments { + let ring = i * (resolution + 1); + let next_ring = (i + 1) * (resolution + 1); + + for j in 0..resolution { + indices.extend_from_slice(&[ + ring + j, + next_ring + j, + ring + j + 1, + next_ring + j, + next_ring + j + 1, + ring + j + 1, + ]); + } + } + + // caps + if self.caps { + let mut build_cap = |top: bool| { + let offset = positions.len() as u32; + let (y, normal_y, winding) = if top { + (self.cylinder.half_height, 1., (1, 0)) + } else { + (-self.cylinder.half_height, -1., (0, 1)) + }; + + for i in 0..self.resolution { + let theta = i as f32 * step_theta; + let (sin, cos) = ops::sin_cos(theta); + + positions.push([cos * self.cylinder.radius, y, sin * self.cylinder.radius]); + normals.push([0.0, normal_y, 0.0]); + uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); + } + + for i in 1..(self.resolution - 1) { + indices.extend_from_slice(&[ + offset, + offset + i + winding.0, + offset + i + winding.1, + ]); + } + }; + + build_cap(true); + build_cap(false); + } + + // Offset the vertex positions Y axis to match the anchor + match self.anchor { + CylinderAnchor::Top => positions + .iter_mut() + .for_each(|p| p[1] -= self.cylinder.half_height), + CylinderAnchor::Bottom => positions + .iter_mut() + .for_each(|p| p[1] += self.cylinder.half_height), + CylinderAnchor::MidPoint => (), + }; + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Cylinder { + type Output = CylinderMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + CylinderMeshBuilder { + cylinder: *self, + ..Default::default() + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/mod.rs b/crates/bevy_shapes/src/meshing/dim3/mod.rs new file mode 100644 index 0000000000000..21cd80b3a420f --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/mod.rs @@ -0,0 +1,23 @@ +mod capsule; +mod cone; +mod conical_frustum; +mod cuboid; +mod cylinder; +mod plane; +mod polyline3d; +mod segment3d; +mod sphere; +mod tetrahedron; +mod torus; +pub(crate) mod triangle3d; + +pub use capsule::*; +pub use cone::*; +pub use conical_frustum::*; +pub use cuboid::*; +pub use cylinder::*; +pub use plane::*; +pub use sphere::*; +pub use tetrahedron::*; +pub use torus::*; +pub use triangle3d::*; diff --git a/crates/bevy_shapes/src/meshing/dim3/plane.rs b/crates/bevy_shapes/src/meshing/dim3/plane.rs new file mode 100644 index 0000000000000..5f5edfb39a72d --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/plane.rs @@ -0,0 +1,205 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Plane3d, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::{Dir3, Quat, Vec2, Vec3}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct PlaneMeshBuilder { + /// The [`Plane3d`] shape. + pub plane: Plane3d, + /// The number of subdivisions along the X axis. + /// + /// 0 - is the original plane geometry, the 4 points in the XZ plane. + /// + /// 1 - adds a vertex in the middle of the X axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the Z axis. + /// + /// 2 - adds 2 vertices along the X axis, resulting in a plane with 3 quads / 6 triangles. + /// + /// and so on... + pub subdivisions_x: u32, + + /// The number of subdivisions along the Z axis. + /// + /// 0 - is the original plane geometry, the 4 points in the XZ plane. + /// + /// 1 - adds a vertex in the middle of the Z axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the X axis. + /// + /// 2 - adds 2 vertices along the Z axis, resulting in a plane with 3 quads / 6 triangles. + /// + /// and so on... + pub subdivisions_z: u32, +} + +impl PlaneMeshBuilder { + /// Creates a new [`PlaneMeshBuilder`] from a given normal and size. + #[inline] + pub fn new(normal: Dir3, size: Vec2) -> Self { + Self { + plane: Plane3d { + normal, + half_size: size / 2.0, + }, + subdivisions_x: 0, + subdivisions_z: 0, + } + } + + /// Creates a new [`PlaneMeshBuilder`] from the given size, with the normal pointing upwards. + #[inline] + pub fn from_size(size: Vec2) -> Self { + Self { + plane: Plane3d { + half_size: size / 2.0, + ..Default::default() + }, + subdivisions_x: 0, + subdivisions_z: 0, + } + } + + /// Creates a new [`PlaneMeshBuilder`] from the given length, with the normal pointing upwards, + /// and the resulting [`PlaneMeshBuilder`] being a square. + #[inline] + pub fn from_length(length: f32) -> Self { + Self { + plane: Plane3d { + half_size: Vec2::splat(length) / 2.0, + ..Default::default() + }, + subdivisions_x: 0, + subdivisions_z: 0, + } + } + + /// Sets the normal of the plane, aka the direction the plane is facing. + #[inline] + #[doc(alias = "facing")] + pub fn normal(mut self, normal: Dir3) -> Self { + self.plane = Plane3d { + normal, + ..self.plane + }; + self + } + + /// Sets the size of the plane mesh. + #[inline] + pub fn size(mut self, width: f32, height: f32) -> Self { + self.plane.half_size = Vec2::new(width, height) / 2.0; + self + } + + /// Sets the subdivisions of the plane mesh. + /// + /// 0 - is the original plane geometry, the 4 points in the XZ plane. + /// + /// 1 - is split by 1 line in the middle of the plane on both the X axis and the Z axis, + /// resulting in a plane with 4 quads / 8 triangles. + /// + /// 2 - is a plane split by 2 lines on both the X and Z axes, subdividing the plane into 3 + /// equal sections along each axis, resulting in a plane with 9 quads / 18 triangles. + #[inline] + pub fn subdivisions(mut self, subdivisions: u32) -> Self { + self.subdivisions_x = subdivisions; + self.subdivisions_z = subdivisions; + self + } + + #[inline] + /// The number of subdivisions along the X axis. + /// + /// 0 - is the original plane geometry, the 4 points in the XZ plane. + /// + /// 1 - adds a vertex in the middle of the X axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the Z axis. + /// + /// 2 - adds 2 vertices along the X axis, resulting in a plane with 3 quads / 6 triangles. + /// + /// and so on... + pub fn subdivisions_x(mut self, subdivisions: u32) -> Self { + self.subdivisions_x = subdivisions; + self + } + + #[inline] + /// The number of subdivisions along the Z axis. + /// + /// 0 - is the original plane geometry, the 4 points in the XZ plane. + /// + /// 1 - adds a vertex in the middle of the Z axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the X axis. + /// + /// 2 - adds 2 vertices along the Z axis, resulting in a plane with 3 quads / 6 triangles. + /// + /// and so on... + pub fn subdivisions_z(mut self, subdivisions: u32) -> Self { + self.subdivisions_z = subdivisions; + self + } +} + +impl MeshBuilder for PlaneMeshBuilder { + fn build(&self) -> Mesh { + let z_vertex_count = self.subdivisions_z + 2; + let x_vertex_count = self.subdivisions_x + 2; + let num_vertices = (z_vertex_count * x_vertex_count) as usize; + let num_indices = ((z_vertex_count - 1) * (x_vertex_count - 1) * 6) as usize; + + let mut positions: Vec = Vec::with_capacity(num_vertices); + let mut normals: Vec<[f32; 3]> = Vec::with_capacity(num_vertices); + let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(num_vertices); + let mut indices: Vec = Vec::with_capacity(num_indices); + + let rotation = Quat::from_rotation_arc(Vec3::Y, *self.plane.normal); + let size = self.plane.half_size * 2.0; + + for z in 0..z_vertex_count { + for x in 0..x_vertex_count { + let tx = x as f32 / (x_vertex_count - 1) as f32; + let tz = z as f32 / (z_vertex_count - 1) as f32; + let pos = rotation * Vec3::new((-0.5 + tx) * size.x, 0.0, (-0.5 + tz) * size.y); + positions.push(pos); + normals.push(self.plane.normal.to_array()); + uvs.push([tx, tz]); + } + } + + for z in 0..z_vertex_count - 1 { + for x in 0..x_vertex_count - 1 { + let quad = z * x_vertex_count + x; + indices.push(quad + x_vertex_count + 1); + indices.push(quad + 1); + indices.push(quad + x_vertex_count); + indices.push(quad); + indices.push(quad + x_vertex_count); + indices.push(quad + 1); + } + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Plane3d { + type Output = PlaneMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + PlaneMeshBuilder { + plane: *self, + subdivisions_x: 0, + subdivisions_z: 0, + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs new file mode 100644 index 0000000000000..68de53bd70587 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs @@ -0,0 +1,41 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Polyline3d, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Polyline3d`] shape. +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Polyline3dMeshBuilder { + polyline: Polyline3d, +} + +impl MeshBuilder for Polyline3dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self.polyline.vertices.clone(); + + let indices = Indices::U32( + (0..self.polyline.vertices.len() as u32 - 1) + .flat_map(|i| [i, i + 1]) + .collect(), + ); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + } +} + +impl Meshable for Polyline3d { + type Output = Polyline3dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Polyline3dMeshBuilder { + polyline: self.clone(), + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/segment3d.rs b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs new file mode 100644 index 0000000000000..2a4b7060cd0d0 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs @@ -0,0 +1,49 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Segment3d, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Segment3d`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Segment3dMeshBuilder { + segment: Segment3d, +} + +impl MeshBuilder for Segment3dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self.segment.vertices.into(); + let indices = Indices::U32(alloc::vec![0, 1]); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + } +} + +impl Meshable for Segment3d { + type Output = Segment3dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Segment3dMeshBuilder { segment: *self } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::meshing::{MeshBuilder, Meshable}; + use bevy_math::Vec3; + + #[test] + fn segment3d_mesh_builder() { + let segment = Segment3d::new(Vec3::ZERO, Vec3::X); + let mesh = segment.mesh_builder().build(); + assert_eq!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().len(), 2); + assert_eq!(mesh.indices().unwrap().len(), 2); + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/sphere.rs b/crates/bevy_shapes/src/meshing/dim3/sphere.rs new file mode 100644 index 0000000000000..7b785c4f87f7e --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/sphere.rs @@ -0,0 +1,267 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Sphere, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::ops; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; +use core::f32::consts::PI; +use hexasphere::shapes::IcoSphere; +use thiserror::Error; + +/// An error when creating an icosphere [`Mesh`] from a [`SphereMeshBuilder`]. +#[derive(Clone, Copy, Debug, Error)] +pub enum IcosphereError { + /// The icosphere has too many vertices. + #[error("Cannot create an icosphere of {subdivisions} subdivisions due to there being too many vertices being generated: {number_of_resulting_points}. (Limited to 65535 vertices or 79 subdivisions)")] + TooManyVertices { + /// The number of subdivisions used. 79 is the largest allowed value for a mesh to be generated. + subdivisions: u32, + /// The number of vertices generated. 65535 is the largest allowed value for a mesh to be generated. + number_of_resulting_points: u32, + }, +} + +/// A type of sphere mesh. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub enum SphereKind { + /// An icosphere, a spherical mesh that consists of similar sized triangles. + Ico { + /// The number of subdivisions applied. + /// The number of faces quadruples with each subdivision. + subdivisions: u32, + }, + /// A UV sphere, a spherical mesh that consists of quadrilaterals + /// apart from triangles at the top and bottom. + Uv { + /// The number of longitudinal sectors, aka the horizontal resolution. + #[doc(alias = "horizontal_resolution")] + sectors: u32, + /// The number of latitudinal stacks, aka the vertical resolution. + #[doc(alias = "vertical_resolution")] + stacks: u32, + }, +} + +impl Default for SphereKind { + fn default() -> Self { + Self::Ico { subdivisions: 5 } + } +} + +/// A builder used for creating a [`Mesh`] with an [`Sphere`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct SphereMeshBuilder { + /// The [`Sphere`] shape. + pub sphere: Sphere, + /// The type of sphere mesh that will be built. + pub kind: SphereKind, +} + +impl SphereMeshBuilder { + /// Creates a new [`SphereMeshBuilder`] from a radius and [`SphereKind`]. + #[inline] + pub const fn new(radius: f32, kind: SphereKind) -> Self { + Self { + sphere: Sphere { radius }, + kind, + } + } + + /// Sets the [`SphereKind`] that will be used for building the mesh. + #[inline] + pub const fn kind(mut self, kind: SphereKind) -> Self { + self.kind = kind; + self + } + + /// Creates an icosphere mesh with the given number of subdivisions. + /// + /// The number of faces quadruples with each subdivision. + /// If there are `80` or more subdivisions, the vertex count will be too large, + /// and an [`IcosphereError`] is returned. + /// + /// A good default is `5` subdivisions. + pub fn ico(&self, subdivisions: u32) -> Result { + if subdivisions >= 80 { + /* + Number of triangles: + N = 20 + + Number of edges: + E = 30 + + Number of vertices: + V = 12 + + Number of points within a triangle (triangular numbers): + inner(s) = (s^2 + s) / 2 + + Number of points on an edge: + edges(s) = s + + Add up all vertices on the surface: + vertices(s) = edges(s) * E + inner(s - 1) * N + V + + Expand and simplify. Notice that the triangular number formula has roots at -1, and 0, so translating it one to the right fixes it. + subdivisions(s) = 30s + 20((s^2 - 2s + 1 + s - 1) / 2) + 12 + subdivisions(s) = 30s + 10s^2 - 10s + 12 + subdivisions(s) = 10(s^2 + 2s) + 12 + + Factor an (s + 1) term to simplify in terms of calculation + subdivisions(s) = 10(s + 1)^2 + 12 - 10 + resulting_vertices(s) = 10(s + 1)^2 + 2 + */ + let temp = subdivisions + 1; + let number_of_resulting_points = temp * temp * 10 + 2; + return Err(IcosphereError::TooManyVertices { + subdivisions, + number_of_resulting_points, + }); + } + let generated = IcoSphere::new(subdivisions as usize, |point| { + let inclination = ops::acos(point.y); + let azimuth = ops::atan2(point.z, point.x); + + let norm_inclination = inclination / PI; + let norm_azimuth = 0.5 - (azimuth / core::f32::consts::TAU); + + [norm_azimuth, norm_inclination] + }); + + let raw_points = generated.raw_points(); + + let points = raw_points + .iter() + .map(|&p| (p * self.sphere.radius).into()) + .collect::>(); + + let normals = raw_points + .iter() + .copied() + .map(Into::into) + .collect::>(); + + let uvs = generated.raw_data().to_vec(); + + let mut indices = Vec::with_capacity(generated.indices_per_main_triangle() * 20); + + for i in 0..20 { + generated.get_indices(i, &mut indices); + } + + let indices = Indices::U32(indices); + + Ok(Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)) + } + + /// Creates a UV sphere [`Mesh`] with the given number of + /// longitudinal sectors and latitudinal stacks, aka horizontal and vertical resolution. + /// + /// A good default is `32` sectors and `18` stacks. + #[expect( + clippy::explicit_counter_loop, + reason = "Clippy suggestion was much less clear." + )] + pub fn uv(&self, sectors: u32, stacks: u32) -> Mesh { + // Largely inspired from http://www.songho.ca/opengl/gl_sphere.html + + let sectors_f32 = sectors as f32; + let stacks_f32 = stacks as f32; + let length_inv = 1. / self.sphere.radius; + let sector_step = 2. * PI / sectors_f32; + let stack_step = PI / stacks_f32; + + let n_vertices = (stacks * sectors) as usize; + let mut vertices: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); + let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); + let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices); + let mut indices: Vec = Vec::with_capacity(n_vertices * 2 * 3); + + for i in 0..stacks + 1 { + let stack_angle = PI / 2. - (i as f32) * stack_step; + let xy = self.sphere.radius * ops::cos(stack_angle); + let z = self.sphere.radius * ops::sin(stack_angle); + + for j in 0..sectors + 1 { + let sector_angle = (j as f32) * sector_step; + let x = xy * ops::cos(sector_angle); + let y = xy * ops::sin(sector_angle); + + vertices.push([x, y, z]); + normals.push([x * length_inv, y * length_inv, z * length_inv]); + uvs.push([(j as f32) / sectors_f32, (i as f32) / stacks_f32]); + } + } + + // indices + // k1--k1+1 + // | / | + // | / | + // k2--k2+1 + for i in 0..stacks { + let mut k1 = i * (sectors + 1); + let mut k2 = k1 + sectors + 1; + for _j in 0..sectors { + if i != 0 { + indices.push(k1); + indices.push(k2); + indices.push(k1 + 1); + } + if i != stacks - 1 { + indices.push(k1 + 1); + indices.push(k2); + indices.push(k2 + 1); + } + k1 += 1; + k2 += 1; + } + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl MeshBuilder for SphereMeshBuilder { + /// Builds a [`Mesh`] according to the configuration in `self`. + /// + /// # Panics + /// + /// Panics if the sphere is a [`SphereKind::Ico`] with a subdivision count + /// that is greater than or equal to `80` because there will be too many vertices. + fn build(&self) -> Mesh { + match self.kind { + SphereKind::Ico { subdivisions } => self.ico(subdivisions).unwrap(), + SphereKind::Uv { sectors, stacks } => self.uv(sectors, stacks), + } + } +} + +impl Meshable for Sphere { + type Output = SphereMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + SphereMeshBuilder { + sphere: *self, + ..Default::default() + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs new file mode 100644 index 0000000000000..07dbd932a30bd --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs @@ -0,0 +1,63 @@ +use crate::{ + meshing::{triangle3d, MeshBuilder, Meshable}, + primitives::{Tetrahedron, Triangle3d}, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Tetrahedron`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct TetrahedronMeshBuilder { + tetrahedron: Tetrahedron, +} + +impl MeshBuilder for TetrahedronMeshBuilder { + fn build(&self) -> Mesh { + let mut faces: Vec<_> = self.tetrahedron.faces().into(); + + // If the tetrahedron has negative orientation, reverse all the triangles so that + // they still face outward. + if self.tetrahedron.signed_volume().is_sign_negative() { + faces.iter_mut().for_each(Triangle3d::reverse); + } + + let mut positions = alloc::vec![]; + let mut normals = alloc::vec![]; + let mut uvs = alloc::vec![]; + + // Each face is meshed as a `Triangle3d`, and we just shove the data into the + // vertex attributes sequentially. + for face in faces { + positions.extend(face.vertices); + + let face_normal = triangle3d::normal_vec(&face); + normals.extend(alloc::vec![face_normal; 3]); + + let face_uvs = triangle3d::uv_coords(&face); + uvs.extend(face_uvs); + } + + // There are four faces and none of them share vertices. + let indices = Indices::U32(alloc::vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Tetrahedron { + type Output = TetrahedronMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + TetrahedronMeshBuilder { tetrahedron: *self } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/torus.rs b/crates/bevy_shapes/src/meshing/dim3/torus.rs new file mode 100644 index 0000000000000..e464375f5ceda --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/torus.rs @@ -0,0 +1,175 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Torus, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::{ops, Vec3}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; +use core::ops::RangeInclusive; + +/// A builder used for creating a [`Mesh`] with a [`Torus`] shape. +#[derive(Clone, Debug, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct TorusMeshBuilder { + /// The [`Torus`] shape. + pub torus: Torus, + /// The number of vertices used for each circular segment + /// in the ring or tube of the torus. + /// + /// The default is `24`. + pub minor_resolution: usize, + /// The number of segments used for the main ring of the torus. + /// + /// A resolution of `4` would make the torus appear rectangular, + /// while a resolution of `32` resembles a circular ring. + /// + /// The default is `32`. + pub major_resolution: usize, + /// Optional angle range in radians, defaults to a full circle (0.0..=2 * PI) + pub angle_range: RangeInclusive, +} + +impl Default for TorusMeshBuilder { + fn default() -> Self { + Self { + torus: Torus::default(), + minor_resolution: 24, + major_resolution: 32, + angle_range: (0.0..=2.0 * core::f32::consts::PI), + } + } +} + +impl TorusMeshBuilder { + /// Creates a new [`TorusMeshBuilder`] from an inner and outer radius. + /// + /// The inner radius is the radius of the hole, and the outer radius + /// is the radius of the entire object. + #[inline] + pub fn new(inner_radius: f32, outer_radius: f32) -> Self { + Self { + torus: Torus::new(inner_radius, outer_radius), + ..Default::default() + } + } + + /// Sets the number of vertices used for each circular segment + /// in the ring or tube of the torus. + #[inline] + pub const fn minor_resolution(mut self, resolution: usize) -> Self { + self.minor_resolution = resolution; + self + } + + /// Sets the number of segments used for the main ring of the torus. + /// + /// A resolution of `4` would make the torus appear rectangular, + /// while a resolution of `32` resembles a circular ring. + #[inline] + pub const fn major_resolution(mut self, resolution: usize) -> Self { + self.major_resolution = resolution; + self + } + + /// Sets a custom angle range in radians instead of a full circle + #[inline] + pub const fn angle_range(mut self, range: RangeInclusive) -> Self { + self.angle_range = range; + self + } +} + +impl MeshBuilder for TorusMeshBuilder { + fn build(&self) -> Mesh { + // code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html + + let n_vertices = (self.major_resolution + 1) * (self.minor_resolution + 1); + let mut positions: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); + let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); + let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices); + + let start_angle = self.angle_range.start(); + let end_angle = self.angle_range.end(); + + let segment_stride = (end_angle - start_angle) / self.major_resolution as f32; + let side_stride = 2.0 * core::f32::consts::PI / self.minor_resolution as f32; + + for segment in 0..=self.major_resolution { + let theta = start_angle + segment_stride * segment as f32; + + for side in 0..=self.minor_resolution { + let phi = side_stride * side as f32; + let (sin_theta, cos_theta) = ops::sin_cos(theta); + let (sin_phi, cos_phi) = ops::sin_cos(phi); + let radius = self.torus.major_radius + self.torus.minor_radius * cos_phi; + + let position = Vec3::new( + cos_theta * radius, + self.torus.minor_radius * sin_phi, + sin_theta * radius, + ); + + let center = Vec3::new( + self.torus.major_radius * cos_theta, + 0., + self.torus.major_radius * sin_theta, + ); + let normal = (position - center).normalize(); + + positions.push(position.into()); + normals.push(normal.into()); + uvs.push([ + segment as f32 / self.major_resolution as f32, + side as f32 / self.minor_resolution as f32, + ]); + } + } + + let n_faces = (self.major_resolution) * (self.minor_resolution); + let n_triangles = n_faces * 2; + let n_indices = n_triangles * 3; + + let mut indices: Vec = Vec::with_capacity(n_indices); + + let n_vertices_per_row = self.minor_resolution + 1; + for segment in 0..self.major_resolution { + for side in 0..self.minor_resolution { + let lt = side + segment * n_vertices_per_row; + let rt = (side + 1) + segment * n_vertices_per_row; + + let lb = side + (segment + 1) * n_vertices_per_row; + let rb = (side + 1) + (segment + 1) * n_vertices_per_row; + + indices.push(lt as u32); + indices.push(rt as u32); + indices.push(lb as u32); + + indices.push(rt as u32); + indices.push(rb as u32); + indices.push(lb as u32); + } + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Torus { + type Output = TorusMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + TorusMeshBuilder { + torus: *self, + ..Default::default() + } + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs new file mode 100644 index 0000000000000..b183883d07b26 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs @@ -0,0 +1,129 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::Triangle3d, +}; +use alloc::vec::Vec; +use bevy_asset::RenderAssetUsages; +use bevy_math::Vec3; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Triangle3d`] shape. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Triangle3dMeshBuilder { + triangle: Triangle3d, +} + +impl MeshBuilder for Triangle3dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self.triangle.vertices.into(); + let uvs: Vec<_> = uv_coords(&self.triangle).into(); + + // Every vertex has the normal of the face of the triangle (or zero if the triangle is degenerate). + let normal: Vec3 = normal_vec(&self.triangle); + let normals = alloc::vec![normal; 3]; + + let indices = Indices::U32(alloc::vec![0, 1, 2]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } +} + +impl Meshable for Triangle3d { + type Output = Triangle3dMeshBuilder; + + fn mesh_builder(&self) -> Self::Output { + Triangle3dMeshBuilder { triangle: *self } + } +} + +/// The normal of a [`Triangle3d`] with zeroing so that a [`Vec3`] is always obtained for meshing. +#[inline] +pub(crate) fn normal_vec(triangle: &Triangle3d) -> Vec3 { + triangle.normal().map_or(Vec3::ZERO, Into::into) +} + +/// Unskewed uv-coordinates for a [`Triangle3d`]. +#[inline] +pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] { + let [a, b, c] = triangle.vertices; + + let main_length = a.distance(b); + let Some(x) = (b - a).try_normalize() else { + return [[0., 0.], [1., 0.], [0., 1.]]; + }; + let y = c - a; + + // `x` corresponds to one of the axes in uv-coordinates; + // to uv-map the triangle without skewing, we use the orthogonalization + // of `y` with respect to `x` as the second direction and construct a rectangle that + // contains `triangle`. + let y_proj = y.project_onto_normalized(x); + + // `offset` represents the x-coordinate of the point `c`; note that x has been shrunk by a + // factor of `main_length`, so `offset` follows it. + let offset = y_proj.dot(x) / main_length; + + // Obtuse triangle leaning to the left => x direction extends to the left, shifting a from 0. + if offset < 0. { + let total_length = 1. - offset; + let a_uv = [offset.abs() / total_length, 0.]; + let b_uv = [1., 0.]; + let c_uv = [0., 1.]; + + [a_uv, b_uv, c_uv] + } + // Obtuse triangle leaning to the right => x direction extends to the right, shifting b from 1. + else if offset > 1. { + let a_uv = [0., 0.]; + let b_uv = [1. / offset, 0.]; + let c_uv = [1., 1.]; + + [a_uv, b_uv, c_uv] + } + // Acute triangle => no extending necessary; a remains at 0 and b remains at 1. + else { + let a_uv = [0., 0.]; + let b_uv = [1., 0.]; + let c_uv = [offset, 1.]; + + [a_uv, b_uv, c_uv] + } +} + +#[cfg(test)] +mod tests { + use super::uv_coords; + use crate::primitives::Triangle3d; + + #[test] + fn uv_test() { + use bevy_math::vec3; + let mut triangle = Triangle3d::new(vec3(0., 0., 0.), vec3(2., 0., 0.), vec3(-1., 1., 0.)); + + let [a_uv, b_uv, c_uv] = uv_coords(&triangle); + assert_eq!(a_uv, [1. / 3., 0.]); + assert_eq!(b_uv, [1., 0.]); + assert_eq!(c_uv, [0., 1.]); + + triangle.vertices[2] = vec3(3., 1., 0.); + let [a_uv, b_uv, c_uv] = uv_coords(&triangle); + assert_eq!(a_uv, [0., 0.]); + assert_eq!(b_uv, [2. / 3., 0.]); + assert_eq!(c_uv, [1., 1.]); + + triangle.vertices[2] = vec3(2., 1., 0.); + let [a_uv, b_uv, c_uv] = uv_coords(&triangle); + assert_eq!(a_uv, [0., 0.]); + assert_eq!(b_uv, [1., 0.]); + assert_eq!(c_uv, [1., 1.]); + } +} diff --git a/crates/bevy_shapes/src/meshing/extrusion.rs b/crates/bevy_shapes/src/meshing/extrusion.rs new file mode 100644 index 0000000000000..7521b18ed6856 --- /dev/null +++ b/crates/bevy_shapes/src/meshing/extrusion.rs @@ -0,0 +1,432 @@ +use crate::{ + meshing::{MeshBuilder, Meshable}, + primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}, +}; +use alloc::vec::Vec; +use bevy_math::{Vec2, Vec3}; + +use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; + +/// A type representing a segment of the perimeter of an extrudable mesh. +pub enum PerimeterSegment { + /// This segment of the perimeter will be shaded smooth. + /// + /// This has the effect of rendering the segment's faces with softened edges, so it is appropriate for curved shapes. + /// + /// The normals for the vertices that are part of this segment will be calculated based on the positions of their neighbors. + /// Each normal is interpolated between the normals of the two line segments connecting it with its neighbors. + /// Closer vertices have a stronger effect on the normal than more distant ones. + /// + /// Since the vertices corresponding to the first and last indices do not have two neighboring vertices, their normals must be provided manually. + Smooth { + /// The normal of the first vertex. + first_normal: Vec2, + /// The normal of the last vertex. + last_normal: Vec2, + /// A list of indices representing this segment of the perimeter of the mesh. + /// + /// The `indices` refer to the indices of the vertices generated by the `MeshBuilder` of the underlying 2D primitive. + /// For example, a triangle has 3 vertices with indices 0, 1 and 2. + /// + /// The indices must be ordered such that the *outside* of the mesh is to the right + /// when walking along the vertices of the mesh in the order provided by the indices. + /// + /// For geometry to be rendered, you must provide at least two indices. + indices: Vec, + }, + /// This segment of the perimeter will be shaded flat. + /// + /// This has the effect of rendering the segment's faces with hard edges. + Flat { + /// A list of indices representing this segment of the perimeter of the mesh. + /// + /// The `indices` refer to the indices of the vertices generated by the `MeshBuilder` of the underlying 2D primitive. + /// For example, a triangle has 3 vertices with indices 0, 1 and 2. + /// + /// The indices must be ordered such that the *outside* of the mesh is to the right + /// when walking along the vertices of the mesh in the order provided by indices. + /// + /// For geometry to be rendered, you must provide at least two indices. + indices: Vec, + }, +} + +impl PerimeterSegment { + /// Returns the amount of vertices each 'layer' of the extrusion should include for this perimeter segment. + /// + /// A layer is the set of vertices sharing a common Z value or depth. + fn vertices_per_layer(&self) -> u32 { + match self { + PerimeterSegment::Smooth { indices, .. } => indices.len() as u32, + PerimeterSegment::Flat { indices } => 2 * (indices.len() as u32 - 1), + } + } + + /// Returns the amount of indices each 'segment' of the extrusion should include for this perimeter segment. + /// + /// A segment is the set of faces on the mantel of the extrusion between two layers of vertices. + fn indices_per_segment(&self) -> usize { + match self { + PerimeterSegment::Smooth { indices, .. } | PerimeterSegment::Flat { indices } => { + 6 * (indices.len() - 1) + } + } + } +} + +/// A trait required for implementing `Meshable` for `Extrusion`. +/// +/// ## Warning +/// +/// By implementing this trait you guarantee that the `primitive_topology` of the mesh returned by +/// this builder is [`PrimitiveTopology::TriangleList`] +/// and that your mesh has a [`Mesh::ATTRIBUTE_POSITION`] attribute. +pub trait Extrudable: MeshBuilder { + /// A list of the indices each representing a part of the perimeter of the mesh. + fn perimeter(&self) -> Vec; +} + +impl

Meshable for Extrusion

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + type Output = ExtrusionBuilder

; + + fn mesh_builder(&self) -> Self::Output { + ExtrusionBuilder { + base_builder: self.base_shape.mesh_builder(), + half_depth: self.half_depth, + segments: 1, + } + } +} + +/// A builder used for creating a [`Mesh`] with an [`Extrusion`] shape. +pub struct ExtrusionBuilder

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + pub base_builder: P::Output, + pub half_depth: f32, + pub segments: usize, +} + +impl

ExtrusionBuilder

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + /// Create a new `ExtrusionBuilder

` from a given `base_shape` and the full `depth` of the extrusion. + pub fn new(base_shape: &P, depth: f32) -> Self { + Self { + base_builder: base_shape.mesh_builder(), + half_depth: depth / 2., + segments: 1, + } + } + + /// Sets the number of segments along the depth of the extrusion. + /// Must be greater than `0` for the geometry of the mantel to be generated. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } + + /// Apply a function to the inner builder + pub fn with_inner(mut self, func: impl Fn(P::Output) -> P::Output) -> Self { + self.base_builder = func(self.base_builder); + self + } +} + +impl ExtrusionBuilder { + /// Sets the number of vertices used for the circle mesh at each end of the extrusion. + pub fn resolution(mut self, resolution: u32) -> Self { + self.base_builder.resolution = resolution; + self + } +} + +impl ExtrusionBuilder { + /// Sets the number of vertices used for the ellipse mesh at each end of the extrusion. + pub fn resolution(mut self, resolution: u32) -> Self { + self.base_builder.resolution = resolution; + self + } +} + +impl ExtrusionBuilder { + /// Sets the number of vertices used in constructing the concentric circles of the annulus mesh at each end of the extrusion. + pub fn resolution(mut self, resolution: u32) -> Self { + self.base_builder.resolution = resolution; + self + } +} + +impl ExtrusionBuilder { + /// Sets the number of vertices used for each hemicircle at the ends of the extrusion. + pub fn resolution(mut self, resolution: u32) -> Self { + self.base_builder.resolution = resolution; + self + } +} + +impl

MeshBuilder for ExtrusionBuilder

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + fn build(&self) -> Mesh { + // Create and move the base mesh to the front + let mut front_face = + self.base_builder + .build() + .translated_by(Vec3::new(0., 0., self.half_depth)); + + // Move the uvs of the front face to be between (0., 0.) and (0.5, 0.5) + if let Some(VertexAttributeValues::Float32x2(uvs)) = + front_face.attribute_mut(Mesh::ATTRIBUTE_UV_0) + { + for uv in uvs { + *uv = uv.map(|coord| coord * 0.5); + } + } + + let back_face = { + let topology = front_face.primitive_topology(); + // Flip the normals, etc. and move mesh to the back + let mut back_face = front_face.clone().scaled_by(Vec3::new(1., 1., -1.)); + + // Move the uvs of the back face to be between (0.5, 0.) and (1., 0.5) + if let Some(VertexAttributeValues::Float32x2(uvs)) = + back_face.attribute_mut(Mesh::ATTRIBUTE_UV_0) + { + for uv in uvs { + *uv = [uv[0] + 0.5, uv[1]]; + } + } + + // By swapping the first and second indices of each triangle we invert the winding order thus making the mesh visible from the other side + if let Some(indices) = back_face.indices_mut() { + match topology { + PrimitiveTopology::TriangleList => match indices { + Indices::U16(indices) => { + for [a, b, _] in indices.as_chunks_mut().0 { + core::mem::swap(a, b); + } + } + Indices::U32(indices) => { + for [a, b, _] in indices.as_chunks_mut().0 { + core::mem::swap(a, b); + } + } + }, + _ => { + panic!("Meshes used with Extrusions must have a primitive topology of `PrimitiveTopology::TriangleList`"); + } + }; + } + back_face + }; + + // An extrusion of depth 0 does not need a mantel + if self.half_depth == 0. { + front_face.merge(&back_face).unwrap(); + return front_face; + } + + let mantel = { + let Some(VertexAttributeValues::Float32x3(cap_verts)) = + front_face.attribute(Mesh::ATTRIBUTE_POSITION) + else { + panic!("The base mesh did not have vertex positions"); + }; + + debug_assert!(self.segments > 0); + + let layers = self.segments + 1; + let layer_depth_delta = self.half_depth * 2.0 / self.segments as f32; + + let perimeter = self.base_builder.perimeter(); + let (vert_count, index_count) = + perimeter + .iter() + .fold((0, 0), |(verts, indices), perimeter| { + ( + verts + layers * perimeter.vertices_per_layer() as usize, + indices + self.segments * perimeter.indices_per_segment(), + ) + }); + let mut positions = Vec::with_capacity(vert_count); + let mut normals = Vec::with_capacity(vert_count); + let mut indices = Vec::with_capacity(index_count); + let mut uvs = Vec::with_capacity(vert_count); + + // Compute the amount of horizontal space allocated to each segment of the perimeter. + let uv_segment_delta = 1. / perimeter.len() as f32; + for (i, segment) in perimeter.into_iter().enumerate() { + // The start of the x range of the area of the current perimeter-segment. + let uv_start = i as f32 * uv_segment_delta; + + match segment { + PerimeterSegment::Flat { + indices: segment_indices, + } => { + let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32; + for i in 0..(segment_indices.len() - 1) { + let uv_x = uv_start + uv_delta * i as f32; + // Get the positions for the current and the next index. + let a = cap_verts[segment_indices[i] as usize]; + let b = cap_verts[segment_indices[i + 1] as usize]; + + // Get the index of the next vertex added to the mantel. + let index = positions.len() as u32; + + // Push the positions of the two indices and their equivalent points on each layer. + for i in 0..layers { + let i = i as f32; + let z = a[2] - layer_depth_delta * i; + positions.push([a[0], a[1], z]); + positions.push([b[0], b[1], z]); + + // UVs for the mantel are between (0, 0.5) and (1, 1). + let uv_y = 0.5 + 0.5 * i / self.segments as f32; + uvs.push([uv_x, uv_y]); + uvs.push([uv_x + uv_delta, uv_y]); + } + + // The normal is calculated to be the normal of the line segment connecting a and b. + let n = Vec3::from_array([b[1] - a[1], a[0] - b[0], 0.]) + .normalize_or_zero() + .to_array(); + normals.extend_from_slice(&alloc::vec![n; 2 * layers]); + + // Add the indices for the vertices created above to the mesh. + for i in 0..self.segments as u32 { + let base_index = index + 2 * i; + indices.extend_from_slice(&[ + base_index, + base_index + 2, + base_index + 1, + base_index + 1, + base_index + 2, + base_index + 3, + ]); + } + } + } + PerimeterSegment::Smooth { + first_normal, + last_normal, + indices: segment_indices, + } => { + let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32; + + // Since the indices for this segment will be added after its vertices have been added, + // we need to store the index of the first vertex that is part of this segment. + let base_index = positions.len() as u32; + + // If there is a first vertex, we need to add it and its counterparts on each layer. + // The normal is provided by `segment.first_normal`. + if let Some(i) = segment_indices.first() { + let p = cap_verts[*i as usize]; + for i in 0..layers { + let i = i as f32; + let z = p[2] - layer_depth_delta * i; + positions.push([p[0], p[1], z]); + + let uv_y = 0.5 + 0.5 * i / self.segments as f32; + uvs.push([uv_start, uv_y]); + } + normals.extend_from_slice(&alloc::vec![ + first_normal.extend(0.).to_array(); + layers + ]); + } + + // For all points inbetween the first and last vertices, we can automatically compute the normals. + for i in 1..(segment_indices.len() - 1) { + let uv_x = uv_start + uv_delta * i as f32; + + // Get the positions for the last, current and the next index. + let a = cap_verts[segment_indices[i - 1] as usize]; + let b = cap_verts[segment_indices[i] as usize]; + let c = cap_verts[segment_indices[i + 1] as usize]; + + // Add the current vertex and its counterparts on each layer. + for i in 0..layers { + let i = i as f32; + let z = b[2] - layer_depth_delta * i; + positions.push([b[0], b[1], z]); + + let uv_y = 0.5 + 0.5 * i / self.segments as f32; + uvs.push([uv_x, uv_y]); + } + + // The normal for the current vertices can be calculated based on the two neighboring vertices. + // The normal is interpolated between the normals of the two line segments connecting the current vertex with its neighbors. + // Closer vertices have a stronger effect on the normal than more distant ones. + let n = { + let ab = Vec2::from_slice(&b) - Vec2::from_slice(&a); + let bc = Vec2::from_slice(&c) - Vec2::from_slice(&b); + let n = ab.normalize_or_zero() + bc.normalize_or_zero(); + Vec2::new(n.y, -n.x) + .normalize_or_zero() + .extend(0.) + .to_array() + }; + normals.extend_from_slice(&alloc::vec![n; layers]); + } + + // If there is a last vertex, we need to add it and its counterparts on each layer. + // The normal is provided by `segment.last_normal`. + if let Some(i) = segment_indices.last() { + let p = cap_verts[*i as usize]; + for i in 0..layers { + let i = i as f32; + let z = p[2] - layer_depth_delta * i; + positions.push([p[0], p[1], z]); + + let uv_y = 0.5 + 0.5 * i / self.segments as f32; + uvs.push([uv_start + uv_segment_delta, uv_y]); + } + normals.extend_from_slice(&alloc::vec![ + last_normal.extend(0.).to_array(); + layers + ]); + } + + let columns = segment_indices.len() as u32; + let segments = self.segments as u32; + let layers = segments + 1; + for s in 0..segments { + for column in 0..(columns - 1) { + let index = base_index + s + column * layers; + indices.extend_from_slice(&[ + index, + index + 1, + index + layers, + index + layers, + index + 1, + index + layers + 1, + ]); + } + } + } + } + } + + Mesh::new(PrimitiveTopology::TriangleList, front_face.asset_usage) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + }; + + front_face.merge(&back_face).unwrap(); + front_face.merge(&mantel).unwrap(); + front_face + } +} diff --git a/crates/bevy_shapes/src/meshing/mod.rs b/crates/bevy_shapes/src/meshing/mod.rs new file mode 100644 index 0000000000000..530cf7da887dd --- /dev/null +++ b/crates/bevy_shapes/src/meshing/mod.rs @@ -0,0 +1,53 @@ +//! Mesh generation for [primitive shapes](bevy_math::primitives). +//! +//! Primitives that support meshing implement the [`Meshable`] trait. +//! Calling [`mesh`](Meshable::mesh) will return either a [`Mesh`] or a builder +//! that can be used to specify shape-specific configuration for creating the [`Mesh`]. +//! +//! ``` +//! # use bevy_asset::Assets; +//! # use bevy_ecs::prelude::ResMut; +//! # use bevy_math::prelude::Circle; +//! # use bevy_mesh::*; +//! # +//! # fn setup(mut meshes: ResMut>) { +//! // Create circle mesh with default configuration +//! let circle = meshes.add(Circle { radius: 25.0 }); +//! +//! // Specify number of vertices +//! let circle = meshes.add(Circle { radius: 25.0 }.mesh().resolution(64)); +//! # } +//! ``` + +mod dim2; +pub use dim2::*; + +mod dim3; +pub use dim3::*; + +#[cfg(feature = "extrusion")] +mod extrusion; +#[cfg(feature = "extrusion")] +pub use extrusion::*; + +use bevy_mesh::Mesh; + +/// A trait for shapes that can be turned into a [`Mesh`]. +pub trait Meshable { + /// The output of [`Self::mesh`]. This will be a [`MeshBuilder`] used for creating a [`Mesh`]. + type Output: MeshBuilder; + + /// Creates a [`MeshBuilder`] for a shape. + fn mesh_builder(&self) -> Self::Output; + + /// Creates a [`Mesh`] for a shape. + fn mesh(&self) -> Mesh { + self.mesh_builder().build() + } +} + +/// A trait used to build [`Mesh`]es from a configuration +pub trait MeshBuilder { + /// Builds a [`Mesh`] based on the configuration in `self`. + fn build(&self) -> Mesh; +} From 36c03e71e63e433cb8ff533225c6518554c18b8e Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 16:19:03 +0200 Subject: [PATCH 04/17] implement bounding (not working yet) --- crates/bevy_shapes/Cargo.toml | 2 + crates/bevy_shapes/src/bounding/dim2.rs | 1016 ++++++++++++++++++ crates/bevy_shapes/src/bounding/dim3.rs | 685 ++++++++++++ crates/bevy_shapes/src/bounding/extrusion.rs | 456 ++++++++ crates/bevy_shapes/src/bounding/mod.rs | 3 + crates/bevy_shapes/src/lib.rs | 3 + 6 files changed, 2165 insertions(+) create mode 100644 crates/bevy_shapes/src/bounding/dim2.rs create mode 100644 crates/bevy_shapes/src/bounding/dim3.rs create mode 100644 crates/bevy_shapes/src/bounding/extrusion.rs create mode 100644 crates/bevy_shapes/src/bounding/mod.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index 91c3b43739b8f..c0f84e7346a38 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -11,6 +11,7 @@ version = "0.20.0-dev" [dependencies] approx = { default-features = false, optional = true, version = "0.5" } +arrayvec = { default-features = false, optional = true, version = "0.7" } bevy_asset = { default-features = false, optional = true, path = "../bevy_asset" } bevy_math = { default-features = false, path = "../bevy_math" } bevy_mesh = { default-features = false, optional = true, path = "../bevy_mesh" } @@ -41,6 +42,7 @@ std = ["alloc", "bevy_math/std"] approx = ["bevy_math/approx", "dep:approx"] bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] +bounding = ["dep:arrayvec"] extrusion = ["meshing"] meshing = [ "bevy_reflect", diff --git a/crates/bevy_shapes/src/bounding/dim2.rs b/crates/bevy_shapes/src/bounding/dim2.rs new file mode 100644 index 0000000000000..092596d3ae4ed --- /dev/null +++ b/crates/bevy_shapes/src/bounding/dim2.rs @@ -0,0 +1,1016 @@ +//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). + +use crate::primitives::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, + Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, +}; +use bevy_math::{ + bounding::{Aabb2d, Bounded2d, BoundingCircle, BoundingVolume}, + ops, Dir2, Isometry2d, Mat2, Rot2, Vec2, +}; +use core::f32::consts::{FRAC_PI_2, PI, TAU}; + +#[cfg(feature = "alloc")] +use crate::primitives::{ConvexPolygon, Polygon, Polyline2d}; + +use arrayvec::ArrayVec; + +impl Bounded2d for Circle { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + Aabb2d::new(isometry.translation, Vec2::splat(self.radius)) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.radius) + } +} + +// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes. +// The return type has room for 7 points so that the CircularSector code can add an additional point. +#[inline] +fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> ArrayVec { + // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. + // We need to compute which axis-aligned extrema are actually contained within the rotated arc. + let mut bounds = ArrayVec::::new(); + let rotation = rotation.into(); + bounds.push(rotation * arc.left_endpoint()); + bounds.push(rotation * arc.right_endpoint()); + + // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. + // Compute the normalized angles of the endpoints with the rotation taken into account, and then + // check if we are looking for an angle that is between or outside them. + let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU); + let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU); + let inverted = left_angle < right_angle; + for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { + let angle = ops::rem_euclid(extremum.to_angle(), TAU); + // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. + // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. + // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. + let angle_within_parameters = if inverted { + angle >= right_angle || angle <= left_angle + } else { + angle >= right_angle && angle <= left_angle + }; + if angle_within_parameters { + bounds.push(extremum * arc.radius); + } + } + bounds +} + +impl Bounded2d for Arc2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + // If our arc covers more than a circle, just return the bounding box of the circle. + if self.half_angle >= PI { + return Circle::new(self.radius).aabb_2d(isometry); + } + + let isometry = isometry.into(); + + Aabb2d::from_point_cloud( + Isometry2d::from_translation(isometry.translation), + &arc_bounding_points(*self, isometry.rotation), + ) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + + // There are two possibilities for the bounding circle. + if self.is_major() { + // If the arc is major, then the widest distance between two points is a diameter of the arc's circle; + // therefore, that circle is the bounding radius. + BoundingCircle::new(isometry.translation, self.radius) + } else { + // Otherwise, the widest distance between two points is the chord, + // so a circle of that diameter around the midpoint will contain the entire arc. + let center = isometry.rotation * self.chord_midpoint(); + BoundingCircle::new(center + isometry.translation, self.half_chord_length()) + } + } +} + +impl Bounded2d for CircularSector { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + // If our sector covers more than a circle, just return the bounding box of the circle. + if self.half_angle() >= PI { + return Circle::new(self.radius()).aabb_2d(isometry); + } + + // Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility. + let mut bounds = arc_bounding_points(self.arc, isometry.rotation); + bounds.push(Vec2::ZERO); + + Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + if self.arc.is_major() { + let isometry = isometry.into(); + + // If the arc is major, that is, greater than a semicircle, + // then bounding circle is just the circle defining the sector. + BoundingCircle::new(isometry.translation, self.arc.radius) + } else { + // However, when the arc is minor, + // we need our bounding circle to include both endpoints of the arc as well as the circle center. + // This means we need the circumcircle of those three points. + // The circumcircle will always have a greater curvature than the circle itself, so it will contain + // the entire circular sector. + Triangle2d::new( + Vec2::ZERO, + self.arc.left_endpoint(), + self.arc.right_endpoint(), + ) + .bounding_circle(isometry) + } + } +} + +impl Bounded2d for CircularSegment { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + self.arc.aabb_2d(isometry) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + self.arc.bounding_circle(isometry) + } +} + +impl Bounded2d for Ellipse { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + // V = (hh * cos(beta), hh * sin(beta)) + // #####*##### + // ### | ### + // # hh | # + // # *---------* U = (hw * cos(alpha), hw * sin(alpha)) + // # hw # + // ### ### + // ########### + + let (hw, hh) = (self.half_size.x, self.half_size.y); + + // Sine and cosine of rotation angle alpha. + let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos(); + + // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions: + // sin(beta) = sin(alpha + pi/2) = cos(alpha) + // cos(beta) = cos(alpha + pi/2) = -sin(alpha) + let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin); + + // Compute points U and V, the extremes of the ellipse + let (ux, uy) = (hw * alpha_cos, hw * alpha_sin); + let (vx, vy) = (hh * beta_cos, hh * beta_sin); + + let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy)); + + Aabb2d::new(isometry.translation, half_size) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.semi_major()) + } +} + +impl Bounded2d for Annulus { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius)) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.outer_circle.radius) + } +} + +impl Bounded2d for Rhombus { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [ + isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0), + isometry.rotation * Vec2::new(0.0, self.half_diagonals.y), + ]; + let aabb_half_extent = rotated_x_half_diagonal + .abs() + .max(rotated_y_half_diagonal.abs()); + + Aabb2d { + min: -aabb_half_extent + isometry.translation, + max: aabb_half_extent + isometry.translation, + } + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.circumradius()) + } +} + +impl Bounded2d for Plane2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + let normal = isometry.rotation * *self.normal; + let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; + let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; + let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; + let half_size = Vec2::new(half_width, half_height); + + Aabb2d::new(isometry.translation, half_size) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, f32::MAX / 2.0) + } +} + +impl Bounded2d for Line2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + let direction = isometry.rotation * *self.direction; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let max = f32::MAX / 2.0; + let half_width = if direction.x == 0.0 { 0.0 } else { max }; + let half_height = if direction.y == 0.0 { 0.0 } else { max }; + let half_size = Vec2::new(half_width, half_height); + + Aabb2d::new(isometry.translation, half_size) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, f32::MAX / 2.0) + } +} + +impl Bounded2d for Segment2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()]) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry: Isometry2d = isometry.into(); + let local_center = self.center(); + let radius = local_center.distance(self.point1()); + let local_circle = BoundingCircle::new(local_center, radius); + local_circle.transformed_by(isometry.translation, isometry.rotation) + } +} + +#[cfg(feature = "alloc")] +impl Bounded2d for Polyline2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) + } +} + +impl Bounded2d for Triangle2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx); + + let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y)); + let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y)); + + Aabb2d { + min: min + isometry.translation, + max: max + isometry.translation, + } + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + let [a, b, c] = self.vertices; + + // The points of the segment opposite to the obtuse or right angle if one exists + let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { + Some((b, c)) + } else if (c - b).dot(a - b) <= 0.0 { + Some((c, a)) + } else if (a - c).dot(b - c) <= 0.0 { + Some((a, b)) + } else { + // The triangle is acute. + None + }; + + // Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices. + // Otherwise, it's the circumcircle and passes through all three. + if let Some((point1, point2)) = side_opposite_to_non_acute { + // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. + // We can compute the minimum bounding circle from the line segment of the longest side. + let segment = Segment2d::new(point1, point2); + segment.bounding_circle(isometry) + } else { + // The triangle is acute, so the smallest bounding circle is the circumcircle. + let (Circle { radius }, circumcenter) = self.circumcircle(); + BoundingCircle::new(isometry * circumcenter, radius) + } + } +} + +impl Bounded2d for Rectangle { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + // Compute the AABB of the rotated rectangle by transforming the half-extents + // by an absolute rotation matrix. + let (sin, cos) = isometry.rotation.sin_cos(); + let abs_rot_mat = + Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]); + let half_size = abs_rot_mat * self.half_size; + + Aabb2d::new(isometry.translation, half_size) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + let radius = self.half_size.length(); + BoundingCircle::new(isometry.translation, radius) + } +} + +#[cfg(feature = "alloc")] +impl Bounded2d for Polygon { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) + } +} + +#[cfg(feature = "alloc")] +impl Bounded2d for ConvexPolygon { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, self.vertices()) + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, self.vertices()) + } +} + +impl Bounded2d for RegularPolygon { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + let mut min = Vec2::ZERO; + let mut max = Vec2::ZERO; + + for vertex in self.vertices(isometry.rotation.as_radians()) { + min = min.min(vertex); + max = max.max(vertex); + } + + Aabb2d { + min: min + isometry.translation, + max: max + isometry.translation, + } + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.circumcircle.radius) + } +} + +impl Bounded2d for Capsule2d { + fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { + let isometry = isometry.into(); + + // Get the line segment between the semicircles of the rotated capsule + let segment = Segment2d::from_direction_and_length( + isometry.rotation * Dir2::Y, + self.half_length * 2., + ); + let (a, b) = (segment.point1(), segment.point2()); + + // Expand the line segment by the capsule radius to get the capsule half-extents + let min = a.min(b) - Vec2::splat(self.radius); + let max = a.max(b) + Vec2::splat(self.radius); + + Aabb2d { + min: min + isometry.translation, + max: max + isometry.translation, + } + } + + fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { + let isometry = isometry.into(); + BoundingCircle::new(isometry.translation, self.radius + self.half_length) + } +} + +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 { + use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU}; + use std::println; + + use approx::assert_abs_diff_eq; + + use crate::primitives::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, + }; + use bevy_math::{ + bounding::Bounded2d, + ops::{self, FloatPow}, + Dir2, Isometry2d, Rot2, Vec2, + }; + + #[test] + fn circle() { + let circle = Circle { radius: 1.0 }; + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = circle.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = circle.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } + + #[test] + // Arcs and circular segments have the same bounding shapes so they share test cases. + fn arc_and_segment() { + struct TestCase { + name: &'static str, + arc: Arc2d, + translation: Vec2, + rotation: f32, + aabb_min: Vec2, + aabb_max: Vec2, + bounding_circle_center: Vec2, + bounding_circle_radius: f32, + } + + impl TestCase { + fn isometry(&self) -> Isometry2d { + Isometry2d::new(self.translation, self.rotation.into()) + } + } + + // The apothem of an arc covering 1/6th of a circle. + let apothem = ops::sqrt(3.0) / 2.0; + let tests = [ + // Test case: a basic minor arc + TestCase { + name: "1/6th circle untransformed", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.5, apothem), + aabb_max: Vec2::new(0.5, 1.0), + bounding_circle_center: Vec2::new(0.0, apothem), + bounding_circle_radius: 0.5, + }, + // Test case: a smaller arc, verifying that radius scaling works + TestCase { + name: "1/6th circle with radius 0.5", + arc: Arc2d::from_radians(0.5, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.25, apothem / 2.0), + aabb_max: Vec2::new(0.25, 0.5), + bounding_circle_center: Vec2::new(0.0, apothem / 2.0), + bounding_circle_radius: 0.25, + }, + // Test case: a larger arc, verifying that radius scaling works + TestCase { + name: "1/6th circle with radius 2.0", + arc: Arc2d::from_radians(2.0, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, 2.0 * apothem), + aabb_max: Vec2::new(1.0, 2.0), + bounding_circle_center: Vec2::new(0.0, 2.0 * apothem), + bounding_circle_radius: 1.0, + }, + // Test case: translation of a minor arc + TestCase { + name: "1/6th circle translated", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.5, 3.0 + apothem), + aabb_max: Vec2::new(2.5, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0 + apothem), + bounding_circle_radius: 0.5, + }, + // Test case: rotation of a minor arc + TestCase { + name: "1/6th circle rotated", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. + rotation: FRAC_PI_6, + aabb_min: Vec2::new(-apothem, 0.5), + aabb_max: Vec2::new(0.0, 1.0), + // The exact coordinates here are not obvious, but can be computed by constructing + // an altitude from the midpoint of the chord to the y-axis and using the right triangle + // similarity theorem. + bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()), + bounding_circle_radius: 0.5, + }, + // Test case: handling of axis-aligned extrema + TestCase { + name: "1/4er circle rotated to be axis-aligned", + arc: Arc2d::from_radians(1.0, FRAC_PI_2), + translation: Vec2::ZERO, + // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. + rotation: -FRAC_PI_4, + aabb_min: Vec2::ZERO, + aabb_max: Vec2::splat(1.0), + bounding_circle_center: Vec2::splat(0.5), + bounding_circle_radius: ops::sqrt(2.0) / 2.0, + }, + // Test case: a basic major arc + TestCase { + name: "5/6th circle untransformed", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, -apothem), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + // Test case: a translated major arc + TestCase { + name: "5/6th circle translated", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.0, 3.0 - apothem), + aabb_max: Vec2::new(3.0, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0), + bounding_circle_radius: 1.0, + }, + // Test case: a rotated major arc, with inverted left/right angles + TestCase { + name: "5/6th circle rotated", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. + rotation: FRAC_PI_6, + aabb_min: Vec2::new(-1.0, -1.0), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + ]; + + for test in tests { + #[cfg(feature = "std")] + println!("subtest case: {}", test.name); + let segment: CircularSegment = test.arc.into(); + + let arc_aabb = test.arc.aabb_2d(test.isometry()); + assert_abs_diff_eq!(test.aabb_min, arc_aabb.min); + assert_abs_diff_eq!(test.aabb_max, arc_aabb.max); + let segment_aabb = segment.aabb_2d(test.isometry()); + assert_abs_diff_eq!(test.aabb_min, segment_aabb.min); + assert_abs_diff_eq!(test.aabb_max, segment_aabb.max); + + let arc_bounding_circle = test.arc.bounding_circle(test.isometry()); + assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center); + assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius()); + let segment_bounding_circle = segment.bounding_circle(test.isometry()); + assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center); + assert_abs_diff_eq!( + test.bounding_circle_radius, + segment_bounding_circle.radius() + ); + } + } + + #[test] + fn circular_sector() { + struct TestCase { + name: &'static str, + arc: Arc2d, + translation: Vec2, + rotation: f32, + aabb_min: Vec2, + aabb_max: Vec2, + bounding_circle_center: Vec2, + bounding_circle_radius: f32, + } + + impl TestCase { + fn isometry(&self) -> Isometry2d { + Isometry2d::new(self.translation, self.rotation.into()) + } + } + + // The apothem of an arc covering 1/6th of a circle. + let apothem = ops::sqrt(3.0) / 2.0; + let inv_sqrt_3 = ops::sqrt(3.0).recip(); + let tests = [ + // Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center + TestCase { + name: "1/3rd circle", + arc: Arc2d::from_radians(1.0, TAU / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-apothem, 0.0), + aabb_max: Vec2::new(apothem, 1.0), + bounding_circle_center: Vec2::new(0.0, 0.5), + bounding_circle_radius: apothem, + }, + // The remaining test cases are selected as for arc_and_segment. + TestCase { + name: "1/6th circle untransformed", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.5, 0.0), + aabb_max: Vec2::new(0.5, 1.0), + // The bounding circle is a circumcircle of an equilateral triangle with side length 1. + // The distance from the corner to the center of such a triangle is 1/sqrt(3). + bounding_circle_center: Vec2::new(0.0, inv_sqrt_3), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/6th circle with radius 0.5", + arc: Arc2d::from_radians(0.5, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.25, 0.0), + aabb_max: Vec2::new(0.25, 0.5), + bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0), + bounding_circle_radius: inv_sqrt_3 / 2.0, + }, + TestCase { + name: "1/6th circle with radius 2.0", + arc: Arc2d::from_radians(2.0, FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, 0.0), + aabb_max: Vec2::new(1.0, 2.0), + bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3), + bounding_circle_radius: 2.0 * inv_sqrt_3, + }, + TestCase { + name: "1/6th circle translated", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.5, 3.0), + aabb_max: Vec2::new(2.5, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/6th circle rotated", + arc: Arc2d::from_radians(1.0, FRAC_PI_3), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. + rotation: FRAC_PI_6, + aabb_min: Vec2::new(-apothem, 0.0), + aabb_max: Vec2::new(0.0, 1.0), + // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2. + bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/4er circle rotated to be axis-aligned", + arc: Arc2d::from_radians(1.0, FRAC_PI_2), + translation: Vec2::ZERO, + // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. + rotation: -FRAC_PI_4, + aabb_min: Vec2::ZERO, + aabb_max: Vec2::splat(1.0), + bounding_circle_center: Vec2::splat(0.5), + bounding_circle_radius: ops::sqrt(2.0) / 2.0, + }, + TestCase { + name: "5/6th circle untransformed", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, -apothem), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle translated", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.0, 3.0 - apothem), + aabb_max: Vec2::new(3.0, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0), + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle rotated", + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. + rotation: FRAC_PI_6, + aabb_min: Vec2::new(-1.0, -1.0), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + ]; + + for test in tests { + #[cfg(feature = "std")] + println!("subtest case: {}", test.name); + let sector: CircularSector = test.arc.into(); + + let aabb = sector.aabb_2d(test.isometry()); + assert_abs_diff_eq!(test.aabb_min, aabb.min); + assert_abs_diff_eq!(test.aabb_max, aabb.max); + + let bounding_circle = sector.bounding_circle(test.isometry()); + assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center); + assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius()); + } + } + + #[test] + fn ellipse() { + let ellipse = Ellipse::new(1.0, 0.5); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = ellipse.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); + assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); + + let bounding_circle = ellipse.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } + + #[test] + fn annulus() { + let annulus = Annulus::new(1.0, 2.0); + let translation = Vec2::new(2.0, 1.0); + let rotation = Rot2::radians(1.0); + let isometry = Isometry2d::new(translation, rotation); + + let aabb = annulus.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(0.0, -1.0)); + assert_eq!(aabb.max, Vec2::new(4.0, 3.0)); + + let bounding_circle = annulus.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 2.0); + } + + #[test] + fn rhombus() { + let rhombus = Rhombus::new(2.0, 1.0); + let translation = Vec2::new(2.0, 1.0); + let rotation = Rot2::radians(FRAC_PI_4); + let isometry = Isometry2d::new(translation, rotation); + + let aabb = rhombus.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323)); + assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068)); + + let bounding_circle = rhombus.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + + let rhombus = Rhombus::new(0.0, 0.0); + let translation = Vec2::new(0.0, 0.0); + let isometry = Isometry2d::new(translation, rotation); + + let aabb = rhombus.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(0.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(0.0, 0.0)); + + let bounding_circle = rhombus.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 0.0); + } + + #[test] + fn plane() { + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry); + assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); + + let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry); + assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); + assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); + + let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry); + assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); + + let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); + } + + #[test] + fn line() { + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry); + assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); + + let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry); + assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); + assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); + + let aabb3 = Line2d { + direction: Dir2::from_xy(1.0, 1.0).unwrap(), + } + .aabb_2d(isometry); + assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); + + let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); + } + + #[test] + fn segment() { + let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = segment.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); + assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); + + let bounding_circle = segment.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); + } + + #[test] + fn polyline() { + let polyline = Polyline2d::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = polyline.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = polyline.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2); + } + + #[test] + fn acute_triangle() { + let acute_triangle = + Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0)); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = acute_triangle.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + // For acute triangles, the center is the circumcenter + let (Circle { radius }, circumcenter) = acute_triangle.circumcircle(); + let bounding_circle = acute_triangle.bounding_circle(isometry); + assert_eq!(bounding_circle.center, circumcenter + translation); + assert_eq!(bounding_circle.radius(), radius); + } + + #[test] + fn obtuse_triangle() { + let obtuse_triangle = Triangle2d::new( + Vec2::new(0.0, 1.0), + Vec2::new(-10.0, -1.0), + Vec2::new(10.0, -1.0), + ); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = obtuse_triangle.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(-8.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(12.0, 2.0)); + + // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle) + let bounding_circle = obtuse_triangle.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation - Vec2::Y); + assert_eq!(bounding_circle.radius(), 10.0); + } + + #[test] + fn rectangle() { + let rectangle = Rectangle::new(2.0, 1.0); + let translation = Vec2::new(2.0, 1.0); + + let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4))); + let expected_half_size = Vec2::splat(1.0606601); + assert_eq!(aabb.min, translation - expected_half_size); + assert_eq!(aabb.max, translation + expected_half_size); + + let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation)); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); + } + + #[test] + fn polygon() { + let polygon = Polygon::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = polygon.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = polygon.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2); + } + + #[test] + fn regular_polygon() { + let regular_polygon = RegularPolygon::new(1.0, 5); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = regular_polygon.aabb_2d(isometry); + assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6); + assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6); + + let bounding_circle = regular_polygon.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } + + #[test] + fn capsule() { + let capsule = Capsule2d::new(0.5, 2.0); + let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); + + let aabb = capsule.aabb_2d(isometry); + assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5)); + assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5)); + + let bounding_circle = capsule.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.5); + } +} diff --git a/crates/bevy_shapes/src/bounding/dim3.rs b/crates/bevy_shapes/src/bounding/dim3.rs new file mode 100644 index 0000000000000..3a91b712371ba --- /dev/null +++ b/crates/bevy_shapes/src/bounding/dim3.rs @@ -0,0 +1,685 @@ +//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). + +use crate::primitives::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Segment3d, Sphere, + Torus, Triangle2d, Triangle3d, +}; +use bevy_math::{ + bounding::{Aabb3d, Bounded2d, Bounded3d, BoundingCircle, BoundingSphere, BoundingVolume}, + ops, Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A, +}; + +#[cfg(feature = "alloc")] +use crate::primitives::Polyline3d; + +impl Bounded3d for Sphere { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + Aabb3d::new(isometry.translation, Vec3::splat(self.radius)) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, self.radius) + } +} + +impl Bounded3d for InfinitePlane3d { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + + let normal = isometry.rotation * *self.normal; + let facing_x = normal == Vec3::X || normal == Vec3::NEG_X; + let facing_y = normal == Vec3::Y || normal == Vec3::NEG_Y; + let facing_z = normal == Vec3::Z || normal == Vec3::NEG_Z; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; + let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; + let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 }; + let half_size = Vec3A::new(half_width, half_height, half_depth); + + Aabb3d::new(isometry.translation, half_size) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, f32::MAX / 2.0) + } +} + +impl Bounded3d for Line3d { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let direction = isometry.rotation * *self.direction; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let max = f32::MAX / 2.0; + let half_width = if direction.x == 0.0 { 0.0 } else { max }; + let half_height = if direction.y == 0.0 { 0.0 } else { max }; + let half_depth = if direction.z == 0.0 { 0.0 } else { max }; + let half_size = Vec3A::new(half_width, half_height, half_depth); + + Aabb3d::new(isometry.translation, half_size) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, f32::MAX / 2.0) + } +} + +impl Bounded3d for Segment3d { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + Aabb3d::from_point_cloud(isometry, [self.point1(), self.point2()].iter().copied()) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + let local_sphere = BoundingSphere::new(self.center(), self.length() / 2.); + local_sphere.transformed_by(isometry.translation, isometry.rotation) + } +} + +#[cfg(feature = "alloc")] +impl Bounded3d for Polyline3d { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + BoundingSphere::from_point_cloud(isometry, &self.vertices) + } +} + +impl Bounded3d for Cuboid { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + + // Compute the AABB of the rotated cuboid by transforming the half-size + // by an absolute rotation matrix. + let rot_mat = Mat3::from_quat(isometry.rotation); + let abs_rot_mat = Mat3::from_cols( + rot_mat.x_axis.abs(), + rot_mat.y_axis.abs(), + rot_mat.z_axis.abs(), + ); + let half_size = abs_rot_mat * self.half_size; + + Aabb3d::new(isometry.translation, half_size) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, self.half_size.length()) + } +} + +impl Bounded3d for Cylinder { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let isometry = isometry.into(); + + let segment_dir = isometry.rotation * Vec3A::Y; + let top = segment_dir * self.half_height; + let bottom = -top; + + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); + + Aabb3d { + min: isometry.translation + (top - half_size).min(bottom - half_size), + max: isometry.translation + (top + half_size).max(bottom + half_size), + } + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + let radius = ops::hypot(self.radius, self.half_height); + BoundingSphere::new(isometry.translation, radius) + } +} + +impl Bounded3d for Capsule3d { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + + // Get the line segment between the hemispheres of the rotated capsule + let segment_dir = isometry.rotation * Vec3A::Y; + let top = segment_dir * self.half_length; + let bottom = -top; + + // Expand the line segment by the capsule radius to get the capsule half-extents + let min = bottom.min(top) - Vec3A::splat(self.radius); + let max = bottom.max(top) + Vec3A::splat(self.radius); + + Aabb3d { + min: min + isometry.translation, + max: max + isometry.translation, + } + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, self.radius + self.half_length) + } +} + +impl Bounded3d for Cone { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let isometry = isometry.into(); + + let segment_dir = isometry.rotation * Vec3A::Y; + let top = segment_dir * 0.5 * self.height; + let bottom = -top; + + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); + + Aabb3d { + min: isometry.translation + top.min(bottom - self.radius * half_extents), + max: isometry.translation + top.max(bottom + self.radius * half_extents), + } + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + + // Get the triangular cross-section of the cone. + let half_height = 0.5 * self.height; + let triangle = Triangle2d::new( + half_height * Vec2::Y, + Vec2::new(-self.radius, -half_height), + Vec2::new(self.radius, -half_height), + ); + + // Because of circular symmetry, we can use the bounding circle of the triangle + // for the bounding sphere of the cone. + let BoundingCircle { circle, center } = triangle.bounding_circle(Isometry2d::IDENTITY); + + BoundingSphere::new( + isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation, + circle.radius, + ) + } +} + +impl Bounded3d for ConicalFrustum { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let isometry = isometry.into(); + + let segment_dir = isometry.rotation * Vec3A::Y; + let top = segment_dir * 0.5 * self.height; + let bottom = -top; + + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); + + Aabb3d { + min: isometry.translation + + (top - self.radius_top * half_extents) + .min(bottom - self.radius_bottom * half_extents), + max: isometry.translation + + (top + self.radius_top * half_extents) + .max(bottom + self.radius_bottom * half_extents), + } + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + let half_height = 0.5 * self.height; + + // To compute the bounding sphere, we'll get the center and radius of the circumcircle + // passing through all four vertices of the trapezoidal cross-section of the conical frustum. + // + // If the circumcenter is inside the trapezoid, we can use that for the bounding sphere. + // Otherwise, we clamp it to the longer parallel side to get a more tightly fitting bounding sphere. + // + // The circumcenter is at the intersection of the bisectors perpendicular to the sides. + // For the isosceles trapezoid, the X coordinate is zero at the center, so a single bisector is enough. + // + // A + // *-------* + // / | \ + // / | \ + // AB / \ | / \ + // / \ | / \ + // / C \ + // *-------------------* + // B + + let a = Vec2::new(-self.radius_top, half_height); + let b = Vec2::new(-self.radius_bottom, -half_height); + let ab = a - b; + let ab_midpoint = b + 0.5 * ab; + let bisector = ab.perp(); + + // Compute intersection between bisector and vertical line at x = 0. + // + // x = ab_midpoint.x + t * bisector.x = 0 + // y = ab_midpoint.y + t * bisector.y = ? + // + // Because ab_midpoint.y = 0 for our conical frustum, we get: + // y = t * bisector.y + // + // Solve x for t: + // t = -ab_midpoint.x / bisector.x + // + // Substitute t to solve for y: + // y = -ab_midpoint.x / bisector.x * bisector.y + let circumcenter_y = -ab_midpoint.x / bisector.x * bisector.y; + + // If the circumcenter is outside the trapezoid, the bounding circle is too large. + // In those cases, we clamp it to the longer parallel side. + let (center, radius) = if circumcenter_y <= -half_height { + (Vec2::new(0.0, -half_height), self.radius_bottom) + } else if circumcenter_y >= half_height { + (Vec2::new(0.0, half_height), self.radius_top) + } else { + let circumcenter = Vec2::new(0.0, circumcenter_y); + // We can use the distance from an arbitrary vertex because they all lie on the circumcircle. + (circumcenter, a.distance(circumcenter)) + }; + + BoundingSphere::new( + isometry.translation + isometry.rotation * Vec3A::from(center.extend(0.0)), + radius, + ) + } +} + +impl Bounded3d for Torus { + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + + // Compute the AABB of a flat disc with the major radius of the torus. + // Reference: http://iquilezles.org/articles/diskbbox/ + let normal = isometry.rotation * Vec3A::Y; + let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO); + let disc_half_size = + self.major_radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); + + // Expand the disc by the minor radius to get the torus half-size + let half_size = disc_half_size + Vec3A::splat(self.minor_radius); + + Aabb3d::new(isometry.translation, half_size) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + BoundingSphere::new(isometry.translation, self.outer_radius()) + } +} + +impl Bounded3d for Triangle3d { + /// Get the bounding box of the triangle. + fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let [a, b, c] = self.vertices; + + let a = isometry.rotation * a; + let b = isometry.rotation * b; + let c = isometry.rotation * c; + + let min = Vec3A::from(a.min(b).min(c)); + let max = Vec3A::from(a.max(b).max(c)); + + let bounding_center = (max + min) / 2.0 + isometry.translation; + let half_extents = (max - min) / 2.0; + + Aabb3d::new(bounding_center, half_extents) + } + + /// Get the bounding sphere of the triangle. + /// + /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as + /// the center of the sphere. For the others, the bounding sphere is the minimal sphere + /// that contains the largest side of the triangle. + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + let isometry = isometry.into(); + + if self.is_degenerate() || self.is_obtuse() { + let (p1, p2) = self.largest_side(); + let (p1, p2) = (Vec3A::from(p1), Vec3A::from(p2)); + let mid_point = (p1 + p2) / 2.0; + let radius = mid_point.distance(p1); + BoundingSphere::new(mid_point + isometry.translation, radius) + } else { + let [a, _, _] = self.vertices; + + let circumcenter = self.circumcenter(); + let radius = circumcenter.distance(a); + BoundingSphere::new(Vec3A::from(circumcenter) + isometry.translation, radius) + } + } +} + +#[cfg(test)] +mod tests { + use bevy_math::{ + bounding::{Bounded3d, BoundingVolume}, + ops, Dir3, Isometry3d, Quat, Vec3, Vec3A, + }; + + use crate::primitives::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, + Segment3d, Sphere, Torus, Triangle3d, + }; + + #[test] + fn sphere() { + let sphere = Sphere { radius: 1.0 }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = sphere.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); + + let bounding_sphere = sphere.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 1.0); + } + + #[test] + fn plane() { + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation); + assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0)); + + let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation); + assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0)); + assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0)); + + let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation); + assert_eq!(aabb3.min, Vec3A::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0)); + assert_eq!(aabb3.max, Vec3A::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0)); + + let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation); + assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); + assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); + + let bounding_sphere = InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); + } + + #[test] + fn line() { + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(translation); + assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, 0.0)); + assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, 0.0)); + + let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(translation); + assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, 0.0)); + assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, 0.0)); + + let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(translation); + assert_eq!(aabb3.min, Vec3A::new(2.0, 1.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec3A::new(2.0, 1.0, f32::MAX / 2.0)); + + let aabb4 = Line3d { + direction: Dir3::from_xyz(1.0, 1.0, 1.0).unwrap(), + } + .aabb_3d(translation); + assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); + assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); + + let bounding_sphere = Line3d { direction: Dir3::Y }.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); + } + + #[test] + fn segment() { + let segment = Segment3d::new(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0)); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = segment.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0)); + assert_eq!(aabb.max, Vec3A::new(3.0, 1.5, 0.0)); + + let bounding_sphere = segment.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); + } + + #[test] + fn polyline() { + let polyline = Polyline3d::new([ + Vec3::ONE, + Vec3::new(-1.0, 1.0, 1.0), + Vec3::NEG_ONE, + Vec3::new(1.0, -1.0, -1.0), + ]); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = polyline.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); + + let bounding_sphere = polyline.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!( + bounding_sphere.radius(), + ops::hypot(ops::hypot(1.0, 1.0), 1.0) + ); + } + + #[test] + fn cuboid() { + let cuboid = Cuboid::new(2.0, 1.0, 1.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cuboid.aabb_3d(Isometry3d::new( + translation, + Quat::from_rotation_z(core::f32::consts::FRAC_PI_4), + )); + let expected_half_size = Vec3A::new(1.0606601, 1.0606601, 0.5); + assert_eq!(aabb.min, Vec3A::from(translation) - expected_half_size); + assert_eq!(aabb.max, Vec3A::from(translation) + expected_half_size); + + let bounding_sphere = cuboid.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!( + bounding_sphere.radius(), + ops::hypot(ops::hypot(1.0, 0.5), 0.5) + ); + } + + #[test] + fn cylinder() { + let cylinder = Cylinder::new(0.5, 2.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cylinder.aabb_3d(translation); + assert_eq!( + aabb.min, + Vec3A::from(translation) - Vec3A::new(0.5, 1.0, 0.5) + ); + assert_eq!( + aabb.max, + Vec3A::from(translation) + Vec3A::new(0.5, 1.0, 0.5) + ); + + let bounding_sphere = cylinder.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); + } + + #[test] + fn capsule() { + let capsule = Capsule3d::new(0.5, 2.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = capsule.aabb_3d(translation); + assert_eq!( + aabb.min, + Vec3A::from(translation) - Vec3A::new(0.5, 1.5, 0.5) + ); + assert_eq!( + aabb.max, + Vec3A::from(translation) + Vec3A::new(0.5, 1.5, 0.5) + ); + + let bounding_sphere = capsule.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 1.5); + } + + #[test] + fn cone() { + let cone = Cone { + radius: 1.0, + height: 2.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cone.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); + + let bounding_sphere = cone.bounding_sphere(translation); + assert_eq!( + bounding_sphere.center, + Vec3A::from(translation) + Vec3A::NEG_Y * 0.25 + ); + assert_eq!(bounding_sphere.radius(), 1.25); + } + + #[test] + fn conical_frustum() { + let conical_frustum = ConicalFrustum { + radius_top: 0.5, + radius_bottom: 1.0, + height: 2.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = conical_frustum.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); + + let bounding_sphere = conical_frustum.bounding_sphere(translation); + assert_eq!( + bounding_sphere.center, + Vec3A::from(translation) + Vec3A::NEG_Y * 0.1875 + ); + assert_eq!(bounding_sphere.radius(), 1.2884705); + } + + #[test] + fn wide_conical_frustum() { + let conical_frustum = ConicalFrustum { + radius_top: 0.5, + radius_bottom: 5.0, + height: 1.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = conical_frustum.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(-3.0, 0.5, -5.0)); + assert_eq!(aabb.max, Vec3A::new(7.0, 1.5, 5.0)); + + // For wide conical frusta like this, the circumcenter can be outside the frustum, + // so the center and radius should be clamped to the longest side. + let bounding_sphere = conical_frustum.bounding_sphere(translation); + assert_eq!( + bounding_sphere.center, + Vec3A::from(translation) + Vec3A::NEG_Y * 0.5 + ); + assert_eq!(bounding_sphere.radius(), 5.0); + } + + #[test] + fn torus() { + let torus = Torus { + minor_radius: 0.5, + major_radius: 1.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = torus.aabb_3d(translation); + assert_eq!(aabb.min, Vec3A::new(0.5, 0.5, -1.5)); + assert_eq!(aabb.max, Vec3A::new(3.5, 1.5, 1.5)); + + let bounding_sphere = torus.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 1.5); + } + + #[test] + fn triangle3d() { + let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); + + let br = zero_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); + assert_eq!( + br.center(), + Vec3::ZERO.into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::ZERO.into(), + "incorrect bounding box half extents" + ); + + let bs = zero_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); + assert_eq!( + bs.center, + Vec3::ZERO.into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius"); + + let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); + let bs = dup_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); + assert_eq!( + bs.center, + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius"); + let br = dup_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); + assert_eq!( + br.center(), + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::new(0.5, 0.0, 0.0).into(), + "incorrect bounding box half extents" + ); + + let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); + let bs = collinear_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); + assert_eq!( + bs.center, + Vec3::ZERO.into(), + "incorrect bounding sphere center" + ); + assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius"); + let br = collinear_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); + assert_eq!( + br.center(), + Vec3::ZERO.into(), + "incorrect bounding box center" + ); + assert_eq!( + br.half_size(), + Vec3::new(1.0, 0.0, 0.0).into(), + "incorrect bounding box half extents" + ); + } +} diff --git a/crates/bevy_shapes/src/bounding/extrusion.rs b/crates/bevy_shapes/src/bounding/extrusion.rs new file mode 100644 index 0000000000000..8e701ac03b7b2 --- /dev/null +++ b/crates/bevy_shapes/src/bounding/extrusion.rs @@ -0,0 +1,456 @@ +use core::f32::consts::FRAC_PI_2; + +use crate::primitives::{ + Capsule2d, Circle, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Primitive2d, Rectangle, + RegularPolygon, Ring, Segment2d, Triangle2d, +}; +use bevy_math::{ + bounding::{Aabb3d, Bounded2d, Bounded3d, BoundingCircle, BoundingSphere, BoundingVolume}, + ops, Isometry2d, Isometry3d, Quat, Rot2, Vec2, Vec3A, Vec3Swizzles, +}; + +#[cfg(feature = "alloc")] +use crate::primitives::{Polygon, Polyline2d}; + +impl BoundedExtrusion for Circle { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let isometry = isometry.into(); + + let segment_dir = isometry.rotation * Vec3A::Z; + let top = (segment_dir * half_depth).abs(); + + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); + + Aabb3d { + min: isometry.translation - half_size - top, + max: isometry.translation + half_size + top, + } + } +} + +impl BoundedExtrusion for Ellipse { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let Vec2 { x: a, y: b } = self.half_size; + let normal = isometry.rotation * Vec3A::Z; + let conjugate_rot = isometry.rotation.conjugate(); + + let [max_x, max_y, max_z] = Vec3A::AXES.map(|axis| { + let Some(axis) = (conjugate_rot * axis.reject_from(normal)) + .xy() + .try_normalize() + else { + return Vec3A::ZERO; + }; + + if axis.element_product() == 0. { + return isometry.rotation * Vec3A::new(a * axis.y, b * axis.x, 0.); + } + let m = -axis.x / axis.y; + let signum = axis.signum(); + + let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a); + let x = signum.x * a * ops::sqrt(1. - y * y / b / b); + isometry.rotation * Vec3A::new(x, y, 0.) + }); + + let half_size = Vec3A::new(max_x.x, max_y.y, max_z.z).abs() + (normal * half_depth).abs(); + Aabb3d::new(isometry.translation, half_size) + } +} + +impl BoundedExtrusion for Line2d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let dir = isometry.rotation * Vec3A::from(self.direction.extend(0.)); + let half_depth = (isometry.rotation * Vec3A::new(0., 0., half_depth)).abs(); + + let max = f32::MAX / 2.; + let half_size = Vec3A::new( + if dir.x == 0. { half_depth.x } else { max }, + if dir.y == 0. { half_depth.y } else { max }, + if dir.z == 0. { half_depth.z } else { max }, + ); + + Aabb3d::new(isometry.translation, half_size) + } +} + +impl BoundedExtrusion for Segment2d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let half_size = isometry.rotation * Vec3A::from(self.point1().extend(0.)); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + Aabb3d::new(isometry.translation, half_size.abs() + depth.abs()) + } +} + +#[cfg(feature = "alloc")] +impl BoundedExtrusion for Polyline2d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + aabb.grow(depth.abs()) + } +} + +impl BoundedExtrusion for Triangle2d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + aabb.grow(depth.abs()) + } +} + +impl BoundedExtrusion for Rectangle { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + Cuboid { + half_size: self.half_size.extend(half_depth), + } + .aabb_3d(isometry) + } +} + +#[cfg(feature = "alloc")] +impl BoundedExtrusion for Polygon { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + aabb.grow(depth.abs()) + } +} + +impl BoundedExtrusion for RegularPolygon { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let aabb = Aabb3d::from_point_cloud( + isometry, + self.vertices(0.).into_iter().map(|v| v.extend(0.)), + ); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + aabb.grow(depth.abs()) + } +} + +impl BoundedExtrusion for Capsule2d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let aabb = Cylinder { + half_height: half_depth, + radius: self.radius, + } + .aabb_3d(isometry.rotation * Quat::from_rotation_x(FRAC_PI_2)); + + let up = isometry.rotation * Vec3A::new(0., self.half_length, 0.); + let half_size = aabb.max + up.abs(); + Aabb3d::new(isometry.translation, half_size) + } +} + +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) + } + + fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { + self.base_shape + .extrusion_bounding_sphere(self.half_depth, isometry) + } +} + +/// A trait implemented on 2D shapes which determines the 3D bounding volumes of their extrusions. +/// +/// Since default implementations can be inferred from 2D bounding volumes, this allows a `Bounded2d` +/// implementation on some shape `MyShape` to be extrapolated to a `Bounded3d` implementation on +/// `Extrusion` without supplying any additional data; e.g.: +/// `impl BoundedExtrusion for MyShape {}` +pub trait BoundedExtrusion: Primitive2d + Bounded2d { + /// Get an axis-aligned bounding box for an extrusion with this shape as a base and the given `half_depth`, transformed by the given `translation` and `rotation`. + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { + let isometry = isometry.into(); + let cap_normal = isometry.rotation * Vec3A::Z; + let conjugate_rot = isometry.rotation.conjugate(); + + // The `(halfsize, offset)` for each axis + let axis_values = Vec3A::AXES.map(|ax| { + // This is the direction of the line of intersection of a plane with the `ax` normal and the plane containing the cap of the extrusion. + let intersect_line = ax.cross(cap_normal); + if intersect_line.length_squared() <= f32::EPSILON { + return (0., 0.); + }; + + // This is the normal vector of the intersection line rotated to be in the XY-plane + let line_normal = (conjugate_rot * intersect_line).yx(); + let angle = line_normal.to_angle(); + + // Since the plane containing the caps of the extrusion is not guaranteed to be orthogonal to the `ax` plane, only a certain "scale" factor + // of the `Aabb2d` will actually go towards the dimensions of the `Aabb3d` + let scale = cap_normal.reject_from(ax).length(); + + // Calculate the `Aabb2d` of the base shape. The shape is rotated so that the line of intersection is parallel to the Y axis in the `Aabb2d` calculations. + // This guarantees that the X value of the `Aabb2d` is closest to the `ax` plane + let aabb2d = self.aabb_2d(Rot2::radians(angle)); + (aabb2d.half_size().x * scale, aabb2d.center().x * scale) + }); + + let offset = Vec3A::from_array(axis_values.map(|(_, offset)| offset)); + let cap_size = Vec3A::from_array(axis_values.map(|(max_val, _)| max_val)).abs(); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); + + Aabb3d::new(isometry.translation - offset, cap_size + depth.abs()) + } + + /// Get a bounding sphere for an extrusion of the `base_shape` with the given `half_depth` with the given translation and rotation + fn extrusion_bounding_sphere( + &self, + half_depth: f32, + isometry: impl Into, + ) -> BoundingSphere { + let isometry = isometry.into(); + + // We calculate the bounding circle of the base shape. + // Since each of the extrusions bases will have the same distance from its center, + // and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere + // of the cylinder defined by the two bounding circles of the bases for any base shape + let BoundingCircle { + center, + circle: Circle { radius }, + } = self.bounding_circle(Isometry2d::IDENTITY); + let radius = ops::hypot(radius, half_depth); + let center = isometry * Vec3A::from(center.extend(0.)); + + BoundingSphere::new(center, radius) + } +} + +#[cfg(test)] +mod tests { + use core::f32::consts::FRAC_PI_4; + + use crate::primitives::{ + Capsule2d, Circle, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Rectangle, + RegularPolygon, Segment2d, Triangle2d, + }; + use bevy_math::{ + bounding::{Bounded3d, BoundingVolume}, + ops, Dir2, EulerRot, Isometry3d, Quat, Vec2, Vec3, Vec3A, + }; + + #[test] + fn circle() { + let cylinder = Extrusion::new(Circle::new(0.5), 2.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cylinder.aabb_3d(translation); + assert_eq!(aabb.center(), Vec3A::from(translation)); + assert_eq!(aabb.half_size(), Vec3A::new(0.5, 0.5, 1.0)); + + let bounding_sphere = cylinder.bounding_sphere(translation); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); + } + + #[test] + fn ellipse() { + let extrusion = Extrusion::new(Ellipse::new(2.0, 0.5), 4.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_euler(EulerRot::ZYX, FRAC_PI_4, FRAC_PI_4, FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), Vec3A::from(translation)); + assert_eq!(aabb.half_size(), Vec3A::new(2.709784, 1.3801551, 2.436141)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); + } + + #[test] + fn line() { + let extrusion = Extrusion::new( + Line2d { + direction: Dir2::new_unchecked(Vec2::Y), + }, + 4., + ); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_y(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.min, Vec3A::new(1.5857864, f32::MIN / 2., 3.5857865)); + assert_eq!(aabb.max, Vec3A::new(4.4142136, f32::MAX / 2., 6.414213)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center(), translation.into()); + assert_eq!(bounding_sphere.radius(), f32::MAX / 2.); + } + + #[test] + fn rectangle() { + let extrusion = Extrusion::new(Rectangle::new(2.0, 1.0), 4.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_z(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(1.0606602, 1.0606602, 2.)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.291288); + } + + #[test] + fn segment() { + let extrusion = Extrusion::new( + Segment2d::new(Vec2::new(0.0, -1.5), Vec2::new(0.0, 1.5)), + 4.0, + ); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(0., 2.4748735, 2.4748735)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.5); + } + + #[test] + fn polyline() { + let polyline = Polyline2d::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let extrusion = Extrusion::new(polyline, 3.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.0615528); + } + + #[test] + fn triangle() { + let triangle = Triangle2d::new( + Vec2::new(0.0, 1.0), + Vec2::new(-10.0, -1.0), + Vec2::new(10.0, -1.0), + ); + let extrusion = Extrusion::new(triangle, 3.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(10., 1.7677668, 1.7677668)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!( + bounding_sphere.center, + Vec3A::new(3.0, 3.2928934, 4.2928934) + ); + assert_eq!(bounding_sphere.radius(), 10.111875); + } + + #[test] + fn polygon() { + let polygon = Polygon::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let extrusion = Extrusion::new(polygon, 3.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.0615528); + } + + #[test] + fn regular_polygon() { + let extrusion = Extrusion::new(RegularPolygon::new(2.0, 7), 4.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!( + aabb.center(), + Vec3A::from(translation) + Vec3A::new(0., 0.0700254, 0.0700254) + ); + assert_eq!( + aabb.half_size(), + Vec3A::new(1.9498558, 2.7584014, 2.7584019) + ); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); + } + + #[test] + fn capsule() { + let extrusion = Extrusion::new(Capsule2d::new(0.5, 2.0), 4.0); + let translation = Vec3::new(3., 4., 5.); + let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); + + let aabb = extrusion.aabb_3d(isometry); + assert_eq!(aabb.center(), translation.into()); + assert_eq!(aabb.half_size(), Vec3A::new(0.5, 2.4748735, 2.4748735)); + + let bounding_sphere = extrusion.bounding_sphere(isometry); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.5); + } +} diff --git a/crates/bevy_shapes/src/bounding/mod.rs b/crates/bevy_shapes/src/bounding/mod.rs new file mode 100644 index 0000000000000..4837e7d6726c6 --- /dev/null +++ b/crates/bevy_shapes/src/bounding/mod.rs @@ -0,0 +1,3 @@ +pub mod dim2; +pub mod dim3; +pub mod extrusion; diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index e49b605153e26..92ebe67cdccf8 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -24,3 +24,6 @@ pub mod primitives; #[cfg(all(feature = "meshing", feature = "alloc"))] pub mod meshing; + +#[cfg(feature = "bounding")] +pub mod bounding; From 41c44882505c06c3fa0e867a8ca41976d548dc80 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 18:51:55 +0200 Subject: [PATCH 05/17] make BoundingCircle not depend on Circle primitive --- .../bevy_math/src/bounding/bounded2d/mod.rs | 28 ++++++++++++------- .../src/bounding/bounded3d/extrusion.rs | 5 +--- .../src/bounding/bounded3d/primitive_impls.rs | 4 +-- crates/bevy_math/src/bounding/raycast2d.rs | 2 +- crates/bevy_shapes/src/bounding/dim3.rs | 4 +-- crates/bevy_shapes/src/bounding/extrusion.rs | 5 +--- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 40920402ab2d9..136a90f64669d 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -469,8 +469,6 @@ mod aabb2d_tests { } } -use crate::primitives::Circle; - /// A bounding circle #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr( @@ -486,8 +484,8 @@ use crate::primitives::Circle; pub struct BoundingCircle { /// The center of the bounding circle pub center: Vec2, - /// The circle - pub circle: Circle, + /// The radius of the circle + pub radius: f32, } impl BoundingCircle { @@ -495,10 +493,7 @@ impl BoundingCircle { #[inline] pub const fn new(center: Vec2, radius: f32) -> Self { debug_assert!(radius >= 0.); - Self { - center, - circle: Circle { radius }, - } + Self { center, radius } } /// Computes a [`BoundingCircle`] containing the given set of points, @@ -526,7 +521,7 @@ impl BoundingCircle { /// Get the radius of the bounding circle #[inline] pub const fn radius(&self) -> f32 { - self.circle.radius + self.radius } /// Computes the smallest [`Aabb2d`] containing this [`BoundingCircle`]. @@ -544,7 +539,20 @@ impl BoundingCircle { /// Otherwise, it will be inside the circle and returned as is. #[inline] pub fn closest_point(&self, point: Vec2) -> Vec2 { - self.circle.closest_point(point - self.center) + self.center + let centered_point = point - self.center; + let distance_squared = centered_point.length_squared(); + + let closest_point = if distance_squared <= self.radius.squared() { + // The point is inside the circle. + point + } else { + // The point is outside the circle. + // Find the closest point on the perimeter of the circle. + let dir_to_point = point / ops::sqrt(distance_squared); + self.radius * dir_to_point + }; + + closest_point + self.center } } diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs index e37783576caac..58f7dde082a6c 100644 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs @@ -245,10 +245,7 @@ pub trait BoundedExtrusion: Primitive2d + Bounded2d { // Since each of the extrusions bases will have the same distance from its center, // and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere // of the cylinder defined by the two bounding circles of the bases for any base shape - let BoundingCircle { - center, - circle: Circle { radius }, - } = self.bounding_circle(Isometry2d::IDENTITY); + let BoundingCircle { center, radius } = self.bounding_circle(Isometry2d::IDENTITY); let radius = ops::hypot(radius, half_depth); let center = isometry * Vec3A::from(center.extend(0.)); diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 7cf118e4d97e8..29780a9a6fbd6 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -203,11 +203,11 @@ impl Bounded3d for Cone { // Because of circular symmetry, we can use the bounding circle of the triangle // for the bounding sphere of the cone. - let BoundingCircle { circle, center } = triangle.bounding_circle(Isometry2d::IDENTITY); + let BoundingCircle { radius, center } = triangle.bounding_circle(Isometry2d::IDENTITY); BoundingSphere::new( isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation, - circle.radius, + radius, ) } } diff --git a/crates/bevy_math/src/bounding/raycast2d.rs b/crates/bevy_math/src/bounding/raycast2d.rs index aa8fcf2591dcc..875372daa9ecc 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_math/src/bounding/raycast2d.rs @@ -172,7 +172,7 @@ impl BoundingCircleCast { /// Get the distance at which the [`BoundingCircle`]s collide, if at all. pub fn circle_collision_at(&self, mut circle: BoundingCircle) -> Option { circle.center -= self.circle.center; - circle.circle.radius += self.circle.radius(); + circle.radius += self.circle.radius(); self.ray.circle_intersection_at(&circle) } } diff --git a/crates/bevy_shapes/src/bounding/dim3.rs b/crates/bevy_shapes/src/bounding/dim3.rs index 3a91b712371ba..2dae4d9098b8c 100644 --- a/crates/bevy_shapes/src/bounding/dim3.rs +++ b/crates/bevy_shapes/src/bounding/dim3.rs @@ -200,11 +200,11 @@ impl Bounded3d for Cone { // Because of circular symmetry, we can use the bounding circle of the triangle // for the bounding sphere of the cone. - let BoundingCircle { circle, center } = triangle.bounding_circle(Isometry2d::IDENTITY); + let BoundingCircle { radius, center } = triangle.bounding_circle(Isometry2d::IDENTITY); BoundingSphere::new( isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation, - circle.radius, + radius, ) } } diff --git a/crates/bevy_shapes/src/bounding/extrusion.rs b/crates/bevy_shapes/src/bounding/extrusion.rs index 8e701ac03b7b2..da3f7672feb4b 100644 --- a/crates/bevy_shapes/src/bounding/extrusion.rs +++ b/crates/bevy_shapes/src/bounding/extrusion.rs @@ -238,10 +238,7 @@ pub trait BoundedExtrusion: Primitive2d + Bounded2d { // Since each of the extrusions bases will have the same distance from its center, // and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere // of the cylinder defined by the two bounding circles of the bases for any base shape - let BoundingCircle { - center, - circle: Circle { radius }, - } = self.bounding_circle(Isometry2d::IDENTITY); + let BoundingCircle { center, radius } = self.bounding_circle(Isometry2d::IDENTITY); let radius = ops::hypot(radius, half_depth); let center = isometry * Vec3A::from(center.extend(0.)); From ef0a0e1b878ff27e317a72a54408937b39042b92 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 19:10:56 +0200 Subject: [PATCH 06/17] make bevy_math standard sampling standalone (uncouple from shapes) --- crates/bevy_math/src/sampling/standard.rs | 35 ++++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/sampling/standard.rs b/crates/bevy_math/src/sampling/standard.rs index fdf7d4cee781b..49446c13aac69 100644 --- a/crates/bevy_math/src/sampling/standard.rs +++ b/crates/bevy_math/src/sampling/standard.rs @@ -21,12 +21,10 @@ //! 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}; +use glam::{Vec2, Vec3}; use rand::{ distr::{Distribution, StandardUniform}, RngExt, @@ -53,23 +51,34 @@ where } } +fn sample_unit_circle(rng: &mut R) -> Vec2 { + let theta = rng.random_range(0.0..TAU); + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(cos, sin) +} + impl Distribution for StandardUniform { #[inline] fn sample(&self, rng: &mut R) -> Dir2 { - let circle = Circle::new(1.0); - let vector = circle.sample_boundary(rng); - Dir2::new_unchecked(vector) + Dir2::new_unchecked(sample_unit_circle(rng)) } } impl FromRng for Dir2 {} +fn sample_unit_sphere(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); - Dir3::new_unchecked(vector) + Dir3::new_unchecked(sample_unit_sphere(rng)) } } @@ -78,9 +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(); - Dir3A::new_unchecked(vector) + Dir3A::new_unchecked(sample_unit_sphere(rng).into()) } } From 7640ad7cec18447e49173c3bf240cb6c5b470aac Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 19:10:56 +0200 Subject: [PATCH 07/17] impl sampling for bevy_shapes --- crates/bevy_shapes/Cargo.toml | 9 +- crates/bevy_shapes/src/lib.rs | 3 + .../bevy_shapes/src/sampling/mesh_sampling.rs | 61 ++ crates/bevy_shapes/src/sampling/mod.rs | 11 + .../src/sampling/shape_sampling.rs | 695 ++++++++++++++++++ 5 files changed, 777 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_shapes/src/sampling/mesh_sampling.rs create mode 100644 crates/bevy_shapes/src/sampling/mod.rs create mode 100644 crates/bevy_shapes/src/sampling/shape_sampling.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index c0f84e7346a38..48b005fc2c864 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -23,6 +23,8 @@ derive_more = { default-features = false, features = [ "into", ], version = "2" } hexasphere = { default-features = false, optional = true, version = "19.0.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" } @@ -34,14 +36,17 @@ bevy_math = { default-features = false, features = [ "approx", ], path = "../bevy_math" } bevy_platform = { default-features = false, path = "../bevy_platform" } +chacha20 = { default-features = false, features = ["rng"], version = "0.10.0" } [features] alloc = ["bevy_math/alloc"] default = ["bevy_math/default", "std"] std = ["alloc", "bevy_math/std"] -approx = ["bevy_math/approx", "dep:approx"] +approx = ["bevy_math/approx", "dep:approx"] bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] +serialize = ["bevy_math/serialize", "dep:serde"] + bounding = ["dep:arrayvec"] extrusion = ["meshing"] meshing = [ @@ -51,7 +56,7 @@ meshing = [ "dep:hexasphere", "dep:wgpu-types", ] -serialize = ["bevy_math/serialize", "dep:serde"] +sampling = ["bevy_math/rand", "dep:rand", "dep:rand_distr"] [lints] workspace = true diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index 92ebe67cdccf8..8aeac60c1ded3 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -27,3 +27,6 @@ pub mod meshing; #[cfg(feature = "bounding")] pub mod bounding; + +#[cfg(feature = "sampling")] +pub mod sampling; diff --git a/crates/bevy_shapes/src/sampling/mesh_sampling.rs b/crates/bevy_shapes/src/sampling/mesh_sampling.rs new file mode 100644 index 0000000000000..612ab3e4947d2 --- /dev/null +++ b/crates/bevy_shapes/src/sampling/mesh_sampling.rs @@ -0,0 +1,61 @@ +//! Functionality related to random sampling from triangle meshes. + +use crate::primitives::{Measured2d, Triangle3d}; +use crate::sampling::ShapeSample; +use alloc::vec::Vec; +use bevy_math::Vec3; +use rand::RngExt; +use rand_distr::{ + weighted::{Error as WeightedError, WeightedAliasIndex}, + Distribution, +}; + +/// A [distribution] that caches data to allow fast sampling from a collection of triangles. +/// Generally used through [`sample`] or [`sample_iter`]. +/// +/// [distribution]: Distribution +/// [`sample`]: Distribution::sample +/// [`sample_iter`]: Distribution::sample_iter +/// +/// Example +/// ``` +/// # use bevy_math::{Vec3, primitives::*}; +/// # use bevy_math::sampling::mesh_sampling::UniformMeshSampler; +/// # use rand::{SeedableRng, rngs::StdRng, distr::Distribution}; +/// let faces = Tetrahedron::default().faces(); +/// let sampler = UniformMeshSampler::try_new(faces).unwrap(); +/// let rng = StdRng::seed_from_u64(8765309); +/// // 50 random points on the tetrahedron: +/// let samples: Vec = sampler.sample_iter(rng).take(50).collect(); +/// ``` +pub struct UniformMeshSampler { + triangles: Vec, + face_distribution: WeightedAliasIndex, +} + +impl Distribution for UniformMeshSampler { + fn sample(&self, rng: &mut R) -> Vec3 { + let face_index = self.face_distribution.sample(rng); + self.triangles[face_index].sample_interior(rng) + } +} + +impl UniformMeshSampler { + /// Construct a new [`UniformMeshSampler`] from a list of [triangles]. + /// + /// Returns an error if the distribution of areas for the collection of triangles could not be formed + /// (most notably if the collection has zero surface area). + /// + /// [triangles]: Triangle3d + pub fn try_new>( + triangles: T, + ) -> Result { + let triangles: Vec = triangles.into_iter().collect(); + let areas = triangles.iter().map(Measured2d::area).collect(); + + WeightedAliasIndex::new(areas).map(|face_distribution| Self { + triangles, + face_distribution, + }) + } +} diff --git a/crates/bevy_shapes/src/sampling/mod.rs b/crates/bevy_shapes/src/sampling/mod.rs new file mode 100644 index 0000000000000..a6b37ac19547a --- /dev/null +++ b/crates/bevy_shapes/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 = "meshing")] +pub mod mesh_sampling; +pub mod shape_sampling; + +#[cfg(feature = "meshing")] +pub use mesh_sampling::*; +pub use shape_sampling::*; diff --git a/crates/bevy_shapes/src/sampling/shape_sampling.rs b/crates/bevy_shapes/src/sampling/shape_sampling.rs new file mode 100644 index 0000000000000..fa5b4b15de61c --- /dev/null +++ b/crates/bevy_shapes/src/sampling/shape_sampling.rs @@ -0,0 +1,695 @@ +//! The [`ShapeSample`] trait, allowing random sampling from geometric shapes. +//! +//! At the most basic level, this allows sampling random points from the interior and boundary of +//! geometric primitives. For example: +//! ``` +//! # use bevy_math::primitives::*; +//! # use bevy_math::ShapeSample; +//! # use rand::SeedableRng; +//! # use rand::rngs::StdRng; +//! // Get some `RngExt`: +//! let mut rng: StdRng = rand::make_rng(); +//! // Make a circle of radius 2: +//! let circle = Circle::new(2.0); +//! // Get a point inside this circle uniformly at random: +//! let interior_pt = circle.sample_interior(&mut rng); +//! // Get a point on the circle's boundary uniformly at random: +//! let boundary_pt = circle.sample_boundary(&mut rng); +//! ``` +//! +//! For repeated sampling, `ShapeSample` also includes methods for accessing a [`Distribution`]: +//! ``` +//! # use bevy_math::primitives::*; +//! # use bevy_math::{Vec2, ShapeSample}; +//! # use rand::SeedableRng; +//! # use rand::rngs::StdRng; +//! # use rand::distr::Distribution; +//! # let mut rng1: StdRng = rand::make_rng(); +//! # let mut rng2: StdRng = rand::make_rng(); +//! // Use a rectangle this time: +//! let rectangle = Rectangle::new(1.0, 2.0); +//! // Get an iterator that spits out random interior points: +//! let interior_iter = rectangle.interior_dist().sample_iter(&mut rng1); +//! // Collect random interior points from the iterator: +//! let interior_pts: Vec = interior_iter.take(1000).collect(); +//! // Similarly, get an iterator over many random boundary points and collect them: +//! let boundary_pts: Vec = rectangle.boundary_dist().sample_iter(&mut rng2).take(1000).collect(); +//! ``` +//! +//! In any case, the [`RngExt`] used as the source of randomness must be provided explicitly. + +use core::f32::consts::{FRAC_PI_2, PI, TAU}; + +use crate::primitives::*; +use bevy_math::{ops, NormedVectorSpace, ScalarField, Vec2, Vec3}; +use rand::{ + distr::{ + uniform::SampleUniform, + weighted::{Weight, WeightedIndex}, + Distribution, + }, + RngExt, +}; + +/// 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. + type Output; + + /// Uniformly sample a point from inside the area/volume of this shape, centered on 0. + /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 with both x and y between -1 and 1. + /// println!("{}", square.sample_interior(&mut rand::rng())); + /// ``` + fn sample_interior(&self, rng: &mut R) -> Self::Output; + + /// Uniformly sample a point from the surface of this shape, centered on 0. + /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 where one of the coordinates is at ±1, + /// // and the other is somewhere between -1 and 1. + /// println!("{}", square.sample_boundary(&mut rand::rng())); + /// ``` + fn sample_boundary(&self, rng: &mut R) -> Self::Output; + + /// Extract a [`Distribution`] whose samples are points of this shape's interior, taken uniformly. + /// + /// # Example + /// + /// ``` + /// # use bevy_math::prelude::*; + /// # use rand::distr::Distribution; + /// let square = Rectangle::new(2.0, 2.0); + /// let rng = rand::rng(); + /// + /// // Iterate over points randomly drawn from `square`'s interior: + /// for random_val in square.interior_dist().sample_iter(rng).take(5) { + /// println!("{}", random_val); + /// } + /// ``` + fn interior_dist(self) -> impl Distribution + where + Self: Sized, + { + InteriorOf(self) + } + + /// Extract a [`Distribution`] whose samples are points of this shape's boundary, taken uniformly. + /// + /// # Example + /// + /// ``` + /// # use bevy_math::prelude::*; + /// # use rand::distr::Distribution; + /// let square = Rectangle::new(2.0, 2.0); + /// let rng = rand::rng(); + /// + /// // Iterate over points randomly drawn from `square`'s boundary: + /// for random_val in square.boundary_dist().sample_iter(rng).take(5) { + /// println!("{}", random_val); + /// } + /// ``` + fn boundary_dist(self) -> impl Distribution + where + Self: Sized, + { + BoundaryOf(self) + } +} + +#[derive(Clone, Copy)] +/// A wrapper struct that allows interior sampling from a [`ShapeSample`] type directly as +/// a [`Distribution`]. +pub struct InteriorOf(pub T); + +#[derive(Clone, Copy)] +/// A wrapper struct that allows boundary sampling from a [`ShapeSample`] type directly as +/// a [`Distribution`]. +pub struct BoundaryOf(pub T); + +impl Distribution<::Output> for InteriorOf { + fn sample(&self, rng: &mut R) -> ::Output { + self.0.sample_interior(rng) + } +} + +impl Distribution<::Output> for BoundaryOf { + fn sample(&self, rng: &mut R) -> ::Output { + self.0.sample_boundary(rng) + } +} + +impl ShapeSample for Circle { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + // https://mathworld.wolfram.com/DiskPointPicking.html + let theta = rng.random_range(0.0..TAU); + let r_squared = rng.random_range(0.0..=(self.radius * self.radius)); + let r = ops::sqrt(r_squared); + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(r * cos, r * sin) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let theta = rng.random_range(0.0..TAU); + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(self.radius * cos, self.radius * sin) + } +} + +impl ShapeSample for CircularSector { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let theta = rng.random_range(-self.half_angle()..=self.half_angle()); + let r_squared = rng.random_range(0.0..=(self.radius() * self.radius())); + let r = ops::sqrt(r_squared); + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(r * sin, r * cos) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + if rng.random_range(0.0..=1.0) <= self.arc_length() / self.perimeter() { + // Sample on the arc + let theta = FRAC_PI_2 + rng.random_range(-self.half_angle()..self.half_angle()); + Vec2::from_angle(theta) * self.radius() + } else { + // Sample on the "inner" straight lines + let dir = self.radius() * Vec2::from_angle(FRAC_PI_2 + self.half_angle()); + let r: f32 = rng.random_range(-1.0..1.0); + (-r).clamp(0.0, 1.0) * dir + r.clamp(0.0, 1.0) * dir * Vec2::new(-1.0, 1.0) + } + } +} + +/// 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 ShapeSample for Sphere { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let r_cubed = rng.random_range(0.0..=(self.radius * self.radius * self.radius)); + let r = ops::cbrt(r_cubed); + + r * sample_unit_sphere_boundary(rng) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + self.radius * sample_unit_sphere_boundary(rng) + } +} + +impl ShapeSample for Annulus { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + let inner_radius = self.inner_circle.radius; + let outer_radius = self.outer_circle.radius; + + // Like random sampling for a circle, radius is weighted by the square. + let r_squared = + rng.random_range((inner_radius * inner_radius)..(outer_radius * outer_radius)); + let r = ops::sqrt(r_squared); + let theta = rng.random_range(0.0..TAU); + let (sin, cos) = ops::sin_cos(theta); + + Vec2::new(r * cos, r * sin) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + let total_perimeter = self.inner_circle.perimeter() + self.outer_circle.perimeter(); + let inner_prob = (self.inner_circle.perimeter() / total_perimeter) as f64; + + // Sample from boundary circles, choosing which one by weighting by perimeter: + let inner = rng.random_bool(inner_prob); + if inner { + self.inner_circle.sample_boundary(rng) + } else { + self.outer_circle.sample_boundary(rng) + } + } +} + +impl ShapeSample for Rhombus { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let x: f32 = rng.random_range(0.0..=1.0); + let y: f32 = rng.random_range(0.0..=1.0); + + let unit_p = Vec2::NEG_X + x * Vec2::ONE + Vec2::new(y, -y); + unit_p * self.half_diagonals + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let x: f32 = rng.random_range(-1.0..=1.0); + let y_sign = if rng.random() { -1.0 } else { 1.0 }; + + let y = (1.0 - ops::abs(x)) * y_sign; + Vec2::new(x, y) * self.half_diagonals + } +} + +impl ShapeSample for Rectangle { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let x = rng.random_range(-self.half_size.x..=self.half_size.x); + let y = rng.random_range(-self.half_size.y..=self.half_size.y); + Vec2::new(x, y) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let primary_side = rng.random_range(-1.0..1.0); + let other_side = if rng.random() { -1.0 } else { 1.0 }; + + if self.half_size.x + self.half_size.y > 0.0 { + if rng.random_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { + Vec2::new(primary_side, other_side) * self.half_size + } else { + Vec2::new(other_side, primary_side) * self.half_size + } + } else { + Vec2::ZERO + } + } +} + +impl ShapeSample for Cuboid { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let x = rng.random_range(-self.half_size.x..=self.half_size.x); + let y = rng.random_range(-self.half_size.y..=self.half_size.y); + let z = rng.random_range(-self.half_size.z..=self.half_size.z); + Vec3::new(x, y, z) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + let primary_side1 = rng.random_range(-1.0..1.0); + let primary_side2 = rng.random_range(-1.0..1.0); + let other_side = if rng.random() { -1.0 } else { 1.0 }; + + if let Ok(dist) = WeightedIndex::new([ + self.half_size.y * self.half_size.z, + self.half_size.x * self.half_size.z, + self.half_size.x * self.half_size.y, + ]) { + match dist.sample(rng) { + 0 => Vec3::new(other_side, primary_side1, primary_side2) * self.half_size, + 1 => Vec3::new(primary_side1, other_side, primary_side2) * self.half_size, + 2 => Vec3::new(primary_side1, primary_side2, other_side) * self.half_size, + _ => unreachable!(), + } + } else { + Vec3::ZERO + } + } +} + +/// Interior sampling for triangles which doesn't depend on the ambient dimension. +fn sample_triangle_interior(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd, + R: RngExt + ?Sized, +{ + let [a, b, c] = vertices; + let ab = b - a; + let ac = c - a; + + // Generate random points on a parallelepiped and reflect so that + // we can use the points that lie outside the triangle + let u = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE); + let v = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE); + + if u + v > P::Scalar::ONE { + let u1 = P::Scalar::ONE - v; + let v1 = P::Scalar::ONE - u; + a + (ab * u1 + ac * v1) + } else { + a + (ab * u + ac * v) + } +} + +/// Boundary sampling for triangles which doesn't depend on the ambient dimension. +fn sample_triangle_boundary(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: Weight + SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>, + R: RngExt + ?Sized, +{ + let [a, b, c] = vertices; + let ab = b - a; + let ac = c - a; + let bc = c - b; + + let t = rng.random_range(::ZERO..=P::Scalar::ONE); + + if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { + match dist.sample(rng) { + 0 => a.lerp(b, t), + 1 => a.lerp(c, t), + 2 => b.lerp(c, t), + _ => unreachable!(), + } + } else { + // This should only occur when the triangle is 0-dimensional degenerate + // so this is actually the correct result. + a + } +} + +impl ShapeSample for Triangle2d { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + sample_triangle_interior(self.vertices, rng) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + sample_triangle_boundary(self.vertices, rng) + } +} + +impl ShapeSample for Triangle3d { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + sample_triangle_interior(self.vertices, rng) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + sample_triangle_boundary(self.vertices, rng) + } +} + +impl ShapeSample for Tetrahedron { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + let [v0, v1, v2, v3] = self.vertices; + + // Generate a random point in a cube: + let mut coords: [f32; 3] = [ + rng.random_range(0.0..1.0), + rng.random_range(0.0..1.0), + rng.random_range(0.0..1.0), + ]; + + // The cube is broken into six tetrahedra of the form 0 <= c_0 <= c_1 <= c_2 <= 1, + // where c_i are the three euclidean coordinates in some permutation. (Since 3! = 6, + // there are six of them). Sorting the coordinates folds these six tetrahedra into the + // tetrahedron 0 <= x <= y <= z <= 1 (i.e. a fundamental domain of the permutation action). + coords.sort_by(|x, y| x.partial_cmp(y).unwrap()); + + // Now, convert a point from the fundamental tetrahedron into barycentric coordinates by + // taking the four successive differences of coordinates; note that these telescope to sum + // to 1, and this transformation is linear, hence preserves the probability density, since + // the latter comes from the Lebesgue measure. + // + // (See https://en.wikipedia.org/wiki/Lebesgue_measure#Properties — specifically, that + // Lebesgue measure of a linearly transformed set is its original measure times the + // determinant.) + let (a, b, c, d) = ( + coords[0], + coords[1] - coords[0], + coords[2] - coords[1], + 1. - coords[2], + ); + + // This is also a linear mapping, so probability density is still preserved. + v0 * a + v1 * b + v2 * c + v3 * d + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + let triangles = self.faces(); + let areas = triangles.iter().map(Measured2d::area); + + if areas.clone().sum::() > 0.0 { + // There is at least one triangle with nonzero area, so this unwrap succeeds. + let dist = WeightedIndex::new(areas).unwrap(); + + // Get a random index, then sample the interior of the associated triangle. + let idx = dist.sample(rng); + triangles[idx].sample_interior(rng) + } else { + // In this branch the tetrahedron has zero surface area; just return a point that's on + // the tetrahedron. + self.vertices[0] + } + } +} + +impl ShapeSample for Cylinder { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let Vec2 { x, y: z } = self.base().sample_interior(rng); + let y = rng.random_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + // This uses the area of the ends divided by the overall surface area (optimized) + // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) + if self.radius + 2.0 * self.half_height > 0.0 { + if rng.random_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { + let Vec2 { x, y: z } = self.base().sample_interior(rng); + if rng.random() { + Vec3::new(x, self.half_height, z) + } else { + Vec3::new(x, -self.half_height, z) + } + } else { + let Vec2 { x, y: z } = self.base().sample_boundary(rng); + let y = rng.random_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + } else { + Vec3::ZERO + } + } +} + +impl ShapeSample for Capsule2d { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let rectangle_area = self.half_length * self.radius * 4.0; + let capsule_area = rectangle_area + PI * self.radius * self.radius; + if capsule_area > 0.0 { + // Check if the random point should be inside the rectangle + if rng.random_bool((rectangle_area / capsule_area) as f64) { + self.to_inner_rectangle().sample_interior(rng) + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_interior(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } else { + Vec2::ZERO + } + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let rectangle_surface = 4.0 * self.half_length; + let capsule_surface = rectangle_surface + TAU * self.radius; + if capsule_surface > 0.0 { + if rng.random_bool((rectangle_surface / capsule_surface) as f64) { + let side_distance = + rng.random_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); + if side_distance < 0.0 { + Vec2::new(self.radius, side_distance + self.half_length) + } else { + Vec2::new(-self.radius, side_distance - self.half_length) + } + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_boundary(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } else { + Vec2::ZERO + } + } +} + +impl ShapeSample for Capsule3d { + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; + // Add 4/3 pi r^3 + let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; + if capsule_vol > 0.0 { + // Check if the random point should be inside the cylinder + if rng.random_bool((cylinder_vol / capsule_vol) as f64) { + self.to_cylinder().sample_interior(rng) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_interior(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } else { + Vec3::ZERO + } + } + + fn sample_boundary(&self, rng: &mut R) -> Vec3 { + let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; + let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; + if capsule_surface > 0.0 { + if rng.random_bool((cylinder_surface / capsule_surface) as f64) { + let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); + let y = rng.random_range(-self.half_length..=self.half_length); + Vec3::new(x, y, z) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_boundary(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } else { + Vec3::ZERO + } + } +} + +impl> ShapeSample for Extrusion

{ + type Output = Vec3; + + fn sample_interior(&self, rng: &mut R) -> Self::Output { + let base_point = self.base_shape.sample_interior(rng); + let depth = rng.random_range(-self.half_depth..self.half_depth); + base_point.extend(depth) + } + + fn sample_boundary(&self, rng: &mut R) -> Self::Output { + let base_area = self.base_shape.area(); + let total_area = self.area(); + + let random = rng.random_range(0.0..total_area); + match random { + x if x < base_area => self.base_shape.sample_interior(rng).extend(self.half_depth), + x if x < 2. * base_area => self + .base_shape + .sample_interior(rng) + .extend(-self.half_depth), + _ => self + .base_shape + .sample_boundary(rng) + .extend(rng.random_range(-self.half_depth..self.half_depth)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chacha20::ChaCha8Rng; + use rand::SeedableRng; + + #[test] + fn circle_interior_sampling() { + let mut rng = ChaCha8Rng::from_seed(Default::default()); + let circle = Circle::new(8.0); + + let boxes = [ + (-3.0, 3.0), + (1.0, 2.0), + (-1.0, -2.0), + (3.0, -2.0), + (1.0, -6.0), + (-3.0, -7.0), + (-7.0, -3.0), + (-6.0, 1.0), + ]; + let mut box_hits = [0; 8]; + + // Checks which boxes (if any) the sampled points are in + for _ in 0..5000 { + let point = circle.sample_interior(&mut rng); + + for (i, box_) in boxes.iter().enumerate() { + if (point.x > box_.0 && point.x < box_.0 + 4.0) + && (point.y > box_.1 && point.y < box_.1 + 4.0) + { + box_hits[i] += 1; + } + } + } + + assert_eq!( + box_hits, + [396, 377, 415, 404, 366, 408, 408, 430], + "samples will occur across all array items at statistically equal chance" + ); + } + + #[test] + fn circle_boundary_sampling() { + let mut rng = ChaCha8Rng::from_seed(Default::default()); + let circle = Circle::new(1.0); + + let mut wedge_hits = [0; 8]; + + // Checks in which eighth of the circle each sampled point is in + for _ in 0..5000 { + let point = circle.sample_boundary(&mut rng); + + let angle = ops::atan(point.y / point.x) + PI / 2.0; + let wedge = ops::floor(angle * 8.0 / PI) as usize; + wedge_hits[wedge] += 1; + } + + assert_eq!( + wedge_hits, + [636, 608, 639, 603, 614, 650, 640, 610], + "samples will occur across all array items at statistically equal chance" + ); + } +} From b91a86c5205dd238a71a05898d775b97915eb96b Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 20:32:33 +0200 Subject: [PATCH 08/17] expose enabled field on gizmos to access it in bevy_shapes for primitive gizmos --- crates/bevy_gizmos/src/gizmos.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 02f13609e3470..0588ec737d242 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -288,7 +288,9 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { - pub(crate) enabled: bool, + // needs to be public for [`bevy_shapes`] + /// whether or not this buffer will be rendered + pub enabled: bool, /// The positions of line segment endpoints. pub list_positions: Vec, /// The colors of line segment endpoints. From d27f74da52f88de0ec809bea0647cc1d8ef413a0 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 20:32:33 +0200 Subject: [PATCH 09/17] implement gizmos for primitives --- crates/bevy_shapes/Cargo.toml | 3 + crates/bevy_shapes/src/gizmos/dim2.rs | 811 ++++++++++++++++++ crates/bevy_shapes/src/gizmos/dim3.rs | 948 ++++++++++++++++++++++ crates/bevy_shapes/src/gizmos/helpers.rs | 45 + crates/bevy_shapes/src/gizmos/mod.rs | 5 + crates/bevy_shapes/src/lib.rs | 3 + crates/bevy_shapes/src/primitives/math.rs | 6 + crates/bevy_shapes/src/primitives/mod.rs | 1 + 8 files changed, 1822 insertions(+) create mode 100644 crates/bevy_shapes/src/gizmos/dim2.rs create mode 100644 crates/bevy_shapes/src/gizmos/dim3.rs create mode 100644 crates/bevy_shapes/src/gizmos/helpers.rs create mode 100644 crates/bevy_shapes/src/gizmos/mod.rs create mode 100644 crates/bevy_shapes/src/primitives/math.rs diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index 48b005fc2c864..36952056b89e4 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -13,6 +13,8 @@ version = "0.20.0-dev" approx = { default-features = false, optional = true, version = "0.5" } arrayvec = { default-features = false, optional = true, version = "0.7" } bevy_asset = { default-features = false, optional = true, path = "../bevy_asset" } +bevy_color = { default-features = false, optional = true, path = "../bevy_color" } +bevy_gizmos = { default-features = false, optional = true, path = "../bevy_gizmos" } bevy_math = { default-features = false, path = "../bevy_math" } bevy_mesh = { default-features = false, optional = true, path = "../bevy_mesh" } bevy_reflect = { default-features = false, features = [ @@ -49,6 +51,7 @@ serialize = ["bevy_math/serialize", "dep:serde"] bounding = ["dep:arrayvec"] extrusion = ["meshing"] +gizmos = ["dep:bevy_color", "dep:bevy_gizmos"] meshing = [ "bevy_reflect", "dep:bevy_asset", diff --git a/crates/bevy_shapes/src/gizmos/dim2.rs b/crates/bevy_shapes/src/gizmos/dim2.rs new file mode 100644 index 0000000000000..afda137c53887 --- /dev/null +++ b/crates/bevy_shapes/src/gizmos/dim2.rs @@ -0,0 +1,811 @@ +//! A module for rendering each of the 2D [`bevy_math::primitives`] with [`GizmoBuffer`]. + +use core::f32::consts::{FRAC_PI_2, PI}; + +use crate::gizmos::helpers::*; + +use crate::primitives::{ + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, +}; +use bevy_color::Color; +use bevy_math::{Dir2, Isometry2d, Rot2, Vec2}; + +use bevy_gizmos::{circles::Ellipse2dBuilder, gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; + +// some magic number since using directions as offsets will result in lines of length 1 pixel +const MIN_LINE_LEN: f32 = 50.0; +const HALF_MIN_LINE_LEN: f32 = 25.0; +// length used to simulate infinite lines +const INFINITE_LEN: f32 = 100_000.0; + +/// A trait for rendering 2D geometric primitives (`P`) with [`GizmoBuffer`]. +pub trait GizmoPrimitive2d { + /// The output of `primitive_2d`. This is a builder to set non-default values. + type Output<'a> + where + Self: 'a; + + /// Renders a 2D primitive with its associated details. + fn primitive_2d( + &mut self, + primitive: &P, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_>; +} + +// direction 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Dir2, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + let isometry = isometry.into(); + let start = Vec2::ZERO; + let end = *primitive * MIN_LINE_LEN; + self.arrow_2d(isometry * start, isometry * end, color); + } +} + +// arc 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Arc2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + let start_iso = isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.half_angle)); + + self.arc_2d( + start_iso, + primitive.half_angle * 2.0, + primitive.radius, + color, + ); + } +} + +// circle 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Ellipse2dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Circle, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + self.circle_2d(isometry, primitive.radius, color) + } +} + +// circular sector 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &CircularSector, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + let color = color.into(); + + let start_iso = + isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); + let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); + + // we need to draw the arc part of the sector, and the two lines connecting the arc and the center + self.arc_2d( + start_iso, + primitive.arc.half_angle * 2.0, + primitive.arc.radius, + color, + ); + + let end_position = primitive.arc.radius * Vec2::Y; + self.line_2d(isometry * Vec2::ZERO, start_iso * end_position, color); + self.line_2d(isometry * Vec2::ZERO, end_iso * end_position, color); + } +} + +// circular segment 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &CircularSegment, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + let color = color.into(); + + let start_iso = + isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); + let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); + + // we need to draw the arc part of the segment, and the line connecting the two ends + self.arc_2d( + start_iso, + primitive.arc.half_angle * 2.0, + primitive.arc.radius, + color, + ); + + let position = primitive.arc.radius * Vec2::Y; + self.line_2d(start_iso * position, end_iso * position, color); + } +} + +// ellipse 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Ellipse2dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_2d<'a>( + &mut self, + primitive: &Ellipse, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + self.ellipse_2d(isometry, primitive.half_size, color) + } +} + +// annulus 2d + +/// Builder for configuring the drawing options of [`Annulus`]. +pub struct Annulus2dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + isometry: Isometry2d, + inner_radius: f32, + outer_radius: f32, + color: Color, + inner_resolution: u32, + outer_resolution: u32, +} + +impl Annulus2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of line-segments for each circle of the annulus. + pub fn resolution(mut self, resolution: u32) -> Self { + self.outer_resolution = resolution; + self.inner_resolution = resolution; + self + } + + /// Set the number of line-segments for the outer circle of the annulus. + pub fn outer_resolution(mut self, resolution: u32) -> Self { + self.outer_resolution = resolution; + self + } + + /// Set the number of line-segments for the inner circle of the annulus. + pub fn inner_resolution(mut self, resolution: u32) -> Self { + self.inner_resolution = resolution; + self + } +} + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Annulus2dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Annulus, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Annulus2dBuilder { + gizmos: self, + isometry: isometry.into(), + inner_radius: primitive.inner_circle.radius, + outer_radius: primitive.outer_circle.radius, + color: color.into(), + inner_resolution: DEFAULT_CIRCLE_RESOLUTION, + outer_resolution: DEFAULT_CIRCLE_RESOLUTION, + } + } +} + +impl Drop for Annulus2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let Annulus2dBuilder { + gizmos, + isometry, + inner_radius, + outer_radius, + inner_resolution, + outer_resolution, + color, + .. + } = self; + + gizmos + .circle_2d(*isometry, *outer_radius, *color) + .resolution(*outer_resolution); + gizmos + .circle_2d(*isometry, *inner_radius, *color) + .resolution(*inner_resolution); + } +} + +// rhombus 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Rhombus, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + }; + let isometry = isometry.into(); + let [a, b, c, d] = + [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)].map(|(sign_x, sign_y)| { + Vec2::new( + primitive.half_diagonals.x * sign_x, + primitive.half_diagonals.y * sign_y, + ) + }); + let positions = [a, b, c, d].map(|vec2| isometry * vec2); + self.lineloop_2d(positions, color); + } +} + +// capsule 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Capsule2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + let isometry = isometry.into(); + let polymorphic_color: Color = color.into(); + + if !self.enabled { + return; + } + + // transform points from the reference unit square to capsule "rectangle" + let [top_left, top_right, bottom_left, bottom_right, top_center, bottom_center] = [ + [-1.0, 1.0], + [1.0, 1.0], + [-1.0, -1.0], + [1.0, -1.0], + // just reuse the pipeline for these points as well + [0.0, 1.0], + [0.0, -1.0], + ] + .map(|[sign_x, sign_y]| Vec2::X * sign_x + Vec2::Y * sign_y) + .map(|reference_point| { + let scaling = Vec2::X * primitive.radius + Vec2::Y * primitive.half_length; + reference_point * scaling + }) + .map(|vec2| isometry * vec2); + + // draw left and right side of capsule "rectangle" + self.line_2d(bottom_left, top_left, polymorphic_color); + self.line_2d(bottom_right, top_right, polymorphic_color); + + let start_angle_top = isometry.rotation.as_radians() - FRAC_PI_2; + let start_angle_bottom = isometry.rotation.as_radians() + FRAC_PI_2; + + // draw arcs + self.arc_2d( + Isometry2d::new(top_center, Rot2::radians(start_angle_top)), + PI, + primitive.radius, + polymorphic_color, + ); + self.arc_2d( + Isometry2d::new(bottom_center, Rot2::radians(start_angle_bottom)), + PI, + primitive.radius, + polymorphic_color, + ); + } +} + +// line 2d +// +/// Builder for configuring the drawing options of [`Line2d`]. +pub struct Line2dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + direction: Dir2, // Direction of the line + + isometry: Isometry2d, + color: Color, // color of the line + + draw_arrow: bool, // decides whether to indicate the direction of the line with an arrow +} + +impl Line2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the drawing mode of the line (arrow vs. plain line) + pub fn draw_arrow(mut self, is_enabled: bool) -> Self { + self.draw_arrow = is_enabled; + self + } +} + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Line2dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Line2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Line2dBuilder { + gizmos: self, + direction: primitive.direction, + isometry: isometry.into(), + color: color.into(), + draw_arrow: false, + } + } +} + +impl Drop for Line2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let [start, end] = [1.0, -1.0] + .map(|sign| sign * INFINITE_LEN) + // offset the line from the origin infinitely into the given direction + .map(|length| self.direction * length) + // transform the line with the given isometry + .map(|offset| self.isometry * offset); + + self.gizmos.line_2d(start, end, self.color); + + // optionally draw an arrow head at the center of the line + if self.draw_arrow { + self.gizmos.arrow_2d( + self.isometry * (-self.direction * MIN_LINE_LEN), + self.isometry * Vec2::ZERO, + self.color, + ); + } + } +} + +// plane 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Plane2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + let isometry = isometry.into(); + let polymorphic_color: Color = color.into(); + + if !self.enabled { + return; + } + // draw normal of the plane (orthogonal to the plane itself) + let normal = primitive.normal; + let normal_segment = Segment2d::from_direction_and_length(normal, HALF_MIN_LINE_LEN * 2.); + self.primitive_2d( + &normal_segment, + // offset the normal so it starts on the plane line + Isometry2d::new(isometry * (HALF_MIN_LINE_LEN * normal), isometry.rotation), + polymorphic_color, + ) + .draw_arrow(true); + + // draw the plane line + let direction = Dir2::new_unchecked(-normal.perp()); + self.primitive_2d(&Line2d { direction }, isometry, polymorphic_color) + .draw_arrow(false); + + // draw an arrow such that the normal is always left side of the plane with respect to the + // planes direction. This is to follow the "counter-clockwise" convention + self.arrow_2d( + isometry * Vec2::ZERO, + isometry * (MIN_LINE_LEN * direction), + polymorphic_color, + ); + } +} + +// segment 2d + +/// Builder for configuring the drawing options of [`Segment2d`]. +pub struct Segment2dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + point1: Vec2, // First point of the segment + point2: Vec2, // Second point of the segment + + isometry: Isometry2d, // isometric transformation of the line segment + color: Color, // color of the line segment + + draw_arrow: bool, // decides whether to draw just a line or an arrow +} + +impl Segment2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the drawing mode of the line (arrow vs. plain line) + pub fn draw_arrow(mut self, is_enabled: bool) -> Self { + self.draw_arrow = is_enabled; + self + } +} + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Segment2dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Segment2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Segment2dBuilder { + gizmos: self, + point1: primitive.point1(), + point2: primitive.point2(), + + isometry: isometry.into(), + color: color.into(), + + draw_arrow: Default::default(), + } + } +} + +impl Drop for Segment2dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let segment = Segment2d::new(self.point1, self.point2).transformed(self.isometry); + + if self.draw_arrow { + self.gizmos + .arrow_2d(segment.point1(), segment.point2(), self.color); + } else { + self.gizmos + .line_2d(segment.point1(), segment.point2(), self.color); + } + } +} + +// polyline 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Polyline2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + self.linestrip_2d( + primitive + .vertices + .iter() + .copied() + .map(|vec2| isometry * vec2), + color, + ); + } +} + +// triangle 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Triangle2d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + let [a, b, c] = primitive.vertices; + let positions = [a, b, c].map(|vec2| isometry * vec2); + self.lineloop_2d(positions, color); + } +} + +// rectangle 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Rectangle, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + let [a, b, c, d] = + [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)].map(|(sign_x, sign_y)| { + Vec2::new( + primitive.half_size.x * sign_x, + primitive.half_size.y * sign_y, + ) + }); + let positions = [a, b, c, d].map(|vec2| isometry * vec2); + self.lineloop_2d(positions, color); + } +} + +// polygon 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &Polygon, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + let vertices = if primitive.vertices.first() == primitive.vertices.last() { + // Strip closing point if there is one + &primitive.vertices[..primitive.vertices.len() - 1] + } else { + &primitive.vertices[..] + }; + + self.lineloop_2d(vertices.iter().map(|&vec2| isometry * vec2), color); + } +} + +// regular polygon 2d + +impl GizmoPrimitive2d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_2d( + &mut self, + primitive: &RegularPolygon, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + let points = (0..primitive.sides) + .map(|n| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, n)) + .map(|vec2| isometry * vec2); + self.lineloop_2d(points, color); + } +} diff --git a/crates/bevy_shapes/src/gizmos/dim3.rs b/crates/bevy_shapes/src/gizmos/dim3.rs new file mode 100644 index 0000000000000..080c2011fc01f --- /dev/null +++ b/crates/bevy_shapes/src/gizmos/dim3.rs @@ -0,0 +1,948 @@ +//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`GizmoBuffer`]. + +use super::helpers::*; + +use crate::primitives::{ + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, Primitive3d, + Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, +}; +use bevy_color::Color; +use bevy_math::{Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3}; + +use bevy_gizmos::{circles::SphereBuilder, gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; + +const DEFAULT_RESOLUTION: u32 = 5; +// length used to simulate infinite lines +const INFINITE_LEN: f32 = 10_000.0; + +/// A trait for rendering 3D geometric primitives (`P`) with [`GizmoBuffer`]. +pub trait GizmoPrimitive3d { + /// The output of `primitive_3d`. This is a builder to set non-default values. + type Output<'a> + where + Self: 'a; + + /// Renders a 3D primitive with its associated details. + fn primitive_3d( + &mut self, + primitive: &P, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_>; +} + +// direction 3d + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Dir3, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + let isometry = isometry.into(); + let start = Vec3::ZERO; + let end = primitive.as_vec3(); + self.arrow(isometry * start, isometry * end, color); + } +} + +// sphere + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = SphereBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Sphere, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + self.sphere(isometry, primitive.radius, color) + } +} + +// plane 3d + +/// Builder for configuring the drawing options of [`Plane3d`]. +pub struct Plane3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Direction of the normal orthogonal to the plane + normal: Dir3, + + isometry: Isometry3d, + // Color of the plane + color: Color, + + // Defines the amount of cells in the x and y axes + cell_count: UVec2, + // Defines the distance between cells along the x and y axes + spacing: Vec2, +} + +impl Plane3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of cells in the x and y axes direction. + pub fn cell_count(mut self, cell_count: UVec2) -> Self { + self.cell_count = cell_count; + self + } + + /// Set the distance between cells along the x and y axes. + pub fn spacing(mut self, spacing: Vec2) -> Self { + self.spacing = spacing; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Plane3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Plane3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Plane3dBuilder { + gizmos: self, + normal: primitive.normal, + isometry: isometry.into(), + color: color.into(), + cell_count: UVec2::splat(3), + spacing: Vec2::splat(1.0), + } + } +} + +impl Drop for Plane3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + self.gizmos + .primitive_3d(&self.normal, self.isometry, self.color); + // the default orientation of the grid is Z-up + let rot = Quat::from_rotation_arc(Vec3::Z, self.normal.as_vec3()); + self.gizmos.grid( + Isometry3d::new(self.isometry.translation, self.isometry.rotation * rot), + self.cell_count, + self.spacing, + self.color, + ); + } +} + +// line 3d + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Line3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + let color = color.into(); + let direction = primitive.direction.as_vec3(); + self.arrow(isometry * Vec3::ZERO, isometry * direction, color); + + let [start, end] = [1.0, -1.0] + .map(|sign| sign * INFINITE_LEN) + .map(|length| primitive.direction * length) + .map(|offset| isometry * offset); + self.line(start, end, color); + } +} + +// segment 3d + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Segment3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let transformed = primitive.transformed(isometry); + self.line(transformed.point1(), transformed.point2(), color); + } +} + +// polyline 3d + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Polyline3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + self.linestrip( + primitive.vertices.iter().map(|vec3| isometry * *vec3), + color, + ); + } +} + +// triangle 3d + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Triangle3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + let [a, b, c] = primitive.vertices; + self.lineloop([a, b, c].map(|vec3| isometry * vec3), color); + } +} + +// cuboid + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Cuboid, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + // transform the points from the reference unit cube to the cuboid coords + let vertices @ [a, b, c, d, e, f, g, h] = [ + [1.0, 1.0, 1.0], + [-1.0, 1.0, 1.0], + [-1.0, -1.0, 1.0], + [1.0, -1.0, 1.0], + [1.0, 1.0, -1.0], + [-1.0, 1.0, -1.0], + [-1.0, -1.0, -1.0], + [1.0, -1.0, -1.0], + ] + .map(Vec3::from) + .map(|vec3| vec3 * primitive.half_size) + .map(|vec3| isometry * vec3); + + // lines for the upper rectangle of the cuboid + let upper = [a, b, c, d] + .into_iter() + .zip([a, b, c, d].into_iter().cycle().skip(1)); + + // lines for the lower rectangle of the cuboid + let lower = [e, f, g, h] + .into_iter() + .zip([e, f, g, h].into_iter().cycle().skip(1)); + + // lines connecting upper and lower rectangles of the cuboid + let connections = vertices.into_iter().zip(vertices.into_iter().skip(4)); + + let color = color.into(); + upper + .chain(lower) + .chain(connections) + .for_each(|(start, end)| { + self.line(start, end, color); + }); + } +} + +// cylinder 3d + +/// Builder for configuring the drawing options of [`Cylinder`]. +pub struct Cylinder3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Radius of the cylinder + radius: f32, + // Half height of the cylinder + half_height: f32, + + isometry: Isometry3d, + // Color of the cylinder + color: Color, + + // Number of lines used to approximate the cylinder geometry + resolution: u32, +} + +impl Cylinder3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of lines used to approximate the top and bottom of the cylinder geometry. + pub fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Cylinder3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Cylinder, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Cylinder3dBuilder { + gizmos: self, + radius: primitive.radius, + half_height: primitive.half_height, + isometry: isometry.into(), + color: color.into(), + resolution: DEFAULT_RESOLUTION, + } + } +} + +impl Drop for Cylinder3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + self.gizmos + .primitive_3d( + &ConicalFrustum { + radius_top: self.radius, + radius_bottom: self.radius, + height: self.half_height * 2.0, + }, + self.isometry, + self.color, + ) + .resolution(self.resolution); + } +} + +// capsule 3d + +/// Builder for configuring the drawing options of [`Capsule3d`]. +pub struct Capsule3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Radius of the capsule + radius: f32, + // Half length of the capsule + half_length: f32, + + isometry: Isometry3d, + // Color of the capsule + color: Color, + + // Number of lines used to approximate the capsule geometry + resolution: u32, +} + +impl Capsule3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of lines used to approximate the capsule geometry. + pub fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Capsule3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Capsule3d, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Capsule3dBuilder { + gizmos: self, + radius: primitive.radius, + half_length: primitive.half_length, + isometry: isometry.into(), + color: color.into(), + resolution: DEFAULT_RESOLUTION, + } + } +} + +impl Drop for Capsule3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let [upper_apex, lower_apex] = [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * (self.half_length + self.radius)) + .map(|vec3| self.isometry * vec3); + let [upper_center, lower_center] = [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * self.half_length) + .map(|vec3| self.isometry * vec3); + + // This prevents allocations (no need for alloc crate) and cloning. + let half_length = self.half_length; + let radius = self.radius; + let resolution = self.resolution; + let isometry = self.isometry; + let mk_upper_lower = move || { + [-1.0, 1.0] + .map(|sign| Vec3::Y * sign * half_length) + .map(|vec3| { + circle_coordinates_closed(radius, resolution) + .map(move |vec2| Vec3::new(vec2.x, 0.0, vec2.y) + vec3) + .map(move |vec3| isometry * vec3) + }) + }; + + let [upper_points, lower_points] = mk_upper_lower(); + upper_points.skip(1).for_each(|start| { + self.gizmos + .short_arc_3d_between(upper_center, start, upper_apex, self.color); + }); + lower_points.skip(1).for_each(|start| { + self.gizmos + .short_arc_3d_between(lower_center, start, lower_apex, self.color); + }); + + let circle_rotation = self + .isometry + .rotation + .mul_quat(Quat::from_rotation_x(core::f32::consts::FRAC_PI_2)); + self.gizmos.circle( + Isometry3d::new(upper_center, circle_rotation), + self.radius, + self.color, + ); + self.gizmos.circle( + Isometry3d::new(lower_center, circle_rotation), + self.radius, + self.color, + ); + + let [upper_points, lower_points] = mk_upper_lower(); + let connection_lines = upper_points.zip(lower_points).skip(1); + connection_lines.for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + } +} + +// cone 3d + +/// Builder for configuring the drawing options of [`Cone`]. +pub struct Cone3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Radius of the cone + radius: f32, + // Height of the cone + height: f32, + + isometry: Isometry3d, + // Color of the cone + color: Color, + + // Number of lines used to approximate the cone base geometry + base_resolution: u32, + + // Number of lines used to approximate the cone height geometry + height_resolution: u32, +} + +impl Cone3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of lines used to approximate the cone geometry for its base and height. + pub fn resolution(mut self, resolution: u32) -> Self { + self.base_resolution = resolution; + self.height_resolution = resolution; + self + } + + /// Set the number of lines used to approximate the height of the cone geometry. + /// + /// `resolution` should be a multiple of the value passed to [`Self::height_resolution`] + /// for the height to connect properly with the base. + pub fn base_resolution(mut self, resolution: u32) -> Self { + self.base_resolution = resolution; + self + } + + /// Set the number of lines used to approximate the height of the cone geometry. + /// + /// `resolution` should be a divisor of the value passed to [`Self::base_resolution`] + /// for the height to connect properly with the base. + pub fn height_resolution(mut self, resolution: u32) -> Self { + self.height_resolution = resolution; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Cone3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Cone, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Cone3dBuilder { + gizmos: self, + radius: primitive.radius, + height: primitive.height, + isometry: isometry.into(), + color: color.into(), + base_resolution: DEFAULT_RESOLUTION, + height_resolution: DEFAULT_RESOLUTION, + } + } +} + +impl Drop for Cone3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let half_height = self.height * 0.5; + let apex = self.isometry * (Vec3::Y * half_height); + let circle_center = half_height * Vec3::NEG_Y; + let circle_coords = || { + circle_coordinates_closed(self.radius, self.height_resolution) + .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + circle_center) + .map(|vec3| self.isometry * vec3) + }; + + // connections to apex + circle_coords() + .skip(1) + .map(|vec3| (vec3, apex)) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + // base circle + circle_coords() + .zip(circle_coords().skip(1)) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + } +} + +// conical frustum 3d + +/// Builder for configuring the drawing options of [`ConicalFrustum`]. +pub struct ConicalFrustum3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Radius of the top circle + radius_top: f32, + // Radius of the bottom circle + radius_bottom: f32, + // Height of the conical frustum + height: f32, + + isometry: Isometry3d, + // Color of the conical frustum + color: Color, + + // Number of lines used to approximate the curved surfaces + resolution: u32, +} + +impl ConicalFrustum3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of lines used to approximate the curved surfaces. + pub fn resolution(mut self, resolution: u32) -> Self { + self.resolution = resolution; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = ConicalFrustum3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &ConicalFrustum, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + ConicalFrustum3dBuilder { + gizmos: self, + radius_top: primitive.radius_top, + radius_bottom: primitive.radius_bottom, + height: primitive.height, + isometry: isometry.into(), + color: color.into(), + resolution: DEFAULT_RESOLUTION, + } + } +} + +impl Drop for ConicalFrustum3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let half_height = self.height * 0.5; + let mk_upper_lower = || { + [(-1.0, self.radius_bottom), (1.0, self.radius_top)].map(|(sign, radius)| { + let translation = Vec3::Y * sign * half_height; + circle_coordinates_closed(radius, self.resolution) + .map(move |vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) + .map(|vec3| self.isometry * vec3) + }) + }; + let [upper_points1, lower_points1] = mk_upper_lower(); + let [upper_points2, lower_points2] = mk_upper_lower(); + + let upper_lines = upper_points1.zip(upper_points2.skip(1)); + let lower_lines = lower_points1.zip(lower_points2.skip(1)); + upper_lines.chain(lower_lines).for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + let [upper_points, lower_points] = mk_upper_lower(); + let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); + connection_lines.for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + } +} + +// torus 3d + +/// Builder for configuring the drawing options of [`Torus`]. +pub struct Torus3dBuilder<'a, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + gizmos: &'a mut GizmoBuffer, + + // Radius of the minor circle (tube) + minor_radius: f32, + // Radius of the major circle (ring) + major_radius: f32, + + isometry: Isometry3d, + // Color of the torus + color: Color, + + // Number of lines in the minor (tube) direction + minor_resolution: u32, + // Number of lines in the major (ring) direction + major_resolution: u32, +} + +impl Torus3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Set the number of lines in the minor (tube) direction. + pub fn minor_resolution(mut self, minor_resolution: u32) -> Self { + self.minor_resolution = minor_resolution; + self + } + + /// Set the number of lines in the major (ring) direction. + pub fn major_resolution(mut self, major_resolution: u32) -> Self { + self.major_resolution = major_resolution; + self + } +} + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = Torus3dBuilder<'a, Config, Clear> + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Torus, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + Torus3dBuilder { + gizmos: self, + minor_radius: primitive.minor_radius, + major_radius: primitive.major_radius, + isometry: isometry.into(), + color: color.into(), + minor_resolution: DEFAULT_RESOLUTION, + major_resolution: DEFAULT_RESOLUTION, + } + } +} + +impl Drop for Torus3dBuilder<'_, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + // draw 4 circles with major_radius + let mk_all_sides = || { + [ + (self.major_radius - self.minor_radius, 0.0), + (self.major_radius + self.minor_radius, 0.0), + (self.major_radius, self.minor_radius), + (self.major_radius, -self.minor_radius), + ] + .map(|(radius, height)| { + let translation = height * Vec3::Y; + circle_coordinates_closed(radius, self.major_resolution) + .map(move |vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) + .map(|vec3| self.isometry * vec3) + }) + }; + let [inner1, outer1, top1, bottom1] = mk_all_sides(); + let [inner2, outer2, top2, bottom2] = mk_all_sides(); + inner1 + .zip(inner2.skip(1)) + .chain(outer1.zip(outer2.skip(1))) + .chain(top1.zip(top2.skip(1))) + .chain(bottom1.zip(bottom2.skip(1))) + .for_each(|(start, end)| { + self.gizmos.line(start, end, self.color); + }); + + let [inner, outer, top, bottom] = mk_all_sides(); + inner + .into_iter() + .zip(top) + .zip(outer) + .zip(bottom) + .flat_map(|(((inner, top), outer), bottom)| { + let center = (inner + top + outer + bottom) * 0.25; + [(inner, top), (top, outer), (outer, bottom), (bottom, inner)] + .map(|(start, end)| (start, end, center)) + }) + .for_each(|(from, to, center)| { + self.gizmos + .short_arc_3d_between(center, from, to, self.color) + .resolution(self.minor_resolution); + }); + } +} + +// tetrahedron + +impl GizmoPrimitive3d for GizmoBuffer +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + type Output<'a> + = () + where + Self: 'a; + + fn primitive_3d( + &mut self, + primitive: &Tetrahedron, + isometry: impl Into, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let isometry = isometry.into(); + + let [a, b, c, d] = primitive.vertices.map(|vec3| isometry * vec3); + + let lines = [(a, b), (a, c), (a, d), (b, c), (b, d), (c, d)]; + + let color = color.into(); + lines.into_iter().for_each(|(start, end)| { + self.line(start, end, color); + }); + } +} diff --git a/crates/bevy_shapes/src/gizmos/helpers.rs b/crates/bevy_shapes/src/gizmos/helpers.rs new file mode 100644 index 0000000000000..1a6c9e4b94eaf --- /dev/null +++ b/crates/bevy_shapes/src/gizmos/helpers.rs @@ -0,0 +1,45 @@ +use core::f32::consts::TAU; + +use bevy_math::{ops, Vec2}; + +pub(crate) const DEFAULT_CIRCLE_RESOLUTION: u32 = 32; + +/// Calculates the `nth` coordinate of a circle. +/// +/// Given a circle's radius and its resolution, this function computes the position +/// of the `nth` point along the circumference of the circle. The rotation starts at `(0.0, radius)` +/// and proceeds counter-clockwise. +pub(crate) fn single_circle_coordinate(radius: f32, resolution: u32, nth_point: u32) -> Vec2 { + let angle = nth_point as f32 * TAU / resolution as f32; + let (x, y) = ops::sin_cos(angle); + Vec2::new(x, y) * radius +} + +/// Generates an iterator over the coordinates of a circle. +/// +/// The coordinates form an open circle, meaning the first and last points aren't the same. +/// +/// This function creates an iterator that yields the positions of points approximating a +/// circle with the given radius, divided into linear segments. The iterator produces `resolution` +/// number of points. +pub(crate) fn circle_coordinates(radius: f32, resolution: u32) -> impl Iterator { + (0..) + .map(move |p| single_circle_coordinate(radius, resolution, p)) + .take(resolution as usize) +} + +/// Generates an iterator over the coordinates of a circle. +/// +/// The coordinates form a closed circle, meaning the first and last points are the same. +/// +/// This function creates an iterator that yields the positions of points approximating a +/// circle with the given radius, divided into linear segments. The iterator produces `resolution` +/// number of points. +pub(crate) fn circle_coordinates_closed( + radius: f32, + resolution: u32, +) -> impl Iterator { + circle_coordinates(radius, resolution).chain(core::iter::once(single_circle_coordinate( + radius, resolution, resolution, + ))) +} diff --git a/crates/bevy_shapes/src/gizmos/mod.rs b/crates/bevy_shapes/src/gizmos/mod.rs new file mode 100644 index 0000000000000..37afa7063d97e --- /dev/null +++ b/crates/bevy_shapes/src/gizmos/mod.rs @@ -0,0 +1,5 @@ +//! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`Gizmos`](`crate::prelude::Gizmos`). +//! +pub mod dim2; +pub mod dim3; +pub mod helpers; diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index 8aeac60c1ded3..72d03fc018a61 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -30,3 +30,6 @@ pub mod bounding; #[cfg(feature = "sampling")] pub mod sampling; + +#[cfg(feature = "gizmos")] +pub mod gizmos; diff --git a/crates/bevy_shapes/src/primitives/math.rs b/crates/bevy_shapes/src/primitives/math.rs new file mode 100644 index 0000000000000..e01c381c208a3 --- /dev/null +++ b/crates/bevy_shapes/src/primitives/math.rs @@ -0,0 +1,6 @@ +use crate::primitives::{Primitive2d, Primitive3d}; +use bevy_math::{Dir2, Dir3, Dir3A}; + +impl Primitive2d for Dir2 {} +impl Primitive3d for Dir3 {} +impl Primitive3d for Dir3A {} diff --git a/crates/bevy_shapes/src/primitives/mod.rs b/crates/bevy_shapes/src/primitives/mod.rs index c2cb17f963123..e9ead38a3d143 100644 --- a/crates/bevy_shapes/src/primitives/mod.rs +++ b/crates/bevy_shapes/src/primitives/mod.rs @@ -13,6 +13,7 @@ mod polygon; pub use half_space::*; mod view_frustum; pub use view_frustum::*; +mod math; /// A marker trait for 2D primitives pub trait Primitive2d {} From 599a1a2c9a431091d45430de3f7541dd7792768d Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 20:49:45 +0200 Subject: [PATCH 10/17] add more docs to make warnings happy --- crates/bevy_shapes/src/bounding/extrusion.rs | 2 ++ crates/bevy_shapes/src/bounding/mod.rs | 8 +++++ crates/bevy_shapes/src/gizmos/helpers.rs | 2 ++ crates/bevy_shapes/src/gizmos/mod.rs | 4 +-- crates/bevy_shapes/src/lib.rs | 38 ++++++++++++++++++++ crates/bevy_shapes/src/meshing/dim2.rs | 3 ++ crates/bevy_shapes/src/meshing/extrusion.rs | 4 +++ 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/bevy_shapes/src/bounding/extrusion.rs b/crates/bevy_shapes/src/bounding/extrusion.rs index da3f7672feb4b..3781071d267c4 100644 --- a/crates/bevy_shapes/src/bounding/extrusion.rs +++ b/crates/bevy_shapes/src/bounding/extrusion.rs @@ -1,3 +1,5 @@ +//! Module implementing [`BoundedExtrusion`] for all the primitives in bevy_shapes + use core::f32::consts::FRAC_PI_2; use crate::primitives::{ diff --git a/crates/bevy_shapes/src/bounding/mod.rs b/crates/bevy_shapes/src/bounding/mod.rs index 4837e7d6726c6..fca2b430687ff 100644 --- a/crates/bevy_shapes/src/bounding/mod.rs +++ b/crates/bevy_shapes/src/bounding/mod.rs @@ -1,3 +1,11 @@ +//! This module contains the implementations for bounding traits from the [`bevy_math`] crate for +//! working with primitive shapes +//! +//! There are four traits used: +//! - [`BoundingVolume`] is a generic abstraction for any bounding volume +//! - [`IntersectsVolume`] abstracts intersection tests against a [`BoundingVolume`] +//! - [`Bounded2d`]/[`Bounded3d`] are abstractions for shapes to generate [`BoundingVolume`]s + pub mod dim2; pub mod dim3; pub mod extrusion; diff --git a/crates/bevy_shapes/src/gizmos/helpers.rs b/crates/bevy_shapes/src/gizmos/helpers.rs index 1a6c9e4b94eaf..27554f39021dc 100644 --- a/crates/bevy_shapes/src/gizmos/helpers.rs +++ b/crates/bevy_shapes/src/gizmos/helpers.rs @@ -1,3 +1,5 @@ +//! Helper functions for primitives gizmos rendering + use core::f32::consts::TAU; use bevy_math::{ops, Vec2}; diff --git a/crates/bevy_shapes/src/gizmos/mod.rs b/crates/bevy_shapes/src/gizmos/mod.rs index 37afa7063d97e..419ef50217450 100644 --- a/crates/bevy_shapes/src/gizmos/mod.rs +++ b/crates/bevy_shapes/src/gizmos/mod.rs @@ -1,5 +1,5 @@ //! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`Gizmos`](`crate::prelude::Gizmos`). -//! + pub mod dim2; pub mod dim3; -pub mod helpers; +pub(crate) mod helpers; diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index 72d03fc018a61..45ff67c751f31 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -1,3 +1,41 @@ +//! # Bevy Shapes +//! +//! Geometric primitives and shape-related traits for Bevy. +//! +//! This crate provides the complete collection of 2D and 3D geometric primitives +//! used throughout the Bevy ecosystem, along with implementations of common traits +//! for operations such as meshing, bounding volumes, sampling, and debug rendering. +//! +//! ## Feature flags +//! +//! Additional functionality is available through optional features: +//! +//! - `meshing` — Generate meshes from supported primitives. +//! - `bounding` — Compute bounding volumes. +//! - `sampling` — Sample points on or within primitives. +//! - `gizmos` — Draw primitives using Bevy Gizmos. +//! +//! ## Examples +//! +//! Constructing a primitive: +//! +//! ``` +//! use bevy_shapes::primitives::Circle; +//! +//! let circle = Circle::new(1.0); +//! ``` +//! +//! Many traits are implemented behind feature flags: +//! +//! ```ignore +//! use bevy_shapes::primitives::Sphere; +//! use bevy_shapes::meshing::Meshable; +//! +//! let mesh = sphere.mesh(); +//! ``` +//! +//! Most users will interact with the types in the [`primitives`] module, while +//! optional modules provide additional functionality for those primitives. #![forbid(unsafe_code)] #![cfg_attr( any(docsrs, docsrs_dep), diff --git a/crates/bevy_shapes/src/meshing/dim2.rs b/crates/bevy_shapes/src/meshing/dim2.rs index ef4686a1b4aa0..dc12e86edb3d0 100644 --- a/crates/bevy_shapes/src/meshing/dim2.rs +++ b/crates/bevy_shapes/src/meshing/dim2.rs @@ -390,6 +390,7 @@ impl Meshable for CircularSegment { #[derive(Clone, Debug, Reflect)] #[reflect(Debug, Clone)] pub struct ConvexPolygonMeshBuilder { + /// The verticies of the convex polygons pub vertices: Vec, } @@ -1199,7 +1200,9 @@ pub struct RingMeshBuilder

where P: Primitive2d + Meshable, { + /// The outside shape of the [`Ring`], determined by a [`Primitive2d`] mesh builder pub outer_shape_builder: P::Output, + /// The inside shape of the [`Ring`], determined by a [`Primitive2d`] mesh builder pub inner_shape_builder: P::Output, } diff --git a/crates/bevy_shapes/src/meshing/extrusion.rs b/crates/bevy_shapes/src/meshing/extrusion.rs index 7521b18ed6856..5598de738971c 100644 --- a/crates/bevy_shapes/src/meshing/extrusion.rs +++ b/crates/bevy_shapes/src/meshing/extrusion.rs @@ -108,8 +108,12 @@ where P: Primitive2d + Meshable, P::Output: Extrudable, { + /// The base mesh builder for a [`Primitive2d`] which is going to be extruded pub base_builder: P::Output, + /// The extrusion length halved. It is applied in both directions 'up' and 'down' from the base + /// shape pub half_depth: f32, + /// The number of segments along the depth of the extrusion pub segments: usize, } From b0609512bfa18d906f83ce4d228dcea3a6a5ca95 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 20:49:45 +0200 Subject: [PATCH 11/17] implement Meshbuilder again but this time from bevy_mesh --- crates/bevy_shapes/src/meshing/dim2.rs | 12 +++++----- .../bevy_shapes/src/meshing/dim3/capsule.rs | 7 ++---- crates/bevy_shapes/src/meshing/dim3/cone.rs | 14 ++++-------- .../src/meshing/dim3/conical_frustum.rs | 7 ++---- crates/bevy_shapes/src/meshing/dim3/cuboid.rs | 7 ++---- .../bevy_shapes/src/meshing/dim3/cylinder.rs | 7 ++---- crates/bevy_shapes/src/meshing/dim3/plane.rs | 7 ++---- .../src/meshing/dim3/polyline3d.rs | 7 ++---- .../bevy_shapes/src/meshing/dim3/segment3d.rs | 9 +++----- crates/bevy_shapes/src/meshing/dim3/sphere.rs | 7 ++---- .../src/meshing/dim3/tetrahedron.rs | 4 ++-- crates/bevy_shapes/src/meshing/dim3/torus.rs | 7 ++---- .../src/meshing/dim3/triangle3d.rs | 7 ++---- crates/bevy_shapes/src/meshing/extrusion.rs | 7 ++---- crates/bevy_shapes/src/meshing/mod.rs | 22 ------------------- 15 files changed, 34 insertions(+), 97 deletions(-) diff --git a/crates/bevy_shapes/src/meshing/dim2.rs b/crates/bevy_shapes/src/meshing/dim2.rs index dc12e86edb3d0..d80ef57cbed11 100644 --- a/crates/bevy_shapes/src/meshing/dim2.rs +++ b/crates/bevy_shapes/src/meshing/dim2.rs @@ -2,17 +2,16 @@ use core::f32::consts::FRAC_PI_2; use core::mem; use bevy_asset::RenderAssetUsages; -use bevy_mesh::{Indices, Mesh, VertexAttributeValues}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, VertexAttributeValues}; use crate::{ - meshing::{extrusion::PerimeterSegment, Extrudable, MeshBuilder, Meshable}, + meshing::{extrusion::PerimeterSegment, Extrudable}, primitives::{ Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, - Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, Triangle3d, - WindingOrder, + Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, + Triangle3d, WindingOrder, }, }; -use bevy_math::prelude::Polyline2d; use bevy_math::{ops, FloatExt, Vec2, Vec3}; use bevy_reflect::prelude::*; use wgpu_types::PrimitiveTopology; @@ -1453,12 +1452,11 @@ where #[cfg(test)] mod tests { - use crate::meshing::{MeshBuilder, Meshable}; use crate::primitives::{Annulus, ConvexPolygon, RegularPolygon}; use bevy_math::{FloatOrd, Vec2}; use bevy_platform::collections::HashSet; - use bevy_mesh::{Mesh, VertexAttributeValues}; + use bevy_mesh::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; fn count_distinct_positions(points: &[[f32; 3]]) -> usize { let mut map = >::default(); diff --git a/crates/bevy_shapes/src/meshing/dim3/capsule.rs b/crates/bevy_shapes/src/meshing/dim3/capsule.rs index 083fdf4710ed6..b79837986da9b 100644 --- a/crates/bevy_shapes/src/meshing/dim3/capsule.rs +++ b/crates/bevy_shapes/src/meshing/dim3/capsule.rs @@ -1,10 +1,7 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Capsule3d, -}; +use crate::primitives::Capsule3d; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, Vec2, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; use alloc::vec::Vec; diff --git a/crates/bevy_shapes/src/meshing/dim3/cone.rs b/crates/bevy_shapes/src/meshing/dim3/cone.rs index 31672b1dbe27d..120bdcee0cf16 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cone.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cone.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Cone, -}; +use crate::primitives::Cone; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// Anchoring options for [`ConeMeshBuilder`] @@ -189,12 +186,9 @@ impl Meshable for Cone { #[cfg(test)] mod tests { - use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Cone, - }; + use crate::primitives::Cone; use bevy_math::Vec2; - use bevy_mesh::{Mesh, VertexAttributeValues}; + use bevy_mesh::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; /// Rounds floats to handle floating point error in tests. fn round_floats(points: &mut [[f32; N]]) { diff --git a/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs index 8677cbaed3de1..dd5ca2087d322 100644 --- a/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs +++ b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::ConicalFrustum, -}; +use crate::primitives::ConicalFrustum; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`ConicalFrustum`] shape. diff --git a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs index 51d9af01c5db1..37152baa9919b 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Cuboid, -}; +use crate::primitives::Cuboid; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::Vec3; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape. diff --git a/crates/bevy_shapes/src/meshing/dim3/cylinder.rs b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs index 8f48753cfa390..70d46a41f106e 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cylinder.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Cylinder, -}; +use crate::primitives::Cylinder; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::ops; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// Anchoring options for [`CylinderMeshBuilder`] diff --git a/crates/bevy_shapes/src/meshing/dim3/plane.rs b/crates/bevy_shapes/src/meshing/dim3/plane.rs index 5f5edfb39a72d..f10460299e7e7 100644 --- a/crates/bevy_shapes/src/meshing/dim3/plane.rs +++ b/crates/bevy_shapes/src/meshing/dim3/plane.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Plane3d, -}; +use crate::primitives::Plane3d; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::{Dir3, Quat, Vec2, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. diff --git a/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs index 68de53bd70587..c9ece3c955977 100644 --- a/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs @@ -1,10 +1,7 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Polyline3d, -}; +use crate::primitives::Polyline3d; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Polyline3d`] shape. diff --git a/crates/bevy_shapes/src/meshing/dim3/segment3d.rs b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs index 2a4b7060cd0d0..ece7ae4494d52 100644 --- a/crates/bevy_shapes/src/meshing/dim3/segment3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs @@ -1,10 +1,7 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Segment3d, -}; +use crate::primitives::Segment3d; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Segment3d`] shape. @@ -36,8 +33,8 @@ impl Meshable for Segment3d { #[cfg(test)] mod tests { use super::*; - use crate::meshing::{MeshBuilder, Meshable}; use bevy_math::Vec3; + use bevy_mesh::{MeshBuilder, Meshable}; #[test] fn segment3d_mesh_builder() { diff --git a/crates/bevy_shapes/src/meshing/dim3/sphere.rs b/crates/bevy_shapes/src/meshing/dim3/sphere.rs index 7b785c4f87f7e..b182c51255d9a 100644 --- a/crates/bevy_shapes/src/meshing/dim3/sphere.rs +++ b/crates/bevy_shapes/src/meshing/dim3/sphere.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Sphere, -}; +use crate::primitives::Sphere; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::ops; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; use core::f32::consts::PI; use hexasphere::shapes::IcoSphere; diff --git a/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs index 07dbd932a30bd..db0a112b420ac 100644 --- a/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs +++ b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs @@ -1,10 +1,10 @@ use crate::{ - meshing::{triangle3d, MeshBuilder, Meshable}, + meshing::triangle3d, primitives::{Tetrahedron, Triangle3d}, }; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Tetrahedron`] shape. diff --git a/crates/bevy_shapes/src/meshing/dim3/torus.rs b/crates/bevy_shapes/src/meshing/dim3/torus.rs index e464375f5ceda..a2a02cd5fc0c2 100644 --- a/crates/bevy_shapes/src/meshing/dim3/torus.rs +++ b/crates/bevy_shapes/src/meshing/dim3/torus.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Torus, -}; +use crate::primitives::Torus; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; use core::ops::RangeInclusive; diff --git a/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs index b183883d07b26..801475133d724 100644 --- a/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::Triangle3d, -}; +use crate::primitives::Triangle3d; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; use bevy_math::Vec3; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Triangle3d`] shape. diff --git a/crates/bevy_shapes/src/meshing/extrusion.rs b/crates/bevy_shapes/src/meshing/extrusion.rs index 5598de738971c..ba30845187de4 100644 --- a/crates/bevy_shapes/src/meshing/extrusion.rs +++ b/crates/bevy_shapes/src/meshing/extrusion.rs @@ -1,11 +1,8 @@ -use crate::{ - meshing::{MeshBuilder, Meshable}, - primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}, -}; +use crate::primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}; use alloc::vec::Vec; use bevy_math::{Vec2, Vec3}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; +use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology, VertexAttributeValues}; /// A type representing a segment of the perimeter of an extrudable mesh. pub enum PerimeterSegment { diff --git a/crates/bevy_shapes/src/meshing/mod.rs b/crates/bevy_shapes/src/meshing/mod.rs index 530cf7da887dd..a7a1fbbf0623b 100644 --- a/crates/bevy_shapes/src/meshing/mod.rs +++ b/crates/bevy_shapes/src/meshing/mod.rs @@ -29,25 +29,3 @@ pub use dim3::*; mod extrusion; #[cfg(feature = "extrusion")] pub use extrusion::*; - -use bevy_mesh::Mesh; - -/// A trait for shapes that can be turned into a [`Mesh`]. -pub trait Meshable { - /// The output of [`Self::mesh`]. This will be a [`MeshBuilder`] used for creating a [`Mesh`]. - type Output: MeshBuilder; - - /// Creates a [`MeshBuilder`] for a shape. - fn mesh_builder(&self) -> Self::Output; - - /// Creates a [`Mesh`] for a shape. - fn mesh(&self) -> Mesh { - self.mesh_builder().build() - } -} - -/// A trait used to build [`Mesh`]es from a configuration -pub trait MeshBuilder { - /// Builds a [`Mesh`] based on the configuration in `self`. - fn build(&self) -> Mesh; -} From 7aec337999cf5c1f938ee16ef16ef107e22b9bfa Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 20:49:45 +0200 Subject: [PATCH 12/17] adjust Meshable trait --- .../src/transform_gizmo_render.rs | 16 +++-- crates/bevy_mesh/src/primitives/dim2.rs | 66 +++++++++---------- .../bevy_mesh/src/primitives/dim3/capsule.rs | 4 +- crates/bevy_mesh/src/primitives/dim3/cone.rs | 6 +- .../src/primitives/dim3/conical_frustum.rs | 4 +- .../bevy_mesh/src/primitives/dim3/cuboid.rs | 4 +- .../bevy_mesh/src/primitives/dim3/cylinder.rs | 4 +- crates/bevy_mesh/src/primitives/dim3/plane.rs | 4 +- .../src/primitives/dim3/polyline3d.rs | 4 +- .../src/primitives/dim3/segment3d.rs | 6 +- .../bevy_mesh/src/primitives/dim3/sphere.rs | 4 +- .../src/primitives/dim3/tetrahedron.rs | 4 +- crates/bevy_mesh/src/primitives/dim3/torus.rs | 4 +- .../src/primitives/dim3/triangle3d.rs | 4 +- crates/bevy_mesh/src/primitives/extrusion.rs | 8 +-- crates/bevy_mesh/src/primitives/mod.rs | 7 +- crates/bevy_pbr/src/decal/forward.rs | 2 +- crates/bevy_pbr/src/volumetric_fog/mod.rs | 4 +- .../bevy_sprite_render/src/sprite_mesh/mod.rs | 2 +- examples/3d/3d_shapes.rs | 13 +++- examples/3d/3d_viewport_to_world.rs | 2 +- examples/3d/anti_aliasing.rs | 2 +- examples/3d/auto_exposure.rs | 2 +- examples/3d/blend_modes.rs | 4 +- examples/3d/bloom_3d.rs | 2 +- examples/3d/camera_sub_view.rs | 2 +- examples/3d/clearcoat.rs | 2 +- examples/3d/clustered_decal_maps.rs | 2 +- examples/3d/contact_shadows.rs | 2 +- examples/3d/deferred_rendering.rs | 4 +- examples/3d/irradiance_volumes.rs | 2 +- examples/3d/light_probe_blending.rs | 4 +- examples/3d/lighting.rs | 8 +-- examples/3d/meshlet.rs | 2 +- examples/3d/mirror.rs | 2 +- examples/3d/occlusion_culling.rs | 2 +- examples/3d/order_independent_transparency.rs | 8 +-- examples/3d/orthographic.rs | 2 +- examples/3d/parallax_mapping.rs | 4 +- examples/3d/pccm.rs | 2 +- examples/3d/rect_light.rs | 2 +- examples/3d/reflection_probes.rs | 2 +- examples/3d/rotate_environment_map.rs | 2 +- examples/3d/shadow_biases.rs | 8 ++- examples/3d/shadow_caster_receiver.rs | 4 +- examples/3d/solari.rs | 6 +- examples/3d/specular_tint.rs | 2 +- examples/3d/spherical_area_lights.rs | 4 +- examples/3d/split_screen.rs | 2 +- examples/3d/spotlight.rs | 6 +- examples/3d/ssao.rs | 2 +- examples/3d/transmission.rs | 6 +- examples/3d/transparency_3d.rs | 8 +-- examples/3d/two_passes.rs | 2 +- examples/3d/vertex_colors.rs | 2 +- examples/3d/visibility_range.rs | 2 +- examples/3d/wireframe.rs | 2 +- examples/animation/animated_mesh.rs | 2 +- examples/animation/animated_mesh_control.rs | 2 +- examples/animation/animated_mesh_events.rs | 2 +- examples/animation/eased_motion.rs | 2 +- examples/asset/compressed_image_saver.rs | 4 +- examples/asset/multi_asset_sync.rs | 4 +- examples/audio/spatial_audio_3d.rs | 2 +- examples/camera/camera_orbit.rs | 2 +- examples/camera/projection_zoom.rs | 2 +- examples/ecs/error_handling.rs | 6 +- examples/ecs/iter_combinations.rs | 4 +- examples/gizmos/3d_gizmos.rs | 2 +- examples/gizmos/axes.rs | 2 +- examples/gizmos/transform_gizmo.rs | 2 +- .../gltf/gltf_extension_animation_graph.rs | 2 +- .../examples/test_compression.rs | 2 +- .../mipmap_generator/examples/test_image.rs | 2 +- examples/math/custom_primitives.rs | 10 +-- examples/math/random_sampling.rs | 4 +- examples/math/render_primitives.rs | 48 +++++++------- examples/mobile/src/lib.rs | 4 +- examples/movement/smooth_follow.rs | 2 +- examples/picking/custom_hit_data.rs | 7 +- examples/picking/mesh_picking.rs | 13 +++- .../shader_material_screenspace_texture.rs | 2 +- examples/shader/shader_prepass.rs | 2 +- examples/stress_tests/many_foxes.rs | 2 +- examples/stress_tests/many_lights.rs | 2 +- examples/testbed/3d.rs | 4 +- examples/transforms/align.rs | 2 +- examples/transforms/transform.rs | 2 +- examples/usage/debug_frustum_culling.rs | 6 +- examples/window/screenshot.rs | 2 +- tests/3d/test_invalid_skinned_mesh.rs | 18 ++++- tests/window/minimizing.rs | 2 +- tests/window/resizing.rs | 2 +- 93 files changed, 258 insertions(+), 212 deletions(-) diff --git a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs index 51d474bce1c85..0e3974800998f 100644 --- a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs +++ b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs @@ -167,11 +167,15 @@ fn spawn_gizmo_meshes( }; // Pre-create meshes - let shaft_mesh = meshes.add(Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH).mesh().build()); - let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh().build()); + let shaft_mesh = meshes.add( + Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH) + .mesh_builder() + .build(), + ); + let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh_builder().build()); let scale_cube_mesh = meshes.add( Cuboid::new(SCALE_CUBE_SIZE, SCALE_CUBE_SIZE, SCALE_CUBE_SIZE) - .mesh() + .mesh_builder() .build(), ); let rotate_torus_mesh = meshes.add( @@ -179,7 +183,7 @@ fn spawn_gizmo_meshes( minor_radius: 0.015, major_radius: ROTATE_RING_RADIUS, } - .mesh() + .mesh_builder() .build(), ); let view_circle_mesh = meshes.add( @@ -187,7 +191,7 @@ fn spawn_gizmo_meshes( minor_radius: VIEW_CIRCLE_MINOR, major_radius: VIEW_CIRCLE_MAJOR, } - .mesh() + .mesh_builder() .build(), ); let view_ring_mesh = meshes.add( @@ -195,7 +199,7 @@ fn spawn_gizmo_meshes( minor_radius: VIEW_RING_MINOR, major_radius: VIEW_RING_MAJOR, } - .mesh() + .mesh_builder() .build(), ); diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 7ad0fc3826a5d..33cf4fb531ccd 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -61,7 +61,7 @@ impl CircleMeshBuilder { impl MeshBuilder for CircleMeshBuilder { fn build(&self) -> Mesh { Ellipse::new(self.circle.radius, self.circle.radius) - .mesh() + .mesh_builder() .resolution(self.resolution) .build() } @@ -80,7 +80,7 @@ impl Extrudable for CircleMeshBuilder { impl Meshable for Circle { type Output = CircleMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { CircleMeshBuilder { circle: *self, ..Default::default() @@ -90,7 +90,7 @@ impl Meshable for Circle { impl From for Mesh { fn from(circle: Circle) -> Self { - circle.mesh().build() + circle.mesh_builder().build() } } @@ -242,7 +242,7 @@ impl Extrudable for CircularSectorMeshBuilder { impl Meshable for CircularSector { type Output = CircularSectorMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { CircularSectorMeshBuilder { sector: *self, ..Default::default() @@ -255,7 +255,7 @@ impl From for Mesh { /// /// See the documentation of [`CircularSectorMeshBuilder`] for more details. fn from(sector: CircularSector) -> Self { - sector.mesh().build() + sector.mesh_builder().build() } } @@ -389,7 +389,7 @@ impl Extrudable for CircularSegmentMeshBuilder { impl Meshable for CircularSegment { type Output = CircularSegmentMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { CircularSegmentMeshBuilder { segment: *self, ..Default::default() @@ -402,7 +402,7 @@ impl From for Mesh { /// /// See the documentation of [`CircularSegmentMeshBuilder`] for more details. fn from(segment: CircularSegment) -> Self { - segment.mesh().build() + segment.mesh_builder().build() } } @@ -419,7 +419,7 @@ pub struct ConvexPolygonMeshBuilder { impl Meshable for ConvexPolygon { type Output = ConvexPolygonMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Self::Output { vertices: self.vertices().to_vec(), } @@ -474,7 +474,7 @@ impl Extrudable for ConvexPolygonMeshBuilder { impl From for Mesh { fn from(polygon: ConvexPolygon) -> Self { - polygon.mesh().build() + polygon.mesh_builder().build() } } @@ -520,7 +520,7 @@ impl RegularPolygonMeshBuilder { impl Meshable for RegularPolygon { type Output = RegularPolygonMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Self::Output { circumradius: self.circumcircle.radius, sides: self.sides, @@ -532,7 +532,7 @@ impl MeshBuilder for RegularPolygonMeshBuilder { fn build(&self) -> Mesh { // The ellipse mesh is just a regular polygon with two radii Ellipse::new(self.circumradius, self.circumradius) - .mesh() + .mesh_builder() .resolution(self.sides) .build() } @@ -548,7 +548,7 @@ impl Extrudable for RegularPolygonMeshBuilder { impl From for Mesh { fn from(polygon: RegularPolygon) -> Self { - polygon.mesh().build() + polygon.mesh_builder().build() } } @@ -643,7 +643,7 @@ impl Extrudable for EllipseMeshBuilder { impl Meshable for Ellipse { type Output = EllipseMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { EllipseMeshBuilder { ellipse: *self, ..Default::default() @@ -653,7 +653,7 @@ impl Meshable for Ellipse { impl From for Mesh { fn from(ellipse: Ellipse) -> Self { - ellipse.mesh().build() + ellipse.mesh_builder().build() } } @@ -685,7 +685,7 @@ impl MeshBuilder for Segment2dMeshBuilder { impl Meshable for Segment2d { type Output = Segment2dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Segment2dMeshBuilder::new(*self) } } @@ -693,7 +693,7 @@ impl Meshable for Segment2d { impl From for Mesh { /// Converts this segment into a [`Mesh`] using a default [`Segment2dMeshBuilder`]. fn from(segment: Segment2d) -> Self { - segment.mesh().build() + segment.mesh_builder().build() } } @@ -728,7 +728,7 @@ impl MeshBuilder for Polyline2dMeshBuilder { impl Meshable for Polyline2d { type Output = Polyline2dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Polyline2dMeshBuilder { polyline: self.clone(), } @@ -737,7 +737,7 @@ impl Meshable for Polyline2d { impl From for Mesh { fn from(polyline: Polyline2d) -> Self { - polyline.mesh().build() + polyline.mesh_builder().build() } } @@ -860,7 +860,7 @@ impl Extrudable for AnnulusMeshBuilder { impl Meshable for Annulus { type Output = AnnulusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { AnnulusMeshBuilder { annulus: *self, ..Default::default() @@ -870,7 +870,7 @@ impl Meshable for Annulus { impl From for Mesh { fn from(annulus: Annulus) -> Self { - annulus.mesh().build() + annulus.mesh_builder().build() } } @@ -947,7 +947,7 @@ impl Extrudable for RhombusMeshBuilder { impl Meshable for Rhombus { type Output = RhombusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Self::Output { half_diagonals: self.half_diagonals, } @@ -956,7 +956,7 @@ impl Meshable for Rhombus { impl From for Mesh { fn from(rhombus: Rhombus) -> Self { - rhombus.mesh().build() + rhombus.mesh_builder().build() } } @@ -979,7 +979,7 @@ impl Triangle2dMeshBuilder { impl Meshable for Triangle2d { type Output = Triangle2dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Self::Output { triangle: *self } } } @@ -1033,7 +1033,7 @@ impl Extrudable for Triangle2dMeshBuilder { impl From for Mesh { fn from(triangle: Triangle2d) -> Self { - triangle.mesh().build() + triangle.mesh_builder().build() } } @@ -1104,7 +1104,7 @@ impl Extrudable for RectangleMeshBuilder { impl Meshable for Rectangle { type Output = RectangleMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { RectangleMeshBuilder { half_size: self.half_size, } @@ -1113,7 +1113,7 @@ impl Meshable for Rectangle { impl From for Mesh { fn from(rectangle: Rectangle) -> Self { - rectangle.mesh().build() + rectangle.mesh_builder().build() } } @@ -1264,7 +1264,7 @@ impl Extrudable for Capsule2dMeshBuilder { impl Meshable for Capsule2d { type Output = Capsule2dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Capsule2dMeshBuilder { capsule: *self, ..Default::default() @@ -1274,7 +1274,7 @@ impl Meshable for Capsule2d { impl From for Mesh { fn from(capsule: Capsule2d) -> Self { - capsule.mesh().build() + capsule.mesh_builder().build() } } @@ -1294,8 +1294,8 @@ where /// Create a new `RingMeshBuilder

` from a given `Ring

` shape. pub fn new(ring: &Ring

) -> Self { Self { - outer_shape_builder: ring.outer_shape.mesh(), - inner_shape_builder: ring.inner_shape.mesh(), + outer_shape_builder: ring.outer_shape.mesh_builder(), + inner_shape_builder: ring.inner_shape.mesh_builder(), } } @@ -1527,7 +1527,7 @@ where { type Output = RingMeshBuilder

; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { RingMeshBuilder::new(self) } } @@ -1537,7 +1537,7 @@ where P: Primitive2d + Meshable, { fn from(ring: Ring

) -> Self { - ring.mesh().build() + ring.mesh_builder().build() } } @@ -1562,7 +1562,7 @@ mod tests { #[test] fn test_annulus() { - let mesh = Annulus::new(1.0, 1.2).mesh().resolution(16).build(); + let mesh = Annulus::new(1.0, 1.2).mesh_builder().resolution(16).build(); assert_eq!( 32, diff --git a/crates/bevy_mesh/src/primitives/dim3/capsule.rs b/crates/bevy_mesh/src/primitives/dim3/capsule.rs index f46ebce0d15a7..3b4161ad69615 100644 --- a/crates/bevy_mesh/src/primitives/dim3/capsule.rs +++ b/crates/bevy_mesh/src/primitives/dim3/capsule.rs @@ -421,7 +421,7 @@ impl MeshBuilder for Capsule3dMeshBuilder { impl Meshable for Capsule3d { type Output = Capsule3dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Capsule3dMeshBuilder { capsule: *self, ..Default::default() @@ -431,6 +431,6 @@ impl Meshable for Capsule3d { impl From for Mesh { fn from(capsule: Capsule3d) -> Self { - capsule.mesh().build() + capsule.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/cone.rs b/crates/bevy_mesh/src/primitives/dim3/cone.rs index d06a57f832ea5..9961cc2fdc25e 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cone.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cone.rs @@ -174,7 +174,7 @@ impl MeshBuilder for ConeMeshBuilder { impl Meshable for Cone { type Output = ConeMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { ConeMeshBuilder { cone: *self, ..Default::default() @@ -184,7 +184,7 @@ impl Meshable for Cone { impl From for Mesh { fn from(cone: Cone) -> Self { - cone.mesh().build() + cone.mesh_builder().build() } } @@ -211,7 +211,7 @@ mod tests { radius: 0.5, height: 1.0, } - .mesh() + .mesh_builder() .resolution(4) .build(); diff --git a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs index 8c69378c01e64..3c738b9ad6c0b 100644 --- a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs +++ b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs @@ -169,7 +169,7 @@ impl MeshBuilder for ConicalFrustumMeshBuilder { impl Meshable for ConicalFrustum { type Output = ConicalFrustumMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { ConicalFrustumMeshBuilder { frustum: *self, ..Default::default() @@ -179,6 +179,6 @@ impl Meshable for ConicalFrustum { impl From for Mesh { fn from(frustum: ConicalFrustum) -> Self { - frustum.mesh().build() + frustum.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs index 40a7cd45d4433..262eb1b792d04 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs @@ -85,7 +85,7 @@ impl MeshBuilder for CuboidMeshBuilder { impl Meshable for Cuboid { type Output = CuboidMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { CuboidMeshBuilder { half_size: self.half_size, } @@ -94,6 +94,6 @@ impl Meshable for Cuboid { impl From for Mesh { fn from(cuboid: Cuboid) -> Self { - cuboid.mesh().build() + cuboid.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs index 7b1b45974ea62..13a5181bca772 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs @@ -207,7 +207,7 @@ impl MeshBuilder for CylinderMeshBuilder { impl Meshable for Cylinder { type Output = CylinderMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { CylinderMeshBuilder { cylinder: *self, ..Default::default() @@ -217,6 +217,6 @@ impl Meshable for Cylinder { impl From for Mesh { fn from(cylinder: Cylinder) -> Self { - cylinder.mesh().build() + cylinder.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/plane.rs b/crates/bevy_mesh/src/primitives/dim3/plane.rs index e937905584f9a..98c499b901b6c 100644 --- a/crates/bevy_mesh/src/primitives/dim3/plane.rs +++ b/crates/bevy_mesh/src/primitives/dim3/plane.rs @@ -190,7 +190,7 @@ impl MeshBuilder for PlaneMeshBuilder { impl Meshable for Plane3d { type Output = PlaneMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { PlaneMeshBuilder { plane: *self, subdivisions_x: 0, @@ -201,6 +201,6 @@ impl Meshable for Plane3d { impl From for Mesh { fn from(plane: Plane3d) -> Self { - plane.mesh().build() + plane.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs index 4d13112579f09..744763c8a486e 100644 --- a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs @@ -29,7 +29,7 @@ impl MeshBuilder for Polyline3dMeshBuilder { impl Meshable for Polyline3d { type Output = Polyline3dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Polyline3dMeshBuilder { polyline: self.clone(), } @@ -38,6 +38,6 @@ impl Meshable for Polyline3d { impl From for Mesh { fn from(polyline: Polyline3d) -> Self { - polyline.mesh().build() + polyline.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs index d032285283afb..7b78aa42335ec 100644 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs @@ -24,14 +24,14 @@ impl MeshBuilder for Segment3dMeshBuilder { impl Meshable for Segment3d { type Output = Segment3dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Segment3dMeshBuilder { segment: *self } } } impl From for Mesh { fn from(segment: Segment3d) -> Self { - segment.mesh().build() + segment.mesh_builder().build() } } @@ -44,7 +44,7 @@ mod tests { #[test] fn segment3d_mesh_builder() { let segment = Segment3d::new(Vec3::ZERO, Vec3::X); - let mesh = segment.mesh().build(); + let mesh = segment.mesh_builder().build(); assert_eq!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().len(), 2); assert_eq!(mesh.indices().unwrap().len(), 2); } diff --git a/crates/bevy_mesh/src/primitives/dim3/sphere.rs b/crates/bevy_mesh/src/primitives/dim3/sphere.rs index 5938526457992..06b6971d96fd1 100644 --- a/crates/bevy_mesh/src/primitives/dim3/sphere.rs +++ b/crates/bevy_mesh/src/primitives/dim3/sphere.rs @@ -253,7 +253,7 @@ impl MeshBuilder for SphereMeshBuilder { impl Meshable for Sphere { type Output = SphereMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { SphereMeshBuilder { sphere: *self, ..Default::default() @@ -263,6 +263,6 @@ impl Meshable for Sphere { impl From for Mesh { fn from(sphere: Sphere) -> Self { - sphere.mesh().build() + sphere.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs index 529805d9a603f..34f1752b7e2dd 100644 --- a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs +++ b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs @@ -54,13 +54,13 @@ impl MeshBuilder for TetrahedronMeshBuilder { impl Meshable for Tetrahedron { type Output = TetrahedronMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { TetrahedronMeshBuilder { tetrahedron: *self } } } impl From for Mesh { fn from(tetrahedron: Tetrahedron) -> Self { - tetrahedron.mesh().build() + tetrahedron.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/torus.rs b/crates/bevy_mesh/src/primitives/dim3/torus.rs index 6f370c13418ca..e4df75b30246f 100644 --- a/crates/bevy_mesh/src/primitives/dim3/torus.rs +++ b/crates/bevy_mesh/src/primitives/dim3/torus.rs @@ -161,7 +161,7 @@ impl MeshBuilder for TorusMeshBuilder { impl Meshable for Torus { type Output = TorusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { TorusMeshBuilder { torus: *self, ..Default::default() @@ -171,6 +171,6 @@ impl Meshable for Torus { impl From for Mesh { fn from(torus: Torus) -> Self { - torus.mesh().build() + torus.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs index e35f272ab9bb1..aef78fa36bf57 100644 --- a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs @@ -35,7 +35,7 @@ impl MeshBuilder for Triangle3dMeshBuilder { impl Meshable for Triangle3d { type Output = Triangle3dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Triangle3dMeshBuilder { triangle: *self } } } @@ -96,7 +96,7 @@ pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] { impl From for Mesh { fn from(triangle: Triangle3d) -> Self { - triangle.mesh().build() + triangle.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/extrusion.rs b/crates/bevy_mesh/src/primitives/extrusion.rs index 9df4821eb392b..519849ae2786b 100644 --- a/crates/bevy_mesh/src/primitives/extrusion.rs +++ b/crates/bevy_mesh/src/primitives/extrusion.rs @@ -92,9 +92,9 @@ where { type Output = ExtrusionBuilder

; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { ExtrusionBuilder { - base_builder: self.base_shape.mesh(), + base_builder: self.base_shape.mesh_builder(), half_depth: self.half_depth, segments: 1, } @@ -120,7 +120,7 @@ where /// Create a new `ExtrusionBuilder

` from a given `base_shape` and the full `depth` of the extrusion. pub fn new(base_shape: &P, depth: f32) -> Self { Self { - base_builder: base_shape.mesh(), + base_builder: base_shape.mesh_builder(), half_depth: depth / 2., segments: 1, } @@ -436,6 +436,6 @@ where P::Output: Extrudable, { fn from(value: Extrusion

) -> Self { - value.mesh().build() + value.mesh_builder().build() } } diff --git a/crates/bevy_mesh/src/primitives/mod.rs b/crates/bevy_mesh/src/primitives/mod.rs index 02aa0f1caf41e..d149070f7d04d 100644 --- a/crates/bevy_mesh/src/primitives/mod.rs +++ b/crates/bevy_mesh/src/primitives/mod.rs @@ -35,8 +35,13 @@ pub trait Meshable { /// The output of [`Self::mesh`]. This will be a [`MeshBuilder`] used for creating a [`Mesh`]. type Output: MeshBuilder; + /// Creates a [`MeshBuilder`] for a shape. + fn mesh_builder(&self) -> Self::Output; + /// Creates a [`Mesh`] for a shape. - fn mesh(&self) -> Self::Output; + fn mesh(&self) -> Mesh { + self.mesh_builder().build() + } } /// A trait used to build [`Mesh`]es from a configuration diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index f00c667d6d2ca..6d3a23d637894 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -30,7 +30,7 @@ impl Plugin for ForwardDecalPlugin { let mesh = app.world_mut().resource_mut::>().add( Rectangle::from_size(Vec2::ONE) - .mesh() + .mesh_builder() .build() .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) .with_generated_tangents() diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index fc1f084c278ab..c6b5fb510f4c7 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -67,8 +67,8 @@ impl Plugin for VolumetricFogPlugin { embedded_asset!(app, "volumetric_fog.wgsl"); let mut meshes = app.world_mut().resource_mut::>(); - let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh()); - let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()); + let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh_builder()); + let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh_builder()); app.add_plugins(SyncComponentPlugin::::default()); diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index a8238a5a09157..f012e429a53ff 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -51,7 +51,7 @@ fn add_mesh( let quad = quad.get_or_insert_with(|| { meshes.add( Rectangle::from_size(vec2(1.0, 1.0)) - .mesh() + .mesh_builder() .build() .with_removed_attribute(Mesh::ATTRIBUTE_NORMAL) .compressed_mesh( diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 5edb6c777f3cc..6fb89a532e8b7 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -73,8 +73,8 @@ fn setup( meshes.add(Cylinder::default()), meshes.add(Cone::default()), meshes.add(ConicalFrustum::default()), - meshes.add(Sphere::default().mesh().ico(5).unwrap()), - meshes.add(Sphere::default().mesh().uv(32, 18)), + meshes.add(Sphere::default().mesh_builder().ico(5).unwrap()), + meshes.add(Sphere::default().mesh_builder().uv(32, 18)), meshes.add(Segment3d::default()), meshes.add(Polyline3d::new(vec![ Vec3::new(-0.5, 0.0, 0.0), @@ -195,7 +195,14 @@ fn setup( // ground plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), + Mesh3d( + meshes.add( + Plane3d::default() + .mesh_builder() + .size(50.0, 50.0) + .subdivisions(10), + ), + ), MeshMaterial3d(materials.add(Color::from(SILVER))), )); diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 231306bd2b6dc..14e7c28a8a1a3 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -46,7 +46,7 @@ fn setup( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20., 20.))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20., 20.))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Ground, )); diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 703920b1e5050..36a13bf935298 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -418,7 +418,7 @@ fn setup( ) { // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20.0, 20.0))), MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))), )); diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 0e640fa0c7e77..7755b918fec2b 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -70,7 +70,7 @@ fn setup( normal: -Dir3::Z, half_size: Vec2::new(2.0, 0.5), } - .mesh(), + .mesh_builder(), )); // Build a dimly lit box around the camera, with a slot to see the bright skybox. diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index 41a91d112115c..32edc41c83b56 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -31,7 +31,7 @@ fn setup( asset_server: Res, ) { let base_color = Color::srgb(0.9, 0.2, 0.3); - let icosphere_mesh = meshes.add(Sphere::new(0.9).mesh().ico(7).unwrap()); + let icosphere_mesh = meshes.add(Sphere::new(0.9).mesh_builder().ico(7).unwrap()); // Opaque let opaque = commands @@ -122,7 +122,7 @@ fn setup( let black_material = materials.add(Color::BLACK); let white_material = materials.add(Color::WHITE); - let plane_mesh = meshes.add(Plane3d::default().mesh().size(2.0, 2.0)); + let plane_mesh = meshes.add(Plane3d::default().mesh_builder().size(2.0, 2.0)); for x in -3..4 { for z in -3..4 { diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index 982b59745fd45..c0993a067b89c 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -52,7 +52,7 @@ fn setup_scene( ..default() }); - let mesh = meshes.add(Sphere::new(0.4).mesh().ico(5).unwrap()); + let mesh = meshes.add(Sphere::new(0.4).mesh_builder().ico(5).unwrap()); for x in -5..5 { for z in -5..5 { diff --git a/examples/3d/camera_sub_view.rs b/examples/3d/camera_sub_view.rs index b6f9a8f39c342..dfc3fe6792d81 100644 --- a/examples/3d/camera_sub_view.rs +++ b/examples/3d/camera_sub_view.rs @@ -32,7 +32,7 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/3d/clearcoat.rs b/examples/3d/clearcoat.rs index 9368b9cc50ea4..be28058538186 100644 --- a/examples/3d/clearcoat.rs +++ b/examples/3d/clearcoat.rs @@ -83,7 +83,7 @@ fn create_sphere_mesh(meshes: &mut Assets) -> Handle { // We're going to use normal maps, so make sure we've generated tangents, or // else the normal maps won't show up. - let mut sphere_mesh = Sphere::new(1.0).mesh().build(); + let mut sphere_mesh = Sphere::new(1.0).mesh_builder().build(); sphere_mesh .generate_tangents() .expect("Failed to generate tangents"); diff --git a/examples/3d/clustered_decal_maps.rs b/examples/3d/clustered_decal_maps.rs index 85c0daf9dcd56..87f3f3943e266 100644 --- a/examples/3d/clustered_decal_maps.rs +++ b/examples/3d/clustered_decal_maps.rs @@ -196,7 +196,7 @@ fn spawn_plane_mesh( normal: Dir3::NEG_Z, half_size: Vec2::splat(PLANE_HALF_SIZE), } - .mesh() + .mesh_builder() .build() .with_duplicated_vertices() .with_computed_flat_normals() diff --git a/examples/3d/contact_shadows.rs b/examples/3d/contact_shadows.rs index bb3dc82903b17..d2b79ab921e1e 100644 --- a/examples/3d/contact_shadows.rs +++ b/examples/3d/contact_shadows.rs @@ -229,7 +229,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ); commands.spawn(( - Mesh3d(asset_server.add(Circle::default().mesh().into())), + Mesh3d(asset_server.add(Circle::default().mesh_builder().into())), MeshMaterial3d(asset_server.add(StandardMaterial { base_color: Color::srgb(0.06, 0.06, 0.06), ..default() diff --git a/examples/3d/deferred_rendering.rs b/examples/3d/deferred_rendering.rs index 87fea95aa39d0..8e9adfd726e8d 100644 --- a/examples/3d/deferred_rendering.rs +++ b/examples/3d/deferred_rendering.rs @@ -88,12 +88,12 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(50.0, 50.0))), MeshMaterial3d(forward_mat_h.clone()), )); let cube_h = meshes.add(Cuboid::new(0.1, 0.1, 0.1)); - let sphere_h = meshes.add(Sphere::new(0.125).mesh().uv(32, 18)); + let sphere_h = meshes.add(Sphere::new(0.125).mesh_builder().uv(32, 18)); // Cubes commands.spawn(( diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index ac1c837ddc237..5f3bd9d3bba67 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -492,7 +492,7 @@ impl FromWorld for ExampleAssets { AnimationGraph::from_clip(fox_animation.clone()); ExampleAssets { - main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)), + main_sphere: world.add_asset(Sphere::default().mesh_builder().uv(32, 18)), fox: world.load_asset(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), main_sphere_material: world.add_asset(Color::from(SILVER)), main_scene: world.load_asset( diff --git a/examples/3d/light_probe_blending.rs b/examples/3d/light_probe_blending.rs index 535e11accf10b..e6b79b251a3f3 100644 --- a/examples/3d/light_probe_blending.rs +++ b/examples/3d/light_probe_blending.rs @@ -271,7 +271,7 @@ fn spawn_reflective_sphere( material: Handle, ) { // Create a mesh. - let sphere = meshes.add(Sphere::default().mesh().uv(32, 18)); + let sphere = meshes.add(Sphere::default().mesh_builder().uv(32, 18)); // Spawn the sphere. commands.spawn(( @@ -296,7 +296,7 @@ fn spawn_reflective_prism( Cuboid { half_size: vec3(2.0, 1.0, 10.0), } - .mesh() + .mesh_builder() .build() // We use flat normals so that the surface appears flat, not curved. .with_duplicated_vertices() diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index 2fa77fb037cba..adf389a992562 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -48,7 +48,7 @@ fn setup( ) { // ground plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(10.0, 10.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::WHITE, perceptual_roughness: 1.0, @@ -109,7 +109,7 @@ fn setup( )); // sphere commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.5).mesh().uv(32, 18))), + Mesh3d(meshes.add(Sphere::new(0.5).mesh_builder().uv(32, 18))), MeshMaterial3d(materials.add(StandardMaterial { base_color: LIMEGREEN.into(), ..default() @@ -136,7 +136,7 @@ fn setup( }, Transform::from_xyz(1.0, 2.0, 0.0), children![( - Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), + Mesh3d(meshes.add(Sphere::new(0.1).mesh_builder().uv(32, 18))), MeshMaterial3d(materials.add(StandardMaterial { base_color: RED.into(), emissive: LinearRgba::new(4.0, 0.0, 0.0, 0.0), @@ -177,7 +177,7 @@ fn setup( }, Transform::from_xyz(0.0, 4.0, 0.0), children![( - Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), + Mesh3d(meshes.add(Sphere::new(0.1).mesh_builder().uv(32, 18))), MeshMaterial3d(materials.add(StandardMaterial { base_color: BLUE.into(), emissive: LinearRgba::new(0.0, 0.0, 713.0, 0.0), diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index 892d0cd515c0b..866f822c1bf5c 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -107,7 +107,7 @@ fn setup( } commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(standard_materials.add(StandardMaterial { base_color: Color::WHITE, perceptual_roughness: 1.0, diff --git a/examples/3d/mirror.rs b/examples/3d/mirror.rs index 584aa12a8a634..07882b95ff7e1 100644 --- a/examples/3d/mirror.rs +++ b/examples/3d/mirror.rs @@ -298,7 +298,7 @@ fn spawn_mirror( .with_translation(MIRROR_POSITION); commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(1.0, 1.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(1.0, 1.0))), MeshMaterial3d(screen_space_texture_materials.add(ExtendedMaterial { base: StandardMaterial { base_color: Color::BLACK, diff --git a/examples/3d/occlusion_culling.rs b/examples/3d/occlusion_culling.rs index c9b9fd9cfa2d9..8e70a83d4663e 100644 --- a/examples/3d/occlusion_culling.rs +++ b/examples/3d/occlusion_culling.rs @@ -285,7 +285,7 @@ fn spawn_small_cubes( // Create the sphere mesh, and extract the positions of its vertices. let sphere = Sphere::new(OUTER_RADIUS) - .mesh() + .mesh_builder() .ico(OUTER_SUBDIVISION_COUNT) .unwrap(); let sphere_positions = sphere.attribute(Mesh::ATTRIBUTE_POSITION).unwrap(); diff --git a/examples/3d/order_independent_transparency.rs b/examples/3d/order_independent_transparency.rs index c5ffb518414c7..6222e6ce05425 100644 --- a/examples/3d/order_independent_transparency.rs +++ b/examples/3d/order_independent_transparency.rs @@ -387,7 +387,7 @@ fn spawn_spheres(commands: &mut Commands, resources: &mut SceneResources) { let offset = Vec3::new(0.0, 0.0, 0.0); - let sphere_handle = meshes.add(Sphere::new(2.0).mesh()); + let sphere_handle = meshes.add(Sphere::new(2.0).mesh_builder()); let alpha = 0.25; @@ -431,7 +431,7 @@ fn spawn_quads(commands: &mut Commands, resources: &mut SceneResources) { let meshes = &mut resources.meshes; let materials = &mut resources.materials; - let quad_handle = meshes.add(Rectangle::new(3.0, 3.0).mesh()); + let quad_handle = meshes.add(Rectangle::new(3.0, 3.0).mesh_builder()); let render_layers = RenderLayers::layer(1); let xform = |x, y, z| { Transform::from_rotation(Quat::from_rotation_y(0.5)) @@ -499,8 +499,8 @@ fn spawn_occlusion_test(commands: &mut Commands, resources: &mut SceneResources) let meshes = &mut resources.meshes; let materials = &mut resources.materials; - let sphere_handle = meshes.add(Sphere::new(1.0).mesh()); - let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh()); + let sphere_handle = meshes.add(Sphere::new(1.0).mesh_builder()); + let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh_builder()); let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6)); let render_layers = RenderLayers::layer(1); diff --git a/examples/3d/orthographic.rs b/examples/3d/orthographic.rs index 1349e49c394b9..8efb6e801daca 100644 --- a/examples/3d/orthographic.rs +++ b/examples/3d/orthographic.rs @@ -30,7 +30,7 @@ fn setup( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cubes diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs index ef7e7cb4c1fb5..445701fc00a45 100644 --- a/examples/3d/parallax_mapping.rs +++ b/examples/3d/parallax_mapping.rs @@ -223,7 +223,7 @@ fn setup( )); // represent the light source as a sphere - let mesh = meshes.add(Sphere::new(0.05).mesh().ico(3).unwrap()); + let mesh = meshes.add(Sphere::new(0.05).mesh_builder().ico(3).unwrap()); // light commands.spawn(( @@ -237,7 +237,7 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(10.0, 10.0))), MeshMaterial3d(materials.add(StandardMaterial { // standard material derived from dark green, but // with roughness and reflectance set. diff --git a/examples/3d/pccm.rs b/examples/3d/pccm.rs index 79f8ffb80b2f8..81f407c16626c 100644 --- a/examples/3d/pccm.rs +++ b/examples/3d/pccm.rs @@ -110,7 +110,7 @@ fn spawn_inner_cube( Cuboid { half_size: Vec3::new(5.0, 1.0, 2.0), } - .mesh() + .mesh_builder() .build() .with_duplicated_vertices() .with_computed_flat_normals(), diff --git a/examples/3d/rect_light.rs b/examples/3d/rect_light.rs index e09ca2e341c2f..b017b64e15713 100644 --- a/examples/3d/rect_light.rs +++ b/examples/3d/rect_light.rs @@ -40,7 +40,7 @@ fn setup( }); commands.insert_resource(FloorMaterial(floor_material.clone())); commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20.0, 20.0))), MeshMaterial3d(floor_material), )); diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index e216af22d5a17..a805fa30539c7 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -137,7 +137,7 @@ fn spawn_sphere( app_status: &AppStatus, ) { // Create a sphere mesh. - let sphere_mesh = meshes.add(Sphere::new(1.0).mesh().ico(7).unwrap()); + let sphere_mesh = meshes.add(Sphere::new(1.0).mesh_builder().ico(7).unwrap()); // Create a sphere. commands.spawn(( diff --git a/examples/3d/rotate_environment_map.rs b/examples/3d/rotate_environment_map.rs index a9ddecc6c5886..6ead18e4209d1 100644 --- a/examples/3d/rotate_environment_map.rs +++ b/examples/3d/rotate_environment_map.rs @@ -51,7 +51,7 @@ fn create_sphere_mesh(meshes: &mut Assets) -> Handle { // We're going to use normal maps, so make sure we've generated tangents, or // else the normal maps won't show up. - let mut sphere_mesh = Sphere::new(1.0).mesh().build(); + let mut sphere_mesh = Sphere::new(1.0).mesh_builder().build(); sphere_mesh .generate_tangents() .expect("Failed to generate tangents"); diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index 6bbb74bed45f6..b6ba7520b1095 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -91,7 +91,13 @@ fn setup( // ground plane let plane_size = 2.0 * spawn_plane_depth; commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(plane_size, plane_size))), + Mesh3d( + meshes.add( + Plane3d::default() + .mesh_builder() + .size(plane_size, plane_size), + ), + ), MeshMaterial3d(white_handle), )); diff --git a/examples/3d/shadow_caster_receiver.rs b/examples/3d/shadow_caster_receiver.rs index 00c4242e506bd..4a1b75498aafe 100644 --- a/examples/3d/shadow_caster_receiver.rs +++ b/examples/3d/shadow_caster_receiver.rs @@ -56,7 +56,7 @@ fn setup( // floating plane - initially not a shadow receiver and not a caster commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20.0, 20.0))), MeshMaterial3d(materials.add(Color::from(LIME))), Transform::from_xyz(0.0, 1.0, -10.0), NotShadowCaster, @@ -65,7 +65,7 @@ fn setup( // lower ground plane - initially a shadow receiver commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20.0, 20.0))), MeshMaterial3d(white_handle), )); diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 6cc31fec18fcd..b81b72e1a4839 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -215,7 +215,7 @@ fn setup_many_lights( let mut rng = ChaCha8Rng::seed_from_u64(42); let mut plane_mesh = Plane3d::default() - .mesh() + .mesh_builder() .size(400.0, 400.0) .build() .with_generated_tangents() @@ -229,14 +229,14 @@ fn setup_many_lights( let plane_mesh = meshes.add(plane_mesh); let cube_mesh = meshes.add( Cuboid::default() - .mesh() + .mesh_builder() .build() .with_generated_tangents() .unwrap(), ); let sphere_mesh = meshes.add( Sphere::new(1.0) - .mesh() + .mesh_builder() .build() .with_generated_tangents() .unwrap(), diff --git a/examples/3d/specular_tint.rs b/examples/3d/specular_tint.rs index 81655cfebd570..fc71dbdf35529 100644 --- a/examples/3d/specular_tint.rs +++ b/examples/3d/specular_tint.rs @@ -102,7 +102,7 @@ fn setup( // Spawn the sphere. commands.spawn(( Transform::from_rotation(Quat::from_rotation_x(PI * 0.5)), - Mesh3d(meshes.add(Sphere::default().mesh().uv(32, 18))), + Mesh3d(meshes.add(Sphere::default().mesh_builder().uv(32, 18))), MeshMaterial3d(standard_materials.add(StandardMaterial { // We want only reflected specular light here, so we set the base // color as black. diff --git a/examples/3d/spherical_area_lights.rs b/examples/3d/spherical_area_lights.rs index f4b244debf0cc..24d7764642b0a 100644 --- a/examples/3d/spherical_area_lights.rs +++ b/examples/3d/spherical_area_lights.rs @@ -26,7 +26,7 @@ fn setup( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(100.0, 100.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.2, 0.2, 0.2), perceptual_roughness: 0.08, @@ -39,7 +39,7 @@ fn setup( let radius_range = 0.0..0.4; let pos_len = position_range.end - position_range.start; let radius_len = radius_range.end - radius_range.start; - let mesh = meshes.add(Sphere::new(1.0).mesh().uv(120, 64)); + let mesh = meshes.add(Sphere::new(1.0).mesh_builder().uv(120, 64)); for i in 0..COUNT { let percent = i as f32 / COUNT as f32; diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 33d77830f13b8..ef3163c9c2bd1 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -23,7 +23,7 @@ fn setup( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/3d/spotlight.rs b/examples/3d/spotlight.rs index 38e38f5a769d5..8e5ed71f70217 100644 --- a/examples/3d/spotlight.rs +++ b/examples/3d/spotlight.rs @@ -42,7 +42,7 @@ fn setup( ) { // ground plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::WHITE)), Movable, )); @@ -71,8 +71,8 @@ fn setup( .take(40), ); - let sphere_mesh = meshes.add(Sphere::new(0.05).mesh().uv(32, 18)); - let sphere_mesh_direction = meshes.add(Sphere::new(0.1).mesh().uv(32, 18)); + let sphere_mesh = meshes.add(Sphere::new(0.05).mesh_builder().uv(32, 18)); + let sphere_mesh_direction = meshes.add(Sphere::new(0.1).mesh_builder().uv(32, 18)); let red_emissive = materials.add(StandardMaterial { base_color: RED.into(), emissive: LinearRgba::new(1.0, 0.0, 0.0, 0.0), diff --git a/examples/3d/ssao.rs b/examples/3d/ssao.rs index 3de273ed71889..98c502ea1f5bf 100644 --- a/examples/3d/ssao.rs +++ b/examples/3d/ssao.rs @@ -58,7 +58,7 @@ fn setup( Transform::from_xyz(1.0, 0.0, 0.0), )); commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.4).mesh().uv(72, 36))), + Mesh3d(meshes.add(Sphere::new(0.4).mesh_builder().uv(72, 36))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.4, 0.4, 0.4), perceptual_roughness: 1.0, diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index 3e969ffdfa3ac..bf57585e90a86 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -64,10 +64,10 @@ fn setup( mut materials: ResMut>, asset_server: Res, ) { - let icosphere_mesh = meshes.add(Sphere::new(0.9).mesh().ico(7).unwrap()); + let icosphere_mesh = meshes.add(Sphere::new(0.9).mesh_builder().ico(7).unwrap()); let cube_mesh = meshes.add(Cuboid::new(0.7, 0.7, 0.7)); - let plane_mesh = meshes.add(Plane3d::default().mesh().size(2.0, 2.0)); - let cylinder_mesh = meshes.add(Cylinder::new(0.5, 2.0).mesh().resolution(50)); + let plane_mesh = meshes.add(Plane3d::default().mesh_builder().size(2.0, 2.0)); + let cylinder_mesh = meshes.add(Cylinder::new(0.5, 2.0).mesh_builder().resolution(50)); // Cube #1 commands.spawn(( diff --git a/examples/3d/transparency_3d.rs b/examples/3d/transparency_3d.rs index c12ff500a595c..fa678cd32f8e9 100644 --- a/examples/3d/transparency_3d.rs +++ b/examples/3d/transparency_3d.rs @@ -19,13 +19,13 @@ fn setup( ) { // Opaque plane, uses `alpha_mode: Opaque` by default commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(6.0, 6.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(6.0, 6.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // Transparent sphere, uses `alpha_mode: Mask(f32)` commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.5).mesh().ico(3).unwrap())), + Mesh3d(meshes.add(Sphere::new(0.5).mesh_builder().ico(3).unwrap())), MeshMaterial3d(materials.add(StandardMaterial { // Alpha channel of the color controls transparency. // We set it to 0.0 here, because it will be changed over time in the @@ -42,7 +42,7 @@ fn setup( // Transparent unlit sphere, uses `alpha_mode: Mask(f32)` commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.5).mesh().ico(3).unwrap())), + Mesh3d(meshes.add(Sphere::new(0.5).mesh_builder().ico(3).unwrap())), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgba(0.2, 0.7, 0.1, 0.0), alpha_mode: AlphaMode::Mask(0.1), @@ -75,7 +75,7 @@ fn setup( // Opaque sphere commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.5).mesh().ico(3).unwrap())), + Mesh3d(meshes.add(Sphere::new(0.5).mesh_builder().ico(3).unwrap())), MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.1))), Transform::from_xyz(0.0, 0.5, -1.5), )); diff --git a/examples/3d/two_passes.rs b/examples/3d/two_passes.rs index 7e6658a066556..d8d4ee6aa3541 100644 --- a/examples/3d/two_passes.rs +++ b/examples/3d/two_passes.rs @@ -17,7 +17,7 @@ fn setup( ) { // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/3d/vertex_colors.rs b/examples/3d/vertex_colors.rs index 7550192ac94f9..74293dbfeb225 100644 --- a/examples/3d/vertex_colors.rs +++ b/examples/3d/vertex_colors.rs @@ -17,7 +17,7 @@ fn setup( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cube diff --git a/examples/3d/visibility_range.rs b/examples/3d/visibility_range.rs index 2ce0d221eb9c8..de4899884b267 100644 --- a/examples/3d/visibility_range.rs +++ b/examples/3d/visibility_range.rs @@ -118,7 +118,7 @@ fn setup( ) { // Spawn a plane. commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(50.0, 50.0))), MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))), )); diff --git a/examples/3d/wireframe.rs b/examples/3d/wireframe.rs index 74376ab0b4c87..a60bf0462cab2 100644 --- a/examples/3d/wireframe.rs +++ b/examples/3d/wireframe.rs @@ -98,7 +98,7 @@ fn setup( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::from(BLUE))), // You can insert this component without the `Wireframe` component // to override the color of the global wireframe for this mesh diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index 544f828ba609b..399f9f8ca3ea6 100644 --- a/examples/animation/animated_mesh.rs +++ b/examples/animation/animated_mesh.rs @@ -111,7 +111,7 @@ fn setup_camera_and_environment( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(500000.0, 500000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/animation/animated_mesh_control.rs b/examples/animation/animated_mesh_control.rs index 412b80ad8df96..b80b11b717a69 100644 --- a/examples/animation/animated_mesh_control.rs +++ b/examples/animation/animated_mesh_control.rs @@ -56,7 +56,7 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(500000.0, 500000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index b4b924b4de0cf..ec4a13a7b1a7b 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -105,7 +105,7 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(500000.0, 500000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/animation/eased_motion.rs b/examples/animation/eased_motion.rs index 3a946199a1740..5b38a1b76c5bf 100644 --- a/examples/animation/eased_motion.rs +++ b/examples/animation/eased_motion.rs @@ -64,7 +64,7 @@ fn setup( // Ground plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50., 50.))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(50., 50.))), MeshMaterial3d(materials.add(Color::from(SILVER))), )); diff --git a/examples/asset/compressed_image_saver.rs b/examples/asset/compressed_image_saver.rs index d727a02044f49..748a77bf6707c 100644 --- a/examples/asset/compressed_image_saver.rs +++ b/examples/asset/compressed_image_saver.rs @@ -51,11 +51,11 @@ fn spawn_scene( let diffuse_env_map = asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"); let specular_env_map = asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"); - let floor_mesh = meshes.add(Circle::new(4.0).mesh().resolution(128).build()); + let floor_mesh = meshes.add(Circle::new(4.0).mesh_builder().resolution(128).build()); let sphere_mesh = meshes.add( Sphere::new(1.0) - .mesh() + .mesh_builder() .kind(SphereKind::Ico { subdivisions: 50 }) .build() .with_generated_tangents() diff --git a/examples/asset/multi_asset_sync.rs b/examples/asset/multi_asset_sync.rs index 1e52d0061b823..2eed4ed440233 100644 --- a/examples/asset/multi_asset_sync.rs +++ b/examples/asset/multi_asset_sync.rs @@ -207,7 +207,7 @@ fn setup_scene( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(50000.0, 50000.0))), MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.2))), Loading, )); @@ -231,7 +231,7 @@ fn wait_on_load( ) { // Change color of plane to green commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(50000.0, 50000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Transform::from_translation(Vec3::Z * -0.01), )); diff --git a/examples/audio/spatial_audio_3d.rs b/examples/audio/spatial_audio_3d.rs index b921fe6fcfd10..a1f144d947599 100644 --- a/examples/audio/spatial_audio_3d.rs +++ b/examples/audio/spatial_audio_3d.rs @@ -26,7 +26,7 @@ fn setup( // sound emitter commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.2).mesh().uv(32, 18))), + Mesh3d(meshes.add(Sphere::new(0.2).mesh_builder().uv(32, 18))), MeshMaterial3d(materials.add(Color::from(BLUE))), Transform::from_xyz(0.0, 0.0, 0.0), Emitter::default(), diff --git a/examples/camera/camera_orbit.rs b/examples/camera/camera_orbit.rs index a4d2fafd3001d..b79fc4ef13f4a 100644 --- a/examples/camera/camera_orbit.rs +++ b/examples/camera/camera_orbit.rs @@ -56,7 +56,7 @@ fn setup( commands.spawn(( Name::new("Plane"), - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), // Turning off culling keeps the plane visible when viewed from beneath. diff --git a/examples/camera/projection_zoom.rs b/examples/camera/projection_zoom.rs index 3797249f33ca5..5b503496fff5d 100644 --- a/examples/camera/projection_zoom.rs +++ b/examples/camera/projection_zoom.rs @@ -70,7 +70,7 @@ fn setup( commands.spawn(( Name::new("Plane"), - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), // Turning off culling keeps the plane visible when viewed from beneath. diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index ca52b4f6dbad8..890e5e9885150 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -66,7 +66,7 @@ fn setup( // Make a plane for establishing space. commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(12.0, 12.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Transform::from_xyz(0.0, -2.5, 0.0), )); @@ -87,7 +87,7 @@ fn setup( )); // Create a new sphere mesh: - let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?; + let mut sphere_mesh = Sphere::new(1.0).mesh_builder().ico(7)?; sphere_mesh.generate_tangents()?; // Spawn the mesh into the scene: @@ -102,7 +102,7 @@ fn setup( let distribution = UniformMeshSampler::try_new(triangles)?; // Setup sample points: - let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?); + let point_mesh = meshes.add(Sphere::new(0.01).mesh_builder().ico(3)?); let point_material = materials.add(StandardMaterial { base_color: Srgba::RED.into(), emissive: LinearRgba::rgb(1.0, 0.0, 0.0), diff --git a/examples/ecs/iter_combinations.rs b/examples/ecs/iter_combinations.rs index 0e998ed508b7f..c41d4cce4548d 100644 --- a/examples/ecs/iter_combinations.rs +++ b/examples/ecs/iter_combinations.rs @@ -41,7 +41,7 @@ fn generate_bodies( mut meshes: ResMut>, mut materials: ResMut>, ) { - let mesh = meshes.add(Sphere::new(1.0).mesh().ico(3).unwrap()); + let mesh = meshes.add(Sphere::new(1.0).mesh_builder().ico(3).unwrap()); let color_range = 0.5..1.0; let vel_range = -0.5..0.5; @@ -94,7 +94,7 @@ fn generate_bodies( commands .spawn(( BodyBundle { - mesh: Mesh3d(meshes.add(Sphere::new(1.0).mesh().ico(5).unwrap())), + mesh: Mesh3d(meshes.add(Sphere::new(1.0).mesh_builder().ico(5).unwrap())), material: MeshMaterial3d(materials.add(StandardMaterial { base_color: ORANGE_RED.into(), emissive: LinearRgba::from(ORANGE_RED) * 2., diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index 9da5f5a30a72a..07efd9d197b84 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -56,7 +56,7 @@ fn setup( )); // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cube diff --git a/examples/gizmos/axes.rs b/examples/gizmos/axes.rs index 71fd1ece46882..daf6920371c6b 100644 --- a/examples/gizmos/axes.rs +++ b/examples/gizmos/axes.rs @@ -86,7 +86,7 @@ fn setup( // A plane to give a sense of place commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20., 20.))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20., 20.))), MeshMaterial3d(materials.add(Color::srgb(0.1, 0.1, 0.1))), Transform::from_xyz(0., -2., 0.), )); diff --git a/examples/gizmos/transform_gizmo.rs b/examples/gizmos/transform_gizmo.rs index ee21a21d1152d..99cce5eb339f7 100644 --- a/examples/gizmos/transform_gizmo.rs +++ b/examples/gizmos/transform_gizmo.rs @@ -49,7 +49,7 @@ fn setup( // Ground plane (not pickable) commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(10.0, 10.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))), Pickable::IGNORE, )); diff --git a/examples/gltf/gltf_extension_animation_graph.rs b/examples/gltf/gltf_extension_animation_graph.rs index c5d89ff646d90..db6386bfb4c85 100644 --- a/examples/gltf/gltf_extension_animation_graph.rs +++ b/examples/gltf/gltf_extension_animation_graph.rs @@ -99,7 +99,7 @@ fn setup_camera_and_environment( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(500000.0, 500000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/large_scenes/mipmap_generator/examples/test_compression.rs b/examples/large_scenes/mipmap_generator/examples/test_compression.rs index 8d890de3d006d..76e3d14c1a1f8 100644 --- a/examples/large_scenes/mipmap_generator/examples/test_compression.rs +++ b/examples/large_scenes/mipmap_generator/examples/test_compression.rs @@ -65,7 +65,7 @@ fn setup( let mut mat_rgba = StandardMaterial::from(images.add(image_rgba)); mat_rgba.unlit = true; - let plane_h = meshes.add(Plane3d::default().mesh().size(20.0, 30.0)); + let plane_h = meshes.add(Plane3d::default().mesh_builder().size(20.0, 30.0)); // planes commands.spawn(( diff --git a/examples/large_scenes/mipmap_generator/examples/test_image.rs b/examples/large_scenes/mipmap_generator/examples/test_image.rs index 250a63441e36a..bb75b7dc7d4e3 100644 --- a/examples/large_scenes/mipmap_generator/examples/test_image.rs +++ b/examples/large_scenes/mipmap_generator/examples/test_image.rs @@ -28,7 +28,7 @@ fn setup( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(20.0, 20.0))), MeshMaterial3d(materials.add(StandardMaterial::from(images.add(image)))), )); // light diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index 5db89f0be85f5..0807f3ca4c1f9 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -179,7 +179,7 @@ fn setup( // Spawn the 2D heart commands.spawn(( // We can use the methods defined on the `MeshBuilder` to customize the mesh. - Mesh3d(meshes.add(HEART.mesh().resolution(50))), + Mesh3d(meshes.add(HEART.mesh_builder().resolution(50))), MeshMaterial3d(materials.add(StandardMaterial { emissive: RED.into(), base_color: RED.into(), @@ -194,7 +194,7 @@ fn setup( // Spawn the 2D heart ring commands.spawn(( // We can use the methods defined on the `MeshBuilder` to customize the mesh. - Mesh3d(meshes.add(RING.mesh().with_inner(|heart| heart.resolution(50)))), + Mesh3d(meshes.add(RING.mesh_builder().with_inner(|heart| heart.resolution(50)))), MeshMaterial3d(materials.add(StandardMaterial { emissive: RED.into(), base_color: RED.into(), @@ -209,7 +209,7 @@ fn setup( // Spawn an extrusion of the heart commands.spawn(( // We can set a custom resolution for the round parts of the extrusion as well. - Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))), + Mesh3d(meshes.add(EXTRUSION.mesh_builder().resolution(50))), MeshMaterial3d(materials.add(StandardMaterial { base_color: RED.into(), ..Default::default() @@ -226,7 +226,7 @@ fn setup( Mesh3d( meshes.add( RING_EXTRUSION - .mesh() + .mesh_builder() .with_inner(|ring| ring.with_inner(|heart| heart.resolution(50))), ), ), @@ -473,7 +473,7 @@ impl Meshable for Heart { // The `MeshBuilder` can be used to create the actual mesh for that primitive. type Output = HeartMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh_builder(&self) -> Self::Output { Self::Output { heart: *self, resolution: 32, diff --git a/examples/math/random_sampling.rs b/examples/math/random_sampling.rs index 7aa1b16daf1fa..0bd6b40f6bd10 100644 --- a/examples/math/random_sampling.rs +++ b/examples/math/random_sampling.rs @@ -59,7 +59,7 @@ fn setup( // Make a plane for establishing space. commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(12.0, 12.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Transform::from_xyz(0.0, -2.5, 0.0), )); @@ -98,7 +98,7 @@ fn setup( commands.insert_resource(PointMesh( meshes.add( Sphere::new(0.03) - .mesh() + .mesh_builder() .kind(SphereKind::Ico { subdivisions: 3 }), ), )); diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index 4d7a3117bfa31..b0dd474e4e396 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -523,26 +523,26 @@ fn spawn_primitive_2d( }; let convex_polygon = ConvexPolygon::new(CONVEX_POLYGON_VERTICES).unwrap(); [ - Some(RECTANGLE.mesh().build()), - Some(CIRCLE.mesh().build()), - Some(ELLIPSE.mesh().build()), - Some(TRIANGLE_2D.mesh().build()), + Some(RECTANGLE.mesh_builder().build()), + Some(CIRCLE.mesh_builder().build()), + Some(ELLIPSE.mesh_builder().build()), + Some(TRIANGLE_2D.mesh_builder().build()), None, // plane None, // line - Some(SEGMENT_2D.mesh().build()), - Some(polyline_2d.mesh().build()), + Some(SEGMENT_2D.mesh_builder().build()), + Some(polyline_2d.mesh_builder().build()), None, // polygon - Some(convex_polygon.mesh().build()), - Some(REGULAR_POLYGON.mesh().build()), - Some(CAPSULE_2D.mesh().build()), + Some(convex_polygon.mesh_builder().build()), + Some(REGULAR_POLYGON.mesh_builder().build()), + Some(CAPSULE_2D.mesh_builder().build()), None, // cylinder None, // cone None, // conical frustum - Some(ANNULUS.mesh().build()), + Some(ANNULUS.mesh_builder().build()), None, // tetrahedron None, // arc - Some(CIRCULAR_SECTOR.mesh().build()), - Some(CIRCULAR_SEGMENT.mesh().build()), + Some(CIRCULAR_SECTOR.mesh_builder().build()), + Some(CIRCULAR_SEGMENT.mesh_builder().build()), ] .into_iter() .zip(PrimitiveSelected::ALL) @@ -574,23 +574,23 @@ fn spawn_primitive_3d( vertices: POLYLINE_3D_VERTICES.to_vec(), }; [ - Some(CUBOID.mesh().build()), - Some(SPHERE.mesh().build()), + Some(CUBOID.mesh_builder().build()), + Some(SPHERE.mesh_builder().build()), None, // ellipse - Some(TRIANGLE_3D.mesh().build()), - Some(PLANE_3D.mesh().build()), + Some(TRIANGLE_3D.mesh_builder().build()), + Some(PLANE_3D.mesh_builder().build()), None, // line - Some(SEGMENT_3D.mesh().build()), - Some(polyline_3d.mesh().build()), + Some(SEGMENT_3D.mesh_builder().build()), + Some(polyline_3d.mesh_builder().build()), None, // polygon None, // convex polygon None, // regular polygon - Some(CAPSULE_3D.mesh().build()), - Some(CYLINDER.mesh().build()), - Some(CONE.mesh().build()), - Some(CONICAL_FRUSTUM.mesh().build()), - Some(TORUS.mesh().build()), - Some(TETRAHEDRON.mesh().build()), + Some(CAPSULE_3D.mesh_builder().build()), + Some(CYLINDER.mesh_builder().build()), + Some(CONE.mesh_builder().build()), + Some(CONICAL_FRUSTUM.mesh_builder().build()), + Some(TORUS.mesh_builder().build()), + Some(TETRAHEDRON.mesh_builder().build()), None, // arc None, // circular sector None, // circular segment diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 894a4673966dd..f29504516751c 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -104,7 +104,7 @@ fn setup_scene( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))), )); // cube @@ -115,7 +115,7 @@ fn setup_scene( )); // sphere commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.5).mesh().ico(4).unwrap())), + Mesh3d(meshes.add(Sphere::new(0.5).mesh_builder().ico(4).unwrap())), MeshMaterial3d(materials.add(Color::srgb(0.1, 0.4, 0.8))), Transform::from_xyz(1.5, 1.5, 1.5), )); diff --git a/examples/movement/smooth_follow.rs b/examples/movement/smooth_follow.rs index a41fcfddfa255..1f73271bf7f74 100644 --- a/examples/movement/smooth_follow.rs +++ b/examples/movement/smooth_follow.rs @@ -46,7 +46,7 @@ fn setup( ) { // A plane: commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(12.0, 12.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.15, 0.3))), Transform::from_xyz(0.0, -2.5, 0.0), )); diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index dabbfa159eca9..dca61435043fa 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -70,7 +70,10 @@ fn setup_scene( ) { let shapes: [(Mesh, Color); 3] = [ (Cuboid::default().into(), RED.into()), - (Sphere::default().mesh().ico(2).unwrap(), GREEN.into()), + ( + Sphere::default().mesh_builder().ico(2).unwrap(), + GREEN.into(), + ), (Cylinder::default().into(), BLUE.into()), ]; @@ -87,7 +90,7 @@ fn setup_scene( } commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(30.0, 30.0))), MeshMaterial3d(materials.add(Color::from(DARK_GRAY))), Pickable::IGNORE, )); diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 4766d43a0a86c..fa62c480fe452 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -59,8 +59,8 @@ fn setup_scene( meshes.add(Cylinder::default()), meshes.add(Cone::default()), meshes.add(ConicalFrustum::default()), - meshes.add(Sphere::default().mesh().ico(5).unwrap()), - meshes.add(Sphere::default().mesh().uv(32, 18)), + meshes.add(Sphere::default().mesh_builder().ico(5).unwrap()), + meshes.add(Sphere::default().mesh_builder().uv(32, 18)), ]; let extrusions = [ @@ -121,7 +121,14 @@ fn setup_scene( // Ground commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), + Mesh3d( + meshes.add( + Plane3d::default() + .mesh_builder() + .size(50.0, 50.0) + .subdivisions(10), + ), + ), MeshMaterial3d(ground_matl.clone()), Pickable::IGNORE, // Disable picking for the ground plane. )); diff --git a/examples/shader/shader_material_screenspace_texture.rs b/examples/shader/shader_material_screenspace_texture.rs index 2b439f7347e7e..0034217d88579 100644 --- a/examples/shader/shader_material_screenspace_texture.rs +++ b/examples/shader/shader_material_screenspace_texture.rs @@ -26,7 +26,7 @@ fn setup( mut standard_materials: ResMut>, ) { commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(standard_materials.add(Color::srgb(0.3, 0.5, 0.3))), )); commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0))); diff --git a/examples/shader/shader_prepass.rs b/examples/shader/shader_prepass.rs index 24acaeb0e6b65..eca9f338fdc55 100644 --- a/examples/shader/shader_prepass.rs +++ b/examples/shader/shader_prepass.rs @@ -60,7 +60,7 @@ fn setup( // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(std_materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 2dfd082a6770c..b88cfc15b8c03 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -238,7 +238,7 @@ fn setup( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5000.0, 5000.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index 5f3111494a121..1768b5aa3698b 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -51,7 +51,7 @@ fn setup( const N_LIGHTS: usize = 100_000; commands.spawn(( - Mesh3d(meshes.add(Sphere::new(RADIUS).mesh().ico(9).unwrap())), + Mesh3d(meshes.add(Sphere::new(RADIUS).mesh_builder().ico(9).unwrap())), MeshMaterial3d(materials.add(Color::WHITE)), Transform::from_scale(Vec3::NEG_ONE), )); diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs index c4553dbb09c89..c4814192c03ba 100644 --- a/examples/testbed/3d.rs +++ b/examples/testbed/3d.rs @@ -132,7 +132,7 @@ mod light { mut materials: ResMut>, ) { commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(10.0, 10.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::WHITE, perceptual_roughness: 1.0, @@ -236,7 +236,7 @@ mod bloom { ..default() }); - let mesh = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap()); + let mesh = meshes.add(Sphere::new(0.5).mesh_builder().ico(5).unwrap()); for z in -2..3_i32 { let material = match (z % 2).abs() { diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index 68a9eb3aaab47..9cd6a223a3f15 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -60,7 +60,7 @@ fn setup( // A plane that we can sit on top of commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), Transform::from_xyz(0., -2., 0.), )); diff --git a/examples/transforms/transform.rs b/examples/transforms/transform.rs index 858e848edd572..11bd3e6a908a4 100644 --- a/examples/transforms/transform.rs +++ b/examples/transforms/transform.rs @@ -45,7 +45,7 @@ fn setup( ) { // Add an object (sphere) for visualizing scaling. commands.spawn(( - Mesh3d(meshes.add(Sphere::new(3.0).mesh().ico(32).unwrap())), + Mesh3d(meshes.add(Sphere::new(3.0).mesh_builder().ico(32).unwrap())), MeshMaterial3d(materials.add(Color::from(YELLOW))), Transform::from_translation(Vec3::ZERO), Center { diff --git a/examples/usage/debug_frustum_culling.rs b/examples/usage/debug_frustum_culling.rs index 008481b489041..a18d39fdbb4c7 100644 --- a/examples/usage/debug_frustum_culling.rs +++ b/examples/usage/debug_frustum_culling.rs @@ -184,7 +184,7 @@ fn setup( Mesh3d( meshes.add( Plane3d::default() - .mesh() + .mesh_builder() .size(SHAPE_RING_RADIUS * 4., SHAPE_RING_RADIUS * 4.), ), ), @@ -192,7 +192,7 @@ fn setup( )); // Blue Wall Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5., 5.))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5., 5.))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.5))), Transform::from_xyz(20., 2.5, 10.).with_rotation(Quat::from_rotation_z(PI / 2.)), )); @@ -230,7 +230,7 @@ fn setup( meshes.add(Cuboid { half_size: Vec3::new(1., 0.1, 2.), }), - meshes.add(Sphere::default().mesh().ico(5).unwrap()), + meshes.add(Sphere::default().mesh_builder().ico(5).unwrap()), ]; let shapes_len = shapes.len() as f32; let mut shape_ring = commands.spawn((Transform::default(), Visibility::default(), ShapeRing)); diff --git a/examples/window/screenshot.rs b/examples/window/screenshot.rs index c864f5763f5ca..330489d15ba72 100644 --- a/examples/window/screenshot.rs +++ b/examples/window/screenshot.rs @@ -54,7 +54,7 @@ fn setup( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cube diff --git a/tests/3d/test_invalid_skinned_mesh.rs b/tests/3d/test_invalid_skinned_mesh.rs index cbd1082fcdb37..c0e56306b7fab 100644 --- a/tests/3d/test_invalid_skinned_mesh.rs +++ b/tests/3d/test_invalid_skinned_mesh.rs @@ -80,7 +80,14 @@ fn setup_environment( // Add a plane behind the meshes so we can see the shadows. commands.spawn(( Transform::from_xyz(0.0, 0.0, -1.0), - Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(100.0, 100.0).normal(Dir3::Z))), + Mesh3d( + mesh_assets.add( + Plane3d::default() + .mesh_builder() + .size(100.0, 100.0) + .normal(Dir3::Z), + ), + ), MeshMaterial3d(material_assets.add(StandardMaterial { base_color: Color::srgb(0.05, 0.05, 0.15), reflectance: 0.2, @@ -211,7 +218,14 @@ fn setup_meshes( // Add a square behind the mesh to distinguish it from the other meshes. commands.spawn(( Transform::from_xyz(transform.translation.x, transform.translation.y, -0.8), - Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(4.3, 4.3).normal(Dir3::Z))), + Mesh3d( + mesh_assets.add( + Plane3d::default() + .mesh_builder() + .size(4.3, 4.3) + .normal(Dir3::Z), + ), + ), MeshMaterial3d(background_material_handle.clone()), )); } diff --git a/tests/window/minimizing.rs b/tests/window/minimizing.rs index 8099c27a76365..f706935ce83b2 100644 --- a/tests/window/minimizing.rs +++ b/tests/window/minimizing.rs @@ -34,7 +34,7 @@ fn setup_3d( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cube diff --git a/tests/window/resizing.rs b/tests/window/resizing.rs index d0b0cb8aaf354..e595d752c065e 100644 --- a/tests/window/resizing.rs +++ b/tests/window/resizing.rs @@ -110,7 +110,7 @@ fn setup_3d( ) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + Mesh3d(meshes.add(Plane3d::default().mesh_builder().size(5.0, 5.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); // cube From 3b97abd8f597d75ed34938f78edfd11c9e2c87e2 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 21:34:13 +0200 Subject: [PATCH 13/17] delete old implementations --- crates/bevy_camera/src/primitives.rs | 2 +- crates/bevy_camera/src/projection.rs | 4 +- crates/bevy_gizmos/src/lib.rs | 2 - crates/bevy_gizmos/src/primitives/dim2.rs | 814 ----- crates/bevy_gizmos/src/primitives/dim3.rs | 931 ------ crates/bevy_gizmos/src/primitives/helpers.rs | 43 - crates/bevy_gizmos/src/primitives/mod.rs | 5 - crates/bevy_light/src/cluster/assign.rs | 2 +- crates/bevy_light/src/directional_light.rs | 2 +- crates/bevy_light/src/point_light.rs | 2 +- crates/bevy_light/src/spot_light.rs | 2 +- .../bevy_math/src/bounding/bounded2d/mod.rs | 2 - .../src/bounding/bounded2d/primitive_impls.rs | 1021 ------- .../src/bounding/bounded3d/extrusion.rs | 463 --- .../bevy_math/src/bounding/bounded3d/mod.rs | 31 +- .../src/bounding/bounded3d/primitive_impls.rs | 690 ----- crates/bevy_math/src/bounding/raycast3d.rs | 2 +- .../src/{primitives => camera}/half_space.rs | 4 +- crates/bevy_math/src/camera/mod.rs | 7 + .../src/camera}/view_frustum.rs | 6 +- crates/bevy_math/src/direction.rs | 8 +- crates/bevy_math/src/lib.rs | 19 +- crates/bevy_math/src/primitives/dim2.rs | 2637 ----------------- crates/bevy_math/src/primitives/dim3.rs | 2063 ------------- crates/bevy_math/src/primitives/inset.rs | 98 - crates/bevy_math/src/primitives/mod.rs | 54 - crates/bevy_math/src/primitives/polygon.rs | 375 --- .../bevy_math/src/primitives/view_frustum.rs | 251 -- crates/bevy_math/src/ray.rs | 30 +- .../bevy_math/src/sampling/mesh_sampling.rs | 62 - crates/bevy_math/src/sampling/mod.rs | 7 - .../bevy_math/src/sampling/shape_sampling.rs | 694 ----- crates/bevy_mesh/src/mesh.rs | 54 +- crates/bevy_mesh/src/primitives/dim2.rs | 1678 ----------- .../bevy_mesh/src/primitives/dim3/capsule.rs | 436 --- crates/bevy_mesh/src/primitives/dim3/cone.rs | 271 -- .../src/primitives/dim3/conical_frustum.rs | 184 -- .../bevy_mesh/src/primitives/dim3/cuboid.rs | 99 - .../bevy_mesh/src/primitives/dim3/cylinder.rs | 222 -- crates/bevy_mesh/src/primitives/dim3/mod.rs | 23 - crates/bevy_mesh/src/primitives/dim3/plane.rs | 206 -- .../src/primitives/dim3/polyline3d.rs | 43 - .../src/primitives/dim3/segment3d.rs | 51 - .../bevy_mesh/src/primitives/dim3/sphere.rs | 268 -- .../src/primitives/dim3/tetrahedron.rs | 66 - crates/bevy_mesh/src/primitives/dim3/torus.rs | 176 -- .../src/primitives/dim3/triangle3d.rs | 130 - crates/bevy_mesh/src/primitives/extrusion.rs | 441 --- crates/bevy_mesh/src/primitives/mod.rs | 22 +- crates/bevy_shapes/src/bounding/dim3.rs | 6 +- crates/bevy_shapes/src/bounding/extrusion.rs | 2 +- crates/bevy_shapes/src/meshing/mod.rs | 21 +- crates/bevy_shapes/src/primitives/dim2.rs | 2 +- .../bevy_shapes/src/primitives/half_space.rs | 153 - crates/bevy_shapes/src/primitives/inset.rs | 10 +- crates/bevy_shapes/src/primitives/mod.rs | 6 +- 56 files changed, 81 insertions(+), 14822 deletions(-) delete mode 100644 crates/bevy_gizmos/src/primitives/dim2.rs delete mode 100644 crates/bevy_gizmos/src/primitives/dim3.rs delete mode 100644 crates/bevy_gizmos/src/primitives/helpers.rs delete mode 100644 crates/bevy_gizmos/src/primitives/mod.rs delete mode 100644 crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs delete mode 100644 crates/bevy_math/src/bounding/bounded3d/extrusion.rs delete mode 100644 crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs rename crates/bevy_math/src/{primitives => camera}/half_space.rs (98%) create mode 100644 crates/bevy_math/src/camera/mod.rs rename crates/{bevy_shapes/src/primitives => bevy_math/src/camera}/view_frustum.rs (99%) delete mode 100644 crates/bevy_math/src/primitives/dim2.rs delete mode 100644 crates/bevy_math/src/primitives/dim3.rs delete mode 100644 crates/bevy_math/src/primitives/inset.rs delete mode 100644 crates/bevy_math/src/primitives/mod.rs delete mode 100644 crates/bevy_math/src/primitives/polygon.rs delete mode 100644 crates/bevy_math/src/primitives/view_frustum.rs delete mode 100644 crates/bevy_math/src/sampling/mesh_sampling.rs delete mode 100644 crates/bevy_math/src/sampling/shape_sampling.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim2.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/capsule.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/cone.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/cuboid.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/cylinder.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/mod.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/plane.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/polyline3d.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/segment3d.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/sphere.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/torus.rs delete mode 100644 crates/bevy_mesh/src/primitives/dim3/triangle3d.rs delete mode 100644 crates/bevy_mesh/src/primitives/extrusion.rs delete mode 100644 crates/bevy_shapes/src/primitives/half_space.rs diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index bb42023f9e5ab..bc97856791273 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -4,7 +4,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; use bevy_math::{ bounding::{Aabb3d, BoundingVolume}, - primitives::{HalfSpace, ViewFrustum}, + camera::{HalfSpace, ViewFrustum}, Affine3A, Mat3A, Vec3, Vec3A, }; use bevy_mesh::{Mesh, VertexAttributeValues}; diff --git a/crates/bevy_camera/src/projection.rs b/crates/bevy_camera/src/projection.rs index 85d849d6557c7..cc073f11fedd0 100644 --- a/crates/bevy_camera/src/projection.rs +++ b/crates/bevy_camera/src/projection.rs @@ -4,9 +4,7 @@ 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::{camera::ViewFrustum, ops, proj, vec4, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_transform::{components::GlobalTransform, TransformSystems}; use derive_more::derive::From; diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 212f04a13a780..79f6c81f3731c 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -33,7 +33,6 @@ pub mod frustum; pub mod gizmos; mod global; pub mod grid; -pub mod primitives; pub mod retained; pub mod rounded_box; mod simplex_stroke_font; @@ -65,7 +64,6 @@ pub mod prelude { }, gizmos::Gizmos, global::gizmo, - primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d}, retained::Gizmo, AppGizmoBuilder, GizmoAsset, }; diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs deleted file mode 100644 index a8f25dcac78d8..0000000000000 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ /dev/null @@ -1,814 +0,0 @@ -//! A module for rendering each of the 2D [`bevy_math::primitives`] with [`GizmoBuffer`]. - -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 crate::{gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; - -// some magic number since using directions as offsets will result in lines of length 1 pixel -const MIN_LINE_LEN: f32 = 50.0; -const HALF_MIN_LINE_LEN: f32 = 25.0; -// length used to simulate infinite lines -const INFINITE_LEN: f32 = 100_000.0; - -/// A trait for rendering 2D geometric primitives (`P`) with [`GizmoBuffer`]. -pub trait GizmoPrimitive2d { - /// The output of `primitive_2d`. This is a builder to set non-default values. - type Output<'a> - where - Self: 'a; - - /// Renders a 2D primitive with its associated details. - fn primitive_2d( - &mut self, - primitive: &P, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_>; -} - -// direction 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Dir2, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - let isometry = isometry.into(); - let start = Vec2::ZERO; - let end = *primitive * MIN_LINE_LEN; - self.arrow_2d(isometry * start, isometry * end, color); - } -} - -// arc 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Arc2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - let start_iso = isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.half_angle)); - - self.arc_2d( - start_iso, - primitive.half_angle * 2.0, - primitive.radius, - color, - ); - } -} - -// circle 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = crate::circles::Ellipse2dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Circle, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - self.circle_2d(isometry, primitive.radius, color) - } -} - -// circular sector 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &CircularSector, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - let color = color.into(); - - let start_iso = - isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); - let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); - - // we need to draw the arc part of the sector, and the two lines connecting the arc and the center - self.arc_2d( - start_iso, - primitive.arc.half_angle * 2.0, - primitive.arc.radius, - color, - ); - - let end_position = primitive.arc.radius * Vec2::Y; - self.line_2d(isometry * Vec2::ZERO, start_iso * end_position, color); - self.line_2d(isometry * Vec2::ZERO, end_iso * end_position, color); - } -} - -// circular segment 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &CircularSegment, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - let color = color.into(); - - let start_iso = - isometry * Isometry2d::from_rotation(Rot2::radians(-primitive.arc.half_angle)); - let end_iso = isometry * Isometry2d::from_rotation(Rot2::radians(primitive.arc.half_angle)); - - // we need to draw the arc part of the segment, and the line connecting the two ends - self.arc_2d( - start_iso, - primitive.arc.half_angle * 2.0, - primitive.arc.radius, - color, - ); - - let position = primitive.arc.radius * Vec2::Y; - self.line_2d(start_iso * position, end_iso * position, color); - } -} - -// ellipse 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = crate::circles::Ellipse2dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_2d<'a>( - &mut self, - primitive: &Ellipse, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - self.ellipse_2d(isometry, primitive.half_size, color) - } -} - -// annulus 2d - -/// Builder for configuring the drawing options of [`Annulus`]. -pub struct Annulus2dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - isometry: Isometry2d, - inner_radius: f32, - outer_radius: f32, - color: Color, - inner_resolution: u32, - outer_resolution: u32, -} - -impl Annulus2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of line-segments for each circle of the annulus. - pub fn resolution(mut self, resolution: u32) -> Self { - self.outer_resolution = resolution; - self.inner_resolution = resolution; - self - } - - /// Set the number of line-segments for the outer circle of the annulus. - pub fn outer_resolution(mut self, resolution: u32) -> Self { - self.outer_resolution = resolution; - self - } - - /// Set the number of line-segments for the inner circle of the annulus. - pub fn inner_resolution(mut self, resolution: u32) -> Self { - self.inner_resolution = resolution; - self - } -} - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Annulus2dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Annulus, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Annulus2dBuilder { - gizmos: self, - isometry: isometry.into(), - inner_radius: primitive.inner_circle.radius, - outer_radius: primitive.outer_circle.radius, - color: color.into(), - inner_resolution: crate::circles::DEFAULT_CIRCLE_RESOLUTION, - outer_resolution: crate::circles::DEFAULT_CIRCLE_RESOLUTION, - } - } -} - -impl Drop for Annulus2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let Annulus2dBuilder { - gizmos, - isometry, - inner_radius, - outer_radius, - inner_resolution, - outer_resolution, - color, - .. - } = self; - - gizmos - .circle_2d(*isometry, *outer_radius, *color) - .resolution(*outer_resolution); - gizmos - .circle_2d(*isometry, *inner_radius, *color) - .resolution(*inner_resolution); - } -} - -// rhombus 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Rhombus, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - }; - let isometry = isometry.into(); - let [a, b, c, d] = - [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)].map(|(sign_x, sign_y)| { - Vec2::new( - primitive.half_diagonals.x * sign_x, - primitive.half_diagonals.y * sign_y, - ) - }); - let positions = [a, b, c, d].map(|vec2| isometry * vec2); - self.lineloop_2d(positions, color); - } -} - -// capsule 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Capsule2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - let isometry = isometry.into(); - let polymorphic_color: Color = color.into(); - - if !self.enabled { - return; - } - - // transform points from the reference unit square to capsule "rectangle" - let [top_left, top_right, bottom_left, bottom_right, top_center, bottom_center] = [ - [-1.0, 1.0], - [1.0, 1.0], - [-1.0, -1.0], - [1.0, -1.0], - // just reuse the pipeline for these points as well - [0.0, 1.0], - [0.0, -1.0], - ] - .map(|[sign_x, sign_y]| Vec2::X * sign_x + Vec2::Y * sign_y) - .map(|reference_point| { - let scaling = Vec2::X * primitive.radius + Vec2::Y * primitive.half_length; - reference_point * scaling - }) - .map(|vec2| isometry * vec2); - - // draw left and right side of capsule "rectangle" - self.line_2d(bottom_left, top_left, polymorphic_color); - self.line_2d(bottom_right, top_right, polymorphic_color); - - let start_angle_top = isometry.rotation.as_radians() - FRAC_PI_2; - let start_angle_bottom = isometry.rotation.as_radians() + FRAC_PI_2; - - // draw arcs - self.arc_2d( - Isometry2d::new(top_center, Rot2::radians(start_angle_top)), - PI, - primitive.radius, - polymorphic_color, - ); - self.arc_2d( - Isometry2d::new(bottom_center, Rot2::radians(start_angle_bottom)), - PI, - primitive.radius, - polymorphic_color, - ); - } -} - -// line 2d -// -/// Builder for configuring the drawing options of [`Line2d`]. -pub struct Line2dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - direction: Dir2, // Direction of the line - - isometry: Isometry2d, - color: Color, // color of the line - - draw_arrow: bool, // decides whether to indicate the direction of the line with an arrow -} - -impl Line2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the drawing mode of the line (arrow vs. plain line) - pub fn draw_arrow(mut self, is_enabled: bool) -> Self { - self.draw_arrow = is_enabled; - self - } -} - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Line2dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Line2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Line2dBuilder { - gizmos: self, - direction: primitive.direction, - isometry: isometry.into(), - color: color.into(), - draw_arrow: false, - } - } -} - -impl Drop for Line2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let [start, end] = [1.0, -1.0] - .map(|sign| sign * INFINITE_LEN) - // offset the line from the origin infinitely into the given direction - .map(|length| self.direction * length) - // transform the line with the given isometry - .map(|offset| self.isometry * offset); - - self.gizmos.line_2d(start, end, self.color); - - // optionally draw an arrow head at the center of the line - if self.draw_arrow { - self.gizmos.arrow_2d( - self.isometry * (-self.direction * MIN_LINE_LEN), - self.isometry * Vec2::ZERO, - self.color, - ); - } - } -} - -// plane 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Plane2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - let isometry = isometry.into(); - let polymorphic_color: Color = color.into(); - - if !self.enabled { - return; - } - // draw normal of the plane (orthogonal to the plane itself) - let normal = primitive.normal; - let normal_segment = Segment2d::from_direction_and_length(normal, HALF_MIN_LINE_LEN * 2.); - self.primitive_2d( - &normal_segment, - // offset the normal so it starts on the plane line - Isometry2d::new(isometry * (HALF_MIN_LINE_LEN * normal), isometry.rotation), - polymorphic_color, - ) - .draw_arrow(true); - - // draw the plane line - let direction = Dir2::new_unchecked(-normal.perp()); - self.primitive_2d(&Line2d { direction }, isometry, polymorphic_color) - .draw_arrow(false); - - // draw an arrow such that the normal is always left side of the plane with respect to the - // planes direction. This is to follow the "counter-clockwise" convention - self.arrow_2d( - isometry * Vec2::ZERO, - isometry * (MIN_LINE_LEN * direction), - polymorphic_color, - ); - } -} - -// segment 2d - -/// Builder for configuring the drawing options of [`Segment2d`]. -pub struct Segment2dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - point1: Vec2, // First point of the segment - point2: Vec2, // Second point of the segment - - isometry: Isometry2d, // isometric transformation of the line segment - color: Color, // color of the line segment - - draw_arrow: bool, // decides whether to draw just a line or an arrow -} - -impl Segment2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the drawing mode of the line (arrow vs. plain line) - pub fn draw_arrow(mut self, is_enabled: bool) -> Self { - self.draw_arrow = is_enabled; - self - } -} - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Segment2dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Segment2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Segment2dBuilder { - gizmos: self, - point1: primitive.point1(), - point2: primitive.point2(), - - isometry: isometry.into(), - color: color.into(), - - draw_arrow: Default::default(), - } - } -} - -impl Drop for Segment2dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let segment = Segment2d::new(self.point1, self.point2).transformed(self.isometry); - - if self.draw_arrow { - self.gizmos - .arrow_2d(segment.point1(), segment.point2(), self.color); - } else { - self.gizmos - .line_2d(segment.point1(), segment.point2(), self.color); - } - } -} - -// polyline 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Polyline2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - self.linestrip_2d( - primitive - .vertices - .iter() - .copied() - .map(|vec2| isometry * vec2), - color, - ); - } -} - -// triangle 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Triangle2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let [a, b, c] = primitive.vertices; - let positions = [a, b, c].map(|vec2| isometry * vec2); - self.lineloop_2d(positions, color); - } -} - -// rectangle 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Rectangle, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let [a, b, c, d] = - [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)].map(|(sign_x, sign_y)| { - Vec2::new( - primitive.half_size.x * sign_x, - primitive.half_size.y * sign_y, - ) - }); - let positions = [a, b, c, d].map(|vec2| isometry * vec2); - self.lineloop_2d(positions, color); - } -} - -// polygon 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &Polygon, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let vertices = if primitive.vertices.first() == primitive.vertices.last() { - // Strip closing point if there is one - &primitive.vertices[..primitive.vertices.len() - 1] - } else { - &primitive.vertices[..] - }; - - self.lineloop_2d(vertices.iter().map(|&vec2| isometry * vec2), color); - } -} - -// regular polygon 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &RegularPolygon, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let points = (0..primitive.sides) - .map(|n| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, n)) - .map(|vec2| isometry * vec2); - self.lineloop_2d(points, color); - } -} diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs deleted file mode 100644 index b4dd923a7b009..0000000000000 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ /dev/null @@ -1,931 +0,0 @@ -//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`GizmoBuffer`]. - -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 crate::{circles::SphereBuilder, gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; - -const DEFAULT_RESOLUTION: u32 = 5; -// length used to simulate infinite lines -const INFINITE_LEN: f32 = 10_000.0; - -/// A trait for rendering 3D geometric primitives (`P`) with [`GizmoBuffer`]. -pub trait GizmoPrimitive3d { - /// The output of `primitive_3d`. This is a builder to set non-default values. - type Output<'a> - where - Self: 'a; - - /// Renders a 3D primitive with its associated details. - fn primitive_3d( - &mut self, - primitive: &P, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_>; -} - -// direction 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Dir3, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - let isometry = isometry.into(); - let start = Vec3::ZERO; - let end = primitive.as_vec3(); - self.arrow(isometry * start, isometry * end, color); - } -} - -// sphere - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = SphereBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Sphere, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - self.sphere(isometry, primitive.radius, color) - } -} - -// plane 3d - -/// Builder for configuring the drawing options of [`Plane3d`]. -pub struct Plane3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Direction of the normal orthogonal to the plane - normal: Dir3, - - isometry: Isometry3d, - // Color of the plane - color: Color, - - // Defines the amount of cells in the x and y axes - cell_count: UVec2, - // Defines the distance between cells along the x and y axes - spacing: Vec2, -} - -impl Plane3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of cells in the x and y axes direction. - pub fn cell_count(mut self, cell_count: UVec2) -> Self { - self.cell_count = cell_count; - self - } - - /// Set the distance between cells along the x and y axes. - pub fn spacing(mut self, spacing: Vec2) -> Self { - self.spacing = spacing; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Plane3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Plane3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Plane3dBuilder { - gizmos: self, - normal: primitive.normal, - isometry: isometry.into(), - color: color.into(), - cell_count: UVec2::splat(3), - spacing: Vec2::splat(1.0), - } - } -} - -impl Drop for Plane3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - self.gizmos - .primitive_3d(&self.normal, self.isometry, self.color); - // the default orientation of the grid is Z-up - let rot = Quat::from_rotation_arc(Vec3::Z, self.normal.as_vec3()); - self.gizmos.grid( - Isometry3d::new(self.isometry.translation, self.isometry.rotation * rot), - self.cell_count, - self.spacing, - self.color, - ); - } -} - -// line 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Line3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - let color = color.into(); - let direction = primitive.direction.as_vec3(); - self.arrow(isometry * Vec3::ZERO, isometry * direction, color); - - let [start, end] = [1.0, -1.0] - .map(|sign| sign * INFINITE_LEN) - .map(|length| primitive.direction * length) - .map(|offset| isometry * offset); - self.line(start, end, color); - } -} - -// segment 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Segment3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let transformed = primitive.transformed(isometry); - self.line(transformed.point1(), transformed.point2(), color); - } -} - -// polyline 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Polyline3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - self.linestrip( - primitive.vertices.iter().map(|vec3| isometry * *vec3), - color, - ); - } -} - -// triangle 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Triangle3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - let [a, b, c] = primitive.vertices; - self.lineloop([a, b, c].map(|vec3| isometry * vec3), color); - } -} - -// cuboid - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Cuboid, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - // transform the points from the reference unit cube to the cuboid coords - let vertices @ [a, b, c, d, e, f, g, h] = [ - [1.0, 1.0, 1.0], - [-1.0, 1.0, 1.0], - [-1.0, -1.0, 1.0], - [1.0, -1.0, 1.0], - [1.0, 1.0, -1.0], - [-1.0, 1.0, -1.0], - [-1.0, -1.0, -1.0], - [1.0, -1.0, -1.0], - ] - .map(Vec3::from) - .map(|vec3| vec3 * primitive.half_size) - .map(|vec3| isometry * vec3); - - // lines for the upper rectangle of the cuboid - let upper = [a, b, c, d] - .into_iter() - .zip([a, b, c, d].into_iter().cycle().skip(1)); - - // lines for the lower rectangle of the cuboid - let lower = [e, f, g, h] - .into_iter() - .zip([e, f, g, h].into_iter().cycle().skip(1)); - - // lines connecting upper and lower rectangles of the cuboid - let connections = vertices.into_iter().zip(vertices.into_iter().skip(4)); - - let color = color.into(); - upper - .chain(lower) - .chain(connections) - .for_each(|(start, end)| { - self.line(start, end, color); - }); - } -} - -// cylinder 3d - -/// Builder for configuring the drawing options of [`Cylinder`]. -pub struct Cylinder3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Radius of the cylinder - radius: f32, - // Half height of the cylinder - half_height: f32, - - isometry: Isometry3d, - // Color of the cylinder - color: Color, - - // Number of lines used to approximate the cylinder geometry - resolution: u32, -} - -impl Cylinder3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of lines used to approximate the top and bottom of the cylinder geometry. - pub fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Cylinder3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Cylinder, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Cylinder3dBuilder { - gizmos: self, - radius: primitive.radius, - half_height: primitive.half_height, - isometry: isometry.into(), - color: color.into(), - resolution: DEFAULT_RESOLUTION, - } - } -} - -impl Drop for Cylinder3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - self.gizmos - .primitive_3d( - &ConicalFrustum { - radius_top: self.radius, - radius_bottom: self.radius, - height: self.half_height * 2.0, - }, - self.isometry, - self.color, - ) - .resolution(self.resolution); - } -} - -// capsule 3d - -/// Builder for configuring the drawing options of [`Capsule3d`]. -pub struct Capsule3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Radius of the capsule - radius: f32, - // Half length of the capsule - half_length: f32, - - isometry: Isometry3d, - // Color of the capsule - color: Color, - - // Number of lines used to approximate the capsule geometry - resolution: u32, -} - -impl Capsule3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of lines used to approximate the capsule geometry. - pub fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Capsule3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Capsule3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Capsule3dBuilder { - gizmos: self, - radius: primitive.radius, - half_length: primitive.half_length, - isometry: isometry.into(), - color: color.into(), - resolution: DEFAULT_RESOLUTION, - } - } -} - -impl Drop for Capsule3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let [upper_apex, lower_apex] = [-1.0, 1.0] - .map(|sign| Vec3::Y * sign * (self.half_length + self.radius)) - .map(|vec3| self.isometry * vec3); - let [upper_center, lower_center] = [-1.0, 1.0] - .map(|sign| Vec3::Y * sign * self.half_length) - .map(|vec3| self.isometry * vec3); - let [upper_points, lower_points] = [-1.0, 1.0] - .map(|sign| Vec3::Y * sign * self.half_length) - .map(|vec3| { - circle_coordinates_closed(self.radius, self.resolution) - .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + vec3) - .map(|vec3| self.isometry * vec3) - .collect::>() - }); - - upper_points.iter().skip(1).copied().for_each(|start| { - self.gizmos - .short_arc_3d_between(upper_center, start, upper_apex, self.color); - }); - lower_points.iter().skip(1).copied().for_each(|start| { - self.gizmos - .short_arc_3d_between(lower_center, start, lower_apex, self.color); - }); - - let circle_rotation = self - .isometry - .rotation - .mul_quat(Quat::from_rotation_x(core::f32::consts::FRAC_PI_2)); - self.gizmos.circle( - Isometry3d::new(upper_center, circle_rotation), - self.radius, - self.color, - ); - self.gizmos.circle( - Isometry3d::new(lower_center, circle_rotation), - self.radius, - self.color, - ); - - let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); - connection_lines.for_each(|(start, end)| { - self.gizmos.line(start, end, self.color); - }); - } -} - -// cone 3d - -/// Builder for configuring the drawing options of [`Cone`]. -pub struct Cone3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Radius of the cone - radius: f32, - // Height of the cone - height: f32, - - isometry: Isometry3d, - // Color of the cone - color: Color, - - // Number of lines used to approximate the cone base geometry - base_resolution: u32, - - // Number of lines used to approximate the cone height geometry - height_resolution: u32, -} - -impl Cone3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of lines used to approximate the cone geometry for its base and height. - pub fn resolution(mut self, resolution: u32) -> Self { - self.base_resolution = resolution; - self.height_resolution = resolution; - self - } - - /// Set the number of lines used to approximate the height of the cone geometry. - /// - /// `resolution` should be a multiple of the value passed to [`Self::height_resolution`] - /// for the height to connect properly with the base. - pub fn base_resolution(mut self, resolution: u32) -> Self { - self.base_resolution = resolution; - self - } - - /// Set the number of lines used to approximate the height of the cone geometry. - /// - /// `resolution` should be a divisor of the value passed to [`Self::base_resolution`] - /// for the height to connect properly with the base. - pub fn height_resolution(mut self, resolution: u32) -> Self { - self.height_resolution = resolution; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Cone3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Cone, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Cone3dBuilder { - gizmos: self, - radius: primitive.radius, - height: primitive.height, - isometry: isometry.into(), - color: color.into(), - base_resolution: DEFAULT_RESOLUTION, - height_resolution: DEFAULT_RESOLUTION, - } - } -} - -impl Drop for Cone3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let half_height = self.height * 0.5; - let apex = self.isometry * (Vec3::Y * half_height); - let circle_center = half_height * Vec3::NEG_Y; - let circle_coords = circle_coordinates_closed(self.radius, self.height_resolution) - .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + circle_center) - .map(|vec3| self.isometry * vec3) - .collect::>(); - - // connections to apex - circle_coords - .iter() - .skip(1) - .map(|vec3| (*vec3, apex)) - .for_each(|(start, end)| { - self.gizmos.line(start, end, self.color); - }); - - // base circle - circle_coords.array_windows().for_each(|&[start, end]| { - self.gizmos.line(start, end, self.color); - }); - } -} - -// conical frustum 3d - -/// Builder for configuring the drawing options of [`ConicalFrustum`]. -pub struct ConicalFrustum3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Radius of the top circle - radius_top: f32, - // Radius of the bottom circle - radius_bottom: f32, - // Height of the conical frustum - height: f32, - - isometry: Isometry3d, - // Color of the conical frustum - color: Color, - - // Number of lines used to approximate the curved surfaces - resolution: u32, -} - -impl ConicalFrustum3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of lines used to approximate the curved surfaces. - pub fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = ConicalFrustum3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &ConicalFrustum, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - ConicalFrustum3dBuilder { - gizmos: self, - radius_top: primitive.radius_top, - radius_bottom: primitive.radius_bottom, - height: primitive.height, - isometry: isometry.into(), - color: color.into(), - resolution: DEFAULT_RESOLUTION, - } - } -} - -impl Drop for ConicalFrustum3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - let half_height = self.height * 0.5; - let [upper_points, lower_points] = [(-1.0, self.radius_bottom), (1.0, self.radius_top)] - .map(|(sign, radius)| { - let translation = Vec3::Y * sign * half_height; - circle_coordinates_closed(radius, self.resolution) - .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) - .map(|vec3| self.isometry * vec3) - .collect::>() - }); - - let upper_lines = upper_points.array_windows(); - let lower_lines = lower_points.array_windows(); - upper_lines.chain(lower_lines).for_each(|&[start, end]| { - self.gizmos.line(start, end, self.color); - }); - - let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); - connection_lines.for_each(|(start, end)| { - self.gizmos.line(start, end, self.color); - }); - } -} - -// torus 3d - -/// Builder for configuring the drawing options of [`Torus`]. -pub struct Torus3dBuilder<'a, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - gizmos: &'a mut GizmoBuffer, - - // Radius of the minor circle (tube) - minor_radius: f32, - // Radius of the major circle (ring) - major_radius: f32, - - isometry: Isometry3d, - // Color of the torus - color: Color, - - // Number of lines in the minor (tube) direction - minor_resolution: u32, - // Number of lines in the major (ring) direction - major_resolution: u32, -} - -impl Torus3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - /// Set the number of lines in the minor (tube) direction. - pub fn minor_resolution(mut self, minor_resolution: u32) -> Self { - self.minor_resolution = minor_resolution; - self - } - - /// Set the number of lines in the major (ring) direction. - pub fn major_resolution(mut self, major_resolution: u32) -> Self { - self.major_resolution = major_resolution; - self - } -} - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = Torus3dBuilder<'a, Config, Clear> - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Torus, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - Torus3dBuilder { - gizmos: self, - minor_radius: primitive.minor_radius, - major_radius: primitive.major_radius, - isometry: isometry.into(), - color: color.into(), - minor_resolution: DEFAULT_RESOLUTION, - major_resolution: DEFAULT_RESOLUTION, - } - } -} - -impl Drop for Torus3dBuilder<'_, Config, Clear> -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - // draw 4 circles with major_radius - let [inner, outer, top, bottom] = [ - (self.major_radius - self.minor_radius, 0.0), - (self.major_radius + self.minor_radius, 0.0), - (self.major_radius, self.minor_radius), - (self.major_radius, -self.minor_radius), - ] - .map(|(radius, height)| { - let translation = height * Vec3::Y; - circle_coordinates_closed(radius, self.major_resolution) - .map(|vec2| Vec3::new(vec2.x, 0.0, vec2.y) + translation) - .map(|vec3| self.isometry * vec3) - .collect::>() - }); - - [&inner, &outer, &top, &bottom] - .iter() - .flat_map(|points| points.array_windows()) - .for_each(|&[start, end]| { - self.gizmos.line(start, end, self.color); - }); - - inner - .into_iter() - .zip(top) - .zip(outer) - .zip(bottom) - .flat_map(|(((inner, top), outer), bottom)| { - let center = (inner + top + outer + bottom) * 0.25; - [(inner, top), (top, outer), (outer, bottom), (bottom, inner)] - .map(|(start, end)| (start, end, center)) - }) - .for_each(|(from, to, center)| { - self.gizmos - .short_arc_3d_between(center, from, to, self.color) - .resolution(self.minor_resolution); - }); - } -} - -// tetrahedron - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &Tetrahedron, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let [a, b, c, d] = primitive.vertices.map(|vec3| isometry * vec3); - - let lines = [(a, b), (a, c), (a, d), (b, c), (b, d), (c, d)]; - - let color = color.into(); - lines.into_iter().for_each(|(start, end)| { - self.line(start, end, color); - }); - } -} diff --git a/crates/bevy_gizmos/src/primitives/helpers.rs b/crates/bevy_gizmos/src/primitives/helpers.rs deleted file mode 100644 index 37253b14a9ac9..0000000000000 --- a/crates/bevy_gizmos/src/primitives/helpers.rs +++ /dev/null @@ -1,43 +0,0 @@ -use core::f32::consts::TAU; - -use bevy_math::{ops, Vec2}; - -/// Calculates the `nth` coordinate of a circle. -/// -/// Given a circle's radius and its resolution, this function computes the position -/// of the `nth` point along the circumference of the circle. The rotation starts at `(0.0, radius)` -/// and proceeds counter-clockwise. -pub(crate) fn single_circle_coordinate(radius: f32, resolution: u32, nth_point: u32) -> Vec2 { - let angle = nth_point as f32 * TAU / resolution as f32; - let (x, y) = ops::sin_cos(angle); - Vec2::new(x, y) * radius -} - -/// Generates an iterator over the coordinates of a circle. -/// -/// The coordinates form an open circle, meaning the first and last points aren't the same. -/// -/// This function creates an iterator that yields the positions of points approximating a -/// circle with the given radius, divided into linear segments. The iterator produces `resolution` -/// number of points. -pub(crate) fn circle_coordinates(radius: f32, resolution: u32) -> impl Iterator { - (0..) - .map(move |p| single_circle_coordinate(radius, resolution, p)) - .take(resolution as usize) -} - -/// Generates an iterator over the coordinates of a circle. -/// -/// The coordinates form a closed circle, meaning the first and last points are the same. -/// -/// This function creates an iterator that yields the positions of points approximating a -/// circle with the given radius, divided into linear segments. The iterator produces `resolution` -/// number of points. -pub(crate) fn circle_coordinates_closed( - radius: f32, - resolution: u32, -) -> impl Iterator { - circle_coordinates(radius, resolution).chain(core::iter::once(single_circle_coordinate( - radius, resolution, resolution, - ))) -} diff --git a/crates/bevy_gizmos/src/primitives/mod.rs b/crates/bevy_gizmos/src/primitives/mod.rs deleted file mode 100644 index 419ef50217450..0000000000000 --- a/crates/bevy_gizmos/src/primitives/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`Gizmos`](`crate::prelude::Gizmos`). - -pub mod dim2; -pub mod dim3; -pub(crate) mod helpers; diff --git a/crates/bevy_light/src/cluster/assign.rs b/crates/bevy_light/src/cluster/assign.rs index 8be51681ac313..cdb2f70490910 100644 --- a/crates/bevy_light/src/cluster/assign.rs +++ b/crates/bevy_light/src/cluster/assign.rs @@ -11,8 +11,8 @@ use bevy_ecs::{ system::{Local, Query, Res}, }; use bevy_math::{ + camera::HalfSpace, ops::{self, sin_cos}, - primitives::HalfSpace, Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _, }; use bevy_transform::components::GlobalTransform; diff --git a/crates/bevy_light/src/directional_light.rs b/crates/bevy_light/src/directional_light.rs index e65a1e1b2024e..f703bdde515f3 100644 --- a/crates/bevy_light/src/directional_light.rs +++ b/crates/bevy_light/src/directional_light.rs @@ -7,7 +7,7 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::primitives::ViewFrustum; +use bevy_math::camera::ViewFrustum; use bevy_reflect::prelude::*; use bevy_transform::components::Transform; use tracing::warn; diff --git a/crates/bevy_light/src/point_light.rs b/crates/bevy_light/src/point_light.rs index 940e124f0cf04..7e230f7fe8879 100644 --- a/crates/bevy_light/src/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -6,7 +6,7 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::{primitives::ViewFrustum, proj}; +use bevy_math::{camera::ViewFrustum, proj}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index 0b23653928c02..9d0d04c83df88 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -6,7 +6,7 @@ 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::{camera::ViewFrustum, proj, Affine3A, Dir3, Mat3, Mat4, Vec3}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 136a90f64669d..6d88efae728dd 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -1,5 +1,3 @@ -mod primitive_impls; - use super::{BoundingVolume, IntersectsVolume}; use crate::{ ops, diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs deleted file mode 100644 index 8e45e645bfeb5..0000000000000 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ /dev/null @@ -1,1021 +0,0 @@ -//! 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 core::f32::consts::{FRAC_PI_2, PI, TAU}; - -#[cfg(feature = "alloc")] -use crate::primitives::{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(); - Aabb2d::new(isometry.translation, Vec2::splat(self.radius)) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.radius) - } -} - -// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes. -// The return type has room for 7 points so that the CircularSector code can add an additional point. -#[inline] -fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> ArrayVec { - // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. - // We need to compute which axis-aligned extrema are actually contained within the rotated arc. - let mut bounds = ArrayVec::::new(); - let rotation = rotation.into(); - bounds.push(rotation * arc.left_endpoint()); - bounds.push(rotation * arc.right_endpoint()); - - // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. - // Compute the normalized angles of the endpoints with the rotation taken into account, and then - // check if we are looking for an angle that is between or outside them. - let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU); - let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU); - let inverted = left_angle < right_angle; - for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { - let angle = ops::rem_euclid(extremum.to_angle(), TAU); - // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. - // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. - // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. - let angle_within_parameters = if inverted { - angle >= right_angle || angle <= left_angle - } else { - angle >= right_angle && angle <= left_angle - }; - if angle_within_parameters { - bounds.push(extremum * arc.radius); - } - } - bounds -} - -impl Bounded2d for Arc2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - // If our arc covers more than a circle, just return the bounding box of the circle. - if self.half_angle >= PI { - return Circle::new(self.radius).aabb_2d(isometry); - } - - let isometry = isometry.into(); - - Aabb2d::from_point_cloud( - Isometry2d::from_translation(isometry.translation), - &arc_bounding_points(*self, isometry.rotation), - ) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - - // There are two possibilities for the bounding circle. - if self.is_major() { - // If the arc is major, then the widest distance between two points is a diameter of the arc's circle; - // therefore, that circle is the bounding radius. - BoundingCircle::new(isometry.translation, self.radius) - } else { - // Otherwise, the widest distance between two points is the chord, - // so a circle of that diameter around the midpoint will contain the entire arc. - let center = isometry.rotation * self.chord_midpoint(); - BoundingCircle::new(center + isometry.translation, self.half_chord_length()) - } - } -} - -impl Bounded2d for CircularSector { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - // If our sector covers more than a circle, just return the bounding box of the circle. - if self.half_angle() >= PI { - return Circle::new(self.radius()).aabb_2d(isometry); - } - - // Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility. - let mut bounds = arc_bounding_points(self.arc, isometry.rotation); - bounds.push(Vec2::ZERO); - - Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - if self.arc.is_major() { - let isometry = isometry.into(); - - // If the arc is major, that is, greater than a semicircle, - // then bounding circle is just the circle defining the sector. - BoundingCircle::new(isometry.translation, self.arc.radius) - } else { - // However, when the arc is minor, - // we need our bounding circle to include both endpoints of the arc as well as the circle center. - // This means we need the circumcircle of those three points. - // The circumcircle will always have a greater curvature than the circle itself, so it will contain - // the entire circular sector. - Triangle2d::new( - Vec2::ZERO, - self.arc.left_endpoint(), - self.arc.right_endpoint(), - ) - .bounding_circle(isometry) - } - } -} - -impl Bounded2d for CircularSegment { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - self.arc.aabb_2d(isometry) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - self.arc.bounding_circle(isometry) - } -} - -impl Bounded2d for Ellipse { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - // V = (hh * cos(beta), hh * sin(beta)) - // #####*##### - // ### | ### - // # hh | # - // # *---------* U = (hw * cos(alpha), hw * sin(alpha)) - // # hw # - // ### ### - // ########### - - let (hw, hh) = (self.half_size.x, self.half_size.y); - - // Sine and cosine of rotation angle alpha. - let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos(); - - // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions: - // sin(beta) = sin(alpha + pi/2) = cos(alpha) - // cos(beta) = cos(alpha + pi/2) = -sin(alpha) - let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin); - - // Compute points U and V, the extremes of the ellipse - let (ux, uy) = (hw * alpha_cos, hw * alpha_sin); - let (vx, vy) = (hh * beta_cos, hh * beta_sin); - - let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy)); - - Aabb2d::new(isometry.translation, half_size) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.semi_major()) - } -} - -impl Bounded2d for Annulus { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius)) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.outer_circle.radius) - } -} - -impl Bounded2d for Rhombus { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [ - isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0), - isometry.rotation * Vec2::new(0.0, self.half_diagonals.y), - ]; - let aabb_half_extent = rotated_x_half_diagonal - .abs() - .max(rotated_y_half_diagonal.abs()); - - Aabb2d { - min: -aabb_half_extent + isometry.translation, - max: aabb_half_extent + isometry.translation, - } - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.circumradius()) - } -} - -impl Bounded2d for Plane2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - let normal = isometry.rotation * *self.normal; - let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; - let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; - - // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations - // like growing or shrinking the AABB without breaking things. - let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; - let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; - let half_size = Vec2::new(half_width, half_height); - - Aabb2d::new(isometry.translation, half_size) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, f32::MAX / 2.0) - } -} - -impl Bounded2d for Line2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - let direction = isometry.rotation * *self.direction; - - // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations - // like growing or shrinking the AABB without breaking things. - let max = f32::MAX / 2.0; - let half_width = if direction.x == 0.0 { 0.0 } else { max }; - let half_height = if direction.y == 0.0 { 0.0 } else { max }; - let half_size = Vec2::new(half_width, half_height); - - Aabb2d::new(isometry.translation, half_size) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, f32::MAX / 2.0) - } -} - -impl Bounded2d for Segment2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()]) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry: Isometry2d = isometry.into(); - let local_center = self.center(); - let radius = local_center.distance(self.point1()); - let local_circle = BoundingCircle::new(local_center, radius); - local_circle.transformed_by(isometry.translation, isometry.rotation) - } -} - -#[cfg(feature = "alloc")] -impl Bounded2d for Polyline2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, &self.vertices) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, &self.vertices) - } -} - -impl Bounded2d for Triangle2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx); - - let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y)); - let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y)); - - Aabb2d { - min: min + isometry.translation, - max: max + isometry.translation, - } - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - let [a, b, c] = self.vertices; - - // The points of the segment opposite to the obtuse or right angle if one exists - let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { - Some((b, c)) - } else if (c - b).dot(a - b) <= 0.0 { - Some((c, a)) - } else if (a - c).dot(b - c) <= 0.0 { - Some((a, b)) - } else { - // The triangle is acute. - None - }; - - // Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices. - // Otherwise, it's the circumcircle and passes through all three. - if let Some((point1, point2)) = side_opposite_to_non_acute { - // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. - // We can compute the minimum bounding circle from the line segment of the longest side. - let segment = Segment2d::new(point1, point2); - segment.bounding_circle(isometry) - } else { - // The triangle is acute, so the smallest bounding circle is the circumcircle. - let (Circle { radius }, circumcenter) = self.circumcircle(); - BoundingCircle::new(isometry * circumcenter, radius) - } - } -} - -impl Bounded2d for Rectangle { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - // Compute the AABB of the rotated rectangle by transforming the half-extents - // by an absolute rotation matrix. - let (sin, cos) = isometry.rotation.sin_cos(); - let abs_rot_mat = - Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]); - let half_size = abs_rot_mat * self.half_size; - - Aabb2d::new(isometry.translation, half_size) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - let radius = self.half_size.length(); - BoundingCircle::new(isometry.translation, radius) - } -} - -#[cfg(feature = "alloc")] -impl Bounded2d for Polygon { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, &self.vertices) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, &self.vertices) - } -} - -#[cfg(feature = "alloc")] -impl Bounded2d for ConvexPolygon { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, self.vertices()) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, self.vertices()) - } -} - -impl Bounded2d for RegularPolygon { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - let mut min = Vec2::ZERO; - let mut max = Vec2::ZERO; - - for vertex in self.vertices(isometry.rotation.as_radians()) { - min = min.min(vertex); - max = max.max(vertex); - } - - Aabb2d { - min: min + isometry.translation, - max: max + isometry.translation, - } - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.circumcircle.radius) - } -} - -impl Bounded2d for Capsule2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - let isometry = isometry.into(); - - // Get the line segment between the semicircles of the rotated capsule - let segment = Segment2d::from_direction_and_length( - isometry.rotation * Dir2::Y, - self.half_length * 2., - ); - let (a, b) = (segment.point1(), segment.point2()); - - // Expand the line segment by the capsule radius to get the capsule half-extents - let min = a.min(b) - Vec2::splat(self.radius); - let max = a.max(b) + Vec2::splat(self.radius); - - Aabb2d { - min: min + isometry.translation, - max: max + isometry.translation, - } - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - let isometry = isometry.into(); - BoundingCircle::new(isometry.translation, self.radius + self.half_length) - } -} - -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 { - use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU}; - use std::println; - - use approx::assert_abs_diff_eq; - use glam::Vec2; - - use crate::{ - bounding::Bounded2d, - ops::{self, FloatPow}, - primitives::{ - Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, - Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, - Triangle2d, - }, - Dir2, Isometry2d, Rot2, - }; - - #[test] - fn circle() { - let circle = Circle { radius: 1.0 }; - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = circle.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); - - let bounding_circle = circle.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0); - } - - #[test] - // Arcs and circular segments have the same bounding shapes so they share test cases. - fn arc_and_segment() { - struct TestCase { - name: &'static str, - arc: Arc2d, - translation: Vec2, - rotation: f32, - aabb_min: Vec2, - aabb_max: Vec2, - bounding_circle_center: Vec2, - bounding_circle_radius: f32, - } - - impl TestCase { - fn isometry(&self) -> Isometry2d { - Isometry2d::new(self.translation, self.rotation.into()) - } - } - - // The apothem of an arc covering 1/6th of a circle. - let apothem = ops::sqrt(3.0) / 2.0; - let tests = [ - // Test case: a basic minor arc - TestCase { - name: "1/6th circle untransformed", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-0.5, apothem), - aabb_max: Vec2::new(0.5, 1.0), - bounding_circle_center: Vec2::new(0.0, apothem), - bounding_circle_radius: 0.5, - }, - // Test case: a smaller arc, verifying that radius scaling works - TestCase { - name: "1/6th circle with radius 0.5", - arc: Arc2d::from_radians(0.5, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-0.25, apothem / 2.0), - aabb_max: Vec2::new(0.25, 0.5), - bounding_circle_center: Vec2::new(0.0, apothem / 2.0), - bounding_circle_radius: 0.25, - }, - // Test case: a larger arc, verifying that radius scaling works - TestCase { - name: "1/6th circle with radius 2.0", - arc: Arc2d::from_radians(2.0, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-1.0, 2.0 * apothem), - aabb_max: Vec2::new(1.0, 2.0), - bounding_circle_center: Vec2::new(0.0, 2.0 * apothem), - bounding_circle_radius: 1.0, - }, - // Test case: translation of a minor arc - TestCase { - name: "1/6th circle translated", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::new(2.0, 3.0), - rotation: 0.0, - aabb_min: Vec2::new(1.5, 3.0 + apothem), - aabb_max: Vec2::new(2.5, 4.0), - bounding_circle_center: Vec2::new(2.0, 3.0 + apothem), - bounding_circle_radius: 0.5, - }, - // Test case: rotation of a minor arc - TestCase { - name: "1/6th circle rotated", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::ZERO, - // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. - rotation: FRAC_PI_6, - aabb_min: Vec2::new(-apothem, 0.5), - aabb_max: Vec2::new(0.0, 1.0), - // The exact coordinates here are not obvious, but can be computed by constructing - // an altitude from the midpoint of the chord to the y-axis and using the right triangle - // similarity theorem. - bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()), - bounding_circle_radius: 0.5, - }, - // Test case: handling of axis-aligned extrema - TestCase { - name: "1/4er circle rotated to be axis-aligned", - arc: Arc2d::from_radians(1.0, FRAC_PI_2), - translation: Vec2::ZERO, - // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. - rotation: -FRAC_PI_4, - aabb_min: Vec2::ZERO, - aabb_max: Vec2::splat(1.0), - bounding_circle_center: Vec2::splat(0.5), - bounding_circle_radius: ops::sqrt(2.0) / 2.0, - }, - // Test case: a basic major arc - TestCase { - name: "5/6th circle untransformed", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-1.0, -apothem), - aabb_max: Vec2::new(1.0, 1.0), - bounding_circle_center: Vec2::ZERO, - bounding_circle_radius: 1.0, - }, - // Test case: a translated major arc - TestCase { - name: "5/6th circle translated", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::new(2.0, 3.0), - rotation: 0.0, - aabb_min: Vec2::new(1.0, 3.0 - apothem), - aabb_max: Vec2::new(3.0, 4.0), - bounding_circle_center: Vec2::new(2.0, 3.0), - bounding_circle_radius: 1.0, - }, - // Test case: a rotated major arc, with inverted left/right angles - TestCase { - name: "5/6th circle rotated", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::ZERO, - // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. - rotation: FRAC_PI_6, - aabb_min: Vec2::new(-1.0, -1.0), - aabb_max: Vec2::new(1.0, 1.0), - bounding_circle_center: Vec2::ZERO, - bounding_circle_radius: 1.0, - }, - ]; - - for test in tests { - #[cfg(feature = "std")] - println!("subtest case: {}", test.name); - let segment: CircularSegment = test.arc.into(); - - let arc_aabb = test.arc.aabb_2d(test.isometry()); - assert_abs_diff_eq!(test.aabb_min, arc_aabb.min); - assert_abs_diff_eq!(test.aabb_max, arc_aabb.max); - let segment_aabb = segment.aabb_2d(test.isometry()); - assert_abs_diff_eq!(test.aabb_min, segment_aabb.min); - assert_abs_diff_eq!(test.aabb_max, segment_aabb.max); - - let arc_bounding_circle = test.arc.bounding_circle(test.isometry()); - assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center); - assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius()); - let segment_bounding_circle = segment.bounding_circle(test.isometry()); - assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center); - assert_abs_diff_eq!( - test.bounding_circle_radius, - segment_bounding_circle.radius() - ); - } - } - - #[test] - fn circular_sector() { - struct TestCase { - name: &'static str, - arc: Arc2d, - translation: Vec2, - rotation: f32, - aabb_min: Vec2, - aabb_max: Vec2, - bounding_circle_center: Vec2, - bounding_circle_radius: f32, - } - - impl TestCase { - fn isometry(&self) -> Isometry2d { - Isometry2d::new(self.translation, self.rotation.into()) - } - } - - // The apothem of an arc covering 1/6th of a circle. - let apothem = ops::sqrt(3.0) / 2.0; - let inv_sqrt_3 = ops::sqrt(3.0).recip(); - let tests = [ - // Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center - TestCase { - name: "1/3rd circle", - arc: Arc2d::from_radians(1.0, TAU / 3.0), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-apothem, 0.0), - aabb_max: Vec2::new(apothem, 1.0), - bounding_circle_center: Vec2::new(0.0, 0.5), - bounding_circle_radius: apothem, - }, - // The remaining test cases are selected as for arc_and_segment. - TestCase { - name: "1/6th circle untransformed", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-0.5, 0.0), - aabb_max: Vec2::new(0.5, 1.0), - // The bounding circle is a circumcircle of an equilateral triangle with side length 1. - // The distance from the corner to the center of such a triangle is 1/sqrt(3). - bounding_circle_center: Vec2::new(0.0, inv_sqrt_3), - bounding_circle_radius: inv_sqrt_3, - }, - TestCase { - name: "1/6th circle with radius 0.5", - arc: Arc2d::from_radians(0.5, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-0.25, 0.0), - aabb_max: Vec2::new(0.25, 0.5), - bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0), - bounding_circle_radius: inv_sqrt_3 / 2.0, - }, - TestCase { - name: "1/6th circle with radius 2.0", - arc: Arc2d::from_radians(2.0, FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-1.0, 0.0), - aabb_max: Vec2::new(1.0, 2.0), - bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3), - bounding_circle_radius: 2.0 * inv_sqrt_3, - }, - TestCase { - name: "1/6th circle translated", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::new(2.0, 3.0), - rotation: 0.0, - aabb_min: Vec2::new(1.5, 3.0), - aabb_max: Vec2::new(2.5, 4.0), - bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3), - bounding_circle_radius: inv_sqrt_3, - }, - TestCase { - name: "1/6th circle rotated", - arc: Arc2d::from_radians(1.0, FRAC_PI_3), - translation: Vec2::ZERO, - // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. - rotation: FRAC_PI_6, - aabb_min: Vec2::new(-apothem, 0.0), - aabb_max: Vec2::new(0.0, 1.0), - // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2. - bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5), - bounding_circle_radius: inv_sqrt_3, - }, - TestCase { - name: "1/4er circle rotated to be axis-aligned", - arc: Arc2d::from_radians(1.0, FRAC_PI_2), - translation: Vec2::ZERO, - // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. - rotation: -FRAC_PI_4, - aabb_min: Vec2::ZERO, - aabb_max: Vec2::splat(1.0), - bounding_circle_center: Vec2::splat(0.5), - bounding_circle_radius: ops::sqrt(2.0) / 2.0, - }, - TestCase { - name: "5/6th circle untransformed", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::ZERO, - rotation: 0.0, - aabb_min: Vec2::new(-1.0, -apothem), - aabb_max: Vec2::new(1.0, 1.0), - bounding_circle_center: Vec2::ZERO, - bounding_circle_radius: 1.0, - }, - TestCase { - name: "5/6th circle translated", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::new(2.0, 3.0), - rotation: 0.0, - aabb_min: Vec2::new(1.0, 3.0 - apothem), - aabb_max: Vec2::new(3.0, 4.0), - bounding_circle_center: Vec2::new(2.0, 3.0), - bounding_circle_radius: 1.0, - }, - TestCase { - name: "5/6th circle rotated", - arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), - translation: Vec2::ZERO, - // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. - rotation: FRAC_PI_6, - aabb_min: Vec2::new(-1.0, -1.0), - aabb_max: Vec2::new(1.0, 1.0), - bounding_circle_center: Vec2::ZERO, - bounding_circle_radius: 1.0, - }, - ]; - - for test in tests { - #[cfg(feature = "std")] - println!("subtest case: {}", test.name); - let sector: CircularSector = test.arc.into(); - - let aabb = sector.aabb_2d(test.isometry()); - assert_abs_diff_eq!(test.aabb_min, aabb.min); - assert_abs_diff_eq!(test.aabb_max, aabb.max); - - let bounding_circle = sector.bounding_circle(test.isometry()); - assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center); - assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius()); - } - } - - #[test] - fn ellipse() { - let ellipse = Ellipse::new(1.0, 0.5); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = ellipse.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); - assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); - - let bounding_circle = ellipse.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0); - } - - #[test] - fn annulus() { - let annulus = Annulus::new(1.0, 2.0); - let translation = Vec2::new(2.0, 1.0); - let rotation = Rot2::radians(1.0); - let isometry = Isometry2d::new(translation, rotation); - - let aabb = annulus.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(0.0, -1.0)); - assert_eq!(aabb.max, Vec2::new(4.0, 3.0)); - - let bounding_circle = annulus.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 2.0); - } - - #[test] - fn rhombus() { - let rhombus = Rhombus::new(2.0, 1.0); - let translation = Vec2::new(2.0, 1.0); - let rotation = Rot2::radians(FRAC_PI_4); - let isometry = Isometry2d::new(translation, rotation); - - let aabb = rhombus.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323)); - assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068)); - - let bounding_circle = rhombus.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0); - - let rhombus = Rhombus::new(0.0, 0.0); - let translation = Vec2::new(0.0, 0.0); - let isometry = Isometry2d::new(translation, rotation); - - let aabb = rhombus.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(0.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(0.0, 0.0)); - - let bounding_circle = rhombus.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 0.0); - } - - #[test] - fn plane() { - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry); - assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); - assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); - - let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry); - assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); - assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); - - let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry); - assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); - assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); - - let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); - } - - #[test] - fn line() { - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry); - assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); - assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); - - let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry); - assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); - assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); - - let aabb3 = Line2d { - direction: Dir2::from_xy(1.0, 1.0).unwrap(), - } - .aabb_2d(isometry); - assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); - assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); - - let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); - } - - #[test] - fn segment() { - let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = segment.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); - assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); - - let bounding_circle = segment.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); - } - - #[test] - fn polyline() { - let polyline = Polyline2d::new([ - Vec2::ONE, - Vec2::new(-1.0, 1.0), - Vec2::NEG_ONE, - Vec2::new(1.0, -1.0), - ]); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = polyline.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); - - let bounding_circle = polyline.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2); - } - - #[test] - fn acute_triangle() { - let acute_triangle = - Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0)); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = acute_triangle.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); - - // For acute triangles, the center is the circumcenter - let (Circle { radius }, circumcenter) = acute_triangle.circumcircle(); - let bounding_circle = acute_triangle.bounding_circle(isometry); - assert_eq!(bounding_circle.center, circumcenter + translation); - assert_eq!(bounding_circle.radius(), radius); - } - - #[test] - fn obtuse_triangle() { - let obtuse_triangle = Triangle2d::new( - Vec2::new(0.0, 1.0), - Vec2::new(-10.0, -1.0), - Vec2::new(10.0, -1.0), - ); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = obtuse_triangle.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(-8.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(12.0, 2.0)); - - // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle) - let bounding_circle = obtuse_triangle.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation - Vec2::Y); - assert_eq!(bounding_circle.radius(), 10.0); - } - - #[test] - fn rectangle() { - let rectangle = Rectangle::new(2.0, 1.0); - let translation = Vec2::new(2.0, 1.0); - - let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4))); - let expected_half_size = Vec2::splat(1.0606601); - assert_eq!(aabb.min, translation - expected_half_size); - assert_eq!(aabb.max, translation + expected_half_size); - - let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation)); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); - } - - #[test] - fn polygon() { - let polygon = Polygon::new([ - Vec2::ONE, - Vec2::new(-1.0, 1.0), - Vec2::NEG_ONE, - Vec2::new(1.0, -1.0), - ]); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = polygon.aabb_2d(isometry); - assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); - assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); - - let bounding_circle = polygon.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2); - } - - #[test] - fn regular_polygon() { - let regular_polygon = RegularPolygon::new(1.0, 5); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = regular_polygon.aabb_2d(isometry); - assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6); - assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6); - - let bounding_circle = regular_polygon.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0); - } - - #[test] - fn capsule() { - let capsule = Capsule2d::new(0.5, 2.0); - let translation = Vec2::new(2.0, 1.0); - let isometry = Isometry2d::from_translation(translation); - - let aabb = capsule.aabb_2d(isometry); - assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5)); - assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5)); - - let bounding_circle = capsule.bounding_circle(isometry); - assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.5); - } -} diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs deleted file mode 100644 index 58f7dde082a6c..0000000000000 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ /dev/null @@ -1,463 +0,0 @@ -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, -}; - -#[cfg(feature = "alloc")] -use crate::primitives::{Polygon, Polyline2d}; - -use crate::{bounding::Bounded2d, primitives::Circle}; - -use super::{Aabb3d, Bounded3d, BoundingSphere}; - -impl BoundedExtrusion for Circle { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - // Reference: http://iquilezles.org/articles/diskbbox/ - - let isometry = isometry.into(); - - let segment_dir = isometry.rotation * Vec3A::Z; - let top = (segment_dir * half_depth).abs(); - - let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); - - Aabb3d { - min: isometry.translation - half_size - top, - max: isometry.translation + half_size + top, - } - } -} - -impl BoundedExtrusion for Ellipse { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let Vec2 { x: a, y: b } = self.half_size; - let normal = isometry.rotation * Vec3A::Z; - let conjugate_rot = isometry.rotation.conjugate(); - - let [max_x, max_y, max_z] = Vec3A::AXES.map(|axis| { - let Some(axis) = (conjugate_rot * axis.reject_from(normal)) - .xy() - .try_normalize() - else { - return Vec3A::ZERO; - }; - - if axis.element_product() == 0. { - return isometry.rotation * Vec3A::new(a * axis.y, b * axis.x, 0.); - } - let m = -axis.x / axis.y; - let signum = axis.signum(); - - let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a); - let x = signum.x * a * ops::sqrt(1. - y * y / b / b); - isometry.rotation * Vec3A::new(x, y, 0.) - }); - - let half_size = Vec3A::new(max_x.x, max_y.y, max_z.z).abs() + (normal * half_depth).abs(); - Aabb3d::new(isometry.translation, half_size) - } -} - -impl BoundedExtrusion for Line2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let dir = isometry.rotation * Vec3A::from(self.direction.extend(0.)); - let half_depth = (isometry.rotation * Vec3A::new(0., 0., half_depth)).abs(); - - let max = f32::MAX / 2.; - let half_size = Vec3A::new( - if dir.x == 0. { half_depth.x } else { max }, - if dir.y == 0. { half_depth.y } else { max }, - if dir.z == 0. { half_depth.z } else { max }, - ); - - Aabb3d::new(isometry.translation, half_size) - } -} - -impl BoundedExtrusion for Segment2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let half_size = isometry.rotation * Vec3A::from(self.point1().extend(0.)); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - Aabb3d::new(isometry.translation, half_size.abs() + depth.abs()) - } -} - -#[cfg(feature = "alloc")] -impl BoundedExtrusion for Polyline2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - -impl BoundedExtrusion for Triangle2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - -impl BoundedExtrusion for Rectangle { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - Cuboid { - half_size: self.half_size.extend(half_depth), - } - .aabb_3d(isometry) - } -} - -#[cfg(feature = "alloc")] -impl BoundedExtrusion for Polygon { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - -impl BoundedExtrusion for RegularPolygon { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = Aabb3d::from_point_cloud( - isometry, - self.vertices(0.).into_iter().map(|v| v.extend(0.)), - ); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - -impl BoundedExtrusion for Capsule2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = Cylinder { - half_height: half_depth, - radius: self.radius, - } - .aabb_3d(isometry.rotation * Quat::from_rotation_x(FRAC_PI_2)); - - let up = isometry.rotation * Vec3A::new(0., self.half_length, 0.); - let half_size = aabb.max + up.abs(); - Aabb3d::new(isometry.translation, half_size) - } -} - -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) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - self.base_shape - .extrusion_bounding_sphere(self.half_depth, isometry) - } -} - -/// A trait implemented on 2D shapes which determines the 3D bounding volumes of their extrusions. -/// -/// Since default implementations can be inferred from 2D bounding volumes, this allows a `Bounded2d` -/// implementation on some shape `MyShape` to be extrapolated to a `Bounded3d` implementation on -/// `Extrusion` without supplying any additional data; e.g.: -/// `impl BoundedExtrusion for MyShape {}` -pub trait BoundedExtrusion: Primitive2d + Bounded2d { - /// Get an axis-aligned bounding box for an extrusion with this shape as a base and the given `half_depth`, transformed by the given `translation` and `rotation`. - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let cap_normal = isometry.rotation * Vec3A::Z; - let conjugate_rot = isometry.rotation.conjugate(); - - // The `(halfsize, offset)` for each axis - let axis_values = Vec3A::AXES.map(|ax| { - // This is the direction of the line of intersection of a plane with the `ax` normal and the plane containing the cap of the extrusion. - let intersect_line = ax.cross(cap_normal); - if intersect_line.length_squared() <= f32::EPSILON { - return (0., 0.); - }; - - // This is the normal vector of the intersection line rotated to be in the XY-plane - let line_normal = (conjugate_rot * intersect_line).yx(); - let angle = line_normal.to_angle(); - - // Since the plane containing the caps of the extrusion is not guaranteed to be orthogonal to the `ax` plane, only a certain "scale" factor - // of the `Aabb2d` will actually go towards the dimensions of the `Aabb3d` - let scale = cap_normal.reject_from(ax).length(); - - // Calculate the `Aabb2d` of the base shape. The shape is rotated so that the line of intersection is parallel to the Y axis in the `Aabb2d` calculations. - // This guarantees that the X value of the `Aabb2d` is closest to the `ax` plane - let aabb2d = self.aabb_2d(Rot2::radians(angle)); - (aabb2d.half_size().x * scale, aabb2d.center().x * scale) - }); - - let offset = Vec3A::from_array(axis_values.map(|(_, offset)| offset)); - let cap_size = Vec3A::from_array(axis_values.map(|(max_val, _)| max_val)).abs(); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - Aabb3d::new(isometry.translation - offset, cap_size + depth.abs()) - } - - /// Get a bounding sphere for an extrusion of the `base_shape` with the given `half_depth` with the given translation and rotation - fn extrusion_bounding_sphere( - &self, - half_depth: f32, - isometry: impl Into, - ) -> BoundingSphere { - let isometry = isometry.into(); - - // We calculate the bounding circle of the base shape. - // Since each of the extrusions bases will have the same distance from its center, - // and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere - // of the cylinder defined by the two bounding circles of the bases for any base shape - let BoundingCircle { center, radius } = self.bounding_circle(Isometry2d::IDENTITY); - let radius = ops::hypot(radius, half_depth); - let center = isometry * Vec3A::from(center.extend(0.)); - - BoundingSphere::new(center, radius) - } -} - -#[cfg(test)] -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, - }; - - #[test] - fn circle() { - let cylinder = Extrusion::new(Circle::new(0.5), 2.0); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = cylinder.aabb_3d(translation); - assert_eq!(aabb.center(), Vec3A::from(translation)); - assert_eq!(aabb.half_size(), Vec3A::new(0.5, 0.5, 1.0)); - - let bounding_sphere = cylinder.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); - } - - #[test] - fn ellipse() { - let extrusion = Extrusion::new(Ellipse::new(2.0, 0.5), 4.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_euler(EulerRot::ZYX, FRAC_PI_4, FRAC_PI_4, FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), Vec3A::from(translation)); - assert_eq!(aabb.half_size(), Vec3A::new(2.709784, 1.3801551, 2.436141)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); - } - - #[test] - fn line() { - let extrusion = Extrusion::new( - Line2d { - direction: Dir2::new_unchecked(Vec2::Y), - }, - 4., - ); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_y(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.min, Vec3A::new(1.5857864, f32::MIN / 2., 3.5857865)); - assert_eq!(aabb.max, Vec3A::new(4.4142136, f32::MAX / 2., 6.414213)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center(), translation.into()); - assert_eq!(bounding_sphere.radius(), f32::MAX / 2.); - } - - #[test] - fn rectangle() { - let extrusion = Extrusion::new(Rectangle::new(2.0, 1.0), 4.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_z(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(1.0606602, 1.0606602, 2.)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 2.291288); - } - - #[test] - fn segment() { - let extrusion = Extrusion::new( - Segment2d::new(Vec2::new(0.0, -1.5), Vec2::new(0.0, 1.5)), - 4.0, - ); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(0., 2.4748735, 2.4748735)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 2.5); - } - - #[test] - fn polyline() { - let polyline = Polyline2d::new([ - Vec2::ONE, - Vec2::new(-1.0, 1.0), - Vec2::NEG_ONE, - Vec2::new(1.0, -1.0), - ]); - let extrusion = Extrusion::new(polyline, 3.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 2.0615528); - } - - #[test] - fn triangle() { - let triangle = Triangle2d::new( - Vec2::new(0.0, 1.0), - Vec2::new(-10.0, -1.0), - Vec2::new(10.0, -1.0), - ); - let extrusion = Extrusion::new(triangle, 3.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(10., 1.7677668, 1.7677668)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!( - bounding_sphere.center, - Vec3A::new(3.0, 3.2928934, 4.2928934) - ); - assert_eq!(bounding_sphere.radius(), 10.111875); - } - - #[test] - fn polygon() { - let polygon = Polygon::new([ - Vec2::ONE, - Vec2::new(-1.0, 1.0), - Vec2::NEG_ONE, - Vec2::new(1.0, -1.0), - ]); - let extrusion = Extrusion::new(polygon, 3.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 2.0615528); - } - - #[test] - fn regular_polygon() { - let extrusion = Extrusion::new(RegularPolygon::new(2.0, 7), 4.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!( - aabb.center(), - Vec3A::from(translation) + Vec3A::new(0., 0.0700254, 0.0700254) - ); - assert_eq!( - aabb.half_size(), - Vec3A::new(1.9498558, 2.7584014, 2.7584019) - ); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32)); - } - - #[test] - fn capsule() { - let extrusion = Extrusion::new(Capsule2d::new(0.5, 2.0), 4.0); - let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_x(FRAC_PI_4); - let isometry = Isometry3d::new(translation, rotation); - - let aabb = extrusion.aabb_3d(isometry); - assert_eq!(aabb.center(), translation.into()); - assert_eq!(aabb.half_size(), Vec3A::new(0.5, 2.4748735, 2.4748735)); - - let bounding_sphere = extrusion.bounding_sphere(isometry); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 2.5); - } -} diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index fa5aaf17b9f40..9d8233a624c99 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -1,12 +1,8 @@ -mod extrusion; -mod primitive_impls; - use glam::Mat3; use super::{BoundingVolume, IntersectsVolume}; use crate::{ ops::{self, FloatPow}, - primitives::Cuboid, Isometry3d, Quat, Vec3A, }; @@ -17,8 +13,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 { @@ -128,15 +122,6 @@ impl Aabb3d { } } -impl From for Aabb3d { - fn from(value: Cuboid) -> Self { - Aabb3d { - min: (-value.half_size).into(), - max: value.half_size.into(), - } - } -} - impl BoundingVolume for Aabb3d { type Translation = Vec3A; type Rotation = Quat; @@ -495,8 +480,6 @@ mod aabb3d_tests { } } -use crate::primitives::Sphere; - /// A bounding sphere #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr( @@ -513,7 +496,7 @@ pub struct BoundingSphere { /// The center of the bounding sphere pub center: Vec3A, /// The sphere - pub sphere: Sphere, + pub radius: f32, } impl BoundingSphere { @@ -522,7 +505,7 @@ impl BoundingSphere { debug_assert!(radius >= 0.); Self { center: center.into(), - sphere: Sphere { radius }, + radius, } } @@ -554,7 +537,7 @@ impl BoundingSphere { /// Get the radius of the bounding sphere #[inline] pub const fn radius(&self) -> f32 { - self.sphere.radius + self.radius } /// Computes the smallest [`Aabb3d`] containing this [`BoundingSphere`]. @@ -637,9 +620,7 @@ impl BoundingVolume for BoundingSphere { debug_assert!(amount >= 0.); Self { center: self.center, - sphere: Sphere { - radius: self.radius() + amount, - }, + radius: self.radius() + amount, } } @@ -650,9 +631,7 @@ impl BoundingVolume for BoundingSphere { debug_assert!(self.radius() >= amount); Self { center: self.center, - sphere: Sphere { - radius: self.radius() - amount, - }, + radius: self.radius() - amount, } } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs deleted file mode 100644 index 29780a9a6fbd6..0000000000000 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ /dev/null @@ -1,690 +0,0 @@ -//! 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, -}; - -#[cfg(feature = "alloc")] -use crate::primitives::Polyline3d; - -use super::{Aabb3d, Bounded3d, BoundingSphere}; - -impl Bounded3d for Sphere { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - Aabb3d::new(isometry.translation, Vec3::splat(self.radius)) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, self.radius) - } -} - -impl Bounded3d for InfinitePlane3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - - let normal = isometry.rotation * *self.normal; - let facing_x = normal == Vec3::X || normal == Vec3::NEG_X; - let facing_y = normal == Vec3::Y || normal == Vec3::NEG_Y; - let facing_z = normal == Vec3::Z || normal == Vec3::NEG_Z; - - // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations - // like growing or shrinking the AABB without breaking things. - let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; - let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; - let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 }; - let half_size = Vec3A::new(half_width, half_height, half_depth); - - Aabb3d::new(isometry.translation, half_size) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, f32::MAX / 2.0) - } -} - -impl Bounded3d for Line3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let direction = isometry.rotation * *self.direction; - - // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations - // like growing or shrinking the AABB without breaking things. - let max = f32::MAX / 2.0; - let half_width = if direction.x == 0.0 { 0.0 } else { max }; - let half_height = if direction.y == 0.0 { 0.0 } else { max }; - let half_depth = if direction.z == 0.0 { 0.0 } else { max }; - let half_size = Vec3A::new(half_width, half_height, half_depth); - - Aabb3d::new(isometry.translation, half_size) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, f32::MAX / 2.0) - } -} - -impl Bounded3d for Segment3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - Aabb3d::from_point_cloud(isometry, [self.point1(), self.point2()].iter().copied()) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - let local_sphere = BoundingSphere::new(self.center(), self.length() / 2.); - local_sphere.transformed_by(isometry.translation, isometry.rotation) - } -} - -#[cfg(feature = "alloc")] -impl Bounded3d for Polyline3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - BoundingSphere::from_point_cloud(isometry, &self.vertices) - } -} - -impl Bounded3d for Cuboid { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - - // Compute the AABB of the rotated cuboid by transforming the half-size - // by an absolute rotation matrix. - let rot_mat = Mat3::from_quat(isometry.rotation); - let abs_rot_mat = Mat3::from_cols( - rot_mat.x_axis.abs(), - rot_mat.y_axis.abs(), - rot_mat.z_axis.abs(), - ); - let half_size = abs_rot_mat * self.half_size; - - Aabb3d::new(isometry.translation, half_size) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, self.half_size.length()) - } -} - -impl Bounded3d for Cylinder { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - // Reference: http://iquilezles.org/articles/diskbbox/ - - let isometry = isometry.into(); - - let segment_dir = isometry.rotation * Vec3A::Y; - let top = segment_dir * self.half_height; - let bottom = -top; - - let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); - - Aabb3d { - min: isometry.translation + (top - half_size).min(bottom - half_size), - max: isometry.translation + (top + half_size).max(bottom + half_size), - } - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - let radius = ops::hypot(self.radius, self.half_height); - BoundingSphere::new(isometry.translation, radius) - } -} - -impl Bounded3d for Capsule3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - - // Get the line segment between the hemispheres of the rotated capsule - let segment_dir = isometry.rotation * Vec3A::Y; - let top = segment_dir * self.half_length; - let bottom = -top; - - // Expand the line segment by the capsule radius to get the capsule half-extents - let min = bottom.min(top) - Vec3A::splat(self.radius); - let max = bottom.max(top) + Vec3A::splat(self.radius); - - Aabb3d { - min: min + isometry.translation, - max: max + isometry.translation, - } - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, self.radius + self.half_length) - } -} - -impl Bounded3d for Cone { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - // Reference: http://iquilezles.org/articles/diskbbox/ - - let isometry = isometry.into(); - - let segment_dir = isometry.rotation * Vec3A::Y; - let top = segment_dir * 0.5 * self.height; - let bottom = -top; - - let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); - - Aabb3d { - min: isometry.translation + top.min(bottom - self.radius * half_extents), - max: isometry.translation + top.max(bottom + self.radius * half_extents), - } - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - - // Get the triangular cross-section of the cone. - let half_height = 0.5 * self.height; - let triangle = Triangle2d::new( - half_height * Vec2::Y, - Vec2::new(-self.radius, -half_height), - Vec2::new(self.radius, -half_height), - ); - - // Because of circular symmetry, we can use the bounding circle of the triangle - // for the bounding sphere of the cone. - let BoundingCircle { radius, center } = triangle.bounding_circle(Isometry2d::IDENTITY); - - BoundingSphere::new( - isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation, - radius, - ) - } -} - -impl Bounded3d for ConicalFrustum { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - // Reference: http://iquilezles.org/articles/diskbbox/ - - let isometry = isometry.into(); - - let segment_dir = isometry.rotation * Vec3A::Y; - let top = segment_dir * 0.5 * self.height; - let bottom = -top; - - let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); - let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); - - Aabb3d { - min: isometry.translation - + (top - self.radius_top * half_extents) - .min(bottom - self.radius_bottom * half_extents), - max: isometry.translation - + (top + self.radius_top * half_extents) - .max(bottom + self.radius_bottom * half_extents), - } - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - let half_height = 0.5 * self.height; - - // To compute the bounding sphere, we'll get the center and radius of the circumcircle - // passing through all four vertices of the trapezoidal cross-section of the conical frustum. - // - // If the circumcenter is inside the trapezoid, we can use that for the bounding sphere. - // Otherwise, we clamp it to the longer parallel side to get a more tightly fitting bounding sphere. - // - // The circumcenter is at the intersection of the bisectors perpendicular to the sides. - // For the isosceles trapezoid, the X coordinate is zero at the center, so a single bisector is enough. - // - // A - // *-------* - // / | \ - // / | \ - // AB / \ | / \ - // / \ | / \ - // / C \ - // *-------------------* - // B - - let a = Vec2::new(-self.radius_top, half_height); - let b = Vec2::new(-self.radius_bottom, -half_height); - let ab = a - b; - let ab_midpoint = b + 0.5 * ab; - let bisector = ab.perp(); - - // Compute intersection between bisector and vertical line at x = 0. - // - // x = ab_midpoint.x + t * bisector.x = 0 - // y = ab_midpoint.y + t * bisector.y = ? - // - // Because ab_midpoint.y = 0 for our conical frustum, we get: - // y = t * bisector.y - // - // Solve x for t: - // t = -ab_midpoint.x / bisector.x - // - // Substitute t to solve for y: - // y = -ab_midpoint.x / bisector.x * bisector.y - let circumcenter_y = -ab_midpoint.x / bisector.x * bisector.y; - - // If the circumcenter is outside the trapezoid, the bounding circle is too large. - // In those cases, we clamp it to the longer parallel side. - let (center, radius) = if circumcenter_y <= -half_height { - (Vec2::new(0.0, -half_height), self.radius_bottom) - } else if circumcenter_y >= half_height { - (Vec2::new(0.0, half_height), self.radius_top) - } else { - let circumcenter = Vec2::new(0.0, circumcenter_y); - // We can use the distance from an arbitrary vertex because they all lie on the circumcircle. - (circumcenter, a.distance(circumcenter)) - }; - - BoundingSphere::new( - isometry.translation + isometry.rotation * Vec3A::from(center.extend(0.0)), - radius, - ) - } -} - -impl Bounded3d for Torus { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - - // Compute the AABB of a flat disc with the major radius of the torus. - // Reference: http://iquilezles.org/articles/diskbbox/ - let normal = isometry.rotation * Vec3A::Y; - let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO); - let disc_half_size = - self.major_radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z)); - - // Expand the disc by the minor radius to get the torus half-size - let half_size = disc_half_size + Vec3A::splat(self.minor_radius); - - Aabb3d::new(isometry.translation, half_size) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - BoundingSphere::new(isometry.translation, self.outer_radius()) - } -} - -impl Bounded3d for Triangle3d { - /// Get the bounding box of the triangle. - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let [a, b, c] = self.vertices; - - let a = isometry.rotation * a; - let b = isometry.rotation * b; - let c = isometry.rotation * c; - - let min = Vec3A::from(a.min(b).min(c)); - let max = Vec3A::from(a.max(b).max(c)); - - let bounding_center = (max + min) / 2.0 + isometry.translation; - let half_extents = (max - min) / 2.0; - - Aabb3d::new(bounding_center, half_extents) - } - - /// Get the bounding sphere of the triangle. - /// - /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as - /// the center of the sphere. For the others, the bounding sphere is the minimal sphere - /// that contains the largest side of the triangle. - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - let isometry = isometry.into(); - - if self.is_degenerate() || self.is_obtuse() { - let (p1, p2) = self.largest_side(); - let (p1, p2) = (Vec3A::from(p1), Vec3A::from(p2)); - let mid_point = (p1 + p2) / 2.0; - let radius = mid_point.distance(p1); - BoundingSphere::new(mid_point + isometry.translation, radius) - } else { - let [a, _, _] = self.vertices; - - let circumcenter = self.circumcenter(); - let radius = circumcenter.distance(a); - BoundingSphere::new(Vec3A::from(circumcenter) + isometry.translation, radius) - } - } -} - -#[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, - }; - - #[test] - fn sphere() { - let sphere = Sphere { radius: 1.0 }; - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = sphere.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); - assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - - let bounding_sphere = sphere.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.0); - } - - #[test] - fn plane() { - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation); - assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0)); - assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0)); - - let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation); - assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0)); - assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0)); - - let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation); - assert_eq!(aabb3.min, Vec3A::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0)); - assert_eq!(aabb3.max, Vec3A::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0)); - - let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation); - assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); - assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); - - let bounding_sphere = InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); - } - - #[test] - fn line() { - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(translation); - assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, 0.0)); - assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, 0.0)); - - let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(translation); - assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, 0.0)); - assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, 0.0)); - - let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(translation); - assert_eq!(aabb3.min, Vec3A::new(2.0, 1.0, -f32::MAX / 2.0)); - assert_eq!(aabb3.max, Vec3A::new(2.0, 1.0, f32::MAX / 2.0)); - - let aabb4 = Line3d { - direction: Dir3::from_xyz(1.0, 1.0, 1.0).unwrap(), - } - .aabb_3d(translation); - assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); - assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); - - let bounding_sphere = Line3d { direction: Dir3::Y }.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); - } - - #[test] - fn segment() { - let segment = Segment3d::new(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0)); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = segment.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0)); - assert_eq!(aabb.max, Vec3A::new(3.0, 1.5, 0.0)); - - let bounding_sphere = segment.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); - } - - #[test] - fn polyline() { - let polyline = Polyline3d::new([ - Vec3::ONE, - Vec3::new(-1.0, 1.0, 1.0), - Vec3::NEG_ONE, - Vec3::new(1.0, -1.0, -1.0), - ]); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = polyline.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); - assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - - let bounding_sphere = polyline.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!( - bounding_sphere.radius(), - ops::hypot(ops::hypot(1.0, 1.0), 1.0) - ); - } - - #[test] - fn cuboid() { - let cuboid = Cuboid::new(2.0, 1.0, 1.0); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = cuboid.aabb_3d(Isometry3d::new( - translation, - Quat::from_rotation_z(core::f32::consts::FRAC_PI_4), - )); - let expected_half_size = Vec3A::new(1.0606601, 1.0606601, 0.5); - assert_eq!(aabb.min, Vec3A::from(translation) - expected_half_size); - assert_eq!(aabb.max, Vec3A::from(translation) + expected_half_size); - - let bounding_sphere = cuboid.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!( - bounding_sphere.radius(), - ops::hypot(ops::hypot(1.0, 0.5), 0.5) - ); - } - - #[test] - fn cylinder() { - let cylinder = Cylinder::new(0.5, 2.0); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = cylinder.aabb_3d(translation); - assert_eq!( - aabb.min, - Vec3A::from(translation) - Vec3A::new(0.5, 1.0, 0.5) - ); - assert_eq!( - aabb.max, - Vec3A::from(translation) + Vec3A::new(0.5, 1.0, 0.5) - ); - - let bounding_sphere = cylinder.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); - } - - #[test] - fn capsule() { - let capsule = Capsule3d::new(0.5, 2.0); - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = capsule.aabb_3d(translation); - assert_eq!( - aabb.min, - Vec3A::from(translation) - Vec3A::new(0.5, 1.5, 0.5) - ); - assert_eq!( - aabb.max, - Vec3A::from(translation) + Vec3A::new(0.5, 1.5, 0.5) - ); - - let bounding_sphere = capsule.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.5); - } - - #[test] - fn cone() { - let cone = Cone { - radius: 1.0, - height: 2.0, - }; - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = cone.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); - assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - - let bounding_sphere = cone.bounding_sphere(translation); - assert_eq!( - bounding_sphere.center, - Vec3A::from(translation) + Vec3A::NEG_Y * 0.25 - ); - assert_eq!(bounding_sphere.radius(), 1.25); - } - - #[test] - fn conical_frustum() { - let conical_frustum = ConicalFrustum { - radius_top: 0.5, - radius_bottom: 1.0, - height: 2.0, - }; - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = conical_frustum.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); - assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - - let bounding_sphere = conical_frustum.bounding_sphere(translation); - assert_eq!( - bounding_sphere.center, - Vec3A::from(translation) + Vec3A::NEG_Y * 0.1875 - ); - assert_eq!(bounding_sphere.radius(), 1.2884705); - } - - #[test] - fn wide_conical_frustum() { - let conical_frustum = ConicalFrustum { - radius_top: 0.5, - radius_bottom: 5.0, - height: 1.0, - }; - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = conical_frustum.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(-3.0, 0.5, -5.0)); - assert_eq!(aabb.max, Vec3A::new(7.0, 1.5, 5.0)); - - // For wide conical frusta like this, the circumcenter can be outside the frustum, - // so the center and radius should be clamped to the longest side. - let bounding_sphere = conical_frustum.bounding_sphere(translation); - assert_eq!( - bounding_sphere.center, - Vec3A::from(translation) + Vec3A::NEG_Y * 0.5 - ); - assert_eq!(bounding_sphere.radius(), 5.0); - } - - #[test] - fn torus() { - let torus = Torus { - minor_radius: 0.5, - major_radius: 1.0, - }; - let translation = Vec3::new(2.0, 1.0, 0.0); - - let aabb = torus.aabb_3d(translation); - assert_eq!(aabb.min, Vec3A::new(0.5, 0.5, -1.5)); - assert_eq!(aabb.max, Vec3A::new(3.5, 1.5, 1.5)); - - let bounding_sphere = torus.bounding_sphere(translation); - assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.5); - } - - #[test] - fn triangle3d() { - let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); - - let br = zero_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); - assert_eq!( - br.center(), - Vec3::ZERO.into(), - "incorrect bounding box center" - ); - assert_eq!( - br.half_size(), - Vec3::ZERO.into(), - "incorrect bounding box half extents" - ); - - let bs = zero_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); - assert_eq!( - bs.center, - Vec3::ZERO.into(), - "incorrect bounding sphere center" - ); - assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius"); - - let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); - let bs = dup_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); - assert_eq!( - bs.center, - Vec3::new(0.5, 0.0, 0.0).into(), - "incorrect bounding sphere center" - ); - assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius"); - let br = dup_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); - assert_eq!( - br.center(), - Vec3::new(0.5, 0.0, 0.0).into(), - "incorrect bounding box center" - ); - assert_eq!( - br.half_size(), - Vec3::new(0.5, 0.0, 0.0).into(), - "incorrect bounding box half extents" - ); - - let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); - let bs = collinear_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); - assert_eq!( - bs.center, - Vec3::ZERO.into(), - "incorrect bounding sphere center" - ); - assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius"); - let br = collinear_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); - assert_eq!( - br.center(), - Vec3::ZERO.into(), - "incorrect bounding box center" - ); - assert_eq!( - br.half_size(), - Vec3::new(1.0, 0.0, 0.0).into(), - "incorrect bounding box half extents" - ); - } -} diff --git a/crates/bevy_math/src/bounding/raycast3d.rs b/crates/bevy_math/src/bounding/raycast3d.rs index ad21766c742e3..37fc602c7401b 100644 --- a/crates/bevy_math/src/bounding/raycast3d.rs +++ b/crates/bevy_math/src/bounding/raycast3d.rs @@ -183,7 +183,7 @@ impl BoundingSphereCast { /// Get the distance at which the [`BoundingSphere`]s collide, if at all. pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option { sphere.center -= self.sphere.center; - sphere.sphere.radius += self.sphere.radius(); + sphere.radius += self.sphere.radius(); self.ray.sphere_intersection_at(&sphere) } } diff --git a/crates/bevy_math/src/primitives/half_space.rs b/crates/bevy_math/src/camera/half_space.rs similarity index 98% rename from crates/bevy_math/src/primitives/half_space.rs rename to crates/bevy_math/src/camera/half_space.rs index a955b8b0a6bb9..5c3bb8a440207 100644 --- a/crates/bevy_math/src/primitives/half_space.rs +++ b/crates/bevy_math/src/camera/half_space.rs @@ -27,7 +27,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// It includes all the points from the bisecting plane towards `NEG_Z`, and the distance /// from the plane to the origin is `-8.0` along `NEG_Z`. /// -/// It is used to define a [`ViewFrustum`](crate::primitives::ViewFrustum), +/// It is used to define a [`ViewFrustum`](crate::camera::ViewFrustum), /// but is also a useful mathematical primitive for rendering tasks such as light computation. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -109,7 +109,7 @@ mod half_space_tests { use approx::assert_relative_eq; use super::HalfSpace; - use crate::{Vec3, Vec4}; + use bevy_math::{Vec3, Vec4}; #[test] fn intersection_point() { diff --git a/crates/bevy_math/src/camera/mod.rs b/crates/bevy_math/src/camera/mod.rs new file mode 100644 index 0000000000000..e3ef8c5545ccb --- /dev/null +++ b/crates/bevy_math/src/camera/mod.rs @@ -0,0 +1,7 @@ +//! Camera related mathematical primitives like [`HalfSpace`]es and [`ViewFrustum`]s + +mod half_space; +pub use half_space::*; + +mod view_frustum; +pub use view_frustum::*; diff --git a/crates/bevy_shapes/src/primitives/view_frustum.rs b/crates/bevy_math/src/camera/view_frustum.rs similarity index 99% rename from crates/bevy_shapes/src/primitives/view_frustum.rs rename to crates/bevy_math/src/camera/view_frustum.rs index 2c314cb8c7c62..356bd3f2004cb 100644 --- a/crates/bevy_shapes/src/primitives/view_frustum.rs +++ b/crates/bevy_math/src/camera/view_frustum.rs @@ -1,5 +1,5 @@ -use crate::primitives::HalfSpace; -use bevy_math::{Mat4, Vec3, Vec4}; +use crate::camera::HalfSpace; +use crate::{Mat4, Vec3, Vec4}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -115,7 +115,7 @@ mod view_frustum_tests { use approx::assert_relative_eq; use super::ViewFrustum; - use crate::primitives::HalfSpace; + use crate::camera::HalfSpace; use bevy_math::{Vec3, Vec4}; #[test] 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..626db7d6c56d3 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -29,6 +29,7 @@ extern crate alloc; mod affine3; mod aspect_ratio; pub mod bounding; +pub mod camera; pub mod common_traits; mod compass; pub mod cubic_splines; @@ -37,7 +38,6 @@ mod float_ord; mod isometry; mod mat3; pub mod ops; -pub mod primitives; mod ray; mod rects; mod rotation2d; @@ -66,7 +66,7 @@ pub use rotation2d::Rot2; pub use curve::Curve; #[cfg(feature = "rand")] -pub use sampling::{FromRng, ShapeSample}; +pub use sampling::FromRng; /// The math prelude. /// @@ -74,15 +74,14 @@ pub use sampling::{FromRng, ShapeSample}; pub mod prelude { #[doc(hidden)] pub use crate::{ - bvec2, bvec3, bvec3a, bvec4, bvec4a, + bvec2, bvec3, bvec3a, bvec4, bvec4a, camera, 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, Rect, Rot2, + StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3A, + Vec3Swizzles, Vec4, Vec4Swizzles, }; #[doc(hidden)] @@ -91,7 +90,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/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs deleted file mode 100644 index b156b459221bc..0000000000000 --- a/crates/bevy_math/src/primitives/dim2.rs +++ /dev/null @@ -1,2637 +0,0 @@ -use core::f32::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI}; -use derive_more::derive::From; -#[cfg(feature = "alloc")] -use thiserror::Error; - -use super::{Measured2d, Primitive2d, WindingOrder}; -use crate::{ - ops::{self, FloatPow}, - primitives::Inset, - Dir2, InvalidDirectionError, Isometry2d, Ray2d, Rot2, Vec2, -}; - -#[cfg(feature = "alloc")] -use super::polygon::is_polygon_simple; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; - -#[cfg(feature = "alloc")] -use alloc::vec::Vec; - -/// A circle primitive, representing the set of points some distance from the origin -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Circle { - /// The radius of the circle - pub radius: f32, -} - -impl Primitive2d for Circle {} - -impl Default for Circle { - /// Returns the default [`Circle`] with a radius of `0.5`. - fn default() -> Self { - Self { radius: 0.5 } - } -} - -impl Circle { - /// Create a new [`Circle`] from a `radius` - #[inline] - pub const fn new(radius: f32) -> Self { - Self { radius } - } - - /// Get the diameter of the circle - #[inline] - pub const fn diameter(&self) -> f32 { - 2.0 * self.radius - } - - /// Finds the point on the circle that is closest to the given `point`. - /// - /// If the point is outside the circle, the returned point will be on the perimeter of the circle. - /// Otherwise, it will be inside the circle and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec2) -> Vec2 { - let distance_squared = point.length_squared(); - - if distance_squared <= self.radius.squared() { - // The point is inside the circle. - point - } else { - // The point is outside the circle. - // Find the closest point on the perimeter of the circle. - let dir_to_point = point / ops::sqrt(distance_squared); - self.radius * dir_to_point - } - } -} - -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. -/// If you want to include the portion of a circle's area swept out by the arc, -/// use the pie-shaped [`CircularSector`]. -/// If you want to include only the space inside the convex hull of the arc, -/// use the bowl-shaped [`CircularSegment`]. -/// -/// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on -/// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this -/// means that the origin may not be within the `Arc2d`'s convex hull. -/// -/// **Warning:** Arcs with negative angle or radius, or with angle greater than an entire circle, are not officially supported. -/// It is recommended to normalize arcs to have an angle in [0, 2π]. -#[derive(Clone, Copy, Debug, PartialEq)] -#[doc(alias("CircularArc", "CircleArc"))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Arc2d { - /// The radius of the circle - pub radius: f32, - /// Half the angle defining the arc - pub half_angle: f32, -} - -impl Primitive2d for Arc2d {} - -impl Default for Arc2d { - /// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle - fn default() -> Self { - Self { - radius: 0.5, - half_angle: 2.0 * FRAC_PI_3, - } - } -} - -impl Arc2d { - /// Create a new [`Arc2d`] from a `radius` and a `half_angle` - #[inline] - pub const fn new(radius: f32, half_angle: f32) -> Self { - Self { radius, half_angle } - } - - /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians - #[inline] - pub const fn from_radians(radius: f32, angle: f32) -> Self { - Self { - radius, - half_angle: angle / 2.0, - } - } - - /// Create a new [`Arc2d`] from a `radius` and an `angle` in degrees. - #[inline] - pub const fn from_degrees(radius: f32, angle: f32) -> Self { - Self { - radius, - half_angle: angle.to_radians() / 2.0, - } - } - - /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a single turn. - /// - /// For instance, `0.5` turns is a semicircle. - #[inline] - pub const fn from_turns(radius: f32, fraction: f32) -> Self { - Self { - radius, - half_angle: fraction * PI, - } - } - - /// Get the angle of the arc - #[inline] - pub const fn angle(&self) -> f32 { - self.half_angle * 2.0 - } - - /// Get the length of the arc - #[inline] - pub const fn length(&self) -> f32 { - self.angle() * self.radius - } - - /// Get the right-hand end point of the arc - #[inline] - pub fn right_endpoint(&self) -> Vec2 { - self.radius * Vec2::from_angle(FRAC_PI_2 - self.half_angle) - } - - /// Get the left-hand end point of the arc - #[inline] - pub fn left_endpoint(&self) -> Vec2 { - self.radius * Vec2::from_angle(FRAC_PI_2 + self.half_angle) - } - - /// Get the endpoints of the arc - #[inline] - pub fn endpoints(&self) -> [Vec2; 2] { - [self.left_endpoint(), self.right_endpoint()] - } - - /// Get the midpoint of the arc - #[inline] - pub fn midpoint(&self) -> Vec2 { - self.radius * Vec2::Y - } - - /// Get half the distance between the endpoints (half the length of the chord) - #[inline] - pub fn half_chord_length(&self) -> f32 { - self.radius * ops::sin(self.half_angle) - } - - /// Get the distance between the endpoints (the length of the chord) - #[inline] - pub fn chord_length(&self) -> f32 { - 2.0 * self.half_chord_length() - } - - /// Get the midpoint of the two endpoints (the midpoint of the chord) - #[inline] - pub fn chord_midpoint(&self) -> Vec2 { - self.apothem() * Vec2::Y - } - - /// Get the length of the apothem of this arc, that is, - /// the distance from the center of the circle to the midpoint of the chord, in the direction of the midpoint of the arc. - /// Equivalently, the [`radius`](Self::radius) minus the [`sagitta`](Self::sagitta). - /// - /// Note that for a [`major`](Self::is_major) arc, the apothem will be negative. - #[inline] - // Naming note: Various sources are inconsistent as to whether the apothem is the segment between the center and the - // midpoint of a chord, or the length of that segment. Given this confusion, we've opted for the definition - // used by Wolfram MathWorld, which is the distance rather than the segment. - pub fn apothem(&self) -> f32 { - let sign = if self.is_minor() { 1.0 } else { -1.0 }; - sign * ops::sqrt(self.radius.squared() - self.half_chord_length().squared()) - } - - /// Get the length of the sagitta of this arc, that is, - /// the length of the line between the midpoints of the arc and its chord. - /// Equivalently, the height of the triangle whose base is the chord and whose apex is the midpoint of the arc. - /// - /// The sagitta is also the sum of the [`radius`](Self::radius) and the [`apothem`](Self::apothem). - pub fn sagitta(&self) -> f32 { - self.radius - self.apothem() - } - - /// Produces true if the arc is at most half a circle. - /// - /// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor. - #[inline] - pub const fn is_minor(&self) -> bool { - self.half_angle <= FRAC_PI_2 - } - - /// Produces true if the arc is at least half a circle. - /// - /// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor. - #[inline] - pub const fn is_major(&self) -> bool { - self.half_angle >= FRAC_PI_2 - } -} - -/// A primitive representing a circular sector: a pie slice of a circle. -/// -/// The segment is positioned so that it always includes [`Vec2::Y`] and is vertically symmetrical. -/// To orient the sector differently, apply a rotation. -/// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. -/// -/// **Warning:** Circular sectors with negative angle or radius, or with angle greater than an entire circle, are not officially supported. -/// We recommend normalizing circular sectors to have an angle in [0, 2π]. -#[derive(Clone, Copy, Debug, PartialEq, From)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct CircularSector { - /// The arc defining the sector - #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] - pub arc: Arc2d, -} - -impl Primitive2d for CircularSector {} - -impl Default for CircularSector { - /// Returns the default [`CircularSector`] with radius `0.5` and covering a third of a circle - fn default() -> Self { - Self::from(Arc2d::default()) - } -} - -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] - pub const fn new(radius: f32, angle: f32) -> Self { - Self { - arc: Arc2d::new(radius, angle), - } - } - - /// Create a new [`CircularSector`] from a `radius` and an `angle` in radians. - #[inline] - pub const fn from_radians(radius: f32, angle: f32) -> Self { - Self { - arc: Arc2d::from_radians(radius, angle), - } - } - - /// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees. - #[inline] - pub const fn from_degrees(radius: f32, angle: f32) -> Self { - Self { - arc: Arc2d::from_degrees(radius, angle), - } - } - - /// Create a new [`CircularSector`] from a `radius` and a number of `turns` of a circle. - /// - /// For instance, `0.5` turns is a semicircle. - #[inline] - pub const fn from_turns(radius: f32, fraction: f32) -> Self { - Self { - arc: Arc2d::from_turns(radius, fraction), - } - } - - /// Get half the angle of the sector - #[inline] - pub const fn half_angle(&self) -> f32 { - self.arc.half_angle - } - - /// Get the angle of the sector - #[inline] - pub const fn angle(&self) -> f32 { - self.arc.angle() - } - - /// Get the radius of the sector - #[inline] - pub const fn radius(&self) -> f32 { - self.arc.radius - } - - /// Get the length of the arc defining the sector - #[inline] - pub const fn arc_length(&self) -> f32 { - self.arc.length() - } - - /// Get half the length of the chord defined by the sector - /// - /// See [`Arc2d::half_chord_length`] - #[inline] - pub fn half_chord_length(&self) -> f32 { - self.arc.half_chord_length() - } - - /// Get the length of the chord defined by the sector - /// - /// See [`Arc2d::chord_length`] - #[inline] - pub fn chord_length(&self) -> f32 { - self.arc.chord_length() - } - - /// Get the midpoint of the chord defined by the sector - /// - /// See [`Arc2d::chord_midpoint`] - #[inline] - pub fn chord_midpoint(&self) -> Vec2 { - self.arc.chord_midpoint() - } - - /// Get the length of the apothem of this sector - /// - /// See [`Arc2d::apothem`] - #[inline] - pub fn apothem(&self) -> f32 { - self.arc.apothem() - } - - /// Get the length of the sagitta of this sector - /// - /// See [`Arc2d::sagitta`] - #[inline] - pub fn sagitta(&self) -> f32 { - self.arc.sagitta() - } -} - -/// A primitive representing a circular segment: -/// the area enclosed by the arc of a circle and its chord (the line between its endpoints). -/// -/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. -/// To orient the segment differently, apply a rotation. -/// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. -/// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful. -/// -/// **Warning:** Circular segments with negative angle or radius, or with angle greater than an entire circle, are not officially supported. -/// We recommend normalizing circular segments to have an angle in [0, 2π]. -#[derive(Clone, Copy, Debug, PartialEq, From)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct CircularSegment { - /// The arc defining the segment - #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] - pub arc: Arc2d, -} - -impl Primitive2d for CircularSegment {} - -impl Default for CircularSegment { - /// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle - fn default() -> Self { - Self::from(Arc2d::default()) - } -} - -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] - pub const fn new(radius: f32, half_angle: f32) -> Self { - Self { - arc: Arc2d::new(radius, half_angle), - } - } - - /// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians. - #[inline] - pub const fn from_radians(radius: f32, angle: f32) -> Self { - Self { - arc: Arc2d::from_radians(radius, angle), - } - } - - /// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees. - #[inline] - pub const fn from_degrees(radius: f32, angle: f32) -> Self { - Self { - arc: Arc2d::from_degrees(radius, angle), - } - } - - /// Create a new [`CircularSegment`] from a `radius` and a number of `turns` of a circle. - /// - /// For instance, `0.5` turns is a semicircle. - #[inline] - pub const fn from_turns(radius: f32, fraction: f32) -> Self { - Self { - arc: Arc2d::from_turns(radius, fraction), - } - } - - /// Get the half-angle of the segment - #[inline] - pub const fn half_angle(&self) -> f32 { - self.arc.half_angle - } - - /// Get the angle of the segment - #[inline] - pub const fn angle(&self) -> f32 { - self.arc.angle() - } - - /// Get the radius of the segment - #[inline] - pub const fn radius(&self) -> f32 { - self.arc.radius - } - - /// Get the length of the arc defining the segment - #[inline] - pub const fn arc_length(&self) -> f32 { - self.arc.length() - } - - /// Get half the length of the segment's base, also known as its chord - #[inline] - #[doc(alias = "half_base_length")] - pub fn half_chord_length(&self) -> f32 { - self.arc.half_chord_length() - } - - /// Get the length of the segment's base, also known as its chord - #[inline] - #[doc(alias = "base_length")] - #[doc(alias = "base")] - pub fn chord_length(&self) -> f32 { - self.arc.chord_length() - } - - /// Get the midpoint of the segment's base, also known as its chord - #[inline] - #[doc(alias = "base_midpoint")] - pub fn chord_midpoint(&self) -> Vec2 { - self.arc.chord_midpoint() - } - - /// Get the length of the apothem of this segment, - /// which is the signed distance between the segment and the center of its circle - /// - /// See [`Arc2d::apothem`] - #[inline] - pub fn apothem(&self) -> f32 { - self.arc.apothem() - } - - /// Get the length of the sagitta of this segment, also known as its height - /// - /// See [`Arc2d::sagitta`] - #[inline] - #[doc(alias = "height")] - pub fn sagitta(&self) -> f32 { - self.arc.sagitta() - } -} - -#[cfg(test)] -mod arc_tests { - use core::f32::consts::FRAC_PI_4; - use core::f32::consts::SQRT_2; - - use approx::assert_abs_diff_eq; - - use super::*; - - struct ArcTestCase { - radius: f32, - half_angle: f32, - angle: f32, - length: f32, - right_endpoint: Vec2, - left_endpoint: Vec2, - endpoints: [Vec2; 2], - midpoint: Vec2, - half_chord_length: f32, - chord_length: f32, - chord_midpoint: Vec2, - apothem: f32, - sagitta: f32, - is_minor: bool, - is_major: bool, - sector_area: f32, - sector_perimeter: f32, - segment_area: f32, - segment_perimeter: f32, - } - - impl ArcTestCase { - fn check_arc(&self, arc: Arc2d) { - assert_abs_diff_eq!(self.radius, arc.radius); - assert_abs_diff_eq!(self.half_angle, arc.half_angle); - assert_abs_diff_eq!(self.angle, arc.angle()); - assert_abs_diff_eq!(self.length, arc.length()); - assert_abs_diff_eq!(self.right_endpoint, arc.right_endpoint()); - assert_abs_diff_eq!(self.left_endpoint, arc.left_endpoint()); - assert_abs_diff_eq!(self.endpoints[0], arc.endpoints()[0]); - assert_abs_diff_eq!(self.endpoints[1], arc.endpoints()[1]); - assert_abs_diff_eq!(self.midpoint, arc.midpoint()); - assert_abs_diff_eq!(self.half_chord_length, arc.half_chord_length()); - assert_abs_diff_eq!(self.chord_length, arc.chord_length(), epsilon = 0.00001); - assert_abs_diff_eq!(self.chord_midpoint, arc.chord_midpoint()); - assert_abs_diff_eq!(self.apothem, arc.apothem()); - assert_abs_diff_eq!(self.sagitta, arc.sagitta()); - assert_eq!(self.is_minor, arc.is_minor()); - assert_eq!(self.is_major, arc.is_major()); - } - - fn check_sector(&self, sector: CircularSector) { - assert_abs_diff_eq!(self.radius, sector.radius()); - assert_abs_diff_eq!(self.half_angle, sector.half_angle()); - assert_abs_diff_eq!(self.angle, sector.angle()); - assert_abs_diff_eq!(self.half_chord_length, sector.half_chord_length()); - assert_abs_diff_eq!(self.chord_length, sector.chord_length(), epsilon = 0.00001); - 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) { - assert_abs_diff_eq!(self.radius, segment.radius()); - assert_abs_diff_eq!(self.half_angle, segment.half_angle()); - assert_abs_diff_eq!(self.angle, segment.angle()); - assert_abs_diff_eq!(self.half_chord_length, segment.half_chord_length()); - assert_abs_diff_eq!(self.chord_length, segment.chord_length(), epsilon = 0.00001); - 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()); - } - } - - #[test] - fn zero_angle() { - let tests = ArcTestCase { - radius: 1.0, - half_angle: 0.0, - angle: 0.0, - length: 0.0, - left_endpoint: Vec2::Y, - right_endpoint: Vec2::Y, - endpoints: [Vec2::Y, Vec2::Y], - midpoint: Vec2::Y, - half_chord_length: 0.0, - chord_length: 0.0, - chord_midpoint: Vec2::Y, - apothem: 1.0, - 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)); - tests.check_sector(CircularSector::new(1.0, 0.0)); - tests.check_segment(CircularSegment::new(1.0, 0.0)); - } - - #[test] - fn zero_radius() { - let tests = ArcTestCase { - radius: 0.0, - half_angle: FRAC_PI_4, - angle: FRAC_PI_2, - length: 0.0, - left_endpoint: Vec2::ZERO, - right_endpoint: Vec2::ZERO, - endpoints: [Vec2::ZERO, Vec2::ZERO], - midpoint: Vec2::ZERO, - half_chord_length: 0.0, - chord_length: 0.0, - chord_midpoint: Vec2::ZERO, - apothem: 0.0, - 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)); - tests.check_sector(CircularSector::new(0.0, FRAC_PI_4)); - tests.check_segment(CircularSegment::new(0.0, FRAC_PI_4)); - } - - #[test] - fn quarter_circle() { - let sqrt_half: f32 = ops::sqrt(0.5); - let tests = ArcTestCase { - radius: 1.0, - half_angle: FRAC_PI_4, - angle: FRAC_PI_2, - length: FRAC_PI_2, - left_endpoint: Vec2::new(-sqrt_half, sqrt_half), - right_endpoint: Vec2::splat(sqrt_half), - endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], - midpoint: Vec2::Y, - half_chord_length: sqrt_half, - chord_length: ops::sqrt(2.0), - chord_midpoint: Vec2::new(0.0, sqrt_half), - apothem: sqrt_half, - 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)); - tests.check_sector(CircularSector::from_turns(1.0, 0.25)); - tests.check_segment(CircularSegment::from_turns(1.0, 0.25)); - } - - #[test] - fn half_circle() { - let tests = ArcTestCase { - radius: 1.0, - half_angle: FRAC_PI_2, - angle: PI, - length: PI, - left_endpoint: Vec2::NEG_X, - right_endpoint: Vec2::X, - endpoints: [Vec2::NEG_X, Vec2::X], - midpoint: Vec2::Y, - half_chord_length: 1.0, - chord_length: 2.0, - chord_midpoint: Vec2::ZERO, - apothem: 0.0, - 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)); - tests.check_sector(CircularSector::from_radians(1.0, PI)); - tests.check_segment(CircularSegment::from_radians(1.0, PI)); - } - - #[test] - fn full_circle() { - let tests = ArcTestCase { - radius: 1.0, - half_angle: PI, - angle: 2.0 * PI, - length: 2.0 * PI, - left_endpoint: Vec2::NEG_Y, - right_endpoint: Vec2::NEG_Y, - endpoints: [Vec2::NEG_Y, Vec2::NEG_Y], - midpoint: Vec2::Y, - half_chord_length: 0.0, - chord_length: 0.0, - chord_midpoint: Vec2::NEG_Y, - apothem: -1.0, - 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)); - tests.check_sector(CircularSector::from_degrees(1.0, 360.0)); - tests.check_segment(CircularSegment::from_degrees(1.0, 360.0)); - } -} - -/// 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( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Ellipse { - /// Half of the width and height of the ellipse. - /// - /// This corresponds to the two perpendicular radii defining the ellipse. - pub half_size: Vec2, -} - -impl Primitive2d for Ellipse {} - -impl Default for Ellipse { - /// Returns the default [`Ellipse`] with a half-width of `1.0` and a half-height of `0.5`. - fn default() -> Self { - Self { - half_size: Vec2::new(1.0, 0.5), - } - } -} - -impl Ellipse { - /// Create a new `Ellipse` from half of its width and height. - /// - /// This corresponds to the two perpendicular radii defining the ellipse. - #[inline] - pub const fn new(half_width: f32, half_height: f32) -> Self { - Self { - half_size: Vec2::new(half_width, half_height), - } - } - - /// Create a new `Ellipse` from a given full size. - /// - /// `size.x` is the diameter along the X axis, and `size.y` is the diameter along the Y axis. - #[inline] - pub const fn from_size(size: Vec2) -> Self { - Self { - half_size: Vec2::new(size.x / 2.0, size.y / 2.0), - } - } - - #[inline] - /// Returns the [eccentricity](https://en.wikipedia.org/wiki/Eccentricity_(mathematics)) of the ellipse. - /// It can be thought of as a measure of how "stretched" or elongated the ellipse is. - /// - /// The value should be in the range [0, 1), where 0 represents a circle, and 1 represents a parabola. - pub fn eccentricity(&self) -> f32 { - let a = self.semi_major(); - let b = self.semi_minor(); - - ops::sqrt(a * a - b * b) / a - } - - #[inline] - /// Get the focal length of the ellipse. This corresponds to the distance between one of the foci and the center of the ellipse. - /// - /// The focal length of an ellipse is related to its eccentricity by `eccentricity = focal_length / semi_major` - pub fn focal_length(&self) -> f32 { - let a = self.semi_major(); - let b = self.semi_minor(); - - ops::sqrt(a * a - b * b) - } - - /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. - #[inline] - pub fn semi_major(&self) -> f32 { - self.half_size.max_element() - } - - /// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse. - #[inline] - pub fn semi_minor(&self) -> f32 { - self.half_size.min_element() - } -} - -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))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "Ring")] -pub struct Annulus { - /// The inner circle of the annulus - pub inner_circle: Circle, - /// The outer circle of the annulus - pub outer_circle: Circle, -} - -impl Primitive2d for Annulus {} - -impl Default for Annulus { - /// Returns the default [`Annulus`] with radii of `0.5` and `1.0`. - fn default() -> Self { - Self { - inner_circle: Circle::new(0.5), - outer_circle: Circle::new(1.0), - } - } -} - -impl Annulus { - /// Create a new [`Annulus`] from the radii of the inner and outer circle - #[inline] - pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { - Self { - inner_circle: Circle::new(inner_radius), - outer_circle: Circle::new(outer_radius), - } - } - - /// Get the diameter of the annulus - #[inline] - pub const fn diameter(&self) -> f32 { - self.outer_circle.diameter() - } - - /// Get the thickness of the annulus - #[inline] - pub const fn thickness(&self) -> f32 { - self.outer_circle.radius - self.inner_circle.radius - } - - /// Finds the point on the annulus that is closest to the given `point`: - /// - /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. - /// - If the point is inside of the inner circle (hole) of the annulus, the returned point will be on the inner perimeter. - /// - Otherwise, the returned point is overlapping the annulus and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec2) -> Vec2 { - let distance_squared = point.length_squared(); - - if self.inner_circle.radius.squared() <= distance_squared { - if distance_squared <= self.outer_circle.radius.squared() { - // The point is inside the annulus. - point - } else { - // The point is outside the annulus and closer to the outer perimeter. - // Find the closest point on the perimeter of the annulus. - let dir_to_point = point / ops::sqrt(distance_squared); - self.outer_circle.radius * dir_to_point - } - } else { - // The point is outside the annulus and closer to the inner perimeter. - // Find the closest point on the perimeter of the annulus. - let dir_to_point = point / ops::sqrt(distance_squared); - self.inner_circle.radius * dir_to_point - } - } -} - -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. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "Diamond")] -pub struct Rhombus { - /// Size of the horizontal and vertical diagonals of the rhombus - pub half_diagonals: Vec2, -} - -impl Primitive2d for Rhombus {} - -impl Default for Rhombus { - /// Returns the default [`Rhombus`] with a half-horizontal and half-vertical diagonal of `0.5`. - fn default() -> Self { - Self { - half_diagonals: Vec2::splat(0.5), - } - } -} - -impl Rhombus { - /// Create a new `Rhombus` from a vertical and horizontal diagonal sizes. - #[inline] - pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { - Self { - half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), - } - } - - /// Create a new `Rhombus` from a side length with all inner angles equal. - #[inline] - pub const fn from_side(side: f32) -> Self { - Self { - half_diagonals: Vec2::splat(side * FRAC_1_SQRT_2), - } - } - - /// Create a new `Rhombus` from a given inradius with all inner angles equal. - #[inline] - pub const fn from_inradius(inradius: f32) -> Self { - let half_diagonal = inradius * 2.0 / core::f32::consts::SQRT_2; - Self { - half_diagonals: Vec2::new(half_diagonal, half_diagonal), - } - } - - /// Get the length of each side of the rhombus - #[inline] - pub fn side(&self) -> f32 { - self.half_diagonals.length() - } - - /// Get the radius of the circumcircle on which all vertices - /// of the rhombus lie - #[inline] - pub const fn circumradius(&self) -> f32 { - self.half_diagonals.x.max(self.half_diagonals.y) - } - - /// Get the radius of the largest circle that can - /// be drawn within the rhombus - #[inline] - #[doc(alias = "apothem")] - pub fn inradius(&self) -> f32 { - let side = self.side(); - if side == 0.0 { - 0.0 - } else { - (self.half_diagonals.x * self.half_diagonals.y) / side - } - } - - /// Finds the point on the rhombus that is closest to the given `point`. - /// - /// If the point is outside the rhombus, the returned point will be on the perimeter of the rhombus. - /// Otherwise, it will be inside the rhombus and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec2) -> Vec2 { - // Fold the problem into the positive quadrant - let point_abs = point.abs(); - let half_diagonals = self.half_diagonals.abs(); // to ensure correct sign - - // The unnormalised normal vector perpendicular to the side of the rhombus - let normal = Vec2::new(half_diagonals.y, half_diagonals.x); - let normal_magnitude_squared = normal.length_squared(); - if normal_magnitude_squared == 0.0 { - return Vec2::ZERO; // A null Rhombus has only one point anyway. - } - - // The last term corresponds to normal.dot(rhombus_vertex) - let distance_unnormalised = normal.dot(point_abs) - half_diagonals.x * half_diagonals.y; - - // The point is already inside so we simply return it. - if distance_unnormalised <= 0.0 { - return point; - } - - // Clamp the point to the edge - let mut result = point_abs - normal * distance_unnormalised / normal_magnitude_squared; - - // Clamp the point back to the positive quadrant - // if it's outside, it needs to be clamped to either vertex - if result.x <= 0.0 { - result = Vec2::new(0.0, half_diagonals.y); - } else if result.y <= 0.0 { - result = Vec2::new(half_diagonals.x, 0.0); - } - - // Finally, we restore the signs of the original vector - result.copysign(point) - } -} - -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)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Plane2d { - /// The normal of the plane. The plane will be placed perpendicular to this direction - pub normal: Dir2, -} - -impl Primitive2d for Plane2d {} - -impl Default for Plane2d { - /// Returns the default [`Plane2d`] with a normal pointing in the `+Y` direction. - fn default() -> Self { - Self { normal: Dir2::Y } - } -} - -impl Plane2d { - /// Create a new `Plane2d` from a normal - /// - /// # Panics - /// - /// Panics if the given `normal` is zero (or very close to zero), or non-finite. - #[inline] - pub fn new(normal: Vec2) -> Self { - Self { - normal: Dir2::new(normal).expect("normal must be nonzero and finite"), - } - } -} - -/// An infinite line going through the origin along a direction in 2D space. -/// -/// For a finite line: [`Segment2d`] -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Line2d { - /// The direction of the line. The line extends infinitely in both the given direction - /// and its opposite direction - pub direction: Dir2, -} - -impl Primitive2d for Line2d {} - -/// A line segment defined by two endpoints in 2D space. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "LineSegment2d")] -pub struct Segment2d { - /// The endpoints of the line segment. - pub vertices: [Vec2; 2], -} - -impl Primitive2d for Segment2d {} - -impl Default for Segment2d { - fn default() -> Self { - Self { - vertices: [Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)], - } - } -} - -impl Segment2d { - /// Create a new `Segment2d` from its endpoints. - #[inline] - pub const fn new(point1: Vec2, point2: Vec2) -> Self { - Self { - vertices: [point1, point2], - } - } - - /// Create a new `Segment2d` centered at the origin with the given direction and length. - /// - /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. - #[inline] - pub fn from_direction_and_length(direction: Dir2, length: f32) -> Self { - let endpoint = 0.5 * length * direction; - Self { - vertices: [-endpoint, endpoint], - } - } - - /// Create a new `Segment2d` centered at the origin from a vector representing - /// the direction and length of the line segment. - /// - /// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`. - #[inline] - pub fn from_scaled_direction(scaled_direction: Vec2) -> Self { - let endpoint = 0.5 * scaled_direction; - Self { - vertices: [-endpoint, endpoint], - } - } - - /// Create a new `Segment2d` starting from the origin of the given `ray`, - /// going in the direction of the ray for the given `length`. - /// - /// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`. - #[inline] - pub fn from_ray_and_length(ray: Ray2d, length: f32) -> Self { - Self { - vertices: [ray.origin, ray.get_point(length)], - } - } - - /// Get the position of the first endpoint of the line segment. - #[inline] - pub const fn point1(&self) -> Vec2 { - self.vertices[0] - } - - /// Get the position of the second endpoint of the line segment. - #[inline] - pub const fn point2(&self) -> Vec2 { - self.vertices[1] - } - - /// Compute the midpoint between the two endpoints of the line segment. - #[inline] - #[doc(alias = "midpoint")] - pub fn center(&self) -> Vec2 { - self.point1().midpoint(self.point2()) - } - - /// Compute the length of the line segment. - #[inline] - pub fn length(&self) -> f32 { - self.point1().distance(self.point2()) - } - - /// Compute the squared length of the line segment. - #[inline] - pub fn length_squared(&self) -> f32 { - self.point1().distance_squared(self.point2()) - } - - /// Compute the normalized direction pointing from the first endpoint to the second endpoint. - /// - /// For the non-panicking version, see [`Segment2d::try_direction`]. - /// - /// # Panics - /// - /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn direction(&self) -> Dir2 { - self.try_direction().unwrap_or_else(|err| { - panic!("Failed to compute the direction of a line segment: {err}") - }) - } - - /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. - /// - /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed, - /// for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn try_direction(&self) -> Result { - Dir2::new(self.scaled_direction()) - } - - /// Compute the vector from the first endpoint to the second endpoint. - #[inline] - pub fn scaled_direction(&self) -> Vec2 { - self.point2() - self.point1() - } - - /// Compute the normalized counterclockwise normal on the left-hand side of the line segment. - /// - /// For the non-panicking version, see [`Segment2d::try_left_normal`]. - /// - /// # Panics - /// - /// Panics if a valid normal could not be computed, for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn left_normal(&self) -> Dir2 { - self.try_left_normal().unwrap_or_else(|err| { - panic!("Failed to compute the left-hand side normal of a line segment: {err}") - }) - } - - /// Try to compute the normalized counterclockwise normal on the left-hand side of the line segment. - /// - /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid normal could not be computed, - /// for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn try_left_normal(&self) -> Result { - Dir2::new(self.scaled_left_normal()) - } - - /// Compute the non-normalized counterclockwise normal on the left-hand side of the line segment. - /// - /// The length of the normal is the distance between the endpoints. - #[inline] - pub fn scaled_left_normal(&self) -> Vec2 { - let scaled_direction = self.scaled_direction(); - Vec2::new(-scaled_direction.y, scaled_direction.x) - } - - /// Compute the normalized clockwise normal on the right-hand side of the line segment. - /// - /// For the non-panicking version, see [`Segment2d::try_right_normal`]. - /// - /// # Panics - /// - /// Panics if a valid normal could not be computed, for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn right_normal(&self) -> Dir2 { - self.try_right_normal().unwrap_or_else(|err| { - panic!("Failed to compute the right-hand side normal of a line segment: {err}") - }) - } - - /// Try to compute the normalized clockwise normal on the right-hand side of the line segment. - /// - /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid normal could not be computed, - /// for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn try_right_normal(&self) -> Result { - Dir2::new(self.scaled_right_normal()) - } - - /// Compute the non-normalized clockwise normal on the right-hand side of the line segment. - /// - /// The length of the normal is the distance between the endpoints. - #[inline] - pub fn scaled_right_normal(&self) -> Vec2 { - let scaled_direction = self.scaled_direction(); - Vec2::new(scaled_direction.y, -scaled_direction.x) - } - - /// Compute the segment transformed by the given [`Isometry2d`]. - #[inline] - pub fn transformed(&self, isometry: impl Into) -> Self { - let isometry: Isometry2d = isometry.into(); - Self::new( - isometry.transform_point(self.point1()), - isometry.transform_point(self.point2()), - ) - } - - /// Compute the segment translated by the given vector. - #[inline] - pub fn translated(&self, translation: Vec2) -> Segment2d { - Self::new(self.point1() + translation, self.point2() + translation) - } - - /// Compute the segment rotated around the origin by the given rotation. - #[inline] - pub fn rotated(&self, rotation: Rot2) -> Segment2d { - Segment2d::new(rotation * self.point1(), rotation * self.point2()) - } - - /// Compute the segment rotated around the given point by the given rotation. - #[inline] - pub fn rotated_around(&self, rotation: Rot2, point: Vec2) -> Segment2d { - // We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back - let offset = self.translated(-point); - let rotated = offset.rotated(rotation); - rotated.translated(point) - } - - /// Compute the segment rotated around its own center. - #[inline] - pub fn rotated_around_center(&self, rotation: Rot2) -> Segment2d { - self.rotated_around(rotation, self.center()) - } - - /// Compute the segment with its center at the origin, keeping the same direction and length. - #[inline] - pub fn centered(&self) -> Segment2d { - let center = self.center(); - self.translated(-center) - } - - /// Compute the segment with a new length, keeping the same direction and center. - #[inline] - pub fn resized(&self, length: f32) -> Segment2d { - let offset_from_origin = self.center(); - let centered = self.translated(-offset_from_origin); - let ratio = length / self.length(); - let segment = Segment2d::new(centered.point1() * ratio, centered.point2() * ratio); - segment.translated(offset_from_origin) - } - - /// Reverses the direction of the line segment by swapping the endpoints. - #[inline] - pub const fn reverse(&mut self) { - let [point1, point2] = &mut self.vertices; - core::mem::swap(point1, point2); - } - - /// Returns the line segment with its direction reversed by swapping the endpoints. - #[inline] - #[must_use] - pub fn reversed(mut self) -> Self { - self.reverse(); - self - } - - /// Returns the point on the [`Segment2d`] that is closest to the specified `point`. - #[inline] - pub fn closest_point(&self, point: Vec2) -> Vec2 { - // `point` - // x - // ^| - // / | - //`offset`/ | - // / | `segment_vector` - // x----.-------------->x - // 0 t 1 - let segment_vector = self.vertices[1] - self.vertices[0]; - let offset = point - self.vertices[0]; - // The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment. - let projection_scaled = segment_vector.dot(offset); - - // `point` is too far "left" in the picture - if projection_scaled <= 0.0 { - return self.vertices[0]; - } - - let length_squared = segment_vector.length_squared(); - // `point` is too far "right" in the picture - if projection_scaled >= length_squared { - return self.vertices[1]; - } - - // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. - let t = projection_scaled / length_squared; - self.vertices[0] + t * segment_vector - } -} - -impl From<[Vec2; 2]> for Segment2d { - #[inline] - fn from(vertices: [Vec2; 2]) -> Self { - Self { vertices } - } -} - -impl From<(Vec2, Vec2)> for Segment2d { - #[inline] - fn from((point1, point2): (Vec2, Vec2)) -> Self { - Self::new(point1, point2) - } -} - -/// A series of connected line segments in 2D space. -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Polyline2d { - /// The vertices of the polyline - pub vertices: Vec, -} - -#[cfg(feature = "alloc")] -impl Primitive2d for Polyline2d {} - -#[cfg(feature = "alloc")] -impl FromIterator for Polyline2d { - fn from_iter>(iter: I) -> Self { - Self { - vertices: iter.into_iter().collect(), - } - } -} - -#[cfg(feature = "alloc")] -impl Default for Polyline2d { - fn default() -> Self { - Self { - vertices: Vec::from([Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)]), - } - } -} - -#[cfg(feature = "alloc")] -impl Polyline2d { - /// Create a new `Polyline2d` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) - } - - /// Create a new `Polyline2d` from two endpoints with subdivision points. - /// `subdivisions = 0` creates a simple line with just start and end points. - /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. - pub fn with_subdivisions(start: Vec2, end: Vec2, subdivisions: usize) -> Self { - let total_vertices = subdivisions + 2; - let mut vertices = Vec::with_capacity(total_vertices); - - let step = (end - start) / (subdivisions + 1) as f32; - for i in 0..total_vertices { - vertices.push(start + step * i as f32); - } - - Self { vertices } - } -} - -/// A triangle in 2D space -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Triangle2d { - /// The vertices of the triangle - pub vertices: [Vec2; 3], -} - -impl Primitive2d for Triangle2d {} - -impl Default for Triangle2d { - /// Returns the default [`Triangle2d`] with the vertices `[0.0, 0.5]`, `[-0.5, -0.5]`, and `[0.5, -0.5]`. - fn default() -> Self { - Self { - vertices: [Vec2::Y * 0.5, Vec2::new(-0.5, -0.5), Vec2::new(0.5, -0.5)], - } - } -} - -impl Triangle2d { - /// Create a new `Triangle2d` from points `a`, `b`, and `c` - #[inline] - pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { - Self { - vertices: [a, b, c], - } - } - - /// Get the [`WindingOrder`] of the triangle - #[inline] - #[doc(alias = "orientation")] - pub fn winding_order(&self) -> WindingOrder { - let [a, b, c] = self.vertices; - let area = (b - a).perp_dot(c - a); - if area > f32::EPSILON { - WindingOrder::CounterClockwise - } else if area < -f32::EPSILON { - WindingOrder::Clockwise - } else { - WindingOrder::Invalid - } - } - - /// Compute the circle passing through all three vertices of the triangle. - /// The vector in the returned tuple is the circumcenter. - pub fn circumcircle(&self) -> (Circle, Vec2) { - // We treat the triangle as translated so that vertex A is at the origin. This simplifies calculations. - // - // A = (0, 0) - // * - // / \ - // / \ - // / \ - // / \ - // / U \ - // / \ - // *-------------* - // B C - - let a = self.vertices[0]; - let (b, c) = (self.vertices[1] - a, self.vertices[2] - a); - let b_length_sq = b.length_squared(); - let c_length_sq = c.length_squared(); - - // Reference: https://en.wikipedia.org/wiki/Circumcircle#Cartesian_coordinates_2 - let inv_d = (2.0 * (b.x * c.y - b.y * c.x)).recip(); - let ux = inv_d * (c.y * b_length_sq - b.y * c_length_sq); - let uy = inv_d * (b.x * c_length_sq - c.x * b_length_sq); - let u = Vec2::new(ux, uy); - - // Compute true circumcenter and circumradius, adding the tip coordinate so that - // A is translated back to its actual coordinate. - let center = u + a; - let radius = u.length(); - - (Circle { radius }, center) - } - - /// Checks if the triangle is degenerate, meaning it has zero area. - /// - /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. - /// This indicates that the three vertices are collinear or nearly collinear. - #[inline] - pub fn is_degenerate(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = (b - a).extend(0.); - let ac = (c - a).extend(0.); - ab.cross(ac).length() < 10e-7 - } - - /// Checks if the triangle is acute, meaning all angles are less than 90 degrees - #[inline] - pub fn is_acute(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = b - a; - let bc = c - b; - let ca = a - c; - - // a^2 + b^2 < c^2 for an acute triangle - let side_lengths = [ - ab.length_squared(), - bc.length_squared(), - ca.length_squared(), - ]; - let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; - let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); - sum - max > max - } - - /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees - #[inline] - pub fn is_obtuse(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = b - a; - let bc = c - b; - let ca = a - c; - - // a^2 + b^2 > c^2 for an obtuse triangle - let side_lengths = [ - ab.length_squared(), - bc.length_squared(), - ca.length_squared(), - ]; - let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; - let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); - sum - max < max - } - - /// Reverse the [`WindingOrder`] of the triangle - /// by swapping the first and last vertices. - #[inline] - pub const fn reverse(&mut self) { - self.vertices.swap(0, 2); - } - - /// This triangle but reversed. - #[inline] - #[must_use] - pub fn reversed(mut self) -> Self { - self.reverse(); - self - } -} - -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))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "Quad")] -pub struct Rectangle { - /// Half of the width and height of the rectangle - pub half_size: Vec2, -} - -impl Primitive2d for Rectangle {} - -impl Default for Rectangle { - /// Returns the default [`Rectangle`] with a half-width and half-height of `0.5`. - fn default() -> Self { - Self { - half_size: Vec2::splat(0.5), - } - } -} - -impl Rectangle { - /// Create a new `Rectangle` from a full width and height - #[inline] - pub const fn new(width: f32, height: f32) -> Self { - Self::from_size(Vec2::new(width, height)) - } - - /// Create a new `Rectangle` from a given full size - #[inline] - pub const fn from_size(size: Vec2) -> Self { - Self { - half_size: Vec2::new(size.x / 2.0, size.y / 2.0), - } - } - - /// Create a new `Rectangle` from two corner points - #[inline] - pub fn from_corners(point1: Vec2, point2: Vec2) -> Self { - Self { - half_size: (point2 - point1).abs() / 2.0, - } - } - - /// Create a `Rectangle` from a single length. - /// The resulting `Rectangle` will be the same size in every direction. - #[inline] - pub const fn from_length(length: f32) -> Self { - Self { - half_size: Vec2::splat(length / 2.0), - } - } - - /// Get the size of the rectangle - #[inline] - pub fn size(&self) -> Vec2 { - 2.0 * self.half_size - } - - /// Finds the point on the rectangle that is closest to the given `point`. - /// - /// If the point is outside the rectangle, the returned point will be on the perimeter of the rectangle. - /// Otherwise, it will be inside the rectangle and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec2) -> Vec2 { - // Clamp point coordinates to the rectangle - point.clamp(-self.half_size, self.half_size) - } -} - -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)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Polygon { - /// The vertices of the `Polygon` - pub vertices: Vec, -} - -#[cfg(feature = "alloc")] -impl Primitive2d for Polygon {} - -#[cfg(feature = "alloc")] -impl FromIterator for Polygon { - fn from_iter>(iter: I) -> Self { - Self { - vertices: iter.into_iter().collect(), - } - } -} - -#[cfg(feature = "alloc")] -impl Polygon { - /// Create a new `Polygon` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) - } - - /// Tests if the polygon is simple. - /// - /// A polygon is simple if it is not self intersecting and not self tangent. - /// As such, no two edges of the polygon may cross each other and each vertex must not lie on another edge. - #[cfg(feature = "alloc")] - pub fn is_simple(&self) -> bool { - is_polygon_simple(&self.vertices) - } -} - -#[cfg(feature = "alloc")] -impl From for Polygon { - fn from(val: ConvexPolygon) -> Self { - Polygon { - vertices: val.vertices, - } - } -} - -/// A convex polygon with `N` vertices. -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct ConvexPolygon { - /// The vertices of the [`ConvexPolygon`]. - vertices: Vec, -} - -#[cfg(feature = "alloc")] -impl Primitive2d for ConvexPolygon {} - -/// An error that happens when creating a [`ConvexPolygon`]. -#[cfg(feature = "alloc")] -#[derive(Error, Debug, Clone)] -pub enum ConvexPolygonError { - /// The created polygon is not convex. - #[error("The created polygon is not convex")] - Concave, -} - -#[cfg(feature = "alloc")] -impl ConvexPolygon { - fn triangle_winding_order( - &self, - a_index: usize, - b_index: usize, - c_index: usize, - ) -> WindingOrder { - let a = self.vertices[a_index]; - let b = self.vertices[b_index]; - let c = self.vertices[c_index]; - Triangle2d::new(a, b, c).winding_order() - } - - /// Create a [`ConvexPolygon`] from its `vertices`. - /// - /// # Errors - /// - /// Returns [`ConvexPolygonError::Concave`] if the `vertices` do not form a convex polygon. - pub fn new(vertices: impl IntoIterator) -> Result { - let polygon = Self::new_unchecked(vertices); - let len = polygon.vertices.len(); - let ref_winding_order = polygon.triangle_winding_order(len - 1, 0, 1); - for i in 1..len { - let winding_order = polygon.triangle_winding_order(i - 1, i, (i + 1) % len); - if winding_order != ref_winding_order { - return Err(ConvexPolygonError::Concave); - } - } - Ok(polygon) - } - - /// Create a [`ConvexPolygon`] from its `vertices`, without checks. - /// Use this version only if you know that the `vertices` make up a convex polygon. - #[inline] - pub fn new_unchecked(vertices: impl IntoIterator) -> Self { - Self { - vertices: vertices.into_iter().collect(), - } - } - - /// Get the vertices of this polygon - #[inline] - pub fn vertices(&self) -> &[Vec2] { - &self.vertices - } -} - -#[cfg(feature = "alloc")] -impl TryFrom for ConvexPolygon { - type Error = ConvexPolygonError; - - fn try_from(val: Polygon) -> Result { - ConvexPolygon::new(val.vertices) - } -} - -/// A polygon centered on the origin where all vertices lie on a circle, equally far apart. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct RegularPolygon { - /// The circumcircle on which all vertices lie - pub circumcircle: Circle, - /// The number of sides - pub sides: u32, -} - -impl Primitive2d for RegularPolygon {} - -impl Default for RegularPolygon { - /// Returns the default [`RegularPolygon`] with six sides (a hexagon) and a circumradius of `0.5`. - fn default() -> Self { - Self { - circumcircle: Circle { radius: 0.5 }, - sides: 6, - } - } -} - -impl RegularPolygon { - /// Create a new `RegularPolygon` - /// from the radius of the circumcircle and a number of sides - /// - /// # Panics - /// - /// Panics if `circumradius` is negative - #[inline] - pub const fn new(circumradius: f32, sides: u32) -> Self { - assert!( - circumradius.is_sign_positive(), - "polygon has a negative radius" - ); - assert!(sides > 2, "polygon has less than 3 sides"); - - Self { - circumcircle: Circle { - radius: circumradius, - }, - sides, - } - } - - /// Get the radius of the circumcircle on which all vertices - /// of the regular polygon lie - #[inline] - pub const fn circumradius(&self) -> f32 { - self.circumcircle.radius - } - - /// Get the inradius or apothem of the regular polygon. - /// This is the radius of the largest circle that can - /// be drawn within the polygon - #[inline] - #[doc(alias = "apothem")] - pub fn inradius(&self) -> f32 { - self.circumradius() * ops::cos(PI / self.sides as f32) - } - - /// Get the length of one side of the regular polygon - #[inline] - pub fn side_length(&self) -> f32 { - 2.0 * self.circumradius() * ops::sin(PI / self.sides as f32) - } - - /// Get the internal angle of the regular polygon in degrees. - /// - /// This is the angle formed by two adjacent sides with points - /// within the angle being in the interior of the polygon - #[inline] - pub const fn internal_angle_degrees(&self) -> f32 { - (self.sides - 2) as f32 / self.sides as f32 * 180.0 - } - - /// Get the internal angle of the regular polygon in radians. - /// - /// This is the angle formed by two adjacent sides with points - /// within the angle being in the interior of the polygon - #[inline] - pub const fn internal_angle_radians(&self) -> f32 { - (self.sides - 2) as f32 * PI / self.sides as f32 - } - - /// Get the external angle of the regular polygon in degrees. - /// - /// This is the angle formed by two adjacent sides with points - /// within the angle being in the exterior of the polygon - #[inline] - pub const fn external_angle_degrees(&self) -> f32 { - 360.0 / self.sides as f32 - } - - /// Get the external angle of the regular polygon in radians. - /// - /// This is the angle formed by two adjacent sides with points - /// within the angle being in the exterior of the polygon - #[inline] - pub const fn external_angle_radians(&self) -> f32 { - 2.0 * PI / self.sides as f32 - } - - /// Returns an iterator over the vertices of the regular polygon, - /// rotated counterclockwise by the given angle in radians. - /// - /// With a rotation of 0, a vertex will be placed at the top `(0.0, circumradius)`. - pub fn vertices(self, rotation: f32) -> impl IntoIterator { - // Add pi/2 so that the polygon has a vertex at the top (sin is 1.0 and cos is 0.0) - let start_angle = rotation + FRAC_PI_2; - let step = core::f32::consts::TAU / self.sides as f32; - - (0..self.sides).map(move |i| { - let theta = start_angle + i as f32 * step; - let (sin, cos) = ops::sin_cos(theta); - Vec2::new(cos, sin) * self.circumcircle.radius - }) - } -} - -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 -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "stadium", alias = "pill")] -pub struct Capsule2d { - /// The radius of the capsule - pub radius: f32, - /// Half the height of the capsule, excluding the semicircles - pub half_length: f32, -} - -impl Primitive2d for Capsule2d {} - -impl Default for Capsule2d { - /// Returns the default [`Capsule2d`] with a radius of `0.5` and a half-height of `0.5`, - /// excluding the semicircles. - fn default() -> Self { - Self { - radius: 0.5, - half_length: 0.5, - } - } -} - -impl Capsule2d { - /// Create a new `Capsule2d` from a radius and length - pub const fn new(radius: f32, length: f32) -> Self { - Self { - radius, - half_length: length / 2.0, - } - } - - /// Get the part connecting the semicircular ends of the capsule as a [`Rectangle`] - #[inline] - pub const fn to_inner_rectangle(&self) -> Rectangle { - Rectangle::new(self.radius * 2.0, self.half_length * 2.0) - } -} - -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 - - use super::*; - use approx::{assert_abs_diff_eq, assert_relative_eq}; - - #[test] - fn rectangle_closest_point() { - let rectangle = Rectangle::new(2.0, 2.0); - assert_eq!(rectangle.closest_point(Vec2::X * 10.0), Vec2::X); - assert_eq!(rectangle.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE); - assert_eq!( - rectangle.closest_point(Vec2::new(0.25, 0.1)), - Vec2::new(0.25, 0.1) - ); - } - - #[test] - fn circle_closest_point() { - let circle = Circle { radius: 1.0 }; - assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X); - assert_eq!( - circle.closest_point(Vec2::NEG_ONE * 10.0), - Vec2::NEG_ONE.normalize() - ); - assert_eq!( - circle.closest_point(Vec2::new(0.25, 0.1)), - Vec2::new(0.25, 0.1) - ); - } - - #[test] - fn annulus_closest_point() { - let annulus = Annulus::new(1.5, 2.0); - assert_eq!(annulus.closest_point(Vec2::X * 10.0), Vec2::X * 2.0); - assert_eq!( - annulus.closest_point(Vec2::NEG_ONE), - Vec2::NEG_ONE.normalize() * 1.5 - ); - assert_eq!( - annulus.closest_point(Vec2::new(1.55, 0.85)), - Vec2::new(1.55, 0.85) - ); - } - - #[test] - fn rhombus_closest_point() { - let rhombus = Rhombus::new(2.0, 1.0); - assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::X); - assert_eq!( - rhombus.closest_point(Vec2::NEG_ONE * 0.2), - Vec2::NEG_ONE * 0.2 - ); - assert_eq!( - rhombus.closest_point(Vec2::new(-0.55, 0.35)), - Vec2::new(-0.5, 0.25) - ); - - let rhombus = Rhombus::new(0.0, 0.0); - assert_eq!(rhombus.closest_point(Vec2::X * 10.0), Vec2::ZERO); - assert_eq!(rhombus.closest_point(Vec2::NEG_ONE * 0.2), Vec2::ZERO); - assert_eq!(rhombus.closest_point(Vec2::new(-0.55, 0.35)), Vec2::ZERO); - } - - #[test] - fn segment_closest_point() { - assert_eq!( - Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(3.0, 0.0)) - .closest_point(Vec2::new(1.0, 6.0)), - Vec2::new(1.0, 0.0) - ); - - let segments = [ - Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0)), - Segment2d::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)), - Segment2d::new(Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)), - Segment2d::new(Vec2::new(1.0, 0.0), Vec2::new(1.0, 5.0 * f32::EPSILON)), - ]; - let points = [ - Vec2::new(0.0, 0.0), - Vec2::new(1.0, 0.0), - Vec2::new(-1.0, 1.0), - Vec2::new(1.0, 1.0), - Vec2::new(-1.0, 0.0), - Vec2::new(5.0, -1.0), - Vec2::new(1.0, f32::EPSILON), - ]; - - for point in points.iter() { - for segment in segments.iter() { - let closest = segment.closest_point(*point); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.point1()), - "Closest point must always be at least as close as either vertex." - ); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.point2()), - "Closest point must always be at least as close as either vertex." - ); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.center()), - "Closest point must always be at least as close as the center." - ); - let closest_to_closest = segment.closest_point(closest); - // Closest point must already be on the segment - assert_relative_eq!(closest_to_closest, closest); - } - } - } - - #[test] - 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] - fn capsule_math() { - let capsule = Capsule2d::new(2.0, 9.0); - assert_eq!( - capsule.to_inner_rectangle(), - 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] - fn annulus_math() { - 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"); - let rhombus = Rhombus::from_side(core::f32::consts::SQRT_2); - assert_abs_diff_eq!(rhombus.half_diagonals, Vec2::new(1.0, 1.0)); - assert_abs_diff_eq!( - rhombus.half_diagonals, - Rhombus::from_inradius(FRAC_1_SQRT_2).half_diagonals - ); - } - - #[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"); - - let line = Ellipse::new(1., 0.); - assert_eq!(line.eccentricity(), 1., "incorrect line eccentricity"); - - let circle = Ellipse::new(2., 2.); - 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()); - - let acute_triangle = - Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 5.)); - let obtuse_triangle = - Triangle2d::new(Vec2::new(-1., 0.), Vec2::new(1., 0.), Vec2::new(0., 0.5)); - - assert!(acute_triangle.is_acute()); - assert!(!acute_triangle.is_obtuse()); - assert!(!obtuse_triangle.is_acute()); - assert!(obtuse_triangle.is_obtuse()); - } - - #[test] - fn triangle_winding_order() { - let mut cw_triangle = Triangle2d::new( - Vec2::new(0.0, 2.0), - Vec2::new(-0.5, -1.2), - Vec2::new(-1.0, -1.0), - ); - assert_eq!(cw_triangle.winding_order(), WindingOrder::Clockwise); - - let ccw_triangle = Triangle2d::new( - Vec2::new(-1.0, -1.0), - Vec2::new(-0.5, -1.2), - Vec2::new(0.0, 2.0), - ); - assert_eq!(ccw_triangle.winding_order(), WindingOrder::CounterClockwise); - - // The clockwise triangle should be the same as the counterclockwise - // triangle when reversed - cw_triangle.reverse(); - assert_eq!(cw_triangle, ccw_triangle); - - let invalid_triangle = Triangle2d::new( - Vec2::new(0.0, 2.0), - Vec2::new(0.0, -1.0), - Vec2::new(0.0, -1.2), - ); - assert_eq!(invalid_triangle.winding_order(), WindingOrder::Invalid); - } - - #[test] - fn rectangle_math() { - let rectangle = Rectangle::new(3.0, 7.0); - assert_eq!( - 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] - fn regular_polygon_math() { - 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, - "incorrect internal angle" - ); - assert_eq!( - polygon.internal_angle_radians(), - 120_f32.to_radians(), - "incorrect internal angle" - ); - assert_eq!( - polygon.external_angle_degrees(), - 60.0, - "incorrect external angle" - ); - assert_eq!( - polygon.external_angle_radians(), - 60_f32.to_radians(), - "incorrect external angle" - ); - } - - #[test] - fn triangle_circumcenter() { - let triangle = Triangle2d::new( - Vec2::new(10.0, 2.0), - Vec2::new(-5.0, -3.0), - Vec2::new(2.0, -1.0), - ); - let (Circle { radius }, circumcenter) = triangle.circumcircle(); - - // Calculated with external calculator - assert_eq!(radius, 98.34887); - assert_eq!(circumcenter, Vec2::new(-28.5, 92.5)); - } - - #[test] - fn regular_polygon_vertices() { - let polygon = RegularPolygon::new(1.0, 4); - - // Regular polygons have a vertex at the top by default - let mut vertices = polygon.vertices(0.0).into_iter(); - assert!((vertices.next().unwrap() - Vec2::Y).length() < 1e-7); - - // Rotate by 45 degrees, forming an axis-aligned square - let mut rotated_vertices = polygon.vertices(core::f32::consts::FRAC_PI_4).into_iter(); - - // Distance from the origin to the middle of a side, derived using Pythagorean theorem - let side_distance = FRAC_1_SQRT_2; - assert!( - (rotated_vertices.next().unwrap() - Vec2::new(-side_distance, side_distance)).length() - < 1e-7, - ); - } -} diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs deleted file mode 100644 index 8764efcff4ce5..0000000000000 --- a/crates/bevy_math/src/primitives/dim3.rs +++ /dev/null @@ -1,2063 +0,0 @@ -use core::f32::consts::{FRAC_PI_3, PI}; - -use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; -use crate::{ - ops::{self, FloatPow}, - Dir3, InvalidDirectionError, Isometry3d, Mat3, 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; - -/// A sphere primitive, representing the set of all points some distance from the origin -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Sphere { - /// The radius of the sphere - pub radius: f32, -} - -impl Primitive3d for Sphere {} - -impl Default for Sphere { - /// Returns the default [`Sphere`] with a radius of `0.5`. - fn default() -> Self { - Self { radius: 0.5 } - } -} - -impl Sphere { - /// Create a new [`Sphere`] from a `radius` - #[inline] - pub const fn new(radius: f32) -> Self { - Self { radius } - } - - /// Get the diameter of the sphere - #[inline] - pub const fn diameter(&self) -> f32 { - 2.0 * self.radius - } - - /// Finds the point on the sphere that is closest to the given `point`. - /// - /// If the point is outside the sphere, the returned point will be on the surface of the sphere. - /// Otherwise, it will be inside the sphere and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec3) -> Vec3 { - let distance_squared = point.length_squared(); - - if distance_squared <= self.radius.squared() { - // The point is inside the sphere. - point - } else { - // The point is outside the sphere. - // Find the closest point on the surface of the sphere. - let dir_to_point = point / ops::sqrt(distance_squared); - self.radius * dir_to_point - } - } -} - -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))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Plane3d { - /// The normal of the plane. The plane will be placed perpendicular to this direction - pub normal: Dir3, - /// Half of the width and height of the plane - pub half_size: Vec2, -} - -impl Primitive3d for Plane3d {} - -impl Default for Plane3d { - /// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`. - fn default() -> Self { - Self { - normal: Dir3::Y, - half_size: Vec2::splat(0.5), - } - } -} - -impl Plane3d { - /// Create a new `Plane3d` from a normal and a half size - /// - /// # Panics - /// - /// Panics if the given `normal` is zero (or very close to zero), or non-finite. - #[inline] - pub fn new(normal: Vec3, half_size: Vec2) -> Self { - Self { - normal: Dir3::new(normal).expect("normal must be nonzero and finite"), - half_size, - } - } - - /// Create a new `Plane3d` based on three points and compute the geometric center - /// of those points. - /// - /// The direction of the plane normal is determined by the winding order - /// of the triangular shape formed by the points. - /// - /// # Panics - /// - /// Panics if a valid normal can not be computed, for example when the points - /// are *collinear* and lie on the same line. - #[inline] - pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { - let normal = Dir3::new((b - a).cross(c - a)).expect( - "finite plane must be defined by three finite points that don't lie on the same line", - ); - let translation = (a + b + c) / 3.0; - - ( - Self { - normal, - ..Default::default() - }, - translation, - ) - } -} -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)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct InfinitePlane3d { - /// The normal of the plane. The plane will be placed perpendicular to this direction - pub normal: Dir3, -} - -impl Primitive3d for InfinitePlane3d {} - -impl Default for InfinitePlane3d { - /// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction. - fn default() -> Self { - Self { normal: Dir3::Y } - } -} - -impl InfinitePlane3d { - /// Create a new `InfinitePlane3d` from a normal - /// - /// # Panics - /// - /// Panics if the given `normal` is zero (or very close to zero), or non-finite. - #[inline] - pub fn new>(normal: T) -> Self - where - >::Error: core::fmt::Debug, - { - Self { - normal: normal - .try_into() - .expect("normal must be nonzero and finite"), - } - } - - /// Create a new `InfinitePlane3d` based on three points and compute the geometric center - /// of those points. - /// - /// The direction of the plane normal is determined by the winding order - /// of the triangular shape formed by the points. - /// - /// # Panics - /// - /// Panics if a valid normal can not be computed, for example when the points - /// are *collinear* and lie on the same line. - #[inline] - pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { - let normal = Dir3::new((b - a).cross(c - a)).expect( - "infinite plane must be defined by three finite points that don't lie on the same line", - ); - let translation = (a + b + c) / 3.0; - - (Self { normal }, translation) - } - - /// Computes the shortest distance between a plane transformed with the given `isometry` and a - /// `point`. The result is a signed value; it's positive if the point lies in the half-space - /// that the plane's normal vector points towards. - #[inline] - pub fn signed_distance(&self, isometry: impl Into, point: Vec3) -> f32 { - let isometry = isometry.into(); - self.normal.dot(isometry.inverse() * point) - } - - /// Injects the `point` into this plane transformed with the given `isometry`. - /// - /// This projects the point orthogonally along the shortest path onto the plane. - #[inline] - pub fn project_point(&self, isometry: impl Into, point: Vec3) -> Vec3 { - point - self.normal * self.signed_distance(isometry, point) - } - - /// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given - /// `origin` to the XY-plane. - /// - /// ## Guarantees - /// - /// * the transformation is a [congruence] meaning it will preserve all distances and angles of - /// the transformed geometry - /// * uses the least rotation possible to transform the geometry - /// * if two geometries are transformed with the same isometry, then the relations between - /// them, like distances, are also preserved - /// * compared to projections, the transformation is lossless (up to floating point errors) - /// reversible - /// - /// ## Non-Guarantees - /// - /// * the rotation used is generally not unique - /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to - /// enforce some kind of alignment the user has to use an extra transformation ontop of this - /// one - /// - /// See [`isometries_xy`] for example usescases. - /// - /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) - /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` - #[inline] - pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d { - let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z); - let transformed_origin = rotation * origin; - Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation) - } - - /// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the - /// given `origin`. - /// - /// ## Guarantees - /// - /// * the transformation is a [congruence] meaning it will preserve all distances and angles of - /// the transformed geometry - /// * uses the least rotation possible to transform the geometry - /// * if two geometries are transformed with the same isometry, then the relations between - /// them, like distances, are also preserved - /// * compared to projections, the transformation is lossless (up to floating point errors) - /// reversible - /// - /// ## Non-Guarantees - /// - /// * the rotation used is generally not unique - /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to - /// enforce some kind of alignment the user has to use an extra transformation ontop of this - /// one - /// - /// See [`isometries_xy`] for example usescases. - /// - /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) - /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` - #[inline] - pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d { - self.isometry_into_xy(origin).inverse() - } - - /// Computes both [isometries] which transforms points from the plane in 3D space with the - /// given `origin` to the XY-plane and back. - /// - /// [isometries]: `Isometry3d` - /// - /// # Example - /// - /// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The - /// workflow would usually look like this: - /// - /// ``` - /// # use bevy_math::{Vec3, Dir3}; - /// # use bevy_math::primitives::InfinitePlane3d; - /// - /// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z]; - /// let center = (a + b + c) / 3.0; - /// - /// let plane = InfinitePlane3d::new(Vec3::ONE); - /// - /// let (to_xy, from_xy) = plane.isometries_xy(center); - /// - /// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate()); - /// - /// // apply some algorithm to `triangle_2d` - /// - /// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3); - /// ``` - #[inline] - pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) { - let projection = self.isometry_into_xy(origin); - (projection, projection.inverse()) - } -} - -/// An infinite line going through the origin along a direction in 3D space. -/// -/// For a finite line: [`Segment3d`] -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Line3d { - /// The direction of the line - pub direction: Dir3, -} - -impl Primitive3d for Line3d {} - -/// A line segment defined by two endpoints in 3D space. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -#[doc(alias = "LineSegment3d")] -pub struct Segment3d { - /// The endpoints of the line segment. - pub vertices: [Vec3; 2], -} - -impl Primitive3d for Segment3d {} - -impl Default for Segment3d { - fn default() -> Self { - Self { - vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)], - } - } -} - -impl Segment3d { - /// Create a new `Segment3d` from its endpoints. - #[inline] - pub const fn new(point1: Vec3, point2: Vec3) -> Self { - Self { - vertices: [point1, point2], - } - } - - /// Create a new `Segment3d` centered at the origin with the given direction and length. - /// - /// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`. - #[inline] - pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self { - let endpoint = 0.5 * length * direction; - Self { - vertices: [-endpoint, endpoint], - } - } - - /// Create a new `Segment3d` centered at the origin from a vector representing - /// the direction and length of the line segment. - /// - /// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`. - #[inline] - pub fn from_scaled_direction(scaled_direction: Vec3) -> Self { - let endpoint = 0.5 * scaled_direction; - Self { - vertices: [-endpoint, endpoint], - } - } - - /// Create a new `Segment3d` starting from the origin of the given `ray`, - /// going in the direction of the ray for the given `length`. - /// - /// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`. - #[inline] - pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self { - Self { - vertices: [ray.origin, ray.get_point(length)], - } - } - - /// Get the position of the first endpoint of the line segment. - #[inline] - pub const fn point1(&self) -> Vec3 { - self.vertices[0] - } - - /// Get the position of the second endpoint of the line segment. - #[inline] - pub const fn point2(&self) -> Vec3 { - self.vertices[1] - } - - /// Compute the midpoint between the two endpoints of the line segment. - #[inline] - #[doc(alias = "midpoint")] - pub fn center(&self) -> Vec3 { - self.point1().midpoint(self.point2()) - } - - /// Compute the length of the line segment. - #[inline] - pub fn length(&self) -> f32 { - self.point1().distance(self.point2()) - } - - /// Compute the squared length of the line segment. - #[inline] - pub fn length_squared(&self) -> f32 { - self.point1().distance_squared(self.point2()) - } - - /// Compute the normalized direction pointing from the first endpoint to the second endpoint. - /// - /// For the non-panicking version, see [`Segment3d::try_direction`]. - /// - /// # Panics - /// - /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn direction(&self) -> Dir3 { - self.try_direction().unwrap_or_else(|err| { - panic!("Failed to compute the direction of a line segment: {err}") - }) - } - - /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. - /// - /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed, - /// for example when the endpoints are coincident, NaN, or infinite. - #[inline] - pub fn try_direction(&self) -> Result { - Dir3::new(self.scaled_direction()) - } - - /// Compute the vector from the first endpoint to the second endpoint. - #[inline] - pub fn scaled_direction(&self) -> Vec3 { - self.point2() - self.point1() - } - - /// Compute the segment transformed by the given [`Isometry3d`]. - #[inline] - pub fn transformed(&self, isometry: impl Into) -> Self { - let isometry: Isometry3d = isometry.into(); - Self::new( - isometry.transform_point(self.point1()).into(), - isometry.transform_point(self.point2()).into(), - ) - } - - /// Compute the segment translated by the given vector. - #[inline] - pub fn translated(&self, translation: Vec3) -> Segment3d { - Self::new(self.point1() + translation, self.point2() + translation) - } - - /// Compute the segment rotated around the origin by the given rotation. - #[inline] - pub fn rotated(&self, rotation: Quat) -> Segment3d { - Segment3d::new(rotation * self.point1(), rotation * self.point2()) - } - - /// Compute the segment rotated around the given point by the given rotation. - #[inline] - pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d { - // We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back - let offset = self.translated(-point); - let rotated = offset.rotated(rotation); - rotated.translated(point) - } - - /// Compute the segment rotated around its own center. - #[inline] - pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d { - self.rotated_around(rotation, self.center()) - } - - /// Compute the segment with its center at the origin, keeping the same direction and length. - #[inline] - pub fn centered(&self) -> Segment3d { - let center = self.center(); - self.translated(-center) - } - - /// Compute the segment with a new length, keeping the same direction and center. - #[inline] - pub fn resized(&self, length: f32) -> Segment3d { - let offset_from_origin = self.center(); - let centered = self.translated(-offset_from_origin); - let ratio = length / self.length(); - let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio); - segment.translated(offset_from_origin) - } - - /// Reverses the direction of the line segment by swapping the endpoints. - #[inline] - pub const fn reverse(&mut self) { - let [point1, point2] = &mut self.vertices; - core::mem::swap(point1, point2); - } - - /// Returns the line segment with its direction reversed by swapping the endpoints. - #[inline] - #[must_use] - pub fn reversed(mut self) -> Self { - self.reverse(); - self - } - - /// Returns the point on the [`Segment3d`] that is closest to the specified `point`. - #[inline] - pub fn closest_point(&self, point: Vec3) -> Vec3 { - // `point` - // x - // ^| - // / | - //`offset`/ | - // / | `segment_vector` - // x----.-------------->x - // 0 t 1 - let segment_vector = self.vertices[1] - self.vertices[0]; - let offset = point - self.vertices[0]; - // The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment. - let projection_scaled = segment_vector.dot(offset); - - // `point` is too far "left" in the picture - if projection_scaled <= 0.0 { - return self.vertices[0]; - } - - let length_squared = segment_vector.length_squared(); - // `point` is too far "right" in the picture - if projection_scaled >= length_squared { - return self.vertices[1]; - } - - // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. - let t = projection_scaled / length_squared; - self.vertices[0] + t * segment_vector - } -} - -impl From<[Vec3; 2]> for Segment3d { - #[inline] - fn from(vertices: [Vec3; 2]) -> Self { - Self { vertices } - } -} - -impl From<(Vec3, Vec3)> for Segment3d { - #[inline] - fn from((point1, point2): (Vec3, Vec3)) -> Self { - Self::new(point1, point2) - } -} - -/// A series of connected line segments in 3D space. -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Polyline3d { - /// The vertices of the polyline - pub vertices: Vec, -} - -#[cfg(feature = "alloc")] -impl Primitive3d for Polyline3d {} - -#[cfg(feature = "alloc")] -impl FromIterator for Polyline3d { - fn from_iter>(iter: I) -> Self { - Self { - vertices: iter.into_iter().collect(), - } - } -} - -#[cfg(feature = "alloc")] -impl Default for Polyline3d { - fn default() -> Self { - Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)]) - } -} - -#[cfg(feature = "alloc")] -impl Polyline3d { - /// Create a new `Polyline3d` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) - } - - /// Create a new `Polyline3d` from two endpoints with subdivision points. - /// `subdivisions = 0` creates a simple line with just start and end points. - /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. - pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self { - let total_vertices = subdivisions + 2; - let mut vertices = Vec::with_capacity(total_vertices); - - let step = (end - start) / (subdivisions + 1) as f32; - for i in 0..total_vertices { - vertices.push(start + step * i as f32); - } - - Self { vertices } - } -} - -/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not -/// required to be the same. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Cuboid { - /// Half of the width, height and depth of the cuboid - pub half_size: Vec3, -} - -impl Primitive3d for Cuboid {} - -impl Default for Cuboid { - /// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`. - fn default() -> Self { - Self { - half_size: Vec3::splat(0.5), - } - } -} - -impl Cuboid { - /// Create a new `Cuboid` from a full x, y, and z length - #[inline] - pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self { - Self::from_size(Vec3::new(x_length, y_length, z_length)) - } - - /// Create a new `Cuboid` from a given full size - #[inline] - pub const fn from_size(size: Vec3) -> Self { - Self { - half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0), - } - } - - /// Create a new `Cuboid` from two corner points - #[inline] - pub fn from_corners(point1: Vec3, point2: Vec3) -> Self { - Self { - half_size: (point2 - point1).abs() / 2.0, - } - } - - /// Create a `Cuboid` from a single length. - /// The resulting `Cuboid` will be the same size in every direction. - #[inline] - pub const fn from_length(length: f32) -> Self { - Self { - half_size: Vec3::splat(length / 2.0), - } - } - - /// Get the size of the cuboid - #[inline] - pub fn size(&self) -> Vec3 { - 2.0 * self.half_size - } - - /// Finds the point on the cuboid that is closest to the given `point`. - /// - /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. - /// Otherwise, it will be inside the cuboid and returned as is. - #[inline] - pub fn closest_point(&self, point: Vec3) -> Vec3 { - // Clamp point coordinates to the cuboid - point.clamp(-self.half_size, self.half_size) - } -} - -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))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Cylinder { - /// The radius of the cylinder - pub radius: f32, - /// The half height of the cylinder - pub half_height: f32, -} - -impl Primitive3d for Cylinder {} - -impl Default for Cylinder { - /// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`. - fn default() -> Self { - Self { - radius: 0.5, - half_height: 0.5, - } - } -} - -impl Cylinder { - /// Create a new `Cylinder` from a radius and full height - #[inline] - pub const fn new(radius: f32, height: f32) -> Self { - Self { - radius, - half_height: height / 2.0, - } - } - - /// Get the base of the cylinder as a [`Circle`] - #[inline] - pub const fn base(&self) -> Circle { - Circle { - radius: self.radius, - } - } - - /// Get the surface area of the side of the cylinder, - /// also known as the lateral area - #[inline] - #[doc(alias = "side_area")] - pub const fn lateral_area(&self) -> f32 { - 4.0 * PI * self.radius * self.half_height - } - - /// Get the surface area of one base of the cylinder - #[inline] - pub fn base_area(&self) -> f32 { - PI * self.radius.squared() - } -} - -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)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Capsule3d { - /// The radius of the capsule - pub radius: f32, - /// Half the height of the capsule, excluding the hemispheres - pub half_length: f32, -} - -impl Primitive3d for Capsule3d {} - -impl Default for Capsule3d { - /// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`. - /// The total height is `2.0`. - fn default() -> Self { - Self { - radius: 0.5, - half_length: 0.5, - } - } -} - -impl Capsule3d { - /// Create a new `Capsule3d` from a radius and length - pub const fn new(radius: f32, length: f32) -> Self { - Self { - radius, - half_length: length / 2.0, - } - } - - /// Get the part connecting the hemispherical ends - /// of the capsule as a [`Cylinder`] - #[inline] - pub const fn to_cylinder(&self) -> Cylinder { - Cylinder { - radius: self.radius, - half_height: self.half_length, - } - } -} - -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. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Cone { - /// The radius of the base - pub radius: f32, - /// The height of the cone - pub height: f32, -} - -impl Primitive3d for Cone {} - -impl Default for Cone { - /// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`. - fn default() -> Self { - Self { - radius: 0.5, - height: 1.0, - } - } -} - -impl Cone { - /// Create a new [`Cone`] from a radius and height. - pub const fn new(radius: f32, height: f32) -> Self { - Self { radius, height } - } - /// Get the base of the cone as a [`Circle`] - #[inline] - pub const fn base(&self) -> Circle { - Circle { - radius: self.radius, - } - } - - /// Get the slant height of the cone, the length of the line segment - /// connecting a point on the base to the apex - #[inline] - #[doc(alias = "side_length")] - pub fn slant_height(&self) -> f32 { - ops::hypot(self.radius, self.height) - } - - /// Get the surface area of the side of the cone, - /// also known as the lateral area - #[inline] - #[doc(alias = "side_area")] - pub fn lateral_area(&self) -> f32 { - PI * self.radius * self.slant_height() - } - - /// Get the surface area of the base of the cone - #[inline] - pub fn base_area(&self) -> f32 { - PI * self.radius.squared() - } -} - -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. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct ConicalFrustum { - /// The radius of the top of the frustum - pub radius_top: f32, - /// The radius of the base of the frustum - pub radius_bottom: f32, - /// The height of the frustum - pub height: f32, -} - -impl Primitive3d for ConicalFrustum {} - -impl Default for ConicalFrustum { - /// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`. - fn default() -> Self { - Self { - radius_top: 0.25, - radius_bottom: 0.5, - height: 0.5, - } - } -} - -impl ConicalFrustum { - /// Get the bottom base of the conical frustum as a [`Circle`] - #[inline] - pub const fn bottom_base(&self) -> Circle { - Circle { - radius: self.radius_bottom, - } - } - - /// Get the top base of the conical frustum as a [`Circle`] - #[inline] - pub const fn top_base(&self) -> Circle { - Circle { - radius: self.radius_top, - } - } - - /// Get the slant height of the conical frustum, the length of the line segment - /// connecting a point on the base to the closest point on the top - #[inline] - #[doc(alias = "side_length")] - pub fn slant_height(&self) -> f32 { - ops::hypot(self.radius_bottom - self.radius_top, self.height) - } - - /// Get the surface area of the side of the conical frustum, - /// also known as the lateral area - #[inline] - #[doc(alias = "side_area")] - pub fn lateral_area(&self) -> f32 { - PI * (self.radius_bottom + self.radius_top) * self.slant_height() - } - - /// Get the surface area of the bottom base of the conical frustum - #[inline] - pub fn bottom_base_area(&self) -> f32 { - PI * self.radius_bottom.squared() - } - - /// Get the surface area of the top base of the conical frustum - #[inline] - pub fn top_base_area(&self) -> f32 { - PI * self.radius_top.squared() - } -} - -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 { - /// A torus that has a ring. - /// The major radius is greater than the minor radius - Ring, - /// A torus that has no hole but also doesn't intersect itself. - /// The major radius is equal to the minor radius - Horn, - /// A self-intersecting torus. - /// The major radius is less than the minor radius - Spindle, - /// A torus with non-geometric properties like - /// a minor or major radius that is non-positive, - /// infinite, or `NaN` - Invalid, -} - -/// A torus primitive, often representing a ring or donut shape -/// The set of points some distance from a circle centered at the origin -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Torus { - /// The radius of the tube of the torus - #[doc( - alias = "ring_radius", - alias = "tube_radius", - alias = "cross_section_radius" - )] - pub minor_radius: f32, - /// The distance from the center of the torus to the center of the tube - #[doc(alias = "radius_of_revolution")] - pub major_radius: f32, -} - -impl Primitive3d for Torus {} - -impl Default for Torus { - /// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`. - fn default() -> Self { - Self { - minor_radius: 0.25, - major_radius: 0.75, - } - } -} - -impl Torus { - /// Create a new `Torus` from an inner and outer radius. - /// - /// The inner radius is the radius of the hole, and the outer radius - /// is the radius of the entire object - #[inline] - pub const fn new(inner_radius: f32, outer_radius: f32) -> Self { - let minor_radius = (outer_radius - inner_radius) / 2.0; - let major_radius = outer_radius - minor_radius; - - Self { - minor_radius, - major_radius, - } - } - - /// Get the inner radius of the torus. - /// For a ring torus, this corresponds to the radius of the hole, - /// or `major_radius - minor_radius` - #[inline] - pub const fn inner_radius(&self) -> f32 { - self.major_radius - self.minor_radius - } - - /// Get the outer radius of the torus. - /// This corresponds to the overall radius of the entire object, - /// or `major_radius + minor_radius` - #[inline] - pub const fn outer_radius(&self) -> f32 { - self.major_radius + self.minor_radius - } - - /// Get the [`TorusKind`] determined by the minor and major radii. - /// - /// The torus can either be a *ring torus* that has a hole, - /// a *horn torus* that doesn't have a hole but also isn't self-intersecting, - /// or a *spindle torus* that is self-intersecting. - /// - /// If the minor or major radius is non-positive, infinite, or `NaN`, - /// [`TorusKind::Invalid`] is returned - #[inline] - pub fn kind(&self) -> TorusKind { - // Invalid if minor or major radius is non-positive, infinite, or NaN - if self.minor_radius <= 0.0 - || !self.minor_radius.is_finite() - || self.major_radius <= 0.0 - || !self.major_radius.is_finite() - { - return TorusKind::Invalid; - } - - match self.major_radius.partial_cmp(&self.minor_radius).unwrap() { - core::cmp::Ordering::Greater => TorusKind::Ring, - core::cmp::Ordering::Equal => TorusKind::Horn, - core::cmp::Ordering::Less => TorusKind::Spindle, - } - } -} - -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))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Triangle3d { - /// The vertices of the triangle. - pub vertices: [Vec3; 3], -} - -impl Primitive3d for Triangle3d {} - -impl Default for Triangle3d { - /// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`. - fn default() -> Self { - Self { - vertices: [ - Vec3::new(0.0, 0.5, 0.0), - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - ], - } - } -} - -impl Triangle3d { - /// Create a new [`Triangle3d`] from points `a`, `b`, and `c`. - #[inline] - pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self { - Self { - vertices: [a, b, c], - } - } - - /// Get the normal of the triangle in the direction of the right-hand rule, assuming - /// the vertices are ordered in a counter-clockwise direction. - /// - /// The normal is computed as the cross product of the vectors `ab` and `ac`. - /// - /// # Errors - /// - /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length - /// of the given vector is zero (or very close to zero), infinite, or `NaN`. - #[inline] - pub fn normal(&self) -> Result { - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - Dir3::new(ab.cross(ac)) - } - - /// Checks if the triangle is degenerate, meaning it has zero area. - /// - /// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`. - /// This indicates that the three vertices are collinear or nearly collinear. - #[inline] - pub fn is_degenerate(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - ab.cross(ac).length() < 10e-7 - } - - /// Checks if the triangle is acute, meaning all angles are less than 90 degrees - #[inline] - pub fn is_acute(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = b - a; - let bc = c - b; - let ca = a - c; - - // a^2 + b^2 < c^2 for an acute triangle - let side_lengths = [ - ab.length_squared(), - bc.length_squared(), - ca.length_squared(), - ]; - let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; - let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); - sum - max > max - } - - /// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees - #[inline] - pub fn is_obtuse(&self) -> bool { - let [a, b, c] = self.vertices; - let ab = b - a; - let bc = c - b; - let ca = a - c; - - // a^2 + b^2 > c^2 for an obtuse triangle - let side_lengths = [ - ab.length_squared(), - bc.length_squared(), - ca.length_squared(), - ]; - let sum = side_lengths[0] + side_lengths[1] + side_lengths[2]; - let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]); - sum - max < max - } - - /// Reverse the triangle by swapping the first and last vertices. - #[inline] - pub const fn reverse(&mut self) { - self.vertices.swap(0, 2); - } - - /// This triangle but reversed. - #[inline] - #[must_use] - pub const fn reversed(mut self) -> Triangle3d { - self.reverse(); - self - } - - /// Get the centroid of the triangle. - /// - /// This function finds the geometric center of the triangle by averaging the vertices: - /// `centroid = (a + b + c) / 3`. - #[doc(alias("center", "barycenter", "baricenter"))] - #[inline] - pub fn centroid(&self) -> Vec3 { - (self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0 - } - - /// Get the largest side of the triangle. - /// - /// Returns the two points that form the largest side of the triangle. - #[inline] - pub fn largest_side(&self) -> (Vec3, Vec3) { - let [a, b, c] = self.vertices; - let ab = b - a; - let bc = c - b; - let ca = a - c; - - let mut largest_side_points = (a, b); - let mut largest_side_length = ab.length_squared(); - - let bc_length = bc.length_squared(); - if bc_length > largest_side_length { - largest_side_points = (b, c); - largest_side_length = bc_length; - } - - let ca_length = ca.length_squared(); - if ca_length > largest_side_length { - largest_side_points = (a, c); - } - - largest_side_points - } - - /// Get the circumcenter of the triangle. - #[inline] - pub fn circumcenter(&self) -> Vec3 { - if self.is_degenerate() { - // If the triangle is degenerate, the circumcenter is the midpoint of the largest side. - let (p1, p2) = self.largest_side(); - return (p1 + p2) / 2.0; - } - - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - let n = ab.cross(ac); - - // Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d - a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac)) - / (2.0 * n.length_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) - } -} - -/// A tetrahedron primitive. -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Debug, PartialEq, Default, Clone) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct Tetrahedron { - /// The vertices of the tetrahedron. - pub vertices: [Vec3; 4], -} - -impl Primitive3d for Tetrahedron {} - -impl Default for Tetrahedron { - /// Returns the default [`Tetrahedron`] with the vertices - /// `[0.5, 0.5, 0.5]`, `[-0.5, 0.5, -0.5]`, `[-0.5, -0.5, 0.5]` and `[0.5, -0.5, -0.5]`. - fn default() -> Self { - Self { - vertices: [ - Vec3::new(0.5, 0.5, 0.5), - Vec3::new(-0.5, 0.5, -0.5), - Vec3::new(-0.5, -0.5, 0.5), - Vec3::new(0.5, -0.5, -0.5), - ], - } - } -} - -impl Tetrahedron { - /// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`. - #[inline] - pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self { - Self { - vertices: [a, b, c, d], - } - } - - /// Get the signed volume of the tetrahedron. - /// - /// If it's negative, the normal vector of the face defined by - /// the first three points using the right-hand rule points - /// away from the fourth vertex. - #[inline] - pub fn signed_volume(&self) -> f32 { - let [a, b, c, d] = self.vertices; - let ab = b - a; - let ac = c - a; - let ad = d - a; - Mat3::from_cols(ab, ac, ad).determinant() / 6.0 - } - - /// Get the centroid of the tetrahedron. - /// - /// This function finds the geometric center of the tetrahedron - /// by averaging the vertices: `centroid = (a + b + c + d) / 4`. - #[doc(alias("center", "barycenter", "baricenter"))] - #[inline] - pub fn centroid(&self) -> Vec3 { - (self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0 - } - - /// Get the triangles that form the faces of this tetrahedron. - /// - /// Note that the orientations of the faces are determined by that of the tetrahedron; if the - /// signed volume of this tetrahedron is positive, then the triangles' normals will point - /// outward, and if the signed volume is negative they will point inward. - #[inline] - pub fn faces(&self) -> [Triangle3d; 4] { - let [a, b, c, d] = self.vertices; - [ - Triangle3d::new(b, c, d), - Triangle3d::new(a, c, d).reversed(), - Triangle3d::new(a, b, d), - Triangle3d::new(a, b, c).reversed(), - ] - } -} - -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, -/// creating a shape with the same cross-section over the entire depth. -/// -/// The resulting volumes are prisms. -/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder. -#[doc(alias = "Prism")] -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct Extrusion { - /// The base shape of the extrusion - pub base_shape: T, - /// Half of the depth of the extrusion - pub half_depth: f32, -} - -impl Primitive3d for Extrusion {} - -impl Extrusion { - /// Create a new `Extrusion` from a given `base_shape` and `depth` - pub fn new(base_shape: T, depth: f32) -> Self { - Self { - base_shape, - half_depth: depth / 2., - } - } -} - -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; - - #[test] - fn direction_creation() { - assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X)); - assert_eq!( - Dir3::new(Vec3::new(0.0, 0.0, 0.0)), - Err(InvalidDirectionError::Zero) - ); - assert_eq!( - Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)), - Err(InvalidDirectionError::Infinite) - ); - assert_eq!( - Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)), - Err(InvalidDirectionError::Infinite) - ); - assert_eq!( - Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)), - Err(InvalidDirectionError::NaN) - ); - assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5))); - - // Test rotation - assert!( - (Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X) - .abs_diff_eq(Vec3::Y, 10e-6) - ); - } - - #[test] - fn cuboid_closest_point() { - let cuboid = Cuboid::new(2.0, 2.0, 2.0); - assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X); - assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE); - assert_eq!( - cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)), - Vec3::new(0.25, 0.1, 0.3) - ); - } - - #[test] - fn sphere_closest_point() { - let sphere = Sphere { radius: 1.0 }; - assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X); - assert_eq!( - sphere.closest_point(Vec3::NEG_ONE * 10.0), - Vec3::NEG_ONE.normalize() - ); - assert_eq!( - sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)), - Vec3::new(0.25, 0.1, 0.3) - ); - } - - #[test] - fn segment_closest_point() { - assert_eq!( - Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0)) - .closest_point(Vec3::new(1.0, 6.0, -2.0)), - Vec3::new(1.0, 0.0, 0.0) - ); - - let segments = [ - Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)), - Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)), - Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)), - Segment3d::new( - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0), - ), - ]; - let points = [ - Vec3::new(0.0, 0.0, 0.0), - Vec3::new(1.0, 0.0, 0.0), - Vec3::new(-1.0, 1.0, 2.0), - Vec3::new(1.0, 1.0, 1.0), - Vec3::new(-1.0, 0.0, 0.0), - Vec3::new(5.0, -1.0, 0.5), - Vec3::new(1.0, f32::EPSILON, 0.0), - ]; - - for point in points.iter() { - for segment in segments.iter() { - let closest = segment.closest_point(*point); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.point1()), - "Closest point must always be at least as close as either vertex." - ); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.point2()), - "Closest point must always be at least as close as either vertex." - ); - assert!( - point.distance_squared(closest) <= point.distance_squared(segment.center()), - "Closest point must always be at least as close as the center." - ); - let closest_to_closest = segment.closest_point(closest); - // Closest point must already be on the segment - assert_relative_eq!(closest_to_closest, closest); - } - } - } - - #[test] - 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] - fn plane_from_points() { - let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); - assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); - assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size"); - assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); - } - - #[test] - fn infinite_plane_math() { - let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); - assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); - assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation"); - - let point_in_plane = Vec3::X + Vec3::Z; - assert_eq!( - plane.signed_distance(origin, point_in_plane), - 0.0, - "incorrect distance" - ); - assert_eq!( - plane.project_point(origin, point_in_plane), - point_in_plane, - "incorrect point" - ); - - let point_outside = Vec3::Y; - assert_eq!( - plane.signed_distance(origin, point_outside), - -1.0, - "incorrect distance" - ); - assert_eq!( - plane.project_point(origin, point_outside), - Vec3::ZERO, - "incorrect point" - ); - - let point_outside = Vec3::NEG_Y; - assert_eq!( - plane.signed_distance(origin, point_outside), - 1.0, - "incorrect distance" - ); - assert_eq!( - plane.project_point(origin, point_outside), - Vec3::ZERO, - "incorrect point" - ); - - let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5; - let (proj, inj) = plane.isometries_xy(origin); - - let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO]; - assert_eq!(area_f(triangle), 0.5, "incorrect area"); - - let triangle_proj = triangle.map(|vec3| proj * vec3); - assert_relative_eq!(area_f(triangle_proj), 0.5); - - let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3); - assert_relative_eq!(area_f(triangle_proj_inj), 0.5); - } - - #[test] - fn cuboid_math() { - let cuboid = Cuboid::new(3.0, 7.0, 2.0); - assert_eq!( - cuboid, - 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] - fn cylinder_math() { - let cylinder = Cylinder::new(2.0, 9.0); - assert_eq!( - cylinder.base(), - Circle { radius: 2.0 }, - "base produces incorrect circle" - ); - assert_eq!( - cylinder.lateral_area(), - 113.097336, - "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] - fn capsule_math() { - let capsule = Capsule3d::new(2.0, 9.0); - assert_eq!( - capsule.to_cylinder(), - 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] - fn cone_math() { - let cone = Cone { - radius: 2.0, - height: 9.0, - }; - assert_eq!( - cone.base(), - Circle { radius: 2.0 }, - "base produces incorrect circle" - ); - 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] - fn conical_frustum_math() { - let frustum = ConicalFrustum { - height: 9.0, - radius_top: 1.0, - radius_bottom: 2.0, - }; - assert_eq!( - frustum.bottom_base(), - Circle { radius: 2.0 }, - "bottom base produces incorrect circle" - ); - assert_eq!( - frustum.top_base(), - Circle { radius: 1.0 }, - "top base produces incorrect circle" - ); - assert_eq!(frustum.slant_height(), 9.055386, "incorrect slant height"); - assert_eq!(frustum.lateral_area(), 85.345, "incorrect lateral area"); - assert_eq!( - frustum.bottom_base_area(), - 12.566371, - "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] - fn torus_math() { - let torus = Torus { - minor_radius: 0.3, - major_radius: 2.8, - }; - assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius"); - assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius"); - assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind"); - assert_eq!( - Torus::new(0.0, 1.0).kind(), - TorusKind::Horn, - "incorrect torus kind" - ); - assert_eq!( - Torus::new(-0.5, 1.0).kind(), - TorusKind::Spindle, - "incorrect torus kind" - ); - assert_eq!( - Torus::new(1.5, 1.0).kind(), - TorusKind::Invalid, - "torus should be invalid" - ); - 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.signed_volume(), - 3.2058334, - "incorrect signed volume" - ); - 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, - "incorrect signed volume" - ); - 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 - let mut default_triangle = Triangle3d::default(); - let reverse_default_triangle = Triangle3d::new( - Vec3::new(0.5, -0.5, 0.0), - 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(), - "incorrect degenerate check" - ); - assert_eq!( - default_triangle.centroid(), - Vec3::new(0.0, -0.16666667, 0.0), - "incorrect centroid" - ); - assert_eq!( - default_triangle.largest_side(), - (Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0)) - ); - default_triangle.reverse(); - assert_eq!( - default_triangle, reverse_default_triangle, - "incorrect reverse" - ); - assert_eq!( - default_triangle.circumcenter(), - Vec3::new(0.0, -0.125, 0.0), - "incorrect circumcenter" - ); - - // Custom triangle tests - let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y); - let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0)); - let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0)); - - assert_eq!( - right_triangle.circumcenter(), - Vec3::new(0.5, 0.5, 0.0), - "incorrect circumcenter" - ); - assert_eq!( - obtuse_triangle.circumcenter(), - Vec3::new(0.0, -4.95, 0.0), - "incorrect circumcenter" - ); - assert_eq!( - acute_triangle.circumcenter(), - Vec3::new(0.5, 2.475, 0.0), - "incorrect circumcenter" - ); - - assert!(acute_triangle.is_acute()); - assert!(!acute_triangle.is_obtuse()); - assert!(!obtuse_triangle.is_acute()); - assert!(obtuse_triangle.is_obtuse()); - - // 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!(!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), - "incorrect circumcenter" - ); - assert_eq!( - triangle.normal(), - Ok(Dir3::new_unchecked(Vec3::new( - -0.04134491, - -0.4134491, - 0.90958804 - ))), - "incorrect normal" - ); - - // Degenerate triangle tests - let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); - assert!( - zero_degenerate_triangle.is_degenerate(), - "incorrect degenerate check" - ); - assert_eq!( - zero_degenerate_triangle.normal(), - Err(InvalidDirectionError::Zero), - "incorrect normal" - ); - assert_eq!( - zero_degenerate_triangle.largest_side(), - (Vec3::ZERO, Vec3::ZERO), - "incorrect largest side" - ); - - let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); - assert!( - dup_degenerate_triangle.is_degenerate(), - "incorrect degenerate check" - ); - assert_eq!( - dup_degenerate_triangle.normal(), - Err(InvalidDirectionError::Zero), - "incorrect normal" - ); - assert_eq!( - dup_degenerate_triangle.largest_side(), - (Vec3::ZERO, Vec3::X), - "incorrect largest side" - ); - - let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); - assert!( - collinear_degenerate_triangle.is_degenerate(), - "incorrect degenerate check" - ); - assert_eq!( - collinear_degenerate_triangle.normal(), - Err(InvalidDirectionError::Zero), - "incorrect normal" - ); - assert_eq!( - collinear_degenerate_triangle.largest_side(), - (Vec3::NEG_X, Vec3::X), - "incorrect largest side" - ); - } -} diff --git a/crates/bevy_math/src/primitives/inset.rs b/crates/bevy_math/src/primitives/inset.rs deleted file mode 100644 index 7690593ad5c66..0000000000000 --- a/crates/bevy_math/src/primitives/inset.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::{ - ops, - primitives::{ - Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, - Triangle2d, - }, - Vec2, -}; - -/// A primitive that can be resized uniformly. -/// -/// See documentation on [`Inset::inset`]. -/// -/// See also [`ToRing`](crate::primitives::ToRing). -pub trait Inset: Primitive2d { - /// Create a new version of this primitive that is resized uniformly. - /// That is, it resizes the shape inwards such that for the lines between vertices, - /// it creates new parallel lines that are `distance` inwards from the original lines. - /// - /// This is useful for creating smaller shapes or making outlines of `distance` thickness with [`Ring`](crate::primitives::Ring). - /// - /// See also [`ToRing::to_ring`](crate::primitives::ToRing::to_ring) - fn inset(self, distance: f32) -> Self; -} - -impl Inset for Circle { - fn inset(mut self, distance: f32) -> Self { - self.radius -= distance; - self - } -} - -impl Inset for Triangle2d { - fn inset(self, distance: f32) -> Self { - fn find_inset_point(a: Vec2, b: Vec2, c: Vec2, distance: f32) -> Vec2 { - let unit_vector_ab = (b - a).normalize(); - let unit_vector_ac = (c - a).normalize(); - let half_angle_bac = unit_vector_ab.angle_to(unit_vector_ac) / 2.0; - let mean = (unit_vector_ab + unit_vector_ac) / 2.0; - let direction = mean.normalize(); - let magnitude = distance / ops::sin(half_angle_bac); - a + direction * magnitude - } - - let [a, b, c] = self.vertices; - - let new_a = find_inset_point(a, b, c, distance); - let new_b = find_inset_point(b, c, a, distance); - let new_c = find_inset_point(c, a, b, distance); - - Self::new(new_a, new_b, new_c) - } -} - -impl Inset for Rhombus { - fn inset(mut self, distance: f32) -> Self { - let [half_width, half_height] = self.half_diagonals.into(); - let angle = ops::atan(half_height / half_width); - let x_offset = distance / ops::sin(angle); - let y_offset = distance / ops::cos(angle); - self.half_diagonals -= Vec2::new(x_offset, y_offset); - self - } -} - -impl Inset for Capsule2d { - fn inset(mut self, distance: f32) -> Self { - self.radius -= distance; - self - } -} - -impl Inset for Rectangle { - fn inset(mut self, distance: f32) -> Self { - self.half_size -= Vec2::splat(distance); - self - } -} - -impl Inset for CircularSegment { - fn inset(self, distance: f32) -> Self { - let old_arc = self.arc; - let radius = old_arc.radius - distance; - let apothem = old_arc.apothem() + distance; - // https://en.wikipedia.org/wiki/Circular_segment - let half_angle = ops::acos(apothem / radius); - Self::new(radius, half_angle) - } -} - -impl Inset for RegularPolygon { - fn inset(mut self, distance: f32) -> Self { - let half_angle = self.internal_angle_radians() / 2.0; - let offset = distance / ops::sin(half_angle); - self.circumcircle.radius -= offset; - self - } -} diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs deleted file mode 100644 index c2cb17f963123..0000000000000 --- a/crates/bevy_math/src/primitives/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This module defines primitive shapes. -//! The origin is (0, 0) for 2D primitives and (0, 0, 0) for 3D primitives, -//! unless stated otherwise. - -mod dim2; -pub use dim2::*; -mod dim3; -pub use dim3::*; -mod inset; -pub use inset::*; -mod half_space; -mod polygon; -pub use half_space::*; -mod view_frustum; -pub use view_frustum::*; - -/// A marker trait for 2D primitives -pub trait Primitive2d {} - -/// A marker trait for 3D primitives -pub trait Primitive3d {} - -/// The winding order for a set of points -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[doc(alias = "Orientation")] -pub enum WindingOrder { - /// A clockwise winding order - Clockwise, - /// A counterclockwise winding order - #[doc(alias = "AntiClockwise")] - CounterClockwise, - /// An invalid winding order indicating that it could not be computed reliably. - /// This often happens in *degenerate cases* where the points lie on the same line - #[doc(alias("Degenerate", "Collinear"))] - 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; -} diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_math/src/primitives/polygon.rs deleted file mode 100644 index 71197d3fd8101..0000000000000 --- a/crates/bevy_math/src/primitives/polygon.rs +++ /dev/null @@ -1,375 +0,0 @@ -#[cfg(feature = "alloc")] -use { - super::{Measured2d, Triangle2d}, - alloc::{collections::BTreeMap, vec::Vec}, - core::cmp::Ordering, -}; - -use crate::Vec2; - -#[derive(Debug, Clone, Copy)] -#[cfg(feature = "alloc")] -enum Endpoint { - Left, - Right, -} - -/// An event in the [`EventQueue`] is either the left or right vertex of an edge of the polygon. -/// -/// Events are ordered so that any event `e1` which is to the left of another event `e2` is less than that event. -/// 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, - /// Type of the vertex (left or right) - endpoint: Endpoint, -} - -#[cfg(feature = "alloc")] -impl SweepLineEvent { - const fn position(&self) -> Vec2 { - match self.endpoint { - Endpoint::Left => self.segment.left, - Endpoint::Right => self.segment.right, - } - } -} - -#[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()) - } -} - -/// 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`. - /// - /// The events in the event queue will be ordered. - fn new(vertices: &[Vec2]) -> Self { - if vertices.is_empty() { - return Self { events: Vec::new() }; - } - - let mut events = Vec::with_capacity(vertices.len() * 2); - for i in 0..vertices.len() { - let v1 = vertices[i]; - let v2 = *vertices.get(i + 1).unwrap_or(&vertices[0]); - let (left, right) = if xy_order(v1, v2) == Ordering::Less { - (v1, v2) - } else { - (v2, v1) - }; - - let segment = Segment { - edge_index: i, - left, - right, - }; - events.push(SweepLineEvent { - segment, - endpoint: Endpoint::Left, - }); - events.push(SweepLineEvent { - segment, - endpoint: Endpoint::Right, - }); - } - - events.sort(); - - Self { events } - } -} - -/// Represents a segment or rather an edge of the polygon in the [`SweepLine`]. -/// -/// 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 - .y - .total_cmp(&other.left.y) - .then_with(|| self.right.y.total_cmp(&other.right.y)) - } -} - -/// 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, - below: Option, -} - -/// A sweep line allows for an efficient search for intersections between [segments](`Segment`). -/// -/// 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 { - vertices, - tree: BTreeMap::new(), - } - } - - /// Determine whether the given edges of the polygon intersect. - fn intersects(&self, edge1: Option, edge2: Option) -> bool { - let Some(edge1) = edge1 else { - return false; - }; - let Some(edge2) = edge2 else { - return false; - }; - - // All adjacent edges intersect at their shared vertex - // but these intersections do not count so we ignore them here. - // Likewise a segment will always intersect itself / an identical edge. - if edge1 == edge2 - || (edge1 + 1) % self.vertices.len() == edge2 - || (edge2 + 1) % self.vertices.len() == edge1 - { - return false; - } - - let s11 = self.vertices[edge1]; - let s12 = *self.vertices.get(edge1 + 1).unwrap_or(&self.vertices[0]); - let s21 = self.vertices[edge2]; - let s22 = *self.vertices.get(edge2 + 1).unwrap_or(&self.vertices[0]); - - // When both points of the second edge are on the same side of the first edge, no intersection is possible. - if point_side(s11, s12, s21) * point_side(s11, s12, s22) > 0.0 { - return false; - } - if point_side(s21, s22, s11) * point_side(s21, s22, s12) > 0.0 { - return false; - } - - true - } - - /// Add a new segment to the sweep line - fn add(&mut self, s: Segment) -> SegmentOrder { - let above = if let Some((next_s, next_ord)) = self.tree.range_mut(s..).next() { - next_ord.below.replace(s.edge_index); - Some(next_s.edge_index) - } else { - None - }; - let below = if let Some((prev_s, prev_ord)) = self.tree.range_mut(..s).next_back() { - prev_ord.above.replace(s.edge_index); - Some(prev_s.edge_index) - } else { - None - }; - - let s_ord = SegmentOrder { above, below }; - self.tree.insert(s, s_ord); - s_ord - } - - /// Get the segment order for the given segment. - /// - /// If `s` has not been added to the [`SweepLine`] `None` will be returned. - fn find(&self, s: &Segment) -> Option<&SegmentOrder> { - self.tree.get(s) - } - - /// Remove `s` from the [`SweepLine`]. - fn remove(&mut self, s: &Segment) { - let Some(s_ord) = self.tree.get(s).copied() else { - return; - }; - - if let Some((_, above_ord)) = self.tree.range_mut(s..).next() { - above_ord.below = s_ord.below; - } - if let Some((_, below_ord)) = self.tree.range_mut(..s).next_back() { - below_ord.above = s_ord.above; - } - - self.tree.remove(s); - } -} - -/// 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) -} - -/// Tests whether the `vertices` describe a simple polygon. -/// The last vertex must not be equal to the first vertex. -/// -/// A polygon is simple if it is not self intersecting and not self tangent. -/// As such, no two edges of the polygon may cross each other and each vertex must not lie on another edge. -/// -/// Any 'polygon' with less than three vertices is simple. -/// -/// 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 event_queue = EventQueue::new(vertices); - let mut sweep_line = SweepLine::new(vertices); - - for e in event_queue.events { - match e.endpoint { - Endpoint::Left => { - let s = sweep_line.add(e.segment); - if sweep_line.intersects(Some(e.segment.edge_index), s.above) - || sweep_line.intersects(Some(e.segment.edge_index), s.below) - { - return false; - } - } - Endpoint::Right => { - if let Some(s) = sweep_line.find(&e.segment) { - if sweep_line.intersects(s.above, s.below) { - return false; - } - sweep_line.remove(&e.segment); - } - } - } - } - - true -} - -#[cfg(test)] -mod tests { - use crate::{primitives::polygon::is_polygon_simple, Vec2}; - - #[test] - fn complex_polygon() { - // A square with one side punching through the opposite side. - let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y, Vec2::new(2.0, 0.5)]; - assert!(!is_polygon_simple(&verts)); - - // A square with a vertex from one side touching the opposite side. - let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y, Vec2::new(1.0, 0.5)]; - assert!(!is_polygon_simple(&verts)); - - // A square with one side touching the opposite side. - let verts = [ - Vec2::ZERO, - Vec2::X, - Vec2::ONE, - Vec2::Y, - Vec2::new(1.0, 0.6), - Vec2::new(1.0, 0.4), - ]; - assert!(!is_polygon_simple(&verts)); - - // Four points lying on a line - let verts = [Vec2::ONE, Vec2::new(3., 2.), Vec2::new(5., 3.), Vec2::NEG_X]; - assert!(!is_polygon_simple(&verts)); - - // Three points lying on a line - let verts = [Vec2::ONE, Vec2::new(3., 2.), Vec2::NEG_X]; - assert!(!is_polygon_simple(&verts)); - - // Two identical points and one other point - let verts = [Vec2::ONE, Vec2::ONE, Vec2::NEG_X]; - assert!(!is_polygon_simple(&verts)); - - // Two triangles with one shared side - let verts = [Vec2::ZERO, Vec2::X, Vec2::Y, Vec2::ONE, Vec2::X, Vec2::Y]; - assert!(!is_polygon_simple(&verts)); - } - - #[test] - fn simple_polygon() { - // A square - let verts = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]; - assert!(is_polygon_simple(&verts)); - - let verts = []; - assert!(is_polygon_simple(&verts)); - } -} diff --git a/crates/bevy_math/src/primitives/view_frustum.rs b/crates/bevy_math/src/primitives/view_frustum.rs deleted file mode 100644 index 604aee6f155aa..0000000000000 --- a/crates/bevy_math/src/primitives/view_frustum.rs +++ /dev/null @@ -1,251 +0,0 @@ -use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4}; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; - -/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s. -/// -/// View Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid. -/// -/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors -/// of the half-spaces point towards the interior of the frustum. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Clone, Debug, Default, PartialEq) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct ViewFrustum { - /// The six half-spaces making up the frustum - pub half_spaces: [HalfSpace; 6], -} - -impl ViewFrustum { - /// The index for the near plane in `half_spaces` - pub const NEAR_PLANE_IDX: usize = 4; - /// The index for the far plane in `half_spaces` - pub const FAR_PLANE_IDX: usize = 5; - /// Vec4 representing an inactive half space. - /// The bisecting plane's unit normal is set to (0, 0, 0). - /// The signed distance along the normal from the plane to the origin is set to `f32::INFINITY`. - const INACTIVE_HALF_SPACE: Vec4 = Vec4::new(0.0, 0.0, 0.0, f32::INFINITY); - - /// Returns a view frustum derived from `clip_from_world`. - #[inline] - pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self { - let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world); - frustum.half_spaces[Self::FAR_PLANE_IDX] = HalfSpace::new(clip_from_world.row(2)); - frustum - } - - /// Returns a view frustum derived from `clip_from_world`, - /// but with a custom far plane. - #[inline] - pub fn from_clip_from_world_custom_far( - clip_from_world: &Mat4, - view_translation: &Vec3, - view_backward: &Vec3, - far: f32, - ) -> Self { - let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world); - let far_center = *view_translation - far * *view_backward; - frustum.half_spaces[Self::FAR_PLANE_IDX] = - HalfSpace::new(view_backward.extend(-view_backward.dot(far_center))); - frustum - } - - /// Calculates the corners of this frustum. Returns `None` if the frustum isn't properly defined. - /// - /// If `Some`, the corners are returned in the following order: - /// near top left, near top right, near bottom right, near bottom left, - /// far top left, far top right, far bottom right, far bottom left. - /// If the far plane is an inactive half space, the intersection points - /// that include the far plane will be `Vec3::NAN`. - #[inline] - pub fn corners(&self) -> Option<[Vec3; 8]> { - let [left, right, top, bottom, near, far] = self.half_spaces; - Some([ - HalfSpace::intersection_point(top, left, near)?, - HalfSpace::intersection_point(top, right, near)?, - HalfSpace::intersection_point(bottom, right, near)?, - HalfSpace::intersection_point(bottom, left, near)?, - HalfSpace::intersection_point(top, left, far)?, - HalfSpace::intersection_point(top, right, far)?, - HalfSpace::intersection_point(bottom, right, far)?, - HalfSpace::intersection_point(bottom, left, far)?, - ]) - } - - // NOTE: This approach of extracting the frustum half-space from the view - // projection matrix is from Foundations of Game Engine Development 2 - // Rendering by Lengyel. - /// Returns a view frustum derived from `view_projection`, - /// without a far plane. - fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self { - let row0 = clip_from_world.row(0); - let row1 = clip_from_world.row(1); - let row2 = clip_from_world.row(2); - let row3 = clip_from_world.row(3); - - Self { - half_spaces: [ - HalfSpace::new(row3 + row0), - HalfSpace::new(row3 - row0), - HalfSpace::new(row3 + row1), - HalfSpace::new(row3 - row1), - HalfSpace::new(row3 + row2), - HalfSpace::new(Self::INACTIVE_HALF_SPACE), - ], - } - } -} - -#[cfg(test)] -mod view_frustum_tests { - use core::f32::consts::FRAC_1_SQRT_2; - - use approx::assert_relative_eq; - - use super::ViewFrustum; - use crate::{primitives::HalfSpace, Vec3, Vec4}; - - #[test] - fn cuboid_frustum_corners() { - let cuboid_frustum = ViewFrustum { - // left: x = -5; right: x = 4 - // near: y = 0; far: y = 6 - // top: z = 3; bottom: z = -2 - half_spaces: [ - // left: yz plane at x = -5 - HalfSpace::new(Vec4::new(1., 0., 0., 5.)), - // right: yz plane at x = 4 - HalfSpace::new(Vec4::new(-1., 0., 0., 4.)), - // top: xy plane at z = 3 - HalfSpace::new(Vec4::new(0., 0., -1., 3.)), - // bottom: xy plane at z = -2 - HalfSpace::new(Vec4::new(0., 0., 1., 2.)), - // near: xz plane at origin (y = 0) - HalfSpace::new(Vec4::new(0., 1., 0., 0.)), - // far: xz plane at y = 6 - HalfSpace::new(Vec4::new(0., -1., 0., 6.)), - ], - }; - let corners = cuboid_frustum.corners().unwrap(); - // near top left - assert_relative_eq!(corners[0], Vec3::new(-5., 0., 3.), epsilon = 2e-7); - // near top right - assert_relative_eq!(corners[1], Vec3::new(4., 0., 3.), epsilon = 2e-7); - // near bottom right - assert_relative_eq!(corners[2], Vec3::new(4., 0., -2.), epsilon = 2e-7); - // near bottom left - assert_relative_eq!(corners[3], Vec3::new(-5., 0., -2.), epsilon = 2e-7); - // far top left - assert_relative_eq!(corners[4], Vec3::new(-5., 6., 3.), epsilon = 2e-7); - // far top right - assert_relative_eq!(corners[5], Vec3::new(4., 6., 3.), epsilon = 2e-7); - // far bottom right - assert_relative_eq!(corners[6], Vec3::new(4., 6., -2.), epsilon = 2e-7); - // far bottom left - assert_relative_eq!(corners[7], Vec3::new(-5., 6., -2.), epsilon = 2e-7); - } - - #[test] - fn pyramid_frustum_corners() { - // a frustum where the near plane intersects the left right top and bottom planes - // at a single point - let pyramid_frustum = ViewFrustum { - half_spaces: [ - // left - HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - // right - HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - // top - HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)), - // bottom - HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), - // near: xz plane at y = -1 - HalfSpace::new(Vec4::new(0., 1., 0., 1.)), - // far: xz plane at y = 3 - HalfSpace::new(Vec4::new(0., -1., 0., 3.)), - ], - }; - let corners = pyramid_frustum.corners().unwrap(); - // near top left - assert_relative_eq!(corners[0], Vec3::new(0., -1., 0.), epsilon = 2e-7); - // near top right - assert_relative_eq!(corners[1], Vec3::new(0., -1., 0.), epsilon = 2e-7); - // near bottom right - assert_relative_eq!(corners[2], Vec3::new(0., -1., 0.), epsilon = 2e-7); - // near bottom left - assert_relative_eq!(corners[3], Vec3::new(0., -1., 0.), epsilon = 2e-7); - // far top left - assert_relative_eq!(corners[4], Vec3::new(-4., 3., 4.), epsilon = 2e-7); - // far top right - assert_relative_eq!(corners[5], Vec3::new(4., 3., 4.), epsilon = 2e-7); - // far bottom right - assert_relative_eq!(corners[6], Vec3::new(4., 3., -4.), epsilon = 2e-7); - // far bottom left - assert_relative_eq!(corners[7], Vec3::new(-4., 3., -4.), epsilon = 2e-7); - } - - #[test] - fn frustum_with_some_nan_corners() { - // frustum with no far plane has NAN far corners - let no_far = ViewFrustum { - half_spaces: [ - // left: a yz plane rotated outwards - HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - // right: a yz plane rotated outwards - HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - // top: an xz plane rotated outwards - HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)), - // bottom: xz plane rotated outwards - HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), - // near: xz plane at origin (y = 0) - HalfSpace::new(Vec4::new(0., 1., 0., 0.)), - // far - HalfSpace::new(ViewFrustum::INACTIVE_HALF_SPACE), - ], - }; - let corners = no_far.corners().unwrap(); - // near top left - assert_relative_eq!(corners[0], Vec3::new(-1., 0., 1.), epsilon = 2e-7); - // near top right - assert_relative_eq!(corners[1], Vec3::new(1., 0., 1.), epsilon = 2e-7); - // near bottom right - assert_relative_eq!(corners[2], Vec3::new(1., 0., -1.), epsilon = 2e-7); - // near bottom left - assert_relative_eq!(corners[3], Vec3::new(-1., 0., -1.), epsilon = 2e-7); - // far top left - assert!(corners[4].is_nan()); - // far top right - assert!(corners[5].is_nan()); - // far bottom right - assert!(corners[6].is_nan()); - // far bottom left - assert!(corners[7].is_nan()); - } - - #[test] - fn invalid_frustum_corners() { - let invalid = ViewFrustum { - half_spaces: [ - // the left and the top half spaces are the same, resulting in no intersection point - HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)), - HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)), - HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)), - HalfSpace::new(Vec4::new(0., 1., 0., 0.)), - HalfSpace::new(Vec4::new(0., -1., 0., 3.)), - ], - }; - assert!(invalid.corners().is_none()); - } -} diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray.rs index 0240ba2a71353..3378979add7a6 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray.rs @@ -1,8 +1,4 @@ -use crate::{ - ops, - primitives::{InfinitePlane3d, Plane2d}, - Dir2, Dir3, Vec2, Vec3, -}; +use crate::{ops, Dir2, Dir3, Vec2, Vec3}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -45,10 +41,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(&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 +56,8 @@ 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_intersection_point(&self, plane_origin: Vec2, plane_normal: Dir2) -> Option { + self.intersect_plane(plane_origin, plane_normal) .map(|distance| self.get_point(distance)) } } @@ -102,10 +98,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(&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 +113,8 @@ impl Ray3d { /// /// Calls [`Ray3d::get_point`] on the result of [`Ray3d::intersect_plane`]. #[inline] - pub fn plane_intersection_point( - &self, - plane_origin: Vec3, - plane: InfinitePlane3d, - ) -> Option { - self.intersect_plane(plane_origin, plane) + pub fn plane_intersection_point(&self, plane_origin: Vec3, plane_normal: Dir3) -> Option { + self.intersect_plane(plane_origin, plane_normal) .map(|distance| self.get_point(distance)) } } diff --git a/crates/bevy_math/src/sampling/mesh_sampling.rs b/crates/bevy_math/src/sampling/mesh_sampling.rs deleted file mode 100644 index 33b55a80b7d6d..0000000000000 --- a/crates/bevy_math/src/sampling/mesh_sampling.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Functionality related to random sampling from triangle meshes. - -use crate::{ - primitives::{Measured2d, Triangle3d}, - ShapeSample, Vec3, -}; -use alloc::vec::Vec; -use rand::RngExt; -use rand_distr::{ - weighted::{Error as WeightedError, WeightedAliasIndex}, - Distribution, -}; - -/// A [distribution] that caches data to allow fast sampling from a collection of triangles. -/// Generally used through [`sample`] or [`sample_iter`]. -/// -/// [distribution]: Distribution -/// [`sample`]: Distribution::sample -/// [`sample_iter`]: Distribution::sample_iter -/// -/// Example -/// ``` -/// # use bevy_math::{Vec3, primitives::*}; -/// # use bevy_math::sampling::mesh_sampling::UniformMeshSampler; -/// # use rand::{SeedableRng, rngs::StdRng, distr::Distribution}; -/// let faces = Tetrahedron::default().faces(); -/// let sampler = UniformMeshSampler::try_new(faces).unwrap(); -/// let rng = StdRng::seed_from_u64(8765309); -/// // 50 random points on the tetrahedron: -/// let samples: Vec = sampler.sample_iter(rng).take(50).collect(); -/// ``` -pub struct UniformMeshSampler { - triangles: Vec, - face_distribution: WeightedAliasIndex, -} - -impl Distribution for UniformMeshSampler { - fn sample(&self, rng: &mut R) -> Vec3 { - let face_index = self.face_distribution.sample(rng); - self.triangles[face_index].sample_interior(rng) - } -} - -impl UniformMeshSampler { - /// Construct a new [`UniformMeshSampler`] from a list of [triangles]. - /// - /// Returns an error if the distribution of areas for the collection of triangles could not be formed - /// (most notably if the collection has zero surface area). - /// - /// [triangles]: Triangle3d - pub fn try_new>( - triangles: T, - ) -> Result { - let triangles: Vec = triangles.into_iter().collect(); - let areas = triangles.iter().map(Measured2d::area).collect(); - - WeightedAliasIndex::new(areas).map(|face_distribution| Self { - triangles, - face_distribution, - }) - } -} 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/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs deleted file mode 100644 index a2e02675c9ce8..0000000000000 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ /dev/null @@ -1,694 +0,0 @@ -//! The [`ShapeSample`] trait, allowing random sampling from geometric shapes. -//! -//! At the most basic level, this allows sampling random points from the interior and boundary of -//! geometric primitives. For example: -//! ``` -//! # use bevy_math::primitives::*; -//! # use bevy_math::ShapeSample; -//! # use rand::SeedableRng; -//! # use rand::rngs::StdRng; -//! // Get some `RngExt`: -//! let mut rng: StdRng = rand::make_rng(); -//! // Make a circle of radius 2: -//! let circle = Circle::new(2.0); -//! // Get a point inside this circle uniformly at random: -//! let interior_pt = circle.sample_interior(&mut rng); -//! // Get a point on the circle's boundary uniformly at random: -//! let boundary_pt = circle.sample_boundary(&mut rng); -//! ``` -//! -//! For repeated sampling, `ShapeSample` also includes methods for accessing a [`Distribution`]: -//! ``` -//! # use bevy_math::primitives::*; -//! # use bevy_math::{Vec2, ShapeSample}; -//! # use rand::SeedableRng; -//! # use rand::rngs::StdRng; -//! # use rand::distr::Distribution; -//! # let mut rng1: StdRng = rand::make_rng(); -//! # let mut rng2: StdRng = rand::make_rng(); -//! // Use a rectangle this time: -//! let rectangle = Rectangle::new(1.0, 2.0); -//! // Get an iterator that spits out random interior points: -//! let interior_iter = rectangle.interior_dist().sample_iter(&mut rng1); -//! // Collect random interior points from the iterator: -//! let interior_pts: Vec = interior_iter.take(1000).collect(); -//! // Similarly, get an iterator over many random boundary points and collect them: -//! let boundary_pts: Vec = rectangle.boundary_dist().sample_iter(&mut rng2).take(1000).collect(); -//! ``` -//! -//! In any case, the [`RngExt`] used as the source of randomness must be provided explicitly. - -use core::f32::consts::{FRAC_PI_2, PI, TAU}; - -use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3}; -use rand::{ - distr::{ - uniform::SampleUniform, - weighted::{Weight, WeightedIndex}, - Distribution, - }, - RngExt, -}; - -/// 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. - type Output; - - /// Uniformly sample a point from inside the area/volume of this shape, centered on 0. - /// - /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. - /// - /// # Example - /// ``` - /// # use bevy_math::prelude::*; - /// let square = Rectangle::new(2.0, 2.0); - /// - /// // Returns a Vec2 with both x and y between -1 and 1. - /// println!("{}", square.sample_interior(&mut rand::rng())); - /// ``` - fn sample_interior(&self, rng: &mut R) -> Self::Output; - - /// Uniformly sample a point from the surface of this shape, centered on 0. - /// - /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. - /// - /// # Example - /// ``` - /// # use bevy_math::prelude::*; - /// let square = Rectangle::new(2.0, 2.0); - /// - /// // Returns a Vec2 where one of the coordinates is at ±1, - /// // and the other is somewhere between -1 and 1. - /// println!("{}", square.sample_boundary(&mut rand::rng())); - /// ``` - fn sample_boundary(&self, rng: &mut R) -> Self::Output; - - /// Extract a [`Distribution`] whose samples are points of this shape's interior, taken uniformly. - /// - /// # Example - /// - /// ``` - /// # use bevy_math::prelude::*; - /// # use rand::distr::Distribution; - /// let square = Rectangle::new(2.0, 2.0); - /// let rng = rand::rng(); - /// - /// // Iterate over points randomly drawn from `square`'s interior: - /// for random_val in square.interior_dist().sample_iter(rng).take(5) { - /// println!("{}", random_val); - /// } - /// ``` - fn interior_dist(self) -> impl Distribution - where - Self: Sized, - { - InteriorOf(self) - } - - /// Extract a [`Distribution`] whose samples are points of this shape's boundary, taken uniformly. - /// - /// # Example - /// - /// ``` - /// # use bevy_math::prelude::*; - /// # use rand::distr::Distribution; - /// let square = Rectangle::new(2.0, 2.0); - /// let rng = rand::rng(); - /// - /// // Iterate over points randomly drawn from `square`'s boundary: - /// for random_val in square.boundary_dist().sample_iter(rng).take(5) { - /// println!("{}", random_val); - /// } - /// ``` - fn boundary_dist(self) -> impl Distribution - where - Self: Sized, - { - BoundaryOf(self) - } -} - -#[derive(Clone, Copy)] -/// A wrapper struct that allows interior sampling from a [`ShapeSample`] type directly as -/// a [`Distribution`]. -pub struct InteriorOf(pub T); - -#[derive(Clone, Copy)] -/// A wrapper struct that allows boundary sampling from a [`ShapeSample`] type directly as -/// a [`Distribution`]. -pub struct BoundaryOf(pub T); - -impl Distribution<::Output> for InteriorOf { - fn sample(&self, rng: &mut R) -> ::Output { - self.0.sample_interior(rng) - } -} - -impl Distribution<::Output> for BoundaryOf { - fn sample(&self, rng: &mut R) -> ::Output { - self.0.sample_boundary(rng) - } -} - -impl ShapeSample for Circle { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Vec2 { - // https://mathworld.wolfram.com/DiskPointPicking.html - let theta = rng.random_range(0.0..TAU); - let r_squared = rng.random_range(0.0..=(self.radius * self.radius)); - let r = ops::sqrt(r_squared); - let (sin, cos) = ops::sin_cos(theta); - Vec2::new(r * cos, r * sin) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec2 { - let theta = rng.random_range(0.0..TAU); - let (sin, cos) = ops::sin_cos(theta); - Vec2::new(self.radius * cos, self.radius * sin) - } -} - -impl ShapeSample for CircularSector { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Vec2 { - let theta = rng.random_range(-self.half_angle()..=self.half_angle()); - let r_squared = rng.random_range(0.0..=(self.radius() * self.radius())); - let r = ops::sqrt(r_squared); - let (sin, cos) = ops::sin_cos(theta); - Vec2::new(r * sin, r * cos) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec2 { - if rng.random_range(0.0..=1.0) <= self.arc_length() / self.perimeter() { - // Sample on the arc - let theta = FRAC_PI_2 + rng.random_range(-self.half_angle()..self.half_angle()); - Vec2::from_angle(theta) * self.radius() - } else { - // Sample on the "inner" straight lines - let dir = self.radius() * Vec2::from_angle(FRAC_PI_2 + self.half_angle()); - let r: f32 = rng.random_range(-1.0..1.0); - (-r).clamp(0.0, 1.0) * dir + r.clamp(0.0, 1.0) * dir * Vec2::new(-1.0, 1.0) - } - } -} - -/// 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 ShapeSample for Sphere { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Vec3 { - let r_cubed = rng.random_range(0.0..=(self.radius * self.radius * self.radius)); - let r = ops::cbrt(r_cubed); - - r * sample_unit_sphere_boundary(rng) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec3 { - self.radius * sample_unit_sphere_boundary(rng) - } -} - -impl ShapeSample for Annulus { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Self::Output { - let inner_radius = self.inner_circle.radius; - let outer_radius = self.outer_circle.radius; - - // Like random sampling for a circle, radius is weighted by the square. - let r_squared = - rng.random_range((inner_radius * inner_radius)..(outer_radius * outer_radius)); - let r = ops::sqrt(r_squared); - let theta = rng.random_range(0.0..TAU); - let (sin, cos) = ops::sin_cos(theta); - - Vec2::new(r * cos, r * sin) - } - - fn sample_boundary(&self, rng: &mut R) -> Self::Output { - let total_perimeter = self.inner_circle.perimeter() + self.outer_circle.perimeter(); - let inner_prob = (self.inner_circle.perimeter() / total_perimeter) as f64; - - // Sample from boundary circles, choosing which one by weighting by perimeter: - let inner = rng.random_bool(inner_prob); - if inner { - self.inner_circle.sample_boundary(rng) - } else { - self.outer_circle.sample_boundary(rng) - } - } -} - -impl ShapeSample for Rhombus { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Vec2 { - let x: f32 = rng.random_range(0.0..=1.0); - let y: f32 = rng.random_range(0.0..=1.0); - - let unit_p = Vec2::NEG_X + x * Vec2::ONE + Vec2::new(y, -y); - unit_p * self.half_diagonals - } - - fn sample_boundary(&self, rng: &mut R) -> Vec2 { - let x: f32 = rng.random_range(-1.0..=1.0); - let y_sign = if rng.random() { -1.0 } else { 1.0 }; - - let y = (1.0 - ops::abs(x)) * y_sign; - Vec2::new(x, y) * self.half_diagonals - } -} - -impl ShapeSample for Rectangle { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Vec2 { - let x = rng.random_range(-self.half_size.x..=self.half_size.x); - let y = rng.random_range(-self.half_size.y..=self.half_size.y); - Vec2::new(x, y) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec2 { - let primary_side = rng.random_range(-1.0..1.0); - let other_side = if rng.random() { -1.0 } else { 1.0 }; - - if self.half_size.x + self.half_size.y > 0.0 { - if rng.random_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { - Vec2::new(primary_side, other_side) * self.half_size - } else { - Vec2::new(other_side, primary_side) * self.half_size - } - } else { - Vec2::ZERO - } - } -} - -impl ShapeSample for Cuboid { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Vec3 { - let x = rng.random_range(-self.half_size.x..=self.half_size.x); - let y = rng.random_range(-self.half_size.y..=self.half_size.y); - let z = rng.random_range(-self.half_size.z..=self.half_size.z); - Vec3::new(x, y, z) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec3 { - let primary_side1 = rng.random_range(-1.0..1.0); - let primary_side2 = rng.random_range(-1.0..1.0); - let other_side = if rng.random() { -1.0 } else { 1.0 }; - - if let Ok(dist) = WeightedIndex::new([ - self.half_size.y * self.half_size.z, - self.half_size.x * self.half_size.z, - self.half_size.x * self.half_size.y, - ]) { - match dist.sample(rng) { - 0 => Vec3::new(other_side, primary_side1, primary_side2) * self.half_size, - 1 => Vec3::new(primary_side1, other_side, primary_side2) * self.half_size, - 2 => Vec3::new(primary_side1, primary_side2, other_side) * self.half_size, - _ => unreachable!(), - } - } else { - Vec3::ZERO - } - } -} - -/// Interior sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_interior(vertices: [P; 3], rng: &mut R) -> P -where - P: NormedVectorSpace, - P::Scalar: SampleUniform + PartialOrd, - R: RngExt + ?Sized, -{ - let [a, b, c] = vertices; - let ab = b - a; - let ac = c - a; - - // Generate random points on a parallelepiped and reflect so that - // we can use the points that lie outside the triangle - let u = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE); - let v = rng.random_range(P::Scalar::ZERO..=P::Scalar::ONE); - - if u + v > P::Scalar::ONE { - let u1 = P::Scalar::ONE - v; - let v1 = P::Scalar::ONE - u; - a + (ab * u1 + ac * v1) - } else { - a + (ab * u + ac * v) - } -} - -/// Boundary sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_boundary(vertices: [P; 3], rng: &mut R) -> P -where - P: NormedVectorSpace, - P::Scalar: Weight + SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>, - R: RngExt + ?Sized, -{ - let [a, b, c] = vertices; - let ab = b - a; - let ac = c - a; - let bc = c - b; - - let t = rng.random_range(::ZERO..=P::Scalar::ONE); - - if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { - match dist.sample(rng) { - 0 => a.lerp(b, t), - 1 => a.lerp(c, t), - 2 => b.lerp(c, t), - _ => unreachable!(), - } - } else { - // This should only occur when the triangle is 0-dimensional degenerate - // so this is actually the correct result. - a - } -} - -impl ShapeSample for Triangle2d { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Self::Output { - sample_triangle_interior(self.vertices, rng) - } - - fn sample_boundary(&self, rng: &mut R) -> Self::Output { - sample_triangle_boundary(self.vertices, rng) - } -} - -impl ShapeSample for Triangle3d { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Self::Output { - sample_triangle_interior(self.vertices, rng) - } - - fn sample_boundary(&self, rng: &mut R) -> Self::Output { - sample_triangle_boundary(self.vertices, rng) - } -} - -impl ShapeSample for Tetrahedron { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Self::Output { - let [v0, v1, v2, v3] = self.vertices; - - // Generate a random point in a cube: - let mut coords: [f32; 3] = [ - rng.random_range(0.0..1.0), - rng.random_range(0.0..1.0), - rng.random_range(0.0..1.0), - ]; - - // The cube is broken into six tetrahedra of the form 0 <= c_0 <= c_1 <= c_2 <= 1, - // where c_i are the three euclidean coordinates in some permutation. (Since 3! = 6, - // there are six of them). Sorting the coordinates folds these six tetrahedra into the - // tetrahedron 0 <= x <= y <= z <= 1 (i.e. a fundamental domain of the permutation action). - coords.sort_by(|x, y| x.partial_cmp(y).unwrap()); - - // Now, convert a point from the fundamental tetrahedron into barycentric coordinates by - // taking the four successive differences of coordinates; note that these telescope to sum - // to 1, and this transformation is linear, hence preserves the probability density, since - // the latter comes from the Lebesgue measure. - // - // (See https://en.wikipedia.org/wiki/Lebesgue_measure#Properties — specifically, that - // Lebesgue measure of a linearly transformed set is its original measure times the - // determinant.) - let (a, b, c, d) = ( - coords[0], - coords[1] - coords[0], - coords[2] - coords[1], - 1. - coords[2], - ); - - // This is also a linear mapping, so probability density is still preserved. - v0 * a + v1 * b + v2 * c + v3 * d - } - - fn sample_boundary(&self, rng: &mut R) -> Self::Output { - let triangles = self.faces(); - let areas = triangles.iter().map(Measured2d::area); - - if areas.clone().sum::() > 0.0 { - // There is at least one triangle with nonzero area, so this unwrap succeeds. - let dist = WeightedIndex::new(areas).unwrap(); - - // Get a random index, then sample the interior of the associated triangle. - let idx = dist.sample(rng); - triangles[idx].sample_interior(rng) - } else { - // In this branch the tetrahedron has zero surface area; just return a point that's on - // the tetrahedron. - self.vertices[0] - } - } -} - -impl ShapeSample for Cylinder { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Vec3 { - let Vec2 { x, y: z } = self.base().sample_interior(rng); - let y = rng.random_range(-self.half_height..=self.half_height); - Vec3::new(x, y, z) - } - - fn sample_boundary(&self, rng: &mut R) -> Vec3 { - // This uses the area of the ends divided by the overall surface area (optimized) - // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) - if self.radius + 2.0 * self.half_height > 0.0 { - if rng.random_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { - let Vec2 { x, y: z } = self.base().sample_interior(rng); - if rng.random() { - Vec3::new(x, self.half_height, z) - } else { - Vec3::new(x, -self.half_height, z) - } - } else { - let Vec2 { x, y: z } = self.base().sample_boundary(rng); - let y = rng.random_range(-self.half_height..=self.half_height); - Vec3::new(x, y, z) - } - } else { - Vec3::ZERO - } - } -} - -impl ShapeSample for Capsule2d { - type Output = Vec2; - - fn sample_interior(&self, rng: &mut R) -> Vec2 { - let rectangle_area = self.half_length * self.radius * 4.0; - let capsule_area = rectangle_area + PI * self.radius * self.radius; - if capsule_area > 0.0 { - // Check if the random point should be inside the rectangle - if rng.random_bool((rectangle_area / capsule_area) as f64) { - self.to_inner_rectangle().sample_interior(rng) - } else { - let circle = Circle::new(self.radius); - let point = circle.sample_interior(rng); - // Add half length if it is the top semi-circle, otherwise subtract half - if point.y > 0.0 { - point + Vec2::Y * self.half_length - } else { - point - Vec2::Y * self.half_length - } - } - } else { - Vec2::ZERO - } - } - - fn sample_boundary(&self, rng: &mut R) -> Vec2 { - let rectangle_surface = 4.0 * self.half_length; - let capsule_surface = rectangle_surface + TAU * self.radius; - if capsule_surface > 0.0 { - if rng.random_bool((rectangle_surface / capsule_surface) as f64) { - let side_distance = - rng.random_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); - if side_distance < 0.0 { - Vec2::new(self.radius, side_distance + self.half_length) - } else { - Vec2::new(-self.radius, side_distance - self.half_length) - } - } else { - let circle = Circle::new(self.radius); - let point = circle.sample_boundary(rng); - // Add half length if it is the top semi-circle, otherwise subtract half - if point.y > 0.0 { - point + Vec2::Y * self.half_length - } else { - point - Vec2::Y * self.half_length - } - } - } else { - Vec2::ZERO - } - } -} - -impl ShapeSample for Capsule3d { - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Vec3 { - let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; - // Add 4/3 pi r^3 - let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; - if capsule_vol > 0.0 { - // Check if the random point should be inside the cylinder - if rng.random_bool((cylinder_vol / capsule_vol) as f64) { - self.to_cylinder().sample_interior(rng) - } else { - let sphere = Sphere::new(self.radius); - let point = sphere.sample_interior(rng); - // Add half length if it is the top semi-sphere, otherwise subtract half - if point.y > 0.0 { - point + Vec3::Y * self.half_length - } else { - point - Vec3::Y * self.half_length - } - } - } else { - Vec3::ZERO - } - } - - fn sample_boundary(&self, rng: &mut R) -> Vec3 { - let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; - let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; - if capsule_surface > 0.0 { - if rng.random_bool((cylinder_surface / capsule_surface) as f64) { - let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); - let y = rng.random_range(-self.half_length..=self.half_length); - Vec3::new(x, y, z) - } else { - let sphere = Sphere::new(self.radius); - let point = sphere.sample_boundary(rng); - // Add half length if it is the top semi-sphere, otherwise subtract half - if point.y > 0.0 { - point + Vec3::Y * self.half_length - } else { - point - Vec3::Y * self.half_length - } - } - } else { - Vec3::ZERO - } - } -} - -impl> ShapeSample for Extrusion

{ - type Output = Vec3; - - fn sample_interior(&self, rng: &mut R) -> Self::Output { - let base_point = self.base_shape.sample_interior(rng); - let depth = rng.random_range(-self.half_depth..self.half_depth); - base_point.extend(depth) - } - - fn sample_boundary(&self, rng: &mut R) -> Self::Output { - let base_area = self.base_shape.area(); - let total_area = self.area(); - - let random = rng.random_range(0.0..total_area); - match random { - x if x < base_area => self.base_shape.sample_interior(rng).extend(self.half_depth), - x if x < 2. * base_area => self - .base_shape - .sample_interior(rng) - .extend(-self.half_depth), - _ => self - .base_shape - .sample_boundary(rng) - .extend(rng.random_range(-self.half_depth..self.half_depth)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chacha20::ChaCha8Rng; - use rand::SeedableRng; - - #[test] - fn circle_interior_sampling() { - let mut rng = ChaCha8Rng::from_seed(Default::default()); - let circle = Circle::new(8.0); - - let boxes = [ - (-3.0, 3.0), - (1.0, 2.0), - (-1.0, -2.0), - (3.0, -2.0), - (1.0, -6.0), - (-3.0, -7.0), - (-7.0, -3.0), - (-6.0, 1.0), - ]; - let mut box_hits = [0; 8]; - - // Checks which boxes (if any) the sampled points are in - for _ in 0..5000 { - let point = circle.sample_interior(&mut rng); - - for (i, box_) in boxes.iter().enumerate() { - if (point.x > box_.0 && point.x < box_.0 + 4.0) - && (point.y > box_.1 && point.y < box_.1 + 4.0) - { - box_hits[i] += 1; - } - } - } - - assert_eq!( - box_hits, - [396, 377, 415, 404, 366, 408, 408, 430], - "samples will occur across all array items at statistically equal chance" - ); - } - - #[test] - fn circle_boundary_sampling() { - let mut rng = ChaCha8Rng::from_seed(Default::default()); - let circle = Circle::new(1.0); - - let mut wedge_hits = [0; 8]; - - // Checks in which eighth of the circle each sampled point is in - for _ in 0..5000 { - let point = circle.sample_boundary(&mut rng); - - let angle = ops::atan(point.y / point.x) + PI / 2.0; - let wedge = ops::floor(angle * 8.0 / PI) as usize; - wedge_hits[wedge] += 1; - } - - assert_eq!( - wedge_hits, - [636, 608, 639, 603, 614, 650, 640, 610], - "samples will occur across all array items at statistically equal chance" - ); - } -} diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 1f14a8e1a25b3..8b0de9efa29f7 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -17,7 +17,6 @@ use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_math::{ bounding::{Aabb2d, Aabb3d}, - primitives::Triangle3d, *, }; use bevy_platform::collections::{hash_map, HashMap}; @@ -2537,17 +2536,15 @@ impl Mesh { /// /// [primitive topology]: PrimitiveTopology /// [triangles]: Triangle3d - pub fn triangles(&self) -> Result + '_, MeshTrianglesError> { + pub fn triangles(&self) -> Result + '_, MeshTrianglesError> { fn indices_to_triangle + Copy>( vertices: &[[f32; 3]], indices: &[T; 3], - ) -> Option { + ) -> Option<[Vec3; 3]> { let vert0 = Vec3::from(*vertices.get(indices[0].try_into().ok()?)?); let vert1 = Vec3::from(*vertices.get(indices[1].try_into().ok()?)?); let vert2 = Vec3::from(*vertices.get(indices[2].try_into().ok()?)?); - Some(Triangle3d { - vertices: [vert0, vert1, vert2], - }) + Some([vert0, vert1, vert2]) } let position_data = self.try_attribute(Mesh::ATTRIBUTE_POSITION)?; @@ -3049,7 +3046,6 @@ mod tests { use crate::{MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::bounding::Aabb3d; - use bevy_math::primitives::Triangle3d; use bevy_math::{Vec3, Vec3A}; use bevy_transform::components::Transform; @@ -3340,22 +3336,18 @@ mod tests { mesh.insert_indices(Indices::U32(vec![0, 1, 2, 2, 3, 0])); assert_eq!( vec![ - Triangle3d { - vertices: [ - Vec3::new(0., 0., 0.), - Vec3::new(1., 0., 0.), - Vec3::new(1., 1., 0.), - ] - }, - Triangle3d { - vertices: [ - Vec3::new(1., 1., 0.), - Vec3::new(0., 1., 0.), - Vec3::new(0., 0., 0.), - ] - } + [ + Vec3::new(0., 0., 0.), + Vec3::new(1., 0., 0.), + Vec3::new(1., 1., 0.), + ], + [ + Vec3::new(1., 1., 0.), + Vec3::new(0., 1., 0.), + Vec3::new(0., 0., 0.), + ] ], - mesh.triangles().unwrap().collect::>() + mesh.triangles().unwrap().collect::>() ); } @@ -3387,20 +3379,12 @@ mod tests { mesh.insert_indices(Indices::U32(vec![0, 1, 2, 3, 4, 5])); assert_eq!( vec![ - Triangle3d { - vertices: [positions[0], positions[1], positions[2]] - }, - Triangle3d { - vertices: [positions[2], positions[1], positions[3]] - }, - Triangle3d { - vertices: [positions[2], positions[3], positions[4]] - }, - Triangle3d { - vertices: [positions[4], positions[3], positions[5]] - }, + [positions[0], positions[1], positions[2]], + [positions[2], positions[1], positions[3]], + [positions[2], positions[3], positions[4]], + [positions[4], positions[3], positions[5]] ], - mesh.triangles().unwrap().collect::>() + mesh.triangles().unwrap().collect::>() ); } diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs deleted file mode 100644 index 33cf4fb531ccd..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ /dev/null @@ -1,1678 +0,0 @@ -use core::f32::consts::FRAC_PI_2; -use core::mem; - -use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment, VertexAttributeValues}; -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_reflect::prelude::*; -use wgpu_types::PrimitiveTopology; - -/// A builder used for creating a [`Mesh`] with a [`Circle`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct CircleMeshBuilder { - /// The [`Circle`] shape. - pub circle: Circle, - /// The number of vertices used for the circle mesh. - /// The default is `32`. - #[doc(alias = "vertices")] - pub resolution: u32, -} - -impl Default for CircleMeshBuilder { - fn default() -> Self { - Self { - circle: Circle::default(), - resolution: 32, - } - } -} - -impl CircleMeshBuilder { - /// Creates a new [`CircleMeshBuilder`] from a given radius and vertex count. - #[inline] - pub const fn new(radius: f32, resolution: u32) -> Self { - Self { - circle: Circle { radius }, - resolution, - } - } - - /// Sets the number of vertices used for the circle mesh. - #[inline] - #[doc(alias = "vertices")] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl MeshBuilder for CircleMeshBuilder { - fn build(&self) -> Mesh { - Ellipse::new(self.circle.radius, self.circle.radius) - .mesh_builder() - .resolution(self.resolution) - .build() - } -} - -impl Extrudable for CircleMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (0..self.resolution).chain([0]).collect(), - }] - } -} - -impl Meshable for Circle { - type Output = CircleMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - CircleMeshBuilder { - circle: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(circle: Circle) -> Self { - circle.mesh_builder().build() - } -} - -/// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. -/// -/// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes -/// the entire circle, particularly the same texture will be displayed with different fractions of a -/// complete circle. -/// -/// It's expected that more will be added in the future, such as a variant that causes the texture to be -/// scaled to fit the bounding box of the shape, which would be good for packed textures only including the -/// portion of the circle that is needed to display. -#[derive(Copy, Clone, Debug, PartialEq, Reflect)] -#[reflect(Default, Debug, Clone)] -#[non_exhaustive] -pub enum CircularMeshUvMode { - /// Treats the shape as a mask over a circle of equal size and radius, - /// with the center of the circle at the center of the texture. - Mask { - /// Angle by which to rotate the shape when generating the UV map. - angle: f32, - }, -} - -impl Default for CircularMeshUvMode { - fn default() -> Self { - CircularMeshUvMode::Mask { angle: 0.0 } - } -} - -/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. -/// -/// The resulting mesh will have a UV-map such that the center of the circle is -/// at the center of the texture. -#[derive(Clone, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct CircularSectorMeshBuilder { - /// The sector shape. - pub sector: CircularSector, - /// The number of vertices used for the arc portion of the sector mesh. - /// The default is `32`. - #[doc(alias = "vertices")] - pub resolution: u32, - /// The UV mapping mode - pub uv_mode: CircularMeshUvMode, -} - -impl Default for CircularSectorMeshBuilder { - fn default() -> Self { - Self { - sector: CircularSector::default(), - resolution: 32, - uv_mode: CircularMeshUvMode::default(), - } - } -} - -impl CircularSectorMeshBuilder { - /// Creates a new [`CircularSectorMeshBuilder`] from a given sector - #[inline] - pub fn new(sector: CircularSector) -> Self { - Self { - sector, - ..Self::default() - } - } - - /// Sets the number of vertices used for the sector mesh. - #[inline] - #[doc(alias = "vertices")] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } - - /// Sets the uv mode used for the sector mesh - #[inline] - pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { - self.uv_mode = uv_mode; - self - } -} - -impl MeshBuilder for CircularSectorMeshBuilder { - fn build(&self) -> Mesh { - let resolution = self.resolution as usize; - let mut indices = Vec::with_capacity((resolution - 1) * 3); - let mut positions = Vec::with_capacity(resolution + 1); - let normals = vec![[0.0, 0.0, 1.0]; resolution + 1]; - let mut uvs = Vec::with_capacity(resolution + 1); - - let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; - - // Push the center of the circle. - positions.push([0.0; 3]); - uvs.push([0.5; 2]); - - let first_angle = FRAC_PI_2 - self.sector.half_angle(); - let last_angle = FRAC_PI_2 + self.sector.half_angle(); - let last_i = (self.resolution - 1) as f32; - for i in 0..self.resolution { - let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); - - // Compute the vertex - let vertex = self.sector.radius() * Vec2::from_angle(angle); - // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). - // We accomplish the Y axis flip by negating the angle. - let uv = - Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); - - positions.push([vertex.x, vertex.y, 0.0]); - uvs.push([uv.x, uv.y]); - } - - for i in 1..self.resolution { - // Index 0 is the center. - indices.extend_from_slice(&[0, i, i + 1]); - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for CircularSectorMeshBuilder { - fn perimeter(&self) -> Vec { - let (sin, cos) = ops::sin_cos(self.sector.arc.half_angle); - let first_normal = Vec2::new(sin, cos); - let last_normal = Vec2::new(-sin, cos); - vec![ - PerimeterSegment::Flat { - indices: vec![self.resolution, 0, 1], - }, - PerimeterSegment::Smooth { - first_normal, - last_normal, - indices: (1..=self.resolution).collect(), - }, - ] - } -} - -impl Meshable for CircularSector { - type Output = CircularSectorMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - CircularSectorMeshBuilder { - sector: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - /// Converts this sector into a [`Mesh`] using a default [`CircularSectorMeshBuilder`]. - /// - /// See the documentation of [`CircularSectorMeshBuilder`] for more details. - fn from(sector: CircularSector) -> Self { - sector.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. -/// -/// The resulting mesh will have a UV-map such that the center of the circle is -/// at the center of the texture. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct CircularSegmentMeshBuilder { - /// The segment shape. - pub segment: CircularSegment, - /// The number of vertices used for the arc portion of the segment mesh. - /// The default is `32`. - #[doc(alias = "vertices")] - pub resolution: u32, - /// The UV mapping mode - pub uv_mode: CircularMeshUvMode, -} - -impl Default for CircularSegmentMeshBuilder { - fn default() -> Self { - Self { - segment: CircularSegment::default(), - resolution: 32, - uv_mode: CircularMeshUvMode::default(), - } - } -} - -impl CircularSegmentMeshBuilder { - /// Creates a new [`CircularSegmentMeshBuilder`] from a given segment - #[inline] - pub fn new(segment: CircularSegment) -> Self { - Self { - segment, - ..Self::default() - } - } - - /// Sets the number of vertices used for the segment mesh. - #[inline] - #[doc(alias = "vertices")] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } - - /// Sets the uv mode used for the segment mesh - #[inline] - pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { - self.uv_mode = uv_mode; - self - } -} - -impl MeshBuilder for CircularSegmentMeshBuilder { - fn build(&self) -> Mesh { - let resolution = self.resolution as usize; - let mut indices = Vec::with_capacity((resolution - 1) * 3); - let mut positions = Vec::with_capacity(resolution + 1); - let normals = vec![[0.0, 0.0, 1.0]; resolution + 1]; - let mut uvs = Vec::with_capacity(resolution + 1); - - let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; - - // Push the center of the chord. - let midpoint_vertex = self.segment.chord_midpoint(); - positions.push([midpoint_vertex.x, midpoint_vertex.y, 0.0]); - // Compute the UV coordinate of the midpoint vertex. - // This is similar to the computation inside the loop for the arc vertices, - // but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius - // to correctly position the vertex. - let midpoint_uv = Vec2::from_angle(-uv_angle - FRAC_PI_2).mul_add( - Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())), - Vec2::splat(0.5), - ); - uvs.push([midpoint_uv.x, midpoint_uv.y]); - - let first_angle = FRAC_PI_2 - self.segment.half_angle(); - let last_angle = FRAC_PI_2 + self.segment.half_angle(); - let last_i = (self.resolution - 1) as f32; - for i in 0..self.resolution { - let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); - - // Compute the vertex - let vertex = self.segment.radius() * Vec2::from_angle(angle); - // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). - // We accomplish the Y axis flip by negating the angle. - let uv = - Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); - - positions.push([vertex.x, vertex.y, 0.0]); - uvs.push([uv.x, uv.y]); - } - - for i in 1..self.resolution { - // Index 0 is the midpoint of the chord. - indices.extend_from_slice(&[0, i, i + 1]); - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for CircularSegmentMeshBuilder { - fn perimeter(&self) -> Vec { - let (sin, cos) = ops::sin_cos(self.segment.arc.half_angle); - let first_normal = Vec2::new(sin, cos); - let last_normal = Vec2::new(-sin, cos); - vec![ - PerimeterSegment::Flat { - indices: vec![self.resolution, 0, 1], - }, - PerimeterSegment::Smooth { - first_normal, - last_normal, - indices: (1..=self.resolution).collect(), - }, - ] - } -} - -impl Meshable for CircularSegment { - type Output = CircularSegmentMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - CircularSegmentMeshBuilder { - segment: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - /// Converts this sector into a [`Mesh`] using a default [`CircularSegmentMeshBuilder`]. - /// - /// See the documentation of [`CircularSegmentMeshBuilder`] for more details. - fn from(segment: CircularSegment) -> Self { - segment.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`ConvexPolygon`] shape. -/// -/// You must verify that the `vertices` are not concave when constructing this type. You can -/// guarantee this by creating a [`ConvexPolygon`] first, then calling [`ConvexPolygon::mesh()`]. -#[derive(Clone, Debug, Reflect)] -#[reflect(Debug, Clone)] -pub struct ConvexPolygonMeshBuilder { - pub vertices: Vec, -} - -impl Meshable for ConvexPolygon { - type Output = ConvexPolygonMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Self::Output { - vertices: self.vertices().to_vec(), - } - } -} - -impl MeshBuilder for ConvexPolygonMeshBuilder { - fn build(&self) -> Mesh { - let len = self.vertices.len(); - let mut indices = Vec::with_capacity((len - 2) * 3); - let mut positions = Vec::with_capacity(len); - let normals = vec![[0.0, 0.0, 1.0]; len]; - let mut uvs = Vec::with_capacity(len); - - let mut min = Vec2::splat(f32::INFINITY); - let mut max = Vec2::splat(f32::NEG_INFINITY); - - for vertex in &self.vertices { - min = min.min(*vertex); - max = max.max(*vertex); - } - - let size = (max - min).max(Vec2::splat(f32::EPSILON)); - - for vertex in &self.vertices { - positions.push([vertex.x, vertex.y, 0.0]); - let uv = (*vertex - min) / size; - // Map each axis independently into [0, 1] over the polygon's AABB. - uvs.push([uv.x, uv.y]); - } - for i in 2..len as u32 { - indices.extend_from_slice(&[0, i - 1, i]); - } - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for ConvexPolygonMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Flat { - indices: (0..self.vertices.len() as u32).chain([0]).collect(), - }] - } -} - -impl From for Mesh { - fn from(polygon: ConvexPolygon) -> Self { - polygon.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`RegularPolygon`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct RegularPolygonMeshBuilder { - circumradius: f32, - sides: u32, -} - -impl Default for RegularPolygonMeshBuilder { - /// Returns the default [`RegularPolygonMeshBuilder`] with six sides (a hexagon) and a circumradius of `0.5`. - fn default() -> Self { - Self { - circumradius: 0.5, - sides: 6, - } - } -} - -impl RegularPolygonMeshBuilder { - /// Creates a new [`RegularPolygonMeshBuilder`] from the radius of a circumcircle and a number - /// of sides. - /// - /// # Panics - /// - /// Panics in debug mode if `circumradius` is negative, or if `sides` is less than 3. - pub const fn new(circumradius: f32, sides: u32) -> Self { - debug_assert!( - circumradius.is_sign_positive(), - "polygon has a negative radius" - ); - debug_assert!(sides > 2, "polygon has less than 3 sides"); - - Self { - circumradius, - sides, - } - } -} - -impl Meshable for RegularPolygon { - type Output = RegularPolygonMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Self::Output { - circumradius: self.circumcircle.radius, - sides: self.sides, - } - } -} - -impl MeshBuilder for RegularPolygonMeshBuilder { - fn build(&self) -> Mesh { - // The ellipse mesh is just a regular polygon with two radii - Ellipse::new(self.circumradius, self.circumradius) - .mesh_builder() - .resolution(self.sides) - .build() - } -} - -impl Extrudable for RegularPolygonMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Flat { - indices: (0..self.sides).chain([0]).collect(), - }] - } -} - -impl From for Mesh { - fn from(polygon: RegularPolygon) -> Self { - polygon.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct EllipseMeshBuilder { - /// The [`Ellipse`] shape. - pub ellipse: Ellipse, - /// The number of vertices used for the ellipse mesh. - /// The default is `32`. - #[doc(alias = "vertices")] - pub resolution: u32, -} - -impl Default for EllipseMeshBuilder { - fn default() -> Self { - Self { - ellipse: Ellipse::default(), - resolution: 32, - } - } -} - -impl EllipseMeshBuilder { - /// Creates a new [`EllipseMeshBuilder`] from a given half width and half height and a vertex count. - #[inline] - pub const fn new(half_width: f32, half_height: f32, resolution: u32) -> Self { - Self { - ellipse: Ellipse::new(half_width, half_height), - resolution, - } - } - - /// Sets the number of vertices used for the ellipse mesh. - #[inline] - #[doc(alias = "vertices")] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl MeshBuilder for EllipseMeshBuilder { - fn build(&self) -> Mesh { - let resolution = self.resolution as usize; - let mut indices = Vec::with_capacity((resolution - 2) * 3); - let mut positions = Vec::with_capacity(resolution); - let normals = vec![[0.0, 0.0, 1.0]; resolution]; - let mut uvs = Vec::with_capacity(resolution); - - // Add pi/2 so that there is a vertex at the top (sin is 1.0 and cos is 0.0) - let start_angle = FRAC_PI_2; - let step = core::f32::consts::TAU / self.resolution as f32; - - for i in 0..self.resolution { - // Compute vertex position at angle theta - let theta = start_angle + i as f32 * step; - let (sin, cos) = ops::sin_cos(theta); - let x = cos * self.ellipse.half_size.x; - let y = sin * self.ellipse.half_size.y; - - positions.push([x, y, 0.0]); - uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); - } - - for i in 1..(self.resolution - 1) { - indices.extend_from_slice(&[0, i, i + 1]); - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for EllipseMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (0..self.resolution).chain([0]).collect(), - }] - } -} - -impl Meshable for Ellipse { - type Output = EllipseMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - EllipseMeshBuilder { - ellipse: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(ellipse: Ellipse) -> Self { - ellipse.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Segment2d`]. -pub struct Segment2dMeshBuilder { - /// The [`Segment2d`] shape. - pub segment: Segment2d, -} - -impl Segment2dMeshBuilder { - /// Creates a new [`Segment2dMeshBuilder`] from a given segment. - #[inline] - pub const fn new(line: Segment2d) -> Self { - Self { segment: line } - } -} - -impl MeshBuilder for Segment2dMeshBuilder { - fn build(&self) -> Mesh { - let positions = self.segment.vertices.map(|v| v.extend(0.0)).to_vec(); - let indices = Indices::U32(vec![0, 1]); - - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_indices(indices) - } -} - -impl Meshable for Segment2d { - type Output = Segment2dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Segment2dMeshBuilder::new(*self) - } -} - -impl From for Mesh { - /// Converts this segment into a [`Mesh`] using a default [`Segment2dMeshBuilder`]. - fn from(segment: Segment2d) -> Self { - segment.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Polyline2d`] shape. -#[derive(Clone, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Polyline2dMeshBuilder { - polyline: Polyline2d, -} - -impl MeshBuilder for Polyline2dMeshBuilder { - fn build(&self) -> Mesh { - let positions: Vec<_> = self - .polyline - .vertices - .iter() - .map(|v| v.extend(0.0)) - .collect(); - - let indices = Indices::U32( - (0..self.polyline.vertices.len() as u32 - 1) - .flat_map(|i| [i, i + 1]) - .collect(), - ); - - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - } -} - -impl Meshable for Polyline2d { - type Output = Polyline2dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Polyline2dMeshBuilder { - polyline: self.clone(), - } - } -} - -impl From for Mesh { - fn from(polyline: Polyline2d) -> Self { - polyline.mesh_builder().build() - } -} - -/// A builder for creating a [`Mesh`] with an [`Annulus`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct AnnulusMeshBuilder { - /// The [`Annulus`] shape. - pub annulus: Annulus, - - /// The number of vertices used in constructing each concentric circle of the annulus mesh. - /// The default is `32`. - pub resolution: u32, -} - -impl Default for AnnulusMeshBuilder { - fn default() -> Self { - Self { - annulus: Annulus::default(), - resolution: 32, - } - } -} - -impl AnnulusMeshBuilder { - /// Create an [`AnnulusMeshBuilder`] with the given inner radius, outer radius, and angular vertex count. - #[inline] - pub fn new(inner_radius: f32, outer_radius: f32, resolution: u32) -> Self { - Self { - annulus: Annulus::new(inner_radius, outer_radius), - resolution, - } - } - - /// Sets the number of vertices used in constructing the concentric circles of the annulus mesh. - #[inline] - pub fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl MeshBuilder for AnnulusMeshBuilder { - fn build(&self) -> Mesh { - let inner_radius = self.annulus.inner_circle.radius; - let outer_radius = self.annulus.outer_circle.radius; - - let num_vertices = (self.resolution as usize + 1) * 2; - let mut indices = Vec::with_capacity(self.resolution as usize * 6); - let mut positions = Vec::with_capacity(num_vertices); - let mut uvs = Vec::with_capacity(num_vertices); - let normals = vec![[0.0, 0.0, 1.0]; num_vertices]; - - // We have one more set of vertices than might be naïvely expected; - // the vertices at `start_angle` are duplicated for the purposes of UV - // mapping. Here, each iteration places a pair of vertices at a fixed - // angle from the center of the annulus. - let start_angle = FRAC_PI_2; - let step = core::f32::consts::TAU / self.resolution as f32; - for i in 0..=self.resolution { - let theta = start_angle + (i % self.resolution) as f32 * step; - let (sin, cos) = ops::sin_cos(theta); - let inner_pos = [cos * inner_radius, sin * inner_radius, 0.]; - let outer_pos = [cos * outer_radius, sin * outer_radius, 0.]; - positions.push(inner_pos); - positions.push(outer_pos); - - // The first UV direction is radial and the second is angular; - // i.e., a single UV rectangle is stretched around the annulus, with - // its top and bottom meeting as the circle closes. Lines of constant - // U map to circles, and lines of constant V map to radial line segments. - let inner_uv = [0., i as f32 / self.resolution as f32]; - let outer_uv = [1., i as f32 / self.resolution as f32]; - uvs.push(inner_uv); - uvs.push(outer_uv); - } - - // Adjacent pairs of vertices form two triangles with each other; here, - // we are just making sure that they both have the right orientation, - // which is the CCW order of - // `inner_vertex` -> `outer_vertex` -> `next_outer` -> `next_inner` - for i in 0..self.resolution { - let inner_vertex = 2 * i; - let outer_vertex = 2 * i + 1; - let next_inner = inner_vertex + 2; - let next_outer = outer_vertex + 2; - indices.extend_from_slice(&[inner_vertex, outer_vertex, next_outer]); - indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]); - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for AnnulusMeshBuilder { - fn perimeter(&self) -> Vec { - let vert_count = 2 * self.resolution; - vec![ - PerimeterSegment::Smooth { - first_normal: Vec2::NEG_Y, - last_normal: Vec2::NEG_Y, - indices: (0..vert_count).step_by(2).chain([0]).rev().collect(), // Inner hole - }, - PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (1..vert_count).step_by(2).chain([1]).collect(), // Outer perimeter - }, - ] - } -} - -impl Meshable for Annulus { - type Output = AnnulusMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - AnnulusMeshBuilder { - annulus: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(annulus: Annulus) -> Self { - annulus.mesh_builder().build() - } -} - -/// A builder for creating a [`Mesh`] with an [`Rhombus`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct RhombusMeshBuilder { - half_diagonals: Vec2, -} - -impl Default for RhombusMeshBuilder { - /// Returns the default [`RhombusMeshBuilder`] with a half-horizontal and half-vertical diagonal of `0.5`. - fn default() -> Self { - Self { - half_diagonals: Vec2::splat(0.5), - } - } -} - -impl RhombusMeshBuilder { - /// Creates a new [`RhombusMeshBuilder`] from a horizontal and vertical diagonal size. - /// - /// # Panics - /// - /// Panics in debug mode if `horizontal_diagonal` or `vertical_diagonal` is negative. - pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { - debug_assert!( - horizontal_diagonal >= 0.0, - "rhombus has a negative horizontal size", - ); - debug_assert!( - vertical_diagonal >= 0.0, - "rhombus has a negative vertical size" - ); - - Self { - half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), - } - } -} - -impl MeshBuilder for RhombusMeshBuilder { - fn build(&self) -> Mesh { - let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y]; - let positions = vec![ - [hhd, 0.0, 0.0], - [0.0, vhd, 0.0], - [-hhd, 0.0, 0.0], - [0.0, -vhd, 0.0], - ]; - let normals = vec![[0.0, 0.0, 1.0]; 4]; - let uvs = vec![[1.0, 0.5], [0.5, 0.0], [0.0, 0.5], [0.5, 1.0]]; - let indices = Indices::U32(vec![2, 0, 1, 2, 3, 0]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Extrudable for RhombusMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Flat { - indices: vec![0, 1, 2, 3, 0], - }] - } -} - -impl Meshable for Rhombus { - type Output = RhombusMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Self::Output { - half_diagonals: self.half_diagonals, - } - } -} - -impl From for Mesh { - fn from(rhombus: Rhombus) -> Self { - rhombus.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Triangle2d`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Triangle2dMeshBuilder { - triangle: Triangle2d, -} - -impl Triangle2dMeshBuilder { - /// Creates a new [`Triangle2dMeshBuilder`] from the points `a`, `b`, and `c`. - pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { - Self { - triangle: Triangle2d::new(a, b, c), - } - } -} - -impl Meshable for Triangle2d { - type Output = Triangle2dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Self::Output { triangle: *self } - } -} - -impl MeshBuilder for Triangle2dMeshBuilder { - fn build(&self) -> Mesh { - let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.)); - - let positions: Vec<_> = vertices_3d.into(); - let normals = vec![[0.0, 0.0, 1.0]; 3]; - - let uvs: Vec<_> = triangle3d::uv_coords(&Triangle3d::new( - vertices_3d[0], - vertices_3d[1], - vertices_3d[2], - )) - .into(); - - let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; - let indices = if is_ccw { - Indices::U32(vec![0, 1, 2]) - } else { - Indices::U32(vec![2, 1, 0]) - }; - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Extrudable for Triangle2dMeshBuilder { - fn perimeter(&self) -> Vec { - let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; - if is_ccw { - vec![PerimeterSegment::Flat { - indices: vec![0, 1, 2, 0], - }] - } else { - vec![PerimeterSegment::Flat { - indices: vec![2, 1, 0, 2], - }] - } - } -} - -impl From for Mesh { - fn from(triangle: Triangle2d) -> Self { - triangle.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Rectangle`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct RectangleMeshBuilder { - half_size: Vec2, -} - -impl Default for RectangleMeshBuilder { - /// Returns the default [`RectangleMeshBuilder`] with a half-width and half-height of `0.5`. - fn default() -> Self { - Self { - half_size: Vec2::splat(0.5), - } - } -} - -impl RectangleMeshBuilder { - /// Creates a new [`RectangleMeshBuilder`] from a full width and height. - /// - /// # Panics - /// - /// Panics in debug mode if `width` or `height` is negative. - pub const fn new(width: f32, height: f32) -> Self { - debug_assert!(width >= 0.0, "rectangle has a negative width"); - debug_assert!(height >= 0.0, "rectangle has a negative height"); - - Self { - half_size: Vec2::new(width / 2.0, height / 2.0), - } - } -} - -impl MeshBuilder for RectangleMeshBuilder { - fn build(&self) -> Mesh { - let [hw, hh] = [self.half_size.x, self.half_size.y]; - let positions = vec![ - [hw, hh, 0.0], - [-hw, hh, 0.0], - [-hw, -hh, 0.0], - [hw, -hh, 0.0], - ]; - let normals = vec![[0.0, 0.0, 1.0]; 4]; - let uvs = vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; - let indices = Indices::U32(vec![0, 1, 2, 0, 2, 3]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Extrudable for RectangleMeshBuilder { - fn perimeter(&self) -> Vec { - vec![PerimeterSegment::Flat { - indices: vec![0, 1, 2, 3, 0], - }] - } -} - -impl Meshable for Rectangle { - type Output = RectangleMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - RectangleMeshBuilder { - half_size: self.half_size, - } - } -} - -impl From for Mesh { - fn from(rectangle: Rectangle) -> Self { - rectangle.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Capsule2dMeshBuilder { - /// The [`Capsule2d`] shape. - pub capsule: Capsule2d, - /// The number of vertices used for one hemicircle. - /// The total number of vertices for the capsule mesh will be two times the resolution. - /// - /// The default is `16`. - pub resolution: u32, -} - -impl Default for Capsule2dMeshBuilder { - fn default() -> Self { - Self { - capsule: Capsule2d::default(), - resolution: 16, - } - } -} - -impl Capsule2dMeshBuilder { - /// Creates a new [`Capsule2dMeshBuilder`] from a given radius, length, and the number of vertices - /// used for one hemicircle. The total number of vertices for the capsule mesh will be two times the resolution. - #[inline] - pub fn new(radius: f32, length: f32, resolution: u32) -> Self { - Self { - capsule: Capsule2d::new(radius, length), - resolution, - } - } - - /// Sets the number of vertices used for one hemicircle. - /// The total number of vertices for the capsule mesh will be two times the resolution. - #[inline] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } -} - -impl MeshBuilder for Capsule2dMeshBuilder { - fn build(&self) -> Mesh { - // The resolution is the number of vertices for one semicircle - let resolution = self.resolution; - let vertex_count = 2 * resolution; - - // Six extra indices for the two triangles between the semicircles - let mut indices = Vec::with_capacity((resolution as usize - 2) * 2 * 3 + 6); - let mut positions = Vec::with_capacity(vertex_count as usize); - let normals = vec![[0.0, 0.0, 1.0]; vertex_count as usize]; - let mut uvs = Vec::with_capacity(vertex_count as usize); - - let radius = self.capsule.radius; - let step = core::f32::consts::TAU / vertex_count as f32; - - // If the vertex count is even, offset starting angle of top semicircle by half a step - // to position the vertices evenly. - let start_angle = if vertex_count.is_multiple_of(2) { - step / 2.0 - } else { - 0.0 - }; - - // How much the hemicircle radius is of the total half-height of the capsule. - // This is used to prevent the UVs from stretching between the semicircles. - let radius_frac = self.capsule.radius / (self.capsule.half_length + self.capsule.radius); - - // Create top semicircle - for i in 0..resolution { - // Compute vertex position at angle theta - let theta = start_angle + i as f32 * step; - let (sin, cos) = ops::sin_cos(theta); - let (x, y) = (cos * radius, sin * radius + self.capsule.half_length); - - positions.push([x, y, 0.0]); - uvs.push([0.5 * (cos + 1.0), radius_frac * (1.0 - 0.5 * (sin + 1.0))]); - } - - // Add top semicircle indices - for i in 1..resolution - 1 { - indices.extend_from_slice(&[0, i, i + 1]); - } - - // Add indices for top left triangle of the part between the semicircles - indices.extend_from_slice(&[0, resolution - 1, resolution]); - - // Create bottom semicircle - for i in resolution..vertex_count { - // Compute vertex position at angle theta - let theta = start_angle + i as f32 * step; - let (sin, cos) = ops::sin_cos(theta); - let (x, y) = (cos * radius, sin * radius - self.capsule.half_length); - - positions.push([x, y, 0.0]); - uvs.push([0.5 * (cos + 1.0), 1.0 - radius_frac * 0.5 * (sin + 1.0)]); - } - - // Add bottom semicircle indices - for i in 1..resolution - 1 { - indices.extend_from_slice(&[resolution, resolution + i, resolution + i + 1]); - } - - // Add indices for bottom right triangle of the part between the semicircles - indices.extend_from_slice(&[resolution, vertex_count - 1, 0]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } -} - -impl Extrudable for Capsule2dMeshBuilder { - fn perimeter(&self) -> Vec { - let resolution = self.resolution; - let top_semi_indices = (0..resolution).collect(); - let bottom_semi_indices = (resolution..(2 * resolution)).collect(); - vec![ - PerimeterSegment::Smooth { - first_normal: Vec2::X, - last_normal: Vec2::NEG_X, - indices: top_semi_indices, - }, // Top semi-circle - PerimeterSegment::Flat { - indices: vec![resolution - 1, resolution], - }, // Left edge - PerimeterSegment::Smooth { - first_normal: Vec2::NEG_X, - last_normal: Vec2::X, - indices: bottom_semi_indices, - }, // Bottom semi-circle - PerimeterSegment::Flat { - indices: vec![2 * resolution - 1, 0], - }, // Right edge - ] - } -} - -impl Meshable for Capsule2d { - type Output = Capsule2dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Capsule2dMeshBuilder { - capsule: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(capsule: Capsule2d) -> Self { - capsule.mesh_builder().build() - } -} - -/// A builder used for creating a [`Mesh`] with a [`Ring`] shape. -pub struct RingMeshBuilder

-where - P: Primitive2d + Meshable, -{ - pub outer_shape_builder: P::Output, - pub inner_shape_builder: P::Output, -} - -impl

RingMeshBuilder

-where - P: Primitive2d + Meshable, -{ - /// Create a new `RingMeshBuilder

` from a given `Ring

` shape. - pub fn new(ring: &Ring

) -> Self { - Self { - outer_shape_builder: ring.outer_shape.mesh_builder(), - inner_shape_builder: ring.inner_shape.mesh_builder(), - } - } - - /// Apply a function to the inner builders - pub fn with_inner(mut self, func: impl Fn(P::Output) -> P::Output) -> Self { - self.outer_shape_builder = func(self.outer_shape_builder); - self.inner_shape_builder = func(self.inner_shape_builder); - self - } - - fn get_vertex_attributes(&self) -> Option { - fn get_positions(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { - if let VertexAttributeValues::Float32x3(data) = - mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)? - { - Some(data) - } else { - None - } - } - - fn get_uvs(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 2]>> { - if let VertexAttributeValues::Float32x2(data) = - mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0)? - { - Some(data) - } else { - None - } - } - - fn get_normals(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { - if let VertexAttributeValues::Float32x3(data) = - mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL)? - { - Some(data) - } else { - None - } - } - - let mut outer = self.outer_shape_builder.build(); - let mut inner = self.inner_shape_builder.build(); - - assert_eq!( - outer.primitive_topology(), - PrimitiveTopology::TriangleList, - "PrimitiveTopology must be a TriangleList, mesh builder not compatible" - ); - assert_eq!( - inner.primitive_topology(), - PrimitiveTopology::TriangleList, - "PrimitiveTopology must be a TriangleList, mesh builder not compatible" - ); - - Some(RingMeshBuilderVertexAttributes { - outer_positions: mem::take(get_positions(&mut outer)?), - inner_positions: mem::take(get_positions(&mut inner)?), - outer_normals: mem::take(get_normals(&mut outer)?), - inner_normals: mem::take(get_normals(&mut inner)?), - outer_uvs: mem::take(get_uvs(&mut outer)?), - inner_uvs: mem::take(get_uvs(&mut inner)?), - }) - } -} - -struct RingMeshBuilderVertexAttributes { - outer_positions: Vec<[f32; 3]>, - inner_positions: Vec<[f32; 3]>, - outer_normals: Vec<[f32; 3]>, - inner_normals: Vec<[f32; 3]>, - outer_uvs: Vec<[f32; 2]>, - inner_uvs: Vec<[f32; 2]>, -} - -impl

MeshBuilder for RingMeshBuilder

-where - P: Primitive2d + Meshable, -{ - /// Builds a [`Mesh`] based on the configuration in `self`. - /// - /// # Panics - /// - /// Panics if the following assumptions are not met. - /// - /// It is assumed that the inner and outer meshes have the same number of vertices. - /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated - /// a different number of vertices for the inner and outer instances of the primitive. - /// - /// It is assumed that the `primitive_topology` of the mesh returned by - /// the underlying builder is [`PrimitiveTopology::TriangleList`] - /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. - fn build(&self) -> Mesh { - if let Some(RingMeshBuilderVertexAttributes { - outer_uvs, - inner_uvs, - outer_positions, - inner_positions, - outer_normals, - inner_normals, - }) = self.get_vertex_attributes() - && outer_uvs.len() == inner_uvs.len() - && outer_positions.len() == inner_positions.len() - && outer_normals.len() == inner_normals.len() - { - let mut uvs = outer_uvs; - let inner_uvs = inner_positions - .iter() - .zip(&outer_positions) - .zip(&inner_uvs) - .map(|((inner_position, outer_position), inner_uv)| -> [f32; 2] { - const UV_CENTER: Vec2 = Vec2::splat(0.5); - let ip = Vec3::from(*inner_position).truncate(); - let op = Vec3::from(*outer_position).truncate(); - let uv = Vec2::from(*inner_uv) - UV_CENTER; - (uv * ip.length() / op.length() + UV_CENTER).into() - }); - uvs.extend(inner_uvs); - - let mut normals = outer_normals; - normals.extend(inner_normals); - - let points = outer_positions.len() as u32; - let mut indices = Vec::with_capacity(outer_positions.len() * 6); - for i in 0..points { - // for five points: - indices.push(i); // 0 1 2 3 4 - indices.push(i + 1); // 1 2 3 4 0 <- - indices.push(points + i); // 0' 1' 2' 3' 4' - indices.push(points + i); // 0' 1' 2' 3' 4' - indices.push(i + 1); // 1 2 3 4 0 <- - indices.push(points + i + 1); // 1' 2' 3' 4' 0' <- - } - let indices_length = indices.len(); - // Fix up the last pair of triangles (return to start) - if let (_, [_, b, _, _, e, f]) = indices.split_at_mut(indices_length.saturating_sub(6)) - { - *b = 0; - *e = 0; - *f = points; - } - - let mut positions = outer_positions; - positions.extend_from_slice(&inner_positions); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(Indices::U32(indices)) - } else { - panic!("The inner and outer meshes should have the same number of vertices, and have required attributes"); - } - } -} - -impl

Extrudable for RingMeshBuilder

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - /// A list of the indices each representing a part of the perimeter of the mesh. - /// - /// # Panics - /// - /// Panics if the following assumptions are not met. - /// - /// It is assumed that the inner and outer meshes have the same number of vertices. - /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated - /// a different number of vertices for the inner and outer instances of the primitive. - /// - /// It is assumed that the `primitive_topology` of the mesh returned by - /// the underlying builder is [`PrimitiveTopology::TriangleList`] - /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. - fn perimeter(&self) -> Vec { - let outer_vertex_count = self - .get_vertex_attributes() - .filter(|r| r.outer_positions.len() == r.inner_positions.len()) - .expect("The inner and outer meshes should have the same number of vertices, and have required attributes") - .outer_positions - .len(); - - let mut outer_perimeter = self.outer_shape_builder.perimeter(); - let inner_perimeter = - self.inner_shape_builder - .perimeter() - .into_iter() - .rev() - .map(|segment| match segment { - PerimeterSegment::Smooth { - first_normal, - last_normal, - mut indices, - } => PerimeterSegment::Smooth { - first_normal: -last_normal, - last_normal: -first_normal, - indices: { - let outer_perimeter_vertex_count = outer_vertex_count as u32; - indices.reverse(); - for i in &mut indices { - *i += outer_perimeter_vertex_count; - } - indices - }, - }, - PerimeterSegment::Flat { mut indices } => PerimeterSegment::Flat { - indices: { - let outer_perimeter_vertex_count = outer_vertex_count as u32; - indices.reverse(); - for i in &mut indices { - *i += outer_perimeter_vertex_count; - } - indices - }, - }, - }); - - outer_perimeter.extend(inner_perimeter); - outer_perimeter - } -} - -impl

Meshable for Ring

-where - P: Primitive2d + Meshable, -{ - type Output = RingMeshBuilder

; - - fn mesh_builder(&self) -> Self::Output { - RingMeshBuilder::new(self) - } -} - -impl

From> for Mesh -where - P: Primitive2d + Meshable, -{ - fn from(ring: Ring

) -> Self { - ring.mesh_builder().build() - } -} - -#[cfg(test)] -mod tests { - use bevy_math::{ - prelude::Annulus, - primitives::{ConvexPolygon, RegularPolygon}, - FloatOrd, Vec2, - }; - use bevy_platform::collections::HashSet; - - use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; - - fn count_distinct_positions(points: &[[f32; 3]]) -> usize { - let mut map = >::default(); - for point in points { - map.insert(point.map(FloatOrd)); - } - map.len() - } - - #[test] - fn test_annulus() { - let mesh = Annulus::new(1.0, 1.2).mesh_builder().resolution(16).build(); - - assert_eq!( - 32, - count_distinct_positions( - mesh.attribute(Mesh::ATTRIBUTE_POSITION) - .unwrap() - .as_float3() - .unwrap() - ) - ); - } - - /// Sin/cos and multiplication computations result in numbers like 0.4999999. - /// Round these to numbers we expect like 0.5. - fn fix_floats(points: &mut [[f32; N]]) { - for point in points.iter_mut() { - for coord in point.iter_mut() { - let round = (*coord * 2.).round() / 2.; - if (*coord - round).abs() < 0.00001 { - *coord = round; - } - } - } - } - - #[test] - fn test_regular_polygon() { - let mut mesh = Mesh::from(RegularPolygon::new(7.0, 4)); - - let Some(VertexAttributeValues::Float32x3(mut positions)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) - else { - panic!("Expected positions f32x3"); - }; - let Some(VertexAttributeValues::Float32x2(mut uvs)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_UV_0) - else { - panic!("Expected uvs f32x2"); - }; - let Some(VertexAttributeValues::Float32x3(normals)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) - else { - panic!("Expected normals f32x3"); - }; - - fix_floats(&mut positions); - fix_floats(&mut uvs); - - assert_eq!( - [ - [0.0, 7.0, 0.0], - [-7.0, 0.0, 0.0], - [0.0, -7.0, 0.0], - [7.0, 0.0, 0.0], - ], - &positions[..] - ); - - // Note V coordinate increases in the opposite direction to the Y coordinate. - assert_eq!([[0.5, 0.0], [0.0, 0.5], [0.5, 1.0], [1.0, 0.5],], &uvs[..]); - - assert_eq!(&[[0.0, 0.0, 1.0]; 4], &normals[..]); - } - - #[test] - fn test_convex_polygon() { - let polygon = ConvexPolygon::new(vec![ - Vec2::new(-2.0, -1.0), - Vec2::new(2.0, -1.0), - Vec2::new(1.0, 3.0), - Vec2::new(-1.0, 2.0), - ]) - .unwrap(); - - let mut mesh = Mesh::from(polygon); - - let Some(VertexAttributeValues::Float32x3(mut positions)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) - else { - panic!("Expected positions f32x3"); - }; - let Some(VertexAttributeValues::Float32x2(mut uvs)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_UV_0) - else { - panic!("Expected uvs f32x2"); - }; - let Some(VertexAttributeValues::Float32x3(normals)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) - else { - panic!("Expected normals f32x3"); - }; - - fix_floats(&mut positions); - fix_floats(&mut uvs); - - assert_eq!( - [ - [-2.0, -1.0, 0.0], - [2.0, -1.0, 0.0], - [1.0, 3.0, 0.0], - [-1.0, 2.0, 0.0], - ], - &positions[..] - ); - - assert_eq!( - [[0.0, 0.0], [1.0, 0.0], [0.75, 1.0], [0.25, 0.75]], - &uvs[..] - ); - - assert_eq!(&[[0.0, 0.0, 1.0]; 4], &normals[..]); - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/capsule.rs b/crates/bevy_mesh/src/primitives/dim3/capsule.rs deleted file mode 100644 index 3b4161ad69615..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/capsule.rs +++ /dev/null @@ -1,436 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Capsule3d, Vec2, Vec3}; -use bevy_reflect::prelude::*; - -/// Manner in which UV coordinates are distributed vertically. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub enum CapsuleUvProfile { - /// UV space is distributed by how much of the capsule consists of the hemispheres. - #[default] - Aspect, - /// Hemispheres get UV space according to the ratio of latitudes to rings. - Uniform, - /// Upper third of the texture goes to the northern hemisphere, middle third to the cylinder - /// and lower third to the southern one. - Fixed, -} - -/// A builder used for creating a [`Mesh`] with a [`Capsule3d`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Capsule3dMeshBuilder { - /// The [`Capsule3d`] shape. - pub capsule: Capsule3d, - /// The number of horizontal lines subdividing the cylindrical part of the capsule. - /// The default is `0`. - pub rings: u32, - /// The number of vertical lines subdividing the hemispheres of the capsule. - /// The default is `32`. - pub longitudes: u32, - /// The number of horizontal lines subdividing the hemispheres of the capsule. - /// The default is `16`. - pub latitudes: u32, - /// The manner in which UV coordinates are distributed vertically. - /// The default is [`CapsuleUvProfile::Aspect`]. - pub uv_profile: CapsuleUvProfile, -} - -impl Default for Capsule3dMeshBuilder { - fn default() -> Self { - Self { - capsule: Capsule3d::default(), - rings: 0, - longitudes: 32, - latitudes: 16, - uv_profile: CapsuleUvProfile::default(), - } - } -} - -impl Capsule3dMeshBuilder { - /// Creates a new [`Capsule3dMeshBuilder`] from a given radius, height, longitudes, and latitudes. - /// - /// Note that `height` is the distance between the centers of the hemispheres. - /// `radius` will be added to both ends to get the real height of the mesh. - #[inline] - pub fn new(radius: f32, height: f32, longitudes: u32, latitudes: u32) -> Self { - Self { - capsule: Capsule3d::new(radius, height), - longitudes, - latitudes, - ..Default::default() - } - } - - /// Sets the number of horizontal lines subdividing the cylindrical part of the capsule. - #[inline] - pub const fn rings(mut self, rings: u32) -> Self { - self.rings = rings; - self - } - - /// Sets the number of vertical lines subdividing the hemispheres of the capsule. - #[inline] - pub const fn longitudes(mut self, longitudes: u32) -> Self { - self.longitudes = longitudes; - self - } - - /// Sets the number of horizontal lines subdividing the hemispheres of the capsule. - #[inline] - pub const fn latitudes(mut self, latitudes: u32) -> Self { - self.latitudes = latitudes; - self - } - - /// Sets the manner in which UV coordinates are distributed vertically. - #[inline] - pub const fn uv_profile(mut self, uv_profile: CapsuleUvProfile) -> Self { - self.uv_profile = uv_profile; - self - } -} - -impl MeshBuilder for Capsule3dMeshBuilder { - fn build(&self) -> Mesh { - // code adapted from https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db - let Capsule3dMeshBuilder { - capsule, - rings, - longitudes, - latitudes, - uv_profile, - } = *self; - let Capsule3d { - radius, - half_length, - } = capsule; - - let calc_middle = rings > 0; - let half_lats = latitudes / 2; - let half_latsn1 = half_lats - 1; - let half_latsn2 = half_lats - 2; - let ringsp1 = rings + 1; - let lonsp1 = longitudes + 1; - let summit = half_length + radius; - - // Vertex index offsets. - let vert_offset_north_hemi = longitudes; - let vert_offset_north_equator = vert_offset_north_hemi + lonsp1 * half_latsn1; - let vert_offset_cylinder = vert_offset_north_equator + lonsp1; - let vert_offset_south_equator = if calc_middle { - vert_offset_cylinder + lonsp1 * rings - } else { - vert_offset_cylinder - }; - let vert_offset_south_hemi = vert_offset_south_equator + lonsp1; - let vert_offset_south_polar = vert_offset_south_hemi + lonsp1 * half_latsn2; - let vert_offset_south_cap = vert_offset_south_polar + lonsp1; - - // Initialize arrays. - let vert_len = (vert_offset_south_cap + longitudes) as usize; - - let mut vs: Vec = vec![Vec3::ZERO; vert_len]; - let mut vts: Vec = vec![Vec2::ZERO; vert_len]; - let mut vns: Vec = vec![Vec3::ZERO; vert_len]; - - let to_theta = 2.0 * core::f32::consts::PI / longitudes as f32; - let to_phi = core::f32::consts::PI / latitudes as f32; - let to_tex_horizontal = 1.0 / longitudes as f32; - let to_tex_vertical = 1.0 / half_lats as f32; - - let vt_aspect_ratio = match uv_profile { - CapsuleUvProfile::Aspect => radius / (2.0 * half_length + radius + radius), - CapsuleUvProfile::Uniform => half_lats as f32 / (ringsp1 + latitudes) as f32, - CapsuleUvProfile::Fixed => 1.0 / 3.0, - }; - let vt_aspect_north = 1.0 - vt_aspect_ratio; - let vt_aspect_south = vt_aspect_ratio; - - let mut theta_cartesian: Vec = vec![Vec2::ZERO; longitudes as usize]; - let mut rho_theta_cartesian: Vec = vec![Vec2::ZERO; longitudes as usize]; - let mut s_texture_cache: Vec = vec![0.0; lonsp1 as usize]; - - for j in 0..longitudes as usize { - let jf = j as f32; - let s_texture_polar = 1.0 - ((jf + 0.5) * to_tex_horizontal); - let theta = jf * to_theta; - - theta_cartesian[j] = Vec2::from_angle(theta); - rho_theta_cartesian[j] = radius * theta_cartesian[j]; - - // North. - vs[j] = Vec3::new(0.0, summit, 0.0); - vts[j] = Vec2::new(s_texture_polar, 1.0); - vns[j] = Vec3::Y; - - // South. - let idx = vert_offset_south_cap as usize + j; - vs[idx] = Vec3::new(0.0, -summit, 0.0); - vts[idx] = Vec2::new(s_texture_polar, 0.0); - vns[idx] = Vec3::new(0.0, -1.0, 0.0); - } - - // Equatorial vertices. - for (j, s_texture_cache_j) in s_texture_cache.iter_mut().enumerate().take(lonsp1 as usize) { - let s_texture = 1.0 - j as f32 * to_tex_horizontal; - *s_texture_cache_j = s_texture; - - // Wrap to first element upon reaching last. - let j_mod = j % longitudes as usize; - let tc = theta_cartesian[j_mod]; - let rtc = rho_theta_cartesian[j_mod]; - - // North equator. - let idxn = vert_offset_north_equator as usize + j; - vs[idxn] = Vec3::new(rtc.x, half_length, -rtc.y); - vts[idxn] = Vec2::new(s_texture, vt_aspect_north); - vns[idxn] = Vec3::new(tc.x, 0.0, -tc.y); - - // South equator. - let idxs = vert_offset_south_equator as usize + j; - vs[idxs] = Vec3::new(rtc.x, -half_length, -rtc.y); - vts[idxs] = Vec2::new(s_texture, vt_aspect_south); - vns[idxs] = Vec3::new(tc.x, 0.0, -tc.y); - } - - // Hemisphere vertices. - for i in 0..half_latsn1 { - let ip1f = i as f32 + 1.0; - let phi = ip1f * to_phi; - - // For coordinates. - let (sin_phi_south, cos_phi_south) = ops::sin_cos(phi); - - // Symmetrical hemispheres mean cosine and sine only needs - // to be calculated once. - let cos_phi_north = sin_phi_south; - let sin_phi_north = -cos_phi_south; - - let rho_cos_phi_north = radius * cos_phi_north; - let rho_sin_phi_north = radius * sin_phi_north; - let z_offset_north = half_length - rho_sin_phi_north; - - let rho_cos_phi_south = radius * cos_phi_south; - let rho_sin_phi_south = radius * sin_phi_south; - let z_offset_sout = -half_length - rho_sin_phi_south; - - // For texture coordinates. - let t_tex_fac = ip1f * to_tex_vertical; - let cmpl_tex_fac = 1.0 - t_tex_fac; - let t_tex_north = cmpl_tex_fac + vt_aspect_north * t_tex_fac; - let t_tex_south = cmpl_tex_fac * vt_aspect_south; - - let i_lonsp1 = i * lonsp1; - let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1; - let vert_curr_lat_south = vert_offset_south_hemi + i_lonsp1; - - for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1 as usize) { - let j_mod = j % longitudes as usize; - - let tc = theta_cartesian[j_mod]; - - // North hemisphere. - let idxn = vert_curr_lat_north as usize + j; - vs[idxn] = Vec3::new( - rho_cos_phi_north * tc.x, - z_offset_north, - -rho_cos_phi_north * tc.y, - ); - vts[idxn] = Vec2::new(*s_texture, t_tex_north); - vns[idxn] = Vec3::new(cos_phi_north * tc.x, -sin_phi_north, -cos_phi_north * tc.y); - - // South hemisphere. - let idxs = vert_curr_lat_south as usize + j; - vs[idxs] = Vec3::new( - rho_cos_phi_south * tc.x, - z_offset_sout, - -rho_cos_phi_south * tc.y, - ); - vts[idxs] = Vec2::new(*s_texture, t_tex_south); - vns[idxs] = Vec3::new(cos_phi_south * tc.x, -sin_phi_south, -cos_phi_south * tc.y); - } - } - - // Cylinder vertices. - if calc_middle { - // Exclude both origin and destination edges - // (North and South equators) from the interpolation. - let to_fac = 1.0 / ringsp1 as f32; - let mut idx_cyl_lat = vert_offset_cylinder as usize; - - for h in 1..ringsp1 { - let fac = h as f32 * to_fac; - let cmpl_fac = 1.0 - fac; - let t_texture = cmpl_fac * vt_aspect_north + fac * vt_aspect_south; - let z = half_length - 2.0 * half_length * fac; - - for (j, s_texture) in s_texture_cache.iter().enumerate().take(lonsp1 as usize) { - let j_mod = j % longitudes as usize; - let tc = theta_cartesian[j_mod]; - let rtc = rho_theta_cartesian[j_mod]; - - vs[idx_cyl_lat] = Vec3::new(rtc.x, z, -rtc.y); - vts[idx_cyl_lat] = Vec2::new(*s_texture, t_texture); - vns[idx_cyl_lat] = Vec3::new(tc.x, 0.0, -tc.y); - - idx_cyl_lat += 1; - } - } - } - - // Triangle indices. - - // Stride is 3 for polar triangles; - // stride is 6 for two triangles forming a quad. - let lons3 = longitudes * 3; - let lons6 = longitudes * 6; - let hemi_lons = half_latsn1 * lons6; - - let tri_offset_north_hemi = lons3; - let tri_offset_cylinder = tri_offset_north_hemi + hemi_lons; - let tri_offset_south_hemi = tri_offset_cylinder + ringsp1 * lons6; - let tri_offset_south_cap = tri_offset_south_hemi + hemi_lons; - - let fs_len = tri_offset_south_cap + lons3; - let mut tris: Vec = vec![0; fs_len as usize]; - - // Polar caps. - let mut i = 0; - let mut k = 0; - let mut m = tri_offset_south_cap as usize; - while i < longitudes { - // North. - tris[k] = i; - tris[k + 1] = vert_offset_north_hemi + i; - tris[k + 2] = vert_offset_north_hemi + i + 1; - - // South. - tris[m] = vert_offset_south_cap + i; - tris[m + 1] = vert_offset_south_polar + i + 1; - tris[m + 2] = vert_offset_south_polar + i; - - i += 1; - k += 3; - m += 3; - } - - // Hemispheres. - - let mut i = 0; - let mut k = tri_offset_north_hemi as usize; - let mut m = tri_offset_south_hemi as usize; - - while i < half_latsn1 { - let i_lonsp1 = i * lonsp1; - - let vert_curr_lat_north = vert_offset_north_hemi + i_lonsp1; - let vert_next_lat_north = vert_curr_lat_north + lonsp1; - - let vert_curr_lat_south = vert_offset_south_equator + i_lonsp1; - let vert_next_lat_south = vert_curr_lat_south + lonsp1; - - let mut j = 0; - while j < longitudes { - // North. - let north00 = vert_curr_lat_north + j; - let north01 = vert_next_lat_north + j; - let north11 = vert_next_lat_north + j + 1; - let north10 = vert_curr_lat_north + j + 1; - - tris[k] = north00; - tris[k + 1] = north11; - tris[k + 2] = north10; - - tris[k + 3] = north00; - tris[k + 4] = north01; - tris[k + 5] = north11; - - // South. - let south00 = vert_curr_lat_south + j; - let south01 = vert_next_lat_south + j; - let south11 = vert_next_lat_south + j + 1; - let south10 = vert_curr_lat_south + j + 1; - - tris[m] = south00; - tris[m + 1] = south11; - tris[m + 2] = south10; - - tris[m + 3] = south00; - tris[m + 4] = south01; - tris[m + 5] = south11; - - j += 1; - k += 6; - m += 6; - } - - i += 1; - } - - // Cylinder. - let mut i = 0; - let mut k = tri_offset_cylinder as usize; - - while i < ringsp1 { - let vert_curr_lat = vert_offset_north_equator + i * lonsp1; - let vert_next_lat = vert_curr_lat + lonsp1; - - let mut j = 0; - while j < longitudes { - let cy00 = vert_curr_lat + j; - let cy01 = vert_next_lat + j; - let cy11 = vert_next_lat + j + 1; - let cy10 = vert_curr_lat + j + 1; - - tris[k] = cy00; - tris[k + 1] = cy11; - tris[k + 2] = cy10; - - tris[k + 3] = cy00; - tris[k + 4] = cy01; - tris[k + 5] = cy11; - - j += 1; - k += 6; - } - - i += 1; - } - - let vs: Vec<[f32; 3]> = vs.into_iter().map(Into::into).collect(); - let vns: Vec<[f32; 3]> = vns.into_iter().map(Into::into).collect(); - let vts: Vec<[f32; 2]> = vts.into_iter().map(Into::into).collect(); - - assert_eq!(vs.len(), vert_len); - assert_eq!(tris.len(), fs_len as usize); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts) - .with_inserted_indices(Indices::U32(tris)) - } -} - -impl Meshable for Capsule3d { - type Output = Capsule3dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Capsule3dMeshBuilder { - capsule: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(capsule: Capsule3d) -> Self { - capsule.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cone.rs b/crates/bevy_mesh/src/primitives/dim3/cone.rs deleted file mode 100644 index 9961cc2fdc25e..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/cone.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Cone, Vec3}; -use bevy_reflect::prelude::*; - -/// Anchoring options for [`ConeMeshBuilder`] -#[derive(Debug, Copy, Clone, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub enum ConeAnchor { - #[default] - /// Midpoint between the tip of the cone and the center of its base. - MidPoint, - /// The Tip of the triangle - Tip, - /// The center of the base circle - Base, -} - -/// A builder used for creating a [`Mesh`] with a [`Cone`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct ConeMeshBuilder { - /// The [`Cone`] shape. - pub cone: Cone, - /// The number of vertices used for the base of the cone. - /// - /// The default is `32`. - pub resolution: u32, - /// The anchor point for the cone mesh, defaults to the midpoint between - /// the tip of the cone and the center of its base - pub anchor: ConeAnchor, -} - -impl Default for ConeMeshBuilder { - fn default() -> Self { - Self { - cone: Cone::default(), - resolution: 32, - anchor: ConeAnchor::default(), - } - } -} - -impl ConeMeshBuilder { - /// Creates a new [`ConeMeshBuilder`] from a given radius, height, - /// and number of vertices used for the base of the cone. - #[inline] - pub const fn new(radius: f32, height: f32, resolution: u32) -> Self { - Self { - cone: Cone { radius, height }, - resolution, - anchor: ConeAnchor::MidPoint, - } - } - - /// Sets the number of vertices used for the base of the cone. - #[inline] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } - - /// Sets a custom anchor point for the mesh - #[inline] - pub const fn anchor(mut self, anchor: ConeAnchor) -> Self { - self.anchor = anchor; - self - } -} - -impl MeshBuilder for ConeMeshBuilder { - fn build(&self) -> Mesh { - let half_height = self.cone.height / 2.0; - - // `resolution` vertices for the base, `resolution` vertices for the bottom of the lateral surface, - // and one vertex for the tip. - let num_vertices = self.resolution as usize * 2 + 1; - let num_indices = self.resolution as usize * 6 - 6; - - let mut positions = Vec::with_capacity(num_vertices); - let mut normals = Vec::with_capacity(num_vertices); - let mut uvs = Vec::with_capacity(num_vertices); - let mut indices = Vec::with_capacity(num_indices); - - // Tip - positions.push([0.0, half_height, 0.0]); - - // The tip doesn't have a singular normal that works correctly. - // We use an invalid normal here so that it becomes NaN in the fragment shader - // and doesn't affect the overall shading. This might seem hacky, but it's one of - // the only ways to get perfectly smooth cones without creases or other shading artifacts. - // - // Note that this requires that normals are not normalized in the vertex shader, - // as that would make the entire triangle invalid and make the cone appear as black. - normals.push([0.0, 0.0, 0.0]); - - // The UVs of the cone are in polar coordinates, so it's like projecting a circle texture from above. - // The center of the texture is at the center of the lateral surface, at the tip of the cone. - uvs.push([0.5, 0.5]); - - // Now we build the lateral surface, the side of the cone. - - // The vertex normals will be perpendicular to the surface. - // - // Here we get the slope of a normal and use it for computing - // the multiplicative inverse of the length of a vector in the direction - // of the normal. This allows us to normalize vertex normals efficiently. - let normal_slope = self.cone.radius / self.cone.height; - // Equivalent to Vec2::new(1.0, slope).length().recip() - let normalization_factor = (1.0 + normal_slope * normal_slope).sqrt().recip(); - - // How much the angle changes at each step - let step_theta = core::f32::consts::TAU / self.resolution as f32; - - // Add vertices for the bottom of the lateral surface. - for segment in 0..self.resolution { - let theta = segment as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - // The vertex normal perpendicular to the side - let normal = Vec3::new(cos, normal_slope, sin) * normalization_factor; - - positions.push([self.cone.radius * cos, -half_height, self.cone.radius * sin]); - normals.push(normal.to_array()); - uvs.push([0.5 + cos * 0.5, 0.5 + sin * 0.5]); - } - - // Add indices for the lateral surface. Each triangle is formed by the tip - // and two vertices at the base. - for j in 1..self.resolution { - indices.extend_from_slice(&[0, j + 1, j]); - } - - // Close the surface with a triangle between the tip, first base vertex, and last base vertex. - indices.extend_from_slice(&[0, 1, self.resolution]); - - // Now we build the actual base of the cone. - - let index_offset = positions.len() as u32; - - // Add base vertices. - for i in 0..self.resolution { - let theta = i as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - positions.push([cos * self.cone.radius, -half_height, sin * self.cone.radius]); - normals.push([0.0, -1.0, 0.0]); - uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); - } - - // Add base indices. - for i in 1..(self.resolution - 1) { - indices.extend_from_slice(&[index_offset, index_offset + i, index_offset + i + 1]); - } - - // Offset the vertex positions Y axis to match the anchor - match self.anchor { - ConeAnchor::Tip => positions.iter_mut().for_each(|p| p[1] -= half_height), - ConeAnchor::Base => positions.iter_mut().for_each(|p| p[1] += half_height), - ConeAnchor::MidPoint => (), - }; - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Cone { - type Output = ConeMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - ConeMeshBuilder { - cone: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(cone: Cone) -> Self { - cone.mesh_builder().build() - } -} - -#[cfg(test)] -mod tests { - use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; - use bevy_math::{primitives::Cone, Vec2}; - - /// Rounds floats to handle floating point error in tests. - fn round_floats(points: &mut [[f32; N]]) { - for point in points.iter_mut() { - for coord in point.iter_mut() { - let round = (*coord * 100.0).round() / 100.0; - if (*coord - round).abs() < 0.00001 { - *coord = round; - } - } - } - } - - #[test] - fn cone_mesh() { - let mut mesh = Cone { - radius: 0.5, - height: 1.0, - } - .mesh_builder() - .resolution(4) - .build(); - - let Some(VertexAttributeValues::Float32x3(mut positions)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) - else { - panic!("Expected positions f32x3"); - }; - let Some(VertexAttributeValues::Float32x3(mut normals)) = - mesh.remove_attribute(Mesh::ATTRIBUTE_NORMAL) - else { - panic!("Expected normals f32x3"); - }; - - round_floats(&mut positions); - round_floats(&mut normals); - - // Vertex positions - assert_eq!( - [ - // Tip - [0.0, 0.5, 0.0], - // Lateral surface - [0.5, -0.5, 0.0], - [0.0, -0.5, 0.5], - [-0.5, -0.5, 0.0], - [0.0, -0.5, -0.5], - // Base - [0.5, -0.5, 0.0], - [0.0, -0.5, 0.5], - [-0.5, -0.5, 0.0], - [0.0, -0.5, -0.5], - ], - &positions[..] - ); - - // Vertex normals - let [x, y] = Vec2::new(0.5, -1.0).perp().normalize().to_array(); - assert_eq!( - &[ - // Tip - [0.0, 0.0, 0.0], - // Lateral surface - [x, y, 0.0], - [0.0, y, x], - [-x, y, 0.0], - [0.0, y, -x], - // Base - [0.0, -1.0, 0.0], - [0.0, -1.0, 0.0], - [0.0, -1.0, 0.0], - [0.0, -1.0, 0.0], - ], - &normals[..] - ); - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs deleted file mode 100644 index 3c738b9ad6c0b..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::ConicalFrustum, Vec3}; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`ConicalFrustum`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct ConicalFrustumMeshBuilder { - /// The [`ConicalFrustum`] shape. - pub frustum: ConicalFrustum, - /// The number of vertices used for the top and bottom of the conical frustum. - /// - /// The default is `32`. - pub resolution: u32, - /// The number of horizontal lines subdividing the lateral surface of the conical frustum. - /// - /// The default is `1`. - pub segments: u32, -} - -impl Default for ConicalFrustumMeshBuilder { - fn default() -> Self { - Self { - frustum: ConicalFrustum::default(), - resolution: 32, - segments: 1, - } - } -} - -impl ConicalFrustumMeshBuilder { - /// Creates a new [`ConicalFrustumMeshBuilder`] from the given top and bottom radii, a height, - /// and a resolution used for the top and bottom. - #[inline] - pub const fn new(radius_top: f32, radius_bottom: f32, height: f32, resolution: u32) -> Self { - Self { - frustum: ConicalFrustum { - radius_top, - radius_bottom, - height, - }, - resolution, - segments: 1, - } - } - - /// Sets the number of vertices used for the top and bottom of the conical frustum. - #[inline] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } - - /// Sets the number of horizontal lines subdividing the lateral surface of the conical frustum. - #[inline] - pub const fn segments(mut self, segments: u32) -> Self { - self.segments = segments; - self - } -} - -impl MeshBuilder for ConicalFrustumMeshBuilder { - fn build(&self) -> Mesh { - debug_assert!(self.resolution > 2); - debug_assert!(self.segments > 0); - - let ConicalFrustum { - radius_top, - radius_bottom, - height, - } = self.frustum; - let half_height = height / 2.0; - - let num_rings = self.segments + 1; - let num_vertices = (self.resolution * 2 + num_rings * (self.resolution + 1)) as usize; - let num_faces = self.resolution * (num_rings - 2); - let num_indices = ((2 * num_faces + 2 * (self.resolution - 1) * 2) * 3) as usize; - - let mut positions = Vec::with_capacity(num_vertices); - let mut normals = Vec::with_capacity(num_vertices); - let mut uvs = Vec::with_capacity(num_vertices); - let mut indices = Vec::with_capacity(num_indices); - - let step_theta = core::f32::consts::TAU / self.resolution as f32; - let step_y = height / self.segments as f32; - let step_radius = (radius_top - radius_bottom) / self.segments as f32; - - // Rings - for ring in 0..num_rings { - let y = -half_height + ring as f32 * step_y; - let radius = radius_bottom + ring as f32 * step_radius; - - for segment in 0..=self.resolution { - let theta = segment as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - positions.push([radius * cos, y, radius * sin]); - normals.push( - Vec3::new(cos, (radius_bottom - radius_top) / height, sin) - .normalize() - .to_array(), - ); - uvs.push([ - segment as f32 / self.resolution as f32, - ring as f32 / self.segments as f32, - ]); - } - } - - // Lateral surface - for i in 0..self.segments { - let ring = i * (self.resolution + 1); - let next_ring = (i + 1) * (self.resolution + 1); - - for j in 0..self.resolution { - indices.extend_from_slice(&[ - ring + j, - next_ring + j, - ring + j + 1, - next_ring + j, - next_ring + j + 1, - ring + j + 1, - ]); - } - } - - // Caps - let mut build_cap = |top: bool, radius: f32| { - let offset = positions.len() as u32; - let (y, normal_y, winding) = if top { - (half_height, 1.0, (1, 0)) - } else { - (-half_height, -1.0, (0, 1)) - }; - - for i in 0..self.resolution { - let theta = i as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - positions.push([cos * radius, y, sin * radius]); - normals.push([0.0, normal_y, 0.0]); - uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); - } - - for i in 1..(self.resolution - 1) { - indices.extend_from_slice(&[ - offset, - offset + i + winding.0, - offset + i + winding.1, - ]); - } - }; - - build_cap(true, radius_top); - build_cap(false, radius_bottom); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for ConicalFrustum { - type Output = ConicalFrustumMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - ConicalFrustumMeshBuilder { - frustum: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(frustum: ConicalFrustum) -> Self { - frustum.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs deleted file mode 100644 index 262eb1b792d04..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Cuboid, Vec3}; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct CuboidMeshBuilder { - half_size: Vec3, -} - -impl Default for CuboidMeshBuilder { - /// Returns the default [`CuboidMeshBuilder`] with a width, height, and depth of `1.0`. - fn default() -> Self { - Self { - half_size: Vec3::splat(0.5), - } - } -} - -impl MeshBuilder for CuboidMeshBuilder { - fn build(&self) -> Mesh { - let min = -self.half_size; - let max = self.half_size; - - // Suppose Y-up right hand, and camera look from +Z to -Z - let vertices = &[ - // Front - ([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]), - ([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]), - ([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]), - ([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]), - // Back - ([min.x, max.y, min.z], [0.0, 0.0, -1.0], [1.0, 0.0]), - ([max.x, max.y, min.z], [0.0, 0.0, -1.0], [0.0, 0.0]), - ([max.x, min.y, min.z], [0.0, 0.0, -1.0], [0.0, 1.0]), - ([min.x, min.y, min.z], [0.0, 0.0, -1.0], [1.0, 1.0]), - // Right - ([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]), - ([max.x, max.y, min.z], [1.0, 0.0, 0.0], [1.0, 0.0]), - ([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]), - ([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]), - // Left - ([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]), - ([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]), - ([min.x, max.y, min.z], [-1.0, 0.0, 0.0], [0.0, 1.0]), - ([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]), - // Top - ([max.x, max.y, min.z], [0.0, 1.0, 0.0], [1.0, 0.0]), - ([min.x, max.y, min.z], [0.0, 1.0, 0.0], [0.0, 0.0]), - ([min.x, max.y, max.z], [0.0, 1.0, 0.0], [0.0, 1.0]), - ([max.x, max.y, max.z], [0.0, 1.0, 0.0], [1.0, 1.0]), - // Bottom - ([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]), - ([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]), - ([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]), - ([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]), - ]; - - let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect(); - let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); - let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); - - let indices = Indices::U32(vec![ - 0, 1, 2, 2, 3, 0, // front - 4, 5, 6, 6, 7, 4, // back - 8, 9, 10, 10, 11, 8, // right - 12, 13, 14, 14, 15, 12, // left - 16, 17, 18, 18, 19, 16, // top - 20, 21, 22, 22, 23, 20, // bottom - ]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(indices) - } -} - -impl Meshable for Cuboid { - type Output = CuboidMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - CuboidMeshBuilder { - half_size: self.half_size, - } - } -} - -impl From for Mesh { - fn from(cuboid: Cuboid) -> Self { - cuboid.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs deleted file mode 100644 index 13a5181bca772..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Cylinder}; -use bevy_reflect::prelude::*; - -/// Anchoring options for [`CylinderMeshBuilder`] -#[derive(Debug, Copy, Clone, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub enum CylinderAnchor { - #[default] - /// Midpoint between the top and bottom caps of the cylinder - MidPoint, - /// The center of the top circle cap - Top, - /// The center of the bottom circle cap - Bottom, -} - -/// A builder used for creating a [`Mesh`] with a [`Cylinder`] shape. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct CylinderMeshBuilder { - /// The [`Cylinder`] shape. - pub cylinder: Cylinder, - /// The number of vertices used for the top and bottom of the cylinder. - /// - /// The default is `32`. - pub resolution: u32, - /// The number of segments along the height of the cylinder. - /// Must be greater than `0` for geometry to be generated. - /// - /// The default is `1`. - pub segments: u32, - /// If set to `true`, the cylinder caps (flat circle faces) are built, - /// otherwise the mesh will be a shallow tube - pub caps: bool, - /// The anchor point for the cylinder mesh, defaults to the midpoint between - /// the top and bottom caps - pub anchor: CylinderAnchor, -} - -impl Default for CylinderMeshBuilder { - fn default() -> Self { - Self { - cylinder: Cylinder::default(), - resolution: 32, - segments: 1, - caps: true, - anchor: CylinderAnchor::default(), - } - } -} - -impl CylinderMeshBuilder { - /// Creates a new [`CylinderMeshBuilder`] from the given radius, a height, - /// and a resolution used for the top and bottom. - #[inline] - pub fn new(radius: f32, height: f32, resolution: u32) -> Self { - Self { - cylinder: Cylinder::new(radius, height), - resolution, - ..Default::default() - } - } - - /// Sets the number of vertices used for the top and bottom of the cylinder. - #[inline] - pub const fn resolution(mut self, resolution: u32) -> Self { - self.resolution = resolution; - self - } - - /// Sets the number of segments along the height of the cylinder. - /// Must be greater than `0` for geometry to be generated. - #[inline] - pub const fn segments(mut self, segments: u32) -> Self { - self.segments = segments; - self - } - - /// Ignore the cylinder caps, making the mesh a shallow tube instead - #[inline] - pub const fn without_caps(mut self) -> Self { - self.caps = false; - self - } - - /// Sets a custom anchor point for the mesh - #[inline] - pub const fn anchor(mut self, anchor: CylinderAnchor) -> Self { - self.anchor = anchor; - self - } -} - -impl MeshBuilder for CylinderMeshBuilder { - fn build(&self) -> Mesh { - let resolution = self.resolution; - let segments = self.segments; - - debug_assert!(resolution > 2); - debug_assert!(segments > 0); - - let num_rings = segments + 1; - let num_vertices = resolution * 2 + num_rings * (resolution + 1); - let num_faces = resolution * (num_rings - 2); - let num_indices = (2 * num_faces + 2 * (resolution - 1) * 2) * 3; - - let mut positions = Vec::with_capacity(num_vertices as usize); - let mut normals = Vec::with_capacity(num_vertices as usize); - let mut uvs = Vec::with_capacity(num_vertices as usize); - let mut indices = Vec::with_capacity(num_indices as usize); - - let step_theta = core::f32::consts::TAU / resolution as f32; - let step_y = 2.0 * self.cylinder.half_height / segments as f32; - - // rings - - for ring in 0..num_rings { - let y = -self.cylinder.half_height + ring as f32 * step_y; - - for segment in 0..=resolution { - let theta = segment as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - positions.push([self.cylinder.radius * cos, y, self.cylinder.radius * sin]); - normals.push([cos, 0., sin]); - uvs.push([ - segment as f32 / resolution as f32, - ring as f32 / segments as f32, - ]); - } - } - - // barrel skin - - for i in 0..segments { - let ring = i * (resolution + 1); - let next_ring = (i + 1) * (resolution + 1); - - for j in 0..resolution { - indices.extend_from_slice(&[ - ring + j, - next_ring + j, - ring + j + 1, - next_ring + j, - next_ring + j + 1, - ring + j + 1, - ]); - } - } - - // caps - if self.caps { - let mut build_cap = |top: bool| { - let offset = positions.len() as u32; - let (y, normal_y, winding) = if top { - (self.cylinder.half_height, 1., (1, 0)) - } else { - (-self.cylinder.half_height, -1., (0, 1)) - }; - - for i in 0..self.resolution { - let theta = i as f32 * step_theta; - let (sin, cos) = ops::sin_cos(theta); - - positions.push([cos * self.cylinder.radius, y, sin * self.cylinder.radius]); - normals.push([0.0, normal_y, 0.0]); - uvs.push([0.5 * (cos + 1.0), 1.0 - 0.5 * (sin + 1.0)]); - } - - for i in 1..(self.resolution - 1) { - indices.extend_from_slice(&[ - offset, - offset + i + winding.0, - offset + i + winding.1, - ]); - } - }; - - build_cap(true); - build_cap(false); - } - - // Offset the vertex positions Y axis to match the anchor - match self.anchor { - CylinderAnchor::Top => positions - .iter_mut() - .for_each(|p| p[1] -= self.cylinder.half_height), - CylinderAnchor::Bottom => positions - .iter_mut() - .for_each(|p| p[1] += self.cylinder.half_height), - CylinderAnchor::MidPoint => (), - }; - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Cylinder { - type Output = CylinderMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - CylinderMeshBuilder { - cylinder: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(cylinder: Cylinder) -> Self { - cylinder.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/mod.rs b/crates/bevy_mesh/src/primitives/dim3/mod.rs deleted file mode 100644 index 21cd80b3a420f..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod capsule; -mod cone; -mod conical_frustum; -mod cuboid; -mod cylinder; -mod plane; -mod polyline3d; -mod segment3d; -mod sphere; -mod tetrahedron; -mod torus; -pub(crate) mod triangle3d; - -pub use capsule::*; -pub use cone::*; -pub use conical_frustum::*; -pub use cuboid::*; -pub use cylinder::*; -pub use plane::*; -pub use sphere::*; -pub use tetrahedron::*; -pub use torus::*; -pub use triangle3d::*; diff --git a/crates/bevy_mesh/src/primitives/dim3/plane.rs b/crates/bevy_mesh/src/primitives/dim3/plane.rs deleted file mode 100644 index 98c499b901b6c..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/plane.rs +++ /dev/null @@ -1,206 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Plane3d, Dir3, Quat, Vec2, Vec3}; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct PlaneMeshBuilder { - /// The [`Plane3d`] shape. - pub plane: Plane3d, - /// The number of subdivisions along the X axis. - /// - /// 0 - is the original plane geometry, the 4 points in the XZ plane. - /// - /// 1 - adds a vertex in the middle of the X axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the Z axis. - /// - /// 2 - adds 2 vertices along the X axis, resulting in a plane with 3 quads / 6 triangles. - /// - /// and so on... - pub subdivisions_x: u32, - - /// The number of subdivisions along the Z axis. - /// - /// 0 - is the original plane geometry, the 4 points in the XZ plane. - /// - /// 1 - adds a vertex in the middle of the Z axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the X axis. - /// - /// 2 - adds 2 vertices along the Z axis, resulting in a plane with 3 quads / 6 triangles. - /// - /// and so on... - pub subdivisions_z: u32, -} - -impl PlaneMeshBuilder { - /// Creates a new [`PlaneMeshBuilder`] from a given normal and size. - #[inline] - pub fn new(normal: Dir3, size: Vec2) -> Self { - Self { - plane: Plane3d { - normal, - half_size: size / 2.0, - }, - subdivisions_x: 0, - subdivisions_z: 0, - } - } - - /// Creates a new [`PlaneMeshBuilder`] from the given size, with the normal pointing upwards. - #[inline] - pub fn from_size(size: Vec2) -> Self { - Self { - plane: Plane3d { - half_size: size / 2.0, - ..Default::default() - }, - subdivisions_x: 0, - subdivisions_z: 0, - } - } - - /// Creates a new [`PlaneMeshBuilder`] from the given length, with the normal pointing upwards, - /// and the resulting [`PlaneMeshBuilder`] being a square. - #[inline] - pub fn from_length(length: f32) -> Self { - Self { - plane: Plane3d { - half_size: Vec2::splat(length) / 2.0, - ..Default::default() - }, - subdivisions_x: 0, - subdivisions_z: 0, - } - } - - /// Sets the normal of the plane, aka the direction the plane is facing. - #[inline] - #[doc(alias = "facing")] - pub fn normal(mut self, normal: Dir3) -> Self { - self.plane = Plane3d { - normal, - ..self.plane - }; - self - } - - /// Sets the size of the plane mesh. - #[inline] - pub fn size(mut self, width: f32, height: f32) -> Self { - self.plane.half_size = Vec2::new(width, height) / 2.0; - self - } - - /// Sets the subdivisions of the plane mesh. - /// - /// 0 - is the original plane geometry, the 4 points in the XZ plane. - /// - /// 1 - is split by 1 line in the middle of the plane on both the X axis and the Z axis, - /// resulting in a plane with 4 quads / 8 triangles. - /// - /// 2 - is a plane split by 2 lines on both the X and Z axes, subdividing the plane into 3 - /// equal sections along each axis, resulting in a plane with 9 quads / 18 triangles. - #[inline] - pub fn subdivisions(mut self, subdivisions: u32) -> Self { - self.subdivisions_x = subdivisions; - self.subdivisions_z = subdivisions; - self - } - - #[inline] - /// The number of subdivisions along the X axis. - /// - /// 0 - is the original plane geometry, the 4 points in the XZ plane. - /// - /// 1 - adds a vertex in the middle of the X axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the Z axis. - /// - /// 2 - adds 2 vertices along the X axis, resulting in a plane with 3 quads / 6 triangles. - /// - /// and so on... - pub fn subdivisions_x(mut self, subdivisions: u32) -> Self { - self.subdivisions_x = subdivisions; - self - } - - #[inline] - /// The number of subdivisions along the Z axis. - /// - /// 0 - is the original plane geometry, the 4 points in the XZ plane. - /// - /// 1 - adds a vertex in the middle of the Z axis, resulting in a plane with 2 quads / 4 triangles, and a new edge along the X axis. - /// - /// 2 - adds 2 vertices along the Z axis, resulting in a plane with 3 quads / 6 triangles. - /// - /// and so on... - pub fn subdivisions_z(mut self, subdivisions: u32) -> Self { - self.subdivisions_z = subdivisions; - self - } -} - -impl MeshBuilder for PlaneMeshBuilder { - fn build(&self) -> Mesh { - let z_vertex_count = self.subdivisions_z + 2; - let x_vertex_count = self.subdivisions_x + 2; - let num_vertices = (z_vertex_count * x_vertex_count) as usize; - let num_indices = ((z_vertex_count - 1) * (x_vertex_count - 1) * 6) as usize; - - let mut positions: Vec = Vec::with_capacity(num_vertices); - let mut normals: Vec<[f32; 3]> = Vec::with_capacity(num_vertices); - let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(num_vertices); - let mut indices: Vec = Vec::with_capacity(num_indices); - - let rotation = Quat::from_rotation_arc(Vec3::Y, *self.plane.normal); - let size = self.plane.half_size * 2.0; - - for z in 0..z_vertex_count { - for x in 0..x_vertex_count { - let tx = x as f32 / (x_vertex_count - 1) as f32; - let tz = z as f32 / (z_vertex_count - 1) as f32; - let pos = rotation * Vec3::new((-0.5 + tx) * size.x, 0.0, (-0.5 + tz) * size.y); - positions.push(pos); - normals.push(self.plane.normal.to_array()); - uvs.push([tx, tz]); - } - } - - for z in 0..z_vertex_count - 1 { - for x in 0..x_vertex_count - 1 { - let quad = z * x_vertex_count + x; - indices.push(quad + x_vertex_count + 1); - indices.push(quad + 1); - indices.push(quad + x_vertex_count); - indices.push(quad); - indices.push(quad + x_vertex_count); - indices.push(quad + 1); - } - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Plane3d { - type Output = PlaneMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - PlaneMeshBuilder { - plane: *self, - subdivisions_x: 0, - subdivisions_z: 0, - } - } -} - -impl From for Mesh { - fn from(plane: Plane3d) -> Self { - plane.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs deleted file mode 100644 index 744763c8a486e..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::Polyline3d; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Polyline3d`] shape. -#[derive(Clone, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Polyline3dMeshBuilder { - polyline: Polyline3d, -} - -impl MeshBuilder for Polyline3dMeshBuilder { - fn build(&self) -> Mesh { - let positions: Vec<_> = self.polyline.vertices.clone(); - - let indices = Indices::U32( - (0..self.polyline.vertices.len() as u32 - 1) - .flat_map(|i| [i, i + 1]) - .collect(), - ); - - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - } -} - -impl Meshable for Polyline3d { - type Output = Polyline3dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Polyline3dMeshBuilder { - polyline: self.clone(), - } - } -} - -impl From for Mesh { - fn from(polyline: Polyline3d) -> Self { - polyline.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs deleted file mode 100644 index 7b78aa42335ec..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::Segment3d; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Segment3d`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Segment3dMeshBuilder { - segment: Segment3d, -} - -impl MeshBuilder for Segment3dMeshBuilder { - fn build(&self) -> Mesh { - let positions: Vec<_> = self.segment.vertices.into(); - let indices = Indices::U32(vec![0, 1]); - - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - } -} - -impl Meshable for Segment3d { - type Output = Segment3dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Segment3dMeshBuilder { segment: *self } - } -} - -impl From for Mesh { - fn from(segment: Segment3d) -> Self { - segment.mesh_builder().build() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::Meshable; - use bevy_math::Vec3; - - #[test] - fn segment3d_mesh_builder() { - let segment = Segment3d::new(Vec3::ZERO, Vec3::X); - let mesh = segment.mesh_builder().build(); - assert_eq!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().len(), 2); - assert_eq!(mesh.indices().unwrap().len(), 2); - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/sphere.rs b/crates/bevy_mesh/src/primitives/dim3/sphere.rs deleted file mode 100644 index 06b6971d96fd1..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/sphere.rs +++ /dev/null @@ -1,268 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Sphere}; -use bevy_reflect::prelude::*; -use core::f32::consts::PI; -use hexasphere::shapes::IcoSphere; -use thiserror::Error; - -/// An error when creating an icosphere [`Mesh`] from a [`SphereMeshBuilder`]. -#[derive(Clone, Copy, Debug, Error)] -pub enum IcosphereError { - /// The icosphere has too many vertices. - #[error("Cannot create an icosphere of {subdivisions} subdivisions due to there being too many vertices being generated: {number_of_resulting_points}. (Limited to 65535 vertices or 79 subdivisions)")] - TooManyVertices { - /// The number of subdivisions used. 79 is the largest allowed value for a mesh to be generated. - subdivisions: u32, - /// The number of vertices generated. 65535 is the largest allowed value for a mesh to be generated. - number_of_resulting_points: u32, - }, -} - -/// A type of sphere mesh. -#[derive(Clone, Copy, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub enum SphereKind { - /// An icosphere, a spherical mesh that consists of similar sized triangles. - Ico { - /// The number of subdivisions applied. - /// The number of faces quadruples with each subdivision. - subdivisions: u32, - }, - /// A UV sphere, a spherical mesh that consists of quadrilaterals - /// apart from triangles at the top and bottom. - Uv { - /// The number of longitudinal sectors, aka the horizontal resolution. - #[doc(alias = "horizontal_resolution")] - sectors: u32, - /// The number of latitudinal stacks, aka the vertical resolution. - #[doc(alias = "vertical_resolution")] - stacks: u32, - }, -} - -impl Default for SphereKind { - fn default() -> Self { - Self::Ico { subdivisions: 5 } - } -} - -/// A builder used for creating a [`Mesh`] with an [`Sphere`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct SphereMeshBuilder { - /// The [`Sphere`] shape. - pub sphere: Sphere, - /// The type of sphere mesh that will be built. - pub kind: SphereKind, -} - -impl SphereMeshBuilder { - /// Creates a new [`SphereMeshBuilder`] from a radius and [`SphereKind`]. - #[inline] - pub const fn new(radius: f32, kind: SphereKind) -> Self { - Self { - sphere: Sphere { radius }, - kind, - } - } - - /// Sets the [`SphereKind`] that will be used for building the mesh. - #[inline] - pub const fn kind(mut self, kind: SphereKind) -> Self { - self.kind = kind; - self - } - - /// Creates an icosphere mesh with the given number of subdivisions. - /// - /// The number of faces quadruples with each subdivision. - /// If there are `80` or more subdivisions, the vertex count will be too large, - /// and an [`IcosphereError`] is returned. - /// - /// A good default is `5` subdivisions. - pub fn ico(&self, subdivisions: u32) -> Result { - if subdivisions >= 80 { - /* - Number of triangles: - N = 20 - - Number of edges: - E = 30 - - Number of vertices: - V = 12 - - Number of points within a triangle (triangular numbers): - inner(s) = (s^2 + s) / 2 - - Number of points on an edge: - edges(s) = s - - Add up all vertices on the surface: - vertices(s) = edges(s) * E + inner(s - 1) * N + V - - Expand and simplify. Notice that the triangular number formula has roots at -1, and 0, so translating it one to the right fixes it. - subdivisions(s) = 30s + 20((s^2 - 2s + 1 + s - 1) / 2) + 12 - subdivisions(s) = 30s + 10s^2 - 10s + 12 - subdivisions(s) = 10(s^2 + 2s) + 12 - - Factor an (s + 1) term to simplify in terms of calculation - subdivisions(s) = 10(s + 1)^2 + 12 - 10 - resulting_vertices(s) = 10(s + 1)^2 + 2 - */ - let temp = subdivisions + 1; - let number_of_resulting_points = temp * temp * 10 + 2; - return Err(IcosphereError::TooManyVertices { - subdivisions, - number_of_resulting_points, - }); - } - let generated = IcoSphere::new(subdivisions as usize, |point| { - let inclination = ops::acos(point.y); - let azimuth = ops::atan2(point.z, point.x); - - let norm_inclination = inclination / PI; - let norm_azimuth = 0.5 - (azimuth / core::f32::consts::TAU); - - [norm_azimuth, norm_inclination] - }); - - let raw_points = generated.raw_points(); - - let points = raw_points - .iter() - .map(|&p| (p * self.sphere.radius).into()) - .collect::>(); - - let normals = raw_points - .iter() - .copied() - .map(Into::into) - .collect::>(); - - let uvs = generated.raw_data().to_owned(); - - let mut indices = Vec::with_capacity(generated.indices_per_main_triangle() * 20); - - for i in 0..20 { - generated.get_indices(i, &mut indices); - } - - let indices = Indices::U32(indices); - - Ok(Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)) - } - - /// Creates a UV sphere [`Mesh`] with the given number of - /// longitudinal sectors and latitudinal stacks, aka horizontal and vertical resolution. - /// - /// A good default is `32` sectors and `18` stacks. - #[expect( - clippy::explicit_counter_loop, - reason = "Clippy suggestion was much less clear." - )] - pub fn uv(&self, sectors: u32, stacks: u32) -> Mesh { - // Largely inspired from http://www.songho.ca/opengl/gl_sphere.html - - let sectors_f32 = sectors as f32; - let stacks_f32 = stacks as f32; - let length_inv = 1. / self.sphere.radius; - let sector_step = 2. * PI / sectors_f32; - let stack_step = PI / stacks_f32; - - let n_vertices = (stacks * sectors) as usize; - let mut vertices: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); - let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); - let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices); - let mut indices: Vec = Vec::with_capacity(n_vertices * 2 * 3); - - for i in 0..stacks + 1 { - let stack_angle = PI / 2. - (i as f32) * stack_step; - let xy = self.sphere.radius * ops::cos(stack_angle); - let z = self.sphere.radius * ops::sin(stack_angle); - - for j in 0..sectors + 1 { - let sector_angle = (j as f32) * sector_step; - let x = xy * ops::cos(sector_angle); - let y = xy * ops::sin(sector_angle); - - vertices.push([x, y, z]); - normals.push([x * length_inv, y * length_inv, z * length_inv]); - uvs.push([(j as f32) / sectors_f32, (i as f32) / stacks_f32]); - } - } - - // indices - // k1--k1+1 - // | / | - // | / | - // k2--k2+1 - for i in 0..stacks { - let mut k1 = i * (sectors + 1); - let mut k2 = k1 + sectors + 1; - for _j in 0..sectors { - if i != 0 { - indices.push(k1); - indices.push(k2); - indices.push(k1 + 1); - } - if i != stacks - 1 { - indices.push(k1 + 1); - indices.push(k2); - indices.push(k2 + 1); - } - k1 += 1; - k2 += 1; - } - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl MeshBuilder for SphereMeshBuilder { - /// Builds a [`Mesh`] according to the configuration in `self`. - /// - /// # Panics - /// - /// Panics if the sphere is a [`SphereKind::Ico`] with a subdivision count - /// that is greater than or equal to `80` because there will be too many vertices. - fn build(&self) -> Mesh { - match self.kind { - SphereKind::Ico { subdivisions } => self.ico(subdivisions).unwrap(), - SphereKind::Uv { sectors, stacks } => self.uv(sectors, stacks), - } - } -} - -impl Meshable for Sphere { - type Output = SphereMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - SphereMeshBuilder { - sphere: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(sphere: Sphere) -> Self { - sphere.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs deleted file mode 100644 index 34f1752b7e2dd..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::triangle3d; -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::primitives::{Tetrahedron, Triangle3d}; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Tetrahedron`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct TetrahedronMeshBuilder { - tetrahedron: Tetrahedron, -} - -impl MeshBuilder for TetrahedronMeshBuilder { - fn build(&self) -> Mesh { - let mut faces: Vec<_> = self.tetrahedron.faces().into(); - - // If the tetrahedron has negative orientation, reverse all the triangles so that - // they still face outward. - if self.tetrahedron.signed_volume().is_sign_negative() { - faces.iter_mut().for_each(Triangle3d::reverse); - } - - let mut positions = vec![]; - let mut normals = vec![]; - let mut uvs = vec![]; - - // Each face is meshed as a `Triangle3d`, and we just shove the data into the - // vertex attributes sequentially. - for face in faces { - positions.extend(face.vertices); - - let face_normal = triangle3d::normal_vec(&face); - normals.extend(vec![face_normal; 3]); - - let face_uvs = triangle3d::uv_coords(&face); - uvs.extend(face_uvs); - } - - // There are four faces and none of them share vertices. - let indices = Indices::U32(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Tetrahedron { - type Output = TetrahedronMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - TetrahedronMeshBuilder { tetrahedron: *self } - } -} - -impl From for Mesh { - fn from(tetrahedron: Tetrahedron) -> Self { - tetrahedron.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/torus.rs b/crates/bevy_mesh/src/primitives/dim3/torus.rs deleted file mode 100644 index e4df75b30246f..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/torus.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{ops, primitives::Torus, Vec3}; -use bevy_reflect::prelude::*; -use core::ops::RangeInclusive; - -/// A builder used for creating a [`Mesh`] with a [`Torus`] shape. -#[derive(Clone, Debug, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct TorusMeshBuilder { - /// The [`Torus`] shape. - pub torus: Torus, - /// The number of vertices used for each circular segment - /// in the ring or tube of the torus. - /// - /// The default is `24`. - pub minor_resolution: usize, - /// The number of segments used for the main ring of the torus. - /// - /// A resolution of `4` would make the torus appear rectangular, - /// while a resolution of `32` resembles a circular ring. - /// - /// The default is `32`. - pub major_resolution: usize, - /// Optional angle range in radians, defaults to a full circle (0.0..=2 * PI) - pub angle_range: RangeInclusive, -} - -impl Default for TorusMeshBuilder { - fn default() -> Self { - Self { - torus: Torus::default(), - minor_resolution: 24, - major_resolution: 32, - angle_range: (0.0..=2.0 * core::f32::consts::PI), - } - } -} - -impl TorusMeshBuilder { - /// Creates a new [`TorusMeshBuilder`] from an inner and outer radius. - /// - /// The inner radius is the radius of the hole, and the outer radius - /// is the radius of the entire object. - #[inline] - pub fn new(inner_radius: f32, outer_radius: f32) -> Self { - Self { - torus: Torus::new(inner_radius, outer_radius), - ..Default::default() - } - } - - /// Sets the number of vertices used for each circular segment - /// in the ring or tube of the torus. - #[inline] - pub const fn minor_resolution(mut self, resolution: usize) -> Self { - self.minor_resolution = resolution; - self - } - - /// Sets the number of segments used for the main ring of the torus. - /// - /// A resolution of `4` would make the torus appear rectangular, - /// while a resolution of `32` resembles a circular ring. - #[inline] - pub const fn major_resolution(mut self, resolution: usize) -> Self { - self.major_resolution = resolution; - self - } - - /// Sets a custom angle range in radians instead of a full circle - #[inline] - pub const fn angle_range(mut self, range: RangeInclusive) -> Self { - self.angle_range = range; - self - } -} - -impl MeshBuilder for TorusMeshBuilder { - fn build(&self) -> Mesh { - // code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html - - let n_vertices = (self.major_resolution + 1) * (self.minor_resolution + 1); - let mut positions: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); - let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n_vertices); - let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n_vertices); - - let start_angle = self.angle_range.start(); - let end_angle = self.angle_range.end(); - - let segment_stride = (end_angle - start_angle) / self.major_resolution as f32; - let side_stride = 2.0 * core::f32::consts::PI / self.minor_resolution as f32; - - for segment in 0..=self.major_resolution { - let theta = start_angle + segment_stride * segment as f32; - - for side in 0..=self.minor_resolution { - let phi = side_stride * side as f32; - let (sin_theta, cos_theta) = ops::sin_cos(theta); - let (sin_phi, cos_phi) = ops::sin_cos(phi); - let radius = self.torus.major_radius + self.torus.minor_radius * cos_phi; - - let position = Vec3::new( - cos_theta * radius, - self.torus.minor_radius * sin_phi, - sin_theta * radius, - ); - - let center = Vec3::new( - self.torus.major_radius * cos_theta, - 0., - self.torus.major_radius * sin_theta, - ); - let normal = (position - center).normalize(); - - positions.push(position.into()); - normals.push(normal.into()); - uvs.push([ - segment as f32 / self.major_resolution as f32, - side as f32 / self.minor_resolution as f32, - ]); - } - } - - let n_faces = (self.major_resolution) * (self.minor_resolution); - let n_triangles = n_faces * 2; - let n_indices = n_triangles * 3; - - let mut indices: Vec = Vec::with_capacity(n_indices); - - let n_vertices_per_row = self.minor_resolution + 1; - for segment in 0..self.major_resolution { - for side in 0..self.minor_resolution { - let lt = side + segment * n_vertices_per_row; - let rt = (side + 1) + segment * n_vertices_per_row; - - let lb = side + (segment + 1) * n_vertices_per_row; - let rb = (side + 1) + (segment + 1) * n_vertices_per_row; - - indices.push(lt as u32); - indices.push(rt as u32); - indices.push(lb as u32); - - indices.push(rt as u32); - indices.push(rb as u32); - indices.push(lb as u32); - } - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Torus { - type Output = TorusMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - TorusMeshBuilder { - torus: *self, - ..Default::default() - } - } -} - -impl From for Mesh { - fn from(torus: Torus) -> Self { - torus.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs deleted file mode 100644 index aef78fa36bf57..0000000000000 --- a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; -use bevy_asset::RenderAssetUsages; -use bevy_math::{primitives::Triangle3d, Vec3}; -use bevy_reflect::prelude::*; - -/// A builder used for creating a [`Mesh`] with a [`Triangle3d`] shape. -#[derive(Clone, Copy, Debug, Default, Reflect)] -#[reflect(Default, Debug, Clone)] -pub struct Triangle3dMeshBuilder { - triangle: Triangle3d, -} - -impl MeshBuilder for Triangle3dMeshBuilder { - fn build(&self) -> Mesh { - let positions: Vec<_> = self.triangle.vertices.into(); - let uvs: Vec<_> = uv_coords(&self.triangle).into(); - - // Every vertex has the normal of the face of the triangle (or zero if the triangle is degenerate). - let normal: Vec3 = normal_vec(&self.triangle); - let normals = vec![normal; 3]; - - let indices = Indices::U32(vec![0, 1, 2]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Meshable for Triangle3d { - type Output = Triangle3dMeshBuilder; - - fn mesh_builder(&self) -> Self::Output { - Triangle3dMeshBuilder { triangle: *self } - } -} - -/// The normal of a [`Triangle3d`] with zeroing so that a [`Vec3`] is always obtained for meshing. -#[inline] -pub(crate) fn normal_vec(triangle: &Triangle3d) -> Vec3 { - triangle.normal().map_or(Vec3::ZERO, Into::into) -} - -/// Unskewed uv-coordinates for a [`Triangle3d`]. -#[inline] -pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] { - let [a, b, c] = triangle.vertices; - - let main_length = a.distance(b); - let Some(x) = (b - a).try_normalize() else { - return [[0., 0.], [1., 0.], [0., 1.]]; - }; - let y = c - a; - - // `x` corresponds to one of the axes in uv-coordinates; - // to uv-map the triangle without skewing, we use the orthogonalization - // of `y` with respect to `x` as the second direction and construct a rectangle that - // contains `triangle`. - let y_proj = y.project_onto_normalized(x); - - // `offset` represents the x-coordinate of the point `c`; note that x has been shrunk by a - // factor of `main_length`, so `offset` follows it. - let offset = y_proj.dot(x) / main_length; - - // Obtuse triangle leaning to the left => x direction extends to the left, shifting a from 0. - if offset < 0. { - let total_length = 1. - offset; - let a_uv = [offset.abs() / total_length, 0.]; - let b_uv = [1., 0.]; - let c_uv = [0., 1.]; - - [a_uv, b_uv, c_uv] - } - // Obtuse triangle leaning to the right => x direction extends to the right, shifting b from 1. - else if offset > 1. { - let a_uv = [0., 0.]; - let b_uv = [1. / offset, 0.]; - let c_uv = [1., 1.]; - - [a_uv, b_uv, c_uv] - } - // Acute triangle => no extending necessary; a remains at 0 and b remains at 1. - else { - let a_uv = [0., 0.]; - let b_uv = [1., 0.]; - let c_uv = [offset, 1.]; - - [a_uv, b_uv, c_uv] - } -} - -impl From for Mesh { - fn from(triangle: Triangle3d) -> Self { - triangle.mesh_builder().build() - } -} - -#[cfg(test)] -mod tests { - use super::uv_coords; - use bevy_math::primitives::Triangle3d; - - #[test] - fn uv_test() { - use bevy_math::vec3; - let mut triangle = Triangle3d::new(vec3(0., 0., 0.), vec3(2., 0., 0.), vec3(-1., 1., 0.)); - - let [a_uv, b_uv, c_uv] = uv_coords(&triangle); - assert_eq!(a_uv, [1. / 3., 0.]); - assert_eq!(b_uv, [1., 0.]); - assert_eq!(c_uv, [0., 1.]); - - triangle.vertices[2] = vec3(3., 1., 0.); - let [a_uv, b_uv, c_uv] = uv_coords(&triangle); - assert_eq!(a_uv, [0., 0.]); - assert_eq!(b_uv, [2. / 3., 0.]); - assert_eq!(c_uv, [1., 1.]); - - triangle.vertices[2] = vec3(2., 1., 0.); - let [a_uv, b_uv, c_uv] = uv_coords(&triangle); - assert_eq!(a_uv, [0., 0.]); - assert_eq!(b_uv, [1., 0.]); - assert_eq!(c_uv, [1., 1.]); - } -} diff --git a/crates/bevy_mesh/src/primitives/extrusion.rs b/crates/bevy_mesh/src/primitives/extrusion.rs deleted file mode 100644 index 519849ae2786b..0000000000000 --- a/crates/bevy_mesh/src/primitives/extrusion.rs +++ /dev/null @@ -1,441 +0,0 @@ -use bevy_math::{ - primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}, - Vec2, Vec3, -}; - -use super::{MeshBuilder, Meshable}; -use crate::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; - -/// A type representing a segment of the perimeter of an extrudable mesh. -pub enum PerimeterSegment { - /// This segment of the perimeter will be shaded smooth. - /// - /// This has the effect of rendering the segment's faces with softened edges, so it is appropriate for curved shapes. - /// - /// The normals for the vertices that are part of this segment will be calculated based on the positions of their neighbors. - /// Each normal is interpolated between the normals of the two line segments connecting it with its neighbors. - /// Closer vertices have a stronger effect on the normal than more distant ones. - /// - /// Since the vertices corresponding to the first and last indices do not have two neighboring vertices, their normals must be provided manually. - Smooth { - /// The normal of the first vertex. - first_normal: Vec2, - /// The normal of the last vertex. - last_normal: Vec2, - /// A list of indices representing this segment of the perimeter of the mesh. - /// - /// The `indices` refer to the indices of the vertices generated by the `MeshBuilder` of the underlying 2D primitive. - /// For example, a triangle has 3 vertices with indices 0, 1 and 2. - /// - /// The indices must be ordered such that the *outside* of the mesh is to the right - /// when walking along the vertices of the mesh in the order provided by the indices. - /// - /// For geometry to be rendered, you must provide at least two indices. - indices: Vec, - }, - /// This segment of the perimeter will be shaded flat. - /// - /// This has the effect of rendering the segment's faces with hard edges. - Flat { - /// A list of indices representing this segment of the perimeter of the mesh. - /// - /// The `indices` refer to the indices of the vertices generated by the `MeshBuilder` of the underlying 2D primitive. - /// For example, a triangle has 3 vertices with indices 0, 1 and 2. - /// - /// The indices must be ordered such that the *outside* of the mesh is to the right - /// when walking along the vertices of the mesh in the order provided by indices. - /// - /// For geometry to be rendered, you must provide at least two indices. - indices: Vec, - }, -} - -impl PerimeterSegment { - /// Returns the amount of vertices each 'layer' of the extrusion should include for this perimeter segment. - /// - /// A layer is the set of vertices sharing a common Z value or depth. - fn vertices_per_layer(&self) -> u32 { - match self { - PerimeterSegment::Smooth { indices, .. } => indices.len() as u32, - PerimeterSegment::Flat { indices } => 2 * (indices.len() as u32 - 1), - } - } - - /// Returns the amount of indices each 'segment' of the extrusion should include for this perimeter segment. - /// - /// A segment is the set of faces on the mantel of the extrusion between two layers of vertices. - fn indices_per_segment(&self) -> usize { - match self { - PerimeterSegment::Smooth { indices, .. } | PerimeterSegment::Flat { indices } => { - 6 * (indices.len() - 1) - } - } - } -} - -/// A trait required for implementing `Meshable` for `Extrusion`. -/// -/// ## Warning -/// -/// By implementing this trait you guarantee that the `primitive_topology` of the mesh returned by -/// this builder is [`PrimitiveTopology::TriangleList`] -/// and that your mesh has a [`Mesh::ATTRIBUTE_POSITION`] attribute. -pub trait Extrudable: MeshBuilder { - /// A list of the indices each representing a part of the perimeter of the mesh. - fn perimeter(&self) -> Vec; -} - -impl

Meshable for Extrusion

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - type Output = ExtrusionBuilder

; - - fn mesh_builder(&self) -> Self::Output { - ExtrusionBuilder { - base_builder: self.base_shape.mesh_builder(), - half_depth: self.half_depth, - segments: 1, - } - } -} - -/// A builder used for creating a [`Mesh`] with an [`Extrusion`] shape. -pub struct ExtrusionBuilder

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - pub base_builder: P::Output, - pub half_depth: f32, - pub segments: usize, -} - -impl

ExtrusionBuilder

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - /// Create a new `ExtrusionBuilder

` from a given `base_shape` and the full `depth` of the extrusion. - pub fn new(base_shape: &P, depth: f32) -> Self { - Self { - base_builder: base_shape.mesh_builder(), - half_depth: depth / 2., - segments: 1, - } - } - - /// Sets the number of segments along the depth of the extrusion. - /// Must be greater than `0` for the geometry of the mantel to be generated. - pub fn segments(mut self, segments: usize) -> Self { - self.segments = segments; - self - } - - /// Apply a function to the inner builder - pub fn with_inner(mut self, func: impl Fn(P::Output) -> P::Output) -> Self { - self.base_builder = func(self.base_builder); - self - } -} - -impl ExtrusionBuilder { - /// Sets the number of vertices used for the circle mesh at each end of the extrusion. - pub fn resolution(mut self, resolution: u32) -> Self { - self.base_builder.resolution = resolution; - self - } -} - -impl ExtrusionBuilder { - /// Sets the number of vertices used for the ellipse mesh at each end of the extrusion. - pub fn resolution(mut self, resolution: u32) -> Self { - self.base_builder.resolution = resolution; - self - } -} - -impl ExtrusionBuilder { - /// Sets the number of vertices used in constructing the concentric circles of the annulus mesh at each end of the extrusion. - pub fn resolution(mut self, resolution: u32) -> Self { - self.base_builder.resolution = resolution; - self - } -} - -impl ExtrusionBuilder { - /// Sets the number of vertices used for each hemicircle at the ends of the extrusion. - pub fn resolution(mut self, resolution: u32) -> Self { - self.base_builder.resolution = resolution; - self - } -} - -impl

MeshBuilder for ExtrusionBuilder

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - fn build(&self) -> Mesh { - // Create and move the base mesh to the front - let mut front_face = - self.base_builder - .build() - .translated_by(Vec3::new(0., 0., self.half_depth)); - - // Move the uvs of the front face to be between (0., 0.) and (0.5, 0.5) - if let Some(VertexAttributeValues::Float32x2(uvs)) = - front_face.attribute_mut(Mesh::ATTRIBUTE_UV_0) - { - for uv in uvs { - *uv = uv.map(|coord| coord * 0.5); - } - } - - let back_face = { - let topology = front_face.primitive_topology(); - // Flip the normals, etc. and move mesh to the back - let mut back_face = front_face.clone().scaled_by(Vec3::new(1., 1., -1.)); - - // Move the uvs of the back face to be between (0.5, 0.) and (1., 0.5) - if let Some(VertexAttributeValues::Float32x2(uvs)) = - back_face.attribute_mut(Mesh::ATTRIBUTE_UV_0) - { - for uv in uvs { - *uv = [uv[0] + 0.5, uv[1]]; - } - } - - // By swapping the first and second indices of each triangle we invert the winding order thus making the mesh visible from the other side - if let Some(indices) = back_face.indices_mut() { - match topology { - PrimitiveTopology::TriangleList => match indices { - Indices::U16(indices) => { - for [a, b, _] in indices.as_chunks_mut().0 { - core::mem::swap(a, b); - } - } - Indices::U32(indices) => { - for [a, b, _] in indices.as_chunks_mut().0 { - core::mem::swap(a, b); - } - } - }, - _ => { - panic!("Meshes used with Extrusions must have a primitive topology of `PrimitiveTopology::TriangleList`"); - } - }; - } - back_face - }; - - // An extrusion of depth 0 does not need a mantel - if self.half_depth == 0. { - front_face.merge(&back_face).unwrap(); - return front_face; - } - - let mantel = { - let Some(VertexAttributeValues::Float32x3(cap_verts)) = - front_face.attribute(Mesh::ATTRIBUTE_POSITION) - else { - panic!("The base mesh did not have vertex positions"); - }; - - debug_assert!(self.segments > 0); - - let layers = self.segments + 1; - let layer_depth_delta = self.half_depth * 2.0 / self.segments as f32; - - let perimeter = self.base_builder.perimeter(); - let (vert_count, index_count) = - perimeter - .iter() - .fold((0, 0), |(verts, indices), perimeter| { - ( - verts + layers * perimeter.vertices_per_layer() as usize, - indices + self.segments * perimeter.indices_per_segment(), - ) - }); - let mut positions = Vec::with_capacity(vert_count); - let mut normals = Vec::with_capacity(vert_count); - let mut indices = Vec::with_capacity(index_count); - let mut uvs = Vec::with_capacity(vert_count); - - // Compute the amount of horizontal space allocated to each segment of the perimeter. - let uv_segment_delta = 1. / perimeter.len() as f32; - for (i, segment) in perimeter.into_iter().enumerate() { - // The start of the x range of the area of the current perimeter-segment. - let uv_start = i as f32 * uv_segment_delta; - - match segment { - PerimeterSegment::Flat { - indices: segment_indices, - } => { - let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32; - for i in 0..(segment_indices.len() - 1) { - let uv_x = uv_start + uv_delta * i as f32; - // Get the positions for the current and the next index. - let a = cap_verts[segment_indices[i] as usize]; - let b = cap_verts[segment_indices[i + 1] as usize]; - - // Get the index of the next vertex added to the mantel. - let index = positions.len() as u32; - - // Push the positions of the two indices and their equivalent points on each layer. - for i in 0..layers { - let i = i as f32; - let z = a[2] - layer_depth_delta * i; - positions.push([a[0], a[1], z]); - positions.push([b[0], b[1], z]); - - // UVs for the mantel are between (0, 0.5) and (1, 1). - let uv_y = 0.5 + 0.5 * i / self.segments as f32; - uvs.push([uv_x, uv_y]); - uvs.push([uv_x + uv_delta, uv_y]); - } - - // The normal is calculated to be the normal of the line segment connecting a and b. - let n = Vec3::from_array([b[1] - a[1], a[0] - b[0], 0.]) - .normalize_or_zero() - .to_array(); - normals.extend_from_slice(&vec![n; 2 * layers]); - - // Add the indices for the vertices created above to the mesh. - for i in 0..self.segments as u32 { - let base_index = index + 2 * i; - indices.extend_from_slice(&[ - base_index, - base_index + 2, - base_index + 1, - base_index + 1, - base_index + 2, - base_index + 3, - ]); - } - } - } - PerimeterSegment::Smooth { - first_normal, - last_normal, - indices: segment_indices, - } => { - let uv_delta = uv_segment_delta / (segment_indices.len() - 1) as f32; - - // Since the indices for this segment will be added after its vertices have been added, - // we need to store the index of the first vertex that is part of this segment. - let base_index = positions.len() as u32; - - // If there is a first vertex, we need to add it and its counterparts on each layer. - // The normal is provided by `segment.first_normal`. - if let Some(i) = segment_indices.first() { - let p = cap_verts[*i as usize]; - for i in 0..layers { - let i = i as f32; - let z = p[2] - layer_depth_delta * i; - positions.push([p[0], p[1], z]); - - let uv_y = 0.5 + 0.5 * i / self.segments as f32; - uvs.push([uv_start, uv_y]); - } - normals.extend_from_slice(&vec![ - first_normal.extend(0.).to_array(); - layers - ]); - } - - // For all points inbetween the first and last vertices, we can automatically compute the normals. - for i in 1..(segment_indices.len() - 1) { - let uv_x = uv_start + uv_delta * i as f32; - - // Get the positions for the last, current and the next index. - let a = cap_verts[segment_indices[i - 1] as usize]; - let b = cap_verts[segment_indices[i] as usize]; - let c = cap_verts[segment_indices[i + 1] as usize]; - - // Add the current vertex and its counterparts on each layer. - for i in 0..layers { - let i = i as f32; - let z = b[2] - layer_depth_delta * i; - positions.push([b[0], b[1], z]); - - let uv_y = 0.5 + 0.5 * i / self.segments as f32; - uvs.push([uv_x, uv_y]); - } - - // The normal for the current vertices can be calculated based on the two neighboring vertices. - // The normal is interpolated between the normals of the two line segments connecting the current vertex with its neighbors. - // Closer vertices have a stronger effect on the normal than more distant ones. - let n = { - let ab = Vec2::from_slice(&b) - Vec2::from_slice(&a); - let bc = Vec2::from_slice(&c) - Vec2::from_slice(&b); - let n = ab.normalize_or_zero() + bc.normalize_or_zero(); - Vec2::new(n.y, -n.x) - .normalize_or_zero() - .extend(0.) - .to_array() - }; - normals.extend_from_slice(&vec![n; layers]); - } - - // If there is a last vertex, we need to add it and its counterparts on each layer. - // The normal is provided by `segment.last_normal`. - if let Some(i) = segment_indices.last() { - let p = cap_verts[*i as usize]; - for i in 0..layers { - let i = i as f32; - let z = p[2] - layer_depth_delta * i; - positions.push([p[0], p[1], z]); - - let uv_y = 0.5 + 0.5 * i / self.segments as f32; - uvs.push([uv_start + uv_segment_delta, uv_y]); - } - normals.extend_from_slice(&vec![ - last_normal.extend(0.).to_array(); - layers - ]); - } - - let columns = segment_indices.len() as u32; - let segments = self.segments as u32; - let layers = segments + 1; - for s in 0..segments { - for column in 0..(columns - 1) { - let index = base_index + s + column * layers; - indices.extend_from_slice(&[ - index, - index + 1, - index + layers, - index + layers, - index + 1, - index + layers + 1, - ]); - } - } - } - } - } - - Mesh::new(PrimitiveTopology::TriangleList, front_face.asset_usage) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - }; - - front_face.merge(&back_face).unwrap(); - front_face.merge(&mantel).unwrap(); - front_face - } -} - -impl

From> for Mesh -where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - fn from(value: Extrusion

) -> Self { - value.mesh_builder().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/mod.rs b/crates/bevy_mesh/src/primitives/mod.rs index d149070f7d04d..be2096304a338 100644 --- a/crates/bevy_mesh/src/primitives/mod.rs +++ b/crates/bevy_mesh/src/primitives/mod.rs @@ -1,13 +1,14 @@ -//! Mesh generation for [primitive shapes](bevy_math::primitives). +//! Mesh generation traits. //! -//! Primitives that support meshing implement the [`Meshable`] trait. -//! Calling [`mesh`](Meshable::mesh) will return either a [`Mesh`] or a builder -//! that can be used to specify shape-specific configuration for creating the [`Mesh`]. +//! Anything that support meshing implement the [`Meshable`] trait. +//! Calling [`mesh`](Meshable::mesh) will return a [`Mesh`] while calling +//! [`mesh_builder`](Meshable::mesh_builder) returns a builder that can be used to specify +//! shape-specific configuration for creating the [`Mesh`]. //! //! ``` //! # use bevy_asset::Assets; //! # use bevy_ecs::prelude::ResMut; -//! # use bevy_math::prelude::Circle; +//! # use bevy_shapes::prelude::Circle; //! # use bevy_mesh::*; //! # //! # fn setup(mut meshes: ResMut>) { @@ -15,19 +16,10 @@ //! let circle = meshes.add(Circle { radius: 25.0 }); //! //! // Specify number of vertices -//! let circle = meshes.add(Circle { radius: 25.0 }.mesh().resolution(64)); +//! let circle = meshes.add(Circle { radius: 25.0 }.mesh_builder().resolution(64)); //! # } //! ``` -mod dim2; -pub use dim2::*; - -mod dim3; -pub use dim3::*; - -mod extrusion; -pub use extrusion::*; - use super::Mesh; /// A trait for shapes that can be turned into a [`Mesh`]. diff --git a/crates/bevy_shapes/src/bounding/dim3.rs b/crates/bevy_shapes/src/bounding/dim3.rs index 2dae4d9098b8c..72171c5f86838 100644 --- a/crates/bevy_shapes/src/bounding/dim3.rs +++ b/crates/bevy_shapes/src/bounding/dim3.rs @@ -640,7 +640,7 @@ mod tests { Vec3::ZERO.into(), "incorrect bounding sphere center" ); - assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius"); + assert_eq!(bs.radius(), 0.0, "incorrect bounding sphere radius"); let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); let bs = dup_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); @@ -649,7 +649,7 @@ mod tests { Vec3::new(0.5, 0.0, 0.0).into(), "incorrect bounding sphere center" ); - assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius"); + assert_eq!(bs.radius(), 0.5, "incorrect bounding sphere radius"); let br = dup_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); assert_eq!( br.center(), @@ -669,7 +669,7 @@ mod tests { Vec3::ZERO.into(), "incorrect bounding sphere center" ); - assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius"); + assert_eq!(bs.radius(), 1.0, "incorrect bounding sphere radius"); let br = collinear_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); assert_eq!( br.center(), diff --git a/crates/bevy_shapes/src/bounding/extrusion.rs b/crates/bevy_shapes/src/bounding/extrusion.rs index 3781071d267c4..7063b557f669d 100644 --- a/crates/bevy_shapes/src/bounding/extrusion.rs +++ b/crates/bevy_shapes/src/bounding/extrusion.rs @@ -1,4 +1,4 @@ -//! Module implementing [`BoundedExtrusion`] for all the primitives in bevy_shapes +//! Implement [`BoundedExtrusion`] for all the primitives in bevy_shapes use core::f32::consts::FRAC_PI_2; diff --git a/crates/bevy_shapes/src/meshing/mod.rs b/crates/bevy_shapes/src/meshing/mod.rs index a7a1fbbf0623b..5c96550a6df41 100644 --- a/crates/bevy_shapes/src/meshing/mod.rs +++ b/crates/bevy_shapes/src/meshing/mod.rs @@ -1,23 +1,4 @@ -//! Mesh generation for [primitive shapes](bevy_math::primitives). -//! -//! Primitives that support meshing implement the [`Meshable`] trait. -//! Calling [`mesh`](Meshable::mesh) will return either a [`Mesh`] or a builder -//! that can be used to specify shape-specific configuration for creating the [`Mesh`]. -//! -//! ``` -//! # use bevy_asset::Assets; -//! # use bevy_ecs::prelude::ResMut; -//! # use bevy_math::prelude::Circle; -//! # use bevy_mesh::*; -//! # -//! # fn setup(mut meshes: ResMut>) { -//! // Create circle mesh with default configuration -//! let circle = meshes.add(Circle { radius: 25.0 }); -//! -//! // Specify number of vertices -//! let circle = meshes.add(Circle { radius: 25.0 }.mesh().resolution(64)); -//! # } -//! ``` +//! [`Meshable`](bevy_mesh::Meshable) implementation for [primitive shapes](bevy_shapes::primitives). mod dim2; pub use dim2::*; diff --git a/crates/bevy_shapes/src/primitives/dim2.rs b/crates/bevy_shapes/src/primitives/dim2.rs index 58b6e5c8eefb3..07240b1c5c1a9 100644 --- a/crates/bevy_shapes/src/primitives/dim2.rs +++ b/crates/bevy_shapes/src/primitives/dim2.rs @@ -4,9 +4,9 @@ use derive_more::derive::From; use thiserror::Error; use super::{Measured2d, Primitive2d, WindingOrder}; +use crate::primitives::Inset; use bevy_math::{ ops::{self, FloatPow}, - primitives::Inset, Dir2, InvalidDirectionError, Isometry2d, Ray2d, Rot2, Vec2, }; diff --git a/crates/bevy_shapes/src/primitives/half_space.rs b/crates/bevy_shapes/src/primitives/half_space.rs deleted file mode 100644 index 52cc7fe38d292..0000000000000 --- a/crates/bevy_shapes/src/primitives/half_space.rs +++ /dev/null @@ -1,153 +0,0 @@ -use bevy_math::{ops, Vec3, Vec3A, Vec4, Vec4Swizzles}; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; - -/// A region of 3D space, specifically an open set whose border is a bisecting 2D plane. -/// -/// This bisecting plane partitions 3D space into two infinite regions, -/// the half-space is one of those regions and excludes the bisecting plane. -/// -/// Each instance of this type is characterized by: -/// - the bisecting plane's unit normal, normalized and pointing "inside" the half-space, -/// - the signed distance along the normal from the bisecting plane to the origin of 3D space. -/// -/// The distance can also be seen as: -/// - the distance along the inverse of the normal from the origin of 3D space to the bisecting plane, -/// - the opposite of the distance along the normal from the origin of 3D space to the bisecting plane. -/// -/// Any point `p` is considered to be within the `HalfSpace` when the length of the projection -/// of p on the normal is greater or equal than the opposite of the distance, -/// meaning: if the equation `normal.dot(p) + distance > 0.` is satisfied. -/// -/// For example, the half-space containing all the points with a z-coordinate lesser -/// or equal than `8.0` would be defined by: `HalfSpace::new(Vec3::NEG_Z.extend(-8.0))`. -/// It includes all the points from the bisecting plane towards `NEG_Z`, and the distance -/// from the plane to the origin is `-8.0` along `NEG_Z`. -/// -/// It is used to define a [`ViewFrustum`](crate::primitives::ViewFrustum), -/// but is also a useful mathematical primitive for rendering tasks such as light computation. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "bevy_reflect", - derive(Reflect), - reflect(Clone, Debug, Default, PartialEq) -)] -#[cfg_attr( - all(feature = "serialize", feature = "bevy_reflect"), - reflect(Serialize, Deserialize) -)] -pub struct HalfSpace { - normal_d: Vec4, -} - -impl HalfSpace { - /// Constructs a `HalfSpace` from a 4D vector whose first 3 components - /// represent the bisecting plane's unit normal, and the last component is - /// the signed distance along the normal from the plane to the origin. - /// The constructor ensures the normal vector is normalized and the distance is appropriately scaled. - #[inline] - pub fn new(normal_d: Vec4) -> Self { - Self { - normal_d: normal_d * normal_d.xyz().length_recip(), - } - } - - /// Returns the unit normal vector of the bisecting plane that characterizes the `HalfSpace`. - #[inline] - pub fn normal(&self) -> Vec3A { - Vec3A::from_vec4(self.normal_d) - } - - /// Returns the signed distance from the bisecting plane to the origin along - /// the plane's unit normal vector. - #[inline] - pub fn d(&self) -> f32 { - self.normal_d.w - } - - /// Returns the bisecting plane's unit normal vector and the signed distance - /// from the plane to the origin. - #[inline] - pub const fn normal_d(&self) -> Vec4 { - self.normal_d - } - - /// Returns the intersection point if the three halfspaces all intersect at a single point. - #[inline] - pub fn intersection_point(a: HalfSpace, b: HalfSpace, c: HalfSpace) -> Option { - let an = a.normal(); - let bn = b.normal(); - let cn = c.normal(); - - let x = Vec3A::new(an.x, bn.x, cn.x); - let y = Vec3A::new(an.y, bn.y, cn.y); - let z = Vec3A::new(an.z, bn.z, cn.z); - - let d = -Vec3A::new(a.d(), b.d(), c.d()); - - let u = y.cross(z); - let v = x.cross(d); - - let denom = x.dot(u); - - if ops::abs(denom) < f32::EPSILON { - return None; - } - - Some(Vec3::new(d.dot(u), z.dot(v), -y.dot(v)) / denom) - } -} - -#[cfg(test)] -mod half_space_tests { - use core::f32; - - use approx::assert_relative_eq; - - use super::HalfSpace; - use bevy_math::{Vec3, Vec4}; - - #[test] - fn intersection_point() { - // Intersection of shifted xy, xz, and yz planes - let xy_at_z_3 = HalfSpace { - normal_d: Vec4::new(0., 0., -1., 3.), - }; - let xz_at_y_2 = HalfSpace { - normal_d: Vec4::new(0., 1., 0., -2.), - }; - let yz_at_x_1 = HalfSpace { - normal_d: Vec4::new(1., 0., 0., -1.), - }; - assert_relative_eq!( - HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, yz_at_x_1).unwrap(), - Vec3::new(1., 2., 3.), - epsilon = 2e-7 - ); - - // Three planes that do not simultaneously intersect - let xz_at_y_3 = HalfSpace { - normal_d: Vec4::new(0., 1., 0., -3.), - }; - assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, xz_at_y_3).is_none()); - - // Three planes that intersect at a line - let other_xz_at_y_2 = HalfSpace { - normal_d: Vec4::new(0., -1., 0., 3.), - }; - assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, other_xz_at_y_2).is_none()); - - // Three identical planes - assert!(HalfSpace::intersection_point(xz_at_y_2, xz_at_y_2, other_xz_at_y_2).is_none()); - - // ill-defined halfspace - let ill_defined = HalfSpace { - normal_d: Vec4::new(0., 0., 0., f32::INFINITY), - }; - assert!(HalfSpace::intersection_point(xy_at_z_3, xz_at_y_2, ill_defined).is_none()); - } -} diff --git a/crates/bevy_shapes/src/primitives/inset.rs b/crates/bevy_shapes/src/primitives/inset.rs index 0cc89e58453a7..abbe2c8cbcfdf 100644 --- a/crates/bevy_shapes/src/primitives/inset.rs +++ b/crates/bevy_shapes/src/primitives/inset.rs @@ -1,11 +1,7 @@ -use bevy_math::{ - ops, - primitives::{ - Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, - Triangle2d, - }, - Vec2, +use crate::primitives::{ + Capsule2d, Circle, CircularSegment, Primitive2d, Rectangle, RegularPolygon, Rhombus, Triangle2d, }; +use bevy_math::{ops, Vec2}; /// A primitive that can be resized uniformly. /// diff --git a/crates/bevy_shapes/src/primitives/mod.rs b/crates/bevy_shapes/src/primitives/mod.rs index e9ead38a3d143..0622f34898a86 100644 --- a/crates/bevy_shapes/src/primitives/mod.rs +++ b/crates/bevy_shapes/src/primitives/mod.rs @@ -8,12 +8,8 @@ mod dim3; pub use dim3::*; mod inset; pub use inset::*; -mod half_space; -mod polygon; -pub use half_space::*; -mod view_frustum; -pub use view_frustum::*; mod math; +mod polygon; /// A marker trait for 2D primitives pub trait Primitive2d {} From 9ec65b85761e3e9a4169ccc209aee23c653df305 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 22:13:55 +0200 Subject: [PATCH 14/17] implement common Mesh constructors used in-engine --- crates/bevy_mesh/src/mesh.rs | 154 +++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 8b0de9efa29f7..f107e642ab5a3 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -2674,6 +2674,160 @@ impl Mesh { } } +// implement common mesh constructors +impl Mesh { + /// Creates a rectangular quad mesh centered at the origin. + /// + /// The resulting mesh lies in the *XY* plane with normals pointing along the + /// positive *Z* axis. `half_size` specifies half of the quad's width and height, + /// so the full dimensions are `half_size * 2.0`. + /// + /// The mesh consists of four vertices and two triangles, and includes position, + /// normal, and UV attributes. + pub fn quad_mesh(half_size: Vec2) -> Self { + let [hw, hh] = half_size.to_array(); + let positions = alloc::vec![ + [hw, hh, 0.0], + [-hw, hh, 0.0], + [-hw, -hh, 0.0], + [hw, -hh, 0.0], + ]; + let normals = alloc::vec![[0.0, 0.0, 1.0]; 4]; + let uvs = alloc::vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; + let indices = Indices::U32(alloc::vec![0, 1, 2, 0, 2, 3]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } + + /// Creates a subdivided plane mesh. + /// + /// The plane is centered at the origin and has a total size of + /// `half_size * 2.0`. It is initially generated in the *XZ* plane and then + /// rotated so that its normal matches `normal`. + /// + /// `subdivisions_x` and `subdivisions_z` control the number of subdivisions + /// along each axis. A value of `0` produces a single quad. + pub fn plane_mesh( + normal: Dir3, + half_size: Vec2, + subdivisions_x: u32, + subdivisions_z: u32, + ) -> Self { + let z_vertex_count = subdivisions_z + 2; + let x_vertex_count = subdivisions_x + 2; + let num_vertices = (z_vertex_count * x_vertex_count) as usize; + let num_indices = ((z_vertex_count - 1) * (x_vertex_count - 1) * 6) as usize; + + let mut positions: Vec = Vec::with_capacity(num_vertices); + let mut normals: Vec<[f32; 3]> = Vec::with_capacity(num_vertices); + let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(num_vertices); + let mut indices: Vec = Vec::with_capacity(num_indices); + + let rotation = Quat::from_rotation_arc(Vec3::Y, *normal); + let size = half_size * 2.0; + + for z in 0..z_vertex_count { + for x in 0..x_vertex_count { + let tx = x as f32 / (x_vertex_count - 1) as f32; + let tz = z as f32 / (z_vertex_count - 1) as f32; + let pos = rotation * Vec3::new((-0.5 + tx) * size.x, 0.0, (-0.5 + tz) * size.y); + positions.push(pos); + normals.push(normal.to_array()); + uvs.push([tx, tz]); + } + } + + for z in 0..z_vertex_count - 1 { + for x in 0..x_vertex_count - 1 { + let quad = z * x_vertex_count + x; + indices.push(quad + x_vertex_count + 1); + indices.push(quad + 1); + indices.push(quad + x_vertex_count); + indices.push(quad); + indices.push(quad + x_vertex_count); + indices.push(quad + 1); + } + } + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + } + + pub fn cuboid_mesh(half_size: Vec3) -> Self { + let min = -half_size; + let max = half_size; + + // Suppose Y-up right hand, and camera look from +Z to -Z + let vertices = &[ + // Front + ([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]), + ([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]), + ([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]), + ([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]), + // Back + ([min.x, max.y, min.z], [0.0, 0.0, -1.0], [1.0, 0.0]), + ([max.x, max.y, min.z], [0.0, 0.0, -1.0], [0.0, 0.0]), + ([max.x, min.y, min.z], [0.0, 0.0, -1.0], [0.0, 1.0]), + ([min.x, min.y, min.z], [0.0, 0.0, -1.0], [1.0, 1.0]), + // Right + ([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]), + ([max.x, max.y, min.z], [1.0, 0.0, 0.0], [1.0, 0.0]), + ([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]), + // Left + ([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]), + ([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]), + ([min.x, max.y, min.z], [-1.0, 0.0, 0.0], [0.0, 1.0]), + ([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]), + // Top + ([max.x, max.y, min.z], [0.0, 1.0, 0.0], [1.0, 0.0]), + ([min.x, max.y, min.z], [0.0, 1.0, 0.0], [0.0, 0.0]), + ([min.x, max.y, max.z], [0.0, 1.0, 0.0], [0.0, 1.0]), + ([max.x, max.y, max.z], [0.0, 1.0, 0.0], [1.0, 1.0]), + // Bottom + ([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]), + ([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]), + ([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]), + ]; + + let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect(); + let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); + let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); + + let indices = Indices::U32(alloc::vec![ + 0, 1, 2, 2, 3, 0, // front + 4, 5, 6, 6, 7, 4, // back + 8, 9, 10, 10, 11, 8, // right + 12, 13, 14, 14, 15, 12, // left + 16, 17, 18, 18, 19, 16, // top + 20, 21, 22, 22, 23, 20, // bottom + ]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(indices) + } +} + #[cfg(feature = "morph")] impl Mesh { /// Whether this mesh has morph targets. From 00a584760fda895908151ed5e7fc9848f288d6a0 Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 22:13:55 +0200 Subject: [PATCH 15/17] fix all in-engine uses --- benches/benches/bevy_camera/primitives.rs | 2 +- crates/bevy_gizmos_render/Cargo.toml | 1 + .../src/transform_gizmo_render.rs | 30 +-- crates/bevy_light/Cargo.toml | 5 +- crates/bevy_light/src/gizmos.rs | 9 +- crates/bevy_pbr/src/decal/forward.rs | 14 +- crates/bevy_pbr/src/render/light.rs | 5 +- crates/bevy_pbr/src/volumetric_fog/mod.rs | 11 +- crates/bevy_shapes/src/meshing/dim2.rs | 244 +----------------- crates/bevy_shapes/src/meshing/dim3/cuboid.rs | 59 +---- crates/bevy_shapes/src/meshing/dim3/plane.rs | 48 +--- crates/bevy_shapes/src/meshing/extrusion.rs | 213 +++++++++++++++ .../bevy_sprite_render/src/sprite_mesh/mod.rs | 8 +- .../src/tilemap_chunk/mod.rs | 4 +- 14 files changed, 259 insertions(+), 394 deletions(-) diff --git a/benches/benches/bevy_camera/primitives.rs b/benches/benches/bevy_camera/primitives.rs index 153fe9e7ca78e..90ee8f5d96b87 100644 --- a/benches/benches/bevy_camera/primitives.rs +++ b/benches/benches/bevy_camera/primitives.rs @@ -1,6 +1,6 @@ use bevy_camera::primitives::{Aabb, Frustum, Sphere}; use bevy_math::{ - primitives::{HalfSpace, ViewFrustum}, + camera::{HalfSpace, ViewFrustum}, Affine3A, Quat, Vec3, Vec3A, Vec4, }; use core::hint::black_box; diff --git a/crates/bevy_gizmos_render/Cargo.toml b/crates/bevy_gizmos_render/Cargo.toml index 12c0e103446b6..7b233408cdf52 100644 --- a/crates/bevy_gizmos_render/Cargo.toml +++ b/crates/bevy_gizmos_render/Cargo.toml @@ -30,6 +30,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.20.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.20.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.20.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.20.0-dev" } +bevy_shapes = { path = "../bevy_shapes", version = "0.20.0-dev", features = [ "meshing" ] } bevy_color = { path = "../bevy_color", version = "0.20.0-dev" } bevy_material = { path = "../bevy_material", version = "0.20.0-dev" } bevy_log = { path = "../bevy_log", 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 0e3974800998f..6f19b699e0894 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_shapes::primitives::{Cone, Cuboid, Cylinder, Torus}; use bevy_transform::{ components::{GlobalTransform, Transform}, systems::propagate_transforms_for, @@ -167,40 +165,30 @@ fn spawn_gizmo_meshes( }; // Pre-create meshes - let shaft_mesh = meshes.add( - Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH) - .mesh_builder() - .build(), - ); - let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh_builder().build()); - let scale_cube_mesh = meshes.add( - Cuboid::new(SCALE_CUBE_SIZE, SCALE_CUBE_SIZE, SCALE_CUBE_SIZE) - .mesh_builder() - .build(), - ); + let shaft_mesh = meshes.add(Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH).mesh()); + let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh()); + let scale_cube_mesh = + meshes.add(Cuboid::new(SCALE_CUBE_SIZE, SCALE_CUBE_SIZE, SCALE_CUBE_SIZE).mesh()); let rotate_torus_mesh = meshes.add( Torus { minor_radius: 0.015, major_radius: ROTATE_RING_RADIUS, } - .mesh_builder() - .build(), + .mesh(), ); let view_circle_mesh = meshes.add( Torus { minor_radius: VIEW_CIRCLE_MINOR, major_radius: VIEW_CIRCLE_MAJOR, } - .mesh_builder() - .build(), + .mesh(), ); let view_ring_mesh = meshes.add( Torus { minor_radius: VIEW_RING_MINOR, major_radius: VIEW_RING_MAJOR, } - .mesh_builder() - .build(), + .mesh(), ); // Axis rotations: cylinder default is Y-up diff --git a/crates/bevy_light/Cargo.toml b/crates/bevy_light/Cargo.toml index f9e85c8b99dfd..5c92d85640503 100644 --- a/crates/bevy_light/Cargo.toml +++ b/crates/bevy_light/Cargo.toml @@ -26,10 +26,12 @@ bevy_color = { path = "../bevy_color", version = "0.20.0-dev", features = [ ] } bevy_gizmos = { path = "../bevy_gizmos", version = "0.20.0-dev", optional = true } bevy_log = { path = "../bevy_log", version = "0.20.0-dev" } +bevy_shapes = { path = "../bevy_shapes", version = "0.20.0-dev", optional = true } +bevy_gizmos = { path = "../bevy_gizmos", version = "0.20.0-dev", optional = true } # other tracing = { version = "0.1", default-features = false } -wgpu-types = { version = "29.0.3", default-features = false } +wgpu-types = { version = "30", default-features = false } half = "2.7.1" smallvec = { version = "1", default-features = false } @@ -38,6 +40,7 @@ default = [] experimental_pbr_pcss = [] webgl = [] webgpu = [] +bevy_gizmos = ["bevy_shapes?/gizmos", "dep:bevy_gizmos", "dep:bevy_shapes"] [lints] workspace = true diff --git a/crates/bevy_light/src/gizmos.rs b/crates/bevy_light/src/gizmos.rs index 681082a9ea7f5..2f781c6603b7e 100644 --- a/crates/bevy_light/src/gizmos.rs +++ b/crates/bevy_light/src/gizmos.rs @@ -2,7 +2,8 @@ use core::f32::consts::PI; -use bevy_gizmos::primitives::dim3::GizmoPrimitive3d; +use bevy_shapes::gizmos::dim3::GizmoPrimitive3d; +use bevy_shapes::primitives::{Cone, Sphere}; use crate::{DirectionalLight, PointLight, RectLight, SpotLight}; use bevy_app::{Plugin, PostUpdate}; @@ -18,11 +19,7 @@ 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_transform::{components::GlobalTransform, TransformSystems}; diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index 6d3a23d637894..1e07a375db598 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -7,8 +7,8 @@ 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_mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}; +use bevy_math::{Quat, Vec2, Vec3}; +use bevy_mesh::{Mesh, Mesh3d, MeshVertexBufferLayoutRef}; use bevy_reflect::{Reflect, TypePath}; use bevy_render::{ render_asset::RenderAssets, @@ -28,14 +28,12 @@ impl Plugin for ForwardDecalPlugin { fn build(&self, app: &mut App) { load_shader_library!(app, "forward_decal.wgsl"); - let mesh = app.world_mut().resource_mut::>().add( - Rectangle::from_size(Vec2::ONE) - .mesh_builder() - .build() + let mesh = app.world_mut().resource_mut::>().add({ + Mesh::quad_mesh(Vec2::ONE) .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) .with_generated_tangents() - .unwrap(), - ); + .unwrap() + }); app.insert_resource(ForwardDecalMesh(mesh)); diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1f22f733e9e28..d20061bf4bb14 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -32,9 +32,8 @@ use bevy_material::{ MaterialProperties, }; use bevy_math::{ - ops, - primitives::{HalfSpace, ViewFrustum}, - proj, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, + camera::{HalfSpace, ViewFrustum}, + ops, proj, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, }; use bevy_mesh::{Mesh3d, MeshVertexBufferLayoutRef}; use bevy_platform::collections::{HashMap, HashSet}; diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index c6b5fb510f4c7..79548559375a7 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -37,11 +37,8 @@ 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_mesh::{Mesh, Meshable}; +use bevy_math::{Dir3, Vec2, Vec3}; +use bevy_mesh::Mesh; use bevy_render::{ render_resource::SpecializedRenderPipelines, sync_component::{SyncComponent, SyncComponentPlugin}, @@ -67,8 +64,8 @@ impl Plugin for VolumetricFogPlugin { embedded_asset!(app, "volumetric_fog.wgsl"); let mut meshes = app.world_mut().resource_mut::>(); - let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh_builder()); - let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh_builder()); + let plane_mesh = meshes.add(Mesh::plane_mesh(Dir3::Z, Vec2::ONE, 0, 0)); + let cube_mesh = meshes.add(Mesh::cuboid_mesh(Vec3::ONE)); app.add_plugins(SyncComponentPlugin::::default()); diff --git a/crates/bevy_shapes/src/meshing/dim2.rs b/crates/bevy_shapes/src/meshing/dim2.rs index d80ef57cbed11..48de0c41ca29a 100644 --- a/crates/bevy_shapes/src/meshing/dim2.rs +++ b/crates/bevy_shapes/src/meshing/dim2.rs @@ -4,13 +4,10 @@ use core::mem; use bevy_asset::RenderAssetUsages; use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, VertexAttributeValues}; -use crate::{ - meshing::{extrusion::PerimeterSegment, Extrudable}, - primitives::{ - Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, - Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, - Triangle3d, WindingOrder, - }, +use crate::primitives::{ + Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, + Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d, + Triangle3d, WindingOrder, }; use bevy_math::{ops, FloatExt, Vec2, Vec3}; use bevy_reflect::prelude::*; @@ -67,16 +64,6 @@ impl MeshBuilder for CircleMeshBuilder { } } -impl Extrudable for CircleMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (0..self.resolution).chain([0]).collect(), - }] - } -} - impl Meshable for Circle { type Output = CircleMeshBuilder; @@ -215,24 +202,6 @@ impl MeshBuilder for CircularSectorMeshBuilder { } } -impl Extrudable for CircularSectorMeshBuilder { - fn perimeter(&self) -> Vec { - let (sin, cos) = ops::sin_cos(self.sector.arc.half_angle); - let first_normal = Vec2::new(sin, cos); - let last_normal = Vec2::new(-sin, cos); - alloc::vec![ - PerimeterSegment::Flat { - indices: alloc::vec![self.resolution, 0, 1], - }, - PerimeterSegment::Smooth { - first_normal, - last_normal, - indices: (1..=self.resolution).collect(), - }, - ] - } -} - impl Meshable for CircularSector { type Output = CircularSectorMeshBuilder; @@ -353,24 +322,6 @@ impl MeshBuilder for CircularSegmentMeshBuilder { } } -impl Extrudable for CircularSegmentMeshBuilder { - fn perimeter(&self) -> Vec { - let (sin, cos) = ops::sin_cos(self.segment.arc.half_angle); - let first_normal = Vec2::new(sin, cos); - let last_normal = Vec2::new(-sin, cos); - alloc::vec![ - PerimeterSegment::Flat { - indices: alloc::vec![self.resolution, 0, 1], - }, - PerimeterSegment::Smooth { - first_normal, - last_normal, - indices: (1..=self.resolution).collect(), - }, - ] - } -} - impl Meshable for CircularSegment { type Output = CircularSegmentMeshBuilder; @@ -441,14 +392,6 @@ impl MeshBuilder for ConvexPolygonMeshBuilder { } } -impl Extrudable for ConvexPolygonMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Flat { - indices: (0..self.vertices.len() as u32).chain([0]).collect(), - }] - } -} - /// A builder used for creating a [`Mesh`] with a [`RegularPolygon`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -509,14 +452,6 @@ impl MeshBuilder for RegularPolygonMeshBuilder { } } -impl Extrudable for RegularPolygonMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Flat { - indices: (0..self.sides).chain([0]).collect(), - }] - } -} - /// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -595,16 +530,6 @@ impl MeshBuilder for EllipseMeshBuilder { } } -impl Extrudable for EllipseMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (0..self.resolution).chain([0]).collect(), - }] - } -} - impl Meshable for Ellipse { type Output = EllipseMeshBuilder; @@ -785,24 +710,6 @@ impl MeshBuilder for AnnulusMeshBuilder { } } -impl Extrudable for AnnulusMeshBuilder { - fn perimeter(&self) -> Vec { - let vert_count = 2 * self.resolution; - alloc::vec![ - PerimeterSegment::Smooth { - first_normal: Vec2::NEG_Y, - last_normal: Vec2::NEG_Y, - indices: (0..vert_count).step_by(2).chain([0]).rev().collect(), // Inner hole - }, - PerimeterSegment::Smooth { - first_normal: Vec2::Y, - last_normal: Vec2::Y, - indices: (1..vert_count).step_by(2).chain([1]).collect(), // Outer perimeter - }, - ] - } -} - impl Meshable for Annulus { type Output = AnnulusMeshBuilder; @@ -876,14 +783,6 @@ impl MeshBuilder for RhombusMeshBuilder { } } -impl Extrudable for RhombusMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Flat { - indices: alloc::vec![0, 1, 2, 3, 0], - }] - } -} - impl Meshable for Rhombus { type Output = RhombusMeshBuilder; @@ -950,21 +849,6 @@ impl MeshBuilder for Triangle2dMeshBuilder { } } -impl Extrudable for Triangle2dMeshBuilder { - fn perimeter(&self) -> Vec { - let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; - if is_ccw { - alloc::vec![PerimeterSegment::Flat { - indices: alloc::vec![0, 1, 2, 0], - }] - } else { - alloc::vec![PerimeterSegment::Flat { - indices: alloc::vec![2, 1, 0, 2], - }] - } - } -} - /// A builder used for creating a [`Mesh`] with a [`Rectangle`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -999,33 +883,7 @@ impl RectangleMeshBuilder { impl MeshBuilder for RectangleMeshBuilder { fn build(&self) -> Mesh { - let [hw, hh] = [self.half_size.x, self.half_size.y]; - let positions = alloc::vec![ - [hw, hh, 0.0], - [-hw, hh, 0.0], - [-hw, -hh, 0.0], - [hw, -hh, 0.0], - ]; - let normals = alloc::vec![[0.0, 0.0, 1.0]; 4]; - let uvs = alloc::vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; - let indices = Indices::U32(alloc::vec![0, 1, 2, 0, 2, 3]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_indices(indices) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - } -} - -impl Extrudable for RectangleMeshBuilder { - fn perimeter(&self) -> Vec { - alloc::vec![PerimeterSegment::Flat { - indices: alloc::vec![0, 1, 2, 3, 0], - }] + Mesh::quad_mesh(self.half_size) } } @@ -1157,32 +1015,6 @@ impl MeshBuilder for Capsule2dMeshBuilder { } } -impl Extrudable for Capsule2dMeshBuilder { - fn perimeter(&self) -> Vec { - let resolution = self.resolution; - let top_semi_indices = (0..resolution).collect(); - let bottom_semi_indices = (resolution..(2 * resolution)).collect(); - alloc::vec![ - PerimeterSegment::Smooth { - first_normal: Vec2::X, - last_normal: Vec2::NEG_X, - indices: top_semi_indices, - }, // Top semi-circle - PerimeterSegment::Flat { - indices: alloc::vec![resolution - 1, resolution], - }, // Left edge - PerimeterSegment::Smooth { - first_normal: Vec2::NEG_X, - last_normal: Vec2::X, - indices: bottom_semi_indices, - }, // Bottom semi-circle - PerimeterSegment::Flat { - indices: alloc::vec![2 * resolution - 1, 0], - }, // Right edge - ] - } -} - impl Meshable for Capsule2d { type Output = Capsule2dMeshBuilder; @@ -1373,72 +1205,6 @@ where } } -impl

Extrudable for RingMeshBuilder

-where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - /// A list of the indices each representing a part of the perimeter of the mesh. - /// - /// # Panics - /// - /// Panics if the following assumptions are not met. - /// - /// It is assumed that the inner and outer meshes have the same number of vertices. - /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated - /// a different number of vertices for the inner and outer instances of the primitive. - /// - /// It is assumed that the `primitive_topology` of the mesh returned by - /// the underlying builder is [`PrimitiveTopology::TriangleList`] - /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. - fn perimeter(&self) -> Vec { - let outer_vertex_count = self - .get_vertex_attributes() - .filter(|r| r.outer_positions.len() == r.inner_positions.len()) - .expect("The inner and outer meshes should have the same number of vertices, and have required attributes") - .outer_positions - .len(); - - let mut outer_perimeter = self.outer_shape_builder.perimeter(); - let inner_perimeter = - self.inner_shape_builder - .perimeter() - .into_iter() - .rev() - .map(|segment| match segment { - PerimeterSegment::Smooth { - first_normal, - last_normal, - mut indices, - } => PerimeterSegment::Smooth { - first_normal: -last_normal, - last_normal: -first_normal, - indices: { - let outer_perimeter_vertex_count = outer_vertex_count as u32; - indices.reverse(); - for i in &mut indices { - *i += outer_perimeter_vertex_count; - } - indices - }, - }, - PerimeterSegment::Flat { mut indices } => PerimeterSegment::Flat { - indices: { - let outer_perimeter_vertex_count = outer_vertex_count as u32; - indices.reverse(); - for i in &mut indices { - *i += outer_perimeter_vertex_count; - } - indices - }, - }, - }); - - outer_perimeter.extend(inner_perimeter); - outer_perimeter - } -} - impl

Meshable for Ring

where P: Primitive2d + Meshable, diff --git a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs index 37152baa9919b..f3127cd0ad6ab 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs @@ -23,64 +23,7 @@ impl Default for CuboidMeshBuilder { impl MeshBuilder for CuboidMeshBuilder { fn build(&self) -> Mesh { - let min = -self.half_size; - let max = self.half_size; - - // Suppose Y-up right hand, and camera look from +Z to -Z - let vertices = &[ - // Front - ([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]), - ([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]), - ([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]), - ([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]), - // Back - ([min.x, max.y, min.z], [0.0, 0.0, -1.0], [1.0, 0.0]), - ([max.x, max.y, min.z], [0.0, 0.0, -1.0], [0.0, 0.0]), - ([max.x, min.y, min.z], [0.0, 0.0, -1.0], [0.0, 1.0]), - ([min.x, min.y, min.z], [0.0, 0.0, -1.0], [1.0, 1.0]), - // Right - ([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]), - ([max.x, max.y, min.z], [1.0, 0.0, 0.0], [1.0, 0.0]), - ([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]), - ([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]), - // Left - ([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]), - ([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]), - ([min.x, max.y, min.z], [-1.0, 0.0, 0.0], [0.0, 1.0]), - ([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]), - // Top - ([max.x, max.y, min.z], [0.0, 1.0, 0.0], [1.0, 0.0]), - ([min.x, max.y, min.z], [0.0, 1.0, 0.0], [0.0, 0.0]), - ([min.x, max.y, max.z], [0.0, 1.0, 0.0], [0.0, 1.0]), - ([max.x, max.y, max.z], [0.0, 1.0, 0.0], [1.0, 1.0]), - // Bottom - ([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]), - ([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]), - ([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]), - ([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]), - ]; - - let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect(); - let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); - let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); - - let indices = Indices::U32(alloc::vec![ - 0, 1, 2, 2, 3, 0, // front - 4, 5, 6, 6, 7, 4, // back - 8, 9, 10, 10, 11, 8, // right - 12, 13, 14, 14, 15, 12, // left - 16, 17, 18, 18, 19, 16, // top - 20, 21, 22, 22, 23, 20, // bottom - ]); - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), - ) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_inserted_indices(indices) + Mesh::cuboid_mesh(self.half_size) } } diff --git a/crates/bevy_shapes/src/meshing/dim3/plane.rs b/crates/bevy_shapes/src/meshing/dim3/plane.rs index f10460299e7e7..f2c50221f1f3b 100644 --- a/crates/bevy_shapes/src/meshing/dim3/plane.rs +++ b/crates/bevy_shapes/src/meshing/dim3/plane.rs @@ -142,50 +142,12 @@ impl PlaneMeshBuilder { impl MeshBuilder for PlaneMeshBuilder { fn build(&self) -> Mesh { - let z_vertex_count = self.subdivisions_z + 2; - let x_vertex_count = self.subdivisions_x + 2; - let num_vertices = (z_vertex_count * x_vertex_count) as usize; - let num_indices = ((z_vertex_count - 1) * (x_vertex_count - 1) * 6) as usize; - - let mut positions: Vec = Vec::with_capacity(num_vertices); - let mut normals: Vec<[f32; 3]> = Vec::with_capacity(num_vertices); - let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(num_vertices); - let mut indices: Vec = Vec::with_capacity(num_indices); - - let rotation = Quat::from_rotation_arc(Vec3::Y, *self.plane.normal); - let size = self.plane.half_size * 2.0; - - for z in 0..z_vertex_count { - for x in 0..x_vertex_count { - let tx = x as f32 / (x_vertex_count - 1) as f32; - let tz = z as f32 / (z_vertex_count - 1) as f32; - let pos = rotation * Vec3::new((-0.5 + tx) * size.x, 0.0, (-0.5 + tz) * size.y); - positions.push(pos); - normals.push(self.plane.normal.to_array()); - uvs.push([tx, tz]); - } - } - - for z in 0..z_vertex_count - 1 { - for x in 0..x_vertex_count - 1 { - let quad = z * x_vertex_count + x; - indices.push(quad + x_vertex_count + 1); - indices.push(quad + 1); - indices.push(quad + x_vertex_count); - indices.push(quad); - indices.push(quad + x_vertex_count); - indices.push(quad + 1); - } - } - - Mesh::new( - PrimitiveTopology::TriangleList, - RenderAssetUsages::default(), + Mesh::plane_mesh( + self.plane.normal, + self.plane.half_size, + self.subdivisions_x, + self.subdivisions_z, ) - .with_inserted_indices(Indices::U32(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } diff --git a/crates/bevy_shapes/src/meshing/extrusion.rs b/crates/bevy_shapes/src/meshing/extrusion.rs index ba30845187de4..653a53773ec77 100644 --- a/crates/bevy_shapes/src/meshing/extrusion.rs +++ b/crates/bevy_shapes/src/meshing/extrusion.rs @@ -431,3 +431,216 @@ where front_face } } + +impl Extrudable for CircleMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (0..self.resolution).chain([0]).collect(), + }] + } +} + +impl Extrudable for CircularSectorMeshBuilder { + fn perimeter(&self) -> Vec { + let (sin, cos) = ops::sin_cos(self.sector.arc.half_angle); + let first_normal = Vec2::new(sin, cos); + let last_normal = Vec2::new(-sin, cos); + alloc::vec![ + PerimeterSegment::Flat { + indices: alloc::vec![self.resolution, 0, 1], + }, + PerimeterSegment::Smooth { + first_normal, + last_normal, + indices: (1..=self.resolution).collect(), + }, + ] + } +} + +impl Extrudable for CircularSegmentMeshBuilder { + fn perimeter(&self) -> Vec { + let (sin, cos) = ops::sin_cos(self.segment.arc.half_angle); + let first_normal = Vec2::new(sin, cos); + let last_normal = Vec2::new(-sin, cos); + alloc::vec![ + PerimeterSegment::Flat { + indices: alloc::vec![self.resolution, 0, 1], + }, + PerimeterSegment::Smooth { + first_normal, + last_normal, + indices: (1..=self.resolution).collect(), + }, + ] + } +} + +impl Extrudable for ConvexPolygonMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: (0..self.vertices.len() as u32).chain([0]).collect(), + }] + } +} + +impl Extrudable for RegularPolygonMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: (0..self.sides).chain([0]).collect(), + }] + } +} + +impl Extrudable for EllipseMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (0..self.resolution).chain([0]).collect(), + }] + } +} + +impl Extrudable for AnnulusMeshBuilder { + fn perimeter(&self) -> Vec { + let vert_count = 2 * self.resolution; + alloc::vec![ + PerimeterSegment::Smooth { + first_normal: Vec2::NEG_Y, + last_normal: Vec2::NEG_Y, + indices: (0..vert_count).step_by(2).chain([0]).rev().collect(), // Inner hole + }, + PerimeterSegment::Smooth { + first_normal: Vec2::Y, + last_normal: Vec2::Y, + indices: (1..vert_count).step_by(2).chain([1]).collect(), // Outer perimeter + }, + ] + } +} + +impl Extrudable for RhombusMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 3, 0], + }] + } +} + +impl Extrudable for Triangle2dMeshBuilder { + fn perimeter(&self) -> Vec { + let is_ccw = self.triangle.winding_order() == WindingOrder::CounterClockwise; + if is_ccw { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 0], + }] + } else { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![2, 1, 0, 2], + }] + } + } +} + +impl Extrudable for RectangleMeshBuilder { + fn perimeter(&self) -> Vec { + alloc::vec![PerimeterSegment::Flat { + indices: alloc::vec![0, 1, 2, 3, 0], + }] + } +} + +impl Extrudable for Capsule2dMeshBuilder { + fn perimeter(&self) -> Vec { + let resolution = self.resolution; + let top_semi_indices = (0..resolution).collect(); + let bottom_semi_indices = (resolution..(2 * resolution)).collect(); + alloc::vec![ + PerimeterSegment::Smooth { + first_normal: Vec2::X, + last_normal: Vec2::NEG_X, + indices: top_semi_indices, + }, // Top semi-circle + PerimeterSegment::Flat { + indices: alloc::vec![resolution - 1, resolution], + }, // Left edge + PerimeterSegment::Smooth { + first_normal: Vec2::NEG_X, + last_normal: Vec2::X, + indices: bottom_semi_indices, + }, // Bottom semi-circle + PerimeterSegment::Flat { + indices: alloc::vec![2 * resolution - 1, 0], + }, // Right edge + ] + } +} + +impl

Extrudable for RingMeshBuilder

+where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + /// A list of the indices each representing a part of the perimeter of the mesh. + /// + /// # Panics + /// + /// Panics if the following assumptions are not met. + /// + /// It is assumed that the inner and outer meshes have the same number of vertices. + /// If not, then the [`MeshBuilder`] of the underlying 2d primitive has generated + /// a different number of vertices for the inner and outer instances of the primitive. + /// + /// It is assumed that the `primitive_topology` of the mesh returned by + /// the underlying builder is [`PrimitiveTopology::TriangleList`] + /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. + fn perimeter(&self) -> Vec { + let outer_vertex_count = self + .get_vertex_attributes() + .filter(|r| r.outer_positions.len() == r.inner_positions.len()) + .expect("The inner and outer meshes should have the same number of vertices, and have required attributes") + .outer_positions + .len(); + + let mut outer_perimeter = self.outer_shape_builder.perimeter(); + let inner_perimeter = + self.inner_shape_builder + .perimeter() + .into_iter() + .rev() + .map(|segment| match segment { + PerimeterSegment::Smooth { + first_normal, + last_normal, + mut indices, + } => PerimeterSegment::Smooth { + first_normal: -last_normal, + last_normal: -first_normal, + indices: { + let outer_perimeter_vertex_count = outer_vertex_count as u32; + indices.reverse(); + for i in &mut indices { + *i += outer_perimeter_vertex_count; + } + indices + }, + }, + PerimeterSegment::Flat { mut indices } => PerimeterSegment::Flat { + indices: { + let outer_perimeter_vertex_count = outer_vertex_count as u32; + indices.reverse(); + for i in &mut indices { + *i += outer_perimeter_vertex_count; + } + indices + }, + }, + }); + + outer_perimeter.extend(inner_perimeter); + outer_perimeter + } +} diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index f012e429a53ff..a6d05e8b5977f 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -9,8 +9,8 @@ use bevy_ecs::{ use bevy_asset::{Assets, Handle}; use bevy_image::TextureAtlasLayout; -use bevy_math::{primitives::Rectangle, vec2}; -use bevy_mesh::{Mesh, Mesh2d, MeshAttributeCompressionFlags, MeshBuilder, Meshable}; +use bevy_math::Vec2; +use bevy_mesh::{Mesh, Mesh2d, MeshAttributeCompressionFlags}; use bevy_platform::collections::HashMap; use bevy_shader::load_shader_library; @@ -50,9 +50,7 @@ fn add_mesh( ) { let quad = quad.get_or_insert_with(|| { meshes.add( - Rectangle::from_size(vec2(1.0, 1.0)) - .mesh_builder() - .build() + Mesh::quad_mesh(Vec2::ONE) .with_removed_attribute(Mesh::ATTRIBUTE_NORMAL) .compressed_mesh( MeshAttributeCompressionFlags::COMPRESS_POSITION diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index d8353466ff7b4..55f1f529125c3 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -15,7 +15,7 @@ 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}; @@ -173,7 +173,7 @@ fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: mesh.clone() } else { let mut meshes = world.resource_mut::>(); - meshes.add(Rectangle::from_size(mesh_size.as_vec2())) + meshes.add(Mesh::quad_mesh(mesh_size.as_vec2())) }; let mut images = world.resource_mut::>(); From 61f7edb4c420125b5a0482fb9c3331b8c00fdbcd Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 22:20:37 +0200 Subject: [PATCH 16/17] add a prelude --- crates/bevy_shapes/Cargo.toml | 1 + crates/bevy_shapes/src/lib.rs | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/bevy_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index 36952056b89e4..4d3e60e8d0ddf 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -53,6 +53,7 @@ bounding = ["dep:arrayvec"] extrusion = ["meshing"] gizmos = ["dep:bevy_color", "dep:bevy_gizmos"] meshing = [ + "alloc", "bevy_reflect", "dep:bevy_asset", "dep:bevy_mesh", diff --git a/crates/bevy_shapes/src/lib.rs b/crates/bevy_shapes/src/lib.rs index 45ff67c751f31..a2b1bc3b064e0 100644 --- a/crates/bevy_shapes/src/lib.rs +++ b/crates/bevy_shapes/src/lib.rs @@ -60,7 +60,7 @@ extern crate alloc; pub mod primitives; -#[cfg(all(feature = "meshing", feature = "alloc"))] +#[cfg(feature = "meshing")] pub mod meshing; #[cfg(feature = "bounding")] @@ -71,3 +71,27 @@ pub mod sampling; #[cfg(feature = "gizmos")] pub mod gizmos; + +/// The shapes prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::primitives::*; + + #[doc(hidden)] + #[cfg(feature = "meshing")] + pub use crate::meshing::*; + + #[doc(hidden)] + #[cfg(feature = "bounding")] + pub use crate::bounding::*; + + #[doc(hidden)] + #[cfg(feature = "sampling")] + pub use crate::sampling::*; + + #[doc(hidden)] + #[cfg(feature = "gizmos")] + pub use crate::gizmos::*; +} From 0c6cf610c4e480b86da876677edd664eece02dcf Mon Sep 17 00:00:00 2001 From: aviac Date: Wed, 15 Jul 2026 23:49:46 +0200 Subject: [PATCH 17/17] fix outstanding compile/clippy errors --- .../src/transform_gizmo_render.rs | 2 +- crates/bevy_internal/Cargo.toml | 8 ++ crates/bevy_internal/src/lib.rs | 1 + crates/bevy_internal/src/prelude.rs | 3 + crates/bevy_light/src/gizmos.rs | 2 +- crates/bevy_math/src/ray.rs | 59 ++++------ crates/bevy_shapes/Cargo.toml | 15 ++- crates/bevy_shapes/src/bounding/extrusion.rs | 2 +- crates/bevy_shapes/src/bounding/mod.rs | 8 +- crates/bevy_shapes/src/gizmos/mod.rs | 8 +- crates/bevy_shapes/src/meshing/dim2.rs | 107 ++++++++++++++++-- .../bevy_shapes/src/meshing/dim3/capsule.rs | 6 + crates/bevy_shapes/src/meshing/dim3/cone.rs | 8 +- .../src/meshing/dim3/conical_frustum.rs | 6 + crates/bevy_shapes/src/meshing/dim3/cuboid.rs | 10 +- .../bevy_shapes/src/meshing/dim3/cylinder.rs | 6 + crates/bevy_shapes/src/meshing/dim3/plane.rs | 12 +- .../src/meshing/dim3/polyline3d.rs | 6 + .../bevy_shapes/src/meshing/dim3/segment3d.rs | 6 + crates/bevy_shapes/src/meshing/dim3/sphere.rs | 6 + .../src/meshing/dim3/tetrahedron.rs | 6 + crates/bevy_shapes/src/meshing/dim3/torus.rs | 6 + .../src/meshing/dim3/triangle3d.rs | 10 +- crates/bevy_shapes/src/meshing/extrusion.rs | 21 +++- examples/2d/mesh2d_arcs.rs | 1 - examples/3d/3d_viewport_to_world.rs | 2 +- examples/3d/auto_exposure.rs | 2 +- examples/3d/irradiance_volumes.rs | 4 +- examples/3d/mirror.rs | 2 +- examples/ecs/error_handling.rs | 5 +- examples/math/custom_primitives.rs | 5 +- examples/math/random_sampling.rs | 1 - examples/shader/extended_material_bindless.rs | 1 - 33 files changed, 259 insertions(+), 88 deletions(-) diff --git a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs index 6f19b699e0894..0154392749745 100644 --- a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs +++ b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut}, }; use bevy_math::{Quat, Vec3}; -use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, Meshable}; +use bevy_mesh::{Mesh, Mesh3d, Meshable}; use bevy_pbr::{MeshMaterial3d, StandardMaterial}; use bevy_shapes::primitives::{Cone, Cuboid, Cylinder, Torus}; use bevy_transform::{ diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 70b9bc34bad59..285669868d54c 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -127,14 +127,17 @@ shader_format_wesl = ["bevy_shader/shader_format_wesl"] serialize = [ "bevy_a11y?/serialize", "bevy_color?/serialize", + "bevy_dev_tools?/serialize", "bevy_ecs/serialize", "bevy_image?/serialize", "bevy_input/serialize", "bevy_math/serialize", + "bevy_shapes/serialize", "bevy_world_serialization?/serialize", "bevy_time/serialize", "bevy_transform/serialize", "bevy_ui?/serialize", + "bevy_utils/serialize", "bevy_window?/serialize", "bevy_winit?/serialize", "bevy_platform/serialize", @@ -416,6 +419,7 @@ std = [ "bevy_input/std", "bevy_input_focus?/std", "bevy_math/std", + "bevy_shapes/std", "bevy_platform/std", "bevy_reflect/std", "bevy_state?/std", @@ -505,6 +509,10 @@ bevy_math = { path = "../bevy_math", version = "0.20.0-dev", default-features = "bevy_reflect", "nostd-libm", ] } +bevy_shapes = { path = "../bevy_shapes", version = "0.20.0-dev", default-features = false, features = [ + "bevy_reflect", + "nostd-libm", +] } 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..1452f0342e242 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -84,6 +84,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_shapes as shapes; #[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..928d417697e35 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -114,3 +114,6 @@ pub use crate::picking::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_clipboard")] pub use crate::clipboard::prelude::*; + +#[doc(hidden)] +pub use crate::shapes::prelude::*; diff --git a/crates/bevy_light/src/gizmos.rs b/crates/bevy_light/src/gizmos.rs index 2f781c6603b7e..94d884252c26d 100644 --- a/crates/bevy_light/src/gizmos.rs +++ b/crates/bevy_light/src/gizmos.rs @@ -2,7 +2,7 @@ use core::f32::consts::PI; -use bevy_shapes::gizmos::dim3::GizmoPrimitive3d; +use bevy_shapes::gizmos::GizmoPrimitive3d; use bevy_shapes::primitives::{Cone, Sphere}; use crate::{DirectionalLight, PointLight, RectLight, SpotLight}; diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray.rs index 3378979add7a6..34298e85bc958 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray.rs @@ -128,38 +128,29 @@ mod tests { let ray = Ray2d::new(Vec2::ZERO, Dir2::Y); // Orthogonal, and test that an inverse plane_normal has the same result - assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::Y)), - Some(1.0) - ); - assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::NEG_Y)), - Some(1.0) - ); - assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::Y)) - .is_none()); - assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::NEG_Y)) - .is_none()); + assert_eq!(ray.intersect_plane(Vec2::Y, Dir2::Y), Some(1.0)); + assert_eq!(ray.intersect_plane(Vec2::Y, Dir2::NEG_Y), Some(1.0)); + assert!(ray.intersect_plane(Vec2::NEG_Y, Dir2::Y).is_none()); + assert!(ray.intersect_plane(Vec2::NEG_Y, Dir2::NEG_Y).is_none()); // Diagonal assert_eq!( - ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::ONE)), + ray.intersect_plane(Vec2::Y, Dir2::new(Vec2::ONE).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::ONE)) + .intersect_plane(Vec2::NEG_Y, Dir2::new(Vec2::ONE).unwrap()) .is_none()); // Parallel - assert!(ray - .intersect_plane(Vec2::X, Plane2d::new(Vec2::X)) - .is_none()); + assert!(ray.intersect_plane(Vec2::X, Dir2::X).is_none()); // Parallel with simulated rounding error assert!(ray - .intersect_plane(Vec2::X, Plane2d::new(Vec2::X + Vec2::Y * f32::EPSILON)) + .intersect_plane( + Vec2::X, + Dir2::new(Vec2::X + Vec2::Y * f32::EPSILON).unwrap() + ) .is_none()); } @@ -168,40 +159,28 @@ mod tests { let ray = Ray3d::new(Vec3::ZERO, Dir3::Z); // Orthogonal, and test that an inverse plane_normal has the same result - assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::Z)), - Some(1.0) - ); - assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::NEG_Z)), - Some(1.0) - ); - assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::Z)) - .is_none()); - assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::NEG_Z)) - .is_none()); + assert_eq!(ray.intersect_plane(Vec3::Z, Dir3::Z), Some(1.0)); + assert_eq!(ray.intersect_plane(Vec3::Z, Dir3::NEG_Z), Some(1.0)); + assert!(ray.intersect_plane(Vec3::NEG_Z, Dir3::Z).is_none()); + assert!(ray.intersect_plane(Vec3::NEG_Z, Dir3::NEG_Z).is_none()); // Diagonal assert_eq!( - ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::ONE)), + ray.intersect_plane(Vec3::Z, Dir3::new(Vec3::ONE).unwrap()), Some(1.0) ); assert!(ray - .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::ONE)) + .intersect_plane(Vec3::NEG_Z, Dir3::new(Vec3::ONE).unwrap()) .is_none()); // Parallel - assert!(ray - .intersect_plane(Vec3::X, InfinitePlane3d::new(Vec3::X)) - .is_none()); + assert!(ray.intersect_plane(Vec3::X, Dir3::X).is_none()); // Parallel with simulated rounding error assert!(ray .intersect_plane( 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_shapes/Cargo.toml b/crates/bevy_shapes/Cargo.toml index 4d3e60e8d0ddf..dc7d674b39750 100644 --- a/crates/bevy_shapes/Cargo.toml +++ b/crates/bevy_shapes/Cargo.toml @@ -41,9 +41,18 @@ bevy_platform = { default-features = false, path = "../bevy_platform" } chacha20 = { default-features = false, features = ["rng"], version = "0.10.0" } [features] -alloc = ["bevy_math/alloc"] -default = ["bevy_math/default", "std"] -std = ["alloc", "bevy_math/std"] +alloc = ["bevy_math/alloc"] +default = [ + "bevy_math/default", + "std", + # all extensions are available (opt-out) + "bounding", + "extrusion", + "gizmos", + "meshing", + "sampling", +] +std = ["alloc", "bevy_math/std"] approx = ["bevy_math/approx", "dep:approx"] bevy_reflect = ["alloc", "bevy_math/bevy_reflect", "dep:bevy_reflect"] diff --git a/crates/bevy_shapes/src/bounding/extrusion.rs b/crates/bevy_shapes/src/bounding/extrusion.rs index 7063b557f669d..7a5d25a36ff46 100644 --- a/crates/bevy_shapes/src/bounding/extrusion.rs +++ b/crates/bevy_shapes/src/bounding/extrusion.rs @@ -1,4 +1,4 @@ -//! Implement [`BoundedExtrusion`] for all the primitives in bevy_shapes +//! Implement [`BoundedExtrusion`] for all the primitives in `bevy_shapes` use core::f32::consts::FRAC_PI_2; diff --git a/crates/bevy_shapes/src/bounding/mod.rs b/crates/bevy_shapes/src/bounding/mod.rs index fca2b430687ff..38b8cf89c1011 100644 --- a/crates/bevy_shapes/src/bounding/mod.rs +++ b/crates/bevy_shapes/src/bounding/mod.rs @@ -6,6 +6,8 @@ //! - [`IntersectsVolume`] abstracts intersection tests against a [`BoundingVolume`] //! - [`Bounded2d`]/[`Bounded3d`] are abstractions for shapes to generate [`BoundingVolume`]s -pub mod dim2; -pub mod dim3; -pub mod extrusion; +mod dim2; +mod dim3; + +mod extrusion; +pub use extrusion::*; diff --git a/crates/bevy_shapes/src/gizmos/mod.rs b/crates/bevy_shapes/src/gizmos/mod.rs index 419ef50217450..4e22a3bcdf79a 100644 --- a/crates/bevy_shapes/src/gizmos/mod.rs +++ b/crates/bevy_shapes/src/gizmos/mod.rs @@ -1,5 +1,9 @@ //! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`Gizmos`](`crate::prelude::Gizmos`). -pub mod dim2; -pub mod dim3; +mod dim2; +pub use dim2::*; + +mod dim3; +pub use dim3::*; + pub(crate) mod helpers; diff --git a/crates/bevy_shapes/src/meshing/dim2.rs b/crates/bevy_shapes/src/meshing/dim2.rs index 48de0c41ca29a..716887e7856d7 100644 --- a/crates/bevy_shapes/src/meshing/dim2.rs +++ b/crates/bevy_shapes/src/meshing/dim2.rs @@ -75,6 +75,12 @@ impl Meshable for Circle { } } +impl From for Mesh { + fn from(value: Circle) -> Self { + value.mesh() + } +} + /// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. /// /// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes @@ -213,6 +219,12 @@ impl Meshable for CircularSector { } } +impl From for Mesh { + fn from(value: CircularSector) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. /// /// The resulting mesh will have a UV-map such that the center of the circle is @@ -333,6 +345,12 @@ impl Meshable for CircularSegment { } } +impl From for Mesh { + fn from(value: CircularSegment) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`ConvexPolygon`] shape. /// /// You must verify that the `vertices` are not concave when constructing this type. You can @@ -354,6 +372,12 @@ impl Meshable for ConvexPolygon { } } +impl From for Mesh { + fn from(value: ConvexPolygon) -> Self { + value.mesh() + } +} + impl MeshBuilder for ConvexPolygonMeshBuilder { fn build(&self) -> Mesh { let len = self.vertices.len(); @@ -397,7 +421,7 @@ impl MeshBuilder for ConvexPolygonMeshBuilder { #[reflect(Default, Debug, Clone)] pub struct RegularPolygonMeshBuilder { circumradius: f32, - sides: u32, + pub(crate) sides: u32, } impl Default for RegularPolygonMeshBuilder { @@ -442,6 +466,12 @@ impl Meshable for RegularPolygon { } } +impl From for Mesh { + fn from(value: RegularPolygon) -> Self { + value.mesh() + } +} + impl MeshBuilder for RegularPolygonMeshBuilder { fn build(&self) -> Mesh { // The ellipse mesh is just a regular polygon with two radii @@ -541,6 +571,12 @@ impl Meshable for Ellipse { } } +impl From for Mesh { + fn from(value: Ellipse) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`Segment2d`]. pub struct Segment2dMeshBuilder { /// The [`Segment2d`] shape. @@ -574,6 +610,12 @@ impl Meshable for Segment2d { } } +impl From for Mesh { + fn from(value: Segment2d) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`Polyline2d`] shape. #[derive(Clone, Debug, Default, Reflect)] #[reflect(Default, Debug, Clone)] @@ -612,6 +654,12 @@ impl Meshable for Polyline2d { } } +impl From for Mesh { + fn from(value: Polyline2d) -> Self { + value.mesh() + } +} + /// A builder for creating a [`Mesh`] with an [`Annulus`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -721,6 +769,12 @@ impl Meshable for Annulus { } } +impl From for Mesh { + fn from(value: Annulus) -> Self { + value.mesh() + } +} + /// A builder for creating a [`Mesh`] with an [`Rhombus`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -793,11 +847,17 @@ impl Meshable for Rhombus { } } +impl From for Mesh { + fn from(value: Rhombus) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`Triangle2d`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] #[reflect(Default, Debug, Clone)] pub struct Triangle2dMeshBuilder { - triangle: Triangle2d, + pub(crate) triangle: Triangle2d, } impl Triangle2dMeshBuilder { @@ -817,6 +877,12 @@ impl Meshable for Triangle2d { } } +impl From for Mesh { + fn from(value: Triangle2d) -> Self { + value.mesh() + } +} + impl MeshBuilder for Triangle2dMeshBuilder { fn build(&self) -> Mesh { let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.)); @@ -897,6 +963,12 @@ impl Meshable for Rectangle { } } +impl From for Mesh { + fn from(value: Rectangle) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -1026,6 +1098,12 @@ impl Meshable for Capsule2d { } } +impl From for Mesh { + fn from(value: Capsule2d) -> Self { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with a [`Ring`] shape. pub struct RingMeshBuilder

where @@ -1056,7 +1134,7 @@ where self } - fn get_vertex_attributes(&self) -> Option { + pub(crate) fn get_vertex_attributes(&self) -> Option { fn get_positions(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { if let VertexAttributeValues::Float32x3(data) = mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)? @@ -1112,13 +1190,13 @@ where } } -struct RingMeshBuilderVertexAttributes { - outer_positions: Vec<[f32; 3]>, - inner_positions: Vec<[f32; 3]>, - outer_normals: Vec<[f32; 3]>, - inner_normals: Vec<[f32; 3]>, - outer_uvs: Vec<[f32; 2]>, - inner_uvs: Vec<[f32; 2]>, +pub(crate) struct RingMeshBuilderVertexAttributes { + pub(crate) outer_positions: Vec<[f32; 3]>, + pub(crate) inner_positions: Vec<[f32; 3]>, + pub(crate) outer_normals: Vec<[f32; 3]>, + pub(crate) inner_normals: Vec<[f32; 3]>, + pub(crate) outer_uvs: Vec<[f32; 2]>, + pub(crate) inner_uvs: Vec<[f32; 2]>, } impl

MeshBuilder for RingMeshBuilder

@@ -1216,6 +1294,15 @@ where } } +impl

From> for Mesh +where + P: Primitive2d + Meshable, +{ + fn from(value: Ring

) -> Self { + value.mesh() + } +} + #[cfg(test)] mod tests { use crate::primitives::{Annulus, ConvexPolygon, RegularPolygon}; diff --git a/crates/bevy_shapes/src/meshing/dim3/capsule.rs b/crates/bevy_shapes/src/meshing/dim3/capsule.rs index b79837986da9b..47e7a076515c9 100644 --- a/crates/bevy_shapes/src/meshing/dim3/capsule.rs +++ b/crates/bevy_shapes/src/meshing/dim3/capsule.rs @@ -431,3 +431,9 @@ impl Meshable for Capsule3d { } } } + +impl From for Mesh { + fn from(value: Capsule3d) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cone.rs b/crates/bevy_shapes/src/meshing/dim3/cone.rs index 120bdcee0cf16..e3c0751ac19b0 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cone.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cone.rs @@ -109,7 +109,7 @@ impl MeshBuilder for ConeMeshBuilder { // of the normal. This allows us to normalize vertex normals efficiently. let normal_slope = self.cone.radius / self.cone.height; // Equivalent to Vec2::new(1.0, slope).length().recip() - let normalization_factor = (1.0 + normal_slope * normal_slope).sqrt().recip(); + let normalization_factor = ops::sqrt(1.0 + normal_slope * normal_slope).recip(); // How much the angle changes at each step let step_theta = core::f32::consts::TAU / self.resolution as f32; @@ -184,6 +184,12 @@ impl Meshable for Cone { } } +impl From for Mesh { + fn from(value: Cone) -> Self { + value.mesh() + } +} + #[cfg(test)] mod tests { use crate::primitives::Cone; diff --git a/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs index dd5ca2087d322..1b1c510f0e79e 100644 --- a/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs +++ b/crates/bevy_shapes/src/meshing/dim3/conical_frustum.rs @@ -178,3 +178,9 @@ impl Meshable for ConicalFrustum { } } } + +impl From for Mesh { + fn from(value: ConicalFrustum) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs index f3127cd0ad6ab..aa3a8fa5b4e58 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cuboid.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cuboid.rs @@ -1,8 +1,6 @@ use crate::primitives::Cuboid; -use alloc::vec::Vec; -use bevy_asset::RenderAssetUsages; use bevy_math::Vec3; -use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use bevy_mesh::{Mesh, MeshBuilder, Meshable}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Cuboid`] shape. @@ -36,3 +34,9 @@ impl Meshable for Cuboid { } } } + +impl From for Mesh { + fn from(value: Cuboid) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/cylinder.rs b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs index 70d46a41f106e..18ddc54be7c40 100644 --- a/crates/bevy_shapes/src/meshing/dim3/cylinder.rs +++ b/crates/bevy_shapes/src/meshing/dim3/cylinder.rs @@ -216,3 +216,9 @@ impl Meshable for Cylinder { } } } + +impl From for Mesh { + fn from(value: Cylinder) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/plane.rs b/crates/bevy_shapes/src/meshing/dim3/plane.rs index f2c50221f1f3b..21cecf33a2fd0 100644 --- a/crates/bevy_shapes/src/meshing/dim3/plane.rs +++ b/crates/bevy_shapes/src/meshing/dim3/plane.rs @@ -1,8 +1,6 @@ use crate::primitives::Plane3d; -use alloc::vec::Vec; -use bevy_asset::RenderAssetUsages; -use bevy_math::{Dir3, Quat, Vec2, Vec3}; -use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use bevy_math::{Dir3, Vec2}; +use bevy_mesh::{Mesh, MeshBuilder, Meshable}; use bevy_reflect::prelude::*; /// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. @@ -162,3 +160,9 @@ impl Meshable for Plane3d { } } } + +impl From for Mesh { + fn from(value: Plane3d) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs index c9ece3c955977..6d4520ac11276 100644 --- a/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/polyline3d.rs @@ -36,3 +36,9 @@ impl Meshable for Polyline3d { } } } + +impl From for Mesh { + fn from(value: Polyline3d) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/segment3d.rs b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs index ece7ae4494d52..292a9c69f40c1 100644 --- a/crates/bevy_shapes/src/meshing/dim3/segment3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/segment3d.rs @@ -30,6 +30,12 @@ impl Meshable for Segment3d { } } +impl From for Mesh { + fn from(value: Segment3d) -> Self { + value.mesh() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_shapes/src/meshing/dim3/sphere.rs b/crates/bevy_shapes/src/meshing/dim3/sphere.rs index b182c51255d9a..48e0a1e5f2901 100644 --- a/crates/bevy_shapes/src/meshing/dim3/sphere.rs +++ b/crates/bevy_shapes/src/meshing/dim3/sphere.rs @@ -262,3 +262,9 @@ impl Meshable for Sphere { } } } + +impl From for Mesh { + fn from(value: Sphere) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs index db0a112b420ac..65d6b5b562aac 100644 --- a/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs +++ b/crates/bevy_shapes/src/meshing/dim3/tetrahedron.rs @@ -61,3 +61,9 @@ impl Meshable for Tetrahedron { TetrahedronMeshBuilder { tetrahedron: *self } } } + +impl From for Mesh { + fn from(value: Tetrahedron) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/torus.rs b/crates/bevy_shapes/src/meshing/dim3/torus.rs index a2a02cd5fc0c2..6d4aeff113b11 100644 --- a/crates/bevy_shapes/src/meshing/dim3/torus.rs +++ b/crates/bevy_shapes/src/meshing/dim3/torus.rs @@ -170,3 +170,9 @@ impl Meshable for Torus { } } } + +impl From for Mesh { + fn from(value: Torus) -> Self { + value.mesh() + } +} diff --git a/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs index 801475133d724..4ee587b8c5cac 100644 --- a/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs +++ b/crates/bevy_shapes/src/meshing/dim3/triangle3d.rs @@ -1,7 +1,7 @@ use crate::primitives::Triangle3d; use alloc::vec::Vec; use bevy_asset::RenderAssetUsages; -use bevy_math::Vec3; +use bevy_math::{ops, Vec3}; use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_reflect::prelude::*; @@ -42,6 +42,12 @@ impl Meshable for Triangle3d { } } +impl From for Mesh { + fn from(value: Triangle3d) -> Self { + value.mesh() + } +} + /// The normal of a [`Triangle3d`] with zeroing so that a [`Vec3`] is always obtained for meshing. #[inline] pub(crate) fn normal_vec(triangle: &Triangle3d) -> Vec3 { @@ -72,7 +78,7 @@ pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] { // Obtuse triangle leaning to the left => x direction extends to the left, shifting a from 0. if offset < 0. { let total_length = 1. - offset; - let a_uv = [offset.abs() / total_length, 0.]; + let a_uv = [ops::abs(offset) / total_length, 0.]; let b_uv = [1., 0.]; let c_uv = [0., 1.]; diff --git a/crates/bevy_shapes/src/meshing/extrusion.rs b/crates/bevy_shapes/src/meshing/extrusion.rs index 653a53773ec77..f87a0554ad91b 100644 --- a/crates/bevy_shapes/src/meshing/extrusion.rs +++ b/crates/bevy_shapes/src/meshing/extrusion.rs @@ -1,6 +1,13 @@ -use crate::primitives::{Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d}; +use crate::meshing::{ + AnnulusMeshBuilder, Capsule2dMeshBuilder, CircleMeshBuilder, CircularSectorMeshBuilder, + CircularSegmentMeshBuilder, ConvexPolygonMeshBuilder, EllipseMeshBuilder, RectangleMeshBuilder, + RegularPolygonMeshBuilder, RhombusMeshBuilder, RingMeshBuilder, Triangle2dMeshBuilder, +}; +use crate::primitives::{ + Annulus, Capsule2d, Circle, Ellipse, Extrusion, Primitive2d, WindingOrder, +}; use alloc::vec::Vec; -use bevy_math::{Vec2, Vec3}; +use bevy_math::{ops, Vec2, Vec3}; use bevy_mesh::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology, VertexAttributeValues}; @@ -99,6 +106,16 @@ where } } +impl

From> for Mesh +where + P: Primitive2d + Meshable, + P::Output: Extrudable, +{ + fn from(value: Extrusion

) -> Mesh { + value.mesh() + } +} + /// A builder used for creating a [`Mesh`] with an [`Extrusion`] shape. pub struct ExtrusionBuilder

where diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index af9c3ec5638b8..702c34aa71c41 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -10,7 +10,6 @@ use bevy::{ bounding::{Bounded2d, BoundingVolume}, Isometry2d, }, - mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, prelude::*, }; diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 14e7c28a8a1a3..2b165e34ed01f 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -22,7 +22,7 @@ fn draw_cursor( // Calculate a ray pointing from the camera into the world based on the cursor's position. && let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) // Calculate if and where the ray is hitting the ground plane. - && let Some(point) = ray.plane_intersection_point(ground.translation(), InfinitePlane3d::new(ground.up())) + && let Some(point) = ray.plane_intersection_point(ground.translation(), ground.up()) { // Draw a circle just above the ground plane at that position. gizmos.circle( diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 7755b918fec2b..c88d53874cf58 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/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 5f3bd9d3bba67..3647e33579619 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -469,9 +469,7 @@ fn handle_mouse_clicks( let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; - let Some(plane_intersection) = - ray.plane_intersection_point(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) - else { + let Some(plane_intersection) = ray.plane_intersection_point(Vec3::ZERO, Dir3::Y) else { return; }; // Move all the main objects. diff --git a/examples/3d/mirror.rs b/examples/3d/mirror.rs index 07882b95ff7e1..d4fa2b63621c1 100644 --- a/examples/3d/mirror.rs +++ b/examples/3d/mirror.rs @@ -465,7 +465,7 @@ fn move_fox_on_mouse_down( let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; - let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else { + let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, Dir3::Y) else { return; }; let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance; diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index 890e5e9885150..6d509e1f8ced0 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; @@ -98,7 +97,9 @@ fn setup( )); // Generate random sample points: - let triangles = sphere_mesh.triangles()?; + let triangles = sphere_mesh + .triangles()? + .map(|vertices| Triangle3d { vertices }); let distribution = UniformMeshSampler::try_new(triangles)?; // Setup sample points: diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index 0807f3ca4c1f9..6a8c2d3066278 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -12,12 +12,9 @@ use bevy::{ color::palettes::css::{RED, WHITE}, input::common_conditions::{input_just_pressed, input_toggle_active}, math::{ - bounding::{ - Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume, - }, + bounding::{Aabb2d, Bounded2d, Bounded3d, BoundingCircle, BoundingVolume}, Isometry2d, }, - mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment}, prelude::*, }; diff --git a/examples/math/random_sampling.rs b/examples/math/random_sampling.rs index 0bd6b40f6bd10..eeacd527cef20 100644 --- a/examples/math/random_sampling.rs +++ b/examples/math/random_sampling.rs @@ -3,7 +3,6 @@ use bevy::{ input::mouse::{AccumulatedMouseMotion, MouseButtonInput}, math::prelude::*, - mesh::SphereKind, prelude::*, }; use chacha20::ChaCha8Rng; diff --git a/examples/shader/extended_material_bindless.rs b/examples/shader/extended_material_bindless.rs index a9d9c35e6de89..0035e4fc4756f 100644 --- a/examples/shader/extended_material_bindless.rs +++ b/examples/shader/extended_material_bindless.rs @@ -4,7 +4,6 @@ use std::f32::consts::FRAC_PI_2; use bevy::{ color::palettes::{css::RED, tailwind::GRAY_600}, - mesh::{SphereKind, SphereMeshBuilder}, pbr::{ExtendedMaterial, MaterialExtension, MeshMaterial3d}, prelude::*, render::render_resource::{AsBindGroup, ShaderType},