diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 498ee95e..dd2b80fc 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -1298,6 +1298,35 @@ void EditorApp::snapSelectedToGround() { } } +void EditorApp::flattenAroundSelected(float radius) { + auto* sel = objectPlacer_.getSelected(); + if (!sel || !terrain_.isLoaded()) return; + + terrainEditor_.beginGeneratorUndo(); + float targetHeight = sel->position.z; + for (int ci = 0; ci < 256; ci++) { + auto& chunk = terrain_.chunks[ci]; + if (!chunk.hasHeightMap()) continue; + bool modified = false; + for (int v = 0; v < 145; v++) { + glm::vec3 vpos = terrainEditor_.getChunkVertexWorldPos(ci, v); + float dist = glm::length(glm::vec2(vpos.x - sel->position.x, vpos.y - sel->position.y)); + if (dist >= radius) continue; + float t = dist / radius; + float blend = t * t; + float relTarget = targetHeight - chunk.position[2]; + chunk.heightMap.heights[v] = chunk.heightMap.heights[v] * blend + relTarget * (1.0f - blend); + modified = true; + } + if (modified) { + terrainEditor_.stitchChunkEdges(ci); + terrainEditor_.markDirty(ci); + } + } + terrainEditor_.endGeneratorUndo(); + showToast("Flattened terrain around object (r=" + std::to_string(static_cast(radius)) + ")"); +} + void EditorApp::alignSelectedToTerrain() { auto& indices = objectPlacer_.getSelectedIndices(); auto& objects = objectPlacer_.getObjects(); @@ -1323,14 +1352,27 @@ void EditorApp::alignSelectedToTerrain() { int EditorApp::batchConvertAssets(const std::string& dataDir) { namespace fs = std::filesystem; - if (!fs::exists(dataDir)) return 0; - int converted = 0; - for (auto& entry : fs::recursive_directory_iterator(dataDir)) { - if (!entry.is_regular_file()) continue; - std::string ext = entry.path().extension().string(); + + // Collect paths from filesystem or manifest + std::vector assetPaths; + if (fs::exists(dataDir)) { + for (auto& entry : fs::recursive_directory_iterator(dataDir)) { + if (entry.is_regular_file()) + assetPaths.push_back(fs::relative(entry.path(), dataDir).string()); + } + } + if (assetPaths.empty() && assetManager_) { + for (const auto& [path, _] : assetManager_->getManifest().getEntries()) + assetPaths.push_back(path); + LOG_INFO("Batch convert: using manifest (", assetPaths.size(), " entries)"); + } + + for (const auto& relPath : assetPaths) { + std::string ext; + auto dot = relPath.rfind('.'); + if (dot != std::string::npos) ext = relPath.substr(dot); for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); - std::string relPath = fs::relative(entry.path(), dataDir).string(); if (ext == ".m2") { auto wom = pipeline::WoweeModelLoader::fromM2(relPath, assetManager_.get()); diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index 4555e52b..47b6ae8d 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -90,6 +90,7 @@ public: void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night void snapSelectedToGround(); void alignSelectedToTerrain(); + void flattenAroundSelected(float radius = 30.0f); void flyToSelected(); int batchConvertAssets(const std::string& dataDir); void clearAllObjects(); diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index a5908d72..0e1267e0 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -1512,6 +1512,10 @@ void EditorUI::renderObjectPanel(EditorApp& app) { ImGui::SameLine(); if (ImGui::Button("Align Slope", ImVec2(75, 0))) app.alignSelectedToTerrain(); + if (ImGui::Button("Flatten Ground", ImVec2(100, 0))) + app.flattenAroundSelected(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Flatten terrain around object to its height"); if (ImGui::Button("Fly To", ImVec2(55, 0))) app.flyToSelected(); ImGui::SameLine(); @@ -1544,16 +1548,32 @@ void EditorUI::renderObjectPanel(EditorApp& app) { ImGui::SliderInt("Count##objsc", &objScatterCount, 1, 50); ImGui::SliderFloat("Radius##objsc", &objScatterRadius, 10.0f, 300.0f); ImGui::DragFloatRange2("Scale##objsc", &objMinScale, &objMaxScale, 0.05f, 0.1f, 10.0f); + static bool scatterAlign = true; + ImGui::Checkbox("Align to terrain", &scatterAlign); auto& brush = app.getTerrainEditor().brush(); if (ImGui::Button("Scatter at Cursor##obj", ImVec2(-1, 0))) { if (brush.isActive() && !placer.getActivePath().empty()) { + size_t before = placer.objectCount(); placer.scatter(brush.getPosition(), objScatterRadius, objScatterCount, objMinScale, objMaxScale); + if (scatterAlign) { + for (size_t i = before; i < placer.objectCount(); i++) { + auto& obj = placer.getObjects()[i]; + rendering::Ray ray; + ray.origin = obj.position + glm::vec3(0, 0, 500); + ray.direction = glm::vec3(0, 0, -1); + glm::vec3 hitPos; + if (app.getTerrainEditor().raycastTerrain(ray, hitPos)) { + obj.position.z = hitPos.z; + glm::vec3 n = app.getTerrainEditor().sampleTerrainNormal(obj.position); + obj.rotation.x = glm::degrees(std::asin(-n.x)); + obj.rotation.z = glm::degrees(std::asin(n.y)); + } + } + } app.markObjectsDirty(); } } - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), - "Scatters selected model with random rotation/scale"); } ImGui::Separator(); diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index ada50149..945c3e21 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -43,6 +43,11 @@ public: // Get chunks modified since last call (for re-upload) std::vector consumeDirtyChunks(); + void markDirty(int chunkIdx) { dirtyChunks_.push_back(chunkIdx); dirty_ = true; } + void stitchChunkEdges(int chunkIdx) { stitchEdges(chunkIdx); } + glm::vec3 getChunkVertexWorldPos(int ci, int vi) const { return chunkVertexWorldPos(ci, vi); } + void beginGeneratorUndo() { recordGeneratorUndo(); } + void endGeneratorUndo() { commitGeneratorUndo(); } // Regenerate mesh for specific chunks pipeline::TerrainMesh regenerateMesh() const;