New circular primitives: Arc2d, CircularSector, CircularSegment (#13482)

# Objective

Adopted #11748

## Solution

I've rebased on main to fix the merge conflicts. ~~Not quite ready to
merge yet~~

* Clippy is happy and the tests are passing, but...
* ~~The new shapes in `examples/2d/2d_shapes.rs` don't look right at
all~~ Never mind, looks like radians and degrees just got mixed up at
some point?
* I have updated one doc comment based on a review in the original PR.

---------

Co-authored-by: Alexis "spectria" Horizon <spectria.limina@gmail.com>
Co-authored-by: Alexis "spectria" Horizon <118812919+spectria-limina@users.noreply.github.com>
Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Ben Harper <ben@tukom.org>
This commit is contained in:
Ben Harper 2024-05-24 02:12:46 +10:00 committed by GitHub
parent da1e6e63ff
commit ec01c2dc45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1485 additions and 16 deletions

View file

@ -423,6 +423,17 @@ description = "Renders a 2d mesh"
category = "2D Rendering" category = "2D Rendering"
wasm = true wasm = true
[[example]]
name = "mesh2d_arcs"
path = "examples/2d/mesh2d_arcs.rs"
doc-scrape-examples = true
[package.metadata.example.mesh2d_arcs]
name = "Arc 2D Meshes"
description = "Demonstrates UV-mapping of the circular segment and sector primitives"
category = "2D Rendering"
wasm = true
[[example]] [[example]]
name = "mesh2d_manual" name = "mesh2d_manual"
path = "examples/2d/mesh2d_manual.rs" path = "examples/2d/mesh2d_manual.rs"

View file

@ -18,6 +18,7 @@ approx = { version = "0.5", optional = true }
rand = { version = "0.8", features = [ rand = { version = "0.8", features = [
"alloc", "alloc",
], default-features = false, optional = true } ], default-features = false, optional = true }
smallvec = { version = "1.11" }
[dev-dependencies] [dev-dependencies]
approx = "0.5" approx = "0.5"
@ -26,6 +27,8 @@ rand = "0.8"
rand_chacha = "0.3" rand_chacha = "0.3"
# Enable the approx feature when testing. # Enable the approx feature when testing.
bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] } bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] }
glam = { version = "0.27", features = ["approx"] }
[features] [features]
default = ["rand"] default = ["rand"]

View file

