#pragma once // cut_graph.hpp // // Phase 6 — Tree-cotree algorithm for computing a cut graph of a triangulated // surface. // // For a closed, orientable, genus-g surface: // #vertices (V), #edges (E), #faces (F) // Euler: V − E + F = 2 − 2g // Primal spanning tree: V − 1 edges // Dual spanning tree: F − 1 edges (avoiding duals of tree edges) // Remaining: E − (V−1) − (F−1) = 2g cut edges // // These 2g cut edges generate H₁(M, ℤ) ≅ ℤ^{2g}. // Cutting along them turns M into a topological disk. // // For open meshes (boundary present) the algorithm still works: the dual BFS // starts from a boundary-adjacent face, and boundary half-edges are skipped. // The number of cut edges will be E − (V−1) − (F−1) − B where B counts // boundary edges treated as dual tree edges. // // Usage: // CutGraph cg = compute_cut_graph(mesh); // // cg.cut_edge_flags[e.idx()] == true → treat edge as seam in BFS // euclidean_layout(mesh, x, maps, &cg); // layout with holonomy tracking #include "conformal_mesh.hpp" #include "gauss_bonnet.hpp" // for euler_characteristic / genus #include #include #include namespace conformallab { // ───────────────────────────────────────────────────────────────────────────── // CutGraph // ───────────────────────────────────────────────────────────────────────────── /// Cut-graph result of the tree-cotree algorithm: the set of `2g` edges /// whose removal turns a closed genus-`g` surface into a topological disk. struct CutGraph { /// cut_edge_flags[e.idx()] = true ↔ this edge is a cut edge. /// Size = mesh.number_of_edges(). std::vector cut_edge_flags; /// Indices of the 2g cut edges in order (size = 2g). std::vector cut_edge_indices; /// Genus of the surface (0 for topological spheres and open patches). int genus = 0; /// `true` iff edge `e` is a cut edge of this graph. bool is_cut(Edge_index e) const { return static_cast(e.idx()) < cut_edge_flags.size() && cut_edge_flags[static_cast(e.idx())]; } }; /// Compute the cut graph of `mesh` via the standard tree-cotree /// algorithm (Erickson–Whittlesey 2005): primal BFS spanning tree T, /// dual BFS spanning tree T* avoiding T-primals, then the `2g` cut /// edges are those in neither T nor T*. inline CutGraph compute_cut_graph(const ConformalMesh& mesh) { const std::size_t nv = mesh.number_of_vertices(); const std::size_t ne = mesh.number_of_edges(); const std::size_t nf = mesh.number_of_faces(); CutGraph cg; cg.cut_edge_flags.assign(ne, false); cg.genus = conformallab::genus(mesh); if (nv == 0 || nf == 0) return cg; // ── Step 1: primal spanning tree via BFS from vertex 0 ─────────────────── std::vector tree_edge(ne, false); std::vector v_visited(nv, false); { std::queue q; auto v0 = *mesh.vertices().begin(); v_visited[v0.idx()] = true; q.push(v0); while (!q.empty()) { Vertex_index v = q.front(); q.pop(); for (Halfedge_index h : CGAL::halfedges_around_target(v, mesh)) { Vertex_index u = mesh.source(h); if (!v_visited[static_cast(u.idx())]) { v_visited[static_cast(u.idx())] = true; tree_edge[static_cast(mesh.edge(h).idx())] = true; q.push(u); } } } } // ── Step 2: dual spanning tree via BFS from face 0 ─────────────────────── // Dual edge between face f and face f_adj crosses primal edge e. // Include dual edge only if: // (a) e is not a primal tree edge (tree_edge[e] == false) // (b) h_adj is not a border halfedge std::vector dual_tree_edge(ne, false); std::vector f_visited(nf, false); { std::queue q; auto f0 = *mesh.faces().begin(); f_visited[static_cast(f0.idx())] = true; q.push(f0); while (!q.empty()) { Face_index f = q.front(); q.pop(); for (Halfedge_index h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) { Halfedge_index h_opp = mesh.opposite(h); if (mesh.is_border(h_opp)) continue; // boundary edge Face_index f_adj = mesh.face(h_opp); if (f_visited[static_cast(f_adj.idx())]) continue; std::size_t eidx = static_cast(mesh.edge(h).idx()); if (!tree_edge[eidx]) { // Use this dual edge in T* f_visited[static_cast(f_adj.idx())] = true; dual_tree_edge[eidx] = true; q.push(f_adj); } } } } // ── Step 3: cut edges = neither in T nor in T* nor on boundary ─────────── // Boundary edges are adjacent to the "outer face" and need no cutting — // they are implicitly handled by the boundary itself. for (Edge_index e : mesh.edges()) { std::size_t idx = static_cast(e.idx()); if (tree_edge[idx] || dual_tree_edge[idx]) continue; // Skip boundary edges — they are not interior homological cycles. Halfedge_index h = mesh.halfedge(e); if (mesh.is_border(h) || mesh.is_border(mesh.opposite(h))) continue; cg.cut_edge_flags[idx] = true; cg.cut_edge_indices.push_back(idx); } return cg; } } // namespace conformallab