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 rhombus primitive, also known as a diamond shape.
|
||||||
/// A four sided polygon, centered on the origin, where opposite sides are parallel but without
|
/// A four sided polygon, centered on the origin, where opposite sides are parallel but without
|
||||||
/// requiring right angles.
|
/// requiring right angles.
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
use core::f32::consts::FRAC_PI_2;
|
|
||||||
|
|
||||||
use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment};
|
use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment};
|
||||||
use bevy_asset::RenderAssetUsages;
|
use bevy_asset::RenderAssetUsages;
|
||||||
|
use core::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
use super::{Extrudable, MeshBuilder, Meshable};
|
use super::{Extrudable, MeshBuilder, Meshable};
|
||||||
use bevy_math::{
|
use bevy_math::{
|
||||||
ops,
|
ops,
|
||||||
primitives::{
|
primitives::{
|
||||||
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse,
|
AnnularSector, Annulus, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon,
|
||||||
Rectangle, RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
|
Ellipse, Rectangle, RegularPolygon, Rhombus, Triangle2d, Triangle3d, WindingOrder,
|
||||||
},
|
},
|
||||||
FloatExt, Vec2,
|
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.
|
/// A builder for creating a [`Mesh`] with an [`Rhombus`] shape.
|
||||||
#[derive(Clone, Copy, Debug, Reflect)]
|
#[derive(Clone, Copy, Debug, Reflect)]
|
||||||
#[reflect(Default, Debug, Clone)]
|
#[reflect(Default, Debug, Clone)]
|
||||||
|
@ -30,7 +30,7 @@ fn main() {
|
|||||||
app.run();
|
app.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const X_EXTENT: f32 = 900.;
|
const X_EXTENT: f32 = 600.;
|
||||||
|
|
||||||
fn setup(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
@ -45,6 +45,7 @@ fn setup(
|
|||||||
meshes.add(CircularSegment::new(50.0, 1.25)),
|
meshes.add(CircularSegment::new(50.0, 1.25)),
|
||||||
meshes.add(Ellipse::new(25.0, 50.0)),
|
meshes.add(Ellipse::new(25.0, 50.0)),
|
||||||
meshes.add(Annulus::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(Capsule2d::new(25.0, 50.0)),
|
||||||
meshes.add(Rhombus::new(75.0, 100.0)),
|
meshes.add(Rhombus::new(75.0, 100.0)),
|
||||||
meshes.add(Rectangle::new(50.0, 100.0)),
|
meshes.add(Rectangle::new(50.0, 100.0)),
|
||||||
@ -56,18 +57,23 @@ fn setup(
|
|||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
let num_shapes = shapes.len();
|
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() {
|
for (i, shape) in shapes.into_iter().enumerate() {
|
||||||
// Distribute colors evenly across the rainbow.
|
// Distribute colors evenly across the rainbow.
|
||||||
let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
|
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((
|
commands.spawn((
|
||||||
Mesh2d(shape),
|
Mesh2d(shape),
|
||||||
MeshMaterial2d(materials.add(color)),
|
MeshMaterial2d(materials.add(color)),
|
||||||
Transform::from_xyz(
|
Transform::from_xyz(
|
||||||
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
|
// Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2 in each row.
|
||||||
-X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
|
-X_EXTENT / 2. + col as f32 / (shapes_per_row - 1) as f32 * X_EXTENT,
|
||||||
0.0,
|
// Position the rows 120 units apart vertically.
|
||||||
|
60.0 - row as f32 * 120.0,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
@ -80,6 +80,7 @@ fn setup(
|
|||||||
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
|
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
|
||||||
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
|
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
|
||||||
meshes.add(Extrusion::new(Annulus::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(Circle::default(), 1.)),
|
||||||
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
|
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
|
||||||
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
|
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
|
||||||
|
Loading…
Reference in New Issue
Block a user