From 0e2b55f9fd6d0ad1ac2749ab7b001217bd62d0d6 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 09:34:17 -0700 Subject: [PATCH] fix(editor): crater button now click-to-place on terrain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Create Crater at Cursor" button used the brush's last hover position, which was stale by the time the user clicked the button — the cursor had to move onto the button itself, dragging the brush position along with it. Result: crater spawned somewhere between where the user wanted and the button. New flow: button arms a "click on terrain to place crater" mode (captures the user's chosen radius/depth/rim at arm time). The next left-click anywhere on terrain spawns the crater at the actual click position, regardless of brush state. Button label changes to "ARMED — click on terrain to place" while waiting, with a Cancel button + Esc to dismiss. Misses on terrain leave the mode armed so the user can retry without re-pressing the button. --- tools/editor/editor_app.cpp | 29 +++++++++++++++++++++++++++++ tools/editor/editor_app.hpp | 16 ++++++++++++++++ tools/editor/editor_ui.cpp | 23 +++++++++++++++++++---- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 57ad1a49..ffbf2537 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -351,6 +351,10 @@ void EditorApp::processEvents() { objectPlacer_.clearSelection(); npcSpawner_.clearSelection(); ui_.clearPath(); + if (pendingCrater_.active) { + pendingCrater_.active = false; + showToast("Crater placement cancelled"); + } } } if (sc == SDL_SCANCODE_DELETE) { @@ -531,6 +535,31 @@ void EditorApp::processEvents() { giz.endDrag(); giz.setMode(TransformMode::None); } else if (event.type == SDL_MOUSEBUTTONDOWN) { + // Pending crater placement: take precedence over the + // mode-based click handling below so the next click + // anywhere on terrain spawns the crater instead of + // doing whatever the current mode does. + if (pendingCrater_.active) { + 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)) { + terrainEditor_.createCrater(hitPos, + pendingCrater_.radius, + pendingCrater_.depth, + pendingCrater_.rim); + showToast("Crater placed"); + pendingCrater_.active = false; + } + // If the click missed terrain, leave the mode armed + // so the user can try again without re-pressing the + // button. + continue; + } // Path point capture (river/road tool) // Alt+click eyedropper in paint mode if (mode_ == EditorMode::Paint && (SDL_GetModState() & KMOD_ALT)) { diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index 7adf7476..be4432fb 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -191,6 +191,22 @@ private: // reads this so subsequent generations honor the active biome instead of // reapplying the same hardcoded heightband textures every time. Biome activeBiome_ = Biome::Grassland; + + // "Click on terrain to place crater" mode. The Crater button arms this + // with the user's chosen radius/depth/rim; the next left-click on + // terrain consumes it and creates the crater at the actual click + // position. Solves the UX problem where pressing the button used a + // stale brush position because the cursor was on the button itself. +public: + struct PendingCrater { + bool active = false; + float radius = 30.0f; + float depth = 10.0f; + float rim = 3.0f; + }; + PendingCrater& pendingCrater() { return pendingCrater_; } +private: + PendingCrater pendingCrater_; }; } // namespace editor diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index eecd9c20..9486839f 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -1236,10 +1236,25 @@ void EditorUI::renderBrushPanel(EditorApp& app) { ImGui::SliderFloat("Radius##crater", &craterRadius, 5.0f, 100.0f); ImGui::SliderFloat("Depth##crater", &craterDepth, 2.0f, 50.0f); ImGui::SliderFloat("Rim Height##crater", &craterRim, 0.0f, 15.0f); - auto& brush5 = app.getTerrainEditor().brush(); - if (ImGui::Button("Create Crater at Cursor", ImVec2(-1, 0)) ) { - app.getTerrainEditor().createCrater(brush5.getPosition(), craterRadius, craterDepth, craterRim); - app.showToast("Crater created"); + // Click-to-place: arm pendingCrater so the next left-click on + // terrain spawns the crater there, not at the (stale) brush + // position that was last set before the cursor moved onto the + // button. + auto& pc = app.pendingCrater(); + if (pc.active) { + ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.3f, 1), + "ARMED — click on terrain to place"); + if (ImGui::Button("Cancel##crater", ImVec2(-1, 0))) { + pc.active = false; + } + } else { + if (ImGui::Button("Click on terrain to place crater", ImVec2(-1, 0))) { + pc.active = true; + pc.radius = craterRadius; + pc.depth = craterDepth; + pc.rim = craterRim; + app.showToast("Click on terrain to place crater (Esc to cancel)"); + } } ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), "Bowl with raised rim. Fill with water for a lake."); }