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 solid_data = topo.solid(solid)?;
62 let shell = topo.shell(solid_data.outer_shell())?;
63
64 let mut total_volume = 0.0;
65 for &fid in shell.faces() {
66 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
67 total_volume += contrib.volume;
68 }
69 Ok(total_volume)
70}
71
72pub fn solid_area(
80 topo: &Topology,
81 solid: SolidId,
82 options: &PropertiesOptions,
83) -> Result<f64, CheckError> {
84 let solid_data = topo.solid(solid)?;
85 let shell = topo.shell(solid_data.outer_shell())?;
86
87 let mut total_area = 0.0;
88 for &fid in shell.faces() {
89 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
90 total_area += contrib.area;
91 }
92 Ok(total_area)
93}
94
95pub fn center_of_mass(
106 topo: &Topology,
107 solid: SolidId,
108 options: &PropertiesOptions,
109) -> Result<Point3, CheckError> {
110 let solid_data = topo.solid(solid)?;
111 let shell = topo.shell(solid_data.outer_shell())?;
112
113 let mut total_volume = 0.0;
114 let mut mx = 0.0;
115 let mut my = 0.0;
116 let mut mz = 0.0;
117
118 for &fid in shell.faces() {
119 let contrib = face_integrator::integrate_face(topo, fid, options.gauss_order)?;
120 total_volume += contrib.volume;
121 mx += contrib.volume_moment_x;
122 my += contrib.volume_moment_y;
123 mz += contrib.volume_moment_z;
124 }
125
126 if total_volume.abs() < 1e-30 {
127 return Err(CheckError::IntegrationFailed(
128 "solid has zero volume".into(),
129 ));
130 }
131
132 Ok(Point3::new(
133 mx / total_volume,
134 my / total_volume,
135 mz / total_volume,
136 ))
137}
138
139pub fn axial_v_range(
151 topo: &Topology,
152 face_id: FaceId,
153 origin: Point3,
154 axis: Vec3,
155) -> Result<(f64, f64), CheckError> {
156 let face_data = topo.face(face_id)?;
157 let outer = topo.wire(face_data.outer_wire())?;
158
159 let mut v_min = f64::MAX;
160 let mut v_max = f64::MIN;
161
162 let inner_wires: Vec<_> = face_data
164 .inner_wires()
165 .iter()
166 .filter_map(|&wid| topo.wire(wid).ok())
167 .collect();
168
169 for wire in std::iter::once(outer).chain(inner_wires.iter().copied()) {
170 for oe in wire.edges() {
171 let edge = topo.edge(oe.edge())?;
172 for vid in [oe.oriented_start(edge), oe.oriented_end(edge)] {
173 let pt = topo.vertex(vid)?.point();
174 let to_pt = Vec3::new(
175 pt.x() - origin.x(),
176 pt.y() - origin.y(),
177 pt.z() - origin.z(),
178 );
179 let v = axis.dot(to_pt);
180 v_min = v_min.min(v);
181 v_max = v_max.max(v);
182 }
183 }
184 }
185
186 if v_min < v_max {
187 Ok((v_min, v_max))
188 } else {
189 Ok((-1.0, 1.0))
190 }
191}
192
193#[cfg(test)]
194#[allow(clippy::unwrap_used, clippy::expect_used)]
195mod tests {
196 use super::*;
197 use brepkit_math::vec::Point3;
198 use brepkit_topology::Topology;
199 use brepkit_topology::test_utils::make_unit_cube_manifold;
200
201 #[test]
202 fn gprops_accumulator_two_cubes() {
203 let a = analytic::box_props(1.0, 1.0, 1.0);
205 let mut b = analytic::box_props(1.0, 1.0, 1.0);
206 b.center = Point3::new(1.5, 0.5, 0.5);
208
209 let mut combined = a;
210 combined.add(&b);
211
212 assert!((combined.mass - 2.0).abs() < 1e-12);
214 assert!((combined.center.x() - 1.0).abs() < 1e-12);
216 assert!((combined.center.y() - 0.5).abs() < 1e-12);
217 assert!((combined.center.z() - 0.5).abs() < 1e-12);
218 }
219
220 #[test]
221 fn box_props_volume_and_com() {
222 let props = analytic::box_props(2.0, 3.0, 4.0);
223 assert!((props.mass - 24.0).abs() < 1e-12);
224 assert!((props.center.x() - 1.0).abs() < 1e-12);
225 assert!((props.center.y() - 1.5).abs() < 1e-12);
226 assert!((props.center.z() - 2.0).abs() < 1e-12);
227 assert!((props.inertia[0] - 50.0).abs() < 1e-12);
229 }
230
231 #[test]
232 fn sphere_props_volume() {
233 let props = analytic::sphere_props(1.0);
234 let expected = 4.0 / 3.0 * std::f64::consts::PI;
235 assert!((props.mass - expected).abs() < 1e-12);
236 assert!((props.center.x()).abs() < 1e-12);
237 assert!((props.center.y()).abs() < 1e-12);
238 assert!((props.center.z()).abs() < 1e-12);
239 }
240
241 #[test]
242 fn cylinder_props_volume_and_com() {
243 let props = analytic::cylinder_props(1.0, 2.0);
244 let expected_v = std::f64::consts::PI * 2.0;
245 assert!((props.mass - expected_v).abs() < 1e-12);
246 assert!((props.center.z() - 1.0).abs() < 1e-12);
247 }
248
249 #[test]
250 fn cone_full_volume() {
251 let props = analytic::cone_props(1.0, 0.0, 3.0);
252 let expected_v = std::f64::consts::PI * 3.0 / 3.0; assert!((props.mass - expected_v).abs() < 1e-12);
254 assert!((props.center.z() - 0.75).abs() < 1e-12);
256 }
257
258 #[test]
259 fn torus_props_volume() {
260 let props = analytic::torus_props(3.0, 1.0);
261 let expected_v = 2.0 * std::f64::consts::PI * std::f64::consts::PI * 3.0;
262 assert!((props.mass - expected_v).abs() < 1e-12);
263 }
264
265 #[test]
266 fn box_surface_area() {
267 let area = analytic::box_area(2.0, 3.0, 4.0);
268 assert!((area - 52.0).abs() < 1e-12);
270 }
271
272 #[test]
273 fn sphere_surface_area() {
274 let area = analytic::sphere_area(2.0);
275 let expected = 4.0 * std::f64::consts::PI * 4.0;
276 assert!((area - expected).abs() < 1e-12);
277 }
278
279 #[test]
280 fn inertia_matrix_symmetric() {
281 let mut props = GProps::new();
282 props.inertia = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0];
283 let mat = props.matrix_of_inertia();
284 assert!((mat[0][1] - mat[1][0]).abs() < 1e-15);
286 assert!((mat[0][2] - mat[2][0]).abs() < 1e-15);
287 assert!((mat[1][2] - mat[2][1]).abs() < 1e-15);
288 assert!((mat[0][0] - 10.0).abs() < 1e-15);
290 assert!((mat[1][1] - 20.0).abs() < 1e-15);
291 assert!((mat[2][2] - 30.0).abs() < 1e-15);
292 }
293
294 #[test]
295 fn bounding_box_unit_cube() {
296 let mut topo = Topology::new();
297 let solid = make_unit_cube_manifold(&mut topo);
298 let aabb = bounding_box(&topo, solid).unwrap();
299 assert!((aabb.min.x()).abs() < 1e-12);
301 assert!((aabb.min.y()).abs() < 1e-12);
302 assert!((aabb.min.z()).abs() < 1e-12);
303 assert!((aabb.max.x() - 1.0).abs() < 1e-12);
304 assert!((aabb.max.y() - 1.0).abs() < 1e-12);
305 assert!((aabb.max.z() - 1.0).abs() < 1e-12);
306 }
307
308 #[test]
309 fn gauss_volume_matches_analytic() {
310 let mut topo = Topology::new();
311 let solid = make_unit_cube_manifold(&mut topo);
312 let options = PropertiesOptions::default();
313 let vol = solid_volume(&topo, solid, &options).unwrap();
314 assert!((vol - 1.0).abs() < 1e-10, "expected volume 1.0, got {vol}");
316 }
317
318 #[test]
319 fn gauss_area_matches_analytic() {
320 let mut topo = Topology::new();
321 let solid = make_unit_cube_manifold(&mut topo);
322 let options = PropertiesOptions::default();
323 let area = solid_area(&topo, solid, &options).unwrap();
324 assert!((area - 6.0).abs() < 1e-10, "expected area 6.0, got {area}");
326 }
327
328 #[test]
329 fn gauss_com_matches_analytic() {
330 let mut topo = Topology::new();
331 let solid = make_unit_cube_manifold(&mut topo);
332 let options = PropertiesOptions::default();
333 let com = center_of_mass(&topo, solid, &options).unwrap();
334 assert!((com.x() - 0.5).abs() < 1e-10, "com.x = {}", com.x());
336 assert!((com.y() - 0.5).abs() < 1e-10, "com.y = {}", com.y());
337 assert!((com.z() - 0.5).abs() < 1e-10, "com.z = {}", com.z());
338 }
339
340 #[test]
341 fn accumulator_default_is_zero() {
342 let props = GProps::default();
343 assert!((props.mass).abs() < 1e-15);
344 assert!((props.center.x()).abs() < 1e-15);
345 assert!((props.center.y()).abs() < 1e-15);
346 assert!((props.center.z()).abs() < 1e-15);
347 for &c in &props.inertia {
348 assert!(c.abs() < 1e-15);
349 }
350 }
351}