* Fillet: refuse a result with openings in its surface (#871) A fillet on #871's part (radius 1 on a window's edges and on the sides of a block 1.26 mm away) came back closed as topology with every face meshing, but the rounds overlapped and the end faces' boundary loops crossed, so the model showed holes. The constant-radius fillet now checks its result before taking it: the body's mesh, welded within a millionth of its size, must have no edge without as many triangles running the other way along it, except for cracks within the mesh tolerance where fitted curves meet. A result that fails is refused, so the corner build gets its try and the radius search treats it as too large. Body::open_mesh_edges is the check. Kernel tests cover the #871 part (refused at radius 1, closed with a round on every edge at 0.5), and an engine test runs the fillet from the report and leaves the part as it was. All 96 robustness seeds build the same with and without it. Fixes #871 Co-Authored-By: Claude Opus 5.5 <[email protected]> * Check fillets for crossed face boundaries, not free mesh edges Free edges in the mesh also come from cracks where fitted curves meet (corner patches, rounds ending on curved edges), so the mesh test refused good fillets. The check is now on the cause of #871: a planar face whose boundary loops cross each other or themselves has no inside. Body::crossed_face_loops samples each planar face's loops within 1/1000 of the size and counts proper crossings (touching within a millionth doesn't count). The pyramid apex blend and the loft fillets build again. Ten action-matrix fillets stay refused, recorded in the baseline: r = 3 on the 2 mm rim of oracle 09's shelled box (the round runs past the inner wall; the result was 240 mm3 lighter than any round can make it), and the six seam-edge fillets on oracle 60's bolt holes, whose boundary jumps across the hole. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
61 lines
2.4 KiB
Rust
61 lines
2.4 KiB
Rust
//! #871: a fillet that opened holes in the model must be refused, not accepted.
|
|
|
|
use solidcraft_geom::Vec3;
|
|
|
|
use crate::{Body, BoolOp, build::box_solid, ops::boolean};
|
|
|
|
/// The part from #871: a plate with a narrower block on top and a window through both, filleted
|
|
/// at radius 1 along every edge of both end faces.
|
|
fn part() -> Body {
|
|
let (a, b) = (12.74437365538526, 22.69275963366936);
|
|
let base = box_solid(Vec3::new(9.5, 8.0, 0.0), Vec3::new(23.5, 29.5, 10.0)).unwrap();
|
|
let top = box_solid(Vec3::new(9.5, a, 10.0), Vec3::new(23.5, b, 20.0)).unwrap();
|
|
let joined = boolean(&base, &top, BoolOp::Union).unwrap().unwrap();
|
|
let window = box_solid(Vec3::new(9.5, 14.0, 7.0), Vec3::new(23.5, 19.2, 14.1)).unwrap();
|
|
boolean(&joined, &window, BoolOp::Cut).unwrap().unwrap()
|
|
}
|
|
|
|
fn edges() -> Vec<Vec3> {
|
|
let yz = [
|
|
(12.74437365538526, 15.0),
|
|
(10.37218682769263, 10.0),
|
|
(8.0, 5.0),
|
|
(18.75, 0.0),
|
|
(29.5, 5.0),
|
|
(26.09637981683468, 10.0),
|
|
(22.69275963366936, 15.0),
|
|
(17.71856664452731, 20.0),
|
|
(16.6, 7.0),
|
|
(14.0, 10.55),
|
|
(16.6, 14.100000000000001),
|
|
(19.200000000000003, 10.55),
|
|
];
|
|
[23.5, 9.5].iter().flat_map(|x| yz.iter().map(move |(y, z)| Vec3::new(*x, *y, *z))).collect()
|
|
}
|
|
|
|
/// Radius 1 on both the window's edges and the block's sides: the wall between them is 1.26
|
|
/// mm thick, so the rounds overlap and the end faces' boundaries cross. That result has
|
|
/// openings and is refused.
|
|
#[test]
|
|
fn overlapping_rounds_are_refused() {
|
|
let p = part();
|
|
assert_eq!(p.crossed_face_loops().unwrap(), 0);
|
|
assert!(crate::fillet(&p, &edges(), 1.0).is_err(), "a fillet with openings in its surface was accepted");
|
|
// The corner build makes that body (what #871 got): closed as topology, every face meshes,
|
|
// but the loops of its end faces cross.
|
|
let sel: Vec<(Vec3, f64)> = edges().into_iter().map(|e| (e, 1.0)).collect();
|
|
if let Ok(b) = crate::blend_corner::fillet_radii(&p, &sel) {
|
|
assert!(b.validity().is_empty() && b.crossed_face_loops().unwrap() > 0);
|
|
}
|
|
}
|
|
|
|
/// Below half the wall the rounds fit: a closed solid with a round on every edge.
|
|
#[test]
|
|
fn rounds_that_fit_are_closed() {
|
|
let p = part();
|
|
let b = crate::fillet(&p, &edges(), 0.5).unwrap();
|
|
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
|
assert_eq!(b.crossed_face_loops().unwrap(), 0);
|
|
assert_eq!(b.face_count(), p.face_count() + edges().len());
|
|
}
|