Distinguishing clockwise and counterclockwise rotation in graph

I’m working on a really basic graph traversal thingie where I’m iterating through an undirected graph checking for polygons. The basic idea is simple: at every vertex, move along the edge that has the greatest clockwise rotation without crossing the edge you’re coming from. At any given iteration of this cycle I have directional vectors corresponding to the last move made, coordinates for connected vertices, and directional vectors representing each edge between the current position and a potential future position.

Where I’m really stuck is on how I should be quantifying “move clockwise”. Vector3.Angle and SignedAngle both produce degrees of rotation, but depending on the orientation of the edge you arrived along, it’s possible for any angle to constitute a clockwise movement.

I don’t think I have a big enough vertex and line buffer in my head to understand your problem.

Perhaps if you rephrase it to explain what it is you are trying to do?

Or perhaps draw some pictures to help us with smaller vertex buffers.

Yeah of course, sorry for not being very articulate! I’m trying to find all of the non-overlapping polygons in an undirected graph. I already have the graph, which is just a List where my vertex class looks like this:

class Vertex{
   Vector3 center;
   List<Vector3> connections;
}

If you were to plot that list in a visual format, it would look something like the following- five (in this case) vertices connected by edges:

Now in data all that exists are the vertices and edges, but when visualized the edges form polygons- the two shapes I shaded in gray.

I’m trying to find every fully enclosed, non-overlapping polygon in the graph. The most straightforward method I’ve found to do that involves “winding” each face in a clockwise fashion. In pseudocode, you basically do this:

while (!polygonFormed, optionsRemaining){
//evaluate every edge connected to the current vertex, pick the vertex with the greatest clockwise rotation relative to the current vertex
//Move to the new vertex
//If you return to the starting vertex, you've closed the loop and formed a polygon!
}

If you run that long enough, you’ll either trace a complete polygon (in which case the vertices you traversed constitute its shape), or you hit a failure condition (run out of choices, go on for too long, etc). This is basically a geometry version of the maze hack- turn right at each junction and you’ll eventually traverse the entire maze.

I’m clear on 90% of this, but what I’m having trouble with is figuring out what criteria constitutes turning right- each time this loops I have the coordinates of my current vertex, all the vertices this particular loop has already visited, and every edge extending from the current vertex, but since the shape is not on a regular grid I’m having trouble figuring out how to consistently curve my traversal to the (relative) right.

Why isn’t SignedAngle working in this case? Let’s say you start out at point D, moving to E. Now you have two new edges to choose from: EB and EA. So you compare SignedAngle(ED, EB) with SignedAngle(ED, EA) and get relative angles for both, independent of the overall orientation.

That’s what I’ve been using, but it produces weird results- if you agree that’s the way to approach it, my eyes must be skipping over a weird mistake I made somewhere. Here’s what I’m using right now to try and transcribe a polygon from a graph, but every time I run it, it just returns a “polygon” that only contains the starting vertice, or occasionally contains one additional vertex that forms a straight line with the starting point…

        public List<Vertex> ClockwiseWalk(Vertex startingVertex)
        {
            List<Vertex> coordinates = new List<Vertex>();
            coordinates.Add(startingVertex);

            bool polygonClosed = false;
            int iterations = 0;

            Vertex currentVertex = startingVertex;
            Vertex bestVertex = null;
            float bestRotation = 360;

            while (!polygonClosed && iterations <30)
            {

                if (bestVertex != null)
                {
                    coordinates.Add(bestVertex);
                    currentVertex = bestVertex;
                }

                for (int i = 0; i < currentVertex.connections.Count; i++)
                {
                    float rotation = 0;
                    if (coordinates.Count > 1) {
                        rotation = Vector3.SignedAngle(currentVertex.center - coordinates[coordinates.Count-2].center, currentVertex.connections[i] - currentVertex.center, Vector3.up);
                    }
                    else
                    {
                        rotation = Vector3.SignedAngle(currentVertex.center, currentVertex.connections[i] - currentVertex.center, Vector3.up);
                    }

                    if (rotation>=0 && rotation < bestRotation)
                    {
                        bestRotation = rotation;
                        bestVertex = GetVertex(currentVertex.connections[i]);
                    }

                    if (currentVertex.connections[i] == startingVertex.center && coordinates.Count>2)
                    {
                        polygonClosed = true;
                    }
                }

              
                iterations++;
            }
            return coordinates;
        }

SignedAngle returns values between -180 and 180. It might be easiest to just iterate through all connected vertices, store their relative angles and then sort the results.
Some pseudocode:

Edge {
    Vector direction
    float angle
}

currentEdge = new Edge() {
    direction = previousVertex - currentVertex
}

list = new List<Edge>()

