More gizmos builders (#13261)

# Objective

- Add GizmoBuilders for some primitives as discussed in #13233

## Solution

- `gizmos.primitive_2d(CIRCLE)` and `gizmos.primitive_2d(ELLIPSE)` now
return `Ellipse2dBuilder` aswell.
- `gizmos.primitive_3d(SPHERE)` and `gizmos.sphere()` now return the
same `SphereBuilder`.
- the `.circle_segments` method on the `SphereBuilder` that used to be
returned by `.sphere()` is now called `.segments`
  - the sphere primitive gizmo now matches the `gizmos.sphere` gizmo
- `gizmos.primitive_2d(ANNULUS)` now returns a `Annulus2dBuilder`
allowing the configuration of the `segments`
- gizmos cylinders and capsules now have only 1 line per axis, similar
to `gizmos.sphere`

## Migration Guide

- Some `gizmos.primitive_nd` methods now return some or different
builders. You may need to adjust types and match statements
- Replace any calls to `circle_segments()` with `.segments()`

---------

Co-authored-by: Raphael Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com>
This commit is contained in:
Lynn 2024-06-03 18:10:14 +02:00 committed by GitHub
parent cca4fc76de
commit e6a0f75a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 259 additions and 285 deletions

View File

@ -178,6 +178,45 @@ where
resolution: DEFAULT_CIRCLE_RESOLUTION,
}
}
/// Draw a wireframe sphere in 3D made out of 3 circles around the axes.
///
/// This should be called for each frame the sphere needs to be rendered.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::Color;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.sphere(Vec3::ZERO, Quat::IDENTITY, 1., Color::BLACK);
///
/// // Each circle has 32 line-segments by default.
/// // You may want to increase this for larger spheres.
/// gizmos
/// .sphere(Vec3::ZERO, Quat::IDENTITY, 5., Color::BLACK)
/// .resolution(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn sphere(
&mut self,
position: Vec3,
rotation: Quat,
radius: f32,
color: impl Into<Color>,
) -> SphereBuilder<'_, 'w, 's, Config, Clear> {
SphereBuilder {
gizmos: self,
radius,
position,
rotation,
color: color.into(),
resolution: DEFAULT_CIRCLE_RESOLUTION,
}
}
}
/// A builder returned by [`Gizmos::ellipse`].
@ -266,3 +305,66 @@ where
self.gizmos.linestrip_2d(positions, self.color);
}
}
/// Builder for configuring the drawing options of [`Sphere`].
pub struct SphereBuilder<'a, 'w, 's, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
gizmos: &'a mut Gizmos<'w, 's, Config, Clear>,
// 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 line-segments used to approximate the sphere geometry
resolution: usize,
}
impl<Config, Clear> SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
/// Set the number of line-segments used to approximate the sphere geometry.
pub fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}
}
impl<Config, Clear> Drop for SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let SphereBuilder {
radius,
position: center,
rotation,
color,
resolution,
..
} = self;
// draws one great circle around each of the local axes
Vec3::AXES.into_iter().for_each(|axis| {
let normal = *rotation * axis;
self.gizmos
.circle(*center, Dir3::new_unchecked(normal), *radius, *color)
.resolution(*resolution);
});
}
}

View File

