Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/MeshModelingTools/Private/DrawAndRevolveTool.cpp
2025-05-18 13:04:45 +08:00

373 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DrawAndRevolveTool.h"
#include "BaseBehaviors/SingleClickBehavior.h"
#include "BaseBehaviors/MouseHoverBehavior.h"
#include "CompositionOps/CurveSweepOp.h"
#include "Generators/SweepGenerator.h"
#include "InteractiveToolManager.h" // To use SceneState.ToolManager
#include "Mechanics/ConstructionPlaneMechanic.h"
#include "Mechanics/CurveControlPointsMechanic.h"
#include "Properties/MeshMaterialProperties.h"
#include "ModelingObjectsCreationAPI.h"
#include "PrimitiveDrawingUtils.h" // FPrimitiveDrawInterface
#include "Selection/ToolSelectionUtil.h"
#include "ToolSceneQueriesUtil.h"
#include "ToolSetupUtil.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(DrawAndRevolveTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UDrawAndRevolveTool"
const FText InitializationModeMessage = LOCTEXT("CurveInitialization", "Draw a path and revolve it around the purple axis. Left-click to place path vertices, and click on the last or first vertex to complete the path. Hold Shift to invert snapping behavior.");
const FText EditModeMessage = LOCTEXT("CurveEditing", "Click points to select them, Shift+click to modify selection. Ctrl+click a segment to add a point, or select an endpoint and Ctrl+click to add new endpoint. Backspace deletes selected points. Hold Shift to invert snapping behavior.");
// Tool builder
bool UDrawAndRevolveToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
return true;
}
UInteractiveTool* UDrawAndRevolveToolBuilder::BuildTool(const FToolBuilderState& SceneState) const
{
UDrawAndRevolveTool* NewTool = NewObject<UDrawAndRevolveTool>(SceneState.ToolManager);
NewTool->SetWorld(SceneState.World);
FQuaterniond DefaultOrientation = (FQuaterniond)FRotator(90, 0, 0);
NewTool->SetInitialDrawFrame(ToolSetupUtil::GetDefaultWorldReferenceFrame(SceneState.ToolManager, DefaultOrientation));
return NewTool;
}
// Operator factory
TUniquePtr<FDynamicMeshOperator> URevolveOperatorFactory::MakeNewOperator()
{
TUniquePtr<FCurveSweepOp> CurveSweepOp = MakeUnique<FCurveSweepOp>();
// Assemble profile curve
CurveSweepOp->ProfileCurve.Reserve(RevolveTool->ControlPointsMechanic->GetNumPoints() + 2); // extra space for top/bottom caps
RevolveTool->ControlPointsMechanic->ExtractPointPositions(CurveSweepOp->ProfileCurve);
CurveSweepOp->bProfileCurveIsClosed = RevolveTool->ControlPointsMechanic->GetIsLoop();
// If we are capping the top and bottom, we just add a couple extra vertices and mark the curve as being closed
if (!CurveSweepOp->bProfileCurveIsClosed && RevolveTool->Settings->bClosePathToAxis)
{
// Project first and last points onto the revolution axis.
FVector3d FirstPoint = CurveSweepOp->ProfileCurve[0];
FVector3d LastPoint = CurveSweepOp->ProfileCurve.Last();
double DistanceAlongAxis = RevolveTool->RevolutionAxisDirection.Dot(LastPoint - RevolveTool->RevolutionAxisOrigin);
FVector3d ProjectedPoint = RevolveTool->RevolutionAxisOrigin + (RevolveTool->RevolutionAxisDirection * DistanceAlongAxis);
CurveSweepOp->ProfileCurve.Add(ProjectedPoint);
DistanceAlongAxis = RevolveTool->RevolutionAxisDirection.Dot(FirstPoint - RevolveTool->RevolutionAxisOrigin);
ProjectedPoint = RevolveTool->RevolutionAxisOrigin + (RevolveTool->RevolutionAxisDirection * DistanceAlongAxis);
CurveSweepOp->ProfileCurve.Add(ProjectedPoint);
CurveSweepOp->bProfileCurveIsClosed = true;
}
RevolveTool->Settings->ApplyToCurveSweepOp(*RevolveTool->MaterialProperties,
RevolveTool->RevolutionAxisOrigin, RevolveTool->RevolutionAxisDirection, *CurveSweepOp);
return CurveSweepOp;
}
// Tool itself
void UDrawAndRevolveTool::RegisterActions(FInteractiveToolActionSet& ActionSet)
{
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 1,
TEXT("DeletePointBackspaceKey"),
LOCTEXT("DeletePointUIName", "Delete Point"),
LOCTEXT("DeletePointTooltip", "Delete currently selected point(s)"),
EModifierKey::None, EKeys::BackSpace,
[this]() { OnPointDeletionKeyPress(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 2,
TEXT("DeletePointDeleteKey"),
LOCTEXT("DeletePointUIName", "Delete Point"),
LOCTEXT("DeletePointTooltip", "Delete currently selected point(s)"),
EModifierKey::None, EKeys::Delete,
[this]() { OnPointDeletionKeyPress(); });
}
void UDrawAndRevolveTool::OnPointDeletionKeyPress()
{
if (ControlPointsMechanic)
{
ControlPointsMechanic->DeleteSelectedPoints();
}
}
bool UDrawAndRevolveTool::CanAccept() const
{
return Preview != nullptr && Preview->HaveValidNonEmptyResult();
}
void UDrawAndRevolveTool::Setup()
{
UInteractiveTool::Setup();
SetToolDisplayName(LOCTEXT("ToolName", "Revolve PolyPath"));
GetToolManager()->DisplayMessage(InitializationModeMessage, EToolMessageLevel::UserNotification);
OutputTypeProperties = NewObject<UCreateMeshObjectTypeProperties>(this);
OutputTypeProperties->RestoreProperties(this);
OutputTypeProperties->InitializeDefault();
OutputTypeProperties->WatchProperty(OutputTypeProperties->OutputType, [this](FString) { OutputTypeProperties->UpdatePropertyVisibility(); });
AddToolPropertySource(OutputTypeProperties);
Settings = NewObject<URevolveToolProperties>(this, TEXT("Revolve Tool Settings"));
Settings->RestoreProperties(this);
Settings->bAllowedToEditDrawPlane = true;
AddToolPropertySource(Settings);
Settings->DrawPlaneOrigin = InitialDrawFrame.Origin;
Settings->DrawPlaneOrientation = (FRotator)InitialDrawFrame.Rotation;
MaterialProperties = NewObject<UNewMeshMaterialProperties>(this);
AddToolPropertySource(MaterialProperties);
MaterialProperties->RestoreProperties(this);
ControlPointsMechanic = NewObject<UCurveControlPointsMechanic>(this);
ControlPointsMechanic->Setup(this);
ControlPointsMechanic->SetWorld(TargetWorld);
ControlPointsMechanic->OnPointsChanged.AddLambda([this]() {
if (Preview)
{
Preview->InvalidateResult();
}
bool bAllowedToEditDrawPlane = (ControlPointsMechanic->GetNumPoints() == 0);
if (Settings->bAllowedToEditDrawPlane != bAllowedToEditDrawPlane)
{
Settings->bAllowedToEditDrawPlane = bAllowedToEditDrawPlane;
NotifyOfPropertyChangeByTool(Settings);
}
});
// This gets called when we enter/leave curve initialization mode
ControlPointsMechanic->OnModeChanged.AddLambda([this]() {
if (ControlPointsMechanic->IsInInteractiveIntialization())
{
// We're back to initializing so hide the preview
if (Preview)
{
Preview->Cancel();
Preview = nullptr;
}
GetToolManager()->DisplayMessage(InitializationModeMessage, EToolMessageLevel::UserNotification);
}
else
{
StartPreview();
GetToolManager()->DisplayMessage(EditModeMessage, EToolMessageLevel::UserNotification);
}
});
ControlPointsMechanic->SetSnappingEnabled(Settings->bEnableSnapping);
UpdateRevolutionAxis();
// The plane mechanic lets us update the plane in which we draw the profile curve, as long as we haven't
// started adding points to it already.
FFrame3d ProfileDrawPlane(Settings->DrawPlaneOrigin, Settings->DrawPlaneOrientation.Quaternion());
PlaneMechanic = NewObject<UConstructionPlaneMechanic>(this);
PlaneMechanic->Setup(this);
PlaneMechanic->Initialize(TargetWorld, ProfileDrawPlane);
PlaneMechanic->UpdateClickPriority(ControlPointsMechanic->ClickBehavior->GetPriority().MakeHigher());
PlaneMechanic->CanUpdatePlaneFunc = [this]()
{
return ControlPointsMechanic->GetNumPoints() == 0;
};
PlaneMechanic->OnPlaneChanged.AddLambda([this]() {
Settings->DrawPlaneOrigin = (FVector)PlaneMechanic->Plane.Origin;
Settings->DrawPlaneOrientation = ((FQuat)PlaneMechanic->Plane.Rotation).Rotator();
NotifyOfPropertyChangeByTool(Settings);
if (ControlPointsMechanic)
{
ControlPointsMechanic->SetPlane(PlaneMechanic->Plane);
}
UpdateRevolutionAxis();
});
ControlPointsMechanic->SetPlane(PlaneMechanic->Plane);
ControlPointsMechanic->SetInteractiveInitialization(true);
ControlPointsMechanic->SetAutoRevertToInteractiveInitialization(true);
// For things to behave nicely, we expect to revolve at least a two point
// segment if it's not a loop, and a three point segment if it is. Revolving
// a two-point loop to make a double sided thing is a pain to support because
// it forces us to deal with manifoldness issues that we would really rather
// not worry about (we'd have to duplicate vertices to stay manifold)
ControlPointsMechanic->SetMinPointsToLeaveInteractiveInitialization(3, 2);
}
/** Uses the settings currently stored in the properties object to update the revolution axis. */
void UDrawAndRevolveTool::UpdateRevolutionAxis()
{
RevolutionAxisOrigin = (FVector3d)Settings->DrawPlaneOrigin;
RevolutionAxisDirection = (FVector3d)Settings->DrawPlaneOrientation.RotateVector(FVector(1,0,0));
const int32 AXIS_SNAP_TARGET_ID = 1;
ControlPointsMechanic->RemoveSnapLine(AXIS_SNAP_TARGET_ID);
ControlPointsMechanic->AddSnapLine(AXIS_SNAP_TARGET_ID, FLine3d(RevolutionAxisOrigin, RevolutionAxisDirection));
}
void UDrawAndRevolveTool::Shutdown(EToolShutdownType ShutdownType)
{
OutputTypeProperties->SaveProperties(this);
Settings->SaveProperties(this);
MaterialProperties->SaveProperties(this);
PlaneMechanic->Shutdown();
ControlPointsMechanic->Shutdown();
if (Preview)
{
if (ShutdownType == EToolShutdownType::Accept)
{
Preview->PreviewMesh->CalculateTangents(); // Copy tangents from the PreviewMesh
GenerateAsset(Preview->Shutdown());
}
else
{
Preview->Cancel();
}
}
}
void UDrawAndRevolveTool::GenerateAsset(const FDynamicMeshOpResult& OpResult)
{
if (OpResult.Mesh->TriangleCount() <= 0)
{
return;
}
GetToolManager()->BeginUndoTransaction(LOCTEXT("RevolveToolTransactionName", "Path Revolve Tool"));
FCreateMeshObjectParams NewMeshObjectParams;
NewMeshObjectParams.TargetWorld = TargetWorld;
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 UDrawAndRevolveTool::StartPreview()
{
URevolveOperatorFactory* RevolveOpCreator = NewObject<URevolveOperatorFactory>();
RevolveOpCreator->RevolveTool = this;
// Normally we wouldn't give the object a name, but since we may destroy the preview using undo,
// the ability to reuse the non-cleaned up memory is useful. Careful if copy-pasting this!
Preview = NewObject<UMeshOpPreviewWithBackgroundCompute>(RevolveOpCreator, "RevolveToolPreview");
Preview->Setup(TargetWorld, RevolveOpCreator);
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 UDrawAndRevolveTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
FFrame3d ProfileDrawPlane(Settings->DrawPlaneOrigin, Settings->DrawPlaneOrientation.Quaternion());
ControlPointsMechanic->SetPlane(ProfileDrawPlane);
PlaneMechanic->SetPlaneWithoutBroadcast(ProfileDrawPlane);
UpdateRevolutionAxis();
ControlPointsMechanic->SetSnappingEnabled(Settings->bEnableSnapping);
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();
}
}
void UDrawAndRevolveTool::OnTick(float DeltaTime)
{
if (PlaneMechanic != nullptr)
{
PlaneMechanic->Tick(DeltaTime);
}
if (Preview)
{
Preview->Tick(DeltaTime);
}
}
void UDrawAndRevolveTool::Render(IToolsContextRenderAPI* RenderAPI)
{
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.0 * 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);
}
if (ControlPointsMechanic != nullptr)
{
ControlPointsMechanic->Render(RenderAPI);
}
}
bool UDrawAndRevolveTool::CanGenerateAsset() const
{
if (Preview)
{
return 0 < Preview->PreviewMesh->GetPreviewDynamicMesh()->TriangleCount();
}
return false;
}
#undef LOCTEXT_NAMESPACE