From 92b4aa7561b385d36425aced4a1d711e88fbbdcf Mon Sep 17 00:00:00 2001 From: ahcorde Date: Thu, 19 Aug 2021 13:49:14 +0200 Subject: [PATCH 1/5] Added Spawn plugin Signed-off-by: ahcorde --- examples/worlds/minimal_scene.sdf | 14 + src/gui/plugins/CMakeLists.txt | 1 + src/gui/plugins/spawn/CMakeLists.txt | 8 + src/gui/plugins/spawn/Spawn.cc | 510 +++++++++++++++++++++++++++ src/gui/plugins/spawn/Spawn.hh | 60 ++++ src/gui/plugins/spawn/Spawn.qml | 28 ++ src/gui/plugins/spawn/Spawn.qrc | 5 + 7 files changed, 626 insertions(+) create mode 100644 src/gui/plugins/spawn/CMakeLists.txt create mode 100644 src/gui/plugins/spawn/Spawn.cc create mode 100644 src/gui/plugins/spawn/Spawn.hh create mode 100644 src/gui/plugins/spawn/Spawn.qml create mode 100644 src/gui/plugins/spawn/Spawn.qrc diff --git a/examples/worlds/minimal_scene.sdf b/examples/worlds/minimal_scene.sdf index 1eee1a7a82..8a755f95c4 100644 --- a/examples/worlds/minimal_scene.sdf +++ b/examples/worlds/minimal_scene.sdf @@ -160,6 +160,20 @@ Missing for parity with GzScene3D: /world/buoyancy/stats + + + + + + + false + 5 + 5 + floating + false + + + diff --git a/src/gui/plugins/CMakeLists.txt b/src/gui/plugins/CMakeLists.txt index 2b0489a16c..c44d2e1f2b 100644 --- a/src/gui/plugins/CMakeLists.txt +++ b/src/gui/plugins/CMakeLists.txt @@ -132,6 +132,7 @@ add_subdirectory(scene3d) add_subdirectory(select_entities) add_subdirectory(scene_manager) add_subdirectory(shapes) +add_subdirectory(spawn) add_subdirectory(transform_control) add_subdirectory(video_recorder) add_subdirectory(view_angle) diff --git a/src/gui/plugins/spawn/CMakeLists.txt b/src/gui/plugins/spawn/CMakeLists.txt new file mode 100644 index 0000000000..dada40b6b3 --- /dev/null +++ b/src/gui/plugins/spawn/CMakeLists.txt @@ -0,0 +1,8 @@ +gz_add_gui_plugin(Spawn + SOURCES + Spawn.cc + QT_HEADERS + Spawn.hh + PUBLIC_LINK_LIBS + ${PROJECT_LIBRARY_TARGET_NAME}-rendering +) diff --git a/src/gui/plugins/spawn/Spawn.cc b/src/gui/plugins/spawn/Spawn.cc new file mode 100644 index 0000000000..d37c644588 --- /dev/null +++ b/src/gui/plugins/spawn/Spawn.cc @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include "Spawn.hh" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "ignition/gazebo/rendering/RenderUtil.hh" +#include "ignition/gazebo/rendering/SceneManager.hh" + +namespace ignition::gazebo +{ + class SpawnPrivate + { + /// \brief Update the 3D scene with new entities + public: void OnRender(); + + /// \brief Delete the visuals generated while an entity is being spawned. + public: void TerminateSpawnPreview(); + + /// \brief Generate a preview of a resource. + /// \param[in] _sdf The SDF to be previewed. + /// \return True on success, false if failure + public: bool GeneratePreview(const sdf::Root &_sdf); + + /// \brief Handle model placement requests + public: void HandleModelPlacement(); + + /// \brief Retrieve the point on a plane at z = 0 in the 3D scene hit by a + /// ray cast from the given 2D screen coordinates. + /// \param[in] _screenPos 2D coordinates on the screen, in pixels. + /// \param[in] _camera User camera + /// \param[in] _rayQuery Ray query for mouse clicks + /// \param[in] _offset Offset along the plane normal + /// \return 3D coordinates of a point in the 3D scene. + math::Vector3d ScreenToPlane( + const ignition::math::Vector2i &_screenPos, + const ignition::rendering::CameraPtr &_camera, + const ignition::rendering::RayQueryPtr &_rayQuery, + const float offset = 0.0); + + /// \brief Generate a unique entity id. + /// \return The unique entity id + ignition::gazebo::Entity UniqueId(); + + /// \brief Ignition communication node. + public: transport::Node node; + + /// \brief Flag for indicating whether we are spawning or not. + public: bool isSpawning = false; + + /// \brief Flag for indicating whether the user is currently placing a + /// resource with the shapes plugin or not + public: bool isPlacing = false; + + /// \brief The SDF string of the resource to be used with plugins that spawn + /// entities. + public: std::string spawnSdfString; + + /// \brief Path of an SDF file, to be used with plugins that spawn entities. + public: std::string spawnSdfPath; + + /// \brief Pointer to the rendering scene + public: ignition::rendering::ScenePtr scene{nullptr}; + + /// \brief A record of the ids currently used by the entity spawner + /// for easy deletion of visuals later + public: std::vector previewIds; + + /// \brief The visual generated from the spawnSdfString + public: ignition::rendering::NodePtr spawnPreview{nullptr}; + + /// \brief Scene manager + public: ignition::gazebo::SceneManager sceneManager; + + /// \brief The pose of the spawn preview. + public: ignition::math::Pose3d spawnPreviewPose = + ignition::math::Pose3d::Zero; + + /// \brief Mouse event + public: common::MouseEvent mouseEvent; + + /// \brief Flag to indicate if mouse event is dirty + public: bool mouseDirty = false; + + /// \brief Flag to indicate if hover event is dirty + public: bool hoverDirty = false; + + /// \brief Flag to indicate whether the escape key has been released. + public: bool escapeReleased = false; + + /// \brief The currently hovered mouse position in screen coordinates + public: math::Vector2i mouseHoverPos = math::Vector2i::Zero; + + /// \brief Ray query for mouse clicks + public: rendering::RayQueryPtr rayQuery{nullptr}; + + /// \brief User camera + public: rendering::CameraPtr camera{nullptr}; + + /// \brief Name of service for creating entity + public: std::string createCmdService; + + /// \brief Name of the world + public: std::string worldName; + }; +} + +using namespace ignition; +using namespace gazebo; + +///////////////////////////////////////////////// +Spawn::Spawn() + : ignition::gui::Plugin(), + dataPtr(std::make_unique()) +{ +} + +///////////////////////////////////////////////// +Spawn::~Spawn() = default; + +///////////////////////////////////////////////// +void Spawn::LoadConfig(const tinyxml2::XMLElement *) +{ + if (this->title.empty()) + this->title = "Spawn"; + + // World name from window, to construct default topics and services + auto worldNames = gui::worldNames(); + if (!worldNames.empty()) + this->dataPtr->worldName = worldNames[0].toStdString(); + + ignition::gui::App()->findChild + ()->installEventFilter(this); +} + + +// TODO(ahcorde): Replace this when this PR is on ign-rendering6 +///////////////////////////////////////////////// +math::Vector3d SpawnPrivate::ScreenToPlane( + const math::Vector2i &_screenPos, + const rendering::CameraPtr &_camera, + const rendering::RayQueryPtr &_rayQuery, + const float offset) +{ + // Normalize point on the image + double width = _camera->ImageWidth(); + double height = _camera->ImageHeight(); + + double nx = 2.0 * _screenPos.X() / width - 1.0; + double ny = 1.0 - 2.0 * _screenPos.Y() / height; + + // Make a ray query + _rayQuery->SetFromCamera( + _camera, math::Vector2d(nx, ny)); + + ignition::math::Planed plane(ignition::math::Vector3d(0, 0, 1), offset); + + math::Vector3d origin = _rayQuery->Origin(); + math::Vector3d direction = _rayQuery->Direction(); + double distance = plane.Distance(origin, direction); + return origin + direction * distance; +} + +///////////////////////////////////////////////// +void SpawnPrivate::HandleModelPlacement() +{ + if (!this->isPlacing) + return; + + if (this->spawnPreview && this->hoverDirty) + { + math::Vector3d pos = this->ScreenToPlane( + this->mouseHoverPos, this->camera, this->rayQuery); + pos.Z(this->spawnPreview->WorldPosition().Z()); + this->spawnPreview->SetWorldPosition(pos); + this->hoverDirty = false; + } + if (this->mouseEvent.Button() == common::MouseEvent::LEFT && + this->mouseEvent.Type() == common::MouseEvent::RELEASE && + !this->mouseEvent.Dragging() && this->mouseDirty) + { + // Delete the generated visuals + this->TerminateSpawnPreview(); + + math::Pose3d modelPose = this->spawnPreviewPose; + std::function cb = + [](const ignition::msgs::Boolean &/*_rep*/, const bool _result) + { + if (!_result) + ignerr << "Error creating model" << std::endl; + }; + math::Vector3d pos = this->ScreenToPlane( + this->mouseEvent.Pos(), this->camera, this->rayQuery); + pos.Z(modelPose.Pos().Z()); + msgs::EntityFactory req; + if (!this->spawnSdfString.empty()) + { + req.set_sdf(this->spawnSdfString); + } + else if (!this->spawnSdfPath.empty()) + { + req.set_sdf_filename(this->spawnSdfPath); + } + else + { + ignwarn << "Failed to find SDF string or file path" << std::endl; + } + req.set_allow_renaming(true); + msgs::Set(req.mutable_pose(), math::Pose3d(pos, modelPose.Rot())); + + if (this->createCmdService.empty()) + { + this->createCmdService = "/world/" + this->worldName + + "/create"; + } + this->createCmdService = transport::TopicUtils::AsValidTopic( + this->createCmdService); + if (this->createCmdService.empty()) + { + ignerr << "Failed to create valid create command service for world [" + << this->worldName <<"]" << std::endl; + return; + } + + this->node.Request(this->createCmdService, req, cb); + this->isPlacing = false; + this->mouseDirty = false; + this->spawnSdfString.clear(); + this->spawnSdfPath.clear(); + } +} + +///////////////////////////////////////////////// +ignition::gazebo::Entity SpawnPrivate::UniqueId() +{ + auto timeout = 100000u; + for (auto i = 0u; i < timeout; ++i) + { + Entity id = std::numeric_limits::max() - i; + if (!this->sceneManager.HasEntity(id)) + return id; + } + return kNullEntity; +} + +///////////////////////////////////////////////// +void SpawnPrivate::OnRender() +{ + if (nullptr == this->scene) + { + this->scene = rendering::sceneFromFirstRenderEngine(); + if (nullptr == this->scene) + { + return; + } + this->sceneManager.SetScene(this->scene); + + for (unsigned int i = 0; i < this->scene->NodeCount(); ++i) + { + auto cam = std::dynamic_pointer_cast( + this->scene->NodeByIndex(i)); + if (cam) + { + if (std::get(cam->UserData("user-camera"))) + { + this->camera = cam; + + // Ray Query + this->rayQuery = this->camera->Scene()->CreateRayQuery(); + + igndbg << "Spawn plugin is using camera [" + << this->camera->Name() << "]" << std::endl; + break; + } + } + } + } + + // Spawn + IGN_PROFILE("IgnRenderer::Render Spawn"); + if (this->isSpawning) + { + // Generate spawn preview + rendering::VisualPtr rootVis = this->scene->RootVisual(); + sdf::Root root; + if (!this->spawnSdfString.empty()) + { + root.LoadSdfString(this->spawnSdfString); + } + else if (!this->spawnSdfPath.empty()) + { + root.Load(this->spawnSdfPath); + } + else + { + ignwarn << "Failed to spawn: no SDF string or path" << std::endl; + } + this->isPlacing = this->GeneratePreview(root); + this->isSpawning = false; + } + + // Escape action, clear all selections and terminate any + // spawned previews if escape button is released + { + if (this->escapeReleased) + { + this->TerminateSpawnPreview(); + this->escapeReleased = false; + } + } + + this->HandleModelPlacement(); +} + +///////////////////////////////////////////////// +void SpawnPrivate::TerminateSpawnPreview() +{ + for (auto _id : this->previewIds) + this->sceneManager.RemoveEntity(_id); + this->previewIds.clear(); + this->isPlacing = false; +} + +///////////////////////////////////////////////// +bool SpawnPrivate::GeneratePreview(const sdf::Root &_sdf) +{ + // Terminate any pre-existing spawned entities + this->TerminateSpawnPreview(); + + if (nullptr == _sdf.Model() && nullptr == _sdf.Light()) + { + ignwarn << "Only model entities can be spawned at the moment." << std::endl; + this->TerminateSpawnPreview(); + return false; + } + + if (_sdf.Model()) + { + // Only preview first model + sdf::Model model = *(_sdf.Model()); + this->spawnPreviewPose = model.RawPose(); + model.SetName(ignition::common::Uuid().String()); + Entity modelId = this->UniqueId(); + if (!modelId) + { + this->TerminateSpawnPreview(); + return false; + } + this->spawnPreview = + this->sceneManager.CreateModel( + modelId, model, + this->sceneManager.WorldId()); + + this->previewIds.push_back(modelId); + for (auto j = 0u; j < model.LinkCount(); j++) + { + sdf::Link link = *(model.LinkByIndex(j)); + link.SetName(ignition::common::Uuid().String()); + Entity linkId = this->UniqueId(); + if (!linkId) + { + this->TerminateSpawnPreview(); + return false; + } + this->sceneManager.CreateLink( + linkId, link, modelId); + this->previewIds.push_back(linkId); + for (auto k = 0u; k < link.VisualCount(); k++) + { + sdf::Visual visual = *(link.VisualByIndex(k)); + visual.SetName(ignition::common::Uuid().String()); + Entity visualId = this->UniqueId(); + if (!visualId) + { + this->TerminateSpawnPreview(); + return false; + } + this->sceneManager.CreateVisual( + visualId, visual, linkId); + this->previewIds.push_back(visualId); + } + } + } + else if (_sdf.Light()) + { + // Only preview first model + sdf::Light light = *(_sdf.Light()); + this->spawnPreviewPose = light.RawPose(); + light.SetName(ignition::common::Uuid().String()); + Entity lightVisualId = this->UniqueId(); + if (!lightVisualId) + { + this->TerminateSpawnPreview(); + return false; + } + Entity lightId = this->UniqueId(); + if (!lightId) + { + this->TerminateSpawnPreview(); + return false; + } + this->spawnPreview = + this->sceneManager.CreateLight( + lightId, light, + this->sceneManager.WorldId()); + this->sceneManager.CreateLightVisual( + lightVisualId, light, lightId); + + this->previewIds.push_back(lightId); + this->previewIds.push_back(lightVisualId); + } + return true; +} + +//////////////////////////////////////////////// +bool Spawn::eventFilter(QObject *_obj, QEvent *_event) +{ + if (_event->type() == ignition::gui::events::Render::kType) + { + this->dataPtr->OnRender(); + } + else if (_event->type() == ignition::gui::events::LeftClickOnScene::kType) + { + ignition::gui::events::LeftClickOnScene *_e = + static_cast(_event); + this->dataPtr->mouseEvent = _e->Mouse(); + this->dataPtr->mouseDirty = true; + } + else if (_event->type() == ignition::gui::events::HoverOnScene::kType) + { + ignition::gui::events::HoverOnScene *_e = + static_cast(_event); + this->dataPtr->mouseHoverPos = _e->Point(); + this->dataPtr->hoverDirty = true; + } + else if (_event->type() == + ignition::gui::events::SpawnFromDescription::kType) + { + ignition::gui::events::SpawnFromDescription *_e = + static_cast(_event); + this->dataPtr->spawnSdfString = _e->Description(); + this->dataPtr->isSpawning = true; + } + else if (_event->type() == ignition::gui::events::SpawnFromPath::kType) + { + auto spawnPreviewPathEvent = + reinterpret_cast(_event); + this->dataPtr->spawnSdfPath = spawnPreviewPathEvent->FilePath(); + this->dataPtr->isSpawning = true; + } + else if (_event->type() == ignition::gui::events::KeyReleaseOnScene::kType) + { + ignition::gui::events::KeyReleaseOnScene *_e = + static_cast(_event); + if (_e->Key().Key() == Qt::Key_Escape) + { + this->dataPtr->escapeReleased = true; + } + } + + return QObject::eventFilter(_obj, _event); +} + +// Register this plugin +IGNITION_ADD_PLUGIN(ignition::gazebo::Spawn, + ignition::gui::Plugin) diff --git a/src/gui/plugins/spawn/Spawn.hh b/src/gui/plugins/spawn/Spawn.hh new file mode 100644 index 0000000000..6697dc67b5 --- /dev/null +++ b/src/gui/plugins/spawn/Spawn.hh @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#ifndef IGNITION_GAZEBO_GUI_Spawn_HH_ +#define IGNITION_GAZEBO_GUI_Spawn_HH_ + +#include + +#include + +namespace ignition +{ +namespace gazebo +{ + class SpawnPrivate; + + /// \brief Provides buttons for adding a box, sphere, or cylinder + /// to the scene + class Spawn : public ignition::gui::Plugin + { + Q_OBJECT + + /// \brief Constructor + public: Spawn(); + + /// \brief Destructor + public: ~Spawn() override; + + // Documentation inherited + public: void LoadConfig(const tinyxml2::XMLElement *_pluginElem) override; + + // Documentation inherited + protected: bool eventFilter(QObject *_obj, QEvent *_event) override; + + /// \brief Callback in Qt thread when mode changes. + /// \param[in] _mode New transform mode + public slots: void OnMode(const QString &_mode); + + /// \internal + /// \brief Pointer to private data. + private: std::unique_ptr dataPtr; + }; +} +} + +#endif diff --git a/src/gui/plugins/spawn/Spawn.qml b/src/gui/plugins/spawn/Spawn.qml new file mode 100644 index 0000000000..873da30014 --- /dev/null +++ b/src/gui/plugins/spawn/Spawn.qml @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 + +// TODO: remove invisible rectangle, see +// https://github.com/ignitionrobotics/ign-gui/issues/220 +Rectangle { + visible: false + Layout.minimumWidth: 100 + Layout.minimumHeight: 100 +} diff --git a/src/gui/plugins/spawn/Spawn.qrc b/src/gui/plugins/spawn/Spawn.qrc new file mode 100644 index 0000000000..bbdcea6f13 --- /dev/null +++ b/src/gui/plugins/spawn/Spawn.qrc @@ -0,0 +1,5 @@ + + + Spawn.qml + + From 46156583cd92d21e90906d2003e8864197e712a4 Mon Sep 17 00:00:00 2001 From: ahcorde Date: Thu, 19 Aug 2021 14:20:05 +0200 Subject: [PATCH 2/5] make linters happy Signed-off-by: ahcorde --- src/gui/plugins/spawn/Spawn.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/plugins/spawn/Spawn.cc b/src/gui/plugins/spawn/Spawn.cc index d37c644588..e9ae3c6b02 100644 --- a/src/gui/plugins/spawn/Spawn.cc +++ b/src/gui/plugins/spawn/Spawn.cc @@ -21,8 +21,10 @@ #include #include +#include #include #include +#include #include #include From d5f43addf1e9985df1c35daa768ed1e7685399ad Mon Sep 17 00:00:00 2001 From: ahcorde Date: Thu, 19 Aug 2021 20:14:38 +0200 Subject: [PATCH 3/5] updated hoverOnScene event Signed-off-by: ahcorde --- src/gui/plugins/spawn/Spawn.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/plugins/spawn/Spawn.cc b/src/gui/plugins/spawn/Spawn.cc index e9ae3c6b02..6f39239d43 100644 --- a/src/gui/plugins/spawn/Spawn.cc +++ b/src/gui/plugins/spawn/Spawn.cc @@ -476,7 +476,7 @@ bool Spawn::eventFilter(QObject *_obj, QEvent *_event) { ignition::gui::events::HoverOnScene *_e = static_cast(_event); - this->dataPtr->mouseHoverPos = _e->Point(); + this->dataPtr->mouseHoverPos = _e->Mouse().Pos(); this->dataPtr->hoverDirty = true; } else if (_event->type() == From f82ec2e16b5fdf237bf117fed3b32db014691032 Mon Sep 17 00:00:00 2001 From: ahcorde Date: Thu, 26 Aug 2021 18:26:49 +0200 Subject: [PATCH 4/5] Fixed spawn Signed-off-by: ahcorde --- .../plugins/select_entities/SelectEntities.cc | 20 ++++++++++++++++++- src/gui/plugins/spawn/Spawn.cc | 3 ++- src/gui/plugins/spawn/Spawn.hh | 10 +++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/gui/plugins/select_entities/SelectEntities.cc b/src/gui/plugins/select_entities/SelectEntities.cc index 4e571cab54..4971b327f9 100644 --- a/src/gui/plugins/select_entities/SelectEntities.cc +++ b/src/gui/plugins/select_entities/SelectEntities.cc @@ -135,6 +135,9 @@ class ignition::gazebo::gui::SelectEntitiesPrivate /// \brief is transform control active ? public: bool transformControlActive = false; + + /// \brief is Spawning from description active + public: bool isSpawnFromDescription{false}; }; using namespace ignition; @@ -489,7 +492,14 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) if (this->dataPtr->mouseEvent.Button() == common::MouseEvent::LEFT && this->dataPtr->mouseEvent.Type() == common::MouseEvent::PRESS) { - this->dataPtr->mouseDirty = true; + if(this->dataPtr->isSpawnFromDescription) + { + this->dataPtr->isSpawnFromDescription = false; + } + else + { + this->dataPtr->mouseDirty = true; + } } } else if (_event->type() == ignition::gui::events::Render::kType) @@ -545,6 +555,13 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) this->dataPtr->selectedEntitiesID.clear(); this->dataPtr->selectedEntities.clear(); } + else if (_event->type() == + ignition::gui::events::SpawnFromDescription::kType || + _event->type() == ignition::gui::events::SpawnFromPath::kType) + { + this->dataPtr->isSpawnFromDescription = true; + this->dataPtr->mouseDirty = true; + } else if (_event->type() == ignition::gui::events::KeyReleaseOnScene::kType) { ignition::gui::events::KeyReleaseOnScene *_e = @@ -553,6 +570,7 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) { this->dataPtr->mouseDirty = true; this->dataPtr->selectionHelper.deselectAll = true; + this->dataPtr->isSpawnFromDescription = false; } } diff --git a/src/gui/plugins/spawn/Spawn.cc b/src/gui/plugins/spawn/Spawn.cc index 6f39239d43..792f34e21c 100644 --- a/src/gui/plugins/spawn/Spawn.cc +++ b/src/gui/plugins/spawn/Spawn.cc @@ -470,7 +470,8 @@ bool Spawn::eventFilter(QObject *_obj, QEvent *_event) ignition::gui::events::LeftClickOnScene *_e = static_cast(_event); this->dataPtr->mouseEvent = _e->Mouse(); - this->dataPtr->mouseDirty = true; + if (this->dataPtr->isSpawning || this->dataPtr->isPlacing) + this->dataPtr->mouseDirty = true; } else if (_event->type() == ignition::gui::events::HoverOnScene::kType) { diff --git a/src/gui/plugins/spawn/Spawn.hh b/src/gui/plugins/spawn/Spawn.hh index 6697dc67b5..1a1df6d78e 100644 --- a/src/gui/plugins/spawn/Spawn.hh +++ b/src/gui/plugins/spawn/Spawn.hh @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Open Source Robotics Foundation + * Copyright (C) 2021 Open Source Robotics Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ * */ -#ifndef IGNITION_GAZEBO_GUI_Spawn_HH_ -#define IGNITION_GAZEBO_GUI_Spawn_HH_ +#ifndef IGNITION_GAZEBO_GUI_SPAWN_HH_ +#define IGNITION_GAZEBO_GUI_SPAWN_HH_ #include @@ -28,8 +28,8 @@ namespace gazebo { class SpawnPrivate; - /// \brief Provides buttons for adding a box, sphere, or cylinder - /// to the scene + /// \brief Allows to spawn models and lights using te gui event + /// SpawnFromDescription class Spawn : public ignition::gui::Plugin { Q_OBJECT From d93a1258d8496dc0816d879c3083fb0e84b9e719 Mon Sep 17 00:00:00 2001 From: Louise Poubel Date: Tue, 7 Sep 2021 21:51:06 -0700 Subject: [PATCH 5/5] tweaks Signed-off-by: Louise Poubel --- examples/worlds/minimal_scene.sdf | 3 +- .../plugins/select_entities/SelectEntities.cc | 16 +-- src/gui/plugins/spawn/Spawn.cc | 121 +++++++++--------- src/gui/plugins/spawn/Spawn.hh | 8 +- 4 files changed, 71 insertions(+), 77 deletions(-) diff --git a/examples/worlds/minimal_scene.sdf b/examples/worlds/minimal_scene.sdf index 8a755f95c4..a992a45b5f 100644 --- a/examples/worlds/minimal_scene.sdf +++ b/examples/worlds/minimal_scene.sdf @@ -13,14 +13,15 @@ Features: * Grid config * Select entities * Transform controls +* Spawn entities through GUI Missing for parity with GzScene3D: -* Spawn entities through GUI * Context menu * Record video * View angles * View collisions, wireframe, transparent, CoM, etc +* Drag and drop from Fuel / meshes * ... --> diff --git a/src/gui/plugins/select_entities/SelectEntities.cc b/src/gui/plugins/select_entities/SelectEntities.cc index 4971b327f9..a38adde8b9 100644 --- a/src/gui/plugins/select_entities/SelectEntities.cc +++ b/src/gui/plugins/select_entities/SelectEntities.cc @@ -136,8 +136,8 @@ class ignition::gazebo::gui::SelectEntitiesPrivate /// \brief is transform control active ? public: bool transformControlActive = false; - /// \brief is Spawning from description active - public: bool isSpawnFromDescription{false}; + /// \brief Is an entity being spawned + public: bool isSpawning{false}; }; using namespace ignition; @@ -488,13 +488,13 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) ignition::gui::events::LeftClickOnScene *_e = static_cast(_event); this->dataPtr->mouseEvent = _e->Mouse(); - // handle transform control + if (this->dataPtr->mouseEvent.Button() == common::MouseEvent::LEFT && - this->dataPtr->mouseEvent.Type() == common::MouseEvent::PRESS) + this->dataPtr->mouseEvent.Type() == common::MouseEvent::RELEASE) { - if(this->dataPtr->isSpawnFromDescription) + if (this->dataPtr->isSpawning) { - this->dataPtr->isSpawnFromDescription = false; + this->dataPtr->isSpawning = false; } else { @@ -559,7 +559,7 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) ignition::gui::events::SpawnFromDescription::kType || _event->type() == ignition::gui::events::SpawnFromPath::kType) { - this->dataPtr->isSpawnFromDescription = true; + this->dataPtr->isSpawning = true; this->dataPtr->mouseDirty = true; } else if (_event->type() == ignition::gui::events::KeyReleaseOnScene::kType) @@ -570,7 +570,7 @@ bool SelectEntities::eventFilter(QObject *_obj, QEvent *_event) { this->dataPtr->mouseDirty = true; this->dataPtr->selectionHelper.deselectAll = true; - this->dataPtr->isSpawnFromDescription = false; + this->dataPtr->isSpawning = false; } } diff --git a/src/gui/plugins/spawn/Spawn.cc b/src/gui/plugins/spawn/Spawn.cc index 792f34e21c..eaac31e183 100644 --- a/src/gui/plugins/spawn/Spawn.cc +++ b/src/gui/plugins/spawn/Spawn.cc @@ -18,7 +18,7 @@ #include "Spawn.hh" #include -#include +#include #include #include @@ -36,6 +36,7 @@ #include #include +#include #include @@ -57,7 +58,7 @@ namespace ignition::gazebo { class SpawnPrivate { - /// \brief Update the 3D scene with new entities + /// \brief Perform operations in the render thread. public: void OnRender(); /// \brief Delete the visuals generated while an entity is being spawned. @@ -68,8 +69,8 @@ namespace ignition::gazebo /// \return True on success, false if failure public: bool GeneratePreview(const sdf::Root &_sdf); - /// \brief Handle model placement requests - public: void HandleModelPlacement(); + /// \brief Handle placement requests + public: void HandlePlacement(); /// \brief Retrieve the point on a plane at z = 0 in the 3D scene hit by a /// ray cast from the given 2D screen coordinates. @@ -79,23 +80,23 @@ namespace ignition::gazebo /// \param[in] _offset Offset along the plane normal /// \return 3D coordinates of a point in the 3D scene. math::Vector3d ScreenToPlane( - const ignition::math::Vector2i &_screenPos, - const ignition::rendering::CameraPtr &_camera, - const ignition::rendering::RayQueryPtr &_rayQuery, + const math::Vector2i &_screenPos, + const rendering::CameraPtr &_camera, + const rendering::RayQueryPtr &_rayQuery, const float offset = 0.0); /// \brief Generate a unique entity id. /// \return The unique entity id - ignition::gazebo::Entity UniqueId(); + Entity UniqueId(); /// \brief Ignition communication node. public: transport::Node node; - /// \brief Flag for indicating whether we are spawning or not. - public: bool isSpawning = false; + /// \brief Flag for indicating whether the preview needs to be generated. + public: bool generatePreview = false; /// \brief Flag for indicating whether the user is currently placing a - /// resource with the shapes plugin or not + /// resource or not public: bool isPlacing = false; /// \brief The SDF string of the resource to be used with plugins that spawn @@ -106,21 +107,21 @@ namespace ignition::gazebo public: std::string spawnSdfPath; /// \brief Pointer to the rendering scene - public: ignition::rendering::ScenePtr scene{nullptr}; + public: rendering::ScenePtr scene{nullptr}; /// \brief A record of the ids currently used by the entity spawner /// for easy deletion of visuals later - public: std::vector previewIds; + public: std::vector previewIds; - /// \brief The visual generated from the spawnSdfString - public: ignition::rendering::NodePtr spawnPreview{nullptr}; + /// \brief Pointer to the preview that the user is placing. + public: rendering::NodePtr spawnPreview{nullptr}; /// \brief Scene manager - public: ignition::gazebo::SceneManager sceneManager; + public: SceneManager sceneManager; /// \brief The pose of the spawn preview. - public: ignition::math::Pose3d spawnPreviewPose = - ignition::math::Pose3d::Zero; + public: math::Pose3d spawnPreviewPose = + math::Pose3d::Zero; /// \brief Mouse event public: common::MouseEvent mouseEvent; @@ -180,7 +181,7 @@ void Spawn::LoadConfig(const tinyxml2::XMLElement *) } -// TODO(ahcorde): Replace this when this PR is on ign-rendering6 +// TODO(ahcorde): Replace this when this function is on ign-rendering6 ///////////////////////////////////////////////// math::Vector3d SpawnPrivate::ScreenToPlane( const math::Vector2i &_screenPos, @@ -199,7 +200,7 @@ math::Vector3d SpawnPrivate::ScreenToPlane( _rayQuery->SetFromCamera( _camera, math::Vector2d(nx, ny)); - ignition::math::Planed plane(ignition::math::Vector3d(0, 0, 1), offset); + math::Planed plane(math::Vector3d(0, 0, 1), offset); math::Vector3d origin = _rayQuery->Origin(); math::Vector3d direction = _rayQuery->Direction(); @@ -208,7 +209,7 @@ math::Vector3d SpawnPrivate::ScreenToPlane( } ///////////////////////////////////////////////// -void SpawnPrivate::HandleModelPlacement() +void SpawnPrivate::HandlePlacement() { if (!this->isPlacing) return; @@ -228,16 +229,16 @@ void SpawnPrivate::HandleModelPlacement() // Delete the generated visuals this->TerminateSpawnPreview(); - math::Pose3d modelPose = this->spawnPreviewPose; - std::function cb = - [](const ignition::msgs::Boolean &/*_rep*/, const bool _result) + auto pose = this->spawnPreviewPose; + std::function cb = + [](const msgs::Boolean &/*_rep*/, const bool _result) { if (!_result) - ignerr << "Error creating model" << std::endl; + ignerr << "Error creating entity" << std::endl; }; math::Vector3d pos = this->ScreenToPlane( this->mouseEvent.Pos(), this->camera, this->rayQuery); - pos.Z(modelPose.Pos().Z()); + pos.Z(pose.Pos().Z()); msgs::EntityFactory req; if (!this->spawnSdfString.empty()) { @@ -252,7 +253,7 @@ void SpawnPrivate::HandleModelPlacement() ignwarn << "Failed to find SDF string or file path" << std::endl; } req.set_allow_renaming(true); - msgs::Set(req.mutable_pose(), math::Pose3d(pos, modelPose.Rot())); + msgs::Set(req.mutable_pose(), math::Pose3d(pos, pose.Rot())); if (this->createCmdService.empty()) { @@ -277,7 +278,7 @@ void SpawnPrivate::HandleModelPlacement() } ///////////////////////////////////////////////// -ignition::gazebo::Entity SpawnPrivate::UniqueId() +Entity SpawnPrivate::UniqueId() { auto timeout = 100000u; for (auto i = 0u; i < timeout; ++i) @@ -324,7 +325,7 @@ void SpawnPrivate::OnRender() // Spawn IGN_PROFILE("IgnRenderer::Render Spawn"); - if (this->isSpawning) + if (this->generatePreview) { // Generate spawn preview rendering::VisualPtr rootVis = this->scene->RootVisual(); @@ -342,7 +343,7 @@ void SpawnPrivate::OnRender() ignwarn << "Failed to spawn: no SDF string or path" << std::endl; } this->isPlacing = this->GeneratePreview(root); - this->isSpawning = false; + this->generatePreview = false; } // Escape action, clear all selections and terminate any @@ -355,14 +356,16 @@ void SpawnPrivate::OnRender() } } - this->HandleModelPlacement(); + this->HandlePlacement(); } ///////////////////////////////////////////////// void SpawnPrivate::TerminateSpawnPreview() { for (auto _id : this->previewIds) + { this->sceneManager.RemoveEntity(_id); + } this->previewIds.clear(); this->isPlacing = false; } @@ -375,8 +378,8 @@ bool SpawnPrivate::GeneratePreview(const sdf::Root &_sdf) if (nullptr == _sdf.Model() && nullptr == _sdf.Light()) { - ignwarn << "Only model entities can be spawned at the moment." << std::endl; - this->TerminateSpawnPreview(); + ignwarn << "Only model or light entities can be spawned at the moment." + << std::endl; return false; } @@ -385,54 +388,50 @@ bool SpawnPrivate::GeneratePreview(const sdf::Root &_sdf) // Only preview first model sdf::Model model = *(_sdf.Model()); this->spawnPreviewPose = model.RawPose(); - model.SetName(ignition::common::Uuid().String()); + model.SetName(common::Uuid().String()); Entity modelId = this->UniqueId(); - if (!modelId) + if (kNullEntity == modelId) { this->TerminateSpawnPreview(); return false; } - this->spawnPreview = - this->sceneManager.CreateModel( - modelId, model, - this->sceneManager.WorldId()); + this->spawnPreview = this->sceneManager.CreateModel( + modelId, model, this->sceneManager.WorldId()); this->previewIds.push_back(modelId); for (auto j = 0u; j < model.LinkCount(); j++) { sdf::Link link = *(model.LinkByIndex(j)); - link.SetName(ignition::common::Uuid().String()); + link.SetName(common::Uuid().String()); Entity linkId = this->UniqueId(); if (!linkId) { this->TerminateSpawnPreview(); return false; } - this->sceneManager.CreateLink( - linkId, link, modelId); + this->sceneManager.CreateLink(linkId, link, modelId); this->previewIds.push_back(linkId); for (auto k = 0u; k < link.VisualCount(); k++) { - sdf::Visual visual = *(link.VisualByIndex(k)); - visual.SetName(ignition::common::Uuid().String()); - Entity visualId = this->UniqueId(); - if (!visualId) - { - this->TerminateSpawnPreview(); - return false; - } - this->sceneManager.CreateVisual( - visualId, visual, linkId); - this->previewIds.push_back(visualId); + sdf::Visual visual = *(link.VisualByIndex(k)); + visual.SetName(common::Uuid().String()); + Entity visualId = this->UniqueId(); + if (!visualId) + { + this->TerminateSpawnPreview(); + return false; + } + this->sceneManager.CreateVisual(visualId, visual, linkId); + this->previewIds.push_back(visualId); } } } else if (_sdf.Light()) { - // Only preview first model + // Only preview first light sdf::Light light = *(_sdf.Light()); this->spawnPreviewPose = light.RawPose(); - light.SetName(ignition::common::Uuid().String()); + light.SetName(common::Uuid().String()); Entity lightVisualId = this->UniqueId(); if (!lightVisualId) { @@ -445,10 +444,8 @@ bool SpawnPrivate::GeneratePreview(const sdf::Root &_sdf) this->TerminateSpawnPreview(); return false; } - this->spawnPreview = - this->sceneManager.CreateLight( - lightId, light, - this->sceneManager.WorldId()); + this->spawnPreview = this->sceneManager.CreateLight( + lightId, light, this->sceneManager.WorldId()); this->sceneManager.CreateLightVisual( lightVisualId, light, lightId); @@ -470,7 +467,7 @@ bool Spawn::eventFilter(QObject *_obj, QEvent *_event) ignition::gui::events::LeftClickOnScene *_e = static_cast(_event); this->dataPtr->mouseEvent = _e->Mouse(); - if (this->dataPtr->isSpawning || this->dataPtr->isPlacing) + if (this->dataPtr->generatePreview || this->dataPtr->isPlacing) this->dataPtr->mouseDirty = true; } else if (_event->type() == ignition::gui::events::HoverOnScene::kType) @@ -486,14 +483,14 @@ bool Spawn::eventFilter(QObject *_obj, QEvent *_event) ignition::gui::events::SpawnFromDescription *_e = static_cast(_event); this->dataPtr->spawnSdfString = _e->Description(); - this->dataPtr->isSpawning = true; + this->dataPtr->generatePreview = true; } else if (_event->type() == ignition::gui::events::SpawnFromPath::kType) { auto spawnPreviewPathEvent = reinterpret_cast(_event); this->dataPtr->spawnSdfPath = spawnPreviewPathEvent->FilePath(); - this->dataPtr->isSpawning = true; + this->dataPtr->generatePreview = true; } else if (_event->type() == ignition::gui::events::KeyReleaseOnScene::kType) { diff --git a/src/gui/plugins/spawn/Spawn.hh b/src/gui/plugins/spawn/Spawn.hh index 1a1df6d78e..b4c0380db1 100644 --- a/src/gui/plugins/spawn/Spawn.hh +++ b/src/gui/plugins/spawn/Spawn.hh @@ -28,8 +28,8 @@ namespace gazebo { class SpawnPrivate; - /// \brief Allows to spawn models and lights using te gui event - /// SpawnFromDescription + /// \brief Allows to spawn models and lights using the spawn gui events. + // TODO(anyone) Support drag and drop class Spawn : public ignition::gui::Plugin { Q_OBJECT @@ -46,10 +46,6 @@ namespace gazebo // Documentation inherited protected: bool eventFilter(QObject *_obj, QEvent *_event) override; - /// \brief Callback in Qt thread when mode changes. - /// \param[in] _mode New transform mode - public slots: void OnMode(const QString &_mode); - /// \internal /// \brief Pointer to private data. private: std::unique_ptr dataPtr;