diff --git a/studio/src/input.rs b/studio/src/input.rs new file mode 100644 index 0000000..8f5465f --- /dev/null +++ b/studio/src/input.rs @@ -0,0 +1,184 @@ +use bevy::{input::mouse::MouseMotion, prelude::*, utils::HashSet}; + +pub struct StudioInputPlugin; + +impl Plugin for StudioInputPlugin { + fn build(&self, app: &mut App) { + app.add_event::(); + app.add_systems( + PreUpdate, + translate_bevy_events.after(bevy::input::InputSystem), + ); + } +} + +#[derive(Event, Clone, Copy)] +pub struct InputAction(pub Lifecycle, pub Action); + +#[derive(Clone, Copy)] +pub enum Lifecycle { + Start, + Ongoing, + Stop, +} + +#[derive(Clone, Copy)] +pub enum Action { + Camera(CameraMotion), +} + +#[derive(Clone, Copy)] +pub enum CameraMotion { + Translate(Vec3), + Pan(Vec2), + Zoom(f32), + Orbit(Vec2), + Rotate(Vec2), +} + +impl From for Action { + fn from(value: CameraMotion) -> Self { + Action::Camera(value) + } +} + +// TODO: Make the bindings of raw device inputs to InputActions configurable +fn translate_bevy_events( + mouse_motion: EventReader, + mouse_buttons: Res>, + keyboard: Res>, + mut input: EventWriter, +) { + // Camera Pan + if mouse_buttons.pressed(MouseButton::Middle) { + let delta = read_mouse_delta(mouse_motion); + let lifecycle = if mouse_buttons.just_pressed(MouseButton::Middle) { + Lifecycle::Start + } else { + Lifecycle::Ongoing + }; + input.send(InputAction(lifecycle, CameraMotion::Pan(delta).into())); + } + // Stop Camera Panning + else if mouse_buttons.just_released(MouseButton::Middle) { + input.send(InputAction( + Lifecycle::Stop, + CameraMotion::Pan(Vec2::ZERO).into(), + )); + } + // Camera Orbit/Zoom + else if keyboard.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]) { + if mouse_buttons.any_pressed([MouseButton::Left, MouseButton::Right]) { + let delta = read_mouse_delta(mouse_motion); + let lifecycle = + if mouse_buttons.any_just_pressed([MouseButton::Left, MouseButton::Right]) { + Lifecycle::Start + } else { + Lifecycle::Ongoing + }; + + let motion = if mouse_buttons.pressed(MouseButton::Left) { + CameraMotion::Orbit(delta) + } else { + CameraMotion::Zoom(delta.y) + }; + + input.send(InputAction(lifecycle, motion.into())); + } + // Stop Camera Orbit + else if mouse_buttons.just_released(MouseButton::Left) { + input.send(InputAction( + Lifecycle::Stop, + CameraMotion::Orbit(Vec2::ZERO).into(), + )); + } + // Stop Camera Zoom + else if mouse_buttons.just_released(MouseButton::Right) { + input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into())); + } + } + // Stop Camera Orbit & Zoom + else if (keyboard.just_released(KeyCode::AltLeft) && !keyboard.pressed(KeyCode::AltRight)) + || (keyboard.just_released(KeyCode::AltRight) && !keyboard.pressed(KeyCode::AltLeft)) + { + input.send(InputAction( + Lifecycle::Stop, + CameraMotion::Orbit(Vec2::ZERO).into(), + )); + input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into())); + } + // Camera Rotation + else if mouse_buttons.pressed(MouseButton::Right) { + let lifecycle = if mouse_buttons.just_pressed(MouseButton::Right) { + Lifecycle::Start + } else { + Lifecycle::Ongoing + }; + + let delta = read_mouse_delta(mouse_motion); + input.send(InputAction(lifecycle, CameraMotion::Rotate(delta).into())); + } + // Stop Camera Rotation + else if mouse_buttons.just_released(MouseButton::Right) { + input.send(InputAction( + Lifecycle::Stop, + CameraMotion::Rotate(Vec2::ZERO).into(), + )) + } + + // Camera Translation (FPS-like) + use KeyCode::*; + let wasd_keys = [W, A, S, D, E, Q]; + let wasd_keys_set = wasd_keys.into_iter().collect::>(); + + // A little unfortunate that `get_pressed()` doesn't just return a reference + // to the original HashSet >_< Maybe the compiler will be nice to us and + // optimize away the redundant clone? + let keys_pressed = keyboard.get_pressed().cloned().collect::>(); + let wasd_keys_pressed = wasd_keys_set + .intersection(&keys_pressed) + .collect::>(); + + if !wasd_keys_pressed.is_empty() { + let mut mvmt = Vec3::ZERO; + + #[rustfmt::skip] { + if wasd_keys_pressed.contains(&W) { mvmt.z -= 1.; } + if wasd_keys_pressed.contains(&A) { mvmt.x -= 1.; } + if wasd_keys_pressed.contains(&S) { mvmt.z += 1.; } + if wasd_keys_pressed.contains(&D) { mvmt.x += 1.; } + if wasd_keys_pressed.contains(&E) { mvmt.y += 1.; } + if wasd_keys_pressed.contains(&Q) { mvmt.y -= 1.; } + }; + + let mvmt = mvmt.normalize_or_zero(); + + // Compare WASD keys pressed with WASD keys _just_ pressed to determine lifecycle + let keys_just_pressed = keyboard.get_just_pressed().cloned().collect::>(); + let wasd_keys_just_pressed = keys_just_pressed + .intersection(&wasd_keys_set) + .collect::>(); + + let lifecycle = if wasd_keys_pressed.len() == wasd_keys_just_pressed.len() { + Lifecycle::Start + } else { + Lifecycle::Ongoing + }; + + input.send(InputAction(lifecycle, CameraMotion::Translate(mvmt).into())); + } + // Stop Translation + else if keyboard.any_just_released(wasd_keys) { + input.send(InputAction( + Lifecycle::Stop, + CameraMotion::Translate(Vec3::ZERO).into(), + )); + } +} + +#[inline] +fn read_mouse_delta(mut mouse_motion: EventReader) -> Vec2 { + mouse_motion + .into_iter() + .fold(Vec2::ZERO, |accum, current| accum + current.delta) +} diff --git a/studio/src/lib.rs b/studio/src/lib.rs index eb5cfa3..a832921 100644 --- a/studio/src/lib.rs +++ b/studio/src/lib.rs @@ -1,13 +1,15 @@ #![allow(clippy::type_complexity)] use bevy::prelude::*; -use orbit_cam::{OrbitCam, OrbitCamPlugin}; +use input::StudioInputPlugin; +use orbit_cam::{CameraMotionSystem, OrbitCam, OrbitCamPlugin}; use skybox::SkyboxPlugin; use test_scene::TestScenePlugin; use ui::UiLayerPlugin; use viewport::ViewportSize; use wasm_bindgen::prelude::*; +mod input; mod orbit_cam; mod skybox; mod test_scene; @@ -32,12 +34,13 @@ pub fn init(viewport_size: ViewportSize) { }), ..default() }), + StudioInputPlugin, OrbitCamPlugin, TestScenePlugin, SkyboxPlugin, UiLayerPlugin, )) - .add_systems(Update, draw_world_axes.after(orbit_cam::pan_orbit_camera)) + .add_systems(Update, draw_world_axes.after(CameraMotionSystem)) .run(); } diff --git a/studio/src/orbit_cam.rs b/studio/src/orbit_cam.rs index 2dcef77..530848b 100644 --- a/studio/src/orbit_cam.rs +++ b/studio/src/orbit_cam.rs @@ -1,21 +1,46 @@ -use std::f32::consts; +use std::{f32::consts, mem}; use bevy::{ - input::mouse::{MouseButton, MouseMotion}, math::vec3, prelude::*, window::{CursorGrabMode, PrimaryWindow}, }; +use crate::input::{Action, CameraMotion, InputAction, Lifecycle}; + pub struct OrbitCamPlugin; +#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct CameraMotionSystem; + impl Plugin for OrbitCamPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, spawn_camera) - .add_systems(Update, pan_orbit_camera); + app.add_systems(Startup, spawn_camera); + app.add_systems( + Update, + ( + camera_control_kinematic.in_set(CameraMotionSystem), + update_velocity + .in_set(CameraMotionSystem) + .after(camera_control_kinematic), + camera_control_physical + .in_set(CameraMotionSystem) + .after(update_velocity), + ), + ); } } +// TODO: These don't really belong in this module +#[derive(Component)] +pub struct Velocity(pub Vec3); + +#[derive(Component)] +pub struct TargetVelocity(pub Vec3); + +#[derive(Component)] +pub struct Interp(pub f32); + #[derive(Component)] pub struct OrbitCam { pub focus: Vec3, @@ -23,13 +48,6 @@ pub struct OrbitCam { pub upside_down: bool, } -impl OrbitCam { - // TODO: Make these configurable - const ORBIT_BTN: MouseButton = MouseButton::Left; - const PAN_BTN: MouseButton = MouseButton::Middle; - const ZOOM_BTN: MouseButton = MouseButton::Right; -} - impl Default for OrbitCam { fn default() -> Self { Self { @@ -40,12 +58,6 @@ impl Default for OrbitCam { } } -enum ControlMode { - Orbit, - Pan, - Zoom, -} - fn spawn_camera(mut cmd: Commands) { let focus = vec3(0., 1.5, 0.); let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y); @@ -61,99 +73,144 @@ fn spawn_camera(mut cmd: Commands) { }, OrbitCam { focus, - radius: camera_xform.translation.length(), + radius: Vec3::distance(camera_xform.translation, focus), ..default() }, + Velocity(Vec3::ZERO), + TargetVelocity(Vec3::ZERO), + Interp(1.), )); } -pub fn pan_orbit_camera( - mut window: Query<&mut Window, With>, - mut mouse_motion: EventReader, - mouse_buttons: Res>, - keyboard: Res>, - mut camera: Query<(&mut OrbitCam, &mut Transform, &Projection)>, +fn camera_control_kinematic( + mut q_window: Query<&mut Window, With>, + mut input: EventReader, + mut q_camera: Query<( + &mut OrbitCam, + &mut TargetVelocity, + &mut Interp, + &mut Transform, + &Projection, + )>, ) { - if (keyboard.just_released(KeyCode::AltLeft) && !keyboard.pressed(KeyCode::AltRight)) - || (keyboard.just_released(KeyCode::AltRight) && !keyboard.pressed(KeyCode::AltLeft)) - || mouse_buttons.just_released(OrbitCam::ORBIT_BTN) - || mouse_buttons.just_released(OrbitCam::PAN_BTN) - || mouse_buttons.just_released(OrbitCam::ZOOM_BTN) - { - let mut window = window.single_mut(); - window.cursor.visible = true; - window.cursor.grab_mode = CursorGrabMode::None; - } - - if !(keyboard.pressed(KeyCode::AltLeft) - || keyboard.pressed(KeyCode::AltRight) - || mouse_buttons.pressed(OrbitCam::PAN_BTN)) - { + if input.is_empty() { return; } - let (mut orbit, mut xform, proj) = camera.single_mut(); + let (mut orbit_cam, mut target_vel, mut t, mut xform, proj) = q_camera.single_mut(); + let mut window = q_window.single_mut(); - if mouse_buttons.just_pressed(OrbitCam::ORBIT_BTN) - || mouse_buttons.just_released(OrbitCam::ORBIT_BTN) - { - orbit.upside_down = xform.up().y <= 0.0; + let mut any_ongoing = false; + let mut any_started = false; + let mut any_stopped = false; + + for action in input.iter() { + #[allow(irrefutable_let_patterns)] // Will eventually be more variants + if let InputAction(lifecycle, Action::Camera(motion)) = action { + use CameraMotion::*; + + // Ignore translation actions for the sake of pointer capture + if mem::discriminant(motion) != mem::discriminant(&Translate(Vec3::ZERO)) { + match lifecycle { + Lifecycle::Start => any_started = true, + Lifecycle::Ongoing => any_ongoing = true, + Lifecycle::Stop => any_stopped = true, + } + } + + match motion { + Translate(delta) => do_translate(*delta, &mut target_vel, &mut t), + Pan(delta) => do_pan(proj, &window, *delta, &mut xform, &mut orbit_cam), + Zoom(delta) => do_zoom(&window, *delta, &mut xform, &mut orbit_cam), + Orbit(delta) => do_orbit(&window, *delta, *lifecycle, &mut xform, &mut orbit_cam), + Rotate(delta) => do_rotate(&window, *delta, &mut xform, &mut orbit_cam), + } + } } - let control_mode = if mouse_buttons.pressed(OrbitCam::ORBIT_BTN) { - ControlMode::Orbit - } else if mouse_buttons.pressed(OrbitCam::ZOOM_BTN) { - ControlMode::Zoom - } else if mouse_buttons.pressed(OrbitCam::PAN_BTN) { - ControlMode::Pan - } else { - return; - }; + if any_started { + capture_pointer(&mut window); + } else if any_stopped && !any_ongoing { + release_pointer(&mut window); + } +} - let mouse_delta = mouse_motion - .into_iter() - .fold(Vec2::ZERO, |accum, current| accum + current.delta); +// TODO: This doesn't really belong in this module +fn update_velocity( + time: Res