microcad_core/geo3d/
bounds.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use cgmath::{ElementWise, InnerSpace};
5use derive_more::Deref;
6
7use crate::*;
8
9/// Bounds3D type alias.
10pub type Bounds3D = Bounds<Vec3>;
11
12/// Corners iterator struct.
13pub struct Bounds3DCorners {
14    bounds: Bounds3D,
15    index: u8, // Only goes from 0 to 7
16}
17
18impl Iterator for Bounds3DCorners {
19    type Item = Vec3;
20
21    fn next(&mut self) -> Option<Self::Item> {
22        if self.index >= 8 {
23            return None;
24        }
25
26        let i = self.index;
27        self.index += 1;
28
29        let x = if i & 1 == 0 {
30            self.bounds.min.x
31        } else {
32            self.bounds.max.x
33        };
34        let y = if i & 2 == 0 {
35            self.bounds.min.y
36        } else {
37            self.bounds.max.y
38        };
39        let z = if i & 4 == 0 {
40            self.bounds.min.z
41        } else {
42            self.bounds.max.z
43        };
44
45        Some(Vec3 { x, y, z })
46    }
47}
48
49impl Bounds3D {
50    /// Calculate extended bounds.
51    pub fn extend(self, other: Bounds3D) -> Self {
52        match (self.is_valid(), other.is_valid()) {
53            (false, false) => Self::default(),
54            (false, true) => other,
55            (true, false) => self,
56            (true, true) => Self::new(
57                Vec3::new(
58                    self.min.x.min(other.min.x),
59                    self.min.y.min(other.min.y),
60                    self.min.z.min(other.min.z),
61                ),
62                Vec3::new(
63                    self.max.x.max(other.max.x),
64                    self.max.y.max(other.max.y),
65                    self.max.z.max(other.max.z),
66                ),
67            ),
68        }
69    }
70
71    /// Check if bounds are valid
72    pub fn is_valid(&self) -> bool {
73        self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z
74    }
75
76    /// Extend these bounds by point.
77    pub fn extend_by_point(&mut self, p: Vec3) {
78        self.min.x = p.x.min(self.min.x);
79        self.min.y = p.y.min(self.min.y);
80        self.min.z = p.z.min(self.min.z);
81        self.max.x = p.x.max(self.max.x);
82        self.max.y = p.y.max(self.max.y);
83        self.max.z = p.z.max(self.max.z);
84    }
85
86    /// Corner iterator.
87    pub fn corners(&self) -> Bounds3DCorners {
88        Bounds3DCorners {
89            bounds: self.clone(),
90            index: 0,
91        }
92    }
93
94    /// Maps a vec3 to bounds.
95    ///
96    /// The resulting `Vec3` is normalized between (0,0,0) = min  and (1,1,1) = max.
97    pub fn map_vec3(&self, v: Vec3) -> Vec3 {
98        (v - self.min).div_element_wise(self.max - self.min)
99    }
100
101    /// Return bounding radius.
102    pub fn radius(&self) -> Scalar {
103        (self.max - self.min).magnitude() * 0.5
104    }
105
106    /// Calculate center of the bounds.
107    pub fn center(&self) -> Vec3 {
108        (self.min + self.max) * 0.5
109    }
110
111    /// Distance to boundary from the bounds' center.
112    pub fn distance_center_to_boundary(&self, dir: Vec3) -> Length {
113        let center = self.center();
114
115        // Handle x-axis intersections
116        let tx = if dir.x > 0.0 {
117            (self.max.x - center.x) / dir.x
118        } else if dir.x < 0.0 {
119            (self.min.x - center.x) / dir.x
120        } else {
121            f64::INFINITY
122        };
123
124        // Handle y-axis intersections
125        let ty = if dir.y > 0.0 {
126            (self.max.y - center.y) / dir.y
127        } else if dir.y < 0.0 {
128            (self.min.y - center.y) / dir.y
129        } else {
130            f64::INFINITY
131        };
132
133        // Handle y-axis intersections
134        let tz = if dir.z > 0.0 {
135            (self.max.z - center.z) / dir.z
136        } else if dir.y < 0.0 {
137            (self.min.z - center.z) / dir.z
138        } else {
139            f64::INFINITY
140        };
141
142        // Return the smallest positive intersection
143        Length::mm(tx.min(ty).min(tz))
144    }
145}
146
147impl Default for Bounds3D {
148    fn default() -> Self {
149        // Bounds are invalid by default.
150        let min = Scalar::MAX;
151        let max = Scalar::MIN;
152        Self::new(Vec3::new(min, min, min), Vec3::new(max, max, max))
153    }
154}
155
156impl FromIterator<Vec3> for Bounds3D {
157    fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
158        let mut iter = iter.into_iter();
159        let first_point = match iter.next() {
160            Some(point) => point,
161            None => return Bounds3D::default(),
162        };
163
164        let mut min = first_point;
165        let mut max = first_point;
166
167        iter.for_each(|p| {
168            min.x = min.x.min(p.x);
169            min.y = min.y.min(p.y);
170            min.z = min.z.min(p.z);
171
172            max.x = max.x.max(p.x);
173            max.y = max.y.max(p.y);
174            max.z = max.z.max(p.z);
175        });
176
177        Bounds3D::new(min, max)
178    }
179}
180
181impl Transformed3D for Bounds3D {
182    fn transformed_3d(&self, mat: &Mat4) -> Self {
183        let mut bounds = Bounds3D::default();
184        self.corners()
185            .for_each(|corner| bounds.extend_by_point((mat * corner.extend(1.0)).truncate()));
186
187        bounds
188    }
189}
190
191/// Trait to calculate a bounding box of 3D geometry.
192pub trait CalcBounds3D {
193    /// Fetch bounds.
194    fn calc_bounds_3d(&self) -> Bounds3D;
195}
196
197/// Transformed version of a 3D geometry.
198pub trait Transformed3D<T = Self> {
199    /// Transform from matrix.
200    fn transformed_3d(&self, mat: &Mat4) -> T;
201}
202
203/// Holds bounds for a 3D object.
204#[derive(Clone, Default, Debug, Deref)]
205pub struct WithBounds3D<T: CalcBounds3D + Transformed3D> {
206    /// Bounds.
207    pub bounds: Bounds3D,
208    /// The inner object.
209    #[deref]
210    pub inner: T,
211}
212
213impl<T: CalcBounds3D + Transformed3D> WithBounds3D<T> {
214    /// Create a new object with bounds.
215    pub fn new(inner: T, bounds: Bounds3D) -> Self {
216        Self { bounds, inner }
217    }
218
219    /// Update the bounds.
220    pub fn update_bounds(&mut self) {
221        self.bounds = self.inner.calc_bounds_3d()
222    }
223}
224
225impl<T: CalcBounds3D + Transformed3D> Transformed3D for WithBounds3D<T> {
226    fn transformed_3d(&self, mat: &Mat4) -> Self {
227        let inner = self.inner.transformed_3d(mat);
228        let bounds = inner.calc_bounds_3d();
229        Self { inner, bounds }
230    }
231}
232
233impl From<Geometry3D> for WithBounds3D<Geometry3D> {
234    fn from(geo: Geometry3D) -> Self {
235        let bounds = geo.calc_bounds_3d();
236        Self::new(geo, bounds)
237    }
238}