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    /// Calculate center of bounds.
36    pub fn center(&self) -> Vec2 {
37        (self.min + self.max) * 0.5
38    }
39
40    /// Return rect.
41    pub fn rect(&self) -> Option<Rect> {
42        if self.is_valid() {
43            Some(Rect::new(
44                coord! {x: self.min.x, y: self.min.y },
45                coord! {x: self.max.x, y: self.max.y },
46            ))
47        } else {
48            None
49        }
50    }
51
52    /// Enlarge bounds by a factor and return new bounds.
53    pub fn enlarge(&self, factor: Scalar) -> Self {
54        match self.rect() {
55            Some(rect) => {
56                let c = rect.center();
57                let s: geo::Coord = (rect.width(), rect.height()).into();
58                let s = s * 0.5 * (1.0 + factor);
59                Rect::new(c - s, c + s).into()
60            }
61            None => Bounds2D::default(),
62        }
63    }
64
65    /// Calculate extended bounds.
66    pub fn extend(mut self, other: Bounds2D) -> Self {
67        self.extend_by_point(other.min);
68        self.extend_by_point(other.max);
69        self
70    }
71
72    /// Extend these bounds by point.
73    pub fn extend_by_point(&mut self, p: Vec2) {
74        self.min.x = p.x.min(self.min.x);
75        self.min.y = p.y.min(self.min.y);
76        self.max.x = p.x.max(self.max.x);
77        self.max.y = p.y.max(self.max.y);
78    }
79
80    /// Return bounding radius.
81    pub fn radius(&self) -> Scalar {
82        use cgmath::InnerSpace;
83        (self.max - self.min).magnitude() * 0.5
84    }
85
86    /// Distance to boundary from the bounds' center.
87    pub fn distance_center_to_boundary(&self, dir: Vec2) -> Length {
88        let center = self.center();
89
90        // Handle x-axis intersections
91        let tx = if dir.x > 0.0 {
92            (self.max.x - center.x) / dir.x
93        } else if dir.x < 0.0 {
94            (self.min.x - center.x) / dir.x
95        } else {
96            f64::INFINITY
97        };
98
99        // Handle y-axis intersections
100        let ty = if dir.y > 0.0 {
101            (self.max.y - center.y) / dir.y
102        } else if dir.y < 0.0 {
103            (self.min.y - center.y) / dir.y
104        } else {
105            f64::INFINITY
106        };
107
108        // Return the smallest positive intersection
109        Length::mm(tx.min(ty))
110    }
111}
112
113impl Default for Bounds2D {
114    fn default() -> Self {
115        // Bounds are invalid by default.
116        let min = Scalar::MAX;
117        let max = Scalar::MIN;
118        Self::new((min, min).into(), (max, max).into())
119    }
120}
121
122impl From<Rect> for Bounds2D {
123    fn from(rect: Rect) -> Self {
124        Self::new(rect.min().x_y().into(), rect.max().x_y().into())
125    }
126}
127
128impl From<Option<Rect>> for Bounds2D {
129    fn from(rect: Option<Rect>) -> Self {
130        match rect {
131            Some(rect) => rect.into(),
132            None => Bounds2D::default(),
133        }
134    }
135}
136
137impl From<Size2> for Bounds2D {
138    fn from(value: Size2) -> Self {
139        Self::new(Vec2::new(0.0, 0.0), Vec2::new(value.width, value.height))
140    }
141}
142
143impl std::fmt::Display for Bounds2D {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        match self.rect() {
146            Some(rect) => write!(
147                f,
148                "[{min:?}, {max:?}]",
149                min = rect.min().x_y(),
150                max = rect.max().x_y()
151            ),
152            None => write!(f, "[no bounds]"),
153        }
154    }
155}
156
157/// Trait to calculate a bounding box of 2D geometry.
158pub trait CalcBounds2D {
159    /// Fetch bounds.
160    fn calc_bounds_2d(&self) -> Bounds2D;
161}
162
163/// Holds bounds for a 3D object.
164#[derive(Clone, Default, Debug, Deref)]
165pub struct WithBounds2D<T: CalcBounds2D + Transformed2D> {
166    /// Bounds.
167    pub bounds: Bounds2D,
168    /// The inner object.
169    #[deref]
170    pub inner: T,
171}
172
173impl<T: CalcBounds2D + Transformed2D> WithBounds2D<T> {
174    /// Create a new object with bounds.
175    pub fn new(inner: T) -> Self {
176        Self {
177            bounds: inner.calc_bounds_2d(),
178            inner,
179        }
180    }
181
182    /// Update the bounds.
183    pub fn update_bounds(&mut self) {
184        self.bounds = self.inner.calc_bounds_2d()
185    }
186}
187
188impl<T: CalcBounds2D + Transformed2D> Transformed2D for WithBounds2D<T> {
189    fn transformed_2d(&self, mat: &Mat3) -> Self {
190        let inner = self.inner.transformed_2d(mat);
191        let bounds = inner.calc_bounds_2d();
192        Self { inner, bounds }
193    }
194}
195
196impl From<Geometry2D> for WithBounds2D<Geometry2D> {
197    fn from(geo: Geometry2D) -> Self {
198        Self::new(geo)
199    }
200}
201
202#[test]
203fn bounds_2d_test() {
204    let bounds1 = Bounds2D::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 3.0));
205    let bounds2 = Bounds2D::new(Vec2::new(4.0, 5.0), Vec2::new(6.0, 7.0));
206
207    let bounds1 = bounds1.extend(bounds2);
208
209    assert_eq!(bounds1.min, Vec2::new(0.0, 1.0));
210    assert_eq!(bounds1.max, Vec2::new(6.0, 7.0));
211}