diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e358d6da..85fffdd1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -266,6 +266,7 @@ Feature #5193: Weapon sheathing Feature #5219: Impelement TestCells console command Feature #5224: Handle NiKeyframeController for NiTriShape + Feature #5274: Editor: Keyboard shortcut to drop objects to ground/obstacle in scene view Feature #5304: Morrowind-style bump-mapping Feature #5314: Ingredient filter in the alchemy window Task #4686: Upgrade media decoder to a more current FFmpeg API diff --git a/CHANGELOG_PR.md b/CHANGELOG_PR.md index f462da32c..94b0d45ec 100644 --- a/CHANGELOG_PR.md +++ b/CHANGELOG_PR.md @@ -43,6 +43,7 @@ New Editor Features: - Changes to height editing can be cancelled without changes to data (press esc to cancel) (#4840) - Land heightmap/shape editing and vertex selection (#5170) - Deleting instances with a keypress (#5172) +- Dropping objects with keyboard shortcuts (#5274) Bug Fixes: - The Mouse Wheel can now be used for key bindings (#2679) diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 442e707bf..89b5283a4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -356,6 +356,10 @@ void CSMPrefs::State::declare() QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); declareModifier ("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); declareShortcut ("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete)); + declareShortcut ("scene-instance-drop-terrain", "Drop to terrain level", QKeySequence(Qt::Key_G)); + declareShortcut ("scene-instance-drop-collision", "Drop to collision", QKeySequence(Qt::Key_H)); + declareShortcut ("scene-instance-drop-terrain-separately", "Drop to terrain level separately", QKeySequence()); + declareShortcut ("scene-instance-drop-collision-separately", "Drop to collision separately", QKeySequence()); declareShortcut ("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); declareShortcut ("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); declareShortcut ("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 92f5cbb2d..7c0020f1e 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -3,9 +3,15 @@ #include #include +#include #include "../../model/prefs/state.hpp" +#include +#include +#include +#include + #include "../../model/world/idtable.hpp" #include "../../model/world/idtree.hpp" #include "../../model/world/commands.hpp" @@ -90,16 +96,26 @@ osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos) return pos * combined; } -CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, QWidget *parent) +CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent) : EditMode (worldspaceWidget, QIcon (":scenetoolbar/editing-instance"), SceneUtil::Mask_EditorReference | SceneUtil::Mask_Terrain, "Instance editing", parent), mSubMode (0), mSubModeId ("move"), mSelectionMode (0), mDragMode (DragMode_None), - mDragAxis (-1), mLocked (false), mUnitScaleDist(1) + mDragAxis (-1), mLocked (false), mUnitScaleDist(1), mParentNode (parentNode) { connect(this, SIGNAL(requestFocus(const std::string&)), worldspaceWidget, SIGNAL(requestFocus(const std::string&))); CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget); connect(deleteShortcut, SIGNAL(activated(bool)), this, SLOT(deleteSelectedInstances(bool))); + + // Following classes could be simplified by using QSignalMapper, which is obsolete in Qt5.10, but not in Qt4.8 and Qt5.14 + CSMPrefs::Shortcut* dropToCollisionShortcut = new CSMPrefs::Shortcut("scene-instance-drop-collision", worldspaceWidget); + connect(dropToCollisionShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollision())); + CSMPrefs::Shortcut* dropToTerrainLevelShortcut = new CSMPrefs::Shortcut("scene-instance-drop-terrain", worldspaceWidget); + connect(dropToTerrainLevelShortcut, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrain())); + CSMPrefs::Shortcut* dropToCollisionShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-collision-separately", worldspaceWidget); + connect(dropToCollisionShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToCollisionSeparately())); + CSMPrefs::Shortcut* dropToTerrainLevelShortcut2 = new CSMPrefs::Shortcut("scene-instance-drop-terrain-separately", worldspaceWidget); + connect(dropToTerrainLevelShortcut2, SIGNAL(activated()), this, SLOT(dropSelectedInstancesToTerrainSeparately())); } void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) @@ -681,3 +697,187 @@ void CSVRender::InstanceMode::deleteSelectedInstances(bool active) getWorldspaceWidget().clearSelection (SceneUtil::Mask_EditorReference); } + +void CSVRender::InstanceMode::dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight) +{ + osg::Vec3d point = object->getPosition().asVec3(); + + osg::Vec3d start = point; + start.z() += objectHeight; + osg::Vec3d end = point; + end.z() = std::numeric_limits::lowest(); + + osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( + osgUtil::Intersector::MODEL, start, end) ); + intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); + osgUtil::IntersectionVisitor visitor(intersector); + + if (dropMode == TerrainSep) + visitor.setTraversalMask(SceneUtil::Mask_Terrain); + if (dropMode == CollisionSep) + visitor.setTraversalMask(SceneUtil::Mask_Terrain | SceneUtil::Mask_EditorReference); + + mParentNode->accept(visitor); + + for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); + it != intersector->getIntersections().end(); ++it) + { + osgUtil::LineSegmentIntersector::Intersection intersection = *it; + ESM::Position position = object->getPosition(); + object->setEdited (Object::Override_Position); + position.pos[2] = intersection.getWorldIntersectPoint().z() + objectHeight; + object->setPosition(position.pos); + + return; + } +} + +float CSVRender::InstanceMode::getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight) +{ + osg::Vec3d point = object->getPosition().asVec3(); + + osg::Vec3d start = point; + start.z() += objectHeight; + osg::Vec3d end = point; + end.z() = std::numeric_limits::lowest(); + + osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector( + osgUtil::Intersector::MODEL, start, end) ); + intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); + osgUtil::IntersectionVisitor visitor(intersector); + + if (dropMode == Terrain) + visitor.setTraversalMask(SceneUtil::Mask_Terrain); + if (dropMode == Collision) + visitor.setTraversalMask(SceneUtil::Mask_Terrain | SceneUtil::Mask_EditorReference); + + mParentNode->accept(visitor); + + for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); + it != intersector->getIntersections().end(); ++it) + { + osgUtil::LineSegmentIntersector::Intersection intersection = *it; + float collisionLevel = intersection.getWorldIntersectPoint().z(); + return point.z() - collisionLevel + objectHeight; + } + + return 0.0f; +} + +void CSVRender::InstanceMode::dropSelectedInstancesToCollision() +{ + handleDropMethod(Collision, "Drop instances to next collision"); +} + +void CSVRender::InstanceMode::dropSelectedInstancesToTerrain() +{ + handleDropMethod(Terrain, "Drop instances to terrain level"); +} + +void CSVRender::InstanceMode::dropSelectedInstancesToCollisionSeparately() +{ + handleDropMethod(TerrainSep, "Drop instances to next collision level separately"); +} + +void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately() +{ + handleDropMethod(CollisionSep, "Drop instances to terrain level separately"); +} + +void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg) +{ + std::vector > selection = getWorldspaceWidget().getSelection (SceneUtil::Mask_EditorReference); + if (selection.empty()) + return; + + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + QUndoStack& undoStack = document.getUndoStack(); + + CSMWorld::CommandMacro macro (undoStack, commandMsg); + + DropObjectDataHandler dropObjectDataHandler(&getWorldspaceWidget()); + + switch (dropMode) + { + case Terrain: + case Collision: + { + float smallestDropHeight = std::numeric_limits::max(); + int counter = 0; + for(osg::ref_ptr tag: selection) + if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + { + float thisDrop = getDropHeight(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]); + if (thisDrop < smallestDropHeight) + smallestDropHeight = thisDrop; + counter++; + } + for(osg::ref_ptr tag: selection) + if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + { + objectTag->mObject->setEdited (Object::Override_Position); + ESM::Position position = objectTag->mObject->getPosition(); + position.pos[2] -= smallestDropHeight; + objectTag->mObject->setPosition(position.pos); + objectTag->mObject->apply (macro); + } + } + break; + + case TerrainSep: + case CollisionSep: + { + int counter = 0; + for(osg::ref_ptr tag: selection) + if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + { + dropInstance(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]); + objectTag->mObject->apply (macro); + counter++; + } + } + break; + } +} + +CSVRender::DropObjectDataHandler::DropObjectDataHandler(WorldspaceWidget* worldspacewidget) + : mWorldspaceWidget(worldspacewidget) +{ + std::vector > selection = mWorldspaceWidget->getSelection (SceneUtil::Mask_EditorReference); + for(osg::ref_ptr tag: selection) + { + if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + { + osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); + osg::ref_ptr objectNodeWithoutGUI = objectTag->mObject->getBaseNode(); + + osg::ComputeBoundsVisitor computeBounds; + computeBounds.setTraversalMask(SceneUtil::Mask_EditorReference); + objectNodeWithoutGUI->accept(computeBounds); + osg::BoundingBox bounds = computeBounds.getBoundingBox(); + float boundingBoxOffset = 0.0f; + if (bounds.valid()) + boundingBoxOffset = bounds.zMin(); + + mObjectHeights.emplace_back(boundingBoxOffset); + mOldMasks.emplace_back(objectNodeWithGUI->getNodeMask()); + + objectNodeWithGUI->setNodeMask(SceneUtil::Mask_Disabled); + } + } +} + +CSVRender::DropObjectDataHandler::~DropObjectDataHandler() +{ + std::vector > selection = mWorldspaceWidget->getSelection (SceneUtil::Mask_EditorReference); + int counter = 0; + for(osg::ref_ptr tag: selection) + { + if (CSVRender::ObjectTag *objectTag = dynamic_cast (tag.get())) + { + osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); + objectNodeWithGUI->setNodeMask(mOldMasks[counter]); + counter++; + } + } +} diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 23df3f37d..beb60aff3 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -1,7 +1,10 @@ #ifndef CSV_RENDER_INSTANCEMODE_H #define CSV_RENDER_INSTANCEMODE_H +#include + #include +#include #include #include @@ -16,6 +19,7 @@ namespace CSVRender { class TagBase; class InstanceSelectionMode; + class Object; class InstanceMode : public EditMode { @@ -29,6 +33,14 @@ namespace CSVRender DragMode_Scale }; + enum DropMode + { + Collision, + Terrain, + CollisionSep, + TerrainSep + }; + CSVWidget::SceneToolMode *mSubMode; std::string mSubModeId; InstanceSelectionMode *mSelectionMode; @@ -36,6 +48,7 @@ namespace CSVRender int mDragAxis; bool mLocked; float mUnitScaleDist; + osg::ref_ptr mParentNode; int getSubModeFromId (const std::string& id) const; @@ -44,10 +57,12 @@ namespace CSVRender osg::Vec3f getSelectionCenter(const std::vector >& selection) const; osg::Vec3f getScreenCoords(const osg::Vec3f& pos); + void dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight); + float getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight); public: - InstanceMode (WorldspaceWidget *worldspaceWidget, QWidget *parent = 0); + InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr parentNode, QWidget *parent = 0); virtual void activate (CSVWidget::SceneToolbar *toolbar); @@ -93,6 +108,24 @@ namespace CSVRender void subModeChanged (const std::string& id); void deleteSelectedInstances(bool active); + void dropSelectedInstancesToCollision(); + void dropSelectedInstancesToTerrain(); + void dropSelectedInstancesToCollisionSeparately(); + void dropSelectedInstancesToTerrainSeparately(); + void handleDropMethod(DropMode dropMode, QString commandMsg); + }; + + /// \brief Helper class to handle object mask data in safe way + class DropObjectDataHandler + { + public: + DropObjectDataHandler(WorldspaceWidget* worldspacewidget); + ~DropObjectDataHandler(); + std::vector mObjectHeights; + + private: + WorldspaceWidget* mWorldspaceWidget; + std::vector mOldMasks; }; } diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 02f89b0ab..6b33cdeb2 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -477,6 +477,16 @@ bool CSVRender::Object::getSelected() const return mSelected; } +osg::ref_ptr CSVRender::Object::getRootNode() +{ + return mRootNode; +} + +osg::ref_ptr CSVRender::Object::getBaseNode() +{ + return mBaseNode; +} + bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index 10a46fc10..4d1763f80 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -146,6 +146,12 @@ namespace CSVRender bool getSelected() const; + /// Get object node with GUI graphics + osg::ref_ptr getRootNode(); + + /// Get object node without GUI graphics + osg::ref_ptr getBaseNode(); + /// \return Did this call result in a modification of the visual representation of /// this object? bool referenceableDataChanged (const QModelIndex& topLeft, diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 77ecba918..6ab4b041b 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -370,7 +370,7 @@ void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) { /// \todo replace EditMode with suitable subclasses - tool->addButton (new InstanceMode (this, tool), "object"); + tool->addButton (new InstanceMode (this, mRootNode, tool), "object"); tool->addButton (new PathgridMode (this, tool), "pathgrid"); }