diff --git a/code/include/conformal_mesh.hpp b/code/include/conformal_mesh.hpp new file mode 100644 index 0000000..f0622e7 --- /dev/null +++ b/code/include/conformal_mesh.hpp @@ -0,0 +1,94 @@ +#pragma once +// conformal_mesh.hpp +// +// Central mesh type for the discrete conformal mapping algorithms. +// Replaces the Java CoHDS (de.varylab.discreteconformal.heds.CoHDS) +// and its associated vertex/edge/face types (CoVertex, CoEdge, CoFace). +// +// Design +// ------ +// Java │ C++ (this file) +// ─────────────────────────────┼────────────────────────────────────────── +// CoHDS │ ConformalMesh (CGAL::Surface_mesh) +// CoVertex / CoEdge / CoFace │ Vertex_index / Edge_index / Face_index +// HyperIdealRadiusAdapter │ property_map +// HalfedgeInterface adapters │ named property maps ("v:lambda", …) +// +// Property-map naming convention +// ─────────────────────────────── +// "v:lambda" per-vertex log scale factor (conformal variable u_i) +// "v:theta" per-vertex target cone angle +// "v:idx" per-vertex solver DOF index (-1 = pinned / boundary) +// "e:alpha" per-edge intersection angle (α_ij, hyperbolic geometry) +// "f:type" per-face geometry type (0=Euclidean, 1=Hyperbolic, 2=Spherical) +// +// All property maps are optional; add only what a given algorithm needs. +// +// Note on descriptor types (CGAL 6.x) +// ──────────────────────────────────── +// CGAL::Surface_mesh exposes its index types as nested types: +// Surface_mesh::Vertex_index, ::Halfedge_index, ::Edge_index, ::Face_index +// The BGL graph_traits aliases expose the same types as vertex_descriptor etc., +// but those live in boost::graph_traits, not in Surface_mesh itself. +// We use the Surface_mesh member names throughout for clarity. + +#include +#include +#include + +namespace conformallab { + +// ── Kernel ────────────────────────────────────────────────────────────────── +// Simple double-precision Cartesian. Conformal mapping algorithms never +// need exact arithmetic — they operate on floating-point lengths and angles. +using Kernel = CGAL::Simple_cartesian; +using Point3 = Kernel::Point_3; +using Point2 = Kernel::Point_2; + +// ── Mesh type ──────────────────────────────────────────────────────────────── +using ConformalMesh = CGAL::Surface_mesh; + +// ── Index/descriptor aliases (CGAL 6.x naming) ─────────────────────────────── +using Vertex_index = ConformalMesh::Vertex_index; +using Halfedge_index = ConformalMesh::Halfedge_index; +using Edge_index = ConformalMesh::Edge_index; +using Face_index = ConformalMesh::Face_index; + +// ── Geometry type constant (replaces Java CoFace.type enum) ───────────────── +enum class GeometryType : int { + Euclidean = 0, + Hyperbolic = 1, + Spherical = 2 +}; + +// ── Standard property-map bundles ──────────────────────────────────────────── + +// Add the vertex properties used by all conformal-map functionals. +// Returns {lambda, theta, idx}. +inline auto add_vertex_properties(ConformalMesh& mesh) +{ + auto [lambda, ok1] = mesh.add_property_map("v:lambda", 0.0); + auto [theta, ok2] = mesh.add_property_map("v:theta", 0.0); + auto [idx, ok3] = mesh.add_property_map ("v:idx", -1); + (void)ok1; (void)ok2; (void)ok3; + return std::make_tuple(lambda, theta, idx); +} + +// Add the edge intersection-angle property used by the hyperbolic functional. +inline auto add_edge_properties(ConformalMesh& mesh) +{ + auto [alpha, ok] = mesh.add_property_map("e:alpha", 0.0); + (void)ok; + return alpha; +} + +// Add the face geometry-type property. +inline auto add_face_properties(ConformalMesh& mesh) +{ + auto [ftype, ok] = mesh.add_property_map( + "f:type", static_cast(GeometryType::Euclidean)); + (void)ok; + return ftype; +} + +} // namespace conformallab diff --git a/code/include/mesh_builder.hpp b/code/include/mesh_builder.hpp new file mode 100644 index 0000000..eafe355 --- /dev/null +++ b/code/include/mesh_builder.hpp @@ -0,0 +1,109 @@ +#pragma once +// mesh_builder.hpp +// +// Factory functions that build simple reference meshes for testing and examples. +// All functions return a ConformalMesh (CGAL::Surface_mesh). +// +// Replaces Java mesh generators: +// CoHDS generators (convex hull, hyper-ideal generator) come later (Phase 3c/4). +// These builders cover the minimal meshes needed for functional unit tests. + +#include "conformal_mesh.hpp" +#include +#include + +namespace conformallab { + +// ── Single triangle ────────────────────────────────────────────────────────── +// +// v2 +// | \ +// | \ +// v0 ─ v1 +// +// Returns a mesh with 1 face, 3 vertices, 3 edges. +// The triangle lies in the xy-plane with a right angle at v0. +inline ConformalMesh make_triangle( + double x0=0, double y0=0, + double x1=1, double y1=0, + double x2=0, double y2=1) +{ + ConformalMesh mesh; + auto v0 = mesh.add_vertex(Point3(x0, y0, 0)); + auto v1 = mesh.add_vertex(Point3(x1, y1, 0)); + auto v2 = mesh.add_vertex(Point3(x2, y2, 0)); + mesh.add_face(v0, v1, v2); + return mesh; +} + +// ── Regular tetrahedron ────────────────────────────────────────────────────── +// +// 4 vertices, 4 faces, 6 edges. +// Euler characteristic: V - E + F = 4 - 6 + 4 = 2 (sphere topology). +// Used to test closed-surface traversal. +inline ConformalMesh make_tetrahedron() +{ + ConformalMesh mesh; + + // Vertices of a regular tetrahedron centred at origin, edge length √2·2 + auto v0 = mesh.add_vertex(Point3( 1, 1, 1)); + auto v1 = mesh.add_vertex(Point3( 1, -1, -1)); + auto v2 = mesh.add_vertex(Point3(-1, 1, -1)); + auto v3 = mesh.add_vertex(Point3(-1, -1, 1)); + + // 4 outward-facing triangles (consistent winding) + mesh.add_face(v0, v2, v1); // bottom (z=-1 side) + mesh.add_face(v0, v1, v3); // front (y=-1 side) + mesh.add_face(v0, v3, v2); // left (x=-1 side) + mesh.add_face(v1, v2, v3); // back + + return mesh; +} + +// ── Two-triangle strip ─────────────────────────────────────────────────────── +// +// v2 ─ v3 +// | \ | +// v0 ─ v1 +// +// 4 vertices, 2 faces, 5 edges (1 interior edge v1–v2 shared by both faces). +// Useful for testing edge-interior vs edge-boundary distinction. +inline ConformalMesh make_quad_strip() +{ + ConformalMesh mesh; + auto v0 = mesh.add_vertex(Point3(0, 0, 0)); + auto v1 = mesh.add_vertex(Point3(1, 0, 0)); + auto v2 = mesh.add_vertex(Point3(0, 1, 0)); + auto v3 = mesh.add_vertex(Point3(1, 1, 0)); + + mesh.add_face(v0, v1, v2); // lower-left triangle + mesh.add_face(v1, v3, v2); // upper-right triangle (shares edge v1–v2) + + return mesh; +} + +// ── Regular flat polygon fan ───────────────────────────────────────────────── +// +// n triangles sharing a central vertex; forms a disk topology (boundary). +// Used to verify valence-n vertex traversal. +inline ConformalMesh make_fan(int n) +{ + CGAL_precondition(n >= 3); + ConformalMesh mesh; + + auto center = mesh.add_vertex(Point3(0, 0, 0)); + + const double dtheta = 2.0 * M_PI / n; + std::vector rim(n); + for (int i = 0; i < n; ++i) { + double a = i * dtheta; + rim[i] = mesh.add_vertex(Point3(std::cos(a), std::sin(a), 0)); + } + + for (int i = 0; i < n; ++i) + mesh.add_face(center, rim[i], rim[(i+1) % n]); + + return mesh; +} + +} // namespace conformallab diff --git a/code/tests/CMakeLists.txt b/code/tests/CMakeLists.txt index dec6b8b..89b9387 100644 --- a/code/tests/CMakeLists.txt +++ b/code/tests/CMakeLists.txt @@ -27,3 +27,8 @@ target_link_libraries(conformallab_tests PRIVATE GTest::gtest_main) include(GoogleTest) gtest_discover_tests(conformallab_tests DISCOVERY_TIMEOUT 60) + +# ── CGAL test suite (requires -DWITH_CGAL=ON) ──────────────────────────────── +if(WITH_CGAL) + add_subdirectory(cgal) +endif() diff --git a/code/tests/cgal/CMakeLists.txt b/code/tests/cgal/CMakeLists.txt new file mode 100644 index 0000000..ad1e0b3 --- /dev/null +++ b/code/tests/cgal/CMakeLists.txt @@ -0,0 +1,48 @@ +# tests/cgal/CMakeLists.txt +# +# CGAL-dependent test target. Only built when -DWITH_CGAL=ON. +# Requires Boost (find_package(Boost REQUIRED) is called in the root CMakeLists). +# +# Run with: +# cmake -S code -B build -DWITH_CGAL=ON +# cmake --build build --target conformallab_cgal_tests +# ctest --test-dir build -R cgal + +add_executable(conformallab_cgal_tests + # ── Phase 3a: mesh infrastructure ────────────────────────────────────── + test_conformal_mesh.cpp + + # ── Phase 3b: HyperIdealFunctional (to be added) ────────────────────── + # test_hyper_ideal_functional.cpp + + # ── Phase 3c: SphericalFunctional (to be added) ────────────────────── + # test_spherical_functional.cpp +) + +target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE + ${CMAKE_SOURCE_DIR}/deps/eigen-3.4.0 + ${CMAKE_SOURCE_DIR}/deps/CGAL-6.1.1/include + ${Boost_INCLUDE_DIRS} +) + +target_include_directories(conformallab_cgal_tests PRIVATE + ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(conformallab_cgal_tests PRIVATE + CGAL_DISABLE_GMP + CGAL_DISABLE_MPFR +) + +# Suppress warnings from CGAL/Boost headers +target_compile_options(conformallab_cgal_tests PRIVATE + $<$:-Wno-unused-parameter> +) + +target_link_libraries(conformallab_cgal_tests PRIVATE GTest::gtest_main) + +include(GoogleTest) +gtest_discover_tests(conformallab_cgal_tests + TEST_PREFIX "cgal." + DISCOVERY_TIMEOUT 60 +) diff --git a/code/tests/cgal/test_conformal_mesh.cpp b/code/tests/cgal/test_conformal_mesh.cpp new file mode 100644 index 0000000..17f9d4a --- /dev/null +++ b/code/tests/cgal/test_conformal_mesh.cpp @@ -0,0 +1,240 @@ +// 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"; +}