#pragma once // Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // fundamental_domain.hpp // // Phase 7 — Fundamental domain polygon for closed surfaces. // // For a closed genus-g surface cut open via a CutGraph + Euclidean layout: // // The universal cover is tiled by copies of the cut-open disk. // The fundamental domain is the polygon whose sides are identified in pairs // by the holonomy generators. // // ─── Genus-1 (flat torus) ──────────────────────────────────────────────────── // // Parallelogram with vertices 0, ω_1, ω_1 + ω_2, ω_2. // The four edges are identified in pairs: // bottom (0 → ω_1) ≡ top (ω_2 → ω_1 + ω_2) — translation ω_2 // left (0 → ω_2) ≡ right (ω_1 → ω_1 + ω_2) — translation ω_1 // // ─── Genus g > 1 (general) ────────────────────────────────────────────────── // // The standard 4g-polygon with sides labelled a_1 b_1 a_1^{-1} b_1^{-1} ... // can be recovered from the layout boundary, but requires walking the // boundary of the cut-open mesh — not yet implemented (see note below). // // For now, this file provides the genus-1 parallelogram only. // The polygon vertices for genus-1 are computed from the holonomy generators. // // ─── API ───────────────────────────────────────────────────────────────────── // // FundamentalDomain fd = compute_fundamental_domain_genus1(holonomy); // fd.vertices — 2D polygon corners (size = 4 for genus-1) // fd.edge_identifications — pairs (i, j) meaning edge i is identified with j // fd.is_valid() — true if genus == 1 and data makes sense #include "layout.hpp" #include "period_matrix.hpp" #include #include namespace conformallab { // ───────────────────────────────────────────────────────────────────────────── // FundamentalDomain // ───────────────────────────────────────────────────────────────────────────── /// Fundamental polygon of a closed surface obtained by cutting along a /// `CutGraph`: corner vertices, paired-edge identifications and holonomy /// generators. For genus-1 the polygon is a parallelogram with 4 corners. struct FundamentalDomain { /// Polygon corners in order (CCW). Size = 4 for genus-1. std::vector vertices; /// edge_identifications[k] = (i, j) means the edge from vertices[i] to /// vertices[(i+1) % n] is identified with the edge from vertices[j] to /// vertices[(j+1) % n] (with matching orientation). std::vector> edge_identifications; /// Holonomy generators (one per identified edge pair). /// For genus-1: generators[0] = ω_1, generators[1] = ω_2. std::vector generators; /// `true` iff the polygon has at least 3 vertices. bool is_valid() const { return vertices.size() >= 3; } }; // ───────────────────────────────────────────────────────────────────────────── // compute_fundamental_domain_genus1 // // Builds the parallelogram fundamental domain from Euclidean holonomy data // with exactly 2 generators ω_1, ω_2. // // Vertices (CCW): // v0 = (0, 0) // v1 = ω_1 // v2 = ω_1 + ω_2 // v3 = ω_2 // // Edge identifications: // bottom (v0→v1) ≡ top (v3→v2) by ω_2 // left (v3→v0) ≡ right (v2→v1) by ω_1 (reversed convention) // ───────────────────────────────────────────────────────────────────────────── /// Build the parallelogram fundamental domain from genus-1 Euclidean /// holonomy data (`hol.translations[0] = ω₁`, `hol.translations[1] = ω₂`). inline FundamentalDomain compute_fundamental_domain_genus1( const HolonomyData& hol) { FundamentalDomain fd; if (hol.translations.size() < 2) return fd; Eigen::Vector2d w1 = hol.translations[0]; Eigen::Vector2d w2 = hol.translations[1]; // Ensure CCW orientation: cross product z-component w1 × w2 > 0 double cross = w1.x() * w2.y() - w1.y() * w2.x(); if (cross < 0.0) std::swap(w1, w2); Eigen::Vector2d origin = Eigen::Vector2d::Zero(); fd.vertices = { origin, w1, w1 + w2, w2 }; // Edge 0: v0→v1 (= bottom), Edge 2: v3→v2 (= top, reversed) // Identification: bottom ≡ top translated by w2 // Edge 1: v1→v2 (= right), Edge 3: v0→v3... wait let me use standard labeling: // Edges by index: 0: v0→v1, 1: v1→v2, 2: v2→v3, 3: v3→v0 // Identifications: 0 ≡ 2 (reversed: bottom ≡ top by w2) // 1 ≡ 3 (reversed: right ≡ left by w1) fd.edge_identifications = { {0, 2}, {1, 3} }; fd.generators = { w1, w2 }; return fd; } // ───────────────────────────────────────────────────────────────────────────── // compute_fundamental_domain // // Dispatcher: for genus-1 uses compute_fundamental_domain_genus1. // For higher genus returns an empty FundamentalDomain (not yet implemented). // ───────────────────────────────────────────────────────────────────────────── // // TODO(Phase 8): Implement the standard 4g-gon fundamental domain for genus g > 1. // // Algorithm outline (boundary-walk method): // ───────────────────────────────────────── // 1. Construct the CutGraph on the cut-open mesh (already done upstream). // This yields 2g cut edges; cutting them converts the closed surface into // a topological disk. // // 2. Walk the boundary of the cut-open disk in CCW order: // Start from any boundary halfedge and follow `next(h)` along the boundary // (i.e. skip to the next boundary halfedge at each vertex). Collect the // 2·(4g) = 8g boundary halfedges in order. // Each halfedge h_k corresponds to a UV vertex `halfedge_uv[h_k.idx()]`. // // 3. Identify paired sides: // The 4g sides of the polygon alternate as a_1 b_1 a_1^{-1} b_1^{-1} … // For each cut edge e_i (i = 1 … 2g) the two sides that are identified // are those whose source/target vertices match under the holonomy generator // ω_i (Euclidean) or T_i (hyperbolic). // Record the identifications as edge_identifications[k] = (i, j). // // 4. Fill FundamentalDomain: // vertices = UV corners from the boundary walk. // edge_identifications = paired-edge list from step 3. // generators = holonomy.translations (Euclidean) or the // fixed points of holonomy.mobius_maps (hyperbolic, // requires computing axis of T_i ∈ SU(1,1)). // // References: // Erickson & Whittlesey, "Greedy optimal homotopy and homology generators" // SODA 2005. // Desbrun, Kanso, Tong, "Discrete Differential Forms for Computational // Modeling", in Discrete Differential Geometry (2008). // // Note: The Siegel period matrix Ω ∈ H_g (g×g complex symmetric, Im Ω > 0) // for genus g > 1 also requires integration of holomorphic differentials — // this is intentionally deferred and NOT implemented here. // See period_matrix.hpp for the genus-1 case (τ = ω_2/ω_1 ∈ ℍ). // ───────────────────────────────────────────────────────────────────────────── /// Dispatcher: for genus 1 returns `compute_fundamental_domain_genus1`, /// for higher genus returns an empty domain (4g-polygon not yet implemented). inline FundamentalDomain compute_fundamental_domain( const HolonomyData& hol) { int n = static_cast(hol.translations.size()); int g = n / 2; if (g == 1) return compute_fundamental_domain_genus1(hol); // Higher genus: boundary-walk 4g-polygon — not yet implemented (see TODO above). return FundamentalDomain{}; } // ───────────────────────────────────────────────────────────────────────────── // tiling_copy // // Given a Layout2D for the cut-open surface and two lattice generators ω_1, ω_2, // return a translated copy of the layout shifted by m·ω_1 + n·ω_2. // Useful for visualising the tiled universal cover. // ───────────────────────────────────────────────────────────────────────────── /// Return a translated copy of `layout` shifted by `m·ω₁ + n·ω₂`. /// Useful for visualising the tiled universal cover. inline Layout2D tiling_copy(const Layout2D& layout, const Eigen::Vector2d& w1, const Eigen::Vector2d& w2, int m, int n) { Layout2D copy = layout; Eigen::Vector2d shift = static_cast(m) * w1 + static_cast(n) * w2; for (auto& p : copy.uv) p += shift; return copy; } // ───────────────────────────────────────────────────────────────────────────── // tiling_neighbourhood // // Returns a vector of tiling copies for (m, n) with |m| ≤ m_max, |n| ≤ n_max. // The result includes the original (m=0, n=0) at index (m_max)(2*n_max+1)+n_max. // ───────────────────────────────────────────────────────────────────────────── /// Build a tiling neighbourhood: all `(m, n)` with `|m| ≤ m_max`, /// `|n| ≤ n_max`. The original tile `(0, 0)` is included. inline std::vector tiling_neighbourhood( const Layout2D& layout, const HolonomyData& hol, int m_max = 2, int n_max = 2) { std::vector tiles; if (hol.translations.size() < 2) { tiles.push_back(layout); return tiles; } const Eigen::Vector2d& w1 = hol.translations[0]; const Eigen::Vector2d& w2 = hol.translations[1]; for (int m = -m_max; m <= m_max; ++m) for (int n = -n_max; n <= n_max; ++n) tiles.push_back(tiling_copy(layout, w1, w2, m, n)); return tiles; } } // namespace conformallab