// 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) 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 #include #include 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(GeometryType::Euclidean), ftype[f]); // Switch all to Hyperbolic for (auto f : mesh.faces()) ftype[f] = static_cast(GeometryType::Hyperbolic); for (auto f : mesh.faces()) EXPECT_EQ(static_cast(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("v:lambda", 0.0); auto [pm2, ok2] = mesh.add_property_map("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"; }