From f637de38c5826888beb779b7e556abdbc3a34ae9 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 21:00:45 -0500 Subject: [PATCH 01/19] Add `Arc` and `CircularSector` math primitives. --- .../src/bounding/bounded2d/primitive_impls.rs | 102 ++++++++++++- crates/bevy_math/src/primitives/dim2.rs | 137 ++++++++++++++++++ .../bevy_render/src/mesh/primitives/dim2.rs | 108 +++++++++++++- examples/2d/2d_shapes.rs | 55 +++++-- 4 files changed, 383 insertions(+), 19 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index ee942e52dd73e..cbd1aae2cdc7a 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -1,10 +1,12 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). +use std::f32::consts::PI; + use glam::{Mat2, Vec2}; use crate::primitives::{ - BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, - Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Direction2d, Ellipse, + Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; use super::{Aabb2d, Bounded2d, BoundingCircle}; @@ -19,6 +21,102 @@ impl Bounded2d for Circle { } } +impl Bounded2d for Arc { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // For a sufficiently wide arc, the bounding points in a given direction will be the outer + // limits of a circle centered at the origin. + // For smaller arcs, the two endpoints of the arc could also be bounding points, + // but the start point is always axis-aligned so it's included as one of the circular limits. + // This gives five possible bounding points, so we will lay them out in an array and then + // select the appropriate slice to compute the bounding box of. + let mut circle_bounds = [ + self.end(), + self.radius * Vec2::X, + self.radius * Vec2::Y, + self.radius * -Vec2::X, + self.radius * -Vec2::Y, + ]; + if self.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; + Aabb2d::from_point_cloud( + translation, + rotation, + &circle_bounds[0..(2 + quarter_turns)], + ) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + // There are two possibilities for the bounding circle. + if self.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 { + // 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()) + } + } +} + +impl Bounded2d for CircularSector { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // This is identical to the implementation for Arc, above, with the additional possibility of the + // origin point, the center of the arc, acting as a bounding point. + // + // 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, + ]; + if self.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; + Aabb2d::from_point_cloud( + translation, + rotation, + &circle_bounds[0..(3 + quarter_turns)], + ) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + // There are three possibilities for the bounding circle. + 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 { + // 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) + } 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()) + } + } +} + impl Bounded2d for Ellipse { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // V = (hh * cos(beta), hh * sin(beta)) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 2befff6808941..1b28bc8f2faed 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -176,6 +176,143 @@ impl Circle { } } +/// A primitive representing an arc: a segment of a circle. +/// +/// An arc has no area. +/// If you want to include the portion of a circle's area swept out by the arc, +/// use [CircularSector]. +/// +/// The arc is drawn starting from [Vec2::X], going counterclockwise. +/// To orient the arc differently, apply a rotation. +/// The arc is drawn with the center of its circle at the origin (0, 0), +/// meaning that the center may not be inside its convex hull. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Arc { + /// The radius of the circle + pub radius: f32, + /// The angle swept out by the arc. + pub angle: f32, +} +impl Primitive2d for Arc {} + +impl Default for Arc { + // Returns the default [`Arc`] with radius `0.5` and angle `1.0`. + fn default() -> Self { + Self { + radius: 0.5, + angle: 1.0, + } + } +} + +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 } + } + + /// Get the length of the arc + #[inline(always)] + pub fn length(&self) -> f32 { + self.angle * self.radius + } + + /// Get the start point of the arc + #[inline(always)] + pub fn start(&self) -> Vec2 { + Vec2::new(self.radius, 0.0) + } + + /// Get the end point of the arc + #[inline(always)] + pub fn end(&self) -> Vec2 { + self.radius * Vec2::from_angle(self.angle) + } + + /// Get the endpoints of the arc + #[inline(always)] + pub fn endpoints(&self) -> [Vec2; 2] { + [self.start(), self.end()] + } + + /// 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) + } + + /// Get the length of the chord subtended by the arc + #[inline(always)] + pub fn chord_length(&self) -> f32 { + 2.0 * self.half_chord_length() + } + + /// Get the distance from the center of the circle to 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)) + } + + /// Get the midpoint of the chord + #[inline(always)] + pub fn chord_midpoint(&self) -> Vec2 { + Vec2::new(self.chord_midpoint_radius(), 0.0).rotate(Vec2::from_angle(self.angle)) + } + + /// Produces true if the arc is at least half a circle. + #[inline(always)] + pub fn is_major(&self) -> bool { + self.angle >= PI + } +} + +/// A primitive representing a circular sector: a pie slice of a circle. +/// +/// The sector is drawn starting from [Vec2::X], going counterclockwise. +/// To orient the sector differently, apply a rotation. +/// The sector is drawn with the center of its circle at the origin (0, 0). +#[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, +} +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, + } + } +} + +impl CircularSector { + /// Create a new [`CircularSector`] from a `radius`, and an `angle` + #[inline(always)] + pub const fn new(radius: f32, angle: f32) -> Self { + Self { radius, angle } + } + + /// Produces the arc of this sector + #[inline(always)] + pub fn arc(&self) -> Arc { + Arc::new(self.radius, self.angle) + } + + /// Returns the area of this sector + #[inline(always)] + pub fn area(&self) -> f32 { + self.radius.powi(2) * self.angle / 2.0 + } +} + /// An ellipse primitive #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 40e018602670c..818f79beb1fc2 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -5,8 +5,11 @@ use crate::{ use super::Meshable; use bevy_math::{ - primitives::{Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, WindingOrder}, - Vec2, + primitives::{ + Capsule2d, Circle, CircularSector, Ellipse, Rectangle, RegularPolygon, Triangle2d, + WindingOrder, + }, + FloatExt, Vec2, }; use wgpu::PrimitiveTopology; @@ -77,6 +80,107 @@ impl From for Mesh { } } +/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. +#[derive(Clone, Copy, Debug)] +pub struct CircularSectorMeshBuilder { + /// The [`sector`] shape. + pub sector: CircularSector, + /// The number of vertices used for the arc portion of the sector mesh. + /// The default is `32`. + #[doc(alias = "vertices")] + pub resolution: usize, +} + +impl Default for CircularSectorMeshBuilder { + fn default() -> Self { + Self { + sector: CircularSector::default(), + resolution: 32, + } + } +} + +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 { + Self { + sector: CircularSector { radius, angle }, + resolution, + } + } + + /// Sets the number of vertices used for the sector 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 circle. + positions.push([0.0; 3]); + uvs.push([0.5; 2]); + + 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)); + + positions.push([ + angle.x * self.sector.radius, + angle.y * self.sector.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_inserted_indices(Indices::U32(indices)) + } +} + +impl Meshable for CircularSector { + type Output = CircularSectorMeshBuilder; + + fn mesh(&self) -> Self::Output { + CircularSectorMeshBuilder { + sector: *self, + ..Default::default() + } + } +} + +impl From for Mesh { + fn from(sector: CircularSector) -> Self { + sector.mesh().build() + } +} + +impl From for Mesh { + fn from(sector: CircularSectorMeshBuilder) -> 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 be5e70529a8aa..6fd1682c5df22 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -4,6 +4,7 @@ use bevy::{ prelude::*, sprite::{MaterialMesh2dBundle, Mesh2dHandle}, }; +use std::f32::consts::PI; fn main() { App::new() @@ -12,8 +13,6 @@ fn main() { .run(); } -const X_EXTENT: f32 = 600.; - fn setup( mut commands: Commands, mut meshes: ResMut>, @@ -21,33 +20,59 @@ fn setup( ) { commands.spawn(Camera2dBundle::default()); + struct Shape { + mesh: Mesh2dHandle, + transform: Transform, + } + + impl Shape { + fn new(mesh: Handle, transform: Transform) -> Self { + Self { + mesh: Mesh2dHandle(mesh), + transform, + } + } + } + impl From> for Shape { + fn from(mesh: Handle) -> Self { + Self::new(mesh, default()) + } + } + + let sector = CircularSector::new(50.0, 5.0); let shapes = [ - Mesh2dHandle(meshes.add(Circle { radius: 50.0 })), - Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))), - Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))), - Mesh2dHandle(meshes.add(Rectangle::new(50.0, 100.0))), - Mesh2dHandle(meshes.add(RegularPolygon::new(50.0, 6))), - Mesh2dHandle(meshes.add(Triangle2d::new( + Shape::from(meshes.add(Circle { radius: 50.0 })), + Shape::new( + meshes.add(CircularSector::new(50.0, 5.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. + default(), //Transform::from_rotation(Quat::from_rotation_z(-sector.arc().angle / 2.0 + PI)), + ), + Shape::from(meshes.add(Ellipse::new(25.0, 50.0))), + Shape::from(meshes.add(Capsule2d::new(25.0, 50.0))), + Shape::from(meshes.add(Rectangle::new(50.0, 100.0))), + Shape::from(meshes.add(RegularPolygon::new(50.0, 6))), + Shape::from(meshes.add(Triangle2d::new( Vec2::Y * 50.0, Vec2::new(-50.0, -50.0), Vec2::new(50.0, -50.0), ))), ]; let num_shapes = shapes.len(); + let x_extent = num_shapes as f32 * 100.0; 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 mut transform = shape.transform; + // Distribute shapes from -x_extent to +x_extent. + transform.translation.x += -x_extent / 2. + i as f32 / (num_shapes - 1) as f32 * x_extent; commands.spawn(MaterialMesh2dBundle { - mesh: shape, + mesh: shape.mesh, material: materials.add(color), - transform: Transform::from_xyz( - // Distribute shapes from -X_EXTENT to +X_EXTENT. - -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT, - 0.0, - 0.0, - ), + transform, ..default() }); } From 9b88d458dd372991ca04810266cad858b38c7f6c Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 21:05:12 -0500 Subject: [PATCH 02/19] Add `CircularSegment` primitve as well. --- .../src/bounding/bounded2d/primitive_impls.rs | 42 +++--- crates/bevy_math/src/primitives/dim2.rs | 140 +++++++++++++----- .../bevy_render/src/mesh/primitives/dim2.rs | 116 ++++++++++++++- examples/2d/2d_shapes.rs | 12 +- 4 files changed, 245 insertions(+), 65 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index cbd1aae2cdc7a..cf5d06e1e7e23 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 1b28bc8f2faed..bbcc5269d06c3 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 818f79beb1fc2..691842d07aef3 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_inserted_indices(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 6fd1682c5df22..e9b3d2ad16749 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -40,14 +40,22 @@ fn setup( } let sector = CircularSector::new(50.0, 5.0); + let segment = CircularSegment::new(50.0, 2.5); let shapes = [ Shape::from(meshes.add(Circle { radius: 50.0 })), Shape::new( - meshes.add(CircularSector::new(50.0, 5.0)), + meshes.add(sector), // 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. - default(), //Transform::from_rotation(Quat::from_rotation_z(-sector.arc().angle / 2.0 + PI)), + Transform::from_rotation(Quat::from_rotation_z(-sector.arc.angle() / 2.0 + PI)), + ), + Shape::new( + meshes.add(segment), + // 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. + Transform::from_rotation(Quat::from_rotation_z(-segment.arc.half_angle + PI / 2.0)), ), Shape::from(meshes.add(Ellipse::new(25.0, 50.0))), Shape::from(meshes.add(Capsule2d::new(25.0, 50.0))), From c36edb5c995f592b2b79b976bfca6275655454fb Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 7 Feb 2024 12:28:14 -0500 Subject: [PATCH 03/19] Fix doc errors. --- crates/bevy_math/src/primitives/dim2.rs | 27 ++++++++++--------- .../bevy_render/src/mesh/primitives/dim2.rs | 4 +-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index bbcc5269d06c3..dc108037dc3bf 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -180,9 +180,11 @@ impl Circle { /// /// An arc has no area. /// If you want to include the portion of a circle's area swept out by the arc, -/// use [CircularSector]. +/// use [`CircularSector`]. +/// If you want to include only the space inside the convex hull of the arc, +/// use [`CircularSegment`]. /// -/// The arc is drawn starting from [Vec2::X], going counterclockwise. +/// The arc is drawn starting from [`Vec2::X`], going counterclockwise. /// To orient the arc differently, apply a rotation. /// The arc is drawn with the center of its circle at the origin (0, 0), /// meaning that the center may not be inside its convex hull. @@ -197,7 +199,7 @@ pub struct Arc { impl Primitive2d for Arc {} impl Default for Arc { - // Returns the default [`Arc`] with radius `0.5` and angle `1.0`. + /// Returns the default [`Arc`] with radius `0.5` and angle `1.0`. fn default() -> Self { Self { radius: 0.5, @@ -282,8 +284,9 @@ impl Arc { /// 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. + /// 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_len). + /// If the arc is [major](Self::is_major), it will be their sum. #[inline(always)] pub fn sagitta_len(&self) -> f32 { if self.is_major() { @@ -302,7 +305,7 @@ impl Arc { /// A primitive representing a circular sector: a pie slice of a circle. /// -/// The sector is drawn starting from [Vec2::X], going counterclockwise. +/// The sector is drawn starting from [`Vec2::X`], going counterclockwise. /// To orient the sector differently, apply a rotation. /// The sector is drawn with the center of its circle at the origin (0, 0). #[derive(Clone, Copy, Debug, PartialEq)] @@ -315,7 +318,7 @@ pub struct CircularSector { impl Primitive2d for CircularSector {} impl Default for CircularSector { - // Returns the default [`CircularSector`] with radius `0.5` and angle `1.0`. + /// Returns the default [`CircularSector`] with radius `0.5` and angle `1.0`. fn default() -> Self { Arc::default().into() } @@ -328,7 +331,7 @@ impl From for CircularSector { } 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 fn new(radius: f32, angle: f32) -> Self { Arc::new(radius, angle).into() @@ -344,10 +347,10 @@ impl CircularSector { /// 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. +/// 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 +/// When positioning the segment, the [`apothem_len`](Arc::apothem_len) and [`sagitta_len`](Arc::sagitta_len) functions /// may be particularly useful. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -359,7 +362,7 @@ pub struct CircularSegment { impl Primitive2d for CircularSegment {} impl Default for CircularSegment { - // Returns the default [CircularSegment] with radius `0.5` and angle `1.0`. + /// Returns the default [`CircularSegment`] with radius `0.5` and angle `1.0`. fn default() -> Self { Arc::default().into() } @@ -372,7 +375,7 @@ impl From for CircularSegment { } impl CircularSegment { - /// Create a new [CircularSegment] from a `radius`, and an `angle` + /// 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() diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 691842d07aef3..b32e81e1b1ea7 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -83,7 +83,7 @@ impl From for Mesh { /// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. #[derive(Clone, Copy, Debug)] pub struct CircularSectorMeshBuilder { - /// The [`sector`] shape. + /// The sector shape. pub sector: CircularSector, /// The number of vertices used for the arc portion of the sector mesh. /// The default is `32`. @@ -184,7 +184,7 @@ 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. + /// The segment shape. pub segment: CircularSegment, /// The number of vertices used for the arc portion of the segment mesh. /// The default is `32`. From 1947ac079aee7f6defc5b73b8dad7ee340c67329 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 21:07:34 -0500 Subject: [PATCH 04/19] Review feedback and other minor improvements. --- .../src/bounding/bounded2d/primitive_impls.rs | 12 +- crates/bevy_math/src/primitives/dim2.rs | 164 +++++++++++++----- crates/bevy_render/src/mesh/primitives/mod.rs | 2 +- 3 files changed, 130 insertions(+), 48 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index cf5d06e1e7e23..c186d775681f2 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use glam::{Mat2, Vec2}; use crate::primitives::{ - Arc, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Direction2d, Ellipse, + Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; @@ -21,7 +21,7 @@ impl Bounded2d for Circle { } } -impl Bounded2d for Arc { +impl Bounded2d for Arc2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // For a sufficiently wide arc, the bounding points in a given direction will be the outer // limits of a circle centered at the origin. @@ -60,14 +60,14 @@ impl Bounded2d for Arc { // 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 center = self.chord_midpoint().rotate(Vec2::from_angle(rotation)); - BoundingCircle::new(center + translation, self.half_chord_len()) + BoundingCircle::new(center + translation, self.half_chord_length()) } } } impl Bounded2d for CircularSector { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - // This is identical to the implementation for Arc, above, with the additional possibility of the + // This is identical to the implementation for Arc2d, above, with the additional possibility of the // origin point, the center of the arc, acting as a bounding point. // // See comments above for discussion. @@ -99,7 +99,7 @@ impl Bounded2d for CircularSector { // 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.arc.radius) - } else if self.arc.chord_len() < self.arc.radius { + } else if self.arc.chord_length() < 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 half_radius = self.arc.radius / 2.0; @@ -110,7 +110,7 @@ impl Bounded2d for CircularSector { // 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 center = self.arc.chord_midpoint().rotate(Vec2::from_angle(rotation)); - BoundingCircle::new(center + translation, self.arc.half_chord_len()) + BoundingCircle::new(center + translation, self.arc.half_chord_length()) } } } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index dc108037dc3bf..541d7ec0bc82f 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -176,7 +176,9 @@ impl Circle { } } -/// A primitive representing an arc: a segment of a circle. +const HALF_PI: f32 = PI / 2.0; + +/// A primitive representing an arc between two points on a circle. /// /// An arc has no area. /// If you want to include the portion of a circle's area swept out by the arc, @@ -189,35 +191,62 @@ impl Circle { /// The arc is drawn with the center of its circle at the origin (0, 0), /// meaning that the center may not be inside its convex hull. #[derive(Clone, Copy, Debug, PartialEq)] +#[doc(alias("CircularArc", "CircleArc"))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct Arc { +pub struct Arc2d { /// The radius of the circle pub radius: f32, - /// Half the angle swept out by the arc. + /// Half the angle subtended by the arc. pub half_angle: f32, } -impl Primitive2d for Arc {} +impl Primitive2d for Arc2d {} -impl Default for Arc { - /// Returns the default [`Arc`] with radius `0.5` and angle `1.0`. +impl Default for Arc2d { + /// Returns the default [`Arc2d`] with radius `0.5`, covering a quarter of a circle. fn default() -> Self { Self { radius: 0.5, - half_angle: 0.5, + half_angle: PI / 4.0, } } } -impl Arc { - /// Create a new [`Arc`] from a `radius`, and an `angle` +impl Arc2d { + /// Create a new [`Arc2d`] from a `radius`, and a `half_angle` #[inline(always)] - pub fn new(radius: f32, angle: f32) -> Self { + pub fn new(radius: f32, half_angle: f32) -> Self { + Self { radius, half_angle } + } + + /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians. + #[inline(always)] + pub fn from_radians(radius: f32, angle: f32) -> Self { Self { radius, half_angle: angle / 2.0, } } + /// Create a new [`Arc2d`] from a `radius` and an angle in `degrees`. + #[inline(always)] + pub fn from_degrees(radius: f32, degrees: f32) -> Self { + Self { + radius, + half_angle: degrees.to_radians() / 2.0, + } + } + + /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a circle. + /// + /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + #[inline(always)] + pub fn from_fraction(radius: f32, fraction: f32) -> Self { + Self { + radius, + half_angle: fraction * PI, + } + } + /// Get the angle of the arc #[inline(always)] pub fn angle(&self) -> f32 { @@ -256,50 +285,63 @@ impl Arc { /// Get half the length of the chord subtended by the arc #[inline(always)] - pub fn half_chord_len(&self) -> f32 { + pub fn half_chord_length(&self) -> f32 { self.radius * f32::sin(self.half_angle) } /// Get the length of the chord subtended by the arc #[inline(always)] - pub fn chord_len(&self) -> f32 { - 2.0 * self.half_chord_len() + pub fn chord_length(&self) -> f32 { + 2.0 * self.half_chord_length() } - /// Get the midpoint of the chord + /// Get the midpoint of the chord subtended by the arc #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { - self.apothem_len() * Vec2::from_angle(self.half_angle) + self.apothem() * Vec2::from_angle(self.half_angle) } /// 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 apothem_len(&self) -> f32 { - f32::sqrt(self.radius.powi(2) - self.half_chord_len().powi(2)) + // Naming note: Various sources are inconsistent as to whether the apothem is the segment between the center and the + // midpoint of a chord, or the length of that segment. Given this confusion, we've opted for the definition + // used by Wolfram MathWorld, which is the distance rather than the segment. + pub fn apothem(&self) -> f32 { + f32::sqrt(self.radius.powi(2) - self.half_chord_length().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_len). + /// If the arc is [minor](Self::is_minor), i.e. less than or equal to a semicircle, + /// this will be the difference of the [`radius`](Self::radius) and the [`apothem`](Self::apothem). /// If the arc is [major](Self::is_major), it will be their sum. #[inline(always)] - pub fn sagitta_len(&self) -> f32 { + pub fn sagitta(&self) -> f32 { if self.is_major() { - self.radius + self.apothem_len() + self.radius + self.apothem() } else { - self.radius - self.apothem_len() + self.radius - self.apothem() } } + /// Produces true if the arc is at most half a circle. + /// + /// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor. + #[inline(always)] + pub fn is_minor(&self) -> bool { + self.half_angle <= HALF_PI + } + /// Produces true if the arc is at least half a circle. + /// + /// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor. #[inline(always)] pub fn is_major(&self) -> bool { - self.angle() >= PI + self.half_angle >= HALF_PI } } @@ -307,25 +349,25 @@ impl Arc { /// /// The sector is drawn starting from [`Vec2::X`], going counterclockwise. /// To orient the sector differently, apply a rotation. -/// The sector is drawn with the center of its circle at the origin (0, 0). +/// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSector { /// The arc from which this sector is contructed. - #[cfg_attr(feature = "seriealize", serde(flatten))] - pub arc: Arc, + #[cfg_attr(feature = "serialize", serde(flatten))] + pub arc: Arc2d, } impl Primitive2d for CircularSector {} impl Default for CircularSector { - /// Returns the default [`CircularSector`] with radius `0.5` and angle `1.0`. + /// Returns the default [`CircularSector`] with radius `0.5` and covering a quarter circle. fn default() -> Self { - Arc::default().into() + Self::from(Arc2d::default()) } } -impl From for CircularSector { - fn from(arc: Arc) -> Self { +impl From for CircularSector { + fn from(arc: Arc2d) -> Self { Self { arc } } } @@ -334,7 +376,27 @@ impl CircularSector { /// Create a new [`CircularSector`] from a `radius`, and an `angle` #[inline(always)] pub fn new(radius: f32, angle: f32) -> Self { - Arc::new(radius, angle).into() + Self::from(Arc2d::new(radius, angle)) + } + + /// Create a new [`CircularSector`] from a `radius` and an `angle` in radians. + #[inline(always)] + pub fn from_radians(radius: f32, angle: f32) -> Self { + Self::from(Arc2d::from_radians(radius, angle)) + } + + /// Create a new [`CircularSector`] from a `radius` and an angle in `degrees`. + #[inline(always)] + pub fn from_degrees(radius: f32, degrees: f32) -> Self { + Self::from(Arc2d::from_degrees(radius, degrees)) + } + + /// Create a new [`CircularSector`] from a `radius` and a `fraction` of a circle. + /// + /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + #[inline(always)] + pub fn from_fraction(radius: f32, fraction: f32) -> Self { + Self::from(Arc2d::from_fraction(radius, fraction)) } /// Returns the area of this sector @@ -349,27 +411,27 @@ impl CircularSector { /// /// 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`](Arc::apothem_len) and [`sagitta_len`](Arc::sagitta_len) functions -/// may be particularly useful. +/// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. +/// When positioning a segment, the [`apothem`](Arc2d::apothem) function may be particularly useful, +/// as it computes the distance between the segment and the origin. #[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, + #[cfg_attr(feature = "serialize", serde(flatten))] + pub arc: Arc2d, } impl Primitive2d for CircularSegment {} impl Default for CircularSegment { - /// Returns the default [`CircularSegment`] with radius `0.5` and angle `1.0`. + /// Returns the default [`CircularSegment`] with radius `0.5` and covering a quarter circle. fn default() -> Self { - Arc::default().into() + Self::from(Arc2d::default()) } } -impl From for CircularSegment { - fn from(arc: Arc) -> Self { +impl From for CircularSegment { + fn from(arc: Arc2d) -> Self { Self { arc } } } @@ -378,7 +440,27 @@ 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() + Self::from(Arc2d::new(radius, angle)) + } + + /// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians. + #[inline(always)] + pub fn from_radians(radius: f32, angle: f32) -> Self { + Self::from(Arc2d::from_radians(radius, angle)) + } + + /// Create a new [`CircularSegment`] from a `radius` and an angle in `degrees`. + #[inline(always)] + pub fn from_degrees(radius: f32, degrees: f32) -> Self { + Self::from(Arc2d::from_degrees(radius, degrees)) + } + + /// Create a new [`CircularSegment`] from a `radius` and a `fraction` of a circle. + /// + /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + #[inline(always)] + pub fn from_fraction(radius: f32, fraction: f32) -> Self { + Self::from(Arc2d::from_fraction(radius, fraction)) } /// Returns the area of this segment diff --git a/crates/bevy_render/src/mesh/primitives/mod.rs b/crates/bevy_render/src/mesh/primitives/mod.rs index a2bb01599b4ee..017f3b4c0ad79 100644 --- a/crates/bevy_render/src/mesh/primitives/mod.rs +++ b/crates/bevy_render/src/mesh/primitives/mod.rs @@ -20,7 +20,7 @@ //! ``` mod dim2; -pub use dim2::{CircleMeshBuilder, EllipseMeshBuilder}; +pub use dim2::*; mod dim3; pub use dim3::*; From 33e2dd27c42578b3a180b6e4719a86aab41c9023 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Mon, 12 Feb 2024 23:49:10 -0500 Subject: [PATCH 05/19] Change the orientation of arc primitives to vertically symmetrical. --- .../src/bounding/bounded2d/primitive_impls.rs | 84 +++++++++---------- crates/bevy_math/src/primitives/dim2.rs | 29 ++++--- .../bevy_render/src/mesh/primitives/dim2.rs | 26 +++++- examples/2d/2d_shapes.rs | 18 ++-- 4 files changed, 83 insertions(+), 74 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index c186d775681f2..4271c8241e30c 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -1,12 +1,11 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). -use std::f32::consts::PI; - use glam::{Mat2, Vec2}; use crate::primitives::{ - Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, Direction2d, Ellipse, - Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment, + Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, + Segment2d, Triangle2d, }; use super::{Aabb2d, Bounded2d, BoundingCircle}; @@ -23,31 +22,23 @@ impl Bounded2d for Circle { impl Bounded2d for Arc2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - // For a sufficiently wide arc, the bounding points in a given direction will be the outer - // limits of a circle centered at the origin. - // For smaller arcs, the two endpoints of the arc could also be bounding points, - // but the start point is always axis-aligned so it's included as one of the circular limits. + // Because our arcs are always symmetrical around Vec::Y, the uppermost point and the two endpoints will always be extrema. + // For an arc that is greater than a semicircle, radii to the left and right will also be bounding points. // This gives five possible bounding points, so we will lay them out in an array and then // select the appropriate slice to compute the bounding box of. - let mut circle_bounds = [ - self.end(), - self.radius * Vec2::X, + let all_bounds = [ + self.left_endpoint(), + self.right_endpoint(), self.radius * Vec2::Y, + self.radius * Vec2::X, self.radius * -Vec2::X, - self.radius * -Vec2::Y, ]; - 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; - Aabb2d::from_point_cloud( - translation, - rotation, - &circle_bounds[0..(2 + quarter_turns)], - ) + let bounds = if self.is_major() { + &all_bounds[0..5] + } else { + &all_bounds[0..3] + }; + Aabb2d::from_point_cloud(translation, rotation, bounds) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { @@ -68,29 +59,23 @@ impl Bounded2d for Arc2d { impl Bounded2d for CircularSector { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // This is identical to the implementation for Arc2d, above, with the additional possibility of the - // origin point, the center of the arc, acting as a bounding point. + // origin point, the center of the arc, acting as a bounding point when the arc is minor. // - // See comments above for discussion. - let mut circle_bounds = [ + // See comments above for an explanation of the logic. + let all_bounds = [ Vec2::ZERO, - self.arc.end(), - self.arc.radius * Vec2::X, + self.arc.left_endpoint(), + self.arc.right_endpoint(), self.arc.radius * Vec2::Y, + self.arc.radius * Vec2::X, self.arc.radius * -Vec2::X, - self.arc.radius * -Vec2::Y, ]; - 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.arc.angle().abs() / (PI / 2.0)).min(3.0) as usize; - Aabb2d::from_point_cloud( - translation, - rotation, - &circle_bounds[0..(3 + quarter_turns)], - ) + let bounds = if self.arc.is_major() { + &all_bounds[1..6] + } else { + &all_bounds[0..4] + }; + Aabb2d::from_point_cloud(translation, rotation, bounds) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { @@ -101,10 +86,9 @@ impl Bounded2d for CircularSector { BoundingCircle::new(translation, self.arc.radius) } else if self.arc.chord_length() < 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. + // so the bounding circle is centered on the midpoint of the radius. let half_radius = self.arc.radius / 2.0; - let angle = Vec2::from_angle(self.arc.half_angle + rotation); - let center = half_radius * angle; + let center = half_radius * Vec2::Y; BoundingCircle::new(center + translation, half_radius) } else { // Otherwise, the widest distance between two points is the chord, @@ -115,6 +99,16 @@ impl Bounded2d for CircularSector { } } +impl Bounded2d for CircularSegment { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + self.arc.aabb_2d(translation, rotation) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + self.arc.bounding_circle(translation, rotation) + } +} + impl Bounded2d for Ellipse { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // V = (hh * cos(beta), hh * sin(beta)) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 541d7ec0bc82f..547084899be05 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -186,10 +186,9 @@ const HALF_PI: f32 = PI / 2.0; /// If you want to include only the space inside the convex hull of the arc, /// use [`CircularSegment`]. /// -/// The arc is drawn starting from [`Vec2::X`], going counterclockwise. -/// To orient the arc differently, apply a rotation. -/// The arc is drawn with the center of its circle at the origin (0, 0), -/// meaning that the center may not be inside its convex hull. +/// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on +/// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this +/// means that the origin may not be within the `Arc2d`'s convex hull. #[derive(Clone, Copy, Debug, PartialEq)] #[doc(alias("CircularArc", "CircleArc"))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -259,28 +258,28 @@ impl Arc2d { self.angle() * self.radius } - /// Get the start point of the arc + /// Get the right-hand end point of the arc #[inline(always)] - pub fn start(&self) -> Vec2 { - Vec2::new(self.radius, 0.0) + pub fn right_endpoint(&self) -> Vec2 { + self.radius * Vec2::from_angle(HALF_PI - self.half_angle) } - /// Get the end point of the arc + /// Get the left-hand end point of the arc #[inline(always)] - pub fn end(&self) -> Vec2 { - self.radius * Vec2::from_angle(self.angle()) + pub fn left_endpoint(&self) -> Vec2 { + self.radius * Vec2::from_angle(HALF_PI + self.half_angle) } /// Get the endpoints of the arc #[inline(always)] pub fn endpoints(&self) -> [Vec2; 2] { - [self.start(), self.end()] + [self.left_endpoint(), self.right_endpoint()] } /// Get the midpoint of the arc #[inline] pub fn midpoint(&self) -> Vec2 { - self.radius * Vec2::from_angle(self.half_angle) + self.radius * Vec2::Y } /// Get half the length of the chord subtended by the arc @@ -298,7 +297,7 @@ impl Arc2d { /// Get the midpoint of the chord subtended by the arc #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { - self.apothem() * Vec2::from_angle(self.half_angle) + self.apothem() * Vec2::Y } /// Get the length of the apothem of this arc, that is, @@ -347,7 +346,7 @@ impl Arc2d { /// A primitive representing a circular sector: a pie slice of a circle. /// -/// The sector is drawn starting from [`Vec2::X`], going counterclockwise. +/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. /// To orient the sector differently, apply a rotation. /// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. #[derive(Clone, Copy, Debug, PartialEq)] @@ -409,7 +408,7 @@ impl CircularSector { /// 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. +/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. /// To orient the segment differently, apply a rotation. /// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. /// When positioning a segment, the [`apothem`](Arc2d::apothem) function may be particularly useful, diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index b32e81e1b1ea7..d662e1b30762e 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -1,3 +1,5 @@ +use std::f32::consts::PI; + use crate::{ mesh::{Indices, Mesh}, render_asset::RenderAssetUsages, @@ -81,6 +83,9 @@ impl From for Mesh { } /// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. +/// +/// The resulting mesh will have a UV-map such that the center of the circle is +/// at the centure of the texture. #[derive(Clone, Copy, Debug)] pub struct CircularSectorMeshBuilder { /// The sector shape. @@ -129,10 +134,12 @@ impl CircularSectorMeshBuilder { positions.push([0.0; 3]); uvs.push([0.5; 2]); - let last = (self.resolution - 1) as f32; + let first_angle = PI / 2.0 - self.sector.arc.half_angle; + let last_angle = PI / 2.0 + self.sector.arc.half_angle; + let last_i = (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.arc.angle(), i as f32 / last)); + let angle = Vec2::from_angle(f32::lerp(first_angle, last_angle, i as f32 / last_i)); positions.push([ angle.x * self.sector.arc.radius, @@ -170,6 +177,9 @@ impl Meshable for CircularSector { } impl From for Mesh { + /// Converts this sector into a [`Mesh`] using a default [`CircularSectorMeshBuilder`]. + /// + /// See the documentation of [`CircularSectorMeshBuilder`] for more details. fn from(sector: CircularSector) -> Self { sector.mesh().build() } @@ -182,6 +192,9 @@ impl From for Mesh { } /// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. +/// +/// The resulting mesh will have a UV-map such that the center of the circle is +/// at the centure of the texture. #[derive(Clone, Copy, Debug)] pub struct CircularSegmentMeshBuilder { /// The segment shape. @@ -231,10 +244,12 @@ impl CircularSegmentMeshBuilder { 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; + let first_angle = PI / 2.0 - self.segment.arc.half_angle; + let last_angle = PI / 2.0 + self.segment.arc.half_angle; + let last_i = (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)); + let angle = Vec2::from_angle(f32::lerp(first_angle, last_angle, i as f32 / last_i)); positions.push([ angle.x * self.segment.arc.radius, @@ -272,6 +287,9 @@ impl Meshable for CircularSegment { } impl From for Mesh { + /// Converts this sector into a [`Mesh`] using a default [`CircularSegmentMeshBuilder`]. + /// + /// See the documentation of [`CircularSegmentMeshBuilder`] for more details. fn from(segment: CircularSegment) -> Self { segment.mesh().build() } diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index e9b3d2ad16749..308b501c3feed 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -39,23 +39,21 @@ fn setup( } } - let sector = CircularSector::new(50.0, 5.0); - let segment = CircularSegment::new(50.0, 2.5); + let sector = CircularSector::from_radians(50.0, 5.0); + let segment = CircularSegment::from_degrees(50.0, 135.0); let shapes = [ Shape::from(meshes.add(Circle { radius: 50.0 })), Shape::new( meshes.add(sector), - // 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. - Transform::from_rotation(Quat::from_rotation_z(-sector.arc.angle() / 2.0 + PI)), + // A sector is drawn symmetrically from the top. + // To make it face right, we must rotate it to the left by 90 degrees. + Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), ), Shape::new( meshes.add(segment), - // 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. - Transform::from_rotation(Quat::from_rotation_z(-segment.arc.half_angle + PI / 2.0)), + // The segment is drawn with the center as the center of the circle. + // By subtracting the apothem, we move the segment down so that it touches the line x = 0. + Transform::from_translation(Vec3::new(0.0, -segment.arc.apothem(), 0.0)), ), Shape::from(meshes.add(Ellipse::new(25.0, 50.0))), Shape::from(meshes.add(Capsule2d::new(25.0, 50.0))), From 971f9b04ac3f1069ea0bc374388981d17a75dc58 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 16:24:26 -0500 Subject: [PATCH 06/19] Fix Circular{Sector,Segment} UV-mapping and add an example. This required adding a parameterizable angle for the UV mapping. It's in an enum to reduce the amount of churn if/when we add additional modes. --- Cargo.toml | 11 ++ .../src/bounding/bounded2d/primitive_impls.rs | 12 +- crates/bevy_math/src/primitives/dim2.rs | 164 +++++++++++++++++- .../bevy_render/src/mesh/primitives/dim2.rs | 123 +++++++++---- examples/2d/mesh2d_circular.rs | 87 ++++++++++ 5 files changed, 349 insertions(+), 48 deletions(-) create mode 100644 examples/2d/mesh2d_circular.rs diff --git a/Cargo.toml b/Cargo.toml index af1aedec06b2b..5584c2bce0c97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -383,6 +383,17 @@ description = "Renders a 2d mesh" category = "2D Rendering" wasm = true +[[example]] +name = "mesh2d_circular" +path = "examples/2d/mesh2d_circular.rs" +doc-scrape-examples = true + +[package.metadata.example.mesh2d_circular] +name = "Circular 2D Meshes" +description = "Demonstrates UV-mapping of circular primitives" +category = "2D Rendering" +wasm = true + [[example]] name = "mesh2d_manual" path = "examples/2d/mesh2d_manual.rs" diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 4271c8241e30c..c5ff577ab7f91 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -66,9 +66,9 @@ impl Bounded2d for CircularSector { Vec2::ZERO, self.arc.left_endpoint(), self.arc.right_endpoint(), - self.arc.radius * Vec2::Y, - self.arc.radius * Vec2::X, - self.arc.radius * -Vec2::X, + self.radius() * Vec2::Y, + self.radius() * Vec2::X, + self.radius() * -Vec2::X, ]; let bounds = if self.arc.is_major() { &all_bounds[1..6] @@ -87,14 +87,14 @@ impl Bounded2d for CircularSector { } else if self.arc.chord_length() < self.arc.radius { // If the chord length is smaller than the radius, then the radius is the widest distance between two points, // so the bounding circle is centered on the midpoint of the radius. - let half_radius = self.arc.radius / 2.0; + let half_radius = self.radius() / 2.0; let center = half_radius * Vec2::Y; 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 center = self.arc.chord_midpoint().rotate(Vec2::from_angle(rotation)); - BoundingCircle::new(center + translation, self.arc.half_chord_length()) + let center = self.chord_midpoint().rotate(Vec2::from_angle(rotation)); + BoundingCircle::new(center + translation, self.half_chord_length()) } } } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 547084899be05..1a6364d4d3bd9 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -258,6 +258,14 @@ impl Arc2d { self.angle() * self.radius } + /// Get the center of the circle defining the arc + /// + /// Always returns `Vec2::ZERO` + #[inline(always)] + pub fn circle_center(&self) -> Vec2 { + Vec2::ZERO + } + /// Get the right-hand end point of the arc #[inline(always)] pub fn right_endpoint(&self) -> Vec2 { @@ -297,7 +305,11 @@ impl Arc2d { /// Get the midpoint of the chord subtended by the arc #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { - self.apothem() * Vec2::Y + if self.is_minor() { + self.apothem() * Vec2::Y + } else { + self.apothem() * Vec2::NEG_Y + } } /// Get the length of the apothem of this arc, that is, @@ -311,7 +323,7 @@ impl Arc2d { f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2)) } - /// Get the legnth of the sagitta of this arc, that is, + /// Get the length 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. /// @@ -352,14 +364,14 @@ impl Arc2d { #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSector { - /// The arc from which this sector is contructed. + /// The arc subtending the sector #[cfg_attr(feature = "serialize", serde(flatten))] pub arc: Arc2d, } impl Primitive2d for CircularSector {} impl Default for CircularSector { - /// Returns the default [`CircularSector`] with radius `0.5` and covering a quarter circle. + /// Returns the default [`CircularSector`] with radius `0.5` and covering a quarter circle fn default() -> Self { Self::from(Arc2d::default()) } @@ -398,6 +410,73 @@ impl CircularSector { Self::from(Arc2d::from_fraction(radius, fraction)) } + /// Get half the angle of the sector + #[inline(always)] + pub fn half_angle(&self) -> f32 { + self.arc.half_angle + } + + /// Get the angle of the sector + #[inline(always)] + pub fn angle(&self) -> f32 { + self.arc.angle() + } + + /// Get the radius of the sector + #[inline(always)] + pub fn radius(&self) -> f32 { + self.arc.radius + } + + /// Get the length of the arc subtending the sector + #[inline(always)] + pub fn arc_length(&self) -> f32 { + self.arc.length() + } + + /// Get the center of the circle defining the sector, corresponding to the apex of the sector + /// + /// Always returns `Vec2::ZERO` + #[inline(always)] + #[doc(alias = "apex")] + pub fn circle_center(&self) -> Vec2 { + Vec2::ZERO + } + + /// Get half the length of the chord subtended by the sector + #[inline(always)] + pub fn half_chord_length(&self) -> f32 { + self.arc.half_chord_length() + } + + /// Get the length of the chord subtended by the sector + #[inline(always)] + pub fn chord_length(&self) -> f32 { + self.arc.chord_length() + } + + /// Get the midpoint of the chord subtended by the sector + #[inline(always)] + pub fn chord_midpoint(&self) -> Vec2 { + self.arc.chord_midpoint() + } + + /// Get the length of the apothem of this sector + /// + /// See [`Arc2d::apothem`] + #[inline(always)] + pub fn apothem(&self) -> f32 { + self.arc.apothem() + } + + /// Get the length of the sagitta of this sector + /// + /// See [`Arc2d::sagitta`] + #[inline(always)] + pub fn sagitta(&self) -> f32 { + self.arc.sagitta() + } + /// Returns the area of this sector #[inline(always)] pub fn area(&self) -> f32 { @@ -411,12 +490,11 @@ impl CircularSector { /// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. /// To orient the segment differently, apply a rotation. /// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. -/// When positioning a segment, the [`apothem`](Arc2d::apothem) function may be particularly useful, -/// as it computes the distance between the segment and the origin. +/// When positioning a segment, the [`apothem`](CircularSegment::apothem) function 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. + /// The arc subtending the segment #[cfg_attr(feature = "serialize", serde(flatten))] pub arc: Arc2d, } @@ -462,6 +540,78 @@ impl CircularSegment { Self::from(Arc2d::from_fraction(radius, fraction)) } + /// Get the half-angle of the segment + #[inline(always)] + pub fn half_angle(&self) -> f32 { + self.arc.half_angle + } + + /// Get the angle of the segment + #[inline(always)] + pub fn angle(&self) -> f32 { + self.arc.angle() + } + + /// Get the radius of the segment + #[inline(always)] + pub fn radius(&self) -> f32 { + self.arc.radius + } + + /// Get the length of the arc subtending the segment + #[inline(always)] + pub fn arc_length(&self) -> f32 { + self.arc.length() + } + + /// Get the center of the circle defining the segment + /// + /// Always returns `Vec2::ZERO` + #[inline(always)] + pub fn circle_center(&self) -> Vec2 { + Vec2::ZERO + } + + /// Get half the length of the chord of the segment, which is the segment's base + #[inline(always)] + #[doc(alias = "half_base_length")] + pub fn half_chord_length(&self) -> f32 { + self.arc.half_chord_length() + } + + /// Get the length of the chord of the segment, which is the segment's base + #[inline(always)] + #[doc(alias = "base_length")] + #[doc(alias = "base")] + pub fn chord_length(&self) -> f32 { + self.arc.chord_length() + } + + /// Get the midpoint of the chord of the segment, which is the segment's base + #[inline(always)] + #[doc(alias = "base_midpoint")] + pub fn chord_midpoint(&self) -> Vec2 { + self.arc.chord_midpoint() + } + + /// Get the length of the apothem of this segment, + /// which is the distance between the segment and the [center of its circle](Self::circle_center) + /// + /// See [`Arc2d::apothem`] + #[inline(always)] + pub fn apothem(&self) -> f32 { + self.arc.apothem() + } + + /// Get the length of the sagitta of this segment, also known as its height + /// + /// See [`Arc2d::sagitta`] + #[inline(always)] + #[doc(alias = "height")] + pub fn sagitta(&self) -> f32 { + self.arc.sagitta() + } + /// Returns the area of this segment #[inline(always)] pub fn area(&self) -> f32 { diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index d662e1b30762e..ff7cbdd141f64 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -82,11 +82,28 @@ impl From for Mesh { } } +/// Specifies how to generate UV-mappings for [`CircularSector`] and [`CircularSegment`] shapes. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum CircularShapeUvMode { + /// Treats the shape as a mask over a circle of equal size and radius, + /// with the center of the circle at the center of the texture. + Mask { + /// Angle by which to rotate the shape when generating the UV map. + angle: f32, + }, +} + +impl Default for CircularShapeUvMode { + fn default() -> Self { + CircularShapeUvMode::Mask { angle: 0.0 } + } +} + /// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. /// /// The resulting mesh will have a UV-map such that the center of the circle is /// at the centure of the texture. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct CircularSectorMeshBuilder { /// The sector shape. pub sector: CircularSector, @@ -94,6 +111,8 @@ pub struct CircularSectorMeshBuilder { /// The default is `32`. #[doc(alias = "vertices")] pub resolution: usize, + /// The UV mapping mode + pub uv_mode: CircularShapeUvMode, } impl Default for CircularSectorMeshBuilder { @@ -101,17 +120,18 @@ impl Default for CircularSectorMeshBuilder { Self { sector: CircularSector::default(), resolution: 32, + uv_mode: CircularShapeUvMode::default(), } } } impl CircularSectorMeshBuilder { - /// Creates a new [`CircularSectorMeshBuilder`] from a given radius, angle, and vertex count. + /// Creates a new [`CircularSectorMeshBuilder`] from a given sector #[inline] - pub fn new(radius: f32, angle: f32, resolution: usize) -> Self { + pub fn new(sector: CircularSector) -> Self { Self { - sector: CircularSector::new(radius, angle), - resolution, + sector, + ..Self::default() } } @@ -123,6 +143,13 @@ impl CircularSectorMeshBuilder { self } + /// Sets the uv mode used for the sector mesh + #[inline] + pub const fn uv_mode(mut self, uv_mode: CircularShapeUvMode) -> Self { + self.uv_mode = uv_mode; + 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); @@ -130,23 +157,27 @@ impl CircularSectorMeshBuilder { let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1]; let mut uvs = Vec::with_capacity(self.resolution + 1); + let CircularShapeUvMode::Mask { angle: uv_angle } = self.uv_mode; + // Push the center of the circle. positions.push([0.0; 3]); uvs.push([0.5; 2]); - let first_angle = PI / 2.0 - self.sector.arc.half_angle; - let last_angle = PI / 2.0 + self.sector.arc.half_angle; + let first_angle = PI / 2.0 - self.sector.half_angle(); + let last_angle = PI / 2.0 + self.sector.half_angle(); let last_i = (self.resolution - 1) as f32; for i in 0..self.resolution { - // Compute vertex position at angle theta - let angle = Vec2::from_angle(f32::lerp(first_angle, last_angle, i as f32 / last_i)); - - positions.push([ - 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)]); + let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); + + // Compute the vertex + let vertex = self.sector.radius() * Vec2::from_angle(angle); + // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). + // We accomplish the Y axis flip by negating the angle. + let uv = + Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); + + positions.push([vertex.x, vertex.y, 0.0]); + uvs.push([uv.x, uv.y]); } for i in 1..(self.resolution as u32) { @@ -203,6 +234,8 @@ pub struct CircularSegmentMeshBuilder { /// The default is `32`. #[doc(alias = "vertices")] pub resolution: usize, + /// The UV mapping mode + pub uv_mode: CircularShapeUvMode, } impl Default for CircularSegmentMeshBuilder { @@ -210,17 +243,18 @@ impl Default for CircularSegmentMeshBuilder { Self { segment: CircularSegment::default(), resolution: 32, + uv_mode: CircularShapeUvMode::default(), } } } impl CircularSegmentMeshBuilder { - /// Creates a new [`CircularSegmentMeshBuilder`] from a given radius, angle, and vertex count. + /// Creates a new [`CircularSegmentMeshBuilder`] from a given segment #[inline] - pub fn new(radius: f32, angle: f32, resolution: usize) -> Self { + pub fn new(segment: CircularSegment) -> Self { Self { - segment: CircularSegment::new(radius, angle), - resolution, + segment, + ..Self::default() } } @@ -232,6 +266,13 @@ impl CircularSegmentMeshBuilder { self } + /// Sets the uv mode used for the segment mesh + #[inline] + pub const fn uv_mode(mut self, uv_mode: CircularShapeUvMode) -> Self { + self.uv_mode = uv_mode; + 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); @@ -239,28 +280,40 @@ impl CircularSegmentMeshBuilder { let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1]; let mut uvs = Vec::with_capacity(self.resolution + 1); + let CircularShapeUvMode::Mask { angle: uv_angle } = self.uv_mode; + // 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 midpoint_vertex = self.segment.chord_midpoint(); + positions.push([midpoint_vertex.x, midpoint_vertex.y, 0.0]); + // Compute the UV coordinate of the midpoint vertex. + // This is similar to the computation inside the loop for the arc vertices, + // but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius + // to correctly position the vertex. + let midpoint_uv = Vec2::from_angle(-uv_angle - PI / 2.0).mul_add( + Vec2::splat(0.5 * (midpoint_vertex.y / self.segment.radius())), + Vec2::splat(0.5), + ); + uvs.push([midpoint_uv.x, midpoint_uv.y]); - let first_angle = PI / 2.0 - self.segment.arc.half_angle; - let last_angle = PI / 2.0 + self.segment.arc.half_angle; + let first_angle = PI / 2.0 - self.segment.half_angle(); + let last_angle = PI / 2.0 + self.segment.half_angle(); let last_i = (self.resolution - 1) as f32; for i in 0..self.resolution { - // Compute vertex position at angle theta - let angle = Vec2::from_angle(f32::lerp(first_angle, last_angle, i as f32 / last_i)); - - 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)]); + let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); + + // Compute the vertex + let vertex = self.segment.radius() * Vec2::from_angle(angle); + // Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5). + // We accomplish the Y axis flip by negating the angle. + let uv = + Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5)); + + positions.push([vertex.x, vertex.y, 0.0]); + uvs.push([uv.x, uv.y]); } for i in 1..(self.resolution as u32) { - // Index 0 is the center. + // Index 0 is the midpoint of the chord. indices.extend_from_slice(&[0, i, i + 1]); } diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs new file mode 100644 index 0000000000000..b8e35d2312d79 --- /dev/null +++ b/examples/2d/mesh2d_circular.rs @@ -0,0 +1,87 @@ +//! Demonstrates UV mappings of the [`CircularSector`] and [`CircularSegment`] primitives. + +use std::f32::consts::PI; + +use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; +use bevy_internal::render::mesh::{ + CircularSectorMeshBuilder, CircularSegmentMeshBuilder, CircularShapeUvMode, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let material = materials.add(asset_server.load("branding/icon.png")); + + commands.spawn(Camera2dBundle { + camera: Camera { + clear_color: ClearColorConfig::Custom(Color::DARK_GRAY), + ..default() + }, + ..default() + }); + + const UPPER_Y: f32 = 50.0; + const LOWER_Y: f32 = -50.0; + const FIRST_X: f32 = -450.0; + const OFFSET: f32 = 100.0; + const NUM_SLICES: i32 = 8; + + // This draws NUM_SLICES copies of the Bevy logo as circular sectors and segments, + // with successively larger angles up to a complete circle. + for i in 0..NUM_SLICES { + let fraction = (i + 1) as f32 / NUM_SLICES as f32; + + let sector = CircularSector::from_fraction(40.0, fraction); + // We want to rotate the circular sector so that the sectors appear clockwise from north. + // We must rotate it both in the Transform and in the mesh's UV mappings. + let sector_angle = -sector.half_angle(); + let sector_mesh = + CircularSectorMeshBuilder::new(sector).uv_mode(CircularShapeUvMode::Mask { + angle: sector_angle, + }); + commands.spawn(MaterialMesh2dBundle { + mesh: meshes.add(sector_mesh).into(), + material: material.clone(), + transform: Transform { + translation: Vec3::new(FIRST_X + OFFSET * i as f32, 2.0 * UPPER_Y, 0.0), + rotation: Quat::from_rotation_z(sector_angle), + ..default() + }, + ..default() + }); + + let segment = CircularSegment::from_fraction(40.0, fraction); + // For the circular segment, we will draw Bevy charging forward, which requires rotating the + // shape and texture by 90 degrees. + // + // Note that this may be unintuitive; it may feel like we should rotate the texture by the + // opposite angle to preserve the orientation of Bevy. But the angle is not the angle of the + // texture itself, rather it is the angle at which the vertices are mapped onto the texture. + // so it is the negative of what you might otherwise expect. + let segment_angle = -PI / 2.0; + let segment_mesh = + CircularSegmentMeshBuilder::new(segment).uv_mode(CircularShapeUvMode::Mask { + angle: -segment_angle, + }); + commands.spawn(MaterialMesh2dBundle { + mesh: meshes.add(segment_mesh).into(), + material: material.clone(), + transform: Transform { + translation: Vec3::new(FIRST_X + OFFSET * i as f32, LOWER_Y, 0.0), + rotation: Quat::from_rotation_z(segment_angle), + ..default() + }, + ..default() + }); + } +} From 609d238314032c16ecff8b83d38d10aab730eefc Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 20:33:01 -0500 Subject: [PATCH 07/19] Allow the apothem to be negative to simplify much of the math. --- crates/bevy_math/src/primitives/dim2.rs | 30 +++++++------------ .../bevy_render/src/mesh/primitives/dim2.rs | 2 +- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 1a6364d4d3bd9..27e12126a2bf7 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -305,38 +305,30 @@ impl Arc2d { /// Get the midpoint of the chord subtended by the arc #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { - if self.is_minor() { - self.apothem() * Vec2::Y - } else { - self.apothem() * Vec2::NEG_Y - } + self.apothem() * Vec2::Y } /// 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. + /// the distance from the center of the circle to the midpoint of the chord, in the direction of the midpoint of the arc. + /// Equivalently, the [`radius`](Self::radius) minus the [`sagitta`](Self::sagitta). + /// + /// Note that for a [`major`](Self::is_major) arc, the apothem will be negative. #[inline(always)] // Naming note: Various sources are inconsistent as to whether the apothem is the segment between the center and the // midpoint of a chord, or the length of that segment. Given this confusion, we've opted for the definition // used by Wolfram MathWorld, which is the distance rather than the segment. pub fn apothem(&self) -> f32 { - f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2)) + let sign = if self.is_minor() { 1.0 } else { -1.0 }; + sign * f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2)) } /// Get the length 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](Self::is_minor), i.e. less than or equal to a semicircle, - /// this will be the difference of the [`radius`](Self::radius) and the [`apothem`](Self::apothem). - /// If the arc is [major](Self::is_major), it will be their sum. - #[inline(always)] + /// The sagitta is also the sum of the [`radius`](Self::radius) and the [`apothem`](Self::apothem). pub fn sagitta(&self) -> f32 { - if self.is_major() { - self.radius + self.apothem() - } else { - self.radius - self.apothem() - } + self.radius - self.apothem() } /// Produces true if the arc is at most half a circle. @@ -490,7 +482,7 @@ impl CircularSector { /// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. /// To orient the segment differently, apply a rotation. /// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. -/// When positioning a segment, the [`apothem`](CircularSegment::apothem) function may be particularly useful. +/// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSegment { @@ -595,7 +587,7 @@ impl CircularSegment { } /// Get the length of the apothem of this segment, - /// which is the distance between the segment and the [center of its circle](Self::circle_center) + /// which is the signed distance between the segment and the [center of its circle](Self::circle_center) /// /// See [`Arc2d::apothem`] #[inline(always)] diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index ff7cbdd141f64..ba6590710644d 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -290,7 +290,7 @@ impl CircularSegmentMeshBuilder { // but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius // to correctly position the vertex. let midpoint_uv = Vec2::from_angle(-uv_angle - PI / 2.0).mul_add( - Vec2::splat(0.5 * (midpoint_vertex.y / self.segment.radius())), + Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())), Vec2::splat(0.5), ); uvs.push([midpoint_uv.x, midpoint_uv.y]); From faabb284b8581663151d588dfab3a855b8e0c3c3 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 21:12:57 -0500 Subject: [PATCH 08/19] Some more small cleanups. Change the default angle to a third of a circle, because a quarter leads to too thin of a segment. Also formatting changes in 2d_shapes.rs. --- crates/bevy_math/src/primitives/dim2.rs | 4 ++-- examples/2d/2d_shapes.rs | 32 ++++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 27e12126a2bf7..b133414567927 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -201,11 +201,11 @@ pub struct Arc2d { impl Primitive2d for Arc2d {} impl Default for Arc2d { - /// Returns the default [`Arc2d`] with radius `0.5`, covering a quarter of a circle. + /// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle. fn default() -> Self { Self { radius: 0.5, - half_angle: PI / 4.0, + half_angle: 2.0 * PI / 3.0, } } } diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 308b501c3feed..3ef4c5a89faa6 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -39,22 +39,26 @@ fn setup( } } - let sector = CircularSector::from_radians(50.0, 5.0); - let segment = CircularSegment::from_degrees(50.0, 135.0); let shapes = [ Shape::from(meshes.add(Circle { radius: 50.0 })), - Shape::new( - meshes.add(sector), - // A sector is drawn symmetrically from the top. - // To make it face right, we must rotate it to the left by 90 degrees. - Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), - ), - Shape::new( - meshes.add(segment), - // The segment is drawn with the center as the center of the circle. - // By subtracting the apothem, we move the segment down so that it touches the line x = 0. - Transform::from_translation(Vec3::new(0.0, -segment.arc.apothem(), 0.0)), - ), + { + let sector = CircularSector::from_radians(50.0, 5.0); + Shape::new( + meshes.add(sector), + // A sector is drawn symmetrically from the top. + // To make it face right, we must rotate it to the left by 90 degrees. + Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), + ) + }, + { + let segment = CircularSegment::from_degrees(50.0, 135.0); + Shape::new( + meshes.add(segment), + // The segment is drawn with the center as the center of the circle. + // By subtracting the apothem, we move the segment down so that it touches the line x = 0. + Transform::from_translation(Vec3::new(0.0, -segment.arc.apothem(), 0.0)), + ) + }, Shape::from(meshes.add(Ellipse::new(25.0, 50.0))), Shape::from(meshes.add(Capsule2d::new(25.0, 50.0))), Shape::from(meshes.add(Rectangle::new(50.0, 100.0))), From 41806b712e348273ddc40c672bc946565531634e Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 20:40:45 -0500 Subject: [PATCH 09/19] Update examples page. --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 0a2b9503c6db5..a30464fda81d9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -99,6 +99,7 @@ Example | Description [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons [2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method +[Circular 2D Meshes](../examples/2d/mesh2d_circular.rs) | Demonstrates UV-mapping of circular primitives [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh From 9240e7e9bee1e8389987b822a289545ff9d7ab87 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Wed, 14 Feb 2024 21:22:18 -0500 Subject: [PATCH 10/19] Rename CircularShapeUvMode to CircularMeshUvMode. --- .../bevy_render/src/mesh/primitives/dim2.rs | 22 +++++++++---------- examples/2d/2d_shapes.rs | 15 +++++-------- examples/2d/mesh2d_circular.rs | 6 ++--- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index ba6590710644d..2e0bc1fa90cb6 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -84,7 +84,7 @@ impl From for Mesh { /// Specifies how to generate UV-mappings for [`CircularSector`] and [`CircularSegment`] shapes. #[derive(Copy, Clone, Debug, PartialEq)] -pub enum CircularShapeUvMode { +pub enum CircularMeshUvMode { /// Treats the shape as a mask over a circle of equal size and radius, /// with the center of the circle at the center of the texture. Mask { @@ -93,9 +93,9 @@ pub enum CircularShapeUvMode { }, } -impl Default for CircularShapeUvMode { +impl Default for CircularMeshUvMode { fn default() -> Self { - CircularShapeUvMode::Mask { angle: 0.0 } + CircularMeshUvMode::Mask { angle: 0.0 } } } @@ -112,7 +112,7 @@ pub struct CircularSectorMeshBuilder { #[doc(alias = "vertices")] pub resolution: usize, /// The UV mapping mode - pub uv_mode: CircularShapeUvMode, + pub uv_mode: CircularMeshUvMode, } impl Default for CircularSectorMeshBuilder { @@ -120,7 +120,7 @@ impl Default for CircularSectorMeshBuilder { Self { sector: CircularSector::default(), resolution: 32, - uv_mode: CircularShapeUvMode::default(), + uv_mode: CircularMeshUvMode::default(), } } } @@ -145,7 +145,7 @@ impl CircularSectorMeshBuilder { /// Sets the uv mode used for the sector mesh #[inline] - pub const fn uv_mode(mut self, uv_mode: CircularShapeUvMode) -> Self { + pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { self.uv_mode = uv_mode; self } @@ -157,7 +157,7 @@ impl CircularSectorMeshBuilder { let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1]; let mut uvs = Vec::with_capacity(self.resolution + 1); - let CircularShapeUvMode::Mask { angle: uv_angle } = self.uv_mode; + let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; // Push the center of the circle. positions.push([0.0; 3]); @@ -235,7 +235,7 @@ pub struct CircularSegmentMeshBuilder { #[doc(alias = "vertices")] pub resolution: usize, /// The UV mapping mode - pub uv_mode: CircularShapeUvMode, + pub uv_mode: CircularMeshUvMode, } impl Default for CircularSegmentMeshBuilder { @@ -243,7 +243,7 @@ impl Default for CircularSegmentMeshBuilder { Self { segment: CircularSegment::default(), resolution: 32, - uv_mode: CircularShapeUvMode::default(), + uv_mode: CircularMeshUvMode::default(), } } } @@ -268,7 +268,7 @@ impl CircularSegmentMeshBuilder { /// Sets the uv mode used for the segment mesh #[inline] - pub const fn uv_mode(mut self, uv_mode: CircularShapeUvMode) -> Self { + pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self { self.uv_mode = uv_mode; self } @@ -280,7 +280,7 @@ impl CircularSegmentMeshBuilder { let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1]; let mut uvs = Vec::with_capacity(self.resolution + 1); - let CircularShapeUvMode::Mask { angle: uv_angle } = self.uv_mode; + let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode; // Push the center of the chord. let midpoint_vertex = self.segment.chord_midpoint(); diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 3ef4c5a89faa6..4da1321e59210 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -41,15 +41,12 @@ fn setup( let shapes = [ Shape::from(meshes.add(Circle { radius: 50.0 })), - { - let sector = CircularSector::from_radians(50.0, 5.0); - Shape::new( - meshes.add(sector), - // A sector is drawn symmetrically from the top. - // To make it face right, we must rotate it to the left by 90 degrees. - Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), - ) - }, + Shape::new( + meshes.add(CircularSector::from_radians(50.0, 5.0)), + // A sector is drawn symmetrically from the top. + // To make it face right, we must rotate it to the left by 90 degrees. + Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)), + ), { let segment = CircularSegment::from_degrees(50.0, 135.0); Shape::new( diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs index b8e35d2312d79..1ae4c047fad41 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_circular.rs @@ -4,7 +4,7 @@ use std::f32::consts::PI; use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; use bevy_internal::render::mesh::{ - CircularSectorMeshBuilder, CircularSegmentMeshBuilder, CircularShapeUvMode, + CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder, }; fn main() { @@ -46,7 +46,7 @@ fn setup( // We must rotate it both in the Transform and in the mesh's UV mappings. let sector_angle = -sector.half_angle(); let sector_mesh = - CircularSectorMeshBuilder::new(sector).uv_mode(CircularShapeUvMode::Mask { + CircularSectorMeshBuilder::new(sector).uv_mode(CircularMeshUvMode::Mask { angle: sector_angle, }); commands.spawn(MaterialMesh2dBundle { @@ -70,7 +70,7 @@ fn setup( // so it is the negative of what you might otherwise expect. let segment_angle = -PI / 2.0; let segment_mesh = - CircularSegmentMeshBuilder::new(segment).uv_mode(CircularShapeUvMode::Mask { + CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask { angle: -segment_angle, }); commands.spawn(MaterialMesh2dBundle { From ac33ec6081a27534aab1a3fe3ba5dae68f151e38 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Thu, 15 Feb 2024 06:37:50 -0500 Subject: [PATCH 11/19] Add tests to primitive math. --- crates/bevy_math/Cargo.toml | 1 + crates/bevy_math/src/primitives/dim2.rs | 219 +++++++++++++++++++++++- 2 files changed, 219 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index d9686abf4f2b7..7dd65f60b6406 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -15,6 +15,7 @@ approx = { version = "0.5", optional = true } [dev-dependencies] approx = "0.5" +glam = { features = ["approx"] } [features] serialize = ["dep:serde", "glam/serde"] diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index b133414567927..39cbe059a91b9 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -607,7 +607,224 @@ impl CircularSegment { /// Returns the area of this segment #[inline(always)] pub fn area(&self) -> f32 { - self.arc.radius.powi(2) * (self.arc.half_angle - self.arc.angle().sin()) + 0.5 * self.arc.radius.powi(2) * (self.arc.angle() - self.arc.angle().sin()) + } +} + +#[cfg(test)] +mod arc_tests { + use approx::assert_abs_diff_eq; + + use super::*; + + struct ArcTestCase { + radius: f32, + half_angle: f32, + angle: f32, + length: f32, + circle_center: Vec2, + right_endpoint: Vec2, + left_endpoint: Vec2, + endpoints: [Vec2; 2], + midpoint: Vec2, + half_chord_length: f32, + chord_length: f32, + chord_midpoint: Vec2, + apothem: f32, + sagitta: f32, + is_minor: bool, + is_major: bool, + sector_area: f32, + segment_area: f32, + } + + impl ArcTestCase { + fn check_arc(&self, arc: Arc2d) { + assert_abs_diff_eq!(self.radius, arc.radius); + assert_abs_diff_eq!(self.half_angle, arc.half_angle); + assert_abs_diff_eq!(self.angle, arc.angle()); + assert_abs_diff_eq!(self.length, arc.length()); + assert_abs_diff_eq!(self.circle_center, arc.circle_center()); + assert_abs_diff_eq!(self.right_endpoint, arc.right_endpoint()); + assert_abs_diff_eq!(self.left_endpoint, arc.left_endpoint()); + assert_abs_diff_eq!(self.endpoints[0], arc.endpoints()[0]); + assert_abs_diff_eq!(self.endpoints[1], arc.endpoints()[1]); + assert_abs_diff_eq!(self.midpoint, arc.midpoint()); + assert_abs_diff_eq!(self.half_chord_length, arc.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, arc.chord_length(), epsilon = 0.00001); + assert_abs_diff_eq!(self.chord_midpoint, arc.chord_midpoint()); + assert_abs_diff_eq!(self.apothem, arc.apothem()); + assert_abs_diff_eq!(self.sagitta, arc.sagitta()); + assert_eq!(self.is_minor, arc.is_minor()); + assert_eq!(self.is_major, arc.is_major()); + } + + fn check_sector(&self, sector: CircularSector) { + assert_abs_diff_eq!(self.radius, sector.radius()); + assert_abs_diff_eq!(self.half_angle, sector.half_angle()); + assert_abs_diff_eq!(self.angle, sector.angle()); + assert_abs_diff_eq!(self.circle_center, sector.circle_center()); + assert_abs_diff_eq!(self.half_chord_length, sector.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, sector.chord_length(), epsilon = 0.00001); + assert_abs_diff_eq!(self.chord_midpoint, sector.chord_midpoint()); + assert_abs_diff_eq!(self.apothem, sector.apothem()); + assert_abs_diff_eq!(self.sagitta, sector.sagitta()); + assert_abs_diff_eq!(self.sector_area, sector.area()); + } + + fn check_segment(&self, segment: CircularSegment) { + assert_abs_diff_eq!(self.radius, segment.radius()); + assert_abs_diff_eq!(self.half_angle, segment.half_angle()); + assert_abs_diff_eq!(self.angle, segment.angle()); + assert_abs_diff_eq!(self.circle_center, segment.circle_center()); + assert_abs_diff_eq!(self.half_chord_length, segment.half_chord_length()); + assert_abs_diff_eq!(self.chord_length, segment.chord_length(), epsilon = 0.00001); + assert_abs_diff_eq!(self.chord_midpoint, segment.chord_midpoint()); + assert_abs_diff_eq!(self.apothem, segment.apothem()); + assert_abs_diff_eq!(self.sagitta, segment.sagitta()); + assert_abs_diff_eq!(self.segment_area, segment.area()); + } + } + + #[test] + fn zero_angle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: 0.0, + angle: 0.0, + length: 0.0, + circle_center: Vec2::ZERO, + left_endpoint: Vec2::Y, + right_endpoint: Vec2::Y, + endpoints: [Vec2::Y, Vec2::Y], + midpoint: Vec2::Y, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::Y, + apothem: 1.0, + sagitta: 0.0, + is_minor: true, + is_major: false, + sector_area: 0.0, + segment_area: 0.0, + }; + + tests.check_arc(Arc2d::new(1.0, 0.0)); + tests.check_sector(CircularSector::new(1.0, 0.0)); + tests.check_segment(CircularSegment::new(1.0, 0.0)); + } + + #[test] + fn zero_radius() { + let tests = ArcTestCase { + radius: 0.0, + half_angle: HALF_PI / 2.0, + angle: HALF_PI, + length: 0.0, + circle_center: Vec2::ZERO, + left_endpoint: Vec2::ZERO, + right_endpoint: Vec2::ZERO, + endpoints: [Vec2::ZERO, Vec2::ZERO], + midpoint: Vec2::ZERO, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::ZERO, + apothem: 0.0, + sagitta: 0.0, + is_minor: true, + is_major: false, + sector_area: 0.0, + segment_area: 0.0, + }; + + tests.check_arc(Arc2d::new(0.0, HALF_PI / 2.0)); + tests.check_sector(CircularSector::new(0.0, HALF_PI / 2.0)); + tests.check_segment(CircularSegment::new(0.0, HALF_PI / 2.0)); + } + + #[test] + fn quarter_circle() { + let sqrt_half: f32 = f32::sqrt(0.5); + let tests = ArcTestCase { + radius: 1.0, + half_angle: HALF_PI / 2.0, + angle: HALF_PI, + length: HALF_PI, + circle_center: Vec2::ZERO, + left_endpoint: Vec2::new(-sqrt_half, sqrt_half), + right_endpoint: Vec2::splat(sqrt_half), + endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], + midpoint: Vec2::Y, + half_chord_length: sqrt_half, + chord_length: f32::sqrt(2.0), + chord_midpoint: Vec2::new(0.0, sqrt_half), + apothem: sqrt_half, + sagitta: 1.0 - sqrt_half, + is_minor: true, + is_major: false, + sector_area: PI / 4.0, + segment_area: PI / 4.0 - 0.5, + }; + + tests.check_arc(Arc2d::from_fraction(1.0, 0.25)); + tests.check_sector(CircularSector::from_fraction(1.0, 0.25)); + tests.check_segment(CircularSegment::from_fraction(1.0, 0.25)); + } + + #[test] + fn half_circle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: HALF_PI, + angle: PI, + length: PI, + circle_center: Vec2::ZERO, + left_endpoint: Vec2::NEG_X, + right_endpoint: Vec2::X, + endpoints: [Vec2::NEG_X, Vec2::X], + midpoint: Vec2::Y, + half_chord_length: 1.0, + chord_length: 2.0, + chord_midpoint: Vec2::ZERO, + apothem: 0.0, + sagitta: 1.0, + is_minor: true, + is_major: true, + sector_area: HALF_PI, + segment_area: HALF_PI, + }; + + tests.check_arc(Arc2d::from_radians(1.0, PI)); + tests.check_sector(CircularSector::from_radians(1.0, PI)); + tests.check_segment(CircularSegment::from_radians(1.0, PI)); + } + + #[test] + fn full_circle() { + let tests = ArcTestCase { + radius: 1.0, + half_angle: PI, + angle: 2.0 * PI, + length: 2.0 * PI, + circle_center: Vec2::ZERO, + left_endpoint: Vec2::NEG_Y, + right_endpoint: Vec2::NEG_Y, + endpoints: [Vec2::NEG_Y, Vec2::NEG_Y], + midpoint: Vec2::Y, + half_chord_length: 0.0, + chord_length: 0.0, + chord_midpoint: Vec2::NEG_Y, + apothem: -1.0, + sagitta: 2.0, + is_minor: false, + is_major: true, + sector_area: PI, + segment_area: PI, + }; + + tests.check_arc(Arc2d::from_degrees(1.0, 360.0)); + tests.check_sector(CircularSector::from_degrees(1.0, 360.0)); + tests.check_segment(CircularSegment::from_degrees(1.0, 360.0)); } } From a4fc07f7e21d8d239b0a6b3800f544d781808a40 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Fri, 16 Feb 2024 14:52:34 -0500 Subject: [PATCH 12/19] Add tests for arc bounding shapes. There were bugs. --- crates/bevy_math/Cargo.toml | 3 +- .../src/bounding/bounded2d/primitive_impls.rs | 273 ++++++++++++++---- crates/bevy_math/src/primitives/dim2.rs | 12 + examples/2d/mesh2d_circular.rs | 7 +- 4 files changed, 241 insertions(+), 54 deletions(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 7dd65f60b6406..4cd7ed3177d02 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -12,10 +12,11 @@ keywords = ["bevy"] glam = { version = "0.25", features = ["bytemuck"] } serde = { version = "1", features = ["derive"], optional = true } approx = { version = "0.5", optional = true } +smallvec = { version = "1.11" } [dev-dependencies] approx = "0.5" -glam = { features = ["approx"] } +glam = { version = "0.25", features = ["approx"] } [features] serialize = ["dep:serde", "glam/serde"] diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index c5ff577ab7f91..e4317174412e6 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -1,6 +1,9 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). +use std::f32::consts::PI; + use glam::{Mat2, Vec2}; +use smallvec::SmallVec; use crate::primitives::{ Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment, @@ -22,23 +25,40 @@ impl Bounded2d for Circle { impl Bounded2d for Arc2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - // Because our arcs are always symmetrical around Vec::Y, the uppermost point and the two endpoints will always be extrema. - // For an arc that is greater than a semicircle, radii to the left and right will also be bounding points. - // This gives five possible bounding points, so we will lay them out in an array and then - // select the appropriate slice to compute the bounding box of. - let all_bounds = [ - self.left_endpoint(), - self.right_endpoint(), - self.radius * Vec2::Y, - self.radius * Vec2::X, - self.radius * -Vec2::X, - ]; - let bounds = if self.is_major() { - &all_bounds[0..5] - } else { - &all_bounds[0..3] - }; - Aabb2d::from_point_cloud(translation, rotation, bounds) + // If our arc covers more than a circle, just return the bounding box of the circle. + if self.half_angle >= PI { + return Circle { + radius: self.radius, + } + .aabb_2d(translation, rotation); + } + + // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. + // We need to compute which axis-aligned extrema are actually contained within the rotated arc. + let mut bounds = SmallVec::<[Vec2; 6]>::new(); + let rotation_vec = Vec2::from_angle(rotation); + bounds.push(self.left_endpoint().rotate(rotation_vec)); + bounds.push(self.right_endpoint().rotate(rotation_vec)); + + // The half-angles are measured from a starting point of π/2, being the angle of Vec::Y. + // Compute the normalized angles of the endpoints with the rotation taken into account, and then + // check if we are looking for an angle that is between or outside them. + let left_angle = (PI / 2.0 + self.half_angle + rotation).rem_euclid(2.0 * PI); + let right_angle = (PI / 2.0 - self.half_angle + rotation).rem_euclid(2.0 * PI); + let inverted = left_angle < right_angle; + for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { + let angle = extremum.to_angle().rem_euclid(2.0 * PI); + // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. + // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. + // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. + if !inverted && angle >= right_angle && angle <= left_angle + || inverted && (angle >= right_angle || angle <= left_angle) + { + bounds.push(extremum * self.radius); + } + } + + Aabb2d::from_point_cloud(translation, 0.0, &dbg!(bounds)) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { @@ -58,43 +78,54 @@ impl Bounded2d for Arc2d { impl Bounded2d for CircularSector { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - // This is identical to the implementation for Arc2d, above, with the additional possibility of the - // origin point, the center of the arc, acting as a bounding point when the arc is minor. - // - // See comments above for an explanation of the logic. - let all_bounds = [ - Vec2::ZERO, - self.arc.left_endpoint(), - self.arc.right_endpoint(), - self.radius() * Vec2::Y, - self.radius() * Vec2::X, - self.radius() * -Vec2::X, - ]; - let bounds = if self.arc.is_major() { - &all_bounds[1..6] - } else { - &all_bounds[0..4] - }; - Aabb2d::from_point_cloud(translation, rotation, bounds) + // If our sector covers more than a circle, just return the bounding box of the circle. + if self.half_angle() >= PI { + return Circle { + radius: self.radius(), + } + .aabb_2d(translation, rotation); + } + + // Otherwise, we use the same logic as for Arc2d, above, just with the circle's cetner as an additional possibility. + // See above for discussion. + let mut bounds = SmallVec::<[Vec2; 7]>::new(); + let rotation_vec = Vec2::from_angle(rotation); + bounds.push(self.arc.left_endpoint().rotate(rotation_vec)); + bounds.push(self.arc.right_endpoint().rotate(rotation_vec)); + bounds.push(self.circle_center()); + + let left_angle = (PI / 2.0 + self.half_angle() + rotation).rem_euclid(2.0 * PI); + let right_angle = (PI / 2.0 - self.half_angle() + rotation).rem_euclid(2.0 * PI); + let inverted = left_angle < right_angle; + for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { + let angle = extremum.to_angle().rem_euclid(2.0 * PI); + if !inverted && angle >= right_angle && angle <= left_angle + || inverted && (angle <= right_angle || angle >= left_angle) + { + bounds.push(extremum * self.radius()); + } + } + + Aabb2d::from_point_cloud(translation, 0.0, &bounds) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { - // There are three possibilities for the bounding circle. 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. + // If the arc is major, that is, greater than a semicircle, + // then bounding circle is just the circle defining the sector. BoundingCircle::new(translation, self.arc.radius) - } else if self.arc.chord_length() < self.arc.radius { - // If the chord length is smaller than the radius, then the radius is the widest distance between two points, - // so the bounding circle is centered on the midpoint of the radius. - let half_radius = self.radius() / 2.0; - let center = half_radius * Vec2::Y; - 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 center = self.chord_midpoint().rotate(Vec2::from_angle(rotation)); - BoundingCircle::new(center + translation, self.half_chord_length()) + // However, when the arc is minor, + // we need our bounding circle to include both endpoints of the arc as well as the circle center. + // This means we need the circumcircle of those three points. + // The circumcircle will always have a greater curvature than the circle itself, so it will contain + // the entire circular sector. + Triangle2d::new( + self.circle_center(), + self.arc.left_endpoint(), + self.arc.right_endpoint(), + ) + .bounding_circle(translation, rotation) } } } @@ -347,13 +378,16 @@ impl Bounded2d for Capsule2d { #[cfg(test)] mod tests { + use std::f32::consts::PI; + + use approx::assert_abs_diff_eq; use glam::Vec2; use crate::{ bounding::Bounded2d, primitives::{ - Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, - Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc2d, Capsule2d, Circle, CircularSegment, Direction2d, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, }; @@ -371,6 +405,145 @@ mod tests { assert_eq!(bounding_circle.radius(), 1.0); } + #[test] + // Arcs and circular segments have the same bounding shapes so they share test cases. + fn arc_and_segment() { + struct TestCase { + name: &'static str, + arc: Arc2d, + translation: Vec2, + rotation: f32, + aabb_min: Vec2, + aabb_max: Vec2, + bounding_circle_center: Vec2, + bounding_circle_radius: f32, + } + + // The apothem of an arc covering 1/6th of a circle. + let apothem = f32::sqrt(3.0) / 2.0; + let tests = [ + TestCase { + name: "1/6th circle untransformed", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.5, apothem), + aabb_max: Vec2::new(0.5, 1.0), + bounding_circle_center: Vec2::new(0.0, apothem), + bounding_circle_radius: 0.5, + }, + TestCase { + name: "1/6th circle with radius 0.5", + arc: Arc2d::from_radians(0.5, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.25, apothem / 2.0), + aabb_max: Vec2::new(0.25, 0.5), + bounding_circle_center: Vec2::new(0.0, apothem / 2.0), + bounding_circle_radius: 0.25, + }, + TestCase { + name: "1/6th circle with radius 2.0", + arc: Arc2d::from_radians(2.0, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, 2.0 * apothem), + aabb_max: Vec2::new(1.0, 2.0), + bounding_circle_center: Vec2::new(0.0, 2.0 * apothem), + bounding_circle_radius: 1.0, + }, + TestCase { + name: "1/6th circle translated", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.5, 3.0 + apothem), + aabb_max: Vec2::new(2.5, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0 + apothem), + bounding_circle_radius: 0.5, + }, + TestCase { + name: "1/6th circle rotated", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. + rotation: PI / 6.0, + aabb_min: Vec2::new(-apothem, 0.5), + aabb_max: Vec2::new(0.0, 1.0), + // The exact coordinates here are not obvious, but can be computed by constructing + // an altitude from the midpoint of the chord to the y-axis and using the right triangle + // similarity theorem. + bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.powi(2)), + bounding_circle_radius: 0.5, + }, + TestCase { + name: "1/4er circle rotated to be axis-aligned", + arc: Arc2d::from_radians(1.0, PI / 2.0), + translation: Vec2::ZERO, + // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. + rotation: -PI / 4.0, + aabb_min: Vec2::ZERO, + aabb_max: Vec2::splat(1.0), + bounding_circle_center: Vec2::splat(0.5), + bounding_circle_radius: f32::sqrt(2.0) / 2.0, + }, + TestCase { + name: "5/6th circle untransformed", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, -apothem), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle translated", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.0, 3.0 - apothem), + aabb_max: Vec2::new(3.0, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0), + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle rotated", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. + rotation: PI / 6.0, + aabb_min: Vec2::new(-1.0, -1.0), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + ]; + + for test in tests { + println!("subtest case: {}", test.name); + let arc = test.arc; + let segment: CircularSegment = arc.clone().into(); + + let arc_aabb = arc.aabb_2d(test.translation, test.rotation); + assert_abs_diff_eq!(test.aabb_min, arc_aabb.min); + assert_abs_diff_eq!(test.aabb_max, arc_aabb.max); + let segment_aabb = segment.aabb_2d(test.translation, test.rotation); + assert_abs_diff_eq!(test.aabb_min, segment_aabb.min); + assert_abs_diff_eq!(test.aabb_max, segment_aabb.max); + + let arc_bounding_circle = arc.bounding_circle(test.translation, test.rotation); + assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center); + assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius()); + let segment_bounding_circle = segment.bounding_circle(test.translation, test.rotation); + assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center); + assert_abs_diff_eq!( + test.bounding_circle_radius, + segment_bounding_circle.radius() + ); + } + } + #[test] fn ellipse() { let ellipse = Ellipse::new(1.0, 0.5); diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 39cbe059a91b9..7c8a014a0dc01 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -189,6 +189,10 @@ const HALF_PI: f32 = PI / 2.0; /// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on /// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this /// means that the origin may not be within the `Arc2d`'s convex hull. +/// +/// **Warning:** Arcs with negative angle, or with angle greater than an entire circle, +/// are not officially supported. +/// We recommend normalizing arcs to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[doc(alias("CircularArc", "CircleArc"))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -353,6 +357,10 @@ impl Arc2d { /// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. /// To orient the sector differently, apply a rotation. /// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. +/// +/// **Warning:** Circular sectors with negative angle, or with angle greater than an entire circle, +/// are not officially supported. +/// We recommend normalizing circular sectors to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSector { @@ -483,6 +491,10 @@ impl CircularSector { /// To orient the segment differently, apply a rotation. /// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. /// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful. +/// +/// **Warning:** Circular segments with negative angle, or with angle greater than an entire circle, +/// are not officially supported. +/// We recommend normalizing circular segments to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSegment { diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs index 1ae4c047fad41..f35672b40f555 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_circular.rs @@ -2,9 +2,10 @@ use std::f32::consts::PI; -use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; -use bevy_internal::render::mesh::{ - CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder, +use bevy::{ + prelude::*, + render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, + sprite::MaterialMesh2dBundle, }; fn main() { From fa6c681dddd3f9d45d77e7bd87fa95de5baa156c Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Fri, 16 Feb 2024 16:05:23 -0500 Subject: [PATCH 13/19] Add tests to bounding shapes for CircularSector. Also draw bounding boxes on the mesh example to visually confirm correctness. --- .../src/bounding/bounded2d/primitive_impls.rs | 233 ++++++++++++++---- crates/bevy_math/src/primitives/dim2.rs | 9 +- examples/2d/mesh2d_circular.rs | 78 ++++-- 3 files changed, 249 insertions(+), 71 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index e4317174412e6..4170b4f9f459d 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -23,6 +23,37 @@ impl Bounded2d for Circle { } } +// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes. +// The return type has room for 7 points so that the CircularSector code can add an additional point. +#[inline] +fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { + // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. + // We need to compute which axis-aligned extrema are actually contained within the rotated arc. + let mut bounds = SmallVec::<[Vec2; 7]>::new(); + let rotation_vec = Vec2::from_angle(rotation); + bounds.push(arc.left_endpoint().rotate(rotation_vec)); + bounds.push(arc.right_endpoint().rotate(rotation_vec)); + + // The half-angles are measured from a starting point of π/2, being the angle of Vec::Y. + // Compute the normalized angles of the endpoints with the rotation taken into account, and then + // check if we are looking for an angle that is between or outside them. + let left_angle = (PI / 2.0 + arc.half_angle + rotation).rem_euclid(2.0 * PI); + let right_angle = (PI / 2.0 - arc.half_angle + rotation).rem_euclid(2.0 * PI); + let inverted = left_angle < right_angle; + for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { + let angle = extremum.to_angle().rem_euclid(2.0 * PI); + // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. + // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. + // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. + if !inverted && angle >= right_angle && angle <= left_angle + || inverted && (angle >= right_angle || angle <= left_angle) + { + bounds.push(extremum * arc.radius); + } + } + bounds +} + impl Bounded2d for Arc2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // If our arc covers more than a circle, just return the bounding box of the circle. @@ -33,32 +64,7 @@ impl Bounded2d for Arc2d { .aabb_2d(translation, rotation); } - // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. - // We need to compute which axis-aligned extrema are actually contained within the rotated arc. - let mut bounds = SmallVec::<[Vec2; 6]>::new(); - let rotation_vec = Vec2::from_angle(rotation); - bounds.push(self.left_endpoint().rotate(rotation_vec)); - bounds.push(self.right_endpoint().rotate(rotation_vec)); - - // The half-angles are measured from a starting point of π/2, being the angle of Vec::Y. - // Compute the normalized angles of the endpoints with the rotation taken into account, and then - // check if we are looking for an angle that is between or outside them. - let left_angle = (PI / 2.0 + self.half_angle + rotation).rem_euclid(2.0 * PI); - let right_angle = (PI / 2.0 - self.half_angle + rotation).rem_euclid(2.0 * PI); - let inverted = left_angle < right_angle; - for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { - let angle = extremum.to_angle().rem_euclid(2.0 * PI); - // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. - // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. - // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. - if !inverted && angle >= right_angle && angle <= left_angle - || inverted && (angle >= right_angle || angle <= left_angle) - { - bounds.push(extremum * self.radius); - } - } - - Aabb2d::from_point_cloud(translation, 0.0, &dbg!(bounds)) + Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation)) } fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { @@ -87,24 +93,8 @@ impl Bounded2d for CircularSector { } // Otherwise, we use the same logic as for Arc2d, above, just with the circle's cetner as an additional possibility. - // See above for discussion. - let mut bounds = SmallVec::<[Vec2; 7]>::new(); - let rotation_vec = Vec2::from_angle(rotation); - bounds.push(self.arc.left_endpoint().rotate(rotation_vec)); - bounds.push(self.arc.right_endpoint().rotate(rotation_vec)); - bounds.push(self.circle_center()); - - let left_angle = (PI / 2.0 + self.half_angle() + rotation).rem_euclid(2.0 * PI); - let right_angle = (PI / 2.0 - self.half_angle() + rotation).rem_euclid(2.0 * PI); - let inverted = left_angle < right_angle; - for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { - let angle = extremum.to_angle().rem_euclid(2.0 * PI); - if !inverted && angle >= right_angle && angle <= left_angle - || inverted && (angle <= right_angle || angle >= left_angle) - { - bounds.push(extremum * self.radius()); - } - } + let mut bounds = arc_bounding_points(self.arc, rotation); + bounds.push(Vec2::ZERO); Aabb2d::from_point_cloud(translation, 0.0, &bounds) } @@ -386,8 +376,8 @@ mod tests { use crate::{ bounding::Bounded2d, primitives::{ - Arc2d, Capsule2d, Circle, CircularSegment, Direction2d, Ellipse, Line2d, Plane2d, - Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Direction2d, Ellipse, + Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, }; @@ -422,6 +412,7 @@ mod tests { // The apothem of an arc covering 1/6th of a circle. let apothem = f32::sqrt(3.0) / 2.0; let tests = [ + // Test case: a basic minor arc TestCase { name: "1/6th circle untransformed", arc: Arc2d::from_radians(1.0, PI / 3.0), @@ -432,6 +423,7 @@ mod tests { bounding_circle_center: Vec2::new(0.0, apothem), bounding_circle_radius: 0.5, }, + // Test case: a smaller arc, verifying that radius scaling works TestCase { name: "1/6th circle with radius 0.5", arc: Arc2d::from_radians(0.5, PI / 3.0), @@ -442,6 +434,7 @@ mod tests { bounding_circle_center: Vec2::new(0.0, apothem / 2.0), bounding_circle_radius: 0.25, }, + // Test case: a larger arc, verifying that radius scaling works TestCase { name: "1/6th circle with radius 2.0", arc: Arc2d::from_radians(2.0, PI / 3.0), @@ -452,6 +445,7 @@ mod tests { bounding_circle_center: Vec2::new(0.0, 2.0 * apothem), bounding_circle_radius: 1.0, }, + // Test case: translation of a minor arc TestCase { name: "1/6th circle translated", arc: Arc2d::from_radians(1.0, PI / 3.0), @@ -462,6 +456,7 @@ mod tests { bounding_circle_center: Vec2::new(2.0, 3.0 + apothem), bounding_circle_radius: 0.5, }, + // Test case: rotation of a minor arc TestCase { name: "1/6th circle rotated", arc: Arc2d::from_radians(1.0, PI / 3.0), @@ -476,6 +471,7 @@ mod tests { bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.powi(2)), bounding_circle_radius: 0.5, }, + // Test case: handling of axis-aligned extrema TestCase { name: "1/4er circle rotated to be axis-aligned", arc: Arc2d::from_radians(1.0, PI / 2.0), @@ -487,6 +483,7 @@ mod tests { bounding_circle_center: Vec2::splat(0.5), bounding_circle_radius: f32::sqrt(2.0) / 2.0, }, + // Test case: a basic major arc TestCase { name: "5/6th circle untransformed", arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), @@ -497,6 +494,7 @@ mod tests { bounding_circle_center: Vec2::ZERO, bounding_circle_radius: 1.0, }, + // Test case: a translated major arc TestCase { name: "5/6th circle translated", arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), @@ -507,6 +505,7 @@ mod tests { bounding_circle_center: Vec2::new(2.0, 3.0), bounding_circle_radius: 1.0, }, + // Test case: a rotated major arc, with inverted left/right angles TestCase { name: "5/6th circle rotated", arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), @@ -544,6 +543,147 @@ mod tests { } } + #[test] + fn circular_sector() { + struct TestCase { + name: &'static str, + arc: Arc2d, + translation: Vec2, + rotation: f32, + aabb_min: Vec2, + aabb_max: Vec2, + bounding_circle_center: Vec2, + bounding_circle_radius: f32, + } + + // The apothem of an arc covering 1/6th of a circle. + let apothem = f32::sqrt(3.0) / 2.0; + let inv_sqrt_3 = f32::sqrt(3.0).recip(); + let tests = [ + // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center + TestCase { + name: "1/3rd circle", + arc: Arc2d::from_radians(1.0, 2.0 * PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-apothem, 0.0), + aabb_max: Vec2::new(apothem, 1.0), + bounding_circle_center: Vec2::new(0.0, 0.5), + bounding_circle_radius: apothem, + }, + // The remaining test cases are selected as for arc_and_segment. + TestCase { + name: "1/6th circle untransformed", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.5, 0.0), + aabb_max: Vec2::new(0.5, 1.0), + // The bounding circle is a circumcircle of an equilateral triangle with side length 1. + // The distance from the corner to the center of such a triangle is 1/sqrt(3). + bounding_circle_center: Vec2::new(0.0, inv_sqrt_3), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/6th circle with radius 0.5", + arc: Arc2d::from_radians(0.5, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-0.25, 0.0), + aabb_max: Vec2::new(0.25, 0.5), + bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0), + bounding_circle_radius: inv_sqrt_3 / 2.0, + }, + TestCase { + name: "1/6th circle with radius 2.0", + arc: Arc2d::from_radians(2.0, PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, 0.0), + aabb_max: Vec2::new(1.0, 2.0), + bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3), + bounding_circle_radius: 2.0 * inv_sqrt_3, + }, + TestCase { + name: "1/6th circle translated", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.5, 3.0), + aabb_max: Vec2::new(2.5, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/6th circle rotated", + arc: Arc2d::from_radians(1.0, PI / 3.0), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. + rotation: PI / 6.0, + aabb_min: Vec2::new(-apothem, 0.0), + aabb_max: Vec2::new(0.0, 1.0), + // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2. + bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5), + bounding_circle_radius: inv_sqrt_3, + }, + TestCase { + name: "1/4er circle rotated to be axis-aligned", + arc: Arc2d::from_radians(1.0, PI / 2.0), + translation: Vec2::ZERO, + // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. + rotation: -PI / 4.0, + aabb_min: Vec2::ZERO, + aabb_max: Vec2::splat(1.0), + bounding_circle_center: Vec2::splat(0.5), + bounding_circle_radius: f32::sqrt(2.0) / 2.0, + }, + TestCase { + name: "5/6th circle untransformed", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::ZERO, + rotation: 0.0, + aabb_min: Vec2::new(-1.0, -apothem), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle translated", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::new(2.0, 3.0), + rotation: 0.0, + aabb_min: Vec2::new(1.0, 3.0 - apothem), + aabb_max: Vec2::new(3.0, 4.0), + bounding_circle_center: Vec2::new(2.0, 3.0), + bounding_circle_radius: 1.0, + }, + TestCase { + name: "5/6th circle rotated", + arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + translation: Vec2::ZERO, + // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. + rotation: PI / 6.0, + aabb_min: Vec2::new(-1.0, -1.0), + aabb_max: Vec2::new(1.0, 1.0), + bounding_circle_center: Vec2::ZERO, + bounding_circle_radius: 1.0, + }, + ]; + + for test in tests { + println!("subtest case: {}", test.name); + let sector: CircularSector = test.arc.into(); + + let aabb = sector.aabb_2d(test.translation, test.rotation); + assert_abs_diff_eq!(test.aabb_min, aabb.min); + assert_abs_diff_eq!(test.aabb_max, aabb.max); + + let bounding_circle = sector.bounding_circle(test.translation, test.rotation); + assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center); + assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius()); + } + } + #[test] fn ellipse() { let ellipse = Ellipse::new(1.0, 0.5); @@ -575,6 +715,7 @@ mod tests { assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0); + dbg!(bounding_circle); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 7c8a014a0dc01..4977c4368b902 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -190,8 +190,7 @@ const HALF_PI: f32 = PI / 2.0; /// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this /// means that the origin may not be within the `Arc2d`'s convex hull. /// -/// **Warning:** Arcs with negative angle, or with angle greater than an entire circle, -/// are not officially supported. +/// **Warning:** Arcs with negative angle or radius, or with angle greater than an entire circle, are not officially supported. /// We recommend normalizing arcs to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[doc(alias("CircularArc", "CircleArc"))] @@ -358,8 +357,7 @@ impl Arc2d { /// To orient the sector differently, apply a rotation. /// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. /// -/// **Warning:** Circular sectors with negative angle, or with angle greater than an entire circle, -/// are not officially supported. +/// **Warning:** Circular sectors with negative angle or radius, or with angle greater than an entire circle, are not officially supported. /// We recommend normalizing circular sectors to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -492,8 +490,7 @@ impl CircularSector { /// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`]. /// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful. /// -/// **Warning:** Circular segments with negative angle, or with angle greater than an entire circle, -/// are not officially supported. +/// **Warning:** Circular segments with negative angle or radius, or with angle greater than an entire circle, are not officially supported. /// We recommend normalizing circular segments to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs index f35672b40f555..1dcc22992857b 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_circular.rs @@ -1,5 +1,6 @@ //! Demonstrates UV mappings of the [`CircularSector`] and [`CircularSegment`] primitives. - +//! +//! Also draws the bounding boxes and circles of the primitives. use std::f32::consts::PI; use bevy::{ @@ -7,14 +8,25 @@ use bevy::{ render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, sprite::MaterialMesh2dBundle, }; +use bevy_internal::math::bounding::{Bounded2d, BoundingVolume}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) + .add_systems( + Update, + ( + draw_bounds::, + draw_bounds::, + ), + ) .run(); } +#[derive(Component, Debug)] +struct DrawBounds(Shape); + fn setup( mut commands: Commands, asset_server: Res, @@ -47,19 +59,22 @@ fn setup( // We must rotate it both in the Transform and in the mesh's UV mappings. let sector_angle = -sector.half_angle(); let sector_mesh = - CircularSectorMeshBuilder::new(sector).uv_mode(CircularMeshUvMode::Mask { + CircularSectorMeshBuilder::new(sector.clone()).uv_mode(CircularMeshUvMode::Mask { angle: sector_angle, }); - commands.spawn(MaterialMesh2dBundle { - mesh: meshes.add(sector_mesh).into(), - material: material.clone(), - transform: Transform { - translation: Vec3::new(FIRST_X + OFFSET * i as f32, 2.0 * UPPER_Y, 0.0), - rotation: Quat::from_rotation_z(sector_angle), + commands.spawn(( + MaterialMesh2dBundle { + mesh: meshes.add(sector_mesh).into(), + material: material.clone(), + transform: Transform { + translation: Vec3::new(FIRST_X + OFFSET * i as f32, 2.0 * UPPER_Y, 0.0), + rotation: Quat::from_rotation_z(sector_angle), + ..default() + }, ..default() }, - ..default() - }); + DrawBounds(sector), + )); let segment = CircularSegment::from_fraction(40.0, fraction); // For the circular segment, we will draw Bevy charging forward, which requires rotating the @@ -71,18 +86,43 @@ fn setup( // so it is the negative of what you might otherwise expect. let segment_angle = -PI / 2.0; let segment_mesh = - CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask { + CircularSegmentMeshBuilder::new(segment.clone()).uv_mode(CircularMeshUvMode::Mask { angle: -segment_angle, }); - commands.spawn(MaterialMesh2dBundle { - mesh: meshes.add(segment_mesh).into(), - material: material.clone(), - transform: Transform { - translation: Vec3::new(FIRST_X + OFFSET * i as f32, LOWER_Y, 0.0), - rotation: Quat::from_rotation_z(segment_angle), + commands.spawn(( + MaterialMesh2dBundle { + mesh: meshes.add(segment_mesh).into(), + material: material.clone(), + transform: Transform { + translation: Vec3::new(FIRST_X + OFFSET * i as f32, LOWER_Y, 0.0), + rotation: Quat::from_rotation_z(segment_angle), + ..default() + }, ..default() }, - ..default() - }); + DrawBounds(segment), + )); + } +} + +fn draw_bounds( + q: Query<(&DrawBounds, &GlobalTransform)>, + mut gizmos: Gizmos, +) { + for (shape, transform) in &q { + let (_, rotation, translation) = transform.to_scale_rotation_translation(); + let translation = translation.truncate(); + let rotation = rotation.to_euler(EulerRot::XYZ).2; + + let aabb = shape.0.aabb_2d(translation, rotation); + gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, Color::RED); + + let bounding_circle = shape.0.bounding_circle(translation, rotation); + dbg!(bounding_circle); + gizmos.circle_2d( + bounding_circle.center, + bounding_circle.radius(), + Color::BLUE, + ); } } From efb05153a728ccc95d7b14a0d86b4682ba1e9a10 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Fri, 16 Feb 2024 16:22:12 -0500 Subject: [PATCH 14/19] Silence clippy and CI. Every suggestion for the boolean condition is less readable. --- .../bevy_math/src/bounding/bounded2d/primitive_impls.rs | 8 ++++---- examples/2d/mesh2d_circular.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 4170b4f9f459d..091d84a22a326 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -45,6 +45,7 @@ fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. + #[allow(clippy::nonminimal_bool)] if !inverted && angle >= right_angle && angle <= left_angle || inverted && (angle >= right_angle || angle <= left_angle) { @@ -521,17 +522,16 @@ mod tests { for test in tests { println!("subtest case: {}", test.name); - let arc = test.arc; - let segment: CircularSegment = arc.clone().into(); + let segment: CircularSegment = test.arc.into(); - let arc_aabb = arc.aabb_2d(test.translation, test.rotation); + let arc_aabb = test.arc.aabb_2d(test.translation, test.rotation); assert_abs_diff_eq!(test.aabb_min, arc_aabb.min); assert_abs_diff_eq!(test.aabb_max, arc_aabb.max); let segment_aabb = segment.aabb_2d(test.translation, test.rotation); assert_abs_diff_eq!(test.aabb_min, segment_aabb.min); assert_abs_diff_eq!(test.aabb_max, segment_aabb.max); - let arc_bounding_circle = arc.bounding_circle(test.translation, test.rotation); + let arc_bounding_circle = test.arc.bounding_circle(test.translation, test.rotation); assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center); assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius()); let segment_bounding_circle = segment.bounding_circle(test.translation, test.rotation); diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs index 1dcc22992857b..d83027035c129 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_circular.rs @@ -4,11 +4,11 @@ use std::f32::consts::PI; use bevy::{ + math::bounding::{Bounded2d, BoundingVolume}, prelude::*, render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, sprite::MaterialMesh2dBundle, }; -use bevy_internal::math::bounding::{Bounded2d, BoundingVolume}; fn main() { App::new() @@ -59,7 +59,7 @@ fn setup( // We must rotate it both in the Transform and in the mesh's UV mappings. let sector_angle = -sector.half_angle(); let sector_mesh = - CircularSectorMeshBuilder::new(sector.clone()).uv_mode(CircularMeshUvMode::Mask { + CircularSectorMeshBuilder::new(sector).uv_mode(CircularMeshUvMode::Mask { angle: sector_angle, }); commands.spawn(( @@ -86,7 +86,7 @@ fn setup( // so it is the negative of what you might otherwise expect. let segment_angle = -PI / 2.0; let segment_mesh = - CircularSegmentMeshBuilder::new(segment.clone()).uv_mode(CircularMeshUvMode::Mask { + CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask { angle: -segment_angle, }); commands.spawn(( From 552aea5dd1c98c27f7f56203a03d9591a954fe32 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" <118812919+spectria-limina@users.noreply.github.com> Date: Sat, 9 Mar 2024 03:35:46 -0500 Subject: [PATCH 15/19] Apply suggestions from code review Co-authored-by: Joona Aalto Co-authored-by: Alice Cecile --- .../src/bounding/bounded2d/primitive_impls.rs | 14 ++++--------- crates/bevy_math/src/primitives/dim2.rs | 20 +++++++++---------- .../bevy_render/src/mesh/primitives/dim2.rs | 6 +++--- examples/2d/mesh2d_circular.rs | 1 - 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 414cc5875fa3d..fca5d55a39d79 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -37,7 +37,7 @@ fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { bounds.push(arc.left_endpoint().rotate(rotation_vec)); bounds.push(arc.right_endpoint().rotate(rotation_vec)); - // The half-angles are measured from a starting point of π/2, being the angle of Vec::Y. + // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. // Compute the normalized angles of the endpoints with the rotation taken into account, and then // check if we are looking for an angle that is between or outside them. let left_angle = (PI / 2.0 + arc.half_angle + rotation).rem_euclid(2.0 * PI); @@ -62,10 +62,7 @@ impl Bounded2d for Arc2d { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // If our arc covers more than a circle, just return the bounding box of the circle. if self.half_angle >= PI { - return Circle { - radius: self.radius, - } - .aabb_2d(translation, rotation); + return Circle::new(self.radius).aabb_2d(translation, rotation); } Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation)) @@ -90,13 +87,10 @@ impl Bounded2d for CircularSector { fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { // If our sector covers more than a circle, just return the bounding box of the circle. if self.half_angle() >= PI { - return Circle { - radius: self.radius(), - } - .aabb_2d(translation, rotation); + return Circle::new(self.radius()).aabb_2d(translation, rotation); } - // Otherwise, we use the same logic as for Arc2d, above, just with the circle's cetner as an additional possibility. + // Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility. let mut bounds = arc_bounding_points(self.arc, rotation); bounds.push(Vec2::ZERO); diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 30f951a63c84d..b7a3b135e540b 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -71,16 +71,16 @@ const HALF_PI: f32 = PI / 2.0; /// /// An arc has no area. /// If you want to include the portion of a circle's area swept out by the arc, -/// use [`CircularSector`]. +/// use the pie-shaped [`CircularSector`]. /// If you want to include only the space inside the convex hull of the arc, -/// use [`CircularSegment`]. +/// use the bowl-shaped [`CircularSegment`]. /// /// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on /// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this /// means that the origin may not be within the `Arc2d`'s convex hull. /// /// **Warning:** Arcs with negative angle or radius, or with angle greater than an entire circle, are not officially supported. -/// We recommend normalizing arcs to have an angle in [0, 2π]. +/// It is recommended to normalize arcs to have an angle in [0, 2π]. #[derive(Clone, Copy, Debug, PartialEq)] #[doc(alias("CircularArc", "CircleArc"))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -103,7 +103,7 @@ impl Default for Arc2d { } impl Arc2d { - /// Create a new [`Arc2d`] from a `radius`, and a `half_angle` + /// Create a new [`Arc2d`] from a `radius` and a `half_angle` #[inline(always)] pub fn new(radius: f32, half_angle: f32) -> Self { Self { radius, half_angle } @@ -258,7 +258,7 @@ pub struct CircularSector { impl Primitive2d for CircularSector {} impl Default for CircularSector { - /// Returns the default [`CircularSector`] with radius `0.5` and covering a quarter circle + /// Returns the default [`CircularSector`] with radius `0.5` and covering a third of a circle fn default() -> Self { Self::from(Arc2d::default()) } @@ -271,7 +271,7 @@ impl From for CircularSector { } 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 fn new(radius: f32, angle: f32) -> Self { Self::from(Arc2d::new(radius, angle)) @@ -391,7 +391,7 @@ pub struct CircularSegment { impl Primitive2d for CircularSegment {} impl Default for CircularSegment { - /// Returns the default [`CircularSegment`] with radius `0.5` and covering a quarter circle. + /// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle. fn default() -> Self { Self::from(Arc2d::default()) } @@ -462,14 +462,14 @@ impl CircularSegment { Vec2::ZERO } - /// Get half the length of the chord of the segment, which is the segment's base + /// Get half the length of the chord, which is the segment's base #[inline(always)] #[doc(alias = "half_base_length")] pub fn half_chord_length(&self) -> f32 { self.arc.half_chord_length() } - /// Get the length of the chord of the segment, which is the segment's base + /// Get the length of the chord, which is the segment's base #[inline(always)] #[doc(alias = "base_length")] #[doc(alias = "base")] @@ -477,7 +477,7 @@ impl CircularSegment { self.arc.chord_length() } - /// Get the midpoint of the chord of the segment, which is the segment's base + /// Get the midpoint of the chord, which is the segment's base #[inline(always)] #[doc(alias = "base_midpoint")] pub fn chord_midpoint(&self) -> Vec2 { diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 2e0bc1fa90cb6..974b9ea37f988 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -82,7 +82,7 @@ impl From for Mesh { } } -/// Specifies how to generate UV-mappings for [`CircularSector`] and [`CircularSegment`] shapes. +/// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. #[derive(Copy, Clone, Debug, PartialEq)] pub enum CircularMeshUvMode { /// Treats the shape as a mask over a circle of equal size and radius, @@ -102,7 +102,7 @@ impl Default for CircularMeshUvMode { /// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape. /// /// The resulting mesh will have a UV-map such that the center of the circle is -/// at the centure of the texture. +/// at the center of the texture. #[derive(Clone, Debug)] pub struct CircularSectorMeshBuilder { /// The sector shape. @@ -225,7 +225,7 @@ impl From for Mesh { /// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. /// /// The resulting mesh will have a UV-map such that the center of the circle is -/// at the centure of the texture. +/// at the center of the texture. #[derive(Clone, Copy, Debug)] pub struct CircularSegmentMeshBuilder { /// The segment shape. diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_circular.rs index d83027035c129..af203b1e1560f 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_circular.rs @@ -118,7 +118,6 @@ fn draw_bounds( gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, Color::RED); let bounding_circle = shape.0.bounding_circle(translation, rotation); - dbg!(bounding_circle); gizmos.circle_2d( bounding_circle.center, bounding_circle.radius(), From 9353ad0eb95314d6d95f88b1ccfb893f5c3f88db Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Sat, 9 Mar 2024 03:42:14 -0500 Subject: [PATCH 16/19] Review changes --- Cargo.toml | 8 +++---- .../src/bounding/bounded2d/primitive_impls.rs | 24 +++++++++---------- .../bevy_render/src/mesh/primitives/dim2.rs | 9 +++++++ .../2d/{mesh2d_circular.rs => mesh2d_arcs.rs} | 11 ++++----- 4 files changed, 28 insertions(+), 24 deletions(-) rename examples/2d/{mesh2d_circular.rs => mesh2d_arcs.rs} (95%) diff --git a/Cargo.toml b/Cargo.toml index 33c7a8dc3c7a0..d7ff6b4253ef3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -404,13 +404,13 @@ category = "2D Rendering" wasm = true [[example]] -name = "mesh2d_circular" -path = "examples/2d/mesh2d_circular.rs" +name = "mesh2d_arcs" +path = "examples/2d/mesh2d_arcs.rs" doc-scrape-examples = true [package.metadata.example.mesh2d_circular] -name = "Circular 2D Meshes" -description = "Demonstrates UV-mapping of circular primitives" +name = "Arc 2D Meshes" +description = "Demonstrates UV-mapping of the circular segment and sector primitives" category = "2D Rendering" wasm = true diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index fca5d55a39d79..926c3db48e24b 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -1,15 +1,14 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). -use std::f32::consts::PI; +use std::f32::consts::{FRAC_PI_2, PI, TAU}; -use glam::{Mat2, Vec2}; use smallvec::SmallVec; use crate::{ primitives::{ - BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment, - Ellipse, Line2d, Plane2d, Polygon, - Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment, + Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, + Triangle2d, }, Dir2, Mat2, Vec2, }; @@ -40,11 +39,11 @@ fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. // Compute the normalized angles of the endpoints with the rotation taken into account, and then // check if we are looking for an angle that is between or outside them. - let left_angle = (PI / 2.0 + arc.half_angle + rotation).rem_euclid(2.0 * PI); - let right_angle = (PI / 2.0 - arc.half_angle + rotation).rem_euclid(2.0 * PI); + let left_angle = (FRAC_PI_2 + arc.half_angle + rotation).rem_euclid(TAU); + let right_angle = (FRAC_PI_2 - arc.half_angle + rotation).rem_euclid(TAU); let inverted = left_angle < right_angle; for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { - let angle = extremum.to_angle().rem_euclid(2.0 * PI); + let angle = extremum.to_angle().rem_euclid(TAU); // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. @@ -366,7 +365,7 @@ impl Bounded2d for Capsule2d { #[cfg(test)] mod tests { - use std::f32::consts::PI; + use std::f32::consts::{PI, TAU}; use approx::assert_abs_diff_eq; use glam::Vec2; @@ -374,8 +373,8 @@ mod tests { use crate::{ bounding::Bounded2d, primitives::{ - Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Direction2d, Ellipse, - Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, + Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, Dir2, }; @@ -561,7 +560,7 @@ mod tests { // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center TestCase { name: "1/3rd circle", - arc: Arc2d::from_radians(1.0, 2.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, TAU / 3.0), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-apothem, 0.0), @@ -713,7 +712,6 @@ mod tests { assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0); - dbg!(bounding_circle); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 974b9ea37f988..e6e284e3790ec 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -83,7 +83,16 @@ impl From for Mesh { } /// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. +/// +/// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes +/// the entire circle, particularly the same texture will be displayed with different fractions of a +/// complete circle. +/// +/// It's expected that more will be added in the future, such as a variant that causes the texture to be +/// scaled to fit the bounding box of the shape, which would be good for packed textures only including the +/// portion of the circle that is needed to display. #[derive(Copy, Clone, Debug, PartialEq)] +#[non_exhaustive] pub enum CircularMeshUvMode { /// Treats the shape as a mask over a circle of equal size and radius, /// with the center of the circle at the center of the texture. diff --git a/examples/2d/mesh2d_circular.rs b/examples/2d/mesh2d_arcs.rs similarity index 95% rename from examples/2d/mesh2d_circular.rs rename to examples/2d/mesh2d_arcs.rs index af203b1e1560f..812fd51be56af 100644 --- a/examples/2d/mesh2d_circular.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -4,6 +4,7 @@ use std::f32::consts::PI; use bevy::{ + color::palettes::css::{BLUE, DARK_SLATE_GREY, RED}, math::bounding::{Bounded2d, BoundingVolume}, prelude::*, render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, @@ -37,7 +38,7 @@ fn setup( commands.spawn(Camera2dBundle { camera: Camera { - clear_color: ClearColorConfig::Custom(Color::DARK_GRAY), + clear_color: ClearColorConfig::Custom(DARK_SLATE_GREY.into()), ..default() }, ..default() @@ -115,13 +116,9 @@ fn draw_bounds( let rotation = rotation.to_euler(EulerRot::XYZ).2; let aabb = shape.0.aabb_2d(translation, rotation); - gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, Color::RED); + gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, RED); let bounding_circle = shape.0.bounding_circle(translation, rotation); - gizmos.circle_2d( - bounding_circle.center, - bounding_circle.radius(), - Color::BLUE, - ); + gizmos.circle_2d(bounding_circle.center, bounding_circle.radius(), BLUE); } } From 1c31b1a19f85e773234c6be91a73ffa033fcbd78 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Sun, 24 Mar 2024 17:54:45 +0900 Subject: [PATCH 17/19] More review comments --- Cargo.toml | 2 +- .../src/bounding/bounded2d/primitive_impls.rs | 52 ++++---- crates/bevy_math/src/primitives/dim2.rs | 122 +++++++----------- .../bevy_render/src/mesh/primitives/dim2.rs | 12 +- examples/2d/2d_shapes.rs | 38 +++--- examples/2d/mesh2d_arcs.rs | 4 +- examples/README.md | 4 +- 7 files changed, 103 insertions(+), 131 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7ff6b4253ef3..17dcc35c7c502 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -408,7 +408,7 @@ name = "mesh2d_arcs" path = "examples/2d/mesh2d_arcs.rs" doc-scrape-examples = true -[package.metadata.example.mesh2d_circular] +[package.metadata.example.mesh2d_arcs] name = "Arc 2D Meshes" description = "Demonstrates UV-mapping of the circular segment and sector primitives" category = "2D Rendering" diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 926c3db48e24b..062c9d9d9d7bb 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -108,7 +108,7 @@ impl Bounded2d for CircularSector { // The circumcircle will always have a greater curvature than the circle itself, so it will contain // the entire circular sector. Triangle2d::new( - self.circle_center(), + Vec2::ZERO, self.arc.left_endpoint(), self.arc.right_endpoint(), ) @@ -365,7 +365,7 @@ impl Bounded2d for Capsule2d { #[cfg(test)] mod tests { - use std::f32::consts::{PI, TAU}; + use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU}; use approx::assert_abs_diff_eq; use glam::Vec2; @@ -413,7 +413,7 @@ mod tests { // Test case: a basic minor arc TestCase { name: "1/6th circle untransformed", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-0.5, apothem), @@ -424,7 +424,7 @@ mod tests { // Test case: a smaller arc, verifying that radius scaling works TestCase { name: "1/6th circle with radius 0.5", - arc: Arc2d::from_radians(0.5, PI / 3.0), + arc: Arc2d::from_radians(0.5, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-0.25, apothem / 2.0), @@ -435,7 +435,7 @@ mod tests { // Test case: a larger arc, verifying that radius scaling works TestCase { name: "1/6th circle with radius 2.0", - arc: Arc2d::from_radians(2.0, PI / 3.0), + arc: Arc2d::from_radians(2.0, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-1.0, 2.0 * apothem), @@ -446,7 +446,7 @@ mod tests { // Test case: translation of a minor arc TestCase { name: "1/6th circle translated", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::new(2.0, 3.0), rotation: 0.0, aabb_min: Vec2::new(1.5, 3.0 + apothem), @@ -457,10 +457,10 @@ mod tests { // Test case: rotation of a minor arc TestCase { name: "1/6th circle rotated", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::ZERO, // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. - rotation: PI / 6.0, + rotation: FRAC_PI_6, aabb_min: Vec2::new(-apothem, 0.5), aabb_max: Vec2::new(0.0, 1.0), // The exact coordinates here are not obvious, but can be computed by constructing @@ -472,10 +472,10 @@ mod tests { // Test case: handling of axis-aligned extrema TestCase { name: "1/4er circle rotated to be axis-aligned", - arc: Arc2d::from_radians(1.0, PI / 2.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_2), translation: Vec2::ZERO, // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. - rotation: -PI / 4.0, + rotation: -FRAC_PI_4, aabb_min: Vec2::ZERO, aabb_max: Vec2::splat(1.0), bounding_circle_center: Vec2::splat(0.5), @@ -484,7 +484,7 @@ mod tests { // Test case: a basic major arc TestCase { name: "5/6th circle untransformed", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-1.0, -apothem), @@ -495,7 +495,7 @@ mod tests { // Test case: a translated major arc TestCase { name: "5/6th circle translated", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::new(2.0, 3.0), rotation: 0.0, aabb_min: Vec2::new(1.0, 3.0 - apothem), @@ -506,10 +506,10 @@ mod tests { // Test case: a rotated major arc, with inverted left/right angles TestCase { name: "5/6th circle rotated", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::ZERO, // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. - rotation: PI / 6.0, + rotation: FRAC_PI_6, aabb_min: Vec2::new(-1.0, -1.0), aabb_max: Vec2::new(1.0, 1.0), bounding_circle_center: Vec2::ZERO, @@ -571,7 +571,7 @@ mod tests { // The remaining test cases are selected as for arc_and_segment. TestCase { name: "1/6th circle untransformed", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-0.5, 0.0), @@ -583,7 +583,7 @@ mod tests { }, TestCase { name: "1/6th circle with radius 0.5", - arc: Arc2d::from_radians(0.5, PI / 3.0), + arc: Arc2d::from_radians(0.5, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-0.25, 0.0), @@ -593,7 +593,7 @@ mod tests { }, TestCase { name: "1/6th circle with radius 2.0", - arc: Arc2d::from_radians(2.0, PI / 3.0), + arc: Arc2d::from_radians(2.0, FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-1.0, 0.0), @@ -603,7 +603,7 @@ mod tests { }, TestCase { name: "1/6th circle translated", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::new(2.0, 3.0), rotation: 0.0, aabb_min: Vec2::new(1.5, 3.0), @@ -613,10 +613,10 @@ mod tests { }, TestCase { name: "1/6th circle rotated", - arc: Arc2d::from_radians(1.0, PI / 3.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_3), translation: Vec2::ZERO, // Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis. - rotation: PI / 6.0, + rotation: FRAC_PI_6, aabb_min: Vec2::new(-apothem, 0.0), aabb_max: Vec2::new(0.0, 1.0), // The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2. @@ -625,10 +625,10 @@ mod tests { }, TestCase { name: "1/4er circle rotated to be axis-aligned", - arc: Arc2d::from_radians(1.0, PI / 2.0), + arc: Arc2d::from_radians(1.0, FRAC_PI_2), translation: Vec2::ZERO, // Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis. - rotation: -PI / 4.0, + rotation: -FRAC_PI_4, aabb_min: Vec2::ZERO, aabb_max: Vec2::splat(1.0), bounding_circle_center: Vec2::splat(0.5), @@ -636,7 +636,7 @@ mod tests { }, TestCase { name: "5/6th circle untransformed", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::ZERO, rotation: 0.0, aabb_min: Vec2::new(-1.0, -apothem), @@ -646,7 +646,7 @@ mod tests { }, TestCase { name: "5/6th circle translated", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::new(2.0, 3.0), rotation: 0.0, aabb_min: Vec2::new(1.0, 3.0 - apothem), @@ -656,10 +656,10 @@ mod tests { }, TestCase { name: "5/6th circle rotated", - arc: Arc2d::from_radians(1.0, 5.0 * PI / 3.0), + arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3), translation: Vec2::ZERO, // Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis. - rotation: PI / 6.0, + rotation: FRAC_PI_6, aabb_min: Vec2::new(-1.0, -1.0), aabb_max: Vec2::new(1.0, 1.0), bounding_circle_center: Vec2::ZERO, diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index b7a3b135e540b..cac403b290a3a 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,4 +1,4 @@ -use std::f32::consts::PI; +use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI}; use super::{Primitive2d, WindingOrder}; use crate::{Dir2, Vec2}; @@ -65,8 +65,6 @@ impl Circle { } } -const HALF_PI: f32 = PI / 2.0; - /// A primitive representing an arc between two points on a circle. /// /// An arc has no area. @@ -87,17 +85,17 @@ const HALF_PI: f32 = PI / 2.0; pub struct Arc2d { /// The radius of the circle pub radius: f32, - /// Half the angle subtended by the arc. + /// Half the angle defining the arc pub half_angle: f32, } impl Primitive2d for Arc2d {} impl Default for Arc2d { - /// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle. + /// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle fn default() -> Self { Self { radius: 0.5, - half_angle: 2.0 * PI / 3.0, + half_angle: 2.0 * FRAC_PI_3, } } } @@ -109,7 +107,7 @@ impl Arc2d { Self { radius, half_angle } } - /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians. + /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians #[inline(always)] pub fn from_radians(radius: f32, angle: f32) -> Self { Self { @@ -127,9 +125,9 @@ impl Arc2d { } } - /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a circle. + /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a circle /// - /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle #[inline(always)] pub fn from_fraction(radius: f32, fraction: f32) -> Self { Self { @@ -150,24 +148,16 @@ impl Arc2d { self.angle() * self.radius } - /// Get the center of the circle defining the arc - /// - /// Always returns `Vec2::ZERO` - #[inline(always)] - pub fn circle_center(&self) -> Vec2 { - Vec2::ZERO - } - /// Get the right-hand end point of the arc #[inline(always)] pub fn right_endpoint(&self) -> Vec2 { - self.radius * Vec2::from_angle(HALF_PI - self.half_angle) + self.radius * Vec2::from_angle(FRAC_PI_2 - self.half_angle) } /// Get the left-hand end point of the arc #[inline(always)] pub fn left_endpoint(&self) -> Vec2 { - self.radius * Vec2::from_angle(HALF_PI + self.half_angle) + self.radius * Vec2::from_angle(FRAC_PI_2 + self.half_angle) } /// Get the endpoints of the arc @@ -182,19 +172,19 @@ impl Arc2d { self.radius * Vec2::Y } - /// Get half the length of the chord subtended by the arc + /// Get half the distance between the endpoints (half the length of the chord) #[inline(always)] pub fn half_chord_length(&self) -> f32 { self.radius * f32::sin(self.half_angle) } - /// Get the length of the chord subtended by the arc + /// Get the distance between the endpoints (the length of the chord) #[inline(always)] pub fn chord_length(&self) -> f32 { 2.0 * self.half_chord_length() } - /// Get the midpoint of the chord subtended by the arc + /// Get the midpoint of the two endpoints (the midpoint of the chord) #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { self.apothem() * Vec2::Y @@ -228,7 +218,7 @@ impl Arc2d { /// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor. #[inline(always)] pub fn is_minor(&self) -> bool { - self.half_angle <= HALF_PI + self.half_angle <= FRAC_PI_2 } /// Produces true if the arc is at least half a circle. @@ -236,7 +226,7 @@ impl Arc2d { /// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor. #[inline(always)] pub fn is_major(&self) -> bool { - self.half_angle >= HALF_PI + self.half_angle >= FRAC_PI_2 } } @@ -251,7 +241,7 @@ impl Arc2d { #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSector { - /// The arc subtending the sector + /// The arc defining the sector #[cfg_attr(feature = "serialize", serde(flatten))] pub arc: Arc2d, } @@ -315,34 +305,31 @@ impl CircularSector { self.arc.radius } - /// Get the length of the arc subtending the sector + /// Get the length of the arc defining the sector #[inline(always)] pub fn arc_length(&self) -> f32 { self.arc.length() } - /// Get the center of the circle defining the sector, corresponding to the apex of the sector + /// Get half the length of the chord defined by the sector /// - /// Always returns `Vec2::ZERO` - #[inline(always)] - #[doc(alias = "apex")] - pub fn circle_center(&self) -> Vec2 { - Vec2::ZERO - } - - /// Get half the length of the chord subtended by the sector + /// See [`Arc2d::half_chord_length`] #[inline(always)] pub fn half_chord_length(&self) -> f32 { self.arc.half_chord_length() } - /// Get the length of the chord subtended by the sector + /// Get the length of the chord defined by the sector + /// + /// See [`Arc2d::chord_length`] #[inline(always)] pub fn chord_length(&self) -> f32 { self.arc.chord_length() } - /// Get the midpoint of the chord subtended by the sector + /// Get the midpoint of the chord defined by the sector + /// + /// See [`Arc2d::chord_midpoint`] #[inline(always)] pub fn chord_midpoint(&self) -> Vec2 { self.arc.chord_midpoint() @@ -384,14 +371,14 @@ impl CircularSector { #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct CircularSegment { - /// The arc subtending the segment + /// The arc defining the segment #[cfg_attr(feature = "serialize", serde(flatten))] pub arc: Arc2d, } impl Primitive2d for CircularSegment {} impl Default for CircularSegment { - /// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle. + /// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle fn default() -> Self { Self::from(Arc2d::default()) } @@ -416,7 +403,7 @@ impl CircularSegment { Self::from(Arc2d::from_radians(radius, angle)) } - /// Create a new [`CircularSegment`] from a `radius` and an angle in `degrees`. + /// Create a new [`CircularSegment`] from a `radius` and an angle in `degrees` #[inline(always)] pub fn from_degrees(radius: f32, degrees: f32) -> Self { Self::from(Arc2d::from_degrees(radius, degrees)) @@ -448,28 +435,20 @@ impl CircularSegment { self.arc.radius } - /// Get the length of the arc subtending the segment + /// Get the length of the arc defining the segment #[inline(always)] pub fn arc_length(&self) -> f32 { self.arc.length() } - /// Get the center of the circle defining the segment - /// - /// Always returns `Vec2::ZERO` - #[inline(always)] - pub fn circle_center(&self) -> Vec2 { - Vec2::ZERO - } - - /// Get half the length of the chord, which is the segment's base + /// Get half the length of the segment's base, also known as its chord #[inline(always)] #[doc(alias = "half_base_length")] pub fn half_chord_length(&self) -> f32 { self.arc.half_chord_length() } - /// Get the length of the chord, which is the segment's base + /// Get the length of the segment's base, also known as its chord #[inline(always)] #[doc(alias = "base_length")] #[doc(alias = "base")] @@ -477,7 +456,7 @@ impl CircularSegment { self.arc.chord_length() } - /// Get the midpoint of the chord, which is the segment's base + /// Get the midpoint of the segment's base, also known as its chord #[inline(always)] #[doc(alias = "base_midpoint")] pub fn chord_midpoint(&self) -> Vec2 { @@ -485,7 +464,7 @@ impl CircularSegment { } /// Get the length of the apothem of this segment, - /// which is the signed distance between the segment and the [center of its circle](Self::circle_center) + /// which is the signed distance between the segment and the center of its circle /// /// See [`Arc2d::apothem`] #[inline(always)] @@ -511,6 +490,8 @@ impl CircularSegment { #[cfg(test)] mod arc_tests { + use std::f32::consts::FRAC_PI_4; + use approx::assert_abs_diff_eq; use super::*; @@ -520,7 +501,6 @@ mod arc_tests { half_angle: f32, angle: f32, length: f32, - circle_center: Vec2, right_endpoint: Vec2, left_endpoint: Vec2, endpoints: [Vec2; 2], @@ -542,7 +522,6 @@ mod arc_tests { assert_abs_diff_eq!(self.half_angle, arc.half_angle); assert_abs_diff_eq!(self.angle, arc.angle()); assert_abs_diff_eq!(self.length, arc.length()); - assert_abs_diff_eq!(self.circle_center, arc.circle_center()); assert_abs_diff_eq!(self.right_endpoint, arc.right_endpoint()); assert_abs_diff_eq!(self.left_endpoint, arc.left_endpoint()); assert_abs_diff_eq!(self.endpoints[0], arc.endpoints()[0]); @@ -561,7 +540,6 @@ mod arc_tests { assert_abs_diff_eq!(self.radius, sector.radius()); assert_abs_diff_eq!(self.half_angle, sector.half_angle()); assert_abs_diff_eq!(self.angle, sector.angle()); - assert_abs_diff_eq!(self.circle_center, sector.circle_center()); assert_abs_diff_eq!(self.half_chord_length, sector.half_chord_length()); assert_abs_diff_eq!(self.chord_length, sector.chord_length(), epsilon = 0.00001); assert_abs_diff_eq!(self.chord_midpoint, sector.chord_midpoint()); @@ -574,7 +552,6 @@ mod arc_tests { assert_abs_diff_eq!(self.radius, segment.radius()); assert_abs_diff_eq!(self.half_angle, segment.half_angle()); assert_abs_diff_eq!(self.angle, segment.angle()); - assert_abs_diff_eq!(self.circle_center, segment.circle_center()); assert_abs_diff_eq!(self.half_chord_length, segment.half_chord_length()); assert_abs_diff_eq!(self.chord_length, segment.chord_length(), epsilon = 0.00001); assert_abs_diff_eq!(self.chord_midpoint, segment.chord_midpoint()); @@ -591,7 +568,6 @@ mod arc_tests { half_angle: 0.0, angle: 0.0, length: 0.0, - circle_center: Vec2::ZERO, left_endpoint: Vec2::Y, right_endpoint: Vec2::Y, endpoints: [Vec2::Y, Vec2::Y], @@ -616,10 +592,9 @@ mod arc_tests { fn zero_radius() { let tests = ArcTestCase { radius: 0.0, - half_angle: HALF_PI / 2.0, - angle: HALF_PI, + half_angle: FRAC_PI_4, + angle: FRAC_PI_2, length: 0.0, - circle_center: Vec2::ZERO, left_endpoint: Vec2::ZERO, right_endpoint: Vec2::ZERO, endpoints: [Vec2::ZERO, Vec2::ZERO], @@ -635,9 +610,9 @@ mod arc_tests { segment_area: 0.0, }; - tests.check_arc(Arc2d::new(0.0, HALF_PI / 2.0)); - tests.check_sector(CircularSector::new(0.0, HALF_PI / 2.0)); - tests.check_segment(CircularSegment::new(0.0, HALF_PI / 2.0)); + tests.check_arc(Arc2d::new(0.0, FRAC_PI_4)); + tests.check_sector(CircularSector::new(0.0, FRAC_PI_4)); + tests.check_segment(CircularSegment::new(0.0, FRAC_PI_4)); } #[test] @@ -645,10 +620,9 @@ mod arc_tests { let sqrt_half: f32 = f32::sqrt(0.5); let tests = ArcTestCase { radius: 1.0, - half_angle: HALF_PI / 2.0, - angle: HALF_PI, - length: HALF_PI, - circle_center: Vec2::ZERO, + half_angle: FRAC_PI_4, + angle: FRAC_PI_2, + length: FRAC_PI_2, left_endpoint: Vec2::new(-sqrt_half, sqrt_half), right_endpoint: Vec2::splat(sqrt_half), endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)], @@ -660,8 +634,8 @@ mod arc_tests { sagitta: 1.0 - sqrt_half, is_minor: true, is_major: false, - sector_area: PI / 4.0, - segment_area: PI / 4.0 - 0.5, + sector_area: FRAC_PI_4, + segment_area: FRAC_PI_4 - 0.5, }; tests.check_arc(Arc2d::from_fraction(1.0, 0.25)); @@ -673,10 +647,9 @@ mod arc_tests { fn half_circle() { let tests = ArcTestCase { radius: 1.0, - half_angle: HALF_PI, + half_angle: FRAC_PI_2, angle: PI, length: PI, - circle_center: Vec2::ZERO, left_endpoint: Vec2::NEG_X, right_endpoint: Vec2::X, endpoints: [Vec2::NEG_X, Vec2::X], @@ -688,8 +661,8 @@ mod arc_tests { sagitta: 1.0, is_minor: true, is_major: true, - sector_area: HALF_PI, - segment_area: HALF_PI, + sector_area: FRAC_PI_2, + segment_area: FRAC_PI_2, }; tests.check_arc(Arc2d::from_radians(1.0, PI)); @@ -704,7 +677,6 @@ mod arc_tests { half_angle: PI, angle: 2.0 * PI, length: 2.0 * PI, - circle_center: Vec2::ZERO, left_endpoint: Vec2::NEG_Y, right_endpoint: Vec2::NEG_Y, endpoints: [Vec2::NEG_Y, Vec2::NEG_Y], diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index e6e284e3790ec..2ae89c8b9ed2f 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -1,4 +1,4 @@ -use std::f32::consts::PI; +use std::f32::consts::FRAC_PI_2; use crate::{ mesh::{Indices, Mesh}, @@ -172,8 +172,8 @@ impl CircularSectorMeshBuilder { positions.push([0.0; 3]); uvs.push([0.5; 2]); - let first_angle = PI / 2.0 - self.sector.half_angle(); - let last_angle = PI / 2.0 + self.sector.half_angle(); + let first_angle = FRAC_PI_2 - self.sector.half_angle(); + let last_angle = FRAC_PI_2 + self.sector.half_angle(); let last_i = (self.resolution - 1) as f32; for i in 0..self.resolution { let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); @@ -298,14 +298,14 @@ impl CircularSegmentMeshBuilder { // This is similar to the computation inside the loop for the arc vertices, // but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius // to correctly position the vertex. - let midpoint_uv = Vec2::from_angle(-uv_angle - PI / 2.0).mul_add( + let midpoint_uv = Vec2::from_angle(-uv_angle - FRAC_PI_2).mul_add( Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())), Vec2::splat(0.5), ); uvs.push([midpoint_uv.x, midpoint_uv.y]); - let first_angle = PI / 2.0 - self.segment.half_angle(); - let last_angle = PI / 2.0 + self.segment.half_angle(); + let first_angle = FRAC_PI_2 - self.segment.half_angle(); + let last_angle = FRAC_PI_2 + self.segment.half_angle(); let last_i = (self.resolution - 1) as f32; for i in 0..self.resolution { let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i); diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 4da1321e59210..d20bf0ad3267b 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -13,6 +13,25 @@ fn main() { .run(); } +struct Shape { + mesh: Mesh2dHandle, + transform: Transform, +} + +impl Shape { + fn new(mesh: Handle, transform: Transform) -> Self { + Self { + mesh: Mesh2dHandle(mesh), + transform, + } + } +} +impl From> for Shape { + fn from(mesh: Handle) -> Self { + Self::new(mesh, default()) + } +} + fn setup( mut commands: Commands, mut meshes: ResMut>, @@ -20,25 +39,6 @@ fn setup( ) { commands.spawn(Camera2dBundle::default()); - struct Shape { - mesh: Mesh2dHandle, - transform: Transform, - } - - impl Shape { - fn new(mesh: Handle, transform: Transform) -> Self { - Self { - mesh: Mesh2dHandle(mesh), - transform, - } - } - } - impl From> for Shape { - fn from(mesh: Handle) -> Self { - Self::new(mesh, default()) - } - } - let shapes = [ Shape::from(meshes.add(Circle { radius: 50.0 })), Shape::new( diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index 812fd51be56af..cf523d91352d3 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -1,7 +1,7 @@ //! Demonstrates UV mappings of the [`CircularSector`] and [`CircularSegment`] primitives. //! //! Also draws the bounding boxes and circles of the primitives. -use std::f32::consts::PI; +use std::f32::consts::FRAC_PI_2; use bevy::{ color::palettes::css::{BLUE, DARK_SLATE_GREY, RED}, @@ -85,7 +85,7 @@ fn setup( // opposite angle to preserve the orientation of Bevy. But the angle is not the angle of the // texture itself, rather it is the angle at which the vertices are mapped onto the texture. // so it is the negative of what you might otherwise expect. - let segment_angle = -PI / 2.0; + let segment_angle = -FRAC_PI_2; let segment_mesh = CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask { angle: -segment_angle, diff --git a/examples/README.md b/examples/README.md index fe7afe0edf504..cbb55349d190f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -98,8 +98,8 @@ Example | Description [2D Bounding Volume Intersections](../examples/2d/bounding_2d.rs) | Showcases bounding volumes and intersection tests [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons -[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method -[Circular 2D Meshes](../examples/2d/mesh2d_circular.rs) | Demonstrates UV-mapping of circular primitives +[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates UV-mapping of the circular segment and sector primitives +[Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of circular primitives [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh From fb29d40f9abc552d60c69a0e393d01f092082d86 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Tue, 26 Mar 2024 17:51:45 +0900 Subject: [PATCH 18/19] Fixups for review & compile fixes --- .../src/bounding/bounded2d/primitive_impls.rs | 38 ++++++++++----- crates/bevy_math/src/direction.rs | 6 +-- crates/bevy_math/src/primitives/dim2.rs | 48 +++++++++---------- crates/bevy_math/src/rotation2d.rs | 6 +-- examples/2d/mesh2d_arcs.rs | 4 +- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 4b908d8ecc0e0..384a57bbd8493 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -32,19 +32,19 @@ impl Bounded2d for Circle { // Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes. // The return type has room for 7 points so that the CircularSector code can add an additional point. #[inline] -fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { +fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> SmallVec<[Vec2; 7]> { // Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle. // We need to compute which axis-aligned extrema are actually contained within the rotated arc. let mut bounds = SmallVec::<[Vec2; 7]>::new(); - let rotation_vec = Vec2::from_angle(rotation); - bounds.push(arc.left_endpoint().rotate(rotation_vec)); - bounds.push(arc.right_endpoint().rotate(rotation_vec)); + let rotation = rotation.into(); + bounds.push(rotation * arc.left_endpoint()); + bounds.push(rotation * arc.right_endpoint()); // The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y. // Compute the normalized angles of the endpoints with the rotation taken into account, and then // check if we are looking for an angle that is between or outside them. - let left_angle = (FRAC_PI_2 + arc.half_angle + rotation).rem_euclid(TAU); - let right_angle = (FRAC_PI_2 - arc.half_angle + rotation).rem_euclid(TAU); + let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU); + let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU); let inverted = left_angle < right_angle; for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] { let angle = extremum.to_angle().rem_euclid(TAU); @@ -62,7 +62,7 @@ fn arc_bounding_points(arc: Arc2d, rotation: f32) -> SmallVec<[Vec2; 7]> { } impl Bounded2d for Arc2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { // If our arc covers more than a circle, just return the bounding box of the circle. if self.half_angle >= PI { return Circle::new(self.radius).aabb_2d(translation, rotation); @@ -71,7 +71,11 @@ impl Bounded2d for Arc2d { Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation)) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { // There are two possibilities for the bounding circle. if self.is_major() { // If the arc is major, then the widest distance between two points is a diameter of the arc's circle; @@ -80,14 +84,14 @@ impl Bounded2d for Arc2d { } 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 center = self.chord_midpoint().rotate(Vec2::from_angle(rotation)); + let center = rotation.into() * self.chord_midpoint(); BoundingCircle::new(center + translation, self.half_chord_length()) } } } impl Bounded2d for CircularSector { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { // If our sector covers more than a circle, just return the bounding box of the circle. if self.half_angle() >= PI { return Circle::new(self.radius()).aabb_2d(translation, rotation); @@ -100,7 +104,11 @@ impl Bounded2d for CircularSector { Aabb2d::from_point_cloud(translation, 0.0, &bounds) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { if self.arc.is_major() { // If the arc is major, that is, greater than a semicircle, // then bounding circle is just the circle defining the sector. @@ -122,11 +130,15 @@ impl Bounded2d for CircularSector { } impl Bounded2d for CircularSegment { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { self.arc.aabb_2d(translation, rotation) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { self.arc.bounding_circle(translation, rotation) } } diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index 150a9105b86e1..f93f12930e0d6 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -202,7 +202,7 @@ impl std::ops::Mul for Rotation2d { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::AbsDiffEq for Dir2 { type Epsilon = f32; fn default_epsilon() -> f32 { @@ -213,7 +213,7 @@ impl approx::AbsDiffEq for Dir2 { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::RelativeEq for Dir2 { fn default_max_relative() -> f32 { f32::EPSILON @@ -224,7 +224,7 @@ impl approx::RelativeEq for Dir2 { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::UlpsEq for Dir2 { fn default_max_ulps() -> u32 { 4 diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index eaaf758c948a0..fd3e91e3c3791 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -116,20 +116,20 @@ impl Arc2d { } } - /// Create a new [`Arc2d`] from a `radius` and an angle in `degrees`. + /// Create a new [`Arc2d`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, degrees: f32) -> Self { + pub fn from_degrees(radius: f32, angle: f32) -> Self { Self { radius, - half_angle: degrees.to_radians() / 2.0, + half_angle: angle.to_radians() / 2.0, } } - /// Create a new [`Arc2d`] from a `radius` and a `fraction` of a circle + /// Create a new [`Arc2d`] from a `radius` and a number of `turns` of a circle. /// - /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle + /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_fraction(radius: f32, fraction: f32) -> Self { + pub fn from_turns(radius: f32, fraction: f32) -> Self { Self { radius, half_angle: fraction * PI, @@ -232,7 +232,7 @@ impl Arc2d { /// A primitive representing a circular sector: a pie slice of a circle. /// -/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side. +/// The segment is positioned so that it always includes [`Vec2::Y`] and is vertically symmetrical. /// To orient the sector differently, apply a rotation. /// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`]. /// @@ -273,18 +273,18 @@ impl CircularSector { Self::from(Arc2d::from_radians(radius, angle)) } - /// Create a new [`CircularSector`] from a `radius` and an angle in `degrees`. + /// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, degrees: f32) -> Self { - Self::from(Arc2d::from_degrees(radius, degrees)) + pub fn from_degrees(radius: f32, angle: f32) -> Self { + Self::from(Arc2d::from_degrees(radius, angle)) } - /// Create a new [`CircularSector`] from a `radius` and a `fraction` of a circle. + /// Create a new [`CircularSector`] from a `radius` and a number of `turns` of a circle. /// - /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_fraction(radius: f32, fraction: f32) -> Self { - Self::from(Arc2d::from_fraction(radius, fraction)) + pub fn from_turns(radius: f32, fraction: f32) -> Self { + Self::from(Arc2d::from_turns(radius, fraction)) } /// Get half the angle of the sector @@ -403,18 +403,18 @@ impl CircularSegment { Self::from(Arc2d::from_radians(radius, angle)) } - /// Create a new [`CircularSegment`] from a `radius` and an angle in `degrees` + /// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, degrees: f32) -> Self { - Self::from(Arc2d::from_degrees(radius, degrees)) + pub fn from_degrees(radius: f32, angle: f32) -> Self { + Self::from(Arc2d::from_degrees(radius, angle)) } - /// Create a new [`CircularSegment`] from a `radius` and a `fraction` of a circle. + /// Create a new [`CircularSegment`] from a `radius` and a number of `turns` of a circle. /// - /// A `fraction` of 1.0 would be a whole circle; 0.5 would be a semicircle. + /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_fraction(radius: f32, fraction: f32) -> Self { - Self::from(Arc2d::from_fraction(radius, fraction)) + pub fn from_turns(radius: f32, fraction: f32) -> Self { + Self::from(Arc2d::from_turns(radius, fraction)) } /// Get the half-angle of the segment @@ -638,9 +638,9 @@ mod arc_tests { segment_area: FRAC_PI_4 - 0.5, }; - tests.check_arc(Arc2d::from_fraction(1.0, 0.25)); - tests.check_sector(CircularSector::from_fraction(1.0, 0.25)); - tests.check_segment(CircularSegment::from_fraction(1.0, 0.25)); + tests.check_arc(Arc2d::from_turns(1.0, 0.25)); + tests.check_sector(CircularSector::from_turns(1.0, 0.25)); + tests.check_segment(CircularSegment::from_turns(1.0, 0.25)); } #[test] diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs index 7291e57c19233..bdeb5620a2c4c 100644 --- a/crates/bevy_math/src/rotation2d.rs +++ b/crates/bevy_math/src/rotation2d.rs @@ -386,7 +386,7 @@ impl std::ops::Mul for Rotation2d { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::AbsDiffEq for Rotation2d { type Epsilon = f32; fn default_epsilon() -> f32 { @@ -397,7 +397,7 @@ impl approx::AbsDiffEq for Rotation2d { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::RelativeEq for Rotation2d { fn default_max_relative() -> f32 { f32::EPSILON @@ -408,7 +408,7 @@ impl approx::RelativeEq for Rotation2d { } } -#[cfg(feature = "approx")] +#[cfg(any(feature = "approx", test))] impl approx::UlpsEq for Rotation2d { fn default_max_ulps() -> u32 { 4 diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index cf523d91352d3..f15421d449fc2 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -55,7 +55,7 @@ fn setup( for i in 0..NUM_SLICES { let fraction = (i + 1) as f32 / NUM_SLICES as f32; - let sector = CircularSector::from_fraction(40.0, fraction); + let sector = CircularSector::from_turns(40.0, fraction); // We want to rotate the circular sector so that the sectors appear clockwise from north. // We must rotate it both in the Transform and in the mesh's UV mappings. let sector_angle = -sector.half_angle(); @@ -77,7 +77,7 @@ fn setup( DrawBounds(sector), )); - let segment = CircularSegment::from_fraction(40.0, fraction); + let segment = CircularSegment::from_turns(40.0, fraction); // For the circular segment, we will draw Bevy charging forward, which requires rotating the // shape and texture by 90 degrees. // From 1eaf34335f4c9589a3bdec8e05671ae987d7d7b9 Mon Sep 17 00:00:00 2001 From: "Alexis \"spectria\" Horizon" Date: Tue, 26 Mar 2024 17:58:00 +0900 Subject: [PATCH 19/19] Fix failing build stuff --- crates/bevy_math/Cargo.toml | 2 +- examples/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index ad3115ca9dc13..716f6e7d8635e 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -15,7 +15,7 @@ serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } rand = { version = "0.8", features = [ - "alloc", + "alloc", ], default-features = false, optional = true } smallvec = { version = "1.11" } diff --git a/examples/README.md b/examples/README.md index 03ea2718ad684..ef692bab8158f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -99,8 +99,8 @@ Example | Description [2D Bounding Volume Intersections](../examples/2d/bounding_2d.rs) | Showcases bounding volumes and intersection tests [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons -[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates UV-mapping of the circular segment and sector primitives -[Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of circular primitives +[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method +[Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh