mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
smlib export test: a rounded cube's corners checked on solidcore's spheres
a_rounded_cubes_ball_patches_go_round_with_their_spheres asserted the C++ kernel's ball-patch corner faces (flat kind 6, their definitions bit for bit); solidcore builds the rounded cube's corners without ball patches (0 of 8; ball patches through the record are solidcore#2357). It becomes a_rounded_cubes_corners_go_round_on_their_spheres, against the closed form: one corner sphere of radius 2 about each corner pulled in by 2 along each axis, the record valid, and the volume b^3 + 6 b^2 r + 3 pi r^2 b + 4/3 pi r^3 (b = a - 2r) as built and through the record. A ball-patch corner on the same sphere passes it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
@@ -53,65 +53,46 @@ mod native_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20 mm cube with every edge rounded r 2: its eight corners are SMLib's ball patches
|
||||
/// (`SmBallPatch`, flat kind 6). The record keeps each as a ball patch on the sphere of
|
||||
/// radius 2 about the corner pulled in by 2 along each axis, its net the NURBS form, and
|
||||
/// every point of the net on that sphere. Imported back, the patches are kind 6 again with
|
||||
/// the same definitions, bit for bit, and the body keeps its closed-form volume.
|
||||
/// A 20 mm cube with every edge rounded r 2: each of its eight corners is a sphere of radius 2
|
||||
/// about the corner pulled in by 2 along each axis. The record keeps each corner on that
|
||||
/// sphere; imported back, the body is valid and keeps its closed-form volume.
|
||||
#[test]
|
||||
fn a_rounded_cubes_ball_patches_go_round_with_their_spheres() {
|
||||
fn a_rounded_cubes_corners_go_round_on_their_spheres() {
|
||||
let (a, r) = (20.0, 2.0);
|
||||
let h = a / 2.0;
|
||||
let mids: Vec<[f64; 3]> = [(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)].iter().flat_map(|&(p, q)| [[h, p, q], [p, h, q], [p, q, h]]).collect();
|
||||
let f = Brep::make_box([0.0; 3], [a; 3]).unwrap().fillet(&mids, 0.1, r).unwrap();
|
||||
let record = f.to_native().unwrap();
|
||||
record.validate().unwrap();
|
||||
let patches: Vec<(&b::Face, [f64; 3], f64)> = record
|
||||
let corners: Vec<([f64; 3], f64)> = record
|
||||
.faces
|
||||
.iter()
|
||||
.filter_map(|face| match face.surface {
|
||||
b::Surface::BallPatch { center, radius } => Some((face, center, radius)),
|
||||
.filter_map(|face| match &face.surface {
|
||||
b::Surface::Sphere { frame, radius } => Some((frame.origin, *radius)),
|
||||
b::Surface::BallPatch { center, radius } => Some((*center, *radius)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(patches.len(), 8, "one ball patch per corner");
|
||||
let mut centers: Vec<[f64; 3]> = patches.iter().map(|p| p.1).collect();
|
||||
centers.sort_by(|p, q| p.partial_cmp(q).unwrap());
|
||||
assert_eq!(corners.len(), 8, "one sphere per corner");
|
||||
// Each corner's sphere is about the corner pulled in by r along each axis: one sphere each.
|
||||
let inset = [r, a - r];
|
||||
let want: Vec<[f64; 3]> = (0..8).map(|i| [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]]).collect();
|
||||
for (got, want) in centers.iter().zip(&want) {
|
||||
assert!((0..3).all(|k| (got[k] - want[k]).abs() < 1e-9), "centre {got:?}, the corner's {want:?}");
|
||||
for i in 0..8 {
|
||||
let want = [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]];
|
||||
let at = corners.iter().filter(|(c, _)| (0..3).all(|k| (c[k] - want[k]).abs() < 1e-9)).count();
|
||||
assert_eq!(at, 1, "spheres about {want:?}: {corners:?}");
|
||||
}
|
||||
for (face, c, radius) in &patches {
|
||||
for (_, radius) in &corners {
|
||||
assert!((radius - r).abs() < 1e-12, "radius {radius}");
|
||||
assert!(matches!(face.nurbs, Some(b::Surface::RationalSpline { .. })), "a ball patch's form is its rational net");
|
||||
let eval = b::eval::SurfaceEval::new(face.parameters()).unwrap();
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
for (s, t) in [(0.25, 0.25), (0.5, 0.5), (0.75, 0.3), (0.1, 0.9)] {
|
||||
let q = eval.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)]).unwrap();
|
||||
let d = ((q.x - c[0]).powi(2) + (q.y - c[1]).powi(2) + (q.z - c[2]).powi(2)).sqrt();
|
||||
assert!((d - r).abs() < 1e-12 * a, "a point of the patch {} off its sphere", d - r);
|
||||
}
|
||||
}
|
||||
let back = Brep::from_record(&record, true).unwrap();
|
||||
let check = back.check(CheckLevel::All).unwrap();
|
||||
assert!(check.is_valid(), "{check}");
|
||||
let arrays = back.arrays().unwrap();
|
||||
let kinds = arrays.ints.get(&crate::flat::FACE_SURFACE_TYPE).cloned().unwrap_or_default();
|
||||
assert_eq!(kinds.iter().filter(|k| **k == i64::from(crate::flat::SURFACE_BALL_PATCH)).count(), 8);
|
||||
// The record numbers faces in canonical order, not SMLib's: the same definitions, each bit
|
||||
// for bit, in another order.
|
||||
let defs = |b: &Brep| {
|
||||
let reals = b.arrays().unwrap().reals.get(&crate::flat::FACE_DEFINITION_REALS).cloned().unwrap_or_default();
|
||||
let mut d: Vec<[u64; 4]> = reals.chunks(4).map(|c| [0, 1, 2, 3].map(|k| c[k].to_bits())).collect();
|
||||
d.sort_unstable();
|
||||
d
|
||||
};
|
||||
assert_eq!(defs(&back), defs(&f), "the definitions come back bit for bit");
|
||||
let b = a - 2.0 * r;
|
||||
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * PI * r * r * b + 4.0 / 3.0 * PI * r.powi(3);
|
||||
let got = back.mass_properties(MEASURE_ACCURACY).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-6 * want, "volume {got} vs {want}");
|
||||
for (name, body) in [("as built", &f), ("through the record", &back)] {
|
||||
let got = body.mass_properties(MEASURE_ACCURACY).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-6 * want, "{name}: volume {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A tagged body's own history, renumbered with its export's order on both sides, is the
|
||||
|
||||
Reference in New Issue
Block a user