Some checks failed
C++ Tests / test-fast (push) Successful in 1m58s
C++ Tests / test-fast (pull_request) Successful in 2m33s
API Docs / doc-build (pull_request) Successful in 51s
C++ Tests / test-cgal (push) Has been skipped
C++ Tests / test-cgal (pull_request) Failing after 10m32s
Adds `operator|` in `namespace CGAL` so package-local named parameters
can be combined left-to-right without modifying CGAL upstream:
auto p = CGAL::parameters::gradient_tolerance(1e-12)
| CGAL::parameters::max_iterations(500)
| CGAL::parameters::output_uv_map(uv);
CGAL::discrete_conformal_map_euclidean(mesh, p);
Why not the canonical `.a().b().c()` syntax
───────────────────────────────────────────
CGAL's standard chaining mechanism requires registering each named
parameter as a member function on `Named_function_parameters` via the
`CGAL_add_named_parameter` macro in
`CGAL/STL_Extension/internal/parameters_interface.h` — a vendored
upstream file that conformallab++ deliberately treats as read-only.
Adding member-function chainers for our package-local tags would
require either forking CGAL or modifying the vendored copy. Neither
is acceptable for a library that wants to remain portable across
future CGAL releases.
The pipe-operator achieves the same compositional semantics via a
free function in `namespace CGAL` (so ADL finds it for
`Named_function_parameters` operands). Implementation: rebuild the
right-hand-side `Named_function_parameters` with the left-hand-side
as its `Base`, producing an indistinguishable chain that every entry
function accepts unchanged.
Implementation: `code/include/CGAL/Conformal_map/internal/parameters.h`
lines 158-187. The operator is constrained to right-hand-sides with
`No_property` base (i.e. fresh single-parameter packs from the helper
functions), so it never collides with any future CGAL operator on the
same type.
Tests (2 new, total Phase-8b-Lite suite 15 → 17)
────────────────────────────────────────────────
* CGALPhase8bLite.NamedParamPipe_MultipleParamsTakeEffect
Chain three parameters; verify all three take effect (tight
tolerance respected + UV pmap populated + iteration cap honoured).
* CGALPhase8bLite.NamedParamPipe_TwoParams
Chain two parameters; verify max_iterations(0) blocks the loop
even when combined with another param.
Full CGAL suite: 234/234 PASSED, 0 SKIPPED (was 232).
Total: 257/257 PASSED, 0 SKIPPED (was 255).
scripts/check-test-counts.sh: OK.
Documentation updates
─────────────────────
* doc/tutorials/add-output-uv-map.md §3.4: "Current limitation: no
chaining" → "Chaining: use the pipe operator `|`". Explains why
CGAL's `.member()` syntax isn't available and shows the `|`
workaround with a working code example.
* doc/architecture/locked-vs-flexible.md §8: chaining now flagged as
shipped via pipe; recommended posture says `.member()` chaining
only if a user pushes for the CGAL-canonical syntax.
* doc/roadmap/porting-status.md §5: API limitations table updated.
* doc/api/tests.md: CGALPhase8bLite row 15 → 17, total 232 → 234.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
488 lines
17 KiB
Markdown
488 lines
17 KiB
Markdown
# 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.
|