mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel: the test target compiles: the last tests on the record and SMLib
mesh memo (one remembered mesh per tolerance), history (a bare body from a record), tests.rs (no coarse-missing field, lumps for touching shells), outline (the foot found by the planar faces in its plane), chamfer sides (the rim from the record's edges), seam faces, coplanar and issue 679 and pinch tests (the boolean is SMLib's: the diagnostics assertions go, every volume, shell and validity check stays), boolean history (a cut of two pieces through the boolean), integration tests naming solidcraft_kernel::MeshTolerance. Deleted, testing internals that are gone: boolean_history_tests' glue_merges_across_the_seam and faces_pushed_apart (coplanar::glue, boolean_apart), planar_fallback (polybool's planar_boolean) and bore_union_of_a_bore_in_two_arcs_has_no_history (coplanar::bore_union); issue_851's the_sphere_at_a_box_corner_is_the_planar_corner_builders (blend_vertex); pinch's prism_boolean shell check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -310,15 +310,15 @@ fn trivial_paths() {
|
||||
assert!(h.faces.iter().all(|f| f.change == Change::Kept));
|
||||
}
|
||||
|
||||
/// A body in two pieces, cut a piece at a time: each piece's faces are the body's own.
|
||||
/// A body in two pieces, cut: each piece's faces are the body's own.
|
||||
#[test]
|
||||
fn by_lumps_composes_the_pieces() {
|
||||
fn a_cut_of_two_pieces_composes_the_pieces() {
|
||||
let left = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let right = boxed(20.0, 0.0, 0.0, 30.0, 10.0, 10.0);
|
||||
let a = boolean(&left, &right, BoolOp::Union).unwrap().unwrap();
|
||||
assert_eq!(a.shell_count(), 2);
|
||||
let b = crate::sphere(Vec3::new(15.0, 5.0, 10.0), 7.0).unwrap();
|
||||
let r = crate::ops::by_lumps(&a, &b, a.lumps().unwrap(), b.lumps().unwrap(), BoolOp::Cut).unwrap().unwrap();
|
||||
let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap();
|
||||
let h = verified(&r, &[&a, &b], 0.02);
|
||||
// Both pieces are bitten: the sphere's patches inside each line its bite.
|
||||
let bites: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect();
|
||||
@@ -327,54 +327,6 @@ fn by_lumps_composes_the_pieces() {
|
||||
assert_eq!(targets.len(), 12);
|
||||
}
|
||||
|
||||
/// Two bodies touching along one whole face, back to back: glued, the two faces gone, the
|
||||
/// coplanar sides each side of the seam merged.
|
||||
#[test]
|
||||
fn glue_merges_across_the_seam() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let b = boxed(10.0, 0.0, 0.0, 20.0, 10.0, 10.0);
|
||||
let r = crate::coplanar::glue(&a, &b).unwrap().unwrap();
|
||||
let h = verified(&r, &[&a, &b], 0.02);
|
||||
assert_eq!(changes(h), [2, 0, 0, 4, 0], "{}", show(h));
|
||||
// The four seam edges went with the merged faces; the ends' edges are each box's.
|
||||
assert_eq!(h.edges.len(), 12);
|
||||
assert!(h.edges.iter().all(|e| !e.origins.is_empty()));
|
||||
}
|
||||
|
||||
/// Coincident faces pushed apart: a cut flush with the target's top, and a union of bodies
|
||||
/// standing on one floor (the floor that is cut back is both operands' floors).
|
||||
#[test]
|
||||
fn faces_pushed_apart() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let tool = boxed(2.0, 2.0, 5.0, 8.0, 8.0, 10.0);
|
||||
let r = crate::coplanar::boolean_apart(&a, &tool, BoolOp::Cut).unwrap().unwrap().unwrap();
|
||||
let h = verified(&r, &[&a, &tool], 0.02);
|
||||
// The pocket: its floor and four sides are the tool's.
|
||||
let tool_faces: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect();
|
||||
assert_eq!(tool_faces.len(), 5, "{}", show(h));
|
||||
let left = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let right = boxed(5.0, 3.0, 0.0, 15.0, 7.0, 4.0);
|
||||
let r = crate::coplanar::boolean_apart(&left, &right, BoolOp::Union).unwrap().unwrap().unwrap();
|
||||
let h = verified(&r, &[&left, &right], 0.02);
|
||||
assert!(h.faces.iter().any(|f| f.change == Change::Merged && f.origins.len() == 2), "{}", show(h));
|
||||
}
|
||||
|
||||
/// The exact polygon boolean: pieces of planar faces joined back into faces.
|
||||
#[test]
|
||||
fn planar_fallback() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let b = boxed(5.0, 5.0, 5.0, 15.0, 15.0, 15.0);
|
||||
for (op, count) in [(BoolOp::Cut, 9), (BoolOp::Union, 12), (BoolOp::Intersect, 6)] {
|
||||
let r = crate::polybool::planar_boolean(&a, &b, op).unwrap().unwrap();
|
||||
let h = verified(&r, &[&a, &b], 0.02);
|
||||
assert_eq!(h.faces.len(), count, "{op:?}\n{}", show(h));
|
||||
assert!(h.faces.iter().all(|f| f.origins.len() == 1));
|
||||
// Three faces of each meet three of the other's.
|
||||
assert_eq!(meets(h).len(), 6, "{op:?}\n{}", show(h));
|
||||
assert!(h.edges.iter().all(|e| e.origins.len() == 1), "{op:?}\n{}", show(h));
|
||||
}
|
||||
}
|
||||
|
||||
/// A boolean with a triangle mesh: the mesh's faces are its triangles (and it has no edges).
|
||||
#[test]
|
||||
fn faceted_tool() {
|
||||
@@ -433,24 +385,6 @@ fn regularized_step_cut() {
|
||||
assert_eq!(faces_of(h), faces_of(ph), "{}", show(h));
|
||||
}
|
||||
|
||||
/// A ring joined under a floor, its bore on the floor's hole (coincident cylinders): the ring's
|
||||
/// bore is narrowed, the bodies joined, and the bore cut out again. The ring's bore is a circle
|
||||
/// built in two arcs, and the recut wall is split where the cutting cylinder's own construction
|
||||
/// splits it, so which arc each piece of it restores is not known: no history.
|
||||
#[test]
|
||||
fn bore_union_of_a_bore_in_two_arcs_has_no_history() {
|
||||
let hole = crate::cylinder(Vec3::new(50.0, 40.0, -5.0), Vec3::Z, 10.0, 30.0).unwrap();
|
||||
let floor = boolean(&boxed(0.0, 0.0, 0.0, 100.0, 80.0, 10.0), &hole, BoolOp::Cut).unwrap().unwrap();
|
||||
let centre = solidcraft_geom::Vec2::new(50.0, 40.0);
|
||||
let annulus = solidcraft_geom::Region2 {
|
||||
outer: solidcraft_geom::Loop2::circle(centre, 20.0),
|
||||
holes: vec![solidcraft_geom::Loop2::circle(centre, 10.0).reversed()],
|
||||
};
|
||||
let ring = crate::extrude(&solidcraft_geom::Plane::XY, &[annulus], -5.0, 2.0).unwrap().remove(0);
|
||||
let r = crate::coplanar::bore_union(&floor, &ring, &|_| true).expect("the bores are recut");
|
||||
assert!(r.history().is_none());
|
||||
}
|
||||
|
||||
/// Bodies clearly apart, joined as one body of several pieces (`union_apart`): every face and
|
||||
/// edge is its piece's own, kept.
|
||||
#[test]
|
||||
|
||||
@@ -210,8 +210,13 @@ fn too_large_or_unsupported_is_refused() {
|
||||
let tilted = crate::build::cylinder(Vec3::new(0.0, 0.0, -10.0), Vec3::new(1.0, 0.0, 2.0), 5.0, 40.0).unwrap();
|
||||
let cut = box_solid(Vec3::new(-50.0, -50.0, 10.0), Vec3::new(50.0, 50.0, 50.0)).unwrap();
|
||||
let t = boolean(&tilted, &cut, BoolOp::Cut).unwrap().unwrap();
|
||||
let solid = t.deep_copy();
|
||||
let rim = crate::body::Body::unique_edges(&solid).iter().filter_map(crate::blend::edge_mid).find(|m| (m.z - 10.0).abs() < 1e-6).unwrap();
|
||||
let rim = t
|
||||
.record()
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(|e| solidcraft_geom::brep::eval::CurveEval::new(&e.curve).ok()?.point(0.5 * (e.interval[0] + e.interval[1])))
|
||||
.find(|m| (m.z - 10.0).abs() < 1e-6)
|
||||
.unwrap();
|
||||
match chamfer_sides(&t, &[rim], 1.0, ChamferSide::Distance(2.0), false) {
|
||||
Err(KernelError::Failed(m)) => assert!(m.starts_with("not supported yet"), "{m}"),
|
||||
Err(e) => panic!("{e}"),
|
||||
|
||||
@@ -25,20 +25,19 @@ fn channel() -> Body {
|
||||
box_solid(Vec3::new(-20.0, 30.0, 10.0), Vec3::new(220.0, 70.0, 20.0)).unwrap()
|
||||
}
|
||||
|
||||
/// What the boolean of `a` and `b` adds to a's volume, the result's lumps, and how it was made.
|
||||
fn change(a: &Body, b: &Body, op: BoolOp) -> (f64, usize, BooleanDiagnostics) {
|
||||
let (r, how) = capture_boolean_diagnostics(|| boolean(a, b, op));
|
||||
let r = r.unwrap().unwrap();
|
||||
(measure(&r).unwrap().volume - measure(a).unwrap().volume, r.lumps().unwrap().len(), how)
|
||||
/// What the boolean of `a` and `b` adds to a's volume, and the result's lumps.
|
||||
fn change(a: &Body, b: &Body, op: BoolOp) -> (f64, usize) {
|
||||
let r = boolean(a, b, op).unwrap().unwrap();
|
||||
(measure(&r).unwrap().volume - measure(a).unwrap().volume, r.lumps().unwrap().len())
|
||||
}
|
||||
|
||||
/// A channel cut along a block, up to the face a rib stands on. The channel's top lies in
|
||||
/// that face's plane and the rib bridges it.
|
||||
#[test]
|
||||
fn a_cut_up_to_a_face_leaves_what_stands_on_the_face() {
|
||||
let (added, lumps, how) = change(&ribbed(), &channel(), BoolOp::Cut);
|
||||
let (added, lumps) = change(&ribbed(), &channel(), BoolOp::Cut);
|
||||
// It takes 200 x 40 x 10 of the block and nothing of the rib.
|
||||
assert!((added + 80_000.0).abs() < 1e-6 && lumps == 1, "{added}, {lumps} lumps, {how:?}");
|
||||
assert!((added + 80_000.0).abs() < 1e-6 && lumps == 1, "{added}, {lumps} lumps");
|
||||
}
|
||||
|
||||
/// The same with a round hole through the block beside the channel (r 4 at 30, 15: y 11 to
|
||||
@@ -48,9 +47,9 @@ fn a_cut_up_to_a_face_leaves_what_stands_on_the_face() {
|
||||
fn a_round_hole_beside_the_cut_changes_nothing() {
|
||||
let hole = cylinder(Vec3::new(30.0, 15.0, -1.0), Vec3::Z, 4.0, 22.0).unwrap();
|
||||
let drilled = boolean(&ribbed(), &hole, BoolOp::Cut).unwrap().unwrap();
|
||||
let (added, lumps, how) = change(&drilled, &channel(), BoolOp::Cut);
|
||||
let (added, lumps) = change(&drilled, &channel(), BoolOp::Cut);
|
||||
// 200 x 40 x 10 again (the hole is measured on its mesh, before and after).
|
||||
assert!((added + 80_000.0).abs() < 1e-3 && lumps == 1, "{added}, {lumps} lumps, {how:?}");
|
||||
assert!((added + 80_000.0).abs() < 1e-3 && lumps == 1, "{added}, {lumps} lumps");
|
||||
}
|
||||
|
||||
/// A pocket whose top lies in the plane of the block's top face, and a pad whose foot does.
|
||||
@@ -59,12 +58,12 @@ fn faces_in_a_common_plane_cut_and_join() {
|
||||
let block = block(200.0, 100.0, 20.0);
|
||||
// A pocket 50 x 60 x 5 from the top face: 15 000 less.
|
||||
let pocket = box_solid(Vec3::new(75.0, 20.0, 15.0), Vec3::new(125.0, 80.0, 20.0)).unwrap();
|
||||
let (added, _, how) = change(&block, &pocket, BoolOp::Cut);
|
||||
assert!((added + 15_000.0).abs() < 1e-6, "{added}, {how:?}");
|
||||
let (added, _) = change(&block, &pocket, BoolOp::Cut);
|
||||
assert!((added + 15_000.0).abs() < 1e-6, "{added}");
|
||||
// A pad 50 x 60 x 10 on the top face: 30 000 more.
|
||||
let pad = box_solid(Vec3::new(75.0, 20.0, 20.0), Vec3::new(125.0, 80.0, 30.0)).unwrap();
|
||||
let (added, lumps, how) = change(&block, &pad, BoolOp::Union);
|
||||
assert!((added - 30_000.0).abs() < 1e-6 && lumps == 1, "{added}, {how:?}");
|
||||
let (added, lumps) = change(&block, &pad, BoolOp::Union);
|
||||
assert!((added - 30_000.0).abs() < 1e-6 && lumps == 1, "{added}");
|
||||
}
|
||||
|
||||
/// A disc joined into a round hole it fills (issue: "boolean Union: no result" for a 1 mm
|
||||
@@ -78,10 +77,9 @@ fn a_disc_joined_into_the_hole_it_fills() {
|
||||
for (z0, z1) in [(0.0, 1.0), (5.0, 8.0), (19.0, 20.0)] {
|
||||
let disc = cylinder(Vec3::new(150.0, 50.0, z0), Vec3::Z, 20.0, z1 - z0).unwrap();
|
||||
let want = measure(&disc).unwrap().volume;
|
||||
let (r, how) = capture_boolean_diagnostics(|| boolean(&drilled, &disc, BoolOp::Union));
|
||||
let r = r.unwrap().unwrap();
|
||||
let r = boolean(&drilled, &disc, BoolOp::Union).unwrap().unwrap();
|
||||
let added = measure(&r).unwrap().volume - measure(&drilled).unwrap().volume;
|
||||
assert!((added - want).abs() < want * 1e-3, "{z0}..{z1}: {added} vs {want}, {how:?}");
|
||||
assert!((added - want).abs() < want * 1e-3, "{z0}..{z1}: {added} vs {want}");
|
||||
assert!(r.lumps().unwrap().len() == 1 && r.validity().is_empty(), "{z0}..{z1}: {:?}", r.validity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ fn substitute_a_body_made_from_a_sketch() {
|
||||
#[test]
|
||||
fn bodies_without_reported_history_have_none() {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(4.0, 3.0, 1.0)).unwrap();
|
||||
let bare = Body::new(a.deep_copy()).unwrap();
|
||||
let bare = Body::from_record(a.record().clone()).unwrap();
|
||||
assert!(bare.history().is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@ fn rect(x0: f64, y0: f64, x1: f64, y1: f64, z0: f64, z1: f64) -> Body {
|
||||
box_solid(Vec3::new(x0, y0, z0), Vec3::new(x1, y1, z1)).unwrap()
|
||||
}
|
||||
|
||||
fn fused(a: &Body, b: &Body) -> (Body, BooleanDiagnostics) {
|
||||
let (r, d) = capture_boolean_diagnostics(|| boolean(a, b, BoolOp::Union));
|
||||
(r.unwrap().unwrap(), d)
|
||||
fn fused(a: &Body, b: &Body) -> Body {
|
||||
boolean(a, b, BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn assert_one_solid(r: &Body, volume: f64, tol: f64, what: &str) {
|
||||
@@ -34,9 +33,8 @@ fn two_ls_up_and_down_fuse_into_one_solid() {
|
||||
let up = prism(&[[-10., -10.], [10., -10.], [10., 0.], [0., 0.], [0., 10.], [-10., 10.]], 0., 10.);
|
||||
let down = prism(&[[-10., 10.], [-10., 0.], [0., 0.], [0., -10.], [10., -10.], [10., 10.]], -10., 0.);
|
||||
for (a, b) in [(&up, &down), (&down, &up)] {
|
||||
let (r, d) = fused(a, b);
|
||||
let r = fused(a, b);
|
||||
assert_one_solid(&r, 6000.0, 1e-6, "#679");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
// Two tops, two bottoms, the two cells left open at z = 0, six sides.
|
||||
assert_eq!(r.face_count(), 12);
|
||||
}
|
||||
@@ -51,12 +49,12 @@ fn several_contact_regions_and_edge_contact() {
|
||||
let ups = boolean(&up, &up2, BoolOp::Union).unwrap().unwrap();
|
||||
// Down under both up cells, and under one more.
|
||||
let down = prism(&[[-10., -10.], [0., -10.], [0., 0.], [10., 0.], [10., 10.], [-10., 10.]], -5., 0.);
|
||||
let (r, _) = fused(&ups, &down);
|
||||
let r = fused(&ups, &down);
|
||||
assert_one_solid(&r, 2.0 * 640.0 + 3.0 * 500.0, 1e-6, "two contacts");
|
||||
// Down under one up cell and beside the other: one face contact and one edge contact.
|
||||
// A plate under one block, flush with two of its sides and running on past the third.
|
||||
let down = rect(-10., -10., -2., 5., -5., 0.);
|
||||
let (r, _) = fused(&up, &down);
|
||||
let r = fused(&up, &down);
|
||||
assert_one_solid(&r, 640.0 + 8.0 * 15.0 * 5.0, 1e-6, "contact and flush sides");
|
||||
}
|
||||
|
||||
@@ -71,9 +69,8 @@ fn a_pad_half_past_the_edge_of_its_face_fuses() {
|
||||
let want = measure(&block).unwrap().volume + measure(&pad).unwrap().volume;
|
||||
assert!((want - (25.0 * 47.5 * 37.5 + std::f64::consts::PI * 25.0 * 2.5)).abs() < 0.1);
|
||||
for (a, b) in [(&block, &pad), (&pad, &block)] {
|
||||
let (r, d) = fused(a, b);
|
||||
let r = fused(a, b);
|
||||
assert_one_solid(&r, want, 1e-6 * want, "#674");
|
||||
assert!(d.events.is_empty(), "an exact join: {d:?}");
|
||||
let bounds = r.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().bounds();
|
||||
assert!((bounds.min.y + 2.5).abs() < 1e-9 && (bounds.min.z + 5.0).abs() < 0.01, "{bounds:?}");
|
||||
}
|
||||
@@ -101,9 +98,8 @@ fn random_rectangles_either_side_of_a_plane_fuse_exactly() {
|
||||
let down = rect(x1, y1, x1 + w1, y1 + h1, -t1, 0.);
|
||||
let want = w0 * h0 * t0 + w1 * h1 * t1;
|
||||
let what = format!("case {case}: up {x0},{y0} {w0}x{h0}x{t0}, down {x1},{y1} {w1}x{h1}x{t1}");
|
||||
let (r, d) = fused(&up, &down);
|
||||
let r = fused(&up, &down);
|
||||
assert_one_solid(&r, want, 1e-6 * want, &what);
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{what}: {d:?}");
|
||||
done += 1;
|
||||
}
|
||||
assert_eq!(done, 30);
|
||||
|
||||
@@ -406,22 +406,6 @@ fn unfilleted_knife_edge_input_has_a_closed_display_plan() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The construction does not depend on the faces being curved: at a box's corner it gives the
|
||||
/// body the planar corner builder does (a sphere's octant where the three rounds meet).
|
||||
#[test]
|
||||
fn the_sphere_at_a_box_corner_is_the_planar_corner_builders() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(30.0, 20.0, 10.0)).unwrap();
|
||||
let picks = [Vec3::new(15.0, 0.0, 10.0), Vec3::new(0.0, 10.0, 10.0), Vec3::new(0.0, 0.0, 5.0)];
|
||||
for r in [1.0, 2.5] {
|
||||
let local = crate::blend_corner::fillet_radii(&b, &picks.iter().map(|p| (*p, r)).collect::<Vec<_>>()).unwrap();
|
||||
let ball = crate::blend_vertex::fillet_ball_vertices(&b, &picks, r).unwrap_or_else(|e| panic!("r={r}: {e}"));
|
||||
assert!(ball.validity().is_empty(), "r={r}: {:?}", ball.validity());
|
||||
assert_eq!(ball.face_count(), local.face_count(), "r={r}");
|
||||
let (a, c) = (planned_volume(&local), planned_volume(&ball));
|
||||
assert!((a - c).abs() < 1e-3 * a, "r={r}: planar corner {a}, ball corner {c}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizes at the ends of the range at a ball vertex: rounded or refused, never a panic.
|
||||
#[test]
|
||||
fn rounds_ending_in_a_sphere_of_any_size_never_panic() {
|
||||
|
||||
@@ -35,8 +35,7 @@ fn regions_cut_from_a_body_keeping_its_meshes_are_unchanged() {
|
||||
);
|
||||
}
|
||||
let memo = kept.meshes.as_ref().unwrap();
|
||||
assert_eq!(memo.shells.lock().unwrap().len(), 1, "one tolerance, one meshing");
|
||||
assert!(!memo.made.lock().unwrap().is_empty());
|
||||
assert_eq!(memo.made.lock().unwrap().len(), 1, "one tolerance, one meshing");
|
||||
// A mesh from the memo is the mesh made afresh.
|
||||
let (a, b) = (kept.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(), plate.mesh(&crate::MeshTolerance::chord(0.05)).unwrap());
|
||||
let (a2, _) = (kept.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(), ());
|
||||
@@ -62,5 +61,4 @@ fn a_copy_with_another_solid_makes_its_own_meshes() {
|
||||
let _ = kept.mesh(&crate::MeshTolerance::chord(0.5)).unwrap();
|
||||
let m = swapped.mesh(&crate::MeshTolerance::chord(0.5)).unwrap();
|
||||
assert_eq!(m.triangles.len(), other.mesh(&crate::MeshTolerance::chord(0.5)).unwrap().triangles.len());
|
||||
assert!(swapped.boolean_meshes(0.01).is_none());
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ fn a_lip_swept_on_a_round_rim_joins_it() {
|
||||
#[test]
|
||||
fn a_swept_lips_foot_is_a_plane() {
|
||||
let l = lip(0.0);
|
||||
let foot = crate::coplanar::faces_in(&l, -Vec3::Z, -10.0, 1e-6);
|
||||
// The faces lying in the plane z = 10 facing down: planar, normal -z, through z = 10.
|
||||
let foot: Vec<usize> = (0..l.face_count())
|
||||
.filter(|&i| l.planar_face(i).is_some_and(|p| p.normal().dot(-Vec3::Z) > 1.0 - 1e-9 && (p.origin.z - 10.0).abs() < 1e-6))
|
||||
.collect();
|
||||
let m = l.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
let area: f64 = m
|
||||
.triangles
|
||||
|
||||
@@ -41,10 +41,9 @@ fn disc_tangent_to_a_side_and_overhanging_joins() {
|
||||
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 = 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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,10 +128,9 @@ fn hole_tangent_inside_a_stepped_boss_cuts_exactly() {
|
||||
for (z0, z1, want) in [(-1., 20., 2000. + 1000. - 25. * 15.), (6., 20., 2000. + 1000. - 25. * 9.), (-1., 4., 2000. + 1000. - 25. * 4.)] {
|
||||
let what = format!("hole z {z0}…{z1}, turned {turn}, flip {flip}");
|
||||
let hole = prism(circle(15., 5.), z0, z1);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &hole, BoolOp::Cut));
|
||||
let res = boolean(&stepped, &hole, BoolOp::Cut);
|
||||
let res = res.unwrap_or_else(|e| panic!("{what}: {e}")).unwrap();
|
||||
assert_solid(&res, PI * want, 1e-5, 1, &what);
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{what}: {d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,10 +144,9 @@ fn hole_tangent_to_a_flat_side_of_a_stepped_block_cuts_exactly() {
|
||||
let boss = prism(rect(10., 10., 40., 30.), 5., 15.);
|
||||
let stepped = boolean(&block, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let hole = prism(disc(35., 20., 5.), -1., 20.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &hole, BoolOp::Cut));
|
||||
let res = boolean(&stepped, &hole, BoolOp::Cut);
|
||||
let res = res.unwrap().unwrap();
|
||||
assert_solid(&res, 8000. + 6000. - std::f64::consts::PI * 25. * 15., 1e-5, 1, "hole tangent to a flat side");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
}
|
||||
|
||||
/// A stepped block cut by a disc tangent to its back side from inside leaves two posts meeting
|
||||
@@ -163,7 +160,7 @@ fn stepped_cuts_touching_along_a_line_keep_their_pieces() {
|
||||
let back = prism(rect(0., 20., 25., 29.), 10., 20.);
|
||||
let stepped = boolean(&block, &back, BoolOp::Union).unwrap().unwrap();
|
||||
let tool = prism(disc(18.5, 4., 25.), -1., 21.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &tool, BoolOp::Cut));
|
||||
let res = boolean(&stepped, &tool, BoolOp::Cut);
|
||||
let res = res.unwrap().unwrap();
|
||||
let n = 400_000;
|
||||
let dx = 31.0 / n as f64;
|
||||
@@ -176,18 +173,14 @@ fn stepped_cuts_touching_along_a_line_keep_their_pieces() {
|
||||
.sum();
|
||||
let v = res.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((v - volume).abs() <= volume * 1e-4, "posts: volume {v} vs {volume}");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
let sewn = crate::prism::prism_boolean(&stepped, &tool, BoolOp::Cut);
|
||||
assert!(!matches!(&sewn, Some(Ok(Some(b))) if b.shell_count() < 2), "posts sewn into one shell");
|
||||
|
||||
let block = prism(rect(0., 0., 40., 40.), 0., 5.);
|
||||
let boss = prism(rect(10., 10., 40., 30.), 5., 15.);
|
||||
let stepped = boolean(&block, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let diamond = prism(poly(&[[30., 20.], [35., 15.], [40., 20.], [35., 25.]]), -1., 20.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &diamond, BoolOp::Cut));
|
||||
let res = boolean(&stepped, &diamond, BoolOp::Cut);
|
||||
let res = res.unwrap().unwrap();
|
||||
assert_solid(&res, 8000. + 6000. - 50. * 15., 1e-9, 1, "diamond hole with a corner on a stepped side");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
}
|
||||
|
||||
/// A row of holes each touching the next (a 5 mm pattern of Ø5 holes) through a stepped plate:
|
||||
@@ -199,9 +192,8 @@ fn row_of_touching_holes_through_a_stepped_plate_cuts_exactly() {
|
||||
let mut body = boolean(&plate, ¬ch, BoolOp::Cut).unwrap().unwrap();
|
||||
for x in [15.45, 20.45, 25.45] {
|
||||
let hole = prism(disc(x, 15.7, 2.5), -5., 7.5);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&body, &hole, BoolOp::Cut));
|
||||
let res = boolean(&body, &hole, BoolOp::Cut);
|
||||
body = res.unwrap().unwrap();
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "hole at {x}: {d:?}");
|
||||
}
|
||||
assert_solid(&body, 7312.5 - 3.0 * std::f64::consts::PI * 6.25 * 7.5, 1e-5, 1, "touching holes");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Round faces read from STEP: a whole turn bounded by one wire that runs along its seam twice.
|
||||
//! A boolean whose intersection loop lies wholly inside such a face is exact.
|
||||
use crate::*;
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const R: f64 = 4.5;
|
||||
|
||||
fn volume(b: &Body) -> f64 {
|
||||
measure(b).unwrap().volume
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs().max(1e-12)
|
||||
}
|
||||
|
||||
/// `body` written to STEP and read back: its round faces come back whole, as other systems
|
||||
/// write them.
|
||||
fn through_step(body: &Body) -> Body {
|
||||
step_import(&step_export(&[body], "t").unwrap()).unwrap().bodies.into_iter().next().unwrap().body
|
||||
}
|
||||
|
||||
/// A pin of radius `R` along X, 40 long, centred on the origin.
|
||||
fn pin() -> Body {
|
||||
cylinder(Vec3::new(-20.0, 0.0, 0.0), Vec3::X, R, 40.0).unwrap()
|
||||
}
|
||||
|
||||
/// A block pressed into the top of the pin's side: 10 long, 6 wide, from 2 above the axis up.
|
||||
fn block() -> Body {
|
||||
box_solid(Vec3::new(-5.0, -3.0, 2.0), Vec3::new(5.0, 3.0, 9.0)).unwrap()
|
||||
}
|
||||
|
||||
/// The part of the pin inside [`block`]: the circle's segment above 2 and within 3 of the
|
||||
/// middle, 10 long.
|
||||
fn pressed_volume() -> f64 {
|
||||
let segment = 3.0 * (R * R - 9.0).sqrt() + R * R * (3.0 / R).asin() - 6.0 * 2.0;
|
||||
10.0 * segment
|
||||
}
|
||||
|
||||
/// The boolean, which must succeed (SMLib's, exact: there is no other).
|
||||
fn exact(a: &Body, b: &Body, op: BoolOp) -> Option<Body> {
|
||||
boolean(a, b, op).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_pressed_into_a_read_pins_side_is_intersected_and_cut_exactly() {
|
||||
let (read, block) = (through_step(&pin()), block());
|
||||
let want = pressed_volume();
|
||||
let shared = exact(&read, &block, BoolOp::Intersect).unwrap();
|
||||
assert!(rel(volume(&shared), want) < 1e-3, "{} {want}", volume(&shared));
|
||||
let dented = exact(&read, &block, BoolOp::Cut).unwrap();
|
||||
assert!(rel(volume(&dented), PI * R * R * 40.0 - want) < 1e-3, "{}", volume(&dented));
|
||||
// The operands' faces carry over: the dented pin's round face is still the pin's.
|
||||
assert!(dented.history().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ball_pressed_into_a_read_pins_side_matches_the_built_pin() {
|
||||
let ball = sphere(Vec3::new(3.0, 1.0, 5.5), 3.0).unwrap();
|
||||
let built = volume(&boolean(&pin(), &ball, BoolOp::Intersect).unwrap().unwrap());
|
||||
let read = volume(&exact(&through_step(&pin()), &ball, BoolOp::Intersect).unwrap());
|
||||
assert!(rel(read, built) < 1e-3, "{read} {built}");
|
||||
}
|
||||
|
||||
/// A cone's side read from STEP is one face along its seam as well.
|
||||
#[test]
|
||||
fn a_block_pressed_into_a_read_cones_side_matches_the_built_cone() {
|
||||
let outline =
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(8.0, 0.0), Vec2::new(5.0, 20.0), Vec2::new(0.0, 20.0)]), holes: vec![] };
|
||||
let cone = revolve(&Plane::YZ, &[outline], Vec2::ZERO, Vec2::Y, 2.0 * PI).unwrap().pop().unwrap();
|
||||
let read = through_step(&cone);
|
||||
let block = box_solid(Vec3::new(-2.0, 4.0, 6.0), Vec3::new(2.0, 12.0, 12.0)).unwrap();
|
||||
let built = volume(&boolean(&cone, &block, BoolOp::Intersect).unwrap().unwrap());
|
||||
let read = volume(&exact(&read, &block, BoolOp::Intersect).unwrap());
|
||||
assert!(rel(read, built) < 1e-3, "{read} {built}");
|
||||
}
|
||||
@@ -2326,9 +2326,6 @@ fn orientation_mesh_reuses_missing_face_check_at_matching_tolerance() {
|
||||
present[face as usize] = true;
|
||||
}
|
||||
assert_eq!(body.unmeshed_faces(), present.iter().filter(|p| !**p).count());
|
||||
if scale == 1.0 {
|
||||
assert_eq!(body.coarse_missing, Some((tol, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2473,7 +2470,7 @@ fn loft_joined_onto_a_box_top_is_one_shell() {
|
||||
// Overhanging the box edge (x = 20) the disc is not inside the top face: whatever the join
|
||||
// does, it never returns touching shells as one body.
|
||||
if let Ok(Some(r)) = boolean(&bx, &loft_at(17.0), BoolOp::Union) {
|
||||
assert!(r.shell_count() == 1 || r.union_lumps().is_ok(), "{} touching shells", r.shell_count());
|
||||
assert!(r.shell_count() == 1 || r.lumps().is_ok(), "{} touching shells", r.shell_count());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ fn chamfer_the_top_lip_at_a_tangent_wall_junction() {
|
||||
assert_eq!(made.crossed_face_loops().unwrap(), 0);
|
||||
let history = made.history().expect("the chamfer retains exact face provenance");
|
||||
assert!(history.faces.iter().any(|f| f.origins.iter().any(|o| matches!(o, solidcraft_kernel::history::FaceOrigin::Edge { operand: 0, .. }))));
|
||||
let mesh = |body: &Body| body.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap();
|
||||
let mesh = |body: &Body| body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.0005)).unwrap();
|
||||
let before = mesh(&body).measure().volume;
|
||||
let after = mesh(&made);
|
||||
let removed = before - after.measure().volume;
|
||||
@@ -114,7 +114,7 @@ fn one_chamfer_pick_follows_the_tangent_chain_at_any_part_size() {
|
||||
assert!(made.validity().is_empty(), "{:?}", made.validity());
|
||||
// Two straight edges (c^2/2 * 40 each) and both end arcs (Pappus, c^2/2 * 2 pi (R - c/3)).
|
||||
let expected = 40.0 + 0.5 * 2.0 * PI * (5.0 - 1.0 / 3.0);
|
||||
let volume = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let volume = |b: &Body| b.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let removed = volume(&body) - volume(&made);
|
||||
assert!((removed - expected).abs() < 0.3, "plate {plate}: removed {removed}, expected {expected}");
|
||||
assert_eq!(made.face_count(), body.face_count() + 4, "plate {plate}: one chamfer face per rim edge");
|
||||
|
||||
@@ -75,7 +75,7 @@ fn a_half_slot_revolved_about_a_centreline_in_a_face_cuts_it() {
|
||||
for turn in [PI, -PI, TAU] {
|
||||
let tool = revolve(&plane, std::slice::from_ref(&r), Vec2::ZERO, Vec2::new(1.0, 0.0), turn).unwrap().pop().unwrap();
|
||||
// A half turn into the plate, or a full one, cuts a groove; a half turn out of it misses.
|
||||
let into = turn.abs() > PI || tool.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().bounds().min.z < 9.0;
|
||||
let into = turn.abs() > PI || tool.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap().bounds().min.z < 9.0;
|
||||
let cut = boolean(&plate, &tool, BoolOp::Cut).unwrap().unwrap();
|
||||
assert!(cut.validity().is_empty(), "flip {flip}, {turn}: {:?}", cut.validity());
|
||||
let removed = whole - measure(&cut).unwrap().volume;
|
||||
|
||||
Reference in New Issue
Block a user