Files
ConformalLabpp/code/tests/cgal/test_geometry_utils.cpp
Tarik Moussa a4438c9a1a
All checks were successful
C++ Tests / test-fast (push) Successful in 2m4s
C++ Tests / test-cgal (push) Has been skipped
test: Java-Geometrie-Utility-Tests portiert (Tests 1–6) + Genus-2-TODO-Stub
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>
2026-05-14 11:09:27 +02:00

343 lines
18 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_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).";
}