Files

109 lines
3.3 KiB
C++

#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <unordered_map>
#include <vector>
#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<EditOp> 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<EditBatch> undoStack;
std::deque<EditBatch> redoStack;
EditBatch pending;
std::unordered_map<Vec3i, std::size_t, Vec3iHash> 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