# Tutorial: The `output_uv_map` Named Parameter This tutorial documents the `output_uv_map` named parameter for the CGAL-style entry functions of conformallab++: how to use it (the "happy path"), how the CGAL named-parameter machinery threads a user-supplied property map through to the layout step, and how to extend the same pattern to add new named parameters to existing or new entry functions. **Target audience.** A working mathematician using the C++ API who is comfortable with templates and CGAL property maps but wants a single authoritative reference for the pattern. **Prerequisite reading.** - `code/include/CGAL/Conformal_map/internal/parameters.h` — tag definitions - `code/include/CGAL/Discrete_conformal_map.h` — wiring in three entries - `code/tests/cgal/test_cgal_phase8b_lite.cpp` — the OutputUvMap_* tests - `doc/tutorials/add-inversive-distance.md` — sibling tutorial in this series --- ## 1. What this tutorial is and isn't ### The UX problem `output_uv_map` solves Before Phase 8b-Lite, computing a flattening required **two separate calls**: the Newton wrapper, then a manual rebuild of `*_maps` plus a replay of the wrapper's gauge/DOF choices, then `*_layout()`. Any drift between the wrapper's gauge vertex and the caller's silently produced inconsistent UVs. The `output_uv_map` named parameter collapses this into one call: ```cpp auto uv = mesh.add_property_map("uv").first; CGAL::discrete_conformal_map_euclidean( mesh, CGAL::parameters::output_uv_map(uv)); // uv[v] now holds the flattened UV coordinate of vertex v. ``` This tutorial is **not** a derivation of the Newton solver (see `add-inversive-distance.md`), not a guide to writing a new layout routine, and not an introduction to CGAL named parameters in general (see ``). --- ## 2. Mathematical background The package implements **two mathematically distinct stages**: ### Stage A — Newton solver (variational optimisation) For the Euclidean DCE functional (Springborn–Schmies–Bobenko 2008), the solver finds the conformal scale factor vector `u ∈ ℝⁿ` that satisfies ``` G(u)_v := Θ_v − Σ_{T ∋ v} α_v(T; u) = 0 ∀ v ∈ V \ {pinned} ``` i.e. the actual cone angles match the target curvature `Θ_v` at every non-pinned vertex. The gradient `G` is the variational derivative of the convex energy ``` E(u) = ∫_0^1 ⟨ G(t u), u ⟩ dt (path integral on the closed Yamabe 1-form) ``` The output is **purely combinatorial-metric**: the converged `u_v` determines an intrinsic flat metric `ℓ_ij(u) = exp(½(u_i + u_j)) · ℓ_ij^(0)` on the abstract triangulation. **No embedding yet exists.** ### Stage B — Layout (geometric realisation) The layout step is a separate **trilateration** turning intrinsic edge lengths into a `Point_2` (or `Point_3`) per vertex: 1. Pick a seed triangle `T₀`; place its three vertices canonically. 2. BFS over the dual graph from `T₀`. Each newly visited triangle has two vertices already placed; the third is the unique solution of two distance constraints (intersection of two circles). 3. The BFS uses a **priority queue keyed on BFS depth** (`euclidean_layout` in `code/include/layout.hpp`) to avoid the numerical drift of pure DFS on long thin meshes. Spherical / hyperbolic variants substitute spherical-cap or Poincaré- disk trilateration respectively. Stage A is convex optimisation in ℝⁿ; Stage B is deterministic realisation with no degrees of freedom. The named parameter chains them. --- ## 3. The user-facing API ### Happy path ```cpp #include #include #include using K = CGAL::Simple_cartesian; using Mesh = CGAL::Surface_mesh; using Vertex_i = Mesh::Vertex_index; Mesh mesh = /* load triangle mesh */; // 1. Allocate a writable property map on the mesh. auto uv = mesh.add_property_map( "v:uv", K::Point_2(0, 0)).first; // 2. Ask the wrapper to populate it. auto res = CGAL::discrete_conformal_map_euclidean( mesh, CGAL::parameters::output_uv_map(uv)); // 3. Use the UVs. if (res.converged) { for (auto v : mesh.vertices()) std::cout << v.idx() << ": " << uv[v] << '\n'; } ``` ### Supported entries | Entry | Value type per vertex | |--------------------------------------|--------------------------------------------------| | `discrete_conformal_map_euclidean` | `Point_2` — UV in ℝ² | | `discrete_conformal_map_spherical` | `Point_3` — point on S² ⊂ ℝ³ | | `discrete_conformal_map_hyper_ideal` | `Point_2` — point in the Poincaré disk (|p|≤1) | ### NOT supported (yet) | Entry | Reason | |----------------------------------------|-------------------------------------------------------------| | `discrete_circle_packing_euclidean` | Face-based parametrisation — UV is per-face, not per-vertex | | `discrete_inversive_distance_map` | Requires `inversive_distance_layout()` using Luo's `ℓ²` formula — not yet written | For the circle-packing entry a separate `output_circle_map` parameter (face → (centre, radius)) is the natural extension; for the inversive- distance entry the layout primitive must be written first. See §7. ### Optional companion: `normalise_layout` ```cpp CGAL::parameters::normalise_layout(true) ``` When the layout writes into `output_uv_map`, this flag triggers a canonical post-processing pass: - Euclidean: PCA — translate centroid to origin, rotate so the principal axis is along `x`. - Spherical: Rodrigues rotation so that the centroid is at the north pole. - Hyperbolic: Möbius centring of the Poincaré disk. Default: `false`. Has no effect unless `output_uv_map` is also passed. ### Current limitation: no chaining The CGAL machinery supports chaining named parameters via `.member()` syntax for the standard CGAL tags (e.g. `geom_traits`, `vertex_point_map`). **Package-local tags do not yet have member-function counterparts**, so ```cpp // DOES NOT COMPILE: CGAL::parameters::output_uv_map(uv).normalise_layout(true); ``` is currently rejected. Pass one parameter per call instead. This is the same limitation noted in `test_cgal_phase8b_lite.cpp`: ```cpp // Named-parameter chaining (`a.b().c()`) is not currently supported on // the package-local tags; pass one parameter per call instead. ``` Adding member-function chaining is a separate Phase 8b extension — requires writing a derived `Named_function_parameters` subclass that exposes `.output_uv_map(...)`, `.normalise_layout(...)`, etc. --- ## 4. Named-parameter mechanism — how it works CGAL's named parameters are a compile-time dispatch trick. Three pieces: ### 4.1 The tag (in `internal_np`) ```cpp namespace CGAL::Conformal_map::internal_np { enum output_uv_map_t { output_uv_map }; } ``` A single-value `enum` is the lightest possible type-level marker. The value `output_uv_map` is what callers will pass as the *key*; the type `output_uv_map_t` is what `get_parameter` will use for the lookup. ### 4.2 The user-facing helper (in `CGAL::parameters`) ```cpp template 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 // no "next" parameter in chain >(pmap); } ``` This wraps the property map in a `Named_function_parameters` object tagged with `output_uv_map_t`. The third template argument is the "previous" link in a chain; `No_property` means this is a singleton pack. (Chaining would substitute the previous pack's type here.) ### 4.3 The internal lookup (inside the entry function) The full read-and-act pattern from `Discrete_conformal_map.h`: ```cpp // Step 1 — try to extract the parameter. auto uv_param = parameters::get_parameter( np, Conformal_map::internal_np::output_uv_map); // Step 2 — at compile time, did we find one? constexpr bool has_uv = !std::is_same_v< decltype(uv_param), internal_np::Param_not_found>; // Step 3 — conditionally do the extra work. 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())); } } } ``` Key points: - `get_parameter` returns either the wrapped value or a sentinel `Param_not_found`. The check is `std::is_same_v<…, Param_not_found>`. - `choose_parameter(get_parameter(np, tag), default_value)` is the one-liner form for scalar parameters with a default. - `constexpr if` ensures that if the user did not pass `output_uv_map`, the layout call is **not even instantiated** — no runtime cost, and no need for the layout routine to be callable on the wrapper's signature (e.g. it can require a Point_2 traits type that the user hasn't supplied). - The layout step is gated on `nr.converged` for a reason: trilateration on a non-converged `u` produces garbage edge lengths and can throw on degenerate triangles. The current contract is **layout iff Newton converged**; on non-convergence the pmap is left at the value the caller initialised it with. The same three-step pattern appears in `discrete_conformal_map_spherical` (writing `Point_3`) and `discrete_conformal_map_hyper_ideal` (writing `Point_2` in the Poincaré disk). --- ## 5. How to extend the pattern Recipe for adding a new package-local named parameter — running example `output_holonomy_map` (a per-edge map carrying the Möbius/translation holonomy data that some downstream algorithms consume). ### Step 1 — Add the tag in `internal/parameters.h` ```cpp namespace CGAL::Conformal_map::internal_np { /// Property-map: edge_descriptor → 2x2 matrix of holonomy data. /// Default: not populated. enum output_holonomy_map_t { output_holonomy_map }; } ``` Two-line block. Use a meaningful Doxygen `\internal` comment because this is the canonical contract for the parameter's semantics. ### Step 2 — Add the user-facing helper in `parameters.h` ```cpp namespace CGAL::parameters { /// `output_holonomy_map(pmap)` — write per-edge holonomy matrices /// into `pmap`. Only meaningful for closed surfaces with non-trivial /// π₁; ignored on simply-connected meshes. template auto output_holonomy_map(const PropertyMap& pmap) { return CGAL::Named_function_parameters< PropertyMap, Conformal_map::internal_np::output_holonomy_map_t, CGAL::internal_np::No_property >(pmap); } } // namespace CGAL::parameters ``` A Doxygen comment is **mandatory** here — this is the public surface. ### Step 3 — Use `get_parameter + constexpr if` in the entry function ```cpp auto hol_param = parameters::get_parameter( np, Conformal_map::internal_np::output_holonomy_map); constexpr bool has_hol = !std::is_same_v< decltype(hol_param), internal_np::Param_not_found>; if constexpr (has_hol) { auto H = ::conformallab::compute_holonomy(mesh, nr.x, maps); for (auto e : mesh.edges()) put(hol_param, e, H[e.idx()]); } ``` ### Step 4 — Add tests At minimum two: 1. **Takes-effect test.** Pass the parameter; verify the side effect actually happens (pmap is populated, values are non-default). 2. **Sanity-when-absent test.** Call the wrapper without the parameter; verify the call still succeeds and produces the expected base result. Both patterns are exemplified in `test_cgal_phase8b_lite.cpp` (next section). ### Optional Step 5 — Companion flag If your new parameter has a Boolean tweak (analogous to `normalise_layout`), follow exactly the same recipe — `enum X_t { X }` + helper + `choose_parameter(..., default)` at the read site. --- ## 6. Tests — walkthrough of `test_cgal_phase8b_lite.cpp` The OutputUvMap_* tests live at the bottom of the file. Each codifies one acceptance criterion. ### 6.1 `OutputUvMap_Euclidean_PopulatesPmap` Two assertions: (a) every UV is finite, (b) at least one vertex has moved off the origin. Test (b) rules out the false-positive where the layout silently returned the pmap's default at every vertex. ### 6.2 `OutputUvMap_Spherical_PopulatesXyz` Geometric round-trip: every output point must lie on the unit sphere. ```cpp const double r = std::sqrt(p.x()*p.x() + p.y()*p.y() + p.z()*p.z()); EXPECT_NEAR(r, 1.0, 1e-6); ``` The spherical analogue of 6.1(b): the layout is correct iff the realisation lands on S². ### 6.3 `OutputUvMap_HyperIdeal_PointsInPoincareDisk` The natural Θ/θ targets do not always reach Newton equilibrium inside the default `max_iterations(200)`. The test branches: ```cpp if (res.converged) { for (auto v : mesh.vertices()) { const double r2 = p.x()*p.x() + p.y()*p.y(); EXPECT_LE(r2, 1.0 + 1e-6); } } // else: layout was skipped by the wrapper's `if (nr.converged)` guard. ``` Documenting the wrapper contract ("layout iff converged") via the test keeps it resilient to PRNG-sensitive Newton paths. ### 6.4 `OutputUvMap_Absent_DoesNotRunLayout` Sanity test: omitting `output_uv_map` must not change the existing behaviour. Catches the regression where adding the new `constexpr if` perturbs the wrapper's result type or consumes extra work. ### 6.5 `OutputUvMap_NormaliseLayout_TakesEffect` Because chaining is not yet implemented (§3), this exercises the toggle indirectly: it runs the wrapper twice into two separate pmaps and verifies both populate finite values. When chaining lands, upgrade this to actually pass `normalise_layout(true)` on the second call and compare UV bounding boxes (the normalised one must be axis-aligned and centroid-zero). --- ## 7. Adding output to the two missing modes ### 7.1 CP-Euclidean (face-based) — proposed `output_circle_map` The circle-packing entry parametrises **faces**: each face `f` has a radius `ρ_f`. The natural per-face output is `(centre, radius)`: ```cpp auto circles = mesh.add_property_map>("f:circle", ...).first; CGAL::discrete_circle_packing_euclidean( mesh, CGAL::parameters::output_circle_map(circles)); ``` Effort: **1–2 days**. The face-graph BFS-packing is Stephenson's `circlepack` algorithm; radii come from Newton, new work is the geometric loop plus the §5 plumbing. ### 7.2 Inversive-Distance — needs new layout routine first The inversive-distance functional yields edge lengths ``` ℓ_ij(u)² = r_i² + r_j² + 2 I_ij r_i r_j (Luo 2004 §3) ``` with `r_i = exp(u_i)`. Required work: write `inversive_distance_layout()` in `code/include/layout.hpp` mirroring `euclidean_layout()` but consuming the above `ℓ_ij(u)` instead of `exp(½(u_i + u_j)) · ℓ_ij^(0)`. Then wire the parameter through `discrete_inversive_distance_map` exactly as §4.3 shows. Effort: **~3 days**. Status: **TBD**. --- ## 8. Acceptance checklist for a new named parameter Use this checklist when extending the package: - [ ] **Tag declared** in `code/include/CGAL/Conformal_map/internal/parameters.h` (`enum X_t { X };`) with a `\internal` Doxygen comment specifying the property-map key/value types and default behaviour. - [ ] **User-facing helper** in the same file's `CGAL::parameters` namespace, with a public Doxygen comment. - [ ] **`get_parameter + constexpr if` pattern** in every entry function that supports the parameter (do not leak the tag into entries that ignore it). - [ ] **At least two tests** in `test_cgal_phase8b_lite.cpp` (or a new test file if introducing a new functional): a *parameter-takes-effect* test and a *sanity-when-absent* test. - [ ] **Limitation documented.** If the parameter is mode-specific (e.g. only meaningful for closed meshes) or cannot be chained with existing helpers, say so in the user-facing Doxygen and in a comment near the read site. - [ ] **No silent regressions.** If the parameter changes the wrapper's observable behaviour even when absent (e.g. via a new default), add a regression test pinning the old default. --- ## 9. Further reading - Springborn, Schmies, Bobenko (2008). *Conformal equivalence of triangle meshes.* SIGGRAPH. — Euclidean DCE functional. - Bobenko, Pinkall, Springborn (2015). *Discrete conformal maps and ideal hyperbolic polyhedra.* Geom. Topol. — hyper-ideal variant. - Glickenstein (2011). *Discrete conformal variations and scalar curvature on piecewise flat manifolds.* JDG 87(2). - `` — upstream machinery.