feat(editor): generator undo, quit confirmation, state cleanup

- All terrain generators now undoable: crater, mesa, hill, voronoi,
  dunes, detail noise, thermal erosion, canyon, island, ridge, road,
  river, perlin noise — all wrapped with recordGeneratorUndo/commit
- Unsaved changes warning on quit: Save & Quit / Quit / Cancel dialog
- createNewTerrain clears quest editor and path capture state
- recordGeneratorUndo/commitGeneratorUndo helper methods snapshot all
  256 chunks before/after any generator operation
This commit is contained in:
Kelsi 2026-05-05 13:26:38 -07:00
parent 86f1a7d109
commit 7e02db73df
4 changed files with 77 additions and 4 deletions

View file

@ -152,6 +152,30 @@ void EditorApp::run() {
ui_.render(*this); ui_.render(*this);
if (showQuitConfirm_) {
ImGui::OpenPopup("Unsaved Changes");
if (ImGui::BeginPopupModal("Unsaved Changes", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("You have unsaved changes. Save before quitting?");
ImGui::Separator();
if (ImGui::Button("Save & Quit", ImVec2(120, 0))) {
quickSave();
window_->setShouldClose(true);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Quit", ImVec2(80, 0))) {
window_->setShouldClose(true);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
showQuitConfirm_ = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
ImGui::Render(); ImGui::Render();
VkRenderPassBeginInfo rpInfo{}; VkRenderPassBeginInfo rpInfo{};
@ -212,8 +236,12 @@ void EditorApp::processEvents() {
ImGui_ImplSDL2_ProcessEvent(&event); ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) { if (event.type == SDL_QUIT) {
window_->setShouldClose(true); if (terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) {
return; showQuitConfirm_ = true;
} else {
window_->setShouldClose(true);
return;
}
} }
if (event.type == SDL_WINDOWEVENT) { if (event.type == SDL_WINDOWEVENT) {
@ -690,8 +718,10 @@ void EditorApp::loadADT(const std::string& mapName, int tileX, int tileY) {
void EditorApp::createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome) { void EditorApp::createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome) {
terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome); terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome);
// Clear previous state // Clear all previous state
clearAllObjects(); clearAllObjects();
questEditor_.clear();
ui_.clearPath();
terrainEditor_.setTerrain(&terrain_); terrainEditor_.setTerrain(&terrain_);
terrainEditor_.history().clear(); terrainEditor_.history().clear();

View file

@ -135,6 +135,7 @@ private:
float autoSaveTimer_ = 0.0f; float autoSaveTimer_ = 0.0f;
float autoSaveInterval_ = 300.0f; float autoSaveInterval_ = 300.0f;
bool autoSaveEnabled_ = true; bool autoSaveEnabled_ = true;
bool showQuitConfirm_ = false;
// Toast notifications // Toast notifications
struct Toast { std::string msg; float timer; }; struct Toast { std::string msg; float timer; };

View file

@ -244,6 +244,20 @@ void TerrainEditor::endStroke() {
history_.endEdit(*terrain_); history_.endEdit(*terrain_);
} }
void TerrainEditor::recordGeneratorUndo() {
if (!terrain_) return;
std::vector<int> valid;
for (int i = 0; i < 256; i++) {
if (terrain_->chunks[i].hasHeightMap()) valid.push_back(i);
}
history_.beginEdit(*terrain_, valid);
}
void TerrainEditor::commitGeneratorUndo() {
if (!terrain_) return;
history_.endEdit(*terrain_);
}
void TerrainEditor::applyBrush(float deltaTime) { void TerrainEditor::applyBrush(float deltaTime) {
if (!terrain_ || !brush_.isActive()) return; if (!terrain_ || !brush_.isActive()) return;
@ -727,6 +741,7 @@ void TerrainEditor::mirrorY() {
void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end, void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end,
float width, float depth) { float width, float depth) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y); glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y); glm::vec2 lineEnd(end.x, end.y);
glm::vec2 lineDir = glm::normalize(lineEnd - lineStart); glm::vec2 lineDir = glm::normalize(lineEnd - lineStart);
@ -762,10 +777,12 @@ void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end,
} }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createCrater(const glm::vec3& center, float radius, float depth, float rimHeight) { void TerrainEditor::createCrater(const glm::vec3& center, float radius, float depth, float rimHeight) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) { for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci]; auto& chunk = terrain_->chunks[ci];
@ -803,11 +820,12 @@ void TerrainEditor::createCrater(const glm::vec3& center, float radius, float de
} }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createMesa(const glm::vec3& center, float radius, float height, float edgeSteepness) { void TerrainEditor::createMesa(const glm::vec3& center, float radius, float height, float edgeSteepness) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) { for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci]; auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue; if (!chunk.hasHeightMap()) continue;
@ -838,10 +856,12 @@ void TerrainEditor::createMesa(const glm::vec3& center, float radius, float heig
} }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createHill(const glm::vec3& center, float radius, float height) { void TerrainEditor::createHill(const glm::vec3& center, float radius, float height) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) { for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci]; auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue; if (!chunk.hasHeightMap()) continue;
@ -858,10 +878,12 @@ void TerrainEditor::createHill(const glm::vec3& center, float radius, float heig
if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); } if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t seed) { void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t seed) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE; float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(terrain_->coord.x)) * TILE_SIZE; float tileNW_Y = (32.0f - static_cast<float>(terrain_->coord.x)) * TILE_SIZE;
@ -898,10 +920,12 @@ void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t s
} }
for (int ci = 0; ci < 256; ci++) stitchEdges(ci); for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createDunes(float wavelength, float amplitude, float direction, uint32_t seed) { void TerrainEditor::createDunes(float wavelength, float amplitude, float direction, uint32_t seed) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
float dirRad = direction * 3.14159f / 180.0f; float dirRad = direction * 3.14159f / 180.0f;
float dx = std::cos(dirRad), dy = std::sin(dirRad); float dx = std::cos(dirRad), dy = std::sin(dirRad);
@ -928,6 +952,7 @@ void TerrainEditor::createDunes(float wavelength, float amplitude, float directi
} }
for (int ci = 0; ci < 256; ci++) stitchEdges(ci); for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::rotateTerrain90() { void TerrainEditor::rotateTerrain90() {
@ -1061,6 +1086,7 @@ void TerrainEditor::smoothBeaches(float waterHeight, float beachWidth) {
} }
void TerrainEditor::addDetailNoise(float amplitude, float frequency, uint32_t seed) { void TerrainEditor::addDetailNoise(float amplitude, float frequency, uint32_t seed) {
recordGeneratorUndo();
if (!terrain_) return; if (!terrain_) return;
auto hash2d = [](int x, int y, uint32_t s) -> float { auto hash2d = [](int x, int y, uint32_t s) -> float {
uint32_t h = static_cast<uint32_t>(x * 374761393 + y * 668265263 + s); uint32_t h = static_cast<uint32_t>(x * 374761393 + y * 668265263 + s);
@ -1116,9 +1142,11 @@ void TerrainEditor::rampEdges(float targetHeight, float rampWidth) {
} }
for (int ci = 0; ci < 256; ci++) stitchEdges(ci); for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::thermalErosion(int iterations, float talusAngle) { void TerrainEditor::thermalErosion(int iterations, float talusAngle) {
recordGeneratorUndo();
if (!terrain_) return; if (!terrain_) return;
float unitSize = CHUNK_SIZE / 8.0f; float unitSize = CHUNK_SIZE / 8.0f;
float maxDelta = std::tan(talusAngle * 3.14159f / 180.0f) * unitSize; float maxDelta = std::tan(talusAngle * 3.14159f / 180.0f) * unitSize;
@ -1184,9 +1212,11 @@ void TerrainEditor::terraceHeights(int steps) {
} }
for (int ci = 0; ci < 256; ci++) stitchEdges(ci); for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createCanyon(float width, float depth, uint32_t seed) { void TerrainEditor::createCanyon(float width, float depth, uint32_t seed) {
recordGeneratorUndo();
if (!terrain_) return; if (!terrain_) return;
float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE; float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE;
@ -1228,10 +1258,12 @@ void TerrainEditor::createCanyon(float width, float depth, uint32_t seed) {
if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); } if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createIsland(float centerHeight, float edgeDropoff) { void TerrainEditor::createIsland(float centerHeight, float edgeDropoff) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
// Island shape: distance from tile center determines height // Island shape: distance from tile center determines height
// Center is high, edges drop below base height (underwater) // Center is high, edges drop below base height (underwater)
@ -1271,11 +1303,13 @@ void TerrainEditor::createIsland(float centerHeight, float edgeDropoff) {
} }
for (int ci = 0; ci < 256; ci++) stitchEdges(ci); for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::createRidge(const glm::vec3& start, const glm::vec3& end, void TerrainEditor::createRidge(const glm::vec3& start, const glm::vec3& end,
float width, float height) { float width, float height) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y); glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y); glm::vec2 lineEnd(end.x, end.y);
glm::vec2 lineDir = glm::normalize(lineEnd - lineStart); glm::vec2 lineDir = glm::normalize(lineEnd - lineStart);
@ -1306,10 +1340,12 @@ void TerrainEditor::createRidge(const glm::vec3& start, const glm::vec3& end,
if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); } if (modified) { stitchEdges(ci); dirtyChunks_.push_back(ci); }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::flattenRoad(const glm::vec3& start, const glm::vec3& end, float width) { void TerrainEditor::flattenRoad(const glm::vec3& start, const glm::vec3& end, float width) {
if (!terrain_) return; if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y); glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y); glm::vec2 lineEnd(end.x, end.y);
glm::vec2 lineDir = glm::normalize(lineEnd - lineStart); glm::vec2 lineDir = glm::normalize(lineEnd - lineStart);
@ -1350,6 +1386,7 @@ void TerrainEditor::flattenRoad(const glm::vec3& start, const glm::vec3& end, fl
} }
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
void TerrainEditor::copyStamp(const glm::vec3& center, float radius) { void TerrainEditor::copyStamp(const glm::vec3& center, float radius) {
@ -1475,6 +1512,7 @@ void TerrainEditor::applyErode(float dt) {
} }
void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, uint32_t seed) { void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, uint32_t seed) {
recordGeneratorUndo();
if (!terrain_) return; if (!terrain_) return;
// Simple value noise with octaves // Simple value noise with octaves
@ -1521,6 +1559,7 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui
dirtyChunks_.push_back(ci); dirtyChunks_.push_back(ci);
} }
dirty_ = true; dirty_ = true;
commitGeneratorUndo();
} }
bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) { bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) {

View file

@ -163,6 +163,9 @@ private:
std::vector<StampVertex> stampData_; std::vector<StampVertex> stampData_;
glm::vec3 stampCenter_{0}; glm::vec3 stampCenter_{0};
void recordGeneratorUndo();
void commitGeneratorUndo();
pipeline::ADTTerrain* terrain_ = nullptr; pipeline::ADTTerrain* terrain_ = nullptr;
EditorBrush brush_; EditorBrush brush_;
EditorHistory history_; EditorHistory history_;