diff --git a/Changelog.md b/Changelog.md index 9f8d81256..309b9b5a3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -163,6 +163,20 @@ ## Gazebo GUI 6 +### Gazebo GUI 6.7.0 (2022-12-02) + +1. Set View Camera controller from plugin configuration + * [Pull request #506](https://github.com/gazebosim/gz-gui/pull/506) + +1. Add service for configuring view control sensitivity + * [Pull request #504](https://github.com/gazebosim/gz-gui/pull/504) + +1. Fix large / unexpected camera movements + * [Pull request #502](https://github.com/gazebosim/gz-gui/pull/502) + +1. Add view control reference visual + * [Pull request #500](https://github.com/gazebosim/gz-gui/pull/500) + ### Gazebo GUI 6.6.1 (2022-08-17) 1. Fix mistaken dialog error message diff --git a/src/plugins/interactive_view_control/InteractiveViewControl.cc b/src/plugins/interactive_view_control/InteractiveViewControl.cc index cb3c2c5fd..b6c4a5514 100644 --- a/src/plugins/interactive_view_control/InteractiveViewControl.cc +++ b/src/plugins/interactive_view_control/InteractiveViewControl.cc @@ -16,6 +16,7 @@ */ #include +#include #include #include @@ -30,6 +31,8 @@ #include #include +#include +#include #include #include #include @@ -48,14 +51,41 @@ class gz::gui::plugins::InteractiveViewControlPrivate /// \brief Callback for camera view controller request /// \param[in] _msg Request message to set the camera view controller - /// \param[in] _res Response data + /// \param[out] _res Response data /// \return True if the request is received public: bool OnViewControl(const msgs::StringMsg &_msg, msgs::Boolean &_res); + /// \brief Callback for camera reference visual request + /// \param[in] _msg Request message to enable/disable the reference visual + /// \param[out] _res Response data + /// \return True if the request is received + public: bool OnReferenceVisual(const msgs::Boolean &_msg, + msgs::Boolean &_res); + + /// \brief Callback for camera view control sensitivity request + /// \param[in] _msg Request message to set the camera view controller + /// sensitivity. Value must be greater than zero. The higher the number + /// the more sensitive camera control is to mouse movements. Affects all + /// camera movements (pan, orbit, zoom) + /// \param[out] _res Response data + /// \return True if the request is received + public: bool OnViewControlSensitivity(const msgs::Double &_msg, + msgs::Boolean &_res); + + /// \brief Update the reference visual. Adjust scale based on distance from + /// camera to target point so it remains the same size on screen. + public: void UpdateReferenceVisual(); + /// \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 if mouse press event is dirty + public: bool mousePressDirty = false; + /// \brief True to block orbiting with the mouse. public: bool blockOrbit = false; @@ -86,17 +116,32 @@ class gz::gui::plugins::InteractiveViewControlPrivate /// \brief View controller public: std::string viewController{"orbit"}; + /// \brief Enable / disable reference visual + public: bool enableRefVisual{true}; + /// \brief Camera view control service public: std::string cameraViewControlService; + /// \brief Camera reference visual service + public: std::string cameraRefVisualService; + + /// \brief Camera view control sensitivity service + public: std::string cameraViewControlSensitivityService; + /// \brief Ray query for mouse clicks public: rendering::RayQueryPtr rayQuery{nullptr}; //// \brief Pointer to the rendering scene public: rendering::ScenePtr scene{nullptr}; + /// \brief Reference visual for visualizing the target point + public: rendering::VisualPtr refVisual{nullptr}; + /// \brief Transport node for making transform control requests public: transport::Node node; + + /// \brief View control sensitivity value. Must be greater than 0. + public: double viewControlSensitivity = 1.0; }; using namespace gz; @@ -151,10 +196,18 @@ void InteractiveViewControlPrivate::OnRender() return; } - if (!this->mouseDirty) + if (!this->camera) return; - if (!this->camera) + // hover + if (this->hoverDirty) + { + if (this->refVisual) + this->refVisual->SetVisible(false); + this->hoverDirty = false; + } + + if (!this->mouseDirty) return; std::lock_guard lock(this->mutex); @@ -176,6 +229,34 @@ void InteractiveViewControlPrivate::OnRender() } this->viewControl->SetCamera(this->camera); + if (this->enableRefVisual) + { + if (!this->refVisual) + { + // create ref visual + this->refVisual = scene->CreateVisual(); + rendering::GeometryPtr sphere = scene->CreateSphere(); + this->refVisual->AddGeometry(sphere); + this->refVisual->SetLocalScale(math::Vector3d(0.2, 0.2, 0.1)); + this->refVisual->SetVisibilityFlags( + GZ_VISIBILITY_GUI & ~GZ_VISIBILITY_SELECTABLE + ); + + // create material + math::Color diffuse(1.0f, 1.0f, 0.0f, 1.0f); + math::Color specular(1.0f, 1.0f, 0.0f, 1.0f); + double transparency = 0.3; + rendering::MaterialPtr material = scene->CreateMaterial(); + material->SetDiffuse(diffuse); + material->SetSpecular(specular); + material->SetTransparency(transparency); + material->SetCastShadows(false); + this->refVisual->SetMaterial(material); + scene->DestroyMaterial(material); + } + this->refVisual->SetVisible(true); + } + if (this->mouseEvent.Type() == common::MouseEvent::SCROLL) { this->target = rendering::screenToScene( @@ -184,29 +265,38 @@ void InteractiveViewControlPrivate::OnRender() this->viewControl->SetTarget(this->target); double distance = this->camera->WorldPosition().Distance( this->target); - double amount = -this->drag.Y() * distance / 5.0; + + math::Vector2d newDrag = this->drag * this->viewControlSensitivity; + double amount = -newDrag.Y() * distance / 5.0; this->viewControl->Zoom(amount); + this->UpdateReferenceVisual(); } else if (this->mouseEvent.Type() == common::MouseEvent::PRESS) { this->target = rendering::screenToScene( this->mouseEvent.PressPos(), this->camera, this->rayQuery); + this->viewControl->SetTarget(this->target); + this->UpdateReferenceVisual(); + this->mousePressDirty = false; } else { + math::Vector2d newDrag = this->drag * this->viewControlSensitivity; // Pan with left button if (this->mouseEvent.Buttons() & common::MouseEvent::LEFT) { if (Qt::ShiftModifier == QGuiApplication::queryKeyboardModifiers()) - this->viewControl->Orbit(this->drag); + this->viewControl->Orbit(newDrag); else - this->viewControl->Pan(this->drag); + this->viewControl->Pan(newDrag); + this->UpdateReferenceVisual(); } // Orbit with middle button else if (this->mouseEvent.Buttons() & common::MouseEvent::MIDDLE) { - this->viewControl->Orbit(this->drag); + this->viewControl->Orbit(newDrag); + this->UpdateReferenceVisual(); } // Zoom with right button else if (this->mouseEvent.Buttons() & common::MouseEvent::RIGHT) @@ -214,16 +304,34 @@ void InteractiveViewControlPrivate::OnRender() double hfov = this->camera->HFOV().Radian(); double vfov = 2.0f * atan(tan(hfov / 2.0f) / this->camera->AspectRatio()); double distance = this->camera->WorldPosition().Distance(this->target); - double amount = ((-this->drag.Y() / + double amount = ((-newDrag.Y() / static_cast(this->camera->ImageHeight())) * distance * tan(vfov/2.0) * 6.0); this->viewControl->Zoom(amount); + this->UpdateReferenceVisual(); } } + this->drag = 0; this->mouseDirty = false; } +///////////////////////////////////////////////// +void InteractiveViewControlPrivate::UpdateReferenceVisual() +{ + if (!this->refVisual || !this->enableRefVisual) + return; + this->refVisual->SetWorldPosition(this->target); + // Update the size of the reference visual based on the distance to the + // target point. + double distance = + this->camera->WorldPosition().Distance(this->target); + + double scale = distance * atan(GZ_DTOR(1.0)); + this->refVisual->SetLocalScale( + math::Vector3d(scale, scale, scale * 0.5)); +} + ///////////////////////////////////////////////// bool InteractiveViewControlPrivate::OnViewControl(const msgs::StringMsg &_msg, msgs::Boolean &_res) @@ -248,6 +356,37 @@ bool InteractiveViewControlPrivate::OnViewControl(const msgs::StringMsg &_msg, return true; } +///////////////////////////////////////////////// +bool InteractiveViewControlPrivate::OnReferenceVisual(const msgs::Boolean &_msg, + msgs::Boolean &_res) +{ + std::lock_guard lock(this->mutex); + this->enableRefVisual = _msg.data(); + + _res.set_data(true); + return true; +} + +///////////////////////////////////////////////// +bool InteractiveViewControlPrivate::OnViewControlSensitivity( + const msgs::Double &_msg, msgs::Boolean &_res) +{ + std::lock_guard lock(this->mutex); + + if (_msg.data() <= 0.0) + { + gzwarn << "View controller sensitivity must be greater than zero [" + << _msg.data() << "]" << std::endl; + _res.set_data(false); + return true; + } + + this->viewControlSensitivity = _msg.data(); + + _res.set_data(true); + return true; +} + ///////////////////////////////////////////////// InteractiveViewControl::InteractiveViewControl() : Plugin(), dataPtr(std::make_unique()) @@ -271,6 +410,25 @@ void InteractiveViewControl::LoadConfig( gzmsg << "Camera view controller topic advertised on [" << this->dataPtr->cameraViewControlService << "]" << std::endl; + // camera reference visual + this->dataPtr->cameraRefVisualService = + "/gui/camera/view_control/reference_visual"; + this->dataPtr->node.Advertise(this->dataPtr->cameraRefVisualService, + &InteractiveViewControlPrivate::OnReferenceVisual, this->dataPtr.get()); + gzmsg << "Camera reference visual topic advertised on [" + << this->dataPtr->cameraRefVisualService << "]" << std::endl; + + // camera view control sensitivity + this->dataPtr->cameraViewControlSensitivityService = + "/gui/camera/view_control/sensitivity"; + this->dataPtr->node.Advertise( + this->dataPtr->cameraViewControlSensitivityService, + &InteractiveViewControlPrivate::OnViewControlSensitivity, + this->dataPtr.get()); + gzmsg << "Camera view control sensitivity advertised on [" + << this->dataPtr->cameraViewControlSensitivityService << "]" + << std::endl; + gz::gui::App()->findChild< gz::gui::MainWindow *>()->installEventFilter(this); } @@ -296,12 +454,16 @@ bool InteractiveViewControl::eventFilter(QObject *_obj, QEvent *_event) auto pressOnScene = reinterpret_cast(_event); this->dataPtr->mouseDirty = true; + this->dataPtr->mousePressDirty = true; this->dataPtr->drag = math::Vector2d::Zero; this->dataPtr->mouseEvent = pressOnScene->Mouse(); } else if (_event->type() == events::DragOnScene::kType) { + if (this->dataPtr->mousePressDirty) + return QObject::eventFilter(_obj, _event); + auto dragOnScene = reinterpret_cast(_event); this->dataPtr->mouseDirty = true; @@ -332,6 +494,10 @@ bool InteractiveViewControl::eventFilter(QObject *_obj, QEvent *_event) _event); this->dataPtr->blockOrbit = blockOrbit->Block(); } + else if (_event->type() == gui::events::HoverOnScene::kType) + { + this->dataPtr->hoverDirty = true; + } // Standard event processing return QObject::eventFilter(_obj, _event); diff --git a/src/plugins/minimal_scene/CMakeLists.txt b/src/plugins/minimal_scene/CMakeLists.txt index bb322e923..8e2a9526c 100644 --- a/src/plugins/minimal_scene/CMakeLists.txt +++ b/src/plugins/minimal_scene/CMakeLists.txt @@ -26,6 +26,7 @@ gz_gui_add_plugin(MinimalScene MinimalScene.hh PUBLIC_LINK_LIBS gz-rendering${GZ_RENDERING_VER}::gz-rendering${GZ_RENDERING_VER} + gz-transport${GZ_TRANSPORT_VER}::gz-transport${GZ_TRANSPORT_VER} ${PROJECT_LINK_LIBS} ) diff --git a/src/plugins/minimal_scene/MinimalScene.cc b/src/plugins/minimal_scene/MinimalScene.cc index f42d37233..903ac6fb2 100644 --- a/src/plugins/minimal_scene/MinimalScene.cc +++ b/src/plugins/minimal_scene/MinimalScene.cc @@ -15,12 +15,16 @@ * */ +#include +#include + #include "MinimalScene.hh" #include "MinimalSceneRhi.hh" #include "MinimalSceneRhiMetal.hh" #include "MinimalSceneRhiOpenGL.hh" #include +#include #include #include #include @@ -38,6 +42,7 @@ #include #include #include +#include #include "gz/gui/Application.hh" #include "gz/gui/Conversions.hh" @@ -59,12 +64,23 @@ class gz::gui::plugins::GzRenderer::Implementation /// \brief Flag to indicate if drop event is dirty public: bool dropDirty{false}; - /// \brief Mouse event + /// \brief Current mouse event public: common::MouseEvent mouseEvent; + /// \brief A list of mouse events + public: std::list mouseEvents; + /// \brief Key event public: common::KeyEvent keyEvent; + /// \brief Max number of mouse events to store in the queue. + /// These events are then propagated to other gui plugins. A queue is used + /// instead of just keeping the latest mouse event so that we can capture + /// important events like mouse presses. However we keep the queue size + /// small on purpose so that we do not flood other gui plugins with events + /// that may be outdated. + public: const unsigned int kMaxMouseEventSize = 5u; + /// \brief Mutex to protect mouse events public: std::mutex mutex; @@ -329,6 +345,28 @@ void GzRenderer::Render(RenderSync *_renderSync) // update and render to texture this->dataPtr->camera->Update(); + if (!this->cameraViewController.empty()) + { + std::string viewControlService = "/gui/camera/view_control"; + transport::Node node; + std::function cb = + [&](const msgs::Boolean &/*_rep*/, const bool _result) + { + if (!_result) + { + // LCOV_EXCL_START + gzerr << "Error setting view controller. Check if the View Angle GUI " + "plugin is loaded." << std::endl; + // LCOV_EXCL_STOP + } + this->cameraViewController = ""; + }; + + msgs::StringMsg req; + req.set_data(this->cameraViewController); + node.Request(viewControlService, req, cb); + } + if (gz::gui::App()) { gz::gui::App()->sendEvent( @@ -342,14 +380,21 @@ void GzRenderer::Render(RenderSync *_renderSync) void GzRenderer::HandleMouseEvent() { std::lock_guard lock(this->dataPtr->mutex); + for (const auto &e : this->dataPtr->mouseEvents) + { + this->dataPtr->mouseEvent = e; + + this->BroadcastDrag(); + this->BroadcastMousePress(); + this->BroadcastLeftClick(); + this->BroadcastRightClick(); + this->BroadcastScroll(); + this->BroadcastKeyPress(); + this->BroadcastKeyRelease(); + } + this->dataPtr->mouseEvents.clear(); + this->BroadcastHoverPos(); - this->BroadcastDrag(); - this->BroadcastMousePress(); - this->BroadcastLeftClick(); - this->BroadcastRightClick(); - this->BroadcastScroll(); - this->BroadcastKeyPress(); - this->BroadcastKeyRelease(); this->BroadcastDrop(); this->dataPtr->mouseDirty = false; } @@ -423,8 +468,6 @@ void GzRenderer::BroadcastDrag() events::DragOnScene dragEvent(this->dataPtr->mouseEvent); App()->sendEvent(App()->findChild(), &dragEvent); - - this->dataPtr->mouseDirty = false; } ///////////////////////////////////////////////// @@ -445,8 +488,6 @@ void GzRenderer::BroadcastLeftClick() events::LeftClickOnScene leftClickOnSceneEvent(this->dataPtr->mouseEvent); App()->sendEvent(App()->findChild(), &leftClickOnSceneEvent); - - this->dataPtr->mouseDirty = false; } ///////////////////////////////////////////////// @@ -467,8 +508,6 @@ void GzRenderer::BroadcastRightClick() events::RightClickOnScene rightClickOnSceneEvent(this->dataPtr->mouseEvent); App()->sendEvent(App()->findChild(), &rightClickOnSceneEvent); - - this->dataPtr->mouseDirty = false; } ///////////////////////////////////////////////// @@ -482,8 +521,6 @@ void GzRenderer::BroadcastMousePress() events::MousePressOnScene event(this->dataPtr->mouseEvent); App()->sendEvent(App()->findChild(), &event); - - this->dataPtr->mouseDirty = false; } ///////////////////////////////////////////////// @@ -497,8 +534,6 @@ void GzRenderer::BroadcastScroll() events::ScrollOnScene scrollOnSceneEvent(this->dataPtr->mouseEvent); App()->sendEvent(App()->findChild(), &scrollOnSceneEvent); - - this->dataPtr->mouseDirty = false; } ///////////////////////////////////////////////// @@ -548,9 +583,9 @@ std::string GzRenderer::Initialize() if (!this->engineName.empty() && loadedEngines.front() != this->engineName) { gzwarn << "Failed to load engine [" << this->engineName - << "]. Using engine [" << loadedEngines.front() - << "], which is already loaded. Currently only one engine is " - << "supported at a time." << std::endl; + << "]. Using engine [" << loadedEngines.front() + << "], which is already loaded. Currently only one engine is " + << "supported at a time." << std::endl; } this->engineName = loadedEngines.front(); engine = rendering::engine(loadedEngines.front()); @@ -676,7 +711,9 @@ void GzRenderer::NewDropEvent(const std::string &_dropText, void GzRenderer::NewMouseEvent(const common::MouseEvent &_e) { std::lock_guard lock(this->dataPtr->mutex); - this->dataPtr->mouseEvent = _e; + if (this->dataPtr->mouseEvents.size() >= this->dataPtr->kMaxMouseEventSize) + this->dataPtr->mouseEvents.pop_front(); + this->dataPtr->mouseEvents.push_back(_e); this->dataPtr->mouseDirty = true; } @@ -1116,6 +1153,14 @@ void RenderWindowItem::SetCameraHFOV(const math::Angle &_fov) this->dataPtr->renderThread->gzRenderer.cameraHFOV = _fov; } +///////////////////////////////////////////////// +void RenderWindowItem::SetCameraViewController( + const std::string &_view_controller) +{ + this->dataPtr->renderThread->gzRenderer.cameraViewController = + _view_controller; +} + ///////////////////////////////////////////////// MinimalScene::MinimalScene() : Plugin(), dataPtr(utils::MakeUniqueImpl()) @@ -1264,6 +1309,12 @@ void MinimalScene::LoadConfig(const tinyxml2::XMLElement *_pluginElem) renderWindow->SetCameraHFOV(fov); } } + + elem = _pluginElem->FirstChildElement("view_controller"); + if (nullptr != elem && nullptr != elem->GetText()) + { + renderWindow->SetCameraViewController(elem->GetText()); + } } renderWindow->SetEngineName(cmdRenderEngine); diff --git a/src/plugins/minimal_scene/MinimalScene.hh b/src/plugins/minimal_scene/MinimalScene.hh index e9c54a0d8..67a88592d 100644 --- a/src/plugins/minimal_scene/MinimalScene.hh +++ b/src/plugins/minimal_scene/MinimalScene.hh @@ -68,6 +68,8 @@ namespace plugins /// defaults to 90 /// * \ : Optional graphics API name. Valid choices are: /// 'opengl', 'metal'. Defaults to 'opengl'. + /// * \ : Set the view controller (InteractiveViewControl + /// currently supports types: ortho or orbit). class MinimalScene : public Plugin { Q_OBJECT @@ -246,6 +248,9 @@ namespace plugins /// \brief Horizontal FOV of the camera; public: math::Angle cameraHFOV = math::Angle(M_PI * 0.5); + /// \brief View controller type + public: std::string cameraViewController{""}; + /// \internal /// \brief Pointer to private data. GZ_UTILS_UNIQUE_IMPL_PTR(dataPtr) @@ -374,6 +379,10 @@ namespace plugins /// \param[in] _graphicsAPI The type of graphics API public: void SetGraphicsAPI(const rendering::GraphicsAPI& _graphicsAPI); + /// \brief Set the camera view controller + /// \param[in] _view_controller The camera view controller type to set + public: void SetCameraViewController(const std::string &_view_controller); + /// \brief Slot called when thread is ready to be started public Q_SLOTS: void Ready(); diff --git a/test/integration/minimal_scene.cc b/test/integration/minimal_scene.cc index 94e6475e0..6e1546fb2 100644 --- a/test/integration/minimal_scene.cc +++ b/test/integration/minimal_scene.cc @@ -90,12 +90,14 @@ TEST(MinimalSceneTest, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) " 5000" "" "60" + "ortho" ""; tinyxml2::XMLDocument pluginDoc; pluginDoc.Parse(pluginStr); EXPECT_TRUE(app.LoadPlugin("MinimalScene", pluginDoc.FirstChildElement("plugin"))); + EXPECT_TRUE(app.LoadPlugin("InteractiveViewControl")); // Get main window auto win = app.findChild(); @@ -130,7 +132,7 @@ TEST(MinimalSceneTest, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); QCoreApplication::processEvents(); - sleep++; + ++sleep; } EXPECT_TRUE(receivedPreRenderEvent); EXPECT_TRUE(receivedRenderEvent); @@ -157,9 +159,12 @@ TEST(MinimalSceneTest, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) EXPECT_NEAR(60, camera->HFOV().Degree(), 1e-4); + EXPECT_EQ(rendering::CameraProjectionType::CPT_ORTHOGRAPHIC, + camera->ProjectionType()); + // Cleanup auto plugins = win->findChildren(); - EXPECT_EQ(1, plugins.size()); + EXPECT_EQ(2, plugins.size()); auto pluginName = plugins[0]->CardItem()->objectName().toStdString(); EXPECT_TRUE(app.RemovePlugin(pluginName));