for i = 0; i < neighbourVertices.length (excluding previousVertex)
{
    newEdge = new Edge()
    {
        direction = neighbourVertices[i] - currentVertex
    }
  
    newEdge.angle = SignedAngle(currentEdge.direction, newEdge.direction)
    list.Add(newEdge)
}

list.SortOn("angle")

Hmm, that’s much cleaner, but I’m seeing kind of funny pathfinding behavior-it looks like it’s turning left and right instead of consistently turning in one direction. Here’s a sample output (the solid lines are edges in the graph, each junction is a vertex, the orange & white balls represent where this thinks a polygon is, and the selected sphere in the bottom-leftish is where it started searching from. The green square at the bottom-left is the output I was expecting but didn’t get):

Do you have any idea what would cause that? I’ve been over the new version for hours, as far as I can tell it should either fail out or wrap as tight a polygon as it can:

        public List<Vector3> ClockwiseWalk(Vertex startingVertex)
        {
            List<Vector3> coordinates = new List<Vector3>();
            coordinates.Add(startingVertex.center);

            bool polygonClosed = false;
            int iterations = 0;

            Vertex currentVertex = startingVertex;
            Vertex previousVertex = startingVertex;
       
            List<Vertex> walkedVertices = new List<Vertex>();
       
            List<Edge> acceptedEdges = new List<Edge>();
            Edge currentEdge = new Edge();
            currentEdge.direction = previousVertex.center - currentVertex.center;
            currentEdge.start = previousVertex.center;
            currentEdge.end = currentVertex.center;
            acceptedEdges.Add(currentEdge);

            while (!polygonClosed && iterations <30)
            {
                List<Edge> edgeList = new List<Edge>();

                for (int i = 0; i < currentVertex.connections.Count; i++)
                {
                    Vertex vertexToConsider = GetVertex(currentVertex.connections[i]);
                    if ( vertexToConsider != null && !walkedVertices.Contains(vertexToConsider)) {
                        Edge newEdge = new Edge();
                        newEdge.direction = currentVertex.connections[i] - currentVertex.center;
                        newEdge.angle = Vector3.SignedAngle(currentEdge.direction, newEdge.direction, Vector3.up);

                        newEdge.start = currentVertex.center;
                        newEdge.end = currentVertex.connections[i];
                        edgeList.Add(newEdge);
                    }

                    if (vertexToConsider == startingVertex && coordinates.Count>2)
                    {
                        polygonClosed = true;
                    }
                }

                edgeList.Sort(SortByAngle);

                if (edgeList.Count > 0)
                {
                    Edge bestEdge = edgeList[edgeList.Count - 1];
                    acceptedEdges.Add(bestEdge);
                    currentEdge = bestEdge;


                    if (bestEdge.end == startingVertex.center && coordinates.Count > 2)
                    {
                        polygonClosed = true;
                    }

                    else
                    {
                        coordinates.Add(bestEdge.end);
                        walkedVertices.Add(currentVertex);
                    }

                    previousVertex = currentVertex;
                    currentVertex = GetVertex(bestEdge.end);
                }
                else
                {
                    iterations = 30;
                }

                iterations++;
            }
            return coordinates;
        }

        int SortByAngle(Edge e1, Edge e2)
        {
            return e1.angle.CompareTo(e2.angle);
        }

Simplify the problem. Make the very smallest tiniest dataset that you can see the problem on, which is probably three vertices. Make them clearly different so you can see what is going on.

Now, every time in your code you consider or copy or add or remove a vert, output it to the Debug.Log() console.

Is the order of what is happening what you expect? Why not? What condition is failing? With a tiny dataset it is much easier to track down your issues.

If a tiny dataset actually works, add one unique vertex to it. Does it still work? Keep adding verts until it fails. What is special about that final vert?

I have some concerns about the definition you’re working with.

  1. You supply an image of verts A,B,C,D,E with edges drawn and the result you expect. Thing is your drawn result is of a triangle and a quadrilateral… both of whom are convex (most often in graph theory and geometry we concern ourselves with convex polygons due to their simplicity). Thing I’m wondering is what stops the pentagon defined by ABCDE from being considered a polygon in your algorithm? It is also convex. If it’s because you want smallest… then what is stopping the triangles BCE and CDE from being considered polygons?

  2. You have this definition:

The greatest clockwise rotation relative to the current vertex? A vertex has no rotation/angle. An angle requires at minimum 3 vertices (hence why the triangle is our simplest polygon… well in euclidian space that is, if we went into spherical or elliptical space you can have a digon). Or of course 2 rays out of the same vertex, or 2 vertices and a ray coming out of 1.

You have your vertex, and the vertex you want to go to… but what is the 3rd vertex? What edge are you measuring the angle off of? I would assume it’s the previous vertex… but then what do you start with from the FIRST vertex since there was no previous.

My concern is that your definition isn’t specific enough to get a consistent and predictable result.

