M5a: hole standards library — counterbore/countersink + ISO/ANSI table

Extend CadFeatureType::Hole (no new enum value) with a style flag
(simple/counterbore/countersink) and the matching geometry: a coaxial
shallow cylinder cut for counterbores, a cone-frustum cut for countersinks.
A file-local hole_std_lookup() resolves screw designations (ISO 273/4762/
10642 metric M3–M10 + common ANSI unified) into clearance/cbore/csink dims;
add_hole_standard() fills the feature from it, add_hole_styled() takes them
explicitly. Six serialized fields appended to both symmetric cereal lists
(recipe version stays 2, golden fixture regenerated 28161->29525). MCP:
hole_styled, hole_standard. 4 new [CadDocument][hole] tests; suite 83/1351.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-25 03:52:01 +02:00
co-authored by Claude Opus 4.8
parent e19e51b150
commit 0bbf22ceae
5 changed files with 319 additions and 2 deletions
+130
View File
@@ -4048,3 +4048,133 @@ TEST_CASE("delete_face round-trip serialization", "[CadDocument][deleteface]")
REQUIRE_THAT(double(z1), WithinAbs(bboxes[i].second.z(), 1e-6));
}
}
TEST_CASE("hole: counterbore removes more material than a simple bore", "[CadDocument][hole]")
{
using Catch::Matchers::WithinAbs;
auto make_box_hole = [](int style, double cbore_d, double cbore_depth,
double csink_d, double csink_angle, const std::string& desig) {
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 30, 30, 0, "Box");
doc.add_extrude(sk, 15.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
double v_box = doc.body_mass_properties(0).volume;
if (style == 0)
doc.add_hole(6.0, 10.0, false, 0.0, 0.0, SketchPlane::XY(), "Hole");
else
doc.add_hole_styled(6.0, 10.0, false, 0.0, 0.0, SketchPlane::XY(), style,
cbore_d, cbore_depth, csink_d, csink_angle, desig, "Hole");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
double v = doc.body_mass_properties(0).volume;
REQUIRE(v < v_box);
return v;
};
double v_simple = make_box_hole(0, 0, 0, 0, 0, "");
double v_cbore = make_box_hole(1, 11.0, 6.0, 0, 0, "M6");
REQUIRE(v_cbore < v_simple);
}
TEST_CASE("hole: countersink removes more material than a simple bore", "[CadDocument][hole]")
{
using Catch::Matchers::WithinAbs;
auto make_box = []() {
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 30, 30, 0, "Box");
doc.add_extrude(sk, 15.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
return doc;
};
CadDocument doc_simple = make_box();
doc_simple.add_hole(6.0, 10.0, false, 0.0, 0.0, SketchPlane::XY(), "Hole");
REQUIRE(doc_simple.recompute());
double v_simple = doc_simple.body_mass_properties(0).volume;
double v_box = 30.0 * 30.0 * 15.0;
CadDocument doc_csink = make_box();
doc_csink.add_hole_styled(6.0, 10.0, false, 0.0, 0.0, SketchPlane::XY(), 2,
0.0, 0.0, 12.0, 90.0, "M6", "Hole");
REQUIRE(doc_csink.recompute());
double v_csink = doc_csink.body_mass_properties(0).volume;
REQUIRE(v_simple < v_box);
REQUIRE(v_csink < v_simple);
}
TEST_CASE("hole: standards table lookup", "[CadDocument][hole]")
{
using Catch::Matchers::WithinAbs;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 30, 30, 0, "Box");
doc.add_extrude(sk, 15.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
int fi = doc.add_hole_standard("M6", 0, true, 10, 0, 0, SketchPlane::XY(), "H");
REQUIRE(fi >= 0);
REQUIRE_THAT(doc.features[fi].hole_diameter, WithinAbs(6.6, 1e-6));
REQUIRE(doc.features[fi].hole_standard == "M6");
REQUIRE_THROWS(doc.add_hole_standard("M999", 0, true, 10, 0, 0, SketchPlane::XY(), "H"));
try {
doc.add_hole_standard("M999", 0, true, 10, 0, 0, SketchPlane::XY(), "H");
} catch (const std::exception& ex) {
CHECK_THAT(std::string(ex.what()), Catch::Matchers::Contains("standard"));
}
}
TEST_CASE("hole: round-trip preserves styled counterbore hole", "[CadDocument][hole]")
{
using Catch::Matchers::WithinAbs;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 30, 30, 0, "Box");
doc.add_extrude(sk, 15.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
doc.add_hole_styled(6.0, 10.0, false, 0.0, 0.0, SketchPlane::XY(), 1,
11.0, 6.0, 0.0, 90.0, "M6", "Cbore");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
std::vector<std::pair<Vec3d, Vec3d>> bboxes;
for (const auto& b : doc.bodies) {
Bnd_Box bb; BRepBndLib::Add(b.shape, bb);
Standard_Real x0, y0, z0, x1, y1, z1;
bb.Get(x0, y0, z0, x1, y1, z1);
bboxes.emplace_back(Vec3d(x0, y0, z0), Vec3d(x1, y1, z1));
}
auto blob = doc.serialize_recipe();
REQUIRE_FALSE(blob.empty());
CadDocument doc2;
REQUIRE(doc2.deserialize_recipe(blob));
REQUIRE(doc2.error.empty());
REQUIRE(doc2.bodies.size() == doc.bodies.size());
for (size_t i = 0; i < bboxes.size(); ++i) {
Bnd_Box bb; BRepBndLib::Add(doc2.bodies[i].shape, bb);
Standard_Real x0, y0, z0, x1, y1, z1;
bb.Get(x0, y0, z0, x1, y1, z1);
REQUIRE_THAT(double(x0), WithinAbs(bboxes[i].first.x(), 1e-6));
REQUIRE_THAT(double(y0), WithinAbs(bboxes[i].first.y(), 1e-6));
REQUIRE_THAT(double(z0), WithinAbs(bboxes[i].first.z(), 1e-6));
REQUIRE_THAT(double(x1), WithinAbs(bboxes[i].second.x(), 1e-6));
REQUIRE_THAT(double(y1), WithinAbs(bboxes[i].second.y(), 1e-6));
REQUIRE_THAT(double(z1), WithinAbs(bboxes[i].second.z(), 1e-6));
}
bool found = false;
for (const auto& f : doc2.features) {
if (f.type == CadFeatureType::Hole && f.name == "Cbore") {
REQUIRE(f.hole_style == 1);
REQUIRE_THAT(f.hole_cbore_diameter, WithinAbs(11.0, 1e-9));
found = true;
}
}
REQUIRE(found);
}