#pragma once #include #include #include #include #include #include #include "Block.h" #include "Math.h" #include "World.h" namespace vf { // One block changed by one edit. `before` is what the world held when the op was // recorded, which is what undo writes back. struct EditOp { Vec3i position{0, 0, 0}; Block before = Block::Air; Block after = Block::Air; }; // Every op belonging to a single user action. One brush stroke, one paste and // one fill are each exactly one batch, so undo takes back the whole action // instead of a block at a time. Positions inside a batch are unique. struct EditBatch { std::string label; uint64_t tick = 0; std::vector ops; std::size_t byteSize() const; bool touches(const Vec3i& position) const; }; struct HistoryStats { std::size_t batchCount = 0; std::size_t redoBatchCount = 0; std::size_t opCount = 0; std::size_t bytes = 0; std::size_t undoCount = 0; std::size_t redoCount = 0; std::size_t droppedBatches = 0; std::size_t mergedBatches = 0; }; // Undo/redo over a World, with a hard byte budget. // // Writes go through recordSet so the previous value is captured at the moment // of the write; the world is never scanned afterwards to guess what changed. // The budget covers both stacks, and is enforced by dropping the oldest undo // batches - history is the first thing that should give way under memory // pressure, and the newest edits are the ones a user actually wants back. class EditHistory { public: explicit EditHistory(std::size_t memoryBudgetBytes = 1u << 20); void setMemoryBudget(std::size_t bytes); std::size_t memoryBudget() const { return budget; } // Two batches merge into one when they overlap on at least one block and // their ticks are within this window, so a burst of small strokes on the // same spot undoes as one action. Zero disables merging. void setMergeWindow(uint64_t ticks) { mergeWindow = ticks; } uint64_t mergeWindowTicks() const { return mergeWindow; } bool beginBatch(const std::string& label, uint64_t tick); bool batchOpen() const { return open; } std::size_t openBatchSize() const { return pending.ops.size(); } bool recordSet(World& world, const Vec3i& position, Block block); bool endBatch(); bool cancelBatch(World& world); bool undo(World& world); bool redo(World& world); std::size_t undoDepth() const { return undoStack.size(); } std::size_t redoDepth() const { return redoStack.size(); } const EditBatch* peekUndo() const; const EditBatch* peekRedo() const; void clear(); HistoryStats stats() const; private: void pushBatch(EditBatch&& batch); bool mergeIntoPrevious(const EditBatch& batch); void enforceBudget(); void recomputeBytes(); static void compact(EditBatch& batch); std::deque undoStack; std::deque redoStack; EditBatch pending; std::unordered_map pendingIndex; bool open = false; std::size_t budget = 1u << 20; std::size_t bytesUsed = 0; uint64_t mergeWindow = 0; std::size_t undoCount = 0; std::size_t redoCount = 0; std::size_t droppedBatches = 0; std::size_t mergedBatches = 0; }; } // namespace vf