I’ll happily concede that my definition could be insufficient- stepping back from the math for a moment, this is for a map generator- the coarse graph I’m working from is generated by an L-system and represents traversable paths (highways, streets, alleys etc). The shapes I’m trying to calculate by finding smallest possible polygons formed by those vertices represent lots, flat surfaces that buildings or other interactable modules can be built on.

So it’s one of those tasks that is easy to describe in human language (‘look at this network of paths and identify the empty spaces between the roads’), but I’m finding incredibly frustrating to coherently state and solve mathematically.

I think mbaske’s stating direction is flipped on line 7. Should be currentVertex-previousVertex (end minus start). But adding lines to print it (as an angle form 0 degrees) along with the other vert+relativeAngle’s should show an obvious problem somewhere.

The “always take sharpest right angle” seems workable, and could be correct, assuming the graph is completely 2D. Plus, if you get it working you’ll easily see and flaws in the plan.

I suspect the core issue must be that its conditions to declare a polygon closed are off although it seems pretty straightforward- lines 41 and 55 on my thing, it just tells the polygon to close if, after moving at least twice (so it doesn’t just move in any direction then loop back), it has a candidate or best match that was also the starting vertex.

This is the above logic run on a rough grid (each cube is connected to up to four nearby cube, one per cardinal direction, the polygon axes are the orange spheres and the vertex each search starts on is circled in gray). There must be some logic driving the mistakes but it appears practically random.

Below is my take on the problem, pretty much starting from scratch. Not thoroughly tested or optimized, but the overall logic should check out.

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class GridTest : MonoBehaviour
{
    private Dictionary<int, Edge> walkedEdges;
    private Dictionary<int, Edge> pendingEdges;
    private List<Polygon> polygons;

    private Node currentNode;
    private Edge currentEdge;
    private Polygon currentPolygon;

    private void Start()
    {
        walkedEdges = new Dictionary<int, Edge>();
        pendingEdges = new Dictionary<int, Edge>();
        polygons = new List<Polygon>();

        CreateGrid();
        FindPolygons();
    }

    private void FindPolygons()
    {
        do
        {
            currentEdge = DequeuePendingEdge();
            currentPolygon = new Polygon(currentEdge);
            AddWalkedEdge(currentEdge);
            MoveToNextNode();

            if (WalkEdges())
            {
                polygons.Add(currentPolygon);

                currentPolygon.Draw(Color.red, 100);
            }
        }
        while (pendingEdges.Count > 0);
    }

    private bool WalkEdges()
    {
        do
        {
            currentEdge = FindNextEdge();
            if (currentEdge != null)
            {
                AddWalkedEdge(currentEdge);
                currentPolygon.AddEdge(currentEdge);
                MoveToNextNode();
            }
        }
        while (currentEdge != null && !currentPolygon.IsClosed() && !currentPolygon.IsConcave());

        return currentPolygon.IsClosed();
    }

    private Edge FindNextEdge()
    {
        Edge nextEdge = null;
        int flippedID = currentEdge.Flip().ID;
        List<Edge> candidates = new List<Edge>();

        foreach (Node node in currentNode.Connected)
        {
            Edge edge = new Edge(currentNode, node);
            bool isCurrent = edge.ID == flippedID;
            if (!walkedEdges.ContainsKey(edge.ID) && !isCurrent)
            {
                candidates.Add(edge);
            }
        }

        if (candidates.Count > 0)
        {
            float maxAngle = -180;
            foreach (Edge edge in candidates)
            {
                float angle = currentEdge.GetAngle(edge);
                if (angle >= maxAngle)
                {
                    nextEdge = edge;
                    maxAngle = angle;
                }
            }
        }

        return nextEdge;
    }

    private void MoveToNextNode()
    {
        currentNode = currentEdge.EndNode;
    }

    private void AddWalkedEdge(Edge edge)
    {
        if (!walkedEdges.ContainsKey(edge.ID))
        {
            walkedEdges.Add(edge.ID, edge);
            pendingEdges.Remove(edge.ID);
        }
    }

    private Edge DequeuePendingEdge()
    {
        List<Edge> list = Enumerable.ToList(pendingEdges.Values);
        Edge edge = list[0];
        pendingEdges.Remove(edge.ID);
        return edge;
    }

    private void CreateGrid(int xSize = 10, int zSize = 10)
    {
        Node[,] nodes = new Node[xSize, zSize];

        for (int x = 0; x < xSize; x++)
        {
            for (int z = 0; z < zSize; z++)
            {
                Vector3 vertex = new Vector3(Displace(x), 0, Displace(z));
                nodes[x, z] = new Node(vertex);
            }
        }

        for (int x = 0; x < xSize; x++)
        {
            for (int z = 0; z < zSize; z++)
            {
                Node node = nodes[x, z];

                do
                {
                    if (RandomFlip() && x > 0)
                    {
                        node.ConnectNode(nodes[x - 1, z]);
                    }
                    if (RandomFlip() && x < xSize - 2)
                    {
                        node.ConnectNode(nodes[x + 1, z]);
                    }

                    if (RandomFlip() && z > 0)
                    {
                        node.ConnectNode(nodes[x, z - 1]);
                    }
                    if (RandomFlip() && z < zSize - 2)
                    {
                        node.ConnectNode(nodes[x, z + 1]);
                    }
                }
                while (node.Connected.Count == 0);

                node.DrawConnections(Color.gray, 100);
            }
        }

        for (int x = 0; x < xSize; x++)
        {
            for (int z = 0; z < zSize; z++)
            {
                Node node = nodes[x, z];
                foreach (Node connected in node.Connected)
                {
                    Edge edge = new Edge(node, connected);
                    pendingEdges.Add(edge.ID, edge);
                }
            }
        }
    }

    private float Displace(int val, float randomness = 0.5f)
    {
        return val + Random.value * randomness - randomness * 0.5f;
    }

    private bool RandomFlip(float probability = 0.35f)
    {
        return probability > 0 && Random.value <= probability;
    }
}

