Files
UnrealEngineUWP/Engine/Plugins/Runtime/GeometryProcessing/Source/DynamicMesh/Private/Operations/MeshSelfUnion.cpp
ryan schmidt 6ad26b69f0 rename UE::Geometry::TTransform3 to TTransformSRT3, update references
#rb none
#rnx
#jira UE-139757
#preflight 61f572d9e52a8a4a910990f1

#ROBOMERGE-AUTHOR: ryan.schmidt
#ROBOMERGE-SOURCE: CL 18784197 in //UE5/Release-5.0/... via CL 18784203 via CL 18784222
#ROBOMERGE-BOT: UE5 (Release-Engine-Test -> Main) (v903-18687472)

[CL 18784226 by ryan schmidt in ue5-main branch]
2022-01-29 14:37:53 -05:00

795 lines
25 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// Port of geometry3Sharp MeshBoolean
#include "Operations/MeshSelfUnion.h"
#include "Operations/MeshMeshCut.h"
#include "Selections/MeshConnectedComponents.h"
#include "DynamicMesh/MeshNormals.h"
#include "Async/ParallelFor.h"
#include "DynamicMesh/MeshTransforms.h"
#include "Spatial/SparseDynamicOctree3.h"
#include "Algo/RemoveIf.h"
#include "DynamicMesh/DynamicMeshAABBTree3.h"
using namespace UE::Geometry;
bool FMeshSelfUnion::Compute()
{
// transform the mesh to a shared space (centered at the origin, and scaled to a unit cube)
FAxisAlignedBox3d AABB = Mesh->GetBounds(true);
double ScaleFactor = 1.0 / FMath::Clamp(AABB.MaxDim(), 0.01, 1000000.0);
FTransformSRT3d TransformToCenteredBox = FTransformSRT3d::Identity();
TransformToCenteredBox.SetTranslation(ScaleFactor * (TransformToCenteredBox.GetTranslation() - AABB.Center()));
TransformToCenteredBox.SetScale(ScaleFactor * FVector3d::One());
MeshTransforms::ApplyTransform(*Mesh, TransformToCenteredBox);
FTransformSRT3d ResultTransform(AABB.Center());
ResultTransform.SetScale((1.0 / ScaleFactor) * FVector3d::One());
// build spatial data and use it to find intersections
FDynamicMeshAABBTree3 Spatial(Mesh);
Spatial.SetTolerance(SnapTolerance);
MeshIntersection::FIntersectionsQueryResult Intersections = Spatial.FindAllSelfIntersections(true, IMeshSpatial::FQueryOptions(),
[this](FIntrTriangle3Triangle3d& Intr)
{
Intr.SetTolerance(SnapTolerance);
return Intr.Find();
}
);
if (Cancelled())
{
return false;
}
// cut the meshes
FMeshSelfCut Cut(Mesh);
Cut.SnapTolerance = SnapTolerance;
Cut.bTrackInsertedVertices = bCollapseDegenerateEdgesOnCut; // to collect candidates to collapse
Cut.Cut(Intersections);
if (Cancelled())
{
return false;
}
// collapse tiny edges along cut boundary
if (bCollapseDegenerateEdgesOnCut)
{
double DegenerateEdgeTolSq = DegenerateEdgeTolFactor * DegenerateEdgeTolFactor * SnapTolerance * SnapTolerance;
// convert vertex chains to edge IDs to simplify logic of finding remaining candidate edges after collapses
TArray<int> EIDs;
for (int ChainIdx = 0; ChainIdx < Cut.VertexChains.Num();)
{
int ChainLen = Cut.VertexChains[ChainIdx];
int ChainEnd = ChainIdx + 1 + ChainLen;
for (int ChainSubIdx = ChainIdx + 1; ChainSubIdx + 1 < ChainEnd; ChainSubIdx++)
{
int VID[2]{ Cut.VertexChains[ChainSubIdx], Cut.VertexChains[ChainSubIdx + 1] };
if ( DistanceSquared(Mesh->GetVertex(VID[0]), Mesh->GetVertex(VID[1])) < DegenerateEdgeTolSq)
{
EIDs.Add(Mesh->FindEdge(VID[0], VID[1]));
}
}
ChainIdx = ChainEnd;
}
TSet<int> AllEIDs(EIDs);
for (int Idx = 0; Idx < EIDs.Num(); Idx++)
{
int EID = EIDs[Idx];
if (!Mesh->IsEdge(EID))
{
continue;
}
FVector3d A, B;
Mesh->GetEdgeV(EID, A, B);
if (DistanceSquared(A, B) > DegenerateEdgeTolSq)
{
continue;
}
FIndex2i EV = Mesh->GetEdgeV(EID);
// if the vertex we'd remove is on a seam, try removing the other one instead
if (Mesh->HasAttributes() && Mesh->Attributes()->IsSeamVertex(EV.B, false))
{
Swap(EV.A, EV.B);
// if they were both on seams, then collapse should not happen? (& would break OnCollapseEdge assumptions in overlay)
if (Mesh->HasAttributes() && Mesh->Attributes()->IsSeamVertex(EV.B, false))
{
continue;
}
}
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
EMeshResult CollapseResult = Mesh->CollapseEdge(EV.A, EV.B, .5, CollapseInfo);
if (CollapseResult == EMeshResult::Ok)
{
for (int i = 0; i < 2; i++)
{
if (AllEIDs.Contains(CollapseInfo.RemovedEdges[i]))
{
int ToAdd = CollapseInfo.KeptEdges[i];
bool bWasPresent;
AllEIDs.Add(ToAdd, &bWasPresent);
if (!bWasPresent)
{
EIDs.Add(ToAdd);
}
}
}
}
}
}
if (Cancelled())
{
return false;
}
// edges that will become new boundary edges after the boolean op removes triangles on each mesh
TArray<int> CutBoundaryEdges;
// Vertices on the cut boundary that *may* not have a corresonding vertex on the other mesh
TSet<int> PossUnmatchedBdryVerts;
FMeshNormals Normals(Mesh);
Normals.ComputeTriangleNormals();
FMeshConnectedComponents ConnectedComponents(Mesh);
ConnectedComponents.FindConnectedTriangles();
TArray<int> TriToComponentID; TriToComponentID.Init(-1, Mesh->MaxTriangleID());
for (int ComponentIdx = 0; ComponentIdx < ConnectedComponents.Num(); ComponentIdx++)
{
const FMeshConnectedComponents::FComponent& Component = ConnectedComponents.GetComponent(ComponentIdx);
for (int TID : Component.Indices)
{
TriToComponentID[TID] = ComponentIdx;
}
}
// remap component IDs so they are ordered corresponding to the order of their first triangles in the mesh
TArray<int> ComponentIDRemap; ComponentIDRemap.Init(-1, ConnectedComponents.Num());
int RemapIdx = 0;
for (int TID = 0; TID < Mesh->MaxTriangleID(); TID++)
{
int ComponentIdx = TriToComponentID[TID];
if (ComponentIdx != -1 && ComponentIDRemap[ComponentIdx] == -1)
{
ComponentIDRemap[ComponentIdx] = RemapIdx++;
}
}
for (int TID = 0; TID < Mesh->MaxTriangleID(); TID++)
{
int& ComponentIdx = TriToComponentID[TID];
if (ComponentIdx > -1)
{
ComponentIdx = ComponentIDRemap[ComponentIdx];
}
}
// delete geometry according to boolean rules, tracking the boundary edges
{ // (just for scope)
// decide what triangles to delete
TArray<bool> KeepTri;
TArray<int32> DeleteIfOtherKept;
TFastWindingTree<FDynamicMesh3> Winding(&Spatial);
int MaxTriID = Mesh->MaxTriangleID();
KeepTri.SetNumUninitialized(MaxTriID);
DeleteIfOtherKept.Init(-1, MaxTriID);
ParallelFor(MaxTriID, [this, &Spatial, &Normals, &TriToComponentID, &KeepTri, &DeleteIfOtherKept, &Winding](int TID)
{
if (!Mesh->IsTriangle(TID))
{
return;
}
FVector3d Centroid = Mesh->GetTriCentroid(TID);
double WindingNum = Winding.FastWindingNumber(Centroid + Normals[TID] * NormalOffset);
bool bKeep = WindingNum < WindingThreshold; // keep if the outside of the tri is outside the shape
if (bTrimFlaps && bKeep) // trimming flaps == also check that the inside of the tri is inside the shape
{
bKeep = Winding.FastWindingNumber(Centroid - Normals[TID] * NormalOffset) > WindingThreshold;
}
// if triangle is a candidate for keeping, check for the coplanar case
if (bKeep)
{
double DSq;
int MyComponentID = TriToComponentID[TID];
IMeshSpatial::FQueryOptions QueryOptions(SnapTolerance,
[&Normals, &TriToComponentID, MyComponentID](int OtherTID)
{
// By convention, the normal for degenerate triangles is the zero vector
return !Normals[OtherTID].IsZero() && TriToComponentID[OtherTID] != MyComponentID;
}
);
int OtherTID = Spatial.FindNearestTriangle(Centroid, DSq, QueryOptions);
if (OtherTID > -1) // only consider it coplanar if there is a matching tri
{
double DotNormals = Normals[OtherTID].Dot(Normals[TID]);
//if (FMath::Abs(DotNormals) > .9) // TODO: do we actually want to check for a normal match? coplanar vertex check below is more robust?
{
// To be extra sure it's a coplanar match, check the vertices are *also* on the other connected component (w/in SnapTolerance)
FTriangle3d Tri;
Mesh->GetTriVertices(TID, Tri.V[0], Tri.V[1], Tri.V[2]);
bool bAllTrisOnOtherComponent = true;
for (int Idx = 0; Idx < 3; Idx++)
{
if (Spatial.FindNearestTriangle(Tri.V[Idx], DSq, QueryOptions) == FDynamicMesh3::InvalidID)
{
bAllTrisOnOtherComponent = false;
break;
}
}
if (bAllTrisOnOtherComponent)
{
if (DotNormals <= 0) // include zero in range to also discard degenerate triangles w/ zero normals
{
KeepTri[TID] = false;
}
else
{
// for two coplanar components with matching normals,
// just keep tris from the component with lower ID
int OtherComponentID = TriToComponentID[OtherTID];
bool bHasPriority = MyComponentID < OtherComponentID;
KeepTri[TID] = bHasPriority;
if (bHasPriority)
{
// If we kept this tri, remember the coplanar pair we expect to be deleted, in case
// it isn't deleted (e.g. because it wasn't coplanar); to then delete this one instead.
// This can help clean up sliver triangles near a cut boundary that look locally coplanar
DeleteIfOtherKept[TID] = OtherTID;
}
}
return;
}
}
}
}
// didn't already return a coplanar result; use the winding-number-based decision
KeepTri[TID] = bKeep;
});
// Don't keep coplanar tris if the matched, "lower priority" tri that we expected to delete was actually kept
for (int TID : Mesh->TriangleIndicesItr())
{
int32 DeleteIfOtherKeptTID = DeleteIfOtherKept[TID];
if (DeleteIfOtherKeptTID > -1 && KeepTri[DeleteIfOtherKeptTID])
{
KeepTri[TID] = false;
}
}
// track where we will create new boundary edges
for (int EID : Mesh->EdgeIndicesItr())
{
FIndex2i TriPair = Mesh->GetEdgeT(EID);
if (TriPair.B == IndexConstants::InvalidID || KeepTri[TriPair.A] == KeepTri[TriPair.B])
{
continue;
}
CutBoundaryEdges.Add(EID);
FIndex2i VertPair = Mesh->GetEdgeV(EID);
PossUnmatchedBdryVerts.Add(VertPair.A);
PossUnmatchedBdryVerts.Add(VertPair.B);
}
// actually delete triangles
for (int TID = 0; TID < KeepTri.Num(); TID++)
{
if (Mesh->IsTriangle(TID) && !KeepTri[TID])
{
Mesh->RemoveTriangle(TID, true, false);
}
}
}
if (Cancelled())
{
return false;
}
// Hash boundary verts for faster search
TPointHashGrid3d<int> PointHash(Mesh->GetBounds(true).MaxDim() / 64, -1);
for (int BoundaryVID : PossUnmatchedBdryVerts)
{
PointHash.InsertPointUnsafe(BoundaryVID, Mesh->GetVertex(BoundaryVID));
}
FSparseDynamicOctree3 EdgeOctree;
EdgeOctree.RootDimension = .25;
EdgeOctree.SetMaxTreeDepth(7);
auto EdgeBounds = [this](int EID)
{
FDynamicMesh3::FEdge Edge = Mesh->GetEdge(EID);
FVector3d A = Mesh->GetVertex(Edge.Vert.A);
FVector3d B = Mesh->GetVertex(Edge.Vert.B);
if (A.X > B.X)
{
Swap(A.X, B.X);
}
if (A.Y > B.Y)
{
Swap(A.Y, B.Y);
}
if (A.Z > B.Z)
{
Swap(A.Z, B.Z);
}
return FAxisAlignedBox3d(A, B);
};
auto AddEdge = [&EdgeOctree, EdgeBounds](int EID)
{
EdgeOctree.InsertObject(EID, EdgeBounds(EID));
};
auto UpdateEdge = [&EdgeOctree, EdgeBounds](int EID)
{
EdgeOctree.ReinsertObject(EID, EdgeBounds(EID));
};
for (int EID : CutBoundaryEdges)
{
AddEdge(EID);
}
TArray<int> EdgesInRange;
// mapping of all accepted correspondences of boundary vertices (both ways -- so if A is connected to B we add both A->B and B->A)
TMap<int, int> FoundMatches;
{ // for scope
double SnapToleranceSq = SnapTolerance * SnapTolerance;
TArray<int> BoundaryNbrEdges;
TArray<int> ExcludeVertices;
for (int BoundaryVID : PossUnmatchedBdryVerts)
{
// skip vertices that we've already matched up
if (FoundMatches.Contains(BoundaryVID))
{
continue;
}
FVector3d Pos = Mesh->GetVertex(BoundaryVID);
// Find a neighborhood of topologically-connected vertices, and exclude these from matching
// TODO: in theory we should walk SnapTolerance away on the connected boundary edges to build the full exclusion set
// (in practice just filtering the immediate neighbors should usually be ok?)
BoundaryNbrEdges.Reset();
ExcludeVertices.Reset();
ExcludeVertices.Add(BoundaryVID);
Mesh->GetAllVtxBoundaryEdges(BoundaryVID, BoundaryNbrEdges);
for (int EID : BoundaryNbrEdges)
{
FIndex2i EdgeVID = Mesh->GetEdgeV(EID);
ExcludeVertices.Add(EdgeVID.A == BoundaryVID ? EdgeVID.B : EdgeVID.A);
}
TPair<int, double> VIDDist = PointHash.FindNearestInRadius(
Pos, SnapTolerance,
[this, &Pos](int VID)
{
return DistanceSquared(Pos, Mesh->GetVertex(VID));
},
[&ExcludeVertices](int VID)
{
return ExcludeVertices.Contains(VID);
}
);
int NearestVID = VIDDist.Key; // ID of nearest vertex on other mesh
double DSq = VIDDist.Value; // square distance to that vertex
if (NearestVID != FDynamicMesh3::InvalidID)
{
int* Match = FoundMatches.Find(NearestVID);
if (Match)
{
double OldDSq = DistanceSquared(Mesh->GetVertex(*Match), Mesh->GetVertex(NearestVID));
if (DSq < OldDSq) // new vertex is a better match than the old one
{
int OldVID = *Match; // copy old VID out of match before updating the TMap
FoundMatches.Add(NearestVID, BoundaryVID); // new VID is recorded as best match
FoundMatches.Add(BoundaryVID, NearestVID);
FoundMatches.Remove(OldVID);
// old VID is swapped in as the one to consider as unmatched
// it will now be matched below
BoundaryVID = OldVID;
Mesh->GetAllVtxBoundaryEdges(BoundaryVID, BoundaryNbrEdges);
Pos = Mesh->GetVertex(BoundaryVID);
DSq = OldDSq;
}
NearestVID = FDynamicMesh3::InvalidID; // one of these vertices will be unmatched
}
else
{
FoundMatches.Add(NearestVID, BoundaryVID);
FoundMatches.Add(BoundaryVID, NearestVID);
}
}
// if we didn't find a valid match, try to split the nearest edge to create a match
if (NearestVID == FDynamicMesh3::InvalidID)
{
// vertex had no match -- try to split edge to match it
FAxisAlignedBox3d QueryBox(Pos, SnapTolerance);
EdgesInRange.Reset();
EdgeOctree.RangeQuery(QueryBox, EdgesInRange);
int OtherEID = FindNearestEdge(EdgesInRange, BoundaryNbrEdges, Pos);
if (OtherEID != FDynamicMesh3::InvalidID)
{
FVector3d EdgePts[2];
Mesh->GetEdgeV(OtherEID, EdgePts[0], EdgePts[1]);
// only accept the match if it's not going to create a degenerate edge -- TODO: filter already-matched edges from the FindNearestEdge query!
if (DistanceSquared(EdgePts[0], Pos) > SnapToleranceSq && DistanceSquared(EdgePts[1], Pos) > SnapToleranceSq)
{
FSegment3d Seg(EdgePts[0], EdgePts[1]);
double Along = Seg.ProjectUnitRange(Pos);
FDynamicMesh3::FEdgeSplitInfo SplitInfo;
if (ensure(EMeshResult::Ok == Mesh->SplitEdge(OtherEID, SplitInfo, Along)))
{
FoundMatches.Add(SplitInfo.NewVertex, BoundaryVID);
FoundMatches.Add(BoundaryVID, SplitInfo.NewVertex);
Mesh->SetVertex(SplitInfo.NewVertex, Pos);
CutBoundaryEdges.Add(SplitInfo.NewEdges.A);
UpdateEdge(OtherEID);
AddEdge(SplitInfo.NewEdges.A);
// Note: Do not update PossUnmatchedBdryVerts with the new vertex, because it is already matched by construction
// Likewise do not update the pointhash -- we don't want it to find vertices that were already perfectly matched
}
}
}
}
}
}
// actually snap the positions together for final matches
for (TPair<int, int>& Match : FoundMatches)
{
if (Match.Value < Match.Key)
{
checkSlow(FoundMatches[Match.Value] == Match.Key);
continue; // everything is in the map twice, so we only process the Key<Value entries
}
Mesh->SetVertex(Match.Value, Mesh->GetVertex(Match.Key));
}
if (bSimplifyAlongNewEdges)
{
SimplifyAlongNewEdges(CutBoundaryEdges, FoundMatches);
}
if (Cancelled())
{
return false;
}
bool bWeldSuccess = true;
if (bWeldSharedEdges)
{
bWeldSuccess = MergeEdges(CutBoundaryEdges, FoundMatches);
}
if (bTrackAllNewEdges)
{
for (int32 eid : CreatedBoundaryEdges)
{
AllNewEdges.Add(eid);
}
}
MeshTransforms::ApplyTransform(*Mesh, ResultTransform);
return bWeldSuccess;
}
void FMeshSelfUnion::SimplifyAlongNewEdges(TArray<int>& CutBoundaryEdges, TMap<int, int>& FoundMatches)
{
double DotTolerance = FMathd::Cos(SimplificationAngleTolerance * FMathd::DegToRad);
TSet<int> CutBoundaryEdgeSet; // set version of CutBoundaryEdges, for faster membership tests
CutBoundaryEdgeSet.Append(CutBoundaryEdges);
int NumCollapses = 0, CollapseIters = 0;
int MaxCollapseIters = 1; // TODO: is there a case where we need more iterations? Perhaps if we add some triangle quality criteria?
while (CollapseIters < MaxCollapseIters)
{
int LastNumCollapses = NumCollapses;
for (int EID : CutBoundaryEdges)
{
// this can happen if a collapse removes another cut boundary edge
// (which can happen e.g. if you have a degenerate (colinear) tri flat on the cut boundary)
if (!Mesh->IsEdge(EID))
{
continue;
}
FDynamicMesh3::FEdge Edge = Mesh->GetEdge(EID);
int Matches[2]{ -1, -1 };
bool bHasMatches = true;
for (int MatchIdx = 0; MatchIdx < 2; MatchIdx++)
{
int* Match = FoundMatches.Find(Edge.Vert[MatchIdx]);
if (Match)
{
Matches[MatchIdx] = *Match;
}
else
{
bHasMatches = false;
// TODO: if we switch to allow collapse on unmatched edges, we shouldn't break here
// b/c we may be partially matched, and need to track which is matched.
break;
}
}
if (!bHasMatches)
{
continue; // edge wasn't matched up; can't collapse it?
// TODO: consider supporting collapses in this case?
}
// if we have matched vertices, we also need a matched edge to collapse
int MatchEID = Mesh->FindEdge(Matches[0], Matches[1]);
if (MatchEID == -1)
{
continue;
}
// track whether the neighborhood of the vertex is flat (and likewise its matched pair's neighborhood, if present)
bool Flat[2]{ false, false };
// normals for each flat vertex, and each "side" (EID side and MatchEID side)
FVector3d FlatNormals[2][2]{ {FVector3d::Zero(), FVector3d::Zero()}, {FVector3d::Zero(), FVector3d::Zero()} };
int NumFlat = 0;
for (int VIdx = 0; VIdx < 2; VIdx++)
{
Flat[VIdx] = FMeshBoolean::IsFlat(*Mesh, Edge.Vert[VIdx], DotTolerance, FlatNormals[VIdx][0])
&& FMeshBoolean::IsFlat(*Mesh, Matches[VIdx], DotTolerance, FlatNormals[VIdx][1]);
if (Flat[VIdx])
{
NumFlat++;
}
}
if (NumFlat == 0)
{
continue;
}
// see if we can collapse to remove either vertex
for (int RemoveVIdx = 0; RemoveVIdx < 2; RemoveVIdx++)
{
if (!Flat[RemoveVIdx])
{
continue;
}
int KeepVIdx = 1 - RemoveVIdx;
// Note: positions are exactly the same on matched edges because snapping has already happened
FVector3d RemoveVPos = Mesh->GetVertex(Edge.Vert[RemoveVIdx]);
FVector3d KeepVPos = Mesh->GetVertex(Edge.Vert[KeepVIdx]);
FVector3d EdgeDir = KeepVPos - RemoveVPos;
if (Normalize(EdgeDir) == 0) // 0 is returned as a special case when the edge was too short to normalize
{
// collapsing degenerate edges above should prevent this
ensure(!bCollapseDegenerateEdgesOnCut);
// Just skip these edges, because in practice we generally have bCollapseDegenerateEdgesOnCut enabled
break; // break instead of continue to skip the whole edge
}
bool bHasBadEdge = false; // will be set if either mesh can't collapse the edge
for (int WhichEdge = 0; !bHasBadEdge && WhichEdge < 2; WhichEdge++) // same processing on EID and on MatchEID
{
int RemoveV = WhichEdge == 0 ? Edge.Vert[RemoveVIdx] : Matches[RemoveVIdx];
int KeepV = WhichEdge == 0 ? Edge.Vert[KeepVIdx] : Matches[KeepVIdx];
int SourceEID = WhichEdge == 0 ? EID : MatchEID;
bHasBadEdge = bHasBadEdge || FMeshBoolean::CollapseWouldHurtTriangleQuality(
*Mesh, FlatNormals[RemoveVIdx][WhichEdge], RemoveV, RemoveVPos, KeepV, KeepVPos, TryToImproveTriQualityThreshold);
bHasBadEdge = bHasBadEdge || FMeshBoolean::CollapseWouldChangeShapeOrUVs(
*Mesh, CutBoundaryEdgeSet, DotTolerance,
SourceEID, RemoveV, RemoveVPos, KeepV, KeepVPos, EdgeDir, bPreserveTriangleGroups,
true, bPreserveVertexUVs, bPreserveOverlayUVs, UVDistortTolerance * UVDistortTolerance);
}
if (bHasBadEdge)
{
continue;
}
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
int RemoveV = Edge.Vert[RemoveVIdx];
int KeepV = Edge.Vert[KeepVIdx];
EMeshResult CollapseResult = Mesh->CollapseEdge(KeepV, RemoveV, 0, CollapseInfo);
if (CollapseResult == EMeshResult::Ok)
{
int OtherRemoveV = Matches[RemoveVIdx];
int OtherKeepV = Matches[KeepVIdx];
FDynamicMesh3::FEdgeCollapseInfo OtherCollapseInfo;
EMeshResult OtherCollapseResult = Mesh->CollapseEdge(OtherKeepV, OtherRemoveV, 0, OtherCollapseInfo);
if (OtherCollapseResult != EMeshResult::Ok)
{
// if we get here, we've somehow managed to collapse the first edge but failed on the second (matched) edge
// which will leave a crack in the result unless we can somehow undo the first collapse, which would require a bunch of extra work
// but the only case where I could see this happen is if the second edge is on an isolated triangle, which means there is a hole anyway
// or if the mesh topology is somehow invalid
ensure(OtherCollapseResult == EMeshResult::Failed_CollapseTriangle);
}
else
{
FoundMatches.Remove(OtherRemoveV);
CutBoundaryEdgeSet.Remove(OtherCollapseInfo.CollapsedEdge);
CutBoundaryEdgeSet.Remove(OtherCollapseInfo.RemovedEdges[0]);
if (OtherCollapseInfo.RemovedEdges[1] != -1)
{
CutBoundaryEdgeSet.Remove(OtherCollapseInfo.RemovedEdges[1]);
}
}
NumCollapses++;
FoundMatches.Remove(RemoveV);
CutBoundaryEdgeSet.Remove(CollapseInfo.CollapsedEdge);
CutBoundaryEdgeSet.Remove(CollapseInfo.RemovedEdges[0]);
if (CollapseInfo.RemovedEdges[1] != -1)
{
CutBoundaryEdgeSet.Remove(CollapseInfo.RemovedEdges[1]);
}
}
break; // if we got through to trying to collapse the edge, don't try to collapse from the other vertex.
}
}
CutBoundaryEdges = CutBoundaryEdgeSet.Array();
if (NumCollapses == LastNumCollapses)
{
break;
}
CollapseIters++;
}
}
bool FMeshSelfUnion::MergeEdges(const TArray<int>& CutBoundaryEdges, const TMap<int, int>& FoundMatches)
{
// find "easy" match candidates using the already-made vertex correspondence
TArray<FIndex2i> CandidateMatches;
for (int EID : CutBoundaryEdges)
{
if (!ensure(Mesh->IsBoundaryEdge(EID)))
{
continue;
}
FIndex2i VIDs = Mesh->GetEdgeV(EID);
const int* OtherA = FoundMatches.Find(VIDs.A);
const int* OtherB = FoundMatches.Find(VIDs.B);
if (OtherA && OtherB)
{
int OtherEID = Mesh->FindEdge(*OtherA, *OtherB);
// because FoundMatches includes both directions of each mapping
// only accept the mapping w/ EID < OtherEID (This also excludes OtherEID == InvalidID)
if (OtherEID > EID)
{
checkSlow(OtherEID != FDynamicMesh3::InvalidID);
CandidateMatches.Add(FIndex2i(EID, OtherEID));
}
}
}
// merge the easy matches
for (FIndex2i Candidate : CandidateMatches)
{
if (!Mesh->IsEdge(Candidate.A) || !Mesh->IsBoundaryEdge(Candidate.A))
{
continue;
}
FDynamicMesh3::FMergeEdgesInfo MergeInfo;
EMeshResult EdgeMergeResult = Mesh->MergeEdges(Candidate.A, Candidate.B, MergeInfo);
if (EdgeMergeResult == EMeshResult::Ok)
{
if (bTrackAllNewEdges)
{
AllNewEdges.Add(Candidate.A);
}
}
}
// collect remaining unmatched edges
TArray<int> UnmatchedEdges;
for (int EID : CutBoundaryEdges)
{
if (Mesh->IsEdge(EID) && Mesh->IsBoundaryEdge(EID))
{
UnmatchedEdges.Add(EID);
}
}
// try to greedily match remaining edges within snap tolerance
double SnapToleranceSq = SnapTolerance * SnapTolerance;
for (int Idx = 0; Idx + 1 < UnmatchedEdges.Num(); Idx++)
{
int EID = UnmatchedEdges[Idx];
if (!Mesh->IsEdge(EID) || !Mesh->IsBoundaryEdge(EID))
{
continue;
}
FVector3d A, B;
Mesh->GetEdgeV(EID, A, B);
for (int OtherIdx = Idx + 1; OtherIdx < UnmatchedEdges.Num(); OtherIdx++)
{
int OtherEID = UnmatchedEdges[OtherIdx];
if (!Mesh->IsEdge(OtherEID) || !Mesh->IsBoundaryEdge(OtherEID))
{
UnmatchedEdges.RemoveAtSwap(OtherIdx, 1, false);
OtherIdx--;
continue;
}
FVector3d OA, OB;
Mesh->GetEdgeV(OtherEID, OA, OB);
if (DistanceSquared(OA, A) < SnapToleranceSq && DistanceSquared(OB, B) < SnapToleranceSq)
{
FDynamicMesh3::FMergeEdgesInfo MergeInfo;
EMeshResult EdgeMergeResult = Mesh->MergeEdges(EID, OtherEID, MergeInfo);
if (EdgeMergeResult == EMeshResult::Ok)
{
UnmatchedEdges.RemoveAtSwap(OtherIdx, 1, false);
if (bTrackAllNewEdges)
{
AllNewEdges.Add(EID);
}
break;
}
}
}
}
// store the failure cases
bool bAllMatched = true;
for (int EID : UnmatchedEdges)
{
if (Mesh->IsEdge(EID) && Mesh->IsBoundaryEdge(EID))
{
CreatedBoundaryEdges.Add(EID);
bAllMatched = false;
}
}
return bAllMatched;
}
int FMeshSelfUnion::FindNearestEdge(const TArray<int>& EIDs, const TArray<int>& BoundaryNbrEdges, FVector3d Pos)
{
int NearEID = FDynamicMesh3::InvalidID;
double NearSqr = SnapTolerance * SnapTolerance;
FVector3d EdgePts[2];
for (int EID : EIDs) {
if (BoundaryNbrEdges.Contains(EID))
{
continue;
}
Mesh->GetEdgeV(EID, EdgePts[0], EdgePts[1]);
FSegment3d Seg(EdgePts[0], EdgePts[1]);
double DSqr = Seg.DistanceSquared(Pos);
if (DSqr < NearSqr)
{
NearEID = EID;
NearSqr = DSqr;
}
}
return NearEID;
}