Skip to main content

render_primitives/
render_primitives.rs

1//! This example demonstrates how each of Bevy's math primitives look like in 2D and 3D with meshes
2//! and with gizmos
3
4use bevy::{input::common_conditions::input_just_pressed, math::Isometry2d, prelude::*};
5
6const LEFT_RIGHT_OFFSET_2D: f32 = 200.0;
7const LEFT_RIGHT_OFFSET_3D: f32 = 2.0;
8
9fn main() {
10    let mut app = App::new();
11
12    app.add_plugins(DefaultPlugins)
13        .init_state::<PrimitiveSelected>()
14        .init_state::<CameraActive>();
15
16    // cameras
17    app.add_systems(Startup, (setup_cameras, setup_lights, setup_ambient_light))
18        .add_systems(
19            Update,
20            (
21                update_active_cameras.run_if(state_changed::<CameraActive>),
22                switch_cameras.run_if(input_just_pressed(KeyCode::KeyC)),
23            ),
24        );
25
26    // text
27
28    // PostStartup since we need the cameras to exist
29    app.add_systems(PostStartup, setup_text);
30    app.add_systems(
31        Update,
32        (update_text.run_if(state_changed::<PrimitiveSelected>),),
33    );
34
35    // primitives
36    app.add_systems(Startup, (spawn_primitive_2d, spawn_primitive_3d))
37        .add_systems(
38            Update,
39            (
40                switch_to_next_primitive.run_if(input_just_pressed(KeyCode::ArrowUp)),
41                switch_to_previous_primitive.run_if(input_just_pressed(KeyCode::ArrowDown)),
42                draw_gizmos_2d.run_if(in_mode(CameraActive::Dim2)),
43                draw_gizmos_3d.run_if(in_mode(CameraActive::Dim3)),
44                update_primitive_meshes.run_if(
45                    state_changed::<PrimitiveSelected>.or_eager(state_changed::<CameraActive>),
46                ),
47                rotate_primitive_2d_meshes,
48                rotate_primitive_3d_meshes,
49            ),
50        );
51
52    app.run();
53}
54
55/// State for tracking which of the two cameras (2D & 3D) is currently active
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
57enum CameraActive {
58    #[default]
59    /// 2D Camera is active
60    Dim2,
61    /// 3D Camera is active
62    Dim3,
63}
64
65/// State for tracking which primitives are currently displayed
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
67enum PrimitiveSelected {
68    #[default]
69    RectangleAndCuboid,
70    CircleAndSphere,
71    Ellipse,
72    Triangle,
73    Plane,
74    Line,
75    Segment,
76    Polyline,
77    Polygon,
78    ConvexPolygon,
79    RegularPolygon,
80    Capsule,
81    Cylinder,
82    Cone,
83    ConicalFrustum,
84    Torus,
85    Tetrahedron,
86    Arc,
87    CircularSector,
88    CircularSegment,
89}
90
91impl std::fmt::Display for PrimitiveSelected {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        let name = match self {
94            PrimitiveSelected::RectangleAndCuboid => String::from("Rectangle/Cuboid"),
95            PrimitiveSelected::CircleAndSphere => String::from("Circle/Sphere"),
96            other => format!("{other:?}"),
97        };
98        write!(f, "{name}")
99    }
100}
101
102impl PrimitiveSelected {
103    const ALL: [Self; 20] = [
104        Self::RectangleAndCuboid,
105        Self::CircleAndSphere,
106        Self::Ellipse,
107        Self::Triangle,
108        Self::Plane,
109        Self::Line,
110        Self::Segment,
111        Self::Polyline,
112        Self::Polygon,
113        Self::ConvexPolygon,
114        Self::RegularPolygon,
115        Self::Capsule,
116        Self::Cylinder,
117        Self::Cone,
118        Self::ConicalFrustum,
119        Self::Torus,
120        Self::Tetrahedron,
121        Self::Arc,
122        Self::CircularSector,
123        Self::CircularSegment,
124    ];
125
126    fn next(self) -> Self {
127        Self::ALL
128            .into_iter()
129            .cycle()
130            .skip_while(|&x| x != self)
131            .nth(1)
132            .unwrap()
133    }
134
135    fn previous(self) -> Self {
136        Self::ALL
137            .into_iter()
138            .rev()
139            .cycle()
140            .skip_while(|&x| x != self)
141            .nth(1)
142            .unwrap()
143    }
144}
145
146const SMALL_2D: f32 = 50.0;
147const BIG_2D: f32 = 100.0;
148
149const SMALL_3D: f32 = 0.5;
150const BIG_3D: f32 = 1.0;
151
152// primitives
153const RECTANGLE: Rectangle = Rectangle {
154    half_size: Vec2::new(SMALL_2D, BIG_2D),
155};
156const CUBOID: Cuboid = Cuboid {
157    half_size: Vec3::new(BIG_3D, SMALL_3D, BIG_3D),
158};
159
160const CIRCLE: Circle = Circle { radius: BIG_2D };
161const SPHERE: Sphere = Sphere { radius: BIG_3D };
162
163const ELLIPSE: Ellipse = Ellipse {
164    half_size: Vec2::new(BIG_2D, SMALL_2D),
165};
166
167const TRIANGLE_2D: Triangle2d = Triangle2d {
168    vertices: [
169        Vec2::new(BIG_2D, 0.0),
170        Vec2::new(0.0, BIG_2D),
171        Vec2::new(-BIG_2D, 0.0),
172    ],
173};
174
175const TRIANGLE_3D: Triangle3d = Triangle3d {
176    vertices: [
177        Vec3::new(BIG_3D, 0.0, 0.0),
178        Vec3::new(0.0, BIG_3D, 0.0),
179        Vec3::new(-BIG_3D, 0.0, 0.0),
180    ],
181};
182
183const PLANE_2D: Plane2d = Plane2d { normal: Dir2::Y };
184const PLANE_3D: Plane3d = Plane3d {
185    normal: Dir3::Y,
186    half_size: Vec2::new(BIG_3D, BIG_3D),
187};
188
189const LINE_2D: Line2d = Line2d { direction: Dir2::X };
190const LINE_3D: Line3d = Line3d { direction: Dir3::X };
191
192const SEGMENT_2D: Segment2d = Segment2d {
193    vertices: [Vec2::new(-BIG_2D / 2., 0.), Vec2::new(BIG_2D / 2., 0.)],
194};
195
196const SEGMENT_3D: Segment3d = Segment3d {
197    vertices: [
198        Vec3::new(-BIG_3D / 2., 0., 0.),
199        Vec3::new(BIG_3D / 2., 0., 0.),
200    ],
201};
202
203const POLYLINE_2D_VERTICES: [Vec2; 4] = [
204    Vec2::new(-BIG_2D, -SMALL_2D),
205    Vec2::new(-SMALL_2D, SMALL_2D),
206    Vec2::new(SMALL_2D, -SMALL_2D),
207    Vec2::new(BIG_2D, SMALL_2D),
208];
209
210const POLYLINE_3D_VERTICES: [Vec3; 4] = [
211    Vec3::new(-BIG_3D, -SMALL_3D, -SMALL_3D),
212    Vec3::new(SMALL_3D, SMALL_3D, 0.0),
213    Vec3::new(-SMALL_3D, -SMALL_3D, 0.0),
214    Vec3::new(BIG_3D, SMALL_3D, SMALL_3D),
215];
216
217const CONVEX_POLYGON_VERTICES: [Vec2; 5] = [
218    Vec2::new(-BIG_2D, -SMALL_2D),
219    Vec2::new(BIG_2D, -SMALL_2D),
220    Vec2::new(BIG_2D, SMALL_2D),
221    Vec2::new(BIG_2D / 2.0, SMALL_2D * 2.0),
222    Vec2::new(-BIG_2D, SMALL_2D),
223];
224
225const REGULAR_POLYGON: RegularPolygon = RegularPolygon {
226    circumcircle: Circle { radius: BIG_2D },
227    sides: 5,
228};
229
230const CAPSULE_2D: Capsule2d = Capsule2d {
231    radius: SMALL_2D,
232    half_length: SMALL_2D,
233};
234
235const CAPSULE_3D: Capsule3d = Capsule3d {
236    radius: SMALL_3D,
237    half_length: SMALL_3D,
238};
239
240const CYLINDER: Cylinder = Cylinder {
241    radius: SMALL_3D,
242    half_height: SMALL_3D,
243};
244
245const CONE: Cone = Cone {
246    radius: BIG_3D,
247    height: BIG_3D,
248};
249
250const CONICAL_FRUSTUM: ConicalFrustum = ConicalFrustum {
251    radius_top: BIG_3D,
252    radius_bottom: SMALL_3D,
253    height: BIG_3D,
254};
255
256const ANNULUS: Annulus = Annulus {
257    inner_circle: Circle { radius: SMALL_2D },
258    outer_circle: Circle { radius: BIG_2D },
259};
260
261const TORUS: Torus = Torus {
262    minor_radius: SMALL_3D / 2.0,
263    major_radius: SMALL_3D * 1.5,
264};
265
266const TETRAHEDRON: Tetrahedron = Tetrahedron {
267    vertices: [
268        Vec3::new(-BIG_3D, 0.0, 0.0),
269        Vec3::new(BIG_3D, 0.0, 0.0),
270        Vec3::new(0.0, 0.0, -BIG_3D * 1.67),
271        Vec3::new(0.0, BIG_3D * 1.67, -BIG_3D * 0.5),
272    ],
273};
274
275const ARC: Arc2d = Arc2d {
276    radius: BIG_2D,
277    half_angle: std::f32::consts::FRAC_PI_4,
278};
279
280const CIRCULAR_SECTOR: CircularSector = CircularSector {
281    arc: Arc2d {
282        radius: BIG_2D,
283        half_angle: std::f32::consts::FRAC_PI_4,
284    },
285};
286
287const CIRCULAR_SEGMENT: CircularSegment = CircularSegment {
288    arc: Arc2d {
289        radius: BIG_2D,
290        half_angle: std::f32::consts::FRAC_PI_4,
291    },
292};
293
294fn setup_cameras(mut commands: Commands) {
295    let start_in_2d = true;
296    let make_camera = |is_active| Camera {
297        is_active,
298        ..Default::default()
299    };
300
301    commands.spawn((Camera2d, make_camera(start_in_2d)));
302
303    commands.spawn((
304        Camera3d::default(),
305        make_camera(!start_in_2d),
306        Transform::from_xyz(0.0, 10.0, 0.0).looking_at(Vec3::ZERO, Vec3::Z),
307    ));
308}
309
310fn setup_ambient_light(mut ambient_light: ResMut<GlobalAmbientLight>) {
311    ambient_light.brightness = 50.0;
312}
313
314fn setup_lights(mut commands: Commands) {
315    commands.spawn((
316        PointLight {
317            intensity: 5000.0,
318            ..default()
319        },
320        Transform::from_translation(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 2.0, 0.0))
321            .looking_at(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0), Vec3::Y),
322    ));
323}
324
325/// Marker component for header text
326#[derive(Debug, Clone, Component, Default, Reflect)]
327pub struct HeaderText;
328
329/// Marker component for header node
330#[derive(Debug, Clone, Component, Default, Reflect)]
331pub struct HeaderNode;
332
333fn update_active_cameras(
334    state: Res<State<CameraActive>>,
335    camera_2d: Single<(Entity, &mut Camera), With<Camera2d>>,
336    camera_3d: Single<(Entity, &mut Camera), (With<Camera3d>, Without<Camera2d>)>,
337    mut text: Query<&mut UiTargetCamera, With<HeaderNode>>,
338) {
339    let (entity_2d, mut cam_2d) = camera_2d.into_inner();
340    let (entity_3d, mut cam_3d) = camera_3d.into_inner();
341    let is_camera_2d_active = matches!(*state.get(), CameraActive::Dim2);
342
343    cam_2d.is_active = is_camera_2d_active;
344    cam_3d.is_active = !is_camera_2d_active;
345
346    let active_camera = if is_camera_2d_active {
347        entity_2d
348    } else {
349        entity_3d
350    };
351
352    text.iter_mut().for_each(|mut target_camera| {
353        *target_camera = UiTargetCamera(active_camera);
354    });
355}
356
357fn switch_cameras(current: Res<State<CameraActive>>, mut next: ResMut<NextState<CameraActive>>) {
358    let next_state = match current.get() {
359        CameraActive::Dim2 => CameraActive::Dim3,
360        CameraActive::Dim3 => CameraActive::Dim2,
361    };
362    next.set(next_state);
363}
364
365fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) {
366    let active_camera = cameras
367        .iter()
368        .find_map(|(entity, camera)| camera.is_active.then_some(entity))
369        .expect("run condition ensures existence");
370    commands.spawn((
371        HeaderNode,
372        Node {
373            justify_self: JustifySelf::Center,
374            top: px(5),
375            ..Default::default()
376        },
377        UiTargetCamera(active_camera),
378        children![(
379            Text::default(),
380            HeaderText,
381            TextLayout::justify(Justify::Center),
382            children![
383                TextSpan::new("Primitive: "),
384                TextSpan(format!("{text}", text = PrimitiveSelected::default())),
385                TextSpan::new("\n\n"),
386                TextSpan::new(
387                    "Press 'C' to switch between 2D and 3D mode\n\
388                    Press 'Up' or 'Down' to switch to the next/previous primitive",
389                ),
390                TextSpan::new("\n\n"),
391                TextSpan::new("(If nothing is displayed, there's no rendering support yet)",),
392            ]
393        )],
394    ));
395}
396
397fn update_text(
398    primitive_state: Res<State<PrimitiveSelected>>,
399    header: Query<Entity, With<HeaderText>>,
400    mut writer: TextUiWriter,
401) {
402    let new_text = format!("{text}", text = primitive_state.get());
403    header.iter().for_each(|header_text| {
404        if let Some(mut text) = writer.get_text(header_text, 2) {
405            (*text).clone_from(&new_text);
406        };
407    });
408}
409
410fn switch_to_next_primitive(
411    current: Res<State<PrimitiveSelected>>,
412    mut next: ResMut<NextState<PrimitiveSelected>>,
413) {
414    let next_state = current.get().next();
415    next.set(next_state);
416}
417
418fn switch_to_previous_primitive(
419    current: Res<State<PrimitiveSelected>>,
420    mut next: ResMut<NextState<PrimitiveSelected>>,
421) {
422    let next_state = current.get().previous();
423    next.set(next_state);
424}
425
426fn in_mode(active: CameraActive) -> impl Fn(Res<State<CameraActive>>) -> bool {
427    move |state| *state.get() == active
428}
429
430fn draw_gizmos_2d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
431    const POSITION: Vec2 = Vec2::new(-LEFT_RIGHT_OFFSET_2D, 0.0);
432    let angle = time.elapsed_secs();
433    let isometry = Isometry2d::new(POSITION, Rot2::radians(angle));
434    let color = Color::WHITE;
435
436    #[expect(
437        clippy::match_same_arms,
438        reason = "Certain primitives don't have any 2D rendering support yet."
439    )]
440    match state.get() {
441        PrimitiveSelected::RectangleAndCuboid => {
442            gizmos.primitive_2d(&RECTANGLE, isometry, color);
443        }
444        PrimitiveSelected::CircleAndSphere => {
445            gizmos.primitive_2d(&CIRCLE, isometry, color);
446        }
447        PrimitiveSelected::Ellipse => drop(gizmos.primitive_2d(&ELLIPSE, isometry, color)),
448        PrimitiveSelected::Triangle => gizmos.primitive_2d(&TRIANGLE_2D, isometry, color),
449        PrimitiveSelected::Plane => gizmos.primitive_2d(&PLANE_2D, isometry, color),
450        PrimitiveSelected::Line => drop(gizmos.primitive_2d(&LINE_2D, isometry, color)),
451        PrimitiveSelected::Segment => {
452            drop(gizmos.primitive_2d(&SEGMENT_2D, isometry, color));
453        }
454        PrimitiveSelected::Polyline => gizmos.primitive_2d(
455            &Polyline2d {
456                vertices: POLYLINE_2D_VERTICES.to_vec(),
457            },
458            isometry,
459            color,
460        ),
461        PrimitiveSelected::ConvexPolygon => gizmos.primitive_2d(
462            &Polygon::from(ConvexPolygon::new(CONVEX_POLYGON_VERTICES).unwrap()),
463            isometry,
464            color,
465        ),
466        PrimitiveSelected::Polygon => gizmos.primitive_2d(
467            &Polygon {
468                vertices: vec![
469                    Vec2::new(-BIG_2D, -SMALL_2D),
470                    Vec2::new(BIG_2D, -SMALL_2D),
471                    Vec2::new(BIG_2D, SMALL_2D),
472                    Vec2::new(0.0, 0.0),
473                    Vec2::new(-BIG_2D, SMALL_2D),
474                ],
475            },
476            isometry,
477            color,
478        ),
479        PrimitiveSelected::RegularPolygon => {
480            gizmos.primitive_2d(&REGULAR_POLYGON, isometry, color);
481        }
482        PrimitiveSelected::Capsule => gizmos.primitive_2d(&CAPSULE_2D, isometry, color),
483        PrimitiveSelected::Cylinder => {}
484        PrimitiveSelected::Cone => {}
485        PrimitiveSelected::ConicalFrustum => {}
486        PrimitiveSelected::Torus => drop(gizmos.primitive_2d(&ANNULUS, isometry, color)),
487        PrimitiveSelected::Tetrahedron => {}
488        PrimitiveSelected::Arc => gizmos.primitive_2d(&ARC, isometry, color),
489        PrimitiveSelected::CircularSector => {
490            gizmos.primitive_2d(&CIRCULAR_SECTOR, isometry, color);
491        }
492        PrimitiveSelected::CircularSegment => {
493            gizmos.primitive_2d(&CIRCULAR_SEGMENT, isometry, color);
494        }
495    }
496}
497
498/// Marker for primitive meshes to record in which state they should be visible in
499#[derive(Debug, Clone, Component, Default, Reflect)]
500pub struct PrimitiveData {
501    camera_mode: CameraActive,
502    primitive_state: PrimitiveSelected,
503}
504
505/// Marker for meshes of 2D primitives
506#[derive(Debug, Clone, Component, Default)]
507pub struct MeshDim2;
508
509/// Marker for meshes of 3D primitives
510#[derive(Debug, Clone, Component, Default)]
511pub struct MeshDim3;
512
513fn spawn_primitive_2d(
514    mut commands: Commands,
515    mut materials: ResMut<Assets<ColorMaterial>>,
516    mut meshes: ResMut<Assets<Mesh>>,
517) {
518    const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_2D, 0.0, 0.0);
519    let material: Handle<ColorMaterial> = materials.add(Color::WHITE);
520    let camera_mode = CameraActive::Dim2;
521    let polyline_2d = Polyline2d {
522        vertices: POLYLINE_2D_VERTICES.to_vec(),
523    };
524    let convex_polygon = ConvexPolygon::new(CONVEX_POLYGON_VERTICES).unwrap();
525    [
526        Some(RECTANGLE.mesh().build()),
527        Some(CIRCLE.mesh().build()),
528        Some(ELLIPSE.mesh().build()),
529        Some(TRIANGLE_2D.mesh().build()),
530        None, // plane
531        None, // line
532        Some(SEGMENT_2D.mesh().build()),
533        Some(polyline_2d.mesh().build()),
534        None, // polygon
535        Some(convex_polygon.mesh().build()),
536        Some(REGULAR_POLYGON.mesh().build()),
537        Some(CAPSULE_2D.mesh().build()),
538        None, // cylinder
539        None, // cone
540        None, // conical frustum
541        Some(ANNULUS.mesh().build()),
542        None, // tetrahedron
543        None, // arc
544        Some(CIRCULAR_SECTOR.mesh().build()),
545        Some(CIRCULAR_SEGMENT.mesh().build()),
546    ]
547    .into_iter()
548    .zip(PrimitiveSelected::ALL)
549    .for_each(|(maybe_mesh, state)| {
550        if let Some(mesh) = maybe_mesh {
551            commands.spawn((
552                MeshDim2,
553                PrimitiveData {
554                    camera_mode,
555                    primitive_state: state,
556                },
557                Mesh2d(meshes.add(mesh)),
558                MeshMaterial2d(material.clone()),
559                Transform::from_translation(POSITION),
560            ));
561        }
562    });
563}
564
565fn spawn_primitive_3d(
566    mut commands: Commands,
567    mut materials: ResMut<Assets<StandardMaterial>>,
568    mut meshes: ResMut<Assets<Mesh>>,
569) {
570    const POSITION: Vec3 = Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
571    let material: Handle<StandardMaterial> = materials.add(Color::WHITE);
572    let camera_mode = CameraActive::Dim3;
573    let polyline_3d = Polyline3d {
574        vertices: POLYLINE_3D_VERTICES.to_vec(),
575    };
576    [
577        Some(CUBOID.mesh().build()),
578        Some(SPHERE.mesh().build()),
579        None, // ellipse
580        Some(TRIANGLE_3D.mesh().build()),
581        Some(PLANE_3D.mesh().build()),
582        None, // line
583        Some(SEGMENT_3D.mesh().build()),
584        Some(polyline_3d.mesh().build()),
585        None, // polygon
586        None, // convex polygon
587        None, // regular polygon
588        Some(CAPSULE_3D.mesh().build()),
589        Some(CYLINDER.mesh().build()),
590        Some(CONE.mesh().build()),
591        Some(CONICAL_FRUSTUM.mesh().build()),
592        Some(TORUS.mesh().build()),
593        Some(TETRAHEDRON.mesh().build()),
594        None, // arc
595        None, // circular sector
596        None, // circular segment
597    ]
598    .into_iter()
599    .zip(PrimitiveSelected::ALL)
600    .for_each(|(maybe_mesh, state)| {
601        if let Some(mesh) = maybe_mesh {
602            commands.spawn((
603                MeshDim3,
604                PrimitiveData {
605                    camera_mode,
606                    primitive_state: state,
607                },
608                Mesh3d(meshes.add(mesh)),
609                MeshMaterial3d(material.clone()),
610                Transform::from_translation(POSITION),
611            ));
612        }
613    });
614}
615
616fn update_primitive_meshes(
617    camera_state: Res<State<CameraActive>>,
618    primitive_state: Res<State<PrimitiveSelected>>,
619    mut primitives: Query<(&mut Visibility, &PrimitiveData)>,
620) {
621    primitives.iter_mut().for_each(|(mut vis, primitive)| {
622        let visible = primitive.camera_mode == *camera_state.get()
623            && primitive.primitive_state == *primitive_state.get();
624        *vis = if visible {
625            Visibility::Inherited
626        } else {
627            Visibility::Hidden
628        };
629    });
630}
631
632fn rotate_primitive_2d_meshes(
633    mut primitives_2d: Query<
634        (&mut Transform, &ViewVisibility),
635        (With<PrimitiveData>, With<MeshDim2>),
636    >,
637    time: Res<Time>,
638) {
639    let rotation_2d = Quat::from_mat3(&Mat3::from_angle(time.elapsed_secs()));
640    primitives_2d
641        .iter_mut()
642        .filter(|(_, vis)| vis.get())
643        .for_each(|(mut transform, _)| {
644            transform.rotation = rotation_2d;
645        });
646}
647
648fn rotate_primitive_3d_meshes(
649    mut primitives_3d: Query<
650        (&mut Transform, &ViewVisibility),
651        (With<PrimitiveData>, With<MeshDim3>),
652    >,
653    time: Res<Time>,
654) {
655    let rotation_3d = Quat::from_rotation_arc(
656        Vec3::Z,
657        Vec3::new(
658            ops::sin(time.elapsed_secs()),
659            ops::cos(time.elapsed_secs()),
660            ops::sin(time.elapsed_secs()) * 0.5,
661        )
662        .try_normalize()
663        .unwrap_or(Vec3::Z),
664    );
665    primitives_3d
666        .iter_mut()
667        .filter(|(_, vis)| vis.get())
668        .for_each(|(mut transform, _)| {
669            transform.rotation = rotation_3d;
670        });
671}
672
673fn draw_gizmos_3d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
674    const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
675    let rotation = Quat::from_rotation_arc(
676        Vec3::Z,
677        Vec3::new(
678            ops::sin(time.elapsed_secs()),
679            ops::cos(time.elapsed_secs()),
680            ops::sin(time.elapsed_secs()) * 0.5,
681        )
682        .try_normalize()
683        .unwrap_or(Vec3::Z),
684    );
685    let isometry = Isometry3d::new(POSITION, rotation);
686    let color = Color::WHITE;
687    let resolution = 10;
688
689    #[expect(
690        clippy::match_same_arms,
691        reason = "Certain primitives don't have any 3D rendering support yet."
692    )]
693    match state.get() {
694        PrimitiveSelected::RectangleAndCuboid => {
695            gizmos.primitive_3d(&CUBOID, isometry, color);
696        }
697        PrimitiveSelected::CircleAndSphere => drop(
698            gizmos
699                .primitive_3d(&SPHERE, isometry, color)
700                .resolution(resolution),
701        ),
702        PrimitiveSelected::Ellipse => {}
703        PrimitiveSelected::Triangle => gizmos.primitive_3d(&TRIANGLE_3D, isometry, color),
704        PrimitiveSelected::Plane => drop(gizmos.primitive_3d(&PLANE_3D, isometry, color)),
705        PrimitiveSelected::Line => gizmos.primitive_3d(&LINE_3D, isometry, color),
706        PrimitiveSelected::Segment => gizmos.primitive_3d(&SEGMENT_3D, isometry, color),
707        PrimitiveSelected::Polyline => gizmos.primitive_3d(
708            &Polyline3d {
709                vertices: POLYLINE_3D_VERTICES.to_vec(),
710            },
711            isometry,
712            color,
713        ),
714        PrimitiveSelected::Polygon => {}
715        PrimitiveSelected::ConvexPolygon => {}
716        PrimitiveSelected::RegularPolygon => {}
717        PrimitiveSelected::Capsule => drop(
718            gizmos
719                .primitive_3d(&CAPSULE_3D, isometry, color)
720                .resolution(resolution),
721        ),
722        PrimitiveSelected::Cylinder => drop(
723            gizmos
724                .primitive_3d(&CYLINDER, isometry, color)
725                .resolution(resolution),
726        ),
727        PrimitiveSelected::Cone => drop(
728            gizmos
729                .primitive_3d(&CONE, isometry, color)
730                .resolution(resolution),
731        ),
732        PrimitiveSelected::ConicalFrustum => {
733            gizmos.primitive_3d(&CONICAL_FRUSTUM, isometry, color);
734        }
735
736        PrimitiveSelected::Torus => drop(
737            gizmos
738                .primitive_3d(&TORUS, isometry, color)
739                .minor_resolution(resolution)
740                .major_resolution(resolution),
741        ),
742        PrimitiveSelected::Tetrahedron => {
743            gizmos.primitive_3d(&TETRAHEDRON, isometry, color);
744        }
745
746        PrimitiveSelected::Arc => {}
747        PrimitiveSelected::CircularSector => {}
748        PrimitiveSelected::CircularSegment => {}
749    }
750}