mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
339 lines
11 KiB
Rust
339 lines
11 KiB
Rust
use bevy::{
|
|
input::mouse::{MouseMotion, MouseWheel},
|
|
prelude::*,
|
|
utils::HashSet,
|
|
};
|
|
use space_editor::prelude::space_undo::UndoRedo;
|
|
|
|
use crate::is_editor;
|
|
|
|
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
|
|
pub struct StudioInputSystem;
|
|
|
|
pub struct StudioInputPlugin;
|
|
impl Plugin for StudioInputPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_event::<InputAction>();
|
|
|
|
// Reading each frame of the mouse delta allows
|
|
// to get rid of the bug that delta is counted from the last reading,
|
|
// which leads to camera jumps if you move the mouse between clicks
|
|
app.add_systems(
|
|
PreUpdate,
|
|
read_mouse_delta
|
|
.in_set(StudioInputSystem)
|
|
.after(bevy::input::InputSystem),
|
|
);
|
|
app.init_resource::<LastFrameMouseDelta>();
|
|
|
|
app.add_systems(
|
|
PreUpdate,
|
|
(
|
|
translate_selection_events,
|
|
translate_debug_toggle,
|
|
translate_dcc_camera_events,
|
|
translate_fps_camera_events,
|
|
send_undo_event.run_if(is_editor),
|
|
)
|
|
.in_set(StudioInputSystem)
|
|
.after(bevy::input::InputSystem)
|
|
.after(read_mouse_delta),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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),
|
|
Selection(Selection),
|
|
ToggleDebug,
|
|
}
|
|
|
|
#[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)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum Selection {
|
|
/// Clear the current selection
|
|
Clear,
|
|
/// Focus the camera on the currently selected entity
|
|
Focus,
|
|
}
|
|
impl From<Selection> for Action {
|
|
fn from(value: Selection) -> Self {
|
|
Action::Selection(value)
|
|
}
|
|
}
|
|
|
|
// TODO: Make the bindings of raw device inputs to InputActions configurable
|
|
|
|
fn translate_selection_events(keyboard: Res<Input<KeyCode>>, mut input: EventWriter<InputAction>) {
|
|
if keyboard.just_pressed(KeyCode::Escape) {
|
|
input.send(InputAction(Lifecycle::Start, Selection::Clear.into()));
|
|
} else if keyboard.just_released(KeyCode::Escape) {
|
|
input.send(InputAction(Lifecycle::Stop, Selection::Clear.into()));
|
|
}
|
|
|
|
if keyboard.just_pressed(KeyCode::F) {
|
|
input.send(InputAction(Lifecycle::Start, Selection::Focus.into()));
|
|
} else if keyboard.just_released(KeyCode::F) {
|
|
input.send(InputAction(Lifecycle::Stop, Selection::Focus.into()));
|
|
}
|
|
}
|
|
|
|
fn translate_debug_toggle(keyboard: Res<Input<KeyCode>>, mut input: EventWriter<InputAction>) {
|
|
if keyboard.pressed(KeyCode::ControlLeft) || keyboard.pressed(KeyCode::ControlRight) {
|
|
if keyboard.just_pressed(KeyCode::Grave) {
|
|
input.send(InputAction(Lifecycle::Start, Action::ToggleDebug));
|
|
} else if keyboard.just_released(KeyCode::Grave) {
|
|
input.send(InputAction(Lifecycle::Stop, Action::ToggleDebug));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn translate_dcc_camera_events(
|
|
mouse_motion: Res<LastFrameMouseDelta>,
|
|
mouse_buttons: Res<Input<MouseButton>>,
|
|
keyboard: Res<Input<KeyCode>>,
|
|
mut input: EventWriter<InputAction>,
|
|
) {
|
|
let delta = mouse_motion.get();
|
|
// Camera Pan
|
|
if mouse_buttons.pressed(MouseButton::Middle) {
|
|
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 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
|
|
};
|
|
|
|
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
|
|
};
|
|
|
|
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(),
|
|
));
|
|
}
|
|
}
|
|
|
|
fn translate_fps_camera_events(
|
|
mut mouse_wheel: EventReader<MouseWheel>,
|
|
keyboard: Res<Input<KeyCode>>,
|
|
mut input: EventWriter<InputAction>,
|
|
) {
|
|
// 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.read() {
|
|
// 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(),
|
|
// ));
|
|
// }
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct LastFrameMouseDelta(pub Vec2);
|
|
|
|
impl LastFrameMouseDelta {
|
|
fn get(&self) -> Vec2 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
fn read_mouse_delta(
|
|
mut mouse_motion: EventReader<MouseMotion>,
|
|
mut last_delta: ResMut<LastFrameMouseDelta>,
|
|
) {
|
|
let delta = mouse_motion
|
|
.read()
|
|
.fold(Vec2::ZERO, |accum, current| accum + current.delta);
|
|
last_delta.0 = delta;
|
|
}
|
|
|
|
// TODO: Make it configurable
|
|
fn send_undo_event(mut events: EventWriter<UndoRedo>, keyboard: Res<Input<KeyCode>>) {
|
|
if keyboard.pressed(KeyCode::ControlLeft)
|
|
&& keyboard.just_pressed(KeyCode::Z)
|
|
&& !keyboard.pressed(KeyCode::ShiftLeft)
|
|
{
|
|
events.send(UndoRedo::Undo);
|
|
info!("Undo event sent");
|
|
}
|
|
|
|
if keyboard.pressed(KeyCode::ControlLeft)
|
|
&& keyboard.just_pressed(KeyCode::Z)
|
|
&& keyboard.pressed(KeyCode::ShiftLeft)
|
|
{
|
|
events.send(UndoRedo::Redo);
|
|
info!("Redo event sent");
|
|
}
|
|
}
|