diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aea8e80378..cf70ca6852 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
uses: actions/checkout@v2
- name: Compile and test
id: ci
- uses: ignition-tooling/action-ignition-ci@master
+ uses: ignition-tooling/action-ignition-ci@bionic
with:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
# TODO(anyone) Enable Focal CI and fix failing tests
diff --git a/README.md b/README.md
index c1c88d49d0..090d774df9 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,10 @@
Build | Status
-- | --
-Test coverage | [![codecov](https://codecov.io/gh/ignitionrobotics/ign-gazebo/branch/master/graph/badge.svg)](https://codecov.io/gh/ignitionrobotics/ign-gazebo)
-Ubuntu Bionic | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gazebo-ci-master-bionic-amd64)](https://build.osrfoundation.org/job/ignition_gazebo-ci-master-bionic-amd64)
-Homebrew | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gazebo-ci-master-homebrew-amd64)](https://build.osrfoundation.org/job/ignition_gazebo-ci-master-homebrew-amd64)
-Windows | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gazebo-ci-master-windows7-amd64)](https://build.osrfoundation.org/job/ignition_gazebo-ci-master-windows7-amd64)
+Test coverage | [![codecov](https://codecov.io/gh/ignitionrobotics/ign-gazebo/branch/main/graph/badge.svg)](https://codecov.io/gh/ignitionrobotics/ign-gazebo)
+Ubuntu Bionic | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gazebo-ci-main-bionic-amd64)](https://build.osrfoundation.org/job/ignition_gazebo-ci-main-bionic-amd64)
+Homebrew | [![Build Status](https://build.osrfoundation.org/buildStatus/icon?job=ignition_gazebo-ci-main-homebrew-amd64)](https://build.osrfoundation.org/job/ignition_gazebo-ci-main-homebrew-amd64)
+Windows | [![Build Status](https://build.osrfoundation.org/job/ign_gazebo-ci-win/badge/icon)](https://build.osrfoundation.org/job/ign_gazebo-ci-win/)
Ignition Gazebo is an open source robotics simulator. Through Ignition Gazebo, users have access to high fidelity physics, rendering, and sensor models. Additionally, users and developers have multiple points of entry to simulation including a graphical user interface, plugins, and asynchronous message passing and services.
@@ -300,7 +300,7 @@ Follow these steps to run tests and static code analysis in your clone of this r
make codecheck
```
-See the [Writing Tests section of the contributor guide](https://github.com/ignitionrobotics/ign-gazebo/blob/master/CONTRIBUTING.md#writing-tests) for help creating or modifying tests.
+See the [Writing Tests section of the contributor guide](https://github.com/ignitionrobotics/ign-gazebo/blob/main/CONTRIBUTING.md#writing-tests) for help creating or modifying tests.
# Folder Structure
@@ -332,12 +332,12 @@ ign-gazebo
# Contributing
Please see
-[CONTRIBUTING.md](https://github.com/ignitionrobotics/ign-gazebo/blob/master/CONTRIBUTING.md).
+[CONTRIBUTING.md](https://github.com/ignitionrobotics/ign-gazebo/blob/main/CONTRIBUTING.md).
# Code of Conduct
Please see
-[CODE_OF_CONDUCT.md](https://github.com/ignitionrobotics/ign-gazebo/blob/master/CODE_OF_CONDUCT.md).
+[CODE_OF_CONDUCT.md](https://github.com/ignitionrobotics/ign-gazebo/blob/main/CODE_OF_CONDUCT.md).
# Versioning
@@ -345,4 +345,4 @@ This library uses [Semantic Versioning](https://semver.org/). Additionally, this
# License
-This library is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See also the [LICENSE](https://github.com/ignitionrobotics/ign-gazebo/blob/master/LICENSE) file.
+This library is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). See also the [LICENSE](https://github.com/ignitionrobotics/ign-gazebo/blob/main/LICENSE) file.
diff --git a/doc/architecture_design.md b/doc/architecture_design.md
index 96e8f6e7be..902e34f370 100644
--- a/doc/architecture_design.md
+++ b/doc/architecture_design.md
@@ -118,7 +118,7 @@ level and performer is only simulated at one runner at a time.
It would be convenient to be able to specify standalone programs in the SDF
file so they're loaded at the same time as the simulation. For example,
Gazebo's
-[JoyPlugin](https://github.com/osrf/gazebo/blob/master/plugins/JoyPlugin.hh)
+[JoyPlugin](https://github.com/osrf/gazebo/blob/gazebo11/plugins/JoyPlugin.hh)
is a `WorldPlugin`, but it doesn't need to access any world API, or to run
in the physics thread, or even to run in the gzserver process. However,
it was implemented as a plugin because that makes it easier to specify in
diff --git a/examples/standalone/joy_to_twist/README.md b/examples/standalone/joy_to_twist/README.md
index aacecb97b5..893bdeed1d 100644
--- a/examples/standalone/joy_to_twist/README.md
+++ b/examples/standalone/joy_to_twist/README.md
@@ -1,9 +1,9 @@
# Joy to Twist
Standalone program that subscribes to
-[ignition::msgs::Joy](https://github.com/ignitionrobotics/ign-msgs/blob/master/proto/ignition/msgs/joy.proto)
+[ignition::msgs::Joy](https://ignitionrobotics.org/api/msgs/5.6/classignition_1_1msgs_1_1Joy.html)
messages and converts publishes
-[ignition::msgs::Twist](https://github.com/ignitionrobotics/ign-msgs/blob/master/proto/ignition/msgs/twist.proto)
+[ignition::msgs::Twist](https://ignitionrobotics.org/api/msgs/5.6/classignition_1_1msgs_1_1Twist.html)
messages according to user-defined configuration.
## Build
diff --git a/examples/standalone/joystick/README.md b/examples/standalone/joystick/README.md
index 3fe88cb2c6..9e05e2e172 100644
--- a/examples/standalone/joystick/README.md
+++ b/examples/standalone/joystick/README.md
@@ -1,7 +1,7 @@
# Joystick
Standalone program that publishes
-[ignition::msgs::Joy](https://github.com/ignitionrobotics/ign-msgs/blob/master/proto/ignition/msgs/joy.proto)
+[ignition::msgs::Joy](https://ignitionrobotics.org/api/msgs/5.6/classignition_1_1msgs_1_1Joy.html)
messages from a joystick device using Ignition Transport.
The mapping of joystick buttons to fields in the message is the same as [this](http://wiki.ros.org/joy).
diff --git a/examples/worlds/ackermann_steering.sdf b/examples/worlds/ackermann_steering.sdf
new file mode 100644
index 0000000000..7dc5408ff2
--- /dev/null
+++ b/examples/worlds/ackermann_steering.sdf
@@ -0,0 +1,449 @@
+
+
+
+
+
+
+ 0.001
+ 1.0
+
+
+
+
+
+
+
+
+
+ true
+ 0 0 10 0 0 0
+ 1 1 1 1
+ 0.5 0.5 0.5 1
+
+ 1000
+ 0.9
+ 0.01
+ 0.001
+
+ -0.5 0.1 -0.9
+
+
+
+ true
+
+
+
+
+ 0 0 1
+ 100 100
+
+
+
+
+
+ 50
+
+
+
+
+
+
+
+ 0 0 1
+ 100 100
+
+
+
+ 0.8 0.8 0.8 1
+ 0.8 0.8 0.8 1
+ 0.8 0.8 0.8 1
+
+
+
+
+
+
+ 0 2 0.325 0 -0 0
+
+
+ -0.151427 -0 0.175 0 -0 0
+
+ 1.14395
+
+ 0.126164
+ 0
+ 0
+ 0.416519
+ 0
+ 0.481014
+
+
+
+
+
+ 2.01142 1 0.568726
+
+
+
+ 0.5 0.5 1.0 1
+ 0.5 0.5 1.0 1
+ 0.0 0.0 1.0 1
+
+
+
+
+
+ 2.01142 1 0.568726
+
+
+
+
+
+
+ 0.554283 0.625029 -0.025 -1.5707 0 0
+
+ 2
+
+ 0.145833
+ 0
+ 0
+ 0.145833
+ 0
+ 0.125
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+
+
+ 0.5
+ 1.0
+ 0 0 1
+
+
+
+
+
+
+
+ -0.957138 0.625029 -0.025 -1.5707 0 0
+
+ 2
+
+ 0.145833
+ 0
+ 0
+ 0.145833
+ 0
+ 0.125
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+
+
+ 0.5
+ 1.0
+ 0 0 1
+
+
+
+
+
+
+
+ 0.554282 -0.625029 -0.025 -1.5707 0 0
+
+ 2
+
+ 0.145833
+ 0
+ 0
+ 0.145833
+ 0
+ 0.125
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+
+
+ 0.5
+ 1.0
+ 0 0 1
+
+
+
+
+
+
+
+ -0.957138 -0.625029 -0.025 -1.5707 0 0
+
+ 2
+
+ 0.145833
+ 0
+ 0
+ 0.145833
+ 0
+ 0.125
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+ 0.2 0.2 0.2 1
+
+
+
+
+
+ 0.15
+ 0.3
+
+
+
+
+
+ 0.5
+ 1.0
+ 0 0 1
+
+
+
+
+
+
+
+ 0.554283 0.5 0.02 0 0 0
+
+ 0.5
+
+ 0.0153
+ 0.025
+ 0.0153
+
+
+
+ 0 0 0 0 0 0
+
+
+ 0.1
+ 0.03
+
+
+
+ 11 11 11
+ 11 11 11
+
+
+
+
+
+ 0.554283 -0.5 0.02 0 0 0
+
+ 0.5
+
+ 0.0153
+ 0.025
+ 0.0153
+
+
+
+ 0 0 0 0 0 0
+
+
+ 0.1
+ 0.03
+
+
+
+ 11 11 11
+ 11 11 11
+
+
+
+
+
+ front_left_wheel_steering_link
+ chassis
+
+ 0 0 1
+
+ -0.6
+ +0.6
+ 1.0
+ 25
+
+ 1
+
+
+
+
+ chassis
+ front_right_wheel_steering_link
+
+ 0 0 1
+
+ -0.6
+ +0.6
+ 1.0
+ 25
+
+
+
+
+
+ front_left_wheel_steering_link
+ front_left_wheel
+
+ 0 0 1
+
+ -1.79769e+308
+ 1.79769e+308
+
+
+
+
+
+ front_right_wheel_steering_link
+ front_right_wheel
+
+ 0 0 1
+
+ -1.79769e+308
+ 1.79769e+308
+
+
+
+
+
+ chassis
+ rear_left_wheel
+
+ 0 0 1
+
+ -1.79769e+308
+ 1.79769e+308
+
+
+
+
+
+ chassis
+ rear_right_wheel
+
+ 0 0 1
+
+ -1.79769e+308
+ 1.79769e+308
+
+
+
+
+
+ front_left_wheel_joint
+ rear_left_wheel_joint
+ front_right_wheel_joint
+ rear_right_wheel_joint
+ front_left_wheel_steering_joint
+ front_right_wheel_steering_joint
+ 1.0
+ 0.5
+ 1.0
+ 1.25
+ 0.3
+ -1
+ 1
+ -3
+ 3
+
+
+
+
+
+
diff --git a/src/gui/gui.config b/src/gui/gui.config
index 8f1b33a42f..c64ed1c064 100644
--- a/src/gui/gui.config
+++ b/src/gui/gui.config
@@ -90,7 +90,6 @@
- Transform control
false
0
0
@@ -130,6 +129,20 @@
+
+
+
+ false
+ 550
+ 0
+ 50
+ 50
+ floating
+ false
+ #666666
+
+
+
diff --git a/src/gui/plugins/CMakeLists.txt b/src/gui/plugins/CMakeLists.txt
index 856c3d69c2..6d7d140fe7 100644
--- a/src/gui/plugins/CMakeLists.txt
+++ b/src/gui/plugins/CMakeLists.txt
@@ -57,6 +57,8 @@ endfunction()
#
# [QT_HEADERS]: Qt headers that need to be moc'ed
#
+# [TEST_SOURCES]: Source files for unit tests.
+#
# [PUBLIC_LINK_LIBS]: Specify a list of libraries to be publicly linked.
#
# [PRIVATE_LINK_LIBS]: Specify a list of libraries to be privately linked.
@@ -64,7 +66,13 @@ endfunction()
function(gz_add_gui_plugin plugin_name)
set(options)
set(oneValueArgs)
- set(multiValueArgs SOURCES QT_HEADERS PUBLIC_LINK_LIBS PRIVATE_LINK_LIBS)
+ set(multiValueArgs
+ SOURCES
+ QT_HEADERS
+ TEST_SOURCES
+ PUBLIC_LINK_LIBS
+ PRIVATE_LINK_LIBS
+ )
cmake_parse_arguments(gz_add_gui_plugin "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
@@ -75,6 +83,26 @@ function(gz_add_gui_plugin plugin_name)
PRIVATE_LINK_LIBS ${gz_add_gui_plugin_PRIVATE_LINK_LIBS} ignition-plugin${IGN_PLUGIN_VER}::register
)
+ if(gz_add_gui_plugin_TEST_SOURCES)
+ # Plugin symbols failing to resolve on Windows:
+ # error LNK2001: unresolved external symbol
+ if(NOT WIN32)
+ ign_build_tests(TYPE UNIT
+ SOURCES
+ ${gz_add_gui_plugin_TEST_SOURCES}
+ LIB_DEPS
+ ignition-gazebo${PROJECT_VERSION_MAJOR}-gui
+ ${plugin_name}
+ INCLUDE_DIRS
+ # Used to make internal source file headers visible to the unit tests
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ # Used to make test-directory headers visible to the unit tests
+ ${PROJECT_SOURCE_DIR}
+ # Used to make test_config.h visible to the unit tests
+ ${PROJECT_BINARY_DIR})
+ endif()
+ endif()
+
install (TARGETS ${plugin_name} DESTINATION ${IGNITION_GAZEBO_GUI_PLUGIN_INSTALL_DIR})
endfunction()
@@ -85,6 +113,7 @@ add_subdirectory(align_tool)
add_subdirectory(component_inspector)
add_subdirectory(entity_tree)
add_subdirectory(grid_config)
+add_subdirectory(joint_position_controller)
add_subdirectory(lights)
add_subdirectory(playback_scrubber)
add_subdirectory(plotting)
diff --git a/src/gui/plugins/entity_tree/EntityTree.cc b/src/gui/plugins/entity_tree/EntityTree.cc
index 411df5f028..8a0ae74152 100644
--- a/src/gui/plugins/entity_tree/EntityTree.cc
+++ b/src/gui/plugins/entity_tree/EntityTree.cc
@@ -328,7 +328,9 @@ void EntityTree::Update(const UpdateInfo &, EntityComponentManager &_ecm)
Q_ARG(QString, entityType(_entity, _ecm)));
return true;
});
- this->dataPtr->initialized = true;
+
+ if (this->dataPtr->worldEntity != kNullEntity)
+ this->dataPtr->initialized = true;
}
else
{
diff --git a/src/gui/plugins/joint_position_controller/CMakeLists.txt b/src/gui/plugins/joint_position_controller/CMakeLists.txt
new file mode 100644
index 0000000000..5b61f0b239
--- /dev/null
+++ b/src/gui/plugins/joint_position_controller/CMakeLists.txt
@@ -0,0 +1,8 @@
+gz_add_gui_plugin(JointPositionController
+ SOURCES
+ JointPositionController.cc
+ QT_HEADERS
+ JointPositionController.hh
+ TEST_SOURCES
+ JointPositionController_TEST.cc
+)
diff --git a/src/gui/plugins/joint_position_controller/Joint.qml b/src/gui/plugins/joint_position_controller/Joint.qml
new file mode 100644
index 0000000000..ffd803c6bc
--- /dev/null
+++ b/src/gui/plugins/joint_position_controller/Joint.qml
@@ -0,0 +1,111 @@
+/*
+ * 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.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import "qrc:/JointPositionController"
+import "qrc:/qml"
+
+Rectangle {
+ id: joint
+ height: slider.height
+ width: jointPositionController.width
+ color: index % 2 == 0 ? lightGrey : darkGrey
+
+ // Position target value
+ property double targetValue: 0.0
+
+ // Horizontal margins
+ property int margin: 15
+
+ Connections {
+ target: joint
+ onTargetValueChanged: {
+ jointPositionController.onCommand(model.name, joint.targetValue);
+ }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ height: parent.height
+ width: margin
+ }
+
+ Text {
+ text: model.name
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredWidth: 100
+ elide: Text.ElideRight
+ ToolTip {
+ visible: ma.containsMouse
+ delay: Qt.styleHints.mousePressAndHoldInterval
+ text: model.name
+ y: -30
+ enter: null
+ exit: null
+ }
+ MouseArea {
+ id: ma
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.RightButton
+ }
+ }
+
+ IgnSpinBox {
+ id: spin
+ value: spin.activeFocus ? joint.targetValue : model.value
+ minimumValue: model.min
+ maximumValue: model.max
+ decimals: 2
+ stepSize: 0.1
+ onEditingFinished: {
+ joint.targetValue = spin.value
+ }
+ }
+
+ Text {
+ text: model.min.toFixed(2)
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ Slider {
+ id: slider
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ from: model.min
+ to: model.max
+ value: slider.activeFocus ? joint.targetValue : model.value
+ onMoved: {
+ joint.targetValue = slider.value
+ }
+ }
+
+ Text {
+ text: model.max.toFixed(2)
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ Item {
+ height: parent.height
+ width: margin
+ }
+ }
+}
diff --git a/src/gui/plugins/joint_position_controller/JointPositionController.cc b/src/gui/plugins/joint_position_controller/JointPositionController.cc
new file mode 100644
index 0000000000..98caba34e4
--- /dev/null
+++ b/src/gui/plugins/joint_position_controller/JointPositionController.cc
@@ -0,0 +1,432 @@
+/*
+ * 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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ignition/gazebo/components/Joint.hh"
+#include "ignition/gazebo/components/JointAxis.hh"
+#include "ignition/gazebo/components/JointPosition.hh"
+#include "ignition/gazebo/components/JointType.hh"
+#include "ignition/gazebo/components/Model.hh"
+#include "ignition/gazebo/components/Name.hh"
+#include "ignition/gazebo/components/ParentEntity.hh"
+#include "ignition/gazebo/EntityComponentManager.hh"
+#include "ignition/gazebo/gui/GuiEvents.hh"
+
+#include "JointPositionController.hh"
+
+namespace ignition::gazebo::gui
+{
+ class JointPositionControllerPrivate
+ {
+ /// \brief Model holding all the joints.
+ public: JointsModel jointsModel;
+
+ /// \brief Model entity being controller.
+ public: Entity modelEntity{kNullEntity};
+
+ /// \brief Name of the model
+ public: QString modelName{"No model selected"};
+
+ /// \brief Whether currently locked on a given entity
+ public: bool locked{false};
+
+ /// \brief Transport node for making command requests
+ public: transport::Node node;
+
+ /// \brief Whether the initial model set from XML has been setup.
+ public: bool xmlModelInitialized{true};
+ };
+}
+
+using namespace ignition;
+using namespace ignition::gazebo;
+using namespace ignition::gazebo::gui;
+
+/////////////////////////////////////////////////
+JointsModel::JointsModel() : QStandardItemModel()
+{
+}
+
+/////////////////////////////////////////////////
+QStandardItem *JointsModel::AddJoint(Entity _entity)
+{
+ IGN_PROFILE_THREAD_NAME("Qt thread");
+ IGN_PROFILE("JointsModel::AddJoint");
+
+ auto itemIt = this->items.find(_entity);
+
+ // Existing item
+ if (itemIt != this->items.end())
+ {
+ return itemIt->second;
+ }
+
+ // New joint item
+ auto item = new QStandardItem(QString::number(_entity));
+
+ this->invisibleRootItem()->appendRow(item);
+ this->items[_entity] = item;
+ return item;
+}
+
+/////////////////////////////////////////////////
+void JointsModel::RemoveJoint(Entity _entity)
+{
+ IGN_PROFILE_THREAD_NAME("Qt thread");
+ IGN_PROFILE("JointsModel::RemoveJoint");
+
+ auto itemIt = this->items.find(_entity);
+
+ // Existing item
+ if (itemIt != this->items.end())
+ {
+ this->invisibleRootItem()->removeRow(itemIt->second->row());
+ this->items.erase(_entity);
+ }
+}
+
+/////////////////////////////////////////////////
+void JointsModel::Clear()
+{
+ IGN_PROFILE_THREAD_NAME("Qt thread");
+ IGN_PROFILE("JointsModel::Clear");
+
+ this->invisibleRootItem()->removeRows(0, this->rowCount());
+ this->items.clear();
+}
+
+/////////////////////////////////////////////////
+QHash JointsModel::roleNames() const
+{
+ return JointsModel::RoleNames();
+}
+
+/////////////////////////////////////////////////
+QHash JointsModel::RoleNames()
+{
+ return {std::pair(100, "entity"),
+ std::pair(101, "name"),
+ std::pair(102, "min"),
+ std::pair(103, "max"),
+ std::pair(104, "value")};
+}
+
+/////////////////////////////////////////////////
+JointPositionController::JointPositionController()
+ : GuiSystem(), dataPtr(std::make_unique())
+{
+ qRegisterMetaType("Entity");
+}
+
+/////////////////////////////////////////////////
+JointPositionController::~JointPositionController() = default;
+
+/////////////////////////////////////////////////
+void JointPositionController::LoadConfig(
+ const tinyxml2::XMLElement *_pluginElem)
+{
+ if (this->title.empty())
+ this->title = "Joint position controller";
+
+ if (_pluginElem)
+ {
+ if (auto elem = _pluginElem->FirstChildElement("model_name"))
+ {
+ this->dataPtr->modelName = QString::fromStdString(elem->GetText());
+ // If model name isn't set, initialization is not complete yet.
+ this->dataPtr->xmlModelInitialized = false;
+ }
+ }
+
+ ignition::gui::App()->findChild<
+ ignition::gui::MainWindow *>()->installEventFilter(this);
+
+ // Connect model
+ this->Context()->setContextProperty(
+ "JointsModel", &this->dataPtr->jointsModel);
+ this->dataPtr->jointsModel.setParent(this);
+}
+
+//////////////////////////////////////////////////
+void JointPositionController::Update(const UpdateInfo &,
+ EntityComponentManager &_ecm)
+{
+ IGN_PROFILE("JointPositionController::Update");
+
+ if (!this->dataPtr->xmlModelInitialized)
+ {
+ auto entity = _ecm.EntityByComponents(
+ components::Name(this->dataPtr->modelName.toStdString()));
+
+ // Don't initialize until we get the entity
+ if (entity == kNullEntity)
+ return;
+
+ this->SetModelEntity(entity);
+ this->SetLocked(true);
+ this->dataPtr->xmlModelInitialized = true;
+ ignmsg << "Controller locked on [" << this->dataPtr->modelName.toStdString()
+ << "]" << std::endl;
+ }
+
+ if (this->dataPtr->modelEntity == kNullEntity ||
+ nullptr == _ecm.Component(
+ this->dataPtr->modelEntity))
+ {
+ QMetaObject::invokeMethod(&this->dataPtr->jointsModel,
+ "Clear",
+ Qt::QueuedConnection);
+ this->SetModelName("No model selected");
+ this->SetLocked(false);
+ return;
+ }
+
+ this->SetModelName(QString::fromStdString(
+ _ecm.ComponentData(
+ this->dataPtr->modelEntity).value()));
+
+ auto jointEntities = _ecm.EntitiesByComponents(components::Joint(),
+ components::ParentEntity(this->dataPtr->modelEntity));
+
+ // List all joints
+ for (const auto &jointEntity : jointEntities)
+ {
+ auto typeComp = _ecm.Component(jointEntity);
+ if (nullptr == typeComp)
+ {
+ ignerr << "Joint [" << jointEntity << "] missing type" << std::endl;
+ continue;
+ }
+
+ if (typeComp->Data() == sdf::JointType::INVALID ||
+ typeComp->Data() == sdf::JointType::BALL ||
+ typeComp->Data() == sdf::JointType::FIXED)
+ {
+ continue;
+ }
+
+ // Get joint item
+ bool newItem{false};
+ QStandardItem *item;
+ auto itemIt = this->dataPtr->jointsModel.items.find(jointEntity);
+ if (itemIt != this->dataPtr->jointsModel.items.end())
+ {
+ item = itemIt->second;
+ }
+ // Add joint to list
+ else
+ {
+ // TODO(louise) Blocking here is not the best idea
+ QMetaObject::invokeMethod(&this->dataPtr->jointsModel,
+ "AddJoint",
+ Qt::BlockingQueuedConnection,
+ Q_RETURN_ARG(QStandardItem *, item),
+ Q_ARG(Entity, jointEntity));
+ newItem = true;
+ }
+
+ if (nullptr == item)
+ {
+ ignerr << "Failed to get item for joint [" << jointEntity << "]"
+ << std::endl;
+ continue;
+ }
+
+ if (newItem)
+ {
+ // Name
+ auto name = _ecm.ComponentData(jointEntity).value();
+ item->setData(QString::fromStdString(name),
+ JointsModel::RoleNames().key("name"));
+
+ // Limits
+ double min = -IGN_PI;
+ double max = IGN_PI;
+ auto axisComp = _ecm.Component(jointEntity);
+ if (axisComp)
+ {
+ min = axisComp->Data().Lower();
+ max = axisComp->Data().Upper();
+ }
+ item->setData(min, JointsModel::RoleNames().key("min"));
+ item->setData(max, JointsModel::RoleNames().key("max"));
+ }
+
+ // Value
+ double value = 0.0;
+ auto posComp = _ecm.Component(jointEntity);
+ if (posComp)
+ {
+ value = posComp->Data()[0];
+ }
+ item->setData(value, JointsModel::RoleNames().key("value"));
+ }
+
+ // Remove joints no longer present
+ for (auto itemIt : this->dataPtr->jointsModel.items)
+ {
+ auto jointEntity = itemIt.first;
+ if (std::find(jointEntities.begin(), jointEntities.end(), jointEntity) ==
+ jointEntities.end())
+ {
+ QMetaObject::invokeMethod(&this->dataPtr->jointsModel,
+ "RemoveJoint",
+ Qt::QueuedConnection,
+ Q_ARG(Entity, jointEntity));
+ }
+ }
+}
+
+/////////////////////////////////////////////////
+bool JointPositionController::eventFilter(QObject *_obj, QEvent *_event)
+{
+ if (!this->dataPtr->locked)
+ {
+ if (_event->type() == gazebo::gui::events::EntitiesSelected::kType)
+ {
+ auto event = reinterpret_cast(_event);
+ if (event && !event->Data().empty())
+ {
+ this->SetModelEntity(*event->Data().begin());
+ }
+ }
+
+ if (_event->type() == gazebo::gui::events::DeselectAllEntities::kType)
+ {
+ auto event = reinterpret_cast(
+ _event);
+ if (event)
+ {
+ this->SetModelEntity(kNullEntity);
+ }
+ }
+ }
+
+ // Standard event processing
+ return QObject::eventFilter(_obj, _event);
+}
+
+/////////////////////////////////////////////////
+Entity JointPositionController::ModelEntity() const
+{
+ return this->dataPtr->modelEntity;
+}
+
+/////////////////////////////////////////////////
+void JointPositionController::SetModelEntity(Entity _entity)
+{
+ this->dataPtr->modelEntity = _entity;
+ this->ModelEntityChanged();
+
+ if (this->dataPtr->modelEntity == kNullEntity)
+ {
+ this->dataPtr->modelName.clear();
+ }
+}
+
+/////////////////////////////////////////////////
+QString JointPositionController::ModelName() const
+{
+ return this->dataPtr->modelName;
+}
+
+/////////////////////////////////////////////////
+void JointPositionController::SetModelName(const QString &_modelName)
+{
+ this->dataPtr->modelName = _modelName;
+ this->ModelNameChanged();
+}
+
+/////////////////////////////////////////////////
+bool JointPositionController::Locked() const
+{
+ return this->dataPtr->locked;
+}
+
+/////////////////////////////////////////////////
+void JointPositionController::SetLocked(bool _locked)
+{
+ this->dataPtr->locked = _locked;
+ this->LockedChanged();
+}
+
+/////////////////////////////////////////////////
+void JointPositionController::OnCommand(const QString &_jointName, double _pos)
+{
+ std::string jointName = _jointName.toStdString();
+
+ ignition::msgs::Double msg;
+ msg.set_data(_pos);
+ auto topic = transport::TopicUtils::AsValidTopic("/model/" +
+ this->dataPtr->modelName.toStdString() + "/joint/" + jointName +
+ "/0/cmd_pos");
+
+ if (topic.empty())
+ {
+ ignerr << "Failed to create valid topic for joint [" << jointName << "]"
+ << std::endl;
+ return;
+ }
+
+ auto pub = this->dataPtr->node.Advertise(topic);
+ pub.Publish(msg);
+}
+
+/////////////////////////////////////////////////
+void JointPositionController::OnReset()
+{
+ for (auto itemIt : this->dataPtr->jointsModel.items)
+ {
+ auto jointName = itemIt.second->data(JointsModel::RoleNames().key("name"))
+ .toString().toStdString();
+ if (jointName.empty())
+ {
+ ignerr << "Internal error: failed to get joint name." << std::endl;
+ continue;
+ }
+
+ ignition::msgs::Double msg;
+ msg.set_data(0);
+ auto topic = transport::TopicUtils::AsValidTopic("/model/" +
+ this->dataPtr->modelName.toStdString() + "/joint/" + jointName +
+ "/0/cmd_pos");
+
+ if (topic.empty())
+ {
+ ignerr << "Failed to create valid topic for joint [" << jointName << "]"
+ << std::endl;
+ return;
+ }
+
+ auto pub = this->dataPtr->node.Advertise(topic);
+ pub.Publish(msg);
+ }
+}
+
+// Register this plugin
+IGNITION_ADD_PLUGIN(ignition::gazebo::gui::JointPositionController,
+ ignition::gui::Plugin)
diff --git a/src/gui/plugins/joint_position_controller/JointPositionController.hh b/src/gui/plugins/joint_position_controller/JointPositionController.hh
new file mode 100644
index 0000000000..3aa9ecfa57
--- /dev/null
+++ b/src/gui/plugins/joint_position_controller/JointPositionController.hh
@@ -0,0 +1,183 @@
+/*
+ * 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_JOINTPOSITIONCONTROLLER_HH_
+#define IGNITION_GAZEBO_GUI_JOINTPOSITIONCONTROLLER_HH_
+
+#include