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 geo::{AffineOps, AffineTransform, CoordsIter, coord};
7
8use crate::{Scalar, Size2, Transformed2D, Vec2, geo2d::Rect, mat3_to_affine_transform};
9
10/// 2D bounds, essentially an optional bounding rect.
11#[derive(Debug, Default, Clone)]
12pub struct Bounds2D(Option<Rect>);
13
14impl Bounds2D {
15    /// Create new 2D bounds.
16    pub fn new(min: Vec2, max: Vec2) -> Self {
17        let min_x = min.x.min(max.x);
18        let min_y = min.y.min(max.y);
19        let max_x = min.x.max(max.x);
20        let max_y = min.y.max(max.y);
21
22        Self(Some(Rect::new(
23            coord! { x: min_x, y: min_y},
24            coord! { x: max_x, y: max_y},
25        )))
26    }
27
28    /// Check if bounds are valid.
29    pub fn is_valid(&self) -> bool {
30        self.0.is_some()
31    }
32
33    /// Minimum corner.
34    pub fn min(&self) -> Option<Vec2> {
35        self.0.as_ref().map(|s| Vec2::new(s.min().x, s.min().y))
36    }
37
38    /// Maximum corner.
39    pub fn max(&self) -> Option<Vec2> {
40        self.0.as_ref().map(|s| Vec2::new(s.max().x, s.max().y))
41    }
42
43    /// Calculate width of these bounds or 0 if bounds are invalid.
44    pub fn width(&self) -> Scalar {
45        self.0.as_ref().map(|r| r.width()).unwrap_or_default()
46    }
47
48    /// Calculate height of these bounds or 0 if bounds are invalid.
49    pub fn height(&self) -> Scalar {
50        self.0.as_ref().map(|r| r.height()).unwrap_or_default()
51    }
52
53    /// Return rect.
54    pub fn rect(&self) -> &Option<Rect> {
55        &self.0
56    }
57
58    /// Enlarge bounds by a factor and return new bounds
59    pub fn enlarge(&self, factor: Scalar) -> Self {
60        Self(self.0.map(|rect| {
61            let c = rect.center();
62            let s: geo::Coord = (rect.width(), rect.height()).into();
63            let s = s * 0.5 * (1.0 + factor);
64            Rect::new(c - s, c + s)
65        }))
66    }
67
68    /// Calculate extended bounds.
69    pub fn extend(self, other: Bounds2D) -> Self {
70        match (self.0, other.0) {
71            (None, None) => Self(None),
72            (None, Some(r)) | (Some(r), None) => Self(Some(r)),
73            (Some(rect1), Some(rect2)) => Self::new(
74                Vec2::new(
75                    rect1.min().x.min(rect2.min().x),
76                    rect1.min().y.min(rect2.min().y),
77                ),
78                Vec2::new(
79                    rect1.max().x.max(rect2.max().x),
80                    rect1.max().y.max(rect2.max().y),
81                ),
82            ),
83        }
84    }
85
86    /// Extend these bounds by point.
87    pub fn extend_by_point(&mut self, p: Vec2) {
88        match &mut self.0 {
89            Some(rect) => {
90                *rect = Rect::new(
91                    coord! {
92                        x: rect.min().x.min(p.x),
93                        y: rect.min().y.min(p.y),
94                    },
95                    coord! {
96                        x: rect.max().x.max(p.x),
97                        y: rect.max().y.max(p.y),
98                    },
99                )
100            }
101            None => *self = Self::new(p, p),
102        }
103    }
104}
105
106impl AffineOps<Scalar> for Bounds2D {
107    fn affine_transform(&self, transform: &AffineTransform<Scalar>) -> Self {
108        match &self.0 {
109            Some(rect) => rect
110                .coords_iter()
111                .fold(Bounds2D::default(), |mut bounds, p| {
112                    let p = transform.apply(p);
113                    bounds.extend_by_point(Vec2::new(p.x, p.y));
114                    bounds
115                }),
116            None => Self(None),
117        }
118    }
119
120    fn affine_transform_mut(&mut self, transform: &AffineTransform<Scalar>) {
121        if let Some(rect) = &mut self.0 {
122            rect.affine_transform_mut(transform)
123        }
124    }
125}
126
127impl Transformed2D for Bounds2D {
128    fn transformed_2d(&self, _: &crate::RenderResolution, mat: &crate::Mat3) -> Self {
129        self.affine_transform(&mat3_to_affine_transform(mat))
130    }
131}
132
133impl From<Option<Rect>> for Bounds2D {
134    fn from(rect: Option<Rect>) -> Self {
135        match rect {
136            Some(rect) => Self::new(rect.min().x_y().into(), rect.max().x_y().into()),
137            None => Self(None),
138        }
139    }
140}
141
142impl From<Size2> for Bounds2D {
143    fn from(value: Size2) -> Self {
144        Self::new(Vec2::new(0.0, 0.0), Vec2::new(value.width, value.height))
145    }
146}
147
148impl std::fmt::Display for Bounds2D {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self.0 {
151            Some(rect) => write!(
152                f,
153                "[{min:?}, {max:?}]",
154                min = rect.min().x_y(),
155                max = rect.max().x_y()
156            ),
157            None => write!(f, "[no bounds]"),
158        }
159    }
160}
161
162/// Trait to return a bounding box of 2D geometry.
163pub trait FetchBounds2D {
164    /// Fetch bounds.
165    fn fetch_bounds_2d(&self) -> Bounds2D;
166}
167
168#[test]
169fn bounds_2d_test() {
170    let bounds1 = Bounds2D::new(Vec2::new(0.0, 1.0), Vec2::new(2.0, 3.0));
171    let bounds2 = Bounds2D::new(Vec2::new(4.0, 5.0), Vec2::new(6.0, 7.0));
172
173    let bounds1 = bounds1.extend(bounds2);
174
175    assert_eq!(bounds1.min(), Some(Vec2::new(0.0, 1.0)));
176    assert_eq!(bounds1.max(), Some(Vec2::new(6.0, 7.0)));
177}