Skip to main content

scena/
controls.rs

1//! Platform-neutral orbit, pan, fly, and focus controls.
2
3use crate::diagnostics::LookupError;
4use crate::scene::Vec3;
5use crate::scene::{CameraKey, Scene, Transform};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum PointerButton {
10    Primary,
11    Secondary,
12    Auxiliary,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum PointerEventKind {
18    Pressed,
19    Released,
20    Moved,
21    Wheel,
22    Cancelled,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct PointerEvent {
27    pub kind: PointerEventKind,
28    pub position: (f32, f32),
29    pub button: Option<PointerButton>,
30    pub delta: (f32, f32),
31    pub scroll_delta: f32,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum TouchEventKind {
37    Started,
38    Moved,
39    Pinched,
40    Ended,
41    Cancelled,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct TouchEvent {
46    pub kind: TouchEventKind,
47    pub position: (f32, f32),
48    pub delta: (f32, f32),
49    pub pinch_delta: f32,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum OrbitControlAction {
55    None,
56    BeginOrbit,
57    Orbit,
58    Pan,
59    Zoom,
60    End,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct OrbitControls {
65    target: Vec3,
66    distance: f32,
67    yaw_radians: f32,
68    pitch_radians: f32,
69    damping_factor: f32,
70    orbiting: bool,
71    panning: bool,
72}
73
74impl OrbitControls {
75    pub fn new(target: Vec3, distance: f32) -> Self {
76        Self {
77            target,
78            distance: distance.max(MIN_DISTANCE),
79            yaw_radians: 0.0,
80            pitch_radians: 0.0,
81            damping_factor: 0.0,
82            orbiting: false,
83            panning: false,
84        }
85    }
86
87    pub fn focus(mut self, target: Vec3, distance: f32) -> Self {
88        self.target = target;
89        self.distance = distance.max(MIN_DISTANCE);
90        self
91    }
92
93    pub fn with_damping(mut self, factor: f32) -> Self {
94        self.damping_factor = if factor.is_finite() {
95            factor.clamp(0.0, 1.0)
96        } else {
97            0.0
98        };
99        self
100    }
101
102    pub fn handle_pointer(&mut self, event: PointerEvent) -> OrbitControlAction {
103        match event.kind {
104            PointerEventKind::Pressed => match event.button {
105                Some(PointerButton::Primary) => {
106                    self.orbiting = true;
107                    OrbitControlAction::BeginOrbit
108                }
109                Some(PointerButton::Secondary) => {
110                    self.panning = true;
111                    OrbitControlAction::Pan
112                }
113                Some(PointerButton::Auxiliary) | None => OrbitControlAction::None,
114            },
115            PointerEventKind::Moved if self.orbiting => {
116                self.yaw_radians += event.delta.0 * ORBIT_RADIANS_PER_PIXEL;
117                self.pitch_radians = (self.pitch_radians + event.delta.1 * ORBIT_RADIANS_PER_PIXEL)
118                    .clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
119                OrbitControlAction::Orbit
120            }
121            PointerEventKind::Moved if self.panning => {
122                self.target.x -= event.delta.0 * PAN_UNITS_PER_PIXEL * self.distance;
123                self.target.y += event.delta.1 * PAN_UNITS_PER_PIXEL * self.distance;
124                OrbitControlAction::Pan
125            }
126            PointerEventKind::Wheel => {
127                let zoom = (1.0 + event.scroll_delta * ZOOM_SCALE).max(0.05);
128                self.distance = (self.distance * zoom).max(MIN_DISTANCE);
129                OrbitControlAction::Zoom
130            }
131            PointerEventKind::Released | PointerEventKind::Cancelled => {
132                self.orbiting = false;
133                self.panning = false;
134                OrbitControlAction::End
135            }
136            PointerEventKind::Moved => OrbitControlAction::None,
137        }
138    }
139
140    pub fn handle_touch(&mut self, event: TouchEvent) -> OrbitControlAction {
141        match event.kind {
142            TouchEventKind::Started => {
143                self.orbiting = true;
144                OrbitControlAction::BeginOrbit
145            }
146            TouchEventKind::Moved if self.orbiting => {
147                self.apply_orbit_delta(event.delta);
148                OrbitControlAction::Orbit
149            }
150            TouchEventKind::Pinched => {
151                self.apply_zoom_delta(event.pinch_delta);
152                OrbitControlAction::Zoom
153            }
154            TouchEventKind::Ended | TouchEventKind::Cancelled => {
155                self.orbiting = false;
156                self.panning = false;
157                OrbitControlAction::End
158            }
159            TouchEventKind::Moved => OrbitControlAction::None,
160        }
161    }
162
163    pub const fn target(&self) -> Vec3 {
164        self.target
165    }
166
167    pub const fn distance(&self) -> f32 {
168        self.distance
169    }
170
171    pub const fn yaw_radians(&self) -> f32 {
172        self.yaw_radians
173    }
174
175    pub const fn pitch_radians(&self) -> f32 {
176        self.pitch_radians
177    }
178
179    pub const fn damping_factor(&self) -> f32 {
180        self.damping_factor
181    }
182
183    pub fn apply_to_scene(&self, scene: &mut Scene, camera: CameraKey) -> Result<(), LookupError> {
184        let camera_node = scene
185            .camera_node(camera)
186            .ok_or(LookupError::CameraNotFound(camera))?;
187        let offset = self.camera_offset();
188        scene.align_to(
189            camera_node,
190            Transform::at(Vec3::new(
191                self.target.x + offset.x,
192                self.target.y + offset.y,
193                self.target.z + offset.z,
194            )),
195        )?;
196        scene.ensure_camera_depth_reaches(camera, self.distance)?;
197        scene.look_at_point(camera, self.target)
198    }
199
200    fn camera_offset(&self) -> Vec3 {
201        let pitch_cos = self.pitch_radians.cos();
202        Vec3::new(
203            self.distance * self.yaw_radians.sin() * pitch_cos,
204            self.distance * self.pitch_radians.sin(),
205            self.distance * self.yaw_radians.cos() * pitch_cos,
206        )
207    }
208
209    fn apply_orbit_delta(&mut self, delta: (f32, f32)) {
210        self.yaw_radians += delta.0 * ORBIT_RADIANS_PER_PIXEL;
211        self.pitch_radians = (self.pitch_radians + delta.1 * ORBIT_RADIANS_PER_PIXEL)
212            .clamp(-MAX_PITCH_RADIANS, MAX_PITCH_RADIANS);
213    }
214
215    fn apply_zoom_delta(&mut self, delta: f32) {
216        let zoom = (1.0 + delta * ZOOM_SCALE).max(0.05);
217        self.distance = (self.distance * zoom).max(MIN_DISTANCE);
218    }
219}
220
221impl PointerEvent {
222    pub const fn primary_pressed(x: f32, y: f32) -> Self {
223        Self::pressed(x, y, PointerButton::Primary)
224    }
225
226    pub const fn secondary_pressed(x: f32, y: f32) -> Self {
227        Self::pressed(x, y, PointerButton::Secondary)
228    }
229
230    pub const fn released(x: f32, y: f32) -> Self {
231        Self {
232            kind: PointerEventKind::Released,
233            position: (x, y),
234            button: None,
235            delta: (0.0, 0.0),
236            scroll_delta: 0.0,
237        }
238    }
239
240    pub const fn moved(x: f32, y: f32, delta_x: f32, delta_y: f32) -> Self {
241        Self {
242            kind: PointerEventKind::Moved,
243            position: (x, y),
244            button: None,
245            delta: (delta_x, delta_y),
246            scroll_delta: 0.0,
247        }
248    }
249
250    pub const fn wheel(x: f32, y: f32, scroll_delta: f32) -> Self {
251        Self {
252            kind: PointerEventKind::Wheel,
253            position: (x, y),
254            button: None,
255            delta: (0.0, 0.0),
256            scroll_delta,
257        }
258    }
259
260    const fn pressed(x: f32, y: f32, button: PointerButton) -> Self {
261        Self {
262            kind: PointerEventKind::Pressed,
263            position: (x, y),
264            button: Some(button),
265            delta: (0.0, 0.0),
266            scroll_delta: 0.0,
267        }
268    }
269}
270
271impl TouchEvent {
272    pub const fn start(x: f32, y: f32) -> Self {
273        Self {
274            kind: TouchEventKind::Started,
275            position: (x, y),
276            delta: (0.0, 0.0),
277            pinch_delta: 0.0,
278        }
279    }
280
281    pub const fn move_by(x: f32, y: f32, delta_x: f32, delta_y: f32) -> Self {
282        Self {
283            kind: TouchEventKind::Moved,
284            position: (x, y),
285            delta: (delta_x, delta_y),
286            pinch_delta: 0.0,
287        }
288    }
289
290    pub const fn pinch(x: f32, y: f32, pinch_delta: f32) -> Self {
291        Self {
292            kind: TouchEventKind::Pinched,
293            position: (x, y),
294            delta: (0.0, 0.0),
295            pinch_delta,
296        }
297    }
298
299    pub const fn end(x: f32, y: f32) -> Self {
300        Self {
301            kind: TouchEventKind::Ended,
302            position: (x, y),
303            delta: (0.0, 0.0),
304            pinch_delta: 0.0,
305        }
306    }
307
308    pub const fn cancel(x: f32, y: f32) -> Self {
309        Self {
310            kind: TouchEventKind::Cancelled,
311            position: (x, y),
312            delta: (0.0, 0.0),
313            pinch_delta: 0.0,
314        }
315    }
316}
317
318const ORBIT_RADIANS_PER_PIXEL: f32 = 0.01;
319const PAN_UNITS_PER_PIXEL: f32 = 0.001;
320const ZOOM_SCALE: f32 = 0.1;
321const MIN_DISTANCE: f32 = 0.001;
322const MAX_PITCH_RADIANS: f32 = 1.553_343;