solidcraft-smlib: offset faces, thicken, and NoRoom for walls with no room

Brep::offset_faces and Brep::thicken reach the fork's new smlib_offset_faces and
smlib_thicken; SMLIB_REV moves to the fork commit that adds them. The shim now refuses an
inward wall that leaves no room inside the body with its own status, Error::NoRoom, instead of
an argument error, so callers can say what is wrong; the test that a wall thicker than the box
fails now expects exactly that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 07:35:00 -04:00
parent a6ca4e0dd4
commit 51211ad1fe
5 changed files with 54 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
14996818bedacc057cf35f296b29876df022faa7
1f4d4a173e9fae1f149d5457b2e1cb0a8d62d005
+25 -1
View File
@@ -1,5 +1,6 @@
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_fillet`,
//! `smlib_chamfer`, `smlib_shell`, `smlib_brep_solid_count`; the fork's `capi/src/smlib_ops.cpp`),
//! `smlib_chamfer`, `smlib_shell`, `smlib_offset_faces`, `smlib_thicken`,
//! `smlib_brep_solid_count`; the fork's `capi/src/smlib_ops.cpp`),
//! declared by hand. Part of the FFI boundary, like its parent: called only through
//! [`crate::ops`].
@@ -14,6 +15,7 @@ use super::{ERR_ARG, OK, RawBrep, new_brep};
pub const ERR_NOT_SOLID: i32 = -1100;
pub const ERR_PICK_NONE: i32 = -1101;
pub const ERR_PICK_AMBIGUOUS: i32 = -1102;
pub const ERR_NO_ROOM: i32 = -1103;
pub const BOOLEAN_UNION: i32 = 0;
pub const BOOLEAN_SUBTRACT: i32 = 1;
@@ -25,6 +27,7 @@ pub enum Pick {
Fillet,
Chamfer,
Shell,
OffsetFaces,
}
/// A failed selection operation: the status, and the index of the point that selected nothing
@@ -65,6 +68,16 @@ unsafe extern "C" {
out: *mut *mut RawBrep,
out_failed_point: *mut i64,
) -> i32;
fn smlib_offset_faces(
brep: *const RawBrep,
points: *const f64,
n_points: i64,
tolerance: f64,
distance: f64,
out: *mut *mut RawBrep,
out_failed_point: *mut i64,
) -> i32;
fn smlib_thicken(brep: *const RawBrep, thickness: f64, symmetric: i32, out: *mut *mut RawBrep) -> i32;
}
#[cfg(feature = "native")]
@@ -89,6 +102,7 @@ pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size
Pick::Fillet => smlib_fillet,
Pick::Chamfer => smlib_chamfer,
Pick::Shell => smlib_shell,
Pick::OffsetFaces => smlib_offset_faces,
};
let flat = points.as_ptr().cast::<f64>();
let mut failed = -1i64;
@@ -98,6 +112,16 @@ pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size
})
}
#[cfg(feature = "native")]
pub fn thicken(b: &Handle, thickness: f64, symmetric: bool) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) })
}
#[cfg(not(feature = "native"))]
pub fn thicken(_: &Handle, _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn boolean(_: &Handle, _: &Handle, _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
+4
View File
@@ -44,6 +44,8 @@ pub enum Error {
NoMatch { point: usize },
/// Selection point `point` is equally near to two edges (faces).
Ambiguous { point: usize },
/// An inward offset (a shell's wall, faces moved in) leaves no room inside the body.
NoRoom,
/// A C++ exception (most likely out of memory) was caught at the boundary.
Exception,
/// The C API is a different version from the one this crate was written against.
@@ -64,6 +66,7 @@ impl std::fmt::Display for Error {
Error::NotSolid => write!(f, "SMLib's result is not a closed solid"),
Error::NoMatch { point } => write!(f, "selection point {} is not near any edge or face", point + 1),
Error::Ambiguous { point } => write!(f, "selection point {} is equally near to two edges or faces", point + 1),
Error::NoRoom => write!(f, "the offset leaves no room inside the body"),
Error::Exception => write!(f, "SMLib raised an internal exception"),
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
@@ -81,6 +84,7 @@ fn error(s: i32) -> Error {
ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument,
ffi::ERR_EMPTY => Error::Empty,
ffi::ops::ERR_NOT_SOLID => Error::NotSolid,
ffi::ops::ERR_NO_ROOM => Error::NoRoom,
ffi::ERR_EXCEPTION => Error::Exception,
ffi::NOT_BUILT => Error::NotBuilt,
s => Error::Status(s),
+19 -2
View File
@@ -1,4 +1,5 @@
//! SMLib's modelling operations: booleans, fillet, chamfer and shell. Each returns a new body
//! SMLib's modelling operations: booleans, fillet, chamfer, shell, offset faces and thicken.
//! Each returns a new body
//! and leaves its inputs unchanged.
//!
//! Edges and faces are chosen by points, never by SMLib's internal order: each point selects the
@@ -66,11 +67,27 @@ impl Brep {
/// Hollowed to a wall of `thickness` (negative: inside the original surface, positive:
/// outside), open at the faces nearest to `open_faces` (each within `tolerance`); none gives
/// a closed hollow body.
/// a closed hollow body. An inward wall that leaves no room inside is [`Error::NoRoom`].
pub fn shell(&self, open_faces: &[[f64; 3]], tolerance: f64, thickness: f64) -> Result<Brep> {
self.pick_op(ffi::ops::Pick::Shell, open_faces, tolerance, thickness, true)
}
/// The solid with the faces nearest to `faces` (each within `tolerance`) moved `distance`
/// along their outward normals (negative: into the body), onto their surfaces' offsets, the
/// faces around them extended or trimmed to meet them. Faces moved in past all of the body
/// behind them are [`Error::NoRoom`].
pub fn offset_faces(&self, faces: &[[f64; 3]], tolerance: f64, distance: f64) -> Result<Brep> {
self.pick_op(ffi::ops::Pick::OffsetFaces, faces, tolerance, distance, false)
}
/// A sheet thickened into a solid by `thickness` along its surfaces' normals (negative:
/// against them), or by half each way when `symmetric`, with ruled walls along its open
/// edges. A closed solid is [`Error::Argument`].
pub fn thicken(&self, thickness: f64, symmetric: bool) -> Result<Brep> {
init()?;
ffi::ops::thicken(&self.0, thickness, symmetric).map(Brep).map_err(error)
}
fn pick_op(&self, kind: ffi::ops::Pick, points: &[[f64; 3]], tolerance: f64, size: f64, allow_none: bool) -> Result<Brep> {
init()?;
if points.len() > MAX_POINTS || (points.is_empty() && !allow_none) {
+5 -5
View File
@@ -206,16 +206,16 @@ fn hostile_inputs_are_errors() {
assert_eq!(c.shell(&[[10.0, 10.0, 20.0]], 0.1, t).unwrap_err(), Error::Argument, "thickness {t}");
}
// Sizes SMLib cannot build fail as errors, whatever SMLib says.
for (what, r) in [
("radius larger than the cube", c.fillet(&mid, 0.1, 50.0)),
("chamfer past the faces", c.chamfer(&mid, 0.1, 30.0)),
("wall thicker than the box", c.shell(&[[10.0, 10.0, 20.0]], 0.1, -15.0)),
] {
for (what, r) in [("radius larger than the cube", c.fillet(&mid, 0.1, 50.0)), ("chamfer past the faces", c.chamfer(&mid, 0.1, 30.0))] {
match r {
Ok(b) => panic!("{what}: SMLib built a body of volume {:?}, counts {:?}", b.volume(ACC), b.counts()),
Err(e) => eprintln!("{what}: {e:?}"),
}
}
// A wall with no room inside it never reaches SMLib's offset.
assert_eq!(c.shell(&[[10.0, 10.0, 20.0]], 0.1, -15.0).unwrap_err(), Error::NoRoom);
assert_eq!(c.offset_faces(&[[10.0, 10.0, 20.0]], 0.1, -20.0).unwrap_err(), Error::NoRoom);
assert_eq!(c.thicken(1.0, false).unwrap_err(), Error::Argument, "a solid is no sheet");
assert_untouched(&c, 8000.0, [6, 12, 8]);
}