From e52980886e93fd74342084074b98002831c77f72 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 15:22:14 +0200 Subject: [PATCH] feat: add EditHistory --- core/src/EditHistory.cpp | 200 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 core/src/EditHistory.cpp diff --git a/core/src/EditHistory.cpp b/core/src/EditHistory.cpp new file mode 100644 index 0000000..f870f32 --- /dev/null +++ b/core/src/EditHistory.cpp @@ -0,0 +1,200 @@ +#include "vf/EditHistory.h" + +#include + +namespace vf { + +std::size_t EditBatch::byteSize() const { + return sizeof(EditBatch) + label.size() + ops.size() * sizeof(EditOp); +} + +bool EditBatch::touches(const Vec3i& position) const { + for (const EditOp& op : ops) + if (op.position == position) return true; + return false; +} + +EditHistory::EditHistory(std::size_t memoryBudgetBytes) : budget(memoryBudgetBytes) {} + +void EditHistory::setMemoryBudget(std::size_t bytes) { + budget = bytes; + enforceBudget(); +} + +bool EditHistory::beginBatch(const std::string& label, uint64_t tick) { + if (open) return false; + pending = EditBatch{}; + pending.label = label; + pending.tick = tick; + pendingIndex.clear(); + open = true; + return true; +} + +bool EditHistory::recordSet(World& world, const Vec3i& position, Block block) { + if (!open) return false; + Block current = world.blockAt(position.x, position.y, position.z); + if (current == block) return false; + + auto known = pendingIndex.find(position); + if (known == pendingIndex.end()) { + pendingIndex.emplace(position, pending.ops.size()); + pending.ops.push_back(EditOp{position, current, block}); + } else { + pending.ops[known->second].after = block; + } + world.setBlock(position.x, position.y, position.z, block); + return true; +} + +void EditHistory::compact(EditBatch& batch) { + batch.ops.erase(std::remove_if(batch.ops.begin(), batch.ops.end(), + [](const EditOp& op) { return op.before == op.after; }), + batch.ops.end()); +} + +bool EditHistory::endBatch() { + if (!open) return false; + open = false; + pendingIndex.clear(); + compact(pending); + if (pending.ops.empty()) { + pending = EditBatch{}; + return false; + } + + redoStack.clear(); + if (!mergeIntoPrevious(pending)) pushBatch(std::move(pending)); + pending = EditBatch{}; + enforceBudget(); + return true; +} + +bool EditHistory::cancelBatch(World& world) { + if (!open) return false; + open = false; + pendingIndex.clear(); + for (std::size_t i = pending.ops.size(); i > 0; --i) { + const EditOp& op = pending.ops[i - 1]; + world.setBlock(op.position.x, op.position.y, op.position.z, op.before); + } + pending = EditBatch{}; + return true; +} + +bool EditHistory::mergeIntoPrevious(const EditBatch& batch) { + if (mergeWindow == 0u || undoStack.empty()) return false; + EditBatch& previous = undoStack.back(); + uint64_t gap = batch.tick >= previous.tick ? batch.tick - previous.tick + : previous.tick - batch.tick; + if (gap > mergeWindow) return false; + + bool overlaps = false; + for (const EditOp& op : batch.ops) { + if (previous.touches(op.position)) { + overlaps = true; + break; + } + } + if (!overlaps) return false; + + std::unordered_map index; + index.reserve(previous.ops.size()); + for (std::size_t i = 0; i < previous.ops.size(); ++i) index.emplace(previous.ops[i].position, i); + + for (const EditOp& op : batch.ops) { + auto known = index.find(op.position); + if (known == index.end()) { + index.emplace(op.position, previous.ops.size()); + previous.ops.push_back(op); + } else { + previous.ops[known->second].after = op.after; + } + } + compact(previous); + previous.tick = batch.tick; + ++mergedBatches; + if (previous.ops.empty()) undoStack.pop_back(); + return true; +} + +void EditHistory::pushBatch(EditBatch&& batch) { undoStack.push_back(std::move(batch)); } + +void EditHistory::recomputeBytes() { + std::size_t total = 0; + for (const EditBatch& batch : undoStack) total += batch.byteSize(); + for (const EditBatch& batch : redoStack) total += batch.byteSize(); + bytesUsed = total; +} + +void EditHistory::enforceBudget() { + recomputeBytes(); + while (bytesUsed > budget && !undoStack.empty()) { + bytesUsed -= undoStack.front().byteSize(); + undoStack.pop_front(); + ++droppedBatches; + } + while (bytesUsed > budget && !redoStack.empty()) { + bytesUsed -= redoStack.front().byteSize(); + redoStack.pop_front(); + ++droppedBatches; + } +} + +bool EditHistory::undo(World& world) { + if (undoStack.empty()) return false; + EditBatch batch = std::move(undoStack.back()); + undoStack.pop_back(); + for (std::size_t i = batch.ops.size(); i > 0; --i) { + const EditOp& op = batch.ops[i - 1]; + world.setBlock(op.position.x, op.position.y, op.position.z, op.before); + } + redoStack.push_back(std::move(batch)); + ++undoCount; + enforceBudget(); + return true; +} + +bool EditHistory::redo(World& world) { + if (redoStack.empty()) return false; + EditBatch batch = std::move(redoStack.back()); + redoStack.pop_back(); + for (const EditOp& op : batch.ops) + world.setBlock(op.position.x, op.position.y, op.position.z, op.after); + undoStack.push_back(std::move(batch)); + ++redoCount; + enforceBudget(); + return true; +} + +const EditBatch* EditHistory::peekUndo() const { + return undoStack.empty() ? nullptr : &undoStack.back(); +} + +const EditBatch* EditHistory::peekRedo() const { + return redoStack.empty() ? nullptr : &redoStack.back(); +} + +void EditHistory::clear() { + undoStack.clear(); + redoStack.clear(); + pending = EditBatch{}; + pendingIndex.clear(); + open = false; + bytesUsed = 0; +} + +HistoryStats EditHistory::stats() const { + HistoryStats summary; + summary.batchCount = undoStack.size(); + summary.redoBatchCount = redoStack.size(); + for (const EditBatch& batch : undoStack) summary.opCount += batch.ops.size(); + summary.bytes = bytesUsed; + summary.undoCount = undoCount; + summary.redoCount = redoCount; + summary.droppedBatches = droppedBatches; + summary.mergedBatches = mergedBatches; + return summary; +} + +} // namespace vf