diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index b519af81..dfcaee59 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -459,6 +459,31 @@ void EditorUI::renderBrushPanel(EditorApp& app) { } ImGui::Separator(); + if (ImGui::CollapsingHeader("River / Path Carver")) { + static glm::vec3 riverStart{0}, riverEnd{0}; + static float riverWidth = 8.0f, riverDepth = 5.0f; + static bool riverStartSet = false; + ImGui::SliderFloat("Width##river", &riverWidth, 2.0f, 50.0f); + ImGui::SliderFloat("Depth##river", &riverDepth, 1.0f, 30.0f); + auto& brush4 = app.getTerrainEditor().brush(); + if (ImGui::Button("Set Start##river", ImVec2(120, 0)) && brush4.isActive()) { + riverStart = brush4.getPosition(); + riverStartSet = true; + app.showToast("River start set"); + } + ImGui::SameLine(); + if (ImGui::Button("Set End + Carve##river", ImVec2(140, 0)) && brush4.isActive() && riverStartSet) { + riverEnd = brush4.getPosition(); + app.getTerrainEditor().carveRiver(riverStart, riverEnd, riverWidth, riverDepth); + app.showToast("River carved"); + riverStartSet = false; + } + if (riverStartSet) + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1), "Start set — click end point"); + else + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), "Set start then end to carve"); + } + if (ImGui::CollapsingHeader("Mirror Terrain")) { if (ImGui::Button("Mirror X (Left<>Right)", ImVec2(-1, 0))) { app.getTerrainEditor().mirrorX(); diff --git a/tools/editor/terrain_editor.cpp b/tools/editor/terrain_editor.cpp index 696f8127..6818bd66 100644 --- a/tools/editor/terrain_editor.cpp +++ b/tools/editor/terrain_editor.cpp @@ -712,6 +712,46 @@ void TerrainEditor::mirrorY() { dirty_ = true; } +void TerrainEditor::carveRiver(const glm::vec3& start, const glm::vec3& end, + float width, float depth) { + if (!terrain_) return; + glm::vec2 lineStart(start.x, start.y); + glm::vec2 lineEnd(end.x, end.y); + glm::vec2 lineDir = glm::normalize(lineEnd - lineStart); + float lineLen = glm::length(lineEnd - lineStart); + + 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 pos = chunkVertexWorldPos(ci, v); + glm::vec2 p(pos.x, pos.y); + + // Project point onto line segment + glm::vec2 toP = p - lineStart; + float t = glm::dot(toP, lineDir); + t = std::clamp(t, 0.0f, lineLen); + glm::vec2 closest = lineStart + lineDir * t; + float dist = glm::length(p - closest); + + if (dist < width) { + float falloff = 1.0f - (dist / width); + falloff = falloff * falloff; // smooth edges + float carve = depth * falloff; + chunk.heightMap.heights[v] -= carve; + modified = true; + } + } + if (modified) { + stitchEdges(ci); + dirtyChunks_.push_back(ci); + } + } + dirty_ = true; +} + void TerrainEditor::copyStamp(const glm::vec3& center, float radius) { if (!terrain_) return; stampData_.clear(); diff --git a/tools/editor/terrain_editor.hpp b/tools/editor/terrain_editor.hpp index 6dae6a6f..9408fdf1 100644 --- a/tools/editor/terrain_editor.hpp +++ b/tools/editor/terrain_editor.hpp @@ -72,6 +72,9 @@ public: void mirrorX(); void mirrorY(); + // Carve a river/path between two points (lowers terrain along line) + void carveRiver(const glm::vec3& start, const glm::vec3& end, float width, float depth); + // Import/export heightmap (raw 16-bit grayscale, 129x129) bool importHeightmap(const std::string& path, float heightScale); bool exportHeightmap(const std::string& path, float heightScale);