bevy_gizmos/arcs.rs
1//! Additional [`GizmoBuffer`] Functions -- Arcs
2//!
3//! Includes the implementation of [`GizmoBuffer::arc_2d`],
4//! and assorted support items.
5
6use crate::{circles::DEFAULT_CIRCLE_RESOLUTION, gizmos::GizmoBuffer, prelude::GizmoConfigGroup};
7use bevy_color::Color;
8use bevy_math::{Isometry2d, Isometry3d, Quat, Rot2, Vec2, Vec3};
9use core::f32::consts::{FRAC_PI_2, TAU};
10
11// === 2D ===
12
13impl<Config, Clear> GizmoBuffer<Config, Clear>
14where
15 Config: GizmoConfigGroup,
16 Clear: 'static + Send + Sync,
17{
18 /// Draw an arc, which is a part of the circumference of a circle, in 2D.
19 ///
20 /// # Arguments
21 /// - `isometry` defines the translation and rotation of the arc.
22 /// - the translation specifies the center of the arc
23 /// - the rotation is counter-clockwise starting from `Vec2::Y`
24 /// - `arc_angle` sets the length of this arc, in radians.
25 /// - `radius` controls the distance from `position` to this arc, and thus its curvature.
26 /// - `color` sets the color to draw the arc.
27 ///
28 /// # Example
29 /// ```
30 /// # use bevy_gizmos::prelude::*;
31 /// # use bevy_math::prelude::*;
32 /// # use std::f32::consts::FRAC_PI_4;
33 /// # use bevy_color::palettes::basic::{GREEN, RED};
34 /// fn system(mut gizmos: Gizmos) {
35 /// gizmos.arc_2d(Isometry2d::IDENTITY, FRAC_PI_4, 1., GREEN);
36 ///
37 /// // Arcs have 32 line-segments by default.
38 /// // You may want to increase this for larger arcs.
39 /// gizmos
40 /// .arc_2d(Isometry2d::IDENTITY, FRAC_PI_4, 5., RED)
41 /// .resolution(64);
42 /// }
43 /// # bevy_ecs::system::assert_is_system(system);
44 /// ```
45 #[inline]
46 pub fn arc_2d(
47 &mut self,
48 isometry: impl Into<Isometry2d>,
49 arc_angle: f32,
50 radius: f32,
51 color: impl Into<Color>,
52 ) -> Arc2dBuilder<'_, Config, Clear> {
53 Arc2dBuilder {
54 gizmos: self,
55 isometry: isometry.into(),
56 arc_angle,
57 radius,
58 color: color.into(),
59 resolution: None,
60 }
61 }
62}
63
64/// A builder returned by [`GizmoBuffer::arc_2d`].
65pub struct Arc2dBuilder<'a, Config, Clear>
66where
67 Config: GizmoConfigGroup,
68 Clear: 'static + Send + Sync,
69{
70 gizmos: &'a mut GizmoBuffer<Config, Clear>,
71 isometry: Isometry2d,
72 arc_angle: f32,
73 radius: f32,
74 color: Color,
75 resolution: Option<u32>,
76}
77
78impl<Config, Clear> Arc2dBuilder<'_, Config, Clear>
79where
80 Config: GizmoConfigGroup,
81 Clear: 'static + Send + Sync,
82{
83 /// Set the number of lines used to approximate the geometry of this arc.
84 pub fn resolution(mut self, resolution: u32) -> Self {
85 self.resolution.replace(resolution);
86 self
87 }
88}
89
90impl<Config, Clear> Drop for Arc2dBuilder<'_, Config, Clear>
91where
92 Config: GizmoConfigGroup,
93 Clear: 'static + Send + Sync,
94{
95 fn drop(&mut self) {
96 if !self.gizmos.enabled {
97 return;
98 }
99
100 let resolution = self
101 .resolution
102 .unwrap_or_else(|| resolution_from_angle(self.arc_angle));
103
104 let positions =
105 arc_2d_inner(self.arc_angle, self.radius, resolution).map(|vec2| self.isometry * vec2);
106 self.gizmos.linestrip_2d(positions, self.color);
107 }
108}
109
110fn arc_2d_inner(arc_angle: f32, radius: f32, resolution: u32) -> impl Iterator<Item = Vec2> {
111 (0..=resolution)
112 .map(move |n| arc_angle * n as f32 / resolution as f32)
113 .map(|angle| angle + FRAC_PI_2)
114 .map(Vec2::from_angle)
115 .map(move |vec2| vec2 * radius)
116}
117
118// === 3D ===
119
120impl<Config, Clear> GizmoBuffer<Config, Clear>
121where
122 Config: GizmoConfigGroup,
123 Clear: 'static + Send + Sync,
124{
125 /// Draw an arc, which is a part of the circumference of a circle, in 3D. For default values
126 /// this is drawing a standard arc. A standard arc is defined as
127 ///
128 /// - an arc with a center at `Vec3::ZERO`
129 /// - starting at `Vec3::X`
130 /// - embedded in the XZ plane
131 /// - rotates counterclockwise
132 ///
133 /// # Arguments
134 /// - `angle`: sets how much of a circle circumference is passed, e.g. PI is half a circle. This
135 /// value should be in the range (-2 * PI..=2 * PI)
136 /// - `radius`: distance between the arc and its center point
137 /// - `isometry` defines the translation and rotation of the arc.
138 /// - the translation specifies the center of the arc
139 /// - the rotation is counter-clockwise starting from `Vec3::Y`
140 /// - `color`: color of the arc
141 ///
142 /// # Builder methods
143 /// The resolution of the arc (i.e. the level of detail) can be adjusted with the
144 /// `.resolution(...)` method.
145 ///
146 /// # Example
147 /// ```
148 /// # use bevy_gizmos::prelude::*;
149 /// # use bevy_math::prelude::*;
150 /// # use std::f32::consts::PI;
151 /// # use bevy_color::palettes::css::ORANGE;
152 /// fn system(mut gizmos: Gizmos) {
153 /// // rotation rotates normal to point in the direction of `Vec3::NEG_ONE`
154 /// let rotation = Quat::from_rotation_arc(Vec3::Y, Vec3::NEG_ONE.normalize());
155 ///
156 /// gizmos
157 /// .arc_3d(
158 /// 270.0_f32.to_radians(),
159 /// 0.25,
160 /// Isometry3d::new(Vec3::ONE, rotation),
161 /// ORANGE
162 /// )
163 /// .resolution(100);
164 /// }
165 /// # bevy_ecs::system::assert_is_system(system);
166 /// ```
167 #[inline]
168 pub fn arc_3d(
169 &mut self,
170 angle: f32,
171 radius: f32,
172 isometry: impl Into<Isometry3d>,
173 color: impl Into<Color>,
174 ) -> Arc3dBuilder<'_, Config, Clear> {
175 Arc3dBuilder {
176 gizmos: self,
177 start_vertex: Vec3::X,
178 isometry: isometry.into(),
179 angle,
180 radius,
181 color: color.into(),
182 resolution: None,
183 }
184 }
185
186 /// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point.
187 ///
188 /// # Arguments
189 ///
190 /// - `center`: The center point around which the arc is drawn.
191 /// - `from`: The starting point of the arc.
192 /// - `to`: The ending point of the arc.
193 /// - `color`: color of the arc
194 ///
195 /// # Builder methods
196 /// The resolution of the arc (i.e. the level of detail) can be adjusted with the
197 /// `.resolution(...)` method.
198 ///
199 /// # Examples
200 /// ```
201 /// # use bevy_gizmos::prelude::*;
202 /// # use bevy_math::prelude::*;
203 /// # use bevy_color::palettes::css::ORANGE;
204 /// fn system(mut gizmos: Gizmos) {
205 /// gizmos.short_arc_3d_between(
206 /// Vec3::ONE,
207 /// Vec3::ONE + Vec3::NEG_ONE,
208 /// Vec3::ZERO,
209 /// ORANGE
210 /// )
211 /// .resolution(100);
212 /// }
213 /// # bevy_ecs::system::assert_is_system(system);
214 /// ```
215 ///
216 /// # Notes
217 /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
218 /// the points is coincident with `center`, nothing is rendered.
219 /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
220 /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
221 /// the results will behave as if this were the case
222 #[inline]
223 pub fn short_arc_3d_between(
224 &mut self,
225 center: Vec3,
226 from: Vec3,
227 to: Vec3,
228 color: impl Into<Color>,
229 ) -> Arc3dBuilder<'_, Config, Clear> {
230 self.arc_from_to(center, from, to, color, |x| x)
231 }
232
233 /// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point.
234 ///
235 /// # Arguments
236 /// - `center`: The center point around which the arc is drawn.
237 /// - `from`: The starting point of the arc.
238 /// - `to`: The ending point of the arc.
239 /// - `color`: color of the arc
240 ///
241 /// # Builder methods
242 /// The resolution of the arc (i.e. the level of detail) can be adjusted with the
243 /// `.resolution(...)` method.
244 ///
245 /// # Examples
246 /// ```
247 /// # use bevy_gizmos::prelude::*;
248 /// # use bevy_math::prelude::*;
249 /// # use bevy_color::palettes::css::ORANGE;
250 /// fn system(mut gizmos: Gizmos) {
251 /// gizmos.long_arc_3d_between(
252 /// Vec3::ONE,
253 /// Vec3::ONE + Vec3::NEG_ONE,
254 /// Vec3::ZERO,
255 /// ORANGE
256 /// )
257 /// .resolution(100);
258 /// }
259 /// # bevy_ecs::system::assert_is_system(system);
260 /// ```
261 ///
262 /// # Notes
263 /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
264 /// the points is coincident with `center`, nothing is rendered.
265 /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
266 /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
267 /// the results will behave as if this were the case.
268 #[inline]
269 pub fn long_arc_3d_between(
270 &mut self,
271 center: Vec3,
272 from: Vec3,
273 to: Vec3,
274 color: impl Into<Color>,
275 ) -> Arc3dBuilder<'_, Config, Clear> {
276 self.arc_from_to(center, from, to, color, |angle| {
277 if angle > 0.0 {
278 TAU - angle
279 } else if angle < 0.0 {
280 -TAU - angle
281 } else {
282 0.0
283 }
284 })
285 }
286
287 #[inline]
288 fn arc_from_to(
289 &mut self,
290 center: Vec3,
291 from: Vec3,
292 to: Vec3,
293 color: impl Into<Color>,
294 angle_fn: impl Fn(f32) -> f32,
295 ) -> Arc3dBuilder<'_, Config, Clear> {
296 // `from` and `to` can be the same here since in either case nothing gets rendered and the
297 // orientation ambiguity of `up` doesn't matter
298 let from_axis = (from - center).normalize_or_zero();
299 let to_axis = (to - center).normalize_or_zero();
300 let (up, angle) = Quat::from_rotation_arc(from_axis, to_axis).to_axis_angle();
301
302 let angle = angle_fn(angle);
303 let radius = center.distance(from);
304 let rotation = Quat::from_rotation_arc(Vec3::Y, up);
305
306 let start_vertex = rotation.inverse() * from_axis;
307
308 Arc3dBuilder {
309 gizmos: self,
310 start_vertex,
311 isometry: Isometry3d::new(center, rotation),
312 angle,
313 radius,
314 color: color.into(),
315 resolution: None,
316 }
317 }
318
319 /// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point.
320 ///
321 /// # Arguments
322 ///
323 /// - `center`: The center point around which the arc is drawn.
324 /// - `from`: The starting point of the arc.
325 /// - `to`: The ending point of the arc.
326 /// - `color`: color of the arc
327 ///
328 /// # Builder methods
329 /// The resolution of the arc (i.e. the level of detail) can be adjusted with the
330 /// `.resolution(...)` method.
331 ///
332 /// # Examples
333 /// ```
334 /// # use bevy_gizmos::prelude::*;
335 /// # use bevy_math::prelude::*;
336 /// # use bevy_color::palettes::css::ORANGE;
337 /// fn system(mut gizmos: Gizmos) {
338 /// gizmos.short_arc_2d_between(
339 /// Vec2::ZERO,
340 /// Vec2::X,
341 /// Vec2::Y,
342 /// ORANGE
343 /// )
344 /// .resolution(100);
345 /// }
346 /// # bevy_ecs::system::assert_is_system(system);
347 /// ```
348 ///
349 /// # Notes
350 /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
351 /// the points is coincident with `center`, nothing is rendered.
352 /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
353 /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
354 /// the results will behave as if this were the case
355 #[inline]
356 pub fn short_arc_2d_between(
357 &mut self,
358 center: Vec2,
359 from: Vec2,
360 to: Vec2,
361 color: impl Into<Color>,
362 ) -> Arc2dBuilder<'_, Config, Clear> {
363 self.arc_2d_from_to(center, from, to, color, core::convert::identity)
364 }
365
366 /// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point.
367 ///
368 /// # Arguments
369 /// - `center`: The center point around which the arc is drawn.
370 /// - `from`: The starting point of the arc.
371 /// - `to`: The ending point of the arc.
372 /// - `color`: color of the arc
373 ///
374 /// # Builder methods
375 /// The resolution of the arc (i.e. the level of detail) can be adjusted with the
376 /// `.resolution(...)` method.
377 ///
378 /// # Examples
379 /// ```
380 /// # use bevy_gizmos::prelude::*;
381 /// # use bevy_math::prelude::*;
382 /// # use bevy_color::palettes::css::ORANGE;
383 /// fn system(mut gizmos: Gizmos) {
384 /// gizmos.long_arc_2d_between(
385 /// Vec2::ZERO,
386 /// Vec2::X,
387 /// Vec2::Y,
388 /// ORANGE
389 /// )
390 /// .resolution(100);
391 /// }
392 /// # bevy_ecs::system::assert_is_system(system);
393 /// ```
394 ///
395 /// # Notes
396 /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of
397 /// the points is coincident with `center`, nothing is rendered.
398 /// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
399 /// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
400 /// the results will behave as if this were the case.
401 #[inline]
402 pub fn long_arc_2d_between(
403 &mut self,
404 center: Vec2,
405 from: Vec2,
406 to: Vec2,
407 color: impl Into<Color>,
408 ) -> Arc2dBuilder<'_, Config, Clear> {
409 self.arc_2d_from_to(center, from, to, color, |angle| angle - TAU)
410 }
411
412 #[inline]
413 fn arc_2d_from_to(
414 &mut self,
415 center: Vec2,
416 from: Vec2,
417 to: Vec2,
418 color: impl Into<Color>,
419 angle_fn: impl Fn(f32) -> f32,
420 ) -> Arc2dBuilder<'_, Config, Clear> {
421 // `from` and `to` can be the same here since in either case nothing gets rendered and the
422 // orientation ambiguity of `up` doesn't matter
423 let from_axis = (from - center).normalize_or_zero();
424 let to_axis = (to - center).normalize_or_zero();
425 let rotation = Vec2::Y.angle_to(from_axis);
426 let arc_angle_raw = from_axis.angle_to(to_axis);
427
428 let arc_angle = angle_fn(arc_angle_raw);
429 let radius = center.distance(from);
430
431 Arc2dBuilder {
432 gizmos: self,
433 isometry: Isometry2d::new(center, Rot2::radians(rotation)),
434 arc_angle,
435 radius,
436 color: color.into(),
437 resolution: None,
438 }
439 }
440}
441
442/// A builder returned by [`GizmoBuffer::arc_2d`].
443pub struct Arc3dBuilder<'a, Config, Clear>
444where
445 Config: GizmoConfigGroup,
446 Clear: 'static + Send + Sync,
447{
448 gizmos: &'a mut GizmoBuffer<Config, Clear>,
449 // this is the vertex the arc starts on in the XZ plane. For the normal arc_3d method this is
450 // always starting at Vec3::X. For the short/long arc methods we actually need a way to start
451 // at the from position and this is where this internal field comes into play. Some implicit
452 // assumptions:
453 //
454 // 1. This is always in the XZ plane
455 // 2. This is always normalized
456 //
457 // DO NOT expose this field to users as it is easy to mess this up
458 start_vertex: Vec3,
459 isometry: Isometry3d,
460 angle: f32,
461 radius: f32,
462 color: Color,
463 resolution: Option<u32>,
464}
465
466impl<Config, Clear> Arc3dBuilder<'_, Config, Clear>
467where
468 Config: GizmoConfigGroup,
469 Clear: 'static + Send + Sync,
470{
471 /// Set the number of lines for this arc.
472 pub fn resolution(mut self, resolution: u32) -> Self {
473 self.resolution.replace(resolution);
474 self
475 }
476}
477
478impl<Config, Clear> Drop for Arc3dBuilder<'_, Config, Clear>
479where
480 Config: GizmoConfigGroup,
481 Clear: 'static + Send + Sync,
482{
483 fn drop(&mut self) {
484 if !self.gizmos.enabled {
485 return;
486 }
487
488 let resolution = self
489 .resolution
490 .unwrap_or_else(|| resolution_from_angle(self.angle));
491
492 let positions = arc_3d_inner(
493 self.start_vertex,
494 self.isometry,
495 self.angle,
496 self.radius,
497 resolution,
498 );
499 self.gizmos.linestrip(positions, self.color);
500 }
501}
502
503fn arc_3d_inner(
504 start_vertex: Vec3,
505 isometry: Isometry3d,
506 angle: f32,
507 radius: f32,
508 resolution: u32,
509) -> impl Iterator<Item = Vec3> {
510 // drawing arcs bigger than TAU degrees or smaller than -TAU degrees makes no sense since
511 // we won't see the overlap and we would just decrease the level of details since the resolution
512 // would be larger
513 let angle = angle.clamp(-TAU, TAU);
514 (0..=resolution)
515 .map(move |frac| frac as f32 / resolution as f32)
516 .map(move |percentage| angle * percentage)
517 .map(move |frac_angle| Quat::from_axis_angle(Vec3::Y, frac_angle) * start_vertex)
518 .map(move |vec3| vec3 * radius)
519 .map(move |vec3| isometry * vec3)
520}
521
522// helper function for getting a default value for the resolution parameter
523fn resolution_from_angle(angle: f32) -> u32 {
524 ((angle.abs() / TAU) * DEFAULT_CIRCLE_RESOLUTION as f32).ceil() as u32
525}