Skip to content

Commit

Permalink
Add Arc and CircularSector math primitives.
Browse files Browse the repository at this point in the history
  • Loading branch information
spectria-limina committed Feb 7, 2024
1 parent c33b8b9 commit 1dae464
Show file tree
Hide file tree
Showing 4 changed files with 375 additions and 10 deletions.
102 changes: 100 additions & 2 deletions crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).

use std::f32::consts::PI;

use glam::{Mat2, Vec2};

use crate::primitives::{
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d,
Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
Arc, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Direction2d, Ellipse,
Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
};

use super::{Aabb2d, Bounded2d, BoundingCircle};
Expand All @@ -19,6 +21,102 @@ impl Bounded2d for Circle {
}
}

impl Bounded2d for Arc {
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
// For a sufficiently wide arc, the bounding points in a given direction will be the outer
// limits of a circle centered at the origin.
// For smaller arcs, the two endpoints of the arc could also be bounding points,
// but the start point is always axis-aligned so it's included as one of the circular limits.
// This gives five possible bounding points, so we will lay them out in an array and then
// select the appropriate slice to compute the bounding box of.
let mut circle_bounds = [
self.end(),
self.radius * Vec2::X,
self.radius * Vec2::Y,
self.radius * -Vec2::X,
self.radius * -Vec2::Y,
];
if self.angle.is_sign_negative() {
// If we have a negative angle, we are going the opposite direction, so negate the Y-axis points.
circle_bounds[2] = -circle_bounds[2];
circle_bounds[4] = -circle_bounds[4];
}
// The number of quarter turns tells us how many extra points to include, between 0 and 3.
let quarter_turns = f32::floor(self.angle.abs() / (PI / 2.0)).min(3.0) as usize;
Aabb2d::from_point_cloud(
translation,
rotation,
&circle_bounds[0..(2 + quarter_turns)],
)
}

fn bounding_circle(&self, translation: Vec2, rotation: f32) -> 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 angle = self.angle + rotation;
let center =
Vec2::new(self.chord_midpoint_radius(), 0.0).rotate(Vec2::from_angle(angle));
BoundingCircle::new(center, self.half_chord_length())
}
}
}

impl Bounded2d for CircularSector {
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
// This is identical to the implementation for Arc, above, with the additional possibility of the
// origin point, the center of the arc, acting as a bounding point.
//
// See comments above for discussion.
let mut circle_bounds = [
Vec2::ZERO,
self.arc().end(),
self.radius * Vec2::X,
self.radius * Vec2::Y,
self.radius * -Vec2::X,
self.radius * -Vec2::Y,
];
if self.angle.is_sign_negative() {
// If we have a negative angle, we are going the opposite direction, so negate the Y-axis points.
circle_bounds[3] = -circle_bounds[3];
circle_bounds[5] = -circle_bounds[5];
}
// The number of quarter turns tells us how many extra points to include, between 0 and 3.
let quarter_turns = f32::floor(self.angle.abs() / (PI / 2.0)).min(3.0) as usize;
Aabb2d::from_point_cloud(
translation,
rotation,
&circle_bounds[0..(3 + quarter_turns)],
)
}

fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
// There are three possibilities for the bounding circle.
if self.arc().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 if self.arc().chord_length() < self.radius {
// If the chord length is smaller than the radius, then the radius is the widest distance between two points,
// so the radius is the diameter of the bounding circle.
let angle = Vec2::from_angle(self.angle / 2.0 + rotation);
let center = angle * self.radius / 2.0;
BoundingCircle::new(center, self.radius / 2.0)
} 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 angle = Vec2::from_angle(self.angle / 2.0 + rotation);
let center = angle * self.arc().chord_midpoint_radius();
BoundingCircle::new(center, self.arc().half_chord_length())
}
}
}

impl Bounded2d for Ellipse {
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
// V = (hh * cos(beta), hh * sin(beta))
Expand Down
137 changes: 137 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,143 @@ impl Circle {
}
}

/// A primitive representing an arc: a segment of 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 [CircularSector].
///
/// The arc is drawn starting from [Vec2::X], going counterclockwise.
/// To orient the arc differently, apply a rotation.
/// The arc is drawn with the center of its circle at the origin (0, 0),
/// meaning that the center may not be inside its convex hull.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Arc {
/// The radius of the circle
pub radius: f32,
/// The angle swept out by the arc.
pub angle: f32,
}
impl Primitive2d for Arc {}

impl Default for Arc {
// Returns the default [`Arc`] with radius `0.5` and angle `1.0`.
fn default() -> Self {
Self {
radius: 0.5,
angle: 1.0,
}
}
}

