Files
UnrealEngineUWP/Engine/Plugins/Runtime/MeshModelingToolset/Source/ModelingComponents/Private/Mechanics/DragAlignmentMechanic.cpp
ryan schmidt db857d56b6 ModelingTools: convert various tools/mechanics/etc to use new SnappingManager functionality for scene hit-tests and snapping
#rb david.hill
#rnx
#jira none
#preflight 61b21920d308710b0c513796

#ROBOMERGE-AUTHOR: ryan.schmidt
#ROBOMERGE-SOURCE: CL 18419130 in //UE5/Release-5.0/... via CL 18422407
#ROBOMERGE-BOT: STARSHIP (Release-Engine-Staging -> Release-Engine-Test) (v897-18405271)

[CL 18422664 by ryan schmidt in ue5-release-engine-test branch]
2021-12-09 14:46:09 -05:00

375 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Mechanics/DragAlignmentMechanic.h"
#include "BaseBehaviors/KeyAsModifierInputBehavior.h"
#include "BaseGizmos/IntervalGizmo.h"
#include "BaseGizmos/RepositionableTransformGizmo.h"
#include "BaseGizmos/CombinedTransformGizmo.h"
#include "BaseGizmos/TransformProxy.h"
#include "Operations/GroupTopologyDeformer.h"
#include "SceneManagement.h" //FPrimitiveDrawInterface
#include "ToolDataVisualizer.h"
#include "ToolSceneQueriesUtil.h"
using namespace UE::Geometry;
void UDragAlignmentMechanic::Setup(UInteractiveTool* ParentToolIn)
{
UInteractionMechanic::Setup(ParentToolIn);
// Register modifier listeners.
UKeyAsModifierInputBehavior* AlignmentToggleBehavior = NewObject<UKeyAsModifierInputBehavior>();
AlignmentToggleBehavior->Initialize(this, AlignmentModifierID, FInputDeviceState::IsCtrlKeyDown);
ParentTool->AddInputBehavior(AlignmentToggleBehavior);
// Set up the function that casts rays into the world.
WorldRayCast = [this, ParentToolIn](const FRay& WorldRay, FHitResult& HitResult,
const TArray<const UPrimitiveComponent*>* ComponentsToIgnore,
const TArray<const UPrimitiveComponent*>* InvisibleComponentsToInclude)
{
bool bSuccess = false;
FVector3d QueryPoint = (FVector3d)WorldRay.PointAt(1);
FVector3d SnapPoint;
ToolSceneQueriesUtil::FSnapGeometry SnapGeometry;
// Try to snap to visible vertices/edges first
ToolSceneQueriesUtil::FFindSceneSnapPointParams SnapParams;
SnapParams.Tool = ParentTool.Get();
SnapParams.Point = &QueryPoint;
SnapParams.SnapPointOut = &SnapPoint;
SnapParams.bEdges = true;
SnapParams.bVertices = true;
SnapParams.VisualAngleThreshold = VisualAngleSnapThreshold;
SnapParams.SnapGeometryOut = &SnapGeometry;
SnapParams.ComponentsToIgnore = ComponentsToIgnore;
SnapParams.InvisibleComponentsToInclude = InvisibleComponentsToInclude;
if (ToolSceneQueriesUtil::FindSceneSnapPoint(SnapParams))
{
HitResult = FHitResult(WorldRay.Origin, (FVector)SnapPoint);
HitResult.ImpactPoint = (FVector)SnapPoint;
HitResult.Distance = WorldRay.GetParameter(HitResult.ImpactPoint);
HitResult.Item = SnapGeometry.PointCount == 1 ? VERT_HIT_TYPE_ID : EDGE_HIT_TYPE_ID;
bSuccess = true;
}
// If we failed, try to hit a simple triangle
else if (ToolSceneQueriesUtil::FindNearestVisibleObjectHit(ParentToolIn, HitResult, WorldRay, ComponentsToIgnore))
{
HitResult.Item = TRIANGLE_HIT_TYPE_ID;
bSuccess = true;
}
return bSuccess;
};
}
void UDragAlignmentMechanic::Shutdown()
{
}
void UDragAlignmentMechanic::AddToGizmo(UCombinedTransformGizmo* TransformGizmo, const TArray<const UPrimitiveComponent*>* ComponentsToIgnoreInAlignment,
const TArray<const UPrimitiveComponent*>* InvisibleComponentsToIncludeInAlignment)
{
// If we have components to ignore/include, we need a copy of the array so that the alignment
// functions can use them later.
TSharedPtr<TArray<const UPrimitiveComponent*>> ComponentsToIgnorePersistent;
if (ComponentsToIgnoreInAlignment)
{
ComponentsToIgnorePersistent = MakeShared<TArray<const UPrimitiveComponent*>>(*ComponentsToIgnoreInAlignment);
}
TSharedPtr<TArray<const UPrimitiveComponent*>> ComponentsToIncludePersistent;
if (InvisibleComponentsToIncludeInAlignment)
{
ComponentsToIncludePersistent = MakeShared<TArray<const UPrimitiveComponent*>>(*InvisibleComponentsToIncludeInAlignment);
}
// Set the alignment functions.
TransformGizmo->SetWorldAlignmentFunctions(
[this]() { return bAlignmentToggle; },
[this, ComponentsToIgnorePersistent, ComponentsToIncludePersistent](const FRay& WorldRay, FVector& OutputPoint) {
return CastRay(WorldRay, OutputPoint, ComponentsToIgnorePersistent.Get(), ComponentsToIncludePersistent.Get(), true);
});
// If we're adding to a repositionable gizmo, we probably don't want to ignore components
// when repositioning the pivot, since ignoring components is mainly used to avoid hitting
// the actual object, which we actually do want to hit in moving moving the pivot.
// If we someday do want to ignore things in a pivot reposition function, we'll have to
// make this caller-configurable somehow.
if (URepositionableTransformGizmo* RepositionableGizmo = Cast<URepositionableTransformGizmo>(TransformGizmo))
{
RepositionableGizmo->SetPivotAlignmentFunctions(
[this]() { return bAlignmentToggle; },
[this, ComponentsToIncludePersistent](const FRay& WorldRay, FVector& OutputPoint) {
return CastRay(WorldRay, OutputPoint, nullptr, ComponentsToIncludePersistent.Get(), false); // don't ignore anything
});
}
// Register listeners to help with rendering. They get us the location that the gizmo ended
// up being placed, so that we can draw a line from the hit point to that location.
TransformGizmo->ActiveTarget->OnTransformChanged.AddWeakLambda(this, [this](UTransformProxy*, FTransform NewTransform){
OnGizmoTransformChanged(NewTransform);
});
TransformGizmo->ActiveTarget->OnPivotChanged.AddWeakLambda(this, [this](UTransformProxy*, FTransform NewTransform) {
OnGizmoTransformChanged(NewTransform);
});
// Register a listener to stop drawing once the gizmo is done moving.
TransformGizmo->ActiveTarget->OnEndTransformEdit.AddWeakLambda(this, [this](UTransformProxy*) {
bPreviewEndpointsValid = false;
bWaitingOnProjectedResult = false;
});
TransformGizmo->ActiveTarget->OnEndPivotEdit.AddWeakLambda(this, [this](UTransformProxy*) {
bPreviewEndpointsValid = false;
bWaitingOnProjectedResult = false;
});
}
void UDragAlignmentMechanic::AddToGizmo(UIntervalGizmo* IntervalGizmo, const TArray<const UPrimitiveComponent*>* ComponentsToIgnoreInAlignment,
const TArray<const UPrimitiveComponent*>* InvisibleComponentsToIncludeInAlignment)
{
// If we have components to ignore/include, we need a copy of the array so that the alignment
// functions can use them later.
TSharedPtr<TArray<const UPrimitiveComponent*>> ComponentsToIgnorePersistent;
if (ComponentsToIgnoreInAlignment)
{
ComponentsToIgnorePersistent = MakeShared<TArray<const UPrimitiveComponent*>>(*ComponentsToIgnoreInAlignment);
}
TSharedPtr<TArray<const UPrimitiveComponent*>> ComponentsToIncludePersistent;
if (InvisibleComponentsToIncludeInAlignment)
{
ComponentsToIncludePersistent = MakeShared<TArray<const UPrimitiveComponent*>>(*InvisibleComponentsToIncludeInAlignment);
}
// Set the alignment functions.
IntervalGizmo->SetWorldAlignmentFunctions(
[this]() { return bAlignmentToggle; },
[this, ComponentsToIgnorePersistent, ComponentsToIncludePersistent](const FRay& WorldRay, FVector& OutputPoint) {
return CastRay(WorldRay, OutputPoint, ComponentsToIgnorePersistent.Get(), ComponentsToIncludePersistent.Get(), true);
});
// Register listener to help with rendering. It gets us the final location of the interval endpoint
// so that we can draw a line from the hit point to that location.
IntervalGizmo->OnIntervalChanged.AddWeakLambda(this, [this](UIntervalGizmo* Gizmo, FVector Direction, float NewParam) {
FTransform Transform = Gizmo->GetGizmoTransform();
Transform.AddToTranslation(Transform.Rotator().RotateVector(Direction) * NewParam);
OnGizmoTransformChanged(Transform);
});
// Register a listener to stop drawing once the gizmo is done moving.
IntervalGizmo->OnEndIntervalGizmoEdit.AddWeakLambda(this, [this](UIntervalGizmo*) {
bPreviewEndpointsValid = false;
bWaitingOnProjectedResult = false;
});
}
void UDragAlignmentMechanic::OnGizmoTransformChanged(FTransform NewTransform)
{
// Ignore changes that weren't preceded by a successful query (likely programatic)
if (bWaitingOnProjectedResult)
{
LastProjectedResult = (FVector3d)NewTransform.GetLocation();
bPreviewEndpointsValid = true;
}
bWaitingOnProjectedResult = false;
}
void UDragAlignmentMechanic::InitializeDeformedMeshRayCast(TFunction<FDynamicMeshAABBTree3* ()> GetSpatialIn,
const UE::Geometry::FTransform3d &TargetTransform, const FGroupTopologyDeformer* LinearDeformer)
{
if (LinearDeformer)
{
// Use the deformer to create a way to filter out triangles we don't want to be hitting (the ones we're manipulating)
const FDynamicMesh3* Mesh = LinearDeformer->GetMesh();
MeshRayCastTriangleFilter = [Mesh, LinearDeformer](int32 Tid)
{
FIndex3i Vids = Mesh->GetTriangle(Tid);
for (int32 i = 0; i < 3; ++i)
{
if (LinearDeformer->GetModifiedVertices().Contains(Vids[i]))
{
return false;
}
}
return true;
};
}
// Set up the actual function that casts rays into the mesh.
MeshRayCast = [this, GetSpatialIn, TargetTransform](const FRay& WorldRay, FHitResult& HitResult, bool bUseFilter)
{
// Transform ray into local space
FRay3d LocalRay(TargetTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
TargetTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
FDynamicMeshAABBTree3* Spatial = GetSpatialIn();
const FDynamicMesh3* Mesh = Spatial->GetMesh();
// Do the actual ray cast. Note that if we're using the filter, then we're presumably deforming the
// mesh, and the aabb tree will not be updated. In that case, we want to tell it to not perform
// that check, since we'll be relying on the filter to make sure that we're not hitting anything
// invalid.
double RayTValue;
int32 HitTid;
IMeshSpatial::FQueryOptions Options;
if (bUseFilter)
{
Options.TriangleFilterF = [this](int32 Tid) { return MeshRayCastTriangleFilter(Tid); };
Options.bAllowUnsafeModifiedMeshQueries = true;
}
if (!Spatial->FindNearestHitTriangle(LocalRay, RayTValue, HitTid, Options))
{
return false;
}
// See if we're close enough to one of the triangle's vertices to snap to it. We have
// to do this check in world space since scaling will affect things.
FVector3d WorldHitPoint = (FVector3d)WorldRay.PointAt(RayTValue);
FVector3d Vert1, Vert2, Vert3;
Mesh->GetTriVertices(HitTid, Vert1, Vert2, Vert3);
Vert1 = TargetTransform.TransformPosition(Vert1);
Vert2 = TargetTransform.TransformPosition(Vert2);
Vert3 = TargetTransform.TransformPosition(Vert3);
FVector3d* ClosestVert = &Vert1;
double MinDistSquared = DistanceSquared(WorldHitPoint, Vert1);
double DistSquared = DistanceSquared(WorldHitPoint, Vert2);
if (DistSquared < MinDistSquared)
{
ClosestVert = &Vert2;
MinDistSquared = DistSquared;
}
DistSquared = DistanceSquared(WorldHitPoint, Vert3);
if (DistSquared < MinDistSquared)
{
ClosestVert = &Vert3;
}
// Check if we're close enough to snap.
if (ToolSceneQueriesUtil::PointSnapQuery(CachedCameraState, WorldHitPoint, *ClosestVert, VisualAngleSnapThreshold))
{
HitResult = FHitResult(WorldRay.Origin, (FVector)*ClosestVert);
HitResult.ImpactPoint = (FVector)*ClosestVert;
HitResult.Item = VERT_HIT_TYPE_ID;
}
else
{
HitResult = FHitResult(WorldRay.Origin, (FVector)WorldHitPoint);
HitResult.ImpactPoint = (FVector)WorldHitPoint;
HitResult.Item = TRIANGLE_HIT_TYPE_ID;
}
HitResult.Distance = WorldRay.GetParameter(HitResult.ImpactPoint);
HitResult.ImpactNormal = (FVector)Mesh->GetTriNormal(HitTid);
return true;
};
}
/**
* Casts a ray into the scene to find an alignment point. Used in the functions bound
* to gizmos.
*
* @param bUseFilter If true, and if there is a dynamic mesh to hit, its triangles are
* filtered to avoid hitting deformed triangles.
* @return true if something was hit.
*/
bool UDragAlignmentMechanic::CastRay(const FRay& WorldRay, FVector& OutputPoint,
const TArray<const UPrimitiveComponent*>* ComponentsToIgnore,
const TArray<const UPrimitiveComponent*>* InvisibleComponentsToInclude,
bool bUseFilter)
{
bool bHit = false;
FHitResult MeshHitResult;
if (MeshRayCast && MeshRayCast(WorldRay, MeshHitResult, bUseFilter))
{
bHit = true;
LastHitResult = MeshHitResult;
OutputPoint = LastHitResult.ImpactPoint;
}
FHitResult WorldHitResult;
if (WorldRayCast
&& WorldRayCast(WorldRay, WorldHitResult, ComponentsToIgnore, InvisibleComponentsToInclude)
&& (!bHit || WorldHitResult.Distance < MeshHitResult.Distance))
{
bHit = true;
LastHitResult = WorldHitResult;
OutputPoint = LastHitResult.ImpactPoint;
}
// We keep track of the last position we served the user, and the position
// that they moved a transform. When we have both, we can render a visualization
// connecting the two. Right now we (may) have the first of the two.
bWaitingOnProjectedResult = bHit;
bPreviewEndpointsValid = false;
return bHit;
}
void UDragAlignmentMechanic::Render(IToolsContextRenderAPI* RenderAPI)
{
// Cache the camera state
GetParentTool()->GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CachedCameraState);
if (!bPreviewEndpointsValid)
{
return;
}
FColor Color = FColor::Orange;
FViewCameraState CameraState = RenderAPI->GetCameraState();
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
float PDIScale = CameraState.GetPDIScalingFactor();
double VisualAngleRenderThreshold = 0.5;
FToolDataVisualizer Renderer;
Renderer.BeginFrame(RenderAPI, CameraState);
if (LastHitResult.Item == TRIANGLE_HIT_TYPE_ID)
{
// We'll draw a circle with an X in it lying on the face we hit.
FFrame3d HitFrame = FFrame3d((FVector3d)LastHitResult.ImpactPoint, (FVector3d)LastHitResult.ImpactNormal);
float TickLength = (float)ToolSceneQueriesUtil::CalculateDimensionFromVisualAngleD(CameraState, HitFrame.Origin, 0.8);
float TickThickness = 4 * PDIScale;
Renderer.DrawLine(
HitFrame.PointAt(-TickLength, -TickLength, 0),
HitFrame.PointAt(TickLength, TickLength, 0), Color, TickThickness, false);
Renderer.DrawLine(
HitFrame.PointAt(-TickLength, TickLength, 0),
HitFrame.PointAt(TickLength, -TickLength, 0), Color, TickThickness, false);
Renderer.DrawCircle(
HitFrame.Origin, HitFrame.Z(), 2.0 * TickLength, 8, Color, 1.0, false);
}
else
{
PDI->DrawPoint(LastHitResult.ImpactPoint, Color, 10.0f * PDIScale, SDPG_Foreground);
}
// If the two endpoints are far enough apart, draw a line connecting them.
if (ToolSceneQueriesUtil::CalculateNormalizedViewVisualAngleD(
CameraState, (FVector3d)LastHitResult.ImpactPoint, LastProjectedResult) > VisualAngleRenderThreshold)
{
PDI->DrawLine(LastHitResult.ImpactPoint, (FVector)LastProjectedResult, Color,
SDPG_Foreground, 0.5f * PDIScale, 0.0f, true);
}
Renderer.EndFrame();
}
void UDragAlignmentMechanic::OnUpdateModifierState(int ModifierID, bool bIsOn)
{
if (ModifierID == AlignmentModifierID)
{
bAlignmentToggle = bIsOn;
}
if (!bAlignmentToggle)
{
bPreviewEndpointsValid = false;
}
}