smlib: Brep::blend_setback_open, set-back corners left open with their holes

smlib_blend_setback_open's binding: the sheet, and per corner a Hole, its
edges in order (each starting where the one before ends), each with the face
beside it and whether that face runs it forwards. The holes' buffer starts at
64 edges a corner and grows once to the length a first call names. Tested on
the r5 box corner set back 8: 9/30/22, a sheet SMLib's check passes, one hole
of twelve edges chained end to start; an unrounded edge at the corner is
Unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-26 12:32:39 -04:00
committed by Irakli Kourtis
parent 29225d2c8a
commit 6263db8d66
4 changed files with 179 additions and 3 deletions
+82 -2
View File
@@ -1,5 +1,5 @@
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_boolean_union_all`, `smlib_boolean2d`, `smlib_fillet`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_setback`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_thicken`, `smlib_draft`, `smlib_merge_faces`, `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`].
@@ -10,7 +10,7 @@ use super::Handle;
#[cfg(not(feature = "native"))]
use super::NOT_BUILT;
#[cfg(feature = "native")]
use super::{ERR_ARG, OK, RawBrep, new_brep};
use super::{ERR_ARG, ERR_BUFFER, OK, RawBrep, new_brep};
pub const ERR_NOT_SOLID: i32 = -1100;
pub const ERR_PICK_NONE: i32 = -1101;
@@ -95,6 +95,23 @@ unsafe extern "C" {
out_failed_point: *mut i64,
out_failed_corner: *mut i64,
) -> i32;
fn smlib_blend_setback_open(
brep: *const RawBrep,
kind: i32,
points: *const f64,
sizes: *const f64,
n_points: i64,
corners: *const f64,
setbacks: *const f64,
n_corners: i64,
tolerance: f64,
out: *mut *mut RawBrep,
holes: *mut i64,
cap: i64,
out_len: *mut i64,
out_failed_point: *mut i64,
out_failed_corner: *mut i64,
) -> i32;
fn smlib_fillet_variable(
brep: *const RawBrep,
point: *const f64,
@@ -327,6 +344,69 @@ pub fn blend_setback(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: &[[f64; 3
Err((NOT_BUILT, None))
}
/// `smlib_blend_setback_open`: the sheet and the holes' values as the C function writes them
/// (per corner: n, then n (edge, face, forward) triples). The buffer starts at room for 64
/// edges a corner and is grown once to the length a first call names.
#[cfg(feature = "native")]
pub fn blend_setback_open(
kind: i32,
b: &Handle,
points: &[[f64; 3]],
sizes: &[f64],
corners: &[[f64; 3]],
setbacks: &[f64],
tolerance: f64,
) -> Result<(Handle, Vec<i64>), PickError> {
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
let nc = i64::try_from(corners.len()).map_err(|_| (ERR_ARG, None))?;
if sizes.len() != points.len() || setbacks.len() != corners.len() {
return Err((ERR_ARG, None));
}
let flat = points.as_ptr().cast::<f64>();
let flat_corners = corners.as_ptr().cast::<f64>();
let mut holes = vec![0i64; corners.len().saturating_mul(1 + 3 * 64)];
for _ in 0..2 {
let cap = i64::try_from(holes.len()).map_err(|_| (ERR_ARG, None))?;
let (mut failed, mut failed_corner, mut len) = (-1i64, -1i64, -1i64);
let made = new_brep(|out| unsafe {
smlib_blend_setback_open(
b.raw(),
kind,
flat,
sizes.as_ptr(),
n,
flat_corners,
setbacks.as_ptr(),
nc,
tolerance,
out,
holes.as_mut_ptr(),
cap,
&mut len,
&mut failed,
&mut failed_corner,
)
});
match made {
Ok(h) => {
holes.truncate(usize::try_from(len).map_err(|_| (ERR_BUFFER, None))?);
return Ok((h, holes));
}
Err(ERR_BUFFER) if len > cap => holes.resize(usize::try_from(len).map_err(|_| (ERR_BUFFER, None))?, 0),
Err(s) => {
let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten();
return Err((s, at));
}
}
}
Err((ERR_BUFFER, None))
}
#[cfg(not(feature = "native"))]
pub fn blend_setback_open(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: &[[f64; 3]], _: &[f64], _: f64) -> Result<(Handle, Vec<i64>), PickError> {
Err((NOT_BUILT, None))
}
/// `smlib_fillet_variable`.
#[cfg(feature = "native")]
pub fn fillet_variable(
+1 -1
View File
@@ -32,7 +32,7 @@ pub mod trim;
pub use check::{CheckLevel, CheckReport};
pub use measure::{MEASURE_ACCURACY, MassProperties};
pub use ops::{Blend, BooleanOp, ChamferSecond, MAX_POINTS, MAX_UNION_BREPS};
pub use ops::{Blend, BooleanOp, ChamferSecond, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS};
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq)]
+70
View File
@@ -55,6 +55,40 @@ fn pick_error((s, at): ffi::ops::PickError) -> Error {
}
}
/// One edge round an opened corner's hole ([`Brep::blend_setback_open`]): its index in the
/// sheet's edge order, the face beside it in its face order, and whether that face, run
/// counter-clockwise about its outward normal, runs the edge from its start vertex to its end.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HoleEdge {
pub edge: usize,
pub face: usize,
pub forward: bool,
}
/// The edges round an opened corner's hole, in order: each starts where the one before it ends.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hole {
pub edges: Vec<HoleEdge>,
}
/// The holes `smlib_blend_setback_open` wrote: `corners` of them, each n and n triples.
fn holes(values: &[i64], corners: usize) -> Option<Vec<Hole>> {
let mut at = 0usize;
let mut out = Vec::with_capacity(corners);
for _ in 0..corners {
let n = usize::try_from(*values.get(at)?).ok()?;
at += 1;
let mut edges = Vec::with_capacity(n);
for _ in 0..n {
let (e, f, fw) = (values.get(at)?, values.get(at + 1)?, values.get(at + 2)?);
edges.push(HoleEdge { edge: usize::try_from(*e).ok()?, face: usize::try_from(*f).ok()?, forward: *fw != 0 });
at += 3;
}
out.push(Hole { edges });
}
(at == values.len()).then_some(out)
}
impl Brep {
/// `self` and `other` combined by `op`: `None` when the result is empty (disjoint bodies
/// intersected, a body subtracted by one that encloses it). Several separate solids come back
@@ -183,6 +217,42 @@ impl Brep {
ffi::ops::blend_setback(kind, &self.0, edges, sizes, corners, setbacks, tolerance).map(Brep).map_err(pick_error)
}
/// [`Brep::blend_setback`] with each set-back corner left open: SMLib's patch between the
/// corner's setback sections is removed, so the result is a sheet whose only free edges are
/// the holes', one [`Hole`] per corner in `corners`' order (a vertex picked twice gives its
/// hole twice). A corner whose patch SMLib does not build as a disc of faces of its own is
/// [`Error::Unsupported`].
pub fn blend_setback_open(
&self,
kind: Blend,
edges: &[[f64; 3]],
sizes: &[f64],
corners: &[[f64; 3]],
setbacks: &[f64],
tolerance: f64,
) -> Result<(Brep, Vec<Hole>)> {
init()?;
if edges.len() > MAX_POINTS
|| edges.is_empty()
|| sizes.len() != edges.len()
|| corners.is_empty()
|| corners.len() > MAX_POINTS
|| setbacks.len() != corners.len()
{
return Err(Error::Argument);
}
let kind = match kind {
Blend::Round => ffi::ops::BLEND_ROUND,
Blend::Curvature => ffi::ops::BLEND_CURVATURE,
Blend::Chord => ffi::ops::BLEND_CHORD,
Blend::Chamfer => ffi::ops::BLEND_CHAMFER,
};
let (h, values) = ffi::ops::blend_setback_open(kind, &self.0, edges, sizes, corners, setbacks, tolerance).map_err(pick_error)?;
// A buffer that does not read as the C API documents it is a buffer error.
let holes = holes(&values, corners.len()).ok_or_else(|| error(ffi::ERR_BUFFER))?;
Ok((Brep(h), holes))
}
/// The one edge nearest to `edge` (within `tolerance`) rounded with a radius
/// that varies along it: a clamped B-spline of `degree` (1 to 3) with `knots` and control
/// `radii` over the position along the edge, 0 at its end nearest `start` and 1 at the other
+26
View File
@@ -380,3 +380,29 @@ fn draft_tilts_a_side_about_its_line_on_the_neutral_plane() {
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]);
}
/// The corner of r5 rounds at (60, 40, 20) on a 60 x 40 x 20 box, set back 8 and left open: the
/// box's six faces and the three rounds (9 faces, 30 edges, 22 vertices), a sheet SMLib's check
/// passes, and one hole of twelve edges (each section and face curve in two), chained end to
/// start, each bounding a different face than the patch's neighbours across it.
#[test]
fn a_setback_corner_left_open_returns_its_hole() {
let b = Brep::make_box([0.0; 3], [60.0, 40.0, 20.0]).unwrap();
let edges = [[30.0, 40.0, 20.0], [60.0, 20.0, 20.0], [60.0, 40.0, 10.0]];
let (open, holes) = b.blend_setback_open(Blend::Round, &edges, &[5.0; 3], &[[60.0, 40.0, 20.0]], &[8.0], 0.1).unwrap();
shot(&open, "setback_open");
assert_eq!(open.counts().unwrap(), [9, 30, 22]);
assert!(!open.is_manifold_solid().unwrap());
let [hole] = holes.as_slice() else { panic!("one corner: {holes:?}") };
assert_eq!(hole.edges.len(), 12);
let flat = open.export().unwrap();
let ends = flat.get_i64(crate::flat::EDGE_VERTICES).unwrap();
let end = |h: &HoleEdge, last: bool| ends[2 * h.edge + usize::from(h.forward == last)];
for (k, h) in hole.edges.iter().enumerate() {
let next = &hole.edges[(k + 1) % 12];
assert_eq!(end(h, true), end(next, false), "edge {k} ends where the next starts");
assert!(h.face < 9);
}
// A corner where an edge is not rounded builds no setback.
assert!(matches!(b.blend_setback_open(Blend::Round, &edges[..2], &[5.0; 2], &[[60.0, 40.0, 20.0]], &[8.0], 0.1), Err(Error::Unsupported)));
}