//! Outlines that touch themselves: a circle tangent to a side, a corner on an edge, corners //! meeting. Joins over such a contact fuse into one solid, cuts that leave pieces meeting only //! along a line give separate shells, and near-tangent numbers from the sketch solver (a //! circle 1e-13 mm past the side) behave like exact ones. use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3}; use crate::*; fn prism(outer: Loop2, z0: f64, z1: f64) -> Body { extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], z0, z1).unwrap().pop().unwrap() } fn poly(pts: &[[f64; 2]]) -> Loop2 { Loop2::polygon(&pts.iter().map(|p| Vec2::new(p[0], p[1])).collect::>()) } fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Loop2 { poly(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) } fn disc(cx: f64, cy: f64, r: f64) -> Loop2 { Loop2::circle(Vec2::new(cx, cy), r) } fn assert_solid(r: &Body, volume: f64, rel: f64, shells: usize, what: &str) { let v = r.tessellate(1e-4).unwrap().measure().volume; assert!((v - volume).abs() <= volume * rel, "{what}: volume {v} vs {volume}"); assert_eq!(r.shell_count(), shells, "{what}: shells"); assert!(r.validity().is_empty(), "{what}: {:?}", r.validity()); } /// The reported case: a disc on a 31 × 29 block, tangent to its back side and overhanging the /// other three, joined exactly, whether the tangency is exact or the solver's (1.3e-13 past). /// The solver's numbers used to fail every route: a split line traced on the disc's face ended /// one chord's sag short of the rim and was dropped, so the contact faces never matched. #[test] fn disc_tangent_to_a_side_and_overhanging_joins() { let block = prism(rect(0., 0., 31., 29.), 0., 10.); for (cy, r) in [(4.0, 25.0), (4.392830461839724, 24.607169538160402), (4.0, 25.0 + 1e-9), (4.0, 25.0 - 1e-9)] { let top = prism(disc(18.5, cy, r), 10., 20.); let want = 8990.0 + std::f64::consts::PI * r * r * 10.0; for (a, b) in [(&block, &top), (&top, &block)] { let (res, d) = capture_boolean_diagnostics(|| boolean(a, b, BoolOp::Union)); let res = res.unwrap().unwrap(); assert_solid(&res, want, 1e-5, 1, &format!("disc at {cy}, r {r}")); assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}"); } } } /// A wall splitting a disc's face where the rim crosses it between two chords of the trace /// mesh: the split happens however the mesh falls. #[test] fn split_line_ending_past_the_last_chord_still_splits() { let top = prism(disc(18.5, 4.392830461839724, 24.607169538160402), 10., 20.); let bottom = (0..top.face_count()).find(|i| matches!(face_surface(&top, *i), Some(FaceSurface::Plane { normal, .. }) if normal.z < -0.9)).unwrap(); let wall = SplitTool::Plane(Plane { origin: Vec3::ZERO, x: Vec3::X, y: Vec3::Z }); let (split, _) = split_faces_with_map(&top, &[bottom], &wall).unwrap(); assert_eq!(split.face_count(), top.face_count() + 1); } /// Lines only: a triangle with its apex on the square's back edge, its base overhanging, and a /// diamond with its corners on the square's edge midpoints, each standing on the square. #[test] fn corner_on_an_edge_joins() { let square = prism(rect(0., 0., 20., 20.), 0., 10.); let triangle = prism(poly(&[[-5., -5.], [25., -5.], [10., 20.]]), 10., 20.); let diamond = prism(poly(&[[10., 0.], [20., 10.], [10., 20.], [0., 10.]]), 10., 20.); for (tool, want) in [(&triangle, 4000.0 + 3750.0), (&diamond, 4000.0 + 2000.0)] { let res = boolean(&square, tool, BoolOp::Union).unwrap().unwrap(); assert_solid(&res, want, 1e-9, 1, "corner on an edge"); } } /// A cut leaving pieces that meet only along a line gives one shell per piece, not one shell /// pinched along that line: a diamond cut through a square leaves four corner posts, a disc /// tangent to the block's back side two. #[test] fn cut_leaving_pieces_meeting_along_a_line_gives_separate_shells() { let square = prism(rect(0., 0., 20., 20.), 0., 10.); let diamond = prism(poly(&[[10., 0.], [20., 10.], [10., 20.], [0., 10.]]), -1., 11.); let res = boolean(&square, &diamond, BoolOp::Cut).unwrap().unwrap(); assert_solid(&res, 2000.0, 1e-9, 4, "diamond through a square"); let block = prism(rect(0., 0., 31., 29.), 0., 10.); let tool = prism(disc(18.5, 4., 25.), -1., 11.); let res = boolean(&block, &tool, BoolOp::Cut).unwrap().unwrap(); // Material left: the block's area outside the disc, as two corner posts 10 mm high. let n = 400_000; let dx = 31.0 / n as f64; let outside: f64 = (0..n) .map(|i| { let x = (i as f64 + 0.5) * dx - 18.5; let h = (625.0 - x * x).max(0.0).sqrt(); (29.0 - (4.0 + h).min(29.0)).max(0.0) * dx }) .sum(); assert_solid(&res, outside * 10.0, 1e-4, 2, "disc tangent to the back side, cut through"); } /// A hole tangent to a side from inside is one piece (the material goes round the hole), and a /// disc standing on a larger one, tangent to its rim from inside, joins. #[test] fn hole_touching_the_outline_stays_one_piece() { let block = prism(rect(0., 0., 31., 29.), 0., 10.); let hole = prism(disc(5., 14.5, 5.), -1., 11.); let res = boolean(&block, &hole, BoolOp::Cut).unwrap().unwrap(); assert_solid(&res, (899.0 - std::f64::consts::PI * 25.0) * 10.0, 1e-5, 1, "hole tangent to a side"); let small = prism(disc(-2., 0., 10.), 0., 10.); let big = prism(disc(0., 0., 12.), 10., 20.); let res = boolean(&small, &big, BoolOp::Union).unwrap().unwrap(); assert_solid(&res, std::f64::consts::PI * (100.0 + 144.0) * 10.0, 1e-5, 1, "disc tangent inside a larger one"); }