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

400 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "RevolveBoundaryTool.h"
#include "ModelingObjectsCreationAPI.h"
#include "BaseBehaviors/SingleClickBehavior.h"
#include "CoreMinimal.h"
#include "CompositionOps/CurveSweepOp.h"
#include "InteractiveToolManager.h"
#include "Mechanics/ConstructionPlaneMechanic.h"
#include "PrimitiveDrawingUtils.h" // FPrimitiveDrawInterface
#include "Selection/PolygonSelectionMechanic.h"
#include "GroupTopology.h"
#include "ToolBuilderUtil.h"
#include "Selection/ToolSelectionUtil.h"
#include "ModelingToolTargetUtil.h"
#include "ToolSceneQueriesUtil.h"
#include "ToolSetupUtil.h"
#include "ToolTargetManager.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(RevolveBoundaryTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "URevolveBoundaryTool"
// Tool builder
USingleSelectionMeshEditingTool* URevolveBoundaryToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
return NewObject<URevolveBoundaryTool>(SceneState.ToolManager);
}
bool URevolveBoundaryToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
// We're disallowing volumes here because volumes won't have open boundaries intrinsically.
return USingleSelectionMeshEditingToolBuilder::CanBuildTool(SceneState) &&
SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(),
[](UActorComponent& Component) { return !ToolBuilderUtil::IsVolume(Component); }) >= 1;
}
// Operator factory
TUniquePtr<FDynamicMeshOperator> URevolveBoundaryOperatorFactory::MakeNewOperator()
{
TUniquePtr<FCurveSweepOp> CurveSweepOp = MakeUnique<FCurveSweepOp>();
// Assemble profile curve
const FGroupTopologySelection& ActiveSelection = RevolveBoundaryTool->SelectionMechanic->GetActiveSelection();
if (ActiveSelection.SelectedEdgeIDs.Num() == 1)
{
int32 EdgeID = ActiveSelection.GetASelectedEdgeID();
if (RevolveBoundaryTool->Topology->IsBoundaryEdge(EdgeID))
{
const TArray<int32>& VertexIndices = RevolveBoundaryTool->Topology->GetGroupEdgeVertices(EdgeID);
FTransform ToWorld = Cast<IPrimitiveComponentBackedTarget>(RevolveBoundaryTool->Target)->GetWorldTransform();
// If boundary loop includes the last vertex as first, stop early.
// (Note: This is generally true unless the mesh has bowties that confuse the boundary walk.)
bool bIsLoop = VertexIndices.Num() && VertexIndices.Last() == VertexIndices[0];
CurveSweepOp->ProfileCurve.Reserve(VertexIndices.Num() - (int32)bIsLoop);
for (int32 i = 0; i < VertexIndices.Num() - (int32)bIsLoop; ++i)
{
int32 VertIndex = VertexIndices[i];
FVector3d NewPos = (FVector3d)ToWorld.TransformPosition((FVector)RevolveBoundaryTool->OriginalMesh->GetVertex(VertIndex));
CurveSweepOp->ProfileCurve.Add(NewPos);
}
CurveSweepOp->bProfileCurveIsClosed = bIsLoop;
}
}
RevolveBoundaryTool->Settings->ApplyToCurveSweepOp(*RevolveBoundaryTool->MaterialProperties,
RevolveBoundaryTool->RevolutionAxisOrigin, RevolveBoundaryTool->RevolutionAxisDirection, *CurveSweepOp);
return CurveSweepOp;
}
// Tool itself
void URevolveBoundaryTool::Setup()
{
UMeshBoundaryToolBase::Setup();
// We're actually going to handle the selection clicks ourselves so that we can align axis if
// we want to.
SelectionMechanic->DisableBehaviors(this);
SelectionMechanic->SetShouldAddToSelectionFunc([]() {return false; });
SelectionMechanic->SetShouldRemoveFromSelectionFunc([]() {return false; });
USingleClickInputBehavior* ClickBehavior = NewObject<USingleClickInputBehavior>();
ClickBehavior->Initialize(this);
ClickBehavior->Modifiers.RegisterModifier(CtrlModifier, FInputDeviceState::IsCtrlKeyDown);
ClickBehavior->Modifiers.RegisterModifier(ShiftModifier, FInputDeviceState::IsShiftKeyDown);
AddInputBehavior(ClickBehavior);
OutputTypeProperties = NewObject<UCreateMeshObjectTypeProperties>(this);
OutputTypeProperties->RestoreProperties(this);
OutputTypeProperties->InitializeDefault();
OutputTypeProperties->WatchProperty(OutputTypeProperties->OutputType, [this](FString) { OutputTypeProperties->UpdatePropertyVisibility(); });
AddToolPropertySource(OutputTypeProperties);
Settings = NewObject<URevolveBoundaryToolProperties>(this);
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
MaterialProperties = NewObject<UNewMeshMaterialProperties>(this);
AddToolPropertySource(MaterialProperties);
MaterialProperties->RestoreProperties(this);
FTransform LocalToWorld = UE::ToolTarget::GetLocalToWorldTransform(Target);
Settings->AxisOrigin = LocalToWorld.GetTranslation();
UpdateRevolutionAxis();
// The plane mechanic is used for the revolution axis
PlaneMechanic = NewObject<UConstructionPlaneMechanic>(this);
PlaneMechanic->Setup(this);
PlaneMechanic->Initialize(GetTargetWorld(), FFrame3d(Settings->AxisOrigin,
FRotator(Settings->AxisOrientation.X, Settings->AxisOrientation.Y, 0).Quaternion()));
PlaneMechanic->UpdateClickPriority(ClickBehavior->GetPriority().MakeLower());
PlaneMechanic->bShowGrid = false;
PlaneMechanic->OnPlaneChanged.AddLambda([this]() {
Settings->AxisOrigin = (FVector)PlaneMechanic->Plane.Origin;
FRotator AxisOrientation = ((FQuat)PlaneMechanic->Plane.Rotation).Rotator();
Settings->AxisOrientation.X = AxisOrientation.Pitch;
Settings->AxisOrientation.Y = AxisOrientation.Yaw;
NotifyOfPropertyChangeByTool(Settings);
UpdateRevolutionAxis();
});
Cast<IPrimitiveComponentBackedTarget>(Target)->SetOwnerVisibility(Settings->bDisplayInputMesh);
SetToolDisplayName(LOCTEXT("ToolName", "Revolve Boundary"));
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartRevolveBoundaryTool", "Revolve an open mesh boundary loop around an axis to create a new mesh. Ctrl+Click to align the axis to a surface/edge."),
EToolMessageLevel::UserNotification);
if (Topology->Edges.Num() == 1)
{
FGroupTopologySelection Selection;
Selection.SelectedEdgeIDs.Add(0);
SelectionMechanic->SetSelection(Selection);
StartPreview();
}
else if (Topology->Edges.Num() == 0)
{
GetToolManager()->DisplayMessage(
LOCTEXT("NoBoundaryLoops", "This mesh does not have any boundary loops to display and revolve. Delete faces to create a boundary or use a different mesh."),
EToolMessageLevel::UserWarning);
}
else
{
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartRevolveBoundaryToolMultipleBoundaries", "Your mesh has multiple boundaries. Select the one you wish to use."),
EToolMessageLevel::UserWarning);
}
}
void URevolveBoundaryTool::OnUpdateModifierState(int ModifierId, bool bIsOn)
{
if (ModifierId == CtrlModifier)
{
// Like with the plane mechanic, clicking an edge while holding ctrl should move the axis
// to that point and (by default) align it.
bMoveAxisOnClick = bIsOn;
}
else if (ModifierId == ShiftModifier)
{
// Like with the plane mechanic, holding ctrl + shift shifts the axis to the point without
// changing the orientation
bAlignAxisOnClick = !bIsOn;
}
}
FInputRayHit URevolveBoundaryTool::IsHitByClick(const FInputDeviceRay& ClickPos)
{
FHitResult OutHit;
if (SelectionMechanic->TopologyHitTest(ClickPos.WorldRay, OutHit))
{
return FInputRayHit(OutHit.Distance);
}
return FInputRayHit(); // bHit is false
}
void URevolveBoundaryTool::OnClicked(const FInputDeviceRay& ClickPos)
{
// Update selection only if we clicked on something. We don't want to be able to
// clear a selection with a click.
FHitResult HitResult;
if (SelectionMechanic->TopologyHitTest(ClickPos.WorldRay, HitResult))
{
FVector3d LocalHitPosition, LocalHitNormal;
SelectionMechanic->UpdateSelection(ClickPos.WorldRay, LocalHitPosition, LocalHitNormal);
// Clear the "multiple boundaries" warning, since we've selected one.
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
// Act on Ctrl/Shift modifiers the same way that the plane mechanic does.
if (bMoveAxisOnClick)
{
const FGroupTopologySelection& Selection = SelectionMechanic->GetActiveSelection();
int32 ClickedEid = Topology->GetGroupEdgeEdges(Selection.GetASelectedEdgeID())[HitResult.Item];
FVector3d VertexA, VertexB;
OriginalMesh->GetEdgeV(ClickedEid, VertexA, VertexB);
FTransform ToWorldTranform = Cast<IPrimitiveComponentBackedTarget>(Target)->GetWorldTransform();
FLine3d EdgeLine = FLine3d::FromPoints((FVector3d)ToWorldTranform.TransformPosition((FVector)VertexA),
(FVector3d)ToWorldTranform.TransformPosition((FVector)VertexB));
// New frame starts as the old one with modified origin
FFrame3d RevolutionAxisFrame(EdgeLine.NearestPoint((FVector3d)HitResult.ImpactPoint), PlaneMechanic->Plane.Rotation);
if (bAlignAxisOnClick)
{
RevolutionAxisFrame.AlignAxis(0, EdgeLine.Direction);
}
PlaneMechanic->SetPlaneWithoutBroadcast(RevolutionAxisFrame);
Settings->AxisOrigin = (FVector)RevolutionAxisFrame.Origin;
FRotator AxisOrientation = ((FQuat)RevolutionAxisFrame.Rotation).Rotator();
Settings->AxisOrientation.X = AxisOrientation.Pitch;
Settings->AxisOrientation.Y = AxisOrientation.Yaw;
UpdateRevolutionAxis();
}
// Update the preview
if (Preview == nullptr)
{
StartPreview();
}
else
{
Preview->InvalidateResult();
}
}
}
bool URevolveBoundaryTool::CanAccept() const
{
return Preview != nullptr && Preview->HaveValidNonEmptyResult();
}
/**
* Uses the settings stored in the properties object to update the revolution axis
*/
void URevolveBoundaryTool::UpdateRevolutionAxis()
{
RevolutionAxisOrigin = (FVector3d)Settings->AxisOrigin;
RevolutionAxisDirection = (FVector3d)FRotator(Settings->AxisOrientation.X, Settings->AxisOrientation.Y, 0).RotateVector(FVector(1, 0, 0));
if (Preview)
{
Preview->InvalidateResult();
}
}
void URevolveBoundaryTool::StartPreview()
{
URevolveBoundaryOperatorFactory* RevolveBoundaryOpCreator = NewObject<URevolveBoundaryOperatorFactory>();
RevolveBoundaryOpCreator->RevolveBoundaryTool = this;
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(RevolveBoundaryOpCreator);
Preview->Setup(GetTargetWorld(), RevolveBoundaryOpCreator);
ToolSetupUtil::ApplyRenderingConfigurationToPreview(Preview->PreviewMesh, nullptr);
Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated);
Preview->ConfigureMaterials(MaterialProperties->Material.Get(),
ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
Preview->PreviewMesh->EnableWireframe(MaterialProperties->bShowWireframe);
Preview->OnMeshUpdated.AddLambda(
[this](const UMeshOpPreviewWithBackgroundCompute* UpdatedPreview)
{
UpdateAcceptWarnings(UpdatedPreview->HaveEmptyResult() ? EAcceptWarning::EmptyForbidden : EAcceptWarning::NoWarning);
}
);
Preview->SetVisibility(true);
Preview->InvalidateResult();
}
void URevolveBoundaryTool::OnShutdown(EToolShutdownType ShutdownType)
{
UMeshBoundaryToolBase::OnShutdown(ShutdownType);
OutputTypeProperties->SaveProperties(this);
Settings->SaveProperties(this);
MaterialProperties->SaveProperties(this);
PlaneMechanic->Shutdown();
Cast<IPrimitiveComponentBackedTarget>(Target)->SetOwnerVisibility(true);
if (Preview)
{
if (ShutdownType == EToolShutdownType::Accept)
{
Preview->PreviewMesh->CalculateTangents();
GenerateAsset(Preview->Shutdown());
}
else
{
Preview->Cancel();
}
}
}
void URevolveBoundaryTool::GenerateAsset(const FDynamicMeshOpResult& OpResult)
{
if (OpResult.Mesh->TriangleCount() <= 0)
{
return;
}
GetToolManager()->BeginUndoTransaction(LOCTEXT("RevolveBoundaryToolTransactionName", "Boundary Revolve Tool"));
FCreateMeshObjectParams NewMeshObjectParams;
NewMeshObjectParams.TargetWorld = GetTargetWorld();
NewMeshObjectParams.Transform = (FTransform)OpResult.Transform;
NewMeshObjectParams.BaseName = TEXT("Revolve");
NewMeshObjectParams.Materials.Add(MaterialProperties->Material.Get());
NewMeshObjectParams.SetMesh(OpResult.Mesh.Get());
OutputTypeProperties->ConfigureCreateMeshObjectParams(NewMeshObjectParams);
FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams));
if (Result.IsOK() && Result.NewActor != nullptr)
{
ToolSelectionUtil::SetNewActorSelection(GetToolManager(), Result.NewActor);
}
GetToolManager()->EndUndoTransaction();
}
void URevolveBoundaryTool::OnTick(float DeltaTime)
{
if (PlaneMechanic != nullptr)
{
PlaneMechanic->Tick(DeltaTime);
}
if (Preview)
{
Preview->Tick(DeltaTime);
}
}
void URevolveBoundaryTool::Render(IToolsContextRenderAPI* RenderAPI)
{
UMeshBoundaryToolBase::Render(RenderAPI);
FViewCameraState CameraState;
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState);
if (PlaneMechanic != nullptr)
{
PlaneMechanic->Render(RenderAPI);
// Draw the axis of rotation
float PdiScale = CameraState.GetPDIScalingFactor();
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
FColor AxisColor(240, 16, 240);
double AxisThickness = 1 * PdiScale;
double AxisHalfLength = ToolSceneQueriesUtil::CalculateDimensionFromVisualAngleD(CameraState, RevolutionAxisOrigin, 90);
FVector3d StartPoint = RevolutionAxisOrigin - (RevolutionAxisDirection * (AxisHalfLength * PdiScale));
FVector3d EndPoint = RevolutionAxisOrigin + (RevolutionAxisDirection * (AxisHalfLength * PdiScale));
PDI->DrawLine((FVector)StartPoint, (FVector)EndPoint, AxisColor, SDPG_Foreground,
AxisThickness, 0.0f, true);
}
}
void URevolveBoundaryTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
PlaneMechanic->SetPlaneWithoutBroadcast(FFrame3d(Settings->AxisOrigin,
FRotator(Settings->AxisOrientation.X, Settings->AxisOrientation.Y, 0).Quaternion()));
UpdateRevolutionAxis();
Cast<IPrimitiveComponentBackedTarget>(Target)->SetOwnerVisibility(Settings->bDisplayInputMesh);
if (Preview)
{
if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UNewMeshMaterialProperties, Material)))
{
Preview->ConfigureMaterials(MaterialProperties->Material.Get(),
ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
}
Preview->PreviewMesh->EnableWireframe(MaterialProperties->bShowWireframe);
Preview->InvalidateResult();
}
}
#undef LOCTEXT_NAMESPACE