diff --git a/src/plugins/grid_3d/CMakeLists.txt b/src/plugins/grid_3d/CMakeLists.txt index 6ce5eb707..444b3f566 100644 --- a/src/plugins/grid_3d/CMakeLists.txt +++ b/src/plugins/grid_3d/CMakeLists.txt @@ -6,6 +6,6 @@ ign_gui_add_plugin(Grid3D TEST_SOURCES # Grid3D_TEST.cc PUBLIC_LINK_LIBS - ${IGNITION-RENDERING_LIBRARIES} + ignition-rendering${IGN_RENDERING_VER}::ignition-rendering${IGN_RENDERING_VER} ) diff --git a/src/plugins/scene3d/CMakeLists.txt b/src/plugins/scene3d/CMakeLists.txt index 469226031..5679bfcf7 100644 --- a/src/plugins/scene3d/CMakeLists.txt +++ b/src/plugins/scene3d/CMakeLists.txt @@ -3,9 +3,7 @@ ign_gui_add_plugin(Scene3D Scene3D.cc QT_HEADERS Scene3D.hh - TEST_SOURCES - # Scene3D_TEST.cc PUBLIC_LINK_LIBS - ${IGNITION-RENDERING_LIBRARIES} + ignition-rendering${IGN_RENDERING_VER}::ignition-rendering${IGN_RENDERING_VER} ) diff --git a/src/plugins/scene3d/Scene3D.cc b/src/plugins/scene3d/Scene3D.cc index 0a5d529bc..78930d35d 100644 --- a/src/plugins/scene3d/Scene3D.cc +++ b/src/plugins/scene3d/Scene3D.cc @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -207,9 +208,15 @@ namespace plugins /// \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 Mouse event public: common::MouseEvent mouseEvent; + /// \brief Key event + public: common::KeyEvent keyEvent; + /// \brief Mouse move distance since last event. public: math::Vector2d drag; @@ -222,6 +229,9 @@ namespace plugins /// \brief Camera orbit controller public: rendering::OrbitViewController viewControl; + /// \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; @@ -885,6 +895,15 @@ void IgnRenderer::Render() void IgnRenderer::HandleMouseEvent() { std::lock_guard lock(this->dataPtr->mutex); + this->BroadcastHoverPos(); + this->BroadcastLeftClick(); + this->BroadcastRightClick(); + this->HandleMouseViewControl(); +} + +///////////////////////////////////////////////// +void IgnRenderer::HandleMouseViewControl() +{ if (!this->dataPtr->mouseDirty) return; @@ -939,6 +958,106 @@ void IgnRenderer::HandleMouseEvent() this->dataPtr->mouseDirty = false; } +//////////////////////////////////////////////// +void IgnRenderer::HandleKeyPress(QKeyEvent *_e) +{ + if (_e->isAutoRepeat()) + return; + + std::lock_guard lock(this->dataPtr->mutex); + + this->dataPtr->keyEvent.SetKey(_e->key()); + this->dataPtr->keyEvent.SetText(_e->text().toStdString()); + + this->dataPtr->keyEvent.SetControl( + (_e->modifiers() & Qt::ControlModifier)); + this->dataPtr->keyEvent.SetShift( + (_e->modifiers() & Qt::ShiftModifier)); + this->dataPtr->keyEvent.SetAlt( + (_e->modifiers() & Qt::AltModifier)); + + this->dataPtr->mouseEvent.SetControl(this->dataPtr->keyEvent.Control()); + this->dataPtr->mouseEvent.SetShift(this->dataPtr->keyEvent.Shift()); + this->dataPtr->mouseEvent.SetAlt(this->dataPtr->keyEvent.Alt()); + this->dataPtr->keyEvent.SetType(common::KeyEvent::PRESS); +} + +//////////////////////////////////////////////// +void IgnRenderer::HandleKeyRelease(QKeyEvent *_e) +{ + if (_e->isAutoRepeat()) + return; + + std::lock_guard lock(this->dataPtr->mutex); + + this->dataPtr->keyEvent.SetKey(0); + + this->dataPtr->keyEvent.SetControl( + (_e->modifiers() & Qt::ControlModifier) + && (_e->key() != Qt::Key_Control)); + this->dataPtr->keyEvent.SetShift( + (_e->modifiers() & Qt::ShiftModifier) + && (_e->key() != Qt::Key_Shift)); + this->dataPtr->keyEvent.SetAlt( + (_e->modifiers() & Qt::AltModifier) + && (_e->key() != Qt::Key_Alt)); + + this->dataPtr->mouseEvent.SetControl(this->dataPtr->keyEvent.Control()); + this->dataPtr->mouseEvent.SetShift(this->dataPtr->keyEvent.Shift()); + this->dataPtr->mouseEvent.SetAlt(this->dataPtr->keyEvent.Alt()); + this->dataPtr->keyEvent.SetType(common::KeyEvent::RELEASE); +} + +///////////////////////////////////////////////// +void IgnRenderer::BroadcastHoverPos() +{ + if (!this->dataPtr->hoverDirty) + return; + + auto pos = this->ScreenToScene(this->dataPtr->mouseHoverPos); + + events::HoverToScene hoverToSceneEvent(pos); + App()->sendEvent(App()->findChild(), &hoverToSceneEvent); +} + +///////////////////////////////////////////////// +void IgnRenderer::BroadcastLeftClick() +{ + if (!this->dataPtr->mouseDirty) + return; + + if (this->dataPtr->mouseEvent.Dragging()) + return; + + if (this->dataPtr->mouseEvent.Button() != common::MouseEvent::LEFT || + this->dataPtr->mouseEvent.Type() != common::MouseEvent::RELEASE) + return; + + auto pos = this->ScreenToScene(this->dataPtr->mouseEvent.Pos()); + + events::LeftClickToScene leftClickToSceneEvent(pos); + App()->sendEvent(App()->findChild(), &leftClickToSceneEvent); +} + +///////////////////////////////////////////////// +void IgnRenderer::BroadcastRightClick() +{ + if (!this->dataPtr->mouseDirty) + return; + + if (this->dataPtr->mouseEvent.Dragging()) + return; + + if (this->dataPtr->mouseEvent.Button() != common::MouseEvent::RIGHT || + this->dataPtr->mouseEvent.Type() != common::MouseEvent::RELEASE) + return; + + auto pos = this->ScreenToScene(this->dataPtr->mouseEvent.Pos()); + + events::RightClickToScene rightClickToSceneEvent(pos); + App()->sendEvent(App()->findChild(), &rightClickToSceneEvent); +} + ///////////////////////////////////////////////// void IgnRenderer::Initialize() { @@ -1016,6 +1135,14 @@ void IgnRenderer::Destroy() } } +///////////////////////////////////////////////// +void IgnRenderer::NewHoverEvent(const math::Vector2i &_hoverPos) +{ + std::lock_guard lock(this->dataPtr->mutex); + this->dataPtr->mouseHoverPos = _hoverPos; + this->dataPtr->hoverDirty = true; +} + ///////////////////////////////////////////////// void IgnRenderer::NewMouseEvent(const common::MouseEvent &_e, const math::Vector2d &_drag) @@ -1455,6 +1582,12 @@ void Scene3D::LoadConfig(const tinyxml2::XMLElement *_pluginElem) } } +///////////////////////////////////////////////// +void RenderWindowItem::OnHovered(const ignition::math::Vector2i &_hoverPos) +{ + this->dataPtr->renderThread->ignRenderer.NewHoverEvent(_hoverPos); +} + ///////////////////////////////////////////////// void RenderWindowItem::mousePressEvent(QMouseEvent *_e) { @@ -1505,23 +1638,57 @@ void RenderWindowItem::wheelEvent(QWheelEvent *_e) this->dataPtr->mouseEvent, math::Vector2d(scroll, scroll)); } -/////////////////////////////////////////////////// -// void Scene3D::resizeEvent(QResizeEvent *_e) -// { -// if (this->dataPtr->renderWindow) -// { -// this->dataPtr->renderWindow->OnResize(_e->size().width(), -// _e->size().height()); -// } -// -// if (this->dataPtr->camera) -// { -// this->dataPtr->camera->SetAspectRatio( -// static_cast(this->width()) / this->height()); -// this->dataPtr->camera->SetHFOV(M_PI * 0.5); -// } -// } -// +//////////////////////////////////////////////// +void RenderWindowItem::HandleKeyPress(QKeyEvent *_e) +{ + this->dataPtr->renderThread->ignRenderer.HandleKeyPress(_e); +} + +//////////////////////////////////////////////// +void RenderWindowItem::HandleKeyRelease(QKeyEvent *_e) +{ + this->dataPtr->renderThread->ignRenderer.HandleKeyRelease(_e); +} + +///////////////////////////////////////////////// +bool Scene3D::eventFilter(QObject *_obj, QEvent *_event) +{ + if (_event->type() == QEvent::KeyPress) + { + QKeyEvent *keyEvent = static_cast(_event); + if (keyEvent) + { + auto renderWindow = this->PluginItem()->findChild(); + renderWindow->HandleKeyPress(keyEvent); + } + } + else if (_event->type() == QEvent::KeyRelease) + { + QKeyEvent *keyEvent = static_cast(_event); + if (keyEvent) + { + auto renderWindow = this->PluginItem()->findChild(); + renderWindow->HandleKeyRelease(keyEvent); + } + } + + // Standard event processing + return QObject::eventFilter(_obj, _event); +} + +///////////////////////////////////////////////// +void Scene3D::OnHovered(int _mouseX, int _mouseY) +{ + auto renderWindow = this->PluginItem()->findChild(); + renderWindow->OnHovered({_mouseX, _mouseY}); +} + +///////////////////////////////////////////////// +void Scene3D::OnFocusWindow() +{ + auto renderWindow = this->PluginItem()->findChild(); + renderWindow->forceActiveFocus(); +} // Register this plugin IGNITION_ADD_PLUGIN(ignition::gui::plugins::Scene3D, diff --git a/src/plugins/scene3d/Scene3D.hh b/src/plugins/scene3d/Scene3D.hh index 1c51054be..db09195c9 100644 --- a/src/plugins/scene3d/Scene3D.hh +++ b/src/plugins/scene3d/Scene3D.hh @@ -68,6 +68,18 @@ namespace plugins /// \brief Destructor public: virtual ~Scene3D(); + /// \brief Callback when the mouse hovers to a new position. + /// \param[in] _mouseX x coordinate of the hovered mouse position. + /// \param[in] _mouseY y coordinate of the hovered mouse position. + public slots: void OnHovered(int _mouseX, int _mouseY); + + /// \brief Callback when the mouse enters the render window to + /// focus the window for mouse/key events + public slots: void OnFocusWindow(); + + // Documentation inherited + protected: bool eventFilter(QObject *_obj, QEvent *_event) override; + // Documentation inherited public: virtual void LoadConfig(const tinyxml2::XMLElement *_pluginElem) override; @@ -106,9 +118,33 @@ namespace plugins public: void NewMouseEvent(const common::MouseEvent &_e, const math::Vector2d &_drag = math::Vector2d::Zero); + /// \brief New hover event triggered. + /// \param[in] _hoverPos Mouse hover screen position + public: void NewHoverEvent(const math::Vector2i &_hoverPos); + + /// \brief Handle key press event for snapping + /// \param[in] _e The key event to process. + public: void HandleKeyPress(QKeyEvent *_e); + + /// \brief Handle key release event for snapping + /// \param[in] _e The key event to process. + public: void HandleKeyRelease(QKeyEvent *_e); + /// \brief Handle mouse event for view control private: void HandleMouseEvent(); + /// \brief Handle mouse event for view control + private: void HandleMouseViewControl(); + + /// \brief Broadcasts the currently hovered 3d scene location. + private: void BroadcastHoverPos(); + + /// \brief Broadcasts a left click within the scene + private: void BroadcastLeftClick(); + + /// \brief Broadcasts a right click within the scene + private: void BroadcastRightClick(); + /// \brief Retrieve the first point on a surface 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. @@ -254,9 +290,22 @@ namespace plugins /// \param[in] _topic Scene topic public: void SetSceneTopic(const std::string &_topic); + /// \brief Called when the mouse hovers to a new position. + /// \param[in] _hoverPos 2D coordinates of the hovered mouse position on + /// the render window. + public: void OnHovered(const ignition::math::Vector2i &_hoverPos); + /// \brief Slot called when thread is ready to be started public Q_SLOTS: void Ready(); + /// \brief Handle key press event for snapping + /// \param[in] _e The key event to process. + public: void HandleKeyPress(QKeyEvent *_e); + + /// \brief Handle key release event for snapping + /// \param[in] _e The key event to process. + public: void HandleKeyRelease(QKeyEvent *_e); + // Documentation inherited protected: virtual void mousePressEvent(QMouseEvent *_e) override; @@ -276,7 +325,7 @@ namespace plugins /// \param[in] _data The node transformation data. /// \return Updated node. private: QSGNode *updatePaintNode(QSGNode *_oldNode, - QQuickItem::UpdatePaintNodeData *_data) override; + QQuickItem::UpdatePaintNodeData *_data) override; /// \internal /// \brief Pointer to private data. diff --git a/src/plugins/scene3d/Scene3D.qml b/src/plugins/scene3d/Scene3D.qml index 304ccf186..cb7a399e7 100644 --- a/src/plugins/scene3d/Scene3D.qml +++ b/src/plugins/scene3d/Scene3D.qml @@ -14,7 +14,7 @@ * limitations under the License. * */ -import QtQuick 2.0 +import QtQuick 2.9 import QtQuick.Controls 2.0 import RenderWindow 1.0 import QtGraphicalEffects 1.0 @@ -28,6 +28,21 @@ Rectangle { */ property bool gammaCorrect: false + /** + * Get mouse position on 3D widget + */ + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + Scene3D.OnFocusWindow() + } + onPositionChanged: { + Scene3D.OnHovered(mouseArea.mouseX, mouseArea.mouseY); + } + } RenderWindow { id: renderWindow diff --git a/src/plugins/scene3d/Scene3D_TEST.cc b/src/plugins/scene3d/Scene3D_TEST.cc deleted file mode 100644 index 8db1ad7ee..000000000 --- a/src/plugins/scene3d/Scene3D_TEST.cc +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2017 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 "ignition/gui/Iface.hh" -#include "ignition/gui/MainWindow.hh" -#include "ignition/gui/Plugin.hh" - -using namespace ignition; -using namespace gui; - -///////////////////////////////////////////////// -TEST(Scene3DTest, Load) -{ - setVerbosity(4); - EXPECT_TRUE(initApp()); - - EXPECT_TRUE(loadPlugin("Scene3D")); - - EXPECT_TRUE(stop()); -} - -///////////////////////////////////////////////// -TEST(Scene3DTest, Resize) -{ - setVerbosity(4); - EXPECT_TRUE(initApp()); - - // Load plugin - EXPECT_TRUE(loadPlugin("Scene3D")); - - // Create main window - EXPECT_TRUE(createMainWindow()); - - // Close window after some time - auto win = mainWindow(); - ASSERT_NE(nullptr, win); - - QTimer::singleShot(300, [&win]() - { - // Check there are no segfaults when resizing - for (auto i : {100, 300, 200, 500, 400}) - { - win->resize(i + (qrand() % 100), i + (qrand() % 100)); - QCoreApplication::processEvents(); - } - win->close(); - }); - - // Show window - EXPECT_TRUE(runMainWindow()); - - EXPECT_TRUE(stop()); -} - -///////////////////////////////////////////////// -TEST(Scene3DTest, Config) -{ - setVerbosity(4); - EXPECT_TRUE(initApp()); - - // Load plugin - const char *pluginStr = - "" - "ogre" - "banana" - "1.0 0 0" - "0 1 0" - "1 2 3 0 0 1.57" - ""; - - tinyxml2::XMLDocument pluginDoc; - pluginDoc.Parse(pluginStr); - EXPECT_TRUE(ignition::gui::loadPlugin("Scene3D", - pluginDoc.FirstChildElement("plugin"))); - - // Create main window - EXPECT_TRUE(createMainWindow()); - - // Check scene - auto engine = rendering::engine("ogre"); - ASSERT_NE(nullptr, engine); - - auto scene = engine->SceneByName("banana"); - ASSERT_NE(nullptr, scene); - - EXPECT_EQ(math::Color(0, 1, 0), scene->BackgroundColor()); - EXPECT_EQ(math::Color(1, 0, 0), scene->AmbientLight()); - - auto root = scene->RootVisual(); - ASSERT_NE(nullptr, root); - EXPECT_EQ(1u, root->ChildCount()); - - // Check camera - auto camera = std::dynamic_pointer_cast( - root->ChildByIndex(0)); - ASSERT_NE(nullptr, camera); - - EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1.57), camera->WorldPose()); - - EXPECT_TRUE(stop()); -} - diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6dd8ad952..d0d40bd18 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -18,6 +18,7 @@ execute_process(COMMAND cmake -E remove_directory ${CMAKE_BINARY_DIR}/test_resul execute_process(COMMAND cmake -E make_directory ${CMAKE_BINARY_DIR}/test_results) include_directories(${GTEST_INCLUDE_DIRS}) +add_subdirectory(helpers) add_subdirectory(integration) add_subdirectory(performance) add_subdirectory(plugins) diff --git a/test/helpers/CMakeLists.txt b/test/helpers/CMakeLists.txt new file mode 100644 index 000000000..46af75c2a --- /dev/null +++ b/test/helpers/CMakeLists.txt @@ -0,0 +1,15 @@ +set (qt_headers + TestHelper.hh +) + +QT5_WRAP_CPP(headers_MOC ${qt_headers}) + +add_library(${PROJECT_NAME}_test_helpers SHARED + TestHelper.cc + ${headers_MOC} +) +target_compile_definitions(${PROJECT_NAME}_test_helpers PRIVATE TestHelper_EXPORTS) +target_link_libraries(${PROJECT_NAME}_test_helpers + PUBLIC + ${PROJECT_LIBRARY_TARGET_NAME} +) diff --git a/test/helpers/TestHelper.cc b/test/helpers/TestHelper.cc new file mode 100644 index 000000000..e7d70984d --- /dev/null +++ b/test/helpers/TestHelper.cc @@ -0,0 +1,38 @@ +/* + * 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 "TestHelper.hh" + +namespace ignition +{ +namespace gui +{ +TestHelper::TestHelper() +{ + App()->findChild()->installEventFilter(this); +} + +bool TestHelper::eventFilter(QObject *_obj, QEvent *_event) +{ + if (this->forwardEvent) + this->forwardEvent(_event); + + // Standard event processing + return QObject::eventFilter(_obj, _event); +} +} +} diff --git a/test/helpers/TestHelper.hh b/test/helpers/TestHelper.hh new file mode 100644 index 000000000..8c2fee0f9 --- /dev/null +++ b/test/helpers/TestHelper.hh @@ -0,0 +1,57 @@ +/* + * 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_GUI_TESTHELPER_HH_ +#define IGNITION_GUI_TESTHELPER_HH_ + +#include +#include +#include + +#ifndef _WIN32 +# define TestHelper_EXPORTS_API +#else +# if (defined(TestHelper_EXPORTS)) +# define TestHelper_EXPORTS_API __declspec(dllexport) +# else +# define TestHelper_EXPORTS_API __declspec(dllimport) +# endif +#endif + +namespace ignition +{ +namespace gui +{ +/// \brief +class TestHelper_EXPORTS_API TestHelper : public QObject +{ + Q_OBJECT + + /// \brief Constructor + public: TestHelper(); + + /// \brief Destructor + public: ~TestHelper() = default; + + /// \brief Documentation inherited + public: bool eventFilter(QObject *_obj, QEvent *_event) override; + + public: std::function forwardEvent; +}; +} +} + +#endif diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index 397fabacb..aeb5c8f1f 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -1,3 +1,13 @@ ign_get_sources(tests) -ign_build_tests(TYPE INTEGRATION SOURCES ${tests}) +find_package(Qt5Test REQUIRED) + +ign_build_tests( + TYPE INTEGRATION + SOURCES ${tests} + LIB_DEPS + ${PROJECT_NAME}_test_helpers + ignition-plugin${IGN_PLUGIN_VER}::loader + ignition-rendering${IGN_RENDERING_VER}::ignition-rendering${IGN_RENDERING_VER} + Qt5::Test +) diff --git a/test/integration/scene3d.cc b/test/integration/scene3d.cc new file mode 100644 index 000000000..ea9dc239e --- /dev/null +++ b/test/integration/scene3d.cc @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 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 "test_config.h" // NOLINT(build/include) +#include "../helpers/TestHelper.hh" +#include "ignition/gui/Application.hh" +#include "ignition/gui/GuiEvents.hh" +#include "ignition/gui/Plugin.hh" +#include "ignition/gui/MainWindow.hh" + +int g_argc = 1; +char **g_argv = new char *[g_argc]; + +using namespace ignition; +using namespace gui; + +///////////////////////////////////////////////// +TEST(Scene3DTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Load)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + + EXPECT_TRUE(app.LoadPlugin("Scene3D")); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Get plugin + auto plugins = win->findChildren(); + EXPECT_EQ(plugins.size(), 1); + + auto plugin = plugins[0]; + EXPECT_EQ(plugin->Title(), "3D Scene"); + + // Cleanup + plugins.clear(); +} + +///////////////////////////////////////////////// +TEST(Scene3DTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Config)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + + // Load plugin + const char *pluginStr = + "" + "ogre" + "banana" + "1.0 0 0" + "0 1 0" + "1 2 3 0 0 1.57" + ""; + + tinyxml2::XMLDocument pluginDoc; + pluginDoc.Parse(pluginStr); + EXPECT_TRUE(app.LoadPlugin("Scene3D", + pluginDoc.FirstChildElement("plugin"))); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Show, but don't exec, so we don't block + win->QuickWindow()->show(); + + // Check scene + auto engine = rendering::engine("ogre"); + ASSERT_NE(nullptr, engine); + + int sleep = 0; + int maxSleep = 30; + while (0 == engine->SceneCount() && sleep < maxSleep) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + sleep++; + } + + EXPECT_EQ(1u, engine->SceneCount()); + auto scene = engine->SceneByName("banana"); + ASSERT_NE(nullptr, scene); + + EXPECT_EQ(math::Color(0, 1, 0), scene->BackgroundColor()); + EXPECT_EQ(math::Color(1, 0, 0), scene->AmbientLight()); + + auto root = scene->RootVisual(); + ASSERT_NE(nullptr, root); + EXPECT_EQ(1u, root->ChildCount()); + + // Check camera + auto camera = std::dynamic_pointer_cast( + root->ChildByIndex(0)); + ASSERT_NE(nullptr, camera); + + EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1.57), camera->WorldPose()); +} + +///////////////////////////////////////////////// +TEST(Scene3DTest, IGN_UTILS_TEST_ENABLED_ONLY_ON_LINUX(Events)) +{ + common::Console::SetVerbosity(4); + + Application app(g_argc, g_argv); + app.AddPluginPath(std::string(PROJECT_BINARY_PATH) + "/lib"); + + // Load plugin + const char *pluginStr = + "" + "ogre" + "banana" + "1.0 0 0" + "0 1 0" + "1 2 3 0 0 1.57" + ""; + + tinyxml2::XMLDocument pluginDoc; + pluginDoc.Parse(pluginStr); + EXPECT_TRUE(app.LoadPlugin("Scene3D", + pluginDoc.FirstChildElement("plugin"))); + + // Get main window + auto win = app.findChild(); + ASSERT_NE(nullptr, win); + + // Show, but don't exec, so we don't block + win->QuickWindow()->show(); + + // Flags to check if events were received + bool receivedRenderEvent{false}; + bool receivedRightEvent{false}; + bool receivedLeftEvent{false}; + bool receivedHoverEvent{false}; + + // Position vectors reported by click events + math::Vector3d leftClickPoint, rightClickPoint; + + // Helper to filter events + auto testHelper = std::make_unique(); + testHelper->forwardEvent = [&](QEvent *_event) + { + if (_event->type() == events::Render::kType) + { + receivedRenderEvent = true; + } + else if (_event->type() == events::RightClickToScene::kType) + { + receivedRightEvent = true; + auto rightClickToScene = static_cast(_event); + rightClickPoint = rightClickToScene->Point(); + } + else if (_event->type() == events::LeftClickToScene::kType) + { + receivedLeftEvent = true; + auto leftClickToScene = static_cast(_event); + leftClickPoint = leftClickToScene->Point(); + } + else if (_event->type() == events::HoverToScene::kType) + { + receivedHoverEvent = true; + } + }; + + int sleep = 0; + int maxSleep = 30; + while ((!receivedRenderEvent || !receivedRightEvent || + !receivedLeftEvent || !receivedHoverEvent) && sleep < maxSleep) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + + QTest::mouseMove(win->QuickWindow(), QPoint(70, 100), -1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + + QTest::mouseClick(win->QuickWindow(), Qt::RightButton, Qt::NoModifier); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + + QTest::mouseClick(win->QuickWindow(), Qt::LeftButton, Qt::NoModifier); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + QCoreApplication::processEvents(); + + sleep++; + } + + EXPECT_TRUE(receivedRenderEvent); + EXPECT_TRUE(receivedLeftEvent); + EXPECT_TRUE(receivedRightEvent); + EXPECT_TRUE(receivedHoverEvent); + + EXPECT_EQ(leftClickPoint, rightClickPoint); + EXPECT_NEAR(1.0, leftClickPoint.X(), 1e-4); + EXPECT_NEAR(11.942695, leftClickPoint.Y(), 1e-4); + EXPECT_NEAR(4.159424, leftClickPoint.Z(), 1e-4); +}