Refuse fillets and chamfers that run past their faces or cross each other (#1080) (#1124)

* 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:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-14 11:46:44 -04:00
1 parent 6df69e9def
commit 563bcaff5b
8 files changed
+679 -46

No files matched your search

+96
View File
@@ -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}");
}