diff --git a/Doxyfile b/Doxyfile index 6eaf994..9ed899c 100644 --- a/Doxyfile +++ b/Doxyfile @@ -31,7 +31,15 @@ EXCLUDE_PATTERNS = */build*/* \ */deps/* \ */.git/* \ */test-reports/* \ - */* 2.hpp + *\ 2.hpp \ + *\ 2.h +# Research-quality LaTeX notes use raw \sinh / \cosh / \frac / \beta / +# \cdot / \partial / \zeta macros which are valid LaTeX but unknown to +# Doxygen. These files are intended to be read as PDF or in a LaTeX- +# aware markdown viewer, not as Doxygen pages. Excluding them removes +# ~500 spurious "unknown command" warnings while keeping the .md files +# discoverable on GitHub. +EXCLUDE = doc/math/hyperideal-hessian-derivation.md EXCLUDE_SYMBOLS = Eigen::* boost::* std::* # Markdown filter: rewrites repo-relative links like [x](doc/api/tests.md) @@ -70,7 +78,7 @@ EXTENSION_MAPPING = h=C++ hpp=C++ # ── Warnings ───────────────────────────────────────────────────────────────── QUIET = NO WARNINGS = YES -WARN_IF_UNDOCUMENTED = NO +WARN_IF_UNDOCUMENTED = YES WARN_IF_DOC_ERROR = YES WARN_IF_INCOMPLETE_DOC = YES WARN_NO_PARAMDOC = NO @@ -80,6 +88,16 @@ WARN_LOGFILE = doc/doxygen/doxygen-warnings.log # ── HTML output ────────────────────────────────────────────────────────────── GENERATE_HTML = YES + +# MathJax — render LaTeX math in markdown ($...$ and $$...$$) and in +# code-comment `\f$ ... \f$` blocks via MathJax in the generated HTML. +# Required for the conformal-mapping math notation (\Theta, \omega, \tau, +# \mathbb{H}, …) in doc/architecture/overall_pipeline.md and the +# header docstrings. +USE_MATHJAX = YES +MATHJAX_VERSION = MathJax_3 +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@3/es5/ HTML_OUTPUT = html HTML_FILE_EXTENSION = .html HTML_COLORSTYLE = LIGHT @@ -100,7 +118,9 @@ SERVER_BASED_SEARCH = NO GENERATE_LATEX = NO GENERATE_RTF = NO GENERATE_MAN = NO -GENERATE_XML = NO +GENERATE_XML = YES +XML_OUTPUT = xml +XML_PROGRAMLISTING = NO GENERATE_DOCBOOK = NO GENERATE_AUTOGEN_DEF = NO GENERATE_PERLMOD = NO @@ -130,3 +150,16 @@ ALIASES += "concept{1}=\xrefitem concept \"Concept\" \"Concepts\" ALIASES += "models{1}=\xrefitem models \"Models\" \"Models\" \1" ALIASES += "cgalRequires{1}=\par Requirements: \n\1" ALIASES += "cgalParam{2}=\param \1 \2" +# CGAL named-parameter block aliases — replicates the upstream +# ${CGAL}/Documentation/doc/Documentation/Doxyfile_common conventions +# so that \cgalParamNBegin{name} … \cgalParamNEnd blocks render as +# nested HTML lists in our Doxygen output. +ALIASES += "cgalNamedParamsBegin=
Optional named parameters
" +ALIASES += "cgalNamedParamsEnd=
" +ALIASES += "cgalParamNBegin{1}=\1" +ALIASES += "cgalParamNEnd=" +ALIASES += "cgalParamDescription{1}=Description: \1
" +ALIASES += "cgalParamType{1}=Type: \1
" +ALIASES += "cgalParamDefault{1}=Default: \1
" +ALIASES += "cgalParamPrecondition{1}=Precondition: \1
" +ALIASES += "cgalParamExtra{1}=\1
" diff --git a/code/include/CGAL/Conformal_map/doxygen_groups.h b/code/include/CGAL/Conformal_map/doxygen_groups.h new file mode 100644 index 0000000..77d010a --- /dev/null +++ b/code/include/CGAL/Conformal_map/doxygen_groups.h @@ -0,0 +1,44 @@ +// Copyright (c) 2024-2026 Tarik Moussa. +// SPDX-License-Identifier: MIT +// +// This header contains only Doxygen \defgroup commands. It is included +// nowhere in the build; its sole purpose is to register the package's +// Doxygen group hierarchy so that `@ingroup Pkg...` references in the +// other public headers resolve cleanly. + +#ifndef CGAL_CONFORMAL_MAP_DOXYGEN_GROUPS_H +#define CGAL_CONFORMAL_MAP_DOXYGEN_GROUPS_H + +/*! +\defgroup PkgConformalMap CGAL Discrete Conformal Map package +\brief Discrete conformal maps on triangulated surfaces — five DCE models +(Euclidean, Spherical, Hyper-Ideal, Circle-Packing Euclidean, +Inversive-Distance). + +This package provides the C++ implementation of the variational discrete +conformal equivalence solvers from Springborn 2020, Bobenko/Pinkall/Springborn +2010, and Luo 2004, together with a CGAL-style named-parameter API. + +See `doc/api/cgal-package.md` for the full design rationale. +*/ + +/*! +\defgroup PkgConformalMapRef Reference manual +\ingroup PkgConformalMap +\brief Public C++ API: entry functions, traits, layout helpers. +*/ + +/*! +\defgroup PkgConformalMapConcepts Concepts +\ingroup PkgConformalMap +\brief C++ concepts and traits classes consumed by the entry functions. +*/ + +/*! +\defgroup PkgConformalMapNamedParameters Named function parameters +\ingroup PkgConformalMap +\brief Package-specific named-parameter helpers in `CGAL::parameters::*`, +plus the pipe-operator chaining convention. +*/ + +#endif // CGAL_CONFORMAL_MAP_DOXYGEN_GROUPS_H diff --git a/code/include/CGAL/Conformal_map/doxygen_namespaces.h b/code/include/CGAL/Conformal_map/doxygen_namespaces.h new file mode 100644 index 0000000..c697fb8 --- /dev/null +++ b/code/include/CGAL/Conformal_map/doxygen_namespaces.h @@ -0,0 +1,88 @@ +// Copyright (c) 2024-2026 Tarik Moussa. +// SPDX-License-Identifier: MIT +// +// Doxygen namespace documentation only — no declarations. Centralised +// here so that each namespace gets a single, consistent description in +// the generated HTML, regardless of which header is parsed first. + +#ifndef CGAL_CONFORMAL_MAP_DOXYGEN_NAMESPACES_H +#define CGAL_CONFORMAL_MAP_DOXYGEN_NAMESPACES_H + +/*! +\namespace CGAL +\brief Root namespace of the CGAL library; conformallab++ adds its +public entry points (`discrete_conformal_map_*`, `Conformal_map_traits`, +…) directly into this namespace, matching CGAL package conventions. +*/ + +/*! +\namespace CGAL::Conformal_map +\brief Implementation-detail namespace for the Discrete Conformal Map +package. Users normally do not need to enter this namespace; all +public entry points are re-exported into `CGAL::`. +*/ + +/*! +\namespace CGAL::Conformal_map::internal_np +\brief Tag types backing the package-local named-function parameters +(`vertex_curvature_map_t`, `gradient_tolerance_t`, `output_uv_map_t`, …). +Users invoke them via the helpers in `CGAL::parameters::*`. +*/ + +/*! +\namespace CGAL::parameters +\brief CGAL named-function-parameter helpers — both upstream CGAL's and +the conformallab++ package extensions (`vertex_curvature_map(...)`, +`gradient_tolerance(...)`, `output_uv_map(...)`, `normalise_layout(...)`). +Also home of the pipe-operator chaining convention; see +`doc/tutorials/add-output-uv-map.md` §3.4. +*/ + +/*! +\namespace conformallab +\brief Core math/algorithm namespace of conformallab++. Holds the five +DCE functionals (Euclidean / Spherical / HyperIdeal / CP-Euclidean / +Inversive-Distance), the Newton solver, layout helpers, mesh-property +typedefs, and serialisation utilities. Lives under +`code/include/*.hpp` and is consumed both by the standalone CLI and +by the thin CGAL wrappers under `CGAL::`. +*/ + +/*! +\namespace conformallab::detail +\brief Implementation-private helpers for the `conformallab` namespace. +Not part of the stable public API. +*/ + +/*! +\namespace conformallab::cp_detail +\brief Implementation-private helpers for the Circle-Packing Euclidean +functional (see `cp_euclidean_functional.hpp`). +*/ + +/*! +\namespace conformallab::id_detail +\brief Implementation-private helpers for the Inversive-Distance +functional (see `inversive_distance_functional.hpp`). +*/ + +/*! +\namespace conformallab::detail_xml +\brief Implementation-private XML helpers for the (de)serialisation +layer (see `serialization.hpp`). +*/ + +/*! +\namespace mesh_utils +\brief Small, opinion-free mesh utilities (loaders, validators, +property-map registration) used by both the standalone tools and the +CGAL wrappers. +*/ + +/*! +\namespace viewer_utils +\brief libigl-based interactive viewer helpers; built only when +`WITH_VIEWER=ON`. Not part of the headless / CGAL public surface. +*/ + +#endif // CGAL_CONFORMAL_MAP_DOXYGEN_NAMESPACES_H diff --git a/code/include/CGAL/Conformal_map/internal/parameters.h b/code/include/CGAL/Conformal_map/internal/parameters.h index 8aa1aac..f234248 100644 --- a/code/include/CGAL/Conformal_map/internal/parameters.h +++ b/code/include/CGAL/Conformal_map/internal/parameters.h @@ -198,11 +198,8 @@ inline auto normalise_layout(bool flag) // operator on the same type. // ════════════════════════════════════════════════════════════════════════════ -/*! -\addtogroup PkgConformalMapNamedParameters -\{ -*/ - +// Close the PkgConformalMapNamedParameters group block that was opened +// above the helper functions (see \addtogroup at the top of this section). /// \} } // namespace parameters diff --git a/code/include/CGAL/Conformal_map_traits.h b/code/include/CGAL/Conformal_map_traits.h index e88c642..7036b44 100644 --- a/code/include/CGAL/Conformal_map_traits.h +++ b/code/include/CGAL/Conformal_map_traits.h @@ -110,20 +110,33 @@ selected automatically when `TriangleMesh = CGAL::Surface_mesh<...>`. template struct Default_conformal_map_traits, K> { + /// The CGAL kernel parameter; defaults to `Simple_cartesian`. using Kernel = K; + /// Field type used for all scalar conformal-map data (lengths, λ, Θ, …). using FT = typename K::FT; + /// 3-D point type used for vertex coordinates. using Point_3 = typename K::Point_3; + /// The triangle-mesh type this specialisation targets. using Triangle_mesh = CGAL::Surface_mesh; + /// Boost-graph vertex descriptor for `Triangle_mesh`. using Vertex_descriptor = typename boost::graph_traits::vertex_descriptor; + /// Boost-graph half-edge descriptor for `Triangle_mesh`. using Halfedge_descriptor = typename boost::graph_traits::halfedge_descriptor; + /// Boost-graph edge descriptor for `Triangle_mesh`. using Edge_descriptor = typename boost::graph_traits::edge_descriptor; + /// Boost-graph face descriptor for `Triangle_mesh`. using Face_descriptor = typename boost::graph_traits::face_descriptor; // Property-map types — match the names used by setup_euclidean_maps(). + + /// Property map vertex → `Point_3` (the mesh's geometric embedding). using Vertex_point_map = typename Triangle_mesh::template Property_map; + /// Property map vertex → target cone angle Θᵥ in radians (legacy name `ev:theta`). using Theta_pmap = typename Triangle_mesh::template Property_map; + /// Property map vertex → contiguous integer index (legacy name `ev:idx`). using Vertex_index_pmap = typename Triangle_mesh::template Property_map; + /// Property map edge → log of original edge length λ⁰ (legacy name `ee:lam0`). using Lambda0_pmap = typename Triangle_mesh::template Property_map; // ─── Property-map accessors ─────────────────────────────────────────── @@ -133,10 +146,12 @@ struct Default_conformal_map_traits, K> // calling either `setup_euclidean_maps(m)` first or the accessor first // is equivalent. + /// Return the built-in vertex-point map of `m` (the geometric embedding). static Vertex_point_map vertex_points(Triangle_mesh& m) { return m.points(); } + /// Return (or create with default 2π) the target-angle property map. static Theta_pmap theta_map(Triangle_mesh& m) { auto [pm, created] = m.template add_property_map( "ev:theta", FT(2.0 * 3.141592653589793238)); @@ -144,6 +159,7 @@ struct Default_conformal_map_traits, K> return pm; } + /// Return (or create with default −1) the vertex-index property map. static Vertex_index_pmap vertex_index_map(Triangle_mesh& m) { auto [pm, created] = m.template add_property_map( "ev:idx", -1); @@ -151,6 +167,7 @@ struct Default_conformal_map_traits, K> return pm; } + /// Return (or create with default 0) the λ⁰ (initial log-length) property map. static Lambda0_pmap lambda0_map(Triangle_mesh& m) { auto [pm, created] = m.template add_property_map( "ee:lam0", FT(0)); diff --git a/code/include/CGAL/Discrete_circle_packing.h b/code/include/CGAL/Discrete_circle_packing.h index 175975d..dca5458 100644 --- a/code/include/CGAL/Discrete_circle_packing.h +++ b/code/include/CGAL/Discrete_circle_packing.h @@ -38,26 +38,51 @@ namespace CGAL { // ── Default traits for CP-Euclidean ─────────────────────────────────────────── +/*! +\ingroup PkgConformalMapConcepts +\brief Traits class for `discrete_circle_packing_euclidean()` — +declares the kernel, mesh and property-map types used by the +BPS-2010 face-based circle-packing functional. + +Primary template; specialise it for non-`Surface_mesh` triangle meshes. +*/ template > struct Default_cp_euclidean_traits; +/*! +\ingroup PkgConformalMapConcepts +\brief Specialisation for `CGAL::Surface_mesh

