Skip to content

Commit bcae8e9

Browse files
authored
Implement Arc3D for Gizmos (#11540)
# Objective - Implement an arc3d API for gizmos - Solves #11536 ## Solution ### `arc_3d` - The current `arc3d` method on gizmos only takes an angle - It draws an "standard arc" by default, this is an arc starting at `Vec3::X`, in the XZ plane, in counter clockwise direction with a normal that is facing up - The "standard arc" can be customized with the usual gizmo builder pattern. This way you'll be able to draw arbitrary arcs ### `short/long_arc_3d_between` - Given `center`, `from`, `to` draws an arc between `from` and `to` --- ## Changelog > This section is optional. If this was a trivial fix, or has no externally-visible impact, you can delete this section. - Added: `Gizmos::arc3d(&mut self, angle)` method - Added: `Gizmos::long_arc_3d_between(&mut self, center, from, to)` method - Added: `Gizmos::short_arc_3d_between(&mut self, center, from, to)` method --- This PR factors out an orthogonal part of another PR as mentioned in [this comment](#11072 (comment))
1 parent 9fcf862 commit bcae8e9

File tree

2 files changed

+301
-12
lines changed

2 files changed

+301
-12
lines changed

crates/bevy_gizmos/src/arcs.rs

Lines changed: 291 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
66
use crate::circles::DEFAULT_CIRCLE_SEGMENTS;
77
use crate::prelude::{GizmoConfigGroup, Gizmos};
8-
use bevy_math::Vec2;
8+
use bevy_math::{Quat, Vec2, Vec3};
99
use bevy_render::color::Color;
1010
use std::f32::consts::TAU;
1111

12+
// === 2D ===
13+
1214
impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
1315
/// Draw an arc, which is a part of the circumference of a circle, in 2D.
1416
///
@@ -73,7 +75,7 @@ pub struct Arc2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
7375
impl<T: GizmoConfigGroup> Arc2dBuilder<'_, '_, '_, T> {
7476
/// Set the number of line-segments for this arc.
7577
pub fn segments(mut self, segments: usize) -> Self {
76-
self.segments = Some(segments);
78+
self.segments.replace(segments);
7779
self
7880
}
7981
}
@@ -83,20 +85,18 @@ impl<T: GizmoConfigGroup> Drop for Arc2dBuilder<'_, '_, '_, T> {
8385
if !self.gizmos.enabled {
8486
return;
8587
}
86-
let segments = match self.segments {
87-
Some(segments) => segments,
88-
// Do a linear interpolation between 1 and `DEFAULT_CIRCLE_SEGMENTS`
89-
// using the arc angle as scalar.
90-
None => ((self.arc_angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize,
91-
};
92-
93-
let positions = arc_inner(self.direction_angle, self.arc_angle, self.radius, segments)
94-
.map(|vec2| vec2 + self.position);
88+
89+
let segments = self
90+
.segments
91+
.unwrap_or_else(|| segments_from_angle(self.arc_angle));
92+
93+
let positions = arc_2d_inner(self.direction_angle, self.arc_angle, self.radius, segments)
94+
.map(|vec2| (vec2 + self.position));
9595
self.gizmos.linestrip_2d(positions, self.color);
9696
}
9797
}
9898

