// Copyright Epic Games, Inc. All Rights Reserved. #include "ToolActivities/PolyEditInsertEdgeActivity.h" #include "BaseBehaviors/SingleClickBehavior.h" #include "BaseBehaviors/MouseHoverBehavior.h" #include "ContextObjectStore.h" #include "CuttingOps/GroupEdgeInsertionOp.h" #include "DynamicMesh/DynamicMeshChangeTracker.h" #include "InteractiveToolManager.h" #include "MeshOpPreviewHelpers.h" #include "Selection/PolygonSelectionMechanic.h" #include "ToolActivities/PolyEditActivityContext.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(PolyEditInsertEdgeActivity) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UPolyEditInsertEdgeActivity" namespace PolyEditInsertEdgeActivityLocals { bool GetSharedBoundary(const FGroupTopology& Topology, const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, int32 StartTopologyID, bool bStartIsCorner, const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint, int32 EndTopologyID, bool bEndIsCorner, int32& GroupIDOut, int32& BoundaryIndexOut); bool DoesBoundaryContainPoint(const FGroupTopology& Topology, const FGroupTopology::FGroupBoundary& Boundary, int32 PointTopologyID, bool bPointIsCorner); FText GroupEdgeStartTransactionName = LOCTEXT("GroupEdgeStartTransactionName", "Group Edge Start"); } TUniquePtr UPolyEditInsertEdgeActivity::MakeNewOperator() { TUniquePtr Op = MakeUnique(); Op->OriginalMesh = ComputeStartMesh; Op->OriginalTopology = ComputeStartTopology; Op->SetTransform(TargetTransform); if (Settings->InsertionMode == EGroupEdgeInsertionMode::PlaneCut) { Op->Mode = FGroupEdgeInserter::EInsertionMode::PlaneCut; } else { Op->Mode = FGroupEdgeInserter::EInsertionMode::Retriangulate; } Op->VertexTolerance = Settings->VertexTolerance; Op->StartPoint = StartPoint; Op->EndPoint = EndPoint; Op->CommonGroupID = CommonGroupID; Op->CommonBoundaryIndex = CommonBoundaryIndex; return Op; } void UPolyEditInsertEdgeActivity::Setup(UInteractiveTool* ParentToolIn) { Super::Setup(ParentToolIn); // Set up properties Settings = NewObject(this); Settings->RestoreProperties(ParentTool.Get()); AddToolPropertySource(Settings); SetToolPropertySourceEnabled(Settings, false); Settings->GetOnModified().AddUObject(this, &UPolyEditInsertEdgeActivity::OnPropertyModified); // These draw the group edges and the loops to be inserted ExistingEdgesRenderer.LineColor = FLinearColor::Red; ExistingEdgesRenderer.LineThickness = 2.0; PreviewEdgeRenderer.LineColor = FLinearColor::Green; PreviewEdgeRenderer.LineThickness = 4.0; PreviewEdgeRenderer.PointColor = FLinearColor::Green; PreviewEdgeRenderer.PointSize = 8.0; PreviewEdgeRenderer.bDepthTested = false; // Set up the topology selector settings TopologySelectorSettings.bEnableEdgeHits = true; TopologySelectorSettings.bEnableCornerHits = true; TopologySelectorSettings.bEnableFaceHits = false; // Set up our input routing USingleClickInputBehavior* ClickBehavior = NewObject(); ClickBehavior->Initialize(this); ParentTool->AddInputBehavior(ClickBehavior); UMouseHoverBehavior* HoverBehavior = NewObject(); HoverBehavior->Initialize(this); ParentTool->AddInputBehavior(HoverBehavior); ActivityContext = ParentTool->GetToolManager()->GetContextObjectStore()->FindContext(); // TODO: When the deprecated function GetTopologySelector is removed from UPolygonSelectionMechanic, we can remove the UMeshTopologySelectionMechanic:: prefix here TopologySelector = ActivityContext->SelectionMechanic->UMeshTopologySelectionMechanic::GetTopologySelector(); ActivityContext->OnUndoRedo.AddWeakLambda(this, [this](bool bGroupTopologyModified) { UpdateComputeInputs(); ToolState = EState::GettingStart; ClearPreview(true); }); } void UPolyEditInsertEdgeActivity::UpdateComputeInputs() { ComputeStartMesh = MakeShared(*ActivityContext->CurrentMesh); TSharedPtr NonConstTopology = MakeShared(*ActivityContext->CurrentTopology); NonConstTopology->RetargetOnClonedMesh(ComputeStartMesh.Get()); ComputeStartTopology = NonConstTopology; } void UPolyEditInsertEdgeActivity::Shutdown(EToolShutdownType ShutdownType) { if (bIsRunning) { End(ShutdownType); } Settings->SaveProperties(ParentTool.Get()); Settings->GetOnModified().Clear(); TopologySelector.Reset(); Settings = nullptr; ActivityContext->OnUndoRedo.RemoveAll(this); ActivityContext = nullptr; Super::Shutdown(ShutdownType); } EToolActivityStartResult UPolyEditInsertEdgeActivity::Start() { ParentTool->GetToolManager()->DisplayMessage( LOCTEXT("InsertEdgeActivityDescription", "Click two points on the boundary of a face to " "insert a new edge between the points and split the face."), EToolMessageLevel::UserNotification); SetToolPropertySourceEnabled(Settings, true); // We don't use selection, so clear it if necessary (have to issue an undo/redo event) if (!ActivityContext->SelectionMechanic->GetActiveSelection().IsEmpty()) { ActivityContext->SelectionMechanic->BeginChange(); ActivityContext->SelectionMechanic->ClearSelection(); ParentTool->GetToolManager()->EmitObjectChange(ActivityContext->SelectionMechanic, ActivityContext->SelectionMechanic->EndChange(), LOCTEXT("ClearSelection", "Clear Selection")); } TargetTransform = ActivityContext->Preview->PreviewMesh->GetTransform(); UpdateComputeInputs(); SetupPreview(); ToolState = EState::GettingStart; bLastComputeSucceeded = false; bIsRunning = true; // Emit activity start transaction ActivityContext->EmitActivityStart(LOCTEXT("BeginInsertEdgeActivity", "Begin Insert Edge")); return EToolActivityStartResult::Running; } EToolActivityEndResult UPolyEditInsertEdgeActivity::End(EToolShutdownType ShutdownType) { if (!bIsRunning) { return EToolActivityEndResult::ErrorDuringEnd; } SetToolPropertySourceEnabled(Settings, false); Settings->GetOnModified().Clear(); ActivityContext->Preview->OnOpCompleted.RemoveAll(this); ActivityContext->Preview->OnMeshUpdated.RemoveAll(this); ToolState = EState::GettingStart; // Note that this does an ensure on us not being in a ToolState that requires the preview points, hence // we do this after resetting ToolState. ClearPreview(true); ActivityContext->Preview->ClearOpFactory(); LatestOpTopologyResult.Reset(); LatestOpChangedTids.Reset(); bIsRunning = false; return CanAccept() ? EToolActivityEndResult::Completed : EToolActivityEndResult::Cancelled; } void UPolyEditInsertEdgeActivity::SetupPreview() { ActivityContext->Preview->ChangeOpFactory(this); // Whenever we get a new result from the op, we need to extract the preview edges so that // we can draw them if we want to. ActivityContext->Preview->OnOpCompleted.AddWeakLambda(this, [this](const FDynamicMeshOperator* UncastOp) { const FGroupEdgeInsertionOp* Op = static_cast(UncastOp); LatestOpTopologyResult.Reset(); LatestOpChangedTids.Reset(); PreviewEdges.Reset(); // See if this compute is actually outdated, i.e. we changed the mesh // out from under it. if (Op->OriginalMesh != ComputeStartMesh) { bLastComputeSucceeded = false; return; } bLastComputeSucceeded = Op->bSucceeded; if (bLastComputeSucceeded) { Op->GetEdgeLocations(PreviewEdges); LatestOpTopologyResult = Op->ResultTopology; LatestOpChangedTids = Op->ChangedTids; } }); // In case of failure, we want to hide the broken preview, since we wouldn't accept it on // a click. Note that this can't be fired OnOpCompleted because the preview is updated // with the op result after that callback, which would undo the reset. The preview edge // extraction can't be lumped in here because it needs the op rather than the preview object. ActivityContext->Preview->OnMeshUpdated.AddWeakLambda(this, [this](UMeshOpPreviewWithBackgroundCompute*) { if (!bLastComputeSucceeded) { ActivityContext->Preview->PreviewMesh->UpdatePreview(ActivityContext->CurrentMesh.Get()); } }); } bool UPolyEditInsertEdgeActivity::CanStart() const { return true; } void UPolyEditInsertEdgeActivity::Tick(float DeltaTime) { if (ToolState == EState::WaitingForInsertComplete && ActivityContext->Preview->HaveValidResult()) { if (bLastComputeSucceeded) { FDynamicMeshChangeTracker ChangeTracker(ActivityContext->CurrentMesh.Get()); ChangeTracker.BeginChange(); ChangeTracker.SaveTriangles(*LatestOpChangedTids, true /*bSaveVertices*/); // Update current mesh ActivityContext->CurrentMesh->Copy(*ActivityContext->Preview->PreviewMesh->GetMesh(), true, true, true, true); *ActivityContext->CurrentTopology = *LatestOpTopologyResult; ActivityContext->CurrentTopology->RetargetOnClonedMesh(ActivityContext->CurrentMesh.Get()); UpdateComputeInputs(); // Emit transaction FGroupTopologySelection EmptySelection; ActivityContext->EmitCurrentMeshChangeAndUpdate(LOCTEXT("EdgeInsertionTransactionName", "Edge Insertion"), ChangeTracker.EndChange(), EmptySelection); ToolState = EState::GettingStart; if (Settings->bContinuousInsertion) { // If continuous insertion is enabled, try to find information associated with new start point. // If found, remain in GettingEnd mode. FVector3d PreviewPoint; if (GetHoveredItem(LastEndPointWorldRay, StartPoint, StartTopologyID, bStartIsCorner, PreviewPoint)) { PreviewPoints.Reset(); PreviewPoints.Add(PreviewPoint); ToolState = EState::GettingEnd; ParentTool->GetToolManager()->EmitObjectChange(this, MakeUnique(CurrentChangeStamp), PolyEditInsertEdgeActivityLocals::GroupEdgeStartTransactionName); } } } else { ToolState = PreviewPoints.Num() > 0 ? EState::GettingEnd : EState::GettingStart; } PreviewEdges.Reset(); } } void UPolyEditInsertEdgeActivity::Render(IToolsContextRenderAPI* RenderAPI) { ParentTool->GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState); // Draw the existing group edges FViewCameraState RenderCameraState = RenderAPI->GetCameraState(); ExistingEdgesRenderer.BeginFrame(RenderAPI, RenderCameraState); ExistingEdgesRenderer.SetTransform(TargetTransform); for (const FGroupTopology::FGroupEdge& Edge : ActivityContext->CurrentTopology->Edges) { FVector3d A, B; for (int32 eid : Edge.Span.Edges) { ActivityContext->CurrentMesh->GetEdgeV(eid, A, B); ExistingEdgesRenderer.DrawLine(A, B); } } ExistingEdgesRenderer.EndFrame(); // Draw the preview edges and points PreviewEdgeRenderer.BeginFrame(RenderAPI, RenderCameraState); PreviewEdgeRenderer.SetTransform(TargetTransform); for (const TPair& EdgeVerts : PreviewEdges) { PreviewEdgeRenderer.DrawLine(EdgeVerts.Key, EdgeVerts.Value); } for (const FVector3d& Point : PreviewPoints) { PreviewEdgeRenderer.DrawPoint(Point); } PreviewEdgeRenderer.EndFrame(); } bool UPolyEditInsertEdgeActivity::CanAccept() const { return ToolState != EState::WaitingForInsertComplete; } void UPolyEditInsertEdgeActivity::OnPropertyModified(UObject* PropertySet, FProperty* Property) { PreviewEdges.Reset(); // Don't clear drawn elements because we may be getting the second endpoint, and still // need to keep the first. ClearPreview(false); } void UPolyEditInsertEdgeActivity::ClearPreview(bool bClearDrawnElements) { ActivityContext->Preview->CancelCompute(); ActivityContext->Preview->PreviewMesh->UpdatePreview(ActivityContext->CurrentMesh.Get()); bShowingBaseMesh = true; if (bClearDrawnElements) { PreviewEdges.Reset(); PreviewPoints.Reset(); // If we're removing the start point, we shouldn't be in a state that requires // it, i.e. getting the second point. if (!ensure(ToolState != EState::GettingEnd)) { ToolState = EState::GettingStart; } } } /** * Update the preview unless we've already computed one with the same parameters (such as when snapping to * the same vertex despite moving the mouse). */ void UPolyEditInsertEdgeActivity::ConditionallyUpdatePreview( const FGroupEdgeInserter::FGroupEdgeSplitPoint& NewEndPoint, int32 NewEndTopologyID, bool bNewEndIsCorner, int32 NewCommonGroupID, int32 NewBoundaryIndex) { if (bShowingBaseMesh || bEndIsCorner != bNewEndIsCorner || EndTopologyID != NewEndTopologyID || EndPoint.bIsVertex != NewEndPoint.bIsVertex || EndPoint.ElementID != NewEndPoint.ElementID || (!NewEndPoint.bIsVertex && NewEndPoint.EdgeTValue != EndPoint.EdgeTValue) || CommonGroupID != NewCommonGroupID || CommonBoundaryIndex != NewBoundaryIndex) { // Update the end variables, since they are apparently different EndPoint = NewEndPoint; EndTopologyID = NewEndTopologyID; bEndIsCorner = bNewEndIsCorner; CommonGroupID = NewCommonGroupID; CommonBoundaryIndex = NewBoundaryIndex; // If either endpoint is a corner, we need to calculate its tangent. This will differ based on which // boundary it is a part of. if (bStartIsCorner) { GetCornerTangent(StartTopologyID, CommonGroupID, CommonBoundaryIndex, StartPoint.Tangent); } if (bEndIsCorner) { GetCornerTangent(EndTopologyID, CommonGroupID, CommonBoundaryIndex, EndPoint.Tangent); } bShowingBaseMesh = false; PreviewEdges.Reset(); ActivityContext->Preview->InvalidateResult(); } } FInputRayHit UPolyEditInsertEdgeActivity::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos) { FInputRayHit Hit; // Early out if the activity is not running. This is actually important because the behavior is // always in the behavior list while the tool is running (we don't have a way to add/remove at // will). if (!bIsRunning) { return Hit; // Hit.bHit is false } switch (ToolState) { case EState::WaitingForInsertComplete: break; // Keep hit invalid case EState::GettingStart: { PreviewPoints.Reset(); FVector3d RayPoint; if (TopologyHitTest(PressPos.WorldRay, RayPoint)) { Hit = FInputRayHit(PressPos.WorldRay.GetParameter((FVector)RayPoint)); } break; } case EState::GettingEnd: { FVector3d RayPoint; FRay3d LocalRay; if (TopologyHitTest(PressPos.WorldRay, RayPoint, &LocalRay)) { Hit = FInputRayHit(PressPos.WorldRay.GetParameter((FVector)RayPoint)); } else { // If we don't hit a valid element, we still do a hover if we hit the mesh. // We still do the topology check in the first place because it accepts missing // rays that are close enough to snap. double RayT = 0; int32 Tid = FDynamicMesh3::InvalidID; if (ActivityContext->MeshSpatial->FindNearestHitTriangle(LocalRay, RayT, Tid)) { Hit = FInputRayHit(RayT); } } break; } } return Hit; } bool UPolyEditInsertEdgeActivity::OnUpdateHover(const FInputDeviceRay& DevicePos) { using namespace PolyEditInsertEdgeActivityLocals; if (!bIsRunning) { return false; } switch (ToolState) { case EState::WaitingForInsertComplete: return false; // Do nothing. case EState::GettingStart: { // Update start variables and show a preview of a point if it's on an edge or corner PreviewPoints.Reset(); FVector3d PreviewPoint; if (GetHoveredItem(DevicePos.WorldRay, StartPoint, StartTopologyID, bStartIsCorner, PreviewPoint)) { PreviewPoints.Add(PreviewPoint); return true; } return false; } case EState::GettingEnd: { // This shouldn't happen- if it does, we messed up with our state handling somewhere. But don't crash, // just reset. if (!ensure(PreviewPoints.Num() > 0)) { ToolState = EState::GettingStart; ClearPreview(true); return false; } PreviewPoints.SetNum(1); // Keep the first element, which is the start point // Don't update the end variables right away so that we can check if they actually changed (they // won't when we snap to the same corner as before). FGroupEdgeInserter::FGroupEdgeSplitPoint SnappedPoint; int32 PointTopologyID, GroupID, BoundaryIndex; bool bPointIsCorner; FVector3d PreviewPoint; FRay3d LocalRay; if (GetHoveredItem(DevicePos.WorldRay, SnappedPoint, PointTopologyID, bPointIsCorner, PreviewPoint, &LocalRay)) { // See if the point is not on the same vertex/edge but is on the same boundary if (!(SnappedPoint.bIsVertex == StartPoint.bIsVertex && SnappedPoint.ElementID == StartPoint.ElementID) && GetSharedBoundary(*ActivityContext->CurrentTopology, StartPoint, StartTopologyID, bStartIsCorner, SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex)) { ConditionallyUpdatePreview(SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex); } else { PreviewEdges.Reset(); // TODO: Maybe we should show a different color edge on a fail, rather than hiding it? } PreviewPoints.Add(PreviewPoint); return true; } // If we're here, then we don't have a valid endpoint. Make sure our preview is reset and draw a line // to the current hit location. if (!bShowingBaseMesh) { ClearPreview(false); } PreviewEdges.Reset(); double RayT = 0; int32 Tid = FDynamicMesh3::InvalidID; if (ActivityContext->MeshSpatial->FindNearestHitTriangle(LocalRay, RayT, Tid)) { PreviewEdges.Emplace(PreviewPoints[0], LocalRay.PointAt(RayT)); return true; } return false; } } check(false); // Each case has its own return, so shouldn't get here return false; } void UPolyEditInsertEdgeActivity::OnEndHover() { if (!bIsRunning) { return; } switch (ToolState) { case EState::WaitingForInsertComplete: case EState::GettingStart: ClearPreview(true); break; case EState::GettingEnd: // Keep the first preview point. ClearPreview(false); PreviewPoints.SetNum(1); PreviewEdges.Reset(); } } FInputRayHit UPolyEditInsertEdgeActivity::IsHitByClick(const FInputDeviceRay& ClickPos) { FInputRayHit Hit; // Early out if the activity is not running. if (!bIsRunning) { return Hit; // Hit.bHit is false } switch (ToolState) { case EState::WaitingForInsertComplete: break; // Keep hit invalid // Same requirement for the other two cases: the click should go on an edge case EState::GettingStart: case EState::GettingEnd: { FVector3d RayPoint; if (TopologyHitTest(ClickPos.WorldRay, RayPoint)) { Hit = FInputRayHit(ClickPos.WorldRay.GetParameter((FVector)RayPoint)); } break; } } return Hit; } void UPolyEditInsertEdgeActivity::OnClicked(const FInputDeviceRay& ClickPos) { using namespace PolyEditInsertEdgeActivityLocals; switch (ToolState) { case EState::WaitingForInsertComplete: break; // Do nothing case EState::GettingStart: { // Update start variables and switch state if successful FVector3d PreviewPoint; if (GetHoveredItem(ClickPos.WorldRay, StartPoint, StartTopologyID, bStartIsCorner, PreviewPoint)) { PreviewPoints.Reset(); PreviewPoints.Add(PreviewPoint); ToolState = EState::GettingEnd; ParentTool->GetToolManager()->BeginUndoTransaction(PolyEditInsertEdgeActivityLocals::GroupEdgeStartTransactionName); ParentTool->GetToolManager()->EmitObjectChange(this, MakeUnique(CurrentChangeStamp), PolyEditInsertEdgeActivityLocals::GroupEdgeStartTransactionName); ParentTool->GetToolManager()->EndUndoTransaction(); } break; } case EState::GettingEnd: { // Don't update the end variables right away so that we can check if they actually changed (they // won't when we snap to the same corner as before). FVector3d PreviewPoint; FGroupEdgeInserter::FGroupEdgeSplitPoint SnappedPoint; int32 PointTopologyID, GroupID, BoundaryIndex; bool bPointIsCorner; if (GetHoveredItem(ClickPos.WorldRay, SnappedPoint, PointTopologyID, bPointIsCorner, PreviewPoint)) { // See if the point is not on the same vertex/edge but is on the same boundary if (!(SnappedPoint.bIsVertex == StartPoint.bIsVertex && SnappedPoint.ElementID == StartPoint.ElementID) && GetSharedBoundary(*ActivityContext->CurrentTopology, StartPoint, StartTopologyID, bStartIsCorner, SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex)) { ConditionallyUpdatePreview(SnappedPoint, PointTopologyID, bPointIsCorner, GroupID, BoundaryIndex); ToolState = EState::WaitingForInsertComplete; } else { ClearPreview(false); } } LastEndPointWorldRay = ClickPos.WorldRay; break; } } } bool UPolyEditInsertEdgeActivity::TopologyHitTest(const FRay& WorldRay, FVector3d& RayPositionOut, FRay3d* LocalRayOut) { FRay3d LocalRay((FVector3d)TargetTransform.InverseTransformPosition(WorldRay.Origin), (FVector3d)TargetTransform.InverseTransformVector(WorldRay.Direction), false); if (LocalRayOut) { *LocalRayOut = LocalRay; } FGroupTopologySelection Selection; FVector3d Position, Normal; TopologySelectorSettings.bHitBackFaces = ActivityContext->SelectionMechanic->Properties->bHitBackFaces; if (TopologySelector->FindSelectedElement(TopologySelectorSettings, LocalRay, Selection, Position, Normal)) { RayPositionOut = TargetTransform.TransformPosition(Position); return true; } return false; } bool UPolyEditInsertEdgeActivity::GetHoveredItem(const FRay& WorldRay, FGroupEdgeInserter::FGroupEdgeSplitPoint& PointOut, int32& TopologyElementIDOut, bool& bIsCornerOut, FVector3d& PositionOut, FRay3d* LocalRayOut) { TopologyElementIDOut = FDynamicMesh3::InvalidID; PointOut.ElementID = FDynamicMesh3::InvalidID; // Cast the ray to see what we hit. FRay3d LocalRay((FVector3d)TargetTransform.InverseTransformPosition(WorldRay.Origin), (FVector3d)TargetTransform.InverseTransformVector(WorldRay.Direction), false); if (LocalRayOut) { *LocalRayOut = LocalRay; } FGroupTopologySelection Selection; FVector3d Position, Normal; int32 EdgeSegmentID; TopologySelectorSettings.bHitBackFaces = ActivityContext->SelectionMechanic->Properties->bHitBackFaces; if (!TopologySelector->FindSelectedElement( TopologySelectorSettings, LocalRay, Selection, Position, Normal, &EdgeSegmentID)) { return false; // Didn't hit anything } else if (Selection.SelectedCornerIDs.Num() > 0) { // Point is a corner TopologyElementIDOut = Selection.GetASelectedCornerID(); bIsCornerOut = true; PointOut.bIsVertex = true; PointOut.ElementID = ActivityContext->CurrentTopology->GetCornerVertexID(TopologyElementIDOut); // We can't initialize the tangent yet because the tangent of a corner will // depend on which boundary it is a part of. PositionOut = ActivityContext->CurrentMesh->GetVertex(PointOut.ElementID); } else { // Point is an edge. We'll need to calculate the t value and some other things. check(Selection.SelectedEdgeIDs.Num() > 0); TopologyElementIDOut = Selection.GetASelectedEdgeID(); bIsCornerOut = false; const FGroupTopology::FGroupEdge& GroupEdge = ActivityContext->CurrentTopology->Edges[TopologyElementIDOut]; int32 Eid = GroupEdge.Span.Edges[EdgeSegmentID]; int32 StartVid = GroupEdge.Span.Vertices[EdgeSegmentID]; int32 EndVid = GroupEdge.Span.Vertices[EdgeSegmentID + 1]; FVector3d StartVert = ActivityContext->CurrentMesh->GetVertex(StartVid); FVector3d EndVert = ActivityContext->CurrentMesh->GetVertex(EndVid); FVector3d EdgeVector = EndVert - StartVert; double EdgeLength = EdgeVector.Length(); check(EdgeLength > 0); PointOut.Tangent = EdgeVector / EdgeLength; FRay EdgeRay((FVector)StartVert, (FVector)PointOut.Tangent, true); float DistDownEdge = EdgeRay.GetParameter((FVector)Position); // See if the point is at a vertex in the group edge span. if (DistDownEdge <= Settings->VertexTolerance) { PointOut.bIsVertex = true; PointOut.ElementID = StartVid; PositionOut = StartVert; if (EdgeSegmentID > 0) { // Average with previous normalized edge vector PointOut.Tangent += UE::Geometry::Normalized(StartVert - ActivityContext->CurrentMesh->GetVertex(GroupEdge.Span.Vertices[EdgeSegmentID - 1])); UE::Geometry::Normalize(PointOut.Tangent); } } else if (abs(DistDownEdge - EdgeLength) <= Settings->VertexTolerance) { PointOut.bIsVertex = true; PointOut.ElementID = EndVid; PositionOut = EndVert; if (EdgeSegmentID + 2 < GroupEdge.Span.Vertices.Num()) { PointOut.Tangent += UE::Geometry::Normalized( ActivityContext->CurrentMesh->GetVertex(GroupEdge.Span.Vertices[EdgeSegmentID + 2]) - EndVert); UE::Geometry::Normalize(PointOut.Tangent); } } else { PointOut.bIsVertex = false; PointOut.ElementID = Eid; PointOut.EdgeTValue = DistDownEdge / EdgeLength; PositionOut = (FVector3d)EdgeRay.PointAt(DistDownEdge); if (ActivityContext->CurrentMesh->GetEdgeV(Eid).A != StartVid) { PointOut.EdgeTValue = 1 - PointOut.EdgeTValue; } } } return true; } void UPolyEditInsertEdgeActivity::GetCornerTangent(int32 CornerID, int32 GroupID, int32 BoundaryIndex, FVector3d& TangentOut) { TangentOut = FVector3d::Zero(); int32 CornerVid = ActivityContext->CurrentTopology->GetCornerVertexID(CornerID); check(CornerVid != FDynamicMesh3::InvalidID); const FGroupTopology::FGroup* Group = ActivityContext->CurrentTopology->FindGroupByID(GroupID); check(Group && BoundaryIndex >= 0 && BoundaryIndex < Group->Boundaries.Num()); const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[BoundaryIndex]; TArray AdjacentPoints; for (int32 GroupEdgeID : Boundary.GroupEdges) { TArray Vertices = ActivityContext->CurrentTopology->Edges[GroupEdgeID].Span.Vertices; if (Vertices[0] == CornerVid) { AdjacentPoints.Add(ActivityContext->CurrentMesh->GetVertex(Vertices[1])); } else if (Vertices.Last() == CornerVid) { AdjacentPoints.Add(ActivityContext->CurrentMesh->GetVertex(Vertices[Vertices.Num()-2])); } } check(AdjacentPoints.Num() == 2); FVector3d CornerPosition = ActivityContext->CurrentMesh->GetVertex(CornerVid); TangentOut = UE::Geometry::Normalized(CornerPosition - AdjacentPoints[0]); TangentOut += UE::Geometry::Normalized(AdjacentPoints[1] - CornerPosition); UE::Geometry::Normalize(TangentOut); } bool PolyEditInsertEdgeActivityLocals::GetSharedBoundary(const FGroupTopology& Topology, const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, int32 StartTopologyID, bool bStartIsCorner, const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint, int32 EndTopologyID, bool bEndIsCorner, int32& GroupIDOut, int32& BoundaryIndexOut) { // The start and endpoints could be on the same boundary of multiple groups at // the same time, and sometimes we won't be able to resolve the ambiguity // (one example is a sphere split into two equal groups, but could even happen // with more than two groups when endpoints are corners). // Sometimes there are things we can do to eliminate some contenders- the best // approach is probably trying to do a plane cut for all of the options and // removing those that fail. However, it's worth noting that such issues won't // arise in the standard application of this tool for low-poly modeling, where // groups are planar, so it's not worth the bother. // Instead, we'll just take one of the results arbitrarily, though we will try to // take one that has a single boundary (this will prefer a cylinder cap over // a cylinder side). // TODO: The code would be simpler if we didn't even want to do that filtering- we'd // just return the first result we found. Should we consider doing that? GroupIDOut = FDynamicMesh3::InvalidID; BoundaryIndexOut = FDynamicMesh3::InvalidID; TArray> CandidateGroupIDsAndBoundaryIndices; if (bStartIsCorner) { // Go through all neighboring groups and their boundaries to find a shared one. const FGroupTopology::FCorner& StartCorner = Topology.Corners[StartTopologyID]; for (int32 GroupID : StartCorner.NeighbourGroupIDs) { const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID); for (int32 i = 0; i < Group->Boundaries.Num(); ++i) { const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i]; if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner) && DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner)) { CandidateGroupIDsAndBoundaryIndices.Emplace(GroupID, i); break; // Can't share more than one boundary in the same group } } } } else { // Start is on an edge, so there are fewer boundaries to look through. const FGroupTopology::FGroupEdge& GroupEdge = Topology.Edges[StartTopologyID]; const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupEdge.Groups.A); for (int32 i = 0; i < Group->Boundaries.Num(); ++i) { const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i]; if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner) && DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner)) { CandidateGroupIDsAndBoundaryIndices.Emplace(GroupEdge.Groups.A, i); break; } } if (GroupEdge.Groups.B != FDynamicMesh3::InvalidID) { Group = Topology.FindGroupByID(GroupEdge.Groups.B); for (int32 i = 0; i < Group->Boundaries.Num(); ++i) { const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i]; if (DoesBoundaryContainPoint(Topology, Boundary, EndTopologyID, bEndIsCorner) && DoesBoundaryContainPoint(Topology, Boundary, StartTopologyID, bStartIsCorner)) { CandidateGroupIDsAndBoundaryIndices.Emplace(GroupEdge.Groups.B, i); break; } } } } if (CandidateGroupIDsAndBoundaryIndices.Num() == 0) { return false; } // Prefer a result that has a single boundary if there are multiple. if (CandidateGroupIDsAndBoundaryIndices.Num() > 1) { for (const TPair& GroupIDBoundaryIdxPair : CandidateGroupIDsAndBoundaryIndices) { if (Topology.FindGroupByID(GroupIDBoundaryIdxPair.Key)->Boundaries.Num() == 1) { GroupIDOut = GroupIDBoundaryIdxPair.Key; BoundaryIndexOut = 0; return true; } } } GroupIDOut = CandidateGroupIDsAndBoundaryIndices[0].Key; BoundaryIndexOut = CandidateGroupIDsAndBoundaryIndices[0].Value; return true; } bool PolyEditInsertEdgeActivityLocals::DoesBoundaryContainPoint(const FGroupTopology& Topology, const FGroupTopology::FGroupBoundary& Boundary, int32 PointTopologyID, bool bPointIsCorner) { for (int32 GroupEdgeID : Boundary.GroupEdges) { if (!bPointIsCorner && GroupEdgeID == PointTopologyID) { return true; } const FGroupTopology::FGroupEdge& GroupEdge = Topology.Edges[GroupEdgeID]; if (bPointIsCorner && (GroupEdge.EndpointCorners.A == PointTopologyID || GroupEdge.EndpointCorners.B == PointTopologyID)) { return true; } } return false; } void FGroupEdgeInsertionFirstPointChange::Revert(UObject* Object) { UPolyEditInsertEdgeActivity* Activity = Cast(Object); check(Activity->ToolState == UPolyEditInsertEdgeActivity::EState::GettingEnd || Activity->ToolState == UPolyEditInsertEdgeActivity::EState::WaitingForInsertComplete); Activity->ToolState = UPolyEditInsertEdgeActivity::EState::GettingStart; Activity->ClearPreview(true); bHaveDoneUndo = true; } #undef LOCTEXT_NAMESPACE