mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
solidcraft-smlib: booleans, fillet, chamfer and shell through SMLib
Brep::boolean (union, subtract, intersect; Ok(None) for an empty result), Brep::solid_count, Brep::fillet, Brep::chamfer and Brep::shell, over the fork's smlib_ops.cpp shims (ffi/ops.rs). Edges and faces are chosen by points, each the nearest within a tolerance; a point that selects nothing or cannot decide is Error::NoMatch / Error::Ambiguous with its index, and a solid input that gives anything but a closed solid is Error::NotSolid. Fillets and chamfers take whole tangent chains; a chamfer distance is the setback along each face; a shell's thickness is negative inward. The C API with these shims is version 2 (the fork's capi-flat line), so ABI_VERSION is 2. Tests (ops_tests.rs) against analytic volumes and counts, SMLib's own check at every level on each result, and history through a tagged box minus box: union 9000 mm3 [11, 24, 16]; slot 6000 [10, 24, 16]; intersection 1000; empty results; a split body (2 solids); a cube with all 12 edges rounded r = 2 against the Minkowski formula; one chamfer (7910, [7, 15, 10]); shells open at the top inward (3392), outward (4672) and closed; a tangent chain from one pick equal to picking all three edges; hostile sizes, tolerances, points and counts. 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 @@
|
||||
0a48843fd764b0b4a6e8998716e800a91518fe7a
|
||||
5fe723c7f0b9275064ffb1995d6c065c4ea64856
|
||||
|
||||
@@ -183,6 +183,7 @@ pub mod check;
|
||||
pub mod flat;
|
||||
pub mod history;
|
||||
pub mod measure;
|
||||
pub mod ops;
|
||||
|
||||
/// Where [`set_log`] sends SMLib's log lines.
|
||||
#[cfg(feature = "native")]
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
//! 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`),
|
||||
//! declared by hand. Part of the FFI boundary, like its parent: called only through
|
||||
//! [`crate::ops`].
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::Handle;
|
||||
#[cfg(not(feature = "native"))]
|
||||
use super::NOT_BUILT;
|
||||
#[cfg(feature = "native")]
|
||||
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 BOOLEAN_UNION: i32 = 0;
|
||||
pub const BOOLEAN_SUBTRACT: i32 = 1;
|
||||
pub const BOOLEAN_INTERSECT: i32 = 2;
|
||||
|
||||
/// Which selection operation: the C functions share one signature.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Pick {
|
||||
Fillet,
|
||||
Chamfer,
|
||||
Shell,
|
||||
}
|
||||
|
||||
/// A failed selection operation: the status, and the index of the point that selected nothing
|
||||
/// or several things (only for `ERR_PICK_*`).
|
||||
pub type PickError = (i32, Option<usize>);
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
type PickFn = unsafe extern "C" fn(*const RawBrep, *const f64, i64, f64, f64, *mut *mut RawBrep, *mut i64) -> i32;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
unsafe extern "C" {
|
||||
fn smlib_boolean(a: *const RawBrep, b: *const RawBrep, op: i32, out: *mut *mut RawBrep) -> i32;
|
||||
fn smlib_brep_solid_count(brep: *const RawBrep, out: *mut i64) -> i32;
|
||||
fn smlib_fillet(
|
||||
brep: *const RawBrep,
|
||||
points: *const f64,
|
||||
n_points: i64,
|
||||
tolerance: f64,
|
||||
radius: f64,
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
fn smlib_chamfer(
|
||||
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_shell(
|
||||
brep: *const RawBrep,
|
||||
points: *const f64,
|
||||
n_points: i64,
|
||||
tolerance: f64,
|
||||
thickness: f64,
|
||||
out: *mut *mut RawBrep,
|
||||
out_failed_point: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub fn boolean(a: &Handle, b: &Handle, op: i32) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_boolean(a.raw(), b.raw(), op, out) })
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub fn solid_count(b: &Handle) -> Result<i64, i32> {
|
||||
let mut n = 0i64;
|
||||
match unsafe { smlib_brep_solid_count(b.raw(), &mut n) } {
|
||||
OK => Ok(n),
|
||||
s => Err(s),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a selection operation; `points` are passed as the flat x, y, z array the C API reads.
|
||||
#[cfg(feature = "native")]
|
||||
pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size: f64) -> Result<Handle, PickError> {
|
||||
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
|
||||
let f: PickFn = match kind {
|
||||
Pick::Fillet => smlib_fillet,
|
||||
Pick::Chamfer => smlib_chamfer,
|
||||
Pick::Shell => smlib_shell,
|
||||
};
|
||||
let flat = points.as_ptr().cast::<f64>();
|
||||
let mut failed = -1i64;
|
||||
new_brep(|out| unsafe { f(b.raw(), flat, n, tolerance, size, 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 boolean(_: &Handle, _: &Handle, _: i32) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn solid_count(_: &Handle) -> Result<i64, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn pick_op(_: Pick, _: &Handle, _: &[[f64; 3]], _: f64, _: f64) -> Result<Handle, PickError> {
|
||||
Err((NOT_BUILT, None))
|
||||
}
|
||||
+13
-1
@@ -22,9 +22,11 @@ pub mod flat;
|
||||
pub mod history;
|
||||
pub mod import;
|
||||
mod measure;
|
||||
mod ops;
|
||||
|
||||
pub use check::{CheckLevel, CheckReport};
|
||||
pub use measure::{MEASURE_ACCURACY, MassProperties};
|
||||
pub use ops::{BooleanOp, MAX_POINTS};
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -33,8 +35,14 @@ pub enum Error {
|
||||
Status(i32),
|
||||
/// An argument the C API rejected before SMLib saw it.
|
||||
Argument,
|
||||
/// SMLib reported success but returned no body.
|
||||
/// SMLib reported success but returned no body (for a boolean: the result is empty).
|
||||
Empty,
|
||||
/// SMLib reported success, but a solid input gave a result that is not a closed solid.
|
||||
NotSolid,
|
||||
/// Selection point `point` is farther than the tolerance from every edge (face).
|
||||
NoMatch { point: usize },
|
||||
/// Selection point `point` is equally near to two edges (faces).
|
||||
Ambiguous { point: usize },
|
||||
/// 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.
|
||||
@@ -52,6 +60,9 @@ impl std::fmt::Display for Error {
|
||||
Error::Status(s) => write!(f, "SMLib failed (status {s})"),
|
||||
Error::Argument => write!(f, "invalid argument for SMLib"),
|
||||
Error::Empty => write!(f, "SMLib returned an empty body"),
|
||||
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::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"),
|
||||
@@ -68,6 +79,7 @@ fn error(s: i32) -> Error {
|
||||
match s {
|
||||
ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument,
|
||||
ffi::ERR_EMPTY => Error::Empty,
|
||||
ffi::ops::ERR_NOT_SOLID => Error::NotSolid,
|
||||
ffi::ERR_EXCEPTION => Error::Exception,
|
||||
ffi::NOT_BUILT => Error::NotBuilt,
|
||||
s => Error::Status(s),
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! SMLib's modelling operations: booleans, fillet, chamfer and shell. 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
|
||||
//! edge (face) nearest to it, which must lie within `tolerance` of it and be nearer than any other
|
||||
//! by more than the body's modelling tolerance ([`Error::NoMatch`], [`Error::Ambiguous`]).
|
||||
|
||||
use crate::{Brep, Error, Result, error, ffi, init};
|
||||
|
||||
/// How [`Brep::boolean`] combines two bodies.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BooleanOp {
|
||||
Union,
|
||||
/// The first body minus the second.
|
||||
Subtract,
|
||||
Intersect,
|
||||
}
|
||||
|
||||
/// Most points one selection may hold (the C API's limit).
|
||||
pub const MAX_POINTS: usize = 4096;
|
||||
|
||||
fn pick_error((s, at): ffi::ops::PickError) -> Error {
|
||||
match (s, at) {
|
||||
(ffi::ops::ERR_PICK_NONE, Some(point)) => Error::NoMatch { point },
|
||||
(ffi::ops::ERR_PICK_AMBIGUOUS, Some(point)) => Error::Ambiguous { point },
|
||||
(s, _) => error(s),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
/// as one body, counted by [`Brep::solid_count`]. Tagged operands ([`Brep::tagged`]) give a
|
||||
/// result whose [`Brep::history`] says where each face and edge came from.
|
||||
pub fn boolean(&self, other: &Brep, op: BooleanOp) -> Result<Option<Brep>> {
|
||||
init()?;
|
||||
let op = match op {
|
||||
BooleanOp::Union => ffi::ops::BOOLEAN_UNION,
|
||||
BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT,
|
||||
BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT,
|
||||
};
|
||||
match ffi::ops::boolean(&self.0, &other.0, op) {
|
||||
Ok(h) => Ok(Some(Brep(h))),
|
||||
Err(ffi::ERR_EMPTY) => Ok(None),
|
||||
Err(s) => Err(error(s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of separate solid regions (voids not counted).
|
||||
pub fn solid_count(&self) -> Result<usize> {
|
||||
ffi::ops::solid_count(&self.0).map(|n| usize::try_from(n).unwrap_or(0)).map_err(error)
|
||||
}
|
||||
|
||||
/// The edges nearest to `edges` (each within `tolerance`), with their tangent chains, rounded
|
||||
/// at `radius`.
|
||||
pub fn fillet(&self, edges: &[[f64; 3]], tolerance: f64, radius: f64) -> Result<Brep> {
|
||||
self.pick_op(ffi::ops::Pick::Fillet, edges, tolerance, radius, false)
|
||||
}
|
||||
|
||||
/// The edges nearest to `edges` (each within `tolerance`), with their tangent chains, cut by
|
||||
/// an equal-distance chamfer `distance` back along each face (measured at each edge's
|
||||
/// midpoint), with bevelled corners.
|
||||
pub fn chamfer(&self, edges: &[[f64; 3]], tolerance: f64, distance: f64) -> Result<Brep> {
|
||||
self.pick_op(ffi::ops::Pick::Chamfer, edges, tolerance, distance, false)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
ffi::ops::pick_op(kind, &self.0, points, tolerance, size).map(Brep).map_err(pick_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ops_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,242 @@
|
||||
//! The operations against analytic volumes and topology counts (built with SMLib only). `SMLIB_OPS_SHOTS=<dir>` also
|
||||
//! writes every result there as a native `.smb`, to render.
|
||||
#![cfg(feature = "native")]
|
||||
|
||||
use super::*;
|
||||
use crate::CheckLevel;
|
||||
|
||||
/// Relative accuracy of SMLib's volume integration in these tests.
|
||||
const ACC: f64 = 1e-7;
|
||||
|
||||
fn cube(a: f64) -> Brep {
|
||||
Brep::make_box([0.0; 3], [a; 3]).unwrap()
|
||||
}
|
||||
|
||||
fn volume(b: &Brep) -> f64 {
|
||||
b.volume(ACC).unwrap()
|
||||
}
|
||||
|
||||
fn assert_close(got: f64, want: f64, rel: f64, what: &str) {
|
||||
assert!((got - want).abs() <= rel * want.abs(), "{what}: {got} vs analytic {want} (rel {:.3e})", (got - want).abs() / want.abs());
|
||||
}
|
||||
|
||||
/// SMLib's own check, every test, finds nothing wrong with `b`; then, when `SMLIB_OPS_SHOTS` is
|
||||
/// set, `b` is written there as `<name>.smb`.
|
||||
fn shot(b: &Brep, name: &str) {
|
||||
let report = b.check(CheckLevel::All).unwrap();
|
||||
assert!(report.is_valid(), "{name}: {} failed validity tests:\n{}", report.failures, report.text);
|
||||
if let Some(dir) = std::env::var_os("SMLIB_OPS_SHOTS") {
|
||||
let dir = std::path::PathBuf::from(dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
b.write(&dir.join(format!("{name}.smb")), false).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// The midpoints of the 12 edges of the cube [0, a]³.
|
||||
fn cube_edge_midpoints(a: f64) -> Vec<[f64; 3]> {
|
||||
let h = a / 2.0;
|
||||
let mut v = Vec::new();
|
||||
for (p, q) in [(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)] {
|
||||
v.push([h, p, q]);
|
||||
v.push([p, h, q]);
|
||||
v.push([p, q, h]);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// The inputs of an operation are left as they were.
|
||||
fn assert_untouched(b: &Brep, vol: f64, counts: [usize; 3]) {
|
||||
assert_close(volume(b), vol, 1e-9, "input volume");
|
||||
assert_eq!(b.counts().unwrap(), counts, "input counts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_of_overlapping_boxes() {
|
||||
let a = cube(20.0);
|
||||
let b = Brep::make_box([10.0, 5.0, 5.0], [20.0, 10.0, 10.0]).unwrap();
|
||||
let u = a.boolean(&b, BooleanOp::Union).unwrap().unwrap();
|
||||
shot(&u, "union");
|
||||
// 20³ + 20·10·10 − the 10·10·10 overlap.
|
||||
assert_close(volume(&u), 9000.0, 1e-9, "union volume");
|
||||
// A's 6 faces (+x now with a hole) and B's 5 outside it; A's 12 edges, B's 8 outside and the
|
||||
// 4 where B passes through +x; A's 8 vertices and B's 8 outside.
|
||||
assert_eq!(u.counts().unwrap(), [11, 24, 16]);
|
||||
assert_eq!(u.solid_count().unwrap(), 1);
|
||||
assert_untouched(&a, 8000.0, [6, 12, 8]);
|
||||
assert_untouched(&b, 2000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtract_cuts_a_slot() {
|
||||
let a = cube(20.0);
|
||||
// A 10 mm wide, 10 mm deep slot across the top, from y = −1 to 21.
|
||||
let tool = Brep::make_box([5.0, -1.0, 10.0], [10.0, 22.0, 15.0]).unwrap();
|
||||
let s = a.boolean(&tool, BooleanOp::Subtract).unwrap().unwrap();
|
||||
shot(&s, "subtract_slot");
|
||||
assert_close(volume(&s), 8000.0 - 10.0 * 20.0 * 10.0, 1e-9, "slot volume");
|
||||
// A prism over a U of 8 corners: 8 sides + 2 ends, 24 edges, 16 vertices.
|
||||
assert_eq!(s.counts().unwrap(), [10, 24, 16]);
|
||||
assert_eq!(s.solid_count().unwrap(), 1);
|
||||
assert_untouched(&a, 8000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_keeps_the_overlap() {
|
||||
let a = cube(20.0);
|
||||
let b = Brep::make_box([10.0, 5.0, 5.0], [20.0, 10.0, 10.0]).unwrap();
|
||||
let i = a.boolean(&b, BooleanOp::Intersect).unwrap().unwrap();
|
||||
shot(&i, "intersect");
|
||||
assert_close(volume(&i), 1000.0, 1e-9, "intersection volume");
|
||||
assert_eq!(i.counts().unwrap(), [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_split_boolean_results() {
|
||||
let a = cube(10.0);
|
||||
let far = Brep::make_box([50.0, 0.0, 0.0], [10.0, 10.0, 10.0]).unwrap();
|
||||
assert!(a.boolean(&far, BooleanOp::Intersect).unwrap().is_none(), "disjoint intersection");
|
||||
let big = Brep::make_box([-5.0; 3], [20.0; 3]).unwrap();
|
||||
assert!(a.boolean(&big, BooleanOp::Subtract).unwrap().is_none(), "subtracting an enclosing body");
|
||||
let two = a.boolean(&far, BooleanOp::Union).unwrap().unwrap();
|
||||
shot(&two, "union_disjoint");
|
||||
assert_eq!(two.solid_count().unwrap(), 2, "a union of disjoint boxes is two solids in one body");
|
||||
assert_close(volume(&two), 2000.0, 1e-9, "disjoint union volume");
|
||||
// A body through a slot splits in two.
|
||||
let bar = cube(20.0);
|
||||
let cut = Brep::make_box([8.0, -1.0, -1.0], [4.0, 22.0, 22.0]).unwrap();
|
||||
let halves = bar.boolean(&cut, BooleanOp::Subtract).unwrap().unwrap();
|
||||
shot(&halves, "subtract_split");
|
||||
assert_eq!(halves.solid_count().unwrap(), 2);
|
||||
assert_close(volume(&halves), 8000.0 - 4.0 * 400.0, 1e-9, "split volume");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_every_edge_of_a_cube() {
|
||||
let (a, r) = (20.0, 2.0);
|
||||
let c = cube(a);
|
||||
let f = c.fillet(&cube_edge_midpoints(a), 0.1, r).unwrap();
|
||||
shot(&f, "fillet_all_edges");
|
||||
// The rounded cube is the Minkowski sum of the inner cube b = a − 2r with a ball of radius r:
|
||||
// b³ (core) + 6·b²·r (face slabs) + 12·(π r²/4)·b (quarter cylinders on the edges)
|
||||
// + 8·(π r³/6) (sphere octants at the corners) = 7804.6961… for a = 20, r = 2.
|
||||
let b = a - 2.0 * r;
|
||||
let pi = std::f64::consts::PI;
|
||||
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * pi * r * r * b + 4.0 / 3.0 * pi * r.powi(3);
|
||||
let got = volume(&f);
|
||||
eprintln!("filleted cube: volume {got} (analytic {want}), counts {:?}", f.counts().unwrap());
|
||||
assert_close(got, want, 1e-6, "rounded cube volume");
|
||||
// 6 planar faces, 12 edge rounds, 8 corner patches.
|
||||
assert_eq!(f.counts().unwrap()[0], 26);
|
||||
assert_untouched(&c, 8000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chamfer_one_edge() {
|
||||
let c = cube(20.0);
|
||||
// The edge y = 20, z = 20, set back 3 mm along both faces: a prism of 3·3/2 × 20 goes.
|
||||
let ch = c.chamfer(&[[10.0, 20.0, 20.0]], 0.1, 3.0).unwrap();
|
||||
shot(&ch, "chamfer_one_edge");
|
||||
assert_close(volume(&ch), 8000.0 - 4.5 * 20.0, 1e-9, "chamfered volume");
|
||||
// One face more; the edge becomes two edges joined by two new ones; 2 vertices more.
|
||||
assert_eq!(ch.counts().unwrap(), [7, 15, 10]);
|
||||
assert_untouched(&c, 8000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_a_box_open_at_the_top() {
|
||||
let c = cube(20.0);
|
||||
let top = [[10.0, 10.0, 20.0]];
|
||||
let inward = c.shell(&top, 0.1, -2.0).unwrap();
|
||||
shot(&inward, "shell_inward");
|
||||
// The cavity is 16 × 16 × 18 (the floor is 2 thick, the top open).
|
||||
assert_close(volume(&inward), 8000.0 - 16.0 * 16.0 * 18.0, 1e-9, "inward shell volume");
|
||||
// Outside 5, inside 5, the rim 1.
|
||||
assert_eq!(inward.counts().unwrap()[0], 11);
|
||||
let outward = c.shell(&top, 0.1, 2.0).unwrap();
|
||||
shot(&outward, "shell_outward");
|
||||
// The outside grows to [−2, 22]² × [−2, 20] around the original box, which becomes the cavity.
|
||||
assert_close(volume(&outward), 24.0 * 24.0 * 22.0 - 8000.0, 1e-9, "outward shell volume");
|
||||
let closed = c.shell(&[], 0.1, -2.0).unwrap();
|
||||
shot(&closed, "shell_closed");
|
||||
assert_close(volume(&closed), 8000.0 - 16.0f64.powi(3), 1e-9, "closed hollow volume");
|
||||
assert_untouched(&c, 8000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_follows_a_tangent_chain() {
|
||||
// Round the vertical edge x = y = 20 first (r = 5): the top rim is then line, arc, line,
|
||||
// meeting tangentially.
|
||||
let c = cube(20.0);
|
||||
let round = c.fillet(&[[20.0, 20.0, 10.0]], 0.1, 5.0).unwrap();
|
||||
shot(&round, "fillet_one_edge");
|
||||
let pi = std::f64::consts::PI;
|
||||
assert_close(volume(&round), 8000.0 - (1.0 - pi / 4.0) * 25.0 * 20.0, 1e-6, "one rounded edge");
|
||||
// One point on the straight part of the rim rounds the whole chain.
|
||||
let chain = round.fillet(&[[7.5, 20.0, 20.0]], 0.1, 1.0).unwrap();
|
||||
shot(&chain, "fillet_tangent_chain");
|
||||
let arc = [15.0 + 5.0 * (pi / 4.0).cos(), 15.0 + 5.0 * (pi / 4.0).sin(), 20.0];
|
||||
let all = round.fillet(&[[7.5, 20.0, 20.0], arc, [20.0, 7.5, 20.0]], 0.1, 1.0).unwrap();
|
||||
shot(&all, "fillet_chain_picked");
|
||||
assert_eq!(chain.counts().unwrap(), all.counts().unwrap(), "one pick = the whole chain");
|
||||
assert_close(volume(&chain), volume(&all), 1e-9, "one pick = the whole chain");
|
||||
assert_eq!(chain.counts().unwrap()[0], round.counts().unwrap()[0] + 3, "three rounds on the chain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_inputs_are_errors() {
|
||||
let c = cube(20.0);
|
||||
let mid = [[10.0, 20.0, 20.0]];
|
||||
for r in [f64::NAN, f64::INFINITY, -1.0, 0.0, 1e7] {
|
||||
assert_eq!(c.fillet(&mid, 0.1, r).unwrap_err(), Error::Argument, "radius {r}");
|
||||
assert_eq!(c.chamfer(&mid, 0.1, r).unwrap_err(), Error::Argument, "distance {r}");
|
||||
}
|
||||
for t in [f64::NAN, 0.0, -1.0, f64::INFINITY] {
|
||||
assert_eq!(c.fillet(&mid, t, 1.0).unwrap_err(), Error::Argument, "tolerance {t}");
|
||||
}
|
||||
assert_eq!(c.fillet(&[[f64::NAN, 0.0, 0.0]], 0.1, 1.0).unwrap_err(), Error::Argument);
|
||||
assert_eq!(c.fillet(&[], 0.1, 1.0).unwrap_err(), Error::Argument);
|
||||
assert_eq!(c.fillet(&vec![[10.0, 20.0, 20.0]; MAX_POINTS + 1], 0.1, 1.0).unwrap_err(), Error::Argument);
|
||||
// A second point far from every edge.
|
||||
assert_eq!(c.fillet(&[mid[0], [100.0, 100.0, 100.0]], 0.1, 1.0).unwrap_err(), Error::NoMatch { point: 1 });
|
||||
assert_eq!(c.shell(&[[10.0, 10.0, 25.0]], 0.1, -1.0).unwrap_err(), Error::NoMatch { point: 0 });
|
||||
// A corner is equally near three edges, an edge equally near two faces.
|
||||
assert_eq!(c.fillet(&[[20.0, 20.0, 20.0]], 0.1, 1.0).unwrap_err(), Error::Ambiguous { point: 0 });
|
||||
assert_eq!(c.shell(&mid, 0.1, -1.0).unwrap_err(), Error::Ambiguous { point: 0 });
|
||||
for t in [0.0, f64::NAN, -1e7] {
|
||||
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)),
|
||||
] {
|
||||
match r {
|
||||
Ok(b) => panic!("{what}: SMLib built a body of volume {:?}, counts {:?}", b.volume(ACC), b.counts()),
|
||||
Err(e) => eprintln!("{what}: {e:?}"),
|
||||
}
|
||||
}
|
||||
assert_untouched(&c, 8000.0, [6, 12, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boolean_history_through_tagged_operands() {
|
||||
// A slot across the top of the cube: the cube's 6 faces give 7 result faces (the top is cut
|
||||
// in two), the tool gives the slot's 2 walls and floor; no face is new.
|
||||
let a = cube(20.0).tagged(0).unwrap();
|
||||
let tool = Brep::make_box([5.0, -1.0, 10.0], [10.0, 22.0, 15.0]).unwrap().tagged(1).unwrap();
|
||||
let s = a.boolean(&tool, BooleanOp::Subtract).unwrap().unwrap();
|
||||
shot(&s, "subtract_slot_tagged");
|
||||
let h = s.history().unwrap();
|
||||
assert_eq!(h.faces.len(), 10);
|
||||
let mut from = [0usize; 2];
|
||||
for f in &h.faces {
|
||||
assert!(!f.origins.is_empty(), "{f:?}");
|
||||
for &(operand, _) in &f.origins {
|
||||
from[operand.min(1)] += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(from, [7, 3], "{h:?}");
|
||||
// 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:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user