Files
ConformalLabpp/code/tests/cgal/test_conformal_mesh.cpp
Tarik Moussa bf9c323d60 feat(phase3a): introduce CGAL Surface_mesh as ConformalMesh foundation
Replaces the Java CoHDS with CGAL::Surface_mesh<Point3> (Simple_cartesian
kernel). Adds domain-specific property maps for lambda/theta/idx/alpha and
face geometry type — the direct C++ equivalent of CoVertex/CoEdge adapters.

New files:
  include/conformal_mesh.hpp   — ConformalMesh type + property-map helpers
  include/mesh_builder.hpp     — mesh factories (triangle, tetrahedron,
                                 quad-strip, fan) for tests and examples
  tests/cgal/                  — second test executable (conformallab_cgal_tests)
                                 built only with -DWITH_CGAL=ON

Test results (local, -DWITH_CGAL=ON):
  conformallab_tests:      36 registered | 23 passed | 13 skipped | 0 failed
  conformallab_cgal_tests: 14 registered | 14 passed |  0 skipped | 0 failed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:36:21 +02:00

241 lines
8.7 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.

// 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";
}