.gitignore: build-Verzeichnisse, .DS_Store, .claude/, CMake-Artefakte Doc-Dateien die beim Restructure-Commit fehlten: doc/api/headers.md — alle 24 Public-Header mit Beschreibung doc/api/tests.md — 26 Suiten, 158 Tests, Einzelzahlen doc/architecture/design-decisions.md — Architekturentscheidungen + Begründung doc/architecture/project-structure.md — Verzeichnisbaum + Build-Targets README.md: Links zu den vier neuen Doc-Dateien ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.6 KiB
Key Design Decisions
Rationale for the architectural choices that distinguish conformallab++ from the Java original and from generic geometry-processing frameworks.
CGAL Surface_mesh as the halfedge data structure
The Java library uses CoHDS — a custom intrusive halfedge data structure with
CoVertex, CoEdge, CoFace types that carry domain-specific data directly as fields.
conformallab++ replaces this with CGAL::Surface_mesh<Point3> and attaches data via
named property maps:
// Java: vertex.getLambda() → C++: maps.lambda0[v]
// Java: edge.getAlpha() → C++: maps.e_alpha[e]
// Java: vertex.getSolverIndex() → C++: maps.v_idx[v]
auto [lambda0, ok] = mesh.add_property_map<Edge_index, double>("e:lambda0", 0.0);
lambda0[e] = 1.234;
This decouples the mesh topology from the algorithm data, makes it straightforward to attach multiple independent data sets to the same mesh, and will enable the Phase 8 traits-class design to work with any CGAL-conforming mesh type.
DOF vector convention
All three functionals use the same indexing scheme: a flat std::vector<double> x
indexed by v_idx[v] (vertices) and e_idx[e] (edges, HyperIdeal only).
Index value -1 means "pinned" — the DOF is fixed at zero and excluded from the
Newton system.
x[maps.v_idx[v]] = uᵥ (conformal scale factor, Euclidean/Spherical)
x[maps.e_idx[e]] = λₑ (edge log-length variable, HyperIdeal only)
-1 pinned — u_v = 0 / λ_e = 0
This is consistent across all three geometry modes, enabling the same Newton solver and linear system infrastructure to serve all three without branching.
Priority-BFS layout
A naive BFS layout places faces in arbitrary order; trilateration errors accumulate along the BFS frontier. conformallab++ uses a priority min-heap on BFS depth:
depth(face) = max(depth[v_src], depth[v_tgt]) + 1 for each new face
Faces with smaller depth (closer to the root) are placed first. This means each face's trilateration uses the two most accurately-placed adjacent vertices, minimising error propagation across the mesh.
Root face selection: largest 3-D area face, with an additional 1.5× bonus for interior faces over boundary faces. This heuristic places the root where metric distortion is lowest.
halfedge_uv semantics
layout.uv[v.idx()] gives the primary UV coordinate of vertex v — the position
from the shallowest BFS visit. At seam edges this is insufficient for GPU rendering:
two faces sharing a seam vertex need different UV values for that vertex.
layout.halfedge_uv[h.idx()] stores the UV of source(h) as seen from face(h):
halfedge h → face(h) → source(h) has UV = halfedge_uv[h.idx()]
opposite(h) → face(h') → source(h) has UV = halfedge_uv[opposite(h).idx()]
(different value at a seam)
At seam halfedges the two opposite halfedges carry different UV values — each face gets its own copy of the seam vertex. This enables a proper GPU texture atlas without vertex duplication in the index buffer.
Spherical Hessian sign convention
The spherical energy functional is concave (negative semidefinite Hessian).
Standard Newton would require solving H·Δx = −G with NSD H, which Cholesky
cannot handle.
newton_spherical() solves (−H)·Δx = G instead — algebraically identical,
but −H is PSD and SimplicialLDLT works correctly. This sign flip is handled
transparently inside newton_spherical(); callers need not be aware of it.
The gradient sign in spherical mode is also flipped vs. Euclidean:
- Euclidean:
G_v = actual_sum − Θᵥ - Spherical:
G_v = Θᵥ − actual_sum
Both conventions drive the same equilibrium condition G = 0.
HyperIdeal Hessian via finite differences
The analytic HyperIdeal Hessian requires differentiating through the chain
(bᵢ, aₑ) → lᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ/βᵢ with four vertex-type combinations
per edge — substantial implementation complexity.
conformallab++ uses a symmetric finite-difference Hessian instead:
H[i,j] = (G(x + ε·eⱼ)[i] − G(x − ε·eⱼ)[i]) / (2ε), ε = 1e-5
Properties:
- O(ε²) accuracy — relative error ≈ 10⁻¹⁰ at ε = 10⁻⁵
- PSD guaranteed by strict convexity of the HyperIdeal energy (Springborn 2020)
- Symmetrised automatically:
H = (H + Hᵀ) / 2 - Cost: n extra gradient evaluations per Newton step (acceptable for < 500 DOFs)
The analytic Hessian is deferred to Phase 9b. See roadmap/java-parity.md.