diff --git a/crates/difftest/src/solidcraft.rs b/crates/difftest/src/solidcraft.rs index dd404d4ad..32b6161fb 100644 --- a/crates/difftest/src/solidcraft.rs +++ b/crates/difftest/src/solidcraft.rs @@ -626,7 +626,7 @@ fn step_rust(k: &mut Kernel, bodies: &mut BTreeMap, step: &Step) - Ok(()) } -fn measure(k: &Kernel, b: &Brep) -> Result { +fn measure(k: &Kernel, b: &mut Brep) -> Result { let store = AttributeStore::default(); let attrs = PropertyAttributes { store: &store, @@ -650,7 +650,7 @@ pub fn run_rust(case: &Case) -> Run { return stop; } } - match bodies.get(RESULT) { + match bodies.get_mut(RESULT) { Some(b) => measure(&k, b).map_or_else(|e| solid(&e), Run::Done), None => Run::Error(format!("no body `{RESULT}`")), } diff --git a/crates/difftest/src/units/mass_integrator.rs b/crates/difftest/src/units/mass_integrator.rs index 5d4c468d5..904e87c6c 100644 --- a/crates/difftest/src/units/mass_integrator.rs +++ b/crates/difftest/src/units/mass_integrator.rs @@ -415,7 +415,7 @@ fn geometry_rust(g: &Geometry) -> Output { )?; let p = props::brep_properties( &wire_kernel, - &brep, + &mut brep, accuracy, Vec3::new(ox, oy, oz), props::MassPropertyBehavior { @@ -497,17 +497,20 @@ fn geometry_rust(g: &Geometry) -> Output { }; let accuracy = p[12]; return match *query { - 2 => Ok(vec![0.0, props::face_area(&fk, &brep, face, accuracy)?]), + 2 => Ok(vec![0.0, props::face_area(&fk, &mut brep, face, accuracy)?]), 3 => { - let (area, c) = props::face_centroid(&fk, &brep, face, accuracy)?; + let (area, c) = props::face_centroid(&fk, &mut brep, face, accuracy)?; Ok(vec![0.0, area, c.x, c.y, c.z]) } - 4 => Ok(vec![0.0, props::brep_area(&fk, &brep, accuracy)?]), - 5 => Ok(vec![0.0, props::brep_volume(&fk, &brep, accuracy, &attrs)?]), + 4 => Ok(vec![0.0, props::brep_area(&fk, &mut brep, accuracy)?]), + 5 => Ok(vec![ + 0.0, + props::brep_volume(&fk, &mut brep, accuracy, &attrs)?, + ]), 6 => { let m = props::brep_mass_properties( &fk, - &brep, + &mut brep, accuracy, 1.0, Vec3::new(p[9], p[10], p[11]), diff --git a/crates/solidcore/examples/primitives/checks.rs b/crates/solidcore/examples/primitives/checks.rs index e32e29e59..511c99bf2 100644 --- a/crates/solidcore/examples/primitives/checks.rs +++ b/crates/solidcore/examples/primitives/checks.rs @@ -289,7 +289,7 @@ impl Evaluation { } } -fn measure(k: &Kernel, b: &Brep) -> Measured { +fn measure(k: &Kernel, b: &mut Brep) -> Measured { let counts = (|| { let faces = bl::faces(b, None)?; let edges = bl::edges(b, None)?; @@ -311,22 +311,23 @@ fn measure(k: &Kernel, b: &Brep) -> Measured { tight: true, expand_by_zone_tol_3d: false, }; - let bbox = calculate_bounding_box(k, b, opts).map(|ext| { + let bbox = calculate_bounding_box(k, &mut *b, opts).map(|ext| { ( Point3::new(ext.min_at(0), ext.min_at(1), ext.min_at(2)), Point3::new(ext.max_at(0), ext.max_at(1), ext.max_at(2)), ) }); - let properties = calculate_bounding_box(k, b, BoundingBoxOptions::default()).and_then(|ext| { - brep_properties( - k, - b, - ACCURACY, - ext.mid(), - MassPropertyBehavior::default(), - &attributes, - ) - }); + let properties = + calculate_bounding_box(k, &mut *b, BoundingBoxOptions::default()).and_then(|ext| { + brep_properties( + k, + b, + ACCURACY, + ext.mid(), + MassPropertyBehavior::default(), + &attributes, + ) + }); Measured { counts, manifold_solid: is_manifold_solid(b), @@ -378,7 +379,7 @@ fn unmeasured(name: &'static str, expected: Value, e: &solidcore::SolidError) -> /// Builds `spec`, measures it and runs every check (none stops the others). pub fn evaluate(spec: &Spec) -> Evaluation { let mut k = Kernel::default(); - let b = match (spec.build)(&mut k) { + let mut b = match (spec.build)(&mut k) { Ok(b) => b, Err(e) => { return Evaluation { @@ -388,7 +389,7 @@ pub fn evaluate(spec: &Spec) -> Evaluation { } } }; - let m = measure(&k, &b); + let m = measure(&k, &mut b); let mut checks = Vec::new(); checks.push(match &m.manifold_solid { Ok(ok) => Check { diff --git a/crates/solidcore/src/mass/integrator/properties.rs b/crates/solidcore/src/mass/integrator/properties.rs index 6aacca5a2..8768a0f40 100644 --- a/crates/solidcore/src/mass/integrator/properties.rs +++ b/crates/solidcore/src/mass/integrator/properties.rs @@ -33,7 +33,7 @@ use crate::surface::surface::Surface; use crate::topology::face::p2::{ OdeArea, PrecisePropertyLayout, PPF_ALL, PPF_AREA, PPF_AREA_FIRST_MOMENTS, }; -use crate::topology::{edge, vertex, Brep, EdgeId, FaceId, RegionId, VertexId}; +use crate::topology::{edge, vertex, Brep, EdgeId, FaceId, FaceuseId, RegionId, VertexId}; use crate::{EntityRef, Result, SolidError}; const OP: &str = "mass::integrator::properties"; @@ -185,11 +185,9 @@ pub fn curve_properties( } /// `SmFace::CreateOrientedTrimBoundary(ctx, TRUE, TRUE, ..)` (unit `topology.face_p1`) on -/// a copy of the B-rep (the caller holds it shared; the UV trim curves the call makes are -/// not kept; the copy is a whole-B-rep clone per face). The copied curves' edge-use -/// owners are not read here. -fn boundary(k: &Kernel, b: &Brep, f: FaceId) -> Result> { - let mut copy = b.clone(); +/// the caller's B-rep: the UV trim curves it makes stay there, as upstream. The returned +/// curves' edge-use owners are not read here. +fn boundary(k: &Kernel, b: &mut Brep, f: FaceId) -> Result> { let key_of = |e: EntityRef| crate::geom::object::ObjectRef { type_code: 0, key: ObjectKey(match e { @@ -197,15 +195,14 @@ fn boundary(k: &Kernel, b: &Brep, f: FaceId) -> Result> { _ => 0, }), }; - crate::topology::face::p1::create_oriented_trim_boundary( - k, &mut copy, f, true, true, None, &key_of, - ) - .map(|t| t.curves) + crate::topology::face::p1::create_oriented_trim_boundary(k, b, f, true, true, None, &key_of) + .map(|t| t.curves) } -/// `SmFace::ComputePreciseProperties`; the default property mask is `PPF_ALL`. +/// `SmFace::ComputePreciseProperties`; the default property mask is `PPF_ALL`. Missing UV +/// trim curves of the face are made on `b`. pub fn face_properties( k: &Kernel, - b: &Brep, + b: &mut Brep, f: FaceId, orientation: OrientType, accuracy: f64, @@ -363,6 +360,16 @@ fn edges(b: &Brep) -> Result> { fn vertices(b: &Brep) -> Result> { crate::topology::brep::p1::vertices(b, None) } +/// The region on the side of face use `fu`. +fn faceuse_region(b: &Brep, fu: FaceuseId) -> Result { + let shell = b + .faceuse(fu)? + .shell + .ok_or_else(|| SolidError::null_pointer(OP))?; + b.shell(shell)? + .region + .ok_or_else(|| SolidError::null_pointer(OP)) +} /// `SmRegion::IsVoid` as a C truth value (`UNSURE`, 2, counts as void). fn region_void(b: &Brep, r: RegionId) -> Result { Ok(b.region(r)?.is_void() != 0) @@ -384,15 +391,16 @@ fn add(total: &mut PreciseProperties, delta: &PreciseProperties, density: f64) { } } /// `SmBrep::ComputePreciseProperties`, including density attributes and wire/point solids. +/// Missing UV trim curves of the integrated faces are made on `b`. pub fn brep_properties( k: &Kernel, - b: &Brep, + b: &mut Brep, accuracy: f64, origin: Point3, behavior: MassPropertyBehavior, attributes: &PropertyAttributes<'_>, ) -> Result { - let bbox = qbrep::calculate_bounding_box(k, b, BoundingBoxOptions::default())?; + let bbox = qbrep::calculate_bounding_box(k, &mut *b, BoundingBoxOptions::default())?; let size = bbox.size(); let estimated = size.z.max(size.x.max(size.y)); let estimated = estimated * estimated / 100.0; @@ -413,16 +421,7 @@ pub fn brep_properties( .faceuse(up)? .mate .ok_or_else(|| SolidError::null_pointer(OP))?; - let region = |fu| -> Result { - let shell = b - .faceuse(fu)? - .shell - .ok_or_else(|| SolidError::null_pointer(OP))?; - b.shell(shell)? - .region - .ok_or_else(|| SolidError::null_pointer(OP)) - }; - let mut separating = region(up)? != region(mate)?; + let mut separating = faceuse_region(b, up)? != faceuse_region(b, mate)?; for (j, fu) in [up, mate].into_iter().enumerate() { let mut do_area = false; let mut do_volume = false; @@ -438,7 +437,9 @@ pub fn brep_properties( .faceuse(fu)? .mate .ok_or_else(|| SolidError::null_pointer(OP))?; - if !region_void(b, region(fu)?)? && region_void(b, region(other)?)? { + if !region_void(b, faceuse_region(b, fu)?)? + && region_void(b, faceuse_region(b, other)?)? + { do_area = true; } } @@ -450,18 +451,19 @@ pub fn brep_properties( 2 => do_area = true, _ => {} } - if behavior.face_volume == 0 && !region_void(b, region(fu)?)? { + if behavior.face_volume == 0 && !region_void(b, faceuse_region(b, fu)?)? { do_volume = true; } if !(do_area || do_volume) { continue; } if separating || mass.y > EFF_ZERO { + let orientation = b.faceuse(fu)?.orientation; let delta = face_properties( k, b, f, - b.faceuse(fu)?.orientation, + orientation, accuracy, origin, estimated, @@ -538,7 +540,7 @@ fn area_estimate(bbox: &Extent3d) -> f64 { d * d / 100.0 } /// `SmApiFaceComputeArea`: clamps accuracy and integrates only area. -pub fn face_area(k: &Kernel, b: &Brep, f: FaceId, accuracy: f64) -> Result { +pub fn face_area(k: &Kernel, b: &mut Brep, f: FaceId, accuracy: f64) -> Result { let bbox = face_box(k, b, f)?; Ok(face_properties( k, @@ -555,7 +557,7 @@ pub fn face_area(k: &Kernel, b: &Brep, f: FaceId, accuracy: f64) -> Result .abs()) } /// `SmApiFaceComputeCentroid`: one area/first-moment integration. -pub fn face_centroid(k: &Kernel, b: &Brep, f: FaceId, accuracy: f64) -> Result<(f64, Point3)> { +pub fn face_centroid(k: &Kernel, b: &mut Brep, f: FaceId, accuracy: f64) -> Result<(f64, Point3)> { let bbox = face_box(k, b, f)?; let origin = bbox.mid(); let p = face_properties( @@ -578,7 +580,7 @@ pub fn face_centroid(k: &Kernel, b: &Brep, f: FaceId, accuracy: f64) -> Result<( )) } /// `SmApiBrepComputeArea`, including open shells and sheets. -pub fn brep_area(k: &Kernel, b: &Brep, accuracy: f64) -> Result { +pub fn brep_area(k: &Kernel, b: &mut Brep, accuracy: f64) -> Result { valid_api_number(accuracy)?; let mut area = 0.0; for f in faces(b)? { @@ -589,12 +591,12 @@ pub fn brep_area(k: &Kernel, b: &Brep, accuracy: f64) -> Result { /// `SmApiBrepComputeVolume`, computed about the bounding-box midpoint. pub fn brep_volume( k: &Kernel, - b: &Brep, + b: &mut Brep, accuracy: f64, attributes: &PropertyAttributes<'_>, ) -> Result { valid_api_number(accuracy)?; - let bbox = qbrep::calculate_bounding_box(k, b, BoundingBoxOptions::default())?; + let bbox = qbrep::calculate_bounding_box(k, &mut *b, BoundingBoxOptions::default())?; Ok(brep_properties( k, b, @@ -627,7 +629,7 @@ fn valid_api_number(value: f64) -> Result<()> { /// `SmApiBrepComputeMassProperties`, with upstream validation and accuracy clamping. pub fn brep_mass_properties( k: &Kernel, - b: &Brep, + b: &mut Brep, accuracy: f64, density: f64, origin: Point3, @@ -725,4 +727,24 @@ mod tests { ); } } + #[test] + fn face_area_makes_missing_uv_trims_on_the_callers_brep() { + // Upstream's `SmFace::ComputePreciseProperties` builds its oriented trim boundary on + // the face itself, so the UV trims it makes stay on the B-rep. + let mut k = Kernel::default(); + let mut b = + crate::primitive::api_primitives::create_box(&mut k, Point3::ZERO, 1.0, 1.0, 1.0) + .unwrap(); + let uses: Vec<_> = b.edgeuses().ids().collect(); + for u in uses { + b.edgeuse_mut(u).unwrap().uv_trim_curve = None; + } + let f = *faces(&b).unwrap().first().unwrap(); + let area = face_area(&k, &mut b, f, 1e-6).unwrap(); + assert!((area - 1.0).abs() <= 1e-9, "{area}"); + assert!( + b.edgeuses().iter().any(|(_, u)| u.uv_trim_curve.is_some()), + "the trims are made on the box itself" + ); + } } diff --git a/crates/solidcore/tests/primitives_solid.rs b/crates/solidcore/tests/primitives_solid.rs index d2f6c79ff..0a41a0647 100644 --- a/crates/solidcore/tests/primitives_solid.rs +++ b/crates/solidcore/tests/primitives_solid.rs @@ -41,7 +41,7 @@ fn precise_sphere_properties_scale_and_translate() { for (center, radius) in [(Vec3::ZERO, 2.0), (Vec3::new(1.0, -2.0, 0.5), 5.0)] { for no_pole in [false, true] { let mut k = Kernel::default(); - let b = if no_pole { + let mut b = if no_pole { create_sphere_no_pole_with_tolerance(&mut k, center, radius, 1e-9) } else { create_sphere(&mut k, center, radius) @@ -52,7 +52,7 @@ fn precise_sphere_properties_scale_and_translate() { let key_of = |_| ObjectKey(0); let properties = brep_properties( &k, - &b, + &mut b, 1e-10, bbox.mid(), MassPropertyBehavior::default(),