use crate::camera::camera::Camera;
#[derive(Clone, Debug)]
pub struct TurntableController {
pub angular_velocity: f32,
pub tilt: f32,
pub azimuth: f32,
}
impl TurntableController {
pub fn new(angular_velocity: f32, tilt: f32) -> Self {
Self {
angular_velocity,
tilt,
azimuth: 0.0,
}
}
pub fn from_camera(camera: &Camera, angular_velocity: f32) -> Self {
let eye_dir = camera.orientation() * glam::Vec3::Z;
let tilt = eye_dir.z.clamp(-1.0, 1.0).acos();
let azimuth = eye_dir.x.atan2(-eye_dir.y);
Self {
angular_velocity,
tilt,
azimuth,
}
}
pub fn update(&mut self, dt: f32, camera: &mut Camera) {
self.azimuth += self.angular_velocity * dt;
self.azimuth = normalize_angle(self.azimuth);
camera.set_orientation(
glam::Quat::from_rotation_z(self.azimuth) * glam::Quat::from_rotation_x(self.tilt),
);
}
}
fn normalize_angle(angle: f32) -> f32 {
use std::f32::consts::TAU;
let a = angle % TAU;
if a > std::f32::consts::PI {
a - TAU
} else if a < -std::f32::consts::PI {
a + TAU
} else {
a
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_camera() -> Camera {
Camera::default()
}
#[test]
fn test_turntable_advances_azimuth() {
let mut cam = default_camera();
let mut tt = TurntableController::new(1.0, 1.0);
let start_az = tt.azimuth;
tt.update(0.1, &mut cam);
assert!((tt.azimuth - (start_az + 0.1)).abs() < 1e-5);
}
#[test]
fn test_turntable_does_not_change_distance() {
let mut cam = default_camera();
cam.set_distance(7.5);
let mut tt = TurntableController::new(1.0, 1.0);
tt.update(1.0, &mut cam);
assert!((cam.distance() - 7.5).abs() < 1e-5);
}
#[test]
fn test_turntable_does_not_change_center() {
let mut cam = default_camera();
cam.set_center(glam::Vec3::new(1.0, 2.0, 3.0));
let mut tt = TurntableController::new(1.0, 1.0);
tt.update(1.0, &mut cam);
assert!((cam.center() - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5);
}
#[test]
fn test_from_camera_round_trip() {
let cam = default_camera();
let tt = TurntableController::from_camera(&cam, 0.5);
let mut cam2 = default_camera();
cam2.set_orientation(
glam::Quat::from_rotation_z(tt.azimuth) * glam::Quat::from_rotation_x(tt.tilt),
);
let orig_eye = cam.orientation() * glam::Vec3::Z;
let new_eye = cam2.orientation() * glam::Vec3::Z;
let diff = (orig_eye - new_eye).length();
assert!(diff < 1e-4, "round-trip eye direction diff={diff}");
}
#[test]
fn test_azimuth_normalization() {
let mut cam = default_camera();
let mut tt = TurntableController::new(10.0, 1.0);
for _ in 0..1000 {
tt.update(0.1, &mut cam);
}
assert!(
tt.azimuth.abs() <= std::f32::consts::PI + 1e-4,
"azimuth out of range: {}",
tt.azimuth
);
}
#[test]
fn test_negative_velocity_reverses() {
let mut cam = default_camera();
let mut tt_fwd = TurntableController::new(1.0, 1.0);
let mut tt_rev = TurntableController::new(-1.0, 1.0);
tt_fwd.update(0.5, &mut cam);
tt_rev.update(0.5, &mut cam);
assert!(tt_fwd.azimuth > 0.0, "forward azimuth should be positive");
assert!(tt_rev.azimuth < 0.0, "reverse azimuth should be negative");
}
}