Files
UnrealEngineUWP/Engine/Plugins/Experimental/MeshModelingToolsetExp/Source/MeshModelingToolsExp/Private/HoleFillTool.cpp

474 lines
14 KiB
C++
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
#include "HoleFillTool.h"
#include "ToolBuilderUtil.h"
#include "InteractiveToolManager.h"
#include "MeshDescriptionToDynamicMesh.h"
#include "ToolSetupUtil.h"
#include "DynamicMesh/MeshNormals.h"
#include "Changes/DynamicMeshChangeTarget.h"
#include "DynamicMeshToMeshDescription.h"
#include "BaseBehaviors/SingleClickBehavior.h"
#include "BaseBehaviors/MouseHoverBehavior.h"
#include "MeshBoundaryLoops.h"
#include "MeshOpPreviewHelpers.h"
#include "Selection/BoundarySelectionMechanic.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
ModelingTools: Reduce surface area of MeshDescriptionProvider/Committer, replace with UE::ToolTarget:: calls where possible. Add new UE::ToolTarget::CommitMeshDescriptionUpdateViaDynamicMesh() function. This is being used for now to avoid potential regressions as UE::ToolTarget::CommitDynamicMeshUpdate will preferentially use DynamicMeshCommitter, and I am not certain it is functionally equivalent in all cases. Add new UE::ToolTarget::CommitDynamicMeshNormalsUpdate(), similar to existing UV version Add new Move-variant of UE::ToolTarget::CommitMeshDescriptionUpdate(), uses new Move-variant of IMeshDescriptionCommitter::CommitMeshDescription. Make existing IMeshDescriptionCommitter::CommitMeshDescription callback interface protected, to prevent usage of this function at public API level (will be removed in future). Tool updates should not change, just using cleaner APIs. EditNormalsTool now uses CommitDynamicMeshNormalsUpdate(), which does go via DynamicMeshCommitter preferentially, where it previously went via MeshDescriptionCommitter. In light testing the results appear equivalent. AttributeEditorTool now operates on MeshDescription copies in various update functions. These are not performance-critical. #rb rinat.abdrashitov #rnx #preflight 61ae45998358693a22c28d1b #ROBOMERGE-AUTHOR: ryan.schmidt #ROBOMERGE-SOURCE: CL 18384350 in //UE5/Release-5.0/... via CL 18384361 #ROBOMERGE-BOT: STARSHIP (Release-Engine-Staging -> Release-Engine-Test) (v896-18170469) [CL 18384373 by ryan schmidt in ue5-release-engine-test branch]
2021-12-06 12:42:19 -05:00
#include "ModelingToolTargetUtil.h"
#include "ToolTargetManager.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(HoleFillTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UHoleFillTool"
/*
* ToolBuilder
*/
USingleSelectionMeshEditingTool* UHoleFillToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
return NewObject<UHoleFillTool>(SceneState.ToolManager);
}
bool UHoleFillToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
// We're disallowing volumes because they shouldn't have holes to fill intrinsically.
return USingleSelectionMeshEditingToolBuilder::CanBuildTool(SceneState) &&
SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(),
[](UActorComponent& Component) { return !ToolBuilderUtil::IsVolume(Component); }) >= 1;
}
/*
* Tool properties
*/
void UHoleFillToolActions::PostAction(EHoleFillToolActions Action)
{
if (ParentTool.IsValid())
{
ParentTool->RequestAction(Action);
}
}
void UHoleFillStatisticsProperties::Initialize(const UHoleFillTool& HoleFillTool)
{
if (!HoleFillTool.BoundaryLoops.IsValid())
{
return;
}
int Initial = HoleFillTool.BoundaryLoops->Loops.Num();
int Selected = 0;
int Successful = 0;
int Failed = 0;
int Remaining = Initial;
InitialHoles = FString::FromInt(Initial);
SelectedHoles = FString::FromInt(Selected);
SuccessfulFills = FString::FromInt(Successful);
FailedFills = FString::FromInt(Failed);
RemainingHoles = FString::FromInt(Remaining);
}
void UHoleFillStatisticsProperties::Update(const UHoleFillTool& HoleFillTool, const FHoleFillOp& Op)
{
if (!HoleFillTool.BoundaryLoops.IsValid())
{
return;
}
int Initial = HoleFillTool.BoundaryLoops->Loops.Num();
int Selected = Op.Loops.Num();
int Failed = Op.NumFailedLoops;
int Successful = Selected - Failed;
int Remaining = Initial - Successful;
InitialHoles = FString::FromInt(Initial);
SelectedHoles = FString::FromInt(Selected);
SuccessfulFills = FString::FromInt(Successful);
FailedFills = FString::FromInt(Failed);
RemainingHoles = FString::FromInt(Remaining);
}
/*
* Op Factory
*/
TUniquePtr<FDynamicMeshOperator> UHoleFillOperatorFactory::MakeNewOperator()
{
TUniquePtr<FHoleFillOp> FillOp = MakeUnique<FHoleFillOp>();
FTransform LocalToWorld = Cast<IPrimitiveComponentBackedTarget>(FillTool->Target)->GetWorldTransform();
FillOp->SetResultTransform((FTransformSRT3d)LocalToWorld);
FillOp->OriginalMesh = FillTool->OriginalMesh;
FillOp->MeshUVScaleFactor = FillTool->MeshUVScaleFactor;
FillTool->GetLoopsToFill(FillOp->Loops);
FillOp->FillType = FillTool->Properties->FillType;
FillOp->FillOptions.bRemoveIsolatedTriangles = FillTool->Properties->bRemoveIsolatedTriangles;
FillOp->FillOptions.bQuickFillSmallHoles = FillTool->Properties->bQuickFillSmallHoles;
// Smooth fill properties
FillOp->SmoothFillOptions = FillTool->SmoothHoleFillProperties->ToSmoothFillOptions();
return FillOp;
}
/*
* Tool
*/
void UHoleFillTool::Setup()
{
USingleSelectionTool::Setup();
if (!Target)
{
return;
}
// create mesh to operate on
OriginalMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>();
*OriginalMesh = UE::ToolTarget::GetDynamicMeshCopy(Target);
// initialize properties
Properties = NewObject<UHoleFillToolProperties>(this, TEXT("Hole Fill Settings"));
Properties->RestoreProperties(this);
AddToolPropertySource(Properties);
SetToolPropertySourceEnabled(Properties, true);
SmoothHoleFillProperties = NewObject<USmoothHoleFillProperties>(this, TEXT("Smooth Fill Settings"));
SmoothHoleFillProperties->RestoreProperties(this);
AddToolPropertySource(SmoothHoleFillProperties);
SetToolPropertySourceEnabled(SmoothHoleFillProperties, Properties->FillType == EHoleFillOpFillType::Smooth);
// Set up a callback for when the type of fill changes
Properties->WatchProperty(Properties->FillType,
[this](EHoleFillOpFillType NewType)
{
SetToolPropertySourceEnabled(SmoothHoleFillProperties, (NewType == EHoleFillOpFillType::Smooth));
});
Actions = NewObject<UHoleFillToolActions>(this, TEXT("Hole Fill Actions"));
Actions->Initialize(this);
AddToolPropertySource(Actions);
SetToolPropertySourceEnabled(Actions, true);
Statistics = NewObject<UHoleFillStatisticsProperties>();
AddToolPropertySource(Statistics);
SetToolPropertySourceEnabled(Statistics, true);
ToolPropertyObjects.Add(this);
// initialize hit query
MeshSpatial.SetMesh(OriginalMesh.Get());
// initialize topology
constexpr bool bAutoComputeLoops = true;
BoundaryLoops = MakeUnique<UE::Geometry::FMeshBoundaryLoops>(OriginalMesh.Get(), bAutoComputeLoops);
const bool bLoopsOK = !BoundaryLoops->bAborted;
if (bLoopsOK && BoundaryLoops->Spans.Num() > 0)
{
// Boundary loop finding finished but some boundaries were too degenerate to form simple loops (this might be due to the
// presence of bowtie vertices on boundaries, for example)
// Since this tool can tolerate failing to fill a boundary loop, just convert the spans to loops and let them fail
for (const FEdgeSpan& Span : BoundaryLoops->Spans)
{
// skip span if there is no edge connecting the first and last vertices, as these cannot be initialized as loops
if (Span.Vertices.Num() < 2 || FDynamicMesh3::InvalidID == OriginalMesh->FindEdge(Span.Vertices[0], Span.Vertices.Last()))
{
continue;
}
FEdgeLoop SpanLoop(OriginalMesh.Get());
if (SpanLoop.InitializeFromVertices(Span.Vertices))
{
BoundaryLoops->Loops.Add(SpanLoop);
}
}
BoundaryLoops->Spans.Empty();
}
// Set up selection mechanic to find and select edges
IPrimitiveComponentBackedTarget* TargetComponent = Cast<IPrimitiveComponentBackedTarget>(Target);
SelectionMechanic = NewObject<UBoundarySelectionMechanic>(this);
SelectionMechanic->bAddSelectionFilterPropertiesToParentTool = false;
SelectionMechanic->Setup(this);
SelectionMechanic->Properties->bSelectEdges = true;
SelectionMechanic->Properties->bSelectFaces = false;
SelectionMechanic->Properties->bSelectVertices = false;
SelectionMechanic->Initialize(OriginalMesh.Get(),
(FTransform3d)TargetComponent->GetWorldTransform(),
GetTargetWorld(),
BoundaryLoops.Get(),
[this]() { return &MeshSpatial; }
);
// allow toggling selection without modifier key
SelectionMechanic->SetShouldAddToSelectionFunc([]() {return true; });
SelectionMechanic->SetShouldRemoveFromSelectionFunc([]() {return true; });
SelectionMechanic->OnSelectionChanged.AddUObject(this, &UHoleFillTool::OnSelectionModified);
// Store a UV scale based on the original mesh bounds
MeshUVScaleFactor = (1.0 / OriginalMesh->GetBounds().MaxDim());
Statistics->Initialize(*this);
// initialize the PreviewMesh+BackgroundCompute object
SetupPreview();
InvalidatePreviewResult();
if (!bLoopsOK)
{
GetToolManager()->DisplayMessage(
LOCTEXT("LoopFindError", "Error finding hole boundary loops."),
EToolMessageLevel::UserWarning);
SetToolPropertySourceEnabled(Properties, false);
SetToolPropertySourceEnabled(SmoothHoleFillProperties, false);
SetToolPropertySourceEnabled(Actions, false);
}
else if (BoundaryLoops->Loops.Num() == 0)
{
GetToolManager()->DisplayMessage(
LOCTEXT("NoHoleNotification", "This mesh has no holes to fill."),
EToolMessageLevel::UserWarning);
SetToolPropertySourceEnabled(Properties, false);
SetToolPropertySourceEnabled(SmoothHoleFillProperties, false);
SetToolPropertySourceEnabled(Actions, false);
}
else
{
GetToolManager()->DisplayMessage(
LOCTEXT("HoleFillToolHighlighted", "Holes in the mesh are highlighted. Select individual holes to fill or use the Select All or Clear buttons."),
EToolMessageLevel::UserNotification);
// Hide all meshes except the Preview
TargetComponent->SetOwnerVisibility(false);
}
SetToolDisplayName(LOCTEXT("ToolName", "Fill Holes"));
GetToolManager()->DisplayMessage(
LOCTEXT("HoleFillToolDescription", "Fill Holes in the selected Mesh by adding triangles. Click on individual holes to fill them, or use the Select All button to fill all holes."),
EToolMessageLevel::UserNotification);
}
void UHoleFillTool::OnTick(float DeltaTime)
{
if (Preview)
{
Preview->Tick(DeltaTime);
}
if (bHavePendingAction)
{
ApplyAction(PendingAction);
bHavePendingAction = false;
PendingAction = EHoleFillToolActions::NoAction;
}
}
void UHoleFillTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
InvalidatePreviewResult();
}
bool UHoleFillTool::CanAccept() const
{
return Super::CanAccept() && Preview->HaveValidResult();
}
void UHoleFillTool::OnShutdown(EToolShutdownType ShutdownType)
{
Properties->SaveProperties(this);
SmoothHoleFillProperties->SaveProperties(this);
if (SelectionMechanic)
{
SelectionMechanic->Shutdown();
}
Cast<IPrimitiveComponentBackedTarget>(Target)->SetOwnerVisibility(true);
FDynamicMeshOpResult Result = Preview->Shutdown();
if (ShutdownType == EToolShutdownType::Accept)
{
GetToolManager()->BeginUndoTransaction(LOCTEXT("HoleFillToolTransactionName", "Hole Fill Tool"));
check(Result.Mesh.Get() != nullptr);
ModelingTools: Reduce surface area of MeshDescriptionProvider/Committer, replace with UE::ToolTarget:: calls where possible. Add new UE::ToolTarget::CommitMeshDescriptionUpdateViaDynamicMesh() function. This is being used for now to avoid potential regressions as UE::ToolTarget::CommitDynamicMeshUpdate will preferentially use DynamicMeshCommitter, and I am not certain it is functionally equivalent in all cases. Add new UE::ToolTarget::CommitDynamicMeshNormalsUpdate(), similar to existing UV version Add new Move-variant of UE::ToolTarget::CommitMeshDescriptionUpdate(), uses new Move-variant of IMeshDescriptionCommitter::CommitMeshDescription. Make existing IMeshDescriptionCommitter::CommitMeshDescription callback interface protected, to prevent usage of this function at public API level (will be removed in future). Tool updates should not change, just using cleaner APIs. EditNormalsTool now uses CommitDynamicMeshNormalsUpdate(), which does go via DynamicMeshCommitter preferentially, where it previously went via MeshDescriptionCommitter. In light testing the results appear equivalent. AttributeEditorTool now operates on MeshDescription copies in various update functions. These are not performance-critical. #rb rinat.abdrashitov #rnx #preflight 61ae45998358693a22c28d1b #ROBOMERGE-AUTHOR: ryan.schmidt #ROBOMERGE-SOURCE: CL 18384350 in //UE5/Release-5.0/... via CL 18384361 #ROBOMERGE-BOT: STARSHIP (Release-Engine-Staging -> Release-Engine-Test) (v896-18170469) [CL 18384373 by ryan schmidt in ue5-release-engine-test branch]
2021-12-06 12:42:19 -05:00
UE::ToolTarget::CommitMeshDescriptionUpdateViaDynamicMesh(Target, *Result.Mesh.Get(), true);
GetToolManager()->EndUndoTransaction();
}
}
void UHoleFillTool::OnSelectionModified()
{
UpdateActiveBoundaryLoopSelection();
InvalidatePreviewResult();
}
void UHoleFillTool::RequestAction(EHoleFillToolActions ActionType)
{
if (bHavePendingAction)
{
return;
}
PendingAction = ActionType;
bHavePendingAction = true;
}
void UHoleFillTool::InvalidatePreviewResult()
{
// Clear any warning message
GetToolManager()->DisplayMessage({}, EToolMessageLevel::UserWarning);
Preview->InvalidateResult();
}
void UHoleFillTool::SetupPreview()
{
UHoleFillOperatorFactory* OpFactory = NewObject<UHoleFillOperatorFactory>();
OpFactory->FillTool = this;
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(OpFactory, "Preview");
Preview->Setup(GetTargetWorld(), OpFactory);
ToolSetupUtil::ApplyRenderingConfigurationToPreview(Preview->PreviewMesh, Target);
FComponentMaterialSet MaterialSet;
Cast<IMaterialProvider>(Target)->GetMaterialSet(MaterialSet);
Preview->ConfigureMaterials(MaterialSet.Materials,
ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())
);
// configure secondary render material
UMaterialInterface* SelectionMaterial = ToolSetupUtil::GetSelectionMaterial(FLinearColor(0.8f, 0.75f, 0.0f), GetToolManager());
if (SelectionMaterial != nullptr)
{
Preview->PreviewMesh->SetSecondaryRenderMaterial(SelectionMaterial);
}
// enable secondary triangle buffers
Preview->OnOpCompleted.AddLambda(
[this](const FDynamicMeshOperator* Op)
{
const FHoleFillOp* HoleFillOp = (const FHoleFillOp*)(Op);
NewTriangleIDs = TSet<int32>(HoleFillOp->NewTriangles);
// Notify the user if any holes could not be filled
if (HoleFillOp->NumFailedLoops > 0)
{
GetToolManager()->DisplayMessage(
FText::Format(LOCTEXT("FillFailNotification", "Failed to fill {0} holes."), HoleFillOp->NumFailedLoops),
EToolMessageLevel::UserWarning);
}
Statistics->Update(*this, *HoleFillOp);
});
Preview->PreviewMesh->EnableSecondaryTriangleBuffers(
[this](const FDynamicMesh3* Mesh, int32 TriangleID)
{
return NewTriangleIDs.Contains(TriangleID);
});
// set initial preview to un-processed mesh
Preview->PreviewMesh->SetTransform(Cast<IPrimitiveComponentBackedTarget>(Target)->GetWorldTransform());
Preview->PreviewMesh->UpdatePreview(OriginalMesh.Get());
Preview->SetVisibility(true);
}
void UHoleFillTool::ApplyAction(EHoleFillToolActions ActionType)
{
switch (ActionType)
{
case EHoleFillToolActions::SelectAll:
SelectAll();
break;
case EHoleFillToolActions::ClearSelection:
ClearSelection();
break;
}
}
void UHoleFillTool::SelectAll()
{
FGroupTopologySelection NewSelection;
for (int32 i = 0; i < BoundaryLoops->Loops.Num(); ++i)
{
NewSelection.SelectedEdgeIDs.Add(i);
}
SelectionMechanic->SetSelection(NewSelection);
UpdateActiveBoundaryLoopSelection();
InvalidatePreviewResult();
}
void UHoleFillTool::ClearSelection()
{
SelectionMechanic->ClearSelection();
UpdateActiveBoundaryLoopSelection();
InvalidatePreviewResult();
}
void UHoleFillTool::UpdateActiveBoundaryLoopSelection()
{
ActiveBoundaryLoopSelection.Reset();
const FGroupTopologySelection& ActiveSelection = SelectionMechanic->GetActiveSelection();
int NumEdges = ActiveSelection.SelectedEdgeIDs.Num();
if (NumEdges == 0)
{
return;
}
ActiveBoundaryLoopSelection.Reserve(NumEdges);
for (int32 EdgeID : ActiveSelection.SelectedEdgeIDs)
{
FSelectedBoundaryLoop& Loop = ActiveBoundaryLoopSelection.Emplace_GetRef();
Loop.EdgeTopoID = EdgeID;
Loop.EdgeIDs = BoundaryLoops->Loops[EdgeID].Edges;
}
}
void UHoleFillTool::Render(IToolsContextRenderAPI* RenderAPI)
{
if (SelectionMechanic)
{
SelectionMechanic->Render(RenderAPI);
}
}
void UHoleFillTool::GetLoopsToFill(TArray<FEdgeLoop>& OutLoops) const
{
OutLoops.Reset();
check(BoundaryLoops.IsValid());
for (const FSelectedBoundaryLoop& FillEdge : ActiveBoundaryLoopSelection)
{
if (OriginalMesh->IsBoundaryEdge(FillEdge.EdgeIDs[0])) // may no longer be boundary due to previous fill
{
int32 LoopID = BoundaryLoops->FindLoopContainingEdge(FillEdge.EdgeIDs[0]);
if (LoopID >= 0)
{
OutLoops.Add(BoundaryLoops->Loops[LoopID]);
}
}
}
}
#undef LOCTEXT_NAMESPACE