1use glam::{Mat3, Mat4, Quat, Vec3};
4use std::time::Instant;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum NavigationStyle {
9 #[default]
11 Turntable,
12 Free,
14 Planar,
16 Arcball,
18 FirstPerson,
20 None,
22}
23
24impl From<u32> for NavigationStyle {
25 fn from(v: u32) -> Self {
26 match v {
27 0 => Self::Turntable,
28 1 => Self::Free,
29 2 => Self::Planar,
30 3 => Self::Arcball,
31 4 => Self::FirstPerson,
32 _ => Self::None,
33 }
34 }
35}
36
37impl From<NavigationStyle> for u32 {
38 fn from(v: NavigationStyle) -> Self {
39 match v {
40 NavigationStyle::Turntable => 0,
41 NavigationStyle::Free => 1,
42 NavigationStyle::Planar => 2,
43 NavigationStyle::Arcball => 3,
44 NavigationStyle::FirstPerson => 4,
45 NavigationStyle::None => 5,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum ProjectionMode {
53 #[default]
55 Perspective,
56 Orthographic,
58}
59
60impl From<u32> for ProjectionMode {
61 fn from(v: u32) -> Self {
62 match v {
63 0 => Self::Perspective,
64 _ => Self::Orthographic,
65 }
66 }
67}
68
69impl From<ProjectionMode> for u32 {
70 fn from(v: ProjectionMode) -> Self {
71 match v {
72 ProjectionMode::Perspective => 0,
73 ProjectionMode::Orthographic => 1,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum AxisDirection {
81 PosX,
83 NegX,
85 #[default]
87 PosY,
88 NegY,
90 PosZ,
92 NegZ,
94}
95
96impl From<u32> for AxisDirection {
97 fn from(v: u32) -> Self {
98 match v {
99 0 => Self::PosX,
100 1 => Self::NegX,
101 2 => Self::PosY,
102 3 => Self::NegY,
103 4 => Self::PosZ,
104 _ => Self::NegZ,
105 }
106 }
107}
108
109impl From<AxisDirection> for u32 {
110 fn from(v: AxisDirection) -> Self {
111 match v {
112 AxisDirection::PosX => 0,
113 AxisDirection::NegX => 1,
114 AxisDirection::PosY => 2,
115 AxisDirection::NegY => 3,
116 AxisDirection::PosZ => 4,
117 AxisDirection::NegZ => 5,
118 }
119 }
120}
121
122impl AxisDirection {
123 #[must_use]
125 pub fn to_vec3(self) -> Vec3 {
126 match self {
127 AxisDirection::PosX => Vec3::X,
128 AxisDirection::NegX => Vec3::NEG_X,
129 AxisDirection::PosY => Vec3::Y,
130 AxisDirection::NegY => Vec3::NEG_Y,
131 AxisDirection::PosZ => Vec3::Z,
132 AxisDirection::NegZ => Vec3::NEG_Z,
133 }
134 }
135
136 #[must_use]
138 pub fn name(self) -> &'static str {
139 match self {
140 AxisDirection::PosX => "+X",
141 AxisDirection::NegX => "-X",
142 AxisDirection::PosY => "+Y",
143 AxisDirection::NegY => "-Y",
144 AxisDirection::PosZ => "+Z",
145 AxisDirection::NegZ => "-Z",
146 }
147 }
148
149 #[must_use]
158 pub fn default_front_direction(self) -> AxisDirection {
159 match self {
160 AxisDirection::PosY => AxisDirection::NegZ,
161 AxisDirection::NegY => AxisDirection::PosZ,
162 AxisDirection::PosZ => AxisDirection::PosX,
163 AxisDirection::NegZ => AxisDirection::NegX,
164 AxisDirection::PosX => AxisDirection::PosY,
165 AxisDirection::NegX => AxisDirection::NegY,
166 }
167 }
168
169 #[must_use]
172 #[allow(clippy::match_same_arms)] pub fn from_index(index: u32) -> Self {
174 match index {
175 0 => AxisDirection::PosX,
176 1 => AxisDirection::NegX,
177 2 => AxisDirection::PosY,
178 3 => AxisDirection::NegY,
179 4 => AxisDirection::PosZ,
180 5 => AxisDirection::NegZ,
181 _ => AxisDirection::PosY, }
183 }
184
185 #[must_use]
187 pub fn to_index(self) -> u32 {
188 match self {
189 AxisDirection::PosX => 0,
190 AxisDirection::NegX => 1,
191 AxisDirection::PosY => 2,
192 AxisDirection::NegY => 3,
193 AxisDirection::PosZ => 4,
194 AxisDirection::NegZ => 5,
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
207pub struct CameraFlight {
208 start_time: Instant,
209 duration_secs: f32,
210 initial_rot: Quat,
212 target_rot: Quat,
214 initial_t: Vec3,
216 target_t: Vec3,
218 initial_fov: f32,
220 target_fov: f32,
222 target_dist: f32,
224}
225
226#[derive(Debug, Clone)]
228pub struct Camera {
229 pub position: Vec3,
231 pub target: Vec3,
233 pub up: Vec3,
235 pub fov: f32,
237 pub aspect_ratio: f32,
239 pub near: f32,
241 pub far: f32,
243 pub navigation_style: NavigationStyle,
245 pub projection_mode: ProjectionMode,
247 pub up_direction: AxisDirection,
249 pub front_direction: AxisDirection,
251 pub move_speed: f32,
253 pub ortho_scale: f32,
255 pub flight: Option<CameraFlight>,
257}
258
259impl Camera {
260 #[must_use]
262 pub fn new(aspect_ratio: f32) -> Self {
263 Self {
264 position: Vec3::new(0.0, 0.0, 3.0),
265 target: Vec3::ZERO,
266 up: Vec3::Y,
267 fov: std::f32::consts::FRAC_PI_4, aspect_ratio,
269 near: 0.01,
270 far: 1000.0,
271 navigation_style: NavigationStyle::Turntable,
272 projection_mode: ProjectionMode::Perspective,
273 up_direction: AxisDirection::PosY,
274 front_direction: AxisDirection::NegZ,
275 move_speed: 1.0,
276 ortho_scale: 1.0,
277 flight: None,
278 }
279 }
280
281 pub fn set_aspect_ratio(&mut self, aspect_ratio: f32) {
283 self.aspect_ratio = aspect_ratio;
284 }
285
286 #[must_use]
288 pub fn view_matrix(&self) -> Mat4 {
289 Mat4::look_at_rh(self.position, self.target, self.up)
290 }
291
292 #[must_use]
294 pub fn projection_matrix(&self) -> Mat4 {
295 match self.projection_mode {
296 ProjectionMode::Perspective => {
297 Mat4::perspective_rh(self.fov, self.aspect_ratio, self.near, self.far)
298 }
299 ProjectionMode::Orthographic => {
300 let half_height = self.ortho_scale;
301 let half_width = half_height * self.aspect_ratio;
302 let dist = (self.position - self.target).length();
307 let ortho_depth = (dist + self.far).max(self.ortho_scale * 100.0);
310 Mat4::orthographic_rh(
311 -half_width,
312 half_width,
313 -half_height,
314 half_height,
315 -ortho_depth, ortho_depth,
317 )
318 }
319 }
320 }
321
322 #[must_use]
324 pub fn view_projection_matrix(&self) -> Mat4 {
325 self.projection_matrix() * self.view_matrix()
326 }
327
328 #[must_use]
330 pub fn forward(&self) -> Vec3 {
331 (self.target - self.position).normalize()
332 }
333
334 #[must_use]
336 pub fn right(&self) -> Vec3 {
337 self.forward().cross(self.up).normalize()
338 }
339
340 #[must_use]
342 pub fn camera_up(&self) -> Vec3 {
343 let view = self.view_matrix();
344 let r = Mat3::from_cols(
345 view.x_axis.truncate(),
346 view.y_axis.truncate(),
347 view.z_axis.truncate(),
348 );
349 r.transpose() * Vec3::Y
350 }
351
352 pub fn orbit_turntable(&mut self, delta_x: f32, delta_y: f32) {
364 let up_vec = self.up_direction.to_vec3();
365
366 let view_mat = self.view_matrix();
368 let r = glam::Mat3::from_cols(
369 view_mat.x_axis.truncate(),
370 view_mat.y_axis.truncate(),
371 view_mat.z_axis.truncate(),
372 );
373 let rt = r.transpose();
374 let frame_look = rt * Vec3::new(0.0, 0.0, -1.0);
375 let frame_right = rt * Vec3::new(1.0, 0.0, 0.0);
376
377 let dot = frame_look.dot(up_vec);
381 let clamped_dy = if dot > 0.99 {
382 delta_y.max(0.0) } else if dot < -0.99 {
384 delta_y.min(0.0) } else {
386 delta_y
387 };
388
389 let mut vm = view_mat;
392 vm *= Mat4::from_translation(self.target);
393
394 vm *= Mat4::from_axis_angle(frame_right, clamped_dy);
399
400 vm *= Mat4::from_axis_angle(up_vec, delta_x);
403
404 vm *= Mat4::from_translation(-self.target);
406
407 let new_view = vm;
409 let inv_view = new_view.inverse();
410 let new_pos = Vec3::new(inv_view.w_axis.x, inv_view.w_axis.y, inv_view.w_axis.z);
411
412 let radius = (self.position - self.target).length();
414 let offset = new_pos - self.target;
415 let actual_dist = offset.length();
416 if actual_dist > 1e-8 {
417 self.position = self.target + offset * (radius / actual_dist);
418 } else {
419 self.position = new_pos;
420 }
421
422 let final_view = Mat4::look_at_rh(self.position, self.target, up_vec);
425 if final_view.is_finite() {
426 let fr = glam::Mat3::from_cols(
428 final_view.x_axis.truncate(),
429 final_view.y_axis.truncate(),
430 final_view.z_axis.truncate(),
431 );
432 self.up = fr.transpose() * Vec3::Y;
433 }
434 }
435
436 pub fn orbit_free(&mut self, delta_x: f32, delta_y: f32) {
441 let radius = (self.position - self.target).length();
442 let right_dir = self.right();
443 let up_dir = self.camera_up();
444
445 let yaw_rot = Mat4::from_axis_angle(up_dir, -delta_x);
448 let pitch_rot = Mat4::from_axis_angle(right_dir, -delta_y);
449
450 let to_center = Mat4::from_translation(self.target);
451 let from_center = Mat4::from_translation(-self.target);
452 let transform = to_center * pitch_rot * yaw_rot * from_center;
453
454 let new_pos = transform.transform_point3(self.position);
455
456 let offset = new_pos - self.target;
458 let actual_dist = offset.length();
459 if actual_dist > 1e-8 {
460 self.position = self.target + offset * (radius / actual_dist);
461 } else {
462 self.position = new_pos;
463 }
464
465 let rot = pitch_rot * yaw_rot;
467 self.up = rot.transform_vector3(self.up).normalize();
468 }
469
470 pub fn orbit_arcball(&mut self, start: [f32; 2], end: [f32; 2]) {
475 let to_sphere = |v: [f32; 2]| -> Vec3 {
476 let x = v[0].clamp(-1.0, 1.0);
477 let y = v[1].clamp(-1.0, 1.0);
478 let mag = x * x + y * y;
479 if mag <= 1.0 {
480 Vec3::new(x, y, -(1.0 - mag).sqrt())
481 } else {
482 Vec3::new(x, y, 0.0).normalize()
483 }
484 };
485
486 let sphere_start = to_sphere(start);
487 let sphere_end = to_sphere(end);
488
489 let rot_axis = -sphere_start.cross(sphere_end);
490 if rot_axis.length_squared() < 1e-12 {
491 return; }
493 let rot_angle = sphere_start.dot(sphere_end).clamp(-1.0, 1.0).acos();
494 if rot_angle.abs() < 1e-8 {
495 return;
496 }
497
498 let view = self.view_matrix();
500 let r = Mat3::from_cols(
501 view.x_axis.truncate(),
502 view.y_axis.truncate(),
503 view.z_axis.truncate(),
504 );
505 let r_inv = r.transpose();
506
507 let cam_rot = Mat3::from_axis_angle(rot_axis.normalize(), rot_angle);
509
510 let world_rot = r_inv * cam_rot * r;
512 let world_rot4 = Mat4::from_mat3(world_rot);
513
514 let to_center = Mat4::from_translation(self.target);
515 let from_center = Mat4::from_translation(-self.target);
516 let transform = to_center * world_rot4 * from_center;
517
518 let radius = (self.position - self.target).length();
519 let new_pos = transform.transform_point3(self.position);
520
521 let offset = new_pos - self.target;
523 let actual_dist = offset.length();
524 if actual_dist > 1e-8 {
525 self.position = self.target + offset * (radius / actual_dist);
526 } else {
527 self.position = new_pos;
528 }
529
530 self.up = (world_rot * self.up).normalize();
532 }
533
534 pub fn mouse_look(&mut self, delta_x: f32, delta_y: f32) {
540 let up_vec = self.up_direction.to_vec3();
541 let look_dir = self.forward();
542
543 let dot = look_dir.dot(up_vec);
545 let clamped_dy = if dot > 0.99 {
546 delta_y.min(0.0)
547 } else if dot < -0.99 {
548 delta_y.max(0.0)
549 } else {
550 delta_y
551 };
552
553 let yaw_rot = Quat::from_axis_angle(up_vec, -delta_x);
556 let right_dir = self.right();
558 let pitch_rot = Quat::from_axis_angle(right_dir, -clamped_dy);
559
560 let new_look = (pitch_rot * yaw_rot * look_dir).normalize();
562
563 let dist = (self.target - self.position).length();
565 self.target = self.position + new_look * dist;
566
567 let new_right = new_look.cross(up_vec).normalize();
569 self.up = new_right.cross(new_look).normalize();
570 if self.up.length_squared() < 0.5 {
571 self.up = up_vec;
572 }
573 }
574
575 pub fn move_first_person(&mut self, delta: Vec3) {
579 let fwd = self.forward();
580 let right = self.right();
581 let cam_up = self.camera_up();
582
583 let world_offset = right * delta.x + cam_up * delta.y + fwd * delta.z;
584 self.position += world_offset;
585 self.target += world_offset;
586 }
587
588 pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
590 self.orbit_turntable(delta_x, delta_y);
591 }
592
593 pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
596 let right = self.right();
597 let up_dir = self.camera_up();
598 let offset = right * delta_x + up_dir * delta_y;
599 self.position += offset;
600 self.target += offset;
601 }
602
603 pub fn zoom(&mut self, delta: f32) {
606 match self.projection_mode {
607 ProjectionMode::Perspective => {
608 let direction = self.forward();
609 let distance = (self.position - self.target).length();
610 let new_distance = (distance - delta).max(0.1);
611 self.position = self.target - direction * new_distance;
612 }
613 ProjectionMode::Orthographic => {
614 let zoom_factor = 1.0 - delta * 0.4;
618 self.ortho_scale = (self.ortho_scale * zoom_factor).clamp(0.01, 1000.0);
619 }
620 }
621 }
622
623 pub fn look_at_box(&mut self, min: Vec3, max: Vec3) {
625 let center = (min + max) * 0.5;
626 let size = (max - min).length();
627 let extents = max - min;
628
629 self.target = center;
630
631 let half_fov_v = self.fov * 0.5;
634 let half_fov_h = (half_fov_v.tan() * self.aspect_ratio).atan();
635 let half_fov = half_fov_v.min(half_fov_h); let radius = size * 0.5;
637 let distance = (radius / half_fov.tan()) * 1.1;
639 self.position = center + Vec3::new(0.0, 0.0, distance);
640
641 self.near = size * 0.001;
642 self.far = size * 100.0;
643
644 let half_height = extents.y.max(extents.x / self.aspect_ratio) * 0.6;
647 self.ortho_scale = half_height.max(0.1);
648 }
649
650 pub fn set_navigation_style(&mut self, style: NavigationStyle) {
652 self.navigation_style = style;
653 }
654
655 pub fn set_projection_mode(&mut self, mode: ProjectionMode) {
657 self.projection_mode = mode;
658 }
659
660 pub fn set_up_direction(&mut self, direction: AxisDirection) {
663 self.up_direction = direction;
664 self.up = direction.to_vec3();
665 self.front_direction = direction.default_front_direction();
666 }
667
668 pub fn set_move_speed(&mut self, speed: f32) {
670 self.move_speed = speed.max(0.01);
671 }
672
673 pub fn set_ortho_scale(&mut self, scale: f32) {
675 self.ortho_scale = scale.max(0.01);
676 }
677
678 pub fn set_fov(&mut self, fov: f32) {
680 self.fov = fov.clamp(0.1, std::f32::consts::PI - 0.1);
681 }
682
683 pub fn set_near(&mut self, near: f32) {
685 self.near = near.max(0.001);
686 }
687
688 pub fn set_far(&mut self, far: f32) {
690 self.far = far.max(self.near + 0.1);
691 }
692
693 fn decompose_view_matrix(view: &Mat4) -> (Quat, Vec3) {
699 let rot_mat = Mat3::from_cols(
700 view.x_axis.truncate(),
701 view.y_axis.truncate(),
702 view.z_axis.truncate(),
703 );
704 let rot = Quat::from_mat3(&rot_mat);
705 let t = Vec3::new(view.w_axis.x, view.w_axis.y, view.w_axis.z);
706 (rot, t)
707 }
708
709 fn camera_from_inverse_view(rot: &Quat, t: &Vec3, target_dist: f32) -> (Vec3, Vec3, Vec3) {
712 let rot_mat = Mat3::from_quat(*rot);
713 let position = *t;
715 let forward = -(rot_mat * Vec3::Z);
717 let up = rot_mat * Vec3::Y;
719 let target = position + forward * target_dist;
720 (position, target, up)
721 }
722
723 pub fn start_flight_to(&mut self, target_view: Mat4, target_fov: f32, duration_secs: f32) {
729 let current_view = self.view_matrix();
730 let current_dist = (self.position - self.target).length().max(0.01);
731
732 let (rot_start, t_start) = Self::decompose_view_matrix(¤t_view.inverse());
735 let (rot_end, t_end) = Self::decompose_view_matrix(&target_view.inverse());
736
737 self.flight = Some(CameraFlight {
738 start_time: Instant::now(),
739 duration_secs,
740 initial_rot: rot_start,
741 target_rot: rot_end,
742 initial_t: t_start,
743 target_t: t_end,
744 initial_fov: self.fov,
745 target_fov,
746 target_dist: current_dist,
747 });
748 }
749
750 pub fn update_flight(&mut self) {
755 let Some(flight) = &self.flight else {
756 return;
757 };
758
759 let elapsed = flight.start_time.elapsed().as_secs_f32();
760 let t = (elapsed / flight.duration_secs).min(1.0);
761 let dist = flight.target_dist;
762
763 if t >= 1.0 {
764 let (position, target, up) =
766 Self::camera_from_inverse_view(&flight.target_rot, &flight.target_t, dist);
767 self.position = position;
768 self.target = target;
769 self.up = up;
770 self.fov = flight.target_fov;
771 self.flight = None;
772 } else {
773 let t_smooth = t * t * (3.0 - 2.0 * t);
775
776 let target_rot = if flight.initial_rot.dot(flight.target_rot) < 0.0 {
779 -flight.target_rot
780 } else {
781 flight.target_rot
782 };
783 let interp_rot = flight.initial_rot.lerp(target_rot, t_smooth).normalize();
784
785 let interp_t = flight.initial_t.lerp(flight.target_t, t_smooth);
787
788 let fov = (1.0 - t) * flight.initial_fov + t * flight.target_fov;
790
791 let (position, target, up) =
792 Self::camera_from_inverse_view(&interp_rot, &interp_t, dist);
793 self.position = position;
794 self.target = target;
795 self.up = up;
796 self.fov = fov;
797 }
798 }
799
800 pub fn cancel_flight(&mut self) {
802 self.flight = None;
803 }
804
805 #[must_use]
807 pub fn is_in_flight(&self) -> bool {
808 self.flight.is_some()
809 }
810
811 #[must_use]
813 pub fn fov_degrees(&self) -> f32 {
814 self.fov.to_degrees()
815 }
816
817 pub fn set_fov_degrees(&mut self, degrees: f32) {
819 self.set_fov(degrees.to_radians());
820 }
821}
822
823impl Default for Camera {
824 fn default() -> Self {
825 Self::new(16.0 / 9.0)
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 #[test]
834 fn test_projection_mode_perspective() {
835 let camera = Camera::new(1.0);
836 let proj = camera.projection_matrix();
837 assert!(proj.w_axis.z != 0.0);
839 }
840
841 #[test]
842 fn test_projection_mode_orthographic() {
843 let mut camera = Camera::new(1.0);
844 camera.projection_mode = ProjectionMode::Orthographic;
845 camera.ortho_scale = 5.0;
846 let proj = camera.projection_matrix();
847 assert!((proj.w_axis.w - 1.0).abs() < 0.001);
849 }
850
851 #[test]
852 fn test_set_fov_clamping() {
853 let mut camera = Camera::new(1.0);
854 camera.set_fov(0.0); assert!(camera.fov >= 0.1);
856
857 camera.set_fov(std::f32::consts::PI); assert!(camera.fov < std::f32::consts::PI);
859 }
860
861 #[test]
862 fn test_zoom_perspective() {
863 let mut camera = Camera::new(1.0);
864 camera.projection_mode = ProjectionMode::Perspective;
865 camera.position = Vec3::new(0.0, 0.0, 5.0);
866 camera.target = Vec3::ZERO;
867
868 let initial_distance = camera.position.distance(camera.target);
869 camera.zoom(1.0); let new_distance = camera.position.distance(camera.target);
871
872 assert!(
873 new_distance < initial_distance,
874 "Perspective zoom in should decrease distance"
875 );
876 }
877
878 #[test]
879 fn test_zoom_orthographic() {
880 let mut camera = Camera::new(1.0);
881 camera.projection_mode = ProjectionMode::Orthographic;
882 camera.ortho_scale = 5.0;
883
884 let initial_scale = camera.ortho_scale;
885 camera.zoom(1.0); let new_scale = camera.ortho_scale;
887
888 assert!(
889 new_scale < initial_scale,
890 "Orthographic zoom in should decrease scale"
891 );
892 }
893}