Merge 2cc1d284db
into f964ee1e3a
This commit is contained in:
commit
9d9c7ef36a
@ -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.
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
),
|
||||
));
|
||||
|
@ -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.)),
|
||||
|
Loading…
Reference in New Issue
Block a user