Files
UnrealEngineUWP/Engine/Plugins/Experimental/MeshModelingToolset/Source/MeshModelingTools/Private/MirrorTool.cpp

571 lines
19 KiB
C++
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MirrorTool.h"
#include "ModelingObjectsCreationAPI.h"
#include "BaseBehaviors/KeyAsModifierInputBehavior.h"
#include "BaseBehaviors/SingleClickBehavior.h"
#include "CompositionOps/MirrorOp.h"
#include "Drawing/MeshDebugDrawing.h"
#include "DynamicMeshToMeshDescription.h"
#include "InteractiveToolManager.h"
#include "MeshDescriptionToDynamicMesh.h"
#include "Misc/MessageDialog.h"
#include "ToolBuilderUtil.h"
#include "ToolSetupUtil.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/MeshDescriptionCommitter.h"
#include "TargetInterfaces/MeshDescriptionProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include "ToolTargetManager.h"
#include "ExplicitUseGeometryMathTypes.h" // using UE::Geometry::(math types)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UMirrorTool"
//Tool builder functions
const FToolTargetTypeRequirements& UMirrorToolBuilder::GetTargetRequirements() const
{
static FToolTargetTypeRequirements TypeRequirements({
UMaterialProvider::StaticClass(),
UMeshDescriptionCommitter::StaticClass(),
UMeshDescriptionProvider::StaticClass(),
UPrimitiveComponentBackedTarget::StaticClass()
});
return TypeRequirements;
}
bool UMirrorToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
return SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()) > 0;
}
UInteractiveTool* UMirrorToolBuilder::BuildTool(const FToolBuilderState& SceneState) const
{
UMirrorTool* NewTool = NewObject<UMirrorTool>(SceneState.ToolManager);
TArray<TObjectPtr<UToolTarget>> Targets = SceneState.TargetManager->BuildAllSelectedTargetable(SceneState, GetTargetRequirements());
NewTool->SetTargets(MoveTemp(Targets));
NewTool->SetWorld(SceneState.World);
return NewTool;
}
// Operator factory
TUniquePtr<FDynamicMeshOperator> UMirrorOperatorFactory::MakeNewOperator()
{
TUniquePtr<FMirrorOp> MirrorOp = MakeUnique<FMirrorOp>();
// Set up inputs and settings
MirrorOp->OriginalMesh = MirrorTool->MeshesToMirror[ComponentIndex]->GetMesh();
MirrorOp->bAppendToOriginal = MirrorTool->Settings->OperationMode == EMirrorOperationMode::MirrorAndAppend;
MirrorOp->bCropFirst = MirrorTool->Settings->bCropAlongMirrorPlaneFirst;
MirrorOp->bWeldAlongPlane = MirrorTool->Settings->bWeldVerticesOnMirrorPlane;
MirrorOp->bAllowBowtieVertexCreation = MirrorTool->Settings->bAllowBowtieVertexCreation;
FTransform LocalToWorld = MirrorTool->TargetComponentInterface(ComponentIndex)->GetWorldTransform();
MirrorOp->SetTransform(LocalToWorld);
// We also need WorldToLocal. Threshold the LocalToWorld scaling transform so we can get the inverse.
FVector LocalToWorldScale = LocalToWorld.GetScale3D();
for (int i = 0; i < 3; i++)
{
float DimScale = FMathf::Abs(LocalToWorldScale[i]);
float Tolerance = KINDA_SMALL_NUMBER;
if (DimScale < Tolerance)
{
LocalToWorldScale[i] = Tolerance * FMathf::SignNonZero(LocalToWorldScale[i]);
}
}
LocalToWorld.SetScale3D(LocalToWorldScale);
UE::Geometry::FTransform3d WorldToLocal = UE::Geometry::FTransform3d(LocalToWorld).Inverse();
// Now we can get the plane parameters in local space.
MirrorOp->LocalPlaneOrigin = WorldToLocal.TransformPosition(MirrorTool->MirrorPlaneOrigin);;
FVector3d WorldNormal = MirrorTool->MirrorPlaneNormal;
MirrorOp->LocalPlaneNormal = WorldToLocal.TransformNormal(MirrorTool->MirrorPlaneNormal);
return MirrorOp;
}
// Tool property functions
void UMirrorToolActionPropertySet::PostAction(EMirrorToolAction Action)
{
if (ParentTool.IsValid())
{
ParentTool->RequestAction(Action);
}
}
// Tool itself
UMirrorTool::UMirrorTool()
{
}
bool UMirrorTool::CanAccept() const
{
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
if (!Preview->HaveValidResult())
{
return false;
}
}
return Super::CanAccept();
}
void UMirrorTool::SetWorld(UWorld* World)
{
TargetWorld = World;
}
void UMirrorTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
// Editing the "show preview" option changes whether we need to be displaying the preview or the original mesh.
if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UMirrorToolProperties, bShowPreview)))
{
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(!Settings->bShowPreview);
}
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->SetVisibility(Settings->bShowPreview);
}
}
// Regardless of what changed, update the previews.
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->InvalidateResult();
}
}
void UMirrorTool::OnTick(float DeltaTime)
{
// Deal with any buttons that may have been clicked
if (PendingAction != EMirrorToolAction::NoAction)
{
ApplyAction(PendingAction);
PendingAction = EMirrorToolAction::NoAction;
}
if (PlaneMechanic != nullptr)
{
// Update snapping behavior based on modifier key.
PlaneMechanic->SetEnableGridSnaping(Settings->bSnapToWorldGrid ^ bSnappingToggle);
PlaneMechanic->Tick(DeltaTime);
}
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->Tick(DeltaTime);
}
}
void UMirrorTool::Render(IToolsContextRenderAPI* RenderAPI)
{
// Have the plane draw itself.
PlaneMechanic->Render(RenderAPI);
}
void UMirrorTool::Setup()
{
UInteractiveTool::Setup();
SetToolDisplayName(LOCTEXT("ToolName", "Mirror"));
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartMirrorTool", "Mirror one or more meshes across a plane. Grid snapping behavior is swapped while the shift key is down. The plane can be set by using the preset buttons, moving the gizmo, or ctrl+clicking on a spot on the original mesh."),
EToolMessageLevel::UserNotification);
// Set up the properties
Settings = NewObject<UMirrorToolProperties>(this, TEXT("Mirror Tool Settings"));
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
ToolActions = NewObject<UMirrorToolActionPropertySet>(this);
ToolActions->Initialize(this);
AddToolPropertySource(ToolActions);
CheckAndDisplayWarnings();
// Fill in the MeshesToMirror array with suitably converted meshes.
for (int i = 0; i < Targets.Num(); i++)
{
IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(i);
IMeshDescriptionProvider* TargetMeshProvider = TargetMeshProviderInterface(i);
// Convert into dynamic mesh
TSharedPtr<FDynamicMesh3, ESPMode::ThreadSafe> DynamicMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>();
FMeshDescriptionToDynamicMesh Converter;
Converter.Convert(TargetMeshProvider->GetMeshDescription(), *DynamicMesh);
// Wrap the dynamic mesh in a replacement change target
UDynamicMeshReplacementChangeTarget* WrappedTarget = MeshesToMirror.Add_GetRef(NewObject<UDynamicMeshReplacementChangeTarget>());
// Set callbacks so previews are invalidated on undo/redo changing the meshes
WrappedTarget->SetMesh(DynamicMesh);
WrappedTarget->OnMeshChanged.AddLambda([this, i]() { Previews[i]->InvalidateResult(); });
}
// Set the visibility of the StaticMeshComponents depending on whether we are showing them or the preview.
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(!Settings->bShowPreview);
}
// Initialize the PreviewMesh and BackgroundCompute objects
SetupPreviews();
// Update the bounding box of the meshes.
CombinedBounds.Init();
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
FVector ComponentOrigin, ComponentExtents;
TargetComponentInterface(ComponentIdx)->GetOwnerActor()->GetActorBounds(false, ComponentOrigin, ComponentExtents);
CombinedBounds += FBox::BuildAABB(ComponentOrigin, ComponentExtents);
}
// Set the initial mirror plane. We want the plane to start in the middle if we're doing a simple
// mirror (i.e., not appending, and not cropping). Otherwise, we want the plane to start to one side.
MirrorPlaneOrigin = (FVector3d)CombinedBounds.GetCenter();
MirrorPlaneNormal = FVector3d(0, -1, 0);
if (Settings->OperationMode == EMirrorOperationMode::MirrorAndAppend || Settings->bCropAlongMirrorPlaneFirst)
{
MirrorPlaneOrigin.Y = CombinedBounds.Min.Y;
}
// Set up the mirror plane mechanic, which manages the gizmo
PlaneMechanic = NewObject<UConstructionPlaneMechanic>(this);
PlaneMechanic->Setup(this);
PlaneMechanic->Initialize(TargetWorld, FFrame3d(MirrorPlaneOrigin, MirrorPlaneNormal));
// Have the plane mechanic update things properly
PlaneMechanic->OnPlaneChanged.AddLambda([this]() {
MirrorPlaneNormal = PlaneMechanic->Plane.Rotation.AxisZ();
MirrorPlaneOrigin = PlaneMechanic->Plane.Origin;
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->InvalidateResult();
}
});
// Modify the Ctrl+click set plane behavior to respond to our CtrlClickBehavior property
PlaneMechanic->SetPlaneCtrlClickBehaviorTarget->OnClickedPositionFunc = [this](const FHitResult& Hit)
{
bool bIgnoreNormal = (Settings->CtrlClickBehavior == EMirrorCtrlClickBehavior::Reposition);
PlaneMechanic->SetDrawPlaneFromWorldPos((FVector3d)Hit.ImpactPoint, (FVector3d)Hit.ImpactNormal, bIgnoreNormal);
};
// Also include the original components in the ctrl+click hit testing even though we made them
// invisible, since we want to be able to reposition the plane onto the original mesh.
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(ComponentIdx);
PlaneMechanic->SetPlaneCtrlClickBehaviorTarget->InvisibleComponentsToHitTest.Add(TargetComponent->GetOwnerComponent());
}
// Add modifier button for snapping
UKeyAsModifierInputBehavior* SnapToggleBehavior = NewObject<UKeyAsModifierInputBehavior>();
SnapToggleBehavior->Initialize(this, SnappingToggleModifierId, FInputDeviceState::IsShiftKeyDown);
AddInputBehavior(SnapToggleBehavior);
// Start the preview calculations
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->InvalidateResult();
}
}
void UMirrorTool::SetupPreviews()
{
// Create a preview (with an op) for each selected component.
int32 NumMeshes = MeshesToMirror.Num();
for (int32 PreviewIndex = 0; PreviewIndex < NumMeshes; ++PreviewIndex)
{
UMirrorOperatorFactory* MirrorOpCreator = NewObject<UMirrorOperatorFactory>();
MirrorOpCreator->MirrorTool = this;
MirrorOpCreator->ComponentIndex = PreviewIndex;
UMeshOpPreviewWithBackgroundCompute* Preview = Previews.Add_GetRef(
NewObject<UMeshOpPreviewWithBackgroundCompute>(MirrorOpCreator, "Preview"));
Preview->Setup(TargetWorld, MirrorOpCreator);
ModelingComponents: Clean up DynamicMeshComponent API. Update Component and Proxy handling of Tangents to use Attribute Overlay if available. Update affected Tools and also convert most of the affected Tools to use UE::ToolTarget helper functions. - Add UE::ToolTarget::CommitMaterialSetUpdate() and ::CommitDynamicMeshUpdate(). ::GetDynamicMeshCopy() can now return tangents if requested. - Add IMeshDescriptionProvider::CalculateAutoGeneratedAttributes(). Default implementation does nothing, UStaticMeshComponentToolTarget implementation initializes auto-generated MeshDescription attributes. Used in ::GetDynamicMeshCopy() to get tangents (but requires a MeshDescription copy). - Clean up handling of Tangents in Simple/OctreeDynamicMeshComponent. Add local MakeTangentsFunc() to generate the Tangents lambda, handle different cases and no-tangents fallbacks consistently. - UDynamicMesh: add optional info arguments to EditMesh() and ChangeInfo struct. Add support for deferring change events from Edit funcs. - Remove UBaseDynamicMeshComponent::InitializeMesh(), ::Bake() APIs, and add ::SetMesh(). Implement in Simple/Octree implementations, update all Tools that used those APIs. - Add USimpleDynamicMeshComponent::ProcessMesh(), EditMesh(). These are now the preferred ways to read/write mesh. - Update USimpleDynamicMeshComponent tangents handling. Externally-computed tangents are now taken directly from the FDynamicMesh3 attribute set. Autogenerated tangents are still computed and stored in an internal FMeshTangentsf, but this is no longer exposed for external updates. - Remove UPreviewMesh pass-through functions for Tangents access, InitializeMesh() and Bake(). Add ProcessMesh() - Update all affected Tools. In most cases these Tools have also been converted to use ModelingToolTargetUtil functions, instead of direct ToolTarget interface casting. #rb none #rnx #jira none #preflight 60c3e71d3e1b3c00015668af [CL 16650666 by Ryan Schmidt in ue5-main branch]
2021-06-11 22:39:18 -04:00
Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated);
FComponentMaterialSet MaterialSet;
TargetMaterialInterface(PreviewIndex)->GetMaterialSet(MaterialSet);
Preview->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
// Set initial preview to unprocessed mesh, so that things don't disappear initially
Preview->PreviewMesh->UpdatePreview(MeshesToMirror[PreviewIndex]->GetMesh().Get());
Preview->PreviewMesh->SetTransform(TargetComponentInterface(PreviewIndex)->GetWorldTransform());
Preview->SetVisibility(Settings->bShowPreview);
}
}
void UMirrorTool::CheckAndDisplayWarnings()
{
// We can have more than one warning, which makes this a bit more work.
FText SameSourceWarning;
FText NonUniformScaleWarning;
// See if any of the selected components have the same source.
TArray<int32> MapToFirstOccurrences;
bool bAnyHaveSameSource = GetMapToSharedSourceData(MapToFirstOccurrences);
if (bAnyHaveSameSource)
{
SameSourceWarning = LOCTEXT("MirrorMultipleAssetsWithSameSource", "WARNING: Multiple meshes in your selection use the same source asset! Only the \"Create New Assets\" save mode is supported.");
// We could forcefully set the save mode to CreateNewAssets, but the setting will persist on new invocations
// of the tool, which may surprise the user. So, it's up to them to set it.
}
// See if any of the selected components have a nonuniform scaling transform.
IPrimitiveComponentBackedTarget* NonUniformScalingTarget = nullptr;
for (int32 i = 0; i < Targets.Num(); ++i)
{
FVector Scaling = TargetComponentInterface(i)->GetWorldTransform().GetScale3D();
if (Scaling.X != Scaling.Y || Scaling.Y != Scaling.Z)
{
NonUniformScalingTarget = TargetComponentInterface(i);
break;
}
}
if (NonUniformScalingTarget)
{
NonUniformScaleWarning = FText::Format(
LOCTEXT("MirrorNonUniformScaledAsset", "WARNING: The item \"{0}\" has a non-uniform scaling transform. This is not supported because mirroring acts on the underlying mesh, and mirroring is not commutative with non-uniform scaling. Consider deforming the mesh rather than scaling it non-uniformly."),
FText::FromString(NonUniformScalingTarget->GetOwnerActor()->GetName()));
}
if (bAnyHaveSameSource && NonUniformScalingTarget)
{
// Concatenates the two warnings with an extra line in between.
GetToolManager()->DisplayMessage(FText::Format(LOCTEXT("CombinedWarnings", "{0}\n\n{1}"),
SameSourceWarning, NonUniformScaleWarning), EToolMessageLevel::UserWarning);
}
else if (bAnyHaveSameSource)
{
GetToolManager()->DisplayMessage(SameSourceWarning, EToolMessageLevel::UserWarning);
}
else if (NonUniformScalingTarget)
{
GetToolManager()->DisplayMessage(NonUniformScaleWarning, EToolMessageLevel::UserWarning);
}
}
void UMirrorTool::Shutdown(EToolShutdownType ShutdownType)
{
Settings->SaveProperties(this);
PlaneMechanic->Shutdown();
// Restore (unhide) the source meshes
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(true);
}
// Swap in results, if appropriate
if (ShutdownType == EToolShutdownType::Accept)
{
// Gather results
TArray<FDynamicMeshOpResult> Results;
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Results.Emplace(Preview->Shutdown());
}
// Convert to output. This will also edit the selection.
GenerateAsset(Results);
}
else
{
for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews)
{
Preview->Cancel();
}
}
}
void UMirrorTool::GenerateAsset(const TArray<FDynamicMeshOpResult>& Results)
{
if (Results.Num() == 0)
{
return;
}
GetToolManager()->BeginUndoTransaction(LOCTEXT("MirrorToolTransactionName", "Mirror Tool"));
ensure(Results.Num() > 0);
int32 NumSourceMeshes = MeshesToMirror.Num();
// check if we entirely cut away any meshes
bool bWantToDestroy = false;
for (int OrigMeshIdx = 0; OrigMeshIdx < NumSourceMeshes; OrigMeshIdx++)
{
if (Results[OrigMeshIdx].Mesh->TriangleCount() == 0)
{
bWantToDestroy = true;
break;
}
}
// if so ask user what to do
if (bWantToDestroy)
{
FText Title = LOCTEXT("MirrorDestroyTitle", "Delete mesh components?");
EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo,
LOCTEXT("PlaneCutDestroyQuestion", "The mirror plane cropping has entirely cut away at least one mesh. Actually destroy these mesh components?"), &Title);
if (Ret == EAppReturnType::No)
{
bWantToDestroy = false;
}
}
// Properly deal with each result, setting up the selection at the same time.
FSelectedOjectsChangeList NewSelection;
NewSelection.ModificationType = ESelectedObjectsModificationType::Replace;
for (int OrigMeshIdx = 0; OrigMeshIdx < NumSourceMeshes; OrigMeshIdx++)
{
IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(OrigMeshIdx);
FDynamicMesh3* Mesh = Results[OrigMeshIdx].Mesh.Get();
check(Mesh != nullptr);
if (Mesh->TriangleCount() == 0)
{
if (bWantToDestroy)
{
TargetComponent->GetOwnerComponent()->DestroyComponent();
}
continue;
}
else if (Settings->SaveMode == EMirrorSaveMode::UpdateAssets)
{
NewSelection.Actors.Add(TargetComponent->GetOwnerActor());
TargetMeshCommitterInterface(OrigMeshIdx)->CommitMeshDescription([&Mesh](const IMeshDescriptionCommitter::FCommitterParams& CommitParams)
{
FDynamicMeshToMeshDescription Converter;
Converter.Convert(Mesh, *CommitParams.MeshDescriptionOut);
});
}
else
{
// Build array of materials from the original.
TArray<UMaterialInterface*> Materials;
IMaterialProvider* TargetMaterial = TargetMaterialInterface(OrigMeshIdx);
for (int MaterialIdx = 0, NumMaterials = TargetMaterial->GetNumMaterials(); MaterialIdx < NumMaterials; MaterialIdx++)
{
Materials.Add(TargetMaterial->GetMaterial(MaterialIdx));
}
FCreateMeshObjectParams NewMeshObjectParams;
NewMeshObjectParams.TargetWorld = TargetWorld;
NewMeshObjectParams.Transform = (FTransform)Results[OrigMeshIdx].Transform;
NewMeshObjectParams.BaseName = TEXT("Mirror");
NewMeshObjectParams.Materials = Materials;
NewMeshObjectParams.SetMesh(Mesh);
FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams));
if (Result.IsOK() && Result.NewActor != nullptr)
{
NewSelection.Actors.Add(Result.NewActor);
}
// Remove the original actor
TargetComponent->GetOwnerComponent()->DestroyComponent();
}
}
// Update the selection
if (NewSelection.Actors.Num() > 0)
{
GetToolManager()->RequestSelectionChange(NewSelection);
}
GetToolManager()->EndUndoTransaction();
}
// Action support
void UMirrorTool::RequestAction(EMirrorToolAction ActionType)
{
if (PendingAction == EMirrorToolAction::NoAction)
{
PendingAction = ActionType;
}
}
void UMirrorTool::ApplyAction(EMirrorToolAction ActionType)
{
FVector3d ShiftedPlaneOrigin = (FVector3d)CombinedBounds.GetCenter();
if (ActionType == EMirrorToolAction::ShiftToCenter)
{
// We keep the same orientation here
PlaneMechanic->SetDrawPlaneFromWorldPos(ShiftedPlaneOrigin, FVector3d(), true);
}
else
{
// We still start from the center, but adjust one of the coordinates and set direction.
FVector3d DirectionVector;
switch (ActionType)
{
case EMirrorToolAction::Left:
ShiftedPlaneOrigin.Y = CombinedBounds.Min.Y;
DirectionVector = FVector3d(0, -1.0, 0);
break;
case EMirrorToolAction::Right:
ShiftedPlaneOrigin.Y = CombinedBounds.Max.Y;
DirectionVector = FVector3d(0, 1.0, 0);
break;
case EMirrorToolAction::Up:
ShiftedPlaneOrigin.Z = CombinedBounds.Max.Z;
DirectionVector = FVector3d(0, 0, 1.0);
break;
case EMirrorToolAction::Down:
ShiftedPlaneOrigin.Z = CombinedBounds.Min.Z;
DirectionVector = FVector3d(0, 0, -1.0);
break;
case EMirrorToolAction::Forward:
ShiftedPlaneOrigin.X = CombinedBounds.Max.X;
DirectionVector = FVector3d(1.0, 0, 0);
break;
case EMirrorToolAction::Backward:
ShiftedPlaneOrigin.X = CombinedBounds.Min.X;
DirectionVector = FVector3d(-1.0, 0, 0);
break;
}
// The user can optionally have the button change the direction only
if (Settings->bButtonsOnlyChangeOrientation)
{
ShiftedPlaneOrigin = MirrorPlaneOrigin; // Keeps the same
}
PlaneMechanic->SetDrawPlaneFromWorldPos(ShiftedPlaneOrigin, DirectionVector, false);
}
}
void UMirrorTool::OnUpdateModifierState(int ModifierID, bool bIsOn)
{
if (ModifierID == SnappingToggleModifierId)
{
bSnappingToggle = bIsOn;
}
}
#undef LOCTEXT_NAMESPACE