Skip to main content

anvilkit_camera/
controller.rs

1use bevy_ecs::prelude::*;
2use glam::Vec3;
3
4/// Camera control mode.
5pub enum CameraMode {
6    /// First-person: mouse directly controls yaw/pitch.
7    FirstPerson,
8    /// Third-person follow: orbit camera around a target point.
9    ThirdPerson {
10        target: Vec3,
11        distance: f32,
12        min_distance: f32,
13        max_distance: f32,
14    },
15    /// Free fly: editor/debug camera.
16    Free,
17}
18
19impl Default for CameraMode {
20    fn default() -> Self {
21        Self::FirstPerson
22    }
23}
24
25/// Camera controller component, attached to camera entities.
26#[derive(Component)]
27pub struct CameraController {
28    pub mode: CameraMode,
29    pub yaw: f32,
30    pub pitch: f32,
31    pub pitch_limits: (f32, f32),
32    pub mouse_sensitivity: f32,
33    pub move_speed: f32,
34    pub zoom_speed: f32,
35    pub smoothing: f32,
36    /// Base FOV in degrees (used as the reference for effects offsets)
37    pub base_fov: f32,
38    // Internal smooth state
39    pub(crate) smooth_yaw: f32,
40    pub(crate) smooth_pitch: f32,
41    pub(crate) smooth_position: Vec3,
42}
43
44impl Default for CameraController {
45    fn default() -> Self {
46        Self {
47            mode: CameraMode::FirstPerson,
48            yaw: 0.0,
49            pitch: 0.0,
50            pitch_limits: (-1.48, 1.48), // ~85 degrees
51            mouse_sensitivity: 0.003,
52            move_speed: 10.0,
53            zoom_speed: 2.0,
54            smoothing: 0.0,
55            base_fov: 70.0,
56            smooth_yaw: 0.0,
57            smooth_pitch: 0.0,
58            smooth_position: Vec3::ZERO,
59        }
60    }
61}
62
63impl CameraController {
64    /// Get the effective yaw (smoothed if smoothing > 0).
65    pub fn effective_yaw(&self) -> f32 {
66        if self.smoothing > 0.0 {
67            self.smooth_yaw
68        } else {
69            self.yaw
70        }
71    }
72
73    /// Get the effective pitch (smoothed if smoothing > 0).
74    pub fn effective_pitch(&self) -> f32 {
75        if self.smoothing > 0.0 {
76            self.smooth_pitch
77        } else {
78            self.pitch
79        }
80    }
81
82    /// Compute rotation quaternion from effective yaw/pitch.
83    pub fn rotation(&self) -> glam::Quat {
84        glam::Quat::from_rotation_y(self.effective_yaw())
85            * glam::Quat::from_rotation_x(self.effective_pitch())
86    }
87
88    /// Compute forward direction (yaw-only, for movement).
89    pub fn forward_xz(&self) -> Vec3 {
90        glam::Quat::from_rotation_y(self.effective_yaw()) * Vec3::Z
91    }
92
93    /// Compute right direction (yaw-only, for movement).
94    pub fn right_xz(&self) -> Vec3 {
95        glam::Quat::from_rotation_y(self.effective_yaw()) * Vec3::X
96    }
97
98    /// Toggle between FirstPerson and ThirdPerson modes.
99    pub fn toggle_perspective(&mut self, player_pos: Vec3) {
100        match &self.mode {
101            CameraMode::FirstPerson => {
102                self.mode = CameraMode::ThirdPerson {
103                    target: player_pos,
104                    distance: 5.0,
105                    min_distance: 2.0,
106                    max_distance: 20.0,
107                };
108                self.smoothing = 0.15;
109            }
110            CameraMode::ThirdPerson { .. } => {
111                self.mode = CameraMode::FirstPerson;
112                self.smoothing = 0.0;
113            }
114            CameraMode::Free => {
115                self.mode = CameraMode::FirstPerson;
116                self.smoothing = 0.0;
117            }
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_default_controller() {
128        let ctrl = CameraController::default();
129        assert_eq!(ctrl.yaw, 0.0);
130        assert_eq!(ctrl.pitch, 0.0);
131        assert_eq!(ctrl.base_fov, 70.0);
132        assert!(matches!(ctrl.mode, CameraMode::FirstPerson));
133    }
134
135    #[test]
136    fn test_rotation_identity_at_zero() {
137        let ctrl = CameraController::default();
138        let rot = ctrl.rotation();
139        let diff = rot.dot(glam::Quat::IDENTITY);
140        assert!((diff - 1.0).abs() < 1e-5);
141    }
142
143    #[test]
144    fn test_forward_xz_at_zero_yaw() {
145        let ctrl = CameraController::default();
146        let fwd = ctrl.forward_xz();
147        assert!(fwd.z.abs() > 0.99);
148    }
149
150    #[test]
151    fn test_toggle_perspective() {
152        let mut ctrl = CameraController::default();
153        assert!(matches!(ctrl.mode, CameraMode::FirstPerson));
154        ctrl.toggle_perspective(Vec3::ZERO);
155        assert!(matches!(ctrl.mode, CameraMode::ThirdPerson { .. }));
156        ctrl.toggle_perspective(Vec3::ZERO);
157        assert!(matches!(ctrl.mode, CameraMode::FirstPerson));
158    }
159
160    #[test]
161    fn test_pitch_limits() {
162        let ctrl = CameraController::default();
163        assert!(ctrl.pitch_limits.0 < 0.0);
164        assert!(ctrl.pitch_limits.1 > 0.0);
165    }
166}