// 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 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& Triangles, TArray& 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& Triangles, TArray& 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& 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& 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& Triangles); /** * Check if current Graph has self-intersections; not optimized, only for debugging */ bool HasSelfIntersections() { for (const FDynamicGraph::FEdge e : Graph.Edges()) { TArray 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 Hits; find_intersecting_edges(A, B, Hits, Tol); // we are going to construct a list of values along segment AB TArray 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::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 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 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& 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& 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& Triangles, bool bHasBoundaryEdgeGroupID, int32 BoundaryEdgeGroupID, bool bHasHoleGroupID, int32 HoleEdgeGroupID, bool bLegacyKeepTrianglesIfBoundaryNotFound, TArray* SkippedEdges); }; } // end namespace UE::Geometry } // end namespace UE