kernel: an SMLib shell opens a round wall whole; split pieces press on either kernel

The SMLib shell passed the user's pick points to SMLib, which refuses a point as near to two
faces; a pick on a hole's wall at the line between its two halves (kernel-cases shell/4010)
failed. The open faces are now picked as truck's shell picks them (the face of the nearest
triangle), grown by the faces on the same surface joined to them (offset::with_same_surface:
a round wall stored in pieces opens whole), and SMLib gets one point inside each.

Offset faces keeps its design on both kernels: when moving the faces fails on a body with
split lines, the chosen pieces are pressed (swept and joined or cut) instead, as truck's path
always did; the booleans of the press run on the selected kernel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 08:13:26 -04:00
parent 82865e2832
commit 6bfbe1041f
3 changed files with 53 additions and 8 deletions
+15
View File
@@ -537,6 +537,21 @@ mod smlib {
assert!(e.to_string().contains("no room"), "{e}");
}
/// A hole's wall is stored as two halves; a pick on the line between them opens the whole
/// wall (kernel-cases shell/4010). The cavity is the plate inset by 2.5 less the hole's
/// cylinder, open to the hole.
#[test]
fn shell_open_at_a_hole_wall_opens_the_whole_wall() {
let hole = Loop2::circle(Vec2::new(20.0, 15.0), 5.0);
let plate = extrude(&Plane::XY, &[Region2 { outer: square(0.0, 0.0, 40.0, 30.0), holes: vec![hole] }], 0.0, 12.5).unwrap().pop().unwrap();
let r = offset_backend::shell(&plate, &[Vec3::new(25.0, 15.0, 6.25)], 2.5).unwrap();
let pi = std::f64::consts::PI;
let want = (15000.0 - pi * 25.0 * 12.5) - (35.0 * 25.0 * 7.5 - pi * 25.0 * 7.5);
// The measure is a mesh volume (within 1e-5 of the true one on this round hole).
assert!((volume(&r) - want).abs() < want * 1e-5, "{} vs {want}", volume(&r));
verified(&r, &plate, reach(2.5 * 1.5));
}
#[test]
fn offset_moves_a_face() {
let b = ell();
+32 -3
View File
@@ -73,14 +73,19 @@ fn run(name: &str, b: &Body, op: impl FnOnce(&Brep) -> std::result::Result<Brep,
Ok((body, history))
}
/// [`crate::shell`] through SMLib: walls of `thickness` inside, open at the faces nearest to
/// `open`.
/// [`crate::shell`] through SMLib: walls of `thickness` inside, open at the faces under the
/// points `open` (the face of the nearest triangle, as truck's shell picks them) and the faces
/// on the same surface joined to them: a round wall stored in pieces (a hole's two halves) opens
/// whole. SMLib is given one point inside each, never the pick itself, which may lie on the
/// line between two pieces.
pub(crate) fn shell(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
if !(thickness.is_finite() && thickness > 1e-6) {
return Err(KernelError::Invalid("shell thickness must be positive".into()));
}
let picked = faces_under(b, open)?;
let opened = crate::offset::with_same_surface(b, &picked);
let tol = pick_tolerance(b);
let pts = points(open);
let pts = points(&face_points(b, &opened)?);
let (r, h) = run("shell", b, |s| s.shell(&pts, tol, -thickness))?;
let h = h.and_then(|h| with_offsets(&r, b, h, thickness));
Ok(r.with_checked_history(h, &record::counts(&[b])))
@@ -118,6 +123,30 @@ pub(crate) fn thicken(b: &Body, thickness: f64, symmetric: bool) -> Result<Body>
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);
let mesh = b.tessellate((b.size() * 1e-3).max(1e-3))?;
let mut out: Vec<usize> = Vec::new();
for p in at {
let near = mesh
.triangles
.iter()
.zip(&mesh.tri_face)
.filter_map(|(t, f)| mesh.tri(t).map(|[x, y, z]| (crate::boolean_guard::point_triangle_dist(*p, x, y, z), *f as usize)))
.min_by(|x, y| x.0.total_cmp(&y.0));
match near {
Some((d, f)) if d < reach => {
if !out.contains(&f) {
out.push(f);
}
}
_ => return Err(KernelError::Invalid(format!("no face at {:?}", [p.x, p.y, p.z]))),
}
}
Ok(out)
}
/// One point inside each face `faces` of `b`: the centroid of its largest triangle.
fn face_points(b: &Body, faces: &[usize]) -> Result<Vec<Vec3>> {
let mesh = b.tessellate((b.size() * 1e-3).max(1e-3))?;
+6 -5
View File
@@ -879,11 +879,12 @@ pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
let run = |body: &Body| -> Result<Body> {
let healed = crate::boolean_record::healed(body)?.with_split_lines(body.splits.clone());
let chosen = faces_at(&healed, at)?;
#[cfg(feature = "smlib")]
if crate::backend()? == crate::Backend::Smlib {
return crate::offset_backend::offset_faces(&healed, &chosen, distance).map(|r| record::after(r, &healed, &record::counts(&[body])));
}
match offset_planar(&healed, |fi, _| if chosen.contains(&fi) { distance } else { 0.0 }) {
let moved = match crate::backend()? {
#[cfg(feature = "smlib")]
crate::Backend::Smlib => crate::offset_backend::offset_faces(&healed, &chosen, distance),
_ => offset_planar(&healed, |fi, _| if chosen.contains(&fi) { distance } else { 0.0 }),
};
match moved {
Ok(r) => Ok(record::after(r, &healed, &record::counts(&[body]))),
// A piece of a split face (its neighbour stays): the piece swept along its normal
// joins on (outward) or cuts in, with walls along the split lines.