//! #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) -> 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}"); } /// #1668: a box shelled outward 2 mm, open at the bottom. R5 on its top outer edge alone would /// reach past the sharp inside corner 2 mm away; with the inside edge rounded in the same /// fillet, that corner is filled, and the two build as one feature as they do one after the /// other: the outer round takes (1 - π/4) · 25 mm² along 104 mm, the inner one adds it along /// 100 mm. #[test] fn rounds_on_both_sides_of_a_wall_build_as_one_fillet() { let outer = box_solid(Vec3::new(-52.0, -32.0, 0.0), Vec3::new(52.0, 32.0, 42.0)).unwrap(); let shelled = cut(&outer, [-50.0, -30.0, -1.0], [50.0, 30.0, 40.0]); let (out_edge, in_edge) = (Vec3::new(0.0, 32.0, 42.0), Vec3::new(0.0, 30.0, 40.0)); let m = too_large(crate::fillet(&shelled, &[out_edge], 5.0)); assert!(m.contains("runs past the faces beside the edge"), "{m}"); let both = crate::fillet(&shelled, &[out_edge, in_edge], 5.0).unwrap(); assert!(both.validity().is_empty(), "{:?}", both.validity()); let section = (1.0 - std::f64::consts::FRAC_PI_4) * 25.0; let want = shelled.volume_at(0.01).unwrap() - section * 104.0 + section * 100.0; let v = crate::measure(&both).unwrap().volume; assert!((v - want).abs() < 1e-4 * want, "{v} vs {want}"); let stepped = crate::fillet(&crate::fillet(&shelled, &[in_edge], 5.0).unwrap(), &[out_edge], 5.0).unwrap(); assert!((crate::measure(&stepped).unwrap().volume - v).abs() < 1e-4 * want); // Past the wall's own room they still cross: R7's outer round reaches the inner one's arc. let m = too_large(crate::fillet(&shelled, &[out_edge, in_edge], 7.0)); assert!(m.contains("the largest radius that fits"), "{m}"); }