1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_coordinate::GeometryError;
5use use_point::Point2;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Aabb2 {
10 min: Point2,
11 max: Point2,
12}
13
14impl Aabb2 {
15 pub fn try_new(min: Point2, max: Point2) -> Result<Self, GeometryError> {
25 let min = min.validate()?;
26 let max = max.validate()?;
27
28 if min.x() > max.x() || min.y() > max.y() {
29 return Err(GeometryError::InvalidBounds {
30 min_x: min.x(),
31 min_y: min.y(),
32 max_x: max.x(),
33 max_y: max.y(),
34 });
35 }
36
37 Ok(Self { min, max })
38 }
39
40 #[must_use]
42 pub const fn from_points(a: Point2, b: Point2) -> Self {
43 Self {
44 min: Point2::new(a.x().min(b.x()), a.y().min(b.y())),
45 max: Point2::new(a.x().max(b.x()), a.y().max(b.y())),
46 }
47 }
48
49 #[must_use]
51 pub const fn from_point(point: Point2) -> Self {
52 Self::from_points(point, point)
53 }
54
55 #[must_use]
57 pub const fn min(&self) -> Point2 {
58 self.min
59 }
60
61 #[must_use]
63 pub const fn max(&self) -> Point2 {
64 self.max
65 }
66
67 #[must_use]
69 pub fn width(&self) -> f64 {
70 self.max.x() - self.min.x()
71 }
72
73 #[must_use]
75 pub fn height(&self) -> f64 {
76 self.max.y() - self.min.y()
77 }
78
79 #[must_use]
81 pub const fn center(&self) -> Point2 {
82 self.min.midpoint(self.max)
83 }
84
85 #[must_use]
87 pub fn area(&self) -> f64 {
88 self.width() * self.height()
89 }
90
91 #[must_use]
93 pub fn contains_point(&self, point: Point2) -> bool {
94 point.x() >= self.min.x()
95 && point.x() <= self.max.x()
96 && point.y() >= self.min.y()
97 && point.y() <= self.max.y()
98 }
99
100 pub fn contains_point_with_tolerance(
109 &self,
110 point: Point2,
111 tolerance: f64,
112 ) -> Result<bool, GeometryError> {
113 let tolerance = GeometryError::validate_tolerance(tolerance)?;
114
115 Ok(point.x() >= self.min.x() - tolerance
116 && point.x() <= self.max.x() + tolerance
117 && point.y() >= self.min.y() - tolerance
118 && point.y() <= self.max.y() + tolerance)
119 }
120
121 #[must_use]
123 pub fn is_degenerate(&self) -> bool {
124 self.width() == 0.0 || self.height() == 0.0
125 }
126
127 pub fn is_degenerate_with_tolerance(&self, tolerance: f64) -> Result<bool, GeometryError> {
136 let tolerance = GeometryError::validate_tolerance(tolerance)?;
137
138 Ok(self.width() <= tolerance || self.height() <= tolerance)
139 }
140}
141
142impl From<Point2> for Aabb2 {
143 fn from(point: Point2) -> Self {
144 Self::from_point(point)
145 }
146}
147
148#[must_use]
150pub const fn aabb_from_points(a: Point2, b: Point2) -> Aabb2 {
151 Aabb2::from_points(a, b)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::{Aabb2, aabb_from_points};
157 use use_coordinate::GeometryError;
158 use use_point::Point2;
159
160 fn approx_eq(left: f64, right: f64) -> bool {
161 (left - right).abs() < 1.0e-10
162 }
163
164 #[test]
165 fn constructs_valid_aabbs() {
166 let bounds =
167 Aabb2::try_new(Point2::new(1.0, 2.0), Point2::new(4.0, 6.0)).expect("valid bounds");
168
169 assert_eq!(bounds.min(), Point2::new(1.0, 2.0));
170 assert_eq!(bounds.max(), Point2::new(4.0, 6.0));
171 }
172
173 #[test]
174 fn rejects_invalid_aabb_ordering() {
175 assert_eq!(
176 Aabb2::try_new(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0)),
177 Err(GeometryError::InvalidBounds {
178 min_x: 4.0,
179 min_y: 1.0,
180 max_x: 1.0,
181 max_y: 3.0,
182 })
183 );
184 }
185
186 #[test]
187 fn normalizes_point_order() {
188 let bounds = Aabb2::from_points(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0));
189
190 assert_eq!(bounds.min(), Point2::new(1.0, 1.0));
191 assert_eq!(bounds.max(), Point2::new(4.0, 3.0));
192 assert_eq!(
193 aabb_from_points(Point2::new(4.0, 1.0), Point2::new(1.0, 3.0)),
194 bounds
195 );
196 }
197
198 #[test]
199 fn computes_dimensions_center_and_area() {
200 let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
201
202 assert!(approx_eq(bounds.width(), 3.0));
203 assert!(approx_eq(bounds.height(), 2.0));
204 assert_eq!(bounds.center(), Point2::new(2.5, 2.0));
205 assert!(approx_eq(bounds.area(), 6.0));
206 }
207
208 #[test]
209 fn contains_points_including_boundary() {
210 let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
211
212 assert!(bounds.contains_point(Point2::new(2.0, 2.0)));
213 assert!(bounds.contains_point(Point2::new(1.0, 3.0)));
214 assert!(!bounds.contains_point(Point2::new(4.5, 3.0)));
215 }
216
217 #[test]
218 fn supports_tolerance_based_containment() {
219 let bounds = Aabb2::from_points(Point2::new(1.0, 1.0), Point2::new(4.0, 3.0));
220
221 assert_eq!(
222 bounds.contains_point_with_tolerance(Point2::new(4.25, 3.0), 0.25),
223 Ok(true)
224 );
225 assert_eq!(
226 bounds.contains_point_with_tolerance(Point2::new(4.25, 3.0), -0.25),
227 Err(GeometryError::NegativeTolerance(-0.25))
228 );
229 }
230
231 #[test]
232 fn detects_degenerate_bounds() {
233 let point_bounds = Aabb2::from_point(Point2::new(2.0, 2.0));
234 let line_bounds = Aabb2::from_points(Point2::new(2.0, 1.0), Point2::new(2.0, 3.0));
235
236 assert!(point_bounds.is_degenerate());
237 assert!(line_bounds.is_degenerate());
238 assert_eq!(line_bounds.is_degenerate_with_tolerance(0.0), Ok(true));
239 assert_eq!(
240 Aabb2::from(Point2::new(1.0, 1.0)),
241 Aabb2::from_point(Point2::new(1.0, 1.0))
242 );
243 }
244}