mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
A body carries its own name, because a body is not its first feature
User report 2026-08-23, and it is right: "you have renamed the feature extrusion,
not the body. i clicked rename on the body feature tree and the feature extrude
changed name. this means that you consider the extrusion = the body. this is very
far from truth as a body can contain several extrusions."
That is exactly what the previous commit did. It resolved a selected body to
CadBody::source_feature and renamed THAT feature, on the reasoning that a body has
no name of its own. The reasoning described an implementation detail — CadBody::
name is derived and restamped on every recompute — and mistook it for the user's
model. An Extrude, a Cut and a Fillet all land on the same body: the maker is one
operation in its history, and renaming it renames the wrong object.
A body now has a name of its own. CadBody::user_name, set only by a rename, is:
- carried across recompute() by body index, next to the per-body colour override
and under the same index contract the GUI already relies on for visibility and
Move — without which a name would survive exactly until the next feature;
- written into the recipe, because bodies are recomputed and never serialised, so
a name has nowhere else to live and would otherwise vanish on reopen;
- shown on the Bodies row ahead of the derived maker name, as "Body N — name",
with the number still leading because every status line, the interference
report and the mate errors identify a body that way.
The recipe block is APPENDED after the variables block rather than given a version
bump. A build that predates it reads features and variables, returns, and never
looks at the trailing bytes — so yesterday's projects open here and today's
projects still open there. A bump would have cost every project written today its
readability by the previous build, for one optional field.
Renaming a FEATURE is unchanged. The feature tree renames features; the Bodies
list renames bodies; neither reaches into the other.
WHAT THIS COST, and why it is written down. Getting here took two wrong turns
inside one fix, both mine:
1. UnselectAll() -> Unselect(). Both trees are wxTR_SINGLE, where UnselectAll()
— the MULTI-selection call — does nothing. That was the one-word reason the
rename had been vetoed everywhere (BEGIN_LABEL_EDIT refuses while
tree_body_selection() >= 0). Fixing it turned a pair of harmless no-ops into
a real loop: the two lists clear each other so "the target" is unambiguous,
so clicking a body row ran apply_body_row -> m_tree->Unselect() -> the feature
tree's SEL_CHANGED -> m_parts->Unselect(), which cleared the row just clicked.
The handler now clears the other list only when it actually holds a selection.
2. Trusting a screenshot taken after a polluted run. A leftover Rib card had
shifted the whole panel, so a click measured against it landed nowhere near
the row. Relaunch, then measure.
VERIFIED, on the rig and in the kernel:
body renamed ('Extrude', user_name=False) -> ('Bracket', user_name=True)
features untouched ['Sketch', 'Extrude'] before and after
survives a recompute add a Hole to the same body: features become
['Sketch', 'Extrude', 'Hole'], body stays 'Bracket'
survives the recipe serialize -> deserialize -> 'Bracket'
New kernel test "a body carries its own name, through recompute and the recipe"
pins all three properties; the suite is 189 cases / 2547 assertions.
Gate green: ALL LADDERS HELD — offer table matches the atlas, kernel suite,
engine rungs 1-8, 977-sheet corpus + the heaviest sheets, gesture ladder 98/98,
offer ladder 108/108.
This commit is contained in:
@@ -1032,47 +1032,25 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// tool vocabulary — a rename that only a double-click reveals is not discoverable, and
|
||||
// the row IS the object, so it belongs in the menu (and on F2) as well as on the row.
|
||||
auto rename_feature = [this] {
|
||||
int sel = tree_selection();
|
||||
// A SELECTED BODY RENAMES THE FEATURE THAT MAKES IT. A body has no name it can
|
||||
// keep — it is recomputed from the recipe on every change and CadBody::name is
|
||||
// derived from its source feature, which is why the label editor vetoes body rows
|
||||
// outright. So the verb resolves the body to that feature instead of refusing.
|
||||
// Reported as "clicking on a body row in feature tree, I cannot find rename on
|
||||
// right click": the offer DID open (that is the designed gesture for a selected
|
||||
// body) and simply had no Rename row, because the verb accepted sk_loop alone.
|
||||
if (sel == wxNOT_FOUND) {
|
||||
const int b = (m_sel_solid_body >= 0) ? m_sel_solid_body : tree_body_selection();
|
||||
if (b >= 0 && b < int(m_doc.bodies.size())) {
|
||||
const int src = m_doc.bodies[b].source_feature;
|
||||
if (src >= 0 && src < int(m_tree_items.size())) {
|
||||
// Unselect(), NOT UnselectAll(): both trees are wxTR_SINGLE, and
|
||||
// UnselectAll() is the MULTI-selection call — on a single-selection tree
|
||||
// it leaves the row selected. That is why every route into the rename
|
||||
// failed identically: the body row stayed selected, so
|
||||
// tree_body_selection() stayed >= 0 and BEGIN_LABEL_EDIT vetoed the
|
||||
// edit before it could open. Proven by discriminator: F2 on a body row
|
||||
// failed exactly like the menu, which rules out the menu's event loop.
|
||||
if (m_parts) m_parts->Unselect();
|
||||
m_tree->SelectItem(m_tree_items[src]);
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
set_status(wxString::Format(
|
||||
_L("A body takes its name from the feature that makes it — renaming '%s'"),
|
||||
wxString::FromUTF8(m_doc.features[src].name)));
|
||||
m_status->Refresh();
|
||||
sel = src;
|
||||
}
|
||||
}
|
||||
// A BODY renames ITSELF. The earlier version resolved the body to
|
||||
// CadBody::source_feature and renamed that feature, which is the wrong object: a
|
||||
// body accumulates many features and the first one is not its name. The body row
|
||||
// is editable now (CadBody::user_name), so the verb opens the editor there.
|
||||
const int b = tree_body_selection();
|
||||
if (b >= 0 && b < int(m_tree_body_items.size())) {
|
||||
const wxTreeItemId row = m_tree_body_items[b];
|
||||
// After the menu, not inside it: an editor opened from within PopupMenu's
|
||||
// nested loop never appears.
|
||||
CallAfter([this, row] { m_parts->SetFocus(); m_parts->EditLabel(row); });
|
||||
return;
|
||||
}
|
||||
const int sel = tree_selection();
|
||||
if (sel != wxNOT_FOUND && sel < int(m_tree_items.size())) {
|
||||
// AFTER the menu, not inside it: the offer runs a nested event loop and an
|
||||
// in-place editor opened from within it never appears (measured three times —
|
||||
// the row selected, the handler ran, no editor). Same family as the
|
||||
// refresh_tree-inside-END_LABEL_EDIT trap documented further down this file.
|
||||
const wxTreeItemId row = m_tree_items[sel];
|
||||
CallAfter([this, row] { m_tree->SetFocus(); m_tree->EditLabel(row); });
|
||||
} else {
|
||||
m_status->SetForegroundColour(wxNullColour); // "nothing selected" is not an error
|
||||
set_status(_L("Select a feature first — click a sketch or feature row, then rename it"));
|
||||
set_status(_L("Select a feature, or a body, first — then rename it"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
};
|
||||
@@ -3100,7 +3078,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
if (!m_viewport) return;
|
||||
// Bodies live in the Parts list now; picking a feature here drops any body selection
|
||||
// so the two lists can't both claim to be "the target".
|
||||
if (m_parts) m_parts->Unselect(); // wxTR_SINGLE: UnselectAll() does nothing here
|
||||
// ONLY when this tree actually has a selection. These two lists clear each other's
|
||||
// selection so that "the target" is never ambiguous, and that was harmless while both
|
||||
// calls were UnselectAll() — a no-op on a wxTR_SINGLE tree. Now that Unselect() really
|
||||
// clears, the pair became a loop: clicking a body row runs apply_body_row, which calls
|
||||
// m_tree->Unselect(), which fires THIS handler, which cleared the body row the user had
|
||||
// just clicked. The guard keeps the mutual-exclusion and drops the echo.
|
||||
if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->Unselect();
|
||||
const int sel = tree_selection();
|
||||
const bool body = (sel >= 0 && sel < int(m_doc.features.size()) &&
|
||||
m_doc.features[sel].type != CadFeatureType::Sketch &&
|
||||
@@ -3326,7 +3310,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_parts_rule->Hide();
|
||||
m_parts = new wxTreeCtrl(m_parts_box, wxID_ANY, wxDefaultPosition, wxSize(-1, 48),
|
||||
wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES |
|
||||
wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE);
|
||||
wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE | wxTR_EDIT_LABELS);
|
||||
if (!dp_dark()) m_parts->SetBackgroundColour(dp_panel_bg());
|
||||
parts_inner->Add(m_parts, 0, wxEXPAND | wxALL, 12);
|
||||
// Start hidden: a fresh document has no bodies, and refresh_parts() only runs on the first
|
||||
@@ -3363,6 +3347,32 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// can see highlighted. That is the confirmation a face pick cannot give: pointing at a face
|
||||
// lights the face, never the body the verb will actually change. The status line above has
|
||||
// been promising this right-click since before it existed.
|
||||
// Renaming a BODY names the body itself. It does NOT rename the feature that created it:
|
||||
// an Extrude, a Cut and a Fillet all land on one body, so source_feature is one operation in
|
||||
// its history and renaming that is renaming the wrong object — reported, correctly, as "you
|
||||
// consider the extrusion = the body". CadBody::user_name is carried across recompute() by
|
||||
// index and written into the recipe, so the name outlives both the rebuild and the save.
|
||||
m_parts->Bind(wxEVT_TREE_BEGIN_LABEL_EDIT, [this](wxTreeEvent& e) {
|
||||
if (tree_body_selection() < 0) { e.Veto(); return; }
|
||||
e.Skip();
|
||||
});
|
||||
m_parts->Bind(wxEVT_TREE_END_LABEL_EDIT, [this](wxTreeEvent& e) {
|
||||
if (e.IsEditCancelled()) return;
|
||||
const int b = tree_body_selection();
|
||||
if (b < 0 || b >= int(m_doc.bodies.size())) { e.Veto(); return; }
|
||||
wxString label = e.GetLabel();
|
||||
label.Trim(true).Trim(false);
|
||||
if (label.empty()) { e.Veto(); return; } // a nameless row is worse than a bad name
|
||||
m_doc.bodies[b].has_user_name = true;
|
||||
m_doc.bodies[b].user_name = std::string(label.ToUTF8().data());
|
||||
sync_recipe_to_model(); // the name is part of what gets saved
|
||||
e.Skip();
|
||||
// Rebuild on the NEXT event-loop turn: refresh_parts() destroys every wxTreeItemId and
|
||||
// we are inside wx's own END_LABEL_EDIT dispatch for one of them. The feature tree
|
||||
// learned this the hard way — doing it here took the process down.
|
||||
CallAfter([this] { refresh_parts(); });
|
||||
});
|
||||
|
||||
m_parts->Bind(wxEVT_TREE_ITEM_MENU, [this, apply_body_row](wxTreeEvent& e) {
|
||||
if (e.GetItem().IsOk())
|
||||
m_parts->SelectItem(e.GetItem()); // the row under the cursor, never a stale one
|
||||
@@ -7074,7 +7084,12 @@ void DesignPanel::refresh_parts()
|
||||
// renaming through this row and seeing the label sit unchanged at "Body 1" would read as
|
||||
// a rename that did nothing.
|
||||
const bool vis = b >= m_body_visible.size() || m_body_visible[b];
|
||||
const wxString bname = wxString::FromUTF8(m_doc.bodies[b].name);
|
||||
// The user's name wins over the derived one (the maker's name, restamped every
|
||||
// recompute); "Body N" still leads, because every status line, the interference report
|
||||
// and the mate errors identify a body by its number.
|
||||
const wxString bname = m_doc.bodies[b].has_user_name
|
||||
? wxString::FromUTF8(m_doc.bodies[b].user_name)
|
||||
: wxString::FromUTF8(m_doc.bodies[b].name);
|
||||
wxTreeItemId id = m_parts->AppendItem(proot,
|
||||
bname.IsEmpty() ? wxString::Format(_L("Body %zu"), b + 1)
|
||||
: wxString::Format(_L("Body %zu — %s"), b + 1, bname));
|
||||
|
||||
Reference in New Issue
Block a user