You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
- Factor out "extra corner" finding code from PolyEd and add support for it in subdivide tool. - Add support for rounded corners for mesh boundaries. - Override subdivision scheme during tool usage rather than just at tool start, to allow extra corners to fix some issues. - Add some clarifying comments about schemes. #rb Jimmy.Andrews #jira UE-185416 [CL 26036654 by semion piskarev in 5.3 branch]
2738 lines
90 KiB
C++
2738 lines
90 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "EditMeshPolygonsTool.h"
|
|
|
|
#include "Algo/ForEach.h"
|
|
#include "BaseBehaviors/SingleClickBehavior.h"
|
|
#include "BaseGizmos/CombinedTransformGizmo.h"
|
|
#include "BaseGizmos/TransformGizmoUtil.h"
|
|
#include "BaseGizmos/TransformProxy.h"
|
|
#include "CompGeom/PolygonTriangulation.h"
|
|
#include "ConstrainedDelaunay2.h"
|
|
#include "Components/BrushComponent.h"
|
|
#include "ContextObjectStore.h"
|
|
#include "DynamicMesh/DynamicMeshAttributeSet.h"
|
|
#include "DynamicMesh/MeshIndexUtil.h" // TriangleToVertexIDs
|
|
#include "DynamicMesh/MeshNormals.h"
|
|
#include "DynamicMesh/MeshTransforms.h"
|
|
#include "DynamicMeshEditor.h"
|
|
#include "FaceGroupUtil.h"
|
|
#include "GroupTopology.h"
|
|
#include "InteractiveGizmoManager.h"
|
|
#include "InteractiveToolManager.h"
|
|
#include "Mechanics/DragAlignmentMechanic.h"
|
|
#include "MeshBoundaryLoops.h"
|
|
#include "MeshOpPreviewHelpers.h" // UMeshOpPreviewWithBackgroundCompute
|
|
#include "MeshRegionBoundaryLoops.h"
|
|
#include "ModelingToolTargetUtil.h" // UE::ToolTarget:: functions
|
|
#include "Operations/SimpleHoleFiller.h"
|
|
#include "Operations/MinimalHoleFiller.h"
|
|
#include "Operations/PolygroupRemesh.h"
|
|
#include "Operations/WeldEdgeSequence.h"
|
|
#include "Operations/LocalPlanarSimplify.h"
|
|
#include "Selection/StoredMeshSelectionUtil.h"
|
|
#include "Selection/PolygonSelectionMechanic.h"
|
|
#include "Selections/MeshConnectedComponents.h"
|
|
#include "TargetInterfaces/MaterialProvider.h"
|
|
#include "TargetInterfaces/MeshDescriptionCommitter.h"
|
|
#include "TargetInterfaces/MeshDescriptionProvider.h"
|
|
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
|
|
#include "ToolActivities/PolyEditActivityContext.h"
|
|
#include "ToolActivities/PolyEditExtrudeActivity.h"
|
|
#include "ToolActivities/PolyEditInsertEdgeActivity.h"
|
|
#include "ToolActivities/PolyEditInsertEdgeLoopActivity.h"
|
|
#include "ToolActivities/PolyEditInsetOutsetActivity.h"
|
|
#include "ToolActivities/PolyEditCutFacesActivity.h"
|
|
#include "ToolActivities/PolyEditPlanarProjectionUVActivity.h"
|
|
#include "ToolActivities/PolyEditBevelEdgeActivity.h"
|
|
#include "ToolContextInterfaces.h" // FToolBuilderState
|
|
#include "ToolSetupUtil.h"
|
|
#include "ToolTargetManager.h"
|
|
#include "TransformTypes.h"
|
|
#include "Util/CompactMaps.h"
|
|
#include "Selections/GeometrySelection.h"
|
|
#include "Selection/GeometrySelectionManager.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(EditMeshPolygonsTool)
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
#define LOCTEXT_NAMESPACE "UEditMeshPolygonsTool"
|
|
|
|
namespace EditMeshPolygonsToolLocals
|
|
{
|
|
FText PolyEditDefaultMessage = LOCTEXT("OnStartEditMeshPolygonsTool_TriangleMode", "Select triangles to edit mesh. Use middle mouse on gizmo to "
|
|
"reposition it. Hold Ctrl while translating or (in local mode) rotating to align to scene. Shift and Ctrl "
|
|
"change marquee select behavior. Q toggles Gizmo Orientation Lock.");
|
|
|
|
FText TriEditDefaultMessage = LOCTEXT("OnStartEditMeshPolygonsTool", "Select PolyGroups to edit mesh. Use middle mouse on gizmo to reposition it. "
|
|
"Hold Ctrl while translating or (in local mode) rotating to align to scene. Shift and Ctrl change marquee select "
|
|
"behavior. Q toggles Gizmo Orientation Lock.");
|
|
|
|
FString GetPropertyCacheIdentifier(bool bTriangleMode)
|
|
{
|
|
return bTriangleMode ? TEXT("TriEditTool") : TEXT("PolyEditTool");
|
|
}
|
|
|
|
TAutoConsoleVariable<int32> CVarEdgeLimit(
|
|
TEXT("modeling.PolyEdit.EdgeLimit"),
|
|
60000,
|
|
TEXT("Maximal number of edges that PolyEd and TriEd support. Meshes that would require "
|
|
"more than this number of edges to be rendered in PolyEd or TriEd force the tools to "
|
|
"be disabled to avoid hanging the editor."));
|
|
|
|
// Allows undo/redo of addition of extra corners in the group topology based on user angle thresholds.
|
|
// Used after user-triggered topology corner changes where the mesh was not actually edited.
|
|
class FExtraCornerChange : public FToolCommandChange
|
|
{
|
|
public:
|
|
FExtraCornerChange(const TSet<int32>& BeforeIn, const TSet<int32>& AfterIn)
|
|
: Before(BeforeIn)
|
|
, After(AfterIn)
|
|
{
|
|
}
|
|
virtual void Apply(UObject* Object) override
|
|
{
|
|
Cast<UEditMeshPolygonsTool>(Object)->RebuildTopologyWithGivenExtraCorners(After);
|
|
}
|
|
virtual void Revert(UObject* Object) override
|
|
{
|
|
Cast<UEditMeshPolygonsTool>(Object)->RebuildTopologyWithGivenExtraCorners(Before);
|
|
}
|
|
virtual bool HasExpired(UObject* Object) const override
|
|
{
|
|
return false;
|
|
}
|
|
virtual FString ToString() const override
|
|
{
|
|
return TEXT("FExtraCornerChange");
|
|
}
|
|
|
|
protected:
|
|
TSet<int32> Before;
|
|
TSet<int32> After;
|
|
};
|
|
}
|
|
|
|
/*
|
|
* ToolBuilder
|
|
*/
|
|
|
|
|
|
USingleTargetWithSelectionTool* UEditMeshPolygonsToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
return NewObject<UEditMeshPolygonsTool>(SceneState.ToolManager);
|
|
}
|
|
|
|
void UEditMeshPolygonsToolBuilder::InitializeNewTool(USingleTargetWithSelectionTool* Tool, const FToolBuilderState& SceneState) const
|
|
{
|
|
USingleTargetWithSelectionToolBuilder::InitializeNewTool(Tool, SceneState);
|
|
UEditMeshPolygonsTool* EditPolygonsTool = CastChecked<UEditMeshPolygonsTool>(Tool);
|
|
if (bTriangleMode)
|
|
{
|
|
EditPolygonsTool->EnableTriangleMode();
|
|
}
|
|
}
|
|
|
|
|
|
bool UEditMeshPolygonsActionModeToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
if (UEditMeshPolygonsToolBuilder::CanBuildTool(SceneState))
|
|
{
|
|
if ( StartupAction == EEditMeshPolygonsToolActions::SimplifyByGroups
|
|
|| StartupAction == EEditMeshPolygonsToolActions::InsertEdge
|
|
|| StartupAction == EEditMeshPolygonsToolActions::InsertEdgeLoop )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( UGeometrySelectionManager* SelectionManager = SceneState.ToolManager->GetContextObjectStore()->FindContext<UGeometrySelectionManager>() )
|
|
{
|
|
EGeometryTopologyType TopologyType = EGeometryTopologyType::Triangle;
|
|
EGeometryElementType ElementType = EGeometryElementType::Face;
|
|
int NumTargets;
|
|
bool bIsEmpty = false;
|
|
SelectionManager->GetActiveSelectionInfo(TopologyType, ElementType, NumTargets, bIsEmpty);
|
|
if (StartupAction == EEditMeshPolygonsToolActions::Extrude
|
|
|| StartupAction == EEditMeshPolygonsToolActions::PushPull
|
|
|| StartupAction == EEditMeshPolygonsToolActions::Offset
|
|
|| StartupAction == EEditMeshPolygonsToolActions::Inset
|
|
|| StartupAction == EEditMeshPolygonsToolActions::Outset
|
|
|| StartupAction == EEditMeshPolygonsToolActions::CutFaces)
|
|
{
|
|
return (TopologyType == EGeometryTopologyType::Polygroup && ElementType == EGeometryElementType::Face && bIsEmpty == false);
|
|
}
|
|
else if (StartupAction == EEditMeshPolygonsToolActions::BevelAuto)
|
|
{
|
|
return (TopologyType == EGeometryTopologyType::Polygroup && ElementType != EGeometryElementType::Vertex && bIsEmpty == false);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsActionModeToolBuilder::InitializeNewTool(USingleTargetWithSelectionTool* Tool, const FToolBuilderState& SceneState) const
|
|
{
|
|
UEditMeshPolygonsToolBuilder::InitializeNewTool(Tool, SceneState);
|
|
UEditMeshPolygonsTool* EditPolygonsTool = CastChecked<UEditMeshPolygonsTool>(Tool);
|
|
|
|
EEditMeshPolygonsToolActions UseAction = StartupAction;
|
|
EditPolygonsTool->PostSetupFunction = [UseAction](UEditMeshPolygonsTool* PolyTool)
|
|
{
|
|
PolyTool->SetToSelectionModeInterface();
|
|
PolyTool->RequestSingleShotAction(UseAction);
|
|
};
|
|
}
|
|
|
|
|
|
|
|
bool UEditMeshPolygonsSelectionModeToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
if (UEditMeshPolygonsToolBuilder::CanBuildTool(SceneState))
|
|
{
|
|
if ( UGeometrySelectionManager* SelectionManager = SceneState.ToolManager->GetContextObjectStore()->FindContext<UGeometrySelectionManager>() )
|
|
{
|
|
// if not actively selecting mesh components, tool can be started in 'standard' full-PolyEd mode
|
|
if (SelectionManager->GetMeshTopologyMode() == UGeometrySelectionManager::EMeshTopologyMode::None)
|
|
{
|
|
return true;
|
|
}
|
|
// otherwise can only start tool in sub-modes
|
|
EGeometryTopologyType TopologyType = EGeometryTopologyType::Triangle;
|
|
EGeometryElementType ElementType = EGeometryElementType::Face;
|
|
int NumTargets;
|
|
bool bIsEmpty = false;
|
|
SelectionManager->GetActiveSelectionInfo(TopologyType, ElementType, NumTargets, bIsEmpty);
|
|
if (TopologyType == EGeometryTopologyType::Polygroup)
|
|
{
|
|
return ElementType != EGeometryElementType::Vertex;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void UEditMeshPolygonsSelectionModeToolBuilder::InitializeNewTool(USingleTargetWithSelectionTool* Tool, const FToolBuilderState& SceneState) const
|
|
{
|
|
UEditMeshPolygonsToolBuilder::InitializeNewTool(Tool, SceneState);
|
|
UEditMeshPolygonsTool* EditPolygonsTool = CastChecked<UEditMeshPolygonsTool>(Tool);
|
|
|
|
// if not actively selecting mesh components, start in full-PolyEd mode
|
|
if (UGeometrySelectionManager* SelectionManager = SceneState.ToolManager->GetContextObjectStore()->FindContext<UGeometrySelectionManager>())
|
|
{
|
|
if (SelectionManager->GetMeshTopologyMode() == UGeometrySelectionManager::EMeshTopologyMode::None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// otherwise can only start tool in sub-modes
|
|
EGeometryTopologyType TopologyType = EGeometryTopologyType::Triangle;
|
|
EGeometryElementType ElementType = EGeometryElementType::Face;
|
|
int NumTargets;
|
|
bool bIsEmpty = false;
|
|
SelectionManager->GetActiveSelectionInfo(TopologyType, ElementType, NumTargets, bIsEmpty);
|
|
if (TopologyType != EGeometryTopologyType::Polygroup)
|
|
{
|
|
return; // should not happen...
|
|
}
|
|
|
|
EEditMeshPolygonsToolSelectionMode UseMode = EEditMeshPolygonsToolSelectionMode::Faces;
|
|
bool bIsEdgeSelection = false;
|
|
if (ElementType == EGeometryElementType::Edge)
|
|
{
|
|
bIsEdgeSelection = true;
|
|
UseMode = EEditMeshPolygonsToolSelectionMode::Edges;
|
|
}
|
|
else if (ElementType == EGeometryElementType::Vertex)
|
|
{
|
|
UseMode = EEditMeshPolygonsToolSelectionMode::Vertices;
|
|
}
|
|
|
|
EditPolygonsTool->PostSetupFunction = [UseMode, bIsEdgeSelection](UEditMeshPolygonsTool* PolyTool)
|
|
{
|
|
PolyTool->SetToolPropertySourceEnabled(PolyTool->EditActions, !bIsEdgeSelection);
|
|
PolyTool->SetToolPropertySourceEnabled(PolyTool->EditEdgeActions, bIsEdgeSelection);
|
|
PolyTool->SetToolPropertySourceEnabled(PolyTool->EditUVActions, !bIsEdgeSelection);
|
|
|
|
PolyTool->SetToolPropertySourceEnabled(PolyTool->TopologyProperties, false);
|
|
|
|
UPolygonSelectionMechanic* SelectionMechanic = PolyTool->SelectionMechanic;
|
|
UMeshTopologySelectionMechanicProperties* SelectionProps = SelectionMechanic->Properties;
|
|
SelectionProps->bSelectFaces = SelectionProps->bSelectEdges = SelectionProps->bSelectVertices = false;
|
|
SelectionProps->bSelectEdgeLoops = SelectionProps->bSelectEdgeRings = false;
|
|
|
|
switch (UseMode)
|
|
{
|
|
default:
|
|
case EEditMeshPolygonsToolSelectionMode::Faces:
|
|
SelectionProps->bSelectFaces = true;
|
|
break;
|
|
case EEditMeshPolygonsToolSelectionMode::Edges:
|
|
SelectionProps->bSelectEdges = true;
|
|
break;
|
|
case EEditMeshPolygonsToolSelectionMode::Vertices:
|
|
SelectionProps->bSelectVertices = true;
|
|
break;
|
|
}
|
|
|
|
PolyTool->SetToolPropertySourceEnabled(SelectionProps, false);
|
|
};
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::SetToSelectionModeInterface()
|
|
{
|
|
if (EditActions) SetToolPropertySourceEnabled(EditActions, false);
|
|
if (EditEdgeActions) SetToolPropertySourceEnabled(EditEdgeActions, false);
|
|
if (EditUVActions) SetToolPropertySourceEnabled(EditUVActions, false);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsToolActionPropertySet::PostAction(EEditMeshPolygonsToolActions Action)
|
|
{
|
|
if (ParentTool.IsValid())
|
|
{
|
|
ParentTool->RequestAction(Action);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Tool methods
|
|
*/
|
|
|
|
UEditMeshPolygonsTool::UEditMeshPolygonsTool()
|
|
{
|
|
SetToolDisplayName(LOCTEXT("EditMeshPolygonsToolName", "PolyGroup Edit"));
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::EnableTriangleMode()
|
|
{
|
|
check(Preview == nullptr); // must not have been initialized!
|
|
bTriangleMode = true;
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::Setup()
|
|
{
|
|
using namespace EditMeshPolygonsToolLocals;
|
|
|
|
// TODO: Currently we draw all the edges in the tool with PDI and can lock up the editor on high-res meshes.
|
|
// As a hack, disable everything if the number of edges is too high, so that user doesn't lose work accidentally
|
|
// if they start the tool on the wrong thing.
|
|
int32 MaxEdges = CVarEdgeLimit.GetValueOnGameThread();
|
|
|
|
CurrentMesh = MakeShared<FDynamicMesh3>(UE::ToolTarget::GetDynamicMeshCopy(Target));
|
|
WorldTransform = UE::ToolTarget::GetLocalToWorldTransform(Target);
|
|
FVector ScaleToBake = WorldTransform.GetScale();
|
|
BakedTransform = FTransformSRT3d(FQuaterniond::Identity(), FVector::Zero(), ScaleToBake);
|
|
WorldTransform.SetScale(FVector::One());
|
|
MeshTransforms::ApplyTransform(*CurrentMesh, BakedTransform, true);
|
|
|
|
if (bTriangleMode)
|
|
{
|
|
bToolDisabled = CurrentMesh->EdgeCount() > MaxEdges;
|
|
if (bToolDisabled)
|
|
{
|
|
GetToolManager()->DisplayMessage(FText::Format(
|
|
LOCTEXT("TriEditTooManyEdges",
|
|
"This tool is currently disallowed from operating on a mesh of this resolution. "
|
|
"Current limit set by \"modeling.PolyEdit.EdgeLimit\" is {0} edges, and mesh has "
|
|
"{1}. Limit can be changed but exists to avoid hanging the editor when trying to "
|
|
"render too many edges using the current system, so make sure to save your work "
|
|
"if you change the upper limit and try to edit a very dense mesh."),
|
|
MaxEdges, CurrentMesh->EdgeCount()), EToolMessageLevel::UserError);
|
|
return;
|
|
}
|
|
|
|
Topology = MakeShared<FTriangleGroupTopology, ESPMode::ThreadSafe>(CurrentMesh.Get(), false);
|
|
}
|
|
else
|
|
{
|
|
Topology = MakeShared<FGroupTopology, ESPMode::ThreadSafe>(CurrentMesh.Get(), false);
|
|
|
|
Topology->ShouldAddExtraCornerAtVert =
|
|
[this](const FGroupTopology& GroupTopology, int32 Vid, const FIndex2i& AttachedGroupEdgeEids)
|
|
{
|
|
// Note: it's important that we don't use CurrentMesh here. It's possible that an activity might create a copy of
|
|
// the topology that uses the same corner forcing function but points to a different mesh, so we want to use
|
|
// whatever mesh the passed-in topology uses.
|
|
return TopologyProperties->bAddExtraCorners
|
|
&& FGroupTopology::IsEdgeAngleSharp(GroupTopology.GetMesh(), Vid, AttachedGroupEdgeEids, ExtraCornerDotProductThreshold);
|
|
};
|
|
}
|
|
|
|
TopologyProperties = NewObject<UPolyEditTopologyProperties>(this);
|
|
TopologyProperties->Initialize(this);
|
|
TopologyProperties->RestoreProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
|
|
auto UpdateExtraCornerThreshold = [this]() { ExtraCornerDotProductThreshold = FMathd::Cos(TopologyProperties->ExtraCornerAngleThresholdDegrees * FMathd::DegToRad); };
|
|
UpdateExtraCornerThreshold();
|
|
TopologyProperties->WatchProperty(TopologyProperties->ExtraCornerAngleThresholdDegrees,
|
|
// Note: it may seem tempting to auto-rebuild the topology as the user drags the threshold slider (rather than waiting for
|
|
// the button click or next topology rebuild), but we have to transact corner additions/removals so that selection events in the
|
|
// undo stack are able to refer to the same edges/corners (this is also why we store the current extra corners in FEditMeshPolygonsToolMeshChange).
|
|
// In an ideal world, we would know the end of a slider drag and only transact at that point, but we don't have that.
|
|
[this, UpdateExtraCornerThreshold](double) { UpdateExtraCornerThreshold(); });
|
|
|
|
Topology->RebuildTopology();
|
|
|
|
if (!bTriangleMode)
|
|
{
|
|
int32 NumEdgesToRender = 0;
|
|
for (const FGroupTopology::FGroupEdge& Edge : Topology->Edges)
|
|
{
|
|
NumEdgesToRender += Edge.Span.Edges.Num();
|
|
}
|
|
|
|
bToolDisabled = NumEdgesToRender > MaxEdges;
|
|
if (bToolDisabled)
|
|
{
|
|
GetToolManager()->DisplayMessage(FText::Format(
|
|
LOCTEXT("PolyEditTooManyEdges",
|
|
"This tool is currently disallowed from operating on a group topology of this resolution. "
|
|
"Current limit set by \"modeling.PolyEdit.EdgeLimit\" is {0} displayed edges, and topology has "
|
|
"{1} edge segments to display. Limit can be changed, but it exists to avoid hanging the editor "
|
|
"when trying to render too many edges using the current system, so make sure to save your work "
|
|
"if you change the upper limit and try to edit a very complicated topology."),
|
|
MaxEdges, NumEdgesToRender), EToolMessageLevel::UserError);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Start by adding the actions, because we want them at the top.
|
|
if (bTriangleMode)
|
|
{
|
|
EditActions_Triangles = NewObject<UEditMeshPolygonsToolActions_Triangles>();
|
|
EditActions_Triangles->Initialize(this);
|
|
AddToolPropertySource(EditActions_Triangles);
|
|
|
|
EditEdgeActions_Triangles = NewObject<UEditMeshPolygonsToolEdgeActions_Triangles>();
|
|
EditEdgeActions_Triangles->Initialize(this);
|
|
AddToolPropertySource(EditEdgeActions_Triangles);
|
|
|
|
SetToolDisplayName(LOCTEXT("EditMeshTrianglesToolName", "Triangle Edit"));
|
|
DefaultMessage = PolyEditDefaultMessage;
|
|
}
|
|
else
|
|
{
|
|
EditActions = NewObject<UEditMeshPolygonsToolActions>();
|
|
EditActions->Initialize(this);
|
|
AddToolPropertySource(EditActions);
|
|
|
|
EditEdgeActions = NewObject<UEditMeshPolygonsToolEdgeActions>();
|
|
EditEdgeActions->Initialize(this);
|
|
AddToolPropertySource(EditEdgeActions);
|
|
|
|
EditUVActions = NewObject<UEditMeshPolygonsToolUVActions>();
|
|
EditUVActions->Initialize(this);
|
|
AddToolPropertySource(EditUVActions);
|
|
|
|
DefaultMessage = TriEditDefaultMessage;
|
|
}
|
|
|
|
GetToolManager()->DisplayMessage(DefaultMessage,
|
|
EToolMessageLevel::UserNotification);
|
|
|
|
// We add an empty line for the error message so that things don't jump when we use it.
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
|
|
|
|
CancelAction = NewObject<UEditMeshPolygonsToolCancelAction>();
|
|
CancelAction->Initialize(this);
|
|
AddToolPropertySource(CancelAction);
|
|
SetToolPropertySourceEnabled(CancelAction, false);
|
|
|
|
AcceptCancelAction = NewObject<UEditMeshPolygonsToolAcceptCancelAction>();
|
|
AcceptCancelAction->Initialize(this);
|
|
AddToolPropertySource(AcceptCancelAction);
|
|
SetToolPropertySourceEnabled(AcceptCancelAction, false);
|
|
|
|
|
|
// Initialize the common properties but don't add them yet, because we want them to be under the activity-specific ones.
|
|
CommonProps = NewObject<UPolyEditCommonProperties>(this);
|
|
CommonProps->RestoreProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
|
|
CommonProps->WatchProperty(CommonProps->LocalFrameMode,
|
|
[this](ELocalFrameMode) { UpdateGizmoFrame(); });
|
|
CommonProps->WatchProperty(CommonProps->bLockRotation,
|
|
[this](bool) { LockedTransfomerFrame = LastTransformerFrame; });
|
|
CommonProps->WatchProperty(CommonProps->bGizmoVisible,
|
|
[this](bool)
|
|
{
|
|
if (!CurrentActivity)
|
|
{
|
|
UpdateGizmoVisibility();
|
|
ResetUserMessage();
|
|
}
|
|
});
|
|
|
|
// We are going to SilentUpdate here because otherwise the Watches above will immediately fire
|
|
// and cause UpdateGizmoFrame() to be called emitting a spurious Transform change.
|
|
CommonProps->SilentUpdateWatched();
|
|
|
|
// TODO: Do we need this?
|
|
FMeshNormals::QuickComputeVertexNormals(*CurrentMesh);
|
|
|
|
// Create the preview object
|
|
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>();
|
|
Preview->Setup(GetTargetWorld());
|
|
ToolSetupUtil::ApplyRenderingConfigurationToPreview(Preview->PreviewMesh, Target);
|
|
Preview->PreviewMesh->SetTransform((FTransform)WorldTransform);
|
|
|
|
// We'll use the spatial inside preview mesh mainly for the convenience of having it update automatically.
|
|
Preview->PreviewMesh->bBuildSpatialDataStructure = true;
|
|
|
|
// set materials
|
|
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target);
|
|
Preview->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
|
|
|
|
// configure secondary render material
|
|
UMaterialInterface* SelectionMaterial = ToolSetupUtil::GetSelectionMaterial(FLinearColor::Yellow, GetToolManager());
|
|
if (SelectionMaterial != nullptr)
|
|
{
|
|
// Note that you have to do it this way rather than reaching into the PreviewMesh because the background compute
|
|
// mesh has to be able to swap in/out a working material and restore the primary/secondary ones.
|
|
Preview->SecondaryMaterial = SelectionMaterial;
|
|
}
|
|
|
|
Preview->PreviewMesh->EnableSecondaryTriangleBuffers(
|
|
[this](const FDynamicMesh3* Mesh, int32 TriangleID)
|
|
{
|
|
return SelectionMechanic->GetActiveSelection().IsSelectedTriangle(Mesh, Topology.Get(), TriangleID);
|
|
});
|
|
|
|
Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated);
|
|
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get());
|
|
Preview->PreviewMesh->EnableWireframe(CommonProps->bShowWireframe);
|
|
Preview->SetVisibility(true);
|
|
|
|
// initialize AABBTree
|
|
MeshSpatial = MakeShared<FDynamicMeshAABBTree3>();
|
|
MeshSpatial->SetMesh(CurrentMesh.Get());
|
|
|
|
// set up SelectionMechanic
|
|
SelectionMechanic = NewObject<UPolygonSelectionMechanic>(this);
|
|
SelectionMechanic->bAddSelectionFilterPropertiesToParentTool = false; // We'll do this ourselves later
|
|
SelectionMechanic->Setup(this);
|
|
SelectionMechanic->SetShowSelectableCorners(CommonProps->bShowSelectableCorners);
|
|
SelectionMechanic->Properties->RestoreProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
SelectionMechanic->Properties->bDisplayPolygroupReliantControls = !bTriangleMode;
|
|
SelectionMechanic->OnSelectionChanged.AddUObject(this, &UEditMeshPolygonsTool::OnSelectionModifiedEvent);
|
|
SelectionMechanic->OnFaceSelectionPreviewChanged.AddWeakLambda(this, [this]() {
|
|
Preview->PreviewMesh->FastNotifySecondaryTrianglesChanged();
|
|
});
|
|
if (bTriangleMode)
|
|
{
|
|
SelectionMechanic->PolyEdgesRenderer.LineThickness = 1.0;
|
|
}
|
|
SelectionMechanic->Initialize(CurrentMesh.Get(),
|
|
(FTransform3d)Preview->PreviewMesh->GetTransform(),
|
|
GetTargetWorld(),
|
|
Topology.Get(),
|
|
[this]() { return &GetSpatial(); }
|
|
);
|
|
|
|
LinearDeformer.Initialize(CurrentMesh.Get(), Topology.Get());
|
|
|
|
// initialize our selection from input selection, if available
|
|
if (HasGeometrySelection())
|
|
{
|
|
const FGeometrySelection& CurSelection = GetGeometrySelection();
|
|
if (CurSelection.TopologyType == EGeometryTopologyType::Triangle && bTriangleMode)
|
|
{
|
|
SelectionMechanic->SetSelection_AsTriangleTopology(CurSelection);
|
|
}
|
|
else if (CurSelection.TopologyType == EGeometryTopologyType::Polygroup && bTriangleMode == false)
|
|
{
|
|
SelectionMechanic->SetSelection_AsGroupTopology(CurSelection);
|
|
}
|
|
}
|
|
|
|
bSelectionStateDirty = SelectionMechanic->HasSelection();
|
|
|
|
// Set UV Scale factor based on initial mesh bounds
|
|
float BoundsMaxDim = CurrentMesh->GetBounds().MaxDim();
|
|
if (BoundsMaxDim > 0)
|
|
{
|
|
UVScaleFactor = 1.0 / BoundsMaxDim;
|
|
}
|
|
|
|
// Wrap the data structures into a context that we can give to the activities
|
|
ActivityContext = NewObject<UPolyEditActivityContext>();
|
|
ActivityContext->bTriangleMode = bTriangleMode;
|
|
ActivityContext->CommonProperties = CommonProps;
|
|
ActivityContext->CurrentMesh = CurrentMesh;
|
|
ActivityContext->Preview = Preview;
|
|
ActivityContext->CurrentTopology = Topology;
|
|
ActivityContext->MeshSpatial = MeshSpatial;
|
|
ActivityContext->SelectionMechanic = SelectionMechanic;
|
|
ActivityContext->EmitActivityStart = [this](const FText& TransactionLabel)
|
|
{
|
|
EmitActivityStart(TransactionLabel);
|
|
};
|
|
ActivityContext->EmitCurrentMeshChangeAndUpdate = [this](const FText& TransactionLabel,
|
|
TUniquePtr<UE::Geometry::FDynamicMeshChange> MeshChangeIn,
|
|
const UE::Geometry::FGroupTopologySelection& OutputSelection)
|
|
{
|
|
EmitCurrentMeshChangeAndUpdate(TransactionLabel, MoveTemp(MeshChangeIn), OutputSelection);
|
|
};
|
|
GetToolManager()->GetContextObjectStore()->RemoveContextObjectsOfType(UPolyEditActivityContext::StaticClass());
|
|
GetToolManager()->GetContextObjectStore()->AddContextObject(ActivityContext);
|
|
|
|
ExtrudeActivity = NewObject<UPolyEditExtrudeActivity>();
|
|
ExtrudeActivity->Setup(this);
|
|
|
|
InsetOutsetActivity = NewObject<UPolyEditInsetOutsetActivity>();
|
|
InsetOutsetActivity->Setup(this);
|
|
|
|
CutFacesActivity = NewObject<UPolyEditCutFacesActivity>();
|
|
CutFacesActivity->Setup(this);
|
|
|
|
PlanarProjectionUVActivity = NewObject<UPolyEditPlanarProjectionUVActivity>();
|
|
PlanarProjectionUVActivity->Setup(this);
|
|
|
|
InsertEdgeLoopActivity = NewObject<UPolyEditInsertEdgeLoopActivity>();
|
|
InsertEdgeLoopActivity->Setup(this);
|
|
|
|
InsertEdgeActivity = NewObject<UPolyEditInsertEdgeActivity>();
|
|
InsertEdgeActivity->Setup(this);
|
|
|
|
BevelEdgeActivity = NewObject<UPolyEditBevelEdgeActivity>();
|
|
BevelEdgeActivity->Setup(this);
|
|
|
|
// Now that we've initialized the activities, add in the selection settings and
|
|
// CommonProps so that they are at the bottom.
|
|
AddToolPropertySource(SelectionMechanic->Properties);
|
|
AddToolPropertySource(CommonProps);
|
|
if (!bTriangleMode)
|
|
{
|
|
AddToolPropertySource(TopologyProperties);
|
|
}
|
|
else
|
|
{
|
|
// Not actually necessary since we don't use the forcing function in triangle mode, but might as well turn it off here too.
|
|
TopologyProperties->bAddExtraCorners = false;
|
|
}
|
|
|
|
// hide input StaticMeshComponent
|
|
UE::ToolTarget::HideSourceObject(Target);
|
|
|
|
UInteractiveGizmoManager* GizmoManager = GetToolManager()->GetPairedGizmoManager();
|
|
|
|
TransformGizmo = UE::TransformGizmoUtil::CreateCustomRepositionableTransformGizmo(GizmoManager,
|
|
ETransformGizmoSubElements::FullTranslateRotateScale, this);
|
|
if (ensure(TransformGizmo)) // If we don't get a valid gizmo a lot of interactions won't work, but at least we won't crash
|
|
{
|
|
// Stop scaling at 0 rather than going negative
|
|
TransformGizmo->SetDisallowNegativeScaling(true);
|
|
// We allow non uniform scale even when the gizmo mode is set to "world" because we're not scaling components- we're
|
|
// moving vertices, so we don't care which axes we "scale" along.
|
|
TransformGizmo->SetIsNonUniformScaleAllowedFunction([]() {
|
|
return true;
|
|
});
|
|
|
|
// Hook up callbacks
|
|
TransformProxy = NewObject<UTransformProxy>(this);
|
|
TransformProxy->OnTransformChanged.AddUObject(this, &UEditMeshPolygonsTool::OnGizmoTransformChanged);
|
|
TransformProxy->OnBeginTransformEdit.AddUObject(this, &UEditMeshPolygonsTool::OnBeginGizmoTransform);
|
|
TransformProxy->OnEndTransformEdit.AddUObject(this, &UEditMeshPolygonsTool::OnEndGizmoTransform);
|
|
TransformProxy->OnEndPivotEdit.AddWeakLambda(this, [this](UTransformProxy* Proxy) {
|
|
LastTransformerFrame = FFrame3d(Proxy->GetTransform());
|
|
if (CommonProps->bLockRotation)
|
|
{
|
|
LockedTransfomerFrame = LastTransformerFrame;
|
|
}
|
|
});
|
|
TransformGizmo->SetActiveTarget(TransformProxy, GetToolManager());
|
|
TransformGizmo->SetVisibility(false);
|
|
}
|
|
|
|
DragAlignmentMechanic = NewObject<UDragAlignmentMechanic>(this);
|
|
DragAlignmentMechanic->Setup(this);
|
|
DragAlignmentMechanic->InitializeDeformedMeshRayCast(
|
|
[this]() { return &GetSpatial(); },
|
|
WorldTransform, &LinearDeformer); // Should happen after LinearDeformer is initialized
|
|
|
|
if (TransformGizmo)
|
|
{
|
|
DragAlignmentMechanic->AddToGizmo(TransformGizmo);
|
|
}
|
|
|
|
if (Topology->Groups.Num() < 2)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("NoGroupsWarning",
|
|
"This object has only a single Polygroup. Use the GrpGen, GrpPnt or TriSel (Create Polygroup) tools to modify PolyGroups."),
|
|
EToolMessageLevel::UserWarning);
|
|
}
|
|
|
|
if (PostSetupFunction)
|
|
{
|
|
PostSetupFunction(this);
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ResetUserMessage()
|
|
{
|
|
// When the gizmo is hidden, notify the user and tell them how to fix it.
|
|
if (!TransformGizmo->IsVisible())
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("ToggleTransformGizmoNotify", "Transform gizmo hidden, unhide by toggling \"Gizmo Visible\" (or using hotkey, if set)"),
|
|
EToolMessageLevel::UserNotification);
|
|
}
|
|
else
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
DefaultMessage,
|
|
EToolMessageLevel::UserNotification);
|
|
}
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::OnShutdown(EToolShutdownType ShutdownType)
|
|
{
|
|
using namespace EditMeshPolygonsToolLocals;
|
|
|
|
if (bToolDisabled)
|
|
{
|
|
CurrentMesh.Reset();
|
|
Topology.Reset();
|
|
return;
|
|
}
|
|
|
|
if (CurrentActivity)
|
|
{
|
|
CurrentActivity->End(ShutdownType);
|
|
CurrentActivity = nullptr;
|
|
}
|
|
CommonProps->SaveProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
SelectionMechanic->Properties->SaveProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
TopologyProperties->SaveProperties(this, GetPropertyCacheIdentifier(bTriangleMode));
|
|
|
|
GetToolManager()->GetContextObjectStore()->RemoveContextObjectsOfType(UPolyEditActivityContext::StaticClass());
|
|
ActivityContext = nullptr;
|
|
|
|
ExtrudeActivity->Shutdown(ShutdownType);
|
|
InsetOutsetActivity->Shutdown(ShutdownType);
|
|
CutFacesActivity->Shutdown(ShutdownType);
|
|
PlanarProjectionUVActivity->Shutdown(ShutdownType);
|
|
InsertEdgeActivity->Shutdown(ShutdownType);
|
|
InsertEdgeLoopActivity->Shutdown(ShutdownType);
|
|
BevelEdgeActivity->Shutdown(ShutdownType);
|
|
|
|
GetToolManager()->GetPairedGizmoManager()->DestroyAllGizmosByOwner(this);
|
|
|
|
DragAlignmentMechanic->Shutdown();
|
|
// We wait to shut down the selection mechanic in case we need to do work to store the selection.
|
|
|
|
if (Preview != nullptr)
|
|
{
|
|
UE::ToolTarget::ShowSourceObject(Target);
|
|
|
|
if (ShutdownType == EToolShutdownType::Accept)
|
|
{
|
|
FGeometrySelection OutputSelection;
|
|
EGeometryElementType CurElemType = EGeometryElementType::Face;
|
|
if (SelectionMechanic->GetActiveSelection().SelectedCornerIDs.Num() > 0)
|
|
{
|
|
CurElemType = EGeometryElementType::Vertex;
|
|
}
|
|
else if (SelectionMechanic->GetActiveSelection().SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
CurElemType = EGeometryElementType::Edge;
|
|
}
|
|
OutputSelection.InitializeTypes( CurElemType, (bTriangleMode) ? EGeometryTopologyType::Triangle : EGeometryTopologyType::Polygroup);
|
|
|
|
FCompactMaps CompactMaps;
|
|
//bool bIsBrushComponent = (Cast<UBrushComponent>(UE::ToolTarget::GetTargetComponent(Target)) == nullptr);
|
|
bool bIsBrushComponent = false; // can we allow this now?
|
|
bool bWantSelection = (SelectionMechanic->GetActiveSelection().IsEmpty() == false);
|
|
|
|
// Note: When not in triangle mode, ModifiedTopologyCounter refers to polygroup topology, so does not tell us
|
|
// about the triangle topology. In this case, we just assume the triangle topology may have been modified.
|
|
bool bModifiedTriangleTopology = bTriangleMode ? ModifiedTopologyCounter > 0 : true;
|
|
|
|
// may need to compact the mesh if we did undo on a mesh edit, then vertices will be dense but compact checks will fail...
|
|
if (bModifiedTriangleTopology)
|
|
{
|
|
// Store the compact maps if we have a selection that we need to update
|
|
CurrentMesh->CompactInPlace(bWantSelection ? &CompactMaps : nullptr);
|
|
}
|
|
|
|
// Finish prepping the stored selection
|
|
if (bWantSelection)
|
|
{
|
|
if (bTriangleMode)
|
|
{
|
|
SelectionMechanic->GetSelection_AsTriangleTopology(OutputSelection, bModifiedTriangleTopology ? &CompactMaps : nullptr);
|
|
}
|
|
else
|
|
{
|
|
SelectionMechanic->GetSelection_AsGroupTopology(OutputSelection, bModifiedTriangleTopology ? &CompactMaps : nullptr);
|
|
}
|
|
}
|
|
|
|
// Bake CurrentMesh back to target inside an undo transaction
|
|
GetToolManager()->BeginUndoTransaction(LOCTEXT("EditMeshPolygonsToolTransactionName", "Deform Mesh"));
|
|
MeshTransforms::ApplyTransformInverse(*CurrentMesh, BakedTransform, true);
|
|
UE::ToolTarget::CommitDynamicMeshUpdate(Target, *CurrentMesh, bModifiedTriangleTopology);
|
|
|
|
if (OutputSelection.IsEmpty() == false)
|
|
{
|
|
UE::Geometry::SetToolOutputGeometrySelectionForTarget(this, Target, OutputSelection);
|
|
}
|
|
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
Preview->Shutdown();
|
|
Preview = nullptr;
|
|
}
|
|
|
|
// The seleciton mechanic shutdown has to happen after (potentially) saving selection above
|
|
SelectionMechanic->Shutdown();
|
|
|
|
// We null out as many pointers as we can because the tool pointer usually ends up sticking
|
|
// around in the undo stack.
|
|
CommonProps = nullptr;
|
|
EditActions = nullptr;
|
|
EditActions_Triangles = nullptr;
|
|
EditEdgeActions = nullptr;
|
|
EditEdgeActions_Triangles = nullptr;
|
|
EditUVActions = nullptr;
|
|
CancelAction = nullptr;
|
|
AcceptCancelAction = nullptr;
|
|
|
|
ExtrudeActivity = nullptr;
|
|
InsetOutsetActivity = nullptr;
|
|
CutFacesActivity = nullptr;
|
|
PlanarProjectionUVActivity = nullptr;
|
|
InsertEdgeActivity = nullptr;
|
|
InsertEdgeLoopActivity = nullptr;
|
|
BevelEdgeActivity = nullptr;
|
|
|
|
SelectionMechanic = nullptr;
|
|
DragAlignmentMechanic = nullptr;
|
|
|
|
TransformGizmo = nullptr;
|
|
TransformProxy = nullptr;
|
|
|
|
CurrentMesh.Reset();
|
|
Topology.Reset();
|
|
MeshSpatial.Reset();
|
|
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::RegisterActions(FInteractiveToolActionSet& ActionSet)
|
|
{
|
|
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 2,
|
|
TEXT("ToggleLockRotation"),
|
|
LOCTEXT("ToggleLockRotationUIName", "Lock Rotation"),
|
|
LOCTEXT("ToggleLockRotationTooltip", "Toggle Frame Rotation Lock on and off"),
|
|
EModifierKey::None, EKeys::Q,
|
|
[this]() { CommonProps->bLockRotation = !CommonProps->bLockRotation; });
|
|
|
|
// Backspace and delete both trigger deletion (as long as the delete button is also enabled)
|
|
auto OnDeletionKeyPress = [this]()
|
|
{
|
|
if ((EditActions && EditActions->IsPropertySetEnabled())
|
|
|| (EditActions_Triangles && EditActions_Triangles->IsPropertySetEnabled()))
|
|
{
|
|
RequestAction(EEditMeshPolygonsToolActions::Delete);
|
|
}
|
|
};
|
|
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 3,
|
|
TEXT("DeleteSelectionBackSpaceKey"),
|
|
LOCTEXT("DeleteSelectionUIName", "Delete Selection"),
|
|
LOCTEXT("DeleteSelectionTooltip", "Delete Selection"),
|
|
EModifierKey::None, EKeys::BackSpace, OnDeletionKeyPress);
|
|
|
|
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 4,
|
|
TEXT("DeleteSelectionDeleteKey"),
|
|
LOCTEXT("DeleteSelectionUIName", "Delete Selection"),
|
|
LOCTEXT("DeleteSelectionTooltip", "Delete Selection"),
|
|
EModifierKey::None, EKeys::Delete, OnDeletionKeyPress);
|
|
|
|
// This hotkey can make the tool seem broken if it is accidentally pressed, so don't set a default.
|
|
// However we still register it because setting a hotkey can be useful in some workflows (when the
|
|
// gizmo gets in the way of shift-selecting multiple things).
|
|
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 5,
|
|
TEXT("ToggleGizmoVisibilityKey"),
|
|
LOCTEXT("ToggleGizmoVisibilityUIName", "Toggle Transform Gizmo Visibility"),
|
|
LOCTEXT("ToggleGizmoVisibilityTooltip", "Toggle the visibility of the transform gizmo"),
|
|
EModifierKey::None, EKeys::Invalid,
|
|
[this]()
|
|
{
|
|
if (!CurrentActivity)
|
|
{
|
|
CommonProps->bGizmoVisible = !CommonProps->bGizmoVisible;
|
|
}
|
|
});
|
|
|
|
// TODO: Esc should be made to exit out of current activity if one is active. However this
|
|
// requires a bit of work because we don't seem to be able to register conditional actions,
|
|
// and we don't want to always capture Esc.
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::RequestAction(EEditMeshPolygonsToolActions ActionType)
|
|
{
|
|
if (SelectionMechanic && SelectionMechanic->IsCurrentlyMarqueeDragging())
|
|
{
|
|
PendingAction = EEditMeshPolygonsToolActions::NoAction;
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("CannotActDuringMarquee", "Cannot perform action while marquee selecting"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
if (PendingAction != EEditMeshPolygonsToolActions::NoAction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PendingAction = ActionType;
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::RequestSingleShotAction(EEditMeshPolygonsToolActions ActionType)
|
|
{
|
|
bTerminateOnPendingActionComplete = true;
|
|
|
|
if ( ActionType == EEditMeshPolygonsToolActions::BevelAuto )
|
|
{
|
|
if (SelectionMechanic->GetActiveSelection().SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
ActionType = EEditMeshPolygonsToolActions::BevelEdges;
|
|
}
|
|
else
|
|
{
|
|
ActionType = EEditMeshPolygonsToolActions::BevelFaces;
|
|
}
|
|
}
|
|
|
|
RequestAction(ActionType);
|
|
}
|
|
|
|
|
|
FDynamicMeshAABBTree3& UEditMeshPolygonsTool::GetSpatial()
|
|
{
|
|
if (bSpatialDirty)
|
|
{
|
|
MeshSpatial->Build();
|
|
bSpatialDirty = false;
|
|
}
|
|
return *MeshSpatial;
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::UpdateGizmoFrame(const FFrame3d* UseFrame)
|
|
{
|
|
FFrame3d SetFrame = LastTransformerFrame;
|
|
if (UseFrame == nullptr)
|
|
{
|
|
if (CommonProps->LocalFrameMode == ELocalFrameMode::FromGeometry)
|
|
{
|
|
SetFrame = LastGeometryFrame;
|
|
}
|
|
else
|
|
{
|
|
SetFrame = FFrame3d(LastGeometryFrame.Origin, WorldTransform.GetRotation());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetFrame = *UseFrame;
|
|
}
|
|
|
|
if (CommonProps->bLockRotation)
|
|
{
|
|
SetFrame.Rotation = LockedTransfomerFrame.Rotation;
|
|
}
|
|
|
|
LastTransformerFrame = SetFrame;
|
|
|
|
if (TransformGizmo)
|
|
{
|
|
// This resets the scale as well
|
|
TransformGizmo->ReinitializeGizmoTransform(SetFrame.ToFTransform());
|
|
}
|
|
}
|
|
|
|
|
|
FBox UEditMeshPolygonsTool::GetWorldSpaceFocusBox()
|
|
{
|
|
if (ensure(SelectionMechanic))
|
|
{
|
|
FAxisAlignedBox3d Bounds = SelectionMechanic->GetSelectionBounds(true);
|
|
return (FBox)Bounds;
|
|
}
|
|
return FBox(EForceInit::ForceInit);
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::GetWorldSpaceFocusPoint(const FRay& WorldRay, FVector& PointOut)
|
|
{
|
|
FRay3d LocalRay(WorldTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
|
|
WorldTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
|
|
UE::Geometry::Normalize(LocalRay.Direction);
|
|
|
|
int32 HitTID = GetSpatial().FindNearestHitTriangle(LocalRay);
|
|
if (HitTID != IndexConstants::InvalidID)
|
|
{
|
|
FIntrRay3Triangle3d TriHit = TMeshQueries<FDynamicMesh3>::TriangleIntersection(*GetSpatial().GetMesh(), HitTID, LocalRay);
|
|
FVector3d LocalPos = LocalRay.PointAt(TriHit.RayParameter);
|
|
PointOut = (FVector)WorldTransform.TransformPosition(LocalPos);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::OnSelectionModifiedEvent()
|
|
{
|
|
bSelectionStateDirty = true;
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::OnBeginGizmoTransform(UTransformProxy* Proxy)
|
|
{
|
|
SelectionMechanic->ClearHighlight();
|
|
UpdateDeformerFromSelection( SelectionMechanic->GetActiveSelection() );
|
|
|
|
FTransform Transform = Proxy->GetTransform();
|
|
InitialGizmoFrame = FFrame3d(Transform);
|
|
InitialGizmoScale = FVector3d(Transform.GetScale3D());
|
|
|
|
BeginDeformerChange();
|
|
|
|
bInGizmoDrag = true;
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::OnGizmoTransformChanged(UTransformProxy* Proxy, FTransform Transform)
|
|
{
|
|
if (bInGizmoDrag)
|
|
{
|
|
LastUpdateGizmoFrame = FFrame3d(Transform);
|
|
LastUpdateGizmoScale = FVector3d(Transform.GetScale3D());
|
|
GetToolManager()->PostInvalidation();
|
|
bGizmoUpdatePending = true;
|
|
bLastUpdateUsedWorldFrame = (TransformGizmo ? TransformGizmo->CurrentCoordinateSystem == EToolContextCoordinateSystem::World : false);
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::OnEndGizmoTransform(UTransformProxy* Proxy)
|
|
{
|
|
bInGizmoDrag = false;
|
|
// Sometimes we don't get a tick between OnGizmoTransformChanged and OnEndGizmoTransform. In
|
|
// most drag cases this is not much of a problem, but if we type values into the gizmo numerical
|
|
// UI, it is.
|
|
if (bGizmoUpdatePending)
|
|
{
|
|
ComputeUpdate_Gizmo();
|
|
}
|
|
bGizmoUpdatePending = false;
|
|
bSpatialDirty = true;
|
|
SelectionMechanic->NotifyMeshChanged(false);
|
|
|
|
FFrame3d TransformFrame(Proxy->GetTransform());
|
|
|
|
if (TransformGizmo)
|
|
{
|
|
if (CommonProps->bLockRotation)
|
|
{
|
|
FFrame3d SetFrame = TransformFrame;
|
|
SetFrame.Rotation = LockedTransfomerFrame.Rotation;
|
|
TransformGizmo->ReinitializeGizmoTransform(SetFrame.ToFTransform());
|
|
}
|
|
else
|
|
{
|
|
TransformGizmo->SetNewChildScale(FVector::OneVector);
|
|
}
|
|
}
|
|
|
|
LastTransformerFrame = TransformFrame;
|
|
|
|
// close change record
|
|
EndDeformerChange();
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::UpdateDeformerFromSelection(const FGroupTopologySelection& Selection)
|
|
{
|
|
//Determine which of the following (corners, edges or faces) has been selected by counting the associated feature's IDs
|
|
if (Selection.SelectedCornerIDs.Num() > 0)
|
|
{
|
|
//Add all the the Corner's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LinearDeformer.SetActiveHandleCorners(Selection.SelectedCornerIDs.Array());
|
|
}
|
|
else if (Selection.SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
//Add all the the edge's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LinearDeformer.SetActiveHandleEdges(Selection.SelectedEdgeIDs.Array());
|
|
}
|
|
else if (Selection.SelectedGroupIDs.Num() > 0)
|
|
{
|
|
LinearDeformer.SetActiveHandleFaces(Selection.SelectedGroupIDs.Array());
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ComputeUpdate_Gizmo()
|
|
{
|
|
if (SelectionMechanic->HasSelection() == false || bGizmoUpdatePending == false)
|
|
{
|
|
return;
|
|
}
|
|
bGizmoUpdatePending = false;
|
|
|
|
FFrame3d CurFrame = LastUpdateGizmoFrame;
|
|
FVector3d CurScale = LastUpdateGizmoScale;
|
|
FVector3d TranslationDelta = CurFrame.Origin - InitialGizmoFrame.Origin;
|
|
FQuaterniond RotateDelta = CurFrame.Rotation - InitialGizmoFrame.Rotation;
|
|
FVector3d CurScaleDelta = CurScale - InitialGizmoScale;
|
|
FVector3d LocalTranslation = WorldTransform.InverseTransformVector(TranslationDelta);
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
if (TranslationDelta.SquaredLength() > 0.0001 || RotateDelta.SquaredLength() > 0.0001 || CurScaleDelta.SquaredLength() > 0.0001)
|
|
{
|
|
if (bLastUpdateUsedWorldFrame)
|
|
{
|
|
// For a world frame gizmo, the scaling needs to happen in world aligned gizmo space, but the
|
|
// rotation is still encoded in the local gizmo frame change.
|
|
FQuaterniond RotationToApply = CurFrame.Rotation * InitialGizmoFrame.Rotation.Inverse();
|
|
LinearDeformer.UpdateSolution(Mesh, [&](FDynamicMesh3* TargetMesh, int VertIdx)
|
|
{
|
|
FVector3d PosLocal = TargetMesh->GetVertex(VertIdx);
|
|
FVector3d PosWorld = WorldTransform.TransformPosition(PosLocal);
|
|
FVector3d PosWorldGizmo = PosWorld - InitialGizmoFrame.Origin;
|
|
|
|
FVector3d NewPosWorld = RotationToApply * (PosWorldGizmo * CurScale) + CurFrame.Origin;
|
|
FVector3d NewPosLocal = WorldTransform.InverseTransformPosition(NewPosWorld);
|
|
return NewPosLocal;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
LinearDeformer.UpdateSolution(Mesh, [&](FDynamicMesh3* TargetMesh, int VertIdx)
|
|
{
|
|
// For a local gizmo, we just get the coordinates in the original frame, scale in that frame,
|
|
// then interpret them as coordinates in the new frame.
|
|
FVector3d PosLocal = TargetMesh->GetVertex(VertIdx);
|
|
FVector3d PosWorld = WorldTransform.TransformPosition(PosLocal);
|
|
FVector3d PosGizmo = InitialGizmoFrame.ToFramePoint(PosWorld);
|
|
PosGizmo = CurScale * PosGizmo;
|
|
FVector3d NewPosWorld = CurFrame.FromFramePoint(PosGizmo);
|
|
FVector3d NewPosLocal = WorldTransform.InverseTransformPosition(NewPosWorld);
|
|
return NewPosLocal;
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Reset mesh to initial positions.
|
|
LinearDeformer.ClearSolution(Mesh);
|
|
}
|
|
|
|
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get(),
|
|
// It's important to use the fast update path for the gizmo manipulations that only
|
|
// affect positions.
|
|
UPreviewMesh::ERenderUpdateMode::FastUpdate,
|
|
EMeshRenderAttributeFlags::Positions | EMeshRenderAttributeFlags::VertexNormals);
|
|
|
|
GetToolManager()->PostInvalidation();
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::OnTick(float DeltaTime)
|
|
{
|
|
if (bToolDisabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Preview->Tick(DeltaTime);
|
|
|
|
if (CurrentActivity)
|
|
{
|
|
CurrentActivity->Tick(DeltaTime);
|
|
}
|
|
|
|
bool bLocalCoordSystem = GetToolManager()->GetPairedGizmoManager()
|
|
->GetContextQueriesAPI()->GetCurrentCoordinateSystem() == EToolContextCoordinateSystem::Local;
|
|
if (CommonProps->bLocalCoordSystem != bLocalCoordSystem)
|
|
{
|
|
CommonProps->bLocalCoordSystem = bLocalCoordSystem;
|
|
NotifyOfPropertyChangeByTool(CommonProps);
|
|
}
|
|
|
|
if (bGizmoUpdatePending)
|
|
{
|
|
ComputeUpdate_Gizmo();
|
|
}
|
|
|
|
if (bSelectionStateDirty)
|
|
{
|
|
// update color highlights
|
|
Preview->PreviewMesh->FastNotifySecondaryTrianglesChanged();
|
|
|
|
UpdateGizmoVisibility();
|
|
|
|
bSelectionStateDirty = false;
|
|
}
|
|
|
|
if (PendingAction != EEditMeshPolygonsToolActions::NoAction)
|
|
{
|
|
// Clear any existing error messages.
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
|
|
|
|
switch (PendingAction)
|
|
{
|
|
|
|
//Interactive operations:
|
|
case EEditMeshPolygonsToolActions::Extrude:
|
|
{
|
|
ExtrudeActivity->ExtrudeMode = FExtrudeOp::EExtrudeMode::MoveAndStitch;
|
|
ExtrudeActivity->PropertySetToUse = UPolyEditExtrudeActivity::EPropertySetToUse::Extrude;
|
|
StartActivity(ExtrudeActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::PushPull:
|
|
{
|
|
ExtrudeActivity->ExtrudeMode = FExtrudeOp::EExtrudeMode::Boolean;
|
|
ExtrudeActivity->PropertySetToUse = UPolyEditExtrudeActivity::EPropertySetToUse::PushPull;
|
|
StartActivity(ExtrudeActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::Offset:
|
|
{
|
|
ExtrudeActivity->ExtrudeMode = FExtrudeOp::EExtrudeMode::MoveAndStitch;
|
|
ExtrudeActivity->PropertySetToUse = UPolyEditExtrudeActivity::EPropertySetToUse::Offset;
|
|
StartActivity(ExtrudeActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::Inset:
|
|
{
|
|
InsetOutsetActivity->Settings->bOutset = false;
|
|
StartActivity(InsetOutsetActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::Outset:
|
|
{
|
|
InsetOutsetActivity->Settings->bOutset = true;
|
|
StartActivity(InsetOutsetActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::CutFaces:
|
|
{
|
|
StartActivity(CutFacesActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::PlanarProjectionUV:
|
|
{
|
|
StartActivity(PlanarProjectionUVActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::InsertEdge:
|
|
{
|
|
StartActivity(InsertEdgeActivity);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::InsertEdgeLoop:
|
|
{
|
|
StartActivity(InsertEdgeLoopActivity);
|
|
break;
|
|
}
|
|
|
|
case EEditMeshPolygonsToolActions::BevelFaces:
|
|
case EEditMeshPolygonsToolActions::BevelEdges:
|
|
{
|
|
StartActivity(BevelEdgeActivity);
|
|
break;
|
|
}
|
|
|
|
case EEditMeshPolygonsToolActions::CancelCurrent:
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Cancel);
|
|
break;
|
|
}
|
|
case EEditMeshPolygonsToolActions::AcceptCurrent:
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Accept);
|
|
break;
|
|
}
|
|
|
|
// Single action operations:
|
|
case EEditMeshPolygonsToolActions::Merge:
|
|
ApplyMerge();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::Delete:
|
|
ApplyDelete();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::RecalculateNormals:
|
|
ApplyRecalcNormals();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::FlipNormals:
|
|
ApplyFlipNormals();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::CollapseEdge:
|
|
ApplyCollapseEdge();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::WeldEdges:
|
|
ApplyWeldEdges();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::StraightenEdge:
|
|
ApplyStraightenEdges();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::FillHole:
|
|
ApplyFillHole();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::BridgeEdges:
|
|
ApplyBridgeEdges();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::SimplifyAlongEdges:
|
|
ApplySimplifyAlongEdges();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::Retriangulate:
|
|
ApplyRetriangulate();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::Decompose:
|
|
ApplyDecompose();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::Disconnect:
|
|
ApplyDisconnect();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::Duplicate:
|
|
ApplyDuplicate();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::PokeSingleFace:
|
|
ApplyPokeSingleFace();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::SplitSingleEdge:
|
|
ApplySplitSingleEdge();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::CollapseSingleEdge:
|
|
ApplyCollapseSingleEdge();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::FlipSingleEdge:
|
|
ApplyFlipSingleEdge();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::SimplifyByGroups:
|
|
SimplifyByGroups();
|
|
break;
|
|
case EEditMeshPolygonsToolActions::RegenerateExtraCorners:
|
|
ApplyRegenerateExtraCorners();
|
|
break;
|
|
}
|
|
|
|
PendingAction = EEditMeshPolygonsToolActions::NoAction;
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::StartActivity(TObjectPtr<UInteractiveToolActivity> Activity)
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Accept);
|
|
|
|
// Right now we rely on the activity to fail to start or to issue an error message if the
|
|
// conditions are not right. Someday, we are going to disable the buttons based on a CanStart
|
|
// call.
|
|
if (Activity->Start() == EToolActivityStartResult::Running)
|
|
{
|
|
if (TransformGizmo)
|
|
{
|
|
TransformGizmo->SetVisibility(false);
|
|
}
|
|
SelectionMechanic->SetIsEnabled(false);
|
|
SetToolPropertySourceEnabled(SelectionMechanic->Properties, false);
|
|
SetToolPropertySourceEnabled(TopologyProperties, false);
|
|
CurrentActivity = Activity;
|
|
|
|
if ( bTerminateOnPendingActionComplete == false )
|
|
{
|
|
if (CurrentActivity->HasAccept())
|
|
{
|
|
SetToolPropertySourceEnabled(AcceptCancelAction, true);
|
|
}
|
|
else
|
|
{
|
|
SetToolPropertySourceEnabled(CancelAction, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetToolPropertySourceEnabled(CommonProps, false);
|
|
}
|
|
|
|
SetActionButtonPanelsVisible(false);
|
|
}
|
|
else
|
|
{
|
|
if (bTerminateOnPendingActionComplete)
|
|
{
|
|
GetToolManager()->PostActiveToolShutdownRequest(this, EToolShutdownType::Cancel);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::EndCurrentActivity(EToolShutdownType ShutdownType)
|
|
{
|
|
if (CurrentActivity)
|
|
{
|
|
if (CurrentActivity->IsRunning())
|
|
{
|
|
CurrentActivity->End(ShutdownType);
|
|
}
|
|
|
|
CurrentActivity = nullptr;
|
|
++ActivityTimestamp;
|
|
|
|
if (bTerminateOnPendingActionComplete)
|
|
{
|
|
GetToolManager()->PostActiveToolShutdownRequest(this, ShutdownType);
|
|
return;
|
|
}
|
|
|
|
SetToolPropertySourceEnabled(CancelAction, false);
|
|
SetToolPropertySourceEnabled(AcceptCancelAction, false);
|
|
SetActionButtonPanelsVisible(true);
|
|
SelectionMechanic->SetIsEnabled(true);
|
|
SetToolPropertySourceEnabled(TopologyProperties, true);
|
|
SetToolPropertySourceEnabled(SelectionMechanic->Properties, true);
|
|
UpdateGizmoVisibility();
|
|
}
|
|
|
|
// If an activity displays a notification, it should be
|
|
// overwritten with an appropriate notification once finished
|
|
ResetUserMessage();
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::NotifyActivitySelfEnded(UInteractiveToolActivity* Activity)
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Accept);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::UpdateGizmoVisibility()
|
|
{
|
|
// Only allow gizmo to become visible if something is selected,
|
|
// the gizmo isn't hidden, and there is no current activity.
|
|
if (SelectionMechanic->HasSelection() && CommonProps->bGizmoVisible && !CurrentActivity)
|
|
{
|
|
if (TransformGizmo)
|
|
{
|
|
TransformGizmo->SetVisibility(true);
|
|
}
|
|
|
|
// Update frame because we might be here due to an undo event/etc,
|
|
// rather than an explicit selection change
|
|
LastGeometryFrame = SelectionMechanic->GetSelectionFrame(true, &LastGeometryFrame);
|
|
UpdateGizmoFrame();
|
|
}
|
|
else if(TransformGizmo)
|
|
{
|
|
TransformGizmo->SetVisibility(false);
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::Render(IToolsContextRenderAPI* RenderAPI)
|
|
{
|
|
if (bToolDisabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Preview->PreviewMesh->EnableWireframe(CommonProps->bShowWireframe);
|
|
SelectionMechanic->Render(RenderAPI);
|
|
DragAlignmentMechanic->Render(RenderAPI);
|
|
|
|
if (CurrentActivity)
|
|
{
|
|
CurrentActivity->Render(RenderAPI);
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI)
|
|
{
|
|
if (bToolDisabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SelectionMechanic->DrawHUD(Canvas, RenderAPI);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
|
|
{
|
|
if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UPolyEditCommonProperties, bShowSelectableCorners)))
|
|
{
|
|
SelectionMechanic->SetShowSelectableCorners(CommonProps->bShowSelectableCorners);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Gizmo change tracking
|
|
//
|
|
void UEditMeshPolygonsTool::UpdateDeformerChangeFromROI(bool bFinal)
|
|
{
|
|
if (ActiveVertexChange == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
ActiveVertexChange->SaveVertices(Mesh, LinearDeformer.GetModifiedVertices(), !bFinal);
|
|
ActiveVertexChange->SaveOverlayNormals(Mesh, LinearDeformer.GetModifiedOverlayNormals(), !bFinal);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::BeginDeformerChange()
|
|
{
|
|
if (ActiveVertexChange == nullptr)
|
|
{
|
|
ActiveVertexChange = new FMeshVertexChangeBuilder(EMeshVertexChangeComponents::VertexPositions | EMeshVertexChangeComponents::OverlayNormals);
|
|
UpdateDeformerChangeFromROI(false);
|
|
}
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::EndDeformerChange()
|
|
{
|
|
if (ActiveVertexChange != nullptr)
|
|
{
|
|
UpdateDeformerChangeFromROI(true);
|
|
GetToolManager()->EmitObjectChange(this, MoveTemp(ActiveVertexChange->Change),
|
|
LOCTEXT("PolyMeshDeformationChange", "PolyMesh Edit"));
|
|
}
|
|
|
|
delete ActiveVertexChange;
|
|
ActiveVertexChange = nullptr;
|
|
}
|
|
|
|
// This gets called by vertex change events emitted via gizmo (deformer) interaction
|
|
void UEditMeshPolygonsTool::ApplyChange(const FMeshVertexChange* Change, bool bRevert)
|
|
{
|
|
Preview->PreviewMesh->ApplyChange(Change, bRevert);
|
|
CurrentMesh->Copy(*Preview->PreviewMesh->GetMesh());
|
|
bSpatialDirty = true;
|
|
SelectionMechanic->NotifyMeshChanged(false);
|
|
|
|
// Topology does not need updating
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::UpdateFromCurrentMesh(bool bUpdateTopology)
|
|
{
|
|
Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get(), UPreviewMesh::ERenderUpdateMode::FullUpdate);
|
|
bSpatialDirty = true;
|
|
SelectionMechanic->NotifyMeshChanged(bUpdateTopology);
|
|
|
|
if (bUpdateTopology)
|
|
{
|
|
Topology->RebuildTopology();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyMerge()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnMergeFailedMessage", "Cannot Merge Current Selection"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
ChangeTracker.SaveTriangles(ActiveTriangleSelection, true);
|
|
FMeshConnectedComponents Components(Mesh);
|
|
Components.FindConnectedTriangles(ActiveTriangleSelection);
|
|
FGroupTopologySelection NewSelection;
|
|
for (const FMeshConnectedComponents::FComponent& Component : Components)
|
|
{
|
|
int32 NewGroupID = Mesh->AllocateTriangleGroup();
|
|
FaceGroupUtil::SetGroupID(*Mesh, Component.Indices, NewGroupID);
|
|
NewSelection.SelectedGroupIDs.Add(NewGroupID);
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshMergeChange", "Merge"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyDelete()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnDeleteFailedMessage", "Cannot Delete Current Selection"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
|
|
// prevent deleting all triangles
|
|
if (ActiveTriangleSelection.Num() >= Mesh->TriangleCount())
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnDeleteAllFailedMessage", "Cannot Delete Entire Mesh"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
ChangeTracker.SaveTriangles(ActiveTriangleSelection, true);
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
Editor.RemoveTriangles(ActiveTriangleSelection, true);
|
|
|
|
FGroupTopologySelection NewSelection;
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshDeleteChange", "Delete"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyRecalcNormals()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnRecalcNormalsFailedMessage", "Cannot Recalculate Normals for Current Selection"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
for (int32 GroupID : ActiveSelection.SelectedGroupIDs)
|
|
{
|
|
ChangeTracker.SaveTriangles(Topology->GetGroupTriangles(GroupID), true);
|
|
Editor.SetTriangleNormals(Topology->GetGroupTriangles(GroupID));
|
|
}
|
|
|
|
// We actually don't even need any of the wrapper around this change since we're not altering
|
|
// positions or topology (so no other structures need updating), but we go ahead and go the
|
|
// same route as everything else. See :HandlePositionOnlyMeshChanges
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshRecalcNormalsChange", "Recalculate Normals"),
|
|
ChangeTracker.EndChange(), ActiveSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyFlipNormals()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnFlipNormalsFailedMessage", "Cannot Flip Normals for Current Selection"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
for (int32 GroupID : ActiveSelection.SelectedGroupIDs)
|
|
{
|
|
for ( int32 tid : Topology->GetGroupTriangles(GroupID) )
|
|
{
|
|
ChangeTracker.SaveTriangle(tid, true);
|
|
Mesh->ReverseTriOrientation(tid);
|
|
}
|
|
}
|
|
|
|
// Note the topology can change in that the ordering of edge elements can reverse
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshFlipNormalsChange", "Flip Normals"),
|
|
ChangeTracker.EndChange(), ActiveSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyRetriangulate()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage( LOCTEXT("OnRetriangulateFailed", "Cannot Retriangulate Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
int32 nCompleted = 0;
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
for (int32 GroupID : ActiveSelection.SelectedGroupIDs)
|
|
{
|
|
const TArray<int32>& Triangles = Topology->GetGroupTriangles(GroupID);
|
|
ChangeTracker.SaveTriangles(Triangles, true);
|
|
FMeshRegionBoundaryLoops RegionLoops(Mesh, Triangles, true);
|
|
if (!RegionLoops.bFailed && RegionLoops.Loops.Num() == 1 && Triangles.Num() > 1)
|
|
{
|
|
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>> VidUVMaps;
|
|
if (Mesh->HasAttributes())
|
|
{
|
|
const FDynamicMeshAttributeSet* Attributes = Mesh->Attributes();
|
|
for (int i = 0; i < Attributes->NumUVLayers(); ++i)
|
|
{
|
|
VidUVMaps.Emplace();
|
|
RegionLoops.GetLoopOverlayMap(RegionLoops.Loops[0], *Attributes->GetUVLayer(i), VidUVMaps.Last());
|
|
}
|
|
}
|
|
|
|
// We don't want to remove isolated vertices while removing triangles because we don't
|
|
// want to throw away boundary verts. However, this means that we'll have to go back
|
|
// through these vertices later to throw away isolated internal verts.
|
|
TArray<int32> OldVertices;
|
|
UE::Geometry::TriangleToVertexIDs(Mesh, Triangles, OldVertices);
|
|
Editor.RemoveTriangles(Topology->GetGroupTriangles(GroupID), false);
|
|
|
|
RegionLoops.Loops[0].Reverse();
|
|
FSimpleHoleFiller Filler(Mesh, RegionLoops.Loops[0]);
|
|
Filler.FillType = FSimpleHoleFiller::EFillType::PolygonEarClipping;
|
|
Filler.Fill(GroupID);
|
|
|
|
// Throw away any of the old verts that are still isolated (they were in the interior of the group)
|
|
Algo::ForEachIf(OldVertices,
|
|
[Mesh](int32 Vid)
|
|
{
|
|
return !Mesh->IsReferencedVertex(Vid);
|
|
},
|
|
[Mesh](int32 Vid)
|
|
{
|
|
checkSlow(!Mesh->IsReferencedVertex(Vid));
|
|
constexpr bool bPreserveManifold = false;
|
|
Mesh->RemoveVertex(Vid, bPreserveManifold);
|
|
}
|
|
);
|
|
|
|
if (Mesh->HasAttributes())
|
|
{
|
|
const FDynamicMeshAttributeSet* Attributes = Mesh->Attributes();
|
|
for (int i = 0; i < Attributes->NumUVLayers(); ++i)
|
|
{
|
|
RegionLoops.UpdateLoopOverlayMapValidity(VidUVMaps[i], *Attributes->GetUVLayer(i));
|
|
}
|
|
Filler.UpdateAttributes(VidUVMaps);
|
|
}
|
|
|
|
nCompleted++;
|
|
}
|
|
}
|
|
if (nCompleted != ActiveSelection.SelectedGroupIDs.Num())
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnRetriangulateFailures", "Some faces could not be retriangulated"), EToolMessageLevel::UserWarning);
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshRetriangulateChange", "Retriangulate"),
|
|
ChangeTracker.EndChange(), ActiveSelection);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::SimplifyByGroups()
|
|
{
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
ChangeTracker.SaveTriangles(Mesh->TriangleIndicesItr(), true); // We will change the entire mesh
|
|
|
|
FPolygroupRemesh Remesh(Mesh, Topology.Get(), ConstrainedDelaunayTriangulate<double>);
|
|
bool bSuccess = Remesh.Compute();
|
|
if (!bSuccess)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnSimplifyByGroupFailures", "Some polygroups could not be correctly simplified"), EToolMessageLevel::UserWarning);
|
|
}
|
|
|
|
FGroupTopologySelection NewSelection; // Empty the selection
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshSimplifyByGroup", "Simplify by Group"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyRegenerateExtraCorners()
|
|
{
|
|
if (!ensure(!bTriangleMode && Topology))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We need to remember the extra corners that get generated and put them into the undo system so that if we
|
|
// change the settings later, undoing still brings us back to the result we saw at that time.
|
|
TSet<int32> PreviousExtraCorners(Topology->GetCurrentExtraCornerVids());
|
|
Topology->RebuildTopology();
|
|
const TSet<int32>& NewExtraCorners = Topology->GetCurrentExtraCornerVids();
|
|
|
|
bool bCornersChanged = PreviousExtraCorners.Num() != NewExtraCorners.Num() || !PreviousExtraCorners.Includes(NewExtraCorners);
|
|
if (bCornersChanged)
|
|
{
|
|
const FText TransactionLabel = LOCTEXT("RegenerateCornersTransactionName", "Regenerate Corners");
|
|
|
|
GetToolManager()->BeginUndoTransaction(TransactionLabel);
|
|
if (SelectionMechanic && !SelectionMechanic->GetActiveSelection().IsEmpty())
|
|
{
|
|
SelectionMechanic->BeginChange();
|
|
SelectionMechanic->ClearSelection();
|
|
GetToolManager()->EmitObjectChange(SelectionMechanic, SelectionMechanic->EndChange(), TransactionLabel);
|
|
}
|
|
|
|
GetToolManager()->EmitObjectChange(this,
|
|
MakeUnique<EditMeshPolygonsToolLocals::FExtraCornerChange>(PreviousExtraCorners, NewExtraCorners),
|
|
TransactionLabel);
|
|
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
if (SelectionMechanic)
|
|
{
|
|
SelectionMechanic->NotifyMeshChanged(true);
|
|
}
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::RebuildTopologyWithGivenExtraCorners(const TSet<int32>& Vids)
|
|
{
|
|
Topology->RebuildTopologyWithSpecificExtraCorners(Vids);
|
|
SelectionMechanic->NotifyMeshChanged(true);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyDecompose()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnDecomposeFailed", "Cannot Decompose Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FGroupTopologySelection NewSelection;
|
|
for (int32 GroupID : SelectionMechanic->GetActiveSelection().SelectedGroupIDs)
|
|
{
|
|
const TArray<int32>& Triangles = Topology->GetGroupTriangles(GroupID);
|
|
ChangeTracker.SaveTriangles(Triangles, true);
|
|
for (int32 tid : Triangles)
|
|
{
|
|
int32 NewGroupID = Mesh->AllocateTriangleGroup();
|
|
Mesh->SetTriangleGroup(tid, NewGroupID);
|
|
NewSelection.SelectedGroupIDs.Add(NewGroupID);
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshDecomposeChange", "Decompose"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyDisconnect()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnDisconnectFailed", "Cannot Disconnect Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
TArray<int32> AllTriangles;
|
|
for (int32 GroupID : ActiveSelection.SelectedGroupIDs)
|
|
{
|
|
AllTriangles.Append(Topology->GetGroupTriangles(GroupID));
|
|
}
|
|
ChangeTracker.SaveTriangles(AllTriangles, true);
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
Editor.DisconnectTriangles(AllTriangles, false);
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshDisconnectChange", "Disconnect"),
|
|
ChangeTracker.EndChange(), ActiveSelection);
|
|
}
|
|
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyDuplicate()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnDuplicateFailed", "Cannot Duplicate Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
TArray<int32> AllTriangles;
|
|
for (int32 GroupID : ActiveSelection.SelectedGroupIDs)
|
|
{
|
|
AllTriangles.Append(Topology->GetGroupTriangles(GroupID));
|
|
}
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FMeshIndexMappings Mappings;
|
|
FDynamicMeshEditResult EditResult;
|
|
Editor.DuplicateTriangles(AllTriangles, Mappings, EditResult);
|
|
|
|
FGroupTopologySelection NewSelection;
|
|
NewSelection.SelectedGroupIDs.Append(bTriangleMode ? EditResult.NewTriangles : EditResult.NewGroups);
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshDisconnectChange", "Disconnect"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyCollapseEdge()
|
|
{
|
|
// AAAHHH cannot do because of overlays!
|
|
return;
|
|
|
|
if (SelectionMechanic->GetActiveSelection().SelectedEdgeIDs.Num() != 1 || BeginMeshEdgeEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnEdgeColllapseFailed", "Cannot Collapse current selection"),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
//const TArray<int32>& EdgeIDs = ActiveEdgeSelection[0].EdgeIDs;
|
|
//for (int32 eid : EdgeIDs)
|
|
//{
|
|
// if (Mesh->IsEdge(eid))
|
|
// {
|
|
// FIndex2i EdgeVerts = Mesh->GetEdgeV(eid);
|
|
// ChangeTracker.SaveVertexOneRingTriangles(EdgeVerts.A, true);
|
|
// ChangeTracker.SaveVertexOneRingTriangles(EdgeVerts.B, true);
|
|
// FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
|
|
// Mesh->CollapseEdge()
|
|
// }
|
|
//}
|
|
|
|
// emit undo
|
|
FGroupTopologySelection NewSelection;
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshEdgeCollapseChange", "Collapse"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyWeldEdges()
|
|
{
|
|
if (SelectionMechanic->GetActiveSelection().SelectedEdgeIDs.Num() != 2)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedEdgeCount", "Cannot Weld current selection, selection must be exactly 2 edges."),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMesh3 MeshCopy(*Mesh); // We are going to operate on this copy and, if successful, copy the changes back into Mesh
|
|
|
|
FGroupTopologySelection CurrentSelection = SelectionMechanic->GetActiveSelection();
|
|
TArray<int32> SelectedEdgeIDs = CurrentSelection.SelectedEdgeIDs.Array();
|
|
FEdgeSpan& SpanA = Topology->Edges[SelectedEdgeIDs[0]].Span;
|
|
FEdgeSpan& SpanB = Topology->Edges[SelectedEdgeIDs[1]].Span;
|
|
|
|
if (SpanA.Vertices[0] == SpanA.Vertices.Last() || SpanB.Vertices[0] == SpanB.Vertices.Last())
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedEdgesAreLoops", "Cannot Weld current selection, selected edges must not be loops."),
|
|
EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
|
|
// Save one ring tri's for vertices along first edge
|
|
for (int Vert : SpanA.Vertices)
|
|
{
|
|
ChangeTracker.SaveVertexOneRingTriangles(Vert, true);
|
|
}
|
|
|
|
// Save one ring tri's for vertices along second edge
|
|
for (int Vert : SpanB.Vertices)
|
|
{
|
|
ChangeTracker.SaveVertexOneRingTriangles(Vert, true);
|
|
}
|
|
|
|
FWeldEdgeSequence EdgeWelder(&MeshCopy, SpanA, SpanB);
|
|
EdgeWelder.bAllowIntermediateTriangleDeletion = true;
|
|
EdgeWelder.bAllowFailedMerge = true;
|
|
|
|
FWeldEdgeSequence::EWeldResult Result = EdgeWelder.Weld();
|
|
if (Result != FWeldEdgeSequence::EWeldResult::Ok)
|
|
{
|
|
switch (Result)
|
|
{
|
|
case FWeldEdgeSequence::EWeldResult::Failed_EdgesNotBoundaryEdges:
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedBoundary", "Cannot Weld current selection, selected edges must be boundary edges."),
|
|
EToolMessageLevel::UserWarning);
|
|
break;
|
|
|
|
case FWeldEdgeSequence::EWeldResult::Failed_CannotSplitEdge:
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedSplitEdge", "Cannot Weld current selection, failed to insert vertex."),
|
|
EToolMessageLevel::UserWarning);
|
|
break;
|
|
|
|
case FWeldEdgeSequence::EWeldResult::Failed_TriangleDeletionDisabled:
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedTriDeleteDisabled", "Cannot Weld current selection, deletion of edges connecting selected edges is disabled."),
|
|
EToolMessageLevel::UserWarning);
|
|
break;
|
|
|
|
case FWeldEdgeSequence::EWeldResult::Failed_CannotDeleteTriangle:
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedTriDeleteFailed", "Cannot Weld current selection, failed to delete edge connecting selected edges."),
|
|
EToolMessageLevel::UserWarning);
|
|
break;
|
|
|
|
case FWeldEdgeSequence::EWeldResult::Failed_Other:
|
|
default:
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesFailedOther", "Cannot Weld current selection, bad geometry."),
|
|
EToolMessageLevel::UserWarning);
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// On success, apply the result by copying over the existing mesh
|
|
*Mesh = MeshCopy;
|
|
|
|
if (EdgeWelder.UnmergedEdgePairsOut.Num() != 0)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnWeldEdgesCompletedSeamsRemain", "Warning: welding incomplete because it would create "
|
|
"invalid geometry (attached non manifold edge or duplicate triangle). Seam still exists at weld "
|
|
"location. Modify attached triangles and retry, or undo."),
|
|
EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
|
|
FGroupTopologySelection NewSelection;
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshWeldEdgeChange", "Weld Edges"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ApplyStraightenEdges()
|
|
{
|
|
if (BeginMeshEdgeEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnStraightenEdgesFailed", "Cannot Straighten current selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
|
|
for (const FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
const TArray<int32>& EdgeVerts = Topology->GetGroupEdgeVertices(Edge.EdgeTopoID);
|
|
int32 NumV = EdgeVerts.Num();
|
|
if ( NumV > 2 )
|
|
{
|
|
ChangeTracker.SaveVertexOneRingTriangles(EdgeVerts, true);
|
|
FVector3d A(Mesh->GetVertex(EdgeVerts[0])), B(Mesh->GetVertex(EdgeVerts[NumV-1]));
|
|
TArray<double> VtxArcLengths;
|
|
double EdgeArcLen = Topology->GetEdgeArcLength(Edge.EdgeTopoID, &VtxArcLengths);
|
|
for (int k = 1; k < NumV-1; ++k)
|
|
{
|
|
double t = VtxArcLengths[k] / EdgeArcLen;
|
|
Mesh->SetVertex(EdgeVerts[k], UE::Geometry::Lerp(A, B, t));
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO :HandlePositionOnlyMeshChanges Due to the group topology storing edge IDs that do not stay the same across
|
|
// undo/redo events even when the mesh topology stays the same after a FDynamicMeshChange, we actually have to treat
|
|
// all FDynamicMeshChange-based transactions as affecting group topology. Here we only changed vertex positions so
|
|
// we could add a separate overload that takes a FMeshVertexChange, and possibly one that takes an attribute change
|
|
// (or unify the three via an interface)
|
|
FGroupTopologySelection NewSelection;
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshStraightenEdgeChange", "Straighten Edges"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplySimplifyAlongEdges()
|
|
{
|
|
if (BeginMeshEdgeEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnSimplifyAlongEdgesFailed", "Cannot Simplify current selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
|
|
// Storage for edge sets is re-used for each selected polygon edge path
|
|
TSet<int32> SimplifyEdgeSet;
|
|
|
|
// Pre-save vertices and triangles along selected edges
|
|
for (const FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
if (Edge.EdgeIDs.Num() > 1)
|
|
{
|
|
const TArray<int32>& EdgeVerts = Topology->GetGroupEdgeVertices(Edge.EdgeTopoID);
|
|
ChangeTracker.SaveVertexOneRingTriangles(EdgeVerts, true);
|
|
}
|
|
}
|
|
|
|
// Attempt simplification along edges
|
|
for (const FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
if (Edge.EdgeIDs.Num() > 1)
|
|
{
|
|
SimplifyEdgeSet.Reset();
|
|
SimplifyEdgeSet.Append(Edge.EdgeIDs);
|
|
FLocalPlanarSimplify LocalSimplify;
|
|
LocalSimplify.bPreserveVertexNormals = false;
|
|
LocalSimplify.SimplifyAlongEdges(*Mesh, SimplifyEdgeSet);
|
|
}
|
|
}
|
|
|
|
FGroupTopologySelection NewSelection;
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshSimplifyAlongEdgesChange", "Simplify Along Edges"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyFillHole()
|
|
{
|
|
if (BeginMeshBoundaryEdgeEditChange(false) == false)
|
|
{
|
|
GetToolManager()->DisplayMessage( LOCTEXT("OnEdgeFillFailed", "Cannot Fill current selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FGroupTopologySelection NewSelection;
|
|
for (FSelectedEdge& FillEdge : ActiveEdgeSelection)
|
|
{
|
|
if (Mesh->IsBoundaryEdge(FillEdge.EdgeIDs[0])) // may no longer be boundary due to previous fill
|
|
{
|
|
FMeshBoundaryLoops BoundaryLoops(Mesh);
|
|
int32 LoopID = BoundaryLoops.FindLoopContainingEdge(FillEdge.EdgeIDs[0]);
|
|
if (LoopID >= 0)
|
|
{
|
|
FEdgeLoop& Loop = BoundaryLoops.Loops[LoopID];
|
|
FSimpleHoleFiller Filler(Mesh, Loop);
|
|
Filler.FillType = FSimpleHoleFiller::EFillType::PolygonEarClipping;
|
|
int32 NewGroupID = Mesh->AllocateTriangleGroup();
|
|
Filler.Fill(NewGroupID);
|
|
if (!bTriangleMode)
|
|
{
|
|
NewSelection.SelectedGroupIDs.Add(NewGroupID);
|
|
}
|
|
else
|
|
{
|
|
NewSelection.SelectedGroupIDs.Append(Filler.NewTriangles);
|
|
}
|
|
|
|
// Compute normals and UVs
|
|
if (Mesh->HasAttributes())
|
|
{
|
|
TArray<FVector3d> VertexPositions;
|
|
Loop.GetVertices(VertexPositions);
|
|
FVector3d PlaneOrigin;
|
|
FVector3d PlaneNormal;
|
|
PolygonTriangulation::ComputePolygonPlane<double>(VertexPositions, PlaneNormal, PlaneOrigin);
|
|
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FFrame3d ProjectionFrame(PlaneOrigin, PlaneNormal);
|
|
Editor.SetTriangleNormals(Filler.NewTriangles);
|
|
Editor.SetTriangleUVsFromProjection(Filler.NewTriangles, ProjectionFrame, UVScaleFactor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshFillHoleChange", "Fill Hole"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ApplyBridgeEdges()
|
|
{
|
|
const FText BridgeFailMessage = LOCTEXT("OnEdgeBridgeFailed", "Cannot Bridge current selection");
|
|
|
|
if (SelectionMechanic->GetActiveSelection().SelectedEdgeIDs.Num() != 2 || BeginMeshBoundaryEdgeEditChange(false) == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(BridgeFailMessage, EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
FGroupTopologySelection CurrentSelection = SelectionMechanic->GetActiveSelection();
|
|
|
|
TArray<int32> LoopVertices;
|
|
TArray<int32> LoopEdges;
|
|
TArray<int32> SelectedEdgeIDs = CurrentSelection.SelectedEdgeIDs.Array();
|
|
|
|
// I think doing this will guarantee that every edge in the span stores the vertices corresponding to the connected triangles orientation
|
|
FEdgeSpan& SpanA = Topology->Edges[SelectedEdgeIDs[0]].Span;
|
|
FEdgeSpan& SpanB = Topology->Edges[SelectedEdgeIDs[1]].Span;
|
|
SpanA.SetCorrectOrientation();
|
|
SpanB.SetCorrectOrientation();
|
|
|
|
// Disallow bridging of edge loops for now
|
|
if (SpanA.Vertices[0] == SpanA.Vertices.Last() || SpanB.Vertices[0] == SpanB.Vertices.Last())
|
|
{
|
|
GetToolManager()->DisplayMessage(BridgeFailMessage, EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
// Add all vertices from first edge
|
|
LoopVertices = SpanA.Vertices;
|
|
|
|
// If first vertex of second edge is not a duplicate of a terminating vertex of the first edge, add vertex
|
|
if (SpanB.Vertices[0] != SpanA.Vertices[0] && SpanB.Vertices[0] != SpanA.Vertices.Last())
|
|
{
|
|
LoopVertices.Add(SpanB.Vertices[0]);
|
|
}
|
|
|
|
// Definitely add the non-terminating vertices of second edge.
|
|
for (int Vertex = 1; Vertex < SpanB.Vertices.Num() - 1; ++Vertex)
|
|
{
|
|
LoopVertices.Add(SpanB.Vertices[Vertex]);
|
|
}
|
|
|
|
// If last vertex of second edge is not a duplicate of a terminating vertex of the first edge, add vertex
|
|
if (SpanB.Vertices.Last() != SpanA.Vertices[0] && SpanB.Vertices.Last() != SpanA.Vertices.Last())
|
|
{
|
|
LoopVertices.Add(SpanB.Vertices.Last());
|
|
}
|
|
|
|
FEdgeLoop::VertexLoopToEdgeLoop(Mesh, LoopVertices, LoopEdges);
|
|
FEdgeLoop Loop(Mesh, LoopVertices, LoopEdges);
|
|
|
|
// We could always use the minimal hole filler, but it doesn't quite do what "bridge" would suggest when
|
|
// the area to be bridged is concave (across two curved-inward edges). Meanwhile simple ear clipping
|
|
// seems to fail in some common cases for reasons that we should investigate. For now, start with ear
|
|
// clipping, and revert to minimal if needed.
|
|
FSimpleHoleFiller SimpleHoleFiller(Mesh, Loop, FSimpleHoleFiller::EFillType::PolygonEarClipping);
|
|
TArray<int32> NewTriangles;
|
|
|
|
// Fill the hole
|
|
if (!SimpleHoleFiller.Fill())
|
|
{
|
|
//Ear clipping doesn't add vertices, so don't need to delete isolated verts
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
Editor.RemoveTriangles(SimpleHoleFiller.NewTriangles, false);
|
|
|
|
FMinimalHoleFiller MinimalHoleFiller(Mesh, Loop);
|
|
|
|
if (!MinimalHoleFiller.Fill())
|
|
{
|
|
Editor.RemoveTriangles(MinimalHoleFiller.NewTriangles, false);
|
|
GetToolManager()->DisplayMessage(BridgeFailMessage, EToolMessageLevel::UserWarning);
|
|
// Even though we've manually 'undone' the changes, this will still change mesh timestamps, so we need to register the mesh update
|
|
UpdateFromCurrentMesh(false);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
NewTriangles = MinimalHoleFiller.NewTriangles;
|
|
}
|
|
}
|
|
else {
|
|
NewTriangles = SimpleHoleFiller.NewTriangles;
|
|
}
|
|
|
|
// Compute normals and UVs
|
|
if (Mesh->HasAttributes())
|
|
{
|
|
TArray<FVector3d> VertexPositions;
|
|
Loop.GetVertices(VertexPositions);
|
|
FVector3d PlaneOrigin;
|
|
FVector3d PlaneNormal;
|
|
PolygonTriangulation::ComputePolygonPlane<double>(VertexPositions, PlaneNormal, PlaneOrigin);
|
|
|
|
FDynamicMeshEditor Editor(Mesh);
|
|
FFrame3d ProjectionFrame(PlaneOrigin, PlaneNormal);
|
|
Editor.SetTriangleNormals(NewTriangles);
|
|
Editor.SetTriangleUVsFromProjection(NewTriangles, ProjectionFrame, UVScaleFactor);
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshBridgeEdgeChange", "Bridge Edge"),
|
|
ChangeTracker.EndChange(), CurrentSelection);
|
|
}
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyPokeSingleFace()
|
|
{
|
|
if (BeginMeshFaceEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnPokeFailedMessage", "Cannot Poke Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
ChangeTracker.SaveTriangles(ActiveTriangleSelection, true);
|
|
FGroupTopologySelection NewSelection;
|
|
for (int32 tid : ActiveTriangleSelection)
|
|
{
|
|
FDynamicMesh3::FPokeTriangleInfo PokeInfo;
|
|
NewSelection.SelectedGroupIDs.Add(tid);
|
|
if (Mesh->PokeTriangle(tid, PokeInfo) == EMeshResult::Ok)
|
|
{
|
|
NewSelection.SelectedGroupIDs.Add(PokeInfo.NewTriangles.A);
|
|
NewSelection.SelectedGroupIDs.Add(PokeInfo.NewTriangles.B);
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshPokeChange", "Poke Faces"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
void UEditMeshPolygonsTool::ApplyFlipSingleEdge()
|
|
{
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
|
|
if (!BeginMeshEdgeEditChange([Mesh](int32 Eid) {return !Mesh->IsBoundaryEdge(Eid) && !Mesh->Attributes()->IsSeamEdge(Eid); }))
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnFlipFailedMessage", "Cannot Flip Current Selection (no non-seam edges selected)"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
for (FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
int32 eid = Edge.EdgeIDs[0];
|
|
if (Mesh->IsEdge(eid) && Mesh->IsBoundaryEdge(eid) == false && Mesh->Attributes()->IsSeamEdge(eid) == false)
|
|
{
|
|
FIndex2i et = Mesh->GetEdgeT(eid);
|
|
ChangeTracker.SaveTriangle(et.A, true);
|
|
ChangeTracker.SaveTriangle(et.B, true);
|
|
FDynamicMesh3::FEdgeFlipInfo FlipInfo;
|
|
Mesh->FlipEdge(eid, FlipInfo);
|
|
}
|
|
}
|
|
|
|
// After flipping edges, edge ID's stay the same but group edge id's in our topology end up swapping around after
|
|
// the topology rebuild (because they are assigned in order of iteration through triangles). In order to update them,
|
|
// we need to go ahead and do the topology rebuild now. This means that we don't actually need to do the rebuild
|
|
// that happens inside EmitCurrentMeshChangeAndUpdate, but it's not worth trying to refactor to avoid it, so we
|
|
// just end up doing an extraneous update.
|
|
FGroupTopologySelection NewSelection;
|
|
Topology->RebuildTopology();
|
|
for (FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
int32 Eid = Edge.EdgeIDs[0];
|
|
if (Mesh->IsEdge(Eid))
|
|
{
|
|
NewSelection.SelectedEdgeIDs.Add(Topology->FindGroupEdgeID(Eid));
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshFlipChange", "Flip Edges"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ApplyCollapseSingleEdge()
|
|
{
|
|
if (BeginMeshEdgeEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnCollapseFailedMessage", "Cannot Collapse Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FGroupTopologySelection ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
TSet<int32> ValidEdgeIDs;
|
|
for (FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
int32 eid = Edge.EdgeIDs[0];
|
|
if (Mesh->IsEdge(eid) && Mesh->Attributes()->IsSeamEdge(eid) == false)
|
|
{
|
|
ValidEdgeIDs.Add(eid);
|
|
}
|
|
}
|
|
TSet<int32> DoneEdgeIDs;
|
|
for (int32 eid : ValidEdgeIDs)
|
|
{
|
|
if (DoneEdgeIDs.Contains(eid) == false && Mesh->IsEdge(eid))
|
|
{
|
|
FIndex2i ev = Mesh->GetEdgeV(eid);
|
|
ChangeTracker.SaveVertexOneRingTriangles(ev.A, true);
|
|
ChangeTracker.SaveVertexOneRingTriangles(ev.B, true);
|
|
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
|
|
if (Mesh->CollapseEdge(ev.A, ev.B, CollapseInfo) == EMeshResult::Ok)
|
|
{
|
|
DoneEdgeIDs.Add(eid);
|
|
DoneEdgeIDs.Add(CollapseInfo.RemovedEdges.A);
|
|
DoneEdgeIDs.Add(CollapseInfo.RemovedEdges.B);
|
|
}
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshCollapseChange", "Collapse Edges"),
|
|
ChangeTracker.EndChange(), FGroupTopologySelection());
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::ApplySplitSingleEdge()
|
|
{
|
|
if (BeginMeshEdgeEditChange() == false)
|
|
{
|
|
GetToolManager()->DisplayMessage(LOCTEXT("OnSplitFailedMessage", "Cannot Split Current Selection"), EToolMessageLevel::UserWarning);
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
FGroupTopologySelection NewSelection;
|
|
FDynamicMeshChangeTracker ChangeTracker(Mesh);
|
|
ChangeTracker.BeginChange();
|
|
for (FSelectedEdge& Edge : ActiveEdgeSelection)
|
|
{
|
|
int32 eid = Edge.EdgeIDs[0];
|
|
if (Mesh->IsEdge(eid))
|
|
{
|
|
FIndex2i et = Mesh->GetEdgeT(eid);
|
|
ChangeTracker.SaveTriangle(et.A, true);
|
|
NewSelection.SelectedGroupIDs.Add(et.A);
|
|
if (et.B != FDynamicMesh3::InvalidID)
|
|
{
|
|
ChangeTracker.SaveTriangle(et.B, true);
|
|
NewSelection.SelectedGroupIDs.Add(et.B);
|
|
}
|
|
FDynamicMesh3::FEdgeSplitInfo SplitInfo;
|
|
if (Mesh->SplitEdge(eid, SplitInfo) == EMeshResult::Ok)
|
|
{
|
|
NewSelection.SelectedGroupIDs.Add(SplitInfo.NewTriangles.A);
|
|
if (SplitInfo.NewTriangles.B != FDynamicMesh3::InvalidID)
|
|
{
|
|
NewSelection.SelectedGroupIDs.Add(SplitInfo.NewTriangles.A);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
EmitCurrentMeshChangeAndUpdate(LOCTEXT("PolyMeshSplitChange", "Split Edges"),
|
|
ChangeTracker.EndChange(), NewSelection);
|
|
}
|
|
|
|
|
|
|
|
|
|
bool UEditMeshPolygonsTool::BeginMeshFaceEditChange()
|
|
{
|
|
ActiveTriangleSelection.Reset();
|
|
|
|
// need some selected faces
|
|
const FGroupTopologySelection& ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
Topology->GetSelectedTriangles(ActiveSelection, ActiveTriangleSelection);
|
|
if (ActiveSelection.SelectedGroupIDs.Num() == 0 || ActiveTriangleSelection.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FDynamicMesh3* Mesh = CurrentMesh.Get();
|
|
ActiveSelectionBounds = FAxisAlignedBox3d::Empty();
|
|
for (int tid : ActiveTriangleSelection)
|
|
{
|
|
ActiveSelectionBounds.Contain(Mesh->GetTriBounds(tid));
|
|
}
|
|
|
|
// world and local frames
|
|
ActiveSelectionFrameLocal = Topology->GetSelectionFrame(ActiveSelection);
|
|
ActiveSelectionFrameWorld = ActiveSelectionFrameLocal;
|
|
ActiveSelectionFrameWorld.Transform(WorldTransform);
|
|
|
|
return true;
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::EmitCurrentMeshChangeAndUpdate(const FText& TransactionLabel,
|
|
TUniquePtr<FDynamicMeshChange> MeshChangeIn,
|
|
const FGroupTopologySelection& OutputSelection)
|
|
{
|
|
// We used to take this as a paremeter, but even if we happen to know that the FDynamicMeshChange doesn't
|
|
// involve topology changes, it acts via deleting/reinserting triangles in undo/redo, which changes the
|
|
// eids in a mesh and causes problems. So we always treat the group topology as modified in this function.
|
|
// TODO: Have an overload that uses a vertex change for non-topology-modifying cases.
|
|
constexpr bool bGroupTopologyModified = true;
|
|
|
|
// open top-level transaction
|
|
GetToolManager()->BeginUndoTransaction(TransactionLabel);
|
|
|
|
// Since we clear the selection in the selection mechanic when topology changes, we need to know
|
|
// when OutputSelection is pointing to the selection in the selection mechanic and is not empty,
|
|
// so that we can copy it ahead of time and reinstate it.
|
|
bool bReferencingSameSelection = (&SelectionMechanic->GetActiveSelection() == &OutputSelection);
|
|
|
|
// Not actually relevant since our assumption of topology being modified means we always clear existing selection.
|
|
// bool bSelectionModified = !bReferencingSameSelection && SelectionMechanic->GetActiveSelection() != OutputSelection;
|
|
|
|
// In case we need to make a selection copy
|
|
FGroupTopologySelection TempSelection;
|
|
const FGroupTopologySelection* OutputSelectionToUse = &OutputSelection;
|
|
|
|
// Emit a selection clear before emitting the mesh change, so that undo restores it properly.
|
|
if (!SelectionMechanic->GetActiveSelection().IsEmpty() /* && (bSelectionModified || bGroupTopologyModified) */)
|
|
{
|
|
if (bReferencingSameSelection)
|
|
{
|
|
// Need to make a copy because OutputSelection will get cleared
|
|
TempSelection = OutputSelection;
|
|
OutputSelectionToUse = &TempSelection;
|
|
}
|
|
|
|
SelectionMechanic->BeginChange();
|
|
SelectionMechanic->ClearSelection();
|
|
GetToolManager()->EmitObjectChange(SelectionMechanic, SelectionMechanic->EndChange(), LOCTEXT("ClearSelection", "Clear Selection"));
|
|
}
|
|
|
|
// Prep and emit the mesh change. This needs to be bookended by the change in extra corners, since
|
|
// those get regenerated in the topology rebuild.
|
|
TUniquePtr<FEditMeshPolygonsToolMeshChange> ChangeToEmit = MakeUnique<FEditMeshPolygonsToolMeshChange>(MoveTemp(MeshChangeIn));
|
|
if (!Topology->GetCurrentExtraCornerVids().IsEmpty())
|
|
{
|
|
ChangeToEmit->ExtraCornerVidsBefore = Topology->GetCurrentExtraCornerVids();
|
|
}
|
|
Topology->RebuildTopology();
|
|
if (!Topology->GetCurrentExtraCornerVids().IsEmpty())
|
|
{
|
|
ChangeToEmit->ExtraCornerVidsAfter = Topology->GetCurrentExtraCornerVids();
|
|
}
|
|
GetToolManager()->EmitObjectChange(this,
|
|
MoveTemp(ChangeToEmit),
|
|
TransactionLabel);
|
|
|
|
// Update other related structures
|
|
UpdateFromCurrentMesh(false);
|
|
SelectionMechanic->NotifyMeshChanged(true); // This wasn't updated in UpdateFromCurrentMesh because we didn't ask to rebuild topology
|
|
ModifiedTopologyCounter += bGroupTopologyModified;
|
|
|
|
// Set output selection if there's a non-empty one. We know we've cleared the selection by
|
|
// this point due to treating topology as always modified.
|
|
if (!OutputSelectionToUse->IsEmpty() /* && (bSelectionModified || bGroupTopologyModified) */)
|
|
{
|
|
SelectionMechanic->BeginChange();
|
|
SelectionMechanic->SetSelection(*OutputSelectionToUse);
|
|
GetToolManager()->EmitObjectChange(SelectionMechanic, SelectionMechanic->EndChange(), LOCTEXT("SetSelection", "Set Selection"));
|
|
}
|
|
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::EmitActivityStart(const FText& TransactionLabel)
|
|
{
|
|
++ActivityTimestamp;
|
|
|
|
GetToolManager()->BeginUndoTransaction(TransactionLabel);
|
|
GetToolManager()->EmitObjectChange(this,
|
|
MakeUnique<FPolyEditActivityStartChange>(ActivityTimestamp),
|
|
TransactionLabel);
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
|
|
bool UEditMeshPolygonsTool::BeginMeshEdgeEditChange()
|
|
{
|
|
return BeginMeshEdgeEditChange([](int32) {return true; });
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::BeginMeshBoundaryEdgeEditChange(bool bOnlySimple)
|
|
{
|
|
if (bOnlySimple)
|
|
{
|
|
return BeginMeshEdgeEditChange(
|
|
[&](int32 GroupEdgeID) { return Topology->IsBoundaryEdge(GroupEdgeID) && Topology->IsSimpleGroupEdge(GroupEdgeID); });
|
|
}
|
|
else
|
|
{
|
|
return BeginMeshEdgeEditChange(
|
|
[&](int32 GroupEdgeID) { return Topology->IsBoundaryEdge(GroupEdgeID); });
|
|
}
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::BeginMeshEdgeEditChange(TFunctionRef<bool(int32)> GroupEdgeIDFilterFunc)
|
|
{
|
|
ActiveEdgeSelection.Reset();
|
|
|
|
const FGroupTopologySelection& ActiveSelection = SelectionMechanic->GetActiveSelection();
|
|
int NumEdges = ActiveSelection.SelectedEdgeIDs.Num();
|
|
if (NumEdges == 0)
|
|
{
|
|
return false;
|
|
}
|
|
ActiveEdgeSelection.Reserve(NumEdges);
|
|
for (int32 EdgeID : ActiveSelection.SelectedEdgeIDs)
|
|
{
|
|
if (GroupEdgeIDFilterFunc(EdgeID))
|
|
{
|
|
FSelectedEdge& Edge = ActiveEdgeSelection.Emplace_GetRef();
|
|
Edge.EdgeTopoID = EdgeID;
|
|
Edge.EdgeIDs = Topology->GetGroupEdgeEdges(EdgeID);
|
|
}
|
|
}
|
|
|
|
return ActiveEdgeSelection.Num() > 0;
|
|
}
|
|
|
|
void UEditMeshPolygonsTool::SetActionButtonPanelsVisible(bool bVisible)
|
|
{
|
|
if (bTriangleMode == false)
|
|
{
|
|
if (EditActions)
|
|
{
|
|
SetToolPropertySourceEnabled(EditActions, bVisible);
|
|
}
|
|
if (EditEdgeActions)
|
|
{
|
|
SetToolPropertySourceEnabled(EditEdgeActions, bVisible);
|
|
}
|
|
if (EditUVActions)
|
|
{
|
|
SetToolPropertySourceEnabled(EditUVActions, bVisible);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (EditActions_Triangles)
|
|
{
|
|
SetToolPropertySourceEnabled(EditActions_Triangles, bVisible);
|
|
}
|
|
if (EditEdgeActions_Triangles)
|
|
{
|
|
SetToolPropertySourceEnabled(EditEdgeActions_Triangles, bVisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool UEditMeshPolygonsTool::CanCurrentlyNestedCancel()
|
|
{
|
|
if ( bTerminateOnPendingActionComplete ) return false;
|
|
|
|
return CurrentActivity != nullptr
|
|
|| (SelectionMechanic && !SelectionMechanic->GetActiveSelection().IsEmpty());
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::ExecuteNestedCancelCommand()
|
|
{
|
|
if (CurrentActivity)
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Cancel);
|
|
return true;
|
|
}
|
|
else if (SelectionMechanic && !SelectionMechanic->GetActiveSelection().IsEmpty())
|
|
{
|
|
SelectionMechanic->BeginChange();
|
|
SelectionMechanic->ClearSelection();
|
|
GetToolManager()->EmitObjectChange(SelectionMechanic, SelectionMechanic->EndChange(), LOCTEXT("ClearSelection", "Clear Selection"));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::CanCurrentlyNestedAccept()
|
|
{
|
|
if ( bTerminateOnPendingActionComplete ) return false;
|
|
return CurrentActivity != nullptr;
|
|
}
|
|
|
|
bool UEditMeshPolygonsTool::ExecuteNestedAcceptCommand()
|
|
{
|
|
if (CurrentActivity)
|
|
{
|
|
EndCurrentActivity(EToolShutdownType::Accept);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FEditMeshPolygonsToolMeshChange::Apply(UObject* Object)
|
|
{
|
|
UEditMeshPolygonsTool* Tool = Cast<UEditMeshPolygonsTool>(Object);
|
|
|
|
MeshChange->Apply(Tool->CurrentMesh.Get(), false);
|
|
Tool->UpdateFromCurrentMesh(false);
|
|
++Tool->ModifiedTopologyCounter;
|
|
|
|
Tool->RebuildTopologyWithGivenExtraCorners(ExtraCornerVidsAfter);
|
|
|
|
Tool->ActivityContext->OnUndoRedo.Broadcast(true);
|
|
}
|
|
|
|
void FEditMeshPolygonsToolMeshChange::Revert(UObject* Object)
|
|
{
|
|
UEditMeshPolygonsTool* Tool = Cast<UEditMeshPolygonsTool>(Object);
|
|
|
|
MeshChange->Apply(Tool->CurrentMesh.Get(), true);
|
|
Tool->UpdateFromCurrentMesh(false);
|
|
++Tool->ModifiedTopologyCounter;
|
|
|
|
Tool->RebuildTopologyWithGivenExtraCorners(ExtraCornerVidsBefore);
|
|
|
|
Tool->ActivityContext->OnUndoRedo.Broadcast(true);
|
|
}
|
|
|
|
FString FEditMeshPolygonsToolMeshChange::ToString() const
|
|
{
|
|
return TEXT("FEditMeshPolygonsToolMeshChange");
|
|
}
|
|
|
|
void FPolyEditActivityStartChange::Revert(UObject* Object)
|
|
{
|
|
//note: previously called EndCurrentActivity() which defauled to Cancel, so leaving that behavior here...
|
|
Cast<UEditMeshPolygonsTool>(Object)->EndCurrentActivity(EToolShutdownType::Cancel);
|
|
bHaveDoneUndo = true;
|
|
}
|
|
bool FPolyEditActivityStartChange::HasExpired(UObject* Object) const
|
|
{
|
|
return bHaveDoneUndo
|
|
|| Cast<UEditMeshPolygonsTool>(Object)->ActivityTimestamp != ActivityTimestamp;
|
|
}
|
|
FString FPolyEditActivityStartChange::ToString() const
|
|
{
|
|
return TEXT("FPolyEditActivityStartChange");
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|