// Copyright Epic Games, Inc. All Rights Reserved. #include "ControlRigTraitCustomization.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "Logging/LogScopedVerbosityOverride.h" #include "IWorkspaceEditor.h" #include "DetailWidgetRow.h" #include "IDetailChildrenBuilder.h" #include "Widgets/SRigVMVariableMappingWidget.h" #include "ControlRigTrait.h" #include "RigVMBlueprintGeneratedClass.h" #include "ControlRigBlueprint.h" #include "Engine/SkeletalMesh.h" #include "IPropertyUtilities.h" #include "AnimNextRigVMAssetEditorData.h" #include "AnimGraphUncookedOnlyUtils.h" #include "RigVMModel/RigVMController.h" #include "AnimNextControlRigModule.h" #include "ScopedTransaction.h" #include "K2Node.h" #include "TraitCore/TraitRegistry.h" #include "RigVMStringUtils.h" #define LOCTEXT_NAMESPACE "ControlRigTraitSharedDataCustomization" namespace UE::AnimNext::Editor { FControlRigTraitSharedDataCustomization::~FControlRigTraitSharedDataCustomization() { if (OnObjectsReinstancedHandle.IsValid()) { UE::AnimNext::ControlRig::FAnimNextControlRigModule::OnObjectsReinstanced.Remove(OnObjectsReinstancedHandle); } } void FControlRigTraitSharedDataCustomization::CustomizeHeader(TSharedRef InPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InCustomizationUtils) { if (TSharedPtr PropertyUtils = InCustomizationUtils.GetPropertyUtilities()) { const TArray< TWeakObjectPtr >& SelectedObjects = PropertyUtils->GetSelectedObjects(); if (SelectedObjects.Num() != 1) { return; } SelectedNodeWeak = Cast(SelectedObjects[0]); } StructPropertyHandle = InPropertyHandle; ScopedControlRigTraitSharedData = GetControlRigSharedData(StructPropertyHandle); FControlRigTraitSharedData* ControlRigTraitSharedData = (FControlRigTraitSharedData*)ScopedControlRigTraitSharedData->GetStructMemory(); CustomPinProperties.Reset(ControlRigTraitSharedData->ExposedPropertyVariableNames.Num() + ControlRigTraitSharedData->ExposedPropertyControlNames.Num()); for (const FName& PropertyName : ControlRigTraitSharedData->ExposedPropertyVariableNames) { FOptionalPinFromProperty OptionalPin(PropertyName, true, true, FString(), FText(), false, NAME_None, false); OptionalPin.bIsOverrideEnabled = false; CustomPinProperties.Add(OptionalPin); } for (const FName& PropertyName : ControlRigTraitSharedData->ExposedPropertyControlNames) { FOptionalPinFromProperty OptionalPin(PropertyName, true, true, FString(), FText(), false, NAME_None, false); OptionalPin.bIsOverrideEnabled = false; CustomPinProperties.Add(OptionalPin); } ControlRigIOMapping = MakeShared(ControlRigTraitSharedData->InputMapping, ControlRigTraitSharedData->OutputMapping, CustomPinProperties); ControlRigIOMapping->GetOnPinCheckStateChangedDelegate().BindSP(this, &FControlRigTraitSharedDataCustomization::OnPropertyExposeCheckboxChanged); ControlRigIOMapping->GetOnVariableMappingChanged().BindSP(this, &FControlRigTraitSharedDataCustomization::OnVariableMappingChanged); ControlRigIOMapping->GetOnGetTargetSkeletonDelegate().BindSP(this, &FControlRigTraitSharedDataCustomization::GetTargetSkeleton); ControlRigIOMapping->GetOnGetTargetClassDelegate().BindSP(this, &FControlRigTraitSharedDataCustomization::GetTargetClass); ControlRigIOMapping->SetIgnoreVariablesWithNoMemory(true); // AnimBP creates it's own variables, AnimNext needs the memory to create the pin OnObjectsReinstancedHandle = UE::AnimNext::ControlRig::FAnimNextControlRigModule::OnObjectsReinstanced.AddRaw(this, &FControlRigTraitSharedDataCustomization::OnObjectsReinstanced); InHeaderRow.NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ]; } void FControlRigTraitSharedDataCustomization::CustomizeChildren(TSharedRef InPropertyHandle, IDetailChildrenBuilder& InChildBuilder, IPropertyTypeCustomizationUtils& InCustomizationUtils) { // --- Add struct default members, as they would be without the customization --- uint32 NumChildren = 0; if (InPropertyHandle->GetNumChildren(NumChildren) == FPropertyAccess::Success) { for (uint32 Index = 0; Index < NumChildren; ++Index) { TSharedRef ChildPropertyHandle = InPropertyHandle->GetChildHandle(Index).ToSharedRef(); InChildBuilder.AddProperty(ChildPropertyHandle); } } IDetailLayoutBuilder& DetailBuilder = InChildBuilder.GetParentCategory().GetParentLayout(); if (ControlRigIOMapping.IsValid()) { ControlRigIOMapping->CreateVariableMappingWidget(DetailBuilder); } } void FControlRigTraitSharedDataCustomization::OnPropertyExposeCheckboxChanged(ECheckBoxState NewState, FName PropertyName) { if (FControlRigTraitSharedData* ControlRigTraitSharedData = (FControlRigTraitSharedData*)ScopedControlRigTraitSharedData->GetStructMemory()) { const bool bInput = ControlRigIOMapping->IsInputProperty(PropertyName); ControlRigIOMapping->SetIOMapping(bInput, PropertyName, NAME_None); if (URigVMEdGraphNode* EdGraphNode = Cast(SelectedNodeWeak.Get())) { if (URigVMNode* ModelNode = EdGraphNode->GetModelNode()) { if (URigVMGraph* Model = EdGraphNode->GetModel()) { URigVMController* Controller = EdGraphNode->GetController(); if (URigVMEdGraph* EdGraph = Cast(SelectedNodeWeak->GetGraph())) { static FName ExposedPropertyVariablesName = GET_MEMBER_NAME_CHECKED(FControlRigTraitSharedData, ExposedPropertyVariableNames); static FName ExposedPropertyControlsName = GET_MEMBER_NAME_CHECKED(FControlRigTraitSharedData, ExposedPropertyControlNames); static FName ExposedPropertyControlTypesName = GET_MEMBER_NAME_CHECKED(FControlRigTraitSharedData, ExposedPropertyControlTypes); static FName ExposedPropertyControlDefaultValuesName = GET_MEMBER_NAME_CHECKED(FControlRigTraitSharedData, ExposedPropertyControlDefaultValues); const FString ControlRigTraitName = FindControlRigTraitPinName(ModelNode); const FString ControlRigTraitPrefix(ControlRigTraitName + TEXT(".")); const FString ExposedVariablesSubPath(ControlRigTraitPrefix + ExposedPropertyVariablesName.ToString()); const FString ExposedControlsSubPath(ControlRigTraitPrefix + ExposedPropertyControlsName.ToString()); const FString ExposedControlTypesSubPath(ControlRigTraitPrefix + ExposedPropertyControlTypesName.ToString()); const FString ExposedControlDefaultValuesSubPath(ControlRigTraitPrefix + ExposedPropertyControlDefaultValuesName.ToString()); static const FText ExposePropertyToPinText = LOCTEXT("ExposePropertyToPin", "Expose Property to Pin"); static const FText RemovePropertyPinText = LOCTEXT("RemovePropertyOptionalPin", "Removed Property Optional Pin"); const FText& TransactionText = NewState == ECheckBoxState::Checked ? ExposePropertyToPinText : RemovePropertyPinText; FRigVMControllerCompileBracketScope CompileScope(Controller); // avoid multiple recompilations and force a details refresh after recompile FScopedTransaction Transaction(TransactionText); Model->Modify(); if (NewState == ECheckBoxState::Checked) { const bool bIsInput = ControlRigIOMapping->IsInputProperty(PropertyName); // if checked, we clear mapping and unclear all children ControlRigIOMapping->SetIOMapping(bIsInput, PropertyName, NAME_None); } URigVMPin* VariablesPin = ModelNode->FindPin(ExposedVariablesSubPath); URigVMPin* ControlsPin = ModelNode->FindPin(ExposedControlsSubPath); URigVMPin* ControlsTypesPin = ModelNode->FindPin(ExposedControlTypesSubPath); URigVMPin* ControlsDefaultValuesPin = ModelNode->FindPin(ExposedControlDefaultValuesSubPath); if (ensure(VariablesPin && ControlsPin && ControlsTypesPin && ControlsDefaultValuesPin)) { ControlRigTraitSharedData->ExposedPropertyVariableNames.Reset(CustomPinProperties.Num()); ControlRigTraitSharedData->ExposedPropertyControlNames.Reset(CustomPinProperties.Num()); ControlRigTraitSharedData->ExposedPropertyControlTypes.Reset(CustomPinProperties.Num()); ControlRigTraitSharedData->ExposedPropertyControlDefaultValues.Reset(CustomPinProperties.Num()); for (const FOptionalPinFromProperty& OptionalPin : CustomPinProperties) { if (OptionalPin.bShowPin) { if (IsVariableProperty(ControlRigTraitSharedData, OptionalPin.PropertyName)) { ControlRigTraitSharedData->ExposedPropertyVariableNames.Add(OptionalPin.PropertyName); } else { const TArray& Controls = ControlRigIOMapping->GetControls(); if (const FControlRigIOMapping::FControlsInfo* ControlInfo = GetControlInfo(Controls, OptionalPin.PropertyName)) { ControlRigTraitSharedData->ExposedPropertyControlNames.Add(OptionalPin.PropertyName); ControlRigTraitSharedData->ExposedPropertyControlTypes.Add(ControlInfo->ControlType); ControlRigTraitSharedData->ExposedPropertyControlDefaultValues.Add(ControlInfo->DefaultValue); } } } } static constexpr const auto SetPinArrayDefaultValue = ([](URigVMController* Controller, URigVMPin* Pin, FControlRigTraitSharedData* ControlRigTraitSharedData, const FName& PropertyName, void* ArrayData) -> bool { if (const FProperty* Property = ControlRigTraitSharedData->StaticStruct()->FindPropertyByName(PropertyName)) { FString DefaultValue; Property->ExportText_Direct(DefaultValue, ArrayData, nullptr, nullptr, PPF_None); if (DefaultValue.IsEmpty()) { static FString EmptyArray(TEXT("()")); DefaultValue = EmptyArray; } return Controller->SetPinDefaultValue(Pin->GetPinPath(), DefaultValue); } return false; }); SetPinArrayDefaultValue(Controller, VariablesPin, ControlRigTraitSharedData, ExposedPropertyVariablesName, &ControlRigTraitSharedData->ExposedPropertyVariableNames); SetPinArrayDefaultValue(Controller, ControlsPin, ControlRigTraitSharedData, ExposedPropertyControlsName, &ControlRigTraitSharedData->ExposedPropertyControlNames); SetPinArrayDefaultValue(Controller, ControlsTypesPin, ControlRigTraitSharedData, ExposedPropertyControlTypesName, &ControlRigTraitSharedData->ExposedPropertyControlTypes); SetPinArrayDefaultValue(Controller, ControlsDefaultValuesPin, ControlRigTraitSharedData, ExposedPropertyControlDefaultValuesName, &ControlRigTraitSharedData->ExposedPropertyControlDefaultValues); } Controller->RepopulatePinsOnNode(ModelNode, true, false, true); } } } } } } void FControlRigTraitSharedDataCustomization::OnVariableMappingChanged(const FName& PathName, const FName& Curve, bool bInput) { if (URigVMEdGraphNode* EdGraphNode = Cast(SelectedNodeWeak.Get())) { if (URigVMNode* ModelNode = EdGraphNode->GetModelNode()) { if (URigVMGraph* Model = EdGraphNode->GetModel()) { URigVMController* Controller = EdGraphNode->GetController(); FRigVMControllerCompileBracketScope CompileScope(Controller); // avoid multiple recompilations and force a details refresh after recompile FScopedTransaction Transaction(LOCTEXT("VariableMappingChanged", "Change Variable Mapping")); Model->Modify(); // @todo: this is not enough when we start breaking down struct ControlRigIOMapping->SetIOMapping(bInput, PathName, Curve); } } } } TSharedPtr FControlRigTraitSharedDataCustomization::GetControlRigSharedData(const TSharedPtr& StructPropertyHandle) { TSharedPtr ControlRigSharedData; TArray> OutStructs; StructPropertyHandle->GetOuterStructs(OutStructs); if (OutStructs.Num() == 1) { ControlRigSharedData = OutStructs[0]; } return ControlRigSharedData; } UClass* FControlRigTraitSharedDataCustomization::GetTargetClass() const { FControlRigTraitSharedData* ControlRigTraitSharedData = (FControlRigTraitSharedData*)ScopedControlRigTraitSharedData->GetStructMemory(); return ControlRigTraitSharedData->ControlRigClass; } USkeleton* FControlRigTraitSharedDataCustomization::GetTargetSkeleton() const { USkeleton* TargetSkeleton = nullptr; if (const FControlRigTraitSharedData* ControlRigTraitSharedData = (FControlRigTraitSharedData*)ScopedControlRigTraitSharedData->GetStructMemory()) { TargetSkeleton = ControlRigTraitSharedData->GetPreviewSkeleton(); } return TargetSkeleton; } const FControlRigIOMapping::FControlsInfo* FControlRigTraitSharedDataCustomization::GetControlInfo(const TArray& Controls, const FName& ControlName) { const FControlRigIOMapping::FControlsInfo* ControlInfo = Controls.FindByPredicate([&ControlName](const FControlRigIOMapping::FControlsInfo& In) { return In.Name == ControlName; }); return ControlInfo; } bool FControlRigTraitSharedDataCustomization::IsVariableProperty(FControlRigTraitSharedData* ControlRigTraitSharedData, const FName& PropertyName ) { if (ControlRigTraitSharedData != nullptr) { UClass* ControlRigClass = ControlRigTraitSharedData->ControlRigClass; if (const UControlRig* CDO = ControlRigClass->GetDefaultObject()) { const TArray PublicVariables = CDO->GetPublicVariables(); const FRigVMExternalVariable* PublicVar = PublicVariables.FindByPredicate([&PropertyName](const FRigVMExternalVariable& In) { return In.Name == PropertyName; }); return PublicVar != nullptr; } } return false; } void FControlRigTraitSharedDataCustomization::OnObjectsReinstanced(const TMap& OldToNewInstanceMap) { if (ScopedControlRigTraitSharedData.IsValid()) { for (const TPair& Pair : OldToNewInstanceMap) { UObject* NewObject = Pair.Value; if ((NewObject != nullptr) && NewObject->IsA()) { if (FControlRigTraitSharedData* ControlRigTraitSharedData = (FControlRigTraitSharedData*)ScopedControlRigTraitSharedData->GetStructMemory()) { if (UClass* ControlRigClass = ControlRigTraitSharedData->ControlRigClass) { if (const UControlRig* CDO = ControlRigClass->GetDefaultObject()) { if (URigVMEdGraphNode* EdGraphNode = Cast(SelectedNodeWeak.Get())) { bool bRequiresPinRefresh = false; const TArray PublicVariables = CDO->GetPublicVariables(); // Check if the currently exposed properties still exist for (const FOptionalPinFromProperty& PinProperty : CustomPinProperties) { if (!PinProperty.bShowPin) // if not exposed, no need to test { continue; } const FName& PropertyName = PinProperty.PropertyName; const FRigVMExternalVariable* PublicVar = PublicVariables.FindByPredicate([&PropertyName](const FRigVMExternalVariable& In) { return In.Name == PropertyName; }); if (PublicVar != nullptr) // found a public var with the same name of the exposed pin { const TArray& RootPins = EdGraphNode->GetInputPins(); for (const URigVMPin* RootPîn : RootPins) { // Try to find a node pin with the same name if (const URigVMPin*const* PinPtr = RootPîn->GetSubPins().FindByPredicate([&PropertyName](const URigVMPin* In) { return In->GetFName() == PropertyName; })) { const URigVMPin* Pin = (*PinPtr); if (Pin->GetCPPType() != PublicVar->TypeName) { bRequiresPinRefresh = true; break; } } else { // No pin found but if it is exposed, refresh if (ControlRigTraitSharedData->ExposedPropertyVariableNames.Contains(PropertyName) || ControlRigTraitSharedData->ExposedPropertyControlNames.Contains(PropertyName)) { bRequiresPinRefresh = true; break; } // else all ok, no pin, but it is not exposed } } } else { // No public var found, maybe a control ? const TArray& Controls = ControlRigIOMapping->GetControls(); if(Controls.FindByPredicate([&PropertyName](const FControlRigIOMapping::FControlsInfo& In) { return In.Name == PropertyName; }) == nullptr) { // No control found, request a rebuild bRequiresPinRefresh = true; break; } } } if (bRequiresPinRefresh) { if (URigVMController* Controller = EdGraphNode->GetController()) { FRigVMControllerCompileBracketScope CompileScope(Controller); // avoid multiple recompilations and force a details refresh after recompile Controller->RepopulatePinsOnNode(EdGraphNode->GetModelNode(), true, false, true); // Force pin refresh on node } } } } } } break; } } } } FString FControlRigTraitSharedDataCustomization::FindControlRigTraitPinName(URigVMNode* ModelNode) { const FTraitRegistry& TraitRegistry = FTraitRegistry::Get(); FString ControlRigTraitName; FString ControlRigTraitDisplayName; if (const FTrait* Trait = TraitRegistry.Find(FControlRigTrait::TraitUID)) // the context menu in the traits create the traits using a sanitized display name { ControlRigTraitName = Trait->GetTraitName(); FString DisplayNameMetadata; Trait->GetTraitSharedDataStruct()->GetStringMetaDataHierarchical(FRigVMStruct::DisplayNameMetaName, &DisplayNameMetadata); ControlRigTraitDisplayName = DisplayNameMetadata.IsEmpty() ? Trait->GetTraitName() : DisplayNameMetadata; RigVMStringUtils::SanitizeName(ControlRigTraitDisplayName, false, false, URigVMSchema::GetMaxNameLength()); } else { ControlRigTraitName = FString(TEXT("FControlRigTrait")); } FString Tmp; for (const auto& Pin : ModelNode->GetPins()) { Pin->GetName(Tmp); if (Tmp.Contains(ControlRigTraitName)) { return Tmp; } if (Tmp.Contains(ControlRigTraitDisplayName)) { return Tmp; } } return FString(); } } // end namespace UE::AnimNext::Editor #undef LOCTEXT_NAMESPACE