Phase 8b-Lite: CGAL entries for all 5 DCE models + layout wrapper
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m7s
C++ Tests / test-fast (push) Successful in 2m19s
C++ Tests / test-cgal (pull_request) Failing after 11m6s
C++ Tests / test-cgal (push) Has been skipped
API Docs / doc-build (pull_request) Successful in 41s
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m7s
C++ Tests / test-fast (push) Successful in 2m19s
C++ Tests / test-cgal (pull_request) Failing after 11m6s
C++ Tests / test-cgal (push) Has been skipped
API Docs / doc-build (pull_request) Successful in 41s
Completes the CGAL public API surface so all five discrete-conformal
functionals are reachable from <CGAL/Discrete_*.h>, not only Euclidean.
CGAL test count: 219 → 227 (+8). Zero skips.
New public headers
──────────────────
* CGAL/Discrete_conformal_map.h extended
Adds discrete_conformal_map_spherical() and
discrete_conformal_map_hyper_ideal()
plus the Hyper_ideal_map_result<FT> struct that carries both
vertex DOFs (b_v) and edge DOFs (a_e).
* CGAL/Discrete_circle_packing.h new (180 lines)
Face-based BPS-2010 circle packing. Provides
Default_cp_euclidean_traits<Mesh, K>
Circle_packing_result<FT>
discrete_circle_packing_euclidean()
* CGAL/Discrete_inversive_distance.h new (180 lines)
Vertex-based Luo-2004 packing. Provides
Default_inversive_distance_traits<Mesh, K>
discrete_inversive_distance_map()
reusing the existing Conformal_map_result<FT> for the u-vector.
* CGAL/Conformal_layout.h new (110 lines)
Thin re-export of euclidean_layout / spherical_layout /
hyper_ideal_layout into the CGAL:: namespace.
Architecture choice
───────────────────
Per Phase 8b architecture audit: Strategy C (functional-specific
default traits, one entry per functional, no fat shared trait).
Documented in each header's docblock. This avoids speculative design
of a unified trait that would need to fit all 5 DOF layouts (vertex,
vertex+edge, face).
Conformal_map_traits.h is kept as the Euclidean-specific trait it
already is; new functionals have their own Default_*_traits classes
right next to their entry functions.
Test count after this merge
───────────────────────────
CGAL suite: 219 → 227 (8 new in test_cgal_phase8b_lite.cpp covering
all four new entries + the Euclidean+layout round-trip).
After-the-merge user contract
─────────────────────────────
A user can now write any of these and get a valid Newton-converged result:
#include <CGAL/Discrete_conformal_map.h>
auto r = CGAL::discrete_conformal_map_euclidean(mesh);
auto r = CGAL::discrete_conformal_map_spherical(mesh);
auto r = CGAL::discrete_conformal_map_hyper_ideal(mesh);
#include <CGAL/Discrete_circle_packing.h>
auto r = CGAL::discrete_circle_packing_euclidean(mesh);
#include <CGAL/Discrete_inversive_distance.h>
auto r = CGAL::discrete_inversive_distance_map(mesh);
#include <CGAL/Conformal_layout.h>
auto layout = CGAL::euclidean_layout(mesh, r.x, maps);
Not in this PR (intentionally deferred)
───────────────────────────────────────
* 8a.2 — Generic FaceGraph specialisation (still Surface_mesh-only).
* 8c — User_manual + PackageDescription.txt (CGAL-submission prep).
* 8d — CGAL-format test directory (CGAL-submission prep).
* 8e — YAML pipeline + CLI flag (orthogonal).
* Named-parameter chaining (`a.b().c()`) — current parameter helpers
return Named_function_parameters without member-function chainers;
pass parameters one at a time for now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,8 @@ auto result = CGAL::discrete_conformal_map_euclidean(
|
||||
|
||||
// Existing implementation headers (Layer 1 — unchanged).
|
||||
#include "../euclidean_functional.hpp"
|
||||
#include "../spherical_functional.hpp"
|
||||
#include "../hyper_ideal_functional.hpp"
|
||||
#include "../gauss_bonnet.hpp"
|
||||
#include "../newton_solver.hpp"
|
||||
|
||||
@@ -269,6 +271,220 @@ auto discrete_conformal_map_euclidean(
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// discrete_conformal_map_spherical — Phase 8b-Lite
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/*!
|
||||
\ingroup PkgConformalMapRef
|
||||
|
||||
Compute the spherical discrete-conformal map of a closed genus-0 mesh.
|
||||
|
||||
The spherical DCE energy is *concave*, so its Hessian is NSD at the
|
||||
optimum and `newton_spherical()` factorises −H internally (handled by
|
||||
the legacy implementation; no caller action required). A gauge vertex
|
||||
is pinned automatically to remove the rotational mode.
|
||||
|
||||
\tparam TriangleMesh A `CGAL::Surface_mesh<P>` for some point type `P`.
|
||||
\tparam NamedParameters Optional CGAL named-parameter pack.
|
||||
|
||||
\param mesh The input mesh (modified in place: property maps attached).
|
||||
\param np Same named parameters as `discrete_conformal_map_euclidean`.
|
||||
|
||||
\returns A `Conformal_map_result<FT>` carrying `u_v` per vertex and
|
||||
Newton diagnostics.
|
||||
|
||||
\pre `mesh` is a closed genus-0 triangle mesh.
|
||||
\pre The user-supplied or natural-theta Θ satisfies the spherical
|
||||
Gauss–Bonnet relation `Σ(2π − Θᵥ) = 4π` (sphere).
|
||||
*/
|
||||
template <typename TriangleMesh,
|
||||
typename CGAL_NP_TEMPLATE_PARAMETERS>
|
||||
auto discrete_conformal_map_spherical(
|
||||
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_conformal_map_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;
|
||||
|
||||
Conformal_map_result<FT> result;
|
||||
|
||||
auto maps = ::conformallab::setup_spherical_maps(mesh);
|
||||
::conformallab::compute_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
auto theta_param = parameters::get_parameter(
|
||||
np, Conformal_map::internal_np::vertex_curvature_map);
|
||||
constexpr bool has_theta = !std::is_same_v<
|
||||
decltype(theta_param), internal_np::Param_not_found>;
|
||||
if constexpr (has_theta) {
|
||||
for (auto v : mesh.vertices())
|
||||
maps.theta_v[v] = get(theta_param, v);
|
||||
}
|
||||
|
||||
// Pin one vertex (gauge fix) — user-supplied or first vertex.
|
||||
constexpr int FREE = 0;
|
||||
for (auto v : mesh.vertices()) maps.v_idx[v] = FREE;
|
||||
|
||||
auto pin_param = parameters::get_parameter(
|
||||
np, Conformal_map::internal_np::fixed_vertex_map);
|
||||
constexpr bool has_pin = !std::is_same_v<
|
||||
decltype(pin_param), internal_np::Param_not_found>;
|
||||
|
||||
bool any_pinned = false;
|
||||
if constexpr (has_pin) {
|
||||
for (auto v : mesh.vertices())
|
||||
if (get(pin_param, v)) { maps.v_idx[v] = -1; any_pinned = true; }
|
||||
}
|
||||
if (!any_pinned) {
|
||||
auto it = mesh.vertices().begin();
|
||||
if (it != mesh.vertices().end()) { maps.v_idx[*it] = -1; any_pinned = true; }
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (auto v : mesh.vertices())
|
||||
if (maps.v_idx[v] != -1) maps.v_idx[v] = idx++;
|
||||
|
||||
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-theta default for the spherical functional.
|
||||
std::vector<double> x0(static_cast<std::size_t>(idx), 0.0);
|
||||
if constexpr (!has_theta) {
|
||||
auto G0 = ::conformallab::spherical_gradient(mesh, x0, maps);
|
||||
for (auto v : mesh.vertices()) {
|
||||
const int j = maps.v_idx[v];
|
||||
if (j >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(j)];
|
||||
}
|
||||
}
|
||||
|
||||
auto nr = ::conformallab::newton_spherical(mesh, x0, maps, tol, max_iter);
|
||||
|
||||
result.u_per_vertex.assign(num_vertices(mesh), FT(0));
|
||||
for (auto v : mesh.vertices()) {
|
||||
const int j = maps.v_idx[v];
|
||||
if (j >= 0) result.u_per_vertex[v.idx()] = nr.x[static_cast<std::size_t>(j)];
|
||||
}
|
||||
result.iterations = nr.iterations;
|
||||
result.gradient_norm = nr.grad_inf_norm;
|
||||
result.converged = nr.converged;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// discrete_conformal_map_hyper_ideal — Phase 8b-Lite
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/*!
|
||||
\ingroup PkgConformalMapRef
|
||||
|
||||
Result of `discrete_conformal_map_hyper_ideal`. Carries both vertex
|
||||
DOFs `b_v` and edge DOFs `a_e` (hyper-ideal triangles in H³).
|
||||
*/
|
||||
template <typename FT = double>
|
||||
struct Hyper_ideal_map_result
|
||||
{
|
||||
/// Vertex DOFs `b_v` (length = num_vertices(mesh); pinned vertices = 0).
|
||||
std::vector<FT> b_per_vertex;
|
||||
/// Edge DOFs `a_e` (length = num_edges(mesh); pinned edges = 0).
|
||||
std::vector<FT> a_per_edge;
|
||||
|
||||
int iterations = 0;
|
||||
FT gradient_norm = FT(0);
|
||||
bool converged = false;
|
||||
};
|
||||
|
||||
/*!
|
||||
\ingroup PkgConformalMapRef
|
||||
|
||||
Compute the hyper-ideal discrete-conformal map of a triangle mesh
|
||||
(Springborn 2020 §4).
|
||||
|
||||
\note Phase 8b-Lite scope: vertex DOFs `b_v` are assigned automatically
|
||||
to all vertices; edge DOFs `a_e` are similarly assigned. The
|
||||
block-FD Hessian (Phase 9b) is used internally — see
|
||||
`newton_hyper_ideal` for the solver convention.
|
||||
*/
|
||||
template <typename TriangleMesh,
|
||||
typename CGAL_NP_TEMPLATE_PARAMETERS>
|
||||
auto discrete_conformal_map_hyper_ideal(
|
||||
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_conformal_map_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;
|
||||
|
||||
Hyper_ideal_map_result<FT> result;
|
||||
|
||||
auto maps = ::conformallab::setup_hyper_ideal_maps(mesh);
|
||||
// Hyper-ideal init does not derive from mesh geometry: the user's
|
||||
// Θ_v and θ_e are the model inputs. Defaults from setup are
|
||||
// Θ_v = 2π, θ_e = π (orthogonal).
|
||||
|
||||
auto theta_param = parameters::get_parameter(
|
||||
np, Conformal_map::internal_np::vertex_curvature_map);
|
||||
constexpr bool has_theta = !std::is_same_v<
|
||||
decltype(theta_param), internal_np::Param_not_found>;
|
||||
if constexpr (has_theta) {
|
||||
for (auto v : mesh.vertices())
|
||||
maps.theta_v[v] = get(theta_param, v);
|
||||
}
|
||||
|
||||
const int n = ::conformallab::assign_all_dof_indices(mesh, maps);
|
||||
|
||||
const FT tol = parameters::choose_parameter(
|
||||
parameters::get_parameter(np, Conformal_map::internal_np::gradient_tolerance),
|
||||
FT(1e-8));
|
||||
const int max_iter = parameters::choose_parameter(
|
||||
parameters::get_parameter(np, Conformal_map::internal_np::max_iterations),
|
||||
200);
|
||||
|
||||
// Initial point: b_v = 1.0 (positive log-scale), a_e = 0.5 (moderate).
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
for (auto v : mesh.vertices()) {
|
||||
int i = maps.v_idx[v];
|
||||
if (i >= 0) x0[static_cast<std::size_t>(i)] = 1.0;
|
||||
}
|
||||
for (auto e : mesh.edges()) {
|
||||
int i = maps.e_idx[e];
|
||||
if (i >= 0) x0[static_cast<std::size_t>(i)] = 0.5;
|
||||
}
|
||||
|
||||
auto nr = ::conformallab::newton_hyper_ideal(mesh, x0, maps, tol, max_iter);
|
||||
|
||||
result.b_per_vertex.assign(num_vertices(mesh), FT(0));
|
||||
result.a_per_edge .assign(num_edges(mesh), FT(0));
|
||||
for (auto v : mesh.vertices()) {
|
||||
int j = maps.v_idx[v];
|
||||
if (j >= 0) result.b_per_vertex[v.idx()] = nr.x[static_cast<std::size_t>(j)];
|
||||
}
|
||||
for (auto e : mesh.edges()) {
|
||||
int j = maps.e_idx[e];
|
||||
if (j >= 0) result.a_per_edge[e.idx()] = nr.x[static_cast<std::size_t>(j)];
|
||||
}
|
||||
result.iterations = nr.iterations;
|
||||
result.gradient_norm = nr.grad_inf_norm;
|
||||
result.converged = nr.converged;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace CGAL
|
||||
|
||||
#endif // CGAL_DISCRETE_CONFORMAL_MAP_H
|
||||
|
||||
Reference in New Issue
Block a user