Skip to main content

esoc_scene/
bounds.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Bounding boxes and data bounds.
3
4/// An axis-aligned bounding box in visual space.
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
6pub struct BoundingBox {
7    /// Left edge.
8    pub x: f32,
9    /// Top edge.
10    pub y: f32,
11    /// Width.
12    pub w: f32,
13    /// Height.
14    pub h: f32,
15}
16
17impl BoundingBox {
18    /// Create a bounding box.
19    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
20        Self { x, y, w, h }
21    }
22
23    /// Right edge.
24    pub fn right(&self) -> f32 {
25        self.x + self.w
26    }
27
28    /// Bottom edge.
29    pub fn bottom(&self) -> f32 {
30        self.y + self.h
31    }
32
33    /// Center point.
34    pub fn center(&self) -> [f32; 2] {
35        [self.x + self.w * 0.5, self.y + self.h * 0.5]
36    }
37
38    /// Whether a point is inside.
39    pub fn contains(&self, p: [f32; 2]) -> bool {
40        p[0] >= self.x && p[0] <= self.right() && p[1] >= self.y && p[1] <= self.bottom()
41    }
42
43    /// Union of two bounding boxes.
44    pub fn union(self, other: Self) -> Self {
45        let x = self.x.min(other.x);
46        let y = self.y.min(other.y);
47        let r = self.right().max(other.right());
48        let b = self.bottom().max(other.bottom());
49        Self {
50            x,
51            y,
52            w: r - x,
53            h: b - y,
54        }
55    }
56}
57
58/// Data-space bounds (f64 for scientific precision).
59#[derive(Clone, Copy, Debug, Default)]
60pub struct DataBounds {
61    /// Minimum X value.
62    pub x_min: f64,
63    /// Maximum X value.
64    pub x_max: f64,
65    /// Minimum Y value.
66    pub y_min: f64,
67    /// Maximum Y value.
68    pub y_max: f64,
69}
70
71impl DataBounds {
72    /// Create from x/y ranges.
73    pub fn new(x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
74        Self {
75            x_min,
76            x_max,
77            y_min,
78            y_max,
79        }
80    }
81
82    /// Union of two data bounds.
83    pub fn union(self, other: Self) -> Self {
84        Self {
85            x_min: self.x_min.min(other.x_min),
86            x_max: self.x_max.max(other.x_max),
87            y_min: self.y_min.min(other.y_min),
88            y_max: self.y_max.max(other.y_max),
89        }
90    }
91
92    /// Expand to include a point.
93    pub fn include_point(&mut self, x: f64, y: f64) {
94        self.x_min = self.x_min.min(x);
95        self.x_max = self.x_max.max(x);
96        self.y_min = self.y_min.min(y);
97        self.y_max = self.y_max.max(y);
98    }
99
100    /// Add padding (fraction of range).
101    pub fn pad(self, fraction: f64) -> Self {
102        let dx = (self.x_max - self.x_min) * fraction;
103        let dy = (self.y_max - self.y_min) * fraction;
104        Self {
105            x_min: self.x_min - dx,
106            x_max: self.x_max + dx,
107            y_min: self.y_min - dy,
108            y_max: self.y_max + dy,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn bbox_contains() {
119        let bb = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
120        assert!(bb.contains([50.0, 40.0]));
121        assert!(!bb.contains([0.0, 0.0]));
122    }
123
124    #[test]
125    fn bbox_union() {
126        let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
127        let b = BoundingBox::new(5.0, 5.0, 10.0, 10.0);
128        let u = a.union(b);
129        assert!((u.x).abs() < 1e-6);
130        assert!((u.y).abs() < 1e-6);
131        assert!((u.w - 15.0).abs() < 1e-6);
132        assert!((u.h - 15.0).abs() < 1e-6);
133    }
134
135    #[test]
136    fn data_bounds_pad() {
137        let b = DataBounds::new(0.0, 100.0, 0.0, 50.0);
138        let padded = b.pad(0.1);
139        assert!((padded.x_min - (-10.0)).abs() < 1e-6);
140        assert!((padded.x_max - 110.0).abs() < 1e-6);
141    }
142}