Implement Phase-Session P1 quick wins (4 independent additions):
9h.1: Add --tol and --max-iter CLI options to conformallab_core
- Newton solver tolerance [default 1e-8]
- Newton iteration limit [default 200]
- Thread both through run_euclidean / run_spherical / run_hyper_ideal
- Update CLI parameter table in documentation
9h.2: Add -g cp_euclidean and -g inversive_distance geometry routes
- run_cp_euclidean() & run_inversive_distance() pipelines (~60 lines each)
- Face-based DOF assignment for CP-Euclidean
- Vertex-based DOF assignment for Inversive-Distance
- Both integrated into CLI geometry validator (IsMember)
9g.1: Create conformal_quality.hpp with validation measures
- IsothermicityMeasure: metric anisotropy (conformality deviation)
- DiscreteConformalEquivalenceMeasure: length-cross-ratio residuals
- FlippedTriangles: detects inverted/degenerate triangles
- LengthCrossRatio: discrete conformal invariant computation
- ConvergenceUtility: aggregated convergence statistics (max/mean/sum)
- Ported from Java: plugin/visualizer + convergence utilities
- Includes sanity tests validating finite outputs on valid layouts
9d.3: Create stereographic_layout.hpp for S² → ℂ projection
- Stereographic projection from north pole: S² → ℂ ∪ {∞}
- Inverse projection: ℂ → S² for round-trip validation
- Möbius centring: centres the 2-D point cloud at origin
- stereographic_layout(Layout3D) -> Layout2D conversion
- Round-trip tests: south pole, equator, random sphere points
- Tests: projection/inverse consistency, north pole handling
Test results: 336/336 CGAL tests pass (272 pre-existing + 64 new from all phases)
- conformal_quality.cpp: 13 new tests (measures, isothermic, dce, convergence)
- stereographic_layout.cpp: 10 new tests (projection, inverse, round-trip, layout)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
204 lines
8.2 KiB
C++
204 lines
8.2 KiB
C++
// 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 <complex>
|
||
#include <vector>
|
||
#include <cmath>
|
||
#include <array>
|
||
|
||
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<double> 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<double>(std::nan(""), std::nan(""));
|
||
}
|
||
return std::complex<double>(x / denom, y / denom);
|
||
}
|
||
|
||
/// Stereographic projection of a 3-D point (as Point3).
|
||
inline std::complex<double> 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<std::complex<double>>& points)
|
||
{
|
||
if (points.empty()) return;
|
||
|
||
// Compute centroid.
|
||
std::complex<double> 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<double>(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<std::complex<double>> 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<double> 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<double>(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<double>::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
|