Files
OneCAD/crates/kernel/src/issue_1038_tests.rs
T

82 lines
4.3 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.
//! #1038: a small hole on a large curved face divides its intersection edges from each point's
//! neighbour (the vendored `IntersectionCurve::parameter_division`) instead of searching both
//! surfaces from nothing for every point. These pin the hinted division to the search from
//! nothing, so a refresh of the vendored crate can't quietly lose the speedup or change a body.
use std::time::{Duration, Instant};
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use truck_modeling as mt;
use truck_modeling::{BoundedCurve, ParameterDivision1D};
use crate::{Body, BoolOp, build::extrude, cylinder, ops::boolean};
/// A 200 × 200 block whose top is one cubic wave across x (a single B-spline face), with a
/// Ø1 mm hole through it at the middle.
fn holed() -> (Body, Body) {
let plane = Plane::new(Vec3::new(0.0, 200.0, 0.0), Vec3::X, Vec3::Z).unwrap();
let (a, b, c, d) = (Vec2::new(0.0, 0.0), Vec2::new(200.0, 0.0), Vec2::new(200.0, 40.0), Vec2::new(0.0, 40.0));
let top = Seg2::Cubic { p0: c, p1: Vec2::new(133.0, 70.0), p2: Vec2::new(67.0, 10.0), p3: d };
let outer = Loop2 { segs: vec![Seg2::Line { a, b }, Seg2::Line { a: b, b: c }, top, Seg2::Line { a: d, b: a }] };
let block = extrude(&plane, &[Region2 { outer, holes: vec![] }], 0.0, 200.0).unwrap().pop().unwrap();
let tool = cylinder(Vec3::new(100.0, 100.0, -10.0), Vec3::Z, 0.5, 100.0).unwrap();
let cut = boolean(&block, &tool, BoolOp::Cut).unwrap().unwrap();
(block, cut)
}
fn intersection_curves(body: &Body) -> Vec<mt::Curve> {
body.solid.edge_iter().map(|e| e.curve()).filter(|c| matches!(c, mt::Curve::IntersectionCurve(_))).collect()
}
fn timed<T>(f: impl Fn() -> T) -> (T, Duration) {
let start = Instant::now();
let out = f();
(out, start.elapsed())
}
#[test]
fn a_small_hole_in_a_large_spline_face_cuts_cleanly() {
let (block, cut) = holed();
assert!(cut.validity().is_empty(), "{:?}", cut.validity());
// The hole adds its wall (two half cylinders) and splits nothing: the bottom and the wave
// each gain a loop.
assert_eq!(cut.face_count(), block.face_count() + 2);
let curves = intersection_curves(&cut);
assert!(!curves.is_empty(), "the hole's rim on the wave is an intersection edge");
// The wave is within a few tenths of a millimetre of flat across a Ø1 hole, so the hole
// removes a cylinder as tall as the wave at its axis (the cubic's middle: z = 40).
let (v0, v1) = (block.volume_at(0.001).unwrap(), cut.volume_at(0.001).unwrap());
let removed = std::f64::consts::PI * 0.25 * 40.0;
assert!((v0 - v1 - removed).abs() < 0.05 * removed, "removed {} of {removed}", v0 - v1);
}
#[test]
fn hinted_division_matches_the_search_from_nothing_and_is_faster() {
let (_, cut) = holed();
let (mut hinted_time, mut scratch_time) = (Duration::ZERO, Duration::ZERO);
for curve in intersection_curves(&cut) {
let mt::Curve::IntersectionCurve(ic) = &curve else { continue };
let range = ic.range_tuple();
for tol in [0.01, 0.001] {
let ((hp, hq), ht) = timed(|| ic.parameter_division(range, tol));
let ((sp, _), st) = timed(|| mt::algo::curve::parameter_division(ic, range, tol));
hinted_time += ht;
scratch_time += st;
// The same division up to rounding: the hinted projections converge to within the
// Newton tolerance of the searched ones, so a split the chord test only just asks for
// can come out one way or the other, but no more than that.
assert_eq!((hp.first(), hp.last()), (sp.first(), sp.last()), "tol {tol}");
assert!(hp.len().abs_diff(sp.len()) <= 2, "tol {tol}: {} points hinted, {} from nothing", hp.len(), sp.len());
// Every hinted point is the curve's own point at its parameter.
for (t, h) in hp.iter().zip(&hq) {
let s = ic.search_triple(*t).unwrap().0;
assert!(mt::MetricSpace::distance(*h, s) < 1e-6, "tol {tol}, t {t}: {h:?} vs {s:?}");
}
}
}
eprintln!("hinted {hinted_time:?}, from nothing {scratch_time:?}");
// The hinted path skips each point's grid search of the B-spline face; a loose bound, so a
// loaded runner doesn't trip it, but one a fallback to the search for every point would.
assert!(hinted_time * 2 < scratch_time, "hinted {hinted_time:?}, from nothing {scratch_time:?}");
}