From 6b3cdd325a789aa09cb38a1f50dea753e11bb132 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 13:00:23 -0700 Subject: [PATCH] feat(editor): click-to-place path points for river/road carver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - River/Road tool now uses click-capture mode instead of button-based cursor position — click terrain directly to set start and end points - 3-step flow: Click Start → Click End → Apply Path (with preview text) - Cancel button available at each step - Path state tracked on EditorUI with setPathPoint()/clearPath() - Intercepts terrain clicks before mode-specific handling when active --- tools/editor/editor_app.cpp | 19 +++++++- tools/editor/editor_ui.cpp | 92 ++++++++++++++++++++++--------------- tools/editor/editor_ui.hpp | 17 +++++++ 3 files changed, 91 insertions(+), 37 deletions(-) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 08ef1926..c37fdb47 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -360,8 +360,25 @@ void EditorApp::processEvents() { giz.endDrag(); giz.setMode(TransformMode::None); } else if (event.type == SDL_MOUSEBUTTONDOWN) { + // Path point capture (river/road tool) + if (ui_.getPathCapture() != EditorUI::PathCapture::None) { + auto ext = window_->getVkContext()->getSwapchainExtent(); + rendering::Ray ray = camera_.getCamera().screenToWorldRay( + static_cast(event.button.x), + static_cast(event.button.y), + static_cast(ext.width), + static_cast(ext.height)); + glm::vec3 hitPos; + if (terrainEditor_.raycastTerrain(ray, hitPos)) { + ui_.setPathPoint(hitPos); + if (ui_.getPathCapture() == EditorUI::PathCapture::None && ui_.isPathReady()) + showToast("Both points set — click Apply Path"); + else if (ui_.getPathCapture() == EditorUI::PathCapture::WaitingEnd) + showToast("Start point set — click terrain for end"); + } + } // Ctrl+click = select object (any mode) - if ((event.key.keysym.mod & KMOD_CTRL) || (SDL_GetModState() & KMOD_CTRL)) { + else if ((event.key.keysym.mod & KMOD_CTRL) || (SDL_GetModState() & KMOD_CTRL)) { auto ext = window_->getVkContext()->getSwapchainExtent(); rendering::Ray ray = camera_.getCamera().screenToWorldRay( static_cast(event.button.x), diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index b8366633..d9f2a7b2 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -130,6 +130,18 @@ void EditorUI::processActions(EditorApp& app) { } } +void EditorUI::setPathPoint(const glm::vec3& pos) { + if (pathCapture_ == PathCapture::WaitingStart) { + pathStart_ = pos; + pathStartSet_ = true; + pathCapture_ = PathCapture::WaitingEnd; + } else if (pathCapture_ == PathCapture::WaitingEnd) { + pathEnd_ = pos; + pathEndSet_ = true; + pathCapture_ = PathCapture::None; + } +} + void EditorUI::renderMenuBar(EditorApp& app) { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { @@ -770,45 +782,53 @@ void EditorUI::renderBrushPanel(EditorApp& app) { ImGui::Separator(); if (ImGui::CollapsingHeader("River / Road Carver")) { - static glm::vec3 pathStart{0}, pathEnd{0}; - static float pathWidth = 8.0f, pathDepth = 5.0f; - static bool pathStartSet = false; - static int pathMode = 0; // 0=river, 1=road - ImGui::RadioButton("River (carve down)", &pathMode, 0); + ImGui::RadioButton("River (carve down)", &pathMode_, 0); ImGui::SameLine(); - ImGui::RadioButton("Road (flatten)", &pathMode, 1); - ImGui::SliderFloat("Width##path", &pathWidth, 2.0f, 50.0f); - if (pathMode == 0) ImGui::SliderFloat("Depth##path", &pathDepth, 1.0f, 30.0f); - auto& brush4 = app.getTerrainEditor().brush(); - auto brushPos = brush4.getPosition(); - ImGui::Text("Cursor: %.0f, %.0f, %.0f %s", - brushPos.x, brushPos.y, brushPos.z, - brush4.isActive() ? "" : "(off terrain)"); - if (ImGui::Button("Set Start##path", ImVec2(120, 0))) { - pathStart = brushPos; - pathStartSet = true; - app.showToast("Path start set"); - } - ImGui::SameLine(); - if (ImGui::Button("Set End + Apply##path", ImVec2(140, 0)) && pathStartSet) { - pathEnd = brushPos; - if (pathMode == 0) { - app.getTerrainEditor().carveRiver(pathStart, pathEnd, pathWidth, pathDepth); - app.getTexturePainter().paintAlongPath(pathStart, pathEnd, pathWidth * 1.5f, - "Tileset\\Ashenvale\\AshenvaleSand.blp"); - app.showToast("River carved + banks textured"); - } else { - app.getTerrainEditor().flattenRoad(pathStart, pathEnd, pathWidth); - app.getTexturePainter().paintAlongPath(pathStart, pathEnd, pathWidth, - "Tileset\\Elwynn\\ElwynnCobblestoneBase.blp"); - app.showToast("Road flattened + textured"); + ImGui::RadioButton("Road (flatten)", &pathMode_, 1); + ImGui::SliderFloat("Width##path", &pathWidth_, 2.0f, 50.0f); + if (pathMode_ == 0) ImGui::SliderFloat("Depth##path", &pathDepth_, 1.0f, 30.0f); + + if (pathCapture_ == PathCapture::None && !pathStartSet_) { + if (ImGui::Button("Click Start Point", ImVec2(-1, 0))) { + pathCapture_ = PathCapture::WaitingStart; + pathStartSet_ = false; + pathEndSet_ = false; + app.showToast("Click terrain to set start point"); } - pathStartSet = false; + } else if (pathCapture_ == PathCapture::WaitingStart) { + ImGui::TextColored(ImVec4(1, 1, 0.3f, 1), "Click terrain for START point..."); + if (ImGui::SmallButton("Cancel##path")) { + pathCapture_ = PathCapture::None; + } + } else if (pathCapture_ == PathCapture::WaitingEnd) { + ImGui::TextColored(ImVec4(0.3f, 1, 0.3f, 1), "Start set at (%.0f, %.0f) — click for END", + pathStart_.x, pathStart_.y); + if (ImGui::SmallButton("Cancel##path")) { + clearPath(); + } + } else if (pathStartSet_ && pathEndSet_) { + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1), + "Start: (%.0f,%.0f) End: (%.0f,%.0f)", pathStart_.x, pathStart_.y, pathEnd_.x, pathEnd_.y); + if (ImGui::Button("Apply Path", ImVec2(-1, 0))) { + if (pathMode_ == 0) { + app.getTerrainEditor().carveRiver(pathStart_, pathEnd_, pathWidth_, pathDepth_); + app.getTexturePainter().paintAlongPath(pathStart_, pathEnd_, pathWidth_ * 1.5f, + "Tileset\\Ashenvale\\AshenvaleSand.blp"); + app.showToast("River carved + banks textured"); + } else { + app.getTerrainEditor().flattenRoad(pathStart_, pathEnd_, pathWidth_); + app.getTexturePainter().paintAlongPath(pathStart_, pathEnd_, pathWidth_, + "Tileset\\Elwynn\\ElwynnCobblestoneBase.blp"); + app.showToast("Road flattened + textured"); + } + clearPath(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Reset##path")) clearPath(); + } else if (pathStartSet_) { + if (ImGui::Button("Click End Point", ImVec2(-1, 0))) + pathCapture_ = PathCapture::WaitingEnd; } - if (pathStartSet) - 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 apply"); } if (ImGui::CollapsingHeader("Mirror / Rotate")) { diff --git a/tools/editor/editor_ui.hpp b/tools/editor/editor_ui.hpp index 873bf232..06cf4dad 100644 --- a/tools/editor/editor_ui.hpp +++ b/tools/editor/editor_ui.hpp @@ -1,6 +1,7 @@ #pragma once #include "terrain_biomes.hpp" +#include #include #include @@ -22,6 +23,15 @@ public: PaintMode getPaintMode() const { return paintMode_; } + // Path point capture: when active, next terrain click sets the point + enum class PathCapture { None, WaitingStart, WaitingEnd }; + PathCapture getPathCapture() const { return pathCapture_; } + void setPathPoint(const glm::vec3& pos); + glm::vec3 getPathStart() const { return pathStart_; } + glm::vec3 getPathEnd() const { return pathEnd_; } + bool isPathReady() const { return pathStartSet_ && pathEndSet_; } + void clearPath() { pathStartSet_ = false; pathEndSet_ = false; pathCapture_ = PathCapture::None; } + private: void renderMenuBar(EditorApp& app); void renderToolbar(EditorApp& app); @@ -71,6 +81,13 @@ private: int objDirIdx_ = -1; bool showM2s_ = true; bool showWMOs_ = true; + + // Path point capture + PathCapture pathCapture_ = PathCapture::None; + glm::vec3 pathStart_{0}, pathEnd_{0}; + bool pathStartSet_ = false, pathEndSet_ = false; + int pathMode_ = 0; // 0=river, 1=road + float pathWidth_ = 8.0f, pathDepth_ = 5.0f; }; } // namespace editor