Files
OneCAD/crates/kernel/src/issue_679_tests.rs
T
bherbruckandClaude Opus 5.5 add4a18961 fix: fuse bodies that touch back to back over part of a face (#679, #674) (#704)
Two L's extruded up and down from one 2 x 2 grid now join into one solid (the planar rebuild gives each fan of faces around a pinched vertex its own vertex copy), and a pad whose end lies half past the edge of the face it stands on fuses exactly: contact faces are imprinted with each other's outline and the common faces glued away, still held to the retry guard. Face splitting handles a line crossing an edge twice; glue matches differently parametrised curves and keeps several pieces apart. Robustness seed 16 now fuses with the exact volume.

Fixes #679, fixes #674.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:02:44 -04:00

111 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Bodies touching back to back over part of a face fuse into one solid: two L's of a 2 × 2
//! grid extruded up and down (#679), a pad half past the edge of the face it stands on (#674),
//! several contact regions, contact beside a shared edge, and random rectangles.
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use crate::*;
fn prism(pts: &[[f64; 2]], z0: f64, z1: f64) -> Body {
let outer = Loop2::polygon(&pts.iter().map(|p| Vec2::new(p[0], p[1])).collect::<Vec<_>>());
extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], z0, z1).unwrap().pop().unwrap()
}
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 assert_one_solid(r: &Body, volume: f64, tol: f64, what: &str) {
let m = measure(r).unwrap();
assert!((m.volume - volume).abs() <= tol, "{what}: volume {} vs {volume}", m.volume);
assert_eq!(r.shell_count(), 1, "{what}: one solid");
assert!(r.validity().is_empty(), "{what}: {:?}", r.validity());
}
/// #679: XO/XX extruded up 10, XX/OX down 10, 10 mm cells. They meet back to back over two
/// whole cells, along an edge and at the centre point: 6 cells × 10 mm × 100 mm².
#[test]
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);
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);
}
}
/// Two blocks up from z = 0 and one plate down under both: two separate contacts fuse into one
/// solid. A plate under a block, flush with its sides along shared edges.
#[test]
fn several_contact_regions_and_edge_contact() {
let up = rect(-10., -10., -2., -2., 0., 10.);
let up2 = rect(2., 2., 10., 10., 0., 10.);
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);
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);
assert_one_solid(&r, 640.0 + 8.0 * 15.0 * 5.0, 1e-6, "contact and flush sides");
}
/// #674 (robustness seed 16): a short cylinder whose end lies on a block's side with half of it
/// past the block's bottom edge. The end is split along the edge, the block's side along the
/// circle, and the half disc they share goes.
#[test]
fn a_pad_half_past_the_edge_of_its_face_fuses() {
let block = rect(0., 0., 25., 47.5, 0., 37.5);
let pad = cylinder(Vec3::new(7.5, 0., 0.), -Vec3::Y, 5.0, 2.5).unwrap();
// Measured as the result is (the round side's volume from its mesh).
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);
assert_one_solid(&r, want, 1e-6 * want, "#674");
assert!(d.events.is_empty(), "an exact join: {d:?}");
let bounds = r.tessellate(0.01).unwrap().bounds();
assert!((bounds.min.y + 2.5).abs() < 1e-9 && (bounds.min.z + 5.0).abs() < 0.01, "{bounds:?}");
}
}
/// Random rectangles extruded up from z = 0 and down from it, overlapping anywhere (inside one
/// another, crossing, sharing a side): one solid of exactly the two prisms' volume.
#[test]
fn random_rectangles_either_side_of_a_plane_fuse_exactly() {
let mut seed = 0x679_u64;
let mut next = |lo: f64, hi: f64| {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
lo + (hi - lo) * ((seed >> 11) as f64 / (1u64 << 53) as f64)
};
let q = |x: f64| (x * 2.0).round() / 2.0;
let mut done = 0;
for case in 0..30 {
let (x0, y0) = (q(next(0., 20.)), q(next(0., 20.)));
let (w0, h0) = (q(next(2., 20.)), q(next(2., 20.)));
// The second overlaps the first over an area.
let (x1, y1) = (q(next(x0 - 15., x0 + w0 - 1.)), q(next(y0 - 15., y0 + h0 - 1.)));
let (w1, h1) = (q(next(1., 25.)).max(x0 + 1. - x1).max(1.), q(next(1., 25.)).max(y0 + 1. - y1).max(1.));
let (t0, t1) = (q(next(1., 10.)), q(next(1., 10.)));
let up = rect(x0, y0, x0 + w0, y0 + h0, 0., t0);
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);
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);
}