diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index 4c93f5ec02beaf..26f95c03465a9d 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -134,7 +134,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Builder methods /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the - /// `.segements(...)` method. + /// `.segments(...)` method. /// /// # Example /// ``` @@ -190,7 +190,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Builder methods /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the - /// `.segements(...)` method. + /// `.segments(...)` method. /// /// # Examples /// ``` @@ -236,7 +236,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// /// # Builder methods /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the - /// `.segements(...)` method. + /// `.segments(...)` method. /// /// # Examples /// ``` diff --git a/crates/bevy_gizmos/src/arrows.rs b/crates/bevy_gizmos/src/arrows.rs index b5416e412f5582..541fc6edab79bf 100644 --- a/crates/bevy_gizmos/src/arrows.rs +++ b/crates/bevy_gizmos/src/arrows.rs @@ -67,7 +67,7 @@ impl Drop for ArrowBuilder<'_, '_, '_, T> { } impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { - /// Draw an arrow in 3D, from `start` to `end`. Has four tips for convienent viewing from any direction. + /// Draw an arrow in 3D, from `start` to `end`. Has four tips for convenient viewing from any direction. /// /// This should be called for each frame the arrow needs to be rendered. /// diff --git a/crates/bevy_gizmos/src/circles.rs b/crates/bevy_gizmos/src/circles.rs index 2143996907ddb1..f8b48753b46a15 100644 --- a/crates/bevy_gizmos/src/circles.rs +++ b/crates/bevy_gizmos/src/circles.rs @@ -4,20 +4,98 @@ //! and assorted support items. use crate::prelude::{GizmoConfigGroup, Gizmos}; +use bevy_math::Mat2; use bevy_math::{primitives::Direction3d, Quat, Vec2, Vec3}; use bevy_render::color::Color; use std::f32::consts::TAU; pub(crate) const DEFAULT_CIRCLE_SEGMENTS: usize = 32; -fn circle_inner(radius: f32, segments: usize) -> impl Iterator { +fn ellipse_inner(half_size: Vec2, segments: usize) -> impl Iterator { (0..segments + 1).map(move |i| { let angle = i as f32 * TAU / segments as f32; - Vec2::from(angle.sin_cos()) * radius + let (x, y) = angle.sin_cos(); + Vec2::new(x, y) * half_size }) } impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { + /// Draw an ellipse in 3D at `position` with the flat side facing `normal`. + /// + /// This should be called for each frame the ellipse needs to be rendered. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(1., 2.), Color::GREEN); + /// + /// // Ellipses have 32 line-segments by default. + /// // You may want to increase this for larger ellipses. + /// gizmos + /// .ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(5., 1.), Color::RED) + /// .segments(64); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + #[inline] + pub fn ellipse( + &mut self, + position: Vec3, + rotation: Quat, + half_size: Vec2, + color: Color, + ) -> EllipseBuilder<'_, 'w, 's, T> { + EllipseBuilder { + gizmos: self, + position, + rotation, + half_size, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw an ellipse in 2D. + /// + /// This should be called for each frame the ellipse needs to be rendered. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(2., 1.), Color::GREEN); + /// + /// // Ellipses have 32 line-segments by default. + /// // You may want to increase this for larger ellipses. + /// gizmos + /// .ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(5., 1.), Color::RED) + /// .segments(64); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + #[inline] + pub fn ellipse_2d( + &mut self, + position: Vec2, + angle: f32, + half_size: Vec2, + color: Color, + ) -> Ellipse2dBuilder<'_, 'w, 's, T> { + Ellipse2dBuilder { + gizmos: self, + position, + rotation: Mat2::from_angle(angle), + half_size, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + /// Draw a circle in 3D at `position` with the flat side facing `normal`. /// /// This should be called for each frame the circle needs to be rendered. @@ -45,12 +123,12 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { normal: Direction3d, radius: f32, color: Color, - ) -> CircleBuilder<'_, 'w, 's, T> { - CircleBuilder { + ) -> EllipseBuilder<'_, 'w, 's, T> { + EllipseBuilder { gizmos: self, position, - normal, - radius, + rotation: Quat::from_rotation_arc(Vec3::Z, *normal), + half_size: Vec2::splat(radius), color, segments: DEFAULT_CIRCLE_SEGMENTS, } @@ -82,70 +160,76 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { position: Vec2, radius: f32, color: Color, - ) -> Circle2dBuilder<'_, 'w, 's, T> { - Circle2dBuilder { + ) -> Ellipse2dBuilder<'_, 'w, 's, T> { + Ellipse2dBuilder { gizmos: self, position, - radius, + rotation: Mat2::IDENTITY, + half_size: Vec2::splat(radius), color, segments: DEFAULT_CIRCLE_SEGMENTS, } } } -/// A builder returned by [`Gizmos::circle`]. -pub struct CircleBuilder<'a, 'w, 's, T: GizmoConfigGroup> { +/// A builder returned by [`Gizmos::ellipse`]. +pub struct EllipseBuilder<'a, 'w, 's, T: GizmoConfigGroup> { gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec3, - normal: Direction3d, - radius: f32, + rotation: Quat, + half_size: Vec2, color: Color, segments: usize, } -impl CircleBuilder<'_, '_, '_, T> { - /// Set the number of line-segments for this circle. +impl EllipseBuilder<'_, '_, '_, T> { + /// Set the number of line-segments for this ellipse. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; self } } -impl Drop for CircleBuilder<'_, '_, '_, T> { +impl Drop for EllipseBuilder<'_, '_, '_, T> { fn drop(&mut self) { if !self.gizmos.enabled { return; } - let rotation = Quat::from_rotation_arc(Vec3::Z, *self.normal); - let positions = circle_inner(self.radius, self.segments) - .map(|vec2| self.position + rotation * vec2.extend(0.)); + + let positions = ellipse_inner(self.half_size, self.segments) + .map(|vec2| self.rotation * vec2.extend(0.)) + .map(|vec3| vec3 + self.position); self.gizmos.linestrip(positions, self.color); } } -/// A builder returned by [`Gizmos::circle_2d`]. -pub struct Circle2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { +/// A builder returned by [`Gizmos::ellipse_2d`]. +pub struct Ellipse2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec2, - radius: f32, + rotation: Mat2, + half_size: Vec2, color: Color, segments: usize, } -impl Circle2dBuilder<'_, '_, '_, T> { - /// Set the number of line-segments for this circle. +impl Ellipse2dBuilder<'_, '_, '_, T> { + /// Set the number of line-segments for this ellipse. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; self } } -impl Drop for Circle2dBuilder<'_, '_, '_, T> { +impl Drop for Ellipse2dBuilder<'_, '_, '_, T> { fn drop(&mut self) { if !self.gizmos.enabled { return; - } - let positions = circle_inner(self.radius, self.segments).map(|vec2| vec2 + self.position); + }; + + let positions = ellipse_inner(self.half_size, self.segments) + .map(|vec2| self.rotation * vec2) + .map(|vec2| vec2 + self.position); self.gizmos.linestrip_2d(positions, self.color); } } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 5a4e3599bc6514..41022bcf4cad68 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -32,6 +32,7 @@ pub mod arrows; pub mod circles; pub mod config; pub mod gizmos; +pub mod primitives; #[cfg(feature = "bevy_sprite")] mod pipeline_2d; @@ -45,6 +46,7 @@ pub mod prelude { aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}, config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore}, gizmos::Gizmos, + primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d}, AppGizmoBuilder, }; } diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs new file mode 100644 index 00000000000000..7217be4e199fee --- /dev/null +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -0,0 +1,545 @@ +//! A module for rendering each of the 2D [`bevy_math::primitives`] with [`Gizmos`]. + +use std::f32::consts::PI; + +use super::helpers::*; + +use bevy_math::primitives::{ + BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d, + Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, +}; +use bevy_math::{Mat2, Vec2}; +use bevy_render::color::Color; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; + +// some magic number since using directions as offsets will result in lines of length 1 pixel +const MIN_LINE_LEN: f32 = 50.0; +const HALF_MIN_LINE_LEN: f32 = 25.0; +// length used to simulate infinite lines +const INFINITE_LEN: f32 = 100_000.0; + +/// A trait for rendering 2D geometric primitives (`P`) with [`Gizmos`]. +pub trait GizmoPrimitive2d { + /// The output of `primitive_2d`. This is a builder to set non-default values. + type Output<'a> + where + Self: 'a; + + /// Renders a 2D primitive with its associated details. + fn primitive_2d( + &mut self, + primitive: P, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_>; +} + +// direction 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self : 'a; + + fn primitive_2d( + &mut self, + primitive: Direction2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let direction = Mat2::from_angle(angle) * *primitive; + + let start = position; + let end = position + MIN_LINE_LEN * direction; + self.arrow_2d(start, end, color); + } +} + +// circle 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Circle, + position: Vec2, + _angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.circle_2d(position, primitive.radius, color); + } +} + +// ellipse 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Ellipse, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.ellipse_2d(position, angle, primitive.half_size, color); + } +} + +// capsule 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Capsule2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let rotation = Mat2::from_angle(angle); + + // transform points from the reference unit square to capsule "rectangle" + let [top_left, top_right, bottom_left, bottom_right, top_center, bottom_center] = [ + [-1.0, 1.0], + [1.0, 1.0], + [-1.0, -1.0], + [1.0, -1.0], + // just reuse the pipeline for these points as well + [0.0, 1.0], + [0.0, -1.0], + ] + .map(|[sign_x, sign_y]| Vec2::X * sign_x + Vec2::Y * sign_y) + .map(|reference_point| { + let scaling = Vec2::X * primitive.radius + Vec2::Y * primitive.half_length; + reference_point * scaling + }) + .map(rotate_then_translate_2d(angle, position)); + + // draw left and right side of capsule "rectangle" + self.line_2d(bottom_left, top_left, color); + self.line_2d(bottom_right, top_right, color); + + // if the capsule is rotated we have to start the arc at a different offset angle, + // calculate that here + let angle_offset = (rotation * Vec2::Y).angle_between(Vec2::Y); + let start_angle_top = angle_offset; + let start_angle_bottom = PI + angle_offset; + + // draw arcs + self.arc_2d(top_center, start_angle_top, PI, primitive.radius, color); + self.arc_2d( + bottom_center, + start_angle_bottom, + PI, + primitive.radius, + color, + ); + } +} + +// line 2d +// +/// Builder for configuring the drawing options of [`Line2d`]. +pub struct Line2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + direction: Direction2d, // Direction of the line + + position: Vec2, // position of the center of the line + rotation: Mat2, // rotation of the line + color: Color, // color of the line + + draw_arrow: bool, // decides whether to indicate the direction of the line with an arrow +} + +impl Line2dBuilder<'_, '_, '_, T> { + /// Set the drawing mode of the line (arrow vs. plain line) + pub fn draw_arrow(mut self, is_enabled: bool) -> Self { + self.draw_arrow = is_enabled; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = Line2dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Line2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + Line2dBuilder { + gizmos: self, + direction: primitive.direction, + position, + rotation: Mat2::from_angle(angle), + color, + draw_arrow: false, + } + } +} + +impl Drop for Line2dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let direction = self.rotation * *self.direction; + + let [start, end] = [1.0, -1.0] + .map(|sign| sign * INFINITE_LEN) + // offset the line from the origin infinitely into the given direction + .map(|length| direction * length) + // translate the line to the given position + .map(|offset| self.position + offset); + + self.gizmos.line_2d(start, end, self.color); + + // optionally draw an arrow head at the center of the line + if self.draw_arrow { + self.gizmos.arrow_2d( + self.position - direction * MIN_LINE_LEN, + self.position, + self.color, + ); + } + } +} + +// plane 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Plane2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + let rotation = Mat2::from_angle(angle); + + // draw normal of the plane (orthogonal to the plane itself) + let normal = primitive.normal; + let normal_segment = Segment2d { + direction: normal, + half_length: HALF_MIN_LINE_LEN, + }; + self.primitive_2d( + normal_segment, + // offset the normal so it starts on the plane line + position + HALF_MIN_LINE_LEN * rotation * *normal, + angle, + color, + ) + .draw_arrow(true); + + // draw the plane line + let direction = Direction2d::new_unchecked(-normal.perp()); + self.primitive_2d(Line2d { direction }, position, angle, color) + .draw_arrow(false); + + // draw an arrow such that the normal is always left side of the plane with respect to the + // planes direction. This is to follow the "counter-clockwise" convention + self.arrow_2d( + position, + position + MIN_LINE_LEN * (rotation * *direction), + color, + ); + } +} + +// segment 2d + +/// Builder for configuring the drawing options of [`Segment2d`]. +pub struct Segment2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + direction: Direction2d, // Direction of the line segment + half_length: f32, // Half-length of the line segment + + position: Vec2, // position of the center of the line segment + rotation: Mat2, // rotation of the line segment + color: Color, // color of the line segment + + draw_arrow: bool, // decides whether to draw just a line or an arrow +} + +impl Segment2dBuilder<'_, '_, '_, T> { + /// Set the drawing mode of the line (arrow vs. plain line) + pub fn draw_arrow(mut self, is_enabled: bool) -> Self { + self.draw_arrow = is_enabled; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = Segment2dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Segment2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + Segment2dBuilder { + gizmos: self, + direction: primitive.direction, + half_length: primitive.half_length, + + position, + rotation: Mat2::from_angle(angle), + color, + + draw_arrow: Default::default(), + } + } +} + +impl Drop for Segment2dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let direction = self.rotation * *self.direction; + let start = self.position - direction * self.half_length; + let end = self.position + direction * self.half_length; + + if self.draw_arrow { + self.gizmos.arrow_2d(start, end, self.color); + } else { + self.gizmos.line_2d(start, end, self.color); + } + } +} + +// polyline 2d + +impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive2d> + for Gizmos<'w, 's, T> +{ + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Polyline2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.linestrip_2d( + primitive + .vertices + .iter() + .copied() + .map(rotate_then_translate_2d(angle, position)), + color, + ); + } +} + +// boxed polyline 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: BoxedPolyline2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.linestrip_2d( + primitive + .vertices + .iter() + .copied() + .map(rotate_then_translate_2d(angle, position)), + color, + ); + } +} + +// triangle 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Triangle2d, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + let [a, b, c] = primitive.vertices; + let positions = [a, b, c, a].map(rotate_then_translate_2d(angle, position)); + self.linestrip_2d(positions, color); + } +} + +// rectangle 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Rectangle, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let [a, b, c, d] = + [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)].map(|(sign_x, sign_y)| { + Vec2::new( + primitive.half_size.x * sign_x, + primitive.half_size.y * sign_y, + ) + }); + let positions = [a, b, c, d, a].map(rotate_then_translate_2d(angle, position)); + self.linestrip_2d(positions, color); + } +} + +// polygon 2d + +impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive2d> + for Gizmos<'w, 's, T> +{ + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: Polygon, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + // Check if the polygon needs a closing point + let closing_point = { + let last = primitive.vertices.last(); + (primitive.vertices.first() != last) + .then_some(last) + .flatten() + .cloned() + }; + + self.linestrip_2d( + primitive + .vertices + .iter() + .copied() + .chain(closing_point) + .map(rotate_then_translate_2d(angle, position)), + color, + ); + } +} + +// boxed polygon 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: BoxedPolygon, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let closing_point = { + let last = primitive.vertices.last(); + (primitive.vertices.first() != last) + .then_some(last) + .flatten() + .cloned() + }; + self.linestrip_2d( + primitive + .vertices + .iter() + .copied() + .chain(closing_point) + .map(rotate_then_translate_2d(angle, position)), + color, + ); + } +} + +// regular polygon 2d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_2d( + &mut self, + primitive: RegularPolygon, + position: Vec2, + angle: f32, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let points = (0..=primitive.sides) + .map(|p| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, p)) + .map(rotate_then_translate_2d(angle, position)); + self.linestrip_2d(points, color); + } +} diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs new file mode 100644 index 00000000000000..3fd3bfdafba903 --- /dev/null +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -0,0 +1,919 @@ +//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`]. + +use super::helpers::*; +use std::f32::consts::TAU; + +use bevy_math::primitives::{ + BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, + Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Torus, +}; +use bevy_math::{Quat, Vec3}; +use bevy_render::color::Color; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; + +const DEFAULT_NUMBER_SEGMENTS: usize = 5; +// length used to simulate infinite lines +const INFINITE_LEN: f32 = 10_000.0; + +/// A trait for rendering 3D geometric primitives (`P`) with [`Gizmos`]. +pub trait GizmoPrimitive3d { + /// The output of `primitive_3d`. This is a builder to set non-default values. + type Output<'a> + where + Self: 'a; + + /// Renders a 3D primitive with its associated details. + fn primitive_3d( + &mut self, + primitive: P, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_>; +} + +// direction 3d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Direction3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + self.arrow(position, position + (rotation * *primitive), color); + } +} + +// sphere + +/// Builder for configuring the drawing options of [`Sphere`]. +pub struct SphereBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the sphere + radius: f32, + + // Rotation of the sphere around the origin in 3D space + rotation: Quat, + // Center position of the sphere in 3D space + position: Vec3, + // Color of the sphere + color: Color, + + // Number of segments used to approximate the sphere geometry + segments: usize, +} + +impl SphereBuilder<'_, '_, '_, T> { + /// Set the number of segments used to approximate the sphere geometry. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = SphereBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Sphere, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + SphereBuilder { + gizmos: self, + radius: primitive.radius, + position, + rotation, + color, + segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for SphereBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let SphereBuilder { + radius, + position: center, + rotation, + color, + segments, + .. + } = self; + + // draws the upper and lower semi spheres + [-1.0, 1.0].into_iter().for_each(|sign| { + let top = *center + (*rotation * Vec3::Y) * sign * *radius; + draw_semi_sphere( + self.gizmos, + *radius, + *segments, + *rotation, + *center, + top, + *color, + ); + }); + + // draws one great circle of the sphere + draw_circle_3d(self.gizmos, *radius, *segments, *rotation, *center, *color); + } +} + +// plane 3d + +/// Builder for configuring the drawing options of [`Sphere`]. +pub struct Plane3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // direction of the normal orthogonal to the plane + normal: Direction3d, + + // Rotation of the sphere around the origin in 3D space + rotation: Quat, + // Center position of the sphere in 3D space + position: Vec3, + // Color of the sphere + color: Color, + + // Number of axis to hint the plane + axis_count: usize, + // Number of segments used to hint the plane + segment_count: usize, + // Length of segments used to hint the plane + segment_length: f32, +} + +impl Plane3dBuilder<'_, '_, '_, T> { + /// Set the number of segments used to hint the plane. + pub fn segment_count(mut self, count: usize) -> Self { + self.segment_count = count; + self + } + + /// Set the length of segments used to hint the plane. + pub fn segment_length(mut self, length: f32) -> Self { + self.segment_length = length; + self + } + + /// Set the number of axis used to hint the plane. + pub fn axis_count(mut self, count: usize) -> Self { + self.axis_count = count; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = Plane3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Plane3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + Plane3dBuilder { + gizmos: self, + normal: primitive.normal, + rotation, + position, + color, + axis_count: 4, + segment_count: 3, + segment_length: 0.25, + } + } +} + +impl Drop for Plane3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + // draws the normal + let normal = self.rotation * *self.normal; + self.gizmos + .primitive_3d(self.normal, self.position, self.rotation, self.color); + let normals_normal = normal.any_orthonormal_vector(); + + // draws the axes + // get rotation for each direction + (0..self.axis_count) + .map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU) + .map(|angle| Quat::from_axis_angle(normal, angle)) + .for_each(|quat| { + let axis_direction = quat * normals_normal; + let direction = Direction3d::new_unchecked(axis_direction); + + // for each axis draw dotted line + (0..) + .filter(|i| i % 2 != 0) + .map(|percent| (percent as f32 + 0.5) * self.segment_length * axis_direction) + .map(|position| position + self.position) + .take(self.segment_count) + .for_each(|position| { + self.gizmos.primitive_3d( + Segment3d { + direction, + half_length: self.segment_length * 0.5, + }, + position, + Quat::IDENTITY, + self.color, + ); + }); + }); + } +} + +// line 3d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Line3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let direction = rotation * *primitive.direction; + self.arrow(position, position + direction, color); + + let [start, end] = [1.0, -1.0] + .map(|sign| sign * INFINITE_LEN) + .map(|length| direction * length) + .map(|offset| position + offset); + self.line(start, end, color); + } +} + +// segment 3d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Segment3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let direction = rotation * *primitive.direction; + let start = position - direction * primitive.half_length; + let end = position + direction * primitive.half_length; + self.line(start, end, color); + } +} + +// polyline 3d + +impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive3d> + for Gizmos<'w, 's, T> +{ + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Polyline3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.linestrip( + primitive + .vertices + .map(rotate_then_translate_3d(rotation, position)), + color, + ); + } +} + +// boxed polyline 3d + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: BoxedPolyline3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + self.linestrip( + primitive + .vertices + .iter() + .copied() + .map(rotate_then_translate_3d(rotation, position)), + color, + ); + } +} + +// cuboid + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Cuboid, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let [half_extend_x, half_extend_y, half_extend_z] = primitive.half_size.to_array(); + + // transform the points from the reference unit cube to the cuboid coords + let vertices @ [a, b, c, d, e, f, g, h] = [ + [1.0, 1.0, 1.0], + [-1.0, 1.0, 1.0], + [-1.0, -1.0, 1.0], + [1.0, -1.0, 1.0], + [1.0, 1.0, -1.0], + [-1.0, 1.0, -1.0], + [-1.0, -1.0, -1.0], + [1.0, -1.0, -1.0], + ] + .map(|[sx, sy, sz]| Vec3::new(sx * half_extend_x, sy * half_extend_y, sz * half_extend_z)) + .map(rotate_then_translate_3d(rotation, position)); + + // lines for the upper rectangle of the cuboid + let upper = [a, b, c, d] + .into_iter() + .zip([a, b, c, d].into_iter().cycle().skip(1)); + + // lines for the lower rectangle of the cuboid + let lower = [e, f, g, h] + .into_iter() + .zip([e, f, g, h].into_iter().cycle().skip(1)); + + // lines connecting upper and lower rectangles of the cuboid + let connections = vertices.into_iter().zip(vertices.into_iter().skip(4)); + + upper + .chain(lower) + .chain(connections) + .for_each(|(start, end)| { + self.line(start, end, color); + }); + } +} + +// cylinder 3d + +/// Builder for configuring the drawing options of [`Cylinder`]. +pub struct Cylinder3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the cylinder + radius: f32, + // Half height of the cylinder + half_height: f32, + + // Center position of the cylinder + position: Vec3, + // Rotation of the cylinder + // + // default orientation is: the cylinder is aligned with `Vec3::Y` axis + rotation: Quat, + // Color of the cylinder + color: Color, + + // Number of segments used to approximate the cylinder geometry + segments: usize, +} + +impl Cylinder3dBuilder<'_, '_, '_, T> { + /// Set the number of segments used to approximate the cylinder geometry. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = Cylinder3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Cylinder, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + Cylinder3dBuilder { + gizmos: self, + radius: primitive.radius, + half_height: primitive.half_height, + position, + rotation, + color, + segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for Cylinder3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let Cylinder3dBuilder { + gizmos, + radius, + half_height, + position, + rotation, + color, + segments, + } = self; + + let normal = *rotation * Vec3::Y; + + // draw upper and lower circle of the cylinder + [-1.0, 1.0].into_iter().for_each(|sign| { + draw_circle_3d( + gizmos, + *radius, + *segments, + *rotation, + *position + sign * *half_height * normal, + *color, + ); + }); + + // draw lines connecting the two cylinder circles + draw_cylinder_vertical_lines( + gizmos, + *radius, + *segments, + *half_height, + *rotation, + *position, + *color, + ); + } +} + +// capsule 3d + +/// Builder for configuring the drawing options of [`Capsule3d`]. +pub struct Capsule3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the capsule + radius: f32, + // Half length of the capsule + half_length: f32, + + // Center position of the capsule + position: Vec3, + // Rotation of the capsule + // + // default orientation is: the capsule is aligned with `Vec3::Y` axis + rotation: Quat, + // Color of the capsule + color: Color, + + // Number of segments used to approximate the capsule geometry + segments: usize, +} + +impl Capsule3dBuilder<'_, '_, '_, T> { + /// Set the number of segments used to approximate the capsule geometry. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = Capsule3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Capsule3d, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + Capsule3dBuilder { + gizmos: self, + radius: primitive.radius, + half_length: primitive.half_length, + position, + rotation, + color, + segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for Capsule3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let Capsule3dBuilder { + gizmos, + radius, + half_length, + position, + rotation, + color, + segments, + } = self; + + let normal = *rotation * Vec3::Y; + + // draw two semi spheres for the capsule + [1.0, -1.0].into_iter().for_each(|sign| { + let center = *position + sign * *half_length * normal; + let top = center + sign * *radius * normal; + draw_semi_sphere(gizmos, *radius, *segments, *rotation, center, top, *color); + draw_circle_3d(gizmos, *radius, *segments, *rotation, center, *color); + }); + + // connect the two semi spheres with lines + draw_cylinder_vertical_lines( + gizmos, + *radius, + *segments, + *half_length, + *rotation, + *position, + *color, + ); + } +} + +// cone 3d + +/// Builder for configuring the drawing options of [`Cone`]. +pub struct Cone3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the cone + radius: f32, + // Height of the cone + height: f32, + + // Center of the cone, half-way between the tip and the base + position: Vec3, + // Rotation of the cone + // + // default orientation is: cone base normal is aligned with the `Vec3::Y` axis + rotation: Quat, + // Color of the cone + color: Color, + + // Number of segments used to approximate the cone geometry + segments: usize, +} + +impl Cone3dBuilder<'_, '_, '_, T> { + /// Set the number of segments used to approximate the cone geometry. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = Cone3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Cone, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + Cone3dBuilder { + gizmos: self, + radius: primitive.radius, + height: primitive.height, + position, + rotation, + color, + segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for Cone3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let Cone3dBuilder { + gizmos, + radius, + height, + position, + rotation, + color, + segments, + } = self; + + let half_height = *height * 0.5; + + // draw the base circle of the cone + draw_circle_3d( + gizmos, + *radius, + *segments, + *rotation, + *position - *rotation * Vec3::Y * half_height, + *color, + ); + + // connect the base circle with the tip of the cone + let end = Vec3::Y * half_height; + circle_coordinates(*radius, *segments) + .map(|p| Vec3::new(p.x, -half_height, p.y)) + .map(move |p| [p, end]) + .map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position))) + .for_each(|[start, end]| { + gizmos.line(start, end, *color); + }); + } +} + +// conical frustum 3d + +/// Builder for configuring the drawing options of [`ConicalFrustum`]. +pub struct ConicalFrustum3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the top circle + radius_top: f32, + // Radius of the bottom circle + radius_bottom: f32, + // Height of the conical frustum + height: f32, + + // Center of conical frustum, half-way between the top and the bottom + position: Vec3, + // Rotation of the conical frustrum + // + // default orientation is: conical frustrum base shape normals are aligned with `Vec3::Y` axis + rotation: Quat, + // Color of the conical frustum + color: Color, + + // Number of segments used to approximate the curved surfaces + segments: usize, +} + +impl ConicalFrustum3dBuilder<'_, '_, '_, T> { + /// Set the number of segments used to approximate the curved surfaces. + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = ConicalFrustum3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: ConicalFrustum, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + ConicalFrustum3dBuilder { + gizmos: self, + radius_top: primitive.radius_top, + radius_bottom: primitive.radius_bottom, + height: primitive.height, + position, + rotation, + color, + segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for ConicalFrustum3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let ConicalFrustum3dBuilder { + gizmos, + radius_top, + radius_bottom, + height, + position, + rotation, + color, + segments, + } = self; + + let half_height = *height * 0.5; + let normal = *rotation * Vec3::Y; + + // draw the two circles of the conical frustrum + [(*radius_top, half_height), (*radius_bottom, -half_height)] + .into_iter() + .for_each(|(radius, height)| { + draw_circle_3d( + gizmos, + radius, + *segments, + *rotation, + *position + height * normal, + *color, + ); + }); + + // connect the two circles of the conical frustrum + circle_coordinates(*radius_top, *segments) + .map(move |p| Vec3::new(p.x, half_height, p.y)) + .zip( + circle_coordinates(*radius_bottom, *segments) + .map(|p| Vec3::new(p.x, -half_height, p.y)), + ) + .map(|(start, end)| [start, end]) + .map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position))) + .for_each(|[start, end]| { + gizmos.line(start, end, *color); + }); + } +} + +// torus 3d + +/// Builder for configuring the drawing options of [`Torus`]. +pub struct Torus3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + + // Radius of the minor circle (tube) + minor_radius: f32, + // Radius of the major circle (ring) + major_radius: f32, + + // Center of the torus + position: Vec3, + // Rotation of the conical frustrum + // + // default orientation is: major circle normal is aligned with `Vec3::Y` axis + rotation: Quat, + // Color of the torus + color: Color, + + // Number of segments in the minor (tube) direction + minor_segments: usize, + // Number of segments in the major (ring) direction + major_segments: usize, +} + +impl Torus3dBuilder<'_, '_, '_, T> { + /// Set the number of segments in the minor (tube) direction. + pub fn minor_segments(mut self, minor_segments: usize) -> Self { + self.minor_segments = minor_segments; + self + } + + /// Set the number of segments in the major (ring) direction. + pub fn major_segments(mut self, major_segments: usize) -> Self { + self.major_segments = major_segments; + self + } +} + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = Torus3dBuilder<'a, 'w, 's, T> where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Torus, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Self::Output<'_> { + Torus3dBuilder { + gizmos: self, + minor_radius: primitive.minor_radius, + major_radius: primitive.major_radius, + position, + rotation, + color, + minor_segments: DEFAULT_NUMBER_SEGMENTS, + major_segments: DEFAULT_NUMBER_SEGMENTS, + } + } +} + +impl Drop for Torus3dBuilder<'_, '_, '_, T> { + fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } + + let Torus3dBuilder { + gizmos, + minor_radius, + major_radius, + position, + rotation, + color, + minor_segments, + major_segments, + } = self; + + let normal = *rotation * Vec3::Y; + + // draw 4 circles with major_radius + [ + (*major_radius - *minor_radius, 0.0), + (*major_radius + *minor_radius, 0.0), + (*major_radius, *minor_radius), + (*major_radius, -*minor_radius), + ] + .into_iter() + .for_each(|(radius, height)| { + draw_circle_3d( + gizmos, + radius, + *major_segments, + *rotation, + *position + height * normal, + *color, + ); + }); + + // along the major circle draw orthogonal minor circles + let affine = rotate_then_translate_3d(*rotation, *position); + circle_coordinates(*major_radius, *major_segments) + .map(|p| Vec3::new(p.x, 0.0, p.y)) + .flat_map(|major_circle_point| { + let minor_center = affine(major_circle_point); + + // direction facing from the center of the torus towards the minor circles center + let dir_to_translation = (minor_center - *position).normalize(); + + // the minor circle is draw with 4 arcs this is done to make the minor circle + // connect properly with each of the major circles + let circle_points = [dir_to_translation, normal, -dir_to_translation, -normal] + .map(|offset| minor_center + offset.normalize() * *minor_radius); + circle_points + .into_iter() + .zip(circle_points.into_iter().cycle().skip(1)) + .map(move |(from, to)| (minor_center, from, to)) + .collect::>() + }) + .for_each(|(center, from, to)| { + gizmos + .short_arc_3d_between(center, from, to, *color) + .segments(*minor_segments); + }); + } +} diff --git a/crates/bevy_gizmos/src/primitives/helpers.rs b/crates/bevy_gizmos/src/primitives/helpers.rs new file mode 100644 index 00000000000000..02d45f02207f7e --- /dev/null +++ b/crates/bevy_gizmos/src/primitives/helpers.rs @@ -0,0 +1,115 @@ +use std::f32::consts::TAU; + +use bevy_math::{Mat2, Quat, Vec2, Vec3}; +use bevy_render::color::Color; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; + +/// Performs an isometric transformation on 2D vectors. +/// +/// This function takes angle and a position vector, and returns a closure that applies +/// the isometric transformation to any given 2D vector. The transformation involves rotating +/// the vector by the specified angle and then translating it by the given position. +pub(crate) fn rotate_then_translate_2d(angle: f32, position: Vec2) -> impl Fn(Vec2) -> Vec2 { + move |v| Mat2::from_angle(angle) * v + position +} + +/// Performs an isometric transformation on 3D vectors. +/// +/// This function takes a quaternion representing rotation and a 3D vector representing +/// translation, and returns a closure that applies the isometric transformation to any +/// given 3D vector. The transformation involves rotating the vector by the specified +/// quaternion and then translating it by the given translation vector. +pub(crate) fn rotate_then_translate_3d(rotation: Quat, translation: Vec3) -> impl Fn(Vec3) -> Vec3 { + move |v| rotation * v + translation +} + +/// Calculates the `nth` coordinate of a circle segment. +/// +/// Given a circle's radiu and the number of segments, this function computes the position +/// of the `nth` point along the circumference of the circle. The rotation starts at `(0.0, radius)` +/// and proceeds counter-clockwise. +pub(crate) fn single_circle_coordinate(radius: f32, segments: usize, nth_point: usize) -> Vec2 { + let angle = nth_point as f32 * TAU / segments as f32; + let (x, y) = angle.sin_cos(); + Vec2::new(x, y) * radius +} + +/// Generates an iterator over the coordinates of a circle segment. +/// +/// This function creates an iterator that yields the positions of points approximating a +/// circle with the given radius, divided into linear segments. The iterator produces `segments` +/// number of points. +pub(crate) fn circle_coordinates(radius: f32, segments: usize) -> impl Iterator { + (0..) + .map(move |p| single_circle_coordinate(radius, segments, p)) + .take(segments) +} + +/// Draws a semi-sphere. +/// +/// This function draws a semi-sphere at the specified `center` point with the given `rotation`, +/// `radius`, and `color`. The `segments` parameter determines the level of detail, and the `top` +/// argument specifies the shape of the semi-sphere's tip. +pub(crate) fn draw_semi_sphere( + gizmos: &mut Gizmos<'_, '_, T>, + radius: f32, + segments: usize, + rotation: Quat, + center: Vec3, + top: Vec3, + color: Color, +) { + circle_coordinates(radius, segments) + .map(|p| Vec3::new(p.x, 0.0, p.y)) + .map(rotate_then_translate_3d(rotation, center)) + .for_each(|from| { + gizmos + .short_arc_3d_between(center, from, top, color) + .segments(segments / 2); + }); +} + +/// Draws a circle in 3D space. +/// +/// # Note +/// +/// This function is necessary to use instead of `gizmos.circle` for certain primitives to ensure that points align correctly. For example, the major circles of a torus are drawn with this method, and using `gizmos.circle` would result in the minor circles not being positioned precisely on the major circles' segment points. +pub(crate) fn draw_circle_3d( + gizmos: &mut Gizmos<'_, '_, T>, + radius: f32, + segments: usize, + rotation: Quat, + translation: Vec3, + color: Color, +) { + let positions = (0..=segments) + .map(|frac| frac as f32 / segments as f32) + .map(|percentage| percentage * TAU) + .map(|angle| Vec2::from(angle.sin_cos()) * radius) + .map(|p| Vec3::new(p.x, 0.0, p.y)) + .map(rotate_then_translate_3d(rotation, translation)); + gizmos.linestrip(positions, color); +} + +/// Draws the connecting lines of a cylinder between the top circle and the bottom circle. +pub(crate) fn draw_cylinder_vertical_lines( + gizmos: &mut Gizmos<'_, '_, T>, + radius: f32, + segments: usize, + half_height: f32, + rotation: Quat, + center: Vec3, + color: Color, +) { + circle_coordinates(radius, segments) + .map(move |point_2d| { + [1.0, -1.0] + .map(|sign| sign * half_height) + .map(|height| Vec3::new(point_2d.x, height, point_2d.y)) + }) + .map(|ps| ps.map(rotate_then_translate_3d(rotation, center))) + .for_each(|[start, end]| { + gizmos.line(start, end, color); + }); +} diff --git a/crates/bevy_gizmos/src/primitives/mod.rs b/crates/bevy_gizmos/src/primitives/mod.rs new file mode 100644 index 00000000000000..86e2c4511f3c11 --- /dev/null +++ b/crates/bevy_gizmos/src/primitives/mod.rs @@ -0,0 +1,5 @@ +//! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`crate::prelude::Gizmos`]. + +pub mod dim2; +pub mod dim3; +pub(crate) mod helpers; diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 55061ba0db4dff..ba105e8780bd46 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -7,6 +7,7 @@ use crate::Vec2; #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Direction2d(Vec2); +impl Primitive2d for Direction2d {} impl Direction2d { /// A unit vector pointing along the positive X axis. diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index c29c696108d8d8..f882487745f5b6 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -7,6 +7,7 @@ use crate::{Quat, Vec3}; #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Direction3d(Vec3); +impl Primitive3d for Direction3d {} impl Direction3d { /// A unit vector pointing along the positive X axis. diff --git a/examples/2d/2d_gizmos.rs b/examples/2d/2d_gizmos.rs index a314809a8857fd..1e7129f51949f7 100644 --- a/examples/2d/2d_gizmos.rs +++ b/examples/2d/2d_gizmos.rs @@ -1,15 +1,17 @@ //! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging. -use std::f32::consts::PI; +use std::f32::consts::{PI, TAU}; use bevy::prelude::*; fn main() { App::new() + .init_state::() .add_plugins(DefaultPlugins) .init_gizmo_group::() .add_systems(Startup, setup) - .add_systems(Update, (system, update_config)) + .add_systems(Update, (draw_example_collection, update_config)) + .add_systems(Update, (draw_primitives, update_primitives)) .run(); } @@ -17,13 +19,61 @@ fn main() { #[derive(Default, Reflect, GizmoConfigGroup)] struct MyRoundGizmos {} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)] +enum PrimitiveState { + #[default] + Nothing, + Circle, + Ellipse, + Capsule, + Line, + Plane, + Segment, + Triangle, + Rectangle, + RegularPolygon, +} + +impl PrimitiveState { + const ALL: [Self; 10] = [ + Self::Nothing, + Self::Circle, + Self::Ellipse, + Self::Capsule, + Self::Line, + Self::Plane, + Self::Segment, + Self::Triangle, + Self::Rectangle, + Self::RegularPolygon, + ]; + fn next(self) -> Self { + Self::ALL + .into_iter() + .cycle() + .skip_while(|&x| x != self) + .nth(1) + .unwrap() + } + fn last(self) -> Self { + Self::ALL + .into_iter() + .rev() + .cycle() + .skip_while(|&x| x != self) + .nth(1) + .unwrap() + } +} + fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); // text commands.spawn(TextBundle::from_section( "Hold 'Left' or 'Right' to change the line width of straight gizmos\n\ Hold 'Up' or 'Down' to change the line width of round gizmos\n\ - Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos", + Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\ + Press 'K' or 'J' to cycle through primitives rendered with gizmos", TextStyle { font: asset_server.load("fonts/FiraMono-Medium.ttf"), font_size: 24., @@ -32,7 +82,11 @@ fn setup(mut commands: Commands, asset_server: Res) { )); } -fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos, time: Res