diff --git a/crates/kernel/examples/step_board_fixture.rs b/crates/kernel/examples/step_board_fixture.rs new file mode 100644 index 0000000000..69691c535e --- /dev/null +++ b/crates/kernel/examples/step_board_fixture.rs @@ -0,0 +1,87 @@ +//! A board-like assembly for issue #883, the worst case for a large STEP import: every +//! component is its own product with a package body and its own pins (nothing shared). +//! +//! ```sh +//! scripts/fetch-smlib.sh +//! SOLIDCRAFT_STEP_PCB_COMPONENTS=600 SOLIDCRAFT_STEP_PCB_PINS=15 \ +//! cargo run --release -p solidcraft-kernel --features smlib --example step_board_fixture -- board.step +//! ``` +//! +//! 600 components of 15 pins give 52 MB. The defaults are 500 and 16; the path defaults to +//! `board.step`. + +#[cfg(not(feature = "smlib"))] +fn main() { + eprintln!("step_board_fixture needs SMLib: run scripts/fetch-smlib.sh, then add --features smlib"); + std::process::exit(2); +} + +#[cfg(feature = "smlib")] +fn main() { + if let Err(e) = run::main() { + eprintln!("step_board_fixture: {e}"); + std::process::exit(1); + } +} + +#[cfg(feature = "smlib")] +mod run { + use solidcraft_geom::Vec3; + use solidcraft_kernel::{Body, ExportBody, ExportProduct, StepHeader, box_solid, cylinder, fillet, step_export_products}; + + /// A count from `SOLIDCRAFT_`, or `default` when it is unset. + fn count(name: &str, default: usize) -> Result { + solidcraft_geom::env::var(name).map_or(Ok(default), |s| s.parse().map_err(|e| format!("SOLIDCRAFT_{name}: {e}"))) + } + + /// A package: a box with its vertical edges and then its top edges rounded (left sharp where + /// a round does not build). + fn package(w: f64, d: f64, h: f64) -> Result { + let b = box_solid(Vec3::ZERO, Vec3::new(w, d, h)).map_err(|e| e.to_string())?; + let vertical = [Vec3::new(0.0, 0.0, h / 2.0), Vec3::new(w, 0.0, h / 2.0), Vec3::new(w, d, h / 2.0), Vec3::new(0.0, d, h / 2.0)]; + let top = [Vec3::new(w / 2.0, 0.0, h), Vec3::new(w, d / 2.0, h), Vec3::new(w / 2.0, d, h), Vec3::new(0.0, d / 2.0, h)]; + let b = fillet(&b, &vertical, 0.3).unwrap_or(b); + Ok(fillet(&b, &top, 0.1).unwrap_or(b)) + } + + pub fn main() -> Result<(), String> { + let components = count("STEP_PCB_COMPONENTS", 500)?; + let pins = count("STEP_PCB_PINS", 16)?; + let path = std::env::args().nth(1).unwrap_or_else(|| "board.step".into()); + let mut owned: Vec> = Vec::new(); + for c in 0..components { + let w = 4.0 + (c % 7) as f64 * 0.37; + let d = 3.0 + (c % 5) as f64 * 0.29; + let mut bodies = vec![package(w, d, 1.2 + (c % 3) as f64 * 0.1)?]; + for p in 0..pins { + let side = if p % 2 == 0 { -0.6 } else { d + 0.6 }; + let x = 0.3 + (p / 2) as f64 * (w - 0.6) / (pins / 2).max(1) as f64 + c as f64 * 1e-4; + bodies.push(cylinder(Vec3::new(x, side, -1.5), Vec3::Z, 0.25, 2.0 + (p % 3) as f64 * 0.01).map_err(|e| e.to_string())?); + } + owned.push(bodies); + } + let mut products: Vec = owned + .iter() + .enumerate() + .map(|(c, bodies)| ExportProduct { + name: format!("U{c}"), + bodies: bodies.iter().enumerate().map(|(k, b)| ExportBody { name: format!("U{c}_{k}"), body: b, color: Some([0.2, 0.2, 0.2]) }).collect(), + children: Vec::new(), + }) + .collect(); + let side = (components as f64).sqrt().ceil() as usize; + let children = (0..components) + .map(|c| { + let (x, y) = ((c % side) as f64 * 12.0, (c / side) as f64 * 10.0); + (c, [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [x, y, 1.6, 1.0]], format!("U{c}")) + }) + .collect(); + products.push(ExportProduct { name: "Board".into(), bodies: Vec::new(), children }); + let root = products.len() - 1; + let header = StepHeader { file_name: "board.step".into(), ..Default::default() }; + let text = step_export_products(&products, root, &header).map_err(|e| e.to_string())?; + std::fs::write(&path, &text).map_err(|e| format!("{path}: {e}"))?; + println!("wrote {path}: {} bytes, {components} components", text.len()); + Ok(()) + } +} diff --git a/crates/kernel/src/modify_history_tests.rs b/crates/kernel/src/modify_history_tests.rs index 45d2d36f2a..3cb4910b8b 100644 --- a/crates/kernel/src/modify_history_tests.rs +++ b/crates/kernel/src/modify_history_tests.rs @@ -511,9 +511,9 @@ fn chamfer_after_box_cuts() { for (name, cut, tool) in box_cuts_of(&b) { let r = crate::chamfer(&cut, &[at], 2.0).unwrap(); let h = verified(&r, &cut, reach(2.0 * 1.5)); - // On the painted block the chamfer face keeps the boundaries the cut left. - let faces = with(h, &[edge(edge_at(&cut, at))]).len(); - assert!(if painted { faces >= 1 } else { faces == 1 }, "{name} (painted: {painted}): {h:?}"); + // The picked edge is one edge of the cut body on either block (its round is one face, + // `fillet_after_box_cuts`), so its chamfer is one face too. + assert_eq!(with(h, &[edge(edge_at(&cut, at))]).len(), 1, "{name} (painted: {painted}): {h:?}"); let mut through = h.clone(); through.substitute(0, cut.history().unwrap()).unwrap(); through.check(&r, &[&b, &tool]).unwrap(); diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index 99049a2c40..9583c575b4 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -1545,60 +1545,6 @@ fn faces_with_too_many_bounds_are_refused() { assert!(start.elapsed() < std::time::Duration::from_secs(10), "took {:?}", start.elapsed()); } -/// A board-like assembly for issue #883: `SOLIDCRAFT_STEP_PCB_FIXTURE=path` writes it (with -/// `SOLIDCRAFT_STEP_PCB_COMPONENTS` and `SOLIDCRAFT_STEP_PCB_PINS`; 600 and 15 give 52 MB). Every component is -/// its own product with a package body and its own pins (nothing shared), the worst case for -/// a large import. -#[test] -#[ignore = "manual fixture generator for large STEP imports"] -fn generate_board_assembly_fixture() { - use crate::step_out::{ExportBody, ExportProduct, StepHeader, step_export_products}; - let count: usize = solidcraft_geom::env::var("STEP_PCB_COMPONENTS").map(|s| s.parse().unwrap()).unwrap_or(500); - let pins: usize = solidcraft_geom::env::var("STEP_PCB_PINS").map(|s| s.parse().unwrap()).unwrap_or(16); - let mut owned: Vec> = Vec::new(); - for c in 0..count { - let w = 4.0 + (c % 7) as f64 * 0.37; - let d = 3.0 + (c % 5) as f64 * 0.29; - let mut bodies = vec![fillet_box(w, d, 1.2 + (c % 3) as f64 * 0.1)]; - for p in 0..pins { - let side = if p % 2 == 0 { -0.6 } else { d + 0.6 }; - let x = 0.3 + (p / 2) as f64 * (w - 0.6) / (pins / 2).max(1) as f64 + c as f64 * 1e-4; - bodies.push(cylinder(Vec3::new(x, side, -1.5), Vec3::Z, 0.25, 2.0 + (p % 3) as f64 * 0.01).unwrap()); - } - owned.push(bodies); - } - let mut products: Vec = owned - .iter() - .enumerate() - .map(|(c, bodies)| ExportProduct { - name: format!("U{c}"), - bodies: bodies.iter().enumerate().map(|(k, b)| ExportBody { name: format!("U{c}_{k}"), body: b, color: Some([0.2, 0.2, 0.2]) }).collect(), - children: Vec::new(), - }) - .collect(); - let side = (count as f64).sqrt().ceil() as usize; - let children = (0..count) - .map(|c| { - let (x, y) = ((c % side) as f64 * 12.0, (c / side) as f64 * 10.0); - (c, [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [x, y, 1.6, 1.0]], format!("U{c}")) - }) - .collect(); - products.push(ExportProduct { name: "Board".into(), bodies: Vec::new(), children }); - let root = products.len() - 1; - let text = step_export_products(&products, root, &StepHeader { file_name: "board.step".into(), ..Default::default() }).unwrap(); - let path = solidcraft_geom::env::var("STEP_PCB_FIXTURE").unwrap_or_else(|| "board.step".into()); - std::fs::write(&path, &text).unwrap(); - println!("wrote {path}: {} bytes, {} components", text.len(), count); -} - -fn fillet_box(w: f64, d: f64, h: f64) -> Body { - let b = box_solid(Vec3::ZERO, Vec3::new(w, d, h)).unwrap(); - let vertical = [Vec3::new(0.0, 0.0, h / 2.0), Vec3::new(w, 0.0, h / 2.0), Vec3::new(w, d, h / 2.0), Vec3::new(0.0, d, h / 2.0)]; - let top = [Vec3::new(w / 2.0, 0.0, h), Vec3::new(w, d / 2.0, h), Vec3::new(w / 2.0, d, h), Vec3::new(0.0, d / 2.0, h)]; - let b = fillet(&b, &vertical, 0.3).unwrap_or(b); - fillet(&b, &top, 0.1).unwrap_or(b) -} - /// Independent boxes as one STEP file (about 3 KB a box). fn boxes_step(count: usize, name: &str) -> String { let bodies: Vec<_> = (0..count) diff --git a/crates/kernel/src/tess/grid.rs b/crates/kernel/src/tess/grid.rs index 1da7da3923..b89ecd923d 100644 --- a/crates/kernel/src/tess/grid.rs +++ b/crates/kernel/src/tess/grid.rs @@ -323,8 +323,15 @@ mod tests { // A triangle-like patch whose top side is a pole (two copies of one point in 3D). let b = vec![[0.0, 0.0], [0.5, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]; let Some(q) = quad(&b) else { panic!("a quad") }; - let Some((_, tris)) = structured(&b, &q, &[0.5], &[0.5]) else { panic!("meshes") }; - assert!(!tris.is_empty()); + let Some((extra, tris)) = structured(&b, &q, &[0.5], &[0.5]) else { panic!("meshes") }; + // One grid node, (0.5, 0.5), fanned to each of the five boundary segments: one triangle + // each, the pole's single segment among them, every one of positive area, tiling the box. + assert_eq!(extra, [[0.5, 0.5]]); + assert_eq!(tris.len(), 5); + let mut uv = b.clone(); + uv.extend(extra); + assert!(tris.iter().all(|t| signed_area(&uv, *t).is_some_and(|a| a > 0.0))); + assert!((area(&uv, &tris) - 1.0).abs() < 1e-12); } #[test] diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index f73098b082..3da88a6286 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -1022,78 +1022,6 @@ fn similar_spheres_meet() { } } -#[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.mesh(&crate::MeshTolerance::chord(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.mesh(&crate::MeshTolerance::chord(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] fn chamfer_top_loop_mitres() { let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); @@ -1202,111 +1130,6 @@ fn chamfer_wider_than_sloped_basin_rim_builds() { } } -#[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.mesh(&crate::MeshTolerance::chord(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))); - } -} - #[test] fn mesh_bodies_move_measure_and_refuse_solid_ops() { // Unit cube as 12 triangles with repeated corners (welded on construction). @@ -1352,26 +1175,6 @@ fn concave_fillet() { assert_eq!(m.merged.face_types.get("cylinder"), Some(&1)); } -#[test] -#[ignore] -fn debug_pocket_corners() { - let b = box_solid(Vec3::ZERO, Vec3::new(80.0, 60.0, 20.0)).unwrap(); - let p = box_solid(Vec3::new(15.0, 15.0, 12.0), Vec3::new(65.0, 45.0, 25.0)).unwrap(); - let mut cur = boolean(&b, &p, BoolOp::Cut).unwrap().unwrap(); - for e in [Vec3::new(65.0, 15.0, 16.0), Vec3::new(15.0, 15.0, 16.0), Vec3::new(65.0, 45.0, 16.0), Vec3::new(15.0, 45.0, 16.0)] { - match fillet(&cur, &[e], 5.0) { - Ok(n) => { - println!("ok {e:?} vol {}", measure(&n).unwrap().volume); - cur = n; - } - Err(err) => { - println!("fail {e:?}: {err}"); - break; - } - } - } -} - #[test] fn pocket_floor_loop_fillet() { let b = box_solid(Vec3::ZERO, Vec3::new(80.0, 60.0, 20.0)).unwrap(); @@ -1429,7 +1232,18 @@ fn coplanar_curved_booleans() { assert!(rel(measure(&cut).unwrap().volume, 32000.0 - half * 20.0) < 1e-3); let i = boolean(&a, &c, BoolOp::Intersect).unwrap().unwrap(); assert!(rel(measure(&i).unwrap().volume, half * 20.0) < 1e-3); - println!("union {:?}", m.merged); + // A cylinder of radius 5 through the middle of a 40 x 30 x 20 box, its caps flush with the + // box's top or bottom or past them: the cut takes the part inside, π·25 a mm of height, and + // the union adds the parts outside. A cylinder inside but for a flush cap adds nothing. + let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); + let disc = PI * 25.0; + for (z0, z1, inside, outside) in [(0.0, 20.0, 20.0, 0.0), (-5.0, 20.0, 20.0, 5.0), (0.0, 25.0, 20.0, 5.0), (-5.0, 25.0, 20.0, 10.0), (5.0, 20.0, 15.0, 0.0), (5.0, 25.0, 15.0, 5.0)] { + let c = cylinder(Vec3::new(20.0, 15.0, z0), Vec3::Z, 5.0, z1 - z0).unwrap(); + let cut = measure(&boolean(&b, &c, BoolOp::Cut).unwrap().unwrap()).unwrap().volume; + assert!(rel(cut, 24000.0 - disc * inside) < 1e-6, "cut z {z0}..{z1}: {cut}"); + let union = measure(&boolean(&b, &c, BoolOp::Union).unwrap().unwrap()).unwrap().volume; + assert!(rel(union, 24000.0 + disc * outside) < 1e-6, "union z {z0}..{z1}: {union}"); + } } #[test] @@ -2102,57 +1916,6 @@ fn fillet_partial_mixed_arc_and_line_perimeter_run() { } } -/// Boolean survey: random boxes, cylinders and spheres combined every way; prints what fails. -#[test] -#[ignore] -fn boolean_survey() { - let mut seed: u64 = 0x2545_f491_4f6c_dd1d; - let mut rnd = || { - seed ^= seed << 13; - seed ^= seed >> 7; - seed ^= seed << 17; - (seed % 1_000_000) as f64 / 1_000_000.0 - }; - let mut shapes: Vec<(String, Body)> = Vec::new(); - for i in 0..40 { - let kind = i % 4; - let (x, y, z) = ((rnd() - 0.5) * 20.0, (rnd() - 0.5) * 20.0, (rnd() - 0.5) * 20.0); - // Snap some coordinates to a 5 mm grid: coincident faces and tangencies. - let snap = |v: f64, s: bool| if s { (v / 5.0).round() * 5.0 } else { v }; - let s = rnd() < 0.5; - let (x, y, z) = (snap(x, s), snap(y, s), snap(z, s)); - let a = 5.0 + snap(rnd() * 20.0, s); - let ax = (rnd() * 3.0) as usize % 3; - let b = match kind { - 0 => box_solid(Vec3::new(x, y, z), Vec3::new(x + a, y + a * 0.8, z + a * 0.6)), - 1 => cylinder(Vec3::new(x, y, z), [Vec3::X, Vec3::Y, Vec3::Z][ax], a * 0.4, a), - 2 => sphere(Vec3::new(x, y, z), a * 0.5), - _ => box_solid(Vec3::new(x, y, z), Vec3::new(x + a * 0.5, y + a, z + a * 0.3)), - }; - if let Ok(b) = b { - shapes.push((format!("{}{i}@({x:.3},{y:.3},{z:.3}) a={a:.3} ax={ax}", ["box", "cyl", "sph", "slab"][kind]), b)); - } - } - let (mut ok, mut empty, mut fail) = (0, 0, Vec::new()); - for i in 0..shapes.len() { - for j in i + 1..(i + 4).min(shapes.len()) { - for op in [BoolOp::Union, BoolOp::Cut, BoolOp::Intersect] { - let (na, a) = &shapes[i]; - let (nb, b) = &shapes[j]; - match boolean(a, b, op) { - Ok(Some(_)) => ok += 1, - Ok(None) => empty += 1, - Err(e) => fail.push(format!("{na} {op:?} {nb}: {}", e.to_string().chars().take(140).collect::())), - } - } - } - } - println!("ok {ok} empty {empty} failed {}", fail.len()); - for f in &fail { - println!(" {f}"); - } -} - #[test] fn booleans_found_by_the_survey() { // A tool wholly inside: a void (the solid keeps an inner shell). @@ -2342,25 +2105,6 @@ fn a_bend_as_tight_as_the_tube_sweeps_a_valid_solid() { assert!(rel(v, want) < 1e-6, "{v} vs {want}"); } -#[test] -#[ignore] -fn boolean_identity_survey() { - let n: u64 = std::env::var("N").ok().and_then(|x| x.parse().ok()).unwrap_or(100); - let seeds: Vec = match std::env::var("SEEDS") { - Ok(s) => s.split(',').filter_map(|x| x.parse().ok()).collect(), - Err(_) => (0..n).collect(), - }; - let mut bad = 0; - for seed in seeds { - let c = crate::fuzz::boolean_case(seed); - if c.outcome != "ok" { - bad += 1; - println!("{seed}: {} {} | {:?} | {:?}", c.outcome, c.detail, c.a, c.b); - } - } - println!("{bad}/{n} failed"); -} - /// A quarter turn of a rectangle with an edge on the axis: no zero-area face along the axis, so /// a cylinder can be cut through the corner (it failed in every boolean attempt before). #[test]