/// <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> /// 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()))); }