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)
This commit is contained in:
Kelsi 2026-05-05 04:34:03 -07:00
parent cc6a72e7b2
commit f5fe9a0101
5 changed files with 93 additions and 0 deletions

View file

@ -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<int>(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<int>(recent.size())) ImGui::SameLine();
}
}
}
ImGui::End();
}

View file

@ -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<float>(terrain_->coord.y)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(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<uint16_t>(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<float>(terrain_->coord.y)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(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<uint16_t>(bit);
}
}
}
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), ci) == dirtyChunks_.end())
dirtyChunks_.push_back(ci);
dirty_ = true;
}
}
} // namespace editor
} // namespace wowee

View file

@ -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; }

View file

@ -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) {

View file

@ -15,6 +15,7 @@ public:
void setActiveTexture(const std::string& texturePath);
const std::string& getActiveTexture() const { return activeTexture_; }
const std::vector<std::string>& 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<std::string> recentTextures_;
static constexpr float TILE_SIZE = 533.33333f;
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.0f;