From fa7eb8fbde1ae0e733faa7249732f98add6eaf7e Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Wed, 17 Jun 2026 17:07:44 -0400 Subject: [PATCH 01/17] Finished code outline and rearranged several utility methods and files --- CMakeLists.txt | 14 +- README.md | 209 ++--- include/feature/feature/core/component_mesh.h | 2 +- include/feature/feature/core/union_meshes.h | 2 +- include/feature/feature/experimental/misc.h | 1 + .../feature/feature/experimental/transfer.h | 2 +- include/feature/feature/feature/features.h | 2 +- include/feature/feature/pybind.h | 3 + include/field/field/cross_field.h | 2 +- include/field/field/facet_field.h | 2 +- include/field/field/field.h | 3 +- include/field/field/forms.h | 1 - include/field/field/intrinsic_field.h | 7 +- include/field/field/rounder.h | 2 +- include/holonomy/holonomy/core/common.h | 1 - include/holonomy/holonomy/pybind.h | 3 + .../holonomy/holonomy/similarity/conformal.h | 6 + .../holonomy/holonomy/similarity/constraint.h | 5 + include/holonomy/holonomy/similarity/energy.h | 6 + include/holonomy/holonomy/similarity/layout.h | 4 + .../similarity_penner_cone_metric.h | 7 + include/metric/metric/area.h | 1 - include/metric/metric/common.h | 54 -- include/metric/metric/cone_metric.h | 3 +- include/metric/metric/constraint.h | 1 - include/metric/metric/flip_matrix_generator.h | 2 +- include/metric/metric/projection.h | 1 - include/metric/metric/reparametrization.h | 2 +- include/metric/metric/shear.h | 1 - include/metric/metric/viewer.h | 3 +- include/optimization/optimization/interface.h | 1 - .../metric_optimization/convergence.h | 49 -- .../metric_optimization/energies.h | 1 - .../metric_optimization/energy_functor.h | 1 - .../metric_optimization/energy_weights.h | 1 - .../explicit_optimization.h | 1 - .../implicit_optimization.h | 23 +- .../nonlinear_optimization.h | 2 +- include/optimization/optimization/pybind.h | 3 + .../optimization/optimization/util/shapes.h | 2 +- .../optimization/util/validation.h | 1 - .../optimization/util/visualization.h | 46 -- .../parametrization/interpolation.h | 2 +- .../parametrization/parametrization/layout.h | 2 +- .../parametrization/parametrize.h | 7 +- .../parametrization/refinement.h | 2 +- .../parametrization/translation.h | 2 +- .../parametrization/triangulation.h | 3 +- include/util/util/boundary.h | 5 + include/util/util/common.h | 5 + include/util/util/embedding.h | 22 +- include/util/util/io.h | 5 + include/util/util/linear_algebra.h | 17 + include/util/util/map.h | 5 + include/util/util/spanning_tree.h | 5 + .../feature => util}/util/union_find.h | 8 +- include/util/util/vector.h | 19 +- include/util/util/vf_mesh.h | 9 + src/CMakeLists.txt | 4 +- src/feature/CMakeLists.txt | 18 - .../boundary_constraint_generator.cpp | 381 ---------- .../boundary_constraint_validator.cpp | 496 ------------ src/feature/experimental/depracated.cpp | 719 ------------------ src/feature/experimental/energy.cpp | 72 -- src/feature/experimental/junctions.cpp | 374 --------- .../experimental/marked_refinement.cpp | 248 ------ src/feature/experimental/newton.cpp | 411 ---------- src/feature/experimental/patches.cpp | 59 -- .../experimental/penner_optimization.cpp | 111 --- src/feature/experimental/polygon_cones.cpp | 72 -- src/feature/experimental/transfer.cpp | 221 ------ src/feature/interface.cpp | 1 - src/metric/common.cpp | 64 -- src/optimization/CMakeLists.txt | 2 - .../metric_optimization/convergence.cpp | 218 ------ .../explicit_optimization.cpp | 2 +- .../implicit_optimization.cpp | 36 + src/optimization/pybind.cpp | 19 - src/optimization/tests/test_area.cpp | 2 +- src/optimization/tests/test_energies.cpp | 1 - src/optimization/tests/test_optimize.cpp | 1 - src/optimization/tests/test_refinement.cpp | 1 - src/optimization/tests/test_regression.cpp | 1 - src/optimization/tests/test_shear.cpp | 2 +- src/optimization/util/visualization.cc | 88 --- src/parametrization/refinement.cpp | 5 +- src/util/linear_algebra.cpp | 27 + src/{feature => }/util/union_find.cpp | 0 src/util/vector.cpp | 11 + src/util/vf_mesh.cpp | 1 + 90 files changed, 343 insertions(+), 3926 deletions(-) delete mode 100644 include/metric/metric/common.h delete mode 100644 include/optimization/optimization/metric_optimization/convergence.h delete mode 100644 include/optimization/optimization/util/visualization.h rename include/{feature/feature => util}/util/union_find.h (98%) delete mode 100644 src/feature/experimental/boundary_constraint_generator.cpp delete mode 100644 src/feature/experimental/boundary_constraint_validator.cpp delete mode 100644 src/feature/experimental/depracated.cpp delete mode 100644 src/feature/experimental/energy.cpp delete mode 100644 src/feature/experimental/junctions.cpp delete mode 100644 src/feature/experimental/marked_refinement.cpp delete mode 100644 src/feature/experimental/newton.cpp delete mode 100644 src/feature/experimental/patches.cpp delete mode 100644 src/feature/experimental/penner_optimization.cpp delete mode 100644 src/feature/experimental/polygon_cones.cpp delete mode 100644 src/feature/experimental/transfer.cpp delete mode 100644 src/metric/common.cpp delete mode 100644 src/optimization/metric_optimization/convergence.cpp delete mode 100644 src/optimization/util/visualization.cc rename src/{feature => }/util/union_find.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fd4a2c..cc880af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,22 +144,10 @@ target_compile_features(DirectionalLib INTERFACE cxx_std_20) # Optionally create rendering library if(RENDER_TEXTURE) - # Build core rendering library - # TODO: Move somewhere reasonable - add_library(rendering - src/optimization/util/visualization.cc - ) - target_include_directories(rendering PUBLIC include/optimization/optimization/util) - target_link_libraries(rendering PUBLIC - igl::core + set(RENDER_LIBRARIES igl::glfw plot ) - - # Change rendering libraries from null to singleton - set(RENDER_LIBRARIES - rendering - ) endif() # Optionally enable polyscope visualization diff --git a/README.md b/README.md index e7afa80..d12b4e6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,30 @@ -# Metric Optimization in Penner Coordinates +# Penner Coordinate Methods for Robust Parametrization -Ryan Capouellez1, Denis Zorin1 -1New York University +An implementation of the Penner Coordinate methods for seamless parametrization, with optional feature alignment constraints and metric optimization, described in the following papers: +- [Metric Optimization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3618394). +- [Seamless Parametrization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3658202). +- [Feature-Aligned Parametrization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3731216). -An implementation of [Metric Optimization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3618394). - -![Parameterization with interpolation](media/teaser.jpg) ### Overview -This method generates an approximately isometric parameterization of an input `obj` mesh with parametric cone angle constraints. Retriangulation is often necessary to satisfy these constraints, so the initial mesh is intrinsically refined to produce an output mesh with a compatible parameterization. +This library provides support for generating approximately isometric parameterization of an input triangle mesh with various constraints, including cone angle, seamless, and feature-alignment constraints. + +Retriangulation is often necessary to satisfy the constraints supported by this library, and in fact this retriangulation is key to the robustness of Penner coordinate methods. When necessary, the initial mesh is intrinsically refined to produce an output mesh with a compatible parameterization. + +The core library provides support for vertex cone angle constraints. The following modules support for additional constraints: +- `Holonomy`: Support for seamless constraints for arbitrary holonomy signatures (see [Seamless Parametrization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3658202)) +- `Feature`: Support for feature alignment constraints (see [Feature-Aligned Parametrization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3731216)) + +The holonomy signature for seamless constraints are generally inferred from a guiding cross-field. A field can be loaded from file, or generated using the following module: +- `Field`: Support for smooth, curvature-aligned cross-field generation + +Penner coordinate methods are primarily useful for robustly generating an initial feasible parameterization satisfying constraints. To optimize this parametrization while preserving constraints, use the following modules: +- `Optimization`: Intrinsic metric optimization in terms of Penner coordinates (See [Metric Optimization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3618394)) +- `SymmetricDirichlet`: $uv$ coordinate optimization for symmetric Dirichlet with field alignment + +Warning: Most of the library is released under a permissive MPL2 license, but `SymmetricDirichlet` and `Field` depend less permissive copyleft licenses. To disable these dependencies, build with the option `-DUSE-COPYLEFT=OFF`. ## Installation @@ -25,133 +39,63 @@ cmake -DCMAKE_BUILD_TYPE=Release .. make -j 4 ``` -## Usage - -The core parameterization method is `bin/optimize_metric`. This executable takes the following arguments: - -|flag | description| -| --- | --- | -|`mesh` | Mesh filepath| -|`cones` | Target cone filepath| -|`energy` | Energy to optimize| -|`direction` | Direction to use for descent| -|`--num_iter` | Maximum number of iterations| -|`--output` | Output directory| -|`--show_parameterization` | Open polyscope viewer for the output| - -Supported parameter values for `energy` and `direction` are listed by `bin/optimize_metric --help`. - -The input mesh must be a manifold surface with a single connected component. The cone file must be a list of newline separated target vertex cone angles satisfying the discrete Gauss-Bonnet condition. Methods to generate such a cone prescription will be provided soon. The output is a refined mesh with a parameterization and a file of metric coordinate values. - -We also provide the executable `bin/optimize_shear` for generating parameterizations using explicit shear coordinate optimization (see paper for details). The executable arguments and output are the same, but the allowed directions are different. This method is in practice much slower to converge than the metric optimization method, but it does have standard formal convergence guarantees. - -## Figure Reproduction - -Scripts to generate the figures of "Metric Optimization in Penner Coordinates" are included in `figures`. - -![Some example figures](media/examples.jpg) - -The models (with cone angles) and cameras used in [Metric Optimization in Penner Coordinates](https://dl.acm.org/doi/10.1145/3618394) necessary for these scripts can be downloaded [here](https://cims.nyu.edu/gcl/papers/2021-Conformal.zip); `MPZ_closed`, `MPZ_open`, `MPZ_cut`, and `cameras` must be copied to `data/closed-Myles`, `data/open-Myles`, `data/cut-Myles`, and `data/cameras` respectively. - -A Conda environment must be activated (before compiling the code) with -``` -conda env create -f environment.yml -conda activate penner-optimization -``` -The figure bash scripts can then be run independently or in batch with -``` -bash fig-all.sh -``` - -Note that most bash scripts generate an output directory with a JSON file specifying parameters for the parameterization and rendering pipeline python script `scripts/_pipeline.py`. Such JSON files can also be used for general batch parameterization and analysis. - -### Library - -Many parametrization and mapping-related problems in geometry processing can be viewed as metric optimization problems, i.e., computing a metric minimizing a functional and satisfying a set of constraints, such as flatness. - -Penner coordinates are global coordinates on the space of metrics on meshes with a fixed vertex set and topology, but varying connectivity, making it homeomorphic to the Euclidean space of dimension equal to the number of edges in the mesh, without any additional constraints imposed. - -Crucially for practical applications, there is an efficient algorithm to convert these abstract Penner coordinates to a more familiar representation of a metric: a mesh with standard Euclidean logarithmic edge lengths. Moreover, this mesh only differs from the input mesh by a finite sequence of edge flips, and the resulting edge lengths are analytic functions of the Penner coordinates. We can thus formulate many constraints, such as vertex cone angles, on the mesh with Euclidean edge lengths as differentiable functions of Penner coordinates. - -To engender future work in this exciting direction, we provide a library `PennerOptimizationLib` containing: - -1. A representation of a mesh with Penner coordinates and cone angle constraints that supports: - 1. the computation of the corresponding mesh with Euclidean logarithmic edge lengths and the Jacobian of the logarithmic edge lengths with respect to Penner coordinates - 2. conformal projection to the cone angle constraints -2. Various energy functionals and constrained optimization methods for meshes with Penner coordinates -3. Layout and refinement methods to generate a parameterization for a mesh from arbitrary Penner coordinates, i.e., the parameterization is an embedding in the uv plane of the metric defined by the Penner coordinate. - ### Library Structure -- Field - - Cross Field: Methods for generating cross fields as represented by four rotationally symmetric tangent vectors. - - Facet Field: Methods for generating cross fields using legacy facet field file representations. - - Field: Methods to generate field directions and cones from an nrosy field. (TODO: Move rosy and combing field functions here) - - Forms: Methods for creating and manipulating one forms on a surface, represented as anti-symmetric halfedge values. - - Frame Field: TODO: move assorted functions here to appropriate locations - - Intrinsic Field: TODO: Needs cleanup -- Optimization - - Core: Underlying methods for differentiable intrinsic metrics, including (a) area and angle values, and (b) length, Penner, shear, and conformal coordinates. - - Area: Methods to compute the area of triangles from intrinsic lengths - - Common: TODO - - Cone Metric: Core representation of a differentiable intrinsic metric on an underlying halfedge mesh - - Constraint: Methods to compute triangle inequality and differentiable cone angle constraints. - - Flip Matrix Generator: Data structure to iteratively compute the change of coordinate matrix for intrinsic flips - - Globals: (TODO: Put structs in relevant files) - - Projection: Methods to project a metric conformally to cone constraints, and to project metric coordinate vectors to the tangent space of the constraint submanifold. - - Reparametrization: Methods for changing edge and interior barycentric coordinates as determined by hyperbolic edge translations. (TODO: This name is pretty confusing in a parameterization library. Change to something better like `barycentric`, and move to parameterization) - - Shear: Methods to compute the shear coordinates of a metric from Penner coordinates and shear coordinate bases, linearly independent from the conformal scaling space. - - Metric Optimization: Methods for optimizing distortion energies for an intrinsic metric with constraints - - Convergence: (TODO: a single function; move somewhere else) - - Energies: Differentiable per-face energy functions with gradients for metric optimization - - Energy Functor: Class for differentiable distortion measures supporting evaluation, gradients, and Hessians - - Energy Weights: Methods to scale per-element values by element weights - - Explicit Optimization: Methods to optimize a metric with angle constraints using an explicit basis complimentary to the conformal scaling space. - - Implicit Optimization: Method to optimize distortion with constraints using a walk-on-manifold approach in a full space of metric coordinates using projection - - Nonlinear Optimization: Methods to perform advanced nonlinear optimization, including conjugate gradient and L-BFGS-B - - Parameterization: (TODO: move to top level. This is fundamental and common code.) - - Interpolation: Data structures to compute pointwise maps between two intrinsic metrics. - - Layout: Methods to layout a uv parameterization determined by intrinsic lengths (TODO: cleanup and documentation) - - Refinement: Methods to refine a triangulation with an accompanying overlay layout sufficiently to ensure the parametrization does not have inverted elements. (TODO: replace `refinement` with `simplification` or something of this sort; check what is in the paper) - - Translation: Method to compute hyperbolic translations determining a continuous map between two intrinsic metrics - - Triangulation: Methods for triangulating self overlapping polygons in the plane - - Util: TODO: Mostly utility for unit tests and validation. Redistribute this code. +- Metric: Underlying methods for differentiable intrinsic metrics, including (a) area and angle values, and (b) length, Penner, shear, and conformal coordinates. + - Area: Methods to compute the area of triangles from intrinsic lengths + - Cone Metric: Core representation of a differentiable intrinsic metric on an underlying halfedge mesh + - Constraint: Methods to compute triangle inequality and differentiable cone angle constraints. + - Flip Matrix Generator: Data structure to iteratively compute the change of coordinate matrix for intrinsic flips + - Globals: Global parameters + - Projection: Methods to project a metric conformally to cone constraints, and to project metric coordinate vectors to the tangent space of the constraint submanifold. + - Reparametrization: Methods for changing edge and interior barycentric coordinates as determined by hyperbolic edge translations. + - Shear: Methods to compute the shear coordinates of a metric from Penner coordinates and shear coordinate bases, linearly independent from the conformal scaling space. +- Parametrization: Methods to generate a UV coordinate map from an intrinsic metric + - Interpolation: Data structures to compute pointwise maps between two intrinsic metrics. + - Layout: Methods to layout a uv parameterization determined by intrinsic lengths + - Parametrize: Methods to build a parameterization for a halfedge mesh with the original embedding geometry and a target intrinsic metric + - Refinement: Methods to refine a triangulation with an accompanying overlay layout sufficiently to ensure the parametrization does not have inverted elements. + - Translation: Method to compute hyperbolic translations determining a continuous map between two intrinsic metrics + - Triangulation: Methods for triangulating self overlapping polygons in the plane - Holonomy: Library for parametrizing surfaces with full seamless constraints with arbitrary holonomy signatures - Core: Basic utilities for tracking dual loops on a surface - Boundary Basis: Basis loops for boundary alignment constraints - Common: Assorted utilities - - Dual Lengths: Compute lengths of dual edges on a surface (TODO: move) + - Dual Lengths: Compute lengths of dual edges on a surface - Dual Loop: Data structures for representing dual loops on a surface with efficient queries of edge and path intersections - Dual Segment: Minimal representation of dual paths on a surface as list of atomic face crossings - Homology Basis: Construct homology basis loops for a surface. - - Quality: Compute triangle quality measures on a mesh (TODO: move) + - Quality: Compute triangle quality measures on a mesh - Viewer: Assorted viewers - Holonomy: - - Cones: Compute cones, as well as related queries and modification heuristics (TODO: move to field, and consolidate cone code here) + - Cones: Compute cones, as well as related queries and modification heuristics - Constraints: Holonomy angle constraints, including cone and dual loop constraints. - Holonomy: Compute holonomy of dual loops on a surface with metric - Marked Penner Cone Metric: Differentiable cone metric with full holonomy signature constraints - Newton: Modified least squares Newton method for holonomy angle constraints - - Rotation Form: Method to compute the rotation of a field across edges from a cross field (TODO: integrate with field code) - - Similarity: TODO. It makes sense to keep this here, since it can satisfy full holonomy signature constraints, but is not integral to the codebase + - Rotation Form: Method to compute the rotation of a field across edges from a cross field + - Similarity: Methods to compute a parametrization satisfying arbitrary holonomy constraints with scale jumps across seams + - Conformal: Method to find a conformally equivalent similarity metric satisfying holonomy constraints + - Constraint: Similarity structure constraints for holonomy signatures, including vertex angle constraints, dual loop holonomy constraints, and closed form constraints + - Layout: Method to parametrize a similarity metric + - Similarity: Representation for a mesh with a differentiable intrinsic metric and additional conformal scaling by an integrated harmonic one form, which is sufficient for arbitrary holonomy constraints - Feature: Library for parametrizing surfaces with both seamless and feature alignment constraints - Core - Boundary Path: Data structure for tracking the line of symmetry between two boundary vertices - - Common: TODO + - Common: Assorted Utilities - Component Mesh: Methods and data structures for splitting a disconnected mesh into separate components, as well as corresponding reindexing maps. - IO: Methods to write/read edges to/from file. - - Quads: Methods to compute relevant quad mesh statistics (TODO: Only contains valence computation; Unify with other quad related code) + - Quads: Methods to compute relevant quad mesh statistics - Union Meshes: Methods to combine collections of meshes into a single mesh with multiple components. - VF Corners: Methods for representing mesh edges using opposite corners. - Viewer: Assorted viewers - - Dirichlet: Methods for enforcing hard and optimizing soft feature alignment constraints (TODO: consider renaming) + - Dirichlet: Methods for enforcing hard and optimizing soft feature alignment constraints - Angle Constraint Relaxer: Method to compute a relaxed angle constraint system that maintains total vertex angles while relaxing feature edge alignment. - Cone Perturber: Data structure to modify sector angle constraints for feature alignment - Constraints: Methods to compute feature aligned seamless constraints and associated Jacobians - - Dirichlet Penner Cone Metric: Extension of the differentiable cone metric using Penner coordinates to include full feature aligned seamless constraints (TODO: finish documenting) + - Dirichlet Penner Cone Metric: Extension of the differentiable cone metric using Penner coordinates to include full feature aligned seamless constraints - Optimization: Two-phase optimization method for alignment of relaxed angle constraint - - Experimental: (TODO: Remove) - - Feature: Methods to find + - Feature: Methods to build a salient feature graph on a mesh. - Error: Methods to compute feature alignment error - Features: Method to find feature edges on a surface and hard feature constraint subsets - Gluing: Methods to get cut mesh edge and vertex correspondences @@ -160,27 +104,30 @@ To engender future work in this exciting direction, we provide a library `Penner - Cut Metric Generator: Data structure to generate fields and differentiable metrics for a mesh cut along feature lines. - Refinement: Data structure with limited support to refine the faces and edges of a mesh for feature alignment - Stitching: Method to stitch a mesh cut along feature lines and parametrized, possibly with edge refinement, into a single parametrized mesh. - - Util: Union Find: (TODO: Move to global utility) - -## Citation - -``` -@article{capouellez:2023:penner, -author = {Capouellez, Ryan and Zorin, Denis}, -title = {Metric Optimization in Penner Coordinates}, -year = {2023}, -issue_date = {December 2023}, -publisher = {Association for Computing Machinery}, -address = {New York, NY, USA}, -volume = {42}, -number = {6}, -issn = {0730-0301}, -url = {https://doi.org/10.1145/3618394}, -doi = {10.1145/3618394}, -journal = {ACM Trans. Graph.}, -month = {dec}, -articleno = {234}, -numpages = {19}, -keywords = {cone metrics, conformal mapping, discrete metrics, intrinsic triangulation, parametrization, penner coordinates} -} -``` +- Optimization + - Metric Optimization: Methods for optimizing distortion energies for an intrinsic metric with constraints + - Energies: Differentiable per-face energy functions with gradients for metric optimization + - Energy Functor: Class for differentiable distortion measures supporting evaluation, gradients, and Hessians + - Energy Weights: Methods to scale per-element values by element weights + - Explicit Optimization: Methods to optimize a metric with angle constraints using an explicit basis complimentary to the conformal scaling space. + - Implicit Optimization: Method to optimize distortion with constraints using a walk-on-manifold approach in a full space of metric coordinates using projection + - Nonlinear Optimization: Methods to perform advanced nonlinear optimization, including conjugate gradient and L-BFGS-B + - Util: Utility for tests and validation +- Field + - Cross Field: Methods for generating cross fields as represented by four rotationally symmetric tangent vectors. + - Facet Field: Methods for generating cross fields using legacy facet field file representations. + - Field: Methods to generate field directions and cones from an nrosy field. + - Forms: Methods for creating and manipulating one forms on a surface, represented as anti-symmetric halfedge values. + - Frame Field: TODO: move assorted functions here to appropriate locations + - Intrinsic Field: Methods to generate a smooth cross field on a halfedge mesh using only the intrinsic metric, with curvature alignment for extrinsic geoemtry +- Util: + - Boundary: Methods to find and navigate the boundary elements of a halfedge mesh + - Common: Type definitions and basic utility functions + - Embedding: Generate maps between edges and halfedges, as well as edges and unique representative edges in doubled symmetric meshes + - IO: Methods to read and write vectors, matrices, and meshes to and from file + - Linear Algebra: Basic linear algebra routines, such as dot products, cross products, and linear solves + - Map: Methods for composing, reindexing, permuting, and combining maps + - Spanning Tree: Methods to build spanning trees and cotrees, either dual or primal, on a halfedge mesh + - Union Find: Union Find data structure +- Vector: Methods to query, convert, and fill vectors of data +- VF Mesh: Methods to process a VF mesh representation \ No newline at end of file diff --git a/include/feature/feature/core/component_mesh.h b/include/feature/feature/core/component_mesh.h index e5b489a..348ea2d 100644 --- a/include/feature/feature/core/component_mesh.h +++ b/include/feature/feature/core/component_mesh.h @@ -12,7 +12,7 @@ #include "feature/core/common.h" #include "feature/core/union_meshes.h" -#include "feature/util/union_find.h" +#include "util/union_find.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" /** diff --git a/include/feature/feature/core/union_meshes.h b/include/feature/feature/core/union_meshes.h index f0791bc..f2cb74b 100644 --- a/include/feature/feature/core/union_meshes.h +++ b/include/feature/feature/core/union_meshes.h @@ -12,7 +12,7 @@ #include "feature/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "feature/util/union_find.h" +#include "util/union_find.h" /** * @brief Methods to combine collections of meshes into a single mesh with multiple components. diff --git a/include/feature/feature/experimental/misc.h b/include/feature/feature/experimental/misc.h index 23f306b..a55db35 100644 --- a/include/feature/feature/experimental/misc.h +++ b/include/feature/feature/experimental/misc.h @@ -6,6 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +#include "util/common.h" struct ParametrizedMesh { diff --git a/include/feature/feature/experimental/transfer.h b/include/feature/feature/experimental/transfer.h index 19b0559..410a724 100644 --- a/include/feature/feature/experimental/transfer.h +++ b/include/feature/feature/experimental/transfer.h @@ -14,7 +14,7 @@ #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/interface.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "feature/util/union_find.h" +#include "util/union_find.h" namespace Penner { namespace Feature { diff --git a/include/feature/feature/feature/features.h b/include/feature/feature/feature/features.h index 77ef349..06ffc0a 100644 --- a/include/feature/feature/feature/features.h +++ b/include/feature/feature/feature/features.h @@ -11,7 +11,7 @@ #include "feature/core/common.h" -#include "feature/util/union_find.h" +#include "util/union_find.h" #include "feature/core/vf_corners.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" diff --git a/include/feature/feature/pybind.h b/include/feature/feature/pybind.h index 12d53e8..356e696 100644 --- a/include/feature/feature/pybind.h +++ b/include/feature/feature/pybind.h @@ -7,6 +7,9 @@ // obtain one at http://mozilla.org/MPL/2.0/. #pragma once + +#include "util/common.h" + #ifdef PYBIND #ifndef MULTIPRECISION diff --git a/include/field/field/cross_field.h b/include/field/field/cross_field.h index a2e8837..d0755a3 100644 --- a/include/field/field/cross_field.h +++ b/include/field/field/cross_field.h @@ -9,7 +9,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" /** * @brief Methods for generating cross fields as represented by four rotationally symmetric diff --git a/include/field/field/facet_field.h b/include/field/field/facet_field.h index a484a3a..f2aa422 100644 --- a/include/field/field/facet_field.h +++ b/include/field/field/facet_field.h @@ -9,7 +9,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" namespace Penner { namespace Field { diff --git a/include/field/field/field.h b/include/field/field/field.h index 874cd5e..5e08c2e 100644 --- a/include/field/field/field.h +++ b/include/field/field/field.h @@ -8,7 +8,8 @@ #pragma once -#include "metric/common.h" + +#include "util/common.h" /** * @brief Methods to generate field directions and cones from an nrosy field. diff --git a/include/field/field/forms.h b/include/field/field/forms.h index 6f49558..bd10b0e 100644 --- a/include/field/field/forms.h +++ b/include/field/field/forms.h @@ -8,7 +8,6 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" /** diff --git a/include/field/field/intrinsic_field.h b/include/field/field/intrinsic_field.h index 92ae87c..9652d9b 100644 --- a/include/field/field/intrinsic_field.h +++ b/include/field/field/intrinsic_field.h @@ -6,9 +6,14 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to generate a smooth cross field on a halfedge mesh using only + * the intrinsic metric, with curvature alignment for extrinsic geoemtry + * + */ + #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" // TODO: This needs a lot of cleanup. To make it clean: diff --git a/include/field/field/rounder.h b/include/field/field/rounder.h index aef2056..9cce062 100644 --- a/include/field/field/rounder.h +++ b/include/field/field/rounder.h @@ -8,8 +8,8 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" +#include "util/common.h" #include "util/vector.h" #include "util/map.h" diff --git a/include/holonomy/holonomy/core/common.h b/include/holonomy/holonomy/core/common.h index 4b48aea..f750262 100644 --- a/include/holonomy/holonomy/core/common.h +++ b/include/holonomy/holonomy/core/common.h @@ -17,7 +17,6 @@ #include #include "optimization/interface.h" -#include "metric/common.h" #include "metric/cone_metric.h" #include "util/vector.h" diff --git a/include/holonomy/holonomy/pybind.h b/include/holonomy/holonomy/pybind.h index 19b7eee..e33c6bf 100644 --- a/include/holonomy/holonomy/pybind.h +++ b/include/holonomy/holonomy/pybind.h @@ -7,6 +7,9 @@ // obtain one at http://mozilla.org/MPL/2.0/. #pragma once + +#include "util/common.h" + #ifdef PYBIND #ifndef MULTIPRECISION diff --git a/include/holonomy/holonomy/similarity/conformal.h b/include/holonomy/holonomy/similarity/conformal.h index 6e9eca7..a845080 100644 --- a/include/holonomy/holonomy/similarity/conformal.h +++ b/include/holonomy/holonomy/similarity/conformal.h @@ -6,6 +6,12 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Method to find a conformally equivalent similarity metric satisfying + * holonomy constraints. + * + */ + #pragma once #include "holonomy/core/common.h" diff --git a/include/holonomy/holonomy/similarity/constraint.h b/include/holonomy/holonomy/similarity/constraint.h index 3be7a55..a363f89 100644 --- a/include/holonomy/holonomy/similarity/constraint.h +++ b/include/holonomy/holonomy/similarity/constraint.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Similarity structue constraints for holonomy signatures, including vertex + * angle constraints, dual loop holonomy constraints, and closed form constraints. + * + */ #pragma once diff --git a/include/holonomy/holonomy/similarity/energy.h b/include/holonomy/holonomy/similarity/energy.h index 126728a..154843d 100644 --- a/include/holonomy/holonomy/similarity/energy.h +++ b/include/holonomy/holonomy/similarity/energy.h @@ -6,6 +6,12 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Various Penner coordinate energies to optimize for a similarity metric, + * including distortion of the scaled metric and the scale jumps across edges + * + */ + #pragma once #include "holonomy/core/common.h" diff --git a/include/holonomy/holonomy/similarity/layout.h b/include/holonomy/holonomy/similarity/layout.h index a29af77..35252dd 100644 --- a/include/holonomy/holonomy/similarity/layout.h +++ b/include/holonomy/holonomy/similarity/layout.h @@ -6,6 +6,10 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Method to parametrize a similarity metric. + * + */ #pragma once diff --git a/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h b/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h index cad1ed8..30fbb7f 100644 --- a/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h +++ b/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h @@ -12,6 +12,13 @@ #include "field/forms.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" +/** + * @brief Representation for a mesh with a differentiable intrinsic metric and additional + * conformal scaling by an integrated harmonic one form, which is sufficient for arbitrary + * holonomy constraints + * + */ + namespace Penner { namespace Holonomy { diff --git a/include/metric/metric/area.h b/include/metric/metric/area.h index 03a4e4b..f1347c8 100644 --- a/include/metric/metric/area.h +++ b/include/metric/metric/area.h @@ -14,7 +14,6 @@ * */ -#include "metric/common.h" #include "metric/cone_metric.h" namespace Penner { diff --git a/include/metric/metric/common.h b/include/metric/metric/common.h deleted file mode 100644 index 4c7814a..0000000 --- a/include/metric/metric/common.h +++ /dev/null @@ -1,54 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -/** - * @brief Assorted utility functions. - * - */ - -#include "util/common.h" -#include "metric/globals.h" - -#include -#include - -#include -#include - -namespace Penner { - -// Check if two vectors are component-wise equal, up to a tolerance. -// -/// @param[in] v: first vector to compare -/// @param[in] w: second vector to compare -/// @param[in] eps: tolerance for equality -/// @return true iff ||v - i||_inf < eps -bool vector_equal(VectorX v, VectorX w, Scalar eps = 1e-10); - -/// Check if a matrix contains a nan -/// -/// @param[in] mat: matrix to check -/// @return true iff mat contains a nan -bool matrix_contains_nan(const Eigen::MatrixXd& mat); - -// Compute the sup norm of a vector. -// -/// @param[in] v: vector -/// @return sup norm of v -Scalar sup_norm(const VectorX& v); - -// Compute the sup norm of a matrix. -// -/// @param[in] matrix: matrix -/// @return sup norm of the matrix -Scalar matrix_sup_norm(const MatrixX& matrix); - - -} // namespace Penner \ No newline at end of file diff --git a/include/metric/metric/cone_metric.h b/include/metric/metric/cone_metric.h index ae1d1d5..ba22ea7 100644 --- a/include/metric/metric/cone_metric.h +++ b/include/metric/metric/cone_metric.h @@ -10,8 +10,9 @@ #include +#include "util/common.h" #include "util/embedding.h" -#include "metric/common.h" +#include "metric/globals.h" #include "metric/flip_matrix_generator.h" /** diff --git a/include/metric/metric/constraint.h b/include/metric/metric/constraint.h index 6d02a53..7bbe2da 100644 --- a/include/metric/metric/constraint.h +++ b/include/metric/metric/constraint.h @@ -8,7 +8,6 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" /** diff --git a/include/metric/metric/flip_matrix_generator.h b/include/metric/metric/flip_matrix_generator.h index d8e4ab7..c5fc77b 100644 --- a/include/metric/metric/flip_matrix_generator.h +++ b/include/metric/metric/flip_matrix_generator.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" /** * @brief Data structure to iteratively compute the change of coordinate matrix for intrinsic flips, e.g., in Penner coordinates. diff --git a/include/metric/metric/projection.h b/include/metric/metric/projection.h index da5415a..e0f7814 100644 --- a/include/metric/metric/projection.h +++ b/include/metric/metric/projection.h @@ -14,7 +14,6 @@ * */ -#include "metric/common.h" #include "metric/cone_metric.h" namespace Penner { diff --git a/include/metric/metric/reparametrization.h b/include/metric/metric/reparametrization.h index 31778c5..924a0c7 100644 --- a/include/metric/metric/reparametrization.h +++ b/include/metric/metric/reparametrization.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" /** * @brief Methods for changing edge and interior barycentric coordinates as determined diff --git a/include/metric/metric/shear.h b/include/metric/metric/shear.h index dfe7882..781b5a7 100644 --- a/include/metric/metric/shear.h +++ b/include/metric/metric/shear.h @@ -8,7 +8,6 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" /** diff --git a/include/metric/metric/viewer.h b/include/metric/metric/viewer.h index f54b7ce..e0fe09d 100644 --- a/include/metric/metric/viewer.h +++ b/include/metric/metric/viewer.h @@ -8,8 +8,7 @@ #pragma once - -#include "metric/common.h" +#include "util/common.h" #ifdef ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" diff --git a/include/optimization/optimization/interface.h b/include/optimization/optimization/interface.h index 11edacc..0c6c119 100644 --- a/include/optimization/optimization/interface.h +++ b/include/optimization/optimization/interface.h @@ -7,7 +7,6 @@ // obtain one at http://mozilla.org/MPL/2.0/. #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" #include "optimization/metric_optimization/energy_functor.h" diff --git a/include/optimization/optimization/metric_optimization/convergence.h b/include/optimization/optimization/metric_optimization/convergence.h deleted file mode 100644 index 35a2b79..0000000 --- a/include/optimization/optimization/metric_optimization/convergence.h +++ /dev/null @@ -1,49 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "metric/common.h" -#include "metric/cone_metric.h" -#include "util/embedding.h" -#include "optimization/metric_optimization/energy_functor.h" -#include "optimization/metric_optimization/implicit_optimization.h" - -/// @file Methods to analyze the convergence of a metric to a global minimum on the -/// constraint surface. -/// -/// TODO: Move elsewhre - -namespace Penner { -namespace Optimization { - -/// Given a metric and direction, compute the energy values for optimization -/// at given (potentially negative) step sizes before and after the projection to the -/// constraint. -/// -/// @param[in] m: input mesh -/// @param[in] opt_energy: optimization energy -/// @param[in] opt_params: parameters for the optimization method -/// @param[in] proj_params: parameters for the projection -/// @param[in] direction: direction for the optimization -/// @param[in] step_sizes: step sizes to compute the energy at -/// @param[out] unprojected_energies: energies at the step sizes before projection -/// @param[out] projected_energies: energies at the step sizes after projection -void compute_direction_energy_values( - const DifferentiableConeMetric& m, - const EnergyFunctor& opt_energy, - std::shared_ptr opt_params, - std::shared_ptr proj_params, - const VectorX& direction, - const VectorX& step_sizes, - VectorX& unprojected_energies, - VectorX& projected_energies); - - -} // namespace Optimization -} // namespace Penner \ No newline at end of file diff --git a/include/optimization/optimization/metric_optimization/energies.h b/include/optimization/optimization/metric_optimization/energies.h index a3d9392..b0dff5e 100644 --- a/include/optimization/optimization/metric_optimization/energies.h +++ b/include/optimization/optimization/metric_optimization/energies.h @@ -8,7 +8,6 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" #include "util/embedding.h" diff --git a/include/optimization/optimization/metric_optimization/energy_functor.h b/include/optimization/optimization/metric_optimization/energy_functor.h index a052c8b..0c49446 100644 --- a/include/optimization/optimization/metric_optimization/energy_functor.h +++ b/include/optimization/optimization/metric_optimization/energy_functor.h @@ -10,7 +10,6 @@ #include "util/embedding.h" #include "util/linear_algebra.h" -#include "metric/common.h" #include "metric/cone_metric.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" diff --git a/include/optimization/optimization/metric_optimization/energy_weights.h b/include/optimization/optimization/metric_optimization/energy_weights.h index a290164..69aa61f 100644 --- a/include/optimization/optimization/metric_optimization/energy_weights.h +++ b/include/optimization/optimization/metric_optimization/energy_weights.h @@ -8,7 +8,6 @@ #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" #include "util/embedding.h" diff --git a/include/optimization/optimization/metric_optimization/explicit_optimization.h b/include/optimization/optimization/metric_optimization/explicit_optimization.h index 6ab23eb..d43d24b 100644 --- a/include/optimization/optimization/metric_optimization/explicit_optimization.h +++ b/include/optimization/optimization/metric_optimization/explicit_optimization.h @@ -9,7 +9,6 @@ #pragma once #include -#include "metric/common.h" #include "util/embedding.h" #include "optimization/metric_optimization/energy_functor.h" #include "optimization/metric_optimization/implicit_optimization.h" diff --git a/include/optimization/optimization/metric_optimization/implicit_optimization.h b/include/optimization/optimization/metric_optimization/implicit_optimization.h index 628e703..cb75e08 100644 --- a/include/optimization/optimization/metric_optimization/implicit_optimization.h +++ b/include/optimization/optimization/metric_optimization/implicit_optimization.h @@ -9,7 +9,6 @@ #pragma once #include -#include "metric/common.h" #include "metric/cone_metric.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" #include "util/embedding.h" @@ -118,6 +117,28 @@ std::unique_ptr optimize_metric( const EnergyFunctor& opt_energy, std::shared_ptr proj_params = nullptr, std::shared_ptr opt_params = nullptr); + +/// Given a metric and direction, compute the energy values for optimization +/// at given (potentially negative) step sizes before and after the projection to the +/// constraint. +/// +/// @param[in] m: input mesh +/// @param[in] opt_energy: optimization energy +/// @param[in] opt_params: parameters for the optimization method +/// @param[in] proj_params: parameters for the projection +/// @param[in] direction: direction for the optimization +/// @param[in] step_sizes: step sizes to compute the energy at +/// @param[out] unprojected_energies: energies at the step sizes before projection +/// @param[out] projected_energies: energies at the step sizes after projection +void compute_direction_energy_values( + const DifferentiableConeMetric& m, + const EnergyFunctor& opt_energy, + std::shared_ptr opt_params, + std::shared_ptr proj_params, + const VectorX& direction, + const VectorX& step_sizes, + VectorX& unprojected_energies, + VectorX& projected_energies); } // namespace Optimization } // namespace Penner \ No newline at end of file diff --git a/include/optimization/optimization/metric_optimization/nonlinear_optimization.h b/include/optimization/optimization/metric_optimization/nonlinear_optimization.h index a721867..182e88a 100644 --- a/include/optimization/optimization/metric_optimization/nonlinear_optimization.h +++ b/include/optimization/optimization/metric_optimization/nonlinear_optimization.h @@ -8,8 +8,8 @@ #pragma once +#include "util/common.h" #include -#include "metric/common.h" /// @file Methods to perform advanced nonlinear optimization, including conjugate gradient /// and L-BFGS-B, using the current gradient, previous gradient, and previous descent direction. diff --git a/include/optimization/optimization/pybind.h b/include/optimization/optimization/pybind.h index 3db28f1..df07c06 100644 --- a/include/optimization/optimization/pybind.h +++ b/include/optimization/optimization/pybind.h @@ -7,6 +7,9 @@ // obtain one at http://mozilla.org/MPL/2.0/. #pragma once + +#include "util/common.h" + #ifdef PYBIND #ifndef MULTIPRECISION diff --git a/include/optimization/optimization/util/shapes.h b/include/optimization/optimization/util/shapes.h index 8f1f69f..44558e6 100644 --- a/include/optimization/optimization/util/shapes.h +++ b/include/optimization/optimization/util/shapes.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" // Some good simple tests are simplex embeddings that are natural (one vertex // at the origin and others at unit vectors) with metrics that are uniform diff --git a/include/optimization/optimization/util/validation.h b/include/optimization/optimization/util/validation.h index 71794d2..397eb7b 100644 --- a/include/optimization/optimization/util/validation.h +++ b/include/optimization/optimization/util/validation.h @@ -10,7 +10,6 @@ #pragma once #include #include -#include "metric/common.h" #include "optimization/metric_optimization/energy_functor.h" #include "metric/constraint.h" #include "metric/cone_metric.h" diff --git a/include/optimization/optimization/util/visualization.h b/include/optimization/optimization/util/visualization.h deleted file mode 100644 index 80870a4..0000000 --- a/include/optimization/optimization/util/visualization.h +++ /dev/null @@ -1,46 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include -#include - -#ifdef RENDER_TEXTURE -//#include deprecated -#endif - - -namespace Penner { -namespace Optimization { - -typedef igl::opengl::glfw::Viewer Viewer; - -Viewer generate_mesh_viewer(const Eigen::MatrixXd &v, - const Eigen::MatrixXi &f, - bool show_lines); - -void view_mesh(Viewer viewer); - -void add_normals_to_mesh(Viewer &viewer, - const Eigen::MatrixXd &n); - -void add_shading_to_mesh(Viewer &viewer, - const Eigen::MatrixXd &color, - const Eigen::VectorXd &AO, - double lighting_factor); - -[[deprecated]] -void save_mesh_screen_capture(Viewer &viewer, - std::string image_path, - int width, - int height); - - -} // namespace Optimization -} // namespace Penner \ No newline at end of file diff --git a/include/parametrization/parametrization/interpolation.h b/include/parametrization/parametrization/interpolation.h index 2eba946..ed76c67 100644 --- a/include/parametrization/parametrization/interpolation.h +++ b/include/parametrization/parametrization/interpolation.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" /** diff --git a/include/parametrization/parametrization/layout.h b/include/parametrization/parametrization/layout.h index cd85912..78ee12d 100644 --- a/include/parametrization/parametrization/layout.h +++ b/include/parametrization/parametrization/layout.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" /** diff --git a/include/parametrization/parametrization/parametrize.h b/include/parametrization/parametrization/parametrize.h index 158a1b1..8e29dcd 100644 --- a/include/parametrization/parametrization/parametrize.h +++ b/include/parametrization/parametrization/parametrize.h @@ -6,8 +6,13 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to build a parameterization for a halfedge mesh with the original embedding + * geometry and a target intrinsic metric + * + */ + #pragma once -#include "metric/common.h" #include "metric/cone_metric.h" namespace Penner { diff --git a/include/parametrization/parametrization/refinement.h b/include/parametrization/parametrization/refinement.h index 9cf78fc..ae0360c 100644 --- a/include/parametrization/parametrization/refinement.h +++ b/include/parametrization/parametrization/refinement.h @@ -6,8 +6,8 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +#include "util/common.h" #include -#include "metric/common.h" /// @file refinement.h /// diff --git a/include/parametrization/parametrization/translation.h b/include/parametrization/parametrization/translation.h index fb77734..644af93 100644 --- a/include/parametrization/parametrization/translation.h +++ b/include/parametrization/parametrization/translation.h @@ -8,7 +8,7 @@ #pragma once -#include "metric/common.h" +#include "util/common.h" /** * @brief Method to compute hyperbolic translations determining a continuous map between diff --git a/include/parametrization/parametrization/triangulation.h b/include/parametrization/parametrization/triangulation.h index 0bb7017..07860f2 100644 --- a/include/parametrization/parametrization/triangulation.h +++ b/include/parametrization/parametrization/triangulation.h @@ -6,7 +6,8 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "metric/common.h" +#pragma once +#include "util/common.h" /** * @brief Methods for triangulating self overlapping polygons in the plane. diff --git a/include/util/util/boundary.h b/include/util/util/boundary.h index ad07e19..67a5e26 100644 --- a/include/util/util/boundary.h +++ b/include/util/util/boundary.h @@ -12,6 +12,11 @@ #include "util/vector.h" #include "util/map.h" +/** + * @brief Methods to find and navigate the boundary elements of a halfedge mesh. + * + */ + namespace Penner { /** diff --git a/include/util/util/common.h b/include/util/util/common.h index d7f4af7..4fb33a6 100644 --- a/include/util/util/common.h +++ b/include/util/util/common.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Typedefs and basic utility functions. + * + */ + #pragma once #include diff --git a/include/util/util/embedding.h b/include/util/util/embedding.h index 6eec189..8bf4479 100644 --- a/include/util/util/embedding.h +++ b/include/util/util/embedding.h @@ -6,20 +6,26 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Generate maps between edges and halfedges, as well as edges and unique + * representative edges in doubled symmetric meshes. + * + * Also methods to expand symmetric functions defined on unique representative edges + * embedded in a symmetric mesh to per edge and per halfedge functions on the + * full symmetric mesh. Conventions: + * E denotes an edge in the embedded mesh + * V denotes a vertex in the embedded mesh + * e denotes an edge in the double mesh + * h denotes a halfedge in the double mesh + * v denotes a vertex in the double mesh + */ + #pragma once #include "util/common.h" namespace Penner { -// Methods to expand symmetric functions defined on unique representative edges -// embedded in a symmetric mesh to per edge and per halfedge functions on the -// full symmetric mesh. Conventions: -// E denotes an edge in the embedded mesh -// V denotes a vertex in the embedded mesh -// e denotes an edge in the double mesh -// h denotes a halfedge in the double mesh -// v denotes a vertex in the double mesh /// Data structure to store maps from full doubled mesh halfedge indices to edge indices /// and to embedded mesh edge indices. Also includes lists of free and fixed vertices and diff --git a/include/util/util/io.h b/include/util/util/io.h index 325f55b..c3aa2fa 100644 --- a/include/util/util/io.h +++ b/include/util/util/io.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to read and write vectors, matrices, and meshes to and from file. + * + */ + #pragma once #include "util/common.h" diff --git a/include/util/util/linear_algebra.h b/include/util/util/linear_algebra.h index 97a8abd..0df352b 100644 --- a/include/util/util/linear_algebra.h +++ b/include/util/util/linear_algebra.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Basic linear algebra routines, such as dot products, cross products, and linear solves. + * + */ + #pragma once #include "util/common.h" @@ -44,6 +49,18 @@ VectorType cross_prod(const VectorType& v1, const VectorType& v2) v1[0] * v2[1] - v1[1] * v2[0]); } +// Compute the sup norm of a vector. +// +/// @param[in] v: vector +/// @return sup norm of v +Scalar sup_norm(const VectorX& v); + +// Compute the sup norm of a matrix. +// +/// @param[in] matrix: matrix +/// @return sup norm of the matrix +Scalar matrix_sup_norm(const MatrixX& matrix); + /** * @brief Compute the signed angle between two 3D vectors around a normal axis. * diff --git a/include/util/util/map.h b/include/util/util/map.h index 0a5b099..d6fced2 100644 --- a/include/util/util/map.h +++ b/include/util/util/map.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods for composing, reindexing, permuting, and combining maps. + * + */ + #pragma once #include "util/common.h" diff --git a/include/util/util/spanning_tree.h b/include/util/util/spanning_tree.h index fb159a0..bb10952 100644 --- a/include/util/util/spanning_tree.h +++ b/include/util/util/spanning_tree.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to build spanning trees and cotrees, either dual or primal, on a halfedge mesh. + * + */ + #pragma once #include "util/common.h" diff --git a/include/feature/feature/util/union_find.h b/include/util/util/union_find.h similarity index 98% rename from include/feature/feature/util/union_find.h rename to include/util/util/union_find.h index f8bbf79..864e487 100644 --- a/include/feature/feature/util/union_find.h +++ b/include/util/util/union_find.h @@ -6,6 +6,11 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Union Find data structure. + * + */ + #pragma once #include "feature/core/common.h" @@ -109,8 +114,6 @@ class UnionFind } /** -<<<<<<< HEAD -======= * @brief Get a representative element for each set. * * @return vector of one element per set @@ -133,7 +136,6 @@ class UnionFind } /** ->>>>>>> resolution * @brief Build labels for the set elements. * * @return map from elements to set index diff --git a/include/util/util/vector.h b/include/util/util/vector.h index 314d784..4391ad9 100644 --- a/include/util/util/vector.h +++ b/include/util/util/vector.h @@ -6,6 +6,14 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to query, convert, and fill vectors of data. + * + * Of particular note are methods to convert between lists of indices + * and boolean mask vectors marking indices in the list. + * + */ + #pragma once #include "util/common.h" @@ -98,6 +106,12 @@ int argmin(const VectorType& v) */ bool vector_contains_nan(const VectorX& v); +/// Check if a matrix contains a nan +/// +/// @param[in] mat: matrix to check +/// @return true iff mat contains a nan +bool matrix_contains_nan(const Eigen::MatrixXd& mat); + /** * @brief Convert an Eigen vector to a std vector * @@ -149,11 +163,12 @@ std::vector concatenate_vector(const std::vector& v, const std::vect * @tparam VectorType * @param v: first vector * @param w: second vector + * @param eps: (optional) tolerance for equality * @return true if equal * @return false otherwise */ template -bool vector_equal(const VectorType& v, const VectorType& w) +bool vector_equal(const VectorType& v, const VectorType& w, Scalar eps = 1e-10) { int n = v.size(); int m = w.size(); @@ -164,7 +179,7 @@ bool vector_equal(const VectorType& v, const VectorType& w) // check all entries for (int i = 0; i < n; ++i) { - if (!float_equal(v[i], w[i])) return false; + if (!float_equal(v[i], w[i], eps)) return false; } return true; diff --git a/include/util/util/vf_mesh.h b/include/util/util/vf_mesh.h index 78ac1c4..b5f415f 100644 --- a/include/util/util/vf_mesh.h +++ b/include/util/util/vf_mesh.h @@ -6,6 +6,15 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +/** + * @brief Methods to process a VF mesh representation. + * + * Significant functions include finding and cutting along seam edges, + * reindexing vertices, and building maps from directed edges + * (represented as vertex pairs) to unique halfedge indices + * + */ + #pragma once #include "util/common.h" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 149d634..e22cbc1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,13 +6,13 @@ set(UtilSrc util/linear_algebra.cpp util/map.cpp util/spanning_tree.cpp + util/union_find.cpp util/vector.cpp util/vf_mesh.cpp ) set(MetricSrc metric/area.cpp - metric/common.cpp metric/cone_metric.cpp metric/constraint.cpp metric/flip_matrix_generator.cpp @@ -48,6 +48,7 @@ target_link_libraries(PennerCoreLib PUBLIC HDF5::HDF5 #CoMISo ${MPFR_LIBRARIES} + ${RENDER_LIBRARIES} ${SUITESPARSE_LIBS} ${POLYSCOPE_LIBRARIES} ) @@ -85,7 +86,6 @@ if(FALSE) # Link libraries target_link_libraries(penner PUBLIC PennerLib - ${RENDER_LIBRARIES} ) # Set pybinding settings diff --git a/src/feature/CMakeLists.txt b/src/feature/CMakeLists.txt index 4d4db84..da596bf 100644 --- a/src/feature/CMakeLists.txt +++ b/src/feature/CMakeLists.txt @@ -30,22 +30,6 @@ set(SurgerySrc surgery/stitching.cpp ) -set(FeatureUtilSrc - util/union_find.cpp -) - -set(FeatureExperimentalSrc - experimental/boundary_constraint_generator.cpp - experimental/boundary_constraint_validator.cpp - #experimental/newton.cpp - experimental/patches.cpp - experimental/transfer.cpp - experimental/energy.cpp - experimental/junctions.cpp - experimental/polygon_cones.cpp - experimental/marked_refinement.cpp -) - add_library(PennerFeatureLib interface.cpp pybind.cpp @@ -53,8 +37,6 @@ add_library(PennerFeatureLib ${DirichletSrc} ${FeatureSrc} ${SurgerySrc} - ${FeatureUtilSrc} - ${FeatureExperimentalSrc} ) target_include_directories(PennerFeatureLib PUBLIC ../../include/feature) target_link_libraries(PennerFeatureLib PUBLIC diff --git a/src/feature/experimental/boundary_constraint_generator.cpp b/src/feature/experimental/boundary_constraint_generator.cpp deleted file mode 100644 index cf6d7f1..0000000 --- a/src/feature/experimental/boundary_constraint_generator.cpp +++ /dev/null @@ -1,381 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/boundary_constraint_generator.h" - -#include "util/boundary.h" -#include "holonomy/core/viewer.h" -#include "feature/feature/features.h" -#include "feature/core/component_mesh.h" -#include "feature/feature/gluing.h" - -#ifdef ENABLE_VISUALIZATION -#include "polyscope/curve_network.h" -#include "polyscope/point_cloud.h" -#endif - -namespace Penner { -namespace Feature { - -BoundaryConstraintGenerator::BoundaryConstraintGenerator(Mesh m) - : m_mesh(m) - , m_out({}) -{ - // find starting halfedges for all all boundary components - std::vector boundary_components = find_boundary_components(m); - - // build trivial features - int num_features = boundary_components.size(); - int num_segments = 0; // will find incrementally - int num_halfedges = m.n_halfedges(); - m_feature_start.resize(num_features); - m_feature_end.resize(num_features); - m_next_feature.resize(num_features); - m_prev_feature.resize(num_features); - m_to.resize(num_features); - m_segment.resize(num_halfedges); - for (int i = 0; i < num_features; ++i) { - // build trivial loop - m_next_feature[i] = i; - m_prev_feature[i] = i; - m_to[i] = -1; - - // build feature data - m_feature_start[i] = num_segments; - int h_start = boundary_components[i]; - std::vector boundary_component = build_boundary_component(m, h_start); - for (int h : boundary_component) { - // mark new segment index for the boundary halfedge - m_segment[h] = num_segments; - - // add segment topology data - // WARNING: need to overwrite next and prev later - m_feature.push_back(i); - m_halfedge.push_back(h); - m_next_segment.push_back(num_segments + 1); - m_prev_segment.push_back(num_segments - 1); - m_target_length.push_back(m.l[h]); - m_pair.push_back(-1); - ++num_segments; - } - m_feature_end[i] = num_segments - 1; - - // mark first and last segments as end of chain - m_prev_segment[m_feature_start[i]] = -1; - m_next_segment[m_feature_end[i]] = -1; - } -} - -void BoundaryConstraintGenerator::pair_boundary_edges( - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map) -{ - // generate boundary identification - const auto& m = get_mesh(); - auto [bd_pairs, boundary_he, he2bd] = generate_boundary_pairs(m, vtx_reindex, V_map); - - // pair each boundary segment - for (const auto& bd_pair : bd_pairs) { - int hij = bd_pair.first; - int hji = bd_pair.second; - m_pair[segment(hij)] = segment(hji); - m_pair[segment(hji)] = segment(hij); - } -} - -void BoundaryConstraintGenerator::add_junction_after_segment(int segment_index) -{ - int feature_index = feature(segment_index); - int next_feature_index = next_feature(feature_index); - int next_segment_index = next_segment(segment_index); - int end = feature_end(feature_index); - int to_junction_index = to(feature_index); - - // add junction to trivial loop - if (to_junction_index == -1) { - int new_junction_index = count_junctions(); - m_to[feature_index] = new_junction_index; - m_out.push_back(feature_index); - - int h_start = halfedge(segment_index); - std::vector boundary_component = build_boundary_component(get_mesh(), h_start); - int num_bd = boundary_component.size(); - for (int i = 0; i < num_bd - 1; ++i) { - int hij = boundary_component[i]; - int hjk = boundary_component[i + 1]; - m_next_segment[m_segment[hij]] = m_segment[hjk]; - } - for (int i = 1; i < num_bd + 1; ++i) { - int hli = boundary_component[(i - 1) % num_bd]; - int hij = boundary_component[i % num_bd]; - int hjk = boundary_component[(i + 1) % num_bd]; - m_next_segment[m_segment[hij]] = m_segment[hjk]; - m_prev_segment[m_segment[hij]] = m_segment[hli]; - } - m_feature_start[feature_index] = m_segment[boundary_component[1 % num_bd]]; - m_feature_end[feature_index] = m_segment[boundary_component[0]]; - m_prev_segment[m_feature_start[feature_index]] = -1; - m_next_segment[m_feature_end[feature_index]] = -1; - - return; - } - - // get new indices - int new_feature_index = count_features(); - int new_junction_index = count_junctions(); - - // split segment connectivity - m_next_segment[segment_index] = -1; - m_prev_segment[next_segment_index] = -1; - - // add new feature - m_feature_start.push_back(next_segment_index); - m_feature_end.push_back(end); - m_next_feature.push_back(next_feature_index); - m_prev_feature.push_back(feature_index); - m_to.push_back(to_junction_index); - - // modify current feature - m_feature_end[feature_index] = segment_index; - m_next_feature[feature_index] = new_feature_index; - m_to[feature_index] = new_junction_index; - - // add junction - m_out.push_back(new_feature_index); - - // mark new feature segments - int curr_segment_index = next_segment_index; - m_feature[curr_segment_index] = new_feature_index; - do { - curr_segment_index = m_next_segment[curr_segment_index]; - m_feature[curr_segment_index] = new_feature_index; - } while (curr_segment_index != end); -} - -int BoundaryConstraintGenerator::split_segment(int segment_index) -{ - // get relevant indices - int feature_index = feature(segment_index); - int next_segment_index = next_segment(segment_index); - int end = feature_end(feature_index); - int halfedge_index = halfedge(segment_index); - - // add new segment - int new_segment_index = count_segments(); - m_next_segment[segment_index] = new_segment_index; - m_next_segment.push_back(next_segment_index); - m_prev_segment.push_back(segment_index); - m_halfedge.push_back(halfedge_index); - m_feature.push_back(feature_index); - - // check if end segment has changed - if (end == segment_index) { - m_feature_end[feature_index] = new_segment_index; - } - - return new_segment_index; -} - -void BoundaryConstraintGenerator::mark_cones_as_junctions() -{ - spdlog::debug("Marking cones as junctions"); - const auto& m = get_mesh(); - - int num_segments = count_segments(); - for (int i = 0; i < num_segments; ++i) { - int hij = halfedge(i); // constant even with feature modification - int vj = m.v_rep[m.to[hij]]; - if (!float_equal(m.Th_hat[vj], 2 * M_PI)) { - spdlog::debug("Adding junction after segment {}", i); - add_junction_after_segment(i); - } - } -} - -std::vector BoundaryConstraintGenerator::enumerate_feature_cones(int feature_index) const -{ - spdlog::debug("Enumerating cones on feature {}", feature_index); - const auto& m = get_mesh(); - std::vector cones = {}; - - // iterate over feature, counting cones - SegmentIterator iter = get_feature_iterator(feature_index); - for (; !iter.is_end(); ++iter) { - int hij = *iter; - int vj = m.v_rep[m.to[hij]]; - Scalar cone_angle = m.Th_hat[vj]; - if (!float_equal(cone_angle, 2 * M_PI)) { - cones.push_back(cone_angle); - } - } - - return cones; -} - -std::vector BoundaryConstraintGenerator::enumerate_interior_cones() const -{ - spdlog::debug("Enumerating interior cones"); - const auto& m = get_mesh(); - std::vector cones = {}; - - // mark boundary vertices - int num_vertices = m.n_ind_vertices(); - int num_segments = count_segments(); - std::vector is_boundary_vertex(num_vertices, false); - for (int i = 0; i < num_segments; ++i) { - int hij = halfedge(i); - int vj = m.v_rep[m.to[hij]]; - is_boundary_vertex[vj] = true; - } - - // iterate over vertices, skipping boundary - for (int vi = 0; vi < num_vertices; ++vi) { - if (is_boundary_vertex[vi]) continue; - - Scalar cone_angle = m.Th_hat[vi]; - if (!float_equal(cone_angle, 4 * M_PI)) { - cones.push_back(cone_angle / 2.); - } - } - - return cones; -} - -void BoundaryConstraintGenerator::distribute_feature_length( - int feature_index, - Scalar target_feature_length) -{ - const auto& m = get_mesh(); - SegmentIterator iter = get_feature_iterator(feature_index); - Scalar feature_length = compute_feature_length(feature_index); - for (; !iter.is_end(); ++iter) { - // get length of mesh halfedge corresponding to the segment - int h = *iter; - Scalar target_segment_length = (target_feature_length / feature_length) * m.l[h]; - set_target_length(m_segment[h], target_segment_length); - } -} - -void BoundaryConstraintGenerator::set_uniform_feature_lengths(Scalar target_feature_length) -{ - int num_features = count_features(); - Scalar total_feature_length = 0.; - for (int i = 0; i < num_features; ++i) { - total_feature_length += compute_feature_length(i); - } - if (target_feature_length == 0.) { - target_feature_length = total_feature_length / num_features; - } - for (int i = 0; i < num_features; ++i) { - distribute_feature_length(i, target_feature_length); - } -} - -Scalar BoundaryConstraintGenerator::compute_feature_length(int feature_index) const -{ - // get mesh and iterator for the feature - const auto& m = get_mesh(); - SegmentIterator iter = get_feature_iterator(feature_index); - - // iterate to compute feature length - Scalar feature_length = 0.; - for (; !iter.is_end(); ++iter) { - // get length of mesh halfedge corresponding to the segment - int h = *iter; - feature_length += m.l[h]; - } - - return feature_length; -} - -std::vector BoundaryConstraintGenerator::compute_feature_halfedges(int feature_index) const -{ - std::vector feature_halfedges = {}; - SegmentIterator iter = get_feature_iterator(feature_index); - for (; !iter.is_end(); ++iter) { - // get halfedge corresponding to the segment - int h = *iter; - feature_halfedges.push_back(h); - } - - return feature_halfedges; -} - -void BoundaryConstraintGenerator::view( - const Eigen::MatrixXd& V, - const std::vector& vtx_reindex) -{ - const Mesh& m = get_mesh(); - auto [V_double, F, F_halfedge] = generate_doubled_mesh(V, m, vtx_reindex); - - // build feature coloring - int num_halfedges = m.n_halfedges(); - int num_features = count_features(); - VectorX is_feature_halfedge = VectorX::Zero(num_halfedges); - for (int i = 0; i < num_features; ++i) { - std::vector feature_halfedges = compute_feature_halfedges(i); - for (int h : feature_halfedges) { - is_feature_halfedge(h) = i + 1; - } - } - VectorX FV_feature_halfedges = generate_FV_halfedge_data(F_halfedge, is_feature_halfedge); - - auto [cone_positions, cone_values] = generate_cone_vertices(V, vtx_reindex, m); - -#ifdef ENABLE_VISUALIZATION - polyscope::init(); - - // add mesh - std::string mesh_handle = "boundary constraint generator"; - polyscope::registerSurfaceMesh(mesh_handle, V_double, F); - polyscope::getSurfaceMesh(mesh_handle) - ->addHalfedgeScalarQuantity( - "features", - convert_scalar_to_double_vector(FV_feature_halfedges)) - ->setEnabled(true); - - // add cones - polyscope::registerPointCloud(mesh_handle + "_cones", cone_positions); - polyscope::getPointCloud(mesh_handle + "_cones") - ->addScalarQuantity("index", cone_values) - ->setColorMap("coolwarm") - ->setMapRange({-M_PI, M_PI}) - ->setEnabled(true); - - polyscope::show(); - -#endif -} - -std::tuple BoundaryConstraintGenerator::build_boundary_constraint_system() const -{ - int num_segments = count_segments(); - int num_halfedges = count_halfedges(); - typedef Eigen::Triplet T; - std::vector tripletList; - tripletList.reserve(num_segments); - VectorX ell(num_segments); - for (int i = 0; i < num_segments; ++i) { - int h = halfedge(i); - assert(is_valid_halfedge_index(h)); - tripletList.push_back(T(i, h, 1.)); - ell[i] = 2. * log(get_target_length(i)); - spdlog::debug("Setting constraint {} = {}", i, ell[i]); - } - - // Create the matrix from the triplets - MatrixX system_matrix; - system_matrix.resize(num_segments, num_halfedges); - system_matrix.reserve(tripletList.size()); - system_matrix.setFromTriplets(tripletList.begin(), tripletList.end()); - - return std::make_tuple(system_matrix, ell); -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/boundary_constraint_validator.cpp b/src/feature/experimental/boundary_constraint_validator.cpp deleted file mode 100644 index f2cc1da..0000000 --- a/src/feature/experimental/boundary_constraint_validator.cpp +++ /dev/null @@ -1,496 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/boundary_constraint_validator.h" - -#include "util/boundary.h" -#include "holonomy/core/viewer.h" -#include "feature/feature/features.h" -#include "feature/core/component_mesh.h" - -#ifdef ENABLE_VISUALIZATION -#include "polyscope/curve_network.h" -#include "polyscope/point_cloud.h" -#endif - -namespace Penner { -namespace Feature { - -BoundaryConstraintValidator::BoundaryConstraintValidator(Mesh m) - : BoundaryConstraintGenerator(m) - , m_path_start_segment({}) - , m_path_end_segment({}) - , m_path_reverse_start_segment({}) - , m_path_reverse_end_segment({}) - , m_path_holonomy({}) -{ - m_path = std::vector(count_segments(), -1); - m_reverse_path = std::vector(count_segments(), -1); -} - -void BoundaryConstraintValidator::add_boundary_path_segments( - const std::vector>& basis_loops, - const std::vector& kappa_hat) -{ - spdlog::debug("Marking boundary paths"); - const auto& m = get_mesh(); - int num_basis_loops = basis_loops.size(); - - for (int i = 0; i < num_basis_loops; ++i) { - // get halfedges at start and end of dual path - int start_h = -1; - int end_h = -1; - int current_type = m.type[m.opp[(*basis_loops[i]->begin())[0]]]; - for (const auto& dual_segment : *basis_loops[i]) { - int h0 = dual_segment[0]; - if ((m.type[h0] == 1) && (current_type == 2)) { - start_h = h0; - current_type = 1; - } - if ((m.type[h0] == 2) && (current_type == 1)) { - end_h = m.opp[h0]; - current_type = 2; - } - } - - // skip interior loops - if ((start_h < 0) || (end_h < 0)) continue; - spdlog::debug("Adding loop from {} to {}", start_h, end_h); - - // split segments - // <- s11 - | | <- s10 - - // ^ v - // - s00 -> | | - s01 -> - int s00 = segment(start_h); - int s10 = segment(end_h); - int s01 = split_segment(s00); - int s11 = split_segment(s10); - - // add forward path - m_path[s00] = count_paths(); - m_reverse_path[s10] = count_paths(); - m_path_start_segment.push_back(s00); - m_path_end_segment.push_back(s11); - m_path_reverse_start_segment.push_back(s10); - m_path_reverse_end_segment.push_back(s01); - m_path_holonomy.push_back(kappa_hat[i]); - } -} - -int BoundaryConstraintValidator::split_segment(int segment_index) -{ - m_path.push_back(-1); - m_reverse_path.push_back(-1); - return BoundaryConstraintGenerator::split_segment(segment_index); -} - -void BoundaryConstraintValidator::add_feature( - int feature_index, - Vector2& d, - Scalar& total_curvature, - std::vector>& tripletList) const -{ - spdlog::debug("Feature start direction: ({}, {})", d[0], d[1]); - const Mesh& m = get_mesh(); - Matrix2x2 R; - SegmentIterator iter = get_feature_iterator(feature_index); - for (; !iter.is_end(); ++iter) { - // get edge tangent vector for the current halfedge - int h = *iter; - tripletList.push_back(T(0, segment(h), d[0])); - tripletList.push_back(T(1, segment(h), d[1])); - - // rotate direction by the angle at the tip of the halfedge - int vi = m.to[h]; - Scalar theta = m.Th_hat[m.v_rep[vi]] / 2. - M_PI; - total_curvature += theta; - R = compute_rotation(theta); - d = R * d; - } - - spdlog::debug("Feature end direction: ({}, {})", d[0], d[1]); -} - -void BoundaryConstraintValidator::add_free_connection( - const std::array& virtual_segments, - bool reverse, - Vector2& d, - std::vector& segment_directions, - std::vector& segment_indices) const -{ - spdlog::debug("Connection start direction: ({}, {})", d[0], d[1]); - - // rotate direction by -pi/2 (pointing into interior w/ ccw orientation) - Scalar theta = -M_PI / 2.; - Matrix2x2 R = compute_rotation(theta); - d = R * d; - - if (!reverse) { - spdlog::debug("Adding forward connection"); - for (auto itr = virtual_segments.begin(); itr != virtual_segments.end(); ++itr) { - // add virtual segment length - int segment = *itr; - segment_directions.push_back(d); - segment_indices.push_back(segment); - - // rotate direction by -pi/2 - theta = -M_PI / 2.; - R = compute_rotation(theta); - d = R * d; - } - } else { - spdlog::debug("Adding reverse connection"); - for (auto itr = virtual_segments.rbegin(); itr != virtual_segments.rend(); ++itr) { - // add virtual segment length - int segment = *itr; - segment_directions.push_back(d); - segment_indices.push_back(segment); - - // rotate direction by pi/2 - theta = M_PI / 2.; - R = compute_rotation(theta); - d = R * d; - } - - // correct final direction - theta = M_PI; - R = compute_rotation(theta); - d = R * d; - } - - spdlog::debug("Connection end direction: ({}, {})", d[0], d[1]); -} - -void BoundaryConstraintValidator::add_fixed_connection( - const std::vector& virtual_segments, - bool is_counterclockwise, - bool reverse, - Vector2& d, - std::vector& segment_directions, - std::vector& segment_indices) const -{ - spdlog::debug("Connection start direction: ({}, {})", d[0], d[1]); - - // rotate direction by pi/2 (pointing into interior w/ ccw orientation) - Matrix2x2 R = compute_rotation(M_PI / 2.); - d = R * d; - - // add holonomy rotations - Scalar theta = (is_counterclockwise) ? (M_PI / 2.) : (-M_PI / 2.); - if (!reverse) { - spdlog::debug("Adding forward connection"); - for (auto itr = virtual_segments.begin(); itr != virtual_segments.end(); ++itr) { - // add virtual segment length - int segment = *itr; - segment_directions.push_back(d); - segment_indices.push_back(segment); - - // rotate direction by pi/2 (with orientation determined by the index sign) - R = compute_rotation(theta); - d = R * d; - } - // correct direction - R = compute_rotation(-theta); - d = R * d; - - } else { - spdlog::debug("Adding reverse connection"); - for (auto itr = virtual_segments.rbegin(); itr != virtual_segments.rend(); ++itr) { - // add virtual segment length - int segment = *itr; - segment_directions.push_back(d); - segment_indices.push_back(segment); - - // rotate direction by pi/2 (with reverse orientation of forward connection) - R = compute_rotation(-theta); - d = R * d; - } - - // correct final direction - R = compute_rotation(theta); - d = R * d; - } - - // rotate into interior - R = compute_rotation(M_PI / 2.); - d = R * d; - - spdlog::debug("Connection end direction: ({}, {})", d[0], d[1]); -} - -// helper function for per-component validity checks -// TODO: currently picks an arbitrary component; should do for an indexed component if keep -std::tuple, std::vector> -BoundaryConstraintValidator::build_component_layout() const -{ - const Mesh& m = get_mesh(); - int num_segments = count_segments(); - int num_features = count_features(); - std::vector segment_directions; - std::vector segment_indices; - int variable_count = num_segments; - int num_paths = count_paths(); - spdlog::debug("Adding {} features with {} segments", num_features, num_segments); - - // build virtual segments for paths - std::vector> virtual_segments(num_paths); - for (int i = 0; i < num_paths; ++i) { - int num_turns = (int)(abs(round(path_holonomy(i) / (M_PI / 2.)))); - int num_segments = num_turns + 1; - virtual_segments[i].resize(num_segments); - std::iota(virtual_segments[i].begin(), virtual_segments[i].end(), variable_count); - variable_count += virtual_segments[i].size(); - } - - // initialize direction tracking - Vector2 d = {1, 0}; - Scalar theta; - Matrix2x2 R; - - // iterate from arbitrary starting segment - int start_segment = 0; - int segment_index = start_segment; - do { - // add segment length to constraint - segment_directions.push_back(d); - segment_indices.push_back(segment_index); - - // add forward connection if start segment - if (is_path_start(segment_index)) { - int path_index = path(segment_index); - bool is_counterclockwise = (path_holonomy(path_index) > 0); - bool reverse = false; - add_fixed_connection( - virtual_segments[path_index], - is_counterclockwise, - reverse, - d, - segment_directions, - segment_indices); - segment_index = path_end_segment(path_index); - } - // add reverse connection if reverse start segment - else if (is_reverse_path_start(segment_index)) { - int path_index = reverse_path(segment_index); - bool is_counterclockwise = (path_holonomy(path_index) > 0); - bool reverse = true; - add_fixed_connection( - virtual_segments[path_index], - is_counterclockwise, - reverse, - d, - segment_directions, - segment_indices); - segment_index = path_reverse_end_segment(path_index); - } - // add cone rotation if end of feature - else if (is_feature_end(segment_index)) { - int h = halfedge(segment_index); - int vi = m.to[h]; - theta = m.Th_hat[m.v_rep[vi]] / 2. - M_PI; - R = compute_rotation(theta); - d = R * d; - segment_index = feature_start(next_feature(feature(segment_index))); - } - // just iterate to next segment otherwise - else { - segment_index = next_segment(segment_index); - } - } while (segment_index != start_segment); - - // add interior cones - std::vector interior_cones = enumerate_interior_cones(); - for (Scalar cone_angle : interior_cones) { - // get new virtual segments for cone connection - std::array cone_virtual_segments; - std::iota(cone_virtual_segments.begin(), cone_virtual_segments.end(), variable_count); - variable_count += cone_virtual_segments.size(); - - // add forward connection - add_free_connection(cone_virtual_segments, false, d, segment_directions, segment_indices); - - // rotate direction by the cone angle defect - Scalar theta = cone_angle - (2. * M_PI); - Matrix2x2 R = compute_rotation(theta); - d = R * d; - - // add reverse connection - add_free_connection(cone_virtual_segments, true, d, segment_directions, segment_indices); - } - - return std::make_tuple(variable_count, segment_directions, segment_indices); -} - -void BoundaryConstraintValidator::view_component_layout(const VectorX& ell) const -{ - auto [variable_count, segment_directions, segment_indices] = build_component_layout(); - - // build layout vertices - int num_layout_edges = segment_directions.size(); - Eigen::MatrixXd layout_vertices(num_layout_edges, 2); - Eigen::MatrixXd inflated_vertices(num_layout_edges, 3); - Eigen::MatrixXi layout_edges(num_layout_edges, 2); - Vector2 v = {0, 0}; - for (int i = 0; i < num_layout_edges; ++i) { - // layout current vertex - layout_vertices.row(i) << (double)(v[0]), (double)(v[1]); - inflated_vertices.row(i) << (double)(v[0]), (double)(v[1]), (0.1 * i); - layout_edges.row(i) << i, (i + 1) % num_layout_edges; - - // update v - Vector2 d = segment_directions[i]; - Scalar l = ell[segment_indices[i]]; - v = v + l * d; - } - -#ifdef ENABLE_VISUALIZATION - polyscope::init(); - polyscope::registerPointCloud2D("component vertices", layout_vertices); - polyscope::registerCurveNetwork2D("component layout", layout_vertices, layout_edges); - polyscope::registerCurveNetwork("inflated layout", inflated_vertices, layout_edges); - polyscope::show(); -#endif -} - -std::tuple BoundaryConstraintValidator::build_component_validity_system() const -{ - auto [variable_count, segment_directions, segment_indices] = build_component_layout(); - - // build system validity matrix for the layout - int num_layout_edges = segment_directions.size(); - typedef Eigen::Triplet T; - std::vector tripletList; - tripletList.reserve(num_layout_edges); - for (int i = 0; i < num_layout_edges; ++i) { - for (int j : {0, 1}) { - tripletList.push_back(T(j, segment_indices[i], segment_directions[i][j])); - } - } - - // create the matrix from the triplets - MatrixX system_matrix; - system_matrix.resize(2, variable_count); - system_matrix.reserve(tripletList.size()); - system_matrix.setFromTriplets(tripletList.begin(), tripletList.end()); - - // sum of oriented lengths should be zero - VectorX ell = VectorX::Zero(2); - - return std::make_tuple(system_matrix, ell); -} - -std::tuple BoundaryConstraintValidator::build_boundary_validity_system() const -{ - int num_segments = count_segments(); - int num_features = count_features(); - const Mesh& m = get_mesh(); - typedef Eigen::Triplet T; - std::vector tripletList; - tripletList.reserve(num_segments); - std::vector ell = {}; - int constraint_count = 0; - int variable_count = num_segments; - std::vector is_segment_seen(num_segments, false); - for (int i = 0; i < num_segments; ++i) { - // only process each segment once - if (is_segment_seen[i]) continue; - is_segment_seen[i] = true; - - if (pair(i) >= 0) { - is_segment_seen[pair(i)] = true; // mark pair as seen - - // add equality constraint - tripletList.push_back(T(constraint_count, i, 1.)); - tripletList.push_back(T(constraint_count, pair(i), -1.)); - ell.push_back(0); - ++constraint_count; - spdlog::debug("Setting constraint {} - {} = {}", i, pair(i), ell.back()); - - } else { - tripletList.push_back(T(constraint_count, i, 1.)); - ell.push_back(get_target_length(i)); - ++constraint_count; - spdlog::debug("Setting constraint {} = {}", i, ell.back()); - } - } - - Vector2 d = {1, 0}; - - std::vector is_feature_seen(num_features, false); - for (int i = 0; i < num_features; ++i) { - if (is_feature_seen[i]) continue; // skip seen features - - // iterate over feature component to build polygon closure condition - int feature_index = i; - Scalar total_curvature = 0.; - int h, vi; - Scalar theta = 0.; - Matrix2x2 R; - do { - SegmentIterator iter = get_feature_iterator(feature_index); - for (; !iter.is_end(); ++iter) { - // get edge tangent vector for the current halfedge - h = *iter; - tripletList.push_back(T(constraint_count + 0, segment(h), d[0])); - tripletList.push_back(T(constraint_count + 1, segment(h), d[1])); - - // rotate direction by the angle at the tip of the halfedge - vi = m.to[h]; - theta = m.Th_hat[m.v_rep[vi]] / 2. - M_PI; - total_curvature += theta; - R = compute_rotation(theta); - d = R * d; - } - is_feature_seen[feature_index] = true; - feature_index = next_feature(feature_index); - } while (feature_index != i); - - // add additional cone variable if total geodesic curvature is not 2 pi - if (!float_equal(total_curvature, -2. * M_PI)) { - spdlog::debug("Adding virtual edge for interior cones"); - // add virtual edge with angle theta/2 - // note that the use of theta/2 is somewhat arbitrary - // we could use any angle in [0, theta] - int virtual_segment = variable_count; - R = compute_rotation(-theta / 2.); - d = R * d; - tripletList.push_back(T(constraint_count + 0, virtual_segment, d[0])); - tripletList.push_back(T(constraint_count + 1, virtual_segment, d[1])); - - // add virtual edge with missing geodesic curvature angle - Scalar curvature_defect = -2 * M_PI - total_curvature; - spdlog::debug("Curvature defect of {}", curvature_defect); - R = compute_rotation(curvature_defect); - d = R * d; - tripletList.push_back(T(constraint_count + 0, virtual_segment, d[0])); - tripletList.push_back(T(constraint_count + 1, virtual_segment, d[1])); - - ++variable_count; - } - - // closure constraint: sum of edge lengths should be zero - ell.push_back(0); - ell.push_back(0); - constraint_count += 2; - } - - // create the matrix from the triplets - MatrixX system_matrix; - system_matrix.resize(constraint_count, variable_count); - system_matrix.reserve(tripletList.size()); - system_matrix.setFromTriplets(tripletList.begin(), tripletList.end()); - - // convert ell to an eigen type - VectorX ell_eigen; - convert_std_to_eigen_vector(ell, ell_eigen); - - return std::make_tuple(system_matrix, ell_eigen); -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/depracated.cpp b/src/feature/experimental/depracated.cpp deleted file mode 100644 index 7ba1dff..0000000 --- a/src/feature/experimental/depracated.cpp +++ /dev/null @@ -1,719 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -std::tuple -parameterize_components( - const MarkedPennerConeMetric& embedding_metric, - const MarkedPennerConeMetric& original_metric, - const MarkedPennerConeMetric& marked_metric, - const Eigen::MatrixXd& V_cut, - const std::vector& vtx_reindex) -{ - // Separate mesh into components - ComponentMesh component_mesh(marked_metric); - const auto& mesh_components = component_mesh.get_mesh_components(); - const auto& he_maps = component_mesh.get_halfedge_maps(); - const auto& embedding_components = ComponentMesh(embedding_metric).get_mesh_components(); - const auto& init_components = ComponentMesh(original_metric).get_mesh_components(); - spdlog::info("Extracted {} components", mesh_components.size()); - - // Parameterize each component - int num_components = embedding_components.size(); - std::vector V_components, uv_components; - std::vector F_components, FT_components; - for (int i = 0; i < num_components; ++i) { - const auto& embedding_component = embedding_components[i]; - const auto& init_component = init_components[i]; - const auto& mesh_component = mesh_components[i]; - const auto& he_index = he_maps[i]; - VectorX metric_init(mesh_component.n_halfedges()); - VectorX metric_coords(mesh_component.n_halfedges()); - for (int i = 0; i < mesh_component.n_halfedges(); ++i) { - metric_init[i] = 2. * log(init_component.l[i]); - metric_coords[i] = 2. * log(mesh_component.l[i]); - } - PennerConeMetric component_init(mesh_component, metric_init); - PennerConeMetric component_metric(mesh_component, metric_coords); - - // Get component vertices - int n_v_component = component_metric.n_vertices(); - int n_ind_v_component = component_metric.n_ind_vertices(); - Eigen::MatrixXd V_component(n_ind_v_component, 3); - for (int i = 0; i < n_v_component; ++i) { - // get vertex in component - int hij = component_metric.out[i]; - int vi = component_metric.v_rep[i]; - - // get vertex in total mesh - int Hij = he_index[hij]; - int Vi = marked_metric.v_rep[marked_metric.to[marked_metric.opp[Hij]]]; - - // build v_rep and vertex positions - embedding_component.v_rep[i] = vi; - init_component.v_rep[i] = vi; - component_metric.v_rep[i] = vi; - V_component.row(vi) = V_cut.row(vtx_reindex[Vi]); - } - - // undouble Th_hat - for (int i = 0; i < n_ind_v_component; ++i) { - embedding_component.Th_hat[i] /= 2.; - } - std::vector vtx_reindex_component; - arange(n_ind_v_component, vtx_reindex_component); - - // Generate overlay - std::vector is_cut = {}; - auto vf_res = Optimization::generate_VF_mesh_from_halfedge_metric( - V_component, - embedding_component, - vtx_reindex_component, - component_init, - component_metric.get_metric_coordinates(), - is_cut, - false); - OverlayMesh m_o = std::get<0>(vf_res); - Eigen::MatrixXd V_o = std::get<1>(vf_res); - Eigen::MatrixXi F_o = std::get<2>(vf_res); - Eigen::MatrixXd uv_o = std::get<3>(vf_res); - Eigen::MatrixXi FT_o = std::get<4>(vf_res); - std::vector is_cut_h_final = std::get<5>(vf_res); - std::vector is_cut_o = std::get<6>(vf_res); - std::vector fn_to_f_o = std::get<7>(vf_res); - std::vector> endpoints_o = std::get<8>(vf_res); - - // Refine mesh - RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); - auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = refinement_mesh.get_VF_mesh(); - - // Add components to lists - V_components.push_back(V_r); - F_components.push_back(F_r); - uv_components.push_back(uv_r); - FT_components.push_back(FT_r); - } - - // Combine components into single mesh - auto [V_r, F_r] = combine_mesh_components(V_components, F_components); - auto [uv_r, FT_r] = combine_mesh_components(uv_components, FT_components); - - return std::make_tuple(V_r, F_r, uv_r, FT_r); -} - - int _merge_segment(EdgeIterator& edge_iterator, int segment_base_vertex) - { - int v_base_hij = segment_base_vertex; - int v_tip_hij = edge_iterator.get_segment_tip(); - int v_tip_hji = edge_iterator.get_opposite_segment_tip(); - int v_edge = v_tip_hji; - int v_corner_hij = get_face_corner_index(refined_F, vv2f, v_base_hij, v_tip_hij); - spdlog::info("merging triangle {}, {}, {}", v_corner_hij, v_base_hij, v_tip_hij); - spdlog::info("merging {} into {}", v_tip_hij, v_tip_hji); - - // get adjacent face of the segment to split - auto [f_old, f_old_corner] = get_face_corner(refined_F, vv2f, v_base_hij, v_tip_hij); - - // refine current face - refined_F[f_old] = {v_corner_hij, v_base_hij, v_edge}; - - // update edge to face map - set_VV_edge_faces(v_corner_hij, v_base_hij, v_edge, f_old); - vv2f.coeffRef(v_base_hij, v_tip_hij) = 0; - vv2f.coeffRef(v_tip_hij, v_corner_hij) = 0; - - // iterate edges - edge_iterator.iterate_segment(); - edge_iterator.iterate_opposite_segment(); - int v_next_hij = edge_iterator.get_segment_tip(); - int v_next_corner_hij = get_face_corner_index(refined_F, vv2f, v_tip_hij, v_next_hij); - - auto [f_next, f_next_corner] = get_face_corner(refined_F, vv2f, v_tip_hij, v_next_hij); - - // refine current face - refined_F[f_next] = {v_next_corner_hij, v_edge, v_next_hij}; - - // update edge to face map - set_VV_edge_faces(v_next_corner_hij, v_edge, v_next_hij, f_next); - vv2f.coeffRef(v_next_corner_hij, v_tip_hij) = 0; - vv2f.coeffRef(v_tip_hij, v_next_hij) = 0; - - // TODO remove - if (!is_valid_face(refined_F[f_old])) - spdlog::error("Invalid old hij face {}", formatted_vector(refined_F[f_old])); - - return v_edge; - } - - - // TODO: this is generally not feasible; it can cause flipped triangles in the uv domain - if (false) { - int num_midpoints = edge_vertices.size(); - std::vector uniform_coords = compute_uniform_coordinates(num_midpoints); - int uv_base_hij = edge_iterator.get_start_uv_vertex(); - int uv_base_hji = edge_iterator.get_opposite_start_uv_vertex(); - int uv_tip_hij = edge_iterator.get_end_uv_vertex(); - int uv_tip_hji = edge_iterator.get_opposite_end_uv_vertex(); - for (int m = 0; m < num_midpoints; ++m) { - double t = uniform_coords[m]; - for (int i : {0, 1}) { - refined_uv[hij_uv_vertices[m]][i] = - (1. - t) * refined_uv[uv_base_hij][i] + t * refined_uv[uv_tip_hij][i]; - refined_uv[hji_uv_vertices[m]][i] = - (1. - t) * refined_uv[uv_base_hji][i] + t * refined_uv[uv_tip_hji][i]; - } - } - } - -MatrixX compute_gluing_matrix( - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map) -{ - int num_glued_vertices = V_map.maxCoeff() + 1; - int num_ind_vertices = m.n_ind_vertices(); - typedef Eigen::Triplet T; - std::vector tripletList; - tripletList.reserve(num_ind_vertices); - - // sum up angles, dividing by 2 to account for doubling - for (int vi = 0; vi < num_ind_vertices; ++vi) { - int wi = V_map[vtx_reindex[vi]]; - if (wi == (num_glued_vertices - 1)) continue; - tripletList.push_back(T(wi, vi, 1.0)); - } - - // Create the matrix from the triplets - MatrixX gluing_matrix; - gluing_matrix.resize(num_glued_vertices - 1, num_ind_vertices); - gluing_matrix.reserve(tripletList.size()); - gluing_matrix.setFromTriplets(tripletList.begin(), tripletList.end()); - return gluing_matrix; -} - -MatrixX compute_relaxed_angle_constraint_matrix( - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const std::vector& is_relaxed_vertex) -{ - int num_glued_vertices = V_map.maxCoeff() + 1; - int num_ind_vertices = m.n_ind_vertices(); - typedef Eigen::Triplet T; - std::vector tripletList; - tripletList.reserve(num_ind_vertices); - - // add unrelaxed vertex constraints - int relaxed_count = 0; - int unrelaxed_count = 0; - std::vector relaxed_map(num_glued_vertices, -1); - std::vector relaxed_counts(num_glued_vertices, 0); - for (int vi = 0; vi < num_ind_vertices; ++vi) { - int wi = V_map[vtx_reindex[vi]]; - if (is_relaxed_vertex[wi]) { - ++relaxed_counts[wi]; - - if (relaxed_map[wi] < 0) { - relaxed_map[wi] = relaxed_count; - ++relaxed_count; - } - - // skip first two occurances of relaxed vertex - if (relaxed_counts[wi] <= 2) continue; - spdlog::info("{} seen {} times", wi, relaxed_counts[wi]); - } - - tripletList.push_back(T(unrelaxed_count, vi, 1.0)); - ++unrelaxed_count; - } - - for (int vi = 0; vi < num_ind_vertices; ++vi) { - int wi = V_map[vtx_reindex[vi]]; - if (!is_relaxed_vertex[wi]) continue; - if (relaxed_map[wi] == (relaxed_count - 1)) continue; - - spdlog::info("Adding constraint {}", relaxed_map[wi]); - tripletList.push_back(T(unrelaxed_count + relaxed_map[wi], vi, 1.0)); - } - - // Create the matrix from the triplets - MatrixX gluing_matrix; - gluing_matrix.resize(unrelaxed_count + relaxed_count - 1, num_ind_vertices); - gluing_matrix.reserve(tripletList.size()); - gluing_matrix.setFromTriplets(tripletList.begin(), tripletList.end()); - return gluing_matrix; -} - -// Measure the angle from a halfedge to a frame field in the face -Scalar compute_cross_field_halfedge_angle( - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::MatrixXd& V, - const Eigen::MatrixXd& R, - const Eigen::MatrixXd& N, - int h) -{ - // Get halfedge direction - int v0 = vtx_reindex[m.to[m.opp[h]]]; - int v1 = vtx_reindex[m.to[h]]; - Eigen::Vector3d h_direction = V.row(v1) - V.row(v0); - - // Get field reference direction and normal vector in the face - int f = m.f[h]; - Eigen::Vector3d field_direction = R.row(f); - Eigen::Vector3d face_normal = N.row(f); - - return signed_angle(h_direction, field_direction, face_normal); -} - -// Measure the intrinsic angle between frame field vectors in two faces across an edge -Scalar compute_cross_field_edge_angle( - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::MatrixXd& V, - const Eigen::MatrixXd& R, - const Eigen::MatrixXd& N, - int h) -{ - // Use consistent halfedge for stability - if (!has_priority(m, vtx_reindex, h)) { - return -compute_cross_field_edge_angle(m, vtx_reindex, V, R, N, m.opp[h]); - } - - // Get halfedge direction - int v0 = vtx_reindex[m.to[m.opp[h]]]; - int v1 = vtx_reindex[m.to[h]]; - Eigen::Vector3d h_direction = V.row(v1) - V.row(v0); - - // Get angle of rotation across the edge - int f0 = m.f[h]; - int f1 = m.f[m.opp[h]]; - double d0 = signed_angle(h_direction, R.row(f0), N.row(f0)); - double d1 = signed_angle(h_direction, R.row(f1), N.row(f1)); - double alpha = (2 * M_PI) + (M_PI / 4) + d0 - d1; - - //return (alpha > 0) ? pos_fmod(alpha, M_PI / 2.0) : -pos_fmod(-alpha, M_PI / 2.0); - return pos_fmod(alpha, M_PI / 2.0) - (M_PI / 4); - //return pos_fmod(alpha - (M_PI/ 4), 2.0 * M_PI); - //Scalar kij = pos_fmod(alpha - (M_PI / 4), 2 * M_PI); - //if (kij > M_PI) { - // kij -= 2 * M_PI; - //} - //if (!(-M_PI < kij && kij <= M_PI)) { - // spdlog::error("{} out of range (-pi, pi])", kij); - //} - //return kij; -} - -template -std::tuple< - Eigen::MatrixXd, - Eigen::MatrixXi, - Eigen::MatrixXd, - Eigen::MatrixXi> -layout_cut_mesh( - const MarkedPennerConeMetric& embedding_metric, - const MarkedPennerConeMetric& marked_metric, - const Eigen::MatrixXd& V_cut, - const std::vector& vtx_reindex) -{ -#ifdef WITH_MPFR - mpfr::mpreal::set_default_prec(100); - mpfr::mpreal::set_emax(mpfr::mpreal::get_emax_max()); - mpfr::mpreal::set_emin(mpfr::mpreal::get_emin_min()); -#endif - - // Separate mesh into components - ComponentMesh component_embedding(embedding_metric); - ComponentMesh component_mesh(marked_metric); - const auto& embedding_components = component_embedding.get_mesh_components(); - const auto& mesh_components = component_mesh.get_mesh_components(); - const auto& he_maps = component_mesh.get_halfedge_maps(); - spdlog::info("Extracted {} components", mesh_components.size()); - - // Parameterize each component - int num_components = embedding_components.size(); - std::vector V_components, uv_components; - std::vector F_components, FT_components; - for (int i = 0; i < num_components; ++i) { - const auto& embedding_component = embedding_components[i]; - const auto& mesh_component = mesh_components[i]; - VectorX metric_coords(mesh_component.n_halfedges()); - for (int i = 0; i < mesh_component.n_halfedges(); ++i) { - metric_coords[i] = 2. * log(mesh_component.l[i]); - } - PennerConeMetric component_metric(mesh_component, metric_coords); - - int n_v_component = component_metric.n_vertices(); - int n_ind_v_component = component_metric.n_ind_vertices(); - Eigen::MatrixXd V_component(n_ind_v_component, 3); - const auto& he_index = he_maps[i]; - for (int i = 0; i < n_v_component; ++i) { - // get vertex in component - int hij = component_metric.out[i]; - int vi = component_metric.v_rep[i]; - - // get vertex in total mesh - int Hij = he_index[hij]; - int Vi = marked_metric.v_rep[marked_metric.to[marked_metric.opp[Hij]]]; - - V_component.row(vi) = V_cut.row(vtx_reindex[Vi]); - } - - // Fit conformal scale factors - VectorX scale_factors; - scale_factors.setZero(n_ind_v_component); - - // Compute interpolation overlay mesh - InterpolationMesh interpolation_mesh, reverse_interpolation_mesh; - interpolate_penner_coordinates( - embedding_component, - component_metric.get_metric_coordinates(), - scale_factors, - interpolation_mesh, - reverse_interpolation_mesh); - OverlayMesh m_o = interpolation_mesh.get_overlay_mesh(); - auto [F_o, uv_o, FT_o] = compute_layout_VF(m_o); - //Optimization::view_mesh_topology(V_component, F_o); - - // Add components to lists - V_components.push_back(V_component); - F_components.push_back(F_o); - uv_components.push_back(uv_o); - FT_components.push_back(FT_o); - } - - // Combine components into single mesh - auto [V_r, F_r] = combine_mesh_components(V_components, F_components); - auto [uv_r, FT_r] = combine_mesh_components(uv_components, FT_components); - auto [V_rr, F_rr] = reindex_mesh(V_r, F_r, vtx_reindex); - - return std::make_tuple(V_rr, F_rr, uv_r, FT_r); -} -/** - * @brief Glue the feature edges of a doubled cut mesh. - * - * @param m: mesh - * @param vtx_reindex: reindexing of cut mesh vertices to VF index - * @param cut_m: mesh with features cut - * @param cut_vtx_reindex: reindexing of cut mesh vertices to cut VF index - * @param V_map: map from cut VF indices to original VF indices - * @return closed mesh with glued features - */ -Mesh glue_feature_edges( - const Mesh& m, - const std::vector& vtx_reindex, - const Mesh& cut_m, - const std::vector& cut_vtx_reindex, - const Eigen::VectorXi& V_map) const -{ - // compute map from vertex-vertex edges to halfedges - int n_v = V_map.maxCoeff() + 1; - Eigen::SparseMatrix vv2he(n_v, n_v); - - typedef Eigen::Triplet Trip; - std::vector trips; - int n_cut_he = cut_m.n_halfedges(); - for (int hij = 0; hij < n_cut_he; ++hij) { - if (cut_m.type[hij] > 1) continue; - - int hji = cut_m.opp[hij]; - int vi = V_map[cut_vtx_reindex[cut_m.v_rep[cut_m.to[hji]]]]; - int vj = V_map[cut_vtx_reindex[cut_m.v_rep[cut_m.to[hij]]]]; - trips.push_back(Trip(vi, vj, hij + 1)); - } - vv2he.setFromTriplets(trips.begin(), trips.end()); - - // compute new length vector - int num_halfedges = m.n_halfedges(); - std::vector l(num_halfedges, -1.); - for (int hij = 0; hij < num_halfedges; ++hij) { - int hji = m.opp[hij]; - if (hij < hji) continue; // only process each edge once - - int vi = vtx_reindex[m.v_rep[m.to[hji]]]; - int vj = vtx_reindex[m.v_rep[m.to[hij]]]; - int cut_hij = vv2he.coeffRef(vi, vj) - 1; - assert(cut_hij >= 0); - m.l[hij] = m.l[hji] = cut_m.l[cut_hij]; - } - - return m; -} - -std::vector FeatureFinder::compute_dual_feature_forest() const -{ - const auto& m = get_mesh(); - - // Initialize an array to keep track of vertices - int num_faces = m.n_faces(); - int num_halfedges = m.n_halfedges(); - std::vector is_processed_face(num_faces, false); - std::vector is_dual_forest_halfedge(num_halfedges, false); - - // initialize the stack of vertices to process with all vertices - std::queue vertices_to_process = {}; - - // initialize a dual spanning tree connecting components - UnionFind component_faces = compute_component_faces(); - std::vector face_labels = component_faces.index_sets(); - int num_components = component_faces.count_sets(); - std::vector is_component_seen(num_components, false); - std::queue faces_to_process; - faces_to_process.push(0); - - // do BFS on component with vl as root - while (!faces_to_process.empty()) { - // Get the next face to process - int fi = faces_to_process.front(); - faces_to_process.pop(); - - // Skip already processed faces - if (is_processed_face[fi]) continue; - is_processed_face[fi] = true; - is_component_seen[face_labels[fi]] = true; - - // Iterate over the face via halfedges - int hij = m.h[fi]; - int hjk = hij; - do { - // get current edge data - hjk = m.n[hjk]; - int hkj = m.opp[hjk]; - int fk = m.f[hkj]; - - // check if the tip vertex has been seen yet - if (is_processed_face[fk]) continue; - - // if opposite face is in unseen component, add to dual forest - if (!is_component_seen[face_labels[fk]]) - { - is_component_seen[face_labels[fk]] = true; - is_dual_forest_halfedge[hjk] = true; - is_dual_forest_halfedge[hkj] = true; - } - - // add halfedge to tree - faces_to_process.push(fk); - } while (hjk != hij); - } - assert(std::find(is_component_seen.begin(), is_component_seen.end(), false) == is_component_seen.end()); - - return is_dual_forest_halfedge; -} - -std::tuple _refine_corner_feature_faces(const FeatureFinder& feature_finder) -{ - const Mesh& m = feature_finder.get_mesh(); - int num_faces = m.n_faces(); - IntrinsicRefinementMesh refinement_mesh(m); - - std::vector is_feature_vertex(m.n_vertices(), false); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - // count number of features in the face - int num_feature_edges = 0; - int hij = m.h[fijk]; - for (int h : {hij, m.n[hij], m.n[m.n[hij]]}) - { - if (feature_finder.is_feature_halfedge(h)) - { - is_feature_vertex[m.to[h]] = true; - ++num_feature_edges; - } - } - - // refine faces with more than one feature - if (num_feature_edges > 1) - { - refinement_mesh.refine_face(fijk); - } - } - - // refine faces with all boundary - - // generate VF mesh - const auto& V = feature_finder.get_vertex_positions(); - const auto& vtx_reindex = feature_finder.get_vertex_reindex(); - return refinement_mesh.generate_mesh(V, vtx_reindex); -} - - -// TODO Broken -std::tuple refine_strip_faces( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F) -{ - // Convert VF mesh to halfedge with vertex reindexing - int num_vertices = V.rows(); - bool fix_boundary = false; - std::vector Th_hat(num_vertices, 0); - std::vector vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops, free_cones; - auto m = FV_to_double( - V, - F, - V, - F, - Th_hat, - vtx_reindex, - indep_vtx, - dep_vtx, - v_rep, - bnd_loops, - free_cones, - fix_boundary); - - IntrinsicRefinementMesh refinement_mesh(m); - - // ensure every component has an interior vertex - std::vector is_boundary_vertex = compute_boundary_vertices(m); - - Eigen::VectorXi components = find_mesh_face_components(m); - int num_components = components.maxCoeff() + 1; - std::vector has_interior(num_components, false); - std::vector component_face(num_components, -1); - int num_halfedges = m.n_halfedges(); - for (int hij = 0; hij < num_halfedges; ++hij) - { - int fijk = m.f[hij]; - component_face[components[fijk]] = fijk; - if (!is_boundary_vertex[m.to[hij]]) - { - has_interior[components[fijk]] = true; - } - } - - for (int i = 0; i < num_components; ++i) - { - if (has_interior[i]) continue; - spdlog::info("refining component {}", i); - refinement_mesh.refine_face(component_face[i]); - } - - // generate VF mesh - return refinement_mesh.generate_mesh(V, vtx_reindex); -} - - -std::tuple _refine_feature_components(const FeatureFinder& feature_finder) -{ - const Mesh& m = feature_finder.get_mesh(); - IntrinsicRefinementMesh refinement_mesh(m); - - // get face components - UnionFind component_faces = feature_finder.compute_component_faces(); - std::vector> components = component_faces.build_sets(); - - std::vector feature_degrees = feature_finder.compute_feature_degrees(); - - // refined edge on faces with no valence two edges - for (const auto& component : components) { - bool has_valence_two = false; - int feature_h = -1; - for (int f : component) { - int hij = m.h[f]; - int hjk = m.n[hij]; - int hki = m.n[hjk]; - for (int h : {hij, hjk, hki}) { - if (feature_degrees[m.v_rep[m.to[h]]] == 2) - { - has_valence_two = true; - } - if (feature_finder.is_feature_halfedge(h)) - { - feature_h = h; - } - } - if (has_valence_two) break; - } - if (!has_valence_two) - { - if (feature_h < 0) - { - spdlog::error("No feature edge for component"); - continue; - } - refinement_mesh.refine_halfedge(feature_h); - } - } - - // generate VF mesh - const auto& V = feature_finder.get_vertex_positions(); - const auto& vtx_reindex = feature_finder.get_vertex_reindex(); - return refinement_mesh.generate_mesh(V, vtx_reindex); -} - -/** - * @brief Generate a Dirichlet metric for a cut mesh . - * - * @param V_cut: cut mesh vertices - * @param F_cut: cut mesh faces - * @param V_map: identification map from cut VF vertices to the glued mesh vertices - * @param direction: per-face reference tangent direction matrix - * @param is_fixed_direction: per-face mask for salient directions - * @param marked_metric_params - * @param marked_corners - * @return std::tuple< - * DirichletPennerConeMetric, - * std::vector, - * std::vector, - * VectorX, - * std::vector> - */ -std::tuple< - DirichletPennerConeMetric, - std::vector, - std::vector, - VectorX, - std::vector> -generate_aligned_metric( - const Eigen::MatrixXd& V_cut, - const Eigen::MatrixXi& F_cut, - const Eigen::VectorXi& V_map, - const Eigen::MatrixXd& direction, - const std::vector& is_fixed_direction, - MarkedMetricParameters marked_metric_params, - std::vector> marked_corners) -{ - CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, marked_corners); - cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); - return cut_metric_generator.get_aligned_metric(V_map, marked_metric_params); -} - -std::tuple, std::vector> generate_union_mesh( - const Eigen::MatrixXd& V_cut, - const Eigen::MatrixXi& F_cut) -{ - auto [meshes, vertex_maps] = generate_component_meshes(V_cut, F_cut); - - // generate total mesh - auto mesh = union_meshes(meshes); - - // reindex vertices - Eigen::VectorXi vertex_map = union_vectors(vertex_maps); - std::vector vtx_reindex(vertex_map.data(), vertex_map.data() + vertex_map.size()); - - return std::make_tuple(mesh, vtx_reindex); -} - - -std::tuple, std::vector, VectorX, std::vector> -generate_union_metric( - const Eigen::MatrixXd& V_cut, - const Eigen::MatrixXi& F_cut, - const Eigen::MatrixXd& direction, - const std::vector& is_fixed_direction, - MarkedMetricParameters marked_metric_params, - std::vector> marked_corners) -{ - // generate a cut metric with trivial gluing - Eigen::VectorXi V_map(0); - CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, marked_corners); - cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); - return cut_metric_generator.get_union_metric(marked_metric_params); -} \ No newline at end of file diff --git a/src/feature/experimental/energy.cpp b/src/feature/experimental/energy.cpp deleted file mode 100644 index 0c207a1..0000000 --- a/src/feature/experimental/energy.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/energy.h" -#include "metric/constraint.h" - -namespace Penner { -namespace Feature { - -ConeEnergy::ConeEnergy(const PennerConeMetric& cone_metric) - : m_cone_metric(cone_metric) -{} - -Scalar ConeEnergy::energy(const VectorX& metric_coords) const -{ - // set metric coordinates - auto cone_metric = m_cone_metric.set_metric_coordinates(metric_coords); - - // compute cone angle constraints - VectorX constraint; - MatrixX J_constraint; - bool need_jacobian = false; - bool only_free_vertices = true; - constraint_with_jacobian( - *cone_metric, - constraint, - J_constraint, - need_jacobian, - only_free_vertices); - - return 0.5 * constraint.dot(constraint); -} - -VectorX ConeEnergy::gradient(const VectorX& metric_coords) const -{ - // set metric coordinates - auto cone_metric = m_cone_metric.set_metric_coordinates(metric_coords); - - // compute cone angle constraints with Jacobian - VectorX constraint; - MatrixX J_constraint; - bool need_jacobian = true; - bool only_free_vertices = true; - constraint_with_jacobian( - *cone_metric, - constraint, - J_constraint, - need_jacobian, - only_free_vertices); - - return constraint.transpose() * J_constraint; -} - -MatrixX ConeEnergy::hessian(const VectorX& metric_coords) const -{ - throw std::runtime_error("No hessian defined"); - return id_matrix(metric_coords.size()); -} - -MatrixX ConeEnergy::hessian_inverse(const VectorX& metric_coords) const -{ - throw std::runtime_error("No hessian defined"); - return id_matrix(metric_coords.size()); -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/junctions.cpp b/src/feature/experimental/junctions.cpp deleted file mode 100644 index 1ef13e4..0000000 --- a/src/feature/experimental/junctions.cpp +++ /dev/null @@ -1,374 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#include "feature/experimental/junctions.h" -#include "feature/core/common.h" - -#include "holonomy/core/viewer.h" -#include "feature/core/component_mesh.h" -#include "feature/core/vf_corners.h" -#include "holonomy/holonomy/cones.h" -#include "util/vector.h" - -#include -#include -#include -#include - -#ifdef ENABLE_VISUALIZATION -#include "polyscope/point_cloud.h" -#include "polyscope/surface_mesh.h" -#endif - -namespace Penner { -namespace Feature { - - -void JunctionFeatureFinder::view_junctions(bool show) -{ - const auto& faces = get_faces(); - const auto& vtx_reindex = get_vertex_reindex(); - const auto& vertex_positions = get_vertex_positions(); - - // get junction vertices - std::vector junction_vertices = vector_reindex(vtx_reindex, compute_junctions()); - Eigen::MatrixXd junctions = - generate_subset_vertices(vertex_positions, junction_vertices); - -#ifdef ENABLE_VISUALIZATION - polyscope::init(); - std::string mesh_handle = "feature mesh"; - polyscope::registerSurfaceMesh(mesh_handle, vertex_positions, faces); - polyscope::registerPointCloud("junctions", junctions); - if (show) polyscope::show(); -#endif -} - -void remove_threshold_junctions( - JunctionFeatureFinder& feature_finder, - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const Eigen::VectorXi& vertex_component, - const VectorX& component_function, - Scalar relative_threshold, - bool do_remove_all_junctions) -{ - // get boolean mask for junctions - std::vector junctions = feature_finder.compute_junctions(); - std::vector is_junction; - convert_index_vector_to_boolean_array(junctions, m.n_ind_vertices(), is_junction); - - // track relaxed components - std::vector is_component_relaxed(component_function.size(), false); - - // prune junctions above relative threshold of maximum function value - Scalar max_function = component_function.maxCoeff(); - for (int vi = 0; vi < m.n_vertices(); ++vi) { - // skip vertices in components above threshold - if (component_function[vertex_component[vi]] < relative_threshold * max_function) continue; - - // skip components already relaxed if not removing all - if ((do_remove_all_junctions) || (!is_component_relaxed[vertex_component[vi]])) { - // remove vertex edges if it is a junction - int vi_root = V_map[vtx_reindex[m.v_rep[vi]]]; - if (!is_junction[vi_root]) continue; - feature_finder.prune_junction(vi_root); - - // mark component as relaxed - is_component_relaxed[vertex_component[vi]] = true; - } - } - - // remove any remaining closed loops - feature_finder.prune_closed_loops(); -} - -void JunctionFeatureFinder::prune_closed_loops() -{ - const auto& m = get_mesh(); - - // get features and the degrees of features - UnionFind feature_unions = compute_feature_components(); - std::vector> features = feature_unions.build_sets(); - std::vector feature_degrees = compute_feature_degrees(); - - // iterate over features - for (const auto& feature : features) { - // check for junctions or darts - bool is_closed_loop = true; - for (int hij : feature) { - int vj = m.v_rep[m.to[hij]]; - if (feature_degrees[vj] != 2) { - is_closed_loop = false; - break; - } - } - - // remove an edge from the loop feature - if (is_closed_loop) { - int hij = feature.front(); - set_feature_edge(hij, false); - } - } -} - -// linear search for a halfedge vertex that maps to a given VF vertex -int compute_domain_vertex( - const Mesh& m, - const std::vector& vtx_reindex, - int vertex_index) -{ - int num_vertices = m.n_vertices(); - for (int vi = 0; vi < num_vertices; ++vi) { - if (vtx_reindex[m.v_rep[vi]] == vertex_index) return vi; - } - - return -1; -} - -// compute distance from a start vertex without crossing cut halfedges -std::vector -find_vertex_weights(const Mesh& m, const std::vector& is_cut, int v_start) -{ - // Initialize an array to keep track of vertices - int num_vertices = m.n_vertices(); - Scalar max_cost = vector_max(m.l); - std::vector is_processed_vertex(num_vertices, false); - std::vector is_boundary_vertex(num_vertices, false); - - // mark all cut vertices as processed - int num_halfedges = is_cut.size(); - for (int hij = 0; hij < num_halfedges; ++hij) { - if (is_cut[hij]) { - is_boundary_vertex[m.to[hij]] = true; - } - } - - // Initialize vertex cost with value above maximum possible weight - Scalar inf_cost = std::max((num_vertices * max_cost) + 1.0, max_cost + 1.0); - std::vector vertex_cost(num_vertices, inf_cost); - - // Mark root with 0 (or cost lower than other edges for all negative weights) - Scalar root_cost = min(max_cost, 0.0); - vertex_cost[v_start] = root_cost; - is_processed_vertex[v_start] = false; - is_boundary_vertex[v_start] = false; - - // Define a custom comparison function for the priority queue - typedef std::pair WeightedVertex; - auto vertex_compare = [](const WeightedVertex& left, const WeightedVertex& right) { - return left.second > right.second; - }; - - // Initialize the stack of vertices to process with all vertices - std::priority_queue, decltype(vertex_compare)> - vertices_to_process(vertex_compare); - for (int vi = 0; vi < num_vertices; ++vi) { - vertices_to_process.push(std::make_pair(vi, vertex_cost[vi])); - } - - // Perform Prim or Dijkstra algorithm - while (!vertices_to_process.empty()) { - // Get the next vertex to process - auto [vi, vi_cost] = vertices_to_process.top(); - vertices_to_process.pop(); - - // Skip already processed vertices and boundary vertices - if (is_processed_vertex[vi]) continue; - if (is_boundary_vertex[vi]) continue; - is_processed_vertex[vi] = true; - - // Check if vertex has uninitialized cost and skip if so - if (vertex_cost[vi] == inf_cost) continue; - - // Iterate over the vertex circulator via halfedges - int h_start = m.out[vi]; - int hij = h_start; - do { - // Get the vertex in the one ring at the tip of the halfedge - int vj = m.to[hij]; - - // Get candidate edge cost (either path length or edge weight) - Scalar candidate_cost = m.l[hij] + vertex_cost[vi]; - - // Check if the edge to the tip vertex is the best seen so far - if ((!is_processed_vertex[vj]) && (vertex_cost[vj] > candidate_cost)) { - vertex_cost[vj] = candidate_cost; - vertices_to_process.push(std::make_pair(vj, vertex_cost[vj])); - } - - // stop at cut edges - if (is_cut[hij]) break; - - // Progress to the next halfedge in the vertex circulator - hij = m.n[m.opp[hij]]; - } while (hij != h_start); - hij = m.opp[m.n[m.n[h_start]]]; - do { - // Get the vertex in the one ring at the tip of the halfedge - int vj = m.to[hij]; - - // Get candidate edge cost (either path length or edge weight) - Scalar candidate_cost = m.l[hij] + vertex_cost[vi]; - - // Check if the edge to the tip vertex is the best seen so far - if ((!is_processed_vertex[vj]) && (vertex_cost[vj] > candidate_cost)) { - vertex_cost[vj] = candidate_cost; - vertices_to_process.push(std::make_pair(vj, vertex_cost[vj])); - } - - // stop at cut edges - if (is_cut[hij]) break; - - // Progress to the next halfedge ccw in the vertex circulator - hij = m.opp[m.n[m.n[hij]]]; - } while (hij != h_start); - } - - return vertex_cost; -} - -void JunctionFeatureFinder::prune_closest_junction(int vertex_index) -{ - // get index of the vertex in the halfedge mesh - const auto& m = get_mesh(); - const auto& vtx_reindex = get_vertex_reindex(); - const auto& vertex_positions = get_vertex_positions(); - int root = compute_domain_vertex(m, vtx_reindex, vertex_index); - - // compute distance to input vertex with features cut - const std::vector& is_feature_halfedge = get_feature_halfedges(); - std::vector vertex_weights = find_vertex_weights(m, is_feature_halfedge, root); - view_vertex_function(m, vtx_reindex, vertex_positions, vertex_weights); - - // get the junction closest to the root vertex - std::vector junctions = compute_junctions(); - std::vector junction_weights = vector_compose(vertex_weights, junctions); - int farthest_cone_index = argmax(junction_weights); - int closest_cone_index = argmin(junction_weights); - if (closest_cone_index < 0) { - spdlog::warn("no junctions remaining"); - return; - } - int closest_cone = junctions[closest_cone_index]; - spdlog::info("closest junction cone is {}", closest_cone); - - // remove features with noninfinite distance - int num_junctions = junctions.size(); - for (int i = 0; i < num_junctions; ++i) { - if (junction_weights[i] >= junction_weights[farthest_cone_index]) continue; - prune_junction(junctions[i]); - break; - } -} - -std::vector JunctionFeatureFinder::compute_junctions() const -{ - // get degrees of all vertices - std::vector feature_degrees = compute_feature_degrees(); - - // get vertices with degree greater than 2 - int num_vertices = feature_degrees.size(); - std::vector vertex_indices = {}; - for (int vi = 0; vi < num_vertices; ++vi) { - if (feature_degrees[vi] > 2) { - vertex_indices.push_back(vi); - } - } - - return vertex_indices; -} - -void JunctionFeatureFinder::prune_junctions() -{ - // save current junctions - const auto& junctions = compute_junctions(); - - // remove all junctions - for (int vi : junctions) { - prune_junction(vi); - } -} - -void JunctionFeatureFinder::prune_junction(int vertex_index) -{ - // prune features adjacent to given vertex - const auto& m = get_mesh(); - std::vector one_ring = generate_vertex_one_ring(m, vertex_index); - for (int hij : one_ring) { - set_feature_edge(hij, false); - } -} - -std::tuple, std::vector> JunctionFeatureFinder::compute_feature_forest( - const std::vector& flood_seeds) const -{ - const auto& m = get_mesh(); - const auto& vtx_reindex_inverse = get_vertex_reindex_inverse(); - - // Initialize an array to keep track of vertices - int num_vertices = m.n_vertices(); - int num_halfedges = m.n_halfedges(); - std::vector is_processed_vertex(num_vertices, false); - std::vector is_seen_halfedge(num_halfedges, false); - std::vector halfedge_from_vertex(num_vertices, -1); - - // initialize the stack of vertices to process with all vertices - std::queue vertices_to_process = {}; - - // build spanning forest - for (int vl_root : flood_seeds) { - int vl = vtx_reindex_inverse[vl_root]; - - // get unprocessed vertex vl - if (is_processed_vertex[vl]) continue; - vertices_to_process.push(vl); - - // do BFS on component with vl as root - while (!vertices_to_process.empty()) { - // Get the next vertex to process - int vi = vertices_to_process.front(); - vertices_to_process.pop(); - - // Skip already processed vertices - if (is_processed_vertex[vi]) continue; - is_processed_vertex[vi] = true; - - // Iterate over the vertex circulator via halfedges - int hij = m.out[vi]; - int hik = hij; - do { - // get current edge data - hik = m.n[m.opp[hik]]; - int hki = m.opp[hik]; - - // only process feature halfedges - if (!is_feature_halfedge(hik)) continue; - - // mark halfedges in current edge as seen - is_seen_halfedge[hik] = true; - is_seen_halfedge[hki] = true; - - // check if the tip vertex has been seen yet - int vk = m.to[hik]; - if (is_processed_vertex[vk]) continue; - - // add halfedge to tree and propagte front to tip vertex - halfedge_from_vertex[vk] = hik; - vertices_to_process.push(vk); - } while (hik != hij); - } - } - - return std::make_tuple(halfedge_from_vertex, is_seen_halfedge); -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/marked_refinement.cpp b/src/feature/experimental/marked_refinement.cpp deleted file mode 100644 index 26e5739..0000000 --- a/src/feature/experimental/marked_refinement.cpp +++ /dev/null @@ -1,248 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/marked_refinement.h" - -#include "util/boundary.h" -#include "util/vector.h" -#include "holonomy/core/viewer.h" -#include "feature/core/component_mesh.h" - -namespace Penner { -namespace Feature { - - -// Refine a single face, disregarding symmetry structure and independent vertices -// k -// / | \ . -// / | \ . -// / l \ . -// / / \ \ . -// i ---------- j -int MarkedRefinementMesh::refine_single_face(int face_index) -{ - Mesh& m = get_mesh(); - - // get halfedges and vertices of triangle by clockwise circulation - int hij = m.h[face_index]; - int hjk = m.n[hij]; - int hki = m.n[hjk]; - int vi = m.to[hki]; - int vj = m.to[hij]; - int vk = m.to[hjk]; - - // get new vertex, face and hafledge indices - int vl = get_new_vertex(); - int fijl = face_index; - int fjkl = get_new_face(); - int fkil = get_new_face(); - auto [hil, hli] = get_new_edge(); - auto [hjl, hlj] = get_new_edge(); - auto [hkl, hlk] = get_new_edge(); - - // set connectivity for faces - auto set_face = [&m](int h0, int h1, int h2, int f) { - m.n[h0] = h1; - m.n[h1] = h2; - m.n[h2] = h0; - m.f[h0] = f; - m.f[h1] = f; - m.f[h2] = f; - m.h[f] = h0; - }; - set_face(hij, hjl, hli, fijl); - set_face(hjk, hkl, hlj, fjkl); - set_face(hki, hil, hlk, fkil); - - // set connectivity for vertices - m.to[hil] = vl; - m.to[hli] = vi; - m.to[hjl] = vl; - m.to[hlj] = vj; - m.to[hkl] = vl; - m.to[hlk] = vk; - m.out[vl] = hli; - - // set length of all edges to 1 and type to that of the face - // TODO Use midpoint length; only topology used currently - int type = m.type[hij]; - int type_input = m.type_input[hij]; - for (int h : {hil, hli, hjl, hlj, hkl, hlk}) { - m.l[h] = 1.; - m.type[h] = type; - m.type_input[h] = type_input; - } - - // refine all dual loops - // TODO optimize linear pass - int fij_opp = m.f[m.opp[hij]]; - int fjk_opp = m.f[m.opp[hjk]]; - int fki_opp = m.f[m.opp[hki]]; - auto& dual_loops = get_dual_loops(); - for (std::vector& dual_loop : dual_loops) { - int num_faces = dual_loop.size(); - auto loop_index = [&num_faces](int i) { return (i + num_faces) % num_faces; }; - std::vector new_dual_loop = {}; - for (int i = 0; i < num_faces; ++i) { - int curr_face = dual_loop[loop_index(i)]; - // handle case for refined face - if (curr_face == face_index) - { - // add refined face opposite previous face - int prev_face = dual_loop[loop_index(i - 1)]; - if (prev_face == fij_opp) - new_dual_loop.push_back(fijl); - else if (prev_face == fjk_opp) - new_dual_loop.push_back(fjkl); - else if (prev_face == fki_opp) - new_dual_loop.push_back(fkil); - - // add refined face opposite next face - int next_face = dual_loop[loop_index(i + 1)]; - if (next_face == fij_opp) - new_dual_loop.push_back(fijl); - else if (next_face == fjk_opp) - new_dual_loop.push_back(fjkl); - else if (next_face == fki_opp) - new_dual_loop.push_back(fkil); - } - // if face not refined, just add to list - else - { - new_dual_loop.push_back(curr_face); - } - } - - // overwrite dual loop - dual_loop = new_dual_loop; - } - - return vl; -} - -// refine halfedge, without considering symmetry -int MarkedRefinementMesh::refine_single_halfedge(int halfedge_index) -{ - Mesh& m = get_mesh(); - - // get halfedges and vertices of triangle by clockwise circulation - int hij = halfedge_index; - int hjk = m.n[hij]; - int hki = m.n[hjk]; - int hji = m.opp[hij]; - int hil = m.n[hji]; - int hlj = m.n[hil]; - int fijk = m.f[hij]; - int fjil = m.f[hji]; - int vi = m.to[hki]; - int vj = m.to[hij]; - int vk = m.to[hjk]; - int vl = m.to[hil]; - - // get new vertex, face and hafledge indices - int vm = get_new_vertex(); - int fmjk = fijk; - int fmki = get_new_face(); - int fmil = fjil; - int fmlj = get_new_face(); - int him = hij; - int hmi = hji; - auto [hmj, hjm] = get_new_edge(); - auto [hmk, hkm] = get_new_edge(); - auto [hml, hlm] = get_new_edge(); - - // set connectivity for faces - auto set_face = [&m](int h0, int h1, int h2, int f) { - m.n[h0] = h1; - m.n[h1] = h2; - m.n[h2] = h0; - m.f[h0] = f; - m.f[h1] = f; - m.f[h2] = f; - m.h[f] = h0; - }; - set_face(hmj, hjk, hkm, fmjk); - set_face(hmk, hki, him, fmki); - set_face(hmi, hil, hlm, fmil); - set_face(hml, hlj, hjm, fmlj); - - // set connectivity for vertices - m.to[hmi] = vi; - m.to[hmj] = vj; - m.to[hmk] = vk; - m.to[hml] = vl; - m.to[him] = vm; - m.to[hjm] = vm; - m.to[hkm] = vm; - m.to[hlm] = vm; - m.out[vm] = hmi; - m.out[vi] = him; - m.out[vj] = hjm; - m_endpoints[vm] = {vi, vj}; - - // set length of all edges to 1 and type to that of the face - //int type_0 = m.type[hij]; - //int type_input = m.type_input[hij]; - //Scalar l = 1.; - // TODO Use midpoint length - - // refine all dual loops - //auto& dual_loops = get_dual_loops(); - //for (std::vector& dual_loop : dual_loops) { - // TODO - //} - - return vm; -} - -MarkedPennerConeMetric MarkedRefinementMesh::generate_marked_metric( - const std::vector& kappa_hat) const -{ - const auto& refined_mesh = get_mesh(); - const auto& refined_basis_loops = get_dual_loops(); - std::vector> basis_loops; - for (const auto& basis_loop : refined_basis_loops) { - basis_loops.push_back(std::make_unique( - Holonomy::DualLoopConnectivity(Holonomy::build_dual_path_from_face_sequence(refined_mesh, basis_loop)))); - } - - int num_halfedges = refined_mesh.n_halfedges(); - VectorX refined_metric_coords(refined_mesh.n_halfedges()); - for (int h = 0; h < num_halfedges; ++h) { - refined_metric_coords[h] = 2. * log(refined_mesh.l[h]); - } - - return MarkedPennerConeMetric(refined_mesh, refined_metric_coords, basis_loops, kappa_hat); -} - -MarkedPennerConeMetric MarkedRefinementMesh::generate_dirichlet_metric( - const std::vector& kappa_hat, - const std::vector& start_vertices, - const MatrixX& boundary_constraint_system, - const VectorX ell) const -{ - auto marked_metric = generate_marked_metric(kappa_hat); - - // generate boundary paths - std::vector boundary_paths = {}; - boundary_paths.reserve(start_vertices.size()); - for (int vi : start_vertices) { - boundary_paths.push_back(BoundaryPath(marked_metric, vi)); - } - - return DirichletPennerConeMetric( - marked_metric, - boundary_paths, - boundary_constraint_system, - ell); -} - - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/newton.cpp b/src/feature/experimental/newton.cpp deleted file mode 100644 index 89c6218..0000000 --- a/src/feature/experimental/newton.cpp +++ /dev/null @@ -1,411 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/newton.h" - -#include "holonomy/holonomy/holonomy.h" -#include "holonomy/holonomy/newton.h" -#include "holonomy/holonomy/constraint.h" -#include "feature/dirichlet/constraint.h" - -#include -#include -#include "optimization/metric_optimization/energies.h" -#include "optimization/metric_optimization/energy_functor.h" -#include "metric/projection.h" -#include "metric/shear.h" -#include "util/io.h" - -#ifdef USE_SUITESPARSE -#include -//#include -#endif - -namespace Penner { -namespace Feature { - -// TODO: Deprecated method; should avoid unless substantial changes needed for logging -// or methodology changes. The override approach used here avoids code duplication, but may -// cause undesireable class hierarchies if abused - -class OptimizeDirichletNewton : public Holonomy::OptimizeHolonomyNewton -{ -public: - // Update the corner angles and holonomy constraint given the marked metric - void update_holonomy_constraint(DirichletPennerConeMetric& dirichlet_metric) - { - // Get corner angles and metric constraints - dirichlet_metric.make_discrete_metric(); - log.num_flips = dirichlet_metric.num_flips(); - dirichlet_metric.get_corner_angles(alpha, cot_alpha); - holonomy_constraint = compute_metric_constraint(dirichlet_metric, alpha); - } - - // Update the corner angles and boundary constraint given the marked metric - void update_boundary_constraint(DirichletPennerConeMetric& dirichlet_metric) - { - // Get corner angles and metric constraints - dirichlet_metric.make_discrete_metric(); - log.num_flips = dirichlet_metric.num_flips(); - dirichlet_metric.get_corner_angles(alpha, cot_alpha); - boundary_constraint = compute_boundary_constraint(dirichlet_metric); - } - - void update_all_constraint(DirichletPennerConeMetric& dirichlet_metric) - { - dirichlet_metric.make_discrete_metric(); - log.num_flips = dirichlet_metric.num_flips(); - dirichlet_metric.get_corner_angles(alpha, cot_alpha); - constraint = dirichlet_metric.constraint(alpha); - } - - void update_dirichlet_constraint(DirichletPennerConeMetric& dirichlet_metric) - { - if (constraint_type == ConstraintType::holonomy) - { - update_holonomy_constraint(dirichlet_metric); - constraint = holonomy_constraint; - } - else if (constraint_type == ConstraintType::boundary) - { - update_boundary_constraint(dirichlet_metric); - constraint = boundary_constraint; - } - else if (constraint_type == ConstraintType::all) - { - update_all_constraint(dirichlet_metric); - } - } - - - // Update the corner angles, constraint, constraint jacobian, and descent direction given the - // marked metric - void update_holonomy_descent_direction( - DirichletPennerConeMetric& dirichlet_metric, - const MatrixX& metric_basis_matrix) - { - double t_start; - prev_descent_direction = descent_direction; - - // Compute corner angles and the constraint with its jacobian - // TODO Add safety checks - t_start = timer.getElapsedTime(); - dirichlet_metric.make_discrete_metric(); - dirichlet_metric.get_corner_angles(alpha, cot_alpha); - constraint = compute_metric_constraint(dirichlet_metric, alpha); - J = compute_metric_constraint_jacobian(dirichlet_metric, cot_alpha); - log.constraint_time = timer.getElapsedTime() - t_start; - dirichlet_metric.write_status_log(metric_status_file); - - // Compute Newton descent direction from the constraint and jacobian - t_start = timer.getElapsedTime(); - solve_linear_system(metric_basis_matrix); - log.direction_time = timer.getElapsedTime() - t_start; - log.direction_norm = descent_direction.norm(); - log.direction_residual = (J * descent_direction + constraint).norm(); - } - - // Update the corner angles, constraint, constraint jacobian, and descent direction given the - // marked metric - void update_boundary_descent_direction( - DirichletPennerConeMetric& dirichlet_metric, - const MatrixX& metric_basis_matrix) - { - double t_start; - prev_descent_direction = descent_direction; - - // Compute corner angles and the constraint with its jacobian - // TODO Add safety checks - t_start = timer.getElapsedTime(); - dirichlet_metric.make_discrete_metric(); - constraint = compute_boundary_constraint(dirichlet_metric); - J = compute_boundary_constraint_jacobian(dirichlet_metric); - log.constraint_time = timer.getElapsedTime() - t_start; - dirichlet_metric.write_status_log(metric_status_file); - - // Compute Newton descent direction from the constraint and jacobian - t_start = timer.getElapsedTime(); - solve_linear_system(metric_basis_matrix); - log.direction_time = timer.getElapsedTime() - t_start; - log.direction_norm = descent_direction.norm(); - log.direction_residual = (J * descent_direction + constraint).norm(); - } - - // Update the corner angles, constraint, constraint jacobian, and descent direction given the - // marked metric - void update_all_descent_direction( - DirichletPennerConeMetric& dirichlet_metric, - const MatrixX& metric_basis_matrix) - { - double t_start; - prev_descent_direction = descent_direction; - - // Compute corner angles and the constraint with its jacobian - // TODO Add safety checks - t_start = timer.getElapsedTime(); - dirichlet_metric.make_discrete_metric(); - dirichlet_metric.get_corner_angles(alpha, cot_alpha); - constraint = dirichlet_metric.constraint(alpha); - J = dirichlet_metric.constraint_jacobian(cot_alpha); - log.constraint_time = timer.getElapsedTime() - t_start; - dirichlet_metric.write_status_log(metric_status_file); - - // Compute Newton descent direction from the constraint and jacobian - t_start = timer.getElapsedTime(); - solve_linear_system(metric_basis_matrix); - log.direction_time = timer.getElapsedTime() - t_start; - log.direction_norm = descent_direction.norm(); - log.direction_residual = (J * descent_direction + constraint).norm(); - } - - void update_dirichlet_descent_direction( - DirichletPennerConeMetric& dirichlet_metric, - const MatrixX& metric_basis_matrix) - { - if (constraint_type == ConstraintType::holonomy) - { - update_holonomy_descent_direction(dirichlet_metric, metric_basis_matrix); - } - else if (constraint_type == ConstraintType::boundary) - { - update_boundary_descent_direction(dirichlet_metric, metric_basis_matrix); - } - else if (constraint_type == ConstraintType::all) - { - update_all_descent_direction(dirichlet_metric, metric_basis_matrix); - } - } - - // Perform a backtracking line search along the current descent direction from the current - // metric coordinates using the initial metric connectivity - void perform_dirichlet_line_search( - const DirichletPennerConeMetric& initial_dirichlet_metric, - DirichletPennerConeMetric& dirichlet_metric, - const MatrixX& metric_basis_matrix) - { - double t_start = timer.getElapsedTime(); - - // Get starting metric coordinates - VectorX reduced_metric_start = reduced_metric_coords; - VectorX d = descent_direction; - - // Get the constraint norm and its dot product with the jacobian-projected descent direction - // Note: the product of the jacobian and descent direction should be the negative - // constraint, but this may fail due to numerical instability or regularization - Scalar l2_c0_sq = constraint.squaredNorm(); - Scalar proj_g0 = constraint.dot(J * descent_direction); - Scalar Jg0_sq = (J.transpose() * constraint).squaredNorm(); - spdlog::debug("Initial squared error norm is {}", l2_c0_sq); - spdlog::debug("Initial projected constraint is {}", proj_g0); - - // Reduce descent direction range to avoid nans/infs - if (alg_params.do_reduction) { - while (lambda * (descent_direction.maxCoeff() - descent_direction.minCoeff()) > 2.5) { - lambda /= 2; - spdlog::debug("Reducing lambda to {} for stability", lambda); - } - } - - // Make initial line step with updated constraint - reduced_metric_coords = reduced_metric_start + lambda * descent_direction; - dirichlet_metric.change_metric(initial_dirichlet_metric, reduced_metric_coords, false, false); - update_dirichlet_constraint(dirichlet_metric); - - // Ideas for line search - // - Use descent direction norm (with linearized or actual jacobian) - // - Use energy slack - - // Line search until the constraint norm decreases and the projected constraint is - // nonpositive We also allow the norm bound to be dropped or made approximate with some - // relative term alpha - spdlog::debug("Starting line search"); - spdlog::debug("{}", metric_basis_matrix.norm()); - Scalar l2_c_sq = constraint.squaredNorm(); - Scalar proj_cons = constraint.dot(J * d); - Scalar Jg_sq = (J.transpose() * constraint).squaredNorm(); - Scalar gamma = 1.; // require ratio of current to initial constraint norm to be below alpha - bool bound_norm = true; - while ( - (proj_cons > 0) - || (bound_norm && (l2_c_sq > (gamma * l2_c0_sq))) - || (0. * Jg_sq > Jg0_sq) - ) { - // Backtrack one step - lambda /= 2; - - // If lambda low enough, stop bounding the norm - if (lambda <= alg_params.bound_norm_thres) { - bound_norm = false; - spdlog::debug("Dropping norm bound."); - } - - // Change metric and update constraint - reduced_metric_coords = reduced_metric_start + lambda * descent_direction; - dirichlet_metric.change_metric(initial_dirichlet_metric, reduced_metric_coords, false, false); - update_dirichlet_constraint(dirichlet_metric); - - // Update squared constraint norm and projected constraint - l2_c_sq = constraint.squaredNorm(); - proj_cons = constraint.dot(J * d); - Jg_sq = (J.transpose() * constraint).squaredNorm(); - spdlog::debug("Squared error norm is {}", l2_c_sq); - spdlog::debug("Projected constraint is {}", proj_cons); - - // Check if lambda is below the termination threshold - if (lambda < alg_params.min_lambda) break; - } - - // Make final line step in original connectivity - spdlog::debug("Updating metric"); - reduced_metric_coords = reduced_metric_start + lambda * descent_direction; - dirichlet_metric.change_metric(initial_dirichlet_metric, reduced_metric_coords, true, false); - - log.line_search_time = timer.getElapsedTime() - t_start; - } - - // Determine if the optimization has converged or maximum time/iteration is reached - bool is_converged() - { - if (constraint.cwiseAbs().maxCoeff() < alg_params.error_eps) { - spdlog::info("Stopping optimization as max error {} reached", alg_params.error_eps); - return true; - } - if (lambda < alg_params.min_lambda) { - spdlog::info("Stopping optimization as step size {} too small", lambda); - return true; - } - if (log.num_iter >= alg_params.max_itr) { - spdlog::trace( - "Stopping optimization as reached maximum iteration {}", - alg_params.max_itr); - return true; - } - if (timer.getElapsedTime() >= alg_params.max_time) { - spdlog::trace("Stopping optimization as reached maximum time {}", alg_params.max_time); - return true; - } - - return false; - } - - // Main method to run the optimization with a given metric, metric basis, and parameters - // Each run completely resets the state of the optimization - DirichletPennerConeMetric run( - const DirichletPennerConeMetric& initial_dirichlet_metric, - const MatrixX& metric_basis_matrix, - const NewtonParameters& input_alg_params) - { - // Initialize logging methods - timer.start(); - alg_params = input_alg_params; - lambda = alg_params.lambda0; - l2_energy = std::make_unique( - Optimization::LogLengthEnergy(initial_dirichlet_metric)); - initialize_logging(); - initialize_logs(); - initialize_checkpoints(); - checkpoint_metric(initial_dirichlet_metric); - - // Get initial metric - DirichletPennerConeMetric dirichlet_metric = initial_dirichlet_metric; - reduced_metric_init = dirichlet_metric.get_reduced_metric_coordinates(); - reduced_metric_coords = reduced_metric_init; - initialize_metric_status_log(dirichlet_metric); - - // Get initial constraint - constraint_type = ConstraintType::all; - update_dirichlet_constraint(dirichlet_metric); - - // Get before-first-iteration information - update_log_error(dirichlet_metric); - write_log_entries(); - spdlog::info("itr(0) lm({}) max_error({}))", lambda, log.max_error); - - int itr = 0; - constraint_type = ConstraintType::all; - while (true) { - // Check termination conditions - if ((is_converged()) && (constraint_type == ConstraintType::holonomy)) - { - spdlog::info("Adding boundary constraints"); - constraint_type = ConstraintType::all; - } - else if ((is_converged()) && (constraint_type == ConstraintType::all)) - { - break; - } - - // Increment iteration - itr++; - log.num_iter = itr; - - // Do descent for holonomy constraint and boundary constraint separately - bool alternate = false; - if (alternate) { - constraint_type = ConstraintType::holonomy; - update_holonomy_descent_direction(dirichlet_metric, metric_basis_matrix); - perform_dirichlet_line_search(initial_dirichlet_metric, dirichlet_metric, metric_basis_matrix); - - // Do descent for boundary constraint - constraint_type = ConstraintType::boundary; - update_boundary_descent_direction(dirichlet_metric, metric_basis_matrix); - perform_dirichlet_line_search(initial_dirichlet_metric, dirichlet_metric, metric_basis_matrix); - } - - // Do descent for all constraints - update_dirichlet_descent_direction(dirichlet_metric, metric_basis_matrix); - perform_dirichlet_line_search(initial_dirichlet_metric, dirichlet_metric, metric_basis_matrix); - - // Log current step - log.step_size = lambda; - log.time = timer.getElapsedTime(); - update_log_error(dirichlet_metric); - write_log_entries(); - checkpoint_metric(dirichlet_metric); - spdlog::info("itr({}) lm({}) max_error({}))", itr, lambda, log.max_error); - - // Update lambda - update_lambda(); - } - - // Close logging - close_logs(); - metric_status_file.close(); - - // Change metric to final values and restore the original connectivity - dirichlet_metric.change_metric(initial_dirichlet_metric, reduced_metric_coords, true, false); - - return dirichlet_metric; - } - - - // Trivial constructor (stateless optimization method) - OptimizeDirichletNewton() {} - -private: - VectorX holonomy_constraint; - VectorX boundary_constraint; - enum ConstraintType { - boundary, - holonomy, - all }; - ConstraintType constraint_type; -}; - -DirichletPennerConeMetric optimize_dirichlet_metric( - const DirichletPennerConeMetric& initial_dirichlet_metric, - const NewtonParameters& alg_params) -{ - // Optimize metric with full metric space (basis is identity) - MatrixX identity = id_matrix(initial_dirichlet_metric.n_reduced_coordinates()); - OptimizeDirichletNewton solver; - return solver.run(initial_dirichlet_metric, identity, alg_params); -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/patches.cpp b/src/feature/experimental/patches.cpp deleted file mode 100644 index 9443530..0000000 --- a/src/feature/experimental/patches.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/patches.h" - -namespace Penner { -namespace Feature { - -/* -ConvexPatchDecomposition::ConvexPatchDecomposition(const Mesh& m) - : m_mesh(m) - , m_cuts({}) - , m_is_cut(m.n_halfedges(), false) -{ - initialize_boundary_edges(); -} - -void ConvexPatchDecomposition::initialize_boundary_edges() -{ - // get boundary edges - m_boundary_edges = find_primal_boundary_halfedges(m); - - // build map from all halfedges to boundary halfedges - int num_bd_edges = m_boundary_edges.size(); - m_h2bd = std::vector(num_bd_edges, -1); - for (int i = 0; i < num_bd_edges; ++i) - { - m_h2bd[m_boundary_edges[i]] = i; - } - - // get components - m_components = find_boundary_components(m); - - // build next and prev maps from components - for (int h_start : m_components) { - std::vector component = build_boundary_component(m, h_start); - int component_size = component.size(); - for (int i = 0; i < component.size(); ++i) - { - int j = (i + 1) % component_size; - int curr = m_h2bd[component[i]]; - int next = m_h2bd[component[j]]; - m_next[curr] = next; - m_prev[next] = curr; - } - } - - // - -} -*/ - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/penner_optimization.cpp b/src/feature/experimental/penner_optimization.cpp deleted file mode 100644 index 42d3f1e..0000000 --- a/src/feature/experimental/penner_optimization.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -/// Build a VF mesh for the embedded mesh in the doubled mesh and also extract -/// the mapping from VF mesh corners to opposite halfedge index -/// -/// @param[in] m: input mesh -/// @param[in] vtx_reindex: reindexing for the vertices wrt the mesh indexing -/// @param[out] F: mesh faces -/// @param[out] corner_to_halfedge: mesh corner to opposite halfedge indexing -void extract_embedded_mesh( - const Mesh& m, - const std::vector& vtx_reindex, - Eigen::MatrixXi& F, - Eigen::MatrixXi& corner_to_halfedge) -{ - // Get number of vertices and faces for the embedded mesh - int num_faces = m.n_faces(); - int num_embedded_faces = 0; - for (int f = 0; f < num_faces; ++f) { - // Skip face if it is in the doubled mesh - int hij = m.h[f]; - // TODO Hack for closed overlay meshes. A better solution would be to have - // a separate method for overlay meshes - if ((!m.type.empty()) && (m.type[hij] == 2)) continue; - - // Count embedded faces - num_embedded_faces++; - } - - // Build faces and halfedge lengths - std::vector> F_vec, corner_to_halfedge_vec; - F_vec.reserve(num_embedded_faces); - corner_to_halfedge_vec.reserve(num_embedded_faces); - for (int f = 0; f < num_faces; ++f) { - // Get halfedges of face - int hli = m.h[f]; - int hij = m.n[hli]; - int hjk = m.n[hij]; - - // TODO Hack for closed overlay meshes. A better solution would be to have - // a separate method for overlay meshes - if ((!m.type.empty()) && (m.type[hij] == 2)) continue; - - // Get vertices of the fist face - int vi = m.to[hli]; - int vj = m.to[hij]; - int vk = m.to[hjk]; - - // Triangle case (l == k) - if (m.n[hjk] == hli) { - // Build vertex embedding for the face - F_vec.push_back({vtx_reindex[vi], vtx_reindex[vj], vtx_reindex[vk]}); - - // Build halfedge index map for the face - corner_to_halfedge_vec.push_back({hjk, hli, hij}); - } - // Polygon case - else { - // Build vertex embedding for the first face - F_vec.push_back({vtx_reindex[vi], vtx_reindex[vj], vtx_reindex[vk]}); - - // Build halfedge index map for the first face - corner_to_halfedge_vec.push_back({hjk, -1, hij}); - - int hkp = m.n[hjk]; - while (m.n[hkp] != hli) { - // Get vertices of the interior face - int vk = m.to[hjk]; - int vp = m.to[hkp]; - - // Build vertex embedding for the interior face for halfedge hkp - F_vec.push_back({vtx_reindex[vi], vtx_reindex[vk], vtx_reindex[vp]}); - - // Build halfedge index map for the interior face for halfedge hkp - corner_to_halfedge_vec.push_back({hkp, -1, -1}); - - // Increment halfedges - hkp = m.n[hkp]; - hjk = m.n[hjk]; - } - - // Get vertices of the final face (p == l) - int vk = m.to[hjk]; - int vp = m.to[hkp]; - - // Build vertex embedding for the final face - F_vec.push_back({vtx_reindex[vi], vtx_reindex[vk], vtx_reindex[vp]}); - - // Build halfedge index map for the final face - corner_to_halfedge_vec.push_back({hkp, hli, -1}); - } - } - - // Copy lists of lists to matrices - int num_triangles = F_vec.size(); - F.resize(num_triangles, 3); - corner_to_halfedge.resize(num_triangles, 3); - for (int fi = 0; fi < num_triangles; ++fi) { - for (int j = 0; j < 3; ++j) { - F(fi, j) = F_vec[fi][j]; - corner_to_halfedge(fi, j) = corner_to_halfedge_vec[fi][j]; - } - } -} \ No newline at end of file diff --git a/src/feature/experimental/polygon_cones.cpp b/src/feature/experimental/polygon_cones.cpp deleted file mode 100644 index c191f5c..0000000 --- a/src/feature/experimental/polygon_cones.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "feature/experimental/polygon_cones.h" - -#include "util/boundary.h" - - -namespace Penner { -namespace Feature { - -std::vector generate_polygon_cones( - const Mesh& m, - const std::vector& vtx_reindex, - int num_vertices, - bool use_length) -{ - if (use_length) - { - // FIXME - spdlog::info("Length based polygon cone generation not implemented"); - return {}; - } - - // Check if topological disk - int genus = Holonomy::compute_genus(m); - std::vector boundary_components = find_boundary_components(m); - if ((genus != 0) || (boundary_components.size() != 1)) { - spdlog::error("Polygon cones only supported for disk topology"); - return {}; - } - - // Build unique boundary component - std::vector boundary_component = build_boundary_component(m, boundary_components.front()); - int num_bd_edges = boundary_component.size(); - - // Find delta for indexing - if ((num_vertices <= 0) || (num_vertices > num_bd_edges)) - { - num_vertices = num_bd_edges; - } - int delta = num_bd_edges / num_vertices; - - // Build list of vertices to make cones - int num_ind_vertices = m.n_ind_vertices(); - std::vector Th_hat(num_ind_vertices, 2. * M_PI); - for (int hij : boundary_component) - { - int vj = m.v_rep[m.to[hij]]; - Th_hat[vtx_reindex[vj]] = M_PI; - } - - // Add defect for corners - Scalar cone_defect = (2 * M_PI) / num_vertices; - for (int i = 0; i < num_vertices; ++i) - { - int hij = boundary_component[i * delta]; - int vj = m.v_rep[m.to[hij]]; - Th_hat[vtx_reindex[vj]] -= cone_defect; - } - - return Th_hat; - -} - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/experimental/transfer.cpp b/src/feature/experimental/transfer.cpp deleted file mode 100644 index 1af5307..0000000 --- a/src/feature/experimental/transfer.cpp +++ /dev/null @@ -1,221 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#include "feature/experimental/transfer.h" - -#include "feature/core/union_meshes.h" -#include "holonomy/holonomy/cones.h" - -#include "util/vector.h" - -namespace Penner { -namespace Feature { - - -// compute map from vertex-vertex edges to halfedges for cut mesh -Eigen::SparseMatrix generate_cut_VV_to_halfedge_map( - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map) -{ - int n_v = V_map.maxCoeff() + 1; - Eigen::SparseMatrix vv2he(n_v, n_v); - - // build sparse matrix - typedef Eigen::Triplet Trip; - std::vector trips; - int n_cut_he = m.n_halfedges(); - for (int hij = 0; hij < n_cut_he; ++hij) { - if (m.type[hij] > 1) continue; - - int hji = m.opp[hij]; - int vi = V_map[vtx_reindex[m.v_rep[m.to[hji]]]]; - int vj = V_map[vtx_reindex[m.v_rep[m.to[hij]]]]; - trips.push_back(Trip(vi, vj, hij + 1)); - } - vv2he.setFromTriplets(trips.begin(), trips.end()); - - return vv2he; -} - -VectorX transfer_edge_data( - const Mesh& m_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const VectorX& halfedge_data, - bool is_signed) -{ - Eigen::SparseMatrix vv2he = generate_cut_VV_to_halfedge_map(m, vtx_reindex, V_map); - - // iterate over halfedges - int num_halfedges = m_0.n_halfedges(); - VectorX halfedge_data_0(num_halfedges); - for (int hij = 0; hij < num_halfedges; ++hij) { - int hji = m_0.opp[hij]; - if (hij < hji) continue; // only process each edge once - if ((m_0.type[hij] == 2) && (m_0.type[hji] == 2)) - continue; // don't process copied interior edges - - // get corrresponding (potentially cut) halfedge in domain mesh from VV values - int vi = V_map_0[vtx_reindex_0[m_0.v_rep[m_0.to[hji]]]]; - int vj = V_map_0[vtx_reindex_0[m_0.v_rep[m_0.to[hij]]]]; - int domain_hij = vv2he.coeffRef(vi, vj) - 1; - assert(domain_hij >= 0); - - // if target mesh edge is cut, just copy edge value directly - if (m_0.type[hij] != m_0.type[hji]) - { - halfedge_data_0[hij] = halfedge_data[domain_hij]; - halfedge_data_0[hji] = halfedge_data[m.opp[domain_hij]]; - } - - // get (type 1) domain halfedge corresponding to the opposite halfedge in the original mesh before cuts - // WARNING: this is generally different than the (type 2) opposite halfedge in the cut mesh - int domain_hji = vv2he.coeffRef(vj, vi) - 1; - assert(domain_hji >= 0); - - // for signed values, take half of difference between values - if (is_signed) - { - halfedge_data_0[hij] = 0.5 * (halfedge_data[domain_hij] - halfedge_data[domain_hji]); - halfedge_data_0[hji] = -halfedge_data_0[hij]; - - // copy to reflection with correct sign - if (m_0.type[hji] == 1) { - halfedge_data_0[m_0.R[hij]] = -halfedge_data_0[hij]; - halfedge_data_0[m_0.R[hji]] = halfedge_data_0[hij]; - } - } - // for unsigned values, take standard average - else - { - halfedge_data_0[hij] = 0.5 * (halfedge_data[domain_hij] + halfedge_data[domain_hji]); - halfedge_data_0[hji] = halfedge_data_0[hij]; - - // copy to reflection - if (m_0.type[hji] == 1) { - halfedge_data_0[m_0.R[hij]] = halfedge_data_0[hij]; - halfedge_data_0[m_0.R[hji]] = halfedge_data_0[hij]; - } - } - } - - return halfedge_data_0; -} - -void transfer_metric( - MarkedPennerConeMetric& marked_metric_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const MarkedPennerConeMetric& marked_metric, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map) -{ - // transfer unsigned metric values - VectorX metric_coords = marked_metric.get_metric_coordinates(); - bool is_signed = false; - VectorX metric_coords_0 = transfer_edge_data( - marked_metric_0, - vtx_reindex_0, - V_map_0, - marked_metric, - vtx_reindex, - V_map, - metric_coords, - is_signed); - - // change metric with new coordinates - marked_metric_0.change_metric(marked_metric_0, metric_coords_0); -} - -VectorX transfer_rotation_form( - const Mesh& m_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const VectorX& rotation_form) -{ - // transfer signed rotation form - bool is_signed = true; - return transfer_edge_data( - m_0, - vtx_reindex_0, - V_map_0, - m, - vtx_reindex, - V_map, - rotation_form, - is_signed); -} - -/* -DEPRECATED -std::tuple, VectorX, std::vector> -generate_transferred_metric( - const Eigen::MatrixXd& V_cut, - const Eigen::MatrixXi& F_cut, - const Eigen::VectorXi& V_map, - const MarkedPennerConeMetric& domain_marked_metric, - const std::vector& domain_vtx_reindex, - const Eigen::VectorXi& domain_V_map, - const VectorX& domain_rotation_form, - Holonomy::MarkedMetricParameters marked_metric_params) -{ - // generate mesh for the new cut topology - auto [union_mesh, union_vtx_reindex] = generate_union_mesh(V_cut, F_cut); - - // transfer rotation form and cone constraints to new union mesh - VectorX transferred_rotation_form = transfer_rotation_form( - union_mesh, - union_vtx_reindex, - V_map, - domain_marked_metric, - domain_vtx_reindex, - domain_V_map, - domain_rotation_form); - std::vector transferred_Th_hat = Holonomy::generate_cones_from_rotation_form( - union_mesh, - union_vtx_reindex, - transferred_rotation_form, - true); - union_mesh.Th_hat = Holonomy::generate_cones_from_rotation_form(union_mesh, transferred_rotation_form); - - // add transferred metric to union mesh - auto union_metric = generate_marked_metric_components( - union_mesh, - transferred_rotation_form, - marked_metric_params); - transfer_metric( - union_metric, - union_vtx_reindex, - V_map, - domain_marked_metric, - domain_vtx_reindex, - domain_V_map); - - // add Dirichlet conditions to union metric - DirichletPennerConeMetric transfered_metric = - generate_dirichlet_metric_from_mesh(union_metric, union_vtx_reindex, V_map); - - return std::make_tuple( - transfered_metric, - union_vtx_reindex, - transferred_rotation_form, - transferred_Th_hat); -} -*/ - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/src/feature/interface.cpp b/src/feature/interface.cpp index 08ddc8a..b6b308e 100644 --- a/src/feature/interface.cpp +++ b/src/feature/interface.cpp @@ -16,7 +16,6 @@ #include "util/boundary.h" #include "parametrization/refinement.h" -#include "metric/common.h" #include "holonomy/interface.h" #include "field/frame_field.h" diff --git a/src/metric/common.cpp b/src/metric/common.cpp deleted file mode 100644 index 35f1651..0000000 --- a/src/metric/common.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "metric/common.h" - -namespace Penner { - -bool vector_equal(VectorX v, VectorX w, Scalar eps) -{ - // Check if the sizes are the same - if (v.size() != w.size()) return false; - - // Check per element equality - for (Eigen::Index i = 0; i < v.size(); ++i) { - if (!float_equal(v[i], w[i], eps)) return false; - } - - // Equal otherwise - return true; -} - -bool matrix_contains_nan(const Eigen::MatrixXd& mat) -{ - for (Eigen::Index i = 0; i < mat.rows(); ++i) { - for (Eigen::Index j = 0; j < mat.cols(); ++j) { - if (std::isnan(mat(i, j))) return true; - } - } - - return false; -} - -Scalar sup_norm(const VectorX& v) -{ - Scalar norm_value = 0.0; - for (Eigen::Index i = 0; i < v.size(); ++i) { - norm_value = max(norm_value, abs(v[i])); - } - - return norm_value; -} - -Scalar matrix_sup_norm(const MatrixX& matrix) -{ - // Check for trivial matrices - if (matrix.size() == 0) return 0; - - // Iterate to determine maximum abs value - Scalar max_value = 0.0; - for (Eigen::Index k = 0; k < matrix.outerSize(); ++k) { - for (MatrixX::InnerIterator it(matrix, k); it; ++it) { - max_value = std::max(max_value, abs(it.value())); - } - } - - return max_value; -} - -} // namespace Penner \ No newline at end of file diff --git a/src/optimization/CMakeLists.txt b/src/optimization/CMakeLists.txt index 455c975..9af5e22 100644 --- a/src/optimization/CMakeLists.txt +++ b/src/optimization/CMakeLists.txt @@ -1,6 +1,5 @@ set(MetricOptimizationSrc - metric_optimization/convergence.cpp metric_optimization/energies.cpp metric_optimization/energy_functor.cpp metric_optimization/energy_weights.cpp @@ -22,7 +21,6 @@ add_library(PennerOptimizationLib target_include_directories(PennerOptimizationLib PUBLIC ../../include/optimization) target_link_libraries(PennerOptimizationLib PUBLIC PennerCoreLib - ${RENDER_LIBRARIES} ${PYBIND_LIBRARIES} ) target_compile_definitions(PennerOptimizationLib PUBLIC diff --git a/src/optimization/metric_optimization/convergence.cpp b/src/optimization/metric_optimization/convergence.cpp deleted file mode 100644 index b397ee4..0000000 --- a/src/optimization/metric_optimization/convergence.cpp +++ /dev/null @@ -1,218 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "optimization/metric_optimization/convergence.h" - -#include "metric/constraint.h" -#include "optimization/metric_optimization/explicit_optimization.h" -#include "optimization/metric_optimization/implicit_optimization.h" -#include "metric/projection.h" - - -namespace Penner { -namespace Optimization { - -void compute_direction_energy_values( - const DifferentiableConeMetric& m, - const EnergyFunctor& opt_energy, - std::shared_ptr opt_params, - std::shared_ptr proj_params, - const VectorX& direction, - const VectorX& step_sizes, - VectorX& unprojected_energies, - VectorX& projected_energies) -{ - VectorX reduced_metric_coords = m.get_reduced_metric_coordinates(); - - // Compute energies at step sizes - int num_nodes = step_sizes.size(); - unprojected_energies.resize(num_nodes); - projected_energies.resize(num_nodes); - for (int i = 0; i < num_nodes; ++i) { - // Compute unprojected step - Scalar step_size = step_sizes[i]; - VectorX reduced_line_step_metric_coords = reduced_metric_coords + step_size * direction; - - // Compute the unprojected energy - std::unique_ptr cone_metric = m.clone_cone_metric(); - unprojected_energies[i] = opt_energy.energy(*cone_metric); - - // Project to the constraint - VectorX u = - compute_constraint_scale_factors(*cone_metric, proj_params, opt_params->output_dir); - std::unique_ptr constrained_cone_metric = - cone_metric->scale_conformally(u); - - // Compute the projected energy - projected_energies[i] = opt_energy.energy(*constrained_cone_metric); - } -} - -/* -TODO: Refactor this energy landscape code and expose to python - void - compute_domain_direction_energy_values( - const DifferentiableConeMetric &m, - const ReductionMaps &reduction_maps, - const VectorX &reduced_metric_target, - const VectorX &domain_coords, - const MatrixX &constraint_domain_matrix, - const MatrixX &constraint_codomain_matrix, - const std::shared_ptr opt_params, - const std::shared_ptr proj_params, - const VectorX &direction, - const VectorX &step_sizes, - VectorX &energies) - { - // Expand the target metric coordinates to the doubled surface - VectorX metric_target; - expand_reduced_function( - reduction_maps.proj, reduced_metric_target, metric_target); - - // Build energy functions for given energy - EnergyFunctor opt_energy(m, metric_target, *opt_params); - - // Compute energies along descent direction - int num_steps = step_sizes.size(); - energies.resize(num_steps); - for (int i = 0; i < num_steps; ++i) - { - Scalar step_size = step_sizes[i]; - VectorX line_step_domain_coords = domain_coords + step_size * direction; - - // Compute the gradient for the shear metric coordinates - Scalar energy; - VectorX gradient; - compute_domain_coordinate_energy_with_gradient( - m, - reduction_maps, - opt_energy, - line_step_domain_coords, - constraint_domain_matrix, - constraint_codomain_matrix, - proj_params, - opt_params, - energy, - gradient); - energies[i] = energy; - } - } - - void - compute_projected_descent_direction_stability_values( - const DifferentiableConeMetric &m, - const VectorX &reduced_metric_coords, - const VectorX &reduced_metric_target, - const OptimizationParameters &opt_params, - const std::shared_ptr proj_params, - const VectorX &step_sizes, - VectorX &max_errors, - VectorX &norm_changes_in_metric_coords, - VectorX &convergence_ratios, - VectorX &gradient_signs) - { - // Compute descent directions - VectorX gradient; - VectorX descent_direction; - VectorX projected_descent_direction; - compute_descent_directions( - m, - reduced_metric_coords, - reduced_metric_target, - opt_params, - gradient, - descent_direction, - projected_descent_direction); - - // Get edge maps - std::vector he2e; - std::vector e2he; - build_edge_maps(m, he2e, e2he); - - // Build refl projection and embedding - std::vector proj; - std::vector embed; - build_refl_proj(m, he2e, e2he, proj, embed); - - // Expand the target metric coordinates to the doubled surface - VectorX metric_target; - expand_reduced_function( - proj, reduced_metric_target, metric_target); - - // Build energy functions for given energy - EnergyFunctor opt_energy(m, metric_target, opt_params); - - // Compute energies at step sizes - int num_nodes = step_sizes.size(); - max_errors.resize(num_nodes); - norm_changes_in_metric_coords.resize(num_nodes); - convergence_ratios.resize(num_nodes); - gradient_signs.resize(num_nodes); - for (int i = 0; i < num_nodes; ++i) - { - // Compute unprojected step - Scalar step_size = step_sizes[i]; - VectorX reduced_line_step_metric_coords = reduced_metric_coords + step_size * -projected_descent_direction; - - // Project to the constraint - VectorX u; - u.setZero(m.n_ind_vertices()); - VectorX reduced_updated_metric_coords; - project_to_constraint(m, - reduced_line_step_metric_coords, - reduced_updated_metric_coords, - u, - proj_params); - VectorX updated_metric_coords; - expand_reduced_function(proj, - reduced_updated_metric_coords, - updated_metric_coords); - - // Compute constraint values - VectorX constraint; - MatrixX J_constraint; - std::vector flip_seq; - bool need_jacobian = false; - bool use_edge_lengths = false; - constraint_with_jacobian(m, - updated_metric_coords, - constraint, - J_constraint, - flip_seq, - need_jacobian, - use_edge_lengths); - - // Compute change in metric coords - VectorX change_in_metric_coords = reduced_updated_metric_coords - reduced_metric_coords; - - // Compute descent directions - VectorX step_gradient; - VectorX step_descent_direction; - VectorX step_projected_descent_direction; - compute_descent_directions( - m, - reduced_updated_metric_coords, - reduced_metric_target, - opt_params, - step_gradient, - step_descent_direction, - step_projected_descent_direction); - - // Compute numerics - max_errors[i] = constraint.maxCoeff(); - norm_changes_in_metric_coords[i] = change_in_metric_coords.norm(); - convergence_ratios[i] = compute_convergence_ratio(step_descent_direction, -step_projected_descent_direction); gradient_signs[i] = --step_projected_descent_direction.dot(projected_descent_direction); - } - } - */ - -} // namespace Optimization -} // namespace Penner \ No newline at end of file diff --git a/src/optimization/metric_optimization/explicit_optimization.cpp b/src/optimization/metric_optimization/explicit_optimization.cpp index bc71739..9d7c5d3 100644 --- a/src/optimization/metric_optimization/explicit_optimization.cpp +++ b/src/optimization/metric_optimization/explicit_optimization.cpp @@ -314,7 +314,7 @@ VectorX compute_codomain_coordinates( Eigen::SimplicialLDLT> solver; solver.compute(inner_product_matrix); VectorX codomain_coords = solver.solve(rhs); - SPDLOG_DEBUG(vector_equal( + SPDLOG_DEBUG(vector_equal( constraint_domain_matrix * domain_coords + constraint_codomain_matrix * codomain_coords, metric_coords)); return codomain_coords; diff --git a/src/optimization/metric_optimization/implicit_optimization.cpp b/src/optimization/metric_optimization/implicit_optimization.cpp index 888a2e5..8b99616 100644 --- a/src/optimization/metric_optimization/implicit_optimization.cpp +++ b/src/optimization/metric_optimization/implicit_optimization.cpp @@ -761,5 +761,41 @@ std::unique_ptr optimize_metric( return optimize_metric_log(initial_cone_metric, opt_energy, log, proj_params, opt_params); } +void compute_direction_energy_values( + const DifferentiableConeMetric& m, + const EnergyFunctor& opt_energy, + std::shared_ptr opt_params, + std::shared_ptr proj_params, + const VectorX& direction, + const VectorX& step_sizes, + VectorX& unprojected_energies, + VectorX& projected_energies) +{ + VectorX reduced_metric_coords = m.get_reduced_metric_coordinates(); + + // Compute energies at step sizes + int num_nodes = step_sizes.size(); + unprojected_energies.resize(num_nodes); + projected_energies.resize(num_nodes); + for (int i = 0; i < num_nodes; ++i) { + // Compute unprojected step + Scalar step_size = step_sizes[i]; + VectorX reduced_line_step_metric_coords = reduced_metric_coords + step_size * direction; + + // Compute the unprojected energy + std::unique_ptr cone_metric = m.clone_cone_metric(); + unprojected_energies[i] = opt_energy.energy(*cone_metric); + + // Project to the constraint + VectorX u = + compute_constraint_scale_factors(*cone_metric, proj_params, opt_params->output_dir); + std::unique_ptr constrained_cone_metric = + cone_metric->scale_conformally(u); + + // Compute the projected energy + projected_energies[i] = opt_energy.energy(*constrained_cone_metric); + } +} + } // namespace Optimization } // namespace Penner \ No newline at end of file diff --git a/src/optimization/pybind.cpp b/src/optimization/pybind.cpp index f3886f0..088a0f7 100644 --- a/src/optimization/pybind.cpp +++ b/src/optimization/pybind.cpp @@ -8,11 +8,9 @@ #include "optimization/pybind.h" -#include "metric/common.h" #include "metric/area.h" #include "metric/constraint.h" -#include "optimization/metric_optimization/convergence.h" #include "util/embedding.h" #include "util/io.h" #include "optimization/metric_optimization/energies.h" @@ -37,7 +35,6 @@ #ifdef RENDER_TEXTURE #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "conformal_ideal_delaunay/Sampling.hh" -#include "visualization.h" #endif namespace Penner { @@ -106,10 +103,6 @@ void init_classes_pybind(pybind11::module& m) .def_readwrite("max_grad_range", &OptimizationParameters::max_grad_range) .def_readwrite("max_angle", &OptimizationParameters::max_angle); -#ifdef RENDER_TEXTURE - pybind11::class_(m, "Viewer").def(pybind11::init<>()); -#endif - pybind11::class_(m, "Connectivity") .def(pybind11::init<>()) .def_readwrite("n", &Connectivity::n) @@ -285,18 +278,6 @@ void init_conformal_pybind(pybind11::module& m) m.def("cpp_viewer", &cpp_viewer, "viewer mesh in libigl gui"); m.def("get_pt_mat", &get_pt_mat, "get pt_mat"); m.def("get_edges", &get_edges, "get edges mesh"); - m.def( - "generate_mesh_viewer", - &generate_mesh_viewer, - "generate viewer for mesh", - pybind11:: - call_guard()); - m.def( - "add_shading_to_mesh", - &add_shading_to_mesh, - "add shading to mesh", - pybind11:: - call_guard()); #endif } diff --git a/src/optimization/tests/test_area.cpp b/src/optimization/tests/test_area.cpp index 0895762..a1f4808 100644 --- a/src/optimization/tests/test_area.cpp +++ b/src/optimization/tests/test_area.cpp @@ -9,8 +9,8 @@ #include #include "metric/area.h" +#include "util/vector.h" #include "optimization/util/shapes.h" -#include "metric/common.h" #include "metric/cone_metric.h" using namespace Penner; diff --git a/src/optimization/tests/test_energies.cpp b/src/optimization/tests/test_energies.cpp index 909cc9f..4f86c9d 100644 --- a/src/optimization/tests/test_energies.cpp +++ b/src/optimization/tests/test_energies.cpp @@ -10,7 +10,6 @@ #include "optimization/metric_optimization/energies.h" #include "optimization/util/shapes.h" -#include "metric/common.h" #include "metric/cone_metric.h" using namespace Penner; diff --git a/src/optimization/tests/test_optimize.cpp b/src/optimization/tests/test_optimize.cpp index e5d74fd..3524be4 100644 --- a/src/optimization/tests/test_optimize.cpp +++ b/src/optimization/tests/test_optimize.cpp @@ -10,7 +10,6 @@ #include "optimization/metric_optimization/implicit_optimization.h" #include "optimization/util/shapes.h" -#include "metric/common.h" #include "metric/cone_metric.h" #include "optimization/metric_optimization/energy_functor.h" diff --git a/src/optimization/tests/test_refinement.cpp b/src/optimization/tests/test_refinement.cpp index 7dc34f8..94023a1 100644 --- a/src/optimization/tests/test_refinement.cpp +++ b/src/optimization/tests/test_refinement.cpp @@ -8,7 +8,6 @@ #include -#include "metric/common.h" #include "parametrization/refinement.h" #include "parametrization/triangulation.h" diff --git a/src/optimization/tests/test_regression.cpp b/src/optimization/tests/test_regression.cpp index ae1cf3c..30f3bd8 100644 --- a/src/optimization/tests/test_regression.cpp +++ b/src/optimization/tests/test_regression.cpp @@ -10,7 +10,6 @@ #include #include -#include "metric/common.h" #include "optimization/interface.h" #include "util/io.h" #include "util/vector.h" diff --git a/src/optimization/tests/test_shear.cpp b/src/optimization/tests/test_shear.cpp index a27abd2..76160ff 100644 --- a/src/optimization/tests/test_shear.cpp +++ b/src/optimization/tests/test_shear.cpp @@ -11,7 +11,7 @@ #include "metric/shear.h" #include "optimization/util/shapes.h" #include "util/embedding.h" -#include "metric/common.h" +#include "util/vector.h" #include "metric/cone_metric.h" #include "optimization/interface.h" #include "metric/projection.h" diff --git a/src/optimization/util/visualization.cc b/src/optimization/util/visualization.cc deleted file mode 100644 index fb52df3..0000000 --- a/src/optimization/util/visualization.cc +++ /dev/null @@ -1,88 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "visualization.h" - -namespace Penner { -namespace Optimization { - - -Viewer generate_mesh_viewer(const Eigen::MatrixXd &v, - const Eigen::MatrixXi &f, - bool show_lines) -{ - // Initialize viewer with mesh - Viewer viewer; - viewer.data().set_mesh(v, f); - - // Optionally show lines - viewer.data().show_lines = show_lines; - - return viewer; -} - -void view_mesh(Viewer viewer) -{ - // Launch widget - viewer.launch(); -} - - -void add_normals_to_mesh(Viewer &viewer, - const Eigen::MatrixXd &n) -{ - viewer.data().set_normals(n); -} - -void add_shading_to_mesh(Viewer &viewer, - const Eigen::MatrixXd &color, - const Eigen::VectorXd &AO, - double lighting_factor) -{ - // Generate shading color map - Eigen::MatrixXd C = color; - for (unsigned i = 0; i < AO.rows(); ++i) - { - C.row(i) *= AO(i); - } - - // Set viewer colors - viewer.data().set_colors(C); - - // Set lighting factor - viewer.core().lighting_factor = lighting_factor; -} - -void save_mesh_screen_capture(Viewer &viewer, - std::string image_path, - int width, - int height) -{ -/* - // Initialize windowless viewer - glfwInit(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - viewer.launch_init(false, false, "", width, height); - viewer.launch_rendering(false); - - // Allocate temporary buffers for 1280x800 image - Eigen::Matrix R(width, height); - Eigen::Matrix G(width, height); - Eigen::Matrix B(width, height); - Eigen::Matrix A(width, height); - - // Draw the scene in the buffers - viewer.core().draw_buffer(viewer.data(), true, R, G, B, A); - - // Save it to specified output path - igl::png::writePNG(R, G, B, A, image_path); -*/ -} - -} // namespace Optimization -} // namespace Penner \ No newline at end of file diff --git a/src/parametrization/refinement.cpp b/src/parametrization/refinement.cpp index 29c92c2..39b6d91 100644 --- a/src/parametrization/refinement.cpp +++ b/src/parametrization/refinement.cpp @@ -1183,8 +1183,8 @@ bool RefinementMesh::triangulate_face( } // FIXME - if (vertex_indices.size() > 3) { - spdlog::trace("Triangulating face {} of size {}", face_index, vertex_indices.size()); + if (vertices.size() > 3) { + spdlog::trace("Triangulating face {} with {} vertices", face_index, vertices.size()); spdlog::trace("Face vertices are {}", formatted_vector(vertex_indices)); } @@ -1210,6 +1210,7 @@ bool RefinementMesh::triangulate_face( // Return false if the face is not self overlapping if (!is_self_overlapping) { spdlog::warn("Face {} with {} vertices is not self overlapping", face_index, vertex_indices.size()); + assert(false); return false; } diff --git a/src/util/linear_algebra.cpp b/src/util/linear_algebra.cpp index 8a2219a..6d6a6e0 100644 --- a/src/util/linear_algebra.cpp +++ b/src/util/linear_algebra.cpp @@ -16,6 +16,33 @@ namespace Penner { +Scalar sup_norm(const VectorX& v) +{ + Scalar norm_value = 0.0; + for (Eigen::Index i = 0; i < v.size(); ++i) { + norm_value = max(norm_value, abs(v[i])); + } + + return norm_value; +} + +Scalar matrix_sup_norm(const MatrixX& matrix) +{ + // Check for trivial matrices + if (matrix.size() == 0) return 0; + + // Iterate to determine maximum abs value + Scalar max_value = 0.0; + for (Eigen::Index k = 0; k < matrix.outerSize(); ++k) { + for (MatrixX::InnerIterator it(matrix, k); it; ++it) { + max_value = std::max(max_value, abs(it.value())); + } + } + + return max_value; +} + + VectorX kronecker_product(const VectorX& vec_1, const VectorX& vec_2) { if (vec_1.size() != vec_2.size()) { diff --git a/src/feature/util/union_find.cpp b/src/util/union_find.cpp similarity index 100% rename from src/feature/util/union_find.cpp rename to src/util/union_find.cpp diff --git a/src/util/vector.cpp b/src/util/vector.cpp index eea17da..7bf325c 100644 --- a/src/util/vector.cpp +++ b/src/util/vector.cpp @@ -30,6 +30,17 @@ bool vector_contains_nan(const VectorX& v) return false; } +bool matrix_contains_nan(const Eigen::MatrixXd& mat) +{ + for (Eigen::Index i = 0; i < mat.rows(); ++i) { + for (Eigen::Index j = 0; j < mat.cols(); ++j) { + if (std::isnan(mat(i, j))) return true; + } + } + + return false; +} + void convert_dense_vector_to_sparse(const VectorX& vector_dense, MatrixX& vector_sparse) { // Copy the vector to a triplet list diff --git a/src/util/vf_mesh.cpp b/src/util/vf_mesh.cpp index a4cd7b7..119df38 100644 --- a/src/util/vf_mesh.cpp +++ b/src/util/vf_mesh.cpp @@ -297,6 +297,7 @@ std::tuple reindex_mesh( } } + return std::make_tuple(V_reindex, F_reindex); } Eigen::MatrixXi find_seams( From b33a1dedd3c15d0ce4686c613406d2ba9511cb3e Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Sun, 28 Jun 2026 23:01:24 -0400 Subject: [PATCH 02/17] Cleaned up includes --- include/feature/feature/core/common.h | 35 +- include/feature/feature/core/component_mesh.h | 4 - include/feature/feature/core/union_meshes.h | 1 - .../dirichlet/angle_constraint_relaxer.h | 1 - .../feature/dirichlet/cone_perturber.h | 3 - .../feature/feature/dirichlet/constraint.h | 1 - .../dirichlet/dirichlet_penner_cone_metric.h | 5 +- .../feature/feature/dirichlet/optimization.h | 1 - .../boundary_constraint_generator.h | 303 ----------------- .../boundary_constraint_validator.h | 130 ------- include/feature/feature/experimental/energy.h | 39 --- .../feature/feature/experimental/junctions.h | 95 ------ .../feature/experimental/layout_optimizer.h | 318 ------------------ .../feature/experimental/marked_refinement.h | 64 ---- include/feature/feature/experimental/misc.h | 59 ---- include/feature/feature/experimental/newton.h | 23 -- .../feature/feature/experimental/patches.h | 50 --- .../feature/experimental/polygon_cones.h | 37 -- .../feature/feature/experimental/transfer.h | 87 ----- include/feature/feature/feature/error.h | 1 - include/feature/feature/feature/features.h | 4 +- .../feature/feature/surgery/cut_mesh_layout.h | 1 - .../feature/surgery/cut_metric_generator.h | 2 - include/feature/feature/surgery/refinement.h | 3 - include/field/field/facet_field.h | 2 + include/field/field/frame_field.h | 2 + include/holonomy/holonomy/core/common.h | 11 +- include/holonomy/holonomy/core/dual_loop.h | 1 - .../holonomy/holonomy/core/homology_basis.h | 1 - include/holonomy/holonomy/core/viewer.h | 1 - include/holonomy/holonomy/holonomy/cones.h | 1 - .../holonomy/holonomy/holonomy/constraint.h | 1 - include/holonomy/holonomy/holonomy/holonomy.h | 1 - .../holonomy/marked_penner_cone_metric.h | 1 - include/holonomy/holonomy/holonomy/newton.h | 5 +- .../holonomy/holonomy/similarity/conformal.h | 1 - .../holonomy/holonomy/similarity/constraint.h | 1 - include/holonomy/holonomy/similarity/energy.h | 2 +- include/holonomy/holonomy/similarity/layout.h | 4 - .../similarity_penner_cone_metric.h | 5 +- include/metric/metric/cone_metric.h | 48 +++ include/metric/metric/reparametrization.h | 1 + include/metric/metric/viewer.h | 1 + .../optimization/optimization/util/shapes.h | 1 + .../parametrization/interpolation.h | 77 ++++- .../parametrization/parametrization/layout.h | 25 +- .../parametrization/translation.h | 1 + include/util/util/boundary.h | 4 +- include/util/util/common.h | 64 +--- include/util/util/embedding.h | 2 +- include/util/util/io.h | 3 +- include/util/util/union_find.h | 4 +- include/util/util/vf_mesh.h | 7 + src/feature/core/common.cpp | 39 --- src/feature/core/component_mesh.cpp | 10 +- src/feature/core/io.cpp | 3 - src/feature/core/quads.cpp | 1 - src/feature/core/vf_corners.cpp | 1 - src/feature/core/viewer.cpp | 30 +- .../dirichlet/angle_constraint_relaxer.cpp | 10 +- src/feature/dirichlet/cone_perturber.cpp | 1 + src/feature/dirichlet/constraint.cpp | 7 +- src/feature/dirichlet/optimization.cpp | 4 +- src/feature/feature/error.cpp | 14 +- src/feature/feature/features.cpp | 8 +- src/feature/interface.cpp | 2 +- src/feature/pybind.cpp | 7 - src/feature/surgery/cut_mesh_layout.cpp | 13 +- src/feature/surgery/cut_metric_generator.cpp | 19 +- src/feature/surgery/refinement.cpp | 4 +- src/feature/surgery/stitching.cpp | 13 +- src/field/cross_field.cpp | 1 - src/field/facet_field.cpp | 2 - src/field/field.cpp | 1 - src/field/frame_field.cpp | 4 - src/holonomy/core/boundary_basis.cpp | 4 +- src/holonomy/core/dual_lengths.cpp | 3 +- src/holonomy/core/dual_loop.cpp | 6 - src/holonomy/core/dual_segment.cpp | 2 - src/holonomy/core/homology_basis.cpp | 3 +- src/holonomy/core/viewer.cpp | 12 +- src/holonomy/holonomy/cones.cpp | 10 +- src/holonomy/holonomy/constraint.cpp | 5 +- src/holonomy/holonomy/holonomy.cpp | 4 - .../holonomy/marked_penner_cone_metric.cpp | 3 - src/holonomy/holonomy/newton.cpp | 16 +- src/holonomy/holonomy/rotation_form.cpp | 2 +- src/holonomy/interface.cpp | 2 +- src/holonomy/pybind.cpp | 1 - src/holonomy/similarity/conformal.cpp | 2 +- src/holonomy/similarity/layout.cpp | 1 + src/metric/area.cpp | 2 - src/metric/constraint.cpp | 1 - src/metric/projection.cpp | 4 - src/metric/reparametrization.cpp | 1 - src/metric/shear.cpp | 4 +- src/metric/viewer.cpp | 4 - src/optimization/interface.cpp | 2 - .../metric_optimization/energy_weights.cpp | 2 +- .../explicit_optimization.cpp | 3 - .../implicit_optimization.cpp | 2 - src/optimization/pybind.cpp | 2 - src/optimization/tests/test_area.cpp | 1 - src/optimization/tests/test_shear.cpp | 1 - src/parametrization/interpolation.cpp | 22 -- src/parametrization/layout.cpp | 80 +---- src/parametrization/parametrize.cpp | 23 +- src/parametrization/refinement.cpp | 9 +- src/parametrization/translation.cpp | 6 +- src/parametrization/triangulation.cpp | 15 +- src/util/boundary.cpp | 1 + src/util/io.cpp | 1 + src/util/map.cpp | 1 + src/util/vf_mesh.cpp | 11 + 114 files changed, 313 insertions(+), 1700 deletions(-) delete mode 100644 include/feature/feature/experimental/boundary_constraint_generator.h delete mode 100644 include/feature/feature/experimental/boundary_constraint_validator.h delete mode 100644 include/feature/feature/experimental/energy.h delete mode 100644 include/feature/feature/experimental/junctions.h delete mode 100644 include/feature/feature/experimental/layout_optimizer.h delete mode 100644 include/feature/feature/experimental/marked_refinement.h delete mode 100644 include/feature/feature/experimental/misc.h delete mode 100644 include/feature/feature/experimental/newton.h delete mode 100644 include/feature/feature/experimental/patches.h delete mode 100644 include/feature/feature/experimental/polygon_cones.h delete mode 100644 include/feature/feature/experimental/transfer.h diff --git a/include/feature/feature/core/common.h b/include/feature/feature/core/common.h index 899873a..d6b5b6f 100644 --- a/include/feature/feature/core/common.h +++ b/include/feature/feature/core/common.h @@ -8,11 +8,13 @@ #pragma once -#include "holonomy/interface.h" #include "holonomy/core/common.h" + +// get holonomy classes to put in feature namespace #include "holonomy/core/dual_loop.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/holonomy/newton.h" +#include "holonomy/interface.h" /** * @brief Assorted utility functions. @@ -27,17 +29,6 @@ using Holonomy::MarkedPennerConeMetric; using Holonomy::NewtonParameters; using Holonomy::MarkedMetricParameters; -/** - * @brief Compute the square of a scalar. - * - * @param x: value to square - * @return squared value - */ -inline Scalar square(Scalar x) -{ - return x * x; -} - /** * @brief Compute the one ring of halfedges emenating from a mesh vertex. * @@ -47,26 +38,6 @@ inline Scalar square(Scalar x) */ std::vector generate_vertex_one_ring(const Mesh& m, int vertex_index); -/** - * @brief Determine if a VF mesh is manifold - * @param F: mesh faces - * @return true iff the mesh is edge and vertex manifold - */ -bool is_manifold(const Eigen::MatrixXi& F); - -/** - * @brief Reindex VF mesh. - * - * @param V: mesh vertices - * @param F: mesh faces - * @param vtx_reindex: map from old to new vertex indices - * @return reindexed mesh vertices - * @return reindexed mesh faces - */ -std::tuple reindex_mesh( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const std::vector& vtx_reindex); /** * @brief Reindex list of edge endpoints under vertex reindexing. diff --git a/include/feature/feature/core/component_mesh.h b/include/feature/feature/core/component_mesh.h index 348ea2d..400fd90 100644 --- a/include/feature/feature/core/component_mesh.h +++ b/include/feature/feature/core/component_mesh.h @@ -11,10 +11,6 @@ #include "feature/core/common.h" -#include "feature/core/union_meshes.h" -#include "util/union_find.h" -#include "holonomy/holonomy/marked_penner_cone_metric.h" - /** * @brief Methods and data structures for splitting a disconnected mesh into separate components, * as well as corresponding reindexing maps. diff --git a/include/feature/feature/core/union_meshes.h b/include/feature/feature/core/union_meshes.h index f2cb74b..ebcef5a 100644 --- a/include/feature/feature/core/union_meshes.h +++ b/include/feature/feature/core/union_meshes.h @@ -12,7 +12,6 @@ #include "feature/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "util/union_find.h" /** * @brief Methods to combine collections of meshes into a single mesh with multiple components. diff --git a/include/feature/feature/dirichlet/angle_constraint_relaxer.h b/include/feature/feature/dirichlet/angle_constraint_relaxer.h index 5d081fc..723c0c0 100644 --- a/include/feature/feature/dirichlet/angle_constraint_relaxer.h +++ b/include/feature/feature/dirichlet/angle_constraint_relaxer.h @@ -10,7 +10,6 @@ #pragma once #include "feature/core/common.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" /** * @brief Method to compute a relaxed angle constraint system that maintains total vertex angles diff --git a/include/feature/feature/dirichlet/cone_perturber.h b/include/feature/feature/dirichlet/cone_perturber.h index 6e554b5..d51e8bf 100644 --- a/include/feature/feature/dirichlet/cone_perturber.h +++ b/include/feature/feature/dirichlet/cone_perturber.h @@ -8,10 +8,7 @@ #pragma once -#include "feature/core/common.h" - #include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "feature/feature/gluing.h" #include /** diff --git a/include/feature/feature/dirichlet/constraint.h b/include/feature/feature/dirichlet/constraint.h index cd0f465..735dc9d 100644 --- a/include/feature/feature/dirichlet/constraint.h +++ b/include/feature/feature/dirichlet/constraint.h @@ -9,7 +9,6 @@ #pragma once -#include "feature/core/common.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" /** diff --git a/include/feature/feature/dirichlet/dirichlet_penner_cone_metric.h b/include/feature/feature/dirichlet/dirichlet_penner_cone_metric.h index 6c0c789..b3e0fde 100644 --- a/include/feature/feature/dirichlet/dirichlet_penner_cone_metric.h +++ b/include/feature/feature/dirichlet/dirichlet_penner_cone_metric.h @@ -8,12 +8,9 @@ #pragma once -#include "feature/core/boundary_path.h" #include "feature/core/common.h" -#include "holonomy/core/dual_loop.h" -#include "holonomy/core/homology_basis.h" +#include "feature/core/boundary_path.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "holonomy/holonomy/constraint.h" /** * @brief Extension of the differentiable cone metric using Penner coordinates diff --git a/include/feature/feature/dirichlet/optimization.h b/include/feature/feature/dirichlet/optimization.h index c0715d3..24a7fee 100644 --- a/include/feature/feature/dirichlet/optimization.h +++ b/include/feature/feature/dirichlet/optimization.h @@ -8,7 +8,6 @@ #pragma once -#include "feature/core/common.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" #include "holonomy/holonomy/newton.h" diff --git a/include/feature/feature/experimental/boundary_constraint_generator.h b/include/feature/feature/experimental/boundary_constraint_generator.h deleted file mode 100644 index 6a15cfe..0000000 --- a/include/feature/feature/experimental/boundary_constraint_generator.h +++ /dev/null @@ -1,303 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "feature/core/common.h" -#include "holonomy/core/dual_loop.h" - -namespace Penner { -namespace Feature { - -// Idea: Want to unify setup for building constraints for meshes where we care about each boundary -// edge length (e.g., feature alignment) and where we only care about the sum between cones (e.g., -// polygon constraints for disks). Also want to generally keep track of boundary edges for checking -// validity or for local modification. To do this, we build a meta mesh of the features and -// junctions of the components, and we allow additional boundary vertices to be marked as junctions -// (in the limit, all boundary vertices). -// -// We can then support features of the following type: -// 1) Consistency of opposite boundary features -// 2) Fixed length (either of boundary or both opposite boundary features) -// -// This in turn allows for the following setups: -// a) Polygon boundary constraints are fixed length coarse boundary features with uniform length. -// b) Disk boundary constraints are fixed length fine boundary features with uniform length. -// c) Isometric boundary constraints are fixed length fine boundary features with initial length. -// d) Feature alignment constraints are consistent length fine boundary features. - -class BoundaryConstraintGenerator -{ -protected: - // declare iterator class - class SegmentIterator; - -public: - /** - * @brief Construct a new Boundary Constraint Generator object, marking all boundary components - * as boundary features with no opposite features or junctions and trivial target lengths. - * - * @param m: underyling mesh - */ - BoundaryConstraintGenerator(Mesh m); - - // feature attributes - int count_features() const { return m_next_feature.size(); } - int count_segments() const { return m_next_segment.size(); } - int count_halfedges() const { return m_mesh.n_halfedges(); } - int next_feature(int feature_index) const - { - assert(is_valid_feature_index(feature_index)); - return m_next_feature[feature_index]; - } - int prev_feature(int feature_index) const - { - assert(is_valid_feature_index(feature_index)); - return m_prev_feature[feature_index]; - } - int to(int feature_index) const - { - assert(is_valid_feature_index(feature_index)); - return m_to[feature_index]; - } - - // junction attributes - int count_junctions() const { return m_out.size(); } - int out(int junction_index) const { return m_out[junction_index]; } - - /** - * @brief Enumerate the cones on a given feature. - * - * @param feature_index: index of the feature - * @return list of cones (in order) around the feature - */ - std::vector enumerate_feature_cones(int feature_index) const; - - /** - * @brief Enumerate the cones in the interior of the mesh - * - * @return list of cones (in arbitrary order) in the interior of the mesh - */ - std::vector enumerate_interior_cones() const; - - /** - * @brief Mark all cone vertices as junctions. - * - */ - void mark_cones_as_junctions(); - - /** - * @brief Split boundary paths segments and store boundary path pairings with holonomy. - * - * @param basis_loops: symmetric loops connecting boundary components - * @param kappa_hat: loop holonomies - */ - void add_boundary_path_segments( - const std::vector>& basis_loops, - const std::vector& kappa_hat - ); - - /** - * @brief Pair all boundary segments identified by a vertex map for a cut mesh - * - * @param vtx_reindex: reindexing from mesh to vertex map indices - * @param V_map: map from cut vertices to original vertices - */ - void pair_boundary_edges(const std::vector& vtx_reindex, const Eigen::VectorXi& V_map); - - /** - * @brief Distribute a target feature length across segments - * - * @param feature_index: index of the feature to distribute - * @param target_feature_length: total target feature length - */ - void distribute_feature_length(int feature_index, Scalar target_feature_length); - - /** - * @brief Set all feature lengths to a uniform value - * - * @param target_feature_length: target feature length - */ - void set_uniform_feature_lengths(Scalar target_feature_length); - - /// Build a segment iterator for a feature. - /// - /// @param feature_index: feature to iterate over - /// @return iterator for the given feature - SegmentIterator get_feature_iterator(int feature_index) const - { - return SegmentIterator(*this, feature_start(feature_index)); - } - - /** - * @brief Compute the total length of the given boundary feature - * - * @param feature_index: index of the given feature - * @return length of the boundary - */ - Scalar compute_feature_length(int feature_index) const; - - /** - * @brief Get the underlying mesh object - * - * @return reference to the mesh - */ - const Mesh& get_mesh() const { return m_mesh; } - - /** - * @brief Get the target length for a given segment - * - * @param segment_index: index of the segment - * @return target length of the segment - */ - Scalar get_target_length(int segment_index) const { return m_target_length[segment_index]; } - - /** - * @brief Set the target length for a given segment - * - * @param segment_index: index of the segment - * @param target_length: new target length of the segment - */ - void set_target_length(int segment_index, Scalar target_length) - { - m_target_length[segment_index] = target_length; - } - - /** - * @brief Generate a list of halfedges in the feature. - * - * @param feature_index: index of the feature - * @return halfedges in the feature - */ - std::vector compute_feature_halfedges(int feature_index) const; - - /** - * @brief Construct a matrix system mapping mesh halfedges to boundary features - * - * @return matrix mapping halfedges to features - * @return vector of constraints for features - */ - std::tuple build_boundary_constraint_system() const; - - /** - * @brief View the current boundary constraint system - * - * @param V: mesh vertices - * @param vtx_reindex: map from mesh vertices to VF vertices - */ - void view(const Eigen::MatrixXd& V, const std::vector& vtx_reindex); - -protected: - bool is_valid_feature_index(int feature_index) const - { - return ((feature_index >= 0) && (feature_index < count_features())); - } - - bool is_valid_halfedge_index(int halfedge_index) const - { - return ((halfedge_index >= 0) && (halfedge_index < count_halfedges())); - } - - // feature segment attributes (hidden from interface) - // a segment is a halfedge in a feature edge (which is necessarily a boundary edge) - int feature_start(int feature_index) const { return m_feature_start[feature_index]; } - int feature_end(int feature_index) const { return m_feature_end[feature_index]; } - int feature(int segment_index) const { return m_feature[segment_index]; } - int halfedge(int segment_index) const { return m_halfedge[segment_index]; } - int next_segment(int segment_index) const { return m_next_segment[segment_index]; } - int prev_segment(int segment_index) const { return m_prev_segment[segment_index]; } - int pair(int segment_index) const { return m_pair[segment_index]; } - int segment(int halfedge_index) const { return m_segment[halfedge_index]; } - - // useful predicates - bool is_feature_end(int segment_index) const { return (feature_end(feature(segment_index)) == segment_index); } - - // feature creation atomic operations - void add_junction_after_segment(int segment_index); - int split_segment(int segment_index); - - class SegmentIterator - { - public: - SegmentIterator(const BoundaryConstraintGenerator& parent, int segment_index) - : m_parent(parent) - { - m_current_segment_index = segment_index; - } - - SegmentIterator& operator++() - { - if (!is_end()) { - m_current_segment_index = m_parent.next_segment(m_current_segment_index); - } - - return *this; - } - - SegmentIterator operator++(int) - { - SegmentIterator temp = *this; - ++*this; - return temp; - } - - SegmentIterator& operator--() - { - if (!is_end()) { - m_current_segment_index = m_parent.prev_segment(m_current_segment_index); - } - - return *this; - } - - SegmentIterator operator--(int) - { - SegmentIterator temp = *this; - --*this; - return temp; - } - - int operator*() { return m_parent.halfedge(m_current_segment_index); } - - bool is_end() { return (m_current_segment_index < 0); } - - private: - const BoundaryConstraintGenerator& m_parent; - int m_current_segment_index; - }; - -private: - // mesh copy - Mesh m_mesh; - - // feature attributes - std::vector m_feature_start; - std::vector m_feature_end; - std::vector m_next_feature; - std::vector m_prev_feature; - std::vector m_to; - std::vector m_target_length; - - // junction attributes - std::vector m_out; - - // feature segment attributes - std::vector m_feature; - std::vector m_halfedge; - std::vector m_next_segment; - std::vector m_prev_segment; - std::vector m_pair; // TODO, make feature attribute - - // halfedge attributes - std::vector m_segment; -}; - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/boundary_constraint_validator.h b/include/feature/feature/experimental/boundary_constraint_validator.h deleted file mode 100644 index 1c6d3d5..0000000 --- a/include/feature/feature/experimental/boundary_constraint_validator.h +++ /dev/null @@ -1,130 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "feature/core/common.h" -#include "feature/experimental/boundary_constraint_generator.h" -#include "holonomy/core/dual_loop.h" - -namespace Penner { -namespace Feature { - -// The validity check approach for meshes with multiple components is proving to be very -// difficult and likely a dead end for practical research. -class BoundaryConstraintValidator : public BoundaryConstraintGenerator -{ -public: - /** - * @brief Construct a new Boundary Constraint Generator object, marking all boundary components - * as boundary features with no opposite features or junctions and trivial target lengths. - * - * @param m: underyling mesh - */ - BoundaryConstraintValidator(Mesh m); - - // path attributes - int count_paths() const { return m_path_holonomy.size(); } - - /** - * @brief Split boundary paths segments and store boundary path pairings with holonomy. - * - * @param basis_loops: symmetric loops connecting boundary components - * @param kappa_hat: loop holonomies - */ - void add_boundary_path_segments( - const std::vector>& basis_loops, - const std::vector& kappa_hat - ); - - /** - * @brief Construct a validity constraint system (Ax = b) for the features. - * - * A solution to the boundary constraint system exists if and only if there are - * positive lengths satisfying length equality constraints and also a closure condition, - * which states that the layout of each polygonal boundary component must be closed - * (possibly after adding a virtual vertex to compensate for missing geodesic curvature). - * - * @return constraint validity matrix (A) - * @return constraint validity vector (b) - */ - std::tuple build_boundary_validity_system() const; - - // TODO: component attributes - std::tuple build_component_validity_system() const; - void view_component_layout(const VectorX& ell) const; - // int component(int feature_index) const { return m_component[feature_index]; } - // std::vector features(int component_index) const { return m_features[component_index]; } - -protected: - - // feature segment attributes (hidden from interface) - int path(int segment_index) const { return m_path[segment_index]; } - int reverse_path(int segment_index) const { return m_reverse_path[segment_index]; } - - // path attributes (hidden from interface) - // a path is an abstract nonseparating path between two boundary components with some target holonomy - int path_start_segment(int path_index) const { return m_path_start_segment[path_index]; } - int path_end_segment(int path_index) const { return m_path_end_segment[path_index]; } - int path_reverse_start_segment(int path_index) const { return m_path_reverse_start_segment[path_index]; } - int path_reverse_end_segment(int path_index) const { return m_path_reverse_end_segment[path_index]; } - Scalar path_holonomy(int path_index) const { return m_path_holonomy[path_index]; } - - // atomic operation with path management - int split_segment(int segment_index); - - // useful predicates - bool is_path_start(int segment_index) const { return (path(segment_index) >= 0); } - bool is_reverse_path_start(int segment_index) const { return (reverse_path(segment_index) >= 0); } - - // path helper functions - // TODO make class - void add_feature( - int feature_index, - Vector2& d, - Scalar& total_curvature, - std::vector>& tripletList) const; - void add_free_connection( - const std::array& virtual_segments, - bool reverse, - Vector2& d, - std::vector& segment_directions, - std::vector& segment_indices) const; - void add_fixed_connection( - const std::vector& virtual_segments, - bool is_counterclockwise, - bool reverse, - Vector2& d, - std::vector& segment_directions, - std::vector& segment_indices) const; - - std::tuple, std::vector> build_component_layout() const; - -private: - // segment attributes - std::vector m_path; // TODO finish - std::vector m_reverse_path; // TODO finish - - // path attributes - std::vector m_path_start_segment; - std::vector m_path_end_segment; - std::vector m_path_reverse_start_segment; - std::vector m_path_reverse_end_segment; - std::vector m_path_holonomy; - - - // component attributes - // TODO - // std::vector m_component; - // std::vector> m_features; -}; - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/energy.h b/include/feature/feature/experimental/energy.h deleted file mode 100644 index 5bf4863..0000000 --- a/include/feature/feature/experimental/energy.h +++ /dev/null @@ -1,39 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "feature/core/common.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "optimization/metric_optimization/energy_functor.h" - -namespace Penner { -namespace Feature { - - -class ConeEnergy : public Optimization::EnergyFunctor -{ -public: - /// Sum of square cone error energy. - /// - /// @param[in] cone_metric: mesh topology - ConeEnergy(const PennerConeMetric& cone_metric); - -private: - PennerConeMetric m_cone_metric; - - virtual Scalar energy(const VectorX& metric_coords) const override; - virtual VectorX gradient(const VectorX& metric_coords) const override; - virtual MatrixX hessian(const VectorX& metric_coords) const override; - virtual MatrixX hessian_inverse(const VectorX& metric_coords) const override; -}; - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/junctions.h b/include/feature/feature/experimental/junctions.h deleted file mode 100644 index 12d1a3c..0000000 --- a/include/feature/feature/experimental/junctions.h +++ /dev/null @@ -1,95 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "feature/core/common.h" -#include "feature/feature/features.h" - -namespace Penner { -namespace Feature { - -class JunctionFeatureFinder : public FeatureFinder -{ -public: - JunctionFeatureFinder(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F); - - /** - * @brief Remove feature edges adjacent to junctions of 3 or more features. - * - */ - void prune_junctions(); - - /** - * @brief Remove feature edges adjacent to a given vertex - * - */ - void prune_junction(int vertex_index); - - /** - * @brief Remove feature edges adjacent to the junction closest to the given vertex - * - * @param vertex_index: vertex in the mesh (could be junction or not) - */ - void prune_closest_junction(int vertex_index); - - /** - * @brief Remove an edge from a closed loop feature - * - */ - void prune_closed_loops(); - - /** - * @brief View the junctions of the feature graph. - * - * @param show: (optional) if true, open polyscope viewer - */ - void view_junctions(bool show = true); - - /** - * @brief Get junctions of the feature graph - * - * @return list of junction vertices - */ - std::vector compute_junctions() const; -private: - std::tuple, std::vector> compute_feature_forest( - const std::vector& flood_seeds) const; - -}; - -/** - * @brief Remove junctions in a feature finder according to a thresholded component - * function heuristc, which relaxes components with a function value above some threshold. - * - * Note that we generally assume the cut mesh edges are the feature edges, but the method - * is robust to different cut structures as long as the original uncut mesh is the same. - * - * @param feature_finder: features to modify - * @param m: cut mesh - * @param vtx_reindex: cut mesh to cut VF reindexing - * @param V_map: map from cut VF vertices to original VF vertices - * @param vertex_component: map from cut mesh vertices to component id - * @param component_function: function on cut mesh components - * @param relative_threshold: (optional) threshold for relaxing a component - * (relative to maximum of the component function) - * @param do_remove_all_junctions: (optional) if true, remove all junctions in a component - */ -void remove_threshold_junctions( - JunctionFeatureFinder& feature_finder, - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const Eigen::VectorXi& vertex_component, - const VectorX& component_function, - Scalar relative_threshold = 0.1, - bool do_remove_all_junctions = true); - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/layout_optimizer.h b/include/feature/feature/experimental/layout_optimizer.h deleted file mode 100644 index 6f4f28d..0000000 --- a/include/feature/feature/experimental/layout_optimizer.h +++ /dev/null @@ -1,318 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "feature/core/common.h" - -namespace Penner { -namespace Feature { - -template -class LayoutOptimizer { -private: - int num_halfedges; - int num_vars; - Eigen::Matrix x; - Eigen::SparseMatrix D; - Eigen::Matrix d; - Eigen::Matrix l0; - - Eigen::SparseMatrix H; - Eigen::Matrix g; - - Eigen::SparseMatrix Hr; - Eigen::Matrix gr; - - typedef Eigen::Triplet ScalarTrip; - - int index(int hij, int coord) const { return (2 * hij) + coord; } - int f2h(int fijk, int i) const { return (3 * fijk) + i; } - - OverlayScalar pow(OverlayScalar x, int n) const - { - if (n < 1) return 1.; - return x * pow(x, n - 1); - } - - void build_difference_matrix(const Mesh& m) - { - typedef Eigen::Triplet ScalarTrip; - std::vector system_trips; - system_trips.reserve(4 * num_halfedges); - - // set rows of matrix as halfedge difference vectors - for (int hij = 0; hij < num_halfedges; ++hij) { - int hki = m.n[m.n[hij]]; - for (int coord = 0; coord < 2; ++coord) - { - system_trips.push_back(ScalarTrip(index(hij, coord), index(hij, coord), 1.)); - system_trips.push_back(ScalarTrip(index(hij, coord), index(hki, coord), -1.)); - } - } - - // build matrix - D.setFromTriplets(system_trips.begin(), system_trips.end()); - } - - void build_difference_matrix(const Eigen::MatrixXi& F) - { - spdlog::info("building basis matrix"); - typedef Eigen::Triplet ScalarTrip; - std::vector system_trips; - system_trips.reserve(4 * num_halfedges); - - // set rows of matrix as halfedge difference vectors - int num_faces = F.rows(); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - for (int i = 0; i < 3; ++i) - { - int j = (i + 1) % 3; - int k = (j + 1) % 3; - for (int coord = 0; coord < 2; ++coord) - { - int hij = f2h(fijk, k); - system_trips.push_back(ScalarTrip(index(hij, coord), index(F(fijk, j), coord), 1.)); - system_trips.push_back(ScalarTrip(index(hij, coord), index(F(fijk, i), coord), -1.)); - } - } - } - - // build matrix - D.setFromTriplets(system_trips.begin(), system_trips.end()); - } - - void initialize( - const Mesh& m, - const std::vector& u, - const std::vector& v) - { - num_halfedges = m.n_halfedges(); - num_vars = 2 * num_halfedges; - g.resize(2 * num_halfedges); - x.resize(2 * num_halfedges); - l0.resize(num_halfedges); - D.resize(2 * num_halfedges, 2 * num_halfedges); - H.resize(2 * num_halfedges, 2 * num_halfedges); - - for (int hij = 0; hij < num_halfedges; ++hij) { - x[index(hij, 0)] = u[hij]; - x[index(hij, 1)] = v[hij]; - l0[hij] = pow(m.l[hij], 2); - } - - build_difference_matrix(m); - } - - void initialize( - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& FT, - const Eigen::MatrixXi& F, - std::vector& u, - std::vector& v) - { - int num_faces = F.rows(); - num_halfedges = 3 * num_faces; - num_vars = u.size(); - g.resize(2 * num_halfedges); - D.resize(2 * num_halfedges, 2 * num_vars); - H.resize(2 * num_halfedges, 2 * num_halfedges); - - spdlog::info("initializing variables"); - x.resize(2 * num_vars); - for (int i = 0; i < num_vars; ++i) - { - x[index(i, 0)] = u[i]; - x[index(i, 1)] = v[i]; - } - - spdlog::info("computing target lengths"); - l0.resize(num_halfedges); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - for (int i = 0; i < 3; ++i) - { - int j = (i + 1) % 3; - int k = (j + 1) % 3; - Eigen::VectorXd dij = uv.row(FT(fijk, j)) - uv.row(FT(fijk, i)); - l0[f2h(fijk, k)] = dij.dot(dij); - } - } - - build_difference_matrix(F); - } - - OverlayScalar compute_length_difference(int hij) const - { - int uij = index(hij, 0); - int vij = index(hij, 1); - OverlayScalar lij = pow(x[uij], 2) + pow(x[vij], 2); - return lij - l0(hij); - } - - void build_quadratic_approximation() - { - // get halfedge direction vectors - d = D * x; - - std::vector system_trips; - system_trips.reserve(4 * num_halfedges); - - // set rows of matrix as halfedge difference vectors - for (int hij = 0; hij < num_halfedges; ++hij) { - int uij = index(hij, 0); - int vij = index(hij, 1); - OverlayScalar lij = pow(d[uij], 2) + pow(d[vij], 2); - OverlayScalar dij = lij - l0[hij]; - OverlayScalar dpij = pow(dij, p - 2); - OverlayScalar a; - - // compute gradient - g[uij] = d[uij] * dij * dpij; - g[vij] = d[vij] * dij * dpij; - - // compute u diagonal - a = (p * pow(d[uij], 2) + pow(d[vij], 2)) * dpij; - system_trips.push_back(ScalarTrip(uij, uij, a)); - - // compute v diagonal - a = (pow(d[uij], 2) + p * pow(d[vij], 2)) * dpij; - system_trips.push_back(ScalarTrip(vij, vij, a)); - - // compute cross terms - a = (p - 1) * d[uij] * d[vij] * dpij; - system_trips.push_back(ScalarTrip(uij, vij, a)); - system_trips.push_back(ScalarTrip(vij, uij, a)); - } - - // build matrix - H.setFromTriplets(system_trips.begin(), system_trips.end()); - } - - OverlayScalar compute_energy() - { - spdlog::info("computing energy"); - - // get halfedge direction vectors - d = D * x; - - OverlayScalar energy(0.); - for (int hij = 0; hij < num_halfedges; ++hij) { - int uij = index(hij, 0); - int vij = index(hij, 1); - OverlayScalar lij = pow(d[uij], 2) + pow(d[vij], 2); - OverlayScalar dij = lij - l0[hij]; - energy += pow(dij, p); - } - - return (1. / (2. * p)) * energy; - } - - Eigen::Matrix compute_descent_direction() - { - // build difference coordinate quadratic system - build_quadratic_approximation(); - - // build reduced variable quadratic system - Hr = D.transpose() * H * D; - gr = D.transpose() * g; - bool use_gradient = false; - if (use_gradient) return -gr; - - // initialize solver - Eigen::SimplicialLDLT> solver; - solver.compute(Hr); - - return -solver.solve(gr); - } - - void do_line_step() - { - auto y = compute_descent_direction(); - OverlayScalar beta = 1.; - OverlayScalar E0 = compute_energy(); - spdlog::info("initial error {}", E0); - - auto x0 = x; - x = x0 + beta * y; - OverlayScalar E = compute_energy(); - spdlog::info("error with line step {} is {}",beta, E); - while (E > E0) - { - beta = beta / 2.; - x = x0 + beta * y; - E = compute_energy(); - spdlog::info("error with line step {} is {}",beta, E); - } - } - - std::vector get_coordinates(int coord) const - { - std::vector coords(num_vars); - for (int hij = 0; hij < num_vars; ++hij) { - coords[hij] = x[index(hij, coord)]; - } - - return coords; - } - -public: - int p; - LayoutOptimizer() - : p(2) - {} - - void run( - const Mesh& m, - std::vector& u, - std::vector& v) - { - initialize(m, u, v); - OverlayScalar energy = compute_energy(); - spdlog::info("initial error is {}", energy); - - for (int i = 0; i < 1000; ++i) - { - do_line_step(); - } - - energy = compute_energy(); - spdlog::info("final error is {}", energy); - - u = get_coordinates(0); - v = get_coordinates(1); - } - - void run( - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& FT, - const Eigen::MatrixXi& F, - std::vector& u, - std::vector& v) - { - initialize(uv, FT, F, u, v); - - OverlayScalar energy = compute_energy(); - spdlog::info("initial error is {}", energy); - - for (int i = 0; i < 5; ++i) - { - do_line_step(); - } - - energy = compute_energy(); - spdlog::info("final error is {}", energy); - - u = get_coordinates(0); - v = get_coordinates(1); - } -}; - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/marked_refinement.h b/include/feature/feature/experimental/marked_refinement.h deleted file mode 100644 index 553b18b..0000000 --- a/include/feature/feature/experimental/marked_refinement.h +++ /dev/null @@ -1,64 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "feature/core/common.h" -#include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "feature/core/boundary_path.h" -#include "feature/feature/features.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "feature/surgery/refinement.h" - -namespace Penner { -namespace Feature { - -class MarkedRefinementMesh : public IntrinsicRefinementMesh -{ -public: - // TODO - MarkedRefinementMesh(const Mesh& m) - : IntrinsicRefinementMesh(m) - {} - - /** - * @brief Generate a marked metric with the given mesh and boundary loops. - * - * @param kappa_hat: holonomy constraints for the refined metric - * @return refined metric - */ - MarkedPennerConeMetric generate_marked_metric(const std::vector& kappa_hat) const; - - /** - * @brief Generate a dirichlet metric with the given mesh and boundary loops - * - * @param kappa_hat: holonomy constraints for the refined metric - * @param start_vertices: vertices to start th - * @param boundary_constraint_system - * @param ell - * @return refined metric with boundary constraints - */ - MarkedPennerConeMetric generate_dirichlet_metric( - const std::vector& kappa_hat, - const std::vector& start_vertices, - const MatrixX& boundary_constraint_system, - const VectorX ell) const; - -private: - // TODO: support full intrinsic refinement for, e.g., intrinsic Delaunay refinement - std::vector> m_dual_loops; - std::vector>& get_dual_loops() { return m_dual_loops; } - const std::vector>& get_dual_loops() const { return m_dual_loops; } - - virtual int refine_single_face(int face_index) override; - virtual int refine_single_halfedge(int halfedge_index) override; - -}; - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/misc.h b/include/feature/feature/experimental/misc.h deleted file mode 100644 index a55db35..0000000 --- a/include/feature/feature/experimental/misc.h +++ /dev/null @@ -1,59 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "util/common.h" - -struct ParametrizedMesh -{ - ParametrizedMesh( - const Eigen::MatrixXd& V_, - const Eigen::MatrixXi& F_, - const Eigen::MatrixXd& uv_, - const Eigen::MatrixXi& F_uv_) - : V(V_) - , F(F_) - , uv(uv_) - , F_uv(F_uv_) - {} - - std::tuple unpack() - { - return std::make_tuple(V, F, uv, F_uv); - } - - - Eigen::MatrixXd V; - Eigen::MatrixXi F; - Eigen::MatrixXd uv; - Eigen::MatrixXi F_uv; -}; - -struct ParametrizedOverlayMesh -{ - ParametrizedOverlayMesh( - const Eigen::MatrixXd& V_, - const Eigen::MatrixXi& F_, - const Eigen::MatrixXd& uv_, - const Eigen::MatrixXi& F_uv_, - const std::vector& Fn_to_F_, - const std::vector>& endpoints_) - : V(V_) - , F(F_) - , uv(uv_) - , F_uv(F_uv_) - , Fn_to_F(Fn_to_F_) - , endpoints(endpoints_) - {} - - Eigen::MatrixXd V; - Eigen::MatrixXi F; - Eigen::MatrixXd uv; - Eigen::MatrixXi F_uv; - std::vector Fn_to_F; - std::vector> endpoints; -}; \ No newline at end of file diff --git a/include/feature/feature/experimental/newton.h b/include/feature/feature/experimental/newton.h deleted file mode 100644 index ac4d18f..0000000 --- a/include/feature/feature/experimental/newton.h +++ /dev/null @@ -1,23 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "feature/core/common.h" -#include "holonomy/holonomy/newton.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" - -namespace Penner { -namespace Feature { - -using Holonomy::NewtonParameters; - -// TODO: deprecated; may reimplement later - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/patches.h b/include/feature/feature/experimental/patches.h deleted file mode 100644 index 846ef96..0000000 --- a/include/feature/feature/experimental/patches.h +++ /dev/null @@ -1,50 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "feature/core/common.h" - -namespace Penner { -namespace Feature { - -// TODO Refactor this and cone metric for more minimal inheritance - -/** - * @brief Class to generate primal edge cuts to convex patches for a mesh. - */ -class ConvexPatchDecomposition -{ -public: - -ConvexPatchDecomposition(const Mesh& m); - -private: - - Mesh m_mesh; - - std::vector m_boundary_edges; - std::vector m_rotation_index; - - std::vector> m_cuts; - std::vector m_is_cut; - - void initialize_boundary_edges(); - - int find_concave_vertex() const; - std::vector find_path(int vertex_index) const; - - std::tuple generate_mesh( - const Eigen::MatrixXd& V, - const std::vector& vtx_reindex) const; - void view_patch_decomposition() const; - -}; - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/polygon_cones.h b/include/feature/feature/experimental/polygon_cones.h deleted file mode 100644 index 046d059..0000000 --- a/include/feature/feature/experimental/polygon_cones.h +++ /dev/null @@ -1,37 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "feature/core/common.h" - - -namespace Penner { -namespace Feature { - - -/** - * @brief Generate a cone angle prescription for a topological disk mapping it to a regular polygon. - * - * If more vertices are requested than possible, all boundary vertices are used. By default, - * edges are equally spaced according to boundary index position; approximately equal edge - * lengths can be used, but the number of vertices is not guaranteed to agree with the input. - * - * @param m: underlying mesh (must be topological disk) - * @param vtx_reindex: reindexing from the mesh to the VF vertex indices - * @param num_vertices: number of vertices in the target polygon (or 0 for all) - * @return cone prescription for m with target boundary shape - */ -std::vector generate_polygon_cones( - const Mesh& m, - const std::vector& vtx_reindex, - int num_vertices=0, - bool use_length=false); - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/experimental/transfer.h b/include/feature/feature/experimental/transfer.h deleted file mode 100644 index 410a724..0000000 --- a/include/feature/feature/experimental/transfer.h +++ /dev/null @@ -1,87 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "feature/core/common.h" - -#include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "holonomy/interface.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "util/union_find.h" - -namespace Penner { -namespace Feature { - -/** - * @brief Transfer edge data stored per halfedge from a domain mesh to a target mesh, - * both obtained from a common base mesh by cutting edges. - * - * @param marked_metric_0: target mesh with metric - * @param vtx_reindex_0: VV to halfedge indexing for target mesh - * @param V_map_0: vertex identification for target mesh - * @param marked_metric: domain mesh with metric - * @param vtx_reindex: VV to halfedge indexing for domain mesh - * @param V_map: vertex identification for domain mesh - * @param halfedge_data: data stored on domain mesh - * @param is_signed: if true, treat halfedge data as having opposite signs for opposite halfedges - * @return halfedge data transfered to target mesh - */ -VectorX transfer_edge_data( - const Mesh& marked_metric_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const Mesh& marked_metric, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const VectorX& halfedge_data, - bool is_signed=false); - -/** - * @brief Transfer metric coordinates from a domain mesh to a target mesh. - * - * @param marked_metric_0: target mesh with metric - * @param vtx_reindex_0: VV to halfedge indexing for target mesh - * @param V_map_0: vertex identification for target mesh - * @param marked_metric: domain mesh with metric - * @param vtx_reindex: VV to halfedge indexing for domain mesh - * @param V_map: vertex identification for domain mesh - */ -void transfer_metric( - MarkedPennerConeMetric& marked_metric_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const MarkedPennerConeMetric& marked_metric, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map); - -/** - * @brief Transfer rotation form from a domain mesh to a target mesh. - * - * @param marked_metric_0: target mesh with metric - * @param vtx_reindex_0: VV to halfedge indexing for target mesh - * @param V_map_0: vertex identification for target mesh - * @param marked_metric: domain mesh with metric - * @param vtx_reindex: VV to halfedge indexing for domain mesh - * @param V_map: vertex identification for domain mesh - * @param rotation_form: rotation form on domain mesh - * @return rotation form transfered to target mesh - */ -VectorX transfer_rotation_form( - const Mesh& m_0, - const std::vector& vtx_reindex_0, - const Eigen::VectorXi& V_map_0, - const Mesh& m, - const std::vector& vtx_reindex, - const Eigen::VectorXi& V_map, - const VectorX& rotation_form); - - -} // namespace Feature -} // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/feature/error.h b/include/feature/feature/feature/error.h index 7c1876d..e36ea65 100644 --- a/include/feature/feature/feature/error.h +++ b/include/feature/feature/feature/error.h @@ -9,7 +9,6 @@ #pragma once -#include "feature/core/common.h" #include "feature/core/vf_corners.h" /** diff --git a/include/feature/feature/feature/features.h b/include/feature/feature/feature/features.h index 06ffc0a..e8b7d9f 100644 --- a/include/feature/feature/feature/features.h +++ b/include/feature/feature/feature/features.h @@ -9,11 +9,9 @@ #pragma once -#include "feature/core/common.h" - #include "util/union_find.h" #include "feature/core/vf_corners.h" -#include "holonomy/holonomy/marked_penner_cone_metric.h" +//#include "holonomy/holonomy/marked_penner_cone_metric.h" /** * @brief Method to find feature edges on a surface and hard feature constraint subsets. diff --git a/include/feature/feature/surgery/cut_mesh_layout.h b/include/feature/feature/surgery/cut_mesh_layout.h index 432e5c3..45df0dd 100644 --- a/include/feature/feature/surgery/cut_mesh_layout.h +++ b/include/feature/feature/surgery/cut_mesh_layout.h @@ -8,7 +8,6 @@ #pragma once -#include "feature/core/common.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" namespace Penner { diff --git a/include/feature/feature/surgery/cut_metric_generator.h b/include/feature/feature/surgery/cut_metric_generator.h index 5ab97d6..138388d 100644 --- a/include/feature/feature/surgery/cut_metric_generator.h +++ b/include/feature/feature/surgery/cut_metric_generator.h @@ -8,9 +8,7 @@ #pragma once -#include "feature/core/common.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" -#include "holonomy/interface.h" namespace Penner { namespace Feature { diff --git a/include/feature/feature/surgery/refinement.h b/include/feature/feature/surgery/refinement.h index 54e2b30..b6dc203 100644 --- a/include/feature/feature/surgery/refinement.h +++ b/include/feature/feature/surgery/refinement.h @@ -8,11 +8,8 @@ #pragma once -#include "feature/core/boundary_path.h" -#include "feature/core/common.h" #include "feature/dirichlet/dirichlet_penner_cone_metric.h" #include "feature/feature/features.h" -#include "holonomy/holonomy/marked_penner_cone_metric.h" /** * @brief Data structure with limited support to refine the faces and edges of a mesh for feature alignment. diff --git a/include/field/field/facet_field.h b/include/field/field/facet_field.h index f2aa422..d4fe849 100644 --- a/include/field/field/facet_field.h +++ b/include/field/field/facet_field.h @@ -10,6 +10,8 @@ #pragma once #include "util/common.h" +#include "util/linear_algebra.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" namespace Penner { namespace Field { diff --git a/include/field/field/frame_field.h b/include/field/field/frame_field.h index 6ac750f..41e2acd 100644 --- a/include/field/field/frame_field.h +++ b/include/field/field/frame_field.h @@ -10,6 +10,8 @@ #pragma once #include "field/intrinsic_field.h" +#include "util/linear_algebra.h" +#include "util/vector.h" /** * @brief TODO: Redistribute this code diff --git a/include/holonomy/holonomy/core/common.h b/include/holonomy/holonomy/core/common.h index f750262..3fa2959 100644 --- a/include/holonomy/holonomy/core/common.h +++ b/include/holonomy/holonomy/core/common.h @@ -8,17 +8,8 @@ #pragma once -#include -#include -#include - -#include -#include -#include - -#include "optimization/interface.h" +#include "util/common.h" #include "metric/cone_metric.h" -#include "util/vector.h" /** * @brief Assorted utility functions. diff --git a/include/holonomy/holonomy/core/dual_loop.h b/include/holonomy/holonomy/core/dual_loop.h index 59c98b9..83c12e3 100644 --- a/include/holonomy/holonomy/core/dual_loop.h +++ b/include/holonomy/holonomy/core/dual_loop.h @@ -8,7 +8,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/core/dual_segment.h" /** diff --git a/include/holonomy/holonomy/core/homology_basis.h b/include/holonomy/holonomy/core/homology_basis.h index 4bff11c..dbc07c0 100644 --- a/include/holonomy/holonomy/core/homology_basis.h +++ b/include/holonomy/holonomy/core/homology_basis.h @@ -9,7 +9,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/core/dual_loop.h" #include "util/spanning_tree.h" diff --git a/include/holonomy/holonomy/core/viewer.h b/include/holonomy/holonomy/core/viewer.h index 90e4493..0164994 100644 --- a/include/holonomy/holonomy/core/viewer.h +++ b/include/holonomy/holonomy/core/viewer.h @@ -9,7 +9,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "metric/viewer.h" diff --git a/include/holonomy/holonomy/holonomy/cones.h b/include/holonomy/holonomy/holonomy/cones.h index 1f650ff..44b89a5 100644 --- a/include/holonomy/holonomy/holonomy/cones.h +++ b/include/holonomy/holonomy/holonomy/cones.h @@ -8,7 +8,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" /** diff --git a/include/holonomy/holonomy/holonomy/constraint.h b/include/holonomy/holonomy/holonomy/constraint.h index 6224012..343d9eb 100644 --- a/include/holonomy/holonomy/holonomy/constraint.h +++ b/include/holonomy/holonomy/holonomy/constraint.h @@ -9,7 +9,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" /** diff --git a/include/holonomy/holonomy/holonomy/holonomy.h b/include/holonomy/holonomy/holonomy/holonomy.h index 5163a38..adae9a5 100644 --- a/include/holonomy/holonomy/holonomy/holonomy.h +++ b/include/holonomy/holonomy/holonomy/holonomy.h @@ -8,7 +8,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" /** diff --git a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h index 08518ee..d44f5b9 100644 --- a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h +++ b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h @@ -11,7 +11,6 @@ #include "holonomy/core/common.h" #include "holonomy/core/dual_loop.h" #include "holonomy/core/homology_basis.h" - #include "metric/cone_metric.h" /** diff --git a/include/holonomy/holonomy/holonomy/newton.h b/include/holonomy/holonomy/holonomy/newton.h index 54bb61e..7981f59 100644 --- a/include/holonomy/holonomy/holonomy/newton.h +++ b/include/holonomy/holonomy/holonomy/newton.h @@ -8,13 +8,14 @@ #pragma once -#include "holonomy/core/common.h" #include "util/io.h" +#include "holonomy/core/common.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" -#include +#include "optimization/metric_optimization/energy_functor.h" #include + /** * @brief Modified Newton method to find a Marked Penner Cone Metric satisfying holonomy * signature constraints. diff --git a/include/holonomy/holonomy/similarity/conformal.h b/include/holonomy/holonomy/similarity/conformal.h index a845080..ea1150f 100644 --- a/include/holonomy/holonomy/similarity/conformal.h +++ b/include/holonomy/holonomy/similarity/conformal.h @@ -14,7 +14,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" namespace Penner { diff --git a/include/holonomy/holonomy/similarity/constraint.h b/include/holonomy/holonomy/similarity/constraint.h index a363f89..ebbc3cc 100644 --- a/include/holonomy/holonomy/similarity/constraint.h +++ b/include/holonomy/holonomy/similarity/constraint.h @@ -14,7 +14,6 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" namespace Penner { diff --git a/include/holonomy/holonomy/similarity/energy.h b/include/holonomy/holonomy/similarity/energy.h index 154843d..15338db 100644 --- a/include/holonomy/holonomy/similarity/energy.h +++ b/include/holonomy/holonomy/similarity/energy.h @@ -14,9 +14,9 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" +// extending energy functor class #include "optimization/metric_optimization/energy_functor.h" namespace Penner { diff --git a/include/holonomy/holonomy/similarity/layout.h b/include/holonomy/holonomy/similarity/layout.h index 35252dd..911141c 100644 --- a/include/holonomy/holonomy/similarity/layout.h +++ b/include/holonomy/holonomy/similarity/layout.h @@ -13,11 +13,7 @@ #pragma once -#include "holonomy/core/common.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" -#include "parametrization/interpolation.h" - -#include "conformal_ideal_delaunay/OverlayMesh.hh" namespace Penner { namespace Holonomy { diff --git a/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h b/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h index dad80c6..f5e6dcd 100644 --- a/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h +++ b/include/holonomy/holonomy/similarity/similarity_penner_cone_metric.h @@ -8,10 +8,11 @@ #pragma once -#include "holonomy/core/common.h" -#include "field/forms.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" +// form getter and setter asserts +#include "field/forms.h" + /** * @brief Representation for a mesh with a differentiable intrinsic metric and additional * conformal scaling by an integrated harmonic one form, which is sufficient for arbitrary diff --git a/include/metric/metric/cone_metric.h b/include/metric/metric/cone_metric.h index ba22ea7..51b86d2 100644 --- a/include/metric/metric/cone_metric.h +++ b/include/metric/metric/cone_metric.h @@ -14,6 +14,8 @@ #include "util/embedding.h" #include "metric/globals.h" #include "metric/flip_matrix_generator.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" +#include "conformal_ideal_delaunay/globals.hh" /** * @brief Core representation of a differentiable intrinsic metric on an underlying halfedge mesh, with @@ -345,4 +347,50 @@ class DiscreteMetric : public DifferentiableConeMetric void expand_metric_coordinates(const VectorX& metric_coords); }; +template +Mesh change_mesh_type(const Mesh& m) +{ + Mesh _m; + _m.n = m.n; + _m.to = m.to; + _m.f = m.f; + _m.h = m.h; + _m.out = m.out; + _m.opp = m.opp; + _m.type = m.type; + _m.type_input = m.type_input; + _m.R = m.R; + _m.v_rep = m.v_rep; + _m.fixed_dof = m.fixed_dof; + _m.pt_in_f = m.pt_in_f; + + int num_pts = m.pts.size(); + _m.pts.resize(num_pts); + for (int i = 0; i < num_pts; ++i) + { + _m.pts[i].f_id = m.pts[i].f_id; + for (int j = 0; j < 3; ++j) + { + _m.pts[i].bc[j] = (NewScalar)(m.pts[i].bc[j]); + } + } + + int num_halfedges = m.l.size(); + _m.l.resize(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + _m.l[hij] = (NewScalar)(m.l[hij]); + } + + int num_vertices = m.Th_hat.size(); + _m.Th_hat.resize(num_vertices); + for (int vi = 0; vi < num_vertices; ++vi) + { + _m.Th_hat[vi] = (NewScalar)(m.Th_hat[vi]); + } + + return _m; +} + + } // namespace Penner \ No newline at end of file diff --git a/include/metric/metric/reparametrization.h b/include/metric/metric/reparametrization.h index 924a0c7..52a6875 100644 --- a/include/metric/metric/reparametrization.h +++ b/include/metric/metric/reparametrization.h @@ -9,6 +9,7 @@ #pragma once #include "util/common.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" /** * @brief Methods for changing edge and interior barycentric coordinates as determined diff --git a/include/metric/metric/viewer.h b/include/metric/metric/viewer.h index e0fe09d..f245bdf 100644 --- a/include/metric/metric/viewer.h +++ b/include/metric/metric/viewer.h @@ -9,6 +9,7 @@ #pragma once #include "util/common.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" #ifdef ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" diff --git a/include/optimization/optimization/util/shapes.h b/include/optimization/optimization/util/shapes.h index 44558e6..b9641e7 100644 --- a/include/optimization/optimization/util/shapes.h +++ b/include/optimization/optimization/util/shapes.h @@ -9,6 +9,7 @@ #pragma once #include "util/common.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" // Some good simple tests are simplex embeddings that are natural (one vertex // at the origin and others at unit vectors) with metrics that are uniform diff --git a/include/parametrization/parametrization/interpolation.h b/include/parametrization/parametrization/interpolation.h index ed76c67..722f823 100644 --- a/include/parametrization/parametrization/interpolation.h +++ b/include/parametrization/parametrization/interpolation.h @@ -9,6 +9,8 @@ #pragma once #include "util/common.h" +#include "util/embedding.h" +#include "metric/cone_metric.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" /** @@ -292,7 +294,80 @@ void interpolate_vertex_positions( OverlayMesh& reverse_overlay_mesh, Eigen::MatrixXd& V_overlay); +/// Generate an overlay mesh for the mesh m with given metric coordinates integrated +/// as the mesh metric. +/// +/// Note that here the original mesh metric is overwritten, whereas during the optimization +/// the two are kept separate and the mesh maintains the original input length metric. +/// +/// @param[in] m: mesh to add overlay to +/// @param[in] reduced_metric_coords: reduced metric coordinates for overlay mesh +/// @return: overlay mesh with new metric coordinates template -bool overlay_has_all_original_halfedges(OverlayMesh& mo); +OverlayMesh add_overlay(const Mesh& m, const VectorX& reduced_metric_coords) +{ + // Get edge maps + std::vector he2e; + std::vector e2he; + build_edge_maps(m, he2e, e2he); + + // Build refl projection and embedding + std::vector proj; + std::vector embed; + build_refl_proj(m, he2e, e2he, proj, embed); + + // Build overlay mesh from mesh m + Mesh m_l = change_mesh_type(m); + + // Convert mesh Penner coordinates to a halfedge length array l for m + int num_halfedges = he2e.size(); + for (int h = 0; h < num_halfedges; ++h) { + m_l.l[h] = OverlayScalar(exp(reduced_metric_coords[proj[he2e[h]]] / 2.0)); + } + + OverlayMesh mo(m_l); + + return mo; +} + +/// @brief: Make an overlay mesh into a tufted double cover +/// +/// @param[in] mo: mesh to make tufted +template +void make_tufted_overlay(OverlayMesh& mo) +{ + auto& m = mo._m; + if (m.type[0] == 0) return; // nothing to do for closed mesh + + int n_ind_v = m.n_ind_vertices(); + int n_he = m.n_halfedges(); + + // Modify the to and out arrays to identify dependent vertices with their reflection + m.out = std::vector(n_ind_v); + for (int i = 0; i < n_he; ++i) + { + m.out[m.v_rep[m.to[i]]] = i; + m.to[i] = m.v_rep[m.to[i]]; + } + m.v_rep = range(0, n_ind_v); +} + +template +bool overlay_has_all_original_halfedges(OverlayMesh& mo) +{ + std::vector has_original_halfedge(mo.cmesh().n_halfedges(), false); + for (int hi = 0; hi < mo.n_halfedges(); ++hi) { + if (mo.n[hi] == -1) continue; // Deleted halfedge + if (mo.edge_type[hi] == ORIGINAL_EDGE) { + has_original_halfedge[mo.origin_of_origin[hi]] = true; + } else if (mo.edge_type[hi] == ORIGINAL_AND_CURRENT_EDGE) { + has_original_halfedge[mo.origin_of_origin[hi]] = true; + } + } + int num_missing_original_halfedges = + std::count(has_original_halfedge.begin(), has_original_halfedge.end(), false); + + return (num_missing_original_halfedges == 0); +} } // namespace Penner \ No newline at end of file diff --git a/include/parametrization/parametrization/layout.h b/include/parametrization/parametrization/layout.h index 78ee12d..a6a9da7 100644 --- a/include/parametrization/parametrization/layout.h +++ b/include/parametrization/parametrization/layout.h @@ -18,23 +18,6 @@ namespace Penner { -/// Generate an overlay mesh for the mesh m with given metric coordinates integrated -/// as the mesh metric. -/// -/// Note that here the original mesh metric is overwritten, whereas during the optimization -/// the two are kept separate and the mesh maintains the original input length metric. -/// -/// @param[in] m: mesh to add overlay to -/// @param[in] reduced_metric_coords: reduced metric coordinates for overlay mesh -/// @return: overlay mesh with new metric coordinates -template -OverlayMesh add_overlay(const Mesh& m, const VectorX& reduced_metric_coords); - -/// @brief: Make an overlay mesh into a tufted double cover -/// -/// @param[in] mo: mesh to make tufted -template -void make_tufted_overlay(OverlayMesh& mo); /// Given a VF mesh, check that the signed face areas are nonnegative /// @@ -82,6 +65,13 @@ bool check_uv( std::vector compute_layout_topology(const Mesh& m, const std::vector& is_cut_h, int start_h = -1); +/** + * @brief Remove regular (that is, not cone) leaf vertices from a cut graph + * + * @param m: mesh data structure + * @param is_cone: per-vertex mask of cone vertices + * @param is_cut: per-halfedge cut graph + */ void trim_topology(const Mesh& m, const std::vector& is_cone, std::vector& is_cut); /// Given a cut defined on the original or current mesh, pull it back to a cut defined on @@ -97,6 +87,7 @@ std::vector pullback_cut_to_overlay( const std::vector& is_cut_h, bool is_original_cut = true); +// TODO: unclear why this is here. May be deprecated. Eigen::Matrix compute_layout_vertex( const Eigen::Matrix& p1, const Eigen::Matrix& p2, diff --git a/include/parametrization/parametrization/translation.h b/include/parametrization/parametrization/translation.h index 644af93..c505066 100644 --- a/include/parametrization/parametrization/translation.h +++ b/include/parametrization/parametrization/translation.h @@ -9,6 +9,7 @@ #pragma once #include "util/common.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" /** * @brief Method to compute hyperbolic translations determining a continuous map between diff --git a/include/util/util/boundary.h b/include/util/util/boundary.h index 67a5e26..f24bbaa 100644 --- a/include/util/util/boundary.h +++ b/include/util/util/boundary.h @@ -9,14 +9,14 @@ #pragma once #include "util/common.h" -#include "util/vector.h" -#include "util/map.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" /** * @brief Methods to find and navigate the boundary elements of a halfedge mesh. * */ + namespace Penner { /** diff --git a/include/util/util/common.h b/include/util/util/common.h index 4fb33a6..a25fad0 100644 --- a/include/util/util/common.h +++ b/include/util/util/common.h @@ -23,14 +23,14 @@ #include #include "spdlog/spdlog.h" - -#include "conformal_ideal_delaunay/OverlayMesh.hh" -#include "conformal_ideal_delaunay/globals.hh" +#include #ifdef ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" #endif +namespace OverlayProblem {} + namespace Penner { using namespace OverlayProblem; @@ -70,6 +70,18 @@ using std::isnan; const Scalar INF = 1e10; +/** + * @brief Compute the square of a scalar. + * + * @param x: value to square + * @return squared value + */ +template +FloatScalar square(FloatScalar x) +{ + return x * x; +} + /// Swap two doubles. /// /// @param[in, out] a: first double to swap @@ -127,50 +139,4 @@ inline std::vector arange(size_t n) inline Scalar pos_fmod(Scalar x, Scalar y) { return (0 == y) ? x : x - y * floor(x / y); } -template -Mesh change_mesh_type(const Mesh& m) -{ - Mesh _m; - _m.n = m.n; - _m.to = m.to; - _m.f = m.f; - _m.h = m.h; - _m.out = m.out; - _m.opp = m.opp; - _m.type = m.type; - _m.type_input = m.type_input; - _m.R = m.R; - _m.v_rep = m.v_rep; - _m.fixed_dof = m.fixed_dof; - _m.pt_in_f = m.pt_in_f; - - int num_pts = m.pts.size(); - _m.pts.resize(num_pts); - for (int i = 0; i < num_pts; ++i) - { - _m.pts[i].f_id = m.pts[i].f_id; - for (int j = 0; j < 3; ++j) - { - _m.pts[i].bc[j] = (NewScalar)(m.pts[i].bc[j]); - } - } - - int num_halfedges = m.l.size(); - _m.l.resize(num_halfedges); - for (int hij = 0; hij < num_halfedges; ++hij) - { - _m.l[hij] = (NewScalar)(m.l[hij]); - } - - int num_vertices = m.Th_hat.size(); - _m.Th_hat.resize(num_vertices); - for (int vi = 0; vi < num_vertices; ++vi) - { - _m.Th_hat[vi] = (NewScalar)(m.Th_hat[vi]); - } - - return _m; -} - - } // namespace Penner \ No newline at end of file diff --git a/include/util/util/embedding.h b/include/util/util/embedding.h index 8bf4479..5636efd 100644 --- a/include/util/util/embedding.h +++ b/include/util/util/embedding.h @@ -23,10 +23,10 @@ #pragma once #include "util/common.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" namespace Penner { - /// Data structure to store maps from full doubled mesh halfedge indices to edge indices /// and to embedded mesh edge indices. Also includes lists of free and fixed vertices and /// edge indices for variable mesh functions. diff --git a/include/util/util/io.h b/include/util/util/io.h index c3aa2fa..8f624ac 100644 --- a/include/util/util/io.h +++ b/include/util/util/io.h @@ -14,8 +14,7 @@ #pragma once #include "util/common.h" - -#include "util/embedding.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" #include #include diff --git a/include/util/util/union_find.h b/include/util/util/union_find.h index 864e487..798d11b 100644 --- a/include/util/util/union_find.h +++ b/include/util/util/union_find.h @@ -13,10 +13,9 @@ #pragma once -#include "feature/core/common.h" +#include "util/common.h" namespace Penner { -namespace Feature { /** * @brief Simple implementation of a union find data structure @@ -189,5 +188,4 @@ class UnionFind std::vector m_parent; }; -} // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/include/util/util/vf_mesh.h b/include/util/util/vf_mesh.h index b5f415f..8df7953 100644 --- a/include/util/util/vf_mesh.h +++ b/include/util/util/vf_mesh.h @@ -137,6 +137,13 @@ std::tuple reindex_mesh( const Eigen::MatrixXi& F, const std::vector& vtx_reindex); +/** + * @brief Determine if a VF mesh is manifold + * @param F: mesh faces + * @return true iff the mesh is edge and vertex manifold + */ +bool is_manifold(const Eigen::MatrixXi& F); + /** * @brief Find the seams of the parameterization of a closed mesh. * diff --git a/src/feature/core/common.cpp b/src/feature/core/common.cpp index a176847..b144c98 100644 --- a/src/feature/core/common.cpp +++ b/src/feature/core/common.cpp @@ -8,22 +8,9 @@ #include "feature/core/common.h" -#include "field/intrinsic_field.h" - -#include -#include - namespace Penner { namespace Feature { -bool is_manifold(const Eigen::MatrixXi& F) -{ - Eigen::VectorXi B; - if (!igl::is_edge_manifold(F)) return false; - if (!igl::is_vertex_manifold(F, B)) return false; - - return true; -} std::vector generate_vertex_one_ring(const Mesh& m, int vertex_index) { @@ -42,32 +29,6 @@ std::vector generate_vertex_one_ring(const Mesh& m, int vertex_inde } -std::tuple reindex_mesh( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const std::vector& vtx_reindex) -{ - int num_orig_vertices = vtx_reindex.size(); - - // reindex vertices - Eigen::MatrixXd V_reindex = V; - for (int vi = 0; vi < num_orig_vertices; ++vi) { - V_reindex.row(vtx_reindex[vi]) = V.row(vi); - } - - // reindex faces - int num_faces = F.rows(); - Eigen::MatrixXi F_reindex = F; - for (int fijk = 0; fijk < num_faces; ++fijk) { - for (int i = 0; i < 3; ++i) { - if (F(fijk, i) >= num_orig_vertices) continue; - F_reindex(fijk, i) = vtx_reindex[F(fijk, i)]; - } - } - - return std::make_tuple(V_reindex, F_reindex); -} - std::vector> reindex_endpoints( const std::vector>& endpoints, const std::vector& vtx_reindex) diff --git a/src/feature/core/component_mesh.cpp b/src/feature/core/component_mesh.cpp index 822ad5d..1a12928 100644 --- a/src/feature/core/component_mesh.cpp +++ b/src/feature/core/component_mesh.cpp @@ -9,15 +9,19 @@ #include "feature/core/component_mesh.h" -#include "holonomy/core/dual_lengths.h" +#include "util/vector.h" +#include "util/union_find.h" + +// mesh unioning +#include "feature/core/union_meshes.h" + +// check for interior vertices #include "holonomy/holonomy/cones.h" #include #include #include -#include "util/vector.h" - namespace Penner { namespace Feature { diff --git a/src/feature/core/io.cpp b/src/feature/core/io.cpp index 7ff182f..e441169 100644 --- a/src/feature/core/io.cpp +++ b/src/feature/core/io.cpp @@ -8,11 +8,8 @@ #include "feature/core/io.h" -#include "feature/core/vf_corners.h" #include "util/vf_mesh.h" -#include - namespace Penner { namespace Feature { diff --git a/src/feature/core/quads.cpp b/src/feature/core/quads.cpp index 408ef52..127bfee 100644 --- a/src/feature/core/quads.cpp +++ b/src/feature/core/quads.cpp @@ -8,7 +8,6 @@ #include "feature/core/quads.h" -#include namespace Penner { namespace Feature { diff --git a/src/feature/core/vf_corners.cpp b/src/feature/core/vf_corners.cpp index 43b5992..471e26e 100644 --- a/src/feature/core/vf_corners.cpp +++ b/src/feature/core/vf_corners.cpp @@ -8,7 +8,6 @@ #include "feature/core/vf_corners.h" -#include #include namespace Penner { diff --git a/src/feature/core/viewer.cpp b/src/feature/core/viewer.cpp index bf612e9..1f6109e 100644 --- a/src/feature/core/viewer.cpp +++ b/src/feature/core/viewer.cpp @@ -8,20 +8,28 @@ #include "feature/core/viewer.h" -#include "feature/core/vf_corners.h" -#include "feature/feature/error.h" -#include "feature/feature/features.h" -#include "feature/feature/gluing.h" -#include "field/facet_field.h" +#include "util/vector.h" +#include "util/vf_mesh.h" + +#include "metric/viewer.h" + +// used to generate initial mesh +#include "optimization/interface.h" + +// frame field from reference #include "field/frame_field.h" -#include "field/cross_field.h" -#include "feature/core/quads.h" + +// cone angle defects for glued mesh +#include "feature/feature/gluing.h" + +// conformal scale factors #include "metric/projection.h" -#include "field/intrinsic_field.h" -#include "metric/viewer.h" -#include "holonomy/core/viewer.h" -#include "util/vf_mesh.h" +// uv alignment error +#include "feature/feature/error.h" + +// quad valences +#include "feature/core/quads.h" #include #include diff --git a/src/feature/dirichlet/angle_constraint_relaxer.cpp b/src/feature/dirichlet/angle_constraint_relaxer.cpp index be07b91..3a88768 100644 --- a/src/feature/dirichlet/angle_constraint_relaxer.cpp +++ b/src/feature/dirichlet/angle_constraint_relaxer.cpp @@ -7,14 +7,12 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "feature/dirichlet/angle_constraint_relaxer.h" -#include "feature/core/component_mesh.h" -#include "holonomy/holonomy/constraint.h" -#include "holonomy/holonomy/holonomy.h" -#include "metric/constraint.h" -#include "metric/viewer.h" -#include "util/io.h" + #include "util/vector.h" +// double mesh for viewer +#include "metric/viewer.h" + namespace Penner { namespace Feature { diff --git a/src/feature/dirichlet/cone_perturber.cpp b/src/feature/dirichlet/cone_perturber.cpp index 6468264..e551244 100644 --- a/src/feature/dirichlet/cone_perturber.cpp +++ b/src/feature/dirichlet/cone_perturber.cpp @@ -10,6 +10,7 @@ #include "metric/constraint.h" #include "feature/core/vf_corners.h" +#include "feature/feature/gluing.h" namespace Penner { namespace Feature { diff --git a/src/feature/dirichlet/constraint.cpp b/src/feature/dirichlet/constraint.cpp index d52759d..f40ea35 100644 --- a/src/feature/dirichlet/constraint.cpp +++ b/src/feature/dirichlet/constraint.cpp @@ -7,13 +7,8 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "feature/dirichlet/constraint.h" -#include "feature/core/component_mesh.h" + #include "holonomy/holonomy/constraint.h" -#include "holonomy/holonomy/holonomy.h" -#include "metric/constraint.h" -#include "metric/viewer.h" -#include "util/io.h" -#include "util/vector.h" namespace Penner { namespace Feature { diff --git a/src/feature/dirichlet/optimization.cpp b/src/feature/dirichlet/optimization.cpp index ffec26a..d0a99cc 100644 --- a/src/feature/dirichlet/optimization.cpp +++ b/src/feature/dirichlet/optimization.cpp @@ -13,9 +13,9 @@ #endif #include "feature/dirichlet/angle_constraint_relaxer.h" -#include "feature/dirichlet/constraint.h" -#include "holonomy/holonomy/constraint.h" #include "metric/constraint.h" +#include "feature/dirichlet/constraint.h" + #include #include diff --git a/src/feature/feature/error.cpp b/src/feature/feature/error.cpp index 6cdcd9a..ce0786b 100644 --- a/src/feature/feature/error.cpp +++ b/src/feature/feature/error.cpp @@ -7,16 +7,16 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "feature/feature/error.h" -#include -#include "feature/core/common.h" -#include "feature/core/component_mesh.h" -#include "feature/dirichlet/constraint.h" -#include "holonomy/holonomy/constraint.h" -#include "metric/constraint.h" #include "util/vf_mesh.h" -#include +// get max in component +#include "feature/core/component_mesh.h" + +// constraint in component +#include "holonomy/holonomy/constraint.h" + +#include namespace Penner { namespace Feature { diff --git a/src/feature/feature/features.cpp b/src/feature/feature/features.cpp index 5877604..c8885a3 100644 --- a/src/feature/feature/features.cpp +++ b/src/feature/feature/features.cpp @@ -8,18 +8,14 @@ #include "feature/feature/features.h" -#include "feature/core/common.h" -#include "holonomy/core/viewer.h" -#include "feature/core/component_mesh.h" -#include "feature/core/vf_corners.h" -#include "holonomy/holonomy/cones.h" #include "util/vector.h" +#include "metric/viewer.h" #include + #include #include -#include #ifdef ENABLE_VISUALIZATION #include "polyscope/point_cloud.h" diff --git a/src/feature/interface.cpp b/src/feature/interface.cpp index b6b308e..20a0203 100644 --- a/src/feature/interface.cpp +++ b/src/feature/interface.cpp @@ -23,7 +23,6 @@ #include "feature/feature/gluing.h" #include "feature/core/component_mesh.h" -#include "holonomy/core/viewer.h" #include "feature/dirichlet/angle_constraint_relaxer.h" #include "feature/core/vf_corners.h" #include "feature/feature/error.h" @@ -32,6 +31,7 @@ #include "feature/surgery/cut_mesh_layout.h" #include "feature/surgery/refinement.h" #include "feature/surgery/stitching.h" +#include "feature/core/union_meshes.h" #include diff --git a/src/feature/pybind.cpp b/src/feature/pybind.cpp index c059f43..575f2a6 100644 --- a/src/feature/pybind.cpp +++ b/src/feature/pybind.cpp @@ -26,16 +26,11 @@ #include "holonomy/core/viewer.h" -#include "feature/core/common.h" #include "feature/core/component_mesh.h" -#include "feature/core/vf_corners.h" #include "feature/dirichlet/constraint.h" #include "feature/dirichlet/optimization.h" -#include "feature/dirichlet/dirichlet_penner_cone_metric.h" #include "feature/dirichlet/angle_constraint_relaxer.h" -#include "feature/experimental/polygon_cones.h" #include "feature/feature/error.h" -#include "feature/feature/features.h" #include "feature/core/viewer.h" #include "feature/interface.h" #include "feature/surgery/refinement.h" @@ -173,7 +168,6 @@ void init_feature_pybind(pybind11::module& m) m.def("compute_face_edges_from_corners", &compute_face_edges_from_corners, default_call_guard); m.def("optimize_relaxed_angles", &optimize_relaxed_angles, default_call_guard); - m.def("is_manifold", &is_manifold, default_call_guard); m.def("align_to_hard_features", &align_to_hard_features, default_call_guard); m.def("stitch_cut_overlay", &stitch_cut_overlay, default_call_guard); m.def("compute_edge_alignment", &compute_edge_alignment, default_call_guard); @@ -201,7 +195,6 @@ void init_feature_pybind(pybind11::module& m) #endif m.def("load_feature_edges", &load_feature_edges, default_call_guard); m.def("load_mesh_edges", &load_mesh_edges, default_call_guard); - m.def("generate_polygon_cones", &generate_polygon_cones, default_call_guard); } diff --git a/src/feature/surgery/cut_mesh_layout.cpp b/src/feature/surgery/cut_mesh_layout.cpp index 6f88dd8..1fdb46f 100644 --- a/src/feature/surgery/cut_mesh_layout.cpp +++ b/src/feature/surgery/cut_mesh_layout.cpp @@ -8,18 +8,19 @@ #include "feature/surgery/cut_mesh_layout.h" +#include "util/vector.h" #include "util/vf_mesh.h" -#include "parametrization/interpolation.h" +#include "conformal_ideal_delaunay/ConformalInterface.hh" #include "parametrization/layout.h" #include "parametrization/parametrize.h" - -#include "feature/feature/error.h" #include "feature/core/component_mesh.h" -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" -#include "conformal_ideal_delaunay/ConformalInterface.hh" -#include "conformal_ideal_delaunay/Layout.hh" +// overlay graph management +#include "parametrization/interpolation.h" + +// check edge alignment +#include "feature/feature/error.h" #include diff --git a/src/feature/surgery/cut_metric_generator.cpp b/src/feature/surgery/cut_metric_generator.cpp index a997139..069ae9e 100644 --- a/src/feature/surgery/cut_metric_generator.cpp +++ b/src/feature/surgery/cut_metric_generator.cpp @@ -8,17 +8,22 @@ #include "feature/surgery/cut_metric_generator.h" -#include "field/cross_field.h" -#include "field/frame_field.h" #include "field/intrinsic_field.h" -#include "holonomy/holonomy/cones.h" - -#include "feature/core/component_mesh.h" -#include "feature/core/union_meshes.h" #include "feature/core/vf_corners.h" -#include "feature/dirichlet/cone_perturber.h" +#include "feature/core/union_meshes.h" +#include "feature/core/component_mesh.h" + +// frame field optimization +#include "field/frame_field.h" +#include "field/cross_field.h" + +// get glued cones #include "feature/feature/gluing.h" +// fix invalid or poor cones +#include "holonomy/holonomy/cones.h" +#include "feature/dirichlet/cone_perturber.h" + #include #include diff --git a/src/feature/surgery/refinement.cpp b/src/feature/surgery/refinement.cpp index d87244c..14a920b 100644 --- a/src/feature/surgery/refinement.cpp +++ b/src/feature/surgery/refinement.cpp @@ -9,9 +9,7 @@ #include "feature/surgery/refinement.h" #include "util/boundary.h" -#include "util/vector.h" -#include "holonomy/core/viewer.h" -#include "feature/core/component_mesh.h" +#include "util/vf_mesh.h" namespace Penner { namespace Feature { diff --git a/src/feature/surgery/stitching.cpp b/src/feature/surgery/stitching.cpp index 98176ba..75a7e2c 100644 --- a/src/feature/surgery/stitching.cpp +++ b/src/feature/surgery/stitching.cpp @@ -8,20 +8,13 @@ #include "feature/surgery/stitching.h" -#include "holonomy/core/viewer.h" -#include "parametrization/refinement.h" -#include "parametrization/layout.h" -#include "util/io.h" #include "util/vector.h" #include "util/vf_mesh.h" -#include "igl/remove_unreferenced.h" -#include "igl/boundary_loop.h" +#include "parametrization/layout.h" +#include "parametrization/refinement.h" -#ifdef ENABLE_VISUALIZATION -#include "polyscope/point_cloud.h" -#include "polyscope/surface_mesh.h" -#endif +#include namespace Penner { namespace Feature { diff --git a/src/field/cross_field.cpp b/src/field/cross_field.cpp index ed8df98..b21091d 100644 --- a/src/field/cross_field.cpp +++ b/src/field/cross_field.cpp @@ -144,7 +144,6 @@ std::array reduce_curl( extField(f, 3 * i + j) = cross_field[i](f, j); } } - spdlog::trace("row {}: {}", f, extField.row(f)); } assert(tb.sources.rows()==extField.rows()); diff --git a/src/field/facet_field.cpp b/src/field/facet_field.cpp index 3871b74..fc9310f 100644 --- a/src/field/facet_field.cpp +++ b/src/field/facet_field.cpp @@ -7,8 +7,6 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "field/facet_field.h" -#include "util/linear_algebra.h" -#include #include namespace Penner { diff --git a/src/field/field.cpp b/src/field/field.cpp index 9877298..84aec09 100644 --- a/src/field/field.cpp +++ b/src/field/field.cpp @@ -12,7 +12,6 @@ #include "field/forms.h" #include "metric/constraint.h" -#include "util/vector.h" #if USE_COMISO #include diff --git a/src/field/frame_field.cpp b/src/field/frame_field.cpp index 457fc49..f8d10a4 100644 --- a/src/field/frame_field.cpp +++ b/src/field/frame_field.cpp @@ -9,8 +9,6 @@ #include "field/frame_field.h" #include "parametrization/refinement.h" #include "util/vf_mesh.h" -#include "util/vector.h" -#include "util/linear_algebra.h" #include "util/map.h" #include @@ -22,8 +20,6 @@ #include #include -// TODO: Cleaning pass - namespace Penner { namespace Field { diff --git a/src/holonomy/core/boundary_basis.cpp b/src/holonomy/core/boundary_basis.cpp index e6a976e..c23d8f3 100644 --- a/src/holonomy/core/boundary_basis.cpp +++ b/src/holonomy/core/boundary_basis.cpp @@ -8,10 +8,12 @@ #include "holonomy/core/boundary_basis.h" -#include "holonomy/core/dual_lengths.h" #include "util/boundary.h" #include "util/vector.h" +// compute dual edge lengths for minimization +#include "holonomy/core/dual_lengths.h" + namespace Penner { namespace Holonomy { diff --git a/src/holonomy/core/dual_lengths.cpp b/src/holonomy/core/dual_lengths.cpp index e370e87..5d2bcf9 100644 --- a/src/holonomy/core/dual_lengths.cpp +++ b/src/holonomy/core/dual_lengths.cpp @@ -8,10 +8,9 @@ #include "holonomy/core/dual_lengths.h" +// corner angle computations #include "metric/constraint.h" -#include - namespace Penner { namespace Holonomy { diff --git a/src/holonomy/core/dual_loop.cpp b/src/holonomy/core/dual_loop.cpp index 0a07094..e9f1df3 100644 --- a/src/holonomy/core/dual_loop.cpp +++ b/src/holonomy/core/dual_loop.cpp @@ -7,12 +7,6 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "holonomy/core/dual_loop.h" -#include -#include - -#ifdef ENABLE_VISUALIZATION -#include "polyscope/surface_mesh.h" -#endif namespace Penner { namespace Holonomy { diff --git a/src/holonomy/core/dual_segment.cpp b/src/holonomy/core/dual_segment.cpp index fa7e027..2b63989 100644 --- a/src/holonomy/core/dual_segment.cpp +++ b/src/holonomy/core/dual_segment.cpp @@ -7,8 +7,6 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "holonomy/core/dual_segment.h" -#include -#include #ifdef ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" diff --git a/src/holonomy/core/homology_basis.cpp b/src/holonomy/core/homology_basis.cpp index 6a69ce4..ee68fcc 100644 --- a/src/holonomy/core/homology_basis.cpp +++ b/src/holonomy/core/homology_basis.cpp @@ -9,10 +9,11 @@ #include "holonomy/core/homology_basis.h" #include "util/vector.h" + +// dual edge lengths for optimization #include "holonomy/core/dual_lengths.h" #include -#include "util/embedding.h" namespace Penner { namespace Holonomy { diff --git a/src/holonomy/core/viewer.cpp b/src/holonomy/core/viewer.cpp index 95e29e1..f7afadb 100644 --- a/src/holonomy/core/viewer.cpp +++ b/src/holonomy/core/viewer.cpp @@ -9,26 +9,24 @@ #include "holonomy/core/viewer.h" +#include "util/vector.h" #include "util/vf_mesh.h" -#include #include #include -#include #include #include #include -#include +//#include -#include "util/vector.h" -#include "util/vf_mesh.h" +// used to visualize constraint error #include "holonomy/holonomy/constraint.h" + +// cross field generation for viewer #include "field/frame_field.h" -#include "metric/viewer.h" #ifdef ENABLE_VISUALIZATION -#include "polyscope/curve_network.h" #include "polyscope/point_cloud.h" #include "polyscope/surface_mesh.h" #endif diff --git a/src/holonomy/holonomy/cones.cpp b/src/holonomy/holonomy/cones.cpp index 64ba7be..6892be5 100644 --- a/src/holonomy/holonomy/cones.cpp +++ b/src/holonomy/holonomy/cones.cpp @@ -9,11 +9,17 @@ #include "holonomy/holonomy/cones.h" #include "util/boundary.h" -#include "field/forms.h" -#include "holonomy/holonomy/holonomy.h" +#include "util/vector.h" +// corner angle computation #include "metric/constraint.h" +// check valid one forms +#include "field/forms.h" + +// holonomy computation +#include "holonomy/holonomy/holonomy.h" + #include namespace Penner { diff --git a/src/holonomy/holonomy/constraint.cpp b/src/holonomy/holonomy/constraint.cpp index cd9fe9f..d925304 100644 --- a/src/holonomy/holonomy/constraint.cpp +++ b/src/holonomy/holonomy/constraint.cpp @@ -8,9 +8,10 @@ #include "holonomy/holonomy/constraint.h" -#include "holonomy/holonomy/holonomy.h" - +#include "util/vector.h" +#include "util/linear_algebra.h" #include "metric/constraint.h" +#include "holonomy/holonomy/holonomy.h" namespace Penner { namespace Holonomy { diff --git a/src/holonomy/holonomy/holonomy.cpp b/src/holonomy/holonomy/holonomy.cpp index c5adf75..d9772c5 100644 --- a/src/holonomy/holonomy/holonomy.cpp +++ b/src/holonomy/holonomy/holonomy.cpp @@ -10,10 +10,6 @@ #include "holonomy/core/dual_loop.h" -#include "metric/area.h" - -#include - namespace Penner { namespace Holonomy { diff --git a/src/holonomy/holonomy/marked_penner_cone_metric.cpp b/src/holonomy/holonomy/marked_penner_cone_metric.cpp index 6073af3..b484fca 100644 --- a/src/holonomy/holonomy/marked_penner_cone_metric.cpp +++ b/src/holonomy/holonomy/marked_penner_cone_metric.cpp @@ -8,7 +8,6 @@ #include "holonomy/holonomy/marked_penner_cone_metric.h" -#include "util/vector.h" #include "holonomy/core/viewer.h" #include "holonomy/holonomy/constraint.h" #include "holonomy/holonomy/holonomy.h" @@ -17,8 +16,6 @@ #include "metric/constraint.h" #include "metric/projection.h" -#include "conformal_ideal_delaunay/ConformalInterface.hh" - #ifdef ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" #endif diff --git a/src/holonomy/holonomy/newton.cpp b/src/holonomy/holonomy/newton.cpp index 859f0b5..ab6ebd2 100644 --- a/src/holonomy/holonomy/newton.cpp +++ b/src/holonomy/holonomy/newton.cpp @@ -8,21 +8,21 @@ #include "holonomy/holonomy/newton.h" -#include "holonomy/holonomy/constraint.h" -#include "holonomy/holonomy/holonomy.h" -#include "holonomy/core/viewer.h" #include "util/vector.h" +#include "util/linear_algebra.h" -#include -#include "optimization/metric_optimization/energies.h" -#include "optimization/metric_optimization/energy_functor.h" +// used for detailed metric checkpoint statistics #include "metric/projection.h" #include "metric/shear.h" -#include "util/io.h" + +// root mean square error computations +#include "optimization/metric_optimization/energies.h" + +// used for state viewing function +#include "holonomy/core/viewer.h" #ifdef USE_SUITESPARSE #include -//#include #endif #ifdef ENABLE_VISUALIZATION diff --git a/src/holonomy/holonomy/rotation_form.cpp b/src/holonomy/holonomy/rotation_form.cpp index 15edfdf..f08cacb 100644 --- a/src/holonomy/holonomy/rotation_form.cpp +++ b/src/holonomy/holonomy/rotation_form.cpp @@ -8,11 +8,11 @@ #include "holonomy/holonomy/rotation_form.h" +#include "util/linear_algebra.h" #include "field/field.h" #include "field/intrinsic_field.h" #include "field/forms.h" -#include #include namespace Penner { diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index 272e2b7..caa83cf 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -20,6 +20,7 @@ #include "metric/cone_metric.h" #include "metric/constraint.h" +#include "optimization/interface.h" #include "parametrization/interpolation.h" #include "parametrization/refinement.h" #include "util/io.h" @@ -27,7 +28,6 @@ #include -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "geometrycentral/surface/integer_coordinates_intrinsic_triangulation.h" diff --git a/src/holonomy/pybind.cpp b/src/holonomy/pybind.cpp index 50554f7..68823ba 100644 --- a/src/holonomy/pybind.cpp +++ b/src/holonomy/pybind.cpp @@ -9,7 +9,6 @@ #include "holonomy/pybind.h" #include "holonomy/interface.h" -#include "holonomy/core/common.h" #include "holonomy/core/quality.h" #include "holonomy/holonomy/cones.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" diff --git a/src/holonomy/similarity/conformal.cpp b/src/holonomy/similarity/conformal.cpp index befbe5c..d5bf047 100644 --- a/src/holonomy/similarity/conformal.cpp +++ b/src/holonomy/similarity/conformal.cpp @@ -8,7 +8,7 @@ #include "holonomy/similarity/conformal.h" -#include +#include "util/linear_algebra.h" #include "holonomy/similarity/constraint.h" #include "holonomy/holonomy/holonomy.h" diff --git a/src/holonomy/similarity/layout.cpp b/src/holonomy/similarity/layout.cpp index d56edbd..accf2b0 100644 --- a/src/holonomy/similarity/layout.cpp +++ b/src/holonomy/similarity/layout.cpp @@ -11,6 +11,7 @@ #include "parametrization/layout.h" #include "parametrization/translation.h" #include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "parametrization/interpolation.h" namespace Penner { namespace Holonomy { diff --git a/src/metric/area.cpp b/src/metric/area.cpp index 13de093..d2506a0 100644 --- a/src/metric/area.cpp +++ b/src/metric/area.cpp @@ -8,8 +8,6 @@ #include "metric/area.h" -#include "util/embedding.h" - namespace Penner { Scalar squared_area(Scalar li, Scalar lj, Scalar lk) diff --git a/src/metric/constraint.cpp b/src/metric/constraint.cpp index 4f2817b..c56e039 100644 --- a/src/metric/constraint.cpp +++ b/src/metric/constraint.cpp @@ -8,7 +8,6 @@ #include "metric/constraint.h" -#include "util/embedding.h" #include "util/linear_algebra.h" #include "util/boundary.h" #include "metric/area.h" diff --git a/src/metric/projection.cpp b/src/metric/projection.cpp index 5b193c7..3fbc4fb 100644 --- a/src/metric/projection.cpp +++ b/src/metric/projection.cpp @@ -9,12 +9,8 @@ #include "metric/projection.h" #include -#include -#include #include "conformal_ideal_delaunay/ConformalInterface.hh" -#include "util/embedding.h" #include "util/linear_algebra.h" -#include "util/vector.h" #include "metric/constraint.h" #include "metric/globals.h" diff --git a/src/metric/reparametrization.cpp b/src/metric/reparametrization.cpp index f79dbe3..d3da646 100644 --- a/src/metric/reparametrization.cpp +++ b/src/metric/reparametrization.cpp @@ -9,7 +9,6 @@ #include "metric/reparametrization.h" #include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" -#include "util/embedding.h" /// FIXME Do cleaning pass diff --git a/src/metric/shear.cpp b/src/metric/shear.cpp index be59033..b496c32 100644 --- a/src/metric/shear.cpp +++ b/src/metric/shear.cpp @@ -8,7 +8,7 @@ #include "metric/shear.h" -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" +#include "util/vector.h" #include "util/embedding.h" #include "metric/projection.h" #include "metric/reparametrization.h" @@ -388,7 +388,7 @@ void compute_shear_basis_coordinates( shear_coords.minCoeff(), shear_coords.maxCoeff()); assert(solver.info() == Eigen::Success); - assert(vector_equal(shear_coords, solver.solve(inner_product_matrix * shear_coords), 1e-5)); + assert(vector_equal(shear_coords, solver.solve(inner_product_matrix * shear_coords), 1e-5)); // Get the corresponding scale factors for shear space to original metric // This is the (additive) inverse of the scale factors from original to shear diff --git a/src/metric/viewer.cpp b/src/metric/viewer.cpp index 2f30330..891316c 100644 --- a/src/metric/viewer.cpp +++ b/src/metric/viewer.cpp @@ -14,17 +14,13 @@ #include #include #include -#include #include -#include -#include #include #include #include "util/vector.h" #include "util/map.h" -#include "util/vf_mesh.h" #ifdef ENABLE_VISUALIZATION #include "polyscope/curve_network.h" diff --git a/src/optimization/interface.cpp b/src/optimization/interface.cpp index 31b2b91..282dcb5 100644 --- a/src/optimization/interface.cpp +++ b/src/optimization/interface.cpp @@ -8,8 +8,6 @@ #include "optimization/interface.h" #include "conformal_ideal_delaunay/ConformalInterface.hh" -#include "util/embedding.h" -#include "util/vector.h" #include "parametrization/interpolation.h" #include "parametrization/layout.h" #include "metric/projection.h" diff --git a/src/optimization/metric_optimization/energy_weights.cpp b/src/optimization/metric_optimization/energy_weights.cpp index 855782c..8e74dbb 100644 --- a/src/optimization/metric_optimization/energy_weights.cpp +++ b/src/optimization/metric_optimization/energy_weights.cpp @@ -8,11 +8,11 @@ #include "optimization/metric_optimization/energy_functor.h" +#include "util/vector.h" #include "metric/area.h" #include "metric/constraint.h" #include "optimization/metric_optimization/energies.h" #include "metric/projection.h" -#include "util/vector.h" #include #include diff --git a/src/optimization/metric_optimization/explicit_optimization.cpp b/src/optimization/metric_optimization/explicit_optimization.cpp index 9d7c5d3..15293ff 100644 --- a/src/optimization/metric_optimization/explicit_optimization.cpp +++ b/src/optimization/metric_optimization/explicit_optimization.cpp @@ -8,10 +8,7 @@ #include "optimization/metric_optimization/explicit_optimization.h" -#include -#include #include "metric/constraint.h" -#include "util/embedding.h" #include "optimization/metric_optimization/energies.h" #include "metric/globals.h" #include "util/io.h" diff --git a/src/optimization/metric_optimization/implicit_optimization.cpp b/src/optimization/metric_optimization/implicit_optimization.cpp index 8b99616..3b63f4b 100644 --- a/src/optimization/metric_optimization/implicit_optimization.cpp +++ b/src/optimization/metric_optimization/implicit_optimization.cpp @@ -9,10 +9,8 @@ #include "optimization/metric_optimization/implicit_optimization.h" #include -#include #include "metric/area.h" #include "metric/constraint.h" -#include "util/embedding.h" #include "optimization/metric_optimization/energies.h" #include "metric/globals.h" #include "util/io.h" diff --git a/src/optimization/pybind.cpp b/src/optimization/pybind.cpp index 088a0f7..f451fd4 100644 --- a/src/optimization/pybind.cpp +++ b/src/optimization/pybind.cpp @@ -11,7 +11,6 @@ #include "metric/area.h" #include "metric/constraint.h" -#include "util/embedding.h" #include "util/io.h" #include "optimization/metric_optimization/energies.h" #include "optimization/metric_optimization/energy_functor.h" @@ -23,7 +22,6 @@ #include "metric/projection.h" #include "parametrization/refinement.h" #include "parametrization/parametrize.h" -#include "metric/reparametrization.h" #include "optimization/util/shapes.h" #include "metric/shear.h" #include "parametrization/translation.h" diff --git a/src/optimization/tests/test_area.cpp b/src/optimization/tests/test_area.cpp index a1f4808..e2b0c1f 100644 --- a/src/optimization/tests/test_area.cpp +++ b/src/optimization/tests/test_area.cpp @@ -9,7 +9,6 @@ #include #include "metric/area.h" -#include "util/vector.h" #include "optimization/util/shapes.h" #include "metric/cone_metric.h" diff --git a/src/optimization/tests/test_shear.cpp b/src/optimization/tests/test_shear.cpp index 76160ff..38527ab 100644 --- a/src/optimization/tests/test_shear.cpp +++ b/src/optimization/tests/test_shear.cpp @@ -10,7 +10,6 @@ #include "metric/shear.h" #include "optimization/util/shapes.h" -#include "util/embedding.h" #include "util/vector.h" #include "metric/cone_metric.h" #include "optimization/interface.h" diff --git a/src/parametrization/interpolation.cpp b/src/parametrization/interpolation.cpp index d46d453..3cd0daa 100644 --- a/src/parametrization/interpolation.cpp +++ b/src/parametrization/interpolation.cpp @@ -8,9 +8,7 @@ #include "parametrization/interpolation.h" -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" #include "conformal_ideal_delaunay/ConformalInterface.hh" -#include "util/embedding.h" #include "util/vector.h" #include "metric/projection.h" #include "metric/reparametrization.h" @@ -1000,26 +998,7 @@ bool InterpolationMesh::are_valid_halfedge_translations(const Vec return true; } -template -bool overlay_has_all_original_halfedges(OverlayMesh& mo) -{ - std::vector has_original_halfedge(mo.cmesh().n_halfedges(), false); - for (int hi = 0; hi < mo.n_halfedges(); ++hi) { - if (mo.n[hi] == -1) continue; // Deleted halfedge - if (mo.edge_type[hi] == ORIGINAL_EDGE) { - has_original_halfedge[mo.origin_of_origin[hi]] = true; - } else if (mo.edge_type[hi] == ORIGINAL_AND_CURRENT_EDGE) { - has_original_halfedge[mo.origin_of_origin[hi]] = true; - } - } - int num_missing_original_halfedges = - std::count(has_original_halfedge.begin(), has_original_halfedge.end(), false); - - return (num_missing_original_halfedges == 0); -} - template class InterpolationMesh; -template bool overlay_has_all_original_halfedges(OverlayMesh& mo); template void interpolate_penner_coordinates( const Mesh& mesh, const VectorX& halfedge_metric_coords, @@ -1042,7 +1021,6 @@ template void interpolate_vertex_positions( #ifdef WITH_MPFR #ifndef MULTIPRECISION template class InterpolationMesh; -template bool overlay_has_all_original_halfedges(OverlayMesh& mo); template void interpolate_penner_coordinates( const Mesh& mesh, const VectorX& halfedge_metric_coords, diff --git a/src/parametrization/layout.cpp b/src/parametrization/layout.cpp index b20ff08..717ca23 100644 --- a/src/parametrization/layout.cpp +++ b/src/parametrization/layout.cpp @@ -11,69 +11,18 @@ #include #include #include -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" + #include "conformal_ideal_delaunay/ConformalInterface.hh" -#include "conformal_ideal_delaunay/Layout.hh" +#include "metric/cone_metric.h" +#include "util/vector.h" #include "util/embedding.h" +//#include "util/io.h" +//#include "metric/constraint.h" +//#include "parametrization/refinement.h" #include "parametrization/interpolation.h" -#include "metric/viewer.h" -#include "metric/projection.h" -#include "metric/constraint.h" -#include "parametrization/refinement.h" -#include "parametrization/translation.h" -#include "util/vector.h" -#include "util/vf_mesh.h" -#include "util/io.h" - -// TODO: cleaning pass namespace Penner { -template -OverlayMesh add_overlay(const Mesh& m, const VectorX& reduced_metric_coords) -{ - // Get edge maps - std::vector he2e; - std::vector e2he; - build_edge_maps(m, he2e, e2he); - - // Build refl projection and embedding - std::vector proj; - std::vector embed; - build_refl_proj(m, he2e, e2he, proj, embed); - - // Build overlay mesh from mesh m - Mesh m_l = change_mesh_type(m); - - // Convert mesh Penner coordinates to a halfedge length array l for m - int num_halfedges = he2e.size(); - for (int h = 0; h < num_halfedges; ++h) { - m_l.l[h] = OverlayScalar(exp(reduced_metric_coords[proj[he2e[h]]] / 2.0)); - } - - OverlayMesh mo(m_l); - - return mo; -} - -template -void make_tufted_overlay(OverlayMesh& mo) -{ - auto& m = mo._m; - if (m.type[0] == 0) return; // nothing to do for closed mesh - - int n_ind_v = m.n_ind_vertices(); - int n_he = m.n_halfedges(); - - // Modify the to and out arrays to identify dependent vertices with their reflection - m.out = std::vector(n_ind_v); - for (int i = 0; i < n_he; ++i) - { - m.out[m.v_rep[m.to[i]]] = i; - m.to[i] = m.v_rep[m.to[i]]; - } - m.v_rep = range(0, n_ind_v); -} bool check_areas(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F) { @@ -849,18 +798,6 @@ void view_halfedge_mesh_type( #endif } -void check_angles(const Mesh& m) -{ - VectorX he2angle, he2cot; - corner_angles(m, he2angle, he2cot); - VectorX vertex_angles(m.n_vertices()); - for (int h = 0; h < m.n_halfedges(); ++h) { - int v = m.to[h]; - vertex_angles[v] += he2angle[m.n[m.n[h]]] / (M_PI / 2.); - } - spdlog::info("Vertex angles: {}", vertex_angles.transpose()); -} - template std::tuple< Eigen::MatrixXi, @@ -1440,8 +1377,6 @@ void compute_layout_faces( #endif -template void make_tufted_overlay(OverlayMesh& mo); -template OverlayMesh add_overlay(const Mesh& m, const VectorX& reduced_metric_coords); template std:: tuple< @@ -1497,9 +1432,6 @@ std::vector pullback_cut_to_overlay( #ifdef WITH_MPFR #ifndef MULTIPRECISION -template void make_tufted_overlay(OverlayMesh& mo); -template OverlayMesh add_overlay(const Mesh& m, const VectorX& reduced_metric_coords); - template std:: tuple< diff --git a/src/parametrization/parametrize.cpp b/src/parametrization/parametrize.cpp index ddc6fda..397f7b8 100644 --- a/src/parametrization/parametrize.cpp +++ b/src/parametrization/parametrize.cpp @@ -8,14 +8,12 @@ #include "parametrization/parametrize.h" -#include "conformal_ideal_delaunay/ConformalInterface.hh" #include "util/boundary.h" -#include "util/embedding.h" -#include "util/vector.h" +#include "metric/projection.h" #include "parametrization/interpolation.h" #include "parametrization/layout.h" -#include "metric/projection.h" -#include "parametrization/translation.h" +#include "util/vector.h" +#include "conformal_ideal_delaunay/ConformalInterface.hh" /// FIXME Do cleaning pass @@ -23,21 +21,6 @@ namespace Penner { -/* -std::vector find_boundary_vertices(const Mesh& m) -{ - std::vector is_bd(m.n_ind_vertices(), false); - for (int i = 0; i < m.n_halfedges(); i++) { - if ((m.type[i] == 1) && (m.type[m.opp[i]] == 2)) - { - is_bd[m.v_rep[m.to[i]]] = true; - } - } - - return is_bd; -} -*/ - template std:: tuple< diff --git a/src/parametrization/refinement.cpp b/src/parametrization/refinement.cpp index 8aafe79..5b386b3 100644 --- a/src/parametrization/refinement.cpp +++ b/src/parametrization/refinement.cpp @@ -7,23 +7,20 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "parametrization/refinement.h" -#include #include #include -#include #include #include -#include -#include #include + #include #include -#include "metric/area.h" + #include "conformal_ideal_delaunay/Halfedge.hh" #include "util/io.h" -#include "parametrization/triangulation.h" #include "util/vector.h" #include "util/vf_mesh.h" +#include "parametrization/triangulation.h" #if ENABLE_VISUALIZATION #include "polyscope/point_cloud.h" diff --git a/src/parametrization/translation.cpp b/src/parametrization/translation.cpp index 120503f..e0db8ea 100644 --- a/src/parametrization/translation.cpp +++ b/src/parametrization/translation.cpp @@ -8,13 +8,9 @@ #include "parametrization/translation.h" -#include -#include -#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" +#include "util/vector.h" #include "util/embedding.h" #include "util/linear_algebra.h" -#include "util/vector.h" -#include "metric/reparametrization.h" #include "metric/shear.h" /// FIXME Do cleaning pass diff --git a/src/parametrization/triangulation.cpp b/src/parametrization/triangulation.cpp index cbb3690..740364d 100644 --- a/src/parametrization/triangulation.cpp +++ b/src/parametrization/triangulation.cpp @@ -8,22 +8,11 @@ #include "parametrization/triangulation.h" -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include "metric/area.h" + #include "util/io.h" -#include "parametrization/refinement.h" +#include "metric/area.h" #include "metric/constraint.h" -#include "util/vector.h" #if ENABLE_VISUALIZATION #include "polyscope/point_cloud.h" diff --git a/src/util/boundary.cpp b/src/util/boundary.cpp index b23ab68..a7ad8bf 100644 --- a/src/util/boundary.cpp +++ b/src/util/boundary.cpp @@ -9,6 +9,7 @@ #include "util/boundary.h" #include "util/vector.h" +#include "util/map.h" namespace Penner { diff --git a/src/util/io.cpp b/src/util/io.cpp index 87931dd..495ff31 100644 --- a/src/util/io.cpp +++ b/src/util/io.cpp @@ -14,6 +14,7 @@ #include #include "util/vector.h" +#include "util/embedding.h" namespace Penner { diff --git a/src/util/map.cpp b/src/util/map.cpp index ebd47af..15e5d37 100644 --- a/src/util/map.cpp +++ b/src/util/map.cpp @@ -9,6 +9,7 @@ #include "util/map.h" #include "util/vector.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" #include #include diff --git a/src/util/vf_mesh.cpp b/src/util/vf_mesh.cpp index 119df38..2d5c21d 100644 --- a/src/util/vf_mesh.cpp +++ b/src/util/vf_mesh.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include namespace Penner { @@ -267,6 +269,15 @@ Eigen::MatrixXd subset_vertices( return V_subset; } +bool is_manifold(const Eigen::MatrixXi& F) +{ + Eigen::VectorXi B; + if (!igl::is_edge_manifold(F)) return false; + if (!igl::is_vertex_manifold(F, B)) return false; + + return true; +} + std::tuple reindex_mesh( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, From c9b48dfa18c272e18e9706cadb3191a2bff5bb08 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Sun, 28 Jun 2026 23:08:11 -0400 Subject: [PATCH 03/17] Changed name of barycentric code --- include/metric/metric/{reparametrization.h => barycentric.h} | 0 src/CMakeLists.txt | 2 +- src/metric/{reparametrization.cpp => barycentric.cpp} | 2 +- src/metric/shear.cpp | 2 +- src/parametrization/interpolation.cpp | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename include/metric/metric/{reparametrization.h => barycentric.h} (100%) rename src/metric/{reparametrization.cpp => barycentric.cpp} (98%) diff --git a/include/metric/metric/reparametrization.h b/include/metric/metric/barycentric.h similarity index 100% rename from include/metric/metric/reparametrization.h rename to include/metric/metric/barycentric.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e22cbc1..3e11882 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,7 +17,7 @@ set(MetricSrc metric/constraint.cpp metric/flip_matrix_generator.cpp metric/projection.cpp - metric/reparametrization.cpp + metric/barycentric.cpp metric/shear.cpp metric/viewer.cpp ) diff --git a/src/metric/reparametrization.cpp b/src/metric/barycentric.cpp similarity index 98% rename from src/metric/reparametrization.cpp rename to src/metric/barycentric.cpp index d3da646..0dacadd 100644 --- a/src/metric/reparametrization.cpp +++ b/src/metric/barycentric.cpp @@ -6,7 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "metric/reparametrization.h" +#include "metric/barycentric.h" #include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" diff --git a/src/metric/shear.cpp b/src/metric/shear.cpp index b496c32..a2d634f 100644 --- a/src/metric/shear.cpp +++ b/src/metric/shear.cpp @@ -11,7 +11,7 @@ #include "util/vector.h" #include "util/embedding.h" #include "metric/projection.h" -#include "metric/reparametrization.h" +#include "metric/barycentric.h" #include "util/vector.h" /// FIXME Do cleaning pass diff --git a/src/parametrization/interpolation.cpp b/src/parametrization/interpolation.cpp index 3cd0daa..0f3a954 100644 --- a/src/parametrization/interpolation.cpp +++ b/src/parametrization/interpolation.cpp @@ -11,7 +11,7 @@ #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "util/vector.h" #include "metric/projection.h" -#include "metric/reparametrization.h" +#include "metric/barycentric.h" #include "parametrization/translation.h" /// FIXME Do cleaning pass From 9d735c18a5db4ba53fc8f44da2f7405411f81b26 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Sun, 28 Jun 2026 23:43:42 -0400 Subject: [PATCH 04/17] Removed global parameters and cleaned optimization includes --- include/metric/metric/cone_metric.h | 20 ++++++++++- include/metric/metric/globals.h | 34 ------------------- .../metric_optimization/energies.h | 2 -- .../metric_optimization/energy_functor.h | 3 -- .../metric_optimization/energy_weights.h | 2 -- .../explicit_optimization.h | 2 -- .../implicit_optimization.h | 4 --- src/metric/projection.cpp | 1 - .../metric_optimization/energies.cpp | 13 ++++--- .../metric_optimization/energy_functor.cpp | 9 +++-- .../metric_optimization/energy_weights.cpp | 8 +---- .../explicit_optimization.cpp | 9 +++-- .../implicit_optimization.cpp | 10 +++--- 13 files changed, 42 insertions(+), 75 deletions(-) delete mode 100644 include/metric/metric/globals.h diff --git a/include/metric/metric/cone_metric.h b/include/metric/metric/cone_metric.h index 51b86d2..7d95dbd 100644 --- a/include/metric/metric/cone_metric.h +++ b/include/metric/metric/cone_metric.h @@ -12,7 +12,6 @@ #include "util/common.h" #include "util/embedding.h" -#include "metric/globals.h" #include "metric/flip_matrix_generator.h" #include "conformal_ideal_delaunay/OverlayMesh.hh" #include "conformal_ideal_delaunay/globals.hh" @@ -27,6 +26,25 @@ namespace Penner { +// Parameters to pass to the conformal method for projecting to the constraint. +// More detail on these parameters can be found in the documentation for that +// method. +struct ProjectionParameters +{ + int max_itr = 100; // maximum number of iterations + Scalar bound_norm_thres = 1e-10; // line search threshold for dropping the gradient norm bound +#ifdef MULTIPRECISION + Scalar error_eps = 1e-24; // minimum error termination condition +#else + Scalar error_eps = 1e-8; // minimum error termination condition +#endif + bool do_reduction = + true; // reduce the initial line step if the range of coordinate values is large + bool initial_ptolemy = true; // initial_ptolemy: use ptolemy flips for the initial make_delaunay + bool use_edge_flips = true; // use intrinsic edge flips + std::string output_dir = ""; +}; + /// Representation of a differentiable intrinsic metric on a mesh with angle constraints at cones. /// /// This abstract representation supports coordinates for the space of intrinsic metrics on a fixed diff --git a/include/metric/metric/globals.h b/include/metric/metric/globals.h deleted file mode 100644 index 71454a8..0000000 --- a/include/metric/metric/globals.h +++ /dev/null @@ -1,34 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#pragma once - -#include "util/common.h" - -namespace Penner { - -// Parameters to pass to the conformal method for projecting to the constraint. -// More detail on these parameters can be found in the documentation for that -// method. -struct ProjectionParameters -{ - int max_itr = 100; // maximum number of iterations - Scalar bound_norm_thres = 1e-10; // line search threshold for dropping the gradient norm bound -#ifdef MULTIPRECISION - Scalar error_eps = 1e-24; // minimum error termination condition -#else - Scalar error_eps = 1e-8; // minimum error termination condition -#endif - bool do_reduction = - true; // reduce the initial line step if the range of coordinate values is large - bool initial_ptolemy = true; // initial_ptolemy: use ptolemy flips for the initial make_delaunay - bool use_edge_flips = true; // use intrinsic edge flips - std::string output_dir = ""; -}; - -} // namespace Penner \ No newline at end of file diff --git a/include/optimization/optimization/metric_optimization/energies.h b/include/optimization/optimization/metric_optimization/energies.h index b0dff5e..9018719 100644 --- a/include/optimization/optimization/metric_optimization/energies.h +++ b/include/optimization/optimization/metric_optimization/energies.h @@ -9,8 +9,6 @@ #pragma once #include "metric/cone_metric.h" -#include "conformal_ideal_delaunay/OverlayMesh.hh" -#include "util/embedding.h" /** \file energies.h * diff --git a/include/optimization/optimization/metric_optimization/energy_functor.h b/include/optimization/optimization/metric_optimization/energy_functor.h index 0c49446..e16c5a4 100644 --- a/include/optimization/optimization/metric_optimization/energy_functor.h +++ b/include/optimization/optimization/metric_optimization/energy_functor.h @@ -8,11 +8,8 @@ #pragma once -#include "util/embedding.h" #include "util/linear_algebra.h" #include "metric/cone_metric.h" -#include "conformal_ideal_delaunay/OverlayMesh.hh" - /** * @brief Class for differentiable distortion measures supporting evaluation, gradients, and Hessians diff --git a/include/optimization/optimization/metric_optimization/energy_weights.h b/include/optimization/optimization/metric_optimization/energy_weights.h index 69aa61f..c687657 100644 --- a/include/optimization/optimization/metric_optimization/energy_weights.h +++ b/include/optimization/optimization/metric_optimization/energy_weights.h @@ -9,8 +9,6 @@ #pragma once #include "metric/cone_metric.h" -#include "conformal_ideal_delaunay/OverlayMesh.hh" -#include "util/embedding.h" /// \file energy_functor.h /// diff --git a/include/optimization/optimization/metric_optimization/explicit_optimization.h b/include/optimization/optimization/metric_optimization/explicit_optimization.h index d43d24b..9e97ce8 100644 --- a/include/optimization/optimization/metric_optimization/explicit_optimization.h +++ b/include/optimization/optimization/metric_optimization/explicit_optimization.h @@ -8,8 +8,6 @@ #pragma once -#include -#include "util/embedding.h" #include "optimization/metric_optimization/energy_functor.h" #include "optimization/metric_optimization/implicit_optimization.h" diff --git a/include/optimization/optimization/metric_optimization/implicit_optimization.h b/include/optimization/optimization/metric_optimization/implicit_optimization.h index cb75e08..ae53009 100644 --- a/include/optimization/optimization/metric_optimization/implicit_optimization.h +++ b/include/optimization/optimization/metric_optimization/implicit_optimization.h @@ -8,10 +8,6 @@ #pragma once -#include -#include "metric/cone_metric.h" -#include "conformal_ideal_delaunay/OverlayMesh.hh" -#include "util/embedding.h" #include "optimization/metric_optimization/energy_functor.h" /** diff --git a/src/metric/projection.cpp b/src/metric/projection.cpp index 3fbc4fb..5338928 100644 --- a/src/metric/projection.cpp +++ b/src/metric/projection.cpp @@ -12,7 +12,6 @@ #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "util/linear_algebra.h" #include "metric/constraint.h" -#include "metric/globals.h" /// FIXME Do cleaning pass diff --git a/src/optimization/metric_optimization/energies.cpp b/src/optimization/metric_optimization/energies.cpp index fc519ad..744e439 100644 --- a/src/optimization/metric_optimization/energies.cpp +++ b/src/optimization/metric_optimization/energies.cpp @@ -8,14 +8,19 @@ #include "optimization/metric_optimization/energies.h" -#include -#include -#include -#include "metric/area.h" #include "metric/cone_metric.h" +#include "metric/area.h" + +// corner angles #include "metric/constraint.h" + +// conformal scaling matrix #include "metric/projection.h" +#include +#include +#include + /// FIXME Do cleaning pass namespace Penner { diff --git a/src/optimization/metric_optimization/energy_functor.cpp b/src/optimization/metric_optimization/energy_functor.cpp index 22b7891..5f36847 100644 --- a/src/optimization/metric_optimization/energy_functor.cpp +++ b/src/optimization/metric_optimization/energy_functor.cpp @@ -8,13 +8,12 @@ #include "optimization/metric_optimization/energy_functor.h" -#include -#include -#include -#include "metric/area.h" -#include "metric/constraint.h" +#include "util/linear_algebra.h" + #include "optimization/metric_optimization/energies.h" #include "optimization/metric_optimization/energy_weights.h" + +// best fit conformal #include "metric/projection.h" /// FIXME Do cleaning pass diff --git a/src/optimization/metric_optimization/energy_weights.cpp b/src/optimization/metric_optimization/energy_weights.cpp index 8e74dbb..c15317c 100644 --- a/src/optimization/metric_optimization/energy_weights.cpp +++ b/src/optimization/metric_optimization/energy_weights.cpp @@ -9,14 +9,8 @@ #include "optimization/metric_optimization/energy_functor.h" #include "util/vector.h" -#include "metric/area.h" -#include "metric/constraint.h" -#include "optimization/metric_optimization/energies.h" -#include "metric/projection.h" -#include -#include -#include +#include "metric/area.h" /// FIXME Do cleaning pass diff --git a/src/optimization/metric_optimization/explicit_optimization.cpp b/src/optimization/metric_optimization/explicit_optimization.cpp index 15293ff..1f7e379 100644 --- a/src/optimization/metric_optimization/explicit_optimization.cpp +++ b/src/optimization/metric_optimization/explicit_optimization.cpp @@ -8,14 +8,13 @@ #include "optimization/metric_optimization/explicit_optimization.h" -#include "metric/constraint.h" -#include "optimization/metric_optimization/energies.h" -#include "metric/globals.h" #include "util/io.h" +#include "util/linear_algebra.h" +#include "util/vector.h" + +#include "metric/shear.h" #include "optimization/metric_optimization/nonlinear_optimization.h" #include "metric/projection.h" -#include "metric/shear.h" -#include "util/vector.h" /// FIXME Do cleaning pass diff --git a/src/optimization/metric_optimization/implicit_optimization.cpp b/src/optimization/metric_optimization/implicit_optimization.cpp index 3b63f4b..0fe4c8f 100644 --- a/src/optimization/metric_optimization/implicit_optimization.cpp +++ b/src/optimization/metric_optimization/implicit_optimization.cpp @@ -8,14 +8,14 @@ #include "optimization/metric_optimization/implicit_optimization.h" -#include -#include "metric/area.h" +#include "util/io.h" + #include "metric/constraint.h" #include "optimization/metric_optimization/energies.h" -#include "metric/globals.h" -#include "util/io.h" -#include "optimization/metric_optimization/nonlinear_optimization.h" #include "metric/projection.h" +#include "optimization/metric_optimization/nonlinear_optimization.h" + +#include /// FIXME Do cleaning pass From d5af287bc25c13d5a9492cf493e06a37f74afae0 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 29 Jun 2026 00:05:39 -0400 Subject: [PATCH 05/17] Moved quality and quad code to utils --- include/{holonomy/holonomy/core => metric/metric}/quality.h | 2 +- include/{feature/feature/core => util/util}/quads.h | 4 +--- src/CMakeLists.txt | 4 +++- src/feature/CMakeLists.txt | 1 - src/feature/core/viewer.cpp | 2 +- src/holonomy/CMakeLists.txt | 1 - src/holonomy/interface.cpp | 2 +- src/holonomy/pybind.cpp | 2 +- src/{holonomy/core => metric}/quality.cpp | 2 +- src/{feature/core => util}/quads.cpp | 4 +--- 10 files changed, 10 insertions(+), 14 deletions(-) rename include/{holonomy/holonomy/core => metric/metric}/quality.h (97%) rename include/{feature/feature/core => util/util}/quads.h (90%) rename src/{holonomy/core => metric}/quality.cpp (98%) rename src/{feature/core => util}/quads.cpp (93%) diff --git a/include/holonomy/holonomy/core/quality.h b/include/metric/metric/quality.h similarity index 97% rename from include/holonomy/holonomy/core/quality.h rename to include/metric/metric/quality.h index b9a35b6..18f7ef2 100644 --- a/include/holonomy/holonomy/core/quality.h +++ b/include/metric/metric/quality.h @@ -6,7 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "holonomy/core/common.h" +#include "metric/cone_metric.h" /** * @brief Methods to compute the quality of triangles in a mesh. diff --git a/include/feature/feature/core/quads.h b/include/util/util/quads.h similarity index 90% rename from include/feature/feature/core/quads.h rename to include/util/util/quads.h index 13b0c6d..3cf5cfc 100644 --- a/include/feature/feature/core/quads.h +++ b/include/util/util/quads.h @@ -9,7 +9,7 @@ #pragma once -#include "feature/core/common.h" +#include "util/common.h" /** * @brief Methods to compute relevant quad mesh statistics, including degree valences. @@ -17,7 +17,6 @@ */ namespace Penner { -namespace Feature { /** * @brief Compute the vertex valences of a given (tri or quad) mesh @@ -27,5 +26,4 @@ namespace Feature { */ std::vector compute_valences(const Eigen::MatrixXi& F); -} // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3e11882..e83a1c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(UtilSrc util/io.cpp util/linear_algebra.cpp util/map.cpp + util/quads.cpp util/spanning_tree.cpp util/union_find.cpp util/vector.cpp @@ -13,11 +14,12 @@ set(UtilSrc set(MetricSrc metric/area.cpp + metric/barycentric.cpp metric/cone_metric.cpp metric/constraint.cpp metric/flip_matrix_generator.cpp metric/projection.cpp - metric/barycentric.cpp + metric/quality.cpp metric/shear.cpp metric/viewer.cpp ) diff --git a/src/feature/CMakeLists.txt b/src/feature/CMakeLists.txt index da596bf..a94f597 100644 --- a/src/feature/CMakeLists.txt +++ b/src/feature/CMakeLists.txt @@ -4,7 +4,6 @@ set(FeatureCoreSrc core/component_mesh.cpp core/io.cpp core/union_meshes.cpp - core/quads.cpp core/viewer.cpp core/vf_corners.cpp ) diff --git a/src/feature/core/viewer.cpp b/src/feature/core/viewer.cpp index 1f6109e..a0d805f 100644 --- a/src/feature/core/viewer.cpp +++ b/src/feature/core/viewer.cpp @@ -29,7 +29,7 @@ #include "feature/feature/error.h" // quad valences -#include "feature/core/quads.h" +#include "util/quads.h" #include #include diff --git a/src/holonomy/CMakeLists.txt b/src/holonomy/CMakeLists.txt index cf8055c..fab7751 100644 --- a/src/holonomy/CMakeLists.txt +++ b/src/holonomy/CMakeLists.txt @@ -5,7 +5,6 @@ set(HolonomyCoreSrc core/dual_loop.cpp core/dual_segment.cpp core/homology_basis.cpp - core/quality.cpp core/viewer.cpp ) diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index caa83cf..d173166 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -11,7 +11,7 @@ #include "holonomy/core/boundary_basis.h" #include "holonomy/core/homology_basis.h" #include "field/intrinsic_field.h" -#include "holonomy/core/quality.h" +#include "metric/quality.h" #include "holonomy/holonomy/cones.h" #include "holonomy/holonomy/holonomy.h" #include "holonomy/holonomy/rotation_form.h" diff --git a/src/holonomy/pybind.cpp b/src/holonomy/pybind.cpp index 68823ba..48ff9cf 100644 --- a/src/holonomy/pybind.cpp +++ b/src/holonomy/pybind.cpp @@ -9,7 +9,7 @@ #include "holonomy/pybind.h" #include "holonomy/interface.h" -#include "holonomy/core/quality.h" +#include "metric/quality.h" #include "holonomy/holonomy/cones.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/holonomy/newton.h" diff --git a/src/holonomy/core/quality.cpp b/src/metric/quality.cpp similarity index 98% rename from src/holonomy/core/quality.cpp rename to src/metric/quality.cpp index 73a2b37..c217ede 100644 --- a/src/holonomy/core/quality.cpp +++ b/src/metric/quality.cpp @@ -6,7 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "holonomy/core/quality.h" +#include "metric/quality.h" namespace Penner { namespace Holonomy { diff --git a/src/feature/core/quads.cpp b/src/util/quads.cpp similarity index 93% rename from src/feature/core/quads.cpp rename to src/util/quads.cpp index 127bfee..3747517 100644 --- a/src/feature/core/quads.cpp +++ b/src/util/quads.cpp @@ -6,11 +6,10 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "feature/core/quads.h" +#include "util/quads.h" namespace Penner { -namespace Feature { std::vector compute_valences(const Eigen::MatrixXi& F) { @@ -44,5 +43,4 @@ std::vector compute_valences(const Eigen::MatrixXi& F) return valences; } -} // namespace Feature } // namespace Penner \ No newline at end of file From fd8a435a3cd042bc40ad61d11098ffbac2960ee5 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 29 Jun 2026 00:58:07 -0400 Subject: [PATCH 06/17] Moved cones and rotation_form to field --- .../holonomy/holonomy => field/field}/cones.h | 31 +--- .../holonomy => field/field}/rotation_form.h | 5 +- .../holonomy/holonomy/holonomy/constraint.h | 16 ++ include/holonomy/holonomy/interface.h | 8 +- include/util/util/boundary.h | 3 + src/feature/core/component_mesh.cpp | 7 +- src/feature/feature/gluing.cpp | 4 +- src/feature/surgery/cut_metric_generator.cpp | 13 +- src/field/CMakeLists.txt | 2 + src/{holonomy/holonomy => field}/cones.cpp | 137 +----------------- .../holonomy => field}/rotation_form.cpp | 6 +- src/holonomy/CMakeLists.txt | 2 - src/holonomy/holonomy/constraint.cpp | 54 +++++++ src/holonomy/interface.cpp | 81 +++++++++-- src/holonomy/pybind.cpp | 10 +- src/util/boundary.cpp | 24 +++ 16 files changed, 204 insertions(+), 199 deletions(-) rename include/{holonomy/holonomy/holonomy => field/field}/cones.h (66%) rename include/{holonomy/holonomy/holonomy => field/field}/rotation_form.h (95%) rename src/{holonomy/holonomy => field}/cones.cpp (68%) rename src/{holonomy/holonomy => field}/rotation_form.cpp (97%) diff --git a/include/holonomy/holonomy/holonomy/cones.h b/include/field/field/cones.h similarity index 66% rename from include/holonomy/holonomy/holonomy/cones.h rename to include/field/field/cones.h index 44b89a5..fbb56f5 100644 --- a/include/holonomy/holonomy/holonomy/cones.h +++ b/include/field/field/cones.h @@ -8,7 +8,7 @@ #pragma once -#include "holonomy/holonomy/marked_penner_cone_metric.h" +#include "metric/cone_metric.h" /** * @brief Methods to compute cones on a surface from field data, as well as various @@ -17,7 +17,7 @@ */ namespace Penner { -namespace Holonomy { +namespace Field { /** * @brief Compute the cones from a rotation form on an intrinsic mesh. @@ -54,22 +54,6 @@ std::vector generate_cones_from_rotation_form( */ bool is_trivial_torus(const Mesh& m); -/** - * @brief Check if the cones are valid for seamless holonomy constraints - * - * Checks both for invalid cones that cannot be satisfied independently (i.e., a negative - * or zero cone) and for cones that cannot be satisfied with seamless holonomy constraints - * (i.e., a torus with a pair of cones). - * - * WARNING: Don't check for trivial torus constraints, but must be accounted for by removing - * holonomy constraints as the trivial torus only supports trivial topology. - * - * @param Th_hat: per-vertex cone angles - * @return true if the cones are valid for seamless holonomy constraints - * @return false otherwise - */ -bool validate_cones(const Mesh& m); - /** * @brief Given target cone angles, fix any problems that prevent them from being valid * for seamless holonomy constraints. @@ -81,16 +65,9 @@ void fix_cones(Mesh& m, int min_cone_index=1); void add_random_cone_pair(Mesh& m, bool only_interior=true, int offset=0); -std::tuple get_constraint_outliers( - MarkedPennerConeMetric& marked_metric, - bool use_interior_vertices=true, - bool use_flat_vertices=true); -std::tuple add_optimal_cone_pair(MarkedPennerConeMetric& marked_metric); - -void make_interior_free(Mesh& m); std::pair count_cones(const Mesh& m); -bool is_interior(const Mesh& m, int vi); -} // namespace Holonomy + +} // namespace Field } // namespace Penner \ No newline at end of file diff --git a/include/holonomy/holonomy/holonomy/rotation_form.h b/include/field/field/rotation_form.h similarity index 95% rename from include/holonomy/holonomy/holonomy/rotation_form.h rename to include/field/field/rotation_form.h index 621cb13..0a874b0 100644 --- a/include/holonomy/holonomy/holonomy/rotation_form.h +++ b/include/field/field/rotation_form.h @@ -8,7 +8,6 @@ #pragma once -#include "holonomy/core/common.h" #include "field/intrinsic_field.h" /** @@ -21,7 +20,7 @@ */ namespace Penner { -namespace Holonomy { +namespace Field { /** * @brief Given a mesh with a per-face frame field, @@ -48,5 +47,5 @@ VectorX generate_intrinsic_rotation_form( const Eigen::MatrixXd& V, const Field::FieldParameters& field_params); -} // namespace Holonomy +} // namespace Field } // namespace Penner \ No newline at end of file diff --git a/include/holonomy/holonomy/holonomy/constraint.h b/include/holonomy/holonomy/holonomy/constraint.h index 343d9eb..1e6cdc9 100644 --- a/include/holonomy/holonomy/holonomy/constraint.h +++ b/include/holonomy/holonomy/holonomy/constraint.h @@ -158,5 +158,21 @@ MatrixX compute_metric_corner_angle_jacobian( std::tuple compute_metric_constraint_with_jacobian_pybind( const MarkedPennerConeMetric& marked_metric); +/** + * @brief Check if the cones are valid for seamless holonomy constraints + * + * Checks both for invalid cones that cannot be satisfied independently (i.e., a negative + * or zero cone) and for cones that cannot be satisfied with seamless holonomy constraints + * (i.e., a torus with a pair of cones). + * + * WARNING: Don't check for trivial torus constraints, but must be accounted for by removing + * holonomy constraints as the trivial torus only supports trivial topology. + * + * @param Th_hat: per-vertex cone angles + * @return true if the cones are valid for seamless holonomy constraints + * @return false otherwise + */ +bool validate_cones(const Mesh& m); + } // namespace Holonomy } // namespace Penner \ No newline at end of file diff --git a/include/holonomy/holonomy/interface.h b/include/holonomy/holonomy/interface.h index 5eeed38..83c28c4 100644 --- a/include/holonomy/holonomy/interface.h +++ b/include/holonomy/holonomy/interface.h @@ -10,7 +10,7 @@ #include "holonomy/core/common.h" #include "field/field.h" -#include "holonomy/holonomy/rotation_form.h" +#include "field/rotation_form.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" @@ -242,5 +242,11 @@ std::vector compute_kappa( DiscreteMetric generate_discrete_metric(const Mesh& m); +std::tuple get_constraint_outliers( + MarkedPennerConeMetric& marked_metric, + bool use_interior_vertices=true, + bool use_flat_vertices=true); +std::tuple add_optimal_cone_pair(MarkedPennerConeMetric& marked_metric); + } // namespace Holonomy } // namespace Penner \ No newline at end of file diff --git a/include/util/util/boundary.h b/include/util/util/boundary.h index f24bbaa..b1e8c0d 100644 --- a/include/util/util/boundary.h +++ b/include/util/util/boundary.h @@ -81,4 +81,7 @@ std::vector find_boundary_components(const Mesh& m); */ std::vector build_boundary_component(const Mesh& m, int halfedge_index); +bool is_interior(const Mesh& m, int vi); +void make_interior_free(Mesh& m); + } // namespace Penner \ No newline at end of file diff --git a/src/feature/core/component_mesh.cpp b/src/feature/core/component_mesh.cpp index 1a12928..3d9fac1 100644 --- a/src/feature/core/component_mesh.cpp +++ b/src/feature/core/component_mesh.cpp @@ -9,6 +9,7 @@ #include "feature/core/component_mesh.h" +#include "util/boundary.h" #include "util/vector.h" #include "util/union_find.h" @@ -16,7 +17,7 @@ #include "feature/core/union_meshes.h" // check for interior vertices -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include #include @@ -482,12 +483,12 @@ void add_component_cone_pair(Mesh& m, Eigen::VectorXi vertex_component, // check if vertex is valid int Vi = m.v_rep[vi]; if (m.Th_hat[Vi] < 2. * angle_delta) continue; - if ((only_interior) && (!Holonomy::is_interior(m, vi))) continue; + if ((only_interior) && (!is_interior(m, vi))) continue; // check if adjacent vertex is valid int vj = m.to[m.out[vi]]; int Vj = m.v_rep[vj]; - if ((only_interior) && (!Holonomy::is_interior(m, vj))) continue; + if ((only_interior) && (!is_interior(m, vj))) continue; // add cones spdlog::debug("Adding negative cone at {} with angle {}", Vi, m.Th_hat[Vi]); diff --git a/src/feature/feature/gluing.cpp b/src/feature/feature/gluing.cpp index 9388bd9..ac8af53 100644 --- a/src/feature/feature/gluing.cpp +++ b/src/feature/feature/gluing.cpp @@ -10,7 +10,7 @@ #include "feature/feature/gluing.h" #include "feature/core/vf_corners.h" -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" namespace Penner { namespace Feature { @@ -107,7 +107,7 @@ std::pair count_glued_cones( ) { // if mesh is not cut, use regular cone counting method bool is_symmetric = (m.type[0] != 0); - if (!is_symmetric) return Holonomy::count_cones(m); + if (!is_symmetric) return Field::count_cones(m); // get glued cone angles std::vector Th_hat = compute_glued_angles(m, vtx_reindex, V_map); diff --git a/src/feature/surgery/cut_metric_generator.cpp b/src/feature/surgery/cut_metric_generator.cpp index 069ae9e..f8af85b 100644 --- a/src/feature/surgery/cut_metric_generator.cpp +++ b/src/feature/surgery/cut_metric_generator.cpp @@ -8,6 +8,7 @@ #include "feature/surgery/cut_metric_generator.h" +#include "util/boundary.h" #include "field/intrinsic_field.h" #include "feature/core/vf_corners.h" #include "feature/core/union_meshes.h" @@ -21,7 +22,7 @@ #include "feature/feature/gluing.h" // fix invalid or poor cones -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include "feature/dirichlet/cone_perturber.h" #include @@ -79,7 +80,7 @@ CutMetricGenerator::CutMetricGenerator( m.fixed_dof = std::vector(m.n_ind_vertices(), false); bool fixed_dof = false; for (int vi = 0; vi < m.n_vertices(); ++vi) { - if (Holonomy::is_interior(m, vi)) { + if (is_interior(m, vi)) { m.fixed_dof[m.v_rep[vi]] = true; fixed_dof = true; break; @@ -257,7 +258,7 @@ void CutMetricGenerator::generate_fields( rotation_forms.push_back(field_generator.compute_rotation_form(m)); field_generator.get_field(m, vtx_reindex, F_cut, face_map, reference_corner, theta, kappa, period_jump); field_generator.get_fixed_faces(m, face_map, is_fixed_face); - m.Th_hat = Holonomy::generate_cones_from_rotation_form(m, rotation_forms.back()); + m.Th_hat = Field::generate_cones_from_rotation_form(m, rotation_forms.back()); // check Guass Bonnet GaussBonnetCheck(m); @@ -298,7 +299,7 @@ void CutMetricGenerator::set_fields( // extract the rotation form and cone angles rotation_forms.push_back(field_generator.compute_rotation_form(m)); - m.Th_hat = Holonomy::generate_cones_from_rotation_form(m, rotation_forms.back()); + m.Th_hat = Field::generate_cones_from_rotation_form(m, rotation_forms.back()); // check Guass Bonnet GaussBonnetCheck(m); @@ -415,8 +416,8 @@ void fix_cones( spdlog::info("Resolving cone pair with split"); int num_vertices = dirichlet_metric.n_ind_vertices(); - add_random_cone_pair(dirichlet_metric, true, num_vertices / 3); - add_random_cone_pair(dirichlet_metric, true, 2 * num_vertices / 3); + Field::add_random_cone_pair(dirichlet_metric, true, num_vertices / 3); + Field::add_random_cone_pair(dirichlet_metric, true, 2 * num_vertices / 3); std::tie(num_neg_cones, num_pos_cones) = count_glued_cones(dirichlet_metric, vtx_reindex, V_map); } diff --git a/src/field/CMakeLists.txt b/src/field/CMakeLists.txt index da75bfa..6c06efe 100644 --- a/src/field/CMakeLists.txt +++ b/src/field/CMakeLists.txt @@ -1,10 +1,12 @@ set(FieldSrc field.cpp frame_field.cpp + cones.cpp cross_field.cpp facet_field.cpp forms.cpp intrinsic_field.cpp + rotation_form.cpp ) add_library(PennerFieldLib diff --git a/src/holonomy/holonomy/cones.cpp b/src/field/cones.cpp similarity index 68% rename from src/holonomy/holonomy/cones.cpp rename to src/field/cones.cpp index 6892be5..8617c86 100644 --- a/src/holonomy/holonomy/cones.cpp +++ b/src/field/cones.cpp @@ -6,7 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include "util/boundary.h" #include "util/vector.h" @@ -17,66 +17,11 @@ // check valid one forms #include "field/forms.h" -// holonomy computation -#include "holonomy/holonomy/holonomy.h" - #include namespace Penner { -namespace Holonomy { - -// Check cones computed from rotation form match more direct vertex iteration computation -bool validate_cones_from_rotation_form( - const Mesh& m, - const VectorX& rotation_form, - const std::vector& Th_hat) -{ - // Compute the corner angles - VectorX he2angle, he2cot; - corner_angles(m, he2angle, he2cot); +namespace Field { - // Get boundary vertices if symmetric mesh with boundary - int num_vertices = m.n_vertices(); - bool is_symmetric = (m.type[0] != 0); - std::vector is_boundary_vertex(num_vertices, false); - if (is_symmetric) { - std::vector boundary_vertices = find_boundary_vertices(m); - convert_index_vector_to_boolean_array( - boundary_vertices, - num_vertices, - is_boundary_vertex); - } - - // Compare cones with direct per-vertex computation - for (int vi = 0; vi < num_vertices; ++vi) { - DualLoopList dual_loop(build_counterclockwise_vertex_dual_segment_sequence(m, vi)); - Scalar rotation = compute_dual_loop_rotation(m, rotation_form, dual_loop); - Scalar holonomy = compute_dual_loop_holonomy(m, he2angle, dual_loop); - - // Special treatment for vertices in interior of doubled mesh - if ((is_symmetric) && (!is_boundary_vertex[vi])) { - if (!float_equal(Th_hat[m.v_rep[vi]] / 2., holonomy - rotation, 1e-3)) { - spdlog::warn( - "Inconsistent interior cones {} and {}", - Th_hat[m.v_rep[vi]] / 2., - holonomy - rotation); - return false; - } - } - // General case - else { - if (!float_equal(Th_hat[m.v_rep[vi]], holonomy - rotation, 1e-3)) { - spdlog::warn( - "Inconsistent cones {} and {}", - Th_hat[m.v_rep[vi]], - holonomy - rotation); - return false; - } - } - } - - return true; -} std::vector generate_cones_from_rotation_form( const Mesh& m, @@ -100,7 +45,6 @@ std::vector generate_cones_from_rotation_form( // the vertex Th_hat[m.v_rep[m.to[h]]] += rotation_form[h]; } - assert(validate_cones_from_rotation_form(m, rotation_form, Th_hat)); for (int vi = 0; vi < num_vertices; ++vi) { Th_hat[vi] = round(Th_hat[vi] / (M_PI / 2.)) * (M_PI / 2.); @@ -282,20 +226,6 @@ void remove_minimum_cone(Mesh& m) *max_cone -= angle_delta; } -bool is_interior(const Mesh& m, int vi) -{ - int h_start = m.out[vi]; - int hij = h_start; - do { - int hji = m.opp[hij]; - if ((m.type[hij] == 1) && (m.type[hji] == 2)) return false; - if ((m.type[hij] == 2) && (m.type[hji] == 1)) return false; - - hij = m.n[m.opp[hij]]; - } while (hij != h_start); - - return true; -} int get_flat_vertex(const Mesh& m, bool only_interior) { @@ -372,59 +302,6 @@ void add_random_cone_pair(Mesh& m, bool only_interior, int offset) } } -std::tuple get_constraint_outliers( - MarkedPennerConeMetric& marked_metric, - bool use_interior_vertices, - bool use_flat_vertices) -{ - bool is_symmetric = (marked_metric.type[0] != 0); - int num_vertices = marked_metric.n_vertices(); - int num_ind_vertices = marked_metric.n_ind_vertices(); - std::vector bd_vertices = find_boundary_vertices(marked_metric); - std::vector is_bd_vertex(num_ind_vertices, false); - for (int vi : bd_vertices) { - is_bd_vertex[marked_metric.v_rep[vi]] = true; - } - - // get constraint errors - VectorX constraint; - MatrixX J_constraint; - bool need_jacobian = false; - bool only_free_vertices = false; - marked_metric.constraint(constraint, J_constraint, need_jacobian, only_free_vertices); - - // get cone indices with minimum and maximum defect - int i = 0; - int j = 0; - Scalar flat_angle = (is_symmetric) ? 4. * M_PI : 2. * M_PI; - for (int k = 0; k < num_vertices; ++k) { - int vi = marked_metric.v_rep[i]; - int vj = marked_metric.v_rep[j]; - int vk = marked_metric.v_rep[k]; - - // only add (optionally) interior cone pairs at flat vertices - if ((use_interior_vertices) && (is_symmetric) && (is_bd_vertex[vk])) continue; - if ((use_flat_vertices) && (!float_equal(marked_metric.Th_hat[vk], flat_angle))) continue; - - if (constraint[vk] < constraint[vi]) i = k; - if (constraint[vk] > constraint[vj]) j = k; - } - - return std::make_tuple(i, j); -} - -std::tuple add_optimal_cone_pair(MarkedPennerConeMetric& marked_metric) -{ - auto [i, j] = get_constraint_outliers(marked_metric, true, true); - spdlog::debug("Adding positive cone at {}", i); - spdlog::debug("Adding negative cone at {}", j); - bool is_symmetric = (marked_metric.type[0] != 0); - Scalar angle_delta = (is_symmetric) ? M_PI : (M_PI / 2.); - marked_metric.Th_hat[marked_metric.v_rep[i]] += angle_delta; - marked_metric.Th_hat[marked_metric.v_rep[j]] -= angle_delta; - - return std::make_tuple(i, j); -} void fix_cones(Mesh& m, int min_cone_index) { @@ -476,14 +353,6 @@ void remove_trivial_boundaries( } } -void make_interior_free(Mesh& m) -{ - m.fixed_dof = std::vector(m.n_ind_vertices(), true); - auto bd_vertices = find_boundary_vertices(m); - for (int vi : bd_vertices) { - m.fixed_dof[m.v_rep[vi]] = false; - } -} -} // namespace Holonomy +} // namespace Field } // namespace Penner \ No newline at end of file diff --git a/src/holonomy/holonomy/rotation_form.cpp b/src/field/rotation_form.cpp similarity index 97% rename from src/holonomy/holonomy/rotation_form.cpp rename to src/field/rotation_form.cpp index f08cacb..57dacc4 100644 --- a/src/holonomy/holonomy/rotation_form.cpp +++ b/src/field/rotation_form.cpp @@ -6,7 +6,7 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. -#include "holonomy/holonomy/rotation_form.h" +#include "field/rotation_form.h" #include "util/linear_algebra.h" #include "field/field.h" @@ -16,7 +16,7 @@ #include namespace Penner { -namespace Holonomy { +namespace Field { // Priority function for halfedges // Assumes that vertex indices are unique @@ -136,5 +136,5 @@ VectorX generate_intrinsic_rotation_form( return field_generator.run_with_viewer(m, vtx_reindex, V); } -} // namespace Holonomy +} // namespace Field } // namespace Penner \ No newline at end of file diff --git a/src/holonomy/CMakeLists.txt b/src/holonomy/CMakeLists.txt index fab7751..74da394 100644 --- a/src/holonomy/CMakeLists.txt +++ b/src/holonomy/CMakeLists.txt @@ -9,12 +9,10 @@ set(HolonomyCoreSrc ) set(HolonomySrc - holonomy/cones.cpp holonomy/constraint.cpp holonomy/holonomy.cpp holonomy/marked_penner_cone_metric.cpp holonomy/newton.cpp - holonomy/rotation_form.cpp ) set(SimilaritySrc diff --git a/src/holonomy/holonomy/constraint.cpp b/src/holonomy/holonomy/constraint.cpp index d925304..7afac14 100644 --- a/src/holonomy/holonomy/constraint.cpp +++ b/src/holonomy/holonomy/constraint.cpp @@ -8,6 +8,7 @@ #include "holonomy/holonomy/constraint.h" +#include "util/boundary.h" #include "util/vector.h" #include "util/linear_algebra.h" #include "metric/constraint.h" @@ -649,5 +650,58 @@ std::tuple compute_metric_constraint_with_jacobian_pybind( return std::make_tuple(constraint, J_constraint); } +// Check cones computed from rotation form match more direct vertex iteration computation +bool validate_cones_from_rotation_form( + const Mesh& m, + const VectorX& rotation_form, + const std::vector& Th_hat) +{ + // Compute the corner angles + VectorX he2angle, he2cot; + corner_angles(m, he2angle, he2cot); + + // Get boundary vertices if symmetric mesh with boundary + int num_vertices = m.n_vertices(); + bool is_symmetric = (m.type[0] != 0); + std::vector is_boundary_vertex(num_vertices, false); + if (is_symmetric) { + std::vector boundary_vertices = find_boundary_vertices(m); + convert_index_vector_to_boolean_array( + boundary_vertices, + num_vertices, + is_boundary_vertex); + } + + // Compare cones with direct per-vertex computation + for (int vi = 0; vi < num_vertices; ++vi) { + DualLoopList dual_loop(build_counterclockwise_vertex_dual_segment_sequence(m, vi)); + Scalar rotation = compute_dual_loop_rotation(m, rotation_form, dual_loop); + Scalar holonomy = compute_dual_loop_holonomy(m, he2angle, dual_loop); + + // Special treatment for vertices in interior of doubled mesh + if ((is_symmetric) && (!is_boundary_vertex[vi])) { + if (!float_equal(Th_hat[m.v_rep[vi]] / 2., holonomy - rotation, 1e-3)) { + spdlog::warn( + "Inconsistent interior cones {} and {}", + Th_hat[m.v_rep[vi]] / 2., + holonomy - rotation); + return false; + } + } + // General case + else { + if (!float_equal(Th_hat[m.v_rep[vi]], holonomy - rotation, 1e-3)) { + spdlog::warn( + "Inconsistent cones {} and {}", + Th_hat[m.v_rep[vi]], + holonomy - rotation); + return false; + } + } + } + + return true; +} + } // namespace Holonomy } // namespace Penner \ No newline at end of file diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index d173166..da1f4e6 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -12,9 +12,10 @@ #include "holonomy/core/homology_basis.h" #include "field/intrinsic_field.h" #include "metric/quality.h" -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include "holonomy/holonomy/holonomy.h" -#include "holonomy/holonomy/rotation_form.h" +#include "holonomy/holonomy/constraint.h" +#include "field/rotation_form.h" #include "holonomy/similarity/energy.h" #include "util/boundary.h" @@ -74,7 +75,7 @@ std::tuple> generate_marked_metric( // Check for invalid cones if (!validate_cones(m)) { spdlog::info("Fixing invalid cones"); - fix_cones(m); + Field::fix_cones(m); } // Use halfedge mesh method @@ -126,10 +127,10 @@ generate_metric_from_field( // build cones bool has_boundary = bnd_loops.size() >= 1; std::vector Th_hat = - Holonomy::generate_cones_from_rotation_form(m, vtx_reindex, rotation_form, has_boundary); + Field::generate_cones_from_rotation_form(m, vtx_reindex, rotation_form, has_boundary); // build marked metric - m.Th_hat = Holonomy::generate_cones_from_rotation_form(m, rotation_form); + m.Th_hat = Field::generate_cones_from_rotation_form(m, rotation_form); MarkedPennerConeMetric marked_metric = generate_marked_metric_from_mesh(m, rotation_form, marked_metric_params); return std::make_tuple(marked_metric, vtx_reindex, rotation_form, Th_hat); @@ -197,13 +198,13 @@ void generate_basis_loops( // (optionally) generate dual loops on the surface // If the mesh is a trivial torus, don't add constraints int num_basis_loops = 0; - if (is_trivial_torus(m)) + if (Field::is_trivial_torus(m)) { spdlog::warn("Trivial torus seen"); } if (marked_metric_params.remove_loop_constraints) return; - if ((marked_metric_params.remove_trivial_torus) && (is_trivial_torus(m))) return; - if (is_trivial_torus(m)) + if ((marked_metric_params.remove_trivial_torus) && (Field::is_trivial_torus(m))) return; + if (Field::is_trivial_torus(m)) { spdlog::warn("Adding constraints for trivial torus"); } @@ -457,11 +458,11 @@ infer_marked_metric( rotation_form = generate_intrinsic_rotation_form(m, field_params); } else { rotation_form = - generate_rotation_form_from_cross_field(m, vtx_reindex_mesh, V, F, frame_field); + Field::generate_rotation_form_from_cross_field(m, vtx_reindex_mesh, V, F, frame_field); } bool has_boundary = bnd_loops.size() >= 1; std::vector Th_hat = - generate_cones_from_rotation_form(m, vtx_reindex_mesh, rotation_form, has_boundary); + Field::generate_cones_from_rotation_form(m, vtx_reindex_mesh, rotation_form, has_boundary); // Generate marked mesh auto [marked_metric, vtx_reindex] = @@ -501,7 +502,7 @@ std::tuple> generate_intrinsic_rotation_form( // generate cones from the rotation form bool has_bd = (cone_metric->type[0] != 0); - Th_hat = generate_cones_from_rotation_form(*cone_metric, vtx_reindex, rotation_form, has_bd); + Th_hat = Field::generate_cones_from_rotation_form(*cone_metric, vtx_reindex, rotation_form, has_bd); return std::make_tuple(rotation_form, Th_hat); } @@ -573,13 +574,13 @@ std::tuple> generate_refine // Get rotation form and corresponding cones Field::FieldParameters field_params; VectorX rotation_form = generate_intrinsic_rotation_form(m, field_params); - std::vector Th_hat = generate_cones_from_rotation_form(m, rotation_form); + std::vector Th_hat = Field::generate_cones_from_rotation_form(m, rotation_form); m.Th_hat = Th_hat; // Check for invalid cones if (!validate_cones(m)) { spdlog::info("Fixing invalid cones"); - fix_cones(m); + Field::fix_cones(m); } // Set cones and check Guass Bonnet @@ -719,5 +720,59 @@ void optimize_triangle_quality(MarkedPennerConeMetric& marked_metric, double max } } +std::tuple get_constraint_outliers( + MarkedPennerConeMetric& marked_metric, + bool use_interior_vertices, + bool use_flat_vertices) +{ + bool is_symmetric = (marked_metric.type[0] != 0); + int num_vertices = marked_metric.n_vertices(); + int num_ind_vertices = marked_metric.n_ind_vertices(); + std::vector bd_vertices = find_boundary_vertices(marked_metric); + std::vector is_bd_vertex(num_ind_vertices, false); + for (int vi : bd_vertices) { + is_bd_vertex[marked_metric.v_rep[vi]] = true; + } + + // get constraint errors + VectorX constraint; + MatrixX J_constraint; + bool need_jacobian = false; + bool only_free_vertices = false; + marked_metric.constraint(constraint, J_constraint, need_jacobian, only_free_vertices); + + // get cone indices with minimum and maximum defect + int i = 0; + int j = 0; + Scalar flat_angle = (is_symmetric) ? 4. * M_PI : 2. * M_PI; + for (int k = 0; k < num_vertices; ++k) { + int vi = marked_metric.v_rep[i]; + int vj = marked_metric.v_rep[j]; + int vk = marked_metric.v_rep[k]; + + // only add (optionally) interior cone pairs at flat vertices + if ((use_interior_vertices) && (is_symmetric) && (is_bd_vertex[vk])) continue; + if ((use_flat_vertices) && (!float_equal(marked_metric.Th_hat[vk], flat_angle))) continue; + + if (constraint[vk] < constraint[vi]) i = k; + if (constraint[vk] > constraint[vj]) j = k; + } + + return std::make_tuple(i, j); +} + +std::tuple add_optimal_cone_pair(MarkedPennerConeMetric& marked_metric) +{ + auto [i, j] = get_constraint_outliers(marked_metric, true, true); + spdlog::debug("Adding positive cone at {}", i); + spdlog::debug("Adding negative cone at {}", j); + bool is_symmetric = (marked_metric.type[0] != 0); + Scalar angle_delta = (is_symmetric) ? M_PI : (M_PI / 2.); + marked_metric.Th_hat[marked_metric.v_rep[i]] += angle_delta; + marked_metric.Th_hat[marked_metric.v_rep[j]] -= angle_delta; + + return std::make_tuple(i, j); +} + } // namespace Holonomy } // namespace Penner \ No newline at end of file diff --git a/src/holonomy/pybind.cpp b/src/holonomy/pybind.cpp index 48ff9cf..6ddfcf6 100644 --- a/src/holonomy/pybind.cpp +++ b/src/holonomy/pybind.cpp @@ -10,11 +10,11 @@ #include "holonomy/interface.h" #include "metric/quality.h" -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/holonomy/newton.h" #include "holonomy/holonomy/constraint.h" -#include "holonomy/holonomy/rotation_form.h" +#include "field/rotation_form.h" #include "field/intrinsic_field.h" #include "field/frame_field.h" #include "holonomy/similarity/conformal.h" @@ -143,8 +143,8 @@ void init_holonomy_pybind(pybind11::module& m) m.def("compute_mesh_quality", &compute_mesh_quality, default_call_guard); m.def("compute_min_angle", &compute_min_angle, default_call_guard); - m.def("fix_cones", &fix_cones, default_call_guard); - m.def("add_random_cone_pair", &add_random_cone_pair, default_call_guard); + m.def("fix_cones", &Field::fix_cones, default_call_guard); + m.def("add_random_cone_pair", &Field::add_random_cone_pair, default_call_guard); m.def("add_optimal_cone_pair", &add_optimal_cone_pair, default_call_guard); m.def( "find_boundary_vertices", @@ -187,7 +187,7 @@ void init_holonomy_pybind(pybind11::module& m) const Mesh&, const std::vector&, const VectorX&, - bool>(&generate_cones_from_rotation_form), default_call_guard); + bool>(&Field::generate_cones_from_rotation_form), default_call_guard); m.def( "generate_VF_mesh_from_similarity_metric", diff --git a/src/util/boundary.cpp b/src/util/boundary.cpp index a7ad8bf..f56f908 100644 --- a/src/util/boundary.cpp +++ b/src/util/boundary.cpp @@ -147,4 +147,28 @@ std::vector find_boundary_components(const Mesh& m) return boundary_components; } +bool is_interior(const Mesh& m, int vi) +{ + int h_start = m.out[vi]; + int hij = h_start; + do { + int hji = m.opp[hij]; + if ((m.type[hij] == 1) && (m.type[hji] == 2)) return false; + if ((m.type[hij] == 2) && (m.type[hji] == 1)) return false; + + hij = m.n[m.opp[hij]]; + } while (hij != h_start); + + return true; +} + +void make_interior_free(Mesh& m) +{ + m.fixed_dof = std::vector(m.n_ind_vertices(), true); + auto bd_vertices = find_boundary_vertices(m); + for (int vi : bd_vertices) { + m.fixed_dof[m.v_rep[vi]] = false; + } +} + } // namespace Penner \ No newline at end of file From b09d339ef51f58f45bd44eb0b187cfd72697a264 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 29 Jun 2026 16:30:45 -0400 Subject: [PATCH 07/17] Restructured field and form code --- include/field/field/comiso.h | 3 - include/field/field/cones.h | 17 +- include/field/field/cross_field.h | 60 +++++- include/field/field/field.h | 36 ---- include/field/field/frame_field.h | 135 ------------- include/field/field/intrinsic_field.h | 6 - include/field/field/vector_field.h | 129 ++++++++++++ include/holonomy/holonomy/interface.h | 2 +- src/feature/core/viewer.cpp | 4 +- src/field/CMakeLists.txt | 2 +- src/field/comiso.h | 180 ----------------- src/field/cones.cpp | 36 ---- src/field/cross_field.cpp | 117 +++++++++++ src/field/field.cpp | 66 ------- src/field/frame_field.cpp | 214 +------------------- src/field/intrinsic_field.cpp | 118 +---------- src/field/rotation_form.cpp | 2 +- src/field/vector_field.cpp | 269 ++++++++++++++++++++++++++ src/holonomy/core/viewer.cpp | 3 +- src/holonomy/interface.cpp | 3 +- 20 files changed, 600 insertions(+), 802 deletions(-) delete mode 100644 include/field/field/field.h create mode 100644 include/field/field/vector_field.h delete mode 100644 src/field/comiso.h delete mode 100644 src/field/field.cpp create mode 100644 src/field/vector_field.cpp diff --git a/include/field/field/comiso.h b/include/field/field/comiso.h index b533924..85dc4ab 100644 --- a/include/field/field/comiso.h +++ b/include/field/field/comiso.h @@ -26,9 +26,6 @@ namespace Penner { namespace Field { - - -// TODO Make option and make cone rounder code public class ConeMISolver : public COMISO::MISolver { public: diff --git a/include/field/field/cones.h b/include/field/field/cones.h index fbb56f5..a785842 100644 --- a/include/field/field/cones.h +++ b/include/field/field/cones.h @@ -63,9 +63,24 @@ bool is_trivial_torus(const Mesh& m); */ void fix_cones(Mesh& m, int min_cone_index=1); +/** + * @brief Add positive and negative curvature cones to constraints. + * + * WARNING: this method places cones arbitrarily and may produce difficult constraints + * + * @param m: mesh with cone constraints + * @param only_interior: only add cone constraints to interior if possible + * @param offset: approximate vertex index to place cones near + */ void add_random_cone_pair(Mesh& m, bool only_interior=true, int offset=0); - +/** + * @brief Count the number of positive and negative curvature cones. + * + * @param m: mesh with cone metric + * @return number of negative curvature cones (cone angle above 2 Pi) + * @return number of postiive curvature cones (cone angle below 2 Pi) + */ std::pair count_cones(const Mesh& m); diff --git a/include/field/field/cross_field.h b/include/field/field/cross_field.h index d0755a3..fdcadb4 100644 --- a/include/field/field/cross_field.h +++ b/include/field/field/cross_field.h @@ -12,8 +12,8 @@ #include "util/common.h" /** - * @brief Methods for generating cross fields as represented by four rotationally symmetric - * tangent vectors. + * @brief Methods for generating cross fields as represented by four tangent vectors + * without explicit encoding of period jumps across edges. * */ @@ -45,6 +45,50 @@ void write_cross_field( const Eigen::MatrixXd& reference_field, const Eigen::VectorXd& theta); +/** + * @brief Load a rosy field from file. + * + * The format is + * ``` + * + * 4 + * + * ... + * ``` + * where d is a representative direction on the face. The remaining directions can be inferred + * by rotational symmetry. + * + * @param input_filename: file location of the rosy field + * @return per-face representative field direction + */ +Eigen::MatrixXd load_rosy_field(const std::string& input_filename); + +/** + * @brief Write a rotationally symmetric rosy field to file. + * + * The format is + * ``` + * + * 4 + * + * ... + * ``` + * where d is a representative direction on the face. The remaining directions can be inferred + * by rotational symmetry. + * + * @param output_filename: file location to serialize the frame field + * @param V: mesh vertices + * @param F: mesh faces + * @param reference_field: per-face reference tangent direction matrix + * @param theta: offset angles of a representative cross field direction relative to the reference + */ +void write_rosy_field( + const std::string& output_filename, + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta); + /** * @brief Given a representative cross field direction defined by a reference direction and an * offset angle, generate all four cross field direction matrices. @@ -78,5 +122,17 @@ std::array reduce_curl( const std::array& cross_field, const std::vector& fixed_faces); +/** + * @brief Generate a rosy field for a mesh + * + * @param V: mesh vertices + * @param F: mesh faces + * @return |F|x3 frame field of per-face field direction vectors + * @return per-vertex cone angles corresponding to the frame field + */ +std::tuple> generate_rosy_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F); + } // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/include/field/field/field.h b/include/field/field/field.h deleted file mode 100644 index 5e08c2e..0000000 --- a/include/field/field/field.h +++ /dev/null @@ -1,36 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - - -#pragma once - -#include "util/common.h" - -/** - * @brief Methods to generate field directions and cones from an nrosy field. - * - */ - -namespace Penner { -namespace Field { - -/** - * @brief Generate a cross field for a mesh - * - * @param V: mesh vertices - * @param F: mesh faces - * @return |F|x3 frame field of per-face field direction vectors - * @return per-vertex cone angles corresponding to the frame field - */ -std::tuple> generate_cross_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F); - - -} // namespace Field -} // namespace Penner \ No newline at end of file diff --git a/include/field/field/frame_field.h b/include/field/field/frame_field.h index 41e2acd..8ec7ff5 100644 --- a/include/field/field/frame_field.h +++ b/include/field/field/frame_field.h @@ -21,85 +21,6 @@ namespace Penner { namespace Field { -/** - * @brief Generate the reference tangent direction of a face along the oriented edge opposite - * the corner with local index {0, 1, 2}. - * - * @param V: mesh vertices - * @param F: mesh faces - * @param fijk: face index - * @param local_index: local index of the corner opposite the reference direction - * @return tangent direction along the edge opposite the corner - */ -Eigen::Vector3d generate_reference_direction( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int fijk, - int local_index); - -/** - * @brief Generate a field of per-face reference directions. - * - * The chosen direction for a face fijk is the directed edge eki. - * - * @param V: mesh vertices - * @param F: mesh faces - * @return per-face tangent direction matrix - */ -Eigen::MatrixXd generate_reference_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F); - -/** - * @brief Generate a field of per-face reference directions determined by opposite reference corners. - * - * @param V: mesh vertices - * @param F: mesh faces - * @param reference_corner: per-face local index of corners opposite the reference direciton - * @return per-face tangent direction matrix - */ -Eigen::MatrixXd generate_reference_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::VectorXi& reference_corner); - -/** - * @brief Given a representative cross field direction defined by a reference direction and an - * offset angle, generate the representative direction matrix - * - * @param V: mesh vertices - * @param F: mesh faces - * @param reference_field: per-face reference tangent direction matrix - * @param theta: offset angles of a representative cross field direction relative to the reference - * @return per-face representative direction matrix - */ -Eigen::MatrixXd generate_frame_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& theta); - -/** - * @brief Compute salient geometry aligned field directions for a mesh. - * - * The parabolic anisotropy used for the relative threshold is ||k2| - |k1|| / max(|k1|, |k2|) - * This measurement is near 0 for parabolic regions and near 1 for highly anisotropic regions - * - * @param V: mesh vertices - * @param F: mesh faces - * @param radius: (optional) vertex radius for fitting a smooth surface for field estimation - * @param abs_threshold: (optional) minimum threshold for mean anisotropy of principal curvatures - * @param rel_threshold: (optional) minimum threshold for parabolic anisotropy of principal curvatures - * @return |F|x3 matrix of per face directions - * @return per face mask indicating whether a direction is salient or not - */ -std::tuple> compute_field_direction( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int radius=5, - Scalar abs_threshold=1., - Scalar rel_threshold=0.9, - Scalar sample_rate=1.); /** * @brief Optimize a cross field on a mesh. @@ -143,30 +64,6 @@ void write_frame_field( const Eigen::MatrixXd& kappa, const Eigen::MatrixXi& period_jump); -/** - * @brief Write a rosy field to file. - * - * The format is - * ``` - * - * 4 - * - * ... - * ``` - * where d is a representative direction on the face. - * - * @param output_filename: file location to serialize the frame field - * @param V: mesh vertices - * @param F: mesh faces - * @param reference_field: per-face reference tangent direction matrix - * @param theta: offset angles of a representative cross field direction relative to the reference - */ -void write_rosy_field( - const std::string& output_filename, - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& theta); /** * @brief Write a combed frame field to file. @@ -207,23 +104,6 @@ comb_frame_field( std::tuple load_combed_field(const std::string& ffield_file); -/** - * @brief Load a rosy field from file. - * - * The format is - * ``` - * - * 4 - * - * ... - * ``` - * where d is a representative direction on the face. - * - * @param input_filename: file location of the rosy field - * @return per-face representative field direction - */ -Eigen::MatrixXd load_rosy_field(const std::string& input_filename); - /** * @brief Load a frame field from file. * @@ -245,21 +125,6 @@ Eigen::MatrixXd load_rosy_field(const std::string& input_filename); std::tuple load_frame_field(const std::string& output_filename); -/** - * @brief Infer the angle offset of a direction field relative to a reference field - * - * @param V: mesh vertices` - * @param F: mesh faces - * @param reference_corner: per-face reference corner opposite the reference direction - * @param direction_field: per-face tangent direction matrix - * @return offset angles of the direction field relative to the reference - */ -Eigen::VectorXd infer_theta( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::VectorXi& reference_corner, - const Eigen::MatrixXd& direction_field); - /** * @brief Extend a frame field defined on a base mesh to a refined mesh. * diff --git a/include/field/field/intrinsic_field.h b/include/field/field/intrinsic_field.h index 9652d9b..812fbef 100644 --- a/include/field/field/intrinsic_field.h +++ b/include/field/field/intrinsic_field.h @@ -40,12 +40,6 @@ struct FieldParameters Scalar abs_anisotropy=0.2; }; -std::tuple -compute_facet_principal_curvature( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int radius=5); - class IntrinsicNRosyField { public: diff --git a/include/field/field/vector_field.h b/include/field/field/vector_field.h new file mode 100644 index 0000000..f1c885c --- /dev/null +++ b/include/field/field/vector_field.h @@ -0,0 +1,129 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + + +#pragma once + +#include "util/common.h" + +/** + * @brief Methods to generate vector field directions on a surface, representated either as + * a 3D vector or as an angle relative to a reference edge. + * + * Also includes principal curvature vector field computation. + * + */ + +namespace Penner { +namespace Field { + +/** + * @brief Generate the reference tangent direction of a face along the oriented edge opposite + * the corner with local index {0, 1, 2}. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param fijk: face index + * @param local_index: local index of the corner opposite the reference direction + * @return tangent direction along the edge opposite the corner + */ +Eigen::Vector3d generate_reference_direction( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int fijk, + int local_index); + +/** + * @brief Generate a field of per-face reference directions. + * + * The chosen direction for a face fijk is the directed edge eki. + * + * @param V: mesh vertices + * @param F: mesh faces + * @return per-face tangent direction matrix + */ +Eigen::MatrixXd generate_reference_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F); + +/** + * @brief Generate a field of per-face reference directions determined by opposite reference corners. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param reference_corner: per-face local index of corners opposite the reference direciton + * @return per-face tangent direction matrix + */ +Eigen::MatrixXd generate_reference_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXi& reference_corner); + +/** + * @brief Given a representative vector field direction defined by a reference direction and an + * offset angle, generate the representative direction matrix + * + * @param V: mesh vertices + * @param F: mesh faces + * @param reference_field: per-face reference tangent direction matrix + * @param theta: offset angles of a representative cross field direction relative to the reference + * @return per-face representative direction matrix + */ +Eigen::MatrixXd generate_vector_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta); + +/** + * @brief Infer the angle offset of a direction field relative to a reference field + * + * @param V: mesh vertices` + * @param F: mesh faces + * @param reference_corner: per-face reference corner opposite the reference direction + * @param direction_field: per-face tangent direction matrix + * @return offset angles of the direction field relative to the reference + */ +Eigen::VectorXd infer_theta( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXi& reference_corner, + const Eigen::MatrixXd& direction_field); + +std::tuple +compute_facet_principal_curvature( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int radius=5); + +/** + * @brief Compute salient geometry aligned field directions for a mesh. + * + * The parabolic anisotropy used for the relative threshold is ||k2| - |k1|| / max(|k1|, |k2|) + * This measurement is near 0 for parabolic regions and near 1 for highly anisotropic regions + * + * @param V: mesh vertices + * @param F: mesh faces + * @param radius: (optional) vertex radius for fitting a smooth surface for field estimation + * @param abs_threshold: (optional) minimum threshold for mean anisotropy of principal curvatures + * @param rel_threshold: (optional) minimum threshold for parabolic anisotropy of principal curvatures + * @return |F|x3 matrix of per face directions + * @return per face mask indicating whether a direction is salient or not + */ +std::tuple> compute_field_direction( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int radius=5, + Scalar abs_threshold=1., + Scalar rel_threshold=0.9, + Scalar sample_rate=1.); + + + +} // namespace Field +} // namespace Penner \ No newline at end of file diff --git a/include/holonomy/holonomy/interface.h b/include/holonomy/holonomy/interface.h index 83c28c4..bc54316 100644 --- a/include/holonomy/holonomy/interface.h +++ b/include/holonomy/holonomy/interface.h @@ -9,7 +9,7 @@ #pragma once #include "holonomy/core/common.h" -#include "field/field.h" +#include "field/vector_field.h" #include "field/rotation_form.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" diff --git a/src/feature/core/viewer.cpp b/src/feature/core/viewer.cpp index a0d805f..0377ebb 100644 --- a/src/feature/core/viewer.cpp +++ b/src/feature/core/viewer.cpp @@ -202,7 +202,7 @@ void view_cross_field( } // generate frame field geometry from cross field - Eigen::MatrixXd frame_field = Field::generate_frame_field(V, F, reference_field, theta); + Eigen::MatrixXd frame_field = Field::generate_vector_field(V, F, reference_field, theta); int num_faces = F.rows(); // transfer kappa and period jump to viewer halfedge indexing @@ -566,7 +566,7 @@ void view_feature_cross_field( } // generate frame field geometry from cross field - Eigen::MatrixXd frame_field = Field::generate_frame_field(V, F, reference_field, theta); + Eigen::MatrixXd frame_field = Field::generate_vector_field(V, F, reference_field, theta); int num_faces = F.rows(); // transfer kappa and period jump to viewer halfedge indexing diff --git a/src/field/CMakeLists.txt b/src/field/CMakeLists.txt index 6c06efe..9278b67 100644 --- a/src/field/CMakeLists.txt +++ b/src/field/CMakeLists.txt @@ -1,5 +1,4 @@ set(FieldSrc - field.cpp frame_field.cpp cones.cpp cross_field.cpp @@ -7,6 +6,7 @@ set(FieldSrc forms.cpp intrinsic_field.cpp rotation_form.cpp + vector_field.cpp ) add_library(PennerFieldLib diff --git a/src/field/comiso.h b/src/field/comiso.h deleted file mode 100644 index aa35cca..0000000 --- a/src/field/comiso.h +++ /dev/null @@ -1,180 +0,0 @@ -#include "field/rounder.h" - -#include "field/rounder.h" - -namespace Penner { -namespace Field { - -// WARNING: CoMISo uses a more restrictive copyleft license -#if USE_COMISO -#include -#include -#include - - -// TODO Make option and make cone rounder code public -class ConeMISolver : public COMISO::MISolver -{ -public: - ConeMISolver() {}; - void solve_cone_rounding( - CSCMatrix& _A, - Vecd& _x, - Vecd& _rhs, - Veci& _to_round, - Rounder& rounder) { - // StopWatch - COMISO::StopWatch sw; - double time_search_next_integer = 0; - - // some statistics - n_local_ = 0; - n_cg_ = 0; - n_full_ = 0; - - // reset cholmod step flag - cholmod_step_done_ = false; - - Veci to_round(_to_round); - // copy to round vector and make it unique - std::sort(to_round.begin(), to_round.end()); - Veci::iterator last_unique; - last_unique = std::unique(to_round.begin(), to_round.end()); - int r = last_unique - to_round.begin(); - to_round.resize( r); - - // initalize old indices - Veci old_idx(_rhs.size()); - for(unsigned int i=0; i 2) std::cerr << "initial full solution" << std::endl; - direct_solver_.calc_system_gmm(_A); - direct_solver_.solve(_x, _rhs); - - cholmod_step_done_ = true; - - ++n_full_; - } - - // neighbors for local optimization - Vecui neigh_i; - - // Vector for reduced solution - Vecd xr(_x); - - // loop until solution computed - for(unsigned int i=0; i 0) - { - std::cerr << "Integer DOF's left: " << to_round.size()-(i+1) << " "; - if( noisy_ > 1) - std::cerr << "residuum_norm: " << COMISO_GMM::residuum_norm( _A, xr, _rhs) << std::endl; - } - - // position in round vector - std::vector tr_best; - - sw.start(); - - RoundingSet rset; - rset.set_threshold(multiple_rounding_threshold_); - - // find index yielding smallest rounding error - for(unsigned int j=0; j 5) - std::cerr << "round " << tr_best.size() << " variables simultaneously\n"; - - // clear neigh for local update - neigh_i.clear(); - - for(unsigned int j = 0; j 2) std::cerr << "final full solution" << std::endl; - - if( gmm::mat_ncols( _A) > 0) - { - if(cholmod_step_done_) - direct_solver_.update_system_gmm(_A); - else - direct_solver_.calc_system_gmm(_A); - - direct_solver_.solve( xr, _rhs); - ++n_full_; - } - } - - // store solution values to result vector - for(unsigned int i=0; i& m, bool only_interior, int offset) } } - void fix_cones(Mesh& m, int min_cone_index) { // Remove any zero cones @@ -318,41 +317,6 @@ void fix_cones(Mesh& m, int min_cone_index) } -// TODO May be worth supporting -void remove_trivial_boundaries( - const Mesh& m, - const std::vector& vtx_reindex, - std::vector& Th_hat) -{ - std::vector boundary_components = find_boundary_components(m); - for (int h_start : boundary_components) { - spdlog::info("Checking for trivial loop at {}", h_start); - bool is_trivial = true; - int h = h_start; - do { - // Circulate to next boundary edge - while (m.type[h] != 2) { - h = m.opp[m.n[h]]; - } - h = m.opp[h]; - - int vi = vtx_reindex[m.v_rep[m.to[h]]]; - if (!float_equal(Th_hat[vi], M_PI)) { - is_trivial = false; - break; - } - } while (h != h_start); - - if (is_trivial) { - spdlog::info("Adjusting trivial loop at {}", h); - int vi = vtx_reindex[m.v_rep[m.to[h]]]; - int vj = vtx_reindex[m.v_rep[m.to[m.opp[h]]]]; - Th_hat[vi] += M_PI / 2.; - Th_hat[vj] -= M_PI / 2.; - } - } -} - } // namespace Field } // namespace Penner \ No newline at end of file diff --git a/src/field/cross_field.cpp b/src/field/cross_field.cpp index b21091d..0788640 100644 --- a/src/field/cross_field.cpp +++ b/src/field/cross_field.cpp @@ -8,6 +8,14 @@ #include "field/cross_field.h" +#include "util/vf_mesh.h" +#include "field/vector_field.h" + +#if USE_COMISO +#include +//#include +#endif + #include #include #include @@ -100,6 +108,73 @@ void write_cross_field( field_file.close(); } +Eigen::MatrixXd load_rosy_field(const std::string& input_filename) +{ + // Open file + spdlog::debug("opening field at {}", input_filename); + std::ifstream input_file(input_filename); + if (!input_file) return {}; + + // get number of faces + std::string line; + std::getline(input_file, line); + std::istringstream iss(line); + int num_faces; + iss >> num_faces; + spdlog::debug("{} faces", num_faces); + std::getline(input_file, line); // skip start line + + // initialize vectors + Eigen::MatrixXd frame_field(num_faces, 3); + + // Read file one face at a time + int f = 0; + while ((f < num_faces) && (std::getline(input_file, line))) { + std::istringstream iss(line); + for (int i : {0 , 1, 2}) + { + iss >> frame_field(f, i); + } + + ++f; + } + + if (num_faces != f) + { + spdlog::error("Number of faces inconsistent with number of lines"); + } + + // Close file + input_file.close(); + return frame_field; +} + +void write_rosy_field( + const std::string& output_filename, + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta) +{ + Eigen::MatrixXd rosy_field = generate_vector_field(V, F, reference_field, theta); + + std::ofstream field_file(output_filename, std::ios::out | std::ios::trunc); + field_file << F.rows() << std::endl; + field_file << "4" << std::endl; + for (int f = 0; f < F.rows(); ++f) + { + for (int j : {0 , 1, 2}) + { + field_file << std::fixed << std::setprecision(17) << rosy_field(f, j) << " "; + } + + field_file << std::endl; + } + + field_file.close(); +} + + std::array generate_cross_field( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, @@ -233,5 +308,47 @@ std::array reduce_curl( return opt_cross_field; } +std::tuple> generate_cross_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F) +{ + // Compute cross field and singularities from comiso + Eigen::MatrixXd frame_field; + Eigen::VectorXd S; +#if USE_COMISO + Eigen::VectorXi b(1); + Eigen::MatrixXd bc(1, 3); + b << 0; + bc << 1, 1, 1; + int degree = 4; + igl::copyleft::comiso::nrosy(V, F, b, bc, degree, frame_field, S); +#else + int num_vertices = V.rows(); + int num_faces = F.rows(); + spdlog::error( + "Comiso solver not enabled for #V={}, #F={} mesh. Set USE_COMISO to use.", + num_vertices, + num_faces); +#endif + + // Get the boundary vertices + std::vector is_boundary_vertex = compute_boundary_vertices(F, V.rows()); + + // Turn singularities into a flat metric + // FIXME This is only accurate for closed meshes; singularities only make sense with doubling + int num_cone_vertices = S.size(); + std::vector Th_hat(num_cone_vertices); + for (int vi = 0; vi < num_cone_vertices; ++vi) { + if (is_boundary_vertex[vi]) { + Th_hat[vi] = M_PI - (2 * M_PI * S[vi]); + } else { + Th_hat[vi] = 2 * M_PI * (1 - S[vi]); + } + } + + return std::make_tuple(frame_field, Th_hat); +} + + } } // namespace Penner \ No newline at end of file diff --git a/src/field/field.cpp b/src/field/field.cpp deleted file mode 100644 index 84aec09..0000000 --- a/src/field/field.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// This file is part of penner-optimization, a constrained parametrization library. -// -// Copyright (C) 2026 Ryan Capouellez -// -// This Source Code Form is subject to the terms of the Mozilla Public License -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at http://mozilla.org/MPL/2.0/. - -#include "field/field.h" - -#include "util/vf_mesh.h" -#include "field/forms.h" - -#include "metric/constraint.h" - -#if USE_COMISO -#include -//#include -#endif - -namespace Penner { -namespace Field { - -std::tuple> generate_cross_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F) -{ - // Compute cross field and singularities from comiso - Eigen::MatrixXd frame_field; - Eigen::VectorXd S; -#if USE_COMISO - Eigen::VectorXi b(1); - Eigen::MatrixXd bc(1, 3); - b << 0; - bc << 1, 1, 1; - int degree = 4; - igl::copyleft::comiso::nrosy(V, F, b, bc, degree, frame_field, S); -#else - int num_vertices = V.rows(); - int num_faces = F.rows(); - spdlog::error( - "Comiso solver not enabled for #V={}, #F={} mesh. Set USE_COMISO to use.", - num_vertices, - num_faces); -#endif - - // Get the boundary vertices - std::vector is_boundary_vertex = compute_boundary_vertices(F, V.rows()); - - // Turn singularities into a flat metric - // FIXME This is only accurate for closed meshes; singularities only make sense with doubling - int num_cone_vertices = S.size(); - std::vector Th_hat(num_cone_vertices); - for (int vi = 0; vi < num_cone_vertices; ++vi) { - if (is_boundary_vertex[vi]) { - Th_hat[vi] = M_PI - (2 * M_PI * S[vi]); - } else { - Th_hat[vi] = 2 * M_PI * (1 - S[vi]); - } - } - - return std::make_tuple(frame_field, Th_hat); -} - -} // namespace Holonomy -} // namespace Penner \ No newline at end of file diff --git a/src/field/frame_field.cpp b/src/field/frame_field.cpp index f8d10a4..521d070 100644 --- a/src/field/frame_field.cpp +++ b/src/field/frame_field.cpp @@ -7,6 +7,7 @@ // obtain one at http://mozilla.org/MPL/2.0/. #include "field/frame_field.h" +#include "field/vector_field.h" #include "parametrization/refinement.h" #include "util/vf_mesh.h" #include "util/map.h" @@ -23,157 +24,6 @@ namespace Penner { namespace Field { -Eigen::Vector3d generate_reference_direction( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int fijk, - int local_index) -{ - int i = local_index; - int j = (i + 1) % 3; - int k = (j + 1) % 3; - int vj = F(fijk, j); - int vk = F(fijk, k); - return (V.row(vk) - V.row(vj)).normalized(); -} - -Eigen::MatrixXd generate_reference_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F) -{ - int num_faces = F.rows(); - int local_index = 1; - Eigen::MatrixXd reference_field(num_faces, 3); - for (int fijk = 0; fijk < num_faces; ++fijk) { - reference_field.row(fijk) = generate_reference_direction(V, F, fijk, local_index); - } - - return reference_field; -} - -Eigen::MatrixXd generate_reference_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::VectorXi& reference_corner) -{ - int num_faces = F.rows(); - Eigen::MatrixXd reference_field(num_faces, 3); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - int local_index = reference_corner[fijk]; - reference_field.row(fijk) = generate_reference_direction(V, F, fijk, local_index); - } - - return reference_field; -} - -Eigen::MatrixXd generate_frame_field( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& theta) -{ - Eigen::MatrixXd B1, B2, B3; - igl::local_basis(V, F, B1, B2, B3); - return igl::rotate_vectors(reference_field, theta, B1, B2); -} - -void write_rosy_field( - const std::string& output_filename, - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& theta) -{ - Eigen::MatrixXd rosy_field = generate_frame_field(V, F, reference_field, theta); - - std::ofstream field_file(output_filename, std::ios::out | std::ios::trunc); - field_file << F.rows() << std::endl; - field_file << "4" << std::endl; - for (int f = 0; f < F.rows(); ++f) - { - for (int j : {0 , 1, 2}) - { - field_file << std::fixed << std::setprecision(17) << rosy_field(f, j) << " "; - } - - field_file << std::endl; - } - - field_file.close(); -} - -// difference of principal curvatures relative to their total magnitude -Scalar compute_relative_anisotropy(Scalar max_val, Scalar min_val) -{ - return abs(max_val - min_val) / (abs(max_val) + abs(min_val)); -} - - -// absolute difference of principal curvatures -Scalar compute_absolute_anisotropy(Scalar max_val, Scalar min_val) -{ - return abs(max_val - min_val); -} - - -// mean of two principal curvatures -Scalar compute_mean_anisotropy(Scalar max_val, Scalar min_val) -{ - return (max_val + min_val) / 2.; -} - - -// this measurement is near 0 for parabolic regions and near 1 for highly anisotropic regions -Scalar compute_parabolic_anisotropy(Scalar max_val, Scalar min_val) -{ - return abs(abs(max_val) - abs(min_val)) / max(abs(max_val), abs(min_val)); -} - -std::tuple> compute_field_direction( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int radius, - Scalar abs_threshold, - Scalar rel_threshold, - Scalar sample_rate) -{ - //auto[max_direction, min_direction, _max_curvature, _min_curvature] = compute_facet_principal_curvature(V, F, radius); - //auto[_max_direction, _min_direction, max_curvature, min_curvature] = compute_facet_principal_curvature(V, F, 3); - auto[max_direction, min_direction, max_curvature, min_curvature] = compute_facet_principal_curvature(V, F, radius); - int num_faces = F.rows(); - std::vector is_fixed_direction(num_faces, false); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - Scalar kmax = max_curvature[fijk]; - Scalar kmin = min_curvature[fijk]; - if (compute_mean_anisotropy(kmax, kmin) < abs_threshold) continue; - if (compute_parabolic_anisotropy(kmax, kmin) < rel_threshold) continue; - is_fixed_direction[fijk] = true; - } - - if (sample_rate < 1) - { - // shuffle directions - std::vector fixed_directions; - convert_boolean_array_to_index_vector(is_fixed_direction, fixed_directions); - std::vector shuffled_directions = shuffle_map_image(fixed_directions); - - // compute number of sampled directions - int num_fixed_directions = fixed_directions.size(); - int num_sampled_directions = sample_rate * num_fixed_directions; - num_sampled_directions = std::min(num_sampled_directions, num_fixed_directions); - - // get first n shuffled directions - is_fixed_direction = std::vector(num_faces, false); - for (int i = 0; i < num_sampled_directions; ++i) - { - is_fixed_direction[shuffled_directions[i]] = true; - } - } - - return std::make_tuple(max_direction, is_fixed_direction); -} std::tuple generate_frame_field( @@ -316,47 +166,6 @@ void write_frame_field( field_file.close(); } -Eigen::MatrixXd load_rosy_field(const std::string& input_filename) -{ - // Open file - spdlog::debug("opening field at {}", input_filename); - std::ifstream input_file(input_filename); - if (!input_file) return {}; - - // get number of faces - std::string line; - std::getline(input_file, line); - std::istringstream iss(line); - int num_faces; - iss >> num_faces; - spdlog::debug("{} faces", num_faces); - std::getline(input_file, line); // skip start line - - // initialize vectors - Eigen::MatrixXd frame_field(num_faces, 3); - - // Read file one face at a time - int f = 0; - while ((f < num_faces) && (std::getline(input_file, line))) { - std::istringstream iss(line); - for (int i : {0 , 1, 2}) - { - iss >> frame_field(f, i); - } - - ++f; - } - - if (num_faces != f) - { - spdlog::error("Number of faces inconsistent with number of lines"); - } - - // Close file - input_file.close(); - return frame_field; -} - Eigen::VectorXd rotate_vector( const Eigen::VectorXd& vec, double angle, @@ -724,27 +533,6 @@ std::vector compute_cone_angle( return Th_hat; } -Eigen::VectorXd infer_theta( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::VectorXi& reference_corner, - const Eigen::MatrixXd& direction_field) -{ - Eigen::MatrixXd N; - igl::per_face_normals(V, F, N); - - Eigen::MatrixXd reference_field = generate_reference_field(V, F, reference_corner); - int num_faces = F.rows(); - Eigen::VectorXd theta(num_faces); - for (int f = 0; f < num_faces; ++f) - { - // TODO: Check if can replace sign by reversing order, i.e., the signed angle is anticommutative - theta[f] = -signed_angle(direction_field.row(f), reference_field.row(f), N.row(f)); - } - - return theta; -} - std::tuple refine_frame_field( const Eigen::MatrixXi& F, const Eigen::MatrixXi& F_uv, diff --git a/src/field/intrinsic_field.cpp b/src/field/intrinsic_field.cpp index 3eabc2a..d758372 100644 --- a/src/field/intrinsic_field.cpp +++ b/src/field/intrinsic_field.cpp @@ -17,11 +17,7 @@ #include #endif -#include -#include -#include - -#include "field/field.h" +#include "field/vector_field.h" #include "util/spanning_tree.h" #include "util/boundary.h" #include "field/forms.h" @@ -43,118 +39,6 @@ namespace Penner { namespace Field { -Eigen::MatrixXd transfer_vertex_direction_to_corner( - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& vertex_direction, - int corner_index -) -{ - int num_faces = F.rows(); - int dim = vertex_direction.cols(); - Eigen::MatrixXd face_direction(num_faces, dim); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - int vi = F(fijk, corner_index); - face_direction.row(fijk) = vertex_direction.row(vi); - } - - return face_direction; -} - -/* -VectorX average_corner_angles(const std::array& corner_angles) -{ - int num_faces = corner_angles[0].size(); - VectorX theta(num_faces); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - Scalar theta_sum = 0.; - for (int i = 0; i < 3; ++ i) - { - theta_sum += corner_angles[fijk][i]; - } - while (theta_sum < 0) - { - theta_sum += 2 * M_PI.; - } - theta_sum = pos_fmod(theta_sum, M_PI / 2.); - face_theta[i] = theta_sum / 2. - } - - return theta; -} -*/ - -Eigen::Vector3d average_line_field(const Eigen::Vector3d& d0, const Eigen::Vector3d& d1, const Eigen::Vector3d& d2) -{ - //start with first vector (implicitly fixing sign) - Eigen::Vector3d d = d0; - - // add second vector with sign corrected to d0 - if ((d0 - d1).norm() < (d0 + d1).norm()) - { - d += d1; - } else { - d -= d1; - } - - // add third vector with sign corrected to d0 - if ((d0 - d2).norm() < (d0 + d2).norm()) - { - d += d2; - } else { - d -= d2; - } - - return d / 3.; -} - -Eigen::MatrixXd average_line_field(const std::array& corner_directions) -{ - int num_faces = corner_directions[0].rows(); - Eigen::MatrixXd face_direction(num_faces, 3); - for (int fijk = 0; fijk < num_faces; ++fijk) - { - face_direction.row(fijk) = average_line_field(corner_directions[0].row(fijk), corner_directions[1].row(fijk), corner_directions[2].row(fijk)); - } - - return face_direction; -} - - -Eigen::MatrixXd average_line_field_onto_faces( - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& vertex_direction) -{ - std::array corner_directions; - for (int i = 0; i < 3; ++i) - { - corner_directions[i] = transfer_vertex_direction_to_corner(F, vertex_direction, i); - } - - return average_line_field(corner_directions); -} - -std::tuple -compute_facet_principal_curvature( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - int radius) -{ - // find principal curvature - // Compute curvature directions via quadric fitting - Eigen::MatrixXd PD1,PD2; - Eigen::VectorXd PV1,PV2; - std::vector bad_vertices; - igl::principal_curvature(V,F,PD1,PD2,PV1,PV2,bad_vertices,radius,(radius!=1)); - Eigen::VectorXd face_max_curvature, face_min_curvature; - igl::average_onto_faces(F, PV1, face_max_curvature); - igl::average_onto_faces(F, PV2, face_min_curvature); - Eigen::MatrixXd face_max_direction = average_line_field_onto_faces(F, PD1); - Eigen::MatrixXd face_min_direction = average_line_field_onto_faces(F, PD2); - - return std::make_tuple(face_max_direction, face_min_direction, face_max_curvature, face_min_curvature); -} // compute the cone angles at vertices VectorX compute_cone_angles(const Mesh& m, const VectorX& alpha) diff --git a/src/field/rotation_form.cpp b/src/field/rotation_form.cpp index 57dacc4..f8b36a7 100644 --- a/src/field/rotation_form.cpp +++ b/src/field/rotation_form.cpp @@ -9,7 +9,7 @@ #include "field/rotation_form.h" #include "util/linear_algebra.h" -#include "field/field.h" +#include "field/vector_field.h" #include "field/intrinsic_field.h" #include "field/forms.h" diff --git a/src/field/vector_field.cpp b/src/field/vector_field.cpp new file mode 100644 index 0000000..a4b9d4c --- /dev/null +++ b/src/field/vector_field.cpp @@ -0,0 +1,269 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#include "field/vector_field.h" + +#include "util/map.h" +#include "util/linear_algebra.h" +#include "util/vector.h" +#include "util/vf_mesh.h" +//#include "field/forms.h" +//#include "metric/constraint.h" + +#include +#include +#include +#include +#include +#include + +namespace Penner { +namespace Field { + +Eigen::Vector3d generate_reference_direction( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int fijk, + int local_index) +{ + int i = local_index; + int j = (i + 1) % 3; + int k = (j + 1) % 3; + int vj = F(fijk, j); + int vk = F(fijk, k); + return (V.row(vk) - V.row(vj)).normalized(); +} + +Eigen::MatrixXd generate_reference_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F) +{ + int num_faces = F.rows(); + int local_index = 1; + Eigen::MatrixXd reference_field(num_faces, 3); + for (int fijk = 0; fijk < num_faces; ++fijk) { + reference_field.row(fijk) = generate_reference_direction(V, F, fijk, local_index); + } + + return reference_field; +} + +Eigen::MatrixXd generate_reference_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXi& reference_corner) +{ + int num_faces = F.rows(); + Eigen::MatrixXd reference_field(num_faces, 3); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + int local_index = reference_corner[fijk]; + reference_field.row(fijk) = generate_reference_direction(V, F, fijk, local_index); + } + + return reference_field; +} + +Eigen::MatrixXd generate_vector_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta) +{ + Eigen::MatrixXd B1, B2, B3; + igl::local_basis(V, F, B1, B2, B3); + return igl::rotate_vectors(reference_field, theta, B1, B2); +} + +// difference of principal curvatures relative to their total magnitude +Scalar compute_relative_anisotropy(Scalar max_val, Scalar min_val) +{ + return abs(max_val - min_val) / (abs(max_val) + abs(min_val)); +} + + +// absolute difference of principal curvatures +Scalar compute_absolute_anisotropy(Scalar max_val, Scalar min_val) +{ + return abs(max_val - min_val); +} + + +// mean of two principal curvatures +Scalar compute_mean_anisotropy(Scalar max_val, Scalar min_val) +{ + return (max_val + min_val) / 2.; +} + + +// this measurement is near 0 for parabolic regions and near 1 for highly anisotropic regions +Scalar compute_parabolic_anisotropy(Scalar max_val, Scalar min_val) +{ + return abs(abs(max_val) - abs(min_val)) / max(abs(max_val), abs(min_val)); +} + +Eigen::MatrixXd transfer_vertex_direction_to_corner( + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& vertex_direction, + int corner_index +) +{ + int num_faces = F.rows(); + int dim = vertex_direction.cols(); + Eigen::MatrixXd face_direction(num_faces, dim); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + int vi = F(fijk, corner_index); + face_direction.row(fijk) = vertex_direction.row(vi); + } + + return face_direction; +} + + +Eigen::Vector3d average_line_field(const Eigen::Vector3d& d0, const Eigen::Vector3d& d1, const Eigen::Vector3d& d2) +{ + //start with first vector (implicitly fixing sign) + Eigen::Vector3d d = d0; + + // add second vector with sign corrected to d0 + if ((d0 - d1).norm() < (d0 + d1).norm()) + { + d += d1; + } else { + d -= d1; + } + + // add third vector with sign corrected to d0 + if ((d0 - d2).norm() < (d0 + d2).norm()) + { + d += d2; + } else { + d -= d2; + } + + return d / 3.; +} + +Eigen::MatrixXd average_line_field(const std::array& corner_directions) +{ + int num_faces = corner_directions[0].rows(); + Eigen::MatrixXd face_direction(num_faces, 3); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + face_direction.row(fijk) = average_line_field(corner_directions[0].row(fijk), corner_directions[1].row(fijk), corner_directions[2].row(fijk)); + } + + return face_direction; +} + + +Eigen::MatrixXd average_line_field_onto_faces( + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& vertex_direction) +{ + std::array corner_directions; + for (int i = 0; i < 3; ++i) + { + corner_directions[i] = transfer_vertex_direction_to_corner(F, vertex_direction, i); + } + + return average_line_field(corner_directions); +} + +std::tuple +compute_facet_principal_curvature( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int radius) +{ + // find principal curvature + // Compute curvature directions via quadric fitting + Eigen::MatrixXd PD1,PD2; + Eigen::VectorXd PV1,PV2; + std::vector bad_vertices; + igl::principal_curvature(V,F,PD1,PD2,PV1,PV2,bad_vertices,radius,(radius!=1)); + Eigen::VectorXd face_max_curvature, face_min_curvature; + igl::average_onto_faces(F, PV1, face_max_curvature); + igl::average_onto_faces(F, PV2, face_min_curvature); + Eigen::MatrixXd face_max_direction = average_line_field_onto_faces(F, PD1); + Eigen::MatrixXd face_min_direction = average_line_field_onto_faces(F, PD2); + + return std::make_tuple(face_max_direction, face_min_direction, face_max_curvature, face_min_curvature); +} + +std::tuple> compute_field_direction( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + int radius, + Scalar abs_threshold, + Scalar rel_threshold, + Scalar sample_rate) +{ + //auto[max_direction, min_direction, _max_curvature, _min_curvature] = compute_facet_principal_curvature(V, F, radius); + //auto[_max_direction, _min_direction, max_curvature, min_curvature] = compute_facet_principal_curvature(V, F, 3); + auto[max_direction, min_direction, max_curvature, min_curvature] = compute_facet_principal_curvature(V, F, radius); + int num_faces = F.rows(); + std::vector is_fixed_direction(num_faces, false); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + Scalar kmax = max_curvature[fijk]; + Scalar kmin = min_curvature[fijk]; + if (compute_mean_anisotropy(kmax, kmin) < abs_threshold) continue; + if (compute_parabolic_anisotropy(kmax, kmin) < rel_threshold) continue; + is_fixed_direction[fijk] = true; + } + + if (sample_rate < 1) + { + // shuffle directions + std::vector fixed_directions; + convert_boolean_array_to_index_vector(is_fixed_direction, fixed_directions); + std::vector shuffled_directions = shuffle_map_image(fixed_directions); + + // compute number of sampled directions + int num_fixed_directions = fixed_directions.size(); + int num_sampled_directions = sample_rate * num_fixed_directions; + num_sampled_directions = std::min(num_sampled_directions, num_fixed_directions); + + // get first n shuffled directions + is_fixed_direction = std::vector(num_faces, false); + for (int i = 0; i < num_sampled_directions; ++i) + { + is_fixed_direction[shuffled_directions[i]] = true; + } + } + + return std::make_tuple(max_direction, is_fixed_direction); +} + +Eigen::VectorXd infer_theta( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXi& reference_corner, + const Eigen::MatrixXd& direction_field) +{ + Eigen::MatrixXd N; + igl::per_face_normals(V, F, N); + + Eigen::MatrixXd reference_field = generate_reference_field(V, F, reference_corner); + int num_faces = F.rows(); + Eigen::VectorXd theta(num_faces); + for (int f = 0; f < num_faces; ++f) + { + // TODO: Check if can replace sign by reversing order, i.e., the signed angle is anticommutative + theta[f] = -signed_angle(direction_field.row(f), reference_field.row(f), N.row(f)); + } + + return theta; +} + + + +} // namespace Holonomy +} // namespace Penner \ No newline at end of file diff --git a/src/holonomy/core/viewer.cpp b/src/holonomy/core/viewer.cpp index f7afadb..cbe74dd 100644 --- a/src/holonomy/core/viewer.cpp +++ b/src/holonomy/core/viewer.cpp @@ -25,6 +25,7 @@ // cross field generation for viewer #include "field/frame_field.h" +#include "field/vector_field.h" #ifdef ENABLE_VISUALIZATION #include "polyscope/point_cloud.h" @@ -144,7 +145,7 @@ void view_cross_field( } // generate frame field geometry from cross field - Eigen::MatrixXd frame_field = Field::generate_frame_field(V, F, reference_field, theta); + Eigen::MatrixXd frame_field = Field::generate_vector_field(V, F, reference_field, theta); int num_faces = F.rows(); // transfer kappa and period jump to viewer halfedge indexing diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index da1f4e6..1ae2ce9 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -11,6 +11,7 @@ #include "holonomy/core/boundary_basis.h" #include "holonomy/core/homology_basis.h" #include "field/intrinsic_field.h" +#include "field/cross_field.h" #include "metric/quality.h" #include "field/cones.h" #include "holonomy/holonomy/holonomy.h" @@ -431,7 +432,7 @@ infer_marked_metric( bool use_intrinsic, MarkedMetricParameters marked_metric_params) { - auto [frame_field, field_Th_hat] = Field::generate_cross_field(V, F); + auto [frame_field, field_Th_hat] = Field::generate_rosy_field(V, F); // Convert VF mesh to halfedge std::vector vtx_reindex_mesh, indep_vtx, dep_vtx, v_rep, bnd_loops; From d24f737801b0b88c5c28c8240f2647d3466bb04f Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Tue, 7 Jul 2026 12:31:54 -0400 Subject: [PATCH 08/17] Added executables --- src/app/CMakeLists.txt | 75 ++++ src/app/generate_aligned_field.cpp | 163 +++++++++ src/app/generate_field.cpp | 191 +++++++++++ src/app/optimize_metric.cpp | 221 ++++++++++++ src/app/optimize_shear.cpp | 183 ++++++++++ src/app/parameterize_aligned.cpp | 527 +++++++++++++++++++++++++++++ src/app/parametrize_seamless.cpp | 411 ++++++++++++++++++++++ src/app/symdir.json | 60 ++++ src/app/util.h | 71 ++++ src/app/view_quad_mesh.cpp | 30 ++ src/app/view_seamless_uv.cpp | 104 ++++++ 11 files changed, 2036 insertions(+) create mode 100644 src/app/CMakeLists.txt create mode 100644 src/app/generate_aligned_field.cpp create mode 100644 src/app/generate_field.cpp create mode 100644 src/app/optimize_metric.cpp create mode 100644 src/app/optimize_shear.cpp create mode 100644 src/app/parameterize_aligned.cpp create mode 100644 src/app/parametrize_seamless.cpp create mode 100644 src/app/symdir.json create mode 100644 src/app/util.h create mode 100644 src/app/view_quad_mesh.cpp create mode 100644 src/app/view_seamless_uv.cpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..66afb74 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,75 @@ +add_library(ApplicationUtilLib INTERFACE) +target_include_directories(ApplicationUtilLib INTERFACE .) +target_link_libraries(ApplicationUtilLib INTERFACE + CLI11::CLI11 +) + +add_executable(generate_field +generate_field.cpp +) +target_link_libraries(generate_field PRIVATE + PennerLib + ApplicationUtilLib +) + +add_executable(generate_aligned_field +generate_aligned_field.cpp +) +target_link_libraries(generate_aligned_field PRIVATE + PennerLib + ApplicationUtilLib +) + +add_executable(parametrize_seamless + parametrize_seamless.cpp +) +target_link_libraries(parametrize_seamless PRIVATE + PennerLib + SymmetricDirichletLib + CLI11::CLI11 +) + +add_executable(parameterize_aligned +parameterize_aligned.cpp +) +target_link_libraries(parameterize_aligned PRIVATE + PennerLib + ApplicationUtilLib +) + +add_executable(optimize_metric + optimize_metric.cpp +) +target_link_libraries(optimize_metric PRIVATE + PennerLib + CLI11::CLI11 +) + +add_executable(optimize_shear + optimize_shear.cpp +) +target_link_libraries(optimize_shear PRIVATE + PennerLib + CLI11::CLI11 +) + +# only build visualization executables if visualization enabled +if (NOT USE_MULTIPRECISION) +if (ENABLE_VISUALIZATION) + add_executable(view_quad_mesh + view_quad_mesh.cpp + ) + target_link_libraries(view_quad_mesh PRIVATE + PennerLib + ApplicationUtilLib + ) + + add_executable(view_seamless + view_seamless.cpp + ) + target_link_libraries(view_seamless PRIVATE + PennerLib + ApplicationUtilLib + ) +endif() +endif() diff --git a/src/app/generate_aligned_field.cpp b/src/app/generate_aligned_field.cpp new file mode 100644 index 0000000..fde35bd --- /dev/null +++ b/src/app/generate_aligned_field.cpp @@ -0,0 +1,163 @@ +#include "holonomy/core/common.h" +#include "feature/core/viewer.h" +#include "feature/interface.h" +#include "feature/feature/features.h" +#include "feature/surgery/cut_metric_generator.h" +#include "feature/core/io.h" +#include "util/io.h" +#include "field/frame_field.h" +#include "holonomy/core/viewer.h" +#include +#include + +#include +#include + +#include "polyscope/point_cloud.h" +#include "polyscope/surface_mesh.h" + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Feature; +using namespace Penner::Holonomy; + +int main(int argc, char* argv[]) +{ + // Get command line arguments + CLI::App app{"Modify frame field in interactive viewer"}; + std::string mesh_filename = "./"; + std::string output_dir = "./"; + Scalar displacement = 0.; + bool show_field = false; + bool collapse_cones = false; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Input mesh")->check(CLI::ExistingFile)->required(); + app.add_option("--output", output_dir, "Output directory"); + app.add_option("--displacement", displacement, "displacement for cross field viewer"); + app.add_flag("--view", show_field, "open viewer to show field"); + app.add_flag("--collapse_cones", collapse_cones, "collapse adjacent cones"); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::info); + std::filesystem::create_directory(output_dir); + std::string mesh = std::filesystem::path(mesh_filename).stem().string(); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("optimizing mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // refine input mesh + std::vector feature_edges, hard_feature_edges; + std::tie(V, F, feature_edges, hard_feature_edges) = generate_refined_feature_mesh(V, F, false); + FeatureFinder feature_finder(V, F); + feature_finder.mark_features(feature_edges); + Eigen::MatrixXd V_cut; + Eigen::MatrixXi F_cut, F_is_feature; + Eigen::VectorXi V_map; + std::tie(V_cut, F_cut, V_map, F_is_feature) = feature_finder.generate_feature_cut_mesh(); + + // fit field directions + int radius = 5; + Scalar rel_anisotropy=0.9; + Scalar abs_anisotropy=0.2; + Scalar bb_diag = igl::bounding_box_diagonal(V); + auto [direction, is_fixed_direction] = Penner::Field::compute_field_direction( + V_cut, + F_cut, + radius, + abs_anisotropy / bb_diag, + rel_anisotropy); + + // generate initial field and halfedge mesh + MarkedMetricParameters marked_metric_params; + Eigen::MatrixXd reference_field; + Eigen::VectorXd theta; + Eigen::MatrixXd kappa; + Eigen::MatrixXi period_jump; + marked_metric_params.use_log_length = true; + CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, {}); + cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); + std::tie(reference_field, theta, kappa, period_jump) = cut_metric_generator.get_field(); + Eigen::VectorXi reference_corner(reference_field.rows()); + + MarkedPennerConeMetric marked_metric; + std::vector vtx_reindex; + std::vector face_reindex; + VectorX rotation_form; + std::vector Th_hat; + std::tie(marked_metric, vtx_reindex, face_reindex, rotation_form, Th_hat) = cut_metric_generator.get_union_metric( marked_metric_params); + + // modify field + Penner::Field::IntrinsicNRosyField field_generator; + field_generator.initialize(marked_metric); + field_generator.set_field(marked_metric, vtx_reindex, F_cut, face_reindex, theta, kappa, period_jump); + Eigen::MatrixXd V_disp = displace_cut_faces(V_cut, F_cut, displacement); + + if (collapse_cones) + { + field_generator.collapse_nearby_cones(marked_metric); + field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); + } + + if (show_field) + { + polyscope::init(); + field_generator.update_viewer(marked_metric, vtx_reindex, V_disp); + auto callback = [&] () { + if (ImGui::Button("collapse cones")) { + field_generator.collapse_adjacent_cones(marked_metric); + field_generator.update_viewer(marked_metric, vtx_reindex, V_disp); + } + if (ImGui::Button("parameterize")) { + field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); + + NewtonParameters alg_params; + alg_params.output_dir = "./"; + alg_params.error_eps = 1e-10; + alg_params.solver = "ldlt"; + MarkedMetricParameters marked_metric_params; + AlignedMetricGenerator aligned_metric_generator( + V, + F, + feature_edges, + hard_feature_edges, + reference_field, + theta, + kappa, + period_jump, + marked_metric_params); + aligned_metric_generator.optimize_full(alg_params); + aligned_metric_generator.optimize_relaxed(alg_params); + aligned_metric_generator.parameterize(false); + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = aligned_metric_generator.get_parameterization(); + view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "refined mesh", false); + + std::string output_filename = join_path(output_dir, mesh+"_param.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + } + if (ImGui::Button("write field")) { + field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); + std::string field_filename = join_path(output_dir, mesh+".ffield"); + Penner::Field::write_frame_field(field_filename, reference_field, theta, kappa, period_jump); + } + }; + polyscope::state::userCallback = callback; + polyscope::show(); + field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); + } + + // write output + std::string output_filename; + output_filename = join_path(output_dir, mesh + ".obj"); + igl::writeOBJ(output_filename, V, F); + output_filename = join_path(output_dir, mesh + "_features"); + write_feature_edges(output_filename, feature_edges); + output_filename = join_path(output_dir, mesh + "_hard_features"); + write_feature_edges(output_filename, hard_feature_edges); + output_filename = join_path(output_dir, mesh+".ffield"); + Penner::Field::write_frame_field(output_filename, reference_field, theta, kappa, period_jump); + +} diff --git a/src/app/generate_field.cpp b/src/app/generate_field.cpp new file mode 100644 index 0000000..e578fc0 --- /dev/null +++ b/src/app/generate_field.cpp @@ -0,0 +1,191 @@ +/********************************************************************************* +* This file is part of reference implementation of SIGGRAPH 2024 Paper * +* `Seamless Parametrization in Penner Coordinates` * +* v1.0 * +* * +* The MIT License * +* * +* Permission is hereby granted, free of charge, to any person obtaining a * +* copy of this software and associated documentation files (the "Software"), * +* to deal in the Software without restriction, including without limitation * +* the rights to use, copy, modify, merge, publish, distribute, sublicense, * +* and/or sell copies of the Software, and to permit persons to whom the * +* Software is furnished to do so, subject to the following conditions: * +* * +* The above copyright notice and this permission notice shall be included in * +* all copies or substantial portions of the Software. * +* * +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * +* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE * +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * +* IN THE SOFTWARE. * +* * +* Author(s): * +* Ryan Capouellez, Denis Zorin, * +* Courant Institute of Mathematical Sciences, New York University, USA * +* * * +*********************************************************************************/ +#include "field/field.h" +#include "holonomy/interface.h" +#include "holonomy/holonomy/cones.h" +#include "util/io.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Holonomy; + +void write_error_record(const std::filesystem::path& filepath, const std::string& error_message) +{ + std::ofstream output_file(filepath, std::ios::out | std::ios::app); + output_file << error_message << std::endl; + output_file.close(); +} + +int main(int argc, char* argv[]) +{ + // get command line arguments + CLI::App app{"Generate smooth cross field data for a mesh."}; + std::string mesh_file = ""; + std::string input_dir = "./"; + std::string output_dir = "./"; + bool is_stl = false; + app.add_option("--mesh", mesh_file, "Mesh file")->required(); + app.add_option("-i,--input", input_dir, "Input filepath") + ->check(CLI::ExistingDirectory) + ->required(); + app.add_option("-o,--output", output_dir, "Output directory")->check(CLI::ExistingDirectory); + app.add_flag("--is_stl", is_stl, "Input is an stl mesh"); + CLI11_PARSE(app, argc, argv); + spdlog::set_level(spdlog::level::debug); + + // get mesh name from file + std::string mesh_name = std::filesystem::path(mesh_file).replace_extension(); + + // get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + std::string mesh_filename = join_path(input_dir, mesh_file); + spdlog::info("Using mesh at {}", mesh_filename); + if (is_stl) { + std::ifstream input_stream(mesh_filename); + Eigen::MatrixXd V_stl, N_stl; + Eigen::MatrixXi F_stl; + Eigen::VectorXi I, J; + igl::readSTL(input_stream, V_stl, F_stl, N_stl); + igl::remove_duplicate_vertices(V_stl, F_stl, 1e-14, V, I, J, F); + input_stream.close(); + } else { + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + } + + // get mesh topology information + Eigen::MatrixXd bd; + Eigen::VectorXi components; + igl::boundary_facets(F, bd); + igl::facet_components(F, components); + + // check validity + std::string error_filename = join_path(output_dir, "error_mesh_list"); + if (bd.size() > 1) { + spdlog::warn("Mesh has boundary"); + write_error_record(error_filename, mesh_name + " has boundary"); + } + if (components.maxCoeff() != 0) { + spdlog::error("Mesh has multiple components"); + write_error_record(error_filename, mesh_name + " has multiple components"); + return 1; + } + if (!igl::is_edge_manifold(F)) { + spdlog::error("Mesh is not edge manifold"); + write_error_record(error_filename, mesh_name + " is not edge manifold"); + return 1; + } + Eigen::VectorXi B; + if (!igl::is_vertex_manifold(F, B)) { + spdlog::error("Mesh is not vertex manifold"); + write_error_record(error_filename, mesh_name + " is not vertex manifold"); + return 1; + } + + // generate cross field + auto [frame_field, field_Th_hat] = generate_cross_field(V, F); + + // build halfedge mesh with angles + std::vector vtx_reindex; + std::vector free_cones(0); + bool fix_boundary = false; + bool use_discrete_metric = true; + std::unique_ptr cone_metric = + Optimization::generate_initial_mesh( + V, + F, + V, + F, + field_Th_hat, + vtx_reindex, + free_cones, + fix_boundary, + use_discrete_metric); + + // check for zero edge lengths and degenerate angles + VectorX he2angle, he2cot; + cone_metric->get_corner_angles(he2angle, he2cot); + int num_halfedges = cone_metric->n_halfedges(); + for (int hij = 0; hij < num_halfedges; ++hij) { + if (float_equal(cone_metric->l[hij], 0.)) { + spdlog::error("Mesh has {} length edge", cone_metric->l[hij]); + write_error_record(error_filename, mesh_name + " has 0 length edge"); + return 1; + } + if (float_equal(he2angle[hij], 0.)) { + spdlog::error("Mesh has {} angle", he2angle[hij]); + write_error_record(error_filename, mesh_name + " has 0 corner angle"); + return 1; + } + if (float_equal(he2angle[hij], M_PI)) { + spdlog::error("Mesh has {} angle", cone_metric->l[hij]); + write_error_record(error_filename, mesh_name + " has pi corner angle"); + return 1; + } + } + + // generate rotation form and cones from cross field + VectorX rotation_form = generate_rotation_form_from_cross_field(*cone_metric, vtx_reindex, V, F, frame_field); + std::vector form_Th_hat = generate_cones_from_rotation_form( + *cone_metric, + vtx_reindex, + rotation_form, + bd.size() > 1); + + // check cones are consistent + int num_vertices = V.rows(); + for (int vi = 0; vi < num_vertices; ++vi) { + if (!float_equal(form_Th_hat[vi], field_Th_hat[vi], 1e-6)) { + spdlog::warn("Inconsistent cones {} and {}", form_Th_hat[vi], field_Th_hat[vi]); + write_error_record(error_filename, mesh_name + " has inconsistent cones"); + } + } + + // write output + igl::writeOBJ(join_path(output_dir, mesh_name + ".obj"), V, F); + write_vector(rotation_form, join_path(output_dir, mesh_name + "_kappa_hat")); + write_vector(form_Th_hat, join_path(output_dir, mesh_name + "_Th_hat")); + + return 0; +} diff --git a/src/app/optimize_metric.cpp b/src/app/optimize_metric.cpp new file mode 100644 index 0000000..92265ef --- /dev/null +++ b/src/app/optimize_metric.cpp @@ -0,0 +1,221 @@ +/********************************************************************************* +* This file is part of reference implementation of SIGGRAPH Asia 2023 Paper * +* `Metric Optimization in Penner Coordinates` * +* v1.0 * +* * +* The MIT License * +* * +* Permission is hereby granted, free of charge, to any person obtaining a * +* copy of this software and associated documentation files (the "Software"), * +* to deal in the Software without restriction, including without limitation * +* the rights to use, copy, modify, merge, publish, distribute, sublicense, * +* and/or sell copies of the Software, and to permit persons to whom the * +* Software is furnished to do so, subject to the following conditions: * +* * +* The above copyright notice and this permission notice shall be included in * +* all copies or substantial portions of the Software. * +* * +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * +* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE * +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * +* IN THE SOFTWARE. * +* * +* Author(s): * +* Ryan Capouellez, Denis Zorin, * +* Courant Institute of Mathematical Sciences, New York University, USA * +* * * +*********************************************************************************/ +#include +#include +#include "optimization/core/common.h" +#include "optimization/core/cone_metric.h" +#include "optimization/metric_optimization/energy_functor.h" +#include "optimization/metric_optimization/implicit_optimization.h" +#include "optimization/interface.h" +#include "optimization/parameterization/refinement.h" +#include "optimization/util/viewers.h" +#include "holonomy/field/field.h" +#include "util/vector.h" +#include "util/io.h" + +#include + +using namespace Penner; +using namespace Optimization; + + +std::vector get_free_cones(const std::vector& Th_hat) +{ + std::vector free_cones = {}; + int num_vertices = Th_hat.size(); + for (int vi = 0; vi < num_vertices; ++vi) + { + if (!float_equal(Th_hat[vi], 2. * M_PI)) + { + free_cones.push_back(vi); + } + } + + return free_cones; +} + +int main(int argc, char* argv[]) +{ +#ifdef MULTIPRECISION + spdlog::info("Using multiprecision"); + mpfr::mpreal::set_default_prec(100); + mpfr::mpreal::set_emax(mpfr::mpreal::get_emax_max()); + mpfr::mpreal::set_emin(mpfr::mpreal::get_emin_min()); +#endif + + // Build maps from strings to enums + std::map energy_choice_map{ + {"log_length", EnergyChoice::log_length}, + {"log_scale", EnergyChoice::log_scale}, + {"quadratic_sym_dirichlet", EnergyChoice::quadratic_sym_dirichlet}, + {"sym_dirichlet", EnergyChoice::sym_dirichlet}, + {"p_norm", EnergyChoice::p_norm}, + }; + + // Get command line arguments + CLI::App app{"Generate approximately isometric parameterization for a mesh."}; + std::string mesh_filename = ""; + std::string Th_hat_filename = ""; + std::string output_dir = "./"; + EnergyChoice energy_choice = EnergyChoice::log_length; + bool use_discrete_metric = false; + bool use_free_cones = false; + bool show_parameterization = false; + auto proj_params = std::make_shared(); + auto opt_params = std::make_shared(); + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + app.add_option("--cones", Th_hat_filename, "Cone angle filepath") + ->check(CLI::ExistingFile); + app.add_option("--energy", energy_choice, "Energy to minimize") + ->transform(CLI::CheckedTransformer(energy_choice_map, CLI::ignore_case)); + app.add_option("--direction", opt_params->direction_choice, "Descent direction: projected_gradient, projected_newton"); + app.add_option( + "--num_iter", + opt_params->num_iter, + "Maximum number of iterations to perform") + ->check(CLI::NonNegativeNumber); + app.add_flag("--use_discrete_metric", use_discrete_metric, "Use edge lengths instead of Penner coordinates"); + app.add_flag("--show_parameterization", show_parameterization, "Show final parameterization"); + app.add_flag("--use_free_cones", use_free_cones, "Let cones have free angles"); + app.add_option("-o,--output", output_dir, "Output directory"); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::info); + std::filesystem::create_directories(output_dir); + opt_params->output_dir = output_dir; + + // TODO Make this automatic + if (use_discrete_metric) + { + proj_params->initial_ptolemy = false; + proj_params->use_edge_flips = false; + proj_params->max_itr = 30; + } + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Optimizing mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // Get input angles + std::vector Th_hat_init; + if (Th_hat_filename != "") + { + spdlog::info("Using cone angles at {}", Th_hat_filename); + read_vector_from_file(Th_hat_filename, Th_hat_init); + } + else + { + Eigen::MatrixXd frame_field; + std::tie(frame_field, Th_hat_init)= Holonomy::generate_cross_field(V, F); + } + std::vector Th_hat = correct_cone_angles(Th_hat_init); + + // Get initial mesh for optimization + std::vector vtx_reindex; + std::vector free_cones = {}; + if (use_free_cones) free_cones = get_free_cones(Th_hat); + bool fix_boundary = false; + std::unique_ptr cone_metric = + generate_initial_mesh(V, F, V, F, Th_hat, vtx_reindex, free_cones, fix_boundary, use_discrete_metric); + + // Get energy + std::unique_ptr opt_energy = generate_energy(V, F, Th_hat, *cone_metric, energy_choice); + + // Optimize the metric + std::unique_ptr optimized_cone_metric = + optimize_metric(*cone_metric, *opt_energy, proj_params, opt_params); + VectorX optimized_metric_coords = optimized_cone_metric->get_reduced_metric_coordinates(); + + // Write the output metric coordinates + std::string output_filename = join_path(output_dir, "optimized_metric_coords"); + write_vector(optimized_metric_coords, output_filename, 17); + + // Generate overlay VF mesh with parametrization + if (use_discrete_metric) { + auto vf_res = generate_VF_mesh_from_discrete_metric( + V, + F, + Th_hat, + optimized_metric_coords); + Eigen::MatrixXd V_l = std::get<0>(vf_res); + Eigen::MatrixXi F_l = std::get<1>(vf_res); + Eigen::MatrixXd uv_l = std::get<2>(vf_res); + Eigen::MatrixXi FT_l = std::get<3>(vf_res); + + // Write the overlay output + output_filename = join_path(output_dir, "mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_l, F_l, uv_l, FT_l); + + // Optionally show final parameterization + if (show_parameterization) view_parameterization(V_l, F_l, uv_l, FT_l); + } else { + std::vector is_cut = {}; + bool do_best_fit_scaling = false; + auto vf_res = generate_VF_mesh_from_metric( + V, + F, + Th_hat, + *cone_metric, + optimized_metric_coords, + is_cut, + do_best_fit_scaling); + OverlayMesh m_o = std::get<0>(vf_res); + Eigen::MatrixXd V_o = std::get<1>(vf_res); + Eigen::MatrixXi F_o = std::get<2>(vf_res); + Eigen::MatrixXd uv_o = std::get<3>(vf_res); + Eigen::MatrixXi FT_o = std::get<4>(vf_res); + std::vector fn_to_f_o = std::get<7>(vf_res); + std::vector> endpoints_o = std::get<8>(vf_res); + + // Write the overlay output + output_filename = join_path(output_dir, "overlay_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_o, F_o, uv_o, FT_o); + + // Get refinement mesh + Eigen::MatrixXd V_r; + Eigen::MatrixXi F_r; + Eigen::MatrixXd uv_r; + Eigen::MatrixXi FT_r; + std::vector fn_to_f_r; + std::vector> endpoints_r; + RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); + refinement_mesh.get_VF_mesh(V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r); + + // Write the refined output + output_filename = join_path(output_dir, "refined_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + + // Optionally show final parameterization + if (show_parameterization) view_parameterization(V_r, F_r, uv_r, FT_r); + } +} diff --git a/src/app/optimize_shear.cpp b/src/app/optimize_shear.cpp new file mode 100644 index 0000000..5dd882e --- /dev/null +++ b/src/app/optimize_shear.cpp @@ -0,0 +1,183 @@ +/********************************************************************************* +* This file is part of reference implementation of SIGGRAPH Asia 2023 Paper * +* `Metric Optimization in Penner Coordinates` * +* v1.0 * +* * +* The MIT License * +* * +* Permission is hereby granted, free of charge, to any person obtaining a * +* copy of this software and associated documentation files (the "Software"), * +* to deal in the Software without restriction, including without limitation * +* the rights to use, copy, modify, merge, publish, distribute, sublicense, * +* and/or sell copies of the Software, and to permit persons to whom the * +* Software is furnished to do so, subject to the following conditions: * +* * +* The above copyright notice and this permission notice shall be included in * +* all copies or substantial portions of the Software. * +* * +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * +* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE * +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * +* IN THE SOFTWARE. * +* * +* Author(s): * +* Ryan Capouellez, Denis Zorin, * +* Courant Institute of Mathematical Sciences, New York University, USA * +* * * +*********************************************************************************/ +#include +#include + +#include "optimization/core/common.h" +#include "optimization/metric_optimization/explicit_optimization.h" +#include "optimization/interface.h" +#include "optimization/core/shear.h" +#include "optimization/parameterization/refinement.h" +#include "optimization/util/viewers.h" +#include "util/io.h" +#include + +using namespace Penner; +using namespace Optimization; + +int main(int argc, char* argv[]) +{ +#ifdef MULTIPRECISION + spdlog::info("Using multiprecision"); + mpfr::mpreal::set_default_prec(60); + mpfr::mpreal::set_emax(mpfr::mpreal::get_emax_max()); + mpfr::mpreal::set_emin(mpfr::mpreal::get_emin_min()); +#endif + + // Build maps from strings to enums + std::map energy_choice_map{ + {"log_length", EnergyChoice::log_length}, + {"log_scale", EnergyChoice::log_scale}, + {"quadratic_sym_dirichlet", EnergyChoice::quadratic_sym_dirichlet}, + {"sym_dirichlet", EnergyChoice::sym_dirichlet}, + {"p_norm", EnergyChoice::p_norm}, + }; + + // Get command line arguments + CLI::App app{"Generate approximately isometric parameterization for a mesh."}; + std::string mesh_filename = ""; + std::string Th_hat_filename = ""; + std::string output_dir = "./"; + EnergyChoice energy_choice = EnergyChoice::log_length; + bool show_parameterization = false; + auto proj_params = std::make_shared(); + auto opt_params = std::make_shared(); + opt_params->direction_choice = "gradient"; + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + app.add_option("--cones", Th_hat_filename, "Cone angle filepath") + ->check(CLI::ExistingFile) + ->required(); + app.add_option("--energy", energy_choice, "Energy to minimize") + ->transform(CLI::CheckedTransformer(energy_choice_map, CLI::ignore_case)); + app.add_option("--direction", opt_params->direction_choice, "Descent direction: gradient, conjugate_gradient, lbfgs"); + app.add_option( + "--num_iter", + opt_params->num_iter, + "Maximum number of iterations to perform") + ->check(CLI::NonNegativeNumber); + app.add_flag("--show_parameterization", show_parameterization, "Show final parameterization"); + app.add_option("-o,--output", output_dir, "Output directory"); + CLI11_PARSE(app, argc, argv); + + // Make output directory + spdlog::set_level(spdlog::level::info); + std::filesystem::create_directories(output_dir); + opt_params->output_dir = output_dir; + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Optimizing mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // Get input angles + std::vector Th_hat_init; + spdlog::info("Using cone angles at {}", Th_hat_filename); + read_vector_from_file(Th_hat_filename, Th_hat_init); + std::vector Th_hat = correct_cone_angles(Th_hat_init); + + // Get initial mesh for optimization + std::vector vtx_reindex; + std::vector free_cones = {}; + bool fix_boundary = false; + bool use_discrete_metric = false; + std::unique_ptr cone_metric = + generate_initial_mesh(V, F, V, F, Th_hat, vtx_reindex, free_cones, fix_boundary, use_discrete_metric); + + // Get energy + std::unique_ptr opt_energy = generate_energy(V, F, Th_hat, *cone_metric, energy_choice); + + // Compute shear dual basis and the corresponding inner product matrix + MatrixX shear_basis_matrix; + std::vector independent_edges; + compute_shear_dual_basis(*cone_metric, shear_basis_matrix, independent_edges); + + // Compute the shear dual coordinates for this basis + VectorX shear_basis_coords_init; + VectorX scale_factors_init; + compute_shear_basis_coordinates( + *cone_metric, + shear_basis_matrix, + shear_basis_coords_init, + scale_factors_init); + + // Optimize the metric + VectorX optimized_metric_coords = optimize_shear_basis_coordinates( + *cone_metric, + *opt_energy, + shear_basis_matrix, + proj_params, + opt_params); + + // Write the metric coordinate output + std::string output_filename = join_path(output_dir, "reduced_metric_coords"); + write_vector(optimized_metric_coords, output_filename); + + // Generate overlay mesh + std::vector is_cut = {}; + bool do_best_fit_scaling = false; + auto vf_res = generate_VF_mesh_from_metric( + V, + F, + Th_hat, + *cone_metric, + optimized_metric_coords, + is_cut, + do_best_fit_scaling); + OverlayMesh m_o = std::get<0>(vf_res); + Eigen::MatrixXd V_o = std::get<1>(vf_res); + Eigen::MatrixXi F_o = std::get<2>(vf_res); + Eigen::MatrixXd uv_o = std::get<3>(vf_res); + Eigen::MatrixXi FT_o = std::get<4>(vf_res); + std::vector fn_to_f_o = std::get<7>(vf_res); + std::vector> endpoints_o = std::get<8>(vf_res); + + // Write the overlay output + output_filename = join_path(output_dir, "overlay_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_o, F_o, uv_o, FT_o); + + // Get refinement mesh + Eigen::MatrixXd V_r; + Eigen::MatrixXi F_r; + Eigen::MatrixXd uv_r; + Eigen::MatrixXi FT_r; + std::vector fn_to_f_r; + std::vector> endpoints_r; + RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); + refinement_mesh.get_VF_mesh(V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r); + + // Write the refined output + output_filename = join_path(output_dir, "refined_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + + // Optionally show final parameterization + if (show_parameterization) view_parameterization(V_r, F_r, uv_r, FT_r); +} diff --git a/src/app/parameterize_aligned.cpp b/src/app/parameterize_aligned.cpp new file mode 100644 index 0000000..ad38306 --- /dev/null +++ b/src/app/parameterize_aligned.cpp @@ -0,0 +1,527 @@ +#include +#include "feature/interface.h" +#include "feature/core/io.h" +#include "field/frame_field.h" +#include "util/io.h" +#include "util.h" +#include "holonomy/core/viewer.h" +#include "feature/surgery/cut_metric_generator.h" +#include "util/vf_mesh.h" + +#include +#include +#include +#include + +#include "polyscope/surface_mesh.h" + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Holonomy; +using namespace Penner::Feature; + +#if USE_UV_OPTIMIZATION +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" + +SymDir::Parameters read_parameters(const nlohmann::json& config) +{ + SymDir::Parameters param; + param.max_iters = config["max_iters"]; // iterations + param.max_time = config["max_time"]; // time in seconds + param.smooth_only_iters = config["smooth_only_iters"]; + param.E_target = config["E_target"]; // Energy target + param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation + param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation + param.local_smooth = config["local_smooth"]; + param.global_smooth = config["global_smooth"]; + param.elen_alpha = config["elen_alpha"]; + param.do_projection = config["do_projection"]; + param.with_cons = config["with_cons"]; + param.Lp = config["Lp"]; + param.save_meshes = config["save_meshes"]; + param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges + param.symdir_weight = config["symdir_weight"]; + param.alignment_weight = config["alignment_weight"]; + param.degenerate_weight = config["degenerate_weight"]; + param.fix_misaligned = config["fix_misaligned"]; + param.use_rref = config["use_rref"]; + // param.solver_type = config["solver_type"]; + param.cg_rel_err = config["cg_rel_err"]; + param.cg_iters = config["cg_iters"]; + + param.percentages = config["percentages"].get>(); + param.percentage_target = config["percentage_target"]; + param.percentage_target_value = config["percentage_target_value"]; + param.save_percentages_meshes = config["save_percentages_meshes"]; + + param.E_abs_err = config["E_abs_err"]; + param.E_rel_err = config["E_rel_err"]; + param.diff_err = config["diff_err"]; + param.grad_abs_err = config["grad_abs_err"]; + param.grad_rel_err = config["grad_rel_err"]; + + param.precompute_seamless = config["precompute_seamless"]; + param.projected_newton = config["projected_newton"]; + param.soft_max = config["soft_max"]; + param.t = config["t"]; + param.precompute_seamless = config["precompute_seamless"]; + + param.percentage_target_converge = config["percentage_target_converge"]; + param.max_grad_abs_converge = config["max_grad_abs_converge"]; + param.max_grad_rel_converge = config["max_grad_rel_converge"]; + param.energy_diff_converge = config["energy_diff_converge"]; + param.use_worst_n_energy_in_ls = config["use_worst_n_energy_in_ls"]; + param.E_abs_converge = config["E_abs_converge"]; + param.E_rel_converge = config["E_rel_converge"]; + + param.last_screenshot_after_optimization = config["last_screenshot_after_optimization"]; + param.screenshot_interval = config["screenshot_interval"]; + param.output_dir_for_screenshots = config["output_dir_for_screenshots"]; + param.uv_scale_for_screenshots = config["uv_scale_for_screenshots"]; + param.angle_to_rotate_model_for_screenshots = config["angle_to_rotate_model_for_screenshots"]; + param.screenshot_during_optimization = config["screenshot_during_optimization"]; + + param.degenerate_vertices_preconditioner = config["degenerate_vertices_preconditioner"]; + param.precond_dim = config["precond_dim"]; + param.triangle_threshold = config["triangle_threshold"]; + + return param; +} + +Eigen::MatrixXd optimize_aligned_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& thetas, + const Eigen::MatrixXi& period_jumps, + const Eigen::MatrixXi& FE_init, + const Eigen::MatrixXi& ME, + const nlohmann::json& config, + bool fix_boundary +) { + SymDir::Parameters param = read_parameters(config); + param.fix_boundary = fix_boundary; + + igl::Timer timer; + double time = 0; + timer.start(); + MeshCutter meshcutter(V_init, uv, F_init, F); + auto [V, EE] = meshcutter.cut_mesh(); + Eigen::MatrixXi FE(0, 0); + time = timer.getElapsedTime(); + if (param.do_feature_alignment) + { + // Loading the feature edge constraints + FE = meshcutter.reindex_feature_edges(FE_init); + } + double cons_residual = check_constraints(EE, FE, uv, F); + spdlog::info("Initial constraints error {}", cons_residual); + + Eigen::MatrixXi new_F; + Eigen::MatrixXd new_V, new_uv; + SymDir::ExtremeOpt extremeopt(V, F); + extremeopt.m_params = param; + + extremeopt.create_mesh(V, F, uv); + extremeopt.set_v_map(F_init, F); + + if (extremeopt.m_params.with_cons) + { + std::vector> EE_e = transform_EE(F, EE); + std::vector> FE_e; + if (extremeopt.m_params.do_feature_alignment) { + FE_e = transform_FE(F, FE); + } + extremeopt.init_constraints(EE_e); + extremeopt.EE = EE; + extremeopt.FE = FE; + extremeopt.ME = ME; + } + + //extremeopt.view(); + extremeopt.comb_matchings(reference_field, thetas, period_jumps); + + Eigen::MatrixXi F_opt = F; + Eigen::MatrixXd uv_opt; + extremeopt.do_optimization_without_log(); + extremeopt.export_mesh(V, F_opt, uv_opt); + + return uv_opt; +} + +#endif + +Eigen::MatrixXi tag_cone_corners( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT, + const std::vector& feature_face_edges +) { + // get mask of feature corners + int num_faces = FT.rows(); + int num_vertices = V.rows(); + Eigen::MatrixXi is_feature = compute_mask_from_face_edges(num_faces, feature_face_edges); + + // get triangle adjacency for mesh + Eigen::MatrixXi TT, TTi; + igl::triangle_triangle_adjacency(F, TT, TTi); + + // get unioned vertices, split by features + int num_halfedges = 3 * num_faces; + UnionFind cut_vertices(num_halfedges); + //Eigen::MatrixXi is_boundary = Eigen::MatrixXi::Zero(num_faces, 3); + Eigen::VectorXi is_boundary = Eigen::VectorXi::Zero(num_vertices); + for (int f = 0; f < num_faces; ++f) { + for (int i = 0 ; i < 3; ++i) + { + int l = (i + 2) % 3; + + // mark feature edges + if (is_feature(f, l)) + { + //is_boundary(f, i) = 1; + is_boundary(F(f, i)) = 1; + } + // union tips across edge otherwise + else + { + // halfedge with F(f, i) at tip + int hki = 3 * f + i; + + // get halfedge rotated clockwise, using libigls halfedge base indexing + int f_opp = TT(f, i); + int j = TTi(f, i); // index for halfedge opposite hij + int k = (j + 1) % 3; // index for halfedge in f pointing to vi + int hji = 3 * f_opp + k; + cut_vertices.union_sets(hki, hji); + } + } + } + + // compute cone angles of unioned vertices + int num_cut_vertices = cut_vertices.count_sets(); + std::vector set_index = cut_vertices.index_sets(); + Eigen::MatrixXd corner_angles; + igl::internal_angles(uv, FT, corner_angles); + VectorX cone_angles = VectorX::Zero(num_cut_vertices); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + for (int i = 0; i < 3; ++i) + { + int vi = set_index[3 * fijk + i]; + cone_angles[vi] += corner_angles(fijk, i); + } + } + + Eigen::MatrixXi vertex_indices(num_faces, 3); + Eigen::MatrixXd halfedge_tip_angles = Eigen::MatrixXd::Zero(num_faces, 3); + Eigen::MatrixXi is_cone = Eigen::MatrixXi::Zero(num_faces, 3); + for (int f = 0; f < num_faces; ++f) { + for (int i = 0 ; i < 3; ++i) + { + int h = 3 * f + i; + int v = set_index[h]; + vertex_indices(f, i) = v; + halfedge_tip_angles(f, i) += cone_angles[v]; + } + } + + int num_uv_vertices = uv.rows(); + std::vector is_uv_cone(num_uv_vertices, false); + Eigen::VectorXi is_uv_cone_mask = Eigen::VectorXi::Zero(num_uv_vertices); + for (int f = 0; f < num_faces; ++f) { + for (int i = 0 ; i < 3; ++i) + { + int v = F(f, i); + int vt = FT(f, i); + if ((!is_boundary[v]) && (!float_equal(halfedge_tip_angles(f, i), 2 * PI))) + { + is_cone(f, i) = 1; + is_uv_cone[vt] = true; + is_uv_cone_mask[vt] = 1; + } + else if ((is_boundary[v]) && (!float_equal(halfedge_tip_angles(f, i), PI))) + { + is_cone(f, i) = 1; + is_uv_cone[vt] = true; + is_uv_cone_mask[vt] = 1; + } + } + } + + bool show_uv_cones = false; + if (show_uv_cones) + { + polyscope::init(); + + // closed mesh + std::string mesh_handle = "mesh"; + polyscope::registerSurfaceMesh(mesh_handle, V, F); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "vertex indices", + vertex_indices.transpose().reshaped()) + ->setEnabled(true); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "3D vertex indices", + F.transpose().reshaped()); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "cone angles", + halfedge_tip_angles.transpose().reshaped()); + polyscope::getSurfaceMesh(mesh_handle) + ->addVertexScalarQuantity( + "is boundary", + is_boundary.transpose().reshaped()) + ->setColorMap("coolwarm"); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "is cone", + is_cone.transpose().reshaped()) + ->setColorMap("coolwarm"); + + // cut mesh along seams + Eigen::MatrixXd V_cut; + cut_mesh_along_parametrization_seams(V, F, uv, FT, V_cut); + mesh_handle = "cut mesh"; + polyscope::registerSurfaceMesh(mesh_handle, V_cut, FT); + polyscope::getSurfaceMesh(mesh_handle) + ->addVertexScalarQuantity( + "is uv cone", + is_uv_cone_mask.transpose().reshaped()) + ->setEnabled(true); + + polyscope::show(); + } + + return is_cone; +} + + +int main(int argc, char* argv[]) +{ + spdlog::set_level(spdlog::level::info); + + // Get command line arguments + CLI::App app{"Generate a feature aligned parametrization."}; + std::string mesh = ""; + std::string input_dir = "./"; + std::string output_dir = "./"; + std::filesystem::path current_dir = std::filesystem::path(__FILE__).parent_path(); + std::filesystem::path input_json = current_dir / "symdir.json"; + bool use_uniform_bc = false; + bool optimize = false; + bool use_free_cones = false; + + int full_itr = 100; + NewtonParameters alg_params; + int max_itr = alg_params.max_itr; + spdlog::level::level_enum log_level = spdlog::level::info; + + // IO Parameters + bool use_existing_field = false; + bool show_field = false; + bool show_parameterization = false; + app.add_option("--name", mesh, "Mesh name (without obj suffix, e.g., fandisk)")->required(); + app.add_option("-i,--input", input_dir, "Input directory")->check(CLI::ExistingDirectory)->required(); + app.add_option("-o,--output", output_dir, "Output directory"); + app.add_flag("--use_existing_field", use_existing_field, "Use precomputed field at the input directory"); + app.add_flag("--use_uniform_bc", use_uniform_bc, "Use uniform barycentric coordinates"); + app.add_flag("--use_free_cones", use_free_cones, "Use free cones and remove holonomy constraints"); + app.add_flag("--optimize", optimize, "Optimize uv coordinates"); + app.add_flag("--show_field", show_field, "Show field constraints"); + app.add_flag("--show_parameterization", show_parameterization, "Show aligned parameterization"); + app.add_option("--full_itr", full_itr, "Initial iterations of full (potentially unsatisfiable) constraints"); + app.add_option("--log_level", log_level, "Level of logging") + ->transform(CLI::CheckedTransformer(log_level_map, CLI::ignore_case)); + + CLI11_PARSE(app, argc, argv); + spdlog::set_level(log_level); + + // Marked Metric Parameters + add_newton_parameters(app, alg_params); + CLI11_PARSE(app, argc, argv); + + std::filesystem::create_directory(output_dir); + + // create filepaths for input data + std::string mesh_filename = join_path(input_dir, mesh + ".obj"); + std::string feature_filename = join_path(input_dir, mesh + "_features"); + std::string hard_feature_filename = join_path(input_dir, mesh + "_hard_features"); + std::string field_filename = join_path(input_dir, mesh + ".ffield"); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("optimizing mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // Get features and field + std::vector feature_edges, hard_feature_edges; + Eigen::MatrixXd reference_field; + Eigen::VectorXd theta; + Eigen::MatrixXd kappa; + Eigen::MatrixXi period_jump; + if (use_existing_field) + { + spdlog::info("loading feature edges"); + feature_edges = load_feature_edges(feature_filename); + hard_feature_edges = load_feature_edges(hard_feature_filename); + + spdlog::info("loading constraints"); + std::tie(reference_field, theta, kappa, period_jump) = Penner::Field::load_frame_field(field_filename); + } + else { + // refine input mesh + std::tie(V, F, feature_edges, hard_feature_edges) = generate_refined_feature_mesh(V, F, false); + FeatureFinder feature_finder(V, F); + feature_finder.mark_features(feature_edges); + auto[V_cut, F_cut, V_map, F_is_feature] = feature_finder.generate_feature_cut_mesh(); + + int radius = 5; + Scalar rel_anisotropy=0.9; + Scalar abs_anisotropy=0.2; + Scalar bb_diag = igl::bounding_box_diagonal(V); + auto [direction, is_fixed_direction] = Penner::Field::compute_field_direction( + V_cut, + F_cut, + radius, + abs_anisotropy / bb_diag, + rel_anisotropy); + MarkedMetricParameters marked_metric_params; + marked_metric_params.remove_trivial_torus = false; // FIXME + marked_metric_params.use_log_length = true; + marked_metric_params.use_initial_zero = false; + CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, {}); + cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); + std::tie(reference_field, theta, kappa, period_jump) = cut_metric_generator.get_field(); + } + if (show_field) view_cross_field(V, F, reference_field, theta, kappa, period_jump); + + // get optimized metric + spdlog::info("projecting to feature constraints"); + alg_params.output_dir = output_dir; + alg_params.error_eps = 1e-10; + alg_params.solver = "ldlt"; + alg_params.do_reduction = true; + MarkedMetricParameters marked_metric_params; + if (use_free_cones) + { + // TODO migrate some of this code to AlignedMetricGenerator + marked_metric_params.use_free_cones = true; + marked_metric_params.max_boundary_constraints = 0; + marked_metric_params.max_loop_constraints = 0; + } + AlignedMetricGenerator aligned_metric_generator( + V, + F, + feature_edges, + hard_feature_edges, + reference_field, + theta, + kappa, + period_jump, + marked_metric_params); + + // run iterations of fully optimized method + alg_params.max_itr = full_itr; + aligned_metric_generator.optimize_full(alg_params); + + // only works for feature alignment + if (!use_free_cones) + { + // run iterations of relaxed optimization + alg_params.max_itr = max_itr; + aligned_metric_generator.optimize_relaxed(alg_params); + } + else + { + aligned_metric_generator.is_axis_aligned = false; + } + + aligned_metric_generator.parameterize(false, use_uniform_bc); + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = aligned_metric_generator.get_parameterization(); + auto [feature_face_edges, misaligned_edges] = aligned_metric_generator.get_refined_features(); + auto feature_edges_r = compute_face_edge_endpoints(feature_face_edges, F_r); + auto misaligned_edges_r = compute_face_edge_endpoints(misaligned_edges, F_r); + auto [reference_field_r, theta_r, kappa_r, period_jump_r] = aligned_metric_generator.get_refined_field(); + + // for free cones, mark all feature edges + if (use_free_cones) + { + feature_edges_r = feature_edges; + } + + // Optionally optimize parameterization + if (optimize) + { +#if USE_UV_OPTIMIZATION + std::ifstream js_in(input_json); + nlohmann::json config = nlohmann::json::parse(js_in); + config["model"] = mesh; + + // get feature edges + int num_features = feature_edges_r.size(); + Eigen::MatrixXi FE(num_features, 2); + for (int eij = 0; eij < num_features; ++eij) + { + FE(eij, 0) = feature_edges_r[eij][0]; + FE(eij, 1) = feature_edges_r[eij][1]; + } + + // get misaliged edges + int num_misaligned = misaligned_edges_r.size(); + Eigen::MatrixXi ME(num_misaligned, 2); + for (int eij = 0; eij < num_misaligned; ++eij) + { + ME(eij, 0) = misaligned_edges_r[eij][0]; + ME(eij, 1) = misaligned_edges_r[eij][1]; + } + + bool fix_boundary = use_free_cones; // fix boundary if using free cones + uv_r = optimize_aligned_parameterization( + V_r, + F_r, + uv_r, + FT_r, + reference_field_r, + theta_r, + period_jump_r, + FE, + ME, + config, + fix_boundary); +#else + spdlog::warn("uv optimization disabled"); +#endif + } + + if (show_parameterization) view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "refined mesh", true); + + std::string output_filename = join_path(output_dir, mesh+"_opt.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + write_mesh_edges(output_filename, feature_edges_r); + output_filename = join_path(output_dir, mesh+".ffield"); + Penner::Field::write_frame_field(output_filename, reference_field_r, theta_r, kappa_r, period_jump_r); + output_filename = join_path(output_dir, mesh+"_fn_to_f"); + write_vector(fn_to_f_r, output_filename); + + // get uv cone vertices + Eigen::MatrixXi is_cone_corner = tag_cone_corners(V_r, F_r, uv_r, FT_r, feature_face_edges); + //std::vector uv_cone_vertices; + //convert_boolean_array_to_index_vector(is_cone_uv, uv_cone_vertices); + output_filename = join_path(output_dir, mesh+"_uv_cone_corners"); + write_integer_matrix(is_cone_corner, output_filename, " "); + + //std::string output_filename = join_path(output_dir, "optimized_corner_coords"); + //write_matrix(opt_corner_coords, output_filename, " "); + + + +} diff --git a/src/app/parametrize_seamless.cpp b/src/app/parametrize_seamless.cpp new file mode 100644 index 0000000..f3d523a --- /dev/null +++ b/src/app/parametrize_seamless.cpp @@ -0,0 +1,411 @@ +/********************************************************************************* +* This file is part of reference implementation of SIGGRAPH 2024 Paper * +* `Seamless Parametrization in Penner Coordinates` * +* v1.0 * +* * +* The MIT License * +* * +* Permission is hereby granted, free of charge, to any person obtaining a * +* copy of this software and associated documentation files (the "Software"), * +* to deal in the Software without restriction, including without limitation * +* the rights to use, copy, modify, merge, publish, distribute, sublicense, * +* and/or sell copies of the Software, and to permit persons to whom the * +* Software is furnished to do so, subject to the following conditions: * +* * +* The above copyright notice and this permission notice shall be included in * +* all copies or substantial portions of the Software. * +* * +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * +* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE * +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * +* IN THE SOFTWARE. * +* * +* Author(s): * +* Ryan Capouellez, Denis Zorin, * +* Courant Institute of Mathematical Sciences, New York University, USA * +* * * +*********************************************************************************/ +#include "field/frame_field.h" +#include "field/intrinsic_field.h" +#include "holonomy/interface.h" +#include "holonomy/holonomy/newton.h" +#include "holonomy/holonomy/cones.h" +#include "holonomy/core/viewer.h" +#include "parametrization/refinement.h" +#include "parametrization/parametrize.h" + +#include +#include +#include +#include + +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Holonomy; + + + +Eigen::MatrixXd optimize_seamless_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv_init, + const Eigen::MatrixXi& FT_init, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& thetas, + const Eigen::MatrixXi& period_jumps, + const nlohmann::json& config +) +{ + Eigen::MatrixXd uv = uv_init; + Eigen::MatrixXi F = FT_init; + + SymDir::Parameters param; + param.max_iters = config["max_iters"]; // iterations + param.smooth_only_iters = config["smooth_only_iters"]; + param.E_target = config["E_target"]; // Energy target + param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation + param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation + // do global/local smooth (local smooth does not optimize boundary vertices) + param.local_smooth = config["local_smooth"]; + param.global_smooth = config["global_smooth"]; + param.elen_alpha = config["elen_alpha"]; + param.do_projection = config["do_projection"]; + param.with_cons = config["with_cons"]; + param.Lp = config["Lp"]; + param.save_meshes = config["save_meshes"]; + param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges + param.symdir_weight = config["symdir_weight"]; + param.alignment_weight = config["alignment_weight"]; + param.fix_misaligned = config["fix_misaligned"]; + param.use_rref = config["use_rref"]; + param.model_name = config["model"]; + param.percentage_target_converge = false; + param.use_worst_n_energy_in_ls = false; + + MeshCutter meshcutter(V_init, uv, F_init, F); + + auto [V, EE] = meshcutter.cut_mesh(); + + Eigen::MatrixXi FE_init; + Eigen::MatrixXi FE(0, 0); + Eigen::MatrixXi ME(0, 0); + if (param.do_feature_alignment) + { + // TODO + // Loading the feature edge constraints + //FE_init = meshcutter.load_feature_edges(input_file); + //FE = meshcutter.reindex_feature_edges(FE_init); + //if (param.fix_misaligned) + //{ + // std::string misaligned_file = input_dir + "/" + model + "_misaligned_edges"; + // ME = meshcutter.load_misaligned_edges(misaligned_file); + //} + } + + double cons_residual = check_constraints(EE, FE, uv, F); + spdlog::info("Initial constraints error {}", cons_residual); + + Eigen::MatrixXi new_F; + Eigen::MatrixXd new_V, new_uv; + SymDir::ExtremeOpt extremeopt(V, F); + extremeopt.m_params = param; + + extremeopt.create_mesh(V, F, uv); + + nlohmann::json opt_log; + opt_log["model_name"] = config["model"]; + opt_log["args"] = config; + + if (extremeopt.m_params.with_cons) + { + std::vector> EE_e = transform_EE(F, EE); + std::vector> FE_e; + if (extremeopt.m_params.do_feature_alignment) { + FE_e = transform_FE(F, FE); + } + extremeopt.init_constraints(EE_e); + extremeopt.EE = EE; + extremeopt.FE = FE; + extremeopt.ME = ME; + } + extremeopt.comb_matchings(reference_field, thetas, period_jumps); + extremeopt.do_optimization(opt_log); + + extremeopt.export_mesh(V, F, uv); + cons_residual = check_constraints(EE, FE, uv, F); + spdlog::info("Final constraints error {}", cons_residual); + + if (extremeopt.m_params.with_cons) extremeopt.export_EE(EE); + + return uv; +} + + +int main(int argc, char* argv[]) +{ + std::map log_level_map { + {"trace", spdlog::level::trace}, + {"debug", spdlog::level::debug}, + {"info", spdlog::level::info}, + {"warn", spdlog::level::warn}, + {"critical", spdlog::level::critical}, + {"off", spdlog::level::off}, + }; + + // Get command line arguments + CLI::App app{"Generate a constrained seamless parametrization."}; + std::filesystem::path mesh_filename = ""; + std::filesystem::path Th_hat_filename = ""; + std::filesystem::path field_filename = ""; + std::filesystem::path output_dir = "./"; + std::filesystem::path input_json = "../scripts/symdir.json"; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + app.add_option("--cones", Th_hat_filename, "Cone angle filepath") + ->check(CLI::ExistingFile); + app.add_option("--field", field_filename, "Rotation field one form") + ->check(CLI::ExistingFile); + app.add_option("-o,--output", output_dir, "Output directory"); + app.add_option("--json", input_json, "JSON specification for uv optimization"); + + // Marked Metric Parameters + // NOTE: Only several parameters are exposed to the CLI + MarkedMetricParameters marked_metric_params; + NewtonParameters alg_params; + //bool use_free_cones = false; + bool optimize = false; + app.add_flag( + "--remove_loop_constraints", + marked_metric_params.remove_loop_constraints, + "Remove homology basis loop holonomy constraints"); + app.add_option("--max_itr", alg_params.max_itr, "Upper bound for newton iterations") + ->check(CLI::NonNegativeNumber); + app.add_option("--error_eps", alg_params.error_eps, "Error convergence threshold") + ->check(CLI::NonNegativeNumber); + app.add_flag("--use_initial_zero", marked_metric_params.use_initial_zero, "Use zero coordinates"); + app.add_flag("--use_free_cones", marked_metric_params.use_free_cones, "Let cones have free angles"); + app.add_flag("--optimize", optimize, "Optimize uv map for distortion and field alignment"); + alg_params.output_dir = output_dir; + alg_params.error_log = true; + + // Miscellaneous + double max_triangle_quality = 0.; + bool use_delaunay = false; + bool fit_field = false; + bool show_parameterization = false; + spdlog::level::level_enum log_level = spdlog::level::info; + app.add_option( + "--max_triangle_quality", + max_triangle_quality, + "Maximum allowed triangle quality (0 for unbounded)") + ->check(CLI::NonNegativeNumber); + app.add_flag("--use_delaunay", use_delaunay, "Use Delaunay connectivity"); + app.add_flag("--fit_field", fit_field, "Fit new cross field"); + app.add_flag("--show_parameterization", show_parameterization, "Show final paramaterization"); + app.add_option("--log_level", log_level, "Level of logging") + ->transform(CLI::CheckedTransformer(log_level_map, CLI::ignore_case)); + + CLI11_PARSE(app, argc, argv); + spdlog::set_level(log_level); + std::string mesh = mesh_filename.stem(); + std::filesystem::create_directory(output_dir); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Using mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // Get input angles from cross field or file + std::string field_format = field_filename.extension(); + MarkedPennerConeMetric marked_metric; + std::vector vtx_reindex; + std::vector Th_hat; + VectorX rotation_form(F.rows() * 3); + Eigen::MatrixXd reference_field; + Eigen::VectorXd theta; + Eigen::MatrixXd kappa; + Eigen::MatrixXi period_jump; + if ((fit_field) || (field_filename == "")) { + FieldParameters field_params; + field_params.use_principal_directions = true; + std::tie(reference_field, theta, kappa, period_jump) = generate_frame_field(V, F, field_params); + std::tie(marked_metric, vtx_reindex, rotation_form, Th_hat) = generate_metric_from_field(V, F, theta, kappa, period_jump, marked_metric_params); + + //std::tie(rotation_form, Th_hat) = generate_intrinsic_rotation_form(V, F, field_params); + //std::string mesh_name = std::filesystem::path(mesh_filename).filename().replace_extension(); + //write_vector(Th_hat, join_path(output_dir, mesh_name + "_Th_hat")); + } + else if (field_format == ".ffield") + { + auto [m, vtx_reindex] = generate_mesh(V, F, V, F, Th_hat); + std::tie(reference_field, theta, kappa, period_jump) = load_frame_field(field_filename); + std::tie(marked_metric, vtx_reindex, rotation_form, Th_hat) = generate_metric_from_field(V, F, theta, kappa, period_jump, marked_metric_params); + } + else if (field_format == ".rosy") + { + auto [m, vtx_reindex] = generate_mesh(V, F, V, F, Th_hat); + auto rosy_field = load_rosy_field(field_filename); + + // initialize feild generator with the given theta + int num_faces = F.rows(); + Eigen::VectorXi reference_corner(num_faces); + theta.resize(num_faces); + kappa.resize(num_faces, 3); + period_jump.resize(num_faces, 3); + IntrinsicNRosyField field_generator; + field_generator.min_angle = M_PI / 2.; + field_generator.use_trivial_boundary = true; + field_generator.initialize(m); + field_generator.get_field(m, vtx_reindex, F, reference_corner, theta, kappa, period_jump); + theta = infer_theta(V, F, reference_corner, rosy_field); + field_generator.set_field(m, vtx_reindex, F, theta, kappa, period_jump); + field_generator.compute_principal_matchings(m); + + // get field + field_generator.get_field(m, vtx_reindex, F, reference_corner, theta, kappa, period_jump); + reference_field = generate_reference_field(V, F, reference_corner); + rotation_form = field_generator.compute_rotation_form(m); + Th_hat = generate_cones_from_rotation_form(m, vtx_reindex, rotation_form); + std::vector free_cones = {}; + std::tie(marked_metric, vtx_reindex) = + generate_marked_metric(V, F, V, F, Th_hat, rotation_form, free_cones, marked_metric_params); + } + else { + // Get input rotation + std::vector rotation_form_vec; + spdlog::info("Using rotation_form at {}", field_filename); + read_vector_from_file(field_filename, rotation_form_vec); + convert_std_to_eigen_vector(rotation_form_vec, rotation_form); + + if (Th_hat_filename != "") + { + spdlog::info("Using cone angles at {}", Th_hat_filename); + read_vector_from_file(Th_hat_filename, Th_hat); + } + else + { + auto [m, vtx_reindex] = generate_mesh(V, F, V, F, Th_hat); + Th_hat = generate_cones_from_rotation_form(m, vtx_reindex, rotation_form); + } + + // Generate initial marked mesh for optimization + std::vector free_cones = {}; + std::tie(marked_metric, vtx_reindex) = + generate_marked_metric(V, F, V, F, Th_hat, rotation_form, free_cones, marked_metric_params); + } + + // Check for invalid cones and fix any issues + if (!validate_cones(marked_metric)) { + spdlog::info("Fixing invalid cones"); + fix_cones(marked_metric); + } + + // add constraints to viewer + view_rotation_form(marked_metric, vtx_reindex, V, rotation_form, Th_hat, "rotation", false); + + // Make initial mesh Delaunay if desired + std::vector flip_seq = {}; + if (use_delaunay) { + marked_metric.make_discrete_metric(); + flip_seq = marked_metric.get_flip_sequence(); + marked_metric.reset(); + } + + // Regularize + if (max_triangle_quality > 0.) { + regularize_metric(marked_metric, max_triangle_quality); + } + + // Optimize metric + spdlog::info("Beginning optimization"); + auto opt_marked_metric = optimize_metric_angles(marked_metric, alg_params); + + // Undo any initial flips to make Delaunay + for (auto iter = flip_seq.rbegin(); iter != flip_seq.rend(); ++iter) { + int h = *iter; + spdlog::trace("Flipping {} cw", h); + opt_marked_metric.flip_ccw(h, true); + opt_marked_metric.flip_ccw(h, true); + opt_marked_metric.flip_ccw(h, true); + } + + // Write the output metric coordinates + std::string output_filename; + output_filename = join_path(output_dir, "optimized_metric_coords"); + write_vector(opt_marked_metric.get_reduced_metric_coordinates(), output_filename); + + // Generate full overlay + std::vector is_cut = {}; + auto vf_res = generate_VF_mesh_from_metric( + V, + F, + Th_hat, + marked_metric, + opt_marked_metric.get_metric_coordinates(), + is_cut, + false); + Eigen::MatrixXd V_o = std::get<1>(vf_res); + Eigen::MatrixXi F_o = std::get<2>(vf_res); + Eigen::MatrixXd uv_o = std::get<3>(vf_res); + Eigen::MatrixXi FT_o = std::get<4>(vf_res); + std::vector fn_to_f_o = std::get<7>(vf_res); + std::vector> endpoints_o = std::get<8>(vf_res); + + // Generate minimal refinement + RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); + refinement_mesh.refine_mesh(); + refinement_mesh.simplify_mesh(); + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = refinement_mesh.get_VF_mesh(); + auto [reference_field_r, theta_r, kappa_r, period_jump_r] = refine_frame_field( + F_r, + FT_r, + fn_to_f_r, + endpoints_r, + F, + reference_field, + theta, + kappa, + period_jump); + + bool write_field = true; + std::string ffield_file = join_path(output_dir, mesh+".ffield"); + if (write_field) + { + write_frame_field(ffield_file, reference_field_r, theta_r, kappa_r, period_jump_r); + } + + // Optionally optimize parameterization + if (optimize) + { + std::ifstream js_in(input_json); + nlohmann::json config = nlohmann::json::parse(js_in); + config["model"] = mesh; + uv_r = optimize_seamless_parameterization( + V_r, + F_r, + uv_r, + FT_r, + reference_field_r, + theta_r, + period_jump_r, + config); + } + + if (show_parameterization) view_triangulation(V_o, F_o, fn_to_f_o, endpoints_o, "refinement", false); + if (show_parameterization) view_seamless_parameterization(V_o, F_o, uv_o, FT_o, "overlay", false); + if (show_parameterization) view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "simplified"); + + // Write the output mesh + output_filename = join_path(output_dir, mesh + "_param.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + +} diff --git a/src/app/symdir.json b/src/app/symdir.json new file mode 100644 index 0000000..0a649b8 --- /dev/null +++ b/src/app/symdir.json @@ -0,0 +1,60 @@ +{ + "max_iters": 500, + "projected_newton": true, + "max_time": 28800, + "smooth_only_iters": 30, + "do_feature_alignment": true, + "fix_misaligned": false, + "do_newton": true, + "do_swap": false, + "do_collapse": false, + "do_split": false, + "split_thresh": 2.1, + "local_smooth": false, + "global_smooth": true, + "use_envelope": false, + "global_upsample": 0, + "ls_iters": 100, + "E_target": 0.0, + "elen_alpha": 0.03, + "do_projection": false, + "with_cons": true, + "save_meshes": false, + "Lp": 2.0, + "symdir_weight": 1e-3, + "alignment_weight": 1.0, + "degenerate_weight": 0.0, + "use_rref": true, + "percentage_target": 0, + "percentage_target_value": 0.0, + "p_energy": 2, + "precompute_seamless": true, + "E_worst_2_target": 1.0, + "E_rel_err": 1e-6, + "E_abs_err": 1e-6, + "grad_abs_err": 1e-4, + "grad_rel_err": 1e-4, + "cg_rel_err": 1e-2, + "cg_iters": 10000, + "diff_err": 1e-4, + "soft_max": false, + "t": 5.0, + "use_worst_n_energy_in_ls": false, + "percentage_target_converge": false, + "energy_diff_converge": true, + "max_grad_abs_converge": true, + "max_grad_rel_converge": false, + "E_abs_converge": false, + "E_rel_converge": false, + "last_screenshot_after_optimization": false, + "screenshot_interval": 1, + "output_dir_for_screenshots": "screenshots", + "uv_scale_for_screenshots": 0.005, + "angle_to_rotate_model_for_screenshots": 180, + "screenshot_during_optimization": false, + "degenerate_vertices_preconditioner": false, + "precond_dim": 2, + "triangle_threshold": 5, + "percentages": [10.0, 50.0, 100.0], + "save_percentages_meshes": false +} diff --git a/src/app/util.h b/src/app/util.h new file mode 100644 index 0000000..e1031ef --- /dev/null +++ b/src/app/util.h @@ -0,0 +1,71 @@ +#pragma once + +#include "feature/core/common.h" +#include "holonomy/interface.h" +#include "holonomy/holonomy/newton.h" + +#include +#include + +#include + +namespace Penner { + +const std::map log_level_map { + {"trace", spdlog::level::trace}, + {"debug", spdlog::level::debug}, + {"info", spdlog::level::info}, + {"warn", spdlog::level::warn}, + {"critical", spdlog::level::critical}, + {"off", spdlog::level::off}, +}; + +inline void add_marked_metric_parameters( + CLI::App& app, + Holonomy::MarkedMetricParameters& marked_metric_params +) { + app.add_flag( + "--use_initial_zero", + marked_metric_params.use_initial_zero, + "Use initial zero coordinates"); + app.add_flag( + "--remove_loop_constraints", + marked_metric_params.remove_loop_constraints, + "Remove homology basis loop holonomy constraints"); + app.add_flag( + "--remove_symmetry", + marked_metric_params.remove_symmetry, + "Remove symmetry structure from mesh"); +} + +inline void add_newton_parameters( + CLI::App& app, + Holonomy::NewtonParameters& alg_params +) { + app.add_flag( + "--do_reduction", + alg_params.do_reduction, + "Reduce line step size to bounded range for stability"); + app.add_option( + "--solver", + alg_params.solver, + "Solver to use for linear systems"); + app.add_flag( + "--reset_lambda", + alg_params.reset_lambda, + "Start with lambda = lambda0 for each newton iteration"); + app.add_option("--max_itr", alg_params.max_itr, "Upper bound for newton iterations") + ->check(CLI::NonNegativeNumber); + app.add_option( + "--bound_norm_thres", + alg_params.bound_norm_thres, + "Line step threshold to stop bounding the error norm") + ->check(CLI::Number); + app.add_option( + "--checkpoint_frequency", + alg_params.checkpoint_frequency, + "Number of iterations to wait between checkpoints (nonpositive for never)") + ->check(CLI::Number); +} + +} // namespace Penner diff --git a/src/app/view_quad_mesh.cpp b/src/app/view_quad_mesh.cpp new file mode 100644 index 0000000..fef5497 --- /dev/null +++ b/src/app/view_quad_mesh.cpp @@ -0,0 +1,30 @@ +#include "holonomy/core/common.h" +#include "feature/core/viewer.h" +#include "feature/core/quads.h" + +#include +#include + +using namespace Penner; +using namespace Penner::Feature; + +int main(int argc, char* argv[]) +{ + // Get command line arguments + CLI::App app{"View a quad mesh"}; + std::string mesh_filename = ""; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::debug); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Using mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + view_quad_mesh(V, F); +} diff --git a/src/app/view_seamless_uv.cpp b/src/app/view_seamless_uv.cpp new file mode 100644 index 0000000..890e57e --- /dev/null +++ b/src/app/view_seamless_uv.cpp @@ -0,0 +1,104 @@ +#include "holonomy/core/common.h" +#include "holonomy/core/viewer.h" +#include "feature/feature/error.h" +#include "feature/core/io.h" +#include "feature/surgery/cut_mesh_layout.h" +#include "feature/interface.h" + +#include "util/vf_mesh.h" + +#include +#include +#include +#include "polyscope/surface_mesh.h" +#include "polyscope/point_cloud.h" +#include "polyscope/curve_network.h" + +using namespace Penner; +using namespace Penner::Holonomy; +using namespace Penner::Feature; + + + +std::tuple generate_cone_vertices( + const Eigen::MatrixXd& V, + const std::vector& Th_hat) +{ + int num_vertices = Th_hat.size(); + std::vector cone_indices; + cone_indices.reserve(num_vertices); + for (int vi = 0; vi < num_vertices; ++vi) { + if (float_equal(Th_hat[vi], 2 * M_PI, 1e-8)) continue; + cone_indices.push_back(vi); + } + + int num_cones = cone_indices.size(); + Eigen::MatrixXd cone_positions(num_cones, 3); + Eigen::VectorXd cone_values(num_cones); + for (int i = 0; i < num_cones; ++i) { + int vi = cone_indices[i]; + cone_positions.row(i) = V.row(vi); + cone_values[i] = (double)(Th_hat[vi]) - (2 * M_PI); + } + + return std::make_tuple(cone_positions, cone_values); +} + +int main(int argc, char* argv[]) +{ + // Get command line arguments + CLI::App app{"View a quad mesh"}; + std::string mesh_filename = ""; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::debug); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Using mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + std::vector E = load_mesh_edges(mesh_filename); + + Eigen::MatrixXi F_is_seam = find_seams(F, FT); + auto [V_seams, E_seams] = generate_edges(V, F, F_is_seam); + + std::vector feature_alignment = compute_feature_alignment(F, uv, FT, E); + spdlog::info("Maximum feature alignment: {}", vector_max(feature_alignment)); + view_seamless_parameterization(V, F, uv, FT, "seamless mesh", false); + //VectorX cone_angles = compute_cone_angles(V, F, uv, FT); + //std::vector cone_angles_vec; + //convert_eigen_to_std_vector(cone_angles, cone_angles_vec); + + //auto [uv_n, FT_n] = generate_connected_parameterization(V, F, uv, FT); + //view_seamless_parameterization(V, F, uv_n, FT_n, "connected seamless mesh", false); + + //std::vector feature_alignment = compute_feature_alignment(F, uv_n, FT_n, E); + //spdlog::info("Maximum feature alignment: {}", vector_max(feature_alignment)); + + //auto [cone_positions, cone_values] = generate_cone_vertices(V, cone_angles_vec); + bool show_layout = false; + if (show_layout) polyscope::registerSurfaceMesh2D("layout", uv, FT); + //polyscope::getSurfaceMesh("base mesh") + // ->addVertexScalarQuantity("cone angles", cone_angles); + //polyscope::registerPointCloud("cones", cone_positions); + //polyscope::getPointCloud("cones") + // ->addScalarQuantity("index", cone_values) + // ->setColorMap("coolwarm") + // ->setMapRange({-M_PI, M_PI}) + // ->setEnabled(true); + Eigen::MatrixXd NV; + Eigen::MatrixXi NE; + Eigen::VectorXi I; + //igl::remove_unreferenced(V, E, NV, NV, I); + polyscope::registerCurveNetwork("features", V, E); + bool show_seams = false; + if (show_seams) polyscope::registerCurveNetwork("seams", V_seams, E_seams); + + polyscope::show(); + +} From 8ab0a0f6787671b3259f1adb3a221cb6192d6dea Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Thu, 9 Jul 2026 16:37:36 -0400 Subject: [PATCH 09/17] Added executables and simplified interface --- CMakeLists.txt | 10 +- {src/app => app}/CMakeLists.txt | 29 +-- app/generate_aligned_field.cpp | 81 ++++++++ {src/app => app}/generate_field.cpp | 72 +------ {src/app => app}/optimize_metric.cpp | 10 +- {src/app => app}/optimize_shear.cpp | 5 +- {src/app => app}/parameterize_aligned.cpp | 32 ++- {src/app => app}/parametrize_seamless.cpp | 23 ++- {src/app => app}/symdir.json | 0 {src/app => app}/util.h | 0 {src/app => app}/view_quad_mesh.cpp | 2 - app/view_seamless.cpp | 71 +++++++ include/feature/feature/core/vf_corners.h | 9 + include/feature/feature/interface.h | 23 +++ include/field/field/cones.h | 20 ++ .../holonomy/marked_penner_cone_metric.h | 14 ++ include/holonomy/holonomy/interface.h | 14 +- include/metric/metric/cone_metric.h | 1 + include/metric/metric/interface.h | 92 +++++++++ include/util/util/vf_mesh.h | 15 +- src/CMakeLists.txt | 1 + src/app/generate_aligned_field.cpp | 163 --------------- src/app/view_seamless_uv.cpp | 104 ---------- src/feature/core/vf_corners.cpp | 13 ++ src/feature/interface.cpp | 60 ++++++ src/feature/surgery/cut_mesh_layout.cpp | 2 +- src/field/cones.cpp | 12 -- src/holonomy/holonomy/constraint.cpp | 9 + .../holonomy/marked_penner_cone_metric.cpp | 21 +- src/holonomy/interface.cpp | 140 +------------ src/metric/cone_metric.cpp | 5 + src/metric/interface.cpp | 192 ++++++++++++++++++ src/util/vf_mesh.cpp | 22 +- 33 files changed, 717 insertions(+), 550 deletions(-) rename {src/app => app}/CMakeLists.txt (79%) create mode 100644 app/generate_aligned_field.cpp rename {src/app => app}/generate_field.cpp (68%) rename {src/app => app}/optimize_metric.cpp (97%) rename {src/app => app}/optimize_shear.cpp (99%) rename {src/app => app}/parameterize_aligned.cpp (95%) rename {src/app => app}/parametrize_seamless.cpp (95%) rename {src/app => app}/symdir.json (100%) rename {src/app => app}/util.h (100%) rename {src/app => app}/view_quad_mesh.cpp (91%) create mode 100644 app/view_seamless.cpp create mode 100644 include/metric/metric/interface.h delete mode 100644 src/app/generate_aligned_field.cpp delete mode 100644 src/app/view_seamless_uv.cpp create mode 100644 src/metric/interface.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2924d3f..8deee1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,9 +36,14 @@ if (PROJECT_IS_TOP_LEVEL) option ( SUITESPARSE_USE_CUDA OFF ) endif() -set ( SUITESPARSE_ENABLE_PROJECTS "${SUITESPARSE_ENABLE_PROJECTS};suitesparse_config;cholmod" CACHE STRING "" FORCE) list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +# add needed suitesparse projects +set ( SUITESPARSE_ENABLE_PROJECTS "${SUITESPARSE_ENABLE_PROJECTS};suitesparse_config;cholmod" CACHE STRING "" FORCE) +if (USE_UV_OPTIMIZATION) + set ( SUITESPARSE_ENABLE_PROJECTS "${SUITESPARSE_ENABLE_PROJECTS};spqr;umfpack" CACHE STRING "" FORCE) +endif() + include(eigen) include(boost) @@ -172,3 +177,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Make main cpp library add_subdirectory(src) + +# Add executable files +add_subdirectory(app) diff --git a/src/app/CMakeLists.txt b/app/CMakeLists.txt similarity index 79% rename from src/app/CMakeLists.txt rename to app/CMakeLists.txt index 66afb74..ed6cc14 100644 --- a/src/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -34,24 +34,25 @@ parameterize_aligned.cpp ) target_link_libraries(parameterize_aligned PRIVATE PennerLib + SymmetricDirichletLib ApplicationUtilLib ) -add_executable(optimize_metric - optimize_metric.cpp -) -target_link_libraries(optimize_metric PRIVATE - PennerLib - CLI11::CLI11 -) +# add_executable(optimize_metric +# optimize_metric.cpp +# ) +# target_link_libraries(optimize_metric PRIVATE +# PennerLib +# CLI11::CLI11 +# ) -add_executable(optimize_shear - optimize_shear.cpp -) -target_link_libraries(optimize_shear PRIVATE - PennerLib - CLI11::CLI11 -) +# add_executable(optimize_shear +# optimize_shear.cpp +# ) +# target_link_libraries(optimize_shear PRIVATE +# PennerLib +# CLI11::CLI11 +# ) # only build visualization executables if visualization enabled if (NOT USE_MULTIPRECISION) diff --git a/app/generate_aligned_field.cpp b/app/generate_aligned_field.cpp new file mode 100644 index 0000000..fb39a78 --- /dev/null +++ b/app/generate_aligned_field.cpp @@ -0,0 +1,81 @@ +#include "field/frame_field.h" +#include "field/intrinsic_field.h" +#include "holonomy/core/viewer.h" +#include "feature/interface.h" +#include "feature/core/io.h" +#include "feature/core/viewer.h" +#include "feature/surgery/cut_metric_generator.h" +#include "util/vector.h" + +#include + +#include +#include + +#include "polyscope/point_cloud.h" +#include "polyscope/surface_mesh.h" + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Feature; +using namespace Penner::Holonomy; + +int main(int argc, char* argv[]) +{ + // Get command line arguments + CLI::App app{"Modify frame field in interactive viewer"}; + std::string mesh_filename = "./"; + std::string output_dir = "./"; + bool show_field = false; + bool collapse_cones = false; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Input mesh")->check(CLI::ExistingFile)->required(); + app.add_option("--output", output_dir, "Output directory"); + app.add_flag("--view", show_field, "open viewer to show field"); + app.add_flag("--collapse_cones", collapse_cones, "collapse adjacent cones"); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::info); + std::filesystem::create_directory(output_dir); + std::string mesh = std::filesystem::path(mesh_filename).stem().string(); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("optimizing mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // refine input mesh + std::vector feature_edges, hard_feature_edges; + std::tie(V, F, feature_edges, hard_feature_edges) = generate_refined_feature_mesh(V, F, false); + + // get cut feature mesh + FeatureFinder feature_finder(V, F); + feature_finder.mark_features(feature_edges); + Eigen::MatrixXd V_cut; + Eigen::MatrixXi F_cut, F_is_feature; + Eigen::VectorXi V_map; + std::tie(V_cut, F_cut, V_map, F_is_feature) = feature_finder.generate_feature_cut_mesh(); + + // generate initial field + Eigen::MatrixXd reference_field; + Eigen::VectorXd theta; + Eigen::MatrixXd kappa; + Eigen::MatrixXi period_jump; + std::tie(reference_field, theta, kappa, period_jump) = generate_refined_feature_field(V_cut, F_cut, V_map, collapse_cones); + + if (show_field) view_cross_field(V, F, reference_field, theta, kappa, period_jump); + + // write output + std::string output_filename; + output_filename = join_path(output_dir, mesh + ".obj"); + igl::writeOBJ(output_filename, V, F); + output_filename = join_path(output_dir, mesh + "_features"); + write_feature_edges(output_filename, feature_edges); + output_filename = join_path(output_dir, mesh + "_hard_features"); + write_feature_edges(output_filename, hard_feature_edges); + output_filename = join_path(output_dir, mesh+".ffield"); + Penner::Field::write_frame_field(output_filename, reference_field, theta, kappa, period_jump); + +} diff --git a/src/app/generate_field.cpp b/app/generate_field.cpp similarity index 68% rename from src/app/generate_field.cpp rename to app/generate_field.cpp index e578fc0..15ebff8 100644 --- a/src/app/generate_field.cpp +++ b/app/generate_field.cpp @@ -28,9 +28,7 @@ * Courant Institute of Mathematical Sciences, New York University, USA * * * * *********************************************************************************/ -#include "field/field.h" -#include "holonomy/interface.h" -#include "holonomy/holonomy/cones.h" +#include "field/frame_field.h" #include "util/io.h" #include @@ -48,7 +46,6 @@ using namespace Penner; using namespace Penner::Field; -using namespace Penner::Holonomy; void write_error_record(const std::filesystem::path& filepath, const std::string& error_message) { @@ -64,6 +61,8 @@ int main(int argc, char* argv[]) std::string mesh_file = ""; std::string input_dir = "./"; std::string output_dir = "./"; + FieldParameters field_params; + field_params.use_principal_directions = true; bool is_stl = false; app.add_option("--mesh", mesh_file, "Mesh file")->required(); app.add_option("-i,--input", input_dir, "Input filepath") @@ -124,68 +123,13 @@ int main(int argc, char* argv[]) } // generate cross field - auto [frame_field, field_Th_hat] = generate_cross_field(V, F); - - // build halfedge mesh with angles - std::vector vtx_reindex; - std::vector free_cones(0); - bool fix_boundary = false; - bool use_discrete_metric = true; - std::unique_ptr cone_metric = - Optimization::generate_initial_mesh( - V, - F, - V, - F, - field_Th_hat, - vtx_reindex, - free_cones, - fix_boundary, - use_discrete_metric); - - // check for zero edge lengths and degenerate angles - VectorX he2angle, he2cot; - cone_metric->get_corner_angles(he2angle, he2cot); - int num_halfedges = cone_metric->n_halfedges(); - for (int hij = 0; hij < num_halfedges; ++hij) { - if (float_equal(cone_metric->l[hij], 0.)) { - spdlog::error("Mesh has {} length edge", cone_metric->l[hij]); - write_error_record(error_filename, mesh_name + " has 0 length edge"); - return 1; - } - if (float_equal(he2angle[hij], 0.)) { - spdlog::error("Mesh has {} angle", he2angle[hij]); - write_error_record(error_filename, mesh_name + " has 0 corner angle"); - return 1; - } - if (float_equal(he2angle[hij], M_PI)) { - spdlog::error("Mesh has {} angle", cone_metric->l[hij]); - write_error_record(error_filename, mesh_name + " has pi corner angle"); - return 1; - } - } - - // generate rotation form and cones from cross field - VectorX rotation_form = generate_rotation_form_from_cross_field(*cone_metric, vtx_reindex, V, F, frame_field); - std::vector form_Th_hat = generate_cones_from_rotation_form( - *cone_metric, - vtx_reindex, - rotation_form, - bd.size() > 1); - - // check cones are consistent - int num_vertices = V.rows(); - for (int vi = 0; vi < num_vertices; ++vi) { - if (!float_equal(form_Th_hat[vi], field_Th_hat[vi], 1e-6)) { - spdlog::warn("Inconsistent cones {} and {}", form_Th_hat[vi], field_Th_hat[vi]); - write_error_record(error_filename, mesh_name + " has inconsistent cones"); - } - } + auto [reference_field, theta, kappa, period_jump] = generate_frame_field(V, F, field_params); // write output - igl::writeOBJ(join_path(output_dir, mesh_name + ".obj"), V, F); - write_vector(rotation_form, join_path(output_dir, mesh_name + "_kappa_hat")); - write_vector(form_Th_hat, join_path(output_dir, mesh_name + "_Th_hat")); + std::string output_path = join_path(output_dir, mesh_name + ".obj"); + igl::writeOBJ(output_path, V, F); + output_path = join_path(output_dir, mesh_name + ".ffield"); + write_frame_field(output_path, reference_field, theta, kappa, period_jump); return 0; } diff --git a/src/app/optimize_metric.cpp b/app/optimize_metric.cpp similarity index 97% rename from src/app/optimize_metric.cpp rename to app/optimize_metric.cpp index 92265ef..195b204 100644 --- a/src/app/optimize_metric.cpp +++ b/app/optimize_metric.cpp @@ -28,20 +28,18 @@ * Courant Institute of Mathematical Sciences, New York University, USA * * * * *********************************************************************************/ -#include -#include -#include "optimization/core/common.h" -#include "optimization/core/cone_metric.h" +#include "metric/cone_metric.h" #include "optimization/metric_optimization/energy_functor.h" #include "optimization/metric_optimization/implicit_optimization.h" #include "optimization/interface.h" -#include "optimization/parameterization/refinement.h" +#include "parameterization/refinement.h" #include "optimization/util/viewers.h" -#include "holonomy/field/field.h" +#include "field/frame_field.h" #include "util/vector.h" #include "util/io.h" #include +#include using namespace Penner; using namespace Optimization; diff --git a/src/app/optimize_shear.cpp b/app/optimize_shear.cpp similarity index 99% rename from src/app/optimize_shear.cpp rename to app/optimize_shear.cpp index 5dd882e..5b70484 100644 --- a/src/app/optimize_shear.cpp +++ b/app/optimize_shear.cpp @@ -28,16 +28,15 @@ * Courant Institute of Mathematical Sciences, New York University, USA * * * * *********************************************************************************/ -#include -#include -#include "optimization/core/common.h" #include "optimization/metric_optimization/explicit_optimization.h" #include "optimization/interface.h" #include "optimization/core/shear.h" #include "optimization/parameterization/refinement.h" #include "optimization/util/viewers.h" #include "util/io.h" + +#include #include using namespace Penner; diff --git a/src/app/parameterize_aligned.cpp b/app/parameterize_aligned.cpp similarity index 95% rename from src/app/parameterize_aligned.cpp rename to app/parameterize_aligned.cpp index ad38306..b63befd 100644 --- a/src/app/parameterize_aligned.cpp +++ b/app/parameterize_aligned.cpp @@ -1,4 +1,3 @@ -#include #include "feature/interface.h" #include "feature/core/io.h" #include "field/frame_field.h" @@ -9,6 +8,7 @@ #include "util/vf_mesh.h" #include +#include #include #include #include @@ -362,6 +362,13 @@ int main(int argc, char* argv[]) spdlog::info("optimizing mesh at {}", mesh_filename); igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + // check if valid input + if (V.rows() < 0) + { + spdlog::error("cannot parametrize empty mesh"); + return 1; + } + // Get features and field std::vector feature_edges, hard_feature_edges; Eigen::MatrixXd reference_field; @@ -378,29 +385,16 @@ int main(int argc, char* argv[]) std::tie(reference_field, theta, kappa, period_jump) = Penner::Field::load_frame_field(field_filename); } else { + spdlog::info("generating feature edges"); + // refine input mesh std::tie(V, F, feature_edges, hard_feature_edges) = generate_refined_feature_mesh(V, F, false); + + spdlog::info("optimizing field"); FeatureFinder feature_finder(V, F); feature_finder.mark_features(feature_edges); auto[V_cut, F_cut, V_map, F_is_feature] = feature_finder.generate_feature_cut_mesh(); - - int radius = 5; - Scalar rel_anisotropy=0.9; - Scalar abs_anisotropy=0.2; - Scalar bb_diag = igl::bounding_box_diagonal(V); - auto [direction, is_fixed_direction] = Penner::Field::compute_field_direction( - V_cut, - F_cut, - radius, - abs_anisotropy / bb_diag, - rel_anisotropy); - MarkedMetricParameters marked_metric_params; - marked_metric_params.remove_trivial_torus = false; // FIXME - marked_metric_params.use_log_length = true; - marked_metric_params.use_initial_zero = false; - CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, {}); - cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); - std::tie(reference_field, theta, kappa, period_jump) = cut_metric_generator.get_field(); + std::tie(reference_field, theta, kappa, period_jump) = generate_refined_feature_field(V_cut, F_cut, V_map); } if (show_field) view_cross_field(V, F, reference_field, theta, kappa, period_jump); diff --git a/src/app/parametrize_seamless.cpp b/app/parametrize_seamless.cpp similarity index 95% rename from src/app/parametrize_seamless.cpp rename to app/parametrize_seamless.cpp index f3d523a..ef20ea1 100644 --- a/src/app/parametrize_seamless.cpp +++ b/app/parametrize_seamless.cpp @@ -29,29 +29,37 @@ * * * *********************************************************************************/ #include "field/frame_field.h" +#include "field/cross_field.h" #include "field/intrinsic_field.h" #include "holonomy/interface.h" #include "holonomy/holonomy/newton.h" -#include "holonomy/holonomy/cones.h" +#include "field/cones.h" #include "holonomy/core/viewer.h" #include "parametrization/refinement.h" #include "parametrization/parametrize.h" +// cone validation +#include "holonomy/holonomy/constraint.h" + +// infer theta from direction field +#include "field/vector_field.h" + #include #include #include #include +#if USE_UV_OPTIMIZATION #include "ExtremeOpt.h" #include "MeshCutter.h" #include "main_helper.h" +#endif using namespace Penner; using namespace Penner::Field; using namespace Penner::Holonomy; - Eigen::MatrixXd optimize_seamless_parameterization( const Eigen::MatrixXd& V_init, const Eigen::MatrixXi& F_init, @@ -165,7 +173,8 @@ int main(int argc, char* argv[]) std::filesystem::path Th_hat_filename = ""; std::filesystem::path field_filename = ""; std::filesystem::path output_dir = "./"; - std::filesystem::path input_json = "../scripts/symdir.json"; + std::filesystem::path current_dir = std::filesystem::path(__FILE__).parent_path(); + std::filesystem::path input_json = current_dir / "symdir.json"; // IO Parameters app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); @@ -201,6 +210,7 @@ int main(int argc, char* argv[]) bool use_delaunay = false; bool fit_field = false; bool show_parameterization = false; + bool show_field = false; spdlog::level::level_enum log_level = spdlog::level::info; app.add_option( "--max_triangle_quality", @@ -210,6 +220,7 @@ int main(int argc, char* argv[]) app.add_flag("--use_delaunay", use_delaunay, "Use Delaunay connectivity"); app.add_flag("--fit_field", fit_field, "Fit new cross field"); app.add_flag("--show_parameterization", show_parameterization, "Show final paramaterization"); + app.add_flag("--show_field", show_field, "Show guiding cross field"); app.add_option("--log_level", log_level, "Level of logging") ->transform(CLI::CheckedTransformer(log_level_map, CLI::ignore_case)); @@ -310,7 +321,7 @@ int main(int argc, char* argv[]) } // add constraints to viewer - view_rotation_form(marked_metric, vtx_reindex, V, rotation_form, Th_hat, "rotation", false); + if (show_field) view_rotation_form(marked_metric, vtx_reindex, V, rotation_form, Th_hat, "rotation", false); // Make initial mesh Delaunay if desired std::vector flip_seq = {}; @@ -400,8 +411,8 @@ int main(int argc, char* argv[]) config); } - if (show_parameterization) view_triangulation(V_o, F_o, fn_to_f_o, endpoints_o, "refinement", false); - if (show_parameterization) view_seamless_parameterization(V_o, F_o, uv_o, FT_o, "overlay", false); + //if (show_parameterization) view_triangulation(V_o, F_o, fn_to_f_o, endpoints_o, "refinement", false); + //if (show_parameterization) view_seamless_parameterization(V_o, F_o, uv_o, FT_o, "overlay", false); if (show_parameterization) view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "simplified"); // Write the output mesh diff --git a/src/app/symdir.json b/app/symdir.json similarity index 100% rename from src/app/symdir.json rename to app/symdir.json diff --git a/src/app/util.h b/app/util.h similarity index 100% rename from src/app/util.h rename to app/util.h diff --git a/src/app/view_quad_mesh.cpp b/app/view_quad_mesh.cpp similarity index 91% rename from src/app/view_quad_mesh.cpp rename to app/view_quad_mesh.cpp index fef5497..a71fbf5 100644 --- a/src/app/view_quad_mesh.cpp +++ b/app/view_quad_mesh.cpp @@ -1,6 +1,4 @@ -#include "holonomy/core/common.h" #include "feature/core/viewer.h" -#include "feature/core/quads.h" #include #include diff --git a/app/view_seamless.cpp b/app/view_seamless.cpp new file mode 100644 index 0000000..cfe8671 --- /dev/null +++ b/app/view_seamless.cpp @@ -0,0 +1,71 @@ +#include "holonomy/core/viewer.h" +#include "feature/feature/error.h" +#include "util/io.h" +#include "feature/core/io.h" +#include "util/vector.h" +#include "util/vf_mesh.h" + +#include +#include + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#endif + +using namespace Penner; +using namespace Penner::Holonomy; +using namespace Penner::Feature; + +int main(int argc, char* argv[]) +{ + // Get command line arguments + CLI::App app{"View a quad mesh"}; + std::string mesh_filename = ""; + bool show_layout = false; + bool show_seams = false; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + app.add_flag("--show_layout", show_layout, "Show layout of parametrization"); + app.add_flag("--show_seams", show_seams, "Show seam edges of parametrization"); + CLI11_PARSE(app, argc, argv); + + spdlog::set_level(spdlog::level::debug); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Using mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + // get tagged feature edges + std::vector feature_edges = load_mesh_edges(mesh_filename); + + // infer seame edges from parametrization connectivity + Eigen::MatrixXi F_is_seam = find_seams(F, FT); + auto [V_seams, E_seams] = generate_edges(V, F, F_is_seam); + + // view seamless parametrization + view_seamless_parameterization(V, F, uv, FT, "seamless mesh", false); + + // show feature edges if any are tagged + if (feature_edges.size() > 0) + { + // compute feature alignment error + std::vector feature_alignment = compute_feature_alignment(F, uv, FT, feature_edges); + spdlog::info("Maximum feature alignment: {}", vector_max(feature_alignment)); + + // plot edges + Eigen::MatrixXi E = compute_edge_matrix(feature_edges); + auto [VN, EN] = remove_unreferenced(V, E); + polyscope::registerCurveNetwork("features", VN, EN); + } + + // optional viewers + if (show_layout) polyscope::registerSurfaceMesh2D("layout", uv, FT); + if (show_seams) polyscope::registerCurveNetwork("seams", V_seams, E_seams); + + polyscope::show(); + +} diff --git a/include/feature/feature/core/vf_corners.h b/include/feature/feature/core/vf_corners.h index 647699e..a68b91d 100644 --- a/include/feature/feature/core/vf_corners.h +++ b/include/feature/feature/core/vf_corners.h @@ -295,5 +295,14 @@ generate_edges( const Eigen::MatrixXi& F, const Eigen::MatrixXi& F_is_edge); +/** + * @brief Covert a list of edge endpoints into an edge matrix with edges as rows. + * + * @param edges: list of edge endpoints + * @return edges in matrix format + */ +Eigen::MatrixXi compute_edge_matrix(const std::vector& edges); + + } // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/include/feature/feature/interface.h b/include/feature/feature/interface.h index 0b5b5c3..2b6cfdf 100644 --- a/include/feature/feature/interface.h +++ b/include/feature/feature/interface.h @@ -35,6 +35,29 @@ std::tuple, std::vecto const Eigen::MatrixXi& F, bool use_minimal_forest=false); +/** + * @brief Find an aligned cross field for a mesh cut along feature edges. + * + * @param V_cut: cut mesh vertices + * @param F_cut: cut mesh faces + * @param V_map: vertex identifications in the cut mesh + * @param collapse_cones: (optional) attempt to collapse nearby cones if true + * @return base per-face reference tangent direction matrix + * @return base offset angles of a representative cross field direction relative to the reference + * @return base per-corner rotation angle of the reference direction field across the opposite edge + * @return base per-corner period jump of the cross field across the opposite edge + */ +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_refined_feature_field( + const Eigen::MatrixXd& V_cut, + const Eigen::MatrixXi& F_cut, + const Eigen::VectorXi& V_map, + bool collapse_cones=false); + /** * @brief Generate a parameterization aligned to features and the given cross field. * diff --git a/include/field/field/cones.h b/include/field/field/cones.h index a785842..a541741 100644 --- a/include/field/field/cones.h +++ b/include/field/field/cones.h @@ -45,6 +45,17 @@ std::vector generate_cones_from_rotation_form( const VectorX& rotation_form, bool has_boundary=false); + +/** + * @brief Determine if a mesh cone prescription contains cones below a threshold + * + * @param Th_hat: mesh with cone constraints + * @param min_cone_index: minimum index for cones + * @return true if there is a cone strictly below the threshold + * @return false otherwise + */ +bool contains_small_cones(const std::vector& Th_hat, int min_cone_index=1); + /** * @brief Determine if a mesh cone prescription corresponds to a trivial torus * @@ -54,6 +65,15 @@ std::vector generate_cones_from_rotation_form( */ bool is_trivial_torus(const Mesh& m); +/** + * @brief Determine if a mesh cone prescription corresponds to a torus with a cone pair. + * + * @param m: mesh with cone constraints + * @return true if the mesh is a torus with a pair of cones + * @return false otherwise + */ +bool is_torus_with_cone_pair(const Mesh& m); + /** * @brief Given target cone angles, fix any problems that prevent them from being valid * for seamless holonomy constraints. diff --git a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h index d44f5b9..222b953 100644 --- a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h +++ b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h @@ -63,6 +63,20 @@ class MarkedPennerConeMetric : public PennerConeMetric const std::vector>& homology_basis_loops, const std::vector& kappa); + + /** + * @brief Construct a new Marked Penner Cone Metric object with given dual loop + * markings and holonomy constraints. + * + * @param cone_metric: mesh with metric + * @param homology_basis_loops: homology basis loops for the surface + * @param kappa: holonomy constraints on the basis loops + */ + MarkedPennerConeMetric( + const PennerConeMetric& cone_metric, + const std::vector>& homology_basis_loops, + const std::vector& kappa); + MarkedPennerConeMetric(const MarkedPennerConeMetric& marked_metric); void operator=(const MarkedPennerConeMetric& m); diff --git a/include/holonomy/holonomy/interface.h b/include/holonomy/holonomy/interface.h index bc54316..7203a52 100644 --- a/include/holonomy/holonomy/interface.h +++ b/include/holonomy/holonomy/interface.h @@ -13,6 +13,7 @@ #include "field/rotation_form.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" +#include "metric/interface.h" namespace Penner { namespace Holonomy { @@ -21,20 +22,14 @@ namespace Holonomy { * @brief Parameters for marked metric construction * */ -struct MarkedMetricParameters +struct MarkedMetricParameters : ConeMetricParameters { typedef HomotopyBasisGenerator::Weighting Weighting; - bool use_initial_zero = false; // use initial zero Penner coordinates - bool use_log_length = false; // use initial log length coordinates instead of Penner bool remove_loop_constraints = false; // don't set dual loop holonomy constraints if true int max_loop_constraints = -1; // set maximum number of loop constraints if positive int max_boundary_constraints = -1; // set maximum number of boundary constraints if positive Weighting weighting = Weighting::minimal_homotopy; // weighting for tree-cotree - bool remove_symmetry = false; // remove symmetry structure from doubled mesh - bool free_interior = false; // remove interior cone constraints - // TODO Implement for meshes without features - bool use_free_cones = false; // use free cones instead of seamless constraints bool remove_trivial_torus = true; // remove loop constraints from trivial torus to make independent bool use_connectivity = true; // use connectivity structure for markings }; @@ -216,9 +211,6 @@ void regularize_metric(MarkedPennerConeMetric& marked_metric, double max_triangl */ void optimize_triangle_quality(MarkedPennerConeMetric& marked_metric, double max_triangle_quality = 50); -VectorX generate_log_edge_lengths(const Mesh& m); -VectorX generate_penner_coordinates(const Mesh& m); - void generate_basis_loops( const Mesh& m, std::vector>& basis_loops, @@ -240,8 +232,6 @@ std::vector compute_kappa( const VectorX& rotation_form, const std::vector>& basis_loops); -DiscreteMetric generate_discrete_metric(const Mesh& m); - std::tuple get_constraint_outliers( MarkedPennerConeMetric& marked_metric, bool use_interior_vertices=true, diff --git a/include/metric/metric/cone_metric.h b/include/metric/metric/cone_metric.h index 7d95dbd..549a7bb 100644 --- a/include/metric/metric/cone_metric.h +++ b/include/metric/metric/cone_metric.h @@ -365,6 +365,7 @@ class DiscreteMetric : public DifferentiableConeMetric void expand_metric_coordinates(const VectorX& metric_coords); }; + template Mesh change_mesh_type(const Mesh& m) { diff --git a/include/metric/metric/interface.h b/include/metric/metric/interface.h new file mode 100644 index 0000000..32f39d2 --- /dev/null +++ b/include/metric/metric/interface.h @@ -0,0 +1,92 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include "metric/cone_metric.h" + + +namespace Penner { + +// Parameters for constructing a cone metric. +struct ConeMetricParameters +{ + bool use_initial_zero = false; // use initial zero Penner coordinates + bool use_log_length = false; // use initial log length coordinates instead of Penner + bool remove_symmetry = false; // remove symmetry structure from doubled mesh + bool free_interior = false; // remove interior cone constraints + // TODO Implement for meshes without features + bool use_free_cones = false; // use free cones instead of seamless constraints +}; + +/** + * @brief Generate logarithmic edge lengths for a discrete metric + * + * @param m: mesh with metric + * @return per halfedge logarithmic edge lengths + */ +VectorX generate_log_edge_lengths(const Mesh& m); + +/** + * @brief Generate Penner coordinates for a discrete metric. + * + * For a Delaunay metric, these are just logarithmic edge lengths. Otherwise, the + * Penner coordinates for the connectivity are computed using a Delauany flip + * algorithm. + * + * @param m: mesh with metric + * @return per halfedge Penner coordinates + */ +VectorX generate_penner_coordinates(const Mesh& m); + +/** + * @brief Convert a mesh into a discrete metric. + * + * Note that the discrete metric supports differentiation, unlike the base mesh. + * + * @param m: mesh with metric + * @return mesh with differentiable metric + */ +DiscreteMetric generate_discrete_metric(const Mesh& m); + +/** + * @brief Generate a cone metric from a VF mesh with target cones. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param uv: mesh metric vertices + * @param F_uv: mesh metric faces + * @param Th_hat: per-vertex cone angles + * @param free_cones: list of cones to leave free + * @param cone_metric_params: (optional) parameters for the cone mesh construction + * @return marked cone metric + * @return vertex reindexing from the halfedge to VF vertices + */ +std::tuple> generate_cone_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv, + const std::vector& Th_hat, + std::vector free_cones, + ConeMetricParameters cone_metric_params = ConeMetricParameters()); + +/** + * @brief Generate a cone metric from a halfedge mesh. + * + * @param m: mesh with metric + * @param cone_metric_params: (optional) parameters for the cone mesh construction + * @return marked cone metric + * @return vertex reindexing from the halfedge to VF vertices + */ +PennerConeMetric generate_cone_metric( + const Mesh& m, + ConeMetricParameters cone_metric_params = ConeMetricParameters()); + + +} // namespace Penner \ No newline at end of file diff --git a/include/util/util/vf_mesh.h b/include/util/util/vf_mesh.h index 8df7953..85685d0 100644 --- a/include/util/util/vf_mesh.h +++ b/include/util/util/vf_mesh.h @@ -36,12 +36,25 @@ int count_components(const Eigen::MatrixXi& F); /// @param[in] F: initial mesh faces /// @param[out] FN: reindexed mesh faces /// @param[out] new_to_old_map: map from new to old vertex indices -/// @return number of connected components void remove_unreferenced( const Eigen::MatrixXi& F, Eigen::MatrixXi& FN, std::vector& new_to_old_map); +/// Given a face index matrix, reindex the vertex indices to removed unreferenced +/// vertex indices in O(|F|) time. +/// +/// Note that libigl has function with similar behavior, but it is a O(|V| + |F|) +/// algorithm due to their bookkeeping method +/// +/// @param[in] V: initial mesh vertices +/// @param[in] F: initial mesh face +/// @return: mesh vertices without unreferenced +/// @return: reindexed mesh faces +std::tuple remove_unreferenced( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F); + /// Given a mesh with a parametrization, generate the 3D seam polylines /// /// @param[in] V: mesh vertices diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e83a1c8..c6c592f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(MetricSrc metric/cone_metric.cpp metric/constraint.cpp metric/flip_matrix_generator.cpp + metric/interface.cpp metric/projection.cpp metric/quality.cpp metric/shear.cpp diff --git a/src/app/generate_aligned_field.cpp b/src/app/generate_aligned_field.cpp deleted file mode 100644 index fde35bd..0000000 --- a/src/app/generate_aligned_field.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "holonomy/core/common.h" -#include "feature/core/viewer.h" -#include "feature/interface.h" -#include "feature/feature/features.h" -#include "feature/surgery/cut_metric_generator.h" -#include "feature/core/io.h" -#include "util/io.h" -#include "field/frame_field.h" -#include "holonomy/core/viewer.h" -#include -#include - -#include -#include - -#include "polyscope/point_cloud.h" -#include "polyscope/surface_mesh.h" - -using namespace Penner; -using namespace Penner::Field; -using namespace Penner::Feature; -using namespace Penner::Holonomy; - -int main(int argc, char* argv[]) -{ - // Get command line arguments - CLI::App app{"Modify frame field in interactive viewer"}; - std::string mesh_filename = "./"; - std::string output_dir = "./"; - Scalar displacement = 0.; - bool show_field = false; - bool collapse_cones = false; - - // IO Parameters - app.add_option("--mesh", mesh_filename, "Input mesh")->check(CLI::ExistingFile)->required(); - app.add_option("--output", output_dir, "Output directory"); - app.add_option("--displacement", displacement, "displacement for cross field viewer"); - app.add_flag("--view", show_field, "open viewer to show field"); - app.add_flag("--collapse_cones", collapse_cones, "collapse adjacent cones"); - CLI11_PARSE(app, argc, argv); - - spdlog::set_level(spdlog::level::info); - std::filesystem::create_directory(output_dir); - std::string mesh = std::filesystem::path(mesh_filename).stem().string(); - - // Get input mesh - Eigen::MatrixXd V, uv, N; - Eigen::MatrixXi F, FT, FN; - spdlog::info("optimizing mesh at {}", mesh_filename); - igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); - - // refine input mesh - std::vector feature_edges, hard_feature_edges; - std::tie(V, F, feature_edges, hard_feature_edges) = generate_refined_feature_mesh(V, F, false); - FeatureFinder feature_finder(V, F); - feature_finder.mark_features(feature_edges); - Eigen::MatrixXd V_cut; - Eigen::MatrixXi F_cut, F_is_feature; - Eigen::VectorXi V_map; - std::tie(V_cut, F_cut, V_map, F_is_feature) = feature_finder.generate_feature_cut_mesh(); - - // fit field directions - int radius = 5; - Scalar rel_anisotropy=0.9; - Scalar abs_anisotropy=0.2; - Scalar bb_diag = igl::bounding_box_diagonal(V); - auto [direction, is_fixed_direction] = Penner::Field::compute_field_direction( - V_cut, - F_cut, - radius, - abs_anisotropy / bb_diag, - rel_anisotropy); - - // generate initial field and halfedge mesh - MarkedMetricParameters marked_metric_params; - Eigen::MatrixXd reference_field; - Eigen::VectorXd theta; - Eigen::MatrixXd kappa; - Eigen::MatrixXi period_jump; - marked_metric_params.use_log_length = true; - CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, {}); - cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); - std::tie(reference_field, theta, kappa, period_jump) = cut_metric_generator.get_field(); - Eigen::VectorXi reference_corner(reference_field.rows()); - - MarkedPennerConeMetric marked_metric; - std::vector vtx_reindex; - std::vector face_reindex; - VectorX rotation_form; - std::vector Th_hat; - std::tie(marked_metric, vtx_reindex, face_reindex, rotation_form, Th_hat) = cut_metric_generator.get_union_metric( marked_metric_params); - - // modify field - Penner::Field::IntrinsicNRosyField field_generator; - field_generator.initialize(marked_metric); - field_generator.set_field(marked_metric, vtx_reindex, F_cut, face_reindex, theta, kappa, period_jump); - Eigen::MatrixXd V_disp = displace_cut_faces(V_cut, F_cut, displacement); - - if (collapse_cones) - { - field_generator.collapse_nearby_cones(marked_metric); - field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); - } - - if (show_field) - { - polyscope::init(); - field_generator.update_viewer(marked_metric, vtx_reindex, V_disp); - auto callback = [&] () { - if (ImGui::Button("collapse cones")) { - field_generator.collapse_adjacent_cones(marked_metric); - field_generator.update_viewer(marked_metric, vtx_reindex, V_disp); - } - if (ImGui::Button("parameterize")) { - field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); - - NewtonParameters alg_params; - alg_params.output_dir = "./"; - alg_params.error_eps = 1e-10; - alg_params.solver = "ldlt"; - MarkedMetricParameters marked_metric_params; - AlignedMetricGenerator aligned_metric_generator( - V, - F, - feature_edges, - hard_feature_edges, - reference_field, - theta, - kappa, - period_jump, - marked_metric_params); - aligned_metric_generator.optimize_full(alg_params); - aligned_metric_generator.optimize_relaxed(alg_params); - aligned_metric_generator.parameterize(false); - auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = aligned_metric_generator.get_parameterization(); - view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "refined mesh", false); - - std::string output_filename = join_path(output_dir, mesh+"_param.obj"); - write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); - } - if (ImGui::Button("write field")) { - field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); - std::string field_filename = join_path(output_dir, mesh+".ffield"); - Penner::Field::write_frame_field(field_filename, reference_field, theta, kappa, period_jump); - } - }; - polyscope::state::userCallback = callback; - polyscope::show(); - field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); - } - - // write output - std::string output_filename; - output_filename = join_path(output_dir, mesh + ".obj"); - igl::writeOBJ(output_filename, V, F); - output_filename = join_path(output_dir, mesh + "_features"); - write_feature_edges(output_filename, feature_edges); - output_filename = join_path(output_dir, mesh + "_hard_features"); - write_feature_edges(output_filename, hard_feature_edges); - output_filename = join_path(output_dir, mesh+".ffield"); - Penner::Field::write_frame_field(output_filename, reference_field, theta, kappa, period_jump); - -} diff --git a/src/app/view_seamless_uv.cpp b/src/app/view_seamless_uv.cpp deleted file mode 100644 index 890e57e..0000000 --- a/src/app/view_seamless_uv.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "holonomy/core/common.h" -#include "holonomy/core/viewer.h" -#include "feature/feature/error.h" -#include "feature/core/io.h" -#include "feature/surgery/cut_mesh_layout.h" -#include "feature/interface.h" - -#include "util/vf_mesh.h" - -#include -#include -#include -#include "polyscope/surface_mesh.h" -#include "polyscope/point_cloud.h" -#include "polyscope/curve_network.h" - -using namespace Penner; -using namespace Penner::Holonomy; -using namespace Penner::Feature; - - - -std::tuple generate_cone_vertices( - const Eigen::MatrixXd& V, - const std::vector& Th_hat) -{ - int num_vertices = Th_hat.size(); - std::vector cone_indices; - cone_indices.reserve(num_vertices); - for (int vi = 0; vi < num_vertices; ++vi) { - if (float_equal(Th_hat[vi], 2 * M_PI, 1e-8)) continue; - cone_indices.push_back(vi); - } - - int num_cones = cone_indices.size(); - Eigen::MatrixXd cone_positions(num_cones, 3); - Eigen::VectorXd cone_values(num_cones); - for (int i = 0; i < num_cones; ++i) { - int vi = cone_indices[i]; - cone_positions.row(i) = V.row(vi); - cone_values[i] = (double)(Th_hat[vi]) - (2 * M_PI); - } - - return std::make_tuple(cone_positions, cone_values); -} - -int main(int argc, char* argv[]) -{ - // Get command line arguments - CLI::App app{"View a quad mesh"}; - std::string mesh_filename = ""; - - // IO Parameters - app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); - CLI11_PARSE(app, argc, argv); - - spdlog::set_level(spdlog::level::debug); - - // Get input mesh - Eigen::MatrixXd V, uv, N; - Eigen::MatrixXi F, FT, FN; - spdlog::info("Using mesh at {}", mesh_filename); - igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); - - std::vector E = load_mesh_edges(mesh_filename); - - Eigen::MatrixXi F_is_seam = find_seams(F, FT); - auto [V_seams, E_seams] = generate_edges(V, F, F_is_seam); - - std::vector feature_alignment = compute_feature_alignment(F, uv, FT, E); - spdlog::info("Maximum feature alignment: {}", vector_max(feature_alignment)); - view_seamless_parameterization(V, F, uv, FT, "seamless mesh", false); - //VectorX cone_angles = compute_cone_angles(V, F, uv, FT); - //std::vector cone_angles_vec; - //convert_eigen_to_std_vector(cone_angles, cone_angles_vec); - - //auto [uv_n, FT_n] = generate_connected_parameterization(V, F, uv, FT); - //view_seamless_parameterization(V, F, uv_n, FT_n, "connected seamless mesh", false); - - //std::vector feature_alignment = compute_feature_alignment(F, uv_n, FT_n, E); - //spdlog::info("Maximum feature alignment: {}", vector_max(feature_alignment)); - - //auto [cone_positions, cone_values] = generate_cone_vertices(V, cone_angles_vec); - bool show_layout = false; - if (show_layout) polyscope::registerSurfaceMesh2D("layout", uv, FT); - //polyscope::getSurfaceMesh("base mesh") - // ->addVertexScalarQuantity("cone angles", cone_angles); - //polyscope::registerPointCloud("cones", cone_positions); - //polyscope::getPointCloud("cones") - // ->addScalarQuantity("index", cone_values) - // ->setColorMap("coolwarm") - // ->setMapRange({-M_PI, M_PI}) - // ->setEnabled(true); - Eigen::MatrixXd NV; - Eigen::MatrixXi NE; - Eigen::VectorXi I; - //igl::remove_unreferenced(V, E, NV, NV, I); - polyscope::registerCurveNetwork("features", V, E); - bool show_seams = false; - if (show_seams) polyscope::registerCurveNetwork("seams", V_seams, E_seams); - - polyscope::show(); - -} diff --git a/src/feature/core/vf_corners.cpp b/src/feature/core/vf_corners.cpp index 471e26e..220d3db 100644 --- a/src/feature/core/vf_corners.cpp +++ b/src/feature/core/vf_corners.cpp @@ -544,5 +544,18 @@ generate_edges( return std::make_tuple(VN, EN); } +Eigen::MatrixXi compute_edge_matrix(const std::vector& edges) +{ + int num_edges = edges.size(); + Eigen::MatrixXi E(num_edges, 2); + for (int e = 0; e < num_edges; ++e) + { + for (int i = 0; i < 2; ++i) + { + E(e, i) = edges[e][i]; + } + } +} + } // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/src/feature/interface.cpp b/src/feature/interface.cpp index 20a0203..983a531 100644 --- a/src/feature/interface.cpp +++ b/src/feature/interface.cpp @@ -34,6 +34,7 @@ #include "feature/core/union_meshes.h" #include +#include namespace Penner { namespace Feature { @@ -218,6 +219,65 @@ std::tuple, std::vecto return refine_feature_components(feature_finder, use_minimal_forest); } +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_refined_feature_field( + const Eigen::MatrixXd& V_cut, + const Eigen::MatrixXi& F_cut, + const Eigen::VectorXi& V_map, + bool collapse_cones) +{ + int radius = 5; + Scalar rel_anisotropy=0.9; + Scalar abs_anisotropy=0.2; + Scalar bb_diag = igl::bounding_box_diagonal(V_cut); + auto [direction, is_fixed_direction] = Penner::Field::compute_field_direction( + V_cut, + F_cut, + radius, + abs_anisotropy / bb_diag, + rel_anisotropy); + + MarkedMetricParameters marked_metric_params; + marked_metric_params.remove_trivial_torus = false; // FIXME + marked_metric_params.use_log_length = true; + marked_metric_params.use_initial_zero = false; + + // generate fields + Eigen::MatrixXd reference_field; + Eigen::VectorXd theta; + Eigen::MatrixXd kappa; + Eigen::MatrixXi period_jump; + CutMetricGenerator cut_metric_generator(V_cut, F_cut, marked_metric_params, {}); + cut_metric_generator.generate_fields(V_cut, F_cut, V_map, direction, is_fixed_direction); + std::tie(reference_field, theta, kappa, period_jump) = cut_metric_generator.get_field(); + + // optionally collapse cones + if (collapse_cones) + { + // generate initial metric to use for collapse + MarkedPennerConeMetric marked_metric; + std::vector vtx_reindex; + std::vector face_reindex; + VectorX rotation_form; + std::vector Th_hat; + std::tie(marked_metric, vtx_reindex, face_reindex, rotation_form, Th_hat) = cut_metric_generator.get_union_metric( marked_metric_params); + + // do collapse and get fixed field + Penner::Field::IntrinsicNRosyField field_generator; + Eigen::VectorXi reference_corner(reference_field.rows()); + field_generator.initialize(marked_metric); + field_generator.set_field(marked_metric, vtx_reindex, F_cut, face_reindex, theta, kappa, period_jump); + field_generator.collapse_nearby_cones(marked_metric); + field_generator.get_field(marked_metric, vtx_reindex, F_cut, face_reindex, reference_corner, theta, kappa, period_jump); + } + + return std::make_tuple(reference_field, theta, kappa, period_jump); +} + AlignedMetricGenerator::AlignedMetricGenerator( const Eigen::MatrixXd& V_, const Eigen::MatrixXi& F_, diff --git a/src/feature/surgery/cut_mesh_layout.cpp b/src/feature/surgery/cut_mesh_layout.cpp index 1fdb46f..4ea3788 100644 --- a/src/feature/surgery/cut_mesh_layout.cpp +++ b/src/feature/surgery/cut_mesh_layout.cpp @@ -74,7 +74,7 @@ parameterize_cut_mesh( const auto& he_index = he_maps[i]; // get initial and final metric coordinates - VectorX metric_init = Holonomy::generate_penner_coordinates(embedding_component); + VectorX metric_init = generate_penner_coordinates(embedding_component); VectorX metric_coords(mesh_component.n_halfedges()); for (int i = 0; i < mesh_component.n_halfedges(); ++i) { // metric_init[i] = 2. * log(init_component.l[i]); diff --git a/src/field/cones.cpp b/src/field/cones.cpp index cf7ae49..93ccb86 100644 --- a/src/field/cones.cpp +++ b/src/field/cones.cpp @@ -89,11 +89,6 @@ bool contains_small_cones(const std::vector& Th_hat, int min_cone_index) return false; } -bool contains_zero_cones(const std::vector& Th_hat) -{ - return contains_small_cones(Th_hat, 1); -} - std::pair count_cones(const Mesh& m) { const auto& Th_hat = m.Th_hat; @@ -203,13 +198,6 @@ bool is_torus_with_cone_pair(const Mesh& m) return ((genus == 1) && (num_neg_cones == 1) && (num_pos_cones == 1)); } -bool validate_cones(const Mesh& m) -{ - if (contains_zero_cones(m.Th_hat)) return false; - if (is_torus_with_cone_pair(m)) return false; - - return true; -} // Helper to fix small cones void remove_minimum_cone(Mesh& m) diff --git a/src/holonomy/holonomy/constraint.cpp b/src/holonomy/holonomy/constraint.cpp index 7afac14..c7f1b10 100644 --- a/src/holonomy/holonomy/constraint.cpp +++ b/src/holonomy/holonomy/constraint.cpp @@ -12,6 +12,7 @@ #include "util/vector.h" #include "util/linear_algebra.h" #include "metric/constraint.h" +#include "field/cones.h" #include "holonomy/holonomy/holonomy.h" namespace Penner { @@ -703,5 +704,13 @@ bool validate_cones_from_rotation_form( return true; } +bool validate_cones(const Mesh& m) +{ + if (Field::contains_small_cones(m.Th_hat, 1)) return false; + if (Field::is_torus_with_cone_pair(m)) return false; + + return true; +} + } // namespace Holonomy } // namespace Penner \ No newline at end of file diff --git a/src/holonomy/holonomy/marked_penner_cone_metric.cpp b/src/holonomy/holonomy/marked_penner_cone_metric.cpp index b484fca..50c1e1c 100644 --- a/src/holonomy/holonomy/marked_penner_cone_metric.cpp +++ b/src/holonomy/holonomy/marked_penner_cone_metric.cpp @@ -124,21 +124,30 @@ MarkedPennerConeMetric::MarkedPennerConeMetric( const VectorX& metric_coords, const std::vector>& homology_basis_loops, const std::vector& kappa) - : PennerConeMetric(m, metric_coords) - , kappa_hat(kappa) - , m_dual_loop_manager(m.n_edges()) + : MarkedPennerConeMetric( + PennerConeMetric(m, metric_coords), + homology_basis_loops, + kappa) { - assert(is_valid_mesh(m)); +} +MarkedPennerConeMetric::MarkedPennerConeMetric( + const PennerConeMetric& cone_metric, + const std::vector>& homology_basis_loops, + const std::vector& kappa) + : PennerConeMetric(cone_metric) + , kappa_hat(kappa) + , m_dual_loop_manager(cone_metric.n_edges()) +{ int num_basis_loops = homology_basis_loops.size(); m_homology_basis_loops.reserve(num_basis_loops); for (int i = 0; i < num_basis_loops; ++i) { m_homology_basis_loops.push_back(homology_basis_loops[i]->clone()); - m_dual_loop_manager.register_loop_edges(i, m, *homology_basis_loops[i]); + m_dual_loop_manager.register_loop_edges(i, cone_metric, *homology_basis_loops[i]); } // TODO - int num_halfedges = m.n_halfedges(); + int num_halfedges = cone_metric.n_halfedges(); original_coords.resize(num_halfedges); for (int h = 0; h < num_halfedges; ++h) { original_coords[h] = 2. * log(l[h]); diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index 1ae2ce9..0605a96 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -8,6 +8,7 @@ #include "holonomy/interface.h" +#include "metric/interface.h" #include "holonomy/core/boundary_basis.h" #include "holonomy/core/homology_basis.h" #include "field/intrinsic_field.h" @@ -137,59 +138,6 @@ generate_metric_from_field( return std::make_tuple(marked_metric, vtx_reindex, rotation_form, Th_hat); } -VectorX generate_log_edge_lengths(const Mesh& m) -{ - // Make copy of mesh delaunay - // Get metric coordinates from copy - int num_halfedges = m.n_halfedges(); - VectorX metric_coords(num_halfedges); - for (int h = 0; h < num_halfedges; ++h) { - metric_coords[h] = 2.0 * log(m.l[h]); - if (isnan(metric_coords[h])) spdlog::warn("generating NaN Penner coordinate"); - } - - return metric_coords; -} - -VectorX generate_penner_coordinates(const Mesh& m) -{ - // Make copy of mesh delaunay - Mesh m_copy = m; - VectorX scale_factors; - scale_factors.setZero(m.n_ind_vertices()); - bool use_ptolemy_flip = false; - DelaunayStats del_stats; - SolveStats solve_stats; - ConformalIdealDelaunay::MakeDelaunay( - m_copy, - scale_factors, - del_stats, - solve_stats, - use_ptolemy_flip); - - // Get flip sequence - const auto& flip_sequence = del_stats.flip_seq; - for (auto iter = flip_sequence.rbegin(); iter != flip_sequence.rend(); ++iter) { - int flip_index = *iter; - if (flip_index < 0) { - flip_index = -flip_index - 1; - } - m_copy.flip_ccw(flip_index); - m_copy.flip_ccw(flip_index); - m_copy.flip_ccw(flip_index); - } - - // Get metric coordinates from copy - int num_halfedges = m.n_halfedges(); - VectorX metric_coords(num_halfedges); - for (int h = 0; h < num_halfedges; ++h) { - metric_coords[h] = 2.0 * log(m_copy.l[h]); - if (isnan(metric_coords[h])) spdlog::warn("generating NaN Penner coordinate"); - } - - return metric_coords; -} - void generate_basis_loops( const Mesh& m, std::vector>& basis_loops, @@ -266,19 +214,6 @@ void generate_basis_loops( } } -DiscreteMetric generate_discrete_metric(const Mesh& m) { - // Build initial metric and target metric from edge lengths - VectorX scale_factors; - scale_factors.setZero(m.n_ind_vertices()); - bool is_hyperbolic = false; - InterpolationMesh interpolation_mesh(m, scale_factors, is_hyperbolic); - - // Get initial log length coordinates - VectorX log_length_coords = interpolation_mesh.get_halfedge_metric_coordinates(); - return DiscreteMetric(m, log_length_coords); -} - - std::vector compute_kappa( const Mesh& discrete_metric, const VectorX& rotation_form, @@ -304,45 +239,6 @@ std::vector compute_kappa( return kappa; } -void make_free_interior(Mesh& m) { - m.fixed_dof = std::vector(m.n_ind_vertices(), true); - auto bd_vertices = find_boundary_vertices(m); - for (int vi : bd_vertices) { - m.fixed_dof[m.v_rep[vi]] = false; - } - - // handle trivial interior case - int num_bd_vertices = bd_vertices.size(); - if (num_bd_vertices == m.n_ind_vertices()) { - m.fixed_dof[0] = true; - } -} - -void remove_symmetry(const Mesh& _m, Mesh& m) { - m.Th_hat = std::vector(m.n_vertices(), 0.); - m.fixed_dof = std::vector(m.n_vertices(), false); - arange(m.n_vertices(), m.v_rep); - int num_halfedges = m.n_halfedges(); - for (int hij = 0; hij < num_halfedges; ++hij) { - m.type[hij] = 0; - // m.R[hij] = 0; - - // split interior cones - m.Th_hat[m.v_rep[m.to[hij]]] = _m.Th_hat[_m.v_rep[_m.to[hij]]] / 2.; - if (_m.type[hij] == 2) { - m.fixed_dof[m.v_rep[m.to[hij]]] = true; - } else { - m.fixed_dof[m.v_rep[m.to[hij]]] = _m.fixed_dof[_m.v_rep[_m.to[hij]]]; - } - } - - std::vector bd_vertices = find_boundary_vertices(_m); - for (int vi : bd_vertices) { - m.Th_hat[m.v_rep[vi]] = _m.Th_hat[_m.v_rep[vi]]; - m.fixed_dof[m.v_rep[vi]] = _m.fixed_dof[_m.v_rep[vi]]; - } -} - MarkedPennerConeMetric generate_marked_metric_from_mesh( const Mesh& _m, const VectorX& rotation_form, @@ -351,43 +247,17 @@ MarkedPennerConeMetric generate_marked_metric_from_mesh( { // Optionally remove symmetry structure // TODO: Need to remake cone angles with half values - Mesh m = _m; + PennerConeMetric cone_metric = generate_cone_metric(_m, marked_metric_params); // Get initial log length coordinates - DiscreteMetric discrete_metric = generate_discrete_metric(m); - VectorX log_length_coords = discrete_metric.get_metric_coordinates(); + DiscreteMetric discrete_metric = generate_discrete_metric(_m); // compute basis loops std::vector> basis_loops; - generate_basis_loops(m, basis_loops, marked_metric_params, marked_halfedges); + generate_basis_loops(_m, basis_loops, marked_metric_params, marked_halfedges); std::vector kappa = compute_kappa(discrete_metric, rotation_form, basis_loops); - // optional modifications - if (marked_metric_params.free_interior) make_free_interior(m); - if (marked_metric_params.remove_symmetry) remove_symmetry(_m, m); - - // if set to free cones, instead mark all free cones - if (marked_metric_params.use_free_cones) - { - std::vector cones = enumerate_cone_vertices(m); - if (!cones.empty()) - { - convert_index_vector_to_boolean_array(cones, m.n_ind_vertices(), m.fixed_dof); - } - } - - // Build initial metric coordinates - VectorX metric_coords; - if (marked_metric_params.use_initial_zero) { - int num_halfedges = m.n_halfedges(); - metric_coords = VectorX::Zero(num_halfedges); - } else if (marked_metric_params.use_log_length) { - metric_coords = log_length_coords; - } else { - metric_coords = generate_penner_coordinates(m); - } - - return MarkedPennerConeMetric(m, metric_coords, basis_loops, kappa); + return MarkedPennerConeMetric(cone_metric, basis_loops, kappa); } std::tuple, std::vector> generate_mesh( diff --git a/src/metric/cone_metric.cpp b/src/metric/cone_metric.cpp index e83fc9e..f950e66 100644 --- a/src/metric/cone_metric.cpp +++ b/src/metric/cone_metric.cpp @@ -11,6 +11,10 @@ #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "metric/constraint.h" #include "metric/projection.h" +#include "util/vector.h" + +// get boundary vertices for free interior +#include "util/boundary.h" // TODO: Clean code @@ -405,4 +409,5 @@ void DiscreteMetric::expand_metric_coordinates(const VectorX& metric_coords) // TODO error } + } // namespace Penner \ No newline at end of file diff --git a/src/metric/interface.cpp b/src/metric/interface.cpp new file mode 100644 index 0000000..3a5ea4a --- /dev/null +++ b/src/metric/interface.cpp @@ -0,0 +1,192 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#include "metric/interface.h" +#include "util/vector.h" + +// used to get cone vertices +#include "metric/constraint.h" + +// get boundary vertices for free interior +#include "util/boundary.h" + +// Delaunay flip algorithm for Penner coordinates +#include "conformal_ideal_delaunay/ConformalInterface.hh" + +// TODO: Clean code + +namespace Penner { + +VectorX generate_log_edge_lengths(const Mesh& m) +{ + // Make copy of mesh delaunay + // Get metric coordinates from copy + int num_halfedges = m.n_halfedges(); + VectorX metric_coords(num_halfedges); + for (int h = 0; h < num_halfedges; ++h) { + metric_coords[h] = 2.0 * log(m.l[h]); + if (isnan(metric_coords[h])) spdlog::warn("generating NaN Penner coordinate"); + } + + return metric_coords; +} + + +VectorX generate_penner_coordinates(const Mesh& m) +{ + // Make copy of mesh delaunay + Mesh m_copy = m; + VectorX scale_factors; + scale_factors.setZero(m.n_ind_vertices()); + bool use_ptolemy_flip = false; + DelaunayStats del_stats; + SolveStats solve_stats; + ConformalIdealDelaunay::MakeDelaunay( + m_copy, + scale_factors, + del_stats, + solve_stats, + use_ptolemy_flip); + + // Get flip sequence + const auto& flip_sequence = del_stats.flip_seq; + for (auto iter = flip_sequence.rbegin(); iter != flip_sequence.rend(); ++iter) { + int flip_index = *iter; + if (flip_index < 0) { + flip_index = -flip_index - 1; + } + m_copy.flip_ccw(flip_index); + m_copy.flip_ccw(flip_index); + m_copy.flip_ccw(flip_index); + } + + // Get metric coordinates from copy + int num_halfedges = m.n_halfedges(); + VectorX metric_coords(num_halfedges); + for (int h = 0; h < num_halfedges; ++h) { + metric_coords[h] = 2.0 * log(m_copy.l[h]); + if (isnan(metric_coords[h])) spdlog::warn("generating NaN Penner coordinate"); + } + + return metric_coords; +} + +void make_free_interior(Mesh& m) { + m.fixed_dof = std::vector(m.n_ind_vertices(), true); + auto bd_vertices = find_boundary_vertices(m); + for (int vi : bd_vertices) { + m.fixed_dof[m.v_rep[vi]] = false; + } + + // handle trivial interior case + int num_bd_vertices = bd_vertices.size(); + if (num_bd_vertices == m.n_ind_vertices()) { + m.fixed_dof[0] = true; + } +} + +void remove_symmetry(Mesh& m) { + // make copy + Mesh _m = m; + + m.Th_hat = std::vector(m.n_vertices(), 0.); + m.fixed_dof = std::vector(m.n_vertices(), false); + arange(m.n_vertices(), m.v_rep); + int num_halfedges = m.n_halfedges(); + for (int hij = 0; hij < num_halfedges; ++hij) { + m.type[hij] = 0; + // m.R[hij] = 0; + + // split interior cones + m.Th_hat[m.v_rep[m.to[hij]]] = _m.Th_hat[_m.v_rep[_m.to[hij]]] / 2.; + if (_m.type[hij] == 2) { + m.fixed_dof[m.v_rep[m.to[hij]]] = true; + } else { + m.fixed_dof[m.v_rep[m.to[hij]]] = _m.fixed_dof[_m.v_rep[_m.to[hij]]]; + } + } + + std::vector bd_vertices = find_boundary_vertices(_m); + for (int vi : bd_vertices) { + m.Th_hat[m.v_rep[vi]] = _m.Th_hat[_m.v_rep[vi]]; + m.fixed_dof[m.v_rep[vi]] = _m.fixed_dof[_m.v_rep[vi]]; + } +} + + +DiscreteMetric generate_discrete_metric(const Mesh& m) { + VectorX log_length_coords = generate_log_edge_lengths(m); + return DiscreteMetric(m, log_length_coords); +} + +std::tuple> generate_cone_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv, + const std::vector& Th_hat, + std::vector free_cones, + ConeMetricParameters cone_metric_params) +{ + // Convert VF mesh to halfedge + bool fix_boundary = false; + std::vector vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops; + Mesh m = FV_to_double( + V, + F, + uv, + F_uv, + Th_hat, + vtx_reindex, + indep_vtx, + dep_vtx, + v_rep, + bnd_loops, + free_cones, + fix_boundary); + + return std::make_tuple(generate_cone_metric(m, cone_metric_params), vtx_reindex); +} + +PennerConeMetric generate_cone_metric( + const Mesh& m, + ConeMetricParameters cone_metric_params) +{ + // Build initial metric coordinates, using zero, edge, or Penner coordinates + VectorX metric_coords; + if (cone_metric_params.use_initial_zero) { + int num_halfedges = m.n_halfedges(); + metric_coords = VectorX::Zero(num_halfedges); + } else if (cone_metric_params.use_log_length) { + metric_coords = generate_log_edge_lengths(m); + } else { + metric_coords = generate_penner_coordinates(m); + } + + // create metric + PennerConeMetric cone_metric(m, metric_coords); + + // optional modifications + if (cone_metric_params.free_interior) make_free_interior(cone_metric); + if (cone_metric_params.remove_symmetry) remove_symmetry(cone_metric); + + // if set to free cones, instead mark all free cones + if (cone_metric_params.use_free_cones) + { + std::vector cones = enumerate_cone_vertices(cone_metric); + if (!cones.empty()) + { + convert_index_vector_to_boolean_array(cones, cone_metric.n_ind_vertices(), cone_metric.fixed_dof); + } + } + + return cone_metric; +} + + +} // namespace Penner \ No newline at end of file diff --git a/src/util/vf_mesh.cpp b/src/util/vf_mesh.cpp index 2d5c21d..6ed47c3 100644 --- a/src/util/vf_mesh.cpp +++ b/src/util/vf_mesh.cpp @@ -70,6 +70,26 @@ void remove_unreferenced( } } +std::tuple remove_unreferenced( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F) +{ + // reindex to remove redundant vertices + Eigen::MatrixXi FN; + std::vector new_to_old_map; + remove_unreferenced(F, FN, new_to_old_map); + + // get new vertices + int num_vertices = new_to_old_map.size(); + Eigen::MatrixXd VN(num_vertices, V.cols()); + for (int i = 0; i < num_vertices; ++i) { + VN.row(i) = V.row(new_to_old_map[i]); + } + + return std::make_tuple(VN, FN); +} + + void cut_mesh_along_parametrization_seams( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, @@ -295,7 +315,7 @@ std::tuple reindex_mesh( } // reindex faces - int num_faces = F.size(); + int num_faces = F.rows(); for (int fijk = 0; fijk < num_faces; ++fijk) { for (int i = 0; i < 3; ++i) From 8296fa36ea30d41b3a6e800bbcb898572eec6ae3 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Tue, 28 Jul 2026 13:51:01 -0400 Subject: [PATCH 10/17] Refactored libraries --- CMakeLists.txt | 10 + app/CMakeLists.txt | 23 +- app/optimize_metric.cpp | 136 +-- app/parameterize_aligned.cpp | 150 +--- app/parametrize_seamless.cpp | 121 +-- app/test_interface.cpp | 100 +++ include/feature/feature/interface.h | 131 ++- include/field/field/frame_field.h | 2 +- include/field/field/intrinsic_field.h | 6 +- .../holonomy/marked_penner_cone_metric.h | 33 +- include/holonomy/holonomy/holonomy/newton.h | 1 + include/holonomy/holonomy/interface.h | 74 ++ include/metric/metric/cone_metric.h | 44 +- include/metric/metric/interface.h | 6 +- .../metric_optimization/uv_optimization.h | 54 ++ .../parametrization/parametrization/error.h | 232 +++++ .../parametrization/parametrization/layout.h | 90 +- .../parametrization/parametrize.h | 75 +- py/.polyscope.ini | 6 + py/imgui.ini | 25 + py/iteration_energy_log.csv | 623 ++++++++++++++ py/iteration_stability_log.csv | 623 ++++++++++++++ py/iteration_timing_log.csv | 16 + py/metric_status_log.csv | 12 + py/tutorial.ipynb | 814 ++++++++++++++++++ src/CMakeLists.txt | 4 +- src/feature/core/vf_corners.cpp | 2 + src/feature/core/viewer.cpp | 2 +- src/feature/dirichlet/optimization.cpp | 19 +- src/feature/interface.cpp | 186 +++- src/feature/pybind.cpp | 112 +-- src/feature/surgery/cut_mesh_layout.cpp | 3 +- src/field/cross_field.cpp | 2 +- src/field/frame_field.cpp | 96 ++- src/holonomy/core/viewer.cpp | 2 +- src/holonomy/holonomy/constraint.cpp | 16 - .../holonomy/marked_penner_cone_metric.cpp | 81 +- src/holonomy/holonomy/newton.cpp | 24 +- src/holonomy/interface.cpp | 81 +- src/holonomy/pybind.cpp | 117 +-- src/holonomy/similarity/layout.cpp | 17 +- src/metric/cone_metric.cpp | 77 +- src/metric/interface.cpp | 10 +- src/optimization/CMakeLists.txt | 8 + .../metric_optimization/uv_optimization.cpp | 170 ++++ src/optimization/pybind.cpp | 91 +- src/parametrization/error.cpp | 118 +++ src/parametrization/layout.cpp | 399 +-------- src/parametrization/parametrize.cpp | 88 +- src/pybind.cpp | 128 +++ 50 files changed, 4011 insertions(+), 1249 deletions(-) create mode 100644 app/test_interface.cpp create mode 100644 include/optimization/optimization/metric_optimization/uv_optimization.h create mode 100644 include/parametrization/parametrization/error.h create mode 100644 py/.polyscope.ini create mode 100644 py/imgui.ini create mode 100644 py/iteration_energy_log.csv create mode 100644 py/iteration_stability_log.csv create mode 100644 py/iteration_timing_log.csv create mode 100644 py/metric_status_log.csv create mode 100644 py/tutorial.ipynb create mode 100644 src/optimization/metric_optimization/uv_optimization.cpp create mode 100644 src/parametrization/error.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8deee1b..123a998 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(PennerOptimization) + set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_options(-fPIC) @@ -110,7 +111,13 @@ if (RENDER_TEXTURE) endif() if(USE_PYBIND) add_compile_definitions(PYBIND) + + # prefer conda environment OpenMP if building in a python environment + if(DEFINED ENV{CONDA_PREFIX} AND NOT DEFINED OpenMP_ROOT) + set(OpenMP_ROOT "$ENV{CONDA_PREFIX}" CACHE PATH "Root directory for OpenMP search") + endif() endif() + # WARNING: This compile definition publicly links suitesparse into the # conformal ideal delaunay library if(USE_SUITESPARSE) @@ -135,6 +142,9 @@ find_package(HDF5 REQUIRED COMPONENTS CXX) if (USE_UV_OPTIMIZATION) add_compile_definitions(USE_UV_OPTIMIZATION) include(seamless-optimization) + set(UV_OPT_LIBRARIES + SymmetricDirichletLib + ) else() include(json) endif() diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ed6cc14..5f90318 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -38,13 +38,22 @@ target_link_libraries(parameterize_aligned PRIVATE ApplicationUtilLib ) -# add_executable(optimize_metric -# optimize_metric.cpp -# ) -# target_link_libraries(optimize_metric PRIVATE -# PennerLib -# CLI11::CLI11 -# ) +add_executable(test_interface +test_interface.cpp +) +target_link_libraries(test_interface PRIVATE + PennerLib + SymmetricDirichletLib + ApplicationUtilLib +) + +add_executable(optimize_metric + optimize_metric.cpp +) +target_link_libraries(optimize_metric PRIVATE + PennerLib + CLI11::CLI11 +) # add_executable(optimize_shear # optimize_shear.cpp diff --git a/app/optimize_metric.cpp b/app/optimize_metric.cpp index 195b204..5957df2 100644 --- a/app/optimize_metric.cpp +++ b/app/optimize_metric.cpp @@ -29,12 +29,14 @@ * * * *********************************************************************************/ #include "metric/cone_metric.h" +#include "metric/viewer.h" #include "optimization/metric_optimization/energy_functor.h" #include "optimization/metric_optimization/implicit_optimization.h" #include "optimization/interface.h" -#include "parameterization/refinement.h" -#include "optimization/util/viewers.h" +#include "parametrization/refinement.h" +#include "parametrization/parametrize.h" #include "field/frame_field.h" +#include "field/cross_field.h" #include "util/vector.h" #include "util/io.h" @@ -110,13 +112,13 @@ int main(int argc, char* argv[]) std::filesystem::create_directories(output_dir); opt_params->output_dir = output_dir; - // TODO Make this automatic - if (use_discrete_metric) - { - proj_params->initial_ptolemy = false; - proj_params->use_edge_flips = false; - proj_params->max_itr = 30; - } + // TODO Make this automatic + if (use_discrete_metric) + { + proj_params->initial_ptolemy = false; + proj_params->use_edge_flips = false; + proj_params->max_itr = 30; + } // Get input mesh Eigen::MatrixXd V, uv, N; @@ -134,7 +136,7 @@ int main(int argc, char* argv[]) else { Eigen::MatrixXd frame_field; - std::tie(frame_field, Th_hat_init)= Holonomy::generate_cross_field(V, F); + std::tie(frame_field, Th_hat_init) = Field::generate_rosy_field(V, F); } std::vector Th_hat = correct_cone_angles(Th_hat_init); @@ -159,61 +161,61 @@ int main(int argc, char* argv[]) write_vector(optimized_metric_coords, output_filename, 17); // Generate overlay VF mesh with parametrization - if (use_discrete_metric) { - auto vf_res = generate_VF_mesh_from_discrete_metric( - V, - F, - Th_hat, - optimized_metric_coords); - Eigen::MatrixXd V_l = std::get<0>(vf_res); - Eigen::MatrixXi F_l = std::get<1>(vf_res); - Eigen::MatrixXd uv_l = std::get<2>(vf_res); - Eigen::MatrixXi FT_l = std::get<3>(vf_res); - - // Write the overlay output - output_filename = join_path(output_dir, "mesh_with_uv.obj"); - write_obj_with_uv(output_filename, V_l, F_l, uv_l, FT_l); - - // Optionally show final parameterization - if (show_parameterization) view_parameterization(V_l, F_l, uv_l, FT_l); - } else { - std::vector is_cut = {}; - bool do_best_fit_scaling = false; - auto vf_res = generate_VF_mesh_from_metric( - V, - F, - Th_hat, - *cone_metric, - optimized_metric_coords, - is_cut, - do_best_fit_scaling); - OverlayMesh m_o = std::get<0>(vf_res); - Eigen::MatrixXd V_o = std::get<1>(vf_res); - Eigen::MatrixXi F_o = std::get<2>(vf_res); - Eigen::MatrixXd uv_o = std::get<3>(vf_res); - Eigen::MatrixXi FT_o = std::get<4>(vf_res); - std::vector fn_to_f_o = std::get<7>(vf_res); - std::vector> endpoints_o = std::get<8>(vf_res); - - // Write the overlay output - output_filename = join_path(output_dir, "overlay_mesh_with_uv.obj"); - write_obj_with_uv(output_filename, V_o, F_o, uv_o, FT_o); - - // Get refinement mesh - Eigen::MatrixXd V_r; - Eigen::MatrixXi F_r; - Eigen::MatrixXd uv_r; - Eigen::MatrixXi FT_r; - std::vector fn_to_f_r; - std::vector> endpoints_r; - RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); - refinement_mesh.get_VF_mesh(V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r); - - // Write the refined output - output_filename = join_path(output_dir, "refined_mesh_with_uv.obj"); - write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); - - // Optionally show final parameterization - if (show_parameterization) view_parameterization(V_r, F_r, uv_r, FT_r); - } + if (use_discrete_metric) { + auto vf_res = generate_VF_mesh_from_discrete_metric( + V, + F, + Th_hat, + optimized_metric_coords); + Eigen::MatrixXd V_l = std::get<0>(vf_res); + Eigen::MatrixXi F_l = std::get<1>(vf_res); + Eigen::MatrixXd uv_l = std::get<2>(vf_res); + Eigen::MatrixXi FT_l = std::get<3>(vf_res); + + // Write the overlay output + output_filename = join_path(output_dir, "mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_l, F_l, uv_l, FT_l); + + // Optionally show final parameterization + if (show_parameterization) view_parameterization(V_l, F_l, uv_l, FT_l); + } else { + std::vector is_cut = {}; + bool do_best_fit_scaling = false; + auto vf_res = generate_VF_mesh_from_metric( + V, + F, + Th_hat, + *cone_metric, + optimized_metric_coords, + is_cut, + do_best_fit_scaling); + OverlayMesh m_o = std::get<0>(vf_res); + Eigen::MatrixXd V_o = std::get<1>(vf_res); + Eigen::MatrixXi F_o = std::get<2>(vf_res); + Eigen::MatrixXd uv_o = std::get<3>(vf_res); + Eigen::MatrixXi FT_o = std::get<4>(vf_res); + std::vector fn_to_f_o = std::get<7>(vf_res); + std::vector> endpoints_o = std::get<8>(vf_res); + + // Write the overlay output + output_filename = join_path(output_dir, "overlay_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_o, F_o, uv_o, FT_o); + + // Get refinement mesh + Eigen::MatrixXd V_r; + Eigen::MatrixXi F_r; + Eigen::MatrixXd uv_r; + Eigen::MatrixXi FT_r; + std::vector fn_to_f_r; + std::vector> endpoints_r; + RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); + refinement_mesh.get_VF_mesh(V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r); + + // Write the refined output + output_filename = join_path(output_dir, "refined_mesh_with_uv.obj"); + write_obj_with_uv(output_filename, V_r, F_r, uv_r, FT_r); + + // Optionally show final parameterization + if (show_parameterization) view_parameterization(V_r, F_r, uv_r, FT_r); + } } diff --git a/app/parameterize_aligned.cpp b/app/parameterize_aligned.cpp index b63befd..d723e57 100644 --- a/app/parameterize_aligned.cpp +++ b/app/parameterize_aligned.cpp @@ -13,147 +13,17 @@ #include #include +// post process uv optimization +#include "optimization/metric_optimization/uv_optimization.h" + #include "polyscope/surface_mesh.h" using namespace Penner; using namespace Penner::Field; +using namespace Penner::Optimization; using namespace Penner::Holonomy; using namespace Penner::Feature; -#if USE_UV_OPTIMIZATION -#include "ExtremeOpt.h" -#include "MeshCutter.h" -#include "main_helper.h" - -SymDir::Parameters read_parameters(const nlohmann::json& config) -{ - SymDir::Parameters param; - param.max_iters = config["max_iters"]; // iterations - param.max_time = config["max_time"]; // time in seconds - param.smooth_only_iters = config["smooth_only_iters"]; - param.E_target = config["E_target"]; // Energy target - param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation - param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation - param.local_smooth = config["local_smooth"]; - param.global_smooth = config["global_smooth"]; - param.elen_alpha = config["elen_alpha"]; - param.do_projection = config["do_projection"]; - param.with_cons = config["with_cons"]; - param.Lp = config["Lp"]; - param.save_meshes = config["save_meshes"]; - param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges - param.symdir_weight = config["symdir_weight"]; - param.alignment_weight = config["alignment_weight"]; - param.degenerate_weight = config["degenerate_weight"]; - param.fix_misaligned = config["fix_misaligned"]; - param.use_rref = config["use_rref"]; - // param.solver_type = config["solver_type"]; - param.cg_rel_err = config["cg_rel_err"]; - param.cg_iters = config["cg_iters"]; - - param.percentages = config["percentages"].get>(); - param.percentage_target = config["percentage_target"]; - param.percentage_target_value = config["percentage_target_value"]; - param.save_percentages_meshes = config["save_percentages_meshes"]; - - param.E_abs_err = config["E_abs_err"]; - param.E_rel_err = config["E_rel_err"]; - param.diff_err = config["diff_err"]; - param.grad_abs_err = config["grad_abs_err"]; - param.grad_rel_err = config["grad_rel_err"]; - - param.precompute_seamless = config["precompute_seamless"]; - param.projected_newton = config["projected_newton"]; - param.soft_max = config["soft_max"]; - param.t = config["t"]; - param.precompute_seamless = config["precompute_seamless"]; - - param.percentage_target_converge = config["percentage_target_converge"]; - param.max_grad_abs_converge = config["max_grad_abs_converge"]; - param.max_grad_rel_converge = config["max_grad_rel_converge"]; - param.energy_diff_converge = config["energy_diff_converge"]; - param.use_worst_n_energy_in_ls = config["use_worst_n_energy_in_ls"]; - param.E_abs_converge = config["E_abs_converge"]; - param.E_rel_converge = config["E_rel_converge"]; - - param.last_screenshot_after_optimization = config["last_screenshot_after_optimization"]; - param.screenshot_interval = config["screenshot_interval"]; - param.output_dir_for_screenshots = config["output_dir_for_screenshots"]; - param.uv_scale_for_screenshots = config["uv_scale_for_screenshots"]; - param.angle_to_rotate_model_for_screenshots = config["angle_to_rotate_model_for_screenshots"]; - param.screenshot_during_optimization = config["screenshot_during_optimization"]; - - param.degenerate_vertices_preconditioner = config["degenerate_vertices_preconditioner"]; - param.precond_dim = config["precond_dim"]; - param.triangle_threshold = config["triangle_threshold"]; - - return param; -} - -Eigen::MatrixXd optimize_aligned_parameterization( - const Eigen::MatrixXd& V_init, - const Eigen::MatrixXi& F_init, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& thetas, - const Eigen::MatrixXi& period_jumps, - const Eigen::MatrixXi& FE_init, - const Eigen::MatrixXi& ME, - const nlohmann::json& config, - bool fix_boundary -) { - SymDir::Parameters param = read_parameters(config); - param.fix_boundary = fix_boundary; - - igl::Timer timer; - double time = 0; - timer.start(); - MeshCutter meshcutter(V_init, uv, F_init, F); - auto [V, EE] = meshcutter.cut_mesh(); - Eigen::MatrixXi FE(0, 0); - time = timer.getElapsedTime(); - if (param.do_feature_alignment) - { - // Loading the feature edge constraints - FE = meshcutter.reindex_feature_edges(FE_init); - } - double cons_residual = check_constraints(EE, FE, uv, F); - spdlog::info("Initial constraints error {}", cons_residual); - - Eigen::MatrixXi new_F; - Eigen::MatrixXd new_V, new_uv; - SymDir::ExtremeOpt extremeopt(V, F); - extremeopt.m_params = param; - - extremeopt.create_mesh(V, F, uv); - extremeopt.set_v_map(F_init, F); - - if (extremeopt.m_params.with_cons) - { - std::vector> EE_e = transform_EE(F, EE); - std::vector> FE_e; - if (extremeopt.m_params.do_feature_alignment) { - FE_e = transform_FE(F, FE); - } - extremeopt.init_constraints(EE_e); - extremeopt.EE = EE; - extremeopt.FE = FE; - extremeopt.ME = ME; - } - - //extremeopt.view(); - extremeopt.comb_matchings(reference_field, thetas, period_jumps); - - Eigen::MatrixXi F_opt = F; - Eigen::MatrixXd uv_opt; - extremeopt.do_optimization_without_log(); - extremeopt.export_mesh(V, F_opt, uv_opt); - - return uv_opt; -} - -#endif Eigen::MatrixXi tag_cone_corners( const Eigen::MatrixXd& V, @@ -478,19 +348,19 @@ int main(int argc, char* argv[]) ME(eij, 1) = misaligned_edges_r[eij][1]; } - bool fix_boundary = use_free_cones; // fix boundary if using free cones + auto [Du, Dv] = comb_frame_field(V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, period_jump_r); + SymDir::Parameters uv_param = read_parameters(config); + uv_param.fix_boundary = use_free_cones; // fix boundary if using free cones uv_r = optimize_aligned_parameterization( V_r, F_r, uv_r, FT_r, - reference_field_r, - theta_r, - period_jump_r, FE, ME, - config, - fix_boundary); + Du, + Dv, + uv_param); #else spdlog::warn("uv optimization disabled"); #endif diff --git a/app/parametrize_seamless.cpp b/app/parametrize_seamless.cpp index ef20ea1..2399a5f 100644 --- a/app/parametrize_seamless.cpp +++ b/app/parametrize_seamless.cpp @@ -38,6 +38,9 @@ #include "parametrization/refinement.h" #include "parametrization/parametrize.h" +// post process uv optimization +#include "optimization/metric_optimization/uv_optimization.h" + // cone validation #include "holonomy/holonomy/constraint.h" @@ -49,113 +52,12 @@ #include #include -#if USE_UV_OPTIMIZATION -#include "ExtremeOpt.h" -#include "MeshCutter.h" -#include "main_helper.h" -#endif - using namespace Penner; using namespace Penner::Field; +using namespace Penner::Optimization; using namespace Penner::Holonomy; -Eigen::MatrixXd optimize_seamless_parameterization( - const Eigen::MatrixXd& V_init, - const Eigen::MatrixXi& F_init, - const Eigen::MatrixXd& uv_init, - const Eigen::MatrixXi& FT_init, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& thetas, - const Eigen::MatrixXi& period_jumps, - const nlohmann::json& config -) -{ - Eigen::MatrixXd uv = uv_init; - Eigen::MatrixXi F = FT_init; - - SymDir::Parameters param; - param.max_iters = config["max_iters"]; // iterations - param.smooth_only_iters = config["smooth_only_iters"]; - param.E_target = config["E_target"]; // Energy target - param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation - param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation - // do global/local smooth (local smooth does not optimize boundary vertices) - param.local_smooth = config["local_smooth"]; - param.global_smooth = config["global_smooth"]; - param.elen_alpha = config["elen_alpha"]; - param.do_projection = config["do_projection"]; - param.with_cons = config["with_cons"]; - param.Lp = config["Lp"]; - param.save_meshes = config["save_meshes"]; - param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges - param.symdir_weight = config["symdir_weight"]; - param.alignment_weight = config["alignment_weight"]; - param.fix_misaligned = config["fix_misaligned"]; - param.use_rref = config["use_rref"]; - param.model_name = config["model"]; - param.percentage_target_converge = false; - param.use_worst_n_energy_in_ls = false; - - MeshCutter meshcutter(V_init, uv, F_init, F); - - auto [V, EE] = meshcutter.cut_mesh(); - - Eigen::MatrixXi FE_init; - Eigen::MatrixXi FE(0, 0); - Eigen::MatrixXi ME(0, 0); - if (param.do_feature_alignment) - { - // TODO - // Loading the feature edge constraints - //FE_init = meshcutter.load_feature_edges(input_file); - //FE = meshcutter.reindex_feature_edges(FE_init); - //if (param.fix_misaligned) - //{ - // std::string misaligned_file = input_dir + "/" + model + "_misaligned_edges"; - // ME = meshcutter.load_misaligned_edges(misaligned_file); - //} - } - - double cons_residual = check_constraints(EE, FE, uv, F); - spdlog::info("Initial constraints error {}", cons_residual); - - Eigen::MatrixXi new_F; - Eigen::MatrixXd new_V, new_uv; - SymDir::ExtremeOpt extremeopt(V, F); - extremeopt.m_params = param; - - extremeopt.create_mesh(V, F, uv); - - nlohmann::json opt_log; - opt_log["model_name"] = config["model"]; - opt_log["args"] = config; - - if (extremeopt.m_params.with_cons) - { - std::vector> EE_e = transform_EE(F, EE); - std::vector> FE_e; - if (extremeopt.m_params.do_feature_alignment) { - FE_e = transform_FE(F, FE); - } - extremeopt.init_constraints(EE_e); - extremeopt.EE = EE; - extremeopt.FE = FE; - extremeopt.ME = ME; - } - extremeopt.comb_matchings(reference_field, thetas, period_jumps); - extremeopt.do_optimization(opt_log); - - extremeopt.export_mesh(V, F, uv); - cons_residual = check_constraints(EE, FE, uv, F); - spdlog::info("Final constraints error {}", cons_residual); - - if (extremeopt.m_params.with_cons) extremeopt.export_EE(EE); - - return uv; -} - - int main(int argc, char* argv[]) { std::map log_level_map { @@ -328,7 +230,7 @@ int main(int argc, char* argv[]) if (use_delaunay) { marked_metric.make_discrete_metric(); flip_seq = marked_metric.get_flip_sequence(); - marked_metric.reset(); + marked_metric.reset_flip_sequence(); } // Regularize @@ -397,18 +299,23 @@ int main(int argc, char* argv[]) // Optionally optimize parameterization if (optimize) { +#if USE_UV_OPTIMIZATION std::ifstream js_in(input_json); nlohmann::json config = nlohmann::json::parse(js_in); config["model"] = mesh; + SymDir::Parameters uv_param = read_parameters(config); + auto [Du, Dv] = comb_frame_field(V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, period_jump_r); uv_r = optimize_seamless_parameterization( V_r, F_r, uv_r, FT_r, - reference_field_r, - theta_r, - period_jump_r, - config); + Du, + Dv, + uv_param); +#else + spdlog::warn("uv optimization disabled"); +#endif } //if (show_parameterization) view_triangulation(V_o, F_o, fn_to_f_o, endpoints_o, "refinement", false); diff --git a/app/test_interface.cpp b/app/test_interface.cpp new file mode 100644 index 0000000..758f191 --- /dev/null +++ b/app/test_interface.cpp @@ -0,0 +1,100 @@ +/********************************************************************************* +* This file is part of reference implementation of SIGGRAPH 2024 Paper * +* `Seamless Parametrization in Penner Coordinates` * +* v1.0 * +* * +* The MIT License * +* * +* Permission is hereby granted, free of charge, to any person obtaining a * +* copy of this software and associated documentation files (the "Software"), * +* to deal in the Software without restriction, including without limitation * +* the rights to use, copy, modify, merge, publish, distribute, sublicense, * +* and/or sell copies of the Software, and to permit persons to whom the * +* Software is furnished to do so, subject to the following conditions: * +* * +* The above copyright notice and this permission notice shall be included in * +* all copies or substantial portions of the Software. * +* * +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * +* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE * +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * +* IN THE SOFTWARE. * +* * +* Author(s): * +* Ryan Capouellez, Denis Zorin, * +* Courant Institute of Mathematical Sciences, New York University, USA * +* * * +*********************************************************************************/ +#include "field/frame_field.h" +#include "field/cross_field.h" +#include "field/intrinsic_field.h" +#include "holonomy/interface.h" +#include "feature/interface.h" +#include "holonomy/holonomy/newton.h" +#include "field/cones.h" +#include "holonomy/core/viewer.h" +#include "parametrization/refinement.h" +#include "parametrization/parametrize.h" + +// cone validation +#include "holonomy/holonomy/constraint.h" + +// infer theta from direction field +#include "field/vector_field.h" + +#include +#include +#include +#include + +#if USE_UV_OPTIMIZATION +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" +#endif + +using namespace Penner; +using namespace Penner::Field; +using namespace Penner::Holonomy; +using namespace Feature; + + +int main(int argc, char* argv[]) +{ + + // Get command line arguments + CLI::App app{"Generate a constrained seamless parametrization."}; + std::filesystem::path mesh_filename = ""; + + // IO Parameters + app.add_option("--mesh", mesh_filename, "Mesh filepath")->check(CLI::ExistingFile)->required(); + FieldParameters field_params; + MarkedMetricParameters marked_metric_params; + NewtonParameters alg_params; + + CLI11_PARSE(app, argc, argv); + std::string mesh = mesh_filename.stem(); + + // Get input mesh + Eigen::MatrixXd V, uv, N; + Eigen::MatrixXi F, FT, FN; + spdlog::info("Using mesh at {}", mesh_filename); + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + + //parametrize_seamless(V, F, field_params, alg_params); + auto field_data = generate_feature_aligned_frame_field(V, F, field_params); + auto [V_ref, F_ref, feature_edges, hard_feature_edges, reference_field, theta, kappa, period_jump] = field_data; + generate_feature_aligned_parameterization( + V_ref, + F_ref, + feature_edges, + hard_feature_edges, + reference_field, + theta, + kappa, + period_jump, + alg_params); +} diff --git a/include/feature/feature/interface.h b/include/feature/feature/interface.h index 2b6cfdf..d018d49 100644 --- a/include/feature/feature/interface.h +++ b/include/feature/feature/interface.h @@ -19,47 +19,69 @@ namespace Penner { namespace Feature { /** - * @brief Find features on a mesh with dihedral angle heuristics, and generate a refined - * mesh suitable for seamless parameterization. + * @brief Generate a parametrization satisfying feature alignment constraints inferred + * from dihedral angle based features and a smooth cross-field, optimized with a MIQ method + * + * The parametrization algorithm is based on a two-phase modified newton's method. * * @param V: mesh vertices * @param F: mesh faces - * @param use_minimal_forest: (optional) if true, use minimal feature forest instead of spanning tree - * @return refined vertex positions - * @return refined face indices - * @return refined feature edges - * @return refined feature forest edges (spanning or minimal) + * @param field_params: (optional) parameters for field generation + * @param alg_params: (optional) parameters for modified Newton method + * @return parametrized VF mesh with uv coordinates + * @return aligned feature edges + * @return target feature edges that are not fully aligned + * @return refined frame field */ -std::tuple, std::vector> generate_refined_feature_mesh( +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXi, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXd> +parametrize_aligned( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, - bool use_minimal_forest=false); + Field::FieldParameters field_params=Field::FieldParameters(), + NewtonParameters alg_params=NewtonParameters()); /** - * @brief Find an aligned cross field for a mesh cut along feature edges. + * @brief Generate feature edges and a feature aligned cross field for a mesh. * - * @param V_cut: cut mesh vertices - * @param F_cut: cut mesh faces - * @param V_map: vertex identifications in the cut mesh - * @param collapse_cones: (optional) attempt to collapse nearby cones if true - * @return base per-face reference tangent direction matrix - * @return base offset angles of a representative cross field direction relative to the reference - * @return base per-corner rotation angle of the reference direction field across the opposite edge - * @return base per-corner period jump of the cross field across the opposite edge + * Generating a feature aligned field that can be parametrized may require some moderate + * refinement, so a refined mesh is also produced. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param field_params: (optional) parameters for cross field optimization + * @return refined mesh vertices + * @return refined mesh faces + * @return feature edges to softly align to coordinate axes + * @return feature edges to hard align to coordinate axes + * @return per-face reference tangent direction matrix + * @return offset angles of a representative cross field direction relative to the reference + * @return per-corner rotation angle of the reference direction field across the opposite edge + * @return per-corner period jump of the cross field across the opposite edge */ std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + std::vector, + std::vector, Eigen::MatrixXd, Eigen::VectorXd, Eigen::MatrixXd, Eigen::MatrixXi> -generate_refined_feature_field( - const Eigen::MatrixXd& V_cut, - const Eigen::MatrixXi& F_cut, - const Eigen::VectorXi& V_map, - bool collapse_cones=false); +generate_feature_aligned_frame_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Field::FieldParameters field_params=Field::FieldParameters()); /** - * @brief Generate a parameterization aligned to features and the given cross field. + * @brief Generate a parameterization aligned to given features and a cross field. * * @param V: mesh vertices * @param F: mesh faces @@ -70,20 +92,22 @@ generate_refined_feature_field( * @param kappa: per-corner rotation angle of the reference direction field across the opposite edge * @param period_jump: per-corner period jump of the cross field across the opposite edge * @param alg_params: parameters for Newton method - * @return aligned mesh vertices - * @return aligned mesh faces - * @return aligned mesh uv vertices - * @return aligned mesh uv faces - * @return map from refined aligned mesh faces to original faces - * @return map from refined aligned vertices to original edge endpoints + * @return parametrized VF mesh with uv coordinates + * @return aligned feature edges + * @return target feature edges that are not fully aligned + * @return refined frame field */ std::tuple< Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::MatrixXi, - std::vector, - std::vector>> + std::vector, + std::vector, + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> generate_feature_aligned_parameterization( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, @@ -93,7 +117,48 @@ generate_feature_aligned_parameterization( const Eigen::VectorXd& theta, const Eigen::MatrixXd& kappa, const Eigen::MatrixXi& period_jump, - const NewtonParameters& alg_params); + NewtonParameters alg_params=NewtonParameters()); + +/** + * @brief Find features on a mesh with dihedral angle heuristics, and generate a refined + * mesh suitable for seamless parameterization. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param use_minimal_forest: (optional) if true, use minimal feature forest instead of spanning tree + * @return refined vertex positions + * @return refined face indices + * @return refined feature edges + * @return refined feature forest edges (spanning or minimal) + */ +std::tuple, std::vector> generate_refined_feature_mesh( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + bool use_minimal_forest=false); + +/** + * @brief Find an aligned cross field for a mesh cut along feature edges. + * + * @param V_cut: cut mesh vertices + * @param F_cut: cut mesh faces + * @param V_map: vertex identifications in the cut mesh + * @param collapse_cones: (optional) attempt to collapse nearby cones if true + * @return base per-face reference tangent direction matrix + * @return base offset angles of a representative cross field direction relative to the reference + * @return base per-corner rotation angle of the reference direction field across the opposite edge + * @return base per-corner period jump of the cross field across the opposite edge + */ +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_refined_feature_field( + const Eigen::MatrixXd& V_cut, + const Eigen::MatrixXi& F_cut, + const Eigen::VectorXi& V_map, + bool collapse_cones=false); + /** * @brief Generate an intrinsic metric aligned to features and the given cross field. diff --git a/include/field/field/frame_field.h b/include/field/field/frame_field.h index 8ec7ff5..6d80f80 100644 --- a/include/field/field/frame_field.h +++ b/include/field/field/frame_field.h @@ -159,7 +159,7 @@ Eigen::VectorXd rotate_vector( const Eigen::VectorXd& B1, const Eigen::VectorXd& B2); -std::vector compute_cone_angle( +std::vector compute_frame_field_cones( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, const Eigen::MatrixXd& kappa, diff --git a/include/field/field/intrinsic_field.h b/include/field/field/intrinsic_field.h index 812fbef..e47c95b 100644 --- a/include/field/field/intrinsic_field.h +++ b/include/field/field/intrinsic_field.h @@ -30,11 +30,11 @@ namespace Field { */ struct FieldParameters { - int min_cone = 0; // minimum allowed cone angle in the cross field - bool fix_cone_pair = false; // collapse infeasible cone pair on a torus + int min_cone = 1; // minimum allowed cone angle in the cross field + bool fix_cone_pair = true; // collapse infeasible cone pair on a torus bool collapse_cones = false; // collapse as many cones as possible TODO bool use_roundings = true; // round away from zero - bool use_principal_directions = false; // round away from zero + bool use_principal_directions = true; // round away from zero Scalar min_cone_pair_distance = 0.; // minimum relative cone pair distance for fitting Scalar rel_anisotropy=0.9; Scalar abs_anisotropy=0.2; diff --git a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h index 222b953..5e22d52 100644 --- a/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h +++ b/include/holonomy/holonomy/holonomy/marked_penner_cone_metric.h @@ -43,9 +43,6 @@ class MarkedPennerConeMetric : public PennerConeMetric // Additional constraints for homology loops std::vector kappa_hat; - // TODO move - VectorX original_coords; - MarkedPennerConeMetric(); /** @@ -87,26 +84,6 @@ class MarkedPennerConeMetric : public PennerConeMetric */ void reset_marked_metric(const MarkedPennerConeMetric& m); - /** - * @brief Change the metric of the given mesh given new coordinates on the original - * connectivity. - * - * The new metric is assumed to be defined on the same initial connectivity as the current - * metric but with potentially new metric coordinates. - * - * TODO: Move to base class - * - * @param m: mesh used to initialize the current mesh - * @param metric_coords: new metric coordinates - * @param need_jacobian: (optional) track change of metric jacobian if true - * @param do_repeat_flips: (optional) repeat flips to restore current connectivity if true - */ - virtual void change_metric( - const MarkedPennerConeMetric& m, - const VectorX& metric_coords, - bool need_jacobian = true, - bool do_repeat_flips = false); - /** * @brief Get number of homology basis loops * @@ -166,12 +143,17 @@ class MarkedPennerConeMetric : public PennerConeMetric // Flip method virtual bool flip_ccw(int _h, bool Ptolemy = true) override; + // WARNING: this is not quite an override since the arguments differ + virtual void change_metric( + const MarkedPennerConeMetric& m, + const VectorX& metric_coords, + bool need_jacobian = true, + bool do_repeat_flips = false); + virtual VectorX constraint(const VectorX& angles); virtual MatrixX constraint_jacobian(const VectorX& cotangents); - Scalar max_constraint_error() const; - virtual std::unique_ptr clone_marked_metric() const { return std::make_unique(MarkedPennerConeMetric(*this)); @@ -182,7 +164,6 @@ class MarkedPennerConeMetric : public PennerConeMetric protected: std::vector> m_homology_basis_loops; DualLoopManager m_dual_loop_manager; - void reset_connectivity(const MarkedPennerConeMetric& m); void reset_markings(const MarkedPennerConeMetric& m); void copy_connectivity(const MarkedPennerConeMetric& m); void copy_metric(const MarkedPennerConeMetric& m); diff --git a/include/holonomy/holonomy/holonomy/newton.h b/include/holonomy/holonomy/holonomy/newton.h index 7981f59..b8308c0 100644 --- a/include/holonomy/holonomy/holonomy/newton.h +++ b/include/holonomy/holonomy/holonomy/newton.h @@ -199,6 +199,7 @@ class OptimizeNewton // logging methods void initialize_logging(); void initialize_metric_status_log(MarkedPennerConeMetric& marked_metric); + void close_metric_status_log(); virtual void initialize_data_log(); virtual void write_data_log_entry(); diff --git a/include/holonomy/holonomy/interface.h b/include/holonomy/holonomy/interface.h index 7203a52..8fae1d3 100644 --- a/include/holonomy/holonomy/interface.h +++ b/include/holonomy/holonomy/interface.h @@ -12,6 +12,7 @@ #include "field/vector_field.h" #include "field/rotation_form.h" #include "holonomy/holonomy/marked_penner_cone_metric.h" +#include "holonomy/holonomy/newton.h" #include "holonomy/similarity/similarity_penner_cone_metric.h" #include "metric/interface.h" @@ -34,6 +35,79 @@ struct MarkedMetricParameters : ConeMetricParameters bool use_connectivity = true; // use connectivity structure for markings }; +/** + * @brief Generate a parametrization satisfying seamless constraints inferred from + * a smooth cross-field, optimized with a MIQ method + * + * The parametrization algorithm is based on a modified newton's method. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param field_params: (optional) parameters for field generation + * @param alg_params: (optional) parameters for modified Newton method + * @return parametrized VF mesh with uv coordinates + * @return cross field direction for u coordinate gradient + * @return cross field direction for v coordinate gradient + */ +std::tuple +parametrize_seamless( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Field::FieldParameters field_params=Field::FieldParameters(), + NewtonParameters alg_params=NewtonParameters()); + +/** + * @brief Generate a parametrization for the metric with given seamless constraints + * + * The parametrization algorithm is based on a modified newton's method. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param marked_metric: metric with differentiable seamless constraints + * @param alg_params: (optional) parameters for modified Newton method + * @return parametrized VF mesh with uv coordinates + * @return map from refined faces to original faces + * @return map from refined edge vertices to edge endpoints + */ +std::tuple, std::vector>> +parametrize_seamless_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const MarkedPennerConeMetric& marked_metric, + NewtonParameters alg_params=NewtonParameters()); + +/** + * @brief Generate a parametrization satisfying seamless constraints inferred from the given cross field. + * + * Since the mesh is refined by the parametrization, the cross field also needs to be refined. + * + * @param V: mesh vertices + * @param F: mesh faces + * @param theta: field angles relative to the reference directions + * @param kappa: angles across edges between reference directions + * @param period_jump: jump in period across edges + * @return parametrized VF mesh with uv coordinates + * @return refined cross field + * + */ +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_seamless_parametrization( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump, + NewtonParameters alg_params=NewtonParameters()); + /** * @brief Generate a mesh with metric from a VF mesh and cones. * diff --git a/include/metric/metric/cone_metric.h b/include/metric/metric/cone_metric.h index 549a7bb..6a15812 100644 --- a/include/metric/metric/cone_metric.h +++ b/include/metric/metric/cone_metric.h @@ -138,7 +138,7 @@ class DifferentiableConeMetric : public Mesh // Constraint methods: need to have a differentiable constraint and method to project to it // **************************************************************************************** - /// Project the metric to the constraints Th_hat, conformally or otherwise. + /// Project the metric to the constraints, conformally or otherwise. /// /// @param[out] solve_stats: (optional) statistics for the projection method /// @param[in] proj_params: (optional) parameters for the projection method @@ -149,13 +149,15 @@ class DifferentiableConeMetric : public Mesh std::unique_ptr project_to_constraint( std::shared_ptr proj_params = nullptr) const; - /// Compute the current differentiable discrepancy between the vertex cone angles and the target angles. + /// Compute the current differentiable constraints /// - /// @param[out] constraint: per-vertex cone angle constraint error (theta - Th_hat) + /// The default constraints are cone angle constraints at vertices. + /// + /// @param[out] constraint: vector valued constraint function /// @param[out] J_constraint: optional Jacobian of the constraint with respect to reduced metric coordinates /// @param[in] need_jacobian: (optional) if true, compute the Jacobian - /// @param[in] only_free_vertices: (optional) if true, only compute the angle error at vertices that are not - /// marked as fixed + /// @param[in] only_free_vertices: (optional) if true, only compute the cone angle error at vertices that are not + /// marked as fixed. virtual bool constraint( VectorX& constraint, MatrixX& J_constraint, @@ -206,6 +208,16 @@ class DifferentiableConeMetric : public Mesh /// Undo all flips that have occurred since the metric was initialized void undo_flips(); + /// Compute the current differentiable constraints + /// + /// @return: vector valued constraint function + VectorX constraint() const; + + /// Compute the maximum absolute differentiable constraint value + /// + /// @return: maximum constraint value + Scalar max_constraint_error() const; + /// Get the number of reduced independent metric coordinates /// /// @return number of reduced coordinates @@ -252,6 +264,8 @@ class DifferentiableConeMetric : public Mesh bool m_is_discrete_metric; std::vector m_flip_seq; MatrixX m_identification; + + void reset_connectivity(const Mesh& m); }; /// Differentiable cone metric using Penner coordinates. The advantage of this choice is any @@ -299,8 +313,26 @@ class PennerConeMetric : public DifferentiableConeMetric // Unique methods: methods unique to this class // ******************************************** + /** + * @brief Change the metric of the given mesh given new coordinates on the original + * connectivity. + * + * The new metric is assumed to be defined on the same initial connectivity as the current + * metric but with potentially new metric coordinates. + * + * @param m: mesh used to initialize the current mesh + * @param metric_coords: new metric coordinates + * @param need_jacobian: (optional) track change of metric jacobian if true + * @param do_repeat_flips: (optional) repeat flips to restore current connectivity if true + */ + virtual void change_metric( + const Mesh& m, + const VectorX& metric_coords, + bool need_jacobian = true, + bool do_repeat_flips = false); + /// Reset the flip sequence, treating the current metric and mesh as the base - void reset(); + void reset_flip_sequence(); protected: std::vector m_embed; diff --git a/include/metric/metric/interface.h b/include/metric/metric/interface.h index 32f39d2..0e08547 100644 --- a/include/metric/metric/interface.h +++ b/include/metric/metric/interface.h @@ -59,8 +59,6 @@ DiscreteMetric generate_discrete_metric(const Mesh& m); * * @param V: mesh vertices * @param F: mesh faces - * @param uv: mesh metric vertices - * @param F_uv: mesh metric faces * @param Th_hat: per-vertex cone angles * @param free_cones: list of cones to leave free * @param cone_metric_params: (optional) parameters for the cone mesh construction @@ -70,8 +68,6 @@ DiscreteMetric generate_discrete_metric(const Mesh& m); std::tuple> generate_cone_metric( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv, const std::vector& Th_hat, std::vector free_cones, ConeMetricParameters cone_metric_params = ConeMetricParameters()); @@ -84,7 +80,7 @@ std::tuple> generate_cone_metric( * @return marked cone metric * @return vertex reindexing from the halfedge to VF vertices */ -PennerConeMetric generate_cone_metric( +PennerConeMetric generate_cone_metric_from_mesh( const Mesh& m, ConeMetricParameters cone_metric_params = ConeMetricParameters()); diff --git a/include/optimization/optimization/metric_optimization/uv_optimization.h b/include/optimization/optimization/metric_optimization/uv_optimization.h new file mode 100644 index 0000000..cd4a159 --- /dev/null +++ b/include/optimization/optimization/metric_optimization/uv_optimization.h @@ -0,0 +1,54 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#if USE_UV_OPTIMIZATION + +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" + + +/// @file Methods to optimize a parametrization in terms of uv coordinates, reducing distortion +/// and also aligning the gradients with a guiding cross-field, while preserving seamless constraints +/// +/// Also supports preservation of feature alignment constraints and a term to improve alignment of +/// misaligned feature edges. + +namespace Penner { +namespace Optimization { + +SymDir::Parameters load_parameters(const std::string& filepath); + +SymDir::Parameters read_parameters(const nlohmann::json& config); + +Eigen::MatrixXd optimize_seamless_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv_init, + const Eigen::MatrixXi& FT_init, + const Eigen::MatrixXd& Du, + const Eigen::MatrixXd& Dv, + SymDir::Parameters param=SymDir::Parameters()); + +Eigen::MatrixXd optimize_aligned_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F, + const Eigen::MatrixXi& FE_init, + const Eigen::MatrixXi& ME, + const Eigen::MatrixXd& Du, + const Eigen::MatrixXd& Dv, + SymDir::Parameters param=SymDir::Parameters()); + +} // namespace Optimization +} // namespace Penner + +#endif \ No newline at end of file diff --git a/include/parametrization/parametrization/error.h b/include/parametrization/parametrization/error.h new file mode 100644 index 0000000..e151551 --- /dev/null +++ b/include/parametrization/parametrization/error.h @@ -0,0 +1,232 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include "util/common.h" + +/** + * @brief Methods to layout a uv parameterization determined by intrinsic lengths. + * + */ + +namespace Penner { + + +/// Given a VF mesh, check that the signed face areas are nonnegative +/// +/// @param[in] V: mesh vertices in 2D +/// @param[in] F: mesh faces +/// @return true iff the face areas are all nonnegative +bool check_areas(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F); + +/// Given a VF mesh with uv coordinates, get the maximum error of the uv lengths +/// across cuts +/// +/// @param[in] F: mesh faces +/// @param[in] uv: mesh uv coordinates in 2D +/// @param[in] F_uv: mesh uv faces +/// @return maximum uv length error across cuts +double compute_uv_length_error( + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv); + +/// Given a VF mesh with uv coordinates, check that it satisfies fundamental uv +/// consistency constraints: +/// - uv lengths match up across cuts +/// - uv face areas are nonnegative +/// +/// @param[in] V: mesh vertices in 3D +/// @param[in] F: mesh faces +/// @param[in] uv: mesh uv coordinates in 2D +/// @param[in] F_uv: mesh uv faces +/// @return true iff the mesh passes all of the tests +bool check_uv( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv); + +/** + * @brief Compute the length of an edge between two endpoints + * + * @param uv_0: first endpoint + * @param uv_1: second endpoint + * @return length of edge + */ +double uv_length(const Eigen::Vector2d& uv_0, const Eigen::Vector2d& uv_1); + +template +int prev_halfedge( + const Mesh& m, + int hij) +{ + int hli = hij; + while (m.n[hli] != hij) + { + hli = m.n[hli]; + } + + return hli; +} + +// compute the area of the triangle with vertices ijk +template +Scalar triangle_area( + const Mesh& m, + const std::vector& u, + const std::vector& v, + int hij) +{ + int hjk = m.n[hij]; + int hli = prev_halfedge(m, hij); + Eigen::Vector2d A = {u[hli], v[hli]}; + Eigen::Vector2d B = {u[hij], v[hij]}; + Eigen::Vector2d C = {u[hjk], v[hjk]}; + return signed_area(A, B, C); +} + +// check that the signed area of the layout triangles are all positive +template +bool check_areas( + const Mesh& m, + const std::vector& u, + const std::vector& v) +{ + int num_halfedges = m.n_halfedges(); + Scalar min_area = triangle_area(m, u, v, 0); + Scalar max_area = min_area; + for (int hij = 0; hij < num_halfedges; ++hij) + { + Scalar area_hijk = triangle_area(m, u, v, hij); + min_area = min(area_hijk, min_area); + max_area = max(area_hijk, max_area); + } + spdlog::debug("minimum overlay triangle face area: {}", min_area); + spdlog::debug("maximum overlay triangle face area: {}", max_area); + + return (min_area >= 0); +} + +// check that the difference of lengths of opposite halfedges +template +Scalar compute_uv_length_error( + const Mesh& m, + const std::vector& u, + const std::vector& v) +{ + int num_halfedges = m.n_halfedges(); + Scalar max_uv_length_error = 0.; + for (int hij = 0; hij < num_halfedges; ++hij) + { + int hji = m.opp[hij]; + + // get other halfedges in the face for vertex computation + int hki = prev_halfedge(m, hij); + int hlj = prev_halfedge(m, hji); + + // get uv vertices on the edge + Eigen::Vector2d uv0i = {u[hki], v[hki]}; + Eigen::Vector2d uv0j = {u[hij], v[hij]}; + Eigen::Vector2d uv1i = {u[hji], v[hji]}; + Eigen::Vector2d uv1j = {u[hlj], v[hlj]}; + + // compute the length of each halfedge + Scalar l0 = uv_length(uv0i, uv0j); + Scalar l1 = uv_length(uv1i, uv1j); + + // determine if the max length inconsistency has increased + if (abs(l0 - l1) > 1e-8) + { + spdlog::warn("uv length consistency error for edge {}, {} is {} - {}", hij, hji, l0, l1); + } + max_uv_length_error = max(max_uv_length_error, abs(l0 - l1)); + } + + // return the max uv length error + return max_uv_length_error; +} + + + +// check that two different layouts of a mesh have consistent lengths +template +bool check_uv_consistency( + const Mesh& m, + const std::vector& u0, + const std::vector& v0, + const std::vector& u1, + const std::vector& v1) +{ + int num_halfedges = m.n_halfedges(); + std::vector u_error(num_halfedges); + std::vector v_error(num_halfedges); + Scalar max_consistency_error = 0.; + for (int hij = 0; hij < num_halfedges; ++hij) + { + // get next halfedge + int hjk = m.n[hij]; + + // get uv vertices on the edge + Eigen::Vector2d uv0j = {u0[hij], v0[hij]}; + Eigen::Vector2d uv0k = {u0[hjk], v0[hjk]}; + Eigen::Vector2d uv1j = {u1[hij], v1[hij]}; + Eigen::Vector2d uv1k = {u1[hjk], v1[hjk]}; + + // compute the length of the halfedge in each metric + Scalar l0 = uv_length(uv0j, uv0k); + Scalar l1 = uv_length(uv1j, uv1k); + + if (abs(l0 - l1) > 1e-8) + { + spdlog::warn("uv length consistency error for {} with previous halfedge {} is {} - {}", hjk, hij, l0, l1); + spdlog::warn("local face is {}, {}, {}, {}, ...", hij, hjk, m.n[hjk], m.n[m.n[hjk]]); + } + max_consistency_error = max(max_consistency_error, abs(l0 - l1)); + } + spdlog::debug("max consistency error is {}", max_consistency_error); + + return (max_consistency_error < 1e-8); +} + +// check the edge consistency and signed area of a mesh layout +template +bool check_uv( + const Mesh& m, + const std::vector& u, + const std::vector& v, + const std::vector& is_cut) +{ + int num_halfedges = m.n_halfedges(); + bool is_valid = true; + + // Check faces agree in number + if ((u.size() != num_halfedges) || (v.size() != num_halfedges)) { + spdlog::error("uv coordinates not in correspondence with mesh halfedges"); + is_valid = false; + } + + // Check length consistency + Scalar uv_length_error = compute_uv_length_error(m, u, v); + if (!float_equal(uv_length_error, 0.0, 1e-6)) { + spdlog::warn("Inconsistent uv length error {} across edges", uv_length_error); + } + + // Check uv face areas + if (!check_areas(m, u, v)) { + spdlog::error("Triangle is flipped in overlay"); + is_valid = false; + } + + // Return true if no issues found + return is_valid; +} + + +} // namespace Penner \ No newline at end of file diff --git a/include/parametrization/parametrization/layout.h b/include/parametrization/parametrization/layout.h index a6a9da7..eaeb381 100644 --- a/include/parametrization/parametrization/layout.h +++ b/include/parametrization/parametrization/layout.h @@ -19,48 +19,13 @@ namespace Penner { -/// Given a VF mesh, check that the signed face areas are nonnegative -/// -/// @param[in] V: mesh vertices in 2D -/// @param[in] F: mesh faces -/// @return true iff the face areas are all nonnegative -bool check_areas(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F); - -/// Given a VF mesh with uv coordinates, get the maximum error of the uv lengths -/// across cuts -/// -/// @param[in] F: mesh faces -/// @param[in] uv: mesh uv coordinates in 2D -/// @param[in] F_uv: mesh uv faces -/// @return maximum uv length error across cuts -double compute_uv_length_error( - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv); - -/// Given a VF mesh with uv coordinates, check that it satisfies fundamental uv -/// consistency constraints: -/// - uv lengths match up across cuts -/// - uv face areas are nonnegative -/// -/// @param[in] V: mesh vertices in 3D -/// @param[in] F: mesh faces -/// @param[in] uv: mesh uv coordinates in 2D -/// @param[in] F_uv: mesh uv faces -/// @return true iff the mesh passes all of the tests -bool check_uv( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv); - /// Given a halfedge mesh, do a bfs on dual graph of mesh to produce a cut /// /// Note that this only lays out the connected component containing the start halfedge. /// /// @param m: mesh data structure /// @param is_cut_h: (optional) pre-defined cuts to be included -/// @param start_h: the first halfedge to be laid out, can be used to control the axis-alignment for the whole patch +/// @param start_h: (optional) the first halfedge to be laid out, can be used to control the axis-alignment for the whole patch /// @return #h vector, mark whether the current halfedge is part of cut graph std::vector compute_layout_topology(const Mesh& m, const std::vector& is_cut_h, int start_h = -1); @@ -87,16 +52,25 @@ std::vector pullback_cut_to_overlay( const std::vector& is_cut_h, bool is_original_cut = true); -// TODO: unclear why this is here. May be deprecated. -Eigen::Matrix compute_layout_vertex( - const Eigen::Matrix& p1, - const Eigen::Matrix& p2, - Scalar l0, - Scalar l1, - Scalar l2); - -// TODO: Document this technical function -// Exposed for usage in other libraries +/** + * @brief Core layout method for generating a parametrization from + * a mesh with metric. + * + * WARNING: this is a technical function with numerous exposed parameters. + * In particular, it uses a metric representation augmented by conformal scale + * factors, which is used for conformal methods and for optional numerical stability. + * + * For a simpler parametrization method, use interface code in parametrize.h + * + * @param _m: original mesh with initial metric and connectivity + * @param mo: overly mesh with final metric (up to scale factors) and connectivity + * @param vtx_reindex: map from halfedge to VF vertices + * @param u: scale factors to apply to the final metric + * @param V_overlay: interpolated vertices of the overlay mesh + * @param is_cut_orig: cut on the original mesh to propagate to the final parametrization + * @param is_cut: cut on the final mesh to use for the layout + * @param use_uniform_bc: (optional) if true, use uniform barycentric coordinates for layout + */ template std:: tuple< @@ -110,23 +84,25 @@ std:: std::vector, // Fn_to_F std::vector> // endpoints_o > - consistent_overlay_mesh_to_VL( + layout_overlay_mesh( const Mesh& _m, OverlayMesh& mo, const std::vector& vtx_reindex, - const std::vector& is_bd, std::vector& u, std::vector>& V_overlay, - std::vector>& endpoints, const std::vector& is_cut_orig, const std::vector& is_cut, - bool use_uniform_bc=false, - std::string layout_output_path=""); - - double signed_area( - const Eigen::Vector2d& A, - const Eigen::Vector2d& B, - const Eigen::Vector2d& C); + bool use_uniform_bc=false); + +inline double signed_area( + const Eigen::Vector2d& A, + const Eigen::Vector2d& B, + const Eigen::Vector2d& C) +{ + const Eigen::Vector2d AB = B - A; + const Eigen::Vector2d AC = C - A; + return AB.x() * AC.y() - AB.y() * AC.x(); +} /** * @brief Generate a mesh of the layout of the intrinsic metric. @@ -160,4 +136,4 @@ std::tuple, std::vector, std::vector> compute_ #endif -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/include/parametrization/parametrization/parametrize.h b/include/parametrization/parametrization/parametrize.h index 8e29dcd..0c419df 100644 --- a/include/parametrization/parametrization/parametrize.h +++ b/include/parametrization/parametrization/parametrize.h @@ -17,17 +17,37 @@ namespace Penner { +/// Given a mesh with initial target and final optimized metric coordinates, generate a corresponding +/// overlay VF mesh with parametrization. +/// +/// @param[in] V: initial mesh vertices +/// @param[in] F: initial mesh faces +/// @param[in] initial_cone_metric: initial metric +/// @param[in] reduced_metric_coords: optimized mesh metric coordinates +/// @return parametrized VF mesh with cut and topology mapping information +std::tuple< + Eigen::MatrixXd, // V_o + Eigen::MatrixXi, // F_o + Eigen::MatrixXd, // uv_o + Eigen::MatrixXi, // FT_o + std::vector, // Fn_to_F_o + std::vector>> // endpoints_o +parametrize_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const DifferentiableConeMetric& initial_cone_metric, + const VectorX& reduced_metric_coords); + /// Given a mesh with initial target and final optimized metric coordinates, generate a corresponding /// overlay VF mesh with parametrization. /// /// @param[in] V: initial mesh vertices /// @param[in] F: initial mesh faces /// @param[in] Th_hat: initial target angles -/// @param[in] m: mesh -/// @param[in] vtx_reindex: map from old to new vertices -/// @param[in] reduced_metric_target: initial mesh Penner coordinates -/// @param[in] reduced_metric_coords: optimized mesh metric -/// @param[in] do_best_fit_scaling: if true, extract best fit scale factors from the metric +/// @param[in] initial_cone_metric: initial metric +/// @param[in] reduced_metric_coords: optimized mesh metric coordinates +/// @param[in] is_cut: (optional) cut on the mesh to use for layout +/// @param[in] do_best_fit_scaling: (optional) if true, use conformal scaling for numerical stability /// @return parametrized VF mesh with cut and topology mapping information std:: tuple< @@ -49,7 +69,31 @@ std:: const VectorX& reduced_metric_coords, std::vector is_cut = {}, bool do_best_fit_scaling = false); - + +/// Given a mesh with final log edge length coordinates, generate a corresponding +/// overlay VF mesh with parametrization. +/// +/// @param[in] V: initial mesh vertices +/// @param[in] F: initial mesh faces +/// @param[in] Th_hat: initial target angles +/// @param[in] is_cut: (optional) cut on the mesh to use for layout +/// @param[in] reduced_log_edge_lengths: optimized mesh metric coordinates +/// @return parametrized VF mesh with cut and topology mapping information +std:: + tuple< + Eigen::MatrixXd, // V_o + Eigen::MatrixXi, // F_o + Eigen::MatrixXd, // uv_o + Eigen::MatrixXi, // FT_o + std::vector // is_cut_h + > + generate_VF_mesh_from_discrete_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const std::vector& Th_hat, + const VectorX& reduced_log_edge_lengths, + std::vector cut_h = {}); + template std:: tuple< @@ -70,23 +114,8 @@ std:: const DifferentiableConeMetric& initial_cone_metric, const VectorX& reduced_metric_coords, std::vector cut_h, - bool do_best_fit_scaling, - bool use_uniform_bc=false, - std::string layout_output_path=""); + bool do_best_fit_scaling=false, + bool use_uniform_bc=false); -std:: - tuple< - Eigen::MatrixXd, // V_o - Eigen::MatrixXi, // F_o - Eigen::MatrixXd, // uv_o - Eigen::MatrixXi, // FT_o - std::vector // is_cut_h - > - generate_VF_mesh_from_discrete_metric( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const std::vector& Th_hat, - const VectorX& reduced_log_edge_lengths, - std::vector cut_h = {}); } // namespace Penner \ No newline at end of file diff --git a/py/.polyscope.ini b/py/.polyscope.ini new file mode 100644 index 0000000..d868f19 --- /dev/null +++ b/py/.polyscope.ini @@ -0,0 +1,6 @@ +{ + "windowHeight": 720, + "windowPosX": 20, + "windowPosY": 59, + "windowWidth": 1280 +} diff --git a/py/imgui.ini b/py/imgui.ini new file mode 100644 index 0000000..93cb4fc --- /dev/null +++ b/py/imgui.ini @@ -0,0 +1,25 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Polyscope] +Pos=10,10 +Size=305,156 +Collapsed=0 + +[Window][Structures] +Pos=10,186 +Size=305,524 +Collapsed=0 + +[Window][Selection] +Pos=770,30 +Size=500,230 +Collapsed=0 + +[Window][Controls] +Pos=325,10 +Size=440,378 +Collapsed=0 + diff --git a/py/iteration_energy_log.csv b/py/iteration_energy_log.csv new file mode 100644 index 0000000..d89a45c --- /dev/null +++ b/py/iteration_energy_log.csv @@ -0,0 +1,623 @@ +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +16.90620771,0.00000000,0.00000000,0.00000000,-6.43470293, +88.34390562,0.13921851,0.00645359,0.00302222,-6.39897403, +99.53635488,0.14711632,0.00687624,0.00322015,-6.39897403, +114.96156150,0.15738566,0.00741044,0.00347031,-6.39897403, +115.45713647,0.15769410,0.00742806,0.00347857,-6.39897403, +115.45840171,0.15769480,0.00742810,0.00347858,-6.39897403, +115.45840174,0.15769480,0.00742810,0.00347858,-6.39897403, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +16.90620771,0.00000000,0.00000000,0.00000000,-6.43470293, +88.34390562,0.13921851,0.00645359,0.00302222,-6.39897403, +99.53635488,0.14711632,0.00687624,0.00322015,-6.39897403, +114.96156150,0.15738566,0.00741044,0.00347031,-6.39897403, +115.45713647,0.15769410,0.00742806,0.00347857,-6.39897403, +115.45840171,0.15769480,0.00742810,0.00347858,-6.39897403, +115.45840174,0.15769480,0.00742810,0.00347858,-6.39897403, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +16.90620771,0.00000000,0.00000000,0.00000000,-6.43470293, +88.34390562,0.13921851,0.00645359,0.00302222,-6.39897403, +99.53635488,0.14711632,0.00687624,0.00322015,-6.39897403, +114.96156150,0.15738566,0.00741044,0.00347031,-6.39897403, +115.45713647,0.15769410,0.00742806,0.00347857,-6.39897403, +115.45840171,0.15769480,0.00742810,0.00347858,-6.39897403, +115.45840174,0.15769480,0.00742810,0.00347858,-6.39897403, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +16.90620771,0.00000000,0.00000000,0.00000000,-6.43470293, +88.34390562,0.13921851,0.00645359,0.00302222,-6.39897403, +99.53635488,0.14711632,0.00687624,0.00322015,-6.39897403, +114.96156150,0.15738566,0.00741044,0.00347031,-6.39897403, +115.45713647,0.15769410,0.00742806,0.00347857,-6.39897403, +115.45840171,0.15769480,0.00742810,0.00347858,-6.39897403, +115.45840174,0.15769480,0.00742810,0.00347858,-6.39897403, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +16.90620771,0.00000000,0.00000000,0.00000000,-6.43470293, +88.34390562,0.13921851,0.00645359,0.00302222,-6.39897403, +99.53635488,0.14711632,0.00687624,0.00322015,-6.39897403, +114.96156150,0.15738566,0.00741044,0.00347031,-6.39897403, +115.45713647,0.15769410,0.00742806,0.00347857,-6.39897403, +115.45840171,0.15769480,0.00742810,0.00347858,-6.39897403, +115.45840174,0.15769480,0.00742810,0.00347858,-6.39897403, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +73.88944751,0.00000000,0.00000000,0.00000000,-6.46953110, +l2_energy,rmsre,rmse,rrmse,average, +996.98440521,0.00000000,0.00000000,0.00000000,-4.77133942, +26.98083306,0.02358938,0.00231898,0.00015120,-4.72358949, +189.65335772,0.06327164,0.00619081,0.00040364,-4.72358949, +663.36809975,0.12230343,0.01195987,0.00077977,-4.72358949, +931.24285635,0.14634427,0.01428832,0.00093159,-4.72358949, +1096.91695517,0.16068484,0.01567776,0.00102218,-4.72358949, +1290.47491507,0.17702191,0.01725911,0.00112528,-4.72358949, +1291.32670967,0.17710802,0.01726985,0.00112598,-4.72358949, +1291.34730847,0.17711065,0.01727014,0.00112600,-4.72358949, +1291.34732236,0.17711065,0.01727014,0.00112600,-4.72358949, +1291.34732236,0.17711065,0.01727014,0.00112600,-4.72358949, +1291.34732236,0.17711065,0.01727014,0.00112600,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1377.11770514,0.00000000,0.00000000,0.00000000,-4.79378555, +5.18055512,0.01044483,0.00136980,0.00008676,-4.72358949, +21.46277435,0.02349018,0.00266251,0.00016864,-4.72358949, +27.00721401,0.02681139,0.00317621,0.00020118,-4.72358949, +27.89840601,0.02733384,0.00326211,0.00020662,-4.72358949, +27.90251296,0.02733609,0.00326280,0.00020667,-4.72358949, +27.90251978,0.02733610,0.00326280,0.00020667,-4.72358949, +27.90251978,0.02733610,0.00326280,0.00020667,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1493.42130779,0.00000000,0.00000000,0.00000000,-4.79799052, +1.16211400,0.00492224,0.00071343,0.00004489,-4.72358949, +1.23555703,0.00507796,0.00074843,0.00004710,-4.72358949, +1.28003938,0.00517046,0.00077103,0.00004852,-4.72358949, +1.30439757,0.00522056,0.00078363,0.00004931,-4.72358949, +1.31712896,0.00524660,0.00079026,0.00004973,-4.72358949, +1.32363565,0.00525987,0.00079366,0.00004994,-4.72358949, +1.32692460,0.00526657,0.00079538,0.00005005,-4.72358949, +1.32857803,0.00526994,0.00079624,0.00005010,-4.72358949, +1.32940699,0.00527162,0.00079668,0.00005013,-4.72358949, +1.32982203,0.00527247,0.00079689,0.00005015,-4.72358949, +1.33002969,0.00527289,0.00079700,0.00005015,-4.72358949, +1.33013356,0.00527310,0.00079706,0.00005016,-4.72358949, +1.33018550,0.00527321,0.00079708,0.00005016,-4.72358949, +1.33021147,0.00527326,0.00079710,0.00005016,-4.72358949, +1.33022446,0.00527328,0.00079710,0.00005016,-4.72358949, +1.33023095,0.00527330,0.00079711,0.00005016,-4.72358949, +1.33023420,0.00527330,0.00079711,0.00005016,-4.72358949, +1.33023582,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023663,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023704,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023724,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023734,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023739,0.00527331,0.00079711,0.00005016,-4.72358949, +1.33023744,0.00527331,0.00079711,0.00005016,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1535.77008427,0.00000000,0.00000000,0.00000000,-4.79999420, +0.01097095,0.00047149,0.00008148,0.00000511,-4.72358949, +0.01103272,0.00047282,0.00008181,0.00000513,-4.72358949, +0.01103273,0.00047282,0.00008181,0.00000513,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1537.31025540,0.00000000,0.00000000,0.00000000,-4.80006184, +0.00000001,0.00000047,0.00000006,0.00000000,-4.72358949, +0.00000001,0.00000047,0.00000006,0.00000000,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1537.31203483,0.00000000,0.00000000,0.00000000,-4.80006196, +0.00000000,0.00000000,0.00000000,0.00000000,-4.72358949, +l2_energy,rmsre,rmse,rrmse,average, +1537.31203483,0.00000000,0.00000000,0.00000000,-4.80006196, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +109.06047676,0.00000000,0.00000000,0.00000000,-4.67985019, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +1491.68182014,0.00000000,0.00000000,0.00000000,-4.79885277, +l2_energy,rmsre,rmse,rrmse,average, +l2_energy,rmsre,rmse,rrmse,average, diff --git a/py/iteration_stability_log.csv b/py/iteration_stability_log.csv new file mode 100644 index 0000000..d050fc6 --- /dev/null +++ b/py/iteration_stability_log.csv @@ -0,0 +1,623 @@ +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.98191519e+00,0.00000000e+00,274,3.83035569e-01,1.88209278e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +5.95372510e-01,1.00000000e+00,374,4.77542509e-01,1.66457947e+00,0.00000000e+00,1.32923967e+01,4.66552562e-13, +2.94125483e-01,5.00000000e-01,342,4.98734612e-01,1.63997886e+00,1.13064001e+00,3.43235817e+00,1.04565389e-12, +2.62515633e-02,1.00000000e+00,332,5.14171769e-01,1.69602205e+00,1.75038178e-01,1.88393824e+00,6.78386036e-13, +2.86352134e-04,1.00000000e+00,334,5.14057520e-01,1.69848898e+00,8.43789142e-01,1.00249158e-01,5.61949334e-14, +1.04619158e-08,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,7.83257295e-01,4.50119529e-04,1.35325577e-16, +3.55271368e-15,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,4.05030640e-01,1.74486809e-08,5.58843907e-21, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.44089210e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.98191519e+00,0.00000000e+00,274,3.83035569e-01,1.88209278e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +5.95372510e-01,1.00000000e+00,374,4.77542509e-01,1.66457947e+00,0.00000000e+00,1.32923967e+01,4.66552562e-13, +2.94125483e-01,5.00000000e-01,342,4.98734612e-01,1.63997886e+00,1.13064001e+00,3.43235817e+00,1.04565389e-12, +2.62515633e-02,1.00000000e+00,332,5.14171769e-01,1.69602205e+00,1.75038178e-01,1.88393824e+00,6.78386036e-13, +2.86352134e-04,1.00000000e+00,334,5.14057520e-01,1.69848898e+00,8.43789142e-01,1.00249158e-01,5.61949334e-14, +1.04619158e-08,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,7.83257295e-01,4.50119529e-04,1.35325577e-16, +3.55271368e-15,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,4.05030640e-01,1.74486809e-08,5.58843907e-21, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.44089210e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.98191519e+00,0.00000000e+00,274,3.83035569e-01,1.88209278e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +5.95372510e-01,1.00000000e+00,374,4.77542509e-01,1.66457947e+00,0.00000000e+00,1.32923967e+01,4.66552562e-13, +2.94125483e-01,5.00000000e-01,342,4.98734612e-01,1.63997886e+00,1.13064001e+00,3.43235817e+00,1.04565389e-12, +2.62515633e-02,1.00000000e+00,332,5.14171769e-01,1.69602205e+00,1.75038178e-01,1.88393824e+00,6.78386036e-13, +2.86352134e-04,1.00000000e+00,334,5.14057520e-01,1.69848898e+00,8.43789142e-01,1.00249158e-01,5.61949334e-14, +1.04619158e-08,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,7.83257295e-01,4.50119529e-04,1.35325577e-16, +3.55271368e-15,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,4.05030640e-01,1.74486809e-08,5.58843907e-21, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.98191519e+00,0.00000000e+00,274,3.83035569e-01,1.88209278e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +5.95372510e-01,1.00000000e+00,374,4.77542509e-01,1.66457947e+00,0.00000000e+00,1.32923967e+01,4.66552562e-13, +2.94125483e-01,5.00000000e-01,342,4.98734612e-01,1.63997886e+00,1.13064001e+00,3.43235817e+00,1.04565389e-12, +2.62515633e-02,1.00000000e+00,332,5.14171769e-01,1.69602205e+00,1.75038178e-01,1.88393824e+00,6.78386036e-13, +2.86352134e-04,1.00000000e+00,334,5.14057520e-01,1.69848898e+00,8.43789142e-01,1.00249158e-01,5.61949334e-14, +1.04619158e-08,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,7.83257295e-01,4.50119529e-04,1.35325577e-16, +3.55271368e-15,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,4.05030640e-01,1.74486809e-08,5.58843907e-21, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +4.98191519e+00,0.00000000e+00,274,3.83035569e-01,1.88209278e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +5.95372510e-01,1.00000000e+00,374,4.77542509e-01,1.66457947e+00,0.00000000e+00,1.32923967e+01,4.66552562e-13, +2.94125483e-01,5.00000000e-01,342,4.98734612e-01,1.63997886e+00,1.13064001e+00,3.43235817e+00,1.04565389e-12, +2.62515633e-02,1.00000000e+00,332,5.14171769e-01,1.69602205e+00,1.75038178e-01,1.88393824e+00,6.78386036e-13, +2.86352134e-04,1.00000000e+00,334,5.14057520e-01,1.69848898e+00,8.43789142e-01,1.00249158e-01,5.61949334e-14, +1.04619158e-08,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,7.83257295e-01,4.50119529e-04,1.35325577e-16, +3.55271368e-15,1.00000000e+00,334,5.14059328e-01,1.69850180e+00,4.05030640e-01,1.74486809e-08,5.58843907e-21, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.55271368e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +3.77475828e-15,0.00000000e+00,334,5.14059328e-01,1.69850180e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +6.28318531e+00,0.00000000e+00,4733,3.43202714e-01,2.25148255e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +4.97408273e+00,1.25000000e-01,4726,3.52957102e-01,2.25885444e+00,0.00000000e+00,5.87668838e+01,1.92903669e-10, +2.98863747e+00,2.50000000e-01,4886,3.47814680e-01,2.28435033e+00,2.53663787e-01,4.91102698e+01,1.71803172e-10, +1.64185281e+00,5.00000000e-01,5871,2.33444067e-01,2.33276783e+00,3.10004359e-01,3.51721427e+01,1.14850540e-10, +1.26791693e+00,5.00000000e-01,6666,2.30820034e-01,2.44126108e+00,5.06407969e-01,1.57670694e+01,5.11157077e-11, +6.41620090e-01,5.00000000e-01,7235,2.08041387e-01,2.35261812e+00,3.99059312e-01,8.83667805e+00,2.91354699e-11, +2.98586302e-01,1.00000000e+00,7838,2.12073371e-01,2.52474520e+00,2.71851978e-01,4.84129991e+00,1.55342603e-11, +2.80797510e-03,1.00000000e+00,7838,2.14672411e-01,2.51886174e+00,1.28210071e+00,2.37672518e-01,3.92933050e-13, +7.75352716e-06,1.00000000e+00,7838,2.14721050e-01,2.51867369e+00,1.05277017e+00,3.54242860e-03,7.76141726e-15, +3.11555226e-11,1.00000000e+00,7838,2.14721056e-01,2.51867363e+00,8.12330285e-01,3.76430543e-06,3.59295530e-18, +1.55750968e-11,5.00000000e-01,7838,2.14721056e-01,2.51867363e+00,2.85602162e-01,1.38917628e-11,1.25585968e-23, +8.88178420e-15,1.00000000e+00,7838,2.14721056e-01,2.51867363e+00,2.68493135e-01,6.91001767e-12,5.22133469e-24, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +5.57747852e+00,0.00000000e+00,7580,1.76618398e-01,2.64035150e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +3.09317978e+00,5.00000000e-01,7612,1.73226733e-01,2.63041001e+00,0.00000000e+00,6.43773570e+00,5.62781137e-12, +1.76918775e+00,1.00000000e+00,7749,1.68679434e-01,2.60990492e+00,8.16581861e-01,3.91334945e+00,4.98413490e-12, +3.51453782e-01,1.00000000e+00,7754,1.70096690e-01,2.61155184e+00,1.08393585e+00,1.71667107e+00,3.27277503e-12, +1.54451241e-02,1.00000000e+00,7742,1.70101935e-01,2.61422576e+00,5.47890659e-01,2.63355184e-01,5.12126196e-13, +4.89635571e-05,1.00000000e+00,7742,1.70099400e-01,2.61424732e+00,9.11603779e-01,5.36552141e-03,5.75779123e-15, +5.07445641e-10,1.00000000e+00,7742,1.70099396e-01,2.61424736e+00,4.98192817e-01,1.24000328e-05,1.07914570e-17, +1.06581410e-14,1.00000000e+00,7742,1.70099396e-01,2.61424736e+00,1.35796138e-01,1.24876425e-10,1.13049737e-22, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.73599907e+00,0.00000000e+00,7918,1.58866816e-01,2.58953467e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +4.15716105e-01,1.00000000e+00,7945,1.84961091e-01,2.58887221e+00,0.00000000e+00,1.52454190e+00,1.90236639e-12, +2.03452203e-01,5.00000000e-01,7945,1.75435026e-01,2.58553826e+00,1.24962661e+00,2.67921568e-01,2.06173134e-13, +1.00575214e-01,5.00000000e-01,7945,1.66709657e-01,2.58411786e+00,5.72973222e-02,1.39327059e-01,1.04190572e-13, +4.99971092e-02,5.00000000e-01,7945,1.62453235e-01,2.58347029e+00,2.64348744e-02,7.10592598e-02,5.12182049e-14, +2.49257836e-02,5.00000000e-01,7945,1.60354714e-01,2.58316231e+00,1.26303775e-02,3.58825000e-02,2.57044570e-14, +1.24446925e-02,5.00000000e-01,7945,1.59313217e-01,2.58301229e+00,6.16738833e-03,1.80297866e-02,1.31529891e-14, +6.21779635e-03,5.00000000e-01,7945,1.58794448e-01,2.58293827e+00,3.04681616e-03,9.03705692e-03,6.48716506e-15, +3.10776073e-03,5.00000000e-01,7945,1.58535563e-01,2.58290152e+00,1.51420997e-03,4.52407242e-03,3.26129079e-15, +1.55359601e-03,5.00000000e-01,7949,1.58406246e-01,2.58288320e+00,7.54808749e-04,2.26342254e-03,1.66002533e-15, +7.76726918e-04,5.00000000e-01,7949,1.58341619e-01,2.58287406e+00,3.76831068e-04,1.13205790e-03,7.98333998e-16, +3.88345687e-04,5.00000000e-01,7949,1.58309314e-01,2.58286949e+00,1.88271667e-04,5.66115610e-04,4.07992353e-16, +1.94168401e-04,5.00000000e-01,7949,1.58293163e-01,2.58286721e+00,9.41001097e-05,2.83079471e-04,2.03388710e-16, +9.70830896e-05,5.00000000e-01,7949,1.58285088e-01,2.58286606e+00,4.70409952e-05,1.41545152e-04,1.11675419e-16, +4.85412671e-05,5.00000000e-01,7949,1.58281050e-01,2.58286549e+00,2.35164119e-05,7.07739303e-05,5.15788433e-17, +2.42705641e-05,5.00000000e-01,7949,1.58279032e-01,2.58286521e+00,1.17611515e-05,3.53873036e-05,2.56476496e-17, +1.21352647e-05,5.00000000e-01,7949,1.58278022e-01,2.58286507e+00,5.88759946e-06,1.76937364e-05,1.26285547e-17, +6.06762801e-06,5.00000000e-01,7949,1.58277518e-01,2.58286499e+00,2.95903446e-06,8.84688933e-06,6.56364427e-18, +3.03381292e-06,5.00000000e-01,7949,1.58277265e-01,2.58286496e+00,1.54527269e-06,4.42345016e-06,3.21416741e-18, +1.51690619e-06,5.00000000e-01,7949,1.58277139e-01,2.58286494e+00,1.25912608e-06,2.21172641e-06,1.59363297e-18, +7.58453026e-07,5.00000000e-01,7949,1.58277076e-01,2.58286493e+00,2.16262006e-06,1.10586334e-06,7.87311467e-19, +3.79226499e-07,5.00000000e-01,7949,1.58277045e-01,2.58286493e+00,3.73517156e-06,5.52931872e-07,3.86334431e-19, +1.89613245e-07,5.00000000e-01,7949,1.58277029e-01,2.58286493e+00,7.65531113e-06,2.76465731e-07,2.01333768e-19, +9.48066159e-08,5.00000000e-01,7949,1.58277021e-01,2.58286492e+00,1.87210542e-05,1.38232961e-07,9.93833365e-20, +1.42108547e-14,1.00000000e+00,7949,1.58277013e-01,2.58286492e+00,2.91730640e-05,6.91163754e-08,4.84771690e-20, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.11093026e-01,0.00000000e+00,8175,1.43184029e-01,2.52930198e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +1.31971483e-03,1.00000000e+00,8167,1.43324017e-01,2.52868355e+00,0.00000000e+00,1.48127958e-01,1.37502820e-13, +2.41934184e-07,1.00000000e+00,8167,1.43323731e-01,2.52868318e+00,1.21734438e+00,1.19101254e-03,1.83776949e-15, +1.06581410e-14,1.00000000e+00,8167,1.43323731e-01,2.52868318e+00,1.32771512e+00,1.64233457e-07,5.31365540e-20, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.18357687e-04,0.00000000e+00,8175,1.43157711e-01,2.52874811e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +1.50788182e-09,1.00000000e+00,8175,1.43157587e-01,2.52874894e+00,0.00000000e+00,1.47974406e-04,1.34137363e-16, +8.88178420e-15,1.00000000e+00,8175,1.43157587e-01,2.52874894e+00,1.34212331e+00,5.92136966e-10,5.13265550e-22, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.53283608e-10,0.00000000e+00,8175,1.43157246e-01,2.52874981e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +1.06581410e-14,1.00000000e+00,8175,1.43157246e-01,2.52874981e+00,0.00000000e+00,1.25094336e-10,1.66613248e-22, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8175,1.43157246e-01,2.52874981e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,558,4.24932001e-01,2.20587379e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +8.88178420e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +1.06581410e-14,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +7.10542736e-15,0.00000000e+00,8199,1.25655006e-01,2.55547318e+00,0.00000000e+00,0.00000000e+00,0.00000000e+00, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, +max_error,step_size,num_flips,min_corner_angle,max_corner_angle,direction_angle_change,direction_norm,direction_residual, diff --git a/py/iteration_timing_log.csv b/py/iteration_timing_log.csv new file mode 100644 index 0000000..66970d1 --- /dev/null +++ b/py/iteration_timing_log.csv @@ -0,0 +1,16 @@ +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, +time,solve_time,constraint_time,direction_time,line_search_time, diff --git a/py/metric_status_log.csv b/py/metric_status_log.csv new file mode 100644 index 0000000..0bf68d6 --- /dev/null +++ b/py/metric_status_log.csv @@ -0,0 +1,12 @@ +num_flips, +0, +274, +299, +336, +345, +353, +353, +353, +353, +353, +353, diff --git a/py/tutorial.ipynb b/py/tutorial.ipynb new file mode 100644 index 0000000..f54e5b6 --- /dev/null +++ b/py/tutorial.ipynb @@ -0,0 +1,814 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "40c17f45", + "metadata": {}, + "source": [ + "# Homology Basis Loop Comparison\n", + "\n", + "Purpose: We look at the error and runtime for Myles meshes with both minimal and maximal length homology basis loops to determine whether we get substantial improvement with shorter loops." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d68fe70a", + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "import numpy as np\n", + "import igl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53a2a75d", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"KMP_DUPLICATE_LIB_OK\"] = \"TRUE\"\n", + "os.environ[\"OMP_NUM_THREADS\"] = \"1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0cfbffdd", + "metadata": {}, + "outputs": [], + "source": [ + "file_dir = './'\n", + "moudle_dir = os.path.join(file_dir)\n", + "sys.path.append(file_dir)\n", + "import penner" + ] + }, + { + "cell_type": "markdown", + "id": "03828c39", + "metadata": {}, + "source": [ + "First, we need a mesh to process. We assume an input mesh given as a collection of two matrices:\n", + "- `V`: a $\\#V \\times 3$ matrix determining geometry, where each row stores the position in space (as x, y, z coordinates) of a mesh vertex\n", + "- `F`: a $\\#F \\times 3$ matrix determining connectivity, where each row stores the vertex indices of a mesh face\n", + "\n", + "Meshes of this form can be easily stored and loaded with the `obj` file format" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a68cbcf6", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = os.path.join('..', '..', 'data', 'closed-Myles')\n", + "V, F = igl.read_triangle_mesh(os.path.join(data_dir, 'fertility_tri.obj'))" + ] + }, + { + "cell_type": "markdown", + "id": "c80b1eca", + "metadata": {}, + "source": [ + "## Seamless Parametrization" + ] + }, + { + "cell_type": "markdown", + "id": "58e59001", + "metadata": {}, + "source": [ + "Our method can reliably produce a seamless parametrization for closed manifold meshes. In order to achieve robust seamless parametrization, it is necessary to change the mesh connectivity. Our method produces a refinement `V_r`, `F_r` of the original mesh, along with a cut to disk connectivity `FT_r` and uv coordinates `uv_r` for this cut mesh.\n", + "\n", + "We also output target directions `Du`, `Dv` for the faces of the refined mesh. These are necessary for further optimization of the parametrization (see below)." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "bfefd4a2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\t" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-07-15 23:39:42.812] [info] 1/27954 faces fixed\n", + "[2026-07-15 23:39:43.421] [info] 461/27954 faces fixed\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "solve_cone_rounding *** Statistics of MiSo Solver ***\n", + "\t\t Number of CG iterations = 0\n", + "\t\t Number of LOCAL iterations = 1707\n", + "\t\t Number of FULL iterations = 2\n", + "\t\t Number of ROUNDING = 14438\n", + "\t\t time searching next integer = 0.363846s\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-07-15 23:39:44.445] [info] 1/27954 faces fixed\n", + "[2026-07-15 23:39:44.477] [info] Gauss-Bonnet error before cone removal: 2.2846506908535957e-09\n", + "[2026-07-15 23:39:44.477] [info] Gauss-Bonnet error after cone removal: 2.2846506908535957e-09\n", + "[2026-07-15 23:39:44.605] [info] Writing energy data to iteration_energy_log.csv\n", + "[2026-07-15 23:39:44.606] [info] Writing stability data to iteration_stability_log.csv\n", + "[2026-07-15 23:39:44.687] [info] itr(0) lm(1) max_error(3.069734796627816))\n", + "[2026-07-15 23:39:44.934] [info] itr(1) lm(0.5) max_error(1.618755830960783) rmsre (0.06580136989604664)\n", + "[2026-07-15 23:39:45.181] [info] itr(2) lm(0.5) max_error(0.8444606579320171) rmsre (0.09868487255928819)\n", + "[2026-07-15 23:39:45.466] [info] itr(3) lm(0.5) max_error(0.42597833272700747) rmsre (0.11512348176811338)\n", + "[2026-07-15 23:39:45.719] [info] itr(4) lm(0.5) max_error(0.20405692373548234) rmsre (0.12355255501195478)\n", + "[2026-07-15 23:39:45.941] [info] itr(5) lm(1) max_error(0.046115636845737384) rmsre (0.13214408047368642)\n", + "[2026-07-15 23:39:46.191] [info] itr(6) lm(1) max_error(0.0003311297526122914) rmsre (0.13215818499420154)\n", + "[2026-07-15 23:39:46.458] [info] itr(7) lm(1) max_error(3.776432677682351e-08) rmsre (0.1321580812549783)\n", + "[2026-07-15 23:39:46.728] [info] itr(8) lm(1) max_error(6.8833827526759706e-15) rmsre (0.13215808124422443)\n", + "[2026-07-15 23:39:46.999] [info] itr(9) lm(0.5) max_error(6.217248937900877e-15) rmsre (0.13215808124422482)\n", + "[2026-07-15 23:39:47.320] [info] itr(10) lm(0.25) max_error(4.6629367034256575e-15) rmsre (0.13215808124422473)\n", + "[2026-07-15 23:39:47.641] [info] itr(11) lm(0.25) max_error(5.773159728050814e-15) rmsre (0.1321580812442248)\n", + "[2026-07-15 23:39:47.938] [info] itr(12) lm(0.25) max_error(4.440892098500626e-15) rmsre (0.13215808124422498)\n", + "[2026-07-15 23:39:48.248] [info] itr(13) lm(0.25) max_error(4.218847493575595e-15) rmsre (0.13215808124422504)\n", + "[2026-07-15 23:39:48.547] [info] itr(14) lm(0.25) max_error(2.6645352591003757e-15) rmsre (0.13215808124422512)\n", + "[2026-07-15 23:39:48.848] [info] itr(15) lm(0.25) max_error(8.215650382226158e-15) rmsre (0.13215808124422537)\n", + "[2026-07-15 23:39:49.408] [info] itr(16) lm(0.00048828125) max_error(8.215650382226158e-15) rmsre (0.13215808124422537)\n", + "[2026-07-15 23:39:49.408] [info] Stopping optimization as error 8.215650382226158e-15 did not decrease\n", + "[2026-07-15 23:39:49.623] [info] [translation.cpp:294] shear change in range [-2.1807210412076516, 2.1586063890979306]\n", + "[2026-07-15 23:39:53.711] [info] combing field on 1 components\n" + ] + } + ], + "source": [ + "field_params = penner.FieldParameters()\n", + "alg_params = penner.NewtonParameters()\n", + "V_r, F_r, uv_r, FT_r, Du, Dv = penner.parametrize_seamless(V, F, field_params, alg_params)" + ] + }, + { + "cell_type": "markdown", + "id": "17e883f5", + "metadata": {}, + "source": [ + "The Penner coordinate methods provide a numerically seamless parametrization satisfying seamless and feature alignment constraints. Generally, these parametrizations are already approximately isometric and high quality. However, they can be further optimized and also made to align with the guiding field directions.\n", + "\n", + "We provide methods to perform this optimization directly in terms of the uv coordinates. We generally use our Penner coordinate parametrizations as input, but any seamless parametrization could be used here." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ac8a9f97", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of threads: 12\n", + "[2026-07-15 23:39:53.754] [info] 2402 boundary faces\n", + "[2026-07-15 23:39:53.898] [info] built map of 15179 vertices\n", + "[2026-07-15 23:39:53.904] [info] Building constraints\n", + "[2026-07-15 23:39:54.038] [info] Adding 0 misalignment constraints\n", + "[2026-07-15 23:39:54.039] [info] 2402 seam vertices\n", + "[2026-07-15 23:39:54.039] [info] computing degree for mesh with 13971 vertices\n", + "[2026-07-15 23:39:54.044] [info] 1305 independent vertices\n", + "[2026-07-15 23:39:54.044] [info] 1097 dependent vertices\n", + "[2026-07-15 23:39:54.045] [info] partially reduced system matrix has 34814 nonzeros\n", + "[2026-07-15 23:39:54.047] [info] Eliminating constraints\n", + "[2026-07-15 23:39:54.047] [info] Factorizing 2404x28164 matrix\n", + "[2026-07-15 23:39:54.047] [info] 15610 nonzeros in A\n", + "[2026-07-15 23:39:54.056] [info] Rank is 210\n", + "[2026-07-15 23:39:54.057] [info] 1552 nonzeros in R\n", + "[2026-07-15 23:39:54.057] [info] Factorizaiton complete\n", + "[2026-07-15 23:39:54.057] [info] Matrix rank: 210\n", + "[2026-07-15 23:39:54.057] [info] Removing redundant constraints\n", + "[2026-07-15 23:39:54.057] [info] Splitting upper triangular matrices\n", + "[2026-07-15 23:39:54.063] [info] Computing dependent variables\n", + "[2026-07-15 23:39:54.065] [info] 2421 nonzeros in B\n", + "[2026-07-15 23:39:54.065] [info] 2421 nonzeros in B\n", + "[2026-07-15 23:39:54.065] [info] Building matrix for independent to full variables\n", + "[2026-07-15 23:39:54.065] [info] Row reduction complete\n", + "[2026-07-15 23:39:54.065] [info] reduced matrix is 210x28164\n", + "[2026-07-15 23:39:54.065] [info] Final row has norm 2\n", + "[2026-07-15 23:39:54.065] [info] 27954 independent and 210 dependent constraints\n", + "[2026-07-15 23:39:54.082] [info] test q2 on R: 0\n", + "[2026-07-15 23:39:54.083] [info] 30375 nonzeros\n", + "[2026-07-15 23:39:54.084] [info] reduced system matrix has 46716 nonzeros\n", + "[2026-07-15 23:39:54.086] [info] constraint elimination time serial: 0.047342316s\n", + "[2026-07-15 23:39:54.226] [info] uv norm: 13713.988771566912\n", + "[2026-07-15 23:39:54.226] [info] Guv norm: 240.11593542329132\n", + "[2026-07-15 23:39:54.226] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:54.266] [info] sym Dirichlet energy: 1.9904210041106574\n", + "[2026-07-15 23:39:54.266] [info] alignment energy: 1.2031359769668133\n", + "[2026-07-15 23:39:54.693] [info] rhs norm is 0.05403463727007721\n", + "[2026-07-15 23:39:54.694] [info] residual of reduced system is 4.869936408394531e-14\n", + "gradient norm is 0.0017375\n", + "newton norm is 8.53224e+07\n", + "projected gradient is -2.25492\n", + "energy from 1.20513 to 0.586874\n", + "[2026-07-15 23:39:54.698] [info] residual of full system is 0.0011924960327396273\n", + "ls_step_size = 0.32768\n", + "[2026-07-15 23:39:54.777] [info] Linesearch failures: energy 4, flip 4\n", + "gradient norm is 0.000221802\n", + "[2026-07-15 23:39:54.869] [info] Initial E = 0.586874477438516, E_worst_2 = 1.4686062275365526, 0.5981543814659773, 0.3570658361840893\n", + "[2026-07-15 23:39:55.005] [info] uv norm: 13272.052115052089\n", + "[2026-07-15 23:39:55.006] [info] Guv norm: 222.37764280606692\n", + "[2026-07-15 23:39:55.006] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:55.042] [info] sym Dirichlet energy: 1.3543116616507997\n", + "[2026-07-15 23:39:55.042] [info] alignment energy: 0.5855201657768652\n", + "[2026-07-15 23:39:55.441] [info] rhs norm is 0.02266403459804336\n", + "[2026-07-15 23:39:55.441] [info] residual of reduced system is 1.3943247022363818e-14\n", + "newton norm is 3.87526e+07\n", + "projected gradient is -1.01859\n", + "[2026-07-15 23:39:55.445] [info] residual of full system is 0.001260102856917715\n", + "energy from 0.586874 to 0.0979003\n", + "ls_step_size = 0.8\n", + "[2026-07-15 23:39:55.480] [info] Linesearch failures: energy 0, flip 1\n", + "gradient norm is 0.000164303\n", + "[2026-07-15 23:39:55.481] [info] GLOBAL smoothing operation time serial: 0.612784152s\n", + "[2026-07-15 23:39:55.565] [info] max gradient = 0.00842072750622303\n", + "[2026-07-15 23:39:55.565] [info] 0.586874477438516, 0.09790032271606593, -1, -1\n", + "[2026-07-15 23:39:55.565] [info] max difference is 10000000000\n", + "[2026-07-15 23:39:55.700] [info] uv norm: 14008.61992847186\n", + "[2026-07-15 23:39:55.700] [info] Guv norm: 226.12102972325945\n", + "[2026-07-15 23:39:55.700] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:55.735] [info] sym Dirichlet energy: 1.7253056032630778\n", + "[2026-07-15 23:39:55.735] [info] alignment energy: 0.09617501711280285\n", + "[2026-07-15 23:39:56.115] [info] rhs norm is 0.013041564915964343\n", + "[2026-07-15 23:39:56.116] [info] residual of reduced system is 1.2331100591752043e-15\n", + "newton norm is 1.5773e+06\n", + "projected gradient is -0.0427333\n", + "energy from 0.0979003 to 0.0762259\n", + "[2026-07-15 23:39:56.119] [info] residual of full system is 0.0012435214732776415\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:56.143] [info] Linesearch failures: energy 0, flip 0\n", + "[2026-07-15 23:39:56.145] [info] GLOBAL smoothing operation time serial: 0.579668299s\n", + "gradient norm is 1.94309e-05\n", + "[2026-07-15 23:39:56.224] [info] max gradient = 0.008346226164890955\n", + "[2026-07-15 23:39:56.224] [info] 0.586874477438516, 0.09790032271606593, 0.07622590293426146, -1\n", + "[2026-07-15 23:39:56.224] [info] max difference is 10000000000\n", + "[2026-07-15 23:39:56.346] [info] uv norm: 14459.179242000098\n", + "[2026-07-15 23:39:56.346] [info] Guv norm: 233.32469534254582\n", + "[2026-07-15 23:39:56.346] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:56.379] [info] sym Dirichlet energy: 1.041150963495301\n", + "[2026-07-15 23:39:56.379] [info] alignment energy: 0.07518475197076616\n", + "[2026-07-15 23:39:56.775] [info] rhs norm is 0.0041688980306213465\n", + "[2026-07-15 23:39:56.776] [info] residual of reduced system is 5.887614816356308e-18\n", + "newton norm is 33.1696\n", + "projected gradient is -0.000519756\n", + "[2026-07-15 23:39:56.779] [info] residual of full system is 0.0012406651690669089\n", + "energy from 0.0762259 to 0.075878\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:56.803] [info] Linesearch failures: energy 0, flip 0\n", + "gradient norm is 4.6355e-06\n", + "[2026-07-15 23:39:56.805] [info] GLOBAL smoothing operation time serial: 0.5810578120000001s\n", + "[2026-07-15 23:39:56.891] [info] max gradient = 0.002516053078493462\n", + "[2026-07-15 23:39:56.891] [info] 0.586874477438516, 0.09790032271606593, 0.07622590293426146, 0.07587800939510048\n", + "[2026-07-15 23:39:56.891] [info] max difference is 0.8331835401270752\n", + "[2026-07-15 23:39:57.026] [info] uv norm: 14458.025418101854\n", + "[2026-07-15 23:39:57.026] [info] Guv norm: 233.34443870417923\n", + "[2026-07-15 23:39:57.026] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:57.060] [info] sym Dirichlet energy: 0.774834085440707\n", + "[2026-07-15 23:39:57.060] [info] alignment energy: 0.07510317530965978\n", + "[2026-07-15 23:39:57.464] [info] rhs norm is 0.0016643200654072477\n", + "[2026-07-15 23:39:57.465] [info] residual of reduced system is 2.3700646679168697e-18\n", + "newton norm is 16.1421\n", + "projected gradient is -0.000217912\n", + "[2026-07-15 23:39:57.468] [info] residual of full system is 0.0012392350721450475\n", + "energy from 0.075878 to 0.0757306\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:57.491] [info] Linesearch failures: energy 0, flip 0\n", + "[2026-07-15 23:39:57.492] [info] GLOBAL smoothing operation time serial: 0.601500197s\n", + "gradient norm is 2.09728e-06\n", + "[2026-07-15 23:39:57.571] [info] max gradient = 0.001035899415522459\n", + "[2026-07-15 23:39:57.571] [info] 0.07573064023669644, 0.09790032271606593, 0.07622590293426146, 0.07587800939510048\n", + "[2026-07-15 23:39:57.571] [info] max difference is 0.22139273069268023\n", + "[2026-07-15 23:39:57.699] [info] uv norm: 14457.191816714176\n", + "[2026-07-15 23:39:57.699] [info] Guv norm: 233.35361685060963\n", + "[2026-07-15 23:39:57.699] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:57.736] [info] sym Dirichlet energy: 0.6615251167679209\n", + "[2026-07-15 23:39:57.736] [info] alignment energy: 0.07506911511992852\n", + "[2026-07-15 23:39:58.155] [info] rhs norm is 0.0006690707327324385\n", + "[2026-07-15 23:39:58.156] [info] residual of reduced system is 1.1579784008882487e-18\n", + "newton norm is 7.61642\n", + "projected gradient is -9.17494e-05\n", + "[2026-07-15 23:39:58.160] [info] residual of full system is 0.0012373798530030723\n", + "energy from 0.0757306 to 0.0756679\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:58.184] [info] Linesearch failures: energy 0, flip 0\n", + "[2026-07-15 23:39:58.186] [info] GLOBAL smoothing operation time serial: 0.614981093s\n", + "gradient norm is 1.62307e-06\n", + "[2026-07-15 23:39:58.278] [info] max gradient = 0.00042459039032905085\n", + "[2026-07-15 23:39:58.278] [info] 0.07573064023669644, 0.0756679312853896, 0.07622590293426146, 0.07587800939510048\n", + "[2026-07-15 23:39:58.278] [info] max difference is 0.004563980560007418\n", + "[2026-07-15 23:39:58.415] [info] uv norm: 14456.761256827007\n", + "[2026-07-15 23:39:58.415] [info] Guv norm: 233.3578999344464\n", + "[2026-07-15 23:39:58.415] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:58.453] [info] sym Dirichlet energy: 0.6156647087713262\n", + "[2026-07-15 23:39:58.453] [info] alignment energy: 0.07505226657661826\n", + "[2026-07-15 23:39:58.859] [info] rhs norm is 0.000272365796097327\n", + "[2026-07-15 23:39:58.859] [info] residual of reduced system is 1.1345367944143563e-18\n", + "newton norm is 2.72395\n", + "projected gradient is -4.16881e-05\n", + "energy from 0.0756679 to 0.0756393\n", + "[2026-07-15 23:39:58.863] [info] residual of full system is 0.001235736746300412\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:58.886] [info] Linesearch failures: energy 0, flip 0\n", + "[2026-07-15 23:39:58.888] [info] GLOBAL smoothing operation time serial: 0.610257024s\n", + "gradient norm is 1.52976e-06\n", + "[2026-07-15 23:39:58.968] [info] max gradient = 0.00016987714218283942\n", + "[2026-07-15 23:39:58.968] [info] 0.07573064023669644, 0.0756679312853896, 0.07563926183735509, 0.07587800939510048\n", + "[2026-07-15 23:39:58.968] [info] max difference is 0.0019421853522365875\n", + "[2026-07-15 23:39:59.099] [info] uv norm: 14456.604394543401\n", + "[2026-07-15 23:39:59.099] [info] Guv norm: 233.3608622280683\n", + "[2026-07-15 23:39:59.099] [info] uTvT norm: 236.44872594285664\n", + "[2026-07-15 23:39:59.129] [info] sym Dirichlet energy: 0.5989230245920802\n", + "[2026-07-15 23:39:59.129] [info] alignment energy: 0.07504033881276301\n", + "[2026-07-15 23:39:59.543] [info] rhs norm is 0.0001086536704740645\n", + "[2026-07-15 23:39:59.544] [info] residual of reduced system is 4.0204844048090377e-19\n", + "newton norm is 1.109\n", + "projected gradient is -1.37879e-05\n", + "[2026-07-15 23:39:59.548] [info] residual of full system is 0.001234938913543747\n", + "energy from 0.0756393 to 0.0756299\n", + "ls_step_size = 1\n", + "[2026-07-15 23:39:59.573] [info] Linesearch failures: energy 0, flip 0\n", + "[2026-07-15 23:39:59.575] [info] GLOBAL smoothing operation time serial: 0.6071312200000001s\n", + "[2026-07-15 23:39:59.661] [info] max gradient = 6.149223025654463e-05\n", + "[2026-07-15 23:39:59.661] [info] Reach target gradient(0.0001) with abs err 0.0001, optimization succeed!\n", + "[2026-07-15 23:39:59.661] [info] Reached max iteration.\n", + "[2026-07-15 23:39:59.661] [info] Total optimization time: 5.7579925560000005s\n" + ] + } + ], + "source": [ + "uv_param = penner.load_parameters(\"../app/symdir.json\")\n", + "uv_r = penner.optimize_seamless_parameterization(V_r, F_r, uv_r, FT_r, Du, Dv, uv_param)" + ] + }, + { + "cell_type": "markdown", + "id": "aff338e9", + "metadata": {}, + "source": [ + "This seamless parametrization can be visualized with the following viewer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "442716eb", + "metadata": {}, + "outputs": [], + "source": [ + "penner.view_seamless_parameterization(V_r, F_r, uv_r, FT_r, \"seamless\", True)" + ] + }, + { + "cell_type": "markdown", + "id": "94410c8a", + "metadata": {}, + "source": [ + "We infer the seamless constraints for our parametrization using a guiding cross field. We use a facet-based cross field, with four rotationally symmetric vectors in each face.\n", + "\n", + "The cross field is represented by a principal direction $d_0$ per face, where the remaining directions $d_1, d_2, d_3$ determined by symmetry. We also explicitly encode the period jumps across edges. A period jump of $i$ across an edge implies that $d_k$ in one face corresponds to the direction $d_{(k + i) \\mod 4}$ in the adjacent face. These period jumps determine the singularities and general topology of the cross field.\n", + "\n", + "We provide an implementation to optimize a smooth cross field on the surface using the mixed integer formulation of Bommes et al. 2009.\n", + "\n", + "The field is represented with the following matrices:\n", + "- `reference_field`: a $\\#F \\times 3$ matrix encoding reference directions $d_r$ on the face, where each row stores the coordinates of a direction on a face\n", + "- `theta`: a length $\\#F$ vector of angles, where each angle corresponds to a counterclockwise rotation in the plane of the face. Rotating the reference direction $d_r$ in the face by this angle gives the representative direction $d_0$ of the smooth cross field. The other three directions are obtained by additional rotations of $\\pi/2$\n", + "- `kappa`: a $\\#F \\times 3$ matrix storing the intrinsic rotation of the reference field directions across the edge opposite the triangle corner. While this angle can be inferred directly from the reference field, there is ambiguity due to periodicity. Rather than using a fragile convention, we store the angles explicitly.\n", + "- `period_jump`: a $\\#F \\times 3$ matrix storing period jumps across the edge opposite the triangle corner.\n", + "\n", + "We also provide custom functions to read and write these matrices to file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "588dde0f", + "metadata": {}, + "outputs": [], + "source": [ + "field_params = penner.FieldParameters()\n", + "reference_field, theta, kappa, period_jump = penner.generate_frame_field(V, F, field_params)" + ] + }, + { + "cell_type": "markdown", + "id": "4bd3b601", + "metadata": {}, + "source": [ + "The following function can be used to view the guiding cross field" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a94e17f3", + "metadata": {}, + "outputs": [], + "source": [ + "penner.view_cross_field(V, F, reference_field, theta, kappa, period_jump, \"field\")" + ] + }, + { + "cell_type": "markdown", + "id": "bcbf723c", + "metadata": {}, + "source": [ + "If a target field is provided, it can be parametrized with the following method.\n", + "\n", + "Since the parametrization may refine the original mesh, this method also refines the input field to produce an equivalent field on the refined mesh." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a578a7bf", + "metadata": {}, + "outputs": [], + "source": [ + "alg_params = penner.NewtonParameters()\n", + "param_data = penner.generate_seamless_parametrization(V, F, reference_field, theta, kappa, period_jump, alg_params)\n", + "V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, kappa_r, period_jump_r = param_data" + ] + }, + { + "cell_type": "markdown", + "id": "70fc5923", + "metadata": {}, + "source": [ + "The following methods explore the inner workings of our approach.\n", + "\n", + "The core data structure of this library is a halfedge mesh representation that stores differentiable intrinsic metric coordinates (in particular, Penner-coordinates).\n", + "\n", + "These meshes also explicitly store the desired constraints. The following function constructs a differentiable metric to satisfy seamless constraints inferred from the cross field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc3bbe78", + "metadata": {}, + "outputs": [], + "source": [ + "marked_metric_params = penner.MarkedMetricParameters()\n", + "marked_metric, vtx_reindex, rotation_form, Th_hat = penner.generate_metric_from_field(\n", + " V,\n", + " F,\n", + " theta,\n", + " kappa,\n", + " period_jump,\n", + " marked_metric_params)" + ] + }, + { + "cell_type": "markdown", + "id": "cd10b64c", + "metadata": {}, + "source": [ + "The core method of this library is a modified Newton's method that seeks a root of the nonlinear constraints, e.g., seamless constraints. The following method runs this optimization and constructs a refinement of the original mesh that satisfies these constraints.\n", + "\n", + "The following data describes the relationship of the refined mesh to the original mesh:\n", + "- `fn_to_f`: a map from the faces of the refined mesh to the parent face of the original mesh. Note that each refined mesh face has a single, well-defined parent.\n", + "- `endpoints`: a map from vertices of the refined mesh to pairs of vertices of the original mesh. For vertices in a refined edge, these are the endpoints of the edge containing the vertex. For vertices already present in the original mesh, this is a dummy value. We do not introduce any vertices in the interiors of faces.\n", + "\n", + "These maps are sufficient to reconstruct essentially all element correspondences of the refined mesh. For instance, these maps can be used to refine the input cross field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fc42804", + "metadata": {}, + "outputs": [], + "source": [ + "alg_params = penner.NewtonParameters()\n", + "V_r, F_r, uv_r, FT_r, fn_to_f, endpoints = penner.parametrize_seamless_metric(V, F, marked_metric, alg_params)" + ] + }, + { + "cell_type": "markdown", + "id": "e98e9e6f", + "metadata": {}, + "source": [ + "## Feature Aligned Parametrization" + ] + }, + { + "cell_type": "markdown", + "id": "ff54164a", + "metadata": {}, + "source": [ + "For surfaces with prominent feature edges, such as crease edges in mechanical models, it is essential to produce parametrizations that are aligned to these features. This enables quad meshes and spline surfaces constructed from these parametrizations to preserve these feature edges.\n", + "\n", + "We open a mesh with prominent feature edges." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b51db263", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = os.path.join('..', '..', 'data', 'closed-Myles')\n", + "V, F = igl.read_triangle_mesh(os.path.join(data_dir, 'fandisk.obj'))" + ] + }, + { + "cell_type": "markdown", + "id": "6bdd3980", + "metadata": {}, + "source": [ + "The following method produces a feature aligned seamless parametrization for closed manifold meshes. The feature edges are determined automatically using a simple dihedral angle test, with some pruning to remove spurious isolated feature edges. \n", + "\n", + "As with basic seamless parametrization, it is necessary to change the mesh connectivity. The output is a refinement `V_r`, `F_r` of the original mesh and a parametrization mesh `uv_r, FT_r`, along with target uv coordinate gradients `Du` and `Dv`. We also produce the following feature labels:\n", + "- `FE`: a $N \\times 2$ matrix, where each row corresponds to an edge with the given vertex indices as endpoints. These are the target feature edges of the mesh that are aligned in the parametrization\n", + "- `ME`: like `FE`, a matrix of edges that were tagged as features in the input mesh, but are not fully aligned in the paramterization. It may be possible to improve the alignment of these edges (see below)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c19856ce", + "metadata": {}, + "outputs": [], + "source": [ + "field_params = penner.FieldParameters()\n", + "alg_params = penner.NewtonParameters()\n", + "V_r, F_r, uv_r, FT_r, FE, ME, Du, Dv = penner.parametrize_aligned(V, F, field_params, alg_params)" + ] + }, + { + "cell_type": "markdown", + "id": "757da16e", + "metadata": {}, + "source": [ + "We provide a uv coordinate optimization that also preserves the aligned feature edges.\n", + "\n", + "In order to keep the misaligned edges softly aligned, we also add a term to the optimization energy that penalizes misalignment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ed1a5d8", + "metadata": {}, + "outputs": [], + "source": [ + "uv_param = penner.load_parameters(\"../app/symdir.json\")\n", + "uv_r = penner.optimize_aligned_parameterization(V_r, F_r, uv_r, FT_r, FE, ME, Du, Dv, uv_param)" + ] + }, + { + "cell_type": "markdown", + "id": "fea4a184", + "metadata": {}, + "source": [ + "We view the optimized parametrization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "172a17fa", + "metadata": {}, + "outputs": [], + "source": [ + "penner.view_seamless_parameterization(V_r, F_r, uv_r, FT_r, \"feature\", True)" + ] + }, + { + "cell_type": "markdown", + "id": "382b5c74", + "metadata": {}, + "source": [ + "Some edges that were identified as target feature edges may be misaligned because, in contrast with seamless constraints, many feature alignment constraints are infeasible.\n", + "\n", + "We thus separate feature alignment constraints into hard constraints, which are guaranteed to be fully aligned if the method converges, and soft constraints, where we try to maximize their alignment but do not guarantee full alignment. We generally use a spanning forest of the feature graph for hard constraints, which we find to work well in most cases.\n", + "\n", + "To infer seamless constraints compatible with the feature alignment constraints, we also compute a cross-field that is aligned with these feature edges, meaning that the cross field direction in faces adjacent to a feature edge is aligned with the edge direction. In order to achieve this alignment, some refinement of the input mesh is needed, e.g., to ensure no face is adjacent to multiple feature edges.\n", + "\n", + "We provide a method to produce feature edges and an aligned cross field on a refined mesh:\n", + "- `V`, `F`: a possibly refined mesh\n", + "- `feature_edges`: a list of all tagged feature edges in the original mesh, represented with the endpoint vertices of the edge\n", + "- `hard_feature_edges`: a subset of the feature edges that are guaranteed to be fully aligned by our method (provided that it converges)\n", + "- `reference_field`, `theta`, `kappa`, `period_jump`: a cross field aligned to the feature edges on the refined mesh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb9a1ff1", + "metadata": {}, + "outputs": [], + "source": [ + "field_params = penner.FieldParameters()\n", + "field_data = penner.generate_feature_aligned_frame_field(V, F, field_params)\n", + "V, F, feature_edges, hard_feature_edges, reference_field, theta, kappa, period_jump = field_data" + ] + }, + { + "cell_type": "markdown", + "id": "9f28288d", + "metadata": {}, + "source": [ + "The following method parametrizes the mesh with the given feature and field-inferred seamless constraints. The output is a refined mesh with feature edges and a cross field on the refined surface.\n", + "\n", + "Note that the set of hard feature edges generally may differ from the set of aligned feature edges in the paramterization, because the soft constraint feature edges may end up being fully aligned." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a053d66", + "metadata": {}, + "outputs": [], + "source": [ + "alg_params = penner.NewtonParameters()\n", + "param_data = penner.generate_feature_aligned_parameterization(\n", + " V,\n", + " F,\n", + " feature_edges,\n", + " hard_feature_edges,\n", + " reference_field,\n", + " theta,\n", + " kappa,\n", + " period_jump,\n", + " alg_params)\n", + "V_r, F_r, uv_r, FT_r, aligned_feature_edges_r, misaligned_feature_edges_r, reference_field_r, theta_r, kappa_r, period_jump_r = param_data" + ] + }, + { + "cell_type": "markdown", + "id": "a136a5e1", + "metadata": {}, + "source": [ + "## Intrinsic Metric Optimization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd0fdda1", + "metadata": {}, + "outputs": [], + "source": [ + "field_params = penner.FieldParameters()\n", + "reference_field, theta, kappa, period_jump = penner.generate_frame_field(V, F, field_params)\n", + "Th_hat = penner.compute_frame_field_cones(V, F, kappa, period_jump)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88cad87f", + "metadata": {}, + "outputs": [], + "source": [ + "free_cones = []\n", + "metric_params = penner.ConeMetricParameters()\n", + "cone_metric, vtx_reindex = penner.generate_cone_metric(V, F, Th_hat, free_cones, metric_params);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93a8d94c", + "metadata": {}, + "outputs": [], + "source": [ + "opt_energy = penner.LogLengthEnergy(cone_metric, 2)\n", + "proj_params = penner.ProjectionParameters()\n", + "opt_params = penner.OptimizationParameters()\n", + "opt_params.num_iter = 5\n", + "cone_metric_opt = penner.optimize_metric(cone_metric, opt_energy, proj_params, opt_params);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed8769c6", + "metadata": {}, + "outputs": [], + "source": [ + "coords_opt = cone_metric_opt.get_metric_coordinates()\n", + "V_r, F_r, uv_r, FT_r = penner.parametrize_metric(V, F, cone_metric, coords_opt)" + ] + }, + { + "cell_type": "markdown", + "id": "54204c73", + "metadata": {}, + "source": [ + "The following function can be used to view a generic parametrization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "229b7495", + "metadata": {}, + "outputs": [], + "source": [ + "penner.view_parametrization(V_r, F_r, uv_r, FT_r, \"cones\", True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f9ca89e", + "metadata": {}, + "outputs": [], + "source": [ + "m = 'eight'\n", + "field_path = os.path.join(data_dir, m + '.ffield')\n", + "penner.load_frame_field(field_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4217240", + "metadata": {}, + "outputs": [], + "source": [ + "#penner.parametrize_seamless(V, F, field_params, alg_params)\n", + "penner.parametrize_seamless(V, F, field_params, alg_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "267d3c2f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2629b434", + "metadata": {}, + "outputs": [], + "source": [ + "import sys, os\n", + "file_dir = '../py'\n", + "moudle_dir = os.path.join(file_dir)\n", + "sys.path.append(file_dir)\n", + "import penner\n", + "import igl\n", + "os.environ[\"KMP_DUPLICATE_LIB_OK\"] = \"TRUE\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9c5ddd6", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = os.path.join('..', '..', 'data', 'closed-Myles')\n", + "V, F = igl.read_triangle_mesh(os.path.join(data_dir, 'eight.obj'))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "penner-optimization", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c6c592f..a1d652c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ set(MetricSrc set(ParametrizationSrc parametrization/interpolation.cpp + parametrization/error.cpp parametrization/layout.cpp parametrization/parametrize.cpp parametrization/refinement.cpp @@ -80,8 +81,7 @@ target_link_libraries(PennerLib INTERFACE PennerFeatureLib ) -if(FALSE) -#if(USE_PYBIND) +if(USE_PYBIND) add_library(penner MODULE pybind.cpp ) diff --git a/src/feature/core/vf_corners.cpp b/src/feature/core/vf_corners.cpp index 220d3db..f0e8cd7 100644 --- a/src/feature/core/vf_corners.cpp +++ b/src/feature/core/vf_corners.cpp @@ -555,6 +555,8 @@ Eigen::MatrixXi compute_edge_matrix(const std::vector& edges) E(e, i) = edges[e][i]; } } + + return E; } } // namespace Feature diff --git a/src/feature/core/viewer.cpp b/src/feature/core/viewer.cpp index 0377ebb..4e6c0ea 100644 --- a/src/feature/core/viewer.cpp +++ b/src/feature/core/viewer.cpp @@ -586,7 +586,7 @@ void view_feature_cross_field( Eigen::MatrixXd V_disp = displace_cut_faces(V, F, displacement); // get cone angles - std::vector Th_hat = Field::compute_cone_angle(V, F, kappa, period_jump); + std::vector Th_hat = Field::compute_frame_field_cones(V, F, kappa, period_jump); auto [cone_positions, cone_values] = generate_cone_vertices(V_disp, Th_hat); diff --git a/src/feature/dirichlet/optimization.cpp b/src/feature/dirichlet/optimization.cpp index d0a99cc..b680331 100644 --- a/src/feature/dirichlet/optimization.cpp +++ b/src/feature/dirichlet/optimization.cpp @@ -43,7 +43,6 @@ class OptimizeDirichletRelaxation const Holonomy::NewtonParameters& input_alg_params) { alg_params = input_alg_params; - num_solves = 0; switch (alg_params.log_level) { case 6: spdlog::set_level(spdlog::level::trace); break; @@ -71,6 +70,7 @@ class OptimizeDirichletRelaxation reduced_metric_init = dirichlet_metric.get_reduced_metric_coordinates(); update_metric(initial_dirichlet_metric, reduced_metric_init); num_solves = 0; + prev_solves = -1; log.max_error = constraint.cwiseAbs().maxCoeff(); // write first data log @@ -97,6 +97,7 @@ class OptimizeDirichletRelaxation // Increment iteration and lambda log.num_iter++; lambda = min(2. * lambda, 1.); + prev_solves = num_solves; // Compute Newton descent direction update_metric(initial_dirichlet_metric, reduced_metric_coords); @@ -142,7 +143,7 @@ class OptimizeDirichletRelaxation // log data std::ofstream log_file; Holonomy::NewtonLog log; - int num_solves; + int num_solves, prev_solves; Scalar max_triangle_quality; Scalar max_hard_error; std::shared_ptr proj_params; @@ -151,6 +152,8 @@ class OptimizeDirichletRelaxation // Open a per iteration data log and write a header void initialize_data_log() { + if (alg_params.output_dir.empty()) return; + // Generate data log path std::filesystem::create_directory(alg_params.output_dir); std::string data_log_path; @@ -172,6 +175,8 @@ class OptimizeDirichletRelaxation // Write newton log iteration data to file void write_data_log_entry() { + if (alg_params.output_dir.empty()) return; + log_file << log.num_iter << ","; log_file << std::fixed << std::setprecision(17) << log.max_error << ","; log_file << std::fixed << std::setprecision(17) << max_hard_error << ","; @@ -185,6 +190,8 @@ class OptimizeDirichletRelaxation // close the log file void close_logs() { + if (alg_params.output_dir.empty()) return; + log_file.close(); } @@ -343,7 +350,7 @@ class OptimizeDirichletRelaxation } // Make final line step in original connectivity - spdlog::info("Updating metric"); + spdlog::info("{} solves performed", num_solves); dirichlet_metric.change_metric(initial_dirichlet_metric, reduced_metric_coords); } @@ -355,6 +362,12 @@ class OptimizeDirichletRelaxation return true; } + // check whether number of solves is stationary + if ((log.num_iter > 1) && (num_solves <= (prev_solves + 1))) { + spdlog::info("Stopping optimization as no more progress can be made"); + return true; + } + // check how many linear solves performed if (num_solves >= alg_params.max_itr) { spdlog::info( diff --git a/src/feature/interface.cpp b/src/feature/interface.cpp index 983a531..5433521 100644 --- a/src/feature/interface.cpp +++ b/src/feature/interface.cpp @@ -39,6 +39,157 @@ namespace Penner { namespace Feature { +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + std::vector, + std::vector, + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_feature_aligned_frame_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Field::FieldParameters field_params) +{ + // refine input mesh + auto [V_ref, F_ref, feature_edges, hard_feature_edges] = generate_refined_feature_mesh(V, F, false); + + spdlog::info("optimizing field"); + FeatureFinder feature_finder(V_ref, F_ref); + feature_finder.mark_features(feature_edges); + auto [V_cut, F_cut, V_map, F_is_feature] = feature_finder.generate_feature_cut_mesh(); + auto [reference_field, theta, kappa, period_jump] = generate_refined_feature_field(V_cut, F_cut, V_map); + + return std::make_tuple(V_ref, F_ref, feature_edges, hard_feature_edges, reference_field, theta, kappa, period_jump); +} + +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXi, + std::vector, + std::vector, + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_feature_aligned_parameterization( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const std::vector& feature_edges, + const std::vector& hard_feature_edges, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump, + NewtonParameters alg_params) +{ + // use utility class + MarkedMetricParameters marked_metric_params; + AlignedMetricGenerator aligned_metric_generator( + V, + F, + feature_edges, + hard_feature_edges, + reference_field, + theta, + kappa, + period_jump, + marked_metric_params); + + int max_itr = alg_params.max_itr; + alg_params.max_itr = 50; + aligned_metric_generator.optimize_full(alg_params); + + alg_params.max_itr = max_itr; + aligned_metric_generator.optimize_relaxed(alg_params); + + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = aligned_metric_generator.get_parameterization(); + auto [feature_face_edges, misaligned_edges] = aligned_metric_generator.get_refined_features(); + auto feature_edges_r = compute_face_edge_endpoints(feature_face_edges, F_r); + auto misaligned_edges_r = compute_face_edge_endpoints(misaligned_edges, F_r); + auto [reference_field_r, theta_r, kappa_r, period_jump_r] = aligned_metric_generator.get_refined_field(); + + return std::make_tuple( + V_r, + F_r, + uv_r, + FT_r, + feature_edges_r, + misaligned_edges_r, + reference_field_r, + theta_r, + kappa_r, + period_jump_r); +} + +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXi, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXd> +parametrize_aligned( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Field::FieldParameters field_params, + NewtonParameters alg_params) +{ + // find feature edges and refine mesh to allow for a well defined cross field + auto [V_ref, F_ref, feature_edges, hard_feature_edges, reference_field, theta, kappa, period_jump] = generate_feature_aligned_frame_field(V, F, field_params); + + // parametrize with feature alignment constraints + auto param_data = generate_feature_aligned_parameterization( + V_ref, + F_ref, + feature_edges, + hard_feature_edges, + reference_field, + theta, + kappa, + period_jump, + alg_params); + auto V_r = std::get<0>(param_data); + auto F_r = std::get<1>(param_data); + auto uv_r = std::get<2>(param_data); + auto FT_r = std::get<3>(param_data); + auto feature_edges_r = std::get<4>(param_data); + auto misaligned_edges_r = std::get<5>(param_data); + auto reference_field_r = std::get<6>(param_data); + auto theta_r = std::get<7>(param_data); + auto kappa_r = std::get<8>(param_data); + auto period_jump_r = std::get<9>(param_data); + + // get feature edges + int num_features = feature_edges_r.size(); + Eigen::MatrixXi FE(num_features, 2); + for (int eij = 0; eij < num_features; ++eij) + { + FE(eij, 0) = feature_edges_r[eij][0]; + FE(eij, 1) = feature_edges_r[eij][1]; + } + + // get misaliged edges + int num_misaligned = misaligned_edges_r.size(); + Eigen::MatrixXi ME(num_misaligned, 2); + for (int eij = 0; eij < num_misaligned; ++eij) + { + ME(eij, 0) = misaligned_edges_r[eij][0]; + ME(eij, 1) = misaligned_edges_r[eij][1]; + } + + // comb frame field + auto [PD1, PD2] = Field::comb_frame_field(V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, period_jump_r); + + return std::make_tuple(V_r, F_r, uv_r, FT_r, FE, ME, PD1, PD2); +} + std::tuple> generate_dirichlet_metric( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, @@ -597,41 +748,6 @@ Eigen::MatrixXd generate_feature_aligned_metric( return aligned_metric_generator.get_metric(); } -std::tuple< - Eigen::MatrixXd, - Eigen::MatrixXi, - Eigen::MatrixXd, - Eigen::MatrixXi, - std::vector, - std::vector>> -generate_feature_aligned_parameterization( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const std::vector& feature_edges, - const std::vector& hard_feature_edges, - const Eigen::MatrixXd& reference_field, - const Eigen::VectorXd& theta, - const Eigen::MatrixXd& kappa, - const Eigen::MatrixXi& period_jump, - const NewtonParameters& alg_params) -{ - // use utility class - MarkedMetricParameters marked_metric_params; - AlignedMetricGenerator aligned_metric_generator( - V, - F, - feature_edges, - hard_feature_edges, - reference_field, - theta, - kappa, - period_jump, - marked_metric_params); - aligned_metric_generator.optimize_relaxed(alg_params); - - return aligned_metric_generator.get_parameterization(); -} - } // namespace Feature } // namespace Penner \ No newline at end of file diff --git a/src/feature/pybind.cpp b/src/feature/pybind.cpp index 575f2a6..1b1f22d 100644 --- a/src/feature/pybind.cpp +++ b/src/feature/pybind.cpp @@ -42,29 +42,23 @@ namespace Penner { namespace Feature { - void init_feature_pybind(pybind11::module& m) { - - m.doc() = "pybind for optimization module"; - - // init_optimization_pybind(m); - // init_holonomy_pybind(m); - spdlog::set_level(spdlog::level::info); pybind11::call_guard default_call_guard; - pybind11::class_(m, "BoundaryPath") - .def(pybind11::init&, int>()); - - pybind11::class_(m, "FaceEdge"); - - pybind11::class_(m, "IntrinsicRefinementMesh") - .def(pybind11::init()) - .def("refine_face", &IntrinsicRefinementMesh::refine_face) - .def("refine_spanning_faces", &IntrinsicRefinementMesh::refine_spanning_faces) - .def("generate_mesh", &IntrinsicRefinementMesh::generate_mesh); + pybind11::class_(m, "FeatureFinder") + .def(pybind11::init()) + .def("mark_features", &FeatureFinder::mark_features) + .def("mark_dihedral_angle_features", &FeatureFinder::mark_dihedral_angle_features) + .def("prune_cycles", &FeatureFinder::prune_cycles) + .def("prune_greedy", &FeatureFinder::prune_greedy) + .def("prune_refined_greedy", &FeatureFinder::prune_refined_greedy) + .def("prune_small_features", &FeatureFinder::prune_small_features) + .def("prune_small_components", &FeatureFinder::prune_small_components) + .def("view_features", &FeatureFinder::view_features) + .def("generate_feature_cut_mesh", &FeatureFinder::generate_feature_cut_mesh); pybind11::class_( m, @@ -84,41 +78,6 @@ void init_feature_pybind(pybind11::module& m) .def("set_angle_constraint_system", &DirichletPennerConeMetric::set_angle_constraint_system) .def("get_path_starting_vertices", &DirichletPennerConeMetric::get_path_starting_vertices); - //pybind11::class_(m, "BoundaryConstraintGenerator") - // .def(pybind11::init&>()) - // .def("mark_cones_as_junctions", &BoundaryConstraintGenerator::mark_cones_as_junctions) - // .def( - // "set_uniform_feature_lengths", - // &BoundaryConstraintGenerator::set_uniform_feature_lengths) - // .def( - // "build_boundary_constraint_system", - // &BoundaryConstraintGenerator::build_boundary_constraint_system); - - - pybind11::class_(m, "FeatureFinder") - .def(pybind11::init()) - .def("mark_dihedral_angle_features", &FeatureFinder::mark_dihedral_angle_features) - .def("mark_features", &FeatureFinder::mark_features) - .def("prune_cycles", &FeatureFinder::prune_cycles) - .def("prune_greedy", &FeatureFinder::prune_greedy) - .def("prune_refined_greedy", &FeatureFinder::prune_refined_greedy) - .def("prune_small_features", &FeatureFinder::prune_small_features) - .def("prune_small_components", &FeatureFinder::prune_small_components) - .def("view_features", &FeatureFinder::view_features) - .def("generate_feature_cut_mesh", &FeatureFinder::generate_feature_cut_mesh); - - pybind11::class_(m, "AngleConstraintMatrixRelaxer") - .def(pybind11::init<>()) - .def("run", &AngleConstraintMatrixRelaxer::run); - - pybind11::class_(m, "CutMetricGenerator") - .def(pybind11::init>>()) - .def("get_aligned_metric", &CutMetricGenerator::get_aligned_metric) - .def("get_fixed_aligned_metric", &CutMetricGenerator::get_fixed_aligned_metric) - .def("get_field", &CutMetricGenerator::get_field) - .def("set_fields", &CutMetricGenerator::set_fields) - .def("generate_fields", &CutMetricGenerator::generate_fields); - pybind11::class_(m, "AlignedMetricGenerator") .def(pybind11::init< const Eigen::MatrixXd&, @@ -141,6 +100,54 @@ void init_feature_pybind(pybind11::module& m) .def("get_refined_field", &AlignedMetricGenerator::get_refined_field) .def("get_refined_features", &AlignedMetricGenerator::get_refined_features); + m.def("load_feature_edges", &load_feature_edges, default_call_guard); + m.def("write_feature_edges", &write_feature_edges, default_call_guard); + m.def("generate_refined_feature_field", &generate_refined_feature_field, default_call_guard); + + m.def("parametrize_aligned", ¶metrize_aligned, default_call_guard); + m.def("generate_feature_aligned_frame_field", &generate_feature_aligned_frame_field, default_call_guard); + m.def("generate_feature_aligned_parameterization", &generate_feature_aligned_parameterization, default_call_guard); +} + +void init_additional_feature_pybind(pybind11::module& m) +{ + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + pybind11::class_(m, "BoundaryPath") + .def(pybind11::init&, int>()); + + pybind11::class_(m, "FaceEdge"); + + pybind11::class_(m, "IntrinsicRefinementMesh") + .def(pybind11::init()) + .def("refine_face", &IntrinsicRefinementMesh::refine_face) + .def("refine_spanning_faces", &IntrinsicRefinementMesh::refine_spanning_faces) + .def("generate_mesh", &IntrinsicRefinementMesh::generate_mesh); + + //pybind11::class_(m, "BoundaryConstraintGenerator") + // .def(pybind11::init&>()) + // .def("mark_cones_as_junctions", &BoundaryConstraintGenerator::mark_cones_as_junctions) + // .def( + // "set_uniform_feature_lengths", + // &BoundaryConstraintGenerator::set_uniform_feature_lengths) + // .def( + // "build_boundary_constraint_system", + // &BoundaryConstraintGenerator::build_boundary_constraint_system); + + pybind11::class_(m, "AngleConstraintMatrixRelaxer") + .def(pybind11::init<>()) + .def("run", &AngleConstraintMatrixRelaxer::run); + + pybind11::class_(m, "CutMetricGenerator") + .def(pybind11::init>>()) + .def("get_aligned_metric", &CutMetricGenerator::get_aligned_metric) + .def("get_fixed_aligned_metric", &CutMetricGenerator::get_fixed_aligned_metric) + .def("get_field", &CutMetricGenerator::get_field) + .def("set_fields", &CutMetricGenerator::set_fields) + .def("generate_fields", &CutMetricGenerator::generate_fields); + m.def("build_boundary_paths", &build_boundary_paths, default_call_guard); m.def("generate_refined_feature_mesh", &generate_refined_feature_mesh, default_call_guard); m.def("refine_corner_feature_faces", &refine_corner_feature_faces, default_call_guard); @@ -193,7 +200,6 @@ void init_feature_pybind(pybind11::module& m) #ifdef WITH_MPFR m.def("generate_connected_parameterization_mpfr", &generate_connected_parameterization, default_call_guard); #endif - m.def("load_feature_edges", &load_feature_edges, default_call_guard); m.def("load_mesh_edges", &load_mesh_edges, default_call_guard); } diff --git a/src/feature/surgery/cut_mesh_layout.cpp b/src/feature/surgery/cut_mesh_layout.cpp index 4ea3788..219b498 100644 --- a/src/feature/surgery/cut_mesh_layout.cpp +++ b/src/feature/surgery/cut_mesh_layout.cpp @@ -115,8 +115,7 @@ parameterize_cut_mesh( component_metric.get_metric_coordinates(), is_cut, false, - use_uniform_bc, - ""); + use_uniform_bc); OverlayMesh m_o = std::get<0>(vf_res); Eigen::MatrixXd V_o = std::get<1>(vf_res); Eigen::MatrixXi F_o = std::get<2>(vf_res); diff --git a/src/field/cross_field.cpp b/src/field/cross_field.cpp index 0788640..9a2a5a6 100644 --- a/src/field/cross_field.cpp +++ b/src/field/cross_field.cpp @@ -308,7 +308,7 @@ std::array reduce_curl( return opt_cross_field; } -std::tuple> generate_cross_field( +std::tuple> generate_rosy_field( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F) { diff --git a/src/field/frame_field.cpp b/src/field/frame_field.cpp index 521d070..6a2c30f 100644 --- a/src/field/frame_field.cpp +++ b/src/field/frame_field.cpp @@ -12,6 +12,7 @@ #include "util/vf_mesh.h" #include "util/map.h" +#include #include #include #include @@ -184,7 +185,7 @@ Eigen::VectorXd rotate_vector( return norm*cos(a) * B1 + norm*sin(a) * B2; } -std::tuple +std::tuple, std::vector> find_u_aligned_edges( const Eigen::MatrixXd& uv, const Eigen::MatrixXi& FT) @@ -193,6 +194,49 @@ find_u_aligned_edges( igl::triangle_triangle_adjacency(FT, TT, TTi); Eigen::ArrayXX is_boundary_edge = (TT.array() < 0); + // get face components + int N = uv.rows(); + Eigen::VectorXi C; + int num_components = igl::facet_components(FT, C); + spdlog::info("combing field on {} components", num_components); + std::vector min_v_diffs(num_components, 1e10); + std::vector min_faces(num_components, -1); + std::vector min_edges(num_components, -1); + + for (int fijk = 0; fijk < FT.rows(); ++fijk) + { + for (int i = 0; i < 3; ++i) + { + if (!is_boundary_edge(fijk, i)) continue; + int ci = C[fijk]; + + int vi = FT(fijk, (i + 0) % 3); + int vj = FT(fijk, (i + 1) % 3); + double signed_u_diff = uv(vj, 0) - uv(vi, 0); + if (signed_u_diff < 0.) continue; + + double v_diff = abs(uv(vj, 1) - uv(vi, 1)); + if (v_diff < min_v_diffs[ci]) { + min_v_diffs[ci] = v_diff; + min_faces[ci] = fijk; + min_edges[ci] = i; + } + } + } + + return std::make_tuple(min_faces, min_edges); + +} + +std::tuple +find_u_aligned_edge( + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT) +{ + Eigen::MatrixXi TT, TTi; + igl::triangle_triangle_adjacency(FT, TT, TTi); + Eigen::ArrayXX is_boundary_edge = (TT.array() < 0); + double min_v_diff = 1e10; int min_face = -1; int min_edge = -1; @@ -227,12 +271,54 @@ std::tuple, Eigen::VectorXi> initialize_matchin const Eigen::MatrixXd& frame_field, const Eigen::MatrixXd& B1, const Eigen::MatrixXd& B2) +{ + Eigen::VectorXi matchings = Eigen::VectorXi::Constant(F.rows(), 0); + std::deque d; + Eigen::VectorXi mark = Eigen::VectorXi::Constant(F.rows(), 0); + + std::vector min_faces, min_edges; + std::tie(min_faces, min_edges) = find_u_aligned_edges(uv, FT); + int num_components = min_edges.size(); + for (int ci = 0; ci < num_components; ++ci) + { + int fijk = min_faces[ci]; + int i = min_edges[ci]; + int vi = F(fijk, (i + 0) % 3); + int vj = F(fijk, (i + 1) % 3); + Eigen::VectorXd dij = V.row(vj) - V.row(vi); + dij.normalize(); + double min = (frame_field.row(fijk) - dij.transpose()).norm(); + for (int i = 1; i < 4; ++i) + { + Eigen::VectorXd rot_dfijk = rotate_vector(frame_field.row(fijk), i * (igl::PI / 2.0), B1.row(fijk), B2.row(fijk)); + double curr_diff = (rot_dfijk - dij).norm(); + if (curr_diff < min) + { + min = curr_diff; + matchings[fijk] = i; + } + } + d.push_back(fijk); + mark[fijk] = 1; + } + + return { matchings, d, mark }; +} + +std::tuple, Eigen::VectorXi> _initialize_matchings( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT, + const Eigen::MatrixXd& frame_field, + const Eigen::MatrixXd& B1, + const Eigen::MatrixXd& B2) { Eigen::VectorXi matchings = Eigen::VectorXi::Constant(FT.rows(), 0); std::deque d; Eigen::VectorXi mark = Eigen::VectorXi::Constant(FT.rows(), 0); - auto [fijk, i] = find_u_aligned_edges(uv, FT); + auto [fijk, i] = find_u_aligned_edge(uv, FT); int vi = F(fijk, (i + 0) % 3); int vj = F(fijk, (i + 1) % 3); Eigen::VectorXd dij = V.row(vj) - V.row(vi); @@ -378,9 +464,9 @@ comb_frame_field( Eigen::MatrixXd PD1 = igl::rotate_vectors(frame_field, u_angles, B1, B2); Eigen::MatrixXd PD2 = igl::rotate_vectors(frame_field, v_angles, B1, B2); - auto [phase, PD1_r, PD2_r] = maximize_combed_frame_alignment(V, F, uv, FT, PD1, PD2); + //auto [phase, PD1_r, PD2_r] = maximize_combed_frame_alignment(V, F, uv, FT, PD1, PD2); - return {PD1_r, PD2_r}; + return {PD1, PD2}; } std::tuple @@ -505,7 +591,7 @@ load_frame_field(const std::string& filename) return std::make_tuple(reference_field, theta, kappa, period_jump); } -std::vector compute_cone_angle( +std::vector compute_frame_field_cones( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, const Eigen::MatrixXd& kappa, diff --git a/src/holonomy/core/viewer.cpp b/src/holonomy/core/viewer.cpp index cbe74dd..371c1e0 100644 --- a/src/holonomy/core/viewer.cpp +++ b/src/holonomy/core/viewer.cpp @@ -162,7 +162,7 @@ void view_cross_field( } // get cone angles - std::vector Th_hat = Field::compute_cone_angle(V, F, kappa, period_jump); + std::vector Th_hat = Field::compute_frame_field_cones(V, F, kappa, period_jump); auto [cone_positions, cone_values] = generate_cone_vertices(V, Th_hat); #ifdef ENABLE_VISUALIZATION diff --git a/src/holonomy/holonomy/constraint.cpp b/src/holonomy/holonomy/constraint.cpp index c7f1b10..924edb7 100644 --- a/src/holonomy/holonomy/constraint.cpp +++ b/src/holonomy/holonomy/constraint.cpp @@ -200,22 +200,6 @@ std::vector compute_dependent_edges(const MarkedPennerConeMetric& marked_me return dependent_edges; } -void add_symmetry_constraints( - const MarkedPennerConeMetric& marked_metric, - const std::vector& dependent_edges, - VectorX& constraint, - int offset) -{ - // add symmetry constraints - int num_dep_edges = dependent_edges.size(); - for (int i = 0; i < num_dep_edges; i++) { - int e = dependent_edges[i]; - int h = marked_metric.e2he[e]; - int Rh = marked_metric.R[h]; - constraint[offset + i] = marked_metric.original_coords[h] - marked_metric.original_coords[Rh]; - } -} - VectorX compute_vertex_constraint( const MarkedPennerConeMetric& marked_metric, const VectorX& angles) diff --git a/src/holonomy/holonomy/marked_penner_cone_metric.cpp b/src/holonomy/holonomy/marked_penner_cone_metric.cpp index 50c1e1c..fcb3afe 100644 --- a/src/holonomy/holonomy/marked_penner_cone_metric.cpp +++ b/src/holonomy/holonomy/marked_penner_cone_metric.cpp @@ -145,13 +145,6 @@ MarkedPennerConeMetric::MarkedPennerConeMetric( m_homology_basis_loops.push_back(homology_basis_loops[i]->clone()); m_dual_loop_manager.register_loop_edges(i, cone_metric, *homology_basis_loops[i]); } - - // TODO - int num_halfedges = cone_metric.n_halfedges(); - original_coords.resize(num_halfedges); - for (int h = 0; h < num_halfedges; ++h) { - original_coords[h] = 2. * log(l[h]); - } } @@ -218,37 +211,6 @@ void MarkedPennerConeMetric::operator=(const MarkedPennerConeMetric& m) copy_holonomy(m); } - -void MarkedPennerConeMetric::reset_connectivity(const MarkedPennerConeMetric& m) -{ - // Halfedge arrays - int num_halfedges = n_halfedges(); - for (int h = 0; h < num_halfedges; ++h) { - n[h] = m.n[h]; - to[h] = m.to[h]; - f[h] = m.f[h]; - l[h] = m.l[h]; - type[h] = m.type[h]; - R[h] = m.R[h]; - - // opp, he2e, e2he do not change - } - - // Vertex arrays - int num_vertices = n_vertices(); - for (int v = 0; v < num_vertices; ++v) { - out[v] = m.out[v]; - - // v_rep, Th_hat, fixed_dof do not change - } - - // Face arrays - int num_faces = n_faces(); - for (int f = 0; f < num_faces; ++f) { - h[f] = m.h[f]; - } -} - void MarkedPennerConeMetric::reset_markings(const MarkedPennerConeMetric& m) { // Loop data @@ -270,7 +232,7 @@ void MarkedPennerConeMetric::reset_marked_metric(const MarkedPennerConeMetric& m // Clear flip data m_is_discrete_metric = false; m_flip_seq.clear(); - PennerConeMetric::reset(); + reset_flip_sequence(); } void MarkedPennerConeMetric::change_metric( @@ -279,34 +241,11 @@ void MarkedPennerConeMetric::change_metric( bool need_jacobian, bool do_repeat_flips) { - // Restore connectivity to that of m - reset_connectivity(m); - - // Change metric coordinates - PennerConeMetric::expand_metric_coordinates(metric_coords); - std::vector flip_seq = m_flip_seq; - spdlog::debug("Repeating {} flips", m_flip_seq.size()); - m_flip_seq.clear(); - m_is_discrete_metric = false; - m_need_jacobian = need_jacobian; - PennerConeMetric::reset(); - - // TODO - int num_halfedges = m.n_halfedges(); - original_coords.resize(num_halfedges); - for (int h = 0; h < num_halfedges; ++h) { - original_coords[h] = 2. * log(l[h]); - } + // Change metric in base class + PennerConeMetric::change_metric(m, metric_coords, need_jacobian, do_repeat_flips); - // Flip back to current connectivity if flag set - if (do_repeat_flips) { - for (int h : flip_seq) { - PennerConeMetric::flip_ccw(h); - } - spdlog::debug("{} flips performed", m_flip_seq.size()); - } - // Reset markings if using original connectivity - else { + // If not repeating flips, restore markings + if (!do_repeat_flips) { reset_markings(m); } } @@ -333,16 +272,6 @@ bool MarkedPennerConeMetric::constraint( return true; } -Scalar MarkedPennerConeMetric::max_constraint_error() const -{ - VectorX cons; - MatrixX J_constraint; - bool need_jacobian = false; - bool only_free_vertices = true; - constraint(cons, J_constraint, need_jacobian, only_free_vertices); - return cons.cwiseAbs().maxCoeff(); -} - VectorX MarkedPennerConeMetric::constraint(const VectorX& angles) { return compute_metric_constraint(*this, angles); diff --git a/src/holonomy/holonomy/newton.cpp b/src/holonomy/holonomy/newton.cpp index ab6ebd2..49318c5 100644 --- a/src/holonomy/holonomy/newton.cpp +++ b/src/holonomy/holonomy/newton.cpp @@ -47,6 +47,8 @@ void OptimizeNewton::initialize_logging() { void OptimizeNewton::initialize_metric_status_log(MarkedPennerConeMetric& marked_metric) { + if ((!alg_params.error_log) || (alg_params.output_dir.empty())) return; + // Open main logging file std::string data_log_path = join_path(alg_params.output_dir, "metric_status_log.csv"); spdlog::info("Writing data to {}", data_log_path); @@ -54,12 +56,15 @@ void OptimizeNewton::initialize_metric_status_log(MarkedPennerConeMetric& marked marked_metric.write_status_log(metric_status_file, true); } +void OptimizeNewton::close_metric_status_log() +{ + if ((!alg_params.error_log) || (alg_params.output_dir.empty())) return; + metric_status_file.close(); +} + // Open a per iteration data log and write a header void OptimizeNewton::initialize_data_log() { - // Do nothing if error logging disabled - if (!alg_params.error_log) return; - // Generate data log path std::filesystem::create_directory(alg_params.output_dir); std::string data_log_path; @@ -80,9 +85,6 @@ void OptimizeNewton::initialize_data_log() // Write newton log iteration data to file void OptimizeNewton::write_data_log_entry() { - // Do nothing if error logging disabled - if (!alg_params.error_log) return; - // Write iteration row log_file << log.num_iter << ","; log_file << std::fixed << std::setprecision(17) << log.max_error << ","; @@ -110,9 +112,6 @@ void OptimizeNewton::initialize_timing_log() // Write newton log iteration data to file void OptimizeNewton::write_timing_log_entry() { - // Do nothing if error logging disabled - if (!alg_params.error_log) return; - // Write iteration row timing_file << std::fixed << std::setprecision(8) << log.time << ","; timing_file << std::fixed << std::setprecision(8) << log.solve_time << ","; @@ -125,12 +124,16 @@ void OptimizeNewton::write_timing_log_entry() // Open all logs void OptimizeNewton::initialize_logs() { + if ((!alg_params.error_log) || (alg_params.output_dir.empty())) return; + initialize_data_log(); initialize_timing_log(); } void OptimizeNewton::write_log_entries() { + if ((!alg_params.error_log) || (alg_params.output_dir.empty())) return; + write_data_log_entry(); write_timing_log_entry(); } @@ -139,7 +142,7 @@ void OptimizeNewton::write_log_entries() void OptimizeNewton::close_logs() { // Do nothing if error logging disabled - if (!alg_params.error_log) return; + if ((!alg_params.error_log) || (alg_params.output_dir.empty())) return; log_file.close(); timing_file.close(); @@ -590,7 +593,6 @@ MarkedPennerConeMetric OptimizeNewton::run( // Close logging close_logs(); - metric_status_file.close(); // Change metric to final values and restore the original connectivity marked_metric->change_metric(initial_marked_metric, reduced_metric_coords, true, false); diff --git a/src/holonomy/interface.cpp b/src/holonomy/interface.cpp index 0605a96..5b74be3 100644 --- a/src/holonomy/interface.cpp +++ b/src/holonomy/interface.cpp @@ -15,6 +15,7 @@ #include "field/cross_field.h" #include "metric/quality.h" #include "field/cones.h" +#include "field/frame_field.h" #include "holonomy/holonomy/holonomy.h" #include "holonomy/holonomy/constraint.h" #include "field/rotation_form.h" @@ -25,6 +26,7 @@ #include "metric/constraint.h" #include "optimization/interface.h" #include "parametrization/interpolation.h" +#include "parametrization/parametrize.h" #include "parametrization/refinement.h" #include "util/io.h" #include "util/vector.h" @@ -245,9 +247,7 @@ MarkedPennerConeMetric generate_marked_metric_from_mesh( MarkedMetricParameters marked_metric_params, std::vector marked_halfedges) { - // Optionally remove symmetry structure - // TODO: Need to remake cone angles with half values - PennerConeMetric cone_metric = generate_cone_metric(_m, marked_metric_params); + PennerConeMetric cone_metric = generate_cone_metric_from_mesh(_m, marked_metric_params); // Get initial log length coordinates DiscreteMetric discrete_metric = generate_discrete_metric(_m); @@ -645,5 +645,78 @@ std::tuple add_optimal_cone_pair(MarkedPennerConeMetric& marked_metric return std::make_tuple(i, j); } +std::tuple, std::vector>> +parametrize_seamless_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const MarkedPennerConeMetric& marked_metric, + NewtonParameters alg_params) +{ + // Optimize metric + auto opt_marked_metric = optimize_metric_angles(marked_metric, alg_params); + + VectorX opt_metric_coords = opt_marked_metric.get_metric_coordinates(); + return parametrize_metric(V, F, marked_metric, opt_metric_coords); +} + +std::tuple +parametrize_seamless( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Field::FieldParameters field_params, + NewtonParameters alg_params) +{ + MarkedMetricParameters marked_metric_params; + auto [reference_field, theta, kappa, period_jump] = generate_frame_field(V, F, field_params); + auto [V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, kappa_r, period_jump_r] = generate_seamless_parametrization( + V, + F, + reference_field, + theta, + kappa, + period_jump, + alg_params); + auto [PD1, PD2] = Field::comb_frame_field(V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, period_jump_r); + return std::make_tuple(V_r, F_r, uv_r, FT_r, PD1, PD2); + +} + +std::tuple< + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::MatrixXi, + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +generate_seamless_parametrization( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump, + NewtonParameters alg_params) +{ + MarkedMetricParameters marked_metric_params; + auto [marked_metric, vtx_reindex, rotation_form, Th_hat] = generate_metric_from_field(V, F, theta, kappa, period_jump, marked_metric_params); + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, endpoints_r] = parametrize_seamless_metric(V, F, marked_metric, alg_params); + + auto [reference_field_r, theta_r, kappa_r, period_jump_r] = Field::refine_frame_field( + F_r, + FT_r, + fn_to_f_r, + endpoints_r, + F, + reference_field, + theta, + kappa, + period_jump); + + return std::make_tuple(V_r, F_r, uv_r, FT_r, reference_field_r, theta_r, kappa_r, period_jump_r); + +} + } // namespace Holonomy -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/holonomy/pybind.cpp b/src/holonomy/pybind.cpp index 6ddfcf6..780b6be 100644 --- a/src/holonomy/pybind.cpp +++ b/src/holonomy/pybind.cpp @@ -14,6 +14,7 @@ #include "holonomy/holonomy/marked_penner_cone_metric.h" #include "holonomy/holonomy/newton.h" #include "holonomy/holonomy/constraint.h" +#include "holonomy/core/viewer.h" #include "field/rotation_form.h" #include "field/intrinsic_field.h" #include "field/frame_field.h" @@ -42,44 +43,12 @@ void writeOBJ( igl::writeOBJ(str, V, F, CN, FN, TC, FTC); } - void init_holonomy_pybind(pybind11::module& m) { spdlog::set_level(spdlog::level::info); pybind11::call_guard default_call_guard; - pybind11::class_ homotopy_basis_generator(m, "HomotopyBasisGenerator"); - pybind11::enum_(homotopy_basis_generator, "Weighting") - .value("minimal_homotopy", HomotopyBasisGenerator::Weighting::minimal_homotopy) - .value("maximal_homotopy", HomotopyBasisGenerator::Weighting::maximal_homotopy) - .value("dual_min_primal_max", HomotopyBasisGenerator::Weighting::dual_min_primal_max) - .value("primal_min_dual_max", HomotopyBasisGenerator::Weighting::primal_min_dual_max) - .export_values(); - - pybind11::class_(m, "IntrinsicNRosyField") - .def(pybind11::init<>()) - .def("initialize", &Field::IntrinsicNRosyField::initialize) - .def("set_field", pybind11::overload_cast< - const Mesh&, - const std::vector&, - const Eigen::MatrixXi&, - const std::vector&, - const Eigen::VectorXd&, - const Eigen::MatrixXd&, - const Eigen::MatrixXi&>(&Field::IntrinsicNRosyField::set_field)) - .def("set_field", pybind11::overload_cast< - const Mesh&, - const std::vector&, - const Eigen::MatrixXi&, - const Eigen::VectorXd&, - const Eigen::MatrixXd&, - const Eigen::MatrixXi&>(&Field::IntrinsicNRosyField::set_field)) - //.def("set_field", &Field::IntrinsicNRosyField::set_field) - .def("compute_principal_matchings", &Field::IntrinsicNRosyField::compute_principal_matchings) - .def("fix_inconsistent_matchings", &Field::IntrinsicNRosyField::fix_inconsistent_matchings) - .def("compute_rotation_form", &Field::IntrinsicNRosyField::compute_rotation_form); - pybind11::class_>(m, "NewtonParameters") .def(pybind11::init<>()) .def_readwrite("output_dir", &NewtonParameters::output_dir) @@ -95,22 +64,22 @@ void init_holonomy_pybind(pybind11::module& m) .def_readwrite("solver", &NewtonParameters::solver) .def_readwrite("log_level", &NewtonParameters::log_level); - pybind11::class_>( + pybind11::class_( m, "MarkedMetricParameters") .def(pybind11::init<>()) - .def_readwrite("use_log_length", &MarkedMetricParameters::use_log_length) - .def_readwrite("use_initial_zero", &MarkedMetricParameters::use_initial_zero) .def_readwrite("remove_loop_constraints", &MarkedMetricParameters::remove_loop_constraints) .def_readwrite("remove_trivial_torus", &MarkedMetricParameters::remove_trivial_torus) - .def_readwrite("free_interior", &MarkedMetricParameters::free_interior) .def_readwrite("weighting", &MarkedMetricParameters::weighting); pybind11::class_>(m, "FieldParameters") .def(pybind11::init<>()) - .def_readwrite("min_cone", &Field::FieldParameters::min_cone); + .def_readwrite("min_cone", &Field::FieldParameters::min_cone) + .def_readwrite("fix_cone_pair", &Field::FieldParameters::fix_cone_pair) + .def_readwrite("use_principal_directions", &Field::FieldParameters::use_principal_directions); + - pybind11::class_(m, "MarkedPennerConeMetric") + pybind11::class_(m, "MarkedPennerConeMetric") .def(pybind11::init< const Mesh&, const VectorX&, @@ -118,22 +87,60 @@ void init_holonomy_pybind(pybind11::module& m) const std::vector& >()) .def_readwrite("kappa_hat", &MarkedPennerConeMetric::kappa_hat) - .def("flip_ccw", &MarkedPennerConeMetric::flip_ccw) - .def("undo_flips", &MarkedPennerConeMetric::undo_flips) .def("change_metric", &MarkedPennerConeMetric::change_metric) - .def("clone_cone_metric", &MarkedPennerConeMetric::clone_cone_metric) - .def("make_discrete_metric", &MarkedPennerConeMetric::make_discrete_metric) - .def("get_flip_sequence", &MarkedPennerConeMetric::get_flip_sequence) - .def("max_constraint_error", &MarkedPennerConeMetric::max_constraint_error) - .def("constraint", - static_cast(&MarkedPennerConeMetric::constraint)) - .def("constraint_jacobian", - static_cast(&MarkedPennerConeMetric::constraint_jacobian)) - .def("n_vertices", &MarkedPennerConeMetric::n_vertices) - .def("n_edges", &MarkedPennerConeMetric::n_edges) - .def("n_faces", &MarkedPennerConeMetric::n_faces) + .def("reset_marked_metric", &MarkedPennerConeMetric::reset_marked_metric) .def("n_homology_basis_loops", &MarkedPennerConeMetric::n_homology_basis_loops); + m.def("generate_marked_metric", &generate_marked_metric, default_call_guard); + m.def("generate_metric_from_field", &generate_metric_from_field, default_call_guard); + m.def("regularize_metric", ®ularize_metric, default_call_guard); + m.def("optimize_metric_angles", &optimize_metric_angles, default_call_guard); + + m.def("parametrize_seamless", ¶metrize_seamless, default_call_guard); + m.def("parametrize_seamless_metric", ¶metrize_seamless_metric, default_call_guard); + m.def("generate_seamless_parametrization", &generate_seamless_parametrization, default_call_guard); + + m.def("view_cross_field", &view_cross_field, default_call_guard); + m.def("view_seamless_parameterization", &view_seamless_parameterization, default_call_guard); +} + +void init_additional_holonomy_pybind(pybind11::module& m) +{ + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + pybind11::class_ homotopy_basis_generator(m, "HomotopyBasisGenerator"); + pybind11::enum_(homotopy_basis_generator, "Weighting") + .value("minimal_homotopy", HomotopyBasisGenerator::Weighting::minimal_homotopy) + .value("maximal_homotopy", HomotopyBasisGenerator::Weighting::maximal_homotopy) + .value("dual_min_primal_max", HomotopyBasisGenerator::Weighting::dual_min_primal_max) + .value("primal_min_dual_max", HomotopyBasisGenerator::Weighting::primal_min_dual_max) + .export_values(); + + pybind11::class_(m, "IntrinsicNRosyField") + .def(pybind11::init<>()) + .def("initialize", &Field::IntrinsicNRosyField::initialize) + .def("set_field", pybind11::overload_cast< + const Mesh&, + const std::vector&, + const Eigen::MatrixXi&, + const std::vector&, + const Eigen::VectorXd&, + const Eigen::MatrixXd&, + const Eigen::MatrixXi&>(&Field::IntrinsicNRosyField::set_field)) + .def("set_field", pybind11::overload_cast< + const Mesh&, + const std::vector&, + const Eigen::MatrixXi&, + const Eigen::VectorXd&, + const Eigen::MatrixXd&, + const Eigen::MatrixXi&>(&Field::IntrinsicNRosyField::set_field)) + //.def("set_field", &Field::IntrinsicNRosyField::set_field) + .def("compute_principal_matchings", &Field::IntrinsicNRosyField::compute_principal_matchings) + .def("fix_inconsistent_matchings", &Field::IntrinsicNRosyField::fix_inconsistent_matchings) + .def("compute_rotation_form", &Field::IntrinsicNRosyField::compute_rotation_form); + pybind11::class_(m, "DualLoop"); pybind11::class_(m, "CoordinateEnergy") @@ -158,7 +165,6 @@ void init_holonomy_pybind(pybind11::module& m) m.def("build_reduced_matrix_rhs", &build_reduced_matrix_rhs, default_call_guard); m.def("compute_triangle_corner_angle_jacobian", &compute_triangle_corner_angle_jacobian, default_call_guard); m.def("FE_to_double", &FE_to_double, default_call_guard); - m.def("generate_marked_metric", &generate_marked_metric, default_call_guard); m.def("generate_marked_metric_from_mesh", &generate_marked_metric_from_mesh, default_call_guard); m.def("generate_refined_marked_metric", &generate_refined_marked_metric, default_call_guard); m.def("build_symmetric_matrix_system", &build_symmetric_matrix_system, default_call_guard); @@ -172,7 +178,6 @@ void init_holonomy_pybind(pybind11::module& m) default_call_guard); m.def("optimize_subspace_metric_angles", &optimize_subspace_metric_angles, default_call_guard); - m.def("optimize_metric_angles", &optimize_metric_angles, default_call_guard); m.def( "generate_intrinsic_rotation_form", pybind11:: @@ -198,13 +203,11 @@ void init_holonomy_pybind(pybind11::module& m) &generate_penner_coordinates, default_call_guard); - m.def("load_frame_field", &Field::load_frame_field, default_call_guard); - m.def("write_frame_field", &Field::write_frame_field, default_call_guard); - m.def("refine_frame_field", &Field::refine_frame_field, default_call_guard); m.def("compute_loop_holonomy_matrix", &compute_loop_holonomy_matrix, default_call_guard); - m.def("compute_field_direction", &Field::compute_field_direction, default_call_guard); m.def("write_obj", writeOBJ, default_call_guard); + m.def("compute_field_direction", &Field::compute_field_direction, default_call_guard); + } #endif diff --git a/src/holonomy/similarity/layout.cpp b/src/holonomy/similarity/layout.cpp index accf2b0..6645a64 100644 --- a/src/holonomy/similarity/layout.cpp +++ b/src/holonomy/similarity/layout.cpp @@ -127,15 +127,6 @@ std:: Mesh m = FV_to_double(V, F, V, F, Th_hat, vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops); - // Find boundary halfedges - std::vector is_bd(m.n_ind_vertices(), false); - for (int i = 0; i < m.n_halfedges(); i++) { - if ((m.type[i] == 1) && (m.type[m.opp[i]] == 2)) - { - is_bd[m.v_rep[m.to[i]]] = true; - } - } - // Compute interpolation overlay mesh // TODO: Use consistent interpolation code from the Penner codebase Eigen::MatrixXd V_overlay; @@ -166,10 +157,6 @@ std:: } make_tufted_overlay(m_o); - // Get endpoints - std::vector> endpoints; - find_origin_endpoints(m_o, endpoints); - // Convert overlay mesh to transposed vector format std::vector> V_overlay_vec(3); for (int i = 0; i < 3; ++i) { @@ -185,14 +172,12 @@ std:: // Convert overlay mesh to VL format spdlog::trace("Getting layout"); std::vector u(m.n_ind_vertices(), 0.0); - return consistent_overlay_mesh_to_VL( + return layout_overlay_mesh( m, m_o, vtx_reindex, - is_bd, u, V_overlay_vec, - endpoints, is_cut, is_cut_integral); } diff --git a/src/metric/cone_metric.cpp b/src/metric/cone_metric.cpp index f950e66..1ec1b69 100644 --- a/src/metric/cone_metric.cpp +++ b/src/metric/cone_metric.cpp @@ -75,6 +75,22 @@ bool DifferentiableConeMetric::constraint( only_free_vertices); } +VectorX DifferentiableConeMetric::constraint() const +{ + VectorX cons; + MatrixX J_constraint; + bool need_jacobian = false; + bool only_free_vertices = true; + constraint(cons, J_constraint, need_jacobian, only_free_vertices); + return cons; +} + +Scalar DifferentiableConeMetric::max_constraint_error() const +{ + VectorX cons = constraint(); + return cons.cwiseAbs().maxCoeff(); +} + void DifferentiableConeMetric::undo_flips() { std::vector flip_seq = m_flip_seq; @@ -137,6 +153,36 @@ MatrixX DifferentiableConeMetric::change_metric_to_reduced_coordinates( return halfedge_jacobian * J_transition; } +void DifferentiableConeMetric::reset_connectivity(const Mesh& m) +{ + // Halfedge arrays + int num_halfedges = n_halfedges(); + for (int h = 0; h < num_halfedges; ++h) { + n[h] = m.n[h]; + to[h] = m.to[h]; + f[h] = m.f[h]; + l[h] = m.l[h]; + type[h] = m.type[h]; + R[h] = m.R[h]; + + // opp, he2e, e2he do not change + } + + // Vertex arrays + int num_vertices = n_vertices(); + for (int v = 0; v < num_vertices; ++v) { + out[v] = m.out[v]; + + // v_rep, Th_hat, fixed_dof do not change + } + + // Face arrays + int num_faces = n_faces(); + for (int f = 0; f < num_faces; ++f) { + h[f] = m.h[f]; + } +} + PennerConeMetric::PennerConeMetric() : m_transition_jacobian_lol(0) {} @@ -167,6 +213,35 @@ std::unique_ptr PennerConeMetric::set_metric_coordinat return std::make_unique(PennerConeMetric(*this, metric_coords)); } +void PennerConeMetric::change_metric( + const Mesh& m, + const VectorX& metric_coords, + bool need_jacobian, + bool do_repeat_flips) +{ + // get initial flip sequence + std::vector flip_seq = m_flip_seq; + + // Restore connectivity to that of m + reset_connectivity(m); + + // Change metric coordinates + expand_metric_coordinates(metric_coords); + + // reset flip data + m_is_discrete_metric = false; + m_need_jacobian = need_jacobian; + reset_flip_sequence(); + + // Flip back to current connectivity if flag set + if (do_repeat_flips) { + for (int h : flip_seq) { + flip_ccw(h); + } + spdlog::debug("{} flips performed", m_flip_seq.size()); + } +} + bool PennerConeMetric::flip_ccw(int halfedge_index, bool Ptolemy) { // Perform the (Ptolemy) flip in the base class @@ -253,7 +328,7 @@ MatrixX PennerConeMetric::get_transition_jacobian() const return m_identification * (transition_jacobian * m_projection); } -void PennerConeMetric::reset() +void PennerConeMetric::reset_flip_sequence() { // Initialize jacobian to the identity m_transition_jacobian_lol.reset(); diff --git a/src/metric/interface.cpp b/src/metric/interface.cpp index 3a5ea4a..8a9ace7 100644 --- a/src/metric/interface.cpp +++ b/src/metric/interface.cpp @@ -127,8 +127,6 @@ DiscreteMetric generate_discrete_metric(const Mesh& m) { std::tuple> generate_cone_metric( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv, const std::vector& Th_hat, std::vector free_cones, ConeMetricParameters cone_metric_params) @@ -139,8 +137,8 @@ std::tuple> generate_cone_metric( Mesh m = FV_to_double( V, F, - uv, - F_uv, + V, + F, Th_hat, vtx_reindex, indep_vtx, @@ -150,10 +148,10 @@ std::tuple> generate_cone_metric( free_cones, fix_boundary); - return std::make_tuple(generate_cone_metric(m, cone_metric_params), vtx_reindex); + return std::make_tuple(generate_cone_metric_from_mesh(m, cone_metric_params), vtx_reindex); } -PennerConeMetric generate_cone_metric( +PennerConeMetric generate_cone_metric_from_mesh( const Mesh& m, ConeMetricParameters cone_metric_params) { diff --git a/src/optimization/CMakeLists.txt b/src/optimization/CMakeLists.txt index 9af5e22..8e64749 100644 --- a/src/optimization/CMakeLists.txt +++ b/src/optimization/CMakeLists.txt @@ -12,6 +12,12 @@ set(OptimizationUtilSrc util/shapes.cpp ) +if (USE_UV_OPTIMIZATION) +list(APPEND MetricOptimizationSrc + metric_optimization/uv_optimization.cpp +) +endif() + add_library(PennerOptimizationLib interface.cpp pybind.cpp @@ -22,6 +28,8 @@ target_include_directories(PennerOptimizationLib PUBLIC ../../include/optimizati target_link_libraries(PennerOptimizationLib PUBLIC PennerCoreLib ${PYBIND_LIBRARIES} + ${UV_OPT_LIBRARIES} + ) target_compile_definitions(PennerOptimizationLib PUBLIC SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG diff --git a/src/optimization/metric_optimization/uv_optimization.cpp b/src/optimization/metric_optimization/uv_optimization.cpp new file mode 100644 index 0000000..0b2ad79 --- /dev/null +++ b/src/optimization/metric_optimization/uv_optimization.cpp @@ -0,0 +1,170 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#include "optimization/metric_optimization/uv_optimization.h" + +#if USE_UV_OPTIMIZATION + + +namespace Penner { +namespace Optimization { + +SymDir::Parameters load_parameters(const std::string& filepath) +{ + std::ifstream js_in(filepath); + nlohmann::json config = nlohmann::json::parse(js_in); + return read_parameters(config); +} + +SymDir::Parameters read_parameters(const nlohmann::json& config) +{ + SymDir::Parameters param; + param.max_iters = config["max_iters"]; // iterations + param.max_time = config["max_time"]; // time in seconds + param.smooth_only_iters = config["smooth_only_iters"]; + param.E_target = config["E_target"]; // Energy target + param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation + param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation + param.local_smooth = config["local_smooth"]; + param.global_smooth = config["global_smooth"]; + param.elen_alpha = config["elen_alpha"]; + param.do_projection = config["do_projection"]; + param.with_cons = config["with_cons"]; + param.Lp = config["Lp"]; + param.save_meshes = config["save_meshes"]; + param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges + param.symdir_weight = config["symdir_weight"]; + param.alignment_weight = config["alignment_weight"]; + param.degenerate_weight = config["degenerate_weight"]; + param.fix_misaligned = config["fix_misaligned"]; + param.use_rref = config["use_rref"]; + // param.solver_type = config["solver_type"]; + param.cg_rel_err = config["cg_rel_err"]; + param.cg_iters = config["cg_iters"]; + + param.percentages = config["percentages"].get>(); + param.percentage_target = config["percentage_target"]; + param.percentage_target_value = config["percentage_target_value"]; + param.save_percentages_meshes = config["save_percentages_meshes"]; + + param.E_abs_err = config["E_abs_err"]; + param.E_rel_err = config["E_rel_err"]; + param.diff_err = config["diff_err"]; + param.grad_abs_err = config["grad_abs_err"]; + param.grad_rel_err = config["grad_rel_err"]; + + param.precompute_seamless = config["precompute_seamless"]; + param.projected_newton = config["projected_newton"]; + param.soft_max = config["soft_max"]; + param.t = config["t"]; + param.precompute_seamless = config["precompute_seamless"]; + + param.percentage_target_converge = config["percentage_target_converge"]; + param.max_grad_abs_converge = config["max_grad_abs_converge"]; + param.max_grad_rel_converge = config["max_grad_rel_converge"]; + param.energy_diff_converge = config["energy_diff_converge"]; + param.use_worst_n_energy_in_ls = config["use_worst_n_energy_in_ls"]; + param.E_abs_converge = config["E_abs_converge"]; + param.E_rel_converge = config["E_rel_converge"]; + + param.last_screenshot_after_optimization = config["last_screenshot_after_optimization"]; + param.screenshot_interval = config["screenshot_interval"]; + param.output_dir_for_screenshots = config["output_dir_for_screenshots"]; + param.uv_scale_for_screenshots = config["uv_scale_for_screenshots"]; + param.angle_to_rotate_model_for_screenshots = config["angle_to_rotate_model_for_screenshots"]; + param.screenshot_during_optimization = config["screenshot_during_optimization"]; + + param.degenerate_vertices_preconditioner = config["degenerate_vertices_preconditioner"]; + param.precond_dim = config["precond_dim"]; + param.triangle_threshold = config["triangle_threshold"]; + + return param; +} + +Eigen::MatrixXd optimize_seamless_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv_init, + const Eigen::MatrixXi& FT_init, + const Eigen::MatrixXd& Du, + const Eigen::MatrixXd& Dv, + SymDir::Parameters param) +{ + // set trivial feature edges + Eigen::MatrixXi FE(0, 0); + Eigen::MatrixXi ME(0, 0); + return optimize_aligned_parameterization( + V_init, + F_init, + uv_init, + FT_init, + FE, + ME, + Du, + Dv, + param); +} + +Eigen::MatrixXd optimize_aligned_parameterization( + const Eigen::MatrixXd& V_init, + const Eigen::MatrixXi& F_init, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F, + const Eigen::MatrixXi& FE_init, + const Eigen::MatrixXi& ME, + const Eigen::MatrixXd& Du, + const Eigen::MatrixXd& Dv, + SymDir::Parameters param) +{ + // cut mesh along seams + MeshCutter meshcutter(V_init, uv, F_init, F); + auto [V, EE] = meshcutter.cut_mesh(); + Eigen::MatrixXi FE(0, 0); + if (param.do_feature_alignment) + { + // Loading the feature edge constraints + FE = meshcutter.reindex_feature_edges(FE_init); + } + + // initialize data structures + SymDir::ExtremeOpt extremeopt(V, F); + extremeopt.m_params = param; + extremeopt.create_mesh(V, F, uv); + extremeopt.set_v_map(F_init, F); + + // set constraints + if (extremeopt.m_params.with_cons) + { + std::vector> EE_e = transform_EE(F, EE); + std::vector> FE_e; + if (extremeopt.m_params.do_feature_alignment) { + FE_e = transform_FE(F, FE); + } + extremeopt.init_constraints(EE_e); + extremeopt.EE = EE; + extremeopt.FE = FE; + extremeopt.ME = ME; + } + + // set field directions + extremeopt.PD1 = Du; + extremeopt.PD2 = Dv; + + // optimize parametrization + Eigen::MatrixXi F_opt = F; + Eigen::MatrixXd uv_opt; + extremeopt.do_optimization_without_log(); + extremeopt.export_mesh(V, F_opt, uv_opt); + + return uv_opt; +} + +} // namespace Optimization +} // namespace Penner + +#endif \ No newline at end of file diff --git a/src/optimization/pybind.cpp b/src/optimization/pybind.cpp index f451fd4..00c2d7d 100644 --- a/src/optimization/pybind.cpp +++ b/src/optimization/pybind.cpp @@ -16,6 +16,7 @@ #include "optimization/metric_optimization/energy_functor.h" #include "optimization/metric_optimization/explicit_optimization.h" #include "optimization/metric_optimization/implicit_optimization.h" +#include "optimization/metric_optimization/uv_optimization.h" #include "parametrization/interpolation.h" #include "parametrization/layout.h" #include "optimization/interface.h" @@ -25,6 +26,7 @@ #include "optimization/util/shapes.h" #include "metric/shear.h" #include "parametrization/translation.h" +#include "parametrization/error.h" #ifdef USE_HIGHFIVE #include @@ -69,18 +71,6 @@ std::tuple, std::vector> FV_to_double_pybind( void init_classes_pybind(pybind11::module& m) { - pybind11::class_>( - m, - "ProjectionParameters") - .def(pybind11::init<>()) - .def_readwrite("max_itr", &ProjectionParameters::max_itr) - .def_readwrite("output_dir", &ProjectionParameters::output_dir) - .def_readwrite("bound_norm_thres", &ProjectionParameters::bound_norm_thres) - .def_readwrite("error_eps", &ProjectionParameters::error_eps) - .def_readwrite("do_reduction", &ProjectionParameters::do_reduction) - .def_readwrite("use_edge_flips", &ProjectionParameters::use_edge_flips) - .def_readwrite("initial_ptolemy", &ProjectionParameters::initial_ptolemy); - pybind11::class_>( m, "OptimizationParameters") @@ -146,39 +136,6 @@ void init_classes_pybind(pybind11::module& m) .def_readwrite("lambda0", &LineSearchParameters::lambda0) .def_readwrite("reset_lambda", &LineSearchParameters::reset_lambda); - pybind11::class_>(m, "Mesh") - .def(pybind11::init<>()) - .def_readwrite("n", &OverlayProblem::Mesh::n) - .def_readwrite("to", &OverlayProblem::Mesh::to) - .def_readwrite("f", &OverlayProblem::Mesh::f) - .def_readwrite("h", &OverlayProblem::Mesh::h) - .def_readwrite("out", &OverlayProblem::Mesh::out) - .def_readwrite("opp", &OverlayProblem::Mesh::opp) - .def_readwrite("R", &OverlayProblem::Mesh::R) - .def_readwrite("type", &OverlayProblem::Mesh::type) - .def_readwrite("Th_hat", &OverlayProblem::Mesh::Th_hat) - .def_readwrite("l", &OverlayProblem::Mesh::l) - .def_readwrite("v_rep", &OverlayProblem::Mesh::v_rep) - .def_readwrite("fixed_dof", &OverlayProblem::Mesh::fixed_dof); - - pybind11::class_>(m, "OverlayMesh") - .def_readwrite("n", &OverlayProblem::OverlayMesh::n) - .def_readwrite("to", &OverlayProblem::OverlayMesh::to) - .def_readwrite("f", &OverlayProblem::OverlayMesh::f) - .def_readwrite("h", &OverlayProblem::OverlayMesh::h) - .def_readwrite("out", &OverlayProblem::OverlayMesh::out) - .def_readwrite("opp", &OverlayProblem::OverlayMesh::opp) - .def_readwrite("R", &OverlayProblem::OverlayMesh::R) - .def_readwrite("type", &OverlayProblem::OverlayMesh::type) - .def_readwrite("prev", &OverlayProblem::OverlayMesh::prev) - .def_readwrite("first_segment", &OverlayProblem::OverlayMesh::first_segment) - .def_readwrite("origin", &OverlayProblem::OverlayMesh::origin) - .def_readwrite("origin_of_origin", &OverlayProblem::OverlayMesh::origin_of_origin) - .def_readwrite("vertex_type", &OverlayProblem::OverlayMesh::vertex_type) - .def_readwrite("edge_type", &OverlayProblem::OverlayMesh::edge_type) - .def_readwrite("seg_bcs", &OverlayProblem::OverlayMesh::seg_bcs) - .def_readwrite("_m", &OverlayProblem::OverlayMesh::_m); - pybind11::class_(m, "ReductionMaps") .def(pybind11::init&>()) .def_readwrite("he2e", &ReductionMaps::he2e) @@ -216,27 +173,6 @@ void init_classes_pybind(pybind11::module& m) &InterpolationMesh::get_overlay_mesh, pybind11::return_value_policy::copy); - pybind11:: - class_, Mesh>( - m, - "DifferentiableConeMetric") - .def("get_metric_coordinates", &DifferentiableConeMetric::get_metric_coordinates) - .def( - "get_corner_angles", - static_cast< - std::tuple< - VectorX, - VectorX - > (DifferentiableConeMetric::*)() const>(&DifferentiableConeMetric::get_corner_angles)) - .def( - "get_reduced_metric_coordinates", - &DifferentiableConeMetric::get_reduced_metric_coordinates) - .def("get_expansion_matrix", &DifferentiableConeMetric::get_expansion_matrix) - .def("set_metric_coordinates", &DifferentiableConeMetric::set_metric_coordinates); - - pybind11::class_(m, "DiscreteMetric") - .def(pybind11::init&, const VectorX&>()); - pybind11::class_(m, "RefinementMesh") .def(pybind11::init< const Eigen::MatrixXd&, // V @@ -407,11 +343,6 @@ void init_parameterization_pybind(pybind11::module& m) "Make overlay mesh a tufted cover", pybind11:: call_guard()); - m.def( - "compute_uv_length_error", - &compute_uv_length_error, - pybind11:: - call_guard()); m.def("compute_layout_VF", &compute_layout_VF, pybind11:: call_guard()); @@ -476,6 +407,22 @@ load_simplify_overlay_output(std::string fname) } #endif +void init_uv_optimization_pybind(pybind11::module& m) +{ +#ifdef USE_UV_OPTIMIZATION + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + pybind11::class_(m, "SymDirParameters") + .def(pybind11::init<>()); + + m.def("load_parameters", &load_parameters, default_call_guard); + m.def("optimize_seamless_parameterization", &optimize_seamless_parameterization, default_call_guard); + m.def("optimize_aligned_parameterization", &optimize_aligned_parameterization, default_call_guard); +#endif +} + void init_optimization_pybind(pybind11::module& m) { init_classes_pybind(m); @@ -484,6 +431,8 @@ void init_optimization_pybind(pybind11::module& m) init_energies_pybind(m); init_opt_pybind(m); init_parameterization_pybind(m); + init_uv_optimization_pybind(m); + #ifdef USE_HIGHFIVE m.def( diff --git a/src/parametrization/error.cpp b/src/parametrization/error.cpp new file mode 100644 index 0000000..80a7191 --- /dev/null +++ b/src/parametrization/error.cpp @@ -0,0 +1,118 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#include "parametrization/layout.h" + +#include +#include +#include + + +namespace Penner { + + +bool check_areas(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F) +{ + Eigen::VectorXd areas; + igl::doublearea(V, F, areas); + double min_area = areas.minCoeff() / 2.0; + double max_area = areas.maxCoeff() / 2.0; + spdlog::debug("Minimum VF face area: {}", min_area); + spdlog::debug("Maximum VF face area: {}", max_area); + + return (min_area >= 0); +} + +// compute the squared length of an edge between two vertices +double uv_length_squared(const Eigen::Vector2d& uv_0, const Eigen::Vector2d& uv_1) +{ + Eigen::Vector2d difference_vector = uv_1 - uv_0; + double length_sq = difference_vector.dot(difference_vector); + return length_sq; +} + +// compute the length of an edge between two vertices +double uv_length(const Eigen::Vector2d& uv_0, const Eigen::Vector2d& uv_1) +{ + return sqrt(uv_length_squared(uv_0, uv_1)); +} + +double compute_uv_length_error( + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv) +{ + // Get the edge topology for the original uncut mesh + Eigen::MatrixXi uE, EF, EI; + Eigen::VectorXi EMAP; + igl::edge_flaps(F, uE, EMAP, EF, EI); + + // Iterate over edges to check the length inconsistencies + double max_uv_length_error = 0.0; + for (Eigen::Index e = 0; e < EF.rows(); ++e) { + // Get face corners corresponding to the current edge + int f0 = EF(e, 0); + int f1 = EF(e, 1); + + // Check first face (if not boundary) + if (f0 < 0) continue; + int i0 = EI(e, 0); // corner vertex face index + int v0n = F_uv(f0, (i0 + 1) % 3); // next vertex + int v0p = F_uv(f0, (i0 + 2) % 3); // previous vertex + + // Check second face (if not boundary) + if (f1 < 0) continue; + int i1 = EI(e, 1); // corner vertex face index + int v1n = F_uv(f1, (i1 + 1) % 3); // next vertex + int v1p = F_uv(f1, (i1 + 2) % 3); // next vertex + + // Compute the length of each halfedge corresponding to the corner in the cut mesh + double l0 = uv_length(uv.row(v0n), uv.row(v0p)); + double l1 = uv_length(uv.row(v1n), uv.row(v1p)); + + // Determine if the max length inconsistency has increased + max_uv_length_error = max(max_uv_length_error, abs(l0 - l1)); + } + + // Return the max uv length error + return max_uv_length_error; +} + +bool check_uv( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& F_uv) +{ + int n_faces = F.rows(); + bool is_valid = true; + + // Check faces agree in number + if (F_uv.rows() != n_faces) { + spdlog::error("Mesh and uv faces are not in one to one correspondence"); + is_valid = false; + } + + // Check length consistency + double uv_length_error = compute_uv_length_error(F, uv, F_uv); + if (!float_equal(uv_length_error, 0.0, 1e-6)) { + spdlog::warn("Inconsistent uv length error {} across VF edges", uv_length_error); + } + + // Check uv face areas + if (!check_areas(uv, F_uv)) { + spdlog::error("Triangle is flipped in VF"); + is_valid = false; + } + + // Return true if no issues found + return is_valid; +} + + +} // namespace Penner diff --git a/src/parametrization/layout.cpp b/src/parametrization/layout.cpp index 717ca23..c711406 100644 --- a/src/parametrization/layout.cpp +++ b/src/parametrization/layout.cpp @@ -13,309 +13,16 @@ #include #include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "util/boundary.h" #include "metric/cone_metric.h" #include "util/vector.h" #include "util/embedding.h" -//#include "util/io.h" -//#include "metric/constraint.h" -//#include "parametrization/refinement.h" +#include "parametrization/error.h" #include "parametrization/interpolation.h" namespace Penner { -bool check_areas(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F) -{ - Eigen::VectorXd areas; - igl::doublearea(V, F, areas); - double min_area = areas.minCoeff() / 2.0; - double max_area = areas.maxCoeff() / 2.0; - spdlog::debug("Minimum VF face area: {}", min_area); - spdlog::debug("Maximum VF face area: {}", max_area); - - return (min_area >= 0); -} - -// compute the squared length of an edge between two vertices -double uv_length_squared(const Eigen::Vector2d& uv_0, const Eigen::Vector2d& uv_1) -{ - Eigen::Vector2d difference_vector = uv_1 - uv_0; - double length_sq = difference_vector.dot(difference_vector); - return length_sq; -} - -// compute the length of an edge between two vertices -double uv_length(const Eigen::Vector2d& uv_0, const Eigen::Vector2d& uv_1) -{ - return sqrt(uv_length_squared(uv_0, uv_1)); -} - -double compute_uv_length_error( - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv) -{ - // Get the edge topology for the original uncut mesh - Eigen::MatrixXi uE, EF, EI; - Eigen::VectorXi EMAP; - igl::edge_flaps(F, uE, EMAP, EF, EI); - - // Iterate over edges to check the length inconsistencies - double max_uv_length_error = 0.0; - for (Eigen::Index e = 0; e < EF.rows(); ++e) { - // Get face corners corresponding to the current edge - int f0 = EF(e, 0); - int f1 = EF(e, 1); - - // Check first face (if not boundary) - if (f0 < 0) continue; - int i0 = EI(e, 0); // corner vertex face index - int v0n = F_uv(f0, (i0 + 1) % 3); // next vertex - int v0p = F_uv(f0, (i0 + 2) % 3); // previous vertex - - // Check second face (if not boundary) - if (f1 < 0) continue; - int i1 = EI(e, 1); // corner vertex face index - int v1n = F_uv(f1, (i1 + 1) % 3); // next vertex - int v1p = F_uv(f1, (i1 + 2) % 3); // next vertex - - // Compute the length of each halfedge corresponding to the corner in the cut mesh - double l0 = uv_length(uv.row(v0n), uv.row(v0p)); - double l1 = uv_length(uv.row(v1n), uv.row(v1p)); - - // Determine if the max length inconsistency has increased - max_uv_length_error = max(max_uv_length_error, abs(l0 - l1)); - } - - // Return the max uv length error - return max_uv_length_error; -} - -bool check_uv( - const Eigen::MatrixXd& V, - const Eigen::MatrixXi& F, - const Eigen::MatrixXd& uv, - const Eigen::MatrixXi& F_uv) -{ - int n_faces = F.rows(); - bool is_valid = true; - - // Check faces agree in number - if (F_uv.rows() != n_faces) { - spdlog::error("Mesh and uv faces are not in one to one correspondence"); - is_valid = false; - } - - // Check length consistency - double uv_length_error = compute_uv_length_error(F, uv, F_uv); - if (!float_equal(uv_length_error, 0.0, 1e-6)) { - spdlog::warn("Inconsistent uv length error {} across VF edges", uv_length_error); - } - - // Check uv face areas - if (!check_areas(uv, F_uv)) { - spdlog::error("Triangle is flipped in VF"); - is_valid = false; - } - - // Return true if no issues found - return is_valid; -} - -// signed area of a triangle ABC -double signed_area( - const Eigen::Vector2d& A, - const Eigen::Vector2d& B, - const Eigen::Vector2d& C) -{ - Eigen::Matrix tet; - tet.row(0) << A(0), A(1), 1.; - tet.row(1) << B(0), B(1), 1.; - tet.row(2) << C(0), C(1), 1.; - - return tet.determinant(); -} - -// find the previous halfedge of a halfedge in a mesh -template -int prev_halfedge( - const Mesh& m, - int hij) -{ - int hli = hij; - while (m.n[hli] != hij) - { - hli = m.n[hli]; - } - - return hli; -} - -template -int prev_halfedge( - const OverlayMesh& m, - int hij) -{ - int hli = hij; - while (m.n[hli] != hij) - { - hli = m.n[hli]; - } - - return hli; -} - -// compute the area of the triangle with vertices ijk -template -Scalar triangle_area( - const Mesh& m, - const std::vector& u, - const std::vector& v, - int hij) -{ - int hjk = m.n[hij]; - int hli = prev_halfedge(m, hij); - Eigen::Vector2d A = {u[hli], v[hli]}; - Eigen::Vector2d B = {u[hij], v[hij]}; - Eigen::Vector2d C = {u[hjk], v[hjk]}; - return signed_area(A, B, C); -} - -// check that the signed area of the layout triangles are all positive -template -bool check_areas( - const Mesh& m, - const std::vector& u, - const std::vector& v) -{ - int num_halfedges = m.n_halfedges(); - Scalar min_area = triangle_area(m, u, v, 0); - Scalar max_area = min_area; - for (int hij = 0; hij < num_halfedges; ++hij) - { - Scalar area_hijk = triangle_area(m, u, v, hij); - min_area = min(area_hijk, min_area); - max_area = max(area_hijk, max_area); - } - spdlog::debug("minimum overlay triangle face area: {}", min_area); - spdlog::debug("maximum overlay triangle face area: {}", max_area); - - return (min_area >= 0); -} - -// check that the difference of lengths of opposite halfedges -template -Scalar compute_uv_length_error( - const Mesh& m, - const std::vector& u, - const std::vector& v) -{ - int num_halfedges = m.n_halfedges(); - Scalar max_uv_length_error = 0.; - for (int hij = 0; hij < num_halfedges; ++hij) - { - int hji = m.opp[hij]; - - // get other halfedges in the face for vertex computation - int hki = prev_halfedge(m, hij); - int hlj = prev_halfedge(m, hji); - - // get uv vertices on the edge - Eigen::Vector2d uv0i = {u[hki], v[hki]}; - Eigen::Vector2d uv0j = {u[hij], v[hij]}; - Eigen::Vector2d uv1i = {u[hji], v[hji]}; - Eigen::Vector2d uv1j = {u[hlj], v[hlj]}; - - // compute the length of each halfedge - Scalar l0 = uv_length(uv0i, uv0j); - Scalar l1 = uv_length(uv1i, uv1j); - - // determine if the max length inconsistency has increased - if (abs(l0 - l1) > 1e-8) - { - spdlog::warn("uv length consistency error for edge {}, {} is {} - {}", hij, hji, l0, l1); - } - max_uv_length_error = max(max_uv_length_error, abs(l0 - l1)); - } - - // return the max uv length error - return max_uv_length_error; -} - -// check the edge consistency and signed area of a mesh layout -template -bool check_uv( - const Mesh& m, - const std::vector& u, - const std::vector& v, - const std::vector& is_cut) -{ - int num_halfedges = m.n_halfedges(); - bool is_valid = true; - - // Check faces agree in number - if ((u.size() != num_halfedges) || (v.size() != num_halfedges)) { - spdlog::error("uv coordinates not in correspondence with mesh halfedges"); - is_valid = false; - } - - // Check length consistency - Scalar uv_length_error = compute_uv_length_error(m, u, v); - if (!float_equal(uv_length_error, 0.0, 1e-6)) { - spdlog::warn("Inconsistent uv length error {} across edges", uv_length_error); - } - - // Check uv face areas - if (!check_areas(m, u, v)) { - spdlog::error("Triangle is flipped in overlay"); - is_valid = false; - } - - // Return true if no issues found - return is_valid; -} - -// check that two different layouts of a mesh have consistent lengths -template -bool check_uv_consistency( - const Mesh& m, - const std::vector& u0, - const std::vector& v0, - const std::vector& u1, - const std::vector& v1) -{ - int num_halfedges = m.n_halfedges(); - std::vector u_error(num_halfedges); - std::vector v_error(num_halfedges); - Scalar max_consistency_error = 0.; - for (int hij = 0; hij < num_halfedges; ++hij) - { - // get next halfedge - int hjk = m.n[hij]; - - // get uv vertices on the edge - Eigen::Vector2d uv0j = {u0[hij], v0[hij]}; - Eigen::Vector2d uv0k = {u0[hjk], v0[hjk]}; - Eigen::Vector2d uv1j = {u1[hij], v1[hij]}; - Eigen::Vector2d uv1k = {u1[hjk], v1[hjk]}; - - // compute the length of the halfedge in each metric - Scalar l0 = uv_length(uv0j, uv0k); - Scalar l1 = uv_length(uv1j, uv1k); - - if (abs(l0 - l1) > 1e-8) - { - spdlog::warn("uv length consistency error for {} with previous halfedge {} is {} - {}", hjk, hij, l0, l1); - spdlog::warn("local face is {}, {}, {}, {}, ...", hij, hjk, m.n[hjk], m.n[m.n[hjk]]); - } - max_consistency_error = max(max_consistency_error, abs(l0 - l1)); - } - spdlog::debug("max consistency error is {}", max_consistency_error); - - return (max_consistency_error < 1e-8); -} - - std::vector compute_layout_topology(const Mesh& m, const std::vector& is_cut_h, int start_h) { @@ -325,13 +32,18 @@ compute_layout_topology(const Mesh& m, const std::vector& is_cut_h // set starting point - use a boundary edge if none given int h = 0; - if (start_h == -1) { + if (start_h < 0) { for (int i = 0; i < m.n_halfedges(); i++) { if (m.type[i] == 1 && m.type[m.opp[i]] == 2) { h = i; + break; } } } + else + { + h = start_h; + } // Keep a record of faces that have been laid out auto done = std::vector(m.n_faces(), false); @@ -446,8 +158,6 @@ Scalar area_from_len_l(Scalar l1, Scalar l2, Scalar l3) { return sqrt(s * (s - l1) * (s - l2) * (s - l3)); } -Scalar square_l(Scalar x) { return x * x; }; - Eigen::Matrix compute_layout_vertex( const Eigen::Matrix& p1, const Eigen::Matrix& p2, @@ -455,7 +165,7 @@ Eigen::Matrix compute_layout_vertex( Scalar l1, Scalar l2) { - return p1 + (p2 - p1) * (1 + square_l(l2 / l0) - square_l(l1 / l0)) / 2 + + return p1 + (p2 - p1) * (1 + square(l2 / l0) - square(l1 / l0)) / 2 + perp_l(p2 - p1) * 2 * area_from_len_l(1.0, l1 / l0, l2 / l0); } @@ -529,20 +239,6 @@ std::tuple, std::vector, std::vector> compute_ Q.push(h); done[m.f[h]] = true; - auto perp = [](Eigen::Matrix a) { - Eigen::Matrix b; - b[0] = -a[1]; - b[1] = a[0]; - return b; - }; - - auto area_from_len = [](Scalar l1, Scalar l2, Scalar l3) { - auto s = 0.5 * (l1 + l2 + l3); - return sqrt(s * (s - l1) * (s - l2) * (s - l3)); - }; - - auto square = [](Scalar x) { return x * x; }; - spdlog::debug("beginning layout"); while (!Q.empty()) { h = Q.front(); @@ -560,9 +256,7 @@ std::tuple, std::vector, std::vector> compute_ Scalar l0 = Scalar(1.0); Scalar l1 = exp((phi[hn] - phi[hp]) / 2) * (m.l[hn] / m.l[h]); Scalar l2 = exp((phi[hn] - phi[h]) / 2) * (m.l[hp] / m.l[h]); - Eigen::Matrix pn = p1 + - (p2 - p1) * (1 + square(l2 / l0) - square(l1 / l0)) / 2 + - perp(p2 - p1) * 2 * area_from_len(1.0, l1 / l0, l2 / l0); + Eigen::Matrix pn = compute_layout_vertex(p1, p2, l0, l1, l2); #ifdef CHECK_VALIDITY if (!float_equal((p1 - p2).norm(), m.l[h])) spdlog::error("inconsistent lengths {}, {}", (p1 - p2).norm(), m.l[h]); if (!float_equal((pn - p2).norm(), m.l[hn])) spdlog::error("inconsistent lengths {}, {}", (pn - p2).norm(), m.l[hn]); @@ -716,39 +410,6 @@ std::vector pullback_cut_to_overlay( } } -// Helper function to determine if any faces in a triangle mesh are flipped -template -void check_if_flipped(Mesh& m, const std::vector& u, const std::vector& v) -{ - Eigen::Matrix uv(u.size(), 2); - Eigen::MatrixXi F_uv(m.n_faces(), 3); - - for (size_t i = 0; i < u.size(); ++i) { - uv(i, 0) = static_cast(u[i]); - uv(i, 1) = static_cast(v[i]); - } - - for (int fi = 0; fi < m.n_faces(); ++fi) { - int h = m.h[fi]; - F_uv(fi, 0) = h; - F_uv(fi, 1) = m.n[h]; - F_uv(fi, 2) = m.n[m.n[h]]; - } - - Eigen::VectorXi flipped_f; - igl::flipped_triangles(uv, F_uv, flipped_f); - spdlog::debug("{} flipped elements in mesh", flipped_f.size()); - for (int i = 0; i < flipped_f.size(); ++i) { - int fi = flipped_f[i]; - spdlog::debug("Face {} is flipped", F_uv.row(fi)); - spdlog::debug( - "Vertices {}, {}, {}", - uv.row(F_uv(fi, 0)), - uv.row(F_uv(fi, 1)), - uv.row(F_uv(fi, 2))); - } -} - template std::tuple build_layout_VF( const Mesh& m, @@ -869,8 +530,7 @@ get_consistent_layout( std::vector singularities, const std::vector& is_cut_orig, const std::vector& is_cut, - bool use_uniform_bc, - std::string layout_output_path) + bool use_uniform_bc) { // Get original overlay face labels auto f_labels = get_overlay_face_labels(m_o); @@ -906,6 +566,7 @@ get_consistent_layout( auto is_cut_c = std::get<2>(layout_res); mc.type = mc_type; + std::string layout_output_path = ""; if (layout_output_path != "") { auto [F_c, uv_c, FT_c] = build_layout_VF(mc, _u_c, _v_c); @@ -1033,8 +694,6 @@ get_consistent_layout( u_o = m_o.interpolate_along_c_bc(mc.n, mc.f, _u_c); v_o = m_o.interpolate_along_c_bc(mc.n, mc.f, _v_c); - //u_o = m_o.interpolate_along_c(_u_c); - //v_o = m_o.interpolate_along_c(_v_c); spdlog::trace("Interpolate on overlay mesh done."); #ifdef CHECK_VALIDITY @@ -1061,7 +720,6 @@ get_consistent_layout( } while (h0 != h1); if (m.to[m.opp[h0]] != m.to[h1]) { spdlog::error("h0 h1 picked wrong."); - exit(0); } m.l[h0] = sqrt(max( (u_o[h0] - u_o[h1]) * (u_o[h0] - u_o[h1]) + (v_o[h0] - v_o[h1]) * (v_o[h0] - v_o[h1]), 0.)); @@ -1183,19 +841,23 @@ std:: std::vector, // Fn_to_F std::vector> // endpoints_o > - consistent_overlay_mesh_to_VL( + layout_overlay_mesh( const Mesh& _m, OverlayMesh& mo, const std::vector& vtx_reindex, - const std::vector& is_bd, std::vector& u, std::vector>& V_overlay, - std::vector>& endpoints, const std::vector& is_cut_orig, const std::vector& is_cut, - bool use_uniform_bc, - std::string layout_output_path) + bool use_uniform_bc) { + // Get boundary vertices + std::vector is_bd = compute_boundary_vertices(_m); + + // Get endpoints + std::vector> endpoints; + find_origin_endpoints(mo, endpoints); + const auto& m = mo.cmesh(); // get cones and bd @@ -1227,8 +889,7 @@ std:: cones, is_cut_orig, is_cut, - use_uniform_bc, - layout_output_path); + use_uniform_bc); auto u_o = std::get<0>(layout_res); auto v_o = std::get<1>(layout_res); auto is_cut_h = std::get<2>(layout_res); @@ -1253,7 +914,7 @@ std:: } } - // reindex back + // reindex to remove vtx reindexing introduced by the halfedge mesh auto u_o_out_copy = u_o_out; auto v_o_out_copy = v_o_out; auto v3d_out_copy = v3d_out; @@ -1390,18 +1051,15 @@ std:: std::vector, // Fn_to_F std::vector> // endpoints_o > - consistent_overlay_mesh_to_VL( + layout_overlay_mesh( const Mesh& _m, OverlayMesh& mo, const std::vector& vtx_reindex, - const std::vector& is_bd, std::vector& u, std::vector>& V_overlay, - std::vector>& endpoints, const std::vector& is_cut_orig, const std::vector& is_cut, - bool use_uniform_bc, - std::string layout_output_path); + bool use_uniform_bc); template std::tuple< @@ -1445,18 +1103,15 @@ std:: std::vector, // Fn_to_F std::vector> // endpoints_o > - consistent_overlay_mesh_to_VL( + layout_overlay_mesh( const Mesh& _m, OverlayMesh& mo, const std::vector& vtx_reindex, - const std::vector& is_bd, std::vector& u, std::vector>& V_overlay, - std::vector>& endpoints, const std::vector& is_cut_orig, const std::vector& is_cut, - bool use_uniform_bc, - std::string layout_output_path); + bool use_uniform_bc); template std::tuple< Eigen::MatrixXi, diff --git a/src/parametrization/parametrize.cpp b/src/parametrization/parametrize.cpp index 397f7b8..27aa552 100644 --- a/src/parametrization/parametrize.cpp +++ b/src/parametrization/parametrize.cpp @@ -8,12 +8,13 @@ #include "parametrization/parametrize.h" -#include "util/boundary.h" #include "metric/projection.h" #include "parametrization/interpolation.h" #include "parametrization/layout.h" #include "util/vector.h" #include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "parametrization/refinement.h" +#include "parametrization/error.h" /// FIXME Do cleaning pass @@ -42,18 +43,14 @@ std:: const VectorX& reduced_metric_coords, std::vector cut_h, bool do_best_fit_scaling, - bool use_uniform_bc, - std::string layout_output_path) + bool use_uniform_bc) { // Get metric target coordinates auto cone_metric = initial_cone_metric.set_metric_coordinates(reduced_metric_coords); VectorX metric_target = initial_cone_metric.get_metric_coordinates(); VectorX metric_coords = cone_metric->get_metric_coordinates(); - // Get boundary vertices - std::vector is_bd = compute_boundary_vertices(m); - - // Fit conformal scale factors + // Optionally fit conformal scale factors for numerical stability VectorX metric_coords_scaled = metric_coords; VectorX scale_factors; scale_factors.setZero(initial_cone_metric.n_ind_vertices()); @@ -63,18 +60,6 @@ std:: metric_coords_scaled = metric_coords - B * scale_factors; } VectorX metric_diff = metric_coords_scaled - metric_target; - SPDLOG_DEBUG( - "Scale factors in range [{}, {}]", - scale_factors.minCoeff(), - scale_factors.maxCoeff()); - SPDLOG_DEBUG( - "Scaled metric coordinates in range [{}, {}]", - metric_coords_scaled.minCoeff(), - metric_coords_scaled.maxCoeff()); - SPDLOG_DEBUG( - "Differences from target to optimized metric in range [{}, {}]", - metric_diff.minCoeff(), - metric_diff.maxCoeff()); // Compute interpolation overlay mesh Eigen::MatrixXd V_overlay; @@ -117,20 +102,16 @@ std:: std::vector vtx_reindex_mutable = vtx_reindex; std::vector u; // (m_o._m.Th_hat.size(), 0.0); convert_eigen_to_std_vector(scale_factors, u); - // auto parametrize_res = overlay_mesh_to_VL(V, F, Th_hat, m_o, u, V_overlay_vec, // vtx_reindex_mutable, endpoints, -1); FIXME - return consistent_overlay_mesh_to_VL( + return layout_overlay_mesh( m, m_o, vtx_reindex, - is_bd, u, V_overlay_vec, - endpoints, is_cut, {}, - use_uniform_bc, - layout_output_path); + use_uniform_bc); } std:: @@ -162,6 +143,42 @@ std:: return generate_VF_mesh_from_halfedge_metric(V, m, vtx_reindex, initial_cone_metric, reduced_metric_coords, cut_h, do_best_fit_scaling); } + std::tuple< + Eigen::MatrixXd, // V_o + Eigen::MatrixXi, // F_o + Eigen::MatrixXd, // uv_o + Eigen::MatrixXi, // FT_o + std::vector, // Fn_to_F_o + std::vector>> // endpoints_o + parametrize_metric( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const DifferentiableConeMetric& initial_cone_metric, + const VectorX& reduced_metric_coords) +{ + // Get mesh with vertex reindexing + std::vector Th_hat(V.rows(), 0); // trivial cones + std::vector vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops; + Mesh m = + FV_to_double(V, F, V, F, Th_hat, vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops); + m.Th_hat = initial_cone_metric.Th_hat; // copy over angle constraints + + std::vector cut_h = {}; + auto vf_res = generate_VF_mesh_from_halfedge_metric(V, m, vtx_reindex, initial_cone_metric, reduced_metric_coords, cut_h); + + // simplify the refined parametrization + Eigen::MatrixXd V_o = std::get<1>(vf_res); + Eigen::MatrixXi F_o = std::get<2>(vf_res); + Eigen::MatrixXd uv_o = std::get<3>(vf_res); + Eigen::MatrixXi FT_o = std::get<4>(vf_res); + std::vector fn_to_f_o = std::get<7>(vf_res); + std::vector> endpoints_o = std::get<8>(vf_res); + RefinementMesh refinement_mesh(V_o, F_o, uv_o, FT_o, fn_to_f_o, endpoints_o); + refinement_mesh.refine_mesh(); + refinement_mesh.simplify_mesh(); + return refinement_mesh.get_VF_mesh(); +} + std:: tuple< Eigen::MatrixXd, // V_o @@ -182,14 +199,11 @@ std:: Mesh m = FV_to_double(V, F, V, F, Th_hat, vtx_reindex, indep_vtx, dep_vtx, v_rep, bnd_loops); - // Get boundary vertices - std::vector is_bd = compute_boundary_vertices(m); - // Get layout topology from mesh std::vector is_cut = compute_layout_topology(m, cut_h); // Set metric for layout - DiscreteMetric discrete_metric(m, reduced_log_edge_lengths); + DiscreteMetric discrete_metric(m, reduced_log_edge_lengths); // Create trivial overlay mesh OverlayMesh m_o(discrete_metric); @@ -204,21 +218,15 @@ std:: } } - // Get endpoints - std::vector> endpoints; - find_origin_endpoints(m_o, endpoints); - - // Compute layout + // Compute layout std::vector u_vec(m.n_ind_vertices(), 0.0); //std::vector vtx_reindex_mutable = vtx_reindex; - auto layout_res = consistent_overlay_mesh_to_VL( + auto layout_res = layout_overlay_mesh( m, m_o, vtx_reindex, - is_bd, u_vec, V_overlay_vec, - endpoints, is_cut, {}); @@ -251,8 +259,7 @@ std:: const VectorX& reduced_metric_coords, std::vector cut_h, bool do_best_fit_scaling, - bool use_uniform_bc, - std::string layout_output_path); + bool use_uniform_bc); #ifdef WITH_MPFR #ifndef MULTIPRECISION @@ -278,8 +285,7 @@ std:: const VectorX& reduced_metric_coords, std::vector cut_h, bool do_best_fit_scaling, - bool use_uniform_bc, - std::string layout_output_path); + bool use_uniform_bc); #endif #endif diff --git a/src/pybind.cpp b/src/pybind.cpp index 351cebe..86d2b1a 100644 --- a/src/pybind.cpp +++ b/src/pybind.cpp @@ -6,22 +6,150 @@ // v. 2.0. If a copy of the MPL was not distributed with this file, You can // obtain one at http://mozilla.org/MPL/2.0/. +#include "metric/cone_metric.h" +#include "metric/interface.h" +#include "metric/viewer.h" +#include "parametrization/parametrize.h" + #include "optimization/pybind.h" #include "holonomy/pybind.h" +#include "holonomy/core/viewer.h" #include "feature/pybind.h" +#include "field/frame_field.h" +#include "field/cross_field.h" + using namespace Penner; using namespace Optimization; using namespace Holonomy; using namespace Feature; +using namespace Field; #ifdef PYBIND #ifndef MULTIPRECISION +void init_metric_pybind(pybind11::module& m) +{ + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + pybind11::class_>( + m, + "ProjectionParameters") + .def(pybind11::init<>()) + .def_readwrite("max_itr", &ProjectionParameters::max_itr) + .def_readwrite("bound_norm_thres", &ProjectionParameters::bound_norm_thres) + .def_readwrite("error_eps", &ProjectionParameters::error_eps) + .def_readwrite("do_reduction", &ProjectionParameters::do_reduction) + .def_readwrite("initial_ptolemy", &ProjectionParameters::initial_ptolemy) + .def_readwrite("use_edge_flips", &ProjectionParameters::use_edge_flips) + .def_readwrite("output_dir", &ProjectionParameters::output_dir); + + pybind11::class_( + m, + "ConeMetricParameters") + .def(pybind11::init<>()) + .def_readwrite("use_log_length", &ConeMetricParameters::use_log_length) + .def_readwrite("use_initial_zero", &ConeMetricParameters::use_initial_zero) + .def_readwrite("free_interior", &ConeMetricParameters::free_interior); + + pybind11::class_>(m, "Mesh") + .def(pybind11::init<>()) + .def_readwrite("n", &OverlayProblem::Mesh::n) + .def_readwrite("to", &OverlayProblem::Mesh::to) + .def_readwrite("f", &OverlayProblem::Mesh::f) + .def_readwrite("h", &OverlayProblem::Mesh::h) + .def_readwrite("out", &OverlayProblem::Mesh::out) + .def_readwrite("opp", &OverlayProblem::Mesh::opp) + .def_readwrite("R", &OverlayProblem::Mesh::R) + .def_readwrite("type", &OverlayProblem::Mesh::type) + .def_readwrite("Th_hat", &OverlayProblem::Mesh::Th_hat) + .def_readwrite("l", &OverlayProblem::Mesh::l) + .def_readwrite("v_rep", &OverlayProblem::Mesh::v_rep) + .def_readwrite("fixed_dof", &OverlayProblem::Mesh::fixed_dof) + .def("n_vertices", &OverlayProblem::Mesh::n_vertices) + .def("n_edges", &OverlayProblem::Mesh::n_edges) + .def("n_faces", &OverlayProblem::Mesh::n_faces); + + pybind11::class_>(m, "OverlayMesh") + .def_readwrite("n", &OverlayProblem::OverlayMesh::n) + .def_readwrite("to", &OverlayProblem::OverlayMesh::to) + .def_readwrite("f", &OverlayProblem::OverlayMesh::f) + .def_readwrite("h", &OverlayProblem::OverlayMesh::h) + .def_readwrite("out", &OverlayProblem::OverlayMesh::out) + .def_readwrite("opp", &OverlayProblem::OverlayMesh::opp) + .def_readwrite("R", &OverlayProblem::OverlayMesh::R) + .def_readwrite("type", &OverlayProblem::OverlayMesh::type) + .def_readwrite("prev", &OverlayProblem::OverlayMesh::prev) + .def_readwrite("first_segment", &OverlayProblem::OverlayMesh::first_segment) + .def_readwrite("origin", &OverlayProblem::OverlayMesh::origin) + .def_readwrite("origin_of_origin", &OverlayProblem::OverlayMesh::origin_of_origin) + .def_readwrite("vertex_type", &OverlayProblem::OverlayMesh::vertex_type) + .def_readwrite("edge_type", &OverlayProblem::OverlayMesh::edge_type) + .def_readwrite("seg_bcs", &OverlayProblem::OverlayMesh::seg_bcs) + .def_readwrite("_m", &OverlayProblem::OverlayMesh::_m); + + pybind11:: + class_, Mesh>( + m, + "DifferentiableConeMetric") + .def("get_metric_coordinates", &DifferentiableConeMetric::get_metric_coordinates) + //.def( + // "get_reduced_metric_coordinates", + // &DifferentiableConeMetric::get_reduced_metric_coordinates) + .def( + "get_corner_angles", + static_cast< + std::tuple< + VectorX, + VectorX + > (DifferentiableConeMetric::*)() const>(&DifferentiableConeMetric::get_corner_angles)) + .def("max_constraint_error", &DifferentiableConeMetric::max_constraint_error) + .def("constraint", + static_cast(&DifferentiableConeMetric::constraint)); + + pybind11::class_(m, "DiscreteMetric") + .def(pybind11::init&, const VectorX&>()); + + pybind11::class_(m, "PennerConeMetric") + .def(pybind11::init< + const Mesh&, + const VectorX& + >()) + .def("flip_ccw", &PennerConeMetric::flip_ccw) + .def("clone_cone_metric", &PennerConeMetric::clone_cone_metric) + .def("set_metric_coordinates", &PennerConeMetric::set_metric_coordinates) + .def("project_to_constraint", &PennerConeMetric::project_to_constraint) + .def("make_discrete_metric", &PennerConeMetric::make_discrete_metric) + .def("undo_flips", &PennerConeMetric::undo_flips) + .def("change_metric", &PennerConeMetric::change_metric); + + m.def("generate_cone_metric", &generate_cone_metric, default_call_guard); + m.def("parametrize_metric", ¶metrize_metric, default_call_guard); + m.def("view_parameterization", &view_parameterization, default_call_guard); +} + +void init_field_pybind(pybind11::module& m) +{ + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + m.def("view_vector_field", &view_vector_field, default_call_guard); + m.def("load_frame_field", &load_frame_field, default_call_guard); + m.def("write_frame_field", &write_frame_field, default_call_guard); + m.def("generate_frame_field", &generate_frame_field, default_call_guard); + m.def("compute_frame_field_cones", &compute_frame_field_cones, default_call_guard); + m.def("refine_frame_field", &refine_frame_field, default_call_guard); +} + // wrap as Python module PYBIND11_MODULE(penner, m) { m.doc() = "pybindings for penner methods"; + init_metric_pybind(m); + init_field_pybind(m); init_optimization_pybind(m); init_holonomy_pybind(m); init_feature_pybind(m); From 869a2cb54f2b770fe8ceb398e124bd46a47bbd87 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 10 Aug 2026 19:57:40 -0400 Subject: [PATCH 11/17] Bug fixes --- include/feature/feature/core/io.h | 3 +- .../parametrization/parametrization/layout.h | 26 +++++++++++ include/util/util/io.h | 27 ++++++++++++ src/feature/core/io.cpp | 6 +-- src/holonomy/CMakeLists.txt | 1 + src/parametrization/layout.cpp | 25 +---------- src/util/io.cpp | 43 +++++++++++++++++++ 7 files changed, 103 insertions(+), 28 deletions(-) diff --git a/include/feature/feature/core/io.h b/include/feature/feature/core/io.h index 1195628..bd53109 100644 --- a/include/feature/feature/core/io.h +++ b/include/feature/feature/core/io.h @@ -57,9 +57,10 @@ void write_mesh_edges( * @brief Load edges from an obj file * * @param fe_filename: filepath to obj mesh + * @param edge_label: (optional) label for edges in obj file (default is 'l') * @return list of edges */ -std::vector load_mesh_edges(const std::string& fe_filename); +std::vector load_mesh_edges(const std::string& fe_filename, std::string edge_label="l"); /** * @brief Write parameterization seams (that are not feature edges) to file diff --git a/include/parametrization/parametrization/layout.h b/include/parametrization/parametrization/layout.h index eaeb381..d199291 100644 --- a/include/parametrization/parametrization/layout.h +++ b/include/parametrization/parametrization/layout.h @@ -131,6 +131,32 @@ std::tuple, std::vector, std::vector> compute_ const std::vector& u, std::vector& is_cut_h, int start_h = -1); + +template +Eigen::Matrix perp_l(Eigen::Matrix a) { + Eigen::Matrix b; + b[0] = -a[1]; + b[1] = a[0]; + return b; +}; + +template +Scalar area_from_len_l(Scalar l1, Scalar l2, Scalar l3) { + auto s = 0.5 * (l1 + l2 + l3); + return sqrt(s * (s - l1) * (s - l2) * (s - l3)); +} + +template +Eigen::Matrix compute_layout_vertex( + const Eigen::Matrix& p1, + const Eigen::Matrix& p2, + Scalar l0, + Scalar l1, + Scalar l2) +{ + return p1 + (p2 - p1) * (1 + square(l2 / l0) - square(l1 / l0)) / 2 + + perp_l(p2 - p1) * 2 * area_from_len_l(1.0, l1 / l0, l2 / l0); +} #ifdef PYBIND #endif diff --git a/include/util/util/io.h b/include/util/util/io.h index 8f624ac..ef1c8d0 100644 --- a/include/util/util/io.h +++ b/include/util/util/io.h @@ -207,6 +207,33 @@ Eigen::MatrixXd read_hdf5_vector_field(const std::string& path, const std::string& name); Eigen::MatrixXi read_hdf5_integer_matrix(const std::string& path, const std::string& name); +/** + * @brief Read mesh geometry from file. Supported formats are obj and h5. + * + * @param mesh_filename: mesh file path (extension obj or h5) + * @param V: mesh vertices + * @param F: mesh faces + */ +void read_mesh( + const std::string& mesh_filename, + Eigen::MatrixXd& V, + Eigen::MatrixXi& F); + +/** + * @brief Read mesh geometry with uv coordinates from file. Supported formats are obj and h5. + * + * @param mesh_filename: mesh file path (extension obj or h5) + * @param V: mesh vertices + * @param F: mesh faces + * @param uv: mesh uv vertices + * @param FT: mesh uv faces + */ +void read_mesh_with_uv( + const std::string& mesh_filename, + Eigen::MatrixXd& V, + Eigen::MatrixXi& F, + Eigen::MatrixXd& uv, + Eigen::MatrixXi& FT); /// Write a sparse matrix to file in i,j,v format. diff --git a/src/feature/core/io.cpp b/src/feature/core/io.cpp index e441169..91d8948 100644 --- a/src/feature/core/io.cpp +++ b/src/feature/core/io.cpp @@ -47,7 +47,7 @@ std::vector load_feature_edges(const std::string& fe_filename) { return feature_edges; } -std::vector load_mesh_edges(const std::string& fe_filename) { +std::vector load_mesh_edges(const std::string& fe_filename, std::string edge_label) { // try to open mesh std::ifstream inf(fe_filename); if (!inf) { @@ -60,12 +60,12 @@ std::vector load_mesh_edges(const std::string& fe_filename) { std::string line{}; std::getline(inf, line); while (std::getline(inf, line)) { - char label; + std::string label; std::istringstream iss(line); // make sure edge line iss >> label; - if (label != 'l') + if (label != edge_label) continue; // get edge vertices diff --git a/src/holonomy/CMakeLists.txt b/src/holonomy/CMakeLists.txt index 74da394..09dd327 100644 --- a/src/holonomy/CMakeLists.txt +++ b/src/holonomy/CMakeLists.txt @@ -38,6 +38,7 @@ target_include_directories(PennerHolonomyLib PUBLIC target_link_libraries(PennerHolonomyLib PUBLIC PennerCoreLib PennerOptimizationLib + PennerFieldLib ${VISUALIZATION_LIBS} ) target_link_libraries(PennerHolonomyLib PRIVATE diff --git a/src/parametrization/layout.cpp b/src/parametrization/layout.cpp index c711406..2adb549 100644 --- a/src/parametrization/layout.cpp +++ b/src/parametrization/layout.cpp @@ -146,29 +146,6 @@ void trim_topology(const Mesh& m, const std::vector& is_cone, std: } } -Eigen::Matrix perp_l(Eigen::Matrix a) { - Eigen::Matrix b; - b[0] = -a[1]; - b[1] = a[0]; - return b; -}; - -Scalar area_from_len_l(Scalar l1, Scalar l2, Scalar l3) { - auto s = 0.5 * (l1 + l2 + l3); - return sqrt(s * (s - l1) * (s - l2) * (s - l3)); -} - -Eigen::Matrix compute_layout_vertex( - const Eigen::Matrix& p1, - const Eigen::Matrix& p2, - Scalar l0, - Scalar l1, - Scalar l2) -{ - return p1 + (p2 - p1) * (1 + square(l2 / l0) - square(l1 / l0)) / 2 + - perp_l(p2 - p1) * 2 * area_from_len_l(1.0, l1 / l0, l2 / l0); -} - // FIXME Remove once fix halfedge origin template std::tuple, std::vector, std::vector> compute_layout_components( @@ -256,7 +233,7 @@ std::tuple, std::vector, std::vector> compute_ Scalar l0 = Scalar(1.0); Scalar l1 = exp((phi[hn] - phi[hp]) / 2) * (m.l[hn] / m.l[h]); Scalar l2 = exp((phi[hn] - phi[h]) / 2) * (m.l[hp] / m.l[h]); - Eigen::Matrix pn = compute_layout_vertex(p1, p2, l0, l1, l2); + Eigen::Matrix pn = compute_layout_vertex(p1, p2, l0, l1, l2); #ifdef CHECK_VALIDITY if (!float_equal((p1 - p2).norm(), m.l[h])) spdlog::error("inconsistent lengths {}, {}", (p1 - p2).norm(), m.l[h]); if (!float_equal((pn - p2).norm(), m.l[hn])) spdlog::error("inconsistent lengths {}, {}", (pn - p2).norm(), m.l[hn]); diff --git a/src/util/io.cpp b/src/util/io.cpp index 495ff31..15d8baf 100644 --- a/src/util/io.cpp +++ b/src/util/io.cpp @@ -11,6 +11,8 @@ #include "spdlog/sinks/basic_file_sink.h" #include "spdlog/sinks/ostream_sink.h" #include + +#include #include #include "util/vector.h" @@ -393,6 +395,47 @@ void read_hdf5_mesh_with_uv(const std::string& path, FT = FT_row_major; } + +void read_mesh( + const std::string& mesh_filename, + Eigen::MatrixXd& V, + Eigen::MatrixXi& F) +{ + std::filesystem::path mesh_file = mesh_filename; + std::string file_format = mesh_file.extension(); + if (file_format == ".obj") + { + Eigen::MatrixXd uv, N; + Eigen::MatrixXi FT, FN; + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + } + else if (file_format == ".h5") + { + read_hdf5_mesh(mesh_filename, V, F); + } +} + +void read_mesh_with_uv( + const std::string& mesh_filename, + Eigen::MatrixXd& V, + Eigen::MatrixXi& F, + Eigen::MatrixXd& uv, + Eigen::MatrixXi& FT) +{ + std::filesystem::path mesh_file = mesh_filename; + std::string file_format = mesh_file.extension(); + if (file_format == ".obj") + { + Eigen::MatrixXd N; + Eigen::MatrixXi FN; + igl::readOBJ(mesh_filename, V, uv, N, F, FT, FN); + } + else if (file_format == ".h5") + { + read_hdf5_mesh_with_uv(mesh_filename, V, F, uv, FT); + } +} + void write_sparse_matrix(const MatrixX& matrix, const std::string& filename, std::string format) { /// Open file From dfc40b9d075b31270bf0f313ca8a0af5e1cc699f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 10 Aug 2026 21:24:04 -0400 Subject: [PATCH 12/17] Fixed missing dependencies --- app/generate_aligned_field.cpp | 2 ++ app/parameterize_aligned.cpp | 4 ++++ include/holonomy/holonomy/core/dual_loop.h | 4 +++- src/holonomy/core/dual_loop.cpp | 3 ++- src/metric/shear.cpp | 4 +++- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/generate_aligned_field.cpp b/app/generate_aligned_field.cpp index fb39a78..fae53bd 100644 --- a/app/generate_aligned_field.cpp +++ b/app/generate_aligned_field.cpp @@ -12,8 +12,10 @@ #include #include +#if ENABLE_VISUALIZATION #include "polyscope/point_cloud.h" #include "polyscope/surface_mesh.h" +#endif using namespace Penner; using namespace Penner::Field; diff --git a/app/parameterize_aligned.cpp b/app/parameterize_aligned.cpp index d723e57..36810d9 100644 --- a/app/parameterize_aligned.cpp +++ b/app/parameterize_aligned.cpp @@ -16,7 +16,9 @@ // post process uv optimization #include "optimization/metric_optimization/uv_optimization.h" +#if ENABLE_VISUALIZATION #include "polyscope/surface_mesh.h" +#endif using namespace Penner; using namespace Penner::Field; @@ -127,6 +129,7 @@ Eigen::MatrixXi tag_cone_corners( bool show_uv_cones = false; if (show_uv_cones) { +#if ENABLE_VISUALIZATION polyscope::init(); // closed mesh @@ -168,6 +171,7 @@ Eigen::MatrixXi tag_cone_corners( ->setEnabled(true); polyscope::show(); +#endif } return is_cone; diff --git a/include/holonomy/holonomy/core/dual_loop.h b/include/holonomy/holonomy/core/dual_loop.h index 83c12e3..3e31504 100644 --- a/include/holonomy/holonomy/core/dual_loop.h +++ b/include/holonomy/holonomy/core/dual_loop.h @@ -10,6 +10,8 @@ #include "holonomy/core/dual_segment.h" +#include + /** * @brief Methods to manage dual loops on a surface. Includes classes to represent dual loops on a surface * and data structures to quickly query intersections of the loops with edges of the underlying mesh. @@ -644,4 +646,4 @@ class DualLoopManager{ }; } // namespace Holonomy -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/holonomy/core/dual_loop.cpp b/src/holonomy/core/dual_loop.cpp index e9f1df3..9020152 100644 --- a/src/holonomy/core/dual_loop.cpp +++ b/src/holonomy/core/dual_loop.cpp @@ -8,6 +8,7 @@ #include "holonomy/core/dual_loop.h" + namespace Penner { namespace Holonomy { @@ -682,4 +683,4 @@ void DualLoopConnectivity::delete_segment_index(int segment_index) } // namespace Holonomy -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/metric/shear.cpp b/src/metric/shear.cpp index a2d634f..15cb83c 100644 --- a/src/metric/shear.cpp +++ b/src/metric/shear.cpp @@ -14,6 +14,8 @@ #include "metric/barycentric.h" #include "util/vector.h" +#include + /// FIXME Do cleaning pass namespace Penner { @@ -403,4 +405,4 @@ void compute_shear_basis_coordinates( scale_factors)); } -} // namespace Penner \ No newline at end of file +} // namespace Penner From 146ce76ee8495556e89a82eaf075b6384514e9c1 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Tue, 25 Aug 2026 13:24:41 -0400 Subject: [PATCH 13/17] Added options to skip flips --- src/feature/interface.cpp | 10 ++++++--- src/feature/surgery/cut_mesh_layout.cpp | 5 +++-- src/holonomy/holonomy/newton.cpp | 28 ++++++++++++++++++++++++- src/metric/cone_metric.cpp | 16 +++++++++++++- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/feature/interface.cpp b/src/feature/interface.cpp index 5433521..9b72729 100644 --- a/src/feature/interface.cpp +++ b/src/feature/interface.cpp @@ -485,8 +485,12 @@ AlignedMetricGenerator::AlignedMetricGenerator( // build an initial metric with Penner coordinates dirichlet_metric = embedding_metric; - VectorX metric_coords = generate_penner_coordinates(embedding_metric); - dirichlet_metric.change_metric(embedding_metric, metric_coords, true, false); + VectorX metric_coords = dirichlet_metric.get_metric_coordinates(); + if (!marked_metric_params.use_log_length) + { + metric_coords = generate_penner_coordinates(embedding_metric); + dirichlet_metric.change_metric(embedding_metric, metric_coords, true, false); + } // build the relaxed constraints std::vector> relaxed_edges = compute_relaxed_edges(relaxed_corners, dirichlet_metric, vtx_reindex, V_map, F_cut); @@ -750,4 +754,4 @@ Eigen::MatrixXd generate_feature_aligned_metric( } // namespace Feature -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/feature/surgery/cut_mesh_layout.cpp b/src/feature/surgery/cut_mesh_layout.cpp index 219b498..cae8339 100644 --- a/src/feature/surgery/cut_mesh_layout.cpp +++ b/src/feature/surgery/cut_mesh_layout.cpp @@ -75,9 +75,10 @@ parameterize_cut_mesh( // get initial and final metric coordinates VectorX metric_init = generate_penner_coordinates(embedding_component); + //VectorX metric_init(mesh_component.n_halfedges()); VectorX metric_coords(mesh_component.n_halfedges()); for (int i = 0; i < mesh_component.n_halfedges(); ++i) { - // metric_init[i] = 2. * log(init_component.l[i]); + //metric_init[i] = 2. * log(init_component.l[i]); metric_coords[i] = 2. * log(mesh_component.l[i]); } PennerConeMetric component_init(mesh_component, metric_init); @@ -520,4 +521,4 @@ generate_connected_parameterization( #endif } // namespace Feature -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/holonomy/holonomy/newton.cpp b/src/holonomy/holonomy/newton.cpp index 49318c5..829313a 100644 --- a/src/holonomy/holonomy/newton.cpp +++ b/src/holonomy/holonomy/newton.cpp @@ -15,6 +15,9 @@ #include "metric/projection.h" #include "metric/shear.h" +// check triangle inequality +#include "metric/constraint.h" + // root mean square error computations #include "optimization/metric_optimization/energies.h" @@ -453,6 +456,22 @@ void OptimizeNewton::perform_line_search( marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); update_constraint(marked_metric); + // make sure initial line step satisfies triangle quality + bool check_triangle_inequality = false; + if (check_triangle_inequality) + { + while (!satisfies_triangle_inequality(marked_metric)) + { + lambda /= 2; + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); + update_constraint(marked_metric); + spdlog::debug("Reducing lambda to {} to satisfy triangle inequality", lambda); + continue; + } + } + + // Line search until the constraint norm decreases and the projected constraint is // nonpositive We also allow the norm bound to be dropped or made approximate with some // relative term alpha @@ -478,6 +497,13 @@ void OptimizeNewton::perform_line_search( marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); update_constraint(marked_metric); + // check triangle inequality + if (!satisfies_triangle_inequality(marked_metric)) + { + spdlog::debug("reducing step due to triangle inequality issue"); + continue; + } + // Update squared constraint norm and projected constraint l2_c_sq = constraint.squaredNorm(); proj_cons = constraint.dot(J * descent_direction); @@ -812,4 +838,4 @@ void view_optimization_state( } } // namespace Holonomy -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/metric/cone_metric.cpp b/src/metric/cone_metric.cpp index 1ec1b69..79890e9 100644 --- a/src/metric/cone_metric.cpp +++ b/src/metric/cone_metric.cpp @@ -311,6 +311,20 @@ std::unique_ptr PennerConeMetric::project_to_constrain void PennerConeMetric::make_discrete_metric() { + bool disable_flips = false; + if (disable_flips) + { + if (!satisfies_triangle_inequality(*this)) + { + spdlog::error("triangle inequality not satisfied"); + return; + } + else { + m_is_discrete_metric = true; + return; + } + } + // Make the copied mesh Delaunay with Ptolemy flips VectorX u; u.setZero(n_ind_vertices()); @@ -485,4 +499,4 @@ void DiscreteMetric::expand_metric_coordinates(const VectorX& metric_coords) } -} // namespace Penner \ No newline at end of file +} // namespace Penner From d6d593a6f0813fbfbbdeef91e78d251dcaa5e5cf Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 26 Oct 2026 20:22:20 +0100 Subject: [PATCH 14/17] Fixed symmetric dirichlet interface --- src/field/intrinsic_field.cpp | 1 + src/holonomy/core/viewer.cpp | 15 ++++++++++++++- src/optimization/metric_optimization/energies.cpp | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/field/intrinsic_field.cpp b/src/field/intrinsic_field.cpp index d758372..28fa9fc 100644 --- a/src/field/intrinsic_field.cpp +++ b/src/field/intrinsic_field.cpp @@ -1282,6 +1282,7 @@ void IntrinsicNRosyField::solve(const Mesh& m) #if USE_COMISO if (use_roundings) { + spdlog::info("Enabling rounding to avoid small cones"); Rounder rounder(m, var2he, halfedge_var_id, base_cones, min_cones); ConeMISolver cs; cs.solve_cone_rounding(Acsc, x, gmm_b, var_edges, rounder); diff --git a/src/holonomy/core/viewer.cpp b/src/holonomy/core/viewer.cpp index 371c1e0..1e27f7f 100644 --- a/src/holonomy/core/viewer.cpp +++ b/src/holonomy/core/viewer.cpp @@ -23,6 +23,9 @@ // used to visualize constraint error #include "holonomy/holonomy/constraint.h" +// used to visualize per face parametrization distortion +#include "optimization/metric_optimization/energies.h" + // cross field generation for viewer #include "field/frame_field.h" #include "field/vector_field.h" @@ -507,11 +510,14 @@ void view_seamless_parameterization( igl::doublearea(uv, FT, uv_area); VectorX corner_angles = compute_corner_angles(V_cut, FT); VectorX uv_corner_angles = compute_corner_angles(uv, FT); + VectorX symmetric_dirichlet; + Optimization::symmetric_dirichlet_energy(V_cut, FT, uv, FT, symmetric_dirichlet); spdlog::info("Max uv length error: {}", uv_length_error.maxCoeff()); spdlog::info("Max uv angle error: {}", uv_angle_error.maxCoeff()); spdlog::info("Min embedding area: {}", area.minCoeff()); spdlog::info("Min uv area: {}", uv_area.minCoeff()); spdlog::info("{} flipped faces", num_flipped); + spdlog::info("Max symmetric dirichlet energy: {}", symmetric_dirichlet.maxCoeff()); // Generate cones VectorX cone_angles = compute_cone_angles(V, F, uv, FT); @@ -546,6 +552,13 @@ void view_seamless_parameterization( ->addFaceScalarQuantity( "uv area", convert_scalar_to_double_vector(uv_area)); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceScalarQuantity( + "symmetric dirichlet energy", + convert_scalar_to_double_vector(symmetric_dirichlet)) + ->setColorMap("coolwarm") + ->setMapRange({4.0, symmetric_dirichlet.maxCoeff()}) + ->setEnabled(true); polyscope::getSurfaceMesh(mesh_handle) ->addHalfedgeScalarQuantity( "uv length error", @@ -653,4 +666,4 @@ void view_homology_basis( } // namespace Holonomy -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/optimization/metric_optimization/energies.cpp b/src/optimization/metric_optimization/energies.cpp index 744e439..db7c4c5 100644 --- a/src/optimization/metric_optimization/energies.cpp +++ b/src/optimization/metric_optimization/energies.cpp @@ -455,7 +455,7 @@ void second_invariant_vf( } } -void symmetric_dirichlet_energy_vf( +void symmetric_dirichlet_energy( const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, const Eigen::MatrixXd& uv, From 439b517bb7a99555145dbae2d3b1e64cd0253538 Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Mon, 26 Oct 2026 22:22:52 +0100 Subject: [PATCH 15/17] Added resolution module --- CMakeLists.txt | 38 + include/resolution/resolution/core/common.h | 134 + .../resolution/core/geodesic_flipper.h | 423 ++ .../resolution/resolution/core/geodesics.h | 312 ++ include/resolution/resolution/core/layout.h | 132 + .../resolution/resolution/core/quad_quality.h | 53 + .../resolution/core/triangulation.h | 104 + include/resolution/resolution/core/viewer.h | 37 + .../experimental/dynamic_markings.h | 100 + .../experimental/holonomy_coordinates.h | 35 + .../resolution/experimental/regularization.h | 307 ++ .../resolution/experimental/representatives.h | 11 + .../resolution/experimental/simplification.h | 246 ++ .../experimental/simplification_overlay.h | 46 + .../resolution/hierarchy/halfedge.h | 482 +++ .../resolution/resolution/hierarchy/overlay.h | 964 +++++ .../resolution/hierarchy/simplifier.h | 257 ++ .../resolution/resolution/hierarchy/voronoi.h | 707 ++++ include/resolution/resolution/interface.h | 12 + .../resolution/optimization/compression.h | 69 + .../resolution/optimization/field.h | 137 + .../resolution/optimization/improvement.h | 306 ++ include/resolution/resolution/pybind.h | 20 + src/CMakeLists.txt | 1 + src/resolution/CMakeLists.txt | 94 + src/resolution/core/common.cpp | 195 + src/resolution/core/geodesic_flipper.cpp | 981 +++++ src/resolution/core/geodesics.cpp | 1263 ++++++ src/resolution/core/layout.cpp | 178 + src/resolution/core/quad_quality.cpp | 185 + src/resolution/core/triangulation.cpp | 298 ++ src/resolution/core/viewer.cpp | 138 + .../experimental/dynamic_markings.cpp | 0 .../experimental/holonomy_coordinates.cpp | 123 + .../experimental/regularization.cpp | 0 .../experimental/representatives.cpp | 133 + .../experimental/simplification.cpp | 1383 +++++++ .../experimental/simplification_overlay.cpp | 379 ++ src/resolution/hierarchy/halfedge.cpp | 1265 ++++++ src/resolution/hierarchy/overlay.cpp | 3468 +++++++++++++++++ src/resolution/hierarchy/simplifier.cpp | 762 ++++ src/resolution/hierarchy/voronoi.cpp | 2859 ++++++++++++++ src/resolution/interface.cpp | 9 + src/resolution/optimization/compression.cpp | 767 ++++ src/resolution/optimization/field.cpp | 805 ++++ src/resolution/optimization/improvement.cpp | 2190 +++++++++++ src/resolution/pybind.cpp | 31 + 47 files changed, 22439 insertions(+) create mode 100644 include/resolution/resolution/core/common.h create mode 100644 include/resolution/resolution/core/geodesic_flipper.h create mode 100644 include/resolution/resolution/core/geodesics.h create mode 100644 include/resolution/resolution/core/layout.h create mode 100644 include/resolution/resolution/core/quad_quality.h create mode 100644 include/resolution/resolution/core/triangulation.h create mode 100644 include/resolution/resolution/core/viewer.h create mode 100644 include/resolution/resolution/experimental/dynamic_markings.h create mode 100644 include/resolution/resolution/experimental/holonomy_coordinates.h create mode 100644 include/resolution/resolution/experimental/regularization.h create mode 100644 include/resolution/resolution/experimental/representatives.h create mode 100644 include/resolution/resolution/experimental/simplification.h create mode 100644 include/resolution/resolution/experimental/simplification_overlay.h create mode 100644 include/resolution/resolution/hierarchy/halfedge.h create mode 100644 include/resolution/resolution/hierarchy/overlay.h create mode 100644 include/resolution/resolution/hierarchy/simplifier.h create mode 100644 include/resolution/resolution/hierarchy/voronoi.h create mode 100644 include/resolution/resolution/interface.h create mode 100644 include/resolution/resolution/optimization/compression.h create mode 100644 include/resolution/resolution/optimization/field.h create mode 100644 include/resolution/resolution/optimization/improvement.h create mode 100644 include/resolution/resolution/pybind.h create mode 100644 src/resolution/CMakeLists.txt create mode 100644 src/resolution/core/common.cpp create mode 100644 src/resolution/core/geodesic_flipper.cpp create mode 100644 src/resolution/core/geodesics.cpp create mode 100644 src/resolution/core/layout.cpp create mode 100644 src/resolution/core/quad_quality.cpp create mode 100644 src/resolution/core/triangulation.cpp create mode 100644 src/resolution/core/viewer.cpp create mode 100644 src/resolution/experimental/dynamic_markings.cpp create mode 100644 src/resolution/experimental/holonomy_coordinates.cpp create mode 100644 src/resolution/experimental/regularization.cpp create mode 100644 src/resolution/experimental/representatives.cpp create mode 100644 src/resolution/experimental/simplification.cpp create mode 100644 src/resolution/experimental/simplification_overlay.cpp create mode 100644 src/resolution/hierarchy/halfedge.cpp create mode 100644 src/resolution/hierarchy/overlay.cpp create mode 100644 src/resolution/hierarchy/simplifier.cpp create mode 100644 src/resolution/hierarchy/voronoi.cpp create mode 100644 src/resolution/interface.cpp create mode 100644 src/resolution/optimization/compression.cpp create mode 100644 src/resolution/optimization/field.cpp create mode 100644 src/resolution/optimization/improvement.cpp create mode 100644 src/resolution/pybind.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 123a998..b14c8a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,36 @@ if (USE_UV_OPTIMIZATION) set ( SUITESPARSE_ENABLE_PROJECTS "${SUITESPARSE_ENABLE_PROJECTS};spqr;umfpack" CACHE STRING "" FORCE) endif() +if(USE_OMP) + if(APPLE) + # Apple/macOS: use homebrew libomp + set(OpenMP_C_FLAGS "-Xpreprocessor -fopenmp") + set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY "omp") + include_directories("/opt/homebrew/opt/libomp/include") + link_directories("/opt/homebrew/opt/libomp/lib") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew/opt/libomp") + else() + # Linux: use GNU OpenMP (libgomp) + set(OpenMP_C_FLAGS "-fopenmp") + set(OpenMP_CXX_FLAGS "-fopenmp") + set(OpenMP_C_LIB_NAMES "gomp") + set(OpenMP_CXX_LIB_NAMES "gomp") + # Explicitly set the libgomp library path for Linux + find_library(LIBGOMP NAMES gomp libgomp.so.1 PATHS /usr/lib/x86_64-linux-gnu /usr/lib /usr/local/lib REQUIRED) + set(OpenMP_gomp_LIBRARY ${LIBGOMP}) + endif() + + find_package(OpenMP REQUIRED) + find_package(BLAS REQUIRED) + set (OMP_LIBS + OpenMP::OpenMP_CXX + ) + add_compile_definitions(USE_OMP) +endif() + include(eigen) include(boost) @@ -182,6 +212,14 @@ if(USE_PYBIND) ) endif() +if(USE_JXL) + add_compile_definitions(USE_JXL) +endif() + +if(USE_EXR) + add_compile_definitions(USE_EXR) +endif() + # Install executables to bin directory set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) diff --git a/include/resolution/resolution/core/common.h b/include/resolution/resolution/core/common.h new file mode 100644 index 0000000..39a36ff --- /dev/null +++ b/include/resolution/resolution/core/common.h @@ -0,0 +1,134 @@ +// This file is part of penner-optimization, a constrained parametrization library. +// +// Copyright (C) 2026 Ryan Capouellez +// +// This Source Code Form is subject to the terms of the Mozilla Public License +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at http://mozilla.org/MPL/2.0/. + +#pragma once + +#include "holonomy/interface.h" +#include "holonomy/core/common.h" +#include "holonomy/core/dual_loop.h" +#include "holonomy/holonomy/marked_penner_cone_metric.h" +#include "holonomy/holonomy/newton.h" + +#include "field/intrinsic_field.h" + +namespace Penner { +namespace Resolution { + +using Holonomy::DualLoop; +using Holonomy::NewtonParameters; +using Holonomy::MarkedPennerConeMetric; +using Holonomy::MarkedMetricParameters; + +typedef Eigen::Matrix GeometryData; +typedef Eigen::Matrix ImageData; + +// TODO: migrate these functions to lower level modules + + +/** + * @brief Generate the map from VF vertex edge pairs to halfedges in m. + * + * If m is a double cover mesh, this method only considers primal edges. + * + * WARNING: Uses 1-indexing for halfedges instead of 0-indexing. + * + * @param m: halfedge mesh + * @param vtx_reindex: map from vertices in the halfedge to VF mesh + * @return sparse matrix mapping vertex pairs to halfedges + */ +Eigen::SparseMatrix generate_VV_to_halfedge_map( + const Mesh& m, + const std::vector& vtx_reindex); + +/** + * @brief Generates a halfedge function on a cover mesh from a function on the opposite corners + * of a VF mesh. + * + * @tparam function type + * @param m: halfedge mesh + * @param vtx_reindex: map from vertices in the halfedge to VF mesh + * @param F: VF mesh faces + * @param corner_func: function on the VF mesh corners + * @return function on the halfedges + */ +template +Eigen::Matrix transfer_corner_function_to_halfedge( + const Mesh& m, + const std::vector& vtx_reindex, + const Eigen::MatrixXi& F, + const Eigen::Matrix& corner_func); + + +/** + * @brief Compute measures of seamless metric failure in halfedge uv coordinates. + * + * @param m: halfedge mesh + * @param uv: coordinates of uv coordinates at the tip of each halfedge + * @return per-halfedge uv edge length deviation for paired halfeges + * @return per-halfedge uv edge rotation deviation from a multiple of pi/2 for paired halfeges + */ +std::tuple compute_seamless_error( + const Mesh &m, + const Eigen::MatrixXd &uv); + +/** + * @brief Print an error message and return false + * + * @param message: message to print to log + * @return false + */ +inline bool errmess(const std::string& message) +{ + spdlog::error(message); + assert(false); + return false; +} + +/** + * @brief Get the next segment in an overlay mesh, ignoring current only segments + * + * @param m_o: overlay mesh + * @param _h: segment in overlay mesh + * @return next original segment in the overlay mesh + */ +int next_orig_segment(const OverlayMesh &m_o, int _h); + +/** + * @brief Remove unreferenced vertices from a VF mesh. + * + * @param V: mesh vertices + * @param F: mesh faces + */ +void remove_unreferenced_inplace(Eigen::MatrixXd& V, Eigen::MatrixXi& F); + +Eigen::Vector3d project_orthogonal_to( + const Eigen::Vector3d& v, + const Eigen::Vector3d& u); + +/** + * @brief Compute the area of a triangle with the given vertices + * + * @param v1: first vertex + * @param v2: second vertex + * @param v3: third vertex + * @return area of triangle spanned by the vertices + */ +double compute_triangle_area( + const Eigen::Vector3d& v1, + const Eigen::Vector3d& v2, + const Eigen::Vector3d& v3); + +/** + * @brief Generate rotation for a pi/2 ccw rotation. + * + * @return rotation matrix + */ +Eigen::Matrix2d generate_ccw_rotation(); + +} // namespace Resolution +} // namespace Penner diff --git a/include/resolution/resolution/core/geodesic_flipper.h b/include/resolution/resolution/core/geodesic_flipper.h new file mode 100644 index 0000000..dc25541 --- /dev/null +++ b/include/resolution/resolution/core/geodesic_flipper.h @@ -0,0 +1,423 @@ +#pragma once +#include "resolution/core/common.h" +#include "resolution/hierarchy/voronoi.h" +#include "resolution/core/geodesics.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" + +namespace Penner +{ +namespace Resolution +{ + +/** + * @brief Representation of a connected piece-wise geodesic path on a halfedge mesh, with indexed wedges + * between adjacent edges. + * + */ +class GeodesicPath +{ +public: + std::vector to; // wedge index at tip of segment + std::vector in; // segment index into wedge + std::vector next; // next segment + std::vector prev; // previous segment + std::vector halfedge; // halfedge on base mesh + std::vector lengths; // history of path lengths + std::vector sizes; // history of path sizes + + /** + * @brief Construct a new geodesic path object from a halfedge path + * + * @param path: continuous sequence of halfedges on a base mesh + */ + GeodesicPath(const std::vector& path); + + /** + * @brief Get the halfedge sequence of the geodesic path + * + * @return sequence of halfedges on the base mesh + */ + std::vector get_path_halfedges() const; + + /** + * @brief Determine if the path is a single edge + * + * @return true iff the path length is one + */ + bool is_edge() { return (start == end); } + + /** + * @brief Get the index of the first segment in the path + * + * @return start index + */ + int get_start() const { return start; } + + /** + * @brief Get the index of the last segment in the path + * + * @return end index + */ + int get_end() const { return end; } + + /** + * @brief Set the start segment of the path + * + * @param index: new path start index + */ + void set_start(int index) { start = index; } + + /** + * @brief Set the end segment of the path + * + * @param index: new path end index + */ + void set_end(int index) { end = index; } + + /** + * @brief Determine if a segment is the start of the path. + * + * @param index: index of segment to check + * @return true iff the index is the start index + */ + bool is_start(int index) { return (index == start); } + + /** + * @brief Determine if a segment is the end of the path. + * + * @param index: index of segment to check + * @return true iff the index is the end index + */ + bool is_end(int index) { return (index == end); } + + /** + * @brief Determine if the wedge is still valid + * + * @param index: wedge index to check + * @return true iff the + */ + bool is_wedge_valid(int index) const { return m_is_wedge_valid[index]; } + + /** + * @brief Create a isolated segment with given halfedge + * + * @param h: halfedge on the base mesh + * @return new segment index + */ + int create_segment(int h); + + /** + * @brief Create a wedge at the tip of a segment with a given index + * + * @param index: index of the segment with new wedge at the tip + * @return index of the new wedge + */ + int create_wedge(int index); + + /** + * @brief Create a segment after the one with the given index and a wedge in between. + * + * @param index: previous segment for new segment + * @param h: halfedge on the base mesh + * @return index of the new segment + */ + int create_next_segment(int index, int h); + + /** + * @brief Mark the wedge as invalid. + * + * @param index: wedge to mark as invalid + */ + void mark_wedge_invalid(int index) { m_is_wedge_valid[index] = false; } + + /** + * @brief Determine if the geodesic path is a valid path on the base mesh + * + * @param m: base mesh + * @return true iff the path is valid and continuous on the base mesh + */ + bool is_valid(Mesh& m); + +private: + int start; + int end; + + std::vector m_is_wedge_valid; +}; + +/** + * @brief Class for flipping a network of edge paths to geodesics. + * + * In some cases, flipping all paths to geodesics is not possible. By default, paths are only + * allowed to intersect at their endpoints. Curves are never allowed to intersect transversely, + * but tangential intersections are possible. + * + * + */ +class GeodesicFlipper +{ +public: + Scalar wedge_eps = 1e-5; + Scalar infinity = 1e10; + int max_flipouts = -1; + bool track_deltas = false; + //Scalar flip_angle = 0.9 * PI; + Scalar flip_angle = PI; + Scalar path_curvature_threshold = 0.; + + /** + * @brief Construct a new Geodesic Flipper object with the given halfedge + * paths as the initial curve network. + * + * @param overlay_mesh: base mesh + * @param paths: halfedge paths forming the initial curve network + * @param allow_intersections: (optional) allow tangential intersections if true + */ + GeodesicFlipper( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths, + bool allow_intersections=false + ); + + /** + * @brief Construct a new Geodesic Flipper object with the given halfedge + * paths as the initial curve network. + * + * @param mesh: base mesh + * @param paths: halfedge paths forming the initial curve network + * @param allow_intersections: (optional) allow tangential intersections if true + */ + GeodesicFlipper( + const Mesh& mesh, + const std::vector>& paths, + bool allow_intersections=false + ); + + /** + * @brief Count the number of faces in the overlay mesh. + * + * @return overlay face count + */ + int count_overlay_faces() const { return overlay_faces; } + + /** + * @brief Count the number of flips done to flip to geodesic. + * + * @return total flip count + */ + int count_flips() const { return flip_sequence.size(); } + + /** + * @brief Basic method to flip the curve network to geodesics. + * + */ + void flip_to_geodesics(); + + /** + * @brief Check if the given wedge in a path is acute and add it to the priority queue if so. + * + * @param path_index: path index + * @param wedge_index: index of the wedge in the path + */ + void check_wedge(int path_index, int wedge_index); + + /** + * @brief Check all wedges. + * + */ + void check_all_wedges(); + + /** + * @brief Check if a wedge can be shorten with flipout, and do so if it can. + * + * @return true iff the wedge is shortened + */ + bool process_wedge(); + + /** + * @brief Flip out acute edges in the wedge between the two halfedges. + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @param is_left_joint: is wedge to left or right of the two edges + * @return number of flips done + */ + int flip_out(int hij, int hjk, bool is_left_joint); + + /** + * @brief Flip an acute angle on the left of the wedge between the two halfedges. + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @return true iff an edge is flipped + */ + bool flip_left_acute_angles(int hij, int hjk); + + /** + * @brief Flip an acute angle on the right of the wedge between the two halfedges. + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @return true iff an edge is flipped + */ + bool flip_right_acute_angles(int hij, int hjk); + + /** + * @brief Flip the given halfedge + * + * @param hij: halfedge to flip + */ + void flip_halfedge(int hij); + + /** + * @brief Shorten a path at a given wedge by replacing the wedge with a shorter path. + * + * WARNING: assumes the wedge is already flipped out + * + * @param path_index: path index + * @param wedge_index: index of the wedge in the path + * @param is_left_joint: is wedge to left or right of the two edges + */ + void shorten_wedge(int path_index, int wedge_index, bool is_left_joint); + + /** + * @brief Undo a given number of flips + * + * @param num_flips: number of flips to do + */ + void undo_flips(int num_flips); + + /** + * @brief Compute the angle of the wedge clockwise between two halfedges + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @return wedges angle + */ + Scalar compute_clockwise_wedge_angle(int hij, int hjk); + + /** + * @brief Compute the angle of the wedge counterclockwise between two halfedges + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @return wedges angle + */ + Scalar compute_ccw_wedge_angle(int hij, int hjk); + + /** + * @brief Compute the curvature of the wedge, defined as the maximum defect from Pi + * + * @param hij: first halfedge of the wedge + * @param hjk: second halfedge of the wedge + * @param use_signed: use signed defect instead (using clockwise wedge angles) + * @return wedge curvature + */ + Scalar compute_wedge_curvature(int hij, int hjk, bool use_signed=false); + + /** + * @brief Compute the total curvature of the path. + * + * @param path_index: path to compute curvature for + * @param use_signed: use signed defect (using clockwise wedge angles) + * @return path curvature + */ + Scalar compute_path_curvature(int path_index, bool use_signed=false); + + /** + * @brief Compute all path curvatures. + * + * @param use_signed: use signed defect (using clockwise wedge angles) + * @return list of curvatures of all paths + */ + std::vector compute_path_curvatures(bool use_signed=false); + + /** + * @brief Generate surface points for all paths on the surface + * + * WARNING: computationally intensive as a reversed overlay mesh needs to be computed + */ + void generate_surface_points(); + + /** + * @brief Get a boolean mask of paths that intersect other paths + * + * @return mask of intersecting paths + */ + std::vector get_intersecting_paths(); + + /** + * @brief Add the curve network to the polyscope viewer. + * + * @param overlay_mesh: base mesh + */ + void add_to_viewer(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief View the curve network. + * + * @param overlay_mesh: base mesh + */ + void view(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Flip the curve network to geodesics while taking screenshots of the curve network. + * + * @param overlay_mesh: base mesh + * @param frame_delta: number of wedge shortens between screenshots + */ + void animate_flip_to_geodesics(VoronoiOverlayMesh& overlay_mesh, int frame_delta); + + // Initialize the stack of vertices to process with all vertices + typedef std::tuple WeightedWedge; + struct WedgeCompare + { + bool operator() (const WeightedWedge& left, const WeightedWedge& right) + { + return std::get<0>(left) > std::get<0>(right); + }; + }; + typedef std::priority_queue, WedgeCompare> PriorityQueue; + + std::vector deltas; + std::vector paths; + std::vector path_length; + std::vector path_curvature; + + std::vector> surface_points; + std::vector>> edge_splits; +private: + //OverlayMesh m_o; + Mesh m_o; + std::vector edge_path_counts; + std::vector>> adjacent_paths; + std::vector h2e, e2h; + bool keep_simple = true; + + // Predicates + bool is_farthest_left(int path_index, int index); + bool is_farthest_right(int path_index, int index); + bool is_left_wedge_empty(int hij, int hjk); + bool is_right_wedge_empty(int hij, int hjk); + bool is_left_wedge_simple(int hij, int hjk); + bool is_right_wedge_simple(int hij, int hjk); + bool is_simple(); + bool is_wedge_simple(int path_index, int wedge_index, bool is_left_joint); + bool is_left_joint_flexible(int path_index, int wedge_index); + bool is_right_joint_flexible(int path_index, int wedge_index); + bool is_joint_flexible(int path_index, int wedge_index, bool is_left_joint); + bool is_vertex_isolated(int vi); + + void add_adjacent_halfedge(int path_index, int index, bool on_left); + void update_face_angles(int face_index); + + bool is_valid(); + + VectorX angles; + std::vector flip_sequence; + PriorityQueue wedges_to_process; + int overlay_faces = 0; +}; + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/core/geodesics.h b/include/resolution/resolution/core/geodesics.h new file mode 100644 index 0000000..4ff67f8 --- /dev/null +++ b/include/resolution/resolution/core/geodesics.h @@ -0,0 +1,312 @@ +#pragma once +#include "resolution/core/common.h" +#include "resolution/hierarchy/voronoi.h" + +#include "geometrycentral/surface/flip_geodesics.h" +#include "geometrycentral/surface/meshio.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" + +/** + * @brief Methods to insert geodesic paths into a halfedge mesh. + * + * TODO: Rename this file and main claiss to clarify that it is inserting geodesic paths. + * + */ + +namespace Penner +{ +namespace Resolution +{ + +/** + * @brief Convert a collection of polyline paths into a flat node-edge representation with component + * labels. + * + * Each path in @p polyline becomes a sequence of directed edges connecting consecutive nodes. + * Nodes from different paths are stored contiguously; the returned label array identifies which + * path each edge belongs to. + * + * @tparam Pos3D position type (e.g. Eigen::Vector3d or a 2D UV coordinate) + * @param polyline: collection of paths, each given as an ordered list of positions + * @return array of node positions + * @return array of edges, each an index pair {src, dst} into the node array + * @return per-edge path label (index into @p polyline) + */ +template +std::tuple, std::vector>, std::vector> +convert_polyline(const std::vector>& polyline) +{ + std::vector nodes; + std::vector> edges; + std::vector labels; + int n_paths = polyline.size(); + for (int i = 0; i < n_paths; ++i) + { + int n_path_nodes = polyline[i].size(); + for (int j = 0; j < n_path_nodes; ++j) + { + int n = nodes.size(); + edges.push_back({n, n + 1}); + nodes.push_back(polyline[i][j]); + labels.push_back(i); + } + nodes.push_back(polyline[i].back()); + } + + return std::make_tuple(nodes, edges, labels); +} + +/** + * @brief A point on a triangle mesh surface, represented in one of three local coordinate frames. + * + * - Vertex: the point coincides with mesh vertex @p index. + * - Edge: the point lies on the edge between vertices @p edge_base and @p edge_tip at parameter + * @p t (i.e. position = (1-t)*base + t*tip). @p index is the edge index. + * - Face: the point lies in the interior of face @p index. @p t and the edge fields are unused. + */ +struct SurfacePoint { + enum class Type { Vertex, Edge, Face }; + Type type; + int index; + int edge_base; + int edge_tip; + Scalar t; +}; + +/** + * @brief Inserts a collection of piecewise-linear surface paths as edges in a halfedge. + * + * Given an overlay mesh and initial path descriptions as SurfacePoints, PathStraightener + * iteratively inserts the paths by: + * 1. Splitting overlay edges at path crossing points. + * 2. Tracing paths between the inserted vertices. + * 3. Separating crossing or overlapping paths (if necessary). + */ +class PathStraightener +{ +public: + using EdgeType = SimplificationOverlayMesh::EdgeType; + using VertexType = SimplificationOverlayMesh::VertexType; + + /** + * @brief Construct a PathStraightener for the given paths + * + * @param overlay_mesh: Voronoi overlay mesh; modified in place during straightening + * @param paths: initial path descriptions as sequences of SurfacePoints + */ + PathStraightener( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths + ); + + /** + * @brief Construct a PathStraightener, marking which paths are known to intersect one another. + * + * Intersecting paths require extra separation steps; non-intersecting paths can be + * inserted independently. + * + * @param overlay_mesh: Voronoi overlay mesh; modified in place during straightening + * @param paths: initial path descriptions as sequences of SurfacePoints + * @param is_intersecting_path: per-path flag; true if the path crosses other paths + */ + PathStraightener( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths, + const std::vector& is_intersecting_path); + + /** + * @brief Split overlay edges at points where paths cross them. + * + * @param overlay_mesh: overlay mesh to modify + */ + void split_edges(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Split overlay edges and record which halfedges were split per path. + * + * @param overlay_mesh: overlay mesh to modify + * @param edges_to_split: output; for each path, the list of {halfedge, halfedge} pairs split + */ + void split_edges( + VoronoiOverlayMesh& overlay_mesh, + std::vector>>& edges_to_split); + + /** + * @brief Create edges between inserted vertices introduced by split_edges + * + * @param overlay_mesh: overlay mesh to modify + */ + void join_edges(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Retrace all redundant paths to ensure they only intersect at endpoints. + * + * @param overlay_mesh: overlay mesh carrying the current metric + */ + void retrace_paths(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Run the full straightening pipeline: split, join, retrace + * + * @param overlay_mesh: overlay mesh to operate on + */ + void run(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Visualize the current state of the paths on the overlay mesh. + * + * @param overlay_mesh: overlay mesh for geometric context + */ + void view(VoronoiOverlayMesh& overlay_mesh); + + /** + * @brief Remove path @p e from the mesh, allowing it to be retraced. + * + * @param overlay_mesh: overlay mesh + * @param e: path index to remove + */ + void remove_path(VoronoiOverlayMesh& overlay_mesh, int e); + + /** + * @brief Retrace path @p e. + * + * @param overlay_mesh: overlay mesh carrying the metric + * @param e: path index to retrace + * @param f: optional starting face hint; -1 uses automatic detection + */ + void retrace_path(VoronoiOverlayMesh& overlay_mesh, int e, int f=-1); + + /** + * @brief Retrace path @p e while enforcing consistent orientation with adjacent paths. + * + * @param overlay_mesh: overlay mesh carrying the metric + * @param e: path index to retrace + */ + void retrace_consistent_path(VoronoiOverlayMesh& overlay_mesh, int e); + + /** + * @brief Check whether path @p e is topologically and geometrically valid. + * + * @param overlay_mesh: overlay mesh providing connectivity + * @param e: path index to validate + * @return true if the path is valid + */ + bool is_valid_path(VoronoiOverlayMesh& overlay_mesh, int e); + + /** + * @brief Compute a spanning subset of paths (a basis for the path complex). + * + * Returns the indices of paths that form a spanning set, eliminating redundant ones. + * + * @param overlay_mesh: overlay mesh for connectivity queries + * @return indices of spanning paths + */ + std::vector compute_spanning_paths(VoronoiOverlayMesh& overlay_mesh); + +private: + Eigen::MatrixXd V; + Eigen::MatrixXi F; + std::vector> polyline_pts; + std::vector is_redundant_path; + + std::vector v_new2old, v_old2new; + Eigen::SparseMatrix vv2he_o; + std::vector> split_halfedges; + std::vector> path_halfedges; + + std::vector> redundant_edges; + std::vector> redundant_vertices; + std::vector halfedge_counts; + + std::vector generate_face_chain(const std::vector>& faces) const; + int rotate_halfedge_to_tip(const VoronoiOverlayMesh& overlay_mesh, int hij, int vk) const; + int get_start_halfedge(const VoronoiOverlayMesh& overlay_mesh, int e); + + /** + * @brief Attempt to separate a simple (non-self-intersecting) path @p e from others. + * + * @param overlay_mesh: overlay mesh to modify + * @param e: path index to separate + * @return true if the separation succeeded, false if the path could not be resolved + */ + bool separate_simple_path(VoronoiOverlayMesh& overlay_mesh, int e); + + /** + * @brief Locally separate any remaining crossing or co-incident paths. + * + * WARNING: currently fails for complex intersections; use retrace_paths instead + * + * @param overlay_mesh: overlay mesh to modify + */ + void separate_paths(VoronoiOverlayMesh& overlay_mesh); +}; + +/** + * @brief Get the 3D position of a SurfacePoint on an overlay mesh. + * + * @param V_o: vertex positions of the original mesh (#V x 3) + * @param overlay_mesh: overlay mesh providing barycentric embedding + * @param pt: surface point to evaluate + * @return 3D position of @p pt + */ +Eigen::Vector3d get_surface_point_position( + Eigen::MatrixXd& V_o, + VoronoiOverlayMesh& overlay_mesh, + SurfacePoint& pt); + +/** + * @brief Get the 3D positions for all SurfacePoints in a polyline. + * + * @param overlay_mesh: overlay mesh providing barycentric embedding + * @param polyline_pts: per-path sequences of SurfacePoints + * @return per-path sequences of 3D positions + */ +std::vector> get_surface_point_positions( + VoronoiOverlayMesh& overlay_mesh, + std::vector>& polyline_pts +); + +/** + * @brief Convert a geometry-central SurfacePoint to the SurfacePoint representation. + * + * WARNING: Face points are an error state, and all geometric data is forgotten. + * + * @param pt: geometry-central surface point + * @return equivalent SurfacePoint + */ +SurfacePoint convert_surface_point(const geometrycentral::surface::SurfacePoint& pt); + +/** + * @brief Compute geodesic path points for a set of vertex-to-vertex paths on an overlay mesh. + * + * Each path in @p paths is a sequence of overlay mesh vertex indices. The returned points + * describe the piecewise-geodesic interpolation of each path as SurfacePoints, suitable for + * subsequent straightening via PathStraightener. + * + * @param overlay_mesh: Voronoi overlay mesh carrying metric and connectivity + * @param paths: paths to process, each given as an ordered list of overlay vertex indices + * @return per-path lists of SurfacePoints tracing the geodesic + */ +std::vector> generate_geodesic_path_points( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths); + +/** + * @brief Embed geodesic paths into a VoronoiOverlayMesh by tracing and straightening them. + * + * Each path in @p paths is a sequence of overlay vertex indices. The function traces a + * piecewise-linear path between consecutive vertices and straightens it into a geodesic + * embedded in the overlay mesh connectivity. + * + * WARNING: Replaced with custom geodesic tracing to avoid any intersections. + * + * @param overlay_mesh: Voronoi overlay mesh; modified to contain the embedded paths + * @param paths: paths to embed, each given as an ordered list of overlay vertex indices + */ +// TODO: replace voronoi with basic overlay once separate retracing step +void add_geodesic_paths( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths); + +} +} diff --git a/include/resolution/resolution/core/layout.h b/include/resolution/resolution/core/layout.h new file mode 100644 index 0000000..efb8279 --- /dev/null +++ b/include/resolution/resolution/core/layout.h @@ -0,0 +1,132 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" + +namespace Penner { +namespace Resolution { + + +/** + * @brief Compute seamless error measure for the rotation that identifies a halfedge + * with its paired halfedge in the uv plane. + * + * The error measure is determined from the fact that the 2x2 rotation matrix + * in the plane R has order that divides 4. The error is the max deviation of the + * coefficients of R^4 from the identity coefficients. + * + * @param halfedge: halfedge data structure + * @param uv: per-halfedge tip uv coordinates + * @param hij: halfedge to compute the rotation across + * @return seamless error measure + */ +double compute_seamless_error( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij); + +/** + * @brief Determine if a 2x2 matrix corresponds to a seamless rotation, up to some + * numerical tolerance + * + * @param R: 2x2 rotation matrix + * @return true if the matrix is numerically seamless + * @return false otherwise + */ +bool is_seamless_rotation(const Eigen::Matrix2d& R); + +/** + * @brief Determine the rotation to identify a halfedge with its paired edge, up to + * translation + * + * @param halfedge: halfedge data structure + * @param uv: per-halfedge tip uv coordinates + * @param hij: halfedge to compute the rotation across + * @return 2x2 rotation matrix + */ +Eigen::Matrix2d infer_float_rotation( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij +); + +/** + * @brief Determine the rotation to identify a halfedge with its paired edge, up to + * translation, rounding the coefficients to the nearest integer. + * + * WARNING: this snapping is only safe for seamless rotations, which are theoretically + * integer valued + * + * @param halfedge: halfedge data structure + * @param uv: per-halfedge tip uv coordinates + * @param hij: halfedge to compute the rotation across + * @return 2x2 rotation matrix + */ +Eigen::Matrix2d infer_rotation( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij +); + +/** + * @brief Rigidly transform the face opposite the face containing the given halfedge + * to be adjacent to the halfedge. + * + * WARNING: assumes the parametrization is seamless so that the rotation for the + * transformation is a numerically stable integer valued matrix + * + * @param halfedge: halfedge data structure + * @param uv: per-halfedge tip uv coordinates + * @param hij: halfedge to join with its mate (this halfedge is fixed; the mate is moved) + * @return rotation matrix used in the transformation + */ +Eigen::Matrix2d transform_opposite_face( + const HalfedgeMesh& halfedge, + std::vector& uv, + int hij +); + +/** + * @brief Rotate a vector in the plane of a face of a halfedge matrix + * + * @param halfedge: halfedge data structure + * @param V: per-halfedge tip 3D coordinates + * @param R: rotation to apply + * @param d; 3D vector, assumed tangent to given face + * @param face_index: face index + * @return rotated vector + */ +Eigen::Vector3d rotate_in_face_plane( + const HalfedgeMesh& halfedge, + const std::vector& V, + const Eigen::Matrix2d& R, + const Eigen::Vector3d& d, + int face_index); + +/** + * @brief Rigidly transform the face opposite the face containing the given halfedge + * to be adjacent to the halfedge, and also transform two per-face vector fields + * representing target uv gradients accordingly so the norm of the gradients from the + * targets is invariant under the transformation + * + * WARNING: assumes the parametrization is seamless so that the rotation for the + * transformation is a numerically stable integer valued matrix + * + * @param halfedge: halfedge data structure + * @param uv: per-halfedge tip uv coordinates + * @param V: per-halfedge tip 3D coordinates + * @param PD1: target u gradient per face + * @param PD2: target v gradient per face + * @param hij: halfedge to join with its mate (this halfedge is fixed; the mate is moved) + * @return rotation matrix used in the transformation + */ +Eigen::Matrix2d transform_opposite_face( + const HalfedgeMesh& halfedge, + std::vector& uv, + const std::vector& V, + std::vector& PD1, + std::vector& PD2, + int hij); + +} +} diff --git a/include/resolution/resolution/core/quad_quality.h b/include/resolution/resolution/core/quad_quality.h new file mode 100644 index 0000000..12e0b77 --- /dev/null +++ b/include/resolution/resolution/core/quad_quality.h @@ -0,0 +1,53 @@ + +#pragma once +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + +Scalar compute_triangle_angle( + const Vector3& x0, + const Vector3& x1, + const Vector3& x2); + + +/** + * @brief Compute the four per-face corner angles of a quad mesh + * + * @param V: quad mesh vertices + * @param F: quad mesh faces + * @return per-face arrays of four corner angles + */ +std::vector> compute_quad_angles( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +); + +/** + * @brief Compute the four per-face edge lengths of a quad mesh + * + * @param V: quad mesh vertices + * @param F: quad mesh faces + * @return per-face arrays of four edge lengths + */ +std::vector> compute_quad_lengths( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +); + +/** + * @brief Compute the per-face scaled jacobian quality measure of a quad mesh. + * + * The values lie between [-1, 1], with [0.3, 1] considered acceptable + * + * @param V: quad mesh vertices + * @param F: quad mesh faces + * @return per-face scaled Jacobian values + */ +std::vector compute_scaled_jacobian( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +); + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/core/triangulation.h b/include/resolution/resolution/core/triangulation.h new file mode 100644 index 0000000..27d6e61 --- /dev/null +++ b/include/resolution/resolution/core/triangulation.h @@ -0,0 +1,104 @@ +#pragma once +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + + /** + * @brief Rotate the vector p 90 degrees counterclockwise. + * + * @param p: vector in plane + * @return rotated vector + */ +Vector2 rotate_counterclockwise(const Vector2&p); + +/** + * @brief Determine if the halfplane to the left of the edge x1 - x0 contains the point p. + * + * @param p: point to check + * @param x0: base of half plane edge + * @param x1: tip of half plane edge + * @param epsilon: (optional) epsilon offset for half plane + * @return true iff the half plane (strictly) contains p + */ +bool does_half_plane_contain( + const Vector2& p, + const Vector2& x0, + const Vector2& x1, + Scalar epsilon=0.); + +/** + * @brief Determine if the triangle bounded by x0, x1, x2 contains the point p + * + * @param p: point to check + * @param x0: first vertex + * @param x1: second vertex + * @param x2: third vertex + * @param epsilon: (optional) epsilon offset for triangle boundaries + * @return true iff the triangle (strictly) contains p + */ +bool does_triangle_contain( + const Vector2& p, + const Vector2& x0, + const Vector2& x1, + const Vector2& x2, + Scalar epsilon=0.); + +/** + * @brief Compute the (signed) area of a triangle bounded by three vertices + * + * @param x0: first vertex + * @param x1: second vertex + * @param x2: third vertex + * @return triangle area + */ +Scalar compute_triangle_area( + const Vector2& x0, + const Vector2& x1, + const Vector2& x2); + +/** + * @brief Compute the angle of a triangle corner + * + * @param x0: first vertex (where angle measured) + * @param x1: second vertex + * @param x2: third vertex + * @return triangle area + */ +Scalar compute_triangle_angle( + const Vector2& x0, + const Vector2& x1, + const Vector2& x2); +Scalar compute_triangle_angle( + const Vector3& x0, + const Vector3& x1, + const Vector3& x2); + +class EarClipper { +public: + Scalar epsilon = 1e-10; + EarClipper(const std::vector& polygon_vertices); + + void triangulate(); + void triangulate_fast(); + void triangulate_optimal(); + + void clip_ear(int i); + void update_reflex_vertex(int i); + + bool is_vertex_ear(int i) const; + bool does_corner_contain(const Vector2& p, int i) const; + + std::vector> edges; + std::vector> triangles; + + void view() const; + +private: + std::vector vertices; + std::vector next; + std::vector prev; + std::vector is_reflex_vertex; +}; +} +} \ No newline at end of file diff --git a/include/resolution/resolution/core/viewer.h b/include/resolution/resolution/core/viewer.h new file mode 100644 index 0000000..186b874 --- /dev/null +++ b/include/resolution/resolution/core/viewer.h @@ -0,0 +1,37 @@ + +#pragma once +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + +/** + * @brief View the uv coordinate layout of a parametrization induced by a metric. + * + * @param marked_metric: metric that induces parametrization + * @param show: if true, show the layout + */ +void view_layout(const MarkedPennerConeMetric& marked_metric, bool show=true); + +/** + * @brief View a combed frame field on a surface + * + * @param V: mesh vertices + * @param F: mesh faces + * @param uv: uv coordinates + * @param FT: uv face connectivity + * @param PD1: first combed vector field (u gradients) + * @param PD2: second combed vector field (v gradients) + * @param mesh_handle: name for mesh in viewer + */ +void view_combed_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT, + const Eigen::MatrixXd& PD1, + const Eigen::MatrixXd& PD2, + std::string mesh_handle=""); + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/experimental/dynamic_markings.h b/include/resolution/resolution/experimental/dynamic_markings.h new file mode 100644 index 0000000..2663d70 --- /dev/null +++ b/include/resolution/resolution/experimental/dynamic_markings.h @@ -0,0 +1,100 @@ + +#pragma once + +#include "resolution/core/common.h" +#include "holonomy/core/common.h" +#include "holonomy/holonomy/newton.h" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "holonomy/holonomy/holonomy.h" +#include "resolution/core/holonomy_coordinates.h" + +namespace Penner { +namespace Resolution { + +class DynamicPennerConeMetric : public MarkedPennerConeMetric +{ +public: + VectorX m_alpha; + VectorX m_rotation_form; + + DynamicPennerConeMetric( + const Mesh& m, + const VectorX& metric_coords, + const VectorX& alpha, + const VectorX& rotation_form) + : MarkedPennerConeMetric(m, metric_coords, {}, {}) + , m_alpha(alpha) + , m_rotation_form(rotation_form) + {} + + VectorX& get_rotation_form() { return m_rotation_form; } + const VectorX& get_rotation_form() const { return m_rotation_form; } + + VectorX& get_holonomy_coordinates() { return m_alpha; } + const VectorX& get_holonomy_coordinates() const { return m_alpha; } + + virtual bool flip_ccw(int _h, bool Ptolemy = true) override + { + auto& rotation_form = get_rotation_form(); + + // make rotation trivial across edge + align_rotation_form_faces(*this, rotation_form, _h); + + // update holonomy + VectorX& alpha = get_holonomy_coordinates(); + update_holonomy_coordinates_under_ccw_flip(*this, alpha, _h); + + bool success = MarkedPennerConeMetric::flip_ccw(_h, Ptolemy); + + return success; + } + + virtual void make_discrete_metric() override + { + // Make the copied mesh Delaunay with Ptolemy flips + VectorX u; + u.setZero(n_ind_vertices()); + DelaunayStats del_stats; + SolveStats solve_stats; + bool use_ptolemy = true; + ConformalIdealDelaunay::MakeDelaunay(*this, u, del_stats, solve_stats, use_ptolemy); + m_is_discrete_metric = true; + + // update markings + MarkedMetricParameters marked_metric_params; + marked_metric_params.use_connectivity = false; + generate_basis_loops(*this, m_homology_basis_loops, marked_metric_params, {}); + + // compute rotation + const auto& rotation_form = get_rotation_form(); + const VectorX& alpha = get_holonomy_coordinates(); + int num_basis_loops = m_homology_basis_loops.size(); + kappa_hat.resize(num_basis_loops); + for (int i = 0; i < num_basis_loops; ++i) { + // Compute field rotation and metric holonomy + Scalar rotation = Holonomy::compute_dual_loop_rotation(*this, rotation_form, *m_homology_basis_loops[i]); + Scalar holonomy = Holonomy::compute_dual_loop_holonomy(*this, alpha, *m_homology_basis_loops[i]); + + // Constraint is the difference of the holonomy and rotation + kappa_hat[i] = holonomy - rotation; + spdlog::info("new kappa {}: {}", i, kappa_hat[i]); + } + + return; + } + + virtual std::unique_ptr clone_cone_metric() const override + { + return std::make_unique(DynamicPennerConeMetric(*this)); + } + + virtual std::unique_ptr clone_marked_metric() const override + { + return std::make_unique(DynamicPennerConeMetric(*this)); + } + + +}; + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/experimental/holonomy_coordinates.h b/include/resolution/resolution/experimental/holonomy_coordinates.h new file mode 100644 index 0000000..1b7740e --- /dev/null +++ b/include/resolution/resolution/experimental/holonomy_coordinates.h @@ -0,0 +1,35 @@ +#pragma once + +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + +/** + * @brief Update the per-corner holonomy coordinates to reflect holonomy after the + * counter-clockwise flip of the given edge + * + * @param m: halfedge mesh + * @param alpha: holonomy coordinates before clip + * @param halfedge_index: edge to be flipped + */ +void update_holonomy_coordinates_under_ccw_flip( + const Mesh& m, + VectorX& alpha, + int halfedge_index); + +/** + * @brief Update the per-corner holonomy coordinates to reflect holonomy after the + * clockwise flip of the given edge + * + * @param m: halfedge mesh + * @param alpha: holonomy coordinates before clip + * @param halfedge_index: edge to be flipped clockwise + */ +void update_holonomy_coordinates_under_clockwise_flip( + const Mesh& m, + VectorX& alpha, + int halfedge_index); + +} // namespace Resolution +} // namespace Penner \ No newline at end of file diff --git a/include/resolution/resolution/experimental/regularization.h b/include/resolution/resolution/experimental/regularization.h new file mode 100644 index 0000000..970e89d --- /dev/null +++ b/include/resolution/resolution/experimental/regularization.h @@ -0,0 +1,307 @@ +#pragma once + +#include "resolution/core/common.h" +#include "holonomy/core/common.h" +#include "holonomy/holonomy/newton.h" + +namespace Penner { +namespace Resolution { + +class OptimizeCorrectionNewton : public Holonomy::OptimizeNewton +{ +public: +OptimizeCorrectionNewton() {} + +protected: + virtual void update_descent_direction(MarkedPennerConeMetric& marked_metric) override + { + double t_start; + + // Compute corner angles and the constraint with its jacobian + // TODO Add safety checks + t_start = timer.getElapsedTime(); + marked_metric.make_discrete_metric(); + marked_metric.get_corner_angles(alpha, cot_alpha); + constraint = marked_metric.constraint(alpha); + J = marked_metric.constraint_jacobian(cot_alpha); + log.constraint_time = timer.getElapsedTime() - t_start; + marked_metric.write_status_log(metric_status_file); + + // Compute Newton descent direction from the constraint and jacobian + t_start = timer.getElapsedTime(); + + SPDLOG_DEBUG("Jacobian with maximum value {}", J.coeffs().maxCoeff()); + SPDLOG_DEBUG("Jacobian with mean {}", J.coeffs().mean()); + SPDLOG_DEBUG("Metric basis with average {}", metric_basis_matrix.coeffs().mean()); + SPDLOG_DEBUG("Constraint with average {}", constraint.mean()); + + solve_linear_system(); + + // add regularization term + VectorX delta_metric = reduced_metric_coords - reduced_metric_init; + MatrixX L = J * J.transpose(); + VectorX w = J * delta_metric; + VectorX projection = J.transpose() * solve_psd_system(L, w); + VectorX complement = delta_metric - projection; + descent_direction -= complement; + + SPDLOG_TRACE("Descent direction found with norm {}", descent_direction.norm()); + SPDLOG_TRACE("Descent direction error is {}", (J * descent_direction + constraint).norm()); + SPDLOG_TRACE("Projected constraint is {}", constraint.dot(J * descent_direction)); + log.direction_time = timer.getElapsedTime() - t_start; + log.direction_norm = descent_direction.norm(); + log.direction_residual = (J * descent_direction + constraint).norm(); + } + + virtual void perform_line_search( + const MarkedPennerConeMetric& initial_marked_metric, + MarkedPennerConeMetric& marked_metric) override + { + double t_start = timer.getElapsedTime(); + Scalar epsilon = 1. / (log.num_iter + 1); + epsilon = 0.; + + // Get starting metric coordinates + VectorX reduced_metric_start = reduced_metric_coords; + + // Get the constraint norm and its dot product with the jacobian-projected descent direction + // Note: the product of the jacobian and descent direction should be the negative + // constraint, but this may fail due to numerical instability or regularization + VectorX metric_delta = reduced_metric_coords - reduced_metric_init; + Scalar l2_c0_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + Scalar proj_g0 = descent_direction.dot(J.transpose() * constraint); + spdlog::debug("Initial squared error norm is {}", l2_c0_sq); + spdlog::debug("Initial projected constraint is {}", proj_g0); + + // Reduce descent direction range to avoid nans/infs + if (alg_params.do_reduction) { + while (lambda * (descent_direction.maxCoeff() - descent_direction.minCoeff()) > 2.5) { + lambda /= 2; + spdlog::debug("Reducing lambda to {} for stability", lambda); + } + } + + // Make initial line step with updated constraint + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); + update_constraint(marked_metric); + + // Line search until the constraint norm decreases and the projected constraint is + // nonpositive We also allow the norm bound to be dropped or made approximate with some + // relative term alpha + spdlog::debug("Starting line search"); + metric_delta = reduced_metric_coords - reduced_metric_init; + Scalar l2_c_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + Scalar proj_cons = descent_direction.dot(J.transpose() * constraint); + Scalar gamma = 1.; // require ratio of current to initial constraint norm to be below alpha + bool bound_norm = true; + while ((bound_norm && (l2_c_sq > (gamma * l2_c0_sq))) || (proj_cons > 0)) { + // Backtrack one step + lambda /= 2; + + // If lambda low enough, stop bounding the norm + if (lambda <= alg_params.bound_norm_thres) { + bound_norm = false; + spdlog::debug("Dropping norm bound."); + } + + // Change metric and update constraint + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); + update_constraint(marked_metric); + + // Update squared constraint norm and projected constraint + metric_delta = reduced_metric_coords - reduced_metric_init; + l2_c_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + proj_cons = descent_direction.dot(J.transpose() * constraint); + spdlog::debug("Squared error norm is {}", l2_c_sq); + spdlog::debug("Projected constraint is {}", proj_cons); + + // Check if lambda is below the termination threshold + if (lambda < alg_params.min_lambda) break; + } + + // Make final line step in original connectivity + spdlog::debug("Updating metric"); + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, true, false); + + log.line_search_time = timer.getElapsedTime() - t_start; + } +}; + +class OptimizeRegularizedNewton : public Holonomy::OptimizeNewton +{ +public: + OptimizeRegularizedNewton() {} + Scalar epsilon = 0.01; + Scalar max_grad = 0.; + VectorX reduced_metric_base; + + void set_base_coordinates(const MarkedPennerConeMetric& marked_metric) + { + reduced_metric_base = marked_metric.get_reduced_metric_coordinates(); + } + +protected: + void initialize_data_log() override + { + // Do nothing if error logging disabled + if (!alg_params.error_log) return; + + // Generate data log path + std::filesystem::create_directory(alg_params.output_dir); + std::string data_log_path; + + // Open main logging file + data_log_path = join_path(alg_params.output_dir, "iteration_data_log.csv"); + spdlog::info("Writing data to {}", data_log_path); + log_file = std::ofstream(data_log_path, std::ios::out | std::ios::app); + log_file << "num_iter,"; + log_file << "max_error,"; + log_file << "max_grad,"; + log_file << "step_size,"; + log_file << "rmsre,"; + log_file << "time,"; + log_file << "solve_time,"; + log_file << std::endl; + } + + // Write newton log iteration data to file + void write_data_log_entry() override + { + // Do nothing if error logging disabled + if (!alg_params.error_log) return; + + // Write iteration row + log_file << log.num_iter << ","; + log_file << std::fixed << std::setprecision(17) << log.max_error << ","; + log_file << std::fixed << std::setprecision(17) << max_grad << ","; + log_file << std::fixed << std::setprecision(17) << log.step_size << ","; + log_file << std::fixed << std::setprecision(17) << log.rmsre << ","; + log_file << std::fixed << std::setprecision(17) << log.time << ","; + log_file << std::fixed << std::setprecision(17) << log.solve_time << ","; + log_file << std::endl; + } + + virtual void update_descent_direction(MarkedPennerConeMetric& marked_metric) override + { + double t_start; + + // Compute corner angles and the constraint with its jacobian + // TODO Add safety checks + t_start = timer.getElapsedTime(); + marked_metric.make_discrete_metric(); + marked_metric.get_corner_angles(alpha, cot_alpha); + constraint = marked_metric.constraint(alpha); + J = marked_metric.constraint_jacobian(cot_alpha); + log.constraint_time = timer.getElapsedTime() - t_start; + marked_metric.write_status_log(metric_status_file); + + // Compute Newton descent direction from the constraint and jacobian + t_start = timer.getElapsedTime(); + + SPDLOG_DEBUG("Jacobian with maximum value {}", J.coeffs().maxCoeff()); + SPDLOG_DEBUG("Jacobian with mean {}", J.coeffs().mean()); + SPDLOG_DEBUG("Metric basis with average {}", metric_basis_matrix.coeffs().mean()); + SPDLOG_DEBUG("Constraint with average {}", constraint.mean()); + + solve_linear_system(); + + // add regularization term + MatrixX identity = id_matrix(marked_metric.n_reduced_coordinates()); + MatrixX L = (J.transpose() * J) + (epsilon * identity); + VectorX delta_metric = reduced_metric_coords - reduced_metric_base; + VectorX w = (J.transpose() * constraint) + (epsilon * delta_metric); + max_grad = sup_norm(w); + spdlog::info("Gradient has 2-norm {}", w.norm()); + spdlog::info("Gradient has sup-norm {}", max_grad); + descent_direction = -solve_psd_system(L, w); + + SPDLOG_TRACE("Descent direction found with norm {}", descent_direction.norm()); + SPDLOG_TRACE("Descent direction error is {}", (J * descent_direction + constraint).norm()); + SPDLOG_TRACE("Projected constraint is {}", constraint.dot(J * descent_direction)); + log.direction_time = timer.getElapsedTime() - t_start; + log.direction_norm = descent_direction.norm(); + log.direction_residual = (J * descent_direction + constraint).norm(); + } + + virtual void perform_line_search( + const MarkedPennerConeMetric& initial_marked_metric, + MarkedPennerConeMetric& marked_metric) override + { + double t_start = timer.getElapsedTime(); + + // Get starting metric coordinates + VectorX reduced_metric_start = reduced_metric_coords; + + // Get the constraint norm and its dot product with the jacobian-projected descent direction + // Note: the product of the jacobian and descent direction should be the negative + // constraint, but this may fail due to numerical instability or regularization + VectorX metric_delta = reduced_metric_coords - reduced_metric_base; + Scalar l2_c0_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + Scalar proj_g0 = descent_direction.dot(J.transpose() * constraint + epsilon * metric_delta); + spdlog::debug("Initial squared error norm is {}", l2_c0_sq); + spdlog::debug("Initial projected constraint is {}", proj_g0); + + // Reduce descent direction range to avoid nans/infs + if (alg_params.do_reduction) { + while (lambda * (descent_direction.maxCoeff() - descent_direction.minCoeff()) > 2.5) { + lambda /= 2; + spdlog::debug("Reducing lambda to {} for stability", lambda); + } + } + + // Make initial line step with updated constraint + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); + update_constraint(marked_metric); + + // Line search until the constraint norm decreases and the projected constraint is + // nonpositive We also allow the norm bound to be dropped or made approximate with some + // relative term alpha + spdlog::debug("Starting line search"); + metric_delta = reduced_metric_coords - reduced_metric_base; + Scalar l2_c_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + Scalar proj_cons = descent_direction.dot(J.transpose() * constraint + epsilon * metric_delta); + Scalar gamma = 1.; // require ratio of current to initial constraint norm to be below alpha + bool bound_norm = (lambda > alg_params.bound_norm_thres); + while ((bound_norm && (l2_c_sq > (gamma * l2_c0_sq))) || (proj_cons > 0)) { + // Backtrack one step + lambda /= 2; + + // If lambda low enough, stop bounding the norm + if (lambda <= alg_params.bound_norm_thres) { + bound_norm = false; + spdlog::debug("Dropping norm bound."); + } + + // Change metric and update constraint + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, false, false); + update_constraint(marked_metric); + + // Update squared constraint norm and projected constraint + metric_delta = reduced_metric_coords - reduced_metric_base; + l2_c_sq = constraint.squaredNorm() + epsilon * metric_delta.squaredNorm(); + proj_cons = descent_direction.dot(J.transpose() * constraint + epsilon * metric_delta); + spdlog::debug("Squared error norm is {}", l2_c_sq); + spdlog::debug("Projected constraint is {}", proj_cons); + + // Check if lambda is below the termination threshold + if (lambda < alg_params.min_lambda) break; + } + + // Make final line step in original connectivity + spdlog::debug("Updating metric"); + reduced_metric_coords = reduced_metric_start + lambda * descent_direction; + marked_metric.change_metric(initial_marked_metric, reduced_metric_coords, true, false); + + log.line_search_time = timer.getElapsedTime() - t_start; + } + +}; + + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/experimental/representatives.h b/include/resolution/resolution/experimental/representatives.h new file mode 100644 index 0000000..0fa9985 --- /dev/null +++ b/include/resolution/resolution/experimental/representatives.h @@ -0,0 +1,11 @@ +#pragma once +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + +std::vector> generate_radial_surface(int genus); +std::vector> generate_representative_surface(int genus); + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/experimental/simplification.h b/include/resolution/resolution/experimental/simplification.h new file mode 100644 index 0000000..ff227a3 --- /dev/null +++ b/include/resolution/resolution/experimental/simplification.h @@ -0,0 +1,246 @@ +#pragma once + +#include "resolution/core/common.h" +#include "field/forms.h" + +namespace Penner { +namespace Resolution { + +/** + * @brief Set the rotation across a halfedge, maintaining edge consistency. + * + * @param m: underlying mesh + * @param rotation_form: rotation of cross field across halfdges + * @param halfedge: halfedge to update + * @param kappa: new rotation across halfedge + */ +void set_rotation_form(const Mesh& m, VectorX& rotation_form, int halfedge_index, Scalar kappa); + +/** + * @brief Align the field in the faces adjacent to the halfedge, making the rotation + * across the edge trivial. + * + * The average angle is used for the aligned faces. + * + * @param m: underlying mesh + * @param rotation_form: rotation of cross field across halfdges + * @param halfedge_index: halfedge of the rotation to modify. + */ +void align_rotation_form_faces(const Mesh& m, VectorX& rotation_form, int halfedge_index); + +class IntrinsicSimplificationMesh +{ +public: + bool use_random_seed = false; + bool maintain_simplicial = true; + + /** + * @brief Construct a new Intrinsic Simplification Mesh object + * + * @param m: initial mesh for simplification + */ + IntrinsicSimplificationMesh(const Mesh& m); + + void set_rotation_form(const VectorX& rotation_form); + + void set_dual_loop(const std::vector>& dual_loops); + + /** + * @brief Simplify the mesh by removing as many flat vertices as possible. + * + */ + void simplify_mesh(); + + /** + * @brief Simplify the mesh by collapsing as many edges as possible. + * + */ + void collapse_edges(); + + bool collapse_edge(int edge_index); + + /** + * @brief Compute the degree of a given vertex + * + * @param vertex_index: index of a vertex + * @return degree of the vertex + */ + int compute_degree(int vertex_index) const; + + /** + * @brief Remove a given vertex if possible + * + * @param vertex_index: index of the vertex to remove + * @return true iff the vertex is successfully removed + */ + bool remove_vertex(int vertex_index); + + /** + * @brief Get the mesh object + * + * @return reference to the mesh + */ + Mesh& get_mesh() { return m_mesh; } + const Mesh& get_mesh() const { return m_mesh; } + + const std::vector& get_e2h() const { return m_e2h; } + const std::vector& get_h2e() const { return m_h2e; } + + /** + * @brief Get the holonomy coordinates vector. + * + * @return vector of per corner holonomy angle coordinates + */ + VectorX& get_holonomy_coordinates() { return m_alpha; } + const VectorX& get_holonomy_coordinates() const { return m_alpha; } + VectorX get_opposite_angles() const; + + /** + * @brief Generate the simplified mesh object. + * + * @return mesh reindexed to remove elements + */ + Mesh get_simplified_mesh() const; + + /** + * @brief Get the simplified marked metric object with consistent holonomy. + * + * Currently uses initial 0 coordinates for the metric. + * + * @return metric for mesh with removed elements + */ + MarkedPennerConeMetric get_simplified_marked_metric( + const MarkedMetricParameters& marked_metric_params + ) const; + + /** + * @brief Get the rotation of the field across a given halfedge + * + * @param halfedge_index: index of the halfedge to query + */ + Scalar get_rotation(int halfedge_index) const + { + return m_rotation_form[halfedge_index]; + } + VectorX& get_rotation_form() { return m_rotation_form; } + const VectorX& get_rotation_form() const { return m_rotation_form; } + int vertex_parent(int vertex_index) const { return m_vertex_parent[vertex_index]; } + + /** + * @brief Set the rotation across a given halfedge, maintaining the invariant + * that the rotation of opposite halfedges sum to 0. + * + * @param halfedge_index: index of the halfedge to modify + * @param kappa: new rotation value + */ + void set_rotation_form(int halfedge_index, Scalar kappa) + { + m_rotation_form[halfedge_index] = kappa; + m_rotation_form[m_mesh.opp[halfedge_index]] = -kappa; + } + + /** + * @brief Align the field in the faces adjacent to the halfedge, making the rotation + * across the edge trivial. + * + * The average angle is used for the aligned faces. + * + * @param halfedge_index: halfedge of the rotation to modify. + */ + void align_faces(int halfedge_index); + + /** + * @brief Generate an embedding of the simplified mesh from the original mesh embedding. + * + * @param V: original mesh vertices + * @param vtx_reindex: reindexing of the halfedge mesh vertices + * @return simplified mesh vertices + * @return simplified mesh faces + */ + std::tuple generate_mesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + /** + * @brief View the simplified mesh + * + * @param V: original mesh vertices + * @param vtx_reindex: reindexing of the halfedge mesh vertices + */ + void view_simplified_mesh(const Eigen::MatrixXd& V, const std::vector& vtx_reindex) const; + + std::vector>& get_dual_loops() { return m_dual_loops; } + const std::vector>& get_dual_loops() const { return m_dual_loops; } + + /** + * @brief Determine whether the holonomy signature of the homology basis and cone angles are consistent + * with the simplified rotation form and holonomy coordinates. + * + * @return true iff the rotation form is consistent; + */ + bool is_rotation_consistent() const; + + void get_dual_loops( + std::vector>& dual_loops) const; + + VectorX get_simplified_rotation_form() const; + + VectorX get_simplified_holonomy_coordinates() const; + + Scalar simplification_factor; + void garbage_collection(); + +private: + Mesh m_mesh; + std::vector m_is_halfedge_deleted; + std::vector m_is_vertex_deleted; + std::vector m_is_face_deleted; + std::vector m_is_edge_deleted; + std::vector m_vertex_parent; + std::vector m_e2h; + std::vector m_h2e; + VectorX m_rotation_form; + VectorX m_alpha; + VectorX m_kappa; + + std::vector prev; + + std::vector> m_dual_loops; + int removed_vert_count; + int initial_vertices; + + + // topology modification with success checks + bool increase_degree(int vertex_index); + bool decrease_degree(int vertex_index); + bool flip_to_degree(int vertex_index, int target_degree); + bool remove_degree_3_vertex(int vertex_index); + bool splice_halfedges(int h, int g); + bool join_halfedge_endpoints(int halfedge_index); + bool join_edge_faces(int edge_index); + void flip_ccw(int hij); + void rotate_edge(int halfedge_index, Scalar theta); + + void delete_halfedge(int halfedge_index); + void delete_edge(int edge_index); + void delete_vertex(int vertex_index); + void delete_face(int face_index); + + int create_face(); + int create_vertex(); + + // predicates + bool will_increase_degree(int hij) const; + bool will_decrease_degree(int hij) const; + bool is_collapse_possible(int edge_index) const; + bool is_flat_vertex(int vertex_index) const; + bool is_degree_1_vertex(int vertex_index) const; + bool is_degree_3_vertex(int vertex_index) const; + bool is_flippable_edge(int halfedge_index) const; + bool is_deleted_vertex(int vertex_index) const { return m_is_vertex_deleted[vertex_index]; } + bool is_deleted_face(int face_index) const { return m_is_face_deleted[face_index]; } + bool is_deleted_halfedge(int halfedge_index) const { return m_is_halfedge_deleted[halfedge_index]; } +}; + +} // namespace Resolution +} // namespace Penner diff --git a/include/resolution/resolution/experimental/simplification_overlay.h b/include/resolution/resolution/experimental/simplification_overlay.h new file mode 100644 index 0000000..d1cfc2f --- /dev/null +++ b/include/resolution/resolution/experimental/simplification_overlay.h @@ -0,0 +1,46 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" +#include "resolution/hierarchy/overlay.h" +#include "resolution/experimental/simplification.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" + +namespace Penner { +namespace Resolution { + + +class IntrinsicSimplificationOverlayMesh : public SimplificationOverlayMesh +{ +public: + IntrinsicSimplificationMesh M; // simplified mesh (TODO: maybe remove this) + + /** + * @brief Construct a new Simplification Overlay Mesh object to simplify the given mesh with an overlay + * + * @param mesh + */ + IntrinsicSimplificationOverlayMesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const Mesh& mesh); + void reset(); + void mark_vertices(); + bool find_edge(int edge_index); +}; + +} +} + + /** + * @brief Representation of a regular polygon for overaly refinement. + * + class RegularOverlayChart + { + public: + RegularOverlayChart(int num_edges); + + private: + void make_edge_between_tip_vertices(int first_halfedge, int second_halfedge); + }; + */ \ No newline at end of file diff --git a/include/resolution/resolution/hierarchy/halfedge.h b/include/resolution/resolution/hierarchy/halfedge.h new file mode 100644 index 0000000..d53cb6f --- /dev/null +++ b/include/resolution/resolution/hierarchy/halfedge.h @@ -0,0 +1,482 @@ +#pragma once + +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + + /** + * @brief Representation of a halfedge mesh that supports standard connectivity modifications: + * - Split Edge + * - Split Vertex + * - Split Face + * - Join Vertices + * - Join Faces + * + * Additional specific operations, such as to remove degree a one vertex, are supported. + * + * There are also methods for common triangle mesh operations: + * - Collapse Edge + * - Flip Edge + * - Split Edge (while preserving triangular faces) + * + * Many operations also feature a "conservative" implementation that only uses an atomic splice + * operation. These methods are generally less efficient and less memory aware, and are primarily + * intended for debugging the faster direct methods. + */ +class HalfedgeMesh +{ +public: + std::vector opp; // Opposite halfedge of halfedge + std::vector n; // Next Halfedge of Halfedge, in Counterclockwise Order + std::vector to; // To Vertex of Halfedge + std::vector face; // Face of Halfedge + std::vector h2e; // map from halfedges to edges + + std::vector prev; // Previous Halfedge of Halfedge, in Counterclockwise Order + std::vector out; // One outgoing Halfedge of vertex; + std::vector f2h; // One Halfedge of Face + std::vector e2h; // map from edges to halfedges + + bool skip_garbage_collection = false; + + /** + * @brief Construct a new trivial Halfedge Mesh. + * + */ + HalfedgeMesh(); + + /** + * @brief Construct a new Halfedge Mesh with given partial halfedge data (the rest are inferred) + * + * @param opp_: opposite halfedge of halfedge + * @param n_: next Halfedge of Halfedge + * @param to_: to Vertex of Halfedge + * @param face_: face of Halfedge + * @param h2e_: map from halfedges to edges + */ + HalfedgeMesh( + const std::vector& opp_, + const std::vector& n_, + const std::vector& to_, + const std::vector& face_, + const std::vector& h2e_ + ); + + /** + * @brief Get the number of halfedges in the mesh + * + * @return halfedge count + */ + int count_halfedges() const {return m_halfedge_count;} + + /** + * @brief Get the number of faces in the mesh + * + * @return face count + */ + int count_faces() const {return m_face_count;} + + /** + * @brief Get the number of edges in the mesh + * + * @return edge count + */ + int count_edges() const {return m_edge_count;} + + /** + * @brief Get the number of vertices in the mesh + * + * @return vertex count + */ + int count_vertices() const {return m_vertex_count;} + + /** + * @brief Number of edges adjacent to a vertex. + * + * @param vertex_index: index of vertex + * @return degree of vertex + */ + int vertex_degree(int vertex_index) const; + + /** + * @brief Number of edges adjacent to a face. + * + * @param face_index: index of face + * @return degree of face + */ + int face_degree(int face_index) const; + + /** + * @brief Predicate for a degree one vertex + * + * @param vertex_index: index of vertex + * @return true iff the vertex is degree one + */ + bool is_vertex_degree_one(int vertex_index) const; + + /** + * @brief Predicate for a degree two vertex + * + * @param vertex_index: index of vertex + * @return true iff the vertex is degree two + */ + bool is_vertex_degree_two(int vertex_index) const; + + /** + * @brief Get a halfedge in a given face with a given vertex at the tip + * + * WARNING: this halfedge may not be unique + * + * @param face_index: index of the face + * @param vertex_index: index of the vertex + * @return halfedge index + */ + int halfedge_from_face_vertex(int face_index, int vertex_index) const; + + /** + * @brief Relabel the tip of halfedge with the given vertex + * + * @param halfedge_index: index of a halfedge with tip to reindex + * @param vertex_index: index of the vertex to put at the tip of the halfedge + */ + void relabel_tip(int halfedge_index, int vertex_index); + + /** + * @brief Relabel the face of a halfedge. + * + * @param halfedge_index: index of a halfedge in the face to relabel + * @param face_index: index of the face label + */ + void relabel_face(int halfedge_index, int face_index); + + /** + * @brief Create and allocate space for a new halfedge index + * + * @return index of new halfedge + */ + virtual int create_halfedge(); + + /** + * @brief Create and allocate space for a new vertex index + * + * @return index of new vertex + */ + virtual int create_vertex(); + + /** + * @brief Create and allocate space for a new edge index + * + * @return index of new edge + */ + virtual int create_edge(); + + /** + * @brief Create and allocate space for a new face index + * + * @return index of new face + */ + virtual int create_face(); + + //TODO: Rename to make_bubble + /** + * @brief Create a new edge on a bubble with two halfedges, two vertices, and one adjacent face + * + * @return index of the new edge + */ + int make_edge(); + + /** + * @brief Delete an edge and its associated halfedges + * + * @param edge_index: index of edge to delete + */ + void delete_edge(int edge_index); + + /** + * @brief Delete an edge without deleting any associated halfedges + * + * @param edge_index: index of edge to delete + */ + void delete_dangling_edge(int edge_index); + + /** + * @brief Delete a vertex + * + * @param vertex_index: index of vertex to delete + */ + void delete_vertex(int vertex_index); + + /** + * @brief Delete a face + * + * @param face_index: index of face to delete + */ + void delete_face(int face_index); + + /** + * @brief Delete an edge on a bubble with two adjacent vertices and one adjacent face, inverse to make_edge + * + * TODO: replace with edge index + * + * @param halfedge_index: halfedge on the bubble to delete + */ + void delete_bubble(int halfedge_index); + + /** + * @brief Perform the atomic halfedge splice operation, swapping the next halfedge of two halfedges. + * + * Resuses existing face and vertex indices to avoid memory usage and fragmentation. + * + * TODO: replace bool with relevant int or void + * + * @param h: first halfedge to splice + * @param g: second halfedge to splice + * @return true as always valid + */ + bool splice_halfedges(int h, int g); + + /** + * @brief Perform the atomic halfedge splice operation, swapping the next halfedge of two halfedges. + * + * Does not reuse any existing face or vertex indices to avoid unintended references. + * + * TODO: replace bool with relevant int or void + * + * @param h: first halfedge to splice + * @param g: second halfedge to splice + * @return true as always valid + */ + bool splice_halfedges_conservative(int h, int g); + + /** + * @brief Determine if the endpoints of the halfedge can be joined. + * + * @param halfedge_index: halfedge to join + * @return true iff the endpoints can be joined + */ + bool is_join_halfedge_endpoints_valid(int halfedge_index) const; + + /** + * @brief Remove an edge, joining the endpoints. + * + * TODO: replace bool with relevant int or void + * + * @param halfedge_index: halfedge to join + * @return true as always succeeds + */ + bool join_halfedge_endpoints(int halfedge_index); + + /** + * @brief Remove an edge, joining the endpoints, using atomic splice operations + * + * TODO: replace bool with relevant int or void + * + * @param halfedge_index: halfedge to join + * @return true as always succeeds + */ + bool join_halfedge_endpoints_conservative(int halfedge_index); + + /** + * @brief Split one vertex into two, creating a new edge between them at the tips of the given halfedges. + * + * TODO: make conservative and fast version, and a test for validity + * + * WARNING: g must be distinct from h but have the same tip + * + * @param h: first halfedge with vertex at tip + * @param g: second halfedge with vertex at tip + * @return one halfedge of the new edge + */ + int split_vertex(int h, int g); + + // TODO: add remove degree two vertex method + + /** + * @brief Split one edge into two, creating one new vertex and no new faces. + * + * @param halfedge_index: index of halfedge in edge to split + * @return index of halfedge pointing to new vertex in same face as the original halfedge + */ + virtual int split_edge(int halfedge_index); + + /** + * @brief Split one edge into two, creating one new vertex and no new faces, using atomic + * splice operations. + * + * @param halfedge_index: index of halfedge in edge to split + * @return index of halfedge pointing to new vertex in same face as the original halfedge + */ + int split_edge_conservative(int halfedge_index); + + /** + * @brief Split one edge in a triangle mesh into two, creating one new vertex and two faces. + * + * @param halfedge_index: index of halfedge in edge to split + * @return index of halfedge pointing to new vertex in same face as the original halfedge + */ + int split_triangle_mesh_edge(int halfedge_index); + + /** + * @brief Determine if an edge can be removed, joining the adjacent faces. + * + * @param halfedge_index: halfedge to remove + * @return true iff the adjacent faces can be joined + */ + bool is_join_edge_faces_valid(int halfedge_index) const; + + /** + * @brief Remove an edge, joining the adjacent faces. + * + * @param halfedge_index: halfedge to remove + * @return index of face containing the now removed edge + */ + virtual int join_edge_faces(int halfedge_index); + + /** + * @brief Remove an edge, joining the adjacent faces, using atomic splice operations. + * + * @param halfedge_index: halfedge to remove + * @return index of face containing the now removed edge + */ + int join_edge_faces_conservative(int halfedge_index); + + /** + * @brief Split the face containing the two halfedges with an edge connecting the tips + * of the halfedges + * + * TODO: add validity check + * + * @param h: first halfedge in face + * @param g: second halfedge in face + * @return halfedge of new edge + */ + virtual int split_face(int h, int g); + + /** + * @brief Split the face containing the two halfedges with an edge connecting the tips + * of the halfedges, using atomic splice operations. + * + * TODO: add validity check + * + * @param h: first halfedge in face + * @param g: second halfedge in face + * @return halfedge of new edge + */ + int split_face_conservative(int h, int g); + + /** + * @brief Remove a degree one vertex and the single adjacent edge + * + * @param vertex_index: vertex to remove + * @return index of vertex at root of trimmed edge + */ + int remove_degree_one_vertex(int vertex_index); + + /** + * @brief Remove a degree one vertex and the single adjacent edge, using atomic splice + * operations + * + * @param vertex_index: vertex to remove + * @return index of vertex at root of trimmed edge + */ + int remove_degree_one_vertex_conservative(int vertex_index); + + /** + * @brief Triangulate a face, connecting each edge to a new vertex at the center + * + * @param face_index face to triangulate with new center + * @return new center vertex + */ + virtual int add_center_to_face(int face_index); + + // TODO: continue refactor from here + + std::tuple generate_halfedge_faces() const; + void reindex_vertices(const std::vector& vtx_reindex); + + bool is_collapse_edge_valid(int halfedge_index) const; + + // WARNING: must remove edges adjacent to tip vertex for overlay + virtual int collapse_edge(int halfedge_index); + int collapse_edge_conservative(int halfedge_index); + + // more restrictive condition for halfedge collapse that prevents degenerate triangles + std::set get_collapse_link(int halfedge_index) const; + bool is_collapse_link_valid(int halfedge_index) const; + + virtual bool triangulate_face(int face_index); + + virtual int flip_edge(int halfedge_index); + + void triangulate(); + + Eigen::SparseMatrix generate_VV_to_halfedge_map() const; + + bool is_simplicial() const; + virtual bool is_valid() const; + virtual void collect_garbage(); + void collect_garbage( + const std::vector& he_new2old, + const std::vector& e_new2old, + const std::vector& v_new2old, + const std::vector& f_new2old, + const std::vector& he_old2new, + const std::vector& e_old2new, + const std::vector& v_old2new, + const std::vector& f_old2new + ); + + std::tuple, std::vector> get_face_reductions() const; + std::tuple, std::vector> get_vertex_reductions() const; + std::tuple, std::vector> get_halfedge_reductions() const; + std::tuple, std::vector> get_edge_reductions() const; + bool is_halfedge_deleted(int halfedge_index) const { return m_is_halfedge_deleted[halfedge_index]; } + bool is_edge_deleted(int edge_index) const { return m_is_edge_deleted[edge_index]; } + bool is_vertex_deleted(int vertex_index) const { return m_is_vertex_deleted[vertex_index]; } + bool is_face_deleted(int face_index) const { return m_is_face_deleted[face_index]; } + +protected: + void delete_halfedge(int halfedge_index); + + void set_next(int h, int hn) + { + n[h] = hn; + prev[hn] = h; + } + + + std::tuple, std::vector, std::vector> reduce_halfedge_maps() const; + std::tuple, std::vector> reduce_face_maps() const; + std::tuple, std::vector> reduce_vertex_maps() const; + std::tuple, std::vector> reduce_edge_maps() const; + +private: + std::vector m_is_halfedge_deleted; // flag for deleted halfedges + std::vector m_is_vertex_deleted; // flag for deleted vertices + std::vector m_is_edge_deleted; // flag for deleted edges + std::vector m_is_face_deleted; // flag for delete faces + int m_halfedge_count; + int m_face_count; + int m_edge_count; + int m_vertex_count; +}; + +/** + * @brief Generate VF faces from minimal halfedge data + * + * @param next: next halfedge in mesh + * @param f2h: map from face to an incident halfedge + * @param to: vertex index at tip of halfedge + * @return: list of list of vertices in faces + * @return: map from triangle corners to opposite halfedge + */ +std::tuple generate_connectivity_faces( + const std::vector& next, + const std::vector& f2h, + const std::vector& to); + +std::tuple, std::vector>> generate_halfedge(const Eigen::MatrixXi& F); + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/hierarchy/overlay.h b/include/resolution/resolution/hierarchy/overlay.h new file mode 100644 index 0000000..4801d46 --- /dev/null +++ b/include/resolution/resolution/hierarchy/overlay.h @@ -0,0 +1,964 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" + +namespace Penner { +namespace Resolution { + +/** + * @brief Halfedge data structure with multiple meshes embedded in or "overlaid" on top + * of an underlying halfedge mesh. + * + * Assumes there is always an orginal simplicial mesh (with corresponding 3D embedding data + * and a frame field) and a simplified mesh (to be parametrized with a 2D layout) + * + */ +class SimplificationOverlayMesh : public HalfedgeMesh +{ +public: + enum class EdgeType { EE, EF }; + enum class VertexType { VV, VE, VF }; + typedef Eigen::Vector BaryCoords; + struct OverlayStructure; + + bool optimize_metric = false; // if true, use Penner coordinate optimization for the parametrization + bool opt_iters = 0; // number of iterations for Penner optimization of the parametrization + bool use_harmonic_overlay_vertices = false; // solve for harmonic positions of + bool use_uniform_overlay_edges = false; // use uniform spacing for splits of original edges + + // experimental: not recommended + bool make_overlay_three_connected = false; // make overlay three connected + bool make_simplified_three_connected = false; // make simplified triangulation three connected + bool skip_degree_two = false; // disable removal of degree two vertices during embedding simplification + bool use_zero_lengths = false; // use zero lengths for the initial Penner coordinates + bool use_intrinsic_lengths = false; // use intrinsic edge lengths for the inital Penner coordinates + + // debugging + bool keep_all_refinement = false; // disable simplification of embedded mesh connectivity + bool show_failure = false; // show viewers for failed overlay constructions with Tutte mappings + + // used internally + bool use_consistency_check = false; // don't check path cyclic orientation consistency + + // -------------- + // Core Interface + // -------------- + + /** + * @brief Construct a new Simplification Overlay Mesh object to simplify the given mesh with an overlay + * + * @param V: mesh vertices + * @param vtx_reindex: map from halfedge to VF vertices + * @param mesh: halfedge mesh + */ + SimplificationOverlayMesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const Mesh& mesh); + + /** + * @brief Convert the overlay halfedge mesh into a mesh with metric inferred from the original metric + * + * @return mesh with intrinsic metric + */ + Mesh generate_mesh() const; + + // ----------- + // Frame Field + // ----------- + + /** + * @brief Set the field data from the original mesh + * + * @param reference_vector: per-face referecne vector field + * @param theta: per-face tangent rotation angles for the field relative to the reference + * @param period_jump: per-corner period jumps of the cross field across edges opposite the corner + */ + void set_original_field( + const Eigen::MatrixXd& reference_vector, + const Eigen::VectorXd& theta, + const Eigen::MatrixXi& period_jump); + + /** + * @brief Given a frame field on the original mesh, extend it to one on the refined overlay mesh + * + * @param F_orig: original mesh faces + * @param reference_vector: per-face referecne vector field + * @param theta: per-face tangent rotation angles for the field relative to the reference + * @param kappa: per-corner rotation of the reference frame field across edges opposite the corner + * @param period_jump: per-corner period jumps of the cross field across edges opposite the corner + * @return corresponding frame field on the VF mesh generated by generate_overlay_faces + */ + std::tuple + refine_frame_field( + const Eigen::MatrixXi& F_orig, + const Eigen::MatrixXd& reference_vector, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump); + + // --------------- + // Parametrization + // --------------- + + /** + * @brief Project a dual loop on the overlay loop to an equivalent simple loop on the + * embedded mesh such that every three consecutive elements are distinct + * + * @param dual_loop: dual loop of faces on the overlay mesh + * @param overlay: embedded mesh connectivity + * @return dual loop on the embedded mesh + */ + std::vector project_dual_loop( + const std::vector& dual_loop, + const OverlayStructure& overlay) const; + + + /** + * @brief Generate a parametrization for the simplified embedded mesh satisfying seamless constraints + * + * WARNING: assumes a valid simplified connectivity is initialized + * + * @param homology_basis_loops: dual loops on the simplified mesh determining a homology basis + * @param kappa: target holonomy angles on the homology basis + * @return 3D mesh vertices with interpolated intersection points for refined edges + * @return overlay mesh with intrinsic metric determining the parametrization + */ + std::tuple> + generate_simplified_parameterization( + const std::vector>& homology_basis_loops, + const std::vector& kappa); + + // ----------------------- + // Barycentric Coordinates + // ----------------------- + + /** + * @brief Get the barycentric coordinates of the tip of an overlay halfedge in an embedded + * mesh face. + * + * @param halfedge_index: overlay mesh halfedge index + * @param face_index: face index in the embedded mesh + * @param overlay: overlay structure containing the embedded face + * @return barycentric coordinates of the tip in the face of the embedded mesh + */ + BaryCoords tip_face_coordinates( + int halfedge_index, + int face_index, + const OverlayStructure& overlay) const; + + /** + * @brief Get the barycentric coordinates of the tip of an overlay halfedge in an embedded + * mesh edge. + * + * @param halfedge_index: overlay mesh halfedge index + * @param edge_index: edge index in the embedded mesh + * @param overlay: overlay structure containing the embedded edge + * @return barycentric coordinates of the tip in the edge of the embedded mesh + */ + BaryCoords tip_edge_coordinates( + int halfedge_index, + int edge_index, + const OverlayStructure& overlay) const; + + /** + * @brief Compute the barycentric coordiantes of the tip of the segment halfedge in the face containing it + * + * @param halfedge_index: index of an segment halfedge + * @return barycentric 2D coordinates of the vertex in the face + */ + Vector2 project_halfedge_tip(int halfedge_index) const; + + /** + * @brief Given an edge coordinate in an overlay segment halfedge, get the corresponding coordinate + * in the original embedding mesh edge containing it. + * + * WARNING: assumes the halfedge in contained in an original edge + * + * @param halfedge_index: overlay mesh halfedge + * @param overlay_edge_coord: edge barycentric coordinate in the overlay edge + * @return edge barycentric coordinates in the original embedded mesh + */ + BaryCoords convert_overlay_to_original_edge_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const; + + /** + * @brief Given an edge coordinate in an overlay segment halfedge, get the corresponding coordinate + * in the original embedding mesh face containing it. + * + * WARNING: assumes the halfedge in contained in an original face interior + * + * @param halfedge_index: overlay mesh halfedge + * @param overlay_edge_coord: edge barycentric coordinate in the overlay edge + * @return face barycentric coordinates in the original embedded mesh + */ + BaryCoords convert_overlay_to_original_face_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const; + + /** + * @brief Given an edge coordinate in an overlay segment halfedge, get the corresponding coordinate + * in the original embedding mesh face or edge containing it. + * + * WARNING: the type of barycentric coordinate is inferred from the parent type of the overlay edge + * + * @param halfedge_index: overlay mesh halfedge + * @param overlay_edge_coord: edge barycentric coordinate in the overlay edge + * @return face or edge barycentric coordinates in the original embedded mesh + */ + BaryCoords convert_overlay_to_original_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const; + + // ------------ + // Edge Lengths + // ------------ + + /** + * @brief Compute the length of the segment halfedge in the overlay mesh + * + * @param halfedge_index: index of an segment halfedge + * @return length of the segment + */ + Scalar halfedge_length(int halfedge_index) const; + + /** + * @brief Compute the intrinsic length of the simplified halfedge beginning at the given index + * + * @param halfedge_index: overlay halfedge at the start of a simplified halfedge path + * @return total length of the simplified halfedge path + */ + Scalar compute_simplified_halfedge_length(int halfedge_index) const; + + /** + * @brief Compute the distance between the simplified halfedge endpoints in 3D + * + * WARNING: this distance can be zero + * + * @param halfedge_index: simplified mesh halfedge index + * @return extrinsic edge length + */ + Scalar compute_extrinsic_halfedge_length(int halfedge_index) const; + + // Compute the Penner coordinates for the simplified mesh using intrinsic lengths + VectorX compute_simplified_penner_coordinates(); + + // Compute the Penner coordinates for the simplified mesh using extrinsic lengths + VectorX compute_extrinsic_penner_coordinates(); + + // Set the lengths of the simplified mesh using intrinsic lengths + void set_simplified_halfedge_lengths(); + + // Set the lengths of the simplified mesh using extrinsic lengths + void set_simplified_halfedge_extrinsic_lengths(); + + // Set the lengths of the simplified mesh using extrinsic lengths in weighted curvature space + void set_simplified_halfedge_curvature_lengths(Scalar normal_scale); + + // Set any zero lengths to an epsilon and interpolate toward uniform if triangle inequality is invalidated + void correct_simplified_halfedge_lengths(); + + // ------------------ + // Embedding Geometry + // ------------------ + + // Set the barycentric coordinates for the nodes of a given edge to uniform values + void set_uniform_edge_coordinates(int start_halfedge, OverlayStructure& overlay); + + // Set the barycentric coordinates for all nodes of a given edge to uniform values + void set_uniform_edge_coordinates(OverlayStructure& overlay); + + // Set barycentric coordinates for the interior nodes using Tutte maps + void find_tutte_coordinates(OverlayStructure& overlay, bool use_adaptive_weight=false); + + // Set barycentric coordinates for the interior nodes of marked faces using Tutte maps + void find_tutte_coordinates(OverlayStructure& overlay, std::vector& use_face, bool use_adaptive_weight=false); + + // Compute Floater mean-value weights for each directed overlay edge in face coordinates + std::vector compute_mean_value_weights(const OverlayStructure& overlay) const; + + // ------------------ + // Embedding Connectivity + // ------------------ + + // Set face parents of an overlay triangulation with a valid edge network using DFS + void flood_faces(); + + // Set face parents of an overlay triangulation with a valid edge network using Union Find + void flood_faces_union_find(); + + // Set the face parent of a given face + // WARNING: inefficient + void flood_face(int h, int f_parent); + + // Get map from overlay halfedges to segments at the start of their path + std::vector find_start_halfedges(const OverlayStructure& overlay) const; + + std::vector find_overlay_vertices(const OverlayStructure& overlay) const; + + // Get map from overlay halfedges to segments at the end of their path + std::vector find_end_halfedges(const OverlayStructure& overlay) const; + + /** + * @brief Get list of overlay halfedges starting from the given segment until a vertex is found + * + * @param halfedge_index: overlay halfedge index + * @param overlay: embedded mesh overlay + * @return embedded mesh halfedges starting at the input halfedge and ending at an embedded vertex + */ + std::vector enumerate_halfedges(int halfedge_index, const OverlayStructure& overlay) const; + + /** + * @brief Get list of all the simplified mesh edge embeddings, represented by their vertex paths + * + * WARNING: uses reduced vertex indexing with deleted elements removed + * + * @return list of mesh edge embedding vertices + */ + std::vector> enumerate_simplified_edges() const; + + /** + * @brief Get list of all the simplified mesh edge embeddings, represented by the overlay halfedge indices + * + * WARNING: uses reduced halfedge indexing with deleted elements removed + * + * @return list of mesh edge embedding halfedges + */ + std::vector> enumerate_simplified_halfedges() const; + + + /** + * @brief Determine whether an overlay segment index is a valid starting point for tracing an + * embedded simplified mesh edge while preserving the cyclic orientation at the base vertex of + * the already traced embedded edges + * + * @param hij: overlay mesh halfedge + * @param halfedge_index: simplified mesh halfedge index to trace + * @return true iff the overlay mesh edge is a valid starting edge for the simplified mesh embedding + */ + bool is_start_cyclic_orientation( + int hij, + int halfedge_index) const; + + /** + * @brief Determine whether an overlay segment index is a valid ending point for tracing an + * embedded simplified mesh edge while preserving the cyclic orientation at the tip vertex of + * the already traced embedded edges + * + * @param hij: overlay mesh halfedge + * @param halfedge_index: simplified mesh halfedge index to trace + * @return true iff the overlay mesh edge is a valid ending edge for the simplified mesh embedding + */ + bool is_end_cyclic_orientation( + int hij, + int halfedge_index) const; + + // ------------------ + // Overlay Navigation + // ------------------ + + /** + * @brief Given an triangulation edge segment, get the next edge segment in the triangulation + * + * @param halfedge_index: segment overlay index + * @param overlay: embedded mesh overlay + * @return next overlay edge + */ + int get_next_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const; + + /** + * @brief Rotate ccw around the base vertex of a halfedge until an overlay halfedge is found + * + * @param halfedge_index: overlay halfedge index + * @param overlay: embedded mesh overlay + * @return halfedge of the next ccw halfedge in an embedded edge of the overlay + */ + int get_ccw_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const; + + /** + * @brief Rotate clockwise around the base vertex of a halfedge until an overlay halfedge is found + * + * @param halfedge_index: overlay halfedge index + * @param overlay: embedded mesh overlay + * @return halfedge of the next ccw halfedge in an embedded edge of the overlay + */ + int get_clockwise_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const; + + // -------------- + // Robust Cleanup + // -------------- + + /** + * @brief Determine if the embedded edge starting at a halfedge contains degenerate segments + * + * @param start_halfedge: overlay halfedge at the start of the embedded edge + * @param overlay: overlay structure containing the embedded edge to check + * @return true iff the edge contains no degenerate segments + */ + bool is_degenerate_edge_coordinates(int start_halfedge, OverlayStructure& overlay); + + /** + * @brief Fix all degenerate edge coordinates in an overlay structure using a simple interpolation + * heuristic + * + * @param overlay: overlay structure to correct + */ + void fix_degenerate_edge_coordinates(OverlayStructure& overlay); + + /** + * @brief Fix all degenerate edge coordinates in an embedded edge of an overlay structure + * using a simple interpolation heuristic + * + * @param start_halfedge: overlay halfedge at the start of the embedded edge + * @param overlay: overlay structure to correct + */ + void fix_degenerate_edge_coordinates(int start_halfedge, OverlayStructure& overlay); + + /** + * @brief Triangulate all faces of the overlay mesh, first fixing degenerate edges and using + * Tutte's embedding to remesh degenerate faces of the original mesh embedding. + * + */ + void triangulate_with_tutte(); + + // -------------- + // Simplification + // -------------- + + /** + * @brief Use heuristics to remove excess refinement that is not needed for a nondegenerate + * embedding of the original and simplified meshes. + * + */ + void remove_excess_refinement(); + + /** + * @brief Check if the edge is an interior face edge in both embeddings that can be safely removed. + * + * @param halfedge_index: overlay segment index to check + * @return true iff the edge is an interior face edge in both embeddings + */ + bool is_edge_removable(int halfedge_index); + + /** + * @brief Check if the edge is an interior face vertex in both embeddings that can be safely removed. + * + * @param vertex_index: overlay vertex index to check + * @return true iff the edge is an interior vertex edge in both embeddings + */ + bool is_vertex_removable(int vertex_index); + + /** + * @brief Determine if collapsing a given halfedge in a simplified edge path results in a valid + * straightening of the embedded edge path. + * + * The collapse is valid if: + * - the tip of the halfedge is not an embedded vertex + * - the halfedge and the next segment in the edge path are in the same original edge path + * - the halfedge and the next segment in the edge path are in the same face, and the endpoints + * of the straightened edge are not embedded in the same original edge path (which necessarily + * results in a degenerate face) + * + * @param halfedge_index: overlay segment index to check + * @return true iff the halfedge can be collapsed + */ + bool is_straightening_collapse(int halfedge_index); + + // ---------------------------- + // Embedded Geometry Generation + // ---------------------------- + + Eigen::Vector3d generate_overlay_vertex( + int vi, + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const OverlayStructure& overlay) const; + + Eigen::MatrixXd generate_overlay_vertices( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const OverlayStructure& overlay) const; + + std::tuple generate_overlay_faces() const; + + Eigen::MatrixXi generate_overlay_faces_simple() const {return std::get<0>(generate_overlay_faces());} + + std::tuple + generate_simplified_edges( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + std::tuple generate_simplified_vf_mesh() const; + + // --------------------- + // Regularity Conditions + // --------------------- + + /** + * @brief Determine if a face is regular with respect to the embedded simplified mesh. + * + * Regularity means that there is no more than one embedded edge adjacent to the face. + * + * @param face_index: overlay face index + * @return true iff the face is regular + */ + virtual bool is_regular_face(int face_index) const; + + /** + * @brief Determine if an is regular with respect to the embedded simplified mesh. + * + * Regularity means that: + * - there is no more than one embedded edge or vertex adjacent to the edge + * - OR the edge itself is an embedded edge + * + * @param edge_index: overlay edge index + * @return true iff the edge is regular + */ + virtual bool is_regular_edge(int edge_index) const; + + /** + * @brief Determine if an overlay edge is combinatorically "parallel", meaning that + * its endpoints are contained in the same embedded edge (including its endpoints) + * + * A face containing a parallel edge cannot be mapped bijectively to the plane with + * straightened edges. + * + * Note that this is a very similar condition to three-connectedness of the face, but + * it allows edges with endpoints on the boundary if they intersect opposite edges of + * the face. + * + * @param edge_index: overlay edge index + * @param overlay: embedded mesh connectivity + * @return true iff the edge is parallel with respect to the embedded mesh + */ + bool is_parallel_edge(int edge_index, const OverlayStructure& overlay) const; + + /** + * @brief Determine if the mesh contains no parallel edges. + * + * @param overlay: embedded mesh connectivity + * @return true iff no edge is parallel with respect to the embedded mesh + */ + bool is_parallel_free(const OverlayStructure& overlay); + + /** + * @brief Determine if the faces of the embedded mesh are three connected. + * + * Note that this is equivalent to the mesh edge being regular with respect to the + * embedded mesh. + * + * @param overlay: embedded mesh connectivity + * @return true iff all embedded mesh faces are three connected + */ + bool is_three_connected(const OverlayStructure& overlay); + + /** + * @brief Make sure all edges are regular by spliting irregular edges + * + */ + void make_edge_regular(); + + /** + * @brief Make sure all faces are regular by adding a midpoint to irregular faces + * + */ + void make_face_regular(); + + /** + * @brief Make all edges and faces regular. + * + */ + void make_regular(); + + /** + * @brief Ensure all edges adjacent to interior vertices of a path of + * halfedges are regular. + * + * @param halfedges: path of halfedges in the overlay mesh + */ + void make_adjacent_regular(const std::vector& halfedges); + + /** + * @brief Ensure all edges adjacent to interior vertices on the lefthand side of + * a path of halfedges are regular. + * + * WARNING: assumes path of connected halfedges with consistent orientation + * + * @param halfedges: path of halfedges in the overlay mesh + */ + void make_oriented_adjacent_regular(const std::vector& halfedges); + + /** + * @brief Make sure there are no parallel edges in an embedded mesh by splitting + * such edges + * + * @param overlay: embedded mesh connectivity + */ + void remove_parallel_edges(const OverlayStructure& overlay); + + /** + * @brief Ensure the mesh is three connected by refining all face interior edges + * with endpoints on embedded edges or vertices. + * + * Note that this is equivalent to making the mesh edge regular with respect to the + * embedded mesh. + * + * @param overlay: embedded mesh connectivity + */ + void make_three_connected(const OverlayStructure& overlay); + + + std::vector compute_corner_angles() const; + std::vector compute_face_areas() const; + Scalar compute_polygon_face_area(int face_index) const; + + /** + * @brief Sets all edge segments and nodes starting from the current segment to be interior + * + * WARNING: segment and node parents are left undefined after this operation + * + * @param start_halfedge: overlay halfedge at the start of the embedded edge path to erase + * @param overlay: embedded mesh overlay + */ + void remove_path(int start_halfedge, OverlayStructure& overlay); + + + // ----------------- + // Atomic Operations + // ----------------- + + // need to override to handle additional bookkeeping + virtual int create_face() override; + virtual int create_edge() override; + virtual int create_vertex() override; + virtual int split_edge(int halfedge_index) override; + virtual int split_face(int h, int g) override; + virtual int join_edge_faces(int halfedge_index) override; + virtual int add_center_to_face(int face_index) override; + virtual bool triangulate_face(int face_index) override; + virtual int collapse_edge(int halfedge_index) override; + virtual void collect_garbage() override; + // void flip_ccw(int halfedge_index); TODO + + // --------------- + // Data Structures + // --------------- + + // original VF mesh used to construct the overlaid original halfedge mesh + Eigen::MatrixXd V_orig; + Eigen::MatrixXi F_orig; + std::vector vtx_reindex_orig; + + // field on original mesh + Eigen::MatrixXd reference_field_orig; + Eigen::VectorXd thetas_orig; + Eigen::VectorXi period_jumps_orig; + + /** + * @brief Data structure for a mesh embedded A in the overlay mesh B. + * + * Includes the mesh connectivity, the mappings from overlay mesh elements to the + * embedded mesh element containing them, and barycentric coordinates for vertices + * of the overlay mesh inside the embedded mesh edges or faces. + * + * Each vertex of B may be contained in a face, edge, or vertex of A. Likewise, each + * edge of B may be contained in a face or edge of A. + * + */ + struct OverlayStructure { + OverlayStructure(int num_vertices, int num_edges, int num_faces) + : face_parent(std::vector(num_faces, -1)) + , vertex_parent(std::vector(num_vertices, -1)) + , vertex_type(num_vertices, VertexType::VF) + , is_bary_init(false) + , bary_coords(num_vertices, {0., 0.}) + , halfedge_parent(std::vector(2 * num_edges, -1)) + , edge_type(num_edges, EdgeType::EF) + { + face_parent.reserve(5 * num_faces); + vertex_parent.reserve(5 * num_vertices); + vertex_type.reserve(5 * num_vertices); + bary_coords.reserve(5 * num_vertices); + halfedge_parent.reserve(5 * 2 * num_edges); + edge_type.reserve(5 * num_edges); + } + + OverlayStructure(const Mesh& mesh) + : face_parent(arange(mesh.n_faces())) + , vertex_parent(arange(mesh.n_vertices())) + , vertex_type(mesh.n_vertices(), VertexType::VV) + , is_bary_init(true) + , bary_coords(mesh.n_vertices(), {0., 0.}) + , halfedge_parent(arange(2 * mesh.n_edges())) + , edge_type(mesh.n_edges(), EdgeType::EE) + , vertex_index(arange(mesh.n_vertices())) + , m(mesh) + { + build_edge_maps(mesh.opp, h2e, e2h); + + int num_faces = mesh.n_faces(); + int num_vertices = mesh.n_vertices(); + int num_edges = mesh.n_edges(); + face_parent.reserve(5 * num_faces); + vertex_parent.reserve(5 * num_vertices); + vertex_type.reserve(5 * num_vertices); + bary_coords.reserve(5 * num_vertices); + halfedge_parent.reserve(5 * 2 * num_edges); + edge_type.reserve(5 * num_edges); + } + + std::vector face_parent; // face of M containing the given face + + std::vector vertex_parent; // face, edge, or vertex of M containing the given vertex + std::vector vertex_type; // label whether the vertex is in a vertex, edge, or face of M + + bool is_bary_init = false; // flag to store whether barycentric coordinates still need to be found + std::vector bary_coords; // barycentric coords of a vertex in the edge or face containing it (0 for unused coordinates) + + std::vector halfedge_parent; // face or edge of M containing the given halfedge + std::vector edge_type; // label whether the edge is in an edge or face of M + + Mesh m; // embedded mesh halfedge connectivity + std::vector h2e; // edge map for m + std::vector e2h; // edge map right inverse for m + + std::vector vertex_index; // map from vertices of the embedded mesh to the overlay mesh + + /** + * @brief Helper function to reindex overlay structure maps during overlay garbage collection. + * + * @param he_new2old: map from new halfedge indices to old indices + * @param v_new2old: map from new vertex indices to old indices + * @param e_new2old: map from new edge indices to old indices + * @param f_new2old: map from new face indices to old indices + */ + void collect_garbage( + const std::vector& he_new2old, + const std::vector& v_new2old, + const std::vector& e_new2old, + const std::vector& f_new2old + ) { + face_parent = vector_compose(face_parent, f_new2old); + vertex_parent = vector_compose(vertex_parent, v_new2old); + vertex_type = vector_compose(vertex_type, v_new2old); + halfedge_parent = vector_compose(halfedge_parent, he_new2old); + edge_type = vector_compose(edge_type, e_new2old); + if (!bary_coords.empty()) + { + bary_coords = vector_compose(bary_coords, v_new2old); + } + } + + /** + * @brief Find the barycentric coordinates of an overlay vertex in a given embedded edge + * + * For a vertex embedded in an endpoint vertex of the edge, returns 1 or 0 (depending on + * the edge orientation) + * + * WARNING: Assumes vertex is contained in the given edge (or its endpoint) + * + * @param vertex_index: vertex index in the overlay mesh + * @param edge_index: edge index in the embedded mesh + * @return barycentric coordinates in the edge (second coordinate always 0.) + */ + BaryCoords vertex_edge_coordinates( + int vertex_index, + int edge_index) const; + + /** + * @brief Find the barycentric coordinates of an overlay vertex in a given embedded face + * + * Also returns limiting barycentric coordinates for a vertex embedded in a boundary edge or vertex + * + * WARNING: Assumes vertex is contained in the given face (or its boundary) + * + * @param vertex_index: vertex index in the overlay mesh + * @param face_index: face index in the embedded mesh + * @return barycentric coordinates in the face + */ + BaryCoords vertex_face_coordinates( + int vertex_index, + int face_index) const; + + /** + * @brief Convert barycentric coordinates of a point in the boundary edge of a face + * to coordinates with respect to the given face. + * + * This method is generally necessary to properly resolve orientation. + * + * @param edge_index: edge index in the embedded mesh + * @param face_index: face index in the embedded mesh + * @param edge_coords: coordinates with respect to the embedded edge (second coordinate ignored) + * @return coordinates with respect to the embedded face + */ + BaryCoords convert_edge_to_face_coordinates( + int edge_index, + int face_index, + BaryCoords edge_coords) const; + + /** + * @brief Convert barycentric coordinates of a point in the boundary halfedge of a face + * to coordinates with respect to the given face. + * + * WARNING: This is very similar to convert_edge_to_face_coordinates + * + * @param halfedge_index: halfedge index in the embedded mesh + * @param face_index: face index in the embedded mesh + * @param edge_coords: coordinates with respect to the embedded edge (second coordinate ignored) + * @return coordinates with respect to the embedded face + */ + BaryCoords convert_halfedge_to_face_coordinates( + int halfedge_index, + int face_index, + BaryCoords edge_coords) const; + }; + + OverlayStructure orig; // original mesh with 3D geometry + OverlayStructure simp; // simplified mesh with parametrization geometry + + + // ------ + // Viewer + // ------ + + virtual void view() const; + + int enumerate_vertex_type(const VertexType& vertex_type) const; + + std::vector enumerate_vertex_type(const std::vector& vertex_type) const; + + void add_simplified_edges_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + void add_simplified_vertices_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + // -------------- + // Validity Tests + // -------------- + + bool is_valid_overlay(const OverlayStructure& overlay) const; + bool is_valid_edge_network() const; + bool is_valid() const override; + bool is_valid_simplified_vertex_index(int vertex_index) const; + bool is_valid_simplified_edge_index(int edge_index) const; + bool is_valid_simplified_face_index(int face_index) const; + bool is_valid_original_vertex_index(int vertex_index) const; + bool is_valid_original_edge_index(int edge_index) const; + bool is_valid_original_face_index(int face_index) const; + bool is_valid_halfedge_index(int halfedge_index) const; + bool is_valid_vertex_index(int vertex_index) const; + +protected: + + + // helper function for updating overlay data during edge split + void split_edge_overlay( + int hij, + int halfedge_index, + int edge_index, + OverlayStructure& overlay); + + // helper function for updating overlay data during face split + int split_face_overlay( + int e, + int face_index, + OverlayStructure& overlay); + + // helper function for updating overlay data during face join + void join_edge_faces_overlay( + int f, + int halfedge_index, + OverlayStructure& overlay); + + // helper function for updating overlay data while adding a face center + void add_center_to_face_overlay( + int vi, + int face_index, + OverlayStructure& overlay); + + // helper function for updating overlay data while collapsing an edge + void collapse_edge_overlay( + int hij, + int halfedge_index, + OverlayStructure& overlay); + + void add_mesh_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + void add_original_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + void add_overlay_to_viewer( + const std::string& mesh_handle, + const OverlayStructure& overlay, + const std::string& prefix) const; + +private: + // TODO: Not currently used + bool is_halfedge_collapsable(int halfedge_index); + std::vector get_face_parent(const OverlayStructure& overlay) const; + +}; + +// ---------------- +// Helper Functions +// ---------------- + +/** + * @brief Remove vertices not referenced in the edges + * + * TODO: move elsewhere + * + * @param V: vertex position matrix + * @param edges: list of edge endpoint pairs + * @return Vertex position matrix with unused vertices removed + * @return edges with new vertex indices + */ +std::tuple>> remove_unreferenced_vertices( + const Eigen::MatrixXd& V, + const std::vector>& edges); + +/** + * @brief Simplify a cycle to make it simple, meaning: + * - There are no repeated elements of the form AA (replaced with A) + * - There are no "tags" of the form ABA (replaced with A) + * More concisely, every subsequence of length three has no repeated elements + * + * WARNING: cycles of length 1 or 2 are always considered simple + * + * TODO: move elsewhere + * + * @param cycle: input generic cycle + * @return simplified cycle + */ +std::vector trim_cycle(const std::vector& cycle); + +/** + * @brief Compute edge lengths with adaptive normal weighting: + * d(v_i, v_j) = ||V_i - V_j|| + normal_scale ||N_i - N_j|| + * where N_i, N_j are the vertex normals at v_i, v_j + * + * @param V: mesh vertices + * @param F: mesh faces + * @param m: halfedge mesh + * @param vtx_reindex: map from halfedge to VF vertices + * @param normal_scale: scale for normal edge weighting (higher increases weights in high curvature regions) + * @return per-edge curvature adapted weights + */ +std::vector compute_curvature_adaptive_lengths( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Mesh& m, + const std::vector& vtx_reindex, + Scalar normal_scale +); + + +} +} diff --git a/include/resolution/resolution/hierarchy/simplifier.h b/include/resolution/resolution/hierarchy/simplifier.h new file mode 100644 index 0000000..e764ac2 --- /dev/null +++ b/include/resolution/resolution/hierarchy/simplifier.h @@ -0,0 +1,257 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/overlay.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" + + + +namespace Penner { +namespace Resolution { + +/** + * @brief Abstract class to handle simplification of an overlay mesh with both 3D and + * parametric data embedded in the surface to construct a VF mesh. + * + * The approach is bottom up, starting from the original triangulation and only refining + * edges as necessary to ensure the parametrization is locally injective + * + */ +class Simplifier +{ + public: + Mesh m; + std::vector is_cut_orig; + std::vector to; + std::vector he_uv; + + std::vector start_halfedge; + std::vector end_halfedge; + + std::vector is_removed_vertex; + std::vector is_refined_halfedge; + std::vector is_refined_face; + double area_threshold; + bool use_angles = true; + + Simplifier( + const Mesh& mesh, + const std::vector& is_cut, + const std::vector& to_, + const std::vector& he_uv_ + ) + : m(mesh) + , is_cut_orig(is_cut) + , to(to_) + , he_uv(he_uv_) + { + int num_halfedges = m.n_halfedges(); + int num_faces = m.h.size(); + is_refined_halfedge = std::vector(num_halfedges, false); + is_refined_face = std::vector(num_faces, false); + + // actually angle threshold + area_threshold = 1e-4; + } + + /** + * @brief Refine halfedges as necessary to ensure the parametrization does not have + * inverted triangles. + * + */ + void refine_halfedges(); + + /** + * @brief Attempt to remove inserted vertices from the parametrization without destroying + * local injectivity and inserting flipped triangles. + * + * WARNING: assumes refine_halfedges has already been called + * + * Note that this method uses a top-down heuristic for removing vertices and is not + * generally optimal + * + */ + void simplify_vertices(); + + /** + * @brief Get the vertices at the tips of the halfedges returned by get_halfedges. + * + * @param halfedge_index: embedded halfedge index + * @return list of vertex tips in the embedded halfedge + */ + std::vector get_halfedge_vertices(int halfedge_index) const; + + /** + * @brief Get the uv and 3D vertex indices on the boundary of the face with current + * refinement state. + * + * @param face_index: embedded face index + * @param uv_vertices: uv vertex indices on the boundary + * @param vertices: vertex indices on the boundary + */ + void get_face_vertex_indices( + int face_index, + std::vector& uv_vertices, + std::vector& vertices) const; + + /** + * @brief Get the overlay halfedges in the embedded halfedge + * + * @param halfedge_index: embedded halfedge index + * @return list of overlay halfedges in the embedded edge path + */ + virtual std::vector get_halfedges(int halfedge_index) const = 0; + + /** + * @brief Determine if the face is self overlapping and can be parametrized without + * inverted triangles. + * + * @param face_index: embedded face index + * @return true iff the face is self overlapping + */ + virtual bool is_face_self_overlapping(int face_index) const = 0; +}; + +class SimplificationOverlaySimplifier : public Simplifier +{ +public: + typedef SimplificationOverlayMesh::EdgeType EdgeType; + SimplificationOverlayMesh& parent; + Eigen::MatrixXd V_o; + + SimplificationOverlaySimplifier( + SimplificationOverlayMesh& overlay_mesh, + const std::vector& is_cut, + const std::vector& he_uv_ + ) + : parent(overlay_mesh) + , Simplifier(overlay_mesh.orig.m, is_cut, overlay_mesh.to, he_uv_) + { + V_o = parent.generate_overlay_vertices( + parent.V_orig, + parent.vtx_reindex_orig, + parent.orig); + start_halfedge = parent.find_start_halfedges(parent.orig); + end_halfedge = parent.find_end_halfedges(parent.orig); + is_removed_vertex = std::vector(parent.out.size(), false); + } + + // Override core functions + std::vector get_halfedges(int halfedge_index) const override; + bool is_face_self_overlapping(int face_index) const override; + + /** + * @brief Get the positions of the vertices on the boundary of the given face. + * + * @param face_index: embedded face index + * @param uv_vertices: uv vertex positions on the boundary + * @param vertices: vertex positions on the boundary + */ + void get_face_vertices( + int face_index, + std::vector& uv_vertices, + std::vector& vertices) const; + + /** + * @brief Triangulate the embedded mesh face (if possible) and get the subfaces + * + * @param face_index: embedded face index + * @param face_triangles: list of 3D sub-faces, defined by 3 vertex indices + * @param uv_face_triangles: list of uv sub-faces, defined by 3 vertex indices + * @param face_indices: list of sub-faces, using local sequential boundary indexing + * @return true if the face was triangulated, and false if not + */ + bool triangulate_face( + int face_index, + std::vector>& face_triangles, + std::vector>& uv_face_triangles, + std::vector>& face_indices) const; + + /** + * @brief Build the refined parametrized mesh with maps to the parent elements in the + * unrefined mesh. + * + * @return refined mesh vertices + * @return refined mesh faces + * @return refined mesh uv vertices + * @return refined mesh uv faces + * @return map from refined to original faces + * @return map from refined to original halfedges (indexed by opposite corner) + * @return mask for halfedges in original mesh (indexed by opposite corner) + * > + */ + std::tuple< + Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::MatrixXi, + std::vector, Eigen::MatrixXi, Eigen::MatrixXi + > + build_refined_mesh(); + + +private: + // experimental: may be buggy + void mark_cut_faces(const std::vector& is_cut_halfedge); + void view_parameterization(); +}; + +/** + * @brief Note currently used + * + */ +class OverlaySimplifier : public Simplifier +{ +public: + OverlayMesh m_o; + + OverlaySimplifier( + const OverlayMesh& m_o_, + const Mesh& m_, + const std::vector& is_cut, + const std::vector& he_uv_ + ) + : m_o(m_o_) + , Simplifier(m_, is_cut, m_o_.to, he_uv_) + { + is_removed_vertex = std::vector(m_o.out.size(), false); + + // find start and end halfedges + start_halfedge = std::vector(m.n_halfedges(), -1); + end_halfedge = std::vector(m.n_halfedges(), -1); + for (int hij = 0; hij < m_o.n_halfedges(); ++hij) + { + // only look for original edges + if (m_o.edge_type[hij] == CURRENT_EDGE) + continue; + + + int vi = m_o.to[m_o.opp[hij]]; + int vj = m_o.to[hij]; + int Hij = m_o.origin_of_origin[hij]; + if (m_o.edge_type[hij] == ORIGINAL_EDGE) + Hij = m_o.origin[hij]; + if (m_o.vertex_type[vi] == ORIGINAL_VERTEX) + { + start_halfedge[Hij] = hij; + } + if (m_o.vertex_type[vj] == ORIGINAL_VERTEX) + { + end_halfedge[Hij] = hij; + } + } + } + + void get_face_vertices( + int face_index, + std::vector& uv_vertices) const; + bool triangulate_face( + int face_index, + std::vector>& face_triangles, + std::vector>& uv_face_triangles) const; + + std::vector get_halfedges(int halfedge_index) const override; + bool is_face_self_overlapping(int face_index) const override; + + std::tuple, std::vector, std::vector> get_refined_mesh(); +}; + +} +} \ No newline at end of file diff --git a/include/resolution/resolution/hierarchy/voronoi.h b/include/resolution/resolution/hierarchy/voronoi.h new file mode 100644 index 0000000..2cfd49b --- /dev/null +++ b/include/resolution/resolution/hierarchy/voronoi.h @@ -0,0 +1,707 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" +#include "resolution/hierarchy/overlay.h" +#include "resolution/hierarchy/simplifier.h" +#include "conformal_ideal_delaunay/OverlayMesh.hh" +#include +#include +#include + +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" + +#include + +namespace Penner { +namespace Resolution { + +//SymDir::Parameters read_parameters(json& config); + +class VoronoiOverlayMesh : public SimplificationOverlayMesh +{ +public: + bool use_geodesic_refinement = false; // straighten paths to geodesics when adding parametrization refinement + bool simplify_parameterization = true; // simplify the parametrized mesh generated from the overlay + bool simplify_param_overlay = false; // simplify the parametrization connectivity before tracing new edges + bool show_trace_failure = false; // open viewer when a tracing operation fails + bool optimize_layout = false; // use uv optimization at the coarse level + std::string output_dir = ""; + std::string name = ""; + std::string symdir_json = "scripts/json/symdir.json"; + Scalar path_curvature_threshold = 0.; + Scalar infinity = 1e20; + + VoronoiOverlayMesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const Mesh& mesh); + + // --------------- + // Voronoi Regions + // --------------- + + /** + * @brief Grow Voronoi regions from the set of initial seeds using a multi-source Dijkstra, + * adding additional seeds as necessary to ensure the tiles are simply connected + * + * @param seeds: initial seed set + * @return true if succeeds + */ + bool grow_voronoi_regions(const std::vector& seeds); + + /** + * @brief Grow Voronoi regions from the set of initial seeds using a multi-source Dijkstra, + * adding additional seeds as necessary to ensure the tiles are simply connected and to create + * an approximately uniform sampling of vertices in the mesh. + * + * @param seeds: initial seed set + * @param ratio: target ratio of simplified to original vertices + * @param normal_scale: if positive, use curvature adaptive weights that prioritize high curvature regions + * @param dual_length_ratio: minimum allowed ratio between the sum of Voronoi boundary edges + */ + void grow_uniform_voronoi_regions( + std::vector& seeds, + Scalar ratio, + Scalar normal_scale=0., + Scalar dual_length_ratio=0.1); + + /** + * @brief Set a maximum voronoi distance, inserting additional seeds as necessary. + * + * @param max_distance: maximum allowed distance between seeds + */ + void set_max_voronoi_distance(Scalar max_distance); + + /** + * @brief Set a maximum voronoi distance for a subset of vertices, inserting additional + * seeds as necessary. + * + * @param max_distance: maximum allowed distance between seeds + * @param is_allowed: is vertex allowed as a seed vertex + */ + void set_constained_max_voronoi_distance( + Scalar max_distance, + const std::vector& is_allowed); + + /** + * @brief Set a maximum weighted voronoi distance, inserting additional seeds as necessary. + * + * @param max_distance: maximum allowed weighted distance between seeds + * @param weights: per-vertex weights to scaled distance by + */ + void set_weighted_max_voronoi_distance( + Scalar max_distance, + const VectorX& weights); + + /** + * @brief Add additional Voronoi seeds to avoid badly conditioned dual Delaunay edges + * + * @param dual_length_ratio: minimum allowed ratio between the sum of Voronoi boundary edges + * adjacent to a vertex relative to the entire perimeter + */ + void make_voronoi_uniform(Scalar dual_length_ratio); + + + //bool grow_voronoi_regions_recursive(); + //void make_voronoi_simple(); + + // ---------------- + // Voronoi Pipeline + // ---------------- + + /** + * @brief Build the basic voronoi connectivity from + * 1. split_voronoi_edges + * 2. split_voronoi_faces + * 3. find_voronoi_dual_edges + * 4. propagate_voronoi_vertices_to_faces + */ + void build_voronoi_connectivity(); + + /** + * @brief Split edges between vertex regions, and store the maximum distance of the inserted vertex + * to a neighboring seed + * + */ + void split_voronoi_edges(); + + /** + * @brief Split voronoi faces to connect split voronoi edges, marking the inserted edges as + * embedded Voronoi region edges. + * + * Also begins to build the Delaunay connectivity (dual to the Voronoi connectivity), excluding + * the opposite relations. + * + * WARNING: assumes split_voronoi_edges has been called + */ + void split_voronoi_faces(); + + /** + * @brief Label the Voronoi region boundary edges, and find the midpoints of these edges + * + * Also completes the Delaunay connectity by computing opposite halfedges, and copies it to the + * simplified mesh embedding overlay data + * + * WARNING: assumes split_voronoi_faces has been called + */ + void find_voronoi_dual_edges(); + + /** + * @brief Label faces with nearest voronoi seed. + * + * WARNING: assumes find_voronoi_dual_edges has been called + */ + void propagate_voronoi_vertices_to_faces(); + + // ----------------- + // Delaunay Pipeline + // ----------------- + + /** + * @brief Trace paths from voronoi region centers to voronoi boundary midpoints, marking + * traced edges as part of the simplified embedded edge. + * + */ + void trace_delaunay_edges(); + + /** + * @brief Iteratively remove and retrace delaunay edge paths, removing the restriction that + * they pass through the Voronoi boundary midpoint. + * + */ + void retrace_edges(); + + /** + * @brief Use an intrinsic geodesic flipping method to straighten embedded simplified edge paths. + * + */ + void straighten_edges(); + + // ----------- + // Frame Field + // ----------- + + /** + * @brief Sample field on an embedded connectivity at all faces of the overlay mesh + * + * @param reference_field_overlay: reference field on the faces of the embedded mesh + * @param thetas_overlay: field angle relative to the reference field on the faces of the embedded mesh + * @param period_jumps_overlay: period jump opposite corners of the embedded mesh + * @param overlay: embedded mesh connectivity + * @return sampled field data on the fine overlay connectivity + */ + std::tuple + sample_field_from_overlay( + const Eigen::MatrixXd& reference_field_overlay, + const Eigen::VectorXd& thetas_overlay, + const Eigen::VectorXi& period_jumps_overlay, + const OverlayStructure& overlay) const; + + /** + * @brief Comb cross field directions so the period jumps are only nontrivial on a given cut + * + * WARNING: assumes the cut is a cut to disk with all singularities on the cut + * + * @param is_overlay_cut: mask for overlay cut edges where period jumps are allowed + * @param start_halfedge_index: comb field starting from the face adjacent to this halfedge + * @param thetas: field angle relative to the reference field on the faces of the overlay mesh + * @param period_jumps: period jump opposite corners of the overlay mesh + * @param overlay: embedded overlay connectivity + */ + void comb_field_to_overlay_seam( + const std::vector is_overlay_cut, + int start_halfedge_index, + Eigen::VectorXd& thetas, + Eigen::VectorXi& period_jumps, + const OverlayStructure& overlay) const; + + /** + * @brief Compute the principal direction of a frame field defined an overlay mesh + * using the face planes of an embedded mesh. + * + * WARNING: assumes field directions are sampled from the embedded mesh geometry + * + * @param V_overlay: vertices of the embedded mesh + * @param F_overlay: faces of the embedded mesh + * @param reference_field: reference field on the faces of the overlay mesh + * @param thetas: field angles on the faces of the overlay mesh + * @param overlay: embedded mesh connectivity + * @return principal field directions on the overlay mesh + */ + Eigen::MatrixXd compute_overlay_principal_direction( + const Eigen::MatrixXd& V_overlay, + const Eigen::MatrixXi& F_overlay, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& thetas, + const OverlayStructure& overlay) const; + + /** + * @brief Average field directions on the faces of the overlay mesh to the parent + * face of an embedded mesh. + * + * The average is area weighted, and projected orthogonally onto the tangent plane + * of the embedded mesh + * + * @param V: embedded mesh vertices + * @param F: embedded mesh faces + * @param field_direction: per-overlay-face principal field direction + * @param overlay: embedded mesh connectivity + * @return averaged principal field direction on the embedded mesh (i.e.., u gradient) + * @return rotated principal field direction (i.e., v gradient) + */ + std::tuple average_overlay_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& field_direction, + const OverlayStructure& overlay) const; + + /** + * @brief Average field directions on the faces of the overlay mesh to the parent + * face of the simplified mesh embedded mesh. + * + * The average is area weighted, and projected orthogonally onto the tangent plane + * of the embedded mesh + * + * @param field_direction: per-overlay-face principal field direction + * @return averaged principal field direction on the embedded mesh (i.e.., u gradient) + * @return rotated principal field direction (i.e., v gradient) + */ + std::tuple transfer_overlay_field_to_simplified( + const Eigen::MatrixXd& field_direction + ) const; + + /** + * @brief Sample the principal directions of the original field on the current + * overlay mesh, combed using a cut on the simplified mesh + * + * @return combed field direction on the overlay mesh + */ + Eigen::MatrixXd transfer_original_field_to_overlay() const; + + // --------------- + // Parametrization + // --------------- + + /** + * @brief Generate a parametrization on the simplified connectivity + * + * @param homology_basis_loops + * @param kappa + */ + void parameterize_with_overlay( + const std::vector>& homology_basis_loops, + const std::vector& kappa); + + /** + * @brief Optimize the parametrization associated with the simplified embedded mesh. + * + * Uses the UV optimization parameter json file at symdir_json. + * + * WARNING: assumes initial simplified parametrization has been computed + * + */ + void optimize_simplified_parameterization(); + + /** + * @brief Create a geometric embedding for the simplified connectivity using Tutte maps, + * and use it to transfer the parametrization on the simplified mesh to the overlay connectivity + * + * Optional side effects include: + * - straightening geodesic paths + * - refining to ensure the embedded meshes are three connected + * - rebuilding the original mesh embedding with harmonic maps + * + * + * If simplify_parametrization is enabled, the overlay mesh is simplified after the + * parametrization is transferred + * + * @return overlay vertices + * @return overlay faces + * @return overlay uv vertices + * @return overlay parametrization faces + * @return map from overlay to original faces + * @return map from overlay to original corners + * @return per-corner mask of original mesh edges + */ + std::tuple< + Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::MatrixXi, + std::vector, Eigen::MatrixXi, Eigen::MatrixXi> + transfer_simplified_parameterization(); + + // ----------------- + // Atomic Operations + // ----------------- + + // need to override to handle additional bookkeeping + virtual int create_face() override; + virtual int create_edge() override; + virtual int create_vertex() override; + virtual int split_edge(int halfedge_index) override; + virtual int split_face(int h, int g) override; + virtual int add_center_to_face(int face_index) override; + virtual bool is_regular_face(int face_index) const override; + virtual bool is_regular_edge(int edge_index) const override; + virtual void view() const override; + virtual void collect_garbage() override; + + // --------------- + // Data Structures + // --------------- + + /** + * @brief Data structure for discrete voronoi regions (dual to a Delauany triangulation) + * + * The voronoi regions are rooted at primal vertex seeds, and are composed of unions of + * vertex regions (which ensures their dual has triangular faces) + * + */ + struct VoronoiStructure { + Scalar infinity = 1e20; + Scalar max_distance = infinity; + + VoronoiStructure(int num_vertices, int num_edges, int num_faces) + : face_region(num_faces, -1) + , edge_region(num_edges, -1) + , edge_type(num_edges, EdgeType::EF) + , vertex_region(num_vertices, -1) + , vertex_type(num_vertices, VertexType::VF) + , distance(num_vertices, infinity) + , dual_halfedge(2 * num_edges, -1) + {} + + void reset() + { + int num_faces = face_region.size(); + int num_edges = edge_region.size(); + int num_vertices = vertex_region.size(); + face_region = std::vector(num_faces, -1); + edge_region = std::vector(num_edges, -1); + edge_type = std::vector(num_edges, EdgeType::EF); + vertex_region = std::vector(num_vertices, -1); + vertex_type = std::vector(num_vertices, VertexType::VF); + distance = std::vector(num_vertices, infinity); + dual_halfedge = std::vector(2 * num_edges, -1); + mesh = Connectivity(); + dual_vertices = {}; + } + + void collect_garbage( + const std::vector& he_new2old, + const std::vector& v_new2old, + const std::vector& e_new2old, + const std::vector& f_new2old, + const std::vector& he_old2new, + const std::vector& v_old2new + ) { + face_region = vector_compose(face_region, f_new2old); + edge_region = vector_compose(edge_region, e_new2old); + edge_type = vector_compose(edge_type, e_new2old); + vertex_region = vector_compose(vertex_region, v_new2old); + vertex_type = vector_compose(vertex_type, v_new2old); + distance = vector_compose(distance, v_new2old); + dual_halfedge = vector_compose(dual_halfedge, he_new2old); + + dual_vertices = vector_compose(v_old2new, dual_vertices); + dual_halfedge_starts = vector_compose(he_old2new, dual_halfedge_starts); + dual_edge_midpoints = vector_compose(v_old2new, dual_edge_midpoints); + } + + std::vector face_region; // face of M containing the given face + std::vector edge_region; // face or edge of M containing the given edge + std::vector edge_type; // label whether the edge is in an edge or face of M + std::vector vertex_region; // face, edge, or vertex of M containing the given vertex + std::vector vertex_type; // label whether the vertex is interior, boundary, or intersection + std::vector distance; // distance from seed + std::vector dual_halfedge; // index for dual halfedge edges + + std::vector dual_vertices; + std::vector dual_halfedge_starts; + std::vector dual_edge_midpoints; + + Connectivity mesh; + }; + + + /** + * @brief Utility class to trace paths on a voronoi mesh between two vertices + * + */ + class PathTracer + { + public: + VoronoiOverlayMesh& parent; + PathTracer(VoronoiOverlayMesh& voronoi) + : parent(voronoi) {} + + bool trace_path( + int first_vertex, + int second_vertex, + int halfedge_index, + int edge_index, + std::vector& halfedges, + int face_index); + + typedef std::pair WeightedVertex; + const Scalar inf_cost = 1e20; + bool show_failure = true; + // Initialize the stack of vertices to process with all vertices + struct VertexCompare + { + bool operator() (const WeightedVertex& left, const WeightedVertex& right) + { + return left.second > right.second; + }; + }; + typedef std::priority_queue, VertexCompare> PriorityQueue; + + private: + + virtual void initialize() {} + virtual void clean() {} + virtual void set_vertex_cost(int vertex, Scalar value) = 0; + virtual Scalar get_vertex_cost(int vertex) = 0; + virtual void set_halfedge_from_vertex(int vertex, int halfedge) = 0; + virtual int get_halfedge_from_vertex(int vertex) = 0; + virtual void add_vertex_to_queue(int vertex) = 0; + virtual WeightedVertex get_vertex_to_process() = 0; + virtual bool is_vertex_queue_empty() = 0; + virtual bool is_processed_vertex(int vertex) = 0; + virtual void mark_processed_vertex(int vertex) = 0; + }; + + class PathTracerList : public PathTracer + { + public: + PathTracerList(VoronoiOverlayMesh& voronoi) + : PathTracer(voronoi) + {} + + private: + std::vector m_is_processed_vertex; + std::vector m_halfedge_from_vertex; + std::vector m_vertex_cost; + PriorityQueue m_vertices_to_process; + + void initialize() override; + + void set_vertex_cost(int vertex, Scalar value) override + { + m_vertex_cost[vertex] = value; + } + + Scalar get_vertex_cost(int vertex) override + { + return m_vertex_cost[vertex]; + } + + void set_halfedge_from_vertex(int vertex, int halfedge) override + { + m_halfedge_from_vertex[vertex] = halfedge; + } + + int get_halfedge_from_vertex(int vertex) override + { + return m_halfedge_from_vertex[vertex]; + } + + void add_vertex_to_queue(int vertex) override + { + m_vertices_to_process.push(std::make_pair(vertex, get_vertex_cost(vertex))); + } + + WeightedVertex get_vertex_to_process() override + { + WeightedVertex weighted_vertex = m_vertices_to_process.top(); + m_vertices_to_process.pop(); + return weighted_vertex; + } + + bool is_vertex_queue_empty() override + { + return m_vertices_to_process.empty(); + } + + bool is_processed_vertex(int vertex) override + { + return m_is_processed_vertex[vertex]; + } + + void mark_processed_vertex(int vertex) override + { + m_is_processed_vertex[vertex] = true; + } + + }; + + class PathTracerCommonList : public PathTracer + { + public: + PathTracerCommonList(VoronoiOverlayMesh& voronoi) + : PathTracer(voronoi) + , m_is_processed_vertex(0) + , m_halfedge_from_vertex(0) + , m_vertex_cost(0) + , m_modified_vertices(0) + , m_vertices_to_process() + {} + + private: + std::vector m_is_processed_vertex; + std::vector m_halfedge_from_vertex; + std::vector m_vertex_cost; + PriorityQueue m_vertices_to_process; + std::vector m_modified_vertices; + + void initialize() override; + void clean() override; + + void set_vertex_cost(int vertex, Scalar value) override + { + m_vertex_cost[vertex] = value; + } + + Scalar get_vertex_cost(int vertex) override + { + return m_vertex_cost[vertex]; + } + + void set_halfedge_from_vertex(int vertex, int halfedge) override + { + m_halfedge_from_vertex[vertex] = halfedge; + } + + int get_halfedge_from_vertex(int vertex) override + { + return m_halfedge_from_vertex[vertex]; + } + + void add_vertex_to_queue(int vertex) override + { + m_vertices_to_process.push(std::make_pair(vertex, get_vertex_cost(vertex))); + m_modified_vertices.push_back(vertex); + } + + WeightedVertex get_vertex_to_process() override + { + WeightedVertex weighted_vertex = m_vertices_to_process.top(); + m_vertices_to_process.pop(); + return weighted_vertex; + } + + bool is_vertex_queue_empty() override + { + return m_vertices_to_process.empty(); + } + + bool is_processed_vertex(int vertex) override + { + return m_is_processed_vertex[vertex]; + } + + void mark_processed_vertex(int vertex) override + { + m_is_processed_vertex[vertex] = true; + } + + }; + + VoronoiStructure voronoi; + PathTracerCommonList path_tracer; + std::vector he_uv; + std::vector he_length; + std::vector u_simp, v_simp; + std::vector is_cut_simp; + + // vertices for simplified mesh overaly + Eigen::MatrixXd simplified_V; + std::vector PD1_o; + std::vector PD2_o; + + /** + * @brief Trace an embedded edge path from the first vertex to the second, marking the + * parent edge and halfedge of the traced edges on the path + * + * @param first_vertex: start vertex of the path + * @param second_vertex: end vertex of the path + * @param halfedge_index: halfedge parent for halfedges in the path + * @param edge_index: edge parent for edges in the path + * @param halfedges: output list of halfedges (in order) in the path + * @param face_index: (optional) if positive, only allow edges interior to the given simplified face index + * @return true iff the path is successfully traced + */ + bool trace_path( + int first_vertex, + int second_vertex, + int halfedge_index, + int edge_index, + std::vector& halfedges, + int face_index=-1); + + /** + * @brief Generate centers of dual faces, where three voronoi regions meet + * + * @return list of faces in the mesh where three voronoi regions meet + */ + std::vector generate_dual_faces() const; + + std::tuple integrate_simplified_function_onto_original( + const VectorX& simplified_function); + + + std::tuple + generate_voronoi_boundary( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + std::vector compute_layout(); + + //std::tuple, std::vector, std::vector> compute_length_layout(); + + // -------------- + // Viewer Helpers + // -------------- + + void add_voronoi_to_viewer( + const std::string& mesh_handle, + const VoronoiStructure& voronoi_structure) const; + void add_voronoi_edges_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + void add_voronoi_points_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const; + + nlohmann::json log; +private: + + igl::Timer timer; + + // helpers for the voronoi structure in complex operations + void split_edge_voronoi( + int halfedge_index, + int edge_index, + VoronoiStructure& voronoi_structure); + int split_face_voronoi( + int e, + int face_index, + VoronoiStructure& voronoi_structure); + void add_center_to_face_voronoi( + int vertex_index, + int face_index, + VoronoiStructure& voronoi_structure); + + // Warning: buggy + void parameterize_with_layout( + const std::vector>& homology_basis_loops, + const std::vector& kappa + ); + +}; + + + +} +} diff --git a/include/resolution/resolution/interface.h b/include/resolution/resolution/interface.h new file mode 100644 index 0000000..2515fbd --- /dev/null +++ b/include/resolution/resolution/interface.h @@ -0,0 +1,12 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" + +namespace Penner { +namespace Resolution { + + + +} // namespace Resolution +} // namespace Penner \ No newline at end of file diff --git a/include/resolution/resolution/optimization/compression.h b/include/resolution/resolution/optimization/compression.h new file mode 100644 index 0000000..9391a3d --- /dev/null +++ b/include/resolution/resolution/optimization/compression.h @@ -0,0 +1,69 @@ +#include "resolution/core/common.h" + +namespace Penner { +namespace Resolution { + +std::tuple build_grid_mesh( + const GeometryData& x, + const GeometryData& y, + const GeometryData& z); + +std::tuple, std::vector, std::vector> +build_grid_halfedge( + const GeometryData& x, + const GeometryData& y, + const GeometryData& z); + +std::tuple read_geometry_image( + const std::string& image_path, + std::string filetype="png"); + + +void write_geometry_image( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless = true, + float quality = 90.0f, + std::string filetype="png"); + +#if USE_JXL +std::tuple read_geometry_image_jxl( + const std::string& image_path); + +// Write a geometry image as JPEG XL. By default the image is encoded +// losslessly; pass lossless=false to use lossy compression at the given +// quality (0..100, higher is better quality). If geometry_safe is true +// (the default), the encoder avoids color/perceptual assumptions that only +// hold for photographic RGB data (XYB/YCoCg color transforms, Gaborish, +// EPF, dots, and patches), since the channels here store independent x/y/z +// coordinates rather than correlated image color channels. +void write_geometry_image_jxl( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless = true, + float quality = 90.0f, + bool geometry_safe = true); +#endif + +#if USE_EXR +std::tuple read_geometry_image_exr( + const std::string& image_path); + +// Write a geometry image as OpenEXR. By default the image is encoded +// losslessly (ZIP compression); pass lossless=false to use lossy DWAA +// compression at the given quality (0..100, higher is better quality). +void write_geometry_image_exr( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless = true, + float quality = 90.0f); +#endif + +} +} diff --git a/include/resolution/resolution/optimization/field.h b/include/resolution/resolution/optimization/field.h new file mode 100644 index 0000000..1145ee4 --- /dev/null +++ b/include/resolution/resolution/optimization/field.h @@ -0,0 +1,137 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/voronoi.h" +#include "field/intrinsic_field.h" +#include "field/frame_field.h" + +namespace Penner { +namespace Resolution { + +/** + * @brief Generate an overlay mesh with a uniformly sampled uniform triangulation + * + * @param V: original mesh vertices + * @param F: original mesh faces + * @param ratio: approximate ratio of simplified to original mesh vertices + * @return overlay mesh with both original and simplified embedded meshes + */ +VoronoiOverlayMesh generate_uniform_triangulation( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Scalar ratio = 1.); + +/** + * @brief Given an overlay mesh with a simplified mesh, construct an intrinsic + * cross field on the simplified geometry + * + * @param voronoi_mesh: overlay mesh + * @param V_simp: vertices of the embedded simplified mesh + * @param F_simp: faces of the embedded simplified mesh + * @param field_params: parameters for the field generation + * @return intrinsic cross field on the simplified mesh + */ +Field::IntrinsicNRosyField compute_simplified_field( + const VoronoiOverlayMesh& voronoi_mesh, + const Eigen::MatrixXd& V_simp, + const Eigen::MatrixXi& F_simp, + const Field::FieldParameters& field_params); + +/** + * @brief Given an overlay mesh and a rotation form on the simplified embedded mesh, + * generate a topologyically equivalent intrinsic field on the original mesh + * + * WARNING: the per-face directions are not yet initialized (since they are irrelevant for topology) + * + * @param voronoi_mesh: overlay mesh + * @param rotation_form_simp: rotation form on the simplified geometry + * @param field_generator: output field on the original geometry + */ +void lift_simplified_field_to_original( + const VoronoiOverlayMesh& voronoi_mesh, + const VectorX& rotation_form_simp, + Field::IntrinsicNRosyField& field_generator); + +/** + * @brief Given an overlay mesh and an intrinsic cross field on the simplified mesh, + * generate sampled field directions on the faces of the overlay mesh + * + * @param voronoi_mesh: overlay mesh + * @param field_generator: field on the simplified embedded mesh + * @param V_simp: vertices of the embedded simplified mesh + * @param F_simp: faces of the embedded simplified mesh + * @return sampled field directions on the overlay geometry + */ +Eigen::MatrixXd lift_simplified_directions_to_overlay( + const VoronoiOverlayMesh& voronoi_mesh, + const Field::IntrinsicNRosyField& field_generator, + const Eigen::MatrixXd& V_simp, + const Eigen::MatrixXi& F_simp); + +/** + * @brief Given an overlay mesh and a field on the simplified embedded mesh, + * generate a topologically equivalent intrinsic field on the original mesh + * with approximately equal field directions. + * + * @param voronoi_mesh: overlay mesh + * @param vtx_reindex: map from halfedge to VF vertices + * @param V: original mesh vertices + * @param F: original mesh faces + * @param rotation_form: rotation form on the simplified geometry + * @param reference_field_simp: per-face referecne vector field on the simplified mesh + * @param theta_simp: per-face tangent rotation angles for the field relative to the reference + * @param kappa_simp: per-corner rotation of the reference frame field across edges opposite the corner + * @param period_jump_simp: per-corner period jumps of the cross field across edges opposite the corner + * @param smooth_iters: (optional) number of iterations of field direction smoothing + * @return frame field on the original mesh + */ +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi +> +lift_simplified_field_to_original( + const VoronoiOverlayMesh& voronoi_mesh, + const std::vector& vtx_reindex, + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXd& rotation_form, + const Eigen::MatrixXd& reference_field_simp, + const Eigen::VectorXd& theta_simp, + const Eigen::MatrixXd& kappa_simp, + const Eigen::MatrixXi& period_jump_simp, + int smooth_iters=10); + +void comb_field_to_cut( + const Mesh& m, + const std::vector is_cut, + Eigen::VectorXd& thetas, + Eigen::VectorXi& period_jumps); + +void lift_basis_loops( + const VoronoiOverlayMesh& voronoi_mesh, + const MarkedPennerConeMetric& marked_metric, + const VectorX& rotation_form, + std::vector>& basis_loops, + std::vector& kappa); + +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +refine_overlay_field( + const VoronoiOverlayMesh& voronoi_mesh, + const std::vector& vtx_reindex, + const Eigen::MatrixXi& F, + const std::vector& fn_to_f_o, + const Eigen::MatrixXi& hn_to_h_o, + const Eigen::MatrixXi& is_edge_o, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump); + +} // namespace Resolution +} // namespace Penner diff --git a/include/resolution/resolution/optimization/improvement.h b/include/resolution/resolution/optimization/improvement.h new file mode 100644 index 0000000..5d29881 --- /dev/null +++ b/include/resolution/resolution/optimization/improvement.h @@ -0,0 +1,306 @@ +#pragma once + +#include "resolution/core/common.h" +#include "resolution/hierarchy/halfedge.h" + +#include + +namespace Penner { +namespace Resolution { + + +Eigen::VectorXd symmetric_dirichlet( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT); + +class MeshImprover +{ +public: + using json = nlohmann::json; + + json results; + + // skip steps of the mesh improvement + bool skip_smooth = false; + bool skip_flips = false; + bool skip_split = false; + bool skip_collapse = false; + //double sym_dir_thres = 10.; + bool show_smooth = false; + + // weights for uv local optimization + double symdir_weight = 1e-3; + double alignment_weight = 1; + int norm_p = 1; + + // minimum area for regularizing symmetric Dirichlet energy + double min_rel_area = 1e-5; + + // saved mesh area for later use + double avg_area = 0; + + /** + * @brief Initialize the Mesh Improver + * + * @param V: mesh vertices + * @param F: mesh faces + * @param uv: mesh uv vertices + * @param FT: mesh uv faces + */ + MeshImprover( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT + ); + + /** + * @brief Set the combed field to maintain during improvement + * + * @param PD1: first combed field direction (u gradient target) + * @param PD2: second combed field direction (v gradient target) + */ + void set_combed_field( + const Eigen::MatrixXd& PD1, + const Eigen::MatrixXd& PD2 + ); + + /** + * @brief Get the combed field maintain during improvement + * + * @return first combed field direction (u gradient target) + * @return second combed field direction (v gradient target) + */ + std::tuple + get_combed_field() const; + + /** + * @brief Run iterations of improvement. + * + * The improvement alternates between steps of: + * - local per-vertex smoothing to improve energy + * - splitting long edges + * - flipping nearly flat edges + * - collapsing short edges + * The latter two operations are only performed if they improve the triangle quality + * in both the 3D and uv mesh + * + * @param num_iter: number of iterations of improvement to perform + */ + void improve(int num_iter); + + /** + * @brief Create a VF mesh with parametrization for the improved mesh, transforming + * faces so that the parametrization is a single connected component + * + * @return improved mesh vertices + * @return improved mesh faces + * @return improved mesh uv vertices + * @return improved mesh uv faces + */ + std::tuple + get_improved_mesh(); + + /** + * @brief Get the uv coordinates of the mesh (assuming no connectivtity changes) + * + * WARNING: only works if no mesh connectivity operations are performed + * + * @param uv: optimized uv vertices + * @param FT: optimized uv faces + */ + void get_uv( + Eigen::MatrixXd& uv, + Eigen::MatrixXi& FT + ) const; + + /** + * @brief Set the uv coordinates of the mesh (assuming no connectivtity changes) + * + * WARNING: only works if no mesh connectivity operations are performed + * + * @param uv: uv vertices + * @param FT: uv faces + */ + void set_uv( + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT + ); + + /** + * @brief Transform the face opposite the given halfedge to make the two paired + * halfedges adjacent in the plane + * + * Also updates field data if present. + * + * @param hij: halfedge to join with pair + */ + void transform_opposite_face(int hij); + + /** + * @brief Transform the faces adjacent to the given vertex to make them adjacent in the plane + * + * Also updates field data if present. + * + * @param vertex_index: vertex to locally relayout + */ + void transform_vertex_adjacent_faces(int vertex_index); + + /** + * @brief Compute 3x3 gradient operator with respect to spatial coordinates + * for functions defined on the vertices of the face. + * + * WARNING: The indexing of the vertices is determined by the halfedge f2h and n operators + * + * @param face_index: face index + * @return gradient operator + */ + Eigen::Matrix compute_face_gradient(int face_index) const; + + /** + * @brief Compute 2x3 gradient operator with respect to local tangent plane coordinates + * for functions defined on the vertices of the face. + * + * For small faces, the face is normalized to an equilateral triangle with target minimal area. + * + * WARNING: The indexing of the vertices is determined by the halfedge f2h and n operators + * + * @param face_index: face index + * @return tangent gradient operator + */ + Eigen::Matrix compute_face_tangent_gradient(int face_index) const; + + /** + * @brief Compute the area of the face with given index in the improved mesh + * + * @param face_index: face index + * @return face area + */ + double compute_face_area(int face_index) const; + + /** + * @brief Compute the total surface area of the improved mesh + * + * @return mesh area + */ + double compute_total_area() const; + + /** + * @brief Compute the optimization energy associated with the given face. + * + * This energy is a weighted comment of a symmetric Dirichlet energy and a field + * alignment term + * + * @param face_index: index of a face in the improved mesh + * @return face energy + */ + double compute_face_energy(int face_index) const; + + /** + * @brief Compute the optimization energy associated with the faces adjacent to a vertex + * + * This energy is a weighted comment of a symmetric Dirichlet energy and a field + * alignment term + * + * @param vertex_index: index of a vertex in the improved mesh + * @return vertex energy + */ + double compute_vertex_energy(int vertex_index) const; + + /** + * @brief Compute the optimization energy for the current improved mesh + * + * This energy is a weighted comment of a symmetric Dirichlet energy and a field + * alignment term + * + * @return total energy + */ + double compute_energy() const; + + /** + * @brief Compute the current energy at a vertex and a descent direction for the vertex + * to improve the energy + * + * This descent direction is a local Newton direction. + * + * @param vertex_index: vertex to improve + * @return energy at vertex + * @return descent direction for the vertex + */ + std::tuple vertex_energy_direction(int vertex_index) const; + + /** + * @brief Optimize the energy at a vertex with a local backtracking line search. + * + * @param vertex_index: vertex to improve + */ + void local_energy_opt(int vertex_index); + + /** + * @brief Flip a halfedge counterclockwise while preserving the parametrization and averaging + * the field directions of the flipped faces, projecting to the tangent plane + * + * @param halfedge_index: edge to flip + */ + void flip_edge_ccw(int halfedge_index); + + /** + * @brief Flip a halfedge clockwise while preserving the parametrization and averaging + * the field directions of the flipped faces, projecting to the tangent plane + * + * @param halfedge_index: edge to flip + */ + void flip_edge_clockwise(int halfedge_index); + + /** + * @brief Collapse the given halfedge, projecting field directions to tangents. + * + * The operation is rejected if the the collapse reduces triangle quality or flips elements + * + * @param halfedge_index: halfedge to collapse + * @return true iff the collapse is performed succesfully + */ + bool collapse_edge(int halfedge_index); + + /** + * @brief Split the given halfedge at the midpoint, duplicating field data for split faces. + * + * @param halfedge_index: halfedge to split + */ + void split_edge(int halfedge_index); + + /** + * @brief Get the uv vertices of the face + * + * WARNING: The indexing of the vertices is determined by the halfedge f2h and n operators, + * and is consistent with the gradient operators + * + * @param face_index + * @return Eigen::Matrix + */ + Eigen::Matrix compute_face_uv(int face_index) const; + +private: + HalfedgeMesh halfedge; + std::vector vtx_reindex; + std::vector> he_to_corner; + std::vector is_cone; + + std::vector halfedge_uv; + std::vector V_r; + + // cross field + std::vector PD1_r; + std::vector PD2_r; + + igl::Timer timer, method_timer; + double avg_edge_length, abs_collapse_edge_thres; + + // experimental + void smooth_vertex(int vertex_index); +}; + +} +} diff --git a/include/resolution/resolution/pybind.h b/include/resolution/resolution/pybind.h new file mode 100644 index 0000000..26b6a49 --- /dev/null +++ b/include/resolution/resolution/pybind.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef PYBIND +#ifndef MULTIPRECISION + +#include +#include +#include +#include + +namespace Penner { +namespace Resolution { + +void init_resolution_pybind(pybind11::module& m); + + +} // namespace on +} // namespace Penner + +#endif +#endif \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1d652c..9649d3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,6 +72,7 @@ add_subdirectory(field) add_subdirectory(optimization) add_subdirectory(holonomy) add_subdirectory(feature) +add_subdirectory(resolution) add_library(PennerLib INTERFACE) target_link_libraries(PennerLib INTERFACE diff --git a/src/resolution/CMakeLists.txt b/src/resolution/CMakeLists.txt new file mode 100644 index 0000000..0f87929 --- /dev/null +++ b/src/resolution/CMakeLists.txt @@ -0,0 +1,94 @@ +find_package(PkgConfig REQUIRED) +if(USE_JXL) + pkg_check_modules(JXL REQUIRED IMPORTED_TARGET libjxl libjxl_threads) +endif() +if(USE_EXR) + pkg_check_modules(EXR REQUIRED IMPORTED_TARGET OpenEXR) +endif() + +set(ResolutionCoreSrc + core/common.cpp + core/geodesics.cpp + core/geodesic_flipper.cpp + core/layout.cpp + core/quad_quality.cpp + core/triangulation.cpp + core/viewer.cpp +) + +set(HierarchySrc + hierarchy/halfedge.cpp + hierarchy/overlay.cpp + hierarchy/voronoi.cpp + hierarchy/simplifier.cpp +) + +set(OptimizationSrc + optimization/field.cpp + optimization/compression.cpp + optimization/improvement.cpp +) + +set(ExperimentalSrc + experimental/dynamic_markings.cpp + experimental/holonomy_coordinates.cpp + experimental/regularization.cpp + experimental/representatives.cpp + experimental/simplification.cpp + experimental/simplification_overlay.cpp +) + +add_library(PennerResolutionLib + pybind.cpp + interface.cpp + ${ResolutionCoreSrc} + ${HierarchySrc} + ${OptimizationSrc} + ${ExperimentalSrc} +) +target_include_directories(PennerResolutionLib PUBLIC ../../include/resolution) +target_link_libraries(PennerResolutionLib PUBLIC + PennerOptimizationLib + PennerHolonomyLib + PennerFeatureLib + ${MPFR_LIBRARIES} + igl::core + igl::png + Eigen3::Eigen + geometry-central + ${SUITESPARSE_LIBS} + ${VISUALIZATION_LIBS} +) +if (USE_JXL) +target_link_libraries(PennerResolutionLib PUBLIC + PkgConfig::JXL +) +endif() +if (USE_EXR) +target_link_libraries(PennerResolutionLib PUBLIC + PkgConfig::EXR +) +endif() +if (USE_UV_OPTIMIZATION) +target_link_libraries(PennerResolutionLib PUBLIC + SymmetricDirichletLib +) +else() +target_link_libraries(PennerResolutionLib PRIVATE + nlohmann_json::nlohmann_json +# OpenMP::OpenMP_CXX +) +endif() +target_compile_definitions(PennerResolutionLib PUBLIC + SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG +) +target_include_directories(PennerResolutionLib PUBLIC + ../ext/autodiff +) + +# TODO Fix for multiprecision +if ((NOT USE_MULTIPRECISION) AND (USE_WARNINGS)) + target_compile_options(PennerResolutionLib PRIVATE + -Wall -Wpedantic -Wextra -Werror + ) +endif() diff --git a/src/resolution/core/common.cpp b/src/resolution/core/common.cpp new file mode 100644 index 0000000..3f2185b --- /dev/null +++ b/src/resolution/core/common.cpp @@ -0,0 +1,195 @@ +#include "resolution/core/common.h" +#include "feature/core/common.h" +#include "field/frame_field.h" +#include "field/cones.h" +#include "holonomy/core/viewer.h" + +#include +#include + +namespace Penner { +namespace Resolution { + + + +Eigen::SparseMatrix generate_VV_to_halfedge_map( + const Mesh& m, + const std::vector& vtx_reindex) +{ + + // generate IJV representation of the matrix + int n_he = m.n_halfedges(); + typedef Eigen::Triplet Trip; + std::vector trips; + trips.reserve(n_he); + for (int hij = 0; hij < n_he; ++hij) { + // only process primal mesh + if (m.type[hij] == 2) continue; + + // get edge endpoints in VF mesh indexing + int hji = m.opp[hij]; + int vi = vtx_reindex[m.v_rep[m.to[hji]]]; + int vj = vtx_reindex[m.v_rep[m.to[hij]]]; + + // add entry mapping (vi, vj) to the one-indexed halfedge index + trips.push_back(Trip(vi, vj, hij + 1)); + } + + // convert IJV to sparse matrix + int n_v = m.n_ind_vertices(); + Eigen::SparseMatrix vv2he(n_v, n_v); + vv2he.setFromTriplets(trips.begin(), trips.end()); + + return vv2he; +} + +template +Eigen::Matrix transfer_corner_function_to_halfedge( + const Mesh& m, + const std::vector& vtx_reindex, + const Eigen::MatrixXi& F, + const Eigen::Matrix& corner_func) +{ + Eigen::SparseMatrix vv2he = generate_VV_to_halfedge_map(m, vtx_reindex); + int num_faces = F.rows(); + Eigen::Matrix halfedge_func(m.n_halfedges()); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + for (int i = 0; i < 3; ++i) + { + int j = (i + 1) % 3; + int k = (j + 1) % 3; + int vj = F(fijk, j); + int vk = F(fijk, k); + int hjk = vv2he.coeff(vj, vk) - 1; + halfedge_func[hjk] = corner_func(fijk, i); + if (m.type[hjk] > 0) + { + halfedge_func[m.R[hjk]] = halfedge_func[hjk]; + } + } + } + + return halfedge_func; +} + +std::tuple compute_seamless_error( + const Mesh &m, + const Eigen::MatrixXd &uv) +{ + // Iterate over edges to check the length inconsistencies + int num_halfedges = m.n_halfedges(); + VectorX uv_length_error = VectorX::Zero(num_halfedges); + VectorX uv_angle_error = VectorX::Zero(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + int hki = m.n[m.n[hij]]; + int hji = m.opp[hij]; + int hlj = m.n[m.n[hji]]; + + // Compute the length of each halfedge corresponding to the corner in the cut mesh + Eigen::Vector2d uv_00 = uv.row(hij); + Eigen::Vector2d uv_01 = uv.row(hki); + Eigen::Vector2d uv_10 = uv.row(hlj); + Eigen::Vector2d uv_11 = uv.row(hji); + Scalar l0 = Holonomy::compute_uv_length(uv_00, uv_01); + Scalar l1 = Holonomy::compute_uv_length(uv_10, uv_11); + Scalar angle = Holonomy::uv_angle(uv_01 - uv_00, uv_11 - uv_10); + Scalar length_error = abs(l0 - l1); + Scalar angle_error = pos_fmod(angle + (4 * M_PI), M_PI / 2.); + angle_error = min(angle_error, (M_PI / 2.) - angle_error); + spdlog::trace("angle of {}: {}", hij, angle); + spdlog::trace("angle error: {}", angle_error); + spdlog::trace("length error: {}", length_error); + + // set length error for the given edge + uv_length_error(hij) = length_error; + uv_angle_error(hij) = angle_error; + } + + return std::make_tuple(uv_length_error, uv_angle_error); +} + + +int next_orig_segment(const OverlayMesh &m_o, int _h) +{ + // handle degenerate cases + // FIXME: could be unnecessary + if (m_o.n[m_o.opp[m_o.n[_h]]] == m_o.opp[_h]) + { + //return m_o.n[_h]; + } + + // iterate around valence 4 vertex + //int cur = m_o.n[m_o.opp[m_o.n[_h]]]; + int cur = m_o.n[_h]; + + // if original edge, keep iterating + while (m_o.edge_type[m_o.e(cur)] == CURRENT_EDGE) + { + //cur = m_o.n[m_o.opp[m_o.n[cur]]]; + cur = m_o.n[m_o.opp[cur]]; + } + return cur; +} + +void remove_unreferenced_inplace(Eigen::MatrixXd& V, Eigen::MatrixXi& F) +{ + std::vector old2new(V.rows(), -1); + Eigen::MatrixXd V_compact(V.rows(), V.cols()); + int num_used = 0; + + for (int f = 0; f < F.rows(); ++f) + { + for (int i = 0; i < F.cols(); ++i) + { + int v_old = F(f, i); + int& v_new = old2new[v_old]; + if (v_new == -1) + { + v_new = num_used++; + V_compact.row(v_new) = V.row(v_old); + } + F(f, i) = v_new; + } + } + + V_compact.conservativeResize(num_used, V.cols()); + V = V_compact; +} + +Eigen::Vector3d project_orthogonal_to( + const Eigen::Vector3d& v, + const Eigen::Vector3d& u) +{ + double denom = u.squaredNorm(); + if (denom == 0.0) { + // If u is zero, the whole space is orthogonal to it + return v; + } + + return v - (v.dot(u) / denom) * u; +} + +double compute_triangle_area( + const Eigen::Vector3d& v1, + const Eigen::Vector3d& v2, + const Eigen::Vector3d& v3) +{ + Eigen::Vector3d d12 = v2 - v1; + Eigen::Vector3d d13 = v3 - v1; + Eigen::Vector3d n = d12.cross(d13); + return 0.5 * std::sqrt(n.dot(n)); +} + +Eigen::Matrix2d generate_ccw_rotation() +{ + Eigen::Matrix2d R; + R.row(0) << 0, -1; + R.row(1) << 1, 0; + + return R; +} + +} // Resolution +} // Penner diff --git a/src/resolution/core/geodesic_flipper.cpp b/src/resolution/core/geodesic_flipper.cpp new file mode 100644 index 0000000..5ab3f5f --- /dev/null +++ b/src/resolution/core/geodesic_flipper.cpp @@ -0,0 +1,981 @@ +#include "resolution/core/geodesic_flipper.h" + +#include "util/vector.h" +#include "util/union_find.h" +#include "metric/constraint.h" +#include "parametrization/interpolation.h" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "conformal_ideal_delaunay/Layout.hh" + + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner +{ +namespace Resolution +{ + +GeodesicPath::GeodesicPath(const std::vector& path) +{ + // build initial chain + int curr_segment = create_segment(path.front()); + start = curr_segment; + for (int i = 1; i < path.size(); ++i) + { + curr_segment = create_next_segment(curr_segment, path[i]); + } + end = curr_segment; +} + +std::vector GeodesicPath::get_path_halfedges() const +{ + int i = start; + std::vector halfedges = {halfedge[i]}; + while (i != end) + { + i = next[i]; + halfedges.push_back(halfedge[i]); + } + + return halfedges; +} + +int GeodesicPath::create_segment(int h) +{ + int index = halfedge.size(); + halfedge.push_back(h); + next.push_back(-1); + prev.push_back(-1); + to.push_back(-1); + + return index; +} + +int GeodesicPath::create_wedge(int index) +{ + int wedge = m_is_wedge_valid.size(); + m_is_wedge_valid.push_back(true); + in.push_back(index); + to[index] = wedge; + return wedge; +} + +int GeodesicPath::create_next_segment(int index, int h) +{ + int next_index = create_segment(h); + next[index] = next_index; + prev[next_index] = index; + int wedge = create_wedge(index); + return next_index; +} + +bool GeodesicPath::is_valid(Mesh& m) +{ + int i = start; + int count = 0; + while (i != end) + { + // check if in a loops + if (count > halfedge.size()) return false; + + // check if next/prev are inverse + int j = next[i]; + if (prev[j] != i) return false; + + // check if halfedges adjacent + int hij = halfedge[i]; + int hjk = halfedge[j]; + if (m.to[hij] != m.to[m.opp[hjk]]) return false; + + i = j; + ++count; + } + + return true; +} + +GeodesicFlipper::GeodesicFlipper( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& halfedge_paths, + bool allow_intersections +) + : GeodesicFlipper(overlay_mesh.generate_mesh(), halfedge_paths, allow_intersections) +{} + +GeodesicFlipper::GeodesicFlipper( + const Mesh& mesh, + const std::vector>& halfedge_paths, + bool allow_intersections +) + : m_o(mesh) + , edge_path_counts(m_o.n_edges(), 0) +{ + build_edge_maps(m_o.opp, h2e, e2h); + keep_simple = !allow_intersections; + + // compute initial corner angles + auto& m = m_o.cmesh(); + VectorX cot_angles; + corner_angles(m, angles, cot_angles); + + // add paths to registry + int num_paths = halfedge_paths.size(); + paths.reserve(num_paths); + path_length = std::vector(num_paths, 0.); + path_curvature = std::vector(num_paths, 0.); + if (allow_intersections) adjacent_paths = std::vector>>(m_o.n_edges(), std::deque>()); + for (int i = 0; i < num_paths; ++i) + { + const auto& path = halfedge_paths[i]; + paths.push_back(path); + + // add adjacent paths to record + for (int j = 0; j < path.size(); ++j) + { + int h = paths[i].halfedge[j]; + ++edge_path_counts[h2e[h]]; + if (allow_intersections) adjacent_paths[h2e[h]].push_back({i, j}); + path_length[i] += m.l[h]; + } + path_curvature[i] = compute_path_curvature(i); + if (track_deltas) + { + paths[i].sizes.push_back(path.size()); + paths[i].lengths.push_back(path_length[i]); + } + } + spdlog::info("initial path total length: {}", compute_total_sum(path_length)); + if (!is_simple()) spdlog::error("cannot straighten paths if not simple"); + assert(is_simple()); +} + +void GeodesicFlipper::flip_to_geodesics() +{ + // add all wedges to priority queue + int num_paths = paths.size(); + + wedges_to_process = PriorityQueue(); + + // iterate until no more wedges to shorten + int count = 1; + while (count > 0) + { + count = 0; + check_all_wedges(); + + // iterate + while (!wedges_to_process.empty()) + { + if (!process_wedge()) continue; + ++count; + + // check if exceed max iteration count + if ((max_flipouts > 0) && (count > max_flipouts)) return; + } + spdlog::info("{} wedges shortened", count); + } + + spdlog::info("final path total length: {}", compute_total_sum(path_length)); +} + +void GeodesicFlipper::check_wedge(int path_index, int wedge_index) +{ + const auto& path = paths[path_index]; + if (!path.is_wedge_valid(wedge_index)) return; + if (path_curvature[path_index] < (path_curvature_threshold)) return; + + int j = path.in[wedge_index]; + int k = path.next[j]; + int hij = path.halfedge[j]; + int hjk = path.halfedge[k]; + + // check if clockwise wedge is acute + Scalar clockwise_wedge_angle = compute_clockwise_wedge_angle(hij, hjk); + if (clockwise_wedge_angle < (flip_angle - wedge_eps)) + { + wedges_to_process.push(std::make_tuple(clockwise_wedge_angle, path_index, wedge_index, true)); + } + + // check if ccw wedge is acute + Scalar ccw_wedge_angle = compute_ccw_wedge_angle(hij, hjk); + if (ccw_wedge_angle < (flip_angle - wedge_eps)) + { + wedges_to_process.push(std::make_tuple(ccw_wedge_angle, path_index, wedge_index, false)); + } +} + + +void GeodesicFlipper::check_all_wedges() +{ + int num_paths = paths.size(); + for (int i = 0; i < num_paths; ++i) + { + const auto& path = paths[i]; + for (int j = path.get_start(); j != path.get_end(); j = path.next[j]) + { + check_wedge(i, path.to[j]); + } + } +} + +bool GeodesicFlipper::process_wedge() +{ + auto [wedge_angle, i, wedge_index, is_left_joint] = wedges_to_process.top(); + wedges_to_process.pop(); + if (!paths[i].is_wedge_valid(wedge_index)) return false; + + // get wedge halfedges and check if joint is still flexible + int j = paths[i].in[wedge_index]; + int k = paths[i].next[j]; + int hij = paths[i].halfedge[j]; + int hjk = paths[i].halfedge[k]; + if (!is_joint_flexible(i, wedge_index, is_left_joint)) return false; + + // flip edges to make shorter path + int num_flips = flip_out(hij, hjk, is_left_joint); + + // check if shorter joint path will result in undesired intersection + if ((keep_simple) && (!is_wedge_simple(i, wedge_index, is_left_joint))) + { + undo_flips(num_flips); + return false; + } + + // shorten the wedge if possible + shorten_wedge(i, wedge_index, is_left_joint); + + return true; +} + +int GeodesicFlipper::flip_out(int hij, int hjk, bool is_left_joint) +{ + // flip opposite acute angles until none remain + bool edge_flipped = true; + int count = 0; + while (edge_flipped) + { + if (is_left_joint) + { + edge_flipped = flip_left_acute_angles(hij, hjk); + } + else + { + edge_flipped = flip_right_acute_angles(hij, hjk); + } + if (edge_flipped) ++count; + if (count == 1000) spdlog::warn("more than a thousand flips done in flip out"); + } + + return count; +} + +bool GeodesicFlipper::flip_left_acute_angles(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + int hjl = m.n[hij]; + while (hjl != hjk) + { + // flip edge if opposite angle is less than pi + Scalar beta = angles[m.n[m.n[hjl]]] + angles[m.n[m.opp[hjl]]]; + if (beta < (PI - wedge_eps)) + { + flip_halfedge(hjl); + return true; + } + + // update halfedge index + hjl = m.n[m.opp[hjl]]; + } + + return false; +} + +bool GeodesicFlipper::flip_right_acute_angles(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + int hkj = m.opp[hjk]; + int hji = m.opp[hij]; + return flip_left_acute_angles(hkj, hji); +} + +void GeodesicFlipper::flip_halfedge(int hij) +{ + // do flip in overlay mesh + bool use_ptolemy = false; + for (int i = 0; i < 3; ++i) + { + m_o.flip_ccw(hij, use_ptolemy); + } + + // record flip + flip_sequence.push_back(hij); + + // update adjacent angles triangle angles + auto& m = m_o.cmesh(); + int hji = m.opp[hij]; + assert(hji >= 0); + update_face_angles(m.f[hij]); + update_face_angles(m.f[hji]); +} + +void GeodesicFlipper::shorten_wedge( + int path_index, + int wedge_index, + bool is_left_joint) +{ + auto& path = paths[path_index]; + auto& m = m_o.cmesh(); + assert(path.is_valid(m_o)); + + // get relative indices + int index = path.in[wedge_index]; + int prev_index = path.prev[index]; + int next_index = path.next[index]; + int hij = path.halfedge[index]; + int hjk = path.halfedge[next_index]; + Scalar old_length = m.l[hij] + m.l[hjk]; + Scalar new_length = 0.; + Scalar new_curvature = 0.; + int count = 0; + + // remove wedge from adjacent path + --edge_path_counts[h2e[hij]]; + --edge_path_counts[h2e[hjk]]; + if (!keep_simple) + { + if (is_left_joint) + { + adjacent_paths[h2e[hij]].pop_front(); + adjacent_paths[h2e[hjk]].pop_front(); + } + else + { + adjacent_paths[h2e[hij]].pop_back(); + adjacent_paths[h2e[hjk]].pop_back(); + } + } + + // delete wedge being pushed + path.mark_wedge_invalid(path.to[index]); + + // connect new path at start + int hjl = (is_left_joint) ? m.n[hij] : m.n[m.n[m.opp[hij]]]; + int h_path = (is_left_joint) ? m.opp[m.n[hjl]] : m.n[m.n[hjl]]; + new_length += m.l[h_path]; + ++count; + if (path.is_start(index)) + { + int curr_index = path.create_segment(h_path); + path.set_start(curr_index); + add_adjacent_halfedge(path_index, curr_index, !is_left_joint); + prev_index = curr_index; + } + else + { + path.mark_wedge_invalid(path.to[prev_index]); + int curr_index = path.create_next_segment(prev_index, h_path); + add_adjacent_halfedge(path_index, curr_index, !is_left_joint); + check_wedge(path_index, path.to[prev_index]); + prev_index = curr_index; + } + + // set any interior paths + int h_end = (is_left_joint) ? hjk : m.opp[hjk]; + while (hjl != h_end) + { + hjl = (is_left_joint) ? m.n[m.opp[hjl]] : m.n[m.n[m.opp[hjl]]]; + h_path = (is_left_joint) ? m.opp[m.n[hjl]] : m.n[m.n[hjl]]; + new_length += m.l[h_path]; + ++count; + assert(m.to[path.halfedge[prev_index]] == m.to[m.opp[h_path]]); + int curr_index = path.create_next_segment(prev_index, h_path); + add_adjacent_halfedge(path_index, curr_index, !is_left_joint); + check_wedge(path_index, path.to[prev_index]); + prev_index = curr_index; + } + assert(m.to[path.halfedge[prev_index]] == m.to[hjk]); + + // connect new path at end + if (path.is_end(next_index)) + { + // mark new halfedge as end of path + path.set_end(prev_index); + } + else + { + path.mark_wedge_invalid(path.to[next_index]); + path.next[prev_index] = path.next[next_index]; + path.prev[path.next[next_index]] = prev_index; + int wedge_index = path.create_wedge(prev_index); + check_wedge(path_index, wedge_index); + } + + path_length[path_index] += (new_length - old_length); + path_curvature[path_index] = compute_path_curvature(path_index); + if (track_deltas) + { + path.lengths.push_back(path.lengths.back() + new_length - old_length); + path.sizes.push_back(path.sizes.back() + count - 2); + deltas.push_back(old_length - new_length); + } + assert(path.is_valid(m_o)); + assert((keep_simple) || (is_simple())); +} + +void GeodesicFlipper::undo_flips(int num_flips) +{ + // do flip in overlay mesh + bool use_ptolemy = false; + for (int i = 0; i < num_flips; ++i) + { + int hij = flip_sequence.back(); + m_o.flip_ccw(hij, use_ptolemy); + flip_sequence.pop_back(); + + // update adjacent angles triangle angles + auto& m = m_o.cmesh(); + int hji = m.opp[hij]; + assert(hji >= 0); + update_face_angles(m.f[hij]); + update_face_angles(m.f[hji]); + } +} + +Scalar GeodesicFlipper::compute_clockwise_wedge_angle(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + assert(m.to[hij] == m.to[m.opp[hjk]]); + + int hjl = m.n[hij]; + Scalar wedge_angle = 0.; + while (hjl != hjk) + { + wedge_angle += angles[m.n[hjl]]; + int hlj = m.opp[hjl]; + if (hlj < 0) return infinity; + hjl = m.n[hlj]; + } + wedge_angle += angles[m.n[hjk]]; + + return wedge_angle; +} + +Scalar GeodesicFlipper::compute_ccw_wedge_angle(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + int hkj = m.opp[hjk]; + int hji = m.opp[hij]; + if ((hji < 0) || (hkj < 0)) return infinity; + return compute_clockwise_wedge_angle(hkj, hji); +} + +Scalar GeodesicFlipper::compute_wedge_curvature(int hij, int hjk, bool use_signed) +{ + Scalar clockwise_angle = compute_clockwise_wedge_angle(hij, hjk); + if (use_signed) + { + return (PI - clockwise_angle); + } + else + { + Scalar ccw_angle = compute_ccw_wedge_angle(hij, hjk); + return abs(PI - min(clockwise_angle, ccw_angle)); + } +} + +Scalar GeodesicFlipper::compute_path_curvature(int path_index, bool use_signed) +{ + Scalar angle = 0.; + const auto& path = paths[path_index]; + for (int i = path.get_start(); i != path.get_end(); i = path.next[i]) + { + int j = path.next[i]; + int hij = path.halfedge[i]; + int hjk = path.halfedge[j]; + angle += compute_wedge_curvature(hij, hjk, use_signed); + } + + return angle; +} + +std::vector GeodesicFlipper::compute_path_curvatures(bool use_signed) +{ + int num_paths = paths.size(); + std::vector angles(num_paths); + for (int i = 0; i < num_paths; ++i) + { + angles[i] = compute_path_curvature(i, use_signed); + } + + return angles; +} + +void GeodesicFlipper::generate_surface_points() +{ + //m_o.garbage_collection(); + OverlayMesh m_rev(m_o.cmesh()); + for (auto iter = flip_sequence.rbegin(); iter != flip_sequence.rend(); ++iter) { + int halfedge_index = *iter; + m_rev.flip_ccw(halfedge_index, false); + } + m_rev.garbage_collection(); + auto& m = m_rev.cmesh(); + + // store size statistic + overlay_faces = m_rev.n_faces(); + + std::vector h2e_rev, e2h_rev; + // TODO may be possible to reuse for forward overlay + build_edge_maps(m.opp, h2e_rev, e2h_rev); + + // get list of first segments of the original edges in the overlay + std::vector first_segment(m.n_halfedges(), -1); + for (int hij = 0; hij < m_rev.n_halfedges(); ++hij) + { + // only look for original edges + if (m_rev.edge_type[hij] == CURRENT_EDGE) + continue; + + + int vi = m_rev.to[m_rev.opp[hij]]; + int Hij = m_rev.origin_of_origin[hij]; + if (m_rev.vertex_type[vi] == ORIGINAL_VERTEX) + first_segment[Hij] = hij; + } + + surface_points.clear(); + std::vector vertex_path(m_rev.n_vertices(), -1); + std::vector vertex_index(m_rev.n_vertices(), -1); + for (int i = 0; i < paths.size(); ++i) + { + // get halfedges in the path + std::vector path_halfedges = paths[i].get_path_halfedges(); + std::vector overlay_halfedges = {}; + for (int h : path_halfedges) + { + // iterate over segments in the overlay of the edge + int seg = first_segment[h]; + overlay_halfedges.push_back(seg); + while (m_rev.vertex_type[m_rev.to[seg]] != ORIGINAL_VERTEX) + { + seg = next_orig_segment(m_rev, seg); + overlay_halfedges.push_back(seg); + } + } + + // get vertices in the path + std::vector path_vertices = {m_rev.to[m_rev.opp[overlay_halfedges.front()]]}; + for (int h : overlay_halfedges) + { + path_vertices.push_back(m_rev.to[h]); + } + + // build point data for each path vertex + int path_size = path_vertices.size(); + surface_points.push_back(std::vector(path_size)); + auto& path_points = surface_points.back(); + for (int j = 0; j < path_size; ++j) + { + int v = path_vertices[j]; + + // path vertex is a vertex of the mesh + if (m_rev.vertex_type[v] == ORIGINAL_VERTEX) + { + path_points[j].type = SurfacePoint::Type::Vertex; + path_points[j].index = v; + } + // path vertex is an inserted vertex on an edge + else + { + path_points[j].type = SurfacePoint::Type::Edge; + int h = m_rev.opp[m_rev.out[v]]; + while (m_rev.edge_type[h] == ORIGINAL_EDGE) + { + h = m_rev.opp[m_rev.n[h]]; + } + + path_points[j].index = h2e_rev[m_rev.origin[h]]; + int Hij = e2h_rev[path_points[j].index]; + path_points[j].edge_base = m.to[m.opp[Hij]]; + path_points[j].edge_tip = m.to[Hij]; + path_points[j].t = (Hij == m_rev.origin[h]) ? m_rev.seg_bcs[h][1] : m_rev.seg_bcs[h][0]; + vertex_path[v] = i; + vertex_index[v] = j; + } + } + } + + edge_splits = std::vector>>(h2e_rev.size(), std::vector>(0)); + for (int eij = 0; eij < e2h_rev.size(); ++eij) + { + int Hij = e2h_rev[eij]; + int hij = m_rev.first_segment[m.opp[Hij]]; + while (m_rev.vertex_type[m_rev.to[hij]] != ORIGINAL_VERTEX) + { + int v = m_rev.to[hij]; + if (vertex_path[v] >= 0) + { + edge_splits[Hij].push_back({vertex_path[v], vertex_index[v]}); + } + hij = m_rev.next_segment(hij); + } + } +} + +std::vector GeodesicFlipper::get_intersecting_paths() +{ + int num_paths = paths.size(); + std::vector is_intersecting(num_paths, false); + if (keep_simple) return is_intersecting; + + for (auto& paths : adjacent_paths) + { + if (paths.size() <= 1) continue; + for (auto& path : paths) + { + int path_index = path.first; + is_intersecting[path_index] = true; + } + } + + return is_intersecting; +} + +void GeodesicFlipper::add_to_viewer(VoronoiOverlayMesh& overlay_mesh) +{ + Eigen::MatrixXd V = overlay_mesh.generate_overlay_vertices(overlay_mesh.V_orig, overlay_mesh.vtx_reindex_orig, overlay_mesh.orig); + auto [F, F_halfedge] = overlay_mesh.generate_overlay_faces(); + std::vector> polyline = get_surface_point_positions(overlay_mesh, surface_points); + auto [nodes, edges, labels] = convert_polyline(polyline); + auto shuffled_labels = shuffle_map_image(labels); + //Eigen::MatrixXi F_c = edgeNetwork.tri->getFaceVertexMatrix(); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh("simplified mesh", V, F); + polyscope::registerCurveNetwork("edges", nodes, edges) + ->setRadius(0.002); + polyscope::getCurveNetwork("edges") + ->addEdgeScalarQuantity("edge parent", labels); + polyscope::getCurveNetwork("edges") + ->addEdgeScalarQuantity("shuffled edge parent", shuffled_labels) + ->setEnabled(true); +#endif +} + +void GeodesicFlipper::view(VoronoiOverlayMesh& overlay_mesh) +{ + add_to_viewer(overlay_mesh); +#ifdef ENABLE_VISUALIZATION + polyscope::show(); +#endif +} + +void GeodesicFlipper::animate_flip_to_geodesics(VoronoiOverlayMesh& overlay_mesh, int frame_delta) +{ + // add all wedges to priority queue + wedges_to_process = PriorityQueue(); + + // iterate until no more wedges to shorten + // TODO: replace with local wedge update + int count = 1; + while (count > 0) + { + count = 0; + check_all_wedges(); + + // iterate + while (!wedges_to_process.empty()) + { + if (!process_wedge()) continue; + if ((count % frame_delta) == 0) + { + spdlog::info("saving screencap"); + add_to_viewer(overlay_mesh); +#ifdef ENABLE_VISUALIZATION + polyscope::screenshot("flip_"+std::to_string(count)+".png"); +#endif + } + ++count; + if ((max_flipouts > 0) && (count > max_flipouts)) return; + } + spdlog::info("{} wedges shortened", count); + } + + spdlog::info("final path total length: {}", compute_total_sum(path_length)); +} + +void GeodesicFlipper::add_adjacent_halfedge(int path_index, int index, bool on_left) +{ + auto& path = paths[path_index]; + int h = path.halfedge[index]; + int e = h2e[h]; + + // add to the count of paths adjacent to the edge + ++edge_path_counts[e]; + + // if not ensuring path is simple, need to keep track of all paths on edge in order + if (!keep_simple) + { + // check if new path is to the left of the oriented edge + if (((on_left) && (e2h[e] == h)) || ((!on_left) && (e2h[e] != h))) + { + adjacent_paths[e].push_front({path_index, index}); + } + // check if the new path is to the right of the oriented edge + else + { + adjacent_paths[h2e[h]].push_back({path_index, index}); + } + } +} + + +bool GeodesicFlipper::is_farthest_left(int path_index, int index) +{ + auto& path = paths[path_index]; + int h = path.halfedge[index]; + int e = h2e[h]; + if (keep_simple) return (edge_path_counts[e] == 1); + + int i, j; + if (e2h[e] == h) + { + std::tie(i, j) = adjacent_paths[e].front(); + } + else + { + std::tie(i, j) = adjacent_paths[e].back(); + } + + return ((i == path_index) && (j == index)); +} + +bool GeodesicFlipper::is_farthest_right(int path_index, int index) +{ + auto& path = paths[path_index]; + int h = path.halfedge[index]; + int e = h2e[h]; + if (keep_simple) return (edge_path_counts[e] == 1); + + int i, j; + if (e2h[e] == h) + { + std::tie(i, j) = adjacent_paths[e].back(); + } + else + { + std::tie(i, j) = adjacent_paths[e].front(); + } + return ((i == path_index) && (j == index)); +} + +bool GeodesicFlipper::is_vertex_isolated(int vi) +{ + auto& m = m_o.cmesh(); + + // check if any edge is adjacent to the wedge vertex + int hij = m.out[vi]; + do + { + if (edge_path_counts[h2e[hij]] != 0) return false; + + // check if adjacent to boundary edge + int hji = m.opp[hij]; + + if (hji < 0) return false; + hij = m.n[hji]; + } while (hij != m.out[vi]); + + return true; +} + +bool GeodesicFlipper::is_left_wedge_empty(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + assert(m.to[hij] == m.to[m.opp[hjk]]); + + // check if any edge is adjacent to the wedge vertex + int hjl = m.n[hij]; + while (hjl != hjk) + { + if (edge_path_counts[h2e[hjl]] != 0) return false; + + // check if adjacent to boundary edge + int hlj = m.opp[hjl]; + + if (hlj < 0) return false; + hjl = m.n[hlj]; + } + + return true; +} + +bool GeodesicFlipper::is_right_wedge_empty(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + int hkj = m.opp[hjk]; + int hji = m.opp[hij]; + + // check if boundary edge + if ((hji < 0) || (hkj < 0)) return false; + + return is_left_wedge_empty(hkj, hji); +} + +bool GeodesicFlipper::is_left_wedge_simple(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + assert(m.to[hij] == m.to[m.opp[hjk]]); + + // check if any edge is adjacent to the left wedge path vertices + int hjl = m.n[hij]; + while (hjl != hjk) + { + if (!is_vertex_isolated(m.to[hjl])) return false; + int hlj = m.opp[hjl]; + if (hlj < 0) return false; + hjl = m.n[hlj]; + } + + return true; +} + +bool GeodesicFlipper::is_right_wedge_simple(int hij, int hjk) +{ + auto& m = m_o.cmesh(); + int hkj = m.opp[hjk]; + int hji = m.opp[hij]; + + // check if boundary edge + if ((hji < 0) || (hkj < 0)) return false; + + return is_left_wedge_simple(hkj, hji); +} + +bool GeodesicFlipper::is_wedge_simple(int path_index, int wedge_index, bool is_left_joint) +{ + int j = paths[path_index].in[wedge_index]; + int k = paths[path_index].next[j]; + int hij = paths[path_index].halfedge[j]; + int hjk = paths[path_index].halfedge[k]; + + if (is_left_joint) + { + return is_left_wedge_simple(hij, hjk); + } + else + { + return is_right_wedge_simple(hij, hjk); + } +} + +bool GeodesicFlipper::is_left_joint_flexible(int path_index, int wedge_index) +{ + int j = paths[path_index].in[wedge_index]; + int k = paths[path_index].next[j]; + int hij = paths[path_index].halfedge[j]; + int hjk = paths[path_index].halfedge[k]; + if (!is_farthest_left(path_index, j)) return false; + if (!is_farthest_left(path_index, k)) return false; + if (!is_left_wedge_empty(hij, hjk)) return false; + return true; +} + +bool GeodesicFlipper::is_right_joint_flexible(int path_index, int wedge_index) +{ + int j = paths[path_index].in[wedge_index]; + int k = paths[path_index].next[j]; + int hij = paths[path_index].halfedge[j]; + int hjk = paths[path_index].halfedge[k]; + if (!is_farthest_right(path_index, j)) return false; + if (!is_farthest_right(path_index, k)) return false; + if (!is_right_wedge_empty(hij, hjk)) return false; + return true; +} + +bool GeodesicFlipper::is_joint_flexible(int path_index, int wedge_index, bool is_left_joint) +{ + if (is_left_joint) + { + return is_left_joint_flexible(path_index, wedge_index); + } + else + { + return is_right_joint_flexible(path_index, wedge_index); + } +} + +void GeodesicFlipper::update_face_angles(int face_index) +{ + auto& m = m_o.cmesh(); + int hi = m.h[face_index]; + int hj = m.n[hi]; + int hk = m.n[hj]; + angles[hi] = 0.0, angles[hj] = 0.0, angles[hk] = 0.0; + Scalar l12 = m.l[hi], l23 = m.l[hj], l31 = m.l[hk]; + const Scalar t31 = +l12 + l23 - l31, t23 = +l12 - l23 + l31, t12 = -l12 + l23 + l31; + // valid triangle + if (t31 > 0 && t23 > 0 && t12 > 0) { + const Scalar l123 = l12 + l23 + l31; + const Scalar denom = sqrt(t12 * t23 * t31 * l123); + angles[hj] = 2 * atan2(t12 * t31, denom); // a1 l23 + angles[hk] = 2 * atan2(t23 * t12, denom); // a2 l31 + angles[hi] = 2 * atan2(t31 * t23, denom); // a3 l12 + } else if (t31 <= 0) + angles[hk] = PI; + else if (t23 <= 0) + angles[hj] = PI; + else if (t12 <= 0) + angles[hi] = PI; + else + angles[hj] = PI; +} + + + +bool is_valid_mesh(Mesh& m) +{ + std::vector prev = invert_map(m.n); + if (!are_polygon_mesh_halfedges_valid(m.n, prev)) return errmess("mesh halfedges not valid"); + //if (!are_polygon_mesh_vertices_valid(m.opp, prev, m.to, m.out)) return errmess("mesh vertices not valid"); + if (!are_polygon_mesh_faces_valid(m.n, m.f, m.h)) return errmess("mesh faces not valid"); + + return true; +} + +bool is_valid_overlay(OverlayMesh& m_o) +{ + m_o.garbage_collection(); + if (!are_polygon_mesh_halfedges_valid(m_o.n, m_o.prev)) return errmess("mesh halfedges not valid"); + //if (!are_polygon_mesh_vertices_valid(m_o.opp, m_o.prev, m_o.to, m_o.out)) return errmess("mesh vertices not valid"); + if (!are_polygon_mesh_faces_valid(m_o.n, m_o.f, m_o.h)) return errmess("mesh faces not valid"); + + return true; +} + +bool GeodesicFlipper::is_valid() +{ + //if (!is_valid_mesh(m_o)) return false; + + return true; +} + + +bool GeodesicFlipper::is_simple() +{ + int num_paths = paths.size(); + for (int count : edge_path_counts) + { + if (count > 1) return false; + } + + return true; +} + +} +} \ No newline at end of file diff --git a/src/resolution/core/geodesics.cpp b/src/resolution/core/geodesics.cpp new file mode 100644 index 0000000..973734e --- /dev/null +++ b/src/resolution/core/geodesics.cpp @@ -0,0 +1,1263 @@ +#include "resolution/core/geodesics.h" +#include "util/union_find.h" +#include "util/vector.h" +#include "metric/constraint.h" +#include "parametrization/interpolation.h" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "conformal_ideal_delaunay/Layout.hh" + + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner +{ +namespace Resolution +{ + + + + + +Eigen::SparseMatrix generate_VV_to_halfedge_map( + geometrycentral::surface::ManifoldSurfaceMesh& mesh) +{ + typedef geometrycentral::surface::Halfedge Halfedge; + int num_vertices = mesh.nVertices(); + Eigen::SparseMatrix vv2he(num_vertices, num_vertices); + + // build sparse matrix + typedef Eigen::Triplet Trip; + std::vector trips; + for(Halfedge he : mesh.halfedges()) { + int vj = he.tailVertex().getIndex(); + int vk= he.tipVertex().getIndex(); + trips.push_back(Trip(vj, vk, he.getIndex() + 1)); + } + vv2he.setFromTriplets(trips.begin(), trips.end()); + + return vv2he; +} + +std::vector> generate_halfedge_paths( + const std::vector>& paths, + geometrycentral::surface::ManifoldSurfaceMesh& mesh) +{ + // create halfedge paths + int num_paths = paths.size(); + typedef geometrycentral::surface::Halfedge Halfedge; + std::vector> halfedge_paths(num_paths); + Eigen::SparseMatrix vv2he = generate_VV_to_halfedge_map(mesh); + for (int i = 0; i < num_paths; ++i) + { + int num_path_nodes = paths[i].size(); + halfedge_paths[i].reserve(num_path_nodes); + for (int j = 0; j < num_path_nodes - 1; ++j) + { + int vi = paths[i][j]; + int vj = paths[i][j + 1]; + halfedge_paths[i].push_back(mesh.halfedge(vv2he.coeff(vi, vj) - 1)); + } + } + + return halfedge_paths; +} + + +Eigen::MatrixXd build_overlay_vertices( + const Eigen::MatrixXd& V, + const std::vector vtx_reindex, + OverlayMesh& m_o) +{ + Mesh& m = m_o.cmesh(); + + std::vector> V_reindex(3); + for (int i = 0; i < 3; i++) { + V_reindex[i].resize(V.rows(), 0); + for (int j = 0; j < V.rows(); j++) { + V_reindex[i][j] = Scalar(V(vtx_reindex[j], i)); + } + } + + // Interpolate vertex positions along the original edges + std::vector> V_overlay_rev(3); + for (int j = 0; j < 3; j++) { + V_overlay_rev[j] = m_o.interpolate_along_o_bc( + m.opp, + m.to, + V_reindex[j]); + } + + // Reindex vertices + Eigen::MatrixXd V_overlay(V_overlay_rev[0].size(), 3); + for (size_t j = 0; j < 3; j++) { + for (size_t i = 0; i < V_overlay_rev[j].size(); i++) { + V_overlay(i, j) = double(V_overlay_rev[j][i]); + } + } + + return V_overlay; +} + +SurfacePoint convert_surface_point(const geometrycentral::surface::SurfacePoint& pt) +{ + SurfacePoint point; + if (pt.type == geometrycentral::surface::SurfacePointType::Vertex) + { + point.index = pt.vertex.getIndex(); + point.type = SurfacePoint::Type::Vertex; + } + else if (pt.type == geometrycentral::surface::SurfacePointType::Edge) + { + point.type = SurfacePoint::Type::Edge; + point.index = pt.edge.getIndex(); + point.edge_base = pt.edge.firstVertex().getIndex(); + point.edge_tip = pt.edge.secondVertex().getIndex(); + point.t = pt.tEdge; + } + else if (pt.type == geometrycentral::surface::SurfacePointType::Face) + { + point.type = SurfacePoint::Type::Face; + } + + return point; +} + +std::vector> generate_geodesic_path_points( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths) +{ + Eigen::MatrixXd V = overlay_mesh.generate_overlay_vertices(overlay_mesh.V_orig, overlay_mesh.vtx_reindex_orig, overlay_mesh.orig); + Eigen::MatrixXi F = overlay_mesh.generate_overlay_faces_simple(); + + geometrycentral::surface::ManifoldSurfaceMesh mesh(F); + geometrycentral::surface::VertexPositionGeometry geometry(mesh, V); + geometrycentral::surface::FlipEdgeNetwork edgeNetwork(mesh, geometry, generate_halfedge_paths(paths, mesh)); + + // Make the path a geodesic + edgeNetwork.straightenAroundMarkedVertices = false; + edgeNetwork.iterativeShorten(); + + // Extract the result as a polyline along the surface + edgeNetwork.posGeom = &geometry; + std::vector> polyline_pts = edgeNetwork.getPathPolyline(); + + int num_paths = polyline_pts.size(); + std::vector> surface_pts(num_paths); + for (int i = 0; i < num_paths; ++i) + { + int num_pts = polyline_pts[i].size(); + surface_pts[i].resize(num_pts); + for (int j = 0; j < num_pts; ++j) + { + surface_pts[i][j] = convert_surface_point(polyline_pts[i][j]); + } + } + + return surface_pts; +} + +PathStraightener::PathStraightener( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths +) + : PathStraightener(overlay_mesh, paths, std::vector(paths.size(), false)) +{} + +PathStraightener::PathStraightener( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths, + const std::vector& is_intersecting_path +) +: V(overlay_mesh.generate_overlay_vertices(overlay_mesh.V_orig, overlay_mesh.vtx_reindex_orig, overlay_mesh.orig)) +, F(overlay_mesh.generate_overlay_faces_simple()) +, polyline_pts(paths) +, is_redundant_path(is_intersecting_path) +{ + std::tie(v_new2old, v_old2new) = overlay_mesh.get_vertex_reductions(); + vv2he_o = overlay_mesh.generate_VV_to_halfedge_map(); +} + +void PathStraightener::split_edges(VoronoiOverlayMesh& overlay_mesh) +{ + int num_paths = polyline_pts.size(); + int num_halfedges = overlay_mesh.n.size(); + std::vector>> edges_to_split(num_halfedges); + + for (int i = 0; i < num_paths; ++i) + { + const auto& polyline_pt = polyline_pts[i]; + int num_pts = polyline_pt.size(); + for (int j = 0; j < num_pts; ++j) + { + const auto& pt = polyline_pt[j]; + if (pt.type == SurfacePoint::Type::Edge) + { + int vi = pt.edge_base; + int vj = pt.edge_tip; + int hij = vv2he_o.coeff(vi, vj) - 1; + assert(hij >= 0); + spdlog::debug("vertices {}, {} with halfedge {}", vi, vj, hij); + edges_to_split[hij].push_back({i, j}); + } + } + } + + split_halfedges = std::vector>(num_paths); + path_halfedges = std::vector>(num_paths); + for (int i = 0; i < num_paths; ++i) + { + int num_pts = polyline_pts[i].size(); + split_halfedges[i] = std::vector(num_pts, -1); + path_halfedges[i] = std::vector(); + path_halfedges[i].reserve(num_pts + 1); + //is_reused[i] = std::vector(num_pts, false); + } + + for (int hij = 0; hij < num_halfedges; ++hij) + { + + const auto& edge_splits = edges_to_split[hij]; + int num_splits = edge_splits.size(); + if (num_splits == 0) continue; + std::vector bary_coords(num_splits); + for (int k = 0; k < num_splits; ++k) + { + auto [i, j] = edge_splits[k]; + bary_coords[k] = overlay_mesh.convert_overlay_to_original_coordinates(hij, polyline_pts[i][j].t); + } + + assert(edges_to_split[overlay_mesh.opp[hij]].empty()); + if (num_splits == 1) + { + auto [i, j] = edge_splits[0]; + int hik = overlay_mesh.split_edge(hij); + split_halfedges[i][j] = hik; + int vk = overlay_mesh.to[hik]; + overlay_mesh.orig.bary_coords[vk] = bary_coords[0]; + } + else + { + spdlog::debug("splitting edge {} times", num_splits); + std::vector idx(num_splits); + std::iota(idx.begin(), idx.end(), 0); + std::sort(idx.begin(), idx.end(), + [&](int i, int j) { + int ii = edge_splits[i][0]; + int ij = edge_splits[i][1]; + int ji = edge_splits[j][0]; + int jj = edge_splits[j][1]; + Scalar it = polyline_pts[ii][ij].t; + Scalar jt = polyline_pts[ji][jj].t; + //if (it == jt) return false; + return (it > jt); + }); + int hik = hij; + Scalar t_prev = 1.; + bool split_done = false; + for (int k = 0; k < num_splits; ++k) + { + auto [i, j] = edge_splits[idx[k]]; + if (is_redundant_path[i]) continue; + const auto& pt = polyline_pts[i][j]; + spdlog::debug("splitting point {} at {} in path {}", pt.index, pt.t, i); + //if ((k > 0) && float_equal(pt.t, t_prev, 1e-6)) + //if ((split_done) && (pt.t == t_prev)) + if (false) + { + spdlog::warn("intersecting geodesics {} at {} with delta {} in edge {}", i, t_prev, pt.t - t_prev, hij); + is_redundant_path[i] = true; + continue; + } + else + { + hik = overlay_mesh.split_edge(hik); + int vk = overlay_mesh.to[hik]; + overlay_mesh.orig.bary_coords[vk] = bary_coords[idx[k]]; + split_done = true; + } + t_prev = pt.t; + split_halfedges[i][j] = overlay_mesh.opp[overlay_mesh.n[hik]]; + assert(overlay_mesh.is_vertex_degree_two(overlay_mesh.to[split_halfedges[i][j]])); + } + } + } +} + +void PathStraightener::split_edges( + VoronoiOverlayMesh& overlay_mesh, + std::vector>>& edges_to_split) +{ + int num_paths = polyline_pts.size(); + int num_halfedges = overlay_mesh.n.size(); + + split_halfedges = std::vector>(num_paths); + path_halfedges = std::vector>(num_paths); + for (int i = 0; i < num_paths; ++i) + { + int num_pts = polyline_pts[i].size(); + split_halfedges[i] = std::vector(num_pts, -1); + path_halfedges[i] = std::vector(); + path_halfedges[i].reserve(num_pts + 1); + //is_reused[i] = std::vector(num_pts, false); + } + + for (int hij = 0; hij < num_halfedges; ++hij) + { + + const auto& edge_splits = edges_to_split[hij]; + int num_splits = edge_splits.size(); + if (num_splits == 0) continue; + std::vector bary_coords(num_splits); + Scalar t_prev = 1.; + for (int k = 0; k < num_splits; ++k) + { + auto [i, j] = edge_splits[k]; + Scalar t = polyline_pts[i][j].t; + bool is_valid = true; + + // check for nans + if (isnan(t)) + { + spdlog::warn("barycentric coordinate is NaN"); + is_valid = false; + } + + + // check for consistent ordering + if ((float_equal(t, t_prev)) || (t_prev < t) || (t < 1e-10)) + { + spdlog::warn("intersecting geodesics {} at {}, {} in edge {}", i, t_prev, t, hij); + is_valid = false; + } + + if (!is_valid) + { + Scalar t_next = 0.; + if (k + 1 < num_splits) + { + auto [i_n, j_n] = edge_splits[k + 1]; + t_next = polyline_pts[i_n][j_n].t; + if (isnan(t_next) || (t_next < 0.) || (t_prev <= t_next)) t_next = 0.; + } + t = 0.5 * (t_prev + t_next); // not a very satisfying fallback + } + bary_coords[k] = overlay_mesh.convert_overlay_to_original_coordinates(hij, t); + t_prev = t; + } + + assert(edges_to_split[overlay_mesh.opp[hij]].empty()); + if (num_splits == 1) + { + auto [i, j] = edge_splits[0]; + int hik = overlay_mesh.split_edge(hij); + split_halfedges[i][j] = hik; + int vk = overlay_mesh.to[hik]; + overlay_mesh.orig.bary_coords[vk] = bary_coords[0]; + } + else + { + spdlog::debug("splitting edge {} times", num_splits); + int hik = hij; + for (int k = 0; k < num_splits; ++k) + { + auto [i, j] = edge_splits[k]; + if (is_redundant_path[i]) continue; + + hik = overlay_mesh.split_edge(hik); + int vk = overlay_mesh.to[hik]; + overlay_mesh.orig.bary_coords[vk] = bary_coords[k]; + split_halfedges[i][j] = overlay_mesh.opp[overlay_mesh.n[hik]]; + assert(overlay_mesh.is_vertex_degree_two(overlay_mesh.to[split_halfedges[i][j]])); + } + } + } +} + +void PathStraightener::join_edges(VoronoiOverlayMesh& overlay_mesh) +{ + // write new edges + int num_paths = polyline_pts.size(); + redundant_edges = std::vector>(num_paths, std::vector()); + redundant_vertices = std::vector>(num_paths, std::vector()); + + // reset labels for simplified mesh, leaving vertex vertex tags + overlay_mesh.simp.edge_type = std::vector(overlay_mesh.e2h.size(), EdgeType::EF); + for (int vi = 0; vi < overlay_mesh.out.size(); ++vi) + { + if (overlay_mesh.simp.vertex_type[vi] == VertexType::VE) overlay_mesh.simp.vertex_type[vi] = VertexType::VF; + } + + for (int i = 0; i < num_paths; ++i) + { + // skip if already makred redundant + if (is_redundant_path[i]) continue; + + int Hij = overlay_mesh.simp.e2h[i]; + int Hji = overlay_mesh.simp.m.opp[Hij]; + int curr = 0; // set to 1 to skip initial vertex + int num_pts = split_halfedges[i].size(); + auto& curr_path_halfeges = path_halfedges[i]; + + // lambda to mark edge halfedges in the overlay and build the halfedge sequence + auto set_simp_edge = [&](int h) { + curr_path_halfeges.push_back(h); + if (overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] != EdgeType::EF) + { + return; + } + overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] = EdgeType::EE; + overlay_mesh.simp.halfedge_parent[h] = Hij; + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[h]] = Hji; + }; + + // lambda to mark edge vertices in the overlay + auto set_simp_vertex = [&](int v) { + if (overlay_mesh.simp.vertex_type[v] != VertexType::VF) + { + return; + } + overlay_mesh.simp.vertex_type[v] = VertexType::VE; + overlay_mesh.simp.vertex_parent[v] = i; + }; + + auto mark_redundant = [&](int h, int index) { + redundant_edges[i].push_back(index); + int h_parent = overlay_mesh.simp.halfedge_parent[h]; + int e_parent = overlay_mesh.simp.h2e[h_parent]; + is_redundant_path[i] = true; + is_redundant_path[e_parent] = true; + //spdlog::warn("edge already labeled in edge {}", i); + //spdlog::debug("edge labeled with {}", e_parent); + }; + + // undo tracing and mark path to retrace + bool aborted = false; + auto abort_path = [&]() { + spdlog::warn("aborting path {}", i); + aborted = true; + is_redundant_path[i] = true; + assert(false); + }; + + // process sequences of split vertices between original vertices in the polyline + while (curr < num_pts - 1) + { + int start = curr; + std::vector vertices = {}; + std::vector halfedges = {}; + std::vector> faces = {}; + vertices.push_back(v_new2old[polyline_pts[i][curr].index]); + curr++; + + // get sequence of split vertices until an original vertex + while (curr < num_pts) + { + if (polyline_pts[i][curr].type == SurfacePoint::Type::Face) + { + spdlog::warn("face in polyline {}", i); + abort_path(); + break; + } + else if (polyline_pts[i][curr].type == SurfacePoint::Type::Vertex) + { + int vk = v_new2old[polyline_pts[i][curr].index]; + set_simp_vertex(vk); + vertices.push_back(vk); + break; + } + else + { + int hik = split_halfedges[i][curr]; + int hki = overlay_mesh.opp[hik]; + int vk = overlay_mesh.to[hik]; + set_simp_vertex(vk); + vertices.push_back(vk); + halfedges.push_back(hik); + faces.push_back({overlay_mesh.face[hik], overlay_mesh.face[hki]}); + } + + curr++; + } + if (aborted) break; + + // casework depends on how many splits are in the current chain + int num_splits = faces.size(); + spdlog::debug("split {} edges", num_splits); + + // special case: existing edge between vertices + if (num_splits == 0) + { + assert(vertices.size() == 2); + int vi = v_old2new[vertices[0]]; + int vj = v_old2new[vertices[1]]; + int h = vv2he_o.coeff(vi, vj) - 1; + if ((overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] == EdgeType::EE) + && (overlay_mesh.h2e[overlay_mesh.simp.halfedge_parent[h]] != i)) + { + //mark_redundant(h, start); + spdlog::warn("path intersects existing edges"); + abort_path(); + break; + } + if ((overlay_mesh.to[h] != vertices[1]) || (overlay_mesh.to[overlay_mesh.opp[h]] != vertices[0])) + { + spdlog::warn("halfedge in path {} no longer exists", i); + //mark_redundant(h, start); + abort_path(); + break; + } + set_simp_edge(h); + } + // special case: if only one split, the face chain is ambiguous + else if (num_splits == 1) + { + if (!overlay_mesh.is_vertex_degree_two(vertices[1])) + { + spdlog::error("split of quad already exists"); + abort_path(); + } + + // TODO may be problematic if the endpoint vertices are the same, but this is very unlikely + int h = overlay_mesh.halfedge_from_face_vertex(faces[0][0], vertices[0]); + int g = overlay_mesh.halfedge_from_face_vertex(faces[0][0], vertices[1]); + if (h < 0) + { + h = overlay_mesh.halfedge_from_face_vertex(faces[0][1], vertices[0]); + g = overlay_mesh.halfedge_from_face_vertex(faces[0][1], vertices[1]); + overlay_mesh.split_face(h, g); + set_simp_edge(overlay_mesh.n[h]); + + h = overlay_mesh.halfedge_from_face_vertex(faces[0][0], vertices[1]); + g = overlay_mesh.halfedge_from_face_vertex(faces[0][0], vertices[2]); + overlay_mesh.split_face(h, g); + set_simp_edge(overlay_mesh.n[h]); + } + else + { + overlay_mesh.split_face(h, g); + set_simp_edge(overlay_mesh.n[h]); + + h = overlay_mesh.halfedge_from_face_vertex(faces[0][1], vertices[1]); + g = overlay_mesh.halfedge_from_face_vertex(faces[0][1], vertices[2]); + overlay_mesh.split_face(h, g); + set_simp_edge(overlay_mesh.n[h]); + + // check that order is unambiguous + assert(overlay_mesh.halfedge_from_face_vertex(faces[0][0], vertices[2]) < 0); + } + } + // general case: sequence of edge splits in a face chain + else + { + std::vector face_chain = generate_face_chain(faces); + for (int k = 0; k < num_splits + 1; ++k) + { + int f = face_chain[k]; + int vk = vertices[k]; + int vj = vertices[k + 1]; + + // special case: face already split (excluding final face of chain) + if ((!overlay_mesh.is_vertex_degree_two(vj)) && (k < num_splits)) + { + spdlog::error("face already split"); + int h = overlay_mesh.n[halfedges[k]]; + int hjk = rotate_halfedge_to_tip(overlay_mesh, h, vk); + if (hjk < 0) + { + abort_path(); + break; + } + int hkj = overlay_mesh.opp[hjk]; + assert(overlay_mesh.to[hkj] == vj); + assert(overlay_mesh.to[hjk] == vk); + //mark_redundant(hkj, start + k); + //set_simp_edge(hkj); + abort_path(); + break; + } + // special case: final face of chain already split + else if ((k == num_splits) && (overlay_mesh.vertex_degree(vk) == 4)) + { + spdlog::error("final face already split"); + int h = overlay_mesh.n[halfedges.back()]; + int hkj = rotate_halfedge_to_tip(overlay_mesh, h, vj); + if (hkj < 0) + { + abort_path(); + break; + } + assert(overlay_mesh.to[hkj] == vj); + //mark_redundant(hkj, start + k); + //set_simp_edge(hkj); + abort_path(); + break; + } + // general case: need to split face + else + { + int h = overlay_mesh.halfedge_from_face_vertex(f, vk); + int g = overlay_mesh.halfedge_from_face_vertex(f, vj); + if ((h < 0) || (g < 0)) + { + spdlog::error("could not find halfedge for split"); + abort_path(); + break; + } + assert((k == 0) ||(overlay_mesh.simp.vertex_parent[vk] == i)); + assert((k == num_splits) ||(overlay_mesh.simp.vertex_parent[vj] == i)); + overlay_mesh.split_face(h, g); + set_simp_edge(overlay_mesh.n[h]); + } + } + } + if (aborted) break; + + int v_end = overlay_mesh.to[curr_path_halfeges.back()]; + if ((curr + 1 < num_pts) && (overlay_mesh.simp.vertex_type[v_end] == VertexType::VV)) + { + spdlog::warn("vertex {} intersected by edge {}", overlay_mesh.simp.vertex_parent[v_end], i); + abort_path(); + //is_redundant_path[i] = true; + //redundant_vertices[i].push_back(curr); + } + int v_parent = overlay_mesh.simp.vertex_parent[v_end]; + if ((curr + 1 < num_pts) && (v_parent != i)) + { + spdlog::warn("vertex in edge {} already labeled in edge {}", i, v_parent); + abort_path(); + //is_redundant_path[i] = true; + //is_redundant_path[v_parent] = true; + //redundant_vertices[i].push_back(curr); + } + if (aborted) break; + } + + if (!aborted) + { + is_redundant_path[i] = false; + assert(is_valid_path(overlay_mesh, i)); + } + } + + // final triangulation + overlay_mesh.triangulate(); + + // ensure that the overlay is not topologically degenerate after tracing + //overlay_mesh.make_three_connected(overlay_mesh.orig); +} + +int PathStraightener::get_start_halfedge(const VoronoiOverlayMesh& overlay_mesh, int h) +{ + int e = overlay_mesh.simp.h2e[h]; + if (h == overlay_mesh.simp.e2h[e]) + { + return path_halfedges[e].front(); + } + else + { + return overlay_mesh.opp[path_halfedges[e].back()]; + } + +} + +bool PathStraightener::is_valid_path(VoronoiOverlayMesh& overlay_mesh, int e) +{ + int Hij = overlay_mesh.simp.e2h[e]; + int Hji = overlay_mesh.simp.m.opp[Hij]; + int Vi = overlay_mesh.simp.m.to[Hji]; + int Vj = overlay_mesh.simp.m.to[Hij]; + for (int i = 0; i < path_halfedges[e].size(); ++i) + { + int hij = path_halfedges[e][i]; + int hji = overlay_mesh.opp[hij]; + int eji = overlay_mesh.h2e[hij]; + int v = overlay_mesh.to[hij]; + if (overlay_mesh.simp.edge_type[eji] != EdgeType::EE) return errmess("halfedge not labeled edge"); + if (overlay_mesh.simp.halfedge_parent[hij] != Hij) return errmess("halfedge parent is inconsistent"); + if (overlay_mesh.simp.halfedge_parent[hij] != Hij) return errmess("halfedge parent is inconsistent"); + if (i != path_halfedges[e].size() - 1) + { + if (overlay_mesh.simp.vertex_type[v] != VertexType::VE) return errmess("interior vertex not labeled edge"); + if (overlay_mesh.simp.vertex_parent[v] != e) return errmess("interior vertex parent is inconsistent"); + } + } + + int vi = overlay_mesh.to[overlay_mesh.opp[path_halfedges[e].front()]]; + if (overlay_mesh.simp.vertex_type[vi] != VertexType::VV) return errmess("start vertex not labeled vertex"); + if (overlay_mesh.simp.vertex_parent[vi] != Vi) return errmess("start vertex parent is inconsistent"); + + int vj = overlay_mesh.to[path_halfedges[e].back()]; + if (overlay_mesh.simp.vertex_type[vj] != VertexType::VV) return errmess("end vertex not labeled vertex"); + if (overlay_mesh.simp.vertex_parent[vj] != Vj) return errmess("end vertex parent is inconsistent"); + + return true; +} + +// need to first connect all vertices, then any holononomy handles, then faces +std::vector PathStraightener::compute_spanning_paths(VoronoiOverlayMesh& overlay_mesh) +{ + auto& m = overlay_mesh.simp.m; + + // get components of already traced paths + UnionFind vertex_components(m.out.size()); + for (int eij = 0; eij < is_redundant_path.size(); ++eij) + { + if (is_redundant_path[eij]) continue; + + // union vertices joined by edge + int hij = overlay_mesh.simp.e2h[eij]; + int hji = m.opp[hij]; + int vi = m.to[hji]; + int vj = m.to[hij]; + vertex_components.union_sets(vi, vj); + } + + std::vector spanning_paths = {}; + for (int eij = 0; eij < is_redundant_path.size(); ++eij) + { + // only process redundant paths + if (!is_redundant_path[eij]) continue; + + // check if edge joins separate components + int hij = overlay_mesh.simp.e2h[eij]; + int hji = m.opp[hij]; + int vi = m.to[hji]; + int vj = m.to[hij]; + if (vertex_components.find_set(vi) != vertex_components.find_set(vj)) + { + vertex_components.union_sets(vi, vj); + spanning_paths.push_back(eij); + } + } + + return spanning_paths; +} + +void PathStraightener::retrace_paths(VoronoiOverlayMesh& overlay_mesh) +{ + std::vector redundant_paths = enumerate_boolean_array(is_redundant_path); + int num_redundant = redundant_paths.size(); + if (num_redundant == 0) return; + spdlog::info("{} redundant paths to retrace", num_redundant); + auto& m = overlay_mesh.simp.m; + + for (int e : redundant_paths) + { + remove_path(overlay_mesh, e); + } + + std::vector vertex_locations(m.out.size(), -1); + for (int vi = 0; vi < overlay_mesh.out.size(); ++vi) + { + if (overlay_mesh.simp.vertex_type[vi] == VertexType::VV) + { + vertex_locations[overlay_mesh.simp.vertex_parent[vi]] = vi; + } + + // reset vertices in deleted path + else if (overlay_mesh.simp.vertex_type[vi] == VertexType::VE) + { + if (is_redundant_path[overlay_mesh.simp.vertex_parent[vi]]) + { + overlay_mesh.simp.vertex_type[vi] = VertexType::VF; + } + } + } + + // check path consistency for remaining paths + for (int e = 0; e < path_halfedges.size(); ++e) + { + if (is_redundant_path[e]) continue; + if (!is_valid_path(overlay_mesh, e)) spdlog::error("path {} broken before tracing", e); + } + + overlay_mesh.make_regular(); + + + // trace paths with no adjacent vertex + for (int e : redundant_paths) + { + int Hij = overlay_mesh.simp.e2h[e]; + int Hji = m.opp[Hij]; + int Vi = m.to[Hji]; + int Vj = m.to[Hij]; + + // get next clockwise nonredundant edge + int Hik = Hij; + do + { + if (!is_redundant_path[overlay_mesh.simp.h2e[Hik]]) break; + Hik = m.n[m.opp[Hik]]; + } while (Hik != Hij); + + // get next clockwise nonredundant edge + int Hjl = Hji; + do + { + if (!is_redundant_path[overlay_mesh.simp.h2e[Hjl]]) break; + Hjl = m.n[m.opp[Hjl]]; + } while (Hjl != Hji); + if ((Hik != Hij) && (Hjl != Hji)) continue; + + // retrace path + spdlog::debug("tracing {} = {}, {}", e, Hij, Hji); + int v_start = vertex_locations[Vi]; + int v_end = vertex_locations[Vj]; + + // trace new path + std::vector halfedges; + overlay_mesh.trace_path(v_start, v_end, Hij, e, halfedges); + for (int h : halfedges) + { + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[h]] = Hji; + } + overlay_mesh.make_adjacent_regular(halfedges); + + path_halfedges[e] = reverse_vector(halfedges); + is_redundant_path[e] = false; + } + + // check path consistency for remaining paths + for (int e = 0; e < path_halfedges.size(); ++e) + { + if (is_redundant_path[e]) continue; + if (!is_valid_path(overlay_mesh, e)) + { + spdlog::error("path {} broken after tracing isolated vertices", e); + remove_path(overlay_mesh, e); + is_redundant_path[e] = true; + redundant_paths.push_back(e); + } + } + + // first, make sure all vertices are connected + std::vector spanning_paths = compute_spanning_paths(overlay_mesh); + for (int e : spanning_paths) + { + if (!is_redundant_path[e]) continue; + spdlog::debug("tracing spanning path {} with consistent orientation", e); + retrace_consistent_path(overlay_mesh, e); + } + + for (int e : redundant_paths) + { + if (!is_redundant_path[e]) continue; + spdlog::debug("tracing remaining path {} with consistent orientation", e); + retrace_consistent_path(overlay_mesh, e); + } +} + +void PathStraightener::retrace_consistent_path(VoronoiOverlayMesh& overlay_mesh, int e) +{ + auto& m = overlay_mesh.simp.m; + int Hij = overlay_mesh.simp.e2h[e]; + int Hji = m.opp[Hij]; + int vi = m.to[Hji]; + int vj = m.to[Hij]; + int hij, hji; + spdlog::debug("tracing path {} from {} to {}", e, vi, vj); + + // get next clockwise nonredundant edge + int Hik = Hij; + do + { + if (!is_redundant_path[overlay_mesh.simp.h2e[Hik]]) break; + Hik = m.n[m.opp[Hik]]; + } while (Hik != Hij); + assert(Hik != Hij); + int hik = get_start_halfedge(overlay_mesh, Hik); + hij = overlay_mesh.opp[overlay_mesh.prev[hik]]; + + // get next clockwise nonredundant edge + int Hjl = Hji; + do + { + if (!is_redundant_path[overlay_mesh.simp.h2e[Hjl]]) break; + Hjl = m.n[m.opp[Hjl]]; + } while (Hjl != Hji); + assert(Hjl != Hji); + int hjl = get_start_halfedge(overlay_mesh, Hjl); + hji = overlay_mesh.opp[overlay_mesh.prev[hjl]]; + + // rotate edges counterclockwise + overlay_mesh.simp.halfedge_parent[hij] = Hij; + overlay_mesh.simp.halfedge_parent[hji] = Hji; + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[hij]] = Hji; + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[hji]] = Hij; + for (int h : {hij, hji}) + { + assert(overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] == EdgeType::EF); + overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] = EdgeType::EE; + + int v = overlay_mesh.to[h]; + assert((overlay_mesh.simp.vertex_type[v] == VertexType::VF) || (overlay_mesh.simp.vertex_parent[v] == e)); + overlay_mesh.simp.vertex_type[v] = VertexType::VE; + overlay_mesh.simp.vertex_parent[v] = e; + } + + int v_start = overlay_mesh.to[hij]; + int v_end = overlay_mesh.to[hji]; + + // trace new path + std::vector halfedges; + overlay_mesh.trace_path(v_start, v_end, Hij, e, halfedges); + is_redundant_path[e] = false; + path_halfedges[e] = {hij}; + for (int i = halfedges.size() - 1; i >= 0; --i) + { + int h = halfedges[i]; + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[h]] = Hji; + path_halfedges[e].push_back(h); + } + path_halfedges[e].push_back(overlay_mesh.opp[hji]); + overlay_mesh.make_adjacent_regular(path_halfedges[e]); +} + +/* + return; + int num_faces = m.n_faces(); + int num_traced = 0; + do + { + num_traced = 0; + for (int f = 0; f < num_faces; ++f) + { + int hij = m.h[f]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + int count = 0; + int e = 0; + int h_ref = 0; + for (int h : {hij, hjk, hki}) + { + if (!is_redundant_path[overlay_mesh.simp.h2e[h]]) + { + count++; + h_ref = h; + } + else + { + e = overlay_mesh.simp.h2e[h]; + } + + } + + if (count == 2) + { + spdlog::info("tracing {} in face {}", e, f); + for (int h = 0; h < overlay_mesh.n.size(); ++h) + { + if (overlay_mesh.is_halfedge_deleted(h)) continue; + if (overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] != EdgeType::EE) continue; + if (overlay_mesh.simp.halfedge_parent[h] == h_ref) + { + spdlog::info("flooding {} in path {}", h, overlay_mesh.simp.h2e[h_ref]); + overlay_mesh.flood_face(h, f); + break; + } + } + retrace_path(overlay_mesh, e, f); + is_redundant_path[e] = false; + num_traced++; + } + } + } + while (num_traced > 0); + + + //for (int e : redundant_paths) + //{ + // retrace_path(overlay_mesh, e); + //} +} +*/ + +void PathStraightener::separate_paths(VoronoiOverlayMesh& overlay_mesh) +{ + std::vector redundant_paths = enumerate_boolean_array(is_redundant_path); + int num_redundant = redundant_paths.size(); + spdlog::info("{} redundant paths to separate", num_redundant); + + overlay_mesh.make_regular(); + overlay_mesh.view(); + halfedge_counts = std::vector(overlay_mesh.e2h.size(), 0); + std::vector vertex_counts(overlay_mesh.out.size(), 0); + for (int i : redundant_paths) + { + int vi = overlay_mesh.to[overlay_mesh.opp[path_halfedges[i].front()]]; + int Hij = overlay_mesh.simp.e2h[i]; + int Hji = overlay_mesh.simp.m.opp[Hij]; + vertex_counts[vi] += 1; + for (int h : path_halfedges[i]) + { + halfedge_counts[overlay_mesh.h2e[h]] += 1; + vertex_counts[overlay_mesh.to[h]] += 1; + } + } + + for (int e : redundant_paths) + { + bool successs = separate_simple_path(overlay_mesh, e); + if (!successs) + { + spdlog::warn("could not simplify {}", e); + } + } +} + +void PathStraightener::remove_path(VoronoiOverlayMesh& overlay_mesh, int e) +{ + for (int h : path_halfedges[e]) + { + int vi = overlay_mesh.to[h]; + if (overlay_mesh.simp.h2e[overlay_mesh.simp.halfedge_parent[h]] == e) + { + overlay_mesh.simp.edge_type[overlay_mesh.h2e[h]] = EdgeType::EF; + } + if ((overlay_mesh.simp.vertex_type[vi] == VertexType::VE) && (overlay_mesh.simp.vertex_parent[vi] == e)) + { + overlay_mesh.simp.vertex_type[vi] = VertexType::VF; + } + } +} + +void PathStraightener::retrace_path(VoronoiOverlayMesh& overlay_mesh, int e, int f) +{ + int Hij = overlay_mesh.simp.e2h[e]; + int Hji = overlay_mesh.simp.m.opp[Hij]; + int v_start = overlay_mesh.to[overlay_mesh.opp[path_halfedges[e].front()]]; + int v_end = overlay_mesh.to[path_halfedges[e].back()]; + + // trace new path + std::vector halfedges; + overlay_mesh.trace_path(v_start, v_end, Hij, e, halfedges, f); + for (int h : halfedges) + { + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[h]] = Hji; + } + overlay_mesh.make_adjacent_regular(halfedges); + + path_halfedges[e] = reverse_vector(halfedges); +} + +bool PathStraightener::separate_simple_path(VoronoiOverlayMesh& overlay_mesh, int e) +{ + // check if starting from a simple halfedge + int h_start = path_halfedges[e][0]; + if (halfedge_counts[overlay_mesh.h2e[h_start]] > 1) return false; + int Hij = overlay_mesh.simp.e2h[e]; + int Hji = overlay_mesh.simp.m.opp[Hij]; + + // get the first edge of the path and determine if it is simple + bool was_simple = true; + int prev = 0; + int next = -1; + int path_length = path_halfedges[e].size(); + std::vector new_path = {h_start}; + for (int i = 1; i < path_length; ++i) + { + // make sure no intersections from the left hand side + int hki = path_halfedges[e][i - 1]; + int hij = path_halfedges[e][i]; + if (overlay_mesh.get_clockwise_overlay_edge(hki, overlay_mesh.simp) != hij) return false; + + // check if current edge is simple + bool is_simple = halfedge_counts[overlay_mesh.h2e[hij]] == 1; + + // if still traversing simple region, update previous halfedge and add to path + if ((was_simple) && (is_simple)) + { + new_path.push_back(hij); + prev = 0; + } + // if entering complex region, remove previous path edge from path + else if ((was_simple) && (!is_simple)) + { + new_path.pop_back(); + } + // if transitioning from complex to simple region, trace the new path + else if ((!was_simple) && (is_simple)) + { + next = i; + int h_prev = path_halfedges[e][prev]; + int h_next = path_halfedges[e][next]; + int v_prev, v_next; + if (prev > 0) + { + overlay_mesh.simp.edge_type[overlay_mesh.h2e[h_prev]] = EdgeType::EF; + v_prev = overlay_mesh.to[overlay_mesh.opp[h_prev]]; + } + else + { + new_path.push_back(h_prev); + v_prev = overlay_mesh.to[h_prev]; + } + if (next < (path_length - 1)) + { + overlay_mesh.simp.edge_type[overlay_mesh.h2e[h_next]] = EdgeType::EF; + v_prev = overlay_mesh.to[h_next]; + } + else + { + v_next = overlay_mesh.to[overlay_mesh.opp[h_next]]; + } + // TODO make local + overlay_mesh.make_regular(); + + // trace new path + std::vector halfedges; + overlay_mesh.trace_path(v_prev, v_next, Hij, e, halfedges); + for (int h : halfedges) + { + new_path.push_back(h_prev); + overlay_mesh.simp.halfedge_parent[overlay_mesh.opp[h]] = Hji; + } + overlay_mesh.make_adjacent_regular(halfedges); + for (int j = prev + 1; j < next; ++j) + { + int h = path_halfedges[e][j]; + halfedge_counts[overlay_mesh.h2e[h]] -= 1; + } + overlay_mesh.view(); + } + // nothing to do if still in complex region + + was_simple = is_simple; + } + + return true; +} + +// rotate hij until it points to vk +int PathStraightener::rotate_halfedge_to_tip(const VoronoiOverlayMesh& overlay_mesh, int hij, int vk) const +{ + int h = hij; + do + { + if (overlay_mesh.to[h] == vk) return h; + h = overlay_mesh.n[overlay_mesh.opp[h]]; + } + while (h != hij); + + return -1; +} + +std::vector PathStraightener::generate_face_chain(const std::vector>& faces) const +{ + std::vector face_chain; + int num_splits = faces.size(); + face_chain.reserve(num_splits + 1); + int j = (vector_contains(faces[1], faces[0][0])) ? 1 : 0; + + // TODO: face chain is invalid in cases of shared edges; need to reconsider + //assert((j == 1) || (vector_contains(faces[1], faces[0][1]))); + + face_chain.push_back(faces[0][j]); + for (int k = 0; k < num_splits - 1; ++k) + { + j = (faces[k][0] == face_chain.back()) ? 1 : 0; + face_chain.push_back(faces[k][j]); + } + j = (faces.back()[0] == face_chain.back()) ? 1 : 0; + face_chain.push_back(faces.back()[j]); + spdlog::debug("built face chain of length {}", face_chain.size()); + + return face_chain; +} + +void PathStraightener::run(VoronoiOverlayMesh& overlay_mesh) +{ + //view(overlay_mesh); + split_edges(overlay_mesh); + join_edges(overlay_mesh); + retrace_paths(overlay_mesh); +} + +Eigen::Vector3d get_surface_point_position( + Eigen::MatrixXd& V_o, + VoronoiOverlayMesh& overlay_mesh, + SurfacePoint& pt) +{ + if (pt.type == SurfacePoint::Type::Vertex) + { + return V_o.row(pt.index); + } + else if (pt.type == SurfacePoint::Type::Edge) + { + Eigen::Vector3d v0 = V_o.row(pt.edge_base); + Eigen::Vector3d v1 = V_o.row(pt.edge_tip); + double t = (double)(pt.t); + return ((1. - t) * v0) + (t * v1); + } + else if (pt.type == SurfacePoint::Type::Face) + { + spdlog::error("faces not supported"); + } + + return Eigen::Vector3d(); +} + +std::vector> get_surface_point_positions( + VoronoiOverlayMesh& overlay_mesh, + std::vector>& polyline_pts +) +{ + Eigen::MatrixXd V_o = overlay_mesh.generate_overlay_vertices( + overlay_mesh.V_orig, + overlay_mesh.vtx_reindex_orig, + overlay_mesh.orig); + int num_paths = polyline_pts.size(); + std::vector> positions(num_paths); + for (int i = 0; i < num_paths; ++i) + { + int path_size = polyline_pts[i].size(); + positions[i].resize(path_size); + for (int j = 0; j < path_size; ++j) + { + positions[i][j] = get_surface_point_position( + V_o, + overlay_mesh, + polyline_pts[i][j]); + } + } + + return positions; +} + +void PathStraightener::view( + VoronoiOverlayMesh& overlay_mesh +) +{ + std::vector> polyline = get_surface_point_positions(overlay_mesh, polyline_pts); + auto [nodes, edges, labels] = convert_polyline(polyline); + //Eigen::MatrixXi F_c = edgeNetwork.tri->getFaceVertexMatrix(); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh("simplified mesh", V, F); + polyscope::registerCurveNetwork("edges", nodes, edges); + polyscope::getCurveNetwork("edges")->addEdgeScalarQuantity("edge parent", labels); + polyscope::show(); +#endif +} + + + +void add_geodesic_paths( + VoronoiOverlayMesh& overlay_mesh, + const std::vector>& paths +) { + std::vector> paths_init = overlay_mesh.enumerate_simplified_edges(); + std::vector> polyline_pts = generate_geodesic_path_points(overlay_mesh, paths_init); + PathStraightener path_straightener(overlay_mesh, polyline_pts); + path_straightener.split_edges(overlay_mesh); + path_straightener.join_edges(overlay_mesh); + path_straightener.retrace_paths(overlay_mesh); +} + +} +} diff --git a/src/resolution/core/layout.cpp b/src/resolution/core/layout.cpp new file mode 100644 index 0000000..ed133f3 --- /dev/null +++ b/src/resolution/core/layout.cpp @@ -0,0 +1,178 @@ +#include "resolution/core/layout.h" + +namespace Penner { +namespace Resolution { + +double compute_seamless_error(const Eigen::Matrix2d& R) +{ + // goal: check fourth power is identity + Eigen::Matrix2d R2 = R * R; + Eigen::Matrix2d R4 = R2 * R2; + + // compute max abs error + double error = 0.; + error = max(error, abs(R4(0, 0) - 1.)); + error = max(error, abs(R4(1, 0))); + error = max(error, abs(R4(0, 1))); + error = max(error, abs(R4(1, 1) - 1.)); + + return error; +} + +double compute_seamless_error( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij) +{ + return compute_seamless_error(infer_float_rotation(halfedge, uv, hij)); +} + +bool is_seamless_rotation(const Eigen::Matrix2d& R) +{ + return (float_equal(compute_seamless_error(R), 0., 1e-6)); +} + +Eigen::Matrix2d infer_float_rotation( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij +) +{ + // get relevant halfedge uv data + int hji = halfedge.opp[hij]; + Eigen::Vector2d uvi = uv[halfedge.prev[hij]]; + Eigen::Vector2d uvj = uv[hij]; + Eigen::Vector2d uvio = uv[hji]; + Eigen::Vector2d uvjo = uv[halfedge.prev[hji]]; + + // check for trivial identity case + if ((uvi == uvio) && (uvj == uvjo)) + { + return Eigen::Matrix2d::Identity(); + } + + // compute edge vectors + Eigen::Vector2d dij = uvj - uvi; + Eigen::Vector2d dijo = uvjo - uvio; + assert(float_equal(dij.norm(), dijo.norm())); + + auto perp = [](const Eigen::Vector2d& v) -> Eigen::Vector2d { + return Eigen::Vector2d(-v.y(), v.x()); + }; + + // Set matrices with edge vectors as columns + Eigen::Matrix2d B1, B2; + B1.col(0) = dij; + B1.col(1) = perp(dij); + B2.col(0) = dijo; + B2.col(1) = perp(dijo); + + // the rotation is the unique matrix that maps the edge vectors + // to each other + return B1 * B2.inverse(); +} + +Eigen::Matrix2d infer_rotation( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij +) +{ + // compute rotation with generic method (for metric parametrizations) + Eigen::Matrix2d R = infer_float_rotation(halfedge, uv, hij); + assert(is_seamless_rotation(R)); + + // round to integer valued matrix for stability + return R.array().round().matrix(); +} + +Eigen::Matrix2d transform_opposite_face( + const HalfedgeMesh& halfedge, + std::vector& uv, + int hij +) { + int hji = halfedge.opp[hij]; + int hjk = halfedge.n[hij]; + int hki = halfedge.n[hjk]; + int hil = halfedge.n[hji]; + int hlj = halfedge.n[hil]; + + // compute the rotation to carry the mate onto the halfedge + Eigen::Matrix2d R = infer_rotation(halfedge, uv, hij); + + // compute the edge vector of the opposite vertex in the face to move + Eigen::Vector2d uvio = uv[hji]; + Eigen::Vector2d uvlo = uv[hil]; + Eigen::Vector2d dilo = uvlo - uvio; + + // set the new opposite face vertices, laying out the opposite vertex using + // the rotated edge vector rooted at the fixed input halfedge + Eigen::Vector2d uvi = uv[hki]; + Eigen::Vector2d uvj = uv[hij]; + uv[hji] = uvi; + uv[hlj] = uvj; + uv[hil] = uvi + (R * dilo); + + assert(float_equal(dilo.norm(), (uv[hil] - uv[hji]).norm())); + assert(float_equal((uvlo - uv[hlj]).norm(), (uv[hil] - uv[hlj]).norm())); + + // return rotation matrix for convenience + return R; +} + +Eigen::Vector3d rotate_in_face_plane( + const HalfedgeMesh& halfedge, + const std::vector& V, + const Eigen::Matrix2d& R, + const Eigen::Vector3d& d, + int face_index) +{ + // get face vertices + const int hij = halfedge.f2h[face_index]; + const int hjk = halfedge.n[hij]; + const int hki = halfedge.n[hjk]; + const int vi = halfedge.to[hki]; + const int vj = halfedge.to[hij]; + const int vk = halfedge.to[hjk]; + + // compute local frame for the face + const Eigen::Vector3d eij = V[vj] - V[vi]; + const Eigen::Vector3d eik = V[vk] - V[vi]; + Eigen::Vector3d b1 = eij.normalized(); + Eigen::Vector3d n = eij.cross(eik).normalized(); + Eigen::Vector3d b2 = n.cross(b1).normalized(); + + // compute local frame coordinates of the tangent vector + Eigen::Vector2d d_local(d.dot(b1), d.dot(b2)); + + // apply rotation to 2D frame coordinates + Eigen::Vector2d d_rot = R * d_local; + + // expand back to 3D vector coefficients + return d_rot[0] * b1 + d_rot[1] * b2; +} + +Eigen::Matrix2d transform_opposite_face( + const HalfedgeMesh& halfedge, + std::vector& uv, + const std::vector& V, + std::vector& PD1, + std::vector& PD2, + int hij +) { + // transform rotation compute rotation + Eigen::Matrix2d R = transform_opposite_face(halfedge, uv, hij); + + // rotate the opposite face target fields so that the norm of the error is invariant + // a clockwise rotation of the triangle results in a counterclockwise rotation of the + // gradient, and vice versa + const int hji = halfedge.opp[hij]; + const int fjil = halfedge.face[hji]; + PD1[fjil] = rotate_in_face_plane(halfedge, V, R.transpose(), PD1[fjil], fjil); + PD2[fjil] = rotate_in_face_plane(halfedge, V, R.transpose(), PD2[fjil], fjil); + + return R; +} + +} +} \ No newline at end of file diff --git a/src/resolution/core/quad_quality.cpp b/src/resolution/core/quad_quality.cpp new file mode 100644 index 0000000..073ecaa --- /dev/null +++ b/src/resolution/core/quad_quality.cpp @@ -0,0 +1,185 @@ + +#include "resolution/core/quad_quality.h" + +namespace Penner { +namespace Resolution { + +Vector3 normalize(const Vector3& d) +{ + return (d.squaredNorm() > 1e-24) ? d.normalized() : d; +} + +// compute the four edge direction vectors of a quad +std::vector> compute_quad_edges( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + int num_faces = F.rows(); + std::vector> quad_edges(num_faces); + for (int f = 0; f < num_faces; ++f) + { + Vector3 x0 = V.row(F(f, 0)); + Vector3 x1 = V.row(F(f, 1)); + Vector3 x2 = V.row(F(f, 2)); + Vector3 x3 = V.row(F(f, 3)); + quad_edges[f][0] = x1 - x0; + quad_edges[f][1] = x2 - x1; + quad_edges[f][2] = x3 - x2; + quad_edges[f][3] = x0 - x3; + } + + return quad_edges; +} + +std::vector> compute_quad_lengths( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + // get the four quad edges + std::vector> quad_edges = compute_quad_edges(V, F); + + // get the lengths of the edges + int num_faces = F.rows(); + std::vector> quad_lengths(num_faces); + for (int f = 0; f < num_faces; ++f) + { + for (int i = 0; i < 4; ++i) + { + quad_lengths[f][i] = quad_edges[f][i].norm(); + } + } + + return quad_lengths; +} + +// compute area weighted normals of quad corners, i.e., cross products +// of adjacent quad edges +std::vector> compute_quad_normals( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + std::vector> quad_edges = compute_quad_edges(V, F); + + int num_faces = F.rows(); + std::vector> quad_normals(num_faces); + for (int f = 0; f < num_faces; ++f) + { + for (int i = 0; i < 4; ++i) + { + int j = (i + 3) % 4; + quad_normals[f][i] = quad_edges[f][j].cross(quad_edges[f][i]); + } + } + + return quad_normals; +} + +// compute area of quads corresponding to corners +std::vector> compute_quad_vertex_areas( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + std::vector> quad_normals = compute_quad_normals(V, F); + + int num_faces = F.rows(); + std::vector> quad_vertex_areas(num_faces); + for (int f = 0; f < num_faces; ++f) + { + Vector3 x0 = V.row(F(f, 0)); + Vector3 x1 = V.row(F(f, 1)); + Vector3 x2 = V.row(F(f, 2)); + Vector3 x3 = V.row(F(f, 3)); + + // compute princpal normal + Vector3 X1 = (x1 - x0) + (x2 - x3); + Vector3 X2 = (x2 - x1) + (x3 - x0); + Vector3 Nc = X1.cross(X2); + Vector3 nc = normalize(Nc); + + // compute vertex areas + for (int i = 0; i < 4; ++i) + { + quad_vertex_areas[f][i] = nc.dot(quad_normals[f][i]); + } + } + + return quad_vertex_areas; +} + +// compute the angle between two vectors +Scalar compute_quad_corner_angle( + const Vector3& d20, + const Vector3& d01, + bool is_inverted) +{ + Scalar dot_prod = -d20.dot(d01); + Scalar norm_prod = d20.norm() * d01.norm(); + if (norm_prod < 1e-24) return 0.; + + // compute the angle between [0, pi] + Scalar angle = acos(max(min(dot_prod / norm_prod, 1.), -1.)); + + // if the angle is known to be obtuse, subtract complementary acute angle from 2 pi + return (is_inverted) ? ((2. * PI) - angle) : angle; +} + +std::vector> compute_quad_angles( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + std::vector> quad_edges = compute_quad_edges(V, F); + std::vector> quad_vertex_areas = compute_quad_vertex_areas(V, F); + + int num_faces = F.rows(); + std::vector> quad_angles(num_faces); + for (int f = 0; f < num_faces; ++f) + { + for (int i = 0; i < 4; ++i) + { + int j = (i + 3) % 4; + + // determine if inverted angle from quad areas + bool is_inverted = (quad_vertex_areas[f][i] < 0.); + + // compute angle of quad corner + quad_angles[f][i] = compute_quad_corner_angle( + quad_edges[f][j], + quad_edges[f][i], + is_inverted); + } + } + + return quad_angles; +} + +std::vector compute_scaled_jacobian( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F +) +{ + std::vector> quad_vertex_areas = compute_quad_vertex_areas(V, F); + std::vector> quad_lengths = compute_quad_lengths(V, F); + + int num_faces = F.rows(); + std::vector scaled_jacobian(num_faces); + for (int f = 0; f < num_faces; ++f) + { + Eigen::Vector4 vals; + for (int i = 0; i < 4; ++i) + { + int j = (i + 3) % 4; + vals[i] = quad_vertex_areas[f][i] / (quad_lengths[f][i] * quad_lengths[f][j]); + } + scaled_jacobian[f] = vals.minCoeff(); + } + + return scaled_jacobian; +} + +} +} diff --git a/src/resolution/core/triangulation.cpp b/src/resolution/core/triangulation.cpp new file mode 100644 index 0000000..601bddb --- /dev/null +++ b/src/resolution/core/triangulation.cpp @@ -0,0 +1,298 @@ +#include "resolution/core/triangulation.h" + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner +{ +namespace Resolution +{ + +Vector2 rotate_counterclockwise(const Vector2&p) +{ + return {-p[1], p[0]}; +} + +bool does_half_plane_contain( + const Vector2& p, + const Vector2& x0, + const Vector2& x1, + Scalar epsilon) +{ + Vector2 d = p - x0; // direction to p + Vector2 edge = (x1 - x0).normalized(); // direction along edge + Vector2 edge_perp = rotate_counterclockwise(edge); + return (edge_perp.dot(d) > (-epsilon)); +} + +Scalar compute_triangle_area( + const Vector2& x0, + const Vector2& x1, + const Vector2& x2) +{ + Vector2 a = x1 - x0; + Vector2 b = x2 - x0; + Vector2 a_perp = rotate_counterclockwise(a); + return 0.5 * a_perp.dot(b); +} + +Scalar cross(const Vector2& a, const Vector2& b) +{ + return (a[0] * b[1]) - (a[1] * b[0]); +} + +Scalar orient( + const Vector2& x0, + const Vector2& x1, + const Vector2& x2) +{ + return cross(x1 - x0, x2 - x0); +} + +Scalar compute_triangle_angle( + const Vector2& x0, + const Vector2& x1, + const Vector2& x2) +{ + Vector2 a = (x1 - x0).normalized(); + Vector2 b = (x2 - x0).normalized(); + return atan2(a[0] * b[1] - a[1] * b[0], a[0] * b[0] + a[1] * b[1]); +} + +Scalar compute_triangle_angle( + const Vector3& x0, + const Vector3& x1, + const Vector3& x2) +{ + Vector3 a = (x1 - x0); + Vector3 b = (x2 - x0); + return atan2(a.cross(b).norm(), a.dot(b)); +} + +bool do_edges_intersect( + const Vector2& x0, + const Vector2& x1, + const Vector2& y0, + const Vector2& y1) +{ + Scalar ox0 = orient(y0, y1, x0); + Scalar ox1 = orient(y0, y1, x1); + Scalar oy0 = orient(x0, x1, y0); + Scalar oy1 = orient(x0, x1, y1); + return (((ox0 * ox1) < 0.) && ((oy0 * oy1) < 0.)); +} + +bool does_oriented_triangle_contain( + const Vector2& p, + const Vector2& x0, + const Vector2& x1, + const Vector2& x2, + Scalar epsilon) +{ + bool c0 = (does_half_plane_contain(p, x0, x1, epsilon)); + bool c1 = (does_half_plane_contain(p, x1, x2, epsilon)); + bool c2 = (does_half_plane_contain(p, x2, x0, epsilon)); + return (c0 && c1 && c2); +} + +bool does_triangle_contain( + const Vector2& p, + const Vector2& x0, + const Vector2& x1, + const Vector2& x2, + Scalar epsilon) +{ + if (does_oriented_triangle_contain(p, x0, x1, x2, epsilon)) return true; + if (does_oriented_triangle_contain(p, x0, x2, x1, epsilon)) return true; + return false; +} + + +EarClipper::EarClipper(const std::vector& polygon_vertices) +: vertices(polygon_vertices) +{ + int num_vertices = vertices.size(); + + // build linked list data structure + next.resize(num_vertices); + prev.resize(num_vertices); + for (int i = 0; i < num_vertices; ++i) + { + int ni = (i + 1) % num_vertices; + next[i] = ni; + prev[ni] = i; + } + + // determine which vertices are reflex (start with all true to avoid need to check later) + is_reflex_vertex = std::vector(num_vertices, true); + for (int i = 0; i < num_vertices; ++i) + { + update_reflex_vertex(i); + } +} + +void EarClipper::triangulate_fast() +{ + int i = 0; + + // clip ears until only one triangle remains + int num_edges = vertices.size(); + int count = 0; + while (num_edges > 3) + { + int ni = next[i]; + if (is_vertex_ear(i)) + { + clip_ear(i); + --num_edges; + count = 0; + } + i = ni; + ++count; + + // check if failed to clip ear + if (count > num_edges) + { + spdlog::error("failed to clip ear of {}-gon", num_edges); + return; + } + } + + // add final triangle + triangles.push_back({i, next[i], next[next[i]]}); +} + +// TODO: get maximum area ear +void EarClipper::triangulate_optimal() +{ + int i = 0; + + // clip ears until only one triangle remains + int num_edges = vertices.size(); + int count = 0; + while (num_edges > 3) + { + int start = i; + Scalar min_angle = 2 * PI; + int ear_candidate = i; + do + { + if (is_vertex_ear(i)) + { + Scalar angle = compute_triangle_angle(vertices[i], vertices[next[i]], vertices[prev[i]]); + if (angle < min_angle) + { + min_angle = angle; + ear_candidate = i; + } + } + + i = next[i]; + } while (i != start); + + int ni = next[ear_candidate]; + clip_ear(ear_candidate); + --num_edges; + i = ni; + } + + // add final triangle + triangles.push_back({i, next[i], next[next[i]]}); +} + +void EarClipper::triangulate() +{ + triangulate_optimal(); +} + +void EarClipper::update_reflex_vertex(int i) +{ + // vertex cannot become reflex during clipping + if (!is_reflex_vertex[i]) return; + + int pi = prev[i]; + int ni = next[i]; + // use negative offset to mark nearly colinear vertices as reflex + is_reflex_vertex[i] = !does_half_plane_contain(vertices[pi], vertices[i], vertices[ni], -epsilon); +} + +bool EarClipper::does_corner_contain(const Vector2& p, int i) const +{ + int pi = prev[i]; + int ni = next[i]; + return does_triangle_contain(p, vertices[pi], vertices[i], vertices[ni], epsilon); +} + +bool EarClipper::is_vertex_ear(int i) const +{ + // vertex must be convex + if (is_reflex_vertex[i]) return false; + + int ni = next[i]; + int pi = prev[i]; + int ci = next[ni]; + + // check if any vertex is contained in the corner of vertex i + while (ci != pi) + { + if ((is_reflex_vertex[ci]) && (does_corner_contain(vertices[ci], i))) return false; + ci = next[ci]; + } + + return true; +} + +void EarClipper::clip_ear(int i) +{ + // add clipped ear to triangulation + int pi = prev[i]; + int ni = next[i]; + edges.push_back({pi, ni}); + triangles.push_back({pi, i, ni}); + + // remove ear from the remaining polygon + next[i] = i; + prev[i] = i; + next[pi] = ni; + prev[ni] = pi; + + // check if adjacent vertices are now reflex + update_reflex_vertex(pi); + update_reflex_vertex(ni); +} + +void EarClipper::view() const +{ + int num_vertices = vertices.size(); + std::vector> boundary; + boundary.reserve(num_vertices); + for (int i = 0; i < num_vertices; ++i) + { + int ni = (i + 1) % num_vertices; + boundary.push_back({i, ni}); + } + + int num_triangles = triangles.size(); + std::vector areas(num_triangles); + for (int i = 0; i < num_triangles; ++i) + { + auto [vi, vj, vk] = triangles[i]; + areas[i] = compute_triangle_area(vertices[vi], vertices[vj], vertices[vk]); + } + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerPointCloud2D("vertices", vertices); + polyscope::registerCurveNetwork2D("boundary", vertices, boundary); + polyscope::registerCurveNetwork2D("edges", vertices, edges); + polyscope::registerSurfaceMesh2D("triangles", vertices, triangles); + polyscope::getSurfaceMesh("triangles")->addFaceScalarQuantity("area", areas); + polyscope::show(); +#endif +} + +} // namespace Resolution +} // namespace Penner \ No newline at end of file diff --git a/src/resolution/core/viewer.cpp b/src/resolution/core/viewer.cpp new file mode 100644 index 0000000..85afce7 --- /dev/null +++ b/src/resolution/core/viewer.cpp @@ -0,0 +1,138 @@ + +#include "resolution/core/viewer.h" +#include "metric/viewer.h" +#include "parametrization/layout.h" +#include "util/vector.h" +#include "util/vf_mesh.h" +#include "SYMDIR_NEW.h" +#include "igl/grad.h" + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/point_cloud.h" +#include "polyscope/curve_network.h" +#include "polyscope/surface_mesh.h" +#endif + +namespace Penner { +namespace Resolution { + +void view_layout(const MarkedPennerConeMetric& marked_metric, bool show) +{ + auto [F, uv, FT] = compute_layout_VF(marked_metric); + + int num_faces = FT.rows(); + Eigen::MatrixXi F_halfedge(num_faces, 3); + for (int fijk = 0; fijk < num_faces; ++fijk) { + for (int i : {0, 1, 2}) + { + int j = (i + 1) % 3; + F_halfedge(fijk, i) = FT(fijk, j); + } + } + + int num_halfedges = marked_metric.n_halfedges(); + VectorX opposite_vertex(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + opposite_vertex[hij] = marked_metric.v_rep[marked_metric.to[marked_metric.n[hij]]]; + } + + VectorX length; + convert_std_to_eigen_vector(marked_metric.l, length); + VectorX length_mesh = generate_FV_halfedge_data(F_halfedge, length); + VectorX opposite_vertex_mesh = generate_FV_halfedge_data(F_halfedge, opposite_vertex); + + std::string mesh_handle = "layout"; +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh2D(mesh_handle, uv, FT); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "length", + convert_scalar_to_double_vector(length_mesh)) + ->setColorMap("coolwarm") + ->setEnabled(true); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "opposite vertex", + convert_scalar_to_double_vector(opposite_vertex_mesh)) + ->setEnabled(true); + + for (int i = 0; i < marked_metric.n_homology_basis_loops(); ++i) { + const auto& homology_basis_loops = marked_metric.get_homology_basis_loops(); + std::vector dual_loop_faces = + homology_basis_loops[i]->generate_face_sequence(marked_metric); + spdlog::info("{} faces in loop", dual_loop_faces.size()); + + VectorX is_dual_loop_halfedge = VectorX::Constant(num_halfedges, 0.5); + for (auto itr = homology_basis_loops[i]->begin(); !itr.is_end(); ++itr) + { + Holonomy::DualSegment segment = *itr; + is_dual_loop_halfedge(segment[0]) = 0; + is_dual_loop_halfedge(segment[1]) = 0; + } + + Eigen::VectorXd is_dual_loop_face = Eigen::VectorXd::Constant(num_faces, 0.5); + for (const auto& dual_loop_face : dual_loop_faces) { + is_dual_loop_face(dual_loop_face) = (double)(marked_metric.kappa_hat[i]); + } + + VectorX dual_loop_mesh = generate_FV_halfedge_data(F_halfedge, is_dual_loop_halfedge); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceScalarQuantity("dual_loop_" + std::to_string(i), is_dual_loop_face); + polyscope::getSurfaceMesh(mesh_handle) + ->addHalfedgeScalarQuantity( + "halfedge_dual_loop_" + std::to_string(i), + convert_scalar_to_double_vector(dual_loop_mesh)); + } + + if (show) polyscope::show(); +#endif +} + +void view_combed_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT, + const Eigen::MatrixXd& PD1, + const Eigen::MatrixXd& PD2, + std::string mesh_handle) +{ + if (mesh_handle == "") { + mesh_handle = "combed_field"; + } + + Eigen::MatrixXd V_cut; + cut_mesh_along_parametrization_seams(V, F, uv, FT, V_cut); + + Eigen::SparseMatrix Grad; + igl::grad(V_cut, FT, Grad); + Eigen::MatrixXd Guv = Grad * uv; + Eigen::MatrixXd G_u = Eigen::Map(Guv.col(0).data(), F.rows(), 3); + Eigen::MatrixXd G_v = Eigen::Map(Guv.col(1).data(), F.rows(), 3); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh(mesh_handle, V_cut, FT); + polyscope::getSurfaceMesh(mesh_handle) + ->addVertexParameterizationQuantity("uv", uv) + ->setEnabled(true); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceVectorQuantity("PD1", PD1) + ->setEnabled(false); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceVectorQuantity("PD2", PD2) + ->setEnabled(false); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceVectorQuantity("grad_u", G_u) + ->setEnabled(false); + polyscope::getSurfaceMesh(mesh_handle) + ->addFaceVectorQuantity("grad_v", G_v) + ->setEnabled(false); + polyscope::show(); +#endif +} + +} +} diff --git a/src/resolution/experimental/dynamic_markings.cpp b/src/resolution/experimental/dynamic_markings.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/resolution/experimental/holonomy_coordinates.cpp b/src/resolution/experimental/holonomy_coordinates.cpp new file mode 100644 index 0000000..0403a51 --- /dev/null +++ b/src/resolution/experimental/holonomy_coordinates.cpp @@ -0,0 +1,123 @@ +#include "resolution/experimental/holonomy_coordinates.h" + +namespace Penner +{ + namespace Resolution + { + + /** + * @brief Build minimal norm change of angle coordinate matrix + * + * WARNING: Rank 5 matrix + * + * @return Change of coordinate matrix. + */ + Eigen::Matrix build_pseduoinverse_ccw_flip_matrix() + { + Eigen::Matrix C; + C.row(0) << 0., 0.25, -0.5, 0., 0.25, 0.5; + C.row(1) << 0., 0., 1., 1., 0., 0.; + C.row(2) << 0., -0.25,-0.5, 0., 0.75, 0.5; + C.row(3) << 0., 0.25, 0.5, 0., 0.25, -0.5; + C.row(4) << 1., 0., 0., 0., 0., 1.; + C.row(5) << 0., 0.75, 0.5, 0., -0.25, -0.5; + + return C; + } + + /** + * @brief Build change of angle coordinate matrix satisfying C^4 = I + * + * WARNING: Observed to create unstable large angles. + * + * @return Change of coordinate matrix. + */ + Eigen::Matrix build_algebraic_ccw_flip_matrix() + { + Eigen::Matrix C; + C.row(0) << 0., 1., -1., 0., 0., 0.; + C.row(1) << 0., 0., 1., 1., 0., 0.; + C.row(2) << 0., -1., 0., 0., 1., 1.; + C.row(3) << 0., 1., 0., 0., 0., -1.; + C.row(4) << 1., 0., 0., 0., 0., 1.; + C.row(5) << 0., 0., 1., 0., 0., 0.; + + return C; + } + + void update_holonomy_coordinates_under_ccw_flip( + const Mesh &m, + VectorX &alpha, + int halfedge_index) + { + // get facet halfedges + int hij = halfedge_index; + int hji = m.opp[hij]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + int hil = m.n[hji]; + int hlj = m.n[hil]; + + // get holonomy angles before rotation + Eigen::Vector a; + a[0] = alpha[hki]; + a[1] = alpha[hij]; + a[2] = alpha[hjk]; + a[3] = alpha[hlj]; + a[4] = alpha[hji]; + a[5] = alpha[hil]; + + // get new angle coordinates + Eigen::Matrix C = build_pseduoinverse_ccw_flip_matrix(); + //Eigen::Matrix C = build_algebraic_ccw_flip_matrix(); + Eigen::Vector b = C * a; + + // rename halfedges for clarity + int hkl = hji; + int hlk = hij; + + // set new angles + alpha[hil] = b[0]; + alpha[hlk] = b[1]; + alpha[hki] = b[2]; + alpha[hjk] = b[3]; + alpha[hkl] = b[4]; + alpha[hlj] = b[5]; + } + + void update_holonomy_coordinates_under_clockwise_flip( + const Mesh &m, + VectorX &alpha, + int halfedge_index) + { + // get facet halfedges + int hlk = halfedge_index; + int hkl = m.opp[hlk]; + int hki = m.n[hlk]; + int hil = m.n[hki]; + int hlj = m.n[hkl]; + int hjk = m.n[hlj]; + + // get holonomy angles before rotation + Scalar bk_il = alpha[hil]; + Scalar bi_lk = alpha[hlk]; + Scalar bl_ki = alpha[hki]; + Scalar bl_jk = alpha[hjk]; + Scalar bj_kl = alpha[hkl]; + Scalar bk_lj = alpha[hlj]; + + // rename halfedges for clarity + int hij = hlk; + int hji = hkl; + + // set new angles, arbitrarily setting hij to 0 + alpha[hki] = -bk_il + bl_jk + bj_kl - bk_lj; + alpha[hij] = bk_il + bk_lj; + alpha[hjk] = bk_lj; + alpha[hlj] = bi_lk - bk_lj; + alpha[hji] = bl_ki + bl_jk; + alpha[hil] = bk_il - bl_jk + bk_lj; + } + + } // namespace Resolution +} // namespace Penner \ No newline at end of file diff --git a/src/resolution/experimental/regularization.cpp b/src/resolution/experimental/regularization.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/resolution/experimental/representatives.cpp b/src/resolution/experimental/representatives.cpp new file mode 100644 index 0000000..c8d221d --- /dev/null +++ b/src/resolution/experimental/representatives.cpp @@ -0,0 +1,133 @@ +#include "resolution/experimental/representatives.h" +#include "util/union_find.h" + +namespace Penner +{ +namespace Resolution +{ + +std::vector> generate_face_union(int num_faces) +{ + std::vector> F_he(num_faces, std::vector(3, -1)); + int hij = 0; + for (int i = 0; i < num_faces; ++i) + { + for (int j = 0; j < 3; ++j) + { + F_he[i][j] = hij; + hij++; + } + } + return F_he; +} + +std::vector> glue_face_union( + const std::vector>& F_he, + const std::vector& gluing_map) +{ + // glue together faces into edges + int num_faces = F_he.size(); + std::vector> F(num_faces, std::vector(3, -1)); + for (int i = 0; i < num_faces; ++i) + { + for (int j = 0; j < 3; ++j) + { + int hij = F_he[i][j]; + F[i][j] = gluing_map[hij]; + } + } + + return F; +} + +std::vector> generate_radial_surface(int genus) +{ + // initialize list of faces with halfedges + int num_polygon_edges = 4 * genus; + int num_faces = num_polygon_edges; + std::vector> F_he = generate_face_union(num_faces); + + // build identification map for polygon + int num_halfedges = 3 * num_faces; + UnionFind edge_gluing(num_halfedges); + + // glue triangles into polygon and list other edges in order + std::vector top_halfedges, bottom_halfedges; + top_halfedges.reserve(2 * genus); + bottom_halfedges.reserve(2 * genus); + top_halfedges.push_back(F_he[0][0]); + bottom_halfedges.push_back(F_he[0][1]); + + // glue triangles into a single polygon + for (int i = 0; i < num_faces; ++i) + { + int j = (i + 1) % num_faces; + edge_gluing.union_sets(F_he[i][2], F_he[j][1]); + } + + // glue together top and bottom + for (int i = 0; i < 2 * genus; ++i) + { + int j = 2 * genus + i; + edge_gluing.union_sets(F_he[i][0], F_he[j][0]); + } + + // get gluing map + std::vector gluing_map = edge_gluing.index_sets(); + + return glue_face_union(F_he, gluing_map); +} + +std::vector> generate_representative_surface(int genus) +{ + // initialize list of faces with halfedges + int num_polygon_edges = 4 * genus; + int num_faces = num_polygon_edges - 2; + std::vector> F_he = generate_face_union(num_faces); + + // build identification map for polygon + int num_halfedges = 3 * num_faces; + UnionFind edge_gluing(num_halfedges); + + // glue triangles into polygon and list other edges in order + std::vector top_halfedges, bottom_halfedges; + top_halfedges.reserve(2 * genus); + bottom_halfedges.reserve(2 * genus); + top_halfedges.push_back(F_he[0][0]); + bottom_halfedges.push_back(F_he[0][1]); + int he_prev = F_he[0][2]; + for (int i = 1; i < num_faces - 1; ++i) + { + if (i % 2) + { + top_halfedges.push_back(F_he[i][0]); + edge_gluing.union_sets(he_prev, F_he[i][1]); + he_prev = F_he[i][2]; + } + else + { + edge_gluing.union_sets(he_prev, F_he[i][0]); + bottom_halfedges.push_back(F_he[i][1]); + he_prev = F_he[i][2]; + } + } + top_halfedges.push_back(F_he[num_faces - 1][0]); + edge_gluing.union_sets(he_prev, F_he[num_faces - 1][1]); + bottom_halfedges.push_back(F_he[num_faces - 1][2]); + + // glue together top and bottom + for (int i = 0; i < 2 * genus; ++i) + { + int top_he = top_halfedges[i]; + int bottom_he = bottom_halfedges[2 * genus - 1 - i]; + edge_gluing.union_sets(top_he, bottom_he); + } + + // get gluing map + std::vector gluing_map = edge_gluing.index_sets(); + + return glue_face_union(F_he, gluing_map); +} + +} +} \ No newline at end of file diff --git a/src/resolution/experimental/simplification.cpp b/src/resolution/experimental/simplification.cpp new file mode 100644 index 0000000..018fa57 --- /dev/null +++ b/src/resolution/experimental/simplification.cpp @@ -0,0 +1,1383 @@ +#include "resolution/experimental/simplification.h" + +#include "util/vector.h" +#include "holonomy/core/viewer.h" +#include "holonomy/holonomy/holonomy.h" +#include "resolution/experimental/holonomy_coordinates.h" +#include "metric/constraint.h" +#include "util/vector.h" +#include +#include + +namespace Penner { +namespace Resolution { + +VectorX shift_halfedge_array(const Mesh& m, const VectorX& v) +{ + int num_halfedges = m.n_halfedges(); + VectorX w(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + w[hij] = v[m.n[hij]]; + } + + return w; +} + +VectorX unshift_halfedge_array(const Mesh& m, const VectorX& v) +{ + int num_halfedges = m.n_halfedges(); + VectorX w(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + w[m.n[hij]] = v[hij]; + } + + return w; +} + +VectorX convert_tip_to_opposite(const Mesh& m, const VectorX& v) +{ + //return v; + return shift_halfedge_array(m, v); +} + +VectorX convert_opposite_to_tip(const Mesh& m, const VectorX& v) +{ + //return v; + return unshift_halfedge_array(m, v); +} + +IntrinsicSimplificationMesh::IntrinsicSimplificationMesh(const Mesh& m) + : m_mesh(m) + , m_is_halfedge_deleted(m.n_halfedges(), false) + , m_is_vertex_deleted(m.n_vertices(), false) + , m_is_face_deleted(m.n_faces(), false) + , m_is_edge_deleted(m.n_edges(), false) + , m_vertex_parent(arange(m.n_vertices())) + , m_h2e(build_edge_map(m.opp)) +{ + m_e2h = invert_map(m_h2e); + m_rotation_form = VectorX::Zero(m.n_halfedges()); + simplification_factor = 0.; + prev = invert_map(m.n); + + // Compute the corner angles + VectorX he2alpha, he2cot; + corner_angles(m, he2alpha, he2cot); + + // convert from opposite halfedge to vertex tip representation (needed for general polygons) + m_alpha = convert_opposite_to_tip(m, he2alpha); + + removed_vert_count = 0; + initial_vertices = m.n_vertices(); + +} + +VectorX IntrinsicSimplificationMesh::get_opposite_angles() const +{ + const Mesh& m = get_mesh(); + return convert_tip_to_opposite(m, get_holonomy_coordinates()); +} + +void IntrinsicSimplificationMesh::set_rotation_form(const VectorX& rotation_form) +{ + m_rotation_form = rotation_form; + assert(Field::is_valid_one_form(m_mesh, rotation_form)); + assert(is_rotation_consistent()); +} + +void IntrinsicSimplificationMesh::set_dual_loop(const std::vector>& dual_loops) +{ + Mesh& m = get_mesh(); + for (const auto& dual_loop : dual_loops) { + std::vector dual_loop_faces = dual_loop->generate_face_sequence(m); + m_dual_loops.push_back(Holonomy::build_dual_path_from_face_sequence(m, dual_loop_faces)); + } + + int num_dual_loops = dual_loops.size(); + m_kappa.resize(num_dual_loops); + for (int i = 0; i < num_dual_loops; ++i) { + // Compute field rotation and metric holonomy + Scalar rotation = Holonomy::compute_dual_loop_rotation(m, m_rotation_form, *dual_loops[i]); + Scalar holonomy = Holonomy::compute_dual_loop_holonomy(m, get_opposite_angles(), *dual_loops[i]); + + // Constraint is the difference of the holonomy and rotation + m_kappa[i] = holonomy - rotation; + } +} + + +int IntrinsicSimplificationMesh::compute_degree(int vertex_index) const +{ + const Mesh& m = get_mesh(); + + // circulate around the vertex to count outgoing edges + int h_start = m.out[vertex_index]; + int degree = 0; + int hij = h_start; + do { + degree++; + hij = m.n[m.opp[hij]]; + } while (hij != h_start); + //} while ((hij != h_start) && (m.opp[hij] != h_start)); + + return degree; +} + +void IntrinsicSimplificationMesh::simplify_mesh() +{ + Mesh& m = get_mesh(); + int target = lround(initial_vertices * (1. - simplification_factor)); + int num_vertices = m.n_vertices(); + while (true) + { + std::vector perm = generate_permutation(num_vertices, use_random_seed); + bool vertex_deleted = false; + + // try to delete vertices + for (int i = 0; i < num_vertices; ++i) { + if (removed_vert_count >= target) return; + + int vi = perm[i]; + + // only delete flat vertices that have not been deleted already + if (is_deleted_vertex(vi)) continue; + if (!is_flat_vertex(vi)) continue; + + if (remove_vertex(vi)) + { + vertex_deleted = true; + ++removed_vert_count; + } + } + + // stop if unable to remove any vertices + if (!vertex_deleted) break; + } +} + +void IntrinsicSimplificationMesh::collapse_edges() +{ + Mesh& m = get_mesh(); + int num_halfedges = m.n_halfedges(); + int target = lround(initial_vertices * (1. - simplification_factor)); + + // Initialize the stack of vertices to process with all vertices + typedef std::pair WeightedEdge; + auto edge_compare = [](const WeightedEdge& left, const WeightedEdge& right) { + return left.second > right.second; + }; + std::priority_queue, decltype(edge_compare)> + edges_to_process(edge_compare); + for (int eij = 0; eij < m_e2h.size(); ++eij) { + int hij = m_e2h[eij]; + edges_to_process.push(std::make_pair(eij, m.l[hij])); + } + + while (true) + { + std::vector perm = generate_permutation(num_halfedges, use_random_seed); + bool vertex_deleted = false; + + // try to delete vertices + for (int i = 0; i < num_halfedges; ++i) { + if (removed_vert_count >= target) return; + + auto [eij, cost] = edges_to_process.top(); + edges_to_process.pop(); + int hij = m_e2h[eij]; + + if (is_deleted_halfedge(hij)) continue; + + // only collapse edges with flat vertices at base that have not been deleted already + int vi = m.to[m.opp[hij]]; + //int vj = m.to[hij]; + if (!is_flat_vertex(vi)) continue; + if (is_degree_1_vertex(vi)) + { + int hjk = m.n[hij]; + if (is_flippable_edge(hjk)) flip_ccw(hjk); + } + + if (collapse_edge(hij)) + { + vertex_deleted = true; + ++removed_vert_count; + } + } + + // stop if unable to remove any vertices + if (!vertex_deleted) break; + } +} + +bool IntrinsicSimplificationMesh::remove_vertex(int vertex_index) +{ + // TODO boundary is different case (degree 2) + + // attempt to flip vertex to degree 3 + bool success = flip_to_degree(vertex_index, 3); + if (!success) return false; + + // remove (now) degree 3 vertex + return remove_degree_3_vertex(vertex_index); +} + +bool IntrinsicSimplificationMesh::will_decrease_degree(int hij) const +{ + if (!is_flippable_edge(hij)) return false; + + const Mesh& m = get_mesh(); + int vi = m.to[m.opp[hij]]; + int vj = m.to[hij]; + int vk = m.to[m.n[hij]]; + int vl = m.to[m.n[m.opp[hij]]]; + + // flipped edge no longer adjacent to vi + if ((vi != vk) && (vi != vl)) return true; + + // original edge doubly adjacent to vi; new edge not + if ((vi == vj) && ((vi != vl) || (vi != vk))) return true; + + return false; +} + +bool IntrinsicSimplificationMesh::will_increase_degree(int hij) const +{ + if (!is_flippable_edge(hij)) return false; + + const Mesh& m = get_mesh(); + int vi = m.to[m.opp[hij]]; + int vj = m.to[hij]; + int vk = m.to[m.n[hij]]; + int vl = m.to[m.n[m.opp[hij]]]; + + // original edge not adjacent to vk + if ((vi != vk) && (vj != vk)) return true; + + // new edge doubly adjacent to vi; original edge not + if ((vk == vl) && ((vi != vk) || (vj != vk))) return true; + + return false; +} + +bool IntrinsicSimplificationMesh::decrease_degree(int vertex_index) +{ + Mesh& m = get_mesh(); + + // circulate around the vertex to find a flippable edge + int h_start = m.out[vertex_index]; + int hij = h_start; + do { + // check if flip increases degree + if (will_decrease_degree(hij)) { + flip_ccw(hij); + return true; + } + hij = m.n[m.opp[hij]]; + } while (hij != h_start); + + // return false if no flip possible + return false; +} + +bool IntrinsicSimplificationMesh::increase_degree(int vertex_index) +{ + Mesh& m = get_mesh(); + + // circulate around the vertex to find a flippable edge + int h_start = m.out[vertex_index]; + int hij = h_start; + do { + int hjk = m.n[hij]; // edge opposite vertex i + if (will_increase_degree(hjk)) { + flip_ccw(hjk); + return true; + } + hij = m.n[m.opp[hij]]; + } while (hij != h_start); + + // return false if no flip possible + return false; +} + +void set_rotation_form(const Mesh& m, VectorX& rotation_form, int halfedge_index, Scalar kappa) +{ + rotation_form[halfedge_index] = kappa; + rotation_form[m.opp[halfedge_index]] = -kappa; +} + +void align_rotation_form_faces(const Mesh& m, VectorX& rotation_form, int halfedge_index) +{ + // get facet halfedges + int hij = halfedge_index; + int hji = m.opp[hij]; + + // get rotations across edges + Scalar rij = rotation_form[hij]; + Scalar rji = -rij; + spdlog::trace("redistributing rotation {} on halfedge {}", rij, hij); + + // set new rotations, adding half the (signed) rotation of hij to the other halfedges + set_rotation_form(m, rotation_form, hij, 0); + int hjk = m.n[hij]; + while (hjk != hij) + { + Scalar rjk = rotation_form[hjk]; + set_rotation_form(m, rotation_form, hjk, rjk - rij/2); + hjk = m.n[hjk]; + } + + int hil = m.n[hji]; + while (hil != hji) + { + Scalar ril = rotation_form[hil]; + set_rotation_form(m, rotation_form, hil, ril - rji/2); + hil = m.n[hil]; + } +} + +void IntrinsicSimplificationMesh::align_faces(int halfedge_index) +{ + const Mesh& m = get_mesh(); + VectorX& rotation_form = get_rotation_form(); + align_rotation_form_faces(m, rotation_form, halfedge_index); +} + + +void IntrinsicSimplificationMesh::flip_ccw(int hij) +{ + Mesh& m = get_mesh(); + spdlog::trace("flipping edge hij={} between vi={} and vj={}", hij, m.to[m.opp[hij]], m.to[hij]); + if (!is_rotation_consistent()) spdlog::error("initial rotation inconsistent"); + + // update rotation data + align_faces(hij); + if (!is_rotation_consistent()) spdlog::error("alignment of halfedge {} failed", hij); + + VectorX& alpha = get_holonomy_coordinates(); + update_holonomy_coordinates_under_ccw_flip(m, alpha, hij); + + // update tracked loops + for (auto& dual_loop : m_dual_loops) { + Holonomy::update_dual_loop_under_ccw_flip(m, hij, dual_loop); + } + + // perform inrtrinsic flip + m.flip_ccw(hij); + + // update prev + int hji = m.opp[hij]; + for (int h : {hij, m.n[hij], m.n[m.n[hij]]}) prev[m.n[h]] = h; + for (int h : {hji, m.n[hji], m.n[m.n[hji]]}) prev[m.n[h]] = h; + + spdlog::trace("flipped edge now between vi={} and vj={}", m.to[m.opp[hij]], m.to[hij]); + if (!is_rotation_consistent()) spdlog::error("flip of halfedge {} failed", hij); + assert(is_rotation_consistent()); +} + +bool IntrinsicSimplificationMesh::flip_to_degree(int vertex_index, int target_degree) +{ + SPDLOG_INFO("flipping from {} to {}", compute_degree(vertex_index), target_degree); + + // decrease degree if too high + while (compute_degree(vertex_index) > target_degree) { + bool success = decrease_degree(vertex_index); + SPDLOG_INFO("new degree is {}", compute_degree(vertex_index)); + if (!success) return false; + } + + // increase degree if too low + while (compute_degree(vertex_index) < target_degree) { + bool success = increase_degree(vertex_index); + SPDLOG_INFO("new degree is {}", compute_degree(vertex_index)); + if (!success) return false; + } + + // degree is correct at end + assert(compute_degree(vertex_index) == target_degree); + return true; +} + +bool triple_contains(int i, std::array triple) +{ + if (triple[0] == i) return true; + if (triple[1] == i) return true; + if (triple[2] == i) return true; + return false; +} + +void prune_caps(std::vector& dual_loop) +{ + bool cap_found = true; + while (cap_found) + { + cap_found = false; + int num_faces = dual_loop.size(); + auto loop_index = [&num_faces](int i) { return (i + num_faces) % num_faces; }; + std::vector new_dual_loop = {}; + + // initialize with back + new_dual_loop.push_back(dual_loop.back()); + if (new_dual_loop[0][0] == new_dual_loop[0][1]) + { + cap_found = true; + + } + int cap_index = -1; + for (int i = 0; i < num_faces-2; ++i) { + Holonomy::DualSegment segment = dual_loop[loop_index(i)]; + if ((!cap_found) && (segment[0] == segment[1])) + { + spdlog::trace("Pruning cap {}=({}, {})", i, segment[0], segment[1]); + new_dual_loop.back()[1] = dual_loop[loop_index(i + 1)][1]; + cap_index = i; + ++i; + cap_found = true; + } + else + { + new_dual_loop.push_back(segment); + } + } + if (cap_index != num_faces - 3) + { + new_dual_loop.push_back(dual_loop[loop_index(-2)]); + } + else + { + spdlog::warn("Skipping last segment"); + } + if (new_dual_loop.back()[0] == new_dual_loop.back()[1]) cap_found = true; + dual_loop = new_dual_loop; + } +} + +bool IntrinsicSimplificationMesh::remove_degree_3_vertex(int vertex_index) +{ + Mesh& m = get_mesh(); + if (!is_degree_3_vertex(vertex_index)) return false; + + // get inner halfedges of triangle by clockwise circulation + int vi = vertex_index; + int hij = m.out[vi]; + int hil = m.n[m.opp[hij]]; + int hik = m.n[m.opp[hil]]; + + // get outer halfedges of triangle and check for degenerate topology + int hjk = m.n[hij]; + int hkl = m.n[hik]; + int hlj = m.n[hil]; + if ((hjk == hkl) || (hkl == hlj) || (hlj == hjk)) return false; + + // get vertex indices and check for degenerate topology + int vj = m.to[hij]; + int vk = m.to[hik]; + int vl = m.to[hil]; + if ((vj == vk) || (vk == vl) || (vl == vj)) return false; + if ((vj == vi) || (vk == vi) || (vl == vi)) return false; + + // get face indices + int fijk = m.f[hjk]; + int fikl = m.f[hkl]; + int filj = m.f[hlj]; + std::array faces = {fijk, fikl, filj}; + spdlog::trace("Collapsing faces {}, {}, {} to {}", fijk, fikl, filj, fijk); + if ((fijk == fikl) || (fikl == filj) || (filj == fijk)) return false; + + // update angles (flattening the vertex by incorporating rotation across the bisecting edge) + VectorX& alpha = get_holonomy_coordinates(); + //alpha[hjk] = alpha[hik] + alpha[m.opp[hij]] + get_rotation(hil); + //alpha[hkl] = alpha[hil] + alpha[m.opp[hik]] + get_rotation(hij); + //alpha[hlj] = alpha[hij] + alpha[m.opp[hil]] + get_rotation(hik); + //Scalar area = alpha[hjk] + alpha[hkl] + alpha[hlj]; + alpha[hjk] = alpha[hik] + alpha[m.opp[hij]] + get_rotation(hil); + alpha[hkl] = alpha[hil] + alpha[m.opp[hik]] + get_rotation(hij); + alpha[hlj] = alpha[hij] + alpha[m.opp[hil]] + get_rotation(hik); + Scalar area = alpha[hjk] + alpha[hkl] + alpha[hlj]; + if (!float_equal(area, PI)) + { + spdlog::warn("vertex removal {} produced triangle area {}", vi, area); + } + + // mark deleted elements + delete_edge(m_h2e[hij]); + delete_edge(m_h2e[hik]); + delete_edge(m_h2e[hil]); + delete_vertex(vi); + delete_face(fikl); + delete_face(filj); + + // update dual loops + auto& dual_loops = get_dual_loops(); + for (std::vector& dual_loop : dual_loops) { + spdlog::trace("Simplifying loop of length {}", dual_loop.size()); + int num_faces = dual_loop.size(); + auto loop_index = [&num_faces](int i) { return (i + num_faces) % num_faces; }; + std::vector new_dual_loop = {}; + int offset = 0; + while (triple_contains(m.f[dual_loop[loop_index(offset)][0]], faces) + && (triple_contains(m.f[dual_loop[loop_index(offset-1)][0]], faces))) + { + offset++; + } + for (int i = offset; i < num_faces + offset; ++i) { + Holonomy::DualSegment segment = dual_loop[loop_index(i)]; + int h_start = segment[0]; + int curr_face = m.f[h_start]; + + if (triple_contains(curr_face, faces)) + { + spdlog::trace("Simplifying segment {}=({}, {}) in face {}", i, segment[0], segment[1], curr_face); + + segment = dual_loop[loop_index(i + 1)]; + int next_face = m.f[segment[0]]; + while (triple_contains(next_face, faces)) + { + spdlog::trace("segment {}=({}, {}) in face {}", i, segment[0], segment[1], next_face); + ++i; + segment = dual_loop[loop_index(i + 1)]; + next_face = m.f[segment[0]]; + } + spdlog::trace("Following segment {}=({}, {}) in face {}", i, segment[0], segment[1], next_face); + } + + int h_end = dual_loop[loop_index(i)][1]; + new_dual_loop.push_back({h_start, h_end}); + } + + // overwrite dual loop + prune_caps(new_dual_loop); + dual_loop = new_dual_loop; + } + + // update halfedge topology + m.n[hjk] = hkl; + m.n[hkl] = hlj; + m.n[hlj] = hjk; + + // update face topology + m.f[hjk] = fijk; + m.f[hkl] = fijk; + m.f[hlj] = fijk; + m.h[fijk] = hjk; + + // update vertex topology (to still valid) + m.out[vj] = hjk; + m.out[vk] = hkl; + m.out[vl] = hlj; + + for (std::vector& dual_loop : dual_loops) { + spdlog::trace("Checking loop of length {}", dual_loop.size()); + if (!Holonomy::is_valid_dual_loop(m, dual_loop)) + { + throw std::runtime_error("Dual loop simplification failed"); + } + } + + // attempt to flip to regular vertex + bool regularize = false; + if (regularize) + { + flip_to_degree(vj, 6); + flip_to_degree(vk, 6); + flip_to_degree(vl, 6); + } + + if (!is_rotation_consistent()) spdlog::error("removal of vertex {} failed", vertex_index); + assert(is_rotation_consistent()); + + return true; +} + +// algebraically rotate a given edge counterclockwise +void IntrinsicSimplificationMesh::rotate_edge(int halfedge_index, Scalar theta) +{ + Mesh& m = get_mesh(); + int hij = halfedge_index; + int hji = m.opp[hij]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + int hil = m.n[hji]; + int hlj = m.n[hil]; + + VectorX& alpha = get_holonomy_coordinates(); + alpha[hki] += theta; + alpha[hij] -= theta; + alpha[hlj] += theta; + alpha[hjk] -= theta; +} + +void relabel_tip(Mesh& m, int hij, int v) +{ + int hkj = hij; + do + { + m.to[hkj] = v; + hkj = m.opp[m.n[hkj]]; + } while (hkj != hij); + m.out[v] = m.opp[hij]; +} + +void relabel_face(Mesh& m, int hij, int f) +{ + int hjk = hij; + do + { + m.f[hjk] = f; + hjk = m.n[hjk]; + } while (hjk != hij); + m.h[f] = hij; +} + +int IntrinsicSimplificationMesh::create_face() +{ + Mesh& m = get_mesh(); + int index = m.h.size(); + m.h.push_back(-1); + m_is_face_deleted.push_back(false); + return index; +} + +int IntrinsicSimplificationMesh::create_vertex() +{ + Mesh& m = get_mesh(); + int index = m.out.size(); + m.out.push_back(-1); + m.v_rep.push_back(-1); + m_is_vertex_deleted.push_back(false); + return index; +} + +// preserve tip/face of h if vertex/face deleted +bool IntrinsicSimplificationMesh::splice_halfedges(int h, int g) +{ + Mesh& m = get_mesh(); + int hn = m.n[h]; + int gn = m.n[g]; + m.n[h] = gn; + m.n[g] = hn; + prev[hn] = g; + prev[gn] = h; + + // create new face if splice separates + if (m.f[h] == m.f[g]) + { + int new_f = create_face(); + relabel_face(m, g, new_f); + relabel_face(m, h, m.f[h]); // need to properly set f2he + } + // otherwise relabel shared face and delete removed face + else + { + delete_face(m.f[g]); + relabel_face(m, g, m.f[h]); + } + + // create new vertex if splice separates + if (m.to[h] == m.to[g]) + { + int new_v = create_vertex(); + m.v_rep[new_v] = m.v_rep[m.to[h]]; + relabel_tip(m, g, new_v); + relabel_tip(m, h, m.to[h]); // need to properly set out + } + // otherwise relabel shared vertex and delete removed vertex + else + { + delete_vertex(m.to[g]); + relabel_tip(m, g, m.to[h]); + } + + return true; +} + + +bool IntrinsicSimplificationMesh::join_halfedge_endpoints(int halfedge_index) +{ + Mesh& m = get_mesh(); + int hij = halfedge_index; + int hji = m.opp[hij]; + int hki = prev[hij]; + int hlj = prev[hji]; + int vi = m.to[hji]; + int vj = m.to[hij]; + spdlog::info("joining edge {}, {}", vi, vj); + assert(hki != hij); + assert(hki != hji); + assert(hlj != hij); + assert(hlj != hji); + + VectorX& alpha = get_holonomy_coordinates(); + alpha[hki] = alpha[hki] + alpha[hij] - M_PI; + alpha[hlj] = alpha[hlj] + alpha[hji] - M_PI; + + align_faces(hij); + + splice_halfedges(hlj, hij); // hij removed, so splice second + splice_halfedges(hki, hji); // hji removed, so splice second + splice_halfedges(hlj, hki); // want to preserve vj, so splice hlj first + + // delete removed edge + delete_edge(m_h2e[hij]); + delete_face(m.f[hij]); + delete_face(m.f[m.opp[hij]]); + delete_vertex(m.to[hij]); + delete_vertex(m.to[m.opp[hij]]); + assert(m.opp[hij] == m.n[hij]); + assert(m.n[m.n[hij]] == hij); + assert(m.n[m.n[m.opp[hij]]] == m.opp[hij]); + assert(m.f[hij] == m.f[m.opp[hij]]); + + return true; +} + +bool IntrinsicSimplificationMesh::join_edge_faces(int edge_index) +{ + Mesh& m = get_mesh(); + int hij = edge_index; + int hji = m.opp[hij]; + int hki = prev[hij]; + int hlj = prev[hji]; + assert(m.f[hij] != m.f[hji]); + + VectorX& alpha = get_holonomy_coordinates(); + alpha[hki] = alpha[hki] + alpha[hji]; + alpha[hlj] = alpha[hlj] + alpha[hij]; + //alpha[hki] = alpha[hki]; + //alpha[hlj] = alpha[hlj]; + + align_faces(hij); + + splice_halfedges(prev[hji], hij); // hij removed, so splice second + splice_halfedges(prev[hij], hji); // hji removed, so splice second + + + // delete removed edge + delete_edge(m_h2e[hij]); + delete_face(m.f[hij]); + delete_face(m.f[m.opp[hij]]); + delete_vertex(m.to[hij]); + delete_vertex(m.to[m.opp[hij]]); + assert(m.opp[hij] == m.n[hij]); + assert(m.n[m.n[hij]] == hij); + assert(m.n[m.n[m.opp[hij]]] == m.opp[hij]); + assert(m.f[hij] == m.f[m.opp[hij]]); + + return true; +} + +int count_adjacency(const Mesh& m, int vi, int v) +{ + int hij = m.out[vi]; + int hkj = hij; + int count = 0; + do + { + if (m.to[hkj] == v) count++; + hkj = m.n[m.opp[hkj]]; + } while (hkj != hij); + + return count; +} + +bool IntrinsicSimplificationMesh::is_collapse_possible(int edge_index) const +{ + const Mesh& m = get_mesh(); + + // get halfedge indices of quad + int hij = edge_index; + int hji = m.opp[hij]; + int hjk = m.n[hij]; + int hil = m.n[hji]; + + // get oppposite halfedges + int hkj = m.opp[hjk]; + int hli = m.opp[hil]; + + // get vertex indices and check for degenerate topology + int vi = m.to[hji]; + int vj = m.to[hij]; + + // check if possible + if (is_degree_1_vertex(vi)) return false; + if (is_degree_1_vertex(vj)) return false; + if (m.f[hjk] == m.f[hkj]) return false; + if (m.f[hil] == m.f[hli]) return false; + if (vi == vj) + { + spdlog::warn("trying to collapse edge with identical endpoints"); + //spdlog::info("face halfedges {}, {}, {}", hij, hjk, hli); + //spdlog::info("opoosite face halfedges {}, {}, {}", hji, hil, hlj); + return false; + } + + if (maintain_simplicial) + { + // check if either endpoint is multiply adjacent (and collapse will make self loop) + if (count_adjacency(m, vi, vj) > 1) return false; + if (count_adjacency(m, vj, vi) > 1) return false; + } + + return true; +} + +bool IntrinsicSimplificationMesh::collapse_edge(int edge_index) +{ + Mesh& m = get_mesh(); + + // get halfedge indices of quad + int hij = edge_index; + int hji = m.opp[hij]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + int hil = m.n[hji]; + int hlj = m.n[hil]; + assert(m.n[m.n[m.n[hij]]] == hij); // should be triangle + assert(m.n[m.n[m.n[hji]]] == hji); // should be triangle + + // get oppposite halfedges + int hkj = m.opp[hjk]; + int hik = m.opp[hki]; + int hli = m.opp[hil]; + int hjl = m.opp[hlj]; + + // get vertex indices and check for degenerate topology + int vi = m.to[hji]; + int vj = m.to[hij]; + int vk = m.to[hjk]; + int vl = m.to[hil]; + spdlog::info("collapsing edge {}, {} opposite {} and {}", vi, vj, vk, vl); + + // get face indices + int fijk = m.f[hij]; + int fjil = m.f[hji]; + spdlog::info("faces {}, {} to collapse", fijk, fjil); + + // check if possible + if (!is_collapse_possible(hij)) return false; + + bool modify_directly = false; + if (modify_directly) + { + // rotate edges to eliminate collapsed edge + VectorX& alpha = get_holonomy_coordinates(); + rotate_edge(hjk, alpha[hjk] / 2.); + rotate_edge(hki, -alpha[hjk] / 2.); + rotate_edge(hil, alpha[hil] / 2.); + rotate_edge(hlj, -alpha[hil] / 2.); + + // align faces across edge + align_faces(hij); + + // combine rotations across edges (will become equivalent after collapse) + Scalar rjk = get_rotation(hjk); + Scalar rki = get_rotation(hki); + Scalar ril = get_rotation(hil); + Scalar rlj = get_rotation(hlj); + set_rotation_form(hjk, rjk - rki); + set_rotation_form(hki, rki - rjk); + set_rotation_form(hil, ril - rlj); + set_rotation_form(hlj, rlj - ril); + + // update vertex topology (face topology still valid) + relabel_tip(m, hji, vj); // must do before touching halfedge connectivity + m.out[vj] = hjl; + m.out[vk] = hkj; + m.out[vl] = hli; + + // update halfedge topology + m.opp[hkj] = hik; + m.opp[hik] = hkj; + m.opp[hli] = hjl; + m.opp[hjl] = hli; + + // keep split mesh halfedge topology valid as split torus with removed vertex + m.opp[hjk] = hil; + m.opp[hki] = hlj; + m.opp[hil] = hjk; + m.opp[hlj] = hki; + relabel_tip(m, hij, vi); // must do after changing halfedge connectivity + m.out[vi] = hij; + + // mark deleted elements + delete_edge(m_h2e[hij]); + delete_halfedge(hjk); + delete_halfedge(hki); + delete_halfedge(hil); + delete_halfedge(hlj); + delete_vertex(vi); + delete_face(fijk); + delete_face(fjil); + } + else + { + join_halfedge_endpoints(hij); + if (!is_deleted_face(m.f[hjk])) join_edge_faces(hjk); + if (!is_deleted_face(m.f[hil])) join_edge_faces(hil); + + /* + delete_edge(hij); + delete_edge(hjk); + delete_edge(hil); + delete_vertex(vi); + delete_face(fijk); + delete_face(fjil); + + bool fixed_vertex = false; + if (!is_deleted_edge(hki)) + { + relabel_tip(m, hki, vj); + m.out[vj] = m.opp[hki]; + m.out[vk] = hki; + int f0_new = m.f[m.n[hki]]; + if ((f0_new == fijk) || (f0_new == fjil)) + { + spdlog::warn("setting to deleted face"); + f0_new = m.f[prev[hki]]; + } + m.f[hki] = f0_new; + m.h[f0_new] = hki; + fixed_vertex = true; + } + if (!is_deleted_edge(hlj)) + { + relabel_tip(m, hlj, vj); + m.out[vj] = m.opp[hlj]; + m.out[vl] = hlj; + int f1_new = m.f[m.n[hlj]]; + if ((f1_new == fijk) || (f1_new == fjil)) + { + spdlog::warn("setting to deleted face"); + f1_new = m.f[prev[hlj]]; + } + m.f[hlj] = f1_new; + m.h[f1_new] = hlj; + fixed_vertex = true; + } + if (!fixed_vertex) spdlog::error("vertex index not corrected"); + assert(fixed_vertex); + */ + } + + //if (!are_polygon_mesh_edges_valid(m.n, prev)) spdlog::error("mesh edges not valid"); + //if (!is_one_sided_inverse(m.opp, m.opp)) spdlog::error("mesh opp map not valid"); + //assert(is_one_sided_inverse(m.opp, m.opp)); + + return true; +} + +void IntrinsicSimplificationMesh::delete_edge(int edge_index) +{ + const Mesh& m = get_mesh(); + int hij = m_e2h[edge_index]; + int hji = m.opp[hij]; + m_is_edge_deleted[edge_index] = true; + delete_halfedge(hij); + delete_halfedge(hji); +} + +void IntrinsicSimplificationMesh::delete_halfedge(int halfedge_index) +{ + m_is_halfedge_deleted[halfedge_index] = true; +} + +void IntrinsicSimplificationMesh::delete_vertex(int vertex_index) +{ + m_is_vertex_deleted[vertex_index] = true; +} + +void IntrinsicSimplificationMesh::delete_face(int face_index) +{ + m_is_face_deleted[face_index] = true; +} + +bool IntrinsicSimplificationMesh::is_flat_vertex(int vertex_index) const +{ + const Mesh& m = get_mesh(); + return (float_equal(m.Th_hat[m.v_rep[vertex_index]], 2. * M_PI)); +} + +bool IntrinsicSimplificationMesh::is_degree_1_vertex(int vertex_index) const +{ + return (compute_degree(vertex_index) == 1); +} + +bool IntrinsicSimplificationMesh::is_degree_3_vertex(int vertex_index) const +{ + return (compute_degree(vertex_index) == 3); +} + +bool IntrinsicSimplificationMesh::is_flippable_edge(int halfedge_index) const +{ + const Mesh& m = get_mesh(); + int hij = halfedge_index; + int hji = m.opp[hij]; + int vi = m.to[hji]; + int vj = m.to[hij]; + int vk = m.to[m.n[hij]]; + int vl = m.to[m.n[hji]]; + + // optionally prevent flip from or to self edge + if (maintain_simplicial) + { + if (vi == vj) return false; + if (vk == vl) return false; + } + + // can't flip edges adjacent to degree one vertex + if (is_degree_1_vertex(vi)) return false; + if (is_degree_1_vertex(vj)) return false; + + return true; +} + +/* +std::tuple, std::vector> index_s(const std::vector& is_deleted) +{ + // Count sets by counting roots + int num_elements = is_deleted.size(); + + // Make map from roots to sets + std::vector reduced; + std::vector set_index(num_elements, -1); + for (int i = 0; i < num_elements; ++i) { + if (!is_deleted[i]) { + set_index[i] = count; + count++; + } + } + + // Index remaining elements + for (int i = 0; i < num_elements; ++i) { + set_index[i] = set_index[find_set(i)]; + } + + return set_index; +} +*/ + +Mesh IntrinsicSimplificationMesh::get_simplified_mesh() const +{ + Mesh m_simp; + const Mesh& m = get_mesh(); + + // enumerate simplified elements + std::vector f_simp, removed_f, f_map; + std::vector v_simp, removed_v, v_map; + std::vector h_simp, removed_h, h_map; + enumerate_boolean_array(m_is_face_deleted, removed_f, f_simp, f_map); + enumerate_boolean_array(m_is_vertex_deleted, removed_v, v_simp, v_map); + enumerate_boolean_array(m_is_halfedge_deleted, removed_h, h_simp, h_map); + + // compute maps with simplification remappings + m_simp.n = vector_compose(h_map, vector_compose(m.n, h_simp)); + m_simp.to = vector_compose(v_map, vector_compose(m.to, h_simp)); + m_simp.f = vector_compose(f_map, vector_compose(m.f, h_simp)); + m_simp.h = vector_compose(h_map, vector_compose(m.h, f_simp)); + m_simp.out = vector_compose(h_map, vector_compose(m.out, v_simp)); + m_simp.opp = vector_compose(h_map, vector_compose(m.opp, h_simp)); + m_simp.l = vector_reindex(m.l, h_simp); + m_simp.type = vector_reindex(m.type, h_simp); + m_simp.type_input = vector_reindex(m.type_input, h_simp); + m_simp.R = vector_reindex(m.R, h_simp); + m_simp.v_rep = vector_compose(v_map, vector_compose(m.v_rep, v_simp)); + + // TODO Need independent vertex reindexing + m_simp.Th_hat = vector_reindex(m.Th_hat, v_simp); + m_simp.fixed_dof = std::vector(m_simp.n_ind_vertices(), false); + m_simp.fixed_dof[0] = true; + int n_v = v_simp.size(); + int n_e = h_simp.size() / 2; + int n_f = f_simp.size(); + spdlog::warn("Fixing arbitrary simplified vertex degree of freedom"); + spdlog::info("simplified mesh with |V|={}, |E|={}, |F|={}", n_v, n_e, n_f); + spdlog::info("Euler characteristic is {}", n_v - n_e + n_f); + if (!Holonomy::is_valid_mesh(m_simp)) spdlog::error("simplification failed"); + assert(Holonomy::is_valid_mesh(m_simp)); + + return m_simp; +} + +void IntrinsicSimplificationMesh::garbage_collection() +{ + Mesh& m = get_mesh(); + + // enumerate simplified elements + std::vector f_simp, removed_f, f_map; + std::vector v_simp, removed_v, v_map; + std::vector h_simp, removed_h, h_map; + std::vector e_simp, removed_e, e_map; + enumerate_boolean_array(m_is_face_deleted, removed_f, f_simp, f_map); + enumerate_boolean_array(m_is_vertex_deleted, removed_v, v_simp, v_map); + enumerate_boolean_array(m_is_halfedge_deleted, removed_h, h_simp, h_map); + enumerate_boolean_array(m_is_edge_deleted, removed_e, e_simp, e_map); + + // compute maps with simplification remappings + m.n = vector_compose(h_map, vector_compose(m.n, h_simp)); + m.to = vector_compose(v_map, vector_compose(m.to, h_simp)); + m.f = vector_compose(f_map, vector_compose(m.f, h_simp)); + m.h = vector_compose(h_map, vector_compose(m.h, f_simp)); + m.out = vector_compose(h_map, vector_compose(m.out, v_simp)); + m.opp = vector_compose(h_map, vector_compose(m.opp, h_simp)); + m.l = vector_reindex(m.l, h_simp); + m.type = vector_reindex(m.type, h_simp); + m.type_input = vector_reindex(m.type_input, h_simp); + m.R = vector_reindex(m.R, h_simp); + m.v_rep = vector_compose(v_map, vector_compose(m.v_rep, v_simp)); + + // TODO Need independent vertex reindexing + m.Th_hat = vector_reindex(m.Th_hat, v_simp); + m.fixed_dof = std::vector(m.n_ind_vertices(), false); + m.fixed_dof[0] = true; + int n_v = v_simp.size(); + int n_e = h_simp.size() / 2; + int n_f = f_simp.size(); + spdlog::warn("Fixing arbitrary simplified vertex degree of freedom"); + spdlog::info("simplified mesh with |V|={}, |E|={}, |F|={}", n_v, n_e, n_f); + spdlog::info("Euler characteristic is {}", n_v - n_e + n_f); + if (!Holonomy::is_valid_mesh(m)) spdlog::error("simplification failed"); + assert(Holonomy::is_valid_mesh(m)); + + m_is_halfedge_deleted = std::vector(m.n_halfedges(), false); + m_is_vertex_deleted = std::vector(m.n_vertices(), false); + m_is_face_deleted = std::vector(m.n_faces(), false); + m_is_edge_deleted = std::vector(m.n_edges(), false); + m_vertex_parent = vector_compose(m_vertex_parent, v_simp); + m_h2e = vector_compose(e_map, vector_compose(m_h2e, h_simp)); + m_e2h = vector_compose(h_map, vector_compose(m_e2h, e_simp)); + m_rotation_form = vector_compose(m_rotation_form, h_simp); + m_alpha = vector_compose(m_alpha, h_simp); + prev = vector_compose(h_map, vector_compose(prev, h_simp)); +} + +VectorX IntrinsicSimplificationMesh::get_simplified_rotation_form() const +{ + // remap the rotation form to the simplified mesh indexing + std::vector h_simp, removed_h, h_map; + enumerate_boolean_array(m_is_halfedge_deleted, removed_h, h_simp, h_map); + return vector_compose(get_rotation_form(), h_simp); +} + +VectorX IntrinsicSimplificationMesh::get_simplified_holonomy_coordinates() const +{ + // remap the rotation form to the simplified mesh indexing + std::vector h_simp, removed_h, h_map; + enumerate_boolean_array(m_is_halfedge_deleted, removed_h, h_simp, h_map); + return vector_compose(get_opposite_angles(), h_simp); +} + +MarkedPennerConeMetric IntrinsicSimplificationMesh::get_simplified_marked_metric( + const MarkedMetricParameters& marked_metric_params +) const +{ + // build the remapped simplified mesh + Mesh m = get_simplified_mesh(); + + // Build initial metric coordinates + int num_halfedges = m.n_halfedges(); + VectorX metric_coords = VectorX::Zero(num_halfedges); + m.l = std::vector(num_halfedges, 1.); + spdlog::warn("Overriding simplified length with uniform"); + + // compute basis loops + std::vector> basis_loops; + generate_basis_loops(m, basis_loops, marked_metric_params); + + // remap the rotation form to the simplified mesh indexing + VectorX rotation_form = get_simplified_rotation_form(); + VectorX alpha = get_simplified_holonomy_coordinates(); + spdlog::info("rotations in range [{}, {}]", rotation_form.maxCoeff(), rotation_form.minCoeff()); + spdlog::info("angles in range [{}, {}]", alpha.maxCoeff(), alpha.minCoeff()); + + // compute rotation + int num_basis_loops = basis_loops.size(); + std::vector kappa(num_basis_loops); + for (int i = 0; i < num_basis_loops; ++i) { + // Compute field rotation and metric holonomy + Scalar rotation = Holonomy::compute_dual_loop_rotation(m, rotation_form, *basis_loops[i]); + Scalar holonomy = Holonomy::compute_dual_loop_holonomy(m, alpha, *basis_loops[i]); + + // Constraint is the difference of the holonomy and rotation + kappa[i] = holonomy - rotation; + } + + int num_ind_vertices = m.n_ind_vertices(); + std::vector Th_hat(num_ind_vertices, 0.); + for (int h = 0; h < m.n_halfedges(); h++) { + // Add angle to vertex opposite the halfedge + Th_hat[m.v_rep[m.to[m.n[h]]]] += alpha[h]; + + // Add rotation to the vertex at the tip of the halfedge + // NOTE: By signing convention, this is the negative of the rotation ccw around + // the vertex + Th_hat[m.v_rep[m.to[h]]] += rotation_form[h]; + } + + for (int vi = 0; vi < num_ind_vertices; ++vi) + { + if (!float_equal(m.Th_hat[vi], Th_hat[vi])) + { + spdlog::error("cone angle {} of {} inconsistent with simplified cone {}", m.Th_hat[vi], vi, Th_hat[vi]); + } + } + + m.Th_hat = Th_hat; + + return MarkedPennerConeMetric(m, metric_coords, basis_loops, kappa); +} + +std::tuple IntrinsicSimplificationMesh::generate_mesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + const Mesh& m = get_mesh(); + + // find unremoved vertices + int num_vertices = m.n_vertices(); + std::vector V_map(num_vertices, -1); + std::vector simplified_vertices = {}; + for (int vi = 0; vi < num_vertices; ++vi) { + if (is_deleted_vertex(vi)) continue; + V_map[vi] = simplified_vertices.size(); + simplified_vertices.push_back(vi); + } + + // copy unremoved vertices + int num_simp_vertices = simplified_vertices.size(); + Eigen::MatrixXd V_simp(num_simp_vertices, 3); + for (int i = 0; i < num_simp_vertices; ++i) { + int vi = simplified_vertices[i]; + V_simp.row(i) = V.row(vtx_reindex[vertex_parent(vi)]); + } + + // find unremoved faces + int num_faces = m.n_faces(); + std::vector simplified_faces = {}; + for (int f = 0; f < num_faces; ++f) { + if (is_deleted_face(f)) continue; + simplified_faces.push_back(f); + } + + // generate doubled faces and halfedge map + int num_simp_faces = simplified_faces.size(); + Eigen::MatrixXi F(num_simp_faces, 3); + for (int i = 0; i < num_simp_faces; ++i) { + int fijk = simplified_faces[i]; + + // Get halfedges of face + int hij = m.h[fijk]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + + // Get vertices of face + int vj = m.to[hij]; + int vk = m.to[hjk]; + int vi = m.to[hki]; + + // Write face with opposite vertex data + F(i, 0) = V_map[vi]; + F(i, 1) = V_map[vj]; + F(i, 2) = V_map[vk]; + } + + return std::make_tuple(V_simp, F); +} + +void IntrinsicSimplificationMesh::view_simplified_mesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [V_double, F] = generate_mesh(V, vtx_reindex); + // spdlog::trace("F: {}", F); + +#ifdef ENABLE_VISUALIZATION + std::string mesh_handle = "simplified mesh"; + polyscope::init(); + polyscope::registerSurfaceMesh(mesh_handle, V_double, F); + polyscope::show(); +#endif +} + +void IntrinsicSimplificationMesh::get_dual_loops( + std::vector>& dual_loops) const +{ + // enumerate simplified elements + std::vector h_simp, removed_h, h_map; + enumerate_boolean_array(m_is_halfedge_deleted, removed_h, h_simp, h_map); + + // Initialize basis list loop and lambda to add loops + dual_loops.reserve(m_dual_loops.size()); + for (const auto& dual_loop : m_dual_loops) + { + int dual_loop_size = dual_loop.size(); + std::vector simp_dual_loop; + simp_dual_loop.reserve(dual_loop_size); + for (int i = 0; i < dual_loop_size; ++i) + { + auto [hij, hjk] = dual_loop[i]; + simp_dual_loop.push_back(Holonomy::DualSegment({h_map[hij], h_map[hjk]})); + } + dual_loops.push_back(std::make_unique( + Holonomy::DualLoopConnectivity(simp_dual_loop))); + } +} + +bool IntrinsicSimplificationMesh::is_rotation_consistent() const +{ +#ifdef CHECK_VALIDITY + const Mesh& m = get_mesh(); + const VectorX& alpha = get_opposite_angles(); + const VectorX& rotation_form = get_rotation_form(); + const auto& basis_loops = get_dual_loops(); + + // compute cone angles + int num_ind_vertices = m.n_ind_vertices(); + std::vector Th_hat(num_ind_vertices, 0.); + for (int h = 0; h < m.n_halfedges(); h++) { + if (is_deleted_halfedge(h)) continue; + + // Add angle to vertex opposite the halfedge + Th_hat[m.v_rep[m.to[m.n[h]]]] += alpha[h]; + + // Add rotation to the vertex at the tip of the halfedge + // NOTE: By signing convention, this is the negative of the rotation ccw around + // the vertex + Th_hat[m.v_rep[m.to[h]]] += rotation_form[h]; + } + + for (int vi = 0; vi < num_ind_vertices; ++vi) + { + if (is_deleted_vertex(vi)) continue; + + if (!float_equal(m.Th_hat[vi], Th_hat[vi], 1e-4)) + { + spdlog::error("cone angle {} of {} inconsistent with simplified cone {}", m.Th_hat[vi], vi, Th_hat[vi]); + return false; + } + } + + // check dual loop holonomy + int num_basis_loops = basis_loops.size(); + for (int i = 0; i < num_basis_loops; ++i) { + // Compute field rotation and metric holonomy + Holonomy::DualLoopList basis_loop(basis_loops[i]); + Scalar rotation = Holonomy::compute_dual_loop_rotation(m, rotation_form, basis_loop); + Scalar holonomy = Holonomy::compute_dual_loop_holonomy(m, alpha, basis_loop); + + // Constraint is the difference of the holonomy and rotation + if (!float_equal(m_kappa[i], holonomy - rotation, 1e-4)) + { + spdlog::error("holonomy {} of {} inconsistent with simplified cone {}", m_kappa[i], i, holonomy-rotation); + return false; + } + } + +#endif + + return true; +} + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/experimental/simplification_overlay.cpp b/src/resolution/experimental/simplification_overlay.cpp new file mode 100644 index 0000000..866be8b --- /dev/null +++ b/src/resolution/experimental/simplification_overlay.cpp @@ -0,0 +1,379 @@ +#include "resolution/experimental/simplification_overlay.h" +#include "metric/viewer.h" +#include "util/union_find.h" +#include "util/map.h" +#include "util/vf_mesh.h" +#include + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner { +namespace Resolution { + +IntrinsicSimplificationOverlayMesh::IntrinsicSimplificationOverlayMesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const Mesh& mesh) +: SimplificationOverlayMesh(V, vtx_reindex, mesh) +, M(mesh) +{ +} + + +// TODO: replace with local version that trims paths above the minimum found length +bool IntrinsicSimplificationOverlayMesh::find_edge(int edge_index) +{ + const auto& m = M.get_mesh(); + int halfedge_index = M.get_e2h()[edge_index]; + int Vi = m.to[m.opp[halfedge_index]]; + int Vj = m.to[halfedge_index]; + int first_vertex = M.vertex_parent(Vi); + int second_vertex = M.vertex_parent(Vj); + spdlog::info("tracing path from {} to {}", Vi, Vj); + + // Initialize an array to keep track of vertices + int num_vertices = out.size(); + std::vector is_processed_vertex(num_vertices, false); + std::vector halfedge_from_vertex(num_vertices, -1); + + // Initialize vertex cost with value above maximum possible weight + //Scalar inf_cost = std::numeric_limits::max(); + Scalar inf_cost = 1e20; + std::vector vertex_cost(num_vertices, inf_cost); + + // Mark root with 0 (or cost lower than other edges for all negative weights) + vertex_cost[first_vertex] = 0.; + + // Define a custom comparison function for the priority queue + typedef std::pair WeightedVertex; + auto vertex_compare = [](const WeightedVertex& left, const WeightedVertex& right) { + return left.second > right.second; + }; + + // Initialize the stack of vertices to process with all vertices + std::priority_queue, decltype(vertex_compare)> + vertices_to_process(vertex_compare); + for (int vi = 0; vi < num_vertices; ++vi) { + vertices_to_process.push(std::make_pair(vi, vertex_cost[vi])); + } + + // Perform Dijkstra algorithm + bool vertex_found = false; + while (!vertices_to_process.empty()) { + // Get the next vertex to process + auto [vi, vi_cost] = vertices_to_process.top(); + vertices_to_process.pop(); + + // Skip already processed vertices + if (is_processed_vertex[vi]) continue; + is_processed_vertex[vi] = true; + + // Iterate over the vertex circulator via halfedges + int h_start = out[vi]; + int hij = h_start; + do { + // Get the vertex in the one ring at the tip of the halfedge + int vj = to[hij]; + + // Get candidate edge cost (either path length or edge weight) + Scalar candidate_cost = halfedge_length(hij) + vertex_cost[vi]; + + bool is_invalid = false; + is_invalid = (is_invalid) || (is_processed_vertex[vj]); // don't process same vertex twice + is_invalid = (is_invalid) || (simp.edge_type[h2e[hij]] != EdgeType::EF); // don't use traced edges + is_invalid = (is_invalid) || (vertex_cost[vj] < candidate_cost); // don't use longer paths + + // if processing edge from the first vertex, check for consistent cyclic orientation + if (to[opp[hij]] == first_vertex) + { + is_invalid = (is_invalid) || (!is_start_cyclic_orientation(hij, halfedge_index)); // maintain consistent orientation + } + + // if processing edge to the second vertex, check for consistent cyclic orientation + if (vj == second_vertex) + { + is_invalid = (is_invalid) || (!is_end_cyclic_orientation(hij, halfedge_index)); // maintain consistent orientation + } + // don't intersect any other vertex + else + { + is_invalid = (is_invalid) || (simp.vertex_type[vj] != VertexType::VF); // don't use traced vertices + } + + // add current candidate to record if it is valid + if (!is_invalid) + { + halfedge_from_vertex[vj] = hij; + vertex_cost[vj] = candidate_cost; + vertices_to_process.push(std::make_pair(vj, vertex_cost[vj])); + if (vj == second_vertex) + { + vertex_found = true; + } + } + + // Progress to the next halfedge in the vertex circulator + hij = opp[prev[hij]]; + } while (hij != h_start); + + if (vertex_found) break; + } + + if (vertex_cost[second_vertex] == inf_cost) + { + //spdlog::error("could not find path from {} to {}", Vi, Vj); + + // visualize costs + bool show_failure = false; + if (show_failure) + { +#ifdef ENABLE_VISUALIZATION + view(); + std::string mesh_handle = "overlay mesh"; + auto [v_new2old, v_old2new] = get_vertex_reductions(); + std::vector cost = vector_compose(vertex_cost, v_new2old); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("cost", cost); + polyscope::show(); +#endif + } + + return false; + } + + // trace back path and mark all edges and vertex parents + int hjk = halfedge_from_vertex[second_vertex]; + while (true) + { + // mark edge parent + int ejk = h2e[hjk]; + simp.edge_type[ejk] = EdgeType::EE; + simp.halfedge_parent[hjk] = edge_index; // TODO broken + + // check if at end + int vj = to[opp[hjk]]; + if (vj == first_vertex) + { + break; + } + // TODO: barycentric coordinates + + // iterate if still interior + simp.vertex_type[vj] = VertexType::VE; + simp.vertex_parent[vj] = edge_index; + hjk = halfedge_from_vertex[vj]; + } + + return true; +} + +void IntrinsicSimplificationOverlayMesh::reset() +{ +const auto& m = M.get_mesh(); +int num_vertices = out.size(); +int num_edges = e2h.size(); +int num_faces = f2h.size(); +orig = OverlayStructure(num_vertices, num_edges, num_faces); + +simp.vertex_parent = std::vector(num_vertices, -1); +simp.halfedge_parent = std::vector(2*num_edges, -1); +simp.face_parent = std::vector(num_faces, -1); +simp.vertex_type = std::vector(num_vertices, VertexType::VF); +simp.edge_type = std::vector(num_edges, EdgeType::EF); +simp.bary_coords = std::vector(num_vertices, {0., 0.}); +} + +void IntrinsicSimplificationOverlayMesh::mark_vertices() +{ + const auto& m = M.get_mesh(); + int num_vertices = m.n_vertices(); + for (int vi = 0; vi < num_vertices; ++vi) + { + int v = M.vertex_parent(vi); + simp.vertex_type[v] = VertexType::VV; + simp.vertex_parent[v] = vi; + } +} +} // namespace Resolution +} // namespace Penner + +/* +bool SimplificationOverlayMesh::collapse_edge(int halfedge_index) +{ + auto& m = M.get_mesh(); + + // get first clockwise non collapsed edge + int h_cw = m.n[m.opp[halfedge_index]]; + h_cw = m.n[m.opp[h_cw]]; + + // get first counterclockwise non collapsed edge + int h_ccw = m.opp[m.n[m.n[halfedge_index]]]; + h_ccw = m.opp[m.n[m.n[h_ccw]]]; + + // determine the degree of the collapsing vertex + int degree = M.compute_degree(m.to[m.opp[halfedge_index]]); + + // get edge in the overlay mesh + int hij = start_halfedge[halfedge_index]; + spdlog::debug("collapsing edge {} = ({}, {})", h2e[hij], hij, opp[hij]); + + // get vertices + int vi = to[opp[hij]]; + int vj = to[hij]; + spdlog::debug("collapsing a degree {} vertex {} to {}", degree, vi, vj); + if (!M.collapse_edge(halfedge_index)) return false; + + // determine how many edges are in the clockwise and counterclockwise sides + int half_edges = (degree % 2) ? (degree + 1) / 2 : (degree / 2) - 1; + + // get the farthest edge in the counterclockwise side + int H_ccw = h_ccw; + for (int i = 0; i < half_edges - 2; ++i) + { + H_ccw = m.opp[m.n[m.n[H_ccw]]]; + } + + // get the farthest edge in the clockwise side + int H_cw = h_cw; + for (int i = 0; i < half_edges - 2; ++i) + { + H_cw = m.n[m.opp[H_cw]]; + } + + // if odd degree vertex, mark central vertex as VF and collapsed edge as EF + if (degree % 2) + { + spdlog::debug("collapsing odd degree vertex"); + vertex_type[vi] = VertexType::VF; + vertex_parent[vi] = m.f[H_ccw]; + bary_coords[vi][0] = 1. / 3.; + bary_coords[vi][1] = 1. / 3.; + edge_type[h2e[hij]] = EdgeType::EF; + assert(m.n[m.opp[H_cw]] == H_ccw); // check rotated start edges + } + // if even degree verte, mark central vertex as VE and collapsed edge as EE + else + { + spdlog::debug("collapsing even degree vertex"); + vertex_type[vi] = VertexType::VE; + vertex_parent[vi] = M.get_h2e()[m.n[m.opp[H_cw]]]; + bary_coords[vi][0] = 0.5; + edge_type[h2e[hij]] = EdgeType::EE; + assert(m.n[m.opp[H_cw]] == m.opp[m.n[m.n[H_ccw]]]); // check rotated start edges + } + edge_parent[h2e[hij]] = vertex_parent[vi]; + + // mark counterclockwise edges as EF + int hik = hij; + for (int i = 0; i < half_edges; ++i) + { + hik = opp[prev[hik]]; + edge_type[h2e[hik]] = EdgeType::EF; + } + // if even degree, mark opposite edge as EE + if ((degree % 2) == 0) + { + hik = opp[prev[hik]]; + edge_type[h2e[hik]] = EdgeType::EE; + } + // continue counterclockwise to mark clockwise edges as EF + for (int i = 0; i < half_edges; ++i) + { + hik = opp[prev[hik]]; + edge_type[h2e[hik]] = EdgeType::EF; + } + assert(opp[prev[hik]] == hij); // ensure we made a complete loop + + // split counterclockwise edges + spdlog::debug("splitting counterclockwise edges"); + hik = hij; + for (int i = 0; i < half_edges; ++i) + { + hik = opp[prev[hik]]; + for (int j = i; j < half_edges - 1; ++j) + { + split_edge(hik); + } + } + + // split clockwise edges + spdlog::debug("splitting clockwise edges"); + hik = hij; + for (int i = 0; i < half_edges; ++i) + { + hik = n[opp[hik]]; + for (int j = i; j < half_edges - 1; ++j) + { + split_edge(hik); + } + } + + hik = opp[prev[hij]]; + for (int i = 0; i < half_edges - 1; ++i) + { + int Hik = H_ccw; + int h = hik; + for (int j = i; j < half_edges - 1; ++j) + { + int vk = to[h]; + spdlog::debug("setting coordinates for VE edge {}", vk); + vertex_type[vk] = VertexType::VE; + vertex_parent[vk] = M.get_h2e()[Hik]; + bary_coords[vk][0] = (1. * i + 1.) / (1. * half_edges); + + h = n[h]; + Hik = m.n[m.opp[Hik]]; + } + hik = opp[prev[hik]]; + } + + hik = n[opp[hij]]; + for (int i = 0; i < half_edges - 1; ++i) + { + int Hik = H_cw; + int h = hik; + for (int j = i; j < half_edges - 1; ++j) + { + int vk = to[h]; + spdlog::debug("setting coordinates for VE edge {}", vk); + vertex_type[vk] = VertexType::VE; + vertex_parent[vk] = M.get_h2e()[Hik]; + bary_coords[vk][0] = (1. * i + 1.) / (1. * half_edges); + + h = opp[prev[opp[h]]]; + Hik = m.opp[m.n[m.n[Hik]]]; + } + hik = n[opp[hik]]; + } + + hik = opp[prev[hij]]; + for (int i = 0; i < half_edges; ++i) + { + int h = hik; + for (int j = max(0, i - 1); j < half_edges - 1; ++j) + { + split_face(prev[opp[h]], n[opp[h]]); + h = n[h]; + } + hik = opp[prev[hik]]; + } + + + hik = n[opp[hij]]; + for (int i = 0; i < half_edges; ++i) + { + int h = hik; + for (int j = max(0, i - 1); j < half_edges - 1; ++j) + { + split_face(prev[prev[h]], h); + h = opp[prev[opp[h]]]; + } + hik = n[opp[hik]]; + } + + return true; +} +*/ diff --git a/src/resolution/hierarchy/halfedge.cpp b/src/resolution/hierarchy/halfedge.cpp new file mode 100644 index 0000000..0b718e6 --- /dev/null +++ b/src/resolution/hierarchy/halfedge.cpp @@ -0,0 +1,1265 @@ +#include "resolution/hierarchy/halfedge.h" + +#include "util/vector.h" + +namespace Penner { +namespace Resolution { + +HalfedgeMesh::HalfedgeMesh() { + m_halfedge_count = 0; + m_vertex_count = 0; + m_edge_count = 0; + m_face_count = 0; +}; + +HalfedgeMesh::HalfedgeMesh( + const std::vector& opp_, + const std::vector& n_, + const std::vector& to_, + const std::vector& face_, + const std::vector& h2e_) +: opp(opp_) +, n(n_) +, to(to_) +, face(face_) +, h2e(h2e_) +{ + // build left inverse maps + prev = invert_map(n); + out = invert_map(vector_compose(to, opp)); + f2h = invert_map(face); + e2h = invert_map(h2e); + + // build deleted element flags + m_is_halfedge_deleted = std::vector(prev.size(), false); + m_is_vertex_deleted = std::vector(out.size(), false); + m_is_edge_deleted = std::vector(e2h.size(), false); + m_is_face_deleted = std::vector(f2h.size(), false); + + // get initial counts + m_halfedge_count = prev.size(); + m_vertex_count = out.size(); + m_edge_count = e2h.size(); + m_face_count = f2h.size(); + + // preallocate extra space + opp.reserve(5 * m_halfedge_count); + n.reserve(5 * m_halfedge_count); + prev.reserve(5 * m_halfedge_count); + to.reserve(5 * m_halfedge_count); + face.reserve(5 * m_halfedge_count); + h2e.reserve(5 * m_halfedge_count); + e2h.reserve(5 * m_edge_count); + out.reserve(5 * m_vertex_count); + f2h.reserve(5 * m_face_count); + m_is_halfedge_deleted.reserve(5 * m_halfedge_count); + m_is_vertex_deleted.reserve(5 * m_vertex_count); + m_is_edge_deleted.reserve(5 * m_edge_count); + m_is_face_deleted.reserve(5 * m_face_count); +} + +int HalfedgeMesh::vertex_degree(int vertex_index) const +{ + int hij = out[vertex_index]; + int hik = hij; + int degree = 0; + do + { + degree++; + hik = opp[prev[hik]]; + } while (hik != hij); + + return degree; +} + +int HalfedgeMesh::face_degree(int face_index) const +{ + int hij = f2h[face_index]; + int hkl = hij; + int degree = 0; + do + { + degree++; + hkl = n[hkl]; + } while (hkl != hij); + + return degree; +} + +int HalfedgeMesh::halfedge_from_face_vertex(int face_index, int vertex_index) const +{ + // search face for halfedge pointing to vertex + int hij = f2h[face_index]; + do + { + if (to[hij] == vertex_index) return hij; + hij = n[hij]; + } while (hij != f2h[face_index]); + + // vertex not in face + return -1; +} + +void HalfedgeMesh::relabel_tip(int halfedge_index, int vertex_index) +{ + int hij = halfedge_index; + m_is_vertex_deleted[to[hij]] = true; + + int hkj = hij; + do + { + to[hkj] = vertex_index; + hkj = opp[n[hkj]]; + } while (hkj != hij); + out[vertex_index] = opp[halfedge_index]; + + m_is_vertex_deleted[vertex_index] = false; +} + +void HalfedgeMesh::relabel_face(int halfedge_index, int face_index) +{ + int hij = halfedge_index; + m_is_face_deleted[face[hij]] = true; + + int hjk = hij; + do + { + face[hjk] = face_index; + hjk = n[hjk]; + } while (hjk != hij); + f2h[face_index] = halfedge_index; + + m_is_face_deleted[face_index] = false; +} + +int HalfedgeMesh::create_halfedge() +{ + int index = opp.size(); + opp.push_back(-1); + n.push_back(-1); + to.push_back(-1); + face.push_back(-1); + h2e.push_back(-1); + prev.push_back(-1); + m_is_halfedge_deleted.push_back(false); + m_halfedge_count++; + return index; +} + +int HalfedgeMesh::create_face() +{ + int index = f2h.size(); + f2h.push_back(-1); + m_is_face_deleted.push_back(false); + m_face_count++; + return index; +} + +int HalfedgeMesh::create_edge() +{ + int index = e2h.size(); + e2h.push_back(-1); + m_is_edge_deleted.push_back(false); + m_edge_count++; + return index; +} + +int HalfedgeMesh::create_vertex() +{ + int index = out.size(); + out.push_back(-1); + m_is_vertex_deleted.push_back(false); + m_vertex_count++; + return index; +} + +int HalfedgeMesh::make_edge() +{ + // create sphere halfedge topology + int h1 = create_halfedge(); + int h2 = create_halfedge(); + opp[h1] = h2; + opp[h2] = h1; + n[h1] = h2; + n[h2] = h1; + prev[h1] = h2; + prev[h2] = h1; + + // add vertex endpoints + int v1 = create_vertex(); + int v2 = create_vertex(); + to[h1] = v1; + to[h2] = v2; + out[v1] = h2; + out[v2] = h1; + + // add face + int f = create_face(); + face[h1] = f; + face[h2] = f; + f2h[f] = h1; + + // add edge + int e = create_edge(); + h2e[h1] = e; + h2e[h2] = e; + e2h[e] = h1; + + //assert(is_valid()); + return e; +} + +void HalfedgeMesh::delete_edge(int edge_index) +{ + int hij = e2h[edge_index]; + int hji = opp[hij]; + delete_halfedge(hij); + delete_halfedge(hji); + delete_dangling_edge(edge_index); +} + +void HalfedgeMesh::delete_dangling_edge(int edge_index) +{ + m_is_edge_deleted[edge_index] = true; + m_edge_count--; +} + + +void HalfedgeMesh::delete_halfedge(int halfedge_index) +{ + m_is_halfedge_deleted[halfedge_index] = true; + m_halfedge_count--; +} + +void HalfedgeMesh::delete_vertex(int vertex_index) +{ + m_is_vertex_deleted[vertex_index] = true; + m_vertex_count--; +} + +void HalfedgeMesh::delete_face(int face_index) +{ + m_is_face_deleted[face_index] = true; + m_face_count--; +} + +void HalfedgeMesh::delete_bubble(int halfedge_index) +{ + int hij = halfedge_index; + assert(opp[hij] == n[hij]); + assert(n[n[hij]] == hij); + assert(n[n[opp[hij]]] == opp[hij]); + assert(face[hij] == face[opp[hij]]); + delete_edge(h2e[hij]); + delete_face(face[hij]); + delete_vertex(to[hij]); + delete_vertex(to[opp[hij]]); +} + +// preserve tip/face of h if vertex/face deleted +bool HalfedgeMesh::splice_halfedges(int h, int g) +{ + int hn = n[h]; + int gn = n[g]; + n[h] = gn; + n[g] = hn; + prev[hn] = g; + prev[gn] = h; + + // create new face if splice separates + if (face[h] == face[g]) + { + int new_f = create_face(); + relabel_face(g, new_f); + relabel_face(h, face[h]); + } + // otherwise relabel shared face and delete removed face + else + { + delete_face(face[g]); + relabel_face(g, face[h]); + } + + // create new vertex if splice separates + if (to[h] == to[g]) + { + int new_v = create_vertex(); + relabel_tip(g, new_v); + relabel_tip(h, to[h]); + } + // otherwise relabel shared vertex and delete removed vertex + else + { + delete_vertex(to[g]); + relabel_tip(g, to[h]); + } + + //assert(is_valid()); + return true; +} + + +// preserve tip/face of h if vertex/face deleted +bool HalfedgeMesh::splice_halfedges_conservative(int h, int g) +{ + int hn = n[h]; + int gn = n[g]; + n[h] = gn; + n[g] = hn; + prev[hn] = g; + prev[gn] = h; + + + // create new face if splice separates + if (face[h] == face[g]) + { + delete_face(face[h]); + int new_fh = create_face(); + int new_fg = create_face(); + relabel_face(h, new_fh); + relabel_face(g, new_fg); + } + // otherwise relabel shared face and delete removed face + else + { + delete_face(face[h]); + delete_face(face[g]); + int new_f = create_face(); + relabel_face(h, new_f); + } + + + // create new vertex if splice separates + if (to[h] == to[g]) + { + delete_vertex(to[h]); + int new_vh = create_vertex(); + int new_vg = create_vertex(); + relabel_tip(h, new_vh); + relabel_tip(g, new_vg); + } + // otherwise relabel shared vertex and delete removed vertex + else + { + delete_vertex(to[h]); + delete_vertex(to[g]); + int new_v = create_vertex(); + relabel_tip(h, new_v); + } + + //assert(is_valid()); + return true; +} + +bool HalfedgeMesh::is_join_halfedge_endpoints_valid(int halfedge_index) const +{ + int hij = halfedge_index; + int hji = opp[hij]; + int hki = prev[hij]; + int hlj = prev[hji]; + if (hki == hij) return false; + if (hki == hji) return false; + if (hlj == hij) return false; + if (hlj == hji) return false; + + return true; +} + +bool HalfedgeMesh::join_halfedge_endpoints(int halfedge_index) +{ + assert(is_join_halfedge_endpoints_valid(halfedge_index)); + + int hij = halfedge_index; + int hji = opp[hij]; + int hi_prev = prev[hij]; + int hj_prev = prev[hji]; + int hi_n = n[hij]; + int hj_n = n[hji]; + int fi = face[hij]; + int fj = face[hji]; + int vi = to[hji]; + int vj = to[hij]; + + n[hi_prev] = hi_n; + prev[hi_n] = hi_prev; + n[hj_prev] = hj_n; + prev[hj_n] = hj_prev; + relabel_tip(hi_prev, vi); + f2h[fi] = hi_n; + f2h[fj] = hj_n; + delete_edge(h2e[hij]); + delete_vertex(vj); + + spdlog::debug("joining edge {}, {}", vi, vj); + + return true; +} + +bool HalfedgeMesh::join_halfedge_endpoints_conservative(int halfedge_index) +{ + assert(is_join_halfedge_endpoints_valid(halfedge_index)); + + int hij = halfedge_index; + int hji = opp[hij]; + int hki = prev[hij]; + int hlj = prev[hji]; + int vi = to[hji]; + int vj = to[hij]; + int fijk = face[hij]; + int fjil = face[hji]; + spdlog::debug("joining edge {}, {}", vi, vj); + + splice_halfedges(hlj, hij); // hij removed, so splice second + splice_halfedges(hki, hji); // hji removed, so splice second + splice_halfedges(hlj, hki); // want to preserve vj, so splice hlj first + + // delete removed edge + delete_bubble(hij); + + relabel_tip(hki, vi); + relabel_face(hki, fijk); + relabel_face(hlj, fjil); + + return true; +} + +int HalfedgeMesh::split_vertex(int h, int g) +{ + assert(h != g); + assert(to[h] == to[g]); + int v = to[h]; + spdlog::debug("splitting vertex {}", v); + + int e = make_edge(); + int h1 = e2h[e]; + int h2 = opp[h1]; + + splice_halfedges(h, g); + splice_halfedges(h1, g); + splice_halfedges(h2, h); + + spdlog::debug("vertex split"); + + return h2; +} + +int HalfedgeMesh::split_edge(int halfedge_index) +{ + int hij = halfedge_index; + int hji = opp[hij]; + int hij_n = n[hij]; + int hji_prev = prev[hji]; + int vj = to[hij]; + int f = face[hij]; + int fo = face[hji]; + spdlog::debug("splitting edge between {} and {}", f, fo); + + int h1 = create_halfedge(); + int h2 = create_halfedge(); + int e = create_edge(); + int vn = create_vertex(); + opp[h1] = h2; + opp[h2] = h1; + h2e[h1] = e; + h2e[h2] = e; + e2h[e] = h1; + n[hij] = h1; + n[h1] = hij_n; + n[hji_prev] = h2; + n[h2] = hji; + prev[h1] = hij; + prev[hij_n] = h1; + prev[h2] = hji_prev; + prev[hji] = h2; + face[h1] = f; + face[h2] = fo; + + to[h1] = vj; + to[h2] = vn; + to[hij] = vn; + out[vj] = h2; + out[vn] = h1; + + return hij; +} + +int HalfedgeMesh::split_edge_conservative(int halfedge_index) +{ + int hij = halfedge_index; + int vj = to[hij]; + int f = face[hij]; + int fo = face[opp[hij]]; + spdlog::debug("splitting edge between {} and {}", f, fo); + int hjk = split_vertex(hij, prev[opp[hij]]); + + // ensure tip of the halfedge remains unchanged + relabel_tip(opp[hjk], vj); + + // ensure face of the halfedge remains unchanged + relabel_face(hij, f); + relabel_face(opp[hij], fo); + + return hij; +} + +int HalfedgeMesh::split_triangle_mesh_edge(int halfedge_index) +{ + int hij = halfedge_index; + int hji = opp[hij]; + int hjk = n[hij]; + int hil = n[hji]; + split_edge(hij); + //split_vertex(hij, prev[hji]); + split_face(hjk, hij); + split_face(hil, prev[hji]); + + return hij; +} + +bool HalfedgeMesh::is_join_edge_faces_valid(int halfedge_index) const +{ + int hij = halfedge_index; + int hji = opp[hij]; + if (face[hij] == face[hji]) return false; + if (n[hij] == hij) return false; + if (n[hji] == hji) return false; + + return true; +} + +int HalfedgeMesh::join_edge_faces(int halfedge_index) +{ + // ensure not in degree 1 face + // TODO Check for bubble + //if (n[halfedge_index] == halfedge_index) return join_edge_faces(opp[halfedge_index]); + assert(is_join_edge_faces_valid(halfedge_index)); + + int hij = halfedge_index; + int hji = opp[hij]; + int hi_prev = prev[hij]; + int hj_prev = prev[hji]; + int hi_n = n[hji]; + int hj_n = n[hij]; + int fi = face[hij]; + int fj = face[hji]; + int vi = to[hji]; + int vj = to[hij]; + + n[hi_prev] = hi_n; + prev[hi_n] = hi_prev; + n[hj_prev] = hj_n; + prev[hj_n] = hj_prev; + relabel_face(hi_prev, fi); + out[vi] = hi_n; + out[vj] = hj_n; + delete_edge(h2e[hij]); + delete_face(fj); + + return fi; +} + +int HalfedgeMesh::join_edge_faces_conservative(int halfedge_index) +{ + // ensure not in degree 1 face + // TODO Check for bubble + //if (n[halfedge_index] == halfedge_index) return join_edge_faces(opp[halfedge_index]); + assert(is_join_edge_faces_valid(halfedge_index)); + + int hij = halfedge_index; + int hji = opp[hij]; + int hki = prev[hij]; + int hlj = prev[hji]; + int vi = to[hki]; + int vj = to[hlj]; + + splice_halfedges(hlj, hij); // hij removed, so splice second + splice_halfedges(hki, hji); // hji removed, so splice second + + // delete removed edge + delete_bubble(hij); + + relabel_tip(hki, vi); + relabel_tip(hlj, vj); + + return face[hki]; +} + +int HalfedgeMesh::split_face(int h, int g) +{ + assert(h != g); + assert(face[h] == face[g]); + int f = face[h]; + int hn = n[h]; + int gn = n[g]; + int v = to[h]; + int w = to[g]; + spdlog::debug("splitting face {}", f); + + int h1 = create_halfedge(); + int h2 = create_halfedge(); + int e = create_edge(); + int fn = create_face(); + opp[h1] = h2; + opp[h2] = h1; + n[h] = h1; + n[h1] = gn; + n[g] = h2; + n[h2] = hn; + prev[h1] = h; + prev[gn] = h1; + prev[h2] = g; + prev[hn] = h2; + face[h1] = f; + face[h2] = fn; + to[h1] = w; + to[h2] = v; + h2e[h1] = e; + h2e[h2] = e; + e2h[e] = h1; + + f2h[f] = h1; + relabel_face(h2, fn); + spdlog::debug("face split"); + + //assert(is_valid()); + return h1; +} + +int HalfedgeMesh::split_face_conservative(int h, int g) +{ + assert(h != g); + assert(face[h] == face[g]); + int f = face[h]; + int v = to[h]; + int w = to[g]; + spdlog::debug("splitting face {}", f); + + int e = make_edge(); + int h1 = e2h[e]; + int h2 = opp[h1]; + + splice_halfedges(g, h1); + splice_halfedges(h, h2); + + spdlog::debug("face split"); + + relabel_tip(h, v); + relabel_tip(g, w); + + //assert(is_valid()); + return h1; +} + +int HalfedgeMesh::remove_degree_one_vertex(int vertex_index) +{ + int vj = vertex_index; + int hji = out[vj]; + int vi = to[hji]; + int hij = opp[hji]; + int f = face[hij]; + int hki = prev[hij]; + int hil = n[hji]; + n[hki] = hil; + prev[hil] = hki; + + // delete removed edge + delete_edge(h2e[hij]); + delete_vertex(vj); + + f2h[f] = hki; + out[vi] = hil; + + return vi; +} + +int HalfedgeMesh::remove_degree_one_vertex_conservative(int vertex_index) +{ + int vj = vertex_index; + int hji = out[vj]; + int vi = to[hji]; + int hij = opp[hji]; + int f = face[hij]; + int hki = prev[hij]; + splice_halfedges(hki, hji); + + // delete removed edge + delete_bubble(hij); + + relabel_tip(hki, vi); + relabel_face(hki, f); + + return vi; +} + +int HalfedgeMesh::add_center_to_face(int face_index) +{ + int hij = f2h[face_index]; + int vj = to[hij]; + int num_edges = face_degree(face_index); + + // make first edge + int e = make_edge(); + int h1 = e2h[e]; + int h2 = opp[h1]; + + // splice edge into face + splice_halfedges(hij, h1); + assert(n[hij] == h2); + + // ensure tip of the halfedge remains unchanged + relabel_tip(hij, vj); + + // connect vertex to other face corners + int hjk = n[opp[n[hij]]]; + for (int i = 1; i < num_edges; ++i) + { + split_face(hjk, h2); + hjk = n[opp[n[hjk]]]; + } + + return to[h2]; +} + +bool HalfedgeMesh::is_collapse_edge_valid(int halfedge_index) const +{ + // cannot collapse degree 1 vertices + int hij = halfedge_index; + int hji = opp[hij]; + if (n[hij] == hji) return false; + if (n[hji] == hij) return false; + + // conditions for joining faces to work + int hjk = n[hij]; + int hlj = prev[hji]; + if (face[hjk] == face[opp[hjk]]) return false; + if (face[hlj] == face[opp[hlj]]) return false; + + // not in halfedge formulation: needed for the two face joins to be distinct + //if (opp[hjk] == hlj) return false; + + return true; +} + + +int HalfedgeMesh::collapse_edge(int halfedge_index) +{ + int hij = halfedge_index; + int hji = opp[hij]; + assert(!is_halfedge_deleted(halfedge_index)); + assert(face_degree(face[hij]) == 3); + assert(face_degree(face[hji]) == 3); + + // halfedges to remove + int hjk = n[hij]; + int hki = n[hjk]; + int hlj = prev[hji]; + int hil = prev[hlj]; + + // faces to remove + int fijk = face[hij]; + int fjil = face[hji]; + + // two edges and vertex to remove + int ejk = h2e[hjk]; + int elj = h2e[hlj]; + int vj = to[hij]; + + // halfedges to join + int hkj = opp[hjk]; + int hik = opp[hki]; + int hjl = opp[hlj]; + int hli = opp[hil]; + + // two edges and vertex to keep + int eik = h2e[hik]; + int eli = h2e[hli]; + int vi = to[hji]; + int vk = to[hjk]; + int vl = to[hil]; + + // h2e, opp: link e_ik = (h_ik, h_kj); e_li = ( h_li,h_jl) + h2e[hkj] = eik; + h2e[hjl] = eli; + opp[hik] = hkj; opp[hkj] = hik; + opp[hli] = hjl; opp[hjl] = hli; + + //e2h: if a halfedge to be deleted is the halfedge assigned to an edge + // reassign the one that we keep + if (e2h[eik] == hki) e2h[eik] = hik; + if (e2h[eli] == hil) e2h[eli] = hli; + + // delete edges and halfedges + delete_edge(h2e[hij]); + for (int h : {hjk, hki, hlj, hil}) + { + delete_halfedge(h); + } + + // only delete edge label, not associated halfedges + delete_dangling_edge(ejk); + delete_dangling_edge(elj); + + // delete faces and vertices + delete_vertex(vj); + delete_face(fijk); + delete_face(fjil); + + // relabel collapsed vertex + relabel_tip(hli, vi); + out[vk] = hkj; + out[vl] = hli; + + assert(face_degree(face[hkj]) == 3); + assert(face_degree(face[hik]) == 3); + assert(face_degree(face[hjl]) == 3); + assert(face_degree(face[hli]) == 3); + + return hli; +} + +int HalfedgeMesh::collapse_edge_conservative(int halfedge_index) +{ + int hij = halfedge_index; + int hji = opp[hij]; + assert(face_degree(face[hij]) == 3); + assert(face_degree(face[hji]) == 3); + int hjk = n[hij]; + int hlj = prev[hji]; + int hki = n[hjk]; + int hil = prev[hlj]; + int vi = to[hji]; + join_halfedge_endpoints(hij); + join_edge_faces(hjk); + if (opp[hlj] != hjk) + { + join_edge_faces(hlj); + assert(face_degree(face[hil]) == 3); + assert(face_degree(face[opp[hil]]) == 3); + } + else + { + join_edge_faces(hil); + } + + assert(face_degree(face[hki]) == 3); + assert(face_degree(face[opp[hki]]) == 3); + return hki; +} + + +// get link without vertices of triangles to be collapsed +std::set HalfedgeMesh::get_collapse_link(int halfedge_index) const +{ + std::set link; + int hij = halfedge_index; + //hij = n[opp[n[opp[hij]]]]; // iterate twice to skip triangle vertex + //int h_end = opp[prev[hij]]; + int h_end = halfedge_index; + do + { + link.insert(to[hij]); + hij = n[opp[hij]]; + } while (hij != h_end); + + return link; +} + +bool HalfedgeMesh::is_collapse_link_valid(int halfedge_index) const +{ + // cannot collapse degree 1 vertices + int hij = halfedge_index; + int hji = opp[hij]; + int hjk = n[hij]; + int hil = n[hji]; + int vk = to[hjk]; + int vl = to[hil]; + std::set vi_link = get_collapse_link(hij); + std::set vj_link = get_collapse_link(hji); + std::vector intersection; + std::set_intersection( + vi_link.begin(), vi_link.end(), + vj_link.begin(), vj_link.end(), + std::back_inserter(intersection) + ); + + if (intersection.size() != 2) return false; + if ((intersection[0] != vk) && (intersection[1] != vk)) return false; + if ((intersection[0] != vl) && (intersection[1] != vl)) return false; + return true; +} + +int HalfedgeMesh::flip_edge(int halfedge_index) +{ + // local halfedges + int hij = halfedge_index; + int hji = opp[hij]; + int hjk = n[hij]; + int hki = n[hjk]; + int hil = n[hji]; + int hlj = n[hil]; + + // local vertices + int vi = to[hji]; + int vj = to[hij]; + int vk = to[hjk]; + int vl = to[hil]; + + // local faces + int fijk = face[hij]; + int fjil = face[hji]; + + // update connectivity + int hlk = hij; // rename for sanity + int hkl = hji; // rename for sanity + set_next(hlk, hki); + set_next(hki, hil); + set_next(hil, hlk); + set_next(hkl, hlj); + set_next(hlj, hjk); + set_next(hjk, hkl); + + // face connectivity + relabel_face(hlk, fijk); + relabel_face(hkl, fjil); + + // vertex connectivity + to[hlk] = vk; + to[hkl] = vl; + out[vi] = hil; + out[vj] = hjk; + + return hlk; +} + +bool HalfedgeMesh::is_vertex_degree_one(int vertex_index) const +{ + int hji = out[vertex_index]; + return (n[opp[hji]] == hji); +} + +bool HalfedgeMesh::is_vertex_degree_two(int vertex_index) const +{ + int hji = out[vertex_index]; + int hjk = n[opp[hji]]; + if (hji == hjk) return false; + return (n[opp[hjk]] == hji); +} + +void HalfedgeMesh::triangulate() +{ + int num_faces = f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (m_is_face_deleted[f]) continue; + bool success = triangulate_face(f); + } +} + +bool HalfedgeMesh::triangulate_face(int face_index) +{ + int hij = f2h[face_index]; + int degree = face_degree(face_index); + if (degree < 3) + { + spdlog::warn("cannot triangulate degree {} face", degree); + return false; + } + + for (int i = 3; i < degree; ++i) + { + // TODO make this a proper nondegenerate triangulation + spdlog::debug("{} split of degree {} face", i - 2, degree); + split_face(hij, n[n[hij]]); + } + + return true; +} + +std::tuple HalfedgeMesh::generate_halfedge_faces() const +{ + auto [opp_r, n_r, prev_r] = reduce_halfedge_maps(); + auto [face_r, f2h_r] = reduce_face_maps(); + auto [to_r, out_r] = reduce_vertex_maps(); + return generate_connectivity_faces(n_r, f2h_r, to_r); +} + +void HalfedgeMesh::reindex_vertices(const std::vector& vtx_reindex) +{ + std::vector vtx_reindex_inv = invert_map(vtx_reindex); + to = vector_compose(vtx_reindex, to); + out = vector_compose(out, vtx_reindex_inv); +} + +Eigen::SparseMatrix HalfedgeMesh::generate_VV_to_halfedge_map() const +{ + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + int num_vertices = v_new2old.size(); + Eigen::SparseMatrix vv2he(num_vertices, num_vertices); + + // build sparse matrix + typedef Eigen::Triplet Trip; + std::vector trips; + int num_halfedges = he_new2old.size(); + trips.reserve(num_halfedges); + for (int i = 0; i < num_halfedges; ++i) + { + int hij = he_new2old[i]; + int vj = v_old2new[to[hij]]; + int vi = v_old2new[to[opp[hij]]]; + trips.push_back(Trip(vi, vj, hij + 1)); + } + vv2he.setFromTriplets(trips.begin(), trips.end()); + + return vv2he; +} + + +std::tuple, std::vector> HalfedgeMesh::get_halfedge_reductions() const +{ + std::vector he_removed, he_new2old, he_old2new; + enumerate_boolean_array(m_is_halfedge_deleted, he_removed, he_new2old, he_old2new); + return std::make_tuple(he_new2old, he_old2new); +} + +std::tuple, std::vector> HalfedgeMesh::get_face_reductions() const +{ + std::vector f_removed, f_new2old, f_old2new; + enumerate_boolean_array(m_is_face_deleted, f_removed, f_new2old, f_old2new); + return std::make_tuple(f_new2old, f_old2new); +} + +std::tuple, std::vector> HalfedgeMesh::get_vertex_reductions() const +{ + std::vector v_removed, v_new2old, v_old2new; + enumerate_boolean_array(m_is_vertex_deleted, v_removed, v_new2old, v_old2new); + return std::make_tuple(v_new2old, v_old2new); +} + +std::tuple, std::vector> HalfedgeMesh::get_edge_reductions() const +{ + std::vector e_removed, e_new2old, e_old2new; + enumerate_boolean_array(m_is_edge_deleted, e_removed, e_new2old, e_old2new); + return std::make_tuple(e_new2old, e_old2new); +} + + +std::tuple, std::vector, std::vector> HalfedgeMesh::reduce_halfedge_maps() const +{ + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + + std::vector opp_new = vector_compose(he_old2new, vector_compose(opp, he_new2old)); + std::vector n_new = vector_compose(he_old2new, vector_compose(n, he_new2old)); + std::vector prev_new = vector_compose(he_old2new, vector_compose(prev, he_new2old)); + + return std::make_tuple(opp_new, n_new, prev_new); +} + +std::tuple, std::vector> HalfedgeMesh::reduce_face_maps() const +{ + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [f_new2old, f_old2new] = get_face_reductions(); + + std::vector face_new = vector_compose(f_old2new, vector_compose(face, he_new2old)); + std::vector f2h_new = vector_compose(he_old2new, vector_compose(f2h, f_new2old)); + + return std::make_tuple(face_new, f2h_new); +} + +std::tuple, std::vector> HalfedgeMesh::reduce_edge_maps() const +{ + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [e_new2old, e_old2new] = get_edge_reductions(); + + std::vector h2e_new = vector_compose(e_old2new, vector_compose(h2e, he_new2old)); + std::vector e2h_new = vector_compose(he_old2new, vector_compose(e2h, e_new2old)); + + return std::make_tuple(h2e_new, e2h_new); +} + +std::tuple, std::vector> HalfedgeMesh::reduce_vertex_maps() const +{ + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + std::vector to_new = vector_compose(v_old2new, vector_compose(to, he_new2old)); + std::vector out_new = vector_compose(he_old2new, vector_compose(out, v_new2old)); + + return std::make_tuple(to_new, out_new); +} + +bool HalfedgeMesh::is_simplicial() const +{ + int num_halfedges = count_halfedges(); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (is_halfedge_deleted(hij)) continue; + if (to[hij] == opp[to[hij]]) return false; + if (face[hij] == face[opp[hij]]) return false; + } + + return true; +} + +bool HalfedgeMesh::is_valid() const +{ + int num_halfedges = count_halfedges(); + auto [opp_r, n_r, prev_r] = reduce_halfedge_maps(); + if (opp_r.size() != num_halfedges) return errmess("opp wrong size"); + if (n_r.size() != num_halfedges) return errmess("next wrong size"); + if (prev_r.size() != num_halfedges) return errmess("prev wrong size"); + + int num_faces = count_faces(); + auto [face_r, f2h_r] = reduce_face_maps(); + if (face_r.size() != num_halfedges) return errmess("face wrong size"); + if (f2h_r.size() != num_faces) return errmess("f2h wrong size"); + + int num_edges = count_edges(); + auto [h2e_r, e2h_r] = reduce_edge_maps(); + if (h2e_r.size() != num_halfedges) return errmess("h2e wrong size"); + if (e2h_r.size() != num_edges) return errmess("e2h wrong size"); + + int num_vertices = count_vertices(); + auto [to_r, out_r] = reduce_vertex_maps(); + if (to_r.size() != num_halfedges) return errmess("to wrong size"); + if (out_r.size() != num_vertices) return errmess("out wrong size"); + + if (!are_polygon_mesh_halfedges_valid(n_r, prev_r)) return errmess("mesh halfedges not valid"); + if (!are_polygon_mesh_edges_valid(opp_r, h2e_r, e2h_r)) return errmess("mesh edges not valid"); + if (!are_polygon_mesh_vertices_valid(opp_r, prev_r, to_r, out_r)) return errmess("mesh vertices not valid"); + if (!are_polygon_mesh_faces_valid(n_r, face_r, f2h_r)) return errmess("mesh faces not valid"); + + return true; +} + +void HalfedgeMesh::collect_garbage() +{ + if (skip_garbage_collection) return; + + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [e_new2old, e_old2new] = get_edge_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + auto [f_new2old, f_old2new] = get_face_reductions(); + collect_garbage( + he_new2old, + e_new2old, + v_new2old, + f_new2old, + he_old2new, + e_old2new, + v_old2new, + f_old2new); +} + +void HalfedgeMesh::collect_garbage( + const std::vector& he_new2old, + const std::vector& e_new2old, + const std::vector& v_new2old, + const std::vector& f_new2old, + const std::vector& he_old2new, + const std::vector& e_old2new, + const std::vector& v_old2new, + const std::vector& f_old2new +) +{ + if (skip_garbage_collection) return; + m_halfedge_count = he_new2old.size(); + m_vertex_count = v_new2old.size(); + m_edge_count = e_new2old.size(); + m_face_count = f_new2old.size(); + opp.resize(m_halfedge_count); + n.resize(m_halfedge_count); + prev.resize(m_halfedge_count); + h2e.resize(m_halfedge_count); + face.resize(m_halfedge_count); + to.resize(m_halfedge_count); + for (int hij = 0; hij < m_halfedge_count; ++hij) + { + opp[hij] = he_old2new[opp[he_new2old[hij]]]; + n[hij] = he_old2new[n[he_new2old[hij]]]; + prev[hij] = he_old2new[prev[he_new2old[hij]]]; + h2e[hij] = e_old2new[h2e[he_new2old[hij]]]; + face[hij] = f_old2new[face[he_new2old[hij]]]; + to[hij] = v_old2new[to[he_new2old[hij]]]; + } + + e2h.resize(m_edge_count); + for (int eij = 0; eij < m_edge_count; ++eij) + { + e2h[eij] = he_old2new[e2h[e_new2old[eij]]]; + } + + out.resize(m_vertex_count); + for (int vi = 0; vi < m_vertex_count; ++vi) + { + out[vi] = he_old2new[out[v_new2old[vi]]]; + } + + f2h.resize(m_face_count); + for (int f = 0; f < m_face_count; ++f) + { + f2h[f] = he_old2new[f2h[f_new2old[f]]]; + } + + // set all elements to undeleted + m_is_halfedge_deleted = std::vector(n.size(), false); + m_is_vertex_deleted = std::vector(out.size(), false); + m_is_edge_deleted = std::vector(e2h.size(), false); + m_is_face_deleted = std::vector(f2h.size(), false); +} + +std::tuple generate_connectivity_faces( + const std::vector& next, + const std::vector& f2h, + const std::vector& to) +{ + int num_faces = f2h.size(); + Eigen::MatrixXi F(num_faces, 3); + Eigen::MatrixXi F_halfedge(num_faces, 3); + + for (int fijk = 0; fijk < num_faces; ++fijk) { + // Get halfedges of face + int hij = f2h[fijk]; + int hjk = next[hij]; + int hki = next[hjk]; + + // Get vertices of face + int vj = to[hij]; + int vk = to[hjk]; + int vi = to[hki]; + + // Write face with halfedge and opposite vertex data + F_halfedge(fijk, 0) = hij; + F_halfedge(fijk, 1) = hjk; + F_halfedge(fijk, 2) = hki; + F(fijk, 0) = vj; + F(fijk, 1) = vk; + F(fijk, 2) = vi; + } + + return std::make_tuple(F, F_halfedge); +} + +std::tuple, std::vector>> generate_halfedge(const Eigen::MatrixXi& F) +{ + std::vector next_he; + std::vector opp; + std::vector bnd_loops; + std::vector> corner_to_he; + std::vector> he_to_corner; + std::vector vtx_reindex; + Connectivity C; + FV_to_NOB(F, next_he, opp, bnd_loops, vtx_reindex, corner_to_he, he_to_corner); + NOB_to_connectivity(next_he, opp, bnd_loops, C); + + std::vector h2e, e2h; + build_edge_maps(C.opp, h2e, e2h); + + HalfedgeMesh halfedge(C.opp, C.n, C.to, C.f, h2e); + + return std::make_tuple(halfedge, vtx_reindex, he_to_corner); +} + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/hierarchy/overlay.cpp b/src/resolution/hierarchy/overlay.cpp new file mode 100644 index 0000000..d0f9348 --- /dev/null +++ b/src/resolution/hierarchy/overlay.cpp @@ -0,0 +1,3468 @@ +#include "resolution/hierarchy/overlay.h" +#include "metric/viewer.h" +#include "holonomy/core/viewer.h" +#include "util/union_find.h" +#include "util/map.h" +#include "util/vf_mesh.h" +#include "holonomy/interface.h" +#include "field/frame_field.h" +#include "parametrization/interpolation.h" +#include "parametrization/refinement.h" +#include "parametrization/parametrize.h" +#include "metric/constraint.h" +#include "resolution/core/triangulation.h" +#include "optimization/metric_optimization/implicit_optimization.h" +//#include + +#include +#include + +#ifdef USE_SUITESPARSE +//#include +#include +#endif + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +#include + +namespace Penner { +namespace Resolution { + + +// -------------- +// Core Interface +// -------------- + +SimplificationOverlayMesh::SimplificationOverlayMesh( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const Mesh& mesh) +: HalfedgeMesh(mesh.opp, mesh.n, mesh.to, mesh.f, build_edge_map(mesh.opp)) +, V_orig(V) +, vtx_reindex_orig(vtx_reindex) +, orig(mesh) +, simp(out.size(), e2h.size(), f2h.size()) +{ +} + +Mesh SimplificationOverlayMesh::generate_mesh() const +{ + auto [opp_r, n_r, prev_r] = reduce_halfedge_maps(); + auto [face_r, f2h_r] = reduce_face_maps(); + auto [to_r, out_r] = reduce_vertex_maps(); + auto [h_new2old, h_old2new] = get_halfedge_reductions(); + + Mesh m; + m.n = n_r; + m.opp = opp_r; + m.f = face_r; + m.h = f2h_r; + m.to = to_r; + m.out = out_r; + m.l = std::vector(m.n.size(), 1.); + for (int hij = 0; hij < m.n.size(); ++hij) + { + int hji = m.opp[hij]; + if (hij < hji) continue; + + int h = h_new2old[hij]; + m.l[hji] = m.l[hij] = halfedge_length(h); + } + m.type = std::vector(m.n.size(), 0); + m.type_input = std::vector(m.n.size(), 0); + m.R = std::vector(m.n.size(), 0); + m.v_rep = arange(m.out.size()); + m.Th_hat = std::vector(m.out.size(), 2 * PI); + m.fixed_dof = std::vector(m.out.size(), false); + m.fixed_dof[0] = true; + + return m; +} + +// ----------- +// Frame Field +// ----------- + +void SimplificationOverlayMesh::set_original_field( + const Eigen::MatrixXd& reference_vector, + const Eigen::VectorXd& theta, + const Eigen::MatrixXi& period_jump) +{ + reference_field_orig = reference_vector; + thetas_orig = theta; + period_jumps_orig = Field::transfer_period_jumps_to_halfedge( + orig.m, + vtx_reindex_orig, + F_orig, + period_jump + ); +} + + +std::tuple +SimplificationOverlayMesh::refine_frame_field( + const Eigen::MatrixXi& F_orig, + const Eigen::MatrixXd& reference_vector, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump) +{ + auto [f_new2old, f_old2new] = get_face_reductions(); + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [F, F_halfedge] = generate_overlay_faces(); + + // build refined field + int num_faces = count_faces(); + Eigen::MatrixXd reference_vector_r(num_faces, 3); + Eigen::VectorXd theta_r(num_faces); + Eigen::MatrixXd kappa_r(num_faces, 3); + Eigen::MatrixXi period_jump_r(num_faces, 3); + for (int fi = 0; fi < num_faces; ++fi) + { + int f_orig = orig.face_parent[f_new2old[fi]]; + reference_vector_r.row(fi) = reference_vector.row(f_orig); + theta_r[fi] = theta[f_orig]; + + for (int j = 0; j < 3; ++j) + { + int hij = prev[he_new2old[F_halfedge(fi, j)]]; + if (orig.edge_type[h2e[hij]] == EdgeType::EF) + { + kappa_r(fi, j) = 0.; + period_jump_r(fi, j) = 0; + } + else + { + int Hij = orig.halfedge_parent[hij]; + int Vk = vtx_reindex_orig[orig.m.to[orig.m.n[Hij]]]; + int i = 0; + while (F_orig(f_orig, i) != Vk) + { + i++; + if (i>= 3) + { + spdlog::error("could not match offset for {}", f_orig); + break; + } + } + kappa_r(fi, j) = kappa(f_orig, i); + period_jump_r(fi, j) = period_jump(f_orig, i); + } + } + } + + return std::make_tuple(reference_vector_r, theta_r, kappa_r, period_jump_r); +} + +// --------------- +// Parametrization +// --------------- + +std::vector SimplificationOverlayMesh::project_dual_loop( + const std::vector& dual_loop, + const OverlayStructure& overlay) const +{ + // get parent face for each face + int num_faces = dual_loop.size(); + std::vector projected_dual_loop(num_faces); + for (int i = 0; i < num_faces; ++i) + { + projected_dual_loop[i] = overlay.face_parent[dual_loop[i]]; + } + + // trim redundant elements from cycle + return trim_cycle(projected_dual_loop); +} + + +std::tuple> +SimplificationOverlayMesh::generate_simplified_parameterization( + const std::vector>& homology_basis_loops, + const std::vector& kappa +) +{ + spdlog::info("mesh lengths in range {}, {}", vector_min(simp.m.l), vector_max(simp.m.l)); + std::vector start_halfedge = find_start_halfedges(simp); + + // compute metric coordiantes from path lengths + VectorX metric_coords; + if (use_zero_lengths) + { + simp.m.l = std::vector(simp.m.n_halfedges(), 1.); + metric_coords = VectorX::Zero(simp.m.n_halfedges()); + } + else if (use_intrinsic_lengths) + { + set_simplified_halfedge_extrinsic_lengths(); + correct_simplified_halfedge_lengths(); + metric_coords = compute_simplified_penner_coordinates(); + } + else + { + set_simplified_halfedge_extrinsic_lengths(); + correct_simplified_halfedge_lengths(); + metric_coords = generate_log_edge_lengths(simp.m); + } + + spdlog::info("Initial coordinates in range {}, {}", metric_coords.minCoeff(), metric_coords.maxCoeff()); + + MarkedPennerConeMetric marked_metric(simp.m, metric_coords, homology_basis_loops, kappa); + Optimization::LogLengthEnergy opt_energy(marked_metric); + double max_triangle_quality = 1e3; + Holonomy::regularize_metric(marked_metric, max_triangle_quality); + + NewtonParameters alg_params; + alg_params.solver = "ldlt"; + alg_params.do_reduction = true; + alg_params.max_itr = 100; + alg_params.log_level = -1; + + MatrixX identity = id_matrix(marked_metric.n_reduced_coordinates()); + Holonomy::OptimizeNewton solver; + MarkedPennerConeMetric opt_marked_metric = solver.run(marked_metric, identity, alg_params); + + + if (solver.get_log().max_error > 1e-10) + { + spdlog::warn("falling back to zero coordinates"); + alg_params.max_itr = 10000; + simp.m.l = std::vector(simp.m.n_halfedges(), 1.); + metric_coords = VectorX::Zero(simp.m.n_halfedges()); + marked_metric.change_metric(marked_metric, metric_coords); + opt_marked_metric = solver.run(marked_metric, identity, alg_params); + } + + if ((optimize_metric) && (opt_iters > 0)) + { + auto proj_params = std::make_shared(); + auto opt_params = std::make_shared(); + opt_params->num_iter = opt_iters; + proj_params->max_itr = 100; + proj_params->error_eps = 0; + auto opt_cone_metric = Optimization::optimize_metric(opt_marked_metric, opt_energy, proj_params, opt_params); + opt_marked_metric.change_metric(marked_metric, opt_cone_metric->get_metric_coordinates()); + } + + + VectorX opt_metric_coords = opt_marked_metric.get_reduced_metric_coordinates(); + VectorX he_metric_coords = opt_marked_metric.get_metric_coordinates(); + spdlog::info("optimized coordinates in range {}, {}", he_metric_coords.minCoeff(), he_metric_coords.maxCoeff()); + + auto [V_simp, F_simp] = generate_simplified_vf_mesh(); + std::vector vtx_reindex_simp = arange(simp.m.n_vertices()); + VectorX scale_factors = VectorX::Zero(simp.m.n_ind_vertices()); + InterpolationMesh interpolation_mesh, reverse_interpolation_mesh; + interpolate_penner_coordinates( + simp.m, + he_metric_coords, + scale_factors, + interpolation_mesh, + reverse_interpolation_mesh); + + Eigen::MatrixXd V_overlay; + interpolate_vertex_positions( + V_simp, + vtx_reindex_simp, + interpolation_mesh, + reverse_interpolation_mesh, + V_overlay); + + bool save_coarse_mesh = false; + if (save_coarse_mesh) + { + std::vector cut_h = {}; + + auto [m_o, V_o, F_o, uv_o, FT_o, is_cut_h, is_cut_o, fn_to_f_o, endpoints_o] = generate_VF_mesh_from_halfedge_metric( + V_simp, + simp.m, + vtx_reindex_simp, + marked_metric, + opt_metric_coords, + cut_h, + false); + + Holonomy::view_seamless_parameterization(V_o, F_o, uv_o, FT_o, "overlay", true); + //Holonomy::view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "refined", true); + + // Write the output mesh + std::string output_filename = "coarse_mesh.obj"; + write_obj_with_uv(output_filename, V_o, F_o, uv_o, FT_o); + } + + OverlayMesh m_o = interpolation_mesh.get_overlay_mesh(); + + return std::make_tuple(V_overlay, m_o); +} + +// ----------------------- +// Barycentric Coordinates +// ----------------------- + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::tip_face_coordinates( + int halfedge_index, + int face_index, + const OverlayStructure& overlay) const +{ + int vertex_index = to[halfedge_index]; + int hij = overlay.m.h[face_index]; + int hjk = overlay.m.n[hij]; + int hki = overlay.m.n[hjk]; + + + if (overlay.vertex_type[vertex_index] == VertexType::VV) + { + int h_bd = halfedge_index; + while (overlay.edge_type[h2e[h_bd]] != EdgeType::EE) + { + h_bd = prev[opp[h_bd]]; + } + + if (overlay.halfedge_parent[h_bd] == hki) return {1., 0.}; + if (overlay.halfedge_parent[h_bd] == hij) return {0., 1.}; + if (overlay.halfedge_parent[h_bd] == hjk) return {0., 0.}; + } + else if (overlay.vertex_type[vertex_index] == VertexType::VE) + { + int h_bd = halfedge_index; + while (overlay.edge_type[h2e[h_bd]] != EdgeType::EE) + { + h_bd = prev[opp[h_bd]]; + } + + BaryCoords edge_coords = overlay.bary_coords[vertex_index]; + return overlay.convert_halfedge_to_face_coordinates(overlay.halfedge_parent[h_bd], face_index, edge_coords); + } + else if (overlay.vertex_type[vertex_index] == VertexType::VF) + { + return overlay.bary_coords[vertex_index]; + } + + spdlog::error("tip not adjacent to face"); + assert(false); + return {0., 0.}; +} + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::tip_edge_coordinates( + int halfedge_index, + int edge_index, + const OverlayStructure& overlay) const +{ + int vertex_index = to[halfedge_index]; + int hij = overlay.e2h[edge_index]; + int hji = overlay.m.opp[edge_index]; + if (overlay.vertex_type[vertex_index] == VertexType::VV) + { + if (overlay.halfedge_parent[halfedge_index] == hji) return {1., 0.}; + if (overlay.halfedge_parent[halfedge_index] == hij) return {0., 0.}; + } + + return overlay.vertex_edge_coordinates(vertex_index, edge_index); +} + +Vector2 SimplificationOverlayMesh::project_halfedge_tip(int halfedge_index) const +{ + int hij = halfedge_index; + int vj = to[hij]; + int fijk = face[hij]; + int Fijk = orig.face_parent[fijk]; + + return orig.vertex_face_coordinates(vj, Fijk); +} + +SimplificationOverlayMesh::BaryCoords SimplificationOverlayMesh::convert_overlay_to_original_edge_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const +{ + assert(orig.edge_type[h2e[halfedge_index]] == EdgeType::EE); + + // get edge with correct orientation + int vi = to[opp[halfedge_index]]; + int vj = to[halfedge_index]; + int h_parent = orig.halfedge_parent[halfedge_index]; + int Eij = orig.h2e[h_parent]; + BaryCoords Vi = orig.vertex_edge_coordinates(vi, Eij); + BaryCoords Vj = orig.vertex_edge_coordinates(vj, Eij); + + Scalar t = overlay_edge_coord; + return ((t * Vj) + ((1. - t) * Vi)); +} + +SimplificationOverlayMesh::BaryCoords SimplificationOverlayMesh::convert_overlay_to_original_face_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const +{ + assert(orig.edge_type[h2e[halfedge_index]] == EdgeType::EF); + + // get edge with correct orientation + int vi = to[opp[halfedge_index]]; + int vj = to[halfedge_index]; + int Fijk = orig.halfedge_parent[halfedge_index]; + BaryCoords Vi = orig.vertex_face_coordinates(vi, Fijk); + BaryCoords Vj = orig.vertex_face_coordinates(vj, Fijk); + + Scalar t = overlay_edge_coord; + return ((t * Vj) + ((1. - t) * Vi)); +} + +SimplificationOverlayMesh::BaryCoords SimplificationOverlayMesh::convert_overlay_to_original_coordinates( + int halfedge_index, + Scalar overlay_edge_coord) const +{ + int eij = h2e[halfedge_index]; + if (orig.edge_type[eij] == EdgeType::EE) + { + return convert_overlay_to_original_edge_coordinates(halfedge_index, overlay_edge_coord); + } + else + { + return convert_overlay_to_original_face_coordinates(halfedge_index, overlay_edge_coord); + } +} + + +// ------------ +// Edge Lengths +// ------------ + +Scalar SimplificationOverlayMesh::halfedge_length(int halfedge_index) const +{ + int hij = halfedge_index; + int vj = to[hij]; + int vi = to[opp[hij]]; + + // use edge coordinates if contained in edge + if (orig.edge_type[h2e[hij]] == EdgeType::EE) + { + int Hij = orig.halfedge_parent[hij]; + int Eij = orig.h2e[Hij]; + Scalar s = orig.vertex_edge_coordinates(vi, Eij)[0]; + Scalar t = orig.vertex_edge_coordinates(vj, Eij)[0]; + Scalar l = orig.m.l[Hij]; + return abs(s - t) * l; + } + // build geometry from 3D positions otherwise + else + { + Eigen::Vector3d Vi = generate_overlay_vertex(vi, V_orig, vtx_reindex_orig, orig); + Eigen::Vector3d Vj = generate_overlay_vertex(vj, V_orig, vtx_reindex_orig, orig); + return (Vi - Vj).norm(); + } +} + +Scalar SimplificationOverlayMesh::compute_simplified_halfedge_length(int halfedge_index) const +{ + std::vector hij_path = enumerate_halfedges(halfedge_index, simp); + Scalar length = 0.; + for (int h : hij_path) + { + length += halfedge_length(h); + } + return length; +} + +Scalar SimplificationOverlayMesh::compute_extrinsic_halfedge_length(int halfedge_index) const +{ + int hij = halfedge_index; + int hji = simp.m.opp[hij]; + int Vi = simp.m.to[hji]; + int Vj = simp.m.to[hij]; + int vi = simp.vertex_index[Vi]; + int vj = simp.vertex_index[Vj]; + if (vi == vj) spdlog::warn("self adjacent edge"); + Eigen::Vector3d pi = generate_overlay_vertex(vi, V_orig, vtx_reindex_orig, orig); + Eigen::Vector3d pj = generate_overlay_vertex(vj, V_orig, vtx_reindex_orig, orig); + return (pi - pj).norm(); +} + +void SimplificationOverlayMesh::set_simplified_halfedge_lengths() +{ + std::vector start_halfedge = find_start_halfedges(simp); + for (int hij = 0; hij < simp.m.n_halfedges(); ++hij) + { + int hji = simp.m.opp[hij]; + if (hij < hji) continue; + Scalar length = compute_simplified_halfedge_length(start_halfedge[hij]); + simp.m.l[hij] = length; + simp.m.l[hji] = length; + } +} + +VectorX SimplificationOverlayMesh::compute_simplified_penner_coordinates() +{ + std::vector start_halfedge = find_start_halfedges(simp); + int num_halfedges = simp.m.n_halfedges(); + VectorX metric_coords(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + int hji = simp.m.opp[hij]; + if (hij < hji) continue; + Scalar length = compute_simplified_halfedge_length(start_halfedge[hij]); + metric_coords[hij] = (float_equal(length, 0.)) ? -10 : 2. * log(length); + metric_coords[hji] = metric_coords[hij]; + } + + return metric_coords; +} + +VectorX SimplificationOverlayMesh::compute_extrinsic_penner_coordinates() +{ + int num_halfedges = simp.m.n_halfedges(); + VectorX metric_coords(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + int hji = simp.m.opp[hij]; + if (hij < hji) continue; + Scalar length = compute_extrinsic_halfedge_length(hij); + if (isnan(length)) spdlog::error("edge length is NaN"); + metric_coords[hij] = (float_equal(length, 0.)) ? -10 : 2. * log(length); + metric_coords[hji] = metric_coords[hij]; + } + + return metric_coords; +} + +void SimplificationOverlayMesh::set_simplified_halfedge_extrinsic_lengths() +{ + for (int hij = 0; hij < simp.m.n_halfedges(); ++hij) + { + int hji = simp.m.opp[hij]; + if (hij < hji) continue; + Scalar length = compute_extrinsic_halfedge_length(hij); + if (isnan(length)) + { + spdlog::warn("edge length is NaN"); + length = 0.; + } + simp.m.l[hij] = length; + simp.m.l[hji] = length; + } +} + +void SimplificationOverlayMesh::set_simplified_halfedge_curvature_lengths(Scalar normal_scale) +{ + // compute extrinsic mesh and approximated normals + auto [V_simp, F_simp] = generate_simplified_vf_mesh(); + std::vector vtx_reindex_simp = arange(simp.m.n_vertices()); + simp.m.l = compute_curvature_adaptive_lengths(V_simp, F_simp, simp.m, vtx_reindex_simp, normal_scale); +} + +void SimplificationOverlayMesh::correct_simplified_halfedge_lengths() +{ + // Check triangle inequality for each halfedge + int num_halfedges = simp.m.n_halfedges(); + bool is_degenerate = false; + do + { + is_degenerate = false; + + // check for degenerate edge + for (int hi = 0; hi < num_halfedges; ++hi) { + int hj = simp.m.n[hi]; + int hk = simp.m.n[hj]; + + // Lengths of the halfedges + Scalar li = simp.m.l[hi]; + Scalar lj = simp.m.l[hj]; + Scalar lk = simp.m.l[hk]; + + // Check for zero length + if (float_equal(li, 0.)) + { + spdlog::trace("zero edge length {}", li); + is_degenerate = true; + } + + // Check triangle inequality + if ((li > lj + lk) || float_equal(li, lj + lk)) + { + spdlog::trace("edge length {} > {} + {}", li, lj, lk); + is_degenerate = true; + } + } + + if (!is_degenerate) return; + + // make more numerically stable + for (int hij = 0; hij < num_halfedges; ++hij) { + int hji = simp.m.opp[hij]; + if (hij < hji) continue; + if (simp.m.l[hij] <= 0.) simp.m.l[hij] = 1e-4; + simp.m.l[hij] = exp(0.9 * log(simp.m.l[hij])); + simp.m.l[hji] = simp.m.l[hij]; + if (isnan(simp.m.l[hij])) spdlog::error("edge length nan"); + } + } while (is_degenerate); +} + +// ------------------ +// Embedding Geometry +// ------------------ + +void SimplificationOverlayMesh::set_uniform_edge_coordinates(int start_halfedge, OverlayStructure& overlay) +{ + int hij = start_halfedge; + if (overlay.edge_type[h2e[hij]] != EdgeType::EE) return; + int Hij = overlay.halfedge_parent[hij]; + int Eij = overlay.h2e[Hij]; + std::vector halfedges = enumerate_halfedges(start_halfedge, overlay); + int hi = opp[halfedges.front()]; + int hj = halfedges.back(); + Scalar delta = 1. / halfedges.size(); + BaryCoords Vi = tip_edge_coordinates(hi, Eij, overlay); + BaryCoords Vj = tip_edge_coordinates(hj, Eij, overlay); + assert(Vi != Vj); + for (int k = 1; k < halfedges.size(); ++k) + { + int vk = to[opp[halfedges[k]]]; + Scalar t = delta * k; + overlay.bary_coords[vk] = (1. - t) * Vi + t * Vj; + } +} + +void SimplificationOverlayMesh::set_uniform_edge_coordinates(OverlayStructure& overlay) +{ + std::vector start_halfedge = find_start_halfedges(overlay); + + // find boundary coordiantes + int num_overlay_edges = overlay.e2h.size(); + for (int Eij = 0; Eij < num_overlay_edges; ++Eij) + { + int Hij = overlay.e2h[Eij]; + set_uniform_edge_coordinates(start_halfedge[Hij], overlay); + } +} + +void SimplificationOverlayMesh::find_tutte_coordinates(OverlayStructure& overlay, std::vector& use_face, bool use_adaptive_weight) +{ + assert(is_valid()); + //remove_parallel_edges(overlay); // need to ensure no parallel edges + //make_three_connected(overlay); + + // nothing to do if no faces flagged + if (!vector_contains(use_face, true)) return; + std::vector faces, old_faces, face_map; + enumerate_boolean_array(use_face, faces, old_faces, face_map); + + std::vector start_halfedge = find_start_halfedges(overlay); + + // get unique index for each vertex in each face + int num_halfedges = n.size(); + UnionFind face_vertices(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // union across face edges + int hkj = opp[n[hij]]; + if (overlay.edge_type[h2e[hkj]] == EdgeType::EF) face_vertices.union_sets(hij, hkj); + } + + // get representative halfedges for face vertices + int num_face_vertices = face_vertices.count_sets(); + std::vector vertex_representatives = face_vertices.get_representative_elements(); + std::vector h2fv = face_vertices.index_sets(); + std::vector mean_value_weights; + if (use_adaptive_weight) mean_value_weights = compute_mean_value_weights(orig); + + // determine boundary vertices + std::vector is_boundary_vertex(num_face_vertices, false); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (is_halfedge_deleted(hij)) continue; + int hji = opp[hij]; + if (overlay.face_parent[face[hij]] == overlay.face_parent[face[hji]]) continue; + + is_boundary_vertex[h2fv[hij]] = true; + } + + // enumerate interior and boundary vertices per face + int num_faces = faces.size(); + std::vector> interior_face_vertices(num_faces, std::vector()); + std::vector> boundary_face_vertices(num_faces, std::vector()); + std::vector vertex_index(num_face_vertices); + for (int vj = 0; vj < num_face_vertices; ++vj) + { + // get a representative halfedge + int hij = vertex_representatives[vj]; + if (is_halfedge_deleted(hij)) continue; + int Fijk = overlay.face_parent[face[hij]]; + if (!use_face[Fijk]) continue; + int f_parent = face_map[Fijk]; + + if (is_boundary_vertex[vj]) + { + vertex_index[vj] = boundary_face_vertices[f_parent].size(); + boundary_face_vertices[f_parent].push_back(vj); + } + else + { + vertex_index[vj] = interior_face_vertices[f_parent].size(); + interior_face_vertices[f_parent].push_back(vj); + } + } + + // build tutte matrix triplets for each face + std::vector> harmonic_matrix_trips(num_faces, std::vector()); + typedef Eigen::Matrix BaryMat; + //typedef Eigen::MatrixXd BaryMat; + std::vector> boundary_coords(num_faces); + for (int i = 0; i < num_faces; ++i) + { + boundary_coords[i] = std::vector(boundary_face_vertices[i].size()); + } + + // find boundary coordiantes + int num_overlay_halfedges = overlay.m.n.size(); + for (int Hij = 0; Hij < num_overlay_halfedges; ++Hij) + { + int Eij = overlay.h2e[Hij]; + int Fijk = overlay.m.f[Hij]; + if (!use_face[Fijk]) continue; + std::vector halfedges = enumerate_halfedges(start_halfedge[Hij], overlay); + for (int k = 1; k < halfedges.size(); ++k) + { + int hij = halfedges[k]; + int vi = to[prev[hij]]; + int i = vertex_index[h2fv[prev[hij]]]; + BaryCoords edge_coords = overlay.bary_coords[vi]; + boundary_coords[face_map[Fijk]][i] = overlay.convert_halfedge_to_face_coordinates(Hij, Fijk, edge_coords); + } + + // set first and last vertex + int hkj = halfedges.back(); + int vj = to[hkj]; + int j = vertex_index[h2fv[hkj]]; + boundary_coords[face_map[Fijk]][j] = tip_face_coordinates(hkj, Fijk, overlay); + + //int hik = halfedges.front(); + //int vi = to[prev[hik]]; + //int i = vertex_index[h2fv[prev[hik]]]; + //boundary_coords[face_map[Fijk]][i] = overlay.tip_face_coordinates(hik, vi, Fijk); + } + + // initialize rhs to zero + std::vector> harmonic_rhs(num_faces); + std::vector>> harmonic_connections(num_faces); + for (int i = 0; i < num_faces; ++i) + { + harmonic_rhs[i] = std::vector(interior_face_vertices[i].size(), {0., 0.}); + harmonic_connections[i].resize(interior_face_vertices[i].size()); + } + + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + int fijk = face[hij]; + int Fijk = overlay.face_parent[fijk]; + if (!use_face[Fijk]) continue; + int f_parent = face_map[Fijk]; + + int vi = h2fv[prev[hij]]; + int vj = h2fv[hij]; + if (is_boundary_vertex[vi]) continue; + + Scalar weight = (use_adaptive_weight) ? mean_value_weights[hij] : 1.; + if (weight <= 0.) + { + spdlog::warn("negative weight {}", weight); + continue; + } + + int i = vertex_index[vi]; + int j = vertex_index[vj]; + harmonic_matrix_trips[f_parent].push_back({i, i, weight}); + if (is_boundary_vertex[vj]) + { + harmonic_rhs[f_parent][i] += weight * boundary_coords[f_parent][j]; + harmonic_connections[f_parent][i].push_back(j); + } + else + { + harmonic_matrix_trips[f_parent].push_back({i, j, -weight}); + } + //harmonic_matrix_trips[f_parent].push_back({vi, vj, 1.}); + //harmonic_matrix_trips[f_parent].push_back({vi, vi, -1.}); + } + + //#pragma omp parallel for + int num_remeshed_faces = 0; + int total_variables = 0; + int max_size = 0; + for (int fijk = 0; fijk < num_faces; ++fijk) + { + int num_variables = interior_face_vertices[fijk].size(); + if (num_variables == 0) continue; + ++num_remeshed_faces; + total_variables += num_variables; + max_size = max(max_size, num_variables); + + BaryMat rhs(num_variables, 2); + for (int i = 0; i < num_variables; ++i) + { + rhs.row(i) = harmonic_rhs[fijk][i]; + } + + MatrixX tutte_system(num_variables, num_variables); + tutte_system.setFromTriplets( + harmonic_matrix_trips[fijk].begin(), + harmonic_matrix_trips[fijk].end()); + + // use LU as general weights may not be symmetric + Eigen::SparseLU solver; + //Eigen::CholmodSupernodalLLT solver; + //Eigen::SimplicialLDLT solver; + solver.compute(tutte_system); + if (solver.info() != Eigen::Success) spdlog::error("matrix factorization failed"); + BaryMat bary_coords = solver.solve(rhs); + + if (solver.info() != Eigen::Success) spdlog::error("matrix solve failed"); + Scalar solver_error = (tutte_system * bary_coords - rhs).norm(); + spdlog::debug("tutte solver error for {} is {}", fijk, solver_error); + + //overlay.he_bary_coords.resize(n.size()); + Scalar min_dist = 1.; + for (int i = 0; i < num_variables; ++i) + { + int vi = interior_face_vertices[fijk][i]; + int hji = vertex_representatives[vi]; + overlay.bary_coords[to[hji]] = bary_coords.row(i); + //overlay.he_bary_coords[hji] = bary_coords.row(i); // TODO remove + if (show_failure) + { + Scalar u = bary_coords(i, 0); + Scalar v = bary_coords(i, 1); + min_dist = min(min_dist, u); + min_dist = min(min_dist, 1 - u); + min_dist = min(min_dist, v); + min_dist = min(min_dist, 1 - v); + min_dist = min(min_dist, 1 - (u + v)); + } + } + + + //if (false) + if ((show_failure) && ((!float_equal(solver_error, 0.)) || (min_dist <= 0))) + { + spdlog::error("tutte solver failed"); + spdlog::error("minimum distance is {}", min_dist); + BaryMat bary_coords = solve_high_precision(tutte_system, rhs).cast(); + + int num_constants = boundary_face_vertices[fijk].size(); + int num_face_vertices = num_variables + num_constants; + BaryMat face_coords(num_face_vertices, 2); + face_coords.topRows(num_variables) = bary_coords; + for (int k = 0; k < num_constants; ++k) + { + face_coords.row(num_variables + k) = boundary_coords[fijk][k]; + } + std::vector> edges = {}; + for (int k = 0; k < tutte_system.outerSize(); ++k) { + for (Eigen::SparseMatrix::InnerIterator it(tutte_system, k); it; ++it) { + int row = it.row(); // row index + int col = it.col(); // column index + //double val = it.value(); + if (row == col) continue; + edges.push_back({row, col}); + } + } + for (int i = 0; i < num_variables; ++i) + { + for (int j : harmonic_connections[fijk][i]) + { + edges.push_back({i, num_variables + j}); + } + } +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerCurveNetwork2D("face layout", face_coords, edges); + polyscope::registerPointCloud2D("face boundary", boundary_coords[fijk]); + polyscope::show(); +#endif + write_sparse_matrix(tutte_system, "tutte_matrix.txt", "matlab"); + write_matrix(bary_coords, "bary_coords.txt"); + write_matrix(rhs, "rhs.txt"); + } + } + spdlog::info("{} faces remeshed with {} total variables", num_remeshed_faces, total_variables); + spdlog::info("largest system is {}", max_size); + + int num_flipped_faces = 0; + for (int fijk = 0; fijk < f2h.size(); ++fijk) + { + if (is_face_deleted(fijk)) continue; + int Fijk = overlay.face_parent[fijk]; + int hij = f2h[fijk]; + int hjk = n[hij]; + int hki = n[hjk]; + BaryCoords uvi = tip_face_coordinates(hki, Fijk, overlay); + BaryCoords uvj = tip_face_coordinates(hij, Fijk, overlay); + BaryCoords uvk = tip_face_coordinates(hjk, Fijk, overlay); + if (igl::predicates::orient2d(uvi, uvj, uvk) != igl::predicates::Orientation::POSITIVE) + { + spdlog::error("flipped face {}", fijk); + ++num_flipped_faces; + } + } + spdlog::info("{} flipped faces", num_flipped_faces); +} + +void SimplificationOverlayMesh::find_tutte_coordinates(OverlayStructure& overlay, bool use_adaptive_weight) +{ + remove_parallel_edges(overlay); // need to ensure no parallel edges + std::vector use_face(overlay.m.n_faces(), true); + find_tutte_coordinates(overlay, use_face, use_adaptive_weight); +} + +std::vector SimplificationOverlayMesh::compute_mean_value_weights( + const OverlayStructure& overlay) const +{ + const Scalar eps = 1e-12; + const Scalar pi = acos(Scalar(-1.)); + std::vector weights(n.size(), -1.); + + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + int hji = opp[hij]; + + // compute edge length and check for degenerate edge + Vector3 vi = generate_overlay_vertex(to[hji], V_orig, vtx_reindex_orig, overlay); + Vector3 vj = generate_overlay_vertex(to[hij], V_orig, vtx_reindex_orig, overlay); + Scalar edge_length = (vj - vi).norm(); + if (edge_length <= eps) + { + spdlog::warn("edge numerically degenerate"); + weights[hij] = 0.; + continue; + } + + // compute triangle angles at halfedge tip + std::array angles; + Vector3 vk = generate_overlay_vertex(to[n[hij]], V_orig, vtx_reindex_orig, overlay); + Vector3 vl = generate_overlay_vertex(to[n[hji]], V_orig, vtx_reindex_orig, overlay); + angles[0] = abs(compute_triangle_angle(vi, vj, vk)); + angles[1] = abs(compute_triangle_angle(vi, vl, vj)); + + // compute tangent sum + Scalar tan_half_angle_sum = 0.; + for (Scalar& angle : angles) + { + angle = min(max(angle, eps), pi - eps); + tan_half_angle_sum += tan(angle / 2.); + } + + // add weight to halfedge tip + Scalar weight = tan_half_angle_sum / edge_length; + assert(weight >= 0.); + weights[hij] = weight; + } + + return weights; +} + +// ------------------ +// Embedding Connectivity +// ------------------ + +// TODO make local and fast +void SimplificationOverlayMesh::flood_face(int h, int f_parent) +{ + // get unique index for each vertex in each face + UnionFind faces(n.size()); + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // always union with next halfedge in face + int hjk = n[hij]; + faces.union_sets(hij, hjk); + + // union across face edges, but not edge edges + int hji = opp[hij]; + if (simp.edge_type[h2e[hij]] == EdgeType::EF) faces.union_sets(hij, hji); + } + + // propagate face indices to all faces, edges, and vertices + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // set parent of face + //int f = faces.find_set(hij); + if (faces.find_set(hij) != faces.find_set(h)) continue; + simp.face_parent[face[hij]] = f_parent; + + // check edge type + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EF) + { + simp.halfedge_parent[hij] = f_parent; + } + + // check tip vertex type + int vj = to[hij]; + if (simp.vertex_type[vj] == VertexType::VF) + { + simp.vertex_parent[vj] = f_parent; + } + } +} + +void SimplificationOverlayMesh::flood_faces_union_find() +{ + assert(is_valid_edge_network()); + // get first halfedge of each simplified halfedge path + std::vector start_halfedge = find_start_halfedges(simp); + + // get unique index for each vertex in each face + UnionFind faces(n.size()); + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // always union with next halfedge in face + int hjk = n[hij]; + faces.union_sets(hij, hjk); + + // union across face edges, but not edge edges + int hji = opp[hij]; + if (simp.edge_type[h2e[hij]] == EdgeType::EF) faces.union_sets(hij, hji); + } + + // set face indices from start halfedges + auto& simp_faces = simp.m.f; + // TODO make option for base mesh, or unify representation + std::vector face_parent(faces.count_sets(), -1); + std::vector face_indices = faces.index_sets(); + for (int hij = 0; hij < simp_faces.size(); ++hij) + { + int h = start_halfedge[hij]; + if (!is_valid_halfedge_index(h)) spdlog::error("start halfedge not valid"); + if (is_halfedge_deleted(h)) spdlog::error("halfedge deleted"); + assert(simp.halfedge_parent[h] == hij); + + //int f = faces.find_set(h); + int face_index = face_indices[h]; + face_parent[face_index] = simp_faces[hij]; + } + + // sanity check + for (int hij = 0; hij < simp_faces.size(); ++hij) + { + int h = start_halfedge[hij]; + int face_index = face_indices[h]; + assert(face_parent[face_index] == simp_faces[hij]); + } + + // propagate face indices to all faces, edges, and vertices + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // set parent of face + //int f = faces.find_set(hij); + int face_index = face_indices[hij]; + int f_parent = face_parent[face_index]; + assert(is_valid_simplified_face_index(f_parent)); + simp.face_parent[face[hij]] = f_parent; + + // check edge type + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EF) + { + simp.halfedge_parent[hij] = f_parent; + } + + // check tip vertex type + int vj = to[hij]; + if (simp.vertex_type[vj] == VertexType::VF) + { + simp.vertex_parent[vj] = f_parent; + } + } +} + +void SimplificationOverlayMesh::flood_faces() +{ + assert(is_valid_edge_network()); + // get first halfedge of each simplified halfedge path + std::vector start_halfedge = find_start_halfedges(simp); + + // set face indices from start halfedges + // TODO make option for base mesh, or unify representation + int num_simp_faces = simp.m.f.size(); + std::vector faces_to_process = {}; + faces_to_process.reserve(num_simp_faces); + for (int hij = 0; hij < num_simp_faces; ++hij) + { + int h = start_halfedge[hij]; + if (!is_valid_halfedge_index(h)) spdlog::error("start halfedge not valid"); + if (is_halfedge_deleted(h)) spdlog::error("halfedge deleted"); + assert(simp.halfedge_parent[h] == hij); + + //int f = faces.find_set(h); + int face_index = face[h]; + simp.face_parent[face_index] = simp.m.f[hij]; + faces_to_process.push_back(face_index); + } + + int num_faces = f2h.size(); + std::vector is_processed_face(num_faces, false); + while (!faces_to_process.empty()) { + // Get the next face to process + int fi = faces_to_process.back(); + faces_to_process.pop_back(); + + // Skip already processed face + if (is_processed_face[fi]) continue; + is_processed_face[fi] = true; + + // Iterate over adjacent faces without crossing overlay edges + int f_parent = simp.face_parent[fi]; + int h_start = f2h[fi]; + int hij = h_start; + do { + // propagate labels across edge if not overlay edge + if (simp.edge_type[h2e[hij]] == EdgeType::EF) + { + // Get the face opposite the edge + int hji = opp[hij]; + int fj = face[hji]; + simp.halfedge_parent[hij] = f_parent; + simp.halfedge_parent[hji] = f_parent; + simp.face_parent[fj] = f_parent; + faces_to_process.push_back(fj); + } + + // label tip vertex if a face vertex + int vj = to[hij]; + if (simp.vertex_type[vj] == VertexType::VF) + { + simp.vertex_parent[vj] = f_parent; + } + + // Progress to the next halfedge in the vertex circulator + hij = n[hij]; + } while (hij != h_start); + } + + for (int f = 0; f < num_simp_faces; ++f) + { + if (is_face_deleted(f)) continue; + if (!is_processed_face[f]) spdlog::error("face {} not processed", f); + } +} + +std::vector SimplificationOverlayMesh::find_start_halfedges(const OverlayStructure& overlay) const +{ + int num_overlay_halfedges = overlay.m.n_halfedges(); + std::vector start_halfedge(num_overlay_halfedges, -1); + int num_halfedges_seen = 0; + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // check if halfedge is in a overlay edge and adjacent to an overlay vertex + if (overlay.edge_type[h2e[hij]] != EdgeType::EE) continue; + if (overlay.vertex_type[to[opp[hij]]] != VertexType::VV) continue; + + // set as start halfedge for edge if so + int Hij = overlay.halfedge_parent[hij]; + assert(Hij >= 0); + assert(Hij < num_overlay_halfedges); + start_halfedge[Hij] = hij; + ++num_halfedges_seen; + } + if (num_halfedges_seen > start_halfedge.size()) + { + spdlog::error("halfedge seen multiple times"); + assert(false); + } + if (num_halfedges_seen < start_halfedge.size()) + { + spdlog::error("not all halfedges seen"); + for (int i = 0; i < num_overlay_halfedges; ++i) + { + if (start_halfedge[i] < 0) spdlog::error("{} not seen", i); + } + assert(false); + } + + return start_halfedge; +} + +std::vector SimplificationOverlayMesh::find_overlay_vertices(const OverlayStructure& overlay) const +{ + int num_overlay_vertices = overlay.m.n_vertices(); + std::vector overlay_vertices(num_overlay_vertices, -1); + int num_vertices_seen = 0; + for (int vi = 0; vi < out.size(); ++vi) + { + if (is_vertex_deleted(vi)) continue; + if (overlay.vertex_type[vi] != VertexType::VV) continue; + overlay_vertices[overlay.vertex_parent[vi]] = vi; + num_vertices_seen++; + } + if (num_vertices_seen != num_overlay_vertices) + { + spdlog::error("inconsistent vertex counts"); + assert(false); + } + + return overlay_vertices; +} + +std::vector SimplificationOverlayMesh::find_end_halfedges(const OverlayStructure& overlay) const +{ + int num_overlay_halfedges = overlay.m.n_halfedges(); + std::vector end_halfedge(num_overlay_halfedges, -1); + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // check if halfedge is in a overlay edge and adjacent to an overlay vertex + if (overlay.edge_type[h2e[hij]] != EdgeType::EE) continue; + if (overlay.vertex_type[to[hij]] != VertexType::VV) continue; + + // set as start halfedge for edge if so + int Hij = overlay.halfedge_parent[hij]; + assert(Hij >= 0); + assert(Hij < num_overlay_halfedges); + end_halfedge[Hij] = hij; + } + + return end_halfedge; +} + +std::vector SimplificationOverlayMesh::enumerate_halfedges(int halfedge_index, const OverlayStructure& overlay) const +{ + int hij = halfedge_index; + std::vector halfedges = { hij }; + while (overlay.vertex_type[to[hij]] != VertexType::VV) + { + assert(overlay.vertex_type[to[hij]] == VertexType::VE); + hij = get_next_overlay_edge(hij, overlay); + halfedges.push_back(hij); + } + + return halfedges; +} + +std::vector> SimplificationOverlayMesh::enumerate_simplified_edges() const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + std::vector start_halfedge = find_start_halfedges(simp); + int num_edges = simp.e2h.size(); + std::vector> paths; + for (int e = 0; e < num_edges; ++e) + { + int h = simp.e2h[e]; + std::vector halfedges = enumerate_halfedges(start_halfedge[h], simp); + paths.push_back(std::vector()); + auto& curr_path = paths.back(); + curr_path.reserve(halfedges.size() + 1); + curr_path.push_back(v_old2new[to[opp[halfedges.front()]]]); + for (int hij : halfedges) + { + curr_path.push_back(v_old2new[to[hij]]); + } + } + + return paths; +} + +bool SimplificationOverlayMesh::is_start_cyclic_orientation( + int h_start, + int halfedge_index) const +{ + if (!use_consistency_check) return true; + + // get first counterclockwise edge around halfedge base + int hij = h_start; + int e_ccw = -1; + do { + hij = opp[prev[hij]]; + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EE) + { + e_ccw = simp.h2e[simp.halfedge_parent[hij]]; + break; + } + } while (hij != h_start); + if (e_ccw < 0) return true; // first edge adjacent to vertex + + // get first clockwise edge around halfedge base + hij = h_start; + int e_cw = -1; + do { + hij = n[opp[hij]]; + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EE) + { + e_cw = simp.h2e[simp.halfedge_parent[hij]]; + break; + } + } while (hij != h_start); + if (e_ccw == e_cw) return true; // second edge adjacent to vertex + + // determine if three edges are consistent in simplified edge + // just need to check if first edge seen clockwise is e_cw + const auto& m = simp.m; + int Hij = halfedge_index; + do { + Hij = m.n[m.opp[Hij]]; + if (simp.h2e[Hij] == e_ccw) return false; + if (simp.h2e[Hij] == e_cw) return true; + } while (Hij != halfedge_index); + + assert(false); // should never get here + return false; +} + +bool SimplificationOverlayMesh::is_end_cyclic_orientation( + int h_start, + int halfedge_index) const +{ + if (!use_consistency_check) return true; + + // get first counterclockwise edge around halfedge tip + int hij = h_start; + int e_ccw = -1; + do { + hij = prev[opp[hij]]; + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EE) + { + e_ccw = simp.h2e[simp.halfedge_parent[hij]]; + break; + } + } while (hij != h_start); + if (e_ccw < 0) return true; // first edge adjacent to vertex + + // get first clockwise edge around halfedge tip + hij = h_start; + int e_cw = -1; + do { + hij = opp[n[hij]]; + int eij = h2e[hij]; + if (simp.edge_type[eij] == EdgeType::EE) + { + e_cw = simp.h2e[simp.halfedge_parent[hij]]; + break; + } + } while (hij != h_start); + if (e_ccw == e_cw) return true; // second edge adjacent to vertex + + // determine if three edges are consistent in simplified edge + // just need to check if first edge seen clockwise is e_cw + const auto& m = simp.m; + int Hij = halfedge_index; + do { + Hij = m.opp[m.n[Hij]]; + if (simp.h2e[Hij] == e_ccw) return false; + if (simp.h2e[Hij] == e_cw) return true; + } while (Hij != halfedge_index); + + assert(false); // should never get here + return false; +} + +std::vector> SimplificationOverlayMesh::enumerate_simplified_halfedges() const +{ + std::vector start_halfedge = find_start_halfedges(simp); + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + int num_edges = simp.e2h.size(); + std::vector> paths; + for (int e = 0; e < num_edges; ++e) + { + int h = simp.e2h[e]; + std::vector halfedges = enumerate_halfedges(start_halfedge[h], simp); + paths.push_back(std::vector()); + auto& curr_path = paths.back(); + curr_path.reserve(halfedges.size()); + for (int hij : halfedges) + { + curr_path.push_back(he_old2new[hij]); + } + } + + return paths; +} + + +// ------------------ +// Overlay Navigation +// ------------------ + +int SimplificationOverlayMesh::get_next_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const +{ + int hij = halfedge_index; + + // get halfedge with base at tip of input halfedge + hij = opp[hij]; + + // rotate ccw around base until hit another overlay edge + do { + hij = n[opp[hij]]; + } while (overlay.edge_type[h2e[hij]] != EdgeType::EE); + assert(overlay.vertex_type[to[hij]] != VertexType::VF); + + return hij; +} + +int SimplificationOverlayMesh::get_ccw_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const +{ + int hij = halfedge_index; + do { + hij = opp[prev[hij]]; + } while (overlay.edge_type[h2e[hij]] != EdgeType::EE); + assert(overlay.vertex_type[to[hij]] != VertexType::VF); + + return hij; +} + +int SimplificationOverlayMesh::get_clockwise_overlay_edge(int halfedge_index, const OverlayStructure& overlay) const +{ + int hij = halfedge_index; + do { + hij = n[opp[hij]]; + } while (overlay.edge_type[h2e[hij]] != EdgeType::EE); + assert(overlay.vertex_type[to[hij]] != VertexType::VF); + + return hij; +} + +// -------------- +// Robust Cleanup +// -------------- + +bool SimplificationOverlayMesh::is_degenerate_edge_coordinates(int start_halfedge, OverlayStructure& overlay) +{ + int hij = start_halfedge; + if (overlay.edge_type[h2e[hij]] != EdgeType::EE) return false; + + int Hij = overlay.halfedge_parent[hij]; + int Eij = overlay.h2e[Hij]; + std::vector halfedges = enumerate_halfedges(start_halfedge, overlay); + if (halfedges.size() == 1) return false; + + int vi = to[opp[halfedges.front()]]; + int vj = to[halfedges.back()]; + int vk = to[halfedges.front()]; + int vl = to[opp[halfedges.back()]]; + BaryCoords Vi = overlay.vertex_edge_coordinates(vi, Eij); + BaryCoords Vj = overlay.vertex_edge_coordinates(vj, Eij); + if (float_equal(Vi[0], overlay.bary_coords[vk][0], 1e-8)) return true; + if (float_equal(Vj[0], overlay.bary_coords[vl][0], 1e-8)) return true; + for (int k = 1; k < halfedges.size() - 1; ++k) + { + int vk = to[opp[halfedges[k]]]; + int vl = to[halfedges[k]]; + Scalar s = overlay.bary_coords[vk][0]; + Scalar t = overlay.bary_coords[vl][0]; + if (float_equal(s, t, 1e-8)) return true; + } + + return false; +} + +void SimplificationOverlayMesh::fix_degenerate_edge_coordinates(int start_halfedge, OverlayStructure& overlay) +{ + int hij = start_halfedge; + if (overlay.edge_type[h2e[hij]] != EdgeType::EE) return; + int Hij = overlay.halfedge_parent[hij]; + int Eij = overlay.h2e[Hij]; + std::vector halfedges = enumerate_halfedges(start_halfedge, overlay); + for (int k = 0; k < halfedges.size()-1; ++k) + { + int vi = to[opp[halfedges[k]]]; + int vj = to[halfedges[k]]; + int vk = to[halfedges[k + 1]]; + BaryCoords Vi = overlay.vertex_edge_coordinates(vi, Eij); + BaryCoords Vj = overlay.vertex_edge_coordinates(vj, Eij); + BaryCoords Vk = overlay.vertex_edge_coordinates(vk, Eij); + if (float_equal(Vi[0], Vj[0], 1e-8)) + { + Scalar t = 0.01; + overlay.bary_coords[vj] = (1. - t) * Vk + t * Vj; + } + else if (float_equal(Vk[0], Vj[0], 1e-8)) + { + Scalar t = 0.01; + overlay.bary_coords[vj] = (1. - t) * Vi + t * Vj; + } + } +} + +void SimplificationOverlayMesh::fix_degenerate_edge_coordinates(OverlayStructure& overlay) +{ + std::vector start_halfedge = find_start_halfedges(overlay); + + // find boundary coordiantes + int num_overlay_edges = overlay.e2h.size(); + int count = 0; + for (int Eij = 0; Eij < num_overlay_edges; ++Eij) + { + int Hij = overlay.e2h[Eij]; + while (is_degenerate_edge_coordinates(start_halfedge[Hij], overlay)) + { + fix_degenerate_edge_coordinates(start_halfedge[Hij], overlay); + ++count; + } + } + spdlog::info("{} edges made uniform", count); +} + +void SimplificationOverlayMesh::triangulate_with_tutte() +{ + // first ensure there are no degenerate edges + fix_degenerate_edge_coordinates(orig); + + std::vector remesh_face(orig.m.n_faces(), false); + int num_faces = f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) continue; + bool success = triangulate_face(f); + if (!success) remesh_face[orig.face_parent[f]] = true; + } + + remove_parallel_edges(simp); // need to ensure no parallel edges + remove_parallel_edges(orig); // need to ensure no parallel edges + + // check for degenerate triangles + num_faces = f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) continue; + if (compute_polygon_face_area(f) < 0.) + { + remesh_face[orig.face_parent[f]] = true; + } + } + + find_tutte_coordinates(orig, remesh_face); +} + + +// -------------- +// Simplification +// -------------- + +void SimplificationOverlayMesh::remove_excess_refinement() +{ + assert(is_valid()); + if (keep_all_refinement) + { + collect_garbage(); + return; + } + + // ensure triangle mesh first for edge collapse + triangulate(); + assert(is_valid()); + + // collapse any possible halfedges to straighten the paths + int edges_collapsed = 0; + do + { + edges_collapsed = 0; + break; // TODO skipping + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + if (!is_collapse_edge_valid(hij)) continue; + + // TODO: need to prevent geometric collapse. The link condition should help + // but we should add an explicit check. This might limit how much refinement we + // can do. + //if (!is_collapse_link_valid(hij)) continue; + + if (!is_straightening_collapse(hij)) continue; + collapse_edge(hij); + ++edges_collapsed; + } + spdlog::info("{} edges collapsed", edges_collapsed); + } while (edges_collapsed > 0); + + // remove any possible halfedges + + int edges_removed = 0; + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + if (!is_edge_removable(hij)) continue; + if (!is_join_edge_faces_valid(hij)) continue; + join_edge_faces(hij); + ++edges_removed; + } + spdlog::info("{} edges removed", edges_removed); + + // remove any degree one vertices (always removable) + int d1_vertices_removed = 0; + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + int vj = to[hij]; + while (is_vertex_degree_one(vj)) + { + ++d1_vertices_removed; + vj = remove_degree_one_vertex(vj); + } + } + spdlog::info("{} degree one vertices removed", d1_vertices_removed); + + // remove any possible degree two vertices + int d2_vertices_removed = 0; + for (int hij = 0; hij < n.size(); ++hij) + { + if (skip_degree_two) break; + if (is_halfedge_deleted(hij)) continue; + int vj = to[hij]; + int vi = to[opp[hij]]; + + // check if interior degree two vertex + if (!is_vertex_degree_two(vj)) continue; + if (orig.vertex_type[vj] == VertexType::VV) continue; + if (simp.vertex_type[vj] == VertexType::VV) continue; + if ((orig.vertex_type[vj] == VertexType::VE) && (orig.edge_type[h2e[hij]] != EdgeType::EE)) continue; + + if (simp.vertex_type[vj] == VertexType::VE) + { + // only collapse simplified vertices along simplified edges + if (simp.edge_type[h2e[hij]] != EdgeType::EE) continue; + + // don't move simplified vertices along or into original edge + if (orig.edge_type[h2e[hij]] == EdgeType::EE) continue; + if (orig.vertex_type[vi] != VertexType::VF) continue; + } + + if (!is_join_halfedge_endpoints_valid(hij)) continue; + join_halfedge_endpoints(hij); + ++d2_vertices_removed; + } + spdlog::info("{} degree two vertices removed", d2_vertices_removed); + assert(is_valid()); + + // restore triangulation + triangulate_with_tutte(); + + // reindex to compress + collect_garbage(); + + // check if degenerate + std::vector face_areas = compute_face_areas(); + spdlog::info("minimum face area after triangulation is {}", vector_min(face_areas)); +} + +/* +void SimplificationOverlayMesh::simplify_original_face(int halfedge_index) +{ + if (keep_all_refinement) return; +} +*/ + +bool SimplificationOverlayMesh::is_edge_removable(int halfedge_index) +{ + int hij = halfedge_index; + if (is_halfedge_deleted(hij)) return false; + + if (orig.edge_type[h2e[hij]] != EdgeType::EF) return false; + if (simp.edge_type[h2e[hij]] != EdgeType::EF) return false; + + return true; +} + +bool SimplificationOverlayMesh::is_vertex_removable(int vertex_index) +{ + if (is_vertex_deleted(vertex_index)) return false; + + if (orig.vertex_type[vertex_index] != VertexType::VF) return false; + if (simp.vertex_type[vertex_index] != VertexType::VF) return false; + + return true; +} + +bool SimplificationOverlayMesh::is_halfedge_collapsable(int halfedge_index) +{ + int hij = halfedge_index; + int vj = to[hij]; + if (is_halfedge_deleted(hij)) return false; + + // can never collapse original or simplified vertex + if (orig.vertex_type[vj] == VertexType::VV) return false; + if (simp.vertex_type[vj] == VertexType::VV) return false; + + // cannot collapse edge vertex if the collapsed halfedge is not an original edge + int hjk = n[hij]; + int hlj = prev[opp[hij]]; + if (orig.vertex_type[vj] == VertexType::VE) + { + if (simp.vertex_type[vj] != VertexType::VF) return false; + if (orig.edge_type[h2e[hij]] != EdgeType::EE) return false; + if (!is_edge_removable(hlj)) return false; + if (!is_edge_removable(hjk)) return false; + } + + // likewise for simplified mesh + if (simp.vertex_type[vj] == VertexType::VE) + { + if (orig.vertex_type[vj] != VertexType::VF) return false; + if (simp.edge_type[h2e[hij]] != EdgeType::EE) return false; + if (!is_edge_removable(hlj)) return false; + if (!is_edge_removable(hjk)) return false; + } + + return true; +} + +bool SimplificationOverlayMesh::is_straightening_collapse(int halfedge_index) +{ + if (is_halfedge_deleted(halfedge_index)) return false; + int hij = halfedge_index; + int eij = h2e[hij]; + int vi = to[opp[hij]]; + int vj = to[hij]; + if (simp.edge_type[eij] != EdgeType::EE) return false; + + // can never collapse across a vertex + if (orig.vertex_type[vj] == VertexType::VV) return false; + if (simp.vertex_type[vj] == VertexType::VV) return false; + + // can only collapse joint intersecting original edge if contained entirely + int hjk = get_next_overlay_edge(hij, simp); + int vk = to[hjk]; + if (orig.vertex_type[vj] == VertexType::VE) + { + if (orig.edge_type[h2e[hij]] != EdgeType::EE) return false; + if (orig.edge_type[h2e[hjk]] != EdgeType::EE) return false; + } + // otherwise, must be entirely in face + else + { + if (orig.edge_type[h2e[hij]] != EdgeType::EF) return false; + if (orig.edge_type[h2e[hjk]] != EdgeType::EF) return false; + + // cannot collapse into edge + if ((orig.vertex_type[vi] == VertexType::VV) && (orig.vertex_type[vk] == VertexType::VV)) return false; + else if ((orig.vertex_type[vi] == VertexType::VE) && (orig.vertex_type[vk] == VertexType::VE)) + { + if (orig.vertex_parent[vi] == orig.vertex_parent[vk]) return false; + } + else if ((orig.vertex_type[vi] == VertexType::VV) && (orig.vertex_type[vk] == VertexType::VE)) + { + int Vi = orig.vertex_parent[vi]; + int Eij = orig.vertex_parent[vk]; + int Hij = orig.e2h[Eij]; + if (orig.m.to[Hij] == Vi) return false; + if (orig.m.to[orig.m.opp[Hij]] == Vi) return false; + } + else if ((orig.vertex_type[vi] == VertexType::VE) && (orig.vertex_type[vk] == VertexType::VV)) + { + int Vk = orig.vertex_parent[vk]; + int Eij = orig.vertex_parent[vi]; + int Hij = orig.e2h[Eij]; + if (orig.m.to[Hij] == Vk) return false; + if (orig.m.to[orig.m.opp[Hij]] == Vk) return false; + } + } + + return true; +} + +// ---------------------------- +// Embedded Geometry Generation +// ---------------------------- + +Eigen::Vector3d SimplificationOverlayMesh::generate_overlay_vertex( + int vi, + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const OverlayStructure& overlay) const +{ + const auto& m = overlay.m; + if (is_vertex_deleted(vi)) return Eigen::Vector3d(); + + if (overlay.vertex_type[vi] == VertexType::VV) + { + int wi = overlay.vertex_parent[vi]; + return V.row(vtx_reindex[m.v_rep[wi]]); + } + else if (overlay.vertex_type[vi] == VertexType::VE) + { + int eij = overlay.vertex_parent[vi]; + int hij = overlay.e2h[eij]; + int wi = m.to[m.opp[hij]]; + int wj = m.to[hij]; + auto Wi = V.row(vtx_reindex[m.v_rep[wi]]); + auto Wj = V.row(vtx_reindex[m.v_rep[wj]]); + double s = (double)(overlay.bary_coords[vi][0]); + return s * Wi + (1. - s) * Wj; + } + else if (overlay.vertex_type[vi] == VertexType::VF) + { + int fijk = overlay.vertex_parent[vi]; + int hij = m.h[fijk]; + int wj = m.to[hij]; + int wk = m.to[m.n[hij]]; + int wi = m.to[m.n[m.n[hij]]]; + auto Wi = V.row(vtx_reindex[m.v_rep[wi]]); + auto Wj = V.row(vtx_reindex[m.v_rep[wj]]); + auto Wk = V.row(vtx_reindex[m.v_rep[wk]]); + double s = (double)(overlay.bary_coords[vi][0]); + double t = (double)(overlay.bary_coords[vi][1]); + return s * Wi + t * Wj + (1. - s - t) * Wk; + } + else + { + spdlog::warn("unexpected vertex type"); + return Eigen::Vector3d(); + } +} + +std::vector SimplificationOverlayMesh::get_face_parent(const OverlayStructure& overlay) const +{ + auto [f_new2old, f_old2new] = get_face_reductions(); + return vector_compose(overlay.face_parent, f_new2old); +} + +Eigen::MatrixXd SimplificationOverlayMesh::generate_overlay_vertices( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex, + const OverlayStructure& overlay) const +{ + + int num_vertices = count_vertices(); + Eigen::MatrixXd V_o(num_vertices, 3); + int i = 0; + for (int vi = 0; vi < out.size(); ++vi) + { + if (is_vertex_deleted(vi)) continue; + V_o.row(i) = generate_overlay_vertex(vi, V, vtx_reindex, overlay); + i++; + } + + return V_o; +} + +std::tuple SimplificationOverlayMesh::generate_overlay_faces() const +{ + return generate_halfedge_faces(); +} + +std::tuple SimplificationOverlayMesh::generate_simplified_vf_mesh() const +{ + Eigen::MatrixXi F_simp, F_halfedge; + std::tie(F_simp, F_halfedge) = generate_connectivity_faces(simp.m.n, simp.m.h, simp.m.to); + int num_vertices = simp.m.n_vertices(); + Eigen::MatrixXd V_simp(num_vertices, 3); + for (int vi = 0; vi < num_vertices; ++vi) + { + int Vi = simp.vertex_index[vi]; + V_simp.row(vi) = generate_overlay_vertex(Vi, V_orig, vtx_reindex_orig, orig); + } + + return std::make_tuple(V_simp, F_simp); +} + +std::tuple +SimplificationOverlayMesh::generate_simplified_edges( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + int num_edges = e2h.size(); + std::vector> edges; + std::vector parent; + edges.reserve(count_edges()); + for (int eij = 0; eij < num_edges; ++eij) + { + // only add edges contained in simpified mesh edges + if (is_edge_deleted(eij)) continue; + if (simp.edge_type[eij] == EdgeType::EF) continue; + + int hij = e2h[eij]; + int vi = v_old2new[to[opp[hij]]]; + int vj = v_old2new[to[hij]]; + edges.push_back({vi, vj}); + parent.push_back(simp.halfedge_parent[e2h[eij]]); // TODO: get edge in original mesh + } + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + auto [V_s, edges_s] = remove_unreferenced_vertices(V_o, edges); + Eigen::MatrixXi E_s(edges_s.size(), 2); + for (int i = 0; i < edges_s.size(); ++i) + { + E_s(i, 0) = edges_s[i][0]; + E_s(i, 1) = edges_s[i][1]; + } + + return std::make_tuple(V_s, E_s); +} + +// --------------------- +// Regularity Conditions +// --------------------- + +bool SimplificationOverlayMesh::is_regular_face(int face_index) const +{ + if (is_face_deleted(face_index)) return false; + int hij = f2h[face_index]; + int hkl = hij; + int num_embedded_edges = 0; + + // count number of embedded edges + do + { + if (simp.edge_type[h2e[hkl]] == EdgeType::EE) + { + ++num_embedded_edges; + } + hkl = n[hkl]; + } while (hij != hkl); + + return (num_embedded_edges < 2); +} + +bool SimplificationOverlayMesh::is_regular_edge(int edge_index) const +{ + if (is_edge_deleted(edge_index)) return false; + + // if the edge is embedded, its always regular + if (simp.edge_type[edge_index] == EdgeType::EE) return true; + + // if not, it must have at least one non-embedded vertex + int hij = e2h[edge_index]; + int hji = opp[hij]; + if (simp.vertex_type[to[hij]] == VertexType::VF) return true; + if (simp.vertex_type[to[hji]] == VertexType::VF) return true; + + return false; +} + +bool SimplificationOverlayMesh::is_parallel_edge(int edge_index, const OverlayStructure& overlay) const +{ + if (is_edge_deleted(edge_index)) return false; + + // an embedded edge cannot be parallel + if (overlay.edge_type[edge_index] == EdgeType::EE) return false; + + // not parallel if one of the edge endpoints is embedded in a face interior + int hij = e2h[edge_index]; + int hji = opp[hij]; + int vi = to[hji]; + int vj = to[hij]; + if (overlay.vertex_type[vi] == VertexType::VF) return false; + if (overlay.vertex_type[vj] == VertexType::VF) return false; + + // parallel if both endpoints are embedded vertices (assuming edge is not embedded) + if ((overlay.vertex_type[vi] == VertexType::VV) && (overlay.vertex_type[vj] == VertexType::VV)) return true; + + // complex case: first endpoint is embedded in an edge + if (overlay.vertex_type[vi] == VertexType::VE) + { + // get index of edge containing the first endpoint + int Ekl = overlay.vertex_parent[vi]; + int Hkl = overlay.e2h[Ekl]; + + // get endpoints of the embedded mesh edge + int Vk = overlay.m.to[overlay.m.opp[Hkl]]; + int Vl = overlay.m.to[Hkl]; + + // parallel if the other endpoint is embedded in the same edge + if ((overlay.vertex_type[vj] == VertexType::VE) && (overlay.vertex_parent[vj] == Ekl)) return true; + + // parallel if the other endpoint is one of the edge endpoints + if ((overlay.vertex_type[vj] == VertexType::VV) && (overlay.vertex_parent[vj] == Vk)) return true; + if ((overlay.vertex_type[vj] == VertexType::VV) && (overlay.vertex_parent[vj] == Vl)) return true; + } + + // equivalent check for other endpoint + if (overlay.vertex_type[vj] == VertexType::VE) + { + int Ekl = overlay.vertex_parent[vj]; + int Hkl = overlay.e2h[Ekl]; + int Vk = overlay.m.to[overlay.m.opp[Hkl]]; + int Vl = overlay.m.to[Hkl]; + if ((overlay.vertex_type[vi] == VertexType::VE) && (overlay.vertex_parent[vi] == Ekl)) return true; + if ((overlay.vertex_type[vi] == VertexType::VV) && (overlay.vertex_parent[vi] == Vk)) return true; + if ((overlay.vertex_type[vi] == VertexType::VV) && (overlay.vertex_parent[vi] == Vl)) return true; + } + + return false; +} + +bool SimplificationOverlayMesh::is_parallel_free(const OverlayStructure& overlay) +{ + // ensure all edges have at most one embedded vertex + int num_edges = e2h.size(); + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) continue; + if (is_parallel_edge(e, overlay)) + { + spdlog::warn("{} is parallel", e); + return false; + } + } + + return true; +} + +bool SimplificationOverlayMesh::is_three_connected(const OverlayStructure& overlay) +{ + // ensure all edges have at most one embedded vertex + int num_edges = e2h.size(); + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) continue; + int h = e2h[e]; + int vi = to[h]; + int vj = to[opp[h]]; + if (overlay.edge_type[e] == EdgeType::EE) continue; + if (overlay.vertex_type[vi] == VertexType::VF) continue; + if (overlay.vertex_type[vj] == VertexType::VF) continue; + spdlog::warn("{} connects two edges", e); + return false; + } + + return true; +} + +void SimplificationOverlayMesh::make_edge_regular() +{ + // ensure all edges have at most one embedded vertex + int num_edges = e2h.size(); + int num_refined_edges = 0; + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) continue; + + // nothing to do if already regular + if (is_regular_edge(e)) continue; + + // split edge (while maintaining triangulation) if not + int h = e2h[e]; + int hik = split_edge(e2h[e]); + int hjk = opp[n[hik]]; + split_face(hik, n[n[hik]]); + split_face(hjk, n[n[hjk]]); + + // increment count if split + num_refined_edges++; + } + spdlog::info("{} refined edges", num_refined_edges); +} + +void SimplificationOverlayMesh::make_face_regular() +{ + // ensure all faces have at most one embedded edge + int num_faces = f2h.size(); + int num_refined_faces = 0; + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) continue; + + // nothing to do if already regular + if (is_regular_face(f)) continue; + + // simply add center to face to make regular + add_center_to_face(f); + + // increment count if split + num_refined_faces++; + } + spdlog::info("{} refined faces", num_refined_faces); +} + +void SimplificationOverlayMesh::make_regular() +{ + // WARNING: order matters + make_edge_regular(); + make_face_regular(); +} + +void SimplificationOverlayMesh::make_adjacent_regular(const std::vector& halfedges) +{ + // get edges adjacent to the interior vertices of the path + std::vector adjacent_edges = {}; + int num_halfedges = halfedges.size(); + adjacent_edges.reserve(6 * num_halfedges); + for (int i = 0; i < num_halfedges; ++i) + { + int hij = halfedges[i]; + if (simp.vertex_type[to[opp[hij]]] == VertexType::VV) continue; + + // circulate around vertex + int h = hij; + do + { + adjacent_edges.push_back(h2e[hij]); + hij = n[opp[hij]]; + } while (hij != h); + } + + // ensure all edges have at most one embedded vertex + int num_refined_edges = 0; + for (int e : adjacent_edges) + { + if (is_edge_deleted(e)) continue; + + // split irregular edges + if (is_regular_edge(e)) continue; + int hik = split_edge(e2h[e]); + int hjk = opp[n[hik]]; + split_face(hik, n[n[hik]]); + split_face(hjk, n[n[hjk]]); + num_refined_edges++; + } + spdlog::trace("{} refined edges", num_refined_edges); +} + +void SimplificationOverlayMesh::make_oriented_adjacent_regular(const std::vector& halfedges) +{ + // get edges adjacent to the interior vertices of the path on the same side as the halfedge + std::vector adjacent_edges = {}; + int num_halfedges = halfedges.size(); + adjacent_edges.reserve(6 * num_halfedges); + for (int i = 0; i < num_halfedges - 1; ++i) + { + // circulate clockwise until the next edge in the path is found + int hij = halfedges[i]; + int hjk = halfedges[i + 1]; + do + { + adjacent_edges.push_back(h2e[hij]); + hij = opp[n[hij]]; + } while (hij != opp[hjk]); + } + + // ensure all edges have at most one embedded vertex + int num_refined_edges = 0; + for (int e : adjacent_edges) + { + if (is_regular_edge(e)) continue; + int hik = split_edge(e2h[e]); + int hjk = opp[n[hik]]; + split_face(hik, n[n[hik]]); + split_face(hjk, n[n[hjk]]); + num_refined_edges++; + } + spdlog::trace("{} refined edges", num_refined_edges); +} + + +void SimplificationOverlayMesh::remove_parallel_edges(const OverlayStructure& overlay) +{ + // ensure all edges have at most one embedded vertex + int num_refined_edges = 0; + do + { + num_refined_edges = 0; + int num_edges = e2h.size(); + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) continue; + + // check if edge is parallel + if (!is_parallel_edge(e, overlay)) continue; + + // split parallel edges + int h = e2h[e]; + int hik = split_edge(e2h[e]); + int hjk = opp[n[hik]]; + split_face(hik, n[n[hik]]); + split_face(hjk, n[n[hjk]]); + num_refined_edges++; + } + spdlog::info("{} refined edges to remove parallel edges", num_refined_edges); + } + while (num_refined_edges > 0); + assert(is_parallel_free(overlay)); +} + + +void SimplificationOverlayMesh::make_three_connected(const OverlayStructure& overlay) +{ + // ensure all edges have at most one embedded vertex + int num_edges = e2h.size(); + int num_refined_edges = 0; + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) continue; + int h = e2h[e]; + int vi = to[h]; + int vj = to[opp[h]]; + + // check if interior edge spans the boundary + if (overlay.edge_type[e] == EdgeType::EE) continue; + if (overlay.vertex_type[vi] == VertexType::VF) continue; + if (overlay.vertex_type[vj] == VertexType::VF) continue; + + // split edge if necessary + int hik = split_edge(e2h[e]); + int hjk = opp[n[hik]]; + split_face(hik, n[n[hik]]); + split_face(hjk, n[n[hjk]]); + num_refined_edges++; + } + spdlog::info("{} refined edges to make three connected", num_refined_edges); + if (!is_three_connected(overlay)) spdlog::warn("not three connected"); + assert(is_three_connected(overlay)); +} + +std::vector SimplificationOverlayMesh::compute_corner_angles() const +{ + int num_halfedges = n.size(); + std::vector corner_angles(num_halfedges, 0.); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (is_halfedge_deleted(hij)) continue; + Vector2 vi = project_halfedge_tip(prev[hij]); + Vector2 vj = project_halfedge_tip(hij); + Vector2 vk = project_halfedge_tip(n[hij]); + corner_angles[hij] = compute_triangle_angle(vj, vk, vi); + } + + return corner_angles; +} + +std::vector SimplificationOverlayMesh::compute_face_areas() const +{ + int num_faces = f2h.size(); + std::vector face_areas; + face_areas.reserve(num_faces); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + if (is_face_deleted(fijk)) continue; + int hij = f2h[fijk]; + Vector2 vi = project_halfedge_tip(prev[hij]); + Vector2 vj = project_halfedge_tip(hij); + Vector2 vk = project_halfedge_tip(n[hij]); + face_areas.push_back(compute_triangle_area(vj, vk, vi)); + } + + return face_areas; +} + +Scalar SimplificationOverlayMesh::compute_polygon_face_area(int face_index) const +{ + int hij = f2h[face_index]; + int degree = face_degree(face_index); + + std::vector face_halfedges(degree); + std::vector face_vertices(degree); + int h = hij; + for (int i = 0; i < degree; ++i) + { + face_halfedges[i] = h; + face_vertices[i] = project_halfedge_tip(h); + h = n[h]; + } + + Scalar area = 0.; + for (int i = 0; i < degree - 2; ++i) + { + int vi = i; + int vj = (i + 1) % degree; + int vk = (i + 2) % degree; + area += compute_triangle_area(face_vertices[vi], face_vertices[vj], face_vertices[vk]); + } + + return area; +} + +// ----------------- +// Atomic Operations +// ----------------- + +int SimplificationOverlayMesh::join_edge_faces(int halfedge_index) +{ + assert(is_edge_removable(halfedge_index)); + int f = HalfedgeMesh::join_edge_faces(halfedge_index); + join_edge_faces_overlay(f, halfedge_index, orig); + join_edge_faces_overlay(f, halfedge_index, simp); + + return f; +} + +/* +int SimplificationOverlayMesh::collapse_edge(int halfedge_index) +{ + int hij = HalfedgeMesh::collapse_edge(halfedge_index); + //collapse_edge_overlay(halfedge_index, orig); + //collapse_edge_overlay(halfedge_index, simp); + + return hij; +} +*/ + +int SimplificationOverlayMesh::collapse_edge(int halfedge_index) +{ + int hij = halfedge_index; + int hji = opp[hij]; + assert(face_degree(face[hij]) == 3); + assert(face_degree(face[hji]) == 3); + int hjk = n[hij]; + int hlj = prev[hji]; + int hki = n[hjk]; + int hil = prev[hlj]; + int vi = to[hji]; + int vj = to[hij]; + join_halfedge_endpoints(hij); + join_edge_faces(hjk); + if (opp[hlj] != hjk) + { + join_edge_faces(hlj); + assert(face_degree(face[hil]) == 3); + assert(face_degree(face[opp[hil]]) == 3); + return hki; + } + else if (is_edge_removable(hil)) + { + join_edge_faces(hil); + return hki; + } + else if (is_edge_removable(hki)) + { + join_edge_faces(hki); + return opp[hil]; + } + else + { + spdlog::error("could not collapse edge"); + assert(false); + return -1; + } +} + +bool SimplificationOverlayMesh::triangulate_face(int face_index) +{ + int hij = f2h[face_index]; + int degree = face_degree(face_index); + + if (degree == 3) + { + return (compute_polygon_face_area(face_index) > 0.); + //return true; + } + else if (degree < 3) + { + spdlog::error("cannot triangulate degree {} face", degree); + assert(false); + return false; + } + + // TODO special case for degree 4 to avoid long computation + //spdlog::debug("{} split of degree {} face", i - 2, degree); + //face_index = clip_ear(face_index); + + std::vector face_halfedges(degree); + std::vector face_vertices(degree); + int h = hij; + for (int i = 0; i < degree; ++i) + { + face_halfedges[i] = h; + face_vertices[i] = project_halfedge_tip(h); + h = n[h]; + } + + // special case for degree 4; just try both options + if (degree == 4) + { + std::array areas; + for (int i = 0; i < 4; ++i) + { + areas[(i + 1) % 4] = compute_triangle_area( + face_vertices[i], + face_vertices[(i + 1) % 4], + face_vertices[(i + 2) % 4]); + } + + Scalar max_area; + if (min(areas[0], areas[2]) < min(areas[1], areas[3])) + { + split_face(face_halfedges[0], face_halfedges[2]); + max_area = min(areas[1], areas[3]); + } + else + { + split_face(face_halfedges[1], face_halfedges[3]); + max_area = min(areas[0], areas[2]); + } + + if (max_area <= 0.) + { + spdlog::debug("degenerate triangle in {}-gon with area {}", degree, max_area); + spdlog::debug("areas are {}", formatted_vector(areas)); + + //polyscope::init(); + //polyscope::registerPointCloud2D("boundary", face_vertices); + //polyscope::show(); + } + return (max_area > 0.); + } + + EarClipper ear_clipper(face_vertices); + ear_clipper.triangulate(); + //if (degree > 4) ear_clipper.view(); + + Scalar min_area = 1e10; + for (int i = 0; i < degree - 2; ++i) + { + auto [vi, vj, vk] = ear_clipper.triangles[i]; + Scalar area = compute_triangle_area(face_vertices[vi], face_vertices[vj], face_vertices[vk]); + min_area = min(min_area, area); + } + if (min_area <= 0.) + { + spdlog::debug("degenerate triangle in {}-gon with area {}", degree, min_area); + bool view_degenerate_polygons = false; + if (view_degenerate_polygons) ear_clipper.view(); + } + + int num_edges = ear_clipper.edges.size(); + if (num_edges != (degree - 3)) ear_clipper.view(); + assert(num_edges == (degree - 3)); + for (int i = 0; i < num_edges; ++i) + { + int vi = ear_clipper.edges[i][0]; + int vj = ear_clipper.edges[i][1]; + int h = face_halfedges[vi]; + int g = face_halfedges[vj]; + split_face(h, g); + + // update halfedge to be in same face + // WARNING: depends on ordering of edge vertices + face_halfedges[vj] = opp[n[g]]; + } + + // TODO: replace area with angles + return (min_area > 0.); +} + + +int SimplificationOverlayMesh::add_center_to_face(int face_index) +{ + int vi = HalfedgeMesh::add_center_to_face(face_index); + add_center_to_face_overlay(vi, face_index, orig); + add_center_to_face_overlay(vi, face_index, simp); + + return vi; +} + +void SimplificationOverlayMesh::remove_path(int start_halfedge, OverlayStructure& overlay) +{ + // set edge and tip vertices to face interior until reach end vertex + int hij = start_halfedge; + while (overlay.vertex_type[to[hij]] != VertexType::VV) + { + assert(overlay.vertex_type[to[hij]] == VertexType::VE); + int hjk = get_next_overlay_edge(hij, overlay); + overlay.edge_type[h2e[hij]] = EdgeType::EF; + overlay.vertex_type[to[hij]] = VertexType::VF; + hij = hjk; + } + + // set last edge type + overlay.edge_type[h2e[hij]] = EdgeType::EF; +} + +void SimplificationOverlayMesh::view() const +{ + add_mesh_to_viewer(V_orig, vtx_reindex_orig); + add_original_to_viewer(V_orig, vtx_reindex_orig); + add_simplified_vertices_to_viewer(V_orig, vtx_reindex_orig); + add_simplified_edges_to_viewer(V_orig, vtx_reindex_orig); + + //add_simplified_to_viewer(V, vtx_reindex); +#ifdef ENABLE_VISUALIZATION + polyscope::show(); +#endif +} + + +int SimplificationOverlayMesh::create_face() +{ + int f = HalfedgeMesh::create_face(); + orig.face_parent.push_back(-1); + simp.face_parent.push_back(-1); + + return f; +} + +int SimplificationOverlayMesh::create_edge() +{ + int e = HalfedgeMesh::create_edge(); + orig.halfedge_parent.push_back(-1); + orig.halfedge_parent.push_back(-1); + orig.edge_type.push_back(EdgeType::EF); + + simp.halfedge_parent.push_back(-1); + simp.halfedge_parent.push_back(-1); + simp.edge_type.push_back(EdgeType::EF); + + return e; +} + +int SimplificationOverlayMesh::create_vertex() +{ + int v = HalfedgeMesh::create_vertex(); + orig.vertex_parent.push_back(-1); + orig.vertex_type.push_back(VertexType::VF); + orig.bary_coords.push_back({0., 0.}); + + simp.vertex_parent.push_back(-1); + simp.vertex_type.push_back(VertexType::VF); + simp.bary_coords.push_back({0., 0.}); + + return v; +} + +int SimplificationOverlayMesh::split_edge(int halfedge_index) +{ + int edge_index = h2e[halfedge_index]; + + spdlog::debug("splitting edge {} = ({},{})", edge_index, to[opp[halfedge_index]], to[halfedge_index]); + int hij = HalfedgeMesh::split_edge(halfedge_index); + + // TODO: need to be careful with face parent; probably overwritten + split_edge_overlay(hij, halfedge_index, edge_index, orig); + split_edge_overlay(hij, halfedge_index, edge_index, simp); + + return hij; +} + +int SimplificationOverlayMesh::split_face(int h, int g) +{ + int face_index = face[h]; + + spdlog::debug("splitting face {} between {} and {}", face[h], to[h], to[g]); + int h_split = HalfedgeMesh::split_face(h, g); + int e = h2e[h_split]; + + split_face_overlay(e, face_index, orig); + split_face_overlay(e, face_index, simp); + + return h_split; +} + +bool SimplificationOverlayMesh::is_valid_overlay(const OverlayStructure& overlay) const +{ + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // check if face halfedge label is consistent + int hji = opp[hij]; + int eij = h2e[hij]; + int f = face[hij]; + int vj = to[hij]; + if (overlay.edge_type[eij] == EdgeType::EF) + { + int F = overlay.face_parent[f]; + if (overlay.halfedge_parent[hij] != F) return errmess("interior halfedge parent inconsistent"); + if (overlay.halfedge_parent[hji] != F) return errmess("interior halfedge parent inconsistent"); + } + else if (overlay.edge_type[eij] == EdgeType::EE) + { + int F = overlay.face_parent[f]; + int Hij = overlay.halfedge_parent[hij]; + int Hji = overlay.halfedge_parent[hji]; + if (overlay.m.opp[Hij] != Hji) return errmess("halfedge opp inconsistent with overlay mesh"); + if (overlay.m.f[Hij] != F) return errmess("halfedge face inconsistent with overlay mesh"); + + // check if edge halfedge tip label is consistent + if (overlay.vertex_type[vj] == VertexType::VE) + { + int Hij = overlay.halfedge_parent[hij]; + if (overlay.vertex_parent[vj] != overlay.h2e[Hij]) return errmess("edge vertex parent inconsistent"); + } + else if (overlay.vertex_type[vj] == VertexType::VV) + { + int Hij = overlay.halfedge_parent[hij]; + int Vj = overlay.vertex_parent[vj]; + if (overlay.m.to[Hij] != Vj) return errmess("halfedge tip inconsistent with overlay mesh"); + } + else + { + assert(false); + return errmess("edge edge tip should not be in a face"); + } + } + } + + for (int vi = 0; vi < out.size(); ++vi) + { + if (is_vertex_deleted(vi)) continue; + + // check if face vertex label is consistent + if (overlay.vertex_type[vi] == VertexType::VF) + { + int f = face[out[vi]]; + if (overlay.vertex_parent[vi] != overlay.face_parent[f]) return errmess("face vertex parent inconsistent"); + } + } + + return true; +} + +bool SimplificationOverlayMesh::is_valid_edge_network() const +{ + if (!HalfedgeMesh::is_valid()) return false; + if (!is_valid_overlay(orig)) return errmess("original overlay not valid"); + + int simp_vertex_count = 0; + for (int vi = 0; vi < out.size(); ++vi) + { + if (is_vertex_deleted(vi)) continue; + if (simp.vertex_type[vi] == VertexType::VV) ++simp_vertex_count; + } + + std::vector start_halfedge = find_start_halfedges(simp); + int num_vertices = simp.m.out.size(); + if (simp_vertex_count != num_vertices) return errmess("isolated vertex found"); + for (int vi = 0; vi < num_vertices; ++vi) + { + int Hij = simp.m.out[vi]; + int Hik = Hij; + do + { + int Hil = simp.m.n[simp.m.opp[Hik]]; + + // check consistency on mesh surface + int hik = start_halfedge[Hik]; + int hil = start_halfedge[Hil]; + if (get_clockwise_overlay_edge(hik, simp) != hil) return errmess("edge orientation not consistent"); + + // iterate + Hik = Hil; + } while (Hik != Hij); + } + + for (int hij = 0; hij < n.size(); ++hij) + { + if (is_halfedge_deleted(hij)) continue; + + // check if face halfedge label is consistent + int hji = opp[hij]; + int eij = h2e[hij]; + int vj = to[hij]; + if (simp.edge_type[eij] == EdgeType::EE) + { + int Hij = simp.halfedge_parent[hij]; + int Hji = simp.halfedge_parent[hji]; + if (simp.m.opp[Hij] != Hji) return errmess("halfedge opp inconsistent with overlay mesh"); + + + // check if edge halfedge tip label is consistent + if (simp.vertex_type[vj] == VertexType::VE) + { + int Hij = simp.halfedge_parent[hij]; + if (simp.vertex_parent[vj] != simp.h2e[Hij]) return errmess("edge vertex parent inconsistent"); + + int hjk = get_next_overlay_edge(hij, simp); + if (simp.halfedge_parent[hjk] != Hij) return errmess("next overlay edge is inconsistent"); + } + else if (simp.vertex_type[vj] == VertexType::VV) + { + int Hij = simp.halfedge_parent[hij]; + int Vj = simp.vertex_parent[vj]; + if (simp.m.to[Hij] != Vj) return errmess("halfedge tip inconsistent with overlay mesh"); + } + else + { + assert(false); + return errmess("edge edge tip should not be in a face"); + } + } + } + + return true; +} + +bool SimplificationOverlayMesh::is_valid() const +{ + if (!HalfedgeMesh::is_valid()) return false; + if (!is_valid_overlay(orig)) return errmess("original overlay not valid"); + if (!is_valid_overlay(simp)) return errmess("simplified overlay not valid"); + + // TODO: make member function of OverlayStructure + //if (face_parent.size() != f2h.size()) return errmess("face parent size is inconsistent"); + //if (edge_parent.size() != e2h.size()) return errmess("edge parent size is inconsistent"); + //if (edge_type.size() != e2h.size()) return errmess("edge type size is inconsistent"); + //if (vertex_parent.size() != out.size()) return errmess("vertex parent size is inconsistent"); + //if (vertex_type.size() != out.size()) return errmess("vertex type size is inconsistent"); + //if (bary_coords.size() != out.size()) return errmess("vertex type size is inconsistent"); + + return true; +} + +bool SimplificationOverlayMesh::is_valid_simplified_vertex_index(int vertex_index) const +{ + return (vertex_index >= 0) && (vertex_index < simp.m.n_vertices()); +} +bool SimplificationOverlayMesh::is_valid_simplified_edge_index(int edge_index) const +{ + return (edge_index >= 0) && (edge_index < simp.m.n_edges()); +} +bool SimplificationOverlayMesh::is_valid_simplified_face_index(int face_index) const +{ + return (face_index >= 0) && (face_index < simp.m.n_faces()); +} + +bool SimplificationOverlayMesh::is_valid_original_vertex_index(int vertex_index) const +{ + return (vertex_index >= 0) && (vertex_index < orig.m.n_vertices()); +} +bool SimplificationOverlayMesh::is_valid_original_edge_index(int edge_index) const +{ + return (edge_index >= 0) && (edge_index < orig.m.n_edges()); +} +bool SimplificationOverlayMesh::is_valid_original_face_index(int face_index) const +{ + return (face_index >= 0) && (face_index < orig.m.n_faces()); +} +bool SimplificationOverlayMesh::is_valid_vertex_index(int vertex_index) const +{ + return (vertex_index >= 0) && (vertex_index < out.size()); +} +bool SimplificationOverlayMesh::is_valid_halfedge_index(int halfedge_index) const +{ + return (halfedge_index >= 0) && (halfedge_index < n.size()); +} + +int SimplificationOverlayMesh::enumerate_vertex_type(const VertexType& vertex_type) const +{ + switch (vertex_type) { + case VertexType::VV: + return 0; + case VertexType::VE: + return 1; + case VertexType::VF: + return 2; + } + + return -1; +} + +std::vector SimplificationOverlayMesh::enumerate_vertex_type(const std::vector& vertex_type) const +{ + int n = vertex_type.size(); + std::vector vertex_enum(n); + for (int i = 0; i < n; ++i) + { + vertex_enum[i] = enumerate_vertex_type(vertex_type[i]); + } + + return vertex_enum; +} + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::OverlayStructure::vertex_edge_coordinates( + int vertex_index, + int edge_index) const +{ + int hij = e2h[edge_index]; + int vi = m.to[m.opp[hij]]; + int vj = m.to[hij]; + if (vertex_type[vertex_index] == VertexType::VV) + { + if (vertex_parent[vertex_index] == vi) return {1., 0.}; + if (vertex_parent[vertex_index] == vj) return {0., 0.}; + } + else if (vertex_type[vertex_index] == VertexType::VE) + { + assert (vertex_parent[vertex_index] == edge_index); + return bary_coords[vertex_index]; + } + + spdlog::error("vertex not adjacent to edge"); + assert(false); + return {0., 0.}; +} + + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::OverlayStructure::vertex_face_coordinates( + int vertex_index, + int face_index) const +{ + int hij = m.h[face_index]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + int vi = m.to[hki]; + int vj = m.to[hij]; + int vk = m.to[hjk]; + if (vertex_type[vertex_index] == VertexType::VV) + { + if (vertex_parent[vertex_index] == vi) return {1., 0.}; + if (vertex_parent[vertex_index] == vj) return {0., 1.}; + if (vertex_parent[vertex_index] == vk) return {0., 0.}; + } + else if (vertex_type[vertex_index] == VertexType::VE) + { + int eij = vertex_parent[vertex_index]; + BaryCoords edge_coords = bary_coords[vertex_index]; + return convert_edge_to_face_coordinates(eij, face_index, edge_coords); + } + else if (vertex_type[vertex_index] == VertexType::VF) + { + assert (vertex_parent[vertex_index] == face_index); + return bary_coords[vertex_index]; + } + + spdlog::error("vertex not adjacent to face"); + assert(false); + return {0., 0.}; +} + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::OverlayStructure::convert_halfedge_to_face_coordinates( + int halfedge_index, + int face_index, + BaryCoords edge_coords) const +{ + int hij = m.h[face_index]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + + Scalar s = edge_coords[0]; // second coord superfluous + Scalar t = 1. - s; + int h = halfedge_index; + if (halfedge_index == hij) + { + if (h == e2h[h2e[h]]) return {s, t}; + else return {t, s}; + } + else if (halfedge_index == hjk) + { + if (h == e2h[h2e[h]]) return {0., s}; + else return {0., t}; + } + else if (halfedge_index == hki) + { + if (h == e2h[h2e[h]]) return {t, 0.}; + else return {s, 0.}; + } + + spdlog::error("halfedge not adjacent to face"); + assert(false); + return {0., 0.}; +} + +SimplificationOverlayMesh::BaryCoords +SimplificationOverlayMesh::OverlayStructure::convert_edge_to_face_coordinates( + int edge_index, + int face_index, + BaryCoords edge_coords) const +{ + int hij = m.h[face_index]; + int hjk = m.n[hij]; + int hki = m.n[hjk]; + + int vi = m.to[hki]; + int vj = m.to[hij]; + int vk = m.to[hjk]; + + Scalar s = edge_coords[0]; // second coord superfluous + Scalar t = 1. - s; + int h = e2h[edge_index]; + if (edge_index == h2e[hij]) + { + if (m.to[h] == vj) return {s, t}; + else return {t, s}; + } + else if (edge_index == h2e[hjk]) + { + if (m.to[h] == vk) return {0., s}; + else return {0., t}; + } + else if (edge_index == h2e[hki]) + { + if (m.to[h] == vi) return {t, 0.}; + else return {s, 0.}; + } + + spdlog::error("edge not adjacent to face"); + assert(false); + return {0., 0.}; +} + +void SimplificationOverlayMesh::split_edge_overlay( + int hij, + int halfedge_index, + int edge_index, + OverlayStructure& overlay) +{ + EdgeType e_type = overlay.edge_type[edge_index]; + int e_parent = overlay.halfedge_parent[halfedge_index]; + overlay.edge_type[h2e[hij]] = e_type; + overlay.edge_type[h2e[n[hij]]] = e_type; + overlay.halfedge_parent[hij] = e_parent; + overlay.halfedge_parent[n[hij]] = e_parent; + // if part of an edge, set opposite halfedge parent to parent opposite halfedge + if (e_type == EdgeType::EE) + { + overlay.halfedge_parent[opp[hij]] = overlay.m.opp[e_parent]; + overlay.halfedge_parent[opp[n[hij]]] = overlay.m.opp[e_parent]; + } + // if part of a face, just propogate the face label + else + { + overlay.halfedge_parent[opp[hij]] = e_parent; + overlay.halfedge_parent[opp[n[hij]]] = e_parent; + } + + // set vertex parent + int vj = to[hij]; + int vi = to[opp[hij]]; + int vk = to[n[hij]]; + if (e_type == EdgeType::EE) + { + overlay.vertex_type[vj] = VertexType::VE; + overlay.vertex_parent[vj] = overlay.h2e[e_parent]; // actually use edge, not halfedge + } + else + { + overlay.vertex_type[vj] = VertexType::VF; + overlay.vertex_parent[vj] = e_parent; + } + + if (overlay.is_bary_init) + { + std::array adjacent_verts = {vi, vk}; + std::array adjacent_coords; + for (int i : {0, 1}) + { + int v = adjacent_verts[i]; + if ((e_type == EdgeType::EE) && (overlay.vertex_type[v] == VertexType::VV)) + { + adjacent_coords[i] = overlay.vertex_edge_coordinates(v, overlay.h2e[e_parent]); + } + else if ((e_type == EdgeType::EF) && (overlay.vertex_type[v] == VertexType::VV)) + { + adjacent_coords[i] = overlay.vertex_face_coordinates(v, e_parent); + } + else if ((e_type == EdgeType::EF) && (overlay.vertex_type[v] == VertexType::VE)) + { + adjacent_coords[i] = overlay.convert_edge_to_face_coordinates( + overlay.vertex_parent[v], + e_parent, + overlay.bary_coords[v] + ); + } + else + { + adjacent_coords[i] = overlay.bary_coords[v]; + } + } + + overlay.bary_coords[vj] = (adjacent_coords[0] + adjacent_coords[1]) / 2.; + } +} + +int SimplificationOverlayMesh::split_face_overlay( + int e, + int face_index, + OverlayStructure& overlay) +{ + int f_parent = overlay.face_parent[face_index]; + overlay.edge_type[e] = EdgeType::EF; + int hij = e2h[e]; + int hji = opp[hij]; + overlay.halfedge_parent[hij] = f_parent; + overlay.halfedge_parent[hji] = f_parent; + overlay.face_parent[face[hij]] = f_parent; + overlay.face_parent[face[hji]] = f_parent; + + return e; +} + +void SimplificationOverlayMesh::join_edge_faces_overlay( + int f, + int halfedge_index, + OverlayStructure& overlay) +{ + int f_parent = overlay.halfedge_parent[halfedge_index]; + overlay.face_parent[f] = f_parent; +} + +void SimplificationOverlayMesh::collapse_edge_overlay( + int hij, + int halfedge_index, + OverlayStructure& overlay) +{ + + overlay.edge_type[h2e[hij]] = overlay.edge_type[h2e[halfedge_index]]; + overlay.halfedge_parent[hij] = overlay.halfedge_parent[opp[halfedge_index]]; + overlay.halfedge_parent[opp[hij]] = overlay.halfedge_parent[halfedge_index]; +} + +void SimplificationOverlayMesh::add_center_to_face_overlay(int vi, int face_index, OverlayStructure& overlay) +{ + int f_parent = overlay.face_parent[face_index]; + overlay.vertex_type[vi] = VertexType::VF; + overlay.vertex_parent[vi] = f_parent; + int hij = out[vi]; + int hik = hij; + std::vector adjacent_verts; + do + { + int eik = h2e[hik]; + overlay.edge_type[eik] = EdgeType::EF; + overlay.halfedge_parent[hik] = f_parent; + overlay.halfedge_parent[opp[hik]] = f_parent; + overlay.face_parent[face[hik]] = f_parent; + adjacent_verts.push_back(to[hik]); + hik = opp[prev[hik]]; + } + while (hik != hij); + + if (overlay.is_bary_init) + { + int degree = adjacent_verts.size(); + overlay.bary_coords[vi] = {0., 0.}; + for (int i = 0; i < degree; ++i) + { + int v = adjacent_verts[i]; + if (overlay.vertex_type[v] == VertexType::VV) + { + overlay.bary_coords[vi] += overlay.vertex_face_coordinates(v, f_parent); + } + else if (overlay.vertex_type[v] == VertexType::VE) + { + overlay.bary_coords[vi] += overlay.convert_edge_to_face_coordinates( + overlay.vertex_parent[v], + f_parent, + overlay.bary_coords[v] + ); + } + else + { + overlay.bary_coords[vi] += overlay.bary_coords[v]; + } + } + + overlay.bary_coords[vi] /= degree; + } +} + + +void SimplificationOverlayMesh::add_simplified_edges_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + int num_edges = e2h.size(); + std::vector> edges; + std::vector parent; + edges.reserve(count_edges()); + for (int eij = 0; eij < num_edges; ++eij) + { + // only add edges contained in simpified mesh edges + if (is_edge_deleted(eij)) continue; + if (simp.edge_type[eij] == EdgeType::EF) continue; + + int hij = e2h[eij]; + int vi = v_old2new[to[opp[hij]]]; + int vj = v_old2new[to[hij]]; + edges.push_back({vi, vj}); + parent.push_back(simp.halfedge_parent[e2h[eij]]); // TODO: get edge in original mesh + } + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + auto [V_s, edges_s] = remove_unreferenced_vertices(V_o, edges); + + std::vector node_parent(V_s.rows(), -1); + for (int i = 0; i < edges.size(); ++i) + { + auto [vi, vj] = edges[i]; + auto [wi, wj] = edges_s[i]; + node_parent[wi] = simp.vertex_parent[v_new2old[vi]]; + node_parent[wj] = simp.vertex_parent[v_new2old[vj]]; + } +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + std::string mesh_handle; + mesh_handle = "simplified edges"; + polyscope::registerCurveNetwork(mesh_handle, V_s, edges_s) + ->setColor(BLACK) + ->setRadius(0.0005); + polyscope::getCurveNetwork(mesh_handle)->addEdgeScalarQuantity("edge parent", parent); + polyscope::getCurveNetwork(mesh_handle)->addNodeScalarQuantity("node parent", node_parent); +#endif +} + +void SimplificationOverlayMesh::add_simplified_vertices_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + std::vector vertices = {}; + std::vector parent; + vertices.reserve(count_vertices()); + int num_vertices = out.size(); + for (int vi = 0; vi < num_vertices; ++vi) + { + // only add simplified vertices + if (is_vertex_deleted(vi)) continue; + if (simp.vertex_type[vi] != VertexType::VV) continue; + + vertices.push_back(v_old2new[vi]); + parent.push_back(simp.vertex_parent[vi]); + } + + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + Eigen::MatrixXd V_s = subset_vertices(V_o, vertices); + + + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + std::string mesh_handle; + mesh_handle = "simplified vertices"; + polyscope::registerPointCloud(mesh_handle, V_s) + ->setPointColor(BLACK) + ->setPointRadius(0.001); + polyscope::getPointCloud(mesh_handle)->addScalarQuantity("parent", parent); +#endif +} + +void SimplificationOverlayMesh::add_overlay_to_viewer( + const std::string& mesh_handle, + const OverlayStructure& overlay, + const std::string& prefix +) const { + auto [f_new2old, f_old2new] = get_face_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + std::vector vertex_type = enumerate_vertex_type(overlay.vertex_type); + std::vector vertex_type_r = vector_compose(vertex_type, v_new2old); + std::vector face_parent_r = vector_compose(overlay.face_parent, f_new2old); + std::vector shuffled_face_parent = shuffle_map_image(face_parent_r); + std::vector u_coord, v_coord; + for (int vi = 0; vi < out.size(); ++vi) + { + if (is_vertex_deleted(vi)) continue; + u_coord.push_back(overlay.bary_coords[vi][0]); + v_coord.push_back(overlay.bary_coords[vi][1]); + } + +#ifdef ENABLE_VISUALIZATION + polyscope::getSurfaceMesh(mesh_handle)->addFaceScalarQuantity(prefix + " face parent", face_parent_r); + polyscope::getSurfaceMesh(mesh_handle)->addFaceScalarQuantity(prefix + " shuffled face parent", shuffled_face_parent); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity(prefix + " vertex type", vertex_type_r); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity(prefix + " u coord", u_coord); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity(prefix + " v coord", v_coord); +#endif +} + +void SimplificationOverlayMesh::add_original_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [f_new2old, f_old2new] = get_face_reductions(); + std::vector to_reindex = vector_compose(vtx_reindex, orig.m.to); + auto [F, F_halfedge] = generate_connectivity_faces(orig.m.n, orig.m.h, to_reindex); + + std::vector face_areas = compute_face_areas(); + std::vector min_area(F.rows(), 1e10); + for (int f = 0; f < f2h.size(); ++f) + { + if (is_face_deleted(f)) continue; + int Fi = orig.face_parent[f]; + min_area[Fi] = min(face_areas[f_old2new[f]], min_area[Fi]); + } + + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + std::string mesh_handle; + mesh_handle = "original mesh"; + polyscope::registerSurfaceMesh(mesh_handle, V, F); + //polyscope::getSurfaceMesh(mesh_handle)->addFaceVectorQuantity("basis 1", B1); + //polyscope::getSurfaceMesh(mesh_handle)->addFaceVectorQuantity("basis 2", B2); + polyscope::getSurfaceMesh(mesh_handle)->addFaceScalarQuantity("min overlay area", min_area); +#endif +} + +void SimplificationOverlayMesh::add_mesh_to_viewer( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [F, F_halfedge] = generate_overlay_faces(); + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + std::vector face_areas = compute_face_areas(); + std::vector min_area(V_o.rows(), 1e10); + std::vector degenerate = {}; + for (int f = 0; f < F.rows(); ++f) + { + for (int i = 0; i < 3; ++i) + { + int vi = F(f, i); + min_area[vi] = min(face_areas[f], min_area[vi]); + if (face_areas[f] <= 0.) degenerate.push_back(vi); + } + } + Eigen::MatrixXd V_degenerate = subset_vertices(V_o, degenerate); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + std::string mesh_handle; + mesh_handle = "overlay mesh"; + polyscope::registerSurfaceMesh(mesh_handle, V_o, F); + if (!degenerate.empty()) polyscope::registerPointCloud("degenerate", V_degenerate); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("vertex indices", v_new2old); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("min adjacent area", min_area); + add_overlay_to_viewer(mesh_handle, orig, "original"); + add_overlay_to_viewer(mesh_handle, simp, "simplified"); +#endif +} + +void SimplificationOverlayMesh::collect_garbage() +{ + if (skip_garbage_collection) return; + + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + auto [e_new2old, e_old2new] = get_edge_reductions(); + auto [f_new2old, f_old2new] = get_face_reductions(); + + orig.collect_garbage(he_new2old, v_new2old, e_new2old, f_new2old); + simp.collect_garbage(he_new2old, v_new2old, e_new2old, f_new2old); + HalfedgeMesh::collect_garbage( + he_new2old, + e_new2old, + v_new2old, + f_new2old, + he_old2new, + e_old2new, + v_old2new, + f_old2new); +} + +std::tuple>> remove_unreferenced_vertices( + const Eigen::MatrixXd& V, + const std::vector>& edges) +{ + // get remaining vertices + std::vector is_deleted(V.rows(), true); + for (const auto& edge : edges) + { + for (int v : edge) is_deleted[v] = false; + } + std::vector v_removed, v_new2old, v_old2new; + enumerate_boolean_array(is_deleted, v_removed, v_new2old, v_old2new); + + int num_edges = edges.size(); + std::vector> new_edges(num_edges); + for (int e = 0; e < num_edges; ++e) + { + for (int i : {0, 1}) + { + new_edges[e][i] = v_old2new[edges[e][i]]; + } + } + + // get vertex subset + Eigen::MatrixXd new_V = subset_vertices(V, v_new2old); + + return std::make_tuple(new_V, new_edges); +} + +// remove trivial cycle self intersections +std::vector remove_cycle_tags(const std::vector& cycle) +{ + // nothing to do for a cycle with two elements + if (cycle.size() <= 2) return cycle; + + // check if the path loops back on itself + int num_elem = cycle.size(); + std::vector is_trimmed(num_elem, false); + + // lambda to get previous untrimmed element + auto get_prev = [&](int n) + { + do + { + n = (n - 1 + num_elem) % num_elem; + } while (is_trimmed[n]); + return n; + }; + + // lambda to get next untrimmed element + auto get_next = [&](int n) + { + do + { + n = (n + 1) % num_elem; + } while (is_trimmed[n]); + return n; + }; + + // search for tags until none remain + int i = 0; + int j = 1; + int k = 2; + int count = 0; + while (count < num_elem) + { + // if tag found, remove and reset count + if (cycle[i] == cycle[k]) + { + is_trimmed[j] = true; + is_trimmed[k] = true; + + // move the window back one (to catch long tails quickly) + j = i; + i = get_prev(i); + k = get_next(k); + count = 0; + } + // otherwise, advance by one and increment counter + else + { + i = j; + j = k; + k = get_next(k); + ++count; + } + } + + // build trimmed vector + std::vector cycle_trimmed = {}; + cycle_trimmed.reserve(num_elem); + for (int i = 0; i < num_elem; ++i) + { + if (!is_trimmed[i]) + { + cycle_trimmed.push_back(cycle[i]); + } + } + + return cycle_trimmed; +} + +// remove trivial cycle self intersections +std::vector remove_one_cycle_tag(const std::vector& cycle) +{ + // nothing to do for a cycle with two elements + if (cycle.size() <= 2) return cycle; + + // look for a tag + int num_elem = cycle.size(); + int tag = -1; + for (int i = 0; i < num_elem; ++i) + { + int k = (i + num_elem - 1) % num_elem; + int j = (i + 1) % num_elem; + int prev = cycle[k]; + int next = cycle[j]; + if (prev == next) + { + tag = i; + break; + } + } + + if (tag < 0) return cycle; + + // remove tag from cycle + std::vector cycle_trimmed = {}; + cycle_trimmed.reserve(num_elem); + for (int i = 0; i < num_elem - 2; ++i) + { + int j = (i + 1 + tag) % num_elem; + cycle_trimmed.push_back(cycle[j]); + } + + return cycle_trimmed; +} + +// given a cycle, remove redundant elements and trivial self intersections so that every three +// consecutive elements are unique +std::vector trim_cycle(const std::vector& cycle) +{ + // prepare new vector for trimmed cycle + std::vector cycle_trimmed = {}; + int num_elem = cycle.size(); + cycle_trimmed.reserve(num_elem); + + // first, remove repeated elements + int prev = cycle.back(); + for (int i = 0; i < num_elem; ++i) + { + int curr = cycle[i]; + if (curr != prev) + { + cycle_trimmed.push_back(curr); + prev = curr; + } + } + + // now remove any tags of the form A B A + return remove_cycle_tags(cycle_trimmed); +} + +std::vector compute_curvature_adaptive_lengths( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Mesh& m, + const std::vector& vtx_reindex, + Scalar normal_scale +) +{ + std::vector l = m.l; + + // compute normals for normal weights + Eigen::MatrixXd N; + igl::per_vertex_normals(V, F, N); + + // compute lengths and total length without normals term + Scalar total_length = 0.; + for (int hij = 0; hij < m.n_halfedges(); ++hij) + { + int hji = m.opp[hij]; + if (hij < hji) continue; + total_length += m.l[hij]; + } + + // add normal distances, weighted by average length + Scalar avg_length = total_length / m.n_edges(); + Scalar a = normal_scale * avg_length; + for (int hij = 0; hij < m.n_halfedges(); ++hij) + { + int hji = m.opp[hij]; + if (hij < hji) continue; + + // get edge vertex positions + int Vi = vtx_reindex[m.to[hji]]; + int Vj = vtx_reindex[m.to[hij]]; + + // get edge normals + Vector3 ni = N.row(Vi); + Vector3 nj = N.row(Vj); + + // update lengths with normals term + Scalar length = a * (ni - nj).norm(); + if (isnan(length)) spdlog::error("edge normal length is NaN"); + l[hij] += length; + l[hji] += length; + } + + return l; +} + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/hierarchy/simplifier.cpp b/src/resolution/hierarchy/simplifier.cpp new file mode 100644 index 0000000..eac8578 --- /dev/null +++ b/src/resolution/hierarchy/simplifier.cpp @@ -0,0 +1,762 @@ +#include "resolution/hierarchy/simplifier.h" +#include "resolution/core/common.h" +#include "metric/viewer.h" +#include "metric/constraint.h" +#include "parametrization/triangulation.h" +#include "util/union_find.h" +#include "util/map.h" +#include "util/vector.h" +#include "util/vf_mesh.h" +#include "holonomy/core/viewer.h" +#include "parametrization/layout.h" +#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "conformal_ideal_delaunay/Layout.hh" + +#include "resolution/core/geodesics.h" +#include "resolution/core/geodesic_flipper.h" + +#include +#include + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner +{ +namespace Resolution +{ + +void Simplifier::refine_halfedges() +{ + int num_faces = m.n_faces(); + + // get all invalid faces + std::stack invalid_faces; + for (int fijk = 0; fijk < num_faces; ++fijk) { + if (is_refined_face[fijk]) continue; + if (!is_face_self_overlapping(fijk)) invalid_faces.push(fijk); + } + spdlog::info("{} faces are invalid", invalid_faces.size()); + + // Refine faces until all are valid + int num_refined_halfedges = 0; + while (!invalid_faces.empty()) { + // Get invalid face + int fijk = invalid_faces.top(); + invalid_faces.pop(); + spdlog::trace("Processing face {}", fijk); + + // Check if face already processed to prevent an infinite loop + // NOTE: This should not happen, but may do to floating point error + // or invalid inputs + if (is_refined_face[fijk]) continue; + + int hij = m.h[fijk]; + int count = 0; + for (int i = 0; i < 3; ++i) + { + // check if current face is self overlapping and refine if not + if (is_face_self_overlapping(fijk)) break; + is_refined_halfedge[hij] = true; + is_refined_halfedge[m.opp[hij]] = true; + ++count; + ++num_refined_halfedges; + + // check if opposite face is self overlapping + int fjil = m.f[m.opp[hij]]; + if (!is_face_self_overlapping(fjil)) invalid_faces.push(fjil); + + // iterate around face + hij = m.n[hij]; + } + + // mark the face as refined if all halfedges are refined + // TODO: make just a function + is_refined_face[fijk] = ((count == 3) && (!is_face_self_overlapping(fijk))); + } + + spdlog::info("{} halfedges were refined", num_refined_halfedges); +} + +void Simplifier::simplify_vertices() +{ + std::vector refined_he, unrefined_he, he_map; + enumerate_boolean_array(is_refined_halfedge, refined_he, unrefined_he, he_map); + int count = 0; + + // Iterate until no vertices can be removed + bool vertex_removed = false; + int max_iterations = 1; + int iter = 0; + do { + // Loop over all inserted vertices to see if one can be removed + vertex_removed = false; + for (int hij : refined_he) + { + // get edge halfedges + int f = m.f[hij]; + int fo = m.f[m.opp[hij]]; + std::vector edge_vertices = get_halfedge_vertices(hij); + int size = edge_vertices.size(); + for (int i = 0; i < size - 1; ++i) + { + int v = edge_vertices[i]; + if (is_removed_vertex[v]) continue; + + // try to remove vertex, and reset if not + is_removed_vertex[v] = true; + if ((is_face_self_overlapping(f)) && (is_face_self_overlapping(fo))) + { + vertex_removed = true; + ++count; + } + else + { + is_removed_vertex[v] = false; + } + } + } + spdlog::info("{} vertices were removed from refinement", count); + + ++iter; + if (iter >= max_iterations) break; + } while (vertex_removed); + +} + +std::vector Simplifier::get_halfedge_vertices(int halfedge_index) const +{ + std::vector halfedges = get_halfedges(halfedge_index); + return vector_compose(to, halfedges); +} + +void Simplifier::get_face_vertex_indices( + int face_index, + std::vector& uv_vertices, + std::vector& vertices) const +{ + int hij = m.h[face_index]; + + for (int i = 0; i < 3; ++i) + { + // add all vertices on edge if refined + if (is_refined_halfedge[hij]) + { + std::vector edge_halfedges = get_halfedges(hij); + for (int h : edge_halfedges) + { + int v = to[h]; + if (is_removed_vertex[v]) continue; + vertices.push_back(v); + uv_vertices.push_back(h); + } + } + // add the uv coordiante for just the tip vertex if not refined + else + { + int h = end_halfedge[hij]; + int v = to[h]; + vertices.push_back(v); + uv_vertices.push_back(h); + } + + // increment halfedge + hij = m.n[hij]; + } + +} + +std::vector SimplificationOverlaySimplifier::get_halfedges(int halfedge_index) const +{ + return parent.enumerate_halfedges(start_halfedge[halfedge_index], parent.orig); +} + +bool SimplificationOverlaySimplifier::is_face_self_overlapping(int face_index) const +{ + if (is_refined_face[face_index]) return false; + + // get vertices for the face + std::vector uv_vertices = {}; + std::vector vertices = {}; + get_face_vertices(face_index, uv_vertices, vertices); + + // Determine if the face is self overlapping + std::vector> is_self_overlapping_subpolygon; + std::vector> splitting_vertices; + std::vector> min_face_areas; + return is_self_overlapping_polygon( + uv_vertices, + vertices, + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + area_threshold, + use_angles); +} + +void SimplificationOverlaySimplifier::get_face_vertices( + int face_index, + std::vector& uv_vertices, + std::vector& vertices) const +{ + std::vector uv_vertex_indices, vertex_indices; + get_face_vertex_indices(face_index, uv_vertex_indices, vertex_indices); + + int num_vertices = vertex_indices.size(); + uv_vertices.resize(num_vertices); + vertices.resize(num_vertices); + for (int i = 0; i < num_vertices; ++i) + { + int h = uv_vertex_indices[i]; + int v = vertex_indices[i]; + uv_vertices[i] = he_uv[h]; + vertices[i] = V_o.row(v); + } +} + +bool SimplificationOverlaySimplifier::triangulate_face( + int face_index, + std::vector>& face_triangles, + std::vector>& uv_face_triangles, + std::vector>& face_indices) const +{ + // Get uv vertices of the face + std::vector vertex_indices; + std::vector uv_vertex_indices; + std::vector uv_vertices; + std::vector vertices; + get_face_vertex_indices(face_index, uv_vertex_indices, vertex_indices); + get_face_vertices(face_index, uv_vertices, vertices); + + // Determine if the face is self overlapping + std::vector> is_self_overlapping_subpolygon; + std::vector> splitting_vertices; + std::vector> min_face_areas; + bool is_self_overlapping = is_self_overlapping_polygon( + uv_vertices, + vertices, + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + area_threshold, + use_angles); + + // Return false if the face is not self overlapping + if (!is_self_overlapping) { + //spdlog::warn("Face {} is not self overlapping", face_index); + return false; + } + + // Triangulate the face if it can be triangulated + std::vector> polygon_faces; + triangulate_self_overlapping_polygon( + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + polygon_faces); + + // Convert polygon face indices to global indices for V and uv + int num_polygon_faces = polygon_faces.size(); + for (int fi = 0; fi < num_polygon_faces; ++fi) { + face_triangles.push_back({}); + uv_face_triangles.push_back({}); + face_indices.push_back({}); + for (int j = 0; j < 3; ++j) { + face_triangles.back()[j] = vertex_indices[polygon_faces[fi][j]]; + uv_face_triangles.back()[j] = uv_vertex_indices[polygon_faces[fi][j]]; + face_indices.back()[j] = polygon_faces[fi][j]; + } + } + + return true; +} + +std::tuple< +Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::MatrixXi, +std::vector, Eigen::MatrixXi, Eigen::MatrixXi +> +SimplificationOverlaySimplifier::build_refined_mesh() +{ + int num_faces = m.n_faces(); + std::vector> face_triangles; + std::vector> uv_face_triangles; + std::vector> face_indices; + std::vector> is_triangle_edge; + + // get uv vertices + auto [h_new2old, h_old2new] = parent.get_halfedge_reductions(); + UnionFind cut_vertices(h_new2old.size()); + for (int hij = 0; hij < parent.n.size(); ++hij) + { + if (parent.is_halfedge_deleted(hij)) continue; + + // union halfedges that are not on the layout cut + if (parent.orig.edge_type[parent.h2e[hij]] == EdgeType::EE) + { + int h_o = parent.orig.halfedge_parent[hij]; + if (is_cut_orig[h_o]) + continue; + } + + // get halfedge in adjacent uncut face pointing to vj + int hkj = parent.prev[parent.opp[hij]]; + cut_vertices.union_sets(h_old2new[hij], h_old2new[hkj]); + } + int num_cut_vertices = cut_vertices.count_sets(); + std::vector cut_vertex_indices = cut_vertices.index_sets(); + + // triangulate faces that are not completely refined + std::vector face_parent = {}; + for (int f = 0; f < num_faces; ++f) + { + if (is_refined_face[f]) continue; + is_refined_face[f] = !(triangulate_face(f, face_triangles, uv_face_triangles, face_indices)); + int curr_faces = face_parent.size(); + int size = face_triangles.size() - curr_faces + 2; + for (int i = curr_faces; i < face_triangles.size(); ++i) + { + face_parent.push_back(f); + is_triangle_edge.push_back({}); + for (int j = 0; j < 3; ++j) + { + if (face_indices[i][j] == (face_indices[i][(j + 2) % 3] + 1) % size) + { + is_triangle_edge.back()[(j + 1) % 3] = true; + } + else + { + is_triangle_edge.back()[(j + 1) % 3] = false; + } + } + } + } + + // add all refined faces from original mesh + for (int f = 0; f < parent.f2h.size(); ++f) + { + if (parent.is_face_deleted(f)) continue; + if (!is_refined_face[parent.orig.face_parent[f]]) continue; + int hij = parent.f2h[f]; + face_parent.push_back(parent.orig.face_parent[f]); + is_triangle_edge.push_back({}); + face_triangles.push_back({}); + uv_face_triangles.push_back({}); + face_indices.push_back({}); + for (int i = 0; i < 3; ++i) + { + face_triangles.back()[i] = parent.to[parent.n[hij]]; + uv_face_triangles.back()[i] = parent.n[hij]; + if (parent.orig.edge_type[parent.h2e[hij]] == EdgeType::EE) + { + is_triangle_edge.back()[i] = true; + } + hij = parent.n[hij]; + } + } + + // build eigen matrices + int num_refined_faces = face_triangles.size(); + Eigen::MatrixXd uv_o(num_cut_vertices, 2); + Eigen::MatrixXi F_o(num_refined_faces, 3); + Eigen::MatrixXi F_uv_o(num_refined_faces, 3); + Eigen::MatrixXi halfedge_parent(num_refined_faces, 3); + Eigen::MatrixXi is_edge = Eigen::MatrixXi::Zero(num_refined_faces, 3); + for (int f = 0; f < num_refined_faces; ++f) + { + for (int i = 0; i < 3; ++i) + { + int j = (i + 1) % 3; + int k = (i + 2) % 3; + int hij = uv_face_triangles[f][i]; + int hjk = uv_face_triangles[f][j]; + int hki = uv_face_triangles[f][k]; + //int hki = parent.orig.m.n[parent.orig.m.n[hij]]; + int uvi = cut_vertex_indices[h_old2new[hij]]; + uv_o.row(uvi) = he_uv[hij]; + F_o(f, i) = face_triangles[f][i]; + F_uv_o(f, i) = uvi; + + if (is_triangle_edge[f][i]) + { + is_edge(f, i) = 1; + halfedge_parent(f, i) = parent.orig.halfedge_parent[hki]; + } + } + } + + // Remove unreferenced vertices + Eigen::MatrixXd V, uv; + Eigen::MatrixXi F, F_uv; + Eigen::VectorXi I, J, I_uv, J_uv; + igl::remove_unreferenced(V_o, F_o, V, F, I, J); + igl::remove_unreferenced(uv_o, F_uv_o, uv, F_uv, I_uv, J_uv); + + return std::make_tuple(V, F, uv, F_uv, face_parent, halfedge_parent, is_edge); +} + + +void OverlaySimplifier::get_face_vertices( + int face_index, + std::vector& uv_vertices) const +{ + std::vector uv_vertex_indices, vertex_indices; + get_face_vertex_indices(face_index, uv_vertex_indices, vertex_indices); + + int num_vertices = vertex_indices.size(); + uv_vertices.resize(num_vertices); + for (int i = 0; i < num_vertices; ++i) + { + int h = uv_vertex_indices[i]; + uv_vertices[i] = he_uv[h]; + } +} + +std::vector OverlaySimplifier::get_halfedges(int halfedge_index) const +{ + int seg = start_halfedge[halfedge_index]; + std::vector overlay_halfedges = {seg}; + while (m_o.vertex_type[m_o.to[seg]] != ORIGINAL_VERTEX) + { + seg = next_orig_segment(m_o, seg); + overlay_halfedges.push_back(seg); + } + + return overlay_halfedges; +} + +bool OverlaySimplifier::is_face_self_overlapping(int face_index) const +{ + if (is_refined_face[face_index]) return false; + + // get vertices for the face + std::vector uv_vertices = {}; + get_face_vertices(face_index, uv_vertices); + + // Determine if the face is self overlapping + std::vector> is_self_overlapping_subpolygon; + std::vector> splitting_vertices; + std::vector> min_face_areas; + return is_self_overlapping_polygon( + uv_vertices, + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + area_threshold, + use_angles); +} + +bool OverlaySimplifier::triangulate_face( + int face_index, + std::vector>& face_triangles, + std::vector>& uv_face_triangles) const +{ + // Get uv vertices of the face + std::vector vertex_indices; + std::vector uv_vertex_indices; + std::vector uv_vertices; + get_face_vertex_indices(face_index, uv_vertex_indices, vertex_indices); + get_face_vertices(face_index, uv_vertices); + + // Determine if the face is self overlapping + std::vector> is_self_overlapping_subpolygon; + std::vector> splitting_vertices; + std::vector> min_face_areas; + bool is_self_overlapping = is_self_overlapping_polygon( + uv_vertices, + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + area_threshold, + use_angles); + + // Return false if the face is not self overlapping + if (!is_self_overlapping) { + //spdlog::warn("Face {} is not self overlapping", face_index); + return false; + } + + // Triangulate the face if it can be triangulated + std::vector> polygon_faces; + triangulate_self_overlapping_polygon( + is_self_overlapping_subpolygon, + splitting_vertices, + min_face_areas, + polygon_faces); + + // Convert polygon face indices to global indices for V and uv + int num_polygon_faces = polygon_faces.size(); + for (int fi = 0; fi < num_polygon_faces; ++fi) { + face_triangles.push_back({}); + uv_face_triangles.push_back({}); + for (int j = 0; j < 3; ++j) { + face_triangles.back()[j] = vertex_indices[polygon_faces[fi][j]]; + uv_face_triangles.back()[j] = uv_vertex_indices[polygon_faces[fi][j]]; + } + } + + return true; +} + +std::tuple, std::vector, std::vector> OverlaySimplifier::get_refined_mesh() +{ + std::vector fn_to_f; + build_face_maps(m_o, fn_to_f); + + std::vector> face_triangles; + std::vector> uv_face_triangles; + for (int f = 0; f < m.n_faces(); ++f) + { + if (is_refined_face[f]) continue; + is_refined_face[f] = !(triangulate_face(f, face_triangles, uv_face_triangles)); + } + + // add all refined faces from original mesh + int num_faces_refined = 0; + for (int f = 0; f < m_o.n_faces(); ++f) + { + int f_orig = fn_to_f[f]; + if (!is_refined_face[f_orig]) continue; + int hij = m_o.h[f]; + face_triangles.push_back({}); + uv_face_triangles.push_back({}); + for (int i = 0; i < 3; ++i) + { + face_triangles.back()[i] = m_o.to[hij]; + uv_face_triangles.back()[i] = hij; + hij = m_o.n[hij]; + } + num_faces_refined++; + } + spdlog::info("{} faces refined", num_faces_refined); + + int num_corners = 3 * uv_face_triangles.size(); + int num_halfedges = m_o.n_halfedges(); + std::vector halfedges(num_corners, -1); + std::vector n_r(num_corners, -1); + std::vector to_r(num_corners, -1); + std::vector f_r(num_corners, -1); + std::vector uv_r(num_corners); + Eigen::SparseMatrix vv2he(num_halfedges, num_halfedges); + std::vector fn_to_f_o(uv_face_triangles.size(), -1); + + // build sparse matrix + typedef Eigen::Triplet Trip; + std::vector trips; + for (int f = 0; f < uv_face_triangles.size(); ++f) + { + for (int i = 0; i < 3; ++i) + { + int j = (i + 1) % 3; + int k = (i + 2) % 3; + int hj = 3 * f + j; + int hk = 3 * f + k; + int vi = uv_face_triangles[f][i]; + int vj = uv_face_triangles[f][j]; + + halfedges[hj] = vj; + n_r[hj] = hk; + f_r[hj] = f; + to_r[hj] = m_o.to[vj]; + uv_r[hj] = he_uv[vj]; + fn_to_f_o[f] = fn_to_f[m_o.f[vj]]; + trips.push_back(Trip(vi, vj, hj + 1)); + } + } + vv2he.setFromTriplets(trips.begin(), trips.end()); + + // build map from original to reduced halfedges + std::vector he_map(num_halfedges, -1); + for (int hij = 0; hij < num_corners; ++hij) + { + int hjk = n_r[hij]; + int hki = n_r[hjk]; + int vi = halfedges[hki]; + int vj = halfedges[hij]; + assert(vv2he.coeff(vi, vj) - 1 == hij); + int hji = vv2he.coeff(vj, vi) - 1; + + // edge crossing faces + if (hji < 0) + { + he_map[vj] = hij; + } + } + + // build opp maps + std::vector opp_r(num_corners, -1); + std::vector is_inserted(num_corners, false); + for (int hij = 0; hij < num_corners; ++hij) + { + int hjk = n_r[hij]; + int hki = n_r[hjk]; + int vi = halfedges[hki]; + int vj = halfedges[hij]; + assert(vv2he.coeff(vi, vj) - 1 == hij); + int hji = vv2he.coeff(vj, vi) - 1; + + // edge crossing faces + if (hji < 0) + { + is_inserted[hij] = false; + int h = m_o.origin_of_origin[vj]; + if (m_o.edge_type[vj] == ORIGINAL_EDGE) + h = m_o.origin[vj]; + if (is_refined_halfedge[h]) + { + opp_r[hij] = he_map[m_o.opp[vj]]; + spdlog::debug("building refined edge {}, {} in {}, {}", hij, opp_r[hij], h, m.opp[h]); + } + else + { + opp_r[hij] = he_map[end_halfedge[m.opp[h]]]; + } + } + // face triangulation edge + else + { + spdlog::debug("building inserted edge {}, {}", hij, hji); + is_inserted[hij] = true; + opp_r[hij] = hji; + } + } + + Mesh m_r; + m_r.n = n_r; + m_r.opp = opp_r; + m_r.to = remove_unreferenced_indices(to_r); // need to remove unreferenced + m_r.f = f_r; + m_r.out = invert_map(vector_compose(m_r.to, m_r.opp)); + m_r.h = invert_map(m_r.f); + + // check validity + std::vector prev = invert_map(m_r.n); + assert(are_polygon_mesh_halfedges_valid(m_r.n, prev)); + assert(are_polygon_mesh_vertices_valid(m_r.opp, prev, m_r.to, m_r.out)); + assert(are_polygon_mesh_faces_valid(m_r.n, m_r.f, m_r.h)); + + m_r.l = std::vector(m_r.n.size(), 1.); + m_r.type = std::vector(m_r.n.size(), 0); + m_r.type_input = std::vector(m_r.n.size(), 0); + m_r.R = std::vector(m_r.n.size(), 0); + m_r.v_rep = arange(m_r.out.size()); + m_r.Th_hat = std::vector(m_r.out.size(), 2 * PI); + m_r.fixed_dof = std::vector(m_r.out.size(), false); + m_r.fixed_dof[0] = true; + + for (int hij = 0; hij < num_corners; ++hij) + { + int hjk = m_r.n[hij]; + int hki = m_r.n[hjk]; + m_r.l[hij] = (uv_r[hij] - uv_r[hki]).norm(); + } + + OverlayMesh m_r_o(m_r); + for (int hij = 0; hij < num_corners; ++hij) + { + if (is_inserted[hij]) + { + m_r_o.edge_type[hij] = CURRENT_EDGE; + } + else + { + int h = halfedges[hij]; + assert(h >= 0); + assert(h < m_o.n_halfedges()); + + // transfer edge type data from original overlay + //m_r_o.edge_type[hij] = m_o.edge_type[h]; + //m_r_o.origin_of_origin[hij] = m_o.origin_of_origin[h]; + //m_r_o.origin[hij] = m_o.origin[hij]; + + m_r_o.edge_type[hij] = ORIGINAL_EDGE; + m_r_o.origin[hij] = m_o.origin_of_origin[h]; + if (m_o.edge_type[h] == ORIGINAL_EDGE) + m_r_o.origin[hij] = m_o.origin[h]; + + // get vertex type as well + int vj = m_r_o.to[hij]; + int Vj = m_o.to[h]; + assert (Vj >= 0); + assert (Vj < m_o.n_vertices()); + m_r_o.vertex_type[vj] = m_o.vertex_type[Vj]; + assert((m_o.vertex_type[Vj] == ORIGINAL_VERTEX) || (m_o.vertex_type[Vj] == SEGMENT_VERTEX)); + if (m_r_o.vertex_type[vj] == SEGMENT_VERTEX) + { + spdlog::debug("inserted segment vertex {}, relabelled {}, on {}", Vj, vj, m_r_o.origin[hij]); + } + } + } + + return std::make_tuple(m_r_o, fn_to_f_o, uv_r); +} + +void SimplificationOverlaySimplifier::mark_cut_faces(const std::vector& is_cut_halfedge) +{ + int num_halfedges = parent.n.size(); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (parent.is_halfedge_deleted(hij)) continue; + + // check if seam edge + if (parent.simp.edge_type[parent.h2e[hij]] != EdgeType::EE) continue; + int h_o = parent.simp.halfedge_parent[hij]; + if (!is_cut_halfedge[h_o]) continue; + + // check if interior edge + if (parent.orig.edge_type[parent.h2e[hij]] != EdgeType::EF) continue; + int f = parent.orig.face_parent[parent.face[hij]]; + + is_refined_face[f] = true; + } + + // mark halfedges of refined faces as refined + for (int f = 0; f < m.n_faces(); ++f) + { + if (!is_refined_face[f]) continue; + + int hij = m.h[f]; + for (int i = 0; i < 3; ++i) + { + is_refined_halfedge[hij] = true; + is_refined_halfedge[m.opp[hij]] = true; + hij = m.n[hij]; + } + } +} + +void SimplificationOverlaySimplifier::view_parameterization() +{ + int num_faces = m.n_faces(); + int num_halfedges = m.n_halfedges(); + Eigen::MatrixXi F(num_faces, 3); + Eigen::MatrixXi F_uv(num_faces, 3); + Eigen::MatrixXd uv(num_halfedges, 2); + std::vector is_self_overlapping(num_faces, 0); + for (int f = 0; f < num_faces; ++f) + { + int hij = m.h[f]; + for (int i = 0; i < 3; ++i) + { + F(f, i) = parent.vtx_reindex_orig[m.v_rep[m.to[hij]]]; + F_uv(f, i) = hij; + uv.row(hij) = he_uv[end_halfedge[hij]]; + + // increment halfedge + hij = m.n[hij]; + } + if (is_face_self_overlapping(f)) is_self_overlapping[f] = 1; + } + + Eigen::MatrixXd& V = parent.V_orig; +// auto [V_r, F_r, uv_r, FT_r] = build_refined_mesh(); + //Holonomy::view_seamless_parameterization(V, F, uv, F_uv, "simplification", false); +// Holonomy::view_seamless_parameterization(V_r, F_r, uv_r, FT_r, "refined"); + //polyscope::getSurfaceMesh("simplification")->addFaceScalarQuantity("is self overlapping", is_self_overlapping); +} + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/hierarchy/voronoi.cpp b/src/resolution/hierarchy/voronoi.cpp new file mode 100644 index 0000000..8404cc9 --- /dev/null +++ b/src/resolution/hierarchy/voronoi.cpp @@ -0,0 +1,2859 @@ +#include "resolution/hierarchy/voronoi.h" +#include "resolution/core/layout.h" +#include "metric/viewer.h" +#include "metric/constraint.h" +#include "parametrization/triangulation.h" +#include "util/union_find.h" +#include "util/map.h" +#include "util/vf_mesh.h" +#include "holonomy/core/viewer.h" +#include "holonomy/core/dual_lengths.h" +#include "field/frame_field.h" +#include "parametrization/layout.h" +#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "metric/area.h" +#include "conformal_ideal_delaunay/Layout.hh" + +#include "resolution/core/geodesics.h" +#include "resolution/core/geodesic_flipper.h" + +#include +#include +#include + +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "main_helper.h" + +#ifdef ENABLE_VISUALIZATION +#include "polyscope/surface_mesh.h" +#include "polyscope/curve_network.h" +#include "polyscope/point_cloud.h" +#endif + +namespace Penner +{ +namespace Resolution +{ + + +VoronoiOverlayMesh::VoronoiOverlayMesh( + const Eigen::MatrixXd &V, + const std::vector &vtx_reindex, + const Mesh &mesh) + : SimplificationOverlayMesh(V, vtx_reindex, mesh), voronoi(out.size(), e2h.size(), f2h.size()), path_tracer(*this) +{ +} + +// ----------------- +// Atomic Operations +// ----------------- + +int VoronoiOverlayMesh::create_face() +{ + int f = SimplificationOverlayMesh::create_face(); + voronoi.face_region.push_back(-1); + return f; +} + +int VoronoiOverlayMesh::create_edge() +{ + int e = SimplificationOverlayMesh::create_edge(); + + voronoi.edge_region.push_back(-1); + voronoi.edge_type.push_back(EdgeType::EF); + + for (int i : {0, 1}) + { + //voronoi.dual_halfedge_next.push_back(-1); + voronoi.dual_halfedge.push_back(-1); + } + + return e; +} + +int VoronoiOverlayMesh::create_vertex() +{ + int v = SimplificationOverlayMesh::create_vertex(); + + voronoi.distance.push_back(infinity); + voronoi.vertex_region.push_back(-1); + voronoi.vertex_type.push_back(VertexType::VF); + + return v; +} + +int VoronoiOverlayMesh::split_edge(int halfedge_index) +{ + int edge_index = h2e[halfedge_index]; + int hij = SimplificationOverlayMesh::split_edge(halfedge_index); + split_edge_voronoi(halfedge_index, edge_index, voronoi); + + return hij; +} + +int VoronoiOverlayMesh::split_face(int h, int g) +{ + int face_index = face[h]; + int h_split = SimplificationOverlayMesh::split_face(h, g); + int e = h2e[h_split]; + split_face_voronoi(e, face_index, voronoi); + + return h_split; +} + +int VoronoiOverlayMesh::add_center_to_face(int face_index) +{ + int vi = SimplificationOverlayMesh::add_center_to_face(face_index); + add_center_to_face_voronoi(vi, face_index, voronoi); + + return vi; +} + +bool VoronoiOverlayMesh::is_regular_face(int face_index) const +{ + if (is_face_deleted(face_index)) + return false; + int hij = f2h[face_index]; + int hkl = hij; + int num_embedded_edges = 0; + do + { + if ((simp.edge_type[h2e[hkl]] == EdgeType::EE) || (voronoi.edge_type[h2e[hkl]] == EdgeType::EE)) + { + ++num_embedded_edges; + } + hkl = n[hkl]; + } while (hij != hkl); + + return (num_embedded_edges < 2); +} + +bool VoronoiOverlayMesh::is_regular_edge(int edge_index) const +{ + if (is_edge_deleted(edge_index)) + return false; + if (simp.edge_type[edge_index] == EdgeType::EE) + return true; + if (voronoi.edge_type[edge_index] == EdgeType::EE) + return true; + int hij = e2h[edge_index]; + int hji = opp[hij]; + if ((simp.vertex_type[to[hij]] == VertexType::VF) && (voronoi.vertex_type[to[hij]] == VertexType::VF)) + return true; + if ((simp.vertex_type[to[hji]] == VertexType::VF) && (voronoi.vertex_type[to[hji]] == VertexType::VF)) + return true; + return false; +} + +// --------------- +// Voronoi Regions +// --------------- + +// TODO: replace with local version that trims paths above the minimum found length +bool VoronoiOverlayMesh::grow_voronoi_regions(const std::vector& seeds) +{ + // Initialize an array to keep track of vertices + int num_vertices = out.size(); + //std::vector halfedge_from_vertex(num_vertices, -1); + + // Mark seeds with 0 distance + int num_dual_vertices = voronoi.dual_vertices.size(); + for (int vi : seeds) + { + voronoi.distance[vi] = 0.; + voronoi.vertex_region[vi] = voronoi.dual_vertices.size();; + voronoi.vertex_type[vi] = VertexType::VF; + voronoi.dual_vertices.push_back(vi); + } + + // Define a custom comparison function for the priority queue + typedef std::pair WeightedVertex; + auto vertex_compare = [](const WeightedVertex &left, const WeightedVertex &right) + { + return left.second > right.second; + }; + + // Initialize the stack of vertices to process with all vertices + std::priority_queue, decltype(vertex_compare)> + vertices_to_process(vertex_compare); + // for (int vi = 0; vi < num_vertices; ++vi) { + for (int vi : seeds) + { + vertices_to_process.push(std::make_pair(vi, voronoi.distance[vi])); + } + + // Perform Dijkstra algorithm + while (!vertices_to_process.empty()) + { + // Get the next vertex to process + auto [vi, vi_cost] = vertices_to_process.top(); + vertices_to_process.pop(); + + // Skip vertices with lower cost + if (vi_cost > voronoi.distance[vi]) + continue; + + // Iterate over the vertex circulator via halfedges + int h_start = out[vi]; + int hij = h_start; + do + { + // Get the vertex in the one ring at the tip of the halfedge + int vj = to[hij]; + + // Get candidate edge cost (either path length or edge weight) + Scalar candidate_cost = halfedge_length(hij) + voronoi.distance[vi]; + + // add current candidate to record if it is valid + if (voronoi.distance[vj] > candidate_cost) + { + int hji = opp[hij]; + int hjk = hji; + bool is_base_region = true; + int num_regions = 0; + do + { + hjk = opp[prev[hjk]]; + int vk = to[hjk]; + + if ((voronoi.vertex_region[vk] == voronoi.vertex_region[vi]) && (!is_base_region)) + { + num_regions++; + } + + is_base_region = (voronoi.vertex_region[vk] == voronoi.vertex_region[vi]); + } while (hjk != hji); + + // if vertex would destroy disk topology or exceed max distance, add as new seed + if ((num_regions > 1) || (candidate_cost > voronoi.max_distance)) + { + voronoi.distance[vj] = 0.; + voronoi.vertex_region[vj] = voronoi.dual_vertices.size(); + voronoi.vertex_type[vj] = VertexType::VF; + voronoi.dual_vertices.push_back(vj); + vertices_to_process.push(std::make_pair(vj, voronoi.distance[vj])); + } + // add vertex otherwise + else + { + //halfedge_from_vertex[vj] = hij; + voronoi.distance[vj] = candidate_cost; + voronoi.vertex_region[vj] = voronoi.vertex_region[vi]; + vertices_to_process.push(std::make_pair(vj, voronoi.distance[vj])); + } + } + + // Progress to the next halfedge in the vertex circulator + hij = opp[prev[hij]]; + } while (hij != h_start); + } + + return true; +} + +void VoronoiOverlayMesh::set_max_voronoi_distance(Scalar max_distance) +{ + spdlog::info("setting max voronoi distance {}", max_distance); + + // if maximally distant vertex too far away from nearest neighbor, add as seed + int num_vertices = out.size(); + for (int vi = 0; vi < num_vertices; ++vi) + { + if (voronoi.distance[vi] < max_distance) continue; + + // grow voronoi regions with new seed + grow_voronoi_regions({vi}); + } +} + +void VoronoiOverlayMesh::set_constained_max_voronoi_distance( + Scalar max_distance, + const std::vector& is_allowed) +{ + spdlog::info("setting max voronoi distance {}", max_distance); + + // sort vertices by distance + int num_vertices = out.size(); + std::vector idx = arange(num_vertices); + std::sort(idx.begin(), idx.end(), + [&](int i, int j) { + return (voronoi.distance[i] > voronoi.distance[j]); + }); + spdlog::info("maximum initial distance is {}", voronoi.distance[idx[0]]); + + // if maximally distant vertex too far away from nearest neighbor, add as seed + for (int i = 0; i < num_vertices; ++i) + { + int vi = idx[i]; + if (!is_allowed[vi]) continue; + if (voronoi.distance[vi] < max_distance) continue; + + // grow voronoi regions with new seed + grow_voronoi_regions({vi}); + } +} + +void VoronoiOverlayMesh::set_weighted_max_voronoi_distance( + Scalar max_distance, + const VectorX& weights) +{ + spdlog::info("setting max voronoi distance {}", max_distance); + + // sort vertices by distance + int num_vertices = out.size(); + std::vector idx = arange(num_vertices); + std::sort(idx.begin(), idx.end(), + [&](int i, int j) { + return (voronoi.distance[i] > voronoi.distance[j]); + }); + spdlog::info("maximum initial distance is {}", voronoi.distance[idx[0]]); + + // if maximally distant vertex too far away from nearest neighbor, add as seed + for (int i = 0; i < num_vertices; ++i) + { + int vi = idx[i]; + if ((weights[vi] * voronoi.distance[vi]) < max_distance) continue; + + // grow voronoi regions with new seed + grow_voronoi_regions({vi}); + } +} + +void VoronoiOverlayMesh::make_voronoi_uniform(Scalar dual_length_ratio) +{ + int num_faces = f2h.size(); + std::vector dual_faces = generate_dual_faces(); + std::vector is_dual_face; + convert_index_vector_to_boolean_array(dual_faces, num_faces, is_dual_face); + int num_simp_faces = dual_faces.size(); + int num_simp_halfedges = 3 * num_simp_faces; + + // need to temporarily store a lot of connectivity data to compute geometric quantities + // TODO: make this a standalone utility function + int num_simp_vertices = voronoi.dual_vertices.size(); + voronoi.mesh.n.resize(num_simp_halfedges); + voronoi.mesh.opp.resize(num_simp_halfedges); + voronoi.mesh.prev.resize(num_simp_halfedges); + voronoi.mesh.to.resize(num_simp_halfedges); + voronoi.mesh.f.resize(num_simp_halfedges); + voronoi.mesh.h.resize(num_simp_faces); + voronoi.mesh.out.resize(num_simp_vertices); + std::vector from(num_simp_halfedges); + voronoi.dual_halfedge_starts.resize(num_simp_halfedges); + + std::vector> dual_edges(num_simp_halfedges, std::vector()); + int F = 0; + typedef Eigen::Triplet T; + std::vector triplet_list; + for (int f = 0; f < num_faces; ++f) + { + if (!is_dual_face[f]) + continue; + + int hij = f2h[f]; + int I = 0; + do + { + int hji = opp[hij]; + int vi = to[hji]; + int vj = to[hij]; + int Vi = voronoi.vertex_region[vi]; + int Vj = voronoi.vertex_region[vj]; + + // mark edge + triplet_list.push_back(T(Vi, Vj, 1)); + + int H = 3 * F + I; + int Hn = 3 * F + ((I + 1) % 3); + assert(F < num_simp_faces); + assert(H < num_simp_halfedges); + voronoi.mesh.n[H] = Hn; + voronoi.mesh.prev[Hn] = H; + voronoi.mesh.f[H] = F; + voronoi.mesh.h[F] = H; + from[H] = Vi; // store from to avoid needing opp + voronoi.mesh.to[H] = Vj; + voronoi.mesh.out[Vj] = Hn; + int h = hji; + dual_edges[H].push_back(hji); + while (!is_dual_face[face[h]]) + { + // iterate to next crossing edge + h = n[h]; + if (voronoi.vertex_region[to[h]] == voronoi.vertex_region[to[opp[h]]]) h = n[h]; + h = opp[h]; + + // add dual edge + dual_edges[H].push_back(h); + } + I = (I + 1) % 3; + hij = n[hij]; + } while (hij != f2h[f]); + F++; + } + + // get halfedge lengths + std::vector dual_lengths = Holonomy::compute_dual_edge_lengths(orig.m); + + // sum up lengths around perimeter + int num_dual_faces = voronoi.dual_vertices.size(); + std::vector lengths(num_simp_halfedges, 0.); + std::vector perimeters(num_simp_vertices, 0.); + for (int Hij = 0; Hij < num_simp_halfedges; ++Hij) + { + int Vj = voronoi.mesh.to[Hij]; + for (int h : dual_edges[Hij]) + { + lengths[Hij] += dual_lengths[h]; + perimeters[Vj] += dual_lengths[h]; + } + } + + // check for short vertex joints relative to perimeter + std::vector is_new_seed(out.size(), false); + for (int Hij = 0; Hij < num_simp_halfedges; ++Hij) + { + int Vj = voronoi.mesh.to[Hij]; + int Hjk = voronoi.mesh.n[Hij]; + if ((lengths[Hij] + lengths[Hjk]) < (dual_length_ratio * perimeters[Vj])) + { + int hkj = dual_edges[Hjk].front(); + int vj = to[hkj]; + assert(voronoi.vertex_region[vj] == Vj); + + // only add if not already root + if (voronoi.distance[vj] != 0.) is_new_seed[vj] = true; + } + } + + // Build adjacency matrix from triplets + int num_entries = triplet_list.size(); + int num_dual_vertices = voronoi.dual_vertices.size(); + Eigen::SparseMatrix adjacency(num_dual_vertices, num_dual_vertices); + adjacency.reserve(num_entries); + adjacency.setFromTriplets(triplet_list.begin(), triplet_list.end()); + + // check for repeated edges and add a vertex to remove + int num_vertices = out.size(); + std::vector is_dual_vertex; + convert_index_vector_to_boolean_array(voronoi.dual_vertices, num_vertices, is_dual_vertex); + for (int Hij = 0; Hij < num_simp_halfedges; ++Hij) + { + int Vi = from[Hij]; + int Vj = voronoi.mesh.to[Hij]; + if (Vi < Vj) continue; // only process each edge once + if (adjacency.coeffRef(Vi, Vj) == 1) continue; // skip simple edges + spdlog::debug("Adding seed for repeated edge {} = {}, {}", Hij, Vi, Vj); + + // add new seed near dual edge + bool is_seed_found = false; + for (int h : dual_edges[Hij]) + { + // try adding seed at tip + int vi = to[h]; + if (!is_dual_vertex[vi]) + { + is_new_seed[vi] = true; + is_seed_found = true; + break; + } + + // try vertex at base if fails + int vj = to[opp[h]]; + if (!is_dual_vertex[vj]) + { + is_new_seed[vj] = true; + is_seed_found = true; + break; + } + } + } + + // get new seeds + std::vector new_seeds; + convert_boolean_array_to_index_vector(is_new_seed, new_seeds); + + if (!new_seeds.empty()) + { + spdlog::info("Adding {} seeds", new_seeds.size()); + // grow voronoi region for new seeds + grow_voronoi_regions(new_seeds); + + // recurse in case not uniform + make_voronoi_uniform(dual_length_ratio); + } + else + { + // seed insertion can introduce non-disk regions; regrow voronoi regions from seeds to remove + std::vector seeds = voronoi.dual_vertices; + + // ensure have sufficiently many seeds + if (seeds.size() < 4) seeds = arange(num_vertices); + + voronoi.reset(); + grow_voronoi_regions(seeds); + } +} + + +void VoronoiOverlayMesh::grow_uniform_voronoi_regions( + std::vector& seeds, + Scalar ratio, + Scalar normal_scale, + Scalar dual_length_ratio) +{ + if (ratio > 0.) + { + std::vector l = orig.m.l; + if (normal_scale > 0.) orig.m.l = compute_curvature_adaptive_lengths(V_orig, F_orig, orig.m, vtx_reindex_orig, normal_scale); + + // compute intrinsic mesh area + VectorX mesh_areas = areas(orig.m); + Scalar mesh_area = mesh_areas.sum() / 3.; // divide by 3 as per halfedge area + + // compute target edge length to achieve density defined by ratio + int num_vertices = orig.m.n_ind_vertices(); + Scalar target_length = sqrt((2. * mesh_area) / (sqrt(3.) * ratio * num_vertices)); + Scalar avg_length = Eigen::Map(orig.m.l.data(), orig.m.l.size()).mean(); + spdlog::info("average length of original mesh is {}", avg_length); + spdlog::info("target length is {}", target_length); + + // adaptively sample vertices by adding max edge length + grow_voronoi_regions(seeds); + set_max_voronoi_distance(target_length); + + // set lengths back to uniform and regrow regions + if (normal_scale > 0.) orig.m.l = l; // reset lengths if changed + seeds = voronoi.dual_vertices; + voronoi.reset(); + } + + // grow region from new seeds and make uniform + grow_voronoi_regions(seeds); + make_voronoi_uniform(dual_length_ratio); +} + +// ---------------- +// Voronoi Pipeline +// ---------------- + +void VoronoiOverlayMesh::build_voronoi_connectivity() +{ + split_voronoi_edges(); + split_voronoi_faces(); + find_voronoi_dual_edges(); + propagate_voronoi_vertices_to_faces(); +} + +void VoronoiOverlayMesh::split_voronoi_edges() +{ + int num_edges = e2h.size(); + //typedef Eigen::Triplet T; + //std::vector triplet_list; + for (int e = 0; e < num_edges; ++e) + { + if (is_edge_deleted(e)) + continue; + + // determine whether edge separates two distinct voronoi regions + int hij = e2h[e]; + int vj = to[hij]; + int vi = to[opp[hij]]; + if (voronoi.vertex_type[vi] != VertexType::VF) + continue; + if (voronoi.vertex_type[vj] != VertexType::VF) + continue; + if (voronoi.vertex_region[vi] == voronoi.vertex_region[vj]) + continue; + + // split edge, and label inserted vertex as an edge vertex + int hik = split_edge(hij); + int vk = to[hik]; + voronoi.vertex_type[vk] = VertexType::VE; + + // set distance to vertex as the maximum distance to a neighboring region + voronoi.distance[vk] = max(voronoi.distance[vi], voronoi.distance[vj]); + + //int Vi = voronoi.vertex_region[vi]; + //int Vj = voronoi.vertex_region[vj]; + //triplet_list.push_back(T(min(Vi, Vj), max(Vi, Vj), -vk - 1)); + } + +#ifdef ENABLE_VISUALIZATION + if (false) + { + view(); + std::string mesh_handle = "overlay mesh"; + auto [v_new2old, v_old2new] = get_vertex_reductions(); + std::vector vertex_region = vector_compose(voronoi.vertex_region, v_new2old); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("vertex region", vertex_region); + polyscope::show(); + } +#endif +} + + +std::vector VoronoiOverlayMesh::generate_dual_faces() const +{ + int num_faces = f2h.size(); + std::vector dual_faces = {}; + dual_faces.reserve(num_faces); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) + continue; + int hij = f2h[f]; + int vi = to[hij]; + int vj = to[n[hij]]; + int vk = to[n[n[hij]]]; + + // check if all vertices are distinct regions + if (voronoi.vertex_region[vi] == voronoi.vertex_region[vj]) continue; + if (voronoi.vertex_region[vj] == voronoi.vertex_region[vk]) continue; + if (voronoi.vertex_region[vk] == voronoi.vertex_region[vi]) continue; + + // mark dual vertex + dual_faces.push_back(f); + } + + return dual_faces; +} + +void VoronoiOverlayMesh::split_voronoi_faces() +{ + // count the number of faces where three voronoi regions meet + int num_faces = f2h.size(); + int num_simp_faces = 0; + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) + continue; + + // for a split triangle mesh, the number of edges is 6 iff three voronoi regions meet here + if (face_degree(f) == 6) + num_simp_faces++; + } + + // initialize halfedge data structure array sizes + int num_simp_halfedges = 3 * num_simp_faces; + int num_simp_vertices = voronoi.dual_vertices.size(); + voronoi.mesh.n.resize(num_simp_halfedges); + voronoi.mesh.opp.resize(num_simp_halfedges); + voronoi.mesh.prev.resize(num_simp_halfedges); + voronoi.mesh.to.resize(num_simp_halfedges); + voronoi.mesh.f.resize(num_simp_halfedges); + voronoi.mesh.h.resize(num_simp_faces); + voronoi.mesh.out.resize(num_simp_vertices); + voronoi.dual_halfedge_starts.resize(num_simp_halfedges); + std::vector circ(num_simp_halfedges); + + int F = 0; + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) + continue; + int degree = face_degree(f); + + // case: unrefined face contained in a voronoi region, so nothing to do + if (degree <= 3) + { + continue; // face not refined + } + // case: dual face where three voronoi regions meet + else if (degree == 6) + { + // add center to face + spdlog::debug("adding center to face {}", f); + int vi = add_center_to_face(f); + voronoi.vertex_type[vi] = VertexType::VV; + voronoi.distance[vi] = 0.; + int hij = out[vi]; + int I = 0; + do + { + int vj = to[hij]; + + // case: halfedge is part of the voronoi region boundary + if (voronoi.vertex_type[vj] != VertexType::VF) + { + // mark adjacent edges in the voronoi boundary and mark halfedge start + int H = 3 * F + I; + voronoi.edge_type[h2e[hij]] = EdgeType::EE; + voronoi.dual_halfedge_starts[H] = hij; + } + // case: halfedge is interior to a voronoi vertex region + else + { + // set distance to vertex as maximum distance of adjacent regions + voronoi.distance[vi] = max(voronoi.distance[vi], voronoi.distance[vj]); + + // set connectivity data in this region (except opp relations) + int Vj = voronoi.vertex_region[vj]; + int H = 3 * F + I; + int Hn = 3 * F + ((I + 1) % 3); + assert(F < num_simp_faces); + assert(H < num_simp_halfedges); + voronoi.mesh.n[H] = Hn; + voronoi.mesh.prev[Hn] = H; + voronoi.mesh.f[H] = F; + voronoi.mesh.h[F] = H; + voronoi.mesh.to[H] = Vj; + voronoi.mesh.out[Vj] = Hn; + I = (I + 1) % 3; + } + hij = opp[prev[hij]]; + } while (hij != out[vi]); + F++; + } + // case: boundary face where two voronoi regions meet + else + { + // find first halfedge with split vertex at the tip + int hij = f2h[f]; + while (voronoi.vertex_type[to[hij]] != VertexType::VE) + hij = n[hij]; + + // find second halfedge with split vertex at the tip + int hjk = n[hij]; + while (voronoi.vertex_type[to[hjk]] != VertexType::VE) + hjk = n[hjk]; + + // split the face between the two vertices and mark it as voronoi region edge + int h_split = split_face(hij, hjk); + voronoi.edge_type[h2e[h_split]] = EdgeType::EE; + } + } + + // ensure mesh is a triangulation + triangulate(); // TODO Remove + assert(are_polygon_mesh_halfedges_valid(voronoi.mesh.n, voronoi.mesh.prev)); + assert(are_polygon_mesh_faces_valid(voronoi.mesh.n, voronoi.mesh.f, voronoi.mesh.h)); + +#ifdef ENABLE_VISUALIZATION + if (false) + { + view(); + std::string mesh_handle = "overlay mesh"; + auto [v_new2old, v_old2new] = get_vertex_reductions(); + std::vector vertex_region = vector_compose(voronoi.vertex_region, v_new2old); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("vertex region", vertex_region); + polyscope::show(); + } +#endif +} + +void VoronoiOverlayMesh::find_voronoi_dual_edges() +{ + int num_dual_halfedges = voronoi.dual_halfedge_starts.size(); + spdlog::info("tracing {} dual halfedges", num_dual_halfedges); + + for (int Hij = 0; Hij < num_dual_halfedges; ++Hij) + { + // get start of embedded boundary chain (already computed) and label it + int hij = voronoi.dual_halfedge_starts[Hij]; + int min_node = to[hij]; + voronoi.dual_halfedge[hij] = Hij; + int length = 1; + + // traverse Voronoi halfedge, labelling the overlay edges and searching for + // a minimal distance vertex on the chain to set as the midpoint + while (voronoi.vertex_type[to[hij]] != VertexType::VV) + { + int vj = to[hij]; + int hjk = opp[hij]; + + // get next dual halfedge in chain + do + { + hjk = opp[prev[hjk]]; + int ejk = h2e[hjk]; + if (voronoi.edge_type[ejk] == EdgeType::EE) + { + break; + } + } while (hjk != opp[hij]); + + // label overlay halfedge + voronoi.dual_halfedge[hjk] = Hij; + + // check if shorter maximum distance found + if (voronoi.distance[vj] < voronoi.distance[min_node]) + min_node = vj; + + //iterate + hij = hjk; + length++; + } + if (length == 1) + spdlog::warn("encountered single edge voronoi segment"); + + // set the minimum distance node as the midpoint + voronoi.dual_edge_midpoints.push_back(min_node); + } + + // build opp maps by propagating overlay opposite maps + for (int Hij = 0; Hij < num_dual_halfedges; ++Hij) + { + int hij = voronoi.dual_halfedge_starts[Hij]; + int hji = opp[hij]; + int Hji = voronoi.dual_halfedge[hji]; + voronoi.mesh.opp[Hij] = Hji; + assert(voronoi.mesh.to[Hji] == voronoi.mesh.to[voronoi.mesh.prev[Hij]]); + + // ensure consistency + voronoi.dual_edge_midpoints[Hij] = voronoi.dual_edge_midpoints[Hji]; + } + assert(are_polygon_mesh_vertices_valid(voronoi.mesh.opp, voronoi.mesh.prev, voronoi.mesh.to, voronoi.mesh.out)); + + // copy voronoi dual connectivity to the simplified mesh + simp.m.n = voronoi.mesh.n; + simp.m.opp = voronoi.mesh.opp; + simp.m.f = voronoi.mesh.f; + simp.m.h = voronoi.mesh.h; + simp.m.to = voronoi.mesh.to; + simp.m.out = voronoi.mesh.out; + simp.m.opp = voronoi.mesh.opp; + simp.m.v_rep = arange(simp.m.out.size()); + + // set arbitrary metric and constraints + simp.m.l = std::vector(simp.m.n.size(), 1.); + simp.m.Th_hat = std::vector(simp.m.out.size(), 2 * PI); + simp.m.fixed_dof = std::vector(simp.m.out.size(), false); + simp.m.fixed_dof[0] = true; + + // set trivial reflection structure + simp.m.type = std::vector(simp.m.n.size(), 0); + simp.m.type_input = std::vector(simp.m.n.size(), 0); + simp.m.R = std::vector(simp.m.n.size(), 0); + + build_edge_maps(simp.m.opp, simp.h2e, simp.e2h); +} + +void VoronoiOverlayMesh::propagate_voronoi_vertices_to_faces() +{ + int num_faces = f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) + continue; + + // each face contains at least one interior vertex: use to label face + int hij = f2h[f]; + do + { + int vj = to[hij]; + + // find interior vertex + if (voronoi.vertex_type[vj] == VertexType::VF) + { + voronoi.face_region[f] = voronoi.vertex_region[vj]; + break; + } + hij = n[hij]; + } while (hij != f2h[f]); + } +} + +// ----------------- +// Delaunay Pipeline +// ----------------- + +void VoronoiOverlayMesh::trace_delaunay_edges() +{ + // enusre regular at the start + make_regular(); + + int num_halfedges = voronoi.dual_edge_midpoints.size(); + int num_regions = voronoi.dual_vertices.size(); + simp.vertex_index = voronoi.dual_vertices; + std::vector is_traced(num_halfedges, false); + for (int hij = 0; hij < num_halfedges; ++hij) + { + // mark voronoi seed vertex as an embedded vertex + int Vi = simp.m.to[simp.m.opp[hij]]; + int vi = voronoi.dual_vertices[Vi]; + simp.vertex_type[vi] = VertexType::VV; + simp.vertex_parent[vi] = Vi; + + // mark voronoi boundary as an embedded edge vertex + int vk = voronoi.dual_edge_midpoints[hij]; + simp.vertex_type[vk] = VertexType::VE; + simp.vertex_parent[vk] = simp.h2e[hij]; + + // trace path from the seed to the boundary + std::vector halfedges = {}; + if (!trace_path(vi, vk, hij, simp.h2e[hij], halfedges)) + { + spdlog::error("could not trace path from {} to {}", vi, vk); + } + + // set partent of opposite halfedges + for (int h : halfedges) + { + simp.halfedge_parent[opp[h]] = simp.m.opp[hij]; + } + + // check locally whether the connectivity is still regular in regions + // where tracing is still necessary + if (!is_traced[simp.m.opp[simp.m.n[simp.m.n[hij]]]]) + { + make_oriented_adjacent_regular(reverse_vector(halfedges)); + } + if (!is_traced[simp.m.n[simp.m.opp[hij]]]) + { + make_oriented_adjacent_regular(vector_compose(opp, halfedges)); + } + + // mark the halfedge as traced + is_traced[hij] = true; + } +} + +void VoronoiOverlayMesh::retrace_edges() +{ + spdlog::info("retracing edges"); + std::vector overlay_vertices = find_overlay_vertices(simp); + std::vector start_halfedge = find_start_halfedges(simp); + int num_edges = simp.e2h.size(); + use_consistency_check = true; + for (int e = 0; e < num_edges; ++e) + { + int hij = simp.e2h[e]; + int hji = simp.m.opp[hij]; + int Vi = simp.m.to[hji]; + int Vj = simp.m.to[hij]; + int vi = overlay_vertices[Vi]; + int vj = overlay_vertices[Vj]; + + // remove the current path + remove_path(start_halfedge[hij], simp); + + // retrace the path + std::vector halfedges = {}; + if (!trace_path(vi, vj, hij, e, halfedges)) + { + spdlog::error("could not trace path from {} to {}", vi, vj); + } + for (int h : halfedges) + { + simp.halfedge_parent[opp[h]] = hji; + } + //make_adjacent_regular(halfedges); + } + use_consistency_check = false; +} + +void VoronoiOverlayMesh::straighten_edges() +{ + std::vector> halfedge_path = enumerate_simplified_halfedges(); + + // get straightened edges + GeodesicFlipper geodesic_flipper(*this, halfedge_path); + geodesic_flipper.flip_to_geodesics(); + geodesic_flipper.generate_surface_points(); + std::vector>& surface_points = geodesic_flipper.surface_points; + std::vector is_intersecting = geodesic_flipper.get_intersecting_paths(); + + // insert straightened paths + PathStraightener path_straightener(*this, surface_points, is_intersecting); + path_straightener.split_edges(*this, geodesic_flipper.edge_splits); + path_straightener.join_edges(*this); + path_straightener.retrace_paths(*this); + + // create new embedded face data + flood_faces(); + + // remove excess refinement from original paths + remove_excess_refinement(); +} + +// ----------- +// Frame Field +// ----------- + +std::tuple +VoronoiOverlayMesh::sample_field_from_overlay( + const Eigen::MatrixXd& reference_field_overlay, + const Eigen::VectorXd& thetas_overlay, + const Eigen::VectorXi& period_jumps_overlay, + const OverlayStructure& overlay) const +{ + // sample theta and reference direction on each face + int num_faces = f2h.size(); + Eigen::MatrixXd reference_field = Eigen::MatrixXd::Zero(num_faces, 3); + Eigen::VectorXd thetas = Eigen::VectorXd::Zero(num_faces); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) continue; + int F = overlay.face_parent[f]; + reference_field.row(f) = reference_field_overlay.row(F); + thetas[f] = thetas_overlay[F]; + } + + // sample period jump across overlay edges + int num_halfedges = n.size(); + Eigen::VectorXi period_jumps = Eigen::VectorXi::Zero(num_halfedges); + for (int h = 0; h < num_halfedges; ++h) + { + if (is_halfedge_deleted(h)) continue; + + // skip interior face edges + if (overlay.edge_type[h2e[h]] == EdgeType::EF) continue; + + period_jumps[h] = period_jumps_overlay[overlay.halfedge_parent[h]]; + } + + return std::make_tuple(reference_field, thetas, period_jumps); +} + +void VoronoiOverlayMesh::comb_field_to_overlay_seam( + const std::vector is_overlay_cut, + int start_halfedge_index, + Eigen::VectorXd& thetas, + Eigen::VectorXi& period_jumps, + const OverlayStructure& overlay) const +{ + // initialize queue and markings with first halfedge + int h = start_halfedge_index; + while (is_halfedge_deleted(h)) ++h; // make sure not deleted + std::queue Q; + std::vector done(f2h.size()); + Q.push(h); + done[face[h]] = true; + + while (!Q.empty()) { + h = Q.front(); + Q.pop(); + int hn = n[h]; + int hp = n[hn]; + + for (int h_curr : {h, hn, hp}) + { + int e = h2e[h_curr]; + int ho = opp[h_curr]; + int fo = face[ho]; + + // stop at visited edges and explicitly cut edges + if (done[face[ho]]) continue; + if ((overlay.edge_type[e] == EdgeType::EE) && (is_overlay_cut[overlay.halfedge_parent[h_curr]])) continue; + + // bake period jump across edge into the face angle + int pj = period_jumps[ho]; + thetas[fo] += pj * (PI / 2.); + + // push period jumps through face to opposite wedge + period_jumps[ho] = period_jumps[opp[ho]] = 0; + period_jumps[n[ho]] -= pj; + period_jumps[prev[ho]] -= pj; + period_jumps[opp[n[ho]]] += pj; + period_jumps[opp[prev[ho]]] += pj; + + // mark opposite face as seen and queue + done[fo] = true; + Q.push(ho); + } + } +} + +Eigen::MatrixXd VoronoiOverlayMesh::compute_overlay_principal_direction( + const Eigen::MatrixXd& V_overlay, + const Eigen::MatrixXi& F_overlay, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& thetas, + const OverlayStructure& overlay) const +{ + int num_faces = reference_field.rows(); + Eigen::MatrixXd PD1(num_faces, 3); + + // get local basis for rotations in overlay mesh + Eigen::MatrixXd B1, B2, B3; + igl::local_basis(V_overlay, F_overlay, B1, B2, B3); + + // rotate reference field in overlay faces + for (int f = 0; f < num_faces; ++f) + { + int f_orig = overlay.face_parent[f]; + PD1.row(f) = Field::rotate_vector(reference_field.row(f), thetas(f), B1.row(f_orig), B2.row(f_orig)); + } + + return PD1; +} + + +std::tuple +VoronoiOverlayMesh::average_overlay_field( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& field_direction, + const OverlayStructure& overlay) const +{ + int num_overlay_faces = F.rows(); + + // get local basis for rotations in overlay mesh + Eigen::MatrixXd B1, B2, N; + igl::local_basis(V, F, B1, B2, N); + + // compute area weighted average directions + int num_faces = f2h.size(); + std::vector areas = compute_face_areas(); + Eigen::MatrixXd weighted_direction = Eigen::MatrixXd::Zero(num_overlay_faces, 3); + for (int f = 0; f < num_faces; ++f) + { + if (is_face_deleted(f)) continue; + int f_orig = overlay.face_parent[f]; + weighted_direction.row(f_orig) += areas[f] * field_direction.row(f); + } + + // normalize directions and generate rotated frame direction + Eigen::MatrixXd PD1(num_overlay_faces, 3); + Eigen::MatrixXd PD2(num_overlay_faces, 3); + for (int f = 0; f < num_overlay_faces; ++f) + { + PD1.row(f) = project_orthogonal_to(weighted_direction.row(f), N.row(f)).stableNormalized(); + PD2.row(f) = Field::rotate_vector(PD1.row(f), PI / 2., B1.row(f), B2.row(f)); + } + + return std::make_tuple(PD1, PD2); +} + +Eigen::MatrixXd VoronoiOverlayMesh::transfer_original_field_to_overlay() const +{ + // sample field on overlay mesh + auto [reference_field, thetas, period_jumps] = sample_field_from_overlay( + reference_field_orig, + thetas_orig, + period_jumps_orig, + orig); + + // comb field on overlay to match simplified cut + int h_start = 0; + comb_field_to_overlay_seam(is_cut_simp, h_start, thetas, period_jumps, simp); + + // compute combed principal field direction + Eigen::MatrixXd field_direction = compute_overlay_principal_direction( + V_orig, + F_orig, + reference_field, + thetas, + orig); + + bool view_sampled_field = false; + if (view_sampled_field) + { + Eigen::MatrixXd frame_field_orig = Field::generate_vector_field(V_orig, F_orig, reference_field_orig, thetas_orig); + Holonomy::view_vector_field(V_orig, F_orig, frame_field_orig, "original"); + + // generate frame field geometry from cross field + Eigen::MatrixXd V_o = generate_overlay_vertices(V_orig, vtx_reindex_orig, orig); + auto [F_o, F_halfedge] = generate_overlay_faces(); + auto [f_new2old, f_old2new] = get_face_reductions(); + Eigen::MatrixXd reference_field_o = subset_vertices(reference_field, f_new2old); + Eigen::MatrixXd theta_o = subset_vertices(thetas, f_new2old); + Eigen::MatrixXd frame_field_o = Field::generate_vector_field(V_o, F_o, reference_field_o, theta_o); + Holonomy::view_vector_field(V_o, F_o, frame_field_o, "overaly"); + } + + return field_direction; +} + +std::tuple +VoronoiOverlayMesh::transfer_overlay_field_to_simplified( + const Eigen::MatrixXd& field_direction +) const +{ + // compute averaged field on the simplified mesh + auto [F_simp, F_halfedge] = generate_connectivity_faces(simp.m.n, simp.m.h, simp.m.to); + auto [PD1, PD2] = average_overlay_field(simplified_V, F_simp, field_direction, simp); + + bool view_sampled_field = false; + if (view_sampled_field) + { + // view coarse mesh + Holonomy::view_vector_field(simplified_V, F_simp, PD1, "simplified"); + } + + return std::make_tuple(PD1, PD2); +} + +// --------------- +// Parametrization +// --------------- + +std::vector VoronoiOverlayMesh::compute_layout() +{ + std::vector is_cut_place_holder; + std::vector is_cut = compute_layout_topology(orig.m, is_cut_place_holder); + std::vector is_cone(orig.m.n_vertices(), false); + for (int vi = 0; vi < orig.m.n_vertices(); ++vi) + { + is_cone[vi] = (!float_equal(orig.m.Th_hat[orig.m.v_rep[vi]] , 2 * PI)); + } + trim_topology(orig.m, is_cone, is_cut); + + // set starting point - use a boundary edge + int h = 0; + while (is_halfedge_deleted(h)) ++h; + spdlog::debug("Using edge {} as layout start", h); + auto done = std::vector(f2h.size(), false); + + Eigen::MatrixXd V_o = generate_overlay_vertices(V_orig, vtx_reindex_orig, orig); + std::vector V_r(V_o.rows()); + // TODO make this an option + for (int vi = 0; vi < V_o.rows(); ++vi) + { + V_r[vi] = V_o.row(vi); + } + + std::queue Q; + Q.push(h); + done[face[h]] = true; + + spdlog::debug("beginning layout"); + while (!Q.empty()) { + h = Q.front(); + Q.pop(); + int hn = n[h]; + int hp = n[hn]; + + for (int h_curr : {h, hn, hp}) + { + int e = h2e[h_curr]; + int ho = opp[h_curr]; + if (done[face[ho]]) continue; + if ((orig.edge_type[e] == EdgeType::EE) && (is_cut[orig.halfedge_parent[h_curr]])) continue; + + done[face[ho]] = true; + if (PD1_o.empty() || PD2_o.empty()) + { + transform_opposite_face(*this, he_uv, h_curr); + } + else + { + transform_opposite_face(*this, he_uv, V_r, PD1_o, PD2_o, h_curr); + } + Q.push(ho); + } + } + + return is_cut; +}; + + +std::tuple, std::vector, std::vector, std::vector, +std::vector, std::vector, std::vector> +get_consistent_layout(Mesh &m_orig, OverlayMesh &m_o) +{ + std::vector is_cut = {}; + std::vector is_cut_orig = compute_layout_topology(m_orig, is_cut); + + // Get original overlay face labels + auto f_labels = get_overlay_face_labels(m_o); + + // Compute layout of the underlying flipped mesh + auto mc = m_o.cmesh(); + m_o.garbage_collection(); + + spdlog::info("computing layout"); + auto mc_type = mc.type; + mc.type = std::vector(mc.n_halfedges(), 0); + std::vector u_vec(mc.n_ind_vertices(), 0.); + // std::vector _is_cut_place_holder; // TODO Remove + // auto layout_res = compute_layout(mc, u_vec, _is_cut_place_holder, 0); + std::vector _is_cut_place_holder = is_cut; + auto layout_res = compute_layout(mc, u_vec, _is_cut_place_holder); + auto _u_c = std::get<0>(layout_res); + auto _v_c = std::get<1>(layout_res); + auto is_cut_c = std::get<2>(layout_res); + mc.type = mc_type; + + std::vector u_o, v_o, u_unif, v_unif; + + // Interpolate layout to the overlay mesh + spdlog::info("interpolating layout"); + Eigen::Matrix u_eig; + u_eig.resize(u_vec.size()); + for (size_t i = 0; i < u_vec.size(); i++) + { + u_eig(i) = u_vec[i]; + } + m_o.bc_eq_to_scaled(mc.n, mc.to, mc.l, u_eig); + + // check alignment + for (int hij = 0; hij < mc.n_halfedges(); ++hij) + { + if (m_o.first_segment[hij] == m_o.last_segment(hij)) + continue; + int hji = mc.opp[hij]; + + // get the vertices on the _h side + std::vector lambdas; + int current_seg = m_o.first_segment[hij]; + while (m_o.vertex_type[m_o.to[current_seg]] != ORIGINAL_VERTEX) + { + std::array tmp = m_o.seg_bcs[current_seg]; + lambdas.push_back(tmp[1]); + current_seg = m_o.next_segment(current_seg); + } + // check the vertices on the _hopp side + + current_seg = m_o.first_segment[hji]; + int cnt = lambdas.size() - 1; + while (m_o.vertex_type[m_o.to[current_seg]] != ORIGINAL_VERTEX) + { + std::array tmp = m_o.seg_bcs[current_seg]; + if (abs(lambdas[cnt] - tmp[0]) > 1e-10) + { + spdlog::error("alignment problem: {}, {}", lambdas[cnt], tmp[0]); + } + cnt--; + current_seg = m_o.next_segment(current_seg); + } + } + + spdlog::info("interpolating on overlay mesh"); + u_o = m_o.interpolate_along_c_bc(mc.n, mc.f, _u_c); + v_o = m_o.interpolate_along_c_bc(mc.n, mc.f, _v_c); + + // Build a new mesh directly from the triangulated overlay mesh + spdlog::info("creating new mesh with overlay connectivity"); + Mesh m; + m.n = m_o.n; + m.opp = m_o.opp; + m.f = m_o.f; + m.h = m_o.h; + m.out = m_o.out; + m.to = m_o.to; + m.l = std::vector(m.n.size(), 0.0); + for (int i = 0; i < m.n_halfedges(); i++) + { + int h0 = i; + int h1 = h0; + do + { + if (m.n[h1] == h0) + break; + h1 = m.n[h1]; + } while (h0 != h1); + if (m.to[m.opp[h0]] != m.to[h1]) + { + spdlog::error("h0 h1 picked wrong."); + exit(0); + } + m.l[h0] = sqrt(max( + (u_o[h0] - u_o[h1]) * (u_o[h0] - u_o[h1]) + (v_o[h0] - v_o[h1]) * (v_o[h0] - v_o[h1]), 0.)); + if (std::isnan(double(m.l[h0]))) + spdlog::error("length computation produced nan."); + } + m.type = std::vector(m.n.size(), 0); + int num_m_halfedges = m.n.size(); + for (int hi = 0; hi < num_m_halfedges; ++hi) + { + if (!float_equal(m.l[hi], m.l[m.opp[hi]])) + { + spdlog::error("inconsistent edge lengths"); + } + if (m_o.edge_type[hi] == CURRENT_EDGE) + { + m.type[hi] = 4; + } + else if (m_o.edge_type[hi] == ORIGINAL_AND_CURRENT_EDGE) + { + m.type[hi] = m_orig.type[m_o.origin_of_origin[hi]]; + } + else if (m_o.edge_type[hi] == ORIGINAL_EDGE) + { + m.type[hi] = m_orig.type[m_o.origin[hi]]; + } + } + m.type_input = m.type; + + spdlog::info("triangulating overlay mesh"); + triangulate_polygon_mesh(m, u_o, v_o, f_labels); + m.R = std::vector(m.n.size(), 0); + m.type.resize(m.n.size(), 4); + OverlayMesh m_o_tri(m); + for (int i = m_o.n_halfedges(); i < m_o_tri.n_halfedges(); i++) + { + m_o_tri.edge_type[i] = CURRENT_EDGE; // make sure do not use the new diagonal + } + + // map angles to mesh + m.v_rep = range(0, m.out.size()); + m.Th_hat = std::vector(m.out.size(), 2 * PI); + for (int vi = 0; vi < m_o.n_vertices(); vi++) + { + if (m_o.vertex_type[vi] == ORIGINAL_VERTEX) + { + m.Th_hat[vi] = mc.Th_hat[mc.v_rep[vi]]; + } + } + + // Pullback cut on the original mesh to the overlay + spdlog::info("pulling back cut"); + bool is_original_cut = !(is_cut_orig.empty()); + std::vector is_cut_poly; + is_cut_poly = pullback_cut_to_overlay(m_o, is_cut_orig, is_original_cut); + + // Extend the overlay cut to the triangulated mesh + // WARNING: Assumes triangulation halfedges added to the end + std::vector is_cut_o = std::vector(m.n_halfedges(), false); + for (int h = 0; h < m_o.n_halfedges(); ++h) + { + is_cut_o[h] = is_cut_poly[h]; + } + + // Now directly do layout on triangulated overlay mesh + spdlog::info("creating new layout for triangulated overlay"); + std::vector phi(m.n_vertices(), 0.0); + auto overlay_layout_res = compute_layout_components(m, phi, is_cut_o); + std::vector _u_o = std::get<0>(overlay_layout_res); + std::vector _v_o = std::get<1>(overlay_layout_res); + is_cut_o = std::get<2>(overlay_layout_res); + + return std::make_tuple(m, _u_o, _v_o, is_cut_o, _u_c, _v_c, is_cut_c); +} + +// WARNING: Only works if all simplified vertices are original vertices +std::tuple VoronoiOverlayMesh::integrate_simplified_function_onto_original( + const VectorX& simplified_function) +{ + std::vector start_halfedge = find_start_halfedges(orig); + int num_halfedges = orig.m.n.size(); + + // get integrated values between halfedges rotated at vertices + VectorX corner_integral = VectorX::Zero(num_halfedges); + for (int Hij = 0; Hij < num_halfedges; ++Hij) + { + int Hji = orig.m.opp[Hij]; + int hij = start_halfedge[Hij]; + do + { + if (simp.edge_type[h2e[hij]] == EdgeType::EE) + { + int Hij_simp = simp.halfedge_parent[hij]; + corner_integral[Hij] += simplified_function[Hij_simp]; + corner_integral[Hji] -= simplified_function[Hij_simp]; + } + + hij = opp[prev[hij]]; + } + while (orig.edge_type[h2e[hij]] != EdgeType::EE); + } + + // get integrated values along edges + VectorX edge_integral = VectorX::Zero(num_halfedges); + for (int Hij = 0; Hij < num_halfedges; ++Hij) + { + int Hji = orig.m.opp[Hij]; + if (Hji < Hij) continue; + + int hij = start_halfedge[Hij]; + while (orig.vertex_type[to[hij]] != VertexType::VV) + { + if (simp.vertex_type[to[hij]] == VertexType::VE) + { + int hjk = get_next_overlay_edge(hij, simp); + int Hjk_simp = simp.halfedge_parent[hjk]; + edge_integral[Hij] += simplified_function[Hjk_simp]; + edge_integral[Hji] -= simplified_function[Hjk_simp]; + } + else if (simp.vertex_type[to[hij]] == VertexType::VV) + { + spdlog::error("cannot integrate if simplified vertices intersect edges"); + } + + hij = get_next_overlay_edge(hij, orig); + } + } + + return std::make_tuple(corner_integral, edge_integral); +} + +SymDir::Parameters read_parameters(nlohmann::json& config) +{ + SymDir::Parameters param; + param.max_iters = config["max_iters"]; // iterations + param.max_time = config["max_time"]; // time in seconds + param.smooth_only_iters = config["smooth_only_iters"]; + param.E_target = config["E_target"]; // Energy target + param.ls_iters = config["ls_iters"]; // param for linesearch in smoothing operation + param.do_newton = config["do_newton"]; // do newton/gd steps for smoothing operation + param.local_smooth = config["local_smooth"]; + param.global_smooth = config["global_smooth"]; + param.elen_alpha = config["elen_alpha"]; + param.do_projection = config["do_projection"]; + param.with_cons = config["with_cons"]; + param.Lp = config["Lp"]; + param.save_meshes = config["save_meshes"]; + param.do_feature_alignment = config["do_feature_alignment"]; // align feature edges + param.symdir_weight = config["symdir_weight"]; + param.alignment_weight = config["alignment_weight"]; + param.degenerate_weight = config["degenerate_weight"]; + param.fix_misaligned = config["fix_misaligned"]; + param.use_rref = config["use_rref"]; + // param.solver_type = config["solver_type"]; + param.cg_rel_err = config["cg_rel_err"]; + param.cg_iters = config["cg_iters"]; + + param.percentages = config["percentages"].get>(); + param.percentage_target = config["percentage_target"]; + param.percentage_target_value = config["percentage_target_value"]; + param.save_percentages_meshes = config["save_percentages_meshes"]; + + param.E_abs_err = config["E_abs_err"]; + param.E_rel_err = config["E_rel_err"]; + param.diff_err = config["diff_err"]; + param.grad_abs_err = config["grad_abs_err"]; + param.grad_rel_err = config["grad_rel_err"]; + + param.precompute_seamless = config["precompute_seamless"]; + param.projected_newton = config["projected_newton"]; + param.soft_max = config["soft_max"]; + param.t = config["t"]; + param.precompute_seamless = config["precompute_seamless"]; + + param.percentage_target_converge = config["percentage_target_converge"]; + param.max_grad_abs_converge = config["max_grad_abs_converge"]; + param.max_grad_rel_converge = config["max_grad_rel_converge"]; + param.energy_diff_converge = config["energy_diff_converge"]; + param.use_worst_n_energy_in_ls = config["use_worst_n_energy_in_ls"]; + param.E_abs_converge = config["E_abs_converge"]; + param.E_rel_converge = config["E_rel_converge"]; + + param.last_screenshot_after_optimization = config["last_screenshot_after_optimization"]; + param.screenshot_interval = config["screenshot_interval"]; + param.output_dir_for_screenshots = config["output_dir_for_screenshots"]; + param.uv_scale_for_screenshots = config["uv_scale_for_screenshots"]; + param.angle_to_rotate_model_for_screenshots = config["angle_to_rotate_model_for_screenshots"]; + param.screenshot_during_optimization = config["screenshot_during_optimization"]; + + param.degenerate_vertices_preconditioner = config["degenerate_vertices_preconditioner"]; + param.precond_dim = config["precond_dim"]; + param.triangle_threshold = config["triangle_threshold"]; + + return param; +} + +void VoronoiOverlayMesh::optimize_simplified_parameterization() +{ + const Mesh& m_tri = simp.m; + auto [F_tri, F_halfedge] = generate_connectivity_faces(m_tri.n, m_tri.h, m_tri.to); + //Holonomy::view_seamless_parameterization(simplified_V, F_tri, uv_s, F_halfedge); + + // trim cut topology to just cones + std::vector is_cone(simp.m.n_vertices(), false); + for (int vi = 0; vi < simp.m.n_vertices(); ++vi) + { + is_cone[vi] = (!float_equal(simp.m.Th_hat[simp.m.v_rep[vi]] , 2 * PI)); + } + trim_topology(m_tri, is_cone, is_cut_simp); + + int num_halfedges = m_tri.n_halfedges(); + int num_faces = m_tri.n_faces(); + UnionFind cut_vertices(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (is_cut_simp[hij]) continue; + + // get halfedge in adjacent uncut face pointing to vj + int hkj = m_tri.n[m_tri.n[m_tri.opp[hij]]]; + cut_vertices.union_sets(hij, hkj); + } + int num_cut_vertices = cut_vertices.count_sets(); + std::vector cut_vertex_indices = cut_vertices.index_sets(); + + Eigen::MatrixXd V_tri(num_cut_vertices, 3); + Eigen::MatrixXi FT_tri(num_faces, 3); + Eigen::MatrixXd uv_tri(num_cut_vertices, 2); + std::vector> edge_pairings; + edge_pairings.reserve(num_faces); + for (int f = 0; f < num_faces; ++f) + { + for (int i = 0; i < 3; ++i) + { + int hij = F_halfedge(f, i); + uv_tri(cut_vertex_indices[hij], 0) = u_simp[hij]; + uv_tri(cut_vertex_indices[hij], 1) = v_simp[hij]; + V_tri.row(cut_vertex_indices[hij]) = simplified_V.row(m_tri.to[hij]); + FT_tri(f, i) = cut_vertex_indices[hij]; + + // check for seam edges + if (is_cut_simp[hij]) + { + int hji = m_tri.opp[hij]; + if (hij < hji) continue; // only process edge once + int uvj = cut_vertex_indices[hij]; + int uvi = cut_vertex_indices[m_tri.n[m_tri.n[hij]]]; + int uvi_opp = cut_vertex_indices[hji]; + int uvj_opp = cut_vertex_indices[m_tri.n[m_tri.n[hji]]]; + edge_pairings.push_back({ uvi, uvj, uvj_opp, uvi_opp }); + } + } + } + //Holonomy::view_seamless_parameterization(simplified_V, F_tri, uv_tri, FT_tri, "cut"); + //Holonomy::view_seamless_parameterization(V_tri, FT_tri, uv_tri, FT_tri, "cut"); + + Eigen::MatrixXi EE(edge_pairings.size(), 4); + for (int i = 0; i < edge_pairings.size(); ++i) { + EE.row(i) << edge_pairings[i][0], edge_pairings[i][1], edge_pairings[i][2], edge_pairings[i][3]; + } + + if (output_dir != "") + { + std::string output_filename = join_path(output_dir, name + "_initial_coarse_mesh.obj"); + write_obj_with_uv(output_filename, simplified_V, F_tri, uv_tri, FT_tri); + } + + //MeshCutter meshcutter(simplified_V, uv_tri, F_tri, FT_tri); + //auto [V_tri, EE] = meshcutter.cut_mesh(); + spdlog::info("Initializing optimization"); + SymDir::ExtremeOpt extremeopt(V_tri, FT_tri); + + spdlog::info("Reading parameters from file"); + std::ifstream js_in(symdir_json); + nlohmann::json config = nlohmann::json::parse(js_in); + SymDir::Parameters param; + param = read_parameters(config); + //param.diff_err = 1e-4; + //param.grad_abs_err = 1e-4; + //param.energy_diff_converge = false; + extremeopt.m_params = param; + + spdlog::info("Creating optimization mesh"); + extremeopt.create_mesh(V_tri, FT_tri, uv_tri); + extremeopt.set_v_map(F_tri, FT_tri); + + spdlog::info("Initializing constraints"); + std::vector> EE_e = transform_EE(FT_tri, EE); + extremeopt.init_constraints(EE_e); + extremeopt.EE = EE; + + // optionally align layout to field + int phase = 0; + bool do_align_layout_to_field = true; + if (do_align_layout_to_field) + { + Eigen::MatrixXd field_direction = transfer_original_field_to_overlay(); + + bool optimize_coarse_alignment = true; + if (optimize_coarse_alignment) + { + // causes consistency issues with fine field rotation + auto [PD1, PD2] = transfer_overlay_field_to_simplified(field_direction); + std::tie(phase, extremeopt.PD1, extremeopt.PD2) = Field::maximize_combed_frame_alignment( + simplified_V, F_tri, uv_tri, FT_tri, PD1, PD2); + } + else + { + std::tie(extremeopt.PD1, extremeopt.PD2) = transfer_overlay_field_to_simplified(field_direction); + } + + // save combed field + // generate frame field geometry from cross field + Eigen::MatrixXd V_o = generate_overlay_vertices(V_orig, vtx_reindex_orig, orig); + std::vector V_r(V_o.rows()); + for (int vi = 0; vi < V_o.rows(); ++vi) + { + V_r[vi] = V_o.row(vi); + } + int num_faces = field_direction.rows(); + PD1_o.resize(num_faces); + PD2_o.resize(num_faces); + Eigen::Matrix2d R = generate_ccw_rotation(); + for (int fi = 0; fi < num_faces; ++fi) + { + // get initial direction + PD1_o[fi] = field_direction.row(fi); + + // align phase with combed field + for (int i = 0; i < phase; ++i) + { + PD1_o[fi] = rotate_in_face_plane(*this, V_r, R, PD1_o[fi], fi); + } + + // v gradient target is one more rotation + PD2_o[fi] = rotate_in_face_plane(*this, V_r, R, PD1_o[fi], fi); + } + + std::string field_filename = join_path(output_dir, name + "_coarse.cfield"); + spdlog::info("writing field to {}", field_filename); + Field::write_combed_field(field_filename, extremeopt.PD1, extremeopt.PD2); + } + else + { + param.alignment_weight = 0.; + } + + spdlog::info("Optimizing layout"); + extremeopt.do_optimization_without_log(); + extremeopt.export_mesh(V_tri, FT_tri, uv_tri); + + //Holonomy::view_seamless_parameterization(V_tri, FT_tri, uv_tri, FT_tri, "optimized"); + if (output_dir != "") + { + std::string output_filename = join_path(output_dir, name + "_coarse.obj"); + write_obj_with_uv(output_filename, simplified_V, F_tri, uv_tri, FT_tri); + } + + // copy back optimized uv coordinates + for (int f = 0; f < num_faces; ++f) + { + for (int i = 0; i < 3; ++i) + { + int hij = F_halfedge(f, i); + u_simp[hij] = uv_tri(cut_vertex_indices[hij], 0); + v_simp[hij] = uv_tri(cut_vertex_indices[hij], 1); + } + } +} + +void VoronoiOverlayMesh::parameterize_with_overlay( + const std::vector>& homology_basis_loops, + const std::vector& kappa +) +{ + assert(is_valid()); + + // create parametrization for the simplified mesh with Penner coordinates + timer.start(); + auto [V_overlay, m_o] = generate_simplified_parameterization(homology_basis_loops, kappa); + simplified_V = V_overlay; + log["4.1-param-penner-time"] = timer.getElapsedTime(); + + // find coarse mesh layout + timer.start(); + spdlog::info("generating coarse layout"); + auto &mc = m_o.cmesh(); + std::vector u_vec(mc.n_vertices(), 0.); + auto layout_res = get_consistent_layout(simp.m, m_o); + auto m_tri = std::get<0>(layout_res); + u_simp = std::get<1>(layout_res); + v_simp = std::get<2>(layout_res); + is_cut_simp = std::get<3>(layout_res); + + // convert layout to a matrix + Eigen::MatrixXd uv_s(u_simp.size(), 2); + for (int i = 0; i < u_simp.size(); ++i) + { + uv_s(i, 0) = u_simp[i]; + uv_s(i, 1) = v_simp[i]; + if (isnan(u_simp[i])) spdlog::error("nan in layout"); + if (isnan(v_simp[i])) spdlog::error("nan in layout"); + } + + // check layout error + auto [length_error, angle_error] = compute_seamless_error(m_tri, uv_s); + spdlog::info("coarse length error is {}", length_error.maxCoeff()); + spdlog::info("coarse angle error is {}", angle_error.maxCoeff()); + + // get face map from overlay to original mesh + std::vector fn_to_f; + build_face_maps(m_o, fn_to_f); + + // (optionally) simplify the parametrization connectivity before refining + // WARNING: currently does not support field transfer + if (simplify_param_overlay) + { + spdlog::warn("not implemented for field transfer; need to transfer cut information"); + + // copy parametrization to vector of points + std::vector uv_simp(u_simp.size()); + for (int i = 0; i < u_simp.size(); ++i) + { + uv_simp[i] = {u_simp[i], v_simp[i]}; + } + + // build simplifier data structure and use to simplify + OverlaySimplifier simplifier(m_o, simp.m, is_cut_simp, uv_simp); + simplifier.refine_halfedges(); + std::tie (m_o, fn_to_f, uv_simp) = simplifier.get_refined_mesh(); + + // copy simplified connectivity back + m_tri = m_o.cmesh(); + u_simp.resize(uv_simp.size()); + v_simp.resize(uv_simp.size()); + for (int i = 0; i < uv_simp.size(); ++i) + { + u_simp[i] = uv_simp[i][0]; + v_simp[i] = uv_simp[i][1]; + } + } + + // get edge map for overlay triangulation + std::vector h2e_tri, e2h_tri; + build_edge_maps(m_tri.opp, h2e_tri, e2h_tri); + if (false) + view_halfedge_mesh_layout(m_tri, u_simp, v_simp); + + // check some basic diagnostics + bool check_validity = false; + if (check_validity) + { + VectorX he2angle, he2cot; + corner_angles(m_tri, he2angle, he2cot); + spdlog::info("min angle in coarse overlay: {}", he2angle.minCoeff()); + } + spdlog::info("original coarse mesh has {} halfedges", simp.m.n_halfedges()); + spdlog::info("param overlay mesh has {} halfedges", m_o.n_halfedges()); + spdlog::info("triangulated param mesh has {} halfedges", m_tri.n_halfedges()); + + // build map from vertices to original halfedge originating from them + std::vector first_segment(simp.m.n_halfedges(), -1); + for (int hij = 0; hij < m_o.n_halfedges(); ++hij) + { + // only look for original edges + if (m_o.edge_type[hij] == CURRENT_EDGE) + continue; + + // only look for segment vertices + int vi = m_o.to[m_o.opp[hij]]; + int vj = m_o.to[hij]; + + int Hij = m_o.origin_of_origin[hij]; + if (m_o.edge_type[hij] == ORIGINAL_EDGE) + Hij = m_o.origin[hij]; + if (m_o.vertex_type[vi] == ORIGINAL_VERTEX) + first_segment[Hij] = hij; + } + + // step 1: refine simplified mesh halfedges at intersection points + // the vertices are inserted (approximately) uniformly on the simplified edge path + spdlog::info("splitting original overlay edges"); + std::vector start_halfedge = find_start_halfedges(simp); + std::vector v2v(m_o.n_vertices(), -1); + simp.vertex_index.resize(m_o.n_vertices()); + for (int hij = 0; hij < simp.m.n_halfedges(); ++hij) + { + // only process simplified edge once + if (hij < simp.m.opp[hij]) continue; + spdlog::debug("tracing original edge {}, {}", hij, simp.m.opp[hij]); + + // get the length of the edge and the total length of the path on the overlay + int h_curr = start_halfedge[hij]; + int h = first_segment[hij]; + Scalar edge_length = m_tri.l[h]; + int num_segments = 1; + while (m_o.vertex_type[m_o.to[h]] != ORIGINAL_VERTEX) + { + h = next_orig_segment(m_o, h); + edge_length += m_tri.l[h]; + ++num_segments; + } + h = first_segment[hij]; + Scalar delta = 1. / num_segments; + Scalar path_length = compute_simplified_halfedge_length(h_curr); + + // iterate over overlay edges to insert new vertices on the correct halfedge + v2v[m_o.to[m_o.opp[h]]] = to[opp[h_curr]]; + simp.vertex_index[m_o.to[m_o.opp[h]]] = to[opp[h_curr]]; + Scalar edge_tip_pos = m_tri.l[h]; // FIXME risky to use inconsistent m_o and m_tri + Scalar path_base_pos = 0.; + int count = 0; + while (m_o.vertex_type[m_o.to[h]] != ORIGINAL_VERTEX) + { + // uniform distributes intersections, but increases parameterization distortion + bool use_uniform_intersections = false; + + // compute target edge coordinate + Scalar edge_coord; + if (use_uniform_intersections) + { + edge_coord = (count + 1) * delta; + } + else + { + edge_coord = edge_tip_pos / edge_length; + } + spdlog::debug("tracing overlay edge {}, {} with target coordinate {}", h, m_o.opp[h], edge_coord); + + // increment path halfedge until approximately at the right halfedge + while (path_base_pos / path_length < edge_coord) + { + // stop if at last halfedge in path + if (simp.vertex_type[to[h_curr]] == VertexType::VV) + break; + + // set parent data for current edge and its tip + path_base_pos += halfedge_length(h_curr); + simp.halfedge_parent[h_curr] = h; + simp.halfedge_parent[opp[h_curr]] = m_o.opp[h]; + simp.vertex_parent[to[h_curr]] = h2e_tri[h]; + + // increment to next overlay edge + h_curr = get_next_overlay_edge(h_curr, simp); + assert(simp.edge_type[h2e[h_curr]] == EdgeType::EE); + } + + // split edge (while preserving triangular faces) + h_curr = split_edge(h_curr); + int h_next = n[h_curr]; // need to get before splitting + int h_opp = opp[n[h_curr]]; + split_face(h_curr, n[n[h_curr]]); + split_face(h_opp, n[n[h_opp]]); + + // set parent data for current edge + simp.halfedge_parent[h_curr] = h; + simp.halfedge_parent[opp[h_curr]] = m_o.opp[h]; + + // mark the vertex tip as an embedded vertex and update the vertex map + simp.vertex_type[to[h_curr]] = VertexType::VV; + simp.vertex_parent[to[h_curr]] = m_o.to[h]; + v2v[m_o.to[h]] = to[h_curr]; + simp.vertex_index[m_o.to[h]] = to[h_curr]; + spdlog::debug("new overlay vertex {} at {}", m_o.to[h], to[h_curr]); + + // increment both halfedges and lengths + path_base_pos += halfedge_length(h_curr); + h_curr = h_next; + h = next_orig_segment(m_o, h); + assert(m_o.edge_type[h] != CURRENT_EDGE); + edge_tip_pos += m_tri.l[h]; + ++count; + } + + // label parent of remaining edges + spdlog::debug("tracing remaining overlay edge {}, {}", h, m_o.opp[h]); + std::vector remaining_halfedges = enumerate_halfedges(h_curr, simp); + int num_remaining_halfedges = remaining_halfedges.size(); + for (int h_path : remaining_halfedges) + { + simp.halfedge_parent[h_path] = h; + simp.halfedge_parent[opp[h_path]] = m_o.opp[h]; + } + for (int i = 0; i < num_remaining_halfedges - 1; ++i) + { + int h_path = remaining_halfedges[i]; + simp.vertex_parent[to[h_path]] = h2e_tri[h]; // TODO not for last + } + } + + // restore regular triangulation + triangulate(); + make_regular(); + + // step 2: trace overlay edges not in the original mesh + // note that the overlay mesh is generally not a triangulation + spdlog::info("tracing current overlay edges"); + for (int hij = 0; hij < m_o.n_halfedges(); ++hij) + { + if (hij < m_o.opp[hij]) + continue; // only process once + if (m_o.edge_type[hij] != CURRENT_EDGE) + continue; // only add current edges that were not original edges + + // get endpoints of path to trace + spdlog::debug("tracing overlay edge {}, {}", hij, m_o.opp[hij]); + int vi = m_o.to[m_o.opp[hij]]; + int vj = m_o.to[hij]; + int Vi = v2v[vi]; + int Vj = v2v[vj]; + + // get embedded face to stay inside + int f = fn_to_f[m_o.f[hij]]; + + // trace path while staying in the correct embedded face + std::vector halfedges; + trace_path(Vi, Vj, hij, h2e_tri[hij], halfedges, f); + for (int h : halfedges) + { + simp.halfedge_parent[opp[h]] = m_o.opp[hij]; + assert(simp.face_parent[face[h]] == f); + } + + // maintain regularity + // TODO set the start halfedge of the opposite path + make_adjacent_regular(halfedges); + } + + // step 3: trace additional edges added to make the overlay a triangulation + spdlog::info("tracing current overlay edges {} to {}", m_o.n_halfedges(), m_tri.n_halfedges()); + for (int hij = m_o.n_halfedges(); hij < m_tri.n_halfedges(); ++hij) + { + if (hij < m_tri.opp[hij]) + continue; // only process once + + // get endpoints of path + spdlog::debug("tracing additional edge {}, {}", hij, m_tri.opp[hij]); + int vi = m_tri.to[m_tri.opp[hij]]; + int vj = m_tri.to[hij]; + int Vi = v2v[vi]; + int Vj = v2v[vj]; + + // get halfedge in triangulated face that is in the overlay mesh + int hjk = m_tri.n[hij]; + while (hjk >= m_o.n_halfedges()) + { + hjk = m_tri.n[hjk]; + } + + // get halfedge in the same overlay face that is an original edge + while (m_o.edge_type[hjk] == CURRENT_EDGE) + { + hjk = m_o.n[hjk]; + } + + // get face of embedded original mesh to trace path in + // TODO: should actually update face data and use overlay face labels + int f = simp.m.f[m_o.origin_of_origin[hjk]]; + if (m_o.edge_type[hjk] == ORIGINAL_EDGE) + f = simp.m.f[m_o.origin[hjk]]; + + // trace path while staying in the correct embedded face + std::vector halfedges; + trace_path(Vi, Vj, hij, h2e_tri[hij], halfedges, f); + for (int h : halfedges) + { + simp.halfedge_parent[opp[h]] = m_tri.opp[hij]; + assert(simp.face_parent[face[h]] == f); + } + + // maintain regularity + // TODO set the start halfedge of the opposite path + make_adjacent_regular(halfedges); + // TODO refine + } + + // set simplified mesh to overlay + simp.m = m_tri; + simp.h2e = h2e_tri; + simp.e2h = e2h_tri; + log["4.2-param-refine-time"] = timer.getElapsedTime(); +} + +std::tuple< + Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::MatrixXi, + std::vector, Eigen::MatrixXi, Eigen::MatrixXi +> +VoronoiOverlayMesh::transfer_simplified_parameterization() +{ + // first, ensure no redundancy + timer.start(); + flood_faces(); + remove_excess_refinement(); + log["4.3-param-simplify-time"] = timer.getElapsedTime(); + + // optionally retrace paths with geodesics + // TODO: move to the initial tracing + if (use_geodesic_refinement) + { + assert(HalfedgeMesh::is_valid()); + assert(is_simplicial()); + + timer.start(); + straighten_edges(); + log["4.4-param-retrace-time"] = timer.getElapsedTime(); + } + + timer.start(); + // perform additional refinement to the mesh so that the Tutte maps are not degenerate + if (make_overlay_three_connected) make_three_connected(orig); + if (make_simplified_three_connected) make_three_connected(simp); + + // (optionally) solve for barycentric coordiantes on the original mesh + if (use_uniform_overlay_edges) set_uniform_edge_coordinates(orig); + if (use_harmonic_overlay_vertices) find_tutte_coordinates(orig); + log["4.5-orig-tutte-time"] = timer.getElapsedTime(); + + // solve for barycentric coordiantes on the simplified mesh + spdlog::info("finding harmonic coordiantes"); + timer.start(); + set_uniform_edge_coordinates(simp); + find_tutte_coordinates(simp); + log["4.5-param-tutte-time"] = timer.getElapsedTime(); + + // (optionally) optimize the parametrization + if (optimize_layout) optimize_simplified_parameterization(); + + // lift layout to overlay + timer.start(); + he_uv.resize(n.size()); + assert(is_valid_edge_network()); + for (int hij = 0; hij < n.size(); ++hij) + { + he_uv[hij] = {0., 0.}; + if (is_halfedge_deleted(hij)) + continue; + int fijk = simp.face_parent[face[hij]]; + int hj = simp.m.h[fijk]; + int hk = simp.m.n[hj]; + int hi = simp.m.n[hk]; + assert(hj >= 0); + assert(hj < u_simp.size()); + + // create uv face vertices for the simplified triangle + BaryCoords Vi = {u_simp[hi], v_simp[hi]}; + BaryCoords Vj = {u_simp[hj], v_simp[hj]}; + BaryCoords Vk = {u_simp[hk], v_simp[hk]}; + + // get barycentric coordinates for the tip of the halfedge in the simplified triangle + BaryCoords bary_coords = tip_face_coordinates(hij, fijk, simp); + Scalar u = bary_coords[0]; + Scalar v = bary_coords[1]; + Scalar w = 1. - (u + v); + assert(u >= 0.); + assert(v >= 0.); + assert(w >= 0.); + + // set halfedge tip uv coordinates + he_uv[hij] = u * Vi + v * Vj + w * Vk; + assert(!isnan(he_uv[hij][0])); + assert(!isnan(he_uv[hij][1])); + assert(abs(he_uv[hij][0]) < 1e10); + assert(abs(he_uv[hij][1]) < 1e10); + } + + if (simplify_parameterization) + { + // create parametrized mesh that only refines edges where this is necessary + spdlog::info("Simplifying parameterization"); + std::vector is_cut_orig = compute_layout(); + SimplificationOverlaySimplifier simplifier(*this, is_cut_orig, he_uv); + simplifier.refine_halfedges(); + auto [V_r, F_r, uv_r, FT_r, fn_to_f_r, hn_to_h_r, is_edge] = simplifier.build_refined_mesh(); + + log["4.6-param-vf-time"] = timer.getElapsedTime(); + + return std::make_tuple(V_r, F_r, uv_r, FT_r, fn_to_f_r, hn_to_h_r, is_edge); + } + else + { + spdlog::info("Skipping parameterization simplification"); + + // get reduced indices + auto [h_new2old, h_old2new] = get_halfedge_reductions(); + auto he_uv_r = vector_compose(he_uv, h_new2old); + auto [opp_r, n_r, prev_r] = reduce_halfedge_maps(); + auto [face_r, f2h_r] = reduce_face_maps(); + auto [to_r, out_r] = reduce_vertex_maps(); + int num_faces = f2h_r.size(); + + // find vertices for the uv topology with cut to disk + UnionFind cut_vertices(h_new2old.size()); + for (int hij = 0; hij < h_new2old.size(); ++hij) + { + // union halfedges that are not on the layout cut + if (simp.edge_type[h2e[h_new2old[hij]]] == EdgeType::EE) + { + int h_o = simp.halfedge_parent[h_new2old[hij]]; + if (is_cut_simp[h_o]) + continue; + } + + // get halfedge in adjacent uncut face pointing to vj + int hkj = prev_r[opp_r[hij]]; + cut_vertices.union_sets(hij, hkj); + } + int num_cut_vertices = cut_vertices.count_sets(); + std::vector cut_vertex_indices = cut_vertices.index_sets(); + + // build VF connectivity + Eigen::MatrixXd V_o = generate_overlay_vertices(V_orig, vtx_reindex_orig, orig); + Eigen::MatrixXi F(num_faces, 3); + Eigen::MatrixXi F_uv(num_faces, 3); + Eigen::MatrixXd uv(num_cut_vertices, 2); + for (int f = 0; f < num_faces; ++f) + { + int hij = f2h_r[f]; + int hjk = n_r[hij]; + for (int i = 0; i < 3; ++i) + { + uv(cut_vertex_indices[hij], 0) = (double)(he_uv_r[hij][0]); + uv(cut_vertex_indices[hij], 1) = (double)(he_uv_r[hij][1]); + F(f, i) = to_r[hij]; + F_uv(f, i) = cut_vertex_indices[hij]; + + // increment halfedge + hij = n_r[hij]; + } + } + log["4.6-param-vf-time"] = timer.getElapsedTime(); + std::vector face_parent_o; + Eigen::MatrixXi halfedge_parent, is_edge; + + return std::make_tuple(V_o, F, uv, F_uv, face_parent_o, halfedge_parent, is_edge); + } +} + +std::tuple +VoronoiOverlayMesh::generate_voronoi_boundary( + const Eigen::MatrixXd& V, + const std::vector& vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + int num_edges = e2h.size(); + std::vector> edges; + edges.reserve(count_edges()); + for (int eij = 0; eij < num_edges; ++eij) + { + // only add edges contained in simpified mesh edges + if (is_edge_deleted(eij)) continue; + if (voronoi.edge_type[eij] == EdgeType::EF) continue; + + int hij = e2h[eij]; + int vi = v_old2new[to[opp[hij]]]; + int vj = v_old2new[to[hij]]; + edges.push_back({vi, vj}); + } + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + auto [V_s, edges_s] = remove_unreferenced_vertices(V_o, edges); + Eigen::MatrixXi E_s(edges_s.size(), 2); + for (int i = 0; i < edges_s.size(); ++i) + { + E_s(i, 0) = edges_s[i][0]; + E_s(i, 1) = edges_s[i][1]; + } + + return std::make_tuple(V_s, E_s); +} + +void VoronoiOverlayMesh::PathTracerList::initialize() +{ + // Initialize an array to keep track of vertices + int num_vertices = parent.out.size(); + m_is_processed_vertex = std::vector(num_vertices, false); + m_halfedge_from_vertex = std::vector(num_vertices, -1); + + // Initialize vertex cost with value above maximum possible weight + m_vertex_cost = std::vector(num_vertices, inf_cost); + m_vertices_to_process = PriorityQueue(); +} + +void VoronoiOverlayMesh::PathTracerCommonList::initialize() +{ + // Initialize an array to keep track of vertices + int num_vertices = parent.out.size(); + m_is_processed_vertex.resize(num_vertices, false); + m_halfedge_from_vertex.resize(num_vertices, -1); + + // Initialize vertex cost with value above maximum possible weight + m_vertex_cost.resize(num_vertices, inf_cost); + m_vertices_to_process = PriorityQueue(); + m_modified_vertices.clear(); + assert(m_vertex_cost[argmin(m_vertex_cost)] == inf_cost); + assert(m_halfedge_from_vertex[argmax(m_halfedge_from_vertex)] == -1); +} + +void VoronoiOverlayMesh::PathTracerCommonList::clean() +{ + // Initialize an array to keep track of vertices + for (int vi : m_modified_vertices) + { + m_is_processed_vertex[vi] = false; + m_halfedge_from_vertex[vi] = -1; + m_vertex_cost[vi] = inf_cost; + } +} + +bool VoronoiOverlayMesh::PathTracer::trace_path( + int first_vertex, + int second_vertex, + int halfedge_index, + int edge_index, + std::vector &halfedges, + int face_index) +{ + // nothing to do if same vertex + if (first_vertex == second_vertex) return true; + + // check if valid start + assert(parent.is_valid_vertex_index(first_vertex)); + assert(parent.is_valid_vertex_index(second_vertex)); + if (parent.is_vertex_deleted(first_vertex)) + { + spdlog::error("tracing from deleted vertex"); + return false; + } + + initialize(); + + // Mark root with 0 (or cost lower than other edges for all negative weights) + set_vertex_cost(first_vertex, 0.); + add_vertex_to_queue(first_vertex); + + // Perform Dijkstra algorithm + bool vertex_found = false; + bool terminate_early = false; + Scalar min_cost = inf_cost; // keep track of best cost so far + while (!is_vertex_queue_empty()) + { + // Get the next vertex to process + auto [vi, vi_cost] = get_vertex_to_process(); + + // Skip already processed vertices + if (is_processed_vertex(vi)) + continue; + mark_processed_vertex(vi); + + // Iterate over the vertex circulator via halfedges + int h_start = parent.out[vi]; + int hij = h_start; + do + { + // Get the vertex in the one ring at the tip of the halfedge + int vj = parent.to[hij]; + + // Get candidate edge cost (either path length or edge weight) + Scalar candidate_cost = parent.halfedge_length(hij) + get_vertex_cost(vi); + + bool is_invalid = false; + is_invalid = (is_invalid) || (is_processed_vertex(vj)); // don't process same vertex twice + is_invalid = (is_invalid) || (parent.simp.edge_type[parent.h2e[hij]] != EdgeType::EF); // don't use traced edges + is_invalid = (is_invalid) || (parent.voronoi.edge_type[parent.h2e[hij]] != EdgeType::EF); // don't cross voronoi boundaries + is_invalid = (is_invalid) || (get_vertex_cost(vj) < candidate_cost); // don't use longer paths + is_invalid = (is_invalid) || (min_cost < candidate_cost); // don't use longer paths + + if (face_index >= 0) + { + is_invalid = (is_invalid) || (parent.simp.halfedge_parent[hij] != face_index); // stay in the target face + } + + // if processing edge from the first vertex, check for consistent cyclic orientation + if (vi == first_vertex) + { + is_invalid = (is_invalid) || (!parent.is_start_cyclic_orientation(hij, halfedge_index)); // maintain consistent orientation + } + + // if processing edge to the second vertex, check for consistent cyclic orientation + if (vj == second_vertex) + { + is_invalid = (is_invalid) || (!parent.is_end_cyclic_orientation(hij, halfedge_index)); // maintain consistent orientation + } + // only allow intersections at first and last vertex + if (vj != second_vertex) + { + is_invalid = (is_invalid) || (parent.simp.vertex_type[vj] != VertexType::VF); // don't use traced vertices + is_invalid = (is_invalid) || (parent.voronoi.vertex_type[vj] != VertexType::VF); // don't use voronoi vertices + } + + // add current candidate to record if it is valid + if (!is_invalid) + { + set_halfedge_from_vertex(vj, hij); + set_vertex_cost(vj, candidate_cost); + add_vertex_to_queue(vj); + if (vj == second_vertex) + { + vertex_found = true; + min_cost = min(min_cost, candidate_cost); + } + } + + // Progress to the next halfedge in the vertex circulator + hij = parent.opp[parent.prev[hij]]; + } while (hij != h_start); + + if ((vertex_found) && terminate_early) + break; + } + + if (get_vertex_cost(second_vertex) == inf_cost) + { + spdlog::error("could not find path from {} to {}", first_vertex, second_vertex); + + if (show_failure) + { +#ifdef ENABLE_VISUALIZATION + parent.view(); + std::string mesh_handle = "overlay mesh"; + auto [v_new2old, v_old2new] = parent.get_vertex_reductions(); + std::vector cost(v_new2old.size()); + for (int i = 0; i < v_new2old.size(); ++i) + { + cost[i] = get_vertex_cost(v_new2old[i]); + } + std::vector traced_vertices(v_new2old.size(), 0.); + traced_vertices[v_old2new[first_vertex]] = 1.; + traced_vertices[v_old2new[second_vertex]] = 2.; + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("cost", cost); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("path vertices", traced_vertices); + polyscope::show(); +#endif + } + + clean(); + return false; + } + + // trace back path and mark all edges and vertex parents + int hjk = get_halfedge_from_vertex(second_vertex); + // start_halfedge[m.opp[halfedge_index]] = opp[hjk]; + while (true) + { + // mark edge parent + int ejk = parent.h2e[hjk]; + parent.simp.edge_type[ejk] = EdgeType::EE; + parent.simp.halfedge_parent[hjk] = halfedge_index; + // TODO Make proper edge index + + // add edges to list + halfedges.push_back(hjk); + + // check if at end + int vj = parent.to[parent.opp[hjk]]; + if (vj == first_vertex) + { + // start_halfedge[halfedge_index] = hjk; + break; + } + // TODO: barycentric coordinates + + // iterate if still interior + parent.simp.vertex_type[vj] = VertexType::VE; + parent.simp.vertex_parent[vj] = edge_index; + hjk = get_halfedge_from_vertex(vj); + } + + clean(); + return true; +} + +bool VoronoiOverlayMesh::trace_path( + int first_vertex, + int second_vertex, + int halfedge_index, + int edge_index, + std::vector &halfedges, + int face_index) +{ + path_tracer.show_failure = show_trace_failure; + return path_tracer.trace_path(first_vertex, second_vertex, halfedge_index, edge_index, halfedges, face_index); +} + +void VoronoiOverlayMesh::add_voronoi_edges_to_viewer( + const Eigen::MatrixXd &V, + const std::vector &vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + int num_edges = e2h.size(); + std::vector> edges; + std::vector parent; + edges.reserve(count_edges()); + for (int eij = 0; eij < num_edges; ++eij) + { + // only add edges contained in simpified mesh edges + if (is_edge_deleted(eij)) + continue; + if (voronoi.edge_type[eij] != EdgeType::EE) + continue; + + int hij = e2h[eij]; + int vi = v_old2new[to[opp[hij]]]; + int vj = v_old2new[to[hij]]; + edges.push_back({vi, vj}); + parent.push_back(orig.halfedge_parent[hij]); // TODO: get edge in original mesh + } + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + auto [V_s, edges_s] = remove_unreferenced_vertices(V_o, edges); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + std::string mesh_handle; + mesh_handle = "voronoi edges"; + polyscope::registerCurveNetwork(mesh_handle, V_s, edges_s) + ->setColor(BLACK) + ->setRadius(0.0005); + polyscope::getCurveNetwork(mesh_handle)->addEdgeScalarQuantity("edge parent", parent); +#endif +} + +void VoronoiOverlayMesh::add_voronoi_points_to_viewer( + const Eigen::MatrixXd &V, + const std::vector &vtx_reindex) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + + std::vector dual_vertices_new = vector_compose(v_old2new, voronoi.dual_vertices); + std::vector dual_edge_midpoints_new = vector_compose(v_old2new, voronoi.dual_edge_midpoints); + Eigen::MatrixXd V_o = generate_overlay_vertices(V, vtx_reindex, orig); + Eigen::MatrixXd V_vertex = subset_vertices(V_o, dual_vertices_new); + Eigen::MatrixXd V_edge = subset_vertices(V_o, dual_edge_midpoints_new); + +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerPointCloud("dual vertices", V_vertex); + polyscope::registerPointCloud("dual edge midpoints", V_edge); +#endif +} + +void VoronoiOverlayMesh::add_voronoi_to_viewer( + const std::string &mesh_handle, + const VoronoiStructure &voronoi_structure) const +{ + auto [v_new2old, v_old2new] = get_vertex_reductions(); + auto [f_new2old, f_old2new] = get_face_reductions(); + std::vector vertex_region = vector_compose(voronoi_structure.vertex_region, v_new2old); + std::vector face_region = vector_compose(voronoi_structure.face_region, f_new2old); + std::vector vertex_type = vector_compose(voronoi_structure.vertex_type, v_new2old); + std::vector vertex_enum = enumerate_vertex_type(vertex_type); + std::vector distance = vector_compose(voronoi_structure.distance, v_new2old); + std::vector shuffled_face_region = shuffle_map_image(face_region); +#ifdef ENABLE_VISUALIZATION + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("distance", distance); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("vertex region", vertex_region); + polyscope::getSurfaceMesh(mesh_handle)->addVertexScalarQuantity("vertex type", vertex_enum); + polyscope::getSurfaceMesh(mesh_handle)->addFaceScalarQuantity("face region", face_region); + polyscope::getSurfaceMesh(mesh_handle)->addFaceScalarQuantity("shuffled face region", shuffled_face_region); +#endif +} + +void VoronoiOverlayMesh::view() const +{ + std::string mesh_handle = "overlay mesh"; + add_original_to_viewer(V_orig, vtx_reindex_orig); + add_mesh_to_viewer(V_orig, vtx_reindex_orig); + add_simplified_vertices_to_viewer(V_orig, vtx_reindex_orig); + add_simplified_edges_to_viewer(V_orig, vtx_reindex_orig); + if (voronoi.mesh.n.size() > 0) // TODO + { + add_voronoi_edges_to_viewer(V_orig, vtx_reindex_orig); + add_voronoi_points_to_viewer(V_orig, vtx_reindex_orig); + add_voronoi_to_viewer(mesh_handle, voronoi); + } +#ifdef ENABLE_VISUALIZATION + polyscope::show(); +#endif +} + +void VoronoiOverlayMesh::add_center_to_face_voronoi( + int vertex_index, + int face_index, + VoronoiStructure &voronoi_structure) +{ + int f_parent = voronoi_structure.face_region[face_index]; + voronoi_structure.vertex_type[vertex_index] = VertexType::VF; + voronoi_structure.vertex_region[vertex_index] = f_parent; + int hij = out[vertex_index]; + voronoi_structure.distance[vertex_index] = voronoi_structure.distance[to[hij]]; + int hik = hij; + do + { + int eik = h2e[hik]; + voronoi_structure.edge_type[eik] = EdgeType::EF; + voronoi_structure.edge_region[eik] = f_parent; + voronoi_structure.face_region[face[hik]] = f_parent; + voronoi_structure.distance[vertex_index] = max( + voronoi_structure.distance[to[hij]], + voronoi_structure.distance[vertex_index]); + hik = opp[prev[hik]]; + } while (hik != hij); +} + +void VoronoiOverlayMesh::split_edge_voronoi( + int halfedge_index, + int edge_index, + VoronoiStructure &voronoi_structure) +{ + int hij = halfedge_index; + EdgeType e_type = voronoi_structure.edge_type[edge_index]; + int e_parent = voronoi_structure.edge_region[edge_index]; + voronoi_structure.edge_type[h2e[hij]] = e_type; + voronoi_structure.edge_type[h2e[n[hij]]] = e_type; + voronoi_structure.edge_region[h2e[hij]] = e_parent; + voronoi_structure.edge_region[h2e[n[hij]]] = e_parent; + int vj = to[hij]; + int vi = to[opp[hij]]; + int vk = to[n[hij]]; + voronoi_structure.vertex_region[vj] = e_parent; + voronoi_structure.vertex_type[vj] = (e_type == EdgeType::EE) ? VertexType::VE : VertexType::VF; + voronoi_structure.distance[vj] = max(voronoi_structure.distance[vi], voronoi_structure.distance[vk]); +} + +int VoronoiOverlayMesh::split_face_voronoi( + int e, + int face_index, + VoronoiStructure &voronoi_structure) +{ + int f_parent = voronoi_structure.face_region[face_index]; + voronoi_structure.edge_type[e] = EdgeType::EF; + int hij = e2h[e]; + int hji = opp[hij]; + voronoi_structure.face_region[face[hij]] = f_parent; + voronoi_structure.face_region[face[hji]] = f_parent; + + return e; +} + +void VoronoiOverlayMesh::collect_garbage() +{ + if (skip_garbage_collection) return; + + // get element reindex maps + auto [he_new2old, he_old2new] = get_halfedge_reductions(); + auto [v_new2old, v_old2new] = get_vertex_reductions(); + auto [e_new2old, e_old2new] = get_edge_reductions(); + auto [f_new2old, f_old2new] = get_face_reductions(); + + // collect voronoi garbage + voronoi.collect_garbage(he_new2old, v_new2old, e_new2old, f_new2old, he_old2new, v_old2new); + + // do here to avoid redundant map computation + orig.collect_garbage(he_new2old, v_new2old, e_new2old, f_new2old); + simp.collect_garbage(he_new2old, v_new2old, e_new2old, f_new2old); + HalfedgeMesh::collect_garbage( + he_new2old, + e_new2old, + v_new2old, + f_new2old, + he_old2new, + e_old2new, + v_old2new, + f_old2new); +} + +// WARNING: not debugged yet +void VoronoiOverlayMesh::parameterize_with_layout( + const std::vector>& homology_basis_loops, + const std::vector& kappa +) +{ + assert(is_valid()); + timer.start(); + if (!use_zero_lengths) + { + set_simplified_halfedge_extrinsic_lengths(); + correct_simplified_halfedge_lengths(); + } + auto [V_overlay, m_o] = generate_simplified_parameterization(homology_basis_loops, kappa); + log["4.1-param-penner-time"] = timer.getElapsedTime(); + + // find coarse mesh layout + timer.start(); + spdlog::info("generating coarse layout"); + auto &mc = m_o.cmesh(); + std::vector u_vec(mc.n_vertices(), 0.); + auto layout_res = get_consistent_layout(simp.m, m_o); + auto m_tri = std::get<0>(layout_res); + u_simp = std::get<4>(layout_res); + v_simp = std::get<5>(layout_res); + is_cut_simp = std::get<6>(layout_res); + assert(u_simp.size() == mc.n_halfedges()); + assert(v_simp.size() == mc.n_halfedges()); + assert(is_cut_simp.size() == mc.n_halfedges()); + + // get face map + std::vector fn_to_f; + build_face_maps(m_o, fn_to_f); + + // get halfedge maps for current mesh + std::vector h2e_tri, e2h_tri; + build_edge_maps(mc.opp, h2e_tri, e2h_tri); + + if (false) + view_halfedge_mesh_layout(mc, u_simp, v_simp); + + bool check_validity = true; + if (check_validity) + { + VectorX he2angle, he2cot; + corner_angles(m_tri, he2angle, he2cot); + spdlog::info("min angle in coarse overlay: {}", he2angle.minCoeff()); + } + spdlog::info("original coarse mesh has {} halfedges", simp.m.n_halfedges()); + spdlog::info("param overlay mesh has {} halfedges", m_o.n_halfedges()); + spdlog::info("triangulated param mesh has {} halfedges", m_tri.n_halfedges()); + + // build map from vertices to original halfedge pointing to them + std::vector first_segment(simp.m.n_halfedges(), -1); + for (int hij = 0; hij < m_o.n_halfedges(); ++hij) + { + // only look for original edges + if (m_o.edge_type[hij] == CURRENT_EDGE) + continue; + + // only look for segment vertices + int vi = m_o.to[m_o.opp[hij]]; + int vj = m_o.to[hij]; + + int Hij = m_o.origin_of_origin[hij]; + if (m_o.edge_type[hij] == ORIGINAL_EDGE) + Hij = m_o.origin[hij]; + if (m_o.vertex_type[vi] == ORIGINAL_VERTEX) + first_segment[Hij] = hij; + } + + // Refine halfedges + std::vector start_halfedge = find_start_halfedges(simp); + spdlog::info("splitting original overlay edges"); + std::vector v2v(m_o.n_vertices(), -1); + std::vector original_edges = {}; // record of edges to delete + std::vector original_vertices = {}; // record of edges to delete + for (int hij = 0; hij < simp.m.n_halfedges(); ++hij) + { + if (hij < simp.m.opp[hij]) + continue; // only process once + spdlog::debug("tracing original edge {}, {}", hij, simp.m.opp[hij]); + + // get the length of the edge and the total length of the path on the overlay + int h_curr = start_halfedge[hij]; + int h = first_segment[hij]; + Scalar edge_length = m_tri.l[h]; + int num_segments = 1; + while (m_o.vertex_type[m_o.to[h]] != ORIGINAL_VERTEX) + { + h = next_orig_segment(m_o, h); + edge_length += m_tri.l[h]; + ++num_segments; + } + h = first_segment[hij]; + Scalar delta = 1. / num_segments; + Scalar path_length = compute_simplified_halfedge_length(h_curr); + + // iterate over overlay edges to insert new vertices on the correct halfedge + v2v[m_o.to[m_o.opp[h]]] = to[opp[h_curr]]; + Scalar edge_tip_pos = m_tri.l[h]; // FIXME risky to use inconsistent m_o and m_tri + Scalar path_base_pos = 0.; + int count = 0; + while (m_o.vertex_type[m_o.to[h]] != ORIGINAL_VERTEX) + { + // uniform distributes intersections, but increases parameterization distortion + bool use_uniform_intersections = false; + + // compute target edge coordinate + Scalar edge_coord; + if (use_uniform_intersections) + { + edge_coord = (count + 1) * delta; + } + else + { + edge_coord = edge_tip_pos / edge_length; + } + spdlog::debug("tracing overlay edge {}, {} with target coordinate {}", h, m_o.opp[h], edge_coord); + + // increment path halfedge until approximately at the right halfedge + while (path_base_pos / path_length < edge_coord) + { + // stop if at last halfedge in path + if (simp.vertex_type[to[h_curr]] == VertexType::VV) + break; + + // increment to next overlay edge otherwise + path_base_pos += halfedge_length(h_curr); + original_edges.push_back(h2e[h_curr]); + original_vertices.push_back(to[h_curr]); + assert(m_o.edge_type[h] == ORIGINAL_EDGE); + + h_curr = get_next_overlay_edge(h_curr, simp); + assert(simp.edge_type[h2e[h_curr]] == EdgeType::EE); + } + + // split edge and set endpoint vertex + h_curr = split_edge(h_curr); + int h_next = n[h_curr]; // need to get before splitting + int h_opp = opp[n[h_curr]]; + split_face(h_curr, n[n[h_curr]]); + split_face(h_opp, n[n[h_opp]]); + + original_edges.push_back(h2e[h_curr]); + assert(m_o.edge_type[h] == ORIGINAL_EDGE); + v2v[m_o.to[h]] = to[h_curr]; + spdlog::debug("new overlay vertex {} at {}", m_o.to[h], to[h_curr]); + + // increment both halfedges and lengths + path_base_pos += halfedge_length(h_curr); + h_curr = h_next; + h = next_orig_segment(m_o, h); + assert(m_o.edge_type[h] != CURRENT_EDGE); + edge_tip_pos += m_tri.l[h]; + ++count; + } + // start_halfedge[h] = h_curr; + // start_halfedge[m_o.opp[h]] = -1; // get last halfedge in chain + + // label parent of remaining edges + spdlog::debug("tracing remaining overlay edge {}, {}", h, m_o.opp[h]); + std::vector remaining_halfedges = enumerate_halfedges(h_curr, simp); + int num_remaining_halfedges = remaining_halfedges.size(); + for (int h_path : remaining_halfedges) + { + if (m_o.edge_type[h] == ORIGINAL_EDGE) + { + original_edges.push_back(h2e[h_path]); + } + else + { + simp.halfedge_parent[h_path] = m_o.origin[h]; + simp.halfedge_parent[opp[h_path]] = m_o.origin[m_o.opp[h]]; + } + } + for (int i = 0; i < num_remaining_halfedges - 1; ++i) + { + int h_path = remaining_halfedges[i]; + if (m_o.edge_type[h] == ORIGINAL_EDGE) + { + original_vertices.push_back(to[h_path]); + } + else + { + simp.vertex_parent[to[h_path]] = h2e_tri[m_o.origin[h]]; // TODO not for last + } + } + } + + // restore regular triangulation + triangulate(); + make_regular(); + + // trace current edges not in the original mesh + spdlog::info("tracing current overlay edges"); + for (int hij = 0; hij < m_o.n_halfedges(); ++hij) + { + if (hij < m_o.opp[hij]) + continue; // only process once + if (m_o.edge_type[hij] != CURRENT_EDGE) + continue; // only add current edges + spdlog::debug("tracing overlay edge {}, {}", hij, m_o.opp[hij]); + int vi = m_o.to[m_o.opp[hij]]; + int vj = m_o.to[hij]; + int Vi = v2v[vi]; + int Vj = v2v[vj]; + + int f = fn_to_f[m_o.f[hij]]; + + std::vector halfedges; + trace_path(Vi, Vj, m_o.origin[hij], h2e_tri[m_o.origin[hij]], halfedges, f); + for (int h : halfedges) + { + simp.halfedge_parent[opp[h]] = m_o.origin[m_o.opp[hij]]; + assert(simp.face_parent[face[h]] == f); + // if (simp.face_parent[face[h]] != f) spdlog::error("traced edge in wrong face"); + } + + // set vertices if not set yet + if (simp.vertex_type[Vi] == VertexType::VE) + { + simp.vertex_parent[Vi] = h2e_tri[m_o.origin[hij]]; + } + if (simp.vertex_type[Vj] == VertexType::VE) + { + simp.vertex_parent[Vj] = h2e_tri[m_o.origin[hij]]; + } + // TODO set the start halfedge of the opposite path + make_adjacent_regular(halfedges); + // TODO refine + } + + // remove original edges + for (int e : original_edges) + { + simp.edge_type[e] = EdgeType::EF; + } + for (int v : original_vertices) + { + simp.vertex_type[v] = VertexType::VF; + } + + // set simplified mesh to overlay + simp.m = mc; + simp.h2e = h2e_tri; + simp.e2h = e2h_tri; + log["4.2-param-refine-time"] = timer.getElapsedTime(); +} + + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/interface.cpp b/src/resolution/interface.cpp new file mode 100644 index 0000000..56b130b --- /dev/null +++ b/src/resolution/interface.cpp @@ -0,0 +1,9 @@ +#include "resolution/interface.h" + +namespace Penner { +namespace Resolution { + + + +} +} \ No newline at end of file diff --git a/src/resolution/optimization/compression.cpp b/src/resolution/optimization/compression.cpp new file mode 100644 index 0000000..779babd --- /dev/null +++ b/src/resolution/optimization/compression.cpp @@ -0,0 +1,767 @@ +#include "resolution/optimization/compression.h" + + +#define IGL_STB_IMAGE_IMPLEMENTATION +#include +#include + +#include +#include +#include + +#if USE_JXL +#include +#include +#include +#include +#include +#include +#endif + +#if USE_EXR +#include +#include +#include +#include +#include +#endif + +namespace Penner { +namespace Resolution { + +#if USE_JXL +// Encode a buffer of interleaved 32-bit float RGB pixels as a JPEG XL file. +// If lossless is false, the image is compressed lossily at the given quality +// (0..100, higher is better quality, matching JxlEncoderDistanceFromQuality). +// If geometry_safe is true, the encoder avoids color/perceptual assumptions +// that are only valid for photographic RGB data (the XYB/YCoCg color +// transforms and psychovisual heuristics such as Gaborish, EPF, dots, and +// patches), since the R/G/B channels here actually store independent x/y/z +// geometry coordinates rather than correlated image color channels. +bool encode_jxl_rgb( + const std::string& image_path, + uint32_t width, + uint32_t height, + const std::vector& pixels, + bool lossless, + float quality, + bool geometry_safe) +{ + auto encoder = JxlEncoderMake(nullptr); + auto runner = JxlThreadParallelRunnerMake( + nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + if (JxlEncoderSetParallelRunner( + encoder.get(), JxlThreadParallelRunner, runner.get()) != JXL_ENC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL parallel runner"); + return false; + } + + JxlBasicInfo basic_info; + JxlEncoderInitBasicInfo(&basic_info); + basic_info.xsize = width; + basic_info.ysize = height; + basic_info.bits_per_sample = 32; + basic_info.exponent_bits_per_sample = 8; + basic_info.num_color_channels = 3; + basic_info.uses_original_profile = JXL_TRUE; + if (JxlEncoderSetBasicInfo(encoder.get(), &basic_info) != JXL_ENC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL basic info"); + return false; + } + + JxlColorEncoding color_encoding = {}; + JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE); + color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR; + if (JxlEncoderSetColorEncoding(encoder.get(), &color_encoding) != JXL_ENC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL color encoding"); + return false; + } + + JxlEncoderFrameSettings* frame_settings = + JxlEncoderFrameSettingsCreate(encoder.get(), nullptr); + JxlEncoderSetFrameLossless(frame_settings, lossless ? JXL_TRUE : JXL_FALSE); + if (!lossless) + { + JxlEncoderSetFrameDistance( + frame_settings, JxlEncoderDistanceFromQuality(quality)); + } + if (geometry_safe) + { + // Force modular mode so VarDCT's blockwise DCT and XYB color transform + // are never used, and disable the reversible color transform modular + // mode would otherwise try per group; both assume correlated RGB color + // channels, which does not hold for x/y/z coordinates. + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_MODULAR, 1); + JxlEncoderFrameSettingsSetOption( + frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, 0); + JxlEncoderFrameSettingsSetOption( + frame_settings, JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM, 1); + // Disable psychovisual heuristics and image-specific filters that trade + // numerical accuracy for perceived photographic quality. + JxlEncoderFrameSettingsSetOption( + frame_settings, JXL_ENC_FRAME_SETTING_DISABLE_PERCEPTUAL_HEURISTICS, 1); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EPF, 0); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_GABORISH, 0); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_DOTS, 0); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_PATCHES, 0); + } + + JxlPixelFormat pixel_format = {3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + if (JxlEncoderAddImageFrame( + frame_settings, + &pixel_format, + pixels.data(), + pixels.size() * sizeof(float)) != JXL_ENC_SUCCESS) + { + spdlog::error("Failed to add JPEG XL image frame"); + return false; + } + JxlEncoderCloseInput(encoder.get()); + + std::vector compressed(1 << 20); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size(); + JxlEncoderStatus status = JXL_ENC_NEED_MORE_OUTPUT; + while (status == JXL_ENC_NEED_MORE_OUTPUT) + { + status = JxlEncoderProcessOutput(encoder.get(), &next_out, &avail_out); + if (status == JXL_ENC_NEED_MORE_OUTPUT) + { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + if (status != JXL_ENC_SUCCESS) + { + spdlog::error("Failed to encode JPEG XL image"); + return false; + } + + std::ofstream out_file(image_path, std::ios::binary); + if (!out_file) + { + spdlog::error("Failed to open {} for writing", image_path); + return false; + } + out_file.write(reinterpret_cast(compressed.data()), compressed.size()); + + return true; +} + +// Decode a lossless JPEG XL file into a buffer of interleaved 32-bit float RGB pixels. +bool decode_jxl_rgb( + const std::string& image_path, + uint32_t& width, + uint32_t& height, + std::vector& pixels) +{ + std::ifstream in_file(image_path, std::ios::binary); + if (!in_file) + { + spdlog::error("Failed to open {} for reading", image_path); + return false; + } + std::vector compressed( + (std::istreambuf_iterator(in_file)), std::istreambuf_iterator()); + + auto decoder = JxlDecoderMake(nullptr); + auto runner = JxlThreadParallelRunnerMake( + nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + if (JxlDecoderSetParallelRunner( + decoder.get(), JxlThreadParallelRunner, runner.get()) != JXL_DEC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL parallel runner"); + return false; + } + + if (JxlDecoderSubscribeEvents( + decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) + { + spdlog::error("Failed to subscribe to JPEG XL decoder events"); + return false; + } + + if (JxlDecoderSetInput(decoder.get(), compressed.data(), compressed.size()) != + JXL_DEC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL decoder input"); + return false; + } + JxlDecoderCloseInput(decoder.get()); + + JxlPixelFormat pixel_format = {3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + for (;;) + { + JxlDecoderStatus status = JxlDecoderProcessInput(decoder.get()); + if (status == JXL_DEC_ERROR) + { + spdlog::error("Failed to decode JPEG XL image"); + return false; + } + else if (status == JXL_DEC_BASIC_INFO) + { + JxlBasicInfo basic_info; + if (JxlDecoderGetBasicInfo(decoder.get(), &basic_info) != JXL_DEC_SUCCESS) + { + spdlog::error("Failed to get JPEG XL basic info"); + return false; + } + width = basic_info.xsize; + height = basic_info.ysize; + pixels.resize(static_cast(width) * height * 3); + } + else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) + { + size_t buffer_size; + if (JxlDecoderImageOutBufferSize( + decoder.get(), &pixel_format, &buffer_size) != JXL_DEC_SUCCESS) + { + spdlog::error("Failed to get JPEG XL output buffer size"); + return false; + } + if (buffer_size != pixels.size() * sizeof(float)) + { + spdlog::error("Unexpected JPEG XL output buffer size"); + return false; + } + if (JxlDecoderSetImageOutBuffer( + decoder.get(), &pixel_format, pixels.data(), buffer_size) != JXL_DEC_SUCCESS) + { + spdlog::error("Failed to set JPEG XL output buffer"); + return false; + } + } + else if (status == JXL_DEC_FULL_IMAGE) + { + // Pixels buffer is fully populated; keep looping until JXL_DEC_SUCCESS. + } + else if (status == JXL_DEC_SUCCESS) + { + break; + } + } + + return true; +} + +void write_geometry_image_jxl( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless, + float quality, + bool geometry_safe) +{ + int width = x.cols(); + int height = x.rows(); + std::vector pixels(static_cast(width) * height * 3); + for (int i = 0; i < height; ++i) + { + for (int j = 0; j < width; ++j) + { + size_t p = (static_cast(i) * width + j) * 3; + pixels[p + 0] = static_cast(x(i, j)); + pixels[p + 1] = static_cast(y(i, j)); + pixels[p + 2] = static_cast(z(i, j)); + } + } + + encode_jxl_rgb(image_path, width, height, pixels, lossless, quality, geometry_safe); +} + +std::tuple read_geometry_image_jxl( + const std::string& image_path) +{ + uint32_t width, height; + std::vector pixels; + if (!decode_jxl_rgb(image_path, width, height, pixels)) + { + return std::make_tuple(GeometryData(), GeometryData(), GeometryData()); + } + + GeometryData X(height, width), Y(height, width), Z(height, width); + for (uint32_t i = 0; i < height; ++i) + { + for (uint32_t j = 0; j < width; ++j) + { + size_t p = (static_cast(i) * width + j) * 3; + X(i, j) = pixels[p + 0]; + Y(i, j) = pixels[p + 1]; + Z(i, j) = pixels[p + 2]; + } + } + + return std::make_tuple(X, Y, Z); +} +#endif + +#if USE_EXR +// Encode a buffer of interleaved 32-bit float RGB pixels as an OpenEXR file. +// If lossless is false, the image is compressed lossily with DWAA at the +// given quality (0..100, higher is better quality). +bool encode_exr_rgb( + const std::string& image_path, + uint32_t width, + uint32_t height, + const std::vector& pixels, + bool lossless, + float quality) +{ + Imf::Header header( + static_cast(width), + static_cast(height), + 1.0f, + Imath::V2f(0, 0), + 1.0f, + Imf::INCREASING_Y, + lossless ? Imf::ZIP_COMPRESSION : Imf::DWAA_COMPRESSION); + if (!lossless) + { + // Higher quality should mean lower DWA compression level (less loss). + header.dwaCompressionLevel() = (100.0f - std::clamp(quality, 0.0f, 100.0f)); + } + header.channels().insert("R", Imf::Channel(Imf::FLOAT)); + header.channels().insert("G", Imf::Channel(Imf::FLOAT)); + header.channels().insert("B", Imf::Channel(Imf::FLOAT)); + + try + { + Imf::OutputFile file(image_path.c_str(), header); + + size_t x_stride = 3 * sizeof(float); + size_t y_stride = x_stride * width; + Imf::FrameBuffer frame_buffer; + frame_buffer.insert( + "R", + Imf::Slice( + Imf::FLOAT, + reinterpret_cast(const_cast(pixels.data()) + 0), + x_stride, + y_stride)); + frame_buffer.insert( + "G", + Imf::Slice( + Imf::FLOAT, + reinterpret_cast(const_cast(pixels.data()) + 1), + x_stride, + y_stride)); + frame_buffer.insert( + "B", + Imf::Slice( + Imf::FLOAT, + reinterpret_cast(const_cast(pixels.data()) + 2), + x_stride, + y_stride)); + file.setFrameBuffer(frame_buffer); + file.writePixels(static_cast(height)); + } + catch (const std::exception& e) + { + spdlog::error("Failed to encode OpenEXR image: {}", e.what()); + return false; + } + + return true; +} + +// Decode an OpenEXR file into a buffer of interleaved 32-bit float RGB pixels. +bool decode_exr_rgb( + const std::string& image_path, + uint32_t& width, + uint32_t& height, + std::vector& pixels) +{ + try + { + Imf::InputFile file(image_path.c_str()); + Imath::Box2i data_window = file.header().dataWindow(); + width = static_cast(data_window.max.x - data_window.min.x + 1); + height = static_cast(data_window.max.y - data_window.min.y + 1); + pixels.assign(static_cast(width) * height * 3, 0.0f); + + size_t x_stride = 3 * sizeof(float); + size_t y_stride = x_stride * width; + char* base = reinterpret_cast(pixels.data()) - + (data_window.min.x * static_cast(x_stride)) - + (data_window.min.y * static_cast(y_stride)); + + Imf::FrameBuffer frame_buffer; + frame_buffer.insert( + "R", Imf::Slice(Imf::FLOAT, base + 0 * sizeof(float), x_stride, y_stride)); + frame_buffer.insert( + "G", Imf::Slice(Imf::FLOAT, base + 1 * sizeof(float), x_stride, y_stride)); + frame_buffer.insert( + "B", Imf::Slice(Imf::FLOAT, base + 2 * sizeof(float), x_stride, y_stride)); + file.setFrameBuffer(frame_buffer); + file.readPixels(data_window.min.y, data_window.max.y); + } + catch (const std::exception& e) + { + spdlog::error("Failed to decode OpenEXR image: {}", e.what()); + return false; + } + + return true; +} + +void write_geometry_image_exr( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless, + float quality) +{ + int width = x.cols(); + int height = x.rows(); + std::vector pixels(static_cast(width) * height * 3); + for (int i = 0; i < height; ++i) + { + for (int j = 0; j < width; ++j) + { + size_t p = (static_cast(i) * width + j) * 3; + pixels[p + 0] = static_cast(x(i, j)); + pixels[p + 1] = static_cast(y(i, j)); + pixels[p + 2] = static_cast(z(i, j)); + } + } + + encode_exr_rgb(image_path, width, height, pixels, lossless, quality); +} + +std::tuple read_geometry_image_exr( + const std::string& image_path) +{ + uint32_t width, height; + std::vector pixels; + if (!decode_exr_rgb(image_path, width, height, pixels)) + { + return std::make_tuple(GeometryData(), GeometryData(), GeometryData()); + } + + GeometryData X(height, width), Y(height, width), Z(height, width); + for (uint32_t i = 0; i < height; ++i) + { + for (uint32_t j = 0; j < width; ++j) + { + size_t p = (static_cast(i) * width + j) * 3; + X(i, j) = pixels[p + 0]; + Y(i, j) = pixels[p + 1]; + Z(i, j) = pixels[p + 2]; + } + } + + return std::make_tuple(X, Y, Z); +} +#endif + +std::tuple read_geometry_image_txt( + const std::string& image_path) +{ + // Open file + std::ifstream input_file(image_path); + if (!input_file) return {}; + + // Read file + std::vector> matrix_vec = {}; + std::string line; + while (std::getline(input_file, line)) { + std::istringstream iss(line); + matrix_vec.push_back({}); + double v; + while (iss >> v) + { + matrix_vec.back().push_back(v); + } + } + + // Close file + input_file.close(); + + int height = matrix_vec.size() / 3; + int width = matrix_vec[0].size(); + GeometryData X(height, width); + GeometryData Y(height, width); + GeometryData Z(height, width); + for (int i = 0; i < height; ++i) + { + for (int j = 0; j < width; ++j) + { + X(i, j) = matrix_vec[i][j]; + Y(i, j) = matrix_vec[height + i][j]; + Z(i, j) = matrix_vec[2 * height + i][j]; + } + } + + return std::make_tuple(X, Y, Z); +} + +void write_geometry_channel_txt( + std::ofstream& output_file, + const GeometryData& x) +{ + int width = x.cols(); + int height = x.rows(); + + // Iterate over rows + for (Eigen::Index i = 0; i < height; ++i) { + // Iterate over columns of row i + double v = x(i, 0); + output_file << std::fixed << std::setprecision(17) << v; + for (Eigen::Index j = 1; j < width; ++j) { + double v = x(i, j); + output_file << std::fixed << std::setprecision(17) << " " << v; + } + + // Add newline to end of row + output_file << std::endl; + } +} + +void write_geometry_image_txt( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z) +{ + // Open file + std::ofstream output_file; + output_file.open(image_path); + write_geometry_channel_txt(output_file, x); + write_geometry_channel_txt(output_file, y); + write_geometry_channel_txt(output_file, z); + output_file.close(); +} + +std::tuple build_grid_mesh( + const GeometryData& x, + const GeometryData& y, + const GeometryData& z) +{ + int width = x.cols(); + int height = x.rows(); + int num_vertices = width * height; + int num_quads = (width - 1) * (height - 1); + + // build flattened vertex data from xyz data + Eigen::MatrixXd V(num_vertices, 3); + for (int i = 0; i < height; ++i) + { + for (int j = 0; j < width; ++j) + { + int v = (i * width) + j; + V(v, 0) = x(i, j); + V(v, 1) = y(i, j); + V(v, 2) = z(i, j); + } + } + + // build faces of grid + Eigen::MatrixXi F(num_quads, 4); + for (int i = 0; i < height - 1; ++i) + { + for (int j = 0; j < width - 1; ++j) + { + int q = (i * (width - 1)) + j; + int ip = i + 1; + int jp = j + 1; + int v00 = (i * width) + j; + int v01 = (i * width) + jp; + int v10 = (ip * width) + j; + int v11 = (ip * width) + jp; + F.row(q) << v00, v01, v11, v10; + } + } + + return std::make_tuple(V, F); +} + +std::tuple, std::vector, std::vector> +build_grid_halfedge( + const GeometryData& x, + const GeometryData& y, + const GeometryData& z) +{ + int width = x.cols(); + int height = x.rows(); + int num_vertices = width * height; + int num_quads = (width - 1) * (height - 1); + int num_halfedges = 4 * num_quads; + + // build flattened vertex data from xyz data + Eigen::MatrixXd V(num_vertices, 3); + for (int i = 0; i < height; ++i) + { + for (int j = 0; j < width; ++j) + { + int v = (i * width) + j; + V(v, 0) = x(i, j); + V(v, 1) = y(i, j); + V(v, 2) = z(i, j); + } + } + + // build faces of grid + std::vector n(num_halfedges, -1); + std::vector to(num_halfedges, -1); + for (int i = 0; i < height - 1; ++i) + { + for (int j = 0; j < width - 1; ++j) + { + int q = (i * (width - 1)) + j; + int h = 4 * q; + int ip = i + 1; + int jp = j + 1; + + // build next adjacency + for (int k = 0; k < 4; ++k) + { + int kp = (k + 1) % 4; + n[h + k] = h + kp; + } + + // set halfedge tip vertices + to[h + 0] = (i * width) + jp; // bottom + to[h + 1] = (ip * width) + jp; // right + to[h + 2] = (ip * width) + j; // top + to[h + 3] = (i * width) + j; // left + } + } + + // set horizontal opposites + std::vector opp(num_halfedges, -1); + for (int i = 0; i < height - 1; ++i) + { + for (int j = 1; j < width - 1; ++j) + { + int q = (i * (width - 1)) + j; + int qp = (i * (width - 1)) + (j - 1); + int h = 4 * q + 3; + int ho = 4 * qp + 1; + opp[h] = ho; + opp[ho] = h; + } + } + + // set vertical opposites + for (int i = 1; i < height - 1; ++i) + { + for (int j = 0; j < width - 1; ++j) + { + int q = (i * (width - 1)) + j; + int qp = ((i - 1) * (width - 1)) + j; + int h = 4 * q + 0; + int ho = 4 * qp + 2; + opp[h] = ho; + opp[ho] = h; + } + } + + return std::make_tuple(V, n, opp, to); +} + +ImageData convert_geometry_to_image_data(const GeometryData& X) +{ + return (256 * X).cast(); +} + +GeometryData convert_image_to_geometry_data(const ImageData& X) +{ + return (X / 256).cast(); +} + +std::tuple read_geometry_image_png( + const std::string& image_path) +{ + ImageData x, y, z, alpha; + igl::png::readPNG(image_path, x, y, z, alpha); + + GeometryData X, Y, Z; + X = convert_image_to_geometry_data(x); + Y = convert_image_to_geometry_data(y); + Z = convert_image_to_geometry_data(z); + + return std::make_tuple(X, Y, Z); +} + +void write_geometry_image_png( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z) +{ + ImageData X, Y, Z; + X = convert_geometry_to_image_data(x); + Y = convert_geometry_to_image_data(y); + Z = convert_geometry_to_image_data(z); + ImageData A = ImageData::Constant(X.rows(), X.cols(), 255); + igl::png::writePNG(X, Y, Z, A, image_path); +} + +std::tuple read_geometry_image( + const std::string& image_path, + std::string filetype) +{ + if (filetype == "txt") + { + return read_geometry_image_txt(image_path); + } +#if USE_JXL + else if (filetype == "jxl") + { + return read_geometry_image_jxl(image_path); + } +#endif +#if USE_EXR + else if (filetype == "exr") + { + return read_geometry_image_exr(image_path); + } +#endif + else + { + return read_geometry_image_png(image_path); + } +} + +void write_geometry_image( + const std::string& image_path, + const GeometryData& x, + const GeometryData& y, + const GeometryData& z, + bool lossless, + float quality, + std::string filetype) +{ + if (filetype == "txt") + { + write_geometry_image_txt(image_path, x, y, z); + } +#if USE_JXL + else if (filetype == "jxl") + { + write_geometry_image_jxl(image_path, x, y, z, lossless, quality, true); + } +#endif +#if USE_EXR + else if (filetype == "exr") + { + write_geometry_image_exr(image_path, x, y, z, lossless, quality); + } +#endif + else + { + write_geometry_image_png(image_path, x, y, z); + } +} + + +} +} diff --git a/src/resolution/optimization/field.cpp b/src/resolution/optimization/field.cpp new file mode 100644 index 0000000..8a3cf79 --- /dev/null +++ b/src/resolution/optimization/field.cpp @@ -0,0 +1,805 @@ +#include "resolution/optimization/field.h" +#include "feature/feature/features.h" +#include "field/intrinsic_field.h" +#include "parametrization/layout.h" +#include "field/cones.h" +#include "holonomy/holonomy/holonomy.h" +#include "holonomy/core/homology_basis.h" +#include "metric/constraint.h" + +#include +#include +#include + +namespace Penner +{ +namespace Resolution +{ + +std::vector trace_spanning_tree_to_root( + const std::vector& parent, + int start_index) +{ + std::vector path = {start_index}; + + // Trace up the dual tree until a root is reached + int curr_index = start_index; + while (parent[curr_index] >= 0) { + curr_index = parent[curr_index]; + path.push_back(curr_index); + } + + return path; +} + +std::vector get_spanning_tree_with_cut( + const VoronoiOverlayMesh& voronoi_mesh, + const std::vector& is_not_cut +) +{ + int num_faces = voronoi_mesh.f2h.size(); + int num_halfedges = voronoi_mesh.n.size(); + std::vector is_processed_face(num_faces, false); + std::vector parent_face(num_faces, -1); + + // Initialize queue with boundary faces to process + std::deque faces_to_process; + int fi = 0; + while (voronoi_mesh.is_face_deleted(fi)) ++fi; + is_processed_face[fi] = true; + faces_to_process.push_back(fi); + + // bfs + while (!faces_to_process.empty()) { + // Get the next face to process + int fi = faces_to_process.front(); + faces_to_process.pop_front(); + + // Iterate over the face circulator via halfedges + int h_start = voronoi_mesh.f2h[fi]; + int hij = h_start; + do { + // Get the face in the one ring at the tip of the halfedge + int fj = voronoi_mesh.face[voronoi_mesh.opp[hij]]; + + bool is_invalid = false; + is_invalid = (is_invalid) || (is_processed_face[fj]); + if (voronoi_mesh.orig.edge_type[voronoi_mesh.h2e[hij]] == VoronoiOverlayMesh::EdgeType::EE) + { + int Hij = voronoi_mesh.orig.halfedge_parent[hij]; + is_invalid = (is_invalid) || (!is_not_cut[Hij]); + } + + // Check if the edge to the tip face is the best seen so far + if (!is_invalid) { + is_processed_face[fj] = true; + parent_face[fj] = fi; + faces_to_process.push_back(fj); + } + + // Progress to the next halfedge in the face circulator + hij = voronoi_mesh.n[hij]; + } while (hij != h_start); + } + + return parent_face; +} + + +VoronoiOverlayMesh generate_uniform_triangulation( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + Scalar ratio) +{ + // compute target length from mesh area and vertices + Eigen::VectorXd double_area; + igl::doublearea(V, F, double_area); + Scalar mesh_area = double_area.sum() / 2.; + int num_vertices = V.rows(); + Scalar target_length = sqrt(mesh_area / (ratio * num_vertices)); + + std::vector Th_hat(V.rows(), 2 * PI); + auto [m, vtx_reindex] = Holonomy::generate_mesh(V, F, V, F, Th_hat); + VoronoiOverlayMesh voronoi_mesh(V, vtx_reindex, m); + voronoi_mesh.use_zero_lengths = true; // TODO + + // get feature vertices and junctions to use as seeds + Feature::FeatureFinder feature_finder(V, F); + Scalar feature_angle = 30.; + feature_finder.mark_dihedral_angle_features(feature_angle); + std::vector feature_degrees = feature_finder.compute_feature_degrees(); + std::vector seeds = {}; + std::vector is_feature_vertex(feature_degrees.size(), false); + for (int i = 0; i < feature_degrees.size(); ++i) + { + if (feature_degrees[i] > 0) is_feature_vertex[i] = true; + if (feature_degrees[i] > 2) seeds.push_back(i); + } + if (seeds.empty()) seeds = {0}; + + // build point sampling, starting with feature vertices, then feature edges, then interiors + voronoi_mesh.grow_voronoi_regions(seeds); + voronoi_mesh.set_constained_max_voronoi_distance(target_length, is_feature_vertex); + voronoi_mesh.set_max_voronoi_distance(target_length); + + // build triangulation + voronoi_mesh.split_voronoi_edges(); + voronoi_mesh.split_voronoi_faces(); + voronoi_mesh.find_voronoi_dual_edges(); + voronoi_mesh.propagate_voronoi_vertices_to_faces(); + voronoi_mesh.trace_delaunay_edges(); + voronoi_mesh.flood_faces(); + + // straighten and compute the length of the triangulation edges + //voronoi_mesh.voronoi.reset(); // reset to allow overlay path tracing + //voronoi_mesh.remove_excess_refinement(); + //voronoi_mesh.straighten_edges(); + //voronoi_mesh.set_simplified_halfedge_lengths(); + + return voronoi_mesh; +} + + +// TODO: replace this with generate_frame_field (using the existing mesh) +Field::IntrinsicNRosyField compute_simplified_field( + const VoronoiOverlayMesh& voronoi_mesh, + const Eigen::MatrixXd& V_simp, + const Eigen::MatrixXi& F_simp, + const Field::FieldParameters& field_params) +{ + const Mesh& m = voronoi_mesh.simp.m; + Field::IntrinsicNRosyField field_generator(field_params); + field_generator.initialize(voronoi_mesh.simp.m); + if (field_params.use_principal_directions) + { + int num_faces = F_simp.rows(); + std::vector vtx_reindex = arange(V_simp.rows()); + std::vector face_map = generate_face_map(voronoi_mesh.simp.m); + Eigen::VectorXi reference_corner(num_faces); + Eigen::VectorXd theta(num_faces); + Eigen::MatrixXd kappa(num_faces, 3); + Eigen::MatrixXi period_jump(num_faces, 3); + field_generator.get_field(voronoi_mesh.simp.m, vtx_reindex, F_simp, face_map, reference_corner, theta, kappa, period_jump); + + // fit field directions + int radius = 5; + Scalar abs_anisotropy=0.5; + Scalar rel_anisotropy=0.9; + Scalar bb_diag = igl::bounding_box_diagonal(V_simp); + auto [direction, is_fixed_direction] = Field::compute_field_direction( + V_simp, + F_simp, + radius, + abs_anisotropy / bb_diag, + rel_anisotropy, + 0.05); + // compute normals for angle computations + + Eigen::MatrixXd N; + igl::per_face_normals(V_simp, F_simp, N); + + // get fixed angles from input direction field + std::vector target_theta(num_faces); + std::vector is_fixed(num_faces, false); + for (int i = 0; i < num_faces; ++i) + { + int fijk = face_map[i]; + if (!is_fixed_direction[fijk]) continue; + + // check if adjacent faces also set + bool is_adjacent_fixed = false; + int hij = m.h[i]; + do + { + if (is_fixed[m.f[m.opp[hij]]]) + { + is_adjacent_fixed = true; + } + + hij = m.n[hij]; + } while (hij != m.h[i]); + if (is_adjacent_fixed) continue; + + is_fixed[i] = is_fixed_direction[fijk]; + + // convert extrinsic direction to intrinsic angle + Eigen::Vector3d reference_direction = Field::generate_reference_direction(V_simp, F_simp, fijk, reference_corner[fijk]); + target_theta[i] = -signed_angle(direction.row(fijk), reference_direction, N.row(fijk)); + } + + // set fixed directions + field_generator.set_fixed_directions(voronoi_mesh.simp.m, target_theta, is_fixed); + } + + field_generator.solve(voronoi_mesh.simp.m); + if (field_params.fix_cone_pair) field_generator.fix_cone_pair(voronoi_mesh.simp.m); + if (field_params.min_cone_pair_distance > 0.) field_generator.remove_close_cone_pairs(voronoi_mesh.simp.m, field_params.min_cone_pair_distance); + return field_generator; +} + +void lift_simplified_field_to_original( + const VoronoiOverlayMesh& voronoi_mesh, + const VectorX& rotation_form_simp, + Field::IntrinsicNRosyField& field_generator +) +{ + const auto& m = voronoi_mesh.orig.m; + int num_halfedges = m.n_halfedges(); + int num_vertices = m.n_vertices(); + + std::vector start_halfedge = voronoi_mesh.find_start_halfedges(voronoi_mesh.orig); + + // get target cones from simplified mesh + std::vector Th_hat_simp = Field::generate_cones_from_rotation_form( + voronoi_mesh.simp.m, + rotation_form_simp); + + // Compute the corner angles for the original and simplified mesh + VectorX he2angle, he2cot; + corner_angles(m, he2angle, he2cot); + VectorX he2angle_simp, he2cot_simp; + corner_angles(voronoi_mesh.simp.m, he2angle_simp, he2cot_simp); + + // get base kappas and field + Eigen::VectorXd theta; + Eigen::VectorXd kappa; + Eigen::VectorXi period_jump; + field_generator.get_halfedge_field(m, theta, kappa, period_jump); + std::vector Th_hat_base(num_vertices, 0); + for (int hij = 0; hij < m.n_halfedges(); hij++) { + Th_hat_base[m.v_rep[m.to[m.n[hij]]]] += he2angle[hij]; + Th_hat_base[m.v_rep[m.to[hij]]] += kappa[hij]; + } + + // get cones determined by kappa + std::vector base_cones(num_vertices, 0); + for (int vi = 0; vi < num_vertices; ++vi) + { + base_cones[vi] = lround(Th_hat_base[vi] / (M_PI / 2.)); + } + + // set dual tree halfedges as fixed + std::vector fixed_faces = {0}; + std::vector halfedges_from_face = Field::build_double_dual_bfs_forest(m, fixed_faces); + std::vector is_period_jump_fixed(num_halfedges, false); + for (int h : halfedges_from_face) { + if (h < 0) continue; + for (int h_rel : { h, m.opp[h] }) { + is_period_jump_fixed[h_rel] = true; + } + } + + // get primal cotree + std::vector weights(num_halfedges, 1.); + std::vector is_spanning_tree(num_halfedges, false); + std::vector halfedges_from_vertex = build_primal_forest(m, weights, is_period_jump_fixed); + for (int h : halfedges_from_vertex) { + if (h < 0) continue; + for (int h_rel : { h, m.opp[h] }) { + is_spanning_tree[h_rel] = true; + } + } + + // count the number of unset edges adjacent to each vertex + std::vector adjacent_unset_edges(num_vertices, 0); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (!is_period_jump_fixed[hij]) + { + ++adjacent_unset_edges[m.to[hij]]; + } + } + + // get dual path in overlay to base face that doesn't intersect the unset period jump edges + // TODO: more efficient to build this after first branch trim in zippering + std::vector voronoi_face_parent = get_spanning_tree_with_cut(voronoi_mesh, is_period_jump_fixed); + + // set period jumps for leaves of the unset period jump graph + std::vector vertices_to_process = arange(num_vertices); + + // store loops on surface + std::vector dual_loops = {}; + + //DiscreteMetric discrete_metric_orig = generate_discrete_metric(voronoi_mesh.orig); + //DiscreteMetric discrete_metric_simp = generate_discrete_metric(voronoi_mesh.simp); + // iterate until all halfedge jumps set + int h_test = 0; + while (h_test < num_halfedges) + { + while (!vertices_to_process.empty()) + { + int vi = vertices_to_process.back(); + vertices_to_process.pop_back(); + + if (adjacent_unset_edges[vi] == 1) + { + // find adjacent unfixed edge + int hij = m.out[vi]; + while (is_period_jump_fixed[hij]) + { + hij = m.n[m.opp[hij]]; + } + + // get target cone angle from simplified mesh + int vi_overlay = voronoi_mesh.orig.vertex_index[vi]; + int target_cone = 4; + if (voronoi_mesh.simp.vertex_type[vi_overlay] == SimplificationOverlayMesh::VertexType::VV) + { + int vi_simp = voronoi_mesh.simp.vertex_parent[vi]; + target_cone = lround(Th_hat_simp[vi_simp] / (M_PI / 2.)); + } + + // get cone index determined by adjacent halfedges + int hik = m.n[m.opp[hij]]; + int cone = base_cones[vi]; + while (hik != hij) + { + assert(is_period_jump_fixed[hik]); + cone -= period_jump[hik]; // subtract since vertex at base of halfedge + hik = m.n[m.opp[hik]]; + } + + // update data structures + int vj = m.to[hij]; + if (vi == vj) spdlog::warn("self adjacent edge"); + int hji = m.opp[hij]; + period_jump[hij] = (cone - target_cone); + period_jump[hji] = -(cone - target_cone); + is_period_jump_fixed[hij] = true; + is_period_jump_fixed[hji] = true; + --adjacent_unset_edges[vi]; + --adjacent_unset_edges[vj]; + vertices_to_process.push_back(vj); + } + } + + while (h_test < num_halfedges) + { + if ((!is_period_jump_fixed[h_test]) && (!is_spanning_tree[h_test])) break; + h_test++; + } + + if (h_test < num_halfedges) + { + int hij = h_test; + int hji = m.opp[hij]; + int hij_overlay = start_halfedge[hij]; + int hji_overlay = voronoi_mesh.opp[hij_overlay]; + int vj = m.to[hij]; + int vi = m.to[hji]; + + // build a path of fixed period jumps connecting the opposite halfedges + std::vector path_i = trace_spanning_tree_to_root(voronoi_face_parent, voronoi_mesh.face[hij_overlay]); + std::vector path_j = trace_spanning_tree_to_root(voronoi_face_parent, voronoi_mesh.face[hji_overlay]); + std::vector basis_loop = concatenate_vector(path_i, reverse_vector(path_j)); + + // project the loops to the origianl and simplified mesh + std::vector orig_basis_loop = voronoi_mesh.project_dual_loop(basis_loop, voronoi_mesh.orig); + dual_loops.emplace_back(voronoi_mesh.orig.m, orig_basis_loop); + Holonomy::DualLoopList & orig_dual_path = dual_loops.back(); + + // compute target holonomy of the loop projected on the simplified mesh + std::vector simp_basis_loop = voronoi_mesh.project_dual_loop(basis_loop, voronoi_mesh.simp); + Holonomy::DualLoopList simp_dual_path(voronoi_mesh.simp.m, simp_basis_loop); + Scalar rotation_simp = compute_dual_loop_rotation(voronoi_mesh.simp.m, rotation_form_simp, simp_dual_path); + Scalar holonomy_simp = compute_dual_loop_holonomy(voronoi_mesh.simp.m, he2angle_simp, simp_dual_path); + Scalar target = holonomy_simp - rotation_simp; + spdlog::debug("target angle {}", target); + + // get holonomy on the original mesh + Scalar holonomy = compute_dual_loop_holonomy(m, he2angle, orig_dual_path); + + // compute rotation of base field along the dual loop + Scalar rotation = 0.; + period_jump[hij] = 0; + period_jump[hji] = 0; + for (const auto& dual_segment : orig_dual_path) { + int h = dual_segment[0]; + rotation += kappa[h] + (period_jump[h] * (PI / 2.)); + } + Scalar current = holonomy - rotation; + int defect = round((current - target) / (PI / 2.)); + spdlog::debug("current holonomy {}", current); + spdlog::debug("defect is {}", defect); + + period_jump[hij] = defect; + period_jump[hji] = -defect; + is_period_jump_fixed[hij] = true; + is_period_jump_fixed[hji] = true; + --adjacent_unset_edges[vi]; + --adjacent_unset_edges[vj]; + vertices_to_process.push_back(vi); + vertices_to_process.push_back(vj); + } + } + + assert(vector_max(adjacent_unset_edges) == 0); + // set field with new values + field_generator.set_halfedge_field(m, theta, kappa, period_jump); + + VectorX rotation_form = field_generator.compute_rotation_form(voronoi_mesh.orig.m); + std::vector Th_hat = Field::generate_cones_from_rotation_form( + voronoi_mesh.orig.m, + rotation_form); + + bool check_consistency = false; + if (check_consistency) + { + for (int vi = 0; vi < voronoi_mesh.out.size(); ++vi) + { + if (voronoi_mesh.is_vertex_deleted(vi)) continue; + if (voronoi_mesh.orig.vertex_type[vi] != SimplificationOverlayMesh::VertexType::VV) continue; + int vi_orig = voronoi_mesh.orig.vertex_parent[vi]; + int actual_cone = lround(Th_hat[vi_orig] / (M_PI / 2.)); + int target_cone = 4; + if (voronoi_mesh.simp.vertex_type[vi] == SimplificationOverlayMesh::VertexType::VV) + { + int vi_simp = voronoi_mesh.simp.vertex_parent[vi]; + target_cone = lround(Th_hat_simp[vi_simp] / (M_PI / 2.)); + } + if (target_cone != actual_cone) + { + spdlog::error("inconsistent cone {} at {} with target {}", actual_cone, vi, target_cone); + } + } + + for (const auto& dual_loop : dual_loops) + { + Scalar rotation = compute_dual_loop_rotation(m, rotation_form, dual_loop); + Scalar holonomy = compute_dual_loop_holonomy(m, he2angle, dual_loop); + Scalar current = holonomy - rotation; + spdlog::info("current holonomy {}", current); + } + } +} + + +Eigen::MatrixXd lift_simplified_directions_to_overlay( + const VoronoiOverlayMesh& voronoi_mesh, + const Field::IntrinsicNRosyField& field_generator, + const Eigen::MatrixXd& V_simp, + const Eigen::MatrixXi& F_simp) +{ + std::vector vtx_reindex_simp = arange(V_simp.rows()); + Eigen::MatrixXd frame_field_orig(voronoi_mesh.orig.m.n_faces(), 3); + int num_faces = F_simp.rows(); + std::vector face_map = generate_face_map(voronoi_mesh.simp.m); + Eigen::VectorXi reference_corner(num_faces); + Eigen::VectorXd theta(num_faces); + Eigen::MatrixXd kappa(num_faces, 3); + Eigen::MatrixXi period_jump(num_faces, 3); + field_generator.get_field(voronoi_mesh.simp.m, vtx_reindex_simp, F_simp, face_map, reference_corner, theta, kappa, period_jump); + Eigen::MatrixXd reference_field = Field::generate_reference_field(V_simp, F_simp, reference_corner); + //view_cross_field(V_simp, F_simp, reference_field, theta, kappa, period_jump); + + Eigen::MatrixXd frame_field = Field::generate_vector_field(V_simp, F_simp, reference_field, theta); + auto [f_new2old, f_old2new] = voronoi_mesh.get_face_reductions(); + Eigen::MatrixXd frame_field_overlay(f_new2old.size(), 3); + for (int f = 0; f < f_new2old.size(); ++f) + { + int Fijk = voronoi_mesh.simp.face_parent[f_new2old[f]]; + frame_field_overlay.row(f) = frame_field.row(Fijk); + } + return frame_field_overlay; +} + +// experimental +Field::IntrinsicNRosyField compute_simplified_field( + const VoronoiOverlayMesh& voronoi_mesh, + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& V_simp, + const Eigen::MatrixXi& F_simp, + const Field::FieldParameters& field_params) +{ + Field::IntrinsicNRosyField field_generator(field_params); + field_generator.initialize(voronoi_mesh.simp.m); + if (field_params.use_principal_directions) + { + int num_faces = F_simp.rows(); + std::vector vtx_reindex = arange(V_simp.rows()); + std::vector face_map = generate_face_map(voronoi_mesh.simp.m); + Eigen::VectorXi reference_corner(num_faces); + Eigen::VectorXd theta(num_faces); + Eigen::MatrixXd kappa(num_faces, 3); + Eigen::MatrixXi period_jump(num_faces, 3); + field_generator.get_field(voronoi_mesh.simp.m, vtx_reindex, F_simp, face_map, reference_corner, theta, kappa, period_jump); + + // fit field directions in original mesh + int radius = 5; + Scalar abs_anisotropy=0.5; + Scalar rel_anisotropy=0.9; + Scalar bb_diag = igl::bounding_box_diagonal(V); + auto [direction, is_fixed_direction] = Field::compute_field_direction( + V, + F, + radius, + abs_anisotropy / bb_diag, + rel_anisotropy, + 0.01); + + // compute normals for angle computations + Eigen::MatrixXd N_simp; + igl::per_face_normals(V_simp, F_simp, N_simp); + + // get fixed angles from input direction field + std::vector target_theta(num_faces); + std::vector is_fixed(num_faces); + for (int f = 0; f < voronoi_mesh.f2h.size(); ++f) + { + if (voronoi_mesh.is_face_deleted(f)) continue; + int f_simp = voronoi_mesh.simp.face_parent[f]; + int f_orig = voronoi_mesh.orig.face_parent[f]; + if (!is_fixed_direction[f_orig]) continue; + + // convert extrinsic direction to intrinsic angle + is_fixed[f_simp] = true; + Eigen::Vector3d reference_direction = Field::generate_reference_direction(V_simp, F_simp, f_simp, reference_corner[f_simp]); + Eigen::Vector3d d = project_orthogonal_to(direction.row(f_orig), N_simp.row(f_simp)).stableNormalized(); + target_theta[f_simp] = -signed_angle(d, reference_direction, N_simp.row(f_simp)); + } + + // set fixed directions + field_generator.set_fixed_directions(voronoi_mesh.simp.m, target_theta, is_fixed); + } + + field_generator.solve(voronoi_mesh.simp.m); + if (field_params.fix_cone_pair) field_generator.fix_cone_pair(voronoi_mesh.simp.m); + if (field_params.min_cone_pair_distance > 0.) field_generator.remove_close_cone_pairs(voronoi_mesh.simp.m, field_params.min_cone_pair_distance); + return field_generator; +} + +void comb_field_to_cut( + const Mesh& m, + const std::vector is_cut, + Eigen::VectorXd& thetas, + Eigen::VectorXi& period_jumps) +{ + // initialize queue and markings with first halfedge + int h_start = 0; + std::queue Q; + std::vector done(m.h.size()); + Q.push(h_start); + done[m.f[h_start]] = true; + + while (!Q.empty()) { + int h = Q.front(); + Q.pop(); + int hn = m.n[h]; + int hp = m.n[hn]; + + for (int h_curr : {h, hn, hp}) + { + int ho = m.opp[h_curr]; + int fo = m.f[ho]; + + // stop at visited edges and explicitly cut edges + if (done[m.f[ho]]) continue; + if (is_cut[h_curr]) continue; + + // bake period jump across edge into the face angle + int pj = period_jumps[ho]; + thetas[fo] += pj * (PI / 2.); + + // push period jumps through face to opposite wedge + period_jumps[ho] = period_jumps[m.opp[ho]] = 0; + period_jumps[m.n[ho]] -= pj; + period_jumps[m.n[m.n[ho]]] -= pj; + period_jumps[m.opp[m.n[ho]]] += pj; + period_jumps[m.opp[m.n[m.n[ho]]]] += pj; + + // mark opposite face as seen and queue + done[fo] = true; + Q.push(ho); + } + } +} + + +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi +> +lift_simplified_field_to_original( + const VoronoiOverlayMesh& voronoi_mesh, + const std::vector& vtx_reindex, + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::VectorXd& rotation_form, + const Eigen::MatrixXd& reference_field_simp, + const Eigen::VectorXd& theta_simp, + const Eigen::MatrixXd& kappa_simp, + const Eigen::MatrixXi& period_jump_simp, + int smooth_iters) +{ + const auto& m = voronoi_mesh.orig.m; + int num_faces = F.rows(); + Field::IntrinsicNRosyField field_generator_int; + field_generator_int.initialize(voronoi_mesh.orig.m); + std::vector face_map = generate_face_map(m); + Eigen::VectorXi reference_corner(num_faces); + Eigen::VectorXd theta(num_faces); + Eigen::MatrixXd kappa(num_faces, 3); + Eigen::MatrixXi period_jump(num_faces, 3); + int num_overlay_faces = voronoi_mesh.f2h.size(); + field_generator_int.get_field(m, vtx_reindex, F, face_map, reference_corner, theta, kappa, period_jump); + Eigen::MatrixXd reference_field = Field::generate_reference_field(V, F, reference_corner); + + // get overlay mesh and frame field + Eigen::MatrixXd V_o = voronoi_mesh.generate_overlay_vertices(V, vtx_reindex, voronoi_mesh.orig); + auto [F_o, F_halfedge] = voronoi_mesh.generate_overlay_faces(); + + // set period jumps in original mesh from simplified mesh + lift_simplified_field_to_original(voronoi_mesh, rotation_form, field_generator_int); + + // lift field directions to overlay + auto [reference_field_overlay, theta_overlay, period_jump_overlay] = voronoi_mesh.sample_field_from_overlay( + reference_field_simp, + theta_simp, + period_jump_simp, + voronoi_mesh.simp); + + // comb field to original mesh seam + spdlog::info("combing field to original mesh"); + Eigen::VectorXd kappa_halfedge; + Eigen::VectorXi period_jump_halfedge; + field_generator_int.get_halfedge_field(m, theta, kappa_halfedge, period_jump_halfedge); + std::vector is_cut_place_holder = {}; + std::vector is_cut_orig = compute_layout_topology(voronoi_mesh.orig.m, is_cut_place_holder); + voronoi_mesh.comb_field_to_overlay_seam(is_cut_orig, 0, theta_overlay, period_jump_overlay, voronoi_mesh.orig); + comb_field_to_cut(voronoi_mesh.orig.m, is_cut_orig, theta, period_jump_halfedge); + + // average directions in original face with area weight + spdlog::info("computing average field on original mesh"); + Eigen::MatrixXd frame_field_overlay = Field::generate_vector_field(V_o, F_o, reference_field_overlay, theta_overlay); + + Eigen::MatrixXd frame_field_orig = Eigen::MatrixXd::Zero(num_faces, 3); + Eigen::VectorXd doublearea; + igl::doublearea(V_o, F_o, doublearea); + for (int f = 0; f < num_overlay_faces; ++f) + { + if (voronoi_mesh.is_face_deleted(f)) continue; + int f_orig = voronoi_mesh.orig.face_parent[f]; + Scalar area = doublearea[f] / 2.; + frame_field_orig.row(f_orig) += area * frame_field_overlay.row(f); + } + + // compute angles in original mesh + spdlog::info("computing angles on original mesh"); + Eigen::MatrixXd N; + igl::per_face_normals(V, F, N); + for (int f = 0; f < num_faces; ++f) + { + Eigen::Vector3d d = frame_field_orig.row(f).stableNormalized(); + theta[f] = -signed_angle(d, reference_field.row(f), N.row(f)); + } + + // set optimized field + field_generator_int.set_halfedge_field(m, theta, kappa_halfedge, period_jump_halfedge); + + // smooth field directions + field_generator_int.smooth_theta(m, smooth_iters); + + // get final field + field_generator_int.get_field(m, vtx_reindex, F, face_map, reference_corner, theta, kappa, period_jump); + reference_field = Field::generate_reference_field(V, F, reference_corner); + + return std::make_tuple(reference_field, theta, kappa, period_jump); +} + +void lift_basis_loops( + const VoronoiOverlayMesh& voronoi_mesh, + const MarkedPennerConeMetric& marked_metric, + const VectorX& rotation_form, + std::vector>& basis_loops, + std::vector& kappa) +{ + std::vector> orig_basis_loops = {}; + basis_loops.clear(); + kappa.clear(); + if (!Field::is_trivial_torus(marked_metric)) + { + // generate the homology basis on the overlay mesh + Mesh m_ref = voronoi_mesh.generate_mesh(); + Holonomy::HomologyBasisGenerator homology_basis_generator(m_ref, 0, Holonomy::HomologyBasisGenerator::Weighting::minimal_homotopy); + + // project basis loops on the simplified and original mesh + int num_homology_basis_loops = homology_basis_generator.n_homology_basis_loops(); + basis_loops.reserve(num_homology_basis_loops); + orig_basis_loops.reserve(num_homology_basis_loops); + auto [f_new2old, f_old2new] = voronoi_mesh.get_face_reductions(); + for (int i = 0; i < num_homology_basis_loops; ++i) + { + // compute the dual loop on the overlay mesh (in reduced indexing) + std::vector basis_loop = vector_compose(f_new2old, homology_basis_generator.construct_homology_basis_loop(i)); + spdlog::debug("basis loop with {} faces", basis_loop.size()); + + // project dual loops to the simplified and original mesh + std::vector simp_basis_loop = voronoi_mesh.project_dual_loop(basis_loop, voronoi_mesh.simp); + std::vector orig_basis_loop = voronoi_mesh.project_dual_loop(basis_loop, voronoi_mesh.orig); + assert(!simp_basis_loop.empty()); + assert(!orig_basis_loop.empty()); + + // build the dual loop connectivity on the simplified mesh + auto simp_dual_path = Holonomy::build_dual_path_from_face_sequence(voronoi_mesh.simp.m, simp_basis_loop); + basis_loops.push_back(std::make_unique(Holonomy::DualLoopConnectivity(simp_dual_path))); + assert(!simp_dual_path.empty()); + + // build the dual loop connectivity on the original mesh + auto orig_dual_path = Holonomy::build_dual_path_from_face_sequence(marked_metric, orig_basis_loop); + orig_basis_loops.push_back(std::make_unique(Holonomy::DualLoopList(orig_dual_path))); + assert(!orig_dual_path.empty()); + } + + // compute holonomy angles in the original mesh + DiscreteMetric discrete_metric = generate_discrete_metric(marked_metric); + kappa = compute_kappa(discrete_metric, rotation_form, orig_basis_loops); + spdlog::info("target holonomy angles are {}", formatted_vector(kappa)); + } +} + +std::tuple< + Eigen::MatrixXd, + Eigen::VectorXd, + Eigen::MatrixXd, + Eigen::MatrixXi> +refine_overlay_field( + const VoronoiOverlayMesh& voronoi_mesh, + const std::vector& vtx_reindex, + const Eigen::MatrixXi& F, + const std::vector& fn_to_f_o, + const Eigen::MatrixXi& hn_to_h_o, + const Eigen::MatrixXi& is_edge_o, + const Eigen::MatrixXd& reference_field, + const Eigen::VectorXd& theta, + const Eigen::MatrixXd& kappa, + const Eigen::MatrixXi& period_jump +) +{ + int num_faces = fn_to_f_o.size(); + Eigen::MatrixXd reference_field_r(num_faces, 3); + Eigen::VectorXd theta_r(num_faces); + Eigen::MatrixXd kappa_r(num_faces, 3); + Eigen::MatrixXi period_jump_r(num_faces, 3); + for (int fi = 0; fi < num_faces; ++fi) + { + int f_orig = fn_to_f_o[fi]; + reference_field_r.row(fi) = reference_field.row(f_orig); + theta_r[fi] = theta[f_orig]; + + for (int j = 0; j < 3; ++j) + { + // if e + if (is_edge_o(fi, j) == 0) + { + kappa_r(fi, j) = 0.; + period_jump_r(fi, j) = 0; + } + else + { + int Hij = hn_to_h_o(fi, j); + int Vk = vtx_reindex[voronoi_mesh.orig.m.to[voronoi_mesh.orig.m.n[Hij]]]; + int i = 0; + while (F(f_orig, i) != Vk) + { + i++; + if (i>= 3) + { + spdlog::error("could not match offset for {}", f_orig); + break; + } + } + kappa_r(fi, j) = kappa(f_orig, i); + period_jump_r(fi, j) = period_jump(f_orig, i); + } + } + } + + return std::make_tuple(reference_field_r, theta_r, kappa_r, period_jump_r); +} + + +} // namespace Resolution +} // namespace Penner diff --git a/src/resolution/optimization/improvement.cpp b/src/resolution/optimization/improvement.cpp new file mode 100644 index 0000000..bba2b75 --- /dev/null +++ b/src/resolution/optimization/improvement.cpp @@ -0,0 +1,2190 @@ +#include "resolution/optimization/improvement.h" + +#include "resolution/core/layout.h" +#include "util/vf_mesh.h" +#include "metric/quality.h" +#include "parametrization/layout.h" +#include "util/union_find.h" + +#include +#include +#include +#include + +#include "conformal_ideal_delaunay/ConformalIdealDelaunayMapping.hh" +#include "conformal_ideal_delaunay/ConformalInterface.hh" +#include "conformal_ideal_delaunay/Layout.hh" + +#include + + +#include "ExtremeOpt.h" +#include "MeshCutter.h" +#include "energy.h" +#include "main_helper.h" + + +namespace Penner { +namespace Resolution { + +// helper functions +namespace { + +Eigen::MatrixXd rows_to_matrix(const std::vector& rows, int cols) +{ + Eigen::MatrixXd M(rows.size(), cols); + for (int i = 0; i < rows.size(); ++i) + { + M.row(i) = rows[i].head(cols); + } + + return M; +} + +Eigen::MatrixXd rows_to_matrix(const std::vector& rows) +{ + Eigen::MatrixXd M(rows.size(), 2); + for (int i = 0; i < rows.size(); ++i) + { + M.row(i) = rows[i]; + } + + return M; +} + +std::vector matrix_to_rows2(const Eigen::MatrixXd& M) +{ + std::vector rows(M.rows()); + for (int i = 0; i < M.rows(); ++i) + { + rows[i] = M.row(i); + } + + return rows; +} + +std::vector matrix_to_rows3(const Eigen::MatrixXd& M) +{ + std::vector rows(M.rows()); + for (int i = 0; i < M.rows(); ++i) + { + rows[i] = M.row(i); + } + + return rows; +} + +} // namespace + + +// ---------------- +// Geometry Helpers +// ---------------- + +// measure the error in length of paired halfedges +double compute_length_error( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int hij) +{ + int hji = halfedge.opp[hij]; + Eigen::Vector2d uvi = uv[halfedge.prev[hij]]; + Eigen::Vector2d uvj = uv[hij]; + Eigen::Vector2d uvio = uv[hji]; + Eigen::Vector2d uvjo = uv[halfedge.prev[hji]]; + return abs((uvj - uvi).norm() - (uvjo - uvio).norm()); +} + +// get the normal of a face in 3D space +Eigen::Vector3d compute_face_normal( + const HalfedgeMesh& halfedge, + const std::vector& V, + int face_index) +{ + int hij = halfedge.f2h[face_index]; + int hjk = halfedge.n[hij]; + int hki = halfedge.n[hjk]; + + int vi = halfedge.to[hki]; + int vj = halfedge.to[hij]; + int vk = halfedge.to[hjk]; + + Eigen::Vector3d eij = (V[vj] - V[vi]).transpose(); + Eigen::Vector3d eik = (V[vk] - V[vi]).transpose(); + Eigen::Vector3d n = eij.cross(eik); + double n_norm = n.norm(); + if (n_norm > 0.) n /= n_norm; + + return n.transpose(); +} + +// average the two face normals adjacent to an edge +Eigen::Vector3d average_edge_normal( + const HalfedgeMesh& halfedge, + const std::vector& V, + int halfedge_index) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int f0 = halfedge.face[hij]; + int f1 = halfedge.face[hji]; + + Eigen::Vector3d n0 = compute_face_normal(halfedge, V, f0); + Eigen::Vector3d n1 = compute_face_normal(halfedge, V, f1); + return 0.5 * (n0 + n1); +} + +// measure the dihedral angle of two faces adjacent to an edge +double edge_dihedral_angle( + const HalfedgeMesh& halfedge, + const std::vector& V, + int halfedge_index) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int f0 = halfedge.face[hij]; + int f1 = halfedge.face[hji]; + + double d = compute_face_normal(halfedge, V, f0).dot(compute_face_normal(halfedge, V, f1)); + if (d < -1.) d = -1.; + if (d > 1.) d = 1.; + return acos(d); +} + +// ---------- +// Predicates +// ---------- + +// determine if the one ring of a vertex is seamless +bool is_one_ring_seamless( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int vertex_index) +{ + int h_start = halfedge.out[vertex_index]; + int hij = h_start; + do + { + if (!is_seamless_rotation(infer_float_rotation(halfedge, uv, hij))) return false; + hij = halfedge.n[halfedge.opp[hij]]; + } while (hij != h_start); + + return true; +} + +// determine if a face is flipped in the uv plane with robust predicates +bool is_face_flipped( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int face_index) +{ + int hij = halfedge.f2h[face_index]; + int hjk = halfedge.n[hij]; + int hki = halfedge.n[hjk]; + return (igl::predicates::orient2d( + uv[hki], + uv[hij], + uv[hjk]) != igl::predicates::Orientation::POSITIVE); +} + +// determine if any faces in the one ring of a vertex are flipped +bool is_one_ring_flipped( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int vertex_index) +{ + int h_start = halfedge.out[vertex_index]; + int hij = h_start; + do + { + if (is_face_flipped(halfedge, uv, halfedge.face[hij])) return true; + hij = halfedge.opp[halfedge.prev[hij]]; + } while (hij != h_start); + + return false; +} + +// determine if any faces adjacent to an edge are flipped +bool is_edge_flap_flipped( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int halfedge_index) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + return is_face_flipped(halfedge, uv, halfedge.face[hij]) || is_face_flipped(halfedge, uv, halfedge.face[hji]); +} + +// check if link around vi contains vk +bool link_contains_vertex( + const HalfedgeMesh& halfedge, + int vi, + int vk) +{ + int h_start = halfedge.out[vi]; + int hij = h_start; + do + { + if (halfedge.to[hij] == vk) return true; + + hij = halfedge.opp[halfedge.prev[hij]]; + } while (hij != h_start); + + return false; +} + +// check if link around vi does not contain duplicates +bool is_link_simple( + const HalfedgeMesh& halfedge, + int vi) +{ + int h_start = halfedge.out[vi]; + int hij = h_start; + std::set link; + int count = 0; + do + { + link.insert(halfedge.to[hij]); + ++count; + hij = halfedge.opp[halfedge.prev[hij]]; + } while (hij != h_start); + + return (link.size() == count); +} + + +// ------------- +// Displacements +// ------------- + +// compute displacements to apply to all vertices in the same vertex cycle as a halfedge +// tip to displace the halfedge tip to a given position while maintaining seamless constraints +std::vector compute_halfedge_tip_displacements( + const HalfedgeMesh& halfedge, + std::vector& uv, + int halfedge_index, + Eigen::Vector2d& position +) { + int hij = halfedge_index; + Eigen::Vector2d d = position - uv[hij]; + + Eigen::Matrix2d R = Eigen::Matrix2d::Identity(); + std::vector displacements = {}; + do + { + Eigen::Vector2d Rd = (R.inverse()) * d; + int hjk = halfedge.n[hij]; + if (halfedge.opp[hjk] != halfedge_index) + { + R = R * infer_rotation(halfedge, uv, hjk); + } + displacements.push_back(Rd); + hij = halfedge.opp[hjk]; + } while (hij != halfedge_index); + + return displacements; +} + +// compute all positions of the tips of the halfedges in the same vertex cycle as +// the tip of the given halfedge +std::vector compute_halfedge_tip_positions( + const HalfedgeMesh& halfedge, + std::vector& uv, + int halfedge_index +) { + int hij = halfedge_index; + std::vector positions = {}; + do + { + positions.push_back(uv[hij]); + hij = halfedge.opp[halfedge.n[hij]]; + } while (hij != halfedge_index); + + return positions; +} + +// apply the given vector of displacements (assumed generated from compute_halfedge_tip_displacements) +// to the tips of the halfedges in the vertex cycle +void apply_halfedge_tip_displacements( + const HalfedgeMesh& halfedge, + std::vector& uv, + int halfedge_index, + const std::vector& displacements) +{ + int hij = halfedge_index; + for (int i = 0; i < displacements.size(); ++i) + { + uv[hij] += displacements[i]; + hij = halfedge.opp[halfedge.n[hij]]; + } +} + + +// set the given tips of the halfedges in the vertex cycle to the given positions +// e.g., obtained from compute_halfedge_tip_positions +void set_halfedge_tip_positions( + const HalfedgeMesh& halfedge, + std::vector& uv, + int halfedge_index, + const std::vector& positions) +{ + int hij = halfedge_index; + for (int i = 0; i < positions.size(); ++i) + { + uv[hij] = positions[i]; + hij = halfedge.opp[halfedge.n[hij]]; + } +} + +// scale a vector of displacements by a given value (e.g., to shrink a line step) +void scale_displacements( + std::vector& displacements, + double scale) +{ + for (int i = 0; i < displacements.size(); ++i) + { + displacements[i] *= scale; + } +} + +// set the given tips of the halfedges in the vertex cycle to displace the halfedge +// tip to a given position while maintaining seamless constraints +bool set_halfedge_tip_position( + const HalfedgeMesh& halfedge, + std::vector& uv, + int halfedge_index, + Eigen::Vector2d& position +) { + std::vector displacements = compute_halfedge_tip_displacements(halfedge, uv, halfedge_index, position); + apply_halfedge_tip_displacements(halfedge, uv, halfedge_index, displacements); + return true; +} + +// ------------ +// Local Charts +// ------------ + +// build parametric layout of the given subset of faces +std::tuple get_local_halfedge_layout( + const HalfedgeMesh& halfedge, + const Eigen::MatrixXd& uv, + const std::vector& faces +) { + int num_faces = faces.size(); + Eigen::MatrixXd uv_l(3 * num_faces, 2); + Eigen::MatrixXi F_l(num_faces, 3); + + // build local connectivity + int count = 0; + for (int i = 0; i < num_faces; ++i) + { + int f = faces[i]; + int hij = halfedge.f2h[f]; + for (int j = 0; j < 3; ++j) + { + uv_l.row(count) = uv.row(hij); + F_l(i, j) = count; + + // increment + ++count; + hij = halfedge.n[hij]; + } + } + + return std::make_tuple(uv_l, F_l); +} + +// viewer for parametrization around an edge +void view_edge_flap( + const HalfedgeMesh& halfedge, + const Eigen::MatrixXd& uv, + int hij +) { + int hji = halfedge.opp[hij]; + std::vector flap_faces = {halfedge.face[hij], halfedge.face[hji]}; + auto [uv_l, F_l] = get_local_halfedge_layout(halfedge, uv, flap_faces); +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh2D("edge flap layout", uv_l, F_l); + polyscope::show(); +#endif +} + +void view_vertex_adjacent_faces( + const HalfedgeMesh& halfedge, + const Eigen::MatrixXd& uv, + int vertex_index, + std::string mesh_handle="", + bool show=true) +{ + if (mesh_handle == "") { + mesh_handle = "vertex adjacent face layout"; + } + + if ((vertex_index < 0) || (vertex_index >= halfedge.out.size())) + { + spdlog::warn("invalid vertex index {}", vertex_index); + return; + } + + // get faces adjacent to vertex + std::vector adjacent_faces = {}; + int h_start = halfedge.out[vertex_index]; + int hij = h_start; + do + { + adjacent_faces.push_back(halfedge.face[hij]); + hij = halfedge.opp[halfedge.prev[hij]]; + } while (hij != h_start); + + // build local layout + auto [uv_l, F_l] = get_local_halfedge_layout(halfedge, uv, adjacent_faces); + + // view adjacent faces +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh2D(mesh_handle, uv_l, F_l); + if (show) polyscope::show(); +#endif +} + +// build local 3D mesh and parametrization in the one ring of a vertex +std::tuple +build_local_chart( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V, + int vertex_index +) { + int degree = halfedge.vertex_degree(vertex_index); + int hij = halfedge.out[vertex_index]; + + // build local chart + std::vector V_l(degree + 1); + std::vector uv_l(degree + 1); + Eigen::MatrixXi F_l(degree, 3); + V_l[0] = V[vertex_index]; + uv_l[0] = Eigen::Vector2d::Zero(); + + // set all vertex positions in one ring using rotated displacement + Eigen::Matrix2d R = Eigen::Matrix2d::Identity(); + for (int i = 0; i < degree; ++i) + { + // set 3D geometry and face + int vj = halfedge.to[hij]; + int j = (i + 1) % degree; + V_l[i + 1] = V[vj]; + F_l.row(i) << 0, i + 1, j + 1; + + // get direction vector from i to j + Eigen::Vector2d uvi = uv[halfedge.prev[hij]]; + Eigen::Vector2d uvj = uv[hij]; + Eigen::Vector2d d = uvj - uvi; + + // get rotated direction + Eigen::Vector2d Rd = R * d; + uv_l[i + 1] = Rd; + spdlog::trace("rotation is \n{}", R); + spdlog::trace("displacing {} by {}", hij, Rd); + + // rotate outgoing direction clockwise + R = R * infer_rotation(halfedge, uv, hij); + hij = halfedge.n[halfedge.opp[hij]]; + } + + return std::make_tuple(rows_to_matrix(V_l, 3), F_l, rows_to_matrix(uv_l)); +} + +// build a VF mesh with parametrization with all triangles disconnected +std::tuple explode_halfedge( + const HalfedgeMesh& halfedge, + const std::vector& V, + const std::vector& uv) +{ + int num_faces = halfedge.count_faces(); + Eigen::MatrixXd V_c(3 * num_faces, 3); + Eigen::MatrixXi F_c(num_faces, 3); + Eigen::MatrixXd uv_c(3 * num_faces, 2); + + auto [he_new2old, he_old2new] = halfedge.get_halfedge_reductions(); + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + + for (int Fijk = 0; Fijk < f_new2old.size(); ++Fijk) + { + int f = f_new2old[Fijk]; + int hij = halfedge.f2h[f]; + for (int i = 0; i < 3; ++i) + { + int Hij = he_old2new[hij]; + V_c.row(Hij) = V[halfedge.to[hij]]; + F_c(Fijk, i) = Hij; + uv_c.row(Hij) = uv[hij]; + hij = halfedge.n[hij]; + } + } + + return std::make_tuple(V_c, F_c, uv_c); +} + +// ----------- +// Frame Field +// ----------- + + +// project field directions to the tangent planes of the face +void project_direction_to_tangent( + Eigen::MatrixXd& PD1, + Eigen::MatrixXd& PD2, + const Eigen::MatrixXd& V, + const HalfedgeMesh& halfedge, + int face_index) +{ + const int hij = halfedge.f2h[face_index]; + const int hjk = halfedge.n[hij]; + const int hki = halfedge.n[hjk]; + const int vi = halfedge.to[hki]; + const int vj = halfedge.to[hij]; + const int vk = halfedge.to[hjk]; + + // get face tangent + const Eigen::Vector3d eij = (V.row(vj) - V.row(vi)).transpose(); + const Eigen::Vector3d eik = (V.row(vk) - V.row(vi)).transpose(); + Eigen::Vector3d b1 = eij.normalized(); + Eigen::Vector3d n = eij.cross(eik).normalized(); + Eigen::Vector3d b2 = n.cross(b1).normalized(); + + // do projection + Eigen::Vector3d v = PD1.row(face_index).transpose(); + Eigen::Vector2d v_local(v.dot(b1), v.dot(b2)); + PD1.row(face_index) = (v_local[0] * b1 + v_local[1] * b2).stableNormalized().transpose(); + PD2.row(face_index) = (-v_local[1] * b1 + v_local[0] * b2).stableNormalized().transpose(); +} + +// project field directions to the tangent planes of the face +void project_direction_to_tangent( + std::vector& PD1, + std::vector& PD2, + const std::vector& V, + const HalfedgeMesh& halfedge, + int face_index) +{ + const int hij = halfedge.f2h[face_index]; + const int hjk = halfedge.n[hij]; + const int hki = halfedge.n[hjk]; + const int vi = halfedge.to[hki]; + const int vj = halfedge.to[hij]; + const int vk = halfedge.to[hjk]; + + const Eigen::Vector3d eij = (V[vj] - V[vi]).transpose(); + const Eigen::Vector3d eik = (V[vk] - V[vi]).transpose(); + Eigen::Vector3d b1 = eij.normalized(); + Eigen::Vector3d n = eij.cross(eik).normalized(); + Eigen::Vector3d b2 = n.cross(b1).normalized(); + + Eigen::Vector3d v = PD1[face_index].transpose(); + Eigen::Vector2d v_local(v.dot(b1), v.dot(b2)); + PD1[face_index] = (v_local[0] * b1 + v_local[1] * b2).stableNormalized().transpose(); + PD2[face_index] = (-v_local[1] * b1 + v_local[0] * b2).stableNormalized().transpose(); +} + +// void average_field_across_edge( +// Eigen::MatrixXd& PD1, +// Eigen::MatrixXd& PD2, +// const Eigen::MatrixXd& V, +// const HalfedgeMesh& halfedge, +// int halfedge_index +// ) +// { +// int hij = halfedge_index; +// int hji = halfedge.opp[hij]; +// int fijk = halfedge.face[hij]; +// int fjil = halfedge.face[hji]; + +// // get average direction of faces +// Eigen::Vector3d v0 = PD1.row(fijk).transpose(); +// Eigen::Vector3d v1 = PD1.row(fjil).transpose(); +// Eigen::Vector3d v = 0.5 * (v0 + v1); + +// // project directions to faces +// for (int f : { fijk, fjil }) +// { +// PD1.row(f) = v; +// project_direction_to_tangent(PD1, PD2, V, halfedge, f); +// } + +// } + +// average the field directions across the given edge and project to the tangent planes +void average_field_across_edge( + std::vector& PD1, + std::vector& PD2, + const std::vector& V, + const HalfedgeMesh& halfedge, + int halfedge_index +) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int fijk = halfedge.face[hij]; + int fjil = halfedge.face[hji]; + + Eigen::Vector3d v = 0.5 * (PD1[fijk] + PD1[fjil]).transpose(); + for (int f : { fijk, fjil }) + { + PD1[f] = v.transpose(); + project_direction_to_tangent(PD1, PD2, V, halfedge, f); + } +} + +// ---------------- +// Triangle Quality +// ---------------- + +template +Eigen::Vector3d compute_triangle_lengths( + const HalfedgeMesh& halfedge, + const std::vector& positions, + int face_index, + bool is_tip_index) +{ + int hij = halfedge.f2h[face_index]; + std::array face_uv; + for (int i = 0; i < 3; ++i) + { + face_uv[i] = (is_tip_index) ? positions[hij] : positions[halfedge.to[hij]]; + hij = halfedge.n[hij]; + } + + Eigen::Vector3d lengths; + for (int i = 0; i < 3; ++i) + { + int j = (i + 1) % 3; + lengths[i] = (face_uv[j] - face_uv[i]).norm(); + } + + return lengths; +} + +template +double compute_triangle_quality( + const HalfedgeMesh& halfedge, + const std::vector& positions, + int face_index, + bool is_tip_index) +{ + Eigen::Vector3d l = compute_triangle_lengths(halfedge, positions, face_index, is_tip_index); + return Holonomy::compute_triangle_quality(l[0], l[1], l[2]); +} + +template +double compute_edge_quality( + const HalfedgeMesh& halfedge, + const std::vector& positions, + int halfedge_index, + bool is_tip_index) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + return compute_triangle_quality(halfedge, positions, halfedge.face[hij], is_tip_index) + + compute_triangle_quality(halfedge, positions, halfedge.face[hji], is_tip_index); +} + + +template +double compute_vertex_quality( + const HalfedgeMesh& halfedge, + const std::vector& positions, + int vertex_index, + bool is_tip_index) +{ + double quality = 0.; + int h_start = halfedge.out[vertex_index]; + int hij = h_start; + do + { + quality += compute_triangle_quality(halfedge, positions, halfedge.face[hij], is_tip_index); + hij = halfedge.opp[halfedge.prev[hij]]; + } while (hij != h_start); + + return quality; +} + +template +double compute_total_quality( + const HalfedgeMesh& halfedge, + const std::vector& positions, + bool is_tip_index) +{ + double quality = 0.; + int num_faces = halfedge.f2h.size(); + for (int fijk = 0; fijk < num_faces; ++fijk) + { + if (halfedge.is_face_deleted(fijk)) continue; + quality += compute_triangle_quality(halfedge, positions, fijk, is_tip_index); + } + + return quality; +} + +// ------------------- +// Symmetric Dirichlet +// ------------------- + +// compute the symmetric dirichlet energy for a parametrization +double symmetric_dirichlet_energy( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V, + int vertex_index, + int norm_p=1) +{ + // build local patch + auto [V_l, F_l, uv_l] = build_local_chart(halfedge, uv, V, vertex_index); + + // get gradient from local patch + Eigen::SparseMatrix G; + SymDir::get_grad_op(V_l, F_l, G); + + Eigen::MatrixXd Ji; + SymDir::jacobian_from_uv(G, uv_l, Ji); + + Eigen::VectorXd area; + igl::doublearea(V_l, F_l, area); + area = 0.5 * area; + double energy = SymDir::compute_energy_from_jacobian(Ji, area, norm_p); + spdlog::info("symmetric dirichlet energy is {}", energy); + spdlog::info("area is {}", area.sum()); + + return energy * area.sum(); +} + +double symmetric_dirichlet_energy( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V, + int norm_p=1) +{ + auto [V_c, F_c, uv_c] = explode_halfedge(halfedge, V, uv); + + // get gradient from local patch + Eigen::SparseMatrix G; + SymDir::get_grad_op(V_c, F_c, G); + + Eigen::MatrixXd Ji; + SymDir::jacobian_from_uv(G, uv_c, Ji); + + Eigen::VectorXd area; + igl::doublearea(V_c, F_c, area); + double energy = SymDir::compute_energy_from_jacobian(Ji, area, norm_p); + + return energy; +} + +Eigen::VectorXd face_symmetric_dirichlet_values( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V) +{ + auto [V_c, F_c, uv_c] = explode_halfedge(halfedge, V, uv); + + Eigen::VectorXd face_energy = SymDir::symmetric_dirichlet_energy(V_c, F_c, uv_c, 1); + + // map from reduced to full face list + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + Eigen::VectorXd full_face_energy = Eigen::VectorXd::Zero(f_old2new.size()); + for (int f = 0; f < f_new2old.size(); ++f) + { + full_face_energy[f_new2old[f]] = face_energy[f]; + } + + return full_face_energy; +} + +Eigen::VectorXd vertex_symmetric_dirichlet_values( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V) +{ + Eigen::VectorXd face_sym_dir = face_symmetric_dirichlet_values(halfedge, uv, V); + + int num_vertices = halfedge.out.size(); + int num_halfedges = halfedge.n.size(); + Eigen::VectorXd vertex_sym_dir = Eigen::VectorXd::Zero(num_vertices); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + int vi = halfedge.to[hij]; + int f = halfedge.face[hij]; + vertex_sym_dir(vi) = max(vertex_sym_dir(vi), face_sym_dir(f)); + } + + return vertex_sym_dir; +} + + +void view_halfedge_parameterization( + const HalfedgeMesh& halfedge, + const Eigen::MatrixXd& V, + const Eigen::MatrixXd& uv +) { + auto V_rows = matrix_to_rows3(V); + auto uv_rows = matrix_to_rows2(uv); + auto [V_c, F_c, uv_c] = explode_halfedge(halfedge, V_rows, uv_rows); + + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + Eigen::VectorXd sym_dir = vector_compose(face_symmetric_dirichlet_values(halfedge, uv_rows, V_rows), f_new2old); + Eigen::VectorXd area; + igl::doublearea(V_c, F_c, area); + + // view adjacent faces +#ifdef ENABLE_VISUALIZATION + polyscope::init(); + polyscope::registerSurfaceMesh("mesh", V_c, F_c) + ->addVertexParameterizationQuantity("uv", uv_c); + polyscope::getSurfaceMesh("mesh") + ->addFaceScalarQuantity("sym dirichlet", sym_dir); + polyscope::getSurfaceMesh("mesh") + ->addFaceScalarQuantity("area", area); + polyscope::show(); +#endif +} + +Eigen::VectorXd symmetric_dirichlet( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT) +{ + Eigen::MatrixXd V_cut; + cut_mesh_along_parametrization_seams(V, F, uv, FT, V_cut); + return SymDir::symmetric_dirichlet_energy(V_cut, FT, uv, 1); +} + + +MeshImprover::MeshImprover( + const Eigen::MatrixXd& V, + const Eigen::MatrixXi& F, + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT +) { + avg_edge_length = igl::avg_edge_length(V, F); + spdlog::info("average edge length: {}", avg_edge_length); + abs_collapse_edge_thres = 0.01 * avg_edge_length; + + Eigen::VectorXd area; + igl::doublearea(V, F, area); + avg_area = 0.5 * area.mean(); + + // Convert to halfedge + std::tie(halfedge, vtx_reindex, he_to_corner) = generate_halfedge(F); + + // TODO: replace with final mesh connectivity + VectorX cone_angles = compute_cone_angles(V, F, uv, FT); + int num_vertices = V.rows(); + is_cone = std::vector(num_vertices, false); + for (int vi = 0; vi < num_vertices; ++vi) + { + if (!float_equal(cone_angles[vtx_reindex[vi]], 2 * PI)) + { + is_cone[vi] = true; + } + } + + // transfer uvs to halfedge + halfedge_uv.resize(he_to_corner.size(), Eigen::Vector2d::Zero()); + for (int hij = 0; hij < he_to_corner.size(); ++hij) + { + auto [f, i] = he_to_corner[hij]; + int hjk = halfedge.n[hij]; + Eigen::Vector2d uvk = uv.row(FT(f, i)); + halfedge_uv[hjk] = uvk; + } + + // reindex vertices + V_r.resize(V.rows()); + for (int vi = 0; vi < vtx_reindex.size(); ++vi) + { + V_r[vi] = V.row(vtx_reindex[vi]); + } +} + + +void MeshImprover::set_combed_field( + const Eigen::MatrixXd& PD1, + const Eigen::MatrixXd& PD2 +) +{ + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + const int num_faces = halfedge.count_faces(); + assert(PD1.rows() == f_old2new.size()); + assert(PD2.rows() == f_old2new.size()); + assert(PD1.cols() == PD2.cols()); + + PD1_r.resize(num_faces, Eigen::Vector3d::Zero()); + PD2_r.resize(num_faces, Eigen::Vector3d::Zero()); + + for (int fi = 0; fi < num_faces; ++fi) + { + const int f = f_new2old[fi]; + PD1_r[fi] = PD1.row(f); + PD2_r[fi] = PD2.row(f); + } +} + +std::tuple +MeshImprover::get_combed_field() const +{ + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + Eigen::MatrixXd PD1 = Eigen::MatrixXd::Zero(f_new2old.size(), 3); + Eigen::MatrixXd PD2 = Eigen::MatrixXd::Zero(f_new2old.size(), 3); + + if (PD1_r.empty() || PD2_r.empty()) + { + return std::make_tuple(PD1, PD2); + } + + for (int fi = 0; fi < f_new2old.size(); ++fi) + { + const int f = f_new2old[fi]; + PD1.row(fi) = PD1_r[f]; + PD2.row(fi) = PD2_r[f]; + } + + return std::make_tuple(PD1, PD2); +} + +void MeshImprover::improve(int num_iter) +{ + igl::Timer timer, method_timer; + + // get initial mesh quality + results["initial-quality"] = compute_total_quality(halfedge, halfedge_uv, true); + //results["initial-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + results["initial-energy"] = compute_energy(); + spdlog::info("initial quality: {}", results["initial-quality"]); + if (show_smooth) view_halfedge_parameterization(halfedge, rows_to_matrix(V_r, 3), rows_to_matrix(halfedge_uv)); + + timer.start(); + std::vector iter_results(num_iter); + for (int i = 0; i < num_iter; ++i) + { + // smooth vertices + if (!skip_smooth) + { + // get pre-smooth statistics + method_timer.start(); + spdlog::info("Optimizing symmetric Dirichlet energy with seamless constraints"); + iter_results[i]["symdir-energy"] = compute_energy(); + iter_results[i]["symdir-time"] = method_timer.getElapsedTime(); + spdlog::info("post-symdir energy: {}", iter_results[i]["symdir-energy"]); + + // smooth all vertices + method_timer.start(); + for (int vi = 0; vi < halfedge.out.size(); ++vi) + { + if (halfedge.is_vertex_deleted(vi)) continue; + + // cannot move cone vertex positions + if (is_cone[vi]) + { + spdlog::debug("skipping nonplanar vertex"); + continue; // skip cone vertices with seam + } + assert(!is_one_ring_flipped(halfedge, halfedge_uv, vi)); + + // locally optimize vertex position + local_energy_opt(vi); + } + + // get post-smooth statistics + iter_results[i]["smooth-quality"] = compute_total_quality(halfedge, V_r, false); + iter_results[i]["smooth-uv-quality"] = compute_total_quality(halfedge, halfedge_uv, true); + //iter_results[i]["smooth-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + iter_results[i]["smooth-energy"] = compute_energy(); + iter_results[i]["smooth-time"] = method_timer.getElapsedTime(); + spdlog::info("post-smoothing energy: {}", iter_results[i]["smooth-energy"]); + spdlog::info("post-smoothing quality: {}", iter_results[i]["smooth-quality"]); + spdlog::info("post-smoothing uv quality: {}\n", iter_results[i]["smooth-uv-quality"]); + } + + // try flipping edges + if (!skip_flips) + { + method_timer.start(); + int flip_count = 0; + int num_edges = halfedge.e2h.size(); + for (int eij = 0; eij < num_edges; ++eij) + { + if (halfedge.is_edge_deleted(eij)) continue; + int hij = halfedge.e2h[eij]; + + double orig_quality = compute_edge_quality(halfedge, V_r, hij, false); + double orig_uv_quality = compute_edge_quality(halfedge, halfedge_uv, hij, true); + double orig_total_quality = orig_quality + orig_uv_quality; + spdlog::debug("original quality is {}, {}", orig_quality, orig_uv_quality); + + // don't flip to self adjacent edge + int hji = halfedge.opp[hij]; + int hjk = halfedge.n[hij]; + int hil = halfedge.n[hji]; + if (halfedge.to[hjk] == halfedge.to[hil]) + { + spdlog::debug("tried to flip to self adjacent edge"); + continue; + } + int vk = halfedge.to[hjk]; + int vl = halfedge.to[hil]; + if ((link_contains_vertex(halfedge, vk, vl)) || (link_contains_vertex(halfedge, vl, vk))) + { + spdlog::debug("flipping edge would create double edge"); + continue; + } + + // dihedral angle pre-check + double flip_angle_thres = PI / 90.; + if (abs(edge_dihedral_angle(halfedge, V_r, hij)) > flip_angle_thres) + { + spdlog::debug("edge too sharp to flip"); + continue; + } + Eigen::Vector3d nij = average_edge_normal(halfedge, V_r, hij); + + // get initial field (ater fixing local patch rotation) + transform_opposite_face(hij); + int fijk = halfedge.face[hij]; + int fjil = halfedge.face[hji]; + Eigen::Vector3d d11 = PD1_r[fijk]; + Eigen::Vector3d d12 = PD1_r[fjil]; + Eigen::Vector3d d21 = PD2_r[fijk]; + Eigen::Vector3d d22 = PD2_r[fjil]; + + // perform flip + flip_edge_ccw(hij); + + // get post-flip statistics + double quality = compute_edge_quality(halfedge, V_r, hij, false); + double uv_quality = compute_edge_quality(halfedge, halfedge_uv, hij, true); + double total_quality = quality + uv_quality; + Eigen::Vector3d nkl = average_edge_normal(halfedge, V_r, hij); + spdlog::debug("new quality is {}, {}", quality, uv_quality); + + // check if edge flip improves quality and doesn't flip sharp edges + if ((is_edge_flap_flipped(halfedge, halfedge_uv, hij)) + || (quality > orig_quality) + || (uv_quality > orig_uv_quality) + || (nij.dot(nkl) < 0.5)) + { + flip_edge_clockwise(hij); + PD1_r[fijk] = d11; + PD1_r[fjil] = d12; + PD2_r[fijk] = d21; + PD2_r[fjil] = d22; + + // check if flip undone successfully + quality = compute_edge_quality(halfedge, V_r, hij, false); + uv_quality = compute_edge_quality(halfedge, halfedge_uv, hij, true); + assert(float_equal(uv_quality, orig_uv_quality, 1e-8)); + } + else + { + flip_count++; + } + } + + // get post-flip statistics + iter_results[i]["flip-count"] = flip_count; + iter_results[i]["flip-quality"] = compute_total_quality(halfedge, V_r, false); + iter_results[i]["flip-uv-quality"] = compute_total_quality(halfedge, halfedge_uv, true); + //iter_results[i]["flip-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + iter_results[i]["flip-energy"] = compute_energy(); + iter_results[i]["flip-time"] = method_timer.getElapsedTime(); + spdlog::info("number of edge flips: {}", flip_count); + spdlog::info("post-flip energy: {}", iter_results[i]["flip-energy"]); + spdlog::info("post-flip quality: {}", iter_results[i]["flip-quality"]); + spdlog::info("post-flip uv quality: {}\n", iter_results[i]["flip-uv-quality"]); + } + + // try collapsing edges + if (!skip_collapse) + { + method_timer.start(); + int collapse_count = 0; + int num_halfedges = halfedge.n.size(); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + + // make sure edge can be collapsed + if (!halfedge.is_collapse_edge_valid(hij)) continue; + if (!halfedge.is_collapse_link_valid(hij)) continue; + int vi = halfedge.to[halfedge.opp[hij]]; + int vj = halfedge.to[hij]; + assert(is_link_simple(halfedge, vi)); + assert(is_link_simple(halfedge, vj)); + if (!is_link_simple(halfedge, vi)) continue; + if (!is_link_simple(halfedge, vj)) continue; + + + // check if collapsed vertex vj is a cone and prohibit if so + if (is_cone[vj]) continue; + + // only collapse small edges + if ((V_r[vi] - V_r[vj]).norm() > abs_collapse_edge_thres) + { + continue; + } + + // collapse edge + bool success = collapse_edge(hij); + if (!success) continue; + else collapse_count++; + } + + // check post-collapse data + iter_results[i]["collape-count"] = collapse_count; + iter_results[i]["collapse-quality"] = compute_total_quality(halfedge, V_r, false); + iter_results[i]["collapse-uv-quality"] = compute_total_quality(halfedge, halfedge_uv, true); + //iter_results[i]["collapse-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + iter_results[i]["collapse-energy"] = compute_energy(); + iter_results[i]["collapse-time"] = method_timer.getElapsedTime(); + spdlog::info("number of edge collapses: {}", collapse_count); + spdlog::info("post-collapse energy: {}", iter_results[i]["collapse-energy"]); + spdlog::info("post-collapse quality: {}", iter_results[i]["collapse-quality"]); + spdlog::info("post-collapse uv quality: {}\n", iter_results[i]["collapse-uv-quality"]); + } + + // try refining edges + double abs_split_edge_thres = 10 * avg_edge_length; + if (!skip_split) + { + method_timer.start(); + int split_count = 0; + int num_edges = halfedge.e2h.size(); + for (int eij = 0; eij < num_edges; ++eij) + { + if (halfedge.is_edge_deleted(eij)) continue; + int hij = halfedge.e2h[eij]; + + // check if edge is too long to split + if (halfedge.is_halfedge_deleted(hij)) continue; + int vi = halfedge.to[halfedge.opp[hij]]; + int vj = halfedge.to[hij]; + double lij = (V_r[vi] - V_r[vj]).norm(); + if (lij < abs_split_edge_thres) continue; + + // split the edge + spdlog::debug("splitting edge {} of length {}", hij, lij); + split_edge(hij); + split_count++; + } + + // get post-split statistic + iter_results[i]["split-count"] = split_count; + iter_results[i]["split-quality"] = compute_total_quality(halfedge, V_r, false); + iter_results[i]["split-uv-quality"] = compute_total_quality(halfedge, halfedge_uv, true); + //iter_results[i]["split-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + iter_results[i]["split-energy"] = compute_energy(); + iter_results[i]["split-time"] = method_timer.getElapsedTime(); + spdlog::info("number of splits: {}", split_count); + spdlog::info("post-split energy: {}", iter_results[i]["split-energy"]); + spdlog::info("post-split quality: {}", iter_results[i]["split-quality"]); + spdlog::info("post-split uv quality: {}\n", iter_results[i]["split-uv-quality"]); + } + } + + // get final statistics + results["iter-results"] = iter_results; + results["final-sym-dir"] = symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r); + results["final-energy"] = compute_energy(); + results["total-time"] = timer.getElapsedTime(); + + // check final validity + if (!halfedge.is_valid()) spdlog::error("halfedge is not valid"); + if (!halfedge.is_simplicial()) spdlog::error("mesh is not simplicial"); + assert(halfedge.is_valid()); + assert(halfedge.is_simplicial()); + + // check final seamless error + int num_halfedges = halfedge.n.size(); + double length_error = 0.; + double seamless_error = 0.; + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + length_error = max(length_error, compute_length_error(halfedge, halfedge_uv, hij)); + seamless_error = max(seamless_error, compute_seamless_error(halfedge, halfedge_uv, hij)); + } + spdlog::info("length error: {}", length_error); + spdlog::info("seamless error: {}", seamless_error); + spdlog::info("final energy: {}", results["final-energy"]); + spdlog::info("total time: {}", results["total-time"]); + + if (show_smooth) view_halfedge_parameterization(halfedge, rows_to_matrix(V_r, 3), rows_to_matrix(halfedge_uv)); +} + +std::tuple +MeshImprover::get_improved_mesh() +{ + int num_faces = halfedge.f2h.size(); + int num_halfedges = halfedge.n.size(); + + // build new layout + std::vector is_processed_face(num_faces, false); + std::vector is_cut_halfedge(num_halfedges, true); + + // Initialize queue and record of faces to process + std::queue Q; + int f_start = 0; + while (halfedge.is_face_deleted(f_start)) ++f_start; + Q.push(halfedge.f2h[f_start]); + is_processed_face[f_start] = true; + + // get halfedge cut and make sure layout faces are adjacent in interior + while (!Q.empty()) { + // Get next halfedge to process + int h = Q.front(); + Q.pop(); + + // Get other triangle edges + int hn = halfedge.n[h]; + int hp = halfedge.n[hn]; + int hno = halfedge.opp[hn]; + int hpo = halfedge.opp[hp]; + int ho = halfedge.opp[h]; + + // Check if next edge triangle should be laid out + if (!is_processed_face[halfedge.face[hno]]) { + transform_opposite_face(hn); + is_cut_halfedge[hn] = false; + is_cut_halfedge[hno] = false; + + is_processed_face[halfedge.face[hno]] = true; + Q.push(hno); + } + + // Check if previous edge triangle should be laid out + if (!is_processed_face[halfedge.face[hpo]]) { + transform_opposite_face(hp); + is_cut_halfedge[hp] = false; + is_cut_halfedge[hpo] = false; + + is_processed_face[halfedge.face[hpo]] = true; + Q.push(hpo); + } + + // Check if current edge triangle should be laid out + // NOTE: Should only be used once for original edge + if (!is_processed_face[halfedge.face[ho]]) { + transform_opposite_face(h); + is_cut_halfedge[h] = false; + is_cut_halfedge[ho] = false; + + is_processed_face[halfedge.face[ho]] = true; + Q.push(ho); + } + } + + // trim cut + bool any_trimmed = true; + while (any_trimmed) + { + any_trimmed = false; + for (int hij = 0; hij < num_halfedges; hij++) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + if (!is_cut_halfedge[hij]) continue; + int vj = halfedge.to[hij]; + if ((count_valence(halfedge.n, halfedge.opp, hij, is_cut_halfedge) == 1) && (!is_cone[vj])) + { + int hji = halfedge.opp[hij]; + is_cut_halfedge[hij] = false; + is_cut_halfedge[hji] = false; + any_trimmed = true; + continue; + } + } + } + + Scalar length_error = 0.; + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + length_error = max(length_error, compute_length_error(halfedge, halfedge_uv, hij)); + } + spdlog::info("length error after layout: {}", length_error); + + // get uv vertices + UnionFind uv_vertices(num_halfedges); + for (int hij = 0; hij < num_halfedges; ++hij) + { + if (halfedge.is_halfedge_deleted(hij)) continue; + + // check if cut halfedge + int hkj = halfedge.opp[halfedge.n[hij]]; + if (is_cut_halfedge[hkj]) continue; + + // union tips otherwise + uv_vertices.union_sets(hij, hkj); + } + + // build uv vertices + auto [he_new2old, he_old2new] = halfedge.get_halfedge_reductions(); + auto [f_new2old, f_old2new] = halfedge.get_face_reductions(); + int num_uv_vertices = uv_vertices.count_sets(); + std::vector h2fv = uv_vertices.index_sets(); + Eigen::MatrixXd uv_r(num_uv_vertices, 2); + Eigen::MatrixXi F_r(halfedge.count_faces(), 3); + Eigen::MatrixXi FT_r(halfedge.count_faces(), 3); + for (int f = 0; f < num_faces; ++f) + { + if (halfedge.is_face_deleted(f)) continue; + + int hij = halfedge.f2h[f]; + for (int i = 0; i < 3; ++i) + { + int Fijk = f_old2new[f]; + int vi = halfedge.to[hij]; + int uvi = h2fv[hij]; + uv_r.row(uvi) = halfedge_uv[hij]; + F_r(Fijk, i) = vi; + FT_r(Fijk, i) = uvi; + hij = halfedge.n[hij]; + } + } + + Eigen::MatrixXd V_out = rows_to_matrix(V_r, 3); + remove_unreferenced_inplace(V_out, F_r); + remove_unreferenced_inplace(uv_r, FT_r); + + return std::make_tuple(V_out, F_r, uv_r, FT_r); +} + +void MeshImprover::get_uv( + Eigen::MatrixXd& uv, + Eigen::MatrixXi& FT +) const +{ + // transfer uvs to halfedge + for (int hij = 0; hij < he_to_corner.size(); ++hij) + { + auto [f, i] = he_to_corner[hij]; + int hjk = halfedge.n[hij]; + uv.row(FT(f, i)) = halfedge_uv[hjk]; + } +} + +void MeshImprover::set_uv( + const Eigen::MatrixXd& uv, + const Eigen::MatrixXi& FT +) +{ + // transfer uvs to halfedge + for (int hij = 0; hij < he_to_corner.size(); ++hij) + { + auto [f, i] = he_to_corner[hij]; + int hjk = halfedge.n[hij]; + halfedge_uv[hjk] = uv.row(FT(f, i)); + } +} + +void MeshImprover::transform_opposite_face(int hij) +{ + if (PD1_r.empty() || PD2_r.empty()) + { + Penner::Resolution::transform_opposite_face(halfedge, halfedge_uv, hij); + return; + } + Penner::Resolution::transform_opposite_face(halfedge, halfedge_uv, V_r, PD1_r, PD2_r, hij); +} + +void MeshImprover::transform_vertex_adjacent_faces( + int vertex_index +) { + int h_start = halfedge.out[vertex_index]; + int hij = h_start; + do + { + // transform opposite face (clockwise from current face) + transform_opposite_face(hij); + + // need to rotate halfedge clockwise into newly moved face + hij = halfedge.n[halfedge.opp[hij]]; + } while (hij != h_start); +} + +Eigen::Matrix MeshImprover::compute_face_tangent_gradient( + int face_index) const +{ + // renaming indices of vertices of triangles for convenience + int f = face_index; + int hki = halfedge.f2h[f]; + int hij = halfedge.n[hki]; + int hjk = halfedge.n[hij]; + int vi = halfedge.to[hki]; + int vj = halfedge.to[hij]; + int vk = halfedge.to[hjk]; + + // #F x 3 matrices of triangle edge vectors, named after opposite vertices + Eigen::Vector3d vkj = V_r[vk] - V_r[vj]; + Eigen::Vector3d vik = V_r[vi] - V_r[vk]; + Eigen::Vector3d vji = V_r[vj] - V_r[vi]; + Eigen::Vector3d n = vkj.cross(vik); + + // area of parallelogram is twice area of triangle + // area of parallelogram is || v1 x v2 || + // This does correct l2 norm of rows, so that it contains #F list of twice + // triangle areas + double dblA = std::sqrt(n.dot(n)); + Eigen::Vector3d u = n / dblA; + + // if area of face is too small, use scaled standard simplex + if (dblA < 2. * min_rel_area * avg_area) + { + spdlog::debug("using standard simplex for face {} with area {}", f, dblA); + double target_edge_length = std::sqrt(4 * avg_area / std::sqrt(3)); + Eigen::Vector3d Vi, Vj, Vk; + Vi.setZero(); + Vj.setZero(); + Vk.setZero(); + Vi(0) = target_edge_length * std::sqrt(min_rel_area); + Vj(1) = target_edge_length * std::sqrt(min_rel_area); + Vk(2) = target_edge_length * std::sqrt(min_rel_area); + vkj = Vk - Vj; + vik = Vi - Vk; + vji = Vj - Vi; + n = vkj.cross(vik); + dblA = std::sqrt(n.dot(n)); + u = n / dblA; + } + + // rotate each vector 90 degrees around normal + double normji = std::sqrt(vji.dot(vji)); + double normik = std::sqrt(vik.dot(vik)); + Eigen::Vector3d eperp_ji = u.cross(vji); + eperp_ji = eperp_ji / std::sqrt(eperp_ji.dot(eperp_ji)); + eperp_ji *= normji / dblA; + Eigen::Vector3d eperp_ik = u.cross(vik); + eperp_ik = eperp_ik / std::sqrt(eperp_ik.dot(eperp_ik)); + eperp_ik *= normik / dblA; + Eigen::Vector3d eperp_kj = -eperp_ji - eperp_ik; + + // get surface frame + Eigen::Vector3d b1 = vji.stableNormalized(); + Eigen::Vector3d b2 = (n.cross(b1)).stableNormalized(); + + // compute map from vertex values to x, y derivatives + Eigen::Matrix G; + G(0, 0) = eperp_kj.dot(b1); + G(0, 1) = eperp_ik.dot(b1); + G(0, 2) = eperp_ji.dot(b1); + G(1, 0) = eperp_kj.dot(b2); + G(1, 1) = eperp_ik.dot(b2); + G(1, 2) = eperp_ji.dot(b2); + + return G; +} + +Eigen::Matrix MeshImprover::compute_face_gradient( + int face_index) const +{ + // renaming indices of vertices of triangles for convenience + int f = face_index; + int hki = halfedge.f2h[f]; + int hij = halfedge.n[hki]; + int hjk = halfedge.n[hij]; + int vi = halfedge.to[hki]; + int vj = halfedge.to[hij]; + int vk = halfedge.to[hjk]; + + // #F x 3 matrices of triangle edge vectors, named after opposite vertices + Eigen::Vector3d vkj = V_r[vk] - V_r[vj]; + Eigen::Vector3d vik = V_r[vi] - V_r[vk]; + Eigen::Vector3d vji = V_r[vj] - V_r[vi]; + Eigen::Vector3d n = vkj.cross(vik); + // area of parallelogram is twice area of triangle + // area of parallelogram is || v1 x v2 || + // This does correct l2 norm of rows, so that it contains #F list of twice + // triangle areas + double dblA = std::sqrt(n.dot(n)); + Eigen::Vector3d u = n / dblA; + + // rotate each vector 90 degrees around normal + double normji = std::sqrt(vji.dot(vji)); + double normik = std::sqrt(vik.dot(vik)); + Eigen::Vector3d eperp_ji = u.cross(vji); + eperp_ji = eperp_ji / std::sqrt(eperp_ji.dot(eperp_ji)); + eperp_ji *= normji / dblA; + Eigen::Vector3d eperp_ik = u.cross(vik); + eperp_ik = eperp_ik / std::sqrt(eperp_ik.dot(eperp_ik)); + eperp_ik *= normik / dblA; + Eigen::Vector3d eperp_kj = -eperp_ji - eperp_ik; + + // compute map from vertex values to x, y derivatives + Eigen::Matrix G; + G.col(0) = eperp_kj; + G.col(1) = eperp_ik; + G.col(2) = eperp_ji; + + return G; +} + +double MeshImprover::compute_face_area(int face_index) const +{ + int hij = halfedge.f2h[face_index]; + int hjk = halfedge.n[hij]; + int hki = halfedge.n[hjk]; + int vi = halfedge.to[hki]; + int vj = halfedge.to[hij]; + int vk = halfedge.to[hjk]; + + return compute_triangle_area(V_r[vi], V_r[vj], V_r[vk]); +} + +double MeshImprover::compute_total_area() const +{ + double area = 0.; + int num_faces = halfedge.f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (halfedge.is_face_deleted(f)) continue; + area += compute_face_area(f); + } + + return area; +} + +double MeshImprover::compute_face_energy(int face_index) const +{ + int f = face_index; + double energy = 0.; + + // get face uv vertices + Eigen::Matrix face_uv = compute_face_uv(f); + + // compute face energy + double area = compute_face_area(f); + Eigen::Matrix G = compute_face_tangent_gradient(f); + Eigen::Matrix J_sq = G * face_uv; + energy += area * symdir_weight * SymDir::symmetric_dirichlet_energy_t(J_sq(0, 0), J_sq(1, 0), J_sq(0, 1), J_sq(1, 1), norm_p); + + if ((alignment_weight > 0) && (area > min_rel_area * avg_area)) + { + Eigen::Matrix grad = compute_face_gradient(f); + Eigen::Matrix Guv = grad * face_uv; + Eigen::Matrix uT_vT; + uT_vT.col(0) = PD1_r[f]; + uT_vT.col(1) = PD2_r[f]; + Eigen::MatrixXd R = Guv - uT_vT; + energy += alignment_weight * area * (R.transpose() * R).trace(); + } + + return energy; +} + +double MeshImprover::compute_vertex_energy(int vertex_index) const +{ + int vi = vertex_index; + int hij = halfedge.out[vi]; + int hik = hij; + double energy = 0.; + do + { + int f = halfedge.face[hik]; + energy += compute_face_energy(f); + + // iterate around vertex + hik = halfedge.n[halfedge.opp[hik]]; + } while (hik != hij); + + return energy; +} + +double MeshImprover::compute_energy() const +{ + double energy = 0.; + int num_faces = halfedge.f2h.size(); + for (int f = 0; f < num_faces; ++f) + { + if (halfedge.is_face_deleted(f)) continue; + energy += compute_face_energy(f); + } + + // normalize energy by area + energy = energy / compute_total_area(); + + return energy; +} + +std::tuple MeshImprover::vertex_energy_direction(int vertex_index) const +{ + int vi = vertex_index; + int hij = halfedge.out[vi]; + int hik = hij; + double energy = 0.; + double total_area = 0.; + Eigen::Matrix2d H = Eigen::Matrix2d::Zero(); + Eigen::Vector2d gradient = Eigen::Vector2d::Zero(); + do + { + // get face uv vertices + Eigen::Matrix face_uv; + int f = halfedge.face[hik]; + + // need to use this halfedge for consistency + int h = halfedge.f2h[f]; + int local_vertex_index = -1; + for (int i = 0; i < 3; ++i) + { + face_uv.row(i) = halfedge_uv[h]; + + // look for index correponding to the central vertex + if (halfedge.to[h] == vi) + { + local_vertex_index = i; + } + + h = halfedge.n[h]; + } + + // compute face symdir energy + double area = compute_face_area(f); + Eigen::Matrix G = compute_face_tangent_gradient(f); + Eigen::Vector2d g = G.col(local_vertex_index); + Eigen::Matrix J_sq = G * face_uv; + Eigen::Matrix J = Eigen::Map>(J_sq.data(), 4); + Eigen::Matrix local_hessian; + Eigen::Matrix local_grad; + energy += area * symdir_weight * jakob::gradient_and_hessian_from_J(J, local_grad, local_hessian, norm_p); + total_area += area; + + // extrat gradient and hessian for face + for (int i = 0; i < 2; ++i) + { + gradient[i] += area * symdir_weight * g.dot(local_grad.segment(2 * i, 2)); + for (int j = 0; j < 2; ++j) + { + H(i, j) += area * symdir_weight * g.dot(local_hessian.block(2 * i, 2 * j, 2, 2) * g); + } + } + + // get alignment energy + if ((alignment_weight > 0) && (area > min_rel_area * avg_area)) + { + Eigen::Matrix grad = compute_face_gradient(f); + Eigen::Matrix Guv = grad * face_uv; + Eigen::Matrix uT_vT; + Eigen::Vector3d gi = grad.col(local_vertex_index); + uT_vT.col(0) = PD1_r[f]; + uT_vT.col(1) = PD2_r[f]; + Eigen::Matrix R = Guv - uT_vT; + energy += alignment_weight * area * (R.transpose() * R).trace(); + gradient += 2. * alignment_weight * area * (gi.transpose() * R); + for (int i = 0; i < 2; ++i) + { + H(i, i) += 2. * alignment_weight * area * gi.squaredNorm(); + } + } + + // iterate around vertex + hik = halfedge.n[halfedge.opp[hik]]; + } while (hik != hij); + + spdlog::debug("energy is {}", energy / total_area); + spdlog::debug("gradient is {}", gradient / total_area); + spdlog::debug("hessian is \n{}", H / total_area); + + Eigen::Vector2d d = -H.inverse() * gradient; + spdlog::debug("direction is {}", d); + + return std::make_tuple(energy, d); +} + +void MeshImprover::local_energy_opt(int vertex_index) +{ + // WARNING: assumes hji is in same face as local layout used for the direction computation + int vi = vertex_index; + int hji = halfedge.prev[halfedge.out[vi]]; + + // get search direction for mesh improvement + double orig_energy ; + Eigen::Vector2d d; + std::tie(orig_energy, d) = vertex_energy_direction(vi); + spdlog::debug("original energy for vertex {} is {}", vi, orig_energy); + spdlog::debug("displacing by {}", d); + + // try to move the full line step + Eigen::Vector2d p = halfedge_uv[hji]; + Eigen::Vector2d pn = p + d; + std::vector positions = compute_halfedge_tip_positions(halfedge, halfedge_uv, hji); + std::vector displacements = compute_halfedge_tip_displacements(halfedge, halfedge_uv, hji, pn); + apply_halfedge_tip_displacements(halfedge, halfedge_uv, hji, displacements); + double energy = compute_vertex_energy(vi); + spdlog::debug("new energy is {}", energy); + //assert(float_equal(energy, symmetric_dirichlet_energy(halfedge, halfedge_uv, V_r, vi))); + + // do backtracking line search to make sure no triangles are flipped + int count = 1; + scale_displacements(displacements, -1.); + while ((is_one_ring_flipped(halfedge, halfedge_uv, vi)) || (energy > orig_energy)) + { + if (count >= 5) + { + set_halfedge_tip_positions(halfedge, halfedge_uv, hji, positions); + break; + } + + spdlog::debug("backtracking attempt {}", count); + scale_displacements(displacements, 0.5); + apply_halfedge_tip_displacements(halfedge, halfedge_uv, hji, displacements); + + energy = compute_vertex_energy(vi); + spdlog::debug("energy is {}", energy); + + ++count; + } + + energy = compute_vertex_energy(vi); + spdlog::debug("final energy is {}\n", energy); +} + +void MeshImprover::flip_edge_ccw( + int halfedge_index +) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int hjk = halfedge.n[hij]; + int hil = halfedge.n[hji]; + + // make triangle flap connected before flipping + transform_opposite_face(halfedge_index); + + // update uv data + halfedge_uv[hij] = halfedge_uv[hjk]; + halfedge_uv[hji] = halfedge_uv[hil]; + + // do flip + // WARNING: must update connectivity after geometry + halfedge.flip_edge(halfedge_index); + + // set field to average + average_field_across_edge(PD1_r, PD2_r, V_r, halfedge, hij); +} + +void MeshImprover::flip_edge_clockwise( + int halfedge_index +) +{ + // make triangle flap connected before flipping + transform_opposite_face(halfedge_index); + + // update uv data + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int hjk = halfedge.n[hij]; + int hil = halfedge.n[hji]; + halfedge_uv[hij] = halfedge_uv[hil]; + halfedge_uv[hji] = halfedge_uv[hjk]; + + // do flip + // WARNING: must update connectivity after geometry + for (int i = 0; i < 3; ++i) + { + halfedge.flip_edge(halfedge_index); + } + + // set field to average + average_field_across_edge(PD1_r, PD2_r, V_r, halfedge, hij); +} + +// compute quality in collapse region +template +double compute_collapse_quality( + const HalfedgeMesh& halfedge, + const std::vector& positions, + int halfedge_index, + bool is_tip_index=true) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int fijk = halfedge.face[hij]; + int fjil = halfedge.face[hji]; + + double quality = 0.; + for (int h_start : {hij, hji}) + { + int h_curr = h_start; + do + { + int f = halfedge.face[h_curr]; + if ((f != fijk) && (f != fjil)) + { + quality += compute_triangle_quality(halfedge, positions, f, is_tip_index); + } + h_curr = halfedge.opp[halfedge.prev[h_curr]]; + } while (h_curr != h_start); + } + + return quality; +} + +// check if face is flipped in collapsed region +bool is_collapse_region_flipped( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int halfedge_index) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int fijk = halfedge.face[hij]; + int fjil = halfedge.face[hji]; + + for (int h_start : {hij, hji}) + { + int h_curr = h_start; + do + { + int f = halfedge.face[h_curr]; + if ((f != fijk) && (f != fjil)) + { + if (is_face_flipped(halfedge, uv, f)) return true; + } + h_curr = halfedge.opp[halfedge.prev[h_curr]]; + } while (h_curr != h_start); + } + + return false; +} + +bool MeshImprover::collapse_edge( + int halfedge_index +) +{ + int hij = halfedge_index; + int hki = halfedge.prev[hij]; + int vi = halfedge.to[hki]; + int vj = halfedge.to[hij]; + + // quality of region is sum of vertex regions, minus the overcounted edge flap + double orig_quality = compute_collapse_quality(halfedge, V_r, hij, false); + double orig_uv_quality = compute_collapse_quality(halfedge, halfedge_uv, hij, true); + double orig_total_quality = orig_quality + orig_uv_quality; + spdlog::debug("quality before collapse: {}", orig_quality); + spdlog::debug("uv quality before collapse: {}", orig_uv_quality); + + if (orig_quality > 1e10) + { + //view_vertex_adjacent_faces(halfedge, uv, vi, "vi", false); + //view_vertex_adjacent_faces(halfedge, uv, vj, "vj", true); + } + + // collapse location of edge endpoints + Eigen::Vector2d p = halfedge_uv[hki]; + std::vector positions = compute_halfedge_tip_positions(halfedge, halfedge_uv, hij); + std::vector displacements = compute_halfedge_tip_displacements(halfedge, halfedge_uv, hij, p); + apply_halfedge_tip_displacements(halfedge, halfedge_uv, halfedge_index, displacements); + + // collapse location of 3D position + Eigen::Vector3d pi = V_r[vi]; + Eigen::Vector3d pj = V_r[vj]; + //Eigen::Vector3d pn = (pi + pj) / 2.; + Eigen::Vector3d pn = pi; // collapse to pi + V_r[vi] = pn; + V_r[vj] = pn; + + // TODO: need to change vertex position + // get new quality in collapse region + double quality = compute_collapse_quality(halfedge, V_r, hij, false); + double uv_quality = compute_collapse_quality(halfedge, halfedge_uv, hij); + double total_quality = quality + uv_quality; + spdlog::debug("quality after setting position: {}", quality); + spdlog::debug("uv quality after setting position: {}", uv_quality); + + // check if element degenerates + bool success = true; + if (is_collapse_region_flipped(halfedge, halfedge_uv, hij)) + { + spdlog::debug("flipped element after collapse"); + success = false; + } + // TODO: replace uv quality with symmetric dirichlet + if (uv_quality > orig_uv_quality) + { + spdlog::debug("collapse reduces uv quality"); + success = false; + } + if (quality > orig_quality) + { + spdlog::debug("collapse reduces quality"); + success = false; + } + if (total_quality > orig_total_quality) + { + spdlog::debug("collapse reduces quality"); + success = false; + } + + // reset positions if degenerate and return without changing connectivity + if (!success) + { + spdlog::debug("aborting collapse"); + set_halfedge_tip_positions(halfedge, halfedge_uv, halfedge_index, positions); + V_r[vi] = pi; + V_r[vj] = pj; + + double final_uv_quality = compute_collapse_quality(halfedge, halfedge_uv, hij); + assert(float_equal(orig_uv_quality, final_uv_quality)); + + return false; + } + + // do collapse + // WARNING: must update connectivity after geometry + int hli = halfedge.collapse_edge(halfedge_index); + + // project directions in faces of collapsed vertex one ring + int h = hli; + do + { + int f = halfedge.face[h]; + project_direction_to_tangent(PD1_r, PD2_r, V_r, halfedge, f); + h = halfedge.opp[halfedge.n[h]]; + } while (h != hli); + + double final_uv_quality = compute_vertex_quality(halfedge, halfedge_uv, vi, true); + spdlog::debug("quality after collapse: {}", final_uv_quality); + + //assert(halfedge.is_valid()); + assert(float_equal(uv_quality, final_uv_quality, 1e-10 * quality)); + assert(!is_one_ring_flipped(halfedge, halfedge_uv, vi)); + assert(is_one_ring_seamless(halfedge, halfedge_uv, vi)); + + return true; +} + +void MeshImprover::split_edge( + int halfedge_index +) +{ + int hij = halfedge_index; + int hji = halfedge.opp[hij]; + int hjk = halfedge.n[hij]; + int hil = halfedge.n[hji]; + int vi = halfedge.to[hji]; + int vj = halfedge.to[hij]; + int num_init_halfedges = halfedge.n.size(); + int num_init_vertices = halfedge.out.size(); + int num_init_faces = halfedge.f2h.size(); + + // get initial geometry + Eigen::Vector2d uvi0 = halfedge_uv[halfedge.prev[hij]]; + Eigen::Vector2d uvj0 = halfedge_uv[hij]; + Eigen::Vector2d uvi1 = halfedge_uv[hji]; + Eigen::Vector2d uvj1 = halfedge_uv[halfedge.prev[hji]]; + Eigen::Vector2d uvk = halfedge_uv[hjk]; + Eigen::Vector2d uvl = halfedge_uv[hil]; + Eigen::Vector3d pd10 = PD1_r[halfedge.face[hij]]; + Eigen::Vector3d pd20 = PD2_r[halfedge.face[hij]]; + Eigen::Vector3d pd11 = PD1_r[halfedge.face[hji]]; + Eigen::Vector3d pd21 = PD2_r[halfedge.face[hji]]; + + // do flip and get refined quad connectivity + int hin = halfedge.split_triangle_mesh_edge(halfedge_index); + int hnk = halfedge.n[hin]; + int hkn = halfedge.opp[hnk]; + int hnj = halfedge.n[hkn]; + int hjn = halfedge.opp[hnj]; + int hnl = halfedge.n[hjn]; + int hln = halfedge.opp[hnl]; + int hni = halfedge.n[hln]; + + // add data for six halfedges, two new faces, and one new vertex + // WARNING: count depends on implementation, so using generic expansion method + for (int i = num_init_halfedges; i < halfedge.n.size(); ++i) + { + halfedge_uv.push_back({}); + } + for (int i = num_init_vertices; i < halfedge.out.size(); ++i) + { + V_r.push_back({}); + } + for (int i = num_init_faces; i < halfedge.f2h.size(); ++i) + { + PD1_r.push_back({}); + PD2_r.push_back({}); + } + + // update 3D geometry + int vn = halfedge.to[hin]; + V_r[vn] = 0.5 * (V_r[vi] + V_r[vj]); + + // halfedges and field in face 0 + Eigen::Vector2d uvn0 = 0.5 * (uvi0 + uvj0); + halfedge_uv[hin] = uvn0; + halfedge_uv[hnk] = uvk; + halfedge_uv[hkn] = uvn0; + halfedge_uv[hnj] = uvj0; + PD1_r[halfedge.face[hin]] = pd10; + PD2_r[halfedge.face[hin]] = pd20; + PD1_r[halfedge.face[hnj]] = pd10; + PD2_r[halfedge.face[hnj]] = pd20; + + // halfedges and field in face 1 + Eigen::Vector2d uvn1 = 0.5 * (uvi1 + uvj1); + halfedge_uv[hjn] = uvn1; + halfedge_uv[hnl] = uvl; + halfedge_uv[hln] = uvn1; + halfedge_uv[hni] = uvi1; + PD1_r[halfedge.face[hjn]] = pd11; + PD2_r[halfedge.face[hjn]] = pd21; + PD1_r[halfedge.face[hni]] = pd11; + PD2_r[halfedge.face[hni]] = pd21; +} + +Eigen::Matrix MeshImprover::compute_face_uv(int face_index) const +{ + Eigen::Matrix face_uv; + int h = halfedge.f2h[face_index]; + for (int i = 0; i < 3; ++i) + { + face_uv.row(i) = halfedge_uv[h]; + h = halfedge.n[h]; + } + + return face_uv; +} + +// experimental +Eigen::Vector2d laplacian_direction( + const HalfedgeMesh& halfedge, + const std::vector& uv, + int halfedge_index) +{ + Eigen::Vector2d total_diff = Eigen::Vector2d::Zero(); + int count = 0; + int hij = halfedge_index; + Eigen::Matrix2d R = Eigen::Matrix2d::Identity(); + do + { + int hji = halfedge.prev[hij]; + Eigen::Vector2d vj = uv[hij]; + Eigen::Vector2d vi = uv[hji]; + Eigen::Vector2d Rd = R * (vi - vj); + total_diff += Rd; + ++count; + + int hjk = halfedge.n[hij]; + R = R * infer_rotation(halfedge, uv, hjk); + hij = halfedge.opp[hjk]; + } while (hij != halfedge_index); + + return total_diff / count; +} + +void MeshImprover::smooth_vertex( + int vertex_index +) +{ + int vi = vertex_index; + + double orig_quality= compute_vertex_quality(halfedge, halfedge_uv, vi, true); + spdlog::debug("original quality is {}", orig_quality); + + // get search direction for mesh improvement + int hji = halfedge.opp[halfedge.out[vi]]; + Eigen::Vector2d d = laplacian_direction(halfedge, halfedge_uv, hji); + spdlog::debug("displacing by {}", d); + + // try to move the full line step + Eigen::Vector2d p = halfedge_uv[hji]; + Eigen::Vector2d pn = p + d; + set_halfedge_tip_position(halfedge, halfedge_uv, hji, pn); + double quality = compute_vertex_quality(halfedge, halfedge_uv, vi, true); + spdlog::debug("new quality is {}", compute_vertex_quality(halfedge, halfedge_uv, vi, true)); + + // do backtracking line search to make sure no triangles are flipped + int count = 1; + while ((is_one_ring_flipped(halfedge, halfedge_uv, vi)) || (quality > orig_quality)) + { + if (count >= 5) + { + set_halfedge_tip_position(halfedge, halfedge_uv, hji, p); + break; + } + + spdlog::debug("backtracking attempt {}", count); + d /= 2.; + pn = p + d; + set_halfedge_tip_position(halfedge, halfedge_uv, hji, pn); + + quality = compute_vertex_quality(halfedge, halfedge_uv, vi, true); + spdlog::debug("new quality is {}", compute_vertex_quality(halfedge, halfedge_uv, vi, true)); + + + if (count >= 5) + { + view_vertex_adjacent_faces(halfedge, rows_to_matrix(halfedge_uv), vi); + } + + ++count; + } + + quality = compute_vertex_quality(halfedge, halfedge_uv, vi, true); + spdlog::debug("final quality is {}\n", compute_vertex_quality(halfedge, halfedge_uv, vi, true)); + + if ((quality > 1e10) || (quality > orig_quality + 1e-8)) + { + transform_vertex_adjacent_faces(vi); + view_vertex_adjacent_faces(halfedge, rows_to_matrix(halfedge_uv), vi); + } + assert(quality <= orig_quality + 1e-8); +} + +// experimental +std::tuple symmetric_dirichlet_direction( + const HalfedgeMesh& halfedge, + const std::vector& uv, + const std::vector& V, + int vertex_index, + int norm_p=1) +{ + // build local patch + auto [V_l, F_l, uv_l] = build_local_chart(halfedge, uv, V, vertex_index); + + // get gradient from local patch + Eigen::SparseMatrix G; + SymDir::get_grad_op(V_l, F_l, G); + + Eigen::VectorXd area; + igl::doublearea(V_l, F_l, area); + + Eigen::VectorXd grad; + Eigen::SparseMatrix hessian; + bool get_hessian = true; + double energy = SymDir::get_grad_and_hessian(G, area, uv_l, grad, hessian, get_hessian, norm_p, false); + + int N = uv_l.rows(); + Eigen::Matrix2d H; + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < 2; ++j) + { + H(i, j) = hessian.coeff(N * i, N * j); + } + } + Eigen::Vector2d gradient = { grad(0), grad(N) }; + spdlog::debug("energy is {}", energy); + spdlog::debug("gradient is {}", gradient); + spdlog::debug("hessian is \n{}", H); + + Eigen::Vector2d d = -H.inverse() * gradient; + spdlog::debug("direction is {}", d); + + return std::make_tuple(energy, d); +} + +} +} + +#ifdef USE_UV_OPTIMIZATION +#endif diff --git a/src/resolution/pybind.cpp b/src/resolution/pybind.cpp new file mode 100644 index 0000000..c6e8420 --- /dev/null +++ b/src/resolution/pybind.cpp @@ -0,0 +1,31 @@ +#include "resolution/pybind.h" +#include "resolution/core/quad_quality.h" +#include "resolution/optimization/improvement.h" + +namespace Penner { +namespace Resolution { + +#ifdef PYBIND +#ifndef MULTIPRECISION + +void init_resolution_pybind(pybind11::module& m) +{ + spdlog::set_level(spdlog::level::info); + pybind11::call_guard + default_call_guard; + + m.def("compute_quad_lengths", &compute_quad_lengths, default_call_guard); + m.def("compute_quad_angles", &compute_quad_angles, default_call_guard); + m.def("compute_scaled_jacobian", &compute_scaled_jacobian, default_call_guard); + +#ifdef USE_UV_OPTIMIZATION + m.def("symmetric_dirichlet", &symmetric_dirichlet, default_call_guard); +#endif +} + + +#endif +#endif + +} // namespace Resolution +} // namespace Penner From 957e437626a924ce268847000c1ed5039f9e21eb Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Wed, 4 Nov 2026 16:15:30 +0100 Subject: [PATCH 16/17] Cleaned up dependencies, and added the option to only build some penner modules --- CMakeLists.txt | 31 ++++----- README.md | 3 + app/CMakeLists.txt | 111 +++++++++++++------------------- environment.yml | 2 +- src/CMakeLists.txt | 55 ++++++++++++---- src/feature/CMakeLists.txt | 17 ----- src/holonomy/CMakeLists.txt | 1 - src/optimization/CMakeLists.txt | 1 - src/optimization/pybind.cpp | 7 +- src/pybind.cpp | 36 +++++++++-- src/resolution/CMakeLists.txt | 1 - 11 files changed, 133 insertions(+), 132 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b14c8a4..4097ce3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,10 @@ project(PennerOptimization) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_options(-fPIC) -add_compile_options(-Wno-register) + +# fix deprecated register usage in dependencies +add_compile_options($<$:-Wno-register>) + set(DIR_EXT "${CMAKE_CURRENT_SOURCE_DIR}/ext") list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) @@ -24,7 +27,6 @@ if (PROJECT_IS_TOP_LEVEL) # Visualization options option(ENABLE_VISUALIZATION "Generate viewers for visualization" ON) - option(RENDER_TEXTURE "Render results" ON) # Validity check options option(BUILD_CURVATURE_METRIC_TESTS "Build tests" ON) @@ -37,6 +39,13 @@ if (PROJECT_IS_TOP_LEVEL) option ( SUITESPARSE_USE_CUDA OFF ) endif() +# set Penner libraries to build +set(PENNER_LIBS + "Field;Optimization;Holonomy;Feature" + CACHE STRING + "Libraries exposed through PennerLib; dependencies are added automatically" +) + list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # add needed suitesparse projects @@ -121,12 +130,6 @@ if (USE_COMISO) ) endif() -# Set libigl options for rendering if enabled -if (RENDER_TEXTURE) - option(LIBIGL_OPENGL "Use OpenGL" ON) - option(LIBIGL_GLFW "Use GLFW" ON) - option(LIBIGL_PNG "Use PNG" ON) -endif() # Set compile definitions add_compile_definitions(_USE_MATH_DEFINES) @@ -136,9 +139,6 @@ endif() if(CHECK_VALIDITY) add_compile_definitions(CHECK_VALIDITY) endif() -if (RENDER_TEXTURE) - add_compile_definitions(RENDER_TEXTURE) -endif() if(USE_PYBIND) add_compile_definitions(PYBIND) @@ -186,15 +186,6 @@ target_include_directories(DirectionalLib SYSTEM INTERFACE ) target_compile_features(DirectionalLib INTERFACE cxx_std_20) - -# Optionally create rendering library -if(RENDER_TEXTURE) - set(RENDER_LIBRARIES - igl::glfw - plot - ) -endif() - # Optionally enable polyscope visualization if(ENABLE_VISUALIZATION) add_compile_definitions(ENABLE_VISUALIZATION) diff --git a/README.md b/README.md index d12b4e6..53a8612 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ To install this project on a Unix-based system, use the following standard CMake ```bash git clone https://github.com/geometryprocessing/penner-optimization.git +conda env create -f environment.yml && conda activate penner cd penner-optimization mkdir build cd build @@ -39,6 +40,8 @@ cmake -DCMAKE_BUILD_TYPE=Release .. make -j 4 ``` +Note: the conda environment is needed for HDF5 C++ headers. + ### Library Structure - Metric: Underlying methods for differentiable intrinsic metrics, including (a) area and angle values, and (b) length, Penner, shear, and conformal coordinates. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5f90318..b9f63ad 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -4,56 +4,41 @@ target_link_libraries(ApplicationUtilLib INTERFACE CLI11::CLI11 ) -add_executable(generate_field -generate_field.cpp -) -target_link_libraries(generate_field PRIVATE - PennerLib - ApplicationUtilLib -) - -add_executable(generate_aligned_field -generate_aligned_field.cpp -) -target_link_libraries(generate_aligned_field PRIVATE - PennerLib - ApplicationUtilLib -) - -add_executable(parametrize_seamless - parametrize_seamless.cpp -) -target_link_libraries(parametrize_seamless PRIVATE - PennerLib - SymmetricDirichletLib - CLI11::CLI11 -) - -add_executable(parameterize_aligned -parameterize_aligned.cpp -) -target_link_libraries(parameterize_aligned PRIVATE - PennerLib - SymmetricDirichletLib - ApplicationUtilLib -) - -add_executable(test_interface -test_interface.cpp -) -target_link_libraries(test_interface PRIVATE - PennerLib - SymmetricDirichletLib - ApplicationUtilLib -) - -add_executable(optimize_metric - optimize_metric.cpp -) -target_link_libraries(optimize_metric PRIVATE - PennerLib - CLI11::CLI11 -) +function(penner_add_app name) + cmake_parse_arguments(ARG "" "" "SOURCES;REQUIRES;LIBS" ${ARGN}) + foreach(dep IN LISTS ARG_REQUIRES) + if(NOT TARGET Penner${dep}Lib) + message(STATUS "Skipping ${name}: needs Penner${dep}Lib") + return() + endif() + endforeach() + add_executable(${name} ${ARG_SOURCES}) + target_link_libraries(${name} PRIVATE PennerLib ApplicationUtilLib ${ARG_LIBS}) +endfunction() + +penner_add_app(generate_field + SOURCES generate_field.cpp + REQUIRES Field) + +penner_add_app(optimize_metric + SOURCES optimize_metric.cpp + REQUIRES Field Optimization) + +penner_add_app(parametrize_seamless + SOURCES parametrize_seamless.cpp + REQUIRES Holonomy) + +penner_add_app(generate_aligned_field + SOURCES generate_aligned_field.cpp + REQUIRES Feature) + +penner_add_app(parameterize_aligned + SOURCES parameterize_aligned.cpp + REQUIRES Feature) + +penner_add_app(test_interface + SOURCES test_interface.cpp + REQUIRES Feature) # add_executable(optimize_shear # optimize_shear.cpp @@ -64,22 +49,12 @@ target_link_libraries(optimize_metric PRIVATE # ) # only build visualization executables if visualization enabled -if (NOT USE_MULTIPRECISION) -if (ENABLE_VISUALIZATION) - add_executable(view_quad_mesh - view_quad_mesh.cpp - ) - target_link_libraries(view_quad_mesh PRIVATE - PennerLib - ApplicationUtilLib - ) - - add_executable(view_seamless - view_seamless.cpp - ) - target_link_libraries(view_seamless PRIVATE - PennerLib - ApplicationUtilLib - ) -endif() +if (NOT USE_MULTIPRECISION AND ENABLE_VISUALIZATION) + penner_add_app(view_quad_mesh + SOURCES view_quad_mesh.cpp + REQUIRES Feature) + + penner_add_app(view_seamless + SOURCES view_seamless.cpp + REQUIRES Feature) endif() diff --git a/environment.yml b/environment.yml index d8cc0ff..0696010 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: penner-optimization +name: penner channels: - conda-forge - defaults diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9649d3a..cfbeaff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -68,19 +68,44 @@ if ((NOT USE_MULTIPRECISION) AND (DEBUG_WARNINGS)) ) endif() -add_subdirectory(field) -add_subdirectory(optimization) -add_subdirectory(holonomy) -add_subdirectory(feature) -add_subdirectory(resolution) +# Must stay in dependency order +set(PENNER_ALL_LIBS Field Optimization Holonomy Feature Resolution) +set(PENNER_Holonomy_DEPS Field Optimization) +set(PENNER_Feature_DEPS Field Optimization Holonomy) +set(PENNER_Resolution_DEPS Field Optimization Holonomy Feature) + +foreach(lib IN LISTS PENNER_LIBS) + if(NOT lib IN_LIST PENNER_ALL_LIBS) + message(FATAL_ERROR "Unknown entry '${lib}' in PENNER_LIBS. Valid: ${PENNER_ALL_LIBS}") + endif() +endforeach() + +# Add dependencies: walking in reverse order means one pass is enough +set(PENNER_ENABLED_LIBS ${PENNER_LIBS}) +set(_reverse ${PENNER_ALL_LIBS}) +list(REVERSE _reverse) +foreach(lib IN LISTS _reverse) + if(lib IN_LIST PENNER_ENABLED_LIBS) + list(APPEND PENNER_ENABLED_LIBS ${PENNER_${lib}_DEPS}) + endif() +endforeach() +list(REMOVE_DUPLICATES PENNER_ENABLED_LIBS) +message(STATUS "Penner libraries: requested [${PENNER_LIBS}], building [${PENNER_ENABLED_LIBS}]") add_library(PennerLib INTERFACE) -target_link_libraries(PennerLib INTERFACE - PennerFieldLib - PennerOptimizationLib - PennerHolonomyLib - PennerFeatureLib -) +target_link_libraries(PennerLib INTERFACE PennerCoreLib) + +foreach(lib IN LISTS PENNER_ALL_LIBS) + if(lib IN_LIST PENNER_ENABLED_LIBS) + string(TOLOWER ${lib} dir) + string(TOUPPER ${lib} upper) + add_subdirectory(${dir}) + target_compile_definitions(PennerLib INTERFACE PENNER_WITH_${upper}) + if(lib IN_LIST PENNER_LIBS) + target_link_libraries(PennerLib INTERFACE Penner${lib}Lib) + endif() + endif() +endforeach() if(USE_PYBIND) add_library(penner MODULE @@ -92,6 +117,14 @@ if(USE_PYBIND) PennerLib ) + # set sources for pybind + foreach(lib IN LISTS PENNER_ENABLED_LIBS) + string(TOLOWER ${lib} dir) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/pybind.cpp) + target_sources(penner PRIVATE ${dir}/pybind.cpp) + endif() + endforeach() + # Set pybinding settings set_target_properties(penner PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/py diff --git a/src/feature/CMakeLists.txt b/src/feature/CMakeLists.txt index a94f597..83cc497 100644 --- a/src/feature/CMakeLists.txt +++ b/src/feature/CMakeLists.txt @@ -31,7 +31,6 @@ set(SurgerySrc add_library(PennerFeatureLib interface.cpp - pybind.cpp ${FeatureCoreSrc} ${DirichletSrc} ${FeatureSrc} @@ -59,20 +58,4 @@ if ((NOT USE_MULTIPRECISION) AND (USE_WARNINGS)) ) endif() -if(USE_PYBIND) - add_library(feature_py MODULE - pybind.cpp - ) - - # Link libraries - target_link_libraries(feature_py PUBLIC - PennerFeatureLib - pybind11::module - ) - - # Set pybinding settings - set_target_properties(feature_py PROPERTIES PREFIX - "${PYTHON_MODULE_PREFIX}" - ) -endif() diff --git a/src/holonomy/CMakeLists.txt b/src/holonomy/CMakeLists.txt index 09dd327..4dccaa0 100644 --- a/src/holonomy/CMakeLists.txt +++ b/src/holonomy/CMakeLists.txt @@ -25,7 +25,6 @@ set(SimilaritySrc add_library(PennerHolonomyLib interface.cpp - pybind.cpp ${HolonomyCoreSrc} ${HolonomySrc} ${SimilaritySrc} diff --git a/src/optimization/CMakeLists.txt b/src/optimization/CMakeLists.txt index 8e64749..fb6f23c 100644 --- a/src/optimization/CMakeLists.txt +++ b/src/optimization/CMakeLists.txt @@ -20,7 +20,6 @@ endif() add_library(PennerOptimizationLib interface.cpp - pybind.cpp ${MetricOptimizationSrc} ${OptimizationUtilSrc} ) diff --git a/src/optimization/pybind.cpp b/src/optimization/pybind.cpp index 00c2d7d..b9faa34 100644 --- a/src/optimization/pybind.cpp +++ b/src/optimization/pybind.cpp @@ -32,10 +32,8 @@ #include #endif -#ifdef RENDER_TEXTURE #include "conformal_ideal_delaunay/ConformalInterface.hh" #include "conformal_ideal_delaunay/Sampling.hh" -#endif namespace Penner { namespace Optimization { @@ -208,11 +206,8 @@ void init_conformal_pybind(pybind11::module& m) &overlay_mesh_to_VL, pybind11::call_guard()); -#ifdef RENDER_TEXTURE - m.def("cpp_viewer", &cpp_viewer, "viewer mesh in libigl gui"); m.def("get_pt_mat", &get_pt_mat, "get pt_mat"); m.def("get_edges", &get_edges, "get edges mesh"); -#endif } void init_energies_pybind(pybind11::module& m) @@ -461,4 +456,4 @@ void init_optimization_pybind(pybind11::module& m) #endif } // namespace Optimization -} // namespace Penner \ No newline at end of file +} // namespace Penner diff --git a/src/pybind.cpp b/src/pybind.cpp index 86d2b1a..6642370 100644 --- a/src/pybind.cpp +++ b/src/pybind.cpp @@ -11,19 +11,30 @@ #include "metric/viewer.h" #include "parametrization/parametrize.h" -#include "optimization/pybind.h" -#include "holonomy/pybind.h" -#include "holonomy/core/viewer.h" -#include "feature/pybind.h" +using namespace Penner; +#ifdef PENNER_WITH_FIELD #include "field/frame_field.h" #include "field/cross_field.h" +using namespace Field; +#endif -using namespace Penner; +#ifdef PENNER_WITH_OPTIMIZATION +#include "optimization/pybind.h" using namespace Optimization; +#endif + +#ifdef PENNER_WITH_HOLONOMY +#include "holonomy/pybind.h" +#include "holonomy/core/viewer.h" using namespace Holonomy; +#endif + +#ifdef PENNER_WITH_FEATURE +#include "feature/pybind.h" using namespace Feature; -using namespace Field; +#endif + #ifdef PYBIND #ifndef MULTIPRECISION @@ -130,6 +141,8 @@ void init_metric_pybind(pybind11::module& m) m.def("view_parameterization", &view_parameterization, default_call_guard); } +// TODO: move to field module +#ifdef PENNER_WITH_FIELD void init_field_pybind(pybind11::module& m) { spdlog::set_level(spdlog::level::info); @@ -143,16 +156,27 @@ void init_field_pybind(pybind11::module& m) m.def("compute_frame_field_cones", &compute_frame_field_cones, default_call_guard); m.def("refine_frame_field", &refine_frame_field, default_call_guard); } +#endif // wrap as Python module PYBIND11_MODULE(penner, m) { m.doc() = "pybindings for penner methods"; init_metric_pybind(m); + +#ifdef PENNER_WITH_FIELD init_field_pybind(m); +#endif +#ifdef PENNER_WITH_OPTIMIZATION init_optimization_pybind(m); +#endif +#ifdef PENNER_WITH_HOLONOMY init_holonomy_pybind(m); +#endif +#ifdef PENNER_WITH_FEATURE init_feature_pybind(m); +#endif + } #endif diff --git a/src/resolution/CMakeLists.txt b/src/resolution/CMakeLists.txt index 0f87929..f023498 100644 --- a/src/resolution/CMakeLists.txt +++ b/src/resolution/CMakeLists.txt @@ -39,7 +39,6 @@ set(ExperimentalSrc ) add_library(PennerResolutionLib - pybind.cpp interface.cpp ${ResolutionCoreSrc} ${HierarchySrc} From 71e5c6daaa2d4aca89a6931cdbca25b82696c0eb Mon Sep 17 00:00:00 2001 From: Ryan Capouellez Date: Wed, 4 Nov 2026 16:24:50 +0100 Subject: [PATCH 17/17] Set some CMake defaults in all builds and not just top level build --- CMakeLists.txt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4097ce3..2869516 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,13 +18,9 @@ if (PROJECT_IS_TOP_LEVEL) # Options to generate python bindings option(USE_PYBIND "Generate pybindings" ON) - # Alternative library options + # Use multiprecision instead of double precision, at high performance cost option(USE_MULTIPRECISION "Use high precision floating point" OFF) - option(USE_SUITESPARSE "Use suite sparse methods for matrix inversion" ON) - option(USE_COMISO "Use Comiso for field generation" ON) - option(USE_UV_OPTIMIZATION "Add uv optimization library" ON) - # Visualization options option(ENABLE_VISUALIZATION "Generate viewers for visualization" ON) @@ -32,13 +28,16 @@ if (PROJECT_IS_TOP_LEVEL) option(BUILD_CURVATURE_METRIC_TESTS "Build tests" ON) option(CHECK_VALIDITY "Check validity pre and post conditions" ON) - # Set default libigl and suitesparse options - option(LIBIGL_PREDICATES "Use Predicates" ON) #set ( SUITESPARSE_ENABLE_PROJECTS "suitesparse_config;cholmod;spqr" ) option ( SUITESPARSE_USE_CUDA OFF ) endif() +option(USE_SUITESPARSE "Use suite sparse methods for matrix inversion" ON) +option(USE_COMISO "Use Comiso for field generation" ON) +option(USE_UV_OPTIMIZATION "Add uv optimization library" ON) +option(LIBIGL_PREDICATES "Use Predicates" ON) + # set Penner libraries to build set(PENNER_LIBS "Field;Optimization;Holonomy;Feature"