// Copyright Epic Games, Inc. All Rights Reserved. #include "SClothAssetSelector.h" #include "Animation/DebugSkelMeshComponent.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetData.h" #include "ClothLODData.h" #include "ClothPhysicalMeshData.h" #include "ClothingAsset.h" #include "ClothingAssetBase.h" #include "ClothingAssetExporter.h" #include "ClothingAssetFactoryInterface.h" #include "ClothingAssetListCommands.h" #include "ClothingSimulationFactory.h" #include "ClothingSystemEditorInterfaceModule.h" #include "Containers/ContainersFwd.h" #include "Containers/IndirectArray.h" #include "Containers/Map.h" #include "Containers/UnrealString.h" #include "ContentBrowserDelegates.h" #include "ContentBrowserModule.h" #include "DetailLayoutBuilder.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Engine/SkeletalMesh.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Views/ITypedTableView.h" #include "IContentBrowserSingleton.h" #include "Input/Events.h" #include "InputCoreTypes.h" #include "Internationalization/Internationalization.h" #include "Layout/Children.h" #include "Layout/Margin.h" #include "Layout/WidgetPath.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Modules/ModuleManager.h" #include "PointWeightMap.h" #include "Rendering/SkeletalMeshLODModel.h" #include "Rendering/SkeletalMeshModel.h" #include "SCopyVertexColorSettingsPanel.h" #include "SPositiveActionButton.h" #include "ScopedTransaction.h" #include "SkeletalMeshTypes.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Styling/ISlateStyle.h" #include "Styling/SlateColor.h" #include "Templates/Casts.h" #include "Templates/SubclassOf.h" #include "Textures/SlateIcon.h" #include "Types/SlateStructs.h" #include "UObject/Class.h" #include "UObject/NameTypes.h" #include "UObject/TopLevelAssetPath.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealNames.h" #include "Utils/ClothingMeshUtils.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SExpandableArea.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SHeaderRow.h" #include "Widgets/Views/STableRow.h" class FUICommandInfo; class ITableRow; class STableViewBase; class SWidget; class UObject; struct FGeometry; #define LOCTEXT_NAMESPACE "ClothAssetSelector" FPointWeightMap* FClothingMaskListItem::GetMask() { if(UClothingAssetCommon* Asset = ClothingAsset.Get()) { if(Asset->IsValidLod(LodIndex)) { FClothLODDataCommon& LodData = Asset->LodData[LodIndex]; if(LodData.PointWeightMaps.IsValidIndex(MaskIndex)) { return &LodData.PointWeightMaps[MaskIndex]; } } } return nullptr; } FClothPhysicalMeshData* FClothingMaskListItem::GetMeshData() { UClothingAssetCommon* Asset = ClothingAsset.Get(); return Asset && Asset->IsValidLod(LodIndex) ? &Asset->LodData[LodIndex].PhysicalMeshData : nullptr; } USkeletalMesh* FClothingMaskListItem::GetOwningMesh() { UClothingAssetCommon* Asset = ClothingAsset.Get(); return Asset ? Cast(Asset->GetOuter()) : nullptr; } class SAssetListRow : public STableRow> { public: SLATE_BEGIN_ARGS(SAssetListRow) {} SLATE_EVENT(FSimpleDelegate, OnInvalidateList) SLATE_END_ARGS() void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem) { Item = InItem; OnInvalidateList = InArgs._OnInvalidateList; BindCommands(); STableRow>::Construct( STableRow>::FArguments() .Content() [ SNew(SBox) .Padding(2.0f) [ SAssignNew(EditableText, SInlineEditableTextBlock) .Text(this, &SAssetListRow::GetAssetName) .OnTextCommitted(this, &SAssetListRow::OnCommitAssetName) .IsSelected(this, &SAssetListRow::IsSelected) ] ], InOwnerTable ); } FText GetAssetName() const { if(Item.IsValid()) { return FText::FromString(Item->ClothingAsset->GetName()); } return FText::GetEmpty(); } void OnCommitAssetName(const FText& InText, ETextCommit::Type CommitInfo) { if(Item.IsValid()) { if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { FText TrimText = FText::TrimPrecedingAndTrailing(InText); if(Asset->GetName() != TrimText.ToString()) { FName NewName(*TrimText.ToString()); // Check for an existing object, and if we find one build a unique name based on the request if(UObject* ExistingObject = StaticFindObject(UClothingAssetCommon::StaticClass(), Asset->GetOuter(), *NewName.ToString())) { NewName = MakeUniqueObjectName(Asset->GetOuter(), UClothingAssetCommon::StaticClass(), FName(*TrimText.ToString())); } Asset->Rename(*NewName.ToString(), Asset->GetOuter()); } } } } void BindCommands() { check(!UICommandList.IsValid()); UICommandList = MakeShareable(new FUICommandList); const FClothingAssetListCommands& Commands = FClothingAssetListCommands::Get(); UICommandList->MapAction( FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &SAssetListRow::DeleteAsset) ); UICommandList->MapAction( Commands.RebuildAssetParams, FExecuteAction::CreateSP(this, &SAssetListRow::RebuildLODParameters), FCanExecuteAction::CreateSP(this, &SAssetListRow::CanRebuildLODParameters) ); // Add clothing asset exporters ForEachClothingAssetExporter([this, &Commands](UClass* ExportedType) { if (const TSharedPtr* const CommandId = Commands.ExportAssets.Find(ExportedType->GetFName())) { UICommandList->MapAction( *CommandId, FExecuteAction::CreateSP(this, &SAssetListRow::ExportAsset, ExportedType)); } }); } virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { if(Item.IsValid() && MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { const FClothingAssetListCommands& Commands = FClothingAssetListCommands::Get(); FMenuBuilder Builder(true, UICommandList); Builder.BeginSection(NAME_None, LOCTEXT("AssetActions_SectionName", "Actions")); { Builder.AddMenuEntry(FGenericCommands::Get().Delete); Builder.AddMenuEntry(Commands.RebuildAssetParams); for (const TPair>& CommandId : Commands.ExportAssets) { Builder.AddMenuEntry(CommandId.Value); } } Builder.EndSection(); FWidgetPath Path = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); FSlateApplication::Get().PushMenu(AsShared(), Path, Builder.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect::ContextMenu); return FReply::Handled(); } return STableRow>::OnMouseButtonUp(MyGeometry, MouseEvent); } private: void DeleteAsset() { //Lambda use to sync one of the UserSectionData section from one LOD Model auto SetSkelMeshSourceSectionUserData = [](FSkeletalMeshLODModel& LODModel, const int32 SectionIndex, const int32 OriginalSectionIndex) { FSkelMeshSourceSectionUserData& SourceSectionUserData = LODModel.UserSectionsData.FindOrAdd(OriginalSectionIndex); SourceSectionUserData.bDisabled = LODModel.Sections[SectionIndex].bDisabled; SourceSectionUserData.bCastShadow = LODModel.Sections[SectionIndex].bCastShadow; SourceSectionUserData.bVisibleInRayTracing = LODModel.Sections[SectionIndex].bVisibleInRayTracing; SourceSectionUserData.bRecomputeTangent = LODModel.Sections[SectionIndex].bRecomputeTangent; SourceSectionUserData.GenerateUpToLodIndex = LODModel.Sections[SectionIndex].GenerateUpToLodIndex; SourceSectionUserData.CorrespondClothAssetIndex = LODModel.Sections[SectionIndex].CorrespondClothAssetIndex; SourceSectionUserData.ClothingData = LODModel.Sections[SectionIndex].ClothingData; }; if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { if(USkeletalMesh* SkelMesh = Cast(Asset->GetOuter())) { FScopedSuspendAlternateSkinWeightPreview ScopedSuspendAlternateSkinnWeightPreview(SkelMesh); int32 AssetIndex; if(SkelMesh->GetMeshClothingAssets().Find(Asset, AssetIndex)) { // Need to unregister our components so they shut down their current clothing simulation FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkelMesh); SkelMesh->PreEditChange(nullptr); Asset->UnbindFromSkeletalMesh(SkelMesh); SkelMesh->GetMeshClothingAssets().RemoveAt(AssetIndex); // Need to fix up asset indices on sections. if(FSkeletalMeshModel* Model = SkelMesh->GetImportedModel()) { for(FSkeletalMeshLODModel& LodModel : Model->LODModels) { for (int32 SectionIndex = 0; SectionIndex < LodModel.Sections.Num(); ++SectionIndex) { FSkelMeshSection& Section = LodModel.Sections[SectionIndex]; if(Section.CorrespondClothAssetIndex > AssetIndex) { --Section.CorrespondClothAssetIndex; //Keep the user section data (build source data) in sync SetSkelMeshSourceSectionUserData(LodModel, SectionIndex, Section.OriginalDataSectionIndex); } } } } OnInvalidateList.ExecuteIfBound(); } } } } // Using LOD0 of an asset, rebuild the other LOD masks by mapping the LOD0 parameters onto their meshes void RebuildLODParameters() { if(!Item.IsValid()) { return; } if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { const int32 NumLods = Asset->GetNumLods(); for(int32 CurrIndex = 0; CurrIndex < NumLods - 1; ++CurrIndex) { FClothLODDataCommon& SourceLod = Asset->LodData[CurrIndex]; FClothLODDataCommon& DestLod = Asset->LodData[CurrIndex + 1]; DestLod.PointWeightMaps.Reset(); for(FPointWeightMap& SourceMask : SourceLod.PointWeightMaps) { DestLod.PointWeightMaps.AddDefaulted(); FPointWeightMap& DestMask = DestLod.PointWeightMaps.Last(); DestMask.Name = SourceMask.Name; DestMask.bEnabled = SourceMask.bEnabled; DestMask.CurrentTarget = SourceMask.CurrentTarget; ClothingMeshUtils::FVertexParameterMapper ParameterMapper( DestLod.PhysicalMeshData.Vertices, DestLod.PhysicalMeshData.Normals, SourceLod.PhysicalMeshData.Vertices, SourceLod.PhysicalMeshData.Normals, SourceLod.PhysicalMeshData.Indices ); ParameterMapper.Map(SourceMask.Values, DestMask.Values); } } } } void ExportAsset(UClass* ExportedType) { if (!Item.IsValid() || !ExportedType) { return; } if (const UClothingAssetCommon* const ClothingAsset = Item->ClothingAsset.Get()) { ExportClothingAsset(ClothingAsset, ExportedType); } } bool CanRebuildLODParameters() const { if(!Item.IsValid()) { return false; } if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { if(Asset->GetNumLods() > 1) { return true; } } return false; } TSharedPtr Item; TSharedPtr EditableText; FSimpleDelegate OnInvalidateList; TSharedPtr UICommandList; }; class SMaskListRow : public SMultiColumnTableRow> { public: SLATE_BEGIN_ARGS(SMaskListRow) {} SLATE_EVENT(FSimpleDelegate, OnInvalidateList) SLATE_END_ARGS() void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTable, TSharedPtr InItem, TSharedPtr InAssetSelector ) { OnInvalidateList = InArgs._OnInvalidateList; Item = InItem; AssetSelectorPtr = InAssetSelector; BindCommands(); SMultiColumnTableRow>::Construct(FSuperRowType::FArguments(), InOwnerTable); } static FName Column_Enabled; static FName Column_MaskName; static FName Column_CurrentTarget; virtual TSharedRef GenerateWidgetForColumn(const FName& InColumnName) override { if(InColumnName == Column_Enabled) { return SNew(SBox) .Padding(2.f) [ SNew(SCheckBox) .IsEnabled(this, &SMaskListRow::IsMaskCheckboxEnabled, Item) .IsChecked(this, &SMaskListRow::IsMaskEnabledChecked, Item) .OnCheckStateChanged(this, &SMaskListRow::OnMaskEnabledCheckboxChanged, Item) .ToolTipText(LOCTEXT("MaskEnableCheckBox_ToolTip", "Sets whether this mask is enabled and can affect final parameters for its target parameter.")) ]; } if(InColumnName == Column_MaskName) { return SAssignNew(InlineText, SInlineEditableTextBlock) .Text(this, &SMaskListRow::GetMaskName) .OnTextCommitted(this, &SMaskListRow::OnCommitMaskName) .IsSelected(this, &SMultiColumnTableRow>::IsSelectedExclusively); } if(InColumnName == Column_CurrentTarget) { // Retrieve the mask names for the current clothing simulation factory const TSubclassOf ClothingSimulationFactory = UClothingSimulationFactory::GetDefaultClothingSimulationFactoryClass(); if (ClothingSimulationFactory.Get() != nullptr) { const UEnum* const Enum = ClothingSimulationFactory.GetDefaultObject()->GetWeightMapTargetEnum(); const FPointWeightMap* const Mask = Item->GetMask(); if(Enum && Mask) { return SNew(STextBlock).Text(Enum->GetDisplayNameTextByValue((int64)Mask->CurrentTarget)); } } } return SNullWidget::NullWidget; } FText GetMaskName() const { if(Item.IsValid()) { if(FPointWeightMap* Mask = Item->GetMask()) { return FText::FromName(Mask->Name); } } return LOCTEXT("MaskName_Invalid", "Invalid Mask"); } void OnCommitMaskName(const FText& InText, ETextCommit::Type CommitInfo) { if(Item.IsValid()) { if(FPointWeightMap* Mask = Item->GetMask()) { FText TrimText = FText::TrimPrecedingAndTrailing(InText); Mask->Name = FName(*TrimText.ToString()); } } } virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { // Spawn menu if(MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && Item.IsValid()) { if(FPointWeightMap* Mask = Item->GetMask()) { FMenuBuilder Builder(true, UICommandList); FUIAction DeleteAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnDeleteMask)); Builder.BeginSection(NAME_None, LOCTEXT("MaskActions_SectionName", "Actions")); { Builder.AddSubMenu(LOCTEXT("MaskActions_SetTarget", "Set Target"), LOCTEXT("MaskActions_SetTarget_Tooltip", "Choose the target for this mask"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildTargetSubmenu)); Builder.AddMenuEntry(FGenericCommands::Get().Delete); Builder.AddSubMenu(LOCTEXT("MaskActions_CopyFromMask", "Copy From Mask"), LOCTEXT("MaskActions_CopyFromMask_Tooltip", "Replace this mask with values from another mask on this cloth"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildCopyMaskSubmenu)); Builder.AddSubMenu(LOCTEXT("MaskActions_CopyFromVertexColor", "Copy From Vertex Color"), LOCTEXT("MaskActions_CopyFromVertexColor_Tooltip", "Replace this mask with values from vertex color channel on sim mesh"), FNewMenuDelegate::CreateSP(this, &SMaskListRow::BuildCopyVertexColorSubmenu)); } Builder.EndSection(); FWidgetPath Path = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); FSlateApplication::Get().PushMenu(AsShared(), Path, Builder.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect::ContextMenu); return FReply::Handled(); } } return SMultiColumnTableRow>::OnMouseButtonUp(MyGeometry, MouseEvent); } void EditName() { if(InlineText.IsValid()) { InlineText->EnterEditingMode(); } } private: void BindCommands() { check(!UICommandList.IsValid()); UICommandList = MakeShareable(new FUICommandList); UICommandList->MapAction( FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &SMaskListRow::OnDeleteMask) ); } FClothLODDataCommon* GetCurrentLod() const { if(Item.IsValid()) { if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { if(Asset->LodData.IsValidIndex(Item->LodIndex)) { return &Asset->LodData[Item->LodIndex]; } } } return nullptr; } void OnDeleteMask() { USkeletalMesh* CurrentMesh = Item->GetOwningMesh(); if(CurrentMesh) { FScopedTransaction CurrTransaction(LOCTEXT("DeleteMask_Transaction", "Delete clothing parameter mask.")); Item->ClothingAsset->Modify(); if(FClothLODDataCommon* LodData = GetCurrentLod()) { if(LodData->PointWeightMaps.IsValidIndex(Item->MaskIndex)) { LodData->PointWeightMaps.RemoveAt(Item->MaskIndex); // We've removed a mask, so it will need to be applied to the clothing data if(Item.IsValid()) { if(UClothingAssetCommon* Asset = Item->ClothingAsset.Get()) { Asset->ApplyParameterMasks(); } } OnInvalidateList.ExecuteIfBound(); } } } } void OnSetTarget(int32 InTargetEntryIndex) { USkeletalMesh* CurrentMesh = Item->GetOwningMesh(); if(Item.IsValid() && CurrentMesh) { FScopedTransaction CurrTransaction(LOCTEXT("SetMaskTarget_Transaction", "Set clothing parameter mask target.")); Item->ClothingAsset->Modify(); if(FPointWeightMap* Mask = Item->GetMask()) { Mask->CurrentTarget = (uint8)InTargetEntryIndex; if(Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::None) { // Make sure to disable this mask if it has no valid target Mask->bEnabled = false; } OnInvalidateList.ExecuteIfBound(); } } } void OnCopyMask(int32 InMaskIndex) { if (AssetSelectorPtr.IsValid() && Item.IsValid()) { if (USkeletalMesh* CurrentMesh = Item->GetOwningMesh()) { if (FPointWeightMap* Mask = Item->GetMask()) { if (UClothingAssetCommon* const ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get()) { const int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod(); if (ClothingAsset->LodData.IsValidIndex(LOD)) { const FClothLODDataCommon& LodData = ClothingAsset->LodData[LOD]; if (LodData.PointWeightMaps.IsValidIndex(InMaskIndex)) { { FScopedTransaction CurrTransaction(LOCTEXT("CopyMask_Transaction", "Copy Mask.")); Item->ClothingAsset->Modify(); Mask->Values = LodData.PointWeightMaps[InMaskIndex].Values; } const bool bUpdateFixedVertData = (Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::MaxDistance); ClothingAsset->ApplyParameterMasks(bUpdateFixedVertData); } } } } } } } void BuildTargetSubmenu(FMenuBuilder& Builder) { Builder.BeginSection(NAME_None, LOCTEXT("MaskTargets_SectionName", "Targets")); { // Retrieve the mask names for the current clothing simulation factory const TSubclassOf ClothingSimulationFactory = UClothingSimulationFactory::GetDefaultClothingSimulationFactoryClass(); if (ClothingSimulationFactory.Get() != nullptr) { if (const UEnum* const Enum = ClothingSimulationFactory.GetDefaultObject()->GetWeightMapTargetEnum()) { const int32 NumEntries = Enum->NumEnums(); // Iterate to -1 to skip the _MAX entry appended to the end of the enum for(int32 Index = 0; Index < NumEntries - 1; ++Index) { FUIAction EntryAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnSetTarget, (int32)Enum->GetValueByIndex(Index))); FText EntryText = Enum->GetDisplayNameTextByIndex(Index); Builder.AddMenuEntry(EntryText, FText::GetEmpty(), FSlateIcon(), EntryAction); } } } } Builder.EndSection(); } /** Build sub menu for choosing which vertex color channel to copy to selected mask */ void BuildCopyMaskSubmenu(FMenuBuilder& Builder) { if (AssetSelectorPtr.IsValid() && Item.IsValid()) { if (const UClothingAssetCommon* const ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get()) { const int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod(); if (ClothingAsset->LodData.IsValidIndex(LOD)) { const FClothLODDataCommon& LodData = ClothingAsset->LodData[LOD]; Builder.BeginSection(NAME_None, LOCTEXT("Masks_SectionName", "Masks")); for (int32 MaskIndex = 0; MaskIndex < LodData.PointWeightMaps.Num(); ++MaskIndex) { if (MaskIndex == Item->MaskIndex) { continue; } FUIAction EntryAction(FExecuteAction::CreateSP(this, &SMaskListRow::OnCopyMask, MaskIndex)); Builder.AddMenuEntry(FText::FromName(LodData.PointWeightMaps[MaskIndex].Name), FText::GetEmpty(), FSlateIcon(), EntryAction); } Builder.EndSection(); } } } } /** Build sub menu for choosing which vertex color channel to copy to selected mask */ void BuildCopyVertexColorSubmenu(FMenuBuilder& Builder) { if (AssetSelectorPtr.IsValid()) { UClothingAssetCommon* ClothingAsset = AssetSelectorPtr.Pin()->GetSelectedAsset().Get(); int32 LOD = AssetSelectorPtr.Pin()->GetSelectedLod(); FPointWeightMap* Mask = Item->GetMask(); TSharedRef Widget = SNew(SCopyVertexColorSettingsPanel, ClothingAsset, LOD, Mask); Builder.AddWidget(Widget, FText::GetEmpty(), true, false); } } // Mask enabled checkbox handling ECheckBoxState IsMaskEnabledChecked(TSharedPtr InItem) const { if(InItem.IsValid()) { if(FPointWeightMap* Mask = InItem->GetMask()) { return Mask->bEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } } return ECheckBoxState::Unchecked; } bool IsMaskCheckboxEnabled(TSharedPtr InItem) const { if(InItem.IsValid()) { if(FPointWeightMap* Mask = InItem->GetMask()) { return Mask->CurrentTarget != (uint8)EWeightMapTargetCommon::None; } } return false; } void OnMaskEnabledCheckboxChanged(ECheckBoxState InState, TSharedPtr InItem) { if(InItem.IsValid()) { if(FPointWeightMap* Mask = InItem->GetMask()) { const bool bNewEnableState = (InState == ECheckBoxState::Checked); if(Mask->bEnabled != bNewEnableState) { if(bNewEnableState) { // Disable all other masks that affect this target (there can only be one mask enabled of the same target type at the same time) if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get()) { if(Asset->LodData.IsValidIndex(InItem->LodIndex)) { FClothLODDataCommon& LodData = Asset->LodData[InItem->LodIndex]; TArray AllTargetMasks; LodData.GetParameterMasksForTarget(Mask->CurrentTarget, AllTargetMasks); for(FPointWeightMap* TargetMask : AllTargetMasks) { if(TargetMask && TargetMask != Mask) { TargetMask->bEnabled = false; } } } } } // Set the flag Mask->bEnabled = bNewEnableState; if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get()) { const bool bUpdateFixedVertData = (Mask->CurrentTarget == (uint8)EWeightMapTargetCommon::MaxDistance); Asset->ApplyParameterMasks(bUpdateFixedVertData); } } } } } FSimpleDelegate OnInvalidateList; TSharedPtr Item; TSharedPtr InlineText; TSharedPtr UICommandList; TWeakPtr AssetSelectorPtr; }; FName SMaskListRow::Column_Enabled(TEXT("Enabled")); FName SMaskListRow::Column_MaskName(TEXT("Name")); FName SMaskListRow::Column_CurrentTarget(TEXT("CurrentTarget")); SClothAssetSelector::~SClothAssetSelector() { if(Mesh) { Mesh->UnregisterOnClothingChange(MeshClothingChangedHandle); } if(GEditor) { GEditor->UnregisterForUndo(this); } } void SClothAssetSelector::Construct(const FArguments& InArgs, USkeletalMesh* InMesh) { FClothingAssetListCommands::Register(); Mesh = InMesh; OnSelectionChanged = InArgs._OnSelectionChanged; // Register callback for external changes to clothing items if(Mesh) { MeshClothingChangedHandle = Mesh->RegisterOnClothingChange(FSimpleMulticastDelegate::FDelegate::CreateSP(this, &SClothAssetSelector::OnRefresh)); } if(GEditor) { GEditor->RegisterForUndo(this); } ChildSlot [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f)) .AutoHeight() [ SNew(SExpandableArea) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .HeaderContent() [ SAssignNew(AssetHeaderBox, SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(10.f, 0.f, 0.f, 0.f) [ SNew(STextBlock) .Text(LOCTEXT("AssetExpander_Title", "Clothing Data")) .TransformPolicy(ETextTransformPolicy::ToUpper) .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") .Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont")) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Right) .Padding(0.0f, 0.0f, 4.0f, 0.0f) [ SNew(SPositiveActionButton) .Text(LOCTEXT("CopyClothingFromMeshText_TEXT", "Add Clothing")) .Icon(FAppStyle::Get().GetBrush("Icons.Plus")) .ToolTipText(LOCTEXT("CopyClothingFromMeshText_TOOLTIP", "Copy Clothing from SkeletalMesh")) .OnGetMenuContent(this, &SClothAssetSelector::OnGenerateSkeletalMeshPickerForClothCopy) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SNew(SComboButton) .ForegroundColor(FSlateColor::UseStyle()) .OnGetMenuContent(this, &SClothAssetSelector::OnGetLodMenu) .HasDownArrow(true) .ButtonContent() [ SNew(STextBlock) .Text(this, &SClothAssetSelector::GetLodButtonText) ] ] ] .BodyContent() [ SNew(SBox) .Padding(FMargin(0.f, 2.0f)) .MinDesiredHeight(100.0f) [ SAssignNew(AssetList, SAssetList) .ListItemsSource(&AssetListItems) .OnGenerateRow(this, &SClothAssetSelector::OnGenerateWidgetForClothingAssetItem) .OnSelectionChanged(this, &SClothAssetSelector::OnAssetListSelectionChanged) .ClearSelectionOnClick(false) .SelectionMode(ESelectionMode::Single) ] ] ] + SVerticalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f)) .AutoHeight() [ SNew(SExpandableArea) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .Padding(FMargin(0.f, 2.0f)) .HeaderContent() [ SAssignNew(MaskHeaderBox, SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(10.f, 0.f, 0.f, 0.f) [ SNew(STextBlock) .Text(LOCTEXT("MaskExpander_Title", "Masks")) .TransformPolicy(ETextTransformPolicy::ToUpper) .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") .Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont")) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SAssignNew(NewMaskButton, SButton) .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) .OnClicked(this, &SClothAssetSelector::AddNewMask) .IsEnabled(this, &SClothAssetSelector::CanAddNewMask) .ToolTipText(LOCTEXT("AddMask_Tooltip", "Add a Mask")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("Icons.PlusCircle")) ] ] ] .BodyContent() [ SNew(SBox) .MinDesiredHeight(100.0f) [ SAssignNew(MaskList, SMaskList) .ListItemsSource(&MaskListItems) .OnGenerateRow(this, &SClothAssetSelector::OnGenerateWidgetForMaskItem) .OnSelectionChanged(this, &SClothAssetSelector::OnMaskSelectionChanged) .ClearSelectionOnClick(false) .SelectionMode(ESelectionMode::Single) .HeaderRow ( SNew(SHeaderRow) + SHeaderRow::Column(SMaskListRow::Column_Enabled) .FixedWidth(40) .HAlignCell(HAlign_Right) .DefaultLabel(FText::GetEmpty()) + SHeaderRow::Column(SMaskListRow::Column_MaskName) .FillWidth(0.5f) .DefaultLabel(LOCTEXT("MaskListHeader_Name", "Name")) + SHeaderRow::Column(SMaskListRow::Column_CurrentTarget) .FillWidth(0.3f) .DefaultLabel(LOCTEXT("MaskListHeader_Target", "Target")) ) ] ] ] + SVerticalBox::Slot() // Mesh to mesh skinning .AutoHeight() .Padding(FMargin(0.0f, 0.0f, 0.0f, 1.0f)) [ SNew(SExpandableArea) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .Padding(FMargin(0.f, 2.0f)) .HeaderContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(10, 8, 0, 8) [ SNew(STextBlock) .Text(LOCTEXT("MeshSkinning_Title", "Mesh Skinning")) .TransformPolicy(ETextTransformPolicy::ToUpper) .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") .Font(FAppStyle::Get().GetFontStyle("PropertyWindow.BoldFont")) ] ] .BodyContent() [ // TODO: Replace this with a table view or something more suitable. UETOOL-2341 SNew(SSplitter) .Orientation(EOrientation::Orient_Horizontal) .PhysicalSplitterHandleSize(1.0) + SSplitter::Slot() .Value(.3f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(16.0f, 2.0f, 0.0f, 2.0f) [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("PropertyWindow.NormalFont")) .Text(LOCTEXT("MultipleInfluences", "Use Multiple Influences")) ] + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(16.0f, 2.0f, 0.0f, 2.0f) [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("PropertyWindow.NormalFont")) .Text(LOCTEXT("CurrentRadius", "Kernel Radius")) ] + SVerticalBox::Slot() .Padding(0.0f, 2.0f, 0.0f, 2.0f) .AutoHeight() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFontBold()) .Text(LOCTEXT("SmoothTransition", "Smooth Transition From Skin to Cloth")) .ShadowOffset(FVector2D(1, 1)) ] ] + SSplitter::Slot() [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(12.0f, 4.0f, 0.0f, 4.0f) [ SNew(SCheckBox) .IsChecked(this, &SClothAssetSelector::GetCurrentUseMultipleInfluences) .OnCheckStateChanged(this, &SClothAssetSelector::OnCurrentUseMultipleInfluencesChanged) .IsEnabled(this, &SClothAssetSelector::IsValidClothLodSelected) ] + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(12.0f, 4.0f, 0.0f, 4.0f) [ SNew(SNumericEntryBox) .AllowSpin(true) .MinSliderValue(0) .MinValue(0) .MaxSliderValue(TOptional(1000.0f)) .IsEnabled(this, &SClothAssetSelector::CurrentKernelRadiusIsEnabled) .UndeterminedString(FText::FromString("????")) .Value(this, &SClothAssetSelector::GetCurrentKernelRadius) .OnValueCommitted(this, &SClothAssetSelector::OnCurrentKernelRadiusCommitted) .OnValueChanged(this, &SClothAssetSelector::OnCurrentKernelRadiusChanged) .LabelPadding(0) ] + SVerticalBox::Slot() .Padding(0.0f, 2.0f, 0.0f, 2.0f) .AutoHeight() [ SNew(SCheckBox) .IsChecked(this, &SClothAssetSelector::GetCurrentSmoothTransition) .OnCheckStateChanged(this, &SClothAssetSelector::OnCurrentSmoothTransitionChanged) .IsEnabled(this, &SClothAssetSelector::IsValidClothLodSelected) ] ] ] ] ]; RefreshAssetList(); RefreshMaskList(); } TOptional SClothAssetSelector::GetCurrentKernelRadius() const { UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; return LodData.SkinningKernelRadius; } return TOptional(); } void SClothAssetSelector::OnCurrentKernelRadiusChanged(float InValue) { UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; LodData.SkinningKernelRadius = InValue; } } void SClothAssetSelector::OnCurrentKernelRadiusCommitted(float InValue, ETextCommit::Type CommitType) { UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; LodData.SkinningKernelRadius = InValue; // Recompute weights if (USkeletalMesh* SkeletalMesh = Cast(Asset->GetOuter())) { FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh); SkeletalMesh->InvalidateDeriveDataCacheGUID(); } } } bool SClothAssetSelector::CurrentKernelRadiusIsEnabled() const { return (this->GetCurrentUseMultipleInfluences() == ECheckBoxState::Checked); } ECheckBoxState SClothAssetSelector::GetCurrentUseMultipleInfluences() const { UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; return LodData.bUseMultipleInfluences ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } return ECheckBoxState::Undetermined; } void SClothAssetSelector::OnCurrentUseMultipleInfluencesChanged(ECheckBoxState InValue) { if (InValue == ECheckBoxState::Undetermined) { return; } UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; LodData.bUseMultipleInfluences = (InValue == ECheckBoxState::Checked); // Recompute weights if (USkeletalMesh* SkeletalMesh = Cast(Asset->GetOuter())) { FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh); SkeletalMesh->InvalidateDeriveDataCacheGUID(); } } } ECheckBoxState SClothAssetSelector::GetCurrentSmoothTransition() const { UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; return LodData.bSmoothTransition ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } return ECheckBoxState::Undetermined; } void SClothAssetSelector::OnCurrentSmoothTransitionChanged(ECheckBoxState InValue) { if (InValue == ECheckBoxState::Undetermined) { return; } UClothingAssetCommon* Asset = SelectedAsset.Get(); if (Asset && Asset->IsValidLod(SelectedLod)) { FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; LodData.bSmoothTransition = (InValue == ECheckBoxState::Checked); // Recompute weights if (USkeletalMesh* SkeletalMesh = Cast(Asset->GetOuter())) { FScopedSkeletalMeshPostEditChange ScopedSkeletalMeshPostEditChange(SkeletalMesh); SkeletalMesh->InvalidateDeriveDataCacheGUID(); } } } bool SClothAssetSelector::IsValidClothLodSelected() const { UClothingAssetCommon* Asset = SelectedAsset.Get(); return (Asset && Asset->IsValidLod(SelectedLod)); } TWeakObjectPtr SClothAssetSelector::GetSelectedAsset() const { return SelectedAsset; } int32 SClothAssetSelector::GetSelectedLod() const { return SelectedLod; } int32 SClothAssetSelector::GetSelectedMask() const { return SelectedMask; } void SClothAssetSelector::PostUndo(bool bSuccess) { OnRefresh(); } void SClothAssetSelector::OnCopyClothingAssetSelected(const FAssetData& AssetData) { USkeletalMesh* SourceSkelMesh = Cast(AssetData.GetAsset()); if (Mesh && SourceSkelMesh && Mesh != SourceSkelMesh) { FScopedTransaction Transaction(LOCTEXT("CopiedClothingAssetsFromSkelMesh", "Copied clothing assets from another SkelMesh")); Mesh->Modify(); FClothingSystemEditorInterfaceModule& ClothingEditorModule = FModuleManager::LoadModuleChecked("ClothingSystemEditorInterface"); UClothingAssetFactoryBase* AssetFactory = ClothingEditorModule.GetClothingAssetFactory(); for (UClothingAssetBase* ClothingAsset : SourceSkelMesh->GetMeshClothingAssets()) { UClothingAssetCommon* NewAsset = Cast(AssetFactory->CreateFromExistingCloth(Mesh, SourceSkelMesh, ClothingAsset)); Mesh->AddClothingAsset(NewAsset); } OnRefresh(); } FSlateApplication::Get().DismissAllMenus(); } TSharedRef SClothAssetSelector::OnGenerateSkeletalMeshPickerForClothCopy() { FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked(TEXT("ContentBrowser")); FAssetPickerConfig AssetPickerConfig; AssetPickerConfig.Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName()); AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SClothAssetSelector::OnCopyClothingAssetSelected); AssetPickerConfig.bAllowNullSelection = true; AssetPickerConfig.InitialAssetViewType = EAssetViewType::List; AssetPickerConfig.bFocusSearchBoxWhenOpened = true; AssetPickerConfig.bShowBottomToolbar = false; AssetPickerConfig.SelectionMode = ESelectionMode::Single; return SNew(SBox) .WidthOverride(300) .HeightOverride(400) [ ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig) ]; } EVisibility SClothAssetSelector::GetAssetHeaderButtonTextVisibility() const { bool bShow = AssetHeaderBox.IsValid() && AssetHeaderBox->IsHovered(); return bShow ? EVisibility::HitTestInvisible : EVisibility::Collapsed; } EVisibility SClothAssetSelector::GetMaskHeaderButtonTextVisibility() const { bool bShow = MaskHeaderBox.IsValid() && MaskHeaderBox->IsHovered(); return bShow ? EVisibility::HitTestInvisible : EVisibility::Collapsed; } TSharedRef SClothAssetSelector::OnGetLodMenu() { FMenuBuilder Builder(true, nullptr); int32 NumLods = 0; if(UClothingAssetCommon* CurrAsset = SelectedAsset.Get()) { NumLods = CurrAsset->GetNumLods(); } if(NumLods == 0) { Builder.AddMenuEntry(LOCTEXT("LodMenu_NoLods", "Select an asset..."), FText::GetEmpty(), FSlateIcon(), FUIAction()); } else { for(int32 LodIdx = 0; LodIdx < NumLods; ++LodIdx) { FText ItemText = FText::Format(LOCTEXT("LodMenuItem", "LOD{0}"), FText::AsNumber(LodIdx)); FText ToolTipText = FText::Format(LOCTEXT("LodMenuItemToolTip", "Select LOD{0}"), FText::AsNumber(LodIdx)); FUIAction Action; Action.ExecuteAction.BindSP(this, &SClothAssetSelector::OnClothingLodSelected, LodIdx); Builder.AddMenuEntry(ItemText, ToolTipText, FSlateIcon(), Action); } } return Builder.MakeWidget(); } FText SClothAssetSelector::GetLodButtonText() const { if(SelectedLod == INDEX_NONE) { return LOCTEXT("LodButtonGenTextEmpty", "LOD"); } return FText::Format(LOCTEXT("LodButtonGenText", "LOD{0}"), FText::AsNumber(SelectedLod)); } TSharedRef SClothAssetSelector::OnGenerateWidgetForClothingAssetItem(TSharedPtr InItem, const TSharedRef& OwnerTable) { if(UClothingAssetCommon* Asset = InItem->ClothingAsset.Get()) { return SNew(SAssetListRow, OwnerTable, InItem) .OnInvalidateList(this, &SClothAssetSelector::OnRefresh); } return SNew(STableRow>, OwnerTable) .Content() [ SNew(STextBlock).Text(FText::FromString(TEXT("No Assets Available"))) ]; } void SClothAssetSelector::OnAssetListSelectionChanged(TSharedPtr InSelectedItem, ESelectInfo::Type InSelectInfo) { if(InSelectedItem.IsValid() && InSelectInfo != ESelectInfo::Direct) { SetSelectedAsset(InSelectedItem->ClothingAsset); } } TSharedRef SClothAssetSelector::OnGenerateWidgetForMaskItem(TSharedPtr InItem, const TSharedRef& OwnerTable) { if(FPointWeightMap* Mask = InItem->GetMask()) { return SNew(SMaskListRow, OwnerTable, InItem, SharedThis(this)) .OnInvalidateList(this, &SClothAssetSelector::OnRefresh); } return SNew(STableRow>, OwnerTable) [ SNew(STextBlock).Text(LOCTEXT("MaskList_NoMasks", "No masks available")) ]; } void SClothAssetSelector::OnMaskSelectionChanged(TSharedPtr InSelectedItem, ESelectInfo::Type InSelectInfo) { if(InSelectedItem.IsValid() && InSelectedItem->ClothingAsset.IsValid() && InSelectedItem->LodIndex != INDEX_NONE && InSelectedItem->MaskIndex != INDEX_NONE && InSelectedItem->MaskIndex != SelectedMask && InSelectInfo != ESelectInfo::Direct) { SetSelectedMask(InSelectedItem->MaskIndex); } } FReply SClothAssetSelector::AddNewMask() { if(UClothingAssetCommon* Asset = SelectedAsset.Get()) { if(Asset->LodData.IsValidIndex(SelectedLod)) { FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; const int32 NumRequiredValues = LodData.PhysicalMeshData.Vertices.Num(); LodData.PointWeightMaps.AddDefaulted(); FPointWeightMap& NewMask = LodData.PointWeightMaps.Last(); NewMask.Name = TEXT("New Mask"); NewMask.CurrentTarget = (uint8)EWeightMapTargetCommon::None; NewMask.Values.AddZeroed(NumRequiredValues); OnRefresh(); } } return FReply::Handled(); } bool SClothAssetSelector::CanAddNewMask() const { return SelectedAsset.Get() != nullptr; } void SClothAssetSelector::OnRefresh() { RefreshAssetList(); RefreshMaskList(); } void SClothAssetSelector::RefreshAssetList() { UClothingAssetCommon* CurrSelectedAsset = nullptr; int32 SelectedItem = INDEX_NONE; if(AssetList.IsValid()) { TArray> SelectedItems; AssetList->GetSelectedItems(SelectedItems); if(SelectedItems.Num() > 0) { CurrSelectedAsset = SelectedItems[0]->ClothingAsset.Get(); } } AssetListItems.Empty(); for(UClothingAssetBase* Asset : Mesh->GetMeshClothingAssets()) { UClothingAssetCommon* ConcreteAsset = Cast(Asset); TSharedPtr Entry = MakeShareable(new FClothingAssetListItem); Entry->ClothingAsset = ConcreteAsset; AssetListItems.Add(Entry); if(ConcreteAsset == CurrSelectedAsset) { SelectedItem = AssetListItems.Num() - 1; } } if(AssetListItems.Num() == 0) { // Add an invalid entry so we can show a "none" line AssetListItems.Add(MakeShareable(new FClothingAssetListItem)); } if(AssetList.IsValid()) { AssetList->RequestListRefresh(); if(SelectedItem != INDEX_NONE) { AssetList->SetSelection(AssetListItems[SelectedItem]); } } } void SClothAssetSelector::RefreshMaskList() { int32 CurrSelectedLod = INDEX_NONE; int32 CurrSelectedMask = INDEX_NONE; int32 SelectedItem = INDEX_NONE; if(MaskList.IsValid()) { TArray> SelectedItems; MaskList->GetSelectedItems(SelectedItems); if(SelectedItems.Num() > 0) { CurrSelectedLod = SelectedItems[0]->LodIndex; CurrSelectedMask = SelectedItems[0]->MaskIndex; } } MaskListItems.Empty(); UClothingAssetCommon* Asset = SelectedAsset.Get(); if(Asset && Asset->IsValidLod(SelectedLod)) { const FClothLODDataCommon& LodData = Asset->LodData[SelectedLod]; const int32 NumMasks = LodData.PointWeightMaps.Num(); for(int32 Index = 0; Index < NumMasks; ++Index) { TSharedPtr NewItem = MakeShareable(new FClothingMaskListItem); NewItem->ClothingAsset = SelectedAsset; NewItem->LodIndex = SelectedLod; NewItem->MaskIndex = Index; MaskListItems.Add(NewItem); if(NewItem->LodIndex == CurrSelectedLod && NewItem->MaskIndex == CurrSelectedMask) { SelectedItem = MaskListItems.Num() - 1; } } } if(MaskListItems.Num() == 0) { // Add invalid entry so we can make a widget for "none" TSharedPtr NewItem = MakeShareable(new FClothingMaskListItem); MaskListItems.Add(NewItem); } if(MaskList.IsValid()) { MaskList->RequestListRefresh(); if(SelectedItem != INDEX_NONE) { MaskList->SetSelection(MaskListItems[SelectedItem]); } } } void SClothAssetSelector::OnClothingLodSelected(int32 InNewLod) { if(InNewLod == INDEX_NONE) { SetSelectedLod(InNewLod); //ClothPainterSettings->OnAssetSelectionChanged.Broadcast(SelectedAsset.Get(), SelectedLod, SelectedMask); } if(SelectedAsset.IsValid()) { SetSelectedLod(InNewLod); int32 NewMaskSelection = INDEX_NONE; if(SelectedAsset->LodData.IsValidIndex(SelectedLod)) { const FClothLODDataCommon& LodData = SelectedAsset->LodData[SelectedLod]; if(LodData.PointWeightMaps.Num() > 0) { NewMaskSelection = 0; } } SetSelectedMask(NewMaskSelection); } } void SClothAssetSelector::SetSelectedAsset(TWeakObjectPtr InSelectedAsset) { SelectedAsset = InSelectedAsset; RefreshMaskList(); if(UClothingAssetCommon* NewAsset = SelectedAsset.Get()) { if(NewAsset->GetNumLods() > 0) { SetSelectedLod(0); const FClothLODDataCommon& LodData = NewAsset->LodData[SelectedLod]; if(LodData.PointWeightMaps.Num() > 0) { SetSelectedMask(0); } else { SetSelectedMask(INDEX_NONE); } } else { SetSelectedLod(INDEX_NONE); SetSelectedMask(INDEX_NONE); } OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask); } } void SClothAssetSelector::SetSelectedLod(int32 InLodIndex, bool bRefreshMasks /*= true*/) { if(InLodIndex != SelectedLod) { SelectedLod = InLodIndex; if(MaskList.IsValid() && bRefreshMasks) { // New LOD means new set of masks, refresh that list RefreshMaskList(); } OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask); } } void SClothAssetSelector::SetSelectedMask(int32 InMaskIndex) { SelectedMask = InMaskIndex; if(MaskList.IsValid()) { TSharedPtr* FoundItem = nullptr; if(InMaskIndex != INDEX_NONE) { // Find the item so we can select it in the list FoundItem = MaskListItems.FindByPredicate([&](const TSharedPtr& InItem) { return InItem->MaskIndex == InMaskIndex; }); } if(FoundItem) { MaskList->SetSelection(*FoundItem); } else { MaskList->ClearSelection(); } } OnSelectionChanged.ExecuteIfBound(SelectedAsset, SelectedLod, SelectedMask); } #undef LOCTEXT_NAMESPACE