diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index cbd1aae2cdc7ab..cf5d06e1e7e23f 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -36,13 +36,13 @@ impl Bounded2d for Arc { self.radius * -Vec2::X, self.radius * -Vec2::Y, ]; - if self.angle.is_sign_negative() { + if self.half_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; + let quarter_turns = f32::floor(self.angle().abs() / (PI / 2.0)).min(3.0) as usize; Aabb2d::from_point_cloud( translation, rotation, @@ -59,10 +59,8 @@ impl Bounded2d for Arc { } 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()) + let center = self.chord_midpoint().rotate(Vec2::from_angle(rotation)); + BoundingCircle::new(center + translation, self.half_chord_len()) } } } @@ -75,19 +73,19 @@ impl Bounded2d for CircularSector { // 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, + self.arc.end(), + self.arc.radius * Vec2::X, + self.arc.radius * Vec2::Y, + self.arc.radius * -Vec2::X, + self.arc.radius * -Vec2::Y, ]; - if self.angle.is_sign_negative() { + if self.arc.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; + let quarter_turns = f32::floor(self.arc.angle().abs() / (PI / 2.0)).min(3.0) as usize; Aabb2d::from_point_cloud( translation, rotation, @@ -97,22 +95,22 @@ impl Bounded2d for CircularSector { fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { // There are three possibilities for the bounding circle. - if self.arc().is_major() { + 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 { + BoundingCircle::new(translation, self.arc.radius) + } else if self.arc.chord_len() < self.arc.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) + let half_radius = self.arc.radius / 2.0; + let angle = Vec2::from_angle(self.arc.half_angle + rotation); + let center = half_radius * angle; + BoundingCircle::new(center + translation, half_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 = Vec2::from_angle(self.angle / 2.0 + rotation); - let center = angle * self.arc().chord_midpoint_radius(); - BoundingCircle::new(center, self.arc().half_chord_length()) + let center = self.arc.chord_midpoint().rotate(Vec2::from_angle(rotation)); + BoundingCircle::new(center + translation, self.arc.half_chord_len()) } } } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 1b28bc8f2faeda..bbcc5269d06c39 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -191,8 +191,8 @@ impl Circle { pub struct Arc { /// The radius of the circle pub radius: f32, - /// The angle swept out by the arc. - pub angle: f32, + /// Half the angle swept out by the arc. + pub half_angle: f32, } impl Primitive2d for Arc {} @@ -201,7 +201,7 @@ impl Default for Arc { fn default() -> Self { Self { radius: 0.5, - angle: 1.0, + half_angle: 0.5, } } } @@ -209,14 +209,23 @@ impl Default for Arc { 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 } + pub fn new(radius: f32, angle: f32) -> Self { + Self { + radius, + half_angle: angle / 2.0, + } + } + + /// Get the angle of the arc + #[inline(always)] + pub fn angle(&self) -> f32 { + self.half_angle * 2.0 } /// Get the length of the arc #[inline(always)] pub fn length(&self) -> f32 { - self.angle * self.radius + self.angle() * self.radius } /// Get the start point of the arc @@ -228,7 +237,7 @@ impl Arc { /// Get the end point of the arc #[inline(always)] pub fn end(&self) -> Vec2 { - self.radius * Vec2::from_angle(self.angle) + self.radius * Vec2::from_angle(self.angle()) } /// Get the endpoints of the arc @@ -237,34 +246,57 @@ impl Arc { [self.start(), self.end()] } + /// Get the midpoint of the arc + #[inline] + pub fn midpoint(&self) -> Vec2 { + self.radius * Vec2::from_angle(self.half_angle) + } + /// 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) + pub fn half_chord_len(&self) -> f32 { + self.radius * f32::sin(self.half_angle) } /// Get the length of the chord subtended by the arc #[inline(always)] - pub fn chord_length(&self) -> f32 { - 2.0 * self.half_chord_length() + pub fn chord_len(&self) -> f32 { + 2.0 * self.half_chord_len() } - /// Get the distance from the center of the circle to the midpoint of the chord. + /// Get 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)) + pub fn chord_midpoint(&self) -> Vec2 { + self.apothem_len() * Vec2::from_angle(self.half_angle) } - /// Get the midpoint of the chord + /// Get the length of the apothem of this arc, that is, + /// the distance from the center of the circle to the midpoint of the chord. + /// Equivalently, the height of the triangle whose base is the chord and whose apex is the center of the circle. #[inline(always)] - pub fn chord_midpoint(&self) -> Vec2 { - Vec2::new(self.chord_midpoint_radius(), 0.0).rotate(Vec2::from_angle(self.angle)) + pub fn apothem_len(&self) -> f32 { + f32::sqrt(self.radius.powi(2) - self.half_chord_len().powi(2)) + } + + /// Get the legnth of the sagitta of this arc, that is, + /// the length of the line between the midpoints of the arc and its chord. + /// Equivalently, the height of the triangle whose base is the chord and whose apex is the midpoint of the arc. + /// + /// If the arc is minor, i.e. less than half the circle, the this will be the difference of the [radius](Self::radius) and the [apothem](Self::apothem). + /// If it is [major](Self::major), it will be their sum. + #[inline(always)] + pub fn sagitta_len(&self) -> f32 { + if self.is_major() { + self.radius + self.apothem_len() + } else { + self.radius - self.apothem_len() + } } /// Produces true if the arc is at least half a circle. #[inline(always)] pub fn is_major(&self) -> bool { - self.angle >= PI + self.angle() >= PI } } @@ -276,40 +308,80 @@ impl Arc { #[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, + /// The arc from which this sector is contructed. + #[cfg_attr(feature = "seriealize", serde(flatten))] + pub arc: Arc, } 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, - } + Arc::default().into() + } +} + +impl From for CircularSector { + fn from(arc: Arc) -> Self { + Self { arc } } } impl CircularSector { - /// Create a new [`CircularSector`] from a `radius`, and an `angle` + /// Create a new [CircularSector] from a `radius`, and an `angle` #[inline(always)] - pub const fn new(radius: f32, angle: f32) -> Self { - Self { radius, angle } + pub fn new(radius: f32, angle: f32) -> Self { + Arc::new(radius, angle).into() } - /// Produces the arc of this sector + /// Returns the area of this sector #[inline(always)] - pub fn arc(&self) -> Arc { - Arc::new(self.radius, self.angle) + pub fn area(&self) -> f32 { + self.arc.radius.powi(2) * self.arc.half_angle } +} - /// Returns the area of this sector +/// A primitive representing a circular segment: +/// the area enclosed by the arc of a circle and its chord (the line between its endpoints). +/// +/// The segment is drawn starting from [Vec2::X], going counterclockwise. +/// To orient the segment differently, apply a rotation. +/// The segment is drawn with the center of its circle at the origin (0, 0). +/// When positioning the segment, the [apothem_len](Self::apothem) and [sagitta_len](Sagitta) functions +/// may be particularly useful. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct CircularSegment { + /// The arc from which this segment is contructed. + #[cfg_attr(feature = "seriealize", serde(flatten))] + pub arc: Arc, +} +impl Primitive2d for CircularSegment {} + +impl Default for CircularSegment { + // Returns the default [CircularSegment] with radius `0.5` and angle `1.0`. + fn default() -> Self { + Arc::default().into() + } +} + +impl From for CircularSegment { + fn from(arc: Arc) -> Self { + Self { arc } + } +} + +impl CircularSegment { + /// Create a new [CircularSegment] from a `radius`, and an `angle` + #[inline(always)] + pub fn new(radius: f32, angle: f32) -> Self { + Arc::new(radius, angle).into() + } + + /// Returns the area of this segment #[inline(always)] pub fn area(&self) -> f32 { - self.radius.powi(2) * self.angle / 2.0 + self.arc.radius.powi(2) * (self.arc.half_angle - self.arc.angle().sin()) } } diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 4f9db9b5eaa66c..5ff102c140901e 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -6,8 +6,8 @@ use crate::{ use super::Meshable; use bevy_math::{ primitives::{ - Capsule2d, Circle, CircularSector, Ellipse, Rectangle, RegularPolygon, Triangle2d, - WindingOrder, + Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle, RegularPolygon, + Triangle2d, WindingOrder, }, FloatExt, Vec2, }; @@ -103,9 +103,9 @@ impl Default for CircularSectorMeshBuilder { 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 { + pub fn new(radius: f32, angle: f32, resolution: usize) -> Self { Self { - sector: CircularSector { radius, angle }, + sector: CircularSector::new(radius, angle), resolution, } } @@ -132,11 +132,11 @@ impl CircularSectorMeshBuilder { 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)); + let angle = Vec2::from_angle(f32::lerp(0.0, self.sector.arc.angle(), i as f32 / last)); positions.push([ - angle.x * self.sector.radius, - angle.y * self.sector.radius, + angle.x * self.sector.arc.radius, + angle.y * self.sector.arc.radius, 0.0, ]); uvs.push([0.5 * (angle.x + 1.0), 1.0 - 0.5 * (angle.y + 1.0)]); @@ -181,6 +181,108 @@ impl From for Mesh { } } +/// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. +#[derive(Clone, Copy, Debug)] +pub struct CircularSegmentMeshBuilder { + /// The [`segment`] shape. + pub segment: CircularSegment, + /// The number of vertices used for the arc portion of the segment mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: usize, +} + +impl Default for CircularSegmentMeshBuilder { + fn default() -> Self { + Self { + segment: CircularSegment::default(), + resolution: 32, + } + } +} + +impl CircularSegmentMeshBuilder { + /// Creates a new [`CircularSegmentMeshBuilder`] from a given radius, angle, and vertex count. + #[inline] + pub fn new(radius: f32, angle: f32, resolution: usize) -> Self { + Self { + segment: CircularSegment::new(radius, angle), + resolution, + } + } + + /// Sets the number of vertices used for the segment 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 chord. + let chord_midpoint = self.segment.arc.chord_midpoint(); + positions.push([chord_midpoint.x, chord_midpoint.y, 0.0]); + uvs.push([0.5 + chord_midpoint.x * 0.5, 0.5 + chord_midpoint.y * 0.5]); + + 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.segment.arc.angle(), i as f32 / last)); + + positions.push([ + angle.x * self.segment.arc.radius, + angle.y * self.segment.arc.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 CircularSegment { + type Output = CircularSegmentMeshBuilder; + + fn mesh(&self) -> Self::Output { + CircularSegmentMeshBuilder { + segment: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(segment: CircularSegment) -> Self { + segment.mesh().build() + } +} + +impl From for Mesh { + fn from(sector: CircularSegmentMeshBuilder) -> Self { + sector.build() + } +} + impl Meshable for RegularPolygon { type Output = Mesh; diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 4e717ac194b2e3..914182db35f363 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -22,17 +22,17 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Circle { radius: 50.0 }).into(), material: materials.add(Color::VIOLET), - transform: Transform::from_translation(Vec3::new(-350.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-400.0, 0.0, 0.0)), ..default() }); // Circular sector let sector = CircularSector::new(50.0, 5.0); - let mut sector_transform = Transform::from_translation(Vec3::new(-225.0, 0.0, 0.0)); + let mut sector_transform = Transform::from_translation(Vec3::new(-275.0, 0.0, 0.0)); // A sector is drawn counterclockwise from the right. // To make it face left, rotate by negative half its angle. // To make it face right, rotate by an additional PI radians. - sector_transform.rotate_z(-sector.arc().angle / 2.0 + PI); + sector_transform.rotate_z(-sector.arc.half_angle + PI); commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(sector).into(), material: materials.add(Color::YELLOW), @@ -40,21 +40,37 @@ fn setup( ..default() }); - // Another, smaller circle for some reason, positioned relative to the previous sector. + // Another, smaller circle for some reason commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Circle { radius: 7.0 }).into(), material: materials.add(Color::WHITE), transform: Transform::from_translation( - sector_transform.translation + Vec3::new(45.0, 0.0, 0.0), + sector_transform.translation + Vec3::new(-45.0, 0.0, 0.0), ), ..default() }); + // Circular segment + let segment = CircularSegment::new(50.0, 2.5); + let mut segment_transform = Transform::from_translation(Vec3::new(-150.0, 0.0, 0.0)); + // A segment is drawn counterclockwise from the right. + // To make it symmetrical about the X axis, rotate by negative half its angle. + // To make it symmetrical about the Y axis, rotate by an additional PI/2 radians. + segment_transform.rotate_z(-segment.arc.half_angle + PI / 2.0); + // The segment is drawn with the center as the center of the circle. + // To move the midpoint of the chord to the origin, we must use an additional translation. + commands.spawn(MaterialMesh2dBundle { + mesh: meshes.add(segment).into(), + material: materials.add(Color::MIDNIGHT_BLUE), + transform: segment_transform, + ..default() + }); + // Ellipse commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Ellipse::new(25.0, 50.0)).into(), material: materials.add(Color::TURQUOISE), - transform: Transform::from_translation(Vec3::new(-100.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-50.0, 0.0, 0.0)), ..default() }); @@ -62,7 +78,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Capsule2d::new(25.0, 50.0)).into(), material: materials.add(Color::LIME_GREEN), - transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(50.0, 0.0, 0.0)), ..default() }); @@ -70,7 +86,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Rectangle::new(50.0, 100.0)).into(), material: materials.add(Color::YELLOW), - transform: Transform::from_translation(Vec3::new(100.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(150.0, 0.0, 0.0)), ..default() }); @@ -78,7 +94,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(RegularPolygon::new(50.0, 6)).into(), material: materials.add(Color::ORANGE), - transform: Transform::from_translation(Vec3::new(225.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(275.0, 0.0, 0.0)), ..default() }); @@ -92,7 +108,7 @@ fn setup( )) .into(), material: materials.add(Color::ORANGE_RED), - transform: Transform::from_translation(Vec3::new(350.0, 0.0, 0.0)), + transform: Transform::from_translation(Vec3::new(400.0, 0.0, 0.0)), ..default() }); }