Skip to content

Commit 05e2552

Browse files
lynn-lumenJMS55jgayfer
authored
Add Rounded box gizmos (#11948)
# 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 <[email protected]> Co-authored-by: James Gayfer <[email protected]>
1 parent 65e62ba commit 05e2552

File tree

4 files changed

+400
-0
lines changed

4 files changed

+400
-0
lines changed

crates/bevy_gizmos/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub mod config;
3939
pub mod gizmos;
4040
pub mod grid;
4141
pub mod primitives;
42+
pub mod rounded_box;
4243

4344
#[cfg(feature = "bevy_pbr")]
4445
pub mod light;

crates/bevy_gizmos/src/rounded_box.rs

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
//! Additional [`Gizmos`] Functions -- Rounded cuboids and rectangles
2+
//!
3+
//! Includes the implementation of [`Gizmos::rounded_rect`], [`Gizmos::rounded_rect_2d`] and [`Gizmos::rounded_cuboid`].
4+
//! and assorted support items.
5+
6+
use std::f32::consts::FRAC_PI_2;
7+
8+
use crate::prelude::{GizmoConfigGroup, Gizmos};
9+
use bevy_color::Color;
10+
use bevy_math::{Quat, Vec2, Vec3};
11+
use bevy_transform::components::Transform;
12+
13+
/// A builder returned by [`Gizmos::rounded_rect`] and [`Gizmos::rounded_rect_2d`]
14+
pub struct RoundedRectBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
15+
size: Vec2,
16+
gizmos: &'a mut Gizmos<'w, 's, T>,
17+
config: RoundedBoxConfig,
18+
}
19+
/// A builder returned by [`Gizmos::rounded_cuboid`]
20+
pub struct RoundedCuboidBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
21+
size: Vec3,
22+
gizmos: &'a mut Gizmos<'w, 's, T>,
23+
config: RoundedBoxConfig,
24+
}
25+
struct RoundedBoxConfig {
26+
position: Vec3,
27+
rotation: Quat,
28+
color: Color,
29+
corner_radius: f32,
30+
arc_segments: usize,
31+
}
32+
33+
impl<T: GizmoConfigGroup> RoundedRectBuilder<'_, '_, '_, T> {
34+
/// Change the radius of the corners to be `corner_radius`.
35+
/// The default corner radius is [min axis of size] / 10.0
36+
pub fn corner_radius(mut self, corner_radius: f32) -> Self {
37+
self.config.corner_radius = corner_radius;
38+
self
39+
}
40+
41+
/// Change the segments of the arcs at the corners of the rectangle.
42+
/// The default value is 8
43+
pub fn arc_segments(mut self, arc_segments: usize) -> Self {
44+
self.config.arc_segments = arc_segments;
45+
self
46+
}
47+
}
48+
impl<T: GizmoConfigGroup> RoundedCuboidBuilder<'_, '_, '_, T> {
49+
/// Change the radius of the edges to be `edge_radius`.
50+
/// The default edge radius is [min axis of size] / 10.0
51+
pub fn edge_radius(mut self, edge_radius: f32) -> Self {
52+
self.config.corner_radius = edge_radius;
53+
self
54+
}
55+
56+
/// Change the segments of the arcs at the edges of the cuboid.
57+
/// The default value is 8
58+
pub fn arc_segments(mut self, arc_segments: usize) -> Self {
59+
self.config.arc_segments = arc_segments;
60+
self
61+
}
62+
}
63+
64+
impl<T: GizmoConfigGroup> Drop for RoundedRectBuilder<'_, '_, '_, T> {
65+
fn drop(&mut self) {
66+
if !self.gizmos.enabled {
67+
return;
68+
}
69+
let config = &self.config;
70+
71+
// Calculate inner and outer half size and ensure that the edge_radius is <= any half_length
72+
let mut outer_half_size = self.size.abs() / 2.0;
73+
let inner_half_size =
74+
(outer_half_size - Vec2::splat(config.corner_radius.abs())).max(Vec2::ZERO);
75+
let corner_radius = (outer_half_size - inner_half_size).min_element();
76+
let mut inner_half_size = outer_half_size - Vec2::splat(corner_radius);
77+
78+
if config.corner_radius < 0. {
79+
std::mem::swap(&mut outer_half_size, &mut inner_half_size);
80+
}
81+
82+
// Handle cases where the rectangle collapses into simpler shapes
83+
if outer_half_size.x * outer_half_size.y == 0. {
84+
self.gizmos.line(
85+
config.position + config.rotation * -outer_half_size.extend(0.),
86+
config.position + config.rotation * outer_half_size.extend(0.),
87+
config.color,
88+
);
89+
return;
90+
}
91+
if corner_radius == 0. {
92+
self.gizmos
93+
.rect(config.position, config.rotation, self.size, config.color);
94+
return;
95+
}
96+
97+
let vertices = [
98+
// top right
99+
Vec3::new(inner_half_size.x, outer_half_size.y, 0.),
100+
Vec3::new(inner_half_size.x, inner_half_size.y, 0.),
101+
Vec3::new(outer_half_size.x, inner_half_size.y, 0.),
102+
// bottom right
103+
Vec3::new(outer_half_size.x, -inner_half_size.y, 0.),
104+
Vec3::new(inner_half_size.x, -inner_half_size.y, 0.),
105+
Vec3::new(inner_half_size.x, -outer_half_size.y, 0.),
106+
// bottom left
107+
Vec3::new(-inner_half_size.x, -outer_half_size.y, 0.),
108+
Vec3::new(-inner_half_size.x, -inner_half_size.y, 0.),
109+
Vec3::new(-outer_half_size.x, -inner_half_size.y, 0.),
110+
// top left
111+
Vec3::new(-outer_half_size.x, inner_half_size.y, 0.),
112+
Vec3::new(-inner_half_size.x, inner_half_size.y, 0.),
113+
Vec3::new(-inner_half_size.x, outer_half_size.y, 0.),
114+
]
115+
.map(|v| config.position + config.rotation * v);
116+
117+
for chunk in vertices.chunks_exact(3) {
118+
self.gizmos
119+
.short_arc_3d_between(chunk[1], chunk[0], chunk[2], config.color)
120+
.segments(config.arc_segments);
121+
}
122+
123+
let edges = if config.corner_radius > 0. {
124+
[
125+
(vertices[2], vertices[3]),
126+
(vertices[5], vertices[6]),
127+
(vertices[8], vertices[9]),
128+
(vertices[11], vertices[0]),
129+
]
130+
} else {
131+
[
132+
(vertices[0], vertices[5]),
133+
(vertices[3], vertices[8]),
134+
(vertices[6], vertices[11]),
135+
(vertices[9], vertices[2]),
136+
]
137+
};
138+
139+
for (start, end) in edges {
140+
self.gizmos.line(start, end, config.color);
141+
}
142+
}
143+
}
144+
145+
impl<T: GizmoConfigGroup> Drop for RoundedCuboidBuilder<'_, '_, '_, T> {
146+
fn drop(&mut self) {
147+
if !self.gizmos.enabled {
148+
return;
149+
}
150+
let config = &self.config;
151+
152+
// Calculate inner and outer half size and ensure that the edge_radius is <= any half_length
153+
let outer_half_size = self.size.abs() / 2.0;
154+
let inner_half_size =
155+
(outer_half_size - Vec3::splat(config.corner_radius.abs())).max(Vec3::ZERO);
156+
let mut edge_radius = (outer_half_size - inner_half_size).min_element();
157+
let inner_half_size = outer_half_size - Vec3::splat(edge_radius);
158+
edge_radius *= config.corner_radius.signum();
159+
160+
// Handle cases where the rounded cuboid collapses into simpler shapes
161+
if edge_radius == 0.0 {
162+
let transform = Transform::from_translation(config.position)
163+
.with_rotation(config.rotation)
164+
.with_scale(self.size);
165+
self.gizmos.cuboid(transform, config.color);
166+
return;
167+
}
168+
169+
let rects = [
170+
(
171+
Vec3::X,
172+
Vec2::new(self.size.z, self.size.y),
173+
Quat::from_rotation_y(FRAC_PI_2),
174+
),
175+
(
176+
Vec3::Y,
177+
Vec2::new(self.size.x, self.size.z),
178+
Quat::from_rotation_x(FRAC_PI_2),
179+
),
180+
(Vec3::Z, Vec2::new(self.size.x, self.size.y), Quat::IDENTITY),
181+
];
182+
183+
for (position, size, rotation) in rects {
184+
let world_rotation = config.rotation * rotation;
185+
let local_position = config.rotation * (position * inner_half_size);
186+
self.gizmos
187+
.rounded_rect(
188+
config.position + local_position,
189+
world_rotation,
190+
size,
191+
config.color,
192+
)
193+
.arc_segments(config.arc_segments)
194+
.corner_radius(edge_radius);
195+
196+
self.gizmos
197+
.rounded_rect(
198+
config.position - local_position,
199+
world_rotation,
200+
size,
201+
config.color,
202+
)
203+
.arc_segments(config.arc_segments)
204+
.corner_radius(edge_radius);
205+
}
206+
}
207+
}
208+
209+
impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
210+
/// Draw a wireframe rectangle with rounded corners in 3D.
211+
///
212+
/// This should be called for each frame the rectangle needs to be rendered.
213+
///
214+
/// # Arguments
215+
///
216+
/// - `position`: The center point of the rectangle.
217+
/// - `rotation`: defines orientation of the rectangle, by default we assume the rectangle is contained in a plane parallel to the XY plane.
218+
/// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box.
219+
/// - `color`: color of the rectangle
220+
///
221+
/// # Builder methods
222+
///
223+
/// - The corner radius can be adjusted with the `.corner_radius(...)` method.
224+
/// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the
225+
/// `.arc_segments(...)` method.
226+
///
227+
/// # Example
228+
/// ```
229+
/// # use bevy_gizmos::prelude::*;
230+
/// # use bevy_render::prelude::*;
231+
/// # use bevy_math::prelude::*;
232+
/// # use bevy_color::palettes::css::GREEN;
233+
/// fn system(mut gizmos: Gizmos) {
234+
/// gizmos.rounded_rect(
235+
/// Vec3::ZERO,
236+
/// Quat::IDENTITY,
237+
/// Vec2::ONE,
238+
/// GREEN
239+
/// )
240+
/// .corner_radius(0.25)
241+
/// .arc_segments(10);
242+
/// }
243+
/// # bevy_ecs::system::assert_is_system(system);
244+
/// ```
245+
pub fn rounded_rect(
246+
&mut self,
247+
position: Vec3,
248+
rotation: Quat,
249+
size: Vec2,
250+
color: impl Into<Color>,
251+
) -> RoundedRectBuilder<'_, 'w, 's, T> {
252+
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
253+
RoundedRectBuilder {
254+
gizmos: self,
255+
config: RoundedBoxConfig {
256+
position,
257+
rotation,
258+
color: color.into(),
259+
corner_radius,
260+
arc_segments: DEFAULT_ARC_SEGMENTS,
261+
},
262+
size,
263+
}
264+
}
265+
266+
/// Draw a wireframe rectangle with rounded corners in 2D.
267+
///
268+
/// This should be called for each frame the rectangle needs to be rendered.
269+
///
270+
/// # Arguments
271+
///
272+
/// - `position`: The center point of the rectangle.
273+
/// - `rotation`: defines orientation of the rectangle.
274+
/// - `size`: defines the size of the rectangle. This refers to the 'outer size', similar to a bounding box.
275+
/// - `color`: color of the rectangle
276+
///
277+
/// # Builder methods
278+
///
279+
/// - The corner radius can be adjusted with the `.corner_radius(...)` method.
280+
/// - The number of segments of the arcs at each corner (i.e. the level of detail) can be adjusted with the
281+
/// `.arc_segments(...)` method.
282+
///
283+
/// # Example
284+
/// ```
285+
/// # use bevy_gizmos::prelude::*;
286+
/// # use bevy_render::prelude::*;
287+
/// # use bevy_math::prelude::*;
288+
/// # use bevy_color::palettes::css::GREEN;
289+
/// fn system(mut gizmos: Gizmos) {
290+
/// gizmos.rounded_rect_2d(
291+
/// Vec2::ZERO,
292+
/// 0.,
293+
/// Vec2::ONE,
294+
/// GREEN
295+
/// )
296+
/// .corner_radius(0.25)
297+
/// .arc_segments(10);
298+
/// }
299+
/// # bevy_ecs::system::assert_is_system(system);
300+
/// ```
301+
pub fn rounded_rect_2d(
302+
&mut self,
303+
position: Vec2,
304+
rotation: f32,
305+
size: Vec2,
306+
color: impl Into<Color>,
307+
) -> RoundedRectBuilder<'_, 'w, 's, T> {
308+
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
309+
RoundedRectBuilder {
310+
gizmos: self,
311+
config: RoundedBoxConfig {
312+
position: position.extend(0.),
313+
rotation: Quat::from_rotation_z(rotation),
314+
color: color.into(),
315+
corner_radius,
316+
arc_segments: DEFAULT_ARC_SEGMENTS,
317+
},
318+
size,
319+
}
320+
}
321+
322+
/// Draw a wireframe cuboid with rounded corners in 3D.
323+
///
324+
/// This should be called for each frame the cuboid needs to be rendered.
325+
///
326+
/// # Arguments
327+
///
328+
/// - `position`: The center point of the cuboid.
329+
/// - `rotation`: defines orientation of the cuboid.
330+
/// - `size`: defines the size of the cuboid. This refers to the 'outer size', similar to a bounding box.
331+
/// - `color`: color of the cuboid
332+
///
333+
/// # Builder methods
334+
///
335+
/// - The edge radius can be adjusted with the `.edge_radius(...)` method.
336+
/// - The number of segments of the arcs at each edge (i.e. the level of detail) can be adjusted with the
337+
/// `.arc_segments(...)` method.
338+
///
339+
/// # Example
340+
/// ```
341+
/// # use bevy_gizmos::prelude::*;
342+
/// # use bevy_render::prelude::*;
343+
/// # use bevy_math::prelude::*;
344+
/// # use bevy_color::palettes::css::GREEN;
345+
/// fn system(mut gizmos: Gizmos) {
346+
/// gizmos.rounded_cuboid(
347+
/// Vec3::ZERO,
348+
/// Quat::IDENTITY,
349+
/// Vec3::ONE,
350+
/// GREEN
351+
/// )
352+
/// .edge_radius(0.25)
353+
/// .arc_segments(10);
354+
/// }
355+
/// # bevy_ecs::system::assert_is_system(system);
356+
/// ```
357+
pub fn rounded_cuboid(
358+
&mut self,
359+
position: Vec3,
360+
rotation: Quat,
361+
size: Vec3,
362+
color: impl Into<Color>,
363+
) -> RoundedCuboidBuilder<'_, 'w, 's, T> {
364+
let corner_radius = size.min_element() * DEFAULT_CORNER_RADIUS;
365+
RoundedCuboidBuilder {
366+
gizmos: self,
367+
config: RoundedBoxConfig {
368+
position,
369+
rotation,
370+
color: color.into(),
371+
corner_radius,
372+
arc_segments: DEFAULT_ARC_SEGMENTS,
373+
},
374+
size,
375+
}
376+
}
377+
}
378+
379+
const DEFAULT_ARC_SEGMENTS: usize = 8;
380+
const DEFAULT_CORNER_RADIUS: f32 = 0.1;

0 commit comments

Comments
 (0)