fix(editor): undo now covers texture painting, fix stale buffer bug

- Extend undo/redo to snapshot alpha maps and texture layers alongside
  heights — texture painting operations are now fully undoable
- Bracket paint mode with beginStroke/endStroke like sculpt mode
- Fix stale static char buffer in quest objective loop (showed wrong
  objective's description when editing multiple objectives)
- Zero-initialize all quest UI text buffers for null termination safety
- Fix unused variable warnings in terrain_editor.cpp
This commit is contained in:
Kelsi 2026-05-05 12:58:11 -07:00
parent 617228559a
commit 4e2f704124
5 changed files with 44 additions and 22 deletions

View file

@ -400,12 +400,12 @@ void EditorApp::processEvents() {
}
} else {
painting_ = true;
if (mode_ == EditorMode::Sculpt)
if (mode_ == EditorMode::Sculpt || mode_ == EditorMode::Paint)
terrainEditor_.beginStroke();
}
} else if (event.type == SDL_MOUSEBUTTONUP) {
painting_ = false;
if (mode_ == EditorMode::Sculpt)
if (mode_ == EditorMode::Sculpt || mode_ == EditorMode::Paint)
terrainEditor_.endStroke();
}
}

View file

@ -3,15 +3,37 @@
namespace wowee {
namespace editor {
ChunkSnapshot EditorHistory::captureChunk(const pipeline::ADTTerrain& terrain, int idx) {
ChunkSnapshot snap;
snap.chunkIndex = idx;
snap.heights = terrain.chunks[idx].heightMap.heights;
snap.alphaMap = terrain.chunks[idx].alphaMap;
snap.layers = terrain.chunks[idx].layers;
return snap;
}
void EditorHistory::restoreChunk(pipeline::ADTTerrain& terrain, const ChunkSnapshot& snap) {
terrain.chunks[snap.chunkIndex].heightMap.heights = snap.heights;
terrain.chunks[snap.chunkIndex].alphaMap = snap.alphaMap;
terrain.chunks[snap.chunkIndex].layers = snap.layers;
}
bool EditorHistory::snapshotChanged(const ChunkSnapshot& a, const ChunkSnapshot& b) {
if (a.heights != b.heights) return true;
if (a.alphaMap != b.alphaMap) return true;
if (a.layers.size() != b.layers.size()) return true;
for (size_t i = 0; i < a.layers.size(); i++) {
if (a.layers[i].textureId != b.layers[i].textureId) return true;
}
return false;
}
void EditorHistory::beginEdit(const pipeline::ADTTerrain& terrain,
const std::vector<int>& affectedChunks) {
pending_ = {};
pending_.before.reserve(affectedChunks.size());
for (int idx : affectedChunks) {
ChunkSnapshot snap;
snap.chunkIndex = idx;
snap.heights = terrain.chunks[idx].heightMap.heights;
pending_.before.push_back(snap);
pending_.before.push_back(captureChunk(terrain, idx));
}
}
@ -19,17 +41,13 @@ void EditorHistory::endEdit(const pipeline::ADTTerrain& terrain) {
pending_.after.reserve(pending_.before.size());
lastAffected_.clear();
for (const auto& snap : pending_.before) {
ChunkSnapshot after;
after.chunkIndex = snap.chunkIndex;
after.heights = terrain.chunks[snap.chunkIndex].heightMap.heights;
pending_.after.push_back(after);
pending_.after.push_back(captureChunk(terrain, snap.chunkIndex));
lastAffected_.push_back(snap.chunkIndex);
}
// Only push if something actually changed
bool changed = false;
for (size_t i = 0; i < pending_.before.size(); i++) {
if (pending_.before[i].heights != pending_.after[i].heights) {
if (snapshotChanged(pending_.before[i], pending_.after[i])) {
changed = true;
break;
}
@ -51,7 +69,7 @@ void EditorHistory::undo(pipeline::ADTTerrain& terrain) {
lastAffected_.clear();
for (const auto& snap : cmd.before) {
terrain.chunks[snap.chunkIndex].heightMap.heights = snap.heights;
restoreChunk(terrain, snap);
lastAffected_.push_back(snap.chunkIndex);
}
@ -66,7 +84,7 @@ void EditorHistory::redo(pipeline::ADTTerrain& terrain) {
lastAffected_.clear();
for (const auto& snap : cmd.after) {
terrain.chunks[snap.chunkIndex].heightMap.heights = snap.heights;
restoreChunk(terrain, snap);
lastAffected_.push_back(snap.chunkIndex);
}

View file

@ -11,6 +11,8 @@ namespace editor {
struct ChunkSnapshot {
int chunkIndex;
std::array<float, 145> heights;
std::vector<uint8_t> alphaMap;
std::vector<pipeline::TextureLayer> layers;
};
struct EditCommand {
@ -37,6 +39,10 @@ public:
const std::vector<int>& lastAffectedChunks() const { return lastAffected_; }
private:
static ChunkSnapshot captureChunk(const pipeline::ADTTerrain& terrain, int idx);
static void restoreChunk(pipeline::ADTTerrain& terrain, const ChunkSnapshot& snap);
static bool snapshotChanged(const ChunkSnapshot& a, const ChunkSnapshot& b);
std::vector<EditCommand> undoStack_;
std::vector<EditCommand> redoStack_;
EditCommand pending_;

View file

@ -1706,12 +1706,12 @@ void EditorUI::renderQuestPanel(EditorApp& app) {
auto& tmpl = qe.getTemplate();
if (ImGui::CollapsingHeader("New Quest", ImGuiTreeNodeFlags_DefaultOpen)) {
static char titleBuf[128] = "New Quest";
char titleBuf[128] = {};
std::strncpy(titleBuf, tmpl.title.c_str(), sizeof(titleBuf) - 1);
if (ImGui::InputText("Title##q", titleBuf, sizeof(titleBuf)))
tmpl.title = titleBuf;
static char descBuf[512] = "";
char descBuf[512] = {};
std::strncpy(descBuf, tmpl.description.c_str(), sizeof(descBuf) - 1);
if (ImGui::InputTextMultiline("Description##q", descBuf, sizeof(descBuf), ImVec2(-1, 60)))
tmpl.description = descBuf;
@ -1756,7 +1756,7 @@ void EditorUI::renderQuestPanel(EditorApp& app) {
int ti = static_cast<int>(obj.type);
ImGui::Combo("Type", &ti, types, 6);
obj.type = static_cast<QuestObjectiveType>(ti);
static char objDesc[128];
char objDesc[128] = {};
std::strncpy(objDesc, obj.description.c_str(), sizeof(objDesc) - 1);
if (ImGui::InputText("Desc", objDesc, sizeof(objDesc))) obj.description = objDesc;
int cnt = obj.targetCount;
@ -1786,7 +1786,7 @@ void EditorUI::renderQuestPanel(EditorApp& app) {
ImGui::EndCombo();
}
static char completeBuf[256] = "";
char completeBuf[256] = {};
std::strncpy(completeBuf, tmpl.completionText.c_str(), sizeof(completeBuf) - 1);
if (ImGui::InputTextMultiline("Completion Text##q", completeBuf, sizeof(completeBuf), ImVec2(-1, 40)))
tmpl.completionText = completeBuf;
@ -1816,7 +1816,7 @@ void EditorUI::renderQuestPanel(EditorApp& app) {
if (selectedQuest >= 0 && selectedQuest < static_cast<int>(qe.questCount())) {
auto* sq = qe.getQuest(selectedQuest);
ImGui::TextColored(ImVec4(1, 0.8f, 0.3f, 1), "Editing: [%u] %s", sq->id, sq->title.c_str());
static char etBuf[128];
char etBuf[128] = {};
std::strncpy(etBuf, sq->title.c_str(), sizeof(etBuf) - 1);
if (ImGui::InputText("Title##edit", etBuf, sizeof(etBuf))) sq->title = etBuf;
int elv = sq->requiredLevel;

View file

@ -1445,7 +1445,7 @@ void TerrainEditor::applyErode(float dt) {
if (influence <= 0.0f) continue;
float h = chunk.heightMap.heights[v];
int row = v / 17, col = v % 17;
int col = v % 17;
// Find lowest neighbor (same chunk)
float lowestH = h;
@ -1505,8 +1505,6 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue;
int cx = ci % 16, cy = ci / 16;
for (int v = 0; v < 145; v++) {
glm::vec3 wpos = chunkVertexWorldPos(ci, v);