// Copyright 2020 Phyronnaz #include "VoxelEditorToolsPanel.h" #include "VoxelTools/VoxelToolManager.h" #include "VoxelTools/Tools/VoxelTool.h" #include "VoxelTools/Tools/VoxelFlattenTool.h" #include "VoxelTools/Tools/VoxelLevelTool.h" #include "VoxelTools/Tools/VoxelMeshTool.h" #include "VoxelTools/Tools/VoxelRevertTool.h" #include "VoxelTools/Tools/VoxelSmoothTool.h" #include "VoxelTools/Tools/VoxelSphereTool.h" #include "VoxelTools/Tools/VoxelSurfaceTool.h" #include "VoxelTools/Tools/VoxelTrimTool.h" #include "VoxelWorld.h" #include "VoxelToolsCommands.h" #include "VoxelDelegateHelpers.h" #include "VoxelScopedTransaction.h" #include "ActorFactoryVoxelWorld.h" #include "VoxelUtilities/VoxelConfigUtilities.h" #include "EngineUtils.h" #include "Editor.h" #include "EditorViewportClient.h" #include "DetailLayoutBuilder.h" #include "VariablePrecisionNumericInterface.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" #include "Misc/ConfigCacheIni.h" #include "Misc/MessageDialog.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SSpinBox.h" #include "Slate/SceneViewport.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Application/SlateApplication.h" #include "HAL/PlatformApplicationMisc.h" static const FString ToolConfigSectionName = "VoxelTool"; FVoxelEditorToolsPanel::FVoxelEditorToolsPanel() : Widget(SNew(SBox)) { } FVoxelEditorToolsPanel::~FVoxelEditorToolsPanel() { if (ToolManager) { // Always save before exiting the panel (eg shortcuts do not dirty saves) FVoxelConfigUtilities::SaveConfig(&ToolManager->GetSharedConfig(), ToolConfigSectionName); if (auto* Tool = ToolManager->GetActiveTool()) { FVoxelConfigUtilities::SaveConfig(Tool, ToolConfigSectionName); } ToolManager->SetActiveTool(nullptr); } } void FVoxelEditorToolsPanel::Init(const TSharedPtr& CommandListOverride) { CommandList = CommandListOverride; if (!CommandList.IsValid()) { CommandList = MakeShared(); } FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs(false, false, false, FDetailsViewArgs::HideNameArea); DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Automatic; ToolManager = NewObject(GetTransientPackage(), NAME_None, RF_Transient | RF_Transactional); ToolManager->CreateDefaultTools(true); ToolManager->GetSharedConfig().RefreshDetails.AddSP(this, &FVoxelEditorToolsPanel::RefreshDetails); ToolManager->GetSharedConfig().RegisterTransaction.AddLambda([](FName Name, AVoxelWorld* VoxelWorld) { FVoxelScopedTransaction Transaction(VoxelWorld, Name, EVoxelChangeType::Edit); }); ToolsOptions.Reset(); for (auto* Tool : ToolManager->GetTools()) { FVoxelConfigUtilities::LoadConfig(Tool, ToolConfigSectionName); ToolsOptions.Add(MakeSharedCopy(Tool->GetClass())); } FVoxelConfigUtilities::LoadConfig(&ToolManager->GetSharedConfig(), ToolConfigSectionName); const auto IsPropertyVisibleDelegate = MakeWeakPtrDelegate(this, [=](const FPropertyAndParent& PropertyAndParent) { const auto& ParentProperties = PropertyAndParent.ParentProperties; return IsPropertyVisible(PropertyAndParent.Property, ParentProperties); }); SharedConfigDetailsPanel = PropertyEditorModule.CreateDetailView(DetailsViewArgs); SharedConfigDetailsPanel->SetObject(&ToolManager->GetSharedConfig()); SharedConfigDetailsPanel->SetIsPropertyVisibleDelegate(IsPropertyVisibleDelegate); SharedConfigDetailsPanel->OnFinishedChangingProperties().AddWeakLambda(ToolManager, [=](auto&) { FVoxelConfigUtilities::SaveConfig(&ToolManager->GetSharedConfig(), ToolConfigSectionName); }); ToolDetailsPanel = PropertyEditorModule.CreateDetailView(DetailsViewArgs); ToolDetailsPanel->SetObject(ToolManager->GetActiveTool()); ToolDetailsPanel->SetIsPropertyVisibleDelegate(IsPropertyVisibleDelegate); ToolDetailsPanel->OnFinishedChangingProperties().AddWeakLambda(ToolManager, [=](auto&) { if (auto* Tool = ToolManager->GetActiveTool()) { FVoxelConfigUtilities::SaveConfig(Tool, ToolConfigSectionName); } }); TSharedPtr ToolBarsVerticalBox; TSharedPtr CustomToolBarsVerticalBox; Widget->SetContent( SNew(SScrollBox) + SScrollBox::Slot() [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(FMargin(3, 5)) .AutoHeight() [ SNew(SBorder) .Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FVoxelEditorToolsPanel::GetAddVoxelWorldVisibility))) .BorderBackgroundColor(FLinearColor::Red) .BorderImage(FCoreStyle::Get().GetBrush("ErrorReporting.Box")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Padding(FMargin(3, 0)) [ SNew(SVerticalBox) + SVerticalBox::Slot() [ SNew(STextBlock) .ColorAndOpacity(FLinearColor::White) .Font(FCoreStyle::GetDefaultFontStyle("Regular",12)) .Text(VOXEL_LOCTEXT("No Voxel World in the scene!")) ] + SVerticalBox::Slot() .Padding(FMargin(0, 5)) [ SNew(SButton) .OnClicked(FOnClicked::CreateSP(this, &FVoxelEditorToolsPanel::AddVoxelWorld)) [ SNew(STextBlock) .ColorAndOpacity(FLinearColor::Black) .Font(FCoreStyle::GetDefaultFontStyle("Regular",12)) .Text(VOXEL_LOCTEXT("Add Voxel World")) .Justification(ETextJustify::Center) ] ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(2) [ SAssignNew(ToolBarsVerticalBox, SVerticalBox) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Center) [ SNew(SButton) .Text(VOXEL_LOCTEXT("Browse to tool BP")) .Visibility_Lambda([=]() { if (ToolManager->GetActiveTool() && !ToolManager->GetActiveTool()->GetClass()->HasAnyClassFlags(CLASS_Native)) { return EVisibility::Visible; } else { return EVisibility::Collapsed; } }) .OnClicked_Lambda([=]() { TArray Objects = { ToolManager->GetActiveTool()->GetClass() }; GEditor->SyncBrowserToObjects(Objects); return FReply::Handled(); }) ] + SVerticalBox::Slot() .AutoHeight() [ SharedConfigDetailsPanel.ToSharedRef() ] + SVerticalBox::Slot() .FillHeight(1) [ ToolDetailsPanel.ToSharedRef() ] ]); TArray ToolBarBuilders; TArray CustomToolBarBuilders; BuildToolBars(ToolBarBuilders, CustomToolBarBuilders); BindCommands(); for (auto& ToolBarBuilder : ToolBarBuilders) { ToolBarsVerticalBox->AddSlot() .AutoHeight() .HAlign(HAlign_Center) [ ToolBarBuilder.MakeWidget() ]; } ToolBarsVerticalBox->AddSlot() .AutoHeight() [ SAssignNew(CustomToolBarsVerticalBox, SVerticalBox) .Visibility_Lambda([=]() { return bShowCustomTools ? EVisibility::Visible : EVisibility::Collapsed; }) ]; GConfig->GetBool(TEXT("VoxelEditorToolsPanel"), TEXT("ShowCustomTools"), bShowCustomTools, GEditorPerProjectIni); ToolBarsVerticalBox->AddSlot() .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.AdvancedDropdownBorder")) .Padding(FMargin(0.0f, 3.0f, 16.f, 0.0f)) [ SAssignNew(ExpanderButton, SButton) .ButtonStyle(FAppStyle::Get(), "NoBorder") .HAlign(HAlign_Center) .ContentPadding(2) .OnClicked_Lambda([=]() { bShowCustomTools = !bShowCustomTools; GConfig->SetBool(TEXT("VoxelEditorToolsPanel"), TEXT("ShowCustomTools"), bShowCustomTools, GEditorPerProjectIni); return FReply::Handled(); }) .ToolTipText_Lambda([=]() { return bShowCustomTools ? VOXEL_LOCTEXT("Hide custom tools") : VOXEL_LOCTEXT("Show custom tools"); }) [ SNew(SImage) .Image_Lambda([=]() { if (ExpanderButton->IsHovered()) { return bShowCustomTools ? FAppStyle::GetBrush("DetailsView.PulldownArrow.Up.Hovered") : FAppStyle::GetBrush("DetailsView.PulldownArrow.Down.Hovered"); } else { return bShowCustomTools ? FAppStyle::GetBrush("DetailsView.PulldownArrow.Up") : FAppStyle::GetBrush("DetailsView.PulldownArrow.Down"); } }) ] ] ]; CustomToolBarsVerticalBox->AddSlot() .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryMiddle")) .Padding(FMargin(0.0f, 3.0f, 16.f, 0.0f)) [ SNew(SImage) .Image(FAppStyle::GetBrush("DetailsView.AdvancedDropdownBorder.Open")) ] ]; for (auto& ToolBarBuilder : CustomToolBarBuilders) { CustomToolBarsVerticalBox->AddSlot() .AutoHeight() .HAlign(HAlign_Center) [ ToolBarBuilder.MakeWidget() ]; } FString ActiveTool; GConfig->GetString(TEXT("VoxelEditorToolsPanel"), TEXT("ActiveTool"), ActiveTool, GEditorPerProjectIni); SetActiveTool(*ToolsOptions[0]); for (auto& It : ToolsOptions) { if ((**It).GetName() == ActiveTool) { SetActiveTool(*It); } } // Show tooltip dialog bool bShowToolsShortcutsDialog = true; if (!GConfig->GetBool( TEXT("VoxelEditorToolsPanel"), TEXT("ShowToolsShortcutsDialog"), bShowToolsShortcutsDialog, GEditorPerProjectIni) || bShowToolsShortcutsDialog) { const auto Result = FMessageDialog::Open(EAppMsgType::YesNo, VOXEL_LOCTEXT( "The voxel plugin tools shortcuts are the following:\n" "1-8 (alphanum): select tools\n" "[ ]: decrease/increase brush size\n" "< >: decrease/increase brush strength\n\n" "You can configure these shortcuts at any time in Edit/Editor Preferences/Keyboard Shortcuts/Voxel\n\n" "Continue to show this popup?")); if (Result == EAppReturnType::No) { GConfig->SetBool( TEXT("VoxelEditorToolsPanel"), TEXT("ShowToolsShortcutsDialog"), false, GEditorPerProjectIni); } } } void FVoxelEditorToolsPanel::CustomizeToolbar(FToolBarBuilder& ToolBarBuilder) { } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelEditorToolsPanel::AddReferencedObjects(FReferenceCollector& Collector) { Collector.AddReferencedObject(ToolManager); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelEditorToolsPanel::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) { } void FVoxelEditorToolsPanel::Tick(FEditorViewportClient* ViewportClient, float DeltaTime) { auto* World = ViewportClient->GetWorld(); if (!ensure(World)) return; if (!LastWorld.IsValid()) { // Toggle voxel worlds if none are created on first tick const auto ToggleVoxelWorld = [&]() { for (auto* VoxelWorld : TActorRange(World)) { if (VoxelWorld->IsCreated()) { return; } } for (auto* VoxelWorld : TActorRange(World)) { if (!VoxelWorld->IsCreated()) { VoxelWorld->Toggle(); } } }; ToggleVoxelWorld(); } LastWorld = ViewportClient->GetWorld(); if (ToolManager) { auto* Tool = ToolManager->GetActiveTool(); if (Tool) { check(!ViewportClientForDeproject); TGuardValue Guard(ViewportClientForDeproject, ViewportClient); FViewport* Viewport = ViewportClient->Viewport; TUniquePtr ViewFamily; FSceneView* SceneView = GetSceneView(ViewFamily); if (Viewport && SceneView) { TMap Keys; Keys.Add(FVoxelToolKeys::AlternativeMode, ViewportClient->Viewport->KeyState(EKeys::LeftShift) || ViewportClient->Viewport->KeyState(EKeys::RightShift)); const bool bClick = ViewportClient->Viewport->KeyState(EKeys::LeftMouseButton); FVoxelToolTickData TickData; { auto* SceneViewport = static_cast(Viewport); const auto& Geometry = SceneViewport->GetCachedGeometry(); FVector2D MousePosition = FSlateApplication::Get().GetCursorPos(); if (Geometry.IsUnderLocation(MousePosition) || bClick) // Don't modify the position if editing { MousePosition = Geometry.AbsoluteToLocal(MousePosition); } else { MousePosition = Geometry.Size / 2; } // Make sure to use the window scale factor and not the scale under the cursor, // as the window DPI scale is uniform MousePosition *= ViewportClient->GetDPIScale(); TickData.MousePosition = FVector2D(MousePosition); TickData.CameraViewDirection = SceneView->ViewMatrices.GetInvViewMatrix().TransformVector(FVector(0, 0, 1)); TickData.bEdit = bClick; TickData.Keys = Keys; TickData.Axes.Add(FVoxelToolAxes::BrushSize, BrushSizeDelta); TickData.Axes.Add(FVoxelToolAxes::Falloff, FalloffDelta); TickData.Axes.Add(FVoxelToolAxes::Strength, StrengthDelta); BrushSizeDelta = 0; FalloffDelta = 0; StrengthDelta = 0; const auto DeprojectLambda = [this, WeakPtr = MakeWeakPtr(this)](const FVector2D& InScreenPosition, FVector& OutWorldPosition, FVector& OutWorldDirection) { return WeakPtr.IsValid() && Deproject(InScreenPosition, OutWorldPosition, OutWorldDirection); }; TickData.Init(DeprojectLambda); } Tool->AdvancedTick(World, TickData); if (LastVoxelWorld != Tool->GetVoxelWorld()) { LastVoxelWorld = Tool->GetVoxelWorld(); SharedConfigDetailsPanel->ForceRefresh(); ToolDetailsPanel->ForceRefresh(); } ViewportClientForDeproject = nullptr; } } } TimeUntilNextGC -= DeltaTime; if (TimeUntilNextGC <= 0) { TimeUntilNextGC = 30; LOG_VOXEL(Log, TEXT("Forcing GC")); GEngine->ForceGarbageCollection(true); } } void FVoxelEditorToolsPanel::HandleClick(FEditorViewportClient* ViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) { } bool FVoxelEditorToolsPanel::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) { if (Event != IE_Released && CommandList->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false/*Event == IE_Repeat*/)) { return true; } if (Key == EKeys::LeftMouseButton) { return true; } else if (Key == EKeys::LeftShift || Key == EKeys::RightShift) { return true; } else { return false; } } bool FVoxelEditorToolsPanel::InputAxis(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, float Delta, float DeltaTime) { return false; } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// EVisibility FVoxelEditorToolsPanel::GetAddVoxelWorldVisibility() const { if (!LastWorld.IsValid()) { return EVisibility::Collapsed; } for (auto* VoxelWorld : TActorRange(LastWorld.Get())) { return EVisibility::Collapsed; } return EVisibility::Visible; } FReply FVoxelEditorToolsPanel::AddVoxelWorld() const { if (!LastWorld.IsValid()) { return FReply::Handled(); } auto* Factory = NewObject(); if (ensure(Factory)) { Factory->CreateActor(nullptr, LastWorld->GetLevel(0), FTransform::Identity); } return FReply::Handled(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelEditorToolsPanel::RefreshDetails() const { SharedConfigDetailsPanel->ForceRefresh(); ToolDetailsPanel->ForceRefresh(); } bool FVoxelEditorToolsPanel::IsPropertyVisible(const FProperty& Property, const TArray& ParentProperties, int32 ParentPropertyIndex) const { if (Property.HasMetaData(STATIC_FNAME("HideInPanel"))) { return false; } if (Property.HasMetaData(STATIC_FNAME("PaintMaterial"))) { auto* ActiveTool = ToolManager->GetActiveTool(); if (ActiveTool && !ActiveTool->bShowPaintMaterial) { return false; } } if (Property.HasMetaData(STATIC_FNAME("ShowForMaterialConfigs"))) { const FString& ShowForMaterialConfigs = Property.GetMetaData(STATIC_FNAME("ShowForMaterialConfigs")); if (LastVoxelWorld.IsValid()) { const FString MaterialConfig = StaticEnum()->GetNameStringByValue(int64(LastVoxelWorld->MaterialConfig)); if (!ShowForMaterialConfigs.Contains(MaterialConfig)) { return false; } } } // TODO if (ParentProperties.IsValidIndex(ParentPropertyIndex)) { return IsPropertyVisible(*ParentProperties[ParentPropertyIndex], ParentProperties, ParentPropertyIndex + 1); } else { return true; } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelEditorToolsPanel::SetActiveTool(UClass* ToolClass) { ToolManager->SetActiveToolByClass(ToolClass); ToolDetailsPanel->SetObject(ToolManager->GetActiveTool()); SharedConfigDetailsPanel->ForceRefresh(); if (ComboBox) { ComboBox->SetSelectedItem(*ToolsOptions.FindByPredicate([&](auto& ClassPtr) { return *ClassPtr == ToolClass; })); } GConfig->SetString(TEXT("VoxelEditorToolsPanel"), TEXT("ActiveTool"), ToolClass ? *ToolClass->GetName() : TEXT(""), GEditorPerProjectIni); } bool FVoxelEditorToolsPanel::IsToolActive(UClass* ToolClass) const { auto* Tool = ToolManager->GetActiveTool(); return Tool && Tool->GetClass() == ToolClass; } void FVoxelEditorToolsPanel::BuildToolBars(TArray& OutToolBars, TArray& OutCustomToolBars) { const auto& Commands = FVoxelToolsCommands::Get(); int32 NumTools = 0; const auto GetToolBar = [&]() { if (NumTools % 4 == 0) { OutToolBars.Emplace(CommandList, FMultiBoxCustomization("VoxelEditorTools")); } NumTools++; return OutToolBars.Last(); }; const auto GetUIAction = [&](auto* Class) { return FUIAction( FExecuteAction::CreateSP(this, &FVoxelEditorToolsPanel::SetActiveTool, Class), FCanExecuteAction::CreateLambda([]() { return true; }), FIsActionChecked::CreateSP(this, &FVoxelEditorToolsPanel::IsToolActive, Class)); }; const auto AddTool = [&](auto& Command, auto* Class) { CommandList->MapAction(Command, GetUIAction(Class)); }; AddTool(Commands.FlattenTool, UVoxelFlattenTool::StaticClass()); AddTool(Commands.LevelTool, UVoxelLevelTool::StaticClass()); AddTool(Commands.MeshTool, UVoxelMeshTool::StaticClass()); AddTool(Commands.SmoothTool, UVoxelSmoothTool::StaticClass()); AddTool(Commands.SphereTool, UVoxelSphereTool::StaticClass()); AddTool(Commands.SurfaceTool, UVoxelSurfaceTool::StaticClass()); AddTool(Commands.RevertTool, UVoxelRevertTool::StaticClass()); AddTool(Commands.TrimTool, UVoxelTrimTool::StaticClass()); GetToolBar().AddToolBarButton(Commands.SurfaceTool); GetToolBar().AddToolBarButton(Commands.SmoothTool); GetToolBar().AddToolBarButton(Commands.MeshTool); GetToolBar().AddToolBarButton(Commands.SphereTool); GetToolBar().AddToolBarButton(Commands.FlattenTool); GetToolBar().AddToolBarButton(Commands.LevelTool); GetToolBar().AddToolBarButton(Commands.TrimTool); GetToolBar().AddToolBarButton(Commands.RevertTool); int32 NumCustomTools = 0; const auto GetCustomToolBar = [&]() { if (NumCustomTools % 4 == 0) { OutCustomToolBars.Emplace(CommandList, FMultiBoxCustomization("VoxelEditorTools")); } NumCustomTools++; return OutCustomToolBars.Last(); }; for (auto* Tool : ToolManager->GetTools()) { auto* Class = Tool->GetClass(); if (Class->GetPathName().StartsWith(TEXT("/Script/Voxel."))) { // Builtin tools continue; } GetCustomToolBar().AddToolBarButton( GetUIAction(Class), NAME_None, FText::FromName(Tool->GetToolName()), Tool->ToolTip, FSlateIcon("VoxelStyle", "VoxelTools.Surface"), EUserInterfaceActionType::ToggleButton); } } void FVoxelEditorToolsPanel::BindCommands() { const auto& Commands = FVoxelToolsCommands::Get(); CommandList->MapAction(Commands.IncreaseBrushSize, MakeWeakPtrDelegate(this, [&](){ BrushSizeDelta++; })); CommandList->MapAction(Commands.DecreaseBrushSize, MakeWeakPtrDelegate(this, [&](){ BrushSizeDelta--; })); CommandList->MapAction(Commands.IncreaseBrushFalloff, MakeWeakPtrDelegate(this, [&](){ FalloffDelta++; })); CommandList->MapAction(Commands.DecreaseBrushFalloff, MakeWeakPtrDelegate(this, [&](){ FalloffDelta--; })); CommandList->MapAction(Commands.IncreaseBrushStrength, MakeWeakPtrDelegate(this, [&](){ StrengthDelta++; })); CommandList->MapAction(Commands.DecreaseBrushStrength, MakeWeakPtrDelegate(this, [&](){ StrengthDelta--; })); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FSceneView* FVoxelEditorToolsPanel::GetSceneView(TUniquePtr& ViewFamily) const { if (!ensure(ViewportClientForDeproject)) { return nullptr; } FViewport* Viewport = ViewportClientForDeproject->Viewport; // Make sure we have a valid viewport, otherwise we won't be able to construct an FSceneView if (!Viewport || Viewport->GetSizeXY().GetMin() <= 0) { return nullptr; } ViewFamily = MakeUnique(FSceneViewFamily::ConstructionValues( Viewport, ViewportClientForDeproject->GetScene(), ViewportClientForDeproject->EngineShowFlags) .SetRealtimeUpdate(ViewportClientForDeproject->IsRealtime())); FSceneView* SceneView = ViewportClientForDeproject->CalcSceneView(ViewFamily.Get()); ensure(SceneView); return SceneView; } bool FVoxelEditorToolsPanel::Deproject(const FVector2D& ScreenPosition, FVector& OutWorldPosition, FVector& OutWorldDirection) const { TUniquePtr ViewFamily; auto* SceneView = GetSceneView(ViewFamily); if (!SceneView) { return false; } const FViewportCursorLocation MouseViewportRay(SceneView, ViewportClientForDeproject, FMath::RoundToInt(ScreenPosition.X), FMath::RoundToInt(ScreenPosition.Y)); OutWorldPosition = MouseViewportRay.GetOrigin(); OutWorldDirection = MouseViewportRay.GetDirection(); // If we're dealing with an orthographic view, push the origin of the ray backward along the viewport forward axis // to make sure that we can select objects that are behind the origin! if (!ViewportClientForDeproject->IsPerspective()) { OutWorldPosition -= OutWorldDirection * WORLD_MAX / 2; } return true; }