/// <summary> /// Checks if the other group's outer perimeter is inside this group's perimeter. Inclusive of the two groups /// sharing vertices. /// </summary> /// <param name="otherGroup"></param> /// <returns>True if the other group is inside this group, false otherwise.</returns> public bool IsOtherGroupInThisGroup(ConnectedNodeGroup otherGroup) { HashSet <Vector2> sharedVertices = this.GetSharedVertices(otherGroup); if (sharedVertices.Count == 0) { return(GeometryFuncs.IsPolyInPoly(otherGroup.outerPerimSimplified, this.outerPerimSimplified)); } else { bool isInside = true; foreach (PolygonSplittingGraphNode node in otherGroup.nodes.Values) { var nodeCoord = new Vector2(node.x, node.y); if (sharedVertices.Contains(nodeCoord)) { continue; } if (!GeometryFuncs.IsPointInPoly(nodeCoord, this.outerPerimSimplified)) { isInside = false; break; } } return(isInside); } }
/// <summary> /// Use <param>chords</param> to construct Bipartite Graph Nodes and create a dictionary that maps Node -> Chords /// (so we can easily get which chords were 'selected' from the Max Independent Set later). /// </summary> /// <param name="chords">List of chords in polygon.</param> /// <returns>A dictionary that maps Bipartite Graph Nodes to Chords.</returns> private static Dictionary <BipartiteGraphNode, Chord> _ConvertChordsToNodes(IReadOnlyList <Chord> chords) { var bipartiteNodeToChords = new Dictionary <BipartiteGraphNode, Chord>(); for (int i = 0; i < chords.Count; i++) { Chord chord = chords[i]; var connectedNodeIDs = new List <int>(); for (int j = 0; j < chords.Count; j++) { Chord comparisonChord = chords[j]; if (j == i || comparisonChord.direction == chord.direction) { continue; } if (GeometryFuncs.DoSegmentsIntersect(chord.a, chord.b, comparisonChord.a, comparisonChord.b)) { //chord B is connected to chord A IFF they intersect, B != A, and they have different orientations connectedNodeIDs.Add(j); } } BipartiteGraphNode.BipartiteSide side = (chord.direction == Chord.Direction.Vertical) ? BipartiteGraphNode.BipartiteSide.Left : BipartiteGraphNode.BipartiteSide.Right; var bipartiteGraphNode = new BipartiteGraphNode(i, connectedNodeIDs, side); bipartiteNodeToChords.Add(bipartiteGraphNode, chord); } return(bipartiteNodeToChords); }
/// <summary> /// Iterates through the input <param>perimeter</param> from the vertex with the min X and min Y coordinates (which /// is ALWAYS convex) in CCW order. Three vertices are needed to check whether the middle vertex is concave or not, /// AKA some sequence of vertices P, Q, and R can be used to determine Q given that the vertices are NOT collinear. /// Q is concave IF the midpoint between P and R is WITHIN the polygon, AKA: /// * If the perimeter being checked is not a hole (AKA it is the polygon's outer perim), then P->R's midpoint /// must be inside the polygon for Q to be concave. /// * If the perimeter being checked IS a hole, then P->R's midpoint must NOT be inside the hole for Q to be /// concave. /// </summary> /// <param name="perimeter">Perimeter of either polygon or its holes.</param> /// <returns>List of concave vertices.</returns> private static HashSet <ConcaveVertex> _TracePerimeterForConcaveVertices(IReadOnlyList <Vector2> perimeter, bool hole = false) { var concaveVertices = new HashSet <ConcaveVertex>(); int startID = GeometryFuncs.GetMinXMinYCoord(perimeter); for (int i = 0; i < perimeter.Count - 1; i++) //Count - 1 because of closed loop convention { //as a side note, perim is always ordered CCW thanks to EdgeCollection's simplification algorithm. int thisIndex = (i + startID); Vector2 thisVertex = perimeter[thisIndex % (perimeter.Count - 1)]; Vector2 prevVertex = perimeter[(thisIndex - 1 + perimeter.Count - 1) % (perimeter.Count - 1)]; Vector2 nextVertex = perimeter[(thisIndex + 1) % (perimeter.Count - 1)]; float angle = Mathf.Rad2Deg(Mathf.Atan2(thisVertex.y - prevVertex.y, thisVertex.x - prevVertex.x) - Mathf.Atan2(nextVertex.y - thisVertex.y, nextVertex.x - thisVertex.x)); if (angle < 0) { angle += 360; } if (angle == 270 && hole) { //hole with 90 degree angle would be 270 on the other side .'. concave concaveVertices.Add(new ConcaveVertex(thisVertex, prevVertex, nextVertex)); } else if (angle == 90 && !hole) { //DID you know that if you order polygons CCW then they have negative area via shoelace formula and their angles are measured from the 'outside' and somehow i didn't put two and two together and figure that out because i cannot understand math concaveVertices.Add(new ConcaveVertex(thisVertex, prevVertex, nextVertex)); } } return(concaveVertices); }
public ChordlessPolygon(Vector2[] outerPerim, List <Vector2>[] potentialHoles, Dictionary <Vector2, HashSet <Vector2> > bridges, bool isHole) { if (outerPerim is null) { throw new ArgumentNullException(nameof(outerPerim)); } if (potentialHoles is null) { throw new ArgumentNullException(nameof(potentialHoles)); } this.isHole = isHole; this._outerPerimUnsimplified = outerPerim; this._outerPerim = _SimplifyOuterPerim(outerPerim); this._holes = !this.isHole ? this._GetContainedHoles(potentialHoles) : System.Array.Empty <List <Vector2> >(); this._bridges = bridges; if (!GeometryFuncs.IsPolygonCCW(this._outerPerim)) { var reversePerim = this.outerPerim.ToList(); reversePerim.Reverse(); this._outerPerim = reversePerim.ToArray(); } foreach (List <Vector2> hole in this._holes) { if (!GeometryFuncs.IsPolygonCCW(hole.ToArray())) { hole.Reverse(); } } }
/// <summary> /// Checks which potential holes could be contained within this polygon and adds them /// to holes if they pass the IsPolyInPoly check. /// </summary> /// <param name="potentialHoles"></param> /// <returns></returns> private List <Vector2>[] _GetContainedHoles(List <Vector2>[] potentialHoles) { var confirmedHoles = new List <List <Vector2> >(); foreach (List <Vector2> hole in potentialHoles) { HashSet <Vector2> sharedVertices; if (GeometryFuncs.IsPolyInPoly(hole.ToArray(), this._outerPerim)) { confirmedHoles.Add(hole); } else if ((sharedVertices = this._GetHoleSharedVertices(hole)).Count > 0) { int holeVerticesInPoly = 0; //guilty until proven innocent, to prevent snake poly from containing hole foreach (Vector2 holeVertex in hole) { if (sharedVertices.Contains(holeVertex)) { continue; } if (GeometryFuncs.IsPointInPoly(holeVertex, this._outerPerim) && !GeometryFuncs.IsPointOnPolyBoundary(holeVertex, this._outerPerim)) { holeVerticesInPoly++; } } if (holeVerticesInPoly > 0) { confirmedHoles.Add(hole); } } } return(confirmedHoles.ToArray()); }
/// <summary> /// Cut down the input overextension coordinate to the closest edge. Checks edges from: /// 1. The polygon's perimeter /// 2. The polygon's holes perimeters' /// 3. Extensions that already exist /// </summary> /// <param name="polygon">Input polygon.</param> /// <param name="extensions">Extension edges that already exist.</param> /// <param name="origin">Concave vertex being extended.</param> /// <param name="overextension">Vector2 representing the other side of the extension that is currently too far /// and needs to be cut down.</param> /// <returns>A vector2 representing the other side of where the extension should be, AKA the first (closest) edge it /// hits.</returns> private static Vector2 _GetCutExtension(ChordlessPolygon polygon, List <PolyEdge> extensions, Vector2 origin, Vector2 overextension) { Vector2 cutExtension = overextension; for (int i = 0; i < polygon.outerPerim.Length - 1; i++) { Vector2 perimCoordA = polygon.outerPerim[i]; Vector2 perimCoordB = polygon.outerPerim[i + 1]; if (GeometryFuncs.AreSegmentsParallel(origin, overextension, perimCoordA, perimCoordB) || perimCoordA == origin || perimCoordB == origin) { continue; } Vector2 potentialCutExtension = _CutExtensionWithSegment(origin, overextension, perimCoordA, perimCoordB); if (origin.DistanceSquaredTo(potentialCutExtension) < origin.DistanceSquaredTo(cutExtension)) { cutExtension = potentialCutExtension; } } foreach (ImmutableList <Vector2> hole in polygon.holes) { for (int i = 0; i < hole.Count - 1; i++) { Vector2 perimCoordA = hole[i]; Vector2 perimCoordB = hole[i + 1]; if (GeometryFuncs.AreSegmentsParallel(origin, overextension, perimCoordA, perimCoordB) || perimCoordA == origin || perimCoordB == origin) { continue; } Vector2 potentialCutExtension = _CutExtensionWithSegment(origin, overextension, perimCoordA, perimCoordB); if (origin.DistanceSquaredTo(potentialCutExtension) < origin.DistanceSquaredTo(cutExtension)) { cutExtension = potentialCutExtension; } } } foreach (PolyEdge edge in extensions) { Vector2 extCoordA = edge.a; Vector2 extCoordB = edge.b; if (GeometryFuncs.AreSegmentsParallel(origin, overextension, extCoordA, extCoordB) || extCoordA == origin || extCoordB == origin) { continue; } Vector2 potentialCutExtension = _CutExtensionWithSegment(origin, overextension, extCoordA, extCoordB); if (origin.DistanceSquaredTo(potentialCutExtension) < origin.DistanceSquaredTo(cutExtension)) { cutExtension = potentialCutExtension; } } return(cutExtension); }
/// <summary> /// Checks if a segment between two vertexes is a valid chord, which relies on the following conditions: /// 0. The segment is not in the chords array already (with/without reversed points) /// 1. The vertices are different /// 2. The segment is vertical or horizontal /// 3. The segment does not CONTAIN a part of the polygon's outer perimeter OR hole perimeter(s). /// 4 WHICH I FORGOT. The segment does not intersect any part of the perimeter /// 5 WHICH I ALSO FORGOT. The segment is actually within the polygon. /// </summary> /// <param name="pointA"></param> /// <param name="pointB"></param> /// <param name="allIsoPerims"></param> /// <param name="chords"></param> /// <returns></returns> private static bool _IsChordValid(Vector2 pointA, Vector2 pointB, List <Vector2>[] allIsoPerims, IEnumerable <Chord> chords) { if (chords.Any(chord => (chord.a == pointA && chord.b == pointB) || (chord.a == pointB && chord.b == pointA))) { //if not already in chords array return(false); } if (pointA == pointB) { return(false); //if vertices are different } if (pointA.x != pointB.x && pointA.y != pointB.y) { return(false); //if the segment is vertical or horizontal } Vector2 midpoint = (pointB - pointA) / 2 + pointA; for (int i = 0; i < allIsoPerims.Length; i++) { List <Vector2> perims = allIsoPerims[i]; if (i == 0) { //midpoint not in poly if (!GeometryFuncs.IsPointInPoly(midpoint, perims.ToArray())) { return(false); } } else { //midpoint in hole if (GeometryFuncs.IsPointInPoly(midpoint, perims.ToArray())) { return(false); } } for (int j = 0; j < perims.Count - 1; j++) //i < perims.Count - 1 because perims[0] = perims[last] { //if segment does not contain a part of the polygon's perimeter(s) Vector2 perimVertexA = perims[j]; Vector2 perimVertexB = perims[j + 1]; if (GeometryFuncs.DoSegmentsOverlap(pointA, pointB, perimVertexA, perimVertexB)) { //segment confirmed to contain part of polygon's perimeter(s) return(false); } if (perimVertexA != pointA && perimVertexA != pointB && perimVertexB != pointA && perimVertexB != pointB) { if (GeometryFuncs.DoSegmentsIntersect(pointA, pointB, perimVertexA, perimVertexB)) { //segment intersects part of perimeter return(false); } } } } return(true); }
/// <summary> /// Checks if the input <param>cyclePerim</param> contains any vertices in <param>nodeGroup</param> within itself /// that are NOT part of its perimeter. /// </summary> /// <param name="cyclePerim">A planar face found within <param>connectedGroup</param>, which has a planar embedding.</param> /// <param name="groupNodesPerim">A group of nodes that form the perimeter of the ConnectedNodeGroup <param>cycle</param> /// was extracted from.</param> /// <returns></returns> public static bool IsCycleOuterPerim(Vector2[] cyclePerim, List <PolygonSplittingGraphNode> groupNodesPerim) { if (groupNodesPerim is null) { throw new ArgumentNullException(nameof(groupNodesPerim)); } var groupPerim = new Vector2[groupNodesPerim.Count]; for (int i = 0; i < groupNodesPerim.Count; i++) { groupPerim[i] = new Vector2(groupNodesPerim[i].x, groupNodesPerim[i].y); } return(GeometryFuncs.ArePolysIdentical(cyclePerim, groupPerim)); }
/// <summary> /// Decomposes a complex polygon described by the input <param>allIsoPerims</param> into the minimum number of /// rectangles (actually a lie, decomposes them into chordless polygons, which is decomposed into rectangles in /// another class). /// </summary> /// <param name="allIsoPerims">Array of lists of Vector2s. Each list describes a perimeter, whether it be /// the complex polygon's outer perimeters or holes (the 0'th index is always the outer perimeter).</param> /// <returns>A list of lists of Vector2s. Each list describes a rectangle in coordinates that follow the /// isometric axis (and need to be converted back to the cartesian axis elsewhere).</returns> // ReSharper disable once ReturnTypeCanBeEnumerable.Global public static (List <Chord>, List <ChordlessPolygon>) DecomposeComplexPolygonToRectangles(this List <Vector2>[] allIsoPerims) { if (allIsoPerims is null) { throw new ArgumentNullException(nameof(allIsoPerims)); } foreach (List <Vector2> perim in allIsoPerims) { if (!GeometryFuncs.IsPolygonCCW(perim.ToArray())) { perim.Reverse(); } } (List <Chord> chords, List <ChordlessPolygon> chordlessPolygons) = _ComplexToChordlessPolygons(allIsoPerims); return(chords, chordlessPolygons); }
/// <summary> /// Given some segment represented by <param>segmentA</param> and <param>segmentB</param> that is not parallel to /// the segment represented by <param>origin</param> and <param>overextension</param> and if necessary, cuts down /// the overextension to the segment IFF the segment cuts origin->overextension. /// If the two input segments do not intersect just returns overextension. /// </summary> /// <param name="origin">Concave vertex being extended.</param> /// <param name="overextension">Extension that should reach outside the polygon.</param> /// <param name="segmentA"></param> /// <param name="segmentB">Segment that is being used to cut the overextension.</param> /// <returns>A coordinate between origin and overextension closer to the origin IFF segment cuts origin->overextension, /// or overextension if the segment does not intersect origin->overextension.</returns> private static Vector2 _CutExtensionWithSegment(Vector2 origin, Vector2 overextension, Vector2 segmentA, Vector2 segmentB) { if (!GeometryFuncs.DoSegmentsIntersect(origin, overextension, segmentA, segmentB)) { return(overextension); } Vector2 cutExtension = overextension; if (origin.x == overextension.x) { //VERTICAL cutExtension.y = segmentA.y; } else if (origin.y == overextension.y) { //HORIZONTAL cutExtension.x = segmentA.x; } return(cutExtension); }
/// <summary> /// Searches for vertices in <param>extensionVertices</param> and returns a HashSet containing all of them that are /// between <param>thisVertex</param> and <param>nextVertex</param>. /// </summary> /// <param name="thisVertex"></param> /// <param name="nextVertex"></param> /// <param name="extensionVertices"></param> /// <returns>HashSet containing all vertices in <param>extensionVertices</param> between <param>thisVertex</param> /// and <param>nextVertex</param>.</returns> private static List <Vector2> _GetVerticesInBetween(Vector2 thisVertex, Vector2 nextVertex, HashSet <Vector2> extensionVertices) { var verticesInBetween = new List <Vector2>(); foreach (Vector2 vertex in extensionVertices) { if (vertex == thisVertex || vertex == nextVertex) { continue; } if (!GeometryFuncs.AreSegmentsCollinear(thisVertex, vertex, vertex, nextVertex)) { continue; } if ((thisVertex.x - vertex.x) * (nextVertex.x - vertex.x) <= 0 && (thisVertex.y - vertex.y) * (nextVertex.y - vertex.y) <= 0) { verticesInBetween.Add(vertex); } } return(verticesInBetween); }
/// <summary> /// Partitions a connected node group by: /// 1. Repeatedly running BFS on its nodes to find cycles and removing edges for the next BFS until no more /// cycles can be found. /// 1.1 Note that the BFS only runs on nodes with two or less VALID EDGES. Edges are only removed IFF either /// of its vertices has two or less valid edges. This ensures we do not remove an edge that could be used /// twice, for example two adjacent cycles that share an edge. /// 2. The cycle is added to a list of perimeters (AKA a list of nodes) IFF it is NOT a hole. /// </summary> /// <param name="connectedGroup">ConnectedNodeGroup that is being partitioned.</param> /// <param name="idsContainedInGroup">The IDs of the ConnectedNodeGroups contained within <param>connectedGroup</param></param> /// <param name="connectedGroups">List of all ConnectedNodeGroups.</param> /// <param name="nodes">List of all nodes.</param> /// <param name="holes">Holes of the polygon that was made into a PolygonSplittingGraph.</param> /// <returns></returns> public static List <ChordlessPolygon> PartitionConnectedNodeGroup(ConnectedNodeGroup connectedGroup, SortedDictionary <int, HashSet <int> > idsContainedInGroup, SortedList <int, ConnectedNodeGroup> connectedGroups, SortedList <int, PolygonSplittingGraphNode> nodes, List <Vector2>[] holes) { if (connectedGroup is null) { throw new ArgumentNullException(nameof(connectedGroup)); } if (idsContainedInGroup is null) { throw new ArgumentNullException(nameof(idsContainedInGroup)); } if (connectedGroups is null) { throw new ArgumentNullException(nameof(connectedGroups)); } if (nodes is null) { throw new ArgumentNullException(nameof(nodes)); } if (holes is null) { throw new ArgumentNullException(nameof(holes)); } var outerPerimCycle = new List <PolygonSplittingGraphNode>(); var polyCycles = new List <List <PolygonSplittingGraphNode> >(); var holeCycles = new List <List <PolygonSplittingGraphNode> >(); List <List <PolygonSplittingGraphNode> > allFaces = _GetAllFaces(connectedGroup); var uniqueFaces = new List <List <Vector2> >(); foreach (List <PolygonSplittingGraphNode> newFace in allFaces) { //construct Vector2[] or List<Vector2> describing face perim in Vector2s var newFacePerim = new Vector2[newFace.Count]; for (int i = 0; i < newFace.Count; i++) { PolygonSplittingGraphNode node = newFace[i]; newFacePerim[i] = new Vector2(node.x, node.y); } bool newFaceUnique = true; foreach (List <Vector2> uniqueFace in uniqueFaces) { if (GeometryFuncs.ArePolysIdentical(uniqueFace.ToArray(), newFacePerim)) { newFaceUnique = false; break; } } if (newFaceUnique) { uniqueFaces.Add(newFacePerim.ToList()); if (IsCycleHole(newFacePerim, holes)) { holeCycles.Add(newFace); } else if (IsCycleOuterPerim(newFacePerim, connectedGroup.outerPerimNodes)) { outerPerimCycle.AddRange(newFace); } else if (IsCycleComplex(newFacePerim)) { continue; } else { polyCycles.Add(newFace); } } } var innerHoles = holeCycles.Select(_FinaliseInnerHole).ToList(); var partitions = new List <ChordlessPolygon>(); foreach (List <PolygonSplittingGraphNode> polyCycle in polyCycles) { partitions.Add(_FinalisePartition(polyCycle, connectedGroup, connectedGroups, idsContainedInGroup, nodes, innerHoles)); } if (partitions.Count == 0 && outerPerimCycle.Count > 0) //planar face that represents the outer perim is only relevant IFF there are no other (non-hole) faces { partitions.Add(_FinalisePartition(outerPerimCycle, connectedGroup, connectedGroups, idsContainedInGroup, nodes, innerHoles)); } return(partitions); }
/// <summary> /// Checks if the cycle is a hole. NOTE that no checks are done to ensure that cyclePerim is simplified, but /// this is not required as holes are always passed into PolygonSplittingGraph in their most simple form, and /// when extracted they keep the same vertices. /// Q: "But what if a bridge or chord or something connects the outside perimeter to a point on a hole where a vertex /// does not already exist?" /// A: This will never happen as the only way a hole will be connected to anything else is via a chord, which MUST /// be attached to a concave vertex. /// </summary> /// <param name="cyclePerim">Cycle discovered from planar graph.></param> /// <param name="holes">List of holes (which are a list of Vector2s).</param> /// <returns>True if the cycle is a hole, false otherwise.</returns> public static bool IsCycleHole(Vector2[] cyclePerim, List <Vector2>[] holes) { return(holes.Any(hole => GeometryFuncs.ArePolysIdentical(cyclePerim, hole.ToArray()))); }