mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
kernel: measure is SMLib's exact integrals; one kernel, no switch
A solid body's volume, area and centre are SMLib's mass properties at its documented 1e-4 and its box SMLib's exact one; a body SMLib cannot measure is an error. The three-mesh measure and the mesh cross-check beside SMLib's numbers are gone, and with them the kernel switch (SOLIDCRAFT_KERNEL, Backend): SMLib is the kernel. The smlib feature gates in the crate root, the validity check and the offset backend go, since SMLib's crate is always linked (answering not built without the feature). (not built: base in progress) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -1,51 +0,0 @@
|
||||
//! Which kernel this process runs its operations on: truck (the default) or SMLib.
|
||||
//!
|
||||
//! The setting is the environment variable `SOLIDCRAFT_KERNEL`, read once per process: `truck`
|
||||
//! (or unset) or `smlib`. Every operation that has an SMLib path asks [`backend`]. SMLib needs a
|
||||
//! build with the kernel's `smlib` feature; set without it, or set to anything else, every such
|
||||
//! operation fails with an error that says so rather than quietly running truck, so a comparison
|
||||
//! run can never compare truck with itself.
|
||||
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// The environment variable that picks the kernel.
|
||||
pub const KERNEL_ENV: &str = "SOLIDCRAFT_KERNEL";
|
||||
|
||||
/// A kernel operations run on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
Truck,
|
||||
Smlib,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// The backend a setting names; `None` (unset) is truck.
|
||||
pub fn parse(setting: Option<&str>) -> std::result::Result<Backend, String> {
|
||||
match setting.map(str::trim) {
|
||||
None | Some("") | Some("truck") => Ok(Backend::Truck),
|
||||
Some("smlib") if cfg!(feature = "smlib") => Ok(Backend::Smlib),
|
||||
Some("smlib") => Err(format!("{KERNEL_ENV}=smlib: not supported yet: this build has no SMLib (feature `smlib`)")),
|
||||
Some(other) => Err(format!("{KERNEL_ENV}={other}: expected `truck` or `smlib`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel this process runs on ([`KERNEL_ENV`], read on first use).
|
||||
pub fn backend() -> Result<Backend> {
|
||||
static SETTING: std::sync::OnceLock<std::result::Result<Backend, String>> = std::sync::OnceLock::new();
|
||||
SETTING.get_or_init(|| Backend::parse(std::env::var(KERNEL_ENV).ok().as_deref())).clone().map_err(KernelError::Invalid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_names_a_backend_or_is_refused() {
|
||||
assert_eq!(Backend::parse(None), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some("truck")), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some(" ")), Ok(Backend::Truck));
|
||||
assert_eq!(Backend::parse(Some("smlib")).is_ok(), cfg!(feature = "smlib"));
|
||||
assert!(Backend::parse(Some("occt")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod assemble;
|
||||
mod backend;
|
||||
mod blend;
|
||||
mod blend_corner;
|
||||
mod blend_fit;
|
||||
@@ -83,7 +82,6 @@ mod loft_rails_tests;
|
||||
mod loft_region_tests;
|
||||
mod matchsurf;
|
||||
mod measure;
|
||||
mod measure_backend;
|
||||
mod mesh_convert;
|
||||
mod meshbody;
|
||||
#[cfg(test)]
|
||||
@@ -115,7 +113,6 @@ pub mod provenance;
|
||||
mod record;
|
||||
mod seam_faces;
|
||||
mod sew;
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib_boolean;
|
||||
mod smlib_sweeps;
|
||||
#[cfg(test)]
|
||||
@@ -155,7 +152,6 @@ mod validity;
|
||||
pub mod watch;
|
||||
mod whole_face;
|
||||
|
||||
pub use backend::{Backend, KERNEL_ENV, backend};
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
@@ -189,7 +185,6 @@ pub use matchsurf::{match_surface, seam_continuity};
|
||||
pub use measure::{BodyMeasure, MassMeasure, measure, measure_mass};
|
||||
pub use mesh_convert::convert_prismatic_mesh;
|
||||
pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
||||
#[cfg(feature = "smlib")]
|
||||
pub use native::{body_from_smlib, smlib_body};
|
||||
pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh};
|
||||
pub use offset::with_same_surface;
|
||||
@@ -223,7 +218,7 @@ pub use sweep_path::{RailFit, SweepControl, SweepOrientation, sweep_path, sweep_
|
||||
pub use topo::{CylinderFace, cylinder_end_at, cylinder_face, cylinder_face_at};
|
||||
pub use topo::{TopoCounts, merged_topology, seam_flags};
|
||||
pub use tspline::tspline_sheet;
|
||||
pub use validity::{SmlibCheck, Validity, boolean_kernel, smlib_built, validity};
|
||||
pub use validity::{SmlibCheck, Validity, smlib_built, validity};
|
||||
pub use whole_face::{WholeFace, cut_takes_only_the_face, cut_tool, face_prism, matching_face};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
@@ -243,16 +238,9 @@ pub enum KernelError {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KernelError>;
|
||||
|
||||
/// The native kernel call running right now (SMLib, with the `smlib` feature), for crash reports.
|
||||
/// The native kernel call running right now (SMLib), for crash reports.
|
||||
pub fn native_call_in_flight() -> Option<String> {
|
||||
#[cfg(feature = "smlib")]
|
||||
{
|
||||
solidcraft_smlib::current_call()
|
||||
}
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
solidcraft_smlib::current_call()
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
||||
+26
-135
@@ -1,5 +1,5 @@
|
||||
use serde::Serialize;
|
||||
use solidcraft_geom::{Aabb3, Mesh, Vec3};
|
||||
use solidcraft_geom::{Aabb3, Vec3};
|
||||
|
||||
use crate::Result;
|
||||
use crate::body::Body;
|
||||
@@ -20,56 +20,16 @@ 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).
|
||||
/// What volume, area and centre come from: `smlib` (exact, integrated on the surfaces) or
|
||||
/// `mesh` (a mesh body's own triangles).
|
||||
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).
|
||||
/// Anything the measurement says about itself.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Per-face sums: area, 6 × signed volume and 24 × first moments (divergence theorem), taken
|
||||
/// about `o` (the moments relative to it), and the sum of the area vectors (zero over a closed
|
||||
/// surface). Over a closed surface the volume does not depend on `o`; over one that leaves a
|
||||
/// gap, the gap is closed by a cone to `o`, so `o` near the body keeps that cone small.
|
||||
fn face_sums(m: &Mesh, nf: usize, o: Vec3) -> Vec<(f64, f64, Vec3, Vec3)> {
|
||||
let mut out = vec![(0.0, 0.0, Vec3::ZERO, Vec3::ZERO); nf];
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let (Some([a, b, c]), Some(slot)) = (m.tri(t), out.get_mut(*f as usize)) else { continue };
|
||||
let n = (b - a).cross(c - a) * 0.5;
|
||||
let (a, b, c) = (a - o, b - o, c - o);
|
||||
let d = a.dot(b.cross(c));
|
||||
slot.0 += n.len();
|
||||
slot.1 += d;
|
||||
slot.2 += (a + b + c) * d;
|
||||
slot.3 += n;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Smallest radius of curvature along the mesh's edge polylines (straight edges ignored).
|
||||
fn min_edge_radius(m: &Mesh) -> Option<f64> {
|
||||
let mut best: Option<f64> = None;
|
||||
for e in &m.edges {
|
||||
for w in e.windows(3) {
|
||||
let (Some(&a), Some(&b), Some(&c)) = (w.first(), w.get(1), w.get(2)) else { continue };
|
||||
let (ab, bc, ca) = (b - a, c - b, a - c);
|
||||
let cross = ab.cross(bc).len();
|
||||
if !(cross > 1e-12 * ab.len() * bc.len()) {
|
||||
continue;
|
||||
}
|
||||
let r = ab.len() * bc.len() * ca.len() / (2.0 * cross);
|
||||
if r.is_finite() && r > 0.0 && best.is_none_or(|x| r < x) {
|
||||
best = Some(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Volume, area, centre and box of a body, as [`measure`] works them out, without the topology
|
||||
/// counts (and the tessellation they take).
|
||||
/// counts.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct MassMeasure {
|
||||
pub volume: f64,
|
||||
@@ -83,9 +43,14 @@ pub struct MassMeasure {
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Measure a body from a fine tessellation (chord tolerance 5e-5 of the body size, finer for
|
||||
/// small radii down to 5e-6). Faces whose fine tessellation disagrees with a coarse one (a
|
||||
/// meshing failure) use the coarse result.
|
||||
/// The relative accuracy SMLib integrates at: the finest of SmApiBrepComputeMassProperties'
|
||||
/// documented range ("clamped to [1e-4, 1e-1]", SmApiQueries.h). Its cost is "governed by
|
||||
/// dRelativeAccuracy".
|
||||
const ACCURACY: f64 = 1e-4;
|
||||
|
||||
/// Measure a body. A solid body's volume, area and centre are SMLib's, integrated on its exact
|
||||
/// surfaces, and its box is SMLib's exact box; a body SMLib cannot measure is an error. A mesh
|
||||
/// body is measured from its triangles, exactly.
|
||||
pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
if let Some((pos, tris)) = b.triangle_mesh() {
|
||||
// Exact for a mesh body; topology counts are facets, sides and vertices.
|
||||
@@ -108,12 +73,8 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
notes: Vec::new(),
|
||||
});
|
||||
}
|
||||
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.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?,
|
||||
};
|
||||
let m = measure_mass(b)?;
|
||||
let topo = b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?;
|
||||
Ok(BodyMeasure {
|
||||
volume: m.volume,
|
||||
area: m.area,
|
||||
@@ -124,93 +85,23 @@ 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,
|
||||
measured_by: m.measured_by,
|
||||
notes: m.notes,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`measure`]'s volume, area, centre and box alone: the same numbers, a tessellation fewer
|
||||
/// (for callers that need no topology counts, such as an interference check).
|
||||
/// [`measure`]'s volume, area, centre and box alone (for callers that need no topology counts,
|
||||
/// such as an interference check).
|
||||
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, measured_by: m.measured_by, notes: m.notes });
|
||||
}
|
||||
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
|
||||
/// fine the others go), then a fine and a medium one at once on desktop (one after the other
|
||||
/// in the browser). With `topology`, also the coarse unrefined mesh the topology counts take,
|
||||
/// made alongside them.
|
||||
fn mass(b: &Body, topology: bool) -> Result<(MassMeasure, Option<Mesh>)> {
|
||||
let size = b.size();
|
||||
let coarse_tol = (size * 1e-3).max(1e-3);
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(coarse_tol))?;
|
||||
// Chord error relative to a radius sets the volume error: keep it near 3e-4 of the
|
||||
// smallest radius (thin tubes, small fillets).
|
||||
let tight = min_edge_radius(&coarse).map(|r| r * 3e-4).unwrap_or(f64::INFINITY);
|
||||
let fine_tol = (size * 5e-5).min(tight).max(size * 5e-6).max(1e-4);
|
||||
let mut jobs = vec![(fine_tol, true), ((fine_tol * 5.0).max(2e-4), true)];
|
||||
if topology {
|
||||
jobs.push((coarse_tol, false));
|
||||
}
|
||||
// Independent tessellations of one body: each on its own thread, the calling thread's
|
||||
// profile counting their wall time once as tessellation. Not from inside a worker pool: a
|
||||
// pool thread blocked on threads of its own could leave their meshing (which runs on the
|
||||
// shared pool) without a worker.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let threads = if rayon::current_thread_index().is_none() { jobs.len() } else { 1 };
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let threads = 1;
|
||||
let made = crate::profile::timed(crate::profile::Kind::Tessellate, || {
|
||||
crate::par::map_on_threads(&jobs, threads, |&(tol, refine)| b.mesh(&crate::MeshTolerance::chord(tol)))
|
||||
});
|
||||
let mut made = made.into_iter().map(|m| m.unwrap_or_else(|| Err(crate::KernelError::Internal("tessellation failed".into()))));
|
||||
let missing = || crate::KernelError::Internal("tessellation missing".into());
|
||||
let fine = made.next().ok_or_else(missing)??;
|
||||
let medium = made.next().ok_or_else(missing)??;
|
||||
let topo = match made.next() {
|
||||
Some(m) => Some(m?),
|
||||
None => None,
|
||||
};
|
||||
let nf = b.face_count();
|
||||
// About the box's centre when the meshes leave only a crack: see `face_sums`. A body that is
|
||||
// open on purpose (a surface, or a cup whose lid is still a separate sheet) has no volume of
|
||||
// its own; it keeps the origin, so such bodies still compare with each other when the centre
|
||||
// of their boxes moves (a crowned lid rises, the open cup under it doesn't change).
|
||||
let (surface, gap) = face_sums(&fine, nf, Vec3::ZERO).iter().fold((0.0, Vec3::ZERO), |s, f| (s.0 + f.0, s.1 + f.3));
|
||||
let o = Some(fine.bounds().center()).filter(|c| c.is_finite() && gap.len() <= 0.01 * surface).unwrap_or(Vec3::ZERO);
|
||||
let (sf, sm, sc) = (face_sums(&fine, nf, o), face_sums(&medium, nf, o), face_sums(&coarse, nf, o));
|
||||
let close = |x: f64, y: f64| (x - y).abs() <= 0.01 * x.abs().max(y.abs()) + 1e-9;
|
||||
let (mut area, mut v6, mut mom) = (0.0, 0.0, Vec3::ZERO);
|
||||
for ((f, m), c) in sf.iter().zip(&sm).zip(&sc) {
|
||||
// Trust the finest tessellation that agrees with the next coarser one.
|
||||
let pick = if close(f.0, m.0) {
|
||||
f
|
||||
} else if close(m.0, c.0) {
|
||||
m
|
||||
} else {
|
||||
f
|
||||
};
|
||||
area += pick.0;
|
||||
v6 += pick.1;
|
||||
mom += pick.2;
|
||||
}
|
||||
// The moments back about the origin. A gap the meshes leave (a crack where a trimmed face
|
||||
// meets a hole cut into it) is closed by a cone to the box's centre, which keeps it small
|
||||
// (a crack of 0.3 mm² moved a part ring's volume by 7 % about the origin 1000 mm away; about
|
||||
// the centre, by 0.1 %). Every body is measured by the same meshes this way, so a body and the
|
||||
// pieces a boolean cuts it into add up: a closed display plan measured instead would mesh
|
||||
// some faces more coarsely than the sums do and break that.
|
||||
let mom = o * (4.0 * v6) + mom;
|
||||
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()), measured_by: "mesh", notes: Vec::new() }, topo))
|
||||
let m = crate::guard("SMLib mass properties", || {
|
||||
b.smlib()?.mass_properties(ACCURACY).map_err(|e| crate::KernelError::Failed(format!("SMLib mass properties: {e}")))
|
||||
})?;
|
||||
let [x, y, z] = m.centroid;
|
||||
Ok(MassMeasure { volume: m.volume, area: m.area, centroid: Vec3::new(x, y, z), bbox: b.exact_bounds()?, measured_by: "smlib", notes: Vec::new() })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -218,8 +109,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{BoolOp, boolean, cylinder, torus};
|
||||
|
||||
/// The mass properties alone are [`measure`]'s, to the bit, with the tessellations made at
|
||||
/// once; and on a curved intersection they are the textbook ones.
|
||||
/// The mass properties alone are [`measure`]'s, to the bit; and on a curved intersection
|
||||
/// they are the textbook ones.
|
||||
#[test]
|
||||
fn mass_alone_is_the_full_measure() {
|
||||
let ring = torus(Vec3::ZERO, 10.0, 2.0).unwrap();
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
//! 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;
|
||||
|
||||
/// The relative accuracy SMLib integrates at: the finest of SmApiBrepComputeMassProperties'
|
||||
/// documented range ("clamped to [1e-4, 1e-1]", SmApiQueries.h), ten times inside the 1e-3 the
|
||||
/// measure is held to ([`AGREE`], and the regression rig's volume and area tolerance). Its cost is
|
||||
/// "governed by dRelativeAccuracy": the finest the integrator allows, 1e-8, took tens of seconds
|
||||
/// on a modelled thread's faces.
|
||||
const ACCURACY: f64 = 1e-4;
|
||||
|
||||
/// 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(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());
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
//! ([`FaceOrigin::Offset`]), and its edges as where its faces meet. A face SMLib makes new along
|
||||
//! a sheet's open edge (a thickened sheet's wall) is recorded as made in place of that edge.
|
||||
|
||||
#![cfg(feature = "smlib")]
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Is a body one valid solid? The kernel regression rig (`tests/kernel-cases`) asks this of every
|
||||
//! result: the schema 4 record's structure and geometry (`Brep::validate`, `check_geometry`),
|
||||
//! and, with the `smlib` feature, SMLib's own check of the same record.
|
||||
//! and SMLib's own check of the same record.
|
||||
|
||||
use crate::{Body, brep_record};
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{Body, brep_record};
|
||||
pub struct Validity {
|
||||
/// The record: structure, then geometry against topology within tolerances.
|
||||
pub record: Result<(), String>,
|
||||
/// SMLib's `check(All)` of the record; `None` in a build without SMLib.
|
||||
/// SMLib's `check(All)` of the record.
|
||||
pub smlib: Option<SmlibCheck>,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ pub fn validity(body: &Body) -> Validity {
|
||||
Validity { record: checked, smlib: smlib_validity(body) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
|
||||
let b = match crate::smlib_body(body) {
|
||||
Ok(b) => b,
|
||||
@@ -55,23 +54,8 @@ fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
fn smlib_validity(_: &Body) -> Option<SmlibCheck> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether this build links SMLib (the `smlib` feature).
|
||||
pub fn smlib_built() -> bool {
|
||||
cfg!(feature = "smlib")
|
||||
}
|
||||
|
||||
/// The kernel this process runs on ([`crate::backend`]): `"default"` (truck) or `"smlib"`, and
|
||||
/// `"invalid"` where [`crate::KERNEL_ENV`] names neither or names SMLib in a build without it.
|
||||
/// The regression rig refuses to grade a kernel the process isn't running.
|
||||
pub fn boolean_kernel() -> &'static str {
|
||||
match crate::backend() {
|
||||
Ok(crate::Backend::Truck) => "default",
|
||||
Ok(crate::Backend::Smlib) => "smlib",
|
||||
Err(_) => "invalid",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user