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