Skip to content

Add AnnularSector primitive shape #17928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,116 @@ impl Measured2d for Annulus {
}
}

/// A sector of an [`Annulus`] defined by a half angle.
/// The sector middle is at `Vec2::Y`, extending by `half_angle` radians on either side.
#[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)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct AnnularSector {
/// The annulus that the sector is taken from.
pub annulus: Annulus,
/// The half angle of the sector.
pub half_angle: f32,
}
impl Primitive2d for AnnularSector {}

impl Default for AnnularSector {
/// Returns a sector of the default [`Annulus`] with a half angle of 1.0.
fn default() -> Self {
Self {
annulus: Annulus::default(),
half_angle: 1.0,
}
}
}

impl AnnularSector {
/// Create a new [`AnnularSector`] from the radii of the inner and outer circle, and the half angle.
/// It is created starting from `Vec2::Y`, extending by `half_angle` radians on either side.
#[inline(always)]
pub const fn new(inner_radius: f32, outer_radius: f32, half_angle: f32) -> Self {
Self {
annulus: Annulus::new(inner_radius, outer_radius),
half_angle,
}
}

/// Get the diameter of the outer circle.
#[inline(always)]
pub fn diameter(&self) -> f32 {
self.annulus.diameter()
}

/// Get the thickness of the annulus.
#[inline(always)]
pub fn thickness(&self) -> f32 {
self.annulus.thickness()
}

/// If `point` lies inside the annular sector, it is returned as-is.
/// Otherwise the closest point on the perimeter of the shape is returned.
#[inline(always)]
pub fn closest_point(&self, point: Vec2) -> Vec2 {
let distance_squared = point.length_squared();
let angle = ops::abs(point.angle_to(Vec2::Y));

if angle > self.half_angle {
// Project the point onto the nearest boundary of the sector
let clamped_angle = ops::copysign(self.half_angle, point.x);
let dir_to_point = Vec2::from_angle(clamped_angle);
if distance_squared > self.annulus.outer_circle.radius.squared() {
return self.annulus.outer_circle.radius * dir_to_point;
} else if distance_squared < self.annulus.inner_circle.radius.squared() {
return self.annulus.inner_circle.radius * dir_to_point;
}
return point;
}

if self.annulus.inner_circle.radius.squared() <= distance_squared {
if distance_squared <= self.annulus.outer_circle.radius.squared() {
// The point is inside the annular sector.
point
} else {
// The point is outside the annular sector and closer to the outer perimeter.
let dir_to_point = point / ops::sqrt(distance_squared);
self.annulus.outer_circle.radius * dir_to_point
}
} else {
// The point is outside the annular sector and closer to the inner perimeter.
let dir_to_point = point / ops::sqrt(distance_squared);
self.annulus.inner_circle.radius * dir_to_point
}
}
}

impl Measured2d for AnnularSector {
/// Get the area of the annular sector.
#[inline(always)]
fn area(&self) -> f32 {
self.half_angle
* (self.annulus.outer_circle.radius.squared()
- self.annulus.inner_circle.radius.squared())
}

/// Get the perimeter or circumference of the annular sector.
#[inline(always)]
#[doc(alias = "circumference")]
fn perimeter(&self) -> f32 {
let arc_length_outer = 2.0 * self.half_angle * self.annulus.outer_circle.radius;
let arc_length_inner = 2.0 * self.half_angle * self.annulus.inner_circle.radius;
let radial_edges = 2.0 * self.annulus.thickness();
arc_length_outer + arc_length_inner + radial_edges
}
}

/// 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.
Expand Down
197 changes: 193 additions & 4 deletions crates/bevy_mesh/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use core::f32::consts::FRAC_PI_2;

use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment};
use bevy_asset::RenderAssetUsages;
use core::f32::consts::FRAC_PI_2;

use super::{Extrudable, MeshBuilder, Meshable};
use bevy_math::{
ops,
primitives::{
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse,
Rectangle, RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
AnnularSector, Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon,
Ellipse, Rectangle, RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
},
FloatExt, Vec2,
};
Expand Down Expand Up @@ -769,6 +768,196 @@ impl From<Annulus> for Mesh {
}
}

