// Copyright Epic Games, Inc. All Rights Reserved. #include "SnappingUtils.h" #include "Model.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()->GridEnabled && !IsSnapToVertexEnabled(); } bool FEditorViewportSnapping::IsSnapRotationEnabled() { // Ask Current Editor Mode if Rotation Snap is enabled return GLevelEditorModeTools().IsSnapRotationEnabled(); } bool FEditorViewportSnapping::IsSnapScaleEnabled() { return GetDefault()->SnapScaleEnabled; } bool FEditorViewportSnapping::IsSnapToVertexEnabled(bool bIsPivot) { if( GetDefault()->bSnapVertices ) { return true; } else if( GCurrentLevelEditingViewportClient ) { FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked( TEXT("LevelEditor") ); TSharedPtr Command = bIsPivot ? LevelEditor.GetLevelEditorCommands().HoldToEnablePivotVertexSnapping : LevelEditor.GetLevelEditorCommands().HoldToEnableVertexSnapping; return GCurrentLevelEditingViewportClient->IsCommandChordPressed(Command); } else { return false; } } bool FEditorViewportSnapping::IsSnapToActorEnabled() { return GetDefault()->bEnableActorSnap && !IsSnapToVertexEnabled(); } void FEditorViewportSnapping::EnableActorSnap(bool bEnable) { ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); ViewportSettings->bEnableActorSnap = bEnable; ViewportSettings->PostEditChange(); } float FEditorViewportSnapping::GetActorSnapDistance(bool bScalar) { ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); // 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(); 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; double 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( *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( 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 double SqrdDist = FVector::DistSquared( PivotLocation, Point ); if ( BestActor == nullptr || 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()->PreserveNonUniformScale) { // when using 'auto-precision', we take the max component & snap its scale, then proportionally scale the other components double MaxComponent = Point.GetAbsMax(); if(MaxComponent == 0.0f) { MaxComponent = 1.0f; } const double 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() ) { FVector3f SrcPoint = (FVector3f)Location; FVector3f DestPoint; int32 Temp; if( GWorld->GetModel()->FindNearestVertex(SrcPoint, DestPoint, GetDefault()->SnapDistance, Temp ) >= 0.0) { Location = (FVector)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 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("ViewportSnapping"); Module.RegisterSnappingPolicy(EditorViewportSnapper); }