use glam::Vec3;
use super::action_frame::ActionFrame;
use super::look::{build_orientation, orientation_to_aim, yaw_pitch_from_orientation};
use crate::Camera;
pub struct FirstPersonCameraController {
pub yaw: f32,
pub pitch: f32,
pub sensitivity: f32,
pub pitch_clamp: f32,
}
impl FirstPersonCameraController {
pub const DEFAULT_SENSITIVITY: f32 = 0.005;
pub const DEFAULT_PITCH_CLAMP: f32 = 1.4;
pub fn new(sensitivity: f32, pitch_clamp: f32) -> Self {
Self {
yaw: 0.0,
pitch: 0.0,
sensitivity,
pitch_clamp: pitch_clamp.abs(),
}
}
pub fn apply(&mut self, camera: &mut Camera, frame: &ActionFrame, eye_position: Vec3) {
let look = frame.navigation.orbit;
self.yaw += look.x * self.sensitivity;
self.pitch -= look.y * self.sensitivity;
self.pitch = self.pitch.clamp(-self.pitch_clamp, self.pitch_clamp);
let orientation = build_orientation(self.yaw, self.pitch);
camera.center = eye_position + orientation_to_aim(orientation);
camera.distance = 1.0;
camera.orientation = orientation;
}
pub fn sync_from_camera(&mut self, camera: &Camera) {
let (yaw, pitch) = yaw_pitch_from_orientation(camera.orientation);
self.yaw = yaw;
self.pitch = pitch.clamp(-self.pitch_clamp, self.pitch_clamp);
}
pub fn set_look(&mut self, yaw: f32, pitch: f32) {
self.yaw = yaw;
self.pitch = pitch.clamp(-self.pitch_clamp, self.pitch_clamp);
}
pub fn forward_dir(&self) -> Vec3 {
let (s, c) = self.yaw.sin_cos();
Vec3::new(s, c, 0.0)
}
pub fn right_dir(&self) -> Vec3 {
self.forward_dir().cross(Vec3::Z).normalize_or_zero()
}
pub fn aim_dir(&self) -> Vec3 {
orientation_to_aim(build_orientation(self.yaw, self.pitch))
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec2;
fn cam() -> FirstPersonCameraController {
FirstPersonCameraController::new(0.002, 1.4)
}
fn look(x: f32, y: f32) -> ActionFrame {
let mut f = ActionFrame::default();
f.navigation.orbit = Vec2::new(x, y);
f
}
fn approx_eq(a: Vec3, b: Vec3) -> bool {
(a - b).length() < 1e-4
}
#[test]
fn default_aim_is_forward_along_y() {
assert!(approx_eq(cam().aim_dir(), Vec3::Y));
}
#[test]
fn right_look_increases_yaw() {
let mut c = cam();
let mut camera = Camera::default();
c.apply(&mut camera, &look(100.0, 0.0), Vec3::ZERO);
assert!(c.yaw > 0.0, "look right should increase yaw");
}
#[test]
fn down_look_decreases_pitch() {
let mut c = cam();
let mut camera = Camera::default();
c.apply(&mut camera, &look(0.0, 100.0), Vec3::ZERO);
assert!(c.pitch < 0.0, "look down should decrease pitch");
}
#[test]
fn pitch_is_clamped() {
let mut c = FirstPersonCameraController::new(1.0, 1.0);
let mut camera = Camera::default();
c.apply(&mut camera, &look(0.0, -9999.0), Vec3::ZERO);
assert!(c.pitch <= 1.0 && c.pitch >= -1.0, "pitch={}", c.pitch);
}
#[test]
fn eye_attaches_to_supplied_position() {
let mut c = cam();
let mut camera = Camera::default();
let eye = Vec3::new(3.0, 4.0, 1.8);
c.apply(&mut camera, &look(0.0, 0.0), eye);
assert!((camera.eye_position() - eye).length() < 1e-4);
}
#[test]
fn forward_dir_is_horizontal_and_rotates() {
let mut c = cam();
c.set_look(std::f32::consts::FRAC_PI_2, 0.0);
let fwd = c.forward_dir();
assert!(fwd.z.abs() < 1e-6, "forward must be horizontal");
assert!(approx_eq(fwd, Vec3::X), "forward at yaw=PI/2 should be +X");
}
#[test]
fn sync_from_camera_round_trips() {
let mut c = cam();
let mut camera = Camera::default();
c.set_look(0.7, 0.3);
c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
let mut c2 = cam();
c2.sync_from_camera(&camera);
assert!((c2.yaw - 0.7).abs() < 1e-4, "yaw={}", c2.yaw);
assert!((c2.pitch - 0.3).abs() < 1e-4, "pitch={}", c2.pitch);
}
}