Mesh-Dateien aus Java-Referenzimplementierung übernommen:
code/data/obj/cathead.obj — offenes Mesh (Java: cathead.obj)
code/data/obj/tetraflat.obj — flaches Tetraeder (Java: tetraflat.obj)
code/data/obj/brezel.obj — Genus-1-Brezel (Java: brezel.obj)
code/data/obj/brezel2.obj — Genus-2-Brezel, V=2622 F=5248 χ=−2 (Java: brezel2.obj)
code/.gitignore: !data/**/*.obj — Mesh-Daten von *.obj-Regel ausgenommen.
Neue Tests in test_geometry_utils.cpp:
HomologyGenerators.Genus2_FourCutEdges [vorher: GTEST_SKIP]
Java: HomologyTest.testHomology — brezel2.obj, expects paths.size()==4
C++: compute_cut_graph(brezel2) → cut_edge_indices.size()==4, genus==2
EuclideanLayout.DoLayout_TetraFlat_EdgeLengthsPreserved [neu]
Java: EuclideanLayoutTest.testDoLayout — tetraflat.obj, u=0, l3D==lUV (1e-11)
C++: euclidean_layout(tetraflat, x=0) → alle UV-Kantenlängen == 3D (1e-10)
EuclideanLayout.CatHead_NewtonConverges_AngleSumsTwoPi [neu]
Java: EuclideanLayoutTest.testLayout02 + EuclideanCyclicConvergenceTest
C++: newton_euclidean(cathead) konvergiert, Gradientenreste < 1e-6
SphericalLayout.SphericalTetrahedron_NewtonConverges_AngleSumsTwoPi [neu]
Java: SphericalConvergenceTest.testSphericalConvergence
C++: newton_spherical(sph_tetrahedron) konvergiert, Winkeldefekte < 1e-6
CMakeLists.txt: CONFORMALLAB_DATA_DIR=${CMAKE_SOURCE_DIR}/data als Compile-Def.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
511 lines
26 KiB
C++
511 lines
26 KiB
C++
// 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 PORTIERT
|
||
// EuclideanLayoutTest.java testDoLayout PORTIERT
|
||
// EuclideanCyclicConvergenceTest.java testEuclideanConvergence PORTIERT
|
||
// SphericalConvergenceTest.java testSphericalConvergence PORTIERT
|
||
//
|
||
// ─── 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)).
|
||
//
|
||
// Test 7 Genus-2 Homologie-Generatoren.
|
||
// Java: HomologyTest.testHomology (brezel2.obj)
|
||
// Erwartet: getGeneratorPaths(root).size() == 4 (2g = 4 für g = 2)
|
||
// C++: compute_cut_graph(mesh).cut_edge_indices.size() == 4
|
||
// Mesh: code/data/obj/brezel2.obj (V=2622, F=5248, χ=−2, g=2)
|
||
// Pfad zur Compile-Zeit via CONFORMALLAB_DATA_DIR (CMakeLists.txt).
|
||
//
|
||
// Tests 8–9 Layout-Kanten-Längenerhalt (tetraflat.obj).
|
||
// Java: EuclideanLayoutTest.testDoLayout
|
||
// Nach Layout mit u=0 müssen UV-Kantenlängen == 3D-Kantenlängen (±1e-10).
|
||
//
|
||
// Test 10 Euklidischer Newton auf cathead.obj — Konvergenz + Winkeldefekt.
|
||
// Java: EuclideanLayoutTest.testLayout02 (130-Werte-Array für cathead.heml)
|
||
// C++: Newton ab u=0, prüft Konvergenz + Σα_v ≈ 2π für alle inneren Knoten.
|
||
//
|
||
// Test 11 Sphärischer Newton auf Oktaeder — Konvergenz + Winkeldefekt.
|
||
// Java: SphericalConvergenceTest.testSphericalConvergence (Oktaeder, zufällig
|
||
// störe Radien, seed=1). C++: konstruierter regulärer Oktaeder, prüft
|
||
// Konvergenz und dass Σα_v ≈ 2π (Target für Sphäre nach prepareInvariantData).
|
||
//
|
||
// ─────────────────────────────────────────────────────────────────────────────────────
|
||
|
||
#include "cut_graph.hpp"
|
||
#include "gauss_bonnet.hpp"
|
||
#include "conformal_mesh.hpp"
|
||
#include "mesh_builder.hpp"
|
||
#include "mesh_io.hpp"
|
||
#include "euclidean_functional.hpp"
|
||
#include "spherical_functional.hpp"
|
||
#include "newton_solver.hpp"
|
||
#include "layout.hpp"
|
||
#include <gtest/gtest.h>
|
||
#include <Eigen/Dense>
|
||
#include <array>
|
||
#include <cmath>
|
||
#include <string>
|
||
#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 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<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
|
||
//
|
||
// 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:
|
||
// ConformalMesh mesh = load_mesh("code/data/obj/brezel2.obj");
|
||
// CutGraph cg = compute_cut_graph(mesh);
|
||
// EXPECT_EQ(4u, cg.cut_edge_indices.size()); // 2g = 4
|
||
// EXPECT_EQ(2, cg.genus);
|
||
//
|
||
// Mesh: V=2622, F=5248, E=7872, χ=−2, genus=2.
|
||
// Pfad via CONFORMALLAB_DATA_DIR (CMakeLists.txt: ${CMAKE_SOURCE_DIR}/data).
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(HomologyGenerators, Genus2_FourCutEdges)
|
||
{
|
||
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/brezel2.obj";
|
||
ConformalMesh mesh;
|
||
ASSERT_NO_THROW(mesh = load_mesh(path)) << "brezel2.obj not found at: " << path;
|
||
|
||
// Topology check: genus-2 surface has χ = -2.
|
||
EXPECT_EQ(-2, euler_characteristic(mesh));
|
||
|
||
// Tree-cotree algorithm must produce exactly 2g = 4 cut edges.
|
||
CutGraph cg = compute_cut_graph(mesh);
|
||
EXPECT_EQ(4u, cg.cut_edge_indices.size())
|
||
<< "Genus-2 surface must have 2g = 4 cut edges (homology generators).";
|
||
EXPECT_EQ(2, cg.genus);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Tests 8–9 — EuclideanLayoutTest: Kantenlängenerhalt auf tetraflat.obj
|
||
// Java: EuclideanLayoutTest.testDoLayout
|
||
//
|
||
// Java-Test:
|
||
// Vector u = new SparseVector(n); // u = 0 (kein konformer Faktor)
|
||
// EuclideanLayout.doLayout(hds, fun, u);
|
||
// for (CoEdge e : hds.getEdges())
|
||
// assertEquals(Pn.distanceBetween(s.P, t.P), Pn.distanceBetween(s.T, t.T), 1E-11);
|
||
//
|
||
// Bedeutung: Mit u=0 ist der konforme Faktor 0, also ℓ̃ = ℓ (keine Verformung).
|
||
// Das Layout muss die ursprünglichen 3D-Kantenlängen exakt reproduzieren.
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(EuclideanLayout, DoLayout_TetraFlat_EdgeLengthsPreserved)
|
||
{
|
||
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/tetraflat.obj";
|
||
ConformalMesh mesh;
|
||
ASSERT_NO_THROW(mesh = load_mesh(path)) << "tetraflat.obj not found at: " << path;
|
||
|
||
auto maps = setup_euclidean_maps(mesh);
|
||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||
|
||
// u = 0: no conformal deformation — layout must preserve 3D edge lengths exactly.
|
||
// tetraflat.obj is an open mesh; pin boundary vertices, sequential DOFs interior.
|
||
int idx = 0;
|
||
for (auto v : mesh.vertices())
|
||
maps.v_idx[v] = mesh.is_border(v) ? -1 : idx++;
|
||
const int n = idx;
|
||
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||
|
||
Layout2D layout = euclidean_layout(mesh, x, maps);
|
||
|
||
// For every edge: UV length must equal 3D length within 1e-10.
|
||
for (auto e : mesh.edges()) {
|
||
auto h = mesh.halfedge(e);
|
||
auto vs = mesh.source(h);
|
||
auto vt = mesh.target(h);
|
||
|
||
auto ps = mesh.point(vs);
|
||
auto pt = mesh.point(vt);
|
||
double l3d = std::sqrt(
|
||
(pt.x()-ps.x())*(pt.x()-ps.x()) +
|
||
(pt.y()-ps.y())*(pt.y()-ps.y()) +
|
||
(pt.z()-ps.z())*(pt.z()-ps.z()));
|
||
|
||
auto us = layout.uv[vs.idx()];
|
||
auto ut = layout.uv[vt.idx()];
|
||
double luv = (ut - us).norm();
|
||
|
||
EXPECT_NEAR(l3d, luv, 1e-10)
|
||
<< "Edge " << e.idx() << ": 3D=" << l3d << " UV=" << luv;
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Test 10 — EuclideanCyclicConvergenceTest: Newton auf cathead.obj
|
||
// Java: EuclideanLayoutTest.testLayout02 (130-Werte-Regression auf cathead.heml)
|
||
// EuclideanCyclicConvergenceTest.testEuclideanConvergence
|
||
//
|
||
// Java-Test:
|
||
// EuclideanLayout.doLayout(hdsCat, fun, uCat);
|
||
// for (CoVertex v : interior vertices)
|
||
// assertEquals(2*PI, calculateAngleSum(v), 1E-6);
|
||
// for (CoEdge e : positiveEdges)
|
||
// assertEquals(fun.getNewLength(e, u), tLength, 1E-6);
|
||
//
|
||
// C++-Äquivalent: Newton converges on cathead.obj; interior angle sums ≈ 2π.
|
||
// The 130-value u-vector from the Java test is cathead-topology-specific and
|
||
// depends on vertex ordering in the Java CoHDS — not portable directly.
|
||
// Instead we verify the same mathematical invariant: convergence + angle sums.
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(EuclideanLayout, CatHead_NewtonConverges_AngleSumsTwoPi)
|
||
{
|
||
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/cathead.obj";
|
||
ConformalMesh mesh;
|
||
ASSERT_NO_THROW(mesh = load_mesh(path)) << "cathead.obj not found at: " << path;
|
||
|
||
auto maps = setup_euclidean_maps(mesh);
|
||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||
|
||
// cathead.obj is an open mesh (boundary present).
|
||
// Pin boundary vertices (v_idx = -1), assign sequential DOFs to interior.
|
||
int idx = 0;
|
||
for (auto v : mesh.vertices())
|
||
maps.v_idx[v] = mesh.is_border(v) ? -1 : idx++;
|
||
const int n = idx;
|
||
ASSERT_GT(n, 0) << "No interior vertices found in cathead.obj";
|
||
|
||
enforce_gauss_bonnet(mesh, maps);
|
||
|
||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||
|
||
auto res = newton_euclidean(mesh, x0, maps, 1e-8, 200);
|
||
EXPECT_TRUE(res.converged)
|
||
<< "Newton did not converge on cathead.obj (iterations=" << res.iterations
|
||
<< ", |G|inf=" << res.grad_inf_norm << ")";
|
||
EXPECT_LT(res.grad_inf_norm, 1e-8);
|
||
EXPECT_LT(res.iterations, 200);
|
||
|
||
// After convergence: all interior vertex angle sums must equal θ_v (2π for flat).
|
||
// Matches Java: assertEquals(2*PI, calculateAngleSum(v), 1E-6) for interior v.
|
||
auto G_final = euclidean_gradient(mesh, res.x, maps);
|
||
for (std::size_t i = 0; i < G_final.size(); ++i)
|
||
EXPECT_NEAR(0.0, G_final[i], 1e-6)
|
||
<< "Angle sum residual at DOF " << i << " = " << G_final[i];
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Test 11 — SphericalConvergenceTest: Newton auf Oktaeder
|
||
// Java: SphericalConvergenceTest.testSphericalConvergence
|
||
//
|
||
// Java-Test:
|
||
// FunctionalTest.createOctahedron(hds, aSet);
|
||
// // randomly perturb vertex radii (seed=1)
|
||
// prepareInvariantDataHyperbolicAndSpherical(functional, hds, aSet, u);
|
||
// optimizer.minimize(u, opt);
|
||
// for (CoVertex v) assertEquals(2*PI, sum of angles at v, 1E-8);
|
||
//
|
||
// C++: regulärer Oktaeder (alle Knoten auf S², keine Störung), sphärischer Newton,
|
||
// prüft Konvergenz + Restgradienten (≡ Winkeldefekt = 0 nach Konvergenz).
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(SphericalLayout, SphericalTetrahedron_NewtonConverges_AngleSumsTwoPi)
|
||
{
|
||
// Build a spherical tetrahedron (genus 0, 4 vertices, 4 faces).
|
||
// Java uses a randomly-perturbed octahedron; we use the canonical
|
||
// spherical tetrahedron from mesh_builder.hpp for reproducibility.
|
||
ConformalMesh mesh = make_spherical_tetrahedron();
|
||
|
||
auto maps = setup_spherical_maps(mesh);
|
||
compute_lambda0_from_mesh(mesh, maps); // SphericalMaps version
|
||
int n = assign_vertex_dof_indices(mesh, maps); // pins gauge_vertex, assigns DOFs
|
||
// Note: enforce_gauss_bonnet not needed — natural theta from mesh satisfies Σ(2π-Θ)>0.
|
||
|
||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||
|
||
auto res = newton_spherical(mesh, x0, maps, 1e-8, 200);
|
||
EXPECT_TRUE(res.converged)
|
||
<< "Spherical Newton did not converge (iterations=" << res.iterations
|
||
<< ", |G|inf=" << res.grad_inf_norm << ")";
|
||
EXPECT_LT(res.grad_inf_norm, 1e-8);
|
||
|
||
// Angle sum residual = 0 after convergence (≡ each interior vertex has Σα = θ_v).
|
||
auto G_final = spherical_gradient(mesh, res.x, maps);
|
||
for (std::size_t i = 0; i < G_final.size(); ++i)
|
||
EXPECT_NEAR(0.0, G_final[i], 1e-6)
|
||
<< "Spherical angle sum residual at DOF " << i << " = " << G_final[i];
|
||
}
|