From f5fe9a01011f77517c2f6c27d7e6f8ee0b55fec4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 04:34:03 -0700 Subject: [PATCH] feat(editor): terrain holes, recent textures, sculpt panel polish - Punch Hole / Fill Hole buttons in Sculpt panel: creates terrain holes (4x4 bitmask) for cave entrances, mine shafts, etc. Uses brush radius to determine affected area. - Recent Textures: paint panel shows last 6 used textures as quick- select buttons (no need to re-search the full list) - Holes saved in ADT format (MCNK holes field) and respected by the mesh generator (triangles skipped at hole positions) --- tools/editor/editor_ui.cpp | 28 +++++++++++++++++ tools/editor/terrain_editor.cpp | 54 ++++++++++++++++++++++++++++++++ tools/editor/terrain_editor.hpp | 4 +++ tools/editor/texture_painter.cpp | 5 +++ tools/editor/texture_painter.hpp | 2 ++ 5 files changed, 93 insertions(+) diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 442b0a69..92dff1b6 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -230,6 +230,15 @@ void EditorUI::renderBrushPanel(EditorApp& app) { if (ImGui::IsItemHovered()) ImGui::SetTooltip("Set target height from cursor position"); } + ImGui::Separator(); + ImGui::Text("Terrain Holes (cave entrances):"); + auto& brush = app.getTerrainEditor().brush(); + if (ImGui::Button("Punch Hole", ImVec2(120, 0)) && brush.isActive()) + app.getTerrainEditor().punchHole(brush.getPosition(), s.radius); + ImGui::SameLine(); + if (ImGui::Button("Fill Hole", ImVec2(120, 0)) && brush.isActive()) + app.getTerrainEditor().fillHole(brush.getPosition(), s.radius); + ImGui::Separator(); auto& hist = app.getTerrainEditor().history(); ImGui::Text("Undo: %zu Redo: %zu", hist.undoCount(), hist.redoCount()); @@ -304,6 +313,25 @@ void EditorUI::renderTexturePaintPanel(EditorApp& app) { if (!selectedTexture_.empty()) ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Active: %s", selectedTexture_.c_str()); + + // Recent textures + auto& recent = app.getTexturePainter().getRecentTextures(); + if (!recent.empty()) { + ImGui::Separator(); + ImGui::Text("Recent:"); + for (int i = 0; i < static_cast(recent.size()) && i < 6; i++) { + std::string disp = recent[i]; + auto sl = disp.rfind('\\'); + if (sl != std::string::npos) disp = disp.substr(sl + 1); + if (ImGui::SmallButton(disp.c_str())) { + selectedTexture_ = recent[i]; + app.getTexturePainter().setActiveTexture(recent[i]); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", recent[i].c_str()); + if (i < 5 && i + 1 < static_cast(recent.size())) ImGui::SameLine(); + } + } } ImGui::End(); } diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index 00aee8d0..30f587b5 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -586,5 +586,59 @@ void TerrainEditor::removeWater(const glm::vec3& center, float radius) { } } +void TerrainEditor::punchHole(const glm::vec3& center, float radius) { + if (!terrain_) return; + auto affected = getAffectedChunks(center, radius); + for (int ci : affected) { + auto& chunk = terrain_->chunks[ci]; + // Each chunk has 8x8 quads, holes use a 4x4 bitmask (each bit covers 2x2 quads) + for (int hy = 0; hy < 4; hy++) { + for (int hx = 0; hx < 4; hx++) { + // Center of this 2x2 quad group + int cx = ci % 16, cy = ci / 16; + float tileNW_X = (32.0f - static_cast(terrain_->coord.y)) * TILE_SIZE; + float tileNW_Y = (32.0f - static_cast(terrain_->coord.x)) * TILE_SIZE; + float qx = tileNW_X - cy * CHUNK_SIZE - (hy * 2 + 1) * CHUNK_SIZE / 8.0f; + float qy = tileNW_Y - cx * CHUNK_SIZE - (hx * 2 + 1) * CHUNK_SIZE / 8.0f; + float dist = std::sqrt((qx - center.x) * (qx - center.x) + + (qy - center.y) * (qy - center.y)); + if (dist < radius) { + int bit = 1 << (hy * 4 + hx); + chunk.holes |= static_cast(bit); + } + } + } + if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), ci) == dirtyChunks_.end()) + dirtyChunks_.push_back(ci); + dirty_ = true; + } +} + +void TerrainEditor::fillHole(const glm::vec3& center, float radius) { + if (!terrain_) return; + auto affected = getAffectedChunks(center, radius); + for (int ci : affected) { + auto& chunk = terrain_->chunks[ci]; + for (int hy = 0; hy < 4; hy++) { + for (int hx = 0; hx < 4; hx++) { + int cx = ci % 16, cy = ci / 16; + float tileNW_X = (32.0f - static_cast(terrain_->coord.y)) * TILE_SIZE; + float tileNW_Y = (32.0f - static_cast(terrain_->coord.x)) * TILE_SIZE; + float qx = tileNW_X - cy * CHUNK_SIZE - (hy * 2 + 1) * CHUNK_SIZE / 8.0f; + float qy = tileNW_Y - cx * CHUNK_SIZE - (hx * 2 + 1) * CHUNK_SIZE / 8.0f; + float dist = std::sqrt((qx - center.x) * (qx - center.x) + + (qy - center.y) * (qy - center.y)); + if (dist < radius) { + int bit = 1 << (hy * 4 + hx); + chunk.holes &= ~static_cast(bit); + } + } + } + if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), ci) == dirtyChunks_.end()) + dirtyChunks_.push_back(ci); + dirty_ = true; + } +} + } // namespace editor } // namespace wowee diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index d0796e7d..959cd33e 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -55,6 +55,10 @@ public: void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0); void removeWater(const glm::vec3& center, float radius); + // Hole editing (4x4 bitmask per chunk — cave entrances, mine shafts) + void punchHole(const glm::vec3& center, float radius); + void fillHole(const glm::vec3& center, float radius); + bool hasUnsavedChanges() const { return dirty_; } void markSaved() { dirty_ = false; } diff --git a/tools/editor/texture_painter.cpp b/tools/editor/texture_painter.cpp index 55596b9c..c8f24995 100644 --- a/tools/editor/texture_painter.cpp +++ b/tools/editor/texture_painter.cpp @@ -8,6 +8,11 @@ namespace editor { void TexturePainter::setActiveTexture(const std::string& texturePath) { activeTexture_ = texturePath; + // Track recent textures (max 10) + auto it = std::find(recentTextures_.begin(), recentTextures_.end(), texturePath); + if (it != recentTextures_.end()) recentTextures_.erase(it); + recentTextures_.insert(recentTextures_.begin(), texturePath); + if (recentTextures_.size() > 10) recentTextures_.pop_back(); } uint32_t TexturePainter::ensureTextureInList(const std::string& path) { diff --git a/tools/editor/texture_painter.hpp b/tools/editor/texture_painter.hpp index 5d1d7a66..8ab35d0c 100644 --- a/tools/editor/texture_painter.hpp +++ b/tools/editor/texture_painter.hpp @@ -15,6 +15,7 @@ public: void setActiveTexture(const std::string& texturePath); const std::string& getActiveTexture() const { return activeTexture_; } + const std::vector& getRecentTextures() const { return recentTextures_; } // Paint the active texture at the given world position // Returns list of modified chunk indices @@ -33,6 +34,7 @@ private: pipeline::ADTTerrain* terrain_ = nullptr; std::string activeTexture_; + std::vector recentTextures_; static constexpr float TILE_SIZE = 533.33333f; static constexpr float CHUNK_SIZE = TILE_SIZE / 16.0f;