Chamfer: refuse a result that grows a convex edge's part (#1428)
* Chamfer: refuse a result that grew a convex edge's part (or shrank a concave one's) A chamfer of a curved edge that runs off a face it skims (robustness seed 57's ball rim) built a part bigger than before. Checked from the two faces' normals at the edge; rounds and unclear angles are left to the wedge bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Action matrix: the sheet metal tray's relief corners refuse a 0.25 mm chamfer Each is a 2.5 mm concave edge (the corner of a corner relief, between two planes at right angles): a chamfer there fills at most 0.25²/2 × 2.5 = 0.08 mm³. What was built took 0.95 mm³ away; the chamfer guard now refuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Robustness goldens: seed 57's wrong-way chamfer is now refused (#1390) On main with #391, seed 57's chamfer grew the part; this refuses it, so the run goes on to its later steps and ends ok. --------- Co-authored-by: Phanoob <phanoob@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
5 files changed
+134
-7
No files matched your search
@@ -389,7 +389,78 @@ pub(crate) fn accept(input: &Body, out: Body, edges: &[Vec3], size: Size) -> Res
|
||||
}
|
||||
let sel = select(input, edges)?;
|
||||
check_in(&sel, size)?;
|
||||
volume_in(&sel, input, out, size)
|
||||
let out = volume_in(&sel, input, out, size)?;
|
||||
right_way(input, out, edges, size)
|
||||
}
|
||||
|
||||
/// Whether the edge of `body` nearest `p` is convex (`Some(true)`: a chamfer takes material
|
||||
/// away) or concave (`Some(false)`: it fills), from the two faces' outward normals and the
|
||||
/// directions into each face across the edge at `p`. `None` unless that is clear-cut: faces
|
||||
/// meeting at a few degrees of tangent or of a knife edge, an edge not between two faces.
|
||||
fn convex_at(body: &Body, p: Vec3) -> Option<bool> {
|
||||
use mt::{BoundedCurve, ParametricCurve, SearchNearestParameter};
|
||||
let tol = (body.size() * 1e-4).max(1e-4);
|
||||
let (i, _) = body.nearest_edge(p, tol).ok()??;
|
||||
let edge = Body::unique_edges(&body.solid).into_iter().nth(i)?;
|
||||
let mut sides = Vec::new();
|
||||
for f in body.solid.face_iter() {
|
||||
for e in f.boundary_iters().into_iter().flatten().filter(|e| e.id() == edge.id()) {
|
||||
let c = e.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
let t = c.search_nearest_parameter(crate::body::p3(p), None, 100)?.clamp(t0.min(t1), t0.max(t1));
|
||||
let d = c.der(t);
|
||||
let dir = Vec3::new(d.x, d.y, d.z).normalized()?;
|
||||
let (_, n, _) = crate::curveblend::project(f, p, None)?;
|
||||
// A face's boundary runs counter-clockwise about its outward normal: its inside is
|
||||
// to the left of the edge.
|
||||
sides.push((n, n.cross(dir).normalized()?));
|
||||
}
|
||||
}
|
||||
let [(n1, t1), (n2, t2)] = sides[..] else { return None };
|
||||
// About 3°: the faces neither nearly tangent nor folded onto each other.
|
||||
const CLEAR: f64 = 0.05;
|
||||
if t1.dot(t2) > 1.0 - CLEAR || t1.dot(t2) < -1.0 + CLEAR {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (n1.dot(t2), n2.dot(t1));
|
||||
if a < -CLEAR && b < -CLEAR {
|
||||
Some(true)
|
||||
} else if a > CLEAR && b > CLEAR {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A chamfer of convex edges takes material away and one of concave edges fills it: a result
|
||||
/// that went the other way (a chamfer on a curved edge running off a face it skims, which the
|
||||
/// wedges can't bound) is refused rather than kept. Rounds are left to the wedges: a round
|
||||
/// rolled along a smooth chain may run on round convex and concave stretches alike.
|
||||
fn right_way(input: &Body, out: Body, edges: &[Vec3], size: Size) -> Result<Body> {
|
||||
if matches!(size, Size::Round(_)) {
|
||||
return Ok(out);
|
||||
}
|
||||
let ways: Option<Vec<bool>> = edges.iter().map(|p| convex_at(input, *p)).collect();
|
||||
let Some(ways) = ways else { return Ok(out) };
|
||||
let (convex, concave) = (ways.iter().all(|w| *w), ways.iter().all(|w| !*w));
|
||||
if !(convex || concave) {
|
||||
return Ok(out);
|
||||
}
|
||||
let (Ok(a), Ok(b)) = (crate::measure(input), crate::measure(&out)) else { return Ok(out) };
|
||||
let slack = a.volume.abs() * 1e-5 + 1e-6;
|
||||
let grew = b.volume - a.volume;
|
||||
if (convex && grew > slack) || (concave && grew < -slack) {
|
||||
return Err(KernelError::TooLarge(format!(
|
||||
"{} mm is too large for this {}: on {} edge it would {} the part by {} mm³ instead (it runs off a face next to the edge)",
|
||||
show(size.value()),
|
||||
size.what(),
|
||||
if convex { "a convex" } else { "a concave" },
|
||||
if convex { "grow" } else { "shrink" },
|
||||
// A fraction of a mm³ is typical here: to the hundredth.
|
||||
show((grew.abs() * 100.0).round() / 100.0)
|
||||
)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// After a blend of `edges` at `size` turned `input` into `out`: refused when the volume taken
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Robustness seed 57's part: a block with an arc side, a ball cut from the edge of its back
|
||||
/// face, just touching its bottom face.
|
||||
fn block_with_ball_cut() -> Body {
|
||||
let p = Vec2::new;
|
||||
let line = |a: Vec2, b: Vec2| Seg2::Line { a, b };
|
||||
let (c, r) = (p(24.0625, 11.25), 26.5625);
|
||||
let start = (p(0.0, 22.5) - c).angle();
|
||||
let sweep = ((p(0.0, 0.0) - c).angle() - start).rem_euclid(std::f64::consts::TAU);
|
||||
let segs = vec![
|
||||
line(p(0.0, 0.0), p(47.5, 0.0)),
|
||||
line(p(47.5, 0.0), p(47.5, 22.5)),
|
||||
line(p(47.5, 22.5), p(27.5, 22.5)),
|
||||
line(p(27.5, 22.5), p(27.5, 12.5)),
|
||||
line(p(27.5, 12.5), p(15.0, 12.5)),
|
||||
line(p(15.0, 12.5), p(15.0, 22.5)),
|
||||
line(p(15.0, 22.5), p(0.0, 22.5)),
|
||||
Seg2::Arc { center: c, radius: r, start, sweep },
|
||||
];
|
||||
let block = crate::extrude(&Plane::XY, &[Region2 { outer: Loop2 { segs }, holes: vec![] }], 0.0, 17.5).unwrap().remove(0);
|
||||
let ball = crate::sphere(Vec3::new(2.5, 22.5, 5.0), 5.0).unwrap();
|
||||
boolean(&block, &ball, BoolOp::Cut).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The ball's rim on the back face dips to 0.04 mm above the bottom: a 0.5 mm chamfer of it
|
||||
/// would run off the back face, and what was built grew the part (by 0.65 mm³, more with other
|
||||
/// edge orders). On a convex edge a chamfer takes material away, so it is refused.
|
||||
#[test]
|
||||
fn a_chamfer_that_would_grow_the_part_on_a_convex_edge_is_refused() {
|
||||
let body = block_with_ball_cut();
|
||||
let rim = Vec3::new(3.1526309873924174, 22.5, 0.04277569594852188);
|
||||
let e = chamfer(&body, &[rim], 0.5).expect_err("refused");
|
||||
assert!(matches!(e, KernelError::TooLarge(_)) && e.to_string().contains("convex"), "{e}");
|
||||
}
|
||||
|
||||
/// Curved convex edges that a chamfer does take material from still build: a hole's rim in a
|
||||
/// plate.
|
||||
#[test]
|
||||
fn a_chamfer_of_a_hole_rim_still_builds() {
|
||||
let plate = crate::box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(40.0, 30.0, 10.0)).unwrap();
|
||||
let hole = crate::cylinder(Vec3::new(20.0, 15.0, -1.0), Vec3::Z, 5.0, 12.0).unwrap();
|
||||
let body = boolean(&plate, &hole, BoolOp::Cut).unwrap().unwrap();
|
||||
let v0 = crate::measure(&body).unwrap().volume;
|
||||
let out = chamfer(&body, &[Vec3::new(25.0, 15.0, 10.0)], 1.0).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// A ring of a 1 mm chamfer round a 5 mm hole: (1/2)·2π·(5 + 1/3).
|
||||
let taken = v0 - crate::measure(&out).unwrap().volume;
|
||||
let want = std::f64::consts::PI * (5.0 + 1.0 / 3.0);
|
||||
assert!((taken - want).abs() < 0.02 * want, "{taken} vs {want}");
|
||||
}
|
||||
@@ -17,6 +17,8 @@ mod blend_corner;
|
||||
mod blend_fit;
|
||||
mod blend_variable;
|
||||
mod blend_vertex;
|
||||
#[cfg(test)]
|
||||
mod blend_wrong_way_tests;
|
||||
mod body;
|
||||
mod boolean_diagnostics;
|
||||
mod boolean_guard;
|
||||
|
||||
Reference in new issue
Block a user