test: Java-Geometrie-Utility-Tests portiert (Tests 1–6) + Genus-2-TODO-Stub
All checks were successful
C++ Tests / test-fast (push) Successful in 2m4s
C++ Tests / test-cgal (push) Has been skipped

Portiert aus CuttinUtilityTest, UnwrapUtilityTest, ConvergenceUtilityTests
und HomologyTest (Java-Quelldatei unifgeo/test). Jetzt 170 CGAL-Tests.

Neue Suiten in test_geometry_utils.cpp:
  CuttingUtility       — point_in_triangle_2d (3 Tests, Java-Test 1–2)
  UnwrapUtility        — Eckwinkel law-of-cosines (2 Tests, Java-Test 3)
  ConvergenceUtility   — circumradius + scale-invariant R_f/sqrt(A) (6 Tests, Java-Test 4–6)
  HomologyGenerators   — GTEST_SKIP-Stub für Genus-2 (Java-Test 7, Phase 8)

Der Stub dokumentiert genau was fehlt (Genus-2-Mesh), welcher Code nötig ist
(compute_cut_graph → 4 cut edges) und die Java-Herkunft.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-14 11:09:27 +02:00
parent 62e2875c30
commit a4438c9a1a
2 changed files with 348 additions and 0 deletions

View File

@@ -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 16).
# Test 7 (Genus-2-Homologie) als GTEST_SKIP-Stub bis Phase 8.
test_geometry_utils.cpp
)
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE

View File

@@ -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 12 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 45 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 <gtest/gtest.h>
#include <Eigen/Dense>
#include <array>
#include <cmath>
#include <vector>
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<double, 3> scale_invariant_circumradius_stats(
const std::vector<Eigen::Vector2d>& verts,
const std::vector<std::array<int, 3>>& 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<double>(faces.size());
return {max_r, mean_r, sum_r};
}
// ════════════════════════════════════════════════════════════════════════════
// Tests 12 — 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<Eigen::Vector2d> 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<std::array<int, 3>> 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<Eigen::Vector2d> 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<std::array<int, 3>> 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<Set<CoEdge>> 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).";
}