* Refuse fillets and chamfers that run past their faces or cross each other (#1080) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Blend fit: chamfers keep their old acceptance of crossed loops; skip the volume bound along tangent chains Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Blend fit: one selection per check, a coarse volume pass first; record the refused oversize blends in the action matrix baseline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6df69e9def
commit
563bcaff5b
8 files changed
+679
-46
No files matched your search
@@ -0,0 +1,96 @@
|
||||
//! #1080: fillets and chamfers that would run past the faces beside their edge (into a pocket,
|
||||
//! a hole, another blend) are refused with the largest size that fits, not built through it.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use crate::{Body, BoolOp, ChamferSide, KernelError, build::box_solid, ops::boolean};
|
||||
|
||||
fn cut(a: &Body, lo: [f64; 3], hi: [f64; 3]) -> Body {
|
||||
let t = box_solid(Vec3::new(lo[0], lo[1], lo[2]), Vec3::new(hi[0], hi[1], hi[2])).unwrap();
|
||||
boolean(a, &t, BoolOp::Cut).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// A 40 × 20 × 20 block with a 10 × 10 pocket, 10 deep, 4 mm in from its top edge at x = 40.
|
||||
fn pocketed() -> Body {
|
||||
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(40.0, 20.0, 20.0)).unwrap();
|
||||
cut(&b, [26.0, 5.0, 10.0], [36.0, 15.0, 20.0])
|
||||
}
|
||||
|
||||
const EDGE: Vec3 = Vec3 { x: 40.0, y: 10.0, z: 20.0 };
|
||||
|
||||
fn too_large(r: crate::Result<Body>) -> String {
|
||||
match r {
|
||||
Err(KernelError::TooLarge(m)) => m,
|
||||
Err(e) => panic!("refused, but not as too large: {e}"),
|
||||
Ok(_) => panic!("a blend through the pocket was accepted"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chamfer_into_a_pocket_is_refused_with_the_size_that_fits() {
|
||||
let p = pocketed();
|
||||
let m = too_large(crate::chamfer(&p, &[EDGE], 6.0));
|
||||
assert!(m.contains("runs past the faces beside the edge") && m.contains("the largest size that fits is 4 mm"), "{m}");
|
||||
// Up to the pocket's rim it builds: the triangle along the 20 mm edge.
|
||||
for s in [2.0, 4.0] {
|
||||
let b = crate::chamfer(&p, &[EDGE], s).unwrap();
|
||||
let v = b.volume_at(0.01).unwrap();
|
||||
assert!((p.volume_at(0.01).unwrap() - v - 0.5 * s * s * 20.0).abs() < 1e-6, "{s}: {v}");
|
||||
}
|
||||
// Unequal sides: along the top 6 is past the pocket, along the side it is not.
|
||||
let m = too_large(crate::chamfer_sides(&p, &[EDGE], 6.0, ChamferSide::Distance(2.0), false));
|
||||
assert!(m.contains("runs past"), "{m}");
|
||||
assert!(crate::chamfer_sides(&p, &[EDGE], 2.0, ChamferSide::Distance(6.0), false).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_into_a_pocket_is_refused_and_one_that_fits_builds() {
|
||||
let p = pocketed();
|
||||
// The round build refuses it itself; the largest radius that fits is added.
|
||||
let m = crate::fillet(&p, &[EDGE], 5.0).unwrap_err().to_string();
|
||||
assert!(m.contains("the largest radius that fits is 4 mm"), "{m}");
|
||||
let b = crate::fillet(&p, &[EDGE], 3.0).unwrap();
|
||||
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
||||
assert_eq!(b.face_count(), p.face_count() + 1);
|
||||
}
|
||||
|
||||
/// A rib 4 mm wide: chamfers of its two top edges meet in the middle of its top face at 2 mm;
|
||||
/// past that they would cross each other. Neither reaches the far side's face below 4 mm,
|
||||
/// so only the two blends clash.
|
||||
#[test]
|
||||
fn chamfers_crossing_each_other_are_refused() {
|
||||
let rib = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(40.0, 4.0, 20.0)).unwrap();
|
||||
let edges = [Vec3::new(20.0, 0.0, 20.0), Vec3::new(20.0, 4.0, 20.0)];
|
||||
let m = too_large(crate::chamfer(&rib, &edges, 3.0));
|
||||
assert!(m.contains("would cross each other") && m.contains("the largest size that fits is 2 mm"), "{m}");
|
||||
let m = crate::fillet(&rib, &edges, 3.0).unwrap_err().to_string();
|
||||
assert!(m.contains("the largest radius that fits is 2 mm"), "{m}");
|
||||
// Below half the width both build.
|
||||
let b = crate::chamfer(&rib, &edges, 1.5).unwrap();
|
||||
assert!((b.volume_at(0.01).unwrap() - (3200.0 - 2.0 * 0.5 * 1.5 * 1.5 * 40.0)).abs() < 1e-6);
|
||||
assert!(crate::fillet(&rib, &edges, 1.5).is_ok());
|
||||
}
|
||||
|
||||
/// Edges meeting at a corner mitre: their chamfers overlap there and are not refused for it.
|
||||
#[test]
|
||||
fn chamfers_meeting_at_a_corner_still_build() {
|
||||
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(20.0, 20.0, 20.0)).unwrap();
|
||||
let edges = [Vec3::new(10.0, 0.0, 20.0), Vec3::new(20.0, 10.0, 20.0), Vec3::new(10.0, 20.0, 20.0), Vec3::new(0.0, 10.0, 20.0)];
|
||||
assert!(crate::chamfer(&b, &edges, 5.0).is_ok());
|
||||
assert!(crate::fillet(&b, &edges, 5.0).is_ok());
|
||||
}
|
||||
|
||||
/// A result that changes more of the part than the blends can is refused (#871's shelled box
|
||||
/// lost 240 mm³ more than any round of its edge could take).
|
||||
#[test]
|
||||
fn a_result_that_removes_more_than_the_blend_is_refused() {
|
||||
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(20.0, 20.0, 20.0)).unwrap();
|
||||
let e = [Vec3::new(10.0, 0.0, 20.0)];
|
||||
let size = crate::blend_fit::Size::Chamfer(2.0);
|
||||
let good = crate::chamfer(&b, &e, 2.0).unwrap();
|
||||
assert!(crate::blend_fit::check_volume(&b, good, &e, size).is_ok());
|
||||
// A chamfer and a 1 mm slab off the far side as well.
|
||||
let worse = cut(&crate::chamfer(&b, &e, 2.0).unwrap(), [0.0, 19.0, 0.0], [20.0, 20.0, 20.0]);
|
||||
let m = too_large(crate::blend_fit::check_volume(&b, worse, &e, size));
|
||||
assert!(m.contains("more than the chamfer can"), "{m}");
|
||||
}
|
||||
Reference in new issue
Block a user