docs/bevy/examples/math/render_primitives.rs 22.2 K · 750 lines · raw · history

1 //! This example demonstrates how each of Bevy's math primitives look like in 2D and 3D with meshes
2 //! and with gizmos
3
4 use bevy::{input::common_conditions::input_just_pressed, math::Isometry2d, prelude::*};
5
6 const LEFT_RIGHT_OFFSET_2D: f32 = 200.0;
7 const LEFT_RIGHT_OFFSET_3D: f32 = 2.0;
8
9 fn 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)]
57 enum 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)]
67 enum 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
91 impl 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
102 impl 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
146 const SMALL_2D: f32 = 50.0;
147 const BIG_2D: f32 = 100.0;
148
149 const SMALL_3D: f32 = 0.5;
150 const BIG_3D: f32 = 1.0;
151
152 // primitives
153 const RECTANGLE: Rectangle = Rectangle {
154 half_size: Vec2::new(SMALL_2D, BIG_2D),
155 };
156 const CUBOID: Cuboid = Cuboid {
157 half_size: Vec3::new(BIG_3D, SMALL_3D, BIG_3D),
158 };
159
160 const CIRCLE: Circle = Circle { radius: BIG_2D };
161 const SPHERE: Sphere = Sphere { radius: BIG_3D };
162
163 const ELLIPSE: Ellipse = Ellipse {
164 half_size: Vec2::new(BIG_2D, SMALL_2D),
165 };
166
167 const 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
175 const 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
183 const PLANE_2D: Plane2d = Plane2d { normal: Dir2::Y };
184 const PLANE_3D: Plane3d = Plane3d {
185 normal: Dir3::Y,
186 half_size: Vec2::new(BIG_3D, BIG_3D),
187 };
188
189 const LINE_2D: Line2d = Line2d { direction: Dir2::X };
190 const LINE_3D: Line3d = Line3d { direction: Dir3::X };
191
192 const SEGMENT_2D: Segment2d = Segment2d {
193 vertices: [Vec2::new(-BIG_2D / 2., 0.), Vec2::new(BIG_2D / 2., 0.)],
194 };
195
196 const 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
203 const 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
210 const 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
217 const 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
225 const REGULAR_POLYGON: RegularPolygon = RegularPolygon {
226 circumcircle: Circle { radius: BIG_2D },
227 sides: 5,
228 };
229
230 const CAPSULE_2D: Capsule2d = Capsule2d {
231 radius: SMALL_2D,
232 half_length: SMALL_2D,
233 };
234
235 const CAPSULE_3D: Capsule3d = Capsule3d {
236 radius: SMALL_3D,
237 half_length: SMALL_3D,
238 };
239
240 const CYLINDER: Cylinder = Cylinder {
241 radius: SMALL_3D,
242 half_height: SMALL_3D,
243 };
244
245 const CONE: Cone = Cone {
246 radius: BIG_3D,
247 height: BIG_3D,
248 };
249
250 const CONICAL_FRUSTUM: ConicalFrustum = ConicalFrustum {
251 radius_top: BIG_3D,
252 radius_bottom: SMALL_3D,
253 height: BIG_3D,
254 };
255
256 const ANNULUS: Annulus = Annulus {
257 inner_circle: Circle { radius: SMALL_2D },
258 outer_circle: Circle { radius: BIG_2D },
259 };
260
261 const TORUS: Torus = Torus {
262 minor_radius: SMALL_3D / 2.0,
263 major_radius: SMALL_3D * 1.5,
264 };
265
266 const 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
275 const ARC: Arc2d = Arc2d {
276 radius: BIG_2D,
277 half_angle: std::f32::consts::FRAC_PI_4,
278 };
279
280 const CIRCULAR_SECTOR: CircularSector = CircularSector {
281 arc: Arc2d {
282 radius: BIG_2D,
283 half_angle: std::f32::consts::FRAC_PI_4,
284 },
285 };
286
287 const CIRCULAR_SEGMENT: CircularSegment = CircularSegment {
288 arc: Arc2d {
289 radius: BIG_2D,
290 half_angle: std::f32::consts::FRAC_PI_4,
291 },
292 };
293
294 fn 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
310 fn setup_ambient_light(mut ambient_light: ResMut<GlobalAmbientLight>) {
311 ambient_light.brightness = 50.0;
312 }
313
314 fn 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)]
327 pub struct HeaderText;
328
329 /// Marker component for header node
330 #[derive(Debug, Clone, Component, Default, Reflect)]
331 pub struct HeaderNode;
332
333 fn 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
357 fn 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
365 fn 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
397 fn 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
410 fn 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
418 fn 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
426 fn in_mode(active: CameraActive) -> impl Fn(Res<State<CameraActive>>) -> bool {
427 move |state| *state.get() == active
428 }
429
430 fn 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)]
500 pub 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)]
507 pub struct MeshDim2;
508
509 /// Marker for meshes of 3D primitives
510 #[derive(Debug, Clone, Component, Default)]
511 pub struct MeshDim3;
512
513 fn 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
565 fn 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
616 fn 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
632 fn 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
648 fn 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
673 fn 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 }