@ -2,11 +2,15 @@
use crate::{ use crate::{
primitives::{ primitives::{
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment,
Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d,
Triangle2d,
}, },
Dir2, Mat2, Rotation2d, Vec2, Dir2, Mat2, Rotation2d, Vec2,
}; };
use std::f32::consts::{FRAC_PI_2, PI, TAU};
use smallvec::SmallVec;
use super::{Aabb2d, Bounded2d, BoundingCircle}; use super::{Aabb2d, Bounded2d, BoundingCircle};
@ -24,6 +28,120 @@ impl Bounded2d for Circle {
} }
} }
// 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<Rotation2d>) -> SmallVec<[Vec2; 7]> {
// 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 = SmallVec::<[Vec2; 7]>::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 = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
let inverted = left_angle < right_angle;
for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
let angle = extremum.to_angle().rem_euclid(TAU);
// 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.
#[allow(clippy::nonminimal_bool)]
if !inverted && angle >= right_angle && angle <= left_angle
|| inverted && (angle >= right_angle || angle <= left_angle)
{
bounds.push(extremum * arc.radius);
}
}
bounds
}
impl Bounded2d for Arc2d {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> 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(translation, rotation);
}
Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation))
}
fn bounding_circle(
&self,
translation: Vec2,
rotation: impl Into<Rotation2d>,
) -> BoundingCircle {
// 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(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 = rotation.into() * self.chord_midpoint();
BoundingCircle::new(center + translation, self.half_chord_length())
}
}
}
impl Bounded2d for CircularSector {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
// 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(translation, rotation);
}
// 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, rotation);
bounds.push(Vec2::ZERO);
Aabb2d::from_point_cloud(translation, 0.0, &bounds)
}
fn bounding_circle(
&self,
translation: Vec2,
rotation: impl Into<Rotation2d>,
) -> BoundingCircle {
if self.arc.is_major() {
// If the arc is major, that is, greater than a semicircle,
// then bounding circle is just the circle defining the sector.
BoundingCircle::new(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(translation, rotation)
}
}
}
impl Bounded2d for CircularSegment {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
self.arc.aabb_2d(translation, rotation)
}
fn bounding_circle(
&self,
translation: Vec2,
rotation: impl Into<Rotation2d>,
) -> BoundingCircle {
self.arc.bounding_circle(translation, rotation)
}
}
impl Bounded2d for Ellipse { impl Bounded2d for Ellipse {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d { fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
let rotation: Rotation2d = rotation.into(); let rotation: Rotation2d = rotation.into();
@ -321,13 +439,16 @@ impl Bounded2d for Capsule2d {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
use approx::assert_abs_diff_eq;
use glam::Vec2; use glam::Vec2;
use crate::{ use crate::{
bounding::Bounded2d, bounding::Bounded2d,
primitives::{ primitives::{
Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d,
RegularPolygon, Segment2d, Triangle2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
}, },
Dir2, Dir2,
}; };
@ -346,6 +467,294 @@ mod tests {
assert_eq!(bounding_circle.radius(), 1.0); 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,
}
// The apothem of an arc covering 1/6th of a circle.
let apothem = f32::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.powi(2)),
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: f32::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 {
println!("subtest case: {}", test.name);
let segment: CircularSegment = test.arc.into();
let arc_aabb = test.arc.aabb_2d(test.translation, test.rotation);
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.translation, test.rotation);
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.translation, test.rotation);
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.translation, test.rotation);
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,
}
// The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0;
let inv_sqrt_3 = f32::sqrt(3.0).recip();
let tests = [
// Test case: An 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: f32::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 {
println!("subtest case: {}", test.name);
let sector: CircularSector = test.arc.into();
let aabb = sector.aabb_2d(test.translation, test.rotation);
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.translation, test.rotation);
assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
}
}
#[test] #[test]
fn ellipse() { fn ellipse() {
let ellipse = Ellipse::new(1.0, 0.5); let ellipse = Ellipse::new(1.0, 0.5);

View file

@ -234,7 +234,7 @@ impl std::ops::Mul<Dir2> for Rotation2d {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::AbsDiffEq for Dir2 { impl approx::AbsDiffEq for Dir2 {
type Epsilon = f32; type Epsilon = f32;
fn default_epsilon() -> f32 { fn default_epsilon() -> f32 {
@ -245,7 +245,7 @@ impl approx::AbsDiffEq for Dir2 {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::RelativeEq for Dir2 { impl approx::RelativeEq for Dir2 {
fn default_max_relative() -> f32 { fn default_max_relative() -> f32 {
f32::EPSILON f32::EPSILON
@ -256,7 +256,7 @@ impl approx::RelativeEq for Dir2 {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::UlpsEq for Dir2 { impl approx::UlpsEq for Dir2 {
fn default_max_ulps() -> u32 { fn default_max_ulps() -> u32 {
4 4

View file

@ -1,4 +1,4 @@
use std::f32::consts::PI; use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
use super::{Measured2d, Primitive2d, WindingOrder}; use super::{Measured2d, Primitive2d, WindingOrder};
use crate::{Dir2, Vec2}; use crate::{Dir2, Vec2};
@ -67,6 +67,639 @@ impl Measured2d for Circle {
} }
} }
/// 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))]
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(always)]
pub 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(always)]
pub 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(always)]
pub 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(always)]
pub fn from_turns(radius: f32, fraction: f32) -> Self {
Self {
radius,
half_angle: fraction * PI,
}
}
/// Get the angle of the arc
#[inline(always)]
pub fn angle(&self) -> f32 {
self.half_angle * 2.0
}
/// Get the length of the arc
#[inline(always)]
pub fn length(&self) -> f32 {
self.angle() * self.radius
}
/// Get the right-hand end point of the arc
#[inline(always)]
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(always)]
pub fn left_endpoint(&self) -> Vec2 {
self.radius * Vec2::from_angle(FRAC_PI_2 + self.half_angle)
}
/// Get the endpoints of the arc
#[inline(always)]
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(always)]
pub fn half_chord_length(&self) -> f32 {
self.radius * f32::sin(self.half_angle)
}
/// Get the distance between the endpoints (the length of the chord)
#[inline(always)]
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(always)]
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(always)]
// 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 * f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2))
}
/// 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(always)]
pub 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(always)]
pub 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)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct CircularSector {
/// The arc defining the sector
#[cfg_attr(feature = "serialize", 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 From<Arc2d> for CircularSector {
fn from(arc: Arc2d) -> Self {
Self { arc }
}
}
impl CircularSector {
/// Create a new [`CircularSector`] from a `radius` and an `angle`
#[inline(always)]
pub fn new(radius: f32, angle: f32) -> Self {
Self::from(Arc2d::new(radius, angle))
}
/// Create a new [`CircularSector`] from a `radius` and an `angle` in radians.
#[inline(always)]
pub fn from_radians(radius: f32, angle: f32) -> Self {
Self::from(Arc2d::from_radians(radius, angle))
}
/// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees.
#[inline(always)]
pub fn from_degrees(radius: f32, angle: f32) -> Self {
Self::from(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(always)]
pub fn from_turns(radius: f32, fraction: f32) -> Self {
Self::from(Arc2d::from_turns(radius, fraction))
}
/// Get half the angle of the sector
#[inline(always)]
pub fn half_angle(&self) -> f32 {
self.arc.half_angle
}
/// Get the angle of the sector
#[inline(always)]
pub fn angle(&self) -> f32 {
self.arc.angle()
}
/// Get the radius of the sector
#[inline(always)]
pub fn radius(&self) -> f32 {
self.arc.radius
}
/// Get the length of the arc defining the sector
#[inline(always)]
pub 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(always)]
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(always)]
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(always)]
pub fn chord_midpoint(&self) -> Vec2 {
self.arc.chord_midpoint()
}
/// Get the length of the apothem of this sector
///
/// See [`Arc2d::apothem`]
#[inline(always)]
pub fn apothem(&self) -> f32 {
self.arc.apothem()
}
/// Get the length of the sagitta of this sector
///
/// See [`Arc2d::sagitta`]
#[inline(always)]
pub fn sagitta(&self) -> f32 {
self.arc.sagitta()
}
/// Returns the area of this sector
#[inline(always)]
pub fn area(&self) -> f32 {
self.arc.radius.powi(2) * self.arc.half_angle
}
}
/// 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)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct CircularSegment {
/// The arc defining the segment
#[cfg_attr(feature = "serialize", 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 From<Arc2d> for CircularSegment {
fn from(arc: Arc2d) -> Self {
Self { arc }
}
}
impl CircularSegment {
/// Create a new [`CircularSegment`] from a `radius`, and an `angle`
#[inline(always)]
pub fn new(radius: f32, angle: f32) -> Self {
Self::from(Arc2d::new(radius, angle))
}
/// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians.
#[inline(always)]
pub fn from_radians(radius: f32, angle: f32) -> Self {
Self::from(Arc2d::from_radians(radius, angle))
}
/// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees.
#[inline(always)]
pub fn from_degrees(radius: f32, angle: f32) -> Self {
Self::from(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(always)]
pub fn from_turns(radius: f32, fraction: f32) -> Self {
Self::from(Arc2d::from_turns(radius, fraction))
}
/// Get the half-angle of the segment
#[inline(always)]
pub fn half_angle(&self) -> f32 {
self.arc.half_angle
}
/// Get the angle of the segment
#[inline(always)]
pub fn angle(&self) -> f32 {
self.arc.angle()
}
/// Get the radius of the segment
#[inline(always)]
pub fn radius(&self) -> f32 {
self.arc.radius
}
/// Get the length of the arc defining the segment
#[inline(always)]
pub fn arc_length(&self) -> f32 {
self.arc.length()
}
/// Get half the length of the segment's base, also known as its chord
#[inline(always)]
#[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(always)]
#[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(always)]
#[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(always)]
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(always)]
#[doc(alias = "height")]
pub fn sagitta(&self) -> f32 {
self.arc.sagitta()
}
/// Returns the area of this segment
#[inline(always)]
pub fn area(&self) -> f32 {
0.5 * self.arc.radius.powi(2) * (self.arc.angle() - self.arc.angle().sin())
}
}
#[cfg(test)]
mod arc_tests {
use std::f32::consts::FRAC_PI_4;
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,
segment_area: 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());
}
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());
}
}
#[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,
segment_area: 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,
segment_area: 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 = f32::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: f32::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,
segment_area: FRAC_PI_4 - 0.5,
};
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,
segment_area: FRAC_PI_2,
};
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,
segment_area: 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 /// An ellipse primitive
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]

