Ejemplo n.º 1
0
        /// <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);
        }
Ejemplo n.º 3
0
        /// <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);
        }
Ejemplo n.º 4
0
        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();
                }
            }
        }
Ejemplo n.º 5
0
        /// <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());
        }
Ejemplo n.º 6
0
        /// <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);
        }
Ejemplo n.º 8
0
        /// <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);
        }
Ejemplo n.º 10
0
        /// <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);
        }
Ejemplo n.º 11
0
        /// <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);
        }
Ejemplo n.º 12
0
        /// <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);
        }
Ejemplo n.º 13
0
 /// <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())));
 }