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