View file

@ -386,7 +386,7 @@ impl std::ops::Mul<Vec2> for Rotation2d {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::AbsDiffEq for Rotation2d { impl approx::AbsDiffEq for Rotation2d {
type Epsilon = f32; type Epsilon = f32;
fn default_epsilon() -> f32 { fn default_epsilon() -> f32 {
@ -397,7 +397,7 @@ impl approx::AbsDiffEq for Rotation2d {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::RelativeEq for Rotation2d { impl approx::RelativeEq for Rotation2d {
fn default_max_relative() -> f32 { fn default_max_relative() -> f32 {
f32::EPSILON f32::EPSILON
@ -408,7 +408,7 @@ impl approx::RelativeEq for Rotation2d {
} }
} }
#[cfg(feature = "approx")] #[cfg(any(feature = "approx", test))]
impl approx::UlpsEq for Rotation2d { impl approx::UlpsEq for Rotation2d {
fn default_max_ulps() -> u32 { fn default_max_ulps() -> u32 {
4 4

View file

@ -1,3 +1,5 @@
use std::f32::consts::FRAC_PI_2;
use crate::{ use crate::{
mesh::primitives::dim3::triangle3d, mesh::primitives::dim3::triangle3d,
mesh::{Indices, Mesh}, mesh::{Indices, Mesh},
@ -5,9 +7,12 @@ use crate::{
}; };
use super::{MeshBuilder, Meshable}; use super::{MeshBuilder, Meshable};
use bevy_math::primitives::{ use bevy_math::{
Annulus, Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, Triangle3d, primitives::{
WindingOrder, Annulus, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle,
RegularPolygon, Triangle2d, Triangle3d, WindingOrder,
},
FloatExt, Vec2,
}; };
use wgpu::PrimitiveTopology; use wgpu::PrimitiveTopology;
@ -73,6 +78,287 @@ impl From<Circle> for 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
/// 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)]
#[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)]
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: usize,
/// 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: usize) -> 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
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
let mut positions = Vec::with_capacity(self.resolution + 1);
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
let mut uvs = Vec::with_capacity(self.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 as u32) {
// 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 Meshable for CircularSector {
type Output = CircularSectorMeshBuilder;
fn mesh(&self) -> Self::Output {
CircularSectorMeshBuilder {
sector: *self,
..Default::default()
}
}
}
impl From<CircularSector> 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().build()
}
}
impl From<CircularSectorMeshBuilder> for Mesh {
fn from(sector: CircularSectorMeshBuilder) -> Self {
sector.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)]
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: usize,
/// 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: usize) -> 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
}
/// Builds a [`Mesh`] based on the configuration in `self`.
pub fn build(&self) -> Mesh {
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
let mut positions = Vec::with_capacity(self.resolution + 1);
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
let mut uvs = Vec::with_capacity(self.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 as u32) {
// 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 Meshable for CircularSegment {
type Output = CircularSegmentMeshBuilder;
fn mesh(&self) -> Self::Output {
CircularSegmentMeshBuilder {
segment: *self,
..Default::default()
}
}
}
impl From<CircularSegment> 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().build()
}
}
impl From<CircularSegmentMeshBuilder> for Mesh {
fn from(sector: CircularSegmentMeshBuilder) -> Self {
sector.build()
}
}
impl Meshable for RegularPolygon { impl Meshable for RegularPolygon {
type Output = Mesh; type Output = Mesh;

View file

@ -20,7 +20,7 @@
//! ``` //! ```
mod dim2; mod dim2;
pub use dim2::{CircleMeshBuilder, EllipseMeshBuilder}; pub use dim2::*;
mod dim3; mod dim3;
pub use dim3::*; pub use dim3::*;

View file

@ -12,7 +12,7 @@ fn main() {
.run(); .run();
} }
const X_EXTENT: f32 = 600.; const X_EXTENT: f32 = 800.;
fn setup( fn setup(
mut commands: Commands, mut commands: Commands,
@ -23,6 +23,8 @@ fn setup(
let shapes = [ let shapes = [
Mesh2dHandle(meshes.add(Circle { radius: 50.0 })), Mesh2dHandle(meshes.add(Circle { radius: 50.0 })),
Mesh2dHandle(meshes.add(CircularSector::new(50.0, 1.0))),
Mesh2dHandle(meshes.add(CircularSegment::new(50.0, 1.25))),
Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))), Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))),
Mesh2dHandle(meshes.add(Annulus::new(25.0, 50.0))), Mesh2dHandle(meshes.add(Annulus::new(25.0, 50.0))),
Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))), Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))),

124
examples/2d/mesh2d_arcs.rs Normal file
View file

@ -0,0 +1,124 @@
//! Demonstrates UV mappings of the [`CircularSector`] and [`CircularSegment`] primitives.
//!
//! Also draws the bounding boxes and circles of the primitives.
use std::f32::consts::FRAC_PI_2;
use bevy::{
color::palettes::css::{BLUE, DARK_SLATE_GREY, RED},
math::bounding::{Bounded2d, BoundingVolume},
prelude::*,
render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder},
sprite::MaterialMesh2dBundle,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(
Update,
(
draw_bounds::<CircularSector>,
draw_bounds::<CircularSegment>,
),
)
.run();
}
#[derive(Component, Debug)]
struct DrawBounds<Shape: Bounded2d + Send + Sync + 'static>(Shape);
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let material = materials.add(asset_server.load("branding/icon.png"));
commands.spawn(Camera2dBundle {
camera: Camera {
clear_color: ClearColorConfig::Custom(DARK_SLATE_GREY.into()),
..default()
},
..default()
});
const UPPER_Y: f32 = 50.0;
const LOWER_Y: f32 = -50.0;
const FIRST_X: f32 = -450.0;
const OFFSET: f32 = 100.0;
const NUM_SLICES: i32 = 8;
// This draws NUM_SLICES copies of the Bevy logo as circular sectors and segments,
// with successively larger angles up to a complete circle.
for i in 0..NUM_SLICES {
let fraction = (i + 1) as f32 / NUM_SLICES as f32;
let sector = CircularSector::from_turns(40.0, fraction);
// We want to rotate the circular sector so that the sectors appear clockwise from north.
// We must rotate it both in the Transform and in the mesh's UV mappings.
let sector_angle = -sector.half_angle();
let sector_mesh =
CircularSectorMeshBuilder::new(sector).uv_mode(CircularMeshUvMode::Mask {
angle: sector_angle,
});
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(sector_mesh).into(),
material: material.clone(),
transform: Transform {
translation: Vec3::new(FIRST_X + OFFSET * i as f32, 2.0 * UPPER_Y, 0.0),
rotation: Quat::from_rotation_z(sector_angle),
..default()
},
..default()
},
DrawBounds(sector),
));
let segment = CircularSegment::from_turns(40.0, fraction);
// For the circular segment, we will draw Bevy charging forward, which requires rotating the
// shape and texture by 90 degrees.
//
// Note that this may be unintuitive; it may feel like we should rotate the texture by the
// opposite angle to preserve the orientation of Bevy. But the angle is not the angle of the
// texture itself, rather it is the angle at which the vertices are mapped onto the texture.
// so it is the negative of what you might otherwise expect.
let segment_angle = -FRAC_PI_2;
let segment_mesh =
CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask {
angle: -segment_angle,
});
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(segment_mesh).into(),
material: material.clone(),
transform: Transform {
translation: Vec3::new(FIRST_X + OFFSET * i as f32, LOWER_Y, 0.0),
rotation: Quat::from_rotation_z(segment_angle),
..default()
},
..default()
},
DrawBounds(segment),
));
}
}
fn draw_bounds<Shape: Bounded2d + Send + Sync + 'static>(
q: Query<(&DrawBounds<Shape>, &GlobalTransform)>,
mut gizmos: Gizmos,
) {
for (shape, transform) in &q {
let (_, rotation, translation) = transform.to_scale_rotation_translation();
let translation = translation.truncate();
let rotation = rotation.to_euler(EulerRot::XYZ).2;
let aabb = shape.0.aabb_2d(translation, rotation);
gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, RED);
let bounding_circle = shape.0.bounding_circle(translation, rotation);
gizmos.circle_2d(bounding_circle.center, bounding_circle.radius(), BLUE);
}
}

View file

@ -102,6 +102,7 @@ Example | Description
[2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons [2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons
[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method [2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method
[2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes [2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes
[Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives
[Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute
[Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis
[Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh