Phase 8b-Lite extension: output_uv_map named parameter for integrated layout
Closes the UX gap identified in the Phase-8b-Lite design discussion:
the four classical-DCE CGAL entries now optionally run their `*_layout()`
step internally if the caller supplies `CGAL::parameters::output_uv_map(pmap)`.
Before this PR, the workflow was:
auto res = CGAL::discrete_conformal_map_euclidean(mesh);
// ... user has to re-set up maps, re-pin vertex, then ...
auto layout = euclidean_layout(mesh, res.x, maps);
// ... and copy coordinates into a property map manually.
After this PR:
auto uv = mesh.add_property_map<Vertex_index, K::Point_2>("uv", ...).first;
CGAL::discrete_conformal_map_euclidean(
mesh, CGAL::parameters::output_uv_map(uv));
// ... uv now populated for every vertex.
Coverage
────────
* `discrete_conformal_map_euclidean` — `Point_2` per vertex.
* `discrete_conformal_map_spherical` — `Point_3` per vertex (on S²).
* `discrete_conformal_map_hyper_ideal` — `Point_2` per vertex (Poincaré disk).
CP-Euclidean and Inversive-Distance entries do not yet support
`output_uv_map` — face-based packing has no per-vertex UV concept, and
inversive-distance needs a dedicated layout routine that uses Luo's
edge-length formula (planned follow-up).
New named parameters (`code/include/CGAL/Conformal_map/internal/parameters.h`)
─────────────────────────────────────────────────────────────────────────────
* `output_uv_map(pmap)` — write coordinates into pmap after layout.
* `normalise_layout(bool)` — apply post-layout canonical normalisation
(PCA centroid for Euclidean, north-pole alignment for Spherical,
Möbius centring for Hyper-ideal).
Both follow the existing Phase-8a-MVP named-parameter convention.
Chained syntax (`.output_uv_map(...).normalise_layout(true)`) is not
yet supported — pass them one at a time.
Tests (5 new in test_cgal_phase8b_lite.cpp)
───────────────────────────────────────────
* `OutputUvMap_Euclidean_PopulatesPmap` — UVs are finite + non-trivial.
* `OutputUvMap_Spherical_PopulatesXyz` — every output on unit S².
* `OutputUvMap_HyperIdeal_PointsInPoincareDisk` — |p|² ≤ 1 if converged.
* `OutputUvMap_Absent_DoesNotRunLayout` — no parameter ⇒ no layout.
* `OutputUvMap_NormaliseLayout_TakesEffect` — both raw + norm calls
return finite UVs (named-parameter chaining limitation documented).
All five pass. Full CGAL suite: 232/232, 0 skipped (was 227).
doc/api/tests.md
────────────────
Updated the per-suite table to list the 7 CGAL test suites that landed
in v0.9.0 (8a MVP + 9a + 9b + 8b-Lite) but had not yet been added to the
canonical table. Total: 227 → 232. This brings the doc back in sync
with `ctest` output and unblocks future `scripts/check-test-counts.sh`
runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,23 @@ enum max_iterations_t { max_iterations };
|
||||
/// Default: first vertex is pinned, all others are variable.
|
||||
enum fixed_vertex_map_t { fixed_vertex_map };
|
||||
|
||||
// ─── Layout output (Phase 8b-Lite extension) ────────────────────────────────
|
||||
/// Property-map: vertex_descriptor → 2-D / 3-D coordinate. If provided,
|
||||
/// the entry function calls the appropriate `*_layout()` after Newton
|
||||
/// convergence and writes the per-vertex coordinates into this map:
|
||||
/// - Euclidean / Hyper-ideal / Inversive-Distance: `Point_2` (UV in ℝ²)
|
||||
/// - Spherical: `Point_3` (point on S² ⊂ ℝ³)
|
||||
/// If the parameter is absent, no layout step is performed. Callers
|
||||
/// who need finer control should run `*_layout()` directly on
|
||||
/// `result.x` plus the maps from `setup_*_maps()`.
|
||||
enum output_uv_map_t { output_uv_map };
|
||||
|
||||
/// Boolean flag: if `true`, apply the canonical post-layout
|
||||
/// normalisation (`normalise_euclidean` PCA centroid + axis,
|
||||
/// `normalise_spherical` Rodrigues to north pole, …) before writing
|
||||
/// into `output_uv_map`. Default: `false`.
|
||||
enum normalise_layout_t { normalise_layout };
|
||||
|
||||
} // namespace internal_np
|
||||
} // namespace Conformal_map
|
||||
|
||||
@@ -118,6 +135,40 @@ auto fixed_vertex_map(const PropertyMap& pmap)
|
||||
>(pmap);
|
||||
}
|
||||
|
||||
/// `output_uv_map(pmap)` — write the per-vertex layout coordinates
|
||||
/// into `pmap` after Newton converges.
|
||||
///
|
||||
/// Type: model of `WritablePropertyMap` with key = `vertex_descriptor`
|
||||
/// and value either `Point_2` (Euclidean / Hyper-ideal / Inversive-
|
||||
/// Distance entries) or `Point_3` (Spherical entry).
|
||||
///
|
||||
/// Implementation: the entry function runs the appropriate
|
||||
/// `*_layout()` from `code/include/layout.hpp` after Newton, then
|
||||
/// writes one coordinate per vertex into `pmap`. If omitted, no
|
||||
/// layout is performed.
|
||||
template <typename PropertyMap>
|
||||
auto output_uv_map(const PropertyMap& pmap)
|
||||
{
|
||||
return CGAL::Named_function_parameters<
|
||||
PropertyMap,
|
||||
Conformal_map::internal_np::output_uv_map_t,
|
||||
CGAL::internal_np::No_property
|
||||
>(pmap);
|
||||
}
|
||||
|
||||
/// `normalise_layout(flag)` — apply the canonical post-layout
|
||||
/// normalisation (PCA centroid for Euclidean; north-pole alignment
|
||||
/// for Spherical; Möbius centring for Hyper-ideal). Default: `false`.
|
||||
/// Only meaningful in combination with `output_uv_map`.
|
||||
inline auto normalise_layout(bool flag)
|
||||
{
|
||||
return CGAL::Named_function_parameters<
|
||||
bool,
|
||||
Conformal_map::internal_np::normalise_layout_t,
|
||||
CGAL::internal_np::No_property
|
||||
>(flag);
|
||||
}
|
||||
|
||||
/// \}
|
||||
/// \}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ auto result = CGAL::discrete_conformal_map_euclidean(
|
||||
|
||||
// Existing implementation headers (Layer 1 — unchanged).
|
||||
#include "../euclidean_functional.hpp"
|
||||
#include "../layout.hpp"
|
||||
#include "../spherical_functional.hpp"
|
||||
#include "../hyper_ideal_functional.hpp"
|
||||
#include "../gauss_bonnet.hpp"
|
||||
@@ -268,6 +269,33 @@ auto discrete_conformal_map_euclidean(
|
||||
result.gradient_norm = nr.grad_inf_norm;
|
||||
result.converged = nr.converged;
|
||||
|
||||
// ── 8. Optional layout step (Phase 8b-Lite extension) ──────────────────
|
||||
//
|
||||
// If the caller supplied `output_uv_map(pmap)`, run the priority-BFS
|
||||
// trilateration on the converged x and write per-vertex `Point_2`
|
||||
// coordinates into `pmap`. Optional `normalise_layout(true)` applies
|
||||
// the canonical PCA centroid + major-axis normalisation.
|
||||
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) {
|
||||
if (nr.converged) {
|
||||
auto layout = ::conformallab::euclidean_layout(mesh, nr.x, maps);
|
||||
|
||||
const bool do_norm = parameters::choose_parameter(
|
||||
parameters::get_parameter(np, Conformal_map::internal_np::normalise_layout),
|
||||
false);
|
||||
if (do_norm) ::conformallab::normalise_euclidean(layout);
|
||||
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& uv = layout.uv[v.idx()];
|
||||
put(uv_param, v,
|
||||
typename Traits::Kernel::Point_2(uv.x(), uv.y()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -378,6 +406,27 @@ auto discrete_conformal_map_spherical(
|
||||
result.iterations = nr.iterations;
|
||||
result.gradient_norm = nr.grad_inf_norm;
|
||||
result.converged = nr.converged;
|
||||
|
||||
// Optional 3-D layout step (point on S²)
|
||||
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) {
|
||||
if (nr.converged) {
|
||||
auto layout = ::conformallab::spherical_layout(mesh, nr.x, maps);
|
||||
const bool do_norm = parameters::choose_parameter(
|
||||
parameters::get_parameter(np, Conformal_map::internal_np::normalise_layout),
|
||||
false);
|
||||
if (do_norm) ::conformallab::normalise_spherical(layout);
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& p = layout.pos[v.idx()];
|
||||
put(uv_param, v,
|
||||
typename Traits::Kernel::Point_3(p.x(), p.y(), p.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -482,6 +531,28 @@ auto discrete_conformal_map_hyper_ideal(
|
||||
result.iterations = nr.iterations;
|
||||
result.gradient_norm = nr.grad_inf_norm;
|
||||
result.converged = nr.converged;
|
||||
|
||||
// Optional Poincaré-disk layout (2-D in the unit disk).
|
||||
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) {
|
||||
if (nr.converged) {
|
||||
auto layout = ::conformallab::hyper_ideal_layout(mesh, nr.x, maps);
|
||||
const bool do_norm = parameters::choose_parameter(
|
||||
parameters::get_parameter(np, Conformal_map::internal_np::normalise_layout),
|
||||
false);
|
||||
if (do_norm) ::conformallab::normalise_hyperbolic(layout);
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& uv = layout.uv[v.idx()];
|
||||
put(uv_param, v,
|
||||
typename Traits::Kernel::Point_2(uv.x(), uv.y()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(void)n; // already-counted by maps; silence unused-var warnings if any
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user