mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
solidcraft-smlib: Brep::blend (per-edge sizes; round, G2, chord, chamfer) and Brep::fillet_variable
The Rust side of the fork's smlib_blend and smlib_fillet_variable, which the kernel's fillets and chamfers need on SMLib: `Brep::blend` takes a size per picked edge and a `Blend` (circular round, curvature-continuous round between the round's contacts, constant-chord round, equal-distance chamfer); `Brep::fillet_variable` rounds one edge with a clamped B-spline radius law from the end nearest a start point. Selection errors map as for the other picking operations. SMLIB_REV moves to the fork commit that adds them (smlib-blends, on capi-solidcraft ffcf56d). Tests: per-edge rounds and chamfers and the chord round against analytic volumes, the linear law from either end, and refused laws and picks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
@@ -1 +1 @@
|
||||
e87e9b3bdc348b00de5238963f387bfa4ba337d6
|
||||
d041e7f9679436a4ad3a9620861965dd41105a27
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_fillet`,
|
||||
//! `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
|
||||
//! `smlib_chamfer`, `smlib_blend`, `smlib_fillet_variable`, `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`].
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
@@ -21,6 +21,11 @@ pub const BOOLEAN_UNION: i32 = 0;
|
||||
pub const BOOLEAN_SUBTRACT: i32 = 1;
|
||||
pub const BOOLEAN_INTERSECT: i32 = 2;
|
||||
|
||||
pub const BLEND_ROUND: i32 = 0;
|
||||
pub const BLEND_CURVATURE: i32 = 1;
|
||||
pub const BLEND_CHORD: i32 = 2;
|
||||
pub const BLEND_CHAMFER: i32 = 3;
|
||||
|
||||
/// Which selection operation: the C functions share one signature.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Pick {
|
||||
@@ -59,6 +64,28 @@ unsafe extern "C" {
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
fn smlib_blend(
|
||||
brep: *const RawBrep,
|
||||
kind: i32,
|
||||
points: *const f64,
|
||||
sizes: *const f64,
|
||||
n_points: i64,
|
||||
tolerance: f64,
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
fn smlib_fillet_variable(
|
||||
brep: *const RawBrep,
|
||||
point: *const f64,
|
||||
tolerance: f64,
|
||||
start: *const f64,
|
||||
degree: i32,
|
||||
knots: *const f64,
|
||||
n_knots: i64,
|
||||
radii: *const f64,
|
||||
n_radii: i64,
|
||||
out: *mut *mut RawBrep,
|
||||
) -> i32;
|
||||
fn smlib_shell(
|
||||
brep: *const RawBrep,
|
||||
points: *const f64,
|
||||
@@ -122,6 +149,49 @@ pub fn thicken(_: &Handle, _: f64, _: bool) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
|
||||
/// `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> {
|
||||
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
|
||||
if sizes.len() != points.len() {
|
||||
return Err((ERR_ARG, None));
|
||||
}
|
||||
let flat = points.as_ptr().cast::<f64>();
|
||||
let mut failed = -1i64;
|
||||
new_brep(|out| unsafe { smlib_blend(b.raw(), kind, flat, sizes.as_ptr(), n, tolerance, 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)
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_fillet_variable`.
|
||||
#[cfg(feature = "native")]
|
||||
pub fn fillet_variable(
|
||||
b: &Handle,
|
||||
point: &[f64; 3],
|
||||
tolerance: f64,
|
||||
start: &[f64; 3],
|
||||
degree: i32,
|
||||
knots: &[f64],
|
||||
radii: &[f64],
|
||||
) -> Result<Handle, i32> {
|
||||
let nk = i64::try_from(knots.len()).map_err(|_| ERR_ARG)?;
|
||||
let nr = i64::try_from(radii.len()).map_err(|_| ERR_ARG)?;
|
||||
new_brep(|out| unsafe {
|
||||
smlib_fillet_variable(b.raw(), point.as_ptr(), tolerance, start.as_ptr(), degree, knots.as_ptr(), nk, radii.as_ptr(), nr, out)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn blend(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: f64) -> Result<Handle, PickError> {
|
||||
Err((NOT_BUILT, None))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn fillet_variable(_: &Handle, _: &[f64; 3], _: f64, _: &[f64; 3], _: i32, _: &[f64], _: &[f64]) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn boolean(_: &Handle, _: &Handle, _: i32) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
|
||||
@@ -28,7 +28,7 @@ pub mod sweeps;
|
||||
|
||||
pub use check::{CheckLevel, CheckReport};
|
||||
pub use measure::{MEASURE_ACCURACY, MassProperties};
|
||||
pub use ops::{BooleanOp, MAX_POINTS};
|
||||
pub use ops::{Blend, BooleanOp, MAX_POINTS};
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -17,6 +17,21 @@ pub enum BooleanOp {
|
||||
Intersect,
|
||||
}
|
||||
|
||||
/// What [`Brep::blend`] makes of each selected edge.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Blend {
|
||||
/// A circular cross-section at a constant radius ([`Brep::fillet`]).
|
||||
Round,
|
||||
/// A curvature-continuous (G2) cross-section between the contact lines of the round of
|
||||
/// that radius.
|
||||
Curvature,
|
||||
/// A circular cross-section of constant width from contact line to contact line: the size
|
||||
/// is that width.
|
||||
Chord,
|
||||
/// An equal-distance chamfer ([`Brep::chamfer`]): the size is the setback along each face.
|
||||
Chamfer,
|
||||
}
|
||||
|
||||
/// Most points one selection may hold (the C API's limit).
|
||||
pub const MAX_POINTS: usize = 4096;
|
||||
|
||||
@@ -65,6 +80,36 @@ impl Brep {
|
||||
self.pick_op(ffi::ops::Pick::Chamfer, edges, tolerance, distance, false)
|
||||
}
|
||||
|
||||
/// The edges nearest to `edges` (each within `tolerance`), with their tangent chains, blended
|
||||
/// as `kind`, edge i (and its chain) at `sizes[i]`.
|
||||
pub fn blend(&self, kind: Blend, edges: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result<Brep> {
|
||||
init()?;
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.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,
|
||||
};
|
||||
ffi::ops::blend(kind, &self.0, edges, sizes, tolerance).map(Brep).map_err(pick_error)
|
||||
}
|
||||
|
||||
/// The one edge nearest to `edge` (within `tolerance`, no tangent chain) 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
|
||||
/// (the edge curve's parameter scaled to 0..1).
|
||||
pub fn fillet_variable(&self, edge: [f64; 3], tolerance: f64, start: [f64; 3], degree: usize, knots: &[f64], radii: &[f64]) -> Result<Brep> {
|
||||
init()?;
|
||||
let degree = i32::try_from(degree).map_err(|_| Error::Argument)?;
|
||||
ffi::ops::fillet_variable(&self.0, &edge, tolerance, &start, degree, knots, radii).map(Brep).map_err(|s| match s {
|
||||
ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 },
|
||||
ffi::ops::ERR_PICK_AMBIGUOUS => Error::Ambiguous { point: 0 },
|
||||
s => error(s),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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. An inward wall that leaves no room inside is [`Error::NoRoom`].
|
||||
|
||||
@@ -240,3 +240,38 @@ fn boolean_history_through_tagged_operands() {
|
||||
// The 8 edges where the slot's walls and floor meet the top, front and back are new.
|
||||
assert_eq!(h.edges.iter().filter(|e| e.origins.is_empty()).count(), 8, "{h:?}");
|
||||
}
|
||||
|
||||
/// `blend` gives each picked edge its own size: two parallel top edges of a 20 mm cube rounded at
|
||||
/// 2 and 3 lose (1 - pi/4) r^2 L each, chamfered r^2 L / 2 each; a chord of 2 sqrt 2 on a right
|
||||
/// angle is the round of radius 2.
|
||||
#[test]
|
||||
fn blends_take_a_size_per_edge() {
|
||||
let c = cube(20.0);
|
||||
let tops = [[10.0, 20.0, 20.0], [10.0, 0.0, 20.0]];
|
||||
let q = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let r = c.blend(Blend::Round, &tops, &[2.0, 3.0], 0.1).unwrap();
|
||||
shot(&r, "blend-round-2-3");
|
||||
assert_close(volume(&r), 8000.0 - q * 13.0 * 20.0, 1e-6, "rounds 2 and 3");
|
||||
let ch = c.blend(Blend::Chamfer, &tops, &[2.0, 3.0], 0.1).unwrap();
|
||||
shot(&ch, "blend-chamfer-2-3");
|
||||
assert_close(volume(&ch), 8000.0 - 0.5 * 13.0 * 20.0, 1e-6, "chamfers 2 and 3");
|
||||
let chord = c.blend(Blend::Chord, &tops[..1], &[2.0 * 2f64.sqrt()], 0.1).unwrap();
|
||||
assert_close(volume(&chord), 8000.0 - q * 4.0 * 20.0, 2e-6, "chord");
|
||||
assert!(c.blend(Blend::Round, &tops, &[2.0], 0.1).is_err());
|
||||
}
|
||||
|
||||
/// A radius running linearly from 3 to 1 along a cube edge: (1 - pi/4) L (r0² + r0 r1 + r1²) / 3
|
||||
/// taken away, whichever end is the start.
|
||||
#[test]
|
||||
fn a_variable_round_follows_its_law() {
|
||||
let c = cube(20.0);
|
||||
let q = 1.0 - std::f64::consts::FRAC_PI_4;
|
||||
let knots = [0.0, 0.0, 1.0, 1.0];
|
||||
let r = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[3.0, 1.0]).unwrap();
|
||||
shot(&r, "variable-3-1");
|
||||
assert_close(volume(&r), 8000.0 - q * 20.0 * 13.0 / 3.0, 1e-5, "linear law");
|
||||
let back = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [20.0, 20.0, 20.0], 1, &knots, &[1.0, 3.0]).unwrap();
|
||||
assert_close(volume(&back), volume(&r), 1e-7, "reversed");
|
||||
assert!(c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.5, 1.0, 1.0], &[3.0, 1.0]).is_err());
|
||||
assert!(matches!(c.fillet_variable([90.0, 90.0, 90.0], 0.1, [0.0; 3], 1, &knots, &[3.0, 1.0]), Err(Error::NoMatch { .. })));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user