Skip to content

Commit

Permalink
Add Rounded box gizmos (#11948)
Browse files Browse the repository at this point in the history
# Objective

- Implement rounded cuboids and rectangles, suggestion of #9400 

## Solution

- Added `Gizmos::rounded_cuboid`, `Gizmos::rounded_rect` and
`Gizmos::rounded_rect_2d`.
- All of these return builders that allow configuring of the corner/edge
radius using `.corner_radius(...)` or `.edge_radius(...)` as well as the
line segments of each arc using `.arc_segments(...)`.

---

## Changelog

- Added a new `rounded_box` module to `bevy_gizmos` containing all of
the above methods and builders.
- Updated the examples `2d_gizmos` and `3d_gizmos`

## Additional information

The 3d example now looks like this:

<img width="1440" alt="Screenshot 2024-02-28 at 01 47 28"
src="https://github.com/bevyengine/bevy/assets/62256001/654e30ca-c091-4f14-a402-90138e95c71b">

And this is the updated 2d example showcasing negative corner radius:

<img width="1440" alt="Screenshot 2024-02-28 at 01 59 37"
src="https://github.com/bevyengine/bevy/assets/62256001/3904697a-5462-4ee7-abd9-3e893ca07082">
<img width="1440" alt="Screenshot 2024-02-28 at 01 59 47"
src="https://github.com/bevyengine/bevy/assets/62256001/a8892cfd-3aad-4c0c-87eb-559c17c8864c">

---------

Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
Co-authored-by: James Gayfer <10660608+jgayfer@users.noreply.github.com>
  • Loading branch information
3 people authored May 16, 2024
1 parent 65e62ba commit 05e2552
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 0 deletions.
1 change: 1 addition & 0 deletions crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod config;
pub mod gizmos;
pub mod grid;
pub mod primitives;
pub mod rounded_box;

#[cfg(feature = "bevy_pbr")]
pub mod light;
Expand Down
380 changes: 380 additions & 0 deletions crates/bevy_gizmos/src/rounded_box.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,380 @@
//! Additional [`Gizmos`] Functions -- Rounded cuboids and rectangles
//!
//! Includes the implementation of [`Gizmos::rounded_rect`], [`Gizmos::rounded_rect_2d`] and [`Gizmos::rounded_cuboid`].
//! and assorted support items.

use std::f32::consts::FRAC_PI_2;

use crate::prelude::{GizmoConfigGroup, Gizmos};
use bevy_color::Color;
use bevy_math::{Quat, Vec2, Vec3};
use bevy_transform::components::Transform;

/// A builder returned by [`Gizmos::rounded_rect`] and [`Gizmos::rounded_rect_2d`]
pub struct RoundedRectBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
size: Vec2,
gizmos: &'a mut Gizmos<'w, 's, T>,
config: RoundedBoxConfig,
}
/// A builder returned by [`Gizmos::rounded_cuboid`]
pub struct RoundedCuboidBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
size: Vec3,
gizmos: &'a mut Gizmos<'w, 's, T>,
config: RoundedBoxConfig,
}
struct RoundedBoxConfig {
position: Vec3,
rotation: Quat,
color: Color,
corner_radius: f32,
arc_segments: usize,
}

impl<T: GizmoConfigGroup> RoundedRectBuilder<'_, '_, '_, T> {
/// Change the radius of the corners to be `corner_radius`.
/// The default corner radius is [min axis of size] / 10.0
pub fn corner_radius(mut self, corner_radius: f32) -> Self {
self.config.corner_radius = corner_radius;
self
}

/// Change the segments of the arcs at the corners of the rectangle.
/// The default value is 8
pub fn arc_segments(mut self, arc_segments: usize) -> Self {
self.config.arc_segments = arc_segments;
self
}
}
impl<T: GizmoConfigGroup> RoundedCuboidBuilder<'_, '_, '_, T> {
/// Change the radius of the edges to be `edge_radius`.
/// The default edge radius is [min axis of size] / 10.0
pub fn edge_radius(mut self, edge_radius: f32) -> Self {
self.config.corner_radius = edge_radius;
self
}

/// Change the segments of the arcs at the edges of the cuboid.
/// The default value is 8
pub fn arc_segments(mut self, arc_segments: usize) -> Self {
self.config.arc_segments = arc_segments;
self
}
}

