1use std::f32::consts::{PI, SQRT_2};
5
6#[cfg(not(target_family = "wasm"))]
7use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
8
9use bevy::{
10 asset::RenderAssetUsages,
11 camera::ScalingMode,
12 color::palettes::css::{RED, WHITE},
13 input::common_conditions::{input_just_pressed, input_toggle_active},
14 math::{
15 bounding::{
16 Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume,
17 },
18 Isometry2d,
19 },
20 mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
21 prelude::*,
22};
23
24const HEART: Heart = Heart::new(0.5);
25const HOLLOW: Heart = Heart::new(0.3);
26const RING: Ring<Heart> = Ring::new(HEART, HOLLOW);
28const EXTRUSION: Extrusion<Heart> = Extrusion {
30 base_shape: HEART,
31 half_depth: 0.5,
32};
33const RING_EXTRUSION: Extrusion<Ring<Heart>> = Extrusion {
34 base_shape: RING,
35 half_depth: 0.5,
36};
37
38const TRANSFORM_2D: Transform = Transform {
40 translation: Vec3::ZERO,
41 rotation: Quat::IDENTITY,
42 scale: Vec3::ONE,
43};
44const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
46 near: -1.0,
47 far: 10.0,
48 scale: 1.0,
49 viewport_origin: Vec2::new(0.5, 0.5),
50 scaling_mode: ScalingMode::AutoMax {
51 max_width: 8.0,
52 max_height: 20.0,
53 },
54 area: Rect {
55 min: Vec2::NEG_ONE,
56 max: Vec2::ONE,
57 },
58});
59
60const TRANSFORM_3D: Transform = Transform {
62 translation: Vec3::ZERO,
63 rotation: Quat::from_xyzw(-0.2669336, -0.0, -0.0, 0.96371484),
65 scale: Vec3::ONE,
66};
67const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
69 fov: PI / 4.0,
70 near: 0.1,
71 far: 1000.0,
72 aspect_ratio: 1.0,
73 near_clip_plane: vec4(0.0, 0.0, -1.0, -0.1),
74});
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect, Component)]
80enum ShapeActive {
81 #[default]
82 Heart,
84 Ring,
86 Extrusion,
88 RingExtrusion,
90}
91
92impl ShapeActive {
93 const SHAPES: [ShapeActive; 4] = [
94 ShapeActive::Heart,
95 ShapeActive::Ring,
96 ShapeActive::Extrusion,
97 ShapeActive::RingExtrusion,
98 ];
99
100 fn next_shape(self) -> Self {
101 Self::SHAPES
102 .into_iter()
103 .cycle()
104 .skip_while(|shape| *shape != self)
105 .nth(1) .unwrap()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
112enum BoundingShape {
113 #[default]
114 None,
116 BoundingSphere,
118 BoundingBox,
120}
121
122#[derive(Component)]
124struct Shape2d;
125
126#[derive(Component)]
128struct Shape3d;
129
130fn main() {
131 let mut app = App::new();
132
133 app.add_plugins(DefaultPlugins);
134
135 #[cfg(not(target_family = "wasm"))]
136 app.add_plugins(WireframePlugin::default());
137
138 app.init_state::<BoundingShape>()
139 .init_state::<ShapeActive>()
140 .add_systems(Startup, setup)
141 .add_systems(
142 Update,
143 (
144 (
145 rotate_2d_shapes.run_if(input_toggle_active(true, KeyCode::KeyR)),
146 bounding_shapes_2d,
147 )
148 .run_if(state_in_one_of([ShapeActive::Heart, ShapeActive::Ring])),
149 (
150 rotate_3d_shapes.run_if(input_toggle_active(true, KeyCode::KeyR)),
151 bounding_shapes_3d,
152 )
153 .run_if(state_in_one_of([
154 ShapeActive::Extrusion,
155 ShapeActive::RingExtrusion,
156 ])),
157 update_bounding_shape.run_if(input_just_pressed(KeyCode::KeyB)),
158 switch_shapes.run_if(input_just_pressed(KeyCode::Tab)),
159 ),
160 );
161
162 #[cfg(not(target_family = "wasm"))]
163 app.add_systems(
164 Update,
165 toggle_wireframes.run_if(input_just_pressed(KeyCode::Space)),
166 );
167
168 app.run();
169}
170
171fn setup(
172 mut commands: Commands,
173 mut meshes: ResMut<Assets<Mesh>>,
174 mut materials: ResMut<Assets<StandardMaterial>>,
175) {
176 commands.spawn((Camera3d::default(), TRANSFORM_2D, PROJECTION_2D));
178
179 commands.spawn((
181 Mesh3d(meshes.add(HEART.mesh().resolution(50))),
183 MeshMaterial3d(materials.add(StandardMaterial {
184 emissive: RED.into(),
185 base_color: RED.into(),
186 ..Default::default()
187 })),
188 Transform::from_xyz(0.0, 0.0, 0.0),
189 Shape2d,
190 Visibility::Visible,
191 ShapeActive::Heart,
192 ));
193
194 commands.spawn((
196 Mesh3d(meshes.add(RING.mesh().with_inner(|heart| heart.resolution(50)))),
198 MeshMaterial3d(materials.add(StandardMaterial {
199 emissive: RED.into(),
200 base_color: RED.into(),
201 ..Default::default()
202 })),
203 Transform::from_xyz(0.0, 0.0, 0.0),
204 Shape2d,
205 Visibility::Hidden,
206 ShapeActive::Ring,
207 ));
208
209 commands.spawn((
211 Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))),
213 MeshMaterial3d(materials.add(StandardMaterial {
214 base_color: RED.into(),
215 ..Default::default()
216 })),
217 Transform::from_xyz(0., -3., -5.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
218 Shape3d,
219 Visibility::Hidden,
220 ShapeActive::Extrusion,
221 ));
222
223 commands.spawn((
225 Mesh3d(
227 meshes.add(
228 RING_EXTRUSION
229 .mesh()
230 .with_inner(|ring| ring.with_inner(|heart| heart.resolution(50))),
231 ),
232 ),
233 MeshMaterial3d(materials.add(StandardMaterial {
234 base_color: RED.into(),
235 ..Default::default()
236 })),
237 Transform::from_xyz(0., -3., -5.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
238 Shape3d,
239 Visibility::Hidden,
240 ShapeActive::RingExtrusion,
241 ));
242
243 commands.spawn((
245 PointLight {
246 shadow_maps_enabled: true,
247 intensity: 10_000_000.,
248 range: 100.0,
249 shadow_depth_bias: 0.2,
250 ..default()
251 },
252 Transform::from_xyz(8.0, 12.0, 1.0),
253 ));
254
255 let mut text = "\
256 Press 'B' to cycle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
257 Press 'Tab' to cycle between 2D and 3D shapes\n\
258 Press 'R' to pause/resume rotation".to_string();
259 #[cfg(not(target_family = "wasm"))]
260 text.push_str("\nPress 'Space' to toggle display of wireframes");
261 commands.spawn((
263 Text::new(text),
264 Node {
265 position_type: PositionType::Absolute,
266 top: px(12),
267 left: px(12),
268 ..default()
269 },
270 ));
271}
272
273fn rotate_2d_shapes(mut shapes: Query<&mut Transform, With<Shape2d>>, time: Res<Time>) {
275 let elapsed_seconds = time.elapsed_secs();
276
277 for mut transform in shapes.iter_mut() {
278 transform.rotation = Quat::from_rotation_z(elapsed_seconds);
279 }
280}
281
282fn bounding_shapes_2d(
284 shapes: Query<&Transform, With<Shape2d>>,
285 mut gizmos: Gizmos,
286 bounding_shape: Res<State<BoundingShape>>,
287) {
288 for transform in shapes.iter() {
289 let rotation = transform.rotation.to_scaled_axis().z;
291 let rotation = Rot2::radians(rotation);
292 let isometry = Isometry2d::new(transform.translation.xy(), rotation);
293
294 match bounding_shape.get() {
295 BoundingShape::None => (),
296 BoundingShape::BoundingBox => {
297 let aabb = HEART.aabb_2d(isometry);
299 gizmos.rect_2d(aabb.center(), aabb.half_size() * 2., WHITE);
300 }
301 BoundingShape::BoundingSphere => {
302 let bounding_circle = HEART.bounding_circle(isometry);
304 gizmos
305 .circle_2d(bounding_circle.center(), bounding_circle.radius(), WHITE)
306 .resolution(64);
307 }
308 }
309 }
310}
311
312fn rotate_3d_shapes(mut shapes: Query<&mut Transform, With<Shape3d>>, time: Res<Time>) {
314 let delta_seconds = time.delta_secs();
315
316 for mut transform in shapes.iter_mut() {
317 transform.rotate_y(delta_seconds);
318 }
319}
320
321fn bounding_shapes_3d(
323 shapes: Query<&Transform, With<Shape3d>>,
324 mut gizmos: Gizmos,
325 bounding_shape: Res<State<BoundingShape>>,
326) {
327 for transform in shapes.iter() {
328 match bounding_shape.get() {
329 BoundingShape::None => (),
330 BoundingShape::BoundingBox => {
331 let aabb = EXTRUSION.aabb_3d(transform.to_isometry());
333
334 gizmos.primitive_3d(
335 &Cuboid::from_size(Vec3::from(aabb.half_size()) * 2.),
336 aabb.center(),
337 WHITE,
338 );
339 }
340 BoundingShape::BoundingSphere => {
341 let bounding_sphere = EXTRUSION.bounding_sphere(transform.to_isometry());
343
344 gizmos.sphere(bounding_sphere.center(), bounding_sphere.radius(), WHITE);
345 }
346 }
347 }
348}
349
350fn update_bounding_shape(
352 current: Res<State<BoundingShape>>,
353 mut next: ResMut<NextState<BoundingShape>>,
354) {
355 next.set(match current.get() {
356 BoundingShape::None => BoundingShape::BoundingBox,
357 BoundingShape::BoundingBox => BoundingShape::BoundingSphere,
358 BoundingShape::BoundingSphere => BoundingShape::None,
359 });
360}
361
362fn switch_shapes(
364 current: Res<State<ShapeActive>>,
365 mut next: ResMut<NextState<ShapeActive>>,
366 camera: Single<(&mut Transform, &mut Projection)>,
367 mut shapes: Query<(&mut Visibility, &ShapeActive)>,
368) {
369 let next_state = current.get().next_shape();
370 next.set(next_state);
371
372 for (mut visibility, shape) in &mut shapes {
373 if next_state == *shape {
374 *visibility = Visibility::Visible;
375 } else {
376 *visibility = Visibility::Hidden;
377 }
378 }
379
380 let (mut transform, mut projection) = camera.into_inner();
381 match next_state {
382 ShapeActive::Heart | ShapeActive::Ring => {
383 *transform = TRANSFORM_2D;
384 *projection = PROJECTION_2D;
385 }
386 ShapeActive::Extrusion | ShapeActive::RingExtrusion => {
387 *transform = TRANSFORM_3D;
388 *projection = PROJECTION_3D;
389 }
390 };
391}
392
393#[cfg(not(target_family = "wasm"))]
394fn toggle_wireframes(mut wireframe_config: ResMut<WireframeConfig>) {
395 wireframe_config.global = !wireframe_config.global;
396}
397
398#[derive(Copy, Clone)]
402struct Heart {
403 radius: f32,
405}
406
407impl Primitive2d for Heart {}
410
411impl Heart {
412 const fn new(radius: f32) -> Self {
413 Self { radius }
414 }
415}
416
417impl Measured2d for Heart {
420 fn perimeter(&self) -> f32 {
421 self.radius * (2.5 * PI + ops::powf(2f32, 1.5) + 2.0)
422 }
423
424 fn area(&self) -> f32 {
425 let circle_area = PI * self.radius * self.radius;
426 let triangle_area = self.radius * self.radius * (1.0 + 2f32.sqrt()) / 2.0;
427 let cutout = triangle_area - circle_area * 3.0 / 16.0;
428
429 2.0 * circle_area + 4.0 * cutout
430 }
431}
432
433impl Bounded2d for Heart {
435 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
436 let isometry = isometry.into();
437
438 let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
440 let max_circle = circle_center.abs() + Vec2::splat(self.radius);
442 let min_circle = -max_circle;
444
445 let tip_position = isometry.rotation * Vec2::new(0.0, -self.radius * (1. + SQRT_2));
447
448 Aabb2d {
449 min: isometry.translation + min_circle.min(tip_position),
450 max: isometry.translation + max_circle.max(tip_position),
451 }
452 }
453
454 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
455 let isometry = isometry.into();
456
457 let offset = self.radius / ops::powf(2f32, 1.5);
459 let center = isometry * Vec2::new(0.0, -offset);
461 let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
463
464 BoundingCircle::new(center, radius)
465 }
466}
467impl BoundedExtrusion for Heart {}
470
471impl Meshable for Heart {
473 type Output = HeartMeshBuilder;
475
476 fn mesh(&self) -> Self::Output {
477 Self::Output {
478 heart: *self,
479 resolution: 32,
480 }
481 }
482}
483
484struct HeartMeshBuilder {
486 heart: Heart,
487 resolution: usize,
489}
490
491trait HeartBuilder {
494 fn resolution(self, resolution: usize) -> Self;
496}
497
498impl HeartBuilder for HeartMeshBuilder {
499 fn resolution(mut self, resolution: usize) -> Self {
500 self.resolution = resolution;
501 self
502 }
503}
504
505impl HeartBuilder for ExtrusionBuilder<Heart> {
506 fn resolution(mut self, resolution: usize) -> Self {
507 self.base_builder.resolution = resolution;
508 self
509 }
510}
511
512impl MeshBuilder for HeartMeshBuilder {
513 fn build(&self) -> Mesh {
515 let radius = self.heart.radius;
516 let wing_angle = PI * 1.25;
518
519 let mut vertices = Vec::with_capacity(2 * self.resolution);
521 let mut uvs = Vec::with_capacity(2 * self.resolution);
522 let mut indices = Vec::with_capacity(6 * self.resolution - 9);
523 let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
525
526 vertices.push([0.0; 3]);
528 uvs.push([0.5, 0.5]);
529
530 for i in 1..self.resolution {
532 let angle = (i as f32 / self.resolution as f32) * wing_angle;
533 let (sin, cos) = ops::sin_cos(angle);
534 vertices.push([radius * (cos - 1.0), radius * sin, 0.0]);
535 uvs.push([0.5 - (cos - 1.0) / 4., 0.5 - sin / 2.]);
536 }
537
538 vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
540 uvs.push([0.5, 1.]);
541
542 for i in 0..self.resolution - 1 {
544 let angle = (i as f32 / self.resolution as f32) * wing_angle - PI / 4.;
545 let (sin, cos) = ops::sin_cos(angle);
546 vertices.push([radius * (cos + 1.0), radius * sin, 0.0]);
547 uvs.push([0.5 - (cos + 1.0) / 4., 0.5 - sin / 2.]);
548 }
549
550 for i in 2..2 * self.resolution as u32 {
553 indices.extend_from_slice(&[i - 1, i, 0]);
554 }
555
556 Mesh::new(
558 bevy::mesh::PrimitiveTopology::TriangleList,
559 RenderAssetUsages::default(),
560 )
561 .with_inserted_indices(bevy::mesh::Indices::U32(indices))
562 .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
563 .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
564 .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
565 }
566}
567
568impl Extrudable for HeartMeshBuilder {
570 fn perimeter(&self) -> Vec<PerimeterSegment> {
571 let resolution = self.resolution as u32;
572 vec![
573 PerimeterSegment::Smooth {
575 first_normal: Vec2::X,
577 last_normal: Vec2::new(-1.0, -1.0).normalize(),
578 indices: (0..resolution).collect(),
580 },
581 PerimeterSegment::Flat {
583 indices: vec![resolution - 1, resolution, resolution + 1],
584 },
585 PerimeterSegment::Smooth {
587 first_normal: Vec2::new(1.0, -1.0).normalize(),
588 last_normal: Vec2::NEG_X,
589 indices: (resolution + 1..2 * resolution).chain([0]).collect(),
590 },
591 ]
592 }
593}
594
595fn state_in_one_of<S: States, const N: usize>(
597 states: [S; N],
598) -> impl FnMut(Option<Res<State<S>>>) -> bool + Clone {
599 move |current_state: Option<Res<State<S>>>| match current_state {
600 Some(current_state) => states.contains(¤t_state),
601 None => false,
602 }
603}