From e19e51b15026f78ab0ecdcbeeb155230c4ae2b6b Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 25 Jul 2026 03:28:13 +0200 Subject: [PATCH] =?UTF-8?q?M4:=20delete-face=20direct=20edit=20=E2=80=94?= =?UTF-8?q?=20remove=20faces=20and=20heal=20via=20OCCT=20defeaturing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New CadFeatureType::DeleteFace: removes a set of global face ids from target_body and heals the gap via BRepAlgoAPI_Defeaturing (TKBO, already linked), mirroring the Shell/Draft body-modifying pattern. delete_faces appended to both symmetric cereal lists (recipe version stays 2, golden fixture regenerated 27913->28161). MCP delete_face method (pure additions). 3 new [CadDocument][deleteface] tests: remove a fillet face restores the sharp-box volume, bad index fails safely, round-trip. Full kernel suite green (79 cases, 1309 asserts). Move-face / replace-face deferred to snaporca-3c4 / snaporca-tc6 (no clean shipping OCCT direct-modeling primitive; need research, and replace-face depends on M7 surfaces). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/libslic3r/CadDocument.cpp | 29 +++++++ src/libslic3r/CadDocument.hpp | 14 +++- src/slic3r/GUI/McpControl.cpp | 37 +++++++-- tests/data/cad_recipe_v2.bin | Bin 27913 -> 28161 bytes tests/libslic3r/test_caddocument.cpp | 109 +++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 65cecc6159..135debe4dc 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -680,6 +681,18 @@ int CadDocument::add_shell(double thickness, int face, int target_body, const st return int(features.size()) - 1; } +int CadDocument::add_delete_face(int target_body, const std::vector& faces, + const std::string& name) +{ + CadFeature f; + f.type = CadFeatureType::DeleteFace; + f.name = name; + f.target_body = target_body; + f.delete_faces = faces; + features.push_back(f); + return int(features.size()) - 1; +} + int CadDocument::add_draft(double angle, int face, int target_body, const std::string& name) { CadFeature f; @@ -1873,6 +1886,22 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, if (result.IsNull()) throw std::runtime_error("draft produced no geometry"); break; } + case CadFeatureType::DeleteFace: { + if (!have_body) throw std::runtime_error("delete_face needs a body"); + if (f.delete_faces.empty()) throw std::runtime_error("delete_face needs at least one face"); + BRepAlgoAPI_Defeaturing df; + df.SetShape(result); + for (int fi : f.delete_faces) { + TopoDS_Face fc = GeometryEngine::face_by_index(result, fi); + if (fc.IsNull()) throw std::runtime_error("delete_face: face not found"); + df.AddFaceToRemove(fc); + } + df.Build(); + if (!df.IsDone()) throw std::runtime_error("delete_face failed"); + result = df.Shape(); + if (result.IsNull()) throw std::runtime_error("delete_face produced no geometry"); + break; + } } } diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index 07b3550eb1..0906fb0005 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -17,7 +17,7 @@ namespace Slic3r { -enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project }; +enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace }; enum class SketchShape { Rectangle, Circle }; enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident }; enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge }; @@ -263,6 +263,10 @@ struct CadFeature { std::vector project_edges; // global edge ids to project; empty => use project_face int project_face{-1}; // if project_edges empty, project every edge of this face + // Direct edit: faces to remove (global face indices into target_body's shape), + // healed via BRepAlgoAPI_Defeaturing. + std::vector delete_faces; + template void save(Archive& ar) const { std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string(); @@ -292,7 +296,8 @@ struct CadFeature { xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy, thicken_face, thicken_thickness, thicken_flip, cut_face_body, cut_face, - project_source_body, project_edges, project_face); + project_source_body, project_edges, project_face, + delete_faces); } template void load(Archive& ar) { @@ -323,7 +328,8 @@ struct CadFeature { xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy, thicken_face, thicken_thickness, thicken_flip, cut_face_body, cut_face, - project_source_body, project_edges, project_face); + project_source_body, project_edges, project_face, + delete_faces); imported_solid = brep_from_string(brep); } }; @@ -446,6 +452,8 @@ public: // Offset face `face` of `target_body` by `thickness` along its normal, producing a new // thin solid appended as a new body. flip=true offsets against the normal. int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name); + int add_delete_face(int target_body, const std::vector& faces, + const std::string& name); // Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset // along its normal, optional tilt about a base axis. Produces no solid. int add_plane(int base, double offset, double angle_tilt, int axis, diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index dd22810b61..0ee4c250eb 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -249,13 +249,18 @@ json describe_tools() json{{"name", "keep_upper"},{"type", "boolean"}, {"default", true}}, json{{"name", "keep_lower"},{"type", "boolean"}, {"default", true}}, })}}, - json{{"name", "project"}, {"summary", "Project edges of a solid onto a sketch plane, producing a new sketch feature."}, - {"params", json::array({ - json{{"name", "source_body"}, {"type", "integer"}, {"default", -1}, {"description", "body owning the edges; -1 = last body"}}, - json{{"name", "face"}, {"type", "integer"}, {"default", -1}, {"description", "global face id on the source body; when set, all its edges are projected"}}, - json{{"name", "edges"}, {"type", "array"}, {"default", json::array()}, {"description", "global edge ids to project; empty => project the face"}}, - json{{"name", "plane"}, {"type", "string"}, {"default", "XY"}, {"description", "target sketch plane (XY/XZ/YZ)"}}, - })}}, + json{{"name", "project"}, {"summary", "Project edges of a solid onto a sketch plane, producing a new sketch feature."}, + {"params", json::array({ + json{{"name", "source_body"}, {"type", "integer"}, {"default", -1}, {"description", "body owning the edges; -1 = last body"}}, + json{{"name", "face"}, {"type", "integer"}, {"default", -1}, {"description", "global face id on the source body; when set, all its edges are projected"}}, + json{{"name", "edges"}, {"type", "array"}, {"default", json::array()}, {"description", "global edge ids to project; empty => project the face"}}, + json{{"name", "plane"}, {"type", "string"}, {"default", "XY"}, {"description", "target sketch plane (XY/XZ/YZ)"}}, + })}}, + json{{"name", "delete_face"}, {"summary", "Remove faces from a solid, healing the gap via OCCT defeaturing."}, + {"params", json::array({ + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body"}}, + json{{"name", "faces"}, {"type", "array"}, {"description", "global face ids to delete"}}, + })}}, json{{"name", "bridge"}, {"summary", "Build a cubic-Bezier G1 bridge (BSpline) between two sketch-entity endpoints within a sketch feature."}, {"params", json::array({ json{{"name", "sketch"}, {"type", "integer"}, {"description", "sketch feature index"}}, @@ -966,6 +971,23 @@ json action_project(DesignPanel* panel, const json& params) return json{{"ok", ok}, {"project_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; } +json action_delete_face(DesignPanel* panel, const json& params) +{ + if (!params.contains("faces")) throw std::runtime_error("delete_face needs 'faces' (array of face ids)"); + CadDocument& doc = panel->mcp_doc(); + if (doc.bodies.empty()) throw std::runtime_error("no body to delete faces from"); + int bi = target_body_arg(params, doc); + std::vector faces; + if (params["faces"].is_array()) + for (const auto& v : params["faces"]) faces.push_back(v.get()); + doc.checkpoint(); + int idx = doc.add_delete_face(bi, faces, "DeleteFace"); + bool ok = doc.recompute(); + if (!ok) doc.undo(); + panel->mcp_after_change(); + return json{{"ok", ok}, {"feature_index", idx}, {"bodies", int(doc.bodies.size())}, {"error", doc.error}}; +} + json action_bridge(DesignPanel* panel, const json& params) { if (!params.contains("sketch")) throw std::runtime_error("bridge needs 'sketch' (feature index)"); @@ -1088,6 +1110,7 @@ std::string handle_on_main(const std::string& method, const json& params, const if (method == "thicken") return rpc_result(id, action_thicken(panel, params)); if (method == "split") return rpc_result(id, action_split(panel, params)); if (method == "project") return rpc_result(id, action_project(panel, params)); + if (method == "delete_face") return rpc_result(id, action_delete_face(panel, params)); if (method == "bridge") return rpc_result(id, action_bridge(panel, params)); if (method == "axis") return rpc_result(id, action_axis(panel, params)); if (method == "coordsys") return rpc_result(id, action_coordsys(panel, params)); diff --git a/tests/data/cad_recipe_v2.bin b/tests/data/cad_recipe_v2.bin index 93ab5e116e8b22a0725f3c9f91012231e9d22315..c55160338b25ad7bbd5a0a3bf0f5be2d120adf25 100644 GIT binary patch delta 280 zcmeCY#n^aG*=9|FuYU_0n ziAeim2;+=X0a(V;4Qd{T7t|3YzF80@_X08?47U)dvo?l9rKF<^AX43Nc@W0WM5y-E S)FS4I4jh}WXBe@7f&u_J|6geU delta 163 zcmZp?!`OL?aYF_3<_6|m=FMN(fJ7D-khssA&Ad5CFq3)nVUZl>&E^t7?kZ^@S6Ut@ zJ3*<4c{7*V8RpIRGz*z08yO01&eT1~yxGX`Jo9Ew(>&(Q^%m=yH=Eh4X5ReLzLjt0n8Cc6Efi>QWdxA>IvS`XIUcC@W)e_|ZyL~w= 0); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + double Vbox = double(doc.display_mesh.volume()); + REQUIRE(Vbox > 0.0); + + doc.add_fillet(2.0, FaceGroup::All, "Fillet"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + double Vf = double(doc.display_mesh.volume()); + REQUIRE(Vf < Vbox); + + int nfaces = GeometryEngine::face_count(doc.bodies[0].shape); + doc.checkpoint(); + bool found = false; + for (int fi = 0; fi < nfaces && !found; ++fi) { + int idx = doc.add_delete_face(0, {fi}, "Unfillet"); + (void)idx; + if (doc.recompute() && doc.error.empty()) { + double Vr = double(doc.display_mesh.volume()); + if (Vr > Vf) { + found = true; + } + } + if (!found) doc.undo(); + } + REQUIRE(found); +} + +TEST_CASE("delete_face with bad face index fails safely", "[CadDocument][deleteface]") +{ + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box"); + REQUIRE(sk >= 0); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + doc.add_delete_face(0, {9999}, "Bad"); + REQUIRE_FALSE(doc.recompute()); + REQUIRE_THAT(doc.error, Catch::Matchers::Contains("face")); +} + +TEST_CASE("delete_face round-trip serialization", "[CadDocument][deleteface]") +{ + using Catch::Matchers::WithinRel; + using Catch::Matchers::WithinAbs; + + CadDocument doc; + int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box"); + REQUIRE(sk >= 0); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude"); + doc.add_fillet(2.0, FaceGroup::All, "Fillet"); + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + + int nfaces = GeometryEngine::face_count(doc.bodies[0].shape); + int fillet_face = -1; + for (int fi = 0; fi < nfaces; ++fi) { + doc.checkpoint(); + doc.add_delete_face(0, {fi}, "Unfillet"); + bool ok = doc.recompute(); + if (ok && doc.error.empty()) { + fillet_face = fi; + break; + } + doc.undo(); + } + REQUIRE(fillet_face >= 0); + + 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.push_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.recompute()); + 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)); + } +}