>
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