impl<T: GizmoConfigGroup> Drop for RoundedRectBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let config = &self.config;

// Calculate inner and outer half size and ensure that the edge_radius is <= any half_length
let mut outer_half_size = self.size.abs() / 2.0;
let inner_half_size =
(outer_half_size - Vec2::splat(config.corner_radius.abs())).max(Vec2::ZERO);
let corner_radius = (outer_half_size - inner_half_size).min_element();
let mut inner_half_size = outer_half_size - Vec2::splat(corner_radius);

if config.corner_radius < 0. {
std::mem::swap(&mut outer_half_size, &mut inner_half_size);
}

// Handle cases where the rectangle collapses into simpler shapes
if outer_half_size.x * outer_half_size.y == 0. {
self.gizmos.line(
config.position + config.rotation * -outer_half_size.extend(0.),
config.position + config.rotation * outer_half_size.extend(0.),
config.color,
);
return;
}
if corner_radius == 0. {
self.gizmos
.rect(config.position, config.rotation, self.size, config.color);
return;
}

let vertices = [
// top right
Vec3::new(inner_half_size.x, outer_half_size.y, 0.),
Vec3::new(inner_half_size.x, inner_half_size.y, 0.),
Vec3::new(outer_half_size.x, inner_half_size.y, 0.),
// bottom right
Vec3::new(outer_half_size.x, -inner_half_size.y, 0.),
Vec3::new(inner_half_size.x, -inner_half_size.y, 0.),
Vec3::new(inner_half_size.x, -outer_half_size.y, 0.),
// bottom left
Vec3::new(-inner_half_size.x, -outer_half_size.y, 0.),
Vec3::new(-inner_half_size.x, -inner_half_size.y, 0.),
Vec3::new(-outer_half_size.x, -inner_half_size.y, 0.),
// top left
Vec3::new(-outer_half_size.x, inner_half_size.y, 0.),
Vec3::new(-inner_half_size.x, inner_half_size.y, 0.),
Vec3::new(-inner_half_size.x, outer_half_size.y, 0.),
]
.map(|v| config.position + config.rotation * v);

for chunk in vertices.chunks_exact(3) {
self.gizmos
.short_arc_3d_between(chunk[1], chunk[0], chunk[2], config.color)
.segments(config.arc_segments);
}

let edges = if config.corner_radius > 0. {
[
(vertices[2], vertices[3]),
(vertices[5], vertices[6]),
(vertices[8], vertices[9]),
(vertices[11], vertices[0]),
]
} else {
[
(vertices[0], vertices[5]),
(vertices[3], vertices[8]),
(vertices[6], vertices[11]),
(vertices[9], vertices[2]),
]
};

for (start, end) in edges {
self.gizmos.line(start, end, config.color);
}
}
}

impl<T: GizmoConfigGroup> Drop for RoundedCuboidBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let config = &self.config;

// Calculate inner and outer half size and ensure that the edge_radius is <= any half_length
let outer_half_size = self.size.abs() / 2.0;
let inner_half_size =
(outer_half_size - Vec3::splat(config.corner_radius.abs())).max(Vec3::ZERO);
let mut edge_radius = (outer_half_size - inner_half_size).min_element();
let inner_half_size = outer_half_size - Vec3::splat(edge_radius);
edge_radius *= config.corner_radius.signum();

// Handle cases where the rounded cuboid collapses into simpler shapes
if edge_radius == 0.0 {
let transform = Transform::from_translation(config.position)
.with_rotation(config.rotation)
.with_scale(self.size);
self.gizmos.cuboid(transform, config.color);
return;
}

let rects = [
(
Vec3::X,
Vec2::new(self.size.z, self.size.y),
Quat::from_rotation_y(FRAC_PI_2),
),
(
Vec3::Y,
Vec2::new(self.size.x, self.size.z),
Quat::from_rotation_x(FRAC_PI_2),
),
(Vec3::Z, Vec2::new(self.size.x, self.size.y), Quat::IDENTITY),
];

for (position, size, rotation) in rects {
let world_rotation = config.rotation * rotation;
let local_position = config.rotation * (position * inner_half_size);
self.gizmos
.rounded_rect(
config.position + local_position,
world_rotation,
size,
config.color,
)
.arc_segments(config.arc_segments)
.corner_radius(edge_radius);

self.gizmos
.rounded_rect(
config.position - local_position,
world_rotation,
size,
config.color,
)
.arc_segments(config.arc_segments)
.corner_radius(edge_radius);
}
}
}

impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
/// Draw a wireframe rectangle with rounded corners in 3D.
///
/// This should be called for each frame the rectangle needs to be rendered.
///
/// # Arguments
///
/// - `position`: The center point of the rectangle.
/// - `rotation`: defines orientation of the rectangle, by default we assume the rectangle is contained in a plane parallel to the XY plane.
/// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box.
/// - `color`: color of the rectangle
///
/// # Builder methods
///
/// - The corner radius can be adjusted with the `.corner_radius(...)` method.
/// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the
/// `.arc_segments(...)` method.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::palettes::css::GREEN;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.rounded_rect(
/// Vec3::ZERO,
/// Quat::IDENTITY,
/// Vec2::ONE,
/// GREEN
/// )
/// .corner_radius(0.25)
/// .arc_segments(10);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
pub fn rounded_rect(
&mut self,
position: Vec3,
rotation: Quat,
size: Vec2,
color: impl Into<Color>,
) -> RoundedRectBuilder<'_, 'w, 's, T> {
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
RoundedRectBuilder {
gizmos: self,
config: RoundedBoxConfig {
position,
rotation,
color: color.into(),
corner_radius,
arc_segments: DEFAULT_ARC_SEGMENTS,
},
size,
}
}

/// Draw a wireframe rectangle with rounded corners in 2D.
///
/// This should be called for each frame the rectangle needs to be rendered.
///
/// # Arguments
///
/// - `position`: The center point of the rectangle.
/// - `rotation`: defines orientation of the rectangle.
/// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box.
/// - `color`: color of the rectangle
///
/// # Builder methods
///
/// - The corner radius can be adjusted with the `.corner_radius(...)` method.
/// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the
/// `.arc_segments(...)` method.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::palettes::css::GREEN;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.rounded_rect_2d(
/// Vec2::ZERO,
/// 0.,
/// Vec2::ONE,
/// GREEN
/// )
/// .corner_radius(0.25)
/// .arc_segments(10);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
pub fn rounded_rect_2d(
&mut self,
position: Vec2,
rotation: f32,
size: Vec2,
color: impl Into<Color>,
) -> RoundedRectBuilder<'_, 'w, 's, T> {
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
RoundedRectBuilder {
gizmos: self,
config: RoundedBoxConfig {
position: position.extend(0.),
rotation: Quat::from_rotation_z(rotation),
color: color.into(),
corner_radius,
arc_segments: DEFAULT_ARC_SEGMENTS,
},
size,
}
}

/// Draw a wireframe cuboid with rounded corners in 3D.
///
/// This should be called for each frame the cuboid needs to be rendered.
///
/// # Arguments
///
/// - `position`: The center point of the cuboid.
/// - `rotation`: defines orientation of the cuboid.
/// - `size`: defines the size of the cuboid. This refers to the 'outer size', similar to a bounding box.
/// - `color`: color of the cuboid
///
/// # Builder methods
///
/// - The edge radius can be adjusted with the `.edge_radius(...)` method.
/// - The number of segments of the arcs at each edge (i.e. the level of detail) can be adjusted with the
/// `.arc_segments(...)` method.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use bevy_color::palettes::css::GREEN;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.rounded_cuboid(
/// Vec3::ZERO,
/// Quat::IDENTITY,
/// Vec3::ONE,
/// GREEN
/// )
/// .edge_radius(0.25)
/// .arc_segments(10);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
pub fn rounded_cuboid(
&mut self,
position: Vec3,
rotation: Quat,
size: Vec3,
color: impl Into<Color>,
) -> RoundedCuboidBuilder<'_, 'w, 's, T> {
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
RoundedCuboidBuilder {
gizmos: self,
config: RoundedBoxConfig {
position,
rotation,
color: color.into(),
corner_radius,
arc_segments: DEFAULT_ARC_SEGMENTS,
},
size,
}
}
}

const DEFAULT_ARC_SEGMENTS: usize = 8;
const DEFAULT_CORNER_RADIUS: f32 = 0.1;
Loading

0 comments on commit 05e2552

Please sign in to comment.