/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace EMotionFX { class CopyPasteRagdollJointLimitsFixture : public UIFixture { }; #if AZ_TRAIT_DISABLE_FAILED_EMOTION_FX_EDITOR_TESTS TEST_F(CopyPasteRagdollJointLimitsFixture, DISABLED_TestJointLimits) #else TEST_F(CopyPasteRagdollJointLimitsFixture, TestJointLimits) #endif // AZ_TRAIT_DISABLE_FAILED_EMOTION_FX_EDITOR_TESTS { using testing::_; AZ::SerializeContext* serializeContext = GetSerializeContext(); Physics::MockPhysicsSystem::Reflect(serializeContext); // Required by Ragdoll plugin to fake PhysX Gem is available D6JointLimitConfiguration::Reflect(serializeContext); EMStudio::GetMainWindow()->ApplicationModeChanged("Physics"); auto ragdollPlugin = static_cast(EMStudio::GetPluginManager()->FindActivePlugin(EMotionFX::RagdollNodeInspectorPlugin::CLASS_ID)); ASSERT_TRUE(ragdollPlugin) << "Ragdoll plugin not found."; ragdollPlugin->Init(); Physics::MockPhysicsSystem physicsSystem; Physics::MockPhysicsInterface physicsInterface; Physics::MockJointHelpersInterface jointHelpers; EXPECT_CALL(jointHelpers, GetSupportedJointTypeIds) .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); EXPECT_CALL(jointHelpers, GetSupportedJointTypeId(_)) .WillRepeatedly( [](AzPhysics::JointType jointType) -> AZStd::optional { if (jointType == AzPhysics::JointType::D6Joint) { return azrtti_typeid(); } return AZStd::nullopt; }); EXPECT_CALL(jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) .WillRepeatedly( []([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, [[maybe_unused]] const AZ::Quaternion& childWorldRotation, [[maybe_unused]] const AZ::Vector3& axis, [[maybe_unused]] const AZStd::vector& exampleLocalRotations) { return AZStd::make_unique(); }); AutoRegisteredActor actor {ActorFactory::CreateAndInit(4)}; { AZStd::string result; EXPECT_TRUE(CommandSystem::GetCommandManager()->ExecuteCommand("Select -actorId " + AZStd::to_string(actor->GetID()), result)) << result.c_str(); } CommandRagdollHelpers::AddJointsToRagdoll(actor->GetID(), {"rootJoint", "joint1", "joint2", "joint3"}); const Physics::RagdollConfiguration& ragdollConfig = actor->GetPhysicsSetup()->GetRagdollConfig(); ASSERT_EQ(ragdollConfig.m_nodes.size(), 4); AZStd::shared_ptr rootJointLimit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("rootJoint")->m_jointConfig); ASSERT_TRUE(rootJointLimit); // Set the initial joint limits on the rootJoint to be something different rootJointLimit->m_swingLimitY = 1.0f; rootJointLimit->m_swingLimitZ = 100.0f; auto skeletonOutlinerPlugin = static_cast(EMStudio::GetPluginManager()->FindActivePlugin(EMotionFX::SkeletonOutlinerPlugin::CLASS_ID)); ASSERT_TRUE(skeletonOutlinerPlugin) << "Skeleton outliner plugin not found."; SkeletonModel* model = skeletonOutlinerPlugin->GetModel(); const QModelIndex rootIndex = model->index(0, 0); const QModelIndex joint1Index = model->index(0, 0, rootIndex); const QModelIndex joint2Index = model->index(0, 0, joint1Index); const QModelIndex joint3Index = model->index(0, 0, joint2Index); EXPECT_TRUE(rootIndex.isValid()); EXPECT_TRUE(joint1Index.isValid()); EXPECT_TRUE(joint2Index.isValid()); EXPECT_TRUE(joint3Index.isValid()); EXPECT_EQ(rootIndex.data(SkeletonModel::COLUMN_NAME).toString(), QString("rootJoint")); EXPECT_EQ(joint1Index.data(SkeletonModel::COLUMN_NAME).toString(), QString("joint1")); EXPECT_EQ(joint2Index.data(SkeletonModel::COLUMN_NAME).toString(), QString("joint2")); EXPECT_EQ(joint3Index.data(SkeletonModel::COLUMN_NAME).toString(), QString("joint3")); // Select the rootJoint QItemSelectionModel& selectionModel = model->GetSelectionModel(); selectionModel.select(rootIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); auto nodeWidget = ragdollPlugin->GetDockWidget()->findChild(); ASSERT_TRUE(nodeWidget); EXPECT_FALSE(nodeWidget->HasCopiedJointLimits()); auto jointLimitWidget = ragdollPlugin->GetDockWidget()->findChild(); ASSERT_TRUE(jointLimitWidget); { // Copy the joint limits for the rootJoint jointLimitWidget->contextMenuRequested({}); auto copyAction = jointLimitWidget->findChild("EMFX.RagdollJointLimitWidget.CopyJointLimitsAction"); ASSERT_TRUE(copyAction); copyAction->trigger(); // Process events so that the menu is destroyed QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } EXPECT_TRUE(nodeWidget->HasCopiedJointLimits()); // Select joint1 selectionModel.select(joint1Index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); // Verify initial state of joint1's limits const AZStd::shared_ptr joint1Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint1")->m_jointConfig); EXPECT_EQ(joint1Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint1Limit->m_swingLimitZ, 45.0f); // Paste joint limits from rootJoint { jointLimitWidget->contextMenuRequested({}); auto pasteAction = jointLimitWidget->findChild("EMFX.RagdollJointLimitWidget.PasteJointLimitsAction"); ASSERT_TRUE(pasteAction); pasteAction->trigger(); // Process events so that the menu is destroyed QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } EXPECT_EQ(joint1Limit->m_swingLimitY, 1.0f); EXPECT_EQ(joint1Limit->m_swingLimitZ, 100.0f); // Select joint2 and joint3. They have to be selected independently // because they have different parents selectionModel.select(joint2Index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); selectionModel.select(joint3Index, QItemSelectionModel::Select | QItemSelectionModel::Rows); const AZStd::shared_ptr joint2Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint2")->m_jointConfig); EXPECT_EQ(joint2Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint2Limit->m_swingLimitZ, 45.0f); const AZStd::shared_ptr joint3Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint3")->m_jointConfig); EXPECT_EQ(joint3Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint3Limit->m_swingLimitZ, 45.0f); QTreeView* skeletonTreeView = skeletonOutlinerPlugin->GetDockWidget()->findChild("EMFX.SkeletonOutlinerPlugin.SkeletonOutlinerTreeView"); ASSERT_TRUE(skeletonTreeView); { skeletonTreeView->customContextMenuRequested({}); auto pasteAction = skeletonOutlinerPlugin->GetDockWidget()->findChild("EMFX.RagdollNodeInspectorPlugin.PasteJointLimitsAction"); ASSERT_TRUE(pasteAction); pasteAction->trigger(); // Process events so that the menu is destroyed QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } EXPECT_EQ(joint2Limit->m_swingLimitY, 1.0f); EXPECT_EQ(joint2Limit->m_swingLimitZ, 100.0f); EXPECT_EQ(joint3Limit->m_swingLimitY, 1.0f); EXPECT_EQ(joint3Limit->m_swingLimitZ, 100.0f); } } // namespace EMotionFX