@ -2,14 +2,13 @@
use std::{iter, marker::PhantomData, mem};
use crate::circles::DEFAULT_CIRCLE_RESOLUTION;
use bevy_color::{Color, LinearRgba};
use bevy_ecs::{
component::Tick,
system::{Deferred, ReadOnlySystemParam, Res, Resource, SystemBuffer, SystemMeta, SystemParam},
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
use bevy_math::{Dir3, Quat, Rotation2d, Vec2, Vec3};
use bevy_math::{Quat, Rotation2d, Vec2, Vec3};
use bevy_transform::TransformPoint;
use bevy_utils::default;
@ -459,45 +458,6 @@ where
strip_colors.push(LinearRgba::NAN);
}
/// Draw a wireframe sphere in 3D made out of 3 circles around the axes.
///
/// This should be called for each frame the sphere needs to be rendered.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::Color;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.sphere(Vec3::ZERO, Quat::IDENTITY, 1., Color::BLACK);
///
/// // Each circle has 32 line-segments by default.
/// // You may want to increase this for larger spheres.
/// gizmos
/// .sphere(Vec3::ZERO, Quat::IDENTITY, 5., Color::BLACK)
/// .resolution(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn sphere(
&mut self,
position: Vec3,
rotation: Quat,
radius: f32,
color: impl Into<Color>,
) -> SphereBuilder<'_, 'w, 's, Config, Clear> {
SphereBuilder {
gizmos: self,
position,
rotation: rotation.normalize(),
radius,
color: color.into(),
resolution: DEFAULT_CIRCLE_RESOLUTION,
}
}
/// Draw a wireframe rectangle in 3D.
///
/// This should be called for each frame the rectangle needs to be rendered.
@ -790,49 +750,6 @@ where
}
}
/// A builder returned by [`Gizmos::sphere`].
pub struct SphereBuilder<'a, 'w, 's, Config, Clear = ()>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
gizmos: &'a mut Gizmos<'w, 's, Config, Clear>,
position: Vec3,
rotation: Quat,
radius: f32,
color: Color,
resolution: usize,
}
impl<Config, Clear> SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
/// Set the number of line-segments per circle for this sphere.
pub fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}
}
impl<Config, Clear> Drop for SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
for axis in Dir3::AXES {
self.gizmos
.circle(self.position, self.rotation * axis, self.radius, self.color)
.resolution(self.resolution);
}
}
}
fn rect_inner(size: Vec2) -> [Vec2; 4] {
let half_size = size / 2.;
let tl = Vec2::new(-half_size.x, half_size.y);

View File

@ -102,7 +102,7 @@ where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
type Output<'a> = () where Self: 'a;
type Output<'a> = crate::circles::Ellipse2dBuilder<'a, 'w, 's, Config, Clear> where Self: 'a;
fn primitive_2d(
&mut self,
@ -111,11 +111,7 @@ where
_angle: f32,
color: impl Into<Color>,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.circle_2d(position, primitive.radius, color);
self.circle_2d(position, primitive.radius, color)
}
}
@ -205,46 +201,114 @@ where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
type Output<'a> = () where Self: 'a;
type Output<'a> = crate::circles::Ellipse2dBuilder<'a, 'w, 's, Config, Clear> where Self: 'a;
fn primitive_2d(
fn primitive_2d<'a>(
&mut self,
primitive: &Ellipse,
position: Vec2,
angle: f32,
color: impl Into<Color>,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.ellipse_2d(position, angle, primitive.half_size, color);
self.ellipse_2d(position, angle, primitive.half_size, color)
}
}
// annulus 2d
/// Builder for configuring the drawing options of [`Annulus`].
pub struct Annulus2dBuilder<'a, 'w, 's, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
gizmos: &'a mut Gizmos<'w, 's, Config, Clear>,
position: Vec2,
inner_radius: f32,
outer_radius: f32,
color: Color,
inner_resolution: usize,
outer_resolution: usize,
}
impl<Config, Clear> Annulus2dBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
/// Set the number of line-segments for each circle of the annulus.
pub fn resolution(mut self, resolution: usize) -> Self {
self.outer_resolution = resolution;
self.inner_resolution = resolution;
self
}
/// Set the number of line-segments for the outer circle of the annulus.
pub fn outer_resolution(mut self, resolution: usize) -> Self {
self.outer_resolution = resolution;
self
}
/// Set the number of line-segments for the inner circle of the annulus.
pub fn inner_resolution(mut self, resolution: usize) -> Self {
self.inner_resolution = resolution;
self
}
}
impl<'w, 's, Config, Clear> GizmoPrimitive2d<Annulus> for Gizmos<'w, 's, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
type Output<'a> = () where Self: 'a;
type Output<'a> = Annulus2dBuilder<'a, 'w, 's, Config, Clear> where Self: 'a;
fn primitive_2d(
&mut self,
primitive: &Annulus,
position: Vec2,
angle: f32,
_angle: f32,
color: impl Into<Color>,
) -> Self::Output<'_> {
if !self.enabled {
Annulus2dBuilder {
gizmos: self,
position,
inner_radius: primitive.inner_circle.radius,
outer_radius: primitive.outer_circle.radius,
color: color.into(),
inner_resolution: crate::circles::DEFAULT_CIRCLE_RESOLUTION,
outer_resolution: crate::circles::DEFAULT_CIRCLE_RESOLUTION,
}
}
}
impl<Config, Clear> Drop for Annulus2dBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let color = color.into();
self.primitive_2d(&primitive.inner_circle, position, angle, color);
self.primitive_2d(&primitive.outer_circle, position, angle, color);
let Annulus2dBuilder {
gizmos,
position,
inner_radius,
outer_radius,
inner_resolution,
outer_resolution,
color,
..
} = self;
gizmos
.circle_2d(*position, *outer_radius, *color)
.resolution(*outer_resolution);
gizmos
.circle_2d(*position, *inner_radius, *color)
.resolution(*inner_resolution);
}
}
@ -266,8 +330,7 @@ where
) -> Self::Output<'_> {
if !self.enabled {
return;
}
};
let [a, b, c, d] =
[(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)].map(|(sign_x, sign_y)| {
Vec2::new(

View File

@ -1,7 +1,7 @@
//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`].
use super::helpers::*;
use std::f32::consts::TAU;
use std::f32::consts::{FRAC_PI_2, PI, TAU};
use bevy_color::Color;
use bevy_math::primitives::{
@ -10,6 +10,7 @@ use bevy_math::primitives::{
};
use bevy_math::{Dir3, Quat, Vec3};
use crate::circles::SphereBuilder;
use crate::prelude::{GizmoConfigGroup, Gizmos};
const DEFAULT_RESOLUTION: usize = 5;
@ -55,41 +56,6 @@ where
// sphere
/// Builder for configuring the drawing options of [`Sphere`].
pub struct SphereBuilder<'a, 'w, 's, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
gizmos: &'a mut Gizmos<'w, 's, Config, Clear>,
// 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,
// Resolution of the gizmos used to approximate the sphere geometry
// The number of vertices used to approximate the sphere geometry.
resolution: usize,
}
impl<Config, Clear> SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
/// Set the number of lines used to approximate the sphere geometry.
pub fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}
}
impl<'w, 's, Config, Clear> GizmoPrimitive3d<Sphere> for Gizmos<'w, 's, Config, Clear>
where
Config: GizmoConfigGroup,
@ -104,59 +70,7 @@ where
rotation: Quat,
color: impl Into<Color>,
) -> Self::Output<'_> {
SphereBuilder {
gizmos: self,
radius: primitive.radius,
position,
rotation,
color: color.into(),
resolution: DEFAULT_RESOLUTION,
}
}
}
impl<Config, Clear> Drop for SphereBuilder<'_, '_, '_, Config, Clear>
where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let SphereBuilder {
radius,
position: center,
rotation,
color,
resolution,
..
} = 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,
*resolution,
*rotation,
*center,
top,
*color,
);
});
// draws one great circle of the sphere
draw_circle_3d(
self.gizmos,
*radius,
*resolution,
*rotation,
*center,
*color,
);
self.sphere(position, rotation, primitive.radius, color)
}
}
@ -578,30 +492,27 @@ where
resolution,
} = self;
let normal = *rotation * Vec3::Y;
let normal = Dir3::new_unchecked(*rotation * Vec3::Y);
let up = normal.as_vec3() * *half_height;
// draw upper and lower circle of the cylinder
[-1.0, 1.0].into_iter().for_each(|sign| {
draw_circle_3d(
gizmos,
*radius,
*resolution,
*rotation,
*position + sign * *half_height * normal,
*color,
);
gizmos
.circle(*position + sign * up, normal, *radius, *color)
.resolution(*resolution);
});
// draw lines connecting the two cylinder circles
draw_cylinder_vertical_lines(
gizmos,
*radius,
*resolution,
*half_height,
*rotation,
*position,
[Vec3::NEG_X, Vec3::NEG_Z, Vec3::X, Vec3::Z]
.into_iter()
.for_each(|axis| {
let axis = *rotation * axis;
gizmos.line(
*position + up + axis * *radius,
*position - up + axis * *radius,
*color,
);
});
}
}
@ -691,26 +602,57 @@ where
resolution,
} = 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, *resolution, *rotation, center, top, *color);
draw_circle_3d(gizmos, *radius, *resolution, *rotation, center, *color);
});
// connect the two semi spheres with lines
draw_cylinder_vertical_lines(
gizmos,
// Draw the circles at the top and bottom of the cylinder
let y_offset = *rotation * Vec3::Y;
gizmos
.circle(
*position + y_offset * *half_length,
Dir3::new_unchecked(y_offset),
*radius,
*resolution,
*half_length,
*rotation,
*position,
*color,
)
.resolution(*resolution);
gizmos
.circle(
*position - y_offset * *half_length,
Dir3::new_unchecked(y_offset),
*radius,
*color,
)
.resolution(*resolution);
let y_offset = y_offset * *half_length;
// Draw the vertical lines and the cap semicircles
[Vec3::X, Vec3::Z].into_iter().for_each(|axis| {
let normal = *rotation * axis;
gizmos.line(
*position + normal * *radius + y_offset,
*position + normal * *radius - y_offset,
*color,
);
gizmos.line(
*position - normal * *radius + y_offset,
*position - normal * *radius - y_offset,
*color,
);
let rotation = *rotation
* Quat::from_euler(bevy_math::EulerRot::ZYX, 0., axis.z * FRAC_PI_2, FRAC_PI_2);
gizmos
.arc_3d(PI, *radius, *position + y_offset, rotation, *color)
.resolution(*resolution / 2);
gizmos
.arc_3d(
PI,
*radius,
*position - y_offset,
rotation * Quat::from_rotation_y(PI),
*color,
)
.resolution(*resolution / 2);
});
}
}

