runmat_plot/core/
camera.rs

1//! Camera system for 3D navigation and 2D plotting
2//!
3//! Provides both perspective and orthographic cameras with smooth
4//! navigation controls for interactive plotting.
5
6use glam::{Mat4, Quat, Vec2, Vec3};
7
8/// Camera projection type
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum ProjectionType {
11    Perspective {
12        fov: f32,
13        near: f32,
14        far: f32,
15    },
16    Orthographic {
17        left: f32,
18        right: f32,
19        bottom: f32,
20        top: f32,
21        near: f32,
22        far: f32,
23    },
24}
25
26impl Default for ProjectionType {
27    fn default() -> Self {
28        Self::Perspective {
29            fov: 45.0_f32.to_radians(),
30            near: 0.1,
31            far: 100.0,
32        }
33    }
34}
35
36/// Interactive camera for 3D plotting with smooth navigation
37#[derive(Debug, Clone)]
38pub struct Camera {
39    // Position and orientation
40    pub position: Vec3,
41    pub target: Vec3,
42    pub up: Vec3,
43
44    // Projection parameters
45    pub projection: ProjectionType,
46    pub aspect_ratio: f32,
47
48    // Navigation state
49    pub zoom: f32,
50    pub rotation: Quat,
51
52    // Interaction settings
53    pub pan_sensitivity: f32,
54    pub zoom_sensitivity: f32,
55    pub rotate_sensitivity: f32,
56
57    // Cached matrices
58    view_matrix: Mat4,
59    projection_matrix: Mat4,
60    view_proj_dirty: bool,
61}
62
63impl Default for Camera {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl Camera {
70    /// Create a new camera with default 3D settings
71    pub fn new() -> Self {
72        let mut camera = Self {
73            position: Vec3::new(0.0, 0.0, 5.0),
74            target: Vec3::ZERO,
75            up: Vec3::Y,
76            projection: ProjectionType::default(),
77            aspect_ratio: 16.0 / 9.0,
78            zoom: 1.0,
79            rotation: Quat::IDENTITY,
80            pan_sensitivity: 0.01,
81            zoom_sensitivity: 0.1,
82            rotate_sensitivity: 0.005,
83            view_matrix: Mat4::IDENTITY,
84            projection_matrix: Mat4::IDENTITY,
85            view_proj_dirty: true,
86        };
87        camera.update_matrices();
88        camera
89    }
90
91    /// Create a 2D orthographic camera for 2D plotting
92    pub fn new_2d(bounds: (f32, f32, f32, f32)) -> Self {
93        let (left, right, bottom, top) = bounds;
94        let mut camera = Self {
95            position: Vec3::new(0.0, 0.0, 1.0),
96            target: Vec3::new((left + right) / 2.0, (bottom + top) / 2.0, 0.0),
97            up: Vec3::Y,
98            projection: ProjectionType::Orthographic {
99                left,
100                right,
101                bottom,
102                top,
103                near: -1.0,
104                far: 1.0,
105            },
106            aspect_ratio: (right - left) / (top - bottom),
107            zoom: 1.0,
108            rotation: Quat::IDENTITY,
109            pan_sensitivity: 0.01,
110            zoom_sensitivity: 0.1,
111            rotate_sensitivity: 0.0, // Disable rotation for 2D
112            view_matrix: Mat4::IDENTITY,
113            projection_matrix: Mat4::IDENTITY,
114            view_proj_dirty: true,
115        };
116        camera.update_matrices();
117        camera
118    }
119
120    /// Update aspect ratio (call when window resizes)
121    pub fn update_aspect_ratio(&mut self, aspect_ratio: f32) {
122        self.aspect_ratio = aspect_ratio;
123        self.view_proj_dirty = true;
124    }
125
126    /// Get the view-projection matrix
127    pub fn view_proj_matrix(&mut self) -> Mat4 {
128        if self.view_proj_dirty {
129            self.update_matrices();
130        }
131        self.projection_matrix * self.view_matrix
132    }
133
134    /// Mark the camera matrices as dirty (call after manually modifying projection)
135    pub fn mark_dirty(&mut self) {
136        self.view_proj_dirty = true;
137    }
138
139    /// Get the view matrix
140    pub fn view_matrix(&mut self) -> Mat4 {
141        if self.view_proj_dirty {
142            self.update_matrices();
143        }
144        self.view_matrix
145    }
146
147    /// Get the projection matrix
148    pub fn projection_matrix(&mut self) -> Mat4 {
149        if self.view_proj_dirty {
150            self.update_matrices();
151        }
152        self.projection_matrix
153    }
154
155    /// Pan the camera (screen-space movement)
156    pub fn pan(&mut self, delta: Vec2) {
157        let right = self.view_matrix.x_axis.truncate();
158        let up = self.view_matrix.y_axis.truncate();
159
160        let pan_amount = delta * self.pan_sensitivity * self.zoom;
161        let world_delta = right * pan_amount.x + up * pan_amount.y;
162
163        self.position += world_delta;
164        self.target += world_delta;
165        self.view_proj_dirty = true;
166    }
167
168    /// Zoom the camera (positive = zoom in, negative = zoom out)
169    pub fn zoom(&mut self, delta: f32) {
170        self.zoom *= 1.0 + delta * self.zoom_sensitivity;
171        self.zoom = self.zoom.clamp(0.01, 100.0);
172
173        match &mut self.projection {
174            ProjectionType::Perspective { .. } => {
175                // For perspective, move camera closer/farther
176                let direction = (self.position - self.target).normalize();
177                let distance = (self.position - self.target).length();
178                let new_distance = distance * (1.0 + delta * self.zoom_sensitivity);
179                self.position = self.target + direction * new_distance.clamp(0.1, 1000.0);
180            }
181            ProjectionType::Orthographic {
182                left,
183                right,
184                bottom,
185                top,
186                ..
187            } => {
188                // For orthographic, scale the view bounds
189                let center_x = (*left + *right) / 2.0;
190                let center_y = (*bottom + *top) / 2.0;
191                let width = (*right - *left) * self.zoom;
192                let height = (*top - *bottom) * self.zoom;
193
194                *left = center_x - width / 2.0;
195                *right = center_x + width / 2.0;
196                *bottom = center_y - height / 2.0;
197                *top = center_y + height / 2.0;
198            }
199        }
200
201        self.view_proj_dirty = true;
202    }
203
204    /// Rotate the camera around the target (for 3D)
205    pub fn rotate(&mut self, delta: Vec2) {
206        if self.rotate_sensitivity == 0.0 {
207            return; // Rotation disabled (e.g., for 2D mode)
208        }
209
210        let yaw_delta = -delta.x * self.rotate_sensitivity;
211        let pitch_delta = -delta.y * self.rotate_sensitivity;
212
213        // Create rotation quaternions
214        let yaw_rotation = Quat::from_axis_angle(Vec3::Y, yaw_delta);
215        let pitch_rotation = Quat::from_axis_angle(Vec3::X, pitch_delta);
216
217        // Apply rotations
218        self.rotation = yaw_rotation * self.rotation * pitch_rotation;
219
220        // Update position based on rotation
221        let distance = (self.position - self.target).length();
222        let direction = self.rotation * Vec3::new(0.0, 0.0, distance);
223        self.position = self.target + direction;
224
225        self.view_proj_dirty = true;
226    }
227
228    /// Set camera to look at a specific target
229    pub fn look_at(&mut self, target: Vec3, distance: Option<f32>) {
230        self.target = target;
231
232        if let Some(dist) = distance {
233            let direction = (self.position - self.target).normalize();
234            self.position = self.target + direction * dist;
235        }
236
237        self.view_proj_dirty = true;
238    }
239
240    /// Reset camera to default position
241    pub fn reset(&mut self) {
242        match self.projection {
243            ProjectionType::Perspective { .. } => {
244                self.position = Vec3::new(0.0, 0.0, 5.0);
245                self.target = Vec3::ZERO;
246                self.rotation = Quat::IDENTITY;
247            }
248            ProjectionType::Orthographic { .. } => {
249                self.zoom = 1.0;
250                self.target = Vec3::ZERO;
251            }
252        }
253        self.view_proj_dirty = true;
254    }
255
256    /// Fit the camera to show all data within the given bounds
257    pub fn fit_bounds(&mut self, min_bounds: Vec3, max_bounds: Vec3) {
258        let center = (min_bounds + max_bounds) / 2.0;
259        let size = max_bounds - min_bounds;
260
261        match &mut self.projection {
262            ProjectionType::Perspective { .. } => {
263                let max_size = size.x.max(size.y).max(size.z);
264                let distance = max_size * 2.0; // Ensure everything fits
265
266                self.target = center;
267                let direction = (self.position - self.target).normalize();
268                self.position = self.target + direction * distance;
269            }
270            ProjectionType::Orthographic {
271                left,
272                right,
273                bottom,
274                top,
275                ..
276            } => {
277                let margin = 0.1; // 10% margin
278                let width = size.x * (1.0 + margin);
279                let height = size.y * (1.0 + margin);
280
281                // Maintain aspect ratio
282                let display_width = width.max(height * self.aspect_ratio);
283                let display_height = height.max(width / self.aspect_ratio);
284
285                *left = center.x - display_width / 2.0;
286                *right = center.x + display_width / 2.0;
287                *bottom = center.y - display_height / 2.0;
288                *top = center.y + display_height / 2.0;
289
290                self.target = center;
291            }
292        }
293
294        self.view_proj_dirty = true;
295    }
296
297    /// Convert screen coordinates to world coordinates (for picking)
298    pub fn screen_to_world(&self, screen_pos: Vec2, screen_size: Vec2, depth: f32) -> Vec3 {
299        // Convert screen coordinates to normalized device coordinates
300        let ndc_x = (2.0 * screen_pos.x) / screen_size.x - 1.0;
301        let ndc_y = 1.0 - (2.0 * screen_pos.y) / screen_size.y;
302        let ndc = Vec3::new(ndc_x, ndc_y, depth * 2.0 - 1.0);
303
304        // Unproject to world coordinates
305        let view_proj_inv = (self.projection_matrix * self.view_matrix).inverse();
306        let world_pos = view_proj_inv * ndc.extend(1.0);
307
308        if world_pos.w != 0.0 {
309            world_pos.truncate() / world_pos.w
310        } else {
311            world_pos.truncate()
312        }
313    }
314
315    /// Update the view and projection matrices
316    fn update_matrices(&mut self) {
317        // Update view matrix
318        self.view_matrix = Mat4::look_at_rh(self.position, self.target, self.up);
319
320        // Update projection matrix
321        self.projection_matrix = match self.projection {
322            ProjectionType::Perspective { fov, near, far } => {
323                Mat4::perspective_rh(fov, self.aspect_ratio, near, far)
324            }
325            ProjectionType::Orthographic {
326                left,
327                right,
328                bottom,
329                top,
330                near,
331                far,
332            } => {
333                println!("ORTHO: Creating matrix with bounds: left={left}, right={right}, bottom={bottom}, top={top}, near={near}, far={far}");
334                println!("ORTHO: Camera aspect_ratio={}", self.aspect_ratio);
335                Mat4::orthographic_rh(left, right, bottom, top, near, far)
336            }
337        };
338
339        self.view_proj_dirty = false;
340    }
341}
342
343/// Camera controller for handling input events
344#[derive(Debug, Default)]
345pub struct CameraController {
346    pub is_dragging: bool,
347    pub is_panning: bool,
348    pub last_mouse_pos: Vec2,
349    pub mouse_delta: Vec2,
350}
351
352impl CameraController {
353    pub fn new() -> Self {
354        Self::default()
355    }
356
357    /// Handle mouse press
358    pub fn mouse_press(&mut self, position: Vec2, button: MouseButton) {
359        self.last_mouse_pos = position;
360        match button {
361            MouseButton::Left => self.is_dragging = true,
362            MouseButton::Right => self.is_panning = true,
363            _ => {}
364        }
365    }
366
367    /// Handle mouse release
368    pub fn mouse_release(&mut self, _button: MouseButton) {
369        self.is_dragging = false;
370        self.is_panning = false;
371    }
372
373    /// Handle mouse movement
374    pub fn mouse_move(&mut self, position: Vec2, camera: &mut Camera) {
375        self.mouse_delta = position - self.last_mouse_pos;
376
377        if self.is_dragging {
378            camera.rotate(self.mouse_delta);
379        } else if self.is_panning {
380            camera.pan(self.mouse_delta);
381        }
382
383        self.last_mouse_pos = position;
384    }
385
386    /// Handle mouse wheel
387    pub fn mouse_wheel(&mut self, delta: f32, camera: &mut Camera) {
388        camera.zoom(delta);
389    }
390}
391
392/// Mouse button enum for camera control
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub enum MouseButton {
395    Left,
396    Right,
397    Middle,
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_camera_creation() {
406        let camera = Camera::new();
407        assert_eq!(camera.position, Vec3::new(0.0, 0.0, 5.0));
408        assert_eq!(camera.target, Vec3::ZERO);
409    }
410
411    #[test]
412    fn test_2d_camera() {
413        let camera = Camera::new_2d((-10.0, 10.0, -10.0, 10.0));
414        match camera.projection {
415            ProjectionType::Orthographic {
416                left,
417                right,
418                bottom,
419                top,
420                ..
421            } => {
422                assert_eq!(left, -10.0);
423                assert_eq!(right, 10.0);
424                assert_eq!(bottom, -10.0);
425                assert_eq!(top, 10.0);
426            }
427            _ => panic!("Expected orthographic projection"),
428        }
429    }
430
431    #[test]
432    fn test_camera_bounds_fitting() {
433        let mut camera = Camera::new_2d((-1.0, 1.0, -1.0, 1.0));
434        let min_bounds = Vec3::new(-5.0, -3.0, 0.0);
435        let max_bounds = Vec3::new(5.0, 3.0, 0.0);
436
437        camera.fit_bounds(min_bounds, max_bounds);
438
439        // Check that the bounds were expanded appropriately
440        match camera.projection {
441            ProjectionType::Orthographic {
442                left,
443                right,
444                bottom,
445                top,
446                ..
447            } => {
448                assert!(left <= -5.0);
449                assert!(right >= 5.0);
450                assert!(bottom <= -3.0);
451                assert!(top >= 3.0);
452            }
453            _ => panic!("Expected orthographic projection"),
454        }
455    }
456}