impl Arc {
/// Create a new [`Arc`] from a `radius`, and an `angle`
#[inline(always)]
pub const fn new(radius: f32, angle: f32) -> Self {
Self { radius, angle }
}

/// Get the length of the arc
#[inline(always)]
pub fn length(&self) -> f32 {
self.angle * self.radius
}

/// Get the start point of the arc
#[inline(always)]
pub fn start(&self) -> Vec2 {
Vec2::new(self.radius, 0.0)
}

/// Get the end point of the arc
#[inline(always)]
pub fn end(&self) -> Vec2 {
self.radius * Vec2::from_angle(self.angle)
}

/// Get the endpoints of the arc
#[inline(always)]
pub fn endpoints(&self) -> [Vec2; 2] {
[self.start(), self.end()]
}

/// Get half the length of the chord subtended by the arc
#[inline(always)]
pub fn half_chord_length(&self) -> f32 {
self.radius * f32::sin(self.angle / 2.0)
}

/// Get the length of the chord subtended by the arc
#[inline(always)]
pub fn chord_length(&self) -> f32 {
2.0 * self.half_chord_length()
}

/// Get the distance from the center of the circle to the midpoint of the chord.
#[inline(always)]
pub fn chord_midpoint_radius(&self) -> f32 {
f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2))
}

/// Get the midpoint of the chord
#[inline(always)]
pub fn chord_midpoint(&self) -> Vec2 {
Vec2::new(self.chord_midpoint_radius(), 0.0).rotate(Vec2::from_angle(self.angle))
}

/// Produces true if the arc is at least half a circle.
#[inline(always)]
pub fn is_major(&self) -> bool {
self.angle >= PI
}
}

/// A primitive representing a circular sector: a pie slice of a circle.
///
/// The sector is drawn starting from [Vec2::X], going counterclockwise.
/// To orient the sector differently, apply a rotation.
/// The sector is drawn with the center of its circle at the origin (0, 0).
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct CircularSector {
/// The radius of the circle
pub radius: f32,
/// The angle swept out by the sector.
pub angle: f32,
}
impl Primitive2d for CircularSector {}

impl Default for CircularSector {
// Returns the default [`CircularSector`] with radius `0.5` and angle `1.0`.
fn default() -> Self {
Self {
radius: 0.5,
angle: 1.0,
}
}
}

impl CircularSector {
/// Create a new [`CircularSector`] from a `radius`, and an `angle`
#[inline(always)]
pub const fn new(radius: f32, angle: f32) -> Self {
Self { radius, angle }
}

/// Produces the arc of this sector
#[inline(always)]
pub fn arc(&self) -> Arc {
Arc::new(self.radius, self.angle)
}

/// Returns the area of this sector
#[inline(always)]
pub fn area(&self) -> f32 {
self.radius.powi(2) * self.angle / 2.0
}
}

/// An ellipse primitive
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
Expand Down
108 changes: 106 additions & 2 deletions crates/bevy_render/src/mesh/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use crate::{

use super::Meshable;
use bevy_math::{
primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder},
Vec2,
primitives::{
Capsule2d, Circle, CircularSector, Ellipse, Rectangle, RegularPolygon, Triangle2d,
WindingOrder,
},
FloatExt, Vec2,
};
use wgpu::PrimitiveTopology;

Expand Down Expand Up @@ -77,6 +80,107 @@ impl From<CircleMeshBuilder> for Mesh {
}
}

/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape.
#[derive(Clone, Copy, 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,
}

impl Default for CircularSectorMeshBuilder {
fn default() -> Self {
Self {
sector: CircularSector::default(),
resolution: 32,
}
}
}

impl CircularSectorMeshBuilder {
/// Creates a new [`CircularSectorMeshBuilder`] from a given radius, angle, and vertex count.
#[inline]
pub const fn new(radius: f32, angle: f32, resolution: usize) -> Self {
Self {
sector: CircularSector { radius, angle },
resolution,
}
}

/// 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
}

/// 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);

// Push the center of the circle.
positions.push([0.0; 3]);
uvs.push([0.5; 2]);

let last = (self.resolution - 1) as f32;
for i in 0..self.resolution {
// Compute vertex position at angle theta
let angle = Vec2::from_angle(f32::lerp(0.0, self.sector.angle, i as f32 / last));

positions.push([
angle.x * self.sector.radius,
angle.y * self.sector.radius,
0.0,
]);
uvs.push([0.5 * (angle.x + 1.0), 1.0 - 0.5 * (angle.y + 1.0)]);
}

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_indices(Some(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 {
fn from(sector: CircularSector) -> Self {
sector.mesh().build()
}
}

impl From<CircularSectorMeshBuilder> for Mesh {
fn from(sector: CircularSectorMeshBuilder) -> Self {
sector.build()
}
}

impl Meshable for RegularPolygon {
type Output = Mesh;

Expand Down
Loading

0 comments on commit 1dae464

Please sign in to comment.