microcad_core/geo2d/
bounds.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! 2D Geometry bounds.
5
6use derive_more::Deref;
7use geo::coord;
8
9use crate::*;
10
11/// Bounds2D type alias.
12pub type Bounds2D = Bounds<Vec2>;
13
14impl Bounds2D {
15    /// Check if bounds are valid.
16    pub fn is_valid(&self) -> bool {
17        self.min.x <= self.max.x && self.min.y <= self.max.y
18    }
19
20    /// Calculate width of these bounds.
21    pub fn width(&self) -> Scalar {
22        (self.max.x - self.min.x).max(0.0)
23    }
24
25    /// Calculate height of these bounds.
26    pub fn height(&self) -> Scalar {
27        (self.max.y - self.min.y).max(0.0)
28    }
29
30    /// Maximum of width and height.
31    pub fn max_extent(&self) -> Scalar {
32        self.width().max(self.height())
33    }
34
35    /// Return rect.
36    pub fn rect(&self) -> Option<Rect> {
37        if self.is_valid() {
38            Some(Rect::new(
39                coord! {x: self.min.x, y: self.min.y },
40                coord! {x: self.max.x, y: self.max.y },
41            ))
42        } else {
43            None
44        }
45    }
46
47    /// Enlarge bounds by a factor and return new bounds
48    pub fn enlarge(&self, factor: Scalar) -> Self {
49        match self.rect() {
50            Some(rect) => {
51                let c = rect.center();
52                let s: geo::Coord = (rect.width(), rect.height()).into();
53                let s = s * 0.5 * (1.0 + factor);
54                Rect::new(c - s, c + s).into()
55            }
56            None => Bounds2D::default(),
57        }
58    }
59
60    /// Calculate extended bounds.
61    pub fn extend(mut self, other: Bounds2D) -> Self {
62        self.extend_by_point(other.min);
63        self.extend_by_point(other.max);
64        self
65    }
66
67    /// Extend these bounds by point.
68    pub fn extend_by_point(&mut self, p: Vec2) {
69        self.min.x = p.x.min(self.min.x);
70        self.min.y = p.y.min(self.min.y);
71        self.max.x = p.x.max(self.max.x);
72        self.max.y = p.y.max(self.max.y);
73    }
74}
75
76impl Default for Bounds2D {
77    fn default() -> Self {
78        // Bounds are invalid by default.
79        let min = Scalar::MAX;
80        let max = Scalar::MIN;
81        Self::new((min, min).into(), (max, max).into())
82    }
83}
84
85impl From<Rect> for Bounds2D {
86    fn from(rect: Rect) -> Self {
87        Self::new(rect.min().x_y().into(), rect.max().x_y().into())
88    }
89}
90
91impl From<Option<Rect>> for Bounds2D {
92    fn from(rect: Option<Rect>) -> Self {
93        match rect {
94            Some(rect) => rect.into(),
95            None => Bounds2D::default(),
96        }
97    }
98}
99
100impl From<Size2> for Bounds2D {
101    fn from(value: Size2) -> Self {
102        Self::new(Vec2::new(0.0, 0.0), Vec2::new(value.width, value.height))
103    }
104}
105
106impl std::fmt::Display for Bounds2D {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self.rect() {
109            Some(rect) => write!(
110                f,
111                "[{min:?}, {max:?}]",
112                min = rect.min().x_y(),
113                max = rect.max().x_y()
114            ),
115            None => write!(f, "[no bounds]"),
116        }
117    }
118}
119
120/// Trait to calculate a bounding box of 2D geometry.
121pub trait CalcBounds2D {
122    /// Fetch bounds.
123    fn calc_bounds_2d(&self) -> Bounds2D;
124}
125
126/// Holds bounds for a 3D object.
127#[derive(Clone, Default, Debug, Deref)]
128pub struct WithBounds2D<T: CalcBounds2D + Transformed2D> {
129    /// Bounds.
130    pub bounds: Bounds2D,
131    /// The inner object.
132    #[deref]
133    pub inner: T,
134}
135
136impl<T: CalcBounds2D + Transformed2D> WithBounds2D<T> {
137    /// Create a new object with bounds.
138    pub fn new(inner: T) -> Self {
139        Self {
140            bounds: inner.calc_bounds_2d(),
141            inner,
142        }
143    }
144
145    /// Update the bounds.
146    pub fn update_bounds(&mut self) {
147        self.bounds = self.inner.calc_bounds_2d()
148    }
149}
150
151impl<T: CalcBounds2D + Transformed2D> Transformed2D for WithBounds2D<T> {
152    fn transformed_2d(&self, mat: &Mat3) -> Self {
153        let inner = self.inner.transformed_2d(mat);
154        let bounds = inner.calc_bounds_2d();
155        Self { inner, bounds }
156    }
157}
158
159impl From<Geometry2D> for WithBounds2D<Geometry2D> {
160    fn from(geo: Geometry2D) -> Self {
161        Self::new(geo)
162    }
163}
164
165#[test]
166fn bounds_2d_test() {
167    let bounds1 = Bounds2D::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 3.0));
168    let bounds2 = Bounds2D::new(Vec2::new(4.0, 5.0), Vec2::new(6.0, 7.0));
169
170    let bounds1 = bounds1.extend(bounds2);
171
172    assert_eq!(bounds1.min, Vec2::new(0.0, 1.0));
173    assert_eq!(bounds1.max, Vec2::new(6.0, 7.0));
174}