diff --git a/examples/worlds/minimal_scene.sdf b/examples/worlds/minimal_scene.sdf
index 1eee1a7a82..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
* ...
-->
@@ -160,6 +161,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/select_entities/SelectEntities.cc b/src/gui/plugins/select_entities/SelectEntities.cc
index 4e571cab54..a38adde8b9 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 an entity being spawned
+ public: bool isSpawning{false};
};
using namespace ignition;
@@ -485,11 +488,18 @@ 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)
{
- this->dataPtr->mouseDirty = true;
+ if (this->dataPtr->isSpawning)
+ {
+ this->dataPtr->isSpawning = 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->isSpawning = 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->isSpawning = false;
}
}
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..eaac31e183
--- /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
+#include
+
+#include
+
+#include "ignition/gazebo/rendering/RenderUtil.hh"
+#include "ignition/gazebo/rendering/SceneManager.hh"
+
+namespace ignition::gazebo
+{
+ class SpawnPrivate
+ {
+ /// \brief Perform operations in the render thread.
+ 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 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.
+ /// \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 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
+ Entity UniqueId();
+
+ /// \brief Ignition communication node.
+ public: transport::Node node;
+
+ /// \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 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: 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 Pointer to the preview that the user is placing.
+ public: rendering::NodePtr spawnPreview{nullptr};
+
+ /// \brief Scene manager
+ public: SceneManager sceneManager;
+
+ /// \brief The pose of the spawn preview.
+ public: math::Pose3d spawnPreviewPose =
+ 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 function 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));
+
+ math::Planed plane(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::HandlePlacement()
+{
+ 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();
+
+ auto pose = this->spawnPreviewPose;
+ std::function cb =
+ [](const msgs::Boolean &/*_rep*/, const bool _result)
+ {
+ if (!_result)
+ ignerr << "Error creating entity" << std::endl;
+ };
+ math::Vector3d pos = this->ScreenToPlane(
+ this->mouseEvent.Pos(), this->camera, this->rayQuery);
+ pos.Z(pose.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, pose.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();
+ }
+}
+
+/////////////////////////////////////////////////
+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->generatePreview)
+ {
+ // 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->generatePreview = 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->HandlePlacement();
+}
+
+/////////////////////////////////////////////////
+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 or light entities can be spawned at the moment."
+ << std::endl;
+ return false;
+ }
+
+ if (_sdf.Model())
+ {
+ // Only preview first model
+ sdf::Model model = *(_sdf.Model());
+ this->spawnPreviewPose = model.RawPose();
+ model.SetName(common::Uuid().String());
+ Entity modelId = this->UniqueId();
+ if (kNullEntity == 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(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(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 light
+ sdf::Light light = *(_sdf.Light());
+ this->spawnPreviewPose = light.RawPose();
+ light.SetName(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();
+ if (this->dataPtr->generatePreview || this->dataPtr->isPlacing)
+ 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->Mouse().Pos();
+ 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->generatePreview = true;
+ }
+ else if (_event->type() == ignition::gui::events::SpawnFromPath::kType)
+ {
+ auto spawnPreviewPathEvent =
+ reinterpret_cast(_event);
+ this->dataPtr->spawnSdfPath = spawnPreviewPathEvent->FilePath();
+ this->dataPtr->generatePreview = 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..b4c0380db1
--- /dev/null
+++ b/src/gui/plugins/spawn/Spawn.hh
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ *
+*/
+
+#ifndef IGNITION_GAZEBO_GUI_SPAWN_HH_
+#define IGNITION_GAZEBO_GUI_SPAWN_HH_
+
+#include
+
+#include
+
+namespace ignition
+{
+namespace gazebo
+{
+ class SpawnPrivate;
+
+ /// \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
+
+ /// \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;
+
+ /// \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
+
+