From 5daa359e747614ad5cb4a2f7c0d77c42cd7b83ca Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 04:20:26 -0700 Subject: [PATCH] feat(editor): object scatter, camera bookmarks, shortcut hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Object scatter tool: place N copies of selected M2/WMO in a radius with random rotation and scale range (Min/Max Scale slider) - Camera bookmarks: F5 saves current position, View > Load Bookmark to jump back — useful for working on different parts of a large zone - Shortcut hints shown at bottom of Object panel (G=move, R=rotate, T=scale, Del=remove) - DragFloatRange2 for min/max scale in scatter UI --- tools/editor/editor_app.cpp | 14 +++++++++++++ tools/editor/editor_app.hpp | 7 +++++++ tools/editor/editor_ui.cpp | 36 ++++++++++++++++++++++++++++++++++ tools/editor/object_placer.cpp | 29 +++++++++++++++++++++++++++ tools/editor/object_placer.hpp | 4 ++++ 5 files changed, 90 insertions(+) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index d03980c7..ffdcf6f4 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -189,6 +189,7 @@ void EditorApp::processEvents() { if (event.type == SDL_KEYDOWN) { auto sc = event.key.keysym.scancode; if (sc == SDL_SCANCODE_F3) setWireframe(!isWireframe()); + if (sc == SDL_SCANCODE_F5) saveBookmark(""); // Transform shortcuts (Blender-style) if (objectPlacer_.getSelected()) { if (sc == SDL_SCANCODE_G) startGizmoMode(TransformMode::Move); @@ -603,6 +604,19 @@ void EditorApp::setGizmoAxis(TransformAxis axis) { viewport_.getGizmo().setTarget(sel->position, sel->scale); } +void EditorApp::saveBookmark(const std::string& name) { + CameraBookmark bm; + bm.pos = camera_.getCamera().getPosition(); + bm.yaw = 0; bm.pitch = 0; // EditorCamera doesn't expose these directly + bm.name = name.empty() ? ("Bookmark " + std::to_string(bookmarks_.size() + 1)) : name; + bookmarks_.push_back(bm); +} + +void EditorApp::loadBookmark(int index) { + if (index < 0 || index >= static_cast(bookmarks_.size())) return; + camera_.setPosition(bookmarks_[index].pos); +} + void EditorApp::addAdjacentTile(int offsetX, int offsetY) { if (!terrain_.isLoaded()) return; int newX = loadedTileX_ + offsetX; diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index 0804131c..71360251 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -67,6 +67,12 @@ public: // Multi-tile support void addAdjacentTile(int offsetX, int offsetY); + + // Camera bookmarks + struct CameraBookmark { glm::vec3 pos; float yaw; float pitch; std::string name; }; + void saveBookmark(const std::string& name); + void loadBookmark(int index); + const std::vector& getBookmarks() const { return bookmarks_; } TransformGizmo& getGizmo() { return viewport_.getGizmo(); } bool shouldOpenContextMenu() const { return openContextMenu_; } void clearContextMenuFlag() { openContextMenu_ = false; } @@ -102,6 +108,7 @@ private: bool objectsDirty_ = false; bool openContextMenu_ = false; std::string lastSavePath_; + std::vector bookmarks_; size_t lastObjectCount_ = 0; EditorMode mode_ = EditorMode::Sculpt; float waterHeight_ = 100.0f; diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 32d12195..5903508c 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -94,6 +94,16 @@ void EditorUI::renderMenuBar(EditorApp& app) { bool wf = app.isWireframe(); if (ImGui::MenuItem("Wireframe", "F3", &wf)) app.setWireframe(wf); if (ImGui::MenuItem("Reset Camera")) app.resetCamera(); + ImGui::Separator(); + if (ImGui::MenuItem("Save Bookmark", "F5")) app.saveBookmark(""); + auto& bmarks = app.getBookmarks(); + if (!bmarks.empty() && ImGui::BeginMenu("Load Bookmark")) { + for (int i = 0; i < static_cast(bmarks.size()); i++) { + if (ImGui::MenuItem(bmarks[i].name.c_str())) + app.loadBookmark(i); + } + ImGui::EndMenu(); + } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); @@ -379,6 +389,32 @@ void EditorUI::renderObjectPanel(EditorApp& app) { if (ImGui::Button("Deselect", ImVec2(100, 0))) placer.clearSelection(); } + + ImGui::Separator(); + // Object scatter + if (ImGui::CollapsingHeader("Scatter Objects")) { + static int objScatterCount = 8; + static float objScatterRadius = 60.0f; + static float objMinScale = 0.8f; + static float objMaxScale = 1.5f; + 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); + auto& brush = app.getTerrainEditor().brush(); + if (ImGui::Button("Scatter at Cursor##obj", ImVec2(-1, 0))) { + if (brush.isActive() && !placer.getActivePath().empty()) { + placer.scatter(brush.getPosition(), objScatterRadius, + objScatterCount, objMinScale, objMaxScale); + app.markObjectsDirty(); + } + } + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), + "Scatters selected model with random rotation/scale"); + } + + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Left-click: place | Ctrl+click: select"); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "G: move | R: rotate | T: scale | Del: remove"); } ImGui::End(); } diff --git a/tools/editor/object_placer.cpp b/tools/editor/object_placer.cpp index d7b8d0f6..b9247026 100644 --- a/tools/editor/object_placer.cpp +++ b/tools/editor/object_placer.cpp @@ -2,6 +2,7 @@ #include "core/logger.hpp" #include #include +#include namespace wowee { namespace editor { @@ -95,6 +96,34 @@ void ObjectPlacer::deleteSelected() { selectedIdx_ = -1; } +void ObjectPlacer::scatter(const glm::vec3& center, float radius, int count, + float minScale, float maxScale) { + if (activePath_.empty()) return; + std::mt19937 rng(static_cast(center.x * 100 + center.y * 37 + objects_.size())); + std::uniform_real_distribution distAngle(0.0f, 6.2831853f); + std::uniform_real_distribution distDist(0.0f, 1.0f); + std::uniform_real_distribution distRot(0.0f, 360.0f); + std::uniform_real_distribution distScale(minScale, maxScale); + + for (int i = 0; i < count; i++) { + float angle = distAngle(rng); + float dist = std::sqrt(distDist(rng)) * radius; + glm::vec3 pos = center + glm::vec3(std::cos(angle) * dist, std::sin(angle) * dist, 0.0f); + + PlacedObject obj; + obj.type = activeType_; + obj.path = activePath_; + obj.nameId = 0; + obj.uniqueId = nextUniqueId(); + obj.position = pos; + obj.rotation = glm::vec3(0.0f, distRot(rng), 0.0f); + obj.scale = distScale(rng); + obj.selected = false; + objects_.push_back(obj); + } + LOG_INFO("Scattered ", count, " objects in radius ", radius); +} + void ObjectPlacer::undoLastPlace() { if (undoStack_.empty()) return; int idx = undoStack_.back(); diff --git a/tools/editor/object_placer.hpp b/tools/editor/object_placer.hpp index 45abfef7..6a2c60f7 100644 --- a/tools/editor/object_placer.hpp +++ b/tools/editor/object_placer.hpp @@ -61,6 +61,10 @@ public: bool canUndoPlace() const { return !undoStack_.empty(); } void undoLastPlace(); + // Scatter: place multiple copies with random offset/rotation + void scatter(const glm::vec3& center, float radius, int count, + float minScale, float maxScale); + private: uint32_t nextUniqueId();