diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index c726b60a..b7163630 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,8 @@ void EditorApp::run() { // Handle pending UI actions ui_.processActions(*this); + updateToasts(dt); + // Auto-save if (autoSaveEnabled_ && terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) { autoSaveTimer_ += dt; @@ -593,6 +596,7 @@ void EditorApp::exportZone(const std::string& outputDir) { } lastSavePath_ = outputDir; + showToast("Zone exported to " + base); LOG_INFO("Zone exported to: ", base); } @@ -606,6 +610,16 @@ void EditorApp::requestQuit() { window_->setShouldClose(true); } +void EditorApp::showToast(const std::string& msg, float duration) { + toasts_.push_back({msg, duration}); +} + +void EditorApp::updateToasts(float dt) { + for (auto& t : toasts_) t.timer -= dt; + toasts_.erase(std::remove_if(toasts_.begin(), toasts_.end(), + [](const Toast& t) { return t.timer <= 0; }), toasts_.end()); +} + void EditorApp::setSkyPreset(int preset) { switch (preset) { case 0: // Day diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index 5db202b8..f4ada241 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -117,8 +117,17 @@ private: std::string lastSavePath_; std::vector bookmarks_; float autoSaveTimer_ = 0.0f; - float autoSaveInterval_ = 300.0f; // 5 minutes + float autoSaveInterval_ = 300.0f; bool autoSaveEnabled_ = true; + + // Toast notifications + struct Toast { std::string msg; float timer; }; + std::vector toasts_; +public: + void showToast(const std::string& msg, float duration = 3.0f); + const std::vector& getToasts() const { return toasts_; } + void updateToasts(float dt); +private: size_t lastObjectCount_ = 0; EditorMode mode_ = EditorMode::Sculpt; float waterHeight_ = 100.0f; diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index e2a9a241..49dc937a 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -48,6 +48,25 @@ void EditorUI::render(EditorApp& app) { renderMinimap(app); renderPropertiesPanel(app); renderStatusBar(app); + + // Toast notifications + ImGuiViewport* tvp = ImGui::GetMainViewport(); + float toastY = tvp->Size.y - 60; + for (const auto& t : app.getToasts()) { + float alpha = std::min(1.0f, t.timer); + ImGui::SetNextWindowPos(ImVec2(tvp->Size.x / 2 - 150, toastY)); + ImGui::SetNextWindowSize(ImVec2(300, 30)); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.4f, 0.1f, 0.9f)); + char toastId[32]; std::snprintf(toastId, sizeof(toastId), "##toast%p", (void*)&t); + ImGui::Begin(toastId, nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); + ImGui::Text("%s", t.msg.c_str()); + ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + toastY -= 35; + } } void EditorUI::processActions(EditorApp& app) { @@ -69,6 +88,20 @@ void EditorUI::renderMenuBar(EditorApp& app) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true; if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = true; + if (ImGui::BeginMenu("Import Heightmap", app.hasTerrainLoaded())) { + static char hmPath[256] = "heightmap.raw"; + static float hmScale = 200.0f; + ImGui::InputText("File##hm", hmPath, sizeof(hmPath)); + ImGui::SliderFloat("Height Scale", &hmScale, 10.0f, 1000.0f); + ImGui::TextColored(ImVec4(0.6f,0.6f,0.6f,1), "RAW 16-bit or 8-bit (129x129 or 257x257)"); + if (ImGui::MenuItem("Import")) { + if (app.getTerrainEditor().importHeightmap(hmPath, hmScale)) + app.showToast("Heightmap imported"); + else + app.showToast("Failed to import heightmap"); + } + ImGui::EndMenu(); + } if (ImGui::MenuItem("Clear All", nullptr, false, app.hasTerrainLoaded())) { app.getTerrainEditor().history().clear(); app.getObjectPlacer().clearSelection(); diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index 9c8b780a..d9dd2aa0 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -2,6 +2,7 @@ #include "core/logger.hpp" #include #include +#include #include #include @@ -682,6 +683,65 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui dirty_ = true; } +bool TerrainEditor::importHeightmap(const std::string& path, float heightScale) { + if (!terrain_) return false; + + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) { return false; } + auto fileSize = f.tellg(); + f.seekg(0); + + // Determine resolution from file size + // 129x129 x 2 bytes = 33282 (one chunk row+1 per tile row+1) + // 257x257 x 2 bytes = 132098 (2 samples per chunk quad) + int res = 0; + if (fileSize >= 132098) res = 257; + else if (fileSize >= 33282) res = 129; + else if (fileSize >= 16641) { res = 129; } // 8-bit 129x129 + else return false; + + bool is16bit = (fileSize >= res * res * 2); + std::vector heightData(res * res); + + if (is16bit) { + std::vector raw(res * res); + f.read(reinterpret_cast(raw.data()), res * res * 2); + for (int i = 0; i < res * res; i++) + heightData[i] = static_cast(raw[i]) / 65535.0f; + } else { + std::vector raw(res * res); + f.read(reinterpret_cast(raw.data()), res * res); + for (int i = 0; i < res * res; i++) + heightData[i] = static_cast(raw[i]) / 255.0f; + } + + // Map heightmap pixels to terrain vertices + for (int cy = 0; cy < 16; cy++) { + for (int cx = 0; cx < 16; cx++) { + auto& chunk = terrain_->chunks[cy * 16 + cx]; + if (!chunk.hasHeightMap()) continue; + + for (int v = 0; v < 145; v++) { + int row = v / 17, col = v % 17; + float offX = static_cast(col); + float offY = static_cast(row); + if (col > 8) { offY += 0.5f; offX -= 8.5f; } + + // Map to pixel coords + float px = (cx * 8.0f + offX) / 128.0f * (res - 1); + float py = (cy * 8.0f + offY) / 128.0f * (res - 1); + int ix = std::clamp(static_cast(px), 0, res - 1); + int iy = std::clamp(static_cast(py), 0, res - 1); + + chunk.heightMap.heights[v] = heightData[iy * res + ix] * heightScale; + } + dirtyChunks_.push_back(cy * 16 + cx); + } + } + dirty_ = true; + return true; +} + void TerrainEditor::punchHole(const glm::vec3& center, float radius) { if (!terrain_) return; auto affected = getAffectedChunks(center, radius); diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index bf95ac0d..c65d8894 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -54,6 +54,9 @@ public: // Noise generator: applies procedural height noise to the terrain void applyNoise(float frequency, float amplitude, int octaves, uint32_t seed); + // Import heightmap from raw 16-bit grayscale (129x129 or 257x257) + bool importHeightmap(const std::string& path, float heightScale); + // Water editing void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0); void removeWater(const glm::vec3& center, float radius);