honeycomb_render/render/
camera.rs

1use bevy::input::mouse::{MouseMotion, MouseWheel};
2use bevy::math::vec2;
3use bevy::prelude::*;
4use bevy::window::PrimaryWindow;
5
6use crate::gui::{CustomTab, UiState};
7
8/// Taken from the bevy
9/// [cheatbook](https://bevy-cheatbook.github.io/cookbook/pan-orbit-camera.html).
10#[derive(Component)]
11pub struct PanOrbitCamera {
12    pub(crate) focus: Vec3,
13    pub(crate) radius: f32,
14    pub(crate) upside_down: bool,
15}
16
17impl Default for PanOrbitCamera {
18    fn default() -> Self {
19        Self {
20            focus: Vec3::ZERO,
21            radius: 5.0,
22            upside_down: false,
23        }
24    }
25}
26
27/// Camera update routine.
28pub fn update_camera(
29    window_q: Query<&Window>,
30    mut ev_motion: EventReader<MouseMotion>,
31    mut ev_scroll: EventReader<MouseWheel>,
32    input_mouse: Res<ButtonInput<MouseButton>>,
33    mut query: Query<(&mut PanOrbitCamera, &mut Transform, &Projection)>,
34) {
35    let window = window_q.single();
36    // change input mapping for orbit and panning here
37    let orbit_button = MouseButton::Right;
38    let pan_button = MouseButton::Left;
39
40    let mut pan = Vec2::ZERO;
41    let mut rotation_move = Vec2::ZERO;
42    let mut scroll = 0.0;
43    let mut orbit_button_changed = false;
44
45    if input_mouse.pressed(orbit_button) {
46        for ev in ev_motion.read() {
47            rotation_move += ev.delta;
48        }
49    } else if input_mouse.pressed(pan_button) {
50        // Pan only if we're not rotating at the moment
51        for ev in ev_motion.read() {
52            pan += ev.delta * 2.;
53        }
54    }
55
56    for ev in ev_scroll.read() {
57        scroll += ev.y;
58
59        scroll /= if cfg!(target_arch = "wasm32") {
60            100.0
61        } else {
62            20.0
63        };
64    }
65    if input_mouse.just_released(orbit_button) || input_mouse.just_pressed(orbit_button) {
66        orbit_button_changed = true;
67    }
68
69    for (mut pan_orbit, mut transform, projection) in &mut query {
70        if orbit_button_changed {
71            // only check for upside down when orbiting started or ended this frame
72            // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct
73            let up = transform.rotation * Vec3::Y;
74            pan_orbit.upside_down = up.y <= 0.0;
75        }
76        let window = vec2(
77            window.physical_width() as f32,
78            window.physical_height() as f32,
79        );
80
81        let mut any = false;
82        if rotation_move.length_squared() > 0.0 {
83            any = true;
84            let delta_x = {
85                let delta = rotation_move.x / window.x * std::f32::consts::PI * 2.0;
86                if pan_orbit.upside_down {
87                    -delta
88                } else {
89                    delta
90                }
91            };
92            let delta_y = rotation_move.y / window.y * std::f32::consts::PI;
93            let yaw = Quat::from_rotation_y(-delta_x);
94            let pitch = Quat::from_rotation_x(-delta_y);
95            transform.rotation = yaw * transform.rotation; // rotate around global y axis
96            transform.rotation *= pitch; // rotate around local x axis
97        } else if pan.length_squared() > 0.0 {
98            any = true;
99
100            if let Projection::Perspective(projection) = projection {
101                pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov) / window;
102            }
103            // translate by local axes
104            let right = transform.rotation * Vec3::X * -pan.x;
105            let up = transform.rotation * Vec3::Y * pan.y;
106            // make panning proportional to distance away from focus point
107            let translation = (right + up) * pan_orbit.radius;
108            pan_orbit.focus += translation;
109        } else if scroll.abs() > 0.0 {
110            any = true;
111            pan_orbit.radius -= scroll * pan_orbit.radius * 0.2;
112            // dont allow zoom to reach zero or you get stuck
113            pan_orbit.radius = f32::max(pan_orbit.radius, 0.05);
114        }
115
116        if any {
117            let rot_matrix = Mat3::from_quat(transform.rotation);
118            transform.translation =
119                pan_orbit.focus + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, pan_orbit.radius));
120        }
121    }
122
123    ev_motion.clear();
124}
125
126#[allow(clippy::missing_panics_doc)]
127/// Detects if the cursor is positioned in the render tab.
128///
129/// This is used to ignore camera related input when interacting with something other than the
130pub fn cursor_in_render(
131    q_windows: Query<&Window, With<PrimaryWindow>>,
132    ui_state: Res<UiState>,
133) -> bool {
134    // returns true if and only if the cursor is positioned in the render tab
135    if let Some(position) = q_windows.single().cursor_position() {
136        let tree = ui_state.tree();
137        if let Some((node_idx, _)) = tree.find_tab(&CustomTab::Render) {
138            let rect = &tree[node_idx].rect().expect("unreachable");
139            return rect.contains(position.to_array().into());
140        }
141        return false;
142    }
143    false
144}