diff --git a/code/tests/cgal/CMakeLists.txt b/code/tests/cgal/CMakeLists.txt index 43a31a5..191b64a 100644 --- a/code/tests/cgal/CMakeLists.txt +++ b/code/tests/cgal/CMakeLists.txt @@ -43,6 +43,12 @@ add_executable(conformallab_cgal_tests # ── Phase 7: Java-parity layout — MobiusMap, priority BFS, halfedge_uv, # period matrix, fundamental domain, tiling test_phase7.cpp + + # ── Java-Parität: Geometrie-Utility-Tests ───────────────────────────────── + # Portiert aus CuttinUtilityTest, UnwrapUtilityTest, + # ConvergenceUtilityTests, HomologyTest (Tests 1–6). + # Test 7 (Genus-2-Homologie) als GTEST_SKIP-Stub bis Phase 8. + test_geometry_utils.cpp ) target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE diff --git a/code/tests/cgal/test_geometry_utils.cpp b/code/tests/cgal/test_geometry_utils.cpp new file mode 100644 index 0000000..6b85abd --- /dev/null +++ b/code/tests/cgal/test_geometry_utils.cpp @@ -0,0 +1,342 @@ +// test_geometry_utils.cpp +// +// Portierung der Java ConformalLab Geometrie-Utility-Tests. +// +// Java-Quelle Java-Testmethode Status +// ───────────────────────────────────────────────────────────────────────────────────── +// CuttinUtilityTest.java testIsInConvexTextureFace_False PORTIERT +// CuttinUtilityTest.java testIsInConvexTextureFace_True PORTIERT +// UnwrapUtilityTest.java testGetAngleReturnsPI PORTIERT +// ConvergenceUtilityTests.java testGetTextureCircumRadius PORTIERT +// ConvergenceUtilityTests.java testGetTextureTriangleArea PORTIERT +// ConvergenceUtilityTests.java testScaleInvariantCircumCircleRadius PORTIERT +// HomologyTest.java testHomology GEBLOCKT +// +// ─── Geometrische Grundlage ────────────────────────────────────────────────────────── +// +// Tests 1–2 Punkt-in-konvexem-Dreieck (2D UV-Raum, baryzentrische Vorzeichen-Methode) +// Java: CuttingUtility.isInConvexTextureFace(pp, face, adapters) +// Hinweis: Java-Test 2 hat ein 5-elementiges T-Array mit w=0 (Punkt im +// Unendlichen), was ein Tippfehler im Original ist. Hier werden +// äquivalente, wohlgeformte Koordinaten verwendet. +// +// Test 3 Eckenwinkel für kollineare Vertices über den Kosinussatz. +// Java: UnwrapUtility.getAngle(edge, adapters) — gibt den Winkel am +// Zielknoten zurück. Für v0=(-1,0,0), v1=(0,0,0), v2=(1,0,0) ist +// der Winkel bei v1 genau π (Dreiecksungleichung entartet). +// +// Tests 4–5 2D Umkreisradius und Dreiecksfläche. +// Java: ConvergenceUtility.getTextureCircumCircleRadius(face) +// ConvergenceUtility.getTextureTriangleArea(face) +// Formeln: Area = |det([B-A, C-A])| / 2 +// R = (a·b·c) / (4·Area) +// +// Test 6 Skaleninvarianter Umkreisradius über ein Mesh. +// Java: ConvergenceUtility.getMaxMeanSumScaleInvariantCircumRadius(hds) +// Gibt [max, mean, sum] von R_f / sqrt(total_texture_area) zurück. +// Invariant unter uniformer Skalierung der Texturkoordinaten (Test mit +// homogenem Gewicht w: Position = (T[0]/w, T[1]/w)). +// +// ─── GEBLOCKT (Test 7) ─────────────────────────────────────────────────────────────── +// +// Test 7 Genus-2 Homologie-Generatoren +// Java: HomologyTest.testHomology +// Erwartet: getGeneratorPaths(root, ...).size() == 4 (2g = 4 für g = 2) +// C++-Äquivalent: compute_cut_graph(mesh).cut_edge_indices.size() == 4 +// BLOCKED: Kein Genus-2-Testmesh in mesh_builder.hpp vorhanden. +// TODO(Phase 8): make_genus2_surface() in mesh_builder.hpp implementieren +// oder brezel2.obj via load_mesh importieren, dann GTEST_SKIP entfernen. +// +// ───────────────────────────────────────────────────────────────────────────────────── + +#include "cut_graph.hpp" // für Test 7 (Genus-2 TODO) +#include "conformal_mesh.hpp" +#include "mesh_builder.hpp" +#include +#include +#include +#include +#include + +using namespace conformallab; + +// ───────────────────────────────────────────────────────────────────────────── +// Lokale Geometrie-Hilfsfunktionen +// (portiert aus Java CuttingUtility / ConvergenceUtility) +// ───────────────────────────────────────────────────────────────────────────── + +/// Punkt-in-Dreieck Test (2D, baryzentrische Vorzeichenmethode). +/// Gibt true zurück wenn p strikt innerhalb oder auf dem Rand von v0-v1-v2 liegt. +/// Java: CuttingUtility.isInConvexTextureFace +static bool point_in_triangle_2d( + Eigen::Vector2d p, + Eigen::Vector2d v0, Eigen::Vector2d v1, Eigen::Vector2d v2) +{ + auto cross2d = [](Eigen::Vector2d a, Eigen::Vector2d b) -> double { + return a.x() * b.y() - a.y() * b.x(); + }; + double d0 = cross2d(v1 - v0, p - v0); + double d1 = cross2d(v2 - v1, p - v1); + double d2 = cross2d(v0 - v2, p - v2); + bool has_neg = (d0 < 0.0) || (d1 < 0.0) || (d2 < 0.0); + bool has_pos = (d0 > 0.0) || (d1 > 0.0) || (d2 > 0.0); + return !(has_neg && has_pos); +} + +/// 2D Dreiecksfläche (halbes Kreuzprodukt). +/// Java: ConvergenceUtility.getTextureTriangleArea +static double triangle_area_2d( + Eigen::Vector2d A, Eigen::Vector2d B, Eigen::Vector2d C) +{ + return std::abs((B - A).x() * (C - A).y() + - (B - A).y() * (C - A).x()) * 0.5; +} + +/// 2D Umkreisradius: R = (a·b·c) / (4·Area). +/// Java: ConvergenceUtility.getTextureCircumCircleRadius +static double circumradius_2d( + Eigen::Vector2d A, Eigen::Vector2d B, Eigen::Vector2d C) +{ + double a = (B - C).norm(); + double b = (A - C).norm(); + double c = (A - B).norm(); + double area = triangle_area_2d(A, B, C); + if (area < 1e-14) return 0.0; + return (a * b * c) / (4.0 * area); +} + +/// Skaleninvarianter Umkreisradius für ein Mesh: +/// scale_R_f = R_f / sqrt(total_area) +/// Gibt {max, mean, sum} über alle Flächen zurück. +/// Java: ConvergenceUtility.getMaxMeanSumScaleInvariantCircumRadius +/// +/// Homogene Koordinaten: Position = (x/w, y/w). +static std::array scale_invariant_circumradius_stats( + const std::vector& verts, + const std::vector>& faces) +{ + // Gesamtfläche + double total_area = 0.0; + for (auto& f : faces) + total_area += triangle_area_2d(verts[f[0]], verts[f[1]], verts[f[2]]); + if (total_area < 1e-14) return {0, 0, 0}; + + double sqrt_total = std::sqrt(total_area); + double max_r = 0.0, sum_r = 0.0; + for (auto& f : faces) { + double R = circumradius_2d(verts[f[0]], verts[f[1]], verts[f[2]]); + double sr = R / sqrt_total; + max_r = std::max(max_r, sr); + sum_r += sr; + } + double mean_r = sum_r / static_cast(faces.size()); + return {max_r, mean_r, sum_r}; +} + +// ════════════════════════════════════════════════════════════════════════════ +// Tests 1–2 — CuttingUtility: Punkt-in-konvexem-Dreieck (2D UV-Raum) +// Java: CuttinUtilityTest.testIsInConvexTextureFace_False / _True +// ════════════════════════════════════════════════════════════════════════════ + +// Test 1: Punkt liegt weit außerhalb — exakte Java-Koordinaten +TEST(CuttingUtility, IsInConvexTextureFace_False) +{ + // Winziges Dreieck um (0.7488, 0.0629) — Java-Testkoordinaten (T[3]=1, w=1) + Eigen::Vector2d v0(0.7488102998904661, 0.06293998610761144); + Eigen::Vector2d v1(0.7487811940754379, 0.06289451051246124); + Eigen::Vector2d v2(0.7487254625255592, 0.06291429499873116); + // Testpunkt weit entfernt bei (0.447, 0.000228) + Eigen::Vector2d pp(0.44661534423161037, 2.2808373704822393e-4); + + EXPECT_FALSE(point_in_triangle_2d(pp, v0, v1, v2)); +} + +// Test 2: Punkt liegt innerhalb +// Hinweis: Das originale Java-Array p2 hat 5 Elemente mit w=0 (Tippfehler im +// Java-Original). Hier werden äquivalente, wohlgeformte Koordinaten verwendet, +// die dasselbe geometrische Szenario abbilden. +TEST(CuttingUtility, IsInConvexTextureFace_True) +{ + // Dreieck: (0,0) — (1e-8, 0) — (0, 1e-8) + Eigen::Vector2d v0(0.0, 0.0); + Eigen::Vector2d v1(1e-8, 0.0); + Eigen::Vector2d v2(0.0, 1e-8); + // Schwerpunkt des Dreiecks — liegt immer innen + Eigen::Vector2d pp(1e-8 / 3.0, 1e-8 / 3.0); + + EXPECT_TRUE(point_in_triangle_2d(pp, v0, v1, v2)); +} + +// Zusätzlich: einfaches Einheitsdreieck für Klarheit +TEST(CuttingUtility, IsInConvexTextureFace_UnitTriangle_InAndOut) +{ + Eigen::Vector2d v0(0.0, 0.0), v1(1.0, 0.0), v2(0.0, 1.0); + EXPECT_TRUE( point_in_triangle_2d(Eigen::Vector2d(0.25, 0.25), v0, v1, v2)); + EXPECT_FALSE(point_in_triangle_2d(Eigen::Vector2d(2.0, 2.0), v0, v1, v2)); + EXPECT_FALSE(point_in_triangle_2d(Eigen::Vector2d(0.6, 0.6), v0, v1, v2)); // jenseits Hypotenuse +} + +// ════════════════════════════════════════════════════════════════════════════ +// Test 3 — UnwrapUtility: Eckenwinkel = π für kollineare Vertices +// Java: UnwrapUtilityTest.testGetAngleReturnsPI +// ════════════════════════════════════════════════════════════════════════════ + +// Java: v0=(-1,0,0), v1=(0,0,0), v2=(1,0,0) kollinear. +// Kante e von v2 nach v1. getAngle(e) = Winkel bei v1 = π. +// +// C++: Kosinussatz mit Kantenlängen a=|v0-v1|=1, b=|v1-v2|=1, c=|v0-v2|=2. +// cos(γ_v1) = (a² + b² − c²) / (2ab) = (1 + 1 − 4) / 2 = −1 → γ = π +TEST(UnwrapUtility, GetAngle_CollinearVertices_ReturnsPI) +{ + const double a = 1.0; // |v0 − v1| + const double b = 1.0; // |v1 − v2| + const double c = 2.0; // |v0 − v2| (= a + b, entartet) + double cos_angle = (a*a + b*b - c*c) / (2.0 * a * b); + cos_angle = std::max(-1.0, std::min(1.0, cos_angle)); // numerisches Clamp + double angle = std::acos(cos_angle); + EXPECT_NEAR(M_PI, angle, 1e-15); +} + +// Gegenkontrolle: gleichseitiges Dreieck → Winkel = π/3 +TEST(UnwrapUtility, GetAngle_EquilateralTriangle_ReturnsPiOver3) +{ + const double s = 1.0; + double cos_angle = (s*s + s*s - s*s) / (2.0 * s * s); // = 0.5 + double angle = std::acos(cos_angle); + EXPECT_NEAR(M_PI / 3.0, angle, 1e-15); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Test 4 — ConvergenceUtility: 2D Umkreisradius +// Java: ConvergenceUtilityTests.testGetTextureCircumRadius +// ════════════════════════════════════════════════════════════════════════════ + +TEST(ConvergenceUtility, TextureCircumRadius_RightTriangle) +{ + // A=(0,0), B=(1,0), C=(0,1): rechtwinkliges gleichschenkliges Dreieck + // Seiten: 1, 1, √2. R = √2 / (4 · 0.5) = √2/2 + Eigen::Vector2d A(0.0, 0.0), B(1.0, 0.0), C(0.0, 1.0); + EXPECT_NEAR(std::sqrt(2.0) / 2.0, circumradius_2d(A, B, C), 1e-10); +} + +TEST(ConvergenceUtility, TextureCircumRadius_SmallerTriangle) +{ + // A=(0,0), B=(0.5,0.5), C=(0,1): Java-Variante mit B.T={0.5,0.5,0,1} + // Seiten: √0.5, √0.5, 1. Area = 0.25. R = (√0.5·√0.5·1)/(4·0.25) = 0.5 + Eigen::Vector2d A(0.0, 0.0), B(0.5, 0.5), C(0.0, 1.0); + EXPECT_NEAR(0.5, circumradius_2d(A, B, C), 1e-10); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Test 5 — ConvergenceUtility: 2D Dreiecksfläche +// Java: ConvergenceUtilityTests.testGetTextureTriangleArea +// ════════════════════════════════════════════════════════════════════════════ + +TEST(ConvergenceUtility, TextureTriangleArea_RightTriangle) +{ + // A=(0,0), B=(1,0), C=(0,1) → Fläche = 0.5 + Eigen::Vector2d A(0.0, 0.0), B(1.0, 0.0), C(0.0, 1.0); + EXPECT_NEAR(0.5, triangle_area_2d(A, B, C), 1e-10); +} + +TEST(ConvergenceUtility, TextureTriangleArea_SmallerTriangle) +{ + // A=(0,0), B=(0.5,0.5), C=(0,1) → Fläche = 0.25 + Eigen::Vector2d A(0.0, 0.0), B(0.5, 0.5), C(0.0, 1.0); + EXPECT_NEAR(0.25, triangle_area_2d(A, B, C), 1e-10); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Test 6 — ConvergenceUtility: Skaleninvarianter Umkreisradius +// Java: ConvergenceUtilityTests.testScaleInvariantCircumCircleRadius +// +// Mesh: 4 Vertices (v1..v4), 2 Flächen (f1: v1-v2-v3, f2: v1-v3-v4). +// Skaleninvariante Größe: R_f / sqrt(total_area) — invariant unter +// uniformer Skalierung (homogeneous weight w: pos = (x/w, y/w)). +// ════════════════════════════════════════════════════════════════════════════ + +TEST(ConvergenceUtility, ScaleInvariantCircumRadius_BaseScale) +{ + // Positionen bei w=1 (T[3]=1): v1=(0,0), v2=(1,0), v3=(0,1), v4=(-1,0) + std::vector verts = { + {0.0, 0.0}, // v1 + {1.0, 0.0}, // v2 + {0.0, 1.0}, // v3 + {-1.0, 0.0}, // v4 + }; + // f1: v1-v2-v3, f2: v1-v3-v4 + std::vector> faces = { {0, 1, 2}, {0, 2, 3} }; + + // Einzelflächen-Prüfung (Java testGetTextureTriangleArea-Anforderung) + EXPECT_NEAR(0.5, triangle_area_2d(verts[0], verts[1], verts[2]), 1e-10); + EXPECT_NEAR(0.5, triangle_area_2d(verts[0], verts[2], verts[3]), 1e-10); + + auto [max_r, mean_r, sum_r] = scale_invariant_circumradius_stats(verts, faces); + + // Erwartet: sin(π/4) = √2/2 für max und mean (beide Dreiecke identisch) + EXPECT_NEAR(std::sin(M_PI / 4.0), max_r, 1e-10); + EXPECT_NEAR(std::sin(M_PI / 4.0), mean_r, 1e-10); + EXPECT_NEAR(2.0 * std::sin(M_PI / 4.0), sum_r, 1e-10); +} + +TEST(ConvergenceUtility, ScaleInvariantCircumRadius_HalvedByW2_SameResult) +{ + // Skalierung durch w=2: alle Positionen halbiert (homogene Koordinaten) + // pos_scaled = (T[0]/2, T[1]/2) + std::vector verts = { + {0.0, 0.0}, // v1/2 + {0.5, 0.0}, // v2/2 + {0.0, 0.5}, // v3/2 + {-0.5, 0.0}, // v4/2 + }; + std::vector> faces = { {0, 1, 2}, {0, 2, 3} }; + + // Flächen sind ein Viertel der ursprünglichen (Längen halbiert → Area / 4) + EXPECT_NEAR(0.125, triangle_area_2d(verts[0], verts[1], verts[2]), 1e-10); + EXPECT_NEAR(0.125, triangle_area_2d(verts[0], verts[2], verts[3]), 1e-10); + + auto [max_r, mean_r, sum_r] = scale_invariant_circumradius_stats(verts, faces); + + // Skaleninvariante Größe muss identisch zu w=1 sein + EXPECT_NEAR(std::sin(M_PI / 4.0), max_r, 1e-10); + EXPECT_NEAR(std::sin(M_PI / 4.0), mean_r, 1e-10); + EXPECT_NEAR(2.0 * std::sin(M_PI / 4.0), sum_r, 1e-10); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Test 7 — HomologyTest: Genus-2 Homologie-Generatoren +// Java: HomologyTest.testHomology +// +// GEBLOCKT — kein Genus-2-Testmesh vorhanden. +// +// Java-Test: +// CoHDS hds = TestUtility.readOBJ("brezel2.obj"); // Genus-2-Brezel-Fläche +// List> paths = getGeneratorPaths(hds.getVertex(0), weightAdapter); +// Assert.assertEquals(4, paths.size()); // 2g = 4 für g = 2 +// +// C++-Äquivalent (sobald entsprechendes Mesh verfügbar): +// ConformalMesh mesh = load_mesh("brezel2.obj"); // oder make_genus2_surface() +// CutGraph cg = compute_cut_graph(mesh); +// EXPECT_EQ(4u, cg.cut_edge_indices.size()); // 2g = 4 +// EXPECT_EQ(2, cg.genus); +// +// TODO(Phase 8): Eine der folgenden Optionen implementieren und GTEST_SKIP entfernen: +// Option A — Programmatisch: mesh_builder.hpp um make_genus2_surface() erweitern. +// Ein Genus-2-Mesh lässt sich als zwei miteinander verbundene Tori +// konstruieren (handle attachment). +// Option B — Dateibasiert: brezel2.obj aus dem Java-Projekt (Pfad: +// conformallab/src-test/.../brezel2.obj) via load_mesh importieren. +// Erfordert den Dateipfad zur Laufzeit als CMake-Variable. +// ════════════════════════════════════════════════════════════════════════════ + +TEST(HomologyGenerators, Genus2_FourGeneratorPaths_BLOCKED) +{ + GTEST_SKIP() + << "TODO(Phase 8): Genus-2-Testmesh fehlt.\n" + " Sobald mesh_builder.hpp make_genus2_surface() bereitstellt\n" + " oder brezel2.obj via load_mesh importiert wird, hier prüfen:\n" + " CutGraph cg = compute_cut_graph(mesh);\n" + " EXPECT_EQ(4u, cg.cut_edge_indices.size()); // 2g = 4 fuer g = 2\n" + " EXPECT_EQ(2, cg.genus);\n" + " Java-Quelle: HomologyTest.testHomology (brezel2.obj, 4 Generatoren)."; +}