Engine tests: SMLib's faces, not truck's pieces, in the face counts and measures

SMLib keeps a whole cylinder (and an arc of one past half a turn) as one face closed on its
seam, where truck split it in two; its holes are exact circles; its mass properties come
within 2e-7 of a closed form. The tests that counted truck's pieces or its polygon hole now
check the exact geometry:
- extrude_of_adjacent_profiles_has_whole_edges: the cut-off cylinder is 4 faces (arc side
  4674 = 40·(2π − 1.2019)·23, chord side 1039.8, two ends);
- step_export_of_an_enclosure_with_a_screw_hole: 27 faces, as the file holds them;
- model_faces_gives_a_point...: the hole's wall is one cylinder, its centroid on the axis;
- draft_takes_a_cylinders_side_by_name: one side face (the draft itself waits for SMLib's
  draft of a cylinder);
- convert_to_sheet_metal_takes_the_plates_face_by_name: 15000 − 62.5π to the measure's 2e-7;
- a_checkpoint_survives_file_new: a unit box measures 1 + 2e-16;
- creating_a_component_recomputes_nothing: SMLib's boolean keeps the face certificate the old
  setup relied on losing, so the legacy face plane is made as a design saved before face
  attachments were certified (its anchor removed) and reopened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:11:05 -04:00