99-
fn arc_inner(
99+
fn arc_2d_inner(
100100
direction_angle: f32,
101101
arc_angle: f32,
102102
radius: f32,
@@ -109,3 +109,282 @@ fn arc_inner(
109109
Vec2::from(angle.sin_cos()) * radius
110110
})
111111
}
112+
113+
// === 3D ===
114+
115+
impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
116+
/// Draw an arc, which is a part of the circumference of a circle, in 3D. For default values
117+
/// this is drawing a standard arc. A standard arc is defined as
118+
///
119+
/// - an arc with a center at `Vec3::ZERO`
120+
/// - starting at `Vec3::X`
121+
/// - embedded in the XZ plane
122+
/// - rotates counterclockwise
123+
///
124+
/// This should be called for each frame the arc needs to be rendered.
125+
///
126+
/// # Arguments
127+
/// - `angle`: sets how much of a circle circumference is passed, e.g. PI is half a circle. This
128+
/// value should be in the range (-2 * PI..=2 * PI)
129+
/// - `radius`: distance between the arc and it's center point
130+
/// - `position`: position of the arcs center point
131+
/// - `rotation`: defines orientation of the arc, by default we assume the arc is contained in a
132+
/// plane parallel to the XZ plane and the default starting point is (`position + Vec3::X`)
133+
/// - `color`: color of the arc
134+
///
135+
/// # Builder methods
136+
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
137+
/// `.segements(...)` method.
138+
///
139+
/// # Example
140+
/// ```
141+
/// # use bevy_gizmos::prelude::*;
142+
/// # use bevy_render::prelude::*;
143+
/// # use bevy_math::prelude::*;
144+
/// # use std::f32::consts::PI;
145+
/// fn system(mut gizmos: Gizmos) {
146+
/// // rotation rotates normal to point in the direction of `Vec3::NEG_ONE`
147+
/// let rotation = Quat::from_rotation_arc(Vec3::Y, Vec3::NEG_ONE.normalize());
148+
///
149+
/// gizmos
150+
/// .arc_3d(
151+
/// 270.0_f32.to_radians(),
152+
/// 0.25,
153+
/// Vec3::ONE,
154+
/// rotation,
155+
/// Color::ORANGE
156+
/// )
157+
/// .segments(100);
158+
/// }
159+
/// # bevy_ecs::system::assert_is_system(system);
160+
/// ```
161+
#[inline]
162+
pub fn arc_3d(
163+
&mut self,
164+
angle: f32,
165+
radius: f32,
166+
position: Vec3,
167+
rotation: Quat,
168+
color: Color,
169+
) -> Arc3dBuilder<'_, 'w, 's, T> {
170+
Arc3dBuilder {
171+
gizmos: self,
172+
start_vertex: Vec3::X,
173+
center: position,
174+
rotation,
175+
angle,
176+
radius,
177+
color,
178+
segments: None,
179+
}
180+
}
181+
182+
/// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point.
183+
///
184+
/// # Arguments
185+
///
186+
/// - `center`: The center point around which the arc is drawn.
187+
/// - `from`: The starting point of the arc.
188+
/// - `to`: The ending point of the arc.
189+
/// - `color`: color of the arc
190+
///
191+
/// # Builder methods
192+
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
193+
/// `.segements(...)` method.
194+
///
195+
/// # Examples
196+
/// ```
197+
/// # use bevy_gizmos::prelude::*;
198+
/// # use bevy_render::prelude::*;
199+
/// # use bevy_math::prelude::*;
200+
/// fn system(mut gizmos: Gizmos) {
201+
/// gizmos.short_arc_3d_between(
202+
/// Vec3::ONE,
203+
/// Vec3::ONE + Vec3::NEG_ONE,
204+
/// Vec3::ZERO,
205+
/// Color::ORANGE
206+
/// )
207+
/// .segments(100);
208+
/// }
209+
/// # bevy_ecs::system::assert_is_system(system);
210+
/// ```
211+
///
212+
/// # Notes
213+
/// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
214+
/// the points is coincident with `center`, nothing is rendered.
215+
/// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
216+
/// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
217+
/// the results will behave as if this were the case
218+
#[inline]
219+
pub fn short_arc_3d_between(
220+
&mut self,
221+
center: Vec3,
222+
from: Vec3,
223+
to: Vec3,
224+
color: Color,
225+
) -> Arc3dBuilder<'_, 'w, 's, T> {
226+
self.arc_from_to(center, from, to, color, |x| x)
227+
}
228+
229+
/// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point.
230+
///
231+
/// # Arguments
232+
/// - `center`: The center point around which the arc is drawn.
233+
/// - `from`: The starting point of the arc.
234+
/// - `to`: The ending point of the arc.
235+
/// - `color`: color of the arc
236+
///
237+
/// # Builder methods
238+
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
239+
/// `.segements(...)` method.
240+
///
241+
/// # Examples
242+
/// ```
243+
/// # use bevy_gizmos::prelude::*;
244+
/// # use bevy_render::prelude::*;
245+
/// # use bevy_math::prelude::*;
246+
/// fn system(mut gizmos: Gizmos) {
247+
/// gizmos.long_arc_3d_between(
248+
/// Vec3::ONE,
249+
/// Vec3::ONE + Vec3::NEG_ONE,
250+
/// Vec3::ZERO,
251+
/// Color::ORANGE
252+
/// )
253+
/// .segments(100);
254+
/// }
255+
/// # bevy_ecs::system::assert_is_system(system);
256+
/// ```
257+
///
258+
/// # Notes
259+
/// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
260+
/// the points is coincident with `center`, nothing is rendered.
261+
/// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
262+
/// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
263+
/// the results will behave as if this were the case.
264+
#[inline]
265+
pub fn long_arc_3d_between(
266+
&mut self,
267+
center: Vec3,
268+
from: Vec3,
269+
to: Vec3,
270+
color: Color,
271+
) -> Arc3dBuilder<'_, 'w, 's, T> {
272+
self.arc_from_to(center, from, to, color, |angle| {
273+
if angle > 0.0 {
274+
TAU - angle
275+
} else if angle < 0.0 {
276+
-TAU - angle
277+
} else {
278+
0.0
279+
}
280+
})
281+
}
282+
283+
#[inline]
284+
fn arc_from_to(
285+
&mut self,
286+
center: Vec3,
287+
from: Vec3,
288+
to: Vec3,
289+
color: Color,
290+
angle_fn: impl Fn(f32) -> f32,
291+
) -> Arc3dBuilder<'_, 'w, 's, T> {
292+
// `from` and `to` can be the same here since in either case nothing gets rendered and the
293+
// orientation ambiguity of `up` doesn't matter
294+
let from_axis = (from - center).normalize_or_zero();
295+
let to_axis = (to - center).normalize_or_zero();
296+
let (up, angle) = Quat::from_rotation_arc(from_axis, to_axis).to_axis_angle();
297+
298+
let angle = angle_fn(angle);
299+
let radius = center.distance(from);
300+
let rotation = Quat::from_rotation_arc(Vec3::Y, up);
301+
302+
let start_vertex = rotation.inverse() * from_axis;
303+
304+
Arc3dBuilder {
305+
gizmos: self,
306+
start_vertex,
307+
center,
308+
rotation,
309+
angle,
310+
radius,
311+
color,
312+
segments: None,
313+
}
314+
}
315+
}
316+
317+
/// A builder returned by [`Gizmos::arc_2d`].
318+
pub struct Arc3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
319+
gizmos: &'a mut Gizmos<'w, 's, T>,
320+
// this is the vertex the arc starts on in the XZ plane. For the normal arc_3d method this is
321+
// always starting at Vec3::X. For the short/long arc methods we actually need a way to start
322+
// at the from position and this is where this internal field comes into play. Some implicit
323+
// assumptions:
324+
//
325+
// 1. This is always in the XZ plane
326+
// 2. This is always normalized
327+
//
328+
// DO NOT expose this field to users as it is easy to mess this up
329+
start_vertex: Vec3,
330+
center: Vec3,
331+
rotation: Quat,
332+
angle: f32,
333+
radius: f32,
334+
color: Color,
335+
segments: Option<usize>,
336+
}
337+
338+
impl<T: GizmoConfigGroup> Arc3dBuilder<'_, '_, '_, T> {
339+
/// Set the number of line-segments for this arc.
340+
pub fn segments(mut self, segments: usize) -> Self {
341+
self.segments.replace(segments);
342+
self
343+
}
344+
}
345+
346+
impl<T: GizmoConfigGroup> Drop for Arc3dBuilder<'_, '_, '_, T> {
347+
fn drop(&mut self) {
348+
if !self.gizmos.enabled {
349+
return;
350+
}
351+
352+
let segments = self
353+
.segments
354+
.unwrap_or_else(|| segments_from_angle(self.angle));
355+
356+
let positions = arc_3d_inner(
357+
self.start_vertex,
358+
self.center,
359+
self.rotation,
360+
self.angle,
361+
self.radius,
362+
segments,
363+
);
364+
self.gizmos.linestrip(positions, self.color);
365+
}
366+
}
367+
368+
fn arc_3d_inner(
369+
start_vertex: Vec3,
370+
center: Vec3,
371+
rotation: Quat,
372+
angle: f32,
373+
radius: f32,
374+
segments: usize,
375+
) -> impl Iterator<Item = Vec3> {
376+
// drawing arcs bigger than TAU degrees or smaller than -TAU degrees makes no sense since
377+
// we won't see the overlap and we would just decrease the level of details since the segments
378+
// would be larger
379+
let angle = angle.clamp(-TAU, TAU);
380+
(0..=segments)
381+
.map(move |frac| frac as f32 / segments as f32)
382+
.map(move |percentage| angle * percentage)
383+
.map(move |frac_angle| Quat::from_axis_angle(Vec3::Y, frac_angle) * start_vertex)
384+
.map(move |p| rotation * (p * radius) + center)
385+
}
386+
387+
// helper function for getting a default value for the segments parameter
388+
fn segments_from_angle(angle: f32) -> usize {
389+
((angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize
390+
}

examples/3d/3d_gizmos.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
9696
);
9797
}
9898

99+
my_gizmos
100+
.arc_3d(
101+
180.0_f32.to_radians(),
102+
0.2,
103+
Vec3::ONE,
104+
Quat::from_rotation_arc(Vec3::Y, Vec3::ONE.normalize()),
105+
Color::ORANGE,
106+
)
107+
.segments(10);
108+
99109
// Circles have 32 line-segments by default.
100110
my_gizmos.circle(Vec3::ZERO, Direction3d::Y, 3., Color::BLACK);
101111
// You may want to increase this for larger circles or spheres.

0 commit comments

Comments
 (0)