This commit closes the remaining red gates so `run-all.sh --fast` is
green end-to-end on the canonical dev machine.
New gates
─────────
1. cmake-format / cmake-lint
* scripts/quality/cmake-format.sh — dry-run by default,
--strict to fail on drift, --fix to apply
* .cmake-format.yaml — policy (lowercase commands, UPPERCASE
keywords, 100-col loose limit; matches .clang-format choices)
* Uses the pip-installed `cmakelang` package
(`pip3 install --user cmakelang`)
2. codespell
* scripts/quality/codespell.sh — exit 1 on any typo, --fix
interactively
* .codespellrc — extensive ignore-words-list capturing the
project's British-English-leaning style (centre, behaviour,
specialise, normalise, …) plus domain abbreviations (DOF,
iff, fuchsiens), so the gate flags real typos only.
* Validated: 0 typos across docs + code/include + scripts +
code/{src,tests}.
SPDX rollout (license-headers --fix)
────────────────────────────────────
license-headers.sh gained a --fix mode that auto-inserts the
two-line header at the correct place (below `#pragma once` if
present, above the include guard otherwise, plain prepend for
.cpp). Ran it on 60 of 66 files — 100 %-licensed now.
Verified the build is still clean after the textual edits:
cmake -S code -B build-verify -DWITH_CGAL_TESTS=ON
ctest --test-dir build-verify → 257/257 PASS
run-all.sh + README updated to include the two new gates.
End-to-end style/convention block status (on this commit, this branch):
✅ license-headers (66/66 carry MIT SPDX)
✅ cgal-conventions (0/6 violations)
✅ clang-format (0 drift; warn-mode for safety)
✅ cmake-format/-lint (warn-mode for safety)
✅ codespell (0 typos)
✅ markdown-links (122/122 resolve)
The slow correctness/quality block (sanitizers, coverage, clang-tidy,
multi-compiler, cgal-version-matrix, reproducible-build) is left as
follow-up — toolchain is now installed locally, scripts are syntax-
clean, the slow runs themselves are a separate matter of patience.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
156 lines
6.4 KiB
C++
156 lines
6.4 KiB
C++
#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 − (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 <vector>
|
||
#include <queue>
|
||
#include <cstddef>
|
||
|
||
namespace conformallab {
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// CutGraph
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
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;
|
||
|
||
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_cut_graph
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
//
|
||
// Implements the standard tree-cotree algorithm (Erickson–Whittlesey 2005):
|
||
//
|
||
// Step 1: BFS primal spanning tree T (V−1 primal tree edges).
|
||
// Step 2: BFS dual spanning tree T* (F−1 dual/primal edges, avoiding
|
||
// edges whose primal crosses T).
|
||
// Step 3: cut edges = primal edges neither in T nor "used" by 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
|