Files
UnrealEngine/Engine/Plugins/Experimental/MeshModelingToolsetExp/Source/MeshModelingToolsEditorOnlyExp/Private/DrawSplineTool.cpp
2025-05-18 13:04:45 +08:00

1197 lines
37 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DrawSplineTool.h"
#include "ActorFactories/ActorFactoryEmptyActor.h"
#include "AssetSelection.h" // FActorFactoryAssetProxy
#include "BaseBehaviors/SingleClickOrDragBehavior.h"
#include "BaseGizmos/GizmoMath.h"
#include "Components/SplineComponent.h"
#include "CoreGlobals.h" // GUndo
#include "Drawing/PreviewGeometryActor.h"
#include "Editor/UnrealEdEngine.h" // DuplicateActors
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "ILevelEditor.h"
#include "InputState.h" // FInputDeviceRay
#include "InteractiveToolManager.h"
#include "ISceneOutliner.h"
#include "LevelEditor.h"
#include "Modules/ModuleManager.h"
#include "Kismet2/ComponentEditorUtils.h" // GenerateValidVariableName
#include "Mechanics/ConstructionPlaneMechanic.h"
#include "PrimitiveDrawingUtils.h" // FPrimitiveDrawInterface
#include "Selection/ToolSelectionUtil.h"
#include "Settings/LevelEditorViewportSettings.h"
#include "SplineUtil.h"
#include "ToolBuilderUtil.h"
#include "UnrealEdGlobals.h" // GUnrealEd
#define LOCTEXT_NAMESPACE "UDrawSplineTool"
using namespace UE::Geometry;
/**
* Helper to hide actors from the outliner if it's not an actor that
* we defined to be automatically hidden (e.g. APreviewGeometryActor),
* that works by being friended to FSetActorHiddenInSceneOutliner.
* This is a temporary measure until we have a cleaner way to hide
* ourselves from the outliner through TEDS.
* Note that just creating this class doesn't actually refresh the outliner
* unless you happen to take an action that does (such as reparenting things),
* so you would need to call RefreshOutliner().
*/
class FModelingToolsSetActorHiddenInSceneOutliner
{
public:
FModelingToolsSetActorHiddenInSceneOutliner(AActor* InActor, bool bHidden)
{
FSetActorHiddenInSceneOutliner Setter(InActor, bHidden);
}
/**
* Does a full refresh of the outliner. Note that this can be comparatively
* slow, so it should happen rarely.
*/
void RefreshOutliner()
{
FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
if (!LevelEditorModule)
{
return;
}
TSharedPtr<ILevelEditor> LevelEditor = LevelEditorModule->GetLevelEditorInstance().Pin();
if (!LevelEditor.IsValid())
{
return;
}
for (TWeakPtr<ISceneOutliner> OutlinerWeak : LevelEditor->GetAllSceneOutliners())
{
if (TSharedPtr<ISceneOutliner> Outliner = OutlinerWeak.Pin())
{
Outliner->FullRefresh();
}
}
}
};
namespace DrawSplineToolLocals
{
FText AddPointTransactionName = LOCTEXT("AddPointTransactionName", "Add Point");
USplineComponent* CreateNewSplineInActor(AActor* Actor, bool bTransact = false, bool bSetAsRoot = false)
{
if (!ensure(Actor))
{
return nullptr;
}
if (bTransact)
{
Actor->Modify();
}
FName NewComponentName = *FComponentEditorUtils::GenerateValidVariableName(USplineComponent::StaticClass(), Actor);
// Note that the RF_Transactional is important here for the spline to undo/redo properly in the future
USplineComponent* Spline = NewObject<USplineComponent>(Actor, USplineComponent::StaticClass(), NewComponentName,
bTransact ? RF_Transactional : RF_NoFlags);
if (bSetAsRoot)
{
Actor->SetRootComponent(Spline);
}
else
{
Spline->SetupAttachment(Actor->GetRootComponent());
}
Spline->OnComponentCreated();
Actor->AddInstanceComponent(Spline);
Spline->RegisterComponent();
Spline->ResetRelativeTransform();
Actor->PostEditChange();
return Spline;
};
USplineComponent* GetOrCreateTargetSpline(AActor* Actor, int32 TargetIndex, bool bTransact = false)
{
if (TargetIndex >= 0)
{
TInlineComponentArray<USplineComponent*> SplineComponents;
Actor->GetComponents<USplineComponent>(SplineComponents);
if (TargetIndex < SplineComponents.Num())
{
return SplineComponents[TargetIndex];
}
}
// If we got to here, we didn't have an existing target at that index. Create one.
return CreateNewSplineInActor(Actor, bTransact);
}
void CopySplineToSpline(const USplineComponent& Source, USplineComponent& Destination, bool bTransact = false)
{
if (bTransact)
{
Destination.Modify();
}
Destination.ClearSplinePoints();
Destination.bSplineHasBeenEdited = true;
// We iterate here (rather than just copying over the SplineCurves data) so that we can transform
// the data properly into the coordinate space of the target component.
int32 NumSplinePoints = Source.GetNumberOfSplinePoints();
for (int32 i = 0; i < NumSplinePoints; ++i)
{
Destination.AddSplinePoint(Source.GetLocationAtSplinePoint(i, ESplineCoordinateSpace::World),
ESplineCoordinateSpace::World, false);
Destination.SetUpVectorAtSplinePoint(i, Source.GetUpVectorAtSplinePoint(i, ESplineCoordinateSpace::World),
ESplineCoordinateSpace::World, false);
Destination.SetTangentsAtSplinePoint(i,
Source.GetArriveTangentAtSplinePoint(i, ESplineCoordinateSpace::World),
Source.GetLeaveTangentAtSplinePoint(i, ESplineCoordinateSpace::World),
ESplineCoordinateSpace::World, false);
Destination.SetSplinePointType(i, Source.GetSplinePointType(i), false);
}
Destination.SetClosedLoop(Source.IsClosedLoop());
Destination.UpdateSpline();
};
// Gives the scale used for tangent visualization (and which therefore needs to be used in raycasting the handles)
float GetNonzeroTangentScale()
{
// We'll use the setting that gets used when editing splines in the viewport, but if it is set to 0 (because
// the user presumably doesn't want to see tangent handles in viewport), we'll ignore it and use the default
// value of 0.5, because we need to be able to draw a tangent for our "tangent drag" approach.
// The abs here isn't actually necessary since the setting is clamped in editor, but it looks more correct.
float Scale = FMath::Abs(GetDefault<ULevelEditorViewportSettings>()->SplineTangentScale);
return Scale > 0.0f ? Scale : 0.5f;
}
// Might be useful to have in SplineUtil, but uncertain what the API should be (should it be part of
// DrawSpline? Should there be options for selection color?). Also potentially messier to match the tangent
// scale with the UI interaction..
void DrawTangent(const USplineComponent& SplineComp, int32 PointIndex, IToolsContextRenderAPI& RenderAPI)
{
if (!ensure(PointIndex >= 0 && PointIndex < SplineComp.GetNumberOfSplinePoints()))
{
return;
}
FPrimitiveDrawInterface* PDI = RenderAPI.GetPrimitiveDrawInterface();
const float TangentScale = GetNonzeroTangentScale();
const float TangentHandleSize = 8.0f + GetDefault<ULevelEditorViewportSettings>()->SplineTangentHandleSizeAdjustment;
const FVector Location = SplineComp.GetLocationAtSplinePoint(PointIndex, ESplineCoordinateSpace::World);
const FVector LeaveTangent = SplineComp.GetLeaveTangentAtSplinePoint(PointIndex, ESplineCoordinateSpace::World) * TangentScale;
const FVector ArriveTangent = SplineComp.bAllowDiscontinuousSpline ?
SplineComp.GetArriveTangentAtSplinePoint(PointIndex, ESplineCoordinateSpace::World) * TangentScale : LeaveTangent;
FColor Color = FColor::White;
PDI->DrawLine(Location, Location - ArriveTangent, Color, SDPG_Foreground);
PDI->DrawLine(Location, Location + LeaveTangent, Color, SDPG_Foreground);
PDI->DrawPoint(Location + LeaveTangent, Color, TangentHandleSize, SDPG_Foreground);
PDI->DrawPoint(Location - ArriveTangent, Color, TangentHandleSize, SDPG_Foreground);
}
// Undoes a point addition with an auto tangent
class FSimplePointInsertionChange : public UDrawSplineTool::FSplineChange
{
public:
FSimplePointInsertionChange(const FVector3d& HitLocationIn, const FVector3d& UpVectorIn)
: HitLocation(HitLocationIn)
, UpVector(UpVectorIn)
{
}
virtual void Apply(USplineComponent& Spline) override
{
Spline.AddSplinePoint(HitLocation, ESplineCoordinateSpace::World, false);
int32 PointIndex = Spline.GetNumberOfSplinePoints() - 1;
Spline.SetUpVectorAtSplinePoint(PointIndex, UpVector, ESplineCoordinateSpace::World, true);
}
virtual void Revert(USplineComponent& Spline) override
{
if (ensure(Spline.GetNumberOfSplinePoints() > 0))
{
Spline.RemoveSplinePoint(Spline.GetNumberOfSplinePoints() - 1, true);
}
}
virtual FString ToString() const override
{
return TEXT("FSimplePointInsertionChange");
}
protected:
FVector3d HitLocation;
FVector3d UpVector;
};
// Undoes a point addition with an explicit tangent
class FTangentPointInsertionChange : public UDrawSplineTool::FSplineChange
{
public:
FTangentPointInsertionChange(const FVector3d& HitLocationIn, const FVector3d& UpVectorIn, const FVector3d& TangentIn)
: HitLocation(HitLocationIn)
, UpVector(UpVectorIn)
, Tangent(TangentIn)
{
}
virtual void Apply(USplineComponent& Spline) override
{
Spline.AddSplinePoint(HitLocation, ESplineCoordinateSpace::World, false);
int32 PointIndex = Spline.GetNumberOfSplinePoints() - 1;
Spline.SetUpVectorAtSplinePoint(PointIndex, UpVector, ESplineCoordinateSpace::World, false);
Spline.SetTangentAtSplinePoint(PointIndex, Tangent, ESplineCoordinateSpace::World, true);
}
virtual void Revert(USplineComponent& Spline) override
{
if (ensure(Spline.GetNumberOfSplinePoints() > 0))
{
Spline.RemoveSplinePoint(Spline.GetNumberOfSplinePoints() - 1, true);
}
}
virtual FString ToString() const override
{
return TEXT("FTangentPointInsertionChange");
}
protected:
FVector3d HitLocation;
FVector3d UpVector;
FVector3d Tangent;
};
// Undoes a free draw stroke (multiple points at once)
class FStrokeInsertionChange : public UDrawSplineTool::FSplineChange
{
public:
FStrokeInsertionChange(const TArray<FVector3d>& HitLocationsIn, const TArray<FVector3d>& UpVectorsIn)
: HitLocations(HitLocationsIn)
, UpVectors(UpVectorsIn)
{
if (!ensure(HitLocations.Num() == UpVectors.Num()))
{
int32 Num = FMath::Min(HitLocations.Num(), UpVectors.Num());
HitLocations.SetNum(Num);
UpVectors.SetNum(Num);
}
}
virtual void Apply(USplineComponent& Spline) override
{
for (int32 i = 0; i < HitLocations.Num(); ++i)
{
Spline.AddSplinePoint(HitLocations[i], ESplineCoordinateSpace::World, false);
int32 PointIndex = Spline.GetNumberOfSplinePoints() - 1;
Spline.SetUpVectorAtSplinePoint(PointIndex, UpVectors[i], ESplineCoordinateSpace::World, false);
}
Spline.UpdateSpline();
}
virtual void Revert(USplineComponent& Spline) override
{
for (int32 i = 0; i < HitLocations.Num(); ++i)
{
if (!ensure(Spline.GetNumberOfSplinePoints() > 0))
{
break;
}
Spline.RemoveSplinePoint(Spline.GetNumberOfSplinePoints() - 1, false);
}
Spline.UpdateSpline();
}
virtual FString ToString() const override
{
return TEXT("FStrokeInsertionChange");
}
protected:
TArray<FVector3d> HitLocations;
TArray<FVector3d> UpVectors;
};
}
void UDrawSplineTool::Setup()
{
UInteractiveTool::Setup();
Settings = NewObject<UDrawSplineToolProperties>(this);
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
Settings->TargetActor = StartupSelectedActor;
SetToolDisplayName(LOCTEXT("DrawSplineToolName", "Draw Spline"));
GetToolManager()->DisplayMessage(
LOCTEXT("DrawSplineToolDescription", "Draw a spline to replace an existing one or add it to an actor."),
EToolMessageLevel::UserNotification);
PlaneMechanic = NewObject<UConstructionPlaneMechanic>(this);
PlaneMechanic->Setup(this);
PlaneMechanic->Initialize(GetTargetWorld(), FFrame3d(FVector3d::Zero(), FVector3d::UnitX()));
PlaneMechanic->bShowGrid = Settings->bHitCustomPlane;
PlaneMechanic->CanUpdatePlaneFunc = [this] { return Settings->bHitCustomPlane; };
Settings->WatchProperty(Settings->bHitCustomPlane, [this](bool) {
PlaneMechanic->bShowGrid = Settings->bHitCustomPlane;
});
ClickOrDragBehavior = NewObject<USingleClickOrDragInputBehavior>();
ClickOrDragBehavior->Initialize(this, this);
AddInputBehavior(ClickOrDragBehavior);
// Make sure the plane mechanic captures clicks first, to ensure it sees ctrl+clicks to reposition the plane
PlaneMechanic->UpdateClickPriority(ClickOrDragBehavior->GetPriority().MakeHigher());
Settings->WatchProperty(Settings->bLoop, [this](bool) {
if (ensure(WorkingSpline.IsValid()))
{
WorkingSpline->SetClosedLoop(Settings->bLoop);
bNeedToRerunConstructionScript = true;
}
});
TransitionOutputMode();
Settings->WatchProperty(Settings->OutputMode, [this](EDrawSplineOutputMode) {
TransitionOutputMode();
});
TargetActorWatcherID = Settings->WatchProperty(Settings->TargetActor, [this](TWeakObjectPtr<AActor>) {
// It's possible for the user to use the actor picker to click on our preview, which we don't
// want to be pickable via the actor picker... There doesn't currently seem to be a way prevent
// that, so for now we'll just catch this case and keep whatever the previous value was.
if (Settings->TargetActor == PreviewActor)
{
Settings->TargetActor = IsValid(PreviousTargetActor) ? PreviousTargetActor : nullptr;
Settings->SilentUpdateWatcherAtIndex(TargetActorWatcherID);
}
if (PreviousTargetActor != Settings->TargetActor)
{
// Don't set PreviousTargetActor here because it needs to be made visible, etc inside
// TransitionOutputMode
TransitionOutputMode();
}
});
Settings->WatchProperty(Settings->ExistingSplineIndexToReplace, [this](int32) {
TransitionOutputMode();
});
Settings->WatchProperty(Settings->BlueprintToCreate, [this](TWeakObjectPtr<UBlueprint>) {
TransitionOutputMode();
});
Settings->WatchProperty(Settings->bPreviewUsingActorCopy, [this](bool) {
TransitionOutputMode();
});
Settings->SilentUpdateWatched();
}
// Set things up for a new output mode or destination
void UDrawSplineTool::TransitionOutputMode()
{
using namespace DrawSplineToolLocals;
// Setting up the previews seems to be the most error prone part of the tool because editor duplicating, hiding
// from outliner, and avoiding emitting undo/redo transactions seems to be quite finnicky...
// This function is sometimes called from inside transactions (such as tool start, or dragging the "component to replace"
// slider). Several calls here would transact in that case (for instance, the Destroy() calls on the previews seem
// to do it), which we generally don't want to do. So we disable transacting in this function with the hack below.
// Note that we still have to take care that any editor functions we call don't open their own transactions...
ITransaction* UndoState = GUndo;
GUndo = nullptr; // Pretend we're not in a transaction
ON_SCOPE_EXIT{ GUndo = UndoState; }; // Revert later
// Restore the visibility of the previous target actor and spline, if needed
if (PreviousTargetActor)
{
PreviousTargetActor->GetRootComponent()->SetVisibility(true, true);
PreviousTargetActor = nullptr;
}
if (HiddenSpline.IsValid())
{
HiddenSpline->bDrawDebug = bPreviousSplineVisibility;
HiddenSpline = nullptr;
}
// Keep the previous spline/preview temporarily so we can transfer over spline data
// when we make new previews
AActor* PreviousPreview = PreviewActor;
USplineComponent* PreviousSpline = WorkingSpline.Get();
PreviewActor = nullptr;
WorkingSpline = nullptr;
// Used for visualizing the effect of a spline on some special actor
auto CreateDuplicatePreviewActor = [this](AActor* Actor)
{
TArray<AActor*> NewActors;
GUnrealEd->DuplicateActors({ Actor }, NewActors, GetWorld()->GetCurrentLevel(), FVector3d::Zero());
if (!ensure(NewActors.Num() > 0))
{
return false;
}
PreviewActor = NewActors[0];
PreviewActor->ClearFlags(RF_Transactional);
PreviewActor->SetFlags(RF_Transient);
// Make the original invisible
PreviousTargetActor = Actor;
PreviousTargetActor->GetRootComponent()->SetVisibility(false, true);
// Note: unfortunately this won't hide the spline itself as long as the object is selected because that
// drawing goes through a different path that doesn't seem to be disableable (unless we deselected
// the actor).
// Hide this preview from the outliner
FModelingToolsSetActorHiddenInSceneOutliner Hider(PreviewActor, true);
Hider.RefreshOutliner();
return true;
};
auto FallbackSplinePlacement = [this]()
{
FRotator Rotation(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnInfo;
SpawnInfo.ObjectFlags = RF_Transient;
PreviewActor = GetTargetWorld()->SpawnActor<APreviewGeometryActor>(FVector::ZeroVector, Rotation, SpawnInfo);
USceneComponent* RootComponent = NewObject<USceneComponent>(PreviewActor);
PreviewActor->AddOwnedComponent(RootComponent);
PreviewActor->SetRootComponent(RootComponent);
RootComponent->RegisterComponent();
WorkingSpline = CreateNewSplineInActor(PreviewActor);
};
// Set up the new preview
if (!Settings->bPreviewUsingActorCopy)
{
FallbackSplinePlacement();
}
else
{
switch (Settings->OutputMode)
{
case EDrawSplineOutputMode::EmptyActor:
FallbackSplinePlacement();
break;
case EDrawSplineOutputMode::ExistingActor:
{
if (!Settings->TargetActor.IsValid()
|| !CreateDuplicatePreviewActor(Settings->TargetActor.Get()))
{
FallbackSplinePlacement();
break;
}
WorkingSpline = GetOrCreateTargetSpline(PreviewActor, Settings->ExistingSplineIndexToReplace);
bNeedToRerunConstructionScript = true;
break;
}
case EDrawSplineOutputMode::CreateBlueprint:
{
bool bCanCreateActor = Settings->BlueprintToCreate.IsValid()
&& Settings->BlueprintToCreate->GeneratedClass != nullptr
&& !Settings->BlueprintToCreate->GeneratedClass->HasAnyClassFlags(CLASS_NotPlaceable | CLASS_Abstract);
if (!bCanCreateActor)
{
FallbackSplinePlacement();
break;
}
// Instantiate the blueprint
PreviewActor = FActorFactoryAssetProxy::AddActorForAsset(
Settings->BlueprintToCreate.Get(),
/*bSelectActors =*/ false,
// Important that we don't use the default (RF_Transactional) here, or else we'll end up
// issuing an undo transaction in this call.
EObjectFlags::RF_Transient);
if (!PreviewActor)
{
FallbackSplinePlacement();
break;
}
// Hide this preview from outliner
FModelingToolsSetActorHiddenInSceneOutliner Hider(PreviewActor, true);
Hider.RefreshOutliner();
WorkingSpline = GetOrCreateTargetSpline(PreviewActor, Settings->ExistingSplineIndexToReplace);
bNeedToRerunConstructionScript = true;
break;
}
default:
ensure(false);
}
}
if (ensure(WorkingSpline.IsValid()))
{
if (PreviousSpline)
{
CopySplineToSpline(*PreviousSpline, *WorkingSpline);
}
else
{
WorkingSpline->ClearSplinePoints();
}
WorkingSpline->SetClosedLoop(Settings->bLoop);
// This has to be set so that construction script reruns transfer over current spline state.
WorkingSpline->bSplineHasBeenEdited = true;
// Get the index of the spline in the components array for recapturing on construction script reruns.
if (PreviewActor)
{
TInlineComponentArray<USplineComponent*> SplineComponents;
PreviewActor->GetComponents<USplineComponent>(SplineComponents);
SplineRecaptureIndex = SplineComponents.IndexOfByKey(WorkingSpline.Get());
ensure(SplineRecaptureIndex >= 0);
}
}
// Now that we've copied over previous preview data, destroy the old previews
if (PreviousPreview)
{
PreviousPreview->Destroy();
}
}
void UDrawSplineTool::Shutdown(EToolShutdownType ShutdownType)
{
using namespace DrawSplineToolLocals;
LongTransactions.CloseAll(GetToolManager());
Settings->SaveProperties(this);
if (PreviousTargetActor)
{
PreviousTargetActor->GetRootComponent()->SetVisibility(true, true);
PreviousTargetActor = nullptr;
}
if (HiddenSpline.IsValid())
{
HiddenSpline->bDrawDebug = bPreviousSplineVisibility;
HiddenSpline = nullptr;
}
if (ShutdownType == EToolShutdownType::Accept && WorkingSpline.IsValid() && WorkingSpline->GetNumberOfSplinePoints() > 0)
{
GenerateAsset();
}
PlaneMechanic->Shutdown();
if (WorkingSpline.IsValid())
{
WorkingSpline->DestroyComponent();
}
if (PreviewActor)
{
PreviewActor->Destroy();
}
Super::Shutdown(ShutdownType);
}
void UDrawSplineTool::GenerateAsset()
{
using namespace DrawSplineToolLocals;
USplineComponent* OutputSpline = nullptr;
auto CreateSplineInEmptyActor = [this, &OutputSpline]()
{
// Get centroid of spline
int32 NumSplinePoints = WorkingSpline->GetNumberOfSplinePoints();
FVector3d Center = FVector3d::Zero();
for (int32 i = 0; i < NumSplinePoints; ++i)
{
Center += WorkingSpline->GetLocationAtSplinePoint(i, ESplineCoordinateSpace::World);
}
Center /= NumSplinePoints;
// Spawning via a factory is editor-only
UActorFactoryEmptyActor* EmptyActorFactory = NewObject<UActorFactoryEmptyActor>();
FAssetData AssetData(EmptyActorFactory->GetDefaultActorClass(FAssetData()));
FActorSpawnParameters SpawnParams;
SpawnParams.Name = TEXT("Spline");
SpawnParams.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
AActor* NewActor = EmptyActorFactory->CreateActor(AssetData.GetAsset(),
TargetWorld->GetCurrentLevel(),
FTransform(Center),
SpawnParams);
// This is also editor-only: it's the label that shows up in the hierarchy
FActorLabelUtilities::SetActorLabelUnique(NewActor, TEXT("SplineActor"));
UActorComponent* OldRoot = NewActor->GetRootComponent();
OutputSpline = CreateNewSplineInActor(NewActor, true, true);
OutputSpline->SetWorldTransform(FTransform(Center));
OldRoot->DestroyComponent();
CopySplineToSpline(*WorkingSpline, *OutputSpline, true);
};
GetToolManager()->BeginUndoTransaction(LOCTEXT("DrawSplineTransactionName", "Draw Spline"));
switch (Settings->OutputMode)
{
case EDrawSplineOutputMode::EmptyActor:
CreateSplineInEmptyActor();
break;
case EDrawSplineOutputMode::ExistingActor:
{
if (!Settings->TargetActor.IsValid())
{
CreateSplineInEmptyActor();
break;
}
OutputSpline = GetOrCreateTargetSpline(Settings->TargetActor.Get(), Settings->ExistingSplineIndexToReplace, true);
CopySplineToSpline(*WorkingSpline, *OutputSpline, true);
Settings->TargetActor->RerunConstructionScripts();
break;
}
case EDrawSplineOutputMode::CreateBlueprint:
{
bool bCanCreateActor = Settings->BlueprintToCreate.IsValid()// != nullptr
&& Settings->BlueprintToCreate->GeneratedClass != nullptr
&& !Settings->BlueprintToCreate->GeneratedClass->HasAnyClassFlags(CLASS_NotPlaceable | CLASS_Abstract);
if (!bCanCreateActor)
{
CreateSplineInEmptyActor();
break;
}
// Instantiate the blueprint
AActor* NewActor = FActorFactoryAssetProxy::AddActorForAsset(
Settings->BlueprintToCreate.Get(),
/*bSelectActors =*/ false);
if (!NewActor)
{
CreateSplineInEmptyActor();
break;
}
OutputSpline = GetOrCreateTargetSpline(NewActor, Settings->ExistingSplineIndexToReplace, true);
CopySplineToSpline(*WorkingSpline, *OutputSpline, true);
NewActor->RerunConstructionScripts();
break;
}
default:
ensure(false);
}
// TODO: Someday when we support component selection, we should select OutputSpline directly.
ToolSelectionUtil::SetNewActorSelection(GetToolManager(), OutputSpline->GetAttachmentRootActor());
GetToolManager()->EndUndoTransaction();
}
// Helper to add a point given a hit location and hit normal
void UDrawSplineTool::AddSplinePoint(const FVector3d& HitLocation, const FVector3d& HitNormal)
{
using namespace DrawSplineToolLocals;
if (!WorkingSpline.IsValid())
{
return;
}
int32 NumSplinePoints = WorkingSpline->GetNumberOfSplinePoints();
FVector3d UpVectorToUse = GetUpVectorToUse(HitLocation, HitNormal, NumSplinePoints);
WorkingSpline->AddSplinePoint(HitLocation, ESplineCoordinateSpace::World,
/*bUpdate =*/ false);
WorkingSpline->SetUpVectorAtSplinePoint(NumSplinePoints, UpVectorToUse, ESplineCoordinateSpace::World,
/*bUpdate =*/ true);
}
FVector3d UDrawSplineTool::GetUpVectorToUse(const FVector3d& HitLocation, const FVector3d& HitNormal, int32 NumSplinePointsBeforehand)
{
FVector3d UpVectorToUse = HitNormal;
switch (Settings->UpVectorMode)
{
case EDrawSplineUpVectorMode::AlignToPrevious:
{
if (NumSplinePointsBeforehand == 0)
{
// TODO: Maybe add some different options of what normal to start with
}
else if (NumSplinePointsBeforehand > 1)
{
UpVectorToUse = WorkingSpline->GetUpVectorAtSplinePoint(NumSplinePointsBeforehand - 1, ESplineCoordinateSpace::World);
}
else // if NumSplinePointsBeforehand == 1
{
// If there's only one point, GetUpVectorAtSplinePoint is unreliable because it seeks to build a
// quaternion from the tangent and the set up vector, and the tangent is zero. We want to use
// the "stored" up vector directly.
FVector3d LocalUpVector = WorkingSpline->GetRotationAtSplinePoint(0, ESplineCoordinateSpace::Local).RotateVector(WorkingSpline->DefaultUpVector);
UpVectorToUse = WorkingSpline->GetComponentTransform().TransformVectorNoScale(LocalUpVector);
}
break;
}
case EDrawSplineUpVectorMode::UseHitNormal:
break;
}
return UpVectorToUse;
}
bool UDrawSplineTool::Raycast(const FRay& WorldRay, FVector3d& HitLocationOut, FVector3d& HitNormalOut, double& HitTOut)
{
double BestHitT = TNumericLimits<double>::Max();
if (Settings->bHitCustomPlane)
{
FVector IntersectionPoint;
bool bHitPlane = false;
GizmoMath::RayPlaneIntersectionPoint(PlaneMechanic->Plane.Origin, PlaneMechanic->Plane.Z(),
WorldRay.Origin, WorldRay.Direction, bHitPlane, IntersectionPoint);
if (bHitPlane)
{
HitLocationOut = IntersectionPoint;
HitNormalOut = PlaneMechanic->Plane.Z();
HitTOut = WorldRay.GetParameter(IntersectionPoint);
BestHitT = HitTOut;
}
}
if (Settings->bHitWorld)
{
FHitResult GeometryHit;
TArray<const UPrimitiveComponent*> ComponentsToIgnore;
if (PreviewActor)
{
PreviewActor->GetComponents<const UPrimitiveComponent>(ComponentsToIgnore);
}
if (ToolSceneQueriesUtil::FindNearestVisibleObjectHit(this, GeometryHit, WorldRay, &ComponentsToIgnore)
&& GeometryHit.Distance < BestHitT)
{
HitLocationOut = GeometryHit.ImpactPoint;
HitNormalOut = GeometryHit.ImpactNormal;
HitTOut = GeometryHit.Distance;
BestHitT = HitTOut;
}
}
// Only raycast the ground plane / ortho background if we didn't hit anything else
if (Settings->bHitGroundPlanes && BestHitT == TNumericLimits<double>::Max())
{
FVector3d PlaneNormal = CameraState.bIsOrthographic ? -WorldRay.Direction : FVector3d::UnitZ();
FVector IntersectionPoint;
bool bHitPlane = false;
GizmoMath::RayPlaneIntersectionPoint(FVector3d::Zero(), PlaneNormal,
WorldRay.Origin, WorldRay.Direction, bHitPlane, IntersectionPoint);
if (bHitPlane)
{
HitLocationOut = IntersectionPoint;
HitNormalOut = PlaneNormal;
HitTOut = WorldRay.GetParameter(IntersectionPoint);
BestHitT = HitTOut;
}
}
if (Settings->ClickOffset != 0.0)
{
FVector3d OffsetDirection = HitNormalOut;
if (Settings->OffsetMethod == ESplineOffsetMethod::Custom)
{
OffsetDirection = Settings->OffsetDirection.GetSafeNormal(UE_SMALL_NUMBER, FVector3d::UnitZ());
}
HitLocationOut += OffsetDirection * Settings->ClickOffset;
}
return BestHitT < TNumericLimits<double>::Max();
}
FInputRayHit UDrawSplineTool::IsHitByClick(const FInputDeviceRay& ClickPos)
{
FVector3d HitLocation, HitNormal;
double HitT;
if (Raycast(ClickPos.WorldRay, HitLocation, HitNormal, HitT))
{
return FInputRayHit(HitT);
}
return FInputRayHit();
}
void UDrawSplineTool::OnClicked(const FInputDeviceRay& ClickPos)
{
using namespace DrawSplineToolLocals;
FVector3d HitLocation, HitNormal;
double HitT;
if (Raycast(ClickPos.WorldRay, HitLocation, HitNormal, HitT))
{
switch (Settings->DrawMode)
{
case EDrawSplineDrawMode::ClickAutoTangent:
case EDrawSplineDrawMode::FreeDraw:
{
AddSplinePoint(HitLocation, HitNormal);
int32 PointIndex = WorkingSpline->GetNumberOfSplinePoints() - 1;
GetToolManager()->EmitObjectChange(this,
MakeUnique<FSimplePointInsertionChange>(
HitLocation,
WorkingSpline->GetUpVectorAtSplinePoint(PointIndex, ESplineCoordinateSpace::World)),
AddPointTransactionName);
break;
}
case EDrawSplineDrawMode::TangentDrag:
{
AddSplinePoint(HitLocation, HitNormal);
int32 PointIndex = WorkingSpline->GetNumberOfSplinePoints() - 1;
WorkingSpline->SetTangentAtSplinePoint(PointIndex,
FVector3d::Zero(),
ESplineCoordinateSpace::World, true);
GetToolManager()->EmitObjectChange(this,
MakeUnique<FTangentPointInsertionChange>(
HitLocation,
WorkingSpline->GetUpVectorAtSplinePoint(PointIndex, ESplineCoordinateSpace::World),
FVector3d::Zero()),
AddPointTransactionName);
break;
}
}
bNeedToRerunConstructionScript = true;
}
}
FInputRayHit UDrawSplineTool::CanBeginClickDragSequence(const FInputDeviceRay& PressPos)
{
FVector3d HitLocation, HitNormal;
double HitT;
if (Raycast(PressPos.WorldRay, HitLocation, HitNormal, HitT))
{
return FInputRayHit(HitT);
}
return FInputRayHit();
}
void UDrawSplineTool::OnClickPress(const FInputDeviceRay& PressPos)
{
FVector3d HitLocation, HitNormal;
double HitT;
LongTransactions.Open(DrawSplineToolLocals::AddPointTransactionName, GetToolManager());
// Regardless of DrawMode, start by placing a point, though don't emit a transaction until mouse up
if (ensure(Raycast(PressPos.WorldRay, HitLocation, HitNormal, HitT)))
{
AddSplinePoint(HitLocation, HitNormal);
if (Settings->DrawMode == EDrawSplineDrawMode::FreeDraw)
{
// Remember which point started this stroke
FreeDrawStrokeStartIndex = WorkingSpline->GetNumberOfSplinePoints() - 1;
}
bNeedToRerunConstructionScript = bNeedToRerunConstructionScript || Settings->bRerunConstructionScriptOnDrag;
}
}
void UDrawSplineTool::OnClickDrag(const FInputDeviceRay& DragPos)
{
using namespace DrawSplineToolLocals;
int32 NumSplinePoints = WorkingSpline->GetNumberOfSplinePoints();
if (!ensure(NumSplinePoints > 0))
{
return;
}
FVector3d HitLocation, HitNormal;
double HitT;
if (Raycast(DragPos.WorldRay, HitLocation, HitNormal, HitT))
{
switch (Settings->DrawMode)
{
case EDrawSplineDrawMode::ClickAutoTangent:
{
// Drag the last placed point
FVector3d UpVector = GetUpVectorToUse(HitLocation, HitNormal, NumSplinePoints);
WorkingSpline->SetLocationAtSplinePoint(NumSplinePoints - 1, HitLocation, ESplineCoordinateSpace::World, false);
WorkingSpline->SetUpVectorAtSplinePoint(NumSplinePoints - 1, UpVector, ESplineCoordinateSpace::World, true);
break;
}
case EDrawSplineDrawMode::TangentDrag:
{
// Set the tangent
FVector3d LastPoint = WorkingSpline->GetLocationAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World);
FVector3d Tangent = (HitLocation - LastPoint) / GetNonzeroTangentScale();
WorkingSpline->SetTangentAtSplinePoint(NumSplinePoints - 1, Tangent, ESplineCoordinateSpace::World, true);
bDrawTangentForLastPoint = true;
break;
}
case EDrawSplineDrawMode::FreeDraw:
{
// Instead of dragging the first placed point (which gets placed in OnClickPress), we drag a second "preview" one
// until we get far enough from the previous to where we want to place a new control point.
if (!bFreeDrawPlacedPreviewPoint)
{
AddSplinePoint(HitLocation, HitNormal);
bFreeDrawPlacedPreviewPoint = true;
}
else
{
FVector3d PreviousPoint = WorkingSpline->GetLocationAtSplinePoint(NumSplinePoints - 2, ESplineCoordinateSpace::World);
if (FVector3d::DistSquared(HitLocation, PreviousPoint) >= Settings->MinPointSpacing * Settings->MinPointSpacing)
{
AddSplinePoint(HitLocation, HitNormal);
}
else
{
// Drag the preview point
FVector3d UpVector = GetUpVectorToUse(HitLocation, HitNormal, NumSplinePoints);
WorkingSpline->SetLocationAtSplinePoint(NumSplinePoints - 1, HitLocation, ESplineCoordinateSpace::World, false);
WorkingSpline->SetUpVectorAtSplinePoint(NumSplinePoints - 1, UpVector, ESplineCoordinateSpace::World, true);
}
}
break;
}
}
}
bNeedToRerunConstructionScript = bNeedToRerunConstructionScript || Settings->bRerunConstructionScriptOnDrag;
}
void UDrawSplineTool::OnClickRelease(const FInputDeviceRay& ReleasePos)
{
OnClickDrag(ReleasePos);
OnTerminateDragSequence();
}
void UDrawSplineTool::OnTerminateDragSequence()
{
using namespace DrawSplineToolLocals;
bDrawTangentForLastPoint = false;
bFreeDrawPlacedPreviewPoint = false;
bNeedToRerunConstructionScript = true;
int32 NumSplinePoints = WorkingSpline->GetNumberOfSplinePoints();
// Emit the appropriate undo transaction
switch (Settings->DrawMode)
{
case EDrawSplineDrawMode::ClickAutoTangent:
{
WorkingSpline->GetLocationAtSplinePoint(NumSplinePoints-1, ESplineCoordinateSpace::World);
WorkingSpline->GetUpVectorAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World);
GetToolManager()->EmitObjectChange(this,
MakeUnique<FSimplePointInsertionChange>(
WorkingSpline->GetLocationAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World),
WorkingSpline->GetUpVectorAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World)),
AddPointTransactionName);
break;
}
case EDrawSplineDrawMode::TangentDrag:
{
GetToolManager()->EmitObjectChange(this,
MakeUnique<FTangentPointInsertionChange>(
WorkingSpline->GetLocationAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World),
WorkingSpline->GetUpVectorAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World),
WorkingSpline->GetTangentAtSplinePoint(NumSplinePoints - 1, ESplineCoordinateSpace::World)),
AddPointTransactionName);
break;
}
case EDrawSplineDrawMode::FreeDraw:
{
TArray<FVector3d> HitLocations;
TArray<FVector3d> UpVectors;
for (int32 i = FreeDrawStrokeStartIndex; i < NumSplinePoints; ++i)
{
HitLocations.Add(WorkingSpline->GetLocationAtSplinePoint(i, ESplineCoordinateSpace::World));
UpVectors.Add(WorkingSpline->GetUpVectorAtSplinePoint(i, ESplineCoordinateSpace::World));
}
GetToolManager()->EmitObjectChange(this,
MakeUnique<FStrokeInsertionChange>(HitLocations, UpVectors),
AddPointTransactionName);
break;
}
}
LongTransactions.Close(GetToolManager());
}
void UDrawSplineTool::OnTick(float DeltaTime)
{
if (PlaneMechanic)
{
PlaneMechanic->Tick(DeltaTime);
}
// check if we've invalidated the WorkingSpline
if (PreviewActor && !WorkingSpline.IsValid())
{
bNeedToRerunConstructionScript = true;
}
if (bNeedToRerunConstructionScript)
{
bNeedToRerunConstructionScript = false;
if (PreviewActor)
{
PreviewActor->RerunConstructionScripts();
// Rerunning the construction script can make us lose our reference to the spline, so try to
// recapture.
// TODO: This might be avoidable with FComponentReference?
if (!WorkingSpline.IsValid())
{
TInlineComponentArray<USplineComponent*> SplineComponents;
PreviewActor->GetComponents<USplineComponent>(SplineComponents);
if (ensure(SplineRecaptureIndex >= 0 && SplineRecaptureIndex < SplineComponents.Num()))
{
WorkingSpline = SplineComponents[SplineRecaptureIndex];
}
else
{
// If we failed to recapture, it's not clear what to do. We can switch to working inside
// an empty actor, though we'll lose current spline state.
Settings->OutputMode = EDrawSplineOutputMode::EmptyActor;
Settings->CheckAndUpdateWatched();
}
}
}
}
if (!WorkingSpline.IsValid())
{
GetToolManager()->PostActiveToolShutdownRequest(this, EToolShutdownType::Cancel, true, LOCTEXT("LostWorkingSpline", "The Draw Spline tool must close because the in-progress spline has been unexpectedly deleted."));
}
}
void UDrawSplineTool::Render(IToolsContextRenderAPI* RenderAPI)
{
using namespace DrawSplineToolLocals;
Super::Render(RenderAPI);
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState);
if (PlaneMechanic)
{
PlaneMechanic->Render(RenderAPI);
}
if (WorkingSpline.IsValid())
{
if (bDrawTangentForLastPoint)
{
DrawTangent(*WorkingSpline, WorkingSpline->GetNumberOfSplinePoints() - 1, *RenderAPI);
}
SplineUtil::FDrawSplineSettings DrawSettings;
DrawSettings.ScaleVisualizationWidth = Settings->FrameVisualizationWidth;
SplineUtil::DrawSpline(*WorkingSpline, *RenderAPI, DrawSettings);
}
}
void UDrawSplineTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
}
bool UDrawSplineTool::CanAccept() const
{
return WorkingSpline.IsValid() && WorkingSpline->GetNumberOfSplinePoints() > 0;
}
// To be called by builder
void UDrawSplineTool::SetSelectedActor(AActor* Actor)
{
StartupSelectedActor = Actor;
}
void UDrawSplineTool::SetWorld(UWorld* World)
{
TargetWorld = World;
}
/// Tool builder:
bool UDrawSplineToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
return true;
}
UInteractiveTool* UDrawSplineToolBuilder::BuildTool(const FToolBuilderState& SceneState) const
{
UDrawSplineTool* NewTool = NewObject<UDrawSplineTool>(SceneState.ToolManager);
NewTool->SetWorld(SceneState.World);
// May be null
NewTool->SetSelectedActor(ToolBuilderUtil::FindFirstActor(SceneState, [](AActor*) { return true; }));
return NewTool;
}
void UDrawSplineTool::FSplineChange::Apply(UObject* Object)
{
UDrawSplineTool* Tool = Cast<UDrawSplineTool>(Object);
if (!ensure(Tool))
{
return;
}
TWeakObjectPtr<USplineComponent> Spline = Tool->WorkingSpline;
if (!ensure(Spline.IsValid()))
{
return;
}
Apply(*Spline);
Tool->bNeedToRerunConstructionScript = true;
}
void UDrawSplineTool::FSplineChange::Revert(UObject* Object)
{
UDrawSplineTool* Tool = Cast<UDrawSplineTool>(Object);
if (!ensure(Tool))
{
return;
}
TWeakObjectPtr<USplineComponent> Spline = Tool->WorkingSpline;
if (!ensure(Spline.IsValid()))
{
return;
}
Revert(*Spline);
Tool->bNeedToRerunConstructionScript = true;
}
#undef LOCTEXT_NAMESPACE