volren_core/interaction/
trackball.rs1use super::{
14 events::{InteractionContext, InteractionResult, MouseEventKind},
15 InteractionStyle, KeyEvent, MouseButton, MouseEvent,
16};
17use crate::camera::Camera;
18
19#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
21enum DragState {
22 #[default]
23 None,
24 Orbiting,
25 Dollying,
26 Panning,
27}
28
29#[derive(Debug)]
36pub struct TrackballStyle {
37 drag: DragState,
38 last_pos: (f64, f64),
39 pub orbit_sensitivity: f64,
41 pub zoom_sensitivity: f64,
43}
44
45impl TrackballStyle {
46 #[must_use]
48 pub fn new() -> Self {
49 Self {
50 drag: DragState::None,
51 last_pos: (0.0, 0.0),
52 orbit_sensitivity: 0.005,
53 zoom_sensitivity: 0.1,
54 }
55 }
56}
57
58impl Default for TrackballStyle {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl InteractionStyle for TrackballStyle {
65 fn on_mouse_event(
66 &mut self,
67 event: &MouseEvent,
68 _ctx: &InteractionContext,
69 camera: &mut Camera,
70 ) -> InteractionResult {
71 match event.kind {
72 MouseEventKind::Press(button) => {
73 self.last_pos = event.position;
74 self.drag = match button {
75 MouseButton::Left => DragState::Orbiting,
76 MouseButton::Right => DragState::Dollying,
77 MouseButton::Middle => DragState::Panning,
78 };
79 InteractionResult::nothing()
80 }
81
82 MouseEventKind::Release(_) => {
83 self.drag = DragState::None;
84 InteractionResult::nothing()
85 }
86
87 MouseEventKind::Move => {
88 let dx = event.position.0 - self.last_pos.0;
89 let dy = event.position.1 - self.last_pos.1;
90 self.last_pos = event.position;
91
92 if dx == 0.0 && dy == 0.0 {
93 return InteractionResult::nothing();
94 }
95
96 match self.drag {
97 DragState::None => return InteractionResult::nothing(),
98
99 DragState::Orbiting => {
100 let angle_h = dx * self.orbit_sensitivity;
101 let angle_v = dy * self.orbit_sensitivity;
102 camera.orbit(angle_h, angle_v);
103 }
104
105 DragState::Dollying => {
106 let delta = dy * camera.distance() * 0.01;
107 camera.dolly(delta);
108 }
109
110 DragState::Panning => {
111 let scale = camera.distance() * 0.001;
112 let right = camera.right();
113 let up = camera.view_up_ortho();
114 camera.pan(-right * dx * scale + up * dy * scale);
115 }
116 }
117
118 InteractionResult::camera_only()
119 }
120
121 MouseEventKind::Scroll(delta) => {
122 let factor = if delta > 0.0 {
123 1.0 - self.zoom_sensitivity * delta.abs()
124 } else {
125 1.0 + self.zoom_sensitivity * delta.abs()
126 };
127 camera.zoom(factor.max(0.01));
128 InteractionResult::camera_only()
129 }
130 }
131 }
132
133 fn on_key_event(
134 &mut self,
135 _event: &KeyEvent,
136 _ctx: &InteractionContext,
137 _camera: &mut Camera,
138 ) -> InteractionResult {
139 InteractionResult::nothing()
140 }
141}
142
143#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::interaction::Modifiers;
149 use approx::assert_abs_diff_eq;
150 use glam::DVec3;
151
152 fn default_camera() -> Camera {
153 Camera::new(DVec3::new(0.0, 0.0, 10.0), DVec3::ZERO, DVec3::Y)
154 }
155
156 fn ctx() -> InteractionContext {
157 InteractionContext {
158 viewport_width: 800.0,
159 viewport_height: 600.0,
160 volume_bounds: None,
161 }
162 }
163
164 fn mouse(pos: (f64, f64), kind: MouseEventKind) -> MouseEvent {
165 MouseEvent {
166 position: pos,
167 kind,
168 modifiers: Modifiers::default(),
169 }
170 }
171
172 #[test]
173 fn press_and_move_orbits() {
174 let mut style = TrackballStyle::new();
175 let mut cam = default_camera();
176 let d0 = cam.distance();
177
178 let r1 = style.on_mouse_event(
179 &mouse((0.0, 0.0), MouseEventKind::Press(MouseButton::Left)),
180 &ctx(),
181 &mut cam,
182 );
183 assert!(!r1.camera_changed);
184
185 let r2 = style.on_mouse_event(&mouse((50.0, 0.0), MouseEventKind::Move), &ctx(), &mut cam);
186 assert!(r2.camera_changed);
187 assert_abs_diff_eq!(cam.distance(), d0, epsilon = 1e-6);
189 }
190
191 #[test]
192 fn scroll_zooms_camera() {
193 let mut style = TrackballStyle::new();
194 let mut cam = default_camera();
195 let d0 = cam.distance();
196
197 style.on_mouse_event(
198 &mouse((400.0, 300.0), MouseEventKind::Scroll(1.0)),
199 &ctx(),
200 &mut cam,
201 );
202 assert!(cam.distance() < d0, "scroll should zoom in");
203 }
204
205 #[test]
206 fn no_drag_move_does_nothing() {
207 let mut style = TrackballStyle::new();
208 let mut cam = default_camera();
209 let pos0 = cam.position();
210
211 style.on_mouse_event(
212 &mouse((100.0, 100.0), MouseEventKind::Move),
213 &ctx(),
214 &mut cam,
215 );
216 assert_abs_diff_eq!(cam.position().x, pos0.x, epsilon = 1e-10);
217 }
218}