`; the only one shipped +in Phase 8b-Lite. +*/ template struct Default_cp_euclidean_traits, K> { + /// CGAL kernel parameter (defaults to `Simple_cartesian`). using Kernel = K; + /// Scalar field type used for all CP-Euclidean DOFs (`ρ_f`, `θ_e`, `φ_f`). using FT = typename K::FT; + /// 3-D point type (vertex coordinates). using Point_3 = typename K::Point_3; + /// Triangle-mesh type this specialisation targets. using Triangle_mesh = CGAL::Surface_mesh; + /// Boost-graph vertex descriptor for `Triangle_mesh`. using Vertex_descriptor = typename boost::graph_traits::vertex_descriptor; + /// Boost-graph half-edge descriptor for `Triangle_mesh`. using Halfedge_descriptor = typename boost::graph_traits::halfedge_descriptor; + /// Boost-graph edge descriptor for `Triangle_mesh`. using Edge_descriptor = typename boost::graph_traits::edge_descriptor; + /// Boost-graph face descriptor for `Triangle_mesh`. using Face_descriptor = typename boost::graph_traits::face_descriptor; // CP-Euclidean property maps — note the *face* DOF index map. + + /// Property map face → contiguous integer DOF index (legacy `cf:idx`). using Face_index_pmap = typename Triangle_mesh::template Property_map; + /// Property map edge → intersection angle θₑ (legacy `ce:theta`). using Theta_e_pmap = typename Triangle_mesh::template Property_map; + /// Property map face → target angle sum φ_f (legacy `cf:phi`). using Phi_f_pmap = typename Triangle_mesh::template Property_map; }; @@ -75,8 +100,11 @@ struct Circle_packing_result /// Face DOFs `ρ_f = log R_f` (length = num_faces(mesh); pinned face = 0). std::vector rho_per_face; + /// Newton iterations actually performed (≤ `max_iterations`). int iterations = 0; + /// Final infinity-norm of the gradient (Newton stopping criterion). FT gradient_norm = FT(0); + /// `true` iff `gradient_norm < gradient_tolerance` at exit. bool converged = false; }; diff --git a/code/include/CGAL/Discrete_conformal_map.h b/code/include/CGAL/Discrete_conformal_map.h index 44dd1ae..869748d 100644 --- a/code/include/CGAL/Discrete_conformal_map.h +++ b/code/include/CGAL/Discrete_conformal_map.h @@ -449,8 +449,11 @@ struct Hyper_ideal_map_result /// Edge DOFs `a_e` (length = num_edges(mesh); pinned edges = 0). std::vector a_per_edge; + /// Newton iterations actually performed (≤ `max_iterations`). int iterations = 0; + /// Final infinity-norm of the gradient (Newton stopping criterion). FT gradient_norm = FT(0); + /// `true` iff `gradient_norm < gradient_tolerance` at exit. bool converged = false; }; diff --git a/code/include/CGAL/Discrete_inversive_distance.h b/code/include/CGAL/Discrete_inversive_distance.h index 341b346..1a3aded 100644 --- a/code/include/CGAL/Discrete_inversive_distance.h +++ b/code/include/CGAL/Discrete_inversive_distance.h @@ -47,25 +47,49 @@ namespace CGAL { // ── Default traits for Inversive-Distance ──────────────────────────────────── +/*! +\ingroup PkgConformalMapConcepts +\brief Traits class for `discrete_inversive_distance_map()` — declares +the kernel, mesh and property-map types used by Luo's 2004 vertex-based +inversive-distance circle packing. + +Primary template; specialise it for non-`Surface_mesh` triangle meshes. +*/ template > struct Default_inversive_distance_traits; +/*! +\ingroup PkgConformalMapConcepts +\brief Specialisation for `CGAL::Surface_mesh

