fix(kernel): name the zero-thickness rim when a shell can't open beside a round (#316)
Shelling a body through a face that a convex round meets tangentially, with
the round at least as large as the thickness, puts the cavity wall through
the rim where the round meets the open face. Both shell strategies then fail
and the first one's generic error ("a corner whose faces don't meet the same
way after the move") was reported. The shell now says why and what to change.
Successful shells are unchanged.
Found by fuzz seed 33, which reaches this case now that a disc's two edges can
be rounded to half its thickness.
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06e517eb4e
commit
a3ff816176
3 files changed
+57
-5
No files matched your search
@@ -42,7 +42,7 @@ fn tube_centre(o: Vec3, a: Vec3, big: f64, h: f64, p: Vec3) -> Option<Vec3> {
|
||||
|
||||
impl Surf {
|
||||
/// Outward unit normal of the surface at `p` (on or near it).
|
||||
fn normal(&self, p: Vec3) -> Option<Vec3> {
|
||||
pub(crate) fn normal(&self, p: Vec3) -> Option<Vec3> {
|
||||
match *self {
|
||||
Surf::Plane { n, .. } => Some(n),
|
||||
Surf::Cylinder { o, a, convex, .. } => {
|
||||
|
||||
@@ -373,16 +373,51 @@ fn shell_planar(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
|
||||
return Err(KernelError::Invalid("select at least one face to remove".into()));
|
||||
}
|
||||
let margin = thickness + size * 0.05;
|
||||
shell_opened(b, &healed, open, &opened, thickness, margin).map_err(|e| match rim_without_thickness(&healed, &opened, thickness) {
|
||||
Some(r) => unsupported_shell(&format!(
|
||||
"the rim would have no thickness: an open face meets a round of radius {r:.4} mm tangentially, and the shell is {thickness:.4} mm thick (make the round smaller than the thickness)"
|
||||
)),
|
||||
None => e,
|
||||
})
|
||||
}
|
||||
|
||||
/// The radius of a convex round (cylinder or torus) that meets an open face tangentially and
|
||||
/// is at least `thickness`: the cavity's wall then passes through the rim where the round meets
|
||||
/// the open face, leaving nothing between them.
|
||||
fn rim_without_thickness(b: &Body, opened: &[usize], thickness: f64) -> Option<f64> {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let faces: Vec<&mt::Face> = b.solid.face_iter().collect();
|
||||
let tol = (b.size() * 1e-7).max(1e-9) * 100.0;
|
||||
opened.iter().filter_map(|fi| faces.get(*fi)).find_map(|open| {
|
||||
let plane = crate::offset::surf_of(open, tol)?;
|
||||
faces.iter().filter(|g| !std::ptr::eq(**g, *open)).find_map(|g| {
|
||||
let round = crate::offset::surf_of(g, tol)?;
|
||||
let r = match round {
|
||||
crate::offset::Surf::Cylinder { r, convex: true, .. } | crate::offset::Surf::Torus { r, convex: true, .. } => r,
|
||||
_ => return None,
|
||||
};
|
||||
let shared = g.edge_iter().find(|e| open.edge_iter().any(|x| x.id() == e.id()))?;
|
||||
let c = shared.curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
let mid = from_p3(c.subs((t0 + t1) / 2.0));
|
||||
let tangent = plane.normal(mid)?.dot(round.normal(mid)?) > 1.0 - 1e-6;
|
||||
(tangent && r >= thickness - tol).then_some(r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickness: f64, margin: f64) -> Result<Body> {
|
||||
let size = healed.size();
|
||||
// Open faces pushed out past the body: the simple way, unless a face tangent to an open
|
||||
// one (a fillet along its edge) can't follow; then every face moves in (tangencies keep)
|
||||
// and the opening is the open face's copy swept out through the body.
|
||||
let cavity = match offset_planar(&healed, |fi, _| if opened.contains(&fi) { margin } else { -thickness }) {
|
||||
let cavity = match offset_planar(healed, |fi, _| if opened.contains(&fi) { margin } else { -thickness }) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return shell_by_opening(&healed, &opened, thickness, margin).map_err(|_| e),
|
||||
Err(e) => return shell_by_opening(healed, opened, thickness, margin).map_err(|_| e),
|
||||
};
|
||||
let r = crate::ops::boolean(&healed, &cavity, crate::BoolOp::Cut);
|
||||
let r = crate::ops::boolean(healed, &cavity, crate::BoolOp::Cut);
|
||||
if r.is_err()
|
||||
&& let Ok(x) = shell_by_opening(&healed, &opened, thickness, margin)
|
||||
&& let Ok(x) = shell_by_opening(healed, opened, thickness, margin)
|
||||
{
|
||||
return Ok(x);
|
||||
}
|
||||
|
||||
@@ -1930,3 +1930,20 @@ fn shell_of_a_pocket_whose_floor_falls_to_a_drain() {
|
||||
let m = measure(&s).unwrap();
|
||||
assert!(rel(m.volume, want) < 1e-4, "{} vs {want}", m.volume);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_open_beside_a_round_as_large_as_the_wall_is_refused() {
|
||||
// Opening the bottom of a box whose bottom outline is rounded: the cavity's side walls stand
|
||||
// the thickness in from the outside, where a round at least that large meets the bottom, so
|
||||
// the rim would have no thickness. Smaller rounds leave a rim.
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 10.0)).unwrap();
|
||||
let loop_ = [Vec3::new(20.0, 0.0, 0.0), Vec3::new(40.0, 15.0, 0.0), Vec3::new(20.0, 30.0, 0.0), Vec3::new(0.0, 15.0, 0.0)];
|
||||
let bottom = [Vec3::new(20.0, 15.0, 0.0)];
|
||||
for r in [2.0, 2.4] {
|
||||
let f = fillet(&b, &loop_, r).unwrap();
|
||||
let e = shell(&f, &bottom, 2.0).unwrap_err();
|
||||
assert!(e.to_string().starts_with("the operation failed: not supported yet: the rim"), "{r}: {e}");
|
||||
}
|
||||
let f = fillet(&b, &loop_, 1.5).unwrap();
|
||||
assert!(shell(&f, &bottom, 2.0).unwrap().validity().is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user