mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
Drawing Primitives with Gizmos (#11072)
The PR is in a reviewable state now in the sense that the basic implementations are there. There are still some ToDos that I'm aware of: - [x] docs for all the new structs and traits - [x] implement `Default` and derive other useful traits for the new structs - [x] Take a look at the notes again (Do this after a first round of reviews) - [x] Take care of the repetition in the circle drawing functions --- # Objective - TLDR: This PR enables us to quickly draw all the newly added primitives from `bevy_math` in immediate mode with gizmos - Addresses #10571 ## Solution - This implements the first design idea I had that covered everything that was mentioned in the Issue https://github.com/bevyengine/bevy/issues/10571#issuecomment-1863646197 --- ## Caveats - I added the `Primitive(2/3)d` impls for `Direction(2/3)d` to make them work with the current solution. We could impose less strict requirements for the gizmoable objects and remove the impls afterwards if the community doesn't like the current approach. --- ## Changelog - implement capabilities to draw ellipses on the gizmo in general (this was required to have some code which is able to draw the ellipse primitive) - refactored circle drawing code to use the more general ellipse drawing code to keep code duplication low - implement `Primitive2d` for `Direction2d` and impl `Primitive3d` for `Direction3d` - implement trait to draw primitives with specialized details with gizmos - `GizmoPrimitive2d` for all the 2D primitives - `GizmoPrimitive3d` for all the 3D primitives - (question while writing this: Does it actually matter if we split this in 2D and 3D? I guess it could be useful in the future if we do something based on the main rendering mode even though atm it's kinda useless) --- --------- Co-authored-by: nothendev <borodinov.ilya@gmail.com>
This commit is contained in:
parent
e2916fbad1
commit
041731b7e0
12 changed files with 2104 additions and 43 deletions
|
@ -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
|
||||
/// ```
|
||||
|
|
|
@ -67,7 +67,7 @@ impl<T: GizmoConfigGroup> 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.
|
||||
///
|
||||
|
|
|
@ -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<Item = Vec2> {
|
||||
fn ellipse_inner(half_size: Vec2, segments: usize) -> impl Iterator<Item = Vec2> {
|
||||
(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<T: GizmoConfigGroup> CircleBuilder<'_, '_, '_, T> {
|
||||
/// Set the number of line-segments for this circle.
|
||||
impl<T: GizmoConfigGroup> EllipseBuilder<'_, '_, '_, T> {
|
||||
/// Set the number of line-segments for this ellipse.
|
||||
pub fn segments(mut self, segments: usize) -> Self {
|
||||
self.segments = segments;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GizmoConfigGroup> Drop for CircleBuilder<'_, '_, '_, T> {
|
||||
impl<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> Circle2dBuilder<'_, '_, '_, T> {
|
||||
/// Set the number of line-segments for this circle.
|
||||
impl<T: GizmoConfigGroup> Ellipse2dBuilder<'_, '_, '_, T> {
|
||||
/// Set the number of line-segments for this ellipse.
|
||||
pub fn segments(mut self, segments: usize) -> Self {
|
||||
self.segments = segments;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GizmoConfigGroup> Drop for Circle2dBuilder<'_, '_, '_, T> {
|
||||
impl<T: GizmoConfigGroup> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
545
crates/bevy_gizmos/src/primitives/dim2.rs
Normal file
545
crates/bevy_gizmos/src/primitives/dim2.rs
Normal file
|
@ -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<P: Primitive2d> {
|
||||
/// 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<Direction2d> 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<Circle> 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<Ellipse> 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<Capsule2d> 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<T: GizmoConfigGroup> 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<Line2d> 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<T: GizmoConfigGroup> 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<Plane2d> 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<T: GizmoConfigGroup> 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<Segment2d> 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<T: GizmoConfigGroup> 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<Polyline2d<N>>
|
||||
for Gizmos<'w, 's, T>
|
||||
{
|
||||
type Output<'a> = () where Self: 'a;
|
||||
|
||||
fn primitive_2d(
|
||||
&mut self,
|
||||
primitive: Polyline2d<N>,
|
||||
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<BoxedPolyline2d> 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<Triangle2d> 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<Rectangle> 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<Polygon<N>>
|
||||
for Gizmos<'w, 's, T>
|
||||
{
|
||||
type Output<'a> = () where Self: 'a;
|
||||
|
||||
fn primitive_2d(
|
||||
&mut self,
|
||||
primitive: Polygon<N>,
|
||||
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<BoxedPolygon> 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<RegularPolygon> 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);
|
||||
}
|
||||
}
|
919
crates/bevy_gizmos/src/primitives/dim3.rs
Normal file
919
crates/bevy_gizmos/src/primitives/dim3.rs
Normal file
|
@ -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<P: Primitive3d> {
|
||||
/// 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<Direction3d> 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<T: GizmoConfigGroup> 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<Sphere> 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<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> 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<Plane3d> 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<T: GizmoConfigGroup> 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<Line3d> 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<Segment3d> 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<Polyline3d<N>>
|
||||
for Gizmos<'w, 's, T>
|
||||
{
|
||||
type Output<'a> = () where Self: 'a;
|
||||
|
||||
fn primitive_3d(
|
||||
&mut self,
|
||||
primitive: Polyline3d<N>,
|
||||
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<BoxedPolyline3d> 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<Cuboid> 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<T: GizmoConfigGroup> 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<Cylinder> 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<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> 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<Capsule3d> 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<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> 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<Cone> 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<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> 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<ConicalFrustum> 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<T: GizmoConfigGroup> 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<T: GizmoConfigGroup> 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<Torus> 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<T: GizmoConfigGroup> 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::<Vec<_>>()
|
||||
})
|
||||
.for_each(|(center, from, to)| {
|
||||
gizmos
|
||||
.short_arc_3d_between(center, from, to, *color)
|
||||
.segments(*minor_segments);
|
||||
});
|
||||
}
|
||||
}
|
115
crates/bevy_gizmos/src/primitives/helpers.rs
Normal file
115
crates/bevy_gizmos/src/primitives/helpers.rs
Normal file
|
@ -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<Item = Vec2> {
|
||||
(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<T: GizmoConfigGroup>(
|
||||
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<T: GizmoConfigGroup>(
|
||||
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<T: GizmoConfigGroup>(
|
||||
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);
|
||||
});
|
||||
}
|
5
crates/bevy_gizmos/src/primitives/mod.rs
Normal file
5
crates/bevy_gizmos/src/primitives/mod.rs
Normal file
|
@ -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;
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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::<PrimitiveState>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_gizmo_group::<MyRoundGizmos>()
|
||||
.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<AssetServer>) {
|
||||
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<AssetServer>) {
|
|||
));
|
||||
}
|
||||
|
||||
fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Time>) {
|
||||
fn draw_example_collection(
|
||||
mut gizmos: Gizmos,
|
||||
mut my_gizmos: Gizmos<MyRoundGizmos>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let sin = time.elapsed_seconds().sin() * 50.;
|
||||
gizmos.line_2d(Vec2::Y * -sin, Vec2::splat(-80.), Color::RED);
|
||||
gizmos.ray_2d(Vec2::Y * sin, Vec2::splat(80.), Color::GREEN);
|
||||
|
@ -54,6 +108,12 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
|
|||
|
||||
// The circles have 32 line-segments by default.
|
||||
my_gizmos.circle_2d(Vec2::ZERO, 120., Color::BLACK);
|
||||
my_gizmos.ellipse_2d(
|
||||
Vec2::ZERO,
|
||||
time.elapsed_seconds() % TAU,
|
||||
Vec2::new(100., 200.),
|
||||
Color::YELLOW_GREEN,
|
||||
);
|
||||
// You may want to increase this for larger circles.
|
||||
my_gizmos
|
||||
.circle_2d(Vec2::ZERO, 300., Color::NAVY)
|
||||
|
@ -70,6 +130,92 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
|
|||
);
|
||||
}
|
||||
|
||||
fn draw_primitives(
|
||||
mut gizmos: Gizmos,
|
||||
time: Res<Time>,
|
||||
primitive_state: Res<State<PrimitiveState>>,
|
||||
) {
|
||||
let angle = time.elapsed_seconds();
|
||||
let rotation = Mat2::from_angle(angle);
|
||||
let position = rotation * Vec2::X;
|
||||
let color = Color::WHITE;
|
||||
|
||||
const SIZE: f32 = 50.0;
|
||||
match primitive_state.get() {
|
||||
PrimitiveState::Nothing => {}
|
||||
PrimitiveState::Circle => {
|
||||
gizmos.primitive_2d(Circle { radius: SIZE }, position, angle, color);
|
||||
}
|
||||
PrimitiveState::Ellipse => gizmos.primitive_2d(
|
||||
Ellipse {
|
||||
half_size: Vec2::new(SIZE, SIZE * 0.5),
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
PrimitiveState::Capsule => gizmos.primitive_2d(
|
||||
Capsule2d {
|
||||
radius: SIZE * 0.5,
|
||||
half_length: SIZE,
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
PrimitiveState::Line => drop(gizmos.primitive_2d(
|
||||
Line2d {
|
||||
direction: Direction2d::X,
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
)),
|
||||
PrimitiveState::Plane => gizmos.primitive_2d(
|
||||
Plane2d {
|
||||
normal: Direction2d::Y,
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
PrimitiveState::Segment => drop(gizmos.primitive_2d(
|
||||
Segment2d {
|
||||
direction: Direction2d::X,
|
||||
half_length: SIZE * 0.5,
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
)),
|
||||
PrimitiveState::Triangle => gizmos.primitive_2d(
|
||||
Triangle2d {
|
||||
vertices: [Vec2::ZERO, Vec2::Y, Vec2::X].map(|p| p * SIZE * 0.5),
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
PrimitiveState::Rectangle => gizmos.primitive_2d(
|
||||
Rectangle {
|
||||
half_size: Vec2::splat(SIZE * 0.5),
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
PrimitiveState::RegularPolygon => gizmos.primitive_2d(
|
||||
RegularPolygon {
|
||||
circumcircle: Circle { radius: SIZE * 0.5 },
|
||||
sides: 5,
|
||||
},
|
||||
position,
|
||||
angle,
|
||||
color,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_config(
|
||||
mut config_store: ResMut<GizmoConfigStore>,
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
|
@ -101,3 +247,16 @@ fn update_config(
|
|||
my_config.enabled ^= true;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_primitives(
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
|
||||
primitive_state: Res<State<PrimitiveState>>,
|
||||
) {
|
||||
if keyboard.just_pressed(KeyCode::KeyJ) {
|
||||
next_primitive_state.set(primitive_state.get().last());
|
||||
}
|
||||
if keyboard.just_pressed(KeyCode::KeyK) {
|
||||
next_primitive_state.set(primitive_state.get().next());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,21 @@
|
|||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use bevy::math::primitives::Direction3d;
|
||||
use bevy::math::primitives::{
|
||||
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Segment3d, Sphere, Torus,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_state(PrimitiveState::Nothing)
|
||||
.init_resource::<PrimitiveSegments>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_gizmo_group::<MyRoundGizmos>()
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, (system, rotate_camera, update_config))
|
||||
.add_systems(Update, rotate_camera)
|
||||
.add_systems(Update, (draw_example_collection, update_config))
|
||||
.add_systems(Update, (draw_primitives, update_primitives))
|
||||
.run();
|
||||
}
|
||||
|
||||
|
@ -18,6 +24,62 @@ fn main() {
|
|||
#[derive(Default, Reflect, GizmoConfigGroup)]
|
||||
struct MyRoundGizmos {}
|
||||
|
||||
#[derive(Debug, Clone, Resource)]
|
||||
pub struct PrimitiveSegments(usize);
|
||||
impl Default for PrimitiveSegments {
|
||||
fn default() -> Self {
|
||||
Self(10)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)]
|
||||
enum PrimitiveState {
|
||||
Nothing,
|
||||
Sphere,
|
||||
Plane,
|
||||
Line,
|
||||
LineSegment,
|
||||
Cuboid,
|
||||
Cylinder,
|
||||
Capsule,
|
||||
Cone,
|
||||
ConicalFrustum,
|
||||
Torus,
|
||||
}
|
||||
|
||||
impl PrimitiveState {
|
||||
const ALL: [Self; 11] = [
|
||||
Self::Sphere,
|
||||
Self::Plane,
|
||||
Self::Line,
|
||||
Self::LineSegment,
|
||||
Self::Cuboid,
|
||||
Self::Cylinder,
|
||||
Self::Capsule,
|
||||
Self::Cone,
|
||||
Self::ConicalFrustum,
|
||||
Self::Torus,
|
||||
Self::Nothing,
|
||||
];
|
||||
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,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
|
@ -59,7 +121,9 @@ fn setup(
|
|||
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\n\
|
||||
Press 'A' to show all AABB boxes",
|
||||
Press 'A' to show all AABB boxes\n\
|
||||
Press 'K' or 'J' to cycle through primitives rendered with gizmos\n\
|
||||
Press 'H' or 'L' to decrease/increase the amount of segments in the primitives",
|
||||
TextStyle {
|
||||
font_size: 20.,
|
||||
..default()
|
||||
|
@ -74,7 +138,17 @@ fn setup(
|
|||
);
|
||||
}
|
||||
|
||||
fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Time>) {
|
||||
fn rotate_camera(mut query: Query<&mut Transform, With<Camera>>, time: Res<Time>) {
|
||||
let mut transform = query.single_mut();
|
||||
|
||||
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_seconds() / 2.));
|
||||
}
|
||||
|
||||
fn draw_example_collection(
|
||||
mut gizmos: Gizmos,
|
||||
mut my_gizmos: Gizmos<MyRoundGizmos>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(Vec3::Y * 0.5).with_scale(Vec3::splat(1.25)),
|
||||
Color::BLACK,
|
||||
|
@ -119,10 +193,143 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
|
|||
gizmos.arrow(Vec3::ZERO, Vec3::ONE * 1.5, Color::YELLOW);
|
||||
}
|
||||
|
||||
fn rotate_camera(mut query: Query<&mut Transform, With<Camera>>, time: Res<Time>) {
|
||||
let mut transform = query.single_mut();
|
||||
|
||||
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_seconds() / 2.));
|
||||
fn draw_primitives(
|
||||
mut gizmos: Gizmos,
|
||||
time: Res<Time>,
|
||||
primitive_state: Res<State<PrimitiveState>>,
|
||||
segments: Res<PrimitiveSegments>,
|
||||
) {
|
||||
let normal = Vec3::new(
|
||||
time.elapsed_seconds().sin(),
|
||||
time.elapsed_seconds().cos(),
|
||||
time.elapsed_seconds().sin().cos(),
|
||||
)
|
||||
.try_normalize()
|
||||
.unwrap_or(Vec3::X);
|
||||
let angle = time.elapsed_seconds().to_radians() * 10.0;
|
||||
let center = Quat::from_axis_angle(Vec3::Z, angle) * Vec3::X;
|
||||
let rotation = Quat::from_rotation_arc(Vec3::Y, normal);
|
||||
let segments = segments.0;
|
||||
match primitive_state.get() {
|
||||
PrimitiveState::Nothing => {}
|
||||
PrimitiveState::Sphere => {
|
||||
gizmos
|
||||
.primitive_3d(Sphere { radius: 1.0 }, center, rotation, Color::default())
|
||||
.segments(segments);
|
||||
}
|
||||
PrimitiveState::Plane => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
Plane3d {
|
||||
normal: Direction3d::Y,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.axis_count((segments / 5).max(4))
|
||||
.segment_count(segments)
|
||||
.segment_length(1.0 / segments as f32);
|
||||
}
|
||||
PrimitiveState::Line => {
|
||||
gizmos.primitive_3d(
|
||||
Line3d {
|
||||
direction: Direction3d::X,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
);
|
||||
}
|
||||
PrimitiveState::LineSegment => {
|
||||
gizmos.primitive_3d(
|
||||
Segment3d {
|
||||
direction: Direction3d::X,
|
||||
half_length: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
);
|
||||
}
|
||||
PrimitiveState::Cuboid => {
|
||||
gizmos.primitive_3d(
|
||||
Cuboid {
|
||||
half_size: Vec3::new(1.0, 0.5, 2.0),
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
);
|
||||
}
|
||||
PrimitiveState::Cylinder => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
Cylinder {
|
||||
radius: 1.0,
|
||||
half_height: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.segments(segments);
|
||||
}
|
||||
PrimitiveState::Capsule => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
Capsule3d {
|
||||
radius: 1.0,
|
||||
half_length: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.segments(segments);
|
||||
}
|
||||
PrimitiveState::Cone => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
Cone {
|
||||
radius: 1.0,
|
||||
height: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.segments(segments);
|
||||
}
|
||||
PrimitiveState::ConicalFrustum => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
ConicalFrustum {
|
||||
radius_top: 0.5,
|
||||
radius_bottom: 1.0,
|
||||
height: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.segments(segments);
|
||||
}
|
||||
PrimitiveState::Torus => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
Torus {
|
||||
minor_radius: 0.3,
|
||||
major_radius: 1.0,
|
||||
},
|
||||
center,
|
||||
rotation,
|
||||
Color::default(),
|
||||
)
|
||||
.major_segments(segments)
|
||||
.minor_segments((segments / 4).max(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_config(
|
||||
|
@ -176,3 +383,26 @@ fn update_config(
|
|||
config_store.config_mut::<AabbGizmoConfigGroup>().1.draw_all ^= true;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_primitives(
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
primitive_state: Res<State<PrimitiveState>>,
|
||||
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
|
||||
mut segments: ResMut<PrimitiveSegments>,
|
||||
mut segments_f: Local<f32>,
|
||||
) {
|
||||
if keyboard.just_pressed(KeyCode::KeyK) {
|
||||
next_primitive_state.set(primitive_state.get().next());
|
||||
}
|
||||
if keyboard.just_pressed(KeyCode::KeyJ) {
|
||||
next_primitive_state.set(primitive_state.get().last());
|
||||
}
|
||||
if keyboard.pressed(KeyCode::KeyL) {
|
||||
*segments_f = (*segments_f + 0.05).max(2.0);
|
||||
segments.0 = segments_f.floor() as usize;
|
||||
}
|
||||
if keyboard.pressed(KeyCode::KeyH) {
|
||||
*segments_f = (*segments_f - 0.05).max(2.0);
|
||||
segments.0 = segments_f.floor() as usize;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue