diff --git a/docs/ux/tool_atlas.json b/docs/ux/tool_atlas.json index 1d70edf7df..ec6ce04986 100644 --- a/docs/ux/tool_atlas.json +++ b/docs/ux/tool_atlas.json @@ -1059,14 +1059,11 @@ "feature": null, "mcp": null, "accepts": [ - "body_solid", - "body_sheet", "sk_loop", "art", "datum_plane", "datum_axis", "coordsys", - "bodies_2", "sk_line", "sk_arc", "sk_point", @@ -1079,6 +1076,31 @@ "icon": "design_delete", "hint": "Delete what is selected" }, + { + "id": "delete_body", + "name": "Delete Body", + "slot": "modify", + "key": null, + "action": "btn:delete_body", + "refusal": null, + "accepts": [ + "body_solid", + "body_sheet", + "face_planar", + "face_cyl", + "face_other", + "edge_str", + "edge_circ", + "vertex" + ], + "needs": { + "bodies": 1 + }, + "mode": "model", + "family": null, + "icon": "design_delete", + "hint": "Delete this whole body — removes the feature it was made from" + }, { "id": "sk_line_t", "name": "Line", diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index ebb7fbd913..8897d1f3a0 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -3208,15 +3208,27 @@ bool CadDocument::recompute() for (CadFeature& f : features) for (const auto& [field, e] : f.expr) assign_field(f, field, eval_expr(e, varvals)); - for (CadFeature& f : features) { + for (size_t fi = 0; fi < features.size(); ++fi) { + CadFeature& f = features[fi]; if (!f.enabled) continue; if (f.type == CadFeatureType::Sketch) continue; // consumed by an extrude if (f.type == CadFeatureType::Helix) continue; // consumed by Sweep as a path if (f.type == CadFeatureType::Plane) continue; // datum: no solid, derived on demand if (f.type == CadFeatureType::Axis) continue; // datum axis if (f.type == CadFeatureType::CoordSys) continue; // datum coordinate system - if (f.type == CadFeatureType::Project) { apply_project(built, f); continue; } - route_feature(built, f); + if (f.type == CadFeatureType::Project) { apply_project(built, f); } + else { route_feature(built, f); } + // Record which feature made each body. "Still unset?" is the whole rule, and it 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 — checked, all 8 sites), so an existing 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 here are the ones THIS feature just pushed. That also + // means a feature type added later needs no change here, as long as it keeps to the + // same invariant. + for (CadBody& b : built) + if (b.source_feature < 0) + b.source_feature = int(fi); } } catch (const Standard_Failure& e) { // OCCT raises Standard_Failure (NOT a std::exception) — must be caught diff --git a/src/libslic3r/CadDocument.hpp b/src/libslic3r/CadDocument.hpp index ed00bdddab..62675f2331 100644 --- a/src/libslic3r/CadDocument.hpp +++ b/src/libslic3r/CadDocument.hpp @@ -400,6 +400,12 @@ struct CadBody { // falls back to the auto body-index palette. Carried across recompute() by body index. bool has_color{false}; ColorRGBA color; + // Index into `features` of the feature that CREATED this body, or -1. A body is a + // recomputed result, so without this there is no way back to its maker and "delete this + // body" cannot be expressed at all — the GUI could only answer "select the FEATURE that + // created this body". Stamped in one place, the recompute loop; see the note there for + // why a single "still unset?" test is sufficient and stays correct for new feature types. + int source_feature{-1}; }; // OCCT-only feature tree backing the Design tab. No GUI dependencies (lives in libslic3r). diff --git a/src/slic3r/GUI/DesignOffer.hpp b/src/slic3r/GUI/DesignOffer.hpp index bf3cb83910..1a00098c25 100644 --- a/src/slic3r/GUI/DesignOffer.hpp +++ b/src/slic3r/GUI/DesignOffer.hpp @@ -123,7 +123,8 @@ static const OfferVerb kOfferVerbs[] = { {"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"}, {"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"}, {"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x00000180u, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"}, - {"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7f80u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"}, + {"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"}, + {"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"}, {"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"}, {"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click first / right-click to close the loop"}, {"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"}, @@ -168,7 +169,7 @@ static const OfferVerb kOfferVerbs[] = { {"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"}, {"sk_delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"}, }; -static const int kOfferVerbCount = 86; +static const int kOfferVerbCount = 87; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index c049bffc21..305f255f3d 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -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 diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 3b995220de..34b38f284e 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -175,6 +175,10 @@ private: // Feature-tree editing (Onshape-style): act on the selected tree row. void on_delete_feature(); + // "Delete Body" — the geometry-first counterpart, reached by pointing at a body or any of + // its faces. Resolves the body to the feature that created it and removes THAT, because a + // body is a recomputed result and has nothing else to delete. + void on_delete_body(); void on_new_design(); void on_move_feature(int delta); // -1 = up, +1 = down void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled)