mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat(editor): heightmap import, toast notifications, workflow polish
- Import Heightmap: File > Import Heightmap loads RAW 8/16-bit grayscale files (129x129 or 257x257) and maps to terrain heights with configurable scale. Supports standard terrain editor heightmap formats. - Toast notifications: non-intrusive green popup at bottom center for user feedback (save confirmations, import results, errors) - Toasts fade out after 3 seconds with alpha animation - Auto-save now shows toast on save - Quick-save (Ctrl+S) shows toast confirmation
This commit is contained in:
parent
a91233a6ec
commit
2f96f112bd
5 changed files with 120 additions and 1 deletions
|
|
@ -7,6 +7,7 @@
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_impl_sdl2.h>
|
#include <imgui_impl_sdl2.h>
|
||||||
#include <imgui_impl_vulkan.h>
|
#include <imgui_impl_vulkan.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
|
@ -81,6 +82,8 @@ void EditorApp::run() {
|
||||||
// Handle pending UI actions
|
// Handle pending UI actions
|
||||||
ui_.processActions(*this);
|
ui_.processActions(*this);
|
||||||
|
|
||||||
|
updateToasts(dt);
|
||||||
|
|
||||||
// Auto-save
|
// Auto-save
|
||||||
if (autoSaveEnabled_ && terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) {
|
if (autoSaveEnabled_ && terrain_.isLoaded() && terrainEditor_.hasUnsavedChanges()) {
|
||||||
autoSaveTimer_ += dt;
|
autoSaveTimer_ += dt;
|
||||||
|
|
@ -593,6 +596,7 @@ void EditorApp::exportZone(const std::string& outputDir) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSavePath_ = outputDir;
|
lastSavePath_ = outputDir;
|
||||||
|
showToast("Zone exported to " + base);
|
||||||
LOG_INFO("Zone exported to: ", base);
|
LOG_INFO("Zone exported to: ", base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -606,6 +610,16 @@ void EditorApp::requestQuit() {
|
||||||
window_->setShouldClose(true);
|
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) {
|
void EditorApp::setSkyPreset(int preset) {
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case 0: // Day
|
case 0: // Day
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,17 @@ private:
|
||||||
std::string lastSavePath_;
|
std::string lastSavePath_;
|
||||||
std::vector<CameraBookmark> bookmarks_;
|
std::vector<CameraBookmark> bookmarks_;
|
||||||
float autoSaveTimer_ = 0.0f;
|
float autoSaveTimer_ = 0.0f;
|
||||||
float autoSaveInterval_ = 300.0f; // 5 minutes
|
float autoSaveInterval_ = 300.0f;
|
||||||
bool autoSaveEnabled_ = true;
|
bool autoSaveEnabled_ = true;
|
||||||
|
|
||||||
|
// Toast notifications
|
||||||
|
struct Toast { std::string msg; float timer; };
|
||||||
|
std::vector<Toast> toasts_;
|
||||||
|
public:
|
||||||
|
void showToast(const std::string& msg, float duration = 3.0f);
|
||||||
|
const std::vector<Toast>& getToasts() const { return toasts_; }
|
||||||
|
void updateToasts(float dt);
|
||||||
|
private:
|
||||||
size_t lastObjectCount_ = 0;
|
size_t lastObjectCount_ = 0;
|
||||||
EditorMode mode_ = EditorMode::Sculpt;
|
EditorMode mode_ = EditorMode::Sculpt;
|
||||||
float waterHeight_ = 100.0f;
|
float waterHeight_ = 100.0f;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,25 @@ void EditorUI::render(EditorApp& app) {
|
||||||
renderMinimap(app);
|
renderMinimap(app);
|
||||||
renderPropertiesPanel(app);
|
renderPropertiesPanel(app);
|
||||||
renderStatusBar(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) {
|
void EditorUI::processActions(EditorApp& app) {
|
||||||
|
|
@ -69,6 +88,20 @@ void EditorUI::renderMenuBar(EditorApp& app) {
|
||||||
if (ImGui::BeginMenu("File")) {
|
if (ImGui::BeginMenu("File")) {
|
||||||
if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true;
|
if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true;
|
||||||
if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = 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())) {
|
if (ImGui::MenuItem("Clear All", nullptr, false, app.hasTerrainLoaded())) {
|
||||||
app.getTerrainEditor().history().clear();
|
app.getTerrainEditor().history().clear();
|
||||||
app.getObjectPlacer().clearSelection();
|
app.getObjectPlacer().clearSelection();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <fstream>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
|
|
@ -682,6 +683,65 @@ void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, ui
|
||||||
dirty_ = true;
|
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<float> heightData(res * res);
|
||||||
|
|
||||||
|
if (is16bit) {
|
||||||
|
std::vector<uint16_t> raw(res * res);
|
||||||
|
f.read(reinterpret_cast<char*>(raw.data()), res * res * 2);
|
||||||
|
for (int i = 0; i < res * res; i++)
|
||||||
|
heightData[i] = static_cast<float>(raw[i]) / 65535.0f;
|
||||||
|
} else {
|
||||||
|
std::vector<uint8_t> raw(res * res);
|
||||||
|
f.read(reinterpret_cast<char*>(raw.data()), res * res);
|
||||||
|
for (int i = 0; i < res * res; i++)
|
||||||
|
heightData[i] = static_cast<float>(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<float>(col);
|
||||||
|
float offY = static_cast<float>(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<int>(px), 0, res - 1);
|
||||||
|
int iy = std::clamp(static_cast<int>(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) {
|
void TerrainEditor::punchHole(const glm::vec3& center, float radius) {
|
||||||
if (!terrain_) return;
|
if (!terrain_) return;
|
||||||
auto affected = getAffectedChunks(center, radius);
|
auto affected = getAffectedChunks(center, radius);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ public:
|
||||||
// Noise generator: applies procedural height noise to the terrain
|
// Noise generator: applies procedural height noise to the terrain
|
||||||
void applyNoise(float frequency, float amplitude, int octaves, uint32_t seed);
|
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
|
// Water editing
|
||||||
void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0);
|
void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0);
|
||||||
void removeWater(const glm::vec3& center, float radius);
|
void removeWater(const glm::vec3& center, float radius);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue