mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
kernel: measure through SMLib's exact mass properties, checked against the mesh
With SOLIDCRAFT_KERNEL=smlib, kernel::measure and measure_mass take volume, area and centroid from SMLib's Brep::mass_properties (integrated on the exact surfaces, accuracy 1e-8). The mesh measure is still made, as an independent check. Where SMLib disagrees with it beyond the mesh's own accuracy (1e-3; tangency-labs/solidcore#384), or can't take the body, the mesh measure stands and the measurement says why. BodyMeasure and MassMeasure carry `measured_by` (smlib | mesh) and `notes`, and inspect.measure shows both. The box and topology counts stay the mesh's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,8 @@ pub fn measure_json(b: &solidcraft_doc::ModelBody) -> Value {
|
||||
"vertices": m.merged.vertices,
|
||||
"face_types": m.merged.face_types,
|
||||
"kernel": {"faces": m.faces, "edges": m.edges, "vertices": m.vertices, "shells": m.shells},
|
||||
"measured_by": m.measured_by,
|
||||
"notes": m.notes,
|
||||
}),
|
||||
Err(e) => json!({"name": b.name, "error": e.to_string()}),
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ mod loft_region_tests;
|
||||
mod loopblend;
|
||||
mod matchsurf;
|
||||
mod measure;
|
||||
mod measure_backend;
|
||||
mod mesh_convert;
|
||||
mod meshbody;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -20,6 +20,13 @@ pub struct BodyMeasure {
|
||||
/// Counts after merging split pieces of the same analytic surface/curve (comparable with
|
||||
/// systems that use periodic faces and edges).
|
||||
pub merged: TopoCounts,
|
||||
/// What volume, area and centre come from: `smlib` (exact, integrated on the surfaces, with
|
||||
/// the kernel setting SMLib) or `mesh` (a fine tessellation).
|
||||
pub measured_by: &'static str,
|
||||
/// Why a measurement is not what the kernel setting asks for (SMLib refused the body, or
|
||||
/// disagreed with the mesh).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Per-face sums: area, 6 × signed volume and 24 × first moments (divergence theorem, origin 0).
|
||||
@@ -57,12 +64,17 @@ fn min_edge_radius(m: &Mesh) -> Option<f64> {
|
||||
|
||||
/// Volume, area, centre and box of a body, as [`measure`] works them out, without the topology
|
||||
/// counts (and the tessellation they take).
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct MassMeasure {
|
||||
pub volume: f64,
|
||||
pub area: f64,
|
||||
pub centroid: Vec3,
|
||||
pub bbox: Aabb3,
|
||||
/// As [`BodyMeasure::measured_by`].
|
||||
pub measured_by: &'static str,
|
||||
/// As [`BodyMeasure::notes`].
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Measure a body from a fine tessellation (chord tolerance 5e-5 of the body size, finer for
|
||||
@@ -86,9 +98,12 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
vertices: pos.len(),
|
||||
shells: 1,
|
||||
merged,
|
||||
measured_by: "mesh",
|
||||
notes: Vec::new(),
|
||||
});
|
||||
}
|
||||
let (m, topo) = mass(b, true)?;
|
||||
let (mut m, topo) = mass(b, true)?;
|
||||
let chosen = crate::measure_backend::choose(b, &mut m);
|
||||
let topo = match topo {
|
||||
Some(t) => t,
|
||||
None => b.tessellate((b.size() * 1e-3).max(1e-3))?,
|
||||
@@ -103,6 +118,8 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
vertices: b.vertex_count(),
|
||||
shells: b.shell_count(),
|
||||
merged: merged_topology(b, &topo)?,
|
||||
measured_by: chosen.by,
|
||||
notes: chosen.notes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,9 +128,13 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
pub fn measure_mass(b: &Body) -> Result<MassMeasure> {
|
||||
if b.triangle_mesh().is_some() {
|
||||
let m = measure(b)?;
|
||||
return Ok(MassMeasure { volume: m.volume, area: m.area, centroid: m.centroid, bbox: m.bbox });
|
||||
return Ok(MassMeasure { volume: m.volume, area: m.area, centroid: m.centroid, bbox: m.bbox, measured_by: m.measured_by, notes: m.notes });
|
||||
}
|
||||
Ok(mass(b, false)?.0)
|
||||
let mut m = mass(b, false)?.0;
|
||||
let chosen = crate::measure_backend::choose(b, &mut m);
|
||||
m.measured_by = chosen.by;
|
||||
m.notes = chosen.notes;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// The mass properties of a B-rep body from three tessellations: a coarse one (which sets how
|
||||
@@ -170,7 +191,7 @@ fn mass(b: &Body, topology: bool) -> Result<(MassMeasure, Option<Mesh>)> {
|
||||
}
|
||||
let volume = v6 / 6.0;
|
||||
let centroid = if v6.abs() > 1e-12 { mom / (4.0 * v6) } else { fine.bounds().center() };
|
||||
Ok((MassMeasure { volume, area, centroid, bbox: coarse.bounds().union(&fine.bounds()) }, topo))
|
||||
Ok((MassMeasure { volume, area, centroid, bbox: coarse.bounds().union(&fine.bounds()), measured_by: "mesh", notes: Vec::new() }, topo))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Mass properties through SMLib, when [`crate::backend`] is SMLib: the body goes to SMLib as its
|
||||
//! schema 4 record and SMLib integrates its volume, area and centroid on the exact surfaces
|
||||
//! (`solidcraft_smlib::Brep::mass_properties`). The mesh measure is always made too, as an
|
||||
//! independent check: where SMLib disagrees with it beyond the mesh's own accuracy
|
||||
//! (tangency-labs/solidcore#384), or can't take the body, the mesh measure stands and the
|
||||
//! measurement says why in its notes.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::measure::MassMeasure;
|
||||
|
||||
/// The mesh measure's own accuracy (relative, for volume and area; of the body's size, for the
|
||||
/// centroid): SMLib must agree with it this closely for its numbers to be used.
|
||||
const AGREE: f64 = 1e-3;
|
||||
|
||||
/// Exact volume, area and centroid.
|
||||
pub(crate) struct Exact {
|
||||
pub volume: f64,
|
||||
pub area: f64,
|
||||
pub centroid: Vec3,
|
||||
}
|
||||
|
||||
/// What the measurement comes from, and what it says about it.
|
||||
pub(crate) struct Choice {
|
||||
pub by: &'static str,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// SMLib's mass properties of `b`, or why there are none.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn exact(b: &Body) -> Result<Exact, String> {
|
||||
let sb = crate::native::smlib_body(b).map_err(|e| e.to_string())?;
|
||||
let m = sb.mass_properties(solidcraft_smlib::MEASURE_ACCURACY).map_err(|e| format!("SMLib mass properties: {e}"))?;
|
||||
let [x, y, z] = m.centroid;
|
||||
Ok(Exact { volume: m.volume, area: m.area, centroid: Vec3::new(x, y, z) })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
fn exact(_: &Body) -> Result<Exact, String> {
|
||||
Err("this build has no SMLib".into())
|
||||
}
|
||||
|
||||
/// Where the kernel setting is SMLib, replaces `mesh`'s volume, area and centroid with SMLib's
|
||||
/// exact ones when they agree with it; otherwise keeps the mesh measure and notes why. The box
|
||||
/// stays the mesh's.
|
||||
pub(crate) fn choose(b: &Body, mesh: &mut MassMeasure) -> Choice {
|
||||
if !matches!(crate::backend(), Ok(crate::Backend::Smlib)) {
|
||||
return Choice { by: "mesh", notes: Vec::new() };
|
||||
}
|
||||
let e = match crate::guard("SMLib mass properties", || Ok(exact(b))) {
|
||||
Ok(Ok(e)) => e,
|
||||
Ok(Err(why)) | Err(crate::KernelError::Internal(why)) => {
|
||||
return Choice { by: "mesh", notes: vec![format!("measured from the mesh: SMLib could not measure this body ({why})")] };
|
||||
}
|
||||
Err(e) => return Choice { by: "mesh", notes: vec![format!("measured from the mesh: SMLib could not measure this body ({e})")] },
|
||||
};
|
||||
match disagreement(&e, mesh, b.size()) {
|
||||
None => {
|
||||
mesh.volume = e.volume;
|
||||
mesh.area = e.area;
|
||||
mesh.centroid = e.centroid;
|
||||
Choice { by: "smlib", notes: Vec::new() }
|
||||
}
|
||||
Some(what) => Choice {
|
||||
by: "mesh",
|
||||
notes: vec![format!("measured from the mesh: SMLib's exact mass properties disagree with it ({what}; tangency-labs/solidcore#384)")],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Where SMLib's numbers and the mesh's differ by more than the mesh's accuracy, which and by how
|
||||
/// much.
|
||||
fn disagreement(e: &Exact, m: &MassMeasure, size: f64) -> Option<String> {
|
||||
let rel = |a: f64, b: f64| (a - b).abs() / a.abs().max(b.abs()).max(1e-12);
|
||||
let finite = e.volume.is_finite() && e.area.is_finite() && [e.centroid.x, e.centroid.y, e.centroid.z].iter().all(|x| x.is_finite());
|
||||
if !finite {
|
||||
return Some("SMLib's numbers are not finite".into());
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
if rel(e.volume, m.volume) > AGREE {
|
||||
out.push(format!("volume {:.6} against {:.6}", e.volume, m.volume));
|
||||
}
|
||||
if rel(e.area, m.area) > AGREE {
|
||||
out.push(format!("area {:.6} against {:.6}", e.area, m.area));
|
||||
}
|
||||
let d = e.centroid.dist(m.centroid);
|
||||
if d > AGREE * size.max(1e-9) {
|
||||
out.push(format!("centroid {d:.6} mm apart"));
|
||||
}
|
||||
(!out.is_empty()).then(|| out.join(", "))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solidcraft_geom::Aabb3;
|
||||
|
||||
fn mesh(volume: f64, area: f64, centroid: Vec3) -> MassMeasure {
|
||||
MassMeasure { volume, area, centroid, bbox: Aabb3::EMPTY, measured_by: "mesh", notes: Vec::new() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agreement_is_within_the_mesh_accuracy() {
|
||||
let e = Exact { volume: 1000.0, area: 600.0, centroid: Vec3::new(5.0, 5.0, 5.0) };
|
||||
assert!(disagreement(&e, &mesh(1000.4, 600.2, Vec3::new(5.0, 5.0, 5.001)), 17.3).is_none());
|
||||
let off = disagreement(&e, &mesh(1015.0, 600.0, Vec3::new(5.0, 5.0, 5.0)), 17.3).unwrap();
|
||||
assert!(off.contains("volume 1000.000000 against 1015.000000"), "{off}");
|
||||
let moved = disagreement(&e, &mesh(1000.0, 600.0, Vec3::new(5.0, 5.0, 6.0)), 17.3).unwrap();
|
||||
assert!(moved.contains("centroid"), "{moved}");
|
||||
let nan = Exact { volume: f64::NAN, area: 600.0, centroid: Vec3::ZERO };
|
||||
assert!(disagreement(&nan, &mesh(1000.0, 600.0, Vec3::ZERO), 17.3).is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user