// Copyright Epic Games, Inc. All Rights Reserved. #include "Tools/EdModeInteractiveToolsContext.h" #include "BaseGizmos/GizmoViewContext.h" #include "ContextObjectStore.h" #include "Editor.h" #include "EditorViewportClient.h" #include "EditorModeManager.h" #include "Framework/Application/SlateApplication.h" #include "LevelEditor.h" #include "LevelEditorViewport.h" #include "IAssetViewport.h" #include "Math/Rotator.h" #include "Misc/AssertionMacros.h" #include "SLevelViewport.h" #include "Modules/ModuleManager.h" #include "ShowFlags.h" // for EngineShowFlags #include "Engine/Engine.h" #include "Engine/Selection.h" #include "Engine/World.h" #include "Misc/ITransaction.h" #include "ScopedTransaction.h" #include "Materials/Material.h" #include "Engine/StaticMesh.h" #include "Components/StaticMeshComponent.h" #include "ToolContextInterfaces.h" #include "InteractiveToolObjects.h" #include "EditorInteractiveGizmoManager.h" #include "BaseBehaviors/ClickDragBehavior.h" #include "EditorModeManager.h" #include "EdMode.h" #include "EditorDragTools.h" #include "BaseGizmos/GizmoRenderingUtil.h" #include "UnrealClient.h" #include "EditorDragTools/DragToolsBehaviourSource.h" #include "EditorDragTools/EditorDragToolBehaviorTarget.h" #include "UObject/ObjectSaveContext.h" //#define ENABLE_DEBUG_PRINTING DEFINE_LOG_CATEGORY_STATIC(LogInteractiveToolsContext, Log, All); class FEdModeToolsContextQueriesImpl : public IToolsContextQueriesAPI { public: UEditorInteractiveToolsContext* ToolsContext; FEditorModeTools* EditorModeManager; FViewCameraState CachedViewState; FEditorViewportClient* CachedViewportClient; FEdModeToolsContextQueriesImpl(UEditorInteractiveToolsContext* Context, FEditorModeTools* InEditorModeManager) { ToolsContext = Context; EditorModeManager = InEditorModeManager; } void CacheCurrentViewState(FEditorViewportClient* ViewportClient) { CachedViewportClient = ViewportClient; FViewportCameraTransform ViewTransform = ViewportClient->GetViewTransform(); CachedViewState.bIsOrthographic = ViewportClient->IsOrtho(); CachedViewState.Position = ViewTransform.GetLocation(); CachedViewState.HorizontalFOVDegrees = ViewportClient->ViewFOV; CachedViewState.AspectRatio = ViewportClient->AspectRatio; // ViewTransform rotation is only initialized for perspective! if (CachedViewState.bIsOrthographic == false) { // if using Orbit camera, the rotation in the ViewTransform is not the current camera rotation, it // is set to a different rotation based on the Orbit. So we have to convert back to camera rotation. FRotator ViewRotation = (ViewportClient->bUsingOrbitCamera) ? ViewTransform.ComputeOrbitMatrix().InverseFast().Rotator() : ViewTransform.GetRotation(); CachedViewState.Orientation = ViewRotation.Quaternion(); } else { // These rotations are based on hardcoded values in EditorViewportClient.cpp, see switches in FEditorViewportClient::CalcSceneView and FEditorViewportClient::Draw switch (ViewportClient->ViewportType) { case LVT_OrthoXY: CachedViewState.Orientation = FQuat(FRotator(-90.0f, -90.0f, 0.0f)); break; case LVT_OrthoNegativeXY: CachedViewState.Orientation = FQuat(FRotator(90.0f, 90.0f, 0.0f)); break; case LVT_OrthoXZ: CachedViewState.Orientation = FQuat(FRotator(0.0f, -90.0f, 0.0f)); break; case LVT_OrthoNegativeXZ: CachedViewState.Orientation = FQuat(FRotator(0.0f, 90.0f, 0.0f)); break; case LVT_OrthoYZ: CachedViewState.Orientation = FQuat(FRotator(0.0f, 0.0f, 0.0f)); break; case LVT_OrthoNegativeYZ: CachedViewState.Orientation = FQuat(FRotator(0.0f, 180.0f, 0.0f)); break; default: CachedViewState.Orientation = FQuat::Identity; } CachedViewState.OrthoWorldCoordinateWidth = ViewportClient->GetOrthoUnitsPerPixel(ViewportClient->Viewport) * static_cast(ViewportClient->Viewport->GetSizeXY().X); } CachedViewState.bIsVR = false; } virtual UWorld* GetCurrentEditingWorld() const override { return EditorModeManager->GetWorld(); } virtual void GetCurrentSelectionState(FToolBuilderState& StateOut) const override { StateOut.ToolManager = ToolsContext->ToolManager; StateOut.TargetManager = ToolsContext->TargetManager; StateOut.GizmoManager = ToolsContext->GizmoManager; StateOut.World = EditorModeManager->GetWorld(); EditorModeManager->GetSelectedActors()->GetSelectedObjects(StateOut.SelectedActors); EditorModeManager->GetSelectedComponents()->GetSelectedObjects(StateOut.SelectedComponents); PRAGMA_DISABLE_DEPRECATION_WARNINGS StateOut.TypedElementSelectionSet = EditorModeManager->GetEditorSelectionSet(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } virtual void GetCurrentViewState(FViewCameraState& StateOut) const override { StateOut = CachedViewState; } virtual EToolContextCoordinateSystem GetCurrentCoordinateSystem() const override { ECoordSystem CoordSys = EditorModeManager->GetCoordSystem(); return (CoordSys == COORD_World) ? EToolContextCoordinateSystem::World : EToolContextCoordinateSystem::Local; } virtual EToolContextTransformGizmoMode GetCurrentTransformGizmoMode() const override { if (ToolsContext->GetForceCombinedGizmoModeEnabled() == false) { UE::Widget::EWidgetMode WidgetMode = EditorModeManager->GetWidgetMode(); switch (WidgetMode) { case UE::Widget::EWidgetMode::WM_None: return EToolContextTransformGizmoMode::NoGizmo; case UE::Widget::EWidgetMode::WM_Translate: return EToolContextTransformGizmoMode::Translation; case UE::Widget::EWidgetMode::WM_Rotate: return EToolContextTransformGizmoMode::Rotation; case UE::Widget::EWidgetMode::WM_Scale: return EToolContextTransformGizmoMode::Scale; } } return EToolContextTransformGizmoMode::Combined; } virtual FToolContextSnappingConfiguration GetCurrentSnappingSettings() const override { FToolContextSnappingConfiguration Config; Config.bEnablePositionGridSnapping = (GetDefault()->GridEnabled != 0); float EditorGridSize = GEditor->GetGridSize(); Config.PositionGridDimensions = FVector(EditorGridSize, EditorGridSize, EditorGridSize); Config.bEnableRotationGridSnapping = (GetDefault()->RotGridEnabled != 0); Config.RotationGridAngles = GEditor->GetRotGridSize(); Config.bEnableScaleGridSnapping = (GetDefault()->SnapScaleEnabled != 0); Config.ScaleGridSize = GEditor->GetScaleGridSize(); Config.bEnableAbsoluteWorldSnapping = ToolsContext->GetAbsoluteWorldSnappingEnabled(); return Config; } virtual UMaterialInterface* GetStandardMaterial(EStandardToolContextMaterials MaterialType) const { if (MaterialType == EStandardToolContextMaterials::VertexColorMaterial) { return ToolsContext->StandardVertexColorMaterial; } check(false); return nullptr; } virtual FViewport* GetHoveredViewport() const override { if (FEditorViewportClient* HoveredClient = EditorModeManager->GetHoveredViewportClient()) { return HoveredClient->Viewport; } return nullptr; } virtual FViewport* GetFocusedViewport() const override { if (FEditorViewportClient* FocusedClient = EditorModeManager->GetFocusedViewportClient()) { return FocusedClient->Viewport; } return nullptr; } }; class FEdModeToolsContextTransactionImpl : public IToolsContextTransactionsAPI { public: FEdModeToolsContextTransactionImpl(UEditorInteractiveToolsContext* Context, FEditorModeTools* InEditorModeManager) : ToolsContext(Context) , EditorModeManager(InEditorModeManager) { check(EditorModeManager); check(ToolsContext); } virtual void DisplayMessage(const FText& Message, EToolMessageLevel Level) override { if (Level == EToolMessageLevel::UserNotification || Level == EToolMessageLevel::UserMessage) { UE_LOG(LogInteractiveToolsContext, Display, TEXT("%s"), *Message.ToString()); ToolsContext->PostToolNotificationMessage(Message); } else if (Level == EToolMessageLevel::UserWarning || Level == EToolMessageLevel::UserError) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("%s"), *Message.ToString()); ToolsContext->PostToolWarningMessage(Message); } else { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("%s"), *Message.ToString()); } } virtual void PostInvalidation() override { ToolsContext->PostInvalidation(); } virtual void BeginUndoTransaction(const FText& Description) override { GEditor->BeginTransaction(Description); } virtual void EndUndoTransaction() override { GEditor->EndTransaction(); } virtual void AppendChange(UObject* TargetObject, TUniquePtr Change, const FText& Description) override { FScopedTransaction Transaction(Description); //if (ensure(GUndo != nullptr)) // ideally we would ensure here, but currently this can be hit on world teardown, resolution TBD if (GUndo != nullptr) { GUndo->StoreUndo(TargetObject, MoveTemp(Change)); } } virtual bool RequestSelectionChange(const FSelectedObjectsChangeList& SelectionChange) override { checkf(SelectionChange.Components.Num() == 0, TEXT("FEdModeToolsContextTransactionImpl::RequestSelectionChange - Component selection not supported yet")); if (SelectionChange.ModificationType == ESelectedObjectsModificationType::Clear) { GEditor->SelectNone(true, true, false); return true; } if (SelectionChange.ModificationType == ESelectedObjectsModificationType::Replace ) { GEditor->SelectNone(false, true, false); } bool bAdd = (SelectionChange.ModificationType != ESelectedObjectsModificationType::Remove); int NumActors = SelectionChange.Actors.Num(); for (int k = 0; k < NumActors; ++k) { // Calling GEditor->NoteSelectionChange(true) will not send out change notifications on the TypedElementSelectionSet. // The selection change will work but any Editor stuff listening for changes will not be notified. // This may be a bug, needs further investigation. // In the meantime, just send out the notification on the last SelectActor() call bool bNotify = (k == NumActors-1); GEditor->SelectActor(SelectionChange.Actors[k], bAdd, bNotify, true, false); } GEditor->NoteSelectionChange(true); return true; } protected: UEditorInteractiveToolsContext* ToolsContext; FEditorModeTools* EditorModeManager; }; UEditorInteractiveToolsContext::UEditorInteractiveToolsContext() { QueriesAPI = nullptr; TransactionAPI = nullptr; } void UEditorInteractiveToolsContext::Initialize(IToolsContextQueriesAPI* QueriesAPIIn, IToolsContextTransactionsAPI* TransactionsAPIIn) { UInteractiveToolsContext::Initialize(QueriesAPIIn, TransactionsAPIIn); InvalidationTimestamp = 0; // This gets set up in UInteractiveToolsContext::Initialize; GizmoViewContext = ToolManager->GetContextObjectStore()->FindContext(); UToolsContextCursorAPI* ToolsContextCursorAPI = ToolManager->GetContextObjectStore()->FindContext(); if (!ToolsContextCursorAPI) { ToolsContextCursorAPI = NewObject< UToolsContextCursorAPI >(); ToolManager->GetContextObjectStore()->AddContextObject(ToolsContextCursorAPI); } } void UEditorInteractiveToolsContext::Shutdown() { bIsActive = false; // auto-accept any in-progress tools DeactivateAllActiveTools(EToolShutdownType::Accept); UInteractiveToolsContext::Shutdown(); } void UEditorInteractiveToolsContext::InitializeContextWithEditorModeManager(FEditorModeTools* InEditorModeManager, UInputRouter* UseInputRouter) { check(InEditorModeManager); EditorModeManager = InEditorModeManager; this->TransactionAPI = new FEdModeToolsContextTransactionImpl(this, InEditorModeManager); this->QueriesAPI = new FEdModeToolsContextQueriesImpl(this, InEditorModeManager); SetCreateGizmoManagerFunc([this](const FContextInitInfo& ContextInfo) { UEditorInteractiveGizmoManager* NewGizmoManager = NewObject(ContextInfo.ToolsContext); NewGizmoManager->InitializeWithEditorModeManager(ContextInfo.QueriesAPI, ContextInfo.TransactionsAPI, ContextInfo.InputRouter, EditorModeManager); NewGizmoManager->RegisterDefaultGizmos(); return NewGizmoManager; }); if (UseInputRouter != nullptr) { SetCreateInputRouterFunc([this, UseInputRouter](const FContextInitInfo& ContextInfo) { return UseInputRouter; }); SetShutdownInputRouterFunc([this](UInputRouter*) {}); } Initialize(QueriesAPI, TransactionAPI); if (UseInputRouter != nullptr) { // enable auto invalidation in Editor, because invalidating for all hover and capture events is unpleasant this->InputRouter->bAutoInvalidateOnHover = true; this->InputRouter->bAutoInvalidateOnCapture = true; } // set up standard materials StandardVertexColorMaterial = GEngine->VertexColorMaterial; } void UEditorInteractiveToolsContext::ShutdownContext() { Shutdown(); OnToolNotificationMessage.Clear(); OnToolWarningMessage.Clear(); if (QueriesAPI != nullptr) { delete QueriesAPI; QueriesAPI = nullptr; } if (TransactionAPI != nullptr) { delete TransactionAPI; TransactionAPI = nullptr; } } void UEditorInteractiveToolsContext::TerminateActiveToolsOnPIEStart() { if (bDeactivateOnPIEStart) { DeactivateAllActiveTools(EToolShutdownType::Accept); } } void UEditorInteractiveToolsContext::TerminateActiveToolsOnSaveWorld() { if (bDeactivateOnSaveWorld) { DeactivateAllActiveTools(EToolShutdownType::Accept); } } void UEditorInteractiveToolsContext::TerminateActiveToolsOnWorldTearDown() { DeactivateAllActiveTools(EToolShutdownType::Cancel); } void UEditorInteractiveToolsContext::TerminateActiveToolsOnLevelChange() { DeactivateAllActiveTools(EToolShutdownType::Cancel); } void UEditorInteractiveToolsContext::PostInvalidation() { InvalidationTimestamp++; } UWorld* UEditorInteractiveToolsContext::GetWorld() const { if (bIsActive && EditorModeManager) { return EditorModeManager->GetWorld(); } return nullptr; } void UEditorInteractiveToolsContext::Tick(FEditorViewportClient* ViewportClient, float DeltaTime) { // invalidate this viewport if it's timestamp is not current const int32* FoundTimestamp = InvalidationMap.Find(ViewportClient); if (ViewportClient && FoundTimestamp == nullptr) { ViewportClient->Invalidate(false, false); InvalidationMap.Add(ViewportClient, InvalidationTimestamp); } if (ViewportClient && FoundTimestamp != nullptr && *FoundTimestamp < InvalidationTimestamp) { ViewportClient->Invalidate(false, false); InvalidationMap[ViewportClient] = InvalidationTimestamp; } if ( PendingToolShutdownType ) { UInteractiveToolsContext::EndTool(EToolSide::Mouse, *PendingToolShutdownType); PendingToolShutdownType.Reset(); } if ( PendingToolToStart ) { if (UInteractiveToolsContext::StartTool(EToolSide::Mouse, *PendingToolToStart)) { SetEditorStateForTool(); } PendingToolToStart.Reset(); } // This Tick() is called for every ViewportClient, however we only want to Tick the ToolManager and GizmoManager // once, for the 'Active'/Focused Viewport, so early-out here if (ViewportClient != EditorModeManager->GetFocusedViewportClient()) { return; } // Cache current camera state from this Viewport in the ContextQueries, which we will use for things like snapping/etc that // is computed by the Tool and Gizmo Tick()s // (This is not necessarily correct for Hover, because we might be Hovering over a different Viewport than the Active one...) if (ViewportClient) { ((FEdModeToolsContextQueriesImpl*)this->QueriesAPI)->CacheCurrentViewState(ViewportClient); } // tick our stuff ToolManager->Tick(DeltaTime); GizmoManager->Tick(DeltaTime); OnTick.Broadcast(DeltaTime); } class FEdModeTempRenderContext : public IToolsContextRenderAPI { public: FPrimitiveDrawInterface* PDI; const FSceneView* SceneView; FViewCameraState ViewCameraState; EViewInteractionState ViewInteractionState; FEdModeTempRenderContext(const FSceneView* View, FViewport* Viewport, FEditorViewportClient* ViewportClient, FPrimitiveDrawInterface* DrawInterface, EViewInteractionState ViewInteractionState) :PDI(DrawInterface), SceneView(View), ViewInteractionState(ViewInteractionState) { CacheCurrentViewState(Viewport, ViewportClient); } virtual FPrimitiveDrawInterface* GetPrimitiveDrawInterface() override { return PDI; } virtual const FSceneView* GetSceneView() override { return SceneView; } virtual FViewCameraState GetCameraState() override { return ViewCameraState; } virtual EViewInteractionState GetViewInteractionState() override { return ViewInteractionState; } void CacheCurrentViewState(FViewport* Viewport, FEditorViewportClient* ViewportClient) { if (!ViewportClient) { return; } FViewportCameraTransform ViewTransform = ViewportClient->GetViewTransform(); ViewCameraState.bIsOrthographic = ViewportClient->IsOrtho(); ViewCameraState.Position = ViewTransform.GetLocation(); ViewCameraState.HorizontalFOVDegrees = ViewportClient->ViewFOV; ViewCameraState.AspectRatio = ViewportClient->AspectRatio; // ViewTransform rotation is only initialized for perspective! if (ViewCameraState.bIsOrthographic == false) { // if using Orbit camera, the rotation in the ViewTransform is not the current camera rotation, it // is set to a different rotation based on the Orbit. So we have to convert back to camera rotation. FRotator ViewRotation = (ViewportClient->bUsingOrbitCamera) ? ViewTransform.ComputeOrbitMatrix().InverseFast().Rotator() : ViewTransform.GetRotation(); ViewCameraState.Orientation = ViewRotation.Quaternion(); } else { // These rotations are based on hardcoded values in EditorViewportClient.cpp, see switches in FEditorViewportClient::CalcSceneView and FEditorViewportClient::Draw switch (ViewportClient->ViewportType) { case LVT_OrthoXY: ViewCameraState.Orientation = FQuat(FRotator(-90.0f, -90.0f, 0.0f)); break; case LVT_OrthoNegativeXY: ViewCameraState.Orientation = FQuat(FRotator(90.0f, 90.0f, 0.0f)); break; case LVT_OrthoXZ: ViewCameraState.Orientation = FQuat(FRotator(0.0f, -90.0f, 0.0f)); break; case LVT_OrthoNegativeXZ: ViewCameraState.Orientation = FQuat(FRotator(0.0f, 90.0f, 0.0f)); break; case LVT_OrthoYZ: ViewCameraState.Orientation = FQuat(FRotator(0.0f, 0.0f, 0.0f)); break; case LVT_OrthoNegativeYZ: ViewCameraState.Orientation = FQuat(FRotator(0.0f, 180.0f, 0.0f)); break; default: ViewCameraState.Orientation = FQuat::Identity; } ViewCameraState.OrthoWorldCoordinateWidth = ViewportClient->GetOrthoUnitsPerPixel(ViewportClient->Viewport) * static_cast(ViewportClient->Viewport->GetSizeXY().X); } ViewCameraState.bIsVR = false; } }; void UEditorInteractiveToolsContext::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) { // skip HitProxy rendering passes if desired if (PDI->IsHitTesting() && bEnableRenderingDuringHitProxyPass == false) { return; } // THIS IS NOT SAFE!! However it appears that (1) it is only possible to get certain info from the EditorViewportClient, // but (2) there is no way to know if a FViewportClient is an FEditorViewportClient. Currently this ::Render() function // is only intended to be called by FEdMode/UEdMode::Render(), and their ::Render() calls are only called by the // FEditorViewportClient, which passes it's own Viewport down. So, this cast should be valid (for now) FEditorViewportClient* ViewportClient = static_cast(Viewport->GetClient()); // Update the currently-hovered scene view information, which GizmoArrowComponent and friends will // use to recalculate their size/visibility/etc. if (GizmoViewContext && ViewportClient == EditorModeManager->GetHoveredViewportClient()) { GizmoViewContext->ResetFromSceneView(*View); } // Render Tool and Gizmos const FEditorViewportClient* Focused = EditorModeManager->GetFocusedViewportClient(); const FEditorViewportClient* Hovered = EditorModeManager->GetHoveredViewportClient(); EViewInteractionState InteractionState = EViewInteractionState::None; if (ViewportClient == Focused ) { InteractionState |= EViewInteractionState::Focused; } if (ViewportClient == Hovered ) { InteractionState |= EViewInteractionState::Hovered; } FEdModeTempRenderContext RenderContext(View, Viewport, ViewportClient, PDI, InteractionState); ToolManager->Render(&RenderContext); GizmoManager->Render(&RenderContext); OnRender.Broadcast(&RenderContext); } void UEditorInteractiveToolsContext::DrawHUD(FViewportClient* ViewportClient,FViewport* Viewport,const FSceneView* View, FCanvas* Canvas) { FEditorViewportClient* EditorViewportClient = static_cast(Viewport->GetClient()); const FViewportClient* Focused = EditorModeManager->GetFocusedViewportClient(); const FViewportClient* Hovered = EditorModeManager->GetHoveredViewportClient(); EViewInteractionState InteractionState = EViewInteractionState::None; if (ViewportClient == Focused ) { InteractionState |= EViewInteractionState::Focused; } if (ViewportClient == Hovered ) { InteractionState |= EViewInteractionState::Hovered; } FEdModeTempRenderContext RenderContext(View, Viewport, EditorViewportClient, nullptr /*PDI*/, InteractionState); ToolManager->DrawHUD(Canvas, &RenderContext); GizmoManager->DrawHUD(Canvas, &RenderContext); OnDrawHUD.Broadcast(Canvas, &RenderContext); } bool UEditorInteractiveToolsContext::ProcessEditDelete() { if (ToolManager->HasAnyActiveTool() == false) { return false; } bool bSkipDelete = false; USelection* SelectedActors = GEditor->GetSelectedActors(); for (int i = 0; i < SelectedActors->Num() && bSkipDelete == false; ++i) { UObject* SelectedActor = SelectedActors->GetSelectedObject(i); // If any of the selected actors are AInternalToolFrameworkActor, we do not want to allow them to be deleted, // as generally this will cause problems for the Tool. if ( Cast(SelectedActor) != nullptr) { bSkipDelete = true; } // If any Components of selected Actors implement UToolFrameworkComponent, we disable delete (for now). // (Currently Sculpt and a few other Modeling Tools attach their preview mesh components to the selected Actor) AActor* Actor = Cast(SelectedActor); if (Actor != nullptr) { const TSet& Components = Actor->GetComponents(); for (const UActorComponent* Component : Components) { if ( Component->Implements() ) { bSkipDelete = true; } } } } return bSkipDelete; } FRay UEditorInteractiveToolsContext::GetRayFromMousePos(FEditorViewportClient* ViewportClient, FViewport* Viewport, int MouseX, int MouseY) { if (!ViewportClient) { return FRay(); } FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues( ViewportClient->Viewport, ViewportClient->GetScene(), ViewportClient->EngineShowFlags).SetRealtimeUpdate(ViewportClient->IsRealtime())); // why SetRealtimeUpdate here?? // this View is deleted by the FSceneViewFamilyContext destructor FSceneView* View = ViewportClient->CalcSceneView(&ViewFamily); FViewportCursorLocation MouseViewportRay(View, ViewportClient, MouseX, MouseY); FVector RayOrigin = MouseViewportRay.GetOrigin(); FVector RayDirection = MouseViewportRay.GetDirection(); // in Ortho views, the RayOrigin appears to be completely arbitrary, in some views it is on the view plane, // others it moves back/forth with the OrthoZoom. Translate by a large amount here in hopes of getting // ray origin "outside" the scene (which is a disaster for numerical precision !! ... ) if (ViewportClient->IsOrtho()) { RayOrigin -= UE_FLOAT_HUGE_DISTANCE * RayDirection; } return FRay(RayOrigin, RayDirection, true); } bool UEditorInteractiveToolsContext::CanStartTool(const FString ToolTypeIdentifier) const { return UInteractiveToolsContext::CanStartTool(EToolSide::Mouse, ToolTypeIdentifier); } bool UEditorInteractiveToolsContext::HasActiveTool() const { return UInteractiveToolsContext::HasActiveTool(EToolSide::Mouse); } FString UEditorInteractiveToolsContext::GetActiveToolName() const { return UInteractiveToolsContext::GetActiveToolName(EToolSide::Mouse); } bool UEditorInteractiveToolsContext::ActiveToolHasAccept() const { return UInteractiveToolsContext::ActiveToolHasAccept(EToolSide::Mouse); } bool UEditorInteractiveToolsContext::CanAcceptActiveTool() const { return UInteractiveToolsContext::CanAcceptActiveTool(EToolSide::Mouse); } bool UEditorInteractiveToolsContext::CanCancelActiveTool() const { return UInteractiveToolsContext::CanCancelActiveTool(EToolSide::Mouse); } bool UEditorInteractiveToolsContext::CanCompleteActiveTool() const { return UInteractiveToolsContext::CanCompleteActiveTool(EToolSide::Mouse); } void UEditorInteractiveToolsContext::StartTool(const FString ToolTypeIdentifier) { FString LocalIdentifier(ToolTypeIdentifier); PendingToolToStart = LocalIdentifier; PostInvalidation(); } void UEditorInteractiveToolsContext::EndTool(EToolShutdownType ShutdownType) { PendingToolShutdownType = ShutdownType; PostInvalidation(); } void UEditorInteractiveToolsContext::Activate() { bIsActive = true; } void UEditorInteractiveToolsContext::Deactivate() { bIsActive = false; } void UEditorInteractiveToolsContext::DeactivateActiveTool(EToolSide WhichSide, EToolShutdownType ShutdownType) { UInteractiveToolsContext::DeactivateActiveTool(WhichSide, ShutdownType); RestoreEditorState(); } void UEditorInteractiveToolsContext::DeactivateAllActiveTools(EToolShutdownType ShutdownType) { UInteractiveToolsContext::DeactivateAllActiveTools(ShutdownType); RestoreEditorState(); } void UEditorInteractiveToolsContext::SetEditorStateForTool() { FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); TSharedPtr LevelEditor = LevelEditorModule.GetFirstLevelEditor(); if (LevelEditor.IsValid()) { TArray> Viewports = LevelEditor->GetViewports(); for (const TSharedPtr& ViewportWindow : Viewports) { if (ViewportWindow.IsValid()) { FEditorViewportClient& Viewport = ViewportWindow->GetAssetViewportClient(); Viewport.EnableOverrideEngineShowFlags([](FEngineShowFlags& Flags) { Flags.SetTemporalAA(false); Flags.SetMotionBlur(false); // disable this as depending on fixed exposure settings the entire scene may turn black //Flags.SetEyeAdaptation(false); }); } } } } void UEditorInteractiveToolsContext::RestoreEditorState() { FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); TSharedPtr LevelEditor = LevelEditorModule.GetFirstLevelEditor(); if (LevelEditor.IsValid()) { TArray> Viewports = LevelEditor->GetViewports(); for (const TSharedPtr& ViewportWindow : Viewports) { if (ViewportWindow.IsValid()) { FEditorViewportClient& ViewportClient = ViewportWindow->GetAssetViewportClient(); ViewportClient.DisableOverrideEngineShowFlags(); // Rebuild the cached hit proxy. The tool may have disabled some viewport items while active (e.g. transform gizmo) and // so the hit proxy cache may be out of date. In the past this has led to, for example, the gizmo being visible but not // clickable when returning from a tool (UE-116888). // TODO: Figure out why the hit proxy is not being rebuilt elsewhere ViewportClient.RequestInvalidateHitProxy(ViewportClient.Viewport); } } } } void UEditorInteractiveToolsContext::OnToolEnded(UInteractiveToolManager* InToolManager, UInteractiveTool* InEndedTool) { RestoreEditorState(); } void UEditorInteractiveToolsContext::OnToolPostBuild(UInteractiveToolManager* InToolManager, EToolSide InSide, UInteractiveTool* InBuiltTool, UInteractiveToolBuilder* InToolBuilder, const FToolBuilderState& ToolState) { // todo: Add any shared tool targets for the mode toolkit } void UEditorInteractiveToolsContext::SetEnableRenderingDuringHitProxyPass(bool bEnabled) { bEnableRenderingDuringHitProxyPass = bEnabled; } void UEditorInteractiveToolsContext::SetForceCombinedGizmoMode(bool bEnabled) { bForceCombinedGizmoMode = bEnabled; } void UEditorInteractiveToolsContext::SetAbsoluteWorldSnappingEnabled(bool bEnabled) { bEnableAbsoluteWorldSnapping = bEnabled; } void UEditorInteractiveToolsContext::SetDeactivateToolsOnPIEStart(bool bDeactivateTools) { bDeactivateOnPIEStart = bDeactivateTools; } void UEditorInteractiveToolsContext::SetDeactivateToolsOnSaveWorld(bool bDeactivateTools) { bDeactivateOnSaveWorld = bDeactivateTools; } void UModeManagerInteractiveToolsContext::Tick(FEditorViewportClient* ViewportClient, float DeltaTime) { UEditorInteractiveToolsContext::Tick(ViewportClient, DeltaTime); for (const TObjectPtr& EdModeContext : EdModeToolsContexts) { EdModeContext->Tick(ViewportClient, DeltaTime); } } void UModeManagerInteractiveToolsContext::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) { UEditorInteractiveToolsContext::Render(View, Viewport, PDI); for (const TObjectPtr& EdModeContext : EdModeToolsContexts) { EdModeContext->Render(View, Viewport, PDI); } } void UModeManagerInteractiveToolsContext::DrawHUD(FViewportClient* ViewportClient, FViewport* Viewport, const FSceneView* View, FCanvas* Canvas) { UEditorInteractiveToolsContext::DrawHUD(ViewportClient, Viewport, View, Canvas); for (const TObjectPtr& EdModeContext : EdModeToolsContexts) { EdModeContext->DrawHUD(ViewportClient, Viewport, View, Canvas); } if (DragToolsBehaviorSource) { DragToolsBehaviorSource->RenderTools(View, Canvas); } } bool UModeManagerInteractiveToolsContext::ProcessEditDelete() { bool bHandled = UEditorInteractiveToolsContext::ProcessEditDelete(); for (const TObjectPtr& EdModeContext : EdModeToolsContexts) { bHandled |= EdModeContext->ProcessEditDelete(); } return bHandled; } void UModeManagerInteractiveToolsContext::DeactivateAllActiveTools(EToolShutdownType ShutdownType) { for (UEdModeInteractiveToolsContext* EdModeContext : EdModeToolsContexts) { EdModeContext->DeactivateAllActiveTools(ShutdownType); } Super::DeactivateAllActiveTools(ShutdownType); } void UModeManagerInteractiveToolsContext::UpdateStateWithoutRoutingInputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) { // Currently, the only internal state we keep is the state of various mouse keys being down. Note // that we don't want to save bPressed or bReleased for them as those are one-time events issued // from InputKey, and shouldn't show up on mouse moves or on other keys being pressed/released. if ((Event == IE_Pressed || Event == IE_Released) && Key.IsMouseButton()) { if (Key.IsMouseButton()) { if (Key == EKeys::LeftMouseButton) { CurrentMouseState.Mouse.Left.bDown = (Event == IE_Pressed); } else if (Key == EKeys::MiddleMouseButton) { CurrentMouseState.Mouse.Middle.bDown = (Event == IE_Pressed); } else if (Key == EKeys::RightMouseButton) { CurrentMouseState.Mouse.Right.bDown = (Event == IE_Pressed); } } } } bool UModeManagerInteractiveToolsContext::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) { #ifdef ENABLE_DEBUG_PRINTING if (Event == IE_Pressed) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("PRESSED EVENT")); } else if (Event == IE_Released) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("RELEASED EVENT")); } else if (Event == IE_Repeat) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("REPEAT EVENT")); } else if (Event == IE_Axis) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("AXIS EVENT")); } else if (Event == IE_DoubleClick) { UE_LOG(LogInteractiveToolsContext, Warning, TEXT("DOUBLECLICK EVENT")); } #endif // Update the current state, then route result UpdateStateWithoutRoutingInputKey(ViewportClient, Viewport, Key, Event); if (Event == IE_Pressed || Event == IE_Released || Event == IE_DoubleClick) { if (Key.IsMouseButton()) { bool bIsLeftMouse = (Key == EKeys::LeftMouseButton); bool bIsMiddleMouse = (Key == EKeys::MiddleMouseButton); bool bIsRightMouse = (Key == EKeys::RightMouseButton); if (bIsLeftMouse || bIsMiddleMouse || bIsRightMouse) { // Currently, we don't capture mouse clicks that start with Alt being down because we want // Alt camera manipulation to have priority over tools. So, we let those inputs pass on up // to wherever they get handled. // Someday these kinds of prioritizations will be handled by having camera manipulation be // in a common input router so that behavior priorities can determine the ordering. if (BypassAltModifier()) { if (ViewportClient && ViewportClient->IsAltPressed() && InputRouter->HasActiveMouseCapture() == false) { return false; } } FInputDeviceState InputState = CurrentMouseState; InputState.InputDevice = EInputDevices::Mouse; FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); InputState.SetModifierKeyStates( ModifierKeys.IsShiftDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsControlDown(), ModifierKeys.IsCommandDown()); // DoubleClick replaces a Press event. Include DoubleClick in Pressed/Down events so // that it can still be processed by single click behaviors. For explicit double click // handling, clients can listen for the DoubleClick event with a higher capture priority. const bool bIsPressed = (Event == IE_Pressed || Event == IE_DoubleClick); const bool bIsReleased = (Event == IE_Released); const bool bIsDoubleClick = (Event == IE_DoubleClick); if (bIsLeftMouse) { InputState.Mouse.Left.SetStates(bIsPressed, bIsPressed, bIsReleased, bIsDoubleClick); } else if (bIsMiddleMouse) { InputState.Mouse.Middle.SetStates(bIsPressed, bIsPressed, bIsReleased, bIsDoubleClick); } else { InputState.Mouse.Right.SetStates(bIsPressed, bIsPressed, bIsReleased, bIsDoubleClick); } if (InputRouter->PostInputEvent(InputState)) { return true; } } else if (Key == EKeys::MouseScrollUp || Key == EKeys::MouseScrollDown) { // Note that we get two events for each scroll- an IE_Pressed, and IE_Released. // We pass both of these in, though only the first one will have WheelDelta set. // If a behavior captures the mouse wheel interaction, the second event will give // it the opportunity to immediately release capture. Not passing in the second // event would probably be ok too- capture would get released on next hover event. FInputDeviceState InputState = CurrentMouseState; InputState.InputDevice = EInputDevices::Mouse; FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); InputState.SetModifierKeyStates( ModifierKeys.IsShiftDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsControlDown(), ModifierKeys.IsCommandDown()); InputState.Mouse.WheelDelta = (Event != IE_Pressed) ? 0.f : (Key == EKeys::MouseScrollUp) ? 1.f : -1.f; return InputRouter->PostInputEvent(InputState); } } else if (Key.IsGamepadKey()) { // not supported yet } else if (Key.IsTouch()) { // not supported yet } else if (Key.IsAnalog()) { // not supported yet } else // is this definitely a keyboard key? { FInputDeviceState InputState; InputState.InputDevice = EInputDevices::Keyboard; FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); InputState.SetModifierKeyStates( ModifierKeys.IsShiftDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsControlDown(), ModifierKeys.IsCommandDown()); InputState.Keyboard.ActiveKey.Button = Key; bool bPressed = (Event == IE_Pressed); InputState.Keyboard.ActiveKey.SetStates(bPressed, bPressed, !bPressed); return InputRouter->PostInputEvent(InputState); } } return false; } bool UModeManagerInteractiveToolsContext::MouseEnter(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) { #ifdef ENABLE_DEBUG_PRINTING UE_LOG(LogInteractiveToolsContext, Warning, TEXT("MOUSE ENTER")); #endif CurrentMouseState.Mouse.Position2D = FVector2D(x, y); CurrentMouseState.Mouse.WorldRay = GetRayFromMousePos(ViewportClient, Viewport, x, y); return false; } bool UModeManagerInteractiveToolsContext::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) { #ifdef ENABLE_DEBUG_PRINTING UE_LOG(LogInteractiveToolsContext, Warning, TEXT("HOVER %p"), ViewportClient); #endif CurrentMouseState.Mouse.Position2D = FVector2D(x, y); CurrentMouseState.Mouse.WorldRay = GetRayFromMousePos(ViewportClient, Viewport, x, y); FInputDeviceState InputState = CurrentMouseState; InputState.InputDevice = EInputDevices::Mouse; FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); InputState.SetModifierKeyStates( ModifierKeys.IsShiftDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsControlDown(), ModifierKeys.IsCommandDown()); if (InputRouter->HasActiveMouseCapture()) { // TODO: This should no longer be necessary: test and remove. // This state occurs if InputBehavior did not release capture on mouse release. // UMultiClickSequenceInputBehavior does this, eg for multi-click draw-polygon sequences. // It's not ideal though and maybe would be better done via multiple captures + hover...? InputRouter->PostInputEvent(InputState); } else { InputRouter->PostHoverInputEvent(InputState); } return false; } bool UModeManagerInteractiveToolsContext::LostFocus(FEditorViewportClient* InViewportClient, FViewport* Viewport) const { InputRouter->ForceTerminateAll(); return false; } bool UModeManagerInteractiveToolsContext::MouseLeave(FEditorViewportClient* ViewportClient, FViewport* Viewport) { #ifdef ENABLE_DEBUG_PRINTING UE_LOG(LogInteractiveToolsContext, Warning, TEXT("MOUSE LEAVE")); #endif return false; } bool UModeManagerInteractiveToolsContext::StartTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) { bIsTrackingMouse = InputRouter->HasActiveMouseCapture(); return bIsTrackingMouse; } bool UModeManagerInteractiveToolsContext::CapturedMouseMove(FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY) { FVector2D OldPosition = CurrentMouseState.Mouse.Position2D; CurrentMouseState.Mouse.Position2D = FVector2D(InMouseX, InMouseY); CurrentMouseState.Mouse.WorldRay = GetRayFromMousePos(InViewportClient, InViewport, InMouseX, InMouseY); if (InputRouter->HasActiveMouseCapture()) { #ifdef ENABLE_DEBUG_PRINTING UE_LOG(LogInteractiveToolsContext, Warning, TEXT("CAPTURED MOUSE MOVE")); #endif FInputDeviceState InputState = CurrentMouseState; InputState.InputDevice = EInputDevices::Mouse; FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); InputState.SetModifierKeyStates( ModifierKeys.IsShiftDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsControlDown(), ModifierKeys.IsCommandDown()); InputState.Mouse.Delta2D = CurrentMouseState.Mouse.Position2D - OldPosition; InputRouter->PostInputEvent(InputState); return true; } return false; } bool UModeManagerInteractiveToolsContext::EndTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) { if (bIsTrackingMouse) { // If the input router captured the mouse input, we need to invalidate the viewport client here, since the mouse delta tracker's end tracking will not be called. constexpr bool bForceChildViewportRedraw = true; constexpr bool bInvalidateHitProxies = true; if (InViewportClient) { InViewportClient->Invalidate(bForceChildViewportRedraw, bInvalidateHitProxies); } bIsTrackingMouse = false; return true; } return false; } bool UModeManagerInteractiveToolsContext::GetCursor(EMouseCursor::Type& OutCursor) const { UToolsContextCursorAPI* ToolsContextCursorAPI = ToolManager->GetContextObjectStore()->FindContext(); if (ToolsContextCursorAPI && ToolsContextCursorAPI->IsCursorOverridden()) { OutCursor = ToolsContextCursorAPI->GetCurrentCursorOverride(); return true; } if (bIsTrackingMouse) { OutCursor = EMouseCursor::SlashedCircle; return true; } return false; } FRay UModeManagerInteractiveToolsContext::GetLastWorldRay() const { return CurrentMouseState.Mouse.WorldRay; } void UModeManagerInteractiveToolsContext::Initialize(IToolsContextQueriesAPI* QueriesAPIIn, IToolsContextTransactionsAPI* TransactionsAPIIn) { Super::Initialize(QueriesAPIIn, TransactionsAPIIn); BeginPIEDelegateHandle = FEditorDelegates::BeginPIE.AddLambda([this](bool bSimulating) { TerminateActiveToolsOnPIEStart(); }); PreSaveWorldDelegateHandle = FEditorDelegates::PreSaveWorldWithContext.AddLambda([this](UWorld* World, FObjectPreSaveContext ObjectSaveContext) { TerminateActiveToolsOnSaveWorld(); }); FLevelEditorModule& LevelEditor = FModuleManager::LoadModuleChecked("LevelEditor"); WorldTearDownDelegateHandle = LevelEditor.OnMapChanged().AddLambda([this](UWorld* World, EMapChangeType ChangeType) { if (ChangeType == EMapChangeType::TearDownWorld) { TerminateActiveToolsOnWorldTearDown(); } }); // Tools frequently spawn temporary objects in the level for visualization, gizmos, etc, so having a level be // removed from the world puts the tools at risk of letting those objects be garbage collected and cause crashes FWorldDelegates::PreLevelRemovedFromWorld.AddWeakLambda(this, [this](ULevel*, UWorld*) { TerminateActiveToolsOnLevelChange(); }); ToolManager->OnToolEnded.AddUObject(this, &UModeManagerInteractiveToolsContext::OnToolEnded); ToolManager->OnToolPostBuild.AddUObject(this, &UModeManagerInteractiveToolsContext::OnToolPostBuild); // if viewport clients change we will discard our overrides as we aren't sure what happened ViewportClientListChangedHandle = GEditor->OnViewportClientListChanged().AddLambda([this]() { RestoreEditorState(); }); GetParentEditorModeManager()->OnEditorModeIDChanged().AddUObject(this, &UModeManagerInteractiveToolsContext::OnEditorModeChanged); // Delegates used to handle SelectionMode.EnableITFTools CVAR changes UE::Editor::DragTools::OnEditorDragToolsActivated().AddUObject(this, &UModeManagerInteractiveToolsContext::RegisterDragTools); UE::Editor::DragTools::OnEditorDragToolsDeactivated().AddUObject(this, &UModeManagerInteractiveToolsContext::UnregisterDragTools); } void UModeManagerInteractiveToolsContext::Shutdown() { UnregisterDragTools(); GetParentEditorModeManager()->OnEditorModeIDChanged().RemoveAll(this); UE::Editor::DragTools::OnEditorDragToolsActivated().RemoveAll(this); UE::Editor::DragTools::OnEditorDragToolsDeactivated().RemoveAll(this); if (ToolManager) { ToolManager->OnToolPostBuild.RemoveAll(this); ToolManager->OnToolEnded.RemoveAll(this); } if (FLevelEditorModule* LevelEditor = FModuleManager::GetModulePtr("LevelEditor")) { LevelEditor->OnMapChanged().Remove(WorldTearDownDelegateHandle); FEditorDelegates::BeginPIE.Remove(BeginPIEDelegateHandle); FEditorDelegates::PreSaveWorldWithContext.Remove(PreSaveWorldDelegateHandle); GEditor->OnViewportClientListChanged().Remove(ViewportClientListChangedHandle); } FWorldDelegates::PreLevelRemovedFromWorld.RemoveAll(this); Super::Shutdown(); } UEdModeInteractiveToolsContext* UModeManagerInteractiveToolsContext::CreateNewChildEdModeToolsContext() { UEdModeInteractiveToolsContext* NewModeToolsContext = NewObject(GetTransientPackage(), UEdModeInteractiveToolsContext::StaticClass(), NAME_None, RF_Transient); NewModeToolsContext->InitializeContextFromModeManagerContext(this); return NewModeToolsContext; } bool UModeManagerInteractiveToolsContext::OnChildEdModeActivated(UEdModeInteractiveToolsContext* ChildToolsContext) { if (!ensureMsgf(EdModeToolsContexts.Find(ChildToolsContext), TEXT("Child ToolsContext was already found!"))) { return false; } EdModeToolsContexts.Add(ChildToolsContext); ChildToolsContext->Activate(); return true; } bool UModeManagerInteractiveToolsContext::OnChildEdModeDeactivated(UEdModeInteractiveToolsContext* ChildToolsContext) { for (int32 k = 0; k < EdModeToolsContexts.Num(); ++k) { if (EdModeToolsContexts[k] == ChildToolsContext) { ChildToolsContext->DeactivateAllActiveTools(EToolShutdownType::Cancel); ChildToolsContext->Deactivate(); EdModeToolsContexts.RemoveAt(k); return true; } } ensureMsgf(false, TEXT("Child ToolsContext was not found! It may have already been removed")); return false; } void UModeManagerInteractiveToolsContext::SetBypassAltModifier(bool bInBypassAltModifier) { bBypassAltModifier = bInBypassAltModifier; } void UModeManagerInteractiveToolsContext::RegisterDragTools() { FEditorModeTools* EditorModeTools = GetParentEditorModeManager(); if (!EditorModeTools) { return; } // Ignore uninitialized Mode Tools missing a Viewport Client if (!EditorModeTools->GetFocusedViewportClient()) { return; } // This Mode Manager does not require Drag Tools if (!UsesDragTools()) { return; } // Not initialized yet if (!DragToolsBehaviorSource) { // Alt modifier input required by Drag Tools, but currently skipped by ITF in Tool Context. // Let's make sure Alt keys are processed SetBypassAltModifier(false); DragToolsBehaviorSource = NewObject(); DragToolsBehaviorSource->Initialize(this); DragToolsBehaviorSource->RegisterSource(); } } void UModeManagerInteractiveToolsContext::UnregisterDragTools() { if (DragToolsBehaviorSource) { // Revert to previous state, with Alt Modifier not being processed SetBypassAltModifier(true); DragToolsBehaviorSource->DeregisterSource(); DragToolsBehaviorSource = nullptr; } } void UModeManagerInteractiveToolsContext::OnEditorModeChanged(const FEditorModeID& InModeID, bool bInIsEnteringMode) { if (!UE::Editor::DragTools::UseEditorDragTools()) { return; } if (bInIsEnteringMode) { RegisterDragTools(); } else { UnregisterDragTools(); } } void UEdModeInteractiveToolsContext::InitializeContextFromModeManagerContext(UModeManagerInteractiveToolsContext* ModeManagerToolsContext) { check(ModeManagerToolsContext != nullptr); check(ModeManagerToolsContext->InputRouter != nullptr); FEditorModeTools* ModeManager = ModeManagerToolsContext->GetParentEditorModeManager(); check(ModeManager != nullptr); ParentModeManagerToolsContext = ModeManagerToolsContext; SetCreateContextStoreFunc([this, ModeManagerToolsContext](const FContextInitInfo& ContextInfo) { UContextObjectStore* NewContextStore = NewObject(ModeManagerToolsContext->ContextObjectStore); return NewContextStore; }); InitializeContextWithEditorModeManager(ModeManager, ModeManagerToolsContext->InputRouter); } FRay UEdModeInteractiveToolsContext::GetLastWorldRay() const { return ParentModeManagerToolsContext->GetLastWorldRay(); }