Dedicated primitive example (#11697)

I just implemented this to record a video for the new blog post, but I
figured it would also make a good dedicated example. This also allows us
to remove a lot of code from the 2d/3d gizmo examples since it
supersedes this portion of code.

Depends on: https://github.com/bevyengine/bevy/pull/11699
This commit is contained in:
Robert Walter 2024-02-14 16:55:44 +00:00 committed by GitHub
parent 8cf3447343
commit b446374392
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 681 additions and 374 deletions

View file

@ -2644,6 +2644,17 @@ description = "Demonstrates creating and using custom Ui materials"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true wasm = true
[[example]]
name = "render_primitives"
path = "examples/math/render_primitives.rs"
doc-scrape-examples = true
[package.metadata.example.render_primitives]
name = "Rendering Primitives"
description = "Shows off rendering for all math primitives as both Meshes and Gizmos"
category = "Math"
wasm = true
[profile.wasm-release] [profile.wasm-release]
inherits = "release" inherits = "release"
opt-level = "z" opt-level = "z"

View file

@ -6,12 +6,10 @@ use bevy::prelude::*;
fn main() { fn main() {
App::new() App::new()
.init_state::<PrimitiveState>()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.init_gizmo_group::<MyRoundGizmos>() .init_gizmo_group::<MyRoundGizmos>()
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, (draw_example_collection, update_config)) .add_systems(Update, (draw_example_collection, update_config))
.add_systems(Update, (draw_primitives, update_primitives))
.run(); .run();
} }
@ -19,61 +17,13 @@ fn main() {
#[derive(Default, Reflect, GizmoConfigGroup)] #[derive(Default, Reflect, GizmoConfigGroup)]
struct MyRoundGizmos {} struct MyRoundGizmos {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)]
enum PrimitiveState {
#[default]
Nothing,
Circle,
Ellipse,
Capsule,
Line,
Plane,
Segment,
Triangle,
Rectangle,
RegularPolygon,
}
impl PrimitiveState {
const ALL: [Self; 10] = [
Self::Nothing,
Self::Circle,
Self::Ellipse,
Self::Capsule,
Self::Line,
Self::Plane,
Self::Segment,
Self::Triangle,
Self::Rectangle,
Self::RegularPolygon,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn last(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2dBundle::default());
// text // text
commands.spawn(TextBundle::from_section( commands.spawn(TextBundle::from_section(
"Hold 'Left' or 'Right' to change the line width of straight gizmos\n\ "Hold 'Left' or 'Right' to change the line width of straight gizmos\n\
Hold 'Up' or 'Down' to change the line width of round gizmos\n\ Hold 'Up' or 'Down' to change the line width of round gizmos\n\
Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\ Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos",
Press 'K' or 'J' to cycle through primitives rendered with gizmos",
TextStyle { TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"), font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24., font_size: 24.,
@ -130,92 +80,6 @@ fn draw_example_collection(
); );
} }
fn draw_primitives(
mut gizmos: Gizmos,
time: Res<Time>,
primitive_state: Res<State<PrimitiveState>>,
) {
let angle = time.elapsed_seconds();
let rotation = Mat2::from_angle(angle);
let position = rotation * Vec2::X;
let color = Color::WHITE;
const SIZE: f32 = 50.0;
match primitive_state.get() {
PrimitiveState::Nothing => {}
PrimitiveState::Circle => {
gizmos.primitive_2d(Circle { radius: SIZE }, position, angle, color);
}
PrimitiveState::Ellipse => gizmos.primitive_2d(
Ellipse {
half_size: Vec2::new(SIZE, SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::Capsule => gizmos.primitive_2d(
Capsule2d {
radius: SIZE * 0.5,
half_length: SIZE,
},
position,
angle,
color,
),
PrimitiveState::Line => drop(gizmos.primitive_2d(
Line2d {
direction: Direction2d::X,
},
position,
angle,
color,
)),
PrimitiveState::Plane => gizmos.primitive_2d(
Plane2d {
normal: Direction2d::Y,
},
position,
angle,
color,
),
PrimitiveState::Segment => drop(gizmos.primitive_2d(
Segment2d {
direction: Direction2d::X,
half_length: SIZE * 0.5,
},
position,
angle,
color,
)),
PrimitiveState::Triangle => gizmos.primitive_2d(
Triangle2d {
vertices: [Vec2::ZERO, Vec2::Y, Vec2::X].map(|p| p * SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::Rectangle => gizmos.primitive_2d(
Rectangle {
half_size: Vec2::splat(SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::RegularPolygon => gizmos.primitive_2d(
RegularPolygon {
circumcircle: Circle { radius: SIZE * 0.5 },
sides: 5,
},
position,
angle,
color,
),
}
}
fn update_config( fn update_config(
mut config_store: ResMut<GizmoConfigStore>, mut config_store: ResMut<GizmoConfigStore>,
keyboard: Res<ButtonInput<KeyCode>>, keyboard: Res<ButtonInput<KeyCode>>,
@ -247,16 +111,3 @@ fn update_config(
my_config.enabled ^= true; my_config.enabled ^= true;
} }
} }
fn update_primitives(
keyboard: Res<ButtonInput<KeyCode>>,
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
primitive_state: Res<State<PrimitiveState>>,
) {
if keyboard.just_pressed(KeyCode::KeyJ) {
next_primitive_state.set(primitive_state.get().last());
}
if keyboard.just_pressed(KeyCode::KeyK) {
next_primitive_state.set(primitive_state.get().next());
}
}

View file

@ -2,21 +2,15 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use bevy::math::primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Segment3d, Sphere, Torus,
};
use bevy::prelude::*; use bevy::prelude::*;
fn main() { fn main() {
App::new() App::new()
.insert_state(PrimitiveState::Nothing)
.init_resource::<PrimitiveSegments>()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.init_gizmo_group::<MyRoundGizmos>() .init_gizmo_group::<MyRoundGizmos>()
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, rotate_camera) .add_systems(Update, rotate_camera)
.add_systems(Update, (draw_example_collection, update_config)) .add_systems(Update, (draw_example_collection, update_config))
.add_systems(Update, (draw_primitives, update_primitives))
.run(); .run();
} }
@ -24,62 +18,6 @@ fn main() {
#[derive(Default, Reflect, GizmoConfigGroup)] #[derive(Default, Reflect, GizmoConfigGroup)]
struct MyRoundGizmos {} struct MyRoundGizmos {}
#[derive(Debug, Clone, Resource)]
struct PrimitiveSegments(usize);
impl Default for PrimitiveSegments {
fn default() -> Self {
Self(10)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)]
enum PrimitiveState {
Nothing,
Sphere,
Plane,
Line,
LineSegment,
Cuboid,
Cylinder,
Capsule,
Cone,
ConicalFrustum,
Torus,
}
impl PrimitiveState {
const ALL: [Self; 11] = [
Self::Sphere,
Self::Plane,
Self::Line,
Self::LineSegment,
Self::Cuboid,
Self::Cylinder,
Self::Capsule,
Self::Cone,
Self::ConicalFrustum,
Self::Torus,
Self::Nothing,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn last(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
fn setup( fn setup(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
@ -193,145 +131,6 @@ fn draw_example_collection(
gizmos.arrow(Vec3::ZERO, Vec3::ONE * 1.5, Color::YELLOW); gizmos.arrow(Vec3::ZERO, Vec3::ONE * 1.5, Color::YELLOW);
} }
fn draw_primitives(
mut gizmos: Gizmos,
time: Res<Time>,
primitive_state: Res<State<PrimitiveState>>,
segments: Res<PrimitiveSegments>,
) {
let normal = Vec3::new(
time.elapsed_seconds().sin(),
time.elapsed_seconds().cos(),
time.elapsed_seconds().sin().cos(),
)
.try_normalize()
.unwrap_or(Vec3::X);
let angle = time.elapsed_seconds().to_radians() * 10.0;
let center = Quat::from_axis_angle(Vec3::Z, angle) * Vec3::X;
let rotation = Quat::from_rotation_arc(Vec3::Y, normal);
let segments = segments.0;
match primitive_state.get() {
PrimitiveState::Nothing => {}
PrimitiveState::Sphere => {
gizmos
.primitive_3d(Sphere { radius: 1.0 }, center, rotation, Color::default())
.segments(segments);
}
PrimitiveState::Plane => {
gizmos
.primitive_3d(
Plane3d {
normal: Direction3d::Y,
},
center,
rotation,
Color::default(),
)
.axis_count((segments / 5).max(4))
.segment_count(segments)
.segment_length(1.0 / segments as f32);
}
PrimitiveState::Line => {
gizmos.primitive_3d(
Line3d {
direction: Direction3d::X,
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::LineSegment => {
gizmos.primitive_3d(
Segment3d {
direction: Direction3d::X,
half_length: 1.0,
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::Cuboid => {
gizmos.primitive_3d(
Cuboid {
half_size: Vec3::new(1.0, 0.5, 2.0),
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::Cylinder => {
gizmos
.primitive_3d(
Cylinder {
radius: 1.0,
half_height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Capsule => {
gizmos
.primitive_3d(
Capsule3d {
radius: 1.0,
half_length: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Cone => {
gizmos
.primitive_3d(
Cone {
radius: 1.0,
height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::ConicalFrustum => {
gizmos
.primitive_3d(
ConicalFrustum {
radius_top: 0.5,
radius_bottom: 1.0,
height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Torus => {
gizmos
.primitive_3d(
Torus {
minor_radius: 0.3,
major_radius: 1.0,
},
center,
rotation,
Color::default(),
)
.major_segments(segments)
.minor_segments((segments / 4).max(1));
}
}
}
fn update_config( fn update_config(
mut config_store: ResMut<GizmoConfigStore>, mut config_store: ResMut<GizmoConfigStore>,
keyboard: Res<ButtonInput<KeyCode>>, keyboard: Res<ButtonInput<KeyCode>>,
@ -383,26 +182,3 @@ fn update_config(
config_store.config_mut::<AabbGizmoConfigGroup>().1.draw_all ^= true; config_store.config_mut::<AabbGizmoConfigGroup>().1.draw_all ^= true;
} }
} }
fn update_primitives(
keyboard: Res<ButtonInput<KeyCode>>,
primitive_state: Res<State<PrimitiveState>>,
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
mut segments: ResMut<PrimitiveSegments>,
mut segments_f: Local<f32>,
) {
if keyboard.just_pressed(KeyCode::KeyK) {
next_primitive_state.set(primitive_state.get().next());
}
if keyboard.just_pressed(KeyCode::KeyJ) {
next_primitive_state.set(primitive_state.get().last());
}
if keyboard.pressed(KeyCode::KeyL) {
*segments_f = (*segments_f + 0.05).max(2.0);
segments.0 = segments_f.floor() as usize;
}
if keyboard.pressed(KeyCode::KeyH) {
*segments_f = (*segments_f - 0.05).max(2.0);
segments.0 = segments_f.floor() as usize;
}
}

View file

@ -49,6 +49,7 @@ git checkout v0.4.0
- [ECS (Entity Component System)](#ecs-entity-component-system) - [ECS (Entity Component System)](#ecs-entity-component-system)
- [Games](#games) - [Games](#games)
- [Input](#input) - [Input](#input)
- [Math](#math)
- [Reflection](#reflection) - [Reflection](#reflection)
- [Scene](#scene) - [Scene](#scene)
- [Shaders](#shaders) - [Shaders](#shaders)
@ -277,6 +278,12 @@ Example | Description
[Touch Input](../examples/input/touch_input.rs) | Displays touch presses, releases, and cancels [Touch Input](../examples/input/touch_input.rs) | Displays touch presses, releases, and cancels
[Touch Input Events](../examples/input/touch_input_events.rs) | Prints out all touch inputs [Touch Input Events](../examples/input/touch_input_events.rs) | Prints out all touch inputs
## Math
Example | Description
--- | ---
[Rendering Primitives](../examples/math/render_primitives.rs) | Shows off rendering for all math primitives as both Meshes and Gizmos
## Reflection ## Reflection
Example | Description Example | Description

View file

@ -0,0 +1,662 @@
//! This example demonstrates how each of Bevy's math primitives look like in 2D and 3D with meshes
//! and with gizmos
#![allow(clippy::match_same_arms)]
use bevy::{
input::common_conditions::input_just_pressed, prelude::*, sprite::MaterialMesh2dBundle,
};
const LEFT_RIGHT_OFFSET_2D: f32 = 200.0;
const LEFT_RIGHT_OFFSET_3D: f32 = 2.0;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.init_state::<PrimitiveSelected>()
.init_state::<CameraActive>();
// cameras
app.add_systems(Startup, (setup_cameras, setup_lights, setup_ambient_light))
.add_systems(
Update,
(
update_active_cameras.run_if(state_changed::<CameraActive>),
switch_cameras.run_if(input_just_pressed(KeyCode::KeyC)),
),
);
// text
// PostStartup since we need the cameras to exist
app.add_systems(PostStartup, setup_text);
app.add_systems(
Update,
(update_text.run_if(state_changed::<PrimitiveSelected>),),
);
// primitives
app.add_systems(Startup, (spawn_primitive_2d, spawn_primitive_3d))
.add_systems(
Update,
(
switch_to_next_primitive.run_if(input_just_pressed(KeyCode::ArrowUp)),
switch_to_previous_primitive.run_if(input_just_pressed(KeyCode::ArrowDown)),
draw_gizmos_2d.run_if(in_mode(CameraActive::Dim2)),
draw_gizmos_3d.run_if(in_mode(CameraActive::Dim3)),
update_primitive_meshes.run_if(
state_changed::<PrimitiveSelected>.or_else(state_changed::<CameraActive>),
),
rotate_primitive_2d_meshes,
rotate_primitive_3d_meshes,
),
);
app.run();
}
/// State for tracking which of the two cameras (2D & 3D) is currently active
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum CameraActive {
#[default]
/// 2D Camera is active
Dim2,
/// 3D Camera is active
Dim3,
}
/// State for tracking which primitives are currently displayed
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum PrimitiveSelected {
#[default]
RectangleAndCuboid,
CircleAndSphere,
Ellipse,
Triangle,
Plane,
Line,
Segment,
Polyline,
Polygon,
RegularPolygon,
Capsule,
Cylinder,
Cone,
ConicalFrustrum,
Torus,
}
impl std::fmt::Display for PrimitiveSelected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
PrimitiveSelected::RectangleAndCuboid => String::from("Rectangle/Cuboid"),
PrimitiveSelected::CircleAndSphere => String::from("Circle/Sphere"),
other => format!("{other:?}"),
};
write!(f, "{name}")
}
}
impl PrimitiveSelected {
const ALL: [Self; 15] = [
Self::RectangleAndCuboid,
Self::CircleAndSphere,
Self::Ellipse,
Self::Triangle,
Self::Plane,
Self::Line,
Self::Segment,
Self::Polyline,
Self::Polygon,
Self::RegularPolygon,
Self::Capsule,
Self::Cylinder,
Self::Cone,
Self::ConicalFrustrum,
Self::Torus,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn previous(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
const SMALL_2D: f32 = 50.0;
const BIG_2D: f32 = 100.0;
const SMALL_3D: f32 = 0.5;
const BIG_3D: f32 = 1.0;
// primitives
const RECTANGLE: Rectangle = Rectangle {
half_size: Vec2::new(SMALL_2D, BIG_2D),
};
const CUBOID: Cuboid = Cuboid {
half_size: Vec3::new(BIG_3D, SMALL_3D, BIG_3D),
};
const CIRCLE: Circle = Circle { radius: BIG_2D };
const SPHERE: Sphere = Sphere { radius: BIG_3D };
const ELLIPSE: Ellipse = Ellipse {
half_size: Vec2::new(BIG_2D, SMALL_2D),
};
const TRIANGLE: Triangle2d = Triangle2d {
vertices: [
Vec2::new(SMALL_2D, 0.0),
Vec2::new(0.0, SMALL_2D),
Vec2::new(-SMALL_2D, 0.0),
],
};
const PLANE_2D: Plane2d = Plane2d {
normal: Direction2d::Y,
};
const PLANE_3D: Plane3d = Plane3d {
normal: Direction3d::Y,
};
const LINE2D: Line2d = Line2d {
direction: Direction2d::X,
};
const LINE3D: Line3d = Line3d {
direction: Direction3d::X,
};
const SEGMENT_2D: Segment2d = Segment2d {
direction: Direction2d::X,
half_length: BIG_2D,
};
const SEGMENT_3D: Segment3d = Segment3d {
direction: Direction3d::X,
half_length: BIG_3D,
};
const POLYLINE_2D: Polyline2d<4> = Polyline2d {
vertices: [
Vec2::new(-BIG_2D, -SMALL_2D),
Vec2::new(-SMALL_2D, SMALL_2D),
Vec2::new(SMALL_2D, -SMALL_2D),
Vec2::new(BIG_2D, SMALL_2D),
],
};
const POLYLINE_3D: Polyline3d<4> = Polyline3d {
vertices: [
Vec3::new(-BIG_3D, -SMALL_3D, -SMALL_3D),
Vec3::new(SMALL_3D, SMALL_3D, 0.0),
Vec3::new(-SMALL_3D, -SMALL_3D, 0.0),
Vec3::new(BIG_3D, SMALL_3D, SMALL_3D),
],
};
const POLYGON_2D: Polygon<5> = Polygon {
vertices: [
Vec2::new(-BIG_2D, -SMALL_2D),
Vec2::new(BIG_2D, -SMALL_2D),
Vec2::new(BIG_2D, SMALL_2D),
Vec2::new(0.0, 0.0),
Vec2::new(-BIG_2D, SMALL_2D),
],
};
const REGULAR_POLYGON: RegularPolygon = RegularPolygon {
circumcircle: Circle { radius: BIG_2D },
sides: 5,
};
const CAPSULE_2D: Capsule2d = Capsule2d {
radius: SMALL_2D,
half_length: SMALL_2D,
};
const CAPSULE_3D: Capsule3d = Capsule3d {
radius: SMALL_3D,
half_length: SMALL_3D,
};
const CYLINDER: Cylinder = Cylinder {
radius: SMALL_3D,
half_height: SMALL_3D,
};
const CONE: Cone = Cone {
radius: BIG_3D,
height: BIG_3D,
};
const CONICAL_FRUSTRUM: ConicalFrustum = ConicalFrustum {
radius_top: BIG_3D,
radius_bottom: SMALL_3D,
height: BIG_3D,
};
const TORUS: Torus = Torus {
minor_radius: SMALL_3D / 2.0,
major_radius: SMALL_3D * 1.5,
};
fn setup_cameras(mut commands: Commands) {
let start_in_2d = true;
let make_camera = |is_active| Camera {
is_active,
..Default::default()
};
commands.spawn(Camera2dBundle {
camera: make_camera(start_in_2d),
..Default::default()
});
commands.spawn(Camera3dBundle {
camera: make_camera(!start_in_2d),
transform: Transform::from_xyz(0.0, 10.0, 0.0).looking_at(Vec3::ZERO, Vec3::Z),
..Default::default()
});
}
fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
ambient_light.brightness = 50.0;
}
fn setup_lights(mut commands: Commands) {
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 5000.0,
..default()
},
transform: Transform::from_translation(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 2.0, 0.0))
.looking_at(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0), Vec3::Y),
..default()
});
}
/// Marker component for header text
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct HeaderText;
/// Marker component for header node
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct HeaderNode;
fn update_active_cameras(
state: Res<State<CameraActive>>,
mut camera_2d: Query<(Entity, &mut Camera), With<Camera2d>>,
mut camera_3d: Query<(Entity, &mut Camera), (With<Camera3d>, Without<Camera2d>)>,
mut text: Query<&mut TargetCamera, With<HeaderNode>>,
) {
let (entity_2d, mut cam_2d) = camera_2d.single_mut();
let (entity_3d, mut cam_3d) = camera_3d.single_mut();
let is_camera_2d_active = matches!(*state.get(), CameraActive::Dim2);
cam_2d.is_active = is_camera_2d_active;
cam_3d.is_active = !is_camera_2d_active;
let active_camera = if is_camera_2d_active {
entity_2d
} else {
entity_3d
};
text.iter_mut().for_each(|mut target_camera| {
*target_camera = TargetCamera(active_camera);
});
}
fn switch_cameras(current: Res<State<CameraActive>>, mut next: ResMut<NextState<CameraActive>>) {
let next_state = match current.get() {
CameraActive::Dim2 => CameraActive::Dim3,
CameraActive::Dim3 => CameraActive::Dim2,
};
next.set(next_state);
}
fn setup_text(
mut commands: Commands,
asset_server: Res<AssetServer>,
cameras: Query<(Entity, &Camera)>,
) {
let active_camera = cameras
.iter()
.find_map(|(entity, camera)| camera.is_active.then_some(entity))
.expect("run condition ensures existence");
let text = format!("{text}", text = PrimitiveSelected::default());
let font_size = 24.0;
let font: Handle<Font> = asset_server.load("fonts/FiraMono-Medium.ttf");
let style = TextStyle {
font,
font_size,
color: Color::WHITE,
};
let instructions = "Press 'C' to switch between 2D and 3D mode\n\
Press 'Up' or 'Down' to switch to the next/previous primitive";
let text = [
TextSection::new("Primitive: ", style.clone()),
TextSection::new(text, style.clone()),
TextSection::new("\n\n", style.clone()),
TextSection::new(instructions, style.clone()),
TextSection::new("\n\n", style.clone()),
TextSection::new(
"(If nothing is displayed, there's no rendering support yet)",
style.clone(),
),
];
commands
.spawn((
HeaderNode,
NodeBundle {
style: Style {
justify_self: JustifySelf::Center,
top: Val::Px(5.0),
..Default::default()
},
..Default::default()
},
TargetCamera(active_camera),
))
.with_children(|parent| {
parent.spawn((
HeaderText,
TextBundle::from_sections(text).with_text_justify(JustifyText::Center),
));
});
}
fn update_text(
primitive_state: Res<State<PrimitiveSelected>>,
mut header: Query<&mut Text, With<HeaderText>>,
) {
let new_text = format!("{text}", text = primitive_state.get());
header.iter_mut().for_each(|mut header_text| {
if let Some(kind) = header_text.sections.get_mut(1) {
kind.value = new_text.clone();
};
});
}
fn switch_to_next_primitive(
current: Res<State<PrimitiveSelected>>,
mut next: ResMut<NextState<PrimitiveSelected>>,
) {
let next_state = current.get().next();
next.set(next_state);
}
fn switch_to_previous_primitive(
current: Res<State<PrimitiveSelected>>,
mut next: ResMut<NextState<PrimitiveSelected>>,
) {
let next_state = current.get().previous();
next.set(next_state);
}
fn in_mode(active: CameraActive) -> impl Fn(Res<State<CameraActive>>) -> bool {
move |state| *state.get() == active
}
fn draw_gizmos_2d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
const POSITION: Vec2 = Vec2::new(-LEFT_RIGHT_OFFSET_2D, 0.0);
let angle = time.elapsed_seconds();
let color = Color::WHITE;
match state.get() {
PrimitiveSelected::RectangleAndCuboid => {
gizmos.primitive_2d(RECTANGLE, POSITION, angle, color);
}
PrimitiveSelected::CircleAndSphere => gizmos.primitive_2d(CIRCLE, POSITION, angle, color),
PrimitiveSelected::Ellipse => gizmos.primitive_2d(ELLIPSE, POSITION, angle, color),
PrimitiveSelected::Triangle => gizmos.primitive_2d(TRIANGLE, POSITION, angle, color),
PrimitiveSelected::Plane => gizmos.primitive_2d(PLANE_2D, POSITION, angle, color),
PrimitiveSelected::Line => drop(gizmos.primitive_2d(LINE2D, POSITION, angle, color)),
PrimitiveSelected::Segment => drop(gizmos.primitive_2d(SEGMENT_2D, POSITION, angle, color)),
PrimitiveSelected::Polyline => gizmos.primitive_2d(POLYLINE_2D, POSITION, angle, color),
PrimitiveSelected::Polygon => gizmos.primitive_2d(POLYGON_2D, POSITION, angle, color),
PrimitiveSelected::RegularPolygon => {
gizmos.primitive_2d(REGULAR_POLYGON, POSITION, angle, color);
}
PrimitiveSelected::Capsule => gizmos.primitive_2d(CAPSULE_2D, POSITION, angle, color),
PrimitiveSelected::Cylinder => {}
PrimitiveSelected::Cone => {}
PrimitiveSelected::ConicalFrustrum => {}
PrimitiveSelected::Torus => {}
}
}
/// Marker for primitive meshes to record in which state they should be visible in
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct PrimitiveData {
camera_mode: CameraActive,
primitive_state: PrimitiveSelected,
}
/// Marker for meshes of 2D primitives
#[derive(Debug, Clone, Component, Default)]
pub struct MeshDim2;
/// Marker for meshes of 3D primitives
#[derive(Debug, Clone, Component, Default)]
pub struct MeshDim3;
fn spawn_primitive_2d(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_2D, 0.0, 0.0);
let material: Handle<ColorMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim2;
[
Some(RECTANGLE.mesh()),
Some(CIRCLE.mesh().build()),
Some(ELLIPSE.mesh().build()),
Some(TRIANGLE.mesh()),
None, // plane
None, // line
None, // segment
None, // polyline
None, // polygon
Some(REGULAR_POLYGON.mesh()),
Some(CAPSULE_2D.mesh().build()),
None, // cylinder
None, // cone
None, // conical frustrum
None, // torus
]
.into_iter()
.zip(PrimitiveSelected::ALL)
.for_each(|(maybe_mesh, state)| {
if let Some(mesh) = maybe_mesh {
commands.spawn((
MeshDim2,
PrimitiveData {
camera_mode,
primitive_state: state,
},
MaterialMesh2dBundle {
mesh: meshes.add(mesh).into(),
material: material.clone(),
transform: Transform::from_translation(POSITION),
..Default::default()
},
));
}
});
}
fn spawn_primitive_3d(
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
const POSITION: Vec3 = Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
let material: Handle<StandardMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim3;
[
Some(CUBOID.mesh()),
Some(SPHERE.mesh().build()),
None, // ellipse
None, // triangle
Some(PLANE_3D.mesh().build()),
None, // line
None, // segment
None, // polyline
None, // polygon
None, // regular polygon
Some(CAPSULE_3D.mesh().build()),
Some(CYLINDER.mesh().build()),
None, // cone
None, // conical frustrum
Some(TORUS.mesh().build()),
]
.into_iter()
.zip(PrimitiveSelected::ALL)
.for_each(|(maybe_mesh, state)| {
if let Some(mesh) = maybe_mesh {
commands.spawn((
MeshDim3,
PrimitiveData {
camera_mode,
primitive_state: state,
},
PbrBundle {
mesh: meshes.add(mesh),
material: material.clone(),
transform: Transform::from_translation(POSITION),
..Default::default()
},
));
}
});
}
fn update_primitive_meshes(
camera_state: Res<State<CameraActive>>,
primitive_state: Res<State<PrimitiveSelected>>,
mut primitives: Query<(&mut Visibility, &PrimitiveData)>,
) {
primitives.iter_mut().for_each(|(mut vis, primitive)| {
let visible = primitive.camera_mode == *camera_state.get()
&& primitive.primitive_state == *primitive_state.get();
*vis = if visible {
Visibility::Inherited
} else {
Visibility::Hidden
};
});
}
fn rotate_primitive_2d_meshes(
mut primitives_2d: Query<
(&mut Transform, &ViewVisibility),
(With<PrimitiveData>, With<MeshDim2>),
>,
time: Res<Time>,
) {
let rotation_2d = Quat::from_mat3(&Mat3::from_angle(time.elapsed_seconds()));
primitives_2d
.iter_mut()
.filter(|(_, vis)| vis.get())
.for_each(|(mut transform, _)| {
transform.rotation = rotation_2d;
});
}
fn rotate_primitive_3d_meshes(
mut primitives_3d: Query<
(&mut Transform, &ViewVisibility),
(With<PrimitiveData>, With<MeshDim3>),
>,
time: Res<Time>,
) {
let rotation_3d = Quat::from_rotation_arc(
Vec3::Z,
Vec3::new(
time.elapsed_seconds().sin(),
time.elapsed_seconds().cos(),
time.elapsed_seconds().sin() * 0.5,
)
.try_normalize()
.unwrap_or(Vec3::Z),
);
primitives_3d
.iter_mut()
.filter(|(_, vis)| vis.get())
.for_each(|(mut transform, _)| {
transform.rotation = rotation_3d;
});
}
fn draw_gizmos_3d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
let rotation = Quat::from_rotation_arc(
Vec3::Z,
Vec3::new(
time.elapsed_seconds().sin(),
time.elapsed_seconds().cos(),
time.elapsed_seconds().sin() * 0.5,
)
.try_normalize()
.unwrap_or(Vec3::Z),
);
let color = Color::WHITE;
let segments = 10;
match state.get() {
PrimitiveSelected::RectangleAndCuboid => {
gizmos.primitive_3d(CUBOID, POSITION, rotation, color);
}
PrimitiveSelected::CircleAndSphere => drop(
gizmos
.primitive_3d(SPHERE, POSITION, rotation, color)
.segments(segments),
),
PrimitiveSelected::Ellipse => {}
PrimitiveSelected::Triangle => {}
PrimitiveSelected::Plane => drop(gizmos.primitive_3d(PLANE_3D, POSITION, rotation, color)),
PrimitiveSelected::Line => gizmos.primitive_3d(LINE3D, POSITION, rotation, color),
PrimitiveSelected::Segment => gizmos.primitive_3d(SEGMENT_3D, POSITION, rotation, color),
PrimitiveSelected::Polyline => gizmos.primitive_3d(POLYLINE_3D, POSITION, rotation, color),
PrimitiveSelected::Polygon => {}
PrimitiveSelected::RegularPolygon => {}
PrimitiveSelected::Capsule => drop(
gizmos
.primitive_3d(CAPSULE_3D, POSITION, rotation, color)
.segments(segments),
),
PrimitiveSelected::Cylinder => drop(
gizmos
.primitive_3d(CYLINDER, POSITION, rotation, color)
.segments(segments),
),
PrimitiveSelected::Cone => drop(
gizmos
.primitive_3d(CONE, POSITION, rotation, color)
.segments(segments),
),
PrimitiveSelected::ConicalFrustrum => {
gizmos.primitive_3d(CONICAL_FRUSTRUM, POSITION, rotation, color);
}
PrimitiveSelected::Torus => drop(
gizmos
.primitive_3d(TORUS, POSITION, rotation, color)
.minor_segments(segments)
.major_segments(segments),
),
}
}