Design: a body knows what made it, so "Delete Body" can exist

Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent
faults stacked behind that.

FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound
(DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields
Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but
no face kind, so the row was filtered out entirely — while the status line read "Body 1
face 0 selected", which actively teaches the wrong model.

SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the
FEATURE that created this body". CadBody had no link back to its maker, so the offer was
advertising a verb it could not perform — worse than the action:null rows fixed earlier this
session, because this one is ENABLED and its refusal reads like user error.

CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop,
and the rule is just "still unset?". That is sufficient because of an invariant worth
stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape
(boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the
stamp it was born with; a consumed body is erased outright, taking its stamp with it; and
the only bodies still at -1 are the ones the current feature just pushed. A feature type
added later needs no change here as long as it keeps to that invariant.

"Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by
pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the
body kinds so both can never appear for one selection. Deleting a body removes the feature
that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a
body vanishing from the viewport is not evidence of which feature went, and this is the one
action here that cannot be eyeballed.

Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler
deletes exactly one body, so a two-body selection would have silently deleted whichever was
m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild.

Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete
Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint ->
Undo -> Extrude2 and Body 1 restored.
This commit is contained in:
Tommaso Bianchi
2026-08-02 09:41:58 +02:00
parent 34eb4224a1
commit b2654ebd8a
6 changed files with 97 additions and 8 deletions
+44
View File
@@ -938,6 +938,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
fadd("color", b_color);
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
m_verb_actions["btn:delete_body"] = [this] { on_delete_body(); };
m_verb_actions["btn:edit"] = [this] { on_edit_feature(); };
m_verb_actions["btn:mass"] = [this] { on_mass_properties(); };
@@ -6059,6 +6060,49 @@ void DesignPanel::on_new_design()
set_status_ok();
}
// The verb the offer names when you point at a body, or at any face/edge/vertex of one. A body
// is a recomputed RESULT, so what actually gets deleted is the feature that created it
// (CadBody::source_feature). That is an edit to the recipe and can take other features with it,
// so it asks first and NAMES the feature: a body disappearing from the viewport is not by itself
// evidence of which feature went, and this is the one action here that cannot be eyeballed.
void DesignPanel::on_delete_body()
{
const int nb = int(m_doc.bodies.size());
if (m_sel_solid_body < 0 || m_sel_solid_body >= nb) {
m_status->SetForegroundColour(wxColour(235, 110, 110));
set_status(_L("Select a body first"));
m_status->Refresh();
return;
}
const int src = m_doc.bodies[m_sel_solid_body].source_feature;
if (src < 0 || src >= int(m_doc.features.size())) {
// Only reachable for a body no feature claims — a stale recipe, or a feature type that
// broke the "never replace a whole CadBody" invariant recompute() relies on.
m_status->SetForegroundColour(wxColour(235, 110, 110));
set_status(_L("This body has no feature to delete — use New Design to start over"));
m_status->Refresh();
return;
}
const std::string& raw = m_doc.features[src].name;
const wxString fname = raw.empty() ? wxString::Format(_L("feature %d"), src + 1)
: wxString::FromUTF8(raw);
if (wxMessageBox(wxString::Format(
_L("Delete %s?\n\nThat is the feature this body was made from. "
"Features built on it may be removed or stop working."), fname),
_L("Delete body"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION, this) != wxYES)
return;
// A card left open over a feature that is about to vanish goes stale — same reason
// on_delete_feature() closes it.
if (m_active != Tool::None || m_edit_index >= 0) {
reset_edit_state();
close_tool();
}
m_doc.checkpoint(); // undo boundary: deleting a body's feature
m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1; // the selection is about to
m_sel_solid_vertex = false; // name a body that is gone
after_tree_edit(m_doc.remove_feature(src));
}
void DesignPanel::on_delete_feature()
{
// A Body row has no directly-removable feature (bodies are recomputed results); guide the