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

852 lines
28 KiB
C++
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
#include "GroupEdgeInsertionTool.h"
#include "BaseBehaviors/SingleClickBehavior.h"
#include "BaseBehaviors/MouseHoverBehavior.h"
#include "CuttingOps/GroupEdgeInsertionOp.h"
#include "DynamicMesh/DynamicMeshChangeTracker.h"
#include "DynamicMeshToMeshDescription.h"
#include "InteractiveToolManager.h"
#include "MeshDescriptionToDynamicMesh.h"
#include "ToolBuilderUtil.h"
#include "ToolSceneQueriesUtil.h"
#include "ToolSetupUtil.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/MeshDescriptionCommitter.h"
#include "TargetInterfaces/MeshDescriptionProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include "ExplicitUseGeometryMathTypes.h" // using UE::Geometry::(math types)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UGroupEdgeInsertionTool"
bool GetSharedBoundary(const FGroupTopology& Topology,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, int32 StartTopologyID, bool bStartIsCorner,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint, int32 EndTopologyID, bool bEndIsCorner,
int32& GroupIDOut, int32& BoundaryIndexOut);
bool DoesBoundaryContainPoint(const FGroupTopology& Topology,
const FGroupTopology::FGroupBoundary& Boundary, int32 PointTopologyID, bool bPointIsCorner);
USingleSelectionMeshEditingTool* UGroupEdgeInsertionToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
return NewObject<UGroupEdgeInsertionTool>(SceneState.ToolManager);
}
TUniquePtr<FDynamicMeshOperator> UGroupEdgeInsertionOperatorFactory::MakeNewOperator()
{
TUniquePtr<FGroupEdgeInsertionOp> Op = MakeUnique<FGroupEdgeInsertionOp>();
Op->OriginalMesh = Tool->CurrentMesh;
Op->OriginalTopology = Tool->CurrentTopology;
Op->SetTransform(Cast<IPrimitiveComponentBackedTarget>(Tool->Target)->GetWorldTransform());
if (Tool->bShowingBaseMesh)
{
Op->bShowingBaseMesh = true;
return Op; // No inputs necessary- just showing the base mesh.
}
if (Tool->Settings->InsertionMode == EGroupEdgeInsertionMode::PlaneCut)
{
Op->Mode = FGroupEdgeInserter::EInsertionMode::PlaneCut;
}
else
{
Op->Mode = FGroupEdgeInserter::EInsertionMode::Retriangulate;
}
Op->VertexTolerance = Tool->Settings->VertexTolerance;
Op->StartPoint = Tool->StartPoint;
Op->EndPoint = Tool->EndPoint;
Op->CommonGroupID = Tool->CommonGroupID;
Op->CommonBoundaryIndex = Tool->CommonBoundaryIndex;
return Op;
}
void UGroupEdgeInsertionTool::Setup()
{
USingleSelectionTool::Setup();
if (!Target)
{
return;
}
SetToolDisplayName(LOCTEXT("ToolName", "Insert PolyEdge"));
GetToolManager()->DisplayMessage(
LOCTEXT("GroupEdgeInsertionToolDescription", "Click two points on the boundary of a face to insert a new edge between the points and split the face."),
EToolMessageLevel::UserNotification);
// Initialize the mesh that we'll be operating on
CurrentMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>();
FMeshDescriptionToDynamicMesh Converter;
Converter.Convert(Cast<IMeshDescriptionProvider>(Target)->GetMeshDescription(), *CurrentMesh);
CurrentTopology = MakeShared<FGroupTopology, ESPMode::ThreadSafe>(CurrentMesh.Get(), true);
MeshSpatial.SetMesh(CurrentMesh.Get(), true);
// Set up properties
Settings = NewObject<UGroupEdgeInsertionProperties>(this);
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
// Register ourselves to receive clicks and hover
USingleClickInputBehavior* ClickBehavior = NewObject<USingleClickInputBehavior>();
ClickBehavior->Initialize(this);
AddInputBehavior(ClickBehavior);
UMouseHoverBehavior* HoverBehavior = NewObject<UMouseHoverBehavior>();
HoverBehavior->Initialize(this);
AddInputBehavior(HoverBehavior);
SetupPreview();
// These draw the group edges and the loops to be inserted
ExistingEdgesRenderer.LineColor = FLinearColor::Red;
ExistingEdgesRenderer.LineThickness = 2.0;
PreviewEdgeRenderer.LineColor = FLinearColor::Green;
PreviewEdgeRenderer.LineThickness = 4.0;
PreviewEdgeRenderer.PointColor = FLinearColor::Green;
PreviewEdgeRenderer.PointSize = 8.0;
PreviewEdgeRenderer.bDepthTested = false;
// Set up the topology selector, which we use to select the endpoints
TopologySelector.Initialize(CurrentMesh.Get(), CurrentTopology.Get());
TopologySelector.SetSpatialSource([this]() {return &MeshSpatial; });
TopologySelector.PointsWithinToleranceTest = [this](const FVector3d& Position1, const FVector3d& Position2, double TolScale) {
UE::Geometry::FTransform3d Transform(Cast<IPrimitiveComponentBackedTarget>(Target)->GetWorldTransform());
return ToolSceneQueriesUtil::PointSnapQuery(CameraState, Transform.TransformPosition(Position1), Transform.TransformPosition(Position2),
ToolSceneQueriesUtil::GetDefaultVisualAngleSnapThreshD() * TolScale);
};
TopologySelectorSettings.bEnableEdgeHits = true;
TopologySelectorSettings.bEnableCornerHits = true;
TopologySelectorSettings.bEnableFaceHits = false;
}
void UGroupEdgeInsertionTool::SetupPreview()
{
UGroupEdgeInsertionOperatorFactory* OpFactory = NewObject<UGroupEdgeInsertionOperatorFactory>();
OpFactory->Tool = this;
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(OpFactory);
Preview->Setup(TargetWorld, OpFactory);
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;
Cast<IMaterialProvider>(Target)->GetMaterialSet(MaterialSet);
Preview->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
// Whenever we get a new result from the op, we need to extract the preview edges so that
// we can draw them if we want to.
Preview->OnOpCompleted.AddLambda([this](const FDynamicMeshOperator* UncastOp) {
const FGroupEdgeInsertionOp* Op = static_cast<const FGroupEdgeInsertionOp*>(UncastOp);
bLastComputeSucceeded = Op->bSucceeded;
LatestOpTopologyResult.Reset();
LatestOpChangedTids.Reset();
PreviewEdges.Reset();
if (bLastComputeSucceeded)
{
Op->GetEdgeLocations(PreviewEdges);
LatestOpTopologyResult = Op->ResultTopology;
LatestOpChangedTids = Op->ChangedTids;
}
else
{
// Don't show the broken preview, since we wouldn't accept it on click.
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get());
}
});
Preview->OnOpCompleted.AddLambda([this](const FDynamicMeshOperator*) {
if (!bLastComputeSucceeded)
{
// Don't show the broken preview, since we wouldn't accept it on click.
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get());
}
});
// Set initial preview to unprocessed mesh, so that things don't disappear initially
IPrimitiveComponentBackedTarget* TargetComponent = Cast<IPrimitiveComponentBackedTarget>(Target);
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get());
Preview->PreviewMesh->SetTransform(TargetComponent->GetWorldTransform());
Preview->PreviewMesh->EnableWireframe(Settings->bWireframe);
Preview->SetVisibility(true);
ClearPreview();
TargetComponent->SetOwnerVisibility(false);
}
void UGroupEdgeInsertionTool::Shutdown(EToolShutdownType ShutdownType)
{
// Set visibility before committing so that it doesn't get saved as false.
Cast<IPrimitiveComponentBackedTarget>(Target)->SetOwnerVisibility(true);
if (ShutdownType == EToolShutdownType::Accept)
{
GetToolManager()->BeginUndoTransaction(LOCTEXT("GroupEdgeInsertionToolTransactionName", "Group Edge Insert Tool"));
Cast<IMeshDescriptionCommitter>(Target)->CommitMeshDescription([this](const IMeshDescriptionCommitter::FCommitterParams& CommitParams)
{
FDynamicMeshToMeshDescription Converter;
Converter.Convert(CurrentMesh.Get(), *CommitParams.MeshDescriptionOut);
});
GetToolManager()->EndUndoTransaction();
}
Settings->SaveProperties(this);
Preview->Shutdown();
CurrentMesh.Reset();
CurrentTopology.Reset();
ExpireChanges();
}
void UGroupEdgeInsertionTool::OnTick(float DeltaTime)
{
if (Preview)
{
Preview->Tick(DeltaTime);
if (ToolState == EToolState::WaitingForInsertComplete && Preview->HaveValidResult())
{
if (bLastComputeSucceeded)
{
FDynamicMeshChangeTracker ChangeTracker(CurrentMesh.Get());
ChangeTracker.BeginChange();
ChangeTracker.SaveTriangles(*LatestOpChangedTids, true /*bSaveVertices*/);
// Update current mesh and topology
CurrentMesh->Copy(*Preview->PreviewMesh->GetMesh(), true, true, true, true);
*CurrentTopology = *LatestOpTopologyResult;
CurrentTopology->RetargetOnClonedMesh(CurrentMesh.Get());
MeshSpatial.Build();
TopologySelector.Invalidate(true, true);
// Emit transaction
GetToolManager()->BeginUndoTransaction(LOCTEXT("GroupEdgeInsertionTransactionName", "Group Edge Insertion"));
GetToolManager()->EmitObjectChange(this, MakeUnique<FGroupEdgeInsertionChange>(ChangeTracker.EndChange(), CurrentChangeStamp),
LOCTEXT("GroupEdgeInsertion", "GroupEdge Insertion"));
GetToolManager()->EndUndoTransaction();
ToolState = EToolState::GettingStart;
}
else
{
ToolState = EToolState::GettingEnd;
}
PreviewEdges.Reset();
}
}
}
void UGroupEdgeInsertionTool::Render(IToolsContextRenderAPI* RenderAPI)
{
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState);
// Draw the existing group edges
FViewCameraState RenderCameraState = RenderAPI->GetCameraState();
ExistingEdgesRenderer.BeginFrame(RenderAPI, RenderCameraState);
ExistingEdgesRenderer.SetTransform(Preview->PreviewMesh->GetTransform());
for (const FGroupTopology::FGroupEdge& Edge : CurrentTopology->Edges)
{
FVector3d A, B;
for (int32 eid : Edge.Span.Edges)
{
CurrentMesh->GetEdgeV(eid, A, B);
ExistingEdgesRenderer.DrawLine(A, B);
}
}
ExistingEdgesRenderer.EndFrame();
// Draw the preview edges and points
PreviewEdgeRenderer.BeginFrame(RenderAPI, RenderCameraState);
PreviewEdgeRenderer.SetTransform(Preview->PreviewMesh->GetTransform());
for (const TPair<FVector3d, FVector3d>& EdgeVerts : PreviewEdges)
{
PreviewEdgeRenderer.DrawLine(EdgeVerts.Key, EdgeVerts.Value);
}
for (const FVector3d& Point : PreviewPoints)
{
PreviewEdgeRenderer.DrawPoint(Point);
}
PreviewEdgeRenderer.EndFrame();
}
void UGroupEdgeInsertionTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
PreviewEdges.Reset();
Preview->PreviewMesh->EnableWireframe(Settings->bWireframe);
Preview->InvalidateResult();
}
void UGroupEdgeInsertionTool::ClearPreview(bool bClearDrawnElements, bool bForce)
{
// We don't seem to have a way to cancel the background op on a mesh without shutting down
// the entire preview, hence us clearing the preview this way. When we know that the op is
// not running, we can instead use UpdatePreview() to reset the mesh to the original mesh.
if (!bShowingBaseMesh || bForce)
{
bShowingBaseMesh = true;
Preview->InvalidateResult();
}
if (bClearDrawnElements)
{
PreviewEdges.Reset();
PreviewPoints.Reset();
}
}
void UGroupEdgeInsertionTool::ConditionallyUpdatePreview(
const FGroupEdgeInserter::FGroupEdgeSplitPoint& NewEndPoint, int32 NewEndTopologyID, bool bNewEndIsCorner,
int32 NewCommonGroupID, int32 NewBoundaryIndex)
{
if (bShowingBaseMesh
|| bEndIsCorner != bNewEndIsCorner || EndTopologyID != NewEndTopologyID
|| EndPoint.bIsVertex != NewEndPoint.bIsVertex || EndPoint.ElementID != NewEndPoint.ElementID
|| (!NewEndPoint.bIsVertex && NewEndPoint.EdgeTValue != EndPoint.EdgeTValue)
|| CommonGroupID != NewCommonGroupID || CommonBoundaryIndex != NewBoundaryIndex)
{
// Update the end variables, since they are apparently different
EndPoint = NewEndPoint;
EndTopologyID = NewEndTopologyID;
bEndIsCorner = bNewEndIsCorner;
CommonGroupID = NewCommonGroupID;
CommonBoundaryIndex = NewBoundaryIndex;
// If either endpoint is a corner, we need to calculate its tangent. This will differ based on which
// boundary it is a part of.
if (bStartIsCorner)
{
GetCornerTangent(StartTopologyID, CommonGroupID, CommonBoundaryIndex, StartPoint.Tangent);
}
if (bEndIsCorner)
{
GetCornerTangent(EndTopologyID, CommonGroupID, CommonBoundaryIndex, EndPoint.Tangent);
}
bShowingBaseMesh = false;
PreviewEdges.Reset();
Preview->InvalidateResult();
}
}
FInputRayHit UGroupEdgeInsertionTool::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos)
{
FInputRayHit Hit;
switch (ToolState)
{
case EToolState::WaitingForInsertComplete:
break; // Keep hit invalid
case EToolState::GettingStart:
{
PreviewPoints.Reset();
FVector3d RayPoint;
if (TopologyHitTest(PressPos.WorldRay, RayPoint))
{
Hit = FInputRayHit(PressPos.WorldRay.GetParameter((FVector)RayPoint));
}
}
case EToolState::GettingEnd:
{
FVector3d RayPoint;
FRay3d LocalRay;
if (TopologyHitTest(PressPos.WorldRay, RayPoint, &LocalRay))
{
Hit = FInputRayHit(PressPos.WorldRay.GetParameter((FVector)RayPoint));
}
else
{
// If we don't hit a valid element, we still do a hover if we hit the mesh.
// We still do the topology check in the first place because it accepts missing
// rays that are close enough to snap.
double RayT = 0;
int32 Tid = FDynamicMesh3::InvalidID;
if (MeshSpatial.FindNearestHitTriangle(LocalRay, RayT, Tid))
{
Hit = FInputRayHit(RayT);
}
}
}
}
return Hit;
}
bool UGroupEdgeInsertionTool::OnUpdateHover(const FInputDeviceRay& DevicePos)
{
switch (ToolState)
{
case EToolState::WaitingForInsertComplete:
return false; // Do nothing.
case EToolState::GettingStart:
{
// Update start variables and show a preview of a point if it's on an edge or corner
PreviewPoints.Reset();
FVector3d PreviewPoint;
if (GetHoveredItem(DevicePos.WorldRay, StartPoint, StartTopologyID, bStartIsCorner, PreviewPoint))
{
PreviewPoints.Add(PreviewPoint);
return true;
}
return false;
}
case EToolState::GettingEnd:
{
check(PreviewPoints.Num() > 0);
PreviewPoints.SetNum(1); // Keep the first element, which is the start point
// Don't update the end variables right away so that we can check if they actually changed (they
// won't when we snap to the same corner as before).
FGroupEdgeInserter::FGroupEdgeSplitPoint SnappedPoint;
int32 PointTopologyID, GroupID, BoundaryIndex;
bool bPointIsCorner;
FVector3d PreviewPoint;
FRay3d LocalRay;
if (GetHoveredItem(DevicePos.WorldRay, SnappedPoint, PointTopologyID, bPointIsCorner, PreviewPoint, &LocalRay))
{
// See if the point is not on the same vertex/edge but is on the same boundary
if (!(SnappedPoint.bIsVertex == StartPoint.bIsVertex && SnappedPoint.ElementID == StartPoint.ElementID)
&& GetSharedBoundary(*CurrentTopology, StartPoint, StartTopologyID, bStartIsCorner,
SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex))
{
ConditionallyUpdatePreview(SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex);
}
else
{
PreviewEdges.Reset(); // TODO: Maybe we should show a different color edge on a fail, rather than hiding it?
}
PreviewPoints.Add(PreviewPoint);
return true;
}
// If we don't have a valid endpoint, draw a line to the current hit location.
if (!bShowingBaseMesh)
{
ClearPreview(false);
}
PreviewEdges.Reset();
double RayT = 0;
int32 Tid = FDynamicMesh3::InvalidID;
if (MeshSpatial.FindNearestHitTriangle(LocalRay, RayT, Tid))
{
PreviewEdges.Emplace(PreviewPoints[0], LocalRay.PointAt(RayT));
return true;
}
return false;
}
}
check(false); // Each case has its own return, so shouldn't get here
return false;
}
void UGroupEdgeInsertionTool::OnEndHover()
{
switch (ToolState)
{
case EToolState::WaitingForInsertComplete:
case EToolState::GettingStart:
ClearPreview(true);
break;
case EToolState::GettingEnd:
// Keep the first preview point.
ClearPreview(false);
PreviewPoints.SetNum(1);
PreviewEdges.Reset();
}
}
FInputRayHit UGroupEdgeInsertionTool::IsHitByClick(const FInputDeviceRay& ClickPos)
{
FInputRayHit Hit;
switch (ToolState)
{
case EToolState::WaitingForInsertComplete:
break; // Keep hit invalid
// Same requirement for the other two cases: the click should go on an edge
case EToolState::GettingStart:
case EToolState::GettingEnd:
{
FVector3d RayPoint;
if (TopologyHitTest(ClickPos.WorldRay, RayPoint))
{
Hit = FInputRayHit(ClickPos.WorldRay.GetParameter((FVector)RayPoint));
}
break;
}
}
return Hit;
}
void UGroupEdgeInsertionTool::OnClicked(const FInputDeviceRay& ClickPos)
{
switch (ToolState)
{
case EToolState::WaitingForInsertComplete:
break; // Do nothing
case EToolState::GettingStart:
{
// Update start variables and switch state if successful
FVector3d PreviewPoint;
if (GetHoveredItem(ClickPos.WorldRay, StartPoint, StartTopologyID, bStartIsCorner, PreviewPoint))
{
PreviewPoints.Reset();
PreviewPoints.Add(PreviewPoint);
ToolState = EToolState::GettingEnd;
GetToolManager()->BeginUndoTransaction(LOCTEXT("GroupEdgeStartTransactionName", "Group Edge Start"));
GetToolManager()->EmitObjectChange(this, MakeUnique<FGroupEdgeInsertionFirstPointChange>(CurrentChangeStamp),
LOCTEXT("GroupEdgeStart", "Group Edge Start"));
GetToolManager()->EndUndoTransaction();
}
break;
}
case EToolState::GettingEnd:
{
// Don't update the end variables right away so that we can check if they actually changed (they
// won't when we snap to the same corner as before).
FVector3d PreviewPoint;
FGroupEdgeInserter::FGroupEdgeSplitPoint SnappedPoint;
int32 PointTopologyID, GroupID, BoundaryIndex;
bool bPointIsCorner;
if (GetHoveredItem(ClickPos.WorldRay, SnappedPoint, PointTopologyID, bPointIsCorner, PreviewPoint))
{
// See if the point is not on the same vertex/edge but is on the same boundary
if (!(SnappedPoint.bIsVertex == StartPoint.bIsVertex && SnappedPoint.ElementID == StartPoint.ElementID)
&& GetSharedBoundary(*CurrentTopology, StartPoint, StartTopologyID, bStartIsCorner,
SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex))
{
ConditionallyUpdatePreview(SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex);
ToolState = EToolState::WaitingForInsertComplete;
}
else
{
ClearPreview(false);
}
}
break;
}
}
}
bool UGroupEdgeInsertionTool::TopologyHitTest(const FRay& WorldRay,
FVector3d& RayPositionOut, FRay3d* LocalRayOut)
{
FTransform3d LocalToWorld(Cast<IPrimitiveComponentBackedTarget>(Target)->GetWorldTransform());
FRay3d LocalRay(LocalToWorld.InverseTransformPosition((FVector3d)WorldRay.Origin),
LocalToWorld.InverseTransformVector((FVector3d)WorldRay.Direction), false);
if (LocalRayOut)
{
*LocalRayOut = LocalRay;
}
FGroupTopologySelection Selection;
FVector3d Position, Normal;
if (TopologySelector.FindSelectedElement(TopologySelectorSettings,
LocalRay, Selection, Position, Normal))
{
RayPositionOut = LocalToWorld.TransformPosition(Position);
return true;
}
return false;
}
bool UGroupEdgeInsertionTool::GetHoveredItem(const FRay& WorldRay,
FGroupEdgeInserter::FGroupEdgeSplitPoint& PointOut,
int32& TopologyElementIDOut, bool& bIsCornerOut, FVector3d& PositionOut,
FRay3d* LocalRayOut)
{
TopologyElementIDOut = FDynamicMesh3::InvalidID;
PointOut.ElementID = FDynamicMesh3::InvalidID;
// Cast the ray to see what we hit.
FTransform3d LocalToWorld(Cast<IPrimitiveComponentBackedTarget>(Target)->GetWorldTransform());
FRay3d LocalRay(LocalToWorld.InverseTransformPosition((FVector3d)WorldRay.Origin),
LocalToWorld.InverseTransformVector((FVector3d)WorldRay.Direction), false);
if (LocalRayOut)
{
*LocalRayOut = LocalRay;
}
FGroupTopologySelection Selection;
FVector3d Position, Normal;
int32 EdgeSegmentID;
if (!TopologySelector.FindSelectedElement(
TopologySelectorSettings, LocalRay, Selection, Position, Normal, &EdgeSegmentID))
{
return false; // Didn't hit anything
}
else if (Selection.SelectedCornerIDs.Num() > 0)
{
// Point is a corner
TopologyElementIDOut = Selection.GetASelectedCornerID();
bIsCornerOut = true;
PointOut.bIsVertex = true;
PointOut.ElementID = CurrentTopology->GetCornerVertexID(TopologyElementIDOut);
// We can't initialize the tangent yet because the tangent of a corner will
// depend on which boundary it is a part of.
PositionOut = CurrentMesh->GetVertex(PointOut.ElementID);
}
else
{
// Point is an edge. We'll need to calculate the t value and some other things.
check(Selection.SelectedEdgeIDs.Num() > 0);
TopologyElementIDOut = Selection.GetASelectedEdgeID();
bIsCornerOut = false;
const FGroupTopology::FGroupEdge& GroupEdge = CurrentTopology->Edges[TopologyElementIDOut];
int32 Eid = GroupEdge.Span.Edges[EdgeSegmentID];
int32 StartVid = GroupEdge.Span.Vertices[EdgeSegmentID];
int32 EndVid = GroupEdge.Span.Vertices[EdgeSegmentID + 1];
FVector3d StartVert = CurrentMesh->GetVertex(StartVid);
FVector3d EndVert = CurrentMesh->GetVertex(EndVid);
FVector3d EdgeVector = EndVert - StartVert;
double EdgeLength = EdgeVector.Length();
check(EdgeLength > 0);
PointOut.Tangent = EdgeVector / EdgeLength;
FRay EdgeRay((FVector)StartVert, (FVector)PointOut.Tangent, true);
float DistDownEdge = EdgeRay.GetParameter((FVector)Position);
PositionOut = (FVector3d)EdgeRay.PointAt(DistDownEdge);
// See if the point is at a vertex in the group edge span.
if (DistDownEdge <= Settings->VertexTolerance)
{
PointOut.bIsVertex = true;
PointOut.ElementID = StartVid;
if (EdgeSegmentID > 0)
{
// Average with previous normalized edge vector
PointOut.Tangent += UE::Geometry::Normalized(StartVert - CurrentMesh->GetVertex(GroupEdge.Span.Vertices[EdgeSegmentID - 1]));
UE::Geometry::Normalize(PointOut.Tangent);
}
}
else if (abs(DistDownEdge - EdgeLength) <= Settings->VertexTolerance)
{
PointOut.bIsVertex = true;
PointOut.ElementID = EndVid;
if (EdgeSegmentID + 2 < GroupEdge.Span.Vertices.Num())
{
PointOut.Tangent += UE::Geometry::Normalized(CurrentMesh->GetVertex(GroupEdge.Span.Vertices[EdgeSegmentID + 2]) - EndVert);
UE::Geometry::Normalize(PointOut.Tangent);
}
}
else
{
PointOut.bIsVertex = false;
PointOut.ElementID = Eid;
PointOut.EdgeTValue = DistDownEdge / EdgeLength;
if (CurrentMesh->GetEdgeV(Eid).A != StartVid)
{
PointOut.EdgeTValue = 1 - PointOut.EdgeTValue;
}
}
}
return true;
}
void UGroupEdgeInsertionTool::GetCornerTangent(int32 CornerID, int32 GroupID, int32 BoundaryIndex, FVector3d& TangentOut)
{
TangentOut = FVector3d::Zero();
int32 CornerVid = CurrentTopology->GetCornerVertexID(CornerID);
check(CornerVid != FDynamicMesh3::InvalidID);
const FGroupTopology::FGroup* Group = CurrentTopology->FindGroupByID(GroupID);
check(Group && BoundaryIndex >= 0 && BoundaryIndex < Group->Boundaries.Num());
const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[BoundaryIndex];
TArray<FVector3d> AdjacentPoints;
for (int32 GroupEdgeID : Boundary.GroupEdges)
{
TArray<int32> Vertices = CurrentTopology->Edges[GroupEdgeID].Span.Vertices;
if (Vertices[0] == CornerVid)
{
AdjacentPoints.Add(CurrentMesh->GetVertex(Vertices[1]));
}
else if (Vertices.Last() == CornerVid)
{
AdjacentPoints.Add(CurrentMesh->GetVertex(Vertices[Vertices.Num()-2]));
}
}
check(AdjacentPoints.Num() == 2);
FVector3d CornerPosition = CurrentMesh->GetVertex(CornerVid);
TangentOut = UE::Geometry::Normalized(CornerPosition - AdjacentPoints[0]);
TangentOut += UE::Geometry::Normalized(AdjacentPoints[1] - CornerPosition);
UE::Geometry::Normalize(TangentOut);
}
bool GetSharedBoundary(const FGroupTopology& Topology,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, int32 StartTopologyID, bool bStartIsCorner,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint, int32 EndTopologyID, bool bEndIsCorner,
int32& GroupIDOut, int32& BoundaryIndexOut)
{
// The start and endpoints could be on the same boundary of multiple groups at
// the same time, and sometimes we won't be able to resolve the ambiguity
// (one example is a sphere split into two equal groups, but could even happen
// with more than two groups when endpoints are corners).
// Sometimes there are things we can do to eliminate some contenders- the best
// approach is probably trying to do a plane cut for all of the options and
// removing those that fail. However, it's worth noting that such issues won't
// arise in the standard application of this tool for low-poly modeling, where
// groups are planar, so it's not worth the bother.
// Instead, we'll just take one of the results arbitrarily, though we will try to
// take one that has a single boundary (this will prefer a cylinder cap over
// a cylinder side).
// TODO: The code would be simpler if we didn't even want to do that filtering- we'd
// just return the first result we found. Should we consider doing that?
GroupIDOut = FDynamicMesh3::InvalidID;
BoundaryIndexOut = FDynamicMesh3::InvalidID;
TArray<TPair<int32,int32>> CandidateGroupIDsAndBoundaryIndices;
if (bStartIsCorner)
{
// Go through all neighboring groups and their boundaries to find a shared one.
const FGroupTopology::FCorner& StartCorner = Topology.Corners[StartTopologyID];
for (int32 GroupID : StartCorner.NeighbourGroupIDs)
{
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID);
for (int32 i = 0; i < Group->Boundaries.Num(); ++i)
{
const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i];
if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner)
&& DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner))
{
CandidateGroupIDsAndBoundaryIndices.Emplace(GroupID, i);
break; // Can't share more than one boundary in the same group
}
}
}
}
else
{
// Start is on an edge, so there are fewer boundaries to look through.
const FGroupTopology::FGroupEdge& GroupEdge = Topology.Edges[StartTopologyID];
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupEdge.Groups.A);
for (int32 i = 0; i < Group->Boundaries.Num(); ++i)
{
const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i];
if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner)
&& DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner))
{
CandidateGroupIDsAndBoundaryIndices.Emplace(GroupEdge.Groups.A, i);
break;
}
}
if (GroupEdge.Groups.B != FDynamicMesh3::InvalidID)
{
Group = Topology.FindGroupByID(GroupEdge.Groups.B);
for (int32 i = 0; i < Group->Boundaries.Num(); ++i)
{
const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i];
if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner)
&& DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner))
{
CandidateGroupIDsAndBoundaryIndices.Emplace(GroupEdge.Groups.B, i);
break;
}
}
}
}
if (CandidateGroupIDsAndBoundaryIndices.Num() == 0)
{
return false;
}
// Prefer a result that has a single boundary if there are multiple.
if (CandidateGroupIDsAndBoundaryIndices.Num() > 1)
{
for (const TPair<int32, int32>& GroupIDBoundaryIdxPair : CandidateGroupIDsAndBoundaryIndices)
{
if (Topology.FindGroupByID(GroupIDBoundaryIdxPair.Key)->Boundaries.Num() == 1)
{
GroupIDOut = GroupIDBoundaryIdxPair.Key;
BoundaryIndexOut = 0;
return true;
}
}
}
GroupIDOut = CandidateGroupIDsAndBoundaryIndices[0].Key;
BoundaryIndexOut = CandidateGroupIDsAndBoundaryIndices[0].Value;
return true;
}
bool DoesBoundaryContainPoint(const FGroupTopology& Topology,
const FGroupTopology::FGroupBoundary& Boundary, int32 PointTopologyID, bool bPointIsCorner)
{
for (int32 GroupEdgeID : Boundary.GroupEdges)
{
if (!bPointIsCorner && GroupEdgeID == PointTopologyID)
{
return true;
}
const FGroupTopology::FGroupEdge& GroupEdge = Topology.Edges[GroupEdgeID];
if (bPointIsCorner && (GroupEdge.EndpointCorners.A == PointTopologyID
|| GroupEdge.EndpointCorners.B == PointTopologyID))
{
return true;
}
}
return false;
}
// Undo/redo support
void FGroupEdgeInsertionFirstPointChange::Revert(UObject* Object)
{
UGroupEdgeInsertionTool* Tool = Cast<UGroupEdgeInsertionTool>(Object);
check(Tool->ToolState == UGroupEdgeInsertionTool::EToolState::GettingEnd);
Tool->ToolState = UGroupEdgeInsertionTool::EToolState::GettingStart;
Tool->ClearPreview();
bHaveDoneUndo = true;
}
void FGroupEdgeInsertionChange::Apply(UObject* Object)
{
UGroupEdgeInsertionTool* Tool = Cast<UGroupEdgeInsertionTool>(Object);
MeshChange->Apply(Tool->CurrentMesh.Get(), false);
Tool->MeshSpatial.Build();
Tool->TopologySelector.Invalidate(true, true);
Tool->CurrentTopology->RebuildTopology();
Tool->ClearPreview(true, true);
}
void FGroupEdgeInsertionChange::Revert(UObject* Object)
{
UGroupEdgeInsertionTool* Tool = Cast<UGroupEdgeInsertionTool>(Object);
MeshChange->Apply(Tool->CurrentMesh.Get(), true);
Tool->MeshSpatial.Build();
Tool->TopologySelector.Invalidate(true, true);
Tool->CurrentTopology->RebuildTopology();
Tool->ClearPreview(true, true);
}
#undef LOCTEXT_NAMESPACE