Files
OneCAD/crates/kernel/src/issue_1080_tests.rs
T
bherbruckandClaude Opus 5.5 563bcaff5b 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>
2026-10-14 11:46:44 -04:00

97 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! #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}");
}