use std::{
f32::consts::{FRAC_PI_2, PI},
time::Duration,
};
use bevy_ecs::prelude::*;
use bevy_log::prelude::*;
use bevy_math::{prelude::*, DMat4, DQuat, DVec2, DVec3};
use bevy_reflect::prelude::*;
use bevy_render::prelude::*;
use bevy_time::prelude::*;
use bevy_transform::prelude::*;
use bevy_utils::Instant;
use bevy_window::RequestRedraw;
use super::{
inputs::MotionInputs,
momentum::{Momentum, Velocity},
motion::CurrentMotion,
projections::{OrthographicSettings, PerspectiveSettings},
smoothing::{InputQueue, Smoothing},
};
#[derive(Debug, Clone, Reflect, Component)]
pub struct EditorCam {
pub enabled_motion: EnabledMotion,
pub orbit_constraint: OrbitConstraint,
pub smoothing: Smoothing,
pub sensitivity: Sensitivity,
pub momentum: Momentum,
pub input_debounce: Duration,
pub perspective: PerspectiveSettings,
pub orthographic: OrthographicSettings,
pub last_anchor_depth: f64,
pub current_motion: CurrentMotion,
}
impl Default for EditorCam {
fn default() -> Self {
EditorCam {
orbit_constraint: Default::default(),
smoothing: Default::default(),
sensitivity: Default::default(),
momentum: Default::default(),
input_debounce: Duration::from_millis(80),
perspective: Default::default(),
orthographic: Default::default(),
enabled_motion: Default::default(),
current_motion: Default::default(),
last_anchor_depth: -2.0,
}
}
}
impl EditorCam {
pub fn new(
orbit: OrbitConstraint,
smoothness: Smoothing,
sensitivity: Sensitivity,
momentum: Momentum,
initial_anchor_depth: f64,
) -> Self {
Self {
orbit_constraint: orbit,
smoothing: smoothness,
sensitivity,
momentum,
last_anchor_depth: initial_anchor_depth.abs() * -1.0, ..Default::default()
}
}
pub fn with_initial_anchor_depth(self, initial_anchor_depth: f64) -> Self {
Self {
last_anchor_depth: initial_anchor_depth.abs() * -1.0, ..self
}
}
pub fn motion_inputs(&self) -> Option<&MotionInputs> {
match &self.current_motion {
CurrentMotion::Stationary => None,
CurrentMotion::Momentum { .. } => None,
CurrentMotion::UserControlled { motion_inputs, .. } => Some(motion_inputs),
}
}
fn maybe_update_anchor(&mut self, anchor: Option<DVec3>) -> DVec3 {
let anchor = anchor.unwrap_or(DVec3::new(0.0, 0.0, self.last_anchor_depth.abs() * -1.0));
self.last_anchor_depth = anchor.z;
anchor
}
pub fn anchor_view_space(&self) -> Option<DVec3> {
if let CurrentMotion::UserControlled { anchor, .. } = &self.current_motion {
Some(*anchor)
} else {
None
}
}
pub fn anchor_world_space(&self, camera_transform: &GlobalTransform) -> Option<DVec3> {
self.anchor_view_space().map(|anchor_view_space| {
camera_transform
.compute_matrix()
.as_dmat4()
.transform_point3(anchor_view_space)
});
self.anchor_view_space().map(|anchor_view_space| {
let (_, r, t) = camera_transform.to_scale_rotation_translation();
r.as_dquat() * anchor_view_space + t.as_dvec3()
})
}
pub fn is_actively_controlled(&self) -> bool {
!self.current_motion.is_zooming_only()
&& (self.current_motion.is_user_controlled()
|| self
.current_motion
.momentum_duration()
.map(|duration| duration < self.input_debounce)
.unwrap_or(false))
}
pub fn start_orbit(&mut self, anchor: Option<DVec3>) {
if !self.enabled_motion.orbit {
return;
}
self.current_motion = CurrentMotion::UserControlled {
anchor: self.maybe_update_anchor(anchor),
motion_inputs: MotionInputs::OrbitZoom {
screenspace_inputs: InputQueue::default(),
zoom_inputs: InputQueue::default(),
},
}
}
pub fn start_pan(&mut self, anchor: Option<DVec3>) {
if !self.enabled_motion.pan {
return;
}
self.current_motion = CurrentMotion::UserControlled {
anchor: self.maybe_update_anchor(anchor),
motion_inputs: MotionInputs::PanZoom {
screenspace_inputs: InputQueue::default(),
zoom_inputs: InputQueue::default(),
},
}
}
pub fn start_zoom(&mut self, anchor: Option<DVec3>) {
if !self.enabled_motion.zoom {
return;
}
let anchor = self.maybe_update_anchor(anchor);
let zoom_inputs = match self.current_motion {
CurrentMotion::Stationary | CurrentMotion::Momentum { .. } => InputQueue::default(),
CurrentMotion::UserControlled {
ref mut motion_inputs,
..
} => InputQueue(motion_inputs.zoom_inputs_mut().0.drain(..).collect()),
};
self.current_motion = CurrentMotion::UserControlled {
anchor,
motion_inputs: MotionInputs::Zoom { zoom_inputs },
}
}
pub fn send_screenspace_input(&mut self, screenspace_input: Vec2) {
if let CurrentMotion::UserControlled {
ref mut motion_inputs,
..
} = self.current_motion
{
match motion_inputs {
MotionInputs::OrbitZoom {
screenspace_inputs: ref mut movement,
..
} => movement.process_input(screenspace_input, self.smoothing.orbit),
MotionInputs::PanZoom {
screenspace_inputs: ref mut movement,
..
} => movement.process_input(screenspace_input, self.smoothing.pan),
MotionInputs::Zoom { .. } => (), }
}
}
pub fn send_zoom_input(&mut self, zoom_amount: f32) {
if let CurrentMotion::UserControlled { motion_inputs, .. } = &mut self.current_motion {
motion_inputs
.zoom_inputs_mut()
.process_input(zoom_amount, self.smoothing.zoom)
}
}
pub fn end_move(&mut self) {
let velocity = match self.current_motion {
CurrentMotion::Stationary => return,
CurrentMotion::Momentum { .. } => return,
CurrentMotion::UserControlled {
anchor,
ref motion_inputs,
..
} => match motion_inputs {
MotionInputs::OrbitZoom { .. } => Velocity::Orbit {
anchor,
velocity: motion_inputs.orbit_momentum(self.momentum.init_orbit),
},
MotionInputs::PanZoom { .. } => Velocity::Pan {
anchor,
velocity: motion_inputs.pan_momentum(self.momentum.init_pan),
},
MotionInputs::Zoom { .. } => Velocity::None,
},
};
let momentum_start = Instant::now();
self.current_motion = CurrentMotion::Momentum {
velocity,
momentum_start,
};
}
pub fn update_camera_positions(
mut cameras: Query<(&mut EditorCam, &Camera, &mut Transform, &mut Projection)>,
mut event: EventWriter<RequestRedraw>,
time: Res<Time>,
) {
for (mut camera_controller, camera, ref mut transform, ref mut projection) in
cameras.iter_mut()
{
let dt = time.delta();
camera_controller
.update_transform_and_projection(camera, transform, projection, &mut event, dt);
}
}
pub fn update_transform_and_projection(
&mut self,
camera: &Camera,
cam_transform: &mut Transform,
projection: &mut Projection,
redraw: &mut EventWriter<RequestRedraw>,
delta_time: Duration,
) {
let (anchor, orbit, pan, zoom) = match &mut self.current_motion {
CurrentMotion::Stationary => return,
CurrentMotion::Momentum {
ref mut velocity, ..
} => {
velocity.decay(self.momentum, delta_time);
match velocity {
Velocity::None => {
self.current_motion = CurrentMotion::Stationary;
return;
}
Velocity::Orbit { anchor, velocity } => (anchor, *velocity, DVec2::ZERO, 0.0),
Velocity::Pan { anchor, velocity } => (anchor, DVec2::ZERO, *velocity, 0.0),
}
}
CurrentMotion::UserControlled {
anchor,
motion_inputs,
} => (
anchor,
motion_inputs.smooth_orbit_velocity() * self.sensitivity.orbit.as_dvec2(),
motion_inputs.smooth_pan_velocity(),
motion_inputs.smooth_zoom_velocity() * self.sensitivity.zoom as f64,
),
};
redraw.send(RequestRedraw);
let screen_to_view_space_at_depth = |camera: &Camera, depth: f64| -> Option<DVec2> {
let target_size = camera.logical_viewport_size()?.as_dvec2();
let mut viewport_position = target_size / 2.0 - 1.0;
viewport_position.y = target_size.y - viewport_position.y;
let ndc = viewport_position * 2. / target_size - DVec2::ONE;
let ndc_to_view = match &projection {
Projection::Perspective(p) => DMat4::perspective_infinite_reverse_rh(
p.fov as f64,
p.aspect_ratio as f64,
p.near as f64,
),
Projection::Orthographic(o) => DMat4::orthographic_rh(
o.area.min.x as f64,
o.area.max.x as f64,
o.area.min.y as f64,
o.area.max.y as f64,
o.far as f64,
o.near as f64,
),
}
.inverse(); match &projection {
Projection::Perspective(_) => {
let view_near_plane = ndc_to_view.project_point3(ndc.extend(1.));
let view_far_plane = ndc_to_view.project_point3(ndc.extend(f64::EPSILON));
let direction = view_far_plane - view_near_plane;
let depth_normalized_direction = direction / direction.z;
let view_pos = depth_normalized_direction * depth;
debug_assert_eq!(view_pos.z, depth);
Some(view_pos.truncate())
}
Projection::Orthographic(_) => {
Some(ndc_to_view.project_point3(ndc.extend(0.)).truncate())
}
}
};
let Some(view_offset) = screen_to_view_space_at_depth(camera, anchor.z) else {
error!("Malformed camera");
return;
};
let pan_translation_view_space = (pan * view_offset).extend(0.0);
let zoom_unscaled = (zoom.abs() / 60.0).powf(1.3);
let scaled_zoom = (1.0 - 1.0 / (zoom_unscaled + 1.0)) * zoom.signum();
let zoom_translation_view_space = match projection {
Projection::Perspective(_) => anchor.normalize() * scaled_zoom * anchor.z * -0.15,
Projection::Orthographic(ref mut ortho) => {
ortho.scale *= 1.0 - scaled_zoom as f32 * 0.15;
anchor.normalize() * scaled_zoom * anchor.z.abs() * 0.15 * DVec3::new(1.0, 1.0, 0.0)
}
};
cam_transform.translation += (cam_transform.rotation.as_dquat()
* (pan_translation_view_space + zoom_translation_view_space))
.as_vec3();
*anchor -= pan_translation_view_space + zoom_translation_view_space;
let orbit = orbit * DVec2::new(-1.0, 1.0);
let anchor_world = cam_transform
.compute_matrix()
.as_dmat4()
.transform_point3(*anchor);
let orbit_dir = orbit.normalize().extend(0.0);
let orbit_axis_world = cam_transform
.rotation
.as_dquat()
.mul_vec3(orbit_dir.cross(DVec3::NEG_Z).normalize())
.normalize();
let rotate_around = |transform: &mut Transform, point: DVec3, rotation: DQuat| {
transform.translation =
(point + rotation * (transform.translation.as_dvec3() - point)).as_vec3();
transform.rotation = (rotation * transform.rotation.as_dquat()).as_quat();
};
let orbit_multiplier = 0.005;
if orbit.is_finite() && orbit.length() != 0.0 {
match self.orbit_constraint {
OrbitConstraint::Fixed { up, can_pass_tdc } => {
let epsilon = 1e-3;
let motion_threshold = 1e-5;
let angle_to_bdc = cam_transform.forward().angle_between(up) as f64;
let angle_to_tdc = cam_transform.forward().angle_between(-up) as f64;
let pitch_angle = {
let desired_rotation = orbit.y * orbit_multiplier;
if can_pass_tdc {
desired_rotation
} else if desired_rotation >= 0.0 {
desired_rotation.min(angle_to_tdc - (epsilon as f64).min(angle_to_tdc))
} else {
desired_rotation.max(-angle_to_bdc + (epsilon as f64).min(angle_to_bdc))
}
};
let pitch = if pitch_angle.abs() <= motion_threshold {
DQuat::IDENTITY
} else {
DQuat::from_axis_angle(cam_transform.left().as_dvec3(), pitch_angle)
};
let yaw_angle = orbit.x * orbit_multiplier;
let yaw = if yaw_angle.abs() <= motion_threshold {
DQuat::IDENTITY
} else {
DQuat::from_axis_angle(up.as_dvec3(), yaw_angle)
};
match [pitch == DQuat::IDENTITY, yaw == DQuat::IDENTITY] {
[true, true] => (),
[true, false] => rotate_around(cam_transform, anchor_world, yaw),
[false, true] => rotate_around(cam_transform, anchor_world, pitch),
[false, false] => rotate_around(cam_transform, anchor_world, yaw * pitch),
};
let how_upright = cam_transform.up().angle_between(up).abs();
if how_upright > epsilon && how_upright < FRAC_PI_2 - epsilon {
cam_transform.look_to(cam_transform.forward().into(), up);
} else if how_upright > FRAC_PI_2 + epsilon && how_upright < PI - epsilon {
cam_transform.look_to(cam_transform.forward().into(), -up);
}
}
OrbitConstraint::Free => {
let rotation =
DQuat::from_axis_angle(orbit_axis_world, orbit.length() * orbit_multiplier);
rotate_around(cam_transform, anchor_world, rotation);
}
}
}
self.last_anchor_depth = anchor.z;
}
pub fn last_anchor_depth(&self) -> f64 {
self.last_anchor_depth.abs() * -1.0
}
}
#[derive(Debug, Clone, Copy, Reflect)]
pub enum OrbitConstraint {
Fixed {
up: Vec3,
can_pass_tdc: bool,
},
Free,
}
impl Default for OrbitConstraint {
fn default() -> Self {
Self::Fixed {
up: Vec3::Y,
can_pass_tdc: false,
}
}
}
#[derive(Debug, Clone, Copy, Reflect)]
pub struct Sensitivity {
pub orbit: Vec2,
pub zoom: f32,
}
impl Default for Sensitivity {
fn default() -> Self {
Self {
orbit: Vec2::splat(1.0),
zoom: 1.0,
}
}
}
#[derive(Debug, Clone, Reflect)]
pub struct EnabledMotion {
pub pan: bool,
pub orbit: bool,
pub zoom: bool,
}
impl Default for EnabledMotion {
fn default() -> Self {
Self {
pan: true,
orbit: true,
zoom: true,
}
}
}