2972 lines
106 KiB
C++
2972 lines
106 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SAnimationBlendSpaceGridWidget.h"
|
|
|
|
#include "Animation/AnimSequence.h"
|
|
#include "Animation/BlendSpace.h"
|
|
#include "Animation/BlendSpace1D.h"
|
|
|
|
#include "Widgets/Input/SButton.h"
|
|
#include "Widgets/SBoxPanel.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
#include "Rendering/DrawElements.h"
|
|
#include "Layout/WidgetPath.h"
|
|
#include "Framework/Application/MenuStack.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
#include "Widgets/Input/SNumericEntryBox.h"
|
|
#include "Widgets/SToolTip.h"
|
|
|
|
#include "IDetailsView.h"
|
|
#include "UObject/StructOnScope.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "PropertyEditorModule.h"
|
|
#include "IStructureDetailsView.h"
|
|
|
|
#include "Customization/BlendSampleDetails.h"
|
|
#include "AssetRegistry/AssetData.h"
|
|
#include "DragAndDrop/AssetDragDropOp.h"
|
|
#include "Settings/EditorStyleSettings.h"
|
|
|
|
#include "Widgets/Input/SButton.h"
|
|
#include "Fonts/FontMeasure.h"
|
|
#include "Modules/ModuleManager.h"
|
|
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Styling/StyleColors.h"
|
|
|
|
#include "JsonObjectConverter.h"
|
|
#include "HAL/PlatformApplicationMisc.h"
|
|
#include "AnimGraphNode_BlendSpaceGraphBase.h"
|
|
#include "BlendSpaceGraph.h"
|
|
#include "AnimationBlendSpaceSampleGraph.h"
|
|
#include "EdGraphUtilities.h"
|
|
#include "AnimGraphNode_BlendSpaceSampleResult.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "Framework/Commands/GenericCommands.h"
|
|
#include "Framework/Commands/UICommandList.h"
|
|
|
|
//UE_DISABLE_OPTIMIZATION
|
|
|
|
#define LOCTEXT_NAMESPACE "SAnimationBlendSpaceGridWidget"
|
|
|
|
// Draws additional data on the triangulation to help debugging
|
|
//#define DEBUG_BLENDSPACE_TRIANGULATION
|
|
|
|
// Threshold for it being considered a problem that when a lookup is made at the same location as a
|
|
// sample, the returned weight is less than this.
|
|
static const float SampleLookupWeightThreshold = 0.2f;
|
|
|
|
// Flag any triangle that has an interior angle smaller than this (degrees), calculated from the
|
|
// normalized positions
|
|
static const float CriticalTriangulationAngle = 1.0f;
|
|
|
|
// Flag any triangle that has a smaller area than this (normalized units)
|
|
static const float CriticalTriangulationArea = 5e-4f;
|
|
|
|
// Identifies the clipboard contents as being a blend sample
|
|
static const FString BlendSampleClipboardHeaderAsset = TEXT("COPY_BLENDSAMPLE_ASSET");
|
|
static const FString BlendSampleClipboardHeaderGraph = TEXT("COPY_BLENDSAMPLE_GRAPH");
|
|
|
|
//======================================================================================================================
|
|
// Paint a filled triangle
|
|
static void PaintTriangle(
|
|
const FVector2D& P0,
|
|
const FVector2D& P1,
|
|
const FVector2D& P2,
|
|
const FGeometry& AllottedGeometry,
|
|
FLinearColor Color,
|
|
const FSlateBrush* Brush,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32 DrawLayerId)
|
|
{
|
|
const FVector2D* Points[3] = { &P0, &P1, &P2 };
|
|
|
|
TArray<FSlateVertex> Vertices;
|
|
Vertices.Reserve(3);
|
|
|
|
for (int32 PointIndex = 0; PointIndex != 3; ++PointIndex)
|
|
{
|
|
Vertices.AddZeroed();
|
|
FSlateVertex& NewVert = Vertices.Last();
|
|
NewVert.Position = FVector2f(AllottedGeometry.LocalToAbsolute(*Points[PointIndex])); // LWC_TODO: Precision loss
|
|
NewVert.Color = Color.ToFColor(false);
|
|
}
|
|
|
|
// Fill by making triangles
|
|
TArray<SlateIndex> VertexIndices = { 0, 1, 2 };
|
|
FSlateDrawElement::MakeCustomVerts(
|
|
OutDrawElements, DrawLayerId, Brush->GetRenderingResource(), Vertices, VertexIndices, nullptr, 0, 0);
|
|
}
|
|
|
|
//======================================================================================================================
|
|
// Paints a filled polygon with outline, defined by a set of points which don't need to be sorted.
|
|
// This will handle concave polygons, but only if the centroid lies inside the polygon.
|
|
static void PaintPolygon(
|
|
TArray<FVector2D>& Points,
|
|
const FGeometry& AllottedGeometry,
|
|
FLinearColor FillColor,
|
|
FLinearColor OutlineColor,
|
|
const FSlateBrush* Brush,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32 DrawLayerId)
|
|
{
|
|
TArray<FSlateVertex> Vertices;
|
|
Vertices.Reserve(Points.Num() + 1);
|
|
|
|
// Add a mid-position vertex so that we handle polygons that aren't completely convex
|
|
Vertices.AddZeroed();
|
|
|
|
FSlateVertex& MidVertex = Vertices.Last();
|
|
for (int32 PointIndex = 0; PointIndex != Points.Num(); ++PointIndex)
|
|
{
|
|
Vertices.AddZeroed();
|
|
FSlateVertex& NewVert = Vertices.Last();
|
|
NewVert.Position = FVector2f(AllottedGeometry.LocalToAbsolute(Points[PointIndex])); // LWC_TODO: Precision loss
|
|
NewVert.Color = FillColor.ToFColor(false);
|
|
MidVertex.Position += NewVert.Position;
|
|
}
|
|
MidVertex.Position /= static_cast<float>(Points.Num());
|
|
MidVertex.Color = FillColor.ToFColor(false);
|
|
|
|
// Make sure the points all wind correctly relative to the mid point
|
|
struct FComparePoints
|
|
{
|
|
FComparePoints(const FSlateVertex& Mid) : MidPoint(Mid) {}
|
|
bool operator()(const FSlateVertex& A, const FSlateVertex& B) const
|
|
{
|
|
const FVector2D DeltaA = FVector2D(A.Position) - FVector2D(MidPoint.Position);
|
|
const FVector2D DeltaB = FVector2D(B.Position) - FVector2D(MidPoint.Position);
|
|
const double AngleA = FMath::Atan2(DeltaA.Y, DeltaA.X);
|
|
const double AngleB = FMath::Atan2(DeltaB.Y, DeltaB.X);
|
|
return AngleA < AngleB;
|
|
}
|
|
FSlateVertex MidPoint;
|
|
};
|
|
Algo::Sort(MakeArrayView(Vertices.GetData() + 1, Vertices.Num() - 1), FComparePoints(MidVertex));
|
|
|
|
if (FillColor.A > 0)
|
|
{
|
|
// Fill by making triangles
|
|
TArray<SlateIndex> VertexIndices;
|
|
for (int VertexIndex = 1; VertexIndex < Vertices.Num(); ++VertexIndex)
|
|
{
|
|
VertexIndices.Add(0);
|
|
VertexIndices.Add(VertexIndex);
|
|
VertexIndices.Add(VertexIndex + 1 >= Vertices.Num() ? 1 : VertexIndex + 1);
|
|
}
|
|
|
|
FSlateDrawElement::MakeCustomVerts(
|
|
OutDrawElements, DrawLayerId, Brush->GetRenderingResource(), Vertices, VertexIndices, nullptr, 0, 0);
|
|
}
|
|
|
|
if (OutlineColor.A > 0)
|
|
{
|
|
TArray<FVector2D> LinePoints;
|
|
LinePoints.Reserve(Points.Num() + 1);
|
|
for (int VertexIndex = 1; VertexIndex <= Vertices.Num(); ++VertexIndex)
|
|
{
|
|
LinePoints.Add(FVector2D(VertexIndex < Vertices.Num() ? Vertices[VertexIndex].Position : Vertices[1].Position));
|
|
}
|
|
FSlateDrawElement::MakeLines(
|
|
OutDrawElements, DrawLayerId + 1, FPaintGeometry(), LinePoints, ESlateDrawEffect::None, OutlineColor, true, 1.0f);
|
|
}
|
|
}
|
|
|
|
//======================================================================================================================
|
|
static void PaintCircle(
|
|
const FVector2D& Centre,
|
|
const float Radius,
|
|
const int32 NumVerts,
|
|
const FGeometry& AllottedGeometry,
|
|
FLinearColor FillColor,
|
|
FLinearColor OutlineColor,
|
|
const FSlateBrush* Brush,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32 DrawLayerId)
|
|
{
|
|
// NumVerts needs to be a multiple of 4
|
|
int32 NumVertsPerSector = ((NumVerts + 3) / 4);
|
|
TArray<FVector2D> Points;
|
|
Points.SetNum(NumVertsPerSector * 4);
|
|
for (int32 Index = 0 ; Index != NumVertsPerSector ; ++Index)
|
|
{
|
|
float Angle = Index * (HALF_PI / NumVertsPerSector);
|
|
float S, C;
|
|
FMath::SinCos(&S, &C, Angle);
|
|
Points[Index + NumVertsPerSector * 0] = Centre + FVector2D(C * Radius, S * Radius);
|
|
Points[Index + NumVertsPerSector * 1] = Centre + FVector2D(-S * Radius, C * Radius);
|
|
Points[Index + NumVertsPerSector * 2] = Centre + FVector2D(-C * Radius, -S * Radius);
|
|
Points[Index + NumVertsPerSector * 3] = Centre + FVector2D(S * Radius, -C * Radius);
|
|
}
|
|
PaintPolygon(Points, AllottedGeometry, FillColor, OutlineColor, Brush, OutDrawElements, DrawLayerId);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::Construct(const FArguments& InArgs)
|
|
{
|
|
BlendSpaceBase = InArgs._BlendSpaceBase;
|
|
PreviousBlendSpaceBase = BlendSpaceBase.Get();
|
|
TargetPosition = InArgs._Position;
|
|
FilteredPosition = InArgs._FilteredPosition;
|
|
NotifyHook = InArgs._NotifyHook;
|
|
OnSampleAdded = InArgs._OnSampleAdded;
|
|
OnSampleDuplicated = InArgs._OnSampleDuplicated;
|
|
OnSampleMoved = InArgs._OnSampleMoved;
|
|
OnSampleRemoved = InArgs._OnSampleRemoved;
|
|
OnSampleReplaced = InArgs._OnSampleReplaced;
|
|
OnNavigateUp = InArgs._OnNavigateUp;
|
|
OnNavigateDown = InArgs._OnNavigateDown;
|
|
OnCanvasDoubleClicked = InArgs._OnCanvasDoubleClicked;
|
|
OnSampleDoubleClicked = InArgs._OnSampleDoubleClicked;
|
|
OnGetBlendSpaceSampleName = InArgs._OnGetBlendSpaceSampleName;
|
|
OnExtendSampleTooltip = InArgs._OnExtendSampleTooltip;
|
|
bReadOnly = InArgs._ReadOnly;
|
|
bShowAxisLabels = InArgs._ShowAxisLabels;
|
|
bShowSettingsButtons = InArgs._ShowSettingsButtons;
|
|
StatusBarName = InArgs._StatusBarName;
|
|
|
|
GridType = (BlendSpaceBase.Get() != nullptr && BlendSpaceBase.Get()->IsA<UBlendSpace1D>()) ? EGridType::SingleAxis : EGridType::TwoAxis;
|
|
BlendParametersToDraw = (GridType == EGridType::SingleAxis) ? 1 : 2;
|
|
|
|
HighlightedSampleIndex = SelectedSampleIndex = DraggedSampleIndex = ToolTipSampleIndex = INDEX_NONE;
|
|
DragState = EDragState::None;
|
|
// Initialize flags
|
|
bHighlightPreviewPin = false;
|
|
// Initialize preview value to center or the grid
|
|
PreviewPosition.X = BlendSpaceBase.Get() != nullptr ? (BlendSpaceBase.Get()->GetBlendParameter(0).GetRange() * .5f) + BlendSpaceBase.Get()->GetBlendParameter(0).Min : 0.0f;
|
|
PreviewPosition.Y = BlendSpaceBase.Get() != nullptr ? (GridType == EGridType::TwoAxis ? (BlendSpaceBase.Get()->GetBlendParameter(1).GetRange() * .5f) + BlendSpaceBase.Get()->GetBlendParameter(1).Min : 0.0f) : 0.0f;
|
|
PreviewPosition.Z = 0.0f;
|
|
|
|
PreviewFilteredPosition = PreviewPosition;
|
|
|
|
bShowTriangulation = true;
|
|
bMouseIsOverGeometry = false;
|
|
bRefreshCachedData = true;
|
|
bStretchToFit = true;
|
|
bShowAnimationNames = false;
|
|
|
|
// Register and bind all our menu commands
|
|
FGenericCommands::Register();
|
|
BindCommands();
|
|
|
|
InvalidSamplePositionDragDropText = FText::FromString(TEXT("Invalid Sample Position"));
|
|
|
|
// Retrieve UI color values
|
|
KeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Regular");
|
|
HighlightKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Highlight");
|
|
SelectKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Pressed");
|
|
PreDragKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Pressed");
|
|
DragKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Drag");
|
|
InvalidColor = FAppStyle::GetSlateColor("BlendSpaceKey.Invalid");
|
|
DropKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Drop");
|
|
PreviewKeyColor = FAppStyle::GetSlateColor("BlendSpaceKey.Preview");
|
|
GridLinesColor = GetDefault<UEditorStyleSettings>()->RegularColor;
|
|
GridOutlineColor = GetDefault<UEditorStyleSettings>()->RuleColor;
|
|
TriangulationColor = FSlateColor(EStyleColor::Foreground);
|
|
TriangulationCurrentColor = FSlateColor(EStyleColor::Highlight);
|
|
|
|
// Retrieve background and sample key brushes
|
|
BackgroundImage = FAppStyle::GetBrush(TEXT("Graph.Panel.SolidBackground"));
|
|
KeyBrush = FAppStyle::GetBrush("CurveEd.CurveKey");
|
|
PreviewBrush = FAppStyle::GetBrush("BlendSpaceEditor.PreviewIcon");
|
|
ArrowBrushes[(uint8)EArrowDirection::Up] = FAppStyle::GetBrush("BlendSpaceEditor.ArrowUp");
|
|
ArrowBrushes[(uint8)EArrowDirection::Down] = FAppStyle::GetBrush("BlendSpaceEditor.ArrowDown");
|
|
ArrowBrushes[(uint8)EArrowDirection::Right] = FAppStyle::GetBrush("BlendSpaceEditor.ArrowRight");
|
|
ArrowBrushes[(uint8)EArrowDirection::Left] = FAppStyle::GetBrush("BlendSpaceEditor.ArrowLeft");
|
|
LabelBrush = FAppStyle::GetBrush(TEXT("BlendSpaceEditor.LabelBackground"));
|
|
|
|
// Retrieve font data
|
|
FontInfo = FAppStyle::GetFontStyle("CurveEd.InfoFont");
|
|
|
|
// Initialize UI layout values
|
|
KeySize = FVector2D(11.0f, 11.0f);
|
|
PreviewSize = FVector2D(21.0f, 21.0f);
|
|
DragThreshold = 9.0f;
|
|
ClickAndHighlightThreshold = 12.0f;
|
|
TextMargin = 8.0f;
|
|
GridMargin = bShowAxisLabels ? FMargin(MaxVerticalAxisTextWidth + (TextMargin * 2.0f), TextMargin, (HorizontalAxisMaxTextWidth *.5f) + TextMargin, MaxHorizontalAxisTextHeight + (TextMargin * 2.0f)) :
|
|
FMargin(TextMargin, TextMargin, TextMargin, TextMargin);
|
|
|
|
const bool bShowInputBoxLabel = true;
|
|
// Widget construction
|
|
this->ChildSlot
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
[
|
|
SNew(SVerticalBox)
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.VAlign(VAlign_Top)
|
|
.HAlign(HAlign_Left)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.DesiredSizeScale(FVector2D(1.0f, 1.0f))
|
|
.Padding_Lambda([&]() { return FMargin(GridMargin.Left + 6.f, 0, 0, 0) + GridRatioMargin; })
|
|
[
|
|
SNew(SVerticalBox)
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot() // Button to show triangulation
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetTriangulationButtonVisibility)))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SButton)
|
|
.ToolTipText(LOCTEXT("ShowTriangulation", "Show Triangulation"))
|
|
.OnClicked(this, &SBlendSpaceGridWidget::ToggleTriangulationVisibility)
|
|
.ButtonColorAndOpacity_Lambda([this]() -> FLinearColor { return bShowTriangulation ? FAppStyle::GetSlateColor("SelectionColor").GetSpecifiedColor() : FLinearColor::White; })
|
|
.ContentPadding(1.f)
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::GetBrush("BlendSpaceEditor.ToggleTriangulation"))
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SHorizontalBox::Slot() // Button to toggle labels
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetAnimationNamesButtonVisibility)))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SButton)
|
|
.ToolTipText(LOCTEXT("ShowAnimationNames", "Show Sample Names"))
|
|
.OnClicked(this, &SBlendSpaceGridWidget::ToggleShowAnimationNames)
|
|
.ButtonColorAndOpacity_Lambda([this]() -> FLinearColor { return bShowAnimationNames ? FAppStyle::GetSlateColor("SelectionColor").GetSpecifiedColor() : FLinearColor::White; })
|
|
.ContentPadding(1.f)
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::GetBrush("BlendSpaceEditor.ToggleLabels"))
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SHorizontalBox::Slot() // Button to fit or stretch the graph
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetFittingButtonVisibility)))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SButton)
|
|
.ToolTipText(this, &SBlendSpaceGridWidget::GetFittingTypeButtonToolTipText)
|
|
.OnClicked(this, &SBlendSpaceGridWidget::ToggleFittingType)
|
|
.ContentPadding(1.f)
|
|
.ButtonColorAndOpacity_Lambda([this]() -> FLinearColor { return bStretchToFit ? FAppStyle::GetSlateColor("SelectionColor").GetSpecifiedColor() : FLinearColor::White; })
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::GetBrush("BlendSpaceEditor.ZoomToFit"))
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SHorizontalBox::Slot() // Sample X value input
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetInputBoxVisibility, 0)))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
CreateGridEntryBox(0, bShowInputBoxLabel).ToSharedRef()
|
|
]
|
|
]
|
|
|
|
+ SHorizontalBox::Slot() // Sample Y value input
|
|
.AutoWidth()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("NoBorder"))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetInputBoxVisibility, 1)))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
CreateGridEntryBox(1, bShowInputBoxLabel).ToSharedRef()
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SVerticalBox::Slot() // Tip for dragging in, when there are no samples
|
|
.AutoHeight()
|
|
.Padding(FMargin(2.0f, 3.0f, 0.0f, 0.0f ))
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("BlendSpaceSamplesToolTip", "Drag and Drop Animations from the Asset Browser to place Sample Points"))
|
|
.Font(FAppStyle::GetFontStyle(TEXT("AnimViewport.MessageFont")))
|
|
.ColorAndOpacity(FLinearColor(1.0f, 1.0f, 1.0f, 0.7f))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetSampleToolTipVisibility)))
|
|
]
|
|
|
|
+ SVerticalBox::Slot() // Tip for adjusting the preview point
|
|
.AutoHeight()
|
|
.Padding(FMargin(2.0f, 3.0f, 0.0f, 0.0f))
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("BlendspacePreviewToolTip", "Hold Control to set the Preview Point (Green)" ))
|
|
.Font(FAppStyle::GetFontStyle(TEXT("AnimViewport.MessageFont")))
|
|
.ColorAndOpacity(FLinearColor(1.0f, 1.0f, 1.0f, 0.7f))
|
|
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &SBlendSpaceGridWidget::GetPreviewToolTipVisibility)))
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
SAssignNew(ToolTip, SToolTip)
|
|
.BorderImage(FCoreStyle::Get().GetBrush("ToolTip.Background"))
|
|
[
|
|
SNew(SVerticalBox)
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SBlendSpaceGridWidget::GetToolTipAnimationName)
|
|
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SBlendSpaceGridWidget::GetToolTipSampleValue)
|
|
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SBlendSpaceGridWidget::GetToolTipSampleValidity)
|
|
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
|
|
.Visibility_Lambda([this]() { return GetToolTipSampleValidity().IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible; })
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SAssignNew(ToolTipExtensionContainer, SBox)
|
|
]
|
|
];
|
|
|
|
if(TargetPosition.IsSet())
|
|
{
|
|
StartPreviewing();
|
|
}
|
|
}
|
|
|
|
SBlendSpaceGridWidget::~SBlendSpaceGridWidget()
|
|
{
|
|
EnableStatusBarMessage(false);
|
|
}
|
|
|
|
TSharedPtr<SWidget> SBlendSpaceGridWidget::CreateGridEntryBox(const int32 BoxIndex, const bool bShowLabel)
|
|
{
|
|
return SNew(SNumericEntryBox<float>)
|
|
.Font(FAppStyle::GetFontStyle("CurveEd.InfoFont"))
|
|
.Value(this, &SBlendSpaceGridWidget::GetInputBoxValue, BoxIndex)
|
|
.UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values"))
|
|
.OnValueCommitted(this, &SBlendSpaceGridWidget::OnInputBoxValueCommited, BoxIndex)
|
|
.OnValueChanged(this, &SBlendSpaceGridWidget::OnInputBoxValueChanged, BoxIndex, true)
|
|
.OnBeginSliderMovement(this, &SBlendSpaceGridWidget::OnInputSliderBegin, BoxIndex)
|
|
.OnEndSliderMovement(this, &SBlendSpaceGridWidget::OnInputSliderEnd, BoxIndex)
|
|
.LabelVAlign(VAlign_Center)
|
|
.AllowSpin(true)
|
|
.MinValue(this, &SBlendSpaceGridWidget::GetInputBoxMinValue, BoxIndex)
|
|
.MaxValue(this, &SBlendSpaceGridWidget::GetInputBoxMaxValue, BoxIndex)
|
|
.MinSliderValue(this, &SBlendSpaceGridWidget::GetInputBoxMinValue, BoxIndex)
|
|
.MaxSliderValue(this, &SBlendSpaceGridWidget::GetInputBoxMaxValue, BoxIndex)
|
|
.MinDesiredValueWidth(60.0f)
|
|
.Label()
|
|
[
|
|
SNew(STextBlock)
|
|
.Visibility(bShowLabel ? EVisibility::Visible : EVisibility::Collapsed)
|
|
.Text_Lambda([this, BoxIndex]() { return (BoxIndex == 0) ? ParameterXName : ParameterYName; })
|
|
];
|
|
}
|
|
|
|
int32 SBlendSpaceGridWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
|
|
{
|
|
SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled && IsEnabled());
|
|
|
|
PaintBackgroundAndGrid(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
|
|
#if 0
|
|
// Showing the sample-weights on the grid points is not useful to end users, but can be helpful when debugging
|
|
// the grid-based interpolation.
|
|
PaintGridSampleWeights(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
#endif
|
|
|
|
PaintTriangulation(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
PaintSampleKeys(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
|
|
if(bShowAxisLabels)
|
|
{
|
|
PaintAxisText(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
}
|
|
|
|
if (bShowAnimationNames)
|
|
{
|
|
PaintAnimationNames(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId);
|
|
}
|
|
|
|
return LayerId;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::PaintBackgroundAndGrid(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32& DrawLayerId) const
|
|
{
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
// Create the grid
|
|
const FVector2D GridSize = CachedGridRectangle.GetSize();
|
|
const FVector2D GridOffset = CachedGridRectangle.GetTopLeft();
|
|
|
|
// Fill the background of the grid
|
|
FSlateDrawElement::MakeBox( OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(GridSize, FSlateLayoutTransform(GridOffset)), BackgroundImage );
|
|
|
|
TArray<FVector2D> LinePoints;
|
|
|
|
// Draw grid lines
|
|
LinePoints.SetNumZeroed(2);
|
|
const FVector2D StartVectors[2] = { FVector2D(1.0f, 0.0f), FVector2D(0.0f, 1.0f) };
|
|
const FVector2D OffsetVectors[2] = { FVector2D(0.0f, GridSize.Y), FVector2D(GridSize.X, 0.0f) };
|
|
for (uint32 ParameterIndex = 0; ParameterIndex < BlendParametersToDraw; ++ParameterIndex)
|
|
{
|
|
const FBlendParameter& BlendParameter = BlendSpace->GetBlendParameter(ParameterIndex);
|
|
const float Steps = static_cast<float>(GridSize[ParameterIndex] / ( BlendParameter.GridNum));
|
|
|
|
for (int32 Index = 1; Index < BlendParameter.GridNum; ++Index)
|
|
{
|
|
// Calculate line points
|
|
LinePoints[0] = ((Index * Steps) * StartVectors[ParameterIndex]) + GridOffset;
|
|
LinePoints[1] = LinePoints[0] + OffsetVectors[ParameterIndex];
|
|
|
|
FSlateDrawElement::MakeLines( OutDrawElements, DrawLayerId + 2, AllottedGeometry.ToPaintGeometry(), LinePoints, ESlateDrawEffect::None, GridLinesColor, true);
|
|
}
|
|
}
|
|
|
|
// Draw outer grid lines separately (this will avoid missing lines with 1D blend spaces)
|
|
LinePoints.SetNumZeroed(5);
|
|
|
|
// Top line
|
|
LinePoints[0] = GridOffset;
|
|
|
|
LinePoints[1] = GridOffset;
|
|
LinePoints[1].X += GridSize.X;
|
|
|
|
LinePoints[2] = GridOffset;
|
|
LinePoints[2].X += GridSize.X;
|
|
LinePoints[2].Y += GridSize.Y;
|
|
|
|
LinePoints[3] = GridOffset;
|
|
LinePoints[3].Y += GridSize.Y;
|
|
|
|
LinePoints[4] = GridOffset;
|
|
|
|
FSlateDrawElement::MakeLines( OutDrawElements, DrawLayerId + 3, AllottedGeometry.ToPaintGeometry(), LinePoints, ESlateDrawEffect::None, GridOutlineColor, true, 2.0f);
|
|
|
|
}
|
|
|
|
DrawLayerId += 3;
|
|
}
|
|
|
|
//======================================================================================================================
|
|
float SBlendSpaceGridWidget::GetSampleLookupWeight(int32 SampleIndex) const
|
|
{
|
|
const UBlendSpace* BlendSpace = BlendSpaceBase.Get();
|
|
if (BlendSpace && BlendSpace->bInterpolateUsingGrid)
|
|
{
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
|
|
TArray<FBlendSampleData> SampleDataList;
|
|
int32 TempTriangulationIndex = -1;
|
|
BlendSpace->GetSamplesFromBlendInput(Sample.SampleValue, SampleDataList, TempTriangulationIndex, true);
|
|
float LookedUpSampleWeight = 0.0f;
|
|
for (const FBlendSampleData& LookedUpSample : SampleDataList)
|
|
{
|
|
if (LookedUpSample.SampleDataIndex == SampleIndex)
|
|
{
|
|
LookedUpSampleWeight += LookedUpSample.GetClampedWeight();
|
|
}
|
|
}
|
|
return FMath::Clamp(LookedUpSampleWeight, 0.0f, 1.0f);
|
|
}
|
|
return 1.0f; // Return 1 to avoid anything treating this as a problem
|
|
}
|
|
|
|
|
|
//======================================================================================================================
|
|
void SBlendSpaceGridWidget::PaintSampleKeys(
|
|
const FGeometry& AllottedGeometry,
|
|
const FSlateRect& MyCullingRect,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32& DrawLayerId) const
|
|
{
|
|
const int32 FilteredPositionLayer = DrawLayerId + 1;
|
|
const int32 PreviewPositionLayer = DrawLayerId + 2;
|
|
const int32 SampleLayer = DrawLayerId + 3;
|
|
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
// Draw keys
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
for (int32 SampleIndex = 0; SampleIndex < Samples.Num(); ++SampleIndex)
|
|
{
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
|
|
FLinearColor DrawColor = KeyColor.GetSpecifiedColor();
|
|
if (DraggedSampleIndex == SampleIndex)
|
|
{
|
|
DrawColor = (DragState == EDragState::PreDrag) ? PreDragKeyColor.GetSpecifiedColor() : DragKeyColor.GetSpecifiedColor();
|
|
}
|
|
else if (SelectedSampleIndex == SampleIndex)
|
|
{
|
|
DrawColor = SelectKeyColor.GetSpecifiedColor();
|
|
}
|
|
else if (HighlightedSampleIndex == SampleIndex)
|
|
{
|
|
DrawColor = HighlightKeyColor.GetSpecifiedColor();
|
|
}
|
|
else if(!Sample.bIsValid)
|
|
{
|
|
DrawColor = InvalidColor.GetSpecifiedColor();
|
|
}
|
|
|
|
const FVector2D GridPosition = SampleValueToScreenPosition(Sample.SampleValue) - (KeySize * 0.5f);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, SampleLayer, AllottedGeometry.ToPaintGeometry(KeySize, FSlateLayoutTransform(GridPosition)),
|
|
KeyBrush, ESlateDrawEffect::None, DrawColor );
|
|
|
|
const float SampleLookupWeight = GetSampleLookupWeight(SampleIndex);
|
|
if (SampleLookupWeight <= SampleLookupWeightThreshold)
|
|
{
|
|
const FVector2D CirclePosition = SampleValueToScreenPosition(Sample.SampleValue);
|
|
FLinearColor IsolatedColor = FLinearColor::Red;
|
|
IsolatedColor.A = SampleLookupWeightThreshold > 0.0f ?
|
|
(SampleLookupWeightThreshold - SampleLookupWeight) / SampleLookupWeightThreshold :
|
|
1.0f;
|
|
IsolatedColor.A *= 0.4f;
|
|
PaintCircle(CirclePosition, 8.0f, 12, AllottedGeometry, IsolatedColor, IsolatedColor,
|
|
LabelBrush, OutDrawElements, SampleLayer - 1);
|
|
}
|
|
}
|
|
|
|
// Always draw the filtered position which comes back from whatever is running
|
|
{
|
|
const FVector2D GridPosition = SampleValueToScreenPosition(PreviewFilteredPosition) - (PreviewSize * .5f);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, FilteredPositionLayer, AllottedGeometry.ToPaintGeometry(PreviewSize, FSlateLayoutTransform(GridPosition)),
|
|
PreviewBrush, ESlateDrawEffect::None, PreviewKeyColor.GetSpecifiedColor() * 0.7f);
|
|
}
|
|
|
|
// Always draw the preview position
|
|
{
|
|
const FVector2D GridPosition = SampleValueToScreenPosition(PreviewPosition) - (PreviewSize * .5f);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, PreviewPositionLayer, AllottedGeometry.ToPaintGeometry(PreviewSize, FSlateLayoutTransform(GridPosition)),
|
|
PreviewBrush, ESlateDrawEffect::None, PreviewKeyColor.GetSpecifiedColor());
|
|
}
|
|
|
|
if (DragState == EDragState::DragDrop || DragState == EDragState::InvalidDragDrop)
|
|
{
|
|
const FVector2D GridPoint = SnapScreenPositionToGrid(
|
|
LocalMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown()) - (KeySize * .5f);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, SampleLayer, AllottedGeometry.ToPaintGeometry(KeySize, FSlateLayoutTransform(GridPoint)), KeyBrush, ESlateDrawEffect::None,
|
|
(DragState == EDragState::DragDrop) ? DropKeyColor.GetSpecifiedColor() : InvalidColor.GetSpecifiedColor() );
|
|
}
|
|
|
|
// Also show the weights that are getting picked up as bars, using two overlaid boxes
|
|
if (bSamplePreviewing && FSlateApplication::Get().GetModifierKeys().IsAltDown())
|
|
{
|
|
for (const FBlendSampleData& PreviewedSample : PreviewedSamples)
|
|
{
|
|
float Weight = PreviewedSample.TotalWeight;
|
|
int32 SampleIndex = PreviewedSample.SampleDataIndex;
|
|
FVector2D Point = SampleValueToScreenPosition(Samples[SampleIndex].SampleValue);
|
|
|
|
float MaxWeightWidth = 48;
|
|
float WeightHeight = 6;
|
|
float Border = 1.0f;
|
|
|
|
Point.Y -= KeySize.Y / 2 + WeightHeight * 1.25;
|
|
Point.X -= MaxWeightWidth * 0.5f;
|
|
|
|
FGeometry FillGeometry = AllottedGeometry.MakeChild(
|
|
FVector2D(MaxWeightWidth, WeightHeight),
|
|
FSlateLayoutTransform(FVector2D(Point.X, Point.Y))
|
|
);
|
|
FGeometry BorderGeometry = AllottedGeometry.MakeChild(
|
|
FVector2D(Weight * (MaxWeightWidth - 2 * Border), WeightHeight - 2 * Border),
|
|
FSlateLayoutTransform(FVector2D(Point.X + Border, Point.Y + Border))
|
|
);
|
|
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, FillGeometry.ToPaintGeometry(),
|
|
LabelBrush, ESlateDrawEffect::None, FLinearColor::Black);
|
|
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 2, BorderGeometry.ToPaintGeometry(),
|
|
LabelBrush, ESlateDrawEffect::None, FLinearColor::Gray);
|
|
}
|
|
}
|
|
}
|
|
|
|
DrawLayerId += 3;
|
|
}
|
|
|
|
//======================================================================================================================
|
|
void SBlendSpaceGridWidget::PaintAxisText(
|
|
const FGeometry& AllottedGeometry,
|
|
const FSlateRect& MyCullingRect,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32& DrawLayerId) const
|
|
{
|
|
const TSharedRef< FSlateFontMeasure > FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
|
const FVector2D GridCenter = CachedGridRectangle.GetCenter();
|
|
|
|
// X axis
|
|
FString Text = ParameterXName.ToString();
|
|
FVector2D TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// arrow left
|
|
|
|
FVector2D ArrowSize = ArrowBrushes[(uint8)EArrowDirection::Left]->GetImageSize();
|
|
FVector2D TextPosition = FVector2D(GridCenter.X - (TextSize.X * .5f), CachedGridRectangle.Bottom + TextMargin + (ArrowSize.Y * .25f));
|
|
FVector2D ArrowPosition = FVector2D(TextPosition.X - ArrowSize.X - 10.f/* give padding*/, TextPosition.Y);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(ArrowSize, FSlateLayoutTransform(ArrowPosition)),
|
|
ArrowBrushes[(uint8)EArrowDirection::Left], ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
// Label
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(FVector2D(1.0f, 1.0f), FSlateLayoutTransform(TextPosition)).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
// arrow right
|
|
ArrowSize = ArrowBrushes[(uint8)EArrowDirection::Right]->GetImageSize();
|
|
ArrowPosition = FVector2D(TextPosition.X + TextSize.X + 10.f/* give padding*/, TextPosition.Y);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(ArrowSize, FSlateLayoutTransform(ArrowPosition)),
|
|
ArrowBrushes[(uint8)EArrowDirection::Right], ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
Text = FString::SanitizeFloat(SampleValueMin.X);
|
|
TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// Minimum value
|
|
FVector2D MinTextPosition(CachedGridRectangle.Left - (TextSize.X * .5f), CachedGridRectangle.Bottom + TextMargin + (TextSize.Y * .25f));
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(FVector2D(1.0f, 1.0f), FSlateLayoutTransform(MinTextPosition)).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
Text = FString::SanitizeFloat(SampleValueMax.X);
|
|
TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// Maximum value
|
|
FVector2D MaxTextPosition(CachedGridRectangle.Right - (TextSize.X * .5f), CachedGridRectangle.Bottom + TextMargin + (TextSize.Y * .25f));
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(FVector2D(1.0f, 1.0f), FSlateLayoutTransform(MaxTextPosition)).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
// Only draw Y axis labels if this is a 2D grid
|
|
if (GridType == EGridType::TwoAxis)
|
|
{
|
|
// Y axis
|
|
Text = ParameterYName.ToString();
|
|
TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// arrow up
|
|
ArrowSize = ArrowBrushes[(uint8)EArrowDirection::Up]->GetImageSize();
|
|
TextPosition = FVector2D(((GridMargin.Left - TextSize.X) * 0.5f - (ArrowSize.X * .25f)) + GridRatioMargin.Left, GridCenter.Y - (TextSize.Y * .5f));
|
|
ArrowPosition = FVector2D(TextPosition.X + TextSize.X * 0.5f - ArrowSize.X * 0.5f, TextPosition.Y - ArrowSize.Y - 10.f/* give padding*/);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(ArrowSize, FSlateLayoutTransform(ArrowPosition)),
|
|
ArrowBrushes[(uint8)EArrowDirection::Up], ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
// Label
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(FVector2D(1.0f, 1.0f), FSlateLayoutTransform(TextPosition)).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
// arrow down
|
|
ArrowSize = ArrowBrushes[(uint8)EArrowDirection::Down]->GetImageSize();
|
|
ArrowPosition = FVector2D(TextPosition.X + TextSize.X * 0.5f - ArrowSize.X * 0.5f, TextPosition.Y + TextSize.Y + 10.f/* give padding*/);
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(ArrowSize, FSlateLayoutTransform(ArrowPosition)),
|
|
ArrowBrushes[(uint8)EArrowDirection::Down], ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
Text = FString::SanitizeFloat(SampleValueMin.Y);
|
|
TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// Minimum value
|
|
FVector2D MinValuePosition(((GridMargin.Left - TextSize.X) * 0.5f - (TextSize.X * .25f)) + GridRatioMargin.Left, CachedGridRectangle.Bottom - (TextSize.Y * .5f));
|
|
FSlateDrawElement::MakeText(OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(
|
|
FVector2D(1.0f, 1.0f),
|
|
FSlateLayoutTransform(MinValuePosition)
|
|
).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
|
|
Text = FString::SanitizeFloat(SampleValueMax.Y);
|
|
TextSize = FontMeasure->Measure(Text, FontInfo);
|
|
|
|
// Maximum value
|
|
FSlateDrawElement::MakeText(OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(
|
|
FVector2D(1.0f, 1.0f), FSlateLayoutTransform( FVector2D( ((GridMargin.Left - TextSize.X) * 0.5f - (TextSize.X * .25f) ) + GridRatioMargin.Left, CachedGridRectangle.Top - (TextSize.Y * .5f)) )).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
}
|
|
|
|
DrawLayerId += 1;
|
|
}
|
|
|
|
//======================================================================================================================
|
|
void SBlendSpaceGridWidget::PaintGridSampleWeights(
|
|
const FGeometry& AllottedGeometry,
|
|
const FSlateRect& MyCullingRect,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32& DrawLayerId) const
|
|
{
|
|
const UBlendSpace* BlendSpace = BlendSpaceBase.Get();
|
|
if (!BlendSpace)
|
|
{
|
|
return;
|
|
}
|
|
if (!BlendSpace->bInterpolateUsingGrid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const TSharedRef< FSlateFontMeasure > FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
|
|
|
const TArray<FEditorElement>& GridSamples = BlendSpace->GetGridSamples();
|
|
int32 NumGridSamples = GridSamples.Num();
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
for (int32 GridSampleIndex = 0 ; GridSampleIndex != NumGridSamples ; ++GridSampleIndex)
|
|
{
|
|
const FEditorElement& EditorElement = GridSamples[GridSampleIndex];
|
|
|
|
int32 TextOffset = 0;
|
|
for (int32 ElementIndex = 0 ; ElementIndex != 3 ; ++ElementIndex)
|
|
{
|
|
float SampleWeight = EditorElement.Weights[ElementIndex];
|
|
int32 SampleIndex = EditorElement.Indices[ElementIndex];
|
|
if (SampleWeight <= 0 || SampleIndex < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
|
|
const FText Name = FText::Format(LOCTEXT("SampleNameFormatWeight", "{0} ({1}) {2}"),
|
|
GetSampleName(Sample, SampleIndex), FText::AsNumber(SampleIndex), SampleWeight);
|
|
const FVector2D TextSize = FontMeasure->Measure(Name, FontInfo);
|
|
const FVector2D Padding = FVector2D(12.0f, 4.0f);
|
|
|
|
FVector GridSamplePosition = BlendSpace->GetGridPosition(GridSampleIndex);
|
|
|
|
// Show the sample name/index/weight, going progressively up (because sample labels are
|
|
// below the grid points)
|
|
FVector2D GridPosition = SampleValueToScreenPosition(GridSamplePosition);
|
|
GridPosition += FVector2D(-TextSize.X / 2, -2 * KeySize.Y);
|
|
GridPosition.Y -= TextSize.Y * TextOffset++;
|
|
GridPosition.X -= Padding.X / 2;
|
|
GridPosition.Y += Padding.Y / 2;
|
|
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 2, AllottedGeometry.MakeChild(FVector2f(1.0f, 1.0f),
|
|
FSlateLayoutTransform(GridPosition + Padding / 2)).ToPaintGeometry(), Name, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//======================================================================================================================
|
|
void SBlendSpaceGridWidget::PaintTriangulation(
|
|
const FGeometry& AllottedGeometry,
|
|
const FSlateRect& MyCullingRect,
|
|
FSlateWindowElementList& OutDrawElements,
|
|
int32& DrawLayerId) const
|
|
{
|
|
const UBlendSpace* BlendSpace = BlendSpaceBase.Get();
|
|
if (!BlendSpace)
|
|
{
|
|
return;
|
|
}
|
|
if ((bReadOnly && BlendSpace->bInterpolateUsingGrid) || (!bReadOnly && !bShowTriangulation))
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FVector2D> PolygonPoints;
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
if (!BlendSpace->bInterpolateUsingGrid)
|
|
{
|
|
// Use runtime triangulation
|
|
const FBlendSpaceData& BlendSpaceData = BlendSpace->GetBlendSpaceData();
|
|
for (const FBlendSpaceSegment& Segment : BlendSpaceData.Segments)
|
|
{
|
|
int32 SampleIndex = Segment.SampleIndices[0];
|
|
int32 SampleIndex1 = Segment.SampleIndices[1];
|
|
TArray<FVector2D> Points;
|
|
Points.Add(SampleValueToScreenPosition(Samples[SampleIndex].SampleValue));
|
|
Points.Add(SampleValueToScreenPosition(Samples[SampleIndex1].SampleValue));
|
|
FSlateDrawElement::MakeLines(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.ToPaintGeometry(), Points,
|
|
ESlateDrawEffect::None, TriangulationColor.GetSpecifiedColor(), true, 0.5f);
|
|
}
|
|
|
|
const float CriticalDot = FMath::Cos(FMath::DegreesToRadians(CriticalTriangulationAngle));
|
|
for (const FBlendSpaceTriangle& Triangle : BlendSpaceData.Triangles)
|
|
{
|
|
FLinearColor TriangleFillColor = TriangulationCurrentColor.GetSpecifiedColor();
|
|
FLinearColor TriangleLineColor = TriangulationColor.GetSpecifiedColor();
|
|
TriangleFillColor.A = 0.03f; // Alpha for tinting the triangulation background
|
|
int32 TriangleLayer = DrawLayerId + 1;
|
|
|
|
FVector2D ScreenPositions[3] = {
|
|
SampleValueToScreenPosition(Samples[Triangle.SampleIndices[0]].SampleValue),
|
|
SampleValueToScreenPosition(Samples[Triangle.SampleIndices[1]].SampleValue),
|
|
SampleValueToScreenPosition(Samples[Triangle.SampleIndices[2]].SampleValue),
|
|
};
|
|
|
|
// Show invalid triangles even if there's only one triangle, because that probably
|
|
// happened when somebody failed to place the sample points in a proper line.
|
|
{
|
|
FVector2D NormalizedPositions[3] = {
|
|
SampleValueToNormalizedPosition(Samples[Triangle.SampleIndices[0]].SampleValue),
|
|
SampleValueToNormalizedPosition(Samples[Triangle.SampleIndices[1]].SampleValue),
|
|
SampleValueToNormalizedPosition(Samples[Triangle.SampleIndices[2]].SampleValue),
|
|
};
|
|
|
|
for (int32 Index = 0; Index != 3; ++Index)
|
|
{
|
|
FVector2D A = NormalizedPositions[(Index + 2) % 3] - NormalizedPositions[(Index + 1) % 3];
|
|
FVector2D B = NormalizedPositions[Index] - NormalizedPositions[(Index + 1) % 3];
|
|
double Dot = A.GetSafeNormal() | B.GetSafeNormal();
|
|
double Area = 0.5f * FMath::Abs(B ^ A);
|
|
if (Dot > CriticalDot || Area < CriticalTriangulationArea)
|
|
{
|
|
TriangleFillColor = FLinearColor::Red;
|
|
TriangleFillColor.A = 0.5f;
|
|
TriangleLineColor = FLinearColor::Red;
|
|
TriangleLayer = DrawLayerId + 10; // just bump it up so it definitely shows
|
|
}
|
|
}
|
|
}
|
|
|
|
FVector2D MidPoint(0, 0);
|
|
PolygonPoints.Empty(3);
|
|
for (int32 Index0 = 0; Index0 != FBlendSpaceTriangle::NUM_VERTICES; ++Index0)
|
|
{
|
|
int32 Index1 = (Index0 + 1) % FBlendSpaceTriangle::NUM_VERTICES;
|
|
int32 Index2 = (Index0 + 2) % FBlendSpaceTriangle::NUM_VERTICES;
|
|
int32 SampleIndex0 = Triangle.SampleIndices[Index0];
|
|
int32 SampleIndex1 = Triangle.SampleIndices[Index1];
|
|
int32 SampleIndex2 = Triangle.SampleIndices[Index2];
|
|
|
|
TArray<FVector2D> Points = { ScreenPositions[Index0], ScreenPositions[Index1] };
|
|
MidPoint += SampleValueToScreenPosition(Samples[SampleIndex0].SampleValue) / 3.0f;
|
|
|
|
FSlateDrawElement::MakeLines(
|
|
OutDrawElements, TriangleLayer, AllottedGeometry.ToPaintGeometry(), Points,
|
|
ESlateDrawEffect::None, TriangleLineColor, true, 0.5f);
|
|
PolygonPoints.Push(ScreenPositions[Index0]);
|
|
}
|
|
|
|
PaintTriangle(PolygonPoints[0], PolygonPoints[1], PolygonPoints[2], AllottedGeometry,
|
|
TriangleFillColor, LabelBrush, OutDrawElements, DrawLayerId);
|
|
|
|
#ifdef DEBUG_BLENDSPACE_TRIANGULATION
|
|
// Draw the adjacent triangle indices around the perimeter
|
|
if (!bSamplePreviewing)
|
|
{
|
|
for (int32 Index0 = 0; Index0 != FBlendSpaceTriangle::NUM_VERTICES; ++Index0)
|
|
{
|
|
if (Triangle.EdgeInfo[Index0].NeighbourTriangleIndex < 0)
|
|
{
|
|
int32 Index1 = (Index0 + 1) % FBlendSpaceTriangle::NUM_VERTICES;
|
|
FVector2D MidEdge = ( ScreenPositions[Index0] + ScreenPositions[Index1]) * 0.5;
|
|
float PullInAmount = 0.2;
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(
|
|
FMath::Lerp(ScreenPositions[Index0], MidEdge, PullInAmount), FVector2D(1.0f, 1.0f)).ToPaintGeometry(),
|
|
FText::AsNumber(Triangle.EdgeInfo[Index0].AdjacentPerimeterTriangleIndices[0]),
|
|
FontInfo, ESlateDrawEffect::None, FLinearColor::Red);
|
|
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(
|
|
FMath::Lerp(ScreenPositions[Index1], MidEdge, PullInAmount), FVector2D(1.0f, 1.0f)).ToPaintGeometry(),
|
|
FText::AsNumber(Triangle.EdgeInfo[Index0].AdjacentPerimeterTriangleIndices[1]),
|
|
FontInfo, ESlateDrawEffect::None, FLinearColor::Red);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef DEBUG_BLENDSPACE_TRIANGULATION
|
|
// Draw the triangle indices for debugging
|
|
FText Text = FText::AsNumber(&Triangle - &BlendSpaceData.Triangles[0]);
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(
|
|
FVector2D(MidPoint.X, MidPoint.Y), FVector2D(1.0f, 1.0f)).ToPaintGeometry(),
|
|
Text, FontInfo, ESlateDrawEffect::None, FLinearColor::Gray);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Draw the current triangle (or polygon)
|
|
if (bSamplePreviewing && FSlateApplication::Get().GetModifierKeys().IsAltDown())
|
|
{
|
|
PolygonPoints.Empty(3);
|
|
for (const FBlendSampleData& PreviewedSample : PreviewedSamples)
|
|
{
|
|
float Weight = PreviewedSample.TotalWeight;
|
|
if (Weight)
|
|
{
|
|
int32 SampleIndex = PreviewedSample.SampleDataIndex;
|
|
FVector2D Point = SampleValueToScreenPosition(Samples[SampleIndex].SampleValue);
|
|
PolygonPoints.Push(Point);
|
|
}
|
|
}
|
|
if (PolygonPoints.Num())
|
|
{
|
|
FLinearColor FillColor = TriangulationCurrentColor.GetSpecifiedColor();
|
|
FillColor.A = 0.2f; // Alpha for the current triangulation triangle
|
|
FLinearColor OutlineColor = FillColor;
|
|
OutlineColor.A = 0.5f;
|
|
PaintPolygon(PolygonPoints, AllottedGeometry, FillColor, OutlineColor, LabelBrush, OutDrawElements, DrawLayerId);
|
|
}
|
|
}
|
|
|
|
DrawLayerId += 1;
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetSampleName(const FBlendSample& InBlendSample, int32 InSampleIndex) const
|
|
{
|
|
if(OnGetBlendSpaceSampleName.IsBound())
|
|
{
|
|
return FText::FromName(OnGetBlendSpaceSampleName.Execute(InSampleIndex));
|
|
}
|
|
else
|
|
{
|
|
if(InBlendSample.Animation != nullptr)
|
|
{
|
|
return FText::FromString(InBlendSample.Animation->GetName());
|
|
}
|
|
}
|
|
|
|
return LOCTEXT("NoAnimationSetTooltipText", "No Animation Set");
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::PaintAnimationNames(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32& DrawLayerId) const
|
|
{
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
const TSharedRef< FSlateFontMeasure > FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
for (int32 SampleIndex = 0; SampleIndex < Samples.Num(); ++SampleIndex)
|
|
{
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
|
|
const FText Name = FText::Format(LOCTEXT("SampleNameFormat", "{0} ({1})"), GetSampleName(Sample, SampleIndex), FText::AsNumber(SampleIndex));
|
|
const FVector2D TextSize = FontMeasure->Measure(Name, FontInfo);
|
|
const FVector2D Padding = FVector2D(12.0f, 4.0f);
|
|
|
|
FVector2D GridPosition = SampleValueToScreenPosition(Sample.SampleValue);
|
|
GridPosition += FVector2D(-TextSize.X / 2, KeySize.Y / 2);
|
|
GridPosition.X -= Padding.X / 2;
|
|
GridPosition.Y += Padding.Y / 2;
|
|
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements, DrawLayerId + 1, AllottedGeometry.MakeChild(TextSize + Padding, FSlateLayoutTransform(GridPosition)).ToPaintGeometry(),
|
|
LabelBrush, ESlateDrawEffect::None, FLinearColor::Black);
|
|
FSlateDrawElement::MakeText(
|
|
OutDrawElements, DrawLayerId + 2, AllottedGeometry.MakeChild(FVector2D(1.0f, 1.0f), FSlateLayoutTransform(GridPosition + Padding/2)).ToPaintGeometry(),
|
|
Name, FontInfo, ESlateDrawEffect::None, FLinearColor::White);
|
|
}
|
|
}
|
|
|
|
DrawLayerId += 2;
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
// Check if we are in dropping state and if so snap to the grid and try to add the sample
|
|
if (DragState == EDragState::DragDrop || DragState == EDragState::InvalidDragDrop || DragState == EDragState::DragDropOverride)
|
|
{
|
|
if (DragState == EDragState::DragDrop)
|
|
{
|
|
TSharedPtr<FAssetDragDropOp> DragDropOperation = DragDropEvent.GetOperationAs<FAssetDragDropOp>();
|
|
if (DragDropOperation.IsValid())
|
|
{
|
|
const FVector SampleValue = ScreenPositionToSampleValueWithSnapping(
|
|
LocalMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown());
|
|
|
|
const TArray<FAssetData>& Assets = DragDropOperation->GetAssets();
|
|
for (int Index = 0 ; Index != Assets.Num() ; ++Index)
|
|
{
|
|
const FAssetData& AssetData = Assets[Index];
|
|
UObject* Asset = AssetData.GetAsset();
|
|
UAnimSequence* Animation = (UAnimSequence*) Asset;
|
|
if (OnSampleAdded.IsBound())
|
|
{
|
|
OnSampleAdded.Execute(Animation, SampleValue, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (DragState == EDragState::DragDropOverride)
|
|
{
|
|
TSharedPtr<FAssetDragDropOp> DragDropOperation = DragDropEvent.GetOperationAs<FAssetDragDropOp>();
|
|
if (DragDropOperation.IsValid())
|
|
{
|
|
UAnimSequence* Animation = FAssetData::GetFirstAsset<UAnimSequence>(DragDropOperation->GetAssets());
|
|
int32 DroppedSampleIndex = GetClosestSamplePointIndexToMouse();
|
|
OnSampleReplaced.ExecuteIfBound(DroppedSampleIndex, Animation);
|
|
}
|
|
}
|
|
|
|
DragState = EDragState::None;
|
|
}
|
|
|
|
DragDropAnimationSequence = nullptr;
|
|
DragDropAnimationName = FText::GetEmpty();
|
|
HoveredAnimationName = FText::GetEmpty();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (DragDropEvent.GetOperationAs<FAssetDragDropOp>().IsValid())
|
|
{
|
|
DragState = IsValidDragDropOperation(DragDropEvent, InvalidDragDropText) ? EDragState::DragDrop : EDragState::InvalidDragDrop;
|
|
}
|
|
}
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (DragState == EDragState::DragDrop || DragState == EDragState::InvalidDragDrop || DragState == EDragState::DragDropOverride)
|
|
{
|
|
LocalMousePosition = MyGeometry.AbsoluteToLocal(DragDropEvent.GetScreenSpacePosition());
|
|
|
|
// Always update the tool tip, in case it became invalid
|
|
TSharedPtr<FAssetDragDropOp> DragDropOperation = DragDropEvent.GetOperationAs<FAssetDragDropOp>();
|
|
if (DragDropOperation.IsValid())
|
|
{
|
|
DragDropOperation->SetToolTip(GetToolTipSampleValue(), DragDropOperation->GetIcon());
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnDragLeave(const FDragDropEvent& DragDropEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (DragState == EDragState::DragDrop || DragState == EDragState::InvalidDragDrop || DragState == EDragState::DragDropOverride)
|
|
{
|
|
DragState = EDragState::None;
|
|
DragDropAnimationSequence = nullptr;
|
|
DragDropAnimationName = FText::GetEmpty();
|
|
HoveredAnimationName = FText::GetEmpty();
|
|
}
|
|
}
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (this->HasMouseCapture())
|
|
{
|
|
if (DragState == EDragState::None || DragState == EDragState::PreDrag)
|
|
{
|
|
ProcessClick(MyGeometry, MouseEvent);
|
|
}
|
|
else if (DragState == EDragState::DragSample)
|
|
{
|
|
// Process drag ending
|
|
ResetToolTip();
|
|
OnSampleMoved.ExecuteIfBound(DraggedSampleIndex, LastDragPosition, false);
|
|
}
|
|
|
|
// Reset drag state and index
|
|
DragState = EDragState::None;
|
|
DraggedSampleIndex = INDEX_NONE;
|
|
|
|
return FReply::Handled().ReleaseMouseCapture();
|
|
}
|
|
else
|
|
{
|
|
return ProcessClick(MyGeometry, MouseEvent);
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
|
|
{
|
|
// If we are over a sample, make it our currently (dragged) sample
|
|
if (HighlightedSampleIndex != INDEX_NONE)
|
|
{
|
|
DraggedSampleIndex = SelectedSampleIndex = HighlightedSampleIndex;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
ResetToolTip();
|
|
DragState = EDragState::PreDrag;
|
|
MouseDownPosition = LocalMousePosition;
|
|
|
|
// Start mouse capture
|
|
return FReply::Handled().CaptureMouse(SharedThis(this));
|
|
}
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
|
|
{
|
|
if (SelectedSampleIndex != INDEX_NONE)
|
|
{
|
|
OnSampleDoubleClicked.ExecuteIfBound(SelectedSampleIndex);
|
|
}
|
|
else
|
|
{
|
|
OnCanvasDoubleClicked.ExecuteIfBound();
|
|
}
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
EnableStatusBarMessage(true);
|
|
}
|
|
|
|
// Cache the mouse position in local and screen space
|
|
LocalMousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
|
|
LastMousePosition = MouseEvent.GetScreenSpacePosition();
|
|
|
|
if(!bReadOnly)
|
|
{
|
|
if (this->HasMouseCapture())
|
|
{
|
|
if (DragState == EDragState::None)
|
|
{
|
|
if (HighlightedSampleIndex != INDEX_NONE)
|
|
{
|
|
DragState = EDragState::DragSample;
|
|
DraggedSampleIndex = HighlightedSampleIndex;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
else if (DragState == EDragState::PreDrag)
|
|
{
|
|
// Actually start dragging
|
|
if ((LocalMousePosition - MouseDownPosition).SizeSquared() > DragThreshold)
|
|
{
|
|
DragState = EDragState::DragSample;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
ShowToolTip();
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
else if (IsHovered() && bMouseIsOverGeometry)
|
|
{
|
|
if (MouseEvent.IsControlDown())
|
|
{
|
|
StartPreviewing();
|
|
DragState = EDragState::Preview;
|
|
// Make tool tip visible (this will display the current preview sample value)
|
|
ShowToolTip();
|
|
|
|
// Set flag for showing advanced preview info in tooltip
|
|
bAdvancedPreview = MouseEvent.IsAltDown();
|
|
return FReply::Handled();
|
|
}
|
|
else if(TargetPosition.IsSet())
|
|
{
|
|
StartPreviewing();
|
|
DragState = EDragState::None;
|
|
ShowToolTip();
|
|
|
|
// Set flag for showing advanced preview info in tooltip
|
|
bAdvancedPreview = MouseEvent.IsAltDown();
|
|
return FReply::Handled();
|
|
}
|
|
else if (bSamplePreviewing)
|
|
{
|
|
StopPreviewing();
|
|
DragState = EDragState::None;
|
|
ResetToolTip();
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::ProcessClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
|
|
{
|
|
SelectedSampleIndex = INDEX_NONE;
|
|
|
|
if (HighlightedSampleIndex == INDEX_NONE)
|
|
{
|
|
// If there isn't any sample currently being highlighted, retrieve all of them and see if we are over one
|
|
SelectedSampleIndex = GetClosestSamplePointIndexToMouse();
|
|
}
|
|
else
|
|
{
|
|
// If we are over a sample, make it the selected sample index
|
|
SelectedSampleIndex = HighlightedSampleIndex;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
}
|
|
}
|
|
else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
|
|
{
|
|
auto PushMenu = [this, &MouseEvent](TSharedPtr<SWidget> InMenuContent)
|
|
{
|
|
if (InMenuContent.IsValid())
|
|
{
|
|
const FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();
|
|
const FVector2D MousePosition = MouseEvent.GetScreenSpacePosition();
|
|
// This is of a fixed size atm since MenuContent->GetDesiredSize() will not take the detail
|
|
// customization into account and return an incorrect (small) size
|
|
const FVector2D ExpectedSize(300, 300);
|
|
const FVector2D MenuPosition = FSlateApplication::Get().CalculatePopupWindowPosition(
|
|
FSlateRect(static_cast<float>(MousePosition.X), static_cast<float>(MousePosition.Y), static_cast<float>(MousePosition.X), static_cast<float>(MousePosition.Y)), ExpectedSize, false);
|
|
|
|
FSlateApplication::Get().PushMenu(
|
|
AsShared(),
|
|
WidgetPath,
|
|
InMenuContent.ToSharedRef(),
|
|
MenuPosition,
|
|
FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu)
|
|
);
|
|
}
|
|
};
|
|
|
|
// If we are over a sample open a context menu for editing its data
|
|
if (HighlightedSampleIndex != INDEX_NONE)
|
|
{
|
|
SelectedSampleIndex = HighlightedSampleIndex;
|
|
|
|
// Create context menu
|
|
TSharedPtr<SWidget> MenuContent = CreateBlendSampleContextMenu();
|
|
|
|
// Reset highlight sample index
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
|
|
PushMenu(MenuContent);
|
|
|
|
return FReply::Handled().SetUserFocus(MenuContent.ToSharedRef(), EFocusCause::SetDirectly).ReleaseMouseCapture();
|
|
}
|
|
else
|
|
{
|
|
TSharedPtr<SWidget> MenuContent = CreateNewBlendSampleContextMenu(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()));
|
|
|
|
PushMenu(MenuContent);
|
|
|
|
return FReply::Handled().SetUserFocus(MenuContent.ToSharedRef(), EFocusCause::SetDirectly).ReleaseMouseCapture();
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
if (UICommandList->ProcessCommandBindings(InKeyEvent))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
// Start previewing when either one of the shift keys is pressed
|
|
if (IsHovered() && bMouseIsOverGeometry)
|
|
{
|
|
if ((InKeyEvent.GetKey() == EKeys::LeftControl) || (InKeyEvent.GetKey() == EKeys::RightControl))
|
|
{
|
|
StartPreviewing();
|
|
DragState = EDragState::Preview;
|
|
// Make tool tip visible (this will display the current preview sample value)
|
|
ShowToolTip();
|
|
return FReply::Handled();
|
|
}
|
|
|
|
// Set flag for showing advanced preview info in tooltip
|
|
if ((InKeyEvent.GetKey() == EKeys::LeftAlt) || (InKeyEvent.GetKey() == EKeys::RightAlt))
|
|
{
|
|
bAdvancedPreview = true;
|
|
return FReply::Handled();
|
|
}
|
|
|
|
if (InKeyEvent.GetKey() == EKeys::PageUp)
|
|
{
|
|
OnNavigateUp.ExecuteIfBound();
|
|
return FReply::Handled();
|
|
}
|
|
else if (InKeyEvent.GetKey() == EKeys::PageDown)
|
|
{
|
|
OnNavigateDown.ExecuteIfBound();
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
|
{
|
|
if(!bReadOnly && BlendSpaceBase.IsSet())
|
|
{
|
|
// Stop previewing when shift keys are released
|
|
if ((InKeyEvent.GetKey() == EKeys::LeftControl) || (InKeyEvent.GetKey() == EKeys::RightControl))
|
|
{
|
|
StopPreviewing();
|
|
DragState = EDragState::None;
|
|
ResetToolTip();
|
|
return FReply::Handled();
|
|
}
|
|
|
|
if((InKeyEvent.GetKey() == EKeys::LeftAlt) || (InKeyEvent.GetKey() == EKeys::RightAlt))
|
|
{
|
|
bAdvancedPreview = false;
|
|
return FReply::Handled();
|
|
}
|
|
|
|
// Pressing esc will remove the current key selection
|
|
if( InKeyEvent.GetKey() == EKeys::Escape)
|
|
{
|
|
SelectedSampleIndex = INDEX_NONE;
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::MakeViewContextMenuEntries(FMenuBuilder& InMenuBuilder)
|
|
{
|
|
InMenuBuilder.BeginSection("ViewOptions", LOCTEXT("ViewOptionsMenuHeader", "View Options"));
|
|
{
|
|
if (GetTriangulationButtonVisibility() == EVisibility::Visible)
|
|
{
|
|
TAttribute<FText> ShowTriangulation = TAttribute<FText>::Create(
|
|
[this]()
|
|
{
|
|
return (BlendSpaceBase.Get() && BlendSpaceBase.Get()->bInterpolateUsingGrid)
|
|
? LOCTEXT("ShowGridToSampleConnections", "Show Grid/Sample Connections")
|
|
: LOCTEXT("ShowTriangulation", "Show Triangulation");
|
|
});
|
|
TAttribute<FText> ShowTriangulationToolTip = TAttribute<FText>::Create(
|
|
[this]()
|
|
{
|
|
return (BlendSpaceBase.Get() && BlendSpaceBase.Get()->bInterpolateUsingGrid)
|
|
? LOCTEXT("ShowGridToSampleConnectionsToolTip", "Show which samples each grid point is associated with")
|
|
: LOCTEXT("ShowTriangulationToolTip", "Show the Delaunay triangulation for all blend space samples");
|
|
});
|
|
|
|
InMenuBuilder.AddMenuEntry(
|
|
ShowTriangulation,
|
|
ShowTriangulationToolTip,
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlendSpaceEditor.ToggleTriangulation"),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([this](){ bShowTriangulation = !bShowTriangulation; }),
|
|
FCanExecuteAction(),
|
|
FGetActionCheckState::CreateLambda([this](){ return bShowTriangulation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
|
),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
}
|
|
|
|
InMenuBuilder.AddMenuEntry(
|
|
LOCTEXT("ShowAnimationNames", "Show Sample Names"),
|
|
LOCTEXT("ShowAnimationNamesToolTip", "Show the names of each of the samples"),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlendSpaceEditor.ToggleLabels"),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([this](){ bShowAnimationNames = !bShowAnimationNames; }),
|
|
FCanExecuteAction(),
|
|
FGetActionCheckState::CreateLambda([this](){ return bShowAnimationNames ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
|
),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
|
|
InMenuBuilder.AddMenuEntry(
|
|
LOCTEXT("StretchFittingText", "Stretch Grid to Fit"),
|
|
LOCTEXT("StretchFittingTextToolTip", "Whether to stretch the grid to fit or to fit the grid to the largest axis"),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlendSpaceEditor.ZoomToFit"),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([this](){ ToggleFittingType(); }),
|
|
FCanExecuteAction(),
|
|
FGetActionCheckState::CreateLambda([this](){ return bStretchToFit ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
|
),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
}
|
|
InMenuBuilder.EndSection();
|
|
}
|
|
|
|
TSharedPtr<SWidget> SBlendSpaceGridWidget::CreateBlendSampleContextMenu()
|
|
{
|
|
const bool bShouldCloseWindowAfterMenuSelection = true;
|
|
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, UICommandList);
|
|
|
|
TSharedPtr<IStructureDetailsView> StructureDetailsView;
|
|
// Initialize details view
|
|
FDetailsViewArgs DetailsViewArgs;
|
|
{
|
|
DetailsViewArgs.bAllowSearch = false;
|
|
DetailsViewArgs.bHideSelectionTip = true;
|
|
DetailsViewArgs.bLockable = false;
|
|
DetailsViewArgs.bSearchInitialKeyFocus = true;
|
|
DetailsViewArgs.bUpdatesFromSelection = false;
|
|
DetailsViewArgs.NotifyHook = NotifyHook;
|
|
DetailsViewArgs.bShowOptions = true;
|
|
DetailsViewArgs.bShowModifiedPropertiesOption = false;
|
|
}
|
|
|
|
FStructureDetailsViewArgs StructureViewArgs;
|
|
{
|
|
StructureViewArgs.bShowObjects = true;
|
|
StructureViewArgs.bShowAssets = true;
|
|
StructureViewArgs.bShowClasses = true;
|
|
StructureViewArgs.bShowInterfaces = true;
|
|
}
|
|
|
|
StructureDetailsView = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor")
|
|
.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, nullptr, LOCTEXT("SampleData", "Blend Sample"));
|
|
{
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
const FBlendSample& Sample = BlendSpace->GetBlendSample(HighlightedSampleIndex);
|
|
StructureDetailsView->GetDetailsView()->SetGenericLayoutDetailsDelegate(
|
|
FOnGetDetailCustomizationInstance::CreateStatic(
|
|
&FBlendSampleDetails::MakeInstance, BlendSpace, this, HighlightedSampleIndex));
|
|
|
|
FStructOnScope* Struct = new FStructOnScope(FBlendSample::StaticStruct(), (uint8*)&Sample);
|
|
Struct->SetPackage(BlendSpace->GetOutermost());
|
|
StructureDetailsView->SetStructureData(MakeShareable(Struct));
|
|
}
|
|
}
|
|
|
|
MenuBuilder.BeginSection("Sample", LOCTEXT("SampleMenuHeader", "Sample"));
|
|
{
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut);
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy);
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete);
|
|
MenuBuilder.AddWidget(StructureDetailsView->GetWidget().ToSharedRef(), FText::GetEmpty(), true);
|
|
}
|
|
MenuBuilder.EndSection();
|
|
|
|
MakeViewContextMenuEntries(MenuBuilder);
|
|
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
TSharedPtr<SWidget> SBlendSpaceGridWidget::CreateNewBlendSampleContextMenu(const FVector2D& InMousePosition)
|
|
{
|
|
const bool bShouldCloseWindowAfterMenuSelection = true;
|
|
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, UICommandList);
|
|
|
|
FVector NewSampleValue;
|
|
if(FSlateApplication::Get().GetModifierKeys().IsShiftDown())
|
|
{
|
|
NewSampleValue = ScreenPositionToSampleValue(SnapScreenPositionToGrid(InMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown()), false);
|
|
}
|
|
else
|
|
{
|
|
const FVector2D GridPosition(FMath::Clamp(InMousePosition.X, CachedGridRectangle.Left, CachedGridRectangle.Right),
|
|
FMath::Clamp(InMousePosition.Y, CachedGridRectangle.Top, CachedGridRectangle.Bottom));
|
|
NewSampleValue = ScreenPositionToSampleValue(GridPosition, false);
|
|
}
|
|
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
MenuBuilder.BeginSection("Sample", LOCTEXT("SampleMenuHeader", "Sample"));
|
|
{
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste);
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete);
|
|
|
|
if(!BlendSpace->IsAsset())
|
|
{
|
|
// Blend space graph - add a new graph sample
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT("AddNewSample", "Add New Sample"),
|
|
LOCTEXT("AddNewSampleTooltip", "Add a new sample to the blendspace at this location"),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus"),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda(
|
|
[this, NewSampleValue]()
|
|
{
|
|
if (OnSampleAdded.IsBound())
|
|
{
|
|
OnSampleAdded.Execute(nullptr, NewSampleValue, false);
|
|
}
|
|
})
|
|
)
|
|
);
|
|
}
|
|
}
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
MakeViewContextMenuEntries(MenuBuilder);
|
|
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::ToggleTriangulationVisibility()
|
|
{
|
|
bShowTriangulation = !bShowTriangulation;
|
|
return FReply::Handled();
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::CalculateGridPoints()
|
|
{
|
|
CachedGridPoints.Empty(SampleGridDivisions.X * SampleGridDivisions.Y);
|
|
CachedSamplePoints.Empty(SampleGridDivisions.X * SampleGridDivisions.Y);
|
|
if (SampleGridDivisions.X <= 0 || (GridType == EGridType::TwoAxis && SampleGridDivisions.Y <= 0))
|
|
{
|
|
return;
|
|
}
|
|
for (int32 GridY = 0; GridY < ((GridType == EGridType::TwoAxis) ? SampleGridDivisions.Y + 1 : 1); ++GridY)
|
|
{
|
|
for (int32 GridX = 0; GridX < SampleGridDivisions.X + 1; ++GridX)
|
|
{
|
|
// Calculate grid point in 0-1 form
|
|
FVector2D GridPoint(
|
|
GridX * (1.0f / SampleGridDivisions.X),
|
|
(GridType == EGridType::TwoAxis) ? GridY * (1.0f / SampleGridDivisions.Y) : 0.5f);
|
|
|
|
// Multiply with size and offset according to the grid layout
|
|
GridPoint *= CachedGridRectangle.GetSize();
|
|
GridPoint += CachedGridRectangle.GetTopLeft();
|
|
CachedGridPoints.Add(GridPoint);
|
|
|
|
CachedSamplePoints.Add(FVector(
|
|
SampleValueMin.X + (GridX * (SampleValueRange.X / SampleGridDivisions.X)),
|
|
(GridType == EGridType::TwoAxis) ? SampleValueMax.Y - (GridY * (SampleValueRange.Y / SampleGridDivisions.Y)) : 0.0f,
|
|
0.0f));
|
|
}
|
|
}
|
|
}
|
|
|
|
const FVector2D SBlendSpaceGridWidget::SnapScreenPositionToGrid(const FVector2D& InPosition, bool bForceSnap) const
|
|
{
|
|
const int32 GridPointIndex = FindClosestGridPointIndexFromScreenPosition(InPosition);
|
|
const FVector2D& GridPoint = CachedGridPoints[GridPointIndex];
|
|
|
|
const bool bSnapX = bForceSnap || bSampleSnapToGrid[0];
|
|
const bool bSnapY = bForceSnap || bSampleSnapToGrid[1];
|
|
|
|
return FVector2D
|
|
{
|
|
bSnapX ? GridPoint.X : InPosition.X,
|
|
bSnapY ? GridPoint.Y : InPosition.Y
|
|
};
|
|
}
|
|
|
|
const FVector SBlendSpaceGridWidget::ScreenPositionToSampleValueWithSnapping(const FVector2D& InPosition, bool bForceSnap) const
|
|
{
|
|
FVector SampleValue = ScreenPositionToSampleValue(InPosition, true);
|
|
|
|
const int32 GridPointIndex = FindClosestGridPointIndexFromScreenPosition(InPosition);
|
|
const FVector GridPos = CachedSamplePoints[GridPointIndex];
|
|
|
|
const bool bSnapX = bForceSnap || bSampleSnapToGrid[0];
|
|
const bool bSnapY = bForceSnap || bSampleSnapToGrid[1];
|
|
return FVector
|
|
{
|
|
bSnapX ? GridPos.X : SampleValue.X,
|
|
bSnapY ? GridPos.Y : SampleValue.Y,
|
|
0.f
|
|
};
|
|
}
|
|
|
|
int32 SBlendSpaceGridWidget::FindClosestGridPointIndexFromScreenPosition(const FVector2D& InPosition) const
|
|
{
|
|
// Clamp the screen position to the grid
|
|
const FVector2D GridPosition(
|
|
FMath::Clamp(InPosition.X, CachedGridRectangle.Left, CachedGridRectangle.Right),
|
|
FMath::Clamp(InPosition.Y, CachedGridRectangle.Top, CachedGridRectangle.Bottom));
|
|
// Find the closest grid point
|
|
double Distance = TNumericLimits<double>::Max();
|
|
int32 GridPointIndex = INDEX_NONE;
|
|
for (int32 Index = 0; Index < CachedGridPoints.Num(); ++Index)
|
|
{
|
|
const FVector2D& GridPoint = CachedGridPoints[Index];
|
|
const double DistanceToGrid = FVector2D::DistSquared(GridPosition, GridPoint);
|
|
if (DistanceToGrid < Distance)
|
|
{
|
|
Distance = DistanceToGrid;
|
|
GridPointIndex = Index;
|
|
}
|
|
}
|
|
|
|
checkf(GridPointIndex != INDEX_NONE, TEXT("Unable to find gridpoint"));
|
|
|
|
return GridPointIndex;
|
|
}
|
|
|
|
const FVector2D SBlendSpaceGridWidget::SampleValueToNormalizedPosition(const FVector& SampleValue) const
|
|
{
|
|
// Convert the sample value to 0 to 1 form
|
|
FVector2D NormalizedPosition(
|
|
((SampleValue.X - SampleValueMin.X) / SampleValueRange.X),
|
|
GridType == EGridType::TwoAxis ? ((SampleValueMax.Y - SampleValue.Y) / SampleValueRange.Y) : 0.5f);
|
|
return NormalizedPosition;
|
|
}
|
|
|
|
const FVector2D SBlendSpaceGridWidget::SampleValueToScreenPosition(const FVector& SampleValue) const
|
|
{
|
|
const FVector2D NormalizedPosition = SampleValueToNormalizedPosition(SampleValue);
|
|
const FVector2D GridSize = CachedGridRectangle.GetSize();
|
|
const FVector2D GridCorner = CachedGridRectangle.GetCenter() - 0.5f * GridSize;
|
|
const FVector2D ScreenPosition = GridCorner + NormalizedPosition * CachedGridRectangle.GetSize();
|
|
return ScreenPosition;
|
|
}
|
|
|
|
const FVector SBlendSpaceGridWidget::ScreenPositionToSampleValue(const FVector2D& ScreenPosition, bool bClamp) const
|
|
{
|
|
FVector2D LocalGridPosition = ScreenPosition;
|
|
// Move to center of grid and convert to 0 - 1 form
|
|
LocalGridPosition -= CachedGridRectangle.GetCenter();
|
|
LocalGridPosition /= (CachedGridRectangle.GetSize() * 0.5f);
|
|
LocalGridPosition += FVector2D::UnitVector;
|
|
LocalGridPosition *= 0.5f;
|
|
|
|
// Calculate the sample value by mapping it to the blend parameter range
|
|
FVector SampleValue
|
|
(
|
|
(LocalGridPosition.X * SampleValueRange.X) + SampleValueMin.X,
|
|
(GridType == EGridType::TwoAxis) ? SampleValueMax.Y - (LocalGridPosition.Y * SampleValueRange.Y) : 0.0f,
|
|
0.f
|
|
);
|
|
if (bClamp)
|
|
{
|
|
SampleValue.X = FMath::Clamp(SampleValue.X, SampleValueMin.X, SampleValueMax.X);
|
|
SampleValue.Y = FMath::Clamp(SampleValue.Y, SampleValueMin.Y, SampleValueMax.Y);
|
|
}
|
|
return SampleValue;
|
|
}
|
|
|
|
const FSlateRect SBlendSpaceGridWidget::GetGridRectangleFromGeometry(const FGeometry& MyGeometry)
|
|
{
|
|
const float TopOffset = bReadOnly ? 0.0f : 20.0f; // Ideally we'd get the size of the buttons (showing the label/triangulation etc)
|
|
FSlateRect WindowRect = FSlateRect(0, TopOffset, static_cast<float>(MyGeometry.GetLocalSize().X), static_cast<float>(MyGeometry.GetLocalSize().Y));
|
|
if (!bStretchToFit)
|
|
{
|
|
UpdateGridRatioMargin(WindowRect.GetSize());
|
|
}
|
|
|
|
return WindowRect.InsetBy(GridMargin + GridRatioMargin);
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::IsSampleValueWithinMouseRange(const FVector& SampleValue, float& OutDistance) const
|
|
{
|
|
const FVector2D GridPosition = SampleValueToScreenPosition(SampleValue);
|
|
OutDistance = static_cast<float>(FVector2D::Distance(LocalMousePosition, GridPosition));
|
|
return (OutDistance < ClickAndHighlightThreshold);
|
|
}
|
|
|
|
int32 SBlendSpaceGridWidget::GetClosestSamplePointIndexToMouse() const
|
|
{
|
|
float BestDistance = FLT_MAX;
|
|
int32 BestIndex = INDEX_NONE;
|
|
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
for (int32 SampleIndex = 0; SampleIndex < Samples.Num(); ++SampleIndex)
|
|
{
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
float Distance;
|
|
if (IsSampleValueWithinMouseRange(Sample.SampleValue, Distance))
|
|
{
|
|
if(Distance < BestDistance)
|
|
{
|
|
BestDistance = Distance;
|
|
BestIndex = SampleIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return BestIndex;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::StartPreviewing()
|
|
{
|
|
bSamplePreviewing = true;
|
|
LastPreviewingMousePosition = LocalMousePosition;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::StopPreviewing()
|
|
{
|
|
bSamplePreviewing = false;
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetToolTipSampleValidity() const
|
|
{
|
|
const UBlendSpace* BlendSpace = BlendSpaceBase.Get();
|
|
FText ToolTipText = FText::GetEmpty();
|
|
if (!bReadOnly && BlendSpace && BlendSpace->bInterpolateUsingGrid)
|
|
{
|
|
int32 SampleIndex = INDEX_NONE;
|
|
if (DragState == EDragState::None)
|
|
{
|
|
SampleIndex = HighlightedSampleIndex;
|
|
}
|
|
else if (DragState == EDragState::DragSample)
|
|
{
|
|
SampleIndex = DraggedSampleIndex;
|
|
}
|
|
else
|
|
{
|
|
SampleIndex = INDEX_NONE;
|
|
}
|
|
|
|
if (SampleIndex != INDEX_NONE && BlendSpace->IsValidBlendSampleIndex(SampleIndex))
|
|
{
|
|
float SampleLookupWeight = GetSampleLookupWeight(SampleIndex);
|
|
if (SampleLookupWeight >= 1.0f)
|
|
{
|
|
return ToolTipText;
|
|
}
|
|
else if (SampleLookupWeight <= 0.0f)
|
|
{
|
|
ToolTipText = FText::Format(
|
|
LOCTEXT("SampleValidityZero", "Self weight is zero"),
|
|
SampleLookupWeight);
|
|
}
|
|
else if (SampleLookupWeight <= SampleLookupWeightThreshold)
|
|
{
|
|
ToolTipText = FText::Format(
|
|
LOCTEXT("SampleValidityLow", "Self weight is low: {0}"),
|
|
SampleLookupWeight);
|
|
}
|
|
else if (SampleLookupWeight < 1.0f)
|
|
{
|
|
ToolTipText = FText::Format(
|
|
LOCTEXT("SampleValidity", "Self weight: {0}"),
|
|
SampleLookupWeight);
|
|
}
|
|
}
|
|
}
|
|
return ToolTipText;
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetToolTipAnimationName() const
|
|
{
|
|
FText ToolTipText = FText::GetEmpty();
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
const FText PreviewValue = LOCTEXT("PreviewValueTooltip", "Preview Value");
|
|
|
|
if(bReadOnly)
|
|
{
|
|
ToolTipText = PreviewValue;
|
|
}
|
|
else
|
|
{
|
|
switch (DragState)
|
|
{
|
|
// If we are not dragging, but over a valid blend sample return its animation asset name
|
|
case EDragState::None:
|
|
{
|
|
if (bHighlightPreviewPin)
|
|
{
|
|
ToolTipText = PreviewValue;
|
|
}
|
|
else if (HighlightedSampleIndex != INDEX_NONE && BlendSpace->IsValidBlendSampleIndex(HighlightedSampleIndex))
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(HighlightedSampleIndex);
|
|
ToolTipText = GetSampleName(BlendSample, HighlightedSampleIndex);
|
|
}
|
|
else if(TargetPosition.IsSet())
|
|
{
|
|
ToolTipText = PreviewValue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EDragState::PreDrag:
|
|
{
|
|
break;
|
|
}
|
|
|
|
// If we are dragging a sample return the dragged sample's animation asset name
|
|
case EDragState::DragSample:
|
|
{
|
|
if (BlendSpace->IsValidBlendSampleIndex(DraggedSampleIndex))
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(DraggedSampleIndex);
|
|
ToolTipText = GetSampleName(BlendSample, DraggedSampleIndex);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// If we are performing a drag/drop operation return the cached operation animation name
|
|
case EDragState::DragDrop:
|
|
{
|
|
ToolTipText = DragDropAnimationName;
|
|
break;
|
|
}
|
|
|
|
case EDragState::DragDropOverride:
|
|
{
|
|
ToolTipText = DragDropAnimationName;
|
|
break;
|
|
}
|
|
|
|
case EDragState::InvalidDragDrop:
|
|
{
|
|
break;
|
|
}
|
|
|
|
// If we are previewing return a descriptive label
|
|
case EDragState::Preview:
|
|
{
|
|
ToolTipText = PreviewValue;
|
|
break;
|
|
}
|
|
default:
|
|
check(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ToolTipText;
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetToolTipSampleValue() const
|
|
{
|
|
FText ToolTipText = FText::GetEmpty();
|
|
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
static const FTextFormat OneAxisFormat = LOCTEXT("OneAxisFormat", "{0}: {1}");
|
|
static const FTextFormat TwoAxisFormat = LOCTEXT("TwoAxisFormat", "{0}: {1} {2}: {3}");
|
|
const FTextFormat& ValueFormattingText = (GridType == EGridType::TwoAxis) ? TwoAxisFormat : OneAxisFormat;
|
|
|
|
auto AddAdvancedPreview = [this, &ToolTipText, BlendSpace]()
|
|
{
|
|
FTextBuilder TextBuilder;
|
|
TextBuilder.AppendLine(ToolTipText);
|
|
|
|
if (bAdvancedPreview)
|
|
{
|
|
for (const FBlendSampleData& SampleData : PreviewedSamples)
|
|
{
|
|
if(BlendSpace->IsValidBlendSampleIndex(SampleData.SampleDataIndex))
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(SampleData.SampleDataIndex);
|
|
static const FTextFormat SampleFormat = LOCTEXT("SampleFormat", "{0}: {1}");
|
|
TextBuilder.AppendLine(FText::Format(SampleFormat, GetSampleName(BlendSample, SampleData.SampleDataIndex), FText::AsNumber(SampleData.TotalWeight)));
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolTipText = TextBuilder.ToText();
|
|
};
|
|
|
|
if(bReadOnly)
|
|
{
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(PreviewPosition.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(PreviewPosition.Y)));
|
|
|
|
AddAdvancedPreview();
|
|
}
|
|
else
|
|
{
|
|
switch (DragState)
|
|
{
|
|
// If we are over a sample return its sample value if valid and otherwise show an error message as to why the sample is invalid
|
|
case EDragState::None:
|
|
{
|
|
if (bHighlightPreviewPin)
|
|
{
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(PreviewPosition.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(PreviewPosition.Y)));
|
|
|
|
AddAdvancedPreview();
|
|
}
|
|
else if (HighlightedSampleIndex != INDEX_NONE && BlendSpace->IsValidBlendSampleIndex(HighlightedSampleIndex))
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(HighlightedSampleIndex);
|
|
|
|
// Check if the sample is valid
|
|
if (BlendSample.bIsValid)
|
|
{
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(BlendSample.SampleValue.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(BlendSample.SampleValue.Y)));
|
|
}
|
|
else
|
|
{
|
|
ToolTipText = GetSampleErrorMessage(BlendSample);
|
|
}
|
|
}
|
|
else if(TargetPosition.IsSet())
|
|
{
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(PreviewPosition.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(PreviewPosition.Y)));
|
|
|
|
AddAdvancedPreview();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EDragState::PreDrag:
|
|
{
|
|
break;
|
|
}
|
|
|
|
// If we are dragging a sample return the current sample value it is hovered at
|
|
case EDragState::DragSample:
|
|
{
|
|
if (DraggedSampleIndex != INDEX_NONE)
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(DraggedSampleIndex);
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(BlendSample.SampleValue.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(BlendSample.SampleValue.Y)));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// If we are performing a drag and drop operation return the current sample value it is hovered at
|
|
case EDragState::DragDrop:
|
|
{
|
|
const FVector SampleValue = ScreenPositionToSampleValueWithSnapping(LocalMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown());
|
|
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(SampleValue.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(SampleValue.Y)));
|
|
|
|
break;
|
|
}
|
|
|
|
case EDragState::DragDropOverride:
|
|
{
|
|
if(HoveredAnimationName.IsEmpty())
|
|
{
|
|
static const FTextFormat OverrideAnimationFormat = LOCTEXT("InvalidSampleChangingFormat", "Changing sample to {0}");
|
|
ToolTipText = FText::Format(OverrideAnimationFormat, DragDropAnimationName);
|
|
}
|
|
else
|
|
{
|
|
static const FTextFormat OverrideAnimationFormat = LOCTEXT("ValidSampleChangingFormat", "Changing sample from {0} to {1}");
|
|
ToolTipText = FText::Format(OverrideAnimationFormat, HoveredAnimationName, DragDropAnimationName);
|
|
}
|
|
break;
|
|
}
|
|
// If the drag and drop operation is invalid return the cached error message as to why it is invalid
|
|
case EDragState::InvalidDragDrop:
|
|
{
|
|
ToolTipText = InvalidDragDropText;
|
|
break;
|
|
}
|
|
|
|
// If we are setting the preview value return the current preview sample value
|
|
case EDragState::Preview:
|
|
{
|
|
ToolTipText = FText::Format(ValueFormattingText, ParameterXName, FText::FromString(FString::SanitizeFloat(PreviewPosition.X)), ParameterYName, FText::FromString(FString::SanitizeFloat(PreviewPosition.Y)));
|
|
|
|
AddAdvancedPreview();
|
|
break;
|
|
}
|
|
default:
|
|
check(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ToolTipText;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::EnableStatusBarMessage(bool bEnable)
|
|
{
|
|
if(!bReadOnly)
|
|
{
|
|
if(bEnable)
|
|
{
|
|
if (!StatusBarMessageHandle.IsValid())
|
|
{
|
|
if(UStatusBarSubsystem* StatusBarSubsystem = GEditor->GetEditorSubsystem<UStatusBarSubsystem>())
|
|
{
|
|
StatusBarMessageHandle = StatusBarSubsystem->PushStatusBarMessage(StatusBarName, MakeAttributeLambda([]()
|
|
{
|
|
return LOCTEXT("StatusBarMssage", "Hold Ctrl to move preview value, and Alt to show weight details. Click and drag sample points to move them, with Shift to snap to the grid.");
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (StatusBarMessageHandle.IsValid())
|
|
{
|
|
if(UStatusBarSubsystem* StatusBarSubsystem = GEditor->GetEditorSubsystem<UStatusBarSubsystem>())
|
|
{
|
|
StatusBarSubsystem->PopStatusBarMessage(StatusBarName, StatusBarMessageHandle);
|
|
StatusBarMessageHandle.Reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetSampleErrorMessage(const FBlendSample &BlendSample) const
|
|
{
|
|
const FVector2D GridPosition = SampleValueToScreenPosition(BlendSample.SampleValue);
|
|
// Either an invalid animation asset set
|
|
if (BlendSample.Animation == nullptr)
|
|
{
|
|
static const FText NoAnimationErrorText = LOCTEXT("NoAnimationErrorText", "Invalid Animation for Sample");
|
|
return NoAnimationErrorText;
|
|
}
|
|
// Or not aligned on the grid (which means that it does not match one of the cached grid points, == for FVector2D fails to compare though :/)
|
|
else if (!CachedGridPoints.FindByPredicate([&](const FVector2D& Other) { return FMath::IsNearlyEqual(GridPosition.X, Other.X) && FMath::IsNearlyEqual(GridPosition.Y, Other.Y);}))
|
|
{
|
|
static const FText SampleNotAtGridPoint = LOCTEXT("SampleNotAtGridPointErrorText", "Sample is not on a valid Grid Point");
|
|
return SampleNotAtGridPoint;
|
|
}
|
|
|
|
static const FText UnknownError = LOCTEXT("UnknownErrorText", "Sample is invalid for an Unknown Reason");
|
|
return UnknownError;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::ShowToolTip()
|
|
{
|
|
if(HighlightedSampleIndex != INDEX_NONE && ToolTipSampleIndex != HighlightedSampleIndex)
|
|
{
|
|
ToolTipSampleIndex = HighlightedSampleIndex;
|
|
if(OnExtendSampleTooltip.IsBound())
|
|
{
|
|
ToolTipExtensionContainer->SetContent(OnExtendSampleTooltip.Execute(HighlightedSampleIndex));
|
|
}
|
|
}
|
|
|
|
SetToolTip(ToolTip);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::ResetToolTip()
|
|
{
|
|
ToolTipSampleIndex = INDEX_NONE;
|
|
ToolTipExtensionContainer->SetContent(SNullWidget::NullWidget);
|
|
SetToolTip(nullptr);
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetInputBoxVisibility(const int32 ParameterIndex) const
|
|
{
|
|
bool bVisible = !bReadOnly;
|
|
// Only show input boxes when a sample is selected (hide it when one is being dragged since we have the tooltip information as well)
|
|
bVisible &= (SelectedSampleIndex != INDEX_NONE && DraggedSampleIndex == INDEX_NONE);
|
|
if ( ParameterIndex == 1 )
|
|
{
|
|
bVisible &= (GridType == EGridType::TwoAxis);
|
|
}
|
|
|
|
return bVisible ? EVisibility::Visible : EVisibility::Collapsed;
|
|
}
|
|
|
|
TOptional<float> SBlendSpaceGridWidget::GetInputBoxValue(const int32 ParameterIndex) const
|
|
{
|
|
checkf(ParameterIndex < 3, TEXT("Invalid parameter index, suppose to be within FVector array range"));
|
|
float ReturnValue = 0.0f;
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
if (SelectedSampleIndex != INDEX_NONE && SelectedSampleIndex < BlendSpace->GetNumberOfBlendSamples())
|
|
{
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(SelectedSampleIndex);
|
|
ReturnValue = static_cast<float>(BlendSample.SampleValue[ParameterIndex]);
|
|
}
|
|
}
|
|
return ReturnValue;
|
|
}
|
|
|
|
TOptional<float> SBlendSpaceGridWidget::GetInputBoxMinValue(const int32 ParameterIndex) const
|
|
{
|
|
checkf(ParameterIndex < 3, TEXT("Invalid parameter index, suppose to be within FVector array range"));
|
|
return static_cast<float>(SampleValueMin[ParameterIndex]);
|
|
}
|
|
|
|
TOptional<float> SBlendSpaceGridWidget::GetInputBoxMaxValue(const int32 ParameterIndex) const
|
|
{
|
|
checkf(ParameterIndex < 3, TEXT("Invalid parameter index, suppose to be within FVector array range"));
|
|
return static_cast<float>(SampleValueMax[ParameterIndex]);
|
|
}
|
|
|
|
float SBlendSpaceGridWidget::GetInputBoxDelta(const int32 ParameterIndex) const
|
|
{
|
|
checkf(ParameterIndex < 3, TEXT("Invalid parameter index, suppose to be within FVector array range"));
|
|
return static_cast<float>(SampleGridDelta[ParameterIndex]);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnInputBoxValueCommited(const float NewValue, ETextCommit::Type CommitType, const int32 ParameterIndex)
|
|
{
|
|
OnInputBoxValueChanged(NewValue, ParameterIndex, false);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnInputBoxValueChanged(const float NewValue, const int32 ParameterIndex, bool bIsInteractive)
|
|
{
|
|
// Ignore any SNumericEntryBox.OnValueChanged broadcasts if sliding has finished and OnInputBoxValueCommited will have been broadcasted already
|
|
if (bIsInteractive && !bSliderMovement[ParameterIndex])
|
|
{
|
|
return;
|
|
}
|
|
|
|
checkf(ParameterIndex < 2, TEXT("Invalid parameter index, suppose to be within FVector array range"));
|
|
|
|
if (SelectedSampleIndex != INDEX_NONE && BlendSpaceBase.Get() != nullptr)
|
|
{
|
|
// Retrieve current sample value
|
|
const FBlendSample& Sample = BlendSpaceBase.Get()->GetBlendSample(SelectedSampleIndex);
|
|
FVector SampleValue = Sample.SampleValue;
|
|
|
|
// Calculate snapped value
|
|
if (bSampleSnapToGrid[ParameterIndex])
|
|
{
|
|
const double MinOffset = NewValue - SampleValueMin[ParameterIndex];
|
|
double GridSteps = MinOffset / SampleGridDelta[ParameterIndex];
|
|
int64 FlooredSteps = FMath::FloorToInt(GridSteps);
|
|
GridSteps -= FlooredSteps;
|
|
FlooredSteps = (GridSteps > .5) ? FlooredSteps + 1 : FlooredSteps;
|
|
|
|
// Temporary snap this value to closest point on grid (since the spin box delta does not provide the desired functionality)
|
|
SampleValue[ParameterIndex] = SampleValueMin[ParameterIndex] + (FlooredSteps * SampleGridDelta[ParameterIndex]);
|
|
}
|
|
else
|
|
{
|
|
SampleValue[ParameterIndex] = NewValue;
|
|
}
|
|
|
|
OnSampleMoved.ExecuteIfBound(SelectedSampleIndex, SampleValue, bIsInteractive);
|
|
}
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnInputSliderBegin(const int32 ParameterIndex)
|
|
{
|
|
ensure(bSliderMovement[ParameterIndex] == false);
|
|
bSliderMovement[ParameterIndex] = true;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnInputSliderEnd(const float NewValue, const int32 ParameterIndex)
|
|
{
|
|
ensure(bSliderMovement[ParameterIndex] == true);
|
|
bSliderMovement[ParameterIndex] = false;
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetSampleToolTipVisibility() const
|
|
{
|
|
// Show tool tip when the grid is empty
|
|
return (!bReadOnly && BlendSpaceBase.Get() != nullptr && BlendSpaceBase.Get()->GetNumberOfBlendSamples() == 0) ? EVisibility::Visible : EVisibility::Collapsed;
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetPreviewToolTipVisibility() const
|
|
{
|
|
// Only show preview tooltip until the user discovers the functionality
|
|
return bReadOnly ? EVisibility::Collapsed : EVisibility::Visible;
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetTriangulationButtonVisibility() const
|
|
{
|
|
if (bShowSettingsButtons && GridType == EGridType::TwoAxis)
|
|
{
|
|
if (const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
if (!BlendSpace->bInterpolateUsingGrid)
|
|
{
|
|
return EVisibility::Visible;
|
|
}
|
|
}
|
|
}
|
|
return EVisibility::Collapsed;
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetAnimationNamesButtonVisibility() const
|
|
{
|
|
return bShowSettingsButtons ? EVisibility::Visible : EVisibility::Collapsed;
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::ToggleFittingType()
|
|
{
|
|
bStretchToFit = !bStretchToFit;
|
|
|
|
// If toggle to stretching, reset the margin immediately
|
|
if (bStretchToFit)
|
|
{
|
|
GridRatioMargin.Top = GridRatioMargin.Bottom = GridRatioMargin.Left = GridRatioMargin.Right = 0.0f;
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
|
|
FReply SBlendSpaceGridWidget::ToggleShowAnimationNames()
|
|
{
|
|
bShowAnimationNames = !bShowAnimationNames;
|
|
return FReply::Handled();
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::UpdateGridRatioMargin(const FVector2D& GeometrySize)
|
|
{
|
|
if (GridType == EGridType::TwoAxis)
|
|
{
|
|
// Reset values first
|
|
GridRatioMargin.Top = GridRatioMargin.Bottom = GridRatioMargin.Left = GridRatioMargin.Right = 0.0f;
|
|
if (GeometrySize.Y > GeometrySize.X)
|
|
{
|
|
const double Difference = GeometrySize.Y - GeometrySize.X;
|
|
GridRatioMargin.Top = GridRatioMargin.Bottom = static_cast<float>(Difference) * 0.5f;
|
|
}
|
|
else if (GeometrySize.X > GeometrySize.Y)
|
|
{
|
|
const double Difference = GeometrySize.X - GeometrySize.Y;
|
|
GridRatioMargin.Left = GridRatioMargin.Right = static_cast<float>(Difference) * 0.5f;
|
|
}
|
|
}
|
|
}
|
|
|
|
FText SBlendSpaceGridWidget::GetFittingTypeButtonToolTipText() const
|
|
{
|
|
static const FText StretchText = LOCTEXT("StretchFittingText", "Stretch Grid to Fit");
|
|
static const FText GridRatioText = LOCTEXT("GridRatioFittingText", "Fit Grid to Largest Axis");
|
|
return (bStretchToFit) ? GridRatioText : StretchText;
|
|
}
|
|
|
|
EVisibility SBlendSpaceGridWidget::GetFittingButtonVisibility() const
|
|
{
|
|
return (bShowSettingsButtons && (GridType == EGridType::TwoAxis)) ? EVisibility::Visible : EVisibility::Collapsed;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::UpdateCachedBlendParameterData()
|
|
{
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
const FBlendParameter& BlendParameterX = BlendSpace->GetBlendParameter(0);
|
|
const FBlendParameter& BlendParameterY = BlendSpace->GetBlendParameter(1);
|
|
SampleValueRange.X = BlendParameterX.Max - BlendParameterX.Min;
|
|
SampleValueRange.Y = BlendParameterY.Max - BlendParameterY.Min;
|
|
|
|
SampleValueMin.X = BlendParameterX.Min;
|
|
SampleValueMin.Y = BlendParameterY.Min;
|
|
|
|
SampleValueMax.X = BlendParameterX.Max;
|
|
SampleValueMax.Y = BlendParameterY.Max;
|
|
|
|
SampleGridDelta = SampleValueRange;
|
|
SampleGridDelta.X /= (BlendParameterX.GridNum);
|
|
SampleGridDelta.Y /= (BlendParameterY.GridNum);
|
|
|
|
bSampleSnapToGrid[0] = BlendParameterX.bSnapToGrid;
|
|
bSampleSnapToGrid[1] = BlendParameterY.bSnapToGrid;
|
|
|
|
SampleGridDivisions.X = BlendParameterX.GridNum;
|
|
SampleGridDivisions.Y = BlendParameterY.GridNum;
|
|
|
|
ParameterXName = FText::FromString(BlendParameterX.DisplayName);
|
|
ParameterYName = FText::FromString(BlendParameterY.DisplayName);
|
|
|
|
const TSharedRef< FSlateFontMeasure > FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
|
MaxVerticalAxisTextWidth = HorizontalAxisMaxTextWidth = MaxHorizontalAxisTextHeight = 0.0f;
|
|
FVector2D TextSize = FontMeasure->Measure(ParameterYName, FontInfo);
|
|
MaxVerticalAxisTextWidth = FMath::Max(MaxVerticalAxisTextWidth, static_cast<float>(TextSize.X));
|
|
|
|
TextSize = FontMeasure->Measure(FString::SanitizeFloat(SampleValueMin.Y), FontInfo);
|
|
MaxVerticalAxisTextWidth = FMath::Max(MaxVerticalAxisTextWidth, static_cast<float>(TextSize.X));
|
|
|
|
TextSize = FontMeasure->Measure(FString::SanitizeFloat(SampleValueMax.Y), FontInfo);
|
|
MaxVerticalAxisTextWidth = FMath::Max(MaxVerticalAxisTextWidth, static_cast<float>(TextSize.X));
|
|
|
|
TextSize = FontMeasure->Measure(ParameterXName, FontInfo);
|
|
MaxHorizontalAxisTextHeight = FMath::Max(MaxHorizontalAxisTextHeight, static_cast<float>(TextSize.Y));
|
|
|
|
TextSize = FontMeasure->Measure(FString::SanitizeFloat(SampleValueMin.X), FontInfo);
|
|
MaxHorizontalAxisTextHeight = FMath::Max(MaxHorizontalAxisTextHeight, static_cast<float>(TextSize.Y));
|
|
|
|
TextSize = FontMeasure->Measure(FString::SanitizeFloat(SampleValueMax.X), FontInfo);
|
|
MaxHorizontalAxisTextHeight = FMath::Max(MaxHorizontalAxisTextHeight, static_cast<float>(TextSize.Y));
|
|
HorizontalAxisMaxTextWidth = static_cast<float>(TextSize.X);
|
|
}
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
SCompoundWidget::OnMouseEnter(MyGeometry, MouseEvent);
|
|
bMouseIsOverGeometry = true;
|
|
EnableStatusBarMessage(true);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnMouseLeave(const FPointerEvent& MouseEvent)
|
|
{
|
|
SCompoundWidget::OnMouseLeave(MouseEvent);
|
|
bMouseIsOverGeometry = false;
|
|
EnableStatusBarMessage(false);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnFocusLost(const FFocusEvent& InFocusEvent)
|
|
{
|
|
SCompoundWidget::OnFocusLost(InFocusEvent);
|
|
|
|
if (DragState == EDragState::DragSample)
|
|
{
|
|
OnSampleMoved.ExecuteIfBound(DraggedSampleIndex, LastDragPosition, false);
|
|
}
|
|
HighlightedSampleIndex = DraggedSampleIndex = INDEX_NONE;
|
|
DragState = EDragState::None;
|
|
bSamplePreviewing = false;
|
|
ResetToolTip();
|
|
EnableStatusBarMessage(false);
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::SupportsKeyboardFocus() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
|
|
{
|
|
const int32 PreviousSampleIndex = HighlightedSampleIndex;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
const bool bPreviousHighlightPreviewPin = bHighlightPreviewPin;
|
|
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
if(PreviousBlendSpaceBase.Get() != BlendSpace)
|
|
{
|
|
PreviousBlendSpaceBase = BlendSpace;
|
|
InvalidateCachedData();
|
|
}
|
|
|
|
GridType = BlendSpace->IsA<UBlendSpace1D>() ? EGridType::SingleAxis : EGridType::TwoAxis;
|
|
BlendParametersToDraw = (GridType == EGridType::SingleAxis) ? 1 : 2;
|
|
|
|
if(!bReadOnly)
|
|
{
|
|
if (DragState == EDragState::None)
|
|
{
|
|
// Check if we are highlighting preview pin
|
|
float Distance;
|
|
bHighlightPreviewPin = IsSampleValueWithinMouseRange(PreviewPosition, Distance);
|
|
if (bHighlightPreviewPin)
|
|
{
|
|
if (bHighlightPreviewPin != bPreviousHighlightPreviewPin)
|
|
{
|
|
ShowToolTip();
|
|
}
|
|
}
|
|
else if (bPreviousHighlightPreviewPin != bHighlightPreviewPin)
|
|
{
|
|
ResetToolTip();
|
|
}
|
|
|
|
// Determine highlighted sample
|
|
HighlightedSampleIndex = GetClosestSamplePointIndexToMouse();
|
|
|
|
if (!bHighlightPreviewPin)
|
|
{
|
|
// If we started selecting or selected a different sample make sure we show/hide the tooltip
|
|
if (PreviousSampleIndex != HighlightedSampleIndex)
|
|
{
|
|
if (HighlightedSampleIndex != INDEX_NONE)
|
|
{
|
|
ShowToolTip();
|
|
}
|
|
else
|
|
{
|
|
ResetToolTip();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (DragState == EDragState::DragSample)
|
|
{
|
|
// If we are dragging a sample, find out whether or not it has actually moved to a
|
|
// different grid position since the last tick and update the blend space accordingly
|
|
FVector SampleValue = ScreenPositionToSampleValueWithSnapping(LocalMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown());
|
|
// Only allow dragging on each axis if not locked
|
|
const FBlendSample& BlendSample = BlendSpace->GetBlendSample(DraggedSampleIndex);
|
|
if (SampleValue != LastDragPosition)
|
|
{
|
|
LastDragPosition = SampleValue;
|
|
OnSampleMoved.ExecuteIfBound(DraggedSampleIndex, SampleValue, true);
|
|
}
|
|
}
|
|
else if (DragState == EDragState::DragDrop || DragState == EDragState::InvalidDragDrop || DragState == EDragState::DragDropOverride)
|
|
{
|
|
// Validate that the sample is not overlapping with a current sample when doing a
|
|
// drag/drop operation and that we are dropping a valid animation for the blend
|
|
// space (type)
|
|
const FVector DropSampleValue = ScreenPositionToSampleValueWithSnapping(LocalMousePosition, FSlateApplication::Get().GetModifierKeys().IsShiftDown());
|
|
const bool bValidPosition = BlendSpace->IsSampleWithinBounds(DropSampleValue);
|
|
const bool bExistingSample = BlendSpace->IsTooCloseToExistingSamplePoint(DropSampleValue, INDEX_NONE);
|
|
const bool bValidSequence = ValidateAnimationSequence(DragDropAnimationSequence, InvalidDragDropText);
|
|
|
|
if (!bValidSequence)
|
|
{
|
|
DragState = EDragState::InvalidDragDrop;
|
|
}
|
|
else if (!bValidPosition)
|
|
{
|
|
InvalidDragDropText = InvalidSamplePositionDragDropText;
|
|
DragState = EDragState::InvalidDragDrop;
|
|
}
|
|
else if (bExistingSample)
|
|
{
|
|
const TArray<FBlendSample>& Samples = BlendSpace->GetBlendSamples();
|
|
for (int32 SampleIndex = 0; SampleIndex < Samples.Num(); ++SampleIndex)
|
|
{
|
|
const FBlendSample& Sample = Samples[SampleIndex];
|
|
if (Sample.SampleValue == DropSampleValue)
|
|
{
|
|
HoveredAnimationName = Sample.Animation ? FText::FromString(Sample.Animation->GetName()) : FText::GetEmpty();
|
|
break;
|
|
}
|
|
}
|
|
|
|
DragState = EDragState::DragDropOverride;
|
|
}
|
|
else if (bValidPosition && bValidSequence && !bExistingSample)
|
|
{
|
|
DragState = EDragState::DragDrop;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we should update the preview sample value
|
|
if (bSamplePreviewing)
|
|
{
|
|
// Clamping happens later
|
|
LastPreviewingMousePosition.X = LocalMousePosition.X;
|
|
LastPreviewingMousePosition.Y = LocalMousePosition.Y;
|
|
FModifierKeysState ModifierKeyState = FSlateApplication::Get().GetModifierKeys();
|
|
bool bIsManualPreviewing = !bReadOnly && IsHovered() && bMouseIsOverGeometry && ModifierKeyState.IsControlDown();
|
|
if (TargetPosition.IsSet() && !bIsManualPreviewing)
|
|
{
|
|
PreviewPosition = TargetPosition.Get();
|
|
if (bReadOnly)
|
|
{
|
|
// Happens when we are showing in the graph - don't want to render outside the valid region
|
|
PreviewPosition = BlendSpace->GetClampedAndWrappedBlendInput(PreviewPosition);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PreviewPosition = ScreenPositionToSampleValue(LastPreviewingMousePosition, true);
|
|
}
|
|
|
|
|
|
if (FilteredPosition.IsSet())
|
|
{
|
|
PreviewFilteredPosition = BlendSpace->GetClampedAndWrappedBlendInput(FilteredPosition.Get());
|
|
}
|
|
|
|
// Retrieve and cache weighted samples
|
|
PreviewedSamples.Empty(4);
|
|
BlendSpace->GetSamplesFromBlendInput(PreviewPosition, PreviewedSamples, CachedTriangulationIndex, false);
|
|
}
|
|
}
|
|
|
|
// Refresh cache blendspace/grid data if needed
|
|
if (bRefreshCachedData)
|
|
{
|
|
UpdateCachedBlendParameterData();
|
|
GridMargin = bShowAxisLabels ? FMargin(MaxVerticalAxisTextWidth + (TextMargin * 2.0f), TextMargin, (HorizontalAxisMaxTextWidth *.5f) + TextMargin, MaxHorizontalAxisTextHeight + (TextMargin * 2.0f)) :
|
|
FMargin(TextMargin, TextMargin, TextMargin, TextMargin);
|
|
bRefreshCachedData = false;
|
|
}
|
|
|
|
// Always need to update the rectangle and grid points according to the geometry (this can differ per tick)
|
|
CachedGridRectangle = GetGridRectangleFromGeometry(AllottedGeometry);
|
|
CalculateGridPoints();
|
|
}
|
|
|
|
const FVector SBlendSpaceGridWidget::GetPreviewPosition() const
|
|
{
|
|
return PreviewPosition;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::SetPreviewingState(const FVector& InPosition, const FVector& InFilteredPosition)
|
|
{
|
|
if (const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
PreviewFilteredPosition = BlendSpace->GetClampedAndWrappedBlendInput(InFilteredPosition);
|
|
}
|
|
else
|
|
{
|
|
PreviewFilteredPosition = InFilteredPosition;
|
|
}
|
|
PreviewPosition = InPosition;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::InvalidateCachedData()
|
|
{
|
|
bRefreshCachedData = true;
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::InvalidateState()
|
|
{
|
|
if (HighlightedSampleIndex != INDEX_NONE)
|
|
{
|
|
ResetToolTip();
|
|
}
|
|
|
|
if (DragState != EDragState::None)
|
|
{
|
|
DragState = EDragState::None;
|
|
}
|
|
|
|
SelectedSampleIndex = (BlendSpaceBase.Get() != nullptr && BlendSpaceBase.Get()->IsValidBlendSampleIndex(SelectedSampleIndex)) ? SelectedSampleIndex : INDEX_NONE;
|
|
HighlightedSampleIndex = DraggedSampleIndex = INDEX_NONE;
|
|
}
|
|
|
|
const bool SBlendSpaceGridWidget::IsValidDragDropOperation(const FDragDropEvent& DragDropEvent, FText& InvalidOperationText)
|
|
{
|
|
bool bResult = false;
|
|
|
|
TSharedPtr<FAssetDragDropOp> DragDropOperation = DragDropEvent.GetOperationAs<FAssetDragDropOp>();
|
|
|
|
if (DragDropOperation.IsValid())
|
|
{
|
|
// Check whether or not this animation is compatible with the blend space
|
|
DragDropAnimationSequence = FAssetData::GetFirstAsset<UAnimSequence>(DragDropOperation->GetAssets());
|
|
if (DragDropAnimationSequence)
|
|
{
|
|
bResult = ValidateAnimationSequence(DragDropAnimationSequence, InvalidOperationText);
|
|
}
|
|
else
|
|
{
|
|
// If is isn't an animation set error message
|
|
bResult = false;
|
|
InvalidOperationText = FText::FromString("Invalid Asset Type");
|
|
}
|
|
}
|
|
|
|
if (!bResult)
|
|
{
|
|
DragDropOperation->SetToolTip(InvalidOperationText, DragDropOperation->GetIcon());
|
|
}
|
|
else
|
|
{
|
|
DragDropAnimationName = FText::FromString(DragDropAnimationSequence->GetName());
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::ValidateAnimationSequence(const UAnimSequence* AnimationSequence, FText& InvalidOperationText) const
|
|
{
|
|
if (AnimationSequence != nullptr)
|
|
{
|
|
if(const UBlendSpace* BlendSpace = BlendSpaceBase.Get())
|
|
{
|
|
if(BlendSpace->IsAsset())
|
|
{
|
|
// If there are any existing blend samples check whether or not the the animation should be additive and if so if the additive matches the existing samples
|
|
if ( BlendSpace->GetNumberOfBlendSamples() > 0)
|
|
{
|
|
const bool bIsAdditive = BlendSpace->ShouldAnimationBeAdditive();
|
|
if (AnimationSequence->IsValidAdditive() != bIsAdditive)
|
|
{
|
|
InvalidOperationText = FText::FromString(bIsAdditive ? "Animation should be additive" : "Animation should be non-additive");
|
|
return false;
|
|
}
|
|
|
|
// If it is the supported additive type, but does not match existing samples
|
|
if (!BlendSpace->DoesAnimationMatchExistingSamples(AnimationSequence))
|
|
{
|
|
InvalidOperationText = FText::FromString("Additive Animation Type does not match existing Samples");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if the supplied animation is of a different additive animation type
|
|
if (!BlendSpace->IsAnimationCompatible(AnimationSequence))
|
|
{
|
|
InvalidOperationText = FText::FromString("Invalid Additive Animation Type");
|
|
return false;
|
|
}
|
|
|
|
// Check if the supplied animation is compatible with the skeleton
|
|
if (!BlendSpace->IsAnimationCompatibleWithSkeleton(AnimationSequence))
|
|
{
|
|
InvalidOperationText = FText::FromString("Animation is incompatible with the skeleton");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return AnimationSequence != nullptr;
|
|
}
|
|
|
|
const bool SBlendSpaceGridWidget::IsPreviewing() const
|
|
{
|
|
FModifierKeysState ModifierKeyState = FSlateApplication::Get().GetModifierKeys();
|
|
bool bIsManualPreviewing = !bReadOnly && IsHovered() && bMouseIsOverGeometry && ModifierKeyState.IsControlDown();
|
|
return (bSamplePreviewing && !TargetPosition.IsSet()) || (TargetPosition.IsSet() && bIsManualPreviewing);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::BindCommands()
|
|
{
|
|
// This should not be called twice on the same instance
|
|
check(!UICommandList.IsValid());
|
|
UICommandList = MakeShared<FUICommandList>();
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Cut,
|
|
FExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::OnBlendSampleCut),
|
|
FCanExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::CanBlendSampleCutCopy)
|
|
);
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Copy,
|
|
FExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::OnBlendSampleCopy),
|
|
FCanExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::CanBlendSampleCutCopy)
|
|
);
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Paste,
|
|
FExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::OnBlendSamplePaste),
|
|
FCanExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::CanBlendSamplePaste)
|
|
);
|
|
|
|
UICommandList->MapAction(
|
|
FGenericCommands::Get().Delete,
|
|
FExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::OnBlendSampleDelete),
|
|
FCanExecuteAction::CreateSP(this, &SBlendSpaceGridWidget::CanBlendSampleDelete)
|
|
);
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::CanBlendSampleCutCopy()
|
|
{
|
|
return BlendSpaceBase.Get()->IsValidBlendSampleIndex(SelectedSampleIndex);
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::CanBlendSamplePaste()
|
|
{
|
|
FString PastedText;
|
|
FPlatformApplicationMisc::ClipboardPaste(PastedText);
|
|
return PastedText.StartsWith(BlendSpaceBase.Get()->IsAsset() ? BlendSampleClipboardHeaderAsset : BlendSampleClipboardHeaderGraph);
|
|
}
|
|
|
|
bool SBlendSpaceGridWidget::CanBlendSampleDelete()
|
|
{
|
|
return BlendSpaceBase.Get()->IsValidBlendSampleIndex(SelectedSampleIndex);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnBlendSampleDelete()
|
|
{
|
|
if (SelectedSampleIndex != INDEX_NONE)
|
|
{
|
|
OnSampleRemoved.ExecuteIfBound(SelectedSampleIndex);
|
|
|
|
if (SelectedSampleIndex == HighlightedSampleIndex)
|
|
{
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
ResetToolTip();
|
|
}
|
|
|
|
SelectedSampleIndex = INDEX_NONE;
|
|
}
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnBlendSampleCut()
|
|
{
|
|
if (BlendSpaceBase.Get()->IsValidBlendSampleIndex(SelectedSampleIndex))
|
|
{
|
|
OnBlendSampleCopy();
|
|
OnSampleRemoved.ExecuteIfBound(SelectedSampleIndex);
|
|
}
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnBlendSampleCopy()
|
|
{
|
|
typedef TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>> FStringWriter;
|
|
typedef TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>> FStringWriterFactory;
|
|
TSharedRef<FJsonObject> RootJsonObject = MakeShareable(new FJsonObject());
|
|
|
|
const UBlendSpace* BlendSpace = BlendSpaceBase.Get();
|
|
if (!BlendSpace->IsValidBlendSampleIndex(SelectedSampleIndex))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (BlendSpace->IsAsset())
|
|
{
|
|
FBlendSample SampleToCopy = BlendSpace->GetBlendSample(SelectedSampleIndex);
|
|
FJsonObjectConverter::UStructToJsonObject(FBlendSample::StaticStruct(), &SampleToCopy, RootJsonObject, 0 /* CheckFlags */, 0 /* SkipFlags */);
|
|
}
|
|
else
|
|
{
|
|
UBlendSpaceGraph* BlendSpaceGraph = CastChecked<UBlendSpaceGraph>(BlendSpace->GetOuter());
|
|
UAnimGraphNode_BlendSpaceGraphBase* BlendSpaceNode = CastChecked<UAnimGraphNode_BlendSpaceGraphBase>(BlendSpaceGraph->GetOuter());
|
|
UAnimationBlendSpaceSampleGraph* GraphSampleToCopy = CastChecked<UAnimationBlendSpaceSampleGraph>(BlendSpaceNode->GetGraphs()[SelectedSampleIndex]);
|
|
|
|
TSet<UObject*> NodesSet;
|
|
for (TObjectPtr<UEdGraphNode>& Node : GraphSampleToCopy->Nodes)
|
|
{
|
|
NodesSet.Add(Node);
|
|
}
|
|
|
|
FStringOutputDevice NodesString;
|
|
FEdGraphUtilities::ExportNodesToText(NodesSet, NodesString);
|
|
RootJsonObject->SetField(TEXT("Nodes"), MakeShared<FJsonValueString>(NodesString));
|
|
|
|
// Output Animation Pose node is not pasted as it already exists in the pasted graph
|
|
// Take note of the node that connects to it to reconstruct the link (if exists) on paste
|
|
UEdGraphPin* ResultNodePosePin = GraphSampleToCopy->ResultNode->FindPinChecked(TEXT("Result"), EEdGraphPinDirection::EGPD_Input);
|
|
if (ResultNodePosePin->LinkedTo.Num() == 1)
|
|
{
|
|
UEdGraphPin* ConnectedNodePosePin = ResultNodePosePin->LinkedTo[0];
|
|
UEdGraphNode* OwningNode = ConnectedNodePosePin->GetOwningNode();
|
|
RootJsonObject->SetField(TEXT("NodeConnectedToResult"), MakeShared<FJsonValueString>(OwningNode->NodeGuid.ToString()));
|
|
}
|
|
}
|
|
|
|
FString SerializedStr;
|
|
TSharedRef<FStringWriter> Writer = FStringWriterFactory::Create(&SerializedStr);
|
|
FJsonSerializer::Serialize(RootJsonObject, Writer);
|
|
SerializedStr = *FString::Printf(TEXT("%s%s"), BlendSpace->IsAsset() ? *BlendSampleClipboardHeaderAsset : *BlendSampleClipboardHeaderGraph, *SerializedStr);
|
|
FPlatformApplicationMisc::ClipboardCopy(*SerializedStr);
|
|
}
|
|
|
|
void SBlendSpaceGridWidget::OnBlendSamplePaste()
|
|
{
|
|
FString PastedText;
|
|
FPlatformApplicationMisc::ClipboardPaste(PastedText);
|
|
|
|
check(OnSampleAdded.IsBound());
|
|
|
|
const FVector SampleValue = ScreenPositionToSampleValue(LocalMousePosition, true); // Paste the sample at the cursor's location
|
|
int32 NewSampleIndex = INDEX_NONE;
|
|
|
|
if (BlendSpaceBase.Get()->IsAsset())
|
|
{
|
|
check(PastedText.StartsWith(BlendSampleClipboardHeaderAsset));
|
|
PastedText.RightChopInline(BlendSampleClipboardHeaderAsset.Len());
|
|
|
|
TSharedPtr<FJsonObject> RootJsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(PastedText);
|
|
if (!FJsonSerializer::Deserialize(Reader, RootJsonObject))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FBlendSample SampleToPaste;
|
|
if (FJsonObjectConverter::JsonObjectToUStruct(RootJsonObject.ToSharedRef(), FBlendSample::StaticStruct(), &SampleToPaste, 0 /* CheckFlags */, 0 /* SkipFlags */))
|
|
{
|
|
const FScopedTransaction Transaction(LOCTEXT("PasteBlendSpaceSample", "Paste Blend Space Sample"));
|
|
NewSampleIndex = OnSampleAdded.Execute(SampleToPaste.Animation, SampleValue, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
check(PastedText.StartsWith(BlendSampleClipboardHeaderGraph));
|
|
PastedText.RightChopInline(BlendSampleClipboardHeaderGraph.Len());
|
|
|
|
TSharedPtr<FJsonObject> RootJsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(PastedText);
|
|
if (!FJsonSerializer::Deserialize(Reader, RootJsonObject))
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(RootJsonObject->HasField(TEXT("Nodes")));
|
|
|
|
const FScopedTransaction Transaction(LOCTEXT("PasteBlendSpaceSample", "Paste Blend Space Sample"));
|
|
|
|
UBlendSpaceGraph* BlendSpaceGraph = CastChecked<UBlendSpaceGraph>(BlendSpaceBase.Get()->GetOuter());
|
|
UAnimGraphNode_BlendSpaceGraphBase* BlendSpaceNode = CastChecked<UAnimGraphNode_BlendSpaceGraphBase>(BlendSpaceGraph->GetOuter());
|
|
UAnimationBlendSpaceSampleGraph* DestinationGraph = nullptr;
|
|
NewSampleIndex = OnSampleAdded.Execute(nullptr, SampleValue, false);
|
|
DestinationGraph = CastChecked<UAnimationBlendSpaceSampleGraph>(BlendSpaceNode->GetGraphs()[NewSampleIndex]);
|
|
|
|
FString NodesSetString = RootJsonObject->GetStringField(TEXT("Nodes"));
|
|
TSet<UEdGraphNode*> ImportedNodes;
|
|
FEdGraphUtilities::ImportNodesFromText(DestinationGraph, NodesSetString, ImportedNodes);
|
|
|
|
// Reconstruct link to output, if exists
|
|
if (RootJsonObject->HasField(TEXT("NodeConnectedToResult")))
|
|
{
|
|
FString NodeConnectedToResult = RootJsonObject->GetStringField(TEXT("NodeConnectedToResult"));
|
|
FGuid ResultNodeGuid(NodeConnectedToResult);
|
|
|
|
UEdGraphPin* ResultNodePosePin = DestinationGraph->ResultNode->FindPinChecked(TEXT("Result"), EEdGraphPinDirection::EGPD_Input);
|
|
check(ResultNodePosePin->LinkedTo.Num() == 0);
|
|
for (UEdGraphNode* Node : ImportedNodes)
|
|
{
|
|
if (Node->NodeGuid == ResultNodeGuid)
|
|
{
|
|
UEdGraphPin* PosePin = Node->FindPinChecked(TEXT("Pose"), EEdGraphPinDirection::EGPD_Output);
|
|
check(PosePin->LinkedTo.Num() == 0);
|
|
|
|
PosePin->MakeLinkTo(ResultNodePosePin);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (UEdGraphNode* ImportedNode : ImportedNodes)
|
|
{
|
|
ImportedNode->CreateNewGuid();
|
|
}
|
|
}
|
|
|
|
SelectedSampleIndex = NewSampleIndex;
|
|
HighlightedSampleIndex = INDEX_NONE;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE // "SAnimationBlendSpaceGridWidget"
|