View File

@ -46,33 +46,6 @@ pub(crate) fn circle_coordinates(radius: f32, resolution: usize) -> impl Iterato
.take(resolution)
}
/// Draws a semi-sphere.
///
/// This function draws a semi-sphere at the specified `center` point with the given `rotation`,
/// `radius`, and `color`. The `resolution` 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<Config, Clear>(
gizmos: &mut Gizmos<'_, '_, Config, Clear>,
radius: f32,
resolution: usize,
rotation: Quat,
center: Vec3,
top: Vec3,
color: Color,
) where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
circle_coordinates(radius, resolution)
.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)
.resolution(resolution / 2);
});
}
/// Draws a circle in 3D space.
///
/// # Note
@ -97,28 +70,3 @@ pub(crate) fn draw_circle_3d<Config, Clear>(
.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<Config, Clear>(
gizmos: &mut Gizmos<'_, '_, Config, Clear>,
radius: f32,
resolution: usize,
half_height: f32,
rotation: Quat,
center: Vec3,
color: Color,
) where
Config: GizmoConfigGroup,
Clear: 'static + Send + Sync,
{
circle_coordinates(radius, resolution)
.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);
});
}

View File

@ -452,8 +452,10 @@ fn draw_gizmos_2d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time
PrimitiveSelected::RectangleAndCuboid => {
gizmos.primitive_2d(&RECTANGLE, POSITION, angle, color);
}
PrimitiveSelected::CircleAndSphere => gizmos.primitive_2d(&CIRCLE, POSITION, angle, color),
PrimitiveSelected::Ellipse => gizmos.primitive_2d(&ELLIPSE, POSITION, angle, color),
PrimitiveSelected::CircleAndSphere => {
gizmos.primitive_2d(&CIRCLE, POSITION, angle, color);
}
PrimitiveSelected::Ellipse => drop(gizmos.primitive_2d(&ELLIPSE, POSITION, angle, color)),
PrimitiveSelected::Triangle => gizmos.primitive_2d(&TRIANGLE_2D, POSITION, angle, color),
PrimitiveSelected::Plane => gizmos.primitive_2d(&PLANE_2D, POSITION, angle, color),
PrimitiveSelected::Line => drop(gizmos.primitive_2d(&LINE2D, POSITION, angle, color)),
@ -469,7 +471,7 @@ fn draw_gizmos_2d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time
PrimitiveSelected::Cylinder => {}
PrimitiveSelected::Cone => {}
PrimitiveSelected::ConicalFrustum => {}
PrimitiveSelected::Torus => gizmos.primitive_2d(&ANNULUS, POSITION, angle, color),
PrimitiveSelected::Torus => drop(gizmos.primitive_2d(&ANNULUS, POSITION, angle, color)),
PrimitiveSelected::Tetrahedron => {}
PrimitiveSelected::Arc => gizmos.primitive_2d(&ARC, POSITION, angle, color),
PrimitiveSelected::CircularSector => {