EM_Task/UnrealEd/Private/SnappingUtils.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

501 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SnappingUtils.h"
#include "Modules/ModuleManager.h"
#include "GameFramework/Actor.h"
#include "Settings/LevelEditorViewportSettings.h"
#include "Editor/GroupActor.h"
#include "GameFramework/PhysicsVolume.h"
#include "Engine/PostProcessVolume.h"
#include "GameFramework/WorldSettings.h"
#include "EngineUtils.h"
#include "LevelEditorViewport.h"
#include "Engine/Selection.h"
#include "EditorModeManager.h"
#include "ScopedTransaction.h"
#include "EdMode.h"
#include "LevelEditor.h"
#include "LevelEditorActions.h"
#include "VertexSnapping.h"
#include "ISnappingPolicy.h"
#include "ViewportSnappingModule.h"
#include "ActorGroupingUtils.h"
//////////////////////////////////////////////////////////////////////////
// FEditorViewportSnapping
class FEditorViewportSnapping: public ISnappingPolicy
{
public:
// FEditorViewportSnapping interface
virtual void SnapScale(FVector& Point, const FVector& GridBase) override;
virtual void SnapPointToGrid(FVector& Point, const FVector& GridBase) override;
virtual void SnapRotatorToGrid(FRotator& Rotation) override;
virtual void ClearSnappingHelpers(bool bClearImmediately = false) override;
virtual void DrawSnappingHelpers(const FSceneView* View, FPrimitiveDrawInterface* PDI) override;
// End of FEditorViewportSnapping interface
bool IsSnapToGridEnabled();
bool IsSnapRotationEnabled();
bool IsSnapScaleEnabled();
/**
* @return true if snapping to vertices is enabled
*/
bool IsSnapToVertexEnabled(bool bIsPivot = false);
/**
* @return true if snapping actors to other actors is enabled
*/
bool IsSnapToActorEnabled();
/** Set user setting for actor snap. */
void EnableActorSnap(bool bEnable);
/** Access user setting for distance. Fractional 0.0->100.0 */
float GetActorSnapDistance(bool bScalar = false);
/** Set user setting for distance. Fractional 0.0->100.0 */
void SetActorSnapDistance(float Distance);
/**
* Attempts to snap the selected actors to the nearest other actor
*
* @param DragDelta The current world space drag amount
* @param ViewportClient The viewport client the user is dragging in
*/
bool SnapActorsToNearestActor(FVector& DragDelta, FLevelEditorViewportClient* ViewportClient);
/**
* Snaps actors to the nearest vertex on another actor
*
* @param DragDelta The current world space drag amount that will be modified to account for snapping to a vertex
* @param ViewportClient The viewport client the user is dragging in
* @return true if anything was snapped
*/
bool SnapDraggedActorsToNearestVertex(FVector& DragDelta, FLevelEditorViewportClient* ViewportClient);
/**
* Snaps a delta drag movement to the nearest vertex
*
* @param BaseLocation Location that should be snapped before any drag is applied
* @param DragDelta Delta drag movement that should be snapped. This value will be updated such that BaseLocation+DragDelta is the nearest snapped verted
* @param ViewportClient The viewport client being dragged in.
* @return true if anything was snapped
*/
bool SnapDragLocationToNearestVertex(const FVector& BaseLocation, FVector& DragDelta, FLevelEditorViewportClient* ViewportClient, bool bIsPivot = false);
/**
* Snaps a location to the nearest vertex
*
* @param Location The location to snap
* @param MouseLocation The current 2d mouse location. Vertices closer to the mouse are favored
* @param ViewportClient The viewport client being used
* @param OutVertexNormal The normal at the closest vertex
* @return true if anything was snapped
*/
bool SnapLocationToNearestVertex(FVector& Location, const FVector2D& MouseLocation, FLevelEditorViewportClient* ViewportClient, FVector& OutVertexNormal, bool bDrawVertHelpers);
bool SnapToBSPVertex(FVector& Location, FVector GridBase, FRotator& Rotation);
private:
/** Vertex snapping implementation */
FVertexSnappingImpl VertexSnappingImpl;
};
//////////////////////////////////////////////////////////////////////////
// FEditorViewportSnapping
bool FEditorViewportSnapping::IsSnapToGridEnabled()
{
return GetDefault<ULevelEditorViewportSettings>()->GridEnabled && !IsSnapToVertexEnabled();
}
bool FEditorViewportSnapping::IsSnapRotationEnabled()
{
// Ask Current Editor Mode if Rotation Snap is enabled
return GLevelEditorModeTools().IsSnapRotationEnabled();
}
bool FEditorViewportSnapping::IsSnapScaleEnabled()
{
return GetDefault<ULevelEditorViewportSettings>()->SnapScaleEnabled;
}
bool FEditorViewportSnapping::IsSnapToVertexEnabled(bool bIsPivot)
{
if (GetDefault<ULevelEditorViewportSettings>()->bSnapVertices)
{
return true;
}
else if (GCurrentLevelEditingViewportClient)
{
FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
const FLevelEditorCommands& Commands = LevelEditor.GetLevelEditorCommands();
bool bIsChordPressed = false;
for (uint32 i = 0; i < static_cast<uint8>(EMultipleKeyBindingIndex::NumChords); ++i)
{
EMultipleKeyBindingIndex ChordIndex = static_cast<EMultipleKeyBindingIndex>(i);
const FInputChord& Chord = bIsPivot ? (*Commands.HoldToEnablePivotVertexSnapping->GetActiveChord(ChordIndex)) : (*Commands.HoldToEnableVertexSnapping->GetActiveChord(ChordIndex));
bIsChordPressed |= (Chord.NeedsControl() == GCurrentLevelEditingViewportClient->IsCtrlPressed()) && (Chord.NeedsAlt() == GCurrentLevelEditingViewportClient->IsAltPressed()) && (Chord.NeedsShift() == GCurrentLevelEditingViewportClient->IsShiftPressed()) && GCurrentLevelEditingViewportClient->Viewport->KeyState(Chord.Key) == true;
}
return bIsChordPressed;
}
else
{
return false;
}
}
bool FEditorViewportSnapping::IsSnapToActorEnabled()
{
return GetDefault<ULevelEditorViewportSettings>()->bEnableActorSnap && !IsSnapToVertexEnabled();
}
void FEditorViewportSnapping::EnableActorSnap(bool bEnable)
{
ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault<ULevelEditorViewportSettings>();
ViewportSettings->bEnableActorSnap = bEnable;
ViewportSettings->PostEditChange();
}
float FEditorViewportSnapping::GetActorSnapDistance(bool bScalar)
{
ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault<ULevelEditorViewportSettings>();
// If the user has purposefully exceeded the maximum scale, convert this to the range so that it can be more easily adjusted in the editor
if (ViewportSettings->ActorSnapScale > 1.0f)
{
ViewportSettings->ActorSnapDistance *= ViewportSettings->ActorSnapScale;
ViewportSettings->ActorSnapScale = 1.0f;
ViewportSettings->PostEditChange();
}
if (bScalar)
{
// Clamp to within range (just so slider looks correct)
return FMath::Clamp(ViewportSettings->ActorSnapScale, 0.0f, 1.0f);
}
// Multiply by the max distance allowed to convert to range
return FMath::Max(0.0f, ViewportSettings->ActorSnapScale) * ViewportSettings->ActorSnapDistance;
}
void FEditorViewportSnapping::SetActorSnapDistance(float Distance)
{
ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault<ULevelEditorViewportSettings>();
ViewportSettings->ActorSnapScale = Distance;
ViewportSettings->PostEditChange();
}
bool FEditorViewportSnapping::SnapActorsToNearestActor(FVector& Drag, FLevelEditorViewportClient* ViewportClient)
{
FEditorModeTools& Tools = GLevelEditorModeTools();
// Does the user have actor snapping enabled?
bool bSnapped = false;
if (IsSnapToActorEnabled())
{
// Are there selected actors?
USelection* Selection = GEditor->GetSelectedActors();
if (Selection->Num() > 0)
{
// Nearest results
const AActor* BestActor = NULL;
FVector BestPoint = FVector::ZeroVector;
float BestSqrdDist = 0.0f;
// Find the nearest actor to the pivot point that isn't part of the selection
const FVector PivotLocation = Tools.PivotLocation;
for (FActorIterator It(ViewportClient->GetWorld()); It; ++It) // Actor iterator :( [Note: Also, can't use BoxOverlapMulti to as the pivot may lie outside the bounds of the actor!]
{
AActor* Actor = static_cast<AActor*>(*It);
checkSlow(Actor->IsA(AActor::StaticClass()));
// Make sure this isn't an invalid actor type or one of the selected actors
const FString tmp = Actor->GetActorLabel();
if (!Actor->IsA(AWorldSettings::StaticClass()) && !Actor->IsA(APhysicsVolume::StaticClass()) && !Actor->IsA(APostProcessVolume::StaticClass()) && !Selection->IsSelected(Actor))
{
// Group Actors don't appear in the selected actors list!
if (UActorGroupingUtils::IsGroupingActive())
{
// Valid snaps: locked groups (not self or actors within locked groups), actors within unlocked groups (not the group itself), other actors
const AGroupActor* GroupActor = Cast<AGroupActor>(Actor); // AGroupActor::GetRootForActor( Actor );
if (GroupActor && (!GroupActor->IsLocked() || GroupActor->HasSelectedActors()))
{
continue;
}
}
// Is this the nearest actor to the pivot?
const FVector Point = Actor->GetActorLocation();
const float SqrdDist = FVector::DistSquared(PivotLocation, Point);
if (BestActor == NULL || SqrdDist < BestSqrdDist)
{
BestActor = Actor;
BestPoint = Point;
BestSqrdDist = SqrdDist;
}
}
}
// Did we find an actor?
const FString tmp = BestActor ? BestActor->GetActorLabel() : TEXT("None");
if (BestActor)
{
// Are we within the threshold or exitting it?
const float Dist = GetActorSnapDistance();
if (BestSqrdDist < FMath::Square(Dist))
{
bSnapped = true;
// Are we no already snapped, or is it different to our current location
if (!Tools.SnappedActor || !Tools.CachedLocation.Equals(BestPoint))
{
// Calculate the delta between the snapped location and the current pivot and apply to all the selected actors
const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SnapActorsToActor", "Snap Actors To Actor"));
const FVector PivotDelta = (BestPoint - PivotLocation);
ViewportClient->ApplyDeltaToActors(PivotDelta, FRotator::ZeroRotator, FVector::ZeroVector);
Tools.SetPivotLocation(BestPoint, false); // Overwrite the location for next time we check
Drag = FVector::ZeroVector; // Reset the drag so the pivot doesn't jump
}
}
else if (Tools.SnappedActor && !Tools.CachedLocation.Equals(PivotLocation))
{
const FVector PivotDelta = (PivotLocation - BestPoint);
ViewportClient->ApplyDeltaToActors(PivotDelta, FRotator::ZeroRotator, FVector::ZeroVector);
// GUnrealEd->UpdatePivotLocationForSelection(); // Calling this ends up forcing the pivot back inside the threshold?!
Tools.SetPivotLocation(PivotLocation, false); // Overwrite the location for next time we check
Drag = FVector::ZeroVector; // Reset the drag so the pivot doesn't jump
}
}
}
}
Tools.SnappedActor = bSnapped;
return bSnapped; // Whether or not the selection is snapped in place
}
void FEditorViewportSnapping::SnapPointToGrid(FVector& Point, const FVector& GridBase)
{
if (IsSnapToGridEnabled())
{
Point = (Point - GridBase).GridSnap(GEditor->GetGridSize()) + GridBase;
}
}
void FEditorViewportSnapping::SnapRotatorToGrid(FRotator& Rotation)
{
if (IsSnapRotationEnabled())
{
if (GLevelEditorModeTools().SnapRotatorToGridOverride(Rotation))
{
return;
}
Rotation = Rotation.GridSnap(GEditor->GetRotGridSize());
}
}
void FEditorViewportSnapping::SnapScale(FVector& Point, const FVector& GridBase)
{
if (IsSnapScaleEnabled())
{
if (GEditor->UsePercentageBasedScaling())
{
Point = (Point - GridBase).GridSnap(GEditor->GetGridSize()) + GridBase;
}
else
{
if (GetDefault<ULevelEditorViewportSettings>()->PreserveNonUniformScale)
{
// when using 'auto-precision', we take the max component & snap its scale, then proportionally scale the other components
float MaxComponent = Point.GetAbsMax();
if (MaxComponent == 0.0f)
{
MaxComponent = 1.0f;
}
const float SnappedMaxComponent = FMath::GridSnap(MaxComponent, GEditor->GetScaleGridSize());
Point = Point * (SnappedMaxComponent / MaxComponent);
}
else
{
Point = Point.GridSnap(GEditor->GetScaleGridSize());
}
}
}
}
bool FEditorViewportSnapping::SnapToBSPVertex(FVector& Location, FVector GridBase, FRotator& Rotation)
{
bool bSnapped = false;
SnapRotatorToGrid(Rotation);
if (IsSnapToVertexEnabled())
{
FVector DestPoint;
int32 Temp;
if (GWorld->GetModel()->FindNearestVertex(Location, DestPoint, GetDefault<ULevelEditorViewportSettings>()->SnapDistance, Temp) >= 0.0)
{
Location = DestPoint;
bSnapped = true;
}
}
if (!bSnapped)
{
SnapPointToGrid(Location, GridBase);
}
return bSnapped;
}
void FEditorViewportSnapping::ClearSnappingHelpers(bool bClearImmediately)
{
VertexSnappingImpl.ClearSnappingHelpers(bClearImmediately);
}
void FEditorViewportSnapping::DrawSnappingHelpers(const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
VertexSnappingImpl.DrawSnappingHelpers(View, PDI);
}
bool FEditorViewportSnapping::SnapLocationToNearestVertex(FVector& Location, const FVector2D& MouseLocation, FLevelEditorViewportClient* ViewportClient, FVector& OutVertexNormal, bool bDrawVertHelpers)
{
bool bSnapped = false;
if (IsSnapToVertexEnabled())
{
bSnapped = VertexSnappingImpl.SnapLocationToNearestVertex(Location, MouseLocation, ViewportClient, OutVertexNormal, bDrawVertHelpers);
}
else
{
OutVertexNormal = FVector(ForceInitToZero);
}
return bSnapped;
}
bool FEditorViewportSnapping::SnapDraggedActorsToNearestVertex(FVector& DragDelta, FLevelEditorViewportClient* ViewportClient)
{
bool bSnapped = false;
if (IsSnapToVertexEnabled() && !DragDelta.IsNearlyZero())
{
bSnapped = VertexSnappingImpl.SnapDraggedActorsToNearestVertex(DragDelta, ViewportClient);
}
return bSnapped;
}
bool FEditorViewportSnapping::SnapDragLocationToNearestVertex(const FVector& BaseLocation, FVector& DragDelta, FLevelEditorViewportClient* ViewportClient, bool bIsPivot)
{
bool bSnapped = false;
if (IsSnapToVertexEnabled(bIsPivot) && !DragDelta.IsNearlyZero())
{
bSnapped = VertexSnappingImpl.SnapDragLocationToNearestVertex(BaseLocation, DragDelta, ViewportClient);
}
return bSnapped;
}
//////////////////////////////////////////////////////////////////////////
// FSnappingUtils
TSharedPtr<FEditorViewportSnapping> FSnappingUtils::EditorViewportSnapper;
bool FSnappingUtils::IsSnapToGridEnabled()
{
return EditorViewportSnapper.IsValid() && EditorViewportSnapper->IsSnapToGridEnabled();
}
bool FSnappingUtils::IsRotationSnapEnabled()
{
return EditorViewportSnapper.IsValid() && EditorViewportSnapper->IsSnapRotationEnabled();
}
bool FSnappingUtils::IsScaleSnapEnabled()
{
return EditorViewportSnapper.IsValid() && EditorViewportSnapper->IsSnapScaleEnabled();
}
bool FSnappingUtils::IsSnapToActorEnabled()
{
return EditorViewportSnapper.IsValid() && EditorViewportSnapper->IsSnapToActorEnabled();
}
void FSnappingUtils::EnableActorSnap(bool bEnable)
{
if (EditorViewportSnapper.IsValid())
{
EditorViewportSnapper->EnableActorSnap(bEnable);
}
}
float FSnappingUtils::GetActorSnapDistance(bool bScalar)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->GetActorSnapDistance(bScalar) : 0.0f;
}
void FSnappingUtils::SetActorSnapDistance(float Distance)
{
if (EditorViewportSnapper.IsValid())
{
EditorViewportSnapper->SetActorSnapDistance(Distance);
}
}
bool FSnappingUtils::SnapActorsToNearestActor(FVector& DragDelta, FLevelEditorViewportClient* ViewportClient)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->SnapActorsToNearestActor(DragDelta, ViewportClient) : false;
}
bool FSnappingUtils::SnapDraggedActorsToNearestVertex(FVector& DragDelta, FLevelEditorViewportClient* ViewportClient)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->SnapDraggedActorsToNearestVertex(DragDelta, ViewportClient) : false;
}
bool FSnappingUtils::SnapDragLocationToNearestVertex(const FVector& BaseLocation, FVector& DragDelta, FLevelEditorViewportClient* ViewportClient, bool bIsPivot)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->SnapDragLocationToNearestVertex(BaseLocation, DragDelta, ViewportClient, bIsPivot) : false;
}
bool FSnappingUtils::SnapLocationToNearestVertex(FVector& Location, const FVector2D& MouseLocation, FLevelEditorViewportClient* ViewportClient, FVector& OutVertexNormal, bool bDrawVertHelpers)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->SnapLocationToNearestVertex(Location, MouseLocation, ViewportClient, OutVertexNormal, bDrawVertHelpers) : false;
}
void FSnappingUtils::SnapScale(FVector& Point, const FVector& GridBase)
{
IViewportSnappingModule::GetSnapManager()->SnapScale(Point, GridBase);
}
void FSnappingUtils::SnapPointToGrid(FVector& Point, const FVector& GridBase)
{
IViewportSnappingModule::GetSnapManager()->SnapPointToGrid(Point, GridBase);
}
void FSnappingUtils::SnapRotatorToGrid(FRotator& Rotation)
{
IViewportSnappingModule::GetSnapManager()->SnapRotatorToGrid(Rotation);
}
bool FSnappingUtils::SnapToBSPVertex(FVector& Location, FVector GridBase, FRotator& Rotation)
{
return EditorViewportSnapper.IsValid() ? EditorViewportSnapper->SnapToBSPVertex(Location, GridBase, Rotation) : false;
}
void FSnappingUtils::ClearSnappingHelpers(bool bClearImmediately)
{
IViewportSnappingModule::GetSnapManager()->ClearSnappingHelpers(bClearImmediately);
}
void FSnappingUtils::DrawSnappingHelpers(const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
IViewportSnappingModule::GetSnapManager()->DrawSnappingHelpers(View, PDI);
}
void FSnappingUtils::InitEditorSnappingTools()
{
EditorViewportSnapper = MakeShareable(new FEditorViewportSnapping);
IViewportSnappingModule& Module = FModuleManager::LoadModuleChecked<IViewportSnappingModule>("ViewportSnapping");
Module.RegisterSnappingPolicy(EditorViewportSnapper);
}