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>
244 lines
8.7 KiB
C++
244 lines
8.7 KiB
C++
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// test_conformal_mesh.cpp
|
||
//
|
||
// Phase 3a — CGAL Surface_mesh infrastructure tests.
|
||
//
|
||
// Verifies that ConformalMesh (CGAL::Surface_mesh<Point3>) and the
|
||
// mesh_builder factories behave correctly before we build the functionals
|
||
// on top of them (Phase 3b).
|
||
//
|
||
// Test groups
|
||
// ───────────
|
||
// Topology – vertex/edge/face counts, Euler characteristic
|
||
// Traversal – halfedge iteration around vertex / face / edge
|
||
// PropertyMaps – read/write of lambda, theta, idx, alpha, f:type
|
||
// Validity – all make_* factories produce valid, consistent meshes
|
||
|
||
#include "conformal_mesh.hpp"
|
||
#include "mesh_builder.hpp"
|
||
#include <CGAL/boost/graph/iterator.h>
|
||
#include <gtest/gtest.h>
|
||
#include <cmath>
|
||
|
||
using namespace conformallab;
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// Topology
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// Single triangle: 3 vertices, 1 face, 3 edges.
|
||
TEST(ConformalMeshTopology, SingleTriangle)
|
||
{
|
||
auto mesh = make_triangle();
|
||
EXPECT_EQ(3u, mesh.number_of_vertices());
|
||
EXPECT_EQ(1u, mesh.number_of_faces());
|
||
EXPECT_EQ(3u, mesh.number_of_edges());
|
||
}
|
||
|
||
// Tetrahedron: V=4, E=6, F=4 → Euler = 2 (sphere topology).
|
||
TEST(ConformalMeshTopology, TetrahedronEuler)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
EXPECT_EQ(4u, mesh.number_of_vertices());
|
||
EXPECT_EQ(6u, mesh.number_of_edges());
|
||
EXPECT_EQ(4u, mesh.number_of_faces());
|
||
|
||
int euler = (int)mesh.number_of_vertices()
|
||
- (int)mesh.number_of_edges()
|
||
+ (int)mesh.number_of_faces();
|
||
EXPECT_EQ(2, euler) << "Euler characteristic of closed sphere must be 2";
|
||
}
|
||
|
||
// Two-triangle strip: V=4, E=5, F=2.
|
||
// The interior edge (shared diagonal) has no border halfedge.
|
||
TEST(ConformalMeshTopology, QuadStrip)
|
||
{
|
||
auto mesh = make_quad_strip();
|
||
EXPECT_EQ(4u, mesh.number_of_vertices());
|
||
EXPECT_EQ(5u, mesh.number_of_edges());
|
||
EXPECT_EQ(2u, mesh.number_of_faces());
|
||
|
||
// Count interior (non-boundary) edges
|
||
int interior = 0;
|
||
for (auto e : mesh.edges())
|
||
if (!mesh.is_border(e)) ++interior;
|
||
EXPECT_EQ(1, interior) << "Only the shared diagonal should be interior";
|
||
}
|
||
|
||
// Fan with n triangles: V=n+1, E=2n, F=n.
|
||
TEST(ConformalMeshTopology, FanCounts)
|
||
{
|
||
for (int n : {3, 4, 6, 8}) {
|
||
auto mesh = make_fan(n);
|
||
EXPECT_EQ((std::size_t)(n + 1), mesh.number_of_vertices());
|
||
EXPECT_EQ((std::size_t)(2 * n), mesh.number_of_edges());
|
||
EXPECT_EQ((std::size_t)(n), mesh.number_of_faces());
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// Halfedge Traversal
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// For a regular tetrahedron every vertex has valence 3.
|
||
TEST(ConformalMeshTraversal, TetrahedronVertexValence)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
for (auto v : mesh.vertices()) {
|
||
int degree = 0;
|
||
for (auto h : CGAL::halfedges_around_target(v, mesh))
|
||
{ (void)h; ++degree; }
|
||
EXPECT_EQ(3, degree) << "Each tetrahedron vertex has degree 3";
|
||
}
|
||
}
|
||
|
||
// For a fan with n triangles the center vertex has valence n.
|
||
TEST(ConformalMeshTraversal, FanCenterValence)
|
||
{
|
||
for (int n : {3, 5, 7}) {
|
||
auto mesh = make_fan(n);
|
||
|
||
// Center vertex is always the first one added (index 0).
|
||
auto center = *mesh.vertices().begin();
|
||
int degree = 0;
|
||
for (auto h : CGAL::halfedges_around_target(center, mesh))
|
||
{ (void)h; ++degree; }
|
||
EXPECT_EQ(n, degree)
|
||
<< "Fan center vertex must have valence == n=" << n;
|
||
}
|
||
}
|
||
|
||
// Every face of the tetrahedron has exactly 3 halfedges.
|
||
TEST(ConformalMeshTraversal, FaceHalfedgeCount)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
for (auto f : mesh.faces()) {
|
||
int count = 0;
|
||
for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh))
|
||
{ (void)h; ++count; }
|
||
EXPECT_EQ(3, count) << "Each triangular face must have exactly 3 halfedges";
|
||
}
|
||
}
|
||
|
||
// opposite(h) and h share the same edge; opposite(opposite(h)) == h.
|
||
TEST(ConformalMeshTraversal, OppositeHalfedgeConsistency)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
for (auto h : mesh.halfedges()) {
|
||
auto opp = mesh.opposite(h);
|
||
EXPECT_EQ(mesh.edge(h), mesh.edge(opp))
|
||
<< "h and opposite(h) must share the same edge";
|
||
EXPECT_EQ(h, mesh.opposite(opp))
|
||
<< "opposite(opposite(h)) must equal h";
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// Property Maps
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// The conformal variable lambda can be written and read back per vertex.
|
||
TEST(ConformalMeshProperties, VertexLambdaReadWrite)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
auto [lambda, theta, idx] = add_vertex_properties(mesh);
|
||
|
||
double value = 0.0;
|
||
for (auto v : mesh.vertices()) {
|
||
lambda[v] = value;
|
||
value += 1.0;
|
||
}
|
||
|
||
value = 0.0;
|
||
for (auto v : mesh.vertices()) {
|
||
EXPECT_DOUBLE_EQ(value, lambda[v]);
|
||
value += 1.0;
|
||
}
|
||
}
|
||
|
||
// Default solver index is -1 (pinned); can be overwritten.
|
||
TEST(ConformalMeshProperties, VertexSolverIndex)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
auto [lambda, theta, idx] = add_vertex_properties(mesh);
|
||
|
||
// All vertices start at -1 (pinned / boundary)
|
||
for (auto v : mesh.vertices())
|
||
EXPECT_EQ(-1, idx[v]) << "Default solver index must be -1";
|
||
|
||
// Assign sequential indices
|
||
int i = 0;
|
||
for (auto v : mesh.vertices())
|
||
idx[v] = i++;
|
||
|
||
i = 0;
|
||
for (auto v : mesh.vertices())
|
||
EXPECT_EQ(i++, idx[v]);
|
||
}
|
||
|
||
// Edge alpha (intersection angle): set and retrieve per edge.
|
||
TEST(ConformalMeshProperties, EdgeAlpha)
|
||
{
|
||
auto mesh = make_quad_strip();
|
||
auto alpha = add_edge_properties(mesh);
|
||
|
||
const double kAlpha = M_PI / 3.0; // 60°
|
||
for (auto e : mesh.edges())
|
||
alpha[e] = kAlpha;
|
||
|
||
for (auto e : mesh.edges())
|
||
EXPECT_DOUBLE_EQ(kAlpha, alpha[e]);
|
||
|
||
EXPECT_EQ(5u, mesh.number_of_edges());
|
||
}
|
||
|
||
// Face geometry type: Euclidean by default, switchable to Hyperbolic.
|
||
TEST(ConformalMeshProperties, FaceGeometryType)
|
||
{
|
||
auto mesh = make_tetrahedron();
|
||
auto ftype = add_face_properties(mesh);
|
||
|
||
// Default: Euclidean
|
||
for (auto f : mesh.faces())
|
||
EXPECT_EQ(static_cast<int>(GeometryType::Euclidean), ftype[f]);
|
||
|
||
// Switch all to Hyperbolic
|
||
for (auto f : mesh.faces())
|
||
ftype[f] = static_cast<int>(GeometryType::Hyperbolic);
|
||
|
||
for (auto f : mesh.faces())
|
||
EXPECT_EQ(static_cast<int>(GeometryType::Hyperbolic), ftype[f]);
|
||
}
|
||
|
||
// Adding the same named property map twice: second call returns ok=false
|
||
// and both handles alias the same storage.
|
||
TEST(ConformalMeshProperties, PropertyMapIdempotent)
|
||
{
|
||
auto mesh = make_triangle();
|
||
auto [pm1, ok1] = mesh.add_property_map<Vertex_index, double>("v:lambda", 0.0);
|
||
auto [pm2, ok2] = mesh.add_property_map<Vertex_index, double>("v:lambda", 0.0);
|
||
|
||
EXPECT_TRUE(ok1) << "First add_property_map must succeed";
|
||
EXPECT_FALSE(ok2) << "Second add_property_map on existing name must return ok=false";
|
||
|
||
// Both handles must alias the same storage
|
||
auto v = *mesh.vertices().begin();
|
||
pm1[v] = 42.0;
|
||
EXPECT_DOUBLE_EQ(42.0, pm2[v]) << "Both handles must alias the same storage";
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// Mesh validity
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// CGAL's built-in validity check must pass for all factory meshes.
|
||
TEST(ConformalMeshValidity, AllBuilders)
|
||
{
|
||
EXPECT_TRUE(make_triangle().is_valid()) << "triangle mesh invalid";
|
||
EXPECT_TRUE(make_tetrahedron().is_valid()) << "tetrahedron mesh invalid";
|
||
EXPECT_TRUE(make_quad_strip().is_valid()) << "quad strip mesh invalid";
|
||
EXPECT_TRUE(make_fan(6).is_valid()) << "fan-6 mesh invalid";
|
||
}
|