CAD: split a body by a picked face; add the missing both-halves cut test

Split-by-face reuses the existing Cut feature rather than adding a new type:
two appended fields (cut_face_body, cut_face) let apply_cut derive the cut
plane from a picked face via SketchPlane::from_face when cut_face >= 0,
otherwise it keeps using the base `plane`. cut_offset / cut_flip still apply
along the derived normal, so the same square-wire split machinery handles
both cases. add_split_by_face() is the convenience entry point; MCP gains a
`split` method (body / face_body / face / keep_upper / keep_lower).

Serialization stays append-only — cut_face_body, cut_face appended to
save/load, recipe version unchanged at 2.

Tests: the previously-missing both-halves plane cut (keep_upper && keep_lower
=> two bodies whose volumes sum to the original), split-by-face via a
top-face plane offset into the interior, keep-upper-only, and a round-trip.
Golden fixture regenerated with a GoldenSplit cut-by-face feature and exact
field-value assertions. Suite 63 -> 67 cases, 1119 -> 1178 assertions.

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-24 21:46:24 +02:00
co-authored by Claude Opus 4.8
parent 9da5851534
commit 07b39d45cb
5 changed files with 232 additions and 5 deletions
+25 -1
View File
@@ -724,6 +724,21 @@ int CadDocument::add_cut(const SketchPlane& plane, double offset, bool flip,
return int(features.size()) - 1;
}
int CadDocument::add_split_by_face(int target_body, int face_body, int face,
bool keep_upper, bool keep_lower, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Cut;
f.name = name;
f.target_body = target_body;
f.cut_face_body = face_body;
f.cut_face = face;
f.cut_keep_upper = keep_upper;
f.cut_keep_lower = keep_lower;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_mirror(const SketchPlane& plane, int target_body, BooleanMode mode,
const std::string& name)
{
@@ -1905,7 +1920,16 @@ void CadDocument::apply_cut(std::vector<CadBody>& bodies, const CadFeature& f) c
if (!f.cut_keep_upper && !f.cut_keep_lower)
throw std::runtime_error("cut keeps nothing");
SketchPlane cp = f.plane;
SketchPlane cp;
if (f.cut_face >= 0) {
const int fb = (f.cut_face_body >= 0 && f.cut_face_body < nb) ? f.cut_face_body : tgt;
if (bodies[fb].shape.IsNull()) throw std::runtime_error("cut: face body is empty");
TopoDS_Face fc = GeometryEngine::face_by_index(bodies[fb].shape, f.cut_face);
if (fc.IsNull()) throw std::runtime_error("cut: face not found");
cp = SketchPlane::from_face(fc);
} else {
cp = f.plane;
}
cp.origin += cp.normal * f.cut_offset;
if (f.cut_flip) cp.normal = -cp.normal;
+16 -4
View File
@@ -252,6 +252,12 @@ struct CadFeature {
double thicken_thickness{2}; // wall thickness (always used as |value|)
bool thicken_flip{false}; // true: offset against the face normal
// Cut-by-face: when cut_face >= 0, apply_cut derives the cut plane from this face
// (via SketchPlane::from_face) instead of the base `plane`. cut_offset / cut_flip
// still apply along the derived normal.
int cut_face_body{-1}; // body owning the face; -1 = the target body
int cut_face{-1}; // global face id to cut along; -1 = use `plane`
template<class Archive>
void save(Archive& ar) const {
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
@@ -278,8 +284,9 @@ struct CadFeature {
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip);
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip,
cut_face_body, cut_face);
}
template<class Archive>
void load(Archive& ar) {
@@ -307,8 +314,9 @@ struct CadFeature {
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip);
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip,
cut_face_body, cut_face);
imported_solid = brep_from_string(brep);
}
};
@@ -408,6 +416,10 @@ public:
// +normal / -normal half; both => the body is split into two coexisting bodies.
int add_cut(const SketchPlane& plane, double offset, bool flip,
bool keep_upper, bool keep_lower, int target_body, const std::string& name);
// Split target_body along the plane of face `face` (owned by face_body, -1 = target).
// keep_upper/keep_lower select which half survives; both => split into two bodies.
int add_split_by_face(int target_body, int face_body, int face,
bool keep_upper, bool keep_lower, const std::string& name);
int add_mirror(const SketchPlane& plane, int target_body, BooleanMode mode,
const std::string& name);
// Rigid body transform: rotate `angle_deg` about `axis` through `pivot`, then translate.