viewport_lib/camera/
turntable.rs1use crate::camera::camera::Camera;
4
5#[derive(Clone, Debug)]
20pub struct TurntableController {
21 pub angular_velocity: f32,
24 pub tilt: f32,
28 pub azimuth: f32,
30}
31
32impl TurntableController {
33 pub fn new(angular_velocity: f32, tilt: f32) -> Self {
38 Self {
39 angular_velocity,
40 tilt,
41 azimuth: 0.0,
42 }
43 }
44
45 pub fn from_camera(camera: &Camera, angular_velocity: f32) -> Self {
50 let eye_dir = camera.orientation() * glam::Vec3::Z;
51 let tilt = eye_dir.z.clamp(-1.0, 1.0).acos();
53 let azimuth = eye_dir.x.atan2(-eye_dir.y);
57 Self {
58 angular_velocity,
59 tilt,
60 azimuth,
61 }
62 }
63
64 pub fn update(&mut self, dt: f32, camera: &mut Camera) {
67 self.azimuth += self.angular_velocity * dt;
68 self.azimuth = normalize_angle(self.azimuth);
70 camera.set_orientation(
71 glam::Quat::from_rotation_z(self.azimuth) * glam::Quat::from_rotation_x(self.tilt),
72 );
73 }
74}
75
76fn normalize_angle(angle: f32) -> f32 {
82 use std::f32::consts::TAU;
83 let a = angle % TAU;
84 if a > std::f32::consts::PI {
85 a - TAU
86 } else if a < -std::f32::consts::PI {
87 a + TAU
88 } else {
89 a
90 }
91}
92
93#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn default_camera() -> Camera {
102 Camera::default()
103 }
104
105 #[test]
106 fn test_turntable_advances_azimuth() {
107 let mut cam = default_camera();
108 let mut tt = TurntableController::new(1.0, 1.0);
109 let start_az = tt.azimuth;
110 tt.update(0.1, &mut cam);
111 assert!((tt.azimuth - (start_az + 0.1)).abs() < 1e-5);
112 }
113
114 #[test]
115 fn test_turntable_does_not_change_distance() {
116 let mut cam = default_camera();
117 cam.set_distance(7.5);
118 let mut tt = TurntableController::new(1.0, 1.0);
119 tt.update(1.0, &mut cam);
120 assert!((cam.distance() - 7.5).abs() < 1e-5);
121 }
122
123 #[test]
124 fn test_turntable_does_not_change_center() {
125 let mut cam = default_camera();
126 cam.set_center(glam::Vec3::new(1.0, 2.0, 3.0));
127 let mut tt = TurntableController::new(1.0, 1.0);
128 tt.update(1.0, &mut cam);
129 assert!((cam.center() - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5);
130 }
131
132 #[test]
133 fn test_from_camera_round_trip() {
134 let cam = default_camera();
135 let tt = TurntableController::from_camera(&cam, 0.5);
136 let mut cam2 = default_camera();
138 cam2.set_orientation(
139 glam::Quat::from_rotation_z(tt.azimuth) * glam::Quat::from_rotation_x(tt.tilt),
140 );
141 let orig_eye = cam.orientation() * glam::Vec3::Z;
142 let new_eye = cam2.orientation() * glam::Vec3::Z;
143 let diff = (orig_eye - new_eye).length();
144 assert!(diff < 1e-4, "round-trip eye direction diff={diff}");
145 }
146
147 #[test]
148 fn test_azimuth_normalization() {
149 let mut cam = default_camera();
150 let mut tt = TurntableController::new(10.0, 1.0);
152 for _ in 0..1000 {
153 tt.update(0.1, &mut cam);
154 }
155 assert!(
156 tt.azimuth.abs() <= std::f32::consts::PI + 1e-4,
157 "azimuth out of range: {}",
158 tt.azimuth
159 );
160 }
161
162 #[test]
163 fn test_negative_velocity_reverses() {
164 let mut cam = default_camera();
165 let mut tt_fwd = TurntableController::new(1.0, 1.0);
166 let mut tt_rev = TurntableController::new(-1.0, 1.0);
167 tt_fwd.update(0.5, &mut cam);
168 tt_rev.update(0.5, &mut cam);
169 assert!(tt_fwd.azimuth > 0.0, "forward azimuth should be positive");
170 assert!(tt_rev.azimuth < 0.0, "reverse azimuth should be negative");
171 }
172}