/// Specifies how to generate UV-mappings for the [`AnnularSector`] shape
/// The u-coord is always radial, and by default the v-coord goes from 0-1 over the extent of the sector.
#[derive(Copy, Clone, Default, Debug, PartialEq)]
#[non_exhaustive]
pub enum AnnularSectorMeshUvMode {
/// Scales the uv's v-coord so that the annular sector always maps to a range of 0-1, and thus
/// the v-extent of the sector will always be 1.
#[default]
ArcExtent,
/// Scales the uv's v-coord so that a full circle always maps to a range of 0-1, and thus
/// an annular sector v-coord will be a fraction of that depending on the sector's angular extent.
CircularExtent,
}

/// A builder for creating a [`Mesh`] with an [`AnnularSector`] shape.
pub struct AnnularSectorMeshBuilder {
/// The [`AnnularSector`] shape.
pub annular_sector: AnnularSector,
/// The number of vertices used in constructing each concentric circle of the annulus mesh.
/// The default is `32`.
pub resolution: u32,
/// The uv mapping mode
pub uv_mode: AnnularSectorMeshUvMode,
}

impl AnnularSectorMeshBuilder {
/// Create an [`AnnularSectorMeshBuilder`] with the given inner radius, outer radius, half angle, and angular vertex count.
#[inline]
pub fn new(inner_radius: f32, outer_radius: f32, half_angle: f32) -> Self {
Self {
annular_sector: AnnularSector::new(inner_radius, outer_radius, half_angle),
resolution: 32,
uv_mode: AnnularSectorMeshUvMode::default(),
}
}
/// Sets the uv mode used for the mesh
#[inline]
pub fn uv_mode(mut self, uv_mode: AnnularSectorMeshUvMode) -> Self {
self.uv_mode = uv_mode;
self
}
/// 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
}
/// Calculates the v-coord based on the uv mode.
fn calc_uv_v(&self, i: usize, resolution: usize, arc_extent: f32) -> f32 {
let base_v = i as f32 / resolution as f32;
match self.uv_mode {
AnnularSectorMeshUvMode::ArcExtent => base_v,
AnnularSectorMeshUvMode::CircularExtent => {
base_v * (core::f32::consts::TAU / arc_extent)
}
}
}
}

impl MeshBuilder for AnnularSectorMeshBuilder {
fn build(&self) -> Mesh {
let inner_radius = self.annular_sector.annulus.inner_circle.radius;
let outer_radius = self.annular_sector.annulus.outer_circle.radius;
let resolution = self.resolution as usize;
let mut positions = Vec::with_capacity((resolution + 1) * 2);
let mut uvs = Vec::with_capacity((resolution + 1) * 2);
let normals = vec![[0.0, 0.0, 1.0]; (resolution + 1) * 2];
let mut indices = Vec::with_capacity(resolution * 6);

// Angular range: we center around Vec2::Y (FRAC_PI_2) and extend by the half_angle on both sides.
let start_angle = FRAC_PI_2 - self.annular_sector.half_angle;
let end_angle = FRAC_PI_2 + self.annular_sector.half_angle;

let arc_extent = end_angle - start_angle;
let step = arc_extent / self.resolution as f32;

// Create vertices (each step creates an inner and an outer vertex).
for i in 0..=resolution {
// For a full circle we wrap the index to duplicate the first vertex at the end.
let theta = if self.annular_sector.half_angle == FRAC_PI_2 {
start_angle + ((i % resolution) as f32) * step
} else {
start_angle + i as f32 * step
};

let (sin, cos) = ops::sin_cos(theta);
let inner_pos = [cos * inner_radius, sin * inner_radius, 0.0];
let outer_pos = [cos * outer_radius, sin * outer_radius, 0.0];
positions.push(inner_pos);
positions.push(outer_pos);

// The first UV direction is radial and the second is angular
let v = self.calc_uv_v(i, resolution, arc_extent);
uvs.push([0.0, v]);
uvs.push([1.0, v]);
}

// 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 AnnularSectorMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
// Number of vertex pairs along each arc.
let num_pairs = (self.resolution as usize) + 1;
// Outer vertices: odd indices: 1, 3, 5, ..., (2*num_pairs - 1)
let outer_indices: Vec<u32> = (0..num_pairs).map(|i| (2 * i + 1) as u32).collect();
// Inner vertices: even indices: 0, 2, 4, ..., (2*num_pairs - 2)
let inner_indices: Vec<u32> = (0..num_pairs).map(|i| (2 * i) as u32).collect();

// Endpoint angles.
let left_angle = FRAC_PI_2 - self.annular_sector.half_angle;
let right_angle = FRAC_PI_2 + self.annular_sector.half_angle;

// Outer arc: traverse from left to right.
let (left_sin, left_cos) = ops::sin_cos(left_angle);
let (right_sin, right_cos) = ops::sin_cos(right_angle);
let outer_first_normal = Vec2::new(left_cos, left_sin);
let outer_last_normal = Vec2::new(right_cos, right_sin);
let outer_arc = PerimeterSegment::Smooth {
first_normal: outer_first_normal,
last_normal: outer_last_normal,
indices: outer_indices,
};

// Inner arc: traverse from right to left.
// Reversing the inner vertices so that when walking along the segment,
// the donut-hole is on the right.
let mut inner_indices_rev = inner_indices;
inner_indices_rev.reverse();
let (right_sin, right_cos) = ops::sin_cos(-right_angle);
let (left_sin, left_cos) = ops::sin_cos(-left_angle);
let inner_first_normal = Vec2::new(right_cos, right_sin);
let inner_last_normal = Vec2::new(left_cos, left_sin);
let inner_arc = PerimeterSegment::Smooth {
first_normal: inner_first_normal,
last_normal: inner_last_normal,
indices: inner_indices_rev,
};

// Radial segments are the flat sections connecting the inner and outer arc vertices.
let left_radial = PerimeterSegment::Flat {
indices: vec![0, 1],
};
let right_radial = PerimeterSegment::Flat {
indices: vec![(2 * self.resolution + 1), (2 * self.resolution)],
};

vec![outer_arc, inner_arc, left_radial, right_radial]
}
}

