use std::f64::consts::PI; use solvecraft_geom::{Loop2, Plane, Region2, Vec2, Vec3}; use crate::*; fn rel(a: f64, b: f64) -> f64 { (a - b).abs() / b.abs().max(1e-12) } fn rect(w: f64, h: f64) -> Region2 { Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(w, 0.0), Vec2::new(w, h), Vec2::new(0.0, h)]), holes: vec![] } } #[test] fn box_extrude_measures() { let b = extrude(&Plane::XY, &[rect(40.0, 30.0)], 0.0, 20.0).unwrap().pop().unwrap(); let m = measure(&b).unwrap(); assert!(rel(m.volume, 24000.0) < 1e-9, "{}", m.volume); assert!(rel(m.area, 5200.0) < 1e-9); assert_eq!((m.faces, m.edges, m.vertices), (6, 12, 8)); assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (6, 12, 8)); assert!(m.centroid.dist(Vec3::new(20.0, 15.0, 10.0)) < 1e-6); } #[test] fn cylinder_measures_match_analytic() { let b = extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(Vec2::ZERO, 15.0), holes: vec![] }], 0.0, 50.0).unwrap().pop().unwrap(); let m = measure(&b).unwrap(); let v = PI * 225.0 * 50.0; let a = 2.0 * PI * 225.0 + 2.0 * PI * 15.0 * 50.0; assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume); assert!(rel(m.area, a) < 5e-4, "{} vs {a}", m.area); assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (3, 2, 2), "{:?}", m.merged); assert_eq!(m.merged.face_types.get("cylinder"), Some(&1)); } #[test] fn extrude_on_other_planes_and_offsets() { let b = extrude(&Plane::XZ, &[rect(10.0, 5.0)], -2.0, 3.0).unwrap().pop().unwrap(); let m = measure(&b).unwrap(); assert!(rel(m.volume, 250.0) < 1e-9); // XZ normal is -Y: the body spans y in [-3, 2]. assert!((m.bbox.min.y + 3.0).abs() < 1e-6 && (m.bbox.max.y - 2.0).abs() < 1e-6, "{:?}", m.bbox); } #[test] fn region_with_hole() { let r = Region2 { outer: rect(40.0, 30.0).outer, holes: vec![Loop2::circle(Vec2::new(20.0, 15.0), 5.0).reversed()] }; let b = extrude(&Plane::XY, &[r], 0.0, 10.0).unwrap().pop().unwrap(); let m = measure(&b).unwrap(); assert!(rel(m.volume, (1200.0 - PI * 25.0) * 10.0) < 5e-4, "{}", m.volume); } #[test] fn box_fillet_cut_volume() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); let f = fillet(&b, &[Vec3::new(0.0, 0.0, 10.0)], 3.0).unwrap(); let mf = measure(&f).unwrap(); let expect_f = 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0; assert!(rel(mf.volume, expect_f) < 1e-4, "{} vs {expect_f}", mf.volume); assert_eq!(mf.merged.faces, 7); let hole = cylinder(Vec3::new(20.0, 15.0, -5.0), Vec3::Z, 5.0, 30.0).unwrap(); let cut = boolean(&f, &hole, BoolOp::Cut).unwrap().unwrap(); let mc = measure(&cut).unwrap(); let expect = expect_f - PI * 25.0 * 20.0; assert!(rel(mc.volume, expect) < 1e-3, "{} vs {expect}", mc.volume); } #[test] fn union_and_intersect() { let a = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap(); let b = box_solid(Vec3::new(5.0, 5.0, 5.0), Vec3::new(15.0, 15.0, 15.0)).unwrap(); let u = boolean(&a, &b, BoolOp::Union).unwrap().unwrap(); assert!(rel(measure(&u).unwrap().volume, 2000.0 - 125.0) < 1e-6); let i = boolean(&a, &b, BoolOp::Intersect).unwrap().unwrap(); assert!(rel(measure(&i).unwrap().volume, 125.0) < 1e-6); } #[test] fn revolve_washer_and_sphere() { let r = Region2 { outer: Loop2::polygon(&[Vec2::new(10.0, 0.0), Vec2::new(15.0, 0.0), Vec2::new(15.0, 5.0), Vec2::new(10.0, 5.0)]), holes: vec![] }; let b = revolve(&Plane::XZ, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, 2.0 * PI).unwrap().pop().unwrap(); let m = measure(&b).unwrap(); let v = PI * (225.0 - 100.0) * 5.0; assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume); let half = revolve(&Plane::XZ, &[r], Vec2::ZERO, Vec2::Y, PI).unwrap().pop().unwrap(); assert!(rel(measure(&half).unwrap().volume, v / 2.0) < 5e-4); let s = sphere(Vec3::new(1.0, 2.0, 3.0), 10.0).unwrap(); let ms = measure(&s).unwrap(); assert!(rel(ms.volume, 4.0 / 3.0 * PI * 1000.0) < 1e-3, "{}", ms.volume); } #[test] fn chamfer_and_step() { let b = box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap(); let c = chamfer(&b, &[Vec3::new(20.0, 10.0, 20.0)], 2.0).unwrap(); assert!(rel(measure(&c).unwrap().volume, 8000.0 - 2.0 * 20.0) < 1e-4); let s = step_export(&[&c], "SolveCraft").unwrap(); assert!(s.starts_with("ISO-10303-21;")); assert!(s.contains("MANIFOLD_SOLID_BREP") || s.contains("CLOSED_SHELL")); } #[test] fn hostile_inputs_are_errors() { assert!(extrude(&Plane::XY, &[rect(1.0, 1.0)], 0.0, 0.0).is_err()); assert!(extrude(&Plane::XY, &[rect(1.0, 1.0)], 0.0, f64::NAN).is_err()); assert!(extrude(&Plane::XY, &[], 0.0, 1.0).is_err()); assert!(extrude(&Plane::XY, &[rect(0.0, 1.0)], 0.0, 1.0).is_err()); assert!(box_solid(Vec3::ZERO, Vec3::new(1.0, 0.0, 1.0)).is_err()); assert!(cylinder(Vec3::ZERO, Vec3::ZERO, 1.0, 1.0).is_err()); let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap(); assert!(fillet(&b, &[Vec3::new(0.0, 0.0, 5.0)], -1.0).is_err()); assert!(fillet(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_err()); assert!(fillet(&b, &[Vec3::new(0.0, 0.0, 5.0)], 100.0).is_err()); let r = Region2 { outer: Loop2::polygon(&[Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0), Vec2::new(1.0, 1.0)]), holes: vec![] }; assert!(revolve(&Plane::XZ, &[r], Vec2::ZERO, Vec2::Y, 1.0).is_err(), "crosses axis"); assert!(transform(&b, Vec3::new(f64::NAN, 0.0, 0.0), Vec3::ZERO, Vec3::Z, 0.0).is_err()); } #[test] fn fillets_on_several_edges_and_angles() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); // Two opposite vertical edges, then a horizontal top edge between them is not touching either. let f = fillet(&b, &[Vec3::new(0.0, 0.0, 10.0), Vec3::new(40.0, 30.0, 10.0)], 4.0).unwrap(); let corner = 16.0 - PI * 4.0; assert!(rel(measure(&f).unwrap().volume, 24000.0 - 2.0 * corner * 20.0) < 2e-4); let top = fillet(&b, &[Vec3::new(20.0, 0.0, 20.0)], 2.0).unwrap(); assert!(rel(measure(&top).unwrap().volume, 24000.0 - (4.0 - PI) * 40.0) < 2e-4); // Triangular prism: 60° edges. let tri = Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(30.0, 0.0), Vec2::new(15.0, 25.980762113533)]), holes: vec![] }; let p = extrude(&Plane::XY, &[tri], 0.0, 10.0).unwrap().pop().unwrap(); let r = 2.0; let fp = fillet(&p, &[Vec3::new(0.0, 0.0, 5.0)], r).unwrap(); // Removed area for interior angle φ: r²(cot(φ/2)) − r²(π − φ)/2. let phi = PI / 3.0; let removed = r * r / (phi / 2.0).tan() - r * r * (PI - phi) / 2.0; let v0 = measure(&p).unwrap().volume; assert!(rel(measure(&fp).unwrap().volume, v0 - removed * 10.0) < 2e-4, "{} vs {}", measure(&fp).unwrap().volume, v0 - removed * 10.0); // Adjacent edges at a blended corner are not supported yet (an error, not a crash). assert!(fillet(&b, &[Vec3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 15.0, 20.0)], 2.0).is_err()); // Concave edge: not supported. let u = boolean(&b, &box_solid(Vec3::new(10.0, 10.0, 15.0), Vec3::new(20.0, 20.0, 30.0)).unwrap(), BoolOp::Union).unwrap().unwrap(); assert!(fillet(&u, &[Vec3::new(15.0, 10.0, 20.0)], 1.0).is_err()); } #[test] fn tapered_extrudes() { // 40 x 40 square, 30 up, 10° outward: a frustum. let sq = Region2 { outer: Loop2::polygon(&[Vec2::new(-20.0, -20.0), Vec2::new(20.0, -20.0), Vec2::new(20.0, 20.0), Vec2::new(-20.0, 20.0)]), holes: vec![], }; let b = extrude_tapered(&Plane::XY, &sq, 30.0, 1.0, 10f64.to_radians()).unwrap(); let top = 40.0 + 2.0 * 30.0 * 10f64.to_radians().tan(); let v = 30.0 / 3.0 * (1600.0 + top * top + 40.0 * top); let m = measure(&b).unwrap(); assert!(rel(m.volume, v) < 1e-6, "{} vs {v}", m.volume); assert_eq!(m.merged.faces, 6); // Inward taper on a circle (a cone frustum), downward. let c = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![] }; let b = extrude_tapered(&Plane::XY, &c, 10.0, -1.0, -20f64.to_radians()).unwrap(); let r2 = 10.0 - 10.0 * 20f64.to_radians().tan(); let v = PI * 10.0 / 3.0 * (100.0 + r2 * r2 + 10.0 * r2); let m = measure(&b).unwrap(); assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume); assert!(m.bbox.max.z < 1e-6 && m.bbox.min.z > -10.0 - 1e-6, "{:?}", m.bbox); assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians()).is_err(), "closes the profile"); } #[test] fn polyhedra_shell_and_draft() { let b = box_solid(Vec3::ZERO, Vec3::new(50.0, 40.0, 30.0)).unwrap(); let s = shell(&b, &[Vec3::new(25.0, 20.0, 30.0)], 2.0).unwrap(); let m = measure(&s).unwrap(); assert!(rel(m.volume, 60000.0 - 46.0 * 36.0 * 28.0) < 1e-6, "{}", m.volume); assert_eq!(m.merged.faces, 11); // Draft two side faces 5° in, about the bottom, pulling up. let d = draft(&b, &[Vec3::new(0.0, 20.0, 15.0), Vec3::new(50.0, 20.0, 15.0)], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap(); let t = 30.0 * 5f64.to_radians().tan(); assert!(rel(measure(&d).unwrap().volume, 40.0 * 30.0 * (50.0 + 50.0 - 2.0 * t) / 2.0) < 1e-6, "{}", measure(&d).unwrap().volume); let tet = convex_polyhedron(&[ HalfSpace { n: Vec3::new(0.0, 0.0, -1.0), d: 0.0 }, HalfSpace { n: Vec3::new(-1.0, 0.0, 0.0), d: 0.0 }, HalfSpace { n: Vec3::new(0.0, -1.0, 0.0), d: 0.0 }, HalfSpace { n: Vec3::new(1.0, 1.0, 1.0).normalized().unwrap(), d: 6.0 / 3f64.sqrt() }, ]) .unwrap(); assert!(rel(measure(&tet).unwrap().volume, 36.0) < 1e-9); let cyl = cylinder(Vec3::ZERO, Vec3::Z, 5.0, 5.0).unwrap(); assert!(shell(&cyl, &[Vec3::new(0.0, 0.0, 5.0)], 1.0).is_err(), "curved faces"); } #[test] fn loft_and_sweep() { let sq = Loop2::polygon(&[Vec2::new(-20.0, -20.0), Vec2::new(20.0, -20.0), Vec2::new(20.0, 20.0), Vec2::new(-20.0, 20.0)]); let top = Plane::XY.offset(40.0); let l = loft(&[(Plane::XY, sq), (top, Loop2::circle(Vec2::ZERO, 10.0))]).unwrap(); let m = measure(&l).unwrap(); println!("loft volume {} faces {:?}", m.volume, m.merged); assert!(m.volume > 314.0 * 40.0 && m.volume < 1600.0 * 40.0); // A tube: up 30, quarter bend of radius 20, across 30. let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, 3.0), holes: vec![] }; let path = [ PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 30.0) }, PathSeg::Arc { a: Vec3::new(0.0, 0.0, 30.0), center: Vec3::new(20.0, 0.0, 30.0), axis: Vec3::new(0.0, 1.0, 0.0), angle: std::f64::consts::FRAC_PI_2, }, PathSeg::Line { a: Vec3::new(20.0, 0.0, 50.0), b: Vec3::new(50.0, 0.0, 50.0) }, ]; let t = sweep(&Plane::XY, &disc, &path).unwrap(); for tol in [0.1, 0.01, 0.003, 0.001] { println!("tube tess {tol}: {}", t.tessellate(tol).unwrap().measure().volume); } let area = PI * 9.0; let expect = area * (30.0 + 30.0 + 20.0 * std::f64::consts::FRAC_PI_2); assert!(rel(measure(&t).unwrap().volume, expect) < 1e-3, "{} vs {expect}", measure(&t).unwrap().volume); } #[test] fn planar_booleans_with_coincident_faces() { // L-bracket: a wall flush with three sides of its base. let base = box_solid(Vec3::ZERO, Vec3::new(80.0, 50.0, 8.0)).unwrap(); let wall = box_solid(Vec3::new(0.0, 42.0, 8.0), Vec3::new(80.0, 50.0, 50.0)).unwrap(); let u = planar_boolean(&base, &wall, BoolOp::Union).unwrap().unwrap(); let m = measure(&u).unwrap(); assert!(rel(m.volume, 80.0 * 50.0 * 8.0 + 80.0 * 8.0 * 42.0) < 1e-9, "{}", m.volume); assert_eq!(m.merged.faces, 8, "{:?}", m.merged); // Through the public boolean (B-rep first, polygon fallback). let u2 = boolean(&base, &wall, BoolOp::Union).unwrap().unwrap(); assert!(rel(measure(&u2).unwrap().volume, m.volume) < 1e-9); // Two boxes sharing a face, and a cut flush with a side. let a = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap(); let b = box_solid(Vec3::new(10.0, 0.0, 0.0), Vec3::new(20.0, 10.0, 10.0)).unwrap(); let ab = boolean(&a, &b, BoolOp::Union).unwrap().unwrap(); assert!(rel(measure(&ab).unwrap().volume, 2000.0) < 1e-9); assert_eq!(measure(&ab).unwrap().merged.faces, 6); let notch = box_solid(Vec3::new(0.0, 0.0, 5.0), Vec3::new(5.0, 10.0, 10.0)).unwrap(); let c = boolean(&a, ¬ch, BoolOp::Cut).unwrap().unwrap(); assert!(rel(measure(&c).unwrap().volume, 1000.0 - 250.0) < 1e-9); let i = boolean(&a, ¬ch, BoolOp::Intersect).unwrap().unwrap(); assert!(rel(measure(&i).unwrap().volume, 250.0) < 1e-9); } #[test] #[ignore] fn debug_convergence() { let b = extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(Vec2::ZERO, 15.0), holes: vec![] }], 0.0, 50.0).unwrap().pop().unwrap(); for t in [0.1, 0.05, 0.02, 0.01, 0.005, 0.002] { let m = b.tessellate(t).unwrap(); let mm = m.measure(); println!("tol {t}: tris {} vol {} err {:.3e}", m.triangles.len(), mm.volume, mm.volume / (PI * 225.0 * 50.0) - 1.0); } let m = b.tessellate(0.05).unwrap(); for f in 0..4u32 { let ns: Vec = m.triangles.iter().zip(&m.tri_face).filter(|(_, ff)| **ff == f).flat_map(|(t, _)| t.iter().map(|k| m.normals[*k as usize])).collect(); println!("face {f}: {} normals, first {:?}, max |n.z| {}", ns.len(), ns.first(), ns.iter().map(|n| n.z.abs()).fold(0.0, f64::max)); } } #[test] #[ignore] fn debug_coplanar() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); for (z0, z1) in [(0.0, 20.0), (-5.0, 20.0), (0.0, 25.0), (-5.0, 25.0), (5.0, 20.0), (5.0, 25.0)] { let c = cylinder(Vec3::new(20.0, 15.0, z0), Vec3::Z, 5.0, z1 - z0).unwrap(); let r = boolean(&b, &c, BoolOp::Cut); println!("cut z {z0}..{z1}: {:?}", r.map(|o| o.map(|b| measure(&b).unwrap().volume))); let r = boolean(&b, &c, BoolOp::Union); println!("union z {z0}..{z1}: {:?}", r.map(|o| o.map(|b| measure(&b).unwrap().volume))); } let r = Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0), Vec2::new(40.0, 30.0), Vec2::new(0.0, 30.0)]), holes: vec![] }; for ang in [PI / 2.0, PI, 1.5 * PI, 2.0 * PI] { let q = revolve(&Plane::XY, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, ang).unwrap().pop().unwrap(); let m = measure(&q).unwrap(); println!("revolve {ang}: vol {} expect {} faces {}", m.volume, ang / 2.0 * 1600.0 * 30.0, m.faces); } } #[test] #[ignore] fn debug_blind() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); let t = box_solid(Vec3::new(10.0, 10.0, 5.0), Vec3::new(20.0, 20.0, 25.0)).unwrap(); println!("box pocket: {:?}", boolean(&b, &t, BoolOp::Cut).map(|o| o.map(|b| measure(&b).unwrap().volume))); let t = box_solid(Vec3::new(10.0, 10.0, 5.0), Vec3::new(20.0, 20.0, 15.0)).unwrap(); println!("box void: {:?}", boolean(&b, &t, BoolOp::Cut).map(|o| o.map(|b| measure(&b).unwrap().volume))); for r in [5.0, 4.3] { for z in [5.0, 7.3, 13.1] { let c = cylinder(Vec3::new(20.3, 15.1, z), Vec3::Z, r, 30.0).unwrap(); println!("cyl r{r} z{z}: {:?}", boolean(&b, &c, BoolOp::Cut).map(|o| o.map(|b| measure(&b).unwrap().volume))); } } let c = cylinder(Vec3::new(20.0, 15.0, 25.0), -Vec3::Z, 5.0, 20.0).unwrap(); println!("cyl down: {:?}", boolean(&b, &c, BoolOp::Cut).map(|o| o.map(|b| measure(&b).unwrap().volume))); let s = sphere(Vec3::new(20.0, 15.0, 20.0), 5.0).unwrap(); println!("sphere: {:?}", boolean(&b, &s, BoolOp::Cut).map(|o| o.map(|b| measure(&b).unwrap().volume))); } #[test] #[ignore] fn debug_revolve_doc_case() { let r = Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0), Vec2::new(40.0, 30.0), Vec2::new(0.0, 30.0)]), holes: vec![] }; for ang in [PI / 2.0, -PI / 2.0] { let q = revolve(&Plane::XY, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, ang).unwrap().pop().unwrap(); let m = measure(&q).unwrap(); println!("ang {ang}: vol {} bbox {:?}", m.volume, m.bbox); } let r2 = Region2 { outer: r.outer.reversed(), holes: vec![] }; let q = revolve(&Plane::XY, std::slice::from_ref(&r2), Vec2::ZERO, Vec2::Y, PI / 2.0).unwrap().pop().unwrap(); println!("reversed: {}", measure(&q).unwrap().volume); } #[test] #[ignore] fn debug_coplanar_union_rate() { let a = box_solid(Vec3::ZERO, Vec3::new(40.0, 40.0, 20.0)).unwrap(); let b = extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(Vec2::new(40.0, 20.0), 12.0), holes: vec![] }], 0.0, 40.0).unwrap().pop().unwrap(); let mut ok = 0; for i in 0..20 { let sh = Vec3::new(0.0137 * i as f64, 0.0291 * i as f64, -0.0173 * i as f64); let (a2, b2) = (transform(&a, sh, Vec3::ZERO, Vec3::Z, 0.0).unwrap(), transform(&b, sh, Vec3::ZERO, Vec3::Z, 0.0).unwrap()); for tol in [1e-3, 1e-2, 5e-2] { let r = guard("t", || { let (sa, sb) = (a2.deep_copy(), b2.deep_copy()); Ok(truck_shapeops::or(&sa, &sb, tol)) }); let good = matches!(r, Ok(Some(_))); if good { ok += 1; } println!("shift {i} tol {tol}: {}", if good { "ok" } else { "fail" }); } } println!("ok {ok}/60"); } #[test] fn chamfer_top_loop_mitres() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); let edges = [Vec3::new(0.0, 15.0, 20.0), Vec3::new(20.0, 0.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0)]; let c = chamfer(&b, &edges, 2.0).unwrap(); let m = measure(&c).unwrap(); assert!(rel(m.volume, 23730.666667) < 1e-6, "{}", m.volume); assert_eq!(m.merged.faces, 10); } #[test] #[ignore] fn debug_chamfer_cut() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); // Prism along x: triangle in the YZ plane beyond the top-front edge. let plane = Plane { origin: Vec3::new(-5.0, 0.0, 0.0), x: Vec3::Y, y: Vec3::Z }; let tri = Loop2::polygon(&[Vec2::new(2.3, 20.3), Vec2::new(-0.3, 17.7), Vec2::new(10.0, 30.0)]); let tri = if tri.signed_area() < 0.0 { tri.reversed() } else { tri }; let tool = extrude(&plane, &[Region2 { outer: tri, holes: vec![] }], 0.0, 50.0).unwrap().pop().unwrap(); println!("tool vol {}", measure(&tool).unwrap().volume); let r = boolean(&b, &tool, BoolOp::Cut).unwrap().unwrap(); let m = measure(&r).unwrap(); println!("cut: {} faces {} {:?}", m.volume, m.faces, m.merged); for f in r.faces(0.01).unwrap() { println!("face {} area {:.3} c {:?} n {:?}", f.index, f.area, f.centroid, f.plane_normal); } for tol in [0.1, 0.01, 0.001] { println!("tess {tol}: {}", r.tessellate(tol).unwrap().measure().volume); } } #[test] #[ignore] fn debug_revolved_hole_tool() { let b = box_solid(Vec3::ZERO, Vec3::new(60.0, 40.0, 15.0)).unwrap(); // Drill point profile in the XZ plane through x = 50: revolve about the vertical line x = 50. let pl = Plane { origin: Vec3::new(50.0, 20.0, 0.0), x: Vec3::X, y: Vec3::Z }; let r = 3.0; let tip = r / 59f64.to_radians().tan(); let prof = Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 16.0), Vec2::new(0.0, 5.0 - tip), Vec2::new(r, 5.0), Vec2::new(r, 16.0)]), holes: vec![] }; let tool = revolve(&pl, &[prof], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap(); println!("tool vol {}", measure(&tool).unwrap().volume); let res = boolean(&b, &tool, BoolOp::Cut); println!("cut: {:?}", res.map(|o| o.map(|x| measure(&x).unwrap().volume))); let expect = 36000.0 - std::f64::consts::PI * 9.0 * 10.0 - std::f64::consts::PI * 9.0 * tip / 3.0; println!("expect {expect}"); } #[test] #[ignore] fn debug_sphere() { let s = sphere(Vec3::new(1.0, 2.0, 3.0), 10.0).unwrap(); let m = measure(&s).unwrap(); println!("vol {} area {} faces {} bbox {:?}", m.volume, m.area, m.faces, m.bbox); for f in s.faces(0.05).unwrap() { println!("face {} area {:.3} c {:?}", f.index, f.area, f.centroid); } let b = box_solid(Vec3::new(-25.0, -25.0, -20.0), Vec3::new(25.0, 25.0, 0.0)).unwrap(); for (name, c) in [("origin", Vec3::ZERO), ("up", Vec3::new(0.0, 0.0, 3.0)), ("off", Vec3::new(1.3, 2.1, 0.7))] { let sp = sphere(c, 15.0).unwrap(); println!("{name} cut {:?}", boolean(&b, &sp, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume))); println!("{name} and {:?}", boolean(&b, &sp, BoolOp::Intersect).map(|o| o.map(|x| measure(&x).unwrap().volume))); } } #[test] #[ignore] fn debug_countersink() { let b = box_solid(Vec3::ZERO, Vec3::new(60.0, 40.0, 15.0)).unwrap(); for (name, pts) in [ ("csink", vec![Vec2::new(0.0, 0.6), Vec2::new(6.6, 0.6), Vec2::new(3.0, -3.0), Vec2::new(3.0, -31.0), Vec2::new(0.0, -31.0)]), ("csink-thru-top", vec![Vec2::new(0.0, 0.6), Vec2::new(6.6, 0.6), Vec2::new(3.0, -3.0), Vec2::new(3.0, -16.3), Vec2::new(0.0, -16.3)]), ( "cbore", vec![Vec2::new(0.0, 0.6), Vec2::new(7.0, 0.6), Vec2::new(7.0, -5.0), Vec2::new(4.0, -5.0), Vec2::new(4.0, -31.0), Vec2::new(0.0, -31.0)], ), ] { for (pn, pl) in [ ("xz@top", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::X, Vec3::Z).unwrap()), ("yz@top", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::Y, Vec3::Z).unwrap()), ("diag", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::new(0.6, 0.8, 0.0), Vec3::Z).unwrap()), ] { let tool = revolve(&pl, &[Region2 { outer: Loop2::polygon(&pts), holes: vec![] }], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU) .unwrap() .pop() .unwrap(); println!( "{name} {pn}: tool {} cut {:?}", measure(&tool).unwrap().volume, boolean(&b, &tool, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume)) ); } } } #[test] #[ignore] fn debug_cbore_seq() { let b = box_solid(Vec3::ZERO, Vec3::new(60.0, 40.0, 15.0)).unwrap(); let pl = Plane::from_normal(Vec3::new(15.0, 20.0, 15.0), Vec3::Z).unwrap(); for (s1, s2, r2top) in [(0.0, 0.0, 0.6), (0.613, 1.226, 0.6), (0.3, 0.9, 2.37)] { let t1 = extrude(&pl, &[Region2 { outer: Loop2::circle_from(Vec2::ZERO, 4.0, s1), holes: vec![] }], -31.0, 0.6).unwrap().pop().unwrap(); let c1 = boolean(&b, &t1, BoolOp::Cut).unwrap().unwrap(); let t2 = extrude(&pl, &[Region2 { outer: Loop2::circle_from(Vec2::ZERO, 7.0, s2), holes: vec![] }], -5.0, r2top).unwrap().pop().unwrap(); println!( "seams {s1} {s2}: first {} second {:?}", measure(&c1).unwrap().volume, boolean(&c1, &t2, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume)) ); let c2 = boolean(&b, &t2, BoolOp::Cut).unwrap().unwrap(); println!(" swapped: {:?}", boolean(&c2, &t1, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume))); } }