1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use bevy::prelude::Resource;
use glam::Vec2;
use parry2d::bounding_volume::Aabb;
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Maximum size of a side of the map in meters.
pub const MAX_MAP_SIZE: f32 = 8000.;

#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Resource)]
pub struct MapBounds(Vec2);

impl MapBounds {
    /// Create new map bounds spanning a rectangle between -(size / 2.0) and a
    /// (size / 2.0).
    ///
    /// # Panics
    ///
    /// * If the size coordinates are not finite and positive.
    /// * If the size is too large.
    pub fn new(size: Vec2) -> Self {
        let bounds = Self(size / 2.);
        bounds.validate().unwrap();
        bounds
    }

    /// Minimum point of the map.
    pub fn min(&self) -> Vec2 {
        -self.0
    }

    /// Maximum point of the map.
    pub fn max(&self) -> Vec2 {
        self.0
    }

    /// Bounding box of the map.
    pub fn aabb(&self) -> Aabb {
        Aabb::new(self.min().into(), self.max().into())
    }

    pub fn size(&self) -> Vec2 {
        2. * self.0
    }

    /// Return true if the point lies within map boundaries. Note that map
    /// boundaries are inclusive.
    pub fn contains(&self, point: Vec2) -> bool {
        self.0.cmpge(point.abs()).all()
    }

    /// Projects a point from relative space to the map flat coordinates.
    ///
    /// # Arguments
    ///
    /// * `point` - relative point on the map between (0, 0) and (1, 1). Point
    ///   (0, 0) corresponds to the south-west corner.
    pub fn rel_to_abs(&self, point: Vec2) -> Vec2 {
        self.min() + point * self.size()
    }

    pub(crate) fn validate(&self) -> Result<(), MapBoundsValidationError> {
        if !self.0.is_finite() || self.0.cmple(Vec2::ZERO).any() {
            return Err(MapBoundsValidationError::Invalid(self.0));
        }

        let max = Vec2::splat(0.5 * MAX_MAP_SIZE);
        if self.0.cmpgt(max).any() {
            return Err(MapBoundsValidationError::TooLarge { max, value: self.0 });
        }
        Ok(())
    }
}

#[derive(Error, Debug)]
pub enum MapBoundsValidationError {
    #[error("map half-size has to be a positive and finite: got ({}, {})", .0.x, .0.y)]
    Invalid(Vec2),
    #[error("map half-size ({}, {}) is larger than maximum ({}, {})", .value.x, .value.y, .max.x, .max.y )]
    TooLarge { max: Vec2, value: Vec2 },
}

#[cfg(test)]
mod test {
    use parry2d::math::Point;

    use super::*;

    #[test]
    fn test_bounds() {
        let bounds = MapBounds(Vec2::new(2.5, 3.5));
        assert_eq!(
            bounds.aabb(),
            Aabb::new(Point::new(-2.5, -3.5), Point::new(2.5, 3.5))
        );
    }

    #[test]
    fn test_contains() {
        let bounds = MapBounds(Vec2::new(2., 3.));
        assert!(bounds.contains(Vec2::ZERO));
        assert!(bounds.contains(Vec2::new(2., 3.)));
        assert!(!bounds.contains(Vec2::new(3., 3.)));
        assert!(!bounds.contains(Vec2::new(f32::INFINITY, 3.)));
        assert!(!bounds.contains(Vec2::new(f32::NEG_INFINITY, 3.)));
        assert!(!bounds.contains(Vec2::new(f32::NAN, 3.)));
    }

    #[test]
    fn test_validate() {
        assert!(MapBounds(Vec2::new(2.5, 3.)).validate().is_ok());
        assert!(MapBounds(Vec2::new(f32::NAN, 2.)).validate().is_err());
        assert!(MapBounds(Vec2::new(f32::INFINITY, 2.)).validate().is_err());
        assert!(MapBounds(Vec2::new(f32::NEG_INFINITY, 2.))
            .validate()
            .is_err());
        assert!(MapBounds(Vec2::new(2., 0.)).validate().is_err());

        let invalid_bounds = MapBounds(Vec2::new(-2.5, 3.));
        match invalid_bounds.validate() {
            Err(error) => {
                match error {
                    MapBoundsValidationError::Invalid(size) => {
                        assert_eq!(size, Vec2::new(-2.5, 3.));
                    }
                    _ => unreachable!("Wrong error returned."),
                }

                assert_eq!(
                    format!("{error}"),
                    "map half-size has to be a positive and finite: got (-2.5, 3)"
                );
            }
            Ok(()) => unreachable!(),
        }

        let too_large_bounds = MapBounds(Vec2::new(10., 99999.));
        match too_large_bounds.validate() {
            Err(error) => {
                match error {
                    MapBoundsValidationError::TooLarge { value, max } => {
                        assert_eq!(max, Vec2::new(4000., 4000.));
                        assert_eq!(value, Vec2::new(10., 99999.));
                    }
                    _ => unreachable!("Wrong error returned."),
                }

                assert_eq!(
                    format!("{error}"),
                    "map half-size (10, 99999) is larger than maximum (4000, 4000)"
                );
            }
            Ok(()) => unreachable!(),
        }
    }
}