Skip to main content

ringkernel_wavesim3d/visualization/
camera.rs

1//! 3D camera system for visualization.
2//!
3//! Provides orbital camera controls similar to 3D modeling software.
4
5use glam::{Mat4, Vec3};
6
7/// 3D camera with perspective projection.
8#[derive(Debug, Clone)]
9pub struct Camera3D {
10    /// Camera position in world space
11    position: Vec3,
12    /// Point the camera is looking at
13    target: Vec3,
14    /// Up vector
15    up: Vec3,
16    /// Field of view in radians
17    fov: f32,
18    /// Aspect ratio (width / height)
19    aspect: f32,
20    /// Near clipping plane
21    near: f32,
22    /// Far clipping plane
23    far: f32,
24}
25
26impl Default for Camera3D {
27    fn default() -> Self {
28        Self {
29            position: Vec3::new(5.0, 3.0, 5.0),
30            target: Vec3::ZERO,
31            up: Vec3::Y,
32            fov: std::f32::consts::FRAC_PI_4, // 45 degrees
33            aspect: 16.0 / 9.0,
34            near: 0.1,
35            far: 1000.0,
36        }
37    }
38}
39
40impl Camera3D {
41    /// Create a new camera looking at the origin.
42    pub fn new(position: Vec3, target: Vec3) -> Self {
43        Self {
44            position,
45            target,
46            ..Default::default()
47        }
48    }
49
50    /// Create a camera positioned for a simulation grid.
51    pub fn for_grid(grid_size: (f32, f32, f32)) -> Self {
52        let center = Vec3::new(grid_size.0 / 2.0, grid_size.1 / 2.0, grid_size.2 / 2.0);
53        let distance = (grid_size.0.max(grid_size.2) * 1.5).max(5.0);
54
55        Self {
56            position: center + Vec3::new(distance, distance * 0.7, distance),
57            target: center,
58            ..Default::default()
59        }
60    }
61
62    /// Set the aspect ratio.
63    pub fn set_aspect(&mut self, aspect: f32) {
64        self.aspect = aspect;
65    }
66
67    /// Set the field of view in degrees.
68    pub fn set_fov_degrees(&mut self, degrees: f32) {
69        self.fov = degrees.to_radians();
70    }
71
72    /// Get the camera position.
73    pub fn position(&self) -> Vec3 {
74        self.position
75    }
76
77    /// Get the target position.
78    pub fn target(&self) -> Vec3 {
79        self.target
80    }
81
82    /// Set the camera position.
83    pub fn set_position(&mut self, position: Vec3) {
84        self.position = position;
85    }
86
87    /// Set the target position.
88    pub fn set_target(&mut self, target: Vec3) {
89        self.target = target;
90    }
91
92    /// Move the camera and target by a delta.
93    pub fn translate(&mut self, delta: Vec3) {
94        self.position += delta;
95        self.target += delta;
96    }
97
98    /// Get the view matrix.
99    pub fn view_matrix(&self) -> Mat4 {
100        Mat4::look_at_rh(self.position, self.target, self.up)
101    }
102
103    /// Get the projection matrix.
104    pub fn projection_matrix(&self) -> Mat4 {
105        Mat4::perspective_rh(self.fov, self.aspect, self.near, self.far)
106    }
107
108    /// Get the combined view-projection matrix.
109    pub fn view_projection_matrix(&self) -> Mat4 {
110        self.projection_matrix() * self.view_matrix()
111    }
112
113    /// Get the forward direction (normalized).
114    pub fn forward(&self) -> Vec3 {
115        (self.target - self.position).normalize()
116    }
117
118    /// Get the right direction (normalized).
119    pub fn right(&self) -> Vec3 {
120        self.forward().cross(self.up).normalize()
121    }
122
123    /// Get distance to target.
124    pub fn distance_to_target(&self) -> f32 {
125        (self.position - self.target).length()
126    }
127}
128
129/// Controller for orbital camera movement.
130pub struct CameraController {
131    /// Rotation speed (radians per pixel)
132    pub rotate_speed: f32,
133    /// Pan speed (units per pixel)
134    pub pan_speed: f32,
135    /// Zoom speed (multiplier per scroll unit)
136    pub zoom_speed: f32,
137    /// Minimum zoom distance
138    pub min_distance: f32,
139    /// Maximum zoom distance
140    pub max_distance: f32,
141    /// Current orbital angles (theta, phi)
142    theta: f32,
143    phi: f32,
144    /// Current distance from target
145    distance: f32,
146    /// Mouse state
147    is_rotating: bool,
148    is_panning: bool,
149    last_mouse: (f32, f32),
150}
151
152impl Default for CameraController {
153    fn default() -> Self {
154        Self {
155            rotate_speed: 0.005,
156            pan_speed: 0.01,
157            zoom_speed: 0.1,
158            min_distance: 0.5,
159            max_distance: 500.0,
160            theta: std::f32::consts::FRAC_PI_4,
161            phi: std::f32::consts::FRAC_PI_4,
162            distance: 10.0,
163            is_rotating: false,
164            is_panning: false,
165            last_mouse: (0.0, 0.0),
166        }
167    }
168}
169
170impl CameraController {
171    /// Create a new camera controller.
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Initialize the controller from an existing camera.
177    pub fn from_camera(camera: &Camera3D) -> Self {
178        let dir = camera.position - camera.target;
179        let distance = dir.length();
180
181        let theta = dir.x.atan2(dir.z);
182        let phi = (dir.y / distance).asin();
183
184        Self {
185            distance,
186            theta,
187            phi,
188            ..Default::default()
189        }
190    }
191
192    /// Handle mouse button press.
193    pub fn on_mouse_down(&mut self, button: MouseButton, x: f32, y: f32) {
194        self.last_mouse = (x, y);
195        match button {
196            MouseButton::Left => self.is_rotating = true,
197            MouseButton::Middle => self.is_panning = true,
198            MouseButton::Right => self.is_panning = true,
199        }
200    }
201
202    /// Handle mouse button release.
203    pub fn on_mouse_up(&mut self, button: MouseButton) {
204        match button {
205            MouseButton::Left => self.is_rotating = false,
206            MouseButton::Middle => self.is_panning = false,
207            MouseButton::Right => self.is_panning = false,
208        }
209    }
210
211    /// Handle mouse movement.
212    pub fn on_mouse_move(&mut self, x: f32, y: f32, camera: &mut Camera3D) {
213        let dx = x - self.last_mouse.0;
214        let dy = y - self.last_mouse.1;
215        self.last_mouse = (x, y);
216
217        if self.is_rotating {
218            self.theta -= dx * self.rotate_speed;
219            self.phi = (self.phi + dy * self.rotate_speed).clamp(
220                -std::f32::consts::FRAC_PI_2 + 0.01,
221                std::f32::consts::FRAC_PI_2 - 0.01,
222            );
223            self.update_camera(camera);
224        }
225
226        if self.is_panning {
227            let right = camera.right();
228            let up = camera.up;
229            let pan = (right * -dx + up * dy) * self.pan_speed * self.distance * 0.01;
230            camera.translate(pan);
231        }
232    }
233
234    /// Handle scroll/zoom.
235    pub fn on_scroll(&mut self, delta: f32, camera: &mut Camera3D) {
236        self.distance *= 1.0 - delta * self.zoom_speed;
237        self.distance = self.distance.clamp(self.min_distance, self.max_distance);
238        self.update_camera(camera);
239    }
240
241    /// Update camera position from orbital parameters.
242    pub fn update_camera(&self, camera: &mut Camera3D) {
243        let x = self.distance * self.phi.cos() * self.theta.sin();
244        let y = self.distance * self.phi.sin();
245        let z = self.distance * self.phi.cos() * self.theta.cos();
246
247        camera.position = camera.target + Vec3::new(x, y, z);
248    }
249
250    /// Reset to default view.
251    pub fn reset(&mut self, camera: &mut Camera3D, grid_size: (f32, f32, f32)) {
252        let center = Vec3::new(grid_size.0 / 2.0, grid_size.1 / 2.0, grid_size.2 / 2.0);
253        camera.target = center;
254
255        self.theta = std::f32::consts::FRAC_PI_4;
256        self.phi = std::f32::consts::FRAC_PI_4 * 0.5;
257        self.distance = (grid_size.0.max(grid_size.2) * 1.5).max(5.0);
258
259        self.update_camera(camera);
260    }
261
262    /// Set orbital angles directly.
263    pub fn set_orbit(&mut self, theta: f32, phi: f32, camera: &mut Camera3D) {
264        self.theta = theta;
265        self.phi = phi.clamp(
266            -std::f32::consts::FRAC_PI_2 + 0.01,
267            std::f32::consts::FRAC_PI_2 - 0.01,
268        );
269        self.update_camera(camera);
270    }
271
272    /// Set distance to target.
273    pub fn set_distance(&mut self, distance: f32, camera: &mut Camera3D) {
274        self.distance = distance.clamp(self.min_distance, self.max_distance);
275        self.update_camera(camera);
276    }
277
278    /// Get current orbital parameters.
279    pub fn orbit_params(&self) -> (f32, f32, f32) {
280        (self.theta, self.phi, self.distance)
281    }
282}
283
284/// Mouse button identifiers.
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum MouseButton {
287    Left,
288    Middle,
289    Right,
290}
291
292/// First-person camera controller for immersive view.
293pub struct FirstPersonController {
294    /// Movement speed (units per second)
295    pub move_speed: f32,
296    /// Look sensitivity
297    pub sensitivity: f32,
298    /// Current pitch (up/down rotation)
299    pitch: f32,
300    /// Current yaw (left/right rotation)
301    yaw: f32,
302    /// Movement input state
303    forward: bool,
304    backward: bool,
305    left: bool,
306    right: bool,
307    up: bool,
308    down: bool,
309}
310
311impl Default for FirstPersonController {
312    fn default() -> Self {
313        Self {
314            move_speed: 5.0,
315            sensitivity: 0.003,
316            pitch: 0.0,
317            yaw: 0.0,
318            forward: false,
319            backward: false,
320            left: false,
321            right: false,
322            up: false,
323            down: false,
324        }
325    }
326}
327
328impl FirstPersonController {
329    pub fn new() -> Self {
330        Self::default()
331    }
332
333    /// Handle key press.
334    pub fn on_key_down(&mut self, key: Key) {
335        match key {
336            Key::W => self.forward = true,
337            Key::S => self.backward = true,
338            Key::A => self.left = true,
339            Key::D => self.right = true,
340            Key::Space => self.up = true,
341            Key::Shift => self.down = true,
342            _ => {}
343        }
344    }
345
346    /// Handle key release.
347    pub fn on_key_up(&mut self, key: Key) {
348        match key {
349            Key::W => self.forward = false,
350            Key::S => self.backward = false,
351            Key::A => self.left = false,
352            Key::D => self.right = false,
353            Key::Space => self.up = false,
354            Key::Shift => self.down = false,
355            _ => {}
356        }
357    }
358
359    /// Handle mouse movement for look.
360    pub fn on_mouse_move(&mut self, dx: f32, dy: f32, camera: &mut Camera3D) {
361        self.yaw -= dx * self.sensitivity;
362        self.pitch = (self.pitch - dy * self.sensitivity).clamp(
363            -std::f32::consts::FRAC_PI_2 + 0.01,
364            std::f32::consts::FRAC_PI_2 - 0.01,
365        );
366
367        self.update_camera_direction(camera);
368    }
369
370    /// Update camera each frame.
371    pub fn update(&self, dt: f32, camera: &mut Camera3D) {
372        let forward = camera.forward();
373        let right = camera.right();
374
375        let mut movement = Vec3::ZERO;
376
377        if self.forward {
378            movement += forward;
379        }
380        if self.backward {
381            movement -= forward;
382        }
383        if self.right {
384            movement += right;
385        }
386        if self.left {
387            movement -= right;
388        }
389        if self.up {
390            movement += Vec3::Y;
391        }
392        if self.down {
393            movement -= Vec3::Y;
394        }
395
396        if movement.length_squared() > 0.0 {
397            movement = movement.normalize() * self.move_speed * dt;
398            camera.position += movement;
399            camera.target += movement;
400        }
401    }
402
403    fn update_camera_direction(&self, camera: &mut Camera3D) {
404        let direction = Vec3::new(
405            self.yaw.cos() * self.pitch.cos(),
406            self.pitch.sin(),
407            self.yaw.sin() * self.pitch.cos(),
408        )
409        .normalize();
410
411        camera.target = camera.position + direction;
412    }
413}
414
415/// Key identifiers for first-person controls.
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum Key {
418    W,
419    A,
420    S,
421    D,
422    Space,
423    Shift,
424    Other,
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_camera_creation() {
433        let camera = Camera3D::default();
434        assert!(camera.position.length() > 0.0);
435    }
436
437    #[test]
438    fn test_camera_matrices() {
439        let camera = Camera3D::default();
440
441        let view = camera.view_matrix();
442        let proj = camera.projection_matrix();
443        let view_proj = camera.view_projection_matrix();
444
445        // View-projection should be the product
446        let expected = proj * view;
447        for i in 0..4 {
448            for j in 0..4 {
449                assert!(
450                    (view_proj.col(i)[j] - expected.col(i)[j]).abs() < 0.001,
451                    "Mismatch at [{}, {}]",
452                    i,
453                    j
454                );
455            }
456        }
457    }
458
459    #[test]
460    fn test_camera_controller() {
461        let mut camera = Camera3D::default();
462        let mut controller = CameraController::from_camera(&camera);
463
464        let initial_distance = camera.distance_to_target();
465
466        // Zoom in
467        controller.on_scroll(1.0, &mut camera);
468        assert!(camera.distance_to_target() < initial_distance);
469    }
470
471    #[test]
472    fn test_for_grid() {
473        let camera = Camera3D::for_grid((10.0, 5.0, 10.0));
474
475        // Camera should be looking at approximately the center
476        let center = Vec3::new(5.0, 2.5, 5.0);
477        let diff = (camera.target - center).length();
478        assert!(diff < 0.1);
479    }
480}