Files
UnrealEngine/Engine/Plugins/Runtime/GeometryProcessing/Source/GeometryAlgorithms/Public/Arrangement2d.h
2025-05-18 13:04:45 +08:00

583 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// Port of geometry3Sharp Arrangement2d
#pragma once
#include "BoxTypes.h"
#include "Curve/DynamicGraph2.h"
#include "Intersection/IntrSegment2Segment2.h"
#include "Polygon2.h"
#include "Spatial/PointHashGrid2.h"
#include "Util/GridIndexing2.h"
#include "CoreMinimal.h"
namespace UE {
namespace Geometry {
/**
* Arrangement2d constructs a planar arrangement of a set of 2D line segments.
* When a segment is inserted, existing edges are split, and the inserted
* segment becomes multiple graph edges. So, the resulting FDynamicGraph2d should
* not have any edges that intersect.
*
* Calculations are performed in double-precision, so there is no guarantee
* of correctness.
*
*
* [TODO] multi-level segment has to accelerate find_intersecting_edges()
* [TODO] maybe smarter handling
*
*/
struct FArrangement2d
{
// graph of arrangement
FDynamicGraph2d Graph;
// PointHash for vertices of graph
TPointHashGrid2d<int> PointHash;
// points within this tolerance are merged
double VertexSnapTol = 0.00001;
FArrangement2d(const FAxisAlignedBox2d& BoundsHint)
: PointHash(FMath::Max(FMathd::ZeroTolerance, BoundsHint.MaxDim() / 64), -1)
{
}
FArrangement2d(double PointHashCellSize)
: PointHash(FMath::Max(FMathd::ZeroTolerance, PointHashCellSize), -1)
{
}
/**
* Attempts to triangulates the arrangement with a constrained Delaunay triangulation
* NOTE: Will return all triangles if no edges found with the BoundaryEdgeGroupID
* NOTE: May fail if arrangement has self-intersections
*
* Triangles: Output triangles (as indices into Graph vertices)
* SkippedEdges: Output indices of edges that the algorithm failed to insert
* BoundaryEdgeGroupID: ID of edges corresponding to a boundary; if we have a closed loop of these boundary edges on output triangulation, will discard triangles outside this
* return: false if triangulation algo knows it failed (note Triangles may still have some triangulation of the input in this case; for example it may just be missing some required edges)
*/
UE_DEPRECATED(5.1, "Please use the Triangulate or TriangulateWithBoundary functions instead, which are explicit about whether a BoundaryEdgeGroupID should be present")
bool GEOMETRYALGORITHMS_API AttemptTriangulate(TArray<FIndex3i>& Triangles, TArray<int32>& SkippedEdges, int32 BoundaryEdgeGroupID);
// Variant of AttemptTriangulate using FIntVector instead of FIndex3i; Note this incurs an extra copy of the triangle array
UE_DEPRECATED(5.1, "Please use the Triangulate or TriangulateWithBoundary functions instead, which are explicit about whether a BoundaryEdgeGroupID should be present")
bool GEOMETRYALGORITHMS_API AttemptTriangulate(TArray<FIntVector>& Triangles, TArray<int32>& SkippedEdges, int32 BoundaryEdgeGroupID);
/**
* Attempts to triangulate the arrangement with a constrained Delaunay triangulation
* NOTE: May fail if arrangement has self-intersections
*
* @param Triangles Output triangles (as indices into Graph vertices)
* @param BoundaryEdgeGroupID ID of edges corresponding to a boundary: triangles outside these edges will be removed
* @param HoleEdgeGroupID ID of edges corresponding to internal holes: triangles inside these edges will be removed
* @return false if triangulation algo knows it failed; will still likely have some triangulation even in this case
*/
bool GEOMETRYALGORITHMS_API TriangulateWithBoundaryAndHoles(TArray<FIndex3i>& Triangles, int32 BoundaryEdgeGroupID, int32 HoleEdgeGroupID);
/**
* Attempts to triangulate the arrangement with a constrained Delaunay triangulation
* NOTE: May fail if arrangement has self-intersections
*
* @param Triangles Output triangles (as indices into Graph vertices)
* @param BoundaryEdgeGroupID ID of edges corresponding to a boundary: triangles outside these edges will be removed
* @return false if triangulation algo knows it failed; will still likely have some triangulation even in this case
*/
bool GEOMETRYALGORITHMS_API TriangulateWithBoundary(TArray<FIndex3i>& Triangles, int32 BoundaryEdgeGroupID);
/**
* Attempts to triangulate the arrangement with a constrained Delaunay triangulation
* NOTE: May fail if arrangement has self-intersections
*
* @param Triangles Output triangles (as indices into Graph vertices)
* @return false if triangulation algo knows it failed; will still likely have some triangulation even in this case
*/
bool GEOMETRYALGORITHMS_API Triangulate(TArray<FIndex3i>& Triangles);
/**
* Check if current Graph has self-intersections; not optimized, only for debugging
*/
bool HasSelfIntersections()
{
for (const FDynamicGraph::FEdge e : Graph.Edges())
{
TArray<FIntersection> Hits;
int HitCount = find_intersecting_edges(Graph.GetVertex(e.A), Graph.GetVertex(e.B), Hits, 0.0);
for (const FIntersection& Intersect : Hits)
{
FDynamicGraph::FEdge o = Graph.GetEdge(Intersect.EID);
if (o.A != e.A && o.A != e.B && o.B != e.A && o.B != e.B)
{
return true;
}
}
}
return false;
}
/**
* Subdivide edge at a given position
*/
FIndex2i SplitEdgeAtPoint(int EdgeID, FVector2d Point)
{
FDynamicGraph2d::FEdgeSplitInfo splitInfo;
EMeshResult result = Graph.SplitEdge(EdgeID, splitInfo);
ensureMsgf(result == EMeshResult::Ok, TEXT("SplitEdgeAtPoint: edge split failed?"));
Graph.SetVertex(splitInfo.VNew, Point);
PointHash.InsertPointUnsafe(splitInfo.VNew, Point);
return FIndex2i(splitInfo.VNew, splitInfo.ENewBN);
}
/**
* Check if vertex exists in region
*/
bool HasVertexNear(FVector2d Point, double SearchRadius)
{
return find_nearest_vertex(Point, SearchRadius) > -1;
}
/**
* Insert isolated point P into the arrangement
*/
int Insert(const FVector2d& Pt)
{
return insert_point(Pt, VertexSnapTol);
}
/**
* Insert isolated point P into the arrangement when you know by construction it's not too close to any vertex or edge
* Much faster, but will break things if you use it to insert a point that is on top of any existing element!
*/
int32 InsertNewIsolatedPointUnsafe(const FVector2d& Pt)
{
int32 VID = Graph.AppendVertex(Pt);
PointHash.InsertPointUnsafe(VID, Pt);
return VID;
}
/**
* insert segment [A,B] into the arrangement
*/
void Insert(const FVector2d& A, const FVector2d& B, int GID = -1)
{
insert_segment(A, B, GID, VertexSnapTol);
}
/**
* insert segment into the arrangement
*/
void Insert(const FSegment2d& Segment, int GID = -1)
{
insert_segment(Segment.StartPoint(), Segment.EndPoint(), GID, VertexSnapTol);
}
///**
// * sequentially insert segments of polyline
// */
//void Insert(PolyLine2d pline, int GID = -1)
//{
// int N = pline.VertexCount - 1;
// for (int i = 0; i < N; ++i) {
// FVector2d A = pline[i];
// FVector2d B = pline[i + 1];
// insert_segment(A, B, GID);
// }
//}
///**
// * sequentially insert segments of polygon
// */
void Insert(const FPolygon2d& Poly, int GID = -1)
{
int N = Poly.VertexCount();
for (int i = 0; i < N; ++i)
{
insert_segment(Poly[i], Poly[(i + 1) % N], GID, VertexSnapTol);
}
}
/*
* Graph improvement
*/
/**
* connect open boundary vertices within DistThresh, by inserting new segments
*/
void ConnectOpenBoundaries(double DistThresh)
{
int max_vid = Graph.MaxVertexID();
for (int VID = 0; VID < max_vid; ++VID)
{
if (Graph.IsBoundaryVertex(VID) == false)
{
continue;
}
FVector2d v = Graph.GetVertex(VID);
int snap_with = find_nearest_boundary_vertex(v, DistThresh, VID);
if (snap_with != -1)
{
FVector2d v2 = Graph.GetVertex(snap_with);
Insert(v, v2);
}
}
}
protected:
struct FSegmentPoint
{
double T;
int VID;
};
/**
* insert pt P into the arrangement, splitting existing edges as necessary
*/
int insert_point(const FVector2d& P, double Tol = 0)
{
int PIdx = find_existing_vertex(P);
if (PIdx > -1)
{
return -1;
}
// TODO: currently this tries to add the vertex on the closest edge below tolerance; we should instead insert at *every* edge below tolerance! ... but that is more inconvenient to write
FVector2d x = FVector2d::Zero(), y = FVector2d::Zero();
double ClosestDistSq = Tol*Tol;
int FoundEdgeToSplit = -1;
for (int EID = 0, ExistingEdgeMax = Graph.MaxEdgeID(); EID < ExistingEdgeMax; EID++)
{
if (!Graph.IsEdge(EID))
{
continue;
}
Graph.GetEdgeV(EID, x, y);
FSegment2d Seg(x, y);
double DistSq = Seg.DistanceSquared(P);
if (DistSq < ClosestDistSq)
{
ClosestDistSq = DistSq;
FoundEdgeToSplit = EID;
}
}
if (FoundEdgeToSplit > -1)
{
FDynamicGraph2d::FEdgeSplitInfo splitInfo;
EMeshResult result = Graph.SplitEdge(FoundEdgeToSplit, splitInfo);
ensureMsgf(result == EMeshResult::Ok, TEXT("insert_into_segment: edge split failed?"));
Graph.SetVertex(splitInfo.VNew, P);
PointHash.InsertPointUnsafe(splitInfo.VNew, P);
return splitInfo.VNew;
}
int VID = Graph.AppendVertex(P);
PointHash.InsertPointUnsafe(VID, P);
return VID;
}
/**
* insert edge [A,B] into the arrangement, splitting existing edges as necessary
*/
bool insert_segment(FVector2d A, FVector2d B, int GID = -1, double Tol = 0)
{
// handle degenerate edges
int a_idx = find_existing_vertex(A);
int b_idx = find_existing_vertex(B);
if (a_idx == b_idx && a_idx >= 0)
{
return false;
}
// snap input vertices
if (a_idx >= 0)
{
A = Graph.GetVertex(a_idx);
}
if (b_idx >= 0)
{
B = Graph.GetVertex(b_idx);
}
// handle tiny-segment case
double SegLenSq = DistanceSquared( A, B );
if (SegLenSq <= VertexSnapTol*VertexSnapTol)
{
// seg is too short and was already on an existing vertex; just consider that vertex to be the inserted segment
if (a_idx >= 0 || b_idx >= 0)
{
return false;
}
// seg is too short and wasn't on an existing vertex; add it as an isolated vertex
return insert_point(A, Tol) != -1;
}
// ok find all intersections
TArray<FIntersection> Hits;
find_intersecting_edges(A, B, Hits, Tol);
// we are going to construct a list of <T,vertex_id> values along segment AB
TArray<FSegmentPoint> points;
FSegment2d segAB = FSegment2d(A, B);
find_intersecting_floating_vertices(segAB, a_idx, b_idx, points, Tol);
// insert intersections into existing segments
for (int i = 0, N = Hits.Num(); i < N; ++i)
{
FIntersection Intr = Hits[i];
int EID = Intr.EID;
double t0 = Intr.Intr.Parameter0, t1 = Intr.Intr.Parameter1;
// insert first point at t0
int new_eid = -1;
if (Intr.Intr.Type == EIntersectionType::Point || Intr.Intr.Type == EIntersectionType::Segment)
{
FIndex2i new_info = split_segment_at_t(EID, t0, VertexSnapTol);
new_eid = new_info.B;
FVector2d v = Graph.GetVertex(new_info.A);
points.Add(FSegmentPoint{segAB.Project(v), new_info.A});
}
// if intersection was on-segment, then we have a second point at t1
if (Intr.Intr.Type == EIntersectionType::Segment)
{
if (new_eid == -1)
{
// did not actually split edge for t0, so we can still use EID
FIndex2i new_info = split_segment_at_t(EID, t1, VertexSnapTol);
FVector2d v = Graph.GetVertex(new_info.A);
points.Add(FSegmentPoint{segAB.Project(v), new_info.A});
}
else
{
// find t1 was in EID, rebuild in new_eid
FSegment2d new_seg = Graph.GetEdgeSegment(new_eid);
FVector2d p1 = Intr.Intr.GetSegment1().PointAt(t1);
double new_t1 = new_seg.Project(p1);
// note: new_t1 may be outside of new_seg due to snapping; in this case the segment will just not be split
FIndex2i new_info = split_segment_at_t(new_eid, new_t1, VertexSnapTol);
FVector2d v = Graph.GetVertex(new_info.A);
points.Add(FSegmentPoint{segAB.Project(v), new_info.A});
}
}
}
// find or create start and end points
if (a_idx == -1)
{
a_idx = find_existing_vertex(A);
}
if (a_idx == -1)
{
a_idx = Graph.AppendVertex(A);
PointHash.InsertPointUnsafe(a_idx, A);
}
if (b_idx == -1)
{
b_idx = find_existing_vertex(B);
}
if (b_idx == -1)
{
b_idx = Graph.AppendVertex(B);
PointHash.InsertPointUnsafe(b_idx, B);
}
// add start/end to points list. These may be duplicates but we will sort that out after
points.Add(FSegmentPoint{-segAB.Extent, a_idx});
points.Add(FSegmentPoint{segAB.Extent, b_idx});
// sort by T
points.Sort([](const FSegmentPoint& pa, const FSegmentPoint& pb) { return pa.T < pb.T; });
// connect sequential points, as long as they aren't the same point,
// and the segment doesn't already exist
for (int k = 0; k < points.Num() - 1; ++k)
{
int v0 = points[k].VID;
int v1 = points[k + 1].VID;
if (v0 == v1)
{
continue;
}
if (Graph.FindEdge(v0, v1) == FDynamicGraph2d::InvalidID)
{
// sanity check; technically this can happen and still be correct but it's more likely an error case
ensureMsgf(FMath::Abs(points[k].T - points[k + 1].T) >= std::numeric_limits<float>::epsilon(), TEXT("insert_segment: different points have same T??"));
Graph.AppendEdge(v0, v1, GID);
}
}
return true;
}
/**
* insert new point into segment EID at parameter value T
* If T is within Tol of endpoint of segment, we use that instead.
*/
FIndex2i split_segment_at_t(int EID, double T, double Tol)
{
FIndex2i ev = Graph.GetEdgeV(EID);
FSegment2d seg = FSegment2d(Graph.GetVertex(ev.A), Graph.GetVertex(ev.B));
int use_vid = -1;
int new_eid = -1;
if (T < -(seg.Extent - Tol))
{
use_vid = ev.A;
}
else if (T > (seg.Extent - Tol))
{
use_vid = ev.B;
}
else
{
FVector2d Pt = seg.PointAt(T);
FDynamicGraph2d::FEdgeSplitInfo splitInfo;
EMeshResult result;
int CrossingVert = find_existing_vertex(Pt);
if (CrossingVert == -1)
{
result = Graph.SplitEdge(EID, splitInfo);
}
else
{
result = Graph.SplitEdgeWithExistingVertex(EID, CrossingVert, splitInfo);
}
ensureMsgf(result == EMeshResult::Ok, TEXT("insert_into_segment: edge split failed?"));
use_vid = splitInfo.VNew;
new_eid = splitInfo.ENewBN;
if (CrossingVert == -1)
{ // position + track added vertex
Graph.SetVertex(use_vid, Pt);
PointHash.InsertPointUnsafe(splitInfo.VNew, Pt);
}
}
return FIndex2i(use_vid, new_eid);
}
/**
* find existing vertex at point, if it exists
*/
int find_existing_vertex(FVector2d Pt)
{
return find_nearest_vertex(Pt, VertexSnapTol);
}
/**
* find closest vertex, within SearchRadius
*/
int find_nearest_vertex(FVector2d Pt, double SearchRadius, int IgnoreVID = -1)
{
auto FuncDistSq = [&](int B) { return DistanceSquared(Pt, Graph.GetVertex(B)); };
auto FuncIgnore = [&](int VID) { return VID == IgnoreVID; };
TPair<int, double> found = (IgnoreVID == -1) ? PointHash.FindNearestInRadius(Pt, SearchRadius, FuncDistSq)
: PointHash.FindNearestInRadius(Pt, SearchRadius, FuncDistSq, FuncIgnore);
if (found.Key == PointHash.GetInvalidValue())
{
return -1;
}
return found.Key;
}
/**
* find nearest boundary vertex, within SearchRadius
*/
int find_nearest_boundary_vertex(FVector2d Pt, double SearchRadius, int IgnoreVID = -1)
{
auto FuncDistSq = [&](int B) { return DistanceSquared(Pt, Graph.GetVertex(B)); };
auto FuncIgnore = [&](int VID) { return Graph.IsBoundaryVertex(VID) == false || VID == IgnoreVID; };
TPair<int, double> found =
PointHash.FindNearestInRadius(Pt, SearchRadius, FuncDistSq, FuncIgnore);
if (found.Key == PointHash.GetInvalidValue())
{
return -1;
}
return found.Key;
}
struct FIntersection
{
int EID;
int SideX;
int SideY;
FIntrSegment2Segment2d Intr;
};
/**
* find set of edges in graph that intersect with edge [A,B]
*/
bool find_intersecting_edges(FVector2d A, FVector2d B, TArray<FIntersection>& Hits, double Tol = 0)
{
int num_hits = 0;
FVector2d x = FVector2d::Zero(), y = FVector2d::Zero();
FVector2d EPerp = PerpCW(B - A);
Normalize(EPerp);
for (int EID : Graph.EdgeIndices())
{
Graph.GetEdgeV(EID, x, y);
// inlined version of WhichSide with pre-normalized EPerp, to ensure Tolerance is consistent for different edge lengths
double SignX = EPerp.Dot(x - A);
double SignY = EPerp.Dot(y - A);
int SideX = (SignX > Tol ? +1 : (SignX < -Tol ? -1 : 0));
int SideY = (SignY > Tol ? +1 : (SignY < -Tol ? -1 : 0));
if (SideX == SideY && SideX != 0)
{
continue; // both pts on same side
}
FIntrSegment2Segment2d Intr(FSegment2d(x, y), FSegment2d(A, B));
Intr.SetIntervalThreshold(Tol);
// set a loose DotThreshold as well so almost-parallel segments are treated as parallel;
// otherwise we're more likely to hit later problems when an edge intersects near-overlapping edges at almost the same point
// (TODO: detect + handle that case!)
Intr.SetDotThreshold(1e-4);
if (Intr.Find())
{
Hits.Add(FIntersection{EID, SideX, SideY, Intr});
num_hits++;
}
}
return (num_hits > 0);
}
bool find_intersecting_floating_vertices(const FSegment2d &SegAB, int32 AID, int32 BID, TArray<FSegmentPoint>& Hits, double Tol = 0)
{
int num_hits = 0;
for (int VID : Graph.VertexIndices())
{
if (Graph.GetVtxEdgeCount(VID) > 0 || VID == AID || VID == BID) // if it's an existing edge or on the currently added edge, it's not floating so skip it
{
continue;
}
FVector2d V = Graph.GetVertex(VID);
double T;
double DSQ = SegAB.DistanceSquared(V, T);
if (DSQ < Tol*Tol)
{
Hits.Add(FSegmentPoint{ T, VID });
num_hits++;
}
}
return num_hits > 0;
}
private:
// Full-featured implementation for all Triangulate function variants to call
bool GEOMETRYALGORITHMS_API TriangulateInternal(TArray<FIndex3i>& Triangles, bool bHasBoundaryEdgeGroupID, int32 BoundaryEdgeGroupID, bool bHasHoleGroupID, int32 HoleEdgeGroupID, bool bLegacyKeepTrianglesIfBoundaryNotFound, TArray<int32>* SkippedEdges);
};
} // end namespace UE::Geometry
} // end namespace UE