bevy/crates/bevy_gizmos/src/primitives/dim3.rs
Robert Walter 041731b7e0
Drawing Primitives with Gizmos (#11072)
The PR is in a reviewable state now in the sense that the basic
implementations are there. There are still some ToDos that I'm aware of:

- [x] docs for all the new structs and traits
- [x] implement `Default` and derive other useful traits for the new
structs
- [x] Take a look at the notes again (Do this after a first round of
reviews)
- [x] Take care of the repetition in the circle drawing functions

---

# Objective

- TLDR: This PR enables us to quickly draw all the newly added
primitives from `bevy_math` in immediate mode with gizmos
- Addresses #10571

## Solution

- This implements the first design idea I had that covered everything
that was mentioned in the Issue
https://github.com/bevyengine/bevy/issues/10571#issuecomment-1863646197

--- 

## Caveats

- I added the `Primitive(2/3)d` impls for `Direction(2/3)d` to make them
work with the current solution. We could impose less strict requirements
for the gizmoable objects and remove the impls afterwards if the
community doesn't like the current approach.

---

## Changelog

- implement capabilities to draw ellipses on the gizmo in general (this
was required to have some code which is able to draw the ellipse
primitive)
- refactored circle drawing code to use the more general ellipse drawing
code to keep code duplication low
- implement `Primitive2d` for `Direction2d` and impl `Primitive3d` for
`Direction3d`
- implement trait to draw primitives with specialized details with
gizmos
  - `GizmoPrimitive2d` for all the 2D primitives
  - `GizmoPrimitive3d` for all the 3D primitives
- (question while writing this: Does it actually matter if we split this
in 2D and 3D? I guess it could be useful in the future if we do
something based on the main rendering mode even though atm it's kinda
useless)

---

---------

Co-authored-by: nothendev <borodinov.ilya@gmail.com>
2024-02-02 21:13:03 +00:00

920 lines
26 KiB
Rust

//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`].
use super::helpers::*;
use std::f32::consts::TAU;
use bevy_math::primitives::{
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Torus,
};
use bevy_math::{Quat, Vec3};
use bevy_render::color::Color;
use crate::prelude::{GizmoConfigGroup, Gizmos};
const DEFAULT_NUMBER_SEGMENTS: usize = 5;
// length used to simulate infinite lines
const INFINITE_LEN: f32 = 10_000.0;
/// A trait for rendering 3D geometric primitives (`P`) with [`Gizmos`].
pub trait GizmoPrimitive3d<P: Primitive3d> {
/// The output of `primitive_3d`. This is a builder to set non-default values.
type Output<'a>
where
Self: 'a;
/// Renders a 3D primitive with its associated details.
fn primitive_3d(
&mut self,
primitive: P,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_>;
}
// direction 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Direction3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Direction3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
self.arrow(position, position + (rotation * *primitive), color);
}
}
// sphere
/// Builder for configuring the drawing options of [`Sphere`].
pub struct SphereBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the sphere
radius: f32,
// Rotation of the sphere around the origin in 3D space
rotation: Quat,
// Center position of the sphere in 3D space
position: Vec3,
// Color of the sphere
color: Color,
// Number of segments used to approximate the sphere geometry
segments: usize,
}
impl<T: GizmoConfigGroup> SphereBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the sphere geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Sphere> for Gizmos<'w, 's, T> {
type Output<'a> = SphereBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Sphere,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
SphereBuilder {
gizmos: self,
radius: primitive.radius,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for SphereBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let SphereBuilder {
radius,
position: center,
rotation,
color,
segments,
..
} = self;
// draws the upper and lower semi spheres
[-1.0, 1.0].into_iter().for_each(|sign| {
let top = *center + (*rotation * Vec3::Y) * sign * *radius;
draw_semi_sphere(
self.gizmos,
*radius,
*segments,
*rotation,
*center,
top,
*color,
);
});
// draws one great circle of the sphere
draw_circle_3d(self.gizmos, *radius, *segments, *rotation, *center, *color);
}
}
// plane 3d
/// Builder for configuring the drawing options of [`Sphere`].
pub struct Plane3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// direction of the normal orthogonal to the plane
normal: Direction3d,
// Rotation of the sphere around the origin in 3D space
rotation: Quat,
// Center position of the sphere in 3D space
position: Vec3,
// Color of the sphere
color: Color,
// Number of axis to hint the plane
axis_count: usize,
// Number of segments used to hint the plane
segment_count: usize,
// Length of segments used to hint the plane
segment_length: f32,
}
impl<T: GizmoConfigGroup> Plane3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to hint the plane.
pub fn segment_count(mut self, count: usize) -> Self {
self.segment_count = count;
self
}
/// Set the length of segments used to hint the plane.
pub fn segment_length(mut self, length: f32) -> Self {
self.segment_length = length;
self
}
/// Set the number of axis used to hint the plane.
pub fn axis_count(mut self, count: usize) -> Self {
self.axis_count = count;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Plane3d> for Gizmos<'w, 's, T> {
type Output<'a> = Plane3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Plane3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Plane3dBuilder {
gizmos: self,
normal: primitive.normal,
rotation,
position,
color,
axis_count: 4,
segment_count: 3,
segment_length: 0.25,
}
}
}
impl<T: GizmoConfigGroup> Drop for Plane3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
// draws the normal
let normal = self.rotation * *self.normal;
self.gizmos
.primitive_3d(self.normal, self.position, self.rotation, self.color);
let normals_normal = normal.any_orthonormal_vector();
// draws the axes
// get rotation for each direction
(0..self.axis_count)
.map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU)
.map(|angle| Quat::from_axis_angle(normal, angle))
.for_each(|quat| {
let axis_direction = quat * normals_normal;
let direction = Direction3d::new_unchecked(axis_direction);
// for each axis draw dotted line
(0..)
.filter(|i| i % 2 != 0)
.map(|percent| (percent as f32 + 0.5) * self.segment_length * axis_direction)
.map(|position| position + self.position)
.take(self.segment_count)
.for_each(|position| {
self.gizmos.primitive_3d(
Segment3d {
direction,
half_length: self.segment_length * 0.5,
},
position,
Quat::IDENTITY,
self.color,
);
});
});
}
}
// line 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Line3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Line3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let direction = rotation * *primitive.direction;
self.arrow(position, position + direction, color);
let [start, end] = [1.0, -1.0]
.map(|sign| sign * INFINITE_LEN)
.map(|length| direction * length)
.map(|offset| position + offset);
self.line(start, end, color);
}
}
// segment 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Segment3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Segment3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let direction = rotation * *primitive.direction;
let start = position - direction * primitive.half_length;
let end = position + direction * primitive.half_length;
self.line(start, end, color);
}
}
// polyline 3d
impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive3d<Polyline3d<N>>
for Gizmos<'w, 's, T>
{
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Polyline3d<N>,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip(
primitive
.vertices
.map(rotate_then_translate_3d(rotation, position)),
color,
);
}
}
// boxed polyline 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<BoxedPolyline3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: BoxedPolyline3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip(
primitive
.vertices
.iter()
.copied()
.map(rotate_then_translate_3d(rotation, position)),
color,
);
}
}
// cuboid
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cuboid> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cuboid,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let [half_extend_x, half_extend_y, half_extend_z] = primitive.half_size.to_array();
// transform the points from the reference unit cube to the cuboid coords
let vertices @ [a, b, c, d, e, f, g, h] = [
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
[-1.0, -1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, -1.0],
[1.0, -1.0, -1.0],
]
.map(|[sx, sy, sz]| Vec3::new(sx * half_extend_x, sy * half_extend_y, sz * half_extend_z))
.map(rotate_then_translate_3d(rotation, position));
// lines for the upper rectangle of the cuboid
let upper = [a, b, c, d]
.into_iter()
.zip([a, b, c, d].into_iter().cycle().skip(1));
// lines for the lower rectangle of the cuboid
let lower = [e, f, g, h]
.into_iter()
.zip([e, f, g, h].into_iter().cycle().skip(1));
// lines connecting upper and lower rectangles of the cuboid
let connections = vertices.into_iter().zip(vertices.into_iter().skip(4));
upper
.chain(lower)
.chain(connections)
.for_each(|(start, end)| {
self.line(start, end, color);
});
}
}
// cylinder 3d
/// Builder for configuring the drawing options of [`Cylinder`].
pub struct Cylinder3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the cylinder
radius: f32,
// Half height of the cylinder
half_height: f32,
// Center position of the cylinder
position: Vec3,
// Rotation of the cylinder
//
// default orientation is: the cylinder is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the cylinder
color: Color,
// Number of segments used to approximate the cylinder geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Cylinder3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the cylinder geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cylinder> for Gizmos<'w, 's, T> {
type Output<'a> = Cylinder3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cylinder,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Cylinder3dBuilder {
gizmos: self,
radius: primitive.radius,
half_height: primitive.half_height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Cylinder3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Cylinder3dBuilder {
gizmos,
radius,
half_height,
position,
rotation,
color,
segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw upper and lower circle of the cylinder
[-1.0, 1.0].into_iter().for_each(|sign| {
draw_circle_3d(
gizmos,
*radius,
*segments,
*rotation,
*position + sign * *half_height * normal,
*color,
);
});
// draw lines connecting the two cylinder circles
draw_cylinder_vertical_lines(
gizmos,
*radius,
*segments,
*half_height,
*rotation,
*position,
*color,
);
}
}
// capsule 3d
/// Builder for configuring the drawing options of [`Capsule3d`].
pub struct Capsule3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the capsule
radius: f32,
// Half length of the capsule
half_length: f32,
// Center position of the capsule
position: Vec3,
// Rotation of the capsule
//
// default orientation is: the capsule is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the capsule
color: Color,
// Number of segments used to approximate the capsule geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Capsule3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the capsule geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Capsule3d> for Gizmos<'w, 's, T> {
type Output<'a> = Capsule3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Capsule3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Capsule3dBuilder {
gizmos: self,
radius: primitive.radius,
half_length: primitive.half_length,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Capsule3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Capsule3dBuilder {
gizmos,
radius,
half_length,
position,
rotation,
color,
segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw two semi spheres for the capsule
[1.0, -1.0].into_iter().for_each(|sign| {
let center = *position + sign * *half_length * normal;
let top = center + sign * *radius * normal;
draw_semi_sphere(gizmos, *radius, *segments, *rotation, center, top, *color);
draw_circle_3d(gizmos, *radius, *segments, *rotation, center, *color);
});
// connect the two semi spheres with lines
draw_cylinder_vertical_lines(
gizmos,
*radius,
*segments,
*half_length,
*rotation,
*position,
*color,
);
}
}
// cone 3d
/// Builder for configuring the drawing options of [`Cone`].
pub struct Cone3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the cone
radius: f32,
// Height of the cone
height: f32,
// Center of the cone, half-way between the tip and the base
position: Vec3,
// Rotation of the cone
//
// default orientation is: cone base normal is aligned with the `Vec3::Y` axis
rotation: Quat,
// Color of the cone
color: Color,
// Number of segments used to approximate the cone geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Cone3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the cone geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cone> for Gizmos<'w, 's, T> {
type Output<'a> = Cone3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cone,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Cone3dBuilder {
gizmos: self,
radius: primitive.radius,
height: primitive.height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Cone3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Cone3dBuilder {
gizmos,
radius,
height,
position,
rotation,
color,
segments,
} = self;
let half_height = *height * 0.5;
// draw the base circle of the cone
draw_circle_3d(
gizmos,
*radius,
*segments,
*rotation,
*position - *rotation * Vec3::Y * half_height,
*color,
);
// connect the base circle with the tip of the cone
let end = Vec3::Y * half_height;
circle_coordinates(*radius, *segments)
.map(|p| Vec3::new(p.x, -half_height, p.y))
.map(move |p| [p, end])
.map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position)))
.for_each(|[start, end]| {
gizmos.line(start, end, *color);
});
}
}
// conical frustum 3d
/// Builder for configuring the drawing options of [`ConicalFrustum`].
pub struct ConicalFrustum3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the top circle
radius_top: f32,
// Radius of the bottom circle
radius_bottom: f32,
// Height of the conical frustum
height: f32,
// Center of conical frustum, half-way between the top and the bottom
position: Vec3,
// Rotation of the conical frustrum
//
// default orientation is: conical frustrum base shape normals are aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the conical frustum
color: Color,
// Number of segments used to approximate the curved surfaces
segments: usize,
}
impl<T: GizmoConfigGroup> ConicalFrustum3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the curved surfaces.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<ConicalFrustum> for Gizmos<'w, 's, T> {
type Output<'a> = ConicalFrustum3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: ConicalFrustum,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
ConicalFrustum3dBuilder {
gizmos: self,
radius_top: primitive.radius_top,
radius_bottom: primitive.radius_bottom,
height: primitive.height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for ConicalFrustum3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let ConicalFrustum3dBuilder {
gizmos,
radius_top,
radius_bottom,
height,
position,
rotation,
color,
segments,
} = self;
let half_height = *height * 0.5;
let normal = *rotation * Vec3::Y;
// draw the two circles of the conical frustrum
[(*radius_top, half_height), (*radius_bottom, -half_height)]
.into_iter()
.for_each(|(radius, height)| {
draw_circle_3d(
gizmos,
radius,
*segments,
*rotation,
*position + height * normal,
*color,
);
});
// connect the two circles of the conical frustrum
circle_coordinates(*radius_top, *segments)
.map(move |p| Vec3::new(p.x, half_height, p.y))
.zip(
circle_coordinates(*radius_bottom, *segments)
.map(|p| Vec3::new(p.x, -half_height, p.y)),
)
.map(|(start, end)| [start, end])
.map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position)))
.for_each(|[start, end]| {
gizmos.line(start, end, *color);
});
}
}
// torus 3d
/// Builder for configuring the drawing options of [`Torus`].
pub struct Torus3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the minor circle (tube)
minor_radius: f32,
// Radius of the major circle (ring)
major_radius: f32,
// Center of the torus
position: Vec3,
// Rotation of the conical frustrum
//
// default orientation is: major circle normal is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the torus
color: Color,
// Number of segments in the minor (tube) direction
minor_segments: usize,
// Number of segments in the major (ring) direction
major_segments: usize,
}
impl<T: GizmoConfigGroup> Torus3dBuilder<'_, '_, '_, T> {
/// Set the number of segments in the minor (tube) direction.
pub fn minor_segments(mut self, minor_segments: usize) -> Self {
self.minor_segments = minor_segments;
self
}
/// Set the number of segments in the major (ring) direction.
pub fn major_segments(mut self, major_segments: usize) -> Self {
self.major_segments = major_segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Torus> for Gizmos<'w, 's, T> {
type Output<'a> = Torus3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Torus,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Torus3dBuilder {
gizmos: self,
minor_radius: primitive.minor_radius,
major_radius: primitive.major_radius,
position,
rotation,
color,
minor_segments: DEFAULT_NUMBER_SEGMENTS,
major_segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Torus3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Torus3dBuilder {
gizmos,
minor_radius,
major_radius,
position,
rotation,
color,
minor_segments,
major_segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw 4 circles with major_radius
[
(*major_radius - *minor_radius, 0.0),
(*major_radius + *minor_radius, 0.0),
(*major_radius, *minor_radius),
(*major_radius, -*minor_radius),
]
.into_iter()
.for_each(|(radius, height)| {
draw_circle_3d(
gizmos,
radius,
*major_segments,
*rotation,
*position + height * normal,
*color,
);
});
// along the major circle draw orthogonal minor circles
let affine = rotate_then_translate_3d(*rotation, *position);
circle_coordinates(*major_radius, *major_segments)
.map(|p| Vec3::new(p.x, 0.0, p.y))
.flat_map(|major_circle_point| {
let minor_center = affine(major_circle_point);
// direction facing from the center of the torus towards the minor circles center
let dir_to_translation = (minor_center - *position).normalize();
// the minor circle is draw with 4 arcs this is done to make the minor circle
// connect properly with each of the major circles
let circle_points = [dir_to_translation, normal, -dir_to_translation, -normal]
.map(|offset| minor_center + offset.normalize() * *minor_radius);
circle_points
.into_iter()
.zip(circle_points.into_iter().cycle().skip(1))
.map(move |(from, to)| (minor_center, from, to))
.collect::<Vec<_>>()
})
.for_each(|(center, from, to)| {
gizmos
.short_arc_3d_between(center, from, to, *color)
.segments(*minor_segments);
});
}
}