/// <summary> /// Auxiliary function for _FindOuterPerim. /// When called, chooses the next neighbour from the current vertex that should be added to form the outer /// perimeter of the polygon represented by the nodes in this class. /// </summary> /// <param name="currentVertex">Current vertex selected as part of the outer perimeter.</param> /// <param name="bearing">Bearing that describes the direction from the current vertex to the previous vertex.</param> /// <param name="validNeighbours">A set of valid neighbours </param> /// <returns>The next neighbour from the current vertex to make the outer perimeter.</returns> private static PolygonSplittingGraphNode _ChooseNextNeighbour(PolygonSplittingGraphNode currentVertex, Vector2 bearing, HashSet <PolygonSplittingGraphNode> validNeighbours) { float minAngle = 360; PolygonSplittingGraphNode currentSolution = validNeighbours.First(); foreach (PolygonSplittingGraphNode neighbour in validNeighbours) { //pick the neighbour with the least CCW angle difference Vector2 neighbourDirection = (new Vector2(neighbour.x, neighbour.y) - new Vector2(currentVertex.x, currentVertex.y)).Normalized(); var mirroredBearing = new Vector2(bearing.x, -1 * bearing.y); //MIRROR the y because graphics canvases are ALWAYS MIRRORED, F**K var mirroredNeighbourDirection = new Vector2(neighbourDirection.x, -1 * neighbourDirection.y); float angleDiff = Mathf.Rad2Deg(Mathf.Atan2(mirroredBearing.x * mirroredNeighbourDirection.y - mirroredBearing.y * mirroredNeighbourDirection.x, mirroredBearing.x * mirroredNeighbourDirection.x + mirroredBearing.y * mirroredNeighbourDirection.y)); if (angleDiff <= 0) { angleDiff += 360; } if (!(angleDiff < minAngle)) { continue; } minAngle = angleDiff; currentSolution = neighbour; } return(currentSolution); }
/// <summary> /// A recursive DFS that finds bridges by: /// 1. Labelling every node with a number signifying 'when' it was discovered (preorder) /// 2. Maintaining, for every node N, its lowest reachable vertex from a subtree with vertex N. /// A bridge is confirmed between some node U and some node V if V's lowest reachable vertex is further down the tree /// than U. /// Credit to: https://www.geeksforgeeks.org/bridge-in-a-graph/ /// for the algorithm which i pretty much shamelessly stole. /// </summary> /// <param name="node">Node that this method is visiting.</param> private static void _BridgeDFS(PolygonSplittingGraphNode node) { int visitedID = node.id; _visited.Add(visitedID); _preorder[visitedID] = _low[visitedID] = ++_time; foreach (int neighbourID in node.connectedNodeIDs) { if (!_visited.Contains(neighbourID)) { _parent[neighbourID] = visitedID; _BridgeDFS(_allNodes[neighbourID]); _low[visitedID] = Math.Min(_low[visitedID], _low[neighbourID]); if (_low[neighbourID] > _preorder[visitedID]) { //FOUND BRIDGE, as lowest vertex reachable from neighbourID is further down the tree than visitedID _bridges[visitedID].Add(neighbourID); _bridges[neighbourID].Add(visitedID); } } else if (neighbourID != _parent[visitedID]) { _low[visitedID] = Math.Min(_low[visitedID], _preorder[neighbourID]); } } }
/// <summary> /// Creates a ChordlessPolygon with just an outer perimeter and flags it as a hole. /// </summary> /// <param name="cycle"></param> /// <returns>A ChordlessPolygon flagged as a hole, just with an outer perimeter.</returns> private static ChordlessPolygon _FinaliseInnerHole(List <PolygonSplittingGraphNode> cycle) { var cyclePerim = new Vector2[cycle.Count]; for (int i = 0; i < cycle.Count; i++) { PolygonSplittingGraphNode node = cycle[i]; cyclePerim[i] = new Vector2(node.x, node.y); } var holePolygon = new ChordlessPolygon(cyclePerim, Array.Empty <List <Vector2> >(), new Dictionary <Vector2, HashSet <Vector2> >(), true); return(holePolygon); }
/// <summary> /// Final step in partitioning a ConnectedNodeGroup, by converting it into a RectangularPolygon and checking if /// the partition contains any holes (AKA the outerPerim of any ConnectedNodeGroups its parent ConnectedNodeGroup /// contains). /// </summary> /// <param name="cycle">Cycle discovered from BFS, AKA partition of ConnectedNodeGroup.></param> /// <param name="connectedGroup">The ConnectedNodeGroup <param>cycle</param> was derived from.</param> /// <param name="connectedGroups">A list of all ConnectedNodeGroups.</param> /// <param name="idsContainedInGroup">The IDs of the ConnectedNodeGroups contained within <param>connectedGroup</param></param> /// <param name="nodes"></param> /// <param name="innerHoles">Holes extracted from the same ConnectedNodeGroup.</param> /// <returns></returns> private static ChordlessPolygon _FinalisePartition(List <PolygonSplittingGraphNode> cycle, ConnectedNodeGroup connectedGroup, SortedList <int, ConnectedNodeGroup> connectedGroups, SortedDictionary <int, HashSet <int> > idsContainedInGroup, SortedList <int, PolygonSplittingGraphNode> nodes, List <ChordlessPolygon> innerHoles) { var cycleAsVectorArray = new Vector2[cycle.Count]; for (int i = 0; i < cycle.Count; i++) { PolygonSplittingGraphNode node = cycle[i]; cycleAsVectorArray[i] = new Vector2(node.x, node.y); } var potentialHoles = new List <List <Vector2> >(); potentialHoles.AddRange(innerHoles.Select(innerHole => innerHole.outerPerim.ToList())); var bridges = new Dictionary <Vector2, HashSet <Vector2> >(); foreach (int connectedGroupID in idsContainedInGroup[connectedGroup.id]) { ConnectedNodeGroup groupInside = connectedGroups[connectedGroupID]; potentialHoles.Add(groupInside.outerPerimSimplified.ToList()); foreach (int nodeID in groupInside.bridges.Keys) { if (!cycle.Exists(x => x.id == nodeID)) { continue; } var nodeCoord = new Vector2(nodes[nodeID].x, nodes[nodeID].y); foreach (int neighbourID in groupInside.bridges[nodeID]) { if (!cycle.Exists(x => x.id == neighbourID)) { continue; } var neighbourCoord = new Vector2(nodes[neighbourID].x, nodes[neighbourID].y); if (!bridges.ContainsKey(nodeCoord)) { bridges[nodeCoord] = new HashSet <Vector2>(); } bridges[nodeCoord].Add(neighbourCoord); } } } return(new ChordlessPolygon(cycleAsVectorArray, potentialHoles.ToArray(), bridges, false)); }
/// <summary> /// Finds the outer perimeter of the group of nodes in this class by: /// 1. Getting the node with the smallest X (and Y if X-value is the same) coordinate. /// 2. Defining a bearing as being in the negative Y direction as there are no nodes in that direction. /// 3. Out of the valid edges from the current node, picking the one with the least positive CCW angle change /// from the bearing. /// 4. Defining a new bearing as being the direction FROM THE NEW node TO THE OLD node. /// 5. Repeating until the first node is found again. /// As always, the convention for closed loops of Vector2s has list[first] == list[last]. /// </summary> /// <param name="adjMatrix">Adjacency Matrix of nodes.</param> /// <returns>List of nodes in CCW order that describe the outer perimeter.</returns> private List <PolygonSplittingGraphNode> _FindOuterPerim() { PolygonSplittingGraphNode startVertex = this._xySortedNodes.First().Key; var localOuterPerim = new List <PolygonSplittingGraphNode> { startVertex }; Vector2 bearing = Globals.NorthVec2; PolygonSplittingGraphNode currentVertex = startVertex; do { var validNeighbours = new HashSet <PolygonSplittingGraphNode>(); foreach (int neighbourID in currentVertex.connectedNodeIDs.Where(neighbourID => this.nodes.ContainsKey(neighbourID))) { validNeighbours.Add(this.nodes[neighbourID]); } PolygonSplittingGraphNode nextNeighbour = _ChooseNextNeighbour(currentVertex, bearing, validNeighbours); localOuterPerim.Add(nextNeighbour); bearing = (new Vector2(currentVertex.x, currentVertex.y) - new Vector2(nextNeighbour.x, nextNeighbour.y)).Normalized(); currentVertex = nextNeighbour; } while (!currentVertex.Equals(startVertex)); return(localOuterPerim); }
private int _CompareTo(PolygonSplittingGraphNode other) { return(SortFuncs.SortByXThenYAscending(new Vector2(this.x, this.y), new Vector2(other.x, other.y))); }
private bool _Equals(PolygonSplittingGraphNode other) { return(this.x == other.x && this.y == other.y); }
/// <summary> /// Checks if this class contains the input <param>node</param>. /// </summary> /// <param name="node"></param> /// <returns>True if contained, false otherwise.</returns> public bool ContainsNode(PolygonSplittingGraphNode node) { return(this.nodes.ContainsValue(node)); }
/// <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); }