// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundMappingFunctionDetailsCustomization.h" #include "DetailWidgetRow.h" #include "IDetailChildrenBuilder.h" #include "InstancedStructDetails.h" #include "MetasoundEditorGraphNode.h" #include "MetasoundBuilderBase.h" #include "MetasoundMappingFunctionNode.h" #include "PropertyEditorModule.h" #include "Widgets/Input/SSlider.h" #include "Widgets/Layout/SBox.h" #include "SCurveEditor.h" #define LOCTEXT_NAMESPACE "MetasoundExperimentalEditor" FMappingFunctionNodeConfigurationCustomization::FMappingFunctionNodeConfigurationCustomization(TSharedPtr InStructProperty, TWeakObjectPtr InNode) : Metasound::Editor::FMetaSoundNodeConfigurationDataDetails(InStructProperty, InNode) { if (InStructProperty && InStructProperty->IsValidHandle()) { StructPropertyPath = InStructProperty->GeneratePathToProperty(); } } TArray FMappingFunctionNodeConfigurationCustomization::GetCurves() { TArray Curves; if (RuntimeCurve) { // Provide the FRichCurve for editing Curves.Add(FRichCurveEditInfo(RuntimeCurve->GetRichCurve())); } return Curves; } TArray FMappingFunctionNodeConfigurationCustomization::GetCurves() const { TArray Curves; if (RuntimeCurve) { // Provide the FRichCurve for editing Curves.Add(FRichCurveEditInfoConst(RuntimeCurve->GetRichCurve())); } return Curves; } void FMappingFunctionNodeConfigurationCustomization::GetCurves(TAdderReserverRef Curves) const { if (RuntimeCurve) { Curves.Add(FRichCurveEditInfoConst(RuntimeCurve->GetRichCurveConst())); } } bool FMappingFunctionNodeConfigurationCustomization::IsValidCurve(FRichCurveEditInfo CurveInfo) { // Validate that the curve being edited is the one we expect return RuntimeCurve && (CurveInfo.CurveToEdit == RuntimeCurve->GetRichCurve()); } void FMappingFunctionNodeConfigurationCustomization::MakeTransactional() { // Mark owners as transactional to support undo/redo for (UObject* Owner : OwnerObjects) { if (Owner) { Owner->SetFlags(RF_Transactional); } } } void FMappingFunctionNodeConfigurationCustomization::ModifyOwner() { // Called at the start of a curve edit (begin transaction) for (UObject* Owner : OwnerObjects) { if (Owner) { Owner->Modify(); } } } void FMappingFunctionNodeConfigurationCustomization::ModifyOwnerChange() { // Called during interactive changes (e.g., while dragging a key) for (UObject* Owner : OwnerObjects) { if (Owner) { Owner->Modify(); } } } void FMappingFunctionNodeConfigurationCustomization::OnCurveChanged(const TArray& ChangedCurveEditInfos) { // Enforce clamping and endpoint constraints whenever the curve is edited FRichCurve* RichCurve = RuntimeCurve ? RuntimeCurve->GetRichCurve() : nullptr; if (!RichCurve) { return; } bool bCurveModified = false; // Clamp all keys within [0,1] for both X (Time) and Y (Value) for (FRichCurveKey& Key : RichCurve->Keys) { if (Key.Time < 0.0f) { Key.Time = 0.0f; bCurveModified = true; } if (Key.Time > 1.0f) { Key.Time = 1.0f; bCurveModified = true; } if (Key.Value < 0.0f) { Key.Value = 0.0f; bCurveModified = true; } if (Key.Value > 1.0f) { Key.Value = 1.0f; bCurveModified = true; } } // Ensure a key exists at X = 0.0 (lock first key's X position to 0) if (RichCurve->Keys.Num() == 0 || RichCurve->Keys[0].Time > 0.0f) { float NewValue = (RichCurve->Keys.Num() > 0) ? RichCurve->Eval(0.0f) : 0.0f; RichCurve->AddKey(0.0f, NewValue); RichCurve->Keys[0].InterpMode = RCIM_Linear; bCurveModified = true; } // Ensure a key exists at X = 1.0 (lock last key's X position to 1) if (RichCurve->Keys.Num() == 0 || RichCurve->Keys.Last().Time < 1.0f) { float NewValue = (RichCurve->Keys.Num() > 0) ? RichCurve->Eval(1.0f) : 1.0f; RichCurve->AddKey(1.0f, NewValue); RichCurve->Keys.Last().InterpMode = RCIM_Linear; bCurveModified = true; } // Snap the first and last keys exactly to 0.0 and 1.0 on X (in case they were moved) if (RichCurve->Keys.Num() >= 2) { FRichCurveKey& FirstKey = RichCurve->Keys[0]; FRichCurveKey& LastKey = RichCurve->Keys.Last(); if (!FMath::IsNearlyZero(FirstKey.Time)) { FirstKey.Time = 0.0f; bCurveModified = true; } if (!FMath::IsNearlyEqual(LastKey.Time, 1.0f)) { LastKey.Time = 1.0f; bCurveModified = true; } } if (bCurveModified) { // Notify the engine that the property value has changed, to support undo/redo and UI refresh FProperty* CurveProperty = CurvePropertyHandle.IsValid() ? CurvePropertyHandle->GetProperty() : nullptr; if (CurveProperty) { for (UObject* Owner : OwnerObjects) { if (Owner) { Owner->PreEditChange(CurveProperty); } } FPropertyChangedEvent ChangeEvent(CurveProperty, EPropertyChangeType::ValueSet); for (UObject* Owner : OwnerObjects) { if (Owner) { Owner->PostEditChangeProperty(ChangeEvent); } } } } UpdateMappingFunctionData(); return; } TArray FMappingFunctionNodeConfigurationCustomization::GetOwners() const { TArray Result; for (UObject* Owner : OwnerObjects) { if (Owner) { Result.Add(Owner); } } return Result; } void FMappingFunctionNodeConfigurationCustomization::OnChildRowAdded(IDetailPropertyRow& ChildRow) { TSharedPtr ChildHandle = ChildRow.GetPropertyHandle(); if (!ChildHandle || !ChildHandle->IsValidHandle()) { return; } const FString PropertyPath = ChildHandle->GeneratePathToProperty(); if (PropertyPath == StructPropertyPath + TEXT(".Struct.") + GET_MEMBER_NAME_CHECKED(FMetaSoundMappingFunctionNodeConfiguration, MappingFunction).ToString()) { CurvePropertyHandle = ChildHandle; RuntimeCurve = nullptr; OwnerObjects.Empty(); // Access the raw struct data and cast to a curve TArray RawData; CurvePropertyHandle->AccessRawData(RawData); if (RawData.Num() > 0) { RuntimeCurve = static_cast(RawData[0]); } if (!RuntimeCurve) { return; } // Get outer owning UObject(s) CurvePropertyHandle->GetOuterObjects(OwnerObjects); // Ensure default keys at 0 and 1 with linear mapping if the curve is empty FRichCurve* RichCurve = RuntimeCurve->GetRichCurve(); if (RichCurve->GetNumKeys() == 0) { RichCurve->AddKey(0.0f, 0.0f); RichCurve->AddKey(1.0f, 1.0f); // Set interpolation to linear for a straight line between (0,0) and (1,1) RichCurve->Keys[0].InterpMode = RCIM_Linear; RichCurve->Keys.Last().InterpMode = RCIM_Linear; } CurveEditorWidget = SNew(SCurveEditor) .ViewMinInput(0.0f) .ViewMaxInput(1.0f) .ViewMinOutput(0.0f) .ViewMaxOutput(1.0f) .ZoomToFitHorizontal(false) .ZoomToFitVertical(false) .ShowInputGridNumbers(false) .ShowOutputGridNumbers(false) .AllowZoomOutput(false) .TimelineLength(1.0f) .HideUI(true) .ShowZoomButtons(false) .DesiredSize(FVector2D(300,200)) .ShowCurveSelector(false); CurveEditorWidget->SetCurveOwner(this); TSharedPtr NameWidget; TSharedPtr ValueWidget; FDetailWidgetRow Row; ChildRow.GetDefaultWidgets(NameWidget, ValueWidget, Row); ChildRow.CustomWidget(true) .NameContent() [ NameWidget.ToSharedRef() ] .ValueContent() .MinDesiredWidth(200) .MaxDesiredWidth(400) [ CurveEditorWidget.ToSharedRef() ]; } else if (PropertyPath == StructPropertyPath + TEXT(".Struct.") + GET_MEMBER_NAME_CHECKED(FMetaSoundMappingFunctionNodeConfiguration, bWrapInputs).ToString()) { bWrapInputsPropertyHandle = ChildHandle; } // Add custom onvalue changed TDelegate OnValueChangedDelegate = TDelegate::CreateSP(this, &FMappingFunctionNodeConfigurationCustomization::OnChildPropertyChanged); ChildHandle->SetOnPropertyValueChangedWithData(OnValueChangedDelegate); // Add base class on value changed Metasound::Editor::FMetaSoundNodeConfigurationDataDetails::OnChildRowAdded(ChildRow); // Make sure we update the mapping function data since we added some default keys UpdateMappingFunctionData(); } void FMappingFunctionNodeConfigurationCustomization::UpdateMappingFunctionData() { if (GraphNode.IsValid()) { FMetaSoundFrontendDocumentBuilder& DocBuilder = GraphNode->GetBuilderChecked().GetBuilder(); const FGuid& NodeID = GraphNode->GetNodeID(); TInstancedStruct Config = DocBuilder.FindNodeConfiguration(NodeID); TSharedPtr OperatorData = Config.Get().GetOperatorData(); const TSharedPtr MappingFunctionOperatorData = StaticCastSharedPtr(OperatorData); TSharedPtr MutableMappingFunctionOperatorData = ConstCastSharedPtr(MappingFunctionOperatorData); if (CurvePropertyHandle.IsValid() && CurvePropertyHandle->IsValidHandle()) { TArray RawData; CurvePropertyHandle->AccessRawData(RawData); if (RawData.Num() > 0) { FRuntimeFloatCurve* Curve = static_cast(RawData[0]); MutableMappingFunctionOperatorData->MappingFunction = *Curve; } } else if (bWrapInputsPropertyHandle.IsValid() && bWrapInputsPropertyHandle->IsValidHandle()) { bWrapInputsPropertyHandle->GetValue(MutableMappingFunctionOperatorData->bWrapInputs); } } } void FMappingFunctionNodeConfigurationCustomization::OnChildPropertyChanged(const FPropertyChangedEvent& InPropertyChangedEvent) { UpdateMappingFunctionData(); } #undef LOCTEXT_NAMESPACE