// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // stereographic_layout.hpp // // Phase 9d.3 — Stereographic projection for spherical DCE output. // // Converts a spherical layout (points on S²) to a 2-D conformal map via: // 1. Stereographic projection: S² → ℂ ∪ {∞}, mapping the sphere to the complex plane. // 2. Möbius centring: centres the resulting point cloud for canonical position. // // Mathematical reference: // Stereographic projection from the north pole (0,0,1): // (x,y,z) ↦ (x/(1-z), y/(1-z)) in ℂ (complex coordinate u+iv). // North pole (0,0,1) maps to ∞ (removed from the layout). // South pole (0,0,-1) maps to (0,0) in ℂ. // The projection is conformal (angle-preserving). // // Möbius centring: apply a Möbius transformation to centre the layout // (e.g. shift the centroid to the origin, possibly scale/rotate). // // Java source (ported from): // unwrapper/StereographicUnwrapper.java (266 lines) // The supporting math/CP1 + ComplexUtility.stereographic operations // (deliberately NOT ported — redundant with std::complex). #pragma once #include "conformal_mesh.hpp" #include "layout.hpp" #include #include #include #include namespace conformallab { // ──────────────────────────────────────────────────────────────────────────── // Stereographic Projection: S² → ℂ // ──────────────────────────────────────────────────────────────────────────── /// Stereographic projection from the north pole (0, 0, 1). /// Maps a point on the unit sphere S² to the complex plane ℂ. /// North pole (0,0,1) projects to ∞ (not representable; returns NaN). /// South pole (0,0,-1) projects to 0+0i. /// /// Formula: (x,y,z) ↦ x/(1-z) + i·y/(1-z) inline std::complex stereographic_project(double x, double y, double z) { const double denom = 1.0 - z; if (std::abs(denom) < 1e-15) { // North pole (z ≈ 1) — maps to ∞. // Return NaN to signal infinity. return std::complex(std::nan(""), std::nan("")); } return std::complex(x / denom, y / denom); } /// Stereographic projection of a 3-D point (as Point3). inline std::complex stereographic_project(const Point3& p) { return stereographic_project(p.x(), p.y(), p.z()); } // ──────────────────────────────────────────────────────────────────────────── // Möbius Centring // ──────────────────────────────────────────────────────────────────────────── /// Simple centring: translate the point cloud so that its centroid /// is at the origin (u+iv = 0). inline void centre_at_origin(std::vector>& points) { if (points.empty()) return; // Compute centroid. std::complex centroid(0.0, 0.0); int n_valid = 0; for (const auto& z : points) { if (std::isfinite(z.real()) && std::isfinite(z.imag())) { centroid += z; n_valid++; } } if (n_valid <= 0) return; centroid /= static_cast(n_valid); // Translate: z' = z - centroid. for (auto& z : points) { if (std::isfinite(z.real()) && std::isfinite(z.imag())) { z -= centroid; } } } // ──────────────────────────────────────────────────────────────────────────── // Stereographic Layout: S² → ℂ (2-D) // ──────────────────────────────────────────────────────────────────────────── /// Convert a spherical layout (3-D points on S²) to a 2-D conformal map /// via stereographic projection. /// /// Output: a Layout2D where: /// - uv[v.idx()] = (Re, Im) of the stereographic projection of the 3-D point. /// - The north pole is excluded (uv[v] = NaN for projections at ∞). /// /// Möbius centring: the resulting layout is centred at the origin. /// /// \param mesh Input surface mesh. /// \param layout Input spherical layout (3-D points on S²). /// \return Output Layout2D in the complex plane (ℂ). inline Layout2D stereographic_layout( const ConformalMesh& mesh, const Layout3D& layout) { Layout2D result; result.uv.resize(mesh.number_of_vertices()); result.halfedge_uv.resize(mesh.number_of_halfedges()); // Step 1: Stereographic projection for each vertex. std::vector> complex_points; complex_points.reserve(mesh.number_of_vertices()); for (auto v : mesh.vertices()) { const auto& p3d = layout.pos[v.idx()]; // Convert Eigen::Vector3d to Point3-like coordinates. double x = p3d[0], y = p3d[1], z = p3d[2]; auto z_complex = stereographic_project(x, y, z); complex_points.push_back(z_complex); // Store as Eigen::Vector2d (Re, Im). result.uv[v.idx()] = Eigen::Vector2d(z_complex.real(), z_complex.imag()); } // Step 2: Möbius centring. centre_at_origin(complex_points); // Update uv after centring. for (auto v : mesh.vertices()) { const auto& z = complex_points[v.idx()]; result.uv[v.idx()] = Eigen::Vector2d(z.real(), z.imag()); } // Step 3: Halfedge UV (for texture atlasing). // Copy the primary vertex UV to each halfedge's source. for (auto h : mesh.halfedges()) { Vertex_index src = mesh.source(h); result.halfedge_uv[h.idx()] = result.uv[src.idx()]; } return result; } // ──────────────────────────────────────────────────────────────────────────── // Inverse Stereographic Projection: ℂ → S² // ──────────────────────────────────────────────────────────────────────────── /// Inverse stereographic projection: ℂ → S². /// Given a complex number z = u + iv, recover the 3-D point on the unit sphere. /// /// Formula: (u,v) ↦ (2u/(1+u²+v²), 2v/(1+u²+v²), (u²+v²-1)/(u²+v²+1)) /// Inverse of: (x,y,z) ↦ (x/(1-z), y/(1-z)). /// /// The origin (u,v) = (0,0) maps back to (0,0,-1) (south pole). inline Point3 inverse_stereographic_project(std::complex z) { double u = z.real(); double v = z.imag(); double u2_plus_v2 = u * u + v * v; double denom = 1.0 + u2_plus_v2; double x = 2.0 * u / denom; double y = 2.0 * v / denom; double zz = (u2_plus_v2 - 1.0) / denom; return Point3(x, y, zz); } /// Inverse stereographic projection from a 2-D layout point. inline Point3 inverse_stereographic_project(const Eigen::Vector2d& uv) { return inverse_stereographic_project(std::complex(uv.x(), uv.y())); } /// Round-trip validation: project a 3-D point to 2-D and back. /// Returns the error (distance on S²) between the original and recovered point. inline double stereographic_roundtrip_error(const Point3& original) { auto z = stereographic_project(original); if (!std::isfinite(z.real()) || !std::isfinite(z.imag())) { return std::numeric_limits::infinity(); // north pole } auto recovered = inverse_stereographic_project(z); // Distance on the unit sphere: ‖p - q‖. double dx = original.x() - recovered.x(); double dy = original.y() - recovered.y(); double dz = original.z() - recovered.z(); return std::sqrt(dx * dx + dy * dy + dz * dz); } } // namespace conformallab