mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
solidcraft-smlib: Brep::chamfer_sides, and Error::Unsupported for a radius law with a corner
Brep::chamfer_sides reaches the fork's smlib_chamfer_sides: `distance` back along the face
whose outward normal leans most along a given direction, the other side by a second distance
(ChamferSecond::Distance) or at an angle to the first face (ChamferSecond::Angle). The fork's
smlib_fillet_variable refuses a radius law with a corner with SMLIB_ERR_UNSUPPORTED, which the
crate reads as Error::Unsupported ("not supported yet"): SMLib's section fit ran smoothly
across the corner and missed the law (4 -> 2 -> 4 on a 20 mm edge took 35.1 mm3 for 40.1;
tangency-labs/solidcore#430). Both are in the fork at the pinned bf7c272.
Tests (ops_tests.rs), against analytic values: 5 and 2 either way round (the centroid tells
which face took the 5), 5 at 30 degrees, rejected arguments; a quadratic law 4, 1, 4 and a law
down to zero (1e-5), the corner law refused and the same points joined C1 built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_fillet`,
|
||||
//! `smlib_chamfer`, `smlib_blend`, `smlib_fillet_variable`, `smlib_shell`, `smlib_offset_faces`,
|
||||
//! `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
|
||||
//! hand. Part of the FFI boundary, like its parent: called only through
|
||||
//! [`crate::ops`].
|
||||
@@ -17,6 +17,7 @@ pub const ERR_PICK_NONE: i32 = -1101;
|
||||
pub const ERR_PICK_AMBIGUOUS: i32 = -1102;
|
||||
pub const ERR_NO_ROOM: i32 = -1103;
|
||||
pub const ERR_NO_CUT: i32 = -1104;
|
||||
pub const ERR_UNSUPPORTED: i32 = -1105;
|
||||
|
||||
pub const BOOLEAN_UNION: i32 = 0;
|
||||
pub const BOOLEAN_SUBTRACT: i32 = 1;
|
||||
@@ -88,6 +89,18 @@ unsafe extern "C" {
|
||||
n_radii: i64,
|
||||
out: *mut *mut RawBrep,
|
||||
) -> i32;
|
||||
fn smlib_chamfer_sides(
|
||||
brep: *const RawBrep,
|
||||
points: *const f64,
|
||||
n_points: i64,
|
||||
tolerance: f64,
|
||||
distance: f64,
|
||||
second: f64,
|
||||
second_is_angle: i32,
|
||||
first: *const f64,
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
fn smlib_shell(
|
||||
brep: *const RawBrep,
|
||||
points: *const f64,
|
||||
@@ -189,6 +202,32 @@ pub fn fillet_variable(
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_chamfer_sides`.
|
||||
#[cfg(feature = "native")]
|
||||
pub fn chamfer_sides(
|
||||
b: &Handle,
|
||||
points: &[[f64; 3]],
|
||||
tolerance: f64,
|
||||
distance: f64,
|
||||
second: f64,
|
||||
angle: bool,
|
||||
first: &[f64; 3],
|
||||
) -> 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_chamfer_sides(b.raw(), flat, n, tolerance, distance, second, i32::from(angle), first.as_ptr(), 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 chamfer_sides(_: &Handle, _: &[[f64; 3]], _: f64, _: f64, _: f64, _: bool, _: &[f64; 3]) -> Result<Handle, PickError> {
|
||||
Err((NOT_BUILT, None))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn blend(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: f64) -> Result<Handle, PickError> {
|
||||
Err((NOT_BUILT, None))
|
||||
|
||||
@@ -28,7 +28,7 @@ pub mod sweeps;
|
||||
|
||||
pub use check::{CheckLevel, CheckReport};
|
||||
pub use measure::{MEASURE_ACCURACY, MassProperties};
|
||||
pub use ops::{Blend, BooleanOp, MAX_POINTS};
|
||||
pub use ops::{Blend, BooleanOp, ChamferSecond, MAX_POINTS};
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -47,6 +47,8 @@ pub enum Error {
|
||||
Ambiguous { point: usize },
|
||||
/// An inward offset (a shell's wall, faces moved in) leaves no room inside the body.
|
||||
NoRoom,
|
||||
/// SMLib cannot build this exactly yet (a variable radius whose law has a corner).
|
||||
Unsupported,
|
||||
/// 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.
|
||||
@@ -68,6 +70,7 @@ impl std::fmt::Display for Error {
|
||||
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::Unsupported => write!(f, "not supported yet: SMLib cannot build this exactly"),
|
||||
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"),
|
||||
@@ -86,6 +89,7 @@ fn error(s: i32) -> Error {
|
||||
ffi::ERR_EMPTY => Error::Empty,
|
||||
ffi::ops::ERR_NOT_SOLID => Error::NotSolid,
|
||||
ffi::ops::ERR_NO_ROOM => Error::NoRoom,
|
||||
ffi::ops::ERR_UNSUPPORTED => Error::Unsupported,
|
||||
ffi::ERR_EXCEPTION => Error::Exception,
|
||||
ffi::NOT_BUILT => Error::NotBuilt,
|
||||
s => Error::Status(s),
|
||||
|
||||
@@ -32,6 +32,15 @@ pub enum Blend {
|
||||
Chamfer,
|
||||
}
|
||||
|
||||
/// The second side of [`Brep::chamfer_sides`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ChamferSecond {
|
||||
/// The setback along the other face.
|
||||
Distance(f64),
|
||||
/// The angle (radians) the chamfer makes with the first face.
|
||||
Angle(f64),
|
||||
}
|
||||
|
||||
/// Most points one selection may hold (the C API's limit).
|
||||
pub const MAX_POINTS: usize = 4096;
|
||||
|
||||
@@ -122,6 +131,22 @@ impl Brep {
|
||||
})
|
||||
}
|
||||
|
||||
/// The edges nearest to `edges` (each within `tolerance`), with their tangent chains, cut by
|
||||
/// a chamfer `distance` back along the face whose outward normal leans most along `first`,
|
||||
/// and by `second` on the other, with bevelled corners (setbacks measured at each edge's
|
||||
/// midpoint).
|
||||
pub fn chamfer_sides(&self, edges: &[[f64; 3]], tolerance: f64, distance: f64, second: ChamferSecond, first: [f64; 3]) -> Result<Brep> {
|
||||
init()?;
|
||||
if edges.is_empty() || edges.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (value, angle) = match second {
|
||||
ChamferSecond::Distance(d) => (d, false),
|
||||
ChamferSecond::Angle(a) => (a, true),
|
||||
};
|
||||
ffi::ops::chamfer_sides(&self.0, edges, tolerance, distance, value, angle, &first).map(Brep).map_err(pick_error)
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
|
||||
@@ -274,6 +274,53 @@ fn a_variable_round_follows_its_law() {
|
||||
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 { .. })));
|
||||
// A quadratic law with control radii 4, 1, 4: r(t) = 4(1 − t)² + 2t(1 − t) + 4t².
|
||||
let law = |t: f64| 4.0 * (1.0 - t).powi(2) + 2.0 * t * (1.0 - t) + 4.0 * t * t;
|
||||
let n = 4000;
|
||||
let r2 = (0..n).map(|i| law((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64;
|
||||
let quad = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 2, &[0.0, 0.0, 0.0, 1.0, 1.0, 1.0], &[4.0, 1.0, 4.0]).unwrap();
|
||||
shot(&quad, "variable-quadratic");
|
||||
assert_close(volume(&quad), 8000.0 - q * 20.0 * r2, 1e-5, "quadratic law");
|
||||
// Down to zero at the far end: the round closes to the edge's end point.
|
||||
let zero = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[4.0, 0.0]).unwrap();
|
||||
shot(&zero, "variable-to-zero");
|
||||
assert_close(volume(&zero), 8000.0 - q * 20.0 * 16.0 / 3.0, 1e-5, "down to zero");
|
||||
// A law with a corner (4 → 2 → 4, straight on both halves): SMLib's section fit runs across
|
||||
// the corner and misses it, so it is refused; the same points joined smoothly (C1 cubic
|
||||
// pieces, as RadiusSpline::through makes them) are built.
|
||||
let corner = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 2.0, 4.0]);
|
||||
assert_eq!(corner.unwrap_err(), Error::Unsupported);
|
||||
let smooth_knots = [0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0];
|
||||
let smooth = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 3, &smooth_knots, &[4.0, 10.0 / 3.0, 2.0, 2.0, 2.0, 10.0 / 3.0, 4.0]);
|
||||
shot(&smooth.unwrap(), "variable-smooth-4-2-4");
|
||||
}
|
||||
|
||||
/// Unequal chamfers of a cube's top edge y = 20: 5 back along the face facing `first` most and 2
|
||||
/// along the other take away a triangle 5 × 2 / 2 along the 20 mm edge, either way round; 5 at
|
||||
/// 30° to the first face is 5 and 5 tan 30°.
|
||||
#[test]
|
||||
fn chamfers_with_two_setbacks() {
|
||||
let c = cube(20.0);
|
||||
let top = [[10.0, 20.0, 20.0]];
|
||||
let up = c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Distance(2.0), [0.0, 0.0, 1.0]).unwrap();
|
||||
shot(&up, "chamfer-5-2");
|
||||
assert_close(volume(&up), 8000.0 - 5.0 * 20.0, 1e-9, "5 and 2");
|
||||
let flipped = c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Distance(2.0), [0.0, 0.0, -1.0]).unwrap();
|
||||
assert_close(volume(&flipped), volume(&up), 1e-9, "flipped");
|
||||
// Which face took the 5: the removed prism's centroid lies a third of each setback in from
|
||||
// the edge, so the result's centroid moves away from it.
|
||||
let centroid = |b: &Brep| b.mass_properties(ACC).unwrap().centroid;
|
||||
let left = |dy: f64, dz: f64| [10.0, (8000.0 * 10.0 - 100.0 * (20.0 - dy / 3.0)) / 7900.0, (8000.0 * 10.0 - 100.0 * (20.0 - dz / 3.0)) / 7900.0];
|
||||
for (b, want, what) in [(&up, left(5.0, 2.0), "5 along the top"), (&flipped, left(2.0, 5.0), "5 down the side")] {
|
||||
let got = centroid(b);
|
||||
assert!((0..3).all(|i| (got[i] - want[i]).abs() < 1e-6), "{what}: centroid {got:?}, want {want:?}");
|
||||
}
|
||||
let angle = c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Angle(30f64.to_radians()), [0.0, 0.0, 1.0]).unwrap();
|
||||
shot(&angle, "chamfer-5-at-30");
|
||||
assert_close(volume(&angle), 8000.0 - 5.0 * 5.0 * 30f64.to_radians().tan() / 2.0 * 20.0, 1e-9, "5 at 30 degrees");
|
||||
assert_eq!(c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Angle(4.0), [0.0, 0.0, 1.0]).unwrap_err(), Error::Argument);
|
||||
assert_eq!(c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Distance(2.0), [0.0; 3]).unwrap_err(), Error::Argument);
|
||||
assert_eq!(c.chamfer_sides(&top, 0.1, 5.0, ChamferSecond::Distance(-2.0), [0.0, 0.0, 1.0]).unwrap_err(), Error::Argument);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user