mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Tests: check what each operation is for, not how many faces it makes
The kernel switch broke these tests only through their counts, which record how a kernel splits faces rather than whether the result is right. Each now checks its intent: - a fitted circle is the exact circle (centre, radius, a full turn), not "one segment"; - the tight bend sweeps a valid solid of Pappus' volume, πr²(40 + 5π/2), not a count of faces, edges and vertices; - the Pi case keeps each outer side one face, stays valid after every feature and rebuilds the same topology at 32 whether built there or edited there, without a hard-coded table of totals; - a certified paint stays on the wall's radius and covers as many faces after an edit, a copy and reopening as when it was painted; - the offset sleeve and the extended strip are checked by their exact areas only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,21 +139,14 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
|
||||
/// face count after each feature, and each outer side (the four walls and the bottom) one face
|
||||
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
|
||||
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
|
||||
/// wall at 24) into strips along the openings' tops and bottoms.
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
|
||||
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
|
||||
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
|
||||
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
|
||||
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
|
||||
/// into strips along the openings' tops and bottoms.
|
||||
#[test]
|
||||
fn pi_case_faces_per_step_at_both_heights() {
|
||||
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built. The box
|
||||
// (6), its four corner rounds (10), shelled open at the top (outside 9, inside 9, rim 1:
|
||||
// 19); each standoff (a ring, Ø6 round a Ø2.5 hole) adds its outer and inner wall (each one
|
||||
// cylinder, closed along its seam), its top and the floor inside its hole (4, so 23, and 35
|
||||
// for four); the three port openings through a wall 4 walls each (47); the end cut-out 4
|
||||
// (51); each of five floor vents 4 (55, 71).
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 23), (7, 35), (8, 35), (9, 47), (10, 47), (11, 51), (12, 51), (13, 55), (14, 71)];
|
||||
fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
@@ -176,12 +169,13 @@ fn pi_case_faces_per_step_at_both_heights() {
|
||||
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
|
||||
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
|
||||
let mut steps = Vec::new();
|
||||
for (k, want) in FACES {
|
||||
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
|
||||
for k in 2..=14 {
|
||||
let mut t = s.scratch();
|
||||
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
|
||||
let st = t.world_state().unwrap();
|
||||
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
|
||||
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
|
||||
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
|
||||
// Each outer side: the faces in its plane.
|
||||
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
|
||||
let on = (0..body.body.face_count())
|
||||
|
||||
@@ -15,6 +15,8 @@ fn paint(session: &mut Session, body: &str, point: Value) {
|
||||
run(session, "config.row", json!({"name":"Painted","values":{key:"ABS - Red"}}));
|
||||
run(session, "config.activate", json!({"row":"Painted"}));
|
||||
}
|
||||
/// Every painted face of `name` lies on the cylinder of `radius` round its axis; how many faces
|
||||
/// are painted, to compare a copy with its original.
|
||||
fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
|
||||
let state = session.model.state();
|
||||
let body =
|
||||
@@ -30,7 +32,7 @@ fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(!painted.is_empty());
|
||||
assert!(!painted.is_empty(), "{name}: nothing painted");
|
||||
painted.len()
|
||||
}
|
||||
|
||||
@@ -49,22 +51,22 @@ fn certified_annulus_side_rows_follow_radius_edits_and_clone() {
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance":10,"profiles":[{"point":[9.5,0]}],"body_name":"Ring"}));
|
||||
paint(&mut s, "Ring", json!([10, 0, 5]));
|
||||
assert_eq!(painted_radius(&s, "Ring", 10.), 1, "the outer circle's wall is one face, closed along its seam");
|
||||
let pieces = painted_radius(&s, "Ring", 10.);
|
||||
let Column::FaceAppearance(target) = &s.doc.configs.columns[0] else { panic!() };
|
||||
assert_eq!(target.certificate, Some(FaceCertificate::ExtrudeSide { sketch, curve: "outer".into() }));
|
||||
run(&mut s, "parameters.change", json!({"name":dimensions[0],"expression":"12 mm"}));
|
||||
run(&mut s, "parameters.change", json!({"name":dimensions[1],"expression":"9.25 mm"}));
|
||||
run(&mut s, "config.activate", json!({"row":"Painted"}));
|
||||
assert_eq!(painted_radius(&s, "Ring", 12.), 1);
|
||||
assert_eq!(painted_radius(&s, "Ring", 12.), pieces);
|
||||
run(&mut s, "component.paste_new", json!({"component":part,"translate":[30,0,0]}));
|
||||
assert_eq!(s.doc.configs.columns.len(), 2);
|
||||
run(&mut s, "config.activate", json!({"row":"Painted"}));
|
||||
assert_eq!(painted_radius(&s, "Ring (2)", 12.), 1);
|
||||
assert_eq!(painted_radius(&s, "Ring (2)", 12.), pieces);
|
||||
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
let mut reopened = Session::new(reopened);
|
||||
run(&mut reopened, "config.activate", json!({"row":"Painted"}));
|
||||
assert_eq!(painted_radius(&reopened, "Ring", 12.), 1);
|
||||
assert_eq!(painted_radius(&reopened, "Ring (2)", 12.), 1);
|
||||
assert_eq!(painted_radius(&reopened, "Ring", 12.), pieces);
|
||||
assert_eq!(painted_radius(&reopened, "Ring (2)", 12.), pieces);
|
||||
run(&mut reopened, "config.activate", json!({"row":"Default"}));
|
||||
assert!(reopened.doc.appearances.faces.is_empty());
|
||||
}
|
||||
@@ -227,11 +229,11 @@ fn certified_side_transfer_preserves_geometry_and_unsupported_taper_refuses_atom
|
||||
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[30,20,0],"angle":90,"axis":[0,0,1],"capture":false}));
|
||||
run(&mut s, "component.move_bodies", json!({"component":destination,"bodies":["Round"]}));
|
||||
run(&mut s, "config.activate", json!({"row":"Painted"}));
|
||||
assert_eq!(painted_radius(&s, "Round", 10.), 1);
|
||||
let pieces = painted_radius(&s, "Round", 10.);
|
||||
let before = s.world_state().unwrap().body("Round").unwrap().mesh().bounds();
|
||||
let mut s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
run(&mut s, "config.activate", json!({"row":"Painted"}));
|
||||
assert_eq!(painted_radius(&s, "Round", 10.), 1);
|
||||
assert_eq!(painted_radius(&s, "Round", 10.), pieces);
|
||||
assert!(
|
||||
s.world_state().unwrap().body("Round").unwrap().mesh().bounds().min.dist(before.min) < 1e-8,
|
||||
"before {before:?}, reopened {:?}",
|
||||
|
||||
@@ -1101,7 +1101,7 @@ mod tests {
|
||||
run(&mut s, "surface.offset", json!({"faces": [[10, 0, 10]], "chain": true, "distance": 2, "body": "Can", "body_name": "Sleeve"}));
|
||||
let st = s.world_state().unwrap();
|
||||
let sleeve = &st.body("Sleeve").unwrap().body;
|
||||
assert!(sleeve.is_surface() && sleeve.face_count() == 1);
|
||||
assert!(sleeve.is_surface());
|
||||
let want = 2.0 * std::f64::consts::PI * 12.0 * 20.0;
|
||||
assert!((area(&s, "Sleeve") - want).abs() < want * 1e-3, "{} vs {want}", area(&s, "Sleeve")); // (measured on meshes)
|
||||
assert!(st.body("Can").is_some(), "the source body stays");
|
||||
@@ -1326,8 +1326,6 @@ mod tests {
|
||||
let parabola = 2.5 * (1.0 + 25.0 * k * k).sqrt() + (5.0 * k).asinh() / (2.0 * k);
|
||||
for (mode, run_off) in [("tangent", 5.0), ("curvature", parabola)] {
|
||||
run(&mut s, "surface.extend", json!({"body": "Strip", "edges": [side], "distance": 5, "mode": mode}));
|
||||
let st = s.world_state().unwrap();
|
||||
assert_eq!(st.body("Strip").unwrap().body.face_count(), 1, "{mode}");
|
||||
let want = 10.0 * pi / 2.0 * 20.0 + run_off * 20.0;
|
||||
assert!((area(&s, "Strip") - want).abs() < want * 1e-3, "{mode}: {} vs {want}", area(&s, "Strip")); // (measured on meshes)
|
||||
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
|
||||
@@ -478,8 +478,14 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(fit_loop(&points, 0.001, 1e-6).segs.len(), 16);
|
||||
// A circle is one closed arc.
|
||||
assert_eq!(fit_loop(&points, 0.2, 1e-6).segs.len(), 1);
|
||||
assert_exact_circle(&fit_loop(&points, 0.2, 1e-6), Vec2::ZERO, 10.);
|
||||
}
|
||||
|
||||
/// The loop is the exact circle: one full turn of an arc round `center` at `radius`.
|
||||
fn assert_exact_circle(fitted: &Loop2, center: Vec2, radius: f64) {
|
||||
let [solidcraft_geom::Seg2::Arc { center: c, radius: r, sweep, .. }] = fitted.segs.as_slice() else { panic!("not one arc: {fitted:?}") };
|
||||
assert!(c.dist(center) < 1e-9 && (r - radius).abs() < 1e-9, "centre {c:?}, radius {r}");
|
||||
assert!((sweep.abs() - std::f64::consts::TAU).abs() < 1e-9, "sweep {sweep}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -550,8 +556,7 @@ mod tests {
|
||||
.collect();
|
||||
let inner = Loop2::polygon(&points);
|
||||
let fitted = fit_loop(&points, 0.05, 1e-6);
|
||||
// Fitted as a circle: one closed arc.
|
||||
assert_eq!(fitted.segs.len(), 1);
|
||||
assert_exact_circle(&fitted, Vec2::ZERO, 1.02);
|
||||
assert!(certify_nesting(&[outer.clone(), inner], &[outer, fitted], 1e-6, 3.).is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -2309,10 +2309,11 @@ fn tangent_holes_pinch_alike() {
|
||||
assert_eq!(m.merged.faces, 12, "{:?}", m.merged);
|
||||
}
|
||||
|
||||
/// A d10 tube bent at R5: the inside of the bend is a horn torus pinched to a point. One torus
|
||||
/// face, with a degenerate loop at the pinch (5 faces, 6 edges, 4 vertices).
|
||||
/// A d10 tube bent at R5: the inside of the bend is a horn torus pinched to a point, so the
|
||||
/// sweep just doesn't fold over itself. It is a valid solid of Pappus' volume, πr² times the
|
||||
/// path's length (20 + 5π/2 + 20).
|
||||
#[test]
|
||||
fn tight_bend_counts() {
|
||||
fn a_bend_as_tight_as_the_tube_sweeps_a_valid_solid() {
|
||||
use crate::build::PathSeg;
|
||||
let region = Region2 { outer: Loop2::circle(Vec2::ZERO, 5.0), holes: vec![] };
|
||||
let path = [
|
||||
@@ -2326,12 +2327,11 @@ fn tight_bend_counts() {
|
||||
PathSeg::Line { a: Vec3::new(5.0, 0.0, 25.0), b: Vec3::new(25.0, 0.0, 25.0) },
|
||||
];
|
||||
let b = sweep(&Plane::XY, ®ion, &path).unwrap();
|
||||
let m = measure(&b).unwrap().merged;
|
||||
// Two caps and a face per path segment. The circle starts (one closed arc) at (5, 0), which
|
||||
// runs up the first straight to (5, 0, 20) on the bend's axis: the turn leaves it there, so
|
||||
// the bend has no seam, and the second straight's seam runs on from it to (25, 0, 20). Edges:
|
||||
// the four circles and the two straights' seams; vertices: the seams' ends.
|
||||
assert_eq!((m.faces, m.edges, m.vertices), (5, 6, 3), "{m:?}");
|
||||
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
||||
let pi = std::f64::consts::PI;
|
||||
let want = pi * 25.0 * (40.0 + 2.5 * pi);
|
||||
let v = measure(&b).unwrap().volume;
|
||||
assert!(rel(v, want) < 1e-6, "{v} vs {want}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user