docs: add CLAUDE.md — context file for Claude Code
Covers build commands (all three modes), single-test invocation, header-only architecture, the three geometry modes, Newton solver sign conventions, layout BFS design, test patterns, CI structure, and known quirks (Finder ` 2.hpp` duplicates, CGAL compile flags). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
144
CLAUDE.md
Normal file
144
CLAUDE.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
conformallab++ is a C++ reimplementation of Stefan Sechelmann's [ConformalLab](https://github.com/varylab/conformallab) Java library (TU Berlin, 2016). It solves one precise problem: given a triangulated surface, find a conformally equivalent metric satisfying prescribed curvature (angle-sum) constraints at each vertex. All code lives in `code/`.
|
||||||
|
|
||||||
|
The algorithmic foundation is Sechelmann's dissertation:
|
||||||
|
> *Variational Methods for Discrete Surface Parameterization: Applications and Implementation*, TU Berlin 2016. DOI: 10.14279/depositonce-5415
|
||||||
|
|
||||||
|
## Build commands
|
||||||
|
|
||||||
|
All three modes share the same source root `code/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mode 1 — fast tests only (no CGAL, no Boost, no display)
|
||||||
|
cmake -S code -B build
|
||||||
|
cmake --build build --target conformallab_tests -j$(nproc)
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
|
||||||
|
# Mode 2 — CGAL tests headless (CI mode, requires Boost headers, no wayland/display)
|
||||||
|
# macOS: brew install boost Linux: apt install libboost-dev
|
||||||
|
cmake -S code -B build -DWITH_CGAL_TESTS=ON
|
||||||
|
cmake --build build --target conformallab_cgal_tests -j$(nproc)
|
||||||
|
ctest --test-dir build -R "^cgal\." --output-on-failure
|
||||||
|
|
||||||
|
# Mode 3 — full local build (CLI app + viewer, requires Wayland/X11 dev headers)
|
||||||
|
cmake -S code -B build -DWITH_CGAL=ON
|
||||||
|
cmake --build build -j$(nproc)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** `-DWITH_CGAL=ON` automatically enables `-DWITH_VIEWER=ON`, which pulls in GLFW and requires `wayland-scanner`. Use `-DWITH_CGAL_TESTS=ON` for headless environments.
|
||||||
|
|
||||||
|
### Running a single test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Non-CGAL test by name
|
||||||
|
ctest --test-dir build -R "Clausen" --output-on-failure
|
||||||
|
|
||||||
|
# CGAL test by name (prefix is always "cgal.")
|
||||||
|
ctest --test-dir build -R "cgal.NewtonSolver" --output-on-failure
|
||||||
|
|
||||||
|
# Or run the binary directly for full GTest output
|
||||||
|
./build/conformallab_cgal_tests --gtest_filter="NewtonSolver*"
|
||||||
|
./build/conformallab_tests --gtest_filter="Clausen*"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Everything is header-only
|
||||||
|
|
||||||
|
All algorithms live in `code/include/*.hpp`. There is no compiled library. The CMake targets (`conformallab_tests`, `conformallab_cgal_tests`, `conformallab_core`) all compile the headers directly via their test or app `.cpp` files.
|
||||||
|
|
||||||
|
### Central type: `ConformalMesh`
|
||||||
|
|
||||||
|
Defined in `conformal_mesh.hpp`:
|
||||||
|
```cpp
|
||||||
|
using ConformalMesh = CGAL::Surface_mesh<Point3>; // CGAL::Simple_cartesian<double>
|
||||||
|
```
|
||||||
|
Algorithms attach data via CGAL named property maps rather than intrusive vertex/edge types (replacing the Java CoHDS/CoVertex/CoEdge pattern). Property map naming convention:
|
||||||
|
- `"v:lambda"` — per-vertex log scale factor (the conformal variable uᵢ)
|
||||||
|
- `"v:theta"` — per-vertex target cone angle
|
||||||
|
- `"v:idx"` — solver DOF index (`-1` = pinned/boundary)
|
||||||
|
- `"e:alpha"` — per-edge intersection angle (hyperbolic geometry only)
|
||||||
|
|
||||||
|
### The three geometry modes
|
||||||
|
|
||||||
|
Each mode has its own Maps struct + functional + Hessian header:
|
||||||
|
|
||||||
|
| Mode | Headers | Maps struct | Newton function |
|
||||||
|
|------|---------|-------------|----------------|
|
||||||
|
| Euclidean (ℝ²) | `euclidean_functional.hpp`, `euclidean_hessian.hpp` | `EuclideanMaps` | `newton_euclidean()` |
|
||||||
|
| Spherical (S²) | `spherical_functional.hpp`, `spherical_hessian.hpp` | `SphericalMaps` | `newton_spherical()` |
|
||||||
|
| Hyper-ideal (H²) | `hyper_ideal_functional.hpp`, `hyper_ideal_hessian.hpp` | `HyperIdealMaps` | `newton_hyper_ideal()` |
|
||||||
|
|
||||||
|
Setup follows the same pattern for all three:
|
||||||
|
```cpp
|
||||||
|
EuclideanMaps maps = setup_euclidean_maps(mesh);
|
||||||
|
compute_euclidean_lambda0_from_mesh(mesh, maps); // initialise λ° from 3-D positions
|
||||||
|
maps.v_idx[*mesh.vertices().begin()] = -1; // pin one vertex (gauge fix)
|
||||||
|
int idx = 0;
|
||||||
|
for (auto v : mesh.vertices()) if (maps.v_idx[v] != -1) maps.v_idx[v] = idx++;
|
||||||
|
```
|
||||||
|
After `compute_*_lambda0_from_mesh()` the solver works entirely in scale-factor space; original vertex positions are no longer used.
|
||||||
|
|
||||||
|
### Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
load_mesh() → ConformalMesh
|
||||||
|
setup_*_maps() → *Maps (property maps attached)
|
||||||
|
compute_*_lambda0_from_mesh() → λ° initialised
|
||||||
|
check/enforce_gauss_bonnet() → Σ(2π−Θᵥ) = 2π·χ(M) [mandatory for closed meshes]
|
||||||
|
newton_*() → NewtonResult.x (converged scale factors)
|
||||||
|
compute_cut_graph() → CutGraph (2g seam edges, closed meshes only)
|
||||||
|
euclidean/spherical/hyper_ideal_layout() → Layout2D/3D + HolonomyData
|
||||||
|
normalise_*() → canonical position
|
||||||
|
compute_period_matrix() → PeriodData τ∈ℍ (genus 1 flat torus)
|
||||||
|
compute_fundamental_domain() → FundamentalDomain + tiling
|
||||||
|
save_result_json/xml() → serialised result
|
||||||
|
```
|
||||||
|
|
||||||
|
### Newton solver (`newton_solver.hpp`)
|
||||||
|
|
||||||
|
Sign conventions differ between modes — the solver handles this internally:
|
||||||
|
- Euclidean/HyperIdeal: `G_v = actual − target`, H is PSD → `SimplicialLDLT(H)`
|
||||||
|
- Spherical: `G_v = target − actual`, H is NSD → `SimplicialLDLT(−H)`
|
||||||
|
|
||||||
|
When `SimplicialLDLT` fails (rank-deficient H on closed meshes without pinned vertex), the solver automatically retries with `SparseQR` to find the minimum-norm step. This is the gauge-mode fallback — public API: `solve_linear_system(H, rhs, &used_fallback)`.
|
||||||
|
|
||||||
|
### Layout (`layout.hpp`)
|
||||||
|
|
||||||
|
BFS-trilateration using a min-heap on BFS depth (priority BFS). Root face = largest 3-D area face. Key output fields:
|
||||||
|
- `layout.uv[v.idx()]` — primary UV (first/shallowest BFS visit)
|
||||||
|
- `layout.halfedge_uv[h.idx()]` — UV of `source(h)` as seen from `face(h)` — seam-aware for GPU texture atlasing
|
||||||
|
- `hol.translations[i]` — lattice generators ωᵢ (Euclidean/spherical)
|
||||||
|
- `hol.mobius_maps[i]` — Möbius isometry Tᵢ ∈ SU(1,1) (hyperbolic)
|
||||||
|
|
||||||
|
### Test design pattern
|
||||||
|
|
||||||
|
Tests use the **"natural theta"** trick to construct a known equilibrium at `x* = 0`:
|
||||||
|
```cpp
|
||||||
|
// Evaluate gradient at x=0, use actual angle sums as targets → x*=0 by construction
|
||||||
|
evaluate_euclidean_gradient(mesh, x0, maps);
|
||||||
|
for (auto v : mesh.vertices())
|
||||||
|
if (maps.v_idx[v] >= 0)
|
||||||
|
maps.theta_v[v] = /* actual angle sum from gradient evaluation */;
|
||||||
|
```
|
||||||
|
Gradient checks use finite differences: perturb `xᵢ ± ε`, verify `G(x) ≈ ∂E/∂x`.
|
||||||
|
|
||||||
|
## CI pipeline
|
||||||
|
|
||||||
|
Two jobs defined in `.gitea/workflows/cpp-tests.yml`:
|
||||||
|
- **`test-fast`** — no flags, Eigen+GTest only, runs on all branches
|
||||||
|
- **`test-cgal`** — `-DWITH_CGAL_TESTS=ON`, requires Boost (installed at runtime until Docker image is rebuilt), runs on `main`/`dev`/PRs only, needs `test-fast` to pass first
|
||||||
|
|
||||||
|
Docker image: `git.eulernest.eu/conformallab/ci-cpp:latest` (Ubuntu 22.04 ARM64). Dockerfile at `.gitea/docker/Dockerfile.ci-cpp`.
|
||||||
|
|
||||||
|
## Known issues / conventions
|
||||||
|
|
||||||
|
- Files ending in ` 2.hpp` (e.g. `clausen 2.hpp`, `hyper_ideal_utility 2.hpp`) are macOS Finder duplicates — ignore them, use the canonical name without ` 2`.
|
||||||
|
- `CGAL_DISABLE_GMP` and `CGAL_DISABLE_MPFR` are defined for all CGAL targets — CGAL runs in exact-predicates-inexact-constructions mode with `Simple_cartesian<double>`, which is intentional (conformal geometry does not need exact arithmetic).
|
||||||
|
- All deps are bundled as tarballs in `code/deps/tarballs/` and extracted at CMake configure time — no internet access needed at build time except for GTest (fetched via `FetchContent`).
|
||||||
|
- The `test-fast` job intentionally includes stubs that call `GTEST_SKIP()` — 2 intentional skips are expected in the CGAL suite, not regressions.
|
||||||
Reference in New Issue
Block a user