`; the only one shipped +in Phase 8b-Lite. +*/ template struct Default_inversive_distance_traits, K> { + /// CGAL kernel parameter (defaults to `Simple_cartesian`). using Kernel = K; + /// Scalar field type used for all inversive-distance DOFs. using FT = typename K::FT; + /// 3-D point type (vertex coordinates). using Point_3 = typename K::Point_3; + /// Triangle-mesh type this specialisation targets. using Triangle_mesh = CGAL::Surface_mesh; + /// Boost-graph vertex descriptor for `Triangle_mesh`. using Vertex_descriptor = typename boost::graph_traits::vertex_descriptor; + /// Boost-graph edge descriptor for `Triangle_mesh`. using Edge_descriptor = typename boost::graph_traits::edge_descriptor; // Inversive-distance specific property maps. + + /// Property map vertex → contiguous integer DOF index (legacy `iv:idx`). using Vertex_index_pmap = typename Triangle_mesh::template Property_map; + /// Property map vertex → target cone angle Θᵥ in radians (legacy `iv:theta`). using Theta_v_pmap = typename Triangle_mesh::template Property_map; + /// Property map vertex → initial radius r⁰ᵥ (legacy `iv:r0`). using R0_pmap = typename Triangle_mesh::template Property_map; + /// Property map edge → inversive distance Iᵢⱼ (legacy `ie:I`). using I_e_pmap = typename Triangle_mesh::template Property_map; }; diff --git a/code/include/conformal_mesh.hpp b/code/include/conformal_mesh.hpp index f0622e7..2fe5321 100644 --- a/code/include/conformal_mesh.hpp +++ b/code/include/conformal_mesh.hpp @@ -41,30 +41,42 @@ namespace conformallab { // ── Kernel ────────────────────────────────────────────────────────────────── // Simple double-precision Cartesian. Conformal mapping algorithms never // need exact arithmetic — they operate on floating-point lengths and angles. + +/// CGAL kernel used by all conformallab algorithms (double precision). using Kernel = CGAL::Simple_cartesian; +/// 3-D point type (vertex coordinates). using Point3 = Kernel::Point_3; +/// 2-D point type (UV-domain layout coordinates). using Point2 = Kernel::Point_2; // ── Mesh type ──────────────────────────────────────────────────────────────── +/// Triangle mesh carrying all conformal-map data as property maps. using ConformalMesh = CGAL::Surface_mesh; // ── Index/descriptor aliases (CGAL 6.x naming) ─────────────────────────────── +/// Vertex descriptor of `ConformalMesh`. using Vertex_index = ConformalMesh::Vertex_index; +/// Half-edge descriptor of `ConformalMesh`. using Halfedge_index = ConformalMesh::Halfedge_index; +/// Edge descriptor of `ConformalMesh`. using Edge_index = ConformalMesh::Edge_index; +/// Face descriptor of `ConformalMesh`. using Face_index = ConformalMesh::Face_index; // ── Geometry type constant (replaces Java CoFace.type enum) ───────────────── +/// Discrete geometry type that a face/mesh is interpreted in. +/// Replaces the original Java `CoFace.type` enum. enum class GeometryType : int { - Euclidean = 0, - Hyperbolic = 1, - Spherical = 2 + Euclidean = 0, ///< Flat metric (ℝ²). + Hyperbolic = 1, ///< Hyperbolic metric (ℍ²). + Spherical = 2 ///< Spherical metric (S²). }; // ── Standard property-map bundles ──────────────────────────────────────────── -// Add the vertex properties used by all conformal-map functionals. -// Returns {lambda, theta, idx}. +/// Register and return the vertex-side property maps used by all five +/// DCE functionals: `v:lambda` (log conformal factor), `v:theta` (target +/// cone angle), `v:idx` (contiguous integer index). inline auto add_vertex_properties(ConformalMesh& mesh) { auto [lambda, ok1] = mesh.add_property_map("v:lambda", 0.0); @@ -74,7 +86,8 @@ inline auto add_vertex_properties(ConformalMesh& mesh) return std::make_tuple(lambda, theta, idx); } -// Add the edge intersection-angle property used by the hyperbolic functional. +/// Register and return the edge intersection-angle property `e:alpha` +/// (used by the hyper-ideal functional). inline auto add_edge_properties(ConformalMesh& mesh) { auto [alpha, ok] = mesh.add_property_map("e:alpha", 0.0); @@ -82,7 +95,7 @@ inline auto add_edge_properties(ConformalMesh& mesh) return alpha; } -// Add the face geometry-type property. +/// Register and return the per-face geometry-type property `f:type`. inline auto add_face_properties(ConformalMesh& mesh) { auto [ftype, ok] = mesh.add_property_map( diff --git a/doc/api/tests.md b/doc/api/tests.md index d97c84d..3f96c1e 100644 --- a/doc/api/tests.md +++ b/doc/api/tests.md @@ -45,7 +45,7 @@ All tests have CTest prefix `cgal.` (set via `TEST_PREFIX "cgal."` in CMakeLists | `Normalisation` | `test_phase6.cpp` | 4 | Euclidean centroid, length ratios, Möbius centring | | `MobiusMap` | `test_phase7.cpp` | 8 | Identity, inverse, compose, `from_three`, `apply(Vector2d)` | | `BestRootFace` | `test_phase7.cpp` | 2 | Valid root face selection, interior bonus | -| `HalfedgeUV` | `test_phase7.cpp` | 4 | Size = #halfedges, seam consistency, boundary halfedges = 0 | +| `HalfedgeUV` | `test_phase7.cpp` | 4 | Size = number of half-edges, seam consistency, boundary half-edges = 0 | | `PriorityBFS` | `test_phase7.cpp` | 3 | Success, no seam on open meshes, all vertices placed | | `NormaliseEuclidean` | `test_phase7.cpp` | 2 | UV centroid = 0, halfedge_uv centroid = 0 | | `PeriodMatrix` | `test_phase7.cpp` | 7 | τ ∈ ℍ, SL(2,ℤ) reduction, exception outside ℍ | diff --git a/doc/architecture/locked-vs-flexible.md b/doc/architecture/locked-vs-flexible.md index 3eb5724..5ea2793 100644 --- a/doc/architecture/locked-vs-flexible.md +++ b/doc/architecture/locked-vs-flexible.md @@ -167,7 +167,7 @@ the CGAL-canonical syntax AND we are willing to fork CGAL upstream. | Status | 🟢 opportunistic | | Locked since | Phase 3 + 9a (prefix `ev:`/`sv:`/`v:`/`cf:`/`ce:`/`iv:`/`ie:` set when each functional was introduced) | | Cost to change | One sed-replace + recompile. No user-visible effect because the names are an *internal* convention; the CGAL public API never exposes them. | -| When to revisit | If a future functional reuses an existing letter prefix. Already discussed in [`locked-vs-flexible.md`](#4-five-dce-models-on-the-same-mesh). | +| When to revisit | If a future functional reuses an existing letter prefix. Already discussed in §4 "Five DCE models on the same mesh" above. | --- diff --git a/doc/architecture/overall_pipeline.md b/doc/architecture/overall_pipeline.md index 4129c98..4e10331 100644 --- a/doc/architecture/overall_pipeline.md +++ b/doc/architecture/overall_pipeline.md @@ -150,7 +150,7 @@ maps.theta_v[v] = M_PI / 3; // 60° cone singularity Before solving, the prescribed angles must satisfy: -$$\sum_{v} (2\pi - \Theta_v) = 2\pi \cdot \chi(M)$$ +> ∑ᵥ (2π − Θᵥ) = 2π · χ(M) ```cpp check_gauss_bonnet(mesh, maps); // throws if violated @@ -283,7 +283,7 @@ Both `uv` and `halfedge_uv` are transformed identically. From the two holonomy translations ω₁, ω₂ ∈ ℂ read off from the cut graph, the conformal type of a flat torus is the SL(2,ℤ)-orbit of: -$$\tau = \omega_2 / \omega_1 \in \mathbb{H}$$ +> τ = ω₂ / ω₁ ∈ ℍ ```cpp PeriodData pd = compute_period_matrix(hol); diff --git a/scripts/doxygen-coverage.sh b/scripts/doxygen-coverage.sh new file mode 100755 index 0000000..2b84152 --- /dev/null +++ b/scripts/doxygen-coverage.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# scripts/doxygen-coverage.sh +# +# Measure Doxygen documentation coverage of the public C++ API by parsing +# the Doxygen XML output. Reports: +# * total documentable members (functions, classes, structs, enums, +# typedefs, variables) in code/include/** +# * how many have a non-empty briefdescription/detaileddescription +# * coverage % and list of undocumented members +# +# Prerequisite: doxygen must have been run with GENERATE_XML=YES (which +# the project's Doxyfile sets). This script invokes it if XML is missing. +# +# Usage: +# bash scripts/doxygen-coverage.sh # short summary +# bash scripts/doxygen-coverage.sh --list-undoc # list undocumented members +# bash scripts/doxygen-coverage.sh --threshold 95 # fail if coverage < 95 % +# +# Exit codes: +# 0 coverage ≥ threshold (default 0 — informational only) +# 1 coverage < threshold +# 2 XML output missing / could not be parsed + +set -eu + +XML_DIR="doc/doxygen/xml" +THRESHOLD=0 +LIST_UNDOC=0 + +INCLUDE_DETAIL=0 + +while [ $# -gt 0 ]; do + case "$1" in + --threshold) THRESHOLD="$2"; shift 2 ;; + --list-undoc) LIST_UNDOC=1; shift ;; + --include-detail) INCLUDE_DETAIL=1; shift ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + +if [ ! -d "$XML_DIR" ]; then + echo "XML output missing — running doxygen..." + doxygen Doxyfile >/dev/null 2>&1 +fi + +if [ ! -d "$XML_DIR" ]; then + echo "ERROR: $XML_DIR still missing after doxygen run" >&2 + exit 2 +fi + +python3 - "$XML_DIR" "$LIST_UNDOC" "$THRESHOLD" "$INCLUDE_DETAIL" <<'PYEOF' +import sys, os, glob, xml.etree.ElementTree as ET + +xml_dir, list_undoc, threshold, include_detail = \ + sys.argv[1], int(sys.argv[2]), float(sys.argv[3]), int(sys.argv[4]) + +# Implementation-detail namespaces — not part of the public API surface. +# Skipped by default; pass --include-detail to count them too. +DETAIL_NAMES = ("::detail::", "::detail_xml::", "::cp_detail::", "::id_detail::", + "::detail$", "::detail_xml$", "::cp_detail$", "::id_detail$") + +def is_detail(qualified_name: str) -> bool: + if include_detail: + return False + return any(qualified_name.find(d.rstrip("$")) >= 0 for d in DETAIL_NAMES) + +# Restrict to compounds whose location is under code/include/ (the +# public API). XML output also includes README.md and CLAUDE.md as +# "file" kind compounds, which we want to skip. +PUBLIC_PREFIX = os.path.abspath("code/include") + os.sep + +KINDS = {"function", "class", "struct", "enum", "typedef", "variable", "namespace"} + +total = 0 +documented = 0 +undoc = [] + +for path in sorted(glob.glob(os.path.join(xml_dir, "*.xml"))): + if os.path.basename(path) in {"index.xml", "Doxyfile.xml", "indexpage.xml"}: + continue + if os.path.basename(path).startswith(("namespacestd", "md_")): + continue + try: + tree = ET.parse(path) + except ET.ParseError: + continue + for cd in tree.iter("compounddef"): + kind = cd.attrib.get("kind", "") + # only count compounds living in our public include tree + loc = cd.find("location") + if loc is None: + continue + file_attr = loc.attrib.get("file", "") + if not file_attr.startswith(PUBLIC_PREFIX) and \ + not file_attr.startswith("code/include/"): + continue + + # The compound itself + if kind in {"class", "struct", "namespace"}: + cname = cd.findtext("compoundname", "?") + if not is_detail(cname): + total += 1 + brief = cd.find("briefdescription") + detail = cd.find("detaileddescription") + has_doc = (brief is not None and len("".join(brief.itertext()).strip()) > 0) or \ + (detail is not None and len("".join(detail.itertext()).strip()) > 0) + if has_doc: + documented += 1 + else: + undoc.append(f"{kind:9s} {cname} ({file_attr}:{loc.attrib.get('line','?')})") + + # Members inside the compound + for memberdef in cd.iter("memberdef"): + mkind = memberdef.attrib.get("kind", "") + if mkind not in KINDS: + continue + prot = memberdef.attrib.get("prot", "public") + if prot != "public": + continue + name = memberdef.findtext("name", "?") + qual = memberdef.findtext("qualifiedname", name) + if is_detail(qual): + continue + total += 1 + brief = memberdef.find("briefdescription") + detail = memberdef.find("detaileddescription") + has_doc = (brief is not None and len("".join(brief.itertext()).strip()) > 0) or \ + (detail is not None and len("".join(detail.itertext()).strip()) > 0) + if has_doc: + documented += 1 + else: + mloc = memberdef.find("location") + fl = mloc.attrib.get("file", "?") if mloc is not None else "?" + ln = mloc.attrib.get("line", "?") if mloc is not None else "?" + undoc.append(f"{mkind:9s} {qual} ({fl}:{ln})") + +if total == 0: + print("ERROR: no public members found — check that GENERATE_XML=YES and EXTRACT_ALL=YES") + sys.exit(2) + +pct = 100.0 * documented / total +print(f"Doxygen coverage (public symbols under code/include/):") +print(f" documented: {documented}") +print(f" total: {total}") +print(f" coverage: {pct:.1f}%") +print(f" undocumented: {total - documented}") + +if list_undoc: + print() + print("Undocumented symbols:") + for s in undoc: + print(f" {s}") + +if pct < threshold: + print(f"\nFAIL: coverage {pct:.1f}% < threshold {threshold}%", file=sys.stderr) + sys.exit(1) + +sys.exit(0) +PYEOF