From f870a516dd2d783dd5c6f9bdb394cf60af6681b6 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 14:12:39 -0700 Subject: [PATCH] feat(editor): in-editor "Snap All Spawns to Ground" menu Adds EditorApp::snapAllSpawnsToGround() and a menu item under Generate, mirroring the --snap-zone-to-ground CLI for users who want the action without context-switching to a terminal. Walks every NPC + object, casts a downward ray from baseZ+500y, and writes the hit Z into spawn.position.z. Patrol waypoints on each NPC are snapped too. Marks the project dirty + auto-save pending so the change persists without a manual Ctrl+S. Existing per-selection "Snap Ground" button on the right panel is unchanged; this is the bulk version for "I just edited terrain under a populated area." --- tools/editor/editor_app.cpp | 38 +++++++++++++++++++++++++++++++++++++ tools/editor/editor_app.hpp | 4 ++++ tools/editor/editor_ui.cpp | 8 ++++++++ 3 files changed, 50 insertions(+) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index dacc143b..af1208b1 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -1792,6 +1792,44 @@ void EditorApp::randomPopulateZone(int creatureCount, int objectCount, " creatures + " + std::to_string(placedObjects) + " objects"); } +void EditorApp::snapAllSpawnsToGround() { + if (!terrain_.isLoaded()) { + showToast("Load a tile first"); + return; + } + auto castDown = [&](const glm::vec3& pos, glm::vec3& hit) { + rendering::Ray ray; + ray.origin = pos + glm::vec3(0, 0, 500); + ray.direction = glm::vec3(0, 0, -1); + return terrainEditor_.raycastTerrain(ray, hit); + }; + int snappedC = 0, snappedO = 0; + for (auto& s : npcSpawner_.getSpawns()) { + glm::vec3 hit; + if (castDown(s.position, hit)) { + s.position.z = hit.z; + snappedC++; + } + for (auto& wp : s.patrolPath) { + if (castDown(wp.position, hit)) wp.position.z = hit.z; + } + } + for (auto& o : objectPlacer_.getObjects()) { + glm::vec3 hit; + if (castDown(o.position, hit)) { + o.position.z = hit.z; + snappedO++; + } + } + if (snappedC > 0 || snappedO > 0) { + objectsDirty_ = true; + autoSavePendingChanges_ = true; + viewport_.updateNpcMarkers(npcSpawner_.getSpawns()); + } + showToast("Snapped " + std::to_string(snappedC) + " creature(s) + " + + std::to_string(snappedO) + " object(s) to ground"); +} + void EditorApp::clearAllObjects() { vkDeviceWaitIdle(window_->getVkContext()->getDevice()); objectPlacer_.clearAll(); diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index 5d0c573c..fc12a82b 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -102,6 +102,10 @@ public: // tile's world bbox. Mirrors the --random-populate-zone CLI // command so users can do bulk population from inside the editor. void randomPopulateZone(int creatureCount, int objectCount, uint32_t seed); + // Re-snap every creature + object Z to actual terrain. In-editor + // mirror of the --snap-zone-to-ground CLI; useful after terrain + // edits or random population to fix floating/buried spawns. + void snapAllSpawnsToGround(); void centerOnTerrain(); // Multi-tile support diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index c8abfa21..311be3df 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -395,6 +395,14 @@ void EditorUI::renderMenuBar(EditorApp& app) { "Same seed → same population (reproducible)"); ImGui::EndMenu(); } + if (ImGui::MenuItem("Snap All Spawns to Ground", nullptr, false, + app.hasTerrainLoaded())) { + app.snapAllSpawnsToGround(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Re-snap every creature + object's Z to actual terrain height.\n" + "Run after terrain edits to fix floating/buried spawns."); if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) { if (app.getObjectPlacer().objectCount() > 0 || app.getNpcSpawner().spawnCount() > 0) app.clearAllObjects();