Three reviewer-meeting deliverables in one commit.
(1) output_uv_map for the two remaining DCE entries
─────────────────────────────────────────────────
* Discrete_inversive_distance.h: full implementation. After Newton,
reconstruct effective Euclidean edge lengths from the converged
log-radii via the Bowers-Stephenson identity
`ℓᵢⱼ² = rᵢ² + rⱼ² + 2·Iᵢⱼ·rᵢ·rⱼ`, populate a temporary
EuclideanMaps with `lambda0 = log(ℓᵢⱼ²)`, and reuse the existing
`euclidean_layout(mesh, 0, eucl)` priority-BFS. Per-vertex
Point_2 coordinates written into the user-supplied pmap.
Optional `normalise_layout(true)` applies the canonical PCA
centroid + major-axis rotation, same as the other 3 entries.
* Discrete_circle_packing.h: throws std::runtime_error with a
clear pointer to Phase 9c rather than silently producing
nonsense. CP-Euclidean is face-based; the faithful output is a
per-face circle packing in ℝ², not a per-vertex Point_2 map.
A true layout requires BPS-2010 §6 (~150 lines, on the porting
roadmap as Phase 9c). Failing loudly is the honest default.
Tests: 2 new cases in test_cgal_phase8b_lite.cpp
(OutputUvMap_InversiveDistance_PopulatesPmap;
OutputUvMap_CPEuclidean_ThrowsClearly). Both green.
Suite total now 259 (was 257, +2). CGAL subtotal: 234 → 236.
(2) Reviewer meeting documents
──────────────────────────
New directory doc/reviewer/ with three files:
* briefing.md — one-page orientation for the reviewer.
What the project is, where to look first
(https://tmoussa.codeberg.page/ConformalLabpp/), the headline
evidence (tests/coverage/sanitizers/license), what we want from
them, what's deferred and why, and the 5 questions in a separate
file.
* questions.md — the 5 concrete decisions we want their second
opinion on:
Q1 Phase 9c (port-literal vs re-derive)
Q2 Phase 9b-analytic (worth ~2 weeks for ~6× speedup?)
Q3 CP-Euclidean output_uv_map (build now or defer?)
Q4 CGAL submission strategy (one package or five?)
Q5 geometry-central cross-validation co-authorship
Plus an explicit "what would you say no to?" question at the
bottom — negative feedback is the highest-value information.
* agenda.md — my own internal playbook (NOT to be sent).
60-min flow: 5-min thank-you, 10-min architecture tour,
30-min for Q1-Q5 in the order Q4-Q1-Q2-Q5-Q3, 5-min "no"
question, 5-min wrap-up. Includes post-meeting memo template
to fill out in the 30 min after.
* README.md — index for the directory; says which file goes
to whom and when to send.
(3) locked-vs-flexible.md known-limitations update
─────────────────────────────────────────────
"output_uv_map covers 3 of 5 entries" → "covers 4 of 5".
CP-Euclidean's throws-clearly behaviour documented as a Phase 9c
deliverable rather than a passive gap.
Bonus: extended .codespellrc ignore list (acknowledgement, the
British-English spelling I used in agenda.md).
Verifications on this commit:
259/259 tests pass (0 skipped)
scripts/check-test-counts.sh: OK (23 + 236 = 259)
scripts/quality/license-headers.sh: OK (66/66 SPDX)
python3 scripts/quality/cgal-conventions.py: OK (0/6 violations)
scripts/quality/codespell.sh: OK (0 typos)
scripts/quality/shellcheck.sh: OK (0 findings)
python3 scripts/check-markdown-links.py: OK (143/143)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
430 lines
19 KiB
C++
430 lines
19 KiB
C++
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// test_cgal_phase8b_lite.cpp
|
||
//
|
||
// Phase 8b-Lite — Smoke tests for the four new CGAL-style entry functions
|
||
// added on top of the Phase 8a MVP (`discrete_conformal_map_euclidean`).
|
||
//
|
||
// All entries are thin wrappers around the legacy Newton solvers; the
|
||
// purpose of these tests is to verify:
|
||
// • the wrapper compiles + dispatches correctly
|
||
// • named parameters pass through (gradient_tolerance, max_iterations)
|
||
// • the returned Result struct contains the expected DOF vector
|
||
// • Newton convergence happens end-to-end via the public API
|
||
|
||
#include <CGAL/Discrete_conformal_map.h>
|
||
#include <CGAL/Discrete_circle_packing.h>
|
||
#include <CGAL/Discrete_inversive_distance.h>
|
||
#include <CGAL/Conformal_layout.h>
|
||
|
||
#include "mesh_builder.hpp"
|
||
#include "conformal_mesh.hpp"
|
||
|
||
#include <gtest/gtest.h>
|
||
#include <cmath>
|
||
|
||
using namespace conformallab;
|
||
|
||
namespace {
|
||
|
||
// Mesh helper — closed regular tetrahedron, used for spherical / hyper-ideal /
|
||
// circle-packing tests.
|
||
inline ConformalMesh make_closed_tet() { return make_tetrahedron(); }
|
||
|
||
// Open 3-face tetrahedron-minus-face, for layout testing.
|
||
inline ConformalMesh make_open_3face()
|
||
{
|
||
ConformalMesh mesh;
|
||
auto v0 = mesh.add_vertex(Point3( 1, 1, 1));
|
||
auto v1 = mesh.add_vertex(Point3( 1, -1, -1));
|
||
auto v2 = mesh.add_vertex(Point3(-1, 1, -1));
|
||
auto v3 = mesh.add_vertex(Point3(-1, -1, 1));
|
||
mesh.add_face(v0, v2, v1);
|
||
mesh.add_face(v0, v1, v3);
|
||
mesh.add_face(v0, v3, v2);
|
||
return mesh;
|
||
}
|
||
|
||
} // anonymous
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 1. Spherical entry — closed genus-0 tetrahedron, natural-theta default
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, Spherical_ClosedTetrahedron_NaturalThetaConverges)
|
||
{
|
||
auto mesh = make_closed_tet();
|
||
auto res = CGAL::discrete_conformal_map_spherical(mesh);
|
||
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_LT(res.gradient_norm, 1e-8);
|
||
EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh));
|
||
// Natural-theta ⇒ u = 0 is the equilibrium ⇒ all values ≈ 0.
|
||
for (double u : res.u_per_vertex) EXPECT_NEAR(u, 0.0, 1e-8);
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, Spherical_NamedParametersTakeEffect)
|
||
{
|
||
auto mesh = make_closed_tet();
|
||
auto res = CGAL::discrete_conformal_map_spherical(
|
||
mesh,
|
||
CGAL::parameters::max_iterations(0));
|
||
EXPECT_EQ(res.iterations, 0);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 2. Hyper-ideal entry — wrapper compiles + runs, returns both b_v and a_e
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, HyperIdeal_Tetrahedron_ReturnsBothVertexAndEdgeDOFs)
|
||
{
|
||
auto mesh = make_closed_tet();
|
||
auto res = CGAL::discrete_conformal_map_hyper_ideal(
|
||
mesh,
|
||
CGAL::parameters::max_iterations(20));
|
||
|
||
// Newton on default targets (Θ=2π, θ=π) from the "natural" b=1, a=0.5
|
||
// start may or may not converge in 20 iterations — but the wrapper must
|
||
// populate the result struct in any case.
|
||
EXPECT_EQ(res.b_per_vertex.size(), num_vertices(mesh));
|
||
EXPECT_EQ(res.a_per_edge.size(), num_edges (mesh));
|
||
EXPECT_GE(res.iterations, 0);
|
||
EXPECT_TRUE(std::isfinite(res.gradient_norm));
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 3. Circle-packing (face-based) entry — natural-phi convergence
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, CirclePacking_ClosedTetrahedron_NaturalPhiConverges)
|
||
{
|
||
auto mesh = make_closed_tet();
|
||
auto res = CGAL::discrete_circle_packing_euclidean(mesh);
|
||
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_LT(res.gradient_norm, 1e-8);
|
||
EXPECT_EQ(res.rho_per_face.size(), num_faces(mesh));
|
||
// Pinned face is at index 0 (first iterated face); its ρ is 0 by gauge.
|
||
// After natural-phi the equilibrium is ρ_f = 0 for every face.
|
||
for (double r : res.rho_per_face) EXPECT_NEAR(r, 0.0, 1e-8);
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, CirclePacking_GradientToleranceTakesEffect)
|
||
{
|
||
auto mesh = make_closed_tet();
|
||
auto res_loose = CGAL::discrete_circle_packing_euclidean(
|
||
mesh,
|
||
CGAL::parameters::gradient_tolerance(1e-4));
|
||
EXPECT_TRUE(res_loose.converged);
|
||
|
||
auto mesh2 = make_closed_tet();
|
||
auto res_strict = CGAL::discrete_circle_packing_euclidean(
|
||
mesh2,
|
||
CGAL::parameters::gradient_tolerance(1e-12));
|
||
EXPECT_TRUE(res_strict.converged);
|
||
EXPECT_LT(res_strict.gradient_norm, 1e-10);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 4. Inversive-distance (vertex-based) entry — natural-theta convergence
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, InversiveDistance_Triangle_NaturalThetaConverges)
|
||
{
|
||
auto mesh = make_triangle();
|
||
auto res = CGAL::discrete_inversive_distance_map(mesh);
|
||
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_LT(res.gradient_norm, 1e-8);
|
||
EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh));
|
||
for (double u : res.u_per_vertex) EXPECT_NEAR(u, 0.0, 1e-8);
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, InversiveDistance_QuadStrip_NamedParametersWork)
|
||
{
|
||
auto mesh = make_quad_strip();
|
||
// Named-parameter chaining (`a.b().c()`) is not currently supported on
|
||
// the package-local tags; pass one parameter per call instead.
|
||
auto res = CGAL::discrete_inversive_distance_map(
|
||
mesh,
|
||
CGAL::parameters::max_iterations(50));
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_LE(res.iterations, 50);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 5. Layout wrapper — end-to-end through CGAL API on an open mesh
|
||
//
|
||
// Uses the legacy maps explicitly because the wrappers return the
|
||
// Newton-converged x vector but not the maps. This exercises that the
|
||
// `CGAL::euclidean_layout` shim works as expected.
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, Layout_EuclideanWrapper_RoundTrip)
|
||
{
|
||
auto mesh = make_open_3face();
|
||
|
||
// Set up the maps + run Newton via the CGAL Euclidean entry.
|
||
auto res = CGAL::discrete_conformal_map_euclidean(mesh);
|
||
ASSERT_TRUE(res.converged);
|
||
|
||
// The wrapper does its own DOF assignment internally; we re-fetch
|
||
// the (now-populated) EuclideanMaps from the mesh's property maps
|
||
// to feed the layout wrapper.
|
||
auto maps = setup_euclidean_maps(mesh);
|
||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||
// Pin first vertex (mirrors the wrapper's gauge choice).
|
||
auto vit = mesh.vertices().begin();
|
||
maps.v_idx[*vit++] = -1;
|
||
int idx = 0;
|
||
for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++;
|
||
std::vector<double> x(idx, 0.0); // wrapper's natural-theta equilibrium
|
||
|
||
auto layout = CGAL::euclidean_layout(mesh, x, maps);
|
||
EXPECT_EQ(layout.uv.size(), num_vertices(mesh));
|
||
// All UVs finite — basic sanity that the layout ran.
|
||
for (auto& uv : layout.uv) {
|
||
EXPECT_TRUE(std::isfinite(uv.x()));
|
||
EXPECT_TRUE(std::isfinite(uv.y()));
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 6. output_uv_map named parameter — integrated layout step
|
||
//
|
||
// Phase 8b-Lite extension (2026-05-22): if the caller supplies a property
|
||
// map via `CGAL::parameters::output_uv_map(pmap)`, the entry function runs
|
||
// the appropriate `*_layout()` after Newton and writes the per-vertex
|
||
// coordinates into `pmap`. This closes the prior UX gap where users had
|
||
// to call the wrapper, then re-set up maps, then call the legacy layout
|
||
// API separately.
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_Euclidean_PopulatesPmap)
|
||
{
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_quad_strip();
|
||
|
||
auto uv_map = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv", K::Point_2(0, 0)).first;
|
||
|
||
auto res = CGAL::discrete_conformal_map_euclidean(
|
||
mesh,
|
||
CGAL::parameters::output_uv_map(uv_map));
|
||
|
||
ASSERT_TRUE(res.converged);
|
||
|
||
// The map must be populated with finite values.
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& p = uv_map[v];
|
||
EXPECT_TRUE(std::isfinite(p.x())) << "non-finite UV.x at vertex " << v.idx();
|
||
EXPECT_TRUE(std::isfinite(p.y())) << "non-finite UV.y at vertex " << v.idx();
|
||
}
|
||
|
||
// At least one vertex must have moved off the origin (the layout
|
||
// did NOT just return defaults).
|
||
bool any_nonzero = false;
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& p = uv_map[v];
|
||
if (std::abs(p.x()) + std::abs(p.y()) > 1e-10) { any_nonzero = true; break; }
|
||
}
|
||
EXPECT_TRUE(any_nonzero) << "every UV is exactly (0,0) — layout did not run";
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_Spherical_PopulatesXyz)
|
||
{
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_tetrahedron();
|
||
|
||
auto xyz_map = mesh.add_property_map<Vertex_index, K::Point_3>(
|
||
"v:test_xyz", K::Point_3(0, 0, 0)).first;
|
||
|
||
auto res = CGAL::discrete_conformal_map_spherical(
|
||
mesh,
|
||
CGAL::parameters::output_uv_map(xyz_map));
|
||
|
||
ASSERT_TRUE(res.converged);
|
||
|
||
// Every output point must lie on (or very near) the unit sphere.
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& p = xyz_map[v];
|
||
const double r = std::sqrt(p.x()*p.x() + p.y()*p.y() + p.z()*p.z());
|
||
EXPECT_NEAR(r, 1.0, 1e-6) << "vertex " << v.idx() << " not on unit sphere";
|
||
}
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_HyperIdeal_PointsInPoincareDisk)
|
||
{
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_tetrahedron();
|
||
|
||
auto uv_map = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv_hyp", K::Point_2(0, 0)).first;
|
||
|
||
// Named-parameter chaining is not supported yet — pass output_uv_map only.
|
||
auto res = CGAL::discrete_conformal_map_hyper_ideal(
|
||
mesh,
|
||
CGAL::parameters::output_uv_map(uv_map));
|
||
|
||
// The wrapper must complete and return a well-formed result struct
|
||
// regardless of whether Newton fully converges with the default
|
||
// Θ/θ targets in 200 iterations. We only verify that *if* the
|
||
// layout step ran (which happens only on converged Newton), the
|
||
// output is finite — Poincaré-disk geometric check is conditional.
|
||
EXPECT_EQ(res.b_per_vertex.size(), num_vertices(mesh));
|
||
EXPECT_EQ(res.a_per_edge.size(), num_edges(mesh));
|
||
|
||
if (res.converged) {
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& p = uv_map[v];
|
||
const double r2 = p.x()*p.x() + p.y()*p.y();
|
||
EXPECT_LE(r2, 1.0 + 1e-6)
|
||
<< "vertex " << v.idx() << " outside Poincaré disk (|p|² = " << r2 << ")";
|
||
}
|
||
}
|
||
// (else: Newton did not reach equilibrium; UV pmap is left at its
|
||
// default (0,0) per the wrapper's "if (nr.converged)" guard.
|
||
// No assertion needed; this is documented behaviour.)
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_InversiveDistance_PopulatesPmap)
|
||
{
|
||
// Inversive-Distance: per-vertex u_i = log r_i. With output_uv_map
|
||
// the entry function reconstructs effective Euclidean edge lengths via
|
||
// the Bowers-Stephenson identity and reuses the euclidean_layout
|
||
// priority-BFS to populate per-vertex Point_2 coordinates.
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_quad_strip();
|
||
|
||
auto uv_map = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv_id", K::Point_2(0, 0)).first;
|
||
|
||
auto res = CGAL::discrete_inversive_distance_map(
|
||
mesh, CGAL::parameters::output_uv_map(uv_map));
|
||
|
||
ASSERT_TRUE(res.converged) << "ID Newton did not converge on quad_strip";
|
||
EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh));
|
||
// Every UV must be finite; not all zero.
|
||
bool any_nonzero = false;
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& p = uv_map[v];
|
||
ASSERT_TRUE(std::isfinite(p.x()));
|
||
ASSERT_TRUE(std::isfinite(p.y()));
|
||
if (std::abs(p.x()) > 1e-9 || std::abs(p.y()) > 1e-9) any_nonzero = true;
|
||
}
|
||
EXPECT_TRUE(any_nonzero) << "all UVs are zero — layout did not run";
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_CPEuclidean_ThrowsClearly)
|
||
{
|
||
// CP-Euclidean is face-based; its natural layout is a per-face
|
||
// circle packing in ℝ², not a per-vertex Point_2 map. The entry
|
||
// throws std::runtime_error with a helpful message rather than
|
||
// silently producing nonsense. See doc/architecture/locked-vs-flexible.md.
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_quad_strip();
|
||
|
||
auto uv_map = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv_cp", K::Point_2(0, 0)).first;
|
||
|
||
EXPECT_THROW(
|
||
CGAL::discrete_circle_packing_euclidean(
|
||
mesh, CGAL::parameters::output_uv_map(uv_map)),
|
||
std::runtime_error)
|
||
<< "expected discrete_circle_packing_euclidean to reject "
|
||
"`output_uv_map(...)` (face-based DOF, Phase 9c).";
|
||
|
||
// Sanity: without output_uv_map the entry function still works fine.
|
||
auto res = CGAL::discrete_circle_packing_euclidean(mesh);
|
||
// Convergence depends on the mesh; we only check no-throw + a sane
|
||
// shape of the result struct.
|
||
EXPECT_EQ(res.rho_per_face.size(), num_faces(mesh));
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_Absent_DoesNotRunLayout)
|
||
{
|
||
// Sanity: without the parameter, no layout work happens. Verified
|
||
// here only via the fact that the call still succeeds and produces
|
||
// the same u-vector as before.
|
||
auto mesh = make_quad_strip();
|
||
auto res = CGAL::discrete_conformal_map_euclidean(mesh);
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh));
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, OutputUvMap_NormaliseLayout_TakesEffect)
|
||
{
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_quad_strip();
|
||
|
||
auto uv_raw = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv_raw", K::Point_2(0, 0)).first;
|
||
auto uv_norm = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:test_uv_norm", K::Point_2(0, 0)).first;
|
||
|
||
auto res1 = CGAL::discrete_conformal_map_euclidean(
|
||
mesh, CGAL::parameters::output_uv_map(uv_raw));
|
||
auto res2 = CGAL::discrete_conformal_map_euclidean(
|
||
mesh, CGAL::parameters::output_uv_map(uv_norm));
|
||
// (We can only pass one named parameter at a time without chaining;
|
||
// test the toggle by running the wrapper twice and verifying the
|
||
// raw call works. The normalise_layout flag is exercised in
|
||
// internal unit tests via direct calls to normalise_euclidean.)
|
||
ASSERT_TRUE(res1.converged);
|
||
ASSERT_TRUE(res2.converged);
|
||
// Both maps populated to finite values.
|
||
for (auto v : mesh.vertices()) {
|
||
EXPECT_TRUE(std::isfinite(uv_raw[v].x()));
|
||
EXPECT_TRUE(std::isfinite(uv_norm[v].x()));
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// 7. Named-parameter chaining via pipe-operator
|
||
//
|
||
// CGAL's `.a().b().c()` chaining requires modifying CGAL upstream, which
|
||
// we don't do. conformallab++ provides a `|` operator that achieves the
|
||
// same effect by left-to-right composition. These tests verify that the
|
||
// chain is read back correctly by the entry functions.
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(CGALPhase8bLite, NamedParamPipe_MultipleParamsTakeEffect)
|
||
{
|
||
using K = CGAL::Simple_cartesian<double>;
|
||
auto mesh = make_quad_strip();
|
||
|
||
auto uv = mesh.add_property_map<Vertex_index, K::Point_2>(
|
||
"v:pipe_uv", K::Point_2(0, 0)).first;
|
||
|
||
// Chain three parameters using `|`.
|
||
auto params = CGAL::parameters::gradient_tolerance(1e-12)
|
||
| CGAL::parameters::max_iterations(500)
|
||
| CGAL::parameters::output_uv_map(uv);
|
||
|
||
auto res = CGAL::discrete_conformal_map_euclidean(mesh, params);
|
||
|
||
EXPECT_TRUE(res.converged);
|
||
EXPECT_LT(res.gradient_norm, 1e-10); // tight tolerance applied
|
||
EXPECT_LE(res.iterations, 500);
|
||
// UV pmap was populated.
|
||
bool any_nonzero = false;
|
||
for (auto v : mesh.vertices()) {
|
||
if (std::abs(uv[v].x()) + std::abs(uv[v].y()) > 1e-10) {
|
||
any_nonzero = true;
|
||
break;
|
||
}
|
||
}
|
||
EXPECT_TRUE(any_nonzero);
|
||
}
|
||
|
||
TEST(CGALPhase8bLite, NamedParamPipe_TwoParams)
|
||
{
|
||
// Pipe two parameters and verify both take effect.
|
||
auto mesh = make_triangle();
|
||
auto params = CGAL::parameters::max_iterations(0)
|
||
| CGAL::parameters::gradient_tolerance(1e-6);
|
||
auto res = CGAL::discrete_conformal_map_euclidean(mesh, params);
|
||
EXPECT_EQ(res.iterations, 0); // max_iterations(0) blocks the loop
|
||
}
|