solidcraft-smlib: Brep::draft, and SMLIB_REV at the fork's smlib_draft

Flat faces tilted by an angle about the line where each meets a neutral plane, leaning in along
the pull, and the faces around them extended or trimmed to meet them
(tangency-labs/smlib@e1ac3c3, SMLib's local operation). A face that isn't flat or isn't parallel
to the pull is Error::Unsupported. The test drafts a cube side by +-5 degrees, removing or adding
the analytic wedge to 1e-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruck
2026-10-21 12:50:11 -04:00
committed by jyviko
parent 45f8e2916e
commit 523270fe5e
4 changed files with 80 additions and 2 deletions
+1 -1
View File
@@ -1 +1 @@
be3fc102da38f7537436618dcd01966f02329675
e1ac3c31ae1f6c6dae6bd9e0bdfd19952cecff44
+40 -1
View File
@@ -1,6 +1,6 @@
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_fillet`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_thicken`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by
//! `smlib_thicken`, `smlib_draft`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by
//! hand. Part of the FFI boundary, like its parent: called only through
//! [`crate::ops`].
@@ -120,6 +120,18 @@ unsafe extern "C" {
out_failed_point: *mut i64,
) -> i32;
fn smlib_thicken(brep: *const RawBrep, thickness: f64, symmetric: i32, out: *mut *mut RawBrep) -> i32;
fn smlib_draft(
brep: *const RawBrep,
points: *const f64,
n_points: i64,
tolerance: f64,
neutral_origin: *const f64,
neutral_normal: *const f64,
pull: *const f64,
angle: f64,
out: *mut *mut RawBrep,
out_failed_point: *mut i64,
) -> i32;
}
#[cfg(feature = "native")]
@@ -169,6 +181,33 @@ pub fn thicken(_: &Handle, _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
/// `smlib_draft`: the faces under `points` tilted by `angle` radians about their lines on the
/// neutral plane (`origin`, `normal`), leaning in along `pull`.
#[cfg(feature = "native")]
pub fn draft(
b: &Handle,
points: &[[f64; 3]],
tolerance: f64,
origin: &[f64; 3],
normal: &[f64; 3],
pull: &[f64; 3],
angle: f64,
) -> Result<Handle, PickError> {
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
let flat = points.as_ptr().cast::<f64>();
let mut failed = -1i64;
new_brep(|out| unsafe { smlib_draft(b.raw(), flat, n, tolerance, origin.as_ptr(), normal.as_ptr(), pull.as_ptr(), angle, out, &mut failed) })
.map_err(|s| {
let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten();
(s, at)
})
}
#[cfg(not(feature = "native"))]
pub fn draft(_: &Handle, _: &[[f64; 3]], _: f64, _: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64) -> Result<Handle, PickError> {
Err((NOT_BUILT, None))
}
/// `smlib_blend`: `sizes` holds one size per point (the caller checks the lengths match).
#[cfg(feature = "native")]
pub fn blend(kind: i32, b: &Handle, points: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result<Handle, PickError> {
+21
View File
@@ -170,6 +170,27 @@ impl Brep {
ffi::ops::thicken(&self.0, thickness, symmetric).map(Brep).map_err(error)
}
/// The flat faces nearest to `faces` (each within `tolerance`) tilted by `angle` radians
/// (0 < |angle| < 89.94 degrees) about the line where each meets the neutral plane through
/// `neutral_origin` with normal `neutral_normal`, a positive angle leaning them in going along
/// `pull`; the faces around them extended or trimmed to meet them. A face that isn't flat, or
/// isn't parallel to `pull`, is [`Error::Unsupported`].
pub fn draft(
&self,
faces: &[[f64; 3]],
tolerance: f64,
neutral_origin: [f64; 3],
neutral_normal: [f64; 3],
pull: [f64; 3],
angle: f64,
) -> Result<Brep> {
init()?;
if faces.is_empty() || faces.len() > MAX_POINTS {
return Err(Error::Argument);
}
ffi::ops::draft(&self.0, faces, tolerance, &neutral_origin, &neutral_normal, &pull, angle).map(Brep).map_err(pick_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) {
+18
View File
@@ -336,3 +336,21 @@ fn imprint_cuts_faces_where_a_tool_meets_them() {
let far = Brep::make_box([50.0, 0.0, 0.0], [10.0, 10.0, 10.0]).unwrap();
assert!(cube.imprint(&far).unwrap().is_none());
}
#[test]
fn draft_tilts_a_side_about_its_line_on_the_neutral_plane() {
let c = cube(20.0);
let deg = std::f64::consts::PI / 180.0;
let wedge = 0.5 * 20.0 * 20.0 * (5.0 * deg).tan() * 20.0;
let side = [[20.0, 10.0, 10.0]];
let up = [0.0, 0.0, 1.0];
let leaned = c.draft(&side, 0.1, [0.0; 3], up, up, 5.0 * deg).unwrap();
assert!((leaned.volume(ACC).unwrap() - (8000.0 - wedge)).abs() < 1e-6);
let out = c.draft(&side, 0.1, [0.0; 3], up, up, -5.0 * deg).unwrap();
assert!((out.volume(ACC).unwrap() - (8000.0 + wedge)).abs() < 1e-6);
// The top isn't parallel to the pull.
assert_eq!(c.draft(&[[10.0, 10.0, 20.0]], 0.1, [0.0; 3], up, up, 5.0 * deg).unwrap_err(), Error::Unsupported);
assert_eq!(c.draft(&side, 0.1, [0.0; 3], up, up, 0.0).unwrap_err(), Error::Argument);
assert_eq!(c.draft(&[], 0.1, [0.0; 3], up, up, 5.0 * deg).unwrap_err(), Error::Argument);
assert_untouched(&c, 8000.0, [6, 12, 8]);
}