Files
storyteller-bevy/studio/src/input.rs
T
Danny McGee 57d64f08f0 Touchpad-friendly controls (#2)
Adds `Shift` as an alternate modifier for FPS-style camera rotation, and binds
wheel events to camera zoom/dolly.

An attempt was made to translate 2D scroll events on a touchpad to camera
panning (this is what Blender does, and it's smooth as butter), but Bevy and/or
`winit` and/or the browser is doing some weird preprocessing of those events
before we're able to read them, which makes that unfeasible for now. That issue
has been documented with a `FIXME` comment.
2023-10-06 16:36:38 -04:00

241 lines
7.9 KiB
Rust

use bevy::{
input::mouse::{MouseMotion, MouseWheel},
prelude::*,
utils::HashSet,
};
pub struct StudioInputPlugin;
impl Plugin for StudioInputPlugin {
fn build(&self, app: &mut App) {
app.add_event::<InputAction>();
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<CameraMotion> 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<MouseMotion>,
mut mouse_wheel: EventReader<MouseWheel>,
mouse_buttons: Res<Input<MouseButton>>,
keyboard: Res<Input<KeyCode>>,
mut input: EventWriter<InputAction>,
) {
// 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(),
));
}
// Alt Camera Rotation (designed for laptop)
else if keyboard.pressed(KeyCode::ShiftLeft) {
let lifecycle = if keyboard.just_pressed(KeyCode::ShiftLeft) {
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 keyboard.just_released(KeyCode::ShiftLeft) {
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::<HashSet<_>>();
// 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::<HashSet<_>>();
let wasd_keys_pressed = wasd_keys_set
.intersection(&keys_pressed)
.collect::<HashSet<_>>();
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::<HashSet<_>>();
let wasd_keys_just_pressed = keys_just_pressed
.intersection(&wasd_keys_set)
.collect::<HashSet<_>>();
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(),
));
}
// Wheel zoom
for &MouseWheel { y, .. } in mouse_wheel.iter() {
// info!("wheel: ({x}, {y}) [{unit:?}]");
// let rem_x = (100. / x).abs() % 1.;
// let rem_y = (100. / y).abs() % 1.;
// if (rem_x.is_nan() || rem_x < 1e-5_f32) && (rem_y.is_nan() || rem_y < 1e-5_f32) {
input.send(InputAction(
Lifecycle::Ongoing,
CameraMotion::Zoom(y).into(),
));
// } else {
// FIXME: I had high hopes for this, but the input translation from
// Bevy / winit is herky-jerky-janky af. It seems to be
// trying to force the wheel to only emit one axis at a time,
// unless the user makes a really deliberate diagonal motion
// to start. But even then, it makes bizarre jumps
// intermittently instead of just translating the input
// motion directly.
//
// I Was hoping to achieve something like what Blender does
// with touchpad input to pan, but it doesn't look like
// that's going to be possible without bypassing bevy::input
// and reading the system events directly. We may want to do
// that eventually, but not today.
// input.send(InputAction(
// Lifecycle::Ongoing,
// CameraMotion::Pan(vec2(x, y)).into(),
// ));
// }
}
}
#[inline]
fn read_mouse_delta(mut mouse_motion: EventReader<MouseMotion>) -> Vec2 {
mouse_motion
.into_iter()
.fold(Vec2::ZERO, |accum, current| accum + current.delta)
}