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)
72 * glam::Quat::from_rotation_x(self.tilt),
73 );
74 }
75}
76
77fn normalize_angle(angle: f32) -> f32 {
83 use std::f32::consts::TAU;
84 let a = angle % TAU;
85 if a > std::f32::consts::PI {
86 a - TAU
87 } else if a < -std::f32::consts::PI {
88 a + TAU
89 } else {
90 a
91 }
92}
93
94#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn default_camera() -> Camera {
103 Camera::default()
104 }
105
106 #[test]
107 fn test_turntable_advances_azimuth() {
108 let mut cam = default_camera();
109 let mut tt = TurntableController::new(1.0, 1.0);
110 let start_az = tt.azimuth;
111 tt.update(0.1, &mut cam);
112 assert!((tt.azimuth - (start_az + 0.1)).abs() < 1e-5);
113 }
114
115 #[test]
116 fn test_turntable_does_not_change_distance() {
117 let mut cam = default_camera();
118 cam.set_distance(7.5);
119 let mut tt = TurntableController::new(1.0, 1.0);
120 tt.update(1.0, &mut cam);
121 assert!((cam.distance() - 7.5).abs() < 1e-5);
122 }
123
124 #[test]
125 fn test_turntable_does_not_change_center() {
126 let mut cam = default_camera();
127 cam.set_center(glam::Vec3::new(1.0, 2.0, 3.0));
128 let mut tt = TurntableController::new(1.0, 1.0);
129 tt.update(1.0, &mut cam);
130 assert!((cam.center() - glam::Vec3::new(1.0, 2.0, 3.0)).length() < 1e-5);
131 }
132
133 #[test]
134 fn test_from_camera_round_trip() {
135 let cam = default_camera();
136 let tt = TurntableController::from_camera(&cam, 0.5);
137 let mut cam2 = default_camera();
139 cam2.set_orientation(
140 glam::Quat::from_rotation_z(tt.azimuth) * glam::Quat::from_rotation_x(tt.tilt),
141 );
142 let orig_eye = cam.orientation() * glam::Vec3::Z;
143 let new_eye = cam2.orientation() * glam::Vec3::Z;
144 let diff = (orig_eye - new_eye).length();
145 assert!(diff < 1e-4, "round-trip eye direction diff={diff}");
146 }
147
148 #[test]
149 fn test_azimuth_normalization() {
150 let mut cam = default_camera();
151 let mut tt = TurntableController::new(10.0, 1.0);
153 for _ in 0..1000 {
154 tt.update(0.1, &mut cam);
155 }
156 assert!(
157 tt.azimuth.abs() <= std::f32::consts::PI + 1e-4,
158 "azimuth out of range: {}",
159 tt.azimuth
160 );
161 }
162
163 #[test]
164 fn test_negative_velocity_reverses() {
165 let mut cam = default_camera();
166 let mut tt_fwd = TurntableController::new(1.0, 1.0);
167 let mut tt_rev = TurntableController::new(-1.0, 1.0);
168 tt_fwd.update(0.5, &mut cam);
169 tt_rev.update(0.5, &mut cam);
170 assert!(tt_fwd.azimuth > 0.0, "forward azimuth should be positive");
171 assert!(tt_rev.azimuth < 0.0, "reverse azimuth should be negative");
172 }
173}