// Copyright Epic Games, Inc. All Rights Reserved. #include "CutMeshWithMeshTool.h" #include "CompositionOps/BooleanMeshesOp.h" #include "ToolSetupUtil.h" #include "BaseGizmos/TransformGizmoUtil.h" #include "Selection/ToolSelectionUtil.h" #include "ModelingObjectsCreationAPI.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/MeshTransforms.h" #include "MeshDescriptionToDynamicMesh.h" #include "DynamicMeshToMeshDescription.h" #include "Async/Async.h" #include "TargetInterfaces/MaterialProvider.h" #include "TargetInterfaces/MeshDescriptionCommitter.h" #include "TargetInterfaces/MeshDescriptionProvider.h" #include "TargetInterfaces/PrimitiveComponentBackedTarget.h" #include "TargetInterfaces/AssetBackedTarget.h" #include "ModelingToolTargetUtil.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(CutMeshWithMeshTool) using namespace UE::Geometry; namespace { // probably should be something defined for the whole tool framework... #if WITH_EDITOR static EAsyncExecution CutMeshWithMeshToolAsyncExecTarget = EAsyncExecution::LargeThreadPool; #else static EAsyncExecution CutMeshWithMeshToolAsyncExecTarget = EAsyncExecution::ThreadPool; #endif } #define LOCTEXT_NAMESPACE "UCutMeshWithMeshTool" void UCutMeshWithMeshTool::SetupProperties() { Super::SetupProperties(); CutProperties = NewObject(this); CutProperties->RestoreProperties(this); AddToolPropertySource(CutProperties); SetToolDisplayName(LOCTEXT("CutMeshWithMeshToolName", "Cut With Mesh")); GetToolManager()->DisplayMessage( LOCTEXT("OnStartTool", "Cut the first input mesh with the second input mesh. Use the transform gizmos to modify the position and orientation of the input objects."), EToolMessageLevel::UserNotification); // create intersection preview mesh object IntersectPreviewMesh = NewObject(this); IntersectPreviewMesh->CreateInWorld(GetTargetWorld(), FTransform::Identity); ToolSetupUtil::ApplyRenderingConfigurationToPreview(IntersectPreviewMesh, nullptr); IntersectPreviewMesh->SetVisible(true); IntersectPreviewMesh->SetMaterial(ToolSetupUtil::GetDefaultBrushVolumeMaterial(GetToolManager())); } void UCutMeshWithMeshTool::SaveProperties() { Super::SaveProperties(); CutProperties->SaveProperties(this); IntersectPreviewMesh->Disconnect(); } void UCutMeshWithMeshTool::ConvertInputsAndSetPreviewMaterials(bool bSetPreviewMesh) { // disable output options // (this property set is not registered yet in SetupProperties() above) SetToolPropertySourceEnabled(HandleSourcesProperties, false); SetToolPropertySourceEnabled(OutputTypeProperties, false); FComponentMaterialSet AllMaterialSet; TArray> MaterialRemap; MaterialRemap.SetNum(Targets.Num()); if (!CutProperties->bUseFirstMeshMaterials) { TMap KnownMaterials; for (int ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { const FComponentMaterialSet ComponentMaterialSet = UE::ToolTarget::GetMaterialSet(Targets[ComponentIdx]); for (UMaterialInterface* Mat : ComponentMaterialSet.Materials) { int* FoundMatIdx = KnownMaterials.Find(Mat); int MatIdx; if (FoundMatIdx) { MatIdx = *FoundMatIdx; } else { MatIdx = AllMaterialSet.Materials.Add(Mat); KnownMaterials.Add(Mat, MatIdx); } MaterialRemap[ComponentIdx].Add(MatIdx); } } } else { AllMaterialSet = UE::ToolTarget::GetMaterialSet(Targets[0]); for (int MatIdx = 0; MatIdx < AllMaterialSet.Materials.Num(); MatIdx++) { MaterialRemap[0].Add(MatIdx); } for (int ComponentIdx = 1; ComponentIdx < Targets.Num(); ComponentIdx++) { MaterialRemap[ComponentIdx].Init(0, Cast(Targets[ComponentIdx])->GetNumMaterials()); } } for (int ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { TSharedPtr Mesh = MakeShared(); *Mesh = UE::ToolTarget::GetDynamicMeshCopy(Targets[ComponentIdx]); // ensure materials and attributes are always enabled Mesh->EnableAttributes(); Mesh->Attributes()->EnableMaterialID(); FDynamicMeshMaterialAttribute* MaterialIDs = Mesh->Attributes()->GetMaterialID(); for (int TID : Mesh->TriangleIndicesItr()) { MaterialIDs->SetValue(TID, MaterialRemap[ComponentIdx][MaterialIDs->GetValue(TID)]); } if (ComponentIdx == 0) { OriginalTargetMesh = Mesh; } else { OriginalCuttingMesh = Mesh; } } Preview->ConfigureMaterials(AllMaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())); // check if we have the same mesh on both inputs if (Cast(Targets[0]) != nullptr && Cast(Targets[0])->HasSameSourceData(Targets[1])) { GetToolManager()->DisplayMessage( LOCTEXT("SameSourceError", "WARNING: Both input meshes have the same Asset; both inputs will be affected."), EToolMessageLevel::UserWarning); } } class FCutMeshWithMeshOp : public FDynamicMeshOperator { public: virtual ~FCutMeshWithMeshOp() {} TSharedPtr TargetMesh; FTransform TargetMeshTransform; TSharedPtr CuttingMesh; FTransform CuttingMeshTransform; bool bAttemptToFixHoles = true; bool bCollapseExtraEdges = true; double WindingThreshold = 0.5; virtual void CalculateResult(FProgressCancel* Progress) override { TUniquePtr SubtractOp = MakeUnique(); SubtractOp->CSGOperation = ECSGOperation::DifferenceAB; SubtractOp->bAttemptFixHoles = bAttemptToFixHoles; SubtractOp->bTryCollapseExtraEdges = bCollapseExtraEdges; SubtractOp->WindingThreshold = WindingThreshold; SubtractOp->Meshes.Add(TargetMesh); SubtractOp->Transforms.Add(TargetMeshTransform); SubtractOp->Meshes.Add(CuttingMesh); SubtractOp->Transforms.Add(CuttingMeshTransform); TUniquePtr IntersectOp = MakeUnique(); IntersectOp->CSGOperation = ECSGOperation::Intersect; IntersectOp->bAttemptFixHoles = bAttemptToFixHoles; IntersectOp->bTryCollapseExtraEdges = bCollapseExtraEdges; IntersectOp->WindingThreshold = WindingThreshold; IntersectOp->Meshes.Add(TargetMesh); IntersectOp->Transforms.Add(TargetMeshTransform); IntersectOp->Meshes.Add(CuttingMesh); IntersectOp->Transforms.Add(CuttingMeshTransform); TFuture SubtractFuture = Async(CutMeshWithMeshToolAsyncExecTarget, [&]() { SubtractOp->CalculateResult(Progress); }); TFuture IntersectFuture = Async(CutMeshWithMeshToolAsyncExecTarget, [&]() { IntersectOp->CalculateResult(Progress); }); SubtractFuture.Wait(); IntersectFuture.Wait(); this->ResultMesh = SubtractOp->ExtractResult(); SetResultTransform(SubtractOp->GetResultTransform()); IntersectMesh = IntersectOp->ExtractResult(); CreatedSubtractBoundaryEdges = SubtractOp->GetCreatedBoundaryEdges(); CreatedIntersectBoundaryEdges = IntersectOp->GetCreatedBoundaryEdges(); } TUniquePtr IntersectMesh; TArray CreatedSubtractBoundaryEdges; TArray CreatedIntersectBoundaryEdges; }; void UCutMeshWithMeshTool::SetPreviewCallbacks() { DrawnLineSet = NewObject(Preview->PreviewMesh->GetRootComponent()); DrawnLineSet->SetupAttachment(Preview->PreviewMesh->GetRootComponent()); DrawnLineSet->SetLineMaterial(ToolSetupUtil::GetDefaultLineComponentMaterial(GetToolManager())); DrawnLineSet->RegisterComponent(); Preview->OnOpCompleted.AddLambda( [this](const FDynamicMeshOperator* Op) { const FCutMeshWithMeshOp* CuttingOp = (const FCutMeshWithMeshOp*)(Op); CreatedSubtractBoundaryEdges = CuttingOp->CreatedSubtractBoundaryEdges; CreatedIntersectBoundaryEdges = CuttingOp->CreatedIntersectBoundaryEdges; IntersectionMesh = *CuttingOp->IntersectMesh; // cannot steal this here because it is const... IntersectPreviewMesh->UpdatePreview(&IntersectionMesh); IntersectPreviewMesh->SetTransform((FTransform)Op->GetResultTransform()); } ); Preview->OnMeshUpdated.AddLambda( [this](const UMeshOpPreviewWithBackgroundCompute*) { GetToolManager()->PostInvalidation(); UpdateVisualization(); } ); } void UCutMeshWithMeshTool::UpdateVisualization() { constexpr FColor BoundaryEdgeColor(240, 15, 15); constexpr float BoundaryEdgeThickness = 2.0; constexpr float BoundaryEdgeDepthBias = 2.0f; DrawnLineSet->Clear(); if (CutProperties->bShowNewBoundaries) { const FDynamicMesh3* TargetMesh = Preview->PreviewMesh->GetPreviewDynamicMesh(); FVector3d A, B; for (int EID : CreatedSubtractBoundaryEdges) { TargetMesh->GetEdgeV(EID, A, B); DrawnLineSet->AddLine((FVector)A, (FVector)B, BoundaryEdgeColor, BoundaryEdgeThickness, BoundaryEdgeDepthBias); } for (int EID : CreatedIntersectBoundaryEdges) { IntersectionMesh.GetEdgeV(EID, A, B); DrawnLineSet->AddLine((FVector)A, (FVector)B, BoundaryEdgeColor, BoundaryEdgeThickness, BoundaryEdgeDepthBias); } } } TUniquePtr UCutMeshWithMeshTool::MakeNewOperator() { TUniquePtr CuttingOp = MakeUnique(); CuttingOp->TargetMesh = OriginalTargetMesh; CuttingOp->TargetMeshTransform = TransformProxies[0]->GetTransform(); CuttingOp->CuttingMesh = OriginalCuttingMesh; CuttingOp->CuttingMeshTransform = TransformProxies[1]->GetTransform(); CuttingOp->bAttemptToFixHoles = CutProperties->bTryFixHoles; CuttingOp->bCollapseExtraEdges = CutProperties->bTryCollapseEdges; CuttingOp->WindingThreshold = CutProperties->WindingThreshold; return CuttingOp; } void UCutMeshWithMeshTool::OnPropertyModified(UObject* PropertySet, FProperty* Property) { if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCutMeshWithMeshToolProperties, bUseFirstMeshMaterials))) { if (!AreAllTargetsValid()) { GetToolManager()->DisplayMessage(LOCTEXT("InvalidTargets", "Input meshes are no longer valid"), EToolMessageLevel::UserWarning); return; } ConvertInputsAndSetPreviewMaterials(false); Preview->InvalidateResult(); } else if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCutMeshWithMeshToolProperties, bShowNewBoundaries))) { GetToolManager()->PostInvalidation(); UpdateVisualization(); } else { Super::OnPropertyModified(PropertySet, Property); } } FString UCutMeshWithMeshTool::GetCreatedAssetName() const { return TEXT("Boolean"); } FText UCutMeshWithMeshTool::GetActionName() const { return LOCTEXT("CutMeshWithMeshActionName", "Cut Mesh"); } void UCutMeshWithMeshTool::OnShutdown(EToolShutdownType ShutdownType) { SaveProperties(); HandleSourcesProperties->SaveProperties(this); TransformProperties->SaveProperties(this); FDynamicMeshOpResult OpResult = Preview->Shutdown(); // Restore (unhide) the source meshes for ( int32 ci = 0; ci < Targets.Num(); ++ci) { UE::ToolTarget::ShowSourceObject(Targets[ci]); } if (ShutdownType == EToolShutdownType::Accept) { GetToolManager()->BeginUndoTransaction(GetActionName()); TArray SelectActors; FComponentMaterialSet MaterialSet; MaterialSet.Materials = GetOutputMaterials(); // update subtract asset FTransform3d TargetToWorld = UE::ToolTarget::GetLocalToWorldTransform(Targets[0]); { if (OpResult.Mesh->TriangleCount() > 0) { MeshTransforms::ApplyTransform(*OpResult.Mesh, OpResult.Transform, true); MeshTransforms::ApplyTransformInverse(*OpResult.Mesh, TargetToWorld, true); UE::ToolTarget::CommitMeshDescriptionUpdateViaDynamicMesh(Targets[0], *OpResult.Mesh, true); Cast(Targets[0])->CommitMaterialSetUpdate(MaterialSet, true); } } SelectActors.Add(UE::ToolTarget::GetTargetActor(Targets[0])); // create intersection asset if ( IntersectionMesh.TriangleCount() > 0) { MeshTransforms::ApplyTransform(IntersectionMesh, OpResult.Transform, true); MeshTransforms::ApplyTransformInverse(IntersectionMesh, TargetToWorld, true); FTransform3d NewTransform = TargetToWorld; FString CurName = UE::Modeling::GetComponentAssetBaseName(UE::ToolTarget::GetTargetComponent(Targets[0])); FString UseBaseName = FString::Printf(TEXT("%s_%s"), *CurName, TEXT("CutPart") ); FCreateMeshObjectParams NewMeshObjectParams; NewMeshObjectParams.TargetWorld = GetTargetWorld(); NewMeshObjectParams.Transform = (FTransform)NewTransform; NewMeshObjectParams.BaseName = UseBaseName; NewMeshObjectParams.Materials = GetOutputMaterials(); NewMeshObjectParams.SetMesh(&IntersectionMesh); // note: CutMeshWithMeshTool does not support converting types currently UE::ToolTarget::ConfigureCreateMeshObjectParams(Targets[0], NewMeshObjectParams); FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams)); if (Result.IsOK() && Result.NewActor != nullptr) { SelectActors.Add(Result.NewActor); } } ToolSelectionUtil::SetNewActorSelection(GetToolManager(), SelectActors); GetToolManager()->EndUndoTransaction(); } UInteractiveGizmoManager* GizmoManager = GetToolManager()->GetPairedGizmoManager(); GizmoManager->DestroyAllGizmosByOwner(this); } #undef LOCTEXT_NAMESPACE