Files
UnrealEngine/Engine/Plugins/Experimental/MetasoundExperimental/Source/MetasoundExperimentalEditor/Private/MetasoundMappingFunctionDetailsCustomization.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

329 lines
9.7 KiB
C++

// 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<IPropertyHandle> InStructProperty, TWeakObjectPtr<UMetasoundEditorGraphNode> InNode)
: Metasound::Editor::FMetaSoundNodeConfigurationDataDetails(InStructProperty, InNode)
{
if (InStructProperty && InStructProperty->IsValidHandle())
{
StructPropertyPath = InStructProperty->GeneratePathToProperty();
}
}
TArray<FRichCurveEditInfo> FMappingFunctionNodeConfigurationCustomization::GetCurves()
{
TArray<FRichCurveEditInfo> Curves;
if (RuntimeCurve)
{
// Provide the FRichCurve for editing
Curves.Add(FRichCurveEditInfo(RuntimeCurve->GetRichCurve()));
}
return Curves;
}
TArray<FRichCurveEditInfoConst> FMappingFunctionNodeConfigurationCustomization::GetCurves() const
{
TArray<FRichCurveEditInfoConst> Curves;
if (RuntimeCurve)
{
// Provide the FRichCurve for editing
Curves.Add(FRichCurveEditInfoConst(RuntimeCurve->GetRichCurve()));
}
return Curves;
}
void FMappingFunctionNodeConfigurationCustomization::GetCurves(TAdderReserverRef<FRichCurveEditInfoConst> 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<FRichCurveEditInfo>& 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<const UObject*> FMappingFunctionNodeConfigurationCustomization::GetOwners() const
{
TArray<const UObject*> Result;
for (UObject* Owner : OwnerObjects)
{
if (Owner)
{
Result.Add(Owner);
}
}
return Result;
}
void FMappingFunctionNodeConfigurationCustomization::OnChildRowAdded(IDetailPropertyRow& ChildRow)
{
TSharedPtr<IPropertyHandle> 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<void*> RawData;
CurvePropertyHandle->AccessRawData(RawData);
if (RawData.Num() > 0)
{
RuntimeCurve = static_cast<FRuntimeFloatCurve*>(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<SWidget> NameWidget;
TSharedPtr<SWidget> 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<void(const FPropertyChangedEvent&)> OnValueChangedDelegate = TDelegate<void(const FPropertyChangedEvent&)>::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<FMetaSoundFrontendNodeConfiguration> Config = DocBuilder.FindNodeConfiguration(NodeID);
TSharedPtr<const Metasound::IOperatorData> OperatorData = Config.Get().GetOperatorData();
const TSharedPtr<const Metasound::Experimental::FMappingFunctionNodeOperatorData> MappingFunctionOperatorData = StaticCastSharedPtr<const Metasound::Experimental::FMappingFunctionNodeOperatorData>(OperatorData);
TSharedPtr<Metasound::Experimental::FMappingFunctionNodeOperatorData> MutableMappingFunctionOperatorData = ConstCastSharedPtr<Metasound::Experimental::FMappingFunctionNodeOperatorData>(MappingFunctionOperatorData);
if (CurvePropertyHandle.IsValid() &&
CurvePropertyHandle->IsValidHandle())
{
TArray<void*> RawData;
CurvePropertyHandle->AccessRawData(RawData);
if (RawData.Num() > 0)
{
FRuntimeFloatCurve* Curve = static_cast<FRuntimeFloatCurve*>(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