Files
UnrealEngine/Engine/Plugins/Experimental/MeshModelingToolsetExp/Source/MeshModelingToolsExp/Private/ExtractSplineTool.cpp
2025-05-18 13:04:45 +08:00

649 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ExtractSplineTool.h"
#include "InteractiveToolManager.h"
#include "ToolBuilderUtil.h"
#include "ToolSetupUtil.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMesh/DynamicMeshTriangleAttribute.h"
#include "DynamicMeshEditor.h"
#include "Selection/SelectClickedAction.h"
#include "MeshDescriptionToDynamicMesh.h"
#include "DynamicMeshToMeshDescription.h"
#include "InteractiveGizmoManager.h"
#include "BaseGizmos/GizmoComponents.h"
#include "BaseGizmos/CombinedTransformGizmo.h"
#include "Drawing/MeshDebugDrawing.h"
#include "ModelingObjectsCreationAPI.h"
#include "Changes/ToolCommandChangeSequence.h"
#include "CuttingOps/PlaneCutOp.h"
#include "Misc/MessageDialog.h"
#include "ModelingToolTargetUtil.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include "ModelingToolTargetUtil.h"
#include "TransformTypes.h"
#include "Components/SplineComponent.h"
#include "Selection/ToolSelectionUtil.h"
#include "Drawing/PreviewGeometryActor.h"
#include "VertexConnectedComponents.h"
#include "Selection/PolygonSelectionMechanic.h"
#include "CurveOps/GenerateCrossSectionOp.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(ExtractSplineTool)
#define LOCTEXT_NAMESPACE "UExtractSplineTool"
namespace ExtractSplineToolLocals
{
const FString& CutlinePointSetID(TEXT("CutlinePointSet"));
const FString& CutlineLineSetID(TEXT("CutlineLineSet"));
USplineComponent* CreateNewSplineInActor(UInteractiveToolManager* ToolManager, AActor* Actor, bool bTransact = false, bool bSetAsRoot = false)
{
FCreateComponentParams Params;
Params.HostActor = Actor;
Params.BaseName = ""; //Use the autogenerated name instead
Params.bSetAsRoot = bSetAsRoot;
Params.bTransact = bTransact;
Params.ComponentClass = USplineComponent::StaticClass();
FCreateComponentResult Result = UE::Modeling::CreateNewComponentOnActor(ToolManager, MoveTemp(Params));
if (Result.IsOK())
{
return StaticCast<USplineComponent*>(Result.NewComponent);
}
else
{
return nullptr;
}
};
}
UExtractSplineTool* UExtractSplineToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
UExtractSplineTool* NewTool = NewObject<UExtractSplineTool>(SceneState.ToolManager);
return NewTool;
}
void UExtractSplineTool::Setup()
{
UInteractiveTool::Setup();
Settings = NewObject<UExtractSplineToolProperties>(this);
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
// Convert input target to dynamic mesh
OriginalMesh = MakeShared<FDynamicMesh3>();
*OriginalMesh = UE::ToolTarget::GetDynamicMeshCopy(Target);
OriginalMesh->EnableAttributes();
Topology = MakeShared<FGroupTopology, ESPMode::ThreadSafe>(OriginalMesh.Get(), false);
Topology->RebuildTopology();
// set initial cut plane (also attaches gizmo/proxy)
FBox CombinedBounds; CombinedBounds.Init();
FVector ComponentOrigin, ComponentExtents;
UE::ToolTarget::GetTargetActor(Target)->GetActorBounds(false, ComponentOrigin, ComponentExtents);
CombinedBounds += FBox::BuildAABB(ComponentOrigin, ComponentExtents);
CutPlaneWorld.Origin = (FVector3d)CombinedBounds.GetCenter();
PlaneMechanic = NewObject<UConstructionPlaneMechanic>(this);
PlaneMechanic->Setup(this);
PlaneMechanic->Initialize(GetTargetWorld(), CutPlaneWorld);
PlaneMechanic->CanUpdatePlaneFunc = [this]() {return Settings->ExtractionMode == EExtractSplineMode::PlaneCut; };
PlaneMechanic->OnPlaneChanged.AddLambda([this]() {
CutPlaneWorld = PlaneMechanic->Plane;
InvalidatePreviews();
});
PlaneMechanic->SetPlaneCtrlClickBehaviorTarget->InvisibleComponentsToHitTest.Add(UE::ToolTarget::GetTargetComponent(Target));
// set up SelectionMechanic
SelectionMechanic = NewObject<UPolygonSelectionMechanic>(this);
SelectionMechanic->bAddSelectionFilterPropertiesToParentTool = false; // We'll do this ourselves later
SelectionMechanic->Setup(this);
SelectionMechanic->Properties->RestoreProperties(this);
SelectionMechanic->Properties->bCanSelectFaces = true;
SelectionMechanic->Properties->bCanSelectVertices = false;
SelectionMechanic->Properties->bCanSelectEdges = true;
SelectionMechanic->Properties->bDisplayPolygroupReliantControls = true;
SelectionMechanic->Properties->bEnableMarquee = false;
SelectionMechanic->OnSelectionChanged.AddUObject(this, &UExtractSplineTool::InvalidatePreviews);
SelectionMechanic->SetShouldAddToSelectionFunc([]() {return false; });
SelectionMechanic->SetShouldRemoveFromSelectionFunc([]() {return false; });
SelectionMechanic->Initialize(OriginalMesh.Get(),
(FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target),
GetTargetWorld(),
Topology.Get(),
[this]() { return &GetSpatial(); }
);
AddToolPropertySource(SelectionMechanic->Properties);
SetupPreviews();
InvalidatePreviews();
RegeneratePreviewSplines();
SetInteractionMode();
}
void UExtractSplineTool::Shutdown(EToolShutdownType ShutdownType)
{
Settings->SaveProperties(this);
if (ShutdownType == EToolShutdownType::Accept)
{
GenerateAsset();
}
if (PlaneMechanic)
{
PlaneMechanic->Shutdown();
}
if (SelectionMechanic)
{
SelectionMechanic->Properties->SaveProperties(this);
RemoveToolPropertySource(SelectionMechanic->Properties);
SelectionMechanic->Shutdown();
}
if (Preview)
{
Preview->Shutdown();
}
if (CutlineGeometry)
{
CutlineGeometry->Disconnect();
CutlineGeometry = nullptr;
}
Super::Shutdown(ShutdownType);
}
void UExtractSplineTool::OnTick(float DeltaTime)
{
SelectionMechanic->Tick(DeltaTime);
PlaneMechanic->Tick(DeltaTime);
Preview->Tick(DeltaTime);
}
bool UExtractSplineTool::CanAccept() const
{
return CutLoops.Num() > 0 || CutSpans.Num() > 0;
}
void UExtractSplineTool::Render(IToolsContextRenderAPI* RenderAPI)
{
if (PlaneMechanic && Settings->ExtractionMode == EExtractSplineMode::PlaneCut)
{
PlaneMechanic->Render(RenderAPI);
}
if (SelectionMechanic && Settings->ExtractionMode == EExtractSplineMode::PolygroupLoops)
{
SelectionMechanic->Render(RenderAPI);
}
}
void UExtractSplineTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
SetInteractionMode();
InvalidatePreviews();
}
void UExtractSplineTool::SetInteractionMode()
{
PlaneMechanic->bShowGrid = (Settings->ExtractionMode == EExtractSplineMode::PlaneCut);
SelectionMechanic->SetIsEnabled(Settings->ExtractionMode == EExtractSplineMode::PolygroupLoops);
SetToolPropertySourceEnabled(SelectionMechanic->Properties, Settings->ExtractionMode == EExtractSplineMode::PolygroupLoops);
}
void UExtractSplineTool::SetupPreviews()
{
Factory = NewObject<UGenerateCrossSectionOpFactory>();
GetCutPlane(Factory->LocalPlaneOrigin, Factory->LocalPlaneNormal);
Factory->bSimplifyAlongNewEdges = true;
Factory->OriginalMesh = OriginalMesh;
Factory->TargetTransform = (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target);
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(Factory, "Preview");
Preview->Setup(GetTargetWorld(), Factory);
ToolSetupUtil::ApplyRenderingConfigurationToPreview(Preview->PreviewMesh, nullptr);
Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated);
const FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target);
Preview->ConfigureMaterials(MaterialSet.Materials,
ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())
);
// set initial preview to un-processed mesh, so stuff doesn't just disappear if the first cut takes a while
Preview->PreviewMesh->UpdatePreview(OriginalMesh.Get());
Preview->PreviewMesh->SetTransform((FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target));
Preview->OnOpCompleted.AddWeakLambda(this,
[this](const UE::Geometry::FDynamicMeshOperator* Op)
{
const UE::Geometry::FGenerateCrossSectionOp* GenerateCrossSectionOp = (const UE::Geometry::FGenerateCrossSectionOp*)(Op);
CutLoops = GenerateCrossSectionOp->GetCutLoops();
CutSpans = GenerateCrossSectionOp->GetCutSpans();
RegeneratePreviewSplines();
}
);
// Set up all the components we need to visualize things.
CutlineGeometry = NewObject<UPreviewGeometry>();
CutlineGeometry->CreateInWorld(GetTargetWorld(), (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target));
// These visualize the current spline edges that would be extracted
CutlineGeometry->AddPointSet(ExtractSplineToolLocals::CutlinePointSetID);
CutlineGeometry->AddLineSet(ExtractSplineToolLocals::CutlineLineSetID);
UpdateVisibility();
}
void UExtractSplineTool::InvalidatePreviews()
{
switch (Settings->ExtractionMode)
{
case EExtractSplineMode::PlaneCut:
GetCutPlane(Factory->LocalPlaneOrigin, Factory->LocalPlaneNormal);
Preview->InvalidateResult();
break;
case EExtractSplineMode::OpenBoundary:
GatherSplineDataFromMeshBoundaries();
RegeneratePreviewSplines();
break;
case EExtractSplineMode::PolygroupLoops:
GatherSplineDataFromPolygroupSelection();
RegeneratePreviewSplines();
break;
default:
ensure(false);
}
}
void UExtractSplineTool::GetCutPlane(FVector& Origin, FVector& Normal)
{
FTransform LocalToWorld = (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target);
// for all plane computation, change LocalToWorld to not have any zero scale dims
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);
Origin = LocalToWorld.InverseTransformPosition((FVector)CutPlaneWorld.Origin);
FVector3d WorldNormal = CutPlaneWorld.GetAxis(2);
UE::Geometry::FTransformSRT3d L2WForNormal(LocalToWorld);
Normal = (FVector)L2WForNormal.InverseTransformNormal(WorldNormal);
}
void UExtractSplineTool::UpdateVisibility()
{
// We're not actually using the operation for its dynamic mesh output... so we're only going to display the original mesh
UE::ToolTarget::SetSourceObjectVisible(Target, true);
Preview->SetVisibility(false);
}
void UExtractSplineTool::GenerateAsset()
{
using namespace ExtractSplineToolLocals;
FVector3d Origin, Normal;
GetCutPlane(Origin, Normal);
auto CreateSpline = [this, &Normal](AActor* HostActor, int Index, const TArray<TArray<FVector3d>>& PointLists, bool bIsClosed)
{
FTransform LocalToWorldNoTranslation = (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target);
LocalToWorldNoTranslation.SetTranslation(FVector::Zero());
FVector3d Center = FVector3d::Zero();
for (const FVector3d& Vertex : PointLists[Index])
{
Center += LocalToWorldNoTranslation.TransformVector(Vertex);
}
Center /= PointLists[Index].Num();
USplineComponent* OutputSpline = nullptr;
OutputSpline = CreateNewSplineInActor(GetToolManager(), HostActor, true, false);
OutputSpline->Modify();
OutputSpline->SetRelativeTransform(FTransform(Center), false, nullptr, ETeleportType::ResetPhysics);
OutputSpline->ClearSplinePoints();
OutputSpline->SetClosedLoop(bIsClosed);
OutputSpline->bSplineHasBeenEdited = true;
for (int SplineIndex = 0; SplineIndex < PointLists[Index].Num(); ++SplineIndex)
{
FVector3d Vertex = LocalToWorldNoTranslation.TransformVector(PointLists[Index][SplineIndex]);
Vertex -= Center;
OutputSpline->AddSplinePointAtIndex(Vertex, SplineIndex, ESplineCoordinateSpace::Local, false);
OutputSpline->SetUpVectorAtSplinePoint(SplineIndex, Normal, ESplineCoordinateSpace::Local, false);
OutputSpline->SetSplinePointType(SplineIndex, ESplinePointType::Linear, false);
}
OutputSpline->UpdateSpline();
};
auto CreateLoopSpline = [this, &CreateSpline](AActor* HostActor, int LoopIndex)
{
CreateSpline(HostActor, LoopIndex, CutLoops, true);
};
auto CreateSpanSpline = [this, &CreateSpline](AActor* HostActor, int SpanIndex)
{
CreateSpline(HostActor, SpanIndex, CutSpans, false);
};
GetToolManager()->BeginUndoTransaction(LOCTEXT("ExtractSplineTransactionName", "Extract Spline"));
FCreateActorParams CreateActorParams;
CreateActorParams.TargetWorld = TargetWorld.Get();
CreateActorParams.BaseName = "SplineActor";
CreateActorParams.Transform = FTransform(UE::ToolTarget::GetLocalToWorldTransform(Target).GetTranslation());
CreateActorParams.TemplateAsset = nullptr; // With no template, we create a basic AActor instance
FCreateActorResult Result = UE::Modeling::CreateNewActor(GetToolManager(), MoveTemp(CreateActorParams));
AActor* NewActor = Result.NewActor;
if (NewActor)
{
for (int LoopIndex = 0; LoopIndex < CutLoops.Num(); ++LoopIndex)
{
CreateLoopSpline(NewActor, LoopIndex);
}
for (int SpanIndex = 0; SpanIndex < CutSpans.Num(); ++SpanIndex)
{
CreateSpanSpline(NewActor, SpanIndex);
}
}
GetToolManager()->EndUndoTransaction();
}
void UExtractSplineTool::RegeneratePreviewSplines()
{
UPointSetComponent* PointSet = CutlineGeometry->FindPointSet(ExtractSplineToolLocals::CutlinePointSetID);
ULineSetComponent* LineSet = CutlineGeometry->FindLineSet(ExtractSplineToolLocals::CutlineLineSetID);
PointSet->Clear();
LineSet->Clear();
for (int LoopIndex = 0; LoopIndex < CutLoops.Num(); ++LoopIndex)
{
for (int SplineIndex = 0; SplineIndex < CutLoops[LoopIndex].Num(); ++SplineIndex)
{
PointSet->AddPoint(CutLoops[LoopIndex][SplineIndex], FColor::Green, 5.0f, 0.1f);
LineSet->AddLine(CutLoops[LoopIndex][SplineIndex], CutLoops[LoopIndex][(SplineIndex + 1) % CutLoops[LoopIndex].Num()], FColor::Green, 2.0f, 1.0f);
}
}
for (int SpanIndex = 0; SpanIndex < CutSpans.Num(); ++SpanIndex)
{
for (int SplineIndex = 0; SplineIndex < CutSpans[SpanIndex].Num()-1; ++SplineIndex)
{
PointSet->AddPoint(CutSpans[SpanIndex][SplineIndex], FColor::Blue, 5.0f, 0.1f);
LineSet->AddLine(CutSpans[SpanIndex][SplineIndex], CutSpans[SpanIndex][(SplineIndex + 1)], FColor::Blue, 2.0f, 1.0f);
}
}
}
void UExtractSplineTool::GatherSplineDataFromMeshBoundaries()
{
CutLoops.Empty();
CutSpans.Empty();
UE::Geometry::FMeshBoundaryLoops BoundaryLoops(OriginalMesh.Get());
BoundaryLoops.SpanBehavior = UE::Geometry::FMeshBoundaryLoops::ESpanBehaviors::Compute;
BoundaryLoops.FailureBehavior = UE::Geometry::FMeshBoundaryLoops::EFailureBehaviors::ConvertToOpenSpan;
BoundaryLoops.Compute();
for (UE::Geometry::FEdgeLoop EdgeLoop: BoundaryLoops.Loops)
{
TArray<FVector3d> LoopVertices;
EdgeLoop.GetVertices(LoopVertices);
CutLoops.Add(LoopVertices);
}
for (UE::Geometry::FEdgeSpan EdgeSpan : BoundaryLoops.Spans)
{
UE::Geometry::FPolyline3d Polyline;
EdgeSpan.GetPolyline(Polyline);
CutSpans.Add(Polyline.GetVertices());
}
}
UE::Geometry::FDynamicMeshAABBTree3& UExtractSplineTool::GetSpatial()
{
if (!MeshSpatial)
{
MeshSpatial = MakeUnique<UE::Geometry::FDynamicMeshAABBTree3>(OriginalMesh.Get());
}
return *MeshSpatial;
}
void UExtractSplineTool::GatherSplineDataFromPolygroupSelection()
{
// Basic algorithim:
// Starting with an edge from the set, sequentially pull edges out of the set which connect to the last selected edge.
// Once an ordered list of boundary edges is generated, generate a list of spline points from these polygroup edges which connect into a continous polyline.
auto ProcessSelectedEdges = [this](TSet<int32>& ToProcessEdges)
{
if (ToProcessEdges.IsEmpty())
{
return;
}
// It's possible a set of edges here will produce more than one spline. This occurs when edges form disjoint connectivity sets within the set.
// We handle that by assuming each disjoint set will be a loop and once a loop is closed, moving onto a new starting edge until our processing set is finally empty.
while (ToProcessEdges.Num() > 0)
{
TArray<int32> OrderedLoopEdges;
int32 StartingEdge;
ToProcessEdges.CompactStable();
StartingEdge = ToProcessEdges[FSetElementId::FromInteger(0)];
ensure(ToProcessEdges.Contains(StartingEdge));
ToProcessEdges.Remove(StartingEdge);
int32 NextEdge, PrevEdge = -1;
PrevEdge = StartingEdge;
do
{
OrderedLoopEdges.Add(PrevEdge);
TArray<int32> NextEdgeCandidates;
Topology->FindEdgeNbrEdges(PrevEdge, NextEdgeCandidates);
NextEdgeCandidates.Remove(PrevEdge);
NextEdge = -1;
for (int32 Edge : NextEdgeCandidates)
{
if (ToProcessEdges.Contains(Edge))
{
NextEdge = Edge;
ToProcessEdges.Remove(Edge);
break;
}
}
if (NextEdge == FDynamicMesh3::InvalidID)
{
break;
}
if (ToProcessEdges.Num() == 0)
{
OrderedLoopEdges.Add(NextEdge);
break;
}
PrevEdge = NextEdge;
} while (ToProcessEdges.Num() > 0);
// Once we're done building an ordered list of Polygroup boundaries, next we need to convert them into spline points.
FPolygonVertices SplineVertices;
int TrailingMeshEdge = -1;
int32 LastVertex = -1;
for (int EdgeIndex = 0; EdgeIndex < OrderedLoopEdges.Num(); ++EdgeIndex)
{
TArray<int32> MeshVertices;
MeshVertices = Topology->GetGroupEdgeVertices(OrderedLoopEdges[EdgeIndex]);
if (EdgeIndex == 0)
{
LastVertex = MeshVertices.Last();
}
if (EdgeIndex > 0)
{
if (MeshVertices[0] == TrailingMeshEdge && MeshVertices.Last() != TrailingMeshEdge)
{
Algo::Reverse(MeshVertices);
}
ensure(MeshVertices.Last() == TrailingMeshEdge);
}
TrailingMeshEdge = MeshVertices[0];
if (OrderedLoopEdges.Num() > 1)
{
MeshVertices.RemoveAt(0);
}
TArray<FVector3d> MeshPositions;
for (int MeshVertex : MeshVertices)
{
MeshPositions.Add(OriginalMesh->GetVertexRef(MeshVertex));
}
MeshPositions.Append(SplineVertices);
SplineVertices = MeshPositions;
}
if (OrderedLoopEdges.Num() == 1 || TrailingMeshEdge != LastVertex)
{
CutSpans.Add(SplineVertices);
}
else
{
CutLoops.Add(SplineVertices);
}
}
};
CutLoops.Empty();
CutSpans.Empty();
FGroupTopologySelection Selection = SelectionMechanic->GetActiveSelection();
if (Selection.IsEmpty())
{
return;
}
TSet<int32> MultipleCountedEdges;
TSet<int32> ToProcessEdges;
if (!Selection.SelectedGroupIDs.IsEmpty())
{
// Use this to develop equivalence relations between groups.
UE::Geometry::FSizedDisjointSet DisjointSet;
int32* MaxElement = Algo::MinElement(Selection.SelectedGroupIDs, TGreater<>());
// Make sure we add 1 here, or the indices will be wrong later. The selection seems to be one-indexed for some reason?
DisjointSet.Init((*MaxElement)+1, [&Selection](int32 ElementToTest) {
return Selection.SelectedGroupIDs.Contains(ElementToTest);
});
// First We need to figure out which groups are connected to each other via shared edges. We'll use a FSizeDisjointSet to handle the grouping logic.
for (int32 Group : Selection.SelectedGroupIDs)
{
TArray<int32> NbrGroups = Topology->GetGroupNbrGroups(Group);
for (int32 NbrGroup : NbrGroups)
{
if (Selection.SelectedGroupIDs.Contains(NbrGroup))
{
DisjointSet.Union(Group, NbrGroup);
}
}
}
TArray<int32> CompactIdxToGroupID;
DisjointSet.CompactedGroupIndexToGroupID(&CompactIdxToGroupID, nullptr);
// Now we can, per connected component, add all group edges together into processing set.
for (int32 ConnectedGroupID : CompactIdxToGroupID)
{
for (int32 Group : Selection.SelectedGroupIDs)
{
if (DisjointSet.Find(Group) != ConnectedGroupID)
{
continue;
}
Topology->ForGroupEdges(Group, [&ToProcessEdges, &MultipleCountedEdges](const UE::Geometry::FGroupTopology::FGroupEdge& GroupEdge, int GroupEdgeID)
{
if (ToProcessEdges.Contains(GroupEdgeID))
{
// We'll keep track of any multi-counted edges here, in case of weird non-manifold topology cases where an edge might have more than two neighbors.
// TODO: Check to see what happens if selected groups don't have "simple" boundary loops of edges.
MultipleCountedEdges.Add(GroupEdgeID);
}
ToProcessEdges.Add(GroupEdgeID);
});
}
ToProcessEdges = ToProcessEdges.Difference(MultipleCountedEdges);
// Convert all selected edges into spline ready loops, per the function defined above.
ProcessSelectedEdges(ToProcessEdges);
}
}
else
{
ToProcessEdges = Selection.SelectedEdgeIDs;
ProcessSelectedEdges(ToProcessEdges);
}
}
#undef LOCTEXT_NAMESPACE