From 00176b8169385db3433924724c6d4d6ecb868b3c Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Tue, 21 May 2024 23:42:25 +0800 Subject: [PATCH 01/32] Refactor camera_control to distinguish camera state from user input state Signed-off-by: Reuben Thomas --- .../mod.rs} | 79 ++++---------- .../src/interaction/camera_controls/mouse.rs | 102 ++++++++++++++++++ 2 files changed, 121 insertions(+), 60 deletions(-) rename rmf_site_editor/src/interaction/{camera_controls.rs => camera_controls/mod.rs} (89%) create mode 100644 rmf_site_editor/src/interaction/camera_controls/mouse.rs diff --git a/rmf_site_editor/src/interaction/camera_controls.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs similarity index 89% rename from rmf_site_editor/src/interaction/camera_controls.rs rename to rmf_site_editor/src/interaction/camera_controls/mod.rs index 7562a50c..d3877fa0 100644 --- a/rmf_site_editor/src/interaction/camera_controls.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -29,6 +29,9 @@ use bevy::{ window::PrimaryWindow, }; +mod mouse; +use mouse::{MouseCommand, update_mouse_command}; + /// RenderLayers are used to inform cameras which entities they should render. /// The General render layer is for things that should be visible to all /// cameras. @@ -55,18 +58,6 @@ pub const XRAY_RENDER_LAYER: u8 = 5; /// models in the engine without having them being visible to general cameras pub const MODEL_PREVIEW_LAYER: u8 = 6; -#[derive(Resource)] -struct MouseLocation { - previous: Vec2, -} - -impl Default for MouseLocation { - fn default() -> Self { - MouseLocation { - previous: Vec2::ZERO, - } - } -} #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] pub enum ProjectionMode { Perspective, @@ -382,11 +373,7 @@ impl FromWorld for CameraControls { fn camera_controls( primary_windows: Query<&Window, With>, - mut ev_cursor_moved: EventReader, - mut ev_scroll: EventReader, - input_mouse: Res>, - input_keyboard: Res>, - mut previous_mouse_location: ResMut, + mouse_command: ResMut, mut controls: ResMut, mut cameras: Query<(&mut Projection, &mut Transform)>, mut bevy_cameras: Query<&mut Camera>, @@ -413,43 +400,8 @@ fn camera_controls( return; } - let is_shifting = - input_keyboard.pressed(KeyCode::ShiftLeft) || input_keyboard.pressed(KeyCode::ShiftRight); - let is_panning = input_mouse.pressed(MouseButton::Right) && !is_shifting; - - let is_orbiting = input_mouse.pressed(MouseButton::Middle) - || (input_mouse.pressed(MouseButton::Right) && is_shifting); - let started_orbiting = !controls.was_oribiting && is_orbiting; - let released_orbiting = controls.was_oribiting && !is_orbiting; - controls.was_oribiting = is_orbiting; - - // spin through all mouse cursor-moved events to find the last one - let mut last_pos = previous_mouse_location.previous; - if let Some(ev) = ev_cursor_moved.read().last() { - last_pos.x = ev.position.x; - last_pos.y = ev.position.y; - } - - let mut cursor_motion = Vec2::ZERO; - if is_panning || is_orbiting { - cursor_motion.x = last_pos.x - previous_mouse_location.previous.x; - cursor_motion.y = last_pos.y - previous_mouse_location.previous.y; - } - - previous_mouse_location.previous = last_pos; - - let mut scroll = 0.0; - for ev in ev_scroll.read() { - #[cfg(not(target_arch = "wasm32"))] - { - scroll += ev.y; - } - #[cfg(target_arch = "wasm32")] - { - // scrolling in wasm is a different beast - scroll += 0.4 * ev.y / ev.y.abs(); - } - } + let mut cursor_motion = mouse_command.pan; + let scroll = mouse_command.zoom_delta; if controls.mode() == ProjectionMode::Orthographic { let (mut ortho_proj, mut ortho_transform) = cameras @@ -481,7 +433,9 @@ fn camera_controls( for (mut child_proj, _) in children { *child_proj = proj.clone(); } - } else { + } + + if controls.mode() == ProjectionMode::Perspective { // perspective mode let (mut persp_proj, mut persp_transform) = cameras .get_mut(controls.perspective_camera_entities[0]) @@ -489,14 +443,15 @@ fn camera_controls( if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { let mut changed = false; - if started_orbiting || released_orbiting { + if mouse_command.orbit_state_changed { // only check for upside down when orbiting started or ended this frame // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct let up = persp_transform.rotation * Vec3::Z; controls.orbit_upside_down = up.z <= 0.0; } - if is_orbiting && cursor_motion.length_squared() > 0. { + if mouse_command.orbit.length_squared() > mouse_command.pan.length_squared() { + cursor_motion = mouse_command.orbit; changed = true; if let Ok(window) = primary_windows.get_single() { let window_size = Vec2::new(window.width() as f32, window.height() as f32); @@ -515,7 +470,8 @@ fn camera_controls( persp_transform.rotation = persp_transform.rotation * pitch; // local x } - } else if is_panning && cursor_motion.length_squared() > 0. { + } else if mouse_command.pan.length_squared() > 0. { + cursor_motion = mouse_command.pan; changed = true; // make panning distance independent of resolution and FOV, if let Ok(window) = primary_windows.get_single() { @@ -549,6 +505,8 @@ fn camera_controls( + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, controls.orbit_radius)); } } + + } } @@ -556,10 +514,11 @@ pub struct CameraControlsPlugin; impl Plugin for CameraControlsPlugin { fn build(&self, app: &mut App) { - app.insert_resource(MouseLocation::default()) - .init_resource::() + app.init_resource::() + .init_resource::() .init_resource::() .add_event::() + .add_systems(Update, update_mouse_command) .add_systems(Update, camera_controls); } } diff --git a/rmf_site_editor/src/interaction/camera_controls/mouse.rs b/rmf_site_editor/src/interaction/camera_controls/mouse.rs new file mode 100644 index 00000000..b62af999 --- /dev/null +++ b/rmf_site_editor/src/interaction/camera_controls/mouse.rs @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::prelude::*; +use bevy::input::mouse::{self, MouseMotion, MouseScrollUnit, MouseWheel}; +use crate::interaction::PickingBlockers; + +#[derive(Resource)] +pub struct MouseCommand { + pub pan: Vec2, + pub orbit: Vec2, + pub orbit_state_changed: bool, + pub was_orbitting: bool, + pub zoom_target: Vec3, + pub zoom_delta: f32, +} + +impl Default for MouseCommand { + fn default() -> Self { + Self { + pan: Vec2::ZERO, + orbit: Vec2::ZERO, + orbit_state_changed: false, + was_orbitting: false, + zoom_target: Vec3::ZERO, + zoom_delta: 0.0 + } + } +} + +pub fn update_mouse_command( + mut mouse_command: ResMut, + mut mouse_motion: EventReader, + mut mouse_wheel: EventReader, + mouse_input: Res>, + keyboard_input: Res>, +) { + // Keyboard, mouse inputs + let is_shifting = + keyboard_input.pressed(KeyCode::ShiftLeft) || + keyboard_input.pressed(KeyCode::ShiftRight); + let cursor_motion = mouse_motion.read().map(|event| event.delta) + .fold(Vec2::ZERO, |acc, delta| acc + delta); + let mut scroll_motion = 0.0; + for ev in mouse_wheel.read() { + #[cfg(not(target_arch = "wasm32"))] + { + scroll_motion += ev.y; + } + #[cfg(target_arch = "wasm32")] + { + // scrolling in wasm is a different beast + scroll_motion += 0.4 * ev.y / ev.y.abs(); + } + } + + // Pan + let is_panning = mouse_input.pressed(MouseButton::Right) && !is_shifting; + if is_panning { + mouse_command.pan = cursor_motion; + } else { + mouse_command.pan = Vec2::ZERO; + } + + // Orbit + let is_orbitting = mouse_input.pressed(MouseButton::Middle) || + (mouse_input.pressed(MouseButton::Right) && is_shifting); + mouse_command.orbit_state_changed = mouse_command.was_orbitting != is_orbitting; + mouse_command.was_orbitting = is_orbitting; + if is_orbitting && !is_panning { + mouse_command.orbit = cursor_motion; + } else { + mouse_command.orbit = Vec2::ZERO; + } + + // Zoom + mouse_command.zoom_delta = scroll_motion; + if !is_orbitting && !is_panning { + mouse_command.zoom_target = Vec3::new(0.0, 0.0, scroll_motion); + } else { + mouse_command.zoom_target = Vec3::ZERO; + } +} + + + + + From fd6f4faeecf15aa3bea7f27c5bec4a86ee7c7c38 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 27 May 2024 17:32:52 +0800 Subject: [PATCH 02/32] Selection based pan, zoom, orbit for perspective camera Signed-off-by: Reuben Thomas --- Cargo.lock | 5 +- rmf_site_editor/Cargo.toml | 1 + .../src/interaction/camera_controls/cursor.rs | 257 ++++++++++++++++++ .../src/interaction/camera_controls/mod.rs | 133 ++------- .../src/interaction/camera_controls/mouse.rs | 102 ------- 5 files changed, 289 insertions(+), 209 deletions(-) create mode 100644 rmf_site_editor/src/interaction/camera_controls/cursor.rs delete mode 100644 rmf_site_editor/src/interaction/camera_controls/mouse.rs diff --git a/Cargo.lock b/Cargo.lock index 4338079e..4c9847d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3237,9 +3237,9 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4541eb06dce09c0241ebbaab7102f0a01a0c8994afed2e5d0d66775016e25ac2" +checksum = "3ea4908d4f23254adda3daa60ffef0f1ac7b8c3e9a864cf3cc154b251908a2ef" dependencies = [ "approx", "matrixmultiply", @@ -4112,6 +4112,7 @@ dependencies = [ "geo", "gz-fuel", "itertools 0.12.1", + "nalgebra", "pathdiff", "rfd", "rmf_site_format", diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index ce8112d4..51656c27 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -49,6 +49,7 @@ gz-fuel = { git = "https://github.com/open-rmf/gz-fuel-rs", branch = "luca/ehttp pathdiff = "*" tera = "1.19.1" ehttp = { version = "0.4", features = ["native-async"] } +nalgebra = "0.32.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] clap = { version = "4.0.10", features = ["color", "derive", "help", "usage", "suggestions"] } diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs new file mode 100644 index 00000000..9f8568e5 --- /dev/null +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::render::camera; +use nalgebra::{Matrix3, Matrix3x1}; +use bevy::prelude::*; +use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::window::{PrimaryWindow}; +use bevy_mod_raycast::{deferred::RaycastSource}; +use super::{CameraCommandType, CameraControls, ProjectionMode}; +use crate::interaction::{PickingBlockers, SiteRaycastSet}; + +#[derive(Resource)] +pub struct CursorCommand { + pub target_translation: Vec3, + pub target_rotation: Quat, + pub command_type: CameraCommandType, +} + +impl Default for CursorCommand { + fn default() -> Self { + Self { + target_translation: Vec3::ZERO, + target_rotation: Quat::IDENTITY, + command_type: CameraCommandType::Inactive, + } + } +} + +pub fn update_cursor_command( + camera_controls: Res, + mut cursor_command: ResMut, + mut mouse_motion: EventReader, + mut mouse_wheel: EventReader, + mouse_input: Res>, + picking_blockers: Res, + keyboard_input: Res>, + raycast_sources: Query<&RaycastSource>, + bevy_cameras: Query<&Camera>, + cameras: Query<(&Projection, &Transform, &GlobalTransform)>, + primary_windows: Query<&Window, With>, +) { + if let Ok(window) = primary_windows.get_single() { + + // Cursor inputs + let cursor_position_now = match window.cursor_position() { + Some(pos) => pos, + None => { + *cursor_command = CursorCommand::default(); + return; + } , + }; + let mut cursor_motion = mouse_motion.read().map(|event| event.delta) + .fold(Vec2::ZERO, |acc, delta| acc + delta); + let cursor_position_prev = cursor_position_now - cursor_motion; + + // Scroll inputs + let mut scroll_motion = 0.0; + for ev in mouse_wheel.read() { + #[cfg(not(target_arch = "wasm32"))] + { + scroll_motion += ev.y; + } + #[cfg(target_arch = "wasm32")] + { + // scrolling in wasm is a different beast + scroll_motion += 0.4 * ev.y / ev.y.abs(); + } + } + + // Command type, return if inactive + let command_type = get_command_type( + &keyboard_input, &mouse_input, + &cursor_motion, &scroll_motion, + &picking_blockers + ); + if command_type == CameraCommandType::Inactive { + *cursor_command = CursorCommand::default(); + return; + } + + // Camera projection and transform + let active_camera_entity = match camera_controls.mode() { + ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], + ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], + }; + let (camera_proj, camera_transform, camera_global_transform) = cameras.get(active_camera_entity).unwrap(); + + // Get selection under cursor, cursor direction + let Ok(cursor_raycast_source) = raycast_sources.get_single() else { + return; + }; + let cursor_ray = match cursor_raycast_source.get_ray() { + Some(ray) => ray, + None => return, + }; + let cursor_selected_point = get_cursor_selected_point(&cursor_raycast_source); + let cursor_direction_now = cursor_ray.direction().normalize(); + let cursor_direction_prev = bevy_cameras + .get(active_camera_entity) + .unwrap() + .viewport_to_world(camera_global_transform, cursor_position_prev) + .unwrap() + .direction + .normalize(); + + // 4. Perspective Mode + *cursor_command = match camera_controls.mode() { + ProjectionMode::Perspective => { + get_perspective_cursor_command( + *camera_transform, + command_type, + cursor_direction_prev, + cursor_direction_now, + cursor_selected_point, + scroll_motion + ) + }, + ProjectionMode::Orthographic => { + get_orthographic_cursor_command() + }, + }; + + } else { + *cursor_command = CursorCommand::default(); + } +} + +fn get_orthographic_cursor_command() -> CursorCommand { + CursorCommand::default() +} + +fn get_perspective_cursor_command( + camera_transform: Transform, + command_type: CameraCommandType, + cursor_direction_prev: Vec3, + cursor_direction_now: Vec3, + cursor_selected_point: Vec3, + scroll_motion: f32 +) -> CursorCommand { + // Zoom towards the cursor if zooming only, otherwize zoom to center + let zoom_translation = match command_type { + CameraCommandType::ZoomOnly => cursor_direction_now * 0.5 * scroll_motion, + _ => camera_transform.forward() * scroll_motion + }; + + let mut target_translation = Vec3::ZERO; + let mut target_rotation = Quat::IDENTITY; + + match command_type { + CameraCommandType::ZoomOnly => { + target_translation = zoom_translation; + target_rotation = Quat::IDENTITY; + }, + CameraCommandType::Pan => { + // translation = x1 * right_ transltion + x2 * up_translation + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = camera_transform.rotation * Vec3::Y; + + let camera_to_selection = cursor_selected_point - camera_transform.translation; + + // Solving as a linear system + let a = Matrix3::new( + right_translation.x, up_translation.x, -cursor_direction_prev.x, + right_translation.y, up_translation.y, -cursor_direction_prev.y, + right_translation.z, up_translation.z, -cursor_direction_prev.z, + ); + let b = Matrix3x1::new( + camera_to_selection.x, + camera_to_selection.y, + camera_to_selection.z, + ); + let x = a.lu().solve(&b).unwrap(); + + target_translation = zoom_translation + -x[0] * right_translation + -x[1] * up_translation; + target_rotation = Quat::IDENTITY; + }, + CameraCommandType::Orbit => { + } + _ => () + } + + return CursorCommand { + target_translation, + target_rotation, + command_type, + }; +} + +fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { + let cursor_ray = cursor_raycast_source.get_ray().unwrap(); + match cursor_raycast_source.get_nearest_intersection() { + Some((_, intersection)) => intersection.position(), + None => { + let n_p = Vec3::Z; + let n_r = cursor_ray.direction(); + let denom = n_p.dot(n_r); + //TODO(reuben-thomas) default to arbitrary point if ground plane not available + if denom > 1e-3 { + println!("Cursor ray is parallel to the camera"); + Vec3::ZERO + } else { + let t = (Vec3::Z - cursor_ray.origin()).dot(n_p) / denom; + cursor_ray.origin() + t * cursor_ray.direction() + } + } + } +} + +fn get_command_type( + keyboard_input: &Res>, + mouse_input: &Res>, + cursor_motion: &Vec2, + scroll_motion: &f32, + picking_blockers: &Res, +) -> CameraCommandType { + // Inputs + let is_cursor_moving = cursor_motion.length() > 0.; + let is_scrolling = *scroll_motion != 0.; + let is_shifting = + keyboard_input.pressed(KeyCode::ShiftLeft) || + keyboard_input.pressed(KeyCode::ShiftRight); + + // Panning + if is_cursor_moving && !is_shifting && + mouse_input.pressed(MouseButton::Right) { + return CameraCommandType::Pan + } + + // Orbitting + if is_cursor_moving && + mouse_input.pressed(MouseButton::Middle) || + (mouse_input.pressed(MouseButton::Right) && is_shifting) { + return CameraCommandType::Orbit + } + + // Zoom Only + if is_scrolling { + return CameraCommandType::ZoomOnly + } + + return CameraCommandType::Inactive +} \ No newline at end of file diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index d3877fa0..d1983dba 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -29,8 +29,8 @@ use bevy::{ window::PrimaryWindow, }; -mod mouse; -use mouse::{MouseCommand, update_mouse_command}; +mod cursor; +use cursor::{CursorCommand, update_cursor_command}; /// RenderLayers are used to inform cameras which entities they should render. /// The General render layer is for things that should be visible to all @@ -58,6 +58,21 @@ pub const XRAY_RENDER_LAYER: u8 = 5; /// models in the engine without having them being visible to general cameras pub const MODEL_PREVIEW_LAYER: u8 = 6; + +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum CameraCommandType { + Inactive, + Pan, + Orbit, + ZoomOnly +} + +pub struct CameraCommand { + pub delta_translation: Vec3, + pub delta_rotation: Quat, + pub command_type: CameraCommandType +} + #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] pub enum ProjectionMode { Perspective, @@ -372,8 +387,7 @@ impl FromWorld for CameraControls { } fn camera_controls( - primary_windows: Query<&Window, With>, - mouse_command: ResMut, + cursor_command: ResMut, mut controls: ResMut, mut cameras: Query<(&mut Projection, &mut Transform)>, mut bevy_cameras: Query<&mut Camera>, @@ -400,114 +414,23 @@ fn camera_controls( return; } - let mut cursor_motion = mouse_command.pan; - let scroll = mouse_command.zoom_delta; - - if controls.mode() == ProjectionMode::Orthographic { - let (mut ortho_proj, mut ortho_transform) = cameras - .get_mut(controls.orthographic_camera_entities[0]) - .unwrap(); - if let Projection::Orthographic(ortho_proj) = ortho_proj.as_mut() { - if let Ok(window) = primary_windows.get_single() { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let aspect_ratio = window_size[0] / window_size[1]; - - if cursor_motion.length_squared() > 0.0 { - cursor_motion *= 2. / window_size - * Vec2::new(ortho_proj.scale * aspect_ratio, ortho_proj.scale); - let right = -cursor_motion.x * Vec3::X; - let up = cursor_motion.y * Vec3::Y; - ortho_transform.translation += right + up; - } - if scroll.abs() > 0.0 { - ortho_proj.scale -= scroll * ortho_proj.scale * 0.1; - ortho_proj.scale = f32::max(ortho_proj.scale, 0.02); - } - } - } - - let proj = ortho_proj.clone(); - let children = cameras - .get_many_mut(controls.orthographic_camera_entities) - .unwrap(); - for (mut child_proj, _) in children { - *child_proj = proj.clone(); - } - } - if controls.mode() == ProjectionMode::Perspective { - // perspective mode let (mut persp_proj, mut persp_transform) = cameras .get_mut(controls.perspective_camera_entities[0]) .unwrap(); if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { - let mut changed = false; - - if mouse_command.orbit_state_changed { - // only check for upside down when orbiting started or ended this frame - // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct - let up = persp_transform.rotation * Vec3::Z; - controls.orbit_upside_down = up.z <= 0.0; - } - - if mouse_command.orbit.length_squared() > mouse_command.pan.length_squared() { - cursor_motion = mouse_command.orbit; - changed = true; - if let Ok(window) = primary_windows.get_single() { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let delta_x = { - let delta = cursor_motion.x / window_size.x * std::f32::consts::PI * 2.0; - if controls.orbit_upside_down { - -delta - } else { - delta - } - }; - let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI; - let yaw = Quat::from_rotation_z(-delta_x); - let pitch = Quat::from_rotation_x(-delta_y); - persp_transform.rotation = yaw * persp_transform.rotation; // global y - persp_transform.rotation = persp_transform.rotation * pitch; - // local x - } - } else if mouse_command.pan.length_squared() > 0. { - cursor_motion = mouse_command.pan; - changed = true; - // make panning distance independent of resolution and FOV, - if let Ok(window) = primary_windows.get_single() { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - - cursor_motion *= - Vec2::new(persp_proj.fov * persp_proj.aspect_ratio, persp_proj.fov) - / window_size; - // translate by local axes - let right = persp_transform.rotation * Vec3::X * -cursor_motion.x; - let up = persp_transform.rotation * Vec3::Y * cursor_motion.y; - // make panning proportional to distance away from center point - let translation = (right + up) * controls.orbit_radius; - controls.orbit_center += translation; - } - } - - if scroll.abs() > 0.0 { - changed = true; - controls.orbit_radius -= scroll * controls.orbit_radius * 0.2; - // dont allow zoom to reach zero or you get stuck - controls.orbit_radius = f32::max(controls.orbit_radius, 0.05); - } - - if changed { - // emulating parent/child to make the yaw/y-axis rotation behave like a turntable - // parent = x and y rotation - // child = z-offset - let rot_matrix = Mat3::from_quat(persp_transform.rotation); - persp_transform.translation = controls.orbit_center - + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, controls.orbit_radius)); - } + let new_translation = persp_transform.translation + cursor_command.target_translation; + let new_rotation = persp_transform.rotation * cursor_command.target_rotation; + persp_transform.translation = new_translation; + persp_transform.rotation = new_rotation; } } + + if controls.mode() == ProjectionMode::Orthographic { + return; + } } pub struct CameraControlsPlugin; @@ -515,10 +438,10 @@ pub struct CameraControlsPlugin; impl Plugin for CameraControlsPlugin { fn build(&self, app: &mut App) { app.init_resource::() - .init_resource::() + .init_resource::() .init_resource::() .add_event::() - .add_systems(Update, update_mouse_command) + .add_systems(Update, update_cursor_command) .add_systems(Update, camera_controls); } } diff --git a/rmf_site_editor/src/interaction/camera_controls/mouse.rs b/rmf_site_editor/src/interaction/camera_controls/mouse.rs deleted file mode 100644 index b62af999..00000000 --- a/rmf_site_editor/src/interaction/camera_controls/mouse.rs +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2022 Open Source Robotics Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -*/ - -use bevy::prelude::*; -use bevy::input::mouse::{self, MouseMotion, MouseScrollUnit, MouseWheel}; -use crate::interaction::PickingBlockers; - -#[derive(Resource)] -pub struct MouseCommand { - pub pan: Vec2, - pub orbit: Vec2, - pub orbit_state_changed: bool, - pub was_orbitting: bool, - pub zoom_target: Vec3, - pub zoom_delta: f32, -} - -impl Default for MouseCommand { - fn default() -> Self { - Self { - pan: Vec2::ZERO, - orbit: Vec2::ZERO, - orbit_state_changed: false, - was_orbitting: false, - zoom_target: Vec3::ZERO, - zoom_delta: 0.0 - } - } -} - -pub fn update_mouse_command( - mut mouse_command: ResMut, - mut mouse_motion: EventReader, - mut mouse_wheel: EventReader, - mouse_input: Res>, - keyboard_input: Res>, -) { - // Keyboard, mouse inputs - let is_shifting = - keyboard_input.pressed(KeyCode::ShiftLeft) || - keyboard_input.pressed(KeyCode::ShiftRight); - let cursor_motion = mouse_motion.read().map(|event| event.delta) - .fold(Vec2::ZERO, |acc, delta| acc + delta); - let mut scroll_motion = 0.0; - for ev in mouse_wheel.read() { - #[cfg(not(target_arch = "wasm32"))] - { - scroll_motion += ev.y; - } - #[cfg(target_arch = "wasm32")] - { - // scrolling in wasm is a different beast - scroll_motion += 0.4 * ev.y / ev.y.abs(); - } - } - - // Pan - let is_panning = mouse_input.pressed(MouseButton::Right) && !is_shifting; - if is_panning { - mouse_command.pan = cursor_motion; - } else { - mouse_command.pan = Vec2::ZERO; - } - - // Orbit - let is_orbitting = mouse_input.pressed(MouseButton::Middle) || - (mouse_input.pressed(MouseButton::Right) && is_shifting); - mouse_command.orbit_state_changed = mouse_command.was_orbitting != is_orbitting; - mouse_command.was_orbitting = is_orbitting; - if is_orbitting && !is_panning { - mouse_command.orbit = cursor_motion; - } else { - mouse_command.orbit = Vec2::ZERO; - } - - // Zoom - mouse_command.zoom_delta = scroll_motion; - if !is_orbitting && !is_panning { - mouse_command.zoom_target = Vec3::new(0.0, 0.0, scroll_motion); - } else { - mouse_command.zoom_target = Vec3::ZERO; - } -} - - - - - From 6b5d79020daf33cfd3c649bc290a0eabc491e081 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Tue, 28 May 2024 09:33:41 +0800 Subject: [PATCH 03/32] Preserve cursor selection between camera control updates, corrected previous pan calculation Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 69 ++++++++++++------- .../src/interaction/camera_controls/mod.rs | 14 +--- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 9f8568e5..5b84603c 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -26,16 +26,18 @@ use crate::interaction::{PickingBlockers, SiteRaycastSet}; #[derive(Resource)] pub struct CursorCommand { - pub target_translation: Vec3, - pub target_rotation: Quat, + pub translation_delta: Vec3, + pub rotation_delta: Quat, + pub cursor_selection: Option, pub command_type: CameraCommandType, } impl Default for CursorCommand { fn default() -> Self { Self { - target_translation: Vec3::ZERO, - target_rotation: Quat::IDENTITY, + translation_delta: Vec3::ZERO, + rotation_delta: Quat::IDENTITY, + cursor_selection: None, command_type: CameraCommandType::Inactive, } } @@ -108,7 +110,10 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; - let cursor_selected_point = get_cursor_selected_point(&cursor_raycast_source); + let cursor_selection = match cursor_command.cursor_selection { + Some(selection) => selection, + None => get_cursor_selected_point(&cursor_raycast_source), + }; let cursor_direction_now = cursor_ray.direction().normalize(); let cursor_direction_prev = bevy_cameras .get(active_camera_entity) @@ -117,6 +122,9 @@ pub fn update_cursor_command( .unwrap() .direction .normalize(); + // print both cursor directions + println!("Cursor direction now: {:?}", cursor_direction_now); + println!("Cursor direction prev: {:?}", cursor_direction_prev); // 4. Perspective Mode *cursor_command = match camera_controls.mode() { @@ -126,7 +134,7 @@ pub fn update_cursor_command( command_type, cursor_direction_prev, cursor_direction_now, - cursor_selected_point, + cursor_selection, scroll_motion ) }, @@ -149,7 +157,7 @@ fn get_perspective_cursor_command( command_type: CameraCommandType, cursor_direction_prev: Vec3, cursor_direction_now: Vec3, - cursor_selected_point: Vec3, + cursor_selection: Vec3, scroll_motion: f32 ) -> CursorCommand { // Zoom towards the cursor if zooming only, otherwize zoom to center @@ -158,45 +166,56 @@ fn get_perspective_cursor_command( _ => camera_transform.forward() * scroll_motion }; - let mut target_translation = Vec3::ZERO; - let mut target_rotation = Quat::IDENTITY; + let mut translation_delta = Vec3::ZERO; + let mut rotation_delta = Quat::IDENTITY; + let mut is_cursor_selecting = false; match command_type { CameraCommandType::ZoomOnly => { - target_translation = zoom_translation; - target_rotation = Quat::IDENTITY; + translation_delta = zoom_translation; }, CameraCommandType::Pan => { - // translation = x1 * right_ transltion + x2 * up_translation + // To keep the same point below the cursor, we solve + // selection_to_camera_now + translation_delta = selection_to_camera_next + // selection_to_camera_next = x3 * -cursor_direction_now + let selection_to_camera_now = cursor_selection - camera_transform.translation; + + // translation_delta = x1 * right_ transltion + x2 * up_translation let right_translation = camera_transform.rotation * Vec3::X; let up_translation = camera_transform.rotation * Vec3::Y; - let camera_to_selection = cursor_selected_point - camera_transform.translation; - - // Solving as a linear system let a = Matrix3::new( - right_translation.x, up_translation.x, -cursor_direction_prev.x, - right_translation.y, up_translation.y, -cursor_direction_prev.y, - right_translation.z, up_translation.z, -cursor_direction_prev.z, + right_translation.x, up_translation.x, -cursor_direction_now.x, + right_translation.y, up_translation.y, -cursor_direction_now.y, + right_translation.z, up_translation.z, -cursor_direction_now.z, ); let b = Matrix3x1::new( - camera_to_selection.x, - camera_to_selection.y, - camera_to_selection.z, + selection_to_camera_now.x, + selection_to_camera_now.y, + selection_to_camera_now.z, ); let x = a.lu().solve(&b).unwrap(); - target_translation = zoom_translation + -x[0] * right_translation + -x[1] * up_translation; - target_rotation = Quat::IDENTITY; + translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; + rotation_delta = Quat::IDENTITY; + is_cursor_selecting = true; }, CameraCommandType::Orbit => { + is_cursor_selecting = true; } _ => () } + let cursor_selection = if is_cursor_selecting { + Some(cursor_selection) + } else { + None + }; + return CursorCommand { - target_translation, - target_rotation, + translation_delta, + rotation_delta, + cursor_selection, command_type, }; } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index d1983dba..15288a77 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -67,12 +67,6 @@ pub enum CameraCommandType { ZoomOnly } -pub struct CameraCommand { - pub delta_translation: Vec3, - pub delta_rotation: Quat, - pub command_type: CameraCommandType -} - #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] pub enum ProjectionMode { Perspective, @@ -419,13 +413,9 @@ fn camera_controls( .get_mut(controls.perspective_camera_entities[0]) .unwrap(); if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { - let new_translation = persp_transform.translation + cursor_command.target_translation; - let new_rotation = persp_transform.rotation * cursor_command.target_rotation; - persp_transform.translation = new_translation; - persp_transform.rotation = new_rotation; + persp_transform.translation += cursor_command.translation_delta; + persp_transform.rotation *= cursor_command.rotation_delta; } - - } if controls.mode() == ProjectionMode::Orthographic { From 2b3da968cd96f282b1822796363a9f207ec1d089 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Wed, 29 May 2024 22:48:05 +0800 Subject: [PATCH 04/32] Reverted to prior orbit method Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 183 +++++++++--------- .../src/interaction/camera_controls/mod.rs | 6 +- 2 files changed, 95 insertions(+), 94 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 5b84603c..bbf06f90 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -15,19 +15,21 @@ * */ +use super::{CameraCommandType, CameraControls, ProjectionMode}; +use crate::interaction::SiteRaycastSet; +use bevy::ecs::component::TableStorage; +use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::prelude::*; use bevy::render::camera; +use bevy::window::PrimaryWindow; +use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; -use bevy::prelude::*; -use bevy::input::mouse::{MouseMotion, MouseWheel}; -use bevy::window::{PrimaryWindow}; -use bevy_mod_raycast::{deferred::RaycastSource}; -use super::{CameraCommandType, CameraControls, ProjectionMode}; -use crate::interaction::{PickingBlockers, SiteRaycastSet}; #[derive(Resource)] pub struct CursorCommand { pub translation_delta: Vec3, pub rotation_delta: Quat, + pub scale_delta: f32, pub cursor_selection: Option, pub command_type: CameraCommandType, } @@ -37,6 +39,7 @@ impl Default for CursorCommand { Self { translation_delta: Vec3::ZERO, rotation_delta: Quat::IDENTITY, + scale_delta: 0.0, cursor_selection: None, command_type: CameraCommandType::Inactive, } @@ -49,28 +52,17 @@ pub fn update_cursor_command( mut mouse_motion: EventReader, mut mouse_wheel: EventReader, mouse_input: Res>, - picking_blockers: Res, keyboard_input: Res>, raycast_sources: Query<&RaycastSource>, - bevy_cameras: Query<&Camera>, cameras: Query<(&Projection, &Transform, &GlobalTransform)>, primary_windows: Query<&Window, With>, ) { if let Ok(window) = primary_windows.get_single() { - - // Cursor inputs - let cursor_position_now = match window.cursor_position() { - Some(pos) => pos, - None => { - *cursor_command = CursorCommand::default(); - return; - } , - }; - let mut cursor_motion = mouse_motion.read().map(|event| event.delta) + // Cursor and scroll inputs + let mut cursor_motion = mouse_motion + .read() + .map(|event| event.delta) .fold(Vec2::ZERO, |acc, delta| acc + delta); - let cursor_position_prev = cursor_position_now - cursor_motion; - - // Scroll inputs let mut scroll_motion = 0.0; for ev in mouse_wheel.read() { #[cfg(not(target_arch = "wasm32"))] @@ -83,12 +75,13 @@ pub fn update_cursor_command( scroll_motion += 0.4 * ev.y / ev.y.abs(); } } - + // Command type, return if inactive let command_type = get_command_type( - &keyboard_input, &mouse_input, - &cursor_motion, &scroll_motion, - &picking_blockers + &keyboard_input, + &mouse_input, + &cursor_motion, + &scroll_motion, ); if command_type == CameraCommandType::Inactive { *cursor_command = CursorCommand::default(); @@ -100,8 +93,9 @@ pub fn update_cursor_command( ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], }; - let (camera_proj, camera_transform, camera_global_transform) = cameras.get(active_camera_entity).unwrap(); - + let (camera_proj, camera_transform, camera_global_transform) = + cameras.get(active_camera_entity).unwrap(); + // Get selection under cursor, cursor direction let Ok(cursor_raycast_source) = raycast_sources.get_single() else { return; @@ -114,35 +108,22 @@ pub fn update_cursor_command( Some(selection) => selection, None => get_cursor_selected_point(&cursor_raycast_source), }; - let cursor_direction_now = cursor_ray.direction().normalize(); - let cursor_direction_prev = bevy_cameras - .get(active_camera_entity) - .unwrap() - .viewport_to_world(camera_global_transform, cursor_position_prev) - .unwrap() - .direction - .normalize(); - // print both cursor directions - println!("Cursor direction now: {:?}", cursor_direction_now); - println!("Cursor direction prev: {:?}", cursor_direction_prev); - + let cursor_direction = cursor_ray.direction().normalize(); + // 4. Perspective Mode *cursor_command = match camera_controls.mode() { - ProjectionMode::Perspective => { - get_perspective_cursor_command( - *camera_transform, - command_type, - cursor_direction_prev, - cursor_direction_now, - cursor_selection, - scroll_motion - ) - }, - ProjectionMode::Orthographic => { - get_orthographic_cursor_command() - }, + ProjectionMode::Perspective => get_perspective_cursor_command( + &camera_transform, + &camera_proj, + command_type, + cursor_motion, + cursor_direction, + cursor_selection, + scroll_motion, + window, + ), + ProjectionMode::Orthographic => get_orthographic_cursor_command(), }; - } else { *cursor_command = CursorCommand::default(); } @@ -153,57 +134,81 @@ fn get_orthographic_cursor_command() -> CursorCommand { } fn get_perspective_cursor_command( - camera_transform: Transform, + camera_transform: &Transform, + camera_proj: &Projection, command_type: CameraCommandType, - cursor_direction_prev: Vec3, - cursor_direction_now: Vec3, + cursor_motion: Vec2, + cursor_direction: Vec3, cursor_selection: Vec3, - scroll_motion: f32 + scroll_motion: f32, + window: &Window, ) -> CursorCommand { // Zoom towards the cursor if zooming only, otherwize zoom to center let zoom_translation = match command_type { - CameraCommandType::ZoomOnly => cursor_direction_now * 0.5 * scroll_motion, - _ => camera_transform.forward() * scroll_motion + CameraCommandType::TranslationZoom => cursor_direction * 0.5 * scroll_motion, + _ => camera_transform.forward() * scroll_motion, }; let mut translation_delta = Vec3::ZERO; let mut rotation_delta = Quat::IDENTITY; + let mut scale_delta = 0.0; let mut is_cursor_selecting = false; match command_type { - CameraCommandType::ZoomOnly => { + CameraCommandType::TranslationZoom => { translation_delta = zoom_translation; - }, + } CameraCommandType::Pan => { // To keep the same point below the cursor, we solve - // selection_to_camera_now + translation_delta = selection_to_camera_next - // selection_to_camera_next = x3 * -cursor_direction_now - let selection_to_camera_now = cursor_selection - camera_transform.translation; + // selection_to_camera + translation_delta = selection_to_camera_next + // selection_to_camera_next = x3 * -cursor_direction + let selection_to_camera = cursor_selection - camera_transform.translation; // translation_delta = x1 * right_ transltion + x2 * up_translation let right_translation = camera_transform.rotation * Vec3::X; let up_translation = camera_transform.rotation * Vec3::Y; let a = Matrix3::new( - right_translation.x, up_translation.x, -cursor_direction_now.x, - right_translation.y, up_translation.y, -cursor_direction_now.y, - right_translation.z, up_translation.z, -cursor_direction_now.z, + right_translation.x, + up_translation.x, + -cursor_direction.x, + right_translation.y, + up_translation.y, + -cursor_direction.y, + right_translation.z, + up_translation.z, + -cursor_direction.z, ); let b = Matrix3x1::new( - selection_to_camera_now.x, - selection_to_camera_now.y, - selection_to_camera_now.z, + selection_to_camera.x, + selection_to_camera.y, + selection_to_camera.z, ); let x = a.lu().solve(&b).unwrap(); translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; rotation_delta = Quat::IDENTITY; is_cursor_selecting = true; - }, + } CameraCommandType::Orbit => { - is_cursor_selecting = true; + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; + let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI; + + let yaw = Quat::from_rotation_z(delta_x); + let pitch = Quat::from_rotation_x(delta_y); + + let target_rotation = (yaw * camera_transform.rotation) * pitch; + + // Do not allow rotations to upside down states + if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { + let start_rotation = Mat3::from_quat(camera_transform.rotation); + let target_rotation = Mat3::from_quat(target_rotation); + rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + } + translation_delta = zoom_translation; } - _ => () + _ => (), } let cursor_selection = if is_cursor_selecting { @@ -215,23 +220,24 @@ fn get_perspective_cursor_command( return CursorCommand { translation_delta, rotation_delta, + scale_delta: 0.0, cursor_selection, command_type, }; } +// Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { let cursor_ray = cursor_raycast_source.get_ray().unwrap(); + let default_dist = 100.0; match cursor_raycast_source.get_nearest_intersection() { Some((_, intersection)) => intersection.position(), None => { let n_p = Vec3::Z; let n_r = cursor_ray.direction(); let denom = n_p.dot(n_r); - //TODO(reuben-thomas) default to arbitrary point if ground plane not available if denom > 1e-3 { - println!("Cursor ray is parallel to the camera"); - Vec3::ZERO + cursor_ray.origin() + (default_dist * cursor_ray.direction()) } else { let t = (Vec3::Z - cursor_ray.origin()).dot(n_p) / denom; cursor_ray.origin() + t * cursor_ray.direction() @@ -245,32 +251,29 @@ fn get_command_type( mouse_input: &Res>, cursor_motion: &Vec2, scroll_motion: &f32, - picking_blockers: &Res, ) -> CameraCommandType { // Inputs let is_cursor_moving = cursor_motion.length() > 0.; let is_scrolling = *scroll_motion != 0.; let is_shifting = - keyboard_input.pressed(KeyCode::ShiftLeft) || - keyboard_input.pressed(KeyCode::ShiftRight); + keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight); // Panning - if is_cursor_moving && !is_shifting && - mouse_input.pressed(MouseButton::Right) { - return CameraCommandType::Pan + if is_cursor_moving && !is_shifting && mouse_input.pressed(MouseButton::Right) { + return CameraCommandType::Pan; } // Orbitting - if is_cursor_moving && - mouse_input.pressed(MouseButton::Middle) || - (mouse_input.pressed(MouseButton::Right) && is_shifting) { - return CameraCommandType::Orbit + if is_cursor_moving && mouse_input.pressed(MouseButton::Middle) + || (mouse_input.pressed(MouseButton::Right) && is_shifting) + { + return CameraCommandType::Orbit; } - // Zoom Only + // Zoom if is_scrolling { - return CameraCommandType::ZoomOnly + return CameraCommandType::TranslationZoom; } - return CameraCommandType::Inactive -} \ No newline at end of file + return CameraCommandType::Inactive; +} diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 15288a77..f2048e5a 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -28,9 +28,8 @@ use bevy::{ }, window::PrimaryWindow, }; - mod cursor; -use cursor::{CursorCommand, update_cursor_command}; +use cursor::{update_cursor_command, CursorCommand}; /// RenderLayers are used to inform cameras which entities they should render. /// The General render layer is for things that should be visible to all @@ -58,13 +57,12 @@ pub const XRAY_RENDER_LAYER: u8 = 5; /// models in the engine without having them being visible to general cameras pub const MODEL_PREVIEW_LAYER: u8 = 6; - #[derive(PartialEq, Debug, Copy, Clone)] pub enum CameraCommandType { Inactive, Pan, Orbit, - ZoomOnly + TranslationZoom, } #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] From 4b21ea47d7fbdd59291173a27a4c37d16e120f50 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Thu, 30 May 2024 00:37:35 +0800 Subject: [PATCH 05/32] Add orthographic control with rotation Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 77 +++++++++++++++++-- .../src/interaction/camera_controls/mod.rs | 20 ++++- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index bbf06f90..bef2b386 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -21,8 +21,10 @@ use bevy::ecs::component::TableStorage; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; use bevy::render::camera; +use bevy::transform::helper::ComputeGlobalTransformError; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; +use geo::scale; use nalgebra::{Matrix3, Matrix3x1}; #[derive(Resource)] @@ -113,6 +115,15 @@ pub fn update_cursor_command( // 4. Perspective Mode *cursor_command = match camera_controls.mode() { ProjectionMode::Perspective => get_perspective_cursor_command( + &camera_transform, + command_type, + cursor_motion, + cursor_direction, + cursor_selection, + scroll_motion, + window, + ), + ProjectionMode::Orthographic => get_orthographic_cursor_command( &camera_transform, &camera_proj, command_type, @@ -122,20 +133,75 @@ pub fn update_cursor_command( scroll_motion, window, ), - ProjectionMode::Orthographic => get_orthographic_cursor_command(), }; } else { *cursor_command = CursorCommand::default(); } } -fn get_orthographic_cursor_command() -> CursorCommand { - CursorCommand::default() +fn get_orthographic_cursor_command( + camera_transform: &Transform, + camera_proj: &Projection, + command_type: CameraCommandType, + cursor_motion: Vec2, + cursor_direction: Vec3, + cursor_selection: Vec3, + scroll_motion: f32, + window: &Window, +) -> CursorCommand { + if let Projection::Orthographic(camera_proj) = camera_proj { + // Default Values + let mut translation_delta = Vec3::ZERO; + let mut rotation_delta = Quat::IDENTITY; + let mut scale_delta = 0.0; + let mut is_cursor_selecting = false; + + match command_type { + CameraCommandType::TranslationZoom => { + scale_delta = -scroll_motion * camera_proj.scale * 0.1; + } + CameraCommandType::Pan => { + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + let aspect_ratio = window_size[0] / window_size[1]; + let cursor_motion_adj = cursor_motion * 2.0 / window_size + * Vec2::new(camera_proj.scale * aspect_ratio, camera_proj.scale); + let right_translation = + -cursor_motion_adj.x * (camera_transform.rotation * Vec3::X); + let up_translation = cursor_motion_adj.y * (camera_transform.rotation * Vec3::Y); + + translation_delta = up_translation + right_translation; + scale_delta = -scroll_motion * camera_proj.scale * 0.1; + } + CameraCommandType::Orbit => { + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(delta_x); + rotation_delta = yaw; + is_cursor_selecting = true; + } + CameraCommandType::Inactive => (), + } + + let cursor_selection = if is_cursor_selecting { + Some(cursor_selection) + } else { + None + }; + + return CursorCommand { + translation_delta, + rotation_delta, + scale_delta, + cursor_selection, + command_type, + }; + } else { + CursorCommand::default() + } } fn get_perspective_cursor_command( camera_transform: &Transform, - camera_proj: &Projection, command_type: CameraCommandType, cursor_motion: Vec2, cursor_direction: Vec3, @@ -149,6 +215,7 @@ fn get_perspective_cursor_command( _ => camera_transform.forward() * scroll_motion, }; + // Default values let mut translation_delta = Vec3::ZERO; let mut rotation_delta = Quat::IDENTITY; let mut scale_delta = 0.0; @@ -220,7 +287,7 @@ fn get_perspective_cursor_command( return CursorCommand { translation_delta, rotation_delta, - scale_delta: 0.0, + scale_delta, cursor_selection, command_type, }; diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index f2048e5a..d348f407 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -413,11 +413,29 @@ fn camera_controls( if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { persp_transform.translation += cursor_command.translation_delta; persp_transform.rotation *= cursor_command.rotation_delta; + + // Ensure upright + let forward = persp_transform.forward(); + persp_transform.look_to(forward, Vec3::Z); } } if controls.mode() == ProjectionMode::Orthographic { - return; + let (mut ortho_proj, mut ortho_transform) = cameras + .get_mut(controls.orthographic_camera_entities[0]) + .unwrap(); + if let Projection::Orthographic(ortho_proj) = ortho_proj.as_mut() { + ortho_transform.translation += cursor_command.translation_delta; + ortho_transform.rotation *= cursor_command.rotation_delta; + ortho_proj.scale += cursor_command.scale_delta; + } + let proj = ortho_proj.clone(); + let children = cameras + .get_many_mut(controls.orthographic_camera_entities) + .unwrap(); + for (mut child_proj, _) in children { + *child_proj = proj.clone(); + } } } From b85c3cbf3555ea4c46d1d4236538ff28a59050b4 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Thu, 30 May 2024 10:12:05 +0800 Subject: [PATCH 06/32] Added selection based orthographic panning, support fov zoom Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 147 +++++++++--------- .../src/interaction/camera_controls/mod.rs | 13 +- 2 files changed, 86 insertions(+), 74 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index bef2b386..78f0887f 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -17,14 +17,10 @@ use super::{CameraCommandType, CameraControls, ProjectionMode}; use crate::interaction::SiteRaycastSet; -use bevy::ecs::component::TableStorage; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; -use bevy::render::camera; -use bevy::transform::helper::ComputeGlobalTransformError; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; -use geo::scale; use nalgebra::{Matrix3, Matrix3x1}; #[derive(Resource)] @@ -32,6 +28,7 @@ pub struct CursorCommand { pub translation_delta: Vec3, pub rotation_delta: Quat, pub scale_delta: f32, + pub fov_delta: f32, pub cursor_selection: Option, pub command_type: CameraCommandType, } @@ -42,6 +39,7 @@ impl Default for CursorCommand { translation_delta: Vec3::ZERO, rotation_delta: Quat::IDENTITY, scale_delta: 0.0, + fov_delta: 0.0, cursor_selection: None, command_type: CameraCommandType::Inactive, } @@ -61,7 +59,7 @@ pub fn update_cursor_command( ) { if let Ok(window) = primary_windows.get_single() { // Cursor and scroll inputs - let mut cursor_motion = mouse_motion + let cursor_motion = mouse_motion .read() .map(|event| event.delta) .fold(Vec2::ZERO, |acc, delta| acc + delta); @@ -95,7 +93,7 @@ pub fn update_cursor_command( ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], }; - let (camera_proj, camera_transform, camera_global_transform) = + let (camera_proj, camera_transform, _) = cameras.get(active_camera_entity).unwrap(); // Get selection under cursor, cursor direction @@ -106,9 +104,10 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; + let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); let cursor_selection = match cursor_command.cursor_selection { Some(selection) => selection, - None => get_cursor_selected_point(&cursor_raycast_source), + None => cursor_selection_new, }; let cursor_direction = cursor_ray.direction().normalize(); @@ -128,8 +127,8 @@ pub fn update_cursor_command( &camera_proj, command_type, cursor_motion, - cursor_direction, cursor_selection, + cursor_selection_new, scroll_motion, window, ), @@ -144,60 +143,66 @@ fn get_orthographic_cursor_command( camera_proj: &Projection, command_type: CameraCommandType, cursor_motion: Vec2, - cursor_direction: Vec3, cursor_selection: Vec3, + cursor_selection_new: Vec3, scroll_motion: f32, window: &Window, ) -> CursorCommand { + + let mut cursor_command = CursorCommand::default(); + let mut is_cursor_selecting = false; + + // Zoom if let Projection::Orthographic(camera_proj) = camera_proj { - // Default Values - let mut translation_delta = Vec3::ZERO; - let mut rotation_delta = Quat::IDENTITY; - let mut scale_delta = 0.0; - let mut is_cursor_selecting = false; - - match command_type { - CameraCommandType::TranslationZoom => { - scale_delta = -scroll_motion * camera_proj.scale * 0.1; - } - CameraCommandType::Pan => { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let aspect_ratio = window_size[0] / window_size[1]; - let cursor_motion_adj = cursor_motion * 2.0 / window_size - * Vec2::new(camera_proj.scale * aspect_ratio, camera_proj.scale); - let right_translation = - -cursor_motion_adj.x * (camera_transform.rotation * Vec3::X); - let up_translation = cursor_motion_adj.y * (camera_transform.rotation * Vec3::Y); - - translation_delta = up_translation + right_translation; - scale_delta = -scroll_motion * camera_proj.scale * 0.1; - } - CameraCommandType::Orbit => { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; - let yaw = Quat::from_rotation_z(delta_x); - rotation_delta = yaw; - is_cursor_selecting = true; - } - CameraCommandType::Inactive => (), - } + cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; + } - let cursor_selection = if is_cursor_selecting { - Some(cursor_selection) - } else { - None - }; + match command_type { + CameraCommandType::Pan => { + //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction + let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); + let selection_to_camera = cursor_selection - camera_transform.translation; + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = camera_transform.rotation * Vec3::Y; - return CursorCommand { - translation_delta, - rotation_delta, - scale_delta, - cursor_selection, - command_type, - }; - } else { - CursorCommand::default() + let a = Matrix3::new( + right_translation.x, + up_translation.x, + -cursor_direction.x, + right_translation.y, + up_translation.y, + -cursor_direction.y, + right_translation.z, + up_translation.z, + -cursor_direction.z, + ); + let b = Matrix3x1::new( + selection_to_camera.x, + selection_to_camera.y, + selection_to_camera.z, + ); + let x = a.lu().solve(&b).unwrap(); + + cursor_command.translation_delta = x[0] * right_translation + x[1] * up_translation; + is_cursor_selecting = true; + } + CameraCommandType::Orbit => { + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(delta_x); + cursor_command.rotation_delta = yaw; + is_cursor_selecting = true; + } + _ => (), } + + cursor_command.cursor_selection = if is_cursor_selecting { + Some(cursor_selection) + } else { + None + }; + + return cursor_command; } fn get_perspective_cursor_command( @@ -215,15 +220,15 @@ fn get_perspective_cursor_command( _ => camera_transform.forward() * scroll_motion, }; - // Default values - let mut translation_delta = Vec3::ZERO; - let mut rotation_delta = Quat::IDENTITY; - let mut scale_delta = 0.0; + let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; match command_type { + CameraCommandType::FovZoom => { + cursor_command.fov_delta = -scroll_motion * 0.1; + } CameraCommandType::TranslationZoom => { - translation_delta = zoom_translation; + cursor_command.translation_delta = zoom_translation; } CameraCommandType::Pan => { // To keep the same point below the cursor, we solve @@ -253,8 +258,8 @@ fn get_perspective_cursor_command( ); let x = a.lu().solve(&b).unwrap(); - translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; - rotation_delta = Quat::IDENTITY; + cursor_command.translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; + cursor_command.rotation_delta = Quat::IDENTITY; is_cursor_selecting = true; } CameraCommandType::Orbit => { @@ -271,26 +276,20 @@ fn get_perspective_cursor_command( if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { let start_rotation = Mat3::from_quat(camera_transform.rotation); let target_rotation = Mat3::from_quat(target_rotation); - rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); } - translation_delta = zoom_translation; + cursor_command.translation_delta = zoom_translation; } - _ => (), + CameraCommandType::Inactive => (), } - let cursor_selection = if is_cursor_selecting { + cursor_command.cursor_selection = if is_cursor_selecting { Some(cursor_selection) } else { None }; - return CursorCommand { - translation_delta, - rotation_delta, - scale_delta, - cursor_selection, - command_type, - }; + return cursor_command; } // Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front @@ -338,7 +337,9 @@ fn get_command_type( } // Zoom - if is_scrolling { + if is_scrolling && is_shifting { + return CameraCommandType::FovZoom; + } else if is_scrolling { return CameraCommandType::TranslationZoom; } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index d348f407..fdf2cfeb 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -63,6 +63,7 @@ pub enum CameraCommandType { Pan, Orbit, TranslationZoom, + FovZoom, } #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] @@ -413,11 +414,20 @@ fn camera_controls( if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { persp_transform.translation += cursor_command.translation_delta; persp_transform.rotation *= cursor_command.rotation_delta; + persp_proj.fov += cursor_command.fov_delta; // Ensure upright let forward = persp_transform.forward(); persp_transform.look_to(forward, Vec3::Z); } + + let proj = persp_proj.clone(); + let children = cameras + .get_many_mut(controls.perspective_camera_entities) + .unwrap(); + for (mut child_proj, _) in children { + *child_proj = proj.clone(); + } } if controls.mode() == ProjectionMode::Orthographic { @@ -429,6 +439,7 @@ fn camera_controls( ortho_transform.rotation *= cursor_command.rotation_delta; ortho_proj.scale += cursor_command.scale_delta; } + let proj = ortho_proj.clone(); let children = cameras .get_many_mut(controls.orthographic_camera_entities) @@ -450,4 +461,4 @@ impl Plugin for CameraControlsPlugin { .add_systems(Update, update_cursor_command) .add_systems(Update, camera_controls); } -} +} \ No newline at end of file From 51ba52bfba0b19ab7c5e9c3fa0b73c43196f4434 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Thu, 30 May 2024 10:20:11 +0800 Subject: [PATCH 07/32] Fix styling Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 15 ++++++++------- .../src/interaction/camera_controls/mod.rs | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 78f0887f..67f3a264 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -93,8 +93,7 @@ pub fn update_cursor_command( ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], }; - let (camera_proj, camera_transform, _) = - cameras.get(active_camera_entity).unwrap(); + let (camera_proj, camera_transform, _) = cameras.get(active_camera_entity).unwrap(); // Get selection under cursor, cursor direction let Ok(cursor_raycast_source) = raycast_sources.get_single() else { @@ -104,7 +103,7 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; - let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); + let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); let cursor_selection = match cursor_command.cursor_selection { Some(selection) => selection, None => cursor_selection_new, @@ -148,7 +147,6 @@ fn get_orthographic_cursor_command( scroll_motion: f32, window: &Window, ) -> CursorCommand { - let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; @@ -160,7 +158,8 @@ fn get_orthographic_cursor_command( match command_type { CameraCommandType::Pan => { //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction - let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); + let cursor_direction = + (cursor_selection_new - camera_transform.translation).normalize(); let selection_to_camera = cursor_selection - camera_transform.translation; let right_translation = camera_transform.rotation * Vec3::X; let up_translation = camera_transform.rotation * Vec3::Y; @@ -258,7 +257,8 @@ fn get_perspective_cursor_command( ); let x = a.lu().solve(&b).unwrap(); - cursor_command.translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; + cursor_command.translation_delta = + zoom_translation + x[0] * right_translation + x[1] * up_translation; cursor_command.rotation_delta = Quat::IDENTITY; is_cursor_selecting = true; } @@ -276,7 +276,8 @@ fn get_perspective_cursor_command( if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { let start_rotation = Mat3::from_quat(camera_transform.rotation); let target_rotation = Mat3::from_quat(target_rotation); - cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + cursor_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); } cursor_command.translation_delta = zoom_translation; } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index fdf2cfeb..cc7c0569 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -439,7 +439,7 @@ fn camera_controls( ortho_transform.rotation *= cursor_command.rotation_delta; ortho_proj.scale += cursor_command.scale_delta; } - + let proj = ortho_proj.clone(); let children = cameras .get_many_mut(controls.orthographic_camera_entities) @@ -461,4 +461,4 @@ impl Plugin for CameraControlsPlugin { .add_systems(Update, update_cursor_command) .add_systems(Update, camera_controls); } -} \ No newline at end of file +} From 0726cb22f15eabbbb96b12e5fb44db1ac96d9625 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Thu, 30 May 2024 12:57:53 +0800 Subject: [PATCH 08/32] fixed picking based orbits Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 67f3a264..4ac92967 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -19,6 +19,7 @@ use super::{CameraCommandType, CameraControls, ProjectionMode}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; +use bevy::render::camera; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; @@ -155,11 +156,11 @@ fn get_orthographic_cursor_command( cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; } + //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction + let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); + match command_type { CameraCommandType::Pan => { - //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction - let cursor_direction = - (cursor_selection_new - camera_transform.translation).normalize(); let selection_to_camera = cursor_selection - camera_transform.translation; let right_translation = camera_transform.rotation * Vec3::X; let up_translation = camera_transform.rotation * Vec3::Y; @@ -186,9 +187,16 @@ fn get_orthographic_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; - let yaw = Quat::from_rotation_z(delta_x); + let cursor_direction_prev = + (cursor_selection - camera_transform.translation).normalize(); + + let heading_0 = + (cursor_direction_prev - cursor_direction_prev.project_onto(Vec3::Z)).normalize(); + let heading_1 = (cursor_direction - cursor_direction.project_onto(Vec3::Z)).normalize(); + let is_clockwise = heading_0.cross(heading_1).dot(Vec3::Z) > 0.0; + let yaw = heading_0.angle_between(heading_1) * if is_clockwise { -1.0 } else { 1.0 }; + let yaw = Quat::from_rotation_z(yaw); + cursor_command.rotation_delta = yaw; is_cursor_selecting = true; } @@ -263,23 +271,35 @@ fn get_perspective_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI; - let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI; - - let yaw = Quat::from_rotation_z(delta_x); - let pitch = Quat::from_rotation_x(delta_y); - - let target_rotation = (yaw * camera_transform.rotation) * pitch; + let cursor_direction_prev = + (cursor_selection - camera_transform.translation).normalize(); + + let heading_0 = + (cursor_direction_prev - cursor_direction_prev.project_onto(Vec3::Z)).normalize(); + let heading_1 = (cursor_direction - cursor_direction.project_onto(Vec3::Z)).normalize(); + let is_clockwise = heading_0.cross(heading_1).dot(Vec3::Z) > 0.0; + let yaw = heading_0.angle_between(heading_1) * if is_clockwise { -1.0 } else { 1.0 }; + let yaw = Quat::from_rotation_z(yaw); + + let pitch_0 = cursor_direction_prev.z.acos(); + let pitch_1 = cursor_direction.z.acos(); + let pitch = pitch_1 - pitch_0; + let pitch = Quat::from_rotation_x(pitch); + + // Exclude pitch if end result is upside down + let mut target_rotation = (yaw * camera_transform.rotation) * pitch; + target_rotation = if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { + target_rotation + } else { + yaw * camera_transform.rotation + }; - // Do not allow rotations to upside down states - if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { - let start_rotation = Mat3::from_quat(camera_transform.rotation); - let target_rotation = Mat3::from_quat(target_rotation); - cursor_command.rotation_delta = - Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - } + let start_rotation = Mat3::from_quat(camera_transform.rotation); + let target_rotation = Mat3::from_quat(target_rotation); + cursor_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); cursor_command.translation_delta = zoom_translation; + is_cursor_selecting = true; } CameraCommandType::Inactive => (), } From b806ef61adca81d6a41657666af9b08e84ac7792 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Fri, 31 May 2024 12:44:08 +0800 Subject: [PATCH 09/32] update cursor command type Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 13 +++---------- .../src/interaction/camera_controls/mod.rs | 2 -- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 4ac92967..2e7c5e5b 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -19,7 +19,6 @@ use super::{CameraCommandType, CameraControls, ProjectionMode}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; -use bevy::render::camera; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; @@ -58,7 +57,7 @@ pub fn update_cursor_command( cameras: Query<(&Projection, &Transform, &GlobalTransform)>, primary_windows: Query<&Window, With>, ) { - if let Ok(window) = primary_windows.get_single() { + if let Ok(_) = primary_windows.get_single() { // Cursor and scroll inputs let cursor_motion = mouse_motion .read() @@ -116,21 +115,17 @@ pub fn update_cursor_command( ProjectionMode::Perspective => get_perspective_cursor_command( &camera_transform, command_type, - cursor_motion, cursor_direction, cursor_selection, scroll_motion, - window, ), ProjectionMode::Orthographic => get_orthographic_cursor_command( &camera_transform, &camera_proj, command_type, - cursor_motion, cursor_selection, cursor_selection_new, scroll_motion, - window, ), }; } else { @@ -142,11 +137,9 @@ fn get_orthographic_cursor_command( camera_transform: &Transform, camera_proj: &Projection, command_type: CameraCommandType, - cursor_motion: Vec2, cursor_selection: Vec3, cursor_selection_new: Vec3, scroll_motion: f32, - window: &Window, ) -> CursorCommand { let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; @@ -203,6 +196,7 @@ fn get_orthographic_cursor_command( _ => (), } + cursor_command.command_type = command_type; cursor_command.cursor_selection = if is_cursor_selecting { Some(cursor_selection) } else { @@ -215,11 +209,9 @@ fn get_orthographic_cursor_command( fn get_perspective_cursor_command( camera_transform: &Transform, command_type: CameraCommandType, - cursor_motion: Vec2, cursor_direction: Vec3, cursor_selection: Vec3, scroll_motion: f32, - window: &Window, ) -> CursorCommand { // Zoom towards the cursor if zooming only, otherwize zoom to center let zoom_translation = match command_type { @@ -304,6 +296,7 @@ fn get_perspective_cursor_command( CameraCommandType::Inactive => (), } + cursor_command.command_type = command_type; cursor_command.cursor_selection = if is_cursor_selecting { Some(cursor_selection) } else { diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index cc7c0569..3e8c3792 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -20,13 +20,11 @@ use bevy::{ core_pipeline::clear_color::ClearColorConfig, core_pipeline::core_3d::Camera3dBundle, core_pipeline::tonemapping::Tonemapping, - input::mouse::{MouseButton, MouseWheel}, prelude::*, render::{ camera::{Camera, Projection, ScalingMode}, view::RenderLayers, }, - window::PrimaryWindow, }; mod cursor; use cursor::{update_cursor_command, CursorCommand}; From fb63ade9ff9aa0554376f411f2285f111b47cf91 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 3 Jun 2024 16:00:25 +0800 Subject: [PATCH 10/32] orbit camera around fixed point Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 63 +++++++++++-------- .../src/interaction/camera_controls/mod.rs | 25 ++++++-- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 2e7c5e5b..70d41457 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -19,6 +19,7 @@ use super::{CameraCommandType, CameraControls, ProjectionMode}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; +use bevy::render::camera; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; @@ -30,6 +31,7 @@ pub struct CursorCommand { pub scale_delta: f32, pub fov_delta: f32, pub cursor_selection: Option, + pub camera_selection: Vec3, pub command_type: CameraCommandType, } @@ -41,6 +43,7 @@ impl Default for CursorCommand { scale_delta: 0.0, fov_delta: 0.0, cursor_selection: None, + camera_selection: Vec3::ZERO, command_type: CameraCommandType::Inactive, } } @@ -57,7 +60,7 @@ pub fn update_cursor_command( cameras: Query<(&Projection, &Transform, &GlobalTransform)>, primary_windows: Query<&Window, With>, ) { - if let Ok(_) = primary_windows.get_single() { + if let Ok(window) = primary_windows.get_single() { // Cursor and scroll inputs let cursor_motion = mouse_motion .read() @@ -117,7 +120,11 @@ pub fn update_cursor_command( command_type, cursor_direction, cursor_selection, + cursor_motion, + camera_controls.orbit_center, + camera_controls.orbit_radius, scroll_motion, + window, ), ProjectionMode::Orthographic => get_orthographic_cursor_command( &camera_transform, @@ -211,7 +218,11 @@ fn get_perspective_cursor_command( command_type: CameraCommandType, cursor_direction: Vec3, cursor_selection: Vec3, + cursor_motion: Vec2, + orbit_center: Vec3, + orbit_radius: f32, scroll_motion: f32, + window: &Window, ) -> CursorCommand { // Zoom towards the cursor if zooming only, otherwize zoom to center let zoom_translation = match command_type { @@ -227,7 +238,7 @@ fn get_perspective_cursor_command( cursor_command.fov_delta = -scroll_motion * 0.1; } CameraCommandType::TranslationZoom => { - cursor_command.translation_delta = zoom_translation; + cursor_command.translation_delta = cursor_direction * 0.5 * scroll_motion; } CameraCommandType::Pan => { // To keep the same point below the cursor, we solve @@ -263,34 +274,33 @@ fn get_perspective_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - let cursor_direction_prev = - (cursor_selection - camera_transform.translation).normalize(); - - let heading_0 = - (cursor_direction_prev - cursor_direction_prev.project_onto(Vec3::Z)).normalize(); - let heading_1 = (cursor_direction - cursor_direction.project_onto(Vec3::Z)).normalize(); - let is_clockwise = heading_0.cross(heading_1).dot(Vec3::Z) > 0.0; - let yaw = heading_0.angle_between(heading_1) * if is_clockwise { -1.0 } else { 1.0 }; - let yaw = Quat::from_rotation_z(yaw); - let pitch_0 = cursor_direction_prev.z.acos(); - let pitch_1 = cursor_direction.z.acos(); - let pitch = pitch_1 - pitch_0; - let pitch = Quat::from_rotation_x(pitch); - - // Exclude pitch if end result is upside down - let mut target_rotation = (yaw * camera_transform.rotation) * pitch; - target_rotation = if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { - target_rotation - } else { - yaw * camera_transform.rotation + // Adjust orbit to the window size + // TODO(@reuben-thomas) also adjust to fov + let window_size = Vec2::new(window.width(), window.height()); + let orbit_sensitivity = 1.0; + let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; + let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; + let yaw = Quat::from_rotation_z(-delta_x); + let pitch = Quat::from_rotation_x(-delta_y); + + // Get target rotation, excluding pitch if upside down + let mut target_transform = camera_transform.clone(); + target_transform.rotation = (yaw * camera_transform.rotation) * pitch; + if target_transform.up().dot(Vec3::Z) <= 0.0 { + target_transform.rotation = yaw * camera_transform.rotation; }; + // Calculate translation to orbit around camera centre + let orbit_radius = orbit_radius - 0.5 * scroll_motion; + let target_rotation = Mat3::from_quat(target_transform.rotation); + target_transform.translation = orbit_center + + target_rotation.mul_vec3(Vec3::new(0.0, 0.0, orbit_radius)); + + // Get the rotation difference to be multiplied into the current rotation let start_rotation = Mat3::from_quat(camera_transform.rotation); - let target_rotation = Mat3::from_quat(target_rotation); - cursor_command.rotation_delta = - Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - cursor_command.translation_delta = zoom_translation; + cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + cursor_command.translation_delta = target_transform.translation - camera_transform.translation; is_cursor_selecting = true; } CameraCommandType::Inactive => (), @@ -306,6 +316,7 @@ fn get_perspective_cursor_command( return cursor_command; } + // Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { let cursor_ray = cursor_raycast_source.get_ray().unwrap(); diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 3e8c3792..9b9fcdd3 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -102,8 +102,6 @@ pub struct CameraControls { pub orthographic_headlight: Entity, pub orbit_center: Vec3, pub orbit_radius: f32, - pub orbit_upside_down: bool, - pub was_oribiting: bool, } /// True/false for whether the headlight should be on or off @@ -371,12 +369,23 @@ impl FromWorld for CameraControls { orthographic_headlight, orbit_center: Vec3::ZERO, orbit_radius: (3.0 * 10.0 * 10.0 as f32).sqrt(), - orbit_upside_down: false, - was_oribiting: false, } } } +// Get groundplane intersection of camera direction +fn get_camera_selected_point(camera_transform: &Transform) -> Option { + let n_p = Vec3::Z; + let n_r = camera_transform.forward(); + let denom = n_p.dot(n_r); + if denom > 0.1 { + return None + } else { + let t = (Vec3::Z - camera_transform.translation).dot(n_p) / denom; + return Some(camera_transform.translation + t * camera_transform.forward()); + } +} + fn camera_controls( cursor_command: ResMut, mut controls: ResMut, @@ -417,6 +426,14 @@ fn camera_controls( // Ensure upright let forward = persp_transform.forward(); persp_transform.look_to(forward, Vec3::Z); + + // If pan operation, redefine the orbit center as the point on the groundplane in camera center + if cursor_command.command_type == CameraCommandType::Pan { + let lateral_translation = cursor_command.translation_delta - cursor_command.translation_delta.project_onto(Vec3::Y); + controls.orbit_center = get_camera_selected_point(&persp_transform) + .unwrap_or(controls.orbit_center + lateral_translation); + controls.orbit_radius = (persp_transform.translation - controls.orbit_center).length(); + } } let proj = persp_proj.clone(); From e6fd8a37ca54a50e593ee6d566595248c53d5bb6 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 3 Jun 2024 16:25:19 +0800 Subject: [PATCH 11/32] fix styling, stability when camera horizontal Signed-off-by: Reuben Thomas fix fov clamp remove print Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 20 +++++++++------- .../src/interaction/camera_controls/mod.rs | 24 +++++++++++++------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 70d41457..f8ff14f0 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -274,13 +274,14 @@ fn get_perspective_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - // Adjust orbit to the window size // TODO(@reuben-thomas) also adjust to fov let window_size = Vec2::new(window.width(), window.height()); let orbit_sensitivity = 1.0; - let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; - let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; + let delta_x = + cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; + let delta_y = + cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; let yaw = Quat::from_rotation_z(-delta_x); let pitch = Quat::from_rotation_x(-delta_y); @@ -294,13 +295,15 @@ fn get_perspective_cursor_command( // Calculate translation to orbit around camera centre let orbit_radius = orbit_radius - 0.5 * scroll_motion; let target_rotation = Mat3::from_quat(target_transform.rotation); - target_transform.translation = orbit_center - + target_rotation.mul_vec3(Vec3::new(0.0, 0.0, orbit_radius)); - + target_transform.translation = + orbit_center + target_rotation.mul_vec3(Vec3::new(0.0, 0.0, orbit_radius)); + // Get the rotation difference to be multiplied into the current rotation let start_rotation = Mat3::from_quat(camera_transform.rotation); - cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - cursor_command.translation_delta = target_transform.translation - camera_transform.translation; + cursor_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + cursor_command.translation_delta = + target_transform.translation - camera_transform.translation; is_cursor_selecting = true; } CameraCommandType::Inactive => (), @@ -316,7 +319,6 @@ fn get_perspective_cursor_command( return cursor_command; } - // Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { let cursor_ray = cursor_raycast_source.get_ray().unwrap(); diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 9b9fcdd3..4ee70c31 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -17,9 +17,10 @@ use crate::interaction::PickingBlockers; use bevy::{ - core_pipeline::clear_color::ClearColorConfig, - core_pipeline::core_3d::Camera3dBundle, - core_pipeline::tonemapping::Tonemapping, + core_pipeline::{ + clear_color::ClearColorConfig, core_3d::Camera3dBundle, tonemapping::Tonemapping, + }, + pbr::deferred::prepare_deferred_lighting_pipelines, prelude::*, render::{ camera::{Camera, Projection, ScalingMode}, @@ -55,6 +56,10 @@ pub const XRAY_RENDER_LAYER: u8 = 5; /// models in the engine without having them being visible to general cameras pub const MODEL_PREVIEW_LAYER: u8 = 6; +/// Camera limits +pub const CAMERA_MIN_FOV: f32 = 5.0; +pub const CAMERA_MAX_FOV: f32 = 120.0; + #[derive(PartialEq, Debug, Copy, Clone)] pub enum CameraCommandType { Inactive, @@ -378,8 +383,8 @@ fn get_camera_selected_point(camera_transform: &Transform) -> Option { let n_p = Vec3::Z; let n_r = camera_transform.forward(); let denom = n_p.dot(n_r); - if denom > 0.1 { - return None + if denom > 1e3 { + return None; } else { let t = (Vec3::Z - camera_transform.translation).dot(n_p) / denom; return Some(camera_transform.translation + t * camera_transform.forward()); @@ -422,6 +427,9 @@ fn camera_controls( persp_transform.translation += cursor_command.translation_delta; persp_transform.rotation *= cursor_command.rotation_delta; persp_proj.fov += cursor_command.fov_delta; + persp_proj.fov = persp_proj + .fov + .clamp(CAMERA_MIN_FOV.to_radians(), CAMERA_MAX_FOV.to_radians()); // Ensure upright let forward = persp_transform.forward(); @@ -429,10 +437,12 @@ fn camera_controls( // If pan operation, redefine the orbit center as the point on the groundplane in camera center if cursor_command.command_type == CameraCommandType::Pan { - let lateral_translation = cursor_command.translation_delta - cursor_command.translation_delta.project_onto(Vec3::Y); + let lateral_translation = cursor_command.translation_delta + - cursor_command.translation_delta.project_onto(Vec3::Y); controls.orbit_center = get_camera_selected_point(&persp_transform) .unwrap_or(controls.orbit_center + lateral_translation); - controls.orbit_radius = (persp_transform.translation - controls.orbit_center).length(); + controls.orbit_radius = + (persp_transform.translation - controls.orbit_center).length(); } } From addc6137c2b0fd120b1174e3f5ce78cf5c2bf1df Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Wed, 5 Jun 2024 22:14:22 +0800 Subject: [PATCH 12/32] fix stability near small angles Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 33 ++++++++---- .../src/interaction/camera_controls/mod.rs | 54 ++++++++++++------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index f8ff14f0..87c9cf33 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -15,7 +15,7 @@ * */ -use super::{CameraCommandType, CameraControls, ProjectionMode}; +use super::{CameraCommandType, CameraControls, ProjectionMode, MIN_SELECTION_ANGLE}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; @@ -61,6 +61,12 @@ pub fn update_cursor_command( primary_windows: Query<&Window, With>, ) { if let Ok(window) = primary_windows.get_single() { + // Return if cursor not within window + if window.cursor_position().is_none() { + *cursor_command = CursorCommand::default(); + return; + } + // Cursor and scroll inputs let cursor_motion = mouse_motion .read() @@ -113,7 +119,6 @@ pub fn update_cursor_command( }; let cursor_direction = cursor_ray.direction().normalize(); - // 4. Perspective Mode *cursor_command = match camera_controls.mode() { ProjectionMode::Perspective => get_perspective_cursor_command( &camera_transform, @@ -322,19 +327,25 @@ fn get_perspective_cursor_command( // Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { let cursor_ray = cursor_raycast_source.get_ray().unwrap(); - let default_dist = 100.0; + match cursor_raycast_source.get_nearest_intersection() { Some((_, intersection)) => intersection.position(), None => { - let n_p = Vec3::Z; - let n_r = cursor_ray.direction(); - let denom = n_p.dot(n_r); - if denom > 1e-3 { - cursor_ray.origin() + (default_dist * cursor_ray.direction()) - } else { - let t = (Vec3::Z - cursor_ray.origin()).dot(n_p) / denom; - cursor_ray.origin() + t * cursor_ray.direction() + // If valid intersection with groundplane + let pitch = cursor_ray.direction().z.acos().to_degrees() - 90.0; + let denom = Vec3::Z.dot(cursor_ray.direction()); + if denom.abs() > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { + let dist = (-1.0 * cursor_ray.origin()).dot(Vec3::Z) / denom; + if dist > f32::EPSILON { + return cursor_ray.origin() + cursor_ray.direction() * dist; + } } + + // No groundplane intersection + // Pick a point of a virtual sphere around the camera, of same radius as its height + let height = cursor_ray.origin().y.abs(); + let radius = if height < 1.0 { 1.0 } else { height }; + return cursor_ray.origin() + cursor_ray.direction() * radius; } } } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 4ee70c31..651be89c 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -57,8 +57,9 @@ pub const XRAY_RENDER_LAYER: u8 = 5; pub const MODEL_PREVIEW_LAYER: u8 = 6; /// Camera limits -pub const CAMERA_MIN_FOV: f32 = 5.0; -pub const CAMERA_MAX_FOV: f32 = 120.0; +pub const MIN_FOV: f32 = 5.0; +pub const MAX_FOV: f32 = 120.0; +pub const MIN_SELECTION_ANGLE: f32 = 10.0; #[derive(PartialEq, Debug, Copy, Clone)] pub enum CameraCommandType { @@ -380,15 +381,15 @@ impl FromWorld for CameraControls { // Get groundplane intersection of camera direction fn get_camera_selected_point(camera_transform: &Transform) -> Option { - let n_p = Vec3::Z; - let n_r = camera_transform.forward(); - let denom = n_p.dot(n_r); - if denom > 1e3 { - return None; - } else { - let t = (Vec3::Z - camera_transform.translation).dot(n_p) / denom; - return Some(camera_transform.translation + t * camera_transform.forward()); + let pitch = camera_transform.forward().z.acos().to_degrees() - 90.0; + let denom = Vec3::Z.dot(camera_transform.forward()); + if denom.abs() > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { + let dist = (-1.0 * camera_transform.translation).dot(Vec3::Z) / denom; + if dist > f32::EPSILON { + return Some(camera_transform.translation + dist * camera_transform.forward()); + } } + return None; } fn camera_controls( @@ -429,20 +430,35 @@ fn camera_controls( persp_proj.fov += cursor_command.fov_delta; persp_proj.fov = persp_proj .fov - .clamp(CAMERA_MIN_FOV.to_radians(), CAMERA_MAX_FOV.to_radians()); + .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); // Ensure upright let forward = persp_transform.forward(); persp_transform.look_to(forward, Vec3::Z); - // If pan operation, redefine the orbit center as the point on the groundplane in camera center - if cursor_command.command_type == CameraCommandType::Pan { - let lateral_translation = cursor_command.translation_delta - - cursor_command.translation_delta.project_onto(Vec3::Y); - controls.orbit_center = get_camera_selected_point(&persp_transform) - .unwrap_or(controls.orbit_center + lateral_translation); - controls.orbit_radius = - (persp_transform.translation - controls.orbit_center).length(); + // Update orbit center and radius if camera translates + if cursor_command.command_type == CameraCommandType::Pan || cursor_command.command_type == CameraCommandType::TranslationZoom { + let is_facing_groundplane = persp_transform.forward().z.signum() == -persp_transform.translation.z.signum(); + let denom = Vec3::Z.dot(persp_transform.forward()); + if is_facing_groundplane && denom.abs() > f32::EPSILON { + let pitch = persp_transform.forward().z.acos().to_degrees() - 90.0; + let dist = (-1.0 * persp_transform.translation).dot(Vec3::Z) / denom; + if dist > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { + // println!("looking"); + controls.orbit_center = persp_transform.translation + dist * persp_transform.forward(); + } else { + // println!("translated"); + //TODO Translation above groundplane results in a jump + // relationship between vertical translation and orbit not well defined + controls.orbit_center += cursor_command.translation_delta - cursor_command.translation_delta.project_onto(Vec3::Z); + } + controls.orbit_radius = (persp_transform.translation - controls.orbit_center).length(); + } else { + // If camera not looking at groundplane, orbit around point 1m ahead + // println!("orbiting around 1m ahead"); + controls.orbit_center = persp_transform.translation + persp_transform.forward() * 1.0; + controls.orbit_radius = 1.0; + } } } From a8b04afd2573ca4cc4692ffa69a6a5b236a9c412 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 10 Jun 2024 00:54:32 +0800 Subject: [PATCH 13/32] Selectable orbit center Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 110 ++++++++++++------ .../src/interaction/camera_controls/mod.rs | 74 +++++------- 2 files changed, 104 insertions(+), 80 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 87c9cf33..b5f47af4 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -15,7 +15,7 @@ * */ -use super::{CameraCommandType, CameraControls, ProjectionMode, MIN_SELECTION_ANGLE}; +use super::{CameraCommandType, CameraControls, ProjectionMode, MAX_PITCH, MAX_SELECTION_DIST}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; @@ -31,7 +31,6 @@ pub struct CursorCommand { pub scale_delta: f32, pub fov_delta: f32, pub cursor_selection: Option, - pub camera_selection: Vec3, pub command_type: CameraCommandType, } @@ -43,14 +42,13 @@ impl Default for CursorCommand { scale_delta: 0.0, fov_delta: 0.0, cursor_selection: None, - camera_selection: Vec3::ZERO, command_type: CameraCommandType::Inactive, } } } pub fn update_cursor_command( - camera_controls: Res, + mut camera_controls: ResMut, mut cursor_command: ResMut, mut mouse_motion: EventReader, mut mouse_wheel: EventReader, @@ -91,6 +89,8 @@ pub fn update_cursor_command( &mouse_input, &cursor_motion, &scroll_motion, + cursor_command.command_type, + camera_controls.mode(), ); if command_type == CameraCommandType::Inactive { *cursor_command = CursorCommand::default(); @@ -119,6 +119,19 @@ pub fn update_cursor_command( }; let cursor_direction = cursor_ray.direction().normalize(); + // Update obrit center if this is a select / deselect operation + if command_type == CameraCommandType::SelectOrbitCenter { + camera_controls.orbit_center = Some(cursor_selection_new); + *cursor_command = CursorCommand::default(); + cursor_command.command_type = CameraCommandType::SelectOrbitCenter; + return; + } else if command_type == CameraCommandType::DeselectOrbitCenter { + camera_controls.orbit_center = None; + *cursor_command = CursorCommand::default(); + cursor_command.command_type = CameraCommandType::DeselectOrbitCenter; + return; + } + *cursor_command = match camera_controls.mode() { ProjectionMode::Perspective => get_perspective_cursor_command( &camera_transform, @@ -127,7 +140,6 @@ pub fn update_cursor_command( cursor_selection, cursor_motion, camera_controls.orbit_center, - camera_controls.orbit_radius, scroll_motion, window, ), @@ -224,26 +236,24 @@ fn get_perspective_cursor_command( cursor_direction: Vec3, cursor_selection: Vec3, cursor_motion: Vec2, - orbit_center: Vec3, - orbit_radius: f32, + orbit_center: Option, scroll_motion: f32, window: &Window, ) -> CursorCommand { - // Zoom towards the cursor if zooming only, otherwize zoom to center - let zoom_translation = match command_type { - CameraCommandType::TranslationZoom => cursor_direction * 0.5 * scroll_motion, - _ => camera_transform.forward() * scroll_motion, - }; + let translation_zoom_sensitivity = 0.5; + let fov_zoom_sensitivity = 0.1; + let orbit_sensitivity = 1.0; let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; match command_type { CameraCommandType::FovZoom => { - cursor_command.fov_delta = -scroll_motion * 0.1; + cursor_command.fov_delta = -scroll_motion * fov_zoom_sensitivity; } CameraCommandType::TranslationZoom => { - cursor_command.translation_delta = cursor_direction * 0.5 * scroll_motion; + cursor_command.translation_delta = + cursor_direction * translation_zoom_sensitivity * scroll_motion; } CameraCommandType::Pan => { // To keep the same point below the cursor, we solve @@ -273,6 +283,9 @@ fn get_perspective_cursor_command( ); let x = a.lu().solve(&b).unwrap(); + let zoom_translation = + camera_transform.forward() * translation_zoom_sensitivity * scroll_motion; + cursor_command.translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; cursor_command.rotation_delta = Quat::IDENTITY; @@ -282,36 +295,46 @@ fn get_perspective_cursor_command( // Adjust orbit to the window size // TODO(@reuben-thomas) also adjust to fov let window_size = Vec2::new(window.width(), window.height()); - let orbit_sensitivity = 1.0; let delta_x = cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; - let yaw = Quat::from_rotation_z(-delta_x); - let pitch = Quat::from_rotation_x(-delta_y); + let yaw = Quat::from_rotation_z(delta_x); + let pitch = Quat::from_rotation_x(delta_y); - // Get target rotation, excluding pitch if upside down let mut target_transform = camera_transform.clone(); + // Exclude pitch if exceeds maximum angle target_transform.rotation = (yaw * camera_transform.rotation) * pitch; - if target_transform.up().dot(Vec3::Z) <= 0.0 { + if target_transform.up().z.acos().to_degrees() > MAX_PITCH { target_transform.rotation = yaw * camera_transform.rotation; }; - // Calculate translation to orbit around camera centre - let orbit_radius = orbit_radius - 0.5 * scroll_motion; - let target_rotation = Mat3::from_quat(target_transform.rotation); - target_transform.translation = - orbit_center + target_rotation.mul_vec3(Vec3::new(0.0, 0.0, orbit_radius)); + // Translation if orbitting a point + if let Some(orbit_center) = orbit_center { + let camera_to_orbit_center = orbit_center - camera_transform.translation; + let x = camera_to_orbit_center.dot(camera_transform.local_x()); + let y = camera_to_orbit_center.dot(camera_transform.local_y()); + let z = camera_to_orbit_center.dot(camera_transform.local_z()); + let camera_to_orbit_center_next = target_transform.local_x() * x + + target_transform.local_y() * y + + target_transform.local_z() * z; + + let zoom_translation = camera_to_orbit_center_next.normalize() + * translation_zoom_sensitivity + * scroll_motion; + target_transform.translation = + orbit_center - camera_to_orbit_center_next - zoom_translation; + } - // Get the rotation difference to be multiplied into the current rotation + cursor_command.translation_delta = + target_transform.translation - camera_transform.translation; let start_rotation = Mat3::from_quat(camera_transform.rotation); + let target_rotation = Mat3::from_quat(target_transform.rotation); cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - cursor_command.translation_delta = - target_transform.translation - camera_transform.translation; is_cursor_selecting = true; } - CameraCommandType::Inactive => (), + _ => (), } cursor_command.command_type = command_type; @@ -332,11 +355,10 @@ fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource intersection.position(), None => { // If valid intersection with groundplane - let pitch = cursor_ray.direction().z.acos().to_degrees() - 90.0; let denom = Vec3::Z.dot(cursor_ray.direction()); - if denom.abs() > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { + if denom.abs() > f32::EPSILON { let dist = (-1.0 * cursor_ray.origin()).dot(Vec3::Z) / denom; - if dist > f32::EPSILON { + if dist > f32::EPSILON && dist < MAX_SELECTION_DIST { return cursor_ray.origin() + cursor_ray.direction() * dist; } } @@ -355,6 +377,8 @@ fn get_command_type( mouse_input: &Res>, cursor_motion: &Vec2, scroll_motion: &f32, + prev_command_type: CameraCommandType, + projection_mode: ProjectionMode, ) -> CameraCommandType { // Inputs let is_cursor_moving = cursor_motion.length() > 0.; @@ -375,10 +399,26 @@ fn get_command_type( } // Zoom - if is_scrolling && is_shifting { - return CameraCommandType::FovZoom; - } else if is_scrolling { - return CameraCommandType::TranslationZoom; + if projection_mode.is_orthographic() && is_scrolling { + return CameraCommandType::ScaleZoom; + } + if projection_mode.is_perspective() && is_scrolling { + if is_shifting { + return CameraCommandType::FovZoom; + } else { + return CameraCommandType::TranslationZoom; + } + } + + // Select / Deselect Orbit Center + if projection_mode.is_perspective() { + if mouse_input.just_pressed(MouseButton::Right) + && prev_command_type == CameraCommandType::Inactive + { + return CameraCommandType::SelectOrbitCenter; + } else if keyboard_input.just_pressed(KeyCode::Escape) { + return CameraCommandType::DeselectOrbitCenter; + } } return CameraCommandType::Inactive; diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 651be89c..8e198755 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -20,7 +20,6 @@ use bevy::{ core_pipeline::{ clear_color::ClearColorConfig, core_3d::Camera3dBundle, tonemapping::Tonemapping, }, - pbr::deferred::prepare_deferred_lighting_pipelines, prelude::*, render::{ camera::{Camera, Projection, ScalingMode}, @@ -59,7 +58,8 @@ pub const MODEL_PREVIEW_LAYER: u8 = 6; /// Camera limits pub const MIN_FOV: f32 = 5.0; pub const MAX_FOV: f32 = 120.0; -pub const MIN_SELECTION_ANGLE: f32 = 10.0; +pub const MAX_PITCH: f32 = 85.0; +pub const MAX_SELECTION_DIST: f32 = 100.0; #[derive(PartialEq, Debug, Copy, Clone)] pub enum CameraCommandType { @@ -67,9 +67,15 @@ pub enum CameraCommandType { Pan, Orbit, TranslationZoom, + ScaleZoom, FovZoom, + SelectOrbitCenter, + DeselectOrbitCenter, } +#[derive(Default, Reflect)] +struct OrbitCenterGizmo {} + #[derive(PartialEq, Debug, Copy, Clone, Reflect, Resource)] pub enum ProjectionMode { Perspective, @@ -106,8 +112,7 @@ pub struct CameraControls { pub perspective_headlight: Entity, pub orthographic_camera_entities: [Entity; 4], pub orthographic_headlight: Entity, - pub orbit_center: Vec3, - pub orbit_radius: f32, + pub orbit_center: Option, } /// True/false for whether the headlight should be on or off @@ -373,25 +378,11 @@ impl FromWorld for CameraControls { orthographic_child_cameras[2], ], orthographic_headlight, - orbit_center: Vec3::ZERO, - orbit_radius: (3.0 * 10.0 * 10.0 as f32).sqrt(), + orbit_center: None, } } } -// Get groundplane intersection of camera direction -fn get_camera_selected_point(camera_transform: &Transform) -> Option { - let pitch = camera_transform.forward().z.acos().to_degrees() - 90.0; - let denom = Vec3::Z.dot(camera_transform.forward()); - if denom.abs() > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { - let dist = (-1.0 * camera_transform.translation).dot(Vec3::Z) / denom; - if dist > f32::EPSILON { - return Some(camera_transform.translation + dist * camera_transform.forward()); - } - } - return None; -} - fn camera_controls( cursor_command: ResMut, mut controls: ResMut, @@ -435,31 +426,6 @@ fn camera_controls( // Ensure upright let forward = persp_transform.forward(); persp_transform.look_to(forward, Vec3::Z); - - // Update orbit center and radius if camera translates - if cursor_command.command_type == CameraCommandType::Pan || cursor_command.command_type == CameraCommandType::TranslationZoom { - let is_facing_groundplane = persp_transform.forward().z.signum() == -persp_transform.translation.z.signum(); - let denom = Vec3::Z.dot(persp_transform.forward()); - if is_facing_groundplane && denom.abs() > f32::EPSILON { - let pitch = persp_transform.forward().z.acos().to_degrees() - 90.0; - let dist = (-1.0 * persp_transform.translation).dot(Vec3::Z) / denom; - if dist > f32::EPSILON && pitch.abs() >= MIN_SELECTION_ANGLE { - // println!("looking"); - controls.orbit_center = persp_transform.translation + dist * persp_transform.forward(); - } else { - // println!("translated"); - //TODO Translation above groundplane results in a jump - // relationship between vertical translation and orbit not well defined - controls.orbit_center += cursor_command.translation_delta - cursor_command.translation_delta.project_onto(Vec3::Z); - } - controls.orbit_radius = (persp_transform.translation - controls.orbit_center).length(); - } else { - // If camera not looking at groundplane, orbit around point 1m ahead - // println!("orbiting around 1m ahead"); - controls.orbit_center = persp_transform.translation + persp_transform.forward() * 1.0; - controls.orbit_radius = 1.0; - } - } } let proj = persp_proj.clone(); @@ -491,6 +457,23 @@ fn camera_controls( } } +// TODO(@reuben-thomas) Should be moved to material supporting depth bias +fn update_orbit_center_marker( + controls: Res, + cursor_command: Res, + mut gizmo: Gizmos, +) { + if let Some(orbit_center) = controls.orbit_center { + let color = match cursor_command.command_type { + CameraCommandType::SelectOrbitCenter => Color::BLUE, + CameraCommandType::DeselectOrbitCenter => Color::RED, + CameraCommandType::Orbit => Color::WHITE, + _ => Color::GRAY, + }; + gizmo.sphere(orbit_center, Quat::IDENTITY, 0.1, color); + } +} + pub struct CameraControlsPlugin; impl Plugin for CameraControlsPlugin { @@ -500,6 +483,7 @@ impl Plugin for CameraControlsPlugin { .init_resource::() .add_event::() .add_systems(Update, update_cursor_command) - .add_systems(Update, camera_controls); + .add_systems(Update, camera_controls) + .add_systems(Update, update_orbit_center_marker); } } From 593a7af519c97e653271d0cdc88ee44a749a1b20 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 10 Jun 2024 16:01:22 +0800 Subject: [PATCH 14/32] Add marker with depth bias Signed-off-by: Reuben Thomas --- rmf_site_editor/src/interaction/assets.rs | 28 +++++++- .../src/interaction/camera_controls/cursor.rs | 1 - .../src/interaction/camera_controls/mod.rs | 64 +++++++++++++++---- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/rmf_site_editor/src/interaction/assets.rs b/rmf_site_editor/src/interaction/assets.rs index 432599d2..8b522e13 100644 --- a/rmf_site_editor/src/interaction/assets.rs +++ b/rmf_site_editor/src/interaction/assets.rs @@ -16,7 +16,7 @@ */ use crate::{interaction::*, shapes::*}; -use bevy::{math::Affine3A, prelude::*}; +use bevy::{math::Affine3A, prelude::*, render::mesh::shape::Torus}; use bevy_polyline::{ material::PolylineMaterial, polyline::{Polyline, PolylineBundle}, @@ -28,6 +28,9 @@ pub struct InteractionAssets { pub dagger_material: Handle, pub halo_mesh: Handle, pub halo_material: Handle, + pub orbit_center_mesh: Handle, + pub orbit_center_active_material: Handle, + pub orbit_center_inactive_material: Handle, pub arrow_mesh: Handle, pub point_light_socket_mesh: Handle, pub point_light_shine_mesh: Handle, @@ -224,6 +227,11 @@ impl FromWorld for InteractionAssets { let mut meshes = world.get_resource_mut::>().unwrap(); let dagger_mesh = meshes.add(make_dagger_mesh()); let halo_mesh = meshes.add(make_halo_mesh()); + let orbit_center_mesh = meshes.add(Mesh::from(Torus { + radius: 0.1, + ring_radius: 0.03, + ..Default::default() + })); let arrow_mesh = meshes.add(make_cylinder_arrow_mesh()); let point_light_socket_mesh = meshes.add( make_cylinder(0.06, 0.02) @@ -298,10 +306,25 @@ impl FromWorld for InteractionAssets { }); let dagger_material = materials.add(StandardMaterial { base_color: Color::WHITE, + emissive: Color::WHITE, perceptual_roughness: 0.089, metallic: 0.01, ..default() }); + let orbit_center_active_material = materials.add(StandardMaterial { + base_color: Color::GREEN, + emissive: Color::GREEN, + depth_bias: f32::MAX, + unlit: true, + ..default() + }); + let orbit_center_inactive_material = materials.add(StandardMaterial { + base_color: Color::WHITE, + emissive: Color::WHITE, + // depth_bias: f32::MAX, + unlit: true, + ..default() + }); let light_cover_color = Color::rgb(0.6, 0.7, 0.8); let physical_light_cover_material = materials.add(StandardMaterial { base_color: light_cover_color, @@ -401,6 +424,9 @@ impl FromWorld for InteractionAssets { dagger_material, halo_mesh, halo_material, + orbit_center_mesh, + orbit_center_active_material, + orbit_center_inactive_material, arrow_mesh, point_light_socket_mesh, point_light_shine_mesh, diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index b5f47af4..4c39550e 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -19,7 +19,6 @@ use super::{CameraCommandType, CameraControls, ProjectionMode, MAX_PITCH, MAX_SE use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; -use bevy::render::camera; use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 8e198755..d862f397 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -14,8 +14,7 @@ * limitations under the License. * */ - -use crate::interaction::PickingBlockers; +use crate::interaction::{InteractionAssets, PickingBlockers}; use bevy::{ core_pipeline::{ clear_color::ClearColorConfig, core_3d::Camera3dBundle, tonemapping::Tonemapping, @@ -112,6 +111,7 @@ pub struct CameraControls { pub perspective_headlight: Entity, pub orthographic_camera_entities: [Entity; 4], pub orthographic_headlight: Entity, + pub orbit_center_marker: Entity, pub orbit_center: Option, } @@ -233,6 +233,18 @@ impl CameraControls { impl FromWorld for CameraControls { fn from_world(world: &mut World) -> Self { + let interaction_assets = world.get_resource::().expect( + "make sure that the InteractionAssets resource is initialized before the camera plugin", + ); + let orbit_center_mesh = interaction_assets.orbit_center_mesh.clone(); + let orbit_center_marker = world + .spawn(PbrBundle { + mesh: orbit_center_mesh, + visibility: Visibility::Visible, + ..default() + }) + .id(); + let perspective_headlight = world .spawn(DirectionalLightBundle { directional_light: DirectionalLight { @@ -378,6 +390,7 @@ impl FromWorld for CameraControls { orthographic_child_cameras[2], ], orthographic_headlight, + orbit_center_marker, orbit_center: None, } } @@ -457,20 +470,44 @@ fn camera_controls( } } -// TODO(@reuben-thomas) Should be moved to material supporting depth bias fn update_orbit_center_marker( controls: Res, cursor_command: Res, - mut gizmo: Gizmos, + interaction_assets: Res, + camera_query: Query<&Transform, With>, + mut marker_query: Query< + ( + &mut Transform, + &mut Visibility, + &mut Handle, + ), + Without, + >, ) { - if let Some(orbit_center) = controls.orbit_center { - let color = match cursor_command.command_type { - CameraCommandType::SelectOrbitCenter => Color::BLUE, - CameraCommandType::DeselectOrbitCenter => Color::RED, - CameraCommandType::Orbit => Color::WHITE, - _ => Color::GRAY, - }; - gizmo.sphere(orbit_center, Quat::IDENTITY, 0.1, color); + if let Ok((mut marker_transform, mut marker_visibility, mut marker_material)) = + marker_query.get_mut(controls.orbit_center_marker) + { + if let Some(orbit_center) = controls.orbit_center { + *marker_material = match cursor_command.command_type { + CameraCommandType::DeselectOrbitCenter => { + interaction_assets.orbit_center_active_material.clone() + } + CameraCommandType::SelectOrbitCenter => { + interaction_assets.orbit_center_active_material.clone() + } + CameraCommandType::Orbit => interaction_assets.orbit_center_active_material.clone(), + _ => interaction_assets.orbit_center_inactive_material.clone(), + }; + + let camera_transform = camera_query.get(controls.active_camera()).unwrap(); + let camera_to_orbit_dir = (orbit_center - camera_transform.translation).normalize(); + marker_transform.translation = orbit_center; + marker_transform.rotation = Quat::from_rotation_arc(Vec3::Y, camera_to_orbit_dir); + + *marker_visibility = Visibility::Visible; + } else { + *marker_visibility = Visibility::Hidden; + } } } @@ -482,8 +519,7 @@ impl Plugin for CameraControlsPlugin { .init_resource::() .init_resource::() .add_event::() - .add_systems(Update, update_cursor_command) - .add_systems(Update, camera_controls) + .add_systems(Update, (update_cursor_command, camera_controls).chain()) .add_systems(Update, update_orbit_center_marker); } } From b0cd3222bde6736919b5c5e26bd15a8ba5ad17b9 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Tue, 11 Jun 2024 09:03:27 +0800 Subject: [PATCH 15/32] add wireframe sphere and persistent dot as orbit marker Signed-off-by: Reuben Thomas --- rmf_site_editor/src/interaction/assets.rs | 9 ++--- .../src/interaction/camera_controls/mod.rs | 38 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/rmf_site_editor/src/interaction/assets.rs b/rmf_site_editor/src/interaction/assets.rs index 8b522e13..0d36a4b9 100644 --- a/rmf_site_editor/src/interaction/assets.rs +++ b/rmf_site_editor/src/interaction/assets.rs @@ -16,11 +16,12 @@ */ use crate::{interaction::*, shapes::*}; -use bevy::{math::Affine3A, prelude::*, render::mesh::shape::Torus}; +use bevy::{math::Affine3A, prelude::*}; use bevy_polyline::{ material::PolylineMaterial, polyline::{Polyline, PolylineBundle}, }; +use shape::UVSphere; #[derive(Clone, Debug, Resource)] pub struct InteractionAssets { @@ -227,9 +228,8 @@ impl FromWorld for InteractionAssets { let mut meshes = world.get_resource_mut::>().unwrap(); let dagger_mesh = meshes.add(make_dagger_mesh()); let halo_mesh = meshes.add(make_halo_mesh()); - let orbit_center_mesh = meshes.add(Mesh::from(Torus { - radius: 0.1, - ring_radius: 0.03, + let orbit_center_mesh = meshes.add(Mesh::from(UVSphere { + radius: 0.02, ..Default::default() })); let arrow_mesh = meshes.add(make_cylinder_arrow_mesh()); @@ -321,7 +321,6 @@ impl FromWorld for InteractionAssets { let orbit_center_inactive_material = materials.add(StandardMaterial { base_color: Color::WHITE, emissive: Color::WHITE, - // depth_bias: f32::MAX, unlit: true, ..default() }); diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index d862f397..192ebe5b 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -474,36 +474,34 @@ fn update_orbit_center_marker( controls: Res, cursor_command: Res, interaction_assets: Res, - camera_query: Query<&Transform, With>, - mut marker_query: Query< - ( - &mut Transform, - &mut Visibility, - &mut Handle, - ), - Without, - >, + mut gizmo: Gizmos, + mut marker_query: Query<( + &mut Transform, + &mut Visibility, + &mut Handle, + )>, ) { if let Ok((mut marker_transform, mut marker_visibility, mut marker_material)) = marker_query.get_mut(controls.orbit_center_marker) { if let Some(orbit_center) = controls.orbit_center { - *marker_material = match cursor_command.command_type { - CameraCommandType::DeselectOrbitCenter => { - interaction_assets.orbit_center_active_material.clone() + let sphere_color: Color; + match cursor_command.command_type { + CameraCommandType::Orbit + | CameraCommandType::DeselectOrbitCenter + | CameraCommandType::SelectOrbitCenter => { + *marker_material = interaction_assets.orbit_center_active_material.clone(); + sphere_color = Color::GREEN; } - CameraCommandType::SelectOrbitCenter => { - interaction_assets.orbit_center_active_material.clone() + _ => { + *marker_material = interaction_assets.orbit_center_inactive_material.clone(); + sphere_color = Color::WHITE; } - CameraCommandType::Orbit => interaction_assets.orbit_center_active_material.clone(), - _ => interaction_assets.orbit_center_inactive_material.clone(), }; - let camera_transform = camera_query.get(controls.active_camera()).unwrap(); - let camera_to_orbit_dir = (orbit_center - camera_transform.translation).normalize(); + //TODO(@reuben-thomas) scale to be of constant size in camera + gizmo.sphere(orbit_center, Quat::IDENTITY, 0.1, sphere_color); marker_transform.translation = orbit_center; - marker_transform.rotation = Quat::from_rotation_arc(Vec3::Y, camera_to_orbit_dir); - *marker_visibility = Visibility::Visible; } else { *marker_visibility = Visibility::Hidden; From 387f6b3f782383d8d54f53de2c8dd741224b1b05 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Tue, 11 Jun 2024 11:03:28 +0800 Subject: [PATCH 16/32] keyboard camera control prototype Signed-off-by: Reuben Thomas --- .../interaction/camera_controls/keyboard.rs | 267 ++++++++++++++++++ .../src/interaction/camera_controls/mod.rs | 45 ++- 2 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 rmf_site_editor/src/interaction/camera_controls/keyboard.rs diff --git a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs new file mode 100644 index 00000000..358ecb26 --- /dev/null +++ b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use super::{CameraCommandType, CameraControls, CursorCommand, ProjectionMode}; +use bevy::{prelude::*, window::PrimaryWindow}; + +#[derive(Resource)] +pub struct KeyboardCommand { + pub translation_delta: Vec3, + pub rotation_delta: Quat, + pub scale_delta: f32, + pub fov_delta: f32, + pub keyboard_motion: Vec2, + pub zoom_motion: f32, + pub command_type: CameraCommandType, +} + +impl Default for KeyboardCommand { + fn default() -> Self { + Self { + translation_delta: Vec3::ZERO, + rotation_delta: Quat::IDENTITY, + scale_delta: 0.0, + fov_delta: 0.0, + keyboard_motion: Vec2::ZERO, + zoom_motion: 0.0, + command_type: CameraCommandType::Inactive, + } + } +} + +pub fn update_keyboard_command( + camera_controls: Res, + cursor_command: ResMut, + mut keyboard_command: ResMut, + keyboard_input: Res>, + cameras: Query<(&Projection, &Transform)>, + primary_windows: Query<&Window, With>, +) { + if let Ok(window) = primary_windows.get_single() { + // User inputs + let is_shifting = keyboard_input.pressed(KeyCode::ShiftLeft) + || keyboard_input.pressed(KeyCode::ShiftRight); + let mut target_keyboard_motion = Vec2::ZERO; + if keyboard_input.pressed(KeyCode::W) { + target_keyboard_motion.y += 1.0; + } + if keyboard_input.pressed(KeyCode::A) { + target_keyboard_motion.x += -1.0; + } + if keyboard_input.pressed(KeyCode::S) { + target_keyboard_motion.y += -1.0; + } + if keyboard_input.pressed(KeyCode::D) { + target_keyboard_motion.x += 1.0; + } + if target_keyboard_motion.length() > 0.0 { + target_keyboard_motion = target_keyboard_motion.normalize(); + } + + let mut target_zoom_motion = 0.0; + if keyboard_input.pressed(KeyCode::Q) { + target_zoom_motion += -1.0; + } + if keyboard_input.pressed(KeyCode::E) { + target_zoom_motion += 1.0; + } + + // Smoothen motion using current state + // (1 / reponse_factor) frames = number of frames to reach maximum velocity + let response_factor = 0.05; + let prev_keyboard_motion = keyboard_command.keyboard_motion; + let mut keyboard_motion = prev_keyboard_motion + + (target_keyboard_motion - prev_keyboard_motion).normalize_or_zero() * response_factor; + if keyboard_motion.length() > 1.0 { + keyboard_motion = keyboard_motion.normalize(); + } else if keyboard_motion.length() < 0.1 { + keyboard_motion = Vec2::ZERO; + } + + let prev_zoom_motion = keyboard_command.zoom_motion; + let mut zoom_motion = + prev_zoom_motion + (target_zoom_motion - prev_zoom_motion).signum() * response_factor; + if zoom_motion.abs() > 1.0 { + zoom_motion = zoom_motion.signum(); + } else if zoom_motion.abs() < 0.1 { + zoom_motion = 0.0; + } + + // Get command type + let command_type = if is_shifting && keyboard_motion.length() > 0.0 { + CameraCommandType::Orbit + } else if keyboard_motion.length() > 0.0 { + CameraCommandType::Pan + } else if is_shifting && zoom_motion != 0.0 { + CameraCommandType::FovZoom + } else if zoom_motion != 0.0 { + CameraCommandType::TranslationZoom + } else { + CameraCommandType::Inactive + }; + + if command_type != keyboard_command.command_type + && command_type != CameraCommandType::Inactive + {} + { + zoom_motion = response_factor * target_zoom_motion; + keyboard_motion = response_factor * target_keyboard_motion; + } + + // Camera projection and transform + let active_camera_entity = match camera_controls.mode() { + ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], + ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], + }; + let (camera_proj, camera_transform) = cameras.get(active_camera_entity).unwrap(); + + // Orthographic + match camera_controls.mode() { + ProjectionMode::Orthographic => { + *keyboard_command = update_orthographic_command( + command_type, + &camera_proj, + &camera_transform, + keyboard_motion, + zoom_motion, + ) + } + ProjectionMode::Perspective => { + *keyboard_command = update_perspective_command( + command_type, + &camera_proj, + &camera_transform, + keyboard_motion, + zoom_motion, + window, + ) + } + } + } +} + +fn update_orthographic_command( + command_type: CameraCommandType, + camera_proj: &Projection, + camera_transform: &Transform, + keyboard_motion: Vec2, + zoom_motion: f32, +) -> KeyboardCommand { + let zoom_sensitivity = 0.05; + let orbit_sensitivity = 2.0; + let pan_sensitivity = 2.0; + let mut keyboard_command = KeyboardCommand::default(); + + // Zoom by scaling + let mut target_scale = 0.0; + if let Projection::Orthographic(camera_proj) = camera_proj { + keyboard_command.scale_delta = zoom_motion * camera_proj.scale * zoom_sensitivity; + target_scale = camera_proj.scale + keyboard_command.scale_delta; + } + + // Keyboard motion to scale + let keyboard_motion_adj = keyboard_motion * pan_sensitivity; + + match command_type { + CameraCommandType::Orbit => { + let yaw = keyboard_motion.x * orbit_sensitivity; + let yaw = Quat::from_rotation_z(-yaw); + keyboard_command.rotation_delta = yaw; + } + CameraCommandType::Pan => { + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = camera_transform.rotation * Vec3::Y; + + keyboard_command.translation_delta = + up_translation * keyboard_motion_adj.y + right_translation * keyboard_motion_adj.x; + } + _ => (), + } + + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; + + return keyboard_command; +} + +fn update_perspective_command( + command_type: CameraCommandType, + camera_proj: &Projection, + camera_transform: &Transform, + keyboard_motion: Vec2, + zoom_motion: f32, + window: &Window, +) -> KeyboardCommand { + let fov_zoom_sensitivity = 0.1; + let orbit_sensitivity = 20.0; + let pan_sensitivity = 1.0; + let translation_zoom_sensitivity = pan_sensitivity; + let mut keyboard_command = KeyboardCommand::default(); + + let zoom_translation = camera_transform.forward() * zoom_motion * translation_zoom_sensitivity; + + match command_type { + CameraCommandType::FovZoom => { + if let Projection::Perspective(camera_proj) = camera_proj { + let target_fov = (camera_proj.fov + zoom_motion * fov_zoom_sensitivity).clamp( + std::f32::consts::PI * 10.0 / 180.0, + std::f32::consts::PI * 170.0 / 180.0, + ); + keyboard_command.fov_delta = target_fov - camera_proj.fov; + } + } + CameraCommandType::TranslationZoom => { + keyboard_command.translation_delta = zoom_translation; + } + CameraCommandType::Pan => { + let keyboard_motion_adj = keyboard_motion * pan_sensitivity; + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = -camera_transform.rotation * Vec3::Y; + keyboard_command.translation_delta = + up_translation * keyboard_motion_adj.y + right_translation * keyboard_motion_adj.x; + } + CameraCommandType::Orbit => { + let keyboard_motion_adj = keyboard_motion * orbit_sensitivity; + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + let delta_x = keyboard_motion_adj.x / window_size.x * std::f32::consts::PI * 2.0; + let delta_y = keyboard_motion_adj.y / window_size.y * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(-delta_x); + let pitch = Quat::from_rotation_x(delta_y); + + let mut target_rotation = (yaw * camera_transform.rotation) * pitch; + target_rotation = if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { + target_rotation + } else { + yaw * camera_transform.rotation + }; + + let start_rotation = Mat3::from_quat(camera_transform.rotation); + let target_rotation = Mat3::from_quat(target_rotation); + keyboard_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + keyboard_command.translation_delta = zoom_translation; + } + _ => (), + } + + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; + + return keyboard_command; +} diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 192ebe5b..61f9699c 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -25,9 +25,13 @@ use bevy::{ view::RenderLayers, }, }; + mod cursor; use cursor::{update_cursor_command, CursorCommand}; +mod keyboard; +use keyboard::{update_keyboard_command, KeyboardCommand}; + /// RenderLayers are used to inform cameras which entities they should render. /// The General render layer is for things that should be visible to all /// cameras. @@ -398,6 +402,7 @@ impl FromWorld for CameraControls { fn camera_controls( cursor_command: ResMut, + keyboard_command: ResMut, mut controls: ResMut, mut cameras: Query<(&mut Projection, &mut Transform)>, mut bevy_cameras: Query<&mut Camera>, @@ -424,14 +429,31 @@ fn camera_controls( return; } + let mut translation_delta: Vec3; + let mut rotation_delta: Quat; + let mut fov_delta: f32; + let mut scale_delta: f32; + if cursor_command.command_type != CameraCommandType::Inactive { + translation_delta = cursor_command.translation_delta; + rotation_delta = cursor_command.rotation_delta; + fov_delta = cursor_command.fov_delta; + scale_delta = cursor_command.scale_delta; + } else { + translation_delta = cursor_command.translation_delta; + translation_delta = keyboard_command.translation_delta; + rotation_delta = keyboard_command.rotation_delta; + fov_delta = keyboard_command.fov_delta; + scale_delta = keyboard_command.scale_delta; + } + if controls.mode() == ProjectionMode::Perspective { let (mut persp_proj, mut persp_transform) = cameras .get_mut(controls.perspective_camera_entities[0]) .unwrap(); if let Projection::Perspective(persp_proj) = persp_proj.as_mut() { - persp_transform.translation += cursor_command.translation_delta; - persp_transform.rotation *= cursor_command.rotation_delta; - persp_proj.fov += cursor_command.fov_delta; + persp_transform.translation += translation_delta; + persp_transform.rotation *= rotation_delta; + persp_proj.fov += fov_delta; persp_proj.fov = persp_proj .fov .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); @@ -455,9 +477,9 @@ fn camera_controls( .get_mut(controls.orthographic_camera_entities[0]) .unwrap(); if let Projection::Orthographic(ortho_proj) = ortho_proj.as_mut() { - ortho_transform.translation += cursor_command.translation_delta; - ortho_transform.rotation *= cursor_command.rotation_delta; - ortho_proj.scale += cursor_command.scale_delta; + ortho_transform.translation += translation_delta; + ortho_transform.rotation *= rotation_delta; + ortho_proj.scale += scale_delta; } let proj = ortho_proj.clone(); @@ -515,9 +537,18 @@ impl Plugin for CameraControlsPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() + .init_resource::() .init_resource::() .add_event::() - .add_systems(Update, (update_cursor_command, camera_controls).chain()) + .add_systems( + Update, + ( + update_cursor_command, + update_keyboard_command, + camera_controls, + ) + .chain(), + ) .add_systems(Update, update_orbit_center_marker); } } From e3d514969103bdd4b33e8630c4598b6558c26297 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Tue, 11 Jun 2024 16:46:32 +0800 Subject: [PATCH 17/32] keyboard controls Signed-off-by: Reuben Thomas --- .../interaction/camera_controls/keyboard.rs | 260 +++++++++++------- .../src/interaction/camera_controls/mod.rs | 26 +- 2 files changed, 176 insertions(+), 110 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs index 358ecb26..0dfb7242 100644 --- a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs +++ b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs @@ -15,8 +15,14 @@ * */ -use super::{CameraCommandType, CameraControls, CursorCommand, ProjectionMode}; -use bevy::{prelude::*, window::PrimaryWindow}; +use super::{ + CameraCommandType, CameraControls, CursorCommand, ProjectionMode, MAX_PITCH, MAX_SELECTION_DIST, +}; +use bevy::{math::Vec3A, prelude::*, render::camera, window::PrimaryWindow}; +use bevy_mod_raycast::{ + immediate::{Raycast, RaycastSettings, RaycastVisibility}, + primitives::Ray3d, +}; #[derive(Resource)] pub struct KeyboardCommand { @@ -26,6 +32,7 @@ pub struct KeyboardCommand { pub fov_delta: f32, pub keyboard_motion: Vec2, pub zoom_motion: f32, + pub camera_selection: Option, pub command_type: CameraCommandType, } @@ -38,17 +45,18 @@ impl Default for KeyboardCommand { fov_delta: 0.0, keyboard_motion: Vec2::ZERO, zoom_motion: 0.0, + camera_selection: None, command_type: CameraCommandType::Inactive, } } } pub fn update_keyboard_command( - camera_controls: Res, - cursor_command: ResMut, + mut camera_controls: ResMut, mut keyboard_command: ResMut, keyboard_input: Res>, cameras: Query<(&Projection, &Transform)>, + mut immediate_raycast: Raycast, primary_windows: Query<&Window, With>, ) { if let Ok(window) = primary_windows.get_single() { @@ -81,30 +89,36 @@ pub fn update_keyboard_command( } // Smoothen motion using current state - // (1 / reponse_factor) frames = number of frames to reach maximum velocity - let response_factor = 0.05; + // (1 / reponse_factor) = number of frames to reach maximum keyboard input + let response_factor = 0.1; let prev_keyboard_motion = keyboard_command.keyboard_motion; let mut keyboard_motion = prev_keyboard_motion + (target_keyboard_motion - prev_keyboard_motion).normalize_or_zero() * response_factor; if keyboard_motion.length() > 1.0 { keyboard_motion = keyboard_motion.normalize(); - } else if keyboard_motion.length() < 0.1 { + } else if keyboard_motion.length() < response_factor { keyboard_motion = Vec2::ZERO; } let prev_zoom_motion = keyboard_command.zoom_motion; + let zoom_motion_delta = if (target_zoom_motion - prev_zoom_motion).abs() < response_factor { + 0.0 + } else { + (target_zoom_motion - prev_zoom_motion).signum() + }; let mut zoom_motion = - prev_zoom_motion + (target_zoom_motion - prev_zoom_motion).signum() * response_factor; + prev_zoom_motion + zoom_motion_delta * response_factor; if zoom_motion.abs() > 1.0 { zoom_motion = zoom_motion.signum(); - } else if zoom_motion.abs() < 0.1 { + } else if zoom_motion.abs() < response_factor { zoom_motion = 0.0; } // Get command type - let command_type = if is_shifting && keyboard_motion.length() > 0.0 { + let is_keyboard_motion_active = keyboard_motion.length().max(target_keyboard_motion.length()) > 0.0; + let command_type = if is_shifting && is_keyboard_motion_active { CameraCommandType::Orbit - } else if keyboard_motion.length() > 0.0 { + } else if is_keyboard_motion_active { CameraCommandType::Pan } else if is_shifting && zoom_motion != 0.0 { CameraCommandType::FovZoom @@ -114,10 +128,8 @@ pub fn update_keyboard_command( CameraCommandType::Inactive }; - if command_type != keyboard_command.command_type - && command_type != CameraCommandType::Inactive - {} - { + // Ignore previous motion if new command + if command_type != keyboard_command.command_type && command_type != CameraCommandType::Inactive { zoom_motion = response_factor * target_zoom_motion; keyboard_motion = response_factor * target_keyboard_motion; } @@ -129,139 +141,193 @@ pub fn update_keyboard_command( }; let (camera_proj, camera_transform) = cameras.get(active_camera_entity).unwrap(); + // Set camera selection as orbit center, discard once orbit operation complete + let camera_selection = match keyboard_command.camera_selection { + Some(camera_selection) => camera_selection, + None => get_camera_selected_point(camera_transform, &mut immediate_raycast), + }; + if command_type == CameraCommandType::Orbit { + camera_controls.orbit_center = Some(camera_selection); + } + if keyboard_command.command_type == CameraCommandType::Orbit && keyboard_command.command_type != command_type { + camera_controls.orbit_center = None; + } + // Orthographic match camera_controls.mode() { ProjectionMode::Orthographic => { - *keyboard_command = update_orthographic_command( + *keyboard_command = get_orthographic_command( command_type, - &camera_proj, - &camera_transform, + camera_proj, + camera_transform, keyboard_motion, zoom_motion, ) } ProjectionMode::Perspective => { - *keyboard_command = update_perspective_command( + *keyboard_command = get_perspective_command( command_type, - &camera_proj, - &camera_transform, + camera_proj, + camera_transform, + camera_selection, keyboard_motion, zoom_motion, - window, ) } } + } } -fn update_orthographic_command( +fn get_orthographic_command( command_type: CameraCommandType, camera_proj: &Projection, camera_transform: &Transform, keyboard_motion: Vec2, zoom_motion: f32, ) -> KeyboardCommand { - let zoom_sensitivity = 0.05; - let orbit_sensitivity = 2.0; - let pan_sensitivity = 2.0; + let pan_sensitivity = 0.015; + let orbit_sensitivity = 0.04; + let scale_zoom_sensitivity = 0.035; + let mut keyboard_command = KeyboardCommand::default(); - // Zoom by scaling - let mut target_scale = 0.0; if let Projection::Orthographic(camera_proj) = camera_proj { - keyboard_command.scale_delta = zoom_motion * camera_proj.scale * zoom_sensitivity; - target_scale = camera_proj.scale + keyboard_command.scale_delta; - } + // Zoom by scaling + keyboard_command.scale_delta = -zoom_motion * camera_proj.scale * scale_zoom_sensitivity; - // Keyboard motion to scale - let keyboard_motion_adj = keyboard_motion * pan_sensitivity; + match command_type { + CameraCommandType::Orbit => { + let yaw = -keyboard_motion.x * orbit_sensitivity; + let yaw = Quat::from_rotation_z(yaw); + keyboard_command.rotation_delta = yaw; + } + CameraCommandType::Pan => { + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = camera_transform.rotation * Vec3::Y; - match command_type { - CameraCommandType::Orbit => { - let yaw = keyboard_motion.x * orbit_sensitivity; - let yaw = Quat::from_rotation_z(-yaw); - keyboard_command.rotation_delta = yaw; + keyboard_command.translation_delta = + up_translation * keyboard_motion.y + right_translation * keyboard_motion.x; + keyboard_command.translation_delta *= pan_sensitivity * camera_proj.scale; + } + _ => (), } - CameraCommandType::Pan => { - let right_translation = camera_transform.rotation * Vec3::X; - let up_translation = camera_transform.rotation * Vec3::Y; - keyboard_command.translation_delta = - up_translation * keyboard_motion_adj.y + right_translation * keyboard_motion_adj.x; - } - _ => (), + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; } - - keyboard_command.command_type = command_type; - keyboard_command.keyboard_motion = keyboard_motion; - keyboard_command.zoom_motion = zoom_motion; - return keyboard_command; } -fn update_perspective_command( +fn get_perspective_command( command_type: CameraCommandType, camera_proj: &Projection, camera_transform: &Transform, + camera_selection: Vec3, keyboard_motion: Vec2, zoom_motion: f32, - window: &Window, ) -> KeyboardCommand { - let fov_zoom_sensitivity = 0.1; - let orbit_sensitivity = 20.0; - let pan_sensitivity = 1.0; - let translation_zoom_sensitivity = pan_sensitivity; + let pan_sensitivity = 0.1; + let orbit_sensitivity = 0.005; + let fov_zoom_sensitivity = 0.03; + let translation_zoom_sensitivity = 0.1; + let mut keyboard_command = KeyboardCommand::default(); - let zoom_translation = camera_transform.forward() * zoom_motion * translation_zoom_sensitivity; + if let Projection::Perspective(camera_proj) = camera_proj { + // Scale zoom by distance to object in camera center + let dist_to_selection = (camera_transform.translation - camera_selection) + .length() + .max(1.0); + let zoom_translation = camera_transform.forward() + * (zoom_motion * translation_zoom_sensitivity) + * (dist_to_selection * 0.2); - match command_type { - CameraCommandType::FovZoom => { - if let Projection::Perspective(camera_proj) = camera_proj { - let target_fov = (camera_proj.fov + zoom_motion * fov_zoom_sensitivity).clamp( + match command_type { + CameraCommandType::FovZoom => { + let target_fov = (camera_proj.fov - zoom_motion * fov_zoom_sensitivity).clamp( std::f32::consts::PI * 10.0 / 180.0, std::f32::consts::PI * 170.0 / 180.0, ); keyboard_command.fov_delta = target_fov - camera_proj.fov; } + CameraCommandType::TranslationZoom => { + keyboard_command.translation_delta = zoom_translation + } + CameraCommandType::Pan => { + let keyboard_motion_adj = keyboard_motion * pan_sensitivity * camera_proj.fov; + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = -camera_transform.rotation * Vec3::Y; + keyboard_command.translation_delta = up_translation * keyboard_motion_adj.y + + right_translation * keyboard_motion_adj.x + zoom_translation; + } + CameraCommandType::Orbit => { + let mut target_transform = camera_transform.clone(); + let keyboard_motion_adj = keyboard_motion * orbit_sensitivity * camera_proj.fov; + let delta_x = keyboard_motion_adj.x * std::f32::consts::PI * 2.0; + let delta_y = keyboard_motion_adj.y * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(delta_x); + let pitch = Quat::from_rotation_x(-delta_y); + + // Rotation + // Exclude pitch if exceeds maximum angle + target_transform.rotation = (yaw * camera_transform.rotation) * pitch; + if target_transform.up().z.acos().to_degrees() > MAX_PITCH { + target_transform.rotation = yaw * camera_transform.rotation; + }; + + // Translation around orbit center + let target_rotation = Mat3::from_quat(target_transform.rotation); + let orbit_center = camera_selection; + let orbit_radius = (orbit_center - (camera_transform.translation + zoom_translation)).length(); + target_transform.translation = orbit_center + target_rotation * Vec3::new(0.0, 0.0, orbit_radius); + + + let start_rotation = Mat3::from_quat(camera_transform.rotation); + keyboard_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + keyboard_command.translation_delta = (target_transform.translation - camera_transform.translation); + keyboard_command.camera_selection = Some(orbit_center); + } + _ => (), } - CameraCommandType::TranslationZoom => { - keyboard_command.translation_delta = zoom_translation; - } - CameraCommandType::Pan => { - let keyboard_motion_adj = keyboard_motion * pan_sensitivity; - let right_translation = camera_transform.rotation * Vec3::X; - let up_translation = -camera_transform.rotation * Vec3::Y; - keyboard_command.translation_delta = - up_translation * keyboard_motion_adj.y + right_translation * keyboard_motion_adj.x; - } - CameraCommandType::Orbit => { - let keyboard_motion_adj = keyboard_motion * orbit_sensitivity; - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - let delta_x = keyboard_motion_adj.x / window_size.x * std::f32::consts::PI * 2.0; - let delta_y = keyboard_motion_adj.y / window_size.y * std::f32::consts::PI; - let yaw = Quat::from_rotation_z(-delta_x); - let pitch = Quat::from_rotation_x(delta_y); - - let mut target_rotation = (yaw * camera_transform.rotation) * pitch; - target_rotation = if Transform::from_rotation(target_rotation).up().dot(Vec3::Z) > 0.0 { - target_rotation - } else { - yaw * camera_transform.rotation - }; - - let start_rotation = Mat3::from_quat(camera_transform.rotation); - let target_rotation = Mat3::from_quat(target_rotation); - keyboard_command.rotation_delta = - Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - keyboard_command.translation_delta = zoom_translation; - } - _ => (), + + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; } + return keyboard_command; +} - keyboard_command.command_type = command_type; - keyboard_command.keyboard_motion = keyboard_motion; - keyboard_command.zoom_motion = zoom_motion; +pub fn get_camera_selected_point( + camera_transform: &Transform, + immediate_raycast: &mut Raycast, +) -> Vec3 { + let camera_ray = Ray3d::new(camera_transform.translation, camera_transform.forward()); + let raycast_setting = RaycastSettings::default() + .always_early_exit() + .with_visibility(RaycastVisibility::MustBeVisible); - return keyboard_command; + let intersections = immediate_raycast.cast_ray(camera_ray, &raycast_setting); + + //TODO(@reuben-thomas) Filter for selectable entities + if (intersections.len() > 0) { + let (_, intersection_data) = &intersections[0]; + return intersection_data.position(); + } + + // If valid intersection with groundplane + let denom = Vec3::Z.dot(camera_transform.forward()); + if denom.abs() > f32::EPSILON { + let dist = (-1.0 * camera_transform.translation).dot(Vec3::Z) / denom; + if dist > f32::EPSILON && dist < MAX_SELECTION_DIST { + return camera_transform.translation + camera_transform.forward() * dist; + } + } + + // No groundplne intersection + let height = camera_transform.translation.y.abs(); + let radius = if height < 1.0 { 1.0 } else { height }; + return camera_transform.translation + camera_transform.forward() * radius; } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 61f9699c..28858232 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -62,7 +62,7 @@ pub const MODEL_PREVIEW_LAYER: u8 = 6; pub const MIN_FOV: f32 = 5.0; pub const MAX_FOV: f32 = 120.0; pub const MAX_PITCH: f32 = 85.0; -pub const MAX_SELECTION_DIST: f32 = 100.0; +pub const MAX_SELECTION_DIST: f32 = 30.0; // [m] #[derive(PartialEq, Debug, Copy, Clone)] pub enum CameraCommandType { @@ -494,6 +494,7 @@ fn camera_controls( fn update_orbit_center_marker( controls: Res, + keyboard_command: Res, cursor_command: Res, interaction_assets: Res, mut gizmo: Gizmos, @@ -506,19 +507,18 @@ fn update_orbit_center_marker( if let Ok((mut marker_transform, mut marker_visibility, mut marker_material)) = marker_query.get_mut(controls.orbit_center_marker) { - if let Some(orbit_center) = controls.orbit_center { + if controls.orbit_center.is_some() && controls.mode() == ProjectionMode::Perspective { + let orbit_center = controls.orbit_center.unwrap(); + let sphere_color: Color; - match cursor_command.command_type { - CameraCommandType::Orbit - | CameraCommandType::DeselectOrbitCenter - | CameraCommandType::SelectOrbitCenter => { - *marker_material = interaction_assets.orbit_center_active_material.clone(); - sphere_color = Color::GREEN; - } - _ => { - *marker_material = interaction_assets.orbit_center_inactive_material.clone(); - sphere_color = Color::WHITE; - } + let is_orbitting = cursor_command.command_type == CameraCommandType::Orbit + || keyboard_command.command_type == CameraCommandType::Orbit; + if is_orbitting { + *marker_material = interaction_assets.orbit_center_active_material.clone(); + sphere_color = Color::GREEN; + } else { + *marker_material = interaction_assets.orbit_center_inactive_material.clone(); + sphere_color = Color::WHITE; }; //TODO(@reuben-thomas) scale to be of constant size in camera From d798f9821dcb7e21b01b432e8843a2a09aff9e39 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Wed, 12 Jun 2024 10:40:43 +0800 Subject: [PATCH 18/32] fix styling Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 21 +- .../interaction/camera_controls/keyboard.rs | 238 +++++++++--------- .../src/interaction/camera_controls/mod.rs | 33 ++- 3 files changed, 162 insertions(+), 130 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 4c39550e..394772e3 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -15,7 +15,10 @@ * */ -use super::{CameraCommandType, CameraControls, ProjectionMode, MAX_PITCH, MAX_SELECTION_DIST}; +use super::{ + CameraCommandType, CameraControls, ProjectionMode, MAX_FOV, MAX_PITCH, MAX_SELECTION_DIST, + MIN_FOV, +}; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::prelude::*; @@ -134,9 +137,11 @@ pub fn update_cursor_command( *cursor_command = match camera_controls.mode() { ProjectionMode::Perspective => get_perspective_cursor_command( &camera_transform, + &camera_proj, command_type, cursor_direction, cursor_selection, + cursor_selection_new, cursor_motion, camera_controls.orbit_center, scroll_motion, @@ -231,9 +236,11 @@ fn get_orthographic_cursor_command( fn get_perspective_cursor_command( camera_transform: &Transform, + camera_proj: &Projection, command_type: CameraCommandType, cursor_direction: Vec3, cursor_selection: Vec3, + cursor_selection_new: Vec3, cursor_motion: Vec2, orbit_center: Option, scroll_motion: f32, @@ -248,7 +255,11 @@ fn get_perspective_cursor_command( match command_type { CameraCommandType::FovZoom => { - cursor_command.fov_delta = -scroll_motion * fov_zoom_sensitivity; + if let Projection::Perspective(camera_proj) = camera_proj { + let target_fov = (camera_proj.fov - scroll_motion * fov_zoom_sensitivity) + .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); + cursor_command.fov_delta = target_fov - camera_proj.fov; + } } CameraCommandType::TranslationZoom => { cursor_command.translation_delta = @@ -298,8 +309,8 @@ fn get_perspective_cursor_command( cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; let delta_y = cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; - let yaw = Quat::from_rotation_z(delta_x); - let pitch = Quat::from_rotation_x(delta_y); + let yaw = Quat::from_rotation_z(-delta_x); + let pitch = Quat::from_rotation_x(-delta_y); let mut target_transform = camera_transform.clone(); // Exclude pitch if exceeds maximum angle @@ -364,7 +375,7 @@ fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource, keyboard_input: Res>, cameras: Query<(&Projection, &Transform)>, - mut immediate_raycast: Raycast, + immediate_raycast: Raycast, primary_windows: Query<&Window, With>, ) { - if let Ok(window) = primary_windows.get_single() { + if let Ok(_) = primary_windows.get_single() { // User inputs let is_shifting = keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight); @@ -90,7 +91,7 @@ pub fn update_keyboard_command( // Smoothen motion using current state // (1 / reponse_factor) = number of frames to reach maximum keyboard input - let response_factor = 0.1; + let response_factor = 0.08; let prev_keyboard_motion = keyboard_command.keyboard_motion; let mut keyboard_motion = prev_keyboard_motion + (target_keyboard_motion - prev_keyboard_motion).normalize_or_zero() * response_factor; @@ -106,8 +107,7 @@ pub fn update_keyboard_command( } else { (target_zoom_motion - prev_zoom_motion).signum() }; - let mut zoom_motion = - prev_zoom_motion + zoom_motion_delta * response_factor; + let mut zoom_motion = prev_zoom_motion + zoom_motion_delta * response_factor; if zoom_motion.abs() > 1.0 { zoom_motion = zoom_motion.signum(); } else if zoom_motion.abs() < response_factor { @@ -115,7 +115,10 @@ pub fn update_keyboard_command( } // Get command type - let is_keyboard_motion_active = keyboard_motion.length().max(target_keyboard_motion.length()) > 0.0; + let is_keyboard_motion_active = keyboard_motion + .length() + .max(target_keyboard_motion.length()) + > 0.0; let command_type = if is_shifting && is_keyboard_motion_active { CameraCommandType::Orbit } else if is_keyboard_motion_active { @@ -129,7 +132,9 @@ pub fn update_keyboard_command( }; // Ignore previous motion if new command - if command_type != keyboard_command.command_type && command_type != CameraCommandType::Inactive { + if command_type != keyboard_command.command_type + && command_type != CameraCommandType::Inactive + { zoom_motion = response_factor * target_zoom_motion; keyboard_motion = response_factor * target_keyboard_motion; } @@ -139,49 +144,58 @@ pub fn update_keyboard_command( ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], }; - let (camera_proj, camera_transform) = cameras.get(active_camera_entity).unwrap(); + let (camera_proj, camera_transform) = + cameras.get(active_camera_entity).unwrap(); // Set camera selection as orbit center, discard once orbit operation complete let camera_selection = match keyboard_command.camera_selection { Some(camera_selection) => camera_selection, - None => get_camera_selected_point(camera_transform, &mut immediate_raycast), + None => get_camera_selected_point( + camera_transform, + immediate_raycast, + ), }; if command_type == CameraCommandType::Orbit { camera_controls.orbit_center = Some(camera_selection); } - if keyboard_command.command_type == CameraCommandType::Orbit && keyboard_command.command_type != command_type { + if keyboard_command.command_type == CameraCommandType::Orbit + && keyboard_command.command_type != command_type + { camera_controls.orbit_center = None; } // Orthographic match camera_controls.mode() { ProjectionMode::Orthographic => { - *keyboard_command = get_orthographic_command( - command_type, - camera_proj, - camera_transform, - keyboard_motion, - zoom_motion, - ) + if let Projection::Orthographic(camera_proj) = camera_proj { + *keyboard_command = get_orthographic_command( + command_type, + camera_proj, + camera_transform, + keyboard_motion, + zoom_motion, + ) + } } ProjectionMode::Perspective => { - *keyboard_command = get_perspective_command( - command_type, - camera_proj, - camera_transform, - camera_selection, - keyboard_motion, - zoom_motion, - ) + if let Projection::Perspective(camera_proj) = camera_proj { + *keyboard_command = get_perspective_command( + command_type, + camera_proj, + camera_transform, + camera_selection, + keyboard_motion, + zoom_motion, + ) + } } } - } } fn get_orthographic_command( command_type: CameraCommandType, - camera_proj: &Projection, + camera_proj: &OrthographicProjection, camera_transform: &Transform, keyboard_motion: Vec2, zoom_motion: f32, @@ -192,37 +206,36 @@ fn get_orthographic_command( let mut keyboard_command = KeyboardCommand::default(); - if let Projection::Orthographic(camera_proj) = camera_proj { - // Zoom by scaling - keyboard_command.scale_delta = -zoom_motion * camera_proj.scale * scale_zoom_sensitivity; - - match command_type { - CameraCommandType::Orbit => { - let yaw = -keyboard_motion.x * orbit_sensitivity; - let yaw = Quat::from_rotation_z(yaw); - keyboard_command.rotation_delta = yaw; - } - CameraCommandType::Pan => { - let right_translation = camera_transform.rotation * Vec3::X; - let up_translation = camera_transform.rotation * Vec3::Y; + // Zoom by scaling + keyboard_command.scale_delta = -zoom_motion * camera_proj.scale * scale_zoom_sensitivity; - keyboard_command.translation_delta = - up_translation * keyboard_motion.y + right_translation * keyboard_motion.x; - keyboard_command.translation_delta *= pan_sensitivity * camera_proj.scale; - } - _ => (), + match command_type { + CameraCommandType::Orbit => { + let yaw = -keyboard_motion.x * orbit_sensitivity; + let yaw = Quat::from_rotation_z(yaw); + keyboard_command.rotation_delta = yaw; } + CameraCommandType::Pan => { + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = camera_transform.rotation * Vec3::Y; - keyboard_command.command_type = command_type; - keyboard_command.keyboard_motion = keyboard_motion; - keyboard_command.zoom_motion = zoom_motion; + keyboard_command.translation_delta = + up_translation * keyboard_motion.y + right_translation * keyboard_motion.x; + keyboard_command.translation_delta *= pan_sensitivity * camera_proj.scale; + } + _ => (), } + + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; + return keyboard_command; } fn get_perspective_command( command_type: CameraCommandType, - camera_proj: &Projection, + camera_proj: &PerspectiveProjection, camera_transform: &Transform, camera_selection: Vec3, keyboard_motion: Vec2, @@ -235,74 +248,71 @@ fn get_perspective_command( let mut keyboard_command = KeyboardCommand::default(); - if let Projection::Perspective(camera_proj) = camera_proj { - // Scale zoom by distance to object in camera center - let dist_to_selection = (camera_transform.translation - camera_selection) - .length() - .max(1.0); - let zoom_translation = camera_transform.forward() - * (zoom_motion * translation_zoom_sensitivity) - * (dist_to_selection * 0.2); - - match command_type { - CameraCommandType::FovZoom => { - let target_fov = (camera_proj.fov - zoom_motion * fov_zoom_sensitivity).clamp( - std::f32::consts::PI * 10.0 / 180.0, - std::f32::consts::PI * 170.0 / 180.0, - ); - keyboard_command.fov_delta = target_fov - camera_proj.fov; - } - CameraCommandType::TranslationZoom => { - keyboard_command.translation_delta = zoom_translation - } - CameraCommandType::Pan => { - let keyboard_motion_adj = keyboard_motion * pan_sensitivity * camera_proj.fov; - let right_translation = camera_transform.rotation * Vec3::X; - let up_translation = -camera_transform.rotation * Vec3::Y; - keyboard_command.translation_delta = up_translation * keyboard_motion_adj.y - + right_translation * keyboard_motion_adj.x + zoom_translation; - } - CameraCommandType::Orbit => { - let mut target_transform = camera_transform.clone(); - let keyboard_motion_adj = keyboard_motion * orbit_sensitivity * camera_proj.fov; - let delta_x = keyboard_motion_adj.x * std::f32::consts::PI * 2.0; - let delta_y = keyboard_motion_adj.y * std::f32::consts::PI; - let yaw = Quat::from_rotation_z(delta_x); - let pitch = Quat::from_rotation_x(-delta_y); - - // Rotation - // Exclude pitch if exceeds maximum angle - target_transform.rotation = (yaw * camera_transform.rotation) * pitch; - if target_transform.up().z.acos().to_degrees() > MAX_PITCH { - target_transform.rotation = yaw * camera_transform.rotation; - }; - - // Translation around orbit center - let target_rotation = Mat3::from_quat(target_transform.rotation); - let orbit_center = camera_selection; - let orbit_radius = (orbit_center - (camera_transform.translation + zoom_translation)).length(); - target_transform.translation = orbit_center + target_rotation * Vec3::new(0.0, 0.0, orbit_radius); - - - let start_rotation = Mat3::from_quat(camera_transform.rotation); - keyboard_command.rotation_delta = - Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - keyboard_command.translation_delta = (target_transform.translation - camera_transform.translation); - keyboard_command.camera_selection = Some(orbit_center); - } - _ => (), + // Scale zoom by distance to object in camera center + let dist_to_selection = (camera_transform.translation - camera_selection) + .length() + .max(1.0); + let zoom_translation = camera_transform.forward() + * (zoom_motion * translation_zoom_sensitivity) + * (dist_to_selection * 0.2); + + match command_type { + CameraCommandType::FovZoom => { + let target_fov = (camera_proj.fov - zoom_motion * fov_zoom_sensitivity) + .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); + keyboard_command.fov_delta = target_fov - camera_proj.fov; } - - keyboard_command.command_type = command_type; - keyboard_command.keyboard_motion = keyboard_motion; - keyboard_command.zoom_motion = zoom_motion; + CameraCommandType::TranslationZoom => keyboard_command.translation_delta = zoom_translation, + CameraCommandType::Pan => { + let keyboard_motion_adj = keyboard_motion * pan_sensitivity * camera_proj.fov; + let right_translation = camera_transform.rotation * Vec3::X; + let up_translation = -camera_transform.rotation * Vec3::Y; + keyboard_command.translation_delta = up_translation * keyboard_motion_adj.y + + right_translation * keyboard_motion_adj.x + + zoom_translation; + } + CameraCommandType::Orbit => { + let mut target_transform = camera_transform.clone(); + let keyboard_motion_adj = keyboard_motion * orbit_sensitivity * camera_proj.fov; + let delta_x = keyboard_motion_adj.x * std::f32::consts::PI * 2.0; + let delta_y = keyboard_motion_adj.y * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(delta_x); + let pitch = Quat::from_rotation_x(-delta_y); + + // Rotation + // Exclude pitch if exceeds maximum angle + target_transform.rotation = (yaw * camera_transform.rotation) * pitch; + if target_transform.up().z.acos().to_degrees() > MAX_PITCH { + target_transform.rotation = yaw * camera_transform.rotation; + }; + + // Translation around orbit center + let target_rotation = Mat3::from_quat(target_transform.rotation); + let orbit_center = camera_selection; + let orbit_radius = + (orbit_center - (camera_transform.translation + zoom_translation)).length(); + target_transform.translation = + orbit_center + target_rotation * Vec3::new(0.0, 0.0, orbit_radius); + + let start_rotation = Mat3::from_quat(camera_transform.rotation); + keyboard_command.rotation_delta = + Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + keyboard_command.translation_delta = + target_transform.translation - camera_transform.translation; + keyboard_command.camera_selection = Some(orbit_center); + } + _ => (), } + + keyboard_command.command_type = command_type; + keyboard_command.keyboard_motion = keyboard_motion; + keyboard_command.zoom_motion = zoom_motion; return keyboard_command; } pub fn get_camera_selected_point( camera_transform: &Transform, - immediate_raycast: &mut Raycast, + mut immediate_raycast: Raycast, ) -> Vec3 { let camera_ray = Ray3d::new(camera_transform.translation, camera_transform.forward()); let raycast_setting = RaycastSettings::default() @@ -312,7 +322,7 @@ pub fn get_camera_selected_point( let intersections = immediate_raycast.cast_ray(camera_ray, &raycast_setting); //TODO(@reuben-thomas) Filter for selectable entities - if (intersections.len() > 0) { + if intersections.len() > 0 { let (_, intersection_data) = &intersections[0]; return intersection_data.position(); } @@ -327,7 +337,7 @@ pub fn get_camera_selected_point( } // No groundplne intersection - let height = camera_transform.translation.y.abs(); + let height = camera_transform.translation.z.abs(); let radius = if height < 1.0 { 1.0 } else { height }; return camera_transform.translation + camera_transform.forward() * radius; } diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 28858232..4742a873 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -429,17 +429,16 @@ fn camera_controls( return; } - let mut translation_delta: Vec3; - let mut rotation_delta: Quat; - let mut fov_delta: f32; - let mut scale_delta: f32; + let translation_delta: Vec3; + let rotation_delta: Quat; + let fov_delta: f32; + let scale_delta: f32; if cursor_command.command_type != CameraCommandType::Inactive { translation_delta = cursor_command.translation_delta; rotation_delta = cursor_command.rotation_delta; fov_delta = cursor_command.fov_delta; scale_delta = cursor_command.scale_delta; } else { - translation_delta = cursor_command.translation_delta; translation_delta = keyboard_command.translation_delta; rotation_delta = keyboard_command.rotation_delta; fov_delta = keyboard_command.fov_delta; @@ -498,11 +497,18 @@ fn update_orbit_center_marker( cursor_command: Res, interaction_assets: Res, mut gizmo: Gizmos, - mut marker_query: Query<( - &mut Transform, - &mut Visibility, - &mut Handle, - )>, + // camera_query: Query<( + // &Transform, + // &Projection + // )>, + mut marker_query: Query< + ( + &mut Transform, + &mut Visibility, + &mut Handle, + ), + Without, + >, ) { if let Ok((mut marker_transform, mut marker_visibility, mut marker_material)) = marker_query.get_mut(controls.orbit_center_marker) @@ -510,6 +516,7 @@ fn update_orbit_center_marker( if controls.orbit_center.is_some() && controls.mode() == ProjectionMode::Perspective { let orbit_center = controls.orbit_center.unwrap(); + // Color by current action let sphere_color: Color; let is_orbitting = cursor_command.command_type == CameraCommandType::Orbit || keyboard_command.command_type == CameraCommandType::Orbit; @@ -521,7 +528,11 @@ fn update_orbit_center_marker( sphere_color = Color::WHITE; }; - //TODO(@reuben-thomas) scale to be of constant size in camera + // Scale to maintain size in camera space + // let mut scale_factor = 1.0; + // let (camera_transform, camera_proj) = camera_query.get(controls.active_camera()).unwrap(); + // let orbit_radius = (orbit_center - camera_transform).length(); + gizmo.sphere(orbit_center, Quat::IDENTITY, 0.1, sphere_color); marker_transform.translation = orbit_center; *marker_visibility = Visibility::Visible; From 465b9c3636c9126836bd962d49c61bfabdb3318d Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Fri, 14 Jun 2024 12:31:47 +0800 Subject: [PATCH 19/32] orbit around cursor initital selection Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 59 ++++++++----------- .../interaction/camera_controls/keyboard.rs | 8 +-- .../src/interaction/camera_controls/mod.rs | 23 ++++---- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 394772e3..40721050 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -86,13 +86,14 @@ pub fn update_cursor_command( } // Command type, return if inactive + let prev_command_type = cursor_command.command_type; let command_type = get_command_type( &keyboard_input, &mouse_input, &cursor_motion, &scroll_motion, - cursor_command.command_type, camera_controls.mode(), + &prev_command_type, ); if command_type == CameraCommandType::Inactive { *cursor_command = CursorCommand::default(); @@ -114,24 +115,22 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; - let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); + let new_cursor_selection = get_cursor_selected_point(&cursor_raycast_source); let cursor_selection = match cursor_command.cursor_selection { Some(selection) => selection, - None => cursor_selection_new, + None => new_cursor_selection, }; let cursor_direction = cursor_ray.direction().normalize(); - // Update obrit center if this is a select / deselect operation - if command_type == CameraCommandType::SelectOrbitCenter { - camera_controls.orbit_center = Some(cursor_selection_new); - *cursor_command = CursorCommand::default(); - cursor_command.command_type = CameraCommandType::SelectOrbitCenter; - return; - } else if command_type == CameraCommandType::DeselectOrbitCenter { + // Update orbit center + let was_orbittting = prev_command_type == CameraCommandType::Orbit + || prev_command_type == CameraCommandType::HoldOrbitSelection; + let is_orbitting = command_type == CameraCommandType::Orbit + || command_type == CameraCommandType::HoldOrbitSelection; + if !is_orbitting { camera_controls.orbit_center = None; - *cursor_command = CursorCommand::default(); - cursor_command.command_type = CameraCommandType::DeselectOrbitCenter; - return; + } else if !was_orbittting && is_orbitting { + camera_controls.orbit_center = Some(cursor_selection); } *cursor_command = match camera_controls.mode() { @@ -141,7 +140,6 @@ pub fn update_cursor_command( command_type, cursor_direction, cursor_selection, - cursor_selection_new, cursor_motion, camera_controls.orbit_center, scroll_motion, @@ -152,7 +150,7 @@ pub fn update_cursor_command( &camera_proj, command_type, cursor_selection, - cursor_selection_new, + new_cursor_selection, scroll_motion, ), }; @@ -166,7 +164,7 @@ fn get_orthographic_cursor_command( camera_proj: &Projection, command_type: CameraCommandType, cursor_selection: Vec3, - cursor_selection_new: Vec3, + new_cursor_selection: Vec3, scroll_motion: f32, ) -> CursorCommand { let mut cursor_command = CursorCommand::default(); @@ -178,7 +176,7 @@ fn get_orthographic_cursor_command( } //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction - let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); + let cursor_direction = (new_cursor_selection - camera_transform.translation).normalize(); match command_type { CameraCommandType::Pan => { @@ -240,7 +238,6 @@ fn get_perspective_cursor_command( command_type: CameraCommandType, cursor_direction: Vec3, cursor_selection: Vec3, - cursor_selection_new: Vec3, cursor_motion: Vec2, orbit_center: Option, scroll_motion: f32, @@ -311,15 +308,16 @@ fn get_perspective_cursor_command( cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; let yaw = Quat::from_rotation_z(-delta_x); let pitch = Quat::from_rotation_x(-delta_y); - let mut target_transform = camera_transform.clone(); + + // Rotation // Exclude pitch if exceeds maximum angle target_transform.rotation = (yaw * camera_transform.rotation) * pitch; if target_transform.up().z.acos().to_degrees() > MAX_PITCH { target_transform.rotation = yaw * camera_transform.rotation; }; - // Translation if orbitting a point + // Translation if let Some(orbit_center) = orbit_center { let camera_to_orbit_center = orbit_center - camera_transform.translation; let x = camera_to_orbit_center.dot(camera_transform.local_x()); @@ -387,8 +385,8 @@ fn get_command_type( mouse_input: &Res>, cursor_motion: &Vec2, scroll_motion: &f32, - prev_command_type: CameraCommandType, projection_mode: ProjectionMode, + prev_command_type: &CameraCommandType, ) -> CameraCommandType { // Inputs let is_cursor_moving = cursor_motion.length() > 0.; @@ -403,9 +401,15 @@ fn get_command_type( // Orbitting if is_cursor_moving && mouse_input.pressed(MouseButton::Middle) - || (mouse_input.pressed(MouseButton::Right) && is_shifting) + || (is_shifting && mouse_input.pressed(MouseButton::Right)) { return CameraCommandType::Orbit; + } else if is_shifting + && (*prev_command_type == CameraCommandType::Orbit + || *prev_command_type == CameraCommandType::HoldOrbitSelection) + && projection_mode.is_perspective() + { + return CameraCommandType::HoldOrbitSelection; } // Zoom @@ -420,16 +424,5 @@ fn get_command_type( } } - // Select / Deselect Orbit Center - if projection_mode.is_perspective() { - if mouse_input.just_pressed(MouseButton::Right) - && prev_command_type == CameraCommandType::Inactive - { - return CameraCommandType::SelectOrbitCenter; - } else if keyboard_input.just_pressed(KeyCode::Escape) { - return CameraCommandType::DeselectOrbitCenter; - } - } - return CameraCommandType::Inactive; } diff --git a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs index 52386ea6..c410246e 100644 --- a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs +++ b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs @@ -144,16 +144,12 @@ pub fn update_keyboard_command( ProjectionMode::Orthographic => camera_controls.orthographic_camera_entities[0], ProjectionMode::Perspective => camera_controls.perspective_camera_entities[0], }; - let (camera_proj, camera_transform) = - cameras.get(active_camera_entity).unwrap(); + let (camera_proj, camera_transform) = cameras.get(active_camera_entity).unwrap(); // Set camera selection as orbit center, discard once orbit operation complete let camera_selection = match keyboard_command.camera_selection { Some(camera_selection) => camera_selection, - None => get_camera_selected_point( - camera_transform, - immediate_raycast, - ), + None => get_camera_selected_point(camera_transform, immediate_raycast), }; if command_type == CameraCommandType::Orbit { camera_controls.orbit_center = Some(camera_selection); diff --git a/rmf_site_editor/src/interaction/camera_controls/mod.rs b/rmf_site_editor/src/interaction/camera_controls/mod.rs index 4742a873..83138e18 100644 --- a/rmf_site_editor/src/interaction/camera_controls/mod.rs +++ b/rmf_site_editor/src/interaction/camera_controls/mod.rs @@ -69,11 +69,10 @@ pub enum CameraCommandType { Inactive, Pan, Orbit, + HoldOrbitSelection, TranslationZoom, ScaleZoom, FovZoom, - SelectOrbitCenter, - DeselectOrbitCenter, } #[derive(Default, Reflect)] @@ -433,7 +432,9 @@ fn camera_controls( let rotation_delta: Quat; let fov_delta: f32; let scale_delta: f32; - if cursor_command.command_type != CameraCommandType::Inactive { + if cursor_command.command_type != CameraCommandType::Inactive + && cursor_command.command_type != CameraCommandType::HoldOrbitSelection + { translation_delta = cursor_command.translation_delta; rotation_delta = cursor_command.rotation_delta; fov_delta = cursor_command.fov_delta; @@ -513,13 +514,18 @@ fn update_orbit_center_marker( if let Ok((mut marker_transform, mut marker_visibility, mut marker_material)) = marker_query.get_mut(controls.orbit_center_marker) { - if controls.orbit_center.is_some() && controls.mode() == ProjectionMode::Perspective { + let is_orbitting = cursor_command.command_type == CameraCommandType::Orbit + || cursor_command.command_type == CameraCommandType::HoldOrbitSelection + || keyboard_command.command_type == CameraCommandType::Orbit; + + if is_orbitting + && controls.orbit_center.is_some() + && controls.mode() == ProjectionMode::Perspective + { let orbit_center = controls.orbit_center.unwrap(); // Color by current action let sphere_color: Color; - let is_orbitting = cursor_command.command_type == CameraCommandType::Orbit - || keyboard_command.command_type == CameraCommandType::Orbit; if is_orbitting { *marker_material = interaction_assets.orbit_center_active_material.clone(); sphere_color = Color::GREEN; @@ -528,11 +534,6 @@ fn update_orbit_center_marker( sphere_color = Color::WHITE; }; - // Scale to maintain size in camera space - // let mut scale_factor = 1.0; - // let (camera_transform, camera_proj) = camera_query.get(controls.active_camera()).unwrap(); - // let orbit_radius = (orbit_center - camera_transform).length(); - gizmo.sphere(orbit_center, Quat::IDENTITY, 0.1, sphere_color); marker_transform.translation = orbit_center; *marker_visibility = Visibility::Visible; From 7939a276a9ccb28f488092c7d28d905687931b04 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Fri, 14 Jun 2024 15:45:21 +0800 Subject: [PATCH 20/32] orbit proportional to cursor direction Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 103 ++++++++++-------- .../interaction/camera_controls/keyboard.rs | 13 +-- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index 40721050..e509c494 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -33,6 +33,7 @@ pub struct CursorCommand { pub scale_delta: f32, pub fov_delta: f32, pub cursor_selection: Option, + pub cursor_direction_camera_frame: Vec3, pub command_type: CameraCommandType, } @@ -44,6 +45,7 @@ impl Default for CursorCommand { scale_delta: 0.0, fov_delta: 0.0, cursor_selection: None, + cursor_direction_camera_frame: Vec3::ZERO, command_type: CameraCommandType::Inactive, } } @@ -95,10 +97,10 @@ pub fn update_cursor_command( camera_controls.mode(), &prev_command_type, ); - if command_type == CameraCommandType::Inactive { - *cursor_command = CursorCommand::default(); - return; - } + // if command_type == CameraCommandType::Inactive { + // *cursor_command = CursorCommand::default(); + // return; + // } // Camera projection and transform let active_camera_entity = match camera_controls.mode() { @@ -133,27 +135,34 @@ pub fn update_cursor_command( camera_controls.orbit_center = Some(cursor_selection); } - *cursor_command = match camera_controls.mode() { - ProjectionMode::Perspective => get_perspective_cursor_command( - &camera_transform, - &camera_proj, - command_type, - cursor_direction, - cursor_selection, - cursor_motion, - camera_controls.orbit_center, - scroll_motion, - window, - ), - ProjectionMode::Orthographic => get_orthographic_cursor_command( - &camera_transform, - &camera_proj, - command_type, - cursor_selection, - new_cursor_selection, - scroll_motion, - ), - }; + match camera_controls.mode() { + ProjectionMode::Orthographic => { + if let Projection::Orthographic(camera_proj) = camera_proj { + *cursor_command = get_orthographic_cursor_command( + &camera_transform, + &camera_proj, + command_type, + cursor_selection, + new_cursor_selection, + scroll_motion, + ); + } + } + ProjectionMode::Perspective => { + if let Projection::Perspective(camera_proj) = camera_proj { + *cursor_command = get_perspective_cursor_command( + &camera_transform, + &camera_proj, + command_type, + cursor_direction, + cursor_command.cursor_direction_camera_frame, + cursor_selection, + camera_controls.orbit_center, + scroll_motion, + ); + } + } + } } else { *cursor_command = CursorCommand::default(); } @@ -161,7 +170,7 @@ pub fn update_cursor_command( fn get_orthographic_cursor_command( camera_transform: &Transform, - camera_proj: &Projection, + camera_proj: &OrthographicProjection, command_type: CameraCommandType, cursor_selection: Vec3, new_cursor_selection: Vec3, @@ -171,9 +180,7 @@ fn get_orthographic_cursor_command( let mut is_cursor_selecting = false; // Zoom - if let Projection::Orthographic(camera_proj) = camera_proj { - cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; - } + cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction let cursor_direction = (new_cursor_selection - camera_transform.translation).normalize(); @@ -234,29 +241,27 @@ fn get_orthographic_cursor_command( fn get_perspective_cursor_command( camera_transform: &Transform, - camera_proj: &Projection, + camera_proj: &PerspectiveProjection, command_type: CameraCommandType, cursor_direction: Vec3, + cursor_direction_camera_frame_prev: Vec3, cursor_selection: Vec3, - cursor_motion: Vec2, orbit_center: Option, scroll_motion: f32, - window: &Window, ) -> CursorCommand { let translation_zoom_sensitivity = 0.5; let fov_zoom_sensitivity = 0.1; - let orbit_sensitivity = 1.0; let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; + let cursor_direction_camera_frame = camera_transform.rotation.inverse() * cursor_direction; + match command_type { CameraCommandType::FovZoom => { - if let Projection::Perspective(camera_proj) = camera_proj { - let target_fov = (camera_proj.fov - scroll_motion * fov_zoom_sensitivity) - .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); - cursor_command.fov_delta = target_fov - camera_proj.fov; - } + let target_fov = (camera_proj.fov - scroll_motion * fov_zoom_sensitivity) + .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); + cursor_command.fov_delta = target_fov - camera_proj.fov; } CameraCommandType::TranslationZoom => { cursor_command.translation_delta = @@ -299,15 +304,16 @@ fn get_perspective_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - // Adjust orbit to the window size - // TODO(@reuben-thomas) also adjust to fov - let window_size = Vec2::new(window.width(), window.height()); - let delta_x = - cursor_motion.x / window_size.x * std::f32::consts::PI * orbit_sensitivity; - let delta_y = - cursor_motion.y / window_size.y * std::f32::consts::PI * orbit_sensitivity; - let yaw = Quat::from_rotation_z(-delta_x); - let pitch = Quat::from_rotation_x(-delta_y); + let pitch = cursor_direction_camera_frame_prev.y.acos() + - cursor_direction_camera_frame.y.acos(); + let yaw = (cursor_direction_camera_frame_prev + .z + .atan2(cursor_direction_camera_frame_prev.x)) + - (cursor_direction_camera_frame + .z + .atan2(cursor_direction_camera_frame.x)); + let yaw = Quat::from_rotation_z(-yaw); + let pitch = Quat::from_rotation_x(-pitch); let mut target_transform = camera_transform.clone(); // Rotation @@ -340,12 +346,15 @@ fn get_perspective_cursor_command( let target_rotation = Mat3::from_quat(target_transform.rotation); cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); + cursor_command.cursor_direction_camera_frame = + camera_transform.rotation.inverse() * cursor_direction; is_cursor_selecting = true; } _ => (), } cursor_command.command_type = command_type; + cursor_command.cursor_direction_camera_frame = cursor_direction_camera_frame; cursor_command.cursor_selection = if is_cursor_selecting { Some(cursor_selection) } else { diff --git a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs index c410246e..9f99fab1 100644 --- a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs +++ b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs @@ -65,16 +65,16 @@ pub fn update_keyboard_command( let is_shifting = keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight); let mut target_keyboard_motion = Vec2::ZERO; - if keyboard_input.pressed(KeyCode::W) { + if keyboard_input.pressed(KeyCode::Up) { target_keyboard_motion.y += 1.0; } - if keyboard_input.pressed(KeyCode::A) { + if keyboard_input.pressed(KeyCode::Left) { target_keyboard_motion.x += -1.0; } - if keyboard_input.pressed(KeyCode::S) { + if keyboard_input.pressed(KeyCode::Down) { target_keyboard_motion.y += -1.0; } - if keyboard_input.pressed(KeyCode::D) { + if keyboard_input.pressed(KeyCode::Right) { target_keyboard_motion.x += 1.0; } if target_keyboard_motion.length() > 0.0 { @@ -82,10 +82,10 @@ pub fn update_keyboard_command( } let mut target_zoom_motion = 0.0; - if keyboard_input.pressed(KeyCode::Q) { + if keyboard_input.pressed(KeyCode::Minus) { target_zoom_motion += -1.0; } - if keyboard_input.pressed(KeyCode::E) { + if keyboard_input.pressed(KeyCode::Plus) { target_zoom_motion += 1.0; } @@ -160,7 +160,6 @@ pub fn update_keyboard_command( camera_controls.orbit_center = None; } - // Orthographic match camera_controls.mode() { ProjectionMode::Orthographic => { if let Projection::Orthographic(camera_proj) = camera_proj { From c437a879c110418005eb16b6fa34c6e9c346e05d Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Fri, 14 Jun 2024 18:08:57 +0800 Subject: [PATCH 21/32] proportional orbits Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index e509c494..e0a9f0dc 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -33,7 +33,7 @@ pub struct CursorCommand { pub scale_delta: f32, pub fov_delta: f32, pub cursor_selection: Option, - pub cursor_direction_camera_frame: Vec3, + pub cursor_direction_camera_frame: Option, pub command_type: CameraCommandType, } @@ -45,7 +45,7 @@ impl Default for CursorCommand { scale_delta: 0.0, fov_delta: 0.0, cursor_selection: None, - cursor_direction_camera_frame: Vec3::ZERO, + cursor_direction_camera_frame: None, command_type: CameraCommandType::Inactive, } } @@ -88,19 +88,19 @@ pub fn update_cursor_command( } // Command type, return if inactive - let prev_command_type = cursor_command.command_type; + let command_type_prev = cursor_command.command_type; let command_type = get_command_type( &keyboard_input, &mouse_input, &cursor_motion, &scroll_motion, camera_controls.mode(), - &prev_command_type, + &command_type_prev, ); - // if command_type == CameraCommandType::Inactive { - // *cursor_command = CursorCommand::default(); - // return; - // } + if command_type == CameraCommandType::Inactive { + *cursor_command = CursorCommand::default(); + return; + } // Camera projection and transform let active_camera_entity = match camera_controls.mode() { @@ -117,16 +117,20 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; - let new_cursor_selection = get_cursor_selected_point(&cursor_raycast_source); + let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); let cursor_selection = match cursor_command.cursor_selection { Some(selection) => selection, - None => new_cursor_selection, + None => cursor_selection_new, }; let cursor_direction = cursor_ray.direction().normalize(); + let cursor_direction_camera_frame = camera_transform.rotation.inverse() * cursor_direction; + let cursor_direction_camera_frame_prev = cursor_command + .cursor_direction_camera_frame + .unwrap_or(cursor_direction_camera_frame); // Update orbit center - let was_orbittting = prev_command_type == CameraCommandType::Orbit - || prev_command_type == CameraCommandType::HoldOrbitSelection; + let was_orbittting = command_type_prev == CameraCommandType::Orbit + || command_type_prev == CameraCommandType::HoldOrbitSelection; let is_orbitting = command_type == CameraCommandType::Orbit || command_type == CameraCommandType::HoldOrbitSelection; if !is_orbitting { @@ -143,7 +147,7 @@ pub fn update_cursor_command( &camera_proj, command_type, cursor_selection, - new_cursor_selection, + cursor_selection_new, scroll_motion, ); } @@ -155,10 +159,12 @@ pub fn update_cursor_command( &camera_proj, command_type, cursor_direction, - cursor_command.cursor_direction_camera_frame, + cursor_direction_camera_frame, + cursor_direction_camera_frame_prev, cursor_selection, - camera_controls.orbit_center, scroll_motion, + camera_controls.orbit_center, + window, ); } } @@ -173,7 +179,7 @@ fn get_orthographic_cursor_command( camera_proj: &OrthographicProjection, command_type: CameraCommandType, cursor_selection: Vec3, - new_cursor_selection: Vec3, + cursor_selection_new: Vec3, scroll_motion: f32, ) -> CursorCommand { let mut cursor_command = CursorCommand::default(); @@ -183,7 +189,7 @@ fn get_orthographic_cursor_command( cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction - let cursor_direction = (new_cursor_selection - camera_transform.translation).normalize(); + let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); match command_type { CameraCommandType::Pan => { @@ -244,10 +250,12 @@ fn get_perspective_cursor_command( camera_proj: &PerspectiveProjection, command_type: CameraCommandType, cursor_direction: Vec3, + cursor_direction_camera_frame: Vec3, cursor_direction_camera_frame_prev: Vec3, cursor_selection: Vec3, - orbit_center: Option, scroll_motion: f32, + orbit_center: Option, + window: &Window, ) -> CursorCommand { let translation_zoom_sensitivity = 0.5; let fov_zoom_sensitivity = 0.1; @@ -255,8 +263,6 @@ fn get_perspective_cursor_command( let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; - let cursor_direction_camera_frame = camera_transform.rotation.inverse() * cursor_direction; - match command_type { CameraCommandType::FovZoom => { let target_fov = (camera_proj.fov - scroll_motion * fov_zoom_sensitivity) @@ -304,16 +310,24 @@ fn get_perspective_cursor_command( is_cursor_selecting = true; } CameraCommandType::Orbit => { - let pitch = cursor_direction_camera_frame_prev.y.acos() + // Pitch and yaw inputs from cursor direction vector as spherical coordinates + let pitch_input = cursor_direction_camera_frame_prev.y.acos() - cursor_direction_camera_frame.y.acos(); - let yaw = (cursor_direction_camera_frame_prev + let yaw_input = (cursor_direction_camera_frame_prev .z .atan2(cursor_direction_camera_frame_prev.x)) - (cursor_direction_camera_frame .z .atan2(cursor_direction_camera_frame.x)); - let yaw = Quat::from_rotation_z(-yaw); - let pitch = Quat::from_rotation_x(-pitch); + + // Scale inputs to viewport range + let pitch_viewport_range = camera_proj.fov; + let pitch = pitch_input / pitch_viewport_range * std::f32::consts::PI; + let pitch = Quat::from_rotation_x(pitch); + let yaw_viewport_range = camera_proj.fov * camera_proj.aspect_ratio; + let yaw = yaw_input / yaw_viewport_range * 2.0 * std::f32::consts::PI; + let yaw = Quat::from_rotation_z(yaw); + let mut target_transform = camera_transform.clone(); // Rotation @@ -346,15 +360,13 @@ fn get_perspective_cursor_command( let target_rotation = Mat3::from_quat(target_transform.rotation); cursor_command.rotation_delta = Quat::from_mat3(&(start_rotation.inverse() * target_rotation)); - cursor_command.cursor_direction_camera_frame = - camera_transform.rotation.inverse() * cursor_direction; is_cursor_selecting = true; } _ => (), } cursor_command.command_type = command_type; - cursor_command.cursor_direction_camera_frame = cursor_direction_camera_frame; + cursor_command.cursor_direction_camera_frame = Some(cursor_direction_camera_frame); cursor_command.cursor_selection = if is_cursor_selecting { Some(cursor_selection) } else { @@ -395,7 +407,7 @@ fn get_command_type( cursor_motion: &Vec2, scroll_motion: &f32, projection_mode: ProjectionMode, - prev_command_type: &CameraCommandType, + command_type_prev: &CameraCommandType, ) -> CameraCommandType { // Inputs let is_cursor_moving = cursor_motion.length() > 0.; @@ -414,8 +426,8 @@ fn get_command_type( { return CameraCommandType::Orbit; } else if is_shifting - && (*prev_command_type == CameraCommandType::Orbit - || *prev_command_type == CameraCommandType::HoldOrbitSelection) + && (*command_type_prev == CameraCommandType::Orbit + || *command_type_prev == CameraCommandType::HoldOrbitSelection) && projection_mode.is_perspective() { return CameraCommandType::HoldOrbitSelection; From ddf5aa29a1c1016842074f31d1ee35a8cb9ca241 Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Mon, 17 Jun 2024 22:52:06 +0800 Subject: [PATCH 22/32] consistent camera controls, selecting orbit center from plane ahead Signed-off-by: Reuben Thomas --- .../src/interaction/camera_controls/cursor.rs | 69 ++++----- .../interaction/camera_controls/keyboard.rs | 137 ++++++++++-------- .../src/interaction/camera_controls/mod.rs | 31 +++- 3 files changed, 137 insertions(+), 100 deletions(-) diff --git a/rmf_site_editor/src/interaction/camera_controls/cursor.rs b/rmf_site_editor/src/interaction/camera_controls/cursor.rs index e0a9f0dc..b053f9f6 100644 --- a/rmf_site_editor/src/interaction/camera_controls/cursor.rs +++ b/rmf_site_editor/src/interaction/camera_controls/cursor.rs @@ -16,8 +16,8 @@ */ use super::{ - CameraCommandType, CameraControls, ProjectionMode, MAX_FOV, MAX_PITCH, MAX_SELECTION_DIST, - MIN_FOV, + get_groundplane_else_default_selection, CameraCommandType, CameraControls, ProjectionMode, + MAX_FOV, MAX_PITCH, MAX_SCALE, MIN_FOV, MIN_SCALE, }; use crate::interaction::SiteRaycastSet; use bevy::input::mouse::{MouseMotion, MouseWheel}; @@ -26,6 +26,10 @@ use bevy::window::PrimaryWindow; use bevy_mod_raycast::deferred::RaycastSource; use nalgebra::{Matrix3, Matrix3x1}; +pub const SCALE_ZOOM_SENSITIVITY: f32 = 0.1; +pub const TRANSLATION_ZOOM_SENSITIVITY: f32 = 0.2; +pub const FOV_ZOOM_SENSITIVITY: f32 = 0.1; + #[derive(Resource)] pub struct CursorCommand { pub translation_delta: Vec3, @@ -117,7 +121,8 @@ pub fn update_cursor_command( Some(ray) => ray, None => return, }; - let cursor_selection_new = get_cursor_selected_point(&cursor_raycast_source); + let cursor_selection_new = + get_cursor_selected_point(&camera_transform, &cursor_raycast_source); let cursor_selection = match cursor_command.cursor_selection { Some(selection) => selection, None => cursor_selection_new, @@ -164,7 +169,6 @@ pub fn update_cursor_command( cursor_selection, scroll_motion, camera_controls.orbit_center, - window, ); } } @@ -186,7 +190,10 @@ fn get_orthographic_cursor_command( let mut is_cursor_selecting = false; // Zoom - cursor_command.scale_delta = -scroll_motion * camera_proj.scale * 0.1; + let target_scale = (camera_proj.scale + - scroll_motion * camera_proj.scale * SCALE_ZOOM_SENSITIVITY) + .clamp(MIN_SCALE, MAX_SCALE); + cursor_command.scale_delta = target_scale - camera_proj.scale; //TODO(@reuben-thomas) Find out why cursor ray cannot be used for direction let cursor_direction = (cursor_selection_new - camera_transform.translation).normalize(); @@ -255,23 +262,24 @@ fn get_perspective_cursor_command( cursor_selection: Vec3, scroll_motion: f32, orbit_center: Option, - window: &Window, ) -> CursorCommand { - let translation_zoom_sensitivity = 0.5; - let fov_zoom_sensitivity = 0.1; - let mut cursor_command = CursorCommand::default(); let mut is_cursor_selecting = false; + let zoom_distance_factor = + (0.2 * (camera_transform.translation - cursor_selection).length()).max(1.0); + match command_type { CameraCommandType::FovZoom => { - let target_fov = (camera_proj.fov - scroll_motion * fov_zoom_sensitivity) + let target_fov = (camera_proj.fov - scroll_motion * FOV_ZOOM_SENSITIVITY) .clamp(MIN_FOV.to_radians(), MAX_FOV.to_radians()); cursor_command.fov_delta = target_fov - camera_proj.fov; } CameraCommandType::TranslationZoom => { - cursor_command.translation_delta = - cursor_direction * translation_zoom_sensitivity * scroll_motion; + cursor_command.translation_delta = cursor_direction + * TRANSLATION_ZOOM_SENSITIVITY + * scroll_motion + * zoom_distance_factor; } CameraCommandType::Pan => { // To keep the same point below the cursor, we solve @@ -301,8 +309,10 @@ fn get_perspective_cursor_command( ); let x = a.lu().solve(&b).unwrap(); - let zoom_translation = - camera_transform.forward() * translation_zoom_sensitivity * scroll_motion; + let zoom_translation = camera_transform.forward() + * TRANSLATION_ZOOM_SENSITIVITY + * scroll_motion + * zoom_distance_factor; cursor_command.translation_delta = zoom_translation + x[0] * right_translation + x[1] * up_translation; @@ -348,8 +358,9 @@ fn get_perspective_cursor_command( + target_transform.local_z() * z; let zoom_translation = camera_to_orbit_center_next.normalize() - * translation_zoom_sensitivity - * scroll_motion; + * TRANSLATION_ZOOM_SENSITIVITY + * scroll_motion + * zoom_distance_factor; target_transform.translation = orbit_center - camera_to_orbit_center_next - zoom_translation; } @@ -377,27 +388,19 @@ fn get_perspective_cursor_command( } // Returns the object selected by the cursor, if none, defaults to ground plane or arbitrary point in front -fn get_cursor_selected_point(cursor_raycast_source: &RaycastSource) -> Vec3 { +fn get_cursor_selected_point( + camera_transform: &Transform, + cursor_raycast_source: &RaycastSource, +) -> Vec3 { let cursor_ray = cursor_raycast_source.get_ray().unwrap(); match cursor_raycast_source.get_nearest_intersection() { Some((_, intersection)) => intersection.position(), - None => { - // If valid intersection with groundplane - let denom = Vec3::Z.dot(cursor_ray.direction()); - if denom.abs() > f32::EPSILON { - let dist = (-1.0 * cursor_ray.origin()).dot(Vec3::Z) / denom; - if dist > f32::EPSILON && dist < MAX_SELECTION_DIST { - return cursor_ray.origin() + cursor_ray.direction() * dist; - } - } - - // No groundplane intersection - // Pick a point of a virtual sphere around the camera, of same radius as its height - let height = cursor_ray.origin().z.abs(); - let radius = if height < 1.0 { 1.0 } else { height }; - return cursor_ray.origin() + cursor_ray.direction() * radius; - } + None => get_groundplane_else_default_selection( + cursor_ray.origin(), + cursor_ray.direction(), + camera_transform.forward(), + ), } } diff --git a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs index 9f99fab1..4d5c9d41 100644 --- a/rmf_site_editor/src/interaction/camera_controls/keyboard.rs +++ b/rmf_site_editor/src/interaction/camera_controls/keyboard.rs @@ -16,8 +16,8 @@ */ use super::{ - CameraCommandType, CameraControls, ProjectionMode, MAX_FOV, MAX_PITCH, MAX_SELECTION_DIST, - MIN_FOV, + get_groundplane_else_default_selection, CameraCommandType, CameraControls, ProjectionMode, + MAX_FOV, MAX_PITCH, MAX_SCALE, MIN_FOV, MIN_SCALE, }; use bevy::{prelude::*, window::PrimaryWindow}; use bevy_mod_raycast::{ @@ -25,6 +25,15 @@ use bevy_mod_raycast::{ primitives::Ray3d, }; +// Keyboard control limits +pub const MIN_RESPONSE_TIME: f32 = 0.25; // [s] time taken to reach minimum input, or to reset +pub const MAX_RESPONSE_TIME: f32 = 1.5; // [s] time taken to reach maximum input +pub const MAX_FOV_DELTA: f32 = 0.5; // [rad/s] +pub const MAX_ANGULAR_VEL: f32 = 3.5; // [rad/s] +pub const MAX_TRANSLATION_VEL: f32 = 8.0; // [m/s] +pub const SCALE_ZOOM_SENSITIVITY: f32 = 0.035; +pub const ORTHOGRAPHIC_PAN_SENSITIVITY: f32 = 0.015; + #[derive(Resource)] pub struct KeyboardCommand { pub translation_delta: Vec3, @@ -58,6 +67,7 @@ pub fn update_keyboard_command( keyboard_input: Res>, cameras: Query<(&Projection, &Transform)>, immediate_raycast: Raycast, + time: Res