// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // test_pn_geometry.cpp // // Verifies pn_geometry.hpp against: // (a) closed-form analytic golden values, and // (b) the already-verified projective_math.hpp::hyperbolicDistance // (regression anchor for the HYPERBOLIC sign convention). // // Mirrored Java source: de.jreality.math.Pn #include "pn_geometry.hpp" #include "projective_math.hpp" #include #include #include using namespace conformallab; namespace { Eigen::VectorXd V(std::initializer_list xs) { Eigen::VectorXd v(static_cast(xs.size())); int i = 0; for (double x : xs) v(i++) = x; return v; } constexpr double PN_PI = 3.14159265358979323846; } // namespace // ── Inner product per signature ────────────────────────────────────────────── TEST(PnGeometry, InnerProductSignatures) { auto u = V({1.0, 2.0, 3.0}); // spatial=(1,2), last=3 auto v = V({4.0, 5.0, 6.0}); // spatial=(4,5), last=6 // spat = 1·4 + 2·5 = 14, last = 3·6 = 18 EXPECT_NEAR(pn_inner_product(u, v, PN_EUCLIDEAN), 14.0, 1e-12); EXPECT_NEAR(pn_inner_product(u, v, PN_ELLIPTIC), 14.0+18.0, 1e-12); EXPECT_NEAR(pn_inner_product(u, v, PN_HYPERBOLIC), 18.0-14.0, 1e-12); } // ── Euclidean distance ──────────────────────────────────────────────────────── TEST(PnGeometry, EuclideanDistance) { // 3-4-5 right triangle in the plane (homogeneous w=1). auto p = V({0.0, 0.0, 1.0}); auto q = V({3.0, 4.0, 1.0}); EXPECT_NEAR(pn_distance_between(p, q, PN_EUCLIDEAN), 5.0, 1e-12); // Homogeneous scaling must not change the distance. auto q2 = V({6.0, 8.0, 2.0}); // same point as q after dehomogenize EXPECT_NEAR(pn_distance_between(p, q2, PN_EUCLIDEAN), 5.0, 1e-12); } // ── Elliptic distance = spherical angle ─────────────────────────────────────── TEST(PnGeometry, EllipticDistanceIsAngle) { auto e1 = V({1.0, 0.0, 0.0}); auto e2 = V({0.0, 1.0, 0.0}); auto d = V({1.0, 1.0, 0.0}); // 45° between e1 and e2 EXPECT_NEAR(pn_distance_between(e1, e2, PN_ELLIPTIC), PN_PI / 2.0, 1e-12); EXPECT_NEAR(pn_distance_between(e1, d, PN_ELLIPTIC), PN_PI / 4.0, 1e-12); EXPECT_NEAR(pn_distance_between(e1, e1, PN_ELLIPTIC), 0.0, 1e-12); } // ── Hyperbolic distance: closed form + projective_math.hpp anchor ───────────── TEST(PnGeometry, HyperbolicDistanceClosedFormAndAnchor) { // Upper hyperboloid: p = apex (0,0,1); q = (sinh r, 0, cosh r) at distance r. const double r = 0.873; auto p = V({0.0, 0.0, 1.0}); auto q = V({std::sinh(r), 0.0, std::cosh(r)}); EXPECT_NEAR(pn_distance_between(p, q, PN_HYPERBOLIC), r, 1e-10); // Regression anchor: identical to projective_math.hpp::hyperbolicDistance. EXPECT_NEAR(pn_distance_between(p, q, PN_HYPERBOLIC), hyperbolicDistance(p, q), 1e-12); // Scale-invariance: homogeneous rescaling must not change distance. auto q2 = (2.5 * q).eval(); EXPECT_NEAR(pn_distance_between(p, q2, PN_HYPERBOLIC), r, 1e-10); } // ── Norm / setToLength / normalize ─────────────────────────────────────────── TEST(PnGeometry, NormAndScaling) { auto p = V({3.0, 4.0, 0.0}); EXPECT_NEAR(pn_norm(p, PN_ELLIPTIC), 5.0, 1e-12); auto s = pn_set_to_length(p, 2.0, PN_ELLIPTIC); EXPECT_NEAR(pn_norm(s, PN_ELLIPTIC), 2.0, 1e-12); auto u = pn_normalize(p, PN_ELLIPTIC); EXPECT_NEAR(pn_norm(u, PN_ELLIPTIC), 1.0, 1e-12); // A unit hyperboloid sheet point already has hyperbolic norm 1. auto h = V({std::sinh(0.6), 0.0, std::cosh(0.6)}); EXPECT_NEAR(pn_norm(h, PN_HYPERBOLIC), 1.0, 1e-12); } // ── Geodesic interpolation ──────────────────────────────────────────────────── TEST(PnGeometry, LinearInterpolationGeodesic) { // EUCLIDEAN: affine midpoint. { auto p = V({0.0, 0.0, 1.0}); auto q = V({4.0, 0.0, 1.0}); auto m = pn_linear_interpolation(p, q, 0.5, PN_EUCLIDEAN); EXPECT_NEAR(m(0), 2.0, 1e-12); } // ELLIPTIC: midpoint of e1,e2 is at half the π/2 arc = π/4 from each. { auto e1 = V({1.0, 0.0, 0.0}); auto e2 = V({0.0, 1.0, 0.0}); auto m = pn_linear_interpolation(e1, e2, 0.5, PN_ELLIPTIC); EXPECT_NEAR(pn_distance_between(e1, m, PN_ELLIPTIC), PN_PI / 4.0, 1e-10); EXPECT_NEAR(pn_distance_between(e2, m, PN_ELLIPTIC), PN_PI / 4.0, 1e-10); // Endpoint recovery at t=0. auto m0 = pn_linear_interpolation(e1, e2, 0.0, PN_ELLIPTIC); EXPECT_NEAR(pn_distance_between(e1, m0, PN_ELLIPTIC), 0.0, 1e-10); } // HYPERBOLIC: midpoint is at exactly half the geodesic distance from each end. { const double r = 1.2; auto p = V({0.0, 0.0, 1.0}); auto q = V({std::sinh(r), 0.0, std::cosh(r)}); auto m = pn_linear_interpolation(p, q, 0.5, PN_HYPERBOLIC); EXPECT_NEAR(pn_distance_between(p, m, PN_HYPERBOLIC), r / 2.0, 1e-9); EXPECT_NEAR(pn_distance_between(q, m, PN_HYPERBOLIC), r / 2.0, 1e-9); } }