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);
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();
VkRenderPassBeginInfo rpInfo{};
@ -212,8 +236,12 @@ void EditorApp::processEvents() {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
window_->setShouldClose(true);
return;
if (terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) {
showQuitConfirm_ = true;
} else {
window_->setShouldClose(true);
return;
}
}
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) {
terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome);
// Clear previous state
// Clear all previous state
clearAllObjects();
questEditor_.clear();
ui_.clearPath();
terrainEditor_.setTerrain(&terrain_);
terrainEditor_.history().clear();

View file

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

View file

@ -244,6 +244,20 @@ void TerrainEditor::endStroke() {
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) {
if (!terrain_ || !brush_.isActive()) return;
@ -727,6 +741,7 @@ void TerrainEditor::mirrorY() {
void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end,
float width, float depth) {
if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y);
glm::vec2 lineDir = glm::normalize(lineEnd - lineStart);
@ -762,10 +777,12 @@ void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end,
}
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createCrater(const glm::vec3& center, float radius, float depth, float rimHeight) {
if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci];
@ -803,11 +820,12 @@ void TerrainEditor::createCrater(const glm::vec3& center, float radius, float de
}
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createMesa(const glm::vec3& center, float radius, float height, float edgeSteepness) {
if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue;
@ -838,10 +856,12 @@ void TerrainEditor::createMesa(const glm::vec3& center, float radius, float heig
}
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createHill(const glm::vec3& center, float radius, float height) {
if (!terrain_) return;
recordGeneratorUndo();
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci];
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); }
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t seed) {
if (!terrain_) return;
recordGeneratorUndo();
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;
@ -898,10 +920,12 @@ void TerrainEditor::applyVoronoiNoise(int cellCount, float amplitude, uint32_t s
}
for (int ci = 0; ci < 256; ci++) stitchEdges(ci);
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createDunes(float wavelength, float amplitude, float direction, uint32_t seed) {
if (!terrain_) return;
recordGeneratorUndo();
float dirRad = direction * 3.14159f / 180.0f;
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);
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::rotateTerrain90() {
@ -1061,6 +1086,7 @@ void TerrainEditor::smoothBeaches(float waterHeight, float beachWidth) {
}
void TerrainEditor::addDetailNoise(float amplitude, float frequency, uint32_t seed) {
recordGeneratorUndo();
if (!terrain_) return;
auto hash2d = [](int x, int y, uint32_t s) -> float {
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);
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::thermalErosion(int iterations, float talusAngle) {
recordGeneratorUndo();
if (!terrain_) return;
float unitSize = CHUNK_SIZE / 8.0f;
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);
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createCanyon(float width, float depth, uint32_t seed) {
recordGeneratorUndo();
if (!terrain_) return;
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); }
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createIsland(float centerHeight, float edgeDropoff) {
if (!terrain_) return;
recordGeneratorUndo();
// Island shape: distance from tile center determines height
// 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);
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::createRidge(const glm::vec3& start, const glm::vec3& end,
float width, float height) {
if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y);
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); }
}
dirty_ = true;
commitGeneratorUndo();
}
void TerrainEditor::flattenRoad(const glm::vec3& start, const glm::vec3& end, float width) {
if (!terrain_) return;
recordGeneratorUndo();
glm::vec2 lineStart(start.x, start.y);
glm::vec2 lineEnd(end.x, end.y);
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;
commitGeneratorUndo();
}
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) {
recordGeneratorUndo();
if (!terrain_) return;
// Simple value noise with octaves
@ -1521,6 +1559,7 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui
dirtyChunks_.push_back(ci);
}
dirty_ = true;
commitGeneratorUndo();
}
bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) {

View file

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