solidcraft-smlib: SMLib's own validity check, Brep::check

ValidatePointers, then AssertValid over the whole topology graph (the fork's
smlib_brep_check), at CheckLevel::Basic, Geometry or All. A body that fails is still Ok, with
the failed tests counted and one line each in the CheckReport; Err means the check could not
run. Tests run it on every SMLib result and import.

SMLIB_REV moves to the fork's check commit (39f398e), which also fixes a seam-curve leak that
AssertValid hit on every periodic face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruck
2026-10-21 00:52:33 -04:00
committed by jyviko
parent 30f5e617cf
commit 74912755ea
5 changed files with 126 additions and 1 deletions
+1 -1
View File
@@ -1 +1 @@
6fe0446f4956a4faec9ea022eeab408ffdca5b8c
39f398e99b8e1ee8de158cab75d57bd66d8cbff5
+85
View File
@@ -0,0 +1,85 @@
//! SMLib's own validity check: `SmBrep::ValidatePointers`, then `AssertValid` over the whole
//! topology graph. Tests run it on every SMLib result and every import.
use crate::{Brep, Error, Result, error, ffi};
/// How many of SMLib's tests to run.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckLevel {
/// The fewest: topology and pointers, no geometry.
Basic = 0,
/// Adds the geometry checks (gaps between vertices, edges and faces against tolerances).
Geometry = 1,
/// Every test, the slow ones too.
All = 2,
}
/// What SMLib's check found.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckReport {
/// Failed tests; 0 means SMLib finds the body valid.
pub failures: usize,
/// One line per failed test, such as
/// `SmBrep::Test #82 Tolerance: Vertex tolerance relative to brep tolerance (offset=2, tolerance=0.01) [owner SmVertex]`.
pub text: String,
}
impl CheckReport {
pub fn is_valid(&self) -> bool {
self.failures == 0
}
}
impl std::fmt::Display for CheckReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_valid() { f.write_str("valid") } else { write!(f, "{} failed tests:\n{}", self.failures, self.text) }
}
}
impl Brep {
/// Runs SMLib's validity check at `level`. A body that fails it is still `Ok`, with the
/// failures in the report; `Err` means the check could not run.
pub fn check(&self, level: CheckLevel) -> Result<CheckReport> {
let mut buf = vec![0u8; 1 << 16];
let (len, failures) = match ffi::check::check(&self.0, level as i32, &mut buf) {
Ok(r) => r,
// Too long for the first buffer: once more with the length it asked for.
Err((ffi::ERR_BUFFER, needed)) => {
buf = vec![0u8; needed.saturating_add(1)];
ffi::check::check(&self.0, level as i32, &mut buf).map_err(|(s, _)| error(s))?
}
Err((s, _)) => return Err(error(s)),
};
let text = String::from_utf8_lossy(buf.get(..len).ok_or(Error::Argument)?).into_owned();
Ok(CheckReport { failures: usize::try_from(failures).unwrap_or(0), text })
}
}
#[cfg(all(test, feature = "native"))]
mod tests {
use super::*;
#[test]
fn a_box_is_valid_at_every_level() {
let b = Brep::make_box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
for level in [CheckLevel::Basic, CheckLevel::Geometry, CheckLevel::All] {
let r = b.check(level).unwrap();
assert!(r.is_valid(), "{level:?}: {r}");
assert_eq!(r.text, "");
}
}
#[test]
fn a_copy_and_a_native_file_round_trip_stay_valid() {
let b = Brep::make_box([0.0; 3], [2.0, 3.0, 4.0]).unwrap();
assert!(b.try_clone().unwrap().check(CheckLevel::All).unwrap().is_valid());
let dir = std::env::temp_dir().join(format!("solidcraft-smlib-check-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("box.smb");
b.write(&path, false).unwrap();
let back = Brep::read(&path, false).unwrap();
let _ = std::fs::remove_dir_all(&dir);
let r = back.check(CheckLevel::All).unwrap();
assert!(r.is_valid(), "{r}");
}
}
+1
View File
@@ -179,6 +179,7 @@ mod calls {
pub use calls::*;
pub mod check;
pub mod history;
/// Where [`set_log`] sends SMLib's log lines.
+36
View File
@@ -0,0 +1,36 @@
//! `smlib_brep_check` (the fork's `capi/src/smlib_check.cpp`), declared by hand.
//! Part of the FFI boundary, like its parent: called only through [`crate::check`].
#![allow(unsafe_code)]
use super::Handle;
#[cfg(not(feature = "native"))]
use super::NOT_BUILT;
#[cfg(feature = "native")]
use super::{OK, RawBrep};
#[cfg(feature = "native")]
use std::ffi::c_char;
#[cfg(feature = "native")]
unsafe extern "C" {
pub fn smlib_brep_check(brep: *const RawBrep, level: i32, buf: *mut c_char, cap: i64, out_len: *mut i64, out_failures: *mut i64) -> i32;
}
/// Runs the check into `buf`: the report's length and the failure count, or a status
/// (`ERR_BUFFER` with the needed length when `buf` is too small).
#[cfg(feature = "native")]
pub fn check(b: &Handle, level: i32, buf: &mut [u8]) -> Result<(usize, i64), (i32, usize)> {
let cap = i64::try_from(buf.len()).unwrap_or(i64::MAX);
let (mut len, mut failures) = (0i64, 0i64);
let s = unsafe { smlib_brep_check(b.raw(), level, buf.as_mut_ptr().cast(), cap, &mut len, &mut failures) };
let len = usize::try_from(len).unwrap_or(0);
match s {
OK => Ok((len.min(buf.len()), failures)),
s => Err((s, len)),
}
}
#[cfg(not(feature = "native"))]
pub fn check(_: &Handle, _: i32, _: &mut [u8]) -> Result<(usize, i64), (i32, usize)> {
Err((NOT_BUILT, 0))
}
+3
View File
@@ -15,9 +15,12 @@ use std::path::Path;
use std::sync::Once;
use std::sync::atomic::{AtomicI32, Ordering};
mod check;
mod ffi;
pub mod history;
pub use check::{CheckLevel, CheckReport};
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {