 7aeb1c51a6
			
		
	
	
		7aeb1c51a6
		
			
		
	
	
	
	
		
			
			Unfortunately, Apple platforms don't have enough texture bindings to properly support clustered decals. This should be fixed once `wgpu` has first-class bindless texture support. In the meantime, we disable them. Closes #17553. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
		
			
				
	
	
		
			554 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			554 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Demonstrates clustered decals, which affix decals to surfaces.
 | ||
| 
 | ||
| use std::f32::consts::{FRAC_PI_3, PI};
 | ||
| use std::fmt::{self, Formatter};
 | ||
| use std::process;
 | ||
| 
 | ||
| use bevy::{
 | ||
|     color::palettes::css::{LIME, ORANGE_RED, SILVER},
 | ||
|     input::mouse::AccumulatedMouseMotion,
 | ||
|     pbr::{
 | ||
|         decal::{self, clustered::ClusteredDecal},
 | ||
|         ExtendedMaterial, MaterialExtension,
 | ||
|     },
 | ||
|     prelude::*,
 | ||
|     render::{
 | ||
|         render_resource::{AsBindGroup, ShaderRef},
 | ||
|         renderer::{RenderAdapter, RenderDevice},
 | ||
|     },
 | ||
|     window::SystemCursorIcon,
 | ||
|     winit::cursor::CursorIcon,
 | ||
| };
 | ||
| use ops::{acos, cos, sin};
 | ||
| use widgets::{
 | ||
|     WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
 | ||
|     BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
 | ||
| };
 | ||
| 
 | ||
| #[path = "../helpers/widgets.rs"]
 | ||
| mod widgets;
 | ||
| 
 | ||
| /// The custom material shader that we use to demonstrate how to use the decal
 | ||
| /// `tag` field.
 | ||
| const SHADER_ASSET_PATH: &str = "shaders/custom_clustered_decal.wgsl";
 | ||
| 
 | ||
| /// The speed at which the cube rotates, in radians per frame.
 | ||
| const CUBE_ROTATION_SPEED: f32 = 0.02;
 | ||
| 
 | ||
| /// The speed at which the selection can be moved, in spherical coordinate
 | ||
| /// radians per mouse unit.
 | ||
| const MOVE_SPEED: f32 = 0.008;
 | ||
| /// The speed at which the selection can be scaled, in reciprocal mouse units.
 | ||
| const SCALE_SPEED: f32 = 0.05;
 | ||
| /// The speed at which the selection can be scaled, in radians per mouse unit.
 | ||
| const ROLL_SPEED: f32 = 0.01;
 | ||
| 
 | ||
| /// Various settings for the demo.
 | ||
| #[derive(Resource, Default)]
 | ||
| struct AppStatus {
 | ||
|     /// The object that will be moved, scaled, or rotated when the mouse is
 | ||
|     /// dragged.
 | ||
|     selection: Selection,
 | ||
|     /// What happens when the mouse is dragged: one of a move, rotate, or scale
 | ||
|     /// operation.
 | ||
|     drag_mode: DragMode,
 | ||
| }
 | ||
| 
 | ||
| /// The object that will be moved, scaled, or rotated when the mouse is dragged.
 | ||
| #[derive(Clone, Copy, Component, Default, PartialEq)]
 | ||
| enum Selection {
 | ||
|     /// The camera.
 | ||
|     ///
 | ||
|     /// The camera can only be moved, not scaled or rotated.
 | ||
|     #[default]
 | ||
|     Camera,
 | ||
|     /// The first decal, which an orange bounding box surrounds.
 | ||
|     DecalA,
 | ||
|     /// The second decal, which a lime green bounding box surrounds.
 | ||
|     DecalB,
 | ||
| }
 | ||
| 
 | ||
| impl fmt::Display for Selection {
 | ||
|     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
 | ||
|         match *self {
 | ||
|             Selection::Camera => f.write_str("camera"),
 | ||
|             Selection::DecalA => f.write_str("decal A"),
 | ||
|             Selection::DecalB => f.write_str("decal B"),
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// What happens when the mouse is dragged: one of a move, rotate, or scale
 | ||
| /// operation.
 | ||
| #[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
 | ||
| enum DragMode {
 | ||
|     /// The mouse moves the current selection.
 | ||
|     #[default]
 | ||
|     Move,
 | ||
|     /// The mouse scales the current selection.
 | ||
|     ///
 | ||
|     /// This only applies to decals, not cameras.
 | ||
|     Scale,
 | ||
|     /// The mouse rotates the current selection around its local Z axis.
 | ||
|     ///
 | ||
|     /// This only applies to decals, not cameras.
 | ||
|     Roll,
 | ||
| }
 | ||
| 
 | ||
| impl fmt::Display for DragMode {
 | ||
|     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
 | ||
|         match *self {
 | ||
|             DragMode::Move => f.write_str("move"),
 | ||
|             DragMode::Scale => f.write_str("scale"),
 | ||
|             DragMode::Roll => f.write_str("roll"),
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// A marker component for the help text in the top left corner of the window.
 | ||
| #[derive(Clone, Copy, Component)]
 | ||
| struct HelpText;
 | ||
| 
 | ||
| /// A shader extension that demonstrates how to use the `tag` field to customize
 | ||
| /// the appearance of your decals.
 | ||
| #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
 | ||
| struct CustomDecalExtension {}
 | ||
| 
 | ||
| impl MaterialExtension for CustomDecalExtension {
 | ||
|     fn fragment_shader() -> ShaderRef {
 | ||
|         SHADER_ASSET_PATH.into()
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Entry point.
 | ||
| fn main() {
 | ||
|     App::new()
 | ||
|         .add_plugins(DefaultPlugins.set(WindowPlugin {
 | ||
|             primary_window: Some(Window {
 | ||
|                 title: "Bevy Clustered Decals Example".into(),
 | ||
|                 ..default()
 | ||
|             }),
 | ||
|             ..default()
 | ||
|         }))
 | ||
|         .add_plugins(MaterialPlugin::<
 | ||
|             ExtendedMaterial<StandardMaterial, CustomDecalExtension>,
 | ||
|         >::default())
 | ||
|         .init_resource::<AppStatus>()
 | ||
|         .add_event::<WidgetClickEvent<Selection>>()
 | ||
|         .add_systems(Startup, setup)
 | ||
|         .add_systems(Update, draw_gizmos)
 | ||
|         .add_systems(Update, rotate_cube)
 | ||
|         .add_systems(Update, widgets::handle_ui_interactions::<Selection>)
 | ||
|         .add_systems(
 | ||
|             Update,
 | ||
|             (handle_selection_change, update_radio_buttons)
 | ||
|                 .after(widgets::handle_ui_interactions::<Selection>),
 | ||
|         )
 | ||
|         .add_systems(Update, process_move_input)
 | ||
|         .add_systems(Update, process_scale_input)
 | ||
|         .add_systems(Update, process_roll_input)
 | ||
|         .add_systems(Update, switch_drag_mode)
 | ||
|         .add_systems(Update, update_help_text)
 | ||
|         .add_systems(Update, update_button_visibility)
 | ||
|         .run();
 | ||
| }
 | ||
| 
 | ||
| /// Creates the scene.
 | ||
| fn setup(
 | ||
|     mut commands: Commands,
 | ||
|     asset_server: Res<AssetServer>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
|     render_device: Res<RenderDevice>,
 | ||
|     render_adapter: Res<RenderAdapter>,
 | ||
|     mut meshes: ResMut<Assets<Mesh>>,
 | ||
|     mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>>,
 | ||
| ) {
 | ||
|     // Error out if clustered decals aren't supported on the current platform.
 | ||
|     if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
 | ||
|         eprintln!("Clustered decals aren't usable on this platform.");
 | ||
|         process::exit(1);
 | ||
|     }
 | ||
| 
 | ||
|     spawn_cube(&mut commands, &mut meshes, &mut materials);
 | ||
|     spawn_camera(&mut commands);
 | ||
|     spawn_light(&mut commands);
 | ||
|     spawn_decals(&mut commands, &asset_server);
 | ||
|     spawn_buttons(&mut commands);
 | ||
|     spawn_help_text(&mut commands, &app_status);
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the cube onto which the decals are projected.
 | ||
| fn spawn_cube(
 | ||
|     commands: &mut Commands,
 | ||
|     meshes: &mut Assets<Mesh>,
 | ||
|     materials: &mut Assets<ExtendedMaterial<StandardMaterial, CustomDecalExtension>>,
 | ||
| ) {
 | ||
|     // Rotate the cube a bit just to make it more interesting.
 | ||
|     let mut transform = Transform::IDENTITY;
 | ||
|     transform.rotate_y(FRAC_PI_3);
 | ||
| 
 | ||
|     commands.spawn((
 | ||
|         Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
 | ||
|         MeshMaterial3d(materials.add(ExtendedMaterial {
 | ||
|             base: StandardMaterial {
 | ||
|                 base_color: SILVER.into(),
 | ||
|                 ..default()
 | ||
|             },
 | ||
|             extension: CustomDecalExtension {},
 | ||
|         })),
 | ||
|         transform,
 | ||
|     ));
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the directional light.
 | ||
| fn spawn_light(commands: &mut Commands) {
 | ||
|     commands.spawn((
 | ||
|         DirectionalLight::default(),
 | ||
|         Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
 | ||
|     ));
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the camera.
 | ||
| fn spawn_camera(commands: &mut Commands) {
 | ||
|     commands
 | ||
|         .spawn(Camera3d::default())
 | ||
|         .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
 | ||
|         // Tag the camera with `Selection::Camera`.
 | ||
|         .insert(Selection::Camera);
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the actual clustered decals.
 | ||
| fn spawn_decals(commands: &mut Commands, asset_server: &AssetServer) {
 | ||
|     let image = asset_server.load("branding/icon.png");
 | ||
| 
 | ||
|     commands.spawn((
 | ||
|         ClusteredDecal {
 | ||
|             image: image.clone(),
 | ||
|             // Tint with red.
 | ||
|             tag: 1,
 | ||
|         },
 | ||
|         calculate_initial_decal_transform(vec3(1.0, 3.0, 5.0), Vec3::ZERO, Vec2::splat(1.1)),
 | ||
|         Selection::DecalA,
 | ||
|     ));
 | ||
| 
 | ||
|     commands.spawn((
 | ||
|         ClusteredDecal {
 | ||
|             image: image.clone(),
 | ||
|             // Tint with blue.
 | ||
|             tag: 2,
 | ||
|         },
 | ||
|         calculate_initial_decal_transform(vec3(-2.0, -1.0, 4.0), Vec3::ZERO, Vec2::splat(2.0)),
 | ||
|         Selection::DecalB,
 | ||
|     ));
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the buttons at the bottom of the screen.
 | ||
| fn spawn_buttons(commands: &mut Commands) {
 | ||
|     // Spawn the radio buttons that allow the user to select an object to
 | ||
|     // control.
 | ||
|     commands
 | ||
|         .spawn(widgets::main_ui_node())
 | ||
|         .with_children(|parent| {
 | ||
|             widgets::spawn_option_buttons(
 | ||
|                 parent,
 | ||
|                 "Drag to Move",
 | ||
|                 &[
 | ||
|                     (Selection::Camera, "Camera"),
 | ||
|                     (Selection::DecalA, "Decal A"),
 | ||
|                     (Selection::DecalB, "Decal B"),
 | ||
|                 ],
 | ||
|             );
 | ||
|         });
 | ||
| 
 | ||
|     // Spawn the drag buttons that allow the user to control the scale and roll
 | ||
|     // of the selected object.
 | ||
|     commands
 | ||
|         .spawn(Node {
 | ||
|             flex_direction: FlexDirection::Row,
 | ||
|             position_type: PositionType::Absolute,
 | ||
|             right: Val::Px(10.0),
 | ||
|             bottom: Val::Px(10.0),
 | ||
|             column_gap: Val::Px(6.0),
 | ||
|             ..default()
 | ||
|         })
 | ||
|         .with_children(|parent| {
 | ||
|             spawn_drag_button(parent, "Scale").insert(DragMode::Scale);
 | ||
|             spawn_drag_button(parent, "Roll").insert(DragMode::Roll);
 | ||
|         });
 | ||
| }
 | ||
| 
 | ||
| /// Spawns a button that the user can drag to change a parameter.
 | ||
| fn spawn_drag_button<'a>(
 | ||
|     commands: &'a mut ChildSpawnerCommands,
 | ||
|     label: &str,
 | ||
| ) -> EntityCommands<'a> {
 | ||
|     let mut kid = commands.spawn(Node {
 | ||
|         border: BUTTON_BORDER,
 | ||
|         justify_content: JustifyContent::Center,
 | ||
|         align_items: AlignItems::Center,
 | ||
|         padding: BUTTON_PADDING,
 | ||
|         ..default()
 | ||
|     });
 | ||
|     kid.insert((
 | ||
|         Button,
 | ||
|         BackgroundColor(Color::BLACK),
 | ||
|         BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
 | ||
|         BUTTON_BORDER_COLOR,
 | ||
|     ))
 | ||
|     .with_children(|parent| {
 | ||
|         widgets::spawn_ui_text(parent, label, Color::WHITE);
 | ||
|     });
 | ||
|     kid
 | ||
| }
 | ||
| 
 | ||
| /// Spawns the help text at the top of the screen.
 | ||
| fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
 | ||
|     commands.spawn((
 | ||
|         Text::new(create_help_string(app_status)),
 | ||
|         Node {
 | ||
|             position_type: PositionType::Absolute,
 | ||
|             top: Val::Px(12.0),
 | ||
|             left: Val::Px(12.0),
 | ||
|             ..default()
 | ||
|         },
 | ||
|         HelpText,
 | ||
|     ));
 | ||
| }
 | ||
| 
 | ||
| /// Draws the outlines that show the bounds of the clustered decals.
 | ||
| fn draw_gizmos(
 | ||
|     mut gizmos: Gizmos,
 | ||
|     decals: Query<(&GlobalTransform, &Selection), With<ClusteredDecal>>,
 | ||
| ) {
 | ||
|     for (global_transform, selection) in &decals {
 | ||
|         let color = match *selection {
 | ||
|             Selection::Camera => continue,
 | ||
|             Selection::DecalA => ORANGE_RED,
 | ||
|             Selection::DecalB => LIME,
 | ||
|         };
 | ||
| 
 | ||
|         gizmos.primitive_3d(
 | ||
|             &Cuboid {
 | ||
|                 // Since the clustered decal is a 1×1×1 cube in model space, its
 | ||
|                 // half-size is half of the scaling part of its transform.
 | ||
|                 half_size: global_transform.scale() * 0.5,
 | ||
|             },
 | ||
|             Isometry3d {
 | ||
|                 rotation: global_transform.rotation(),
 | ||
|                 translation: global_transform.translation_vec3a(),
 | ||
|             },
 | ||
|             color,
 | ||
|         );
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Calculates the initial transform of the clustered decal.
 | ||
| fn calculate_initial_decal_transform(start: Vec3, looking_at: Vec3, size: Vec2) -> Transform {
 | ||
|     let direction = looking_at - start;
 | ||
|     let center = start + direction * 0.5;
 | ||
|     Transform::from_translation(center)
 | ||
|         .with_scale((size * 0.5).extend(direction.length()))
 | ||
|         .looking_to(direction, Vec3::Y)
 | ||
| }
 | ||
| 
 | ||
| /// Rotates the cube a bit every frame.
 | ||
| fn rotate_cube(mut meshes: Query<&mut Transform, With<Mesh3d>>) {
 | ||
|     for mut transform in &mut meshes {
 | ||
|         transform.rotate_y(CUBE_ROTATION_SPEED);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Updates the state of the radio buttons when the user clicks on one.
 | ||
| fn update_radio_buttons(
 | ||
|     mut widgets: Query<(
 | ||
|         Entity,
 | ||
|         Option<&mut BackgroundColor>,
 | ||
|         Has<Text>,
 | ||
|         &WidgetClickSender<Selection>,
 | ||
|     )>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
|     mut writer: TextUiWriter,
 | ||
| ) {
 | ||
|     for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
 | ||
|         let selected = app_status.selection == **sender;
 | ||
|         if let Some(mut bg_color) = maybe_bg_color {
 | ||
|             widgets::update_ui_radio_button(&mut bg_color, selected);
 | ||
|         }
 | ||
|         if has_text {
 | ||
|             widgets::update_ui_radio_button_text(entity, &mut writer, selected);
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Changes the selection when the user clicks a radio button.
 | ||
| fn handle_selection_change(
 | ||
|     mut events: EventReader<WidgetClickEvent<Selection>>,
 | ||
|     mut app_status: ResMut<AppStatus>,
 | ||
| ) {
 | ||
|     for event in events.read() {
 | ||
|         app_status.selection = **event;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Process a drag event that moves the selected object.
 | ||
| fn process_move_input(
 | ||
|     mut selections: Query<(&mut Transform, &Selection)>,
 | ||
|     mouse_buttons: Res<ButtonInput<MouseButton>>,
 | ||
|     mouse_motion: Res<AccumulatedMouseMotion>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
| ) {
 | ||
|     // Only process drags when movement is selected.
 | ||
|     if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
 | ||
|         return;
 | ||
|     }
 | ||
| 
 | ||
|     for (mut transform, selection) in &mut selections {
 | ||
|         if app_status.selection != *selection {
 | ||
|             continue;
 | ||
|         }
 | ||
| 
 | ||
|         let position = transform.translation;
 | ||
| 
 | ||
|         // Convert to spherical coordinates.
 | ||
|         let radius = position.length();
 | ||
|         let mut theta = acos(position.y / radius);
 | ||
|         let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
 | ||
| 
 | ||
|         // Camera movement is the inverse of object movement.
 | ||
|         let (phi_factor, theta_factor) = match *selection {
 | ||
|             Selection::Camera => (1.0, -1.0),
 | ||
|             Selection::DecalA | Selection::DecalB => (-1.0, 1.0),
 | ||
|         };
 | ||
| 
 | ||
|         // Adjust the spherical coordinates. Clamp the inclination to (0, π).
 | ||
|         phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
 | ||
|         theta = f32::clamp(
 | ||
|             theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
 | ||
|             0.001,
 | ||
|             PI - 0.001,
 | ||
|         );
 | ||
| 
 | ||
|         // Convert spherical coordinates back to Cartesian coordinates.
 | ||
|         transform.translation =
 | ||
|             radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
 | ||
| 
 | ||
|         // Look at the center, but preserve the previous roll angle.
 | ||
|         let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
 | ||
|         transform.look_at(Vec3::ZERO, Vec3::Y);
 | ||
|         let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
 | ||
|         transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Processes a drag event that scales the selected target.
 | ||
| fn process_scale_input(
 | ||
|     mut selections: Query<(&mut Transform, &Selection)>,
 | ||
|     mouse_buttons: Res<ButtonInput<MouseButton>>,
 | ||
|     mouse_motion: Res<AccumulatedMouseMotion>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
| ) {
 | ||
|     // Only process drags when the scaling operation is selected.
 | ||
|     if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
 | ||
|         return;
 | ||
|     }
 | ||
| 
 | ||
|     for (mut transform, selection) in &mut selections {
 | ||
|         if app_status.selection == *selection {
 | ||
|             transform.scale *= 1.0 + mouse_motion.delta.x * SCALE_SPEED;
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Processes a drag event that rotates the selected target along its local Z
 | ||
| /// axis.
 | ||
| fn process_roll_input(
 | ||
|     mut selections: Query<(&mut Transform, &Selection)>,
 | ||
|     mouse_buttons: Res<ButtonInput<MouseButton>>,
 | ||
|     mouse_motion: Res<AccumulatedMouseMotion>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
| ) {
 | ||
|     // Only process drags when the rolling operation is selected.
 | ||
|     if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
 | ||
|         return;
 | ||
|     }
 | ||
| 
 | ||
|     for (mut transform, selection) in &mut selections {
 | ||
|         if app_status.selection != *selection {
 | ||
|             continue;
 | ||
|         }
 | ||
| 
 | ||
|         let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
 | ||
|         roll += mouse_motion.delta.x * ROLL_SPEED;
 | ||
|         transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Creates the help string at the top left of the screen.
 | ||
| fn create_help_string(app_status: &AppStatus) -> String {
 | ||
|     format!(
 | ||
|         "Click and drag to {} {}",
 | ||
|         app_status.drag_mode, app_status.selection
 | ||
|     )
 | ||
| }
 | ||
| 
 | ||
| /// Changes the drag mode when the user hovers over the "Scale" and "Roll"
 | ||
| /// buttons in the lower right.
 | ||
| ///
 | ||
| /// If the user is hovering over no such button, this system changes the drag
 | ||
| /// mode back to its default value of [`DragMode::Move`].
 | ||
| fn switch_drag_mode(
 | ||
|     mut commands: Commands,
 | ||
|     mut interactions: Query<(&Interaction, &DragMode)>,
 | ||
|     mut windows: Query<Entity, With<Window>>,
 | ||
|     mouse_buttons: Res<ButtonInput<MouseButton>>,
 | ||
|     mut app_status: ResMut<AppStatus>,
 | ||
| ) {
 | ||
|     if mouse_buttons.pressed(MouseButton::Left) {
 | ||
|         return;
 | ||
|     }
 | ||
| 
 | ||
|     for (interaction, drag_mode) in &mut interactions {
 | ||
|         if *interaction != Interaction::Hovered {
 | ||
|             continue;
 | ||
|         }
 | ||
| 
 | ||
|         app_status.drag_mode = *drag_mode;
 | ||
| 
 | ||
|         // Set the cursor to provide the user with a nice visual hint.
 | ||
|         for window in &mut windows {
 | ||
|             commands
 | ||
|                 .entity(window)
 | ||
|                 .insert(CursorIcon::from(SystemCursorIcon::EwResize));
 | ||
|         }
 | ||
|         return;
 | ||
|     }
 | ||
| 
 | ||
|     app_status.drag_mode = DragMode::Move;
 | ||
| 
 | ||
|     for window in &mut windows {
 | ||
|         commands.entity(window).remove::<CursorIcon>();
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Updates the help text in the top left of the screen to reflect the current
 | ||
| /// selection and drag mode.
 | ||
| fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
 | ||
|     for mut text in &mut help_text {
 | ||
|         text.0 = create_help_string(&app_status);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /// Updates the visibility of the drag mode buttons so that they aren't visible
 | ||
| /// if the camera is selected.
 | ||
| fn update_button_visibility(
 | ||
|     mut nodes: Query<&mut Visibility, With<DragMode>>,
 | ||
|     app_status: Res<AppStatus>,
 | ||
| ) {
 | ||
|     for mut visibility in &mut nodes {
 | ||
|         *visibility = match app_status.selection {
 | ||
|             Selection::Camera => Visibility::Hidden,
 | ||
|             Selection::DecalA | Selection::DecalB => Visibility::Visible,
 | ||
|         };
 | ||
|     }
 | ||
| }
 |