impl Meshable for AnnularSector {
type Output = AnnularSectorMeshBuilder;

fn mesh(&self) -> Self::Output {
AnnularSectorMeshBuilder {
annular_sector: *self,
resolution: 32,
uv_mode: AnnularSectorMeshUvMode::default(),
}
}
}

impl From<AnnularSector> for Mesh {
fn from(annular_sector: AnnularSector) -> Self {
annular_sector.mesh().build()
}
}

/// A builder for creating a [`Mesh`] with an [`Rhombus`] shape.
#[derive(Clone, Copy, Debug, Reflect)]
#[reflect(Default, Debug)]
Expand Down
14 changes: 10 additions & 4 deletions examples/2d/2d_shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn main() {
app.run();
}

const X_EXTENT: f32 = 900.;
const X_EXTENT: f32 = 600.;

fn setup(
mut commands: Commands,
Expand All @@ -35,6 +35,7 @@ fn setup(
meshes.add(CircularSegment::new(50.0, 1.25)),
meshes.add(Ellipse::new(25.0, 50.0)),
meshes.add(Annulus::new(25.0, 50.0)),
meshes.add(AnnularSector::new(25.0, 50.0, 1.0)),
meshes.add(Capsule2d::new(25.0, 50.0)),
meshes.add(Rhombus::new(75.0, 100.0)),
meshes.add(Rectangle::new(50.0, 100.0)),
Expand All @@ -46,18 +47,23 @@ fn setup(
)),
];
let num_shapes = shapes.len();
let shapes_per_row = (num_shapes as f32 / 2.0).ceil() as usize;

for (i, shape) in shapes.into_iter().enumerate() {
// Distribute colors evenly across the rainbow.
let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);

let row = i / shapes_per_row;
let col = i % shapes_per_row;

commands.spawn((
Mesh2d(shape),
MeshMaterial2d(materials.add(color)),
Transform::from_xyz(
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
0.0,
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2 in each row.
-X_EXTENT / 2. + col as f32 / (shapes_per_row - 1) as f32 * X_EXTENT,
// Position the rows 120 units apart vertically.
60.0 - row as f32 * 120.0,
0.0,
),
));
Expand Down
1 change: 1 addition & 0 deletions examples/3d/3d_shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fn setup(
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
meshes.add(Extrusion::new(Annulus::default(), 1.)),
meshes.add(Extrusion::new(AnnularSector::default(), 1.)),
meshes.add(Extrusion::new(Circle::default(), 1.)),
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
Expand Down