// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // conformallab_cli.cpp // // ConformalLab++ command-line interface. // // Usage: // conformallab_core -i input.off [-o layout.off] [-g euclidean|spherical|hyper_ideal] // [-j result.json] [-x result.xml] [-s] [-v] // // The tool: // 1. Loads an OFF/OBJ/PLY mesh. // 2. Sets up DOF maps + computes λ° from the input geometry. // 3. Pins one vertex (Euclidean/Spherical) or uses all-free DOFs (HyperIdeal). // 4. Runs Newton until convergence. // 5. Computes a 2-D (Euclidean / HyperIdeal) or 3-D (Spherical) layout. // 6. Saves the layout as an OFF file and optionally serialises the result // to JSON and/or XML. // 7. Optionally shows the input mesh in a viewer (-s flag, requires WITH_VIEWER). #include "conformal_mesh.hpp" #include "mesh_io.hpp" #include "euclidean_functional.hpp" #include "spherical_functional.hpp" #include "hyper_ideal_functional.hpp" #include "newton_solver.hpp" #include "layout.hpp" #include "serialization.hpp" #include #include #include #include #include #include #include // Viewer is always available when WITH_CGAL=ON (implies WITH_VIEWER=ON) #include "viewer_utils.h" #include namespace cl = conformallab; using cl::ConformalMesh; using cl::Vertex_index; using cl::Edge_index; // ───────────────────────────────────────────────────────────────────────────── // Shared helpers (mirroring test_pipeline.cpp patterns) // ───────────────────────────────────────────────────────────────────────────── // Pin vertex 0, assign 0..n-1 to the rest static int pin_first_vertex(ConformalMesh& mesh, cl::EuclideanMaps& maps) { auto vit = mesh.vertices().begin(); Vertex_index v0 = *vit++; maps.v_idx[v0] = -1; int idx = 0; for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++; return idx; } // Natural theta for Euclidean: make x=0 the equilibrium static void set_natural_euclidean_theta(ConformalMesh& mesh, cl::EuclideanMaps& maps, int n) { std::vector x0(static_cast(n), 0.0); auto G = cl::euclidean_gradient(mesh, x0, maps); for (auto v : mesh.vertices()) { int iv = maps.v_idx[v]; if (iv < 0) continue; maps.theta_v[v] -= G[static_cast(iv)]; } } // Natural theta for HyperIdeal at base point (b=1, a=0.5) to avoid x=0 singularity static std::vector set_natural_hyper_ideal_targets( ConformalMesh& mesh, cl::HyperIdealMaps& maps, int n) { const auto sz = static_cast(n); std::vector xbase(sz, 0.0); for (auto v : mesh.vertices()) { int iv = maps.v_idx[v]; if (iv >= 0) xbase[static_cast(iv)] = 1.0; } for (auto e : mesh.edges()) { int ie = maps.e_idx[e]; if (ie >= 0) xbase[static_cast(ie)] = 0.5; } auto G = cl::evaluate_hyper_ideal(mesh, xbase, maps, false).gradient; for (auto v : mesh.vertices()) { int iv = maps.v_idx[v]; if (iv < 0) continue; maps.theta_v[v] += G[static_cast(iv)]; } for (auto e : mesh.edges()) { int ie = maps.e_idx[e]; if (ie < 0) continue; maps.theta_e[e] += G[static_cast(ie)]; } return xbase; } // ───────────────────────────────────────────────────────────────────────────── // Euclidean pipeline // ───────────────────────────────────────────────────────────────────────────── static int run_euclidean(ConformalMesh& mesh, const std::string& out_layout, const std::string& out_json, const std::string& out_xml, bool verbose) { // Setup auto maps = cl::setup_euclidean_maps(mesh); cl::compute_euclidean_lambda0_from_mesh(mesh, maps); // DOF assignment: pin vertex 0 int n = pin_first_vertex(mesh, maps); if (n <= 0) { std::cerr << "Error: mesh has only one vertex.\n"; return 1; } // Natural target angles set_natural_euclidean_theta(mesh, maps, n); // Newton std::vector x0(static_cast(n), 0.0); auto res = cl::newton_euclidean(mesh, x0, maps); if (!res.converged && verbose) std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n"; // Layout cl::Layout2D layout = cl::euclidean_layout(mesh, res.x, maps); // Output if (!out_layout.empty()) cl::save_layout_off(out_layout, mesh, layout); if (!out_json.empty()) cl::save_result_json(out_json, res, "euclidean", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), &layout); if (!out_xml.empty()) cl::save_result_xml(out_xml, res, "euclidean", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), &layout); std::cout << std::fixed << std::setprecision(6); std::cout << "Euclidean: converged=" << (res.converged ? "yes" : "no") << " iter=" << res.iterations << " |grad|_inf=" << std::scientific << std::setprecision(3) << res.grad_inf_norm << "\n"; if (!out_layout.empty()) std::cout << " layout → " << out_layout << "\n"; if (!out_json.empty()) std::cout << " json → " << out_json << "\n"; if (!out_xml.empty()) std::cout << " xml → " << out_xml << "\n"; return 0; } // ───────────────────────────────────────────────────────────────────────────── // Spherical pipeline // ───────────────────────────────────────────────────────────────────────────── static int run_spherical(ConformalMesh& mesh, const std::string& out_layout, const std::string& out_json, const std::string& out_xml, bool verbose) { auto maps = cl::setup_spherical_maps(mesh); cl::compute_lambda0_from_mesh(mesh, maps); int n = cl::assign_vertex_dof_indices(mesh, maps); std::vector x0(static_cast(n), 0.0); auto res = cl::newton_spherical(mesh, x0, maps); if (!res.converged && verbose) std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n"; cl::Layout3D layout = cl::spherical_layout(mesh, res.x, maps); if (!out_layout.empty()) cl::save_layout_off(out_layout, mesh, layout); if (!out_json.empty()) cl::save_result_json(out_json, res, "spherical", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), nullptr, &layout); if (!out_xml.empty()) cl::save_result_xml(out_xml, res, "spherical", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), nullptr, &layout); std::cout << "Spherical: converged=" << (res.converged ? "yes" : "no") << " iter=" << res.iterations << " |grad|_inf=" << std::scientific << std::setprecision(3) << res.grad_inf_norm << "\n"; if (!out_layout.empty()) std::cout << " layout → " << out_layout << "\n"; if (!out_json.empty()) std::cout << " json → " << out_json << "\n"; if (!out_xml.empty()) std::cout << " xml → " << out_xml << "\n"; return 0; } // ───────────────────────────────────────────────────────────────────────────── // HyperIdeal pipeline // ───────────────────────────────────────────────────────────────────────────── static int run_hyper_ideal(ConformalMesh& mesh, const std::string& out_layout, const std::string& out_json, const std::string& out_xml, bool verbose) { auto maps = cl::setup_hyper_ideal_maps(mesh); int n = cl::assign_all_dof_indices(mesh, maps); // Natural targets at base point (b=1, a=0.5) auto xbase = set_natural_hyper_ideal_targets(mesh, maps, n); // Perturb slightly from the base and solve back std::vector x0 = xbase; for (auto& v : x0) v += 0.3; auto res = cl::newton_hyper_ideal(mesh, x0, maps); if (!res.converged && verbose) std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n"; cl::Layout2D layout = cl::hyper_ideal_layout(mesh, res.x, maps); if (!out_layout.empty()) cl::save_layout_off(out_layout, mesh, layout); if (!out_json.empty()) cl::save_result_json(out_json, res, "hyper_ideal", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), &layout); if (!out_xml.empty()) cl::save_result_xml(out_xml, res, "hyper_ideal", static_cast(mesh.number_of_vertices()), static_cast(mesh.number_of_faces()), &layout); std::cout << "HyperIdeal: converged=" << (res.converged ? "yes" : "no") << " iter=" << res.iterations << " |grad|_inf=" << std::scientific << std::setprecision(3) << res.grad_inf_norm << "\n"; if (!out_layout.empty()) std::cout << " layout → " << out_layout << "\n"; if (!out_json.empty()) std::cout << " json → " << out_json << "\n"; if (!out_xml.empty()) std::cout << " xml → " << out_xml << "\n"; return 0; } // ───────────────────────────────────────────────────────────────────────────── // main // ───────────────────────────────────────────────────────────────────────────── int main(int argc, char* argv[]) { CLI::App app{"ConformalLab++ — discrete conformal mapping"}; std::string input_file; std::string out_layout; std::string out_json; std::string out_xml; std::string geometry = "euclidean"; bool show = false; bool verbose = false; app.add_option("-i,--input", input_file, "Input mesh (OFF/OBJ/PLY)")->required(); app.add_option("-o,--output", out_layout, "Output layout OFF file"); app.add_option("-j,--json", out_json, "Save result as JSON"); app.add_option("-x,--xml", out_xml, "Save result as XML"); app.add_option("-g,--geometry", geometry, "Target geometry: euclidean|spherical|hyper_ideal") ->check(CLI::IsMember({"euclidean", "spherical", "hyper_ideal"})); app.add_flag("-s,--show", show, "Visualise input mesh (requires WITH_VIEWER)"); app.add_flag("-v,--verbose", verbose, "Verbose output"); CLI11_PARSE(app, argc, argv); // ── Load mesh ───────────────────────────────────────────────────────────── ConformalMesh mesh; if (!cl::read_mesh(input_file, mesh) || mesh.is_empty()) { std::cerr << "Error: cannot load mesh from '" << input_file << "'\n"; return EXIT_FAILURE; } std::cout << "Loaded: " << input_file << " (" << mesh.number_of_vertices() << "v, " << mesh.number_of_faces() << "f)\n"; // ── Optional viewer ─────────────────────────────────────────────────────── if (show) { // Convert ConformalMesh to Eigen matrices for the libigl viewer const std::size_t nv = mesh.number_of_vertices(); const std::size_t nf = mesh.number_of_faces(); Eigen::MatrixXd V(static_cast(nv), 3); Eigen::MatrixXi F(static_cast(nf), 3); for (auto v : mesh.vertices()) { auto p = mesh.point(v); V.row(v.idx()) << p.x(), p.y(), p.z(); } Eigen::Index fi = 0; for (auto f : mesh.faces()) { int ci = 0; for (auto h : CGAL::halfedges_around_face(mesh.halfedge(f), mesh)) F(fi, ci++) = static_cast(mesh.target(h).idx()); ++fi; } viewer_utils::simple_visualize(V, F); } // ── Dispatch ────────────────────────────────────────────────────────────── if (geometry == "euclidean") return run_euclidean(mesh, out_layout, out_json, out_xml, verbose); if (geometry == "spherical") return run_spherical(mesh, out_layout, out_json, out_xml, verbose); if (geometry == "hyper_ideal") return run_hyper_ideal(mesh, out_layout, out_json, out_xml, verbose); std::cerr << "Unknown geometry: " << geometry << "\n"; return EXIT_FAILURE; }