feat(editor): undo object placement, snap to ground, keyboard shortcuts

- Ctrl+Z in Object/NPC mode undoes last placement (50-deep stack)
- "Snap Ground" button raycasts straight down to place object on terrain
- Useful for objects placed too high or moved off terrain surface
- Undo stack adjusts indices when objects are removed mid-stack
This commit is contained in:
Kelsi 2026-05-05 04:01:06 -07:00
parent ace6173401
commit 88abbfb564
5 changed files with 50 additions and 5 deletions

View file

@ -212,10 +212,17 @@ void EditorApp::processEvents() {
} }
} }
if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) { if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) {
if (event.key.keysym.mod & KMOD_SHIFT) if (mode_ == EditorMode::Sculpt) {
terrainEditor_.redo(); if (event.key.keysym.mod & KMOD_SHIFT)
else terrainEditor_.redo();
terrainEditor_.undo(); else
terrainEditor_.undo();
} else if (mode_ == EditorMode::PlaceObject || mode_ == EditorMode::NPC) {
if (!(event.key.keysym.mod & KMOD_SHIFT) && objectPlacer_.canUndoPlace()) {
objectPlacer_.undoLastPlace();
objectsDirty_ = true;
}
}
} }
} }
if (!io.WantCaptureKeyboard) if (!io.WantCaptureKeyboard)
@ -554,6 +561,21 @@ void EditorApp::setGizmoAxis(TransformAxis axis) {
viewport_.getGizmo().setTarget(sel->position, sel->scale); viewport_.getGizmo().setTarget(sel->position, sel->scale);
} }
void EditorApp::snapSelectedToGround() {
auto* sel = objectPlacer_.getSelected();
if (!sel || !terrain_.isLoaded()) return;
// Cast ray straight down from object position
rendering::Ray ray;
ray.origin = sel->position + glm::vec3(0, 0, 500);
ray.direction = glm::vec3(0, 0, -1);
glm::vec3 hitPos;
if (terrainEditor_.raycastTerrain(ray, hitPos)) {
sel->position.z = hitPos.z;
objectsDirty_ = true;
}
}
void EditorApp::resetCamera() { void EditorApp::resetCamera() {
camera_.setPosition(glm::vec3(0.0f, 0.0f, 300.0f)); camera_.setPosition(glm::vec3(0.0f, 0.0f, 300.0f));
camera_.setYawPitch(0.0f, -30.0f); camera_.setYawPitch(0.0f, -30.0f);

View file

@ -61,6 +61,7 @@ public:
void startGizmoMode(TransformMode mode); void startGizmoMode(TransformMode mode);
void setGizmoAxis(TransformAxis axis); void setGizmoAxis(TransformAxis axis);
void snapSelectedToGround();
TransformGizmo& getGizmo() { return viewport_.getGizmo(); } TransformGizmo& getGizmo() { return viewport_.getGizmo(); }
bool shouldOpenContextMenu() const { return openContextMenu_; } bool shouldOpenContextMenu() const { return openContextMenu_; }
void clearContextMenuFlag() { openContextMenu_ = false; } void clearContextMenuFlag() { openContextMenu_ = false; }

View file

@ -360,8 +360,10 @@ void EditorUI::renderObjectPanel(EditorApp& app) {
if (changed) app.markObjectsDirty(); if (changed) app.markObjectsDirty();
if (ImGui::Button("Delete", ImVec2(100, 0))) placer.deleteSelected(); if (ImGui::Button("Snap Ground", ImVec2(100, 0)))
app.snapSelectedToGround();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Delete", ImVec2(100, 0))) placer.deleteSelected();
if (ImGui::Button("Duplicate", ImVec2(100, 0))) { if (ImGui::Button("Duplicate", ImVec2(100, 0))) {
PlacedObject copy = *sel; PlacedObject copy = *sel;
copy.uniqueId = 0; copy.uniqueId = 0;

View file

@ -29,6 +29,8 @@ void ObjectPlacer::placeObject(const glm::vec3& position) {
obj.selected = false; obj.selected = false;
objects_.push_back(obj); objects_.push_back(obj);
undoStack_.push_back(static_cast<int>(objects_.size() - 1));
if (undoStack_.size() > 50) undoStack_.erase(undoStack_.begin());
LOG_INFO("Placed ", (activeType_ == PlaceableType::M2 ? "M2" : "WMO"), LOG_INFO("Placed ", (activeType_ == PlaceableType::M2 ? "M2" : "WMO"),
": ", activePath_, " at (", position.x, ",", position.y, ",", position.z, ")"); ": ", activePath_, " at (", position.x, ",", position.y, ",", position.z, ")");
} }
@ -93,6 +95,19 @@ void ObjectPlacer::deleteSelected() {
selectedIdx_ = -1; selectedIdx_ = -1;
} }
void ObjectPlacer::undoLastPlace() {
if (undoStack_.empty()) return;
int idx = undoStack_.back();
undoStack_.pop_back();
if (idx >= 0 && idx < static_cast<int>(objects_.size())) {
if (selectedIdx_ == idx) selectedIdx_ = -1;
else if (selectedIdx_ > idx) selectedIdx_--;
objects_.erase(objects_.begin() + idx);
// Adjust remaining undo indices
for (auto& i : undoStack_) { if (i > idx) i--; }
}
}
void ObjectPlacer::syncToTerrain() { void ObjectPlacer::syncToTerrain() {
if (!terrain_) return; if (!terrain_) return;

View file

@ -57,6 +57,10 @@ public:
float getPlacementScale() const { return placementScale_; } float getPlacementScale() const { return placementScale_; }
void setPlacementScale(float s) { placementScale_ = s; } void setPlacementScale(float s) { placementScale_ = s; }
// Undo last placement
bool canUndoPlace() const { return !undoStack_.empty(); }
void undoLastPlace();
private: private:
uint32_t nextUniqueId(); uint32_t nextUniqueId();
@ -65,6 +69,7 @@ private:
PlaceableType activeType_ = PlaceableType::M2; PlaceableType activeType_ = PlaceableType::M2;
std::vector<PlacedObject> objects_; std::vector<PlacedObject> objects_;
std::vector<int> undoStack_; // indices of recently placed objects
int selectedIdx_ = -1; int selectedIdx_ = -1;
uint32_t uniqueIdCounter_ = 1; uint32_t uniqueIdCounter_ = 1;
float placementRotY_ = 0.0f; float placementRotY_ = 0.0f;