mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
Wave 471/509: Kernel tests: the twelve ignored print-only debug tests and surveys go; coplanar_curved_booleans checks the flush-capped cylinder cuts and unions they printed (and 3 more)
4 commits, squashed; the originals stay reachable from tag archive/merge-main-develop. -f81c4b157jyviko: Chamfer history test: the chamfer after each box cut is exactly one face on the painted block too -9c0d6bd1fjyviko: Grid test: a pole side's face is the five triangles fanned from its one grid node, none degenerate -648413556jyviko: Kernel tests: the twelve ignored print-only debug tests and surveys go; coplanar_curved_booleans checks the flush-capped cylinder cuts and unions they printed -35805f899jyviko: Kernel: the #883 board fixture generator is an example, not an ignored test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
@@ -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_<name>`, or `default` when it is unset.
|
||||
fn count(name: &str, default: usize) -> Result<usize, String> {
|
||||
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<Body, String> {
|
||||
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<Body>> = 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<ExportProduct> = 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(())
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Body>> = 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<ExportProduct> = 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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
+12
-268
@@ -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<Vec3> =
|
||||
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::<String>())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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<u64> = 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]
|
||||
|
||||
Reference in New Issue
Block a user