#pragma once // Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // pn_geometry.hpp // // Projective-metric geometry substrate — faithful port of the // `de.jreality.math.Pn` surface that the Java algorithmic core // uses pervasively: // HyperbolicLayout, SphericalLayout, FundamentalPolygon, // KoebePolyhedron, quasi-isothermic utilities, hyperelliptic θ. // // Porting this ~30-function surface once unblocks every downstream // geometric phase (9c / 10c' / 10e / 11c) instead of re-deriving the // metric ad hoc per phase. // // ┌──────────────────────────────────────────────────────────────────┐ // │ Metric signature convention (matches jReality exactly) │ // │ │ // │ EUCLIDEAN = 0 ⟨u,v⟩ = Σ_{i #include #include namespace conformallab { /// Metric signatures matching `de.jreality.math.Pn`. enum PnMetric : int { PN_EUCLIDEAN = 0, PN_ELLIPTIC = +1, PN_HYPERBOLIC = -1 }; // ── Inner product ───────────────────────────────────────────────────────────── /// Bilinear form ⟨u,v⟩ for the given metric. /// Spatial part = all but the last coordinate; the last coord is the /// homogeneous/timelike one. inline double pn_inner_product(const Eigen::VectorXd& u, const Eigen::VectorXd& v, int metric) { const int n = static_cast(u.size()); const double spat = u.head(n - 1).dot(v.head(n - 1)); const double last = u(n - 1) * v(n - 1); switch (metric) { case PN_HYPERBOLIC: return last - spat; case PN_ELLIPTIC: return last + spat; case PN_EUCLIDEAN: default: return spat; } } // ── Norm / scale ────────────────────────────────────────────────────────────── /// Metric norm √|⟨p,p⟩|. Absolute value guards against tiny negative /// round-off in the Euclidean / hyperbolic degenerate cases. inline double pn_norm(const Eigen::VectorXd& p, int metric) { return std::sqrt(std::abs(pn_inner_product(p, p, metric))); } /// Dehomogenise: divide by the last component (jReality `Pn.dehomogenize`). inline Eigen::VectorXd pn_dehomogenize(const Eigen::VectorXd& p) { return p / p(p.size() - 1); } /// Scale `p` to metric norm `length` (jReality `Pn.setToLength`). /// Returns `p` unchanged when its norm is numerically zero. inline Eigen::VectorXd pn_set_to_length(const Eigen::VectorXd& p, double length, int metric) { const double nrm = pn_norm(p, metric); if (nrm < 1e-300) return p; return p * (length / nrm); } /// Normalise to unit metric norm (jReality `Pn.normalize`). inline Eigen::VectorXd pn_normalize(const Eigen::VectorXd& p, int metric) { return pn_set_to_length(p, 1.0, metric); } // ── Distance ────────────────────────────────────────────────────────────────── /// Geodesic distance between two points (jReality `Pn.distanceBetween`). /// EUCLIDEAN: ‖p̂_spatial − q̂_spatial‖ (dehomogenised) /// ELLIPTIC: acos(⟨p̂,q̂⟩) (spherical angle, clamped to [−1,1]) /// HYPERBOLIC: acosh(⟨p̂,q̂⟩) (clamped ≥ 1) inline double pn_distance_between(const Eigen::VectorXd& p, const Eigen::VectorXd& q, int metric) { if (metric == PN_EUCLIDEAN) { const auto pd = pn_dehomogenize(p); const auto qd = pn_dehomogenize(q); const int n = static_cast(pd.size()); return (pd.head(n - 1) - qd.head(n - 1)).norm(); } const double np = pn_norm(p, metric); const double nq = pn_norm(q, metric); double c = pn_inner_product(p, q, metric) / (np * nq); if (metric == PN_HYPERBOLIC) return std::acosh(std::max(1.0, c)); // ELLIPTIC c = std::clamp(c, -1.0, 1.0); return std::acos(c); } // ── Geodesic interpolation ──────────────────────────────────────────────────── /// Constant-speed geodesic interpolation at parameter t ∈ [0,1] /// (jReality `Pn.linearInterpolation`). /// EUCLIDEAN: affine blend of the dehomogenised points. /// ELLIPTIC: spherical slerp: /// r(t) = (sin((1−t)d)·p̂ + sin(td)·q̂) / sin(d) /// HYPERBOLIC: hyperbolic slerp: /// r(t) = (sinh((1−t)d)·p̂ + sinh(td)·q̂) / sinh(d) /// where d = pn_distance_between(p,q,metric) and p̂,q̂ are unit vectors. inline Eigen::VectorXd pn_linear_interpolation(const Eigen::VectorXd& p, const Eigen::VectorXd& q, double t, int metric) { if (metric == PN_EUCLIDEAN) { const auto pd = pn_dehomogenize(p); const auto qd = pn_dehomogenize(q); return (1.0 - t) * pd + t * qd; } const auto ph = pn_normalize(p, metric); const auto qh = pn_normalize(q, metric); const double d = pn_distance_between(ph, qh, metric); if (d < 1e-12) return ph; // coincident — return either endpoint if (metric == PN_ELLIPTIC) { const double s = std::sin(d); return (std::sin((1.0 - t) * d) * ph + std::sin(t * d) * qh) / s; } // HYPERBOLIC const double s = std::sinh(d); return (std::sinh((1.0 - t) * d) * ph + std::sinh(t * d) * qh) / s; } } // namespace conformallab