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                let t = 1.0 - (-follow_strength * dt).exp();
137                self.position.x += (target.x - self.position.x) * t;
138                self.position.y += (target.y - self.position.y) * t;
139            }
140        }
141
142        self.apply_bounds();
143    }
144
145    // ---------------------------------------------------------------------------
146    // Coordinate conversion
147    // ---------------------------------------------------------------------------
148
149    /// Convert a world-space position to a screen-space position, accounting for
150    /// the camera's current position and zoom.
151    ///
152    /// ```
153    /// # use dotzuki_engine::camera::{Camera, Vec2};
154    /// let cam = Camera::new(160.0, 144.0);
155    /// let screen = cam.world_to_screen(Vec2::new(80.0, 72.0));
156    /// assert_eq!(screen.x, 80.0);
157    /// assert_eq!(screen.y, 72.0);
158    /// ```
159    pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
160        Vec2::new(
161            (world_pos.x - self.position.x) * self.zoom,
162            (world_pos.y - self.position.y) * self.zoom,
163        )
164    }
165
166    /// Convert a screen-space position to a world-space position.
167    ///
168    /// This is the inverse of [`world_to_screen`](Self::world_to_screen).
169    ///
170    /// ```
171    /// # use dotzuki_engine::camera::{Camera, Vec2};
172    /// let cam = Camera::new(160.0, 144.0);
173    /// let world = cam.screen_to_world(Vec2::new(160.0, 144.0));
174    /// assert_eq!(world.x, 160.0);
175    /// assert_eq!(world.y, 144.0);
176    /// ```
177    pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
178        Vec2::new(
179            screen_pos.x / self.zoom + self.position.x,
180            screen_pos.y / self.zoom + self.position.y,
181        )
182    }
183
184    // ---------------------------------------------------------------------------
185    // Bounds
186    // ---------------------------------------------------------------------------
187
188    /// Set the world boundary rectangle and immediately clamp the camera to it.
189    ///
190    /// The camera will never show content outside these bounds. The visible area
191    /// (viewport_size / zoom) must fit within the bounds; if the viewport is larger
192    /// than the bounds, the camera is centered on the bounds area.
193    pub fn clamp_to_bounds(&mut self, bounds: Rect) {
194        self.bounds = Some(bounds);
195        self.apply_bounds();
196    }
197
198    /// Remove world boundaries. The camera can scroll anywhere.
199    pub fn clear_bounds(&mut self) {
200        self.bounds = None;
201    }
202
203    /// Clamp `position` so the viewport stays within [`bounds`](Self::bounds).
204    fn apply_bounds(&mut self) {
205        let Some(ref bounds) = self.bounds else {
206            return;
207        };
208
209        let visible_w = self.viewport_size.x / self.zoom;
210        let visible_h = self.viewport_size.y / self.zoom;
211
212        // If the viewport is larger than the bounds, centre on the bounds area.
213        if visible_w >= bounds.w {
214            self.position.x = bounds.x + (bounds.w - visible_w) * 0.5;
215        } else {
216            self.position.x = self.position.x.clamp(bounds.x, bounds.x + bounds.w - visible_w);
217        }
218
219        if visible_h >= bounds.h {
220            self.position.y = bounds.y + (bounds.h - visible_h) * 0.5;
221        } else {
222            self.position.y = self.position.y.clamp(bounds.y, bounds.y + bounds.h - visible_h);
223        }
224    }
225
226    // ---------------------------------------------------------------------------
227    // Zoom
228    // ---------------------------------------------------------------------------
229
230    /// Set the zoom level, clamped to the valid range `[0.1, 10.0]`.
231    pub fn set_zoom(&mut self, zoom: f32) {
232        self.zoom = zoom.clamp(0.1, 10.0);
233        // Re-clamp after zoom change because the visible area changed.
234        self.apply_bounds();
235    }
236
237    // ---------------------------------------------------------------------------
238    // Tile helpers
239    // ---------------------------------------------------------------------------
240
241    /// Which tile column is at the camera's left edge.
242    ///
243    /// Divides the camera's world x-position by `TILE_SIZE` (8 px) and floors.
244    pub fn tile_x(&self) -> i32 {
245        (self.position.x / TILE_SIZE).floor() as i32
246    }
247
248    /// Which tile row is at the camera's top edge.
249    ///
250    /// Divides the camera's world y-position by `TILE_SIZE` (8 px) and floors.
251    pub fn tile_y(&self) -> i32 {
252        (self.position.y / TILE_SIZE).floor() as i32
253    }
254
255    // ---------------------------------------------------------------------------
256    // Visibility checks
257    // ---------------------------------------------------------------------------
258
259    /// Returns `true` if a world-space point is visible on screen (within the
260    /// viewport), accounting for the camera position and zoom.
261    pub fn is_visible(&self, world_pos: Vec2) -> bool {
262        let screen = self.world_to_screen(world_pos);
263        screen.x >= -TILE_SIZE
264            && screen.y >= -TILE_SIZE
265            && screen.x < self.viewport_size.x + TILE_SIZE
266            && screen.y < self.viewport_size.y + TILE_SIZE
267    }
268
269    /// Returns `true` if any part of a world-space axis-aligned rectangle is
270    /// visible on screen.
271    pub fn is_rect_visible(&self, rect: Rect) -> bool {
272        let visible_w = self.viewport_size.x / self.zoom;
273        let visible_h = self.viewport_size.y / self.zoom;
274
275        // Expand viewport by one tile in each direction for culling margin.
276        let margin = TILE_SIZE / self.zoom;
277
278        !(rect.x + rect.w < self.position.x - margin
279            || rect.x > self.position.x + visible_w + margin
280            || rect.y + rect.h < self.position.y - margin
281            || rect.y > self.position.y + visible_h + margin)
282    }
283}
284
285
286// =============================================================================
287// Unit tests
288// =============================================================================
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    // -----------------------------------------------------------------------
295    // Construction
296    // -----------------------------------------------------------------------
297
298    #[test]
299    fn camera_starts_at_origin() {
300        let cam = Camera::new(160.0, 144.0);
301        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
302        assert_eq!(cam.zoom, 1.0);
303        assert_eq!(cam.smooth_factor, 0.0);
304        assert!(cam.target.is_none());
305        assert!(cam.bounds.is_none());
306    }
307
308    #[test]
309    fn new_is_gameboy_screen() {
310        let cam = Camera::new(160.0, 144.0);
311        assert_eq!(cam.viewport_size, Vec2::new(160.0, 144.0));
312    }
313
314    #[test]
315    fn custom_viewport_size() {
316        let cam = Camera::new(640.0, 480.0);
317        assert_eq!(cam.viewport_size, Vec2::new(640.0, 480.0));
318    }
319
320    // -----------------------------------------------------------------------
321    // Follow target
322    // -----------------------------------------------------------------------
323
324    #[test]
325    fn follow_target_sets_target() {
326        let mut cam = Camera::new(160.0, 144.0);
327        cam.follow_target(Vec2::new(100.0, 50.0));
328        assert_eq!(cam.target, Some(Vec2::new(100.0, 50.0)));
329    }
330
331    #[test]
332    fn follow_target_overwrites_previous() {
333        let mut cam = Camera::new(160.0, 144.0);
334        cam.follow_target(Vec2::new(10.0, 20.0));
335        cam.follow_target(Vec2::new(30.0, 40.0));
336        assert_eq!(cam.target, Some(Vec2::new(30.0, 40.0)));
337    }
338
339    #[test]
340    fn stop_follow_clears_target() {
341        let mut cam = Camera::new(160.0, 144.0);
342        cam.follow_target(Vec2::new(100.0, 50.0));
343        cam.stop_follow();
344        assert!(cam.target.is_none());
345    }
346
347    #[test]
348    fn update_snaps_when_smooth_factor_zero() {
349        let mut cam = Camera::new(160.0, 144.0);
350        cam.smooth_factor = 0.0;
351        cam.follow_target(Vec2::new(100.0, 50.0));
352        cam.update(0.016);
353        assert_eq!(cam.position, Vec2::new(100.0, 50.0));
354    }
355
356    #[test]
357    fn update_moves_toward_target_with_smooth() {
358        let mut cam = Camera::new(160.0, 144.0);
359        cam.smooth_factor = 0.5;
360        cam.follow_target(Vec2::new(100.0, 0.0));
361        let start = cam.position;
362        cam.update(0.016);
363        assert!(cam.position.x > start.x);
364        assert!(cam.position.x < 100.0);
365    }
366
367    #[test]
368    fn update_no_movement_when_smooth_factor_one() {
369        let mut cam = Camera::new(160.0, 144.0);
370        cam.smooth_factor = 1.0;
371        cam.position = Vec2::new(50.0, 50.0);
372        cam.follow_target(Vec2::new(200.0, 200.0));
373        cam.update(0.016);
374        assert_eq!(cam.position.x, 50.0);
375        assert_eq!(cam.position.y, 50.0);
376    }
377
378    #[test]
379    fn update_no_target_does_nothing() {
380        let mut cam = Camera::new(160.0, 144.0);
381        cam.position = Vec2::new(30.0, 40.0);
382        cam.update(0.016);
383        assert_eq!(cam.position, Vec2::new(30.0, 40.0));
384    }
385
386    #[test]
387    fn update_converges_after_many_frames() {
388        let mut cam = Camera::new(160.0, 144.0);
389        cam.smooth_factor = 0.2;
390        cam.follow_target(Vec2::new(200.0, 0.0));
391        for _ in 0..200 {
392            cam.update(0.016);
393        }
394        assert!((cam.position.x - 200.0).abs() < 1.0);
395    }
396
397    // -----------------------------------------------------------------------
398    // Coordinate conversion
399    // -----------------------------------------------------------------------
400
401    #[test]
402    fn world_to_screen_at_origin() {
403        let cam = Camera::new(160.0, 144.0);
404        let screen = cam.world_to_screen(Vec2::new(50.0, 30.0));
405        assert_eq!(screen, Vec2::new(50.0, 30.0));
406    }
407
408    #[test]
409    fn world_to_screen_offset() {
410        let mut cam = Camera::new(160.0, 144.0);
411        cam.position = Vec2::new(100.0, 50.0);
412        let screen = cam.world_to_screen(Vec2::new(150.0, 100.0));
413        assert_eq!(screen, Vec2::new(50.0, 50.0));
414    }
415
416    #[test]
417    fn world_to_screen_negative_when_world_is_left_of_camera() {
418        let mut cam = Camera::new(160.0, 144.0);
419        cam.position = Vec2::new(100.0, 50.0);
420        let screen = cam.world_to_screen(Vec2::new(50.0, 20.0));
421        assert_eq!(screen, Vec2::new(-50.0, -30.0));
422    }
423
424    #[test]
425    fn screen_to_world_at_origin() {
426        let cam = Camera::new(160.0, 144.0);
427        let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
428        assert_eq!(world, Vec2::new(80.0, 72.0));
429    }
430
431    #[test]
432    fn screen_to_world_offset() {
433        let mut cam = Camera::new(160.0, 144.0);
434        cam.position = Vec2::new(100.0, 50.0);
435        let world = cam.screen_to_world(Vec2::new(20.0, 30.0));
436        assert_eq!(world, Vec2::new(120.0, 80.0));
437    }
438
439    #[test]
440    fn round_trip_world_to_screen_to_world() {
441        let mut cam = Camera::new(160.0, 144.0);
442        cam.position = Vec2::new(42.5, 17.3);
443        cam.zoom = 1.5;
444
445        let world = Vec2::new(123.4, 56.7);
446        let screen = cam.world_to_screen(world);
447        let round_tripped = cam.screen_to_world(screen);
448
449        assert!((round_tripped.x - world.x).abs() < 0.001);
450        assert!((round_tripped.y - world.y).abs() < 0.001);
451    }
452
453    #[test]
454    fn round_trip_screen_to_world_to_screen() {
455        let mut cam = Camera::new(160.0, 144.0);
456        cam.position = Vec2::new(50.0, 30.0);
457        cam.zoom = 2.0;
458
459        let screen = Vec2::new(80.0, 72.0);
460        let world = cam.screen_to_world(screen);
461        let round_tripped = cam.world_to_screen(world);
462
463        assert!((round_tripped.x - screen.x).abs() < 0.001);
464        assert!((round_tripped.y - screen.y).abs() < 0.001);
465    }
466
467    // -----------------------------------------------------------------------
468    // Zoom
469    // -----------------------------------------------------------------------
470
471    #[test]
472    fn zoom_affects_world_to_screen() {
473        let mut cam = Camera::new(160.0, 144.0);
474        cam.position = Vec2::new(100.0, 100.0);
475
476        // At zoom 2.0, world point 200,200 should appear at screen 200,200
477        // (world - position) * zoom = (200-100)*2, (200-100)*2 = 200, 200
478        cam.zoom = 2.0;
479        let screen = cam.world_to_screen(Vec2::new(200.0, 200.0));
480        assert_eq!(screen, Vec2::new(200.0, 200.0));
481    }
482
483    #[test]
484    fn zoom_affects_screen_to_world() {
485        let mut cam = Camera::new(160.0, 144.0);
486        cam.position = Vec2::new(100.0, 100.0);
487        cam.zoom = 2.0;
488
489        // screen 80,72 → world: 80/2+100, 72/2+100 = 140, 136
490        let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
491        assert_eq!(world, Vec2::new(140.0, 136.0));
492    }
493
494    #[test]
495    fn set_zoom_clamps_to_range() {
496        let mut cam = Camera::new(160.0, 144.0);
497        cam.set_zoom(0.0);
498        assert_eq!(cam.zoom, 0.1);
499        cam.set_zoom(100.0);
500        assert_eq!(cam.zoom, 10.0);
501        cam.set_zoom(1.5);
502        assert_eq!(cam.zoom, 1.5);
503    }
504
505    #[test]
506    fn set_zoom_clamps_bounds() {
507        let mut cam = Camera::new(160.0, 144.0);
508        cam.position = Vec2::new(0.0, 0.0);
509        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 320.0, 288.0));
510
511        // At zoom 1.0, visible area is 160×144, max position is 160,144
512        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
513        cam.position = Vec2::new(200.0, 200.0);
514        cam.set_zoom(1.0);
515        assert_eq!(cam.position, Vec2::new(160.0, 144.0));
516    }
517
518    // -----------------------------------------------------------------------
519    // Boundary clamping
520    // -----------------------------------------------------------------------
521
522    #[test]
523    fn clamp_keeps_camera_in_bounds() {
524        let mut cam = Camera::new(160.0, 144.0);
525        cam.position = Vec2::new(500.0, 500.0);
526        // 256 - 160 = 96 max x, 256 - 144 = 112 max y
527        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
528        assert_eq!(cam.position, Vec2::new(96.0, 112.0));
529    }
530
531    #[test]
532    fn clamp_prevents_negative_position() {
533        let mut cam = Camera::new(160.0, 144.0);
534        cam.position = Vec2::new(-100.0, -50.0);
535        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
536        assert_eq!(cam.position, Vec2::new(0.0, 0.0));
537    }
538
539    #[test]
540    fn clamp_centers_when_viewport_larger_than_bounds() {
541        let mut cam = Camera::new(320.0, 240.0);
542        cam.position = Vec2::new(0.0, 0.0);
543        // Bounds are smaller than viewport → camera should center
544        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 160.0, 120.0));
545        // center: (160 - 320) / 2 = -80, (120 - 240) / 2 = -60
546        assert_eq!(cam.position, Vec2::new(-80.0, -60.0));
547    }
548
549    #[test]
550    fn clear_bounds_allows_any_position() {
551        let mut cam = Camera::new(160.0, 144.0);
552        // viewport > bounds: centres to (-30, -22)
553        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 100.0, 100.0));
554        assert_eq!(cam.position, Vec2::new(-30.0, -22.0));
555
556        cam.position = Vec2::new(500.0, 500.0);
557        cam.clear_bounds();
558        assert_eq!(cam.position, Vec2::new(500.0, 500.0));
559    }
560
561    #[test]
562    fn update_clamps_after_follow() {
563        let mut cam = Camera::new(160.0, 144.0);
564        cam.smooth_factor = 0.0;
565        // 200 - 160 = 40 max x, 200 - 144 = 56 max y
566        cam.clamp_to_bounds(Rect::new(0.0, 0.0, 200.0, 200.0));
567        cam.follow_target(Vec2::new(500.0, 500.0));
568        cam.update(0.016);
569        assert_eq!(cam.position, Vec2::new(40.0, 56.0));
570    }
571
572    // -----------------------------------------------------------------------
573    // Tile helpers
574    // -----------------------------------------------------------------------
575
576    #[test]
577    fn tile_x_returns_correct_column() {
578        let mut cam = Camera::new(160.0, 144.0);
579        cam.position = Vec2::new(0.0, 0.0);
580        assert_eq!(cam.tile_x(), 0);
581
582        cam.position = Vec2::new(8.0, 0.0);
583        assert_eq!(cam.tile_x(), 1);
584
585        cam.position = Vec2::new(15.0, 0.0);
586        assert_eq!(cam.tile_x(), 1); // floor(15/8) = 1
587
588        cam.position = Vec2::new(16.0, 0.0);
589        assert_eq!(cam.tile_x(), 2);
590
591        cam.position = Vec2::new(-5.0, 0.0);
592        assert_eq!(cam.tile_x(), -1); // floor(-5/8) = -1
593    }
594
595    #[test]
596    fn tile_y_returns_correct_row() {
597        let mut cam = Camera::new(160.0, 144.0);
598        cam.position = Vec2::new(0.0, 0.0);
599        assert_eq!(cam.tile_y(), 0);
600
601        cam.position = Vec2::new(0.0, 8.0);
602        assert_eq!(cam.tile_y(), 1);
603
604        cam.position = Vec2::new(0.0, 72.0);
605        assert_eq!(cam.tile_y(), 9); // floor(72/8) = 9
606    }
607
608    // -----------------------------------------------------------------------
609    // Visibility checks
610    // -----------------------------------------------------------------------
611
612    #[test]
613    fn is_visible_in_viewport() {
614        let cam = Camera::new(160.0, 144.0);
615        // Point in centre of screen
616        assert!(cam.is_visible(Vec2::new(80.0, 72.0)));
617        // Point at top-left corner
618        assert!(cam.is_visible(Vec2::new(0.0, 0.0)));
619        // Point just outside
620        assert!(cam.is_visible(Vec2::new(-7.0, 0.0))); // within margin
621        // Point far outside
622        assert!(!cam.is_visible(Vec2::new(1000.0, 1000.0)));
623    }
624
625    #[test]
626    fn is_rect_visible_intersects() {
627        let cam = Camera::new(160.0, 144.0);
628        assert!(cam.is_rect_visible(Rect::new(0.0, 0.0, 32.0, 32.0)));
629        assert!(cam.is_rect_visible(Rect::new(140.0, 120.0, 32.0, 32.0)));
630    }
631
632    #[test]
633    fn is_rect_visible_far_away() {
634        let cam = Camera::new(160.0, 144.0);
635        assert!(!cam.is_rect_visible(Rect::new(1000.0, 1000.0, 32.0, 32.0)));
636    }
637
638    // -----------------------------------------------------------------------
639    // Vec2 / Rect
640    // -----------------------------------------------------------------------
641
642    #[test]
643    fn vec2_new() {
644        let v = Vec2::new(3.0, 4.0);
645        assert_eq!(v.x, 3.0);
646        assert_eq!(v.y, 4.0);
647    }
648
649    #[test]
650    fn rect_new() {
651        let r = Rect::new(1.0, 2.0, 10.0, 20.0);
652        assert_eq!(r.x, 1.0);
653        assert_eq!(r.y, 2.0);
654        assert_eq!(r.w, 10.0);
655        assert_eq!(r.h, 20.0);
656    }
657}