# 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<Vertex_index, K::Point_2>("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 `<CGAL/Named_function_parameters.h>`).

---

## 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 <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Discrete_conformal_map.h>

using K        = CGAL::Simple_cartesian<double>;
using Mesh     = CGAL::Surface_mesh<K::Point_3>;
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<Vertex_i, K::Point_2>(
              "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.

### Chaining: use the pipe operator `|`

CGAL upstream supports chaining via `.member()` syntax for the standard
CGAL tags (e.g. `.geom_traits(...).vertex_point_map(...)`), but the
mechanism requires modifying CGAL's `parameters_interface.h` to register
new member functions on `Named_function_parameters` — which we
deliberately treat as a read-only vendored dependency.

Instead, conformallab++ provides a **pipe-operator overload** in
`namespace CGAL` (so ADL finds it) that achieves the same effect by
left-to-right composition:

```cpp
auto params = CGAL::parameters::gradient_tolerance(1e-12)
            | CGAL::parameters::max_iterations(500)
            | CGAL::parameters::output_uv_map(uv);
CGAL::discrete_conformal_map_euclidean(mesh, params);
```

Read as: "first apply gradient_tolerance, then max_iterations, then
output_uv_map".  The result is a `Named_function_parameters` chain
indistinguishable from what `.gradient_tolerance(...).max_iterations(...)
.output_uv_map(...)` would have produced, so every entry function in the
package accepts it as-is.

Implementation: see the `operator|` in
`code/include/CGAL/Conformal_map/internal/parameters.h`.
Tests: `CGALPhase8bLite.NamedParamPipe_MultipleParamsTakeEffect` +
`NamedParamPipe_TwoParams`.

The `.member()` chaining of CGAL standard tags continues to work
unchanged — `|` is added alongside, not replacing.

---

## 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 <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      // 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 <typename PropertyMap>
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<Face_index,
                   std::pair<K::Point_2, double>>("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).
- `<CGAL/Named_function_parameters.h>` — upstream machinery.
