CAD: undo() never rolled back variables, so a bad one bricked the recipe

checkpoint() snapshotted `features` and undo() restored `features`, but
`variables` is a separate member of CadDocument. Every caller of the documented
checkpoint -> mutate -> recompute -> undo-on-failure pattern therefore failed to
roll a variable back: the bad value stayed in the document and every later
recompute failed, which is exactly the corruption the pattern exists to prevent.
Feature `expr` bindings were unaffected only because expr lives inside
CadFeature and rode along in the features snapshot — which is why the feature
side appeared to work.

This was a kernel gap, not a GUI one: McpControl::action_set_variable has the
same sequence and was equally broken.

The undo/redo stacks now hold a {features, variables} Snapshot. Nothing here is
serialized, so no recipe version change and no golden-fixture regeneration.

Two tests, both verified to FAIL against a faithful reproduction of the bug
(undo() leaving `variables` untouched) at test_caddocument.cpp:4420 and :4441:
one covers restoring a variable's previous value, the other covers removing a
variable that did not exist before the checkpoint. Worth recording that the
first mutation attempt was NOT faithful — it dropped the restore but kept
std::move(variables) into the redo stack, which empties the map as a side effect
and made the second test pass for the wrong reason. A mutation has to reproduce
the original defect, not merely break the code.

Second defect, same area: undo() calls recompute(), which succeeds and clears
doc.error, so the reason an edit was rejected was destroyed before anything
could display it. Six sites — four in DesignPanel, two in McpControl — now carry
the message across the rollback. on_remove_variable additionally asserted
"referenced by a feature expression" as fact; it now offers that as the likely
cause and appends the real error, since that diagnosis is wrong for any other
failure.

Suite 141 cases / 1972 assertions (was 139/1960). GUI compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-07-25 23:56:34 +02:00
co-authored by Claude Opus 5
parent 33275f2c74
commit c565d6ba86
5 changed files with 94 additions and 20 deletions
+8 -6
View File
@@ -1728,8 +1728,8 @@ void CadDocument::clear()
void CadDocument::checkpoint()
{
m_undo.push_back(features); // snapshot the pre-mutation recipe
m_redo.clear(); // any new action invalidates the redo branch
m_undo.push_back(Snapshot{features, variables}); // snapshot the pre-mutation recipe
m_redo.clear(); // any new action invalidates the redo branch
if (m_undo.size() > k_undo_cap)
m_undo.erase(m_undo.begin());
}
@@ -1738,8 +1738,9 @@ bool CadDocument::undo()
{
if (m_undo.empty())
return false;
m_redo.push_back(std::move(features)); // current state becomes redoable
features = std::move(m_undo.back());
m_redo.push_back(Snapshot{std::move(features), std::move(variables)}); // current state becomes redoable
features = std::move(m_undo.back().features);
variables = std::move(m_undo.back().variables);
m_undo.pop_back();
recompute(); // benign-empty (only a sketch / empty doc) is a valid undo target
return true;
@@ -1749,8 +1750,9 @@ bool CadDocument::redo()
{
if (m_redo.empty())
return false;
m_undo.push_back(std::move(features));
features = std::move(m_redo.back());
m_undo.push_back(Snapshot{std::move(features), std::move(variables)});
features = std::move(m_redo.back().features);
variables = std::move(m_redo.back().variables);
m_redo.pop_back();
recompute();
return true;
+15 -5
View File
@@ -662,11 +662,21 @@ private:
static DatumCoordSys datum_frame(const std::vector<CadBody>& bodies, const CadFeature& f);
void apply_mate(std::vector<CadBody>& bodies, const CadFeature& f) const;
// Undo/redo stacks of feature-list snapshots. checkpoint() pushes onto m_undo and
// clears m_redo; undo()/redo() shuffle the current state between them. Capped so a
// long session can't grow unbounded.
std::vector<std::vector<CadFeature>> m_undo;
std::vector<std::vector<CadFeature>> m_redo;
// Undo/redo stacks of recipe snapshots. checkpoint() pushes onto m_undo and clears
// m_redo; undo()/redo() shuffle the current state between them. Capped so a long
// session can't grow unbounded.
//
// The snapshot MUST carry `variables` as well as `features`: a caller that sets a bad
// variable, sees recompute() fail and calls undo() to roll it back would otherwise be
// left with the bad variable still in the document, so every later recompute fails —
// the exact corruption the checkpoint/undo pattern exists to prevent. Not serialized,
// so this changes no on-disk format.
struct Snapshot {
std::vector<CadFeature> features;
std::map<std::string, std::string> variables;
};
std::vector<Snapshot> m_undo;
std::vector<Snapshot> m_redo;
static constexpr size_t k_undo_cap = 200;
};