mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user