committed by Irakli Kourtis
parent ab68a3d0fb
commit 24bcaca039
3 changed files with 34 additions and 39 deletions
@@ -176,8 +176,8 @@ fn step_export_of_an_enclosure_with_a_screw_hole_finishes_and_reads_back() {
let s = replay(include_str!("../../../examples/regressions/step_export_screw_hole_enclosure.json"));
let state = s.model.state();
let body = &state.body("Unterteil").unwrap().body;
// 27 faces; the kernel splits each of the three whole turns (the hole, two standoffs) in two.
assert_eq!(body.face_count(), 30);
// 27 faces, as the file holds them: each whole turn (the hole, two standoffs) is one face.
assert_eq!(body.face_count(), 27);
let (text, file_faces) = read_back(&s, body);
assert_eq!(text.matches("CYLINDRICAL_SURFACE(").count(), 7, "the seven round faces stay cylinders");
assert_eq!(file_faces, 27);
+11 -10
View File
@@ -43,8 +43,8 @@ fn top_and_hole_names(s: &mut Session) -> (String, Vec<String>) {
}
/// A 60 x 40 x 10 plate with a hole of radius 5: its top face is a rectangle with a hole in it,
/// whose centroid (the plate's middle, (30, 20, 10)) is in the hole; the hole's wall is two half
/// cylinders, whose centroids are inside the cylinder, 2r/π from its axis.
/// whose centroid (the plate's middle, (30, 20, 10)) is in the hole; the hole's wall is one
/// whole cylinder (SMLib's, closed on its seam), whose centroid is on its axis, halfway up.
#[test]
fn model_faces_gives_a_point_on_each_face_and_the_hole_axis_and_radius() {
let mut s = Session::default();
@@ -61,10 +61,10 @@ fn model_faces_gives_a_point_on_each_face_and_the_hole_axis_and_radius() {
Some(c) => {
let radial = (at.x - 30.0).hypot(at.y - 20.0);
assert!((radial - 5.0).abs() < 1e-9 && at.z > 0.0 && at.z < 10.0, "{at:?}");
// The centroid of a half cylinder's wall is 2r/π = 3.18 from the axis, not on the wall.
// The centroid of a whole cylinder's wall is on its axis, not on the wall.
let centroid = vec(&f["centroid"]);
let inside = (centroid.x - 30.0).hypot(centroid.y - 20.0);
assert!((2.5..4.0).contains(&inside), "the centroid of a half cylinder is inside it: {inside}");
assert!(inside < 1e-9 && (centroid.z - 5.0).abs() < 1e-9, "the centroid of the hole's wall is on its axis: {centroid:?}");
// The hole's axis: the line through (30, 20) along z, its radius 5.
let (origin, axis) = (vec(&c["origin"]), vec(&c["axis"]));
assert!((origin.x - 30.0).abs() < 1e-9 && (origin.y - 20.0).abs() < 1e-9, "{origin:?}");
@@ -122,10 +122,11 @@ fn convert_to_sheet_metal_takes_the_plates_face_by_name() {
let span = at.iter().cloned().fold(f64::MIN, f64::max) - at.iter().cloned().fold(f64::MAX, f64::min);
assert!(span > 9.8 && span < 10.0 + 1e-6, "{span}");
}
// The plate is still 100 x 60 x 2.5 less the hole: 15000 - π·5²·2.5 = 14803.65 mm³ for a round hole, a little more
// (less than 2% of the hole's 196.35 mm³) for a polygon inside the circle.
// The plate is still 100 x 60 x 2.5 less the hole, which is round: 15000 - π·5²·2.5 =
// 14803.65 mm³, within the measure's accuracy (SMLib's mass properties come within 2e-7 of
// a closed form).
let (v, round) = (volume(&mut s, "Plate"), 15000.0 - 25.0 * PI * 2.5);
assert!(v >= round - 1e-6 && v - round < 0.02 * 25.0 * PI * 2.5, "{v}");
assert!((v - round).abs() < 2e-7 * round, "{v} vs {round}");
}
/// Shell, draft and offset face share the parser of Delete Face.
@@ -207,8 +208,8 @@ fn offset_face_takes_the_face_by_name() {
assert!((volume(&mut s, "Tray") - 100.0 * 60.0 * 35.0).abs() < 1e-6, "{}", volume(&mut s, "Tray"));
}
/// A cylinder of radius 10 and height 20 has a side of two half cylinders, each of which
/// `model.faces` describes (its axis and radius); drafting them 5 degrees from the base plane makes
/// A cylinder of radius 10 and height 20 has one side face (SMLib's, closed on its seam), which
/// `model.faces` describes (its axis and radius); drafting it 5 degrees from the base plane makes
/// a frustum of radius 10 at the base and 10 - 20 tan 5 degrees at the top.
#[test]
fn draft_takes_a_cylinders_side_by_name() {
@@ -216,7 +217,7 @@ fn draft_takes_a_cylinders_side_by_name() {
run(&mut s, "solid.cylinder", json!({"radius": 10, "height": 20, "body_name": "Pin"}));
let all = faces(&mut s, "Pin");
let sides: Vec<&Value> = all.iter().filter(|f| f.get("cylinder").is_some()).collect();
assert_eq!(sides.len(), 2, "two half cylinders: {all:?}");
assert_eq!(sides.len(), 1, "one side: {all:?}");
for side in &sides {
assert!((side["cylinder"]["radius"].as_f64().unwrap_or(f64::NAN) - 10.0).abs() < 1e-9, "{side}");
}
+21 -27
View File
@@ -627,7 +627,9 @@ fn extrude_of_adjacent_profiles_has_whole_edges() {
assert_eq!(below.len(), 2, "{si}");
run(&mut s, "solid.extrude", json!({"sketch": "Holder", "profiles": below, "distance": 23, "body_name": "Holder"}));
let topo = run(&mut s, "model.faces", json!({"body": "Holder"}));
assert_eq!(topo["faces"].as_array().unwrap().len(), 5, "a cut-off cylinder: {topo}");
// The arc side (one face: SMLib keeps a cylinder's arc of more than half a turn whole), the
// chord's flat side and the two ends.
assert_eq!(topo["faces"].as_array().unwrap().len(), 4, "a cut-off cylinder: {topo}");
let lo = run(&mut s, "inspect.measure", json!({"bodies": ["Holder"]}))["bodies"][0]["bbox"]["min"][1].as_f64().unwrap();
run(&mut s, "solid.move", json!({"bodies": ["Holder"], "translate": [0, -5.0 - lo, 0]}));
// The handle: r35 on the Y axis with a bevelled end.
@@ -1920,13 +1922,15 @@ fn a_checkpoint_survives_file_new() {
run(&mut s, "solid.box", json!({"length": 1.0, "width": 1.0, "height": 1.0}));
let id = run(&mut s, "edit.checkpoint", json!({}))["checkpoint"].as_u64().unwrap();
run(&mut s, "file.new", json!({"name": "Replacement"}));
// The volume as SMLib integrates it, to rounding (a unit box measures 1 + 2e-16).
let volume = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN);
run(&mut s, "solid.box", json!({"length": 2.0, "width": 2.0, "height": 2.0}));
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(8.0));
assert!((volume(&mut s) - 8.0).abs() < 1e-12);
run(&mut s, "edit.restore_checkpoint", json!({"checkpoint": id}));
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(1.0));
assert!((volume(&mut s) - 1.0).abs() < 1e-12);
// The undo history is the restored design's own: one step back is the empty design.
run(&mut s, "edit.undo", json!({}));
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(0.0));
assert!(volume(&mut s).abs() < 1e-12);
}
#[test]
@@ -4432,31 +4436,21 @@ fn creating_a_component_recomputes_nothing() {
run(&mut s, "sketch.circle.center", json!({"center": [25, 25], "radius": 2}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Post sketch", "to": [25, 25, 10], "operation": "new", "body_name": "Post"}));
// A plate whose top face loses its certificate: a patterned cut, a pad joined flush with
// the top, then one more cut. Extruding that face makes a sketch on a legacy face plane.
// A sketch on a face without a construction certificate: a face extrude on a plate, saved
// as a design from before face attachments were certified (no anchor), and reopened.
run(&mut s, "solid.box", json!({"length": 100, "width": 100, "height": 10, "corner": [200, 0, 0], "operation": "new", "body_name": "Plate"}));
run(
&mut s,
"solid.cylinder",
json!({"diameter": 4, "height": 12, "base": [210, 10, -1], "operation": "cut", "targets": ["Plate"], "name": "Pilot"}),
);
run(
&mut s,
"solid.pattern.rectangular",
json!({"features": ["Pilot"], "dir1": [1, 0, 0], "count1": 4, "spacing1": 20, "dir2": [0, 1, 0], "count2": 4, "spacing2": 20}),
);
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "5 mm", "name": "Pad sketch"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [260, 60], "p1": [295, 95]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "Pad sketch", "distance": 5, "operation": "join", "targets": ["Plate"]}));
run(&mut s, "solid.box", json!({"length": 5, "width": 100, "height": 12, "corner": [199, 0, -1], "operation": "cut", "targets": ["Plate"]}));
run(&mut s, "solid.extrude", json!({"face": [240, 40, 10], "distance": 1, "operation": "join", "name": "Face layer"}));
let legacy_face = s
.doc
.features
.iter()
.any(|f| matches!(&f.kind, solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor: None, .. }, .. }));
assert!(legacy_face, "the face extrude's sketch is on a legacy face plane");
let mut legacy = (*s.doc).clone();
let mut legacy_face = false;
for f in &mut legacy.features {
if let solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, .. }, .. } = &mut f.kind {
*anchor = None;
legacy_face = true;
}
}
assert!(legacy_face, "the face extrude's sketch is on a face plane");
let mut s = Session::new(solidcraft_doc::Document::from_json(&legacy.to_json()).unwrap());
assert!(s.model.results.iter().all(|r| r.error.is_none()), "the legacy design evaluates");
for name in ["First", "Second"] {
run(&mut s, "component.create", json!({"name": name}));
assert_eq!(s.model.last_recomputed, 0, "creating {name} recomputed a feature");