Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m24s
API Docs / doc-build (pull_request) Successful in 1m5s
Markdown link check / check (pull_request) Successful in 50s
C++ Tests / test-cgal (pull_request) Failing after 12m53s
C++ Tests / quality-gates (pull_request) Successful in 2m33s
Three reviewer-meeting deliverables in one commit.
(1) output_uv_map for the two remaining DCE entries
─────────────────────────────────────────────────
* Discrete_inversive_distance.h: full implementation. After Newton,
reconstruct effective Euclidean edge lengths from the converged
log-radii via the Bowers-Stephenson identity
`ℓᵢⱼ² = rᵢ² + rⱼ² + 2·Iᵢⱼ·rᵢ·rⱼ`, populate a temporary
EuclideanMaps with `lambda0 = log(ℓᵢⱼ²)`, and reuse the existing
`euclidean_layout(mesh, 0, eucl)` priority-BFS. Per-vertex
Point_2 coordinates written into the user-supplied pmap.
Optional `normalise_layout(true)` applies the canonical PCA
centroid + major-axis rotation, same as the other 3 entries.
* Discrete_circle_packing.h: throws std::runtime_error with a
clear pointer to Phase 9c rather than silently producing
nonsense. CP-Euclidean is face-based; the faithful output is a
per-face circle packing in ℝ², not a per-vertex Point_2 map.
A true layout requires BPS-2010 §6 (~150 lines, on the porting
roadmap as Phase 9c). Failing loudly is the honest default.
Tests: 2 new cases in test_cgal_phase8b_lite.cpp
(OutputUvMap_InversiveDistance_PopulatesPmap;
OutputUvMap_CPEuclidean_ThrowsClearly). Both green.
Suite total now 259 (was 257, +2). CGAL subtotal: 234 → 236.
(2) Reviewer meeting documents
──────────────────────────
New directory doc/reviewer/ with three files:
* briefing.md — one-page orientation for the reviewer.
What the project is, where to look first
(https://tmoussa.codeberg.page/ConformalLabpp/), the headline
evidence (tests/coverage/sanitizers/license), what we want from
them, what's deferred and why, and the 5 questions in a separate
file.
* questions.md — the 5 concrete decisions we want their second
opinion on:
Q1 Phase 9c (port-literal vs re-derive)
Q2 Phase 9b-analytic (worth ~2 weeks for ~6× speedup?)
Q3 CP-Euclidean output_uv_map (build now or defer?)
Q4 CGAL submission strategy (one package or five?)
Q5 geometry-central cross-validation co-authorship
Plus an explicit "what would you say no to?" question at the
bottom — negative feedback is the highest-value information.
* agenda.md — my own internal playbook (NOT to be sent).
60-min flow: 5-min thank-you, 10-min architecture tour,
30-min for Q1-Q5 in the order Q4-Q1-Q2-Q5-Q3, 5-min "no"
question, 5-min wrap-up. Includes post-meeting memo template
to fill out in the 30 min after.
* README.md — index for the directory; says which file goes
to whom and when to send.
(3) locked-vs-flexible.md known-limitations update
─────────────────────────────────────────────
"output_uv_map covers 3 of 5 entries" → "covers 4 of 5".
CP-Euclidean's throws-clearly behaviour documented as a Phase 9c
deliverable rather than a passive gap.
Bonus: extended .codespellrc ignore list (acknowledgement, the
British-English spelling I used in agenda.md).
Verifications on this commit:
259/259 tests pass (0 skipped)
scripts/check-test-counts.sh: OK (23 + 236 = 259)
scripts/quality/license-headers.sh: OK (66/66 SPDX)
python3 scripts/quality/cgal-conventions.py: OK (0/6 violations)
scripts/quality/codespell.sh: OK (0 typos)
scripts/quality/shellcheck.sh: OK (0 findings)
python3 scripts/check-markdown-links.py: OK (143/143)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
208 lines
8.8 KiB
C++
208 lines
8.8 KiB
C++
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
//
|
||
// Package: conformallab++ / Discrete_conformal_map (Phase 8b-Lite, 2026-05-21)
|
||
|
||
/*!
|
||
\file CGAL/Discrete_circle_packing.h
|
||
\ingroup PkgConformalMapRef
|
||
|
||
User-facing entry for the **face-based** circle-packing functional of
|
||
Bobenko-Pinkall-Springborn 2010. See `cp_euclidean_functional.hpp`
|
||
for the underlying algorithm and `doc/architecture/phase-9a-validation.md`
|
||
for the line-by-line mapping to the Java original
|
||
`CPEuclideanFunctional.java`.
|
||
|
||
This functional has a fundamentally different DOF structure to the
|
||
classical Euclidean / Spherical / HyperIdeal modes — one log-radius
|
||
`ρ_f` per **face** rather than one log-scale `u_v` per vertex. We
|
||
therefore expose it via a dedicated header with its own default-trait
|
||
class (Strategy C of the Phase 8b architecture audit).
|
||
*/
|
||
|
||
#ifndef CGAL_DISCRETE_CIRCLE_PACKING_H
|
||
#define CGAL_DISCRETE_CIRCLE_PACKING_H
|
||
|
||
#include <CGAL/Conformal_map/internal/parameters.h>
|
||
#include <CGAL/Kernel_traits.h>
|
||
#include <CGAL/Named_function_parameters.h>
|
||
#include <CGAL/boost/graph/named_params_helper.h>
|
||
#include <CGAL/Surface_mesh.h>
|
||
#include <CGAL/Simple_cartesian.h>
|
||
#include <boost/graph/graph_traits.hpp>
|
||
|
||
#include "../cp_euclidean_functional.hpp"
|
||
#include "../newton_solver.hpp"
|
||
|
||
#include <stdexcept>
|
||
|
||
namespace CGAL {
|
||
|
||
// ── Default traits for CP-Euclidean ───────────────────────────────────────────
|
||
|
||
template <typename TriangleMesh,
|
||
typename Kernel_ = CGAL::Simple_cartesian<double>>
|
||
struct Default_cp_euclidean_traits;
|
||
|
||
template <typename K>
|
||
struct Default_cp_euclidean_traits<CGAL::Surface_mesh<typename K::Point_3>, K>
|
||
{
|
||
using Kernel = K;
|
||
using FT = typename K::FT;
|
||
using Point_3 = typename K::Point_3;
|
||
using Triangle_mesh = CGAL::Surface_mesh<Point_3>;
|
||
|
||
using Vertex_descriptor = typename boost::graph_traits<Triangle_mesh>::vertex_descriptor;
|
||
using Halfedge_descriptor = typename boost::graph_traits<Triangle_mesh>::halfedge_descriptor;
|
||
using Edge_descriptor = typename boost::graph_traits<Triangle_mesh>::edge_descriptor;
|
||
using Face_descriptor = typename boost::graph_traits<Triangle_mesh>::face_descriptor;
|
||
|
||
// CP-Euclidean property maps — note the *face* DOF index map.
|
||
using Face_index_pmap = typename Triangle_mesh::template Property_map<Face_descriptor, int>;
|
||
using Theta_e_pmap = typename Triangle_mesh::template Property_map<Edge_descriptor, FT>;
|
||
using Phi_f_pmap = typename Triangle_mesh::template Property_map<Face_descriptor, FT>;
|
||
};
|
||
|
||
// ── Result type ───────────────────────────────────────────────────────────────
|
||
|
||
/*!
|
||
\ingroup PkgConformalMapRef
|
||
|
||
Result of `discrete_circle_packing_euclidean`. Carries face DOFs
|
||
`ρ_f = log R_f` rather than the vertex DOFs of the classical modes.
|
||
*/
|
||
template <typename FT = double>
|
||
struct Circle_packing_result
|
||
{
|
||
/// Face DOFs `ρ_f = log R_f` (length = num_faces(mesh); pinned face = 0).
|
||
std::vector<FT> rho_per_face;
|
||
|
||
int iterations = 0;
|
||
FT gradient_norm = FT(0);
|
||
bool converged = false;
|
||
};
|
||
|
||
// ── Entry function ────────────────────────────────────────────────────────────
|
||
|
||
/*!
|
||
\ingroup PkgConformalMapRef
|
||
|
||
Compute the BPS-2010 face-based circle-packing of `mesh`.
|
||
|
||
\tparam TriangleMesh A `CGAL::Surface_mesh<P>`.
|
||
\tparam NamedParameters Optional CGAL named-parameter pack.
|
||
|
||
\param mesh Input triangle mesh.
|
||
\param np Named parameters (subset of those documented on
|
||
`discrete_conformal_map_euclidean`; the curvature-map
|
||
parameter `vertex_curvature_map` is **not** used in this
|
||
face-based mode — instead the per-face target angle sum
|
||
`φ_f` and per-edge intersection angle `θ_e` are set via
|
||
the property maps on `mesh` before this call, or left at
|
||
their defaults `φ_f = 2π`, `θ_e = π/2`).
|
||
|
||
\returns A `Circle_packing_result<FT>` with `ρ_f` per face.
|
||
|
||
\pre `mesh` is a triangle mesh.
|
||
\pre `φ_f` and `θ_e` satisfy the BPS-2010 admissibility conditions
|
||
(Σ_f φ_f = 2π·χ + Σ_e (π − θ_e), see paper §6).
|
||
*/
|
||
template <typename TriangleMesh,
|
||
typename CGAL_NP_TEMPLATE_PARAMETERS>
|
||
auto discrete_circle_packing_euclidean(
|
||
TriangleMesh& mesh,
|
||
const CGAL_NP_CLASS& np = parameters::default_values())
|
||
{
|
||
using Point_type = typename TriangleMesh::Point;
|
||
using Default_kernel = typename CGAL::Kernel_traits<Point_type>::Kernel;
|
||
using Default_traits = Default_cp_euclidean_traits<TriangleMesh, Default_kernel>;
|
||
using Traits = typename internal_np::Lookup_named_param_def<
|
||
internal_np::geom_traits_t,
|
||
CGAL_NP_CLASS,
|
||
Default_traits>::type;
|
||
using FT = typename Traits::FT;
|
||
|
||
Circle_packing_result<FT> result;
|
||
|
||
auto maps = ::conformallab::setup_cp_euclidean_maps(mesh);
|
||
|
||
// Pin first face by default; `fixed_vertex_map` is reused here as the
|
||
// "fixed face" override hook (the parameter tag is generic enough).
|
||
// For a richer API, a dedicated `fixed_face_map` tag could be added.
|
||
auto it = mesh.faces().begin();
|
||
if (it == mesh.faces().end()) {
|
||
return result; // empty mesh; trivial
|
||
}
|
||
const int n = ::conformallab::assign_cp_euclidean_face_dof_indices(mesh, maps, *it);
|
||
|
||
const FT tol = parameters::choose_parameter(
|
||
parameters::get_parameter(np, Conformal_map::internal_np::gradient_tolerance),
|
||
FT(1e-10));
|
||
const int max_iter = parameters::choose_parameter(
|
||
parameters::get_parameter(np, Conformal_map::internal_np::max_iterations),
|
||
200);
|
||
|
||
// Natural-phi default: shift φ_f so the gradient at ρ = 0 is zero.
|
||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||
auto G0 = ::conformallab::cp_euclidean_gradient(mesh, x0, maps);
|
||
for (auto f : mesh.faces()) {
|
||
int i = maps.f_idx[f];
|
||
if (i >= 0) maps.phi_f[f] -= G0[static_cast<std::size_t>(i)];
|
||
}
|
||
|
||
auto nr = ::conformallab::newton_cp_euclidean(mesh, x0, maps, tol, max_iter);
|
||
|
||
result.rho_per_face.assign(num_faces(mesh), FT(0));
|
||
for (auto f : mesh.faces()) {
|
||
int j = maps.f_idx[f];
|
||
if (j >= 0) result.rho_per_face[f.idx()] = nr.x[static_cast<std::size_t>(j)];
|
||
}
|
||
result.iterations = nr.iterations;
|
||
result.gradient_norm = nr.grad_inf_norm;
|
||
result.converged = nr.converged;
|
||
|
||
// ── output_uv_map (Phase 8b-Lite extension) ────────────────────────────
|
||
//
|
||
// The CP-Euclidean functional carries one DOF per *face* (the log of the
|
||
// face-circle radius `ρ_f = log R_f`), not per vertex. A faithful
|
||
// layout therefore produces a circle packing in ℝ² — each face f is
|
||
// mapped to a circle of radius `R_f` at some centre `c_f`, with
|
||
// adjacent circles meeting at the prescribed intersection angle `θ_e`.
|
||
// That is a per-face output, not the per-vertex Point_2 that
|
||
// `output_uv_map` is typed for.
|
||
//
|
||
// For Phase 8b-Lite we deliberately don't fake it. If the caller
|
||
// supplies `output_uv_map(pmap)` we throw `std::runtime_error` with a
|
||
// clear pointer to Phase 9c (BPS-2010 §6 face-based circle-packing
|
||
// layout, ~150 lines, on the porting roadmap). Failing loudly is
|
||
// better than silently writing zeros.
|
||
//
|
||
// Users who want a UV-like coordinate today can:
|
||
// 1. Solve a Euclidean DCE on the same mesh (vertex DOFs),
|
||
// 2. Use `discrete_inversive_distance_map(... output_uv_map(pmap))`,
|
||
// 3. Or compute face-centre positions by hand from `result.rho_per_face`
|
||
// + the per-edge `θ_e` values, plus a priority-BFS of their own.
|
||
{
|
||
auto uv_param = parameters::get_parameter(
|
||
np, Conformal_map::internal_np::output_uv_map);
|
||
constexpr bool has_uv = !std::is_same_v<
|
||
decltype(uv_param), internal_np::Param_not_found>;
|
||
if constexpr (has_uv) {
|
||
throw std::runtime_error(
|
||
"CGAL::discrete_circle_packing_euclidean: the "
|
||
"`output_uv_map(...)` named parameter is not yet supported "
|
||
"for face-based CP-Euclidean. The faithful output is a "
|
||
"circle packing in the plane (per-face), not per-vertex "
|
||
"UVs. Tracked as Phase 9c; "
|
||
"see doc/architecture/locked-vs-flexible.md and "
|
||
"doc/tutorials/add-output-uv-map.md.");
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
} // namespace CGAL
|
||
|
||
#endif // CGAL_DISCRETE_CIRCLE_PACKING_H
|