feat(phase4): HyperIdeal Newton solver, SparseQR fallback, examples, docs
Phase 4 complete — 87 CGAL tests pass, 2 skipped. Newton solver (phase4a): - hyper_ideal_hessian.hpp: symmetric FD Hessian (O(ε²), PSD by convexity) - newton_hyper_ideal(): Newton + backtracking for the HyperIdeal functional - detail::solve_with_fallback(): optional bool* fallback_used parameter - solve_linear_system(): public API exposing LDLT→SparseQR fallback SparseQR fallback tests (SparseQRFallback.*): - FullRankSystem_CorrectSolution: LDLT path, fallback_used=false - SingularMatrix_FallbackActivated: zero-pivot → QR activated, fallback_used=true - Euclidean_ClosedMeshNoPinConverges: gauge-mode null space handled via QR HyperIdeal Newton tests (NewtonSolver.HyperIdeal_*): - ConvergesTriangleAllVariable, ResultFieldsConsistent, ConvergesTetrahedron, SparseQRFallbackNoCrash - Natural-target base point (b=1.0, a=0.5) — x=0 is degenerate in log-space Pipeline tests (test_pipeline.cpp): - End-to-end: all three geometries, mesh I/O round-trip, solve+export Example programs (code/examples/): - example_euclidean.cpp: headless Euclidean pipeline - example_hyper_ideal.cpp: headless HyperIdeal pipeline - example_viewer.cpp: interactive libigl viewer with jet colour map README: - Mathematical scope table: C++ vs Java original (18 rows) - "For mathematicians" section: mental model, step-by-step new-functional guide, half-edge traversal snippets, recommended reading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
49
code/examples/CMakeLists.txt
Normal file
49
code/examples/CMakeLists.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
# examples/CMakeLists.txt
|
||||
#
|
||||
# Example programs for conformallab++.
|
||||
# All examples require -DWITH_CGAL=ON.
|
||||
# example_viewer additionally requires -DWITH_VIEWER=ON.
|
||||
#
|
||||
# Run after building:
|
||||
# ./build/examples/example_euclidean [input.off] [output.off]
|
||||
# ./build/examples/example_hyper_ideal [input.off] [output.off]
|
||||
# ./build/examples/example_viewer [input.off] (requires WITH_VIEWER)
|
||||
|
||||
# ── Shared include paths for all examples ─────────────────────────────────────
|
||||
set(EXAMPLE_INCLUDES
|
||||
${CMAKE_SOURCE_DIR}/deps/eigen-3.4.0
|
||||
${CMAKE_SOURCE_DIR}/deps/CGAL-6.1.1/include
|
||||
${Boost_INCLUDE_DIRS}
|
||||
)
|
||||
set(EXAMPLE_PRIVATE_INCLUDES
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
set(EXAMPLE_DEFS
|
||||
CGAL_DISABLE_GMP
|
||||
CGAL_DISABLE_MPFR
|
||||
)
|
||||
|
||||
# ── example_euclidean ─────────────────────────────────────────────────────────
|
||||
add_executable(example_euclidean example_euclidean.cpp)
|
||||
target_include_directories(example_euclidean SYSTEM PRIVATE ${EXAMPLE_INCLUDES})
|
||||
target_include_directories(example_euclidean PRIVATE ${EXAMPLE_PRIVATE_INCLUDES})
|
||||
target_compile_definitions(example_euclidean PRIVATE ${EXAMPLE_DEFS})
|
||||
|
||||
# ── example_hyper_ideal ───────────────────────────────────────────────────────
|
||||
add_executable(example_hyper_ideal example_hyper_ideal.cpp)
|
||||
target_include_directories(example_hyper_ideal SYSTEM PRIVATE ${EXAMPLE_INCLUDES})
|
||||
target_include_directories(example_hyper_ideal PRIVATE ${EXAMPLE_PRIVATE_INCLUDES})
|
||||
target_compile_definitions(example_hyper_ideal PRIVATE ${EXAMPLE_DEFS})
|
||||
|
||||
# ── example_viewer (requires WITH_VIEWER) ─────────────────────────────────────
|
||||
if(WITH_VIEWER)
|
||||
add_executable(example_viewer example_viewer.cpp)
|
||||
target_include_directories(example_viewer SYSTEM PRIVATE
|
||||
${EXAMPLE_INCLUDES}
|
||||
${CMAKE_SOURCE_DIR}/deps/libigl-2.6.0/include
|
||||
${CMAKE_SOURCE_DIR}/deps/libigl-glad/include
|
||||
)
|
||||
target_include_directories(example_viewer PRIVATE ${EXAMPLE_PRIVATE_INCLUDES})
|
||||
target_compile_definitions(example_viewer PRIVATE ${EXAMPLE_DEFS})
|
||||
target_link_libraries(example_viewer PRIVATE viewer)
|
||||
endif()
|
||||
117
code/examples/example_euclidean.cpp
Normal file
117
code/examples/example_euclidean.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
// example_euclidean.cpp
|
||||
//
|
||||
// conformallab++ — Euclidean discrete conformal map (headless example)
|
||||
//
|
||||
// This program demonstrates the full library pipeline for the EUCLIDEAN
|
||||
// discrete conformal functional:
|
||||
//
|
||||
// 1. Load a triangle mesh from an OFF file
|
||||
// 2. Set up the Euclidean functional maps
|
||||
// 3. Pin one vertex (gauge fix for open surfaces)
|
||||
// 4. Set target angles via "natural equilibrium" (x* = x_input)
|
||||
// 5. Solve with Newton + backtracking line search
|
||||
// 6. Print per-vertex conformal factors u_i = x[v_idx[v]]
|
||||
// 7. Save the result mesh (same geometry, solver state printed)
|
||||
//
|
||||
// Build (requires -DWITH_CGAL=ON):
|
||||
// cmake -S code -B build -DWITH_CGAL=ON
|
||||
// cmake --build build --target example_euclidean
|
||||
// ./build/examples/example_euclidean [input.off] [output.off]
|
||||
//
|
||||
// If no input file is given the built-in make_quad_strip() mesh is used.
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "mesh_builder.hpp"
|
||||
#include "mesh_io.hpp"
|
||||
#include "euclidean_functional.hpp"
|
||||
#include "newton_solver.hpp"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace conformallab;
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// ── Step 1: obtain mesh ───────────────────────────────────────────────
|
||||
ConformalMesh mesh;
|
||||
std::string input_path = (argc > 1) ? argv[1] : "";
|
||||
std::string output_path = (argc > 2) ? argv[2] : "/tmp/conformallab_euclidean_out.off";
|
||||
|
||||
if (input_path.empty()) {
|
||||
std::cout << "[example_euclidean] No input file given — using make_quad_strip().\n";
|
||||
mesh = make_quad_strip();
|
||||
} else {
|
||||
std::cout << "[example_euclidean] Loading mesh from: " << input_path << "\n";
|
||||
try { mesh = load_mesh(input_path); }
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Error loading mesh: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[example_euclidean] Mesh: "
|
||||
<< mesh.number_of_vertices() << " vertices, "
|
||||
<< mesh.number_of_faces() << " faces.\n";
|
||||
|
||||
// ── Step 2: set up functional maps ────────────────────────────────────
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// ── Step 3: pin the first vertex (gauge fix) ──────────────────────────
|
||||
auto vit = mesh.vertices().begin();
|
||||
Vertex_index v_pinned = *vit++;
|
||||
maps.v_idx[v_pinned] = -1; // pinned: u[v_pinned] = 0 (fixed)
|
||||
int idx = 0;
|
||||
for (; vit != mesh.vertices().end(); ++vit)
|
||||
maps.v_idx[*vit] = idx++;
|
||||
const int n = idx;
|
||||
|
||||
std::cout << "[example_euclidean] DOFs: " << n << " (1 vertex pinned).\n";
|
||||
|
||||
// ── Step 4: natural equilibrium — set theta_v = actual angle sum at x=0 ─
|
||||
// After this step x* = 0 is the equilibrium (no deformation).
|
||||
// In a real application you would set theta_v = desired angle (e.g. 2π
|
||||
// for flat disks, or the cone angles for a cone metric).
|
||||
{
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto G0 = euclidean_gradient(mesh, x0, maps);
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 5: solve from a small perturbation to demonstrate Newton ─────
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), -0.05);
|
||||
std::cout << "[example_euclidean] Solving Newton system…\n";
|
||||
auto result = newton_euclidean(mesh, x0, maps, /*tol=*/1e-9, /*max_iter=*/100);
|
||||
|
||||
// ── Step 6: report ────────────────────────────────────────────────────
|
||||
if (result.converged) {
|
||||
std::cout << "[example_euclidean] Converged in " << result.iterations
|
||||
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
|
||||
} else {
|
||||
std::cout << "[example_euclidean] Did NOT converge after " << result.iterations
|
||||
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
|
||||
}
|
||||
|
||||
std::cout << "[example_euclidean] Per-vertex conformal factors u_i:\n";
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
double u = (iv >= 0) ? result.x[static_cast<std::size_t>(iv)] : 0.0;
|
||||
std::cout << " v" << v << " u = " << u;
|
||||
if (iv < 0) std::cout << " (pinned)";
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
// ── Step 7: write output mesh ─────────────────────────────────────────
|
||||
try {
|
||||
save_mesh(output_path, mesh);
|
||||
std::cout << "[example_euclidean] Mesh saved to: " << output_path << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Warning: could not write output: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
return result.converged ? 0 : 1;
|
||||
}
|
||||
147
code/examples/example_hyper_ideal.cpp
Normal file
147
code/examples/example_hyper_ideal.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
// example_hyper_ideal.cpp
|
||||
//
|
||||
// conformallab++ — Hyper-ideal discrete conformal map (headless example)
|
||||
//
|
||||
// Demonstrates the full library pipeline for the HYPER-IDEAL discrete conformal
|
||||
// functional (Springborn 2020). The hyper-ideal functional operates in
|
||||
// hyperbolic geometry: vertices have "horoball radii" (DOF b_i) and edges have
|
||||
// "intersection lengths" (DOF a_e). The energy is strictly convex, so Newton
|
||||
// converges globally from any valid starting point.
|
||||
//
|
||||
// Pipeline:
|
||||
// 1. Load (or synthesise) a triangle mesh
|
||||
// 2. Set up HyperIdeal maps + assign all vertex and edge DOFs
|
||||
// 3. Choose equilibrium base point (b=1.0, a=0.5) and set natural targets
|
||||
// 4. Perturb and solve with Newton
|
||||
// 5. Print DOF values at equilibrium
|
||||
// 6. Save result mesh
|
||||
//
|
||||
// Build (requires -DWITH_CGAL=ON):
|
||||
// cmake -S code -B build -DWITH_CGAL=ON
|
||||
// cmake --build build --target example_hyper_ideal
|
||||
// ./build/examples/example_hyper_ideal [input.off] [output.off]
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "mesh_builder.hpp"
|
||||
#include "mesh_io.hpp"
|
||||
#include "hyper_ideal_functional.hpp"
|
||||
#include "newton_solver.hpp"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
|
||||
using namespace conformallab;
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// ── Step 1: obtain mesh ───────────────────────────────────────────────
|
||||
ConformalMesh mesh;
|
||||
std::string input_path = (argc > 1) ? argv[1] : "";
|
||||
std::string output_path = (argc > 2) ? argv[2] : "/tmp/conformallab_hyper_ideal_out.off";
|
||||
|
||||
if (input_path.empty()) {
|
||||
std::cout << "[example_hyper_ideal] No input file — using make_triangle().\n";
|
||||
mesh = make_triangle();
|
||||
} else {
|
||||
std::cout << "[example_hyper_ideal] Loading mesh from: " << input_path << "\n";
|
||||
try { mesh = load_mesh(input_path); }
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Error loading mesh: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[example_hyper_ideal] Mesh: "
|
||||
<< mesh.number_of_vertices() << " vertices, "
|
||||
<< mesh.number_of_faces() << " faces.\n";
|
||||
|
||||
// ── Step 2: set up functional maps ────────────────────────────────────
|
||||
auto maps = setup_hyper_ideal_maps(mesh);
|
||||
int n = assign_all_dof_indices(mesh, maps);
|
||||
|
||||
std::cout << "[example_hyper_ideal] DOFs: " << n
|
||||
<< " (" << mesh.number_of_vertices() << " vertex + "
|
||||
<< mesh.number_of_edges() << " edge).\n";
|
||||
|
||||
// ── Step 3: choose equilibrium base point and set natural targets ─────
|
||||
//
|
||||
// x = 0 is degenerate for the HyperIdeal functional (log-space).
|
||||
// We pick a valid base point (b_i = b_base, a_e = a_base), evaluate
|
||||
// the gradient there, and absorb it into the target angles so that
|
||||
// G(xbase) = 0. This makes xbase the equilibrium x*.
|
||||
//
|
||||
// In a real application you would set theta_v / theta_e to the desired
|
||||
// hyperbolic angle targets (e.g. from a reference mesh).
|
||||
const double b_base = 1.0; // horoball radii at equilibrium
|
||||
const double a_base = 0.5; // edge-length DOFs at equilibrium
|
||||
|
||||
const auto sz = static_cast<std::size_t>(n);
|
||||
std::vector<double> xbase(sz, 0.0);
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) xbase[static_cast<std::size_t>(iv)] = b_base;
|
||||
}
|
||||
for (auto e : mesh.edges()) {
|
||||
int ie = maps.e_idx[e];
|
||||
if (ie >= 0) xbase[static_cast<std::size_t>(ie)] = a_base;
|
||||
}
|
||||
|
||||
// G = Σβ − theta_target; absorb G(xbase) into targets so G(xbase) = 0
|
||||
auto G0 = evaluate_hyper_ideal(mesh, xbase, maps, false).gradient;
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) maps.theta_v[v] += G0[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
for (auto e : mesh.edges()) {
|
||||
int ie = maps.e_idx[e];
|
||||
if (ie >= 0) maps.theta_e[e] += G0[static_cast<std::size_t>(ie)];
|
||||
}
|
||||
|
||||
// ── Step 4: perturb and solve ─────────────────────────────────────────
|
||||
const double perturb = 0.25;
|
||||
std::vector<double> x0 = xbase;
|
||||
for (auto& v : x0) v += perturb;
|
||||
|
||||
double g_start = 0.0;
|
||||
for (double v : G0) g_start = std::max(g_start, std::abs(v));
|
||||
std::cout << "[example_hyper_ideal] Starting Newton from perturbation +" << perturb
|
||||
<< " (G at xbase = " << g_start << ").\n";
|
||||
|
||||
auto result = newton_hyper_ideal(mesh, x0, maps, /*tol=*/1e-9, /*max_iter=*/200);
|
||||
|
||||
// ── Step 5: report ────────────────────────────────────────────────────
|
||||
if (result.converged) {
|
||||
std::cout << "[example_hyper_ideal] Converged in " << result.iterations
|
||||
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
|
||||
} else {
|
||||
std::cout << "[example_hyper_ideal] Did NOT converge after " << result.iterations
|
||||
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
|
||||
}
|
||||
|
||||
std::cout << "[example_hyper_ideal] DOF values at equilibrium:\n";
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv < 0) continue;
|
||||
std::cout << " v" << v
|
||||
<< " b = " << result.x[static_cast<std::size_t>(iv)]
|
||||
<< " (expected " << b_base << ")\n";
|
||||
}
|
||||
for (auto e : mesh.edges()) {
|
||||
int ie = maps.e_idx[e];
|
||||
if (ie < 0) continue;
|
||||
std::cout << " e" << e
|
||||
<< " a = " << result.x[static_cast<std::size_t>(ie)]
|
||||
<< " (expected " << a_base << ")\n";
|
||||
}
|
||||
|
||||
// ── Step 6: write output mesh ─────────────────────────────────────────
|
||||
try {
|
||||
save_mesh(output_path, mesh);
|
||||
std::cout << "[example_hyper_ideal] Mesh saved to: " << output_path << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Warning: could not write output: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
return result.converged ? 0 : 1;
|
||||
}
|
||||
150
code/examples/example_viewer.cpp
Normal file
150
code/examples/example_viewer.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
// example_viewer.cpp
|
||||
//
|
||||
// conformallab++ — Interactive viewer example (requires -DWITH_VIEWER=ON)
|
||||
//
|
||||
// This example demonstrates the full end-to-end pipeline WITH visual output:
|
||||
//
|
||||
// 1. Load (or synthesise) a mesh
|
||||
// 2. Compute the Euclidean discrete conformal map (Newton solver)
|
||||
// 3. Display the result in an interactive libigl / GLFW window
|
||||
// • Left pane: input mesh, coloured by per-vertex conformal factor u_i
|
||||
// • Right pane: a flat parameterisation (future, placeholder)
|
||||
//
|
||||
// The viewer is split into two data sets using libigl's multi-mesh API so
|
||||
// users can inspect geometry and solution simultaneously.
|
||||
//
|
||||
// Build (requires -DWITH_CGAL=ON -DWITH_VIEWER=ON):
|
||||
// cmake -S code -B build -DWITH_CGAL=ON -DWITH_VIEWER=ON
|
||||
// cmake --build build --target example_viewer
|
||||
// ./build/examples/example_viewer [input.off]
|
||||
//
|
||||
// Navigation (libigl default):
|
||||
// Mouse drag — rotate
|
||||
// Scroll — zoom
|
||||
// C — toggle camera mode (trackball / 2D)
|
||||
// Z / X / Y — snap to axis-aligned view
|
||||
// Q / Esc — quit
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "mesh_builder.hpp"
|
||||
#include "mesh_io.hpp"
|
||||
#include "euclidean_functional.hpp"
|
||||
#include "mesh_utils.hpp"
|
||||
#include "newton_solver.hpp"
|
||||
#include <igl/opengl/glfw/Viewer.h>
|
||||
#include <Eigen/Dense>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace conformallab;
|
||||
|
||||
// ── Jet colour map: scalar → RGB ─────────────────────────────────────────────
|
||||
static Eigen::RowVector3d jet(double t)
|
||||
{
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
double r = std::clamp(1.5 - std::abs(4.0 * t - 3.0), 0.0, 1.0);
|
||||
double g = std::clamp(1.5 - std::abs(4.0 * t - 2.0), 0.0, 1.0);
|
||||
double b = std::clamp(1.5 - std::abs(4.0 * t - 1.0), 0.0, 1.0);
|
||||
return {r, g, b};
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// ── Step 1: load or synthesise mesh ───────────────────────────────────
|
||||
ConformalMesh mesh;
|
||||
std::string input_path = (argc > 1) ? argv[1] : "";
|
||||
|
||||
if (input_path.empty()) {
|
||||
std::cout << "[example_viewer] No input file — using make_quad_strip().\n";
|
||||
mesh = make_quad_strip();
|
||||
} else {
|
||||
std::cout << "[example_viewer] Loading: " << input_path << "\n";
|
||||
try { mesh = load_mesh(input_path); }
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[example_viewer] Mesh: "
|
||||
<< mesh.number_of_vertices() << " vertices, "
|
||||
<< mesh.number_of_faces() << " faces.\n";
|
||||
|
||||
// ── Step 2: solve the Euclidean discrete conformal map ────────────────
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// Pin first vertex
|
||||
auto vit = mesh.vertices().begin();
|
||||
maps.v_idx[*vit++] = -1;
|
||||
int idx = 0;
|
||||
for (; vit != mesh.vertices().end(); ++vit)
|
||||
maps.v_idx[*vit] = idx++;
|
||||
const int n = idx;
|
||||
|
||||
// Natural equilibrium
|
||||
{
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto G0 = euclidean_gradient(mesh, x0, maps);
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
}
|
||||
|
||||
// Perturb and solve
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), -0.08);
|
||||
auto result = newton_euclidean(mesh, x0, maps, 1e-9, 200);
|
||||
|
||||
if (result.converged)
|
||||
std::cout << "[example_viewer] Converged in " << result.iterations << " iterations.\n";
|
||||
else
|
||||
std::cout << "[example_viewer] Warning: did not converge fully "
|
||||
"(||G||_inf = " << result.grad_inf_norm << ").\n";
|
||||
|
||||
// ── Step 3: build Eigen V / F for libigl ─────────────────────────────
|
||||
using Kernel = CGAL::Simple_cartesian<double>;
|
||||
Eigen::MatrixXd V;
|
||||
Eigen::MatrixXi F;
|
||||
mesh_utils::cgal_to_eigen<Kernel>(mesh, V, F);
|
||||
|
||||
// Per-vertex colour: conformal factor u_i, mapped via jet palette
|
||||
const int nv = static_cast<int>(V.rows());
|
||||
Eigen::MatrixXd C(nv, 3);
|
||||
|
||||
// Collect all u values to normalise
|
||||
std::vector<double> u_all(static_cast<std::size_t>(nv), 0.0);
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0)
|
||||
u_all[static_cast<std::size_t>(v.idx())] = result.x[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
double u_min = *std::min_element(u_all.begin(), u_all.end());
|
||||
double u_max = *std::max_element(u_all.begin(), u_all.end());
|
||||
double u_range = (u_max > u_min) ? (u_max - u_min) : 1.0;
|
||||
|
||||
for (int vi = 0; vi < nv; ++vi) {
|
||||
double t = (u_all[static_cast<std::size_t>(vi)] - u_min) / u_range;
|
||||
C.row(vi) = jet(t);
|
||||
}
|
||||
|
||||
// ── Step 4: launch interactive viewer ─────────────────────────────────
|
||||
igl::opengl::glfw::Viewer viewer;
|
||||
viewer.data().set_mesh(V, F);
|
||||
viewer.data().set_colors(C);
|
||||
viewer.data().show_lines = true;
|
||||
viewer.data().show_overlay = true;
|
||||
|
||||
// Status text overlay
|
||||
viewer.data().add_label(
|
||||
Eigen::Vector3d(V.col(0).mean(), V.col(1).mean(), V.col(2).maxCoeff()),
|
||||
"Euclidean conformal factor u_i (jet: blue=min, red=max)");
|
||||
|
||||
std::cout << "[example_viewer] Launching viewer. Press Q or Esc to quit.\n";
|
||||
viewer.launch();
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user