diff --git a/CMakeLists.txt b/CMakeLists.txt index ba2a731238..9ae2a9c6b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,7 +75,7 @@ set(IGN_FUEL_TOOLS_VER ${ignition-fuel_tools4_VERSION_MAJOR}) #-------------------------------------- # Find ignition-gui -ign_find_package(ignition-gui3 REQUIRED VERSION 3.1) +ign_find_package(ignition-gui3 REQUIRED VERSION 3.4) set(IGN_GUI_VER ${ignition-gui3_VERSION_MAJOR}) ign_find_package (Qt5 COMPONENTS diff --git a/src/gui/plugins/CMakeLists.txt b/src/gui/plugins/CMakeLists.txt index 7cca6905e6..35a4d49fa4 100644 --- a/src/gui/plugins/CMakeLists.txt +++ b/src/gui/plugins/CMakeLists.txt @@ -89,6 +89,7 @@ add_subdirectory(playback_scrubber) add_subdirectory(resource_spawner) add_subdirectory(scene3d) add_subdirectory(shapes) +add_subdirectory(tape_measure) add_subdirectory(transform_control) add_subdirectory(video_recorder) add_subdirectory(view_angle) diff --git a/src/gui/plugins/scene3d/Scene3D.cc b/src/gui/plugins/scene3d/Scene3D.cc index 4321dbbfe8..1df01b31aa 100644 --- a/src/gui/plugins/scene3d/Scene3D.cc +++ b/src/gui/plugins/scene3d/Scene3D.cc @@ -55,6 +55,7 @@ #include #include +#include #include #include @@ -767,12 +768,44 @@ Entity IgnRenderer::UniqueId() void IgnRenderer::HandleMouseEvent() { std::lock_guard lock(this->dataPtr->mutex); + this->BroadcastHoverPos(); + this->BroadcastLeftClick(); this->HandleMouseContextMenu(); this->HandleModelPlacement(); this->HandleMouseTransformControl(); this->HandleMouseViewControl(); } +///////////////////////////////////////////////// +void IgnRenderer::BroadcastHoverPos() +{ + if (this->dataPtr->hoverDirty) + { + math::Vector3d pos = this->ScreenToScene(this->dataPtr->mouseHoverPos); + + ignition::gui::events::HoverToScene hoverToSceneEvent(pos); + ignition::gui::App()->sendEvent( + ignition::gui::App()->findChild(), + &hoverToSceneEvent); + } +} + +///////////////////////////////////////////////// +void IgnRenderer::BroadcastLeftClick() +{ + if (this->dataPtr->mouseEvent.Button() == common::MouseEvent::LEFT && + this->dataPtr->mouseEvent.Type() == common::MouseEvent::RELEASE && + !this->dataPtr->mouseEvent.Dragging() && this->dataPtr->mouseDirty) + { + math::Vector3d pos = this->ScreenToScene(this->dataPtr->mouseEvent.Pos()); + + ignition::gui::events::LeftClickToScene leftClickToSceneEvent(pos); + ignition::gui::App()->sendEvent( + ignition::gui::App()->findChild(), + &leftClickToSceneEvent); + } +} + ///////////////////////////////////////////////// void IgnRenderer::HandleMouseContextMenu() { diff --git a/src/gui/plugins/scene3d/Scene3D.hh b/src/gui/plugins/scene3d/Scene3D.hh index 761f37295f..48cfdcb11b 100644 --- a/src/gui/plugins/scene3d/Scene3D.hh +++ b/src/gui/plugins/scene3d/Scene3D.hh @@ -351,6 +351,12 @@ inline namespace IGNITION_GAZEBO_VERSION_NAMESPACE { /// \brief Handle model placement requests private: void HandleModelPlacement(); + /// \brief Broadcasts the currently hovered 3d scene location. + private: void BroadcastHoverPos(); + + /// \brief Broadcasts a left click within the scene + private: void BroadcastLeftClick(); + /// \brief Generate a unique entity id. /// \return The unique entity id private: Entity UniqueId(); diff --git a/src/gui/plugins/tape_measure/CMakeLists.txt b/src/gui/plugins/tape_measure/CMakeLists.txt new file mode 100644 index 0000000000..4b5322fc2a --- /dev/null +++ b/src/gui/plugins/tape_measure/CMakeLists.txt @@ -0,0 +1,6 @@ +gz_add_gui_plugin(TapeMeasure + SOURCES TapeMeasure.cc + QT_HEADERS TapeMeasure.hh + PRIVATE_LINK_LIBS + ${IGNITION-RENDERING_LIBRARIES} +) diff --git a/src/gui/plugins/tape_measure/TapeMeasure.cc b/src/gui/plugins/tape_measure/TapeMeasure.cc new file mode 100644 index 0000000000..2642c052f3 --- /dev/null +++ b/src/gui/plugins/tape_measure/TapeMeasure.cc @@ -0,0 +1,301 @@ +/* + * 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. + * +*/ + +#include "ignition/gazebo/gui/GuiEvents.hh" + +#include "TapeMeasure.hh" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ignition::gazebo +{ + class TapeMeasurePrivate + { + /// \brief Ignition communication node. + public: transport::Node node; + + /// \brief True if currently measuring, else false. + public: bool measure = false; + + /// \brief The id of the start point marker. + public: const int kStartPointId = 1; + + /// \brief The id of the end point marker. + public: const int kEndPointId = 2; + + /// \brief The id of the line marker. + public: const int kLineId = 3; + + /// \brief The id of the start or end point marker that is currently + /// being placed. This is primarily used to track the state machine of + /// the plugin. + public: int currentId = kStartPointId; + + /// \brief The location of the placed starting point of the tape measure + /// tool, only set when the user clicks to set the point. + public: ignition::math::Vector3d startPoint = + ignition::math::Vector3d::Zero; + + /// \brief The location of the placed ending point of the tape measure + /// tool, only set when the user clicks to set the point. + public: ignition::math::Vector3d endPoint = ignition::math::Vector3d::Zero; + + /// \brief The color to set the marker when hovering the mouse over the + /// scene. + public: ignition::math::Color + hoverColor{ignition::math::Color(0.2, 0.2, 0.2, 0.5)}; + + /// \brief The color to draw the marker when the user clicks to confirm + /// its location. + public: ignition::math::Color + drawColor{ignition::math::Color(0.2, 0.2, 0.2, 1.0)}; + + /// \brief A set of the currently placed markers. Used to make sure a + /// non-existent marker is not deleted. + public: std::unordered_set placedMarkers; + + /// \brief The current distance between the two points. This distance + /// is updated as the user hovers the end point as well. + public: double distance = 0.0; + + /// \brief The namespace that the markers for this plugin are placed in. + public: std::string ns = "tape_measure"; + }; +} + +using namespace ignition; +using namespace gazebo; + +///////////////////////////////////////////////// +TapeMeasure::TapeMeasure() + : ignition::gui::Plugin(), + dataPtr(std::make_unique()) +{ +} + +///////////////////////////////////////////////// +TapeMeasure::~TapeMeasure() = default; + +///////////////////////////////////////////////// +void TapeMeasure::LoadConfig(const tinyxml2::XMLElement *) +{ + if (this->title.empty()) + this->title = "Tape measure"; + + ignition::gui::App()->findChild + ()->installEventFilter(this); + ignition::gui::App()->findChild + ()->QuickWindow()->installEventFilter(this); +} + +///////////////////////////////////////////////// +void TapeMeasure::OnMeasure() +{ + this->Measure(); +} + +///////////////////////////////////////////////// +void TapeMeasure::Measure() +{ + this->Reset(); + this->dataPtr->measure = true; + QGuiApplication::setOverrideCursor(Qt::CrossCursor); +} + +///////////////////////////////////////////////// +void TapeMeasure::OnReset() +{ + this->Reset(); +} + +///////////////////////////////////////////////// +void TapeMeasure::Reset() +{ + this->DeleteMarker(this->dataPtr->kStartPointId); + this->DeleteMarker(this->dataPtr->kEndPointId); + this->DeleteMarker(this->dataPtr->kLineId); + + this->dataPtr->currentId = this->dataPtr->kStartPointId; + this->dataPtr->startPoint = ignition::math::Vector3d::Zero; + this->dataPtr->endPoint = ignition::math::Vector3d::Zero; + this->dataPtr->distance = 0.0; + this->dataPtr->measure = false; + this->newDistance(); + QGuiApplication::restoreOverrideCursor(); +} + +///////////////////////////////////////////////// +double TapeMeasure::Distance() +{ + return this->dataPtr->distance; +} + +///////////////////////////////////////////////// +void TapeMeasure::DeleteMarker(int _id) +{ + if (this->dataPtr->placedMarkers.find(_id) == + this->dataPtr->placedMarkers.end()) + return; + + // Delete the previously created marker + ignition::msgs::Marker markerMsg; + markerMsg.set_ns(this->dataPtr->ns); + markerMsg.set_id(_id); + markerMsg.set_action(ignition::msgs::Marker::DELETE_MARKER); + this->dataPtr->node.Request("/marker", markerMsg); + this->dataPtr->placedMarkers.erase(_id); +} + +///////////////////////////////////////////////// +void TapeMeasure::DrawPoint(int _id, + ignition::math::Vector3d &_point, ignition::math::Color &_color) +{ + this->DeleteMarker(_id); + + ignition::msgs::Marker markerMsg; + markerMsg.set_ns(this->dataPtr->ns); + markerMsg.set_id(_id); + markerMsg.set_action(ignition::msgs::Marker::ADD_MODIFY); + markerMsg.set_type(ignition::msgs::Marker::SPHERE); + ignition::msgs::Set(markerMsg.mutable_material()->mutable_ambient(), _color); + ignition::msgs::Set(markerMsg.mutable_material()->mutable_diffuse(), _color); + ignition::msgs::Set(markerMsg.mutable_scale(), + ignition::math::Vector3d(0.1, 0.1, 0.1)); + ignition::msgs::Set(markerMsg.mutable_pose(), + ignition::math::Pose3d(_point.X(), _point.Y(), _point.Z(), 0, 0, 0)); + + this->dataPtr->node.Request("/marker", markerMsg); + this->dataPtr->placedMarkers.insert(_id); +} + +///////////////////////////////////////////////// +void TapeMeasure::DrawLine(int _id, ignition::math::Vector3d &_startPoint, + ignition::math::Vector3d &_endPoint, ignition::math::Color &_color) +{ + this->DeleteMarker(_id); + + ignition::msgs::Marker markerMsg; + markerMsg.set_ns(this->dataPtr->ns); + markerMsg.set_id(_id); + markerMsg.set_action(ignition::msgs::Marker::ADD_MODIFY); + markerMsg.set_type(ignition::msgs::Marker::LINE_LIST); + ignition::msgs::Set(markerMsg.mutable_material()->mutable_ambient(), _color); + ignition::msgs::Set(markerMsg.mutable_material()->mutable_diffuse(), _color); + ignition::msgs::Set(markerMsg.add_point(), _startPoint); + ignition::msgs::Set(markerMsg.add_point(), _endPoint); + + this->dataPtr->node.Request("/marker", markerMsg); + this->dataPtr->placedMarkers.insert(_id); +} + +///////////////////////////////////////////////// +bool TapeMeasure::eventFilter(QObject *_obj, QEvent *_event) +{ + if (_event->type() == ignition::gui::events::HoverToScene::kType) + { + auto hoverToSceneEvent = + reinterpret_cast(_event); + + // This event is called in Scene3d's RenderThread, so it's safe to make + // rendering calls here + if (this->dataPtr->measure && hoverToSceneEvent) + { + ignition::math::Vector3d point = hoverToSceneEvent->Point(); + this->DrawPoint(this->dataPtr->currentId, point, + this->dataPtr->hoverColor); + + // If the user is currently choosing the end point, draw the connecting + // line and update the new distance. + if (this->dataPtr->currentId == this->dataPtr->kEndPointId) + { + this->DrawLine(this->dataPtr->kLineId, this->dataPtr->startPoint, + point, this->dataPtr->hoverColor); + this->dataPtr->distance = this->dataPtr->startPoint.Distance(point); + this->newDistance(); + } + } + } + else if (_event->type() == ignition::gui::events::LeftClickToScene::kType) + { + auto leftClickToSceneEvent = + reinterpret_cast(_event); + + // This event is called in Scene3d's RenderThread, so it's safe to make + // rendering calls here + if (this->dataPtr->measure && leftClickToSceneEvent) + { + ignition::math::Vector3d point = leftClickToSceneEvent->Point(); + this->DrawPoint(this->dataPtr->currentId, point, + this->dataPtr->drawColor); + // If the user is placing the start point, update its position + if (this->dataPtr->currentId == this->dataPtr->kStartPointId) + { + this->dataPtr->startPoint = point; + } + // If the user is placing the end point, update the end position, + // end the measurement state, and update the draw line and distance + else + { + this->dataPtr->endPoint = point; + this->dataPtr->measure = false; + this->DrawLine(this->dataPtr->kLineId, this->dataPtr->startPoint, + this->dataPtr->endPoint, this->dataPtr->drawColor); + this->dataPtr->distance = + this->dataPtr->startPoint.Distance(this->dataPtr->endPoint); + this->newDistance(); + QGuiApplication::restoreOverrideCursor(); + } + this->dataPtr->currentId = this->dataPtr->kEndPointId; + } + } + else if (_event->type() == QEvent::KeyPress) + { + QKeyEvent *keyEvent = static_cast(_event); + if (keyEvent && keyEvent->key() == Qt::Key_M) + { + this->Reset(); + this->Measure(); + } + } + else if (_event->type() == QEvent::KeyRelease) + { + QKeyEvent *keyEvent = static_cast(_event); + if (keyEvent && keyEvent->key() == Qt::Key_Escape && + this->dataPtr->measure) + { + this->Reset(); + } + } + return QObject::eventFilter(_obj, _event); +} + +// Register this plugin +IGNITION_ADD_PLUGIN(ignition::gazebo::TapeMeasure, + ignition::gui::Plugin) diff --git a/src/gui/plugins/tape_measure/TapeMeasure.hh b/src/gui/plugins/tape_measure/TapeMeasure.hh new file mode 100644 index 0000000000..0da8719f2b --- /dev/null +++ b/src/gui/plugins/tape_measure/TapeMeasure.hh @@ -0,0 +1,105 @@ +/* + * 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_TAPEMEASURE_HH_ +#define IGNITION_GAZEBO_GUI_TAPEMEASURE_HH_ + +#include + +#include +#include +#include + +namespace ignition +{ +namespace gazebo +{ + class TapeMeasurePrivate; + + /// \brief Provides buttons for the tape measure tool. + class TapeMeasure : public ignition::gui::Plugin + { + Q_OBJECT + + /// \brief Constructor + public: TapeMeasure(); + + /// \brief Destructor + public: ~TapeMeasure() override; + + // Documentation inherited + public: void LoadConfig(const tinyxml2::XMLElement *_pluginElem) override; + + /// \brief Deletes the marker with the provided id within the + /// "tape_measure" namespace. + /// \param[in] _id The id of the marker + public: void DeleteMarker(int _id); + + /// \brief Resets all of the relevant data for this plugin. Called when + /// the user clicks the reset button and when the user starts a new + /// measurement. + public: void Reset(); + + /// \brief Starts a new measurement. Erases any previous measurement in + /// progress or already made. + public: void Measure(); + + /// \brief Draws a point marker. Called to display the start and end + /// point of the tape measure. + /// \param[in] _id The id of the marker + /// \param[in] _point The x, y, z coordinates of where to place the marker + /// \param[in] _color The rgba color to set the marker + public: void DrawPoint(int _id, + ignition::math::Vector3d &_point, + ignition::math::Color &_color); + + /// \brief Draws a line marker. Called to display the line between the + /// start and end point of the tape measure. + /// \param[in] _id The id of the marker + /// \param[in] _startPoint The x, y, z coordinates of the line start point + /// \param[in] _endPoint The x, y, z coordinates of the line end point + /// \param[in] _color The rgba color to set the marker + public: void DrawLine(int _id, + ignition::math::Vector3d &_startPoint, + ignition::math::Vector3d &_endPoint, + ignition::math::Color &_color); + + /// \brief Callback in Qt thread when the new measurement button is + /// clicked. + public slots: void OnMeasure(); + + /// \brief Callback in Qt thread when the reset button is clicked. + public slots: void OnReset(); + + /// \brief Callback in Qt thread to get the distance to display in the + /// gui window. + /// \return The distance between the start and end point of the measurement + public slots: double Distance(); + + // Documentation inherited + protected: bool eventFilter(QObject *_obj, QEvent *_event) override; + + /// \brief Signal fired when a new tape measure distance is set. + signals: void newDistance(); + + /// \internal + /// \brief Pointer to private data. + private: std::unique_ptr dataPtr; + }; +} +} + +#endif diff --git a/src/gui/plugins/tape_measure/TapeMeasure.qml b/src/gui/plugins/tape_measure/TapeMeasure.qml new file mode 100644 index 0000000000..29362bcf09 --- /dev/null +++ b/src/gui/plugins/tape_measure/TapeMeasure.qml @@ -0,0 +1,100 @@ +/* + * 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. + * +*/ +import QtQuick 2.9 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.2 +import QtQuick.Controls.Material.impl 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 +import "qrc:/qml" + +ToolBar { + id: tapeMeasure + Layout.minimumWidth: 250 + Layout.minimumHeight: 100 + + property var distance: 0.0 + + function updateDistance() { + distance = TapeMeasure.Distance(); + } + + Connections { + target: TapeMeasure + onNewDistance: { + updateDistance(); + } + } + + background: Rectangle { + color: "transparent" + } + + ButtonGroup { + id: group + } + + RowLayout { + spacing: 1 + ToolButton { + id: select + checkable: true + checked: true + ButtonGroup.group: group + ToolTip.text: "Tape Measure Tool" + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + contentItem: Image { + fillMode: Image.Pad + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + source: "tape_measure.png" + sourceSize.width: 36; + sourceSize.height: 36; + } + onClicked: { + TapeMeasure.OnMeasure(); + } + } + ToolButton { + id: reset + checkable: true + checked: true + ButtonGroup.group: group + ToolTip.text: "Reset measurement" + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + contentItem: Image { + fillMode: Image.Pad + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + source: "trashcan.png" + sourceSize.width: 36; + sourceSize.height: 36; + } + onClicked: { + TapeMeasure.OnReset(); + } + } + Text { + text: qsTr(" Distance (m): " + distance.toFixed(3)) + font.pointSize: 14 + color: Material.theme == Material.Light ? "#444444" : "#bbbbbb" + } + } +} diff --git a/src/gui/plugins/tape_measure/TapeMeasure.qrc b/src/gui/plugins/tape_measure/TapeMeasure.qrc new file mode 100644 index 0000000000..e87212d276 --- /dev/null +++ b/src/gui/plugins/tape_measure/TapeMeasure.qrc @@ -0,0 +1,7 @@ + + + TapeMeasure.qml + tape_measure.png + trashcan.png + + diff --git a/src/gui/plugins/tape_measure/tape_measure.png b/src/gui/plugins/tape_measure/tape_measure.png new file mode 100644 index 0000000000..228e583442 Binary files /dev/null and b/src/gui/plugins/tape_measure/tape_measure.png differ diff --git a/src/gui/plugins/tape_measure/trashcan.png b/src/gui/plugins/tape_measure/trashcan.png new file mode 100644 index 0000000000..62edd38a51 Binary files /dev/null and b/src/gui/plugins/tape_measure/trashcan.png differ