kernel: draft through SMLib

With SOLIDCRAFT_KERNEL=smlib, kernel::draft hands the picked faces (a point inside each,
from the face under each pick) to Brep::draft, with SMLib's own history: the tilted faces are
the faces themselves (Modified). Faces SMLib can't draft (a round wall, a face not parallel to
the pull) are "not supported yet", never truck. Tested in modify_history_tests: a 40 x 30 x 20
block's side at 5 degrees removes 20 x 20 tan 5 deg / 2 x 30, with its history checked against
the geometry.

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 523270fe5e
commit 02bfd6e10d
3 changed files with 56 additions and 0 deletions
+22
View File
@@ -571,6 +571,28 @@ mod smlib {
assert!(e.to_string().contains("no room"), "{e}");
}
#[test]
fn draft_tilts_a_side_and_keeps_its_name() {
let b = cube();
let side = Vec3::new(40.0, 15.0, 10.0);
let deg = std::f64::consts::PI / 180.0;
let r = offset_backend::draft(&b, &[side], &Plane::XY, Vec3::Z, 5.0 * deg).unwrap();
// The side leans in 20 tan 5 deg at the top: a wedge of 20 x 20 tan 5 deg / 2 x 30 comes off.
let wedge = 0.5 * 20.0 * 20.0 * (5.0 * deg).tan() * 30.0;
assert!((volume(&r) - (24000.0 - wedge)).abs() < 1e-3, "{}", volume(&r));
let h = verified(&r, &b, Reach { made: 0.0, moved: 2.0 });
assert_eq!(h.faces.len(), 6);
let tilted = with(h, &[face(face_at(&b, side))]);
assert_eq!(tilted.len(), 1, "{h:?}");
assert_eq!(h.faces[tilted[0]].change, Change::Modified);
// A round wall isn't flat; the top isn't parallel to the pull.
let rod = cylinder(Vec3::ZERO, Vec3::Z, 5.0, 10.0).unwrap();
let e = offset_backend::draft(&rod, &[Vec3::new(5.0, 0.0, 5.0)], &Plane::XY, Vec3::Z, 5.0 * deg).unwrap_err();
assert!(e.to_string().contains("not supported yet"), "{e}");
let e = offset_backend::draft(&b, &[Vec3::new(20.0, 15.0, 20.0)], &Plane::XY, Vec3::Z, 5.0 * deg).unwrap_err();
assert!(e.to_string().contains("not supported yet"), "{e}");
}
#[test]
fn thicken_offsets_the_surface() {
let s = crate::patch_region(&Plane::XY, &Region2 { outer: square(0.0, 0.0, 10.0, 10.0), holes: vec![] }).unwrap();
+30
View File
@@ -36,6 +36,7 @@ fn refused(op: &str, e: SmError) -> KernelError {
SmError::NoRoom => KernelError::Invalid(format!("{op}: the wall is too thick: it leaves no room inside the body")),
SmError::NoMatch { point } => KernelError::Invalid(format!("{op}: no face at selection point {}", point + 1)),
SmError::Ambiguous { point } => KernelError::Invalid(format!("{op}: selection point {} is as near to two faces", point + 1)),
SmError::Unsupported => KernelError::Failed(format!("not supported yet: SMLib {op} for this geometry")),
e => failed(op, "operation", e),
}
}
@@ -123,6 +124,35 @@ pub(crate) fn thicken(b: &Body, thickness: f64, symmetric: bool) -> Result<Body>
Ok(r.with_checked_history(h, &record::counts(&[b])))
}
/// [`crate::draft`] through SMLib: the faces under the points `at` (flat, parallel to `pull`)
/// tilted by `angle` about their lines on `neutral`, leaning in going along `pull`; the faces
/// around them extended or trimmed to meet them. A tilted face is the face itself (Modified).
/// A face SMLib can't draft (not flat, or not parallel to the pull) is "not supported yet".
pub(crate) fn draft(b: &Body, at: &[Vec3], neutral: &solidcraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 - 1e-3 {
return Err(KernelError::Invalid("draft angle must be between −90° and 90°".into()));
}
let pull = pull.normalized().ok_or_else(|| KernelError::Invalid("pull direction".into()))?;
let normal = neutral.normal().normalized().ok_or_else(|| KernelError::Invalid("neutral plane".into()))?;
if normal.dot(pull).abs() < 1e-9 {
return Err(KernelError::Failed("the neutral plane is parallel to the pull direction".into()));
}
let chosen = faces_under(b, at)?;
let tol = pick_tolerance(b);
let pts = points(&face_points(b, &chosen)?);
let o = neutral.origin;
let (r, h) = run("draft", b, |s| s.draft(&pts, tol, [o.x, o.y, o.z], [normal.x, normal.y, normal.z], [pull.x, pull.y, pull.z], angle))?;
let h = h.map(|mut h| {
for f in &mut h.faces {
if f.change == Change::Kept && f.origins.iter().any(|o| matches!(o, FaceOrigin::Face { face, .. } if chosen.contains(face))) {
f.change = Change::Modified;
}
}
h
});
Ok(r.with_checked_history(h, &record::counts(&[b])))
}
/// The face of `b` under each point: the face of the nearest triangle, within the picking reach.
fn faces_under(b: &Body, at: &[Vec3]) -> Result<Vec<usize>> {
let reach = pick_tolerance(b);
+4
View File
@@ -251,6 +251,10 @@ fn offsets(offset: &Body) -> Option<History> {
/// lean in (positive angle) going along `pull`.
pub fn draft(b: &Body, faces: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64) -> Result<Body> {
b.require_brep("draft")?;
#[cfg(feature = "smlib")]
if crate::backend()? == crate::Backend::Smlib {
return crate::offset_backend::draft(b, faces, neutral, pull, angle);
}
if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 - 1e-3 {
return Err(KernelError::Invalid("draft angle must be between −90° and 90°".into()));
}