Last active
October 26, 2023 10:30
-
-
Save johnfredcee/5f10ca57e30003e6217e0eab979c6197 to your computer and use it in GitHub Desktop.
Slate Polyline Editor Control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) John Connors 2023 | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : | |
// | |
// The above copyright notice and this permission notice shall be included in all copies | |
// or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED “AS IS”, | |
// WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
#include "SPolylineEditor.h" | |
#include "SlateOptMacros.h" | |
#include "GeometryScript/ShapeFunctions.h" | |
#include <Brushes/SlateColorBrush.h> | |
const FVector2f SPolylineEditor::PointRadius = FVector2f(8.0f, 8.0f); | |
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION | |
void SPolylineEditor::Construct(const FArguments& InArgs) | |
{ | |
Brush = FInvalidatableBrushAttribute(InArgs._Brush); | |
Points = InArgs._Points; | |
LineThickness = InArgs._LineThickness, | |
LineScale = InArgs._LineScale, | |
bClosedLine = InArgs._bClosedLine; | |
bMirror = InArgs._bMirror; | |
ColorAndOpacity = FLinearColor::White; | |
OnPointAdded = InArgs._OnPointAdded; | |
OnPointRemoved = InArgs._OnPointRemoved; | |
OnPointMoved = InArgs._OnPointMoved; | |
PointBeingDragged = INDEX_NONE; | |
} | |
bool SPolylineEditor::IsMirrorVertex(int Index) const | |
{ | |
bool bResult = false; | |
if (bMirror) | |
{ | |
bResult = (Index >= Points.Num()); | |
if ((bClosedLine) && (Index >= Points.Num() * 2)) | |
{ | |
bResult = false; | |
} | |
} | |
return bResult; | |
} | |
void SPolylineEditor::ComputeAllPoints(TArray<FVector2f>& AllPoints, bool bIncludeMirrorPoints) const | |
{ | |
AllPoints.Reserve(Points.Num() + bClosedLine ? 1 : 0); | |
for (auto& Point : Points) | |
{ | |
AllPoints.AddZeroed(); | |
FVector2f& LinePoint = AllPoints.Last(); | |
LinePoint = Point; | |
} | |
if ((bMirror) && (bIncludeMirrorPoints)) | |
{ | |
auto PointIt = Points.CreateConstIterator(); | |
auto PointBeginIt = Points.CreateConstIterator(); | |
PointIt.SetToEnd(); | |
do { | |
--PointIt; | |
AllPoints.AddZeroed(); | |
FVector2f& LinePoint = AllPoints.Last(); | |
LinePoint = *PointIt; | |
LinePoint.X = -LinePoint.X; | |
} while (PointIt != PointBeginIt); | |
} | |
if (bClosedLine) | |
{ | |
AllPoints.AddZeroed(); | |
AllPoints.Last() = AllPoints[0]; | |
} | |
} | |
void SPolylineEditor::ComputeLinePoints(TArray<FVector2f>& LinePoints, const FGeometry& InGeometry, bool bIncludeMirrorPoints) const | |
{ | |
const FVector2f Pos = FVector2f::ZeroVector; | |
const FVector2f Size = InGeometry.GetLocalSize(); | |
const FVector2f Center = Pos + 0.5f * Size; | |
LinePoints.Reserve(Points.Num() + bClosedLine ? 1 : 0); | |
FVector2f Scale{ | |
Size.X / (LineScale.X * 2.0f), Size.Y / (LineScale.Y * 2.0f) | |
}; | |
for (auto& Point : Points) | |
{ | |
LinePoints.AddZeroed(); | |
FVector2f& LinePoint = LinePoints.Last(); | |
LinePoint = Point * Scale + Center; | |
} | |
if ((bMirror) && (bIncludeMirrorPoints)) | |
{ | |
auto PointIt = Points.CreateConstIterator(); | |
auto PointBeginIt = Points.CreateConstIterator(); | |
PointIt.SetToEnd(); | |
do | |
{ | |
--PointIt; | |
LinePoints.AddZeroed(); | |
FVector2f& LinePoint = LinePoints.Last(); | |
LinePoint = *PointIt; | |
LinePoint.X = -LinePoint.X; | |
LinePoint = LinePoint * Scale + Center; | |
} | |
while (PointIt != PointBeginIt); | |
} | |
if (bClosedLine) | |
{ | |
LinePoints.AddZeroed(); | |
LinePoints.Last() = LinePoints[0]; | |
} | |
} | |
FVector2f SPolylineEditor::MakePoint(const FGeometry& InGeometry, const FVector2f& InPoint) | |
{ | |
const FVector2f Pos = FVector2f::ZeroVector; | |
const FVector2f Size = InGeometry.GetLocalSize(); | |
const FVector2f Center = Pos + 0.5f * Size; | |
FVector2f InverseScale{ | |
(LineScale.X * 2.0f) / Size.X , (LineScale.Y * 2.0f) / Size.Y | |
}; | |
return (InPoint - Center) * InverseScale; | |
} | |
FVector2D SPolylineEditor::ComputeDesiredSize(float LayoutScaleMultiplier) const | |
{ | |
return FVector2D(LineScale.X * 100.0, LineScale.Y * 100.0); | |
} | |
int32 SPolylineEditor::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const | |
{ | |
const FVector2f Pos = FVector2f::ZeroVector; | |
const FVector2f Size = AllottedGeometry.GetLocalSize(); | |
const FVector2f Center = Pos + 0.5f * Size; | |
//const float Radius = FMath::Min(Size.X, Size.Y) * 0.5f; | |
FSlateColorBrush WhiteBox = FSlateColorBrush(FColor::White); | |
const FSlateBrush* SlateBrush = Brush.GetImage().Get(); | |
FLinearColor LinearColor = GetColorAndOpacity() * InWidgetStyle.GetColorAndOpacityTint() * SlateBrush->GetTint(InWidgetStyle); | |
FColor FinalColorAndOpacity = LinearColor.ToFColor(true); | |
//SLeafWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled && IsEnabled()); | |
// Fill the background of the grid | |
const FSlateBrush* BackgroundImage = FAppStyle::GetBrush(TEXT("Graph.Panel.SolidBackground")); | |
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), BackgroundImage); | |
if (Points.Num() >= 2) | |
{ | |
TArray<FVector2f> LinePoints; | |
ComputeLinePoints(LinePoints, AllottedGeometry); | |
const FSlateResourceHandle Handle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*SlateBrush); | |
FSlateDrawElement::MakeLines(OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(), LinePoints, ESlateDrawEffect::NoPixelSnapping, | |
FinalColorAndOpacity, true); | |
const FSlateFontInfo FontInfo = FCoreStyle::GetDefaultFontStyle("Bold", 10); | |
for (int i = 0; i < LinePoints.Num(); i++) | |
{ | |
auto Point = LinePoints[i]; | |
FLinearColor BoxColor = IsMirrorVertex(i) ? FLinearColor(FColor::Orange) : FLinearColor::Green; | |
FSlateDrawElement::MakeBox( | |
OutDrawElements, | |
LayerId + 2, | |
AllottedGeometry.ToPaintGeometry(FVector2D(2.f * PointRadius), FSlateLayoutTransform(Point - PointRadius)), | |
&WhiteBox, | |
// bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect, | |
ESlateDrawEffect::NoPixelSnapping, | |
BoxColor * FLinearColor(1.f, 1.f, 1.f, 0.25f)); | |
FSlateDrawElement::MakeText(OutDrawElements, LayerId + 3, | |
AllottedGeometry.ToPaintGeometry(FVector2D(2.f * PointRadius), FSlateLayoutTransform(Point - PointRadius)), | |
FString::FromInt(i), | |
FontInfo); | |
} | |
} | |
else | |
{ | |
FSlateDrawElement::MakeDebugQuad(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FLinearColor::Blue); | |
} | |
return LayerId; | |
} | |
FReply SPolylineEditor::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) | |
{ | |
if ((PointBeingDragged == INDEX_NONE) && (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)) | |
{ | |
const FVector2f LocalCursorPos = UE::Slate::CastToVector2f(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition())); | |
PointBeingDragged = PointIndexFromCursorPos(MyGeometry, LocalCursorPos, PointRadius.X * PointRadius.X * 2.0f * 2.0f); | |
if (PointBeingDragged != INDEX_NONE) | |
{ | |
return FReply::Handled().CaptureMouse(SharedThis(this)); | |
} | |
} | |
return FReply::Unhandled(); | |
} | |
FReply SPolylineEditor::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) | |
{ | |
if ((PointBeingDragged != INDEX_NONE) && (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)) | |
{ | |
const FVector2f LocalCursorPos = UE::Slate::CastToVector2f(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition())); | |
PointBeingDragged = INDEX_NONE; | |
return FReply::Handled().ReleaseMouseCapture(); | |
} | |
if ((PointBeingDragged == INDEX_NONE) && (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)) | |
{ | |
const FVector2f LocalCursorPos = UE::Slate::CastToVector2f(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition())); | |
int32 PointToDelete = PointIndexFromCursorPos(MyGeometry, LocalCursorPos, PointRadius.X * PointRadius.X * 2.0f * 2.0f); | |
if (PointToDelete != INDEX_NONE) | |
{ | |
Points.RemoveAt(PointToDelete); | |
OnPointMoved.ExecuteIfBound(); | |
} | |
} | |
return FReply::Unhandled(); | |
} | |
FReply SPolylineEditor::OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) | |
{ | |
if (InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) | |
{ | |
const FVector2f LocalCursorPos = UE::Slate::CastToVector2f(InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition())); | |
AddClosestPointAndIndex(InMyGeometry, LocalCursorPos); | |
OnPointAdded.ExecuteIfBound(); | |
return FReply::Handled(); | |
} | |
return FReply::Unhandled(); | |
} | |
FReply SPolylineEditor::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) | |
{ | |
if (PointBeingDragged != INDEX_NONE) | |
{ | |
const FVector2f LocalCursorPos = UE::Slate::CastToVector2f(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition())); | |
Points[PointBeingDragged] = MakePoint(MyGeometry, LocalCursorPos); | |
OnPointMoved.ExecuteIfBound(); | |
return FReply::Handled(); | |
} | |
return FReply::Unhandled(); | |
} | |
int32 SPolylineEditor::PointIndexFromCursorPos(const FGeometry& MyGeometry, const FVector2f LocalCursorPos, const float RadiusSquared) | |
{ | |
TArray<FVector2f> LinePoints; | |
ComputeLinePoints(LinePoints, MyGeometry, false); | |
for (int i = 0; i < LinePoints.Num(); ++i) | |
{ | |
const float SizeSquared = (LinePoints[i] - LocalCursorPos).SizeSquared(); | |
if (SizeSquared < RadiusSquared) | |
{ | |
return i; | |
} | |
} | |
return INDEX_NONE; | |
} | |
void SPolylineEditor::AddClosestPointAndIndex(const FGeometry& MyGeometry, FVector2f LocalCursorPos) | |
{ | |
using URayFuncs = UGeometryScriptLibrary_RayFunctions; | |
auto SignedDistancePointLine2D = [](const FVector2f Point, const FVector2f Start, const FVector2f End) { | |
using FReal = FVector::FReal; | |
auto Cross2D = [](const FVector2f& A, const FVector2f& B) { | |
return A.X * B.Y - A.Y * B.X; | |
}; | |
const FVector2f Seg(End - Start); | |
const FVector2f Dir(Point - Start); | |
const FReal Nom = Cross2D(Seg, Dir); | |
const FReal Den = Seg.SquaredLength(); | |
const FReal Dist = Den > UE_KINDA_SMALL_NUMBER ? (Nom / FMath::Sqrt(Den)) : 0.0; | |
return Dist; | |
}; | |
auto ProjectPointOnSegment2D = [](const FVector2f Point, const FVector2f Start, const FVector2f End) { | |
using FReal = FVector::FReal; | |
const FVector2f Seg(End - Start); | |
const FVector2f Dir(Point - Start); | |
const FReal D = Seg.SquaredLength(); | |
const FReal T = FVector2f::DotProduct(Seg, Dir); | |
if (T < 0.0) | |
{ | |
return 0.0; | |
} | |
else if (T > D) | |
{ | |
return 1.0; | |
} | |
return D > UE_KINDA_SMALL_NUMBER ? (T / D) : 0.0; | |
}; | |
TArray<FVector2f> LinePoints; | |
ComputeLinePoints(LinePoints, MyGeometry, false); | |
int32 NumLinePoints = LinePoints.Num(); | |
int IndexNearest = -1; | |
float ShortestDist = BIG_NUMBER; | |
for (int i = 0; i < NumLinePoints - 1; ++i) | |
{ | |
FVector2f PointA(LinePoints[i]); | |
FVector2f PointB(LinePoints[(i + 1) % NumLinePoints]); | |
float Dist = FMath::Abs(SignedDistancePointLine2D(LocalCursorPos, PointA, PointB)); | |
if ((Dist < ShortestDist) | |
&& (Dist < PointRadius.X * 2.0)) | |
{ | |
ShortestDist = Dist; | |
IndexNearest = i; | |
} | |
} | |
if (IndexNearest == -1) | |
{ | |
Points.Add(MakePoint(MyGeometry, LocalCursorPos)); | |
return; | |
} | |
FVector2f PointA = LinePoints[IndexNearest]; | |
FVector2f PointB = LinePoints[(IndexNearest + 1) % NumLinePoints]; | |
float D = ProjectPointOnSegment2D(LocalCursorPos, PointA, PointB); | |
if ((D > KINDA_SMALL_NUMBER) | |
&& (D < 1.0f - KINDA_SMALL_NUMBER)) | |
{ | |
FVector2f NewLinePoint = (PointB - PointA) * D + PointA; | |
FVector2f NewPoint = MakePoint(MyGeometry, NewLinePoint); | |
Points.Insert(NewPoint, (IndexNearest + 1) % NumLinePoints); | |
} | |
} | |
void SPolylineEditor::SetColorAndOpacity(FLinearColor InColorAndOpacity) | |
{ | |
ColorAndOpacity = InColorAndOpacity; | |
} | |
const FLinearColor& SPolylineEditor::GetColorAndOpacity() const | |
{ | |
return ColorAndOpacity; | |
} | |
void SPolylineEditor::SetBrush(FSlateBrush* InBrush) | |
{ | |
Brush.SetImage(*this, InBrush); | |
} | |
void SPolylineEditor::SetLineScale(FVector2f InScale) | |
{ | |
LineScale = InScale; | |
} | |
void SPolylineEditor::SetLineThickness(float InThickness) | |
{ | |
LineThickness = InThickness; | |
} | |
const TArray<FVector2f>& SPolylineEditor::GetPoints() | |
{ | |
return Points; | |
} | |
void SPolylineEditor::SetPoints(const TArray<FVector2f>& InPoints) | |
{ | |
Points = InPoints; | |
} | |
void SPolylineEditor::SetClosedLine(const bool bInClosedLine) | |
{ | |
bClosedLine = bInClosedLine; | |
} | |
void SPolylineEditor::SetMirrored(bool bInMirror) | |
{ | |
bMirror = bInMirror; | |
} | |
END_SLATE_FUNCTION_BUILD_OPTIMIZATION |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment