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