Docs/Internals/Undo/Redo

Design: Full-Document Snapshots

MeshCraft uses a snapshot-based undo model: before every mutation, the entire Mc3Document is deep-copied and pushed onto a stack. Undo restores the previous copy; redo replays forward copies.

This is the simplest correct undo implementation possible. The alternative — a command/inverse-command pattern — requires every mutation to provide a reversible inverse, which is error-prone and requires considerably more code per feature.

Data Structures

class MeshCraftApplication : public cna::Application {
  std::shared_ptr<Mc3Document>  document_;
  std::vector<Mc3Document>      undoStack_;  // past states
  std::vector<Mc3Document>      redoStack_;  // future states (cleared on new edit)
  static constexpr int           MAX_UNDO = 20;
};

Push Protocol

Every editor action that modifies the document follows this exact sequence:

void MeshCraftApplication::pushUndo() {
    if (undoStack_.size() >= MAX_UNDO)
        undoStack_.erase(undoStack_.begin());     // drop oldest
    undoStack_.push_back(*document_);             // deep copy current state
    redoStack_.clear();                           // new branch → forget future
    renderer_.clearCaches();                      // pointers will change after undo
}
⚠️

Always call pushUndo() before mutating the document. Calling it after means the undo stack captures the already-mutated state.

Undo (Ctrl+Z)

void MeshCraftApplication::undo() {
    if (undoStack_.empty()) return;
    redoStack_.push_back(*document_);        // save current for redo
    *document_ = undoStack_.back();          // restore previous
    undoStack_.pop_back();
    renderer_.clearCaches();
    selectedObjects_.clear();               // selection may dangle
}

Redo (Ctrl+Shift+Z / Ctrl+Y)

void MeshCraftApplication::redo() {
    if (redoStack_.empty()) return;
    undoStack_.push_back(*document_);        // save current for undo
    *document_ = redoStack_.back();          // restore future
    redoStack_.pop_back();
    renderer_.clearCaches();
    selectedObjects_.clear();
}

Deep Copy Behaviour

Mc3Document has a compiler-generated copy constructor that deep-copies all fields because all nested objects are stored by value or std::vector/std::string:

struct Mc3Document {
    std::string              version;
    std::string              model;
    std::string              unit;
    std::string              sourcePath;
    std::vector<Mc3Light>    lights;
    std::vector<Mc3Camera>   cameras;
    std::vector<Mc3Texture>  textures;
    std::vector<Mc3Material> materials;
    std::vector<Mc3Object>   definitions;
    std::vector<Mc3Object>   objects;     // each Mc3Object owns its children
    std::vector<Mc3Action>   actions;
};

Crucially, Mc3Object.children is a std::vector<Mc3Object> — not pointers. This means the entire scene tree is copied recursively without any manual deep-copy logic. The copy is O(n) in the number of objects × fields per object.

Trade-offs

Snapshot model (current)Command pattern (alternative)
Correctness riskLow — only Mc3Document must be copyableHigh — every command must correctly implement its inverse
Memory cost per stepO(entire document)O(changed data only)
Time cost per editO(n) deep copyO(1) record command
Maximum history depth20 steps (configurable constant)Unlimited (or configurable by memory)
Implementation complexity~30 linesHundreds of lines + one inverse per mutation type

For typical scenes (dozens to a few hundred objects), the 20-step snapshot stack uses at most a few megabytes of RAM. For very large scenes (thousands of objects), memory consumption may become noticeable.

Cache Invalidation After Undo/Redo

After every undo or redo operation:

  1. renderer_.clearCaches() evicts all GPU mesh caches and the CSG cache. This is necessary because Mc3Object* pointers in the restored document are different addresses from the pre-undo document — all cache lookups would miss or return wrong data.
  2. selectedObjects_.clear() clears the selection because the selected Mc3Object* pointers now dangle. The user must re-select after undo.

What Is NOT Undoable

ℹ️

The undo stack is in RAM only. Closing the editor without saving loses all undo history as well as any unsaved document changes.