brepkit_check/properties/
mod.rs1pub mod accumulator;
4pub mod analytic;
5pub mod bbox;
6pub mod face_integrator;
7
8pub use accumulator::GProps;
9
10use brepkit_math::aabb::Aabb3;
11use brepkit_math::vec::{Point3, Vec3};
12use brepkit_topology::Topology;
13use brepkit_topology::face::FaceId;
14use brepkit_topology::solid::SolidId;
15
16use crate::CheckError;
17
18#[derive(Debug, Clone)]
20pub struct PropertiesOptions {
21 pub gauss_order: usize,
23 pub adaptive_eps: f64,
25 pub max_depth: usize,
27}
28
29impl Default for PropertiesOptions {
30 fn default() -> Self {
31 Self {
32 gauss_order: 5,
33 adaptive_eps: 1e-6,
34 max_depth: 8,
35 }
36 }
37}
38
39pub fn bounding_box(topo: &Topology, solid: SolidId) -> Result<Aabb3, CheckError> {
45 bbox::bounding_box(topo, solid)
46}
47
48pub fn solid_volume(
57 topo: &Topology,
58 solid: SolidId,
59 options: &PropertiesOptions,
60) -> Result<f64, CheckError> {
61 let mut total_volume = 0.0;
62 for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
63 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
64 total_volume += contrib.volume;
65 }
66 Ok(total_volume)
67}
68
69pub fn solid_area(
77 topo: &Topology,
78 solid: SolidId,
79 options: &PropertiesOptions,
80) -> Result<f64, CheckError> {
81 let mut total_area = 0.0;
82 for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
83 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
84 total_area += contrib.area;
85 }
86 Ok(total_area)
87}
88
89pub fn center_of_mass(
100 topo: &Topology,
101 solid: SolidId,
102 options: &PropertiesOptions,
103) -> Result<Point3, CheckError> {
104 let mut total_volume = 0.0;
105 let mut mx = 0.0;
106 let mut my = 0.0;
107 let mut mz = 0.0;
108
109 for fid in brepkit_topology::explorer::solid_faces(topo, solid)? {
110 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
111 total_volume += contrib.volume;
112 mx += contrib.volume_moment_x;
113 my += contrib.volume_moment_y;
114 mz += contrib.volume_moment_z;
115 }
116
117 if total_volume.abs() < 1e-30 {
118 return Err(CheckError::IntegrationFailed(
119 "solid has zero volume".into(),
120 ));
121 }
122
123 Ok(Point3::new(
124 mx / total_volume,
125 my / total_volume,
126 mz / total_volume,
127 ))
128}
129
130pub fn axial_v_range(
142 topo: &Topology,
143 face_id: FaceId,
144 origin: Point3,
145 axis: Vec3,
146) -> Result<(f64, f64), CheckError> {
147 let face_data = topo.face(face_id)?;
148 let outer = topo.wire(face_data.outer_wire())?;
149
150 let mut v_min = f64::MAX;
151 let mut v_max = f64::MIN;
152
153 let inner_wires: Vec<_> = face_data
155 .inner_wires()
156 .iter()
157 .filter_map(|&wid| topo.wire(wid).ok())
158 .collect();
159
160 for wire in std::iter::once(outer).chain(inner_wires.iter().copied()) {
161 for oe in wire.edges() {
162 let edge = topo.edge(oe.edge())?;
163 for vid in [oe.oriented_start(edge), oe.oriented_end(edge)] {
164 let pt = topo.vertex(vid)?.point();
165 let to_pt = Vec3::new(
166 pt.x() - origin.x(),
167 pt.y() - origin.y(),
168 pt.z() - origin.z(),
169 );
170 let v = axis.dot(to_pt);
171 v_min = v_min.min(v);
172 v_max = v_max.max(v);
173 }
174 }
175 }
176
177 if v_min < v_max {
178 Ok((v_min, v_max))
179 } else {
180 Ok((-1.0, 1.0))
181 }
182}
183
184#[cfg(test)]
185#[allow(clippy::unwrap_used, clippy::expect_used)]
186mod tests {
187 use super::*;
188 use brepkit_math::vec::Point3;
189 use brepkit_topology::Topology;
190 use brepkit_topology::test_utils::make_unit_cube_manifold;
191
192 #[test]
193 fn gprops_accumulator_two_cubes() {
194 let a = analytic::box_props(1.0, 1.0, 1.0);
196 let mut b = analytic::box_props(1.0, 1.0, 1.0);
197 b.center = Point3::new(1.5, 0.5, 0.5);
199
200 let mut combined = a;
201 combined.add(&b);
202
203 assert!((combined.mass - 2.0).abs() < 1e-12);
205 assert!((combined.center.x() - 1.0).abs() < 1e-12);
207 assert!((combined.center.y() - 0.5).abs() < 1e-12);
208 assert!((combined.center.z() - 0.5).abs() < 1e-12);
209 }
210
211 #[test]
212 fn box_props_volume_and_com() {
213 let props = analytic::box_props(2.0, 3.0, 4.0);
214 assert!((props.mass - 24.0).abs() < 1e-12);
215 assert!((props.center.x() - 1.0).abs() < 1e-12);
216 assert!((props.center.y() - 1.5).abs() < 1e-12);
217 assert!((props.center.z() - 2.0).abs() < 1e-12);
218 assert!((props.inertia[0] - 50.0).abs() < 1e-12);
220 }
221
222 #[test]
223 fn sphere_props_volume() {
224 let props = analytic::sphere_props(1.0);
225 let expected = 4.0 / 3.0 * std::f64::consts::PI;
226 assert!((props.mass - expected).abs() < 1e-12);
227 assert!((props.center.x()).abs() < 1e-12);
228 assert!((props.center.y()).abs() < 1e-12);
229 assert!((props.center.z()).abs() < 1e-12);
230 }
231
232 #[test]
233 fn cylinder_props_volume_and_com() {
234 let props = analytic::cylinder_props(1.0, 2.0);
235 let expected_v = std::f64::consts::PI * 2.0;
236 assert!((props.mass - expected_v).abs() < 1e-12);
237 assert!((props.center.z() - 1.0).abs() < 1e-12);
238 }
239
240 #[test]
241 fn cone_full_volume() {
242 let props = analytic::cone_props(1.0, 0.0, 3.0);
243 let expected_v = std::f64::consts::PI * 3.0 / 3.0; assert!((props.mass - expected_v).abs() < 1e-12);
245 assert!((props.center.z() - 0.75).abs() < 1e-12);
247 }
248
249 #[test]
250 fn torus_props_volume() {
251 let props = analytic::torus_props(3.0, 1.0);
252 let expected_v = 2.0 * std::f64::consts::PI * std::f64::consts::PI * 3.0;
253 assert!((props.mass - expected_v).abs() < 1e-12);
254 }
255
256 #[test]
257 fn box_surface_area() {
258 let area = analytic::box_area(2.0, 3.0, 4.0);
259 assert!((area - 52.0).abs() < 1e-12);
261 }
262
263 #[test]
264 fn sphere_surface_area() {
265 let area = analytic::sphere_area(2.0);
266 let expected = 4.0 * std::f64::consts::PI * 4.0;
267 assert!((area - expected).abs() < 1e-12);
268 }
269
270 #[test]
271 fn inertia_matrix_symmetric() {
272 let mut props = GProps::new();
273 props.inertia = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0];
274 let mat = props.matrix_of_inertia();
275 assert!((mat[0][1] - mat[1][0]).abs() < 1e-15);
277 assert!((mat[0][2] - mat[2][0]).abs() < 1e-15);
278 assert!((mat[1][2] - mat[2][1]).abs() < 1e-15);
279 assert!((mat[0][0] - 10.0).abs() < 1e-15);
281 assert!((mat[1][1] - 20.0).abs() < 1e-15);
282 assert!((mat[2][2] - 30.0).abs() < 1e-15);
283 }
284
285 #[test]
286 fn bounding_box_unit_cube() {
287 let mut topo = Topology::new();
288 let solid = make_unit_cube_manifold(&mut topo);
289 let aabb = bounding_box(&topo, solid).unwrap();
290 assert!((aabb.min.x()).abs() < 1e-12);
292 assert!((aabb.min.y()).abs() < 1e-12);
293 assert!((aabb.min.z()).abs() < 1e-12);
294 assert!((aabb.max.x() - 1.0).abs() < 1e-12);
295 assert!((aabb.max.y() - 1.0).abs() < 1e-12);
296 assert!((aabb.max.z() - 1.0).abs() < 1e-12);
297 }
298
299 #[test]
300 fn gauss_volume_matches_analytic() {
301 let mut topo = Topology::new();
302 let solid = make_unit_cube_manifold(&mut topo);
303 let options = PropertiesOptions::default();
304 let vol = solid_volume(&topo, solid, &options).unwrap();
305 assert!((vol - 1.0).abs() < 1e-10, "expected volume 1.0, got {vol}");
307 }
308
309 #[test]
310 fn gauss_area_matches_analytic() {
311 let mut topo = Topology::new();
312 let solid = make_unit_cube_manifold(&mut topo);
313 let options = PropertiesOptions::default();
314 let area = solid_area(&topo, solid, &options).unwrap();
315 assert!((area - 6.0).abs() < 1e-10, "expected area 6.0, got {area}");
317 }
318
319 #[test]
320 fn gauss_com_matches_analytic() {
321 let mut topo = Topology::new();
322 let solid = make_unit_cube_manifold(&mut topo);
323 let options = PropertiesOptions::default();
324 let com = center_of_mass(&topo, solid, &options).unwrap();
325 assert!((com.x() - 0.5).abs() < 1e-10, "com.x = {}", com.x());
327 assert!((com.y() - 0.5).abs() < 1e-10, "com.y = {}", com.y());
328 assert!((com.z() - 0.5).abs() < 1e-10, "com.z = {}", com.z());
329 }
330
331 #[test]
332 fn accumulator_default_is_zero() {
333 let props = GProps::default();
334 assert!((props.mass).abs() < 1e-15);
335 assert!((props.center.x()).abs() < 1e-15);
336 assert!((props.center.y()).abs() < 1e-15);
337 assert!((props.center.z()).abs() < 1e-15);
338 for &c in &props.inertia {
339 assert!(c.abs() < 1e-15);
340 }
341 }
342}