Files
ConformalLabpp/code/include/cut_graph.hpp
user2595 704f42bbfd
Some checks failed
C++ Tests / test-fast (push) Has started running
C++ Tests / test-cgal (push) Has been cancelled
API Docs / doc-build (push) Has been cancelled
Doxygen → Codeberg Pages / publish (push) Has been cancelled
Markdown link check / check (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Merge pull request 'ci+quality: structural gates (CI: 3 new; local: 7 new + .clang-tidy)' (#18) from ci/structural-tests into main
2026-05-26 09:14:45 +00:00

152 lines
6.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT
// 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 (V1) (F1) = 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 (V1) (F1) 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 <vector>
#include <queue>
#include <cstddef>
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<bool> cut_edge_flags;
/// Indices of the 2g cut edges in order (size = 2g).
std::vector<std::size_t> 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<std::size_t>(e.idx()) < cut_edge_flags.size()
&& cut_edge_flags[static_cast<std::size_t>(e.idx())];
}
};
/// Compute the cut graph of `mesh` via the standard tree-cotree
/// algorithm (EricksonWhittlesey 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<bool> tree_edge(ne, false);
std::vector<bool> v_visited(nv, false);
{
std::queue<Vertex_index> 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<std::size_t>(u.idx())]) {
v_visited[static_cast<std::size_t>(u.idx())] = true;
tree_edge[static_cast<std::size_t>(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<bool> dual_tree_edge(ne, false);
std::vector<bool> f_visited(nf, false);
{
std::queue<Face_index> q;
auto f0 = *mesh.faces().begin();
f_visited[static_cast<std::size_t>(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<std::size_t>(f_adj.idx())]) continue;
std::size_t eidx = static_cast<std::size_t>(mesh.edge(h).idx());
if (!tree_edge[eidx]) {
// Use this dual edge in T*
f_visited[static_cast<std::size_t>(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<std::size_t>(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