M4: delete-face direct edit — remove faces and heal via OCCT defeaturing

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-25 03:28:25 +02:00
co-authored by Claude Opus 4.8
parent 65caa2ea6e
commit e19e51b150
5 changed files with 179 additions and 10 deletions
+29
View File
@@ -13,6 +13,7 @@
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Defeaturing.hxx>
#include <BRepAlgoAPI_BooleanOperation.hxx>
#include <BRepOffsetAPI_MakePipe.hxx>
#include <BRepOffsetAPI_MakePipeShell.hxx>
@@ -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<int>& 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;
}
}
}
+11 -3
View File
@@ -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<int> 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<int> delete_faces;
template<class Archive>
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<class Archive>
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<int>& 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,
+30 -7
View File
@@ -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<int> faces;
if (params["faces"].is_array())
for (const auto& v : params["faces"]) faces.push_back(v.get<int>());
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));
Binary file not shown.
+109
View File
@@ -3939,3 +3939,112 @@ TEST_CASE("bridge round-trip serialization", "[CadDocument][bridge]")
REQUIRE_THAT(br.ctrl.back().x(), WithinAbs(20.0, 1e-9));
REQUIRE_THAT(br.ctrl.back().y(), WithinAbs(0.0, 1e-9));
}
TEST_CASE("delete_face removes a fillet face and restores volume", "[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");
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<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.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));
}
}