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:
Tommaso Bianchi
2026-08-23 19:09:17 +02:00
parent 0ac9ac91f7
commit 6e7f6429fa
5 changed files with 168 additions and 40 deletions
+54 -1
View File
@@ -3592,6 +3592,12 @@ bool CadDocument::recompute()
built[i].has_color = true;
built[i].color = bodies[i].color;
}
// ...and the name the user gave the body, for the same reason and by the same index
// contract. Without this a rename would live exactly until the next feature was added.
if (bodies[i].has_user_name) {
built[i].has_user_name = true;
built[i].user_name = bodies[i].user_name;
}
}
bodies = std::move(built);
// The face and edge maps have just been rebuilt, so every global id handed out before this
@@ -3729,6 +3735,31 @@ std::string CadDocument::serialize_recipe() const
uint32_t vlen = static_cast<uint32_t>(vb.size());
ar(vlen);
ar(cereal::binary_data(vb.data(), vb.size()));
// BODY NAMES, appended after the variables block. Bodies are not serialised — they are
// recomputed — so a name the user gave one has nowhere else to live, and without this it
// would survive a recompute (see the carry-over in recompute()) but not a save.
//
// APPENDED RATHER THAN VERSION-BUMPED, on purpose: a build that predates this block
// reads features and variables, returns, and never looks at the trailing bytes, so its
// projects still open here AND this build's projects still open there. A version bump
// would have made every project written today unreadable by yesterday's build for the
// sake of one optional field. Written as (index, name) pairs so an unnamed body costs
// nothing.
// A map, not a vector of pairs: cereal's map support is already included here and its
// pair support is not, and one more include for one more field is not worth it.
std::map<uint32_t, std::string> named;
for (uint32_t i = 0; i < bodies.size(); ++i)
if (bodies[i].has_user_name && !bodies[i].user_name.empty())
named[i] = bodies[i].user_name;
std::ostringstream bos;
{
cereal::BinaryOutputArchive ba(bos);
ba(named);
}
std::string bb = bos.str();
uint32_t blen = static_cast<uint32_t>(bb.size());
ar(blen);
ar(cereal::binary_data(bb.data(), bb.size()));
}
return oss.str();
}
@@ -3789,7 +3820,29 @@ bool CadDocument::deserialize_recipe(const std::string& blob)
} catch (...) {
// Variables blob predates this build: keep what was read, default the rest.
}
return recompute();
// Body names, if this project carries them. A project written before the block
// simply ends here, so the read throws and there are no names — not an error.
std::map<uint32_t, std::string> named;
try {
uint32_t blen;
ar(blen);
std::string bbuf(blen, '\0');
if (blen > 0)
ar(cereal::binary_data(&bbuf[0], blen));
std::istringstream bs(bbuf);
cereal::BinaryInputArchive ba(bs);
ba(named);
} catch (...) {
named.clear();
}
// AFTER the rebuild, never before: recompute() replaces the bodies vector wholesale.
const bool ok = recompute();
for (const auto& kv : named)
if (kv.first < bodies.size()) {
bodies[kv.first].has_user_name = true;
bodies[kv.first].user_name = kv.second;
}
return ok;
}
if (v == 4) {
// Pre-framing flat path, unchanged: v4 projects keep opening exactly as before.