// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimationEditorUtils.h" #include "Framework/Commands/UIAction.h" #include "Textures/SlateIcon.h" #include "Misc/MessageDialog.h" #include "Misc/FeedbackContext.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SUniformGridPanel.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SButton.h" #include "Styling/CoreStyle.h" #include "EditorStyleSet.h" #include "Animation/AnimMontage.h" #include "Animation/AnimBlueprint.h" #include "Factories/AnimBlueprintFactory.h" #include "Factories/AnimCompositeFactory.h" #include "Factories/AnimMontageFactory.h" #include "Factories/BlendSpaceFactory1D.h" #include "Factories/AimOffsetBlendSpaceFactory1D.h" #include "Factories/BlendSpaceFactoryNew.h" #include "Factories/AimOffsetBlendSpaceFactoryNew.h" #include "Engine/PoseWatch.h" #include "Animation/AnimBlueprintGeneratedClass.h" #include "Animation/AnimBoneCompressionSettings.h" #include "Animation/AnimComposite.h" #include "Animation/AnimCompress.h" #include "Animation/BlendSpace.h" #include "Animation/BlendSpace1D.h" #include "Animation/AimOffsetBlendSpace.h" #include "Animation/AimOffsetBlendSpace1D.h" #include "AnimationGraph.h" #include "AnimStateNodeBase.h" #include "AnimStateTransitionNode.h" #include "Animation/AnimNodeBase.h" #include "AnimGraphNode_Base.h" #include "AnimGraphNode_StateMachineBase.h" #include "AnimationStateMachineGraph.h" #include "K2Node_Composite.h" #include "Kismet2/BlueprintEditorUtils.h" #include "AssetRegistryModule.h" #include "Interfaces/IMainFrameModule.h" #define LOCTEXT_NAMESPACE "AnimationEditorUtils" ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // Create Animation dialog to determine a newly created asset's name /////////////////////////////////////////////////////////////////////////////// FText SCreateAnimationAssetDlg::LastUsedAssetPath; void SCreateAnimationAssetDlg::Construct(const FArguments& InArgs) { AssetPath = FText::FromString(FPackageName::GetLongPackagePath(InArgs._DefaultAssetPath.ToString())); AssetName = FText::FromString(FPackageName::GetLongPackageAssetName(InArgs._DefaultAssetPath.ToString())); if (AssetPath.IsEmpty()) { AssetPath = LastUsedAssetPath; } else { LastUsedAssetPath = AssetPath; } FPathPickerConfig PathPickerConfig; PathPickerConfig.DefaultPath = AssetPath.ToString(); PathPickerConfig.OnPathSelected = FOnPathSelected::CreateSP(this, &SCreateAnimationAssetDlg::OnPathChange); PathPickerConfig.bAddDefaultPath = true; FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); SWindow::Construct(SWindow::FArguments() .Title(LOCTEXT("SCreateAnimationAssetDlg_Title", "Create a New Animation Asset")) .SupportsMinimize(false) .SupportsMaximize(false) //.SizingRule( ESizingRule::Autosized ) .ClientSize(FVector2D(450, 450)) [SNew(SVerticalBox) + SVerticalBox::Slot() // Add user input block .Padding(2) [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [SNew(STextBlock) .Text(LOCTEXT("SelectPath", "Select Path to create animation")) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 14))] + SVerticalBox::Slot() .FillHeight(1) .Padding(3) [ContentBrowserModule.Get().CreatePathPicker(PathPickerConfig)] + SVerticalBox::Slot() .AutoHeight() [SNew(SSeparator)] + SVerticalBox::Slot() .AutoHeight() .Padding(3) [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 0, 10, 0) .VAlign(VAlign_Center) [SNew(STextBlock) .Text(LOCTEXT("AnimationName", "Animation Name"))] + SHorizontalBox::Slot() [SNew(SEditableTextBox) .Text(AssetName) .OnTextCommitted(this, &SCreateAnimationAssetDlg::OnNameChange) .MinDesiredWidth(250)]] ]] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) .Padding(5) [SNew(SUniformGridPanel) .SlotPadding(FEditorStyle::GetMargin("StandardDialog.SlotPadding")) .MinDesiredSlotWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) .MinDesiredSlotHeight(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) + SUniformGridPanel::Slot(0, 0) [SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FEditorStyle::GetMargin("StandardDialog.ContentPadding")) .Text(LOCTEXT("OK", "OK")) .OnClicked(this, &SCreateAnimationAssetDlg::OnButtonClick, EAppReturnType::Ok)] + SUniformGridPanel::Slot(1, 0) [SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FEditorStyle::GetMargin("StandardDialog.ContentPadding")) .Text(LOCTEXT("Cancel", "Cancel")) .OnClicked(this, &SCreateAnimationAssetDlg::OnButtonClick, EAppReturnType::Cancel)]]]); } void SCreateAnimationAssetDlg::OnNameChange(const FText& NewName, ETextCommit::Type CommitInfo) { AssetName = NewName; } void SCreateAnimationAssetDlg::OnPathChange(const FString& NewPath) { AssetPath = FText::FromString(NewPath); LastUsedAssetPath = AssetPath; } FReply SCreateAnimationAssetDlg::OnButtonClick(EAppReturnType::Type ButtonID) { UserResponse = ButtonID; if (ButtonID != EAppReturnType::Cancel) { if (!ValidatePackage()) { // reject the request return FReply::Handled(); } } RequestDestroyWindow(); return FReply::Handled(); } /** Ensures supplied package name information is valid */ bool SCreateAnimationAssetDlg::ValidatePackage() { FText Reason; FString FullPath = GetFullAssetPath(); if (!FPackageName::IsValidLongPackageName(FullPath, false, &Reason) || !FName(*AssetName.ToString()).IsValidObjectName(Reason)) { FMessageDialog::Open(EAppMsgType::Ok, Reason); return false; } return true; } EAppReturnType::Type SCreateAnimationAssetDlg::ShowModal() { GEditor->EditorAddModalWindow(SharedThis(this)); return UserResponse; } FString SCreateAnimationAssetDlg::GetAssetPath() { return AssetPath.ToString(); } FString SCreateAnimationAssetDlg::GetAssetName() { return AssetName.ToString(); } FString SCreateAnimationAssetDlg::GetFullAssetPath() { return AssetPath.ToString() + "/" + AssetName.ToString(); } ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// // Dialog to prompt user to select an animation compression settings asset. ///////////////////////////////////////////////////// SAnimationCompressionSelectionDialog::SAnimationCompressionSelectionDialog() : bValidAssetChosen(false) {} SAnimationCompressionSelectionDialog::~SAnimationCompressionSelectionDialog() {} void SAnimationCompressionSelectionDialog::SetOnAssetSelected(const FOnAssetSelected& InHandler) { OnAssetSelectedHandler = InHandler; } void SAnimationCompressionSelectionDialog::DoSelectAsset(const FAssetData& SelectedAsset) { bValidAssetChosen = true; OnAssetSelectedHandler.ExecuteIfBound(SelectedAsset); CloseDialog(); } FReply SAnimationCompressionSelectionDialog::OnConfirmClicked() { TArray SelectedAssets = GetCurrentSelectionDelegate.Execute(); if (SelectedAssets.Num() > 0) { DoSelectAsset(SelectedAssets[0]); } return FReply::Handled(); } FReply SAnimationCompressionSelectionDialog::OnCancelClicked() { CloseDialog(); return FReply::Handled(); } void SAnimationCompressionSelectionDialog::CloseDialog() { TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); if (ContainingWindow.IsValid()) { ContainingWindow->RequestDestroyWindow(); } } void SAnimationCompressionSelectionDialog::OnAssetSelected(const FAssetData& AssetData) { CurrentlySelectedAssets = GetCurrentSelectionDelegate.Execute(); } void SAnimationCompressionSelectionDialog::OnAssetsActivated(const TArray& SelectedAssets, EAssetTypeActivationMethod::Type ActivationType) { const bool bCorrectActivationMethod = ActivationType == EAssetTypeActivationMethod::DoubleClicked || ActivationType == EAssetTypeActivationMethod::Opened; if (SelectedAssets.Num() > 0 && bCorrectActivationMethod) { DoSelectAsset(SelectedAssets[0]); } } bool SAnimationCompressionSelectionDialog::IsConfirmButtonEnabled() const { return CurrentlySelectedAssets.Num() > 0; } void SAnimationCompressionSelectionDialog::Construct(const FArguments& InArgs, const FAnimationCompressionSelectionDialogConfig& InConfig) { FAssetPickerConfig AssetPickerConfig; AssetPickerConfig.Filter.ClassNames.Push(UAnimBoneCompressionSettings::StaticClass()->GetFName()); AssetPickerConfig.Filter.bRecursiveClasses = true; AssetPickerConfig.InitialAssetViewType = EAssetViewType::List; AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SAnimationCompressionSelectionDialog::OnAssetSelected); AssetPickerConfig.OnAssetsActivated = FOnAssetsActivated::CreateSP(this, &SAnimationCompressionSelectionDialog::OnAssetsActivated); AssetPickerConfig.GetCurrentSelectionDelegates.Add(&GetCurrentSelectionDelegate); AssetPickerConfig.SaveSettingsName = TEXT("AnimationCompressionSelectionDialog"); AssetPickerConfig.bCanShowFolders = false; AssetPickerConfig.bCanShowDevelopersFolder = true; AssetPickerConfig.bAllowNullSelection = false; AssetPickerConfig.bAllowDragging = false; AssetPickerConfig.SelectionMode = ESelectionMode::Single; AssetPickerConfig.bFocusSearchBoxWhenOpened = true; AssetPickerConfig.InitialAssetSelection = InConfig.DefaultSelectedAsset != nullptr ? InConfig.DefaultSelectedAsset : FAnimationUtils::GetDefaultAnimationBoneCompressionSettings(); AssetPickerConfig.bForceShowEngineContent = true; if (AssetPickerConfig.InitialAssetSelection.IsValid()) { CurrentlySelectedAssets.Add(AssetPickerConfig.InitialAssetSelection); } FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked(TEXT("ContentBrowser")); AssetPicker = ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig); // The root widget in this dialog. TSharedRef MainVerticalBox = SNew(SVerticalBox); // Asset view MainVerticalBox->AddSlot() .FillHeight(1) .Padding(0, 0, 0, 4) [SNew(SSplitter) + SSplitter::Slot() .Value(0.75f) [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [AssetPicker.ToSharedRef()]]]; // Buttons and asset name TSharedRef ButtonsAndNameBox = SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Right).VAlign(VAlign_Bottom).Padding(4, 3)[SNew(SButton).Text(LOCTEXT("AnimationCompressionSelectionDialogSelectButton", "Select")).ContentPadding(FMargin(8, 2, 8, 2)).IsEnabled(this, &SAnimationCompressionSelectionDialog::IsConfirmButtonEnabled).OnClicked(this, &SAnimationCompressionSelectionDialog::OnConfirmClicked)] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Bottom).Padding(4, 3)[SNew(SButton).ContentPadding(FMargin(8, 2, 8, 2)).Text(LOCTEXT("AnimationCompressionSelectionDialogCancelButton", "Cancel")).OnClicked(this, &SAnimationCompressionSelectionDialog::OnCancelClicked)]; MainVerticalBox->AddSlot() .AutoHeight() .HAlign(HAlign_Right) .Padding(0) [ButtonsAndNameBox]; ChildSlot [MainVerticalBox]; } ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// // Animation editor utility functions ///////////////////////////////////////////////////// namespace AnimationEditorUtils { FAssetData CreateModalAnimationCompressionSelectionDialog(const FAnimationCompressionSelectionDialogConfig& InConfig) { struct FModalResult { void OnAssetSelected(const FAssetData& SelectedAsset) { SavedResult = SelectedAsset; } FAssetData SavedResult; }; FModalResult ModalWindowResult; auto OnAssetSelectedDelegate = SAnimationCompressionSelectionDialog::FOnAssetSelected::CreateRaw(&ModalWindowResult, &FModalResult::OnAssetSelected); TSharedRef Dialog = SNew(SAnimationCompressionSelectionDialog, InConfig); Dialog->SetOnAssetSelected(OnAssetSelectedDelegate); const FVector2D DefaultWindowSize(400.0f, 500.0f); const FVector2D WindowSize = InConfig.WindowSizeOverride.IsZero() ? DefaultWindowSize : InConfig.WindowSizeOverride; const FText WindowTitle = InConfig.DialogTitleOverride.IsEmpty() ? LOCTEXT("GenericAnimationCompressionSelectionDialogWindowHeader", "Select compression settings") : InConfig.DialogTitleOverride; TSharedRef DialogWindow = SNew(SWindow) .Title(WindowTitle) .ClientSize(WindowSize); DialogWindow->SetContent(Dialog); IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); if (MainFrameParentWindow.IsValid()) { FSlateApplication::Get().AddModalWindow(DialogWindow, MainFrameParentWindow.ToSharedRef()); } return ModalWindowResult.SavedResult; } /** Creates a unique package and asset name taking the form InBasePackageName+InSuffix */ void CreateUniqueAssetName(const FString& InBasePackageName, const FString& InSuffix, FString& OutPackageName, FString& OutAssetName) { FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); AssetToolsModule.Get().CreateUniqueAssetName(InBasePackageName, InSuffix, OutPackageName, OutAssetName); } void CreateAnimationAssets(const TArray>& SkeletonsOrSkeletalMeshes, TSubclassOf AssetClass, const FString& InPrefix, FAnimAssetCreated AssetCreated, UObject* NameBaseObject /*= nullptr*/, bool bDoNotShowNameDialog /*= false*/) { TArray ObjectsToSync; for (auto SkelIt = SkeletonsOrSkeletalMeshes.CreateConstIterator(); SkelIt; ++SkelIt) { USkeletalMesh* SkeletalMesh = nullptr; USkeleton* Skeleton = Cast(SkelIt->Get()); if (Skeleton == nullptr) { SkeletalMesh = CastChecked(SkelIt->Get()); Skeleton = SkeletalMesh->GetSkeleton(); } if (Skeleton) { FString Name; FString PackageName; FString AssetPath = (NameBaseObject) ? NameBaseObject->GetOutermost()->GetName() : Skeleton->GetOutermost()->GetName(); // Determine an appropriate name CreateUniqueAssetName(AssetPath, InPrefix, PackageName, Name); if (bDoNotShowNameDialog == false) { // set the unique asset as a default name TSharedRef NewAnimDlg = SNew(SCreateAnimationAssetDlg) .DefaultAssetPath(FText::FromString(PackageName)); // show a dialog to determine a new asset name if (NewAnimDlg->ShowModal() == EAppReturnType::Cancel) { return; } PackageName = NewAnimDlg->GetFullAssetPath(); Name = NewAnimDlg->GetAssetName(); } // Create the asset, and assign its skeleton FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools"); UAnimationAsset* NewAsset = Cast(AssetToolsModule.Get().CreateAsset(Name, FPackageName::GetLongPackagePath(PackageName), AssetClass, NULL)); if (NewAsset) { NewAsset->SetSkeleton(Skeleton); if (SkeletalMesh) { NewAsset->SetPreviewMesh(SkeletalMesh); } NewAsset->MarkPackageDirty(); ObjectsToSync.Add(NewAsset); } } } if (AssetCreated.IsBound()) { if (!AssetCreated.Execute(ObjectsToSync)) { // Destroy the assets we just create for (UObject* ObjectToDelete: ObjectsToSync) { // Notify the asset registry FAssetRegistryModule::AssetDeleted(ObjectToDelete); ObjectToDelete->ClearFlags(RF_Standalone | RF_Public); ObjectToDelete->RemoveFromRoot(); ObjectToDelete->MarkPendingKill(); } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } } void CreateNewAnimBlueprint(TArray> SkeletonsOrSkeletalMeshes, FAnimAssetCreated AssetCreated, bool bInContentBrowser) { const FString DefaultSuffix = TEXT("_AnimBlueprint"); if (SkeletonsOrSkeletalMeshes.Num() == 1) { USkeletalMesh* SkeletalMesh = nullptr; USkeleton* Skeleton = Cast(SkeletonsOrSkeletalMeshes[0].Get()); if (Skeleton == nullptr) { SkeletalMesh = CastChecked(SkeletonsOrSkeletalMeshes[0].Get()); Skeleton = SkeletalMesh->GetSkeleton(); } if (Skeleton) { // Determine an appropriate name for inline-rename FString Name; FString PackageName; CreateUniqueAssetName(Skeleton->GetOutermost()->GetName(), DefaultSuffix, PackageName, Name); UAnimBlueprintFactory* Factory = NewObject(); Factory->TargetSkeleton = Skeleton; Factory->PreviewSkeletalMesh = SkeletalMesh; if (bInContentBrowser) { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); ContentBrowserModule.Get().CreateNewAsset(Name, FPackageName::GetLongPackagePath(PackageName), UAnimBlueprint::StaticClass(), Factory); } else { FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools"); UAnimBlueprint* NewAsset = CastChecked(AssetToolsModule.Get().CreateAsset(Name, FPackageName::GetLongPackagePath(PackageName), UAnimBlueprint::StaticClass(), Factory)); if (NewAsset && AssetCreated.IsBound()) { TArray NewObjects; NewObjects.Add(NewAsset); if (!AssetCreated.Execute(NewObjects)) { // Destroy the assets we just create for (UObject* ObjectToDelete: NewObjects) { ObjectToDelete->ClearFlags(RF_Standalone | RF_Public); ObjectToDelete->RemoveFromRoot(); ObjectToDelete->MarkPendingKill(); } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } } } } else { TArray AssetsToSync; for (auto ObjIt = SkeletonsOrSkeletalMeshes.CreateConstIterator(); ObjIt; ++ObjIt) { USkeletalMesh* SkeletalMesh = nullptr; USkeleton* Skeleton = Cast(ObjIt->Get()); if (Skeleton == nullptr) { SkeletalMesh = CastChecked(ObjIt->Get()); Skeleton = SkeletalMesh->GetSkeleton(); } if (Skeleton) { // Determine an appropriate name FString Name; FString PackageName; CreateUniqueAssetName(Skeleton->GetOutermost()->GetName(), DefaultSuffix, PackageName, Name); // Create the anim blueprint factory used to generate the asset UAnimBlueprintFactory* Factory = NewObject(); Factory->TargetSkeleton = Skeleton; Factory->PreviewSkeletalMesh = SkeletalMesh; FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools"); UObject* NewAsset = AssetToolsModule.Get().CreateAsset(Name, FPackageName::GetLongPackagePath(PackageName), UAnimBlueprint::StaticClass(), Factory); if (NewAsset) { AssetsToSync.Add(NewAsset); } } } if (AssetCreated.IsBound()) { if (!AssetCreated.Execute(AssetsToSync)) { // Destroy the assets we just create for (UObject* ObjectToDelete: AssetsToSync) { ObjectToDelete->ClearFlags(RF_Standalone | RF_Public); ObjectToDelete->RemoveFromRoot(); ObjectToDelete->MarkPendingKill(); } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } } } void FillCreateAssetMenu(FMenuBuilder& MenuBuilder, const TArray>& SkeletonsOrSkeletalMeshes, FAnimAssetCreated AssetCreated, bool bInContentBrowser) { MenuBuilder.BeginSection("CreateAnimAssets", LOCTEXT("CreateAnimAssetsMenuHeading", "Anim Assets")); { // only allow for content browser until we support multi assets so we can open new persona with this BP MenuBuilder.AddMenuEntry( LOCTEXT("Skeleton_NewAnimBlueprint", "Anim Blueprint"), LOCTEXT("Skeleton_NewAnimBlueprintTooltip", "Creates an Anim Blueprint using the selected skeleton."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.AnimBlueprint"), FUIAction( FExecuteAction::CreateStatic(&CreateNewAnimBlueprint, SkeletonsOrSkeletalMeshes, AssetCreated, bInContentBrowser), FCanExecuteAction())); MenuBuilder.AddMenuEntry( LOCTEXT("Skeleton_NewAnimComposite", "Anim Composite"), LOCTEXT("Skeleton_NewAnimCompositeTooltip", "Creates an AnimComposite using the selected skeleton."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.AnimComposite"), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_Composite"), AssetCreated, bInContentBrowser), FCanExecuteAction())); MenuBuilder.AddMenuEntry( LOCTEXT("Skeleton_NewAnimMontage", "Anim Montage"), LOCTEXT("Skeleton_NewAnimMontageTooltip", "Creates an AnimMontage using the selected skeleton."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.AnimMontage"), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_Montage"), AssetCreated, bInContentBrowser), FCanExecuteAction())); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("CreateBlendSpace", LOCTEXT("CreateBlendSpaceMenuHeading", "Blend Spaces")); { MenuBuilder.AddMenuEntry( LOCTEXT("SkeletalMesh_New2DBlendspace", "Blend Space"), LOCTEXT("SkeletalMesh_New2DBlendspaceTooltip", "Creates a Blend Space using the selected skeleton."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.BlendSpace"), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_BlendSpace"), AssetCreated, bInContentBrowser), FCanExecuteAction())); MenuBuilder.AddMenuEntry( LOCTEXT("SkeletalMesh_New1DBlendspace", "Blend Space 1D"), LOCTEXT("SkeletalMesh_New1DBlendspaceTooltip", "Creates a 1D Blend Space using the selected skeleton."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.BlendSpace1D"), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_BlendSpace1D"), AssetCreated, bInContentBrowser), FCanExecuteAction())); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("CreateAimOffset", LOCTEXT("CreateAimOffsetMenuHeading", "Aim Offsets")); { MenuBuilder.AddMenuEntry( LOCTEXT("SkeletalMesh_New2DAimOffset", "Aim Offset"), LOCTEXT("SkeletalMesh_New2DAimOffsetTooltip", "Creates a Aim Offset blendspace using the selected skeleton."), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_AimOffset2D"), AssetCreated, bInContentBrowser), FCanExecuteAction())); MenuBuilder.AddMenuEntry( LOCTEXT("SkeletalMesh_New1DAimOffset", "Aim Offset 1D"), LOCTEXT("SkeletalMesh_New1DAimOffsetTooltip", "Creates a 1D Aim Offset blendspace using the selected skeleton."), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&ExecuteNewAnimAsset, SkeletonsOrSkeletalMeshes, FString("_AimOffset1D"), AssetCreated, bInContentBrowser), FCanExecuteAction())); } MenuBuilder.EndSection(); } bool ApplyCompressionAlgorithm(TArray& AnimSequencePtrs, UAnimBoneCompressionSettings* OverrideSettings) { const bool bProceed = (AnimSequencePtrs.Num() > 1) ? EAppReturnType::Yes == FMessageDialog::Open(EAppMsgType::YesNo, FText::Format(NSLOCTEXT("UnrealEd", "AboutToCompressAnimations_F", "About to compress {0} animations. Proceed?"), FText::AsNumber(AnimSequencePtrs.Num()))) : true; if (bProceed) { GWarn->BeginSlowTask(LOCTEXT("AnimCompressing", "Compressing"), true); { TSharedPtr CompressContext = MakeShareable(new FAnimCompressContext(false, true, AnimSequencePtrs.Num())); for (UAnimSequence* AnimSeq: AnimSequencePtrs) { if (OverrideSettings != nullptr) { AnimSeq->BoneCompressionSettings = OverrideSettings; } // Clear CompressCommandletVersion so we can recompress these animations later. AnimSeq->CompressCommandletVersion = 0; AnimSeq->RequestAnimCompression(FRequestAnimCompressionParams(true, CompressContext)); ++CompressContext->AnimIndex; } } GWarn->EndSlowTask(); return true; } return false; } bool IsAnimGraph(UEdGraph* Graph) { return Cast(Graph) != nullptr; } void RegenerateSubGraphArrays(UAnimBlueprint* Blueprint) { // The anim graph should be the first function graph on the blueprint if (Blueprint->FunctionGraphs.Num() > 0) { if (UAnimationGraph* AnimGraph = Cast(Blueprint->FunctionGraphs[0])) { RegenerateGraphSubGraphs(Blueprint, AnimGraph); } } } void RegenerateGraphSubGraphs(UAnimBlueprint* OwningBlueprint, UEdGraph* GraphToFix) { TArray ChildGraphs; FindChildGraphsFromNodes(GraphToFix, ChildGraphs); for (UEdGraph* Child: ChildGraphs) { RegenerateGraphSubGraphs(OwningBlueprint, Child); } if (ChildGraphs != GraphToFix->SubGraphs) { UE_LOG(LogAnimation, Log, TEXT("Fixed missing or duplicated graph entries in SubGraph array for graph %s in AnimBP %s"), *GraphToFix->GetName(), *OwningBlueprint->GetName()); GraphToFix->SubGraphs = ChildGraphs; } } void RemoveDuplicateSubGraphs(UEdGraph* GraphToClean) { TArray NewSubGraphArray; for (UEdGraph* SubGraph: GraphToClean->SubGraphs) { NewSubGraphArray.AddUnique(SubGraph); } if (NewSubGraphArray.Num() != GraphToClean->SubGraphs.Num()) { GraphToClean->SubGraphs = NewSubGraphArray; } } void FindChildGraphsFromNodes(UEdGraph* GraphToSearch, TArray& ChildGraphs) { for (UEdGraphNode* CurrentNode: GraphToSearch->Nodes) { if (UAnimGraphNode_StateMachineBase* StateMachine = Cast(CurrentNode)) { ChildGraphs.AddUnique(StateMachine->EditorStateMachineGraph); } else if (UAnimStateNodeBase* StateNode = Cast(CurrentNode)) { ChildGraphs.AddUnique(StateNode->GetBoundGraph()); if (UAnimStateTransitionNode* TransitionNode = Cast(StateNode)) { if (TransitionNode->CustomTransitionGraph) { ChildGraphs.AddUnique(TransitionNode->CustomTransitionGraph); } } } else if (UK2Node_Composite* CompositeNode = Cast(CurrentNode)) { ChildGraphs.AddUnique(CompositeNode->BoundGraph); } } } void SetPoseWatch(UPoseWatch* PoseWatch, UAnimBlueprint* AnimBlueprintIfKnown) { #if WITH_EDITORONLY_DATA if (UAnimGraphNode_Base* TargetNode = Cast(PoseWatch->Node)) { UAnimBlueprint* AnimBlueprint = AnimBlueprintIfKnown ? AnimBlueprintIfKnown : Cast(FBlueprintEditorUtils::FindBlueprintForNode(TargetNode)); if ((AnimBlueprint != NULL) && (AnimBlueprint->GeneratedClass != NULL)) { if (UAnimBlueprintGeneratedClass* AnimBPGenClass = Cast(*AnimBlueprint->GeneratedClass)) { // Find the insertion point from the debugging data int32 LinkID = AnimBPGenClass->GetLinkIDForNode(TargetNode); AnimBPGenClass->GetAnimBlueprintDebugData().AddPoseWatch(LinkID, PoseWatch->PoseWatchColour); } } } #endif // #if WITH_EDITORONLY_DATA } UPoseWatch* FindPoseWatchForNode(const UEdGraphNode* Node, UAnimBlueprint* AnimBlueprintIfKnown) { #if WITH_EDITORONLY_DATA UAnimBlueprint* AnimBlueprint = AnimBlueprintIfKnown ? AnimBlueprintIfKnown : Cast(FBlueprintEditorUtils::FindBlueprintForNode(Node)); if (AnimBlueprint) { // iterate backwards so we can remove invalid pose watches as we go for (int32 Index = AnimBlueprint->PoseWatches.Num() - 1; Index >= 0; --Index) { UPoseWatch* PoseWatch = AnimBlueprint->PoseWatches[Index]; if (PoseWatch == nullptr || PoseWatch->Node == nullptr) { AnimBlueprint->PoseWatches.RemoveAtSwap(Index); continue; } // Return this pose watch if the node location matches the given node if (PoseWatch->Node == Node) { return PoseWatch; } } } return nullptr; #endif } void MakePoseWatchForNode(UAnimBlueprint* AnimBlueprint, UEdGraphNode* Node, FColor PoseWatchColour) { #if WITH_EDITORONLY_DATA UPoseWatch* NewPoseWatch = NewObject(AnimBlueprint); NewPoseWatch->Node = Node; NewPoseWatch->PoseWatchColour = PoseWatchColour; AnimBlueprint->PoseWatches.Add(NewPoseWatch); SetPoseWatch(NewPoseWatch, AnimBlueprint); #endif } void RemovePoseWatch(UPoseWatch* PoseWatch, UAnimBlueprint* AnimBlueprintIfKnown) { #if WITH_EDITORONLY_DATA if (UAnimGraphNode_Base* TargetNode = Cast(PoseWatch->Node)) { UAnimBlueprint* AnimBlueprint = AnimBlueprintIfKnown ? AnimBlueprintIfKnown : Cast(FBlueprintEditorUtils::FindBlueprintForNode(TargetNode)); if (AnimBlueprint) { AnimBlueprint->PoseWatches.Remove(PoseWatch); if (UAnimBlueprintGeneratedClass* AnimBPGenClass = AnimBlueprint->GetAnimBlueprintGeneratedClass()) { int32 LinkID = AnimBPGenClass->GetLinkIDForNode(Cast(PoseWatch->Node)); AnimBPGenClass->GetAnimBlueprintDebugData().RemovePoseWatch(LinkID); } } } #endif } void UpdatePoseWatchColour(UPoseWatch* PoseWatch, FColor NewPoseWatchColour) { #if WITH_EDITORONLY_DATA PoseWatch->PoseWatchColour = NewPoseWatchColour; if (UAnimGraphNode_Base* TargetNode = Cast(PoseWatch->Node)) { UAnimBlueprint* AnimBlueprint = Cast(FBlueprintEditorUtils::FindBlueprintForNode(TargetNode)); if ((AnimBlueprint != NULL) && (AnimBlueprint->GeneratedClass != NULL)) { if (UAnimBlueprintGeneratedClass* AnimBPGenClass = Cast(*AnimBlueprint->GeneratedClass)) { // Find the insertion point from the debugging data int32 LinkID = AnimBPGenClass->GetLinkIDForNode(TargetNode); AnimBPGenClass->GetAnimBlueprintDebugData().UpdatePoseWatchColour(LinkID, NewPoseWatchColour); } } } #endif } } // namespace AnimationEditorUtils #undef LOCTEXT_NAMESPACE