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