1use crate::core::interaction::Modifiers;
7use crate::core::{ClipPolicy, DepthMode};
8use glam::{Mat4, Quat, Vec2, Vec3};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum ProjectionType {
13 Perspective {
14 fov: f32,
15 near: f32,
16 far: f32,
17 },
18 Orthographic {
19 left: f32,
20 right: f32,
21 bottom: f32,
22 top: f32,
23 near: f32,
24 far: f32,
25 },
26}
27
28impl Default for ProjectionType {
29 fn default() -> Self {
30 Self::Perspective {
31 fov: 45.0_f32.to_radians(),
32 near: 0.1,
33 far: 100.0,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct Camera {
41 pub position: Vec3,
43 pub target: Vec3,
44 pub up: Vec3,
45
46 pub projection: ProjectionType,
48 pub aspect_ratio: f32,
49 pub depth_mode: DepthMode,
51
52 pub zoom: f32,
54 pub rotation: Quat,
55
56 pub pan_sensitivity: f32,
58 pub zoom_sensitivity: f32,
59 pub rotate_sensitivity: f32,
60
61 view_matrix: Mat4,
63 projection_matrix: Mat4,
64 view_proj_dirty: bool,
65}
66
67impl Default for Camera {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Camera {
74 pub fn new() -> Self {
76 let mut camera = Self {
77 position: Vec3::new(3.5, 3.5, 3.5),
79 target: Vec3::ZERO,
80 up: Vec3::Z,
81 projection: ProjectionType::default(),
82 aspect_ratio: 16.0 / 9.0,
83 depth_mode: DepthMode::default(),
84 zoom: 1.0,
85 rotation: Quat::IDENTITY,
86 pan_sensitivity: 0.01,
87 zoom_sensitivity: 0.1,
88 rotate_sensitivity: 0.005,
89 view_matrix: Mat4::IDENTITY,
90 projection_matrix: Mat4::IDENTITY,
91 view_proj_dirty: true,
92 };
93 camera.update_matrices();
94 camera
95 }
96
97 pub fn new_2d(bounds: (f32, f32, f32, f32)) -> Self {
99 let (left, right, bottom, top) = bounds;
100 let center_x = (left + right) / 2.0;
101 let center_y = (bottom + top) / 2.0;
102 let mut camera = Self {
103 position: Vec3::new(center_x, center_y, 1.0),
107 target: Vec3::new(center_x, center_y, 0.0),
108 up: Vec3::Y,
110 projection: ProjectionType::Orthographic {
111 left,
112 right,
113 bottom,
114 top,
115 near: -1.0,
116 far: 1.0,
117 },
118 aspect_ratio: (right - left) / (top - bottom),
119 depth_mode: DepthMode::default(),
120 zoom: 1.0,
121 rotation: Quat::IDENTITY,
122 pan_sensitivity: 0.01,
123 zoom_sensitivity: 0.1,
124 rotate_sensitivity: 0.0, view_matrix: Mat4::IDENTITY,
126 projection_matrix: Mat4::IDENTITY,
127 view_proj_dirty: true,
128 };
129 camera.update_matrices();
130 camera
131 }
132
133 pub fn update_aspect_ratio(&mut self, aspect_ratio: f32) {
135 self.aspect_ratio = aspect_ratio;
136 self.view_proj_dirty = true;
137 }
138
139 pub fn view_proj_matrix(&mut self) -> Mat4 {
141 if self.view_proj_dirty {
142 self.update_matrices();
143 }
144 self.projection_matrix * self.view_matrix
145 }
146
147 pub fn mark_dirty(&mut self) {
149 self.view_proj_dirty = true;
150 }
151
152 pub fn view_matrix(&mut self) -> Mat4 {
154 if self.view_proj_dirty {
155 self.update_matrices();
156 }
157 self.view_matrix
158 }
159
160 pub fn projection_matrix(&mut self) -> Mat4 {
162 if self.view_proj_dirty {
163 self.update_matrices();
164 }
165 self.projection_matrix
166 }
167
168 pub fn pan(&mut self, delta: Vec2) {
170 let view = self.view_matrix();
172 let right = view.x_axis.truncate();
173 let up = view.y_axis.truncate();
174
175 let dist = (self.position - self.target).length().max(1e-3);
177 let delta = Vec2::new(-delta.x, delta.y);
180 let pan_amount = delta * self.pan_sensitivity * dist;
181 let world_delta = right * pan_amount.x + up * pan_amount.y;
182
183 self.position += world_delta;
184 self.target += world_delta;
185 self.view_proj_dirty = true;
186 }
187
188 pub fn zoom(&mut self, delta: f32) {
190 let mut factor = 1.0 - delta * self.zoom_sensitivity;
192 if factor.abs() < 1e-3 {
193 return;
194 }
195 factor = factor.clamp(0.2, 5.0);
196 self.zoom = (self.zoom * factor).clamp(0.01, 100.0);
197
198 match &mut self.projection {
199 ProjectionType::Perspective { .. } => {
200 let delta_vec = self.position - self.target;
202 let distance = delta_vec.length();
203 if !distance.is_finite() || distance < 1e-4 {
204 return;
206 }
207 let direction = delta_vec / distance;
208 let new_distance = (distance * factor).clamp(0.1, 1000.0);
209 self.position = self.target + direction * new_distance;
210 }
211 ProjectionType::Orthographic {
212 left,
213 right,
214 bottom,
215 top,
216 ..
217 } => {
218 let center_x = (*left + *right) / 2.0;
220 let center_y = (*bottom + *top) / 2.0;
221 let width = (*right - *left) * factor;
222 let height = (*top - *bottom) * factor;
223
224 *left = center_x - width / 2.0;
225 *right = center_x + width / 2.0;
226 *bottom = center_y - height / 2.0;
227 *top = center_y + height / 2.0;
228 }
229 }
230
231 self.view_proj_dirty = true;
232 }
233
234 pub fn rotate(&mut self, delta: Vec2) {
236 if self.rotate_sensitivity == 0.0 {
237 return; }
239
240 let yaw = -delta.x * self.rotate_sensitivity;
247 let pitch = -delta.y * self.rotate_sensitivity;
248
249 let world_up = Vec3::Z;
252 let mut offset = self.position - self.target;
253 if offset.length_squared() < 1e-9 {
254 offset = Vec3::new(0.0, 0.0, 1.0);
255 }
256
257 let yaw_rot = Quat::from_axis_angle(world_up, yaw);
259 offset = yaw_rot * offset;
260
261 let forward = (-offset).normalize_or_zero();
263 let right = forward.cross(world_up).normalize_or_zero();
264 if right.length_squared() > 1e-9 {
265 let pitch_rot = Quat::from_axis_angle(right, pitch);
266 let candidate = pitch_rot * offset;
267 let up_dot = candidate.normalize_or_zero().dot(world_up).abs();
269 if up_dot < 0.995 {
270 offset = candidate;
271 }
272 }
273
274 self.position = self.target + offset;
275
276 self.view_proj_dirty = true;
277 }
278
279 pub fn look_at(&mut self, target: Vec3, distance: Option<f32>) {
281 self.target = target;
282
283 if let Some(dist) = distance {
284 let direction = (self.position - self.target).normalize();
285 self.position = self.target + direction * dist;
286 }
287
288 self.view_proj_dirty = true;
289 }
290
291 pub fn set_view_angles_deg(&mut self, azimuth_deg: f32, elevation_deg: f32) {
292 let distance = (self.position - self.target).length().max(0.1);
293 let az = azimuth_deg.to_radians();
294 let el = elevation_deg.to_radians();
295 let dir = Vec3::new(el.cos() * az.cos(), el.cos() * az.sin(), el.sin());
296 self.up = Vec3::Z;
297 self.position = self.target + dir * distance;
298 self.view_proj_dirty = true;
299 }
300
301 pub fn reset(&mut self) {
303 match self.projection {
304 ProjectionType::Perspective { .. } => {
305 self.position = Vec3::new(3.5, 3.5, 3.5);
306 self.target = Vec3::ZERO;
307 self.rotation = Quat::IDENTITY;
308 self.up = Vec3::Z;
309 }
310 ProjectionType::Orthographic { .. } => {
311 self.zoom = 1.0;
312 self.target = Vec3::ZERO;
313 }
314 }
315 self.view_proj_dirty = true;
316 }
317
318 pub fn fit_bounds(&mut self, min_bounds: Vec3, max_bounds: Vec3) {
320 let center = (min_bounds + max_bounds) / 2.0;
321 let size = max_bounds - min_bounds;
322
323 match &mut self.projection {
324 ProjectionType::Perspective { near, far, .. } => {
325 let max_size = size.x.max(size.y).max(size.z);
326 let distance = max_size * 2.0; self.target = center;
329 let direction = (self.position - self.target).normalize();
330 self.position = self.target + direction * distance;
331
332 let radius = (size.length() * 0.5).max(1e-3);
336 let dist = (self.position - self.target).length().max(1e-3);
337 let desired_near = (dist - radius * 4.0).max(0.01);
338 let desired_far = (dist + radius * 4.0).max(desired_near + 1.0);
339 *near = desired_near;
340 *far = desired_far;
341 }
342 ProjectionType::Orthographic {
343 left,
344 right,
345 bottom,
346 top,
347 ..
348 } => {
349 let margin = 0.1; let width = size.x * (1.0 + margin);
351 let height = size.y * (1.0 + margin);
352
353 let display_width = width.max(height * self.aspect_ratio);
355 let display_height = height.max(width / self.aspect_ratio);
356
357 *left = center.x - display_width / 2.0;
358 *right = center.x + display_width / 2.0;
359 *bottom = center.y - display_height / 2.0;
360 *top = center.y + display_height / 2.0;
361
362 self.target = center;
363 }
364 }
365
366 self.view_proj_dirty = true;
367 }
368
369 pub fn set_clip_planes(&mut self, near: f32, far: f32) {
370 match &mut self.projection {
371 ProjectionType::Perspective {
372 near: n, far: f, ..
373 } => {
374 *n = near.max(1e-4);
375 *f = far.max(*n + 1e-3);
376 self.view_proj_dirty = true;
377 }
378 ProjectionType::Orthographic {
379 near: n, far: f, ..
380 } => {
381 *n = near;
382 *f = far;
383 self.view_proj_dirty = true;
384 }
385 }
386 }
387
388 pub fn update_clip_planes_from_world_aabb(
393 &mut self,
394 world_min: Vec3,
395 world_max: Vec3,
396 policy: &ClipPolicy,
397 ) {
398 if !policy.dynamic {
399 return;
400 }
401 let ProjectionType::Perspective { .. } = self.projection else {
402 return;
403 };
404
405 let view = Mat4::look_at_rh(self.position, self.target, self.up);
407 let corners = [
408 Vec3::new(world_min.x, world_min.y, world_min.z),
409 Vec3::new(world_max.x, world_min.y, world_min.z),
410 Vec3::new(world_min.x, world_max.y, world_min.z),
411 Vec3::new(world_max.x, world_max.y, world_min.z),
412 Vec3::new(world_min.x, world_min.y, world_max.z),
413 Vec3::new(world_max.x, world_min.y, world_max.z),
414 Vec3::new(world_min.x, world_max.y, world_max.z),
415 Vec3::new(world_max.x, world_max.y, world_max.z),
416 ];
417
418 let mut min_depth = f32::INFINITY;
419 let mut max_depth = f32::NEG_INFINITY;
420 for c in corners {
421 let v = (view * c.extend(1.0)).truncate();
422 if !(v.x.is_finite() && v.y.is_finite() && v.z.is_finite()) {
423 continue;
424 }
425 let depth = (-v.z).max(0.0);
427 if depth > 0.0 {
428 min_depth = min_depth.min(depth);
429 max_depth = max_depth.max(depth);
430 }
431 }
432 if !min_depth.is_finite() || !max_depth.is_finite() || max_depth <= 0.0 {
433 return;
434 }
435
436 let mut near = (min_depth * policy.near_padding).max(policy.min_near);
437 let mut far = (max_depth * policy.far_padding).max(near + 1.0);
438 if far > policy.max_far {
439 far = policy.max_far.max(near + 1.0);
440 }
441 if (far / near).is_finite() && far / near > 1.0e6 {
443 near = (far / 1.0e6).max(policy.min_near);
444 }
445 self.set_clip_planes(near, far);
446 }
447
448 pub fn screen_to_world(&mut self, screen_pos: Vec2, screen_size: Vec2, depth: f32) -> Vec3 {
450 if self.view_proj_dirty {
451 self.update_matrices();
452 }
453 let ndc_x = (2.0 * screen_pos.x) / screen_size.x - 1.0;
455 let ndc_y = 1.0 - (2.0 * screen_pos.y) / screen_size.y;
456 let ndc = Vec3::new(ndc_x, ndc_y, depth * 2.0 - 1.0);
457
458 let view_proj_inv = (self.projection_matrix * self.view_matrix).inverse();
460 let world_pos = view_proj_inv * ndc.extend(1.0);
461
462 if world_pos.w != 0.0 {
463 world_pos.truncate() / world_pos.w
464 } else {
465 world_pos.truncate()
466 }
467 }
468
469 fn update_matrices(&mut self) {
471 self.view_matrix = Mat4::look_at_rh(self.position, self.target, self.up);
473
474 self.projection_matrix = match self.projection {
476 ProjectionType::Perspective { fov, near, far } => {
477 match self.depth_mode {
478 DepthMode::Standard => Mat4::perspective_rh(fov, self.aspect_ratio, near, far),
479 DepthMode::ReversedZ => {
480 let f = 1.0 / (0.5 * fov).tan();
484 let a = self.aspect_ratio.max(1e-6);
485 let nf = (far - near).max(1e-6);
486 let m00 = f / a;
487 let m11 = f;
488 let m22 = near / nf;
490 let m32 = (near * far) / nf;
491 Mat4::from_cols_array(&[
492 m00, 0.0, 0.0, 0.0, 0.0, m11, 0.0, 0.0, 0.0, 0.0, m22, -1.0, 0.0, 0.0, m32, 0.0, ])
497 }
498 }
499 }
500 ProjectionType::Orthographic {
501 left,
502 right,
503 bottom,
504 top,
505 near,
506 far,
507 } => {
508 log::trace!(
509 target: "runmat_plot",
510 "ortho matrix bounds l={} r={} b={} t={} n={} f={}",
511 left, right, bottom, top, near, far
512 );
513 log::trace!(target: "runmat_plot", "camera aspect_ratio={}", self.aspect_ratio);
514 Mat4::orthographic_rh(left, right, bottom, top, near, far)
515 }
516 };
517
518 self.view_proj_dirty = false;
519 }
520}
521
522#[derive(Debug, Default)]
524pub struct CameraController {
525 pub active_button: Option<MouseButton>,
526 pub last_mouse_pos: Vec2,
527 pub mouse_delta: Vec2,
528}
529
530impl CameraController {
531 pub fn new() -> Self {
532 Self::default()
533 }
534
535 pub fn mouse_press(&mut self, position: Vec2, button: MouseButton, _modifiers: Modifiers) {
537 self.last_mouse_pos = position;
538 self.active_button = Some(button);
539 }
540
541 pub fn mouse_release(&mut self, _position: Vec2, button: MouseButton, _modifiers: Modifiers) {
543 if self.active_button == Some(button) {
544 self.active_button = None;
545 }
546 }
547
548 pub fn mouse_move(
558 &mut self,
559 position: Vec2,
560 delta: Vec2,
561 viewport_px: (u32, u32),
562 modifiers: Modifiers,
563 camera: &mut Camera,
564 ) {
565 let Some(button) = self.active_button else {
566 self.last_mouse_pos = position;
567 return;
568 };
569 self.mouse_delta = if delta.length_squared() > 0.0 {
571 delta
572 } else {
573 position - self.last_mouse_pos
574 };
575
576 match camera.projection {
577 ProjectionType::Perspective { .. } => {
578 let fine = if modifiers.ctrl || modifiers.meta {
585 0.35
586 } else {
587 1.0
588 };
589 let d = self.mouse_delta * fine;
590
591 if modifiers.alt {
592 match button {
593 MouseButton::Left => camera.rotate(d),
594 MouseButton::Middle => camera.pan(d),
595 MouseButton::Right => {
596 let zoom_delta = (-d.y / 120.0).clamp(-5.0, 5.0);
598 self.mouse_wheel(
599 Vec2::new(0.0, zoom_delta),
600 position,
601 viewport_px,
602 modifiers,
603 camera,
604 );
605 }
606 }
607 } else {
608 let want_pan = modifiers.shift;
609 match button {
610 MouseButton::Middle | MouseButton::Right => {
611 if want_pan {
612 camera.pan(d);
613 } else if modifiers.ctrl || modifiers.meta {
614 let zoom_delta = (-d.y / 120.0).clamp(-5.0, 5.0);
616 self.mouse_wheel(
617 Vec2::new(0.0, zoom_delta),
618 position,
619 viewport_px,
620 modifiers,
621 camera,
622 );
623 } else {
624 camera.rotate(d);
625 }
626 }
627 MouseButton::Left => {
628 if want_pan {
629 camera.pan(d);
630 } else if modifiers.ctrl || modifiers.meta {
631 let zoom_delta = (-d.y / 120.0).clamp(-5.0, 5.0);
632 self.mouse_wheel(
633 Vec2::new(0.0, zoom_delta),
634 position,
635 viewport_px,
636 modifiers,
637 camera,
638 );
639 } else {
640 camera.rotate(d);
641 }
642 }
643 }
644 }
645 }
646 ProjectionType::Orthographic {
647 ref mut left,
648 ref mut right,
649 ref mut bottom,
650 ref mut top,
651 ..
652 } => {
653 let _ = (button, modifiers);
655 {
656 let (vw, vh) = (viewport_px.0.max(1) as f32, viewport_px.1.max(1) as f32);
657 let width = (*right - *left).abs().max(1e-6);
658 let height = (*top - *bottom).abs().max(1e-6);
659
660 let dx = -self.mouse_delta.x * (width / vw);
664 let dy = self.mouse_delta.y * (height / vh);
667
668 *left += dx;
669 *right += dx;
670 *bottom += dy;
671 *top += dy;
672 camera.mark_dirty();
673 }
674 }
675 }
676
677 self.last_mouse_pos = position;
678 }
679
680 pub fn mouse_wheel(
682 &mut self,
683 delta: Vec2,
684 position_px: Vec2,
685 viewport_px: (u32, u32),
686 modifiers: Modifiers,
687 camera: &mut Camera,
688 ) {
689 let delta_y = delta.y;
696
697 match &mut camera.projection {
698 ProjectionType::Perspective { .. } => {
699 if modifiers.shift {
700 let pan_px = Vec2::new(delta.x, -delta.y);
704 camera.pan(pan_px * 6.0);
705 return;
706 }
707
708 let sens = camera.zoom_sensitivity;
709 let mut factor = 1.0 - delta_y * sens;
710 if factor.abs() < 1e-3 {
711 return;
712 }
713 factor = factor.clamp(0.2, 5.0);
714
715 let (vw, vh) = (viewport_px.0.max(1) as f32, viewport_px.1.max(1) as f32);
716 let screen_size = Vec2::new(vw, vh);
717 let pos = Vec2::new(position_px.x.clamp(0.0, vw), position_px.y.clamp(0.0, vh));
718
719 let p_near = camera.screen_to_world(pos, screen_size, 0.0);
721 let p_far = camera.screen_to_world(pos, screen_size, 1.0);
722 let dir = (p_far - p_near).normalize_or_zero();
723 if dir.length_squared() < 1e-9 {
724 return;
725 }
726
727 let origin = camera.position;
730 let mut pivot = None;
731 if dir.z.abs() > 1e-6 {
732 let t = (-origin.z) / dir.z;
733 if t.is_finite() && t > 0.0 {
734 pivot = Some(origin + dir * t);
735 }
736 }
737 if pivot.is_none() {
738 let forward = (camera.target - camera.position).normalize_or_zero();
739 let denom = dir.dot(forward);
740 if denom.abs() > 1e-6 {
741 let t = (camera.target - origin).dot(forward) / denom;
742 if t.is_finite() && t > 0.0 {
743 pivot = Some(origin + dir * t);
744 }
745 }
746 }
747 let pivot = pivot.unwrap_or(camera.target);
748
749 let s = (pivot - origin).length().max(1e-3);
750 let new_s = (s * factor).clamp(0.05, 1.0e9);
751 let delta_dist = s - new_s;
752 let translate = dir * delta_dist;
753
754 camera.position += translate;
757 camera.target += translate;
758 camera.view_proj_dirty = true;
759 }
760 ProjectionType::Orthographic {
761 left,
762 right,
763 bottom,
764 top,
765 ..
766 } => {
767 if modifiers.shift {
768 let vw = viewport_px.0.max(1) as f32;
770 let vh = viewport_px.1.max(1) as f32;
771 let w = (*right - *left).max(1e-6);
772 let h = (*top - *bottom).max(1e-6);
773 let dx = -delta.x * (w / vw);
774 let dy = delta.y * (h / vh);
775 *left += dx;
776 *right += dx;
777 *bottom += dy;
778 *top += dy;
779 camera.mark_dirty();
780 return;
781 }
782
783 let sens = camera.zoom_sensitivity;
784 let mut factor = 1.0 - delta_y * sens;
785 if factor.abs() < 1e-3 {
786 return;
787 }
788 factor = factor.clamp(0.2, 5.0);
789
790 let w = (*right - *left).max(1e-6);
792 let h = (*top - *bottom).max(1e-6);
793 let vw = viewport_px.0.max(1) as f32;
794 let vh = viewport_px.1.max(1) as f32;
795 let tx = (position_px.x / vw).clamp(0.0, 1.0);
796 let ty = (position_px.y / vh).clamp(0.0, 1.0);
797 let pivot_x = *left + tx * w;
798 let pivot_y = *top - ty * h;
799 let new_left = pivot_x - (pivot_x - *left) * factor;
800 let new_right = pivot_x + (*right - pivot_x) * factor;
801 let new_bottom = pivot_y - (pivot_y - *bottom) * factor;
802 let new_top = pivot_y + (*top - pivot_y) * factor;
803 *left = new_left;
804 *right = new_right;
805 *bottom = new_bottom;
806 *top = new_top;
807 camera.mark_dirty();
808 }
809 }
810 }
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815pub enum MouseButton {
816 Left,
817 Right,
818 Middle,
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 #[test]
826 fn test_camera_creation() {
827 let camera = Camera::new();
828 assert_eq!(camera.position, Vec3::new(3.5, 3.5, 3.5));
829 assert_eq!(camera.target, Vec3::ZERO);
830 assert_eq!(camera.up, Vec3::Z);
831 }
832
833 #[test]
834 fn test_2d_camera() {
835 let camera = Camera::new_2d((-10.0, 10.0, -10.0, 10.0));
836 assert_eq!(camera.position, Vec3::new(0.0, 0.0, 1.0));
837 assert_eq!(camera.target, Vec3::new(0.0, 0.0, 0.0));
838 match camera.projection {
839 ProjectionType::Orthographic {
840 left,
841 right,
842 bottom,
843 top,
844 ..
845 } => {
846 assert_eq!(left, -10.0);
847 assert_eq!(right, 10.0);
848 assert_eq!(bottom, -10.0);
849 assert_eq!(top, 10.0);
850 }
851 _ => panic!("Expected orthographic projection"),
852 }
853 }
854
855 #[test]
856 fn test_camera_bounds_fitting() {
857 let mut camera = Camera::new_2d((-1.0, 1.0, -1.0, 1.0));
858 let min_bounds = Vec3::new(-5.0, -3.0, 0.0);
859 let max_bounds = Vec3::new(5.0, 3.0, 0.0);
860
861 camera.fit_bounds(min_bounds, max_bounds);
862
863 match camera.projection {
865 ProjectionType::Orthographic {
866 left,
867 right,
868 bottom,
869 top,
870 ..
871 } => {
872 assert!(left <= -5.0);
873 assert!(right >= 5.0);
874 assert!(bottom <= -3.0);
875 assert!(top >= 3.0);
876 }
877 _ => panic!("Expected orthographic projection"),
878 }
879 }
880
881 #[test]
882 fn test_2d_camera_tracks_non_origin_bounds_center() {
883 let camera = Camera::new_2d((10.0, 30.0, -2.0, 2.0));
884 assert_eq!(camera.position, Vec3::new(20.0, 0.0, 1.0));
885 assert_eq!(camera.target, Vec3::new(20.0, 0.0, 0.0));
886 }
887
888 #[test]
889 fn test_set_view_angles_preserves_distance() {
890 let mut camera = Camera::new();
891 camera.target = Vec3::new(1.0, 2.0, 3.0);
892 camera.position = camera.target + Vec3::new(2.0, 0.0, 0.0);
893 camera.set_view_angles_deg(90.0, 0.0);
894 let offset = camera.position - camera.target;
895 assert!((offset.length() - 2.0).abs() < 1e-5);
896 assert!(offset.x.abs() < 1e-4);
897 assert!((offset.y - 2.0).abs() < 1e-4);
898 }
899}