use bevy_app::prelude::*;
use bevy_camera::prelude::*;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_input::{
mouse::{MouseScrollUnit, MouseWheel},
prelude::*,
};
use bevy_math::{prelude::*, DVec2, DVec3};
use bevy_platform::collections::HashMap;
use bevy_reflect::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
use bevy_picking::pointer::{
PointerAction, PointerId, PointerInput, PointerInteraction, PointerLocation, PointerMap,
};
use crate::prelude::{component::EditorCam, inputs::MotionInputs};
#[derive(Debug, Clone, Copy, Reflect, PartialEq, Eq)]
pub enum MotionKind {
OrbitZoom,
PanZoom,
Zoom,
}
impl From<&MotionInputs> for MotionKind {
fn from(value: &MotionInputs) -> Self {
match value {
MotionInputs::OrbitZoom { .. } => MotionKind::OrbitZoom,
MotionInputs::PanZoom { .. } => MotionKind::PanZoom,
MotionInputs::Zoom { .. } => MotionKind::Zoom,
}
}
}
pub struct DefaultInputPlugin;
impl Plugin for DefaultInputPlugin {
fn build(&self, app: &mut App) {
app.add_message::<crate::input::EditorCamInputMessage>()
.init_resource::<crate::input::CameraPointerMap>()
.add_systems(
PreUpdate,
(
default_camera_inputs,
EditorCamInputMessage::receive_messages,
EditorCamInputMessage::send_pointer_inputs,
)
.chain()
.after(bevy_picking::PickingSystems::Last)
.before(crate::controller::component::EditorCam::update_camera_positions),
);
}
}
pub fn default_camera_inputs(
pointers: Query<(&PointerId, &PointerLocation)>,
pointer_map: Res<CameraPointerMap>,
mut controller: MessageWriter<EditorCamInputMessage>,
mut mouse_wheel: MessageReader<MouseWheel>,
mouse_input: Res<ButtonInput<MouseButton>>,
cameras: Query<(Entity, &Camera, &EditorCam)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
let orbit_start = MouseButton::Right;
let pan_start = MouseButton::Left;
let zoom_stop = 0.0;
if let Some(&camera) = pointer_map.get(&PointerId::Mouse) {
let camera_query = cameras.get(camera).ok();
let is_in_zoom_mode = camera_query
.map(|(.., editor_cam)| editor_cam.current_motion.is_zooming_only())
.unwrap_or_default();
let zoom_amount_abs = camera_query
.and_then(|(.., editor_cam)| {
editor_cam
.current_motion
.inputs()
.map(|inputs| inputs.zoom_velocity_abs(editor_cam.smoothing.zoom.mul_f32(2.0)))
})
.unwrap_or(0.0);
let should_zoom_end = is_in_zoom_mode && zoom_amount_abs <= zoom_stop;
if mouse_input.any_just_released([orbit_start, pan_start]) || should_zoom_end {
controller.write(EditorCamInputMessage::End { camera });
}
}
for (&pointer, pointer_location) in pointers
.iter()
.filter_map(|(id, loc)| loc.location().map(|loc| (id, loc)))
{
match pointer {
PointerId::Mouse => {
let Some((camera, ..)) = cameras.iter().find(|(_, camera, _)| {
pointer_location.is_in_viewport(camera, &primary_window)
}) else {
continue; };
if mouse_input.just_pressed(orbit_start) {
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::OrbitZoom,
camera,
pointer,
});
} else if mouse_input.just_pressed(pan_start) {
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::PanZoom,
camera,
pointer,
});
} else if mouse_wheel.read().map(|mw| mw.y.abs()).sum::<f32>() > 0.0 {
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::Zoom,
camera,
pointer,
});
}
}
PointerId::Touch(_) => continue,
PointerId::Custom(_) => continue,
}
}
mouse_wheel.clear();
}
#[derive(Debug, Clone, Default, Deref, DerefMut, Reflect, Resource)]
pub struct CameraPointerMap(HashMap<PointerId, Entity>);
#[derive(Debug, Clone, Reflect, Message)]
pub enum EditorCamInputMessage {
Start {
kind: MotionKind,
camera: Entity,
pointer: PointerId,
},
End {
camera: Entity,
},
}
impl EditorCamInputMessage {
pub fn camera(&self) -> Entity {
match self {
EditorCamInputMessage::Start { camera, .. } => *camera,
EditorCamInputMessage::End { camera } => *camera,
}
}
pub fn receive_messages(
mut events: MessageReader<Self>,
mut controllers: Query<(&mut EditorCam, &GlobalTransform)>,
mut camera_map: ResMut<CameraPointerMap>,
pointer_map: Res<PointerMap>,
pointer_interactions: Query<&PointerInteraction>,
pointer_locations: Query<&PointerLocation>,
cameras: Query<(&Camera, &Projection)>,
) {
for event in events.read() {
let Ok((mut controller, cam_transform)) = controllers.get_mut(event.camera()) else {
continue;
};
match event {
EditorCamInputMessage::Start { kind, pointer, .. } => {
if controller.is_actively_controlled() {
continue;
}
let anchor = pointer_map
.get_entity(*pointer)
.and_then(|entity| pointer_interactions.get(entity).ok())
.and_then(|interaction| interaction.get_nearest_hit())
.and_then(|(_, hit)| hit.position)
.map(|world_space_hit| {
cam_transform
.to_matrix()
.as_dmat4()
.inverse()
.transform_point3(world_space_hit.into())
})
.filter(|p| {
#[cfg(debug_assertions)]
if !p.is_finite() {
bevy_log::warn!("Non-finite input fed to camera controller: {p:?}")
}
p.is_finite()
})
.or_else(|| {
let camera = cameras.get(event.camera()).ok();
let pointer_location = pointer_map
.get_entity(*pointer)
.and_then(|entity| pointer_locations.get(entity).ok())
.and_then(|l| l.location());
if let Some(((camera, proj), pointer_location)) =
camera.zip(pointer_location)
{
screen_to_view_space(
camera,
proj,
&controller,
pointer_location.position,
)
} else {
None
}
})
.filter(|p| p.is_finite());
match kind {
MotionKind::OrbitZoom => controller.start_orbit(anchor),
MotionKind::PanZoom => controller.start_pan(anchor),
MotionKind::Zoom => controller.start_zoom(anchor),
}
camera_map.insert(*pointer, event.camera());
}
EditorCamInputMessage::End { .. } => {
controller.end_move();
if let Some(pointer) = camera_map
.iter()
.find(|(.., &camera)| camera == event.camera())
.map(|(&pointer, ..)| pointer)
{
camera_map.remove(&pointer);
}
}
}
}
}
pub fn send_pointer_inputs(
camera_map: Res<CameraPointerMap>,
mut camera_controllers: Query<&mut EditorCam>,
mut mouse_wheel: MessageReader<MouseWheel>,
mut moves: MessageReader<PointerInput>,
) {
let moves_list: Vec<_> = moves.read().collect();
for (pointer, camera) in camera_map.iter() {
let Ok(mut camera_controller) = camera_controllers.get_mut(*camera) else {
continue;
};
let screenspace_input = moves_list
.iter()
.filter(|m| m.pointer_id.eq(pointer))
.filter_map(|m| match m.action {
PointerAction::Move { delta } => Some(delta),
PointerAction::Press { .. } => None,
PointerAction::Cancel => None,
_ => None,
})
.sum();
let zoom_amount = match pointer {
PointerId::Mouse => mouse_wheel
.read()
.map(|mw| {
let scroll_multiplier = match mw.unit {
MouseScrollUnit::Line => 150.0,
MouseScrollUnit::Pixel => 1.0,
};
mw.y * scroll_multiplier
})
.sum::<f32>(),
_ => 0.0,
};
camera_controller.send_screenspace_input(screenspace_input);
camera_controller.send_zoom_input(zoom_amount);
}
mouse_wheel.clear();
}
}
fn screen_to_view_space(
camera: &Camera,
proj: &Projection,
controller: &EditorCam,
target_position: Vec2,
) -> Option<DVec3> {
let mut viewport_position = if let Some(rect) = camera.logical_viewport_rect() {
target_position.as_dvec2() - rect.min.as_dvec2()
} else {
target_position.as_dvec2()
};
let target_size = camera.logical_viewport_size()?.as_dvec2();
viewport_position.y = target_size.y - viewport_position.y;
let ndc = viewport_position * 2. / target_size - DVec2::ONE;
let ndc_to_view = proj.get_clip_from_view().as_dmat4().inverse();
let view_near_plane = ndc_to_view.project_point3(ndc.extend(1.));
match &proj {
Projection::Perspective(_) | Projection::Custom(_) => {
let view_far_plane = ndc_to_view.project_point3(ndc.extend(f64::EPSILON));
let direction = (view_far_plane - view_near_plane).normalize();
Some((direction / direction.z) * controller.last_anchor_depth())
}
Projection::Orthographic(_) => Some(DVec3::new(
view_near_plane.x,
view_near_plane.y,
controller.last_anchor_depth(),
)),
}
}