public class Node
{
    public static int Count;

    public int ID;
    public Vector3 Vertex;
    public List<Node> Connected;

    public Node(Vector3 vertex)
    {
        ID = Count++;
        Vertex = vertex;
        Connected = new List<Node>();
    }

    public bool ConnectNode(Node node)
    {
        if (!Connected.Contains(node))
        {
            Connected.Add(node);
            node.ConnectNode(this); // connections are always both ways
            return true;
        }
        return false;
    }

    public void DrawConnections(Color color, float duration)
    {
        foreach (Node node in Connected)
        {
            Debug.DrawLine(Vertex, node.Vertex, color, duration);
        }
    }
}

public class Edge
{
    public int ID;
    public Node StartNode;
    public Node EndNode;
    public Vector3 Vector;

    public Edge(Node start, Node end)
    {
        ID = GetID(start, end);
        StartNode = start;
        EndNode = end;
        Vector = end.Vertex - start.Vertex;
    }

    public Edge Flip()
    {
        return new Edge(EndNode, StartNode);
    }

    public int GetID(Node start, Node end)
    {
        return start.ID * Node.Count + end.ID;
    }

    public float GetAngle(Edge edge)
    {
        return Vector3.SignedAngle(Vector, edge.Vector, Vector3.up);
    }

    public void Draw(Color color, float duration)
    {
        Debug.DrawLine(StartNode.Vertex, EndNode.Vertex, color, duration);
    }
}

public class Polygon
{
    private static int count;

    public int ID;
    public List<Edge> Edges;
    public List<Node> Nodes;

    private float windingAngle;

    public Polygon(Edge startEdge)
    {
        ID = count++;
        Edges = new List<Edge>() { startEdge };
        Nodes = new List<Node>() { startEdge.StartNode };
    }

    public void AddEdge(Edge edge)
    {
        windingAngle += Vector3.SignedAngle(Edges[Edges.Count - 1].Vector, edge.Vector, Vector3.up);

        Edges.Add(edge);
        Nodes.Add(edge.StartNode);
    }

    public bool IsConcave(float threshold = -90)
    {
        return windingAngle <= threshold;
    }

    public bool IsClosed()
    {
        return Nodes[0].ID == Edges[Edges.Count - 1].EndNode.ID;
    }

    public Vector3 GetCentroid()
    {
        Vector3 vertex = Vector3.zero;
        foreach (Node node in Nodes)
        {
            vertex += node.Vertex;
        }
        return vertex / (float)Nodes.Count;
    }

    public void Draw(Color color, float duration)
    {
        float offset = 0.05f;
        List<Vector3> offsetVertices = new List<Vector3>();
        Vector3 centroid = GetCentroid();
 
        foreach (Node node in Nodes)
        {
            Vector3 dir = (centroid - node.Vertex).normalized;
            offsetVertices.Add(node.Vertex + dir * offset);
        }

        int n = offsetVertices.Count - 1;
        Debug.DrawLine(offsetVertices[n], offsetVertices[0], color, duration);

        for (int i = 1; i <= n; i++)
        {
            Debug.DrawLine(offsetVertices[i - 1], offsetVertices[i], color, duration);
        }
    }
}
1 Like

It absolutely does, so thank you for all the time you put into helping me on this- I know beginners must be incredibly frustrating to work with, so I really appreciate everyone’s time and feedback. :slight_smile:

If it’s helpful for anyone else who hits the same problem, https://blog.mrpetovan.com/web-development/algorithm-101-finding-all-polygons-in-an-undirected-graph/ was also super-duper useful when I was trying to formalize my approach. ^^