Skip to main content

dotzuki_engine/
camera.rs

1//! Camera system — viewport positioning with smooth follow, zoom, and boundary clamping.
2//!
3//! A general-purpose camera that supports arbitrary world sizes, fractional pixel
4//! positions, smooth interpolation, and zoom. It supersedes the older fixed
5//! Game Boy-style scroll/viewport approach.
6//!
7//! ## Usage
8//!
9//! ```ignore
10//! use dotzuki_engine::camera::{Camera, Vec2, Rect};
11//!
12//! let mut cam = Camera::new(160.0, 144.0);
13//! cam.clamp_to_bounds(Rect::new(0.0, 0.0, 2048.0, 2048.0));
14//! cam.follow_target(Vec2::new(player_x, player_y));
15//!
16//! // Every frame:
17//! cam.update(dt);
18//! let screen_pos = cam.world_to_screen(Vec2::new(npc_x, npc_y));
19//! ```
20
21/// Two-dimensional vector (floating-point).
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Vec2 {
24    pub x: f32,
25    pub y: f32,
26}
27
28impl Vec2 {
29    pub const fn new(x: f32, y: f32) -> Self {
30        Self { x, y }
31    }
32}
33
34/// Axis-aligned rectangle with floating-point coordinates.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct Rect {
37    pub x: f32,
38    pub y: f32,
39    pub w: f32,
40    pub h: f32,
41}
42
43impl Rect {
44    pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
45        Self { x, y, w, h }
46    }
47}
48
49/// Tile size in pixels used for coordinate calculations.
50const TILE_SIZE: f32 = 8.0;
51
52/// Default smooth-follow speed multiplier. Higher values produce snappier movement.
53const DEFAULT_FOLLOW_SPEED: f32 = 12.0;
54
55/// Camera controlling which portion of the world is visible on screen.
56///
57/// The camera's `position` represents the **top-left corner** of the viewport in
58/// world-space pixels. All coordinate conversions account for `zoom`.
59#[derive(Debug, Clone, PartialEq)]
60pub struct Camera {
61    /// Current camera position in world pixels (top-left corner of the viewport).
62    pub position: Vec2,
63    /// Desired position for smooth follow (world pixels). `None` means no follow target.
64    pub target: Option<Vec2>,
65    /// World boundaries that clamp the camera. `None` means unbounded.
66    pub bounds: Option<Rect>,
67    /// Smooth-follow factor: 0.0 = instant snap, 1.0 = no movement at all.
68    pub smooth_factor: f32,
69    /// Zoom level: 1.0 = normal, > 1.0 = zoomed in, < 1.0 = zoomed out.
70    pub zoom: f32,
71    /// Size of the viewport / screen in pixels.
72    pub viewport_size: Vec2,
73}
74
75impl Camera {
76    // ---------------------------------------------------------------------------
77    // Construction
78    // ---------------------------------------------------------------------------
79
80    /// Create a new camera with default settings.
81    ///
82    /// Position starts at (0, 0), zoom is 1.0, and smooth follow is disabled
83    /// (smooth_factor = 0.0 — instant snap).
84    pub fn new(viewport_width: f32, viewport_height: f32) -> Self {
85        Self {
86            position: Vec2::new(0.0, 0.0),
87            target: None,
88            bounds: None,
89            smooth_factor: 0.0,
90            zoom: 1.0,
91            viewport_size: Vec2::new(viewport_width, viewport_height),
92        }
93    }
94
95    // ---------------------------------------------------------------------------
96    // Targeting
97    // ---------------------------------------------------------------------------
98
99    /// Set (or replace) the target world position for smooth follow.
100    ///
101    /// Call [`update`](Self::update) every frame to interpolate `position` toward
102    /// this target.
103    pub fn follow_target(&mut self, target: Vec2) {
104        self.target = Some(target);
105    }
106
107    /// Stop following any target. The camera stays at its current position.
108    pub fn stop_follow(&mut self) {
109        self.target = None;
110    }
111
112    // ---------------------------------------------------------------------------
113    // Per-frame update
114    // ---------------------------------------------------------------------------
115
116    /// Advance the camera by `dt` seconds.
117    ///
118    /// If a [`target`](Self::target) is set, `position` is interpolated toward it
119    /// using exponential decay that is frame-rate-independent. After interpolation,
120    /// `position` is clamped to [`bounds`](Self::bounds) (if set).
121    ///
122    /// When `smooth_factor` is 0.0 the position snaps to the target instantly.
123    ///
124    /// # Panics
125    ///
126    /// Panics if `dt` is negative.
127    pub fn update(&mut self, dt: f32) {
128        assert!(dt >= 0.0, "dt must be non-negative, got {dt}");
129
130        if let Some(ref target) = self.target {
131            if self.smooth_factor <= 0.0 {
132                // Instant snap
133                self.position = *target;
134            } else {
135                let follow_strength = (1.0 - self.smooth_factor) * DEFAULT_FOLLOW_SPEED;
136                // f32::exp is not available on no_std targets; libm's expf is
137                // used there (bit-identical inputs, same decay curve).
138                #[cfg(target_os = "none")]
139                let decay = libm::expf(-follow_strength * dt);
140                #[cfg(not(target_os = "none"))]
141                let decay = (-follow_strength * dt).exp();
142                let t = 1.0 - decay;
143                self.position.x += (target.x - self.position.x) * t;
144                self.position.y += (target.y - self.position.y) * t;
145            }
146        }
147
148        self.apply_bounds();
149    }
150
151    // ---------------------------------------------------------------------------
152    // Coordinate conversion
153    // ---------------------------------------------------------------------------
154
155    /// Convert a world-space position to a screen-space position, accounting for
156    /// the camera's current position and zoom.
157    ///
158    /// ```
159    /// # use dotzuki_engine::camera::{Camera, Vec2};
160    /// let cam = Camera::new(160.0, 144.0);
161    /// let screen = cam.world_to_screen(Vec2::new(80.0, 72.0));
162    /// assert_eq!(screen.x, 80.0);
163    /// assert_eq!(screen.y, 72.0);
164    /// ```
165    pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
166        Vec2::new(
167            (world_pos.x - self.position.x) * self.zoom,
168            (world_pos.y - self.position.y) * self.zoom,
169        )
170    }
171
172    /// Convert a screen-space position to a world-space position.
173    ///
174    /// This is the inverse of [`world_to_screen`](Self::world_to_screen).
175    ///
176    /// ```
177    /// # use dotzuki_engine::camera::{Camera, Vec2};
178    /// let cam = Camera::new(160.0, 144.0);
179    /// let world = cam.screen_to_world(Vec2::new(160.0, 144.0));
180    /// assert_eq!(world.x, 160.0);
181    /// assert_eq!(world.y, 144.0);
182    /// ```
183    pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
184        Vec2::new(
185            screen_pos.x / self.zoom + self.position.x,
186            screen_pos.y / self.zoom + self.position.y,
187        )
188    }
189
190    // ---------------------------------------------------------------------------
191    // Bounds
192    // ---------------------------------------------------------------------------
193
194    /// Set the world boundary rectangle and immediately clamp the camera to it.
195    ///
196    /// The camera will never show content outside these bounds. The visible area
197    /// (viewport_size / zoom) must fit within the bounds; if the viewport is larger
198    /// than the bounds, the camera is centered on the bounds area.
199    pub fn clamp_to_bounds(&mut self, bounds: Rect) {
200        self.bounds = Some(bounds);
201        self.apply_bounds();
202    }
203
204    /// Remove world boundaries. The camera can scroll anywhere.
205    pub fn clear_bounds(&mut self) {
206        self.bounds = None;
207    }
208
209    /// Clamp `position` so the viewport stays within [`bounds`](Self::bounds).
210    fn apply_bounds(&mut self) {
211        let Some(ref bounds) = self.bounds else {
212            return;
213        };
214
215        let visible_w = self.viewport_size.x / self.zoom;
216        let visible_h = self.viewport_size.y / self.zoom;
217
218        // If the viewport is larger than the bounds, centre on the bounds area.
219        if visible_w >= bounds.w {
220            self.position.x = bounds.x + (bounds.w - visible_w) * 0.5;
221        } else {
222            self.position.x = self
223                .position
224                .x
225                .clamp(bounds.x, bounds.x + bounds.w - visible_w);
226        }
227
228        if visible_h >= bounds.h {
229            self.position.y = bounds.y + (bounds.h - visible_h) * 0.5;
230        } else {
231            self.position.y = self
232                .position
233                .y
234                .clamp(bounds.y, bounds.y + bounds.h - visible_h);
235        }
236    }
237
238    // ---------------------------------------------------------------------------
239    // Zoom
240    // ---------------------------------------------------------------------------
241
242    /// Set the zoom level, clamped to the valid range `[0.1, 10.0]`.
243    pub fn set_zoom(&mut self, zoom: f32) {
244        self.zoom = zoom.clamp(0.1, 10.0);
245        // Re-clamp after zoom change because the visible area changed.
246        self.apply_bounds();
247    }
248
249    // ---------------------------------------------------------------------------
250    // Tile helpers
251    // ---------------------------------------------------------------------------
252
253    /// Which tile column is at the camera's left edge.
254    ///
255    /// Divides the camera's world x-position by `TILE_SIZE` (8 px) and floors.
256    pub fn tile_x(&self) -> i32 {
257        // f32::floor is std-only; libm's floorf on no_std targets.
258        #[cfg(target_os = "none")]
259        {
260            libm::floorf(self.position.x / TILE_SIZE) as i32
261        }
262        #[cfg(not(target_os = "none"))]
263        {
264            (self.position.x / TILE_SIZE).floor() as i32
265        }
266    }
267
268    /// Which tile row is at the camera's top edge.
269    ///
270    /// Divides the camera's world y-position by `TILE_SIZE` (8 px) and floors.
271    pub fn tile_y(&self) -> i32 {
272        #[cfg(target_os = "none")]
273        {
274            libm::floorf(self.position.y / TILE_SIZE) as i32
275        }
276        #[cfg(not(target_os = "none"))]
277        {
278            (self.position.y / TILE_SIZE).floor() as i32
279        }
280    }
281
282    // ---------------------------------------------------------------------------
283    // Visibility checks
284    // ---------------------------------------------------------------------------
285
286    /// Returns `true` if a world-space point is visible on screen (within the
287    /// viewport), accounting for the camera position and zoom.
288    pub fn is_visible(&self, world_pos: Vec2) -> bool {
289        let screen = self.world_to_screen(world_pos);
290        screen.x >= -TILE_SIZE
291            && screen.y >= -TILE_SIZE
292            && screen.x < self.viewport_size.x + TILE_SIZE
293            && screen.y < self.viewport_size.y + TILE_SIZE
294    }
295
296    /// Returns `true` if any part of a world-space axis-aligned rectangle is
297    /// visible on screen.
298    pub fn is_rect_visible(&self, rect: Rect) -> bool {
299        let visible_w = self.viewport_size.x / self.zoom;
300        let visible_h = self.viewport_size.y / self.zoom;
301
302        // Expand viewport by one tile in each direction for culling margin.
303        let margin = TILE_SIZE / self.zoom;
304
305        !(rect.x + rect.w < self.position.x - margin
306            || rect.x > self.position.x + visible_w + margin
307            || rect.y + rect.h < self.position.y - margin
308            || rect.y > self.position.y + visible_h + margin)
309    }
310}
311
312// =============================================================================
313// Unit tests
314// =============================================================================
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    // -----------------------------------------------------------------------
321    // Construction
322    // -----------------------------------------------------------------------
323
324    #[test]
325    fn camera_starts_at_origin() {
326        let cam = Camera::new(160.0, 144.0);
327        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
328        assert_eq!(cam.zoom, 1.0);
329        assert_eq!(cam.smooth_factor, 0.0);
330        assert!(cam.target.is_none());
331        assert!(cam.bounds.is_none());
332    }
333
334    #[test]
335    fn new_is_gameboy_screen() {
336        let cam = Camera::new(160.0, 144.0);
337        assert_eq!(cam.viewport_size, Vec2::new(160.0, 144.0));
338    }
339
340    #[test]
341    fn custom_viewport_size() {
342        let cam = Camera::new(640.0, 480.0);
343        assert_eq!(cam.viewport_size, Vec2::new(640.0, 480.0));
344    }
345
346    // -----------------------------------------------------------------------
347    // Follow target
348    // -----------------------------------------------------------------------
349
350    #[test]
351    fn follow_target_sets_target() {
352        let mut cam = Camera::new(160.0, 144.0);
353        cam.follow_target(Vec2::new(100.0, 50.0));
354        assert_eq!(cam.target, Some(Vec2::new(100.0, 50.0)));
355    }
356
357    #[test]
358    fn follow_target_overwrites_previous() {
359        let mut cam = Camera::new(160.0, 144.0);
360        cam.follow_target(Vec2::new(10.0, 20.0));
361        cam.follow_target(Vec2::new(30.0, 40.0));
362        assert_eq!(cam.target, Some(Vec2::new(30.0, 40.0)));
363    }
364
365    #[test]
366    fn stop_follow_clears_target() {
367        let mut cam = Camera::new(160.0, 144.0);
368        cam.follow_target(Vec2::new(100.0, 50.0));
369        cam.stop_follow();
370        assert!(cam.target.is_none());
371    }
372
373    #[test]
374    fn update_snaps_when_smooth_factor_zero() {
375        let mut cam = Camera::new(160.0, 144.0);
376        cam.smooth_factor = 0.0;
377        cam.follow_target(Vec2::new(100.0, 50.0));
378        cam.update(0.016);
379        assert_eq!(cam.position, Vec2::new(100.0, 50.0));
380    }
381
382    #[test]
383    fn update_moves_toward_target_with_smooth() {
384        let mut cam = Camera::new(160.0, 144.0);
385        cam.smooth_factor = 0.5;
386        cam.follow_target(Vec2::new(100.0, 0.0));
387        let start = cam.position;
388        cam.update(0.016);
389        assert!(cam.position.x > start.x);
390        assert!(cam.position.x < 100.0);
391    }
392
393    #[test]
394    fn update_no_movement_when_smooth_factor_one() {
395        let mut cam = Camera::new(160.0, 144.0);
396        cam.smooth_factor = 1.0;
397        cam.position = Vec2::new(50.0, 50.0);
398        cam.follow_target(Vec2::new(200.0, 200.0));
399        cam.update(0.016);
400        assert_eq!(cam.position.x, 50.0);
401        assert_eq!(cam.position.y, 50.0);
402    }
403
404    #[test]
405    fn update_no_target_does_nothing() {
406        let mut cam = Camera::new(160.0, 144.0);
407        cam.position = Vec2::new(30.0, 40.0);
408        cam.update(0.016);
409        assert_eq!(cam.position, Vec2::new(30.0, 40.0));
410    }
411
412    #[test]
413    fn update_converges_after_many_frames() {
414        let mut cam = Camera::new(160.0, 144.0);
415        cam.smooth_factor = 0.2;
416        cam.follow_target(Vec2::new(200.0, 0.0));
417        for _ in 0..200 {
418            cam.update(0.016);
419        }
420        assert!((cam.position.x - 200.0).abs() < 1.0);
421    }
422
423    // -----------------------------------------------------------------------
424    // Coordinate conversion
425    // -----------------------------------------------------------------------
426
427    #[test]
428    fn world_to_screen_at_origin() {
429        let cam = Camera::new(160.0, 144.0);
430        let screen = cam.world_to_screen(Vec2::new(50.0, 30.0));
431        assert_eq!(screen, Vec2::new(50.0, 30.0));
432    }
433
434    #[test]
435    fn world_to_screen_offset() {
436        let mut cam = Camera::new(160.0, 144.0);
437        cam.position = Vec2::new(100.0, 50.0);
438        let screen = cam.world_to_screen(Vec2::new(150.0, 100.0));
439        assert_eq!(screen, Vec2::new(50.0, 50.0));
440    }
441
442    #[test]
443    fn world_to_screen_negative_when_world_is_left_of_camera() {
444        let mut cam = Camera::new(160.0, 144.0);
445        cam.position = Vec2::new(100.0, 50.0);
446        let screen = cam.world_to_screen(Vec2::new(50.0, 20.0));
447        assert_eq!(screen, Vec2::new(-50.0, -30.0));
448    }
449
450    #[test]
451    fn screen_to_world_at_origin() {
452        let cam = Camera::new(160.0, 144.0);
453        let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
454        assert_eq!(world, Vec2::new(80.0, 72.0));
455    }
456
457    #[test]
458    fn screen_to_world_offset() {
459        let mut cam = Camera::new(160.0, 144.0);
460        cam.position = Vec2::new(100.0, 50.0);
461        let world = cam.screen_to_world(Vec2::new(20.0, 30.0));
462        assert_eq!(world, Vec2::new(120.0, 80.0));
463    }
464
465    #[test]
466    fn round_trip_world_to_screen_to_world() {
467        let mut cam = Camera::new(160.0, 144.0);
468        cam.position = Vec2::new(42.5, 17.3);
469        cam.zoom = 1.5;
470
471        let world = Vec2::new(123.4, 56.7);
472        let screen = cam.world_to_screen(world);
473        let round_tripped = cam.screen_to_world(screen);
474
475        assert!((round_tripped.x - world.x).abs() < 0.001);
476        assert!((round_tripped.y - world.y).abs() < 0.001);
477    }
478
479    #[test]
480    fn round_trip_screen_to_world_to_screen() {
481        let mut cam = Camera::new(160.0, 144.0);
482        cam.position = Vec2::new(50.0, 30.0);
483        cam.zoom = 2.0;
484
485        let screen = Vec2::new(80.0, 72.0);
486        let world = cam.screen_to_world(screen);
487        let round_tripped = cam.world_to_screen(world);
488
489        assert!((round_tripped.x - screen.x).abs() < 0.001);
490        assert!((round_tripped.y - screen.y).abs() < 0.001);
491    }
492
493    // -----------------------------------------------------------------------
494    // Zoom
495    // -----------------------------------------------------------------------
496
497    #[test]
498    fn zoom_affects_world_to_screen() {
499        let mut cam = Camera::new(160.0, 144.0);
500        cam.position = Vec2::new(100.0, 100.0);
501
502        // At zoom 2.0, world point 200,200 should appear at screen 200,200
503        // (world - position) * zoom = (200-100)*2, (200-100)*2 = 200, 200
504        cam.zoom = 2.0;
505        let screen = cam.world_to_screen(Vec2::new(200.0, 200.0));
506        assert_eq!(screen, Vec2::new(200.0, 200.0));
507    }
508
509    #[test]
510    fn zoom_affects_screen_to_world() {
511        let mut cam = Camera::new(160.0, 144.0);
512        cam.position = Vec2::new(100.0, 100.0);
513        cam.zoom = 2.0;
514
515        // screen 80,72 → world: 80/2+100, 72/2+100 = 140, 136
516        let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
517        assert_eq!(world, Vec2::new(140.0, 136.0));
518    }
519
520    #[test]
521    fn set_zoom_clamps_to_range() {
522        let mut cam = Camera::new(160.0, 144.0);
523        cam.set_zoom(0.0);
524        assert_eq!(cam.zoom, 0.1);
525        cam.set_zoom(100.0);
526        assert_eq!(cam.zoom, 10.0);
527        cam.set_zoom(1.5);
528        assert_eq!(cam.zoom, 1.5);
529    }
530
531    #[test]
532    fn set_zoom_clamps_bounds() {
533        let mut cam = Camera::new(160.0, 144.0);
534        cam.position = Vec2::new(0.0, 0.0);
535        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 320.0, 288.0));
536
537        // At zoom 1.0, visible area is 160×144, max position is 160,144
538        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
539        cam.position = Vec2::new(200.0, 200.0);
540        cam.set_zoom(1.0);
541        assert_eq!(cam.position, Vec2::new(160.0, 144.0));
542    }
543
544    // -----------------------------------------------------------------------
545    // Boundary clamping
546    // -----------------------------------------------------------------------
547
548    #[test]
549    fn clamp_keeps_camera_in_bounds() {
550        let mut cam = Camera::new(160.0, 144.0);
551        cam.position = Vec2::new(500.0, 500.0);
552        // 256 - 160 = 96 max x, 256 - 144 = 112 max y
553        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
554        assert_eq!(cam.position, Vec2::new(96.0, 112.0));
555    }
556
557    #[test]
558    fn clamp_prevents_negative_position() {
559        let mut cam = Camera::new(160.0, 144.0);
560        cam.position = Vec2::new(-100.0, -50.0);
561        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
562        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
563    }
564
565    #[test]
566    fn clamp_centers_when_viewport_larger_than_bounds() {
567        let mut cam = Camera::new(320.0, 240.0);
568        cam.position = Vec2::new(0.0, 0.0);
569        // Bounds are smaller than viewport → camera should center
570        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 160.0, 120.0));
571        // center: (160 - 320) / 2 = -80, (120 - 240) / 2 = -60
572        assert_eq!(cam.position, Vec2::new(-80.0, -60.0));
573    }
574
575    #[test]
576    fn clear_bounds_allows_any_position() {
577        let mut cam = Camera::new(160.0, 144.0);
578        // viewport > bounds: centres to (-30, -22)
579        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 100.0, 100.0));
580        assert_eq!(cam.position, Vec2::new(-30.0, -22.0));
581
582        cam.position = Vec2::new(500.0, 500.0);
583        cam.clear_bounds();
584        assert_eq!(cam.position, Vec2::new(500.0, 500.0));
585    }
586
587    #[test]
588    fn update_clamps_after_follow() {
589        let mut cam = Camera::new(160.0, 144.0);
590        cam.smooth_factor = 0.0;
591        // 200 - 160 = 40 max x, 200 - 144 = 56 max y
592        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 200.0, 200.0));
593        cam.follow_target(Vec2::new(500.0, 500.0));
594        cam.update(0.016);
595        assert_eq!(cam.position, Vec2::new(40.0, 56.0));
596    }
597
598    // -----------------------------------------------------------------------
599    // Tile helpers
600    // -----------------------------------------------------------------------
601
602    #[test]
603    fn tile_x_returns_correct_column() {
604        let mut cam = Camera::new(160.0, 144.0);
605        cam.position = Vec2::new(0.0, 0.0);
606        assert_eq!(cam.tile_x(), 0);
607
608        cam.position = Vec2::new(8.0, 0.0);
609        assert_eq!(cam.tile_x(), 1);
610
611        cam.position = Vec2::new(15.0, 0.0);
612        assert_eq!(cam.tile_x(), 1); // floor(15/8) = 1
613
614        cam.position = Vec2::new(16.0, 0.0);
615        assert_eq!(cam.tile_x(), 2);
616
617        cam.position = Vec2::new(-5.0, 0.0);
618        assert_eq!(cam.tile_x(), -1); // floor(-5/8) = -1
619    }
620
621    #[test]
622    fn tile_y_returns_correct_row() {
623        let mut cam = Camera::new(160.0, 144.0);
624        cam.position = Vec2::new(0.0, 0.0);
625        assert_eq!(cam.tile_y(), 0);
626
627        cam.position = Vec2::new(0.0, 8.0);
628        assert_eq!(cam.tile_y(), 1);
629
630        cam.position = Vec2::new(0.0, 72.0);
631        assert_eq!(cam.tile_y(), 9); // floor(72/8) = 9
632    }
633
634    // -----------------------------------------------------------------------
635    // Visibility checks
636    // -----------------------------------------------------------------------
637
638    #[test]
639    fn is_visible_in_viewport() {
640        let cam = Camera::new(160.0, 144.0);
641        // Point in centre of screen
642        assert!(cam.is_visible(Vec2::new(80.0, 72.0)));
643        // Point at top-left corner
644        assert!(cam.is_visible(Vec2::new(0.0, 0.0)));
645        // Point just outside
646        assert!(cam.is_visible(Vec2::new(-7.0, 0.0))); // within margin
647                                                       // Point far outside
648        assert!(!cam.is_visible(Vec2::new(1000.0, 1000.0)));
649    }
650
651    #[test]
652    fn is_rect_visible_intersects() {
653        let cam = Camera::new(160.0, 144.0);
654        assert!(cam.is_rect_visible(Rect::new(0.0, 0.0, 32.0, 32.0)));
655        assert!(cam.is_rect_visible(Rect::new(140.0, 120.0, 32.0, 32.0)));
656    }
657
658    #[test]
659    fn is_rect_visible_far_away() {
660        let cam = Camera::new(160.0, 144.0);
661        assert!(!cam.is_rect_visible(Rect::new(1000.0, 1000.0, 32.0, 32.0)));
662    }
663
664    // -----------------------------------------------------------------------
665    // Vec2 / Rect
666    // -----------------------------------------------------------------------
667
668    #[test]
669    fn vec2_new() {
670        let v = Vec2::new(3.0, 4.0);
671        assert_eq!(v.x, 3.0);
672        assert_eq!(v.y, 4.0);
673    }
674
675    #[test]
676    fn rect_new() {
677        let r = Rect::new(1.0, 2.0, 10.0, 20.0);
678        assert_eq!(r.x, 1.0);
679        assert_eq!(r.y, 2.0);
680        assert_eq!(r.w, 10.0);
681        assert_eq!(r.h, 20.0);
682    }
683}