Files
UnrealEngine/Engine/Plugins/Runtime/GeometryProcessing/Source/DynamicMesh/Private/Operations/GroupEdgeInserter.cpp
2025-05-18 13:04:45 +08:00

1649 lines
59 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Operations/GroupEdgeInserter.h"
#include "Algo/ForEach.h"
#include "CompGeom/PolygonTriangulation.h"
#include "ConstrainedDelaunay2.h"
#include "DynamicMesh/DynamicMeshChangeTracker.h"
#include "DynamicMeshEditor.h"
#include "FrameTypes.h"
#include "DynamicMesh/MeshIndexUtil.h"
#include "MeshRegionBoundaryLoops.h"
#include "Operations/GroupEdgeInserter.h"
#include "Operations/MeshPlaneCut.h"
#include "Operations/EmbedSurfacePath.h"
#include "Operations/SimpleHoleFiller.h"
#include "Operations/LocalPlanarSimplify.h"
#include "Selections/MeshConnectedComponents.h"
#include "Util/ProgressCancel.h"
#include "Util/IndexUtil.h"
using namespace UE::Geometry;
// Forward declarations of local helper functions. Normally these would be marked as static or
// in an anonymous namespace, but apparently this could still result in clashes due to unity builds.
namespace GroupEdgeInserterLocals {
bool GetEdgeLoopOpposingEdgeAndCorner(const FGroupTopology& Topology, int32 GroupID, int32 GroupEdgeIDIn,
int32 CornerIDIn, int32& GroupEdgeIDOut, int32& CornerIDOut, int32& BoundaryIndexOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut);
bool InsertEdgeLoopEdgesInDirection(const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartEndpoints,
int32 NextGroupID, int32 NextEdgeID, int32 NextCornerID, int32 NextBoundaryID,
TSet<int32>& AlteredGroups, int32& NumInserted, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress);
bool InsertNewVertexEndpoints(
const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params,
int32 GroupEdgeID, int32 StartCornerID,
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPointsOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut);
void ConvertProportionsToArcLengths(
const FGroupTopology& Topology, int32 GroupEdgeID,
const TArray<double>& ProportionsIn,
TArray<double>& ArcLengthsOut, TArray<double>* PerVertexLengthsOut);
bool ConnectEndpoints(
const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
int32& NumGroupsCreated, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress);
bool ConnectMultipleUsingRetriangulation(
FDynamicMesh3& Mesh, const FGroupTopology& Topology, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray <FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
int32& NumGroupsCreated, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress);
bool DeleteGroupTrianglesAndGetLoop(FDynamicMesh3& Mesh, const FGroupTopology& Topology, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary, TArray<int32>& BoundaryVerticesOut,
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>>& BoundaryVidUVMapsOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress);
void AppendInclusiveRangeWrapAround(const TArray<int32>& InputArray, TArray<int32>& OutputArray,
int32 StartIndex, int32 InclusiveEndIndex);
bool RetriangulateLoop(FDynamicMesh3& Mesh, const TArray<int32>& LoopVertices,
int32 NewGroupID, TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>>& VidUVMaps);
bool ConnectMultipleUsingPlaneCut(FDynamicMesh3& Mesh,
const FGroupTopology& Topology, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray <FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
double VertexTolerance, bool bSimplifyAlongPath, int32& NumGroupsCreated,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress);
bool EmbedPlaneCutPath(FDynamicMesh3& Mesh, const FGroupTopology& Topology, int32 GroupID,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
double VertexTolerance, bool bSimplifyAlongPath, TSet<int32>& PathEidsOut,
TSet<int32>* ChangedTrisOut, FProgressCancel* Progress);
bool CreateNewGroups(FDynamicMesh3& Mesh, TSet<int32>& PathEids, int32 OriginalGroupID, int32& NumGroupsCreated,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress);
bool GetPlaneCutPath(const FDynamicMesh3& Mesh, int32 GroupID,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
TArray<TPair<FMeshSurfacePoint, int>>& OutputPath, double VertexCutTolerance,
const TSet<int32>& DisallowedVids, FProgressCancel* Progress);
bool InsertSingleWithRetriangulation(FDynamicMesh3& Mesh, FGroupTopology& Topology,
int32 GroupID, int32 BoundaryIndex,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress);
}
/** Inserts an edge loop into a mesh, where an edge loop is a sequence of (group) edges across quads. */
bool FGroupEdgeInserter::InsertEdgeLoops(const FEdgeLoopInsertionParams& Params, FOptionalOutputParams OptionalOut, FProgressCancel* Progress)
{
using namespace GroupEdgeInserterLocals;
if (Progress && Progress->Cancelled())
{
return false;
}
// Validate the inputs
check(Params.Mesh);
check(Params.Topology);
check(Params.SortedInputLengths);
check(Params.GroupEdgeID != FDynamicMesh3::InvalidID);
check(Params.StartCornerID != FDynamicMesh3::InvalidID);
const FGroupTopology::FGroupEdge& GroupEdge = Params.Topology->Edges[Params.GroupEdgeID];
// We check whether we have a valid path forward or backward first, because we don't want
// to do any edge splits if we have neither.
int32 ForwardGroupID = GroupEdge.Groups.A;
int32 ForwardEdgeID, ForwardCornerID, ForwardBoundaryIndex;
bool bHaveForwardEdge = GetEdgeLoopOpposingEdgeAndCorner(*Params.Topology, ForwardGroupID,
Params.GroupEdgeID, Params.StartCornerID, ForwardEdgeID, ForwardCornerID, ForwardBoundaryIndex, OptionalOut);
int32 BackwardGroupID = GroupEdge.Groups.B;
int32 BackwardEdgeID, BackwardCornerID, BackwardBoundaryIndex;
bool bHaveBackwardEdge = GetEdgeLoopOpposingEdgeAndCorner(*Params.Topology, BackwardGroupID,
Params.GroupEdgeID, Params.StartCornerID, BackwardEdgeID, BackwardCornerID, BackwardBoundaryIndex, OptionalOut);
if (!bHaveForwardEdge && !bHaveBackwardEdge)
{
// Neither of the neighbors is quad-like, so we can't insert an edge loop at this edge.
return false;
}
// Depending on the topology, it is possible for our loop to attempt to cross itself from the side. We
// could support this case, because the "loop" will still eventually end. However, this isn't a particularly
// useful feature, so for now, we'll keep things a cleaner by ending the loop if we arrive at a group that
// we've already altered. This also allows us to avoid updating the topology as we go along.
TSet<int32> AlteredGroups;
// We will need to keep the first endpoints around in case we use them to close the loop.
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint> StartEndpoints;
// Although we have code that can insert new edges at edge endpoints, it is cleaner to do splits for all the loops
// down an edge ahead of time to make vertex endpoints, in part because if we don't, a split can change the eid
// of the next endpoint.
bool bSuccess = InsertNewVertexEndpoints(Params, Params.GroupEdgeID, Params.StartCornerID, StartEndpoints, OptionalOut);
if (!bSuccess || StartEndpoints.Num() == 0 || (Progress && Progress->Cancelled()))
{
return false;
}
// Insert edges in both directions. In case of a loop, the second call won't do anything because
// AlteredGroups will be updated.
int32 TotalNumInserted = 0;
if (bHaveForwardEdge)
{
bSuccess = InsertEdgeLoopEdgesInDirection(Params, StartEndpoints, ForwardGroupID, ForwardEdgeID,
ForwardCornerID, ForwardBoundaryIndex, AlteredGroups, TotalNumInserted, OptionalOut, Progress);
}
if (bSuccess && bHaveBackwardEdge)
{
// TODO: The behavior here is not ideal in that if we fail to insert edges across a group we fail
// the entire insertion. For instance, in a curved staircase, where the bottom is a C-shape but otherwise
// a quad, we may fail to perform a plane cut, but we should still insert edges on the steps, where it
// doesn't fail. Unfortunately, being tolerant of insertion failures requires us to be able to revert
// the failed group to its original state on failure. We need to add support for that.
int32 NumInserted = 0;
bSuccess = InsertEdgeLoopEdgesInDirection(Params, StartEndpoints, BackwardGroupID, BackwardEdgeID,
BackwardCornerID, BackwardBoundaryIndex, AlteredGroups, NumInserted, OptionalOut, Progress) && bSuccess;
TotalNumInserted += NumInserted;
}
if (TotalNumInserted == 0 || (Progress && Progress->Cancelled()))
{
return false;
}
return Params.Topology->RebuildTopology() && bSuccess;
}
namespace GroupEdgeInserterLocals {
/**
* Given a group edge and the (adjoining) quad-like group across which we want to continue an edge loop,
* finds the id of the opposite (i.e., destination) group edge. Additionally, gives the corner ID attached
* to the provided one. Safe to call with an FDynamicMesh3::InvalidID parameters (will return false)
*
* If OptionalOut.ProblemGroupEdgeIDsOut is not null, will add the boundaries of a non-quad component to
* that set if the component turns out not to be valid due to not being quad-like.
*
* @returns true if a satisfactory edge was found.
*/
bool GetEdgeLoopOpposingEdgeAndCorner(const FGroupTopology& Topology, int32 GroupID, int32 GroupEdgeIDIn,
int32 CornerIDIn, int32& GroupEdgeIDOut, int32& CornerIDOut, int32& BoundaryIndexOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut)
{
GroupEdgeIDOut = FDynamicMesh3::InvalidID;
CornerIDOut = FDynamicMesh3::InvalidID;
BoundaryIndexOut = FDynamicMesh3::InvalidID;
if (GroupEdgeIDIn == FDynamicMesh3::InvalidID || GroupID == FDynamicMesh3::InvalidID)
{
return false;
}
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID);
check(Group);
for (int32 i = 0; i < Group->Boundaries.Num(); ++i)
{
const FGroupTopology::FGroupBoundary& Boundary = Group->Boundaries[i];
int32 GroupEdgeIndex = Boundary.GroupEdges.IndexOfByKey(GroupEdgeIDIn);
if (GroupEdgeIndex != INDEX_NONE)
{
if (Boundary.GroupEdges.Num() != 4)
{
if (OptionalOut.ProblemGroupEdgeIDsOut)
{
OptionalOut.ProblemGroupEdgeIDsOut->Append(Boundary.GroupEdges);
}
return false;
}
GroupEdgeIDOut = Boundary.GroupEdges[(GroupEdgeIndex + 2) % 4];
BoundaryIndexOut = i;
// Get the corner attached to the one we were given
if (CornerIDIn != FDynamicMesh3::InvalidID)
{
FGroupTopology::FGroupEdge SideEdge1 = Topology.Edges[Boundary.GroupEdges[(GroupEdgeIndex + 1) % 4]];
FGroupTopology::FGroupEdge SideEdge2 = Topology.Edges[Boundary.GroupEdges[(GroupEdgeIndex + 3) % 4]];
if (SideEdge1.EndpointCorners.A == CornerIDIn)
{
CornerIDOut = SideEdge1.EndpointCorners.B;
}
else if (SideEdge1.EndpointCorners.B == CornerIDIn)
{
CornerIDOut = SideEdge1.EndpointCorners.A;
}
else if (SideEdge2.EndpointCorners.A == CornerIDIn)
{
CornerIDOut = SideEdge2.EndpointCorners.B;
}
else if (SideEdge2.EndpointCorners.B == CornerIDIn)
{
CornerIDOut = SideEdge2.EndpointCorners.A;
}
}
return true;
}
}
return false;
}
/**
* Helper function, continues the loop in one direction from a start edge.
* @returns false if there is an error.
*/
bool InsertEdgeLoopEdgesInDirection(const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartEndpoints,
int32 NextGroupID, int32 NextEdgeID, int32 NextCornerID, int32 NextBoundaryIndex,
TSet<int32>& AlteredGroups, int32& NumInserted, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress)
{
NumInserted = 0;
if (AlteredGroups.Contains(NextGroupID) || StartEndpoints.Num() == 0)
{
return true;
}
// We keep endpoints in two arrays and swap the current one as we move along
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint> EndpointStorage1 = StartEndpoints;
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint> EndpointStorage2;
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>* CurrentEndpoints = &EndpointStorage1;
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>* NextEndpoints = &EndpointStorage2;
bool bHaveNextGroup = true;
bool bSuccess = true;
while (bHaveNextGroup && !AlteredGroups.Contains(NextGroupID))
{
if (Progress && Progress->Cancelled())
{
return false;
}
const FGroupTopology::FGroup* CurrentGroup = Params.Topology->FindGroupByID(NextGroupID);
check(CurrentGroup);
const FGroupTopology::FGroupBoundary& Boundary = CurrentGroup->Boundaries[NextBoundaryIndex];
// See if we looped around to the start
if (NextEdgeID == Params.GroupEdgeID)
{
int32 NumGroupsCreated;
bSuccess = ConnectEndpoints(Params, NextGroupID, Boundary, *CurrentEndpoints, StartEndpoints,
NumGroupsCreated, OptionalOut, Progress);
AlteredGroups.Add(NextGroupID);
NumInserted += (NumGroupsCreated > 1 ? 1 : 0);
break;
}
// Otherwise, create next endpoints
bSuccess = InsertNewVertexEndpoints(Params, NextEdgeID, NextCornerID, *NextEndpoints, OptionalOut);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
if (NextEndpoints->Num() == 0)
{
// Next edge wasn't long enough for the input lengths we wanted. Stop here.
// TODO: Actually we now clamp to max length. Should we?
return true;
}
// Connect up the endpoints
int32 NumGroupsCreated;
bSuccess = ConnectEndpoints(Params, NextGroupID, Boundary, *CurrentEndpoints, *NextEndpoints,
NumGroupsCreated, OptionalOut, Progress);
AlteredGroups.Add(NextGroupID);
NumInserted += (NumGroupsCreated > 1 ? 1 : 0);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
// Get the next group edge target
if (Params.Topology->IsBoundaryEdge(NextEdgeID))
{
break;
}
NextGroupID = Params.Topology->Edges[NextEdgeID].OtherGroupID(NextGroupID);
bHaveNextGroup = GetEdgeLoopOpposingEdgeAndCorner(*Params.Topology, NextGroupID, NextEdgeID, NextCornerID,
NextEdgeID, NextCornerID, NextBoundaryIndex, OptionalOut); // outputs
Swap(CurrentEndpoints, NextEndpoints);
}
return bSuccess;
}
/**
* Inserts vertices along an existing group edge that will be used as endpoints
* for new group edges.
* Note that due to tolerance, multiple inputs can map to the same vertex. We
* want to keep this functionality because in the case of proportion inputs, the
* snapping will partly depend on the narrowness of an edge, and we still want to
* allow connection to adjacent edges.
*
* Clears EndPointsOut before use.
*/
bool InsertNewVertexEndpoints(
const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params,
int32 GroupEdgeID, int32 StartCornerID,
TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPointsOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut)
{
EndPointsOut.Reset();
if (Params.SortedInputLengths->Num() == 0)
{
return false;
}
const FGroupTopology::FGroupEdge& GroupEdge = Params.Topology->Edges[GroupEdgeID];
// Prep the list of vertex ids and the corresponding cumulative lengths. It is easier
// to make our own copies because we may need to iterate backwards relative to the
// order in the topology.
bool bGoBackward = (GroupEdge.Span.Vertices.Last() == Params.Topology->GetCornerVertexID(StartCornerID));
TArray<int32> SpanVids;
if (!bGoBackward)
{
SpanVids = GroupEdge.Span.Vertices;
}
else
{
for (int i = GroupEdge.Span.Vertices.Num() - 1; i >= 0; --i)
{
SpanVids.Add(GroupEdge.Span.Vertices[i]);
}
}
TArray<double> PerVertexLengths;
TArray<double> ArcLengths;
if (Params.bInputsAreProportions)
{
ConvertProportionsToArcLengths(*Params.Topology, GroupEdgeID,
*Params.SortedInputLengths, ArcLengths, &PerVertexLengths);
}
else
{
ArcLengths = *Params.SortedInputLengths;
Params.Topology->GetEdgeArcLength(GroupEdgeID, &PerVertexLengths); // Get per vertex lengths
}
double TotalLength = PerVertexLengths.Last();
if (bGoBackward)
{
// Reverse order and update lengths to be TotalLength-length. Could do in one pass but then
// don't forget to modify the middle.
Algo::Reverse(PerVertexLengths);
Algo::ForEach(PerVertexLengths, [TotalLength](double& Length) {
Length = TotalLength - Length;
});
}
// We're going to walk forward selecting existing vertices or adding new ones as we go along.
// CurrentVid and CurrentArcLength may take on values that are not in SpanVids or PerVertexLengths
// as we insert new vertices. NextIndex, however is always an index into those two structures
// of the next vertex in front of the current one.
int32 CurrentVid = SpanVids[0];
double CurrentArcLength = 0;
int32 NextIndex = 1;
for (double TargetLength : ArcLengths)
{
// If the next target is beyond the last vertex, clamp it to the last vertex
if (TargetLength > TotalLength + Params.VertexTolerance)
{
TargetLength = TotalLength;
}
// Advance until the next vertex would overshoot the target length.
while (NextIndex < PerVertexLengths.Num() && PerVertexLengths[NextIndex] <= TargetLength)
{
CurrentVid = SpanVids[NextIndex];
CurrentArcLength = PerVertexLengths[NextIndex];
++NextIndex;
}
// The point is now either at the current vertex, or on the edge going forward (if there is one)
FGroupEdgeInserter::FGroupEdgeSplitPoint SplitPoint;
// Used if we find ourselves needing to snap to one of the endpoints of the edge
auto SetSplitPointToVertex = [&SplitPoint, &SpanVids, &Params, NextIndex](int32 Vid)
{
SplitPoint.ElementID = Vid;
SplitPoint.bIsVertex = true;
// Get the tangent vector. We haven't been keeping track of any previous verts we inserted,
// but inserted verts must be on an edge and must have the forward edge as their tangent.
bool bVertexIsOriginal = (Vid == SpanVids[NextIndex - 1]);
if (!bVertexIsOriginal)
{
SplitPoint.Tangent = Normalized(Params.Mesh->GetVertex(SpanVids[NextIndex]) - Params.Mesh->GetVertex(Vid));
}
else
{
FVector3d VertexPosition = Params.Mesh->GetVertex(Vid);
SplitPoint.Tangent = FVector3d::Zero();
if (NextIndex > 1)
{
SplitPoint.Tangent += Normalized(VertexPosition - Params.Mesh->GetVertex(SpanVids[NextIndex - 2]));
}
if (NextIndex < SpanVids.Num())
{
SplitPoint.Tangent += Normalized(Params.Mesh->GetVertex(SpanVids[NextIndex]) - VertexPosition);
}
Normalize(SplitPoint.Tangent);
}
};
// See if the target is at the current vertex
if (TargetLength - CurrentArcLength <= Params.VertexTolerance)
{
SetSplitPointToVertex(CurrentVid);
}
else if (NextIndex < PerVertexLengths.Num()
&& PerVertexLengths[NextIndex] - CurrentArcLength <= Params.VertexTolerance)
{
SetSplitPointToVertex(SpanVids[NextIndex]);
}
else
{
// Target must be on the edge that goes to the next vertex
int32 CurrentEid = Params.Mesh->FindEdge(CurrentVid, SpanVids[NextIndex]);
if (!ensure(CurrentEid >= 0))
{
// We shouldn't end up here, but if we do, it could be because something failed and
// stopped loop progress in one direction despite having performed edge splits forward,
// and now we have arrived at the split edge from the other direction, not realizing
// that SpanVids has changed
return false;
}
double SplitT = (TargetLength - CurrentArcLength) / (PerVertexLengths[NextIndex] - CurrentArcLength);
// See if the edge is stored backwards relative to the direction we're traveling
if (Params.Mesh->GetEdge(CurrentEid).Vert.B != SpanVids[NextIndex])
{
SplitT = 1 - SplitT;
}
// Perform the split
FDynamicMesh3::FEdgeSplitInfo EdgeSplitInfo;
Params.Mesh->SplitEdge(CurrentEid, EdgeSplitInfo, SplitT);
// Record the changed triangles
if (OptionalOut.ChangedTidsOut)
{
OptionalOut.ChangedTidsOut->Add(EdgeSplitInfo.OriginalTriangles.A);
if (EdgeSplitInfo.OriginalTriangles.B != FDynamicMesh3::InvalidID)
{
OptionalOut.ChangedTidsOut->Add(EdgeSplitInfo.OriginalTriangles.B);
}
}
// Update our position given that we're at a new vertex.
CurrentVid = EdgeSplitInfo.NewVertex;
CurrentArcLength = TargetLength;
// Assemble the actual output
SplitPoint.ElementID = CurrentVid;
SplitPoint.bIsVertex = true; // we made the vertex that is this target
SplitPoint.Tangent = Normalized(Params.Mesh->GetVertex(SpanVids[NextIndex]) - Params.Mesh->GetVertex(CurrentVid));
}
EndPointsOut.Add(SplitPoint);
}//end going through targets
return true;
}
void ConvertProportionsToArcLengths(
const FGroupTopology& Topology, int32 GroupEdgeID,
const TArray<double>& ProportionsIn,
TArray<double>& ArcLengthsOut, TArray<double>* PerVertexLengthsOut)
{
ArcLengthsOut.Reset(ProportionsIn.Num());
double TotalLength = Topology.GetEdgeArcLength(GroupEdgeID, PerVertexLengthsOut);
for (double Proportion : ProportionsIn)
{
ArcLengthsOut.Add(Proportion * TotalLength);
}
}
/**
* Helper function, assumes that StartPoints and EndPoints are all vertices and connects them.
* See ConnectMultipleUsingRetriangulation for details.
*
* @param GroupID Group across which to make the connections.
* @param GroupBoundary Boundary of the group across which the connections are being made.
*/
bool ConnectEndpoints(
const FGroupEdgeInserter::FEdgeLoopInsertionParams& Params, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
int32& NumGroupsCreated, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress)
{
if (Params.Mode == FGroupEdgeInserter::EInsertionMode::Retriangulate)
{
return ConnectMultipleUsingRetriangulation(*Params.Mesh, *Params.Topology, GroupID,
GroupBoundary, StartPoints, EndPoints, NumGroupsCreated, OptionalOut, Progress);
}
else if (Params.Mode == FGroupEdgeInserter::EInsertionMode::PlaneCut)
{
return ConnectMultipleUsingPlaneCut(*Params.Mesh, *Params.Topology, GroupID,
GroupBoundary, StartPoints, EndPoints, Params.VertexTolerance, Params.bSimplifyAlongPath,
NumGroupsCreated, OptionalOut, Progress);
}
else
{
checkf(false, TEXT("GroupEdgeInserter:ConnectEndpoints: Unimplemented insertion method."));
}
return false;
}
/**
* Connects multiple endpoints across the same group, making the assumption that StartPoints and
* EndPoints are composed only of vertex endpoints, are 1:1 in their arrays and are ordered
* sequentially away from the first startpoint and first endpoint, which have no endpoints between
* them. So the arrangement is something like this (may be mirrored):
*---*
| |
s0-e0
| |
s1-e1
| |
*---*
* The assumption is used to order the generated loops.
* The function exists because it is more efficient than generating each edge one at a
* time in a multi-loop case.
* @returns false if there's an error.
*/
bool ConnectMultipleUsingRetriangulation(
FDynamicMesh3& Mesh,
const FGroupTopology& Topology,
int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray <FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
int32& NumGroupsCreated, FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress)
{
NumGroupsCreated = 0;
if (Progress && Progress->Cancelled())
{
return false;
}
int32 NumNewEdges = FMath::Min(StartPoints.Num(), EndPoints.Num());
if (NumNewEdges == 0)
{
return true; // Nothing to do.
}
TArray<int32> BoundaryVertices;
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>> VidUVMaps;
bool bSuccess = DeleteGroupTrianglesAndGetLoop(Mesh, Topology, GroupID, GroupBoundary,
BoundaryVertices, VidUVMaps, OptionalOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
// Convert endpoint arrays to arrays of indices into the boundary vertex array.
TArray<int32> StartIndices;
TArray<int32> EndIndices;
for (int32 i = 0; i < NumNewEdges; ++i)
{
check(StartPoints[i].bIsVertex && EndPoints[i].bIsVertex);
int32 StartIndex = BoundaryVertices.Find(StartPoints[i].ElementID);
int32 EndIndex = BoundaryVertices.Find(EndPoints[i].ElementID);
check(StartIndex != INDEX_NONE && EndIndex != INDEX_NONE);
StartIndices.Add(StartIndex);
EndIndices.Add(EndIndex);
}
// We don't know which way the vertices are ordered relative to the counterclockwise ordering
// of the original group. If we were to go ccw from the first start vertex, we would expect
// to reach the second start vertex before the first end vertex. If we reach the end vertex
// first, then the diagram in the function header is flipped.
bool bReverseSubloopDirection = NumNewEdges > 1 &&
(StartIndices[1] - StartIndices[0] + BoundaryVertices.Num()) % BoundaryVertices.Num() // ccw distance from start
> (EndIndices[0] - StartIndices[0] + BoundaryVertices.Num()) % BoundaryVertices.Num();// ccw distance from start
if (Progress && Progress->Cancelled())
{
return false;
}
// Due to snapping and such, we may end up with degenerate loops that don't need triangulation.
// This could be the first loop, so we need to assign the existing group ID to the first loop
// that is not degenerate.
bool bUsedOriginalGroup = false;
// Do the first loop
TArray<int32> LoopVids;
if (!bReverseSubloopDirection)
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, EndIndices[0], StartIndices[0]);
}
else
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, StartIndices[0], EndIndices[0]);
}
if (LoopVids.Num() > 2)
{
bSuccess = RetriangulateLoop(Mesh, LoopVids, GroupID, VidUVMaps);
bUsedOriginalGroup = true;
NumGroupsCreated += (bSuccess ? 1 : 0);
}
// Do the middle loops
for (int32 i = 1; i < NumNewEdges; ++i)
{
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
// Check for a degenerate loop
if (StartIndices[i - 1] == StartIndices[i] && EndIndices[i - 1] == EndIndices[i])
{
continue;
}
LoopVids.Reset();
if (!bReverseSubloopDirection)
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, StartIndices[i - 1], StartIndices[i]); // previous to current
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, EndIndices[i], EndIndices[i - 1]); // current to previous
}
else
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, StartIndices[i], StartIndices[i - 1]); // current to previous
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, EndIndices[i - 1], EndIndices[i]); // previous to current
}
int32 GroupIDToUse = bUsedOriginalGroup ? Mesh.AllocateTriangleGroup() : GroupID;
bSuccess = RetriangulateLoop(Mesh, LoopVids, GroupIDToUse, VidUVMaps);
bUsedOriginalGroup = true;
NumGroupsCreated += (bSuccess ? 1 : 0);
}
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
// Do the last loop
LoopVids.Reset();
if (!bReverseSubloopDirection)
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, StartIndices.Last(), EndIndices.Last());
}
else
{
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, EndIndices.Last(), StartIndices.Last());
}
if (LoopVids.Num() > 2)
{
int32 GroupIDToUse = bUsedOriginalGroup ? Mesh.AllocateTriangleGroup() : GroupID;
bSuccess = RetriangulateLoop(Mesh, LoopVids, GroupIDToUse, VidUVMaps);
bUsedOriginalGroup = true;
NumGroupsCreated += (bSuccess ? 1 : 0);
}
if (OptionalOut.NewEidsOut)
{
for (int32 i = 0; i < NumNewEdges; ++i)
{
OptionalOut.NewEidsOut->Add(Mesh.FindEdge(StartPoints[i].ElementID, EndPoints[i].ElementID));
}
}
return bSuccess;
}
/**
* Helper function, deletes triangles in a group connected component and outputs the corresponding boundary.
* Does not delete the vertices.
*/
bool DeleteGroupTrianglesAndGetLoop(FDynamicMesh3& Mesh, const FGroupTopology& Topology, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary, TArray<int32>& BoundaryVerticesOut,
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>>& BoundaryVidUVMapsOut,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress)
{
if (Progress && Progress->Cancelled())
{
return false;
}
// Since groups may not be contiguous, we have to do a connected component search
// rather than deleting all triangles marked with that group, so get the seeds for the search.
int32 FirstEid = Topology.Edges[GroupBoundary.GroupEdges[0]].Span.Edges[0];
FIndex2i PotentialSeedTriangles = Mesh.GetEdge(FirstEid).Tri;
TArray<int32> SeedTriangles;
if (Mesh.GetTriangleGroup(PotentialSeedTriangles.A) == GroupID)
{
SeedTriangles.Add(PotentialSeedTriangles.A);
}
else
{
check(PotentialSeedTriangles.B != FDynamicMesh3::InvalidID && Mesh.GetTriangleGroup(PotentialSeedTriangles.B) == GroupID);
SeedTriangles.Add(PotentialSeedTriangles.B);
}
FMeshConnectedComponents ConnectedComponents(&Mesh);
ConnectedComponents.FindTrianglesConnectedToSeeds(SeedTriangles, [&](int32 t0, int32 t1) {
return Mesh.GetTriangleGroup(t0) == Mesh.GetTriangleGroup(t1);
});
if (Progress && Progress->Cancelled())
{
return false;
}
FMeshConnectedComponents::FComponent& Component = ConnectedComponents.GetComponent(0);
// Get the boundary loop
FMeshRegionBoundaryLoops RegionLoops(&Mesh, Component.Indices, true);
if (RegionLoops.bFailed || RegionLoops.Loops.Num() != 1)
{
// We don't support components with multiple boundaries (like a single cylinder side) because
// group edge insertion only works in very limited circumstances here (for instance, connecting
// multiple boundaries generally can't be done with a single group edge, since the group will
// remain connected), and retriangulation would be a huge pain.
return false;
}
RegionLoops.Loops[0].Reverse();
BoundaryVerticesOut = RegionLoops.Loops[0].Vertices;
if (Mesh.HasAttributes())
{
const FDynamicMeshAttributeSet* Attributes = Mesh.Attributes();
for (int i = 0; i < Attributes->NumUVLayers(); ++i)
{
BoundaryVidUVMapsOut.Emplace();
RegionLoops.GetLoopOverlayMap(RegionLoops.Loops[0],
*Mesh.Attributes()->GetUVLayer(i), BoundaryVidUVMapsOut.Last());
}
}
if (Progress && Progress->Cancelled())
{
return false;
}
if (OptionalOut.ChangedTidsOut)
{
OptionalOut.ChangedTidsOut->Append(Component.Indices);
}
// When deleting, we don't we don't want to remove isolated verts on the boundary,
// but we do want to remove isolated verts on the interior of the component. We
// could finish the retriangulation and look for isolated verts afterwards, but
// that requires us to keep track of the old verts until we're done triangulating.
// Instead, we'll just go ahead and delete any old verts not on the boundary.
// Get all verts in the component, and the verts on the boundary
TArray<int32> ComponentVids;
UE::Geometry::TriangleToVertexIDs(&Mesh, Component.Indices, ComponentVids);
TSet<int32> BoundaryVidSet(RegionLoops.Loops[0].Vertices);
// Delete the triangles
FDynamicMeshEditor Editor(&Mesh);
Editor.RemoveTriangles(Component.Indices, false); // don't remove isolated verts
// Remove verts that weren't on the boundary
Algo::ForEachIf(ComponentVids,
[&BoundaryVidSet](int32 Vid)
{
return !BoundaryVidSet.Contains(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(BoundaryVidUVMapsOut[i],
*Mesh.Attributes()->GetUVLayer(i));
}
}
return true;
}
/**
* Appends entries from an input array from a start index to end index (inclusive), wrapping around
* at the end.
*/
void AppendInclusiveRangeWrapAround(const TArray<int32>& InputArray, TArray<int32>& OutputArray,
int32 StartIndex, int32 InclusiveEndIndex)
{
check(InclusiveEndIndex >= 0 && InclusiveEndIndex < InputArray.Num()
&& StartIndex >= 0 && StartIndex < InputArray.Num());
int32 CurrentIndex = StartIndex;
while (CurrentIndex != InclusiveEndIndex)
{
OutputArray.Add(InputArray[CurrentIndex]);
CurrentIndex = (CurrentIndex + 1) % InputArray.Num();
}
OutputArray.Add(InputArray[InclusiveEndIndex]);
}
bool RetriangulateLoop(FDynamicMesh3& Mesh,
const TArray<int32>& LoopVertices, int32 NewGroupID,
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>>& VidUVMaps)
{
TArray<int32> LoopEdges;
FEdgeLoop::VertexLoopToEdgeLoop(&Mesh, LoopVertices, LoopEdges);
FEdgeLoop Loop(&Mesh, LoopVertices, LoopEdges);
FSimpleHoleFiller HoleFiller(&Mesh, Loop, FSimpleHoleFiller::EFillType::PolygonEarClipping);
if (!HoleFiller.Fill(NewGroupID))
{
// If the hole filler failed, it is probably because two non-adjacent vertices in the loop
// already have an edge between them ("on the other side" of this group), so inserting
// two more triangles between the verts is not possible while keeping things manifold.
// As an example, imagine a cube with one face deleted but then an obtuse triangle added
// to one of the boundary edges to make one cube side an irregular pentagon.
// Granted, perhaps the user shouldn't be trying to be doing simple retriangulation in a
// situation like this, but let's do something reasonable anyway. We'll retriangulate with
// an added center vert, since this will only add to the edges that we know are boundaries.
// Delete any triangles we already added
FDynamicMeshEditor Editor(&Mesh);
Editor.RemoveTriangles(HoleFiller.NewTriangles, false); // don't remove isolated verts
// Change the hole fill type
HoleFiller.FillType = FSimpleHoleFiller::EFillType::TriangleFan;
if (!HoleFiller.Fill(NewGroupID))
{
// Not sure how this could happen... Did we not delete the interior triangles somehow
// or give the wrong loop?
return ensure(false);
}
}
if (Mesh.HasAttributes())
{
if (!HoleFiller.UpdateAttributes(VidUVMaps))
{
return false;
}
}
return true;
}
/**
* See ConnectMultipleUsingRetriangulation for details.
*/
bool ConnectMultipleUsingPlaneCut(FDynamicMesh3& Mesh,
const FGroupTopology& Topology, int32 GroupID,
const FGroupTopology::FGroupBoundary& GroupBoundary,
const TArray<FGroupEdgeInserter::FGroupEdgeSplitPoint>& StartPoints,
const TArray <FGroupEdgeInserter::FGroupEdgeSplitPoint>& EndPoints,
double VertexTolerance, bool bSimplifyAlongPath, int32& NumGroupsCreated,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut,
FProgressCancel* Progress)
{
NumGroupsCreated = 0;
if (Progress && Progress->Cancelled())
{
return false;
}
int32 NumEdgesToInsert = FMath::Min(StartPoints.Num(), EndPoints.Num());
TSet<int32> PathsEids;
for (int32 i = 0; i < NumEdgesToInsert; ++i)
{
bool bSuccess = EmbedPlaneCutPath(Mesh, Topology, GroupID, StartPoints[i], EndPoints[i],
VertexTolerance, bSimplifyAlongPath, PathsEids, OptionalOut.ChangedTidsOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
}
if (OptionalOut.NewEidsOut)
{
*OptionalOut.NewEidsOut = OptionalOut.NewEidsOut->Union(PathsEids);
}
bool bSuccess = CreateNewGroups(Mesh, PathsEids, GroupID, NumGroupsCreated, OptionalOut, Progress);
return bSuccess;
}
/**
* Places a plane path connecting the endpoints into the mesh, but does not give the triangles new groups yet.
* However, outputs the path edge ID's so that can be done later.
*/
bool EmbedPlaneCutPath(FDynamicMesh3& Mesh, const FGroupTopology& Topology,
int32 GroupID, const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
double VertexTolerance, bool bSimplifyAlongPath, TSet<int32>& PathEidsOut,
TSet<int32>* ChangedTrisOut, FProgressCancel* Progress)
{
if (Progress && Progress->Cancelled())
{
return false;
}
// We don't allow snapping to any of the boundary vertices via plane distance because this can lead
// to a situation where we snap via plane cut but not via absolute distance (depending on plane
// orientation relative boundary), which results in us arriving at the boundary at a nearby vertex
// and then walking along the boundary to the destination. Aside from looking/behaving weirdly,
// this is very bad for edge loops, where doing so can join the edge at a different point from the
// one at which it continues on the other side, which means that quad-like topology gets
// inadvertantly broken without the user realizing why.
TSet<int32> DisallowedVids;
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID);
if (ensure(Group))
{
for (const FGroupTopology::FGroupBoundary& Boundary : Group->Boundaries)
{
for (int32 GroupEdgeID : Boundary.GroupEdges)
{
if (ensure(GroupEdgeID < Topology.Edges.Num()))
{
DisallowedVids.Append(Topology.Edges[GroupEdgeID].Span.Vertices);
}
}
}
}
// Find the path we're going to take.
TArray<TPair<FMeshSurfacePoint, int>> CutPath;
bool bSuccess = GetPlaneCutPath(Mesh, GroupID, StartPoint, EndPoint,
CutPath, VertexTolerance, DisallowedVids, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
check(CutPath.Num() >= 2);
// Save triangles that will change
if (ChangedTrisOut)
{
for (int32 i = 0; i < CutPath.Num(); ++i)
{
FMeshSurfacePoint& Point = CutPath[i].Key;
if (Point.PointType == ESurfacePointType::Edge)
{
FIndex2i EdgeTris = Mesh.GetEdgeT(Point.ElementID);
ChangedTrisOut->Add(EdgeTris.A);
if (EdgeTris.B != FDynamicMesh3::InvalidID)
{
ChangedTrisOut->Add(EdgeTris.B);
}
}
}
}
// Embed the path.
FMeshSurfacePath PathEmbedder(&Mesh);
PathEmbedder.Path = CutPath;
TArray<int32> PathVertices;
bSuccess = PathEmbedder.EmbedSimplePath(false, PathVertices, false);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
check(PathVertices.Num() >= 2);
for (int32 i = 1; i < PathVertices.Num(); ++i)
{
int32 Eid = Mesh.FindEdge(PathVertices[i - 1], PathVertices[i]);
if (ensure(Eid >= 0))
{
PathEidsOut.Add(Eid);
}
}
if (bSimplifyAlongPath)
{
FLocalPlanarSimplify Simplify;
// Note: Vertex normals are not reliable here, so preserving them will just prevent simplifications that are fine ...
Simplify.bPreserveVertexNormals = false;
// Note: Any triangles that this simplify can remove should already be in ChangedTrisOut, so we shouldn't need to update that array here
Simplify.SimplifyAlongEdges(Mesh, PathEidsOut);
}
return true;
}
/**
* Uses the given path edge ID's to split a group into new groups.
*/
bool CreateNewGroups(FDynamicMesh3& Mesh, TSet<int32>& PathEids, int32 OriginalGroup, int32& NumGroupsCreated,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress)
{
NumGroupsCreated = 0;
if (Progress && Progress->Cancelled())
{
return false;
}
// Create the new groups.
TSet<int32> SeedTriangleSet;
for (int32 Eid : PathEids)
{
FIndex2i Tris = Mesh.GetEdgeT(Eid);
if (Mesh.GetTriangleGroup(Tris.A) == OriginalGroup)
{
SeedTriangleSet.Add(Tris.A);
}
if (Tris.B != FDynamicMesh3::InvalidID && Mesh.GetTriangleGroup(Tris.B) == OriginalGroup)
{
SeedTriangleSet.Add(Tris.B);
}
}
FMeshConnectedComponents ConnectedComponents(&Mesh);
ConnectedComponents.FindTrianglesConnectedToSeeds(SeedTriangleSet.Array(), [&](int32 t0, int32 t1) {
// Triangles are connected only if they have the same group and are not across one of the
// newly inserted group edges.
int32 Group0 = Mesh.GetTriangleGroup(t0);
int32 Group1 = Mesh.GetTriangleGroup(t1);
if (Group0 == Group1)
{
int32 SharedEdge = Mesh.FindEdgeFromTriPair(t0, t1);
return !PathEids.Contains(SharedEdge);
}
return false;
});
if (Progress && Progress->Cancelled())
{
return false;
}
// Assign a new group id for each component. The first component keeps the old group ID.
for (int32 i = 1; i < ConnectedComponents.Num(); ++i)
{
FMeshConnectedComponents::FComponent& Component = ConnectedComponents.GetComponent(i);
if (OptionalOut.ChangedTidsOut)
{
OptionalOut.ChangedTidsOut->Append(Component.Indices);
}
int32 NewGroupID = Mesh.AllocateTriangleGroup();
for (int Tid : Component.Indices)
{
Mesh.SetTriangleGroup(Tid, NewGroupID);
}
}
NumGroupsCreated = ConnectedComponents.Num();
return true;
}
}//end namespace GroupEdgeInserterLocals
/** Inserts a group edge into a given group. */
bool FGroupEdgeInserter::InsertGroupEdge(FGroupEdgeInsertionParams& Params, FGroupEdgeInserter::FOptionalOutputParams OptionalOut, FProgressCancel* Progress)
{
using namespace GroupEdgeInserterLocals;
if (Progress && Progress->Cancelled())
{
return false;
}
// Validate the inputs
check(Params.Mesh);
check(Params.Topology);
check(Params.GroupID != FDynamicMesh3::InvalidID);
check(Params.StartPoint.ElementID != FDynamicMesh3::InvalidID);
check(Params.EndPoint.ElementID != FDynamicMesh3::InvalidID);
if (Params.StartPoint.bIsVertex == Params.EndPoint.bIsVertex
&& Params.StartPoint.ElementID == Params.EndPoint.ElementID)
{
// Points are on same vertex or edge.
return false;
}
if (Params.Mode == EInsertionMode::PlaneCut)
{
TSet<int32> TempNewEids;
TSet<int32>* NewEids = OptionalOut.NewEidsOut ? OptionalOut.NewEidsOut : &TempNewEids;
bool bSuccess = EmbedPlaneCutPath(*Params.Mesh, *Params.Topology, Params.GroupID, Params.StartPoint,
Params.EndPoint, Params.VertexTolerance, Params.bSimplifyAlongPath, *NewEids, OptionalOut.ChangedTidsOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
int32 NumGroupsCreated;
bSuccess = CreateNewGroups(*Params.Mesh, *NewEids, Params.GroupID, NumGroupsCreated, OptionalOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
}
else if (Params.Mode == EInsertionMode::Retriangulate)
{
bool bSuccess = InsertSingleWithRetriangulation(*Params.Mesh, *Params.Topology,
Params.GroupID, Params.GroupBoundaryIndex, Params.StartPoint, Params.EndPoint,
OptionalOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
}
else
{
checkf(false, TEXT("GroupEdgeInserter:InsertGroupEdge: Unimplemented insertion method."));
}
Params.Topology->RebuildTopology();
return true;
}
namespace GroupEdgeInserterLocals {
/** Helper function. Not used when inserting multiple edges at once into a group to avoid continuously retriangulating and deleting. */
bool InsertSingleWithRetriangulation(FDynamicMesh3& Mesh, FGroupTopology& Topology,
int32 GroupID, int32 BoundaryIndex,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
FGroupEdgeInserter::FOptionalOutputParams& OptionalOut, FProgressCancel* Progress)
{
if (Progress && Progress->Cancelled())
{
return false;
}
if (StartPoint.bIsVertex == EndPoint.bIsVertex
&& StartPoint.ElementID == EndPoint.ElementID)
{
// Points are on same vertex or edge.
return false;
}
// Function we use to split the endpoints if needed
auto AddVertOnEdge = [](FDynamicMesh3& Mesh, int32 Eid, double EdgeTValue, TSet<int32>* ChangedTidsOut) {
FDynamicMesh3::FEdgeSplitInfo SplitInfo;
Mesh.SplitEdge(Eid, SplitInfo, EdgeTValue);
// Record the changed triangles
if (ChangedTidsOut)
{
ChangedTidsOut->Add(SplitInfo.OriginalTriangles.A);
if (SplitInfo.OriginalTriangles.B != FDynamicMesh3::InvalidID)
{
ChangedTidsOut->Add(SplitInfo.OriginalTriangles.B);
}
}
return SplitInfo.NewVertex;
};
int32 StartVid = StartPoint.ElementID;
if (!StartPoint.bIsVertex)
{
StartVid = AddVertOnEdge(Mesh, StartPoint.ElementID, StartPoint.EdgeTValue, OptionalOut.ChangedTidsOut);
}
int32 EndVid = EndPoint.ElementID;
if (!EndPoint.bIsVertex)
{
EndVid = AddVertOnEdge(Mesh, EndPoint.ElementID, EndPoint.EdgeTValue, OptionalOut.ChangedTidsOut);
}
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID);
check(Group && BoundaryIndex >= 0 && BoundaryIndex < Group->Boundaries.Num());
TArray<int32> BoundaryVertices;
TArray<FMeshRegionBoundaryLoops::VidOverlayMap<FVector2f>> VidUVMaps;
bool bSuccess = DeleteGroupTrianglesAndGetLoop(Mesh, Topology, GroupID, Group->Boundaries[BoundaryIndex],
BoundaryVertices, VidUVMaps, OptionalOut, Progress);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
int32 IndexA = BoundaryVertices.IndexOfByKey(StartVid);
int32 IndexB = BoundaryVertices.IndexOfByKey(EndVid);
TArray<int32> LoopVids;
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, IndexA, IndexB);
if (LoopVids.Num() < 3)
{
// If one our endpoints turn out to be adjacent, there's nothing to insert.
// TODO: we could do a tiny bit more work to detect this earlier.
return false;
}
bSuccess = RetriangulateLoop(Mesh, LoopVids, GroupID, VidUVMaps);
if (!bSuccess || (Progress && Progress->Cancelled()))
{
return false;
}
LoopVids.Reset();
AppendInclusiveRangeWrapAround(BoundaryVertices, LoopVids, IndexB, IndexA);
if (LoopVids.Num() < 3)
{
return false;
}
bSuccess = RetriangulateLoop(Mesh, LoopVids, Mesh.AllocateTriangleGroup(), VidUVMaps);
if (OptionalOut.NewEidsOut)
{
OptionalOut.NewEidsOut->Add(Mesh.FindEdge(StartVid, EndVid));
}
return bSuccess;
}
/**
* Creates a path of FMeshSurfacePoint instances across a group that can be embedded into the mesh,
* based on a plane cut from start to end. Does not actually embed that path yet.
*
* Assumes that the start and end points are on the boundary of the group, and doesn't try to deal
* with some complicated edge cases that could arise in nonplanar groups.
*
* Instead of having this function, we should modify EmbedSurfacePath.cpp::WalkMeshPlanar to allow the start and
* end points to be edges/vertices and to have a filter function that we can use to filter out triangles that are
* not in the group we want.
*
* @returns false if path could not be found.
*/
bool GetPlaneCutPath(const FDynamicMesh3& Mesh, int32 GroupID,
const FGroupEdgeInserter::FGroupEdgeSplitPoint& StartPoint, const FGroupEdgeInserter::FGroupEdgeSplitPoint& EndPoint,
TArray<TPair<FMeshSurfacePoint, int>>& OutputPath, double VertexCutTolerance,
const TSet<int32>& DisallowedVids, FProgressCancel* Progress)
{
if (Progress && Progress->Cancelled())
{
return false;
}
// Used to make sure we don't end up walking in a loop or backwards. This could happen with a high vertex cut
// tolerance and pathological topology via vertices. It shouldn't be possible via edges for the cases where
// we use this function (between points of a contiguous group boundary), but we'll be safe.
TSet<int32> CrossedVids;
TSet<int32> CrossedEids;
// Start by determining the plane we will use.
FVector3d StartPosition = StartPoint.bIsVertex ? Mesh.GetVertex(StartPoint.ElementID)
: Mesh.GetEdgePoint(StartPoint.ElementID, StartPoint.EdgeTValue);
FVector3d EndPosition = EndPoint.bIsVertex ? Mesh.GetVertex(EndPoint.ElementID)
: Mesh.GetEdgePoint(EndPoint.ElementID, EndPoint.EdgeTValue);
FVector3d InPlaneVector = Normalized(EndPosition - StartPosition);
// Get components of the two tangents that are orthogonal to the vector between the points.
FVector3d NormalA = Normalized(StartPoint.Tangent - StartPoint.Tangent.Dot(InPlaneVector) * InPlaneVector,
static_cast<double>(KINDA_SMALL_NUMBER));
FVector3d NormalB = Normalized(EndPoint.Tangent - EndPoint.Tangent.Dot(InPlaneVector) * InPlaneVector,
static_cast<double>(KINDA_SMALL_NUMBER));
if (NormalA.IsZero() || NormalB.IsZero())
{
// One or both of the tangents were pointing directly toward the destination, and in such
// cases the cutting plane we fit is likely to be nonsense (along the edge of a cube,
// for instance, we may end up trying to use a plane roughly coplanar with the face we're
// trying to "cut").
return false;
}
// Make the vectors be in the same half space so that their average represents the closer average of the
// corresponding lines.
if (NormalA.Dot(NormalB) < 0)
{
NormalB = -NormalB;
}
// Do the averaging
FVector CutPlaneNormal = (FVector)Normalized(NormalA + NormalB);
if (CutPlaneNormal.IsZero())
{
// This shouldn't happen because we already checked for both of them being zero, and we made them be in
// the same half space.
return ensure(false);
}
FVector CutPlaneOrigin = (FVector)StartPosition;
// These store distances of the current edge from the plane, so they don't have to be recomputed when
// finding the next point.
float CurrentEdgeVertPlaneDistances[2];
// Prep the first point.
OutputPath.Empty();
if (StartPoint.bIsVertex)
{
OutputPath.Emplace(FMeshSurfacePoint(StartPoint.ElementID), FDynamicMesh3::InvalidID);
}
else
{
// Note that we do not clamp the endpoints in this function because clamping based
// on plane distance can result in different behavior depending on which way the plane is oriented, which
// could end up clamping to different endpoints as we connect multiple paths through the same start/end
// point (such as when following a loop)
FIndex2i EdgeVids = Mesh.GetEdgeV(StartPoint.ElementID);
CurrentEdgeVertPlaneDistances[0] = (float)FVector::PointPlaneDist((FVector)Mesh.GetVertex(EdgeVids.A), CutPlaneOrigin, CutPlaneNormal);
CurrentEdgeVertPlaneDistances[1] = (float)FVector::PointPlaneDist((FVector)Mesh.GetVertex(EdgeVids.B), CutPlaneOrigin, CutPlaneNormal);
OutputPath.Emplace(FMeshSurfacePoint(StartPoint.ElementID, StartPoint.EdgeTValue), FDynamicMesh3::InvalidID);
}
check(OutputPath.Num() == 1);
// Set up a few more variables we'll need as we walk from start to end
bool bCurrentPointIsVertex = (OutputPath[0].Key.PointType == ESurfacePointType::Vertex);
int32 CurrentElementID = OutputPath[0].Key.ElementID;
int32 PointCount = 1; // Used as a sanity check
// Tracks which triangle (if not an edge) we traversed to get to the next point, so we can avoid
// backtracking in the next step.
int32 TraversedTid = FDynamicMesh3::InvalidID;
// Do the walk. The exit condition is that the last point of our output path has the same ID and type
// as our endpoint.
while (!(CurrentElementID == EndPoint.ElementID && bCurrentPointIsVertex == EndPoint.bIsVertex))
{
if (Progress && Progress->Cancelled())
{
return false;
}
// Prevent ourselves from going in a loop
if (bCurrentPointIsVertex)
{
if (CrossedVids.Contains(CurrentElementID))
{
return false;
}
CrossedVids.Add(CurrentElementID);
}
else
{
if (CrossedEids.Contains(CurrentElementID))
{
return false;
}
CrossedEids.Add(CurrentElementID);
}
check(PointCount < Mesh.EdgeCount()); // sanity check to avoid infinite loop
if (bCurrentPointIsVertex)
{
FMeshSurfacePoint NextPoint(FDynamicMesh3::InvalidID);
FVector3d CurrentPosition = OutputPath.Last().Key.Pos(&Mesh);
// Look through the surrounding triangles that have the group we want and find one that intersects the plane
int32 CandidateTraversedTid = FDynamicMesh3::InvalidID;
for (int32 Tid : Mesh.VtxTrianglesItr(CurrentElementID))
{
if (Tid == TraversedTid || Mesh.GetTriangleGroup(Tid) != GroupID)
{
continue;
}
// See if one of the triangle edges has the endpoint, in which case we can go straight there.
if (!EndPoint.bIsVertex)
{
const FIndex3i& TriangleEids = Mesh.GetTriEdges(Tid);
for (int32 i = 0; i < 3; ++i)
{
if (EndPoint.ElementID == TriangleEids[i])
{
// Got to the end
OutputPath.Emplace(FMeshSurfacePoint(EndPoint.ElementID, EndPoint.EdgeTValue), FDynamicMesh3::InvalidID);
return true;
}
}
}
// Check to see if one of the triangle vertices is the end
const FIndex3i& TriangleVids = Mesh.GetTriangle(Tid);
int32 VertA = (TriangleVids.A == CurrentElementID) ? TriangleVids.C : TriangleVids.A;
int32 VertB = (TriangleVids.B == CurrentElementID) ? TriangleVids.C : TriangleVids.B;
if (EndPoint.bIsVertex && (EndPoint.ElementID == VertA || EndPoint.ElementID == VertB))
{
OutputPath.Emplace(FMeshSurfacePoint(EndPoint.ElementID), FDynamicMesh3::InvalidID);
return true;
}
// See if one of the other vertices is on the plane (and is therefore the next destination)
float PlaneDistanceA = (float)FVector::PointPlaneDist((FVector)Mesh.GetVertex(VertA), CutPlaneOrigin, CutPlaneNormal);
float PlaneDistanceB = (float)FVector::PointPlaneDist((FVector)Mesh.GetVertex(VertB), CutPlaneOrigin, CutPlaneNormal);
bool bVertAIsOnPlane = abs(PlaneDistanceA) <= VertexCutTolerance;
bool bVertBIsOnPlane = abs(PlaneDistanceB) <= VertexCutTolerance;
auto UpdateNextPoint = [&InPlaneVector, &Mesh, &CurrentPosition, &NextPoint](const FMeshSurfacePoint& CandidateSurfacePoint)
{
if (NextPoint.ElementID == CandidateSurfacePoint.ElementID && NextPoint.PointType == CandidateSurfacePoint.PointType)
{
// We're looking at the same point from an adjacent triangle
return false;
}
// Update the next point if the candidate moves more directly toward the destination.
// TOOD: This hack is necessary to deal with some common ambiguous cases (such as starting from a
// vertex in the concave region of a nonconvex planar surface), but the proper solution is to alter
// and use EmbedSurfacePath.cpp::WalkMeshPlanar
if (NextPoint.ElementID == FDynamicMesh3::InvalidID
|| (InPlaneVector.Dot(NextPoint.Pos(&Mesh) - CurrentPosition) <
InPlaneVector.Dot(CandidateSurfacePoint.Pos(&Mesh) - CurrentPosition)))
{
NextPoint = CandidateSurfacePoint;
return true;
}
return false;
};
bool bEdgeVertIsPreferred = false;
if (bVertAIsOnPlane && !DisallowedVids.Contains(VertA))
{
bEdgeVertIsPreferred = true;
UpdateNextPoint(FMeshSurfacePoint(VertA));
}
if (bVertBIsOnPlane && !DisallowedVids.Contains(VertB))
{
bEdgeVertIsPreferred = true;
UpdateNextPoint(FMeshSurfacePoint(VertB));
}
if (!bEdgeVertIsPreferred && PlaneDistanceA * PlaneDistanceB < 0)
{
// The triangle's opposite edge crosses the plane, and the edge verts are not
// valid snap targets
int32 Eid = Mesh.FindEdgeFromTri(VertA, VertB, Tid);
double EdgeTValue = PlaneDistanceA / (PlaneDistanceA - PlaneDistanceB);
if (VertA != Mesh.GetEdgeV(Eid).A)
{
EdgeTValue = 1 - EdgeTValue;
}
if (UpdateNextPoint(FMeshSurfacePoint(Eid, EdgeTValue)))
{
CurrentEdgeVertPlaneDistances[0] = PlaneDistanceA;
CurrentEdgeVertPlaneDistances[1] = PlaneDistanceB;
if (VertA != Mesh.GetEdgeV(Eid).A)
{
Swap(CurrentEdgeVertPlaneDistances[0], CurrentEdgeVertPlaneDistances[1]);
}
CandidateTraversedTid = Tid;
}
}
}//end going through triangles
// Make sure we found the next point.
if (NextPoint.ElementID == FDynamicMesh3::InvalidID)
{
return false;
}
else
{
OutputPath.Emplace(NextPoint, FDynamicMesh3::InvalidID);
TraversedTid = (NextPoint.PointType == ESurfacePointType::Edge) ? CandidateTraversedTid
: FDynamicMesh3::InvalidID;
}
}//end if at vert
else
{
const FDynamicMesh3::FEdge& Edge = Mesh.GetEdge(CurrentElementID);
// We're starting from an edge. Get the triangle that we're dealing with.
int32 NextTid;
if (Edge.Tri.A == TraversedTid)
{
NextTid = Edge.Tri.B;
}
else if (Edge.Tri.B == TraversedTid)
{
NextTid = Edge.Tri.A;
}
else
{
NextTid = Mesh.GetTriangleGroup(Edge.Tri.A) == GroupID ? Edge.Tri.A : Edge.Tri.B;
}
if (NextTid == FDynamicMesh3::InvalidID || Mesh.GetTriangleGroup(NextTid) != GroupID)
{
// We hit a dead end before getting to the end.
return false;
}
TraversedTid = NextTid;
// See if the opposite vert is our endpoint.
int32 OppositeVert = IndexUtil::FindTriOtherVtx(Edge.Vert.A, Edge.Vert.B, Mesh.GetTriangle(NextTid));
if (EndPoint.bIsVertex && EndPoint.ElementID == OppositeVert)
{
OutputPath.Emplace(FMeshSurfacePoint(EndPoint.ElementID), FDynamicMesh3::InvalidID);
return true;
}
// See if one of the triangle edges has the endpoint, in which case we can go straight there.
if (!EndPoint.bIsVertex)
{
const FIndex3i& TriangleEids = Mesh.GetTriEdges(NextTid);
for (int32 i = 0; i < 3; ++i)
{
if (EndPoint.ElementID == TriangleEids[i])
{
// Got to the end
OutputPath.Emplace(FMeshSurfacePoint(EndPoint.ElementID, EndPoint.EdgeTValue), FDynamicMesh3::InvalidID);
return true;
}
}
}
// We'll keep going. Get the placement of the opposite vert relative to the plane.
float OppositeVertPlaneDistance = (float)FVector::PointPlaneDist((FVector)Mesh.GetVertex(OppositeVert), CutPlaneOrigin, CutPlaneNormal);
if (abs(OppositeVertPlaneDistance) <= VertexCutTolerance && !DisallowedVids.Contains(OppositeVert))
{
// We are cutting through a vertex
OutputPath.Emplace(FMeshSurfacePoint(OppositeVert), FDynamicMesh3::InvalidID);
}
else
{
// We are cutting through an edge. Figure out which one
int32 SecondVertOfNextEdge;
float SecondPlaneDistance;
if (CurrentEdgeVertPlaneDistances[0] * OppositeVertPlaneDistance < 0)
{
SecondVertOfNextEdge = Edge.Vert.A;
SecondPlaneDistance = CurrentEdgeVertPlaneDistances[0];
}
else if (CurrentEdgeVertPlaneDistances[1] * OppositeVertPlaneDistance < 0)
{
SecondVertOfNextEdge = Edge.Vert.B;
SecondPlaneDistance = CurrentEdgeVertPlaneDistances[1];
}
else
{
// For neither of the edge vertices to be on the opposite side of the plane from
// the opposite vertex, the edge must lie in the plane. This can only happen if
// we started on an edge and then fit a bad cutting plane (otherwise we would have
// snapped to a vertex instead). This should usually not happen since we check the
// tangents, but we use a different type of check/tolerance there, and it could
// still happen if endpoints weren't snapped for some reason and the edge is super short.
return false;
}
// Add the edge point to output
int32 Eid = Mesh.FindEdge(OppositeVert, SecondVertOfNextEdge);
// The division here is safe because SecondPlaneDistance is guaranteed to have an opposite sign to
// OppositeVertPlaneDistance by the if-else statements above.
double EdgeTValue = OppositeVertPlaneDistance / (OppositeVertPlaneDistance - SecondPlaneDistance);
CurrentEdgeVertPlaneDistances[0] = OppositeVertPlaneDistance;
CurrentEdgeVertPlaneDistances[1] = SecondPlaneDistance;
if (OppositeVert != Mesh.GetEdgeV(Eid).A)
{
EdgeTValue = 1 - EdgeTValue;
Swap(CurrentEdgeVertPlaneDistances[0], CurrentEdgeVertPlaneDistances[1]);
}
OutputPath.Emplace(FMeshSurfacePoint(Eid, EdgeTValue), FDynamicMesh3::InvalidID);
}//end cutting through edge
}//end if last point was edge
++PointCount;
check(PointCount == OutputPath.Num()) // Another sanity check, to make sure we're always advancing
CurrentElementID = OutputPath.Last().Key.ElementID;
bCurrentPointIsVertex = (OutputPath.Last().Key.PointType == ESurfacePointType::Vertex);
}//end until we get to end
return true;
}
}//end namespace GroupEdgeInserterLocals