Lists the faces of a planar straight-line embedding as cycles of vertices, by walking each directed edge to the next edge in angular order.

auto faces = planarFaces(pts, edges);   // vector<P>, undirected edges {u, v}; no crossings
// every face is a vertex cycle; the outer face of each connected component has negative signed area

Complexity: .

For the directed edge , the next edge of the same face is where is the neighbour of that comes right after in clockwise order around (faces on the left are traversed CCW). Every directed edge belongs to exactly one face, so there are directed edges in total.

Euler's formula here is , not

This algorithm has no way to know that two disconnected components share the same unbounded outer region — tracing faces purely from edge/vertex incidence, each connected component gets its own separate outer-boundary cycle, since there is no edge to walk between components. So each component independently satisfies the connected-graph formula , and summing over components gives — not the formula for a single planar subdivision with one shared unbounded face. If your application needs the components merged into one true planar subdivision (one shared outer face), union them under one super-vertex or otherwise connect them before calling this.

A bridge appears twice in the same face (walking it out and back, since there is no “other side” face distinct from the one it’s already part of). Dual graph: connect faces sharing an edge.

vector<vector<int>> planarFaces(const vector<P>& pts, const vector<pair<int, int>>& edges) {
    int n = pts.size();
    vector<vector<int>> adj(n);
    for (auto [u, v] : edges) { adj[u].push_back(v); adj[v].push_back(u); }
    for (int v = 0; v < n; v++)   // counter-clockwise order of neighbours around v
        sort(adj[v].begin(), adj[v].end(), [&](int a, int b) {
            P pa = pts[a] - pts[v], pb = pts[b] - pts[v];
            if (half(pa) != half(pb)) return half(pa) < half(pb);
            return cross(pa, pb) > 0;
        });
    map<pair<int, int>, int> where;
    for (int v = 0; v < n; v++) for (int i = 0; i < (int)adj[v].size(); i++) where[{v, adj[v][i]}] = i;
    vector<vector<char>> used(n);
    for (int v = 0; v < n; v++) used[v].assign(adj[v].size(), 0);
    vector<vector<int>> faces;
    for (int s = 0; s < n; s++) {
        for (int i = 0; i < (int)adj[s].size(); i++) {
            if (used[s][i]) continue;
            vector<int> face;
            int u = s, k = i;
            while (!used[u][k]) {
                used[u][k] = 1;
                face.push_back(u);
                int v = adj[u][k];
                int back = where[{v, u}];                             // position of u around v
                int nk = (back - 1 + adj[v].size()) % adj[v].size();  // previous in CCW = next clockwise
                u = v; k = nk;
            }
            faces.push_back(face);
        }
    }
    return faces;
}

Tested: planarFaces checked against this algorithm’s Euler identity () on several hand-built graphs (a triangle, a square with a diagonal, a graph with a bridge edge, two disjoint triangles, a single edge) and on random planar graphs built by triangulating ear-clipped random simple polygons (guaranteed planar and crossing-free by construction); every directed edge is confirmed to appear in exactly one traced face, and exactly one face per connected component has non-positive signed area (strictly negative for a component with a real cycle; exactly zero for a degenerate tree component, which has no enclosed area at all).