This commit is contained in:
Richard Jones 2025-07-16 23:50:37 +02:00 committed by GitHub
commit 9d9c7ef36a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 314 additions and 8 deletions

View File

@ -1024,6 +1024,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.

View File

@ -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,
};
@ -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, Clone)]

View File

@ -30,7 +30,7 @@ fn main() {
app.run();
}
const X_EXTENT: f32 = 900.;
const X_EXTENT: f32 = 600.;
fn setup(
mut commands: Commands,
@ -45,6 +45,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)),
@ -56,18 +57,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,
),
));

View File

@ -80,6 +80,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.)),