From 0bbf22ceaee3e39af568c2ae5e7d710cff3fca28 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 25 Jul 2026 03:51:47 +0200 Subject: [PATCH] =?UTF-8?q?M5a:=20hole=20standards=20library=20=E2=80=94?= =?UTF-8?q?=20counterbore/countersink=20+=20ISO/ANSI=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/libslic3r/CadDocument.cpp | 88 ++++++++++++++++++ src/libslic3r/CadDocument.hpp | 25 +++++- src/slic3r/GUI/McpControl.cpp | 78 ++++++++++++++++ tests/data/cad_recipe_v2.bin | Bin 28161 -> 29525 bytes tests/libslic3r/test_caddocument.cpp | 130 +++++++++++++++++++++++++++ 5 files changed, 319 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 135debe4dc..0838eb45eb 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -575,6 +576,72 @@ int CadDocument::add_hole(double diameter, double depth, bool through, return int(features.size()) - 1; } +// Hole standards lookup table (representative ISO 273 medium / ISO 4762 / ANSI unified). +struct HoleStdEntry { const char* desig; double clearance; double cbore_d; double cbore_depth; double csink_d; }; +static const HoleStdEntry kHoleStdTable[] = { + {"M3", 3.4, 6.0, 3.4, 6.3}, + {"M4", 4.5, 8.0, 4.4, 8.4}, + {"M5", 5.5, 10.0, 5.4, 10.4}, + {"M6", 6.6, 11.0, 6.8, 12.6}, + {"M8", 9.0, 15.0, 8.8, 17.3}, + {"M10", 11.0, 18.0, 11.0, 20.0}, + {"#6-32", 3.7, 8.8, 4.2, 8.7}, + {"#8-32", 4.4, 9.9, 5.1, 10.2}, + {"1/4-20", 6.9, 14.4, 7.2, 14.7}, + {"5/16-18", 8.8, 17.0, 8.2, 17.3}, + {"3/8-16", 10.5, 19.6, 9.5, 19.8}, +}; +static bool hole_std_lookup(const std::string& desig, double& clearance, + double& cbore_d, double& cbore_depth, double& csink_d) +{ + for (const auto& e : kHoleStdTable) { + if (e.desig == desig) { + clearance = e.clearance; + cbore_d = e.cbore_d; + cbore_depth = e.cbore_depth; + csink_d = e.csink_d; + return true; + } + } + return false; +} + +int CadDocument::add_hole_styled(double diameter, double depth, bool through, + double x, double y, const SketchPlane& plane, int style, + double cbore_diameter, double cbore_depth, + double csink_diameter, double csink_angle, + const std::string& standard, const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::Hole; + f.name = name; + f.plane = plane; + f.hole_diameter = diameter; + f.hole_depth = depth; + f.hole_through = through; + f.hole_x = x; + f.hole_y = y; + f.hole_style = style; + f.hole_cbore_diameter = cbore_diameter; + f.hole_cbore_depth = cbore_depth; + f.hole_csink_diameter = csink_diameter; + f.hole_csink_angle = csink_angle; + f.hole_standard = standard; + features.push_back(f); + return int(features.size()) - 1; +} + +int CadDocument::add_hole_standard(const std::string& designation, int style, bool through, + double depth, double x, double y, + const SketchPlane& plane, const std::string& name) +{ + double clearance, cbore_d, cbore_depth, csink_d; + if (!hole_std_lookup(designation, clearance, cbore_d, cbore_depth, csink_d)) + throw std::runtime_error("unknown hole standard \"" + designation + "\""); + return add_hole_styled(clearance, depth, through, x, y, plane, style, + cbore_d, cbore_depth, csink_d, 90, designation, name); +} + int CadDocument::add_thread(double radius, double pitch, double height, double depth, bool internal, double x, double y, const SketchPlane& plane, const std::string& name) @@ -1749,6 +1816,27 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, BRepAlgoAPI_Cut cut(result, tool); if (!cut.IsDone()) throw std::runtime_error("hole cut failed"); result = cut.Shape(); + + // Enlarge the entry for a screw head (style 1 = counterbore, 2 = countersink). + if (f.hole_style == 1 && f.hole_cbore_diameter > f.hole_diameter + && f.hole_cbore_depth > 1e-6) { + gp_Ax2 cbax(o, n); + TopoDS_Shape cb = BRepPrimAPI_MakeCylinder(cbax, f.hole_cbore_diameter * 0.5, + f.hole_cbore_depth).Shape(); + BRepAlgoAPI_Cut cbc(result, cb); + if (cbc.IsDone() && !cbc.Shape().IsNull()) result = cbc.Shape(); + } else if (f.hole_style == 2 && f.hole_csink_diameter > f.hole_diameter) { + const double Rmaj = f.hole_csink_diameter * 0.5; + const double Rmin = f.hole_diameter * 0.5; + const double half = f.hole_csink_angle * 0.5 * M_PI / 180.0; + const double h = (Rmaj - Rmin) / std::tan(half); + if (h > 1e-6) { + gp_Ax2 csax(o, n); + TopoDS_Shape cs = BRepPrimAPI_MakeCone(csax, Rmaj, Rmin, h).Shape(); + BRepAlgoAPI_Cut csc(result, cs); + if (csc.IsDone() && !csc.Shape().IsNull()) result = csc.Shape(); + } + } break; } case CadFeatureType::Thread: { diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index 0906fb0005..3c6668191b 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -118,6 +118,15 @@ struct CadFeature { double hole_x{0}; // position on the plane (plane u/x axis) double hole_y{0}; // position on the plane (plane v/y axis) + // Hole standards library (extends the plain bore above). + // hole_style: 0 = simple, 1 = counterbore, 2 = countersink. + int hole_style{0}; + double hole_cbore_diameter{0}; // counterbore cylinder diameter (mm), style==1 + double hole_cbore_depth{0}; // counterbore depth from the entry face (mm), style==1 + double hole_csink_diameter{0}; // countersink major diameter at entry face (mm), style==2 + double hole_csink_angle{90}; // countersink included angle (degrees), style==2 + std::string hole_standard; // provenance only, e.g. "M6" / "1/4-20"; not used by geometry + // Thread params (helical thread about the plane normal at a positioned point) double thread_radius{5}; // nominal cylinder radius double thread_pitch{2}; // axial advance per turn @@ -297,7 +306,9 @@ struct CadFeature { thicken_face, thicken_thickness, thicken_flip, cut_face_body, cut_face, project_source_body, project_edges, project_face, - delete_faces); + delete_faces, + hole_style, hole_cbore_diameter, hole_cbore_depth, + hole_csink_diameter, hole_csink_angle, hole_standard); } template void load(Archive& ar) { @@ -329,7 +340,9 @@ struct CadFeature { thicken_face, thicken_thickness, thicken_flip, cut_face_body, cut_face, project_source_body, project_edges, project_face, - delete_faces); + delete_faces, + hole_style, hole_cbore_diameter, hole_cbore_depth, + hole_csink_diameter, hole_csink_angle, hole_standard); imported_solid = brep_from_string(brep); } }; @@ -410,6 +423,14 @@ public: int add_hole(double diameter, double depth, bool through, double x, double y, const SketchPlane& plane, const std::string& name); + int add_hole_styled(double diameter, double depth, bool through, + double x, double y, const SketchPlane& plane, int style, + double cbore_diameter, double cbore_depth, + double csink_diameter, double csink_angle, + const std::string& standard, const std::string& name); + int add_hole_standard(const std::string& designation, int style, bool through, + double depth, double x, double y, + const SketchPlane& plane, const std::string& name); int add_thread(double radius, double pitch, double height, double depth, bool internal, double x, double y, const SketchPlane& plane, const std::string& name); diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 0ee4c250eb..466c0eea07 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -153,6 +153,31 @@ json describe_tools() json{{"name", "y"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, })}}, + json{{"name", "hole_styled"}, {"summary", "Drill a hole with optional counterbore (style=1) or countersink (style=2) at (x,y) on a plane."}, + {"params", json::array({ + json{{"name", "diameter"}, {"type", "number"}, {"unit", "mm"}, {"default", 5}, {"min", 0.01}}, + json{{"name", "depth"}, {"type", "number"}, {"unit", "mm"}, {"default", 10}, {"min", 0.01}}, + json{{"name", "through"}, {"type", "boolean"}, {"default", true}}, + json{{"name", "x"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "y"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, + json{{"name", "style"}, {"type", "integer"}, {"default", 0}, {"description", "0=simple, 1=counterbore, 2=countersink"}}, + json{{"name", "cbore_diameter"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "cbore_depth"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "csink_diameter"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "csink_angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 90}}, + json{{"name", "standard"}, {"type", "string"}, {"default", ""}, {"description", "provenance designation, e.g. M6"}}, + })}}, + json{{"name", "hole_standard"}, {"summary", "Drill a standard clearance hole (ISO 273 / ANSI) at (x,y) on a plane. style: 0=simple, 1=counterbore, 2=countersink."}, + {"params", json::array({ + json{{"name", "designation"}, {"type", "string"}, {"description", "e.g. M6, 1/4-20"}}, + json{{"name", "style"}, {"type", "integer"}, {"default", 0}}, + json{{"name", "through"}, {"type", "boolean"}, {"default", true}}, + json{{"name", "depth"}, {"type", "number"}, {"unit", "mm"}, {"default", 10}, {"min", 0.01}}, + json{{"name", "x"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "y"}, {"type", "number"}, {"unit", "mm"}, {"default", 0}}, + json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, + })}}, json{{"name", "boolean"}, {"summary", "Combine two bodies: union | subtract (tool from target) | intersect."}, {"params", json::array({ json{{"name", "op"}, {"type", "string"}, {"enum", json::array({"union", "subtract", "intersect"})}, {"default", "subtract"}}, @@ -811,6 +836,57 @@ json action_hole(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"hole_index", h}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_hole_styled(DesignPanel* panel, const json& params) +{ + const double dia = params.value("diameter", 5.0); + const double depth = params.value("depth", 10.0); + const bool thru = params.value("through", true); + const double x = params.value("x", 0.0), y = params.value("y", 0.0); + const int style = params.value("style", 0); + const double cbore_diameter = params.value("cbore_diameter", 0.0); + const double cbore_depth = params.value("cbore_depth", 0.0); + const double csink_diameter = params.value("csink_diameter", 0.0); + const double csink_angle = params.value("csink_angle", 90.0); + const std::string standard = params.value("standard", std::string("")); + if (dia <= 0) throw std::runtime_error("diameter must be > 0"); + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to drill"); + SketchPlane pl = plane_from(params, doc); + doc.checkpoint(); + int h = doc.add_hole_styled(dia, depth, thru, x, y, pl, style, + cbore_diameter, cbore_depth, + csink_diameter, csink_angle, standard, "Hole"); + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"hole_index", h}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; +} + +json action_hole_standard(DesignPanel* panel, const json& params) +{ + const std::string desig = params.value("designation", std::string("")); + if (desig.empty()) throw std::runtime_error("designation is required"); + const int style = params.value("style", 0); + const bool thru = params.value("through", true); + const double depth = params.value("depth", 10.0); + const double x = params.value("x", 0.0), y = params.value("y", 0.0); + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to drill"); + SketchPlane pl = plane_from(params, doc); + doc.checkpoint(); + try { + int h = doc.add_hole_standard(desig, style, thru, depth, x, y, pl, "Hole"); + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"hole_index", h}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; + } catch (const std::exception& ex) { + doc.undo(); + panel->mcp_after_change(); + return json{{"ok", false}, {"error", ex.what()}}; + } +} + json action_boolean(DesignPanel* panel, const json& params) { BooleanMode m = bool_from(params.value("op", std::string("subtract"))); @@ -1101,6 +1177,8 @@ std::string handle_on_main(const std::string& method, const json& params, const if (method == "fillet") return rpc_result(id, action_fillet(panel, params)); if (method == "chamfer") return rpc_result(id, action_chamfer(panel, params)); if (method == "hole") return rpc_result(id, action_hole(panel, params)); + if (method == "hole_styled") return rpc_result(id, action_hole_styled(panel, params)); + if (method == "hole_standard") return rpc_result(id, action_hole_standard(panel, params)); if (method == "boolean") return rpc_result(id, action_boolean(panel, params)); if (method == "pattern") return rpc_result(id, action_pattern(panel, params)); if (method == "shell") return rpc_result(id, action_shell(panel, params)); diff --git a/tests/data/cad_recipe_v2.bin b/tests/data/cad_recipe_v2.bin index c55160338b25ad7bbd5a0a3bf0f5be2d120adf25..331f6fcd71b697c8966aebc6873287a51cf43d75 100644 GIT binary patch delta 517 zcmZp?!+7->F{t|{UvQJ&EU9)B#msA(jmT;?d+R<&=1=z4mD}uM zh^mLtv<^j$mW}-8Nft|x#Wri%>_@TZp?xz7KgJmtb_lm^zTox%MSg`Bs^4Y&s!-(n z0xMAX|3Xm1rZ56k{o`oVxQdPk`T;5KE+(Pc=b8o#I{{`{aK!G;bYp=k<_1PI0~7!N DscNkI delta 171 zcmccmjIr?!TrOE4cew&k z)NcaJbeG^#3ytym1fO)f8B+!DRF+gr|0#FHiN*VL!mFYkw Op4nB*o8RYIu>b%w5JE%% diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index ee9b224f6e..e60a61